Skip to content

Commit fffc371

Browse files
committed
onboarding: add daemon lifecycle flow and align docs
1 parent a15b84e commit fffc371

12 files changed

Lines changed: 942 additions & 23 deletions

File tree

docs/operations-admin/maintenance.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,14 @@ Guided alternative:
7373

7474
## Service Operation (Linux systemd)
7575

76-
If installed as system service:
76+
Preferred (CLI-managed):
77+
78+
```bash
79+
sudo ./kafclaw daemon status
80+
sudo ./kafclaw daemon restart
81+
```
82+
83+
Direct `systemctl` fallback:
7784

7885
```bash
7986
sudo systemctl daemon-reload

docs/operations-admin/manage-kafclaw.md

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@ Operator-focused guide for managing KafClaw from CLI and runtime endpoints.
1818
| `kafclaw security` | Unified security checks/audit/fix (`check`, `audit --deep`, `fix --yes`) |
1919
| `kafclaw config` | Low-level dotted-path config read/write/unset |
2020
| `kafclaw configure` | Guided/non-interactive config updates (subagents, skills, Kafka group security) |
21-
| `kafclaw skills` | Skills lifecycle (`enable/disable/list/status/verify/install/update/auth/prereq`) |
21+
| `kafclaw skills` | Skills lifecycle (`enable/disable/list/status/enable-skill/disable-skill/verify/install/update/exec/auth/prereq`) |
2222
| `kafclaw group` | Join/leave/status/members for Kafka collaboration group |
2323
| `kafclaw kshark` | Kafka connectivity and protocol diagnostics |
2424
| `kafclaw agent -m` | Single-shot direct CLI interaction with agent loop |
2525
| `kafclaw pairing` | Approve/deny pending Slack/Teams sender pairings |
2626
| `kafclaw whatsapp-setup` | Configure WhatsApp auth and initial lists |
2727
| `kafclaw whatsapp-auth` | Approve/deny/list WhatsApp JIDs |
2828
| `kafclaw install` | Install local binary (`/usr/local/bin` as root, `~/.local/bin` as non-root) |
29+
| `kafclaw daemon` | Manage systemd service lifecycle (`install`, `uninstall`, `start`, `stop`, `restart`, `status`) |
2930
| `kafclaw update` | Update lifecycle (`plan`, `apply`, `backup`, `rollback`) |
3031
| `kafclaw completion` | Generate shell completion scripts (`bash|zsh|fish|powershell`) |
3132
| `kafclaw version` | Print build version |
@@ -191,6 +192,41 @@ Onboarding also scaffolds workspace files:
191192

192193
Use `--force` to overwrite existing config and scaffold files.
193194

195+
Lifecycle flags (operator-focused):
196+
197+
```bash
198+
./kafclaw onboard --reset-scope config --non-interactive --accept-risk --profile local --llm skip
199+
./kafclaw onboard --wait-for-gateway --health-timeout 20s
200+
./kafclaw onboard --skip-healthcheck
201+
./kafclaw onboard --daemon-runtime native
202+
```
203+
204+
If onboarding installs systemd (`--systemd`), service activation is automatic by default.
205+
Disable auto-activation with `--systemd-activate=false`.
206+
207+
## 4.1 Daemon / Service Lifecycle (Linux systemd)
208+
209+
Install service and activate immediately:
210+
211+
```bash
212+
sudo ./kafclaw daemon install --activate
213+
```
214+
215+
Service operations:
216+
217+
```bash
218+
sudo ./kafclaw daemon status
219+
sudo ./kafclaw daemon restart
220+
sudo ./kafclaw daemon stop
221+
sudo ./kafclaw daemon start
222+
```
223+
224+
Uninstall service:
225+
226+
```bash
227+
sudo ./kafclaw daemon uninstall
228+
```
229+
194230
## 5. Daily Health Checks
195231

196232
### Status snapshot

docs/reference/cli-reference.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,19 @@ Primary command groups:
1010
- `kafclaw gateway` - start API + dashboard + runtime services
1111
- `kafclaw status` - runtime/config health snapshot
1212
- `kafclaw doctor` - diagnostics and setup checks
13+
- `kafclaw security` - security checks, deep audit, and safe remediation (`check|audit|fix`)
1314
- `kafclaw config` / `kafclaw configure` - low-level and guided config changes
1415
- `kafclaw agent -m` - one-shot interaction
16+
- `kafclaw skills` - bundled/external skill lifecycle and auth/prereq flows (`enable|disable|list|status|enable-skill|disable-skill|verify|install|update|exec|prereq|auth`)
1517
- `kafclaw install` - install local built binary (`/usr/local/bin` root, `~/.local/bin` non-root)
1618
- `kafclaw update` - update lifecycle (`plan`, `apply`, `backup`, `rollback`)
19+
- `kafclaw daemon` - system service lifecycle (`install`, `uninstall`, `start`, `stop`, `restart`, `status`)
1720
- `kafclaw completion` - generate shell completion scripts
1821
- `kafclaw whatsapp-setup` / `kafclaw whatsapp-auth` - WhatsApp setup and auth controls
1922
- `kafclaw pairing` - Slack/Teams pairing approvals
2023
- `kafclaw group` - group collaboration controls
2124
- `kafclaw kshark` - Kafka diagnostics
25+
- `kafclaw version` - print build version
2226

2327
Automation-friendly lifecycle output:
2428
- `kafclaw onboard --json`
@@ -27,8 +31,12 @@ Automation-friendly lifecycle output:
2731
- `kafclaw doctor --json`
2832
- `kafclaw security <check|audit|fix> --json`
2933
- `kafclaw update <plan|backup|apply|rollback> --json`
34+
- `kafclaw daemon <install|uninstall|start|stop|restart|status> --json`
3035

3136
Detailed command examples:
3237
- [Getting Started](../start-here/getting-started/)
3338
- [User Manual - CLI Reference section](../start-here/user-manual/#3-cli-reference)
3439
- [Manage KafClaw](../operations-admin/manage-kafclaw/)
40+
41+
Skills execution example:
42+
- `kafclaw skills exec <skill-id> --input '{"text":"..."}'`

docs/start-here/getting-started.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,13 @@ To reconfigure provider/token later, run onboarding again (interactive) or use:
204204
- Use `--force` to overwrite existing config and identity templates
205205
- Gateway also auto-scaffolds missing identity files at startup if workspace is incomplete
206206

207+
Minimal lifecycle flags:
208+
209+
```bash
210+
./kafclaw onboard --reset-scope config --non-interactive --accept-risk --profile local --llm skip
211+
./kafclaw onboard --wait-for-gateway --health-timeout 20s
212+
```
213+
207214
## 5. Verify
208215

209216
```bash
@@ -248,6 +255,13 @@ sudo ./kafclaw onboard --systemd
248255

