@cydm/happy-elves 0.1.0-beta.270 → 0.1.0-beta.271

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.
@@ -32,6 +32,7 @@ const valueFlags = new Set([
32
32
  "memory-project",
33
33
  "memory-top-n",
34
34
  "name",
35
+ "package-url",
35
36
  "model",
36
37
  "mode",
37
38
  "output",
@@ -61,6 +62,7 @@ const valueFlags = new Set([
61
62
  "sessions",
62
63
  "session",
63
64
  "secret",
65
+ "sha256",
64
66
  "since",
65
67
  "source",
66
68
  "stop-on",
@@ -81,6 +83,8 @@ const valueFlags = new Set([
81
83
  "url",
82
84
  "turn",
83
85
  "until",
86
+ "version",
87
+ "wait-timeout",
84
88
  ]);
85
89
  const booleanFlags = new Set([
86
90
  "all",
@@ -30,6 +30,10 @@ Check local controller config, relay reachability, and daemon state.
30
30
  happy-elves remote add-controller [--device-id <id>] [--device-name <name>] --json
31
31
  happy-elves remote rename <deviceId> --name <name> --json
32
32
  happy-elves remote revoke <deviceId> --json
33
+ happy-elves remote daemon-status <machineId> [--json]
34
+ happy-elves remote daemon-restart <machineId> [--wait] [--timeout 2m] [--json]
35
+ happy-elves remote daemon-update <machineId> [--version latest|0.1.0-beta.N] [--package-url <url> --sha256 <hex>] [--wait] [--timeout 10m] [--json]
36
+ happy-elves remote daemon-logs <machineId> [--tail 100] [--json]
33
37
  `,
34
38
  machine: `Usage:
35
39
  happy-elves machine list [--verbose] [--json]
@@ -1,5 +1,5 @@
1
1
  import os from "node:os";
2
- import { ControllerClient, configPath, ok, parseControllerJoinUrl, parseDurationMs, randomId, readConfig, relayHealth, requirePositional, requireRelayUrl, requireString, showMachine, wantsJson, wantsVerbose, writeConfig } from "./lib/index.js";
2
+ import { CliError, ControllerClient, configPath, ok, parseControllerJoinUrl, parseDurationMs, randomId, readConfig, relayHealth, requirePositional, requireRelayUrl, requireString, showMachine, wantsJson, wantsVerbose, writeConfig } from "./lib/index.js";
3
3
  export async function handleRemote({ domain, action, positional, flags }) {
4
4
  if (domain === "remote" && action === "devices") {
5
5
  const config = await readConfig(flags);
@@ -81,6 +81,56 @@ export async function handleRemote({ domain, action, positional, flags }) {
81
81
  ok("remote.status", data);
82
82
  return true;
83
83
  }
84
+ if (domain === "remote" && action === "daemon-status") {
85
+ const config = await readConfig(flags);
86
+ const machineId = requirePositional(positional[0], "machineId");
87
+ const result = await new ControllerClient(config).daemonLifecycle({
88
+ machineId,
89
+ action: "status",
90
+ waitTimeoutMs: parseDurationMs(flags["wait-timeout"], 45_000),
91
+ });
92
+ printDaemonLifecycle("remote.daemon-status", result, flags);
93
+ return true;
94
+ }
95
+ if (domain === "remote" && action === "daemon-logs") {
96
+ const config = await readConfig(flags);
97
+ const machineId = requirePositional(positional[0], "machineId");
98
+ const tail = typeof flags.tail === "string" ? Number(flags.tail) : 100;
99
+ if (!Number.isInteger(tail) || tail <= 0)
100
+ throw new CliError("--tail must be a positive integer", "INVALID_ARGUMENT");
101
+ const result = await new ControllerClient(config).daemonLifecycle({
102
+ machineId,
103
+ action: "logs",
104
+ tail,
105
+ waitTimeoutMs: parseDurationMs(flags["wait-timeout"], 45_000),
106
+ });
107
+ printDaemonLifecycle("remote.daemon-logs", result, flags);
108
+ return true;
109
+ }
110
+ if (domain === "remote" && (action === "daemon-restart" || action === "daemon-update")) {
111
+ const config = await readConfig(flags);
112
+ const client = new ControllerClient(config);
113
+ const machineId = requirePositional(positional[0], "machineId");
114
+ const isUpdate = action === "daemon-update";
115
+ const result = await client.daemonLifecycle({
116
+ machineId,
117
+ action: isUpdate ? "update" : "restart",
118
+ targetVersion: isUpdate ? (typeof flags.version === "string" ? flags.version : "latest") : undefined,
119
+ packageUrl: isUpdate && typeof flags["package-url"] === "string" ? flags["package-url"] : undefined,
120
+ sha256: isUpdate && typeof flags.sha256 === "string" ? flags.sha256 : undefined,
121
+ waitTimeoutMs: parseDurationMs(flags["wait-timeout"], 45_000),
122
+ });
123
+ let finalResult = result;
124
+ if (result.accepted === true && flags.wait) {
125
+ finalResult = await waitForDaemonLifecycle(client, machineId, result.requestId, isUpdate ? "update" : "restart", {
126
+ timeoutMs: parseDurationMs(flags.timeout, isUpdate ? 10 * 60_000 : 2 * 60_000),
127
+ });
128
+ }
129
+ printDaemonLifecycle(isUpdate ? "remote.daemon-update" : "remote.daemon-restart", finalResult, flags);
130
+ if (finalResult.accepted === false || finalResult.updateState?.status === "failed")
131
+ process.exitCode = 1;
132
+ return true;
133
+ }
84
134
  if (domain === "remote" &&
85
135
  ((action === "pairing" && positional[0] === "new") || (action === "machine-pairing" && positional[0] === "new"))) {
86
136
  const config = await readConfig(flags);
@@ -148,4 +198,83 @@ export async function handleRemote({ domain, action, positional, flags }) {
148
198
  }
149
199
  return false;
150
200
  }
201
+ function printDaemonLifecycle(type, result, flags) {
202
+ const success = result.accepted !== false && result.updateState?.status !== "failed";
203
+ if (wantsJson(flags)) {
204
+ ok(type, result, { requestId: result.requestId, machineId: result.machineId }, success);
205
+ return;
206
+ }
207
+ const state = result.updateState;
208
+ const summary = [
209
+ `Machine: ${result.machineId}`,
210
+ `Action: ${result.action}`,
211
+ `Version: ${result.version ?? "-"}`,
212
+ `Running: ${result.running === undefined ? "-" : result.running ? "yes" : "no"}`,
213
+ `Install: ${result.installMode ?? "-"}`,
214
+ ];
215
+ if (result.updateSupported === false)
216
+ summary.push(`Update: unsupported (${result.updateUnsupportedReason ?? "unknown"})`);
217
+ if (result.accepted !== undefined)
218
+ summary.push(`Accepted: ${result.accepted ? "yes" : "no"}`);
219
+ if (state) {
220
+ summary.push(`Update state: ${state.status}`);
221
+ if (state.targetVersion)
222
+ summary.push(`Target: ${state.targetVersion}`);
223
+ if (state.toVersion)
224
+ summary.push(`Installed: ${state.toVersion}`);
225
+ if (state.logPath)
226
+ summary.push(`Log: ${state.logPath}`);
227
+ if (state.error)
228
+ summary.push(`Error: ${state.error}`);
229
+ }
230
+ for (const line of summary)
231
+ console.log(line);
232
+ if (result.logs?.length) {
233
+ console.log("Logs:");
234
+ for (const line of result.logs)
235
+ console.log(line);
236
+ }
237
+ }
238
+ async function waitForDaemonLifecycle(client, machineId, requestId, action, options) {
239
+ const deadline = Date.now() + Math.max(1, options.timeoutMs);
240
+ let lastResult;
241
+ let lastError;
242
+ while (Date.now() < deadline) {
243
+ try {
244
+ const result = await client.daemonLifecycle({ machineId, action: "status", waitTimeoutMs: 15_000 });
245
+ lastResult = result;
246
+ const state = result.updateState;
247
+ if (state?.requestId === requestId) {
248
+ if (state.status === "succeeded" || state.status === "failed")
249
+ return result;
250
+ }
251
+ else if (action === "restart" && result.running === true) {
252
+ return result;
253
+ }
254
+ }
255
+ catch (error) {
256
+ lastError = error;
257
+ const code = errorCode(error);
258
+ if (code !== "MACHINE_OFFLINE" && code !== "COMMAND_TIMEOUT" && code !== "RELAY_CONNECT_TIMEOUT")
259
+ throw error;
260
+ }
261
+ await sleep(1000);
262
+ }
263
+ if (lastResult)
264
+ return {
265
+ ...lastResult,
266
+ accepted: false,
267
+ updateUnsupportedReason: `WAIT_TIMEOUT_${requestId}`,
268
+ };
269
+ throw new CliError(`Timed out waiting for daemon ${action}: ${errorMessage(lastError)}`, "COMMAND_TIMEOUT");
270
+ }
271
+ function errorCode(error) {
272
+ return typeof error === "object" && error !== null && "code" in error ? String(error.code) : undefined;
273
+ }
274
+ function errorMessage(error) {
275
+ return error instanceof Error ? error.message : String(error ?? "unknown");
276
+ }
277
+ function sleep(ms) {
278
+ return new Promise((resolve) => setTimeout(resolve, ms));
279
+ }
151
280
  //# sourceMappingURL=remote.js.map
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves-cli",
3
- "version": "0.1.0-beta.270",
3
+ "version": "0.1.0-beta.271",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -8,6 +8,8 @@ export declare const loopStoreLockPath: string;
8
8
  export declare const gatewayStorePath: string;
9
9
  export declare const scheduleStorePath: string;
10
10
  export declare const memoryRootDir: string;
11
+ export declare const updatesDir: string;
12
+ export declare const updateStatePath: string;
11
13
  export declare const pidPath: string;
12
14
  export declare const daemonScriptPath: string;
13
15
  export declare const daemonScriptRelativePath: string;
@@ -13,6 +13,8 @@ export const loopStoreLockPath = path.join(configDir, "loops.json.lock");
13
13
  export const gatewayStorePath = path.join(configDir, "gateway.json");
14
14
  export const scheduleStorePath = path.join(configDir, "schedules.json");
15
15
  export const memoryRootDir = path.join(configDir, "memory");
16
+ export const updatesDir = path.join(configDir, "updates");
17
+ export const updateStatePath = path.join(configDir, "update-state.json");
16
18
  export const pidPath = path.join(configDir, "daemon.pid");
17
19
  export const daemonScriptPath = fileURLToPath(new URL(import.meta.url.endsWith(".ts") ? "./cli.ts" : "./cli.js", import.meta.url));
18
20
  export const daemonScriptRelativePath = path.relative(process.cwd(), daemonScriptPath);
@@ -12,6 +12,7 @@ import { handleFork, handleRename, handleRewind } from "../session/primitives.js
12
12
  import { handleReconcileTurn } from "../session/reconcile.js";
13
13
  import { parseRelayMessage, relayWsUrl } from "../relay-http.js";
14
14
  import { handleDevExec, handleDiagnose } from "./devtools.js";
15
+ import { handleDaemonLifecycleCommand } from "./lifecycle.js";
15
16
  import { registerMachine } from "./register.js";
16
17
  import { clearActiveRelaySocket, configureRelaySender, flushRelayOutbox, handleMachineMessageAck, send, sendCommandResponse, setActiveRelaySocket, } from "./send.js";
17
18
  async function handleServerMessage(ws, config, message) {
@@ -63,6 +64,10 @@ async function handleServerMessage(ws, config, message) {
63
64
  await handleDevExec(ws, config, message);
64
65
  return;
65
66
  }
67
+ if (message.type === "machine:daemonLifecycle") {
68
+ await handleDaemonLifecycleCommand(ws, config, message);
69
+ return;
70
+ }
66
71
  if (message.type === "machine:gateway") {
67
72
  await handleGatewayCommand(ws, config, message);
68
73
  return;
@@ -0,0 +1,7 @@
1
+ import type { MachineCommand } from "../../../../packages/shared/dist/index.js";
2
+ import type { DaemonConfig } from "../types.js";
3
+ type DaemonLifecycleCommand = Extract<MachineCommand, {
4
+ type: "machine:daemonLifecycle";
5
+ }>;
6
+ export declare function handleDaemonLifecycleCommand(ws: WebSocket, config: DaemonConfig, command: DaemonLifecycleCommand): Promise<void>;
7
+ export {};
@@ -0,0 +1,476 @@
1
+ import { spawn } from "node:child_process";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import daemonPackage from "../../package.json" with { type: "json" };
5
+ import { appendAudit } from "../audit.js";
6
+ import { auditLogPath, configDir, daemonScriptPath, pidPath, updateStatePath, updatesDir } from "../paths.js";
7
+ import { localStatus } from "../process.js";
8
+ import { claimCommandRequest, sendCommandResponse } from "./send.js";
9
+ const defaultTargetPackage = "@cydm/happy-elves";
10
+ const maxLogTail = 2000;
11
+ export async function handleDaemonLifecycleCommand(ws, config, command) {
12
+ if (!claimCommandRequest(ws, command.requestId))
13
+ return;
14
+ const result = await executeDaemonLifecycleCommand(command);
15
+ await appendAudit({
16
+ machineId: config.machineId,
17
+ actor: "user",
18
+ action: `daemon.lifecycle.${command.action}`,
19
+ summary: `Daemon lifecycle ${command.action}: ${result.accepted === false ? "rejected" : "accepted"}`,
20
+ evidence: {
21
+ requestId: command.requestId,
22
+ targetVersion: command.targetVersion,
23
+ packageUrl: command.packageUrl ? redactUrl(command.packageUrl) : undefined,
24
+ status: result.updateState?.status,
25
+ updateSupported: result.updateSupported,
26
+ updateUnsupportedReason: result.updateUnsupportedReason,
27
+ },
28
+ });
29
+ await sendCommandResponse(ws, {
30
+ type: "machine:daemonLifecycleResult",
31
+ requestId: command.requestId,
32
+ machineId: config.machineId,
33
+ result,
34
+ });
35
+ if ((command.action === "restart" || command.action === "update") && result.accepted === true && result.updateState) {
36
+ spawnUpdaterRunner(result.updateState.requestId);
37
+ }
38
+ }
39
+ async function executeDaemonLifecycleCommand(command) {
40
+ if (command.action === "status") {
41
+ return await lifecycleStatus("status");
42
+ }
43
+ if (command.action === "logs") {
44
+ return {
45
+ ...(await lifecycleStatus("logs")),
46
+ logs: await readLifecycleLogTail(command.tail ?? 100),
47
+ };
48
+ }
49
+ if (command.action === "restart") {
50
+ const state = await createUpdateIntent(command, "restart");
51
+ await writeUpdateState(state);
52
+ await writeRunnerFile(state.requestId, runnerInput(state, "restart"));
53
+ return {
54
+ ...(await lifecycleStatus("restart")),
55
+ accepted: true,
56
+ updateState: state,
57
+ };
58
+ }
59
+ if (command.action === "update") {
60
+ const status = await lifecycleStatus("update");
61
+ if (!status.updateSupported) {
62
+ return {
63
+ ...status,
64
+ accepted: false,
65
+ updateUnsupportedReason: status.updateUnsupportedReason ?? "UPDATE_UNSUPPORTED_INSTALL_MODE",
66
+ };
67
+ }
68
+ if (command.packageUrl && !command.sha256) {
69
+ return {
70
+ ...status,
71
+ accepted: false,
72
+ updateUnsupportedReason: "PACKAGE_SHA256_REQUIRED",
73
+ };
74
+ }
75
+ if (command.sha256 && !command.packageUrl) {
76
+ return {
77
+ ...status,
78
+ accepted: false,
79
+ updateUnsupportedReason: "PACKAGE_URL_REQUIRED_FOR_SHA256",
80
+ };
81
+ }
82
+ const state = await createUpdateIntent(command, "update");
83
+ await writeUpdateState(state);
84
+ await writeRunnerFile(state.requestId, runnerInput(state, "update"));
85
+ return {
86
+ ...status,
87
+ accepted: true,
88
+ updateState: state,
89
+ };
90
+ }
91
+ return {
92
+ action: command.action,
93
+ accepted: false,
94
+ updateUnsupportedReason: `Unsupported daemon lifecycle action: ${command.action}`,
95
+ };
96
+ }
97
+ async function lifecycleStatus(action) {
98
+ const local = await localStatus();
99
+ const installMode = detectInstallMode();
100
+ const updateState = await readUpdateState();
101
+ return {
102
+ action,
103
+ version: daemonPackage.version,
104
+ pid: local.pid,
105
+ running: local.running,
106
+ platform: process.platform,
107
+ arch: process.arch,
108
+ installMode,
109
+ updateSupported: installMode === "npm-global",
110
+ ...(installMode === "npm-global" ? {} : { updateUnsupportedReason: "UPDATE_UNSUPPORTED_DEV_INSTALL" }),
111
+ ...(updateState ? { updateState } : {}),
112
+ };
113
+ }
114
+ function detectInstallMode() {
115
+ if (process.env.HAPPY_ELVES_TEST_INSTALL_MODE === "npm-global")
116
+ return "npm-global";
117
+ if (process.env.HAPPY_ELVES_TEST_INSTALL_MODE === "repo-dev")
118
+ return "repo-dev";
119
+ if (daemonPackage.name === "@cydm/happy-elves-daemon")
120
+ return "npm-global";
121
+ if (daemonPackage.private === true)
122
+ return "repo-dev";
123
+ return "unknown";
124
+ }
125
+ async function createUpdateIntent(command, action) {
126
+ const logPath = path.join(updatesDir, command.requestId, "updater.log");
127
+ const targetVersion = action === "update" ? (command.targetVersion ?? "latest") : undefined;
128
+ const packageSpec = action === "update" && !command.packageUrl ? `${defaultTargetPackage}@${targetVersion}` : undefined;
129
+ return {
130
+ requestId: command.requestId,
131
+ action,
132
+ ...(targetVersion ? { targetVersion } : {}),
133
+ ...(packageSpec ? { packageSpec } : {}),
134
+ ...(command.packageUrl ? { packageUrl: command.packageUrl } : {}),
135
+ ...(command.sha256 ? { sha256: command.sha256 } : {}),
136
+ status: "accepted",
137
+ fromVersion: daemonPackage.version,
138
+ startedAt: new Date().toISOString(),
139
+ logPath,
140
+ };
141
+ }
142
+ async function readUpdateState() {
143
+ try {
144
+ return JSON.parse(await fs.readFile(updateStatePath, "utf8"));
145
+ }
146
+ catch (error) {
147
+ if (error.code === "ENOENT")
148
+ return undefined;
149
+ return undefined;
150
+ }
151
+ }
152
+ async function writeUpdateState(state) {
153
+ await fs.mkdir(path.dirname(updateStatePath), { recursive: true });
154
+ const tempPath = `${updateStatePath}.${process.pid}.${Date.now()}.tmp`;
155
+ await fs.writeFile(tempPath, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
156
+ await fs.rename(tempPath, updateStatePath);
157
+ }
158
+ function runnerInput(state, action) {
159
+ return {
160
+ action,
161
+ configDir,
162
+ daemonScriptPath,
163
+ homeEnv: process.env.HAPPY_ELVES_HOME ?? configDir,
164
+ logPath: state.logPath,
165
+ packageSpec: state.packageSpec,
166
+ packageUrl: state.packageUrl,
167
+ pidPath,
168
+ requestId: state.requestId,
169
+ sha256: state.sha256,
170
+ startMode: action === "restart" ? "current-script" : "global-cli",
171
+ statePath: updateStatePath,
172
+ targetPackage: defaultTargetPackage,
173
+ targetVersion: state.targetVersion,
174
+ };
175
+ }
176
+ async function writeRunnerFile(requestId, input) {
177
+ const runnerDir = path.join(updatesDir, requestId);
178
+ await fs.mkdir(runnerDir, { recursive: true });
179
+ await fs.writeFile(path.join(runnerDir, "runner.mjs"), renderRunnerScript(input), { mode: 0o700 });
180
+ }
181
+ function spawnUpdaterRunner(requestId) {
182
+ if (process.env.HAPPY_ELVES_TEST_NO_SPAWN_UPDATER === "1")
183
+ return;
184
+ const runnerPath = path.join(updatesDir, requestId, "runner.mjs");
185
+ const child = spawn(process.execPath, [runnerPath], {
186
+ cwd: process.cwd(),
187
+ detached: true,
188
+ env: { ...process.env, HAPPY_ELVES_HOME: process.env.HAPPY_ELVES_HOME ?? configDir },
189
+ stdio: "ignore",
190
+ windowsHide: true,
191
+ });
192
+ child.unref();
193
+ }
194
+ async function readLifecycleLogTail(tail) {
195
+ const boundedTail = Math.max(1, Math.min(maxLogTail, Math.floor(tail)));
196
+ const state = await readUpdateState();
197
+ const paths = [auditLogPath, state?.logPath].filter((item) => Boolean(item));
198
+ const lines = [];
199
+ for (const filePath of paths) {
200
+ try {
201
+ const text = await fs.readFile(filePath, "utf8");
202
+ const label = filePath === auditLogPath ? "daemon" : "updater";
203
+ for (const line of text.split(/\r?\n/u).filter(Boolean).slice(-boundedTail)) {
204
+ lines.push(`[${label}] ${line}`);
205
+ }
206
+ }
207
+ catch (error) {
208
+ if (error.code !== "ENOENT") {
209
+ lines.push(`[log-error] ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
210
+ }
211
+ }
212
+ }
213
+ return lines.slice(-boundedTail);
214
+ }
215
+ function redactUrl(value) {
216
+ try {
217
+ const url = new URL(value);
218
+ if (url.password)
219
+ url.password = "redacted";
220
+ if (url.username)
221
+ url.username = "redacted";
222
+ return url.toString();
223
+ }
224
+ catch {
225
+ return value;
226
+ }
227
+ }
228
+ function renderRunnerScript(input) {
229
+ const runner = {
230
+ ...input,
231
+ startDelayMs: 750,
232
+ };
233
+ const inputJson = JSON.stringify(runner);
234
+ return `#!/usr/bin/env node
235
+ import { spawn } from "node:child_process";
236
+ import { createHash } from "node:crypto";
237
+ import fs from "node:fs/promises";
238
+ import path from "node:path";
239
+
240
+ const input = ${inputJson};
241
+
242
+ main().catch(async (error) => {
243
+ await fail(error);
244
+ process.exitCode = 1;
245
+ });
246
+
247
+ async function main() {
248
+ await fs.mkdir(path.dirname(input.logPath), { recursive: true });
249
+ await delay(input.startDelayMs);
250
+ await updateState({ status: "running" });
251
+ await log("runner started " + JSON.stringify({ action: input.action, targetVersion: input.targetVersion, packageSpec: input.packageSpec, packageUrl: input.packageUrl ? redactUrl(input.packageUrl) : undefined }));
252
+ await stopDaemon();
253
+ if (input.action === "update") {
254
+ await installPackage();
255
+ }
256
+ await startDaemon();
257
+ await waitForDaemonRunning();
258
+ const version = await currentHappyElvesVersion().catch(() => undefined);
259
+ await updateState({ status: "succeeded", finishedAt: new Date().toISOString(), ...(version ? { toVersion: version } : {}) });
260
+ await log("runner succeeded");
261
+ }
262
+
263
+ async function fail(error) {
264
+ const message = error instanceof Error ? error.stack || error.message : String(error);
265
+ await log("runner failed: " + message).catch(() => {});
266
+ await updateState({ status: "failed", finishedAt: new Date().toISOString(), error: String(error instanceof Error ? error.message : error) }).catch(() => {});
267
+ }
268
+
269
+ async function updateState(patch) {
270
+ const current = JSON.parse(await fs.readFile(input.statePath, "utf8"));
271
+ const next = { ...current, ...patch };
272
+ const tempPath = input.statePath + "." + process.pid + "." + Date.now() + ".tmp";
273
+ await fs.writeFile(tempPath, JSON.stringify(next, null, 2) + "\\n", { mode: 0o600 });
274
+ await fs.rename(tempPath, input.statePath);
275
+ }
276
+
277
+ async function log(line) {
278
+ await fs.mkdir(path.dirname(input.logPath), { recursive: true });
279
+ await fs.appendFile(input.logPath, "[" + new Date().toISOString() + "] " + line + "\\n", { mode: 0o600 });
280
+ }
281
+
282
+ async function stopDaemon() {
283
+ await updateState({ status: "stopping" });
284
+ const pid = await readPid();
285
+ if (!pid || pid === process.pid) {
286
+ await fs.rm(input.pidPath, { force: true }).catch(() => {});
287
+ await log("no managed daemon pid to stop");
288
+ return;
289
+ }
290
+ await log("stopping daemon pid " + pid);
291
+ try {
292
+ process.kill(pid, "SIGTERM");
293
+ } catch (error) {
294
+ await log("SIGTERM failed: " + String(error instanceof Error ? error.message : error));
295
+ }
296
+ const deadline = Date.now() + 10_000;
297
+ while (Date.now() < deadline) {
298
+ if (!isPidAlive(pid)) {
299
+ await fs.rm(input.pidPath, { force: true }).catch(() => {});
300
+ await log("daemon stopped");
301
+ return;
302
+ }
303
+ await delay(200);
304
+ }
305
+ throw new Error("Timed out stopping daemon pid " + pid);
306
+ }
307
+
308
+ async function installPackage() {
309
+ await updateState({ status: "installing" });
310
+ if (input.packageUrl) {
311
+ const tarballPath = path.join(path.dirname(input.logPath), "package.tgz");
312
+ await download(input.packageUrl, tarballPath);
313
+ if (!input.sha256) throw new Error("packageUrl update requires sha256");
314
+ const actual = await sha256File(tarballPath);
315
+ if (actual.toLowerCase() !== String(input.sha256).toLowerCase()) {
316
+ throw new Error("Package sha256 mismatch: expected " + input.sha256 + " got " + actual);
317
+ }
318
+ await run(commandName("npm"), ["install", "-g", "--force", tarballPath], { timeoutMs: 300_000 });
319
+ return;
320
+ }
321
+ const spec = input.packageSpec || input.targetPackage + "@" + (input.targetVersion || "latest");
322
+ await run(commandName("npm"), ["install", "-g", "--force", spec], { timeoutMs: 300_000 });
323
+ }
324
+
325
+ async function startDaemon() {
326
+ await updateState({ status: "starting" });
327
+ if (input.startMode === "current-script") {
328
+ await log("starting daemon from current script");
329
+ await spawnDetached(process.execPath, [input.daemonScriptPath, "start", "--json"]);
330
+ return;
331
+ }
332
+ await log("starting daemon from global happy-elves");
333
+ await spawnDetached(commandName("happy-elves"), ["daemon", "start", "--json"]);
334
+ }
335
+
336
+ async function waitForDaemonRunning() {
337
+ const deadline = Date.now() + 45_000;
338
+ while (Date.now() < deadline) {
339
+ const result = await captureStatus().catch(() => undefined);
340
+ if (result?.ok === true && (result.data?.local?.running === true || result.data?.running === true)) {
341
+ await log("daemon status running");
342
+ return;
343
+ }
344
+ await delay(1000);
345
+ }
346
+ throw new Error("Timed out waiting for restarted daemon");
347
+ }
348
+
349
+ async function captureStatus() {
350
+ const status = statusCommand();
351
+ const result = await run(status.command, status.args, { timeoutMs: 10_000, allowFailure: true });
352
+ const text = result.stdout.trim();
353
+ return text ? JSON.parse(text) : undefined;
354
+ }
355
+
356
+ async function currentHappyElvesVersion() {
357
+ if (input.startMode === "current-script") return undefined;
358
+ const result = await run(commandName("happy-elves"), ["--version"], { timeoutMs: 10_000, allowFailure: true });
359
+ const text = (result.stdout + "\\n" + result.stderr).trim();
360
+ const match = text.match(/(\\d+\\.\\d+\\.\\d+(?:-[\\w.-]+)?)/);
361
+ return match?.[1];
362
+ }
363
+
364
+ function statusCommand() {
365
+ if (input.startMode === "current-script") {
366
+ return { command: process.execPath, args: [input.daemonScriptPath, "status", "--json"] };
367
+ }
368
+ return { command: commandName("happy-elves"), args: ["daemon", "status", "--local", "--json"] };
369
+ }
370
+
371
+ async function spawnDetached(command, args) {
372
+ const child = spawn(command, args, {
373
+ cwd: input.configDir,
374
+ detached: true,
375
+ env: { ...process.env, HAPPY_ELVES_HOME: input.homeEnv },
376
+ stdio: "ignore",
377
+ windowsHide: true,
378
+ });
379
+ child.unref();
380
+ await log("spawned " + command + " pid " + (child.pid || "unknown"));
381
+ }
382
+
383
+ async function run(command, args, options = {}) {
384
+ await log("run " + command + " " + args.join(" "));
385
+ return await new Promise((resolve, reject) => {
386
+ const child = spawn(command, args, {
387
+ cwd: input.configDir,
388
+ env: { ...process.env, HAPPY_ELVES_HOME: input.homeEnv },
389
+ windowsHide: true,
390
+ });
391
+ let stdout = "";
392
+ let stderr = "";
393
+ const timer = setTimeout(() => {
394
+ child.kill();
395
+ reject(new Error("Command timed out: " + command));
396
+ }, options.timeoutMs || 60_000);
397
+ child.stdout?.on("data", (chunk) => { stdout += chunk.toString(); });
398
+ child.stderr?.on("data", (chunk) => { stderr += chunk.toString(); });
399
+ child.on("error", (error) => {
400
+ clearTimeout(timer);
401
+ reject(error);
402
+ });
403
+ child.on("close", (code, signal) => {
404
+ clearTimeout(timer);
405
+ const compactStdout = compact(stdout);
406
+ const compactStderr = compact(stderr);
407
+ if (compactStdout) void log("stdout: " + compactStdout).catch(() => {});
408
+ if (compactStderr) void log("stderr: " + compactStderr).catch(() => {});
409
+ if (!options.allowFailure && code !== 0) {
410
+ reject(new Error(command + " exited " + (code ?? signal ?? "unknown") + ": " + compactStderr));
411
+ return;
412
+ }
413
+ resolve({ code, signal, stdout, stderr });
414
+ });
415
+ });
416
+ }
417
+
418
+ async function download(url, targetPath) {
419
+ await log("download " + redactUrl(url));
420
+ const response = await fetch(url);
421
+ if (!response.ok) throw new Error("Download failed: HTTP " + response.status);
422
+ const buffer = Buffer.from(await response.arrayBuffer());
423
+ await fs.writeFile(targetPath, buffer, { mode: 0o600 });
424
+ }
425
+
426
+ async function sha256File(filePath) {
427
+ const hash = createHash("sha256");
428
+ hash.update(await fs.readFile(filePath));
429
+ return hash.digest("hex");
430
+ }
431
+
432
+ async function readPid() {
433
+ try {
434
+ const raw = await fs.readFile(input.pidPath, "utf8");
435
+ const pid = Number(raw.trim());
436
+ return Number.isInteger(pid) && pid > 0 ? pid : undefined;
437
+ } catch (error) {
438
+ if (error && error.code === "ENOENT") return undefined;
439
+ throw error;
440
+ }
441
+ }
442
+
443
+ function isPidAlive(pid) {
444
+ try {
445
+ process.kill(pid, 0);
446
+ return true;
447
+ } catch {
448
+ return false;
449
+ }
450
+ }
451
+
452
+ function commandName(name) {
453
+ return process.platform === "win32" ? name + ".cmd" : name;
454
+ }
455
+
456
+ function compact(text) {
457
+ return text.replace(/\\s+/g, " ").trim().slice(0, 2000);
458
+ }
459
+
460
+ function delay(ms) {
461
+ return new Promise((resolve) => setTimeout(resolve, ms));
462
+ }
463
+
464
+ function redactUrl(value) {
465
+ try {
466
+ const url = new URL(value);
467
+ if (url.username) url.username = "redacted";
468
+ if (url.password) url.password = "redacted";
469
+ return url.toString();
470
+ } catch {
471
+ return value;
472
+ }
473
+ }
474
+ `;
475
+ }
476
+ //# sourceMappingURL=lifecycle.js.map
@@ -57,6 +57,7 @@ export async function registerMachine(ws, config) {
57
57
  directoryBrowser: true,
58
58
  filePreview: true,
59
59
  devTools: process.env.HAPPY_ELVES_DEV_REMOTE_EXEC === "1",
60
+ daemonLifecycle: true,
60
61
  },
61
62
  });
62
63
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves-daemon",
3
- "version": "0.1.0-beta.270",
3
+ "version": "0.1.0-beta.271",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -483,6 +483,39 @@ export function handleControllerMessage(context, connection, message) {
483
483
  });
484
484
  return;
485
485
  }
486
+ if (message.type === "controller:daemonLifecycle") {
487
+ if (!allowControllerAction(connection, "machine.lifecycle", message.requestId))
488
+ return;
489
+ if (!allowControllerMachine(connection, message.machineId, message.requestId))
490
+ return;
491
+ const machineConnection = connectedMachine(connection.accountId, message.machineId);
492
+ if (!machineConnection) {
493
+ sendMachineOffline(connection, message.requestId);
494
+ return;
495
+ }
496
+ if (machineCapabilities(connection.accountId, message.machineId)?.daemonLifecycle !== true) {
497
+ sendError(connection, {
498
+ type: "server:error",
499
+ requestId: message.requestId,
500
+ code: "DAEMON_LIFECYCLE_UNSUPPORTED",
501
+ capability: "machine.lifecycle",
502
+ message: "This daemon does not support remote lifecycle commands. Upgrade it manually once.",
503
+ });
504
+ return;
505
+ }
506
+ markCommandPending(connection.accountId, message.machineId, message.requestId, { command: "daemonLifecycle" });
507
+ send(machineConnection.socket, {
508
+ type: "machine:daemonLifecycle",
509
+ requestId: message.requestId,
510
+ machineId: message.machineId,
511
+ action: message.action,
512
+ targetVersion: message.targetVersion,
513
+ packageUrl: message.packageUrl,
514
+ sha256: message.sha256,
515
+ tail: message.tail,
516
+ });
517
+ return;
518
+ }
486
519
  if (message.type === "controller:gateway") {
487
520
  if (!allowControllerAction(connection, "gateway.manage", message.requestId))
488
521
  return;
@@ -119,6 +119,18 @@ export function handleMachineMessage(context, connection, message) {
119
119
  broadcastControllers(connection.accountId, response);
120
120
  return;
121
121
  }
122
+ if (message.type === "machine:daemonLifecycleResult") {
123
+ const response = {
124
+ type: "server:daemonLifecycleResult",
125
+ requestId: message.requestId,
126
+ machineId: connection.machineId,
127
+ result: message.result,
128
+ };
129
+ rememberCommandResponse(connection.accountId, message.requestId, response);
130
+ clearCommandPending(connection.accountId, message.requestId);
131
+ broadcastControllers(connection.accountId, response);
132
+ return;
133
+ }
122
134
  if (message.type === "machine:gatewayResult") {
123
135
  const response = {
124
136
  type: "server:gatewayResult",
@@ -114,6 +114,8 @@ export function normalizeMachineCapabilities(value) {
114
114
  capabilities.filePreview = record.filePreview;
115
115
  if (typeof record.devTools === "boolean")
116
116
  capabilities.devTools = record.devTools;
117
+ if (typeof record.daemonLifecycle === "boolean")
118
+ capabilities.daemonLifecycle = record.daemonLifecycle;
117
119
  if (isRecord(record.sessionPrimitives)) {
118
120
  capabilities.sessionPrimitives = {
119
121
  resume: primitiveSupport(record.sessionPrimitives.resume),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves",
3
- "version": "0.1.0-beta.270",
3
+ "version": "0.1.0-beta.271",
4
4
  "description": "Remote controller for local coding agents with hosted or self-hosted relay support.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  import { type DirectoryListing, type EncryptedSessionEvent, type EncryptedEnvelope, type HistoricalSessionSnapshot, type MachineSnapshot, type PairingStartResponse, type SessionSnapshot } from "../../shared/dist/index.js";
2
2
  import { sendCommand } from "./transport.js";
3
- import type { AddControllerDeviceInput, AddControllerDeviceResult, CancelInput, CollectInput, CommandResult, ControllerClientConfig, ControllerInviteClaimInput, ControllerInviteClaimResult, ControllerInviteCreateInput, ControllerInviteCreateResult, ControllerSnapshot, ControllerSubscriptionHandlers, CreateScopedTokenInput, CreateSessionInput, DecodedSessionEvent, DecodedSessionEventsPage, DeviceListResponse, DirectoryInput, FilePreviewInput, ForkInput, GatewayCommandInput, GatewayCommandResponse, HistoricalSessionListPage, HistoricalSessionsInput, ImportHistoricalSessionInput, MachineMetadata, MachineDevExecInput, MachineDevExecResponse, MachineDiagnosticsResponse, MemoryCommandInput, MemoryCommandResponse, DecodedMemoryRecord, RepairSessionHeadInput, RepairSessionHeadResult, RunOptions, ScheduleCommandInput, ScheduleCommandResponse, ScopedTokenInfo, ScopedTokenSelf, SessionFilter, SessionMetadata, WaitInput, MemorySaveInput, MemorySearchInput, MemorySettings, TurnReconcileResponse, WorkspaceCwdPin, WorkspaceCwdPinInput, WorkspaceProjectPreference, WorkspaceProjectPreferenceInput, WorkspaceRecentSession, WorkspaceSessionStarResult, WorkspaceSyncState } from "./types.js";
3
+ import type { AddControllerDeviceInput, AddControllerDeviceResult, CancelInput, CollectInput, CommandResult, ControllerClientConfig, ControllerInviteClaimInput, ControllerInviteClaimResult, ControllerInviteCreateInput, ControllerInviteCreateResult, ControllerSnapshot, ControllerSubscriptionHandlers, CreateScopedTokenInput, CreateSessionInput, DaemonLifecycleInput, DaemonLifecycleResponse, DecodedSessionEvent, DecodedSessionEventsPage, DeviceListResponse, DirectoryInput, FilePreviewInput, ForkInput, GatewayCommandInput, GatewayCommandResponse, HistoricalSessionListPage, HistoricalSessionsInput, ImportHistoricalSessionInput, MachineMetadata, MachineDevExecInput, MachineDevExecResponse, MachineDiagnosticsResponse, MemoryCommandInput, MemoryCommandResponse, DecodedMemoryRecord, RepairSessionHeadInput, RepairSessionHeadResult, RunOptions, ScheduleCommandInput, ScheduleCommandResponse, ScopedTokenInfo, ScopedTokenSelf, SessionFilter, SessionMetadata, WaitInput, MemorySaveInput, MemorySearchInput, MemorySettings, TurnReconcileResponse, WorkspaceCwdPin, WorkspaceCwdPinInput, WorkspaceProjectPreference, WorkspaceProjectPreferenceInput, WorkspaceRecentSession, WorkspaceSessionStarResult, WorkspaceSyncState } from "./types.js";
4
4
  export declare class ControllerClient {
5
5
  readonly config: ControllerClientConfig;
6
6
  constructor(config: ControllerClientConfig);
@@ -73,6 +73,7 @@ export declare class ControllerClient {
73
73
  previewFile(input: FilePreviewInput): Promise<import("../../shared/dist/index.js").FilePreviewResult>;
74
74
  machineDiagnostics(machineId: string): Promise<MachineDiagnosticsResponse>;
75
75
  machineDevExec(input: MachineDevExecInput): Promise<MachineDevExecResponse>;
76
+ daemonLifecycle(input: DaemonLifecycleInput): Promise<DaemonLifecycleResponse>;
76
77
  gateway(input: GatewayCommandInput): Promise<GatewayCommandResponse>;
77
78
  schedule(input: ScheduleCommandInput): Promise<ScheduleCommandResponse>;
78
79
  memory(input: MemoryCommandInput): Promise<MemoryCommandResponse>;
@@ -3,7 +3,7 @@ import { addControllerDevice, claimControllerInvite, createControllerInvite, cre
3
3
  import { ControllerClientError, ControllerCommandError } from "./errors.js";
4
4
  import { authenticatedJson, normalizeRelayUrl, readRelayJson, relayHttpError } from "./http.js";
5
5
  import { parseRepairSessionHeadResult, parseSessionEventsResponse, } from "./parsers.js";
6
- import { sendAndWaitForDirectoryListing, sendAndWaitForFilePreview, sendAndWaitForGatewayResult, sendAndWaitForHistoricalSessions, sendAndWaitForMachineDevExecResult, sendAndWaitForMachineDiagnostics, sendAndWaitForMemoryResult, sendAndWaitForScheduleResult, sendAndWaitForTurnReconcileResult, sendCommand, subscribe, } from "./transport.js";
6
+ import { sendAndWaitForDirectoryListing, sendAndWaitForDaemonLifecycleResult, sendAndWaitForFilePreview, sendAndWaitForGatewayResult, sendAndWaitForHistoricalSessions, sendAndWaitForMachineDevExecResult, sendAndWaitForMachineDiagnostics, sendAndWaitForMemoryResult, sendAndWaitForScheduleResult, sendAndWaitForTurnReconcileResult, sendCommand, subscribe, } from "./transport.js";
7
7
  import { DEFAULT_SESSION_CREATE_WAIT_MS, } from "./types.js";
8
8
  import { isWaitCondition, matchesWaitCondition, parseEventCursor, parseEventLimit, parsePositiveDuration, requireNonEmpty, sleep, } from "./validation.js";
9
9
  const DEFAULT_BLOCKING_RUN_WAIT_MS = 30 * 60 * 1000;
@@ -364,6 +364,21 @@ export class ControllerClient {
364
364
  }, input.waitTimeoutMs);
365
365
  return message.result;
366
366
  }
367
+ async daemonLifecycle(input) {
368
+ const machineId = requireNonEmpty(input.machineId, "machineId");
369
+ const requestId = randomId("req");
370
+ const message = await sendAndWaitForDaemonLifecycleResult(this.config, {
371
+ type: "controller:daemonLifecycle",
372
+ requestId,
373
+ machineId,
374
+ action: input.action,
375
+ targetVersion: input.targetVersion,
376
+ packageUrl: input.packageUrl,
377
+ sha256: input.sha256,
378
+ tail: input.tail,
379
+ }, input.waitTimeoutMs);
380
+ return { ...message.result, requestId, machineId: message.machineId };
381
+ }
367
382
  async gateway(input) {
368
383
  const machineId = requireNonEmpty(input.machineId, "machineId");
369
384
  const requestId = randomId("req");
@@ -1,4 +1,4 @@
1
1
  export { ControllerClient } from "./client.js";
2
2
  export { ControllerClientError, ControllerCommandError } from "./errors.js";
3
- export type { AddControllerDeviceInput, AddControllerDeviceResult, CancelInput, CollectInput, CommandResult, ControllerClientConfig, ControllerDevice, ControllerInviteClaimInput, ControllerInviteClaimResult, ControllerInviteCreateInput, ControllerInviteCreateResult, ControllerSnapshot, ControllerSubscriptionHandlers, CreateScopedTokenInput, CreateSessionInput, DecodedSessionEvent, DecodedSessionEventsPage, DeviceListResponse, DirectoryInput, DirectoryListResponse, FilePreviewInput, FilePreviewResponse, EventCursor, ForkInput, HistoricalSessionListPage, HistoricalSessionListResponse, HistoricalSessionsInput, ImportHistoricalSessionInput, MachineListResponse, MachineMetadata, DecodedMemoryRecord, EncryptedMemoryRecord, MemoryPayload, MemoryCommandInput, MemoryCommandResponse, MemorySaveInput, MemorySearchInput, MemorySettings, PermissionMode, RepairSessionHeadInput, RepairSessionHeadResult, RunOptions, ScopedTokenInfo, ScopedTokenScope, ScopedTokenSelf, SessionFilter, SessionMetadata, WaitCondition, WaitInput, WorkspaceCwdPin, WorkspaceCwdPinInput, WorkspaceProjectPreference, WorkspaceProjectPreferenceInput, WorkspaceRecentSession, WorkspaceSessionStar, WorkspaceSessionStarResult, WorkspaceSyncState, } from "./types.js";
3
+ export type { AddControllerDeviceInput, AddControllerDeviceResult, CancelInput, CollectInput, CommandResult, ControllerClientConfig, ControllerDevice, ControllerInviteClaimInput, ControllerInviteClaimResult, ControllerInviteCreateInput, ControllerInviteCreateResult, ControllerSnapshot, ControllerSubscriptionHandlers, CreateScopedTokenInput, CreateSessionInput, DecodedSessionEvent, DecodedSessionEventsPage, DaemonLifecycleInput, DaemonLifecycleResponse, DeviceListResponse, DirectoryInput, DirectoryListResponse, FilePreviewInput, FilePreviewResponse, EventCursor, ForkInput, HistoricalSessionListPage, HistoricalSessionListResponse, HistoricalSessionsInput, ImportHistoricalSessionInput, MachineListResponse, MachineMetadata, DecodedMemoryRecord, EncryptedMemoryRecord, MemoryPayload, MemoryCommandInput, MemoryCommandResponse, MemorySaveInput, MemorySearchInput, MemorySettings, PermissionMode, RepairSessionHeadInput, RepairSessionHeadResult, RunOptions, ScopedTokenInfo, ScopedTokenScope, ScopedTokenSelf, SessionFilter, SessionMetadata, WaitCondition, WaitInput, WorkspaceCwdPin, WorkspaceCwdPinInput, WorkspaceProjectPreference, WorkspaceProjectPreferenceInput, WorkspaceRecentSession, WorkspaceSessionStar, WorkspaceSessionStarResult, WorkspaceSyncState, } from "./types.js";
4
4
  //# sourceMappingURL=index.d.ts.map
@@ -37,6 +37,11 @@ export declare function sendAndWaitForMachineDevExecResult(config: ControllerCli
37
37
  }>, waitTimeoutMs?: number): Promise<Extract<ServerMessage, {
38
38
  type: "server:machineDevExecResult";
39
39
  }>>;
40
+ export declare function sendAndWaitForDaemonLifecycleResult(config: ControllerClientConfig, command: Extract<ControllerClientMessage, {
41
+ type: "controller:daemonLifecycle";
42
+ }>, waitTimeoutMs?: number): Promise<Extract<ServerMessage, {
43
+ type: "server:daemonLifecycleResult";
44
+ }>>;
40
45
  export declare function sendAndWaitForGatewayResult(config: ControllerClientConfig, command: Extract<ControllerClientMessage, {
41
46
  type: "controller:gateway";
42
47
  }>): Promise<Extract<ServerMessage, {
@@ -64,6 +64,10 @@ export function subscribe(config, handlers) {
64
64
  void handlers.onMemoryResult?.(message);
65
65
  return;
66
66
  }
67
+ if (message.type === "server:daemonLifecycleResult") {
68
+ void handlers.onDaemonLifecycleResult?.(message);
69
+ return;
70
+ }
67
71
  if (message.type === "server:turnReconciled") {
68
72
  void handlers.onTurnReconciled?.(message);
69
73
  return;
@@ -248,6 +252,9 @@ export async function sendAndWaitForMachineDiagnostics(config, command) {
248
252
  export async function sendAndWaitForMachineDevExecResult(config, command, waitTimeoutMs) {
249
253
  return await sendAndWaitForServerMessage(config, command, "server:machineDevExecResult", "machine dev exec result; remote result is unknown", waitTimeoutMs ?? (command.timeoutMs ?? 60_000) + 15_000);
250
254
  }
255
+ export async function sendAndWaitForDaemonLifecycleResult(config, command, waitTimeoutMs) {
256
+ return await sendAndWaitForServerMessage(config, command, "server:daemonLifecycleResult", "daemon lifecycle result", waitTimeoutMs ?? 45_000);
257
+ }
251
258
  export async function sendAndWaitForGatewayResult(config, command) {
252
259
  return await sendAndWaitForServerMessage(config, command, "server:gatewayResult", "gateway result", 30_000);
253
260
  }
@@ -1,4 +1,4 @@
1
- import type { DirectoryListing, EncryptedSessionEvent, FilePreviewMode, FilePreviewResult, GatewayChannelInput, GatewayCommandResult, HistoricalSessionSnapshot, MachineSnapshot, MachineDevExecResult, MachineDiagnostics, ScheduleCommandResult, ScheduleCreateInput, MemoryCommandResult, MemoryMode, MemoryPromoteInput, MemoryWriteInput, ServerMessage, SessionEventPayload, SessionSnapshot, TurnReconcileResult } from "../../shared/dist/index.js";
1
+ import type { DirectoryListing, DaemonLifecycleAction, DaemonLifecycleResult, EncryptedSessionEvent, FilePreviewMode, FilePreviewResult, GatewayChannelInput, GatewayCommandResult, HistoricalSessionSnapshot, MachineSnapshot, MachineDevExecResult, MachineDiagnostics, ScheduleCommandResult, ScheduleCreateInput, MemoryCommandResult, MemoryMode, MemoryPromoteInput, MemoryWriteInput, ServerMessage, SessionEventPayload, SessionSnapshot, TurnReconcileResult } from "../../shared/dist/index.js";
2
2
  export type ControllerClientConfig = {
3
3
  relayUrl: string;
4
4
  controllerToken: string;
@@ -51,6 +51,9 @@ export type ControllerSubscriptionHandlers = {
51
51
  onMemoryResult?: (message: Extract<ServerMessage, {
52
52
  type: "server:memoryResult";
53
53
  }>) => void;
54
+ onDaemonLifecycleResult?: (message: Extract<ServerMessage, {
55
+ type: "server:daemonLifecycleResult";
56
+ }>) => void;
54
57
  onTurnReconciled?: (message: Extract<ServerMessage, {
55
58
  type: "server:turnReconciled";
56
59
  }>) => void;
@@ -290,6 +293,19 @@ export type MachineDevExecInput = {
290
293
  timeoutMs?: number;
291
294
  waitTimeoutMs?: number;
292
295
  };
296
+ export type DaemonLifecycleInput = {
297
+ machineId: string;
298
+ action: DaemonLifecycleAction;
299
+ targetVersion?: string;
300
+ packageUrl?: string;
301
+ sha256?: string;
302
+ tail?: number;
303
+ waitTimeoutMs?: number;
304
+ };
305
+ export type DaemonLifecycleResponse = DaemonLifecycleResult & {
306
+ requestId: string;
307
+ machineId: string;
308
+ };
293
309
  export type GatewayCommandInput = {
294
310
  machineId: string;
295
311
  action: "list" | "get" | "upsert" | "enable" | "disable" | "test";
@@ -94,6 +94,7 @@ export declare const capabilitiesSchema: z.ZodObject<{
94
94
  directoryBrowser: z.ZodOptional<z.ZodBoolean>;
95
95
  filePreview: z.ZodOptional<z.ZodBoolean>;
96
96
  devTools: z.ZodOptional<z.ZodBoolean>;
97
+ daemonLifecycle: z.ZodOptional<z.ZodBoolean>;
97
98
  }, z.core.$strip>;
98
99
  export declare const machineSnapshotSchema: z.ZodObject<{
99
100
  id: z.ZodString;
@@ -182,6 +183,7 @@ export declare const machineSnapshotSchema: z.ZodObject<{
182
183
  directoryBrowser: z.ZodOptional<z.ZodBoolean>;
183
184
  filePreview: z.ZodOptional<z.ZodBoolean>;
184
185
  devTools: z.ZodOptional<z.ZodBoolean>;
186
+ daemonLifecycle: z.ZodOptional<z.ZodBoolean>;
185
187
  }, z.core.$strip>;
186
188
  encryptedMetadata: z.ZodOptional<z.ZodObject<{
187
189
  v: z.ZodLiteral<1>;
@@ -413,14 +415,88 @@ export declare const machineDevExecResultSchema: z.ZodObject<{
413
415
  durationMs: z.ZodNumber;
414
416
  timedOut: z.ZodOptional<z.ZodBoolean>;
415
417
  }, z.core.$strip>;
418
+ export declare const daemonUpdateStateSchema: z.ZodObject<{
419
+ requestId: z.ZodString;
420
+ action: z.ZodEnum<{
421
+ restart: "restart";
422
+ update: "update";
423
+ }>;
424
+ targetVersion: z.ZodOptional<z.ZodString>;
425
+ packageSpec: z.ZodOptional<z.ZodString>;
426
+ packageUrl: z.ZodOptional<z.ZodString>;
427
+ sha256: z.ZodOptional<z.ZodString>;
428
+ status: z.ZodEnum<{
429
+ running: "running";
430
+ failed: "failed";
431
+ accepted: "accepted";
432
+ stopping: "stopping";
433
+ installing: "installing";
434
+ starting: "starting";
435
+ succeeded: "succeeded";
436
+ }>;
437
+ fromVersion: z.ZodString;
438
+ toVersion: z.ZodOptional<z.ZodString>;
439
+ startedAt: z.ZodString;
440
+ finishedAt: z.ZodOptional<z.ZodString>;
441
+ error: z.ZodOptional<z.ZodString>;
442
+ logPath: z.ZodString;
443
+ }, z.core.$strip>;
444
+ export declare const daemonLifecycleResultSchema: z.ZodObject<{
445
+ action: z.ZodEnum<{
446
+ status: "status";
447
+ restart: "restart";
448
+ update: "update";
449
+ logs: "logs";
450
+ }>;
451
+ accepted: z.ZodOptional<z.ZodBoolean>;
452
+ version: z.ZodOptional<z.ZodString>;
453
+ pid: z.ZodOptional<z.ZodNumber>;
454
+ running: z.ZodOptional<z.ZodBoolean>;
455
+ platform: z.ZodOptional<z.ZodString>;
456
+ arch: z.ZodOptional<z.ZodString>;
457
+ installMode: z.ZodOptional<z.ZodEnum<{
458
+ "npm-global": "npm-global";
459
+ "repo-dev": "repo-dev";
460
+ unknown: "unknown";
461
+ }>>;
462
+ updateSupported: z.ZodOptional<z.ZodBoolean>;
463
+ updateUnsupportedReason: z.ZodOptional<z.ZodString>;
464
+ updateState: z.ZodOptional<z.ZodObject<{
465
+ requestId: z.ZodString;
466
+ action: z.ZodEnum<{
467
+ restart: "restart";
468
+ update: "update";
469
+ }>;
470
+ targetVersion: z.ZodOptional<z.ZodString>;
471
+ packageSpec: z.ZodOptional<z.ZodString>;
472
+ packageUrl: z.ZodOptional<z.ZodString>;
473
+ sha256: z.ZodOptional<z.ZodString>;
474
+ status: z.ZodEnum<{
475
+ running: "running";
476
+ failed: "failed";
477
+ accepted: "accepted";
478
+ stopping: "stopping";
479
+ installing: "installing";
480
+ starting: "starting";
481
+ succeeded: "succeeded";
482
+ }>;
483
+ fromVersion: z.ZodString;
484
+ toVersion: z.ZodOptional<z.ZodString>;
485
+ startedAt: z.ZodString;
486
+ finishedAt: z.ZodOptional<z.ZodString>;
487
+ error: z.ZodOptional<z.ZodString>;
488
+ logPath: z.ZodString;
489
+ }, z.core.$strip>>;
490
+ logs: z.ZodOptional<z.ZodArray<z.ZodString>>;
491
+ }, z.core.$strip>;
416
492
  export declare const turnReconcileResultSchema: z.ZodObject<{
417
493
  sessionId: z.ZodString;
418
494
  turnId: z.ZodOptional<z.ZodString>;
419
495
  requestId: z.ZodOptional<z.ZodString>;
420
496
  status: z.ZodEnum<{
497
+ unknown: "unknown";
421
498
  active: "active";
422
499
  terminal: "terminal";
423
- unknown: "unknown";
424
500
  not_found: "not_found";
425
501
  uninspectable: "uninspectable";
426
502
  }>;
@@ -1359,12 +1435,12 @@ export declare const memoryTurnOptionsSchema: z.ZodObject<{
1359
1435
  export declare const memoryCommandResultSchema: z.ZodObject<{
1360
1436
  action: z.ZodEnum<{
1361
1437
  search: "search";
1438
+ status: "status";
1362
1439
  overview: "overview";
1363
1440
  read: "read";
1364
1441
  reindex: "reindex";
1365
1442
  write: "write";
1366
1443
  promote: "promote";
1367
- status: "status";
1368
1444
  sync: "sync";
1369
1445
  }>;
1370
1446
  overview: z.ZodOptional<z.ZodObject<{
@@ -53,6 +53,7 @@ export const capabilitiesSchema = z.object({
53
53
  directoryBrowser: z.boolean().optional(),
54
54
  filePreview: z.boolean().optional(),
55
55
  devTools: z.boolean().optional(),
56
+ daemonLifecycle: z.boolean().optional(),
56
57
  });
57
58
  const sessionCapabilitiesSchema = z.object({
58
59
  resume: z.enum(["acpx", "native", "checkpoint", "unsupported"]),
@@ -198,6 +199,35 @@ export const machineDevExecResultSchema = z.object({
198
199
  durationMs: z.number().nonnegative(),
199
200
  timedOut: z.boolean().optional(),
200
201
  });
202
+ export const daemonUpdateStateSchema = z.object({
203
+ requestId: z.string().min(1),
204
+ action: z.enum(["restart", "update"]),
205
+ targetVersion: z.string().min(1).optional(),
206
+ packageSpec: z.string().min(1).optional(),
207
+ packageUrl: z.string().min(1).optional(),
208
+ sha256: z.string().regex(/^[a-fA-F0-9]{64}$/).optional(),
209
+ status: z.enum(["accepted", "running", "stopping", "installing", "starting", "succeeded", "failed"]),
210
+ fromVersion: z.string().min(1),
211
+ toVersion: z.string().min(1).optional(),
212
+ startedAt: z.string().min(1),
213
+ finishedAt: z.string().min(1).optional(),
214
+ error: z.string().optional(),
215
+ logPath: z.string().min(1),
216
+ });
217
+ export const daemonLifecycleResultSchema = z.object({
218
+ action: z.enum(["status", "restart", "update", "logs"]),
219
+ accepted: z.boolean().optional(),
220
+ version: z.string().min(1).optional(),
221
+ pid: z.number().int().positive().optional(),
222
+ running: z.boolean().optional(),
223
+ platform: z.string().min(1).optional(),
224
+ arch: z.string().min(1).optional(),
225
+ installMode: z.enum(["npm-global", "repo-dev", "unknown"]).optional(),
226
+ updateSupported: z.boolean().optional(),
227
+ updateUnsupportedReason: z.string().min(1).optional(),
228
+ updateState: daemonUpdateStateSchema.optional(),
229
+ logs: z.array(z.string()).optional(),
230
+ });
201
231
  export const turnReconcileResultSchema = z.object({
202
232
  sessionId: z.string().min(1),
203
233
  turnId: z.string().min(1).optional(),
@@ -46,6 +46,7 @@ export type MachineSnapshot = {
46
46
  directoryBrowser?: boolean;
47
47
  filePreview?: boolean;
48
48
  devTools?: boolean;
49
+ daemonLifecycle?: boolean;
49
50
  };
50
51
  encryptedMetadata?: EncryptedEnvelope;
51
52
  };
@@ -175,6 +176,38 @@ export type MachineDevExecResult = {
175
176
  durationMs: number;
176
177
  timedOut?: boolean;
177
178
  };
179
+ export type DaemonLifecycleAction = "status" | "restart" | "update" | "logs";
180
+ export type DaemonInstallMode = "npm-global" | "repo-dev" | "unknown";
181
+ export type DaemonUpdateStatus = "accepted" | "running" | "stopping" | "installing" | "starting" | "succeeded" | "failed";
182
+ export type DaemonUpdateState = {
183
+ requestId: string;
184
+ action: "restart" | "update";
185
+ targetVersion?: string;
186
+ packageSpec?: string;
187
+ packageUrl?: string;
188
+ sha256?: string;
189
+ status: DaemonUpdateStatus;
190
+ fromVersion: string;
191
+ toVersion?: string;
192
+ startedAt: string;
193
+ finishedAt?: string;
194
+ error?: string;
195
+ logPath: string;
196
+ };
197
+ export type DaemonLifecycleResult = {
198
+ action: DaemonLifecycleAction;
199
+ accepted?: boolean;
200
+ version?: string;
201
+ pid?: number;
202
+ running?: boolean;
203
+ platform?: string;
204
+ arch?: string;
205
+ installMode?: DaemonInstallMode;
206
+ updateSupported?: boolean;
207
+ updateUnsupportedReason?: string;
208
+ updateState?: DaemonUpdateState;
209
+ logs?: string[];
210
+ };
178
211
  export type TurnInspectionStatus = "active" | "terminal" | "unknown" | "not_found" | "uninspectable";
179
212
  export type TurnReconcileResult = {
180
213
  sessionId: string;
@@ -1,7 +1,7 @@
1
1
  import type { EncryptedEnvelope } from "./crypto.js";
2
- import type { DirectoryListing, EncryptedSessionEvent, EncryptedTranscriptReplacementEvent, FilePreviewMode, FilePreviewResult, GatewayChannelInput, GatewayCommandResult, HistoricalSessionSnapshot, MachineDevExecResult, MachineDiagnostics, MachineSnapshot, MemoryCommandResult, MemoryMode, MemoryPromoteInput, MemoryWriteInput, PermissionMode, ScheduleCommandResult, ScheduleCreateInput, SessionHeadAdvanceBasis, SessionSnapshot, TurnReconcileResult } from "./protocol-types.js";
2
+ import type { DirectoryListing, DaemonLifecycleAction, DaemonLifecycleResult, EncryptedSessionEvent, EncryptedTranscriptReplacementEvent, FilePreviewMode, FilePreviewResult, GatewayChannelInput, GatewayCommandResult, HistoricalSessionSnapshot, MachineDevExecResult, MachineDiagnostics, MachineSnapshot, MemoryCommandResult, MemoryMode, MemoryPromoteInput, MemoryWriteInput, PermissionMode, ScheduleCommandResult, ScheduleCreateInput, SessionHeadAdvanceBasis, SessionSnapshot, TurnReconcileResult } from "./protocol-types.js";
3
3
  export { encryptedEnvelopeSchema } from "./protocol-schemas.js";
4
- export type { AgentProfile, DirectoryEntry, DirectoryListing, EncryptedSessionEvent, EncryptedTranscriptReplacementEvent, FilePreviewKind, FilePreviewMode, FilePreviewResult, GatewayChannelInput, GatewayChannelDomain, GatewayDestination, GatewayEventPayload, GatewayInboundMode, GatewayLastDeliveredMessage, GatewayLastMessageRef, GatewayChannelPublic, GatewayChannelType, GatewayCommandResult, GatewayDeliveryResult, GatewayProcessingReactionConfig, HistoricalSessionSnapshot, MachineDevExecResult, MachineDiagnostics, MachineSnapshot, MemoryCitation, MemoryCommandResult, MemoryFileSummary, MemoryLayer, MemoryMode, MemoryOverview, MemoryPromoteInput, MemorySearchResult, MemorySyncConflict, MemoryTurnOptions, MemoryWriteInput, PairingClaimRequest, PairingClaimResponse, PairingStartRequest, PairingStartResponse, PermissionMode, ScheduleCommandResult, ScheduleCreateInput, ScheduleExecution, ScheduleMemoryMode, ScheduleRecord, ScheduleRunRecord, ScheduleSpec, ScheduleStopReason, ScheduleTarget, SessionHeadAdvanceBasis, SessionEventPayload, SessionSnapshot, SessionUsageSnapshot, TurnInspectionStatus, TurnReconcileResult, } from "./protocol-types.js";
4
+ export type { AgentProfile, DaemonInstallMode, DaemonLifecycleAction, DaemonLifecycleResult, DaemonUpdateState, DaemonUpdateStatus, DirectoryEntry, DirectoryListing, EncryptedSessionEvent, EncryptedTranscriptReplacementEvent, FilePreviewKind, FilePreviewMode, FilePreviewResult, GatewayChannelInput, GatewayChannelDomain, GatewayDestination, GatewayEventPayload, GatewayInboundMode, GatewayLastDeliveredMessage, GatewayLastMessageRef, GatewayChannelPublic, GatewayChannelType, GatewayCommandResult, GatewayDeliveryResult, GatewayProcessingReactionConfig, HistoricalSessionSnapshot, MachineDevExecResult, MachineDiagnostics, MachineSnapshot, MemoryCitation, MemoryCommandResult, MemoryFileSummary, MemoryLayer, MemoryMode, MemoryOverview, MemoryPromoteInput, MemorySearchResult, MemorySyncConflict, MemoryTurnOptions, MemoryWriteInput, PairingClaimRequest, PairingClaimResponse, PairingStartRequest, PairingStartResponse, PermissionMode, ScheduleCommandResult, ScheduleCreateInput, ScheduleExecution, ScheduleMemoryMode, ScheduleRecord, ScheduleRunRecord, ScheduleSpec, ScheduleStopReason, ScheduleTarget, SessionHeadAdvanceBasis, SessionEventPayload, SessionSnapshot, SessionUsageSnapshot, TurnInspectionStatus, TurnReconcileResult, } from "./protocol-types.js";
5
5
  export type ControllerClientMessage = {
6
6
  type: "controller:createSession";
7
7
  requestId: string;
@@ -80,6 +80,15 @@ export type ControllerClientMessage = {
80
80
  cwd?: string;
81
81
  shell?: string;
82
82
  timeoutMs?: number;
83
+ } | {
84
+ type: "controller:daemonLifecycle";
85
+ requestId: string;
86
+ machineId: string;
87
+ action: DaemonLifecycleAction;
88
+ targetVersion?: string;
89
+ packageUrl?: string;
90
+ sha256?: string;
91
+ tail?: number;
83
92
  } | {
84
93
  type: "controller:gateway";
85
94
  requestId: string;
@@ -233,6 +242,11 @@ export type MachineClientMessage = {
233
242
  requestId: string;
234
243
  machineId: string;
235
244
  result: MachineDevExecResult;
245
+ } | {
246
+ type: "machine:daemonLifecycleResult";
247
+ requestId: string;
248
+ machineId: string;
249
+ result: DaemonLifecycleResult;
236
250
  } | {
237
251
  type: "machine:gatewayResult";
238
252
  requestId: string;
@@ -388,6 +402,15 @@ export type MachineCommand = {
388
402
  cwd?: string;
389
403
  shell?: string;
390
404
  timeoutMs?: number;
405
+ } | {
406
+ type: "machine:daemonLifecycle";
407
+ requestId: string;
408
+ machineId: string;
409
+ action: DaemonLifecycleAction;
410
+ targetVersion?: string;
411
+ packageUrl?: string;
412
+ sha256?: string;
413
+ tail?: number;
391
414
  } | {
392
415
  type: "machine:gateway";
393
416
  requestId: string;
@@ -520,6 +543,11 @@ export type ServerMessage = {
520
543
  requestId: string;
521
544
  machineId: string;
522
545
  result: MachineDevExecResult;
546
+ } | {
547
+ type: "server:daemonLifecycleResult";
548
+ requestId: string;
549
+ machineId: string;
550
+ result: DaemonLifecycleResult;
523
551
  } | {
524
552
  type: "server:gatewayResult";
525
553
  requestId: string;
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { capabilitiesSchema, directoryEntrySchema, encryptedEnvelopeSchema, encryptedSessionEventSchema, encryptedTranscriptReplacementEventSchema, filePreviewResultSchema, gatewayChannelInputSchema, gatewayCommandResultSchema, historicalSessionSnapshotSchema, machineDevExecResultSchema, machineDiagnosticsSchema, machineSnapshotSchema, memoryCommandResultSchema, memoryPromoteInputSchema, memoryWriteInputSchema, scheduleCommandResultSchema, scheduleCreateInputSchema, sessionSnapshotSchema, turnReconcileResultSchema, } from "./protocol-schemas.js";
2
+ import { capabilitiesSchema, daemonLifecycleResultSchema, directoryEntrySchema, encryptedEnvelopeSchema, encryptedSessionEventSchema, encryptedTranscriptReplacementEventSchema, filePreviewResultSchema, gatewayChannelInputSchema, gatewayCommandResultSchema, historicalSessionSnapshotSchema, machineDevExecResultSchema, machineDiagnosticsSchema, machineSnapshotSchema, memoryCommandResultSchema, memoryPromoteInputSchema, memoryWriteInputSchema, scheduleCommandResultSchema, scheduleCreateInputSchema, sessionSnapshotSchema, turnReconcileResultSchema, } from "./protocol-schemas.js";
3
3
  import { isSessionNameValid } from "./session-name.js";
4
4
  export { encryptedEnvelopeSchema } from "./protocol-schemas.js";
5
5
  const controllerMessageSchema = z.discriminatedUnion("type", [
@@ -92,6 +92,16 @@ const controllerMessageSchema = z.discriminatedUnion("type", [
92
92
  shell: z.string().optional(),
93
93
  timeoutMs: z.number().int().positive().max(600_000).optional(),
94
94
  }),
95
+ z.object({
96
+ type: z.literal("controller:daemonLifecycle"),
97
+ requestId: z.string().min(1),
98
+ machineId: z.string().min(1),
99
+ action: z.enum(["status", "restart", "update", "logs"]),
100
+ targetVersion: z.string().min(1).optional(),
101
+ packageUrl: z.string().min(1).optional(),
102
+ sha256: z.string().regex(/^[a-fA-F0-9]{64}$/).optional(),
103
+ tail: z.number().int().positive().max(2000).optional(),
104
+ }),
95
105
  z.object({
96
106
  type: z.literal("controller:gateway"),
97
107
  requestId: z.string().min(1),
@@ -283,6 +293,12 @@ const machineMessageSchema = z.discriminatedUnion("type", [
283
293
  machineId: z.string().min(1),
284
294
  result: machineDevExecResultSchema,
285
295
  }),
296
+ z.object({
297
+ type: z.literal("machine:daemonLifecycleResult"),
298
+ requestId: z.string().min(1),
299
+ machineId: z.string().min(1),
300
+ result: daemonLifecycleResultSchema,
301
+ }),
286
302
  z.object({
287
303
  type: z.literal("machine:gatewayResult"),
288
304
  requestId: z.string().min(1),
@@ -463,6 +479,16 @@ const machineCommandSchema = z.discriminatedUnion("type", [
463
479
  shell: z.string().optional(),
464
480
  timeoutMs: z.number().int().positive().max(600_000).optional(),
465
481
  }),
482
+ z.object({
483
+ type: z.literal("machine:daemonLifecycle"),
484
+ requestId: z.string().min(1),
485
+ machineId: z.string().min(1),
486
+ action: z.enum(["status", "restart", "update", "logs"]),
487
+ targetVersion: z.string().min(1).optional(),
488
+ packageUrl: z.string().min(1).optional(),
489
+ sha256: z.string().regex(/^[a-fA-F0-9]{64}$/).optional(),
490
+ tail: z.number().int().positive().max(2000).optional(),
491
+ }),
466
492
  z.object({
467
493
  type: z.literal("machine:gateway"),
468
494
  requestId: z.string().min(1),
@@ -624,6 +650,12 @@ const serverMessageSchema = z.union([
624
650
  machineId: z.string().min(1),
625
651
  result: machineDevExecResultSchema,
626
652
  }),
653
+ z.object({
654
+ type: z.literal("server:daemonLifecycleResult"),
655
+ requestId: z.string().min(1),
656
+ machineId: z.string().min(1),
657
+ result: daemonLifecycleResultSchema,
658
+ }),
627
659
  z.object({
628
660
  type: z.literal("server:gatewayResult"),
629
661
  requestId: z.string().min(1),