@ravefox/space 0.2.1 → 0.4.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 (167) hide show
  1. package/LICENSE +21 -205
  2. package/README.md +104 -31
  3. package/bin/space.js +113 -0
  4. package/lib/agents.js +181 -0
  5. package/lib/boardTools.js +89 -0
  6. package/lib/credentials.js +95 -0
  7. package/lib/daemon.js +294 -0
  8. package/lib/harness.js +166 -0
  9. package/lib/http.js +101 -0
  10. package/lib/login.js +68 -0
  11. package/lib/mcp.js +39 -0
  12. package/lib/runtime.js +253 -0
  13. package/lib/store.js +96 -0
  14. package/lib/tools.js +93 -0
  15. package/lib/workspaceSandbox.js +77 -0
  16. package/lib/workspaceTools.js +160 -0
  17. package/lib/workspaceWorker.js +17 -0
  18. package/package.json +27 -46
  19. package/NOTICE +0 -55
  20. package/assets/webhook-sdk/python/bot.py +0 -63
  21. package/assets/webhook-sdk/python/commonly.py +0 -232
  22. package/node_modules/@commonlyai/seats/README.md +0 -25
  23. package/node_modules/@commonlyai/seats/package.json +0 -22
  24. package/node_modules/@commonlyai/seats/src/adapters/hermes.js +0 -264
  25. package/node_modules/@commonlyai/seats/src/adapters/stub.js +0 -24
  26. package/node_modules/@commonlyai/seats/src/contract.js +0 -48
  27. package/node_modules/@commonlyai/seats/src/failures.js +0 -44
  28. package/node_modules/@commonlyai/seats/src/harness-plan.js +0 -329
  29. package/node_modules/@commonlyai/seats/src/index.js +0 -22
  30. package/node_modules/@commonlyai/seats/src/process-group.js +0 -269
  31. package/node_modules/@commonlyai/seats/src/record/format.js +0 -737
  32. package/node_modules/@commonlyai/seats/src/record/otel.js +0 -238
  33. package/node_modules/@commonlyai/seats/src/runtime-observation.js +0 -29
  34. package/node_modules/@commonlyai/seats/src/seat-model.js +0 -17
  35. package/node_modules/@commonlyai/seats/src/supervisor.js +0 -308
  36. package/node_modules/@commonlyai/seats/src/trace/events.js +0 -46
  37. package/node_modules/@commonlyai/seats/src/trace/store.js +0 -276
  38. package/node_modules/@commonlyai/seats/src/turn-processes.js +0 -226
  39. package/node_modules/@commonlyai/seats/src/upstream-refusal.js +0 -418
  40. package/node_modules/@commonlyai/seats/src/valet/env.js +0 -201
  41. package/node_modules/@commonlyai/seats/src/valet/grant.js +0 -123
  42. package/node_modules/@commonlyai/seats/src/valet/permissions.js +0 -213
  43. package/node_modules/@commonlyai/seats/src/wire/provider-proxy.js +0 -529
  44. package/skills/commonly/SKILL.md +0 -233
  45. package/src/commands/agent.js +0 -6334
  46. package/src/commands/daemon.js +0 -933
  47. package/src/commands/dev.js +0 -153
  48. package/src/commands/doctor.js +0 -141
  49. package/src/commands/inbox.js +0 -330
  50. package/src/commands/login.js +0 -194
  51. package/src/commands/mcp.js +0 -56
  52. package/src/commands/pod.js +0 -120
  53. package/src/commands/task.js +0 -279
  54. package/src/commands/verify.js +0 -48
  55. package/src/index.js +0 -99
  56. package/src/lib/adapter-login.js +0 -76
  57. package/src/lib/adapters/broker-bridge-entry.js +0 -76
  58. package/src/lib/adapters/claude.js +0 -2207
  59. package/src/lib/adapters/codex-broker-bridge.mjs +0 -298
  60. package/src/lib/adapters/codex.js +0 -1898
  61. package/src/lib/adapters/dsh.js +0 -513
  62. package/src/lib/adapters/goose.js +0 -510
  63. package/src/lib/adapters/hermes.js +0 -18
  64. package/src/lib/adapters/index.js +0 -68
  65. package/src/lib/adapters/opencode.js +0 -1681
  66. package/src/lib/adapters/pi-commonly-mcp.mjs +0 -71
  67. package/src/lib/adapters/pi-mcp-client.mjs +0 -617
  68. package/src/lib/adapters/pi-shell-env.mjs +0 -41
  69. package/src/lib/adapters/pi.js +0 -1025
  70. package/src/lib/adapters/qwen.js +0 -523
  71. package/src/lib/adapters/seat-model.js +0 -8
  72. package/src/lib/adapters/stub.js +0 -1
  73. package/src/lib/adapters/tested-version.js +0 -106
  74. package/src/lib/api.js +0 -205
  75. package/src/lib/claim-outcome.js +0 -128
  76. package/src/lib/claimed-work-gate.js +0 -73
  77. package/src/lib/cli-hint.js +0 -33
  78. package/src/lib/commit-attribution.js +0 -391
  79. package/src/lib/config.js +0 -155
  80. package/src/lib/content-refusal.js +0 -94
  81. package/src/lib/credential-file.js +0 -208
  82. package/src/lib/daemon-agent-states.js +0 -95
  83. package/src/lib/daemon-hint.js +0 -18
  84. package/src/lib/daemon-logs.js +0 -43
  85. package/src/lib/daemon-service-windows.js +0 -263
  86. package/src/lib/daemon-service.js +0 -486
  87. package/src/lib/daemon-state.js +0 -84
  88. package/src/lib/daemon-store.js +0 -119
  89. package/src/lib/daemon-supervisor.js +0 -629
  90. package/src/lib/daemon-watch.js +0 -115
  91. package/src/lib/declared-mcp-guard.js +0 -284
  92. package/src/lib/default-environment.js +0 -207
  93. package/src/lib/device-login.js +0 -126
  94. package/src/lib/digest-frame.js +0 -151
  95. package/src/lib/doctor.js +0 -291
  96. package/src/lib/egress-proxy.js +0 -160
  97. package/src/lib/enforcement.js +0 -946
  98. package/src/lib/environment.js +0 -681
  99. package/src/lib/first-output-watchdog.js +0 -191
  100. package/src/lib/git-readonly-server.js +0 -138
  101. package/src/lib/git-readonly.js +0 -463
  102. package/src/lib/git-remote-commonly.mjs +0 -100
  103. package/src/lib/grant-broker-guard.js +0 -345
  104. package/src/lib/hooks-config.js +0 -238
  105. package/src/lib/host-gh-broker.js +0 -152
  106. package/src/lib/host-gh-shim.mjs +0 -98
  107. package/src/lib/inbox.js +0 -483
  108. package/src/lib/instance-proxy.js +0 -76
  109. package/src/lib/machine-host.js +0 -29
  110. package/src/lib/mcp-credential-delivery.js +0 -121
  111. package/src/lib/mcp-first-run.js +0 -50
  112. package/src/lib/mcp-home.js +0 -644
  113. package/src/lib/mcp-ready.js +0 -68
  114. package/src/lib/mcp-server-version.js +0 -123
  115. package/src/lib/mcp-token.js +0 -36
  116. package/src/lib/mcp-warm-child.mjs +0 -412
  117. package/src/lib/memory-bridge.js +0 -191
  118. package/src/lib/memory-import.js +0 -156
  119. package/src/lib/missing-harness.js +0 -31
  120. package/src/lib/model-unavailable.js +0 -154
  121. package/src/lib/native-runtime-inventory.js +0 -40
  122. package/src/lib/outsider-grant-mcp.js +0 -82
  123. package/src/lib/own-key-launch.js +0 -42
  124. package/src/lib/pod-catch-up.js +0 -55
  125. package/src/lib/pod-focus.js +0 -224
  126. package/src/lib/pod-tail.js +0 -68
  127. package/src/lib/poll-liveness.js +0 -103
  128. package/src/lib/poll-retry.js +0 -81
  129. package/src/lib/poller.js +0 -200
  130. package/src/lib/provider-proxy.js +0 -42
  131. package/src/lib/public-seat-mcp-flag.js +0 -36
  132. package/src/lib/public-tool-error.js +0 -60
  133. package/src/lib/public-withheld-tools.js +0 -48
  134. package/src/lib/reply-receipt.js +0 -55
  135. package/src/lib/resolve-on-path.js +0 -22
  136. package/src/lib/runtime-inventory.js +0 -56
  137. package/src/lib/runtime-observation.js +0 -37
  138. package/src/lib/sandbox/bwrap.js +0 -275
  139. package/src/lib/sandbox/codex-perimeter.js +0 -16
  140. package/src/lib/sandbox/host-check.js +0 -268
  141. package/src/lib/sandbox/mode.js +0 -49
  142. package/src/lib/sandbox/opencode-loopback.mjs +0 -68
  143. package/src/lib/sandbox/seatbelt.js +0 -684
  144. package/src/lib/sandbox/turn-dir.js +0 -56
  145. package/src/lib/sandbox/windows-unsandboxed.js +0 -183
  146. package/src/lib/sandbox/workspace-git-config.js +0 -243
  147. package/src/lib/sandbox/workspace-pin.js +0 -158
  148. package/src/lib/seat-runtime-controls.js +0 -61
  149. package/src/lib/seat-stop.js +0 -97
  150. package/src/lib/server-skills.js +0 -32
  151. package/src/lib/session-recycle.js +0 -219
  152. package/src/lib/session-store.js +0 -215
  153. package/src/lib/skill-prompt.js +0 -74
  154. package/src/lib/skills-import.js +0 -113
  155. package/src/lib/spawn-credential.js +0 -402
  156. package/src/lib/spawn-retry.js +0 -311
  157. package/src/lib/stable-install.js +0 -176
  158. package/src/lib/supervised-turn.js +0 -352
  159. package/src/lib/task.js +0 -235
  160. package/src/lib/turn-failure-notice.js +0 -177
  161. package/src/lib/unconfined-seat-guard.js +0 -94
  162. package/src/lib/update-check.js +0 -192
  163. package/src/lib/upstream-refusal.js +0 -1
  164. package/src/lib/webhook-server.js +0 -109
  165. package/src/lib/windows-command.js +0 -393
  166. package/src/lib/workspace-git.js +0 -87
  167. package/src/lib/workspace-trust.js +0 -162
