totemlayer 0.1.4 → 0.1.6

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.
@@ -0,0 +1,609 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { constants } from "node:fs";
4
+ import { lstat, mkdir, open, opendir, rename, unlink } from "node:fs/promises";
5
+ import { dirname, isAbsolute, join, parse, resolve, sep } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { parseArgs } from "node:util";
8
+
9
+ import { readAgentCredential, readAgentProfile, withAgentProfileLock } from "./agent-profile.mjs";
10
+ import { localClientEnvironment } from "./platform-client.mjs";
11
+ import { Totem } from "./sdk/index.js";
12
+
13
+ const CLI_PATH = fileURLToPath(new URL("./totem.mjs", import.meta.url));
14
+ const UUID = /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i;
15
+ const PHASES = new Set([
16
+ "starting",
17
+ "connected",
18
+ "reconnecting",
19
+ "working",
20
+ "waiting",
21
+ "blocked",
22
+ "stopping",
23
+ "stopped",
24
+ "failed",
25
+ ]);
26
+ const LABELS = {
27
+ starting: "Connecting your agent…",
28
+ connected: "Agent connected. Looking for approved Mission work.",
29
+ reconnecting: "Connection interrupted. Waiting to reconnect from saved progress.",
30
+ working: "Working on an assigned Mission task.",
31
+ waiting: "Connected and waiting for approved work.",
32
+ blocked: "A Mission needs attention. Open Missions to review the blocker.",
33
+ stopping: "Stopping the agent and saving its progress…",
34
+ stopped: "Agent stopped. Saved progress will be checked when it starts again.",
35
+ failed:
36
+ "The agent stopped with an error. Check its connection and Mission status before restarting.",
37
+ };
38
+
39
+ export const AGENT_START_HELP = `Run your connected Totem agent
40
+
41
+ totem start [--profile default] [--background] [--json]
42
+ totem stop [--profile default] [--json]
43
+ totem status [--profile default] [--json]
44
+
45
+ Run setup once first. Start receives approved Mission assignments automatically
46
+ and keeps checking for new work. Stop preserves the worker's recovery journal.
47
+ Use --background to keep this process running after closing the terminal.
48
+ Your computer and local model must remain running. This does not install a
49
+ login service, start a model, download weights, or grant new permissions.
50
+ `;
51
+
52
+ function alive(pid) {
53
+ if (!Number.isSafeInteger(pid) || pid < 1) return false;
54
+ try {
55
+ process.kill(pid, 0);
56
+ return true;
57
+ } catch (error) {
58
+ return error?.code === "EPERM";
59
+ }
60
+ }
61
+
62
+ function privateEntry(info, directory = false, opened = false) {
63
+ if (
64
+ !(directory ? info.isDirectory() : info.isFile()) ||
65
+ info.isSymbolicLink() ||
66
+ (!directory && ((info.nlink !== 1 && !(opened && info.nlink === 0)) || info.size > 4096)) ||
67
+ (typeof process.getuid === "function" && info.uid !== process.getuid()) ||
68
+ (process.platform !== "win32" && (info.mode & 0o777) !== (directory ? 0o700 : 0o600))
69
+ )
70
+ throw new Error("The agent status storage is not a bounded private file or directory.");
71
+ }
72
+
73
+ async function directoryTree(path, privateRoot) {
74
+ if (typeof path !== "string" || !isAbsolute(path) || resolve(path) !== path)
75
+ throw new Error("The agent status directory is invalid.");
76
+ let current = parse(path).root;
77
+ for (const part of path.slice(current.length).split(sep).filter(Boolean)) {
78
+ current = join(current, part);
79
+ const info = await lstat(current);
80
+ if (!info.isDirectory() || info.isSymbolicLink())
81
+ throw new Error("The agent status directory must not contain symbolic links.");
82
+ if (current === privateRoot || current.startsWith(`${privateRoot}${sep}`))
83
+ privateEntry(info, true);
84
+ }
85
+ }
86
+
87
+ async function verifyRuntimeDirectory(directory) {
88
+ await directoryTree(directory, dirname(dirname(dirname(directory))));
89
+ }
90
+
91
+ async function runtimeDirectory(profile, create = false) {
92
+ await directoryTree(profile.stateDirectory, dirname(dirname(profile.stateDirectory)));
93
+ const path = join(profile.stateDirectory, "runner");
94
+ if (create) {
95
+ try {
96
+ await mkdir(path, { mode: 0o700 });
97
+ } catch (error) {
98
+ if (error.code !== "EEXIST") throw error;
99
+ }
100
+ }
101
+ await verifyRuntimeDirectory(path);
102
+ return path;
103
+ }
104
+
105
+ async function readPrivateJson(path) {
106
+ await verifyRuntimeDirectory(dirname(path));
107
+ for (let attempt = 0; ; attempt += 1) {
108
+ const info = await lstat(path);
109
+ // macOS may finish a path lookup against the inode an atomic rename just
110
+ // unlinked. Retry that snapshot only, after all other safety checks pass.
111
+ privateEntry(info, false, true);
112
+ if (info.nlink === 1) break;
113
+ if (attempt === 2)
114
+ throw new Error("The agent status record kept changing during inspection. Retry shortly.");
115
+ }
116
+ const file = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
117
+ try {
118
+ // Replacement may unlink this already-open snapshot. It is still bounded and
119
+ // private; multiple hardlinks remain forbidden on both the path and descriptor.
120
+ privateEntry(await file.stat(), false, true);
121
+ const buffer = Buffer.alloc(4097);
122
+ let length = 0;
123
+ while (length < buffer.length) {
124
+ const { bytesRead } = await file.read(buffer, length, buffer.length - length, length);
125
+ if (!bytesRead) break;
126
+ length += bytesRead;
127
+ }
128
+ if (length > 4096) throw new Error("The agent status record exceeds its size limit.");
129
+ try {
130
+ return JSON.parse(buffer.subarray(0, length).toString("utf8"));
131
+ } catch {
132
+ throw new Error("The saved agent status is invalid.");
133
+ }
134
+ } finally {
135
+ await file.close();
136
+ }
137
+ }
138
+
139
+ async function writePrivateJson(path, record) {
140
+ await verifyRuntimeDirectory(dirname(path));
141
+ const body = `${JSON.stringify(record)}\n`;
142
+ if (Buffer.byteLength(body) > 4096) throw new Error("The agent status record is too large.");
143
+ const file = await open(
144
+ path,
145
+ constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0),
146
+ 0o600,
147
+ );
148
+ try {
149
+ await file.writeFile(body);
150
+ } finally {
151
+ await file.close();
152
+ }
153
+ }
154
+
155
+ async function saveStatus(directory, status) {
156
+ const temporary = join(directory, `.${status.runId}.${randomUUID()}.tmp`);
157
+ try {
158
+ await writePrivateJson(temporary, status);
159
+ const destination = join(directory, `${status.runId}.json`);
160
+ try {
161
+ privateEntry(await lstat(destination));
162
+ } catch (error) {
163
+ if (error.code !== "ENOENT") throw error;
164
+ }
165
+ await verifyRuntimeDirectory(directory);
166
+ await rename(temporary, destination);
167
+ } finally {
168
+ await unlink(temporary).catch(() => {});
169
+ }
170
+ }
171
+
172
+ export async function readAgentRuntimeStatus(profile, { isAlive = alive, now = Date.now } = {}) {
173
+ let directory;
174
+ try {
175
+ directory = await runtimeDirectory(profile);
176
+ } catch (error) {
177
+ if (error?.code === "ENOENT") return { running: false, phase: "not_started" };
178
+ throw error;
179
+ }
180
+ const names = [];
181
+ let entryCount = 0;
182
+ for await (const entry of await opendir(directory)) {
183
+ entryCount += 1;
184
+ if (UUID.test(entry.name.replace(/\.json$/, "")) && entry.name.endsWith(".json"))
185
+ names.push(entry.name);
186
+ if (names.length > 1000 || entryCount > 2000)
187
+ throw new Error(
188
+ "Too many saved agent sessions. Inspect the private status directory before restarting.",
189
+ );
190
+ }
191
+ const records = [];
192
+ for (const name of names) {
193
+ const record = await readPrivateJson(join(directory, name));
194
+ if (
195
+ !record ||
196
+ typeof record !== "object" ||
197
+ Array.isArray(record) ||
198
+ record.version !== 1 ||
199
+ record.runId !== name.slice(0, -5) ||
200
+ !PHASES.has(record.phase) ||
201
+ record.agentId !== profile.agentId ||
202
+ record.baseUrl !== profile.baseUrl ||
203
+ !Number.isSafeInteger(record.pid) ||
204
+ record.pid < 1 ||
205
+ typeof record.updatedAt !== "string" ||
206
+ typeof record.startedAt !== "string" ||
207
+ !Number.isFinite(Date.parse(record.updatedAt)) ||
208
+ !Number.isFinite(Date.parse(record.startedAt)) ||
209
+ Date.parse(record.startedAt) > Date.parse(record.updatedAt) ||
210
+ Date.parse(record.updatedAt) > now() + 5000
211
+ )
212
+ throw new Error("The saved agent status is invalid. Inspect it before restarting.");
213
+ records.push(record);
214
+ }
215
+ records.sort((left, right) => Date.parse(right.startedAt) - Date.parse(left.startedAt));
216
+ const running = records.find(
217
+ (record) => !["stopped", "failed"].includes(record.phase) && isAlive(record.pid),
218
+ );
219
+ const record = running ?? records[0];
220
+ if (!record) return { running: false, phase: "not_started" };
221
+ const fresh = now() - Date.parse(record.updatedAt) <= 30_000;
222
+ return {
223
+ running: Boolean(running),
224
+ phase: running
225
+ ? fresh
226
+ ? record.phase
227
+ : "unresponsive"
228
+ : ["stopped", "failed"].includes(record.phase)
229
+ ? record.phase
230
+ : "interrupted",
231
+ runId: record.runId,
232
+ updatedAt: record.updatedAt,
233
+ ...(UUID.test(record.missionId ?? "") ? { missionId: record.missionId } : {}),
234
+ ...(UUID.test(record.workItemId ?? "") ? { workItemId: record.workItemId } : {}),
235
+ };
236
+ }
237
+
238
+ function eventState(event) {
239
+ const candidate = event?.state ?? event?.status ?? event?.type;
240
+ const phase =
241
+ typeof candidate === "string" ? candidate.replace(/^mission_agent_|^agent_/, "") : "";
242
+ if (
243
+ ["reconnecting", "runtime_reconnecting"].includes(phase) ||
244
+ [event?.code, event?.reason].some((code) =>
245
+ ["service_unavailable", "runtime_reconnecting"].includes(code),
246
+ )
247
+ )
248
+ return { phase: "reconnecting" };
249
+ if (phase === "completed")
250
+ return {
251
+ phase: "waiting",
252
+ ...(UUID.test(event.missionId ?? "") ? { missionId: event.missionId } : {}),
253
+ };
254
+ if (!["connected", "working", "waiting", "blocked"].includes(phase)) return null;
255
+ return {
256
+ phase,
257
+ ...(UUID.test(event.missionId ?? "") ? { missionId: event.missionId } : {}),
258
+ ...(UUID.test(event.workItemId ?? "") ? { workItemId: event.workItemId } : {}),
259
+ };
260
+ }
261
+
262
+ /** Only fixed labels and validated IDs leave the local worker; model output never becomes a log. */
263
+ export async function runAgentSession(profile, options = {}) {
264
+ if (profile.client)
265
+ throw new Error(
266
+ "This profile connects an existing AI app. Automatic Missions currently require a separate local-agent profile.",
267
+ );
268
+ const intervalMs = options.statusIntervalMs ?? 1000;
269
+ if (!Number.isInteger(intervalMs) || intervalMs < 10 || intervalMs > 60_000)
270
+ throw new Error("Invalid agent status interval.");
271
+ const apiKey = await readAgentCredential(profile);
272
+ const client = (
273
+ options.createClient ?? ((input) => new Totem({ ...input, timeoutMs: 10_000, maxRetries: 0 }))
274
+ )({ apiKey, baseUrl: profile.baseUrl });
275
+ const identity = await client.agent({ signal: options.signal });
276
+ if (
277
+ identity.id !== profile.agentId ||
278
+ identity.environment !== profile.environment ||
279
+ identity.address !== profile.address
280
+ )
281
+ throw new Error("This credential no longer matches the saved agent. Run setup again.");
282
+ const runId = options.runId ?? randomUUID();
283
+ if (!UUID.test(runId)) throw new Error("Invalid agent run identifier.");
284
+ const directory = join(profile.stateDirectory, "runner");
285
+ const controller = new AbortController();
286
+ const stop = () => controller.abort();
287
+ options.signal?.addEventListener("abort", stop, { once: true });
288
+ if (options.signal?.aborted) controller.abort();
289
+ let status = {
290
+ version: 1,
291
+ runId,
292
+ pid: process.pid,
293
+ agentId: profile.agentId,
294
+ baseUrl: profile.baseUrl,
295
+ startedAt: new Date().toISOString(),
296
+ updatedAt: new Date().toISOString(),
297
+ phase: "starting",
298
+ };
299
+ let writes = Promise.resolve();
300
+ let persistenceFailed = false;
301
+ const persist = () => {
302
+ const snapshot = { ...status, updatedAt: new Date().toISOString() };
303
+ writes = writes
304
+ .then(() => saveStatus(directory, snapshot))
305
+ .catch(() => {
306
+ persistenceFailed = true;
307
+ controller.abort();
308
+ });
309
+ return writes;
310
+ };
311
+ const emit = (state) => {
312
+ status = { ...status, missionId: undefined, workItemId: undefined, ...state };
313
+ try {
314
+ options.emit?.({
315
+ phase: status.phase,
316
+ message: LABELS[status.phase],
317
+ ...(status.missionId ? { missionId: status.missionId } : {}),
318
+ });
319
+ } catch {
320
+ persistenceFailed = true;
321
+ controller.abort();
322
+ }
323
+ void persist();
324
+ };
325
+ try {
326
+ await withAgentProfileLock(profile, async () => {
327
+ const before = await readAgentRuntimeStatus(profile);
328
+ if (before.running)
329
+ throw new Error(
330
+ "This agent is already running. Use totem status or totem stop for this profile.",
331
+ );
332
+ await runtimeDirectory(profile, true);
333
+ try {
334
+ await lstat(join(directory, `${runId}.json`));
335
+ throw new Error("An agent run identifier cannot be reused.");
336
+ } catch (error) {
337
+ if (error.code !== "ENOENT") throw error;
338
+ }
339
+ await persist();
340
+ if (persistenceFailed) throw new Error("Could not save agent status. No worker was started.");
341
+ });
342
+ } catch (error) {
343
+ options.signal?.removeEventListener("abort", stop);
344
+ await writes;
345
+ throw error;
346
+ }
347
+ let polling;
348
+ let stopReadFailed = false;
349
+ const poll = async () => {
350
+ try {
351
+ const request = await readPrivateJson(join(directory, `${runId}.stop`));
352
+ if (
353
+ !request ||
354
+ request.runId !== runId ||
355
+ Object.keys(request).some((key) => key !== "runId")
356
+ )
357
+ throw new Error("The existing stop request is invalid.");
358
+ controller.abort();
359
+ } catch (error) {
360
+ if (error?.code !== "ENOENT") {
361
+ stopReadFailed = true;
362
+ controller.abort();
363
+ }
364
+ }
365
+ await persist();
366
+ };
367
+ let timer;
368
+ let result = 0;
369
+ let acceptingEvents = true;
370
+ try {
371
+ await poll();
372
+ timer = setInterval(() => {
373
+ if (!polling)
374
+ polling = poll().finally(() => {
375
+ polling = undefined;
376
+ });
377
+ }, intervalMs);
378
+ if (!controller.signal.aborted) {
379
+ const runService =
380
+ options.runService ?? (await import("./mission-agent-service.mjs")).runMissionAgentService;
381
+ await runService({
382
+ client,
383
+ expectedAgentId: profile.agentId,
384
+ baseUrl: profile.baseUrl,
385
+ stateDirectory: profile.stateDirectory,
386
+ repositoryBindings: profile.repositoryRoots.map(({ repositoryId, root }) => ({
387
+ repositoryUrl: repositoryId,
388
+ root,
389
+ })),
390
+ provider: profile.provider,
391
+ qwenUrl: profile.qwenUrl,
392
+ model: profile.model,
393
+ signal: controller.signal,
394
+ emit: (event) => {
395
+ if (!acceptingEvents) return;
396
+ const state = eventState(event);
397
+ if (state) emit(state);
398
+ },
399
+ });
400
+ }
401
+ if (persistenceFailed || stopReadFailed)
402
+ throw new Error("Agent status could not be saved; the worker was stopped.");
403
+ } catch {
404
+ result = 1;
405
+ controller.abort();
406
+ } finally {
407
+ acceptingEvents = false;
408
+ clearInterval(timer);
409
+ await polling;
410
+ options.signal?.removeEventListener("abort", stop);
411
+ await writes;
412
+ if (persistenceFailed || stopReadFailed) result = 1;
413
+ emit({ phase: result ? "failed" : "stopped" });
414
+ await writes;
415
+ if (persistenceFailed) result = 1;
416
+ await unlink(join(directory, `${runId}.stop`)).catch(() => {});
417
+ }
418
+ return result;
419
+ }
420
+
421
+ function runnerOptions(args) {
422
+ const parsed = parseArgs({
423
+ args,
424
+ strict: true,
425
+ allowPositionals: false,
426
+ options: {
427
+ profile: { type: "string", default: "default" },
428
+ background: { type: "boolean" },
429
+ json: { type: "boolean" },
430
+ help: { type: "boolean", short: "h" },
431
+ "run-id": { type: "string" },
432
+ },
433
+ });
434
+ return parsed.values;
435
+ }
436
+
437
+ async function requestStop(profile, runId) {
438
+ if (!UUID.test(runId)) throw new Error("Invalid agent run identifier.");
439
+ const directory = await runtimeDirectory(profile, true);
440
+ const path = join(directory, `${runId}.stop`);
441
+ const temporary = join(directory, `.${runId}.${randomUUID()}.stop.tmp`);
442
+ try {
443
+ // Only publish a closed, complete record. Polling must never see the empty
444
+ // or partially written file while an ordinary stop request is being saved.
445
+ await writePrivateJson(temporary, { runId });
446
+ try {
447
+ const existing = await readPrivateJson(path);
448
+ if (
449
+ !existing ||
450
+ existing.runId !== runId ||
451
+ Object.keys(existing).some((key) => key !== "runId")
452
+ )
453
+ throw new Error("The existing stop request is invalid.");
454
+ return;
455
+ } catch (error) {
456
+ if (error?.code !== "ENOENT") throw error;
457
+ }
458
+ await verifyRuntimeDirectory(directory);
459
+ await rename(temporary, path);
460
+ } finally {
461
+ await unlink(temporary).catch(() => {});
462
+ }
463
+ }
464
+
465
+ export async function agentStartMain(args, options = {}) {
466
+ const environment = options.env ?? process.env;
467
+ const output = options.stdout ?? process.stdout;
468
+ const parsed = runnerOptions(args);
469
+ if (parsed.help) {
470
+ output.write(AGENT_START_HELP);
471
+ return 0;
472
+ }
473
+ const profile = await readAgentProfile(parsed.profile, { environment });
474
+ if (profile.client)
475
+ throw new Error("Existing-client MCP profiles cannot start automatic Missions.");
476
+ const report = (value) =>
477
+ output.write(`${parsed.json ? JSON.stringify(value) : value.message}\n`);
478
+ if (parsed.background) {
479
+ if (parsed["run-id"])
480
+ throw new Error("An internal run identifier cannot be combined with background mode.");
481
+ const existing = await readAgentRuntimeStatus(profile);
482
+ if (existing.running) {
483
+ report({
484
+ ...existing,
485
+ message:
486
+ existing.phase === "unresponsive"
487
+ ? "The existing agent session is unresponsive. Inspect its status before restarting."
488
+ : "This agent is already running.",
489
+ });
490
+ return 0;
491
+ }
492
+ const runId = randomUUID();
493
+ const env = localClientEnvironment(environment, {
494
+ ...(environment.TOTEM_CONFIG_DIR ? { TOTEM_CONFIG_DIR: environment.TOTEM_CONFIG_DIR } : {}),
495
+ });
496
+ delete env.TOTEM_NETWORK_API_KEY_FILE;
497
+ delete env.TOTEM_BASE_URL;
498
+ const child = (options.spawnProcess ?? spawn)(
499
+ process.execPath,
500
+ [CLI_PATH, "start", "--profile", parsed.profile, "--run-id", runId, "--json"],
501
+ { env, detached: true, stdio: "ignore", windowsHide: true },
502
+ );
503
+ let failed = false;
504
+ child.once("error", () => {
505
+ failed = true;
506
+ });
507
+ child.once("exit", () => {
508
+ failed = true;
509
+ });
510
+ child.unref();
511
+ try {
512
+ const startupTimeoutMs = options.startupTimeoutMs ?? 10_000;
513
+ if (
514
+ !Number.isInteger(startupTimeoutMs) ||
515
+ startupTimeoutMs < 10 ||
516
+ startupTimeoutMs > 120_000
517
+ )
518
+ throw new Error("Invalid background startup deadline.");
519
+ const deadline = Date.now() + startupTimeoutMs;
520
+ while (Date.now() < deadline) {
521
+ await new Promise((resolve) =>
522
+ setTimeout(resolve, Math.min(250, Math.max(1, deadline - Date.now()))),
523
+ );
524
+ if (failed) break;
525
+ const state = await readAgentRuntimeStatus(profile);
526
+ if (
527
+ state.runId === runId &&
528
+ state.running &&
529
+ ["connected", "working", "waiting", "blocked"].includes(state.phase)
530
+ ) {
531
+ report({
532
+ ...state,
533
+ message:
534
+ "Agent started in the background. Use totem status to check its connection and work.",
535
+ });
536
+ return 0;
537
+ }
538
+ if (state.runId === runId && ["failed", "stopped", "interrupted"].includes(state.phase))
539
+ break;
540
+ }
541
+ } catch {
542
+ // The run-bound stop is also requested when a status read fails closed.
543
+ }
544
+ await requestStop(profile, runId);
545
+ throw new Error(
546
+ "The background agent did not confirm startup. Run totem start without --background to check its setup.",
547
+ );
548
+ }
549
+ const controller = new AbortController();
550
+ const stop = () => controller.abort();
551
+ process.once("SIGINT", stop);
552
+ process.once("SIGTERM", stop);
553
+ try {
554
+ return await runAgentSession(profile, {
555
+ ...options,
556
+ runId: parsed["run-id"],
557
+ signal: options.signal
558
+ ? AbortSignal.any([controller.signal, options.signal])
559
+ : controller.signal,
560
+ emit: report,
561
+ });
562
+ } finally {
563
+ process.removeListener("SIGINT", stop);
564
+ process.removeListener("SIGTERM", stop);
565
+ }
566
+ }
567
+
568
+ export async function agentStopMain(args, options = {}) {
569
+ const parsed = runnerOptions(args);
570
+ const output = options.stdout ?? process.stdout;
571
+ if (parsed.help) {
572
+ output.write(AGENT_START_HELP);
573
+ return 0;
574
+ }
575
+ const profile = await readAgentProfile(parsed.profile, {
576
+ environment: options.env ?? process.env,
577
+ });
578
+ const state = await readAgentRuntimeStatus(profile);
579
+ if (state.running) await requestStop(profile, state.runId);
580
+ const result = {
581
+ requested: state.running,
582
+ message: state.running
583
+ ? "Stop requested. The worker will cancel pending work and preserve its recovery journal."
584
+ : "This agent is not running.",
585
+ };
586
+ output.write(`${parsed.json ? JSON.stringify(result) : result.message}\n`);
587
+ return 0;
588
+ }
589
+
590
+ export async function agentRunnerMain(command, args, options = {}) {
591
+ try {
592
+ return command === "stop"
593
+ ? await agentStopMain(args, options)
594
+ : await agentStartMain(args, options);
595
+ } catch {
596
+ const output = args.includes("--json")
597
+ ? (options.stdout ?? process.stdout)
598
+ : (options.stderr ?? process.stderr);
599
+ const result = {
600
+ error: {
601
+ code: "AGENT_START_FAILED",
602
+ message:
603
+ "Could not change the agent's running state. Run totem status for this profile, check its setup, then retry. Existing-client MCP profiles do not run automatic Missions.",
604
+ },
605
+ };
606
+ output.write(`${args.includes("--json") ? JSON.stringify(result) : result.error.message}\n`);
607
+ return 1;
608
+ }
609
+ }