249256
This can create service user, install unit files, and write runtime env file.
250257

258+
After onboarding, manage service state with:
259+
260+
```bash
261+
sudo ./kafclaw daemon status
262+
sudo ./kafclaw daemon restart
263+
```
264+
251265
## 8. Where Config Lives
252266

253267
- Main config: `~/.kafclaw/config.json`

docs/start-here/user-manual.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ Once the gateway is running:
128128
## 3. CLI Reference
129129

130130
KafClaw provides the following CLI commands. Run `kafclaw --help` for the full list.
131-
Core startup commands: `onboard`, `doctor`, `status`, `gateway`, `agent`, `config`.
131+
Core startup commands: `onboard`, `doctor`, `status`, `gateway`, `daemon`, `agent`, `config`.
132132

133133
### 3.1 `gateway`
134134

@@ -174,9 +174,21 @@ Common onboarding profiles:
174174

175175
Useful onboarding flags:
176176
- `--systemd` to install service/override/env (Linux)
177+
- `--reset-scope` (`none|config|full`) for deterministic reset behavior
178+
- `--wait-for-gateway` and `--health-timeout` for post-onboard health gating
179+
- `--skip-healthcheck` to bypass readiness checks in constrained automation
180+
- `--daemon-runtime` to persist daemon runtime label in config
177181
- `--subagents-max-spawn-depth`, `--subagents-max-children`, `--subagents-max-concurrent`
178182
- `--subagents-archive-minutes`, `--subagents-model`, `--subagents-thinking`
179183

