@hydra-acp/cli 0.1.40 → 0.1.41
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/README.md +35 -14
- package/dist/cli.js +443 -42
- package/dist/index.d.ts +11 -0
- package/dist/index.js +216 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -187,7 +187,7 @@ hydra-acp session
|
|
|
187
187
|
|
|
188
188
|
# 6. Attach a second client to an existing session.
|
|
189
189
|
# Bare invocation auto-detects: TUI in a terminal, ACP shim when piped.
|
|
190
|
-
hydra-acp --session
|
|
190
|
+
hydra-acp --session hydra_session_abc123
|
|
191
191
|
```
|
|
192
192
|
|
|
193
193
|
## CLI
|
|
@@ -198,34 +198,55 @@ hydra-acp shim # explicit shim mode (forces shim re
|
|
|
198
198
|
hydra-acp tui # explicit terminal-UI mode
|
|
199
199
|
hydra-acp launch <agent> # launcher mode: shim that forces the
|
|
200
200
|
# daemon to spawn <agent> on session/new
|
|
201
|
-
hydra-acp
|
|
201
|
+
hydra-acp cat [-p <prompt>] [--detach] # pipe-friendly headless mode: feeds stdin
|
|
202
|
+
# to a session as prompts and streams the
|
|
203
|
+
# agent's reply to stdout
|
|
204
|
+
hydra-acp --session <id-or-url> # attach to existing session
|
|
202
205
|
# (TUI in a TTY, shim otherwise)
|
|
206
|
+
hydra-acp --reattach # pick the most-recent session for cwd
|
|
207
|
+
hydra-acp --new # force a fresh session
|
|
208
|
+
hydra-acp --readonly # open a session as a transcript viewer (with --session)
|
|
203
209
|
|
|
204
210
|
hydra-acp init # generate the service token
|
|
211
|
+
|
|
212
|
+
hydra-acp daemon [status] # output status of daemon
|
|
205
213
|
hydra-acp daemon start [--foreground] # detached by default; --foreground to attach
|
|
206
|
-
hydra-acp daemon stop
|
|
207
|
-
hydra-acp daemon
|
|
214
|
+
hydra-acp daemon stop # stop running daemon
|
|
215
|
+
hydra-acp daemon restart # stop then start the daemon
|
|
216
|
+
hydra-acp daemon logs [-f] [-n N] # tail (default 50) or follow the daemon log
|
|
208
217
|
|
|
209
|
-
hydra-acp session
|
|
210
|
-
hydra-acp session kill <id>
|
|
211
|
-
hydra-acp session remove <id>
|
|
218
|
+
hydra-acp session [list ] # list sessions
|
|
219
|
+
hydra-acp session kill <id> # close a live session (keeps the on-disk record so it can be resurrected)
|
|
220
|
+
hydra-acp session remove <id> # remove a session entirely (live or cold)
|
|
212
221
|
hydra-acp session export <id> [--out <file>|.]
|
|
213
222
|
# write a session bundle (meta + history) to <file>,
|
|
214
223
|
# to a default-named file when --out=., or to stdout
|
|
215
|
-
hydra-acp session
|
|
224
|
+
hydra-acp session transcript <id>|<file> [--out <file>|.]
|
|
225
|
+
# render a session (id via daemon, or a local .hydra
|
|
226
|
+
# bundle) as a markdown transcript
|
|
227
|
+
hydra-acp session import <file>|- [--replace] [--cwd <path>] [--info]
|
|
216
228
|
# import a bundle from <file> or stdin (-);
|
|
217
|
-
# --replace overwrites
|
|
229
|
+
# --replace overwrites a lineage match (kills it
|
|
230
|
+
# if live); --cwd overrides the bundle's recorded
|
|
231
|
+
# working directory; --info prints the bundle's
|
|
232
|
+
# meta without importing
|
|
218
233
|
|
|
219
|
-
hydra-acp extension
|
|
234
|
+
hydra-acp extension [list] # list configured extensions and live state
|
|
220
235
|
hydra-acp extension add <name> # add to config (--command, --args, --env, --disabled)
|
|
221
236
|
hydra-acp extension remove <name> # remove from config
|
|
222
237
|
hydra-acp extension start|stop|restart <n> # lifecycle on a running extension
|
|
223
238
|
hydra-acp extension logs <name> [-f] [-n] # tail (default 50) or follow an extension's log
|
|
224
239
|
|
|
225
|
-
hydra-acp agent
|
|
240
|
+
hydra-acp agent [list] # list agents in the registry
|
|
226
241
|
hydra-acp agent install <id> # pre-install an agent (else lazy on first use)
|
|
242
|
+
hydra-acp agent refresh # force a registry re-fetch
|
|
243
|
+
hydra-acp agent sync <id> # spawn <id> just long enough to ACP session/list it,
|
|
244
|
+
# then persist any sessions it remembers as cold rows
|
|
245
|
+
# (lets you bring in pre-existing agent sessions)
|
|
227
246
|
|
|
228
|
-
hydra-acp
|
|
247
|
+
hydra-acp auth # list active session tokens
|
|
248
|
+
hydra-acp auth password [--force] # set the daemon's master password
|
|
249
|
+
hydra-acp auth revoke <id> # revoke a session token
|
|
229
250
|
```
|
|
230
251
|
|
|
231
252
|
A bare invocation (`hydra-acp` with no subcommand) auto-dispatches based on whether stdout is a TTY: a real terminal launches the TUI, a piped stdio (the editor-spawned case) drops into shim mode. Pass `shim` or `tui` explicitly to force one or the other. Editors should configure `hydra-acp shim` so the choice is unambiguous regardless of how the editor wires stdio.
|
|
@@ -317,11 +338,11 @@ Every config-knob flag has an `HYDRA_ACP_FOO_BAR` env-var equivalent. Flag wins
|
|
|
317
338
|
| `--name` | `HYDRA_ACP_NAME` |
|
|
318
339
|
| `--agent` | `HYDRA_ACP_AGENT` |
|
|
319
340
|
| `--model` | `HYDRA_ACP_MODEL` |
|
|
320
|
-
| `--session
|
|
341
|
+
| `--session` | `HYDRA_ACP_SESSION` |
|
|
321
342
|
|
|
322
343
|
`--model` is a one-shot override for the per-agent `defaultModels` entry in `~/.hydra-acp/config.json`. It only applies at fresh session creation — resurrect and `/hydra agent` switch ignore it (resurrected sessions stay on whatever model they were last using).
|
|
323
344
|
|
|
324
|
-
Action commands (`init`, `daemon`, `
|
|
345
|
+
Action commands (`init`, `daemon`, `session`, `extension`, `agent`, `auth`, `cat`, `--help`, `--version`, `--rotate-token`) are not config knobs and are flag-only.
|
|
325
346
|
|
|
326
347
|
### Registry id resolution
|
|
327
348
|
|
package/dist/cli.js
CHANGED
|
@@ -91,6 +91,11 @@ var init_paths = __esm({
|
|
|
91
91
|
extensionLogFile: (name) => path.join(hydraHome(), "extensions", `${name}.log`),
|
|
92
92
|
extensionPidFile: (name) => path.join(hydraHome(), "extensions", `${name}.pid`),
|
|
93
93
|
tuiHistoryFile: (id) => path.join(hydraHome(), "sessions", id, "prompt-history"),
|
|
94
|
+
// Cross-session prompt history. Up-arrow / ^R fall through to this
|
|
95
|
+
// after the per-session list is exhausted. JSONL, one entry per
|
|
96
|
+
// line, append-only so concurrent TUIs don't lose each other's
|
|
97
|
+
// writes.
|
|
98
|
+
globalTuiHistoryFile: () => path.join(hydraHome(), "prompt-history"),
|
|
94
99
|
tuiLogFile: () => path.join(hydraHome(), "tui.log")
|
|
95
100
|
};
|
|
96
101
|
}
|
|
@@ -3832,7 +3837,7 @@ function parseHistory(text) {
|
|
|
3832
3837
|
}
|
|
3833
3838
|
return out;
|
|
3834
3839
|
}
|
|
3835
|
-
function appendEntry(history, entry) {
|
|
3840
|
+
function appendEntry(history, entry, cap = HISTORY_CAP) {
|
|
3836
3841
|
const trimmed = entry.replace(/\n+$/, "");
|
|
3837
3842
|
if (trimmed.length === 0) {
|
|
3838
3843
|
return history;
|
|
@@ -3841,8 +3846,8 @@ function appendEntry(history, entry) {
|
|
|
3841
3846
|
return history;
|
|
3842
3847
|
}
|
|
3843
3848
|
const out = history.concat(trimmed);
|
|
3844
|
-
if (out.length >
|
|
3845
|
-
return out.slice(out.length -
|
|
3849
|
+
if (out.length > cap) {
|
|
3850
|
+
return out.slice(out.length - cap);
|
|
3846
3851
|
}
|
|
3847
3852
|
return out;
|
|
3848
3853
|
}
|
|
@@ -3851,11 +3856,30 @@ async function saveHistory(file, history) {
|
|
|
3851
3856
|
const lines = history.map((entry) => JSON.stringify(entry));
|
|
3852
3857
|
await fs10.writeFile(file, lines.length > 0 ? lines.join("\n") + "\n" : "");
|
|
3853
3858
|
}
|
|
3854
|
-
|
|
3859
|
+
async function appendHistoryLine(file, entry) {
|
|
3860
|
+
const trimmed = entry.replace(/\n+$/, "");
|
|
3861
|
+
if (trimmed.length === 0) {
|
|
3862
|
+
return;
|
|
3863
|
+
}
|
|
3864
|
+
await fs10.mkdir(path6.dirname(file), { recursive: true });
|
|
3865
|
+
await fs10.appendFile(file, JSON.stringify(trimmed) + "\n", {
|
|
3866
|
+
encoding: "utf8"
|
|
3867
|
+
});
|
|
3868
|
+
}
|
|
3869
|
+
function buildCombinedHistory(global, session) {
|
|
3870
|
+
if (session.length === 0) {
|
|
3871
|
+
return [...global];
|
|
3872
|
+
}
|
|
3873
|
+
const sessionSet = new Set(session);
|
|
3874
|
+
const filteredGlobal = global.filter((e) => !sessionSet.has(e));
|
|
3875
|
+
return [...filteredGlobal, ...session];
|
|
3876
|
+
}
|
|
3877
|
+
var HISTORY_CAP, GLOBAL_HISTORY_CAP;
|
|
3855
3878
|
var init_history = __esm({
|
|
3856
3879
|
"src/tui/history.ts"() {
|
|
3857
3880
|
"use strict";
|
|
3858
3881
|
HISTORY_CAP = 500;
|
|
3882
|
+
GLOBAL_HISTORY_CAP = 2e3;
|
|
3859
3883
|
}
|
|
3860
3884
|
});
|
|
3861
3885
|
|
|
@@ -7353,24 +7377,22 @@ uncaught: ${err.stack ?? err.message}
|
|
|
7353
7377
|
this.writeProgressIndicator(this.banner.status === "busy" ? 3 : 0);
|
|
7354
7378
|
this.syncedPartialRepaint(() => this.drawBanner());
|
|
7355
7379
|
}
|
|
7356
|
-
// Wrap a partial repaint (banner-only,
|
|
7380
|
+
// Wrap a partial repaint (banner-only, prompt-only, etc.) in a
|
|
7357
7381
|
// synchronized-output bracket so the row swap is atomic on terminals
|
|
7358
|
-
// that support DEC 2026
|
|
7359
|
-
//
|
|
7360
|
-
//
|
|
7361
|
-
//
|
|
7362
|
-
//
|
|
7382
|
+
// that support DEC 2026. Cursor movement (moveTo) is buffered inside
|
|
7383
|
+
// BSU/ESU, so the cursor appears at its final placeCursor position
|
|
7384
|
+
// without visibly visiting intermediate rows. We intentionally do NOT
|
|
7385
|
+
// hide the cursor here: ?25l/h (cursor visibility) is terminal *state*
|
|
7386
|
+
// applied immediately rather than buffered, so hiding inside a BSU/ESU
|
|
7387
|
+
// block causes a visible blink (cursor disappears → frame commits →
|
|
7388
|
+
// cursor reappears) on every banner tick — worse than any skitter.
|
|
7363
7389
|
syncedPartialRepaint(paint) {
|
|
7364
7390
|
if (!this.started) {
|
|
7365
7391
|
return;
|
|
7366
7392
|
}
|
|
7367
7393
|
withSync(() => {
|
|
7368
|
-
this.term.hideCursor();
|
|
7369
7394
|
paint();
|
|
7370
7395
|
this.placeCursor();
|
|
7371
|
-
if (this.permissionPrompt || this.confirmPrompt || this.helpPrompt) {
|
|
7372
|
-
this.term.hideCursor(false);
|
|
7373
|
-
}
|
|
7374
7396
|
});
|
|
7375
7397
|
}
|
|
7376
7398
|
currentModeId() {
|
|
@@ -8462,7 +8484,7 @@ uncaught: ${err.stack ?? err.message}
|
|
|
8462
8484
|
const body = ` ${marker} ${i + 1}. ${truncate(opt.label, w - 8)}`;
|
|
8463
8485
|
writeRow(`perm|o|${w}|${i}|${isSel ? "1" : "0"}|${opt.label}`, () => {
|
|
8464
8486
|
if (isSel) {
|
|
8465
|
-
this.term.
|
|
8487
|
+
this.term.brightYellow(body);
|
|
8466
8488
|
} else {
|
|
8467
8489
|
this.term.dim(body);
|
|
8468
8490
|
}
|
|
@@ -11019,11 +11041,13 @@ function toolIconStyle(status) {
|
|
|
11019
11041
|
}
|
|
11020
11042
|
function formatPlan(event) {
|
|
11021
11043
|
const stopped = event.stopped === true;
|
|
11044
|
+
const amended = event.amended === true;
|
|
11045
|
+
const stoppedStyle = amended ? "tool-status-cancelled" : "tool-status-fail";
|
|
11022
11046
|
if (event.entries.length === 0) {
|
|
11023
11047
|
return [
|
|
11024
11048
|
{
|
|
11025
11049
|
prefix: "\u25A3 ",
|
|
11026
|
-
prefixStyle: stopped ?
|
|
11050
|
+
prefixStyle: stopped ? stoppedStyle : "plan",
|
|
11027
11051
|
body: "(empty plan)",
|
|
11028
11052
|
bodyStyle: "dim"
|
|
11029
11053
|
}
|
|
@@ -11032,7 +11056,7 @@ function formatPlan(event) {
|
|
|
11032
11056
|
const allComplete = event.entries.every(
|
|
11033
11057
|
(e) => (e.status ?? "pending") === "completed"
|
|
11034
11058
|
);
|
|
11035
|
-
const headerStyle = allComplete ? "plan-done" : stopped ?
|
|
11059
|
+
const headerStyle = allComplete ? "plan-done" : stopped ? stoppedStyle : "plan";
|
|
11036
11060
|
const lines = [
|
|
11037
11061
|
{
|
|
11038
11062
|
prefix: "\u25A3 ",
|
|
@@ -11656,9 +11680,35 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
|
|
|
11656
11680
|
initialQueue = hydraMeta.queue;
|
|
11657
11681
|
}
|
|
11658
11682
|
const historyFile = paths.tuiHistoryFile(resolvedSessionId);
|
|
11683
|
+
const globalHistoryFile = paths.globalTuiHistoryFile();
|
|
11659
11684
|
let history = await loadHistory(historyFile).catch(() => []);
|
|
11660
|
-
|
|
11685
|
+
let globalHistory = await loadHistory(globalHistoryFile).catch(() => []);
|
|
11686
|
+
if (globalHistory.length > GLOBAL_HISTORY_CAP) {
|
|
11687
|
+
globalHistory = globalHistory.slice(globalHistory.length - GLOBAL_HISTORY_CAP);
|
|
11688
|
+
}
|
|
11689
|
+
const dispatcher = new InputDispatcher({
|
|
11690
|
+
history: buildCombinedHistory(globalHistory, history)
|
|
11691
|
+
});
|
|
11661
11692
|
dispatcherRef = dispatcher;
|
|
11693
|
+
const recordHistoryEntry = (entry) => {
|
|
11694
|
+
const trimmed = entry.replace(/\n+$/, "");
|
|
11695
|
+
if (trimmed.length === 0) {
|
|
11696
|
+
return;
|
|
11697
|
+
}
|
|
11698
|
+
const nextSession = appendEntry(history, trimmed);
|
|
11699
|
+
const sessionChanged = nextSession !== history;
|
|
11700
|
+
history = nextSession;
|
|
11701
|
+
const nextGlobal = appendEntry(globalHistory, trimmed, GLOBAL_HISTORY_CAP);
|
|
11702
|
+
const globalChanged = nextGlobal !== globalHistory;
|
|
11703
|
+
globalHistory = nextGlobal;
|
|
11704
|
+
dispatcher.setHistory(buildCombinedHistory(globalHistory, history));
|
|
11705
|
+
if (sessionChanged) {
|
|
11706
|
+
saveHistory(historyFile, history).catch(() => void 0);
|
|
11707
|
+
}
|
|
11708
|
+
if (globalChanged) {
|
|
11709
|
+
appendHistoryLine(globalHistoryFile, trimmed).catch(() => void 0);
|
|
11710
|
+
}
|
|
11711
|
+
};
|
|
11662
11712
|
if (pendingTurns > 0) {
|
|
11663
11713
|
dispatcher.setTurnRunning(true);
|
|
11664
11714
|
}
|
|
@@ -12035,8 +12085,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
|
|
|
12035
12085
|
}
|
|
12036
12086
|
const pendingDraft = dispatcher.state().buffer.join("\n");
|
|
12037
12087
|
if (pendingDraft.replace(/\s+$/, "").length > 0) {
|
|
12038
|
-
|
|
12039
|
-
dispatcher.setHistory(history);
|
|
12088
|
+
recordHistoryEntry(pendingDraft);
|
|
12040
12089
|
}
|
|
12041
12090
|
screen.pauseRepaint();
|
|
12042
12091
|
screen.stop({ keepFullscreen: true });
|
|
@@ -12415,9 +12464,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
|
|
|
12415
12464
|
if (handleBuiltinCommand(text)) {
|
|
12416
12465
|
return;
|
|
12417
12466
|
}
|
|
12418
|
-
|
|
12419
|
-
dispatcher.setHistory(history);
|
|
12420
|
-
saveHistory(historyFile, history).catch(() => void 0);
|
|
12467
|
+
recordHistoryEntry(text);
|
|
12421
12468
|
void runPrompt(text, attachments);
|
|
12422
12469
|
};
|
|
12423
12470
|
const amendPrompt = (text, attachments) => {
|
|
@@ -12425,9 +12472,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
|
|
|
12425
12472
|
if (handleBuiltinCommand(text)) {
|
|
12426
12473
|
return;
|
|
12427
12474
|
}
|
|
12428
|
-
|
|
12429
|
-
dispatcher.setHistory(history);
|
|
12430
|
-
saveHistory(historyFile, history).catch(() => void 0);
|
|
12475
|
+
recordHistoryEntry(text);
|
|
12431
12476
|
if (!daemonSupportsAmend || currentHeadMessageId === void 0) {
|
|
12432
12477
|
void runPrompt(text, attachments);
|
|
12433
12478
|
return;
|
|
@@ -12787,16 +12832,18 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
|
|
|
12787
12832
|
const end = toolsBlockEndedAt ?? Date.now();
|
|
12788
12833
|
const elapsed = end - toolsBlockStartedAt;
|
|
12789
12834
|
const stoppedReason = !inProgress && toolsBlockStopReason !== null && toolsBlockStopReason !== "end_turn" ? toolsBlockStopReason : null;
|
|
12835
|
+
const isAmended = stoppedReason === "amended";
|
|
12836
|
+
const stoppedLabel = isAmended ? `amended \xB7 ${formatElapsed(elapsed)}` : `stopped (${stoppedReason}) \xB7 ${formatElapsed(elapsed)}`;
|
|
12790
12837
|
let summary;
|
|
12791
12838
|
if (total === 0) {
|
|
12792
12839
|
if (stoppedReason !== null) {
|
|
12793
|
-
summary =
|
|
12840
|
+
summary = stoppedLabel;
|
|
12794
12841
|
} else {
|
|
12795
12842
|
summary = inProgress ? `thinking \xB7 ${formatElapsed(elapsed)}` : `thought \xB7 ${formatElapsed(elapsed)}`;
|
|
12796
12843
|
}
|
|
12797
12844
|
} else {
|
|
12798
12845
|
const noun = total === 1 ? "tool" : "tools";
|
|
12799
|
-
const timing = stoppedReason !== null ?
|
|
12846
|
+
const timing = stoppedReason !== null ? stoppedLabel : inProgress ? formatElapsed(elapsed) : `took ${formatElapsed(elapsed)}`;
|
|
12800
12847
|
const parts = [`${total} ${noun}`, timing];
|
|
12801
12848
|
if (inProgress) {
|
|
12802
12849
|
if (hidden > 0) {
|
|
@@ -12808,8 +12855,9 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
|
|
|
12808
12855
|
summary = parts.join(" \xB7 ");
|
|
12809
12856
|
}
|
|
12810
12857
|
const pureThinking = total === 0 && inProgress;
|
|
12811
|
-
const
|
|
12812
|
-
const
|
|
12858
|
+
const stoppedHeaderStyle = isAmended ? "tool-status-cancelled" : "tool-status-fail";
|
|
12859
|
+
const frozenStyle = stoppedReason !== null ? stoppedHeaderStyle : "tool";
|
|
12860
|
+
const frozenBodyStyle = stoppedReason !== null ? stoppedHeaderStyle : "dim";
|
|
12813
12861
|
const lines = [
|
|
12814
12862
|
{
|
|
12815
12863
|
prefix: "\u2699 ",
|
|
@@ -12985,7 +13033,11 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
|
|
|
12985
13033
|
effectiveStopReason = "error";
|
|
12986
13034
|
}
|
|
12987
13035
|
if (lastPlanEvent !== null && effectiveStopReason !== void 0 && effectiveStopReason !== "end_turn") {
|
|
12988
|
-
const lines = formatEvent({
|
|
13036
|
+
const lines = formatEvent({
|
|
13037
|
+
...lastPlanEvent,
|
|
13038
|
+
stopped: true,
|
|
13039
|
+
amended: event.amended === true
|
|
13040
|
+
});
|
|
12989
13041
|
if (lines.length > 0) {
|
|
12990
13042
|
screen.upsertLines("plan", lines);
|
|
12991
13043
|
}
|
|
@@ -12997,7 +13049,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
|
|
|
12997
13049
|
toolsBlockStopReason = effectiveStopReason ?? null;
|
|
12998
13050
|
renderToolsBlock();
|
|
12999
13051
|
screen.clearKey("tools");
|
|
13000
|
-
} else if (effectiveStopReason !== void 0 && effectiveStopReason !== "end_turn") {
|
|
13052
|
+
} else if (effectiveStopReason !== void 0 && effectiveStopReason !== "end_turn" && effectiveStopReason !== "amended") {
|
|
13001
13053
|
screen.appendLines([
|
|
13002
13054
|
{
|
|
13003
13055
|
prefix: "\u26A0 ",
|
|
@@ -14693,6 +14745,12 @@ var SessionRecord = z4.object({
|
|
|
14693
14745
|
agentCommands: z4.array(PersistedAgentCommand).optional(),
|
|
14694
14746
|
agentModes: z4.array(PersistedAgentMode).optional(),
|
|
14695
14747
|
agentModels: z4.array(PersistedAgentModel).optional(),
|
|
14748
|
+
// One-shot flag set when `hydra agent sync` mints a row from an
|
|
14749
|
+
// agent-side session/list entry: signals that the first resurrect
|
|
14750
|
+
// should *keep* the agent's session/load replay (instead of draining
|
|
14751
|
+
// it) so the local history.jsonl gets populated from the agent's
|
|
14752
|
+
// memory. Cleared after that first resurrect completes.
|
|
14753
|
+
pendingHistorySync: z4.boolean().optional(),
|
|
14696
14754
|
createdAt: z4.string(),
|
|
14697
14755
|
updatedAt: z4.string()
|
|
14698
14756
|
});
|
|
@@ -14813,6 +14871,7 @@ function recordFromMemorySession(args) {
|
|
|
14813
14871
|
agentCommands: args.agentCommands,
|
|
14814
14872
|
agentModes: args.agentModes,
|
|
14815
14873
|
agentModels: args.agentModels,
|
|
14874
|
+
pendingHistorySync: args.pendingHistorySync,
|
|
14816
14875
|
createdAt: args.createdAt ?? now,
|
|
14817
14876
|
updatedAt: args.updatedAt ?? now
|
|
14818
14877
|
};
|
|
@@ -15118,7 +15177,13 @@ var SessionManager = class {
|
|
|
15118
15177
|
await agent.kill().catch(() => void 0);
|
|
15119
15178
|
return this.doResurrectFromImport(params);
|
|
15120
15179
|
}
|
|
15121
|
-
|
|
15180
|
+
if (params.pendingHistorySync === true) {
|
|
15181
|
+
void this.clearPendingHistorySync(params.hydraSessionId).catch(
|
|
15182
|
+
() => void 0
|
|
15183
|
+
);
|
|
15184
|
+
} else {
|
|
15185
|
+
agent.connection.drainBuffered("session/update");
|
|
15186
|
+
}
|
|
15122
15187
|
const session = new Session({
|
|
15123
15188
|
sessionId: params.hydraSessionId,
|
|
15124
15189
|
cwd: params.cwd,
|
|
@@ -15208,6 +15273,133 @@ var SessionManager = class {
|
|
|
15208
15273
|
}
|
|
15209
15274
|
return os3.homedir();
|
|
15210
15275
|
}
|
|
15276
|
+
// Pull every session the agent itself remembers (across all cwds) and
|
|
15277
|
+
// persist a cold hydra record for each one we don't already track.
|
|
15278
|
+
// Used by `hydra agent sync <id>` to surface sessions created outside
|
|
15279
|
+
// hydra — or by other tools — in `hydra session list` so the picker
|
|
15280
|
+
// can resurrect them. Spawns a throwaway agent process for the
|
|
15281
|
+
// initialize + session/list pair, then kills it. Records are minted
|
|
15282
|
+
// with pendingHistorySync:true so the first resurrect records the
|
|
15283
|
+
// agent's session/load replay into history.jsonl rather than dropping
|
|
15284
|
+
// it.
|
|
15285
|
+
async syncFromAgent(agentId) {
|
|
15286
|
+
const agentDef = await this.registry.getAgent(agentId);
|
|
15287
|
+
if (!agentDef) {
|
|
15288
|
+
const err = new Error(
|
|
15289
|
+
`agent ${agentId} not found in registry`
|
|
15290
|
+
);
|
|
15291
|
+
err.code = JsonRpcErrorCodes.AgentNotInstalled;
|
|
15292
|
+
throw err;
|
|
15293
|
+
}
|
|
15294
|
+
const plan = await planSpawn(agentDef, [], {
|
|
15295
|
+
npmRegistry: this.npmRegistry
|
|
15296
|
+
});
|
|
15297
|
+
const agent = this.spawner({
|
|
15298
|
+
agentId,
|
|
15299
|
+
cwd: os3.homedir(),
|
|
15300
|
+
plan
|
|
15301
|
+
});
|
|
15302
|
+
let initResult;
|
|
15303
|
+
try {
|
|
15304
|
+
initResult = await agent.connection.request(
|
|
15305
|
+
"initialize",
|
|
15306
|
+
{
|
|
15307
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
15308
|
+
clientCapabilities: {},
|
|
15309
|
+
clientInfo: { name: "hydra", version: HYDRA_VERSION }
|
|
15310
|
+
}
|
|
15311
|
+
);
|
|
15312
|
+
} catch (err) {
|
|
15313
|
+
await agent.kill().catch(() => void 0);
|
|
15314
|
+
throw err;
|
|
15315
|
+
}
|
|
15316
|
+
const caps = initResult.agentCapabilities ?? {};
|
|
15317
|
+
if (caps.sessionCapabilities?.list === void 0) {
|
|
15318
|
+
await agent.kill().catch(() => void 0);
|
|
15319
|
+
throw new Error(
|
|
15320
|
+
`agent ${agentId} does not advertise sessionCapabilities.list; cannot sync`
|
|
15321
|
+
);
|
|
15322
|
+
}
|
|
15323
|
+
let entries;
|
|
15324
|
+
try {
|
|
15325
|
+
entries = await this.collectAgentSessions(agent);
|
|
15326
|
+
} catch (err) {
|
|
15327
|
+
await agent.kill().catch(() => void 0);
|
|
15328
|
+
throw err;
|
|
15329
|
+
}
|
|
15330
|
+
await agent.kill().catch(() => void 0);
|
|
15331
|
+
const existing = /* @__PURE__ */ new Set();
|
|
15332
|
+
for (const live of this.sessions.values()) {
|
|
15333
|
+
existing.add(`${live.agentId}::${live.upstreamSessionId}`);
|
|
15334
|
+
}
|
|
15335
|
+
const stored = await this.store.list().catch(() => []);
|
|
15336
|
+
for (const rec of stored) {
|
|
15337
|
+
existing.add(`${rec.agentId}::${rec.upstreamSessionId}`);
|
|
15338
|
+
}
|
|
15339
|
+
const synced = [];
|
|
15340
|
+
let skipped = 0;
|
|
15341
|
+
for (const entry of entries) {
|
|
15342
|
+
const dedupeKey = `${agentId}::${entry.sessionId}`;
|
|
15343
|
+
if (existing.has(dedupeKey)) {
|
|
15344
|
+
skipped += 1;
|
|
15345
|
+
continue;
|
|
15346
|
+
}
|
|
15347
|
+
existing.add(dedupeKey);
|
|
15348
|
+
const newId = `${HYDRA_SESSION_PREFIX}${generateRawSessionId()}`;
|
|
15349
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
15350
|
+
const ts = entry.updatedAt ?? now;
|
|
15351
|
+
const recordArgs = {
|
|
15352
|
+
sessionId: newId,
|
|
15353
|
+
lineageId: generateLineageId(),
|
|
15354
|
+
upstreamSessionId: entry.sessionId,
|
|
15355
|
+
agentId,
|
|
15356
|
+
cwd: entry.cwd,
|
|
15357
|
+
pendingHistorySync: true,
|
|
15358
|
+
createdAt: ts,
|
|
15359
|
+
updatedAt: ts
|
|
15360
|
+
};
|
|
15361
|
+
if (entry.title !== void 0) {
|
|
15362
|
+
recordArgs.title = entry.title;
|
|
15363
|
+
}
|
|
15364
|
+
const record = recordFromMemorySession(recordArgs);
|
|
15365
|
+
await this.store.write(record);
|
|
15366
|
+
synced.push({ version: 1, ...record });
|
|
15367
|
+
}
|
|
15368
|
+
return { synced, skipped };
|
|
15369
|
+
}
|
|
15370
|
+
// Paginate the agent's session/list, threading nextCursor until the
|
|
15371
|
+
// agent stops returning one. Each entry the spec guarantees has
|
|
15372
|
+
// { sessionId, cwd }; title and updatedAt are optional.
|
|
15373
|
+
async collectAgentSessions(agent) {
|
|
15374
|
+
const out = [];
|
|
15375
|
+
let cursor;
|
|
15376
|
+
for (let page = 0; page < 100; page += 1) {
|
|
15377
|
+
const params = {};
|
|
15378
|
+
if (cursor !== void 0) {
|
|
15379
|
+
params.cursor = cursor;
|
|
15380
|
+
}
|
|
15381
|
+
const result = await agent.connection.request("session/list", params);
|
|
15382
|
+
const rows = Array.isArray(result.sessions) ? result.sessions : [];
|
|
15383
|
+
for (const row of rows) {
|
|
15384
|
+
if (typeof row.sessionId !== "string" || typeof row.cwd !== "string") {
|
|
15385
|
+
continue;
|
|
15386
|
+
}
|
|
15387
|
+
const entry = { sessionId: row.sessionId, cwd: row.cwd };
|
|
15388
|
+
if (typeof row.title === "string") {
|
|
15389
|
+
entry.title = row.title;
|
|
15390
|
+
}
|
|
15391
|
+
if (typeof row.updatedAt === "string") {
|
|
15392
|
+
entry.updatedAt = row.updatedAt;
|
|
15393
|
+
}
|
|
15394
|
+
out.push(entry);
|
|
15395
|
+
}
|
|
15396
|
+
if (typeof result.nextCursor !== "string" || result.nextCursor.length === 0) {
|
|
15397
|
+
break;
|
|
15398
|
+
}
|
|
15399
|
+
cursor = result.nextCursor;
|
|
15400
|
+
}
|
|
15401
|
+
return out;
|
|
15402
|
+
}
|
|
15211
15403
|
// Bootstrap a fresh agent process: registry resolve → spawn → initialize
|
|
15212
15404
|
// → session/new. Shared by create() and the /hydra agent path so both
|
|
15213
15405
|
// go through the same env / capabilities / error-handling.
|
|
@@ -15400,9 +15592,21 @@ var SessionManager = class {
|
|
|
15400
15592
|
agentCommands: record.agentCommands,
|
|
15401
15593
|
agentModes: record.agentModes,
|
|
15402
15594
|
agentModels: record.agentModels,
|
|
15403
|
-
createdAt: record.createdAt
|
|
15595
|
+
createdAt: record.createdAt,
|
|
15596
|
+
pendingHistorySync: record.pendingHistorySync
|
|
15404
15597
|
};
|
|
15405
15598
|
}
|
|
15599
|
+
async clearPendingHistorySync(sessionId) {
|
|
15600
|
+
await this.enqueueMetaWrite(sessionId, async () => {
|
|
15601
|
+
const record = await this.store.read(sessionId);
|
|
15602
|
+
if (!record || record.pendingHistorySync !== true) {
|
|
15603
|
+
return;
|
|
15604
|
+
}
|
|
15605
|
+
const next = { ...record };
|
|
15606
|
+
delete next.pendingHistorySync;
|
|
15607
|
+
await this.store.write(next);
|
|
15608
|
+
});
|
|
15609
|
+
}
|
|
15406
15610
|
// Best-effort: peek at the persisted history's first prompt and use
|
|
15407
15611
|
// its first line (capped to 200 chars) as a session title. Returns
|
|
15408
15612
|
// undefined if no usable prompt is found or any I/O fails.
|
|
@@ -17451,7 +17655,8 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
17451
17655
|
}
|
|
17452
17656
|
|
|
17453
17657
|
// src/daemon/routes/agents.ts
|
|
17454
|
-
|
|
17658
|
+
init_types();
|
|
17659
|
+
function registerAgentRoutes(app, registry, manager, opts = {}) {
|
|
17455
17660
|
app.get("/v1/agents", async () => {
|
|
17456
17661
|
const doc = await registry.load();
|
|
17457
17662
|
return {
|
|
@@ -17472,6 +17677,61 @@ function registerAgentRoutes(app, registry) {
|
|
|
17472
17677
|
const doc = await registry.refresh();
|
|
17473
17678
|
return { version: doc.version, agentCount: doc.agents.length };
|
|
17474
17679
|
});
|
|
17680
|
+
app.post("/v1/agents/:id/install", async (request, reply) => {
|
|
17681
|
+
const id = request.params.id;
|
|
17682
|
+
const agent = await registry.getAgent(id);
|
|
17683
|
+
if (!agent) {
|
|
17684
|
+
reply.code(404).send({ error: `agent ${id} not found in registry` });
|
|
17685
|
+
return;
|
|
17686
|
+
}
|
|
17687
|
+
if (agent.distribution.uvx && !agent.distribution.npx && !agent.distribution.binary) {
|
|
17688
|
+
reply.send({
|
|
17689
|
+
agentId: agent.id,
|
|
17690
|
+
version: agent.version ?? "current",
|
|
17691
|
+
distribution: "uvx",
|
|
17692
|
+
installed: false,
|
|
17693
|
+
message: "uvx agents resolve on first run; nothing to pre-install."
|
|
17694
|
+
});
|
|
17695
|
+
return;
|
|
17696
|
+
}
|
|
17697
|
+
try {
|
|
17698
|
+
const plan = await planSpawn(agent, [], { npmRegistry: opts.npmRegistry });
|
|
17699
|
+
const distribution = agent.distribution.npx ? "npx" : agent.distribution.binary ? "binary" : "unknown";
|
|
17700
|
+
reply.send({
|
|
17701
|
+
agentId: agent.id,
|
|
17702
|
+
version: plan.version,
|
|
17703
|
+
distribution,
|
|
17704
|
+
installed: true,
|
|
17705
|
+
command: plan.command
|
|
17706
|
+
});
|
|
17707
|
+
} catch (err) {
|
|
17708
|
+
reply.code(500).send({ error: err.message });
|
|
17709
|
+
}
|
|
17710
|
+
});
|
|
17711
|
+
app.post("/v1/agents/:id/sync", async (request, reply) => {
|
|
17712
|
+
const agentId = request.params.id;
|
|
17713
|
+
try {
|
|
17714
|
+
const { synced, skipped } = await manager.syncFromAgent(agentId);
|
|
17715
|
+
return {
|
|
17716
|
+
synced: synced.map((r) => ({
|
|
17717
|
+
sessionId: r.sessionId,
|
|
17718
|
+
upstreamSessionId: r.upstreamSessionId,
|
|
17719
|
+
agentId: r.agentId,
|
|
17720
|
+
cwd: r.cwd,
|
|
17721
|
+
title: r.title,
|
|
17722
|
+
updatedAt: r.updatedAt
|
|
17723
|
+
})),
|
|
17724
|
+
skipped
|
|
17725
|
+
};
|
|
17726
|
+
} catch (err) {
|
|
17727
|
+
const e = err;
|
|
17728
|
+
if (e.code === JsonRpcErrorCodes.AgentNotInstalled) {
|
|
17729
|
+
reply.code(404).send({ error: e.message });
|
|
17730
|
+
return;
|
|
17731
|
+
}
|
|
17732
|
+
reply.code(409).send({ error: e.message });
|
|
17733
|
+
}
|
|
17734
|
+
});
|
|
17475
17735
|
}
|
|
17476
17736
|
|
|
17477
17737
|
// src/daemon/routes/health.ts
|
|
@@ -18542,7 +18802,7 @@ async function startDaemon(config, serviceToken) {
|
|
|
18542
18802
|
agentId: config.defaultAgent,
|
|
18543
18803
|
cwd: config.defaultCwd
|
|
18544
18804
|
});
|
|
18545
|
-
registerAgentRoutes(app, registry);
|
|
18805
|
+
registerAgentRoutes(app, registry, manager, { npmRegistry: config.npmRegistry });
|
|
18546
18806
|
registerExtensionRoutes(app, extensions);
|
|
18547
18807
|
registerConfigRoutes(app, {
|
|
18548
18808
|
defaultAgent: config.defaultAgent,
|
|
@@ -19886,6 +20146,136 @@ async function runAgentsList() {
|
|
|
19886
20146
|
Registry version: ${body.version}
|
|
19887
20147
|
`);
|
|
19888
20148
|
}
|
|
20149
|
+
async function runAgentsInstall(agentId) {
|
|
20150
|
+
if (!agentId) {
|
|
20151
|
+
process.stderr.write("Usage: hydra-acp agent install <agent-id>\n");
|
|
20152
|
+
process.exit(2);
|
|
20153
|
+
return;
|
|
20154
|
+
}
|
|
20155
|
+
const config = await loadConfig();
|
|
20156
|
+
const serviceToken = await loadServiceToken();
|
|
20157
|
+
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
20158
|
+
process.stdout.write(`Installing ${agentId}\u2026
|
|
20159
|
+
`);
|
|
20160
|
+
let body;
|
|
20161
|
+
try {
|
|
20162
|
+
const r = await fetch(
|
|
20163
|
+
`${baseUrl}/v1/agents/${encodeURIComponent(agentId)}/install`,
|
|
20164
|
+
{
|
|
20165
|
+
method: "POST",
|
|
20166
|
+
headers: { Authorization: `Bearer ${serviceToken}` }
|
|
20167
|
+
}
|
|
20168
|
+
);
|
|
20169
|
+
if (!r.ok) {
|
|
20170
|
+
let detail = `HTTP ${r.status}`;
|
|
20171
|
+
try {
|
|
20172
|
+
const j = await r.json();
|
|
20173
|
+
if (j.error) {
|
|
20174
|
+
detail = j.error;
|
|
20175
|
+
}
|
|
20176
|
+
} catch {
|
|
20177
|
+
}
|
|
20178
|
+
process.stderr.write(`hydra agent install ${agentId}: ${detail}
|
|
20179
|
+
`);
|
|
20180
|
+
process.exit(1);
|
|
20181
|
+
}
|
|
20182
|
+
body = await r.json();
|
|
20183
|
+
} catch (err) {
|
|
20184
|
+
process.stderr.write(
|
|
20185
|
+
`Could not reach daemon at ${baseUrl}: ${err.message}
|
|
20186
|
+
`
|
|
20187
|
+
);
|
|
20188
|
+
process.exit(1);
|
|
20189
|
+
return;
|
|
20190
|
+
}
|
|
20191
|
+
if (!body.installed) {
|
|
20192
|
+
process.stdout.write(
|
|
20193
|
+
`${body.agentId} (${body.version}, ${body.distribution}): ${body.message ?? "nothing to install"}
|
|
20194
|
+
`
|
|
20195
|
+
);
|
|
20196
|
+
return;
|
|
20197
|
+
}
|
|
20198
|
+
process.stdout.write(
|
|
20199
|
+
`Installed ${body.agentId} (${body.version}, ${body.distribution})
|
|
20200
|
+
`
|
|
20201
|
+
);
|
|
20202
|
+
if (body.command) {
|
|
20203
|
+
process.stdout.write(` \u2192 ${body.command}
|
|
20204
|
+
`);
|
|
20205
|
+
}
|
|
20206
|
+
}
|
|
20207
|
+
async function runAgentsSync(agentId) {
|
|
20208
|
+
if (!agentId) {
|
|
20209
|
+
process.stderr.write("Usage: hydra-acp agent sync <agent-id>\n");
|
|
20210
|
+
process.exit(2);
|
|
20211
|
+
return;
|
|
20212
|
+
}
|
|
20213
|
+
const config = await loadConfig();
|
|
20214
|
+
const serviceToken = await loadServiceToken();
|
|
20215
|
+
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
20216
|
+
let body;
|
|
20217
|
+
try {
|
|
20218
|
+
const r = await fetch(`${baseUrl}/v1/agents/${encodeURIComponent(agentId)}/sync`, {
|
|
20219
|
+
method: "POST",
|
|
20220
|
+
headers: { Authorization: `Bearer ${serviceToken}` }
|
|
20221
|
+
});
|
|
20222
|
+
if (!r.ok) {
|
|
20223
|
+
let detail = `HTTP ${r.status}`;
|
|
20224
|
+
try {
|
|
20225
|
+
const j = await r.json();
|
|
20226
|
+
if (j.error) {
|
|
20227
|
+
detail = j.error;
|
|
20228
|
+
}
|
|
20229
|
+
} catch {
|
|
20230
|
+
}
|
|
20231
|
+
process.stderr.write(`hydra agent sync ${agentId}: ${detail}
|
|
20232
|
+
`);
|
|
20233
|
+
process.exit(1);
|
|
20234
|
+
}
|
|
20235
|
+
body = await r.json();
|
|
20236
|
+
} catch (err) {
|
|
20237
|
+
process.stderr.write(
|
|
20238
|
+
`Could not reach daemon at ${baseUrl}: ${err.message}
|
|
20239
|
+
`
|
|
20240
|
+
);
|
|
20241
|
+
process.exit(1);
|
|
20242
|
+
return;
|
|
20243
|
+
}
|
|
20244
|
+
if (body.synced.length === 0) {
|
|
20245
|
+
process.stdout.write(
|
|
20246
|
+
`Nothing new to sync (${body.skipped} already tracked).
|
|
20247
|
+
`
|
|
20248
|
+
);
|
|
20249
|
+
return;
|
|
20250
|
+
}
|
|
20251
|
+
const rows = body.synced.map((s) => ({
|
|
20252
|
+
id: s.sessionId,
|
|
20253
|
+
upstream: s.upstreamSessionId,
|
|
20254
|
+
cwd: s.cwd,
|
|
20255
|
+
title: s.title ?? "-"
|
|
20256
|
+
}));
|
|
20257
|
+
const header = { id: "ID", upstream: "UPSTREAM", cwd: "CWD", title: "TITLE" };
|
|
20258
|
+
const widths = {
|
|
20259
|
+
id: maxLen3(header.id, rows.map((r) => r.id)),
|
|
20260
|
+
upstream: maxLen3(header.upstream, rows.map((r) => r.upstream)),
|
|
20261
|
+
cwd: maxLen3(header.cwd, rows.map((r) => r.cwd))
|
|
20262
|
+
};
|
|
20263
|
+
const fmt = (r) => [
|
|
20264
|
+
r.id.padEnd(widths.id),
|
|
20265
|
+
r.upstream.padEnd(widths.upstream),
|
|
20266
|
+
r.cwd.padEnd(widths.cwd),
|
|
20267
|
+
r.title
|
|
20268
|
+
].join(" ");
|
|
20269
|
+
process.stdout.write(fmt(header) + "\n");
|
|
20270
|
+
for (const r of rows) {
|
|
20271
|
+
process.stdout.write(fmt(r) + "\n");
|
|
20272
|
+
}
|
|
20273
|
+
process.stdout.write(
|
|
20274
|
+
`
|
|
20275
|
+
Synced ${body.synced.length} session(s); skipped ${body.skipped} already tracked.
|
|
20276
|
+
`
|
|
20277
|
+
);
|
|
20278
|
+
}
|
|
19889
20279
|
async function runAgentsRefresh() {
|
|
19890
20280
|
const config = await loadConfig();
|
|
19891
20281
|
const serviceToken = await loadServiceToken();
|
|
@@ -20942,7 +21332,11 @@ async function main() {
|
|
|
20942
21332
|
const daemonIdx = argv.indexOf("daemon");
|
|
20943
21333
|
const tail = argv.slice(daemonIdx + 1);
|
|
20944
21334
|
const sub = tail[0];
|
|
20945
|
-
if (sub ===
|
|
21335
|
+
if (sub === void 0 || sub === "status") {
|
|
21336
|
+
await runDaemonStatus();
|
|
21337
|
+
return;
|
|
21338
|
+
}
|
|
21339
|
+
if (sub === "start") {
|
|
20946
21340
|
await runDaemonStart(flags);
|
|
20947
21341
|
return;
|
|
20948
21342
|
}
|
|
@@ -20954,10 +21348,6 @@ async function main() {
|
|
|
20954
21348
|
await runDaemonRestart();
|
|
20955
21349
|
return;
|
|
20956
21350
|
}
|
|
20957
|
-
if (sub === "status") {
|
|
20958
|
-
await runDaemonStatus();
|
|
20959
|
-
return;
|
|
20960
|
-
}
|
|
20961
21351
|
if (sub === "logs") {
|
|
20962
21352
|
await runDaemonLogs(tail.slice(1));
|
|
20963
21353
|
return;
|
|
@@ -21070,6 +21460,14 @@ async function main() {
|
|
|
21070
21460
|
await runAgentsRefresh();
|
|
21071
21461
|
return;
|
|
21072
21462
|
}
|
|
21463
|
+
if (sub === "install") {
|
|
21464
|
+
await runAgentsInstall(positional[2]);
|
|
21465
|
+
return;
|
|
21466
|
+
}
|
|
21467
|
+
if (sub === "sync") {
|
|
21468
|
+
await runAgentsSync(positional[2]);
|
|
21469
|
+
return;
|
|
21470
|
+
}
|
|
21073
21471
|
process.stderr.write(`Unknown agent subcommand: ${sub}
|
|
21074
21472
|
`);
|
|
21075
21473
|
process.exit(2);
|
|
@@ -21226,8 +21624,9 @@ function printHelp() {
|
|
|
21226
21624
|
" --readonly Open a session as a transcript viewer (requires --session).",
|
|
21227
21625
|
" HYDRA_ACP_SESSION Env var equivalent of --session (flag wins).",
|
|
21228
21626
|
" hydra-acp init [--rotate-token] Initialize ~/.hydra-acp/config.json",
|
|
21627
|
+
" hydra-acp daemon [status] Show daemon pid/version (default when no subcommand)",
|
|
21229
21628
|
" hydra-acp daemon start [--foreground] Start daemon (detached by default; --foreground to attach)",
|
|
21230
|
-
" hydra-acp daemon stop|restart
|
|
21629
|
+
" hydra-acp daemon stop|restart",
|
|
21231
21630
|
" hydra-acp daemon logs [-f] [-n N] Tail or follow the daemon log",
|
|
21232
21631
|
" hydra-acp session [list] [--all] [--json] [--host=<host>]",
|
|
21233
21632
|
" List sessions (live + 20 most-recent cold; --all for everything; --json emits JSON for scripts).",
|
|
@@ -21249,6 +21648,8 @@ function printHelp() {
|
|
|
21249
21648
|
" hydra-acp extension logs <name> [-f] [-n N] Tail or follow an extension's log",
|
|
21250
21649
|
" hydra-acp agent [list] List agents in the cached registry",
|
|
21251
21650
|
" hydra-acp agent refresh Force a registry re-fetch",
|
|
21651
|
+
" hydra-acp agent install <id> Pre-install <id> from the registry (else lazy on first session)",
|
|
21652
|
+
" hydra-acp agent sync <id> Spawn <id> just long enough to ACP session/list it, then persist any sessions it remembers (across every cwd) as cold rows in `session list`",
|
|
21252
21653
|
" hydra-acp auth password [--force] Set the daemon's master password",
|
|
21253
21654
|
" hydra-acp auth [list] List active session tokens",
|
|
21254
21655
|
" hydra-acp auth revoke <id> Revoke a session token",
|
package/dist/index.d.ts
CHANGED
|
@@ -1984,6 +1984,7 @@ declare const SessionRecord: z.ZodObject<{
|
|
|
1984
1984
|
name?: string | undefined;
|
|
1985
1985
|
description?: string | undefined;
|
|
1986
1986
|
}>, "many">>;
|
|
1987
|
+
pendingHistorySync: z.ZodOptional<z.ZodBoolean>;
|
|
1987
1988
|
createdAt: z.ZodString;
|
|
1988
1989
|
updatedAt: z.ZodString;
|
|
1989
1990
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -2022,6 +2023,7 @@ declare const SessionRecord: z.ZodObject<{
|
|
|
2022
2023
|
name?: string | undefined;
|
|
2023
2024
|
description?: string | undefined;
|
|
2024
2025
|
}[] | undefined;
|
|
2026
|
+
pendingHistorySync?: boolean | undefined;
|
|
2025
2027
|
}, {
|
|
2026
2028
|
sessionId: string;
|
|
2027
2029
|
version: 1;
|
|
@@ -2058,6 +2060,7 @@ declare const SessionRecord: z.ZodObject<{
|
|
|
2058
2060
|
name?: string | undefined;
|
|
2059
2061
|
description?: string | undefined;
|
|
2060
2062
|
}[] | undefined;
|
|
2063
|
+
pendingHistorySync?: boolean | undefined;
|
|
2061
2064
|
}>;
|
|
2062
2065
|
type SessionRecord = z.infer<typeof SessionRecord>;
|
|
2063
2066
|
declare class SessionStore {
|
|
@@ -2305,6 +2308,7 @@ interface ResurrectParams {
|
|
|
2305
2308
|
agentModes?: AdvertisedMode[];
|
|
2306
2309
|
agentModels?: AdvertisedModel[];
|
|
2307
2310
|
createdAt?: string;
|
|
2311
|
+
pendingHistorySync?: boolean;
|
|
2308
2312
|
}
|
|
2309
2313
|
type AgentSpawner = (opts: AgentInstanceOptions) => AgentInstance;
|
|
2310
2314
|
interface SessionManagerOptions {
|
|
@@ -2333,11 +2337,17 @@ declare class SessionManager {
|
|
|
2333
2337
|
private doResurrect;
|
|
2334
2338
|
private doResurrectFromImport;
|
|
2335
2339
|
private resolveImportCwd;
|
|
2340
|
+
syncFromAgent(agentId: string): Promise<{
|
|
2341
|
+
synced: SessionRecord[];
|
|
2342
|
+
skipped: number;
|
|
2343
|
+
}>;
|
|
2344
|
+
private collectAgentSessions;
|
|
2336
2345
|
private bootstrapAgent;
|
|
2337
2346
|
private attachManagerHooks;
|
|
2338
2347
|
getHistory(sessionId: string): Promise<HistoryEntry[] | undefined>;
|
|
2339
2348
|
loadHistory(sessionId: string): Promise<HistoryEntry[]>;
|
|
2340
2349
|
loadFromDisk(sessionId: string): Promise<ResurrectParams | undefined>;
|
|
2350
|
+
private clearPendingHistorySync;
|
|
2341
2351
|
private deriveTitleFromHistory;
|
|
2342
2352
|
get(sessionId: string): Session | undefined;
|
|
2343
2353
|
activeAgentVersions(): Map<string, Set<string>>;
|
|
@@ -2459,6 +2469,7 @@ declare const paths: {
|
|
|
2459
2469
|
extensionLogFile: (name: string) => string;
|
|
2460
2470
|
extensionPidFile: (name: string) => string;
|
|
2461
2471
|
tuiHistoryFile: (id: string) => string;
|
|
2472
|
+
globalTuiHistoryFile: () => string;
|
|
2462
2473
|
tuiLogFile: () => string;
|
|
2463
2474
|
};
|
|
2464
2475
|
|
package/dist/index.js
CHANGED
|
@@ -87,6 +87,11 @@ var paths = {
|
|
|
87
87
|
extensionLogFile: (name) => path.join(hydraHome(), "extensions", `${name}.log`),
|
|
88
88
|
extensionPidFile: (name) => path.join(hydraHome(), "extensions", `${name}.pid`),
|
|
89
89
|
tuiHistoryFile: (id) => path.join(hydraHome(), "sessions", id, "prompt-history"),
|
|
90
|
+
// Cross-session prompt history. Up-arrow / ^R fall through to this
|
|
91
|
+
// after the per-session list is exhausted. JSONL, one entry per
|
|
92
|
+
// line, append-only so concurrent TUIs don't lose each other's
|
|
93
|
+
// writes.
|
|
94
|
+
globalTuiHistoryFile: () => path.join(hydraHome(), "prompt-history"),
|
|
90
95
|
tuiLogFile: () => path.join(hydraHome(), "tui.log")
|
|
91
96
|
};
|
|
92
97
|
|
|
@@ -4325,6 +4330,12 @@ var SessionRecord = z4.object({
|
|
|
4325
4330
|
agentCommands: z4.array(PersistedAgentCommand).optional(),
|
|
4326
4331
|
agentModes: z4.array(PersistedAgentMode).optional(),
|
|
4327
4332
|
agentModels: z4.array(PersistedAgentModel).optional(),
|
|
4333
|
+
// One-shot flag set when `hydra agent sync` mints a row from an
|
|
4334
|
+
// agent-side session/list entry: signals that the first resurrect
|
|
4335
|
+
// should *keep* the agent's session/load replay (instead of draining
|
|
4336
|
+
// it) so the local history.jsonl gets populated from the agent's
|
|
4337
|
+
// memory. Cleared after that first resurrect completes.
|
|
4338
|
+
pendingHistorySync: z4.boolean().optional(),
|
|
4328
4339
|
createdAt: z4.string(),
|
|
4329
4340
|
updatedAt: z4.string()
|
|
4330
4341
|
});
|
|
@@ -4445,6 +4456,7 @@ function recordFromMemorySession(args) {
|
|
|
4445
4456
|
agentCommands: args.agentCommands,
|
|
4446
4457
|
agentModes: args.agentModes,
|
|
4447
4458
|
agentModels: args.agentModels,
|
|
4459
|
+
pendingHistorySync: args.pendingHistorySync,
|
|
4448
4460
|
createdAt: args.createdAt ?? now,
|
|
4449
4461
|
updatedAt: args.updatedAt ?? now
|
|
4450
4462
|
};
|
|
@@ -4780,7 +4792,13 @@ var SessionManager = class {
|
|
|
4780
4792
|
await agent.kill().catch(() => void 0);
|
|
4781
4793
|
return this.doResurrectFromImport(params);
|
|
4782
4794
|
}
|
|
4783
|
-
|
|
4795
|
+
if (params.pendingHistorySync === true) {
|
|
4796
|
+
void this.clearPendingHistorySync(params.hydraSessionId).catch(
|
|
4797
|
+
() => void 0
|
|
4798
|
+
);
|
|
4799
|
+
} else {
|
|
4800
|
+
agent.connection.drainBuffered("session/update");
|
|
4801
|
+
}
|
|
4784
4802
|
const session = new Session({
|
|
4785
4803
|
sessionId: params.hydraSessionId,
|
|
4786
4804
|
cwd: params.cwd,
|
|
@@ -4870,6 +4888,133 @@ var SessionManager = class {
|
|
|
4870
4888
|
}
|
|
4871
4889
|
return os2.homedir();
|
|
4872
4890
|
}
|
|
4891
|
+
// Pull every session the agent itself remembers (across all cwds) and
|
|
4892
|
+
// persist a cold hydra record for each one we don't already track.
|
|
4893
|
+
// Used by `hydra agent sync <id>` to surface sessions created outside
|
|
4894
|
+
// hydra — or by other tools — in `hydra session list` so the picker
|
|
4895
|
+
// can resurrect them. Spawns a throwaway agent process for the
|
|
4896
|
+
// initialize + session/list pair, then kills it. Records are minted
|
|
4897
|
+
// with pendingHistorySync:true so the first resurrect records the
|
|
4898
|
+
// agent's session/load replay into history.jsonl rather than dropping
|
|
4899
|
+
// it.
|
|
4900
|
+
async syncFromAgent(agentId) {
|
|
4901
|
+
const agentDef = await this.registry.getAgent(agentId);
|
|
4902
|
+
if (!agentDef) {
|
|
4903
|
+
const err = new Error(
|
|
4904
|
+
`agent ${agentId} not found in registry`
|
|
4905
|
+
);
|
|
4906
|
+
err.code = JsonRpcErrorCodes.AgentNotInstalled;
|
|
4907
|
+
throw err;
|
|
4908
|
+
}
|
|
4909
|
+
const plan = await planSpawn(agentDef, [], {
|
|
4910
|
+
npmRegistry: this.npmRegistry
|
|
4911
|
+
});
|
|
4912
|
+
const agent = this.spawner({
|
|
4913
|
+
agentId,
|
|
4914
|
+
cwd: os2.homedir(),
|
|
4915
|
+
plan
|
|
4916
|
+
});
|
|
4917
|
+
let initResult;
|
|
4918
|
+
try {
|
|
4919
|
+
initResult = await agent.connection.request(
|
|
4920
|
+
"initialize",
|
|
4921
|
+
{
|
|
4922
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
4923
|
+
clientCapabilities: {},
|
|
4924
|
+
clientInfo: { name: "hydra", version: HYDRA_VERSION }
|
|
4925
|
+
}
|
|
4926
|
+
);
|
|
4927
|
+
} catch (err) {
|
|
4928
|
+
await agent.kill().catch(() => void 0);
|
|
4929
|
+
throw err;
|
|
4930
|
+
}
|
|
4931
|
+
const caps = initResult.agentCapabilities ?? {};
|
|
4932
|
+
if (caps.sessionCapabilities?.list === void 0) {
|
|
4933
|
+
await agent.kill().catch(() => void 0);
|
|
4934
|
+
throw new Error(
|
|
4935
|
+
`agent ${agentId} does not advertise sessionCapabilities.list; cannot sync`
|
|
4936
|
+
);
|
|
4937
|
+
}
|
|
4938
|
+
let entries;
|
|
4939
|
+
try {
|
|
4940
|
+
entries = await this.collectAgentSessions(agent);
|
|
4941
|
+
} catch (err) {
|
|
4942
|
+
await agent.kill().catch(() => void 0);
|
|
4943
|
+
throw err;
|
|
4944
|
+
}
|
|
4945
|
+
await agent.kill().catch(() => void 0);
|
|
4946
|
+
const existing = /* @__PURE__ */ new Set();
|
|
4947
|
+
for (const live of this.sessions.values()) {
|
|
4948
|
+
existing.add(`${live.agentId}::${live.upstreamSessionId}`);
|
|
4949
|
+
}
|
|
4950
|
+
const stored = await this.store.list().catch(() => []);
|
|
4951
|
+
for (const rec of stored) {
|
|
4952
|
+
existing.add(`${rec.agentId}::${rec.upstreamSessionId}`);
|
|
4953
|
+
}
|
|
4954
|
+
const synced = [];
|
|
4955
|
+
let skipped = 0;
|
|
4956
|
+
for (const entry of entries) {
|
|
4957
|
+
const dedupeKey = `${agentId}::${entry.sessionId}`;
|
|
4958
|
+
if (existing.has(dedupeKey)) {
|
|
4959
|
+
skipped += 1;
|
|
4960
|
+
continue;
|
|
4961
|
+
}
|
|
4962
|
+
existing.add(dedupeKey);
|
|
4963
|
+
const newId = `${HYDRA_SESSION_PREFIX}${generateRawSessionId()}`;
|
|
4964
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4965
|
+
const ts = entry.updatedAt ?? now;
|
|
4966
|
+
const recordArgs = {
|
|
4967
|
+
sessionId: newId,
|
|
4968
|
+
lineageId: generateLineageId(),
|
|
4969
|
+
upstreamSessionId: entry.sessionId,
|
|
4970
|
+
agentId,
|
|
4971
|
+
cwd: entry.cwd,
|
|
4972
|
+
pendingHistorySync: true,
|
|
4973
|
+
createdAt: ts,
|
|
4974
|
+
updatedAt: ts
|
|
4975
|
+
};
|
|
4976
|
+
if (entry.title !== void 0) {
|
|
4977
|
+
recordArgs.title = entry.title;
|
|
4978
|
+
}
|
|
4979
|
+
const record = recordFromMemorySession(recordArgs);
|
|
4980
|
+
await this.store.write(record);
|
|
4981
|
+
synced.push({ version: 1, ...record });
|
|
4982
|
+
}
|
|
4983
|
+
return { synced, skipped };
|
|
4984
|
+
}
|
|
4985
|
+
// Paginate the agent's session/list, threading nextCursor until the
|
|
4986
|
+
// agent stops returning one. Each entry the spec guarantees has
|
|
4987
|
+
// { sessionId, cwd }; title and updatedAt are optional.
|
|
4988
|
+
async collectAgentSessions(agent) {
|
|
4989
|
+
const out = [];
|
|
4990
|
+
let cursor;
|
|
4991
|
+
for (let page = 0; page < 100; page += 1) {
|
|
4992
|
+
const params = {};
|
|
4993
|
+
if (cursor !== void 0) {
|
|
4994
|
+
params.cursor = cursor;
|
|
4995
|
+
}
|
|
4996
|
+
const result = await agent.connection.request("session/list", params);
|
|
4997
|
+
const rows = Array.isArray(result.sessions) ? result.sessions : [];
|
|
4998
|
+
for (const row of rows) {
|
|
4999
|
+
if (typeof row.sessionId !== "string" || typeof row.cwd !== "string") {
|
|
5000
|
+
continue;
|
|
5001
|
+
}
|
|
5002
|
+
const entry = { sessionId: row.sessionId, cwd: row.cwd };
|
|
5003
|
+
if (typeof row.title === "string") {
|
|
5004
|
+
entry.title = row.title;
|
|
5005
|
+
}
|
|
5006
|
+
if (typeof row.updatedAt === "string") {
|
|
5007
|
+
entry.updatedAt = row.updatedAt;
|
|
5008
|
+
}
|
|
5009
|
+
out.push(entry);
|
|
5010
|
+
}
|
|
5011
|
+
if (typeof result.nextCursor !== "string" || result.nextCursor.length === 0) {
|
|
5012
|
+
break;
|
|
5013
|
+
}
|
|
5014
|
+
cursor = result.nextCursor;
|
|
5015
|
+
}
|
|
5016
|
+
return out;
|
|
5017
|
+
}
|
|
4873
5018
|
// Bootstrap a fresh agent process: registry resolve → spawn → initialize
|
|
4874
5019
|
// → session/new. Shared by create() and the /hydra agent path so both
|
|
4875
5020
|
// go through the same env / capabilities / error-handling.
|
|
@@ -5062,9 +5207,21 @@ var SessionManager = class {
|
|
|
5062
5207
|
agentCommands: record.agentCommands,
|
|
5063
5208
|
agentModes: record.agentModes,
|
|
5064
5209
|
agentModels: record.agentModels,
|
|
5065
|
-
createdAt: record.createdAt
|
|
5210
|
+
createdAt: record.createdAt,
|
|
5211
|
+
pendingHistorySync: record.pendingHistorySync
|
|
5066
5212
|
};
|
|
5067
5213
|
}
|
|
5214
|
+
async clearPendingHistorySync(sessionId) {
|
|
5215
|
+
await this.enqueueMetaWrite(sessionId, async () => {
|
|
5216
|
+
const record = await this.store.read(sessionId);
|
|
5217
|
+
if (!record || record.pendingHistorySync !== true) {
|
|
5218
|
+
return;
|
|
5219
|
+
}
|
|
5220
|
+
const next = { ...record };
|
|
5221
|
+
delete next.pendingHistorySync;
|
|
5222
|
+
await this.store.write(next);
|
|
5223
|
+
});
|
|
5224
|
+
}
|
|
5068
5225
|
// Best-effort: peek at the persisted history's first prompt and use
|
|
5069
5226
|
// its first line (capped to 200 chars) as a session title. Returns
|
|
5070
5227
|
// undefined if no usable prompt is found or any I/O fails.
|
|
@@ -7461,7 +7618,7 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
7461
7618
|
}
|
|
7462
7619
|
|
|
7463
7620
|
// src/daemon/routes/agents.ts
|
|
7464
|
-
function registerAgentRoutes(app, registry) {
|
|
7621
|
+
function registerAgentRoutes(app, registry, manager, opts = {}) {
|
|
7465
7622
|
app.get("/v1/agents", async () => {
|
|
7466
7623
|
const doc = await registry.load();
|
|
7467
7624
|
return {
|
|
@@ -7482,6 +7639,61 @@ function registerAgentRoutes(app, registry) {
|
|
|
7482
7639
|
const doc = await registry.refresh();
|
|
7483
7640
|
return { version: doc.version, agentCount: doc.agents.length };
|
|
7484
7641
|
});
|
|
7642
|
+
app.post("/v1/agents/:id/install", async (request, reply) => {
|
|
7643
|
+
const id = request.params.id;
|
|
7644
|
+
const agent = await registry.getAgent(id);
|
|
7645
|
+
if (!agent) {
|
|
7646
|
+
reply.code(404).send({ error: `agent ${id} not found in registry` });
|
|
7647
|
+
return;
|
|
7648
|
+
}
|
|
7649
|
+
if (agent.distribution.uvx && !agent.distribution.npx && !agent.distribution.binary) {
|
|
7650
|
+
reply.send({
|
|
7651
|
+
agentId: agent.id,
|
|
7652
|
+
version: agent.version ?? "current",
|
|
7653
|
+
distribution: "uvx",
|
|
7654
|
+
installed: false,
|
|
7655
|
+
message: "uvx agents resolve on first run; nothing to pre-install."
|
|
7656
|
+
});
|
|
7657
|
+
return;
|
|
7658
|
+
}
|
|
7659
|
+
try {
|
|
7660
|
+
const plan = await planSpawn(agent, [], { npmRegistry: opts.npmRegistry });
|
|
7661
|
+
const distribution = agent.distribution.npx ? "npx" : agent.distribution.binary ? "binary" : "unknown";
|
|
7662
|
+
reply.send({
|
|
7663
|
+
agentId: agent.id,
|
|
7664
|
+
version: plan.version,
|
|
7665
|
+
distribution,
|
|
7666
|
+
installed: true,
|
|
7667
|
+
command: plan.command
|
|
7668
|
+
});
|
|
7669
|
+
} catch (err) {
|
|
7670
|
+
reply.code(500).send({ error: err.message });
|
|
7671
|
+
}
|
|
7672
|
+
});
|
|
7673
|
+
app.post("/v1/agents/:id/sync", async (request, reply) => {
|
|
7674
|
+
const agentId = request.params.id;
|
|
7675
|
+
try {
|
|
7676
|
+
const { synced, skipped } = await manager.syncFromAgent(agentId);
|
|
7677
|
+
return {
|
|
7678
|
+
synced: synced.map((r) => ({
|
|
7679
|
+
sessionId: r.sessionId,
|
|
7680
|
+
upstreamSessionId: r.upstreamSessionId,
|
|
7681
|
+
agentId: r.agentId,
|
|
7682
|
+
cwd: r.cwd,
|
|
7683
|
+
title: r.title,
|
|
7684
|
+
updatedAt: r.updatedAt
|
|
7685
|
+
})),
|
|
7686
|
+
skipped
|
|
7687
|
+
};
|
|
7688
|
+
} catch (err) {
|
|
7689
|
+
const e = err;
|
|
7690
|
+
if (e.code === JsonRpcErrorCodes.AgentNotInstalled) {
|
|
7691
|
+
reply.code(404).send({ error: e.message });
|
|
7692
|
+
return;
|
|
7693
|
+
}
|
|
7694
|
+
reply.code(409).send({ error: e.message });
|
|
7695
|
+
}
|
|
7696
|
+
});
|
|
7485
7697
|
}
|
|
7486
7698
|
|
|
7487
7699
|
// src/daemon/routes/health.ts
|
|
@@ -8597,7 +8809,7 @@ async function startDaemon(config, serviceToken) {
|
|
|
8597
8809
|
agentId: config.defaultAgent,
|
|
8598
8810
|
cwd: config.defaultCwd
|
|
8599
8811
|
});
|
|
8600
|
-
registerAgentRoutes(app, registry);
|
|
8812
|
+
registerAgentRoutes(app, registry, manager, { npmRegistry: config.npmRegistry });
|
|
8601
8813
|
registerExtensionRoutes(app, extensions);
|
|
8602
8814
|
registerConfigRoutes(app, {
|
|
8603
8815
|
defaultAgent: config.defaultAgent,
|