@cydm/happy-elves 0.1.0-beta.3 → 0.1.0-beta.5

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 (50) hide show
  1. package/apps/cli/dist/index.js +506 -20
  2. package/apps/cli/dist/index.js.map +1 -1
  3. package/apps/daemon/dist/cli.js +67 -10
  4. package/apps/daemon/dist/cli.js.map +1 -1
  5. package/apps/daemon/dist/turn-coordinator.d.ts +26 -0
  6. package/apps/daemon/dist/turn-coordinator.js +77 -0
  7. package/apps/daemon/dist/turn-coordinator.js.map +1 -0
  8. package/apps/relay/dist/auth.d.ts +1 -0
  9. package/apps/relay/dist/auth.js +16 -0
  10. package/apps/relay/dist/auth.js.map +1 -0
  11. package/apps/relay/dist/connections.d.ts +11 -0
  12. package/apps/relay/dist/connections.js +11 -0
  13. package/apps/relay/dist/connections.js.map +1 -0
  14. package/apps/relay/dist/controller-handlers.d.ts +40 -0
  15. package/apps/relay/dist/controller-handlers.js +322 -0
  16. package/apps/relay/dist/controller-handlers.js.map +1 -0
  17. package/apps/relay/dist/db.d.ts +5 -0
  18. package/apps/relay/dist/db.js +8 -0
  19. package/apps/relay/dist/db.js.map +1 -1
  20. package/apps/relay/dist/http-schemas.d.ts +1 -1
  21. package/apps/relay/dist/http-schemas.js +4 -4
  22. package/apps/relay/dist/http-schemas.js.map +1 -1
  23. package/apps/relay/dist/index.js +94 -564
  24. package/apps/relay/dist/index.js.map +1 -1
  25. package/apps/relay/dist/machine-handlers.d.ts +30 -0
  26. package/apps/relay/dist/machine-handlers.js +175 -0
  27. package/apps/relay/dist/machine-handlers.js.map +1 -0
  28. package/apps/relay/dist/repositories.d.ts +13 -0
  29. package/apps/relay/dist/repositories.js +60 -0
  30. package/apps/relay/dist/repositories.js.map +1 -0
  31. package/apps/relay/dist/security.d.ts +0 -1
  32. package/apps/relay/dist/security.js +0 -3
  33. package/apps/relay/dist/security.js.map +1 -1
  34. package/package.json +1 -1
  35. package/packages/client/dist/index.d.ts +8 -1
  36. package/packages/client/dist/index.d.ts.map +1 -1
  37. package/packages/client/dist/index.js +56 -22
  38. package/packages/client/dist/index.js.map +1 -1
  39. package/packages/runtime/dist/index.d.ts +2 -0
  40. package/packages/runtime/dist/index.d.ts.map +1 -1
  41. package/packages/runtime-cli/dist/codex-app-server.d.ts +5 -1
  42. package/packages/runtime-cli/dist/codex-app-server.d.ts.map +1 -1
  43. package/packages/runtime-cli/dist/codex-app-server.js +109 -13
  44. package/packages/runtime-cli/dist/codex-app-server.js.map +1 -1
  45. package/packages/runtime-cli/dist/index.d.ts.map +1 -1
  46. package/packages/runtime-cli/dist/index.js +16 -2
  47. package/packages/runtime-cli/dist/index.js.map +1 -1
  48. package/packages/shared/dist/protocol.d.ts +3 -2
  49. package/packages/shared/dist/protocol.d.ts.map +1 -1
  50. package/packages/shared/dist/protocol.js.map +1 -1