184+
Service lifecycle commands:
185+
186+
```bash
187+
sudo kafclaw daemon install --activate
188+
sudo kafclaw daemon status
189+
sudo kafclaw daemon restart
190+
```
191+
180192
Subagent runtime notes:
181193
- `sessions_spawn` accepts `runTimeoutSeconds` for per-run hard timeout
182194
- `subagents` supports selectors (`last`, numeric index, runId prefix, label prefix, child session key)

internal/cli/daemon.go

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
package cli
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
"os/exec"
8+
"path/filepath"
9+
"runtime"
10+
"strings"
11+
12+
"github.com/KafClaw/KafClaw/internal/config"
13+
"github.com/KafClaw/KafClaw/internal/onboarding"
14+
"github.com/spf13/cobra"
15+
)
16+
17+
var daemonCmd = &cobra.Command{
18+
Use: "daemon",
19+
Short: "Manage gateway daemon service lifecycle",
20+
}
21+
22+
var daemonInstallCmd = &cobra.Command{
23+
Use: "install",
24+
Short: "Install and optionally activate systemd service (Linux)",
25+
RunE: runDaemonInstall,
26+
}
27+
28+
var daemonUninstallCmd = &cobra.Command{
29+
Use: "uninstall",
30+
Short: "Disable and remove systemd service (Linux)",
31+
RunE: runDaemonUninstall,
32+
}
33+
34+
var daemonStartCmd = &cobra.Command{
35+
Use: "start",
36+
Short: "Start systemd service (Linux)",
37+
RunE: runDaemonStart,
38+
}
39+
40+
var daemonStopCmd = &cobra.Command{
41+
Use: "stop",
42+
Short: "Stop systemd service (Linux)",
43+
RunE: runDaemonStop,
44+
}
45+
46+
var daemonRestartCmd = &cobra.Command{
47+
Use: "restart",
48+
Short: "Restart systemd service (Linux)",
49+
RunE: runDaemonRestart,
50+
}
51+
52+
var daemonStatusCmd = &cobra.Command{
53+
Use: "status",
54+
Short: "Show systemd service status (Linux)",
55+
RunE: runDaemonStatus,
56+
}
57+
58+
var daemonServiceUser string
59+
var daemonServiceBinary string
60+
var daemonServicePort int
61+
var daemonInstallRoot string
62+
var daemonRuntimeLabel string
63+
var daemonActivate bool
64+
var daemonJSON bool
65+
66+
var daemonExecFn = func(name string, args ...string) ([]byte, error) {
67+
return exec.Command(name, args...).CombinedOutput()
68+
}
69+
var daemonOS = runtime.GOOS
70+
var daemonCurrentEUID = os.Geteuid
71+
var daemonSetupSystemdFn = onboarding.SetupSystemdGateway
72+
var daemonActivateSystemdFn = onboarding.ActivateSystemdGateway
73+
74+
func init() {
75+
daemonInstallCmd.Flags().StringVar(&daemonServiceUser, "service-user", "kafclaw", "Service user for systemd unit")
76+
daemonInstallCmd.Flags().StringVar(&daemonServiceBinary, "service-binary", "/usr/local/bin/kafclaw", "kafclaw binary path in ExecStart")
77+
daemonInstallCmd.Flags().IntVar(&daemonServicePort, "service-port", 18790, "Gateway port for systemd ExecStart")
78+
daemonInstallCmd.Flags().StringVar(&daemonInstallRoot, "service-install-root", "/", "Root path for systemd files (testing/packaging)")
79+
daemonInstallCmd.Flags().StringVar(&daemonRuntimeLabel, "daemon-runtime", "native", "Daemon runtime label persisted in config")
80+
daemonInstallCmd.Flags().BoolVar(&daemonActivate, "activate", true, "Run daemon-reload and enable --now after install")
81+
daemonInstallCmd.Flags().BoolVar(&daemonJSON, "json", false, "Output machine-readable JSON")
82+
_ = daemonInstallCmd.Flags().MarkHidden("service-install-root")
83+
84+
daemonUninstallCmd.Flags().BoolVar(&daemonJSON, "json", false, "Output machine-readable JSON")
85+
daemonStartCmd.Flags().BoolVar(&daemonJSON, "json", false, "Output machine-readable JSON")
86+
daemonStopCmd.Flags().BoolVar(&daemonJSON, "json", false, "Output machine-readable JSON")
87+
daemonRestartCmd.Flags().BoolVar(&daemonJSON, "json", false, "Output machine-readable JSON")
88+
daemonStatusCmd.Flags().BoolVar(&daemonJSON, "json", false, "Output machine-readable JSON")
89+
90+
daemonCmd.AddCommand(daemonInstallCmd, daemonUninstallCmd, daemonStartCmd, daemonStopCmd, daemonRestartCmd, daemonStatusCmd)
91+
rootCmd.AddCommand(daemonCmd)
92+
}
93+
94+
func runDaemonInstall(cmd *cobra.Command, args []string) error {
95+
if daemonOS != "linux" {
96+
return daemonResult(cmd, "error", "install", map[string]any{"os": daemonOS}, "daemon install currently supports Linux systemd only")
97+
}
98+
cfg, err := config.Load()
99+
if err != nil {
100+
return daemonResult(cmd, "error", "install", nil, fmt.Sprintf("load config: %v", err))
101+
}
102+
if daemonServicePort <= 0 {
103+
if cfg.Gateway.Port > 0 {
104+
daemonServicePort = cfg.Gateway.Port
105+
} else {
106+
daemonServicePort = 18790
107+
}
108+
}
109+
cfg.Gateway.DaemonRuntime = strings.TrimSpace(daemonRuntimeLabel)
110+
if cfg.Gateway.DaemonRuntime == "" {
111+
cfg.Gateway.DaemonRuntime = "native"
112+
}
113+
if err := config.Save(cfg); err != nil {
114+
return daemonResult(cmd, "error", "install", nil, fmt.Sprintf("save config: %v", err))
115+
}
116+
117+
result, err := daemonSetupSystemdFn(onboarding.SetupOptions{
118+
ServiceUser: daemonServiceUser,
119+
BinaryPath: daemonServiceBinary,
120+
Port: daemonServicePort,
121+
Profile: "default",
122+
Version: version,
123+
InstallRoot: daemonInstallRoot,
124+
})
125+
if err != nil {
126+
return daemonResult(cmd, "error", "install", nil, err.Error())
127+
}
128+
if daemonActivate {
129+
if daemonCurrentEUID() != 0 {
130+
return daemonResult(cmd, "error", "install", map[string]any{"servicePath": result.ServicePath}, "activation requires root privileges")
131+
}
132+
if err := daemonActivateSystemdFn(); err != nil {
133+
return daemonResult(cmd, "error", "install", map[string]any{"servicePath": result.ServicePath}, err.Error())
134+
}
135+
}
136+
return daemonResult(cmd, "ok", "install", map[string]any{
137+
"servicePath": result.ServicePath,
138+
"overridePath": result.OverridePath,
139+
"envPath": result.EnvPath,
140+
"userCreated": result.UserCreated,
141+
"activated": daemonActivate,
142+
"runtime": cfg.Gateway.DaemonRuntime,
143+
}, "")
144+
}
145+
146+
func runDaemonUninstall(cmd *cobra.Command, args []string) error {
147+
if daemonOS != "linux" {
148+
return daemonResult(cmd, "error", "uninstall", map[string]any{"os": daemonOS}, "daemon uninstall currently supports Linux systemd only")
149+
}
150+
if daemonCurrentEUID() != 0 {
151+
return daemonResult(cmd, "error", "uninstall", nil, "uninstall requires root privileges")
152+
}
153+
_, _ = daemonExecFn("systemctl", "disable", "--now", "kafclaw-gateway.service")
154+
_ = os.Remove(filepath.Join("/", "etc", "systemd", "system", "kafclaw-gateway.service"))
155+
_, _ = daemonExecFn("systemctl", "daemon-reload")
156+
return daemonResult(cmd, "ok", "uninstall", nil, "")
157+
}
158+
159+
func runDaemonStart(cmd *cobra.Command, args []string) error {
160+
return runDaemonSystemctlAction(cmd, "start")
161+
}
162+
163+
func runDaemonStop(cmd *cobra.Command, args []string) error {
164+
return runDaemonSystemctlAction(cmd, "stop")
165+
}
166+
167+
func runDaemonRestart(cmd *cobra.Command, args []string) error {
168+
return runDaemonSystemctlAction(cmd, "restart")
169+
}
170+
171+
func runDaemonSystemctlAction(cmd *cobra.Command, action string) error {
172+
if daemonOS != "linux" {
173+
return daemonResult(cmd, "error", action, map[string]any{"os": daemonOS}, "daemon actions currently support Linux systemd only")
174+
}
175+
if daemonCurrentEUID() != 0 {
176+
return daemonResult(cmd, "error", action, nil, fmt.Sprintf("%s requires root privileges", action))
177+
}
178+
out, err := daemonExecFn("systemctl", action, "kafclaw-gateway.service")
179+
if err != nil {
180+
return daemonResult(cmd, "error", action, map[string]any{"output": strings.TrimSpace(string(out))}, err.Error())
181+
}
182+
return daemonResult(cmd, "ok", action, map[string]any{"output": strings.TrimSpace(string(out))}, "")
183+
}
184+
185+
func runDaemonStatus(cmd *cobra.Command, args []string) error {
186+
if daemonOS != "linux" {
187+
return daemonResult(cmd, "error", "status", map[string]any{"os": daemonOS}, "daemon status currently supports Linux systemd only")
188+
}
189+
enabledOut, enabledErr := daemonExecFn("systemctl", "is-enabled", "kafclaw-gateway.service")
190+
activeOut, activeErr := daemonExecFn("systemctl", "is-active", "kafclaw-gateway.service")
191+
result := map[string]any{
192+
"enabled": strings.TrimSpace(string(enabledOut)),
193+
"active": strings.TrimSpace(string(activeOut)),
194+
}
195+
if enabledErr != nil || activeErr != nil {
196+
return daemonResult(cmd, "error", "status", result, "service not enabled/active")
197+
}
198+
return daemonResult(cmd, "ok", "status", result, "")
199+
}
200+
201+
func daemonResult(cmd *cobra.Command, status, action string, result map[string]any, errMsg string) error {
202+
if daemonJSON {
203+
payload := map[string]any{
204+
"status": strings.TrimSpace(status),
205+
"command": "daemon",
206+
"action": strings.TrimSpace(action),
207+
}
208+
if len(result) > 0 {
209+
payload["result"] = result
210+
}
211+
if strings.TrimSpace(errMsg) != "" {
212+
payload["error"] = strings.TrimSpace(errMsg)
213+
}
214+
b, _ := json.MarshalIndent(payload, "", " ")
215+
fmt.Fprintln(cmd.OutOrStdout(), string(b))
216+
if strings.EqualFold(status, "error") {
217+
return fmt.Errorf("%s", errMsg)
218+
}
219+
return nil
220+
}
221+
if strings.EqualFold(status, "error") {
222+
return fmt.Errorf("%s", errMsg)
223+
}
224+
fmt.Fprintf(cmd.OutOrStdout(), "daemon %s: ok\n", action)
225+
return nil
226+
}

0 commit comments

Comments
 (0)