@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.
- package/apps/cli/dist/index.js +506 -20
- package/apps/cli/dist/index.js.map +1 -1
- package/apps/daemon/dist/cli.js +67 -10
- package/apps/daemon/dist/cli.js.map +1 -1
- package/apps/daemon/dist/turn-coordinator.d.ts +26 -0
- package/apps/daemon/dist/turn-coordinator.js +77 -0
- package/apps/daemon/dist/turn-coordinator.js.map +1 -0
- package/apps/relay/dist/auth.d.ts +1 -0
- package/apps/relay/dist/auth.js +16 -0
- package/apps/relay/dist/auth.js.map +1 -0
- package/apps/relay/dist/connections.d.ts +11 -0
- package/apps/relay/dist/connections.js +11 -0
- package/apps/relay/dist/connections.js.map +1 -0
- package/apps/relay/dist/controller-handlers.d.ts +40 -0
- package/apps/relay/dist/controller-handlers.js +322 -0
- package/apps/relay/dist/controller-handlers.js.map +1 -0
- package/apps/relay/dist/db.d.ts +5 -0
- package/apps/relay/dist/db.js +8 -0
- package/apps/relay/dist/db.js.map +1 -1
- package/apps/relay/dist/http-schemas.d.ts +1 -1
- package/apps/relay/dist/http-schemas.js +4 -4
- package/apps/relay/dist/http-schemas.js.map +1 -1
- package/apps/relay/dist/index.js +94 -564
- package/apps/relay/dist/index.js.map +1 -1
- package/apps/relay/dist/machine-handlers.d.ts +30 -0
- package/apps/relay/dist/machine-handlers.js +175 -0
- package/apps/relay/dist/machine-handlers.js.map +1 -0
- package/apps/relay/dist/repositories.d.ts +13 -0
- package/apps/relay/dist/repositories.js +60 -0
- package/apps/relay/dist/repositories.js.map +1 -0
- package/apps/relay/dist/security.d.ts +0 -1
- package/apps/relay/dist/security.js +0 -3
- package/apps/relay/dist/security.js.map +1 -1
- package/package.json +1 -1
- package/packages/client/dist/index.d.ts +8 -1
- package/packages/client/dist/index.d.ts.map +1 -1
- package/packages/client/dist/index.js +56 -22
- package/packages/client/dist/index.js.map +1 -1
- package/packages/runtime/dist/index.d.ts +2 -0
- package/packages/runtime/dist/index.d.ts.map +1 -1
- package/packages/runtime-cli/dist/codex-app-server.d.ts +5 -1
- package/packages/runtime-cli/dist/codex-app-server.d.ts.map +1 -1
- package/packages/runtime-cli/dist/codex-app-server.js +109 -13
- package/packages/runtime-cli/dist/codex-app-server.js.map +1 -1
- package/packages/runtime-cli/dist/index.d.ts.map +1 -1
- package/packages/runtime-cli/dist/index.js +16 -2
- package/packages/runtime-cli/dist/index.js.map +1 -1
- package/packages/shared/dist/protocol.d.ts +3 -2
- package/packages/shared/dist/protocol.d.ts.map +1 -1
- package/packages/shared/dist/protocol.js.map +1 -1
package/apps/cli/dist/index.js
CHANGED
|
@@ -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
|
|
57
|
-
|
|
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> [--
|
|
95
|
-
run <sessionId> --text-file <path> [--
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1759
|
-
|
|
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
|
-
|
|
1786
|
-
|
|
1787
|
-
cursor:
|
|
1788
|
-
|
|
1789
|
-
|
|
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") {
|