@task0/cli 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +49 -0
  2. package/dist/main.js +426 -12
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -62,6 +62,55 @@ task0 task triage <id> # decompose an IDEA into ISSUE files
62
62
  task0 run <id> # drive the full triage → plan → refine → exec loop
63
63
  ```
64
64
 
65
+ ## Run as a service (autostart on login / boot)
66
+
67
+ `task0 daemon register` installs an OS-level autostart service in addition to
68
+ recording the daemon identity. After registering you can sign out, reboot, and
69
+ the daemon reconnects on its own.
70
+
71
+ ```sh
72
+ # user-level (default) — starts at user login, no sudo needed
73
+ task0 daemon register --server https://central.example.com:4318
74
+
75
+ # system-level — starts at boot, runs without an active login
76
+ sudo -E task0 daemon register --system --server https://central.example.com:4318
77
+
78
+ # pause / resume without forgetting the identity
79
+ task0 daemon stop
80
+ task0 daemon start
81
+
82
+ # disable everything: stop the service, remove the unit, clear the identity
83
+ task0 daemon logout
84
+ ```
85
+
86
+ Per platform:
87
+
88
+ | Platform | Scope | File written |
89
+ |----------|----------|--------------|
90
+ | macOS | user | `~/Library/LaunchAgents/cc.cy0.task0.plist` (launchd) |
91
+ | macOS | system | `/Library/LaunchDaemons/cc.cy0.task0.plist` (launchd, needs sudo) |
92
+ | Linux | user | `~/.config/systemd/user/cc.cy0.task0.service` (systemd) |
93
+ | Linux | system | `/etc/systemd/system/cc.cy0.task0.service` (systemd, needs sudo) |
94
+
95
+ Logs land in `~/.task0/logs/daemon.{out,err}.log`. To watch them live:
96
+
97
+ ```sh
98
+ tail -f ~/.task0/logs/daemon.out.log
99
+ ```
100
+
101
+ Linux user-scope note: systemd kills the service when you log out. To keep it
102
+ running across logouts, enable lingering once: `sudo loginctl enable-linger $USER`.
103
+
104
+ Useful flags on `register`:
105
+
106
+ - `--no-install` — register identity only; do not write a service unit
107
+ - `--no-start` — write the unit but do not start it immediately
108
+ - `--force` — re-register over an existing identity
109
+
110
+ For debugging or supervised setups you can also invoke the WebSocket loop
111
+ directly in the foreground with `task0 daemon run` (this is the same command
112
+ that the service unit invokes internally).
113
+
65
114
  ## Environment variables
66
115
 
67
116
  | Variable | Purpose |
package/dist/main.js CHANGED
@@ -901,6 +901,9 @@ var init_task_state2 = __esm({
901
901
  });
902
902
 
903
903
  // src/main.ts
904
+ import { readFileSync } from "fs";
905
+ import { fileURLToPath as fileURLToPath2 } from "url";
906
+ import path24 from "path";
904
907
  import { Command as Command22 } from "commander";
905
908
 
906
909
  // src/commands/source.ts
@@ -1141,11 +1144,11 @@ async function request(method, pathname, body) {
1141
1144
  return parsed;
1142
1145
  }
1143
1146
  var api = {
1144
- get: (path21) => request("GET", path21),
1145
- post: (path21, body) => request("POST", path21, body ?? {}),
1146
- put: (path21, body) => request("PUT", path21, body ?? {}),
1147
- patch: (path21, body) => request("PATCH", path21, body ?? {}),
1148
- del: (path21) => request("DELETE", path21)
1147
+ get: (path25) => request("GET", path25),
1148
+ post: (path25, body) => request("POST", path25, body ?? {}),
1149
+ put: (path25, body) => request("PUT", path25, body ?? {}),
1150
+ patch: (path25, body) => request("PATCH", path25, body ?? {}),
1151
+ del: (path25) => request("DELETE", path25)
1149
1152
  };
1150
1153
 
1151
1154
  // src/commands/task/triage.ts
@@ -3809,7 +3812,7 @@ async function streamOutput(ref, runtimeId) {
3809
3812
  }
3810
3813
 
3811
3814
  // src/commands/daemon.ts
3812
- import os5 from "os";
3815
+ import os6 from "os";
3813
3816
  import { Command as Command20 } from "commander";
3814
3817
  import chalk20 from "chalk";
3815
3818
  import WebSocket from "ws";
@@ -3887,6 +3890,308 @@ var rpcHandlers = {
3887
3890
  }
3888
3891
  };
3889
3892
 
3893
+ // src/core/daemon-service/launchd.ts
3894
+ import { spawnSync as spawnSync5 } from "child_process";
3895
+ import fs23 from "fs";
3896
+ import path22 from "path";
3897
+
3898
+ // src/core/daemon-service/binary.ts
3899
+ import fs22 from "fs";
3900
+ import { fileURLToPath } from "url";
3901
+ function resolveTask0Invocation() {
3902
+ const node = process.execPath;
3903
+ const argv1 = process.argv[1] ?? fileURLToPath(import.meta.url);
3904
+ const main2 = fs22.realpathSync(argv1);
3905
+ if (!isInstalledBuild(main2) && process.env.TASK0_ALLOW_DEV_SERVICE !== "1") {
3906
+ throw new Error(
3907
+ `Refusing to install autostart service pointing at ${main2}.
3908
+ That looks like a development source, not an installed @task0/cli build.
3909
+ Install the CLI globally (\`npm i -g @task0/cli\`) and retry, or set
3910
+ TASK0_ALLOW_DEV_SERVICE=1 to override.`
3911
+ );
3912
+ }
3913
+ return { node, main: main2, args: ["daemon", "run"] };
3914
+ }
3915
+ function isInstalledBuild(p) {
3916
+ return /[/\\]dist[/\\]main\.js$/.test(p);
3917
+ }
3918
+
3919
+ // src/core/daemon-service/paths.ts
3920
+ import os5 from "os";
3921
+ import path21 from "path";
3922
+
3923
+ // src/core/daemon-service/types.ts
3924
+ var SERVICE_LABEL = "cc.cy0.task0";
3925
+
3926
+ // src/core/daemon-service/paths.ts
3927
+ function unitPath(scope) {
3928
+ const platform = process.platform;
3929
+ if (platform === "darwin") {
3930
+ return scope === "user" ? path21.join(os5.homedir(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`) : path21.join("/", "Library", "LaunchDaemons", `${SERVICE_LABEL}.plist`);
3931
+ }
3932
+ if (platform === "linux") {
3933
+ return scope === "user" ? path21.join(os5.homedir(), ".config", "systemd", "user", `${SERVICE_LABEL}.service`) : path21.join("/", "etc", "systemd", "system", `${SERVICE_LABEL}.service`);
3934
+ }
3935
+ throw new Error(`Unsupported platform for service install: ${platform}`);
3936
+ }
3937
+ function logDir() {
3938
+ return path21.join(os5.homedir(), ".task0", "logs");
3939
+ }
3940
+ function logPaths() {
3941
+ const dir = logDir();
3942
+ return {
3943
+ out: path21.join(dir, "daemon.out.log"),
3944
+ err: path21.join(dir, "daemon.err.log")
3945
+ };
3946
+ }
3947
+
3948
+ // src/core/daemon-service/launchd.ts
3949
+ function escapeXml(s) {
3950
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3951
+ }
3952
+ function renderPlist(opts) {
3953
+ const programArgs = [opts.node, opts.main, ...opts.args].map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
3954
+ return `<?xml version="1.0" encoding="UTF-8"?>
3955
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3956
+ <plist version="1.0">
3957
+ <dict>
3958
+ <key>Label</key>
3959
+ <string>${SERVICE_LABEL}</string>
3960
+ <key>ProgramArguments</key>
3961
+ <array>
3962
+ ${programArgs}
3963
+ </array>
3964
+ <key>RunAtLoad</key>
3965
+ <true/>
3966
+ <key>KeepAlive</key>
3967
+ <dict>
3968
+ <key>SuccessfulExit</key>
3969
+ <false/>
3970
+ </dict>
3971
+ <key>ThrottleInterval</key>
3972
+ <integer>5</integer>
3973
+ <key>StandardOutPath</key>
3974
+ <string>${escapeXml(opts.out)}</string>
3975
+ <key>StandardErrorPath</key>
3976
+ <string>${escapeXml(opts.err)}</string>
3977
+ <key>WorkingDirectory</key>
3978
+ <string>${escapeXml(opts.home)}</string>
3979
+ <key>EnvironmentVariables</key>
3980
+ <dict>
3981
+ <key>PATH</key>
3982
+ <string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>
3983
+ </dict>
3984
+ </dict>
3985
+ </plist>
3986
+ `;
3987
+ }
3988
+ function domainTarget(scope) {
3989
+ if (scope === "system") return "system";
3990
+ const uid = process.getuid?.() ?? 0;
3991
+ return `gui/${uid}`;
3992
+ }
3993
+ function serviceTarget(scope) {
3994
+ return `${domainTarget(scope)}/${SERVICE_LABEL}`;
3995
+ }
3996
+ function run2(cmd, args) {
3997
+ const res = spawnSync5(cmd, args, { encoding: "utf-8" });
3998
+ return { code: res.status ?? -1, stdout: res.stdout ?? "", stderr: res.stderr ?? "" };
3999
+ }
4000
+ function createLaunchdManager(scope) {
4001
+ const file = unitPath(scope);
4002
+ const logs = logPaths();
4003
+ async function install() {
4004
+ const inv = resolveTask0Invocation();
4005
+ fs23.mkdirSync(logDir(), { recursive: true });
4006
+ fs23.mkdirSync(path22.dirname(file), { recursive: true });
4007
+ const body = renderPlist({
4008
+ node: inv.node,
4009
+ main: inv.main,
4010
+ args: inv.args,
4011
+ home: process.env.HOME ?? "/",
4012
+ out: logs.out,
4013
+ err: logs.err
4014
+ });
4015
+ fs23.writeFileSync(file, body, { mode: 420 });
4016
+ const bootstrap = run2("launchctl", ["bootstrap", domainTarget(scope), file]);
4017
+ if (bootstrap.code !== 0) {
4018
+ const already = /already loaded|service already bootstrapped/i.test(bootstrap.stderr);
4019
+ if (already) {
4020
+ run2("launchctl", ["bootout", serviceTarget(scope)]);
4021
+ const retry = run2("launchctl", ["bootstrap", domainTarget(scope), file]);
4022
+ if (retry.code !== 0) {
4023
+ const legacy = run2("launchctl", ["load", "-w", file]);
4024
+ if (legacy.code !== 0) {
4025
+ throw new Error(
4026
+ `launchctl bootstrap/load failed: ${retry.stderr || legacy.stderr || bootstrap.stderr}`
4027
+ );
4028
+ }
4029
+ }
4030
+ } else {
4031
+ const legacy = run2("launchctl", ["load", "-w", file]);
4032
+ if (legacy.code !== 0) {
4033
+ throw new Error(`launchctl bootstrap/load failed: ${bootstrap.stderr || legacy.stderr}`);
4034
+ }
4035
+ }
4036
+ }
4037
+ return { unitPath: file };
4038
+ }
4039
+ async function uninstall() {
4040
+ run2("launchctl", ["bootout", serviceTarget(scope)]);
4041
+ if (fs23.existsSync(file)) {
4042
+ run2("launchctl", ["unload", file]);
4043
+ fs23.unlinkSync(file);
4044
+ }
4045
+ }
4046
+ async function start() {
4047
+ const res = run2("launchctl", ["kickstart", "-k", serviceTarget(scope)]);
4048
+ if (res.code !== 0) {
4049
+ const load = run2("launchctl", ["load", "-w", file]);
4050
+ if (load.code !== 0) {
4051
+ throw new Error(`launchctl start failed: ${res.stderr || load.stderr}`);
4052
+ }
4053
+ }
4054
+ }
4055
+ async function stop() {
4056
+ const res = run2("launchctl", ["kill", "SIGTERM", serviceTarget(scope)]);
4057
+ if (res.code !== 0) {
4058
+ run2("launchctl", ["unload", file]);
4059
+ }
4060
+ }
4061
+ async function status() {
4062
+ if (!fs23.existsSync(file)) return "absent";
4063
+ const printed = run2("launchctl", ["print", serviceTarget(scope)]);
4064
+ if (printed.code !== 0) return "installed";
4065
+ const out = printed.stdout;
4066
+ if (/\bstate\s*=\s*running/.test(out)) return "running";
4067
+ if (/\bstate\s*=\s*spawning/.test(out)) return "installed";
4068
+ if (/last exit code\s*=\s*[1-9]/i.test(out)) return "errored";
4069
+ return "stopped";
4070
+ }
4071
+ return {
4072
+ scope,
4073
+ install,
4074
+ uninstall,
4075
+ start,
4076
+ stop,
4077
+ status,
4078
+ unitPath: () => file,
4079
+ logPaths: () => logs
4080
+ };
4081
+ }
4082
+
4083
+ // src/core/daemon-service/systemd.ts
4084
+ import { spawnSync as spawnSync6 } from "child_process";
4085
+ import fs24 from "fs";
4086
+ import path23 from "path";
4087
+ function shellEscape(s) {
4088
+ if (!/[\s"\\$]/.test(s)) return s;
4089
+ return `"${s.replace(/[\\"]/g, (m) => `\\${m}`)}"`;
4090
+ }
4091
+ function renderUnit(opts) {
4092
+ const execStart = [opts.node, opts.main, ...opts.args].map(shellEscape).join(" ");
4093
+ const wantedBy = opts.scope === "user" ? "default.target" : "multi-user.target";
4094
+ return `[Unit]
4095
+ Description=task0 daemon \u2014 central-server bridge
4096
+ After=network-online.target
4097
+ Wants=network-online.target
4098
+
4099
+ [Service]
4100
+ Type=simple
4101
+ ExecStart=${execStart}
4102
+ Restart=on-failure
4103
+ RestartSec=5s
4104
+ StandardOutput=append:${opts.out}
4105
+ StandardError=append:${opts.err}
4106
+ Environment=NODE_ENV=production
4107
+
4108
+ [Install]
4109
+ WantedBy=${wantedBy}
4110
+ `;
4111
+ }
4112
+ function scopeFlag(scope) {
4113
+ return scope === "user" ? ["--user"] : [];
4114
+ }
4115
+ function run3(cmd, args) {
4116
+ const res = spawnSync6(cmd, args, { encoding: "utf-8" });
4117
+ return { code: res.status ?? -1, stdout: res.stdout ?? "", stderr: res.stderr ?? "" };
4118
+ }
4119
+ function createSystemdManager(scope) {
4120
+ const file = unitPath(scope);
4121
+ const logs = logPaths();
4122
+ const unitName = `${SERVICE_LABEL}.service`;
4123
+ async function install() {
4124
+ const inv = resolveTask0Invocation();
4125
+ fs24.mkdirSync(logDir(), { recursive: true });
4126
+ fs24.mkdirSync(path23.dirname(file), { recursive: true });
4127
+ const body = renderUnit({
4128
+ node: inv.node,
4129
+ main: inv.main,
4130
+ args: inv.args,
4131
+ out: logs.out,
4132
+ err: logs.err,
4133
+ scope
4134
+ });
4135
+ fs24.writeFileSync(file, body, { mode: 420 });
4136
+ const reload = run3("systemctl", [...scopeFlag(scope), "daemon-reload"]);
4137
+ if (reload.code !== 0) {
4138
+ throw new Error(`systemctl daemon-reload failed: ${reload.stderr}`);
4139
+ }
4140
+ return { unitPath: file };
4141
+ }
4142
+ async function uninstall() {
4143
+ run3("systemctl", [...scopeFlag(scope), "disable", "--now", unitName]);
4144
+ if (fs24.existsSync(file)) {
4145
+ fs24.unlinkSync(file);
4146
+ }
4147
+ run3("systemctl", [...scopeFlag(scope), "daemon-reload"]);
4148
+ }
4149
+ async function start() {
4150
+ const res = run3("systemctl", [...scopeFlag(scope), "enable", "--now", unitName]);
4151
+ if (res.code !== 0) {
4152
+ throw new Error(`systemctl enable --now failed: ${res.stderr}`);
4153
+ }
4154
+ }
4155
+ async function stop() {
4156
+ const res = run3("systemctl", [...scopeFlag(scope), "stop", unitName]);
4157
+ if (res.code !== 0) {
4158
+ throw new Error(`systemctl stop failed: ${res.stderr}`);
4159
+ }
4160
+ }
4161
+ async function status() {
4162
+ if (!fs24.existsSync(file)) return "absent";
4163
+ const res = run3("systemctl", [...scopeFlag(scope), "is-active", unitName]);
4164
+ const out = res.stdout.trim();
4165
+ if (out === "active") return "running";
4166
+ if (out === "inactive") return "stopped";
4167
+ if (out === "failed") return "errored";
4168
+ if (out === "activating" || out === "deactivating") return "installed";
4169
+ return "installed";
4170
+ }
4171
+ return {
4172
+ scope,
4173
+ install,
4174
+ uninstall,
4175
+ start,
4176
+ stop,
4177
+ status,
4178
+ unitPath: () => file,
4179
+ logPaths: () => logs
4180
+ };
4181
+ }
4182
+
4183
+ // src/core/daemon-service/index.ts
4184
+ function isPlatformSupported() {
4185
+ return process.platform === "darwin" || process.platform === "linux";
4186
+ }
4187
+ function getServiceManager(scope) {
4188
+ if (process.platform === "darwin") return createLaunchdManager(scope);
4189
+ if (process.platform === "linux") return createSystemdManager(scope);
4190
+ throw new Error(
4191
+ `Autostart service installation is not supported on ${process.platform}. Run \`task0 daemon run\` in a process supervisor of your choice.`
4192
+ );
4193
+ }
4194
+
3890
4195
  // src/commands/daemon.ts