@@ -1,933 +0,0 @@
1
- import { collectRuntimeInventory, createInventoryCache } from '../lib/runtime-inventory.js';
2
- /**
3
- * commonly daemon <subcommand>
4
- *
5
- * Phase 2, slice 1: register one machine, persist its scoped daemon bearer,
6
- * and report/send heartbeats. Adoption and supervision deliberately arrive in
7
- * later slices; this command never reads an agent runtime credential.
8
- */
9
-
10
- import { hostname, homedir } from 'os';
11
- import { spawn } from 'child_process';
12
- import {
13
- chmodSync, closeSync, existsSync, mkdirSync, openSync, rmSync, watch,
14
- } from 'fs';
15
- import { createClient } from '../lib/api.js';
16
- import { getToken, resolveInstance, resolveInstanceUrl } from '../lib/config.js';
17
- import {
18
- clearDaemonStop, loadDaemonRecord, loadDaemonStop, removeDaemonRecord,
19
- saveDaemonRecord, saveDaemonStop,
20
- } from '../lib/daemon-store.js';
21
- import {
22
- createDaemonSupervisor,
23
- DEFAULT_HEARTBEAT_MS,
24
- DEFAULT_POLL_MS,
25
- } from '../lib/daemon-supervisor.js';
26
- import { loadAgentToken, saveAgentToken } from './agent.js';
27
- import { getLastTurn } from '../lib/session-store.js';
28
- import { getAdapter, providerKeyEnvNames, seatProviderKeyEnv } from '../lib/adapters/index.js';
29
- import { loginState, runLoginStatus, signedOutAdapters, signedOutHint } from '../lib/adapter-login.js';
30
-
31
- import {
32
- installDaemonService,
33
- installSummary,
34
- uninstallDaemonService,
35
- startDaemonService,
36
- stopDaemonService,
37
- restartDaemonService,
38
- daemonLogPath,
39
- servicePaths,
40
- serviceOwnerQuery,
41
- loadedServiceFile,
42
- foreignServiceRefusal,
43
- decodeCmdOutput,
44
- } from '../lib/daemon-service.js';
45
-
46
- import { daemonLogsDir, daemonSeatLogPath, readLogTail } from '../lib/daemon-logs.js';
47
- import { watchWindowsLauncher } from '../lib/daemon-service-windows.js';
48
- import {
49
- UNSANDBOXED_FLAG, clearUnsandboxedOptIn, hostSandboxReport, loadUnsandboxedOptIn, saveUnsandboxedOptIn, unsandboxedFlagRefusal,
50
- } from '../lib/sandbox/windows-unsandboxed.js';
51
- import { loadDaemonState, saveDaemonState } from '../lib/daemon-state.js';
52
- import {
53
- CLEAR_SCREEN, renderDaemonWatch, shouldRedraw, WATCH_INTERVAL_MS,
54
- } from '../lib/daemon-watch.js';
55
- import {
56
- createReachCache, loadPodReach, readSessionLastTurn,
57
- } from '../lib/daemon-agent-states.js';
58
- import { installedCliVersion, startUpdateNotice, UPDATE_CHECK_OPT_OUT_ENV } from '../lib/update-check.js';
59
- import { machineHostReport } from '../lib/machine-host.js';
60
- import { orchestrateDaemonSetup } from '../lib/stable-install.js';
61
- import { CLI } from '../lib/cli-hint.js';
62
-
63
- // Set on the steps `daemon setup` runs, so a step can leave a line to setup.
64
- const SETUP_CHILD_ENV = 'COMMONLY_DAEMON_SETUP_STEP';
65
-
66
- // F459: login and register take --instance, so the first-timer walk tried it
67
- // on run too and got "unknown option". Run serves the one registration this
68
- // computer has, so --instance only confirms it: the same instance runs, any
69
- // other (or an unknown saved key) is refused with the register line instead of
70
- // quietly serving the registered one.
71
- const instanceKey = (url) => String(url || '').replace(/\/+$/, '').toLowerCase();
72
- export const daemonRunInstanceRefusal = (record, instanceArg, resolve = resolveInstance) => {
73
- if (!instanceArg) return null;
74
- const wanted = resolve(instanceArg)?.url;
75
- if (wanted && instanceKey(wanted) === instanceKey(record?.instanceUrl)) return null;
76
- if (!wanted) {
77
- return `This computer is registered with ${record?.instanceUrl}, and no saved instance is named ${instanceArg}. `
78
- + `To use another instance, sign in to it first (${CLI} login --instance <url>), then ${CLI} daemon register --instance <url>`;
79
- }
80
- return `This computer is registered with ${record?.instanceUrl}, not ${instanceArg}. To run it for that instance instead: `
81
- + `${CLI} daemon register --instance ${instanceArg}`;
82
- };
83
-
84
- // launchd, systemd and Task Scheduler hold one Commonly daemon per computer
85
- // account. Install refuses to stop another HOME's; this flag takes it over.
86
- const REPLACE_FLAG = '--replace';
87
- const REPLACE_HELP = 'Replace a Space daemon that another account (another HOME) runs on this computer account. Without it, install stops and names that daemon.';
88
-
89
- // One supervised seat, as `commonly daemon status --verbose` names it. Two of these
90
- // fields are worth reading closely: the seat's `instanceId` (the identity the
91
- // platform routes by, so a seat attached to the wrong instance is otherwise
92
- // invisible) and the supervisor's `restarts` flap counter — which is written,
93
- // persisted and served, and was shown by no surface at all, so a seat that had
94
- // been crash-looping looked exactly like a healthy one (TASK-072, 2026-09-19).
95
- //
96
- // `restarts` distinguishes 0 from absent on purpose: a state file written by an
97
- // older daemon has no such field, and printing 0 for it would claim a reading the
98
- // file does not carry.
99
- export const formatSeatLine = (seat = {}) => {
100
- const model = seat.model || 'default model';
101
- const effort = seat.effort ? `/${seat.effort}` : '';
102
- const error = seat.lastError ? ` error=${seat.lastError}` : '';
103
- const restarts = seat.restarts === undefined || seat.restarts === null ? '-' : seat.restarts;
104
- return ` ${seat.agentName}: ${seat.state} instance=${seat.instanceId || '-'} adapter=${seat.adapter || 'unknown'} model=${model}${effort} pid=${seat.pid || '-'} restarts=${restarts} lastTurn=${seat.lastTurnAt || 'never'}${error}`;
105
- };
106
-
107
- // `daemon status --watch`: one frame, then a redraw every 2 s on a terminal. Off a
108
- // terminal, or with NO_COLOR set, it prints one frame and returns. A failed
109
- // fetch is shown in the frame, so the loop keeps running through a brief outage.
110
- const watchDaemonStatus = async () => {
111
- const redraw = shouldRedraw({ isTTY: process.stdout.isTTY });
112
- // Each agent's pod comes from its token record. The verdict for a pod is read
113
- // through the operator's login and cached for 10 s, so the 2 s frame does not
114
- // re-read it. A failed read is thrown, so the next frame retries.
115
- const reachFor = createReachCache({
116
- load: (podId) => loadPodReach({
117
- client: createClient({ instance: requireDaemonRecord().instanceUrl }),
118
- podId,
119
- }),
120
- });
121
- const frame = async () => {
122
- try {
123
- const record = requireDaemonRecord();
124
- // A failed machine read still shows the local seats and logs, with the
125
- // machine marked unreachable, so the operator can see what is running here.
126
- let machine;
127
- let machineNote = '';
128
- try {
129
- machine = await getDaemonMachineStatus({
130
- client: createClient({ instance: record.instanceUrl, token: record.daemonToken }),
131
- record,
132
- });
133
- } catch (error) {
134
- machine = { name: record.machineName, status: 'unreachable', lastSeenAt: null };
135
- machineNote = `Machine status unavailable (${error.message}): showing local seats only.`;
136
- }
137
- const seats = (loadDaemonState()?.seats || []).map((seat) => ({
138
- ...seat,
139
- // The supervisor stamps spawn time; the session store holds real turns.
140
- lastTurnAt: readSessionLastTurn(seat.recordKey || seat.agentName),
141
- }));
142
- // Keyed by the local record, so two instances of one agent never read
143
- // each other's log, last turn or pod (F427).
144
- const logTails = {};
145
- for (const seat of seats) {
146
- const key = seat.recordKey || seat.agentName;
147
- const tail = readLogTail(daemonSeatLogPath(key), 3);
148
- logTails[key] = tail ? tail.split('\n') : [];
149
- }
150
- let states = null;
151
- let stateNote = '';
152
- if (!getToken(record.instanceUrl)) {
153
- stateNote = `Not logged in: showing local process state only. Run: ${CLI} login --instance ${record.instanceUrl}`;
154
- } else {
155
- const podIds = [...new Set(seats.map((seat) => loadAgentToken(seat.recordKey || seat.agentName)?.podId).filter(Boolean))];
156
- try {
157
- states = await reachFor(podIds);
158
- } catch (error) {
159
- stateNote = `Agent states unavailable (${error.message}): showing local process state only`;
160
- }
161
- }
162
- return renderDaemonWatch({
163
- machine, seats, logTails, states, stateNote: [machineNote, stateNote].filter(Boolean).join(' '), now: Date.now(), width: process.stdout.columns || 80, color: redraw,
164
- });
165
- } catch (error) {
166
- return daemonStatusErrorMessage(error);
167
- }
168
- };
169
- if (!redraw) {
170
- console.log(await frame());
171
- return;
172
- }
173
- let busy = false;
174
- const tick = async () => {
175
- if (busy) return;
176
- busy = true;
177
- try {
178
- process.stdout.write(`${CLEAR_SCREEN}${await frame()}\n`);
179
- } finally {
180
- busy = false;
181
- }
182
- };
183
- await tick();
184
- const timer = setInterval(tick, WATCH_INTERVAL_MS);
185
- const stop = () => {
186
- clearInterval(timer);
187
- process.stdout.write('\n');
188
- process.exit(0);
189
- };
190
- process.once('SIGINT', stop);
191
- process.once('SIGTERM', stop);
192
- };
193
-
194
- const requireDaemonRecord = () => {
195
- const record = loadDaemonRecord();
196
- if (!record) {
197
- throw new Error(`No local daemon is registered. Run: ${CLI} daemon register`);
198
- }
199
- return record;
200
- };
201
-
202
- const requireUserToken = (instance) => {
203
- const token = getToken(instance);
204
- if (!token) throw new Error(`Not logged in to ${instance}. Run: ${CLI} login --instance ${instance}`);
205
- return token;
206
- };
207
-
208
- const requireRegistrationResponse = (response) => {
209
- const machine = response?.machine;
210
- if (!machine?.id || !machine?.machineId || !machine?.name || !response?.daemonToken) {
211
- throw new Error('Server returned an incomplete machine registration. No daemon credential was stored.');
212
- }
213
- return { machine, daemonToken: response.daemonToken };
214
- };
215
-
216
- // Exported for a service-level test: persistence failure must revoke the
217
- // freshly-created machine because the raw bearer is not recoverable later.
218
- export const registerDaemonMachine = async ({
219
- client,
220
- instanceUrl,
221
- name,
222
- persist = saveDaemonRecord,
223
- host = machineHostReport(),
224
- }) => {
225
- // TASK-784: the host report is metadata only (platform, OS release major,
226
- // arch, CLI version). An older server ignores it.
227
- const { machine, daemonToken } = requireRegistrationResponse(
228
- await client.post('/api/machines', { name, host }),
229
- );
230
- const record = {
231
- machineDbId: machine.id,
232
- machineId: machine.machineId,
233
- machineName: machine.name,
234
- instanceUrl,
235
- daemonToken,
236
- registeredAt: new Date().toISOString(),
237
- };
238
-
239
- try {
240
- persist(record);
241
- } catch (error) {
242
- // The token is a one-time response. If it cannot be secured locally, tear
243
- // down the server row so it cannot remain a live, unrecoverable bearer.
244
- try {
245
- await client.del(`/api/machines/${machine.id}`);
246
- } catch (revokeError) {
247
- throw new Error(
248
- `Could not store the daemon credential and could not revoke machine ${machine.name} (${machine.id}): ${revokeError.message}`,
249
- );
250
- }
251
- throw new Error(`Could not store the daemon credential securely: ${error.message}. Machine registration was revoked.`);
252
- }
253
-
254
- return { machine, record };
255
- };
256
-
257
- // What each supervisor check-in tells the server about this computer.
258
- // Native Windows has no OS sandbox for any adapter, so it never reports
259
- // 'default': Codex there runs on its own profiles only, and a removed marker
260
- // must not read as "confined" while seats may still run. An older daemon sends
261
- // neither field, which leaves the server's record. TASK-784: the host report
262
- // rides along, so a machine an older CLI registered fills in on upgrade.
263
- export const daemonMachineReport = () => ({ sandbox: hostSandboxReport(), host: machineHostReport() });
264
-
265
- export const heartbeatDaemonMachine = async ({ client, record }) => (
266
- client.post(`/api/machines/${record.machineDbId}/heartbeat`)
267
- );
268
-
269
- export const getDaemonMachineStatus = async ({ client, record }) => {
270
- const response = await client.get('/api/machines/me');
271
- const machine = response?.machine;
272
- if (!machine || typeof machine !== 'object' || Array.isArray(machine)
273
- || !machine.id || !machine.machineId
274
- || (record && (machine.id !== record.machineDbId || machine.machineId !== record.machineId))) {
275
- throw new Error('Unexpected daemon status response; local credential kept.');
276
- }
277
- return machine;
278
- };
279
-
280
- const isRevokedDaemonKey = (error) => error?.status === 401 && [
281
- 'Daemon token revoked or unknown',
282
- 'Machine access revoked',
283
- ].includes(error?.body?.message);
284
-
285
- const isMissingDaemonMachine = (error) => error?.status === 404
286
- && error?.body?.message === 'Machine not found';
287
-
288
- const storedDaemonState = async (client, record) => {
289
- try {
290
- await getDaemonMachineStatus({ client, record });
291
- return 'active';
292
- } catch (error) {
293
- if (isRevokedDaemonKey(error)) return 'revoked';
294
- if (isMissingDaemonMachine(error)) return 'missing';
295
- throw error;
296
- }
297
- };
298
-
299
- export const daemonStatusErrorMessage = (error) => (isRevokedDaemonKey(error)
300
- ? `This computer's old key was revoked. Run: ${CLI} daemon register`
301
- : isMissingDaemonMachine(error)
302
- ? `This computer is no longer registered. Run: ${CLI} daemon register`
303
- : `Daemon status failed: ${error.message}`);
304
-
305
- // `daemon setup` on a computer that is already registered for this instance
306
- // skips login and register (it would stop at "already registered"). Only a
307
- // stored key the server still accepts counts; a revoked or missing one goes
308
- // through register, which replaces it.
309
- export const setupFindsRegistration = async ({
310
- instance, load = loadDaemonRecord, state = storedDaemonState, clientFor = createClient,
311
- }) => {
312
- const record = load();
313
- if (!record || !instance) return false;
314
- if (String(record.instanceUrl || '').replace(/\/+$/, '') !== instance) return false;
315
- const client = clientFor({ instance: record.instanceUrl, token: record.daemonToken });
316
- return (await state(client, record)) === 'active';
317
- };
318
-
319
- // Keep the old record until a new registration is securely persisted. A
320
- // network failure or a still-live daemon key must never silently mint another.
321
- export const allowRevokedDaemonReplacement = async ({ existing, client }) => {
322
- if (!existing) return null;
323
- const state = await storedDaemonState(client, existing);
324
- if (state === 'active') {
325
- throw new Error(`A local daemon is already registered for ${existing.machineName}. Run: ${CLI} daemon status`);
326
- }
327
- return state;
328
- };
329
-
330
- // Revoke remotely before unlinking locally. An owner-session 401/403 is not
331
- // proof of revocation: check the stored daemon key before discarding it.
332
- export const unregisterDaemonMachine = async ({
333
- client, daemonClient, record, remove = removeDaemonRecord,
334
- }) => {
335
- let removedRemotely = true;
336
- try {
337
- await client.del(`/api/machines/${record.machineDbId}`);
338
- } catch (error) {
339
- if (isMissingDaemonMachine(error)) {
340
- removedRemotely = false;
341
- } else if ([401, 403].includes(error?.status) && daemonClient) {
342
- if (await storedDaemonState(daemonClient, record) === 'active') throw error;
343
- removedRemotely = false;
344
- } else {
345
- throw error;
346
- }
347
- }
348
- remove();
349
- return { removedRemotely };
350
- };
351
-
352
- // The adapter names a binary on THIS machine — the one fact the server cannot
353
- // know (same reasoning as `agent run`'s env bootstrap). A server-declared
354
- // preference is honored when that CLI is installed; otherwise probe only the
355
- // established fallbacks. OpenCode must be named explicitly in runtime.adapter.
356
- export const resolveAdapterForRuntime = async (runtime, registry = { getAdapter }) => {
357
- const candidates = [runtime?.adapter, 'claude', 'codex'].filter(Boolean);
358
- for (const name of candidates) {
359
- const adapter = registry.getAdapter(name);
360
- // eslint-disable-next-line no-await-in-loop
361
- if (adapter && await adapter.detect()) return name;
362
- }
363
- return null;
364
- };
365
-
366
- /**
367
- * The daemon's first poll and heartbeat, with the Windows launcher watch armed
368
- * BEFORE them (D245). Arming first puts the EPIPE guard on stdout and stderr
369
- * before any supervision log line, and a launcher that is already gone stops
370
- * the daemon here instead of after a tick logged into a dead pipe. Returns
371
- * false when the daemon should not go on to its timers.
372
- */
373
- export const firstSupervisionPass = async ({
374
- supervisor,
375
- onLauncherGone,
376
- isEnded = () => false,
377
- armLauncherWatch = watchWindowsLauncher,
378
- }) => {
379
- let launcherGone = false;
380
- armLauncherWatch({
381
- onGone: (pid) => {
382
- launcherGone = true;
383
- onLauncherGone(pid);
384
- },
385
- });
386
- const stopped = () => launcherGone || isEnded();
387
- if (stopped()) return false;
388
- await supervisor.tick();
389
- if (stopped()) return false;
390
- await supervisor.heartbeat();
391
- return !stopped();
392
- };
393
-
394
- export const registerDaemon = (program) => {
395
- const daemon = program.command('daemon').description('Manage the local Space daemon');
396
- const signedOutNotes = async () => (await signedOutAdapters({
397
- detect: (name) => getAdapter(name)?.detect(),
398
- runStatus: runLoginStatus,
399
- })).map((name) => `Note: ${signedOutHint(name)}.`);
400
-
401
- daemon.addHelpText('after', `
402
- The daemon token is scoped to this machine and stored in a 0600 file under
403
- ~/.commonly/daemon/. It is never shown again after registration.
404
-
405
- Examples:
406
- $ ${CLI} daemon setup # install, log in, register, and start — one step
407
- $ ${CLI} daemon register --name "Sam's MacBook"
408
- $ ${CLI} daemon heartbeat
409
- $ ${CLI} daemon status --verbose
410
- $ ${CLI} daemon logs --seat my-agent -f
411
- $ ${CLI} daemon start|stop|restart
412
- $ ${CLI} daemon unregister
413
- `);
414
-
415
- daemon
416
- .command('setup')
417
- .description('From a fresh computer in one step: install the CLI under your home (no sudo, no global install), log in, register this computer, and start the daemon')
418
- .option('--instance <url-or-key>', 'Target Space instance')
419
- .option(UNSANDBOXED_FLAG, `Native Windows only: the agents this daemon runs have no sandbox (see \`${CLI} daemon install\`). Forwarded to the install step.`)
420
- .option(REPLACE_FLAG, REPLACE_HELP)
421
- .action(async (opts) => {
422
- try {
423
- const instanceUrl = resolveInstanceUrl(opts.instance);
424
- await orchestrateDaemonSetup({
425
- version: installedCliVersion(),
426
- instanceUrl,
427
- unsandboxed: Boolean(opts.unsandboxed),
428
- alreadyRegistered: (instance) => setupFindsRegistration({ instance }),
429
- replace: Boolean(opts.replace),
430
- foreignDaemon: async () => {
431
- const target = servicePaths(process.platform, homedir());
432
- const answer = await readServiceCmd(serviceOwnerQuery(process.platform)).catch(() => '');
433
- return foreignServiceRefusal({ target, loadedFile: loadedServiceFile(target.kind, answer) });
434
- },
435
- log: (line) => console.log(line),
436
- // Run the stable CLI once with the user's terminal attached, so login
437
- // device-auth and any prompts work; reject on a non-zero exit so a
438
- // failed step stops the chain before the next one.
439
- runCli: (entry, args) => new Promise((resolve, reject) => {
440
- // SETUP_CHILD_ENV: register leaves the signed-out note to setup's last line.
441
- const child = spawn(process.execPath, [entry, ...args], { stdio: 'inherit', env: { ...process.env, [SETUP_CHILD_ENV]: '1' } });
442
- child.on('error', reject);
443
- child.on('close', (code) => (code === 0
444
- ? resolve()
445
- : reject(new Error(`\`commonly ${args.join(' ')}\` exited with code ${code}`))));
446
- }),
447
- });
448
- // Growth's F370 walk: setup said all was well, and the first @mention
449
- // then failed with "claude exited 1: Not logged in".
450
- for (const line of await signedOutNotes()) console.log(line);
451
- } catch (error) {
452
- console.error(`Daemon setup failed: ${error.message}`);
453
- process.exitCode = 1;
454
- }
455
- });
456
-
457
- daemon
458
- .command('register')
459
- .description('Register this machine and securely store its daemon credential')
460
- .option('--name <name>', 'Machine name (default: this computer\'s hostname)')
461
- .option('--instance <url-or-key>', 'Target Space instance')
462
- .action(async (opts) => {
463
- try {
464
- const existing = loadDaemonRecord();
465
- if (existing) {
466
- const state = await allowRevokedDaemonReplacement({
467
- existing,
468
- client: createClient({ instance: existing.instanceUrl, token: existing.daemonToken }),
469
- });
470
- console.log(state === 'revoked'
471
- ? "This computer's old key was revoked; registering it again."
472
- : 'This computer is no longer registered; registering it again.');
473
- }
474
- const instanceUrl = resolveInstanceUrl(opts.instance);
475
- const client = createClient({ instance: instanceUrl, token: requireUserToken(opts.instance || instanceUrl) });
476
- const { machine, record } = await registerDaemonMachine({
477
- client,
478
- instanceUrl,
479
- name: String(opts.name || hostname()).trim(),
480
- });
481
- clearDaemonStop();
482
- console.log(`Registered ${machine.name}. Daemon credential stored securely.`);
483
-
484
- try {
485
- await heartbeatDaemonMachine({
486
- client: createClient({ instance: record.instanceUrl, token: record.daemonToken }),
487
- record,
488
- });
489
- console.log('Initial heartbeat accepted.');
490
- } catch (error) {
491
- console.error(`Machine is registered, but its initial heartbeat failed: ${error.message}`);
492
- console.error(`Retry with: ${CLI} daemon heartbeat`);
493
- process.exitCode = 1;
494
- }
495
- if (existing && existsSync(servicePaths(process.platform, homedir()).file)) {
496
- console.log(`Restart the installed daemon to use the new key: ${CLI} daemon restart`);
497
- }
498
- // F458: register run by hand (the F454 walk) said nothing, and the
499
- // agent looked healthy until its first message failed. Setup prints
500
- // this itself, after the install step, so its own register doesn't.
501
- if (!process.env[SETUP_CHILD_ENV]) {
502
- for (const line of await signedOutNotes()) console.log(line);
503
- }
504
- } catch (error) {
505
- console.error(`Daemon registration failed: ${error.message}`);
506
- process.exitCode = 1;
507
- }
508
- });
509
-
510
- daemon
511
- .command('unregister')
512
- .description('Revoke this machine on the server and remove its local daemon credential')
513
- .action(async () => {
514
- try {
515
- const record = requireDaemonRecord();
516
- const client = createClient({
517
- instance: record.instanceUrl,
518
- token: requireUserToken(record.instanceUrl),
519
- });
520
- const { removedRemotely } = await unregisterDaemonMachine({
521
- client,
522
- daemonClient: createClient({ instance: record.instanceUrl, token: record.daemonToken }),
523
- record,
524
- });
525
- console.log(removedRemotely
526
- ? `Unregistered ${record.machineName} and removed its local daemon credential.`
527
- : `Removed ${record.machineName}'s local daemon credential. Server access was already revoked or the machine was removed.`);
528
- } catch (error) {
529
- console.error(`Daemon unregister failed: ${error.message}`);
530
- process.exitCode = 1;
531
- }
532
- });
533
-
534
- daemon
535
- .command('heartbeat')
536
- .description('Send a machine liveness heartbeat using the stored daemon credential')
537
- .action(async () => {
538
- try {
539
- const record = requireDaemonRecord();
540
- const response = await heartbeatDaemonMachine({
541
- client: createClient({ instance: record.instanceUrl, token: record.daemonToken }),
542
- record,
543
- });
544
- console.log(`Heartbeat accepted for ${response?.machine?.name || record.machineName}.`);
545
- } catch (error) {
546
- console.error(`Daemon heartbeat failed: ${error.message}`);
547
- process.exitCode = 1;
548
- }
549
- });
550
-
551
- // ── install / uninstall (ADR-026 D1) ──────────────────────────────────────
552
- // The service manager's answer about the live daemon; rejects when it has none.
553
- const readServiceCmd = (argv) => new Promise((resolvePromise, rejectPromise) => {
554
- const child = spawn(argv[0], argv.slice(1), { stdio: ['ignore', 'pipe', 'ignore'] });
555
- const chunks = [];
556
- child.stdout.on('data', (chunk) => { chunks.push(chunk); });
557
- child.on('error', rejectPromise);
558
- child.on('close', (code) => (code === 0
559
- ? resolvePromise(decodeCmdOutput(Buffer.concat(chunks)))
560
- : rejectPromise(new Error(`${argv.join(' ')} exited ${code}`))));
561
- });
562
-
563
- const serviceDeps = () => ({
564
- // writeFile is deliberately NOT injected here: daemon-service.js's default
565
- // writer sets the 0600 mode at CREATION, and a bare `writeFileSync(file,
566
- // content, 'utf8')` dropped that — leaving the service file, which can hold
567
- // a provider key, readable at umask until the chmod a moment later.
568
- mkdirp: (dir) => { if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); },
569
- chmod: (path, mode) => chmodSync(path, mode),
570
- ensureFile: (path) => { const fd = openSync(path, 'a', 0o600); closeSync(fd); },
571
- existsFile: (file) => existsSync(file),
572
- removeFile: (file) => rmSync(file),
573
- readCmd: readServiceCmd,
574
- execCmd: (argv) => new Promise((resolvePromise, rejectPromise) => {
575
- const child = spawn(argv[0], argv.slice(1), { stdio: 'ignore' });
576
- child.on('error', rejectPromise);
577
- child.on('exit', (code) => (code === 0
578
- ? resolvePromise()
579
- : rejectPromise(new Error(`${argv.join(' ')} exited ${code}`))));
580
- }),
581
- log: (line) => console.log(line),
582
- });
583
-
584
- const runServiceAction = async (action, actionFn) => {
585
- const deps = serviceDeps();
586
- const target = servicePaths(process.platform, homedir());
587
- if (!deps.existsFile(target.file)) {
588
- throw new Error(`No installed daemon service found. Run: ${CLI} daemon install`);
589
- }
590
- await actionFn({ platform: process.platform, home: homedir(), execCmd: deps.execCmd });
591
- console.log(`Daemon ${action} requested (${target.kind}).`);
592
- };
593
-
594
- daemon
595
- .command('install')
596
- .description('Register the daemon as a login service (launchd, systemd, or a Windows scheduled task); on Linux, also turn on lingering so it starts at boot')
597
- .option(UNSANDBOXED_FLAG, 'Native Windows only: the agents this daemon runs have no sandbox, and run with your own access to files and commands. They get no connector grants. Codex can run commands in invite-only pods, where anyone in the pod, including people invited later, can direct it. Install again without it to turn it off.')
598
- .option(REPLACE_FLAG, REPLACE_HELP)
599
- .action(async (opts) => {
600
- try {
601
- if (opts.unsandboxed) {
602
- const flagRefusal = unsandboxedFlagRefusal();
603
- if (flagRefusal) throw new Error(flagRefusal);
604
- }
605
- // A service without a credential just crash-loops at boot.
606
- requireDaemonRecord();
607
- // D245: this command is the only writer of the opt-in, and installing
608
- // without the flag is how it is turned off. Written before the task
609
- // starts, so the first seats it spawns already read the answer.
610
- // N3: the marker's state before this install, so a failed install can
611
- // put it back rather than leave an opt-in with no daemon behind it.
612
- const optedInBefore = loadUnsandboxedOptIn();
613
- if (opts.unsandboxed) {
614
- saveUnsandboxedOptIn();
615
- console.log(`Agents this daemon runs will have no sandbox: they act with your own access to files and commands, and get no connector grants. Codex can run commands in invite-only pods, where anyone in the pod, people you invite later included, can direct it. To turn this off, run: ${CLI} daemon install`);
616
- } else if (clearUnsandboxedOptIn()) {
617
- console.log('Turned off running without a sandbox: on Windows, agents this daemon runs are refused again until you choose otherwise.');
618
- }
619
- // Same class one layer over (TASK-049): a service without the SEATS'
620
- // provider key comes up clean, and every pi seat then dies inside its
621
- // adapter. The keys are captured here because this is the only moment
622
- // the operator's shell is in reach; the ones that are missing are named
623
- // by installDaemonService rather than left to be discovered per seat.
624
- let installed;
625
- try {
626
- installed = await installDaemonService({
627
- ...serviceDeps(),
628
- replace: Boolean(opts.replace),
629
- providerKeyEnvNames: providerKeyEnvNames(),
630
- optionalEnvNames: [UPDATE_CHECK_OPT_OUT_ENV],
631
- });
632
- } catch (error) {
633
- if (optedInBefore) saveUnsandboxedOptIn(); else clearUnsandboxedOptIn();
634
- throw error;
635
- }
636
- console.log(installSummary(installed));
637
- } catch (error) {
638
- console.error(`Daemon install failed: ${error.message}`);
639
- process.exitCode = 1;
640
- }
641
- });
642
-
643
- daemon
644
- .command('uninstall')
645
- .description('Remove the daemon login service (agents stop being supervised on this machine)')
646
- .action(async () => {
647
- try {
648
- await uninstallDaemonService(serviceDeps());
649
- } catch (error) {
650
- console.error(`Daemon uninstall failed: ${error.message}`);
651
- process.exitCode = 1;
652
- } finally {
653
- // N4: an uninstall that half-failed still turns the opt-in off; a
654
- // later install has to ask for it again.
655
- clearUnsandboxedOptIn();
656
- }
657
- });
658
-
659
- daemon
660
- .command('start')
661
- .description('Start the installed daemon service and return to the terminal')
662
- .action(async () => {
663
- try {
664
- await runServiceAction('start', startDaemonService);
665
- } catch (error) {
666
- console.error(`Daemon start failed: ${error.message}`);
667
- process.exitCode = 1;
668
- }
669
- });
670
-
671
- daemon
672
- .command('stop')
673
- .description('Stop the installed daemon service')
674
- .action(async () => {
675
- try {
676
- await runServiceAction('stop', stopDaemonService);
677
- } catch (error) {
678
- console.error(`Daemon stop failed: ${error.message}`);
679
- process.exitCode = 1;
680
- }
681
- });
682
-
683
- daemon
684
- .command('restart')
685
- .description('Restart the installed daemon service and return to the terminal')
686
- .action(async () => {
687
- try {
688
- await runServiceAction('restart', restartDaemonService);
689
- } catch (error) {
690
- console.error(`Daemon restart failed: ${error.message}`);
691
- process.exitCode = 1;
692
- }
693
- });
694
-
695
- // ── run (ADR-026 Phase 2, slice 2) ────────────────────────────────────────
696
- daemon
697
- .command('run')
698
- .description('Run the resident supervisor: adopt requested agents, keep bound agents running, report per-agent state')
699
- .option('--poll <ms>', 'Work-list poll interval in ms', String(DEFAULT_POLL_MS))
700
- .option('--heartbeat <ms>', 'Heartbeat interval in ms', String(DEFAULT_HEARTBEAT_MS))
701
- .option('--foreground', 'Keep the supervisor attached to this terminal (default for direct invocation)')
702
- .option('--instance <url-or-key>', 'Confirm the instance this computer is registered with')
703
- .action(async (opts) => {
704
- let record;
705
- try {
706
- const terminal = loadDaemonStop();
707
- if (terminal) {
708
- console.error(`Daemon stopped: no valid local registration. Run: ${CLI} daemon register`);
709
- process.exitCode = process.platform === 'darwin' ? 0 : terminal.exitCode;
710
- return;
711
- }
712
- record = requireDaemonRecord();
713
- const instanceRefusal = daemonRunInstanceRefusal(record, opts.instance);
714
- if (instanceRefusal) {
715
- console.error(instanceRefusal);
716
- process.exitCode = 1;
717
- return;
718
- }
719
- const stopped = loadDaemonStop(record);
720
- if (stopped) {
721
- console.error(`Daemon stopped: this computer's key was revoked. Run: ${CLI} daemon register`);
722
- // launchd restarts the first nonzero terminal exit once; a saved
723
- // verdict makes that second invocation successful and ends the loop.
724
- process.exitCode = process.platform === 'darwin' ? 0 : stopped.exitCode;
725
- return;
726
- }
727
- const client = createClient({ instance: record.instanceUrl, token: record.daemonToken });
728
- const logsDir = daemonLogsDir();
729
- if (!existsSync(logsDir)) mkdirSync(logsDir, { recursive: true });
730
- chmodSync(logsDir, 0o700);
731
- const stampLog = (line) => console.log(`${new Date().toISOString()} ${line}`);
732
-
733
- // TASK-049: fail loudly, once, naming the variable.
734
- //
735
- // A service-started daemon inherits a clean environment, so a provider
736
- // key that lives only in the operator's shell is absent here; a pi seat
737
- // then dies at adapter start with `COMMONLY_LITELLM_KEY is not set` and
738
- // the operator sees crash loops with no cause at daemon level. The
739
- // warning names the seat that needs the key, when that seat starts:
740
- // at daemon start it went to every BYO user, who needs none of them
741
- // (Growth's F370 walk). Not fatal: the daemon's other seats are
742
- // unaffected.
743
- const warnedKeys = new Set();
744
- const warnMissingSeatKey = (agentName) => {
745
- let saved = null;
746
- try { saved = loadAgentToken(agentName); } catch { return; }
747
- const name = seatProviderKeyEnv({ adapter: saved?.adapter, environment: saved?.environment });
748
- if (!name || process.env[name] || warnedKeys.has(`${agentName}:${name}`)) return;
749
- warnedKeys.add(`${agentName}:${name}`);
750
- stampLog(`WARNING: [${agentName}] needs ${name}, which is not set in this environment, so it will fail to start (a provider.keyFile avoids this); export it and re-run: ${CLI} daemon install (docs/agents/daemon-service-environment.md)`);
751
- };
752
- // TASK-660: the daemon outlives many publishes, so say once at start-up
753
- // when it is stale. Not awaited: supervision never waits on the registry.
754
- startUpdateNotice({ installedVersion: installedCliVersion(), log: stampLog });
755
-
756
- let pollTimer;
757
- let heartbeatTimer;
758
- let supervisor;
759
- let terminalEnded = false;
760
- const terminalAuth = (error) => isRevokedDaemonKey(error)
761
- || isMissingDaemonMachine(error);
762
- const onTerminalAuthError = () => {
763
- terminalEnded = true;
764
- let saved = false;
765
- try {
766
- saveDaemonStop(record, 77);
767
- saved = true;
768
- } catch (error) {
769
- stampLog(`could not save revoked-machine stop marker: ${error.message}`);
770
- }
771
- stampLog(`machine key revoked; stopping supervised agents. Run: ${CLI} daemon register`);
772
- clearInterval(pollTimer);
773
- clearInterval(heartbeatTimer);
774
- supervisor.stop();
775
- // Without the marker launchd would restart forever. A successful
776
- // exit parks it on macOS; systemd honors 77 directly.
777
- process.exitCode = saved || process.platform !== 'darwin' ? 77 : 0;
778
- };
779
- const runtimeInventory = createInventoryCache((harnesses) => collectRuntimeInventory(getAdapter, undefined, harnesses, { loginState }));
780
- supervisor = createDaemonSupervisor({
781
- record,
782
- client,
783
- isTerminalAuthError: terminalAuth,
784
- onTerminalAuthError,
785
- // One child per agent, logging to its own file. The child is the
786
- // ordinary `commonly agent run <name>` — the daemon is its
787
- // supervisor, never its replacement (D6).
788
- // `recordKey` is the local record: `<name>`, or `<name>@<id>` for
789
- // another instance, which `agent run` resolves the same way (F427).
790
- spawnChild: (recordKey) => {
791
- warnMissingSeatKey(recordKey);
792
- const seatLog = daemonSeatLogPath(recordKey);
793
- const out = openSync(seatLog, 'a', 0o600);
794
- chmodSync(seatLog, 0o600);
795
- return spawn(process.execPath, [process.argv[1], 'agent', 'run', recordKey], {
796
- stdio: ['ignore', out, out],
797
- // Windows: never flash a console window per seat (no-op elsewhere).
798
- windowsHide: true,
799
- env: { ...process.env, COMMONLY_MANAGED_RUNNER: '1' },
800
- });
801
- },
802
- loadToken: loadAgentToken,
803
- saveToken: saveAgentToken,
804
- machineReport: daemonMachineReport,
805
- runtimeInventory,
806
- resolveAdapter: (runtime) => resolveAdapterForRuntime(runtime),
807
- log: stampLog,
808
- persistState: (seats) => saveDaemonState({
809
- machineName: record.machineName,
810
- machineDbId: record.machineDbId,
811
- updatedAt: new Date().toISOString(),
812
- seats: seats.map((seat) => ({
813
- ...seat,
814
- lastTurnAt: getLastTurn(seat.recordKey || seat.agentName) || seat.lastTurnAt,
815
- })),
816
- }),
817
- });
818
-
819
- stampLog(`daemon supervising for ${record.machineName} — poll ${opts.poll}ms, heartbeat ${opts.heartbeat}ms (ctrl+c to stop)`);
820
- const shutdown = () => {
821
- stampLog('daemon stopping — terminating supervised agents');
822
- clearInterval(pollTimer);
823
- clearInterval(heartbeatTimer);
824
- supervisor.stop();
825
- process.exit(0);
826
- };
827
- // D245: under the Windows launcher, the launcher's exit is the stop
828
- // signal. Armed before the first tick, so the EPIPE guard covers its
829
- // log lines and a launcher already gone stops the daemon at once.
830
- const started = await firstSupervisionPass({
831
- supervisor,
832
- onLauncherGone: (pid) => {
833
- // stdout and stderr already swallow EPIPE (armed with the watch).
834
- try { stampLog(`launcher ${pid} has exited`); } catch { /* stdout is gone */ }
835
- shutdown();
836
- },
837
- isEnded: () => terminalEnded,
838
- });
839
- if (!started) return;
840
- const runtimeFailure = (error) => {
841
- if (terminalEnded) return;
842
- terminalEnded = true;
843
- stampLog(`daemon runtime failed: ${error.message}`);
844
- clearInterval(pollTimer);
845
- clearInterval(heartbeatTimer);
846
- supervisor.stop();
847
- process.exitCode = 75;
848
- };
849
- pollTimer = setInterval(() => { void supervisor.tick().catch(runtimeFailure); }, Number(opts.poll) || DEFAULT_POLL_MS);
850
- heartbeatTimer = setInterval(() => { void supervisor.heartbeat().catch(runtimeFailure); }, Number(opts.heartbeat) || DEFAULT_HEARTBEAT_MS);
851
- process.on('SIGINT', shutdown);
852
- process.on('SIGTERM', shutdown);
853
- } catch (error) {
854
- console.error(`Daemon run failed: ${error.message}`);
855
- const badConfig = !record || error.message.startsWith('Daemon credential file');
856
- if (badConfig) {
857
- try {
858
- saveDaemonStop(record || { machineDbId: 'unregistered' }, 78);
859
- } catch (markerError) {
860
- console.error(`Could not save daemon stop marker: ${markerError.message}`);
861
- if (process.platform === 'darwin') {
862
- process.exitCode = 0;
863
- return;
864
- }
865
- }
866
- }
867
- process.exitCode = badConfig ? 78 : 75;
868
- }
869
- });
870
-
871
- daemon
872
- .command('status')
873
- .description('Show the server-derived liveness of this machine')
874
- .option('--verbose', 'Include locally supervised seat state and process details')
875
- .option('--watch', 'Redraw the machine and agent view every 2 s until Ctrl-C')
876
- .action(async (opts) => {
877
- if (opts.watch) {
878
- await watchDaemonStatus();
879
- return;
880
- }
881
- try {
882
- const record = requireDaemonRecord();
883
- const machine = await getDaemonMachineStatus({
884
- client: createClient({ instance: record.instanceUrl, token: record.daemonToken }),
885
- record,
886
- });
887
- const lastSeen = machine.lastSeenAt ? new Date(machine.lastSeenAt).toLocaleString() : 'never';
888
- console.log(`${machine.name}: ${machine.status} (last heartbeat: ${lastSeen})`);
889
- if (loadUnsandboxedOptIn()) console.log(`Sandbox: unsandboxed (opted in). Agents run with your own access and get no connector grants, and anyone in an invite-only pod can direct a Codex agent there. Turn off: ${CLI} daemon install`);
890
- if (opts.verbose) {
891
- const local = loadDaemonState();
892
- if (!local) {
893
- console.log('Local supervisor state: unavailable (daemon has not written state yet).');
894
- } else if (!local.seats.length) {
895
- console.log('Local supervisor state: no supervised seats.');
896
- } else {
897
- console.log('Local supervised seats:');
898
- for (const seat of local.seats) console.log(formatSeatLine(seat));
899
- }
900
- }
901
- } catch (error) {
902
- console.error(daemonStatusErrorMessage(error));
903
- process.exitCode = 1;
904
- }
905
- });
906
-
907
- daemon
908
- .command('logs')
909
- .description('Show daemon or per-seat logs')
910
- .option('--seat <name>', 'Show one supervised seat log instead of the daemon log')
911
- .option('-f, --follow', 'Continue printing appended log output')
912
- .option('--lines <n>', 'Number of trailing lines to show', '50')
913
- .action(async (opts) => {
914
- const path = opts.seat ? daemonSeatLogPath(opts.seat) : daemonLogPath();
915
- const printTail = () => {
916
- const output = readLogTail(path, opts.lines);
917
- if (output === null) {
918
- console.error(`No log found at ${path}`);
919
- return false;
920
- }
921
- if (output) console.log(output);
922
- return true;
923
- };
924
- if (!printTail() || !opts.follow) return;
925
- const watcher = watch(path, () => printTail());
926
- const close = () => {
927
- watcher.close();
928
- process.exit(0);
929
- };
930
- process.once('SIGINT', close);
931
- process.once('SIGTERM', close);
932
- });
933
- };