codepiper 0.1.0

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 (149) hide show
  1. package/.env.example +28 -0
  2. package/CHANGELOG.md +10 -0
  3. package/LEGAL_NOTICE.md +39 -0
  4. package/LICENSE +21 -0
  5. package/README.md +524 -0
  6. package/package.json +90 -0
  7. package/packages/cli/package.json +13 -0
  8. package/packages/cli/src/commands/analytics.ts +157 -0
  9. package/packages/cli/src/commands/attach.ts +299 -0
  10. package/packages/cli/src/commands/audit.ts +50 -0
  11. package/packages/cli/src/commands/auth.ts +261 -0
  12. package/packages/cli/src/commands/daemon.ts +162 -0
  13. package/packages/cli/src/commands/doctor.ts +303 -0
  14. package/packages/cli/src/commands/env-set.ts +162 -0
  15. package/packages/cli/src/commands/hook-forward.ts +268 -0
  16. package/packages/cli/src/commands/keys.ts +77 -0
  17. package/packages/cli/src/commands/kill.ts +19 -0
  18. package/packages/cli/src/commands/logs.ts +419 -0
  19. package/packages/cli/src/commands/model.ts +172 -0
  20. package/packages/cli/src/commands/policy-set.ts +185 -0
  21. package/packages/cli/src/commands/policy.ts +227 -0
  22. package/packages/cli/src/commands/providers.ts +114 -0
  23. package/packages/cli/src/commands/resize.ts +34 -0
  24. package/packages/cli/src/commands/send.ts +184 -0
  25. package/packages/cli/src/commands/sessions.ts +202 -0
  26. package/packages/cli/src/commands/slash.ts +92 -0
  27. package/packages/cli/src/commands/start.ts +243 -0
  28. package/packages/cli/src/commands/stop.ts +19 -0
  29. package/packages/cli/src/commands/tail.ts +137 -0
  30. package/packages/cli/src/commands/workflow.ts +786 -0
  31. package/packages/cli/src/commands/workspace.ts +127 -0
  32. package/packages/cli/src/lib/api.ts +78 -0
  33. package/packages/cli/src/lib/args.ts +72 -0
  34. package/packages/cli/src/lib/format.ts +93 -0
  35. package/packages/cli/src/main.ts +563 -0
  36. package/packages/core/package.json +7 -0
  37. package/packages/core/src/config.ts +30 -0
  38. package/packages/core/src/errors.ts +38 -0
  39. package/packages/core/src/eventBus.ts +56 -0
  40. package/packages/core/src/eventBusAdapter.ts +143 -0
  41. package/packages/core/src/index.ts +10 -0
  42. package/packages/core/src/sqliteEventBus.ts +336 -0
  43. package/packages/core/src/types.ts +63 -0
  44. package/packages/daemon/package.json +11 -0
  45. package/packages/daemon/src/api/analyticsRoutes.ts +343 -0
  46. package/packages/daemon/src/api/authRoutes.ts +344 -0
  47. package/packages/daemon/src/api/bodyLimit.ts +133 -0
  48. package/packages/daemon/src/api/envSetRoutes.ts +170 -0
  49. package/packages/daemon/src/api/gitRoutes.ts +409 -0
  50. package/packages/daemon/src/api/hooks.ts +588 -0
  51. package/packages/daemon/src/api/inputPolicy.ts +249 -0
  52. package/packages/daemon/src/api/notificationRoutes.ts +532 -0
  53. package/packages/daemon/src/api/policyRoutes.ts +234 -0
  54. package/packages/daemon/src/api/policySetRoutes.ts +445 -0
  55. package/packages/daemon/src/api/routeUtils.ts +28 -0
  56. package/packages/daemon/src/api/routes.ts +1004 -0
  57. package/packages/daemon/src/api/server.ts +1388 -0
  58. package/packages/daemon/src/api/settingsRoutes.ts +367 -0
  59. package/packages/daemon/src/api/sqliteErrors.ts +47 -0
  60. package/packages/daemon/src/api/stt.ts +143 -0
  61. package/packages/daemon/src/api/terminalRoutes.ts +200 -0
  62. package/packages/daemon/src/api/validation.ts +287 -0
  63. package/packages/daemon/src/api/validationRoutes.ts +174 -0
  64. package/packages/daemon/src/api/workflowRoutes.ts +567 -0
  65. package/packages/daemon/src/api/workspaceRoutes.ts +151 -0
  66. package/packages/daemon/src/api/ws.ts +1588 -0
  67. package/packages/daemon/src/auth/apiRateLimiter.ts +73 -0
  68. package/packages/daemon/src/auth/authMiddleware.ts +305 -0
  69. package/packages/daemon/src/auth/authService.ts +496 -0
  70. package/packages/daemon/src/auth/rateLimiter.ts +137 -0
  71. package/packages/daemon/src/config/pricing.ts +79 -0
  72. package/packages/daemon/src/crypto/encryption.ts +196 -0
  73. package/packages/daemon/src/db/db.ts +2745 -0
  74. package/packages/daemon/src/db/index.ts +16 -0
  75. package/packages/daemon/src/db/migrations.ts +182 -0
  76. package/packages/daemon/src/db/policyDb.ts +349 -0
  77. package/packages/daemon/src/db/schema.sql +408 -0
  78. package/packages/daemon/src/db/workflowDb.ts +464 -0
  79. package/packages/daemon/src/git/gitUtils.ts +544 -0
  80. package/packages/daemon/src/index.ts +6 -0
  81. package/packages/daemon/src/main.ts +525 -0
  82. package/packages/daemon/src/notifications/pushNotifier.ts +369 -0
  83. package/packages/daemon/src/providers/codexAppServerScaffold.ts +49 -0
  84. package/packages/daemon/src/providers/registry.ts +111 -0
  85. package/packages/daemon/src/providers/types.ts +82 -0
  86. package/packages/daemon/src/sessions/auditLogger.ts +103 -0
  87. package/packages/daemon/src/sessions/policyEngine.ts +165 -0
  88. package/packages/daemon/src/sessions/policyMatcher.ts +114 -0
  89. package/packages/daemon/src/sessions/policyTypes.ts +94 -0
  90. package/packages/daemon/src/sessions/ptyProcess.ts +141 -0
  91. package/packages/daemon/src/sessions/sessionManager.ts +1770 -0
  92. package/packages/daemon/src/sessions/tmuxSession.ts +1073 -0
  93. package/packages/daemon/src/sessions/transcriptManager.ts +110 -0
  94. package/packages/daemon/src/sessions/transcriptParser.ts +149 -0
  95. package/packages/daemon/src/sessions/transcriptTailer.ts +214 -0
  96. package/packages/daemon/src/tracking/tokenTracker.ts +168 -0
  97. package/packages/daemon/src/workflows/contextManager.ts +83 -0
  98. package/packages/daemon/src/workflows/index.ts +31 -0
  99. package/packages/daemon/src/workflows/resultExtractor.ts +118 -0
  100. package/packages/daemon/src/workflows/waitConditionPoller.ts +131 -0
  101. package/packages/daemon/src/workflows/workflowParser.ts +217 -0
  102. package/packages/daemon/src/workflows/workflowRunner.ts +969 -0
  103. package/packages/daemon/src/workflows/workflowTypes.ts +188 -0
  104. package/packages/daemon/src/workflows/workflowValidator.ts +533 -0
  105. package/packages/providers/claude-code/package.json +11 -0
  106. package/packages/providers/claude-code/src/index.ts +7 -0
  107. package/packages/providers/claude-code/src/overlaySettings.ts +198 -0
  108. package/packages/providers/claude-code/src/provider.ts +311 -0
  109. package/packages/web/dist/android-chrome-192x192.png +0 -0
  110. package/packages/web/dist/android-chrome-512x512.png +0 -0
  111. package/packages/web/dist/apple-touch-icon.png +0 -0
  112. package/packages/web/dist/assets/AnalyticsPage-BIopKWRf.js +17 -0
  113. package/packages/web/dist/assets/PoliciesPage-CjdLN3dl.js +11 -0
  114. package/packages/web/dist/assets/SessionDetailPage-BtSA0V0M.js +179 -0
  115. package/packages/web/dist/assets/SettingsPage-Dbbz4Ca5.js +37 -0
  116. package/packages/web/dist/assets/WorkflowsPage-Dv6f3GgU.js +1 -0
  117. package/packages/web/dist/assets/chart-vendor-DlOHLaCG.js +49 -0
  118. package/packages/web/dist/assets/codicon-ngg6Pgfi.ttf +0 -0
  119. package/packages/web/dist/assets/css.worker-BvV5MPou.js +93 -0
  120. package/packages/web/dist/assets/editor.worker-CKy7Pnvo.js +26 -0
  121. package/packages/web/dist/assets/html.worker-BLJhxQJQ.js +470 -0
  122. package/packages/web/dist/assets/index-BbdhRfr2.css +1 -0
  123. package/packages/web/dist/assets/index-hgphORiw.js +204 -0
  124. package/packages/web/dist/assets/json.worker-usMZ-FED.js +58 -0
  125. package/packages/web/dist/assets/monaco-core-B_19GPAS.css +1 -0
  126. package/packages/web/dist/assets/monaco-core-DQ5Mk8AK.js +1234 -0
  127. package/packages/web/dist/assets/monaco-react-DfZNWvtW.js +11 -0
  128. package/packages/web/dist/assets/monacoSetup-DvBj52bT.js +1 -0
  129. package/packages/web/dist/assets/pencil-Dbczxz59.js +11 -0
  130. package/packages/web/dist/assets/react-vendor-B5MgMUHH.js +136 -0
  131. package/packages/web/dist/assets/refresh-cw-B0MGsYPL.js +6 -0
  132. package/packages/web/dist/assets/tabs-C8LsWiR5.js +1 -0
  133. package/packages/web/dist/assets/terminal-vendor-Cs8KPbV3.js +9 -0
  134. package/packages/web/dist/assets/terminal-vendor-LcAfv9l9.css +32 -0
  135. package/packages/web/dist/assets/trash-2-Btlg0d4l.js +6 -0
  136. package/packages/web/dist/assets/ts.worker-DGHjMaqB.js +67731 -0
  137. package/packages/web/dist/favicon.ico +0 -0
  138. package/packages/web/dist/icon.svg +1 -0
  139. package/packages/web/dist/index.html +29 -0
  140. package/packages/web/dist/manifest.json +29 -0
  141. package/packages/web/dist/og-image.png +0 -0
  142. package/packages/web/dist/originals/android-chrome-192x192.png +0 -0
  143. package/packages/web/dist/originals/android-chrome-512x512.png +0 -0
  144. package/packages/web/dist/originals/apple-touch-icon.png +0 -0
  145. package/packages/web/dist/originals/favicon.ico +0 -0
  146. package/packages/web/dist/piper.svg +1 -0
  147. package/packages/web/dist/sounds/codepiper-soft-chime.wav +0 -0
  148. package/packages/web/dist/sw.js +257 -0
  149. package/scripts/postinstall-link-workspaces.mjs +58 -0