3891
4196
  var DAEMON_VERSION = "0.1.0";
3892
4197
  async function dispatchRpc(ws, id, method, params) {
@@ -3936,15 +4241,31 @@ async function jsonGet(url) {
3936
4241
  }
3937
4242
  return res.json();
3938
4243
  }
4244
+ function requireRootIfSystem(scope, rerunHint) {
4245
+ if (scope !== "system") return;
4246
+ const uid = process.getuid?.();
4247
+ if (uid === 0) return;
4248
+ console.error(chalk20.red("--system requires root."));
4249
+ console.error("Re-run with sudo, preserving env:");
4250
+ console.error(chalk20.cyan(` sudo -E ${rerunHint}`));
4251
+ process.exit(1);
4252
+ }
4253
+ function rerunArgv() {
4254
+ return ["task0", ...process.argv.slice(2)].join(" ");
4255
+ }
3939
4256
  var daemonCmd = new Command20("daemon").description("Manage this host as a task0 daemon registered with a central server");
3940
- daemonCmd.command("register").description("Register this host with a central server and save the identity locally").requiredOption("-s, --server <url>", "Central server URL (e.g. https://central.example.com:4318)").option("-n, --name <name>", "Display name for this daemon (defaults to hostname)").option("--force", "Overwrite existing identity if present").action(async (opts) => {
4257
+ daemonCmd.command("register").description("Register this host with a central server, install the autostart service, and start it").requiredOption("-s, --server <url>", "Central server URL (e.g. https://central.example.com:4318)").option("-n, --name <name>", "Display name for this daemon (defaults to hostname)").option("--force", "Overwrite existing identity if present").option("--system", "Install at the system layer (LaunchDaemons / /etc/systemd/system, requires sudo)").option("--no-install", "Skip installing the autostart service unit").option("--no-start", "Install the service unit but do not start it now").action(async (opts) => {
4258
+ const scope = opts.system ? "system" : "user";
4259
+ if (opts.install) {
4260
+ requireRootIfSystem(scope, rerunArgv());
4261
+ }
3941
4262
  const existing = readDaemonIdentity();
3942
4263
  if (existing && !opts.force) {
3943
4264
  fail6(`Already registered as ${existing.daemon_id}. Pass --force to re-register.`);
3944
4265
  }
3945
4266
  const base = opts.server.replace(/\/$/, "");
3946
4267
  const body = {
3947
- hostname: os5.hostname(),
4268
+ hostname: os6.hostname(),
3948
4269
  platform: process.platform,
3949
4270
  name: opts.name
3950
4271
  };
@@ -3974,8 +4295,67 @@ daemonCmd.command("register").description("Register this host with a central ser
3974
4295
  writeDaemonIdentity(identity);
3975
4296
  console.log(chalk20.green(`Registered as ${data.daemon.object_id} (${data.daemon.name})`));
3976
4297
  console.log(`Identity saved to ${daemonConfigPath()}`);
4298
+ if (!opts.install) {
4299
+ console.log(chalk20.dim("Skipping service install (--no-install)."));
4300
+ console.log(chalk20.dim(`Run \`task0 daemon run\` to start the WebSocket loop in foreground.`));
4301
+ return;
4302
+ }
4303
+ if (!isPlatformSupported()) {
4304
+ console.log(
4305
+ chalk20.yellow(
4306
+ `Autostart service is not supported on ${process.platform}. Run \`task0 daemon run\` under a supervisor of your choice.`
4307
+ )
4308
+ );
4309
+ return;
4310
+ }
4311
+ const svc = getServiceManager(scope);
4312
+ try {
4313
+ const { unitPath: unitPath2 } = await svc.install({ identity });
4314
+ const logs = svc.logPaths();
4315
+ console.log(chalk20.green(`Installed service unit at ${unitPath2}`));
4316
+ console.log(chalk20.dim(`Logs: ${logs.out}`));
4317
+ console.log(chalk20.dim(` ${logs.err}`));
4318
+ if (opts.start) {
4319
+ await svc.start();
4320
+ console.log(chalk20.green("Service started."));
4321
+ } else {
4322
+ console.log(chalk20.dim("Skipping start (--no-start). Run `task0 daemon start` when ready."));
4323
+ }
4324
+ } catch (error2) {
4325
+ console.error(chalk20.red(`Service install failed: ${error2 instanceof Error ? error2.message : String(error2)}`));
4326
+ console.error(chalk20.dim("Identity is saved; rerun `task0 daemon register --force` once the issue is resolved."));
4327
+ process.exit(1);
4328
+ }
4329
+ });
4330
+ daemonCmd.command("start").description("Start the installed autostart service via launchctl / systemctl").option("--system", "Target the system-layer service").action(async (opts) => {
4331
+ const scope = opts.system ? "system" : "user";
4332
+ requireRootIfSystem(scope, rerunArgv());
4333
+ if (!isPlatformSupported()) {
4334
+ fail6(`Service control is not supported on ${process.platform}.`);
4335
+ }
4336
+ const svc = getServiceManager(scope);
4337
+ try {
4338
+ await svc.start();
4339
+ console.log(chalk20.green("Service started."));
4340
+ } catch (error2) {
4341
+ fail6(error2 instanceof Error ? error2.message : String(error2));
4342
+ }
4343
+ });
4344
+ daemonCmd.command("stop").description("Stop the autostart service via launchctl / systemctl").option("--system", "Target the system-layer service").action(async (opts) => {
4345
+ const scope = opts.system ? "system" : "user";
4346
+ requireRootIfSystem(scope, rerunArgv());
4347
+ if (!isPlatformSupported()) {
4348
+ fail6(`Service control is not supported on ${process.platform}.`);
4349
+ }
4350
+ const svc = getServiceManager(scope);
4351
+ try {
4352
+ await svc.stop();
4353
+ console.log(chalk20.green("Service stopped."));
4354
+ } catch (error2) {
4355
+ fail6(error2 instanceof Error ? error2.message : String(error2));
4356
+ }
3977
4357
  });
3978
- daemonCmd.command("start").description("Start the local daemon and connect to the registered server (long-running)").action(async () => {
4358
+ daemonCmd.command("run").description("Run the daemon WebSocket loop in foreground (invoked by the service unit; useful for debugging)").action(async () => {
3979
4359
  const identity = loadRequiredIdentity();
3980
4360
  const wsUrl = identity.server_url.replace(/^http/, "ws").replace(/\/$/, "") + "/ws/daemon";
3981
4361
  console.log(chalk20.green(`Starting daemon ${identity.daemon_id} \u2192 ${wsUrl}`));
@@ -4066,17 +4446,42 @@ daemonCmd.command("list").description("List daemons registered on the configured
4066
4446
  );
4067
4447
  }
4068
4448
  });
4069
- daemonCmd.command("show [daemonId]").description("Show local daemon identity (no arg) or remote daemon by id").action(async (daemonId) => {
4449
+ daemonCmd.command("show [daemonId]").description("Show local daemon identity (no arg) or remote daemon by id").option("--system", "Inspect the system-layer service status (default: user)").action(async (daemonId, opts) => {
4070
4450
  if (!daemonId) {
4071
4451
  const identity = loadRequiredIdentity();
4072
4452
  console.log(JSON.stringify({ ...identity, token: "***" }, null, 2));
4453
+ if (isPlatformSupported()) {
4454
+ const scope = opts.system ? "system" : "user";
4455
+ try {
4456
+ const svc = getServiceManager(scope);
4457
+ const state2 = await svc.status();
4458
+ console.log(chalk20.dim(`service (${scope}): ${state2} \u2192 ${svc.unitPath()}`));
4459
+ } catch (error2) {
4460
+ console.log(chalk20.dim(`service status unavailable: ${error2 instanceof Error ? error2.message : String(error2)}`));
4461
+ }
4462
+ }
4073
4463
  return;
4074
4464
  }
4075
4465
  const base = serverBase(readDaemonIdentity());
4076
4466
  const data = await jsonGet(`${base}/api/daemons/${encodeURIComponent(daemonId)}`);
4077
4467
  console.log(JSON.stringify(data.daemon, null, 2));
4078
4468
  });
4079
- daemonCmd.command("logout").description("Forget the locally stored daemon identity").action(() => {
4469
+ daemonCmd.command("logout").description("Stop and uninstall the autostart service, then forget the locally stored daemon identity").option("--system", "Target the system-layer service").option("--keep-service", "Leave the installed service unit in place; only clear the identity file").action(async (opts) => {
4470
+ const scope = opts.system ? "system" : "user";
4471
+ if (!opts.keepService) {
4472
+ requireRootIfSystem(scope, rerunArgv());
4473
+ if (isPlatformSupported()) {
4474
+ const svc = getServiceManager(scope);
4475
+ try {
4476
+ await svc.uninstall();
4477
+ console.log(chalk20.dim(`Service unit removed (${scope}).`));
4478
+ } catch (error2) {
4479
+ console.error(
4480
+ chalk20.yellow(`Service uninstall warning: ${error2 instanceof Error ? error2.message : String(error2)}`)
4481
+ );
4482
+ }
4483
+ }
4484
+ }
4080
4485
  if (clearDaemonIdentity()) {
4081
4486
  console.log(chalk20.green("Daemon identity cleared."));
4082
4487
  } else {
@@ -4345,7 +4750,16 @@ function captureTopLevel(err, options) {
4345
4750
  }
4346
4751
 
4347
4752
  // src/main.ts
4348
- var TASK0_VERSION = "0.1.0";
4753
+ var TASK0_VERSION = readVersion();
4754
+ function readVersion() {
4755
+ try {
4756
+ const here = path24.dirname(fileURLToPath2(import.meta.url));
4757
+ const pkg = JSON.parse(readFileSync(path24.resolve(here, "..", "package.json"), "utf-8"));
4758
+ return pkg.version ?? "unknown";
4759
+ } catch {
4760
+ return "unknown";
4761
+ }
4762
+ }
4349
4763
  var program = new Command22().name("task0").description("Task-centric control layer for agent workflow").version(TASK0_VERSION);
4350
4764
  program.addCommand(source);
4351
4765
  program.addCommand(project);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@task0/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "task0 — task-centric agent workflow CLI",
5
5
  "homepage": "https://github.com/cy0-labs/task0#readme",
6
6
  "repository": {