@@ -21,12 +21,15 @@ const valueFlags = new Set([
21
21
  "machine-id",
22
22
  "name",
23
23
  "port",
24
+ "permission",
25
+ "permission-mode",
24
26
  "prompt",
25
27
  "every",
26
28
  "reason",
27
29
  "relay",
28
30
  "actions",
29
31
  "expires-in",
32
+ "format",
30
33
  "loops",
31
34
  "machines",
32
35
  "sessions",
@@ -37,6 +40,7 @@ const valueFlags = new Set([
37
40
  "text",
38
41
  "text-file",
39
42
  "timeout",
43
+ "runtime-timeout",
40
44
  "to",
41
45
  "token",
42
46
  "ttl",
@@ -50,14 +54,44 @@ const configDir = process.env.HAPPY_ELVES_HOME
50
54
  ? path.resolve(process.env.HAPPY_ELVES_HOME)
51
55
  : path.join(os.homedir(), ".happy-elves");
52
56
  const configPath = path.join(configDir, "controller.json");
57
+ const outputRoot = path.join(configDir, "outputs");
53
58
  const defaultHostedRelayUrl = process.env.HAPPY_ELVES_DEFAULT_RELAY_URL ?? "https://relay.happyelves.ai";
54
59
  const defaultHostedControllerUrl = process.env.HAPPY_ELVES_DEFAULT_CONTROLLER_URL ?? "https://happyelves.ai";
55
60
  const startDaemonTimeoutMs = 10_000;
56
- function usage() {
57
- writeError("USAGE", `happy-elves
61
+ function usageText() {
62
+ return `happy-elves
63
+
64
+ What it is:
65
+ Happy Elves is a remote control plane for coding agents. Use it to discover
66
+ machines, inspect workspaces, resume existing runtime sessions, create worker
67
+ sessions, run prompts, collect outputs, and manage scoped automation tokens.
68
+
69
+ When to use it:
70
+ Use happy-elves when a user asks you to work with Happy Elves machines,
71
+ projects, workspaces, sessions, or remote agents. Do not bypass Happy Elves by
72
+ operating on a local checkout just because a similarly named project exists on
73
+ your machine.
74
+
75
+ Agent usage guidance:
76
+ Prefer id-first commands and --json output when another agent will parse the
77
+ result. Use human output for direct terminal inspection.
78
+
79
+ Recommended agent workflow:
80
+ 1. machine list --json
81
+ 2. workspace sessions --machine <machineId> --cwd <path> --json
82
+ 3. session list [--machine <machineId>] --json
83
+ 4. session create --machine <machineId> --agent codex --cwd <path> --json
84
+ 5. run <sessionId> --text <prompt> --wait --json
85
+ 6. result <sessionId> --json
86
+
87
+ Output modes:
88
+ Human output is concise by default. Add --json when another program or agent
89
+ needs a stable envelope. Add --verbose to history/collect when raw events are
90
+ needed for debugging.
58
91
 
59
92
  Start:
60
93
  start [--relay <url>] [--controller <url>] [--cwd <path>] [--no-open] [--json]
94
+ status [--json]
61
95
  doctor [--relay <url>] [--json]
62
96
 
63
97
  Config:
@@ -79,22 +113,27 @@ Machines:
79
113
  machine show <machineId> --json
80
114
  machine status <machineId> --json
81
115
 
116
+ Workspaces:
117
+ workspace sessions --cwd <path> [--machine <machineId>] [--agent codex] [--limit 50] [--json]
118
+
82
119
  Sessions:
83
- session create --machine <machineId> [--agent codex] [--cwd <path>] [--name <name>] --json
84
- session list [--machine <machineId>] [--agent <agent>] [--cwd <cwd>] [--name <name>] --json
120
+ session create --machine <machineId> [--agent codex] [--cwd <path>] [--name <name>] [--permission-mode <mode>] --json
121
+ session list [--machine <machineId>] [--agent <agent>] [--cwd <cwd>] [--name <name>] [--summary] [--verbose] [--json]
85
122
  session show <sessionId> --json
86
123
  session status <sessionId> --json
87
- session history <sessionId> [--limit 20] --json
124
+ session history <sessionId> [--limit 20] [--verbose] [--json] # debug/API events
125
+ session last-output <sessionId> [--json]
88
126
  session resume <sessionId> --json
89
127
  session rewind <sessionId> --to <turn-or-checkpoint-id> --json
90
128
  session fork <sessionId> --from <turn-or-checkpoint-id> --name <name> --json
91
129
  session close <sessionId> [--reason <reason>] --json
92
130
 
93
131
  Run:
94
- run <sessionId> --text <prompt> [--no-wait] --json
95
- run <sessionId> --text-file <path> [--no-wait] --json
132
+ run <sessionId> --text <prompt> [--wait|--detach] [--timeout 30m] [--runtime-timeout 30m] [--permission-mode approve-reads|approve-all|deny-all] [--json]
133
+ run <sessionId> --text-file <path> [--wait|--detach] [--timeout 30m] [--runtime-timeout 30m] [--permission-mode approve-reads|approve-all|deny-all] [--json]
134
+ result <sessionId> [--turn <turnId>] [--json]
96
135
  wait <sessionId> [--until idle|ready|running|completed|failed|cancelled|offline|closed] [--timeout 30m] --json
97
- collect <sessionId> [--since <eventId>] [--limit 20] --json
136
+ collect <sessionId> [--since <eventId|event:id|cursor_id>] [--limit 20] [--verbose] [--json] # debug/API events
98
137
  cancel <sessionId> [--turn <turnId>] [--reason <reason>] --json
99
138
 
100
139
  Scoped Tokens:
@@ -125,7 +164,19 @@ Relay:
125
164
  relay serve [--host 0.0.0.0] [--port 8787] --json
126
165
  relay status [--relay <url>] --json
127
166
  relay doctor [--relay <url>] --json
128
- `);
167
+
168
+ Security notes:
169
+ Account secrets stay on controllers and daemons. Relay payloads are encrypted.
170
+ Scoped tokens should include only the actions they need, such as snapshot,
171
+ session.run, collect, directory.list, loop.create, or loop.run.
172
+ `;
173
+ }
174
+ function usage() {
175
+ if (!process.argv.includes("--json")) {
176
+ console.error(usageText());
177
+ process.exit(1);
178
+ }
179
+ writeError("USAGE", usageText());
129
180
  process.exit(1);
130
181
  }
131
182
  function parseFlags(argv) {
@@ -367,6 +418,197 @@ function deriveOrchestration(session, events = []) {
367
418
  async function showSession(client, session, events = []) {
368
419
  return { ...session, metadata: await client.decodeSessionMetadata(session), orchestration: deriveOrchestration(session, events) };
369
420
  }
421
+ function wantsJson(flags) {
422
+ return flags.json === true;
423
+ }
424
+ function wantsVerbose(flags) {
425
+ return flags.verbose === true || flags.format === "verbose";
426
+ }
427
+ function eventText(event) {
428
+ const payload = event.decoded;
429
+ if (!payload)
430
+ return event.decryptError ? `[decrypt error] ${event.decryptError}` : "";
431
+ if (payload.type === "user_prompt")
432
+ return payload.text.trim();
433
+ if (payload.type === "text_delta" && payload.stream !== "thought")
434
+ return payload.text.trim();
435
+ if (payload.type === "control")
436
+ return payload.text.trim();
437
+ if (payload.type === "done")
438
+ return payload.error?.trim() ?? payload.stopReason?.trim() ?? "";
439
+ return "";
440
+ }
441
+ function compactText(value, max = 220) {
442
+ const normalized = value.replace(/\s+/g, " ").trim();
443
+ return normalized.length > max ? `${normalized.slice(0, max - 1)}…` : normalized;
444
+ }
445
+ function lastAssistantOutput(events) {
446
+ let currentTurn = "";
447
+ const chunks = [];
448
+ for (const event of events) {
449
+ const payload = event.decoded;
450
+ if (payload?.type !== "text_delta" || payload.stream === "thought" || !payload.text)
451
+ continue;
452
+ if (event.turnId !== currentTurn) {
453
+ currentTurn = event.turnId;
454
+ chunks.length = 0;
455
+ }
456
+ chunks.push(payload.text);
457
+ }
458
+ return chunks.join("").trim();
459
+ }
460
+ function turnStatus(events, turnId) {
461
+ const done = [...events].reverse().find((event) => event.turnId === turnId && event.decoded?.type === "done")?.decoded;
462
+ if (done?.type !== "done")
463
+ return "unknown";
464
+ return done.status;
465
+ }
466
+ function assistantOutputForTurn(events, turnId) {
467
+ return events
468
+ .filter((event) => event.turnId === turnId && event.decoded?.type === "text_delta" && event.decoded.stream !== "thought")
469
+ .map((event) => event.decoded?.type === "text_delta" ? event.decoded.text : "")
470
+ .join("")
471
+ .trim();
472
+ }
473
+ function outputPaths(sessionId, turnId) {
474
+ const dir = path.join(outputRoot, sessionId);
475
+ const latest = path.join(dir, "latest.md");
476
+ return {
477
+ dir,
478
+ latest,
479
+ latestMetadata: path.join(dir, "latest.json"),
480
+ ...(turnId ? {
481
+ turn: path.join(dir, `${turnId}.md`),
482
+ metadata: path.join(dir, `${turnId}.json`),
483
+ } : {}),
484
+ };
485
+ }
486
+ function displayPath(filePath) {
487
+ const relative = path.relative(process.cwd(), filePath);
488
+ return relative.startsWith("..") ? filePath : relative || ".";
489
+ }
490
+ async function writeTurnOutput(sessionId, turnId, events) {
491
+ const paths = outputPaths(sessionId, turnId);
492
+ if (!paths.turn || !paths.metadata)
493
+ throw new CliError("Output path construction failed", "OUTPUT_FAILED");
494
+ await fs.mkdir(paths.dir, { recursive: true });
495
+ const output = assistantOutputForTurn(events, turnId);
496
+ const turnEvents = events.filter((event) => event.turnId === turnId);
497
+ const metadata = {
498
+ sessionId,
499
+ turnId,
500
+ status: turnStatus(events, turnId),
501
+ startedAt: turnEvents[0]?.createdAt,
502
+ completedAt: Date.now(),
503
+ eventCount: turnEvents.length,
504
+ outputPath: paths.turn,
505
+ latestPath: paths.latest,
506
+ };
507
+ await fs.writeFile(paths.turn, output ? `${output}\n` : "", "utf8");
508
+ await fs.writeFile(paths.metadata, `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
509
+ await fs.writeFile(paths.latestMetadata, `${JSON.stringify({ sessionId, turnId, metadataPath: paths.metadata, outputPath: paths.turn }, null, 2)}\n`, "utf8");
510
+ await fs.rm(paths.latest, { force: true });
511
+ try {
512
+ await fs.symlink(path.basename(paths.turn), paths.latest);
513
+ }
514
+ catch {
515
+ await fs.copyFile(paths.turn, paths.latest);
516
+ }
517
+ return { ...metadata, output, metadataPath: paths.metadata };
518
+ }
519
+ async function readStoredTurnOutput(sessionId, turnId) {
520
+ const paths = outputPaths(sessionId, turnId);
521
+ let outputPath = paths.turn;
522
+ let metadataPath = paths.metadata;
523
+ if (!turnId) {
524
+ outputPath = paths.latest;
525
+ const latest = await fs.readFile(paths.latestMetadata, "utf8").then((value) => JSON.parse(value)).catch(() => undefined);
526
+ if (latest && typeof latest.turnId === "string") {
527
+ metadataPath = typeof latest.metadataPath === "string" ? latest.metadataPath : path.join(paths.dir, `${latest.turnId}.json`);
528
+ outputPath = typeof latest.outputPath === "string" ? latest.outputPath : outputPath;
529
+ }
530
+ else {
531
+ const real = await fs.realpath(paths.latest).catch(() => paths.latest);
532
+ const match = path.basename(real).match(/^(.*)\.md$/);
533
+ if (match && match[1] !== "latest")
534
+ metadataPath = path.join(paths.dir, `${match[1]}.json`);
535
+ }
536
+ }
537
+ if (!outputPath)
538
+ return undefined;
539
+ const output = await fs.readFile(outputPath, "utf8").catch(() => undefined);
540
+ if (output === undefined)
541
+ return undefined;
542
+ const metadata = metadataPath ? await fs.readFile(metadataPath, "utf8").then((value) => JSON.parse(value)).catch(() => ({})) : {};
543
+ const resolvedTurnId = typeof metadata.turnId === "string" ? metadata.turnId : turnId ?? path.basename(outputPath, ".md");
544
+ return {
545
+ sessionId,
546
+ turnId: resolvedTurnId,
547
+ status: typeof metadata.status === "string" ? metadata.status : "unknown",
548
+ output: output.trimEnd(),
549
+ outputPath,
550
+ latestPath: paths.latest,
551
+ metadataPath: metadataPath ?? path.join(paths.dir, `${resolvedTurnId}.json`),
552
+ startedAt: typeof metadata.startedAt === "number" ? metadata.startedAt : undefined,
553
+ completedAt: typeof metadata.completedAt === "number" ? metadata.completedAt : 0,
554
+ eventCount: typeof metadata.eventCount === "number" ? metadata.eventCount : 0,
555
+ };
556
+ }
557
+ async function rebuildTurnOutput(client, sessionId, turnId) {
558
+ const events = await client.history(sessionId, 1000);
559
+ const resolvedTurnId = turnId ?? [...events].reverse().find((event) => event.decoded?.type === "text_delta" || event.decoded?.type === "done")?.turnId;
560
+ if (!resolvedTurnId)
561
+ throw new CliError("No turn output found for session", "RESULT_NOT_FOUND");
562
+ if (turnId && !events.some((event) => event.turnId === turnId)) {
563
+ throw new CliError(`Turn ${turnId} was not found in the latest 1000 relay events. Re-run without --turn for the latest result, or use a local output file from ${displayPath(outputPaths(sessionId).dir)} if available.`, "RESULT_HISTORY_WINDOW_EXCEEDED");
564
+ }
565
+ return await writeTurnOutput(sessionId, resolvedTurnId, events);
566
+ }
567
+ function conciseEvents(events) {
568
+ const result = [];
569
+ for (const event of events) {
570
+ const payload = event.decoded;
571
+ if (!payload)
572
+ continue;
573
+ if (payload.type === "user_prompt")
574
+ result.push({ id: event.id, turnId: event.turnId, role: "user", text: payload.text });
575
+ if (payload.type === "text_delta" && payload.stream !== "thought" && payload.text.trim()) {
576
+ result.push({ id: event.id, turnId: event.turnId, role: "assistant", text: payload.text });
577
+ }
578
+ if (payload.type === "control" || payload.type === "done") {
579
+ const text = eventText(event);
580
+ if (text)
581
+ result.push({ id: event.id, turnId: event.turnId, role: payload.type, text });
582
+ }
583
+ }
584
+ return result;
585
+ }
586
+ function printConciseEvents(events) {
587
+ const concise = conciseEvents(events);
588
+ if (concise.length === 0) {
589
+ console.log("No readable output.");
590
+ return;
591
+ }
592
+ for (const event of concise) {
593
+ console.log(`[${event.id}] ${event.role}: ${event.text.trim()}`);
594
+ }
595
+ }
596
+ async function conciseSessionSummary(client, session) {
597
+ const metadata = await client.decodeSessionMetadata(session);
598
+ const events = await client.history(session.id, 50);
599
+ const stored = await readStoredTurnOutput(session.id).catch(() => undefined);
600
+ return {
601
+ sessionId: session.id,
602
+ name: metadata.name ?? session.name,
603
+ agent: metadata.agent ?? session.agent,
604
+ machineId: session.machineId,
605
+ cwd: metadata.cwd ?? session.cwd,
606
+ status: session.status,
607
+ updatedAt: session.updatedAt,
608
+ lastOutput: lastAssistantOutput(events),
609
+ latestOutputPath: stored?.latestPath,
610
+ };
611
+ }
370
612
  async function requireProjectedSession(client, sessionId, action) {
371
613
  const session = await client.getSession(sessionId);
372
614
  if (!session) {
@@ -408,6 +650,71 @@ async function machineSessionOverview(client, sessions, machineId) {
408
650
  sessions: projectedSessions,
409
651
  };
410
652
  }
653
+ async function listWorkspaceHistoricalSessions(client, input) {
654
+ const snapshot = await client.snapshot();
655
+ const selectedMachines = snapshot.machines.filter((machine) => {
656
+ if (input.machineId)
657
+ return machine.id === input.machineId;
658
+ return machine.online;
659
+ });
660
+ if (input.machineId && selectedMachines.length === 0)
661
+ throw new CliError("Machine not found", "MACHINE_NOT_FOUND");
662
+ const machines = await Promise.all(selectedMachines.map(async (machine) => ({
663
+ machine,
664
+ projected: await showMachine(client, machine),
665
+ })));
666
+ const rows = await Promise.all(machines.map(async ({ machine, projected }) => {
667
+ const machineName = projected.metadata.name ?? projected.metadata.hostname ?? machine.name;
668
+ try {
669
+ const sessions = await client.listHistoricalSessions({
670
+ machineId: machine.id,
671
+ cwd: input.cwd,
672
+ agent: input.agent,
673
+ limit: input.limit,
674
+ });
675
+ return {
676
+ sessions: sessions.map((session) => ({ ...session, machineId: machine.id, machineName })),
677
+ error: undefined,
678
+ machine: { machineId: machine.id, machineName },
679
+ };
680
+ }
681
+ catch (error) {
682
+ const enriched = error;
683
+ return {
684
+ sessions: [],
685
+ error: {
686
+ machineId: machine.id,
687
+ machineName,
688
+ code: enriched.code,
689
+ message: error instanceof Error ? error.message : String(error),
690
+ },
691
+ machine: { machineId: machine.id, machineName },
692
+ };
693
+ }
694
+ }));
695
+ return {
696
+ sessions: rows.flatMap((row) => row.sessions).sort((left, right) => right.updatedAt - left.updatedAt),
697
+ errors: rows.flatMap((row) => row.error ? [row.error] : []),
698
+ machines: rows.map((row) => row.machine),
699
+ };
700
+ }
701
+ function printWorkspaceHistoricalSessions(sessions, errors) {
702
+ if (sessions.length === 0) {
703
+ console.log("No runtime sessions found for this workspace.");
704
+ }
705
+ else {
706
+ console.log("MACHINE | AGENT | UPDATED | RUNTIME SESSION | LOADED | SUMMARY");
707
+ for (const session of sessions) {
708
+ const updated = new Date(session.updatedAt).toISOString();
709
+ const loaded = session.loadedSessionId ?? "-";
710
+ const summary = compactText(session.summary ?? session.runtimeSessionName ?? "");
711
+ console.log(`${session.machineName} (${session.machineId}) | ${session.agent} | ${updated} | ${session.runtimeSessionId} | ${loaded} | ${summary}`);
712
+ }
713
+ }
714
+ for (const error of errors) {
715
+ console.error(`Warning: ${error.machineName} (${error.machineId}) ${error.code ? `${error.code}: ` : ""}${error.message}`);
716
+ }
717
+ }
411
718
  function parseDurationMs(value, fallbackMs) {
412
719
  if (typeof value !== "string")
413
720
  return fallbackMs;
@@ -462,12 +769,29 @@ function parseEventCursor(value) {
462
769
  const match = normalized.match(/^cursor_(\d+)$/);
463
770
  if (match)
464
771
  return `cursor_${Number(match[1])}`;
772
+ const eventMatch = normalized.match(/^event:(\d+)$/);
773
+ if (eventMatch)
774
+ return Number(eventMatch[1]);
465
775
  const numeric = Number(normalized);
466
776
  if (!Number.isInteger(numeric) || numeric < 0) {
467
777
  throw new CliError(`Invalid --since cursor: ${value}`, "INVALID_ARGUMENT");
468
778
  }
469
779
  return numeric;
470
780
  }
781
+ function parsePermissionMode(value) {
782
+ if (value === undefined || value === false)
783
+ return undefined;
784
+ if (value === true)
785
+ return "approve-reads";
786
+ const normalized = value.trim().toLowerCase();
787
+ if (normalized === "yolo" || normalized === "approve-all")
788
+ return "approve-all";
789
+ if (normalized === "plan" || normalized === "deny-all" || normalized === "read-only")
790
+ return "deny-all";
791
+ if (normalized === "default" || normalized === "ask" || normalized === "accept-edits" || normalized === "approve-reads")
792
+ return "approve-reads";
793
+ throw new CliError(`Invalid permission mode: ${value}`, "INVALID_ARGUMENT");
794
+ }
471
795
  function parseControllerJoinUrl(value) {
472
796
  let url;
473
797
  try {
@@ -969,7 +1293,8 @@ function startRelayUrl(flags, existing) {
969
1293
  if (typeof flags.relay === "string") {
970
1294
  return { relayUrl: requireRelayUrl(flags), explicit: true };
971
1295
  }
972
- return { relayUrl: existing?.relayUrl ?? normalizeRelayUrl(defaultHostedRelayUrl), explicit: false };
1296
+ const relayUrl = normalizeRelayUrl(defaultHostedRelayUrl);
1297
+ return { relayUrl, explicit: existing?.relayUrl !== undefined && existing.relayUrl !== relayUrl };
973
1298
  }
974
1299
  function writeHumanStartOutput(data) {
975
1300
  console.log("Happy Elves is running.");
@@ -1126,11 +1451,55 @@ async function topLevelDoctor(flags) {
1126
1451
  checks.push({ name: "daemon-process", ok: local.running, message: local.running ? "running" : "not running", details: local });
1127
1452
  ok("doctor", { ok: checks.every((check) => check.ok), checks });
1128
1453
  }
1454
+ async function topLevelStatus(flags) {
1455
+ const config = await readConfig(flags);
1456
+ const client = new ControllerClient(config);
1457
+ const [local, snapshot] = await Promise.all([localDaemonStatus(), client.snapshot()]);
1458
+ const latestSession = [...snapshot.sessions].sort((left, right) => right.updatedAt - left.updatedAt)[0];
1459
+ const latestOutput = latestSession ? await readStoredTurnOutput(latestSession.id).catch(() => undefined) : undefined;
1460
+ const data = {
1461
+ relayUrl: config.relayUrl,
1462
+ daemon: local,
1463
+ machines: {
1464
+ count: snapshot.machines.length,
1465
+ onlineCount: snapshot.machines.filter((machine) => machine.online).length,
1466
+ },
1467
+ sessions: {
1468
+ count: snapshot.sessions.length,
1469
+ runningCount: snapshot.sessions.filter((session) => session.status === "running").length,
1470
+ },
1471
+ latestSession: latestSession ? {
1472
+ sessionId: latestSession.id,
1473
+ status: latestSession.status,
1474
+ latestOutputPath: latestOutput?.latestPath,
1475
+ resultCommand: `happy-elves result ${latestSession.id}`,
1476
+ runCommand: `happy-elves run ${latestSession.id} --text "..." --wait`,
1477
+ } : undefined,
1478
+ };
1479
+ if (wantsJson(flags)) {
1480
+ ok("status", data);
1481
+ return;
1482
+ }
1483
+ console.log(`Relay: ${config.relayUrl}`);
1484
+ console.log(`Daemon: ${local.running ? `running${local.pid ? ` pid ${local.pid}` : ""}` : "not running"}`);
1485
+ console.log(`Machines: ${data.machines.onlineCount}/${data.machines.count} online`);
1486
+ console.log(`Sessions: ${data.sessions.count} total, ${data.sessions.runningCount} running`);
1487
+ if (latestSession) {
1488
+ console.log(`Latest session: ${latestSession.id} ${latestSession.status}`);
1489
+ console.log(`Latest output: ${latestOutput ? displayPath(latestOutput.latestPath) : "none"}`);
1490
+ console.log(`Result: happy-elves result ${latestSession.id}`);
1491
+ console.log(`Run: happy-elves run ${latestSession.id} --text "..." --wait`);
1492
+ }
1493
+ }
1129
1494
  async function main() {
1130
1495
  const argv = process.argv.slice(2);
1131
1496
  const [domain] = argv;
1132
1497
  if (!domain)
1133
1498
  usage();
1499
+ if (domain === "--help" || domain === "-h" || domain === "help") {
1500
+ console.log(usageText());
1501
+ return;
1502
+ }
1134
1503
  if (domain === "start") {
1135
1504
  const { flags } = parseFlags(argv.slice(1));
1136
1505
  await startBootstrap(flags);
@@ -1141,6 +1510,11 @@ async function main() {
1141
1510
  await topLevelDoctor(flags);
1142
1511
  return;
1143
1512
  }
1513
+ if (domain === "status") {
1514
+ const { flags } = parseFlags(argv.slice(1));
1515
+ await topLevelStatus(flags);
1516
+ return;
1517
+ }
1144
1518
  const [, action, ...rest] = argv;
1145
1519
  const { positional, flags } = parseFlags(rest);
1146
1520
  if (domain === "account" && action === "create") {
@@ -1639,12 +2013,33 @@ async function main() {
1639
2013
  }, { machineId: machine.id });
1640
2014
  return;
1641
2015
  }
2016
+ if (domain === "workspace" && action === "sessions") {
2017
+ const cwd = requireString(flags, "cwd");
2018
+ const limit = typeof flags.limit === "string" ? Number(flags.limit) : 50;
2019
+ if (!Number.isInteger(limit) || limit <= 0 || limit > 200) {
2020
+ throw new CliError(`Invalid --limit: ${flags.limit}`, "INVALID_ARGUMENT");
2021
+ }
2022
+ const result = await listWorkspaceHistoricalSessions(client, {
2023
+ cwd,
2024
+ machineId: typeof flags.machine === "string" ? flags.machine : undefined,
2025
+ agent: typeof flags.agent === "string" ? flags.agent : undefined,
2026
+ limit,
2027
+ });
2028
+ if (wantsJson(flags)) {
2029
+ ok("workspace.sessions", { cwd, agent: typeof flags.agent === "string" ? flags.agent : undefined, ...result });
2030
+ }
2031
+ else {
2032
+ printWorkspaceHistoricalSessions(result.sessions, result.errors);
2033
+ }
2034
+ return;
2035
+ }
1642
2036
  if (domain === "session" && action === "create") {
1643
2037
  const machineId = requireString(flags, "machine");
1644
2038
  const agent = typeof flags.agent === "string" ? flags.agent : "codex";
1645
2039
  const cwd = typeof flags.cwd === "string" ? flags.cwd : process.cwd();
1646
2040
  const name = typeof flags.name === "string" ? flags.name : "main";
1647
- const result = await client.createSession({ machineId, agent, cwd, name });
2041
+ const permissionMode = parsePermissionMode(flags["permission-mode"] ?? flags.permission);
2042
+ const result = await client.createSession({ machineId, agent, cwd, name, permissionMode });
1648
2043
  const session = result.sessionId ? await client.getSession(result.sessionId) : undefined;
1649
2044
  ok("session.create", {
1650
2045
  sessionId: result.sessionId,
@@ -1659,7 +2054,27 @@ async function main() {
1659
2054
  cwd: typeof flags.cwd === "string" ? flags.cwd : undefined,
1660
2055
  name: typeof flags.name === "string" ? flags.name : undefined,
1661
2056
  });
1662
- ok("session.list", { sessions: await Promise.all(sessions.map((session) => showSession(client, session))) });
2057
+ if (wantsJson(flags) || wantsVerbose(flags)) {
2058
+ const projected = await Promise.all(sessions.map(async (session) => ({
2059
+ ...(await showSession(client, session)),
2060
+ latestOutput: await readStoredTurnOutput(session.id).catch(() => undefined),
2061
+ })));
2062
+ ok("session.list", { sessions: projected });
2063
+ }
2064
+ else {
2065
+ const summaries = await Promise.all(sessions.map((session) => conciseSessionSummary(client, session)));
2066
+ if (summaries.length === 0) {
2067
+ console.log("No sessions.");
2068
+ }
2069
+ else {
2070
+ console.log("SESSION | AGENT | STATUS | NAME | CWD | LATEST");
2071
+ for (const session of summaries) {
2072
+ const preview = session.lastOutput ? ` | ${compactText(session.lastOutput)}` : "";
2073
+ const latest = session.latestOutputPath ? displayPath(session.latestOutputPath) : "-";
2074
+ console.log(`${session.sessionId} | ${session.agent} | ${session.status} | ${session.name} | ${session.cwd} | ${latest}${preview}`);
2075
+ }
2076
+ }
2077
+ }
1663
2078
  return;
1664
2079
  }
1665
2080
  if (domain === "session" && action === "show") {
@@ -1676,6 +2091,7 @@ async function main() {
1676
2091
  const metadata = await client.decodeSessionMetadata(session);
1677
2092
  const recentEvents = await client.history(session.id, 20);
1678
2093
  const orchestration = deriveOrchestration(session, recentEvents);
2094
+ const latestOutput = await readStoredTurnOutput(session.id).catch(() => undefined);
1679
2095
  ok("session.status", {
1680
2096
  sessionId: session.id,
1681
2097
  machineId: session.machineId,
@@ -1687,6 +2103,11 @@ async function main() {
1687
2103
  sourceSessionId: session.sourceSessionId,
1688
2104
  sourceCheckpointId: session.sourceCheckpointId,
1689
2105
  orchestration,
2106
+ latestOutput,
2107
+ next: {
2108
+ result: `happy-elves result ${session.id}`,
2109
+ run: `happy-elves run ${session.id} --text "..." --wait`,
2110
+ },
1690
2111
  metadata,
1691
2112
  capabilities: session.capabilities,
1692
2113
  }, { machineId: session.machineId, sessionId: session.id });
@@ -1698,7 +2119,25 @@ async function main() {
1698
2119
  if (!Number.isInteger(limit) || limit <= 0) {
1699
2120
  throw new CliError(`Invalid --limit: ${flags.limit}`, "INVALID_ARGUMENT");
1700
2121
  }
1701
- ok("session.history", { sessionId, events: await client.history(sessionId, limit) }, { sessionId });
2122
+ const events = await client.history(sessionId, limit);
2123
+ if (wantsJson(flags) || wantsVerbose(flags)) {
2124
+ ok("session.history", { sessionId, events }, { sessionId });
2125
+ }
2126
+ else {
2127
+ printConciseEvents(events);
2128
+ }
2129
+ return;
2130
+ }
2131
+ if (domain === "session" && action === "last-output") {
2132
+ const sessionId = requirePositional(positional[0], "sessionId");
2133
+ const events = await client.history(sessionId, 200);
2134
+ const output = lastAssistantOutput(events);
2135
+ if (wantsJson(flags)) {
2136
+ ok("session.last-output", { sessionId, output }, { sessionId });
2137
+ }
2138
+ else {
2139
+ console.log(output || "No assistant output yet.");
2140
+ }
1702
2141
  return;
1703
2142
  }
1704
2143
  if (domain === "session" && action === "resume") {
@@ -1755,8 +2194,52 @@ async function main() {
1755
2194
  : "";
1756
2195
  if (!text.trim())
1757
2196
  throw new CliError("Missing --text or --text-file", "MISSING_ARGUMENT");
1758
- const result = await client.run(sessionId, text, { wait: flags["no-wait"] !== true });
1759
- ok("session.run", { sessionId, turnId: result.turnId, events: await client.decodeEvents(result.events) }, { requestId: result.requestId, sessionId });
2197
+ if (flags.wait === true && (flags.detach === true || flags["no-wait"] === true)) {
2198
+ throw new CliError("--wait cannot be combined with --detach or --no-wait", "INVALID_ARGUMENT");
2199
+ }
2200
+ if (flags["no-wait"] === true && !wantsJson(flags)) {
2201
+ console.error("Warning: --no-wait is deprecated; use --detach.");
2202
+ }
2203
+ const permissionMode = parsePermissionMode(flags["permission-mode"] ?? flags.permission);
2204
+ const wait = flags.wait === true || (wantsJson(flags) && flags.detach !== true && flags["no-wait"] !== true);
2205
+ const watchTimeoutMs = parseDurationMs(flags.timeout, 30 * 60_000);
2206
+ const ackTimeoutMs = 15_000;
2207
+ const runtimeTimeoutMs = typeof flags["runtime-timeout"] === "string" ? parseDurationMs(flags["runtime-timeout"], 30 * 60_000) : undefined;
2208
+ const result = await client.run(sessionId, text, { wait, permissionMode, waitTimeoutMs: watchTimeoutMs, ackTimeoutMs, runtimeTimeoutMs });
2209
+ const events = await client.decodeEvents(result.events);
2210
+ const output = wait && result.turnId ? await writeTurnOutput(sessionId, result.turnId, events) : undefined;
2211
+ if (wantsJson(flags)) {
2212
+ ok("session.run", { sessionId, turnId: result.turnId, events, output }, { requestId: result.requestId, sessionId });
2213
+ }
2214
+ else if (wait) {
2215
+ printConciseEvents(events);
2216
+ if (output) {
2217
+ const label = output.status === "completed" ? "Completed" : output.status[0]?.toUpperCase() + output.status.slice(1);
2218
+ console.log(`${label} ${output.turnId}`);
2219
+ console.log(`Output: ${displayPath(output.outputPath)}`);
2220
+ console.log(`Latest: ${displayPath(output.latestPath)}`);
2221
+ console.log(`View: happy-elves result ${sessionId}`);
2222
+ }
2223
+ }
2224
+ else {
2225
+ console.log(`Turn started: ${result.turnId}`);
2226
+ console.log(`Track: happy-elves run ${sessionId} --text-file <next-prompt> --wait`);
2227
+ console.log(`Wait: happy-elves wait ${sessionId} --until ready`);
2228
+ console.log(`Result: happy-elves result ${sessionId}`);
2229
+ }
2230
+ return;
2231
+ }
2232
+ if (domain === "result") {
2233
+ const sessionId = requirePositional(action, "sessionId");
2234
+ const turnId = typeof flags.turn === "string" ? flags.turn : undefined;
2235
+ const stored = await readStoredTurnOutput(sessionId, turnId).catch(() => undefined);
2236
+ const result = stored ?? await rebuildTurnOutput(client, sessionId, turnId);
2237
+ if (wantsJson(flags)) {
2238
+ ok("session.result", result, { sessionId });
2239
+ }
2240
+ else {
2241
+ console.log(result.output || "No assistant output yet.");
2242
+ }
1760
2243
  return;
1761
2244
  }
1762
2245
  if (domain === "wait") {
@@ -1782,11 +2265,14 @@ async function main() {
1782
2265
  }
1783
2266
  const events = await client.collect(sessionId, { since, limit });
1784
2267
  const lastEventId = events.at(-1)?.id;
1785
- ok("session.collect", {
1786
- sessionId,
1787
- cursor: lastEventId === undefined ? (since ?? null) : `cursor_${lastEventId}`,
1788
- events,
1789
- }, { sessionId });
2268
+ const nextSince = lastEventId === undefined ? (since ?? null) : `cursor_${lastEventId}`;
2269
+ if (wantsJson(flags) || wantsVerbose(flags)) {
2270
+ ok("session.collect", { sessionId, cursor: nextSince, nextSince, events }, { sessionId });
2271
+ }
2272
+ else {
2273
+ printConciseEvents(events);
2274
+ console.log(`nextSince: ${nextSince ?? "none"}`);
2275
+ }
1790
2276
  return;
1791
2277
  }
1792
2278
  if (domain === "cancel") {