@@ -0,0 +1,261 @@
1
+ import { readErrorJson, readJson, responseErrorMessage } from "../lib/api";
2
+ import { getRequiredValue } from "../lib/args";
3
+
4
+ interface AuthOptions {
5
+ socket: string;
6
+ subcommand: string;
7
+ args: string[];
8
+ }
9
+
10
+ function parseAuthOptions(args: string[]): AuthOptions {
11
+ let socket = "/tmp/codepiper.sock";
12
+ const positional: string[] = [];
13
+
14
+ for (let i = 0; i < args.length; i++) {
15
+ const arg = args[i];
16
+ if (arg === undefined) {
17
+ continue;
18
+ }
19
+ if (arg === "--socket" || arg === "-s") {
20
+ socket = getRequiredValue(args, i, arg);
21
+ i++;
22
+ } else {
23
+ positional.push(arg);
24
+ }
25
+ }
26
+
27
+ return {
28
+ socket,
29
+ subcommand: positional[0] || "status",
30
+ args: positional.slice(1),
31
+ };
32
+ }
33
+
34
+ async function daemonRequest<T>(socket: string, path: string, init?: RequestInit): Promise<T> {
35
+ const response = await fetch(`http://localhost${path}`, {
36
+ unix: socket,
37
+ ...init,
38
+ headers: {
39
+ "Content-Type": "application/json",
40
+ ...init?.headers,
41
+ },
42
+ });
43
+
44
+ if (!response.ok) {
45
+ const errorData = await readErrorJson(response);
46
+ throw new Error(responseErrorMessage(response, errorData));
47
+ }
48
+
49
+ return readJson<T>(response);
50
+ }
51
+
52
+ async function handleStatus(socket: string): Promise<void> {
53
+ const status = await daemonRequest<{ setupRequired: boolean; mfaEnabled: boolean }>(
54
+ socket,
55
+ "/auth/status"
56
+ );
57
+
58
+ console.log("Auth Configuration:");
59
+ if (status.setupRequired) {
60
+ console.log(" Password: \x1b[33mNot set\x1b[0m (setup required)");
61
+ } else {
62
+ console.log(" Password: \x1b[32mConfigured\x1b[0m");
63
+ }
64
+
65
+ if (status.mfaEnabled) {
66
+ console.log(" MFA: \x1b[32mEnabled\x1b[0m");
67
+ } else {
68
+ console.log(" MFA: \x1b[33mDisabled\x1b[0m");
69
+ }
70
+ }
71
+
72
+ async function handleResetPassword(socket: string): Promise<void> {
73
+ process.stdout.write("New password: ");
74
+ const password = await readPasswordFromStdin();
75
+
76
+ if (password.length < 8) {
77
+ console.error("Error: Password must be at least 8 characters");
78
+ process.exit(1);
79
+ }
80
+
81
+ process.stdout.write("Confirm password: ");
82
+ const confirm = await readPasswordFromStdin();
83
+
84
+ if (password !== confirm) {
85
+ console.error("Error: Passwords do not match");
86
+ process.exit(1);
87
+ }
88
+
89
+ await daemonRequest(socket, "/auth/cli/reset-password", {
90
+ method: "POST",
91
+ body: JSON.stringify({ password }),
92
+ });
93
+
94
+ console.log("\x1b[32m✓\x1b[0m Password reset successfully");
95
+ console.log(" All existing sessions have been invalidated.");
96
+ }
97
+
98
+ async function handleResetMfa(socket: string): Promise<void> {
99
+ process.stdout.write("Are you sure you want to disable MFA? (yes/no): ");
100
+ const answer = await readLineFromStdin();
101
+
102
+ if (answer.toLowerCase() !== "yes") {
103
+ console.log("Cancelled.");
104
+ return;
105
+ }
106
+
107
+ await daemonRequest(socket, "/auth/cli/reset-mfa", {
108
+ method: "POST",
109
+ });
110
+
111
+ console.log("\x1b[32m✓\x1b[0m MFA has been disabled");
112
+ }
113
+
114
+ async function handleSessions(socket: string): Promise<void> {
115
+ const data = await daemonRequest<{
116
+ sessions: Array<{
117
+ createdAt: string | number;
118
+ lastUsedAt: string | number;
119
+ ipAddress: string | null;
120
+ userAgent: string | null;
121
+ }>;
122
+ }>(socket, "/auth/sessions");
123
+ const sessions = data.sessions;
124
+
125
+ if (sessions.length === 0) {
126
+ console.log("No active sessions.");
127
+ return;
128
+ }
129
+
130
+ console.log(`Active sessions (${sessions.length}):\n`);
131
+
132
+ for (const session of sessions) {
133
+ const created = new Date(session.createdAt).toLocaleString();
134
+ const lastUsed = new Date(session.lastUsedAt).toLocaleString();
135
+ const ip = session.ipAddress || "unknown";
136
+ const ua = parseUserAgent(session.userAgent);
137
+
138
+ console.log(` ${ua}`);
139
+ console.log(` IP: ${ip} | Created: ${created} | Last active: ${lastUsed}`);
140
+ console.log();
141
+ }
142
+ }
143
+
144
+ async function handleRevokeAll(socket: string): Promise<void> {
145
+ process.stdout.write("Revoke all active sessions? (yes/no): ");
146
+ const answer = await readLineFromStdin();
147
+
148
+ if (answer.toLowerCase() !== "yes") {
149
+ console.log("Cancelled.");
150
+ return;
151
+ }
152
+
153
+ await daemonRequest(socket, "/auth/sessions/revoke-all", {
154
+ method: "POST",
155
+ });
156
+
157
+ console.log("\x1b[32m✓\x1b[0m All sessions have been revoked");
158
+ }
159
+
160
+ function parseUserAgent(ua: string | null): string {
161
+ if (!ua) return "Unknown device";
162
+ if (ua.includes("Firefox")) return "Firefox";
163
+ if (ua.includes("Edg/")) return "Edge";
164
+ if (ua.includes("Chrome")) return "Chrome";
165
+ if (ua.includes("Safari")) return "Safari";
166
+ if (ua.includes("curl")) return "curl";
167
+ return "Browser";
168
+ }
169
+
170
+ async function readLineFromStdin(): Promise<string> {
171
+ const buf = Buffer.alloc(1024);
172
+ const n = await new Promise<number>((resolve) => {
173
+ process.stdin.once("readable", () => {
174
+ const chunk = process.stdin.read();
175
+ if (chunk) {
176
+ chunk.copy(buf);
177
+ resolve(chunk.length);
178
+ } else {
179
+ resolve(0);
180
+ }
181
+ });
182
+ });
183
+ return buf.subarray(0, n).toString("utf-8").trim();
184
+ }
185
+
186
+ async function readPasswordFromStdin(): Promise<string> {
187
+ // Disable echo for password input
188
+ if (process.stdin.isTTY) {
189
+ process.stdin.setRawMode(true);
190
+ }
191
+
192
+ const chars: string[] = [];
193
+
194
+ return new Promise((resolve) => {
195
+ const onData = (data: Buffer) => {
196
+ const char = data.toString("utf-8");
197
+
198
+ for (const c of char) {
199
+ if (c === "\r" || c === "\n") {
200
+ // Enter pressed
201
+ if (process.stdin.isTTY) {
202
+ process.stdin.setRawMode(false);
203
+ }
204
+ process.stdin.removeListener("data", onData);
205
+ process.stdout.write("\n");
206
+ resolve(chars.join(""));
207
+ return;
208
+ }
209
+ if (c === "\x03") {
210
+ // Ctrl+C
211
+ if (process.stdin.isTTY) {
212
+ process.stdin.setRawMode(false);
213
+ }
214
+ process.stdout.write("\n");
215
+ process.exit(1);
216
+ }
217
+ if (c === "\x7f" || c === "\b") {
218
+ // Backspace
219
+ if (chars.length > 0) {
220
+ chars.pop();
221
+ process.stdout.write("\b \b");
222
+ }
223
+ } else {
224
+ chars.push(c);
225
+ process.stdout.write("*");
226
+ }
227
+ }
228
+ };
229
+
230
+ process.stdin.resume();
231
+ process.stdin.on("data", onData);
232
+ });
233
+ }
234
+
235
+ export async function runAuthCommand(args: string[]): Promise<void> {
236
+ const options = parseAuthOptions(args);
237
+
238
+ switch (options.subcommand) {
239
+ case "status":
240
+ await handleStatus(options.socket);
241
+ break;
242
+ case "reset-password":
243
+ await handleResetPassword(options.socket);
244
+ break;
245
+ case "reset-mfa":
246
+ await handleResetMfa(options.socket);
247
+ break;
248
+ case "sessions":
249
+ await handleSessions(options.socket);
250
+ break;
251
+ case "revoke-all":
252
+ await handleRevokeAll(options.socket);
253
+ break;
254
+ default:
255
+ console.error(`Unknown auth subcommand: ${options.subcommand}`);
256
+ console.error(
257
+ "Available subcommands: status, reset-password, reset-mfa, sessions, revoke-all"
258
+ );
259
+ process.exit(1);
260
+ }
261
+ }
@@ -0,0 +1,162 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { daemonJson } from "../lib/api";
5
+ import { getFlag, getSocket } from "../lib/args";
6
+ import { colors, info, success } from "../lib/format";
7
+
8
+ const PID_FILE = path.join(os.homedir(), ".codepiper", "daemon.pid");
9
+
10
+ function readPid(): number | null {
11
+ try {
12
+ if (!fs.existsSync(PID_FILE)) return null;
13
+ const pid = Number.parseInt(fs.readFileSync(PID_FILE, "utf-8").trim(), 10);
14
+ return Number.isNaN(pid) ? null : pid;
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+
20
+ function isProcessRunning(pid: number): boolean {
21
+ try {
22
+ process.kill(pid, 0);
23
+ return true;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ async function startDaemon(args: string[]): Promise<void> {
30
+ const daemonScript = path.resolve(__dirname, "../../../daemon/src/main.ts");
31
+ const detach = getFlag(args, "detach");
32
+
33
+ // Pass through all args to the daemon process
34
+ const daemonArgs = ["run", daemonScript, ...args];
35
+
36
+ if (detach) {
37
+ // In detach mode, the daemon main.ts handles backgrounding itself
38
+ console.log("Starting CodePiper daemon (detached)...");
39
+ } else {
40
+ console.log("Starting CodePiper daemon...");
41
+ }
42
+
43
+ const proc = Bun.spawn(["bun", ...daemonArgs], {
44
+ stdio: detach ? ["ignore", "inherit", "inherit"] : ["inherit", "inherit", "inherit"],
45
+ env: process.env,
46
+ });
47
+
48
+ const exitCode = await proc.exited;
49
+ process.exit(exitCode);
50
+ }
51
+
52
+ async function stopDaemon(): Promise<void> {
53
+ const pid = readPid();
54
+
55
+ if (!pid) {
56
+ throw new Error(`No PID file found at ${PID_FILE}. Is the daemon running?`);
57
+ }
58
+
59
+ if (!isProcessRunning(pid)) {
60
+ // Process already dead, clean up PID file
61
+ try {
62
+ fs.unlinkSync(PID_FILE);
63
+ } catch {
64
+ /* ignore */
65
+ }
66
+ console.log(`Daemon (PID: ${pid}) is not running. Cleaned up stale PID file.`);
67
+ return;
68
+ }
69
+
70
+ console.log(`Stopping daemon (PID: ${pid})...`);
71
+ process.kill(pid, "SIGTERM");
72
+
73
+ // Wait for process to exit (up to 10 seconds)
74
+ const deadline = Date.now() + 10_000;
75
+ while (Date.now() < deadline) {
76
+ if (!isProcessRunning(pid)) {
77
+ // Clean up PID file if daemon didn't
78
+ try {
79
+ fs.unlinkSync(PID_FILE);
80
+ } catch {
81
+ /* ignore */
82
+ }
83
+ success("Daemon stopped");
84
+ return;
85
+ }
86
+ await new Promise((r) => setTimeout(r, 200));
87
+ }
88
+
89
+ // Force kill if still running
90
+ console.log("Daemon did not stop gracefully, sending SIGKILL...");
91
+ try {
92
+ process.kill(pid, "SIGKILL");
93
+ } catch {
94
+ // already dead
95
+ }
96
+ try {
97
+ fs.unlinkSync(PID_FILE);
98
+ } catch {
99
+ /* ignore */
100
+ }
101
+ success("Daemon killed");
102
+ }
103
+
104
+ async function showStatus(args: string[]): Promise<void> {
105
+ const socket = getSocket(args);
106
+ const pid = readPid();
107
+
108
+ console.log(`${colors.bold}Daemon Status${colors.reset}\n`);
109
+ info("PID file", PID_FILE);
110
+ info("Socket", socket);
111
+
112
+ if (!pid) {
113
+ info("Status", `${colors.red}not running${colors.reset} (no PID file)`);
114
+ return;
115
+ }
116
+
117
+ info("PID", String(pid));
118
+
119
+ if (!isProcessRunning(pid)) {
120
+ info("Status", `${colors.red}not running${colors.reset} (stale PID file)`);
121
+ return;
122
+ }
123
+
124
+ info("Process", `${colors.green}running${colors.reset}`);
125
+
126
+ // Try to reach the health endpoint
127
+ try {
128
+ const health = await daemonJson<{
129
+ status: string;
130
+ uptime?: number;
131
+ zombieSessionCount?: number;
132
+ }>("/health", { socket });
133
+ info("Health", `${colors.green}${health.status}${colors.reset}`);
134
+ if (typeof health.zombieSessionCount === "number") {
135
+ const zombieColor = health.zombieSessionCount > 0 ? colors.yellow : colors.green;
136
+ info("Zombie sessions", `${zombieColor}${health.zombieSessionCount}${colors.reset}`);
137
+ }
138
+ if (health.uptime !== undefined) {
139
+ const hours = Math.floor(health.uptime / 3600);
140
+ const mins = Math.floor((health.uptime % 3600) / 60);
141
+ info("Uptime", `${hours}h ${mins}m`);
142
+ }
143
+ } catch {
144
+ info("Health", `${colors.yellow}unreachable${colors.reset} (socket may not be ready)`);
145
+ }
146
+ }
147
+
148
+ export async function runDaemonCommand(args: string[]): Promise<void> {
149
+ const subcommand = args[0];
150
+
151
+ switch (subcommand) {
152
+ case "stop":
153
+ return stopDaemon();
154
+ case "status":
155
+ return showStatus(args.slice(1));
156
+ case "start":
157
+ return startDaemon(args.slice(1));
158
+ default:
159
+ // No subcommand or unknown — treat as start (backward compatible)
160
+ return startDaemon(args);
161
+ }
162
+ }