@hydra-acp/cli 0.1.40 → 0.1.42
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 +528 -57
- package/dist/index.d.ts +14 -0
- package/dist/index.js +226 -7
- package/package.json +1 -1
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
|
}
|
|
@@ -980,7 +985,8 @@ function mergeMeta(passthrough, ours) {
|
|
|
980
985
|
function sessionListEntryToWire(entry) {
|
|
981
986
|
const hydraMeta = {
|
|
982
987
|
attachedClients: entry.attachedClients,
|
|
983
|
-
status: entry.status
|
|
988
|
+
status: entry.status,
|
|
989
|
+
busy: entry.busy
|
|
984
990
|
};
|
|
985
991
|
if (entry.agentId !== void 0) {
|
|
986
992
|
hydraMeta.agentId = entry.agentId;
|
|
@@ -1126,6 +1132,10 @@ var init_types = __esm({
|
|
|
1126
1132
|
updatedAt: z3.string(),
|
|
1127
1133
|
attachedClients: z3.number().int().nonnegative(),
|
|
1128
1134
|
status: z3.enum(["live", "cold"]).default("live"),
|
|
1135
|
+
// True while the session is mid-turn (an agent prompt is in flight).
|
|
1136
|
+
// Always false for cold sessions. Lets pickers render a busy dot
|
|
1137
|
+
// without having to attach.
|
|
1138
|
+
busy: z3.boolean().default(false),
|
|
1129
1139
|
_meta: z3.record(z3.unknown()).optional()
|
|
1130
1140
|
});
|
|
1131
1141
|
SessionListEntryWire = z3.object({
|
|
@@ -3832,7 +3842,7 @@ function parseHistory(text) {
|
|
|
3832
3842
|
}
|
|
3833
3843
|
return out;
|
|
3834
3844
|
}
|
|
3835
|
-
function appendEntry(history, entry) {
|
|
3845
|
+
function appendEntry(history, entry, cap = HISTORY_CAP) {
|
|
3836
3846
|
const trimmed = entry.replace(/\n+$/, "");
|
|
3837
3847
|
if (trimmed.length === 0) {
|
|
3838
3848
|
return history;
|
|
@@ -3841,8 +3851,8 @@ function appendEntry(history, entry) {
|
|
|
3841
3851
|
return history;
|
|
3842
3852
|
}
|
|
3843
3853
|
const out = history.concat(trimmed);
|
|
3844
|
-
if (out.length >
|
|
3845
|
-
return out.slice(out.length -
|
|
3854
|
+
if (out.length > cap) {
|
|
3855
|
+
return out.slice(out.length - cap);
|
|
3846
3856
|
}
|
|
3847
3857
|
return out;
|
|
3848
3858
|
}
|
|
@@ -3851,11 +3861,30 @@ async function saveHistory(file, history) {
|
|
|
3851
3861
|
const lines = history.map((entry) => JSON.stringify(entry));
|
|
3852
3862
|
await fs10.writeFile(file, lines.length > 0 ? lines.join("\n") + "\n" : "");
|
|
3853
3863
|
}
|
|
3854
|
-
|
|
3864
|
+
async function appendHistoryLine(file, entry) {
|
|
3865
|
+
const trimmed = entry.replace(/\n+$/, "");
|
|
3866
|
+
if (trimmed.length === 0) {
|
|
3867
|
+
return;
|
|
3868
|
+
}
|
|
3869
|
+
await fs10.mkdir(path6.dirname(file), { recursive: true });
|
|
3870
|
+
await fs10.appendFile(file, JSON.stringify(trimmed) + "\n", {
|
|
3871
|
+
encoding: "utf8"
|
|
3872
|
+
});
|
|
3873
|
+
}
|
|
3874
|
+
function buildCombinedHistory(global, session) {
|
|
3875
|
+
if (session.length === 0) {
|
|
3876
|
+
return [...global];
|
|
3877
|
+
}
|
|
3878
|
+
const sessionSet = new Set(session);
|
|
3879
|
+
const filteredGlobal = global.filter((e) => !sessionSet.has(e));
|
|
3880
|
+
return [...filteredGlobal, ...session];
|
|
3881
|
+
}
|
|
3882
|
+
var HISTORY_CAP, GLOBAL_HISTORY_CAP;
|
|
3855
3883
|
var init_history = __esm({
|
|
3856
3884
|
"src/tui/history.ts"() {
|
|
3857
3885
|
"use strict";
|
|
3858
3886
|
HISTORY_CAP = 500;
|
|
3887
|
+
GLOBAL_HISTORY_CAP = 2e3;
|
|
3859
3888
|
}
|
|
3860
3889
|
});
|
|
3861
3890
|
|
|
@@ -4425,7 +4454,8 @@ async function listSessions(target, opts = {}, fetchImpl = fetch) {
|
|
|
4425
4454
|
currentUsage: s.currentUsage,
|
|
4426
4455
|
title: s.title,
|
|
4427
4456
|
importedFromMachine: s.importedFromMachine,
|
|
4428
|
-
importedFromUpstreamSessionId: s.importedFromUpstreamSessionId
|
|
4457
|
+
importedFromUpstreamSessionId: s.importedFromUpstreamSessionId,
|
|
4458
|
+
busy: s.busy
|
|
4429
4459
|
}));
|
|
4430
4460
|
}
|
|
4431
4461
|
async function killSession(target, id, fetchImpl = fetch) {
|
|
@@ -4549,7 +4579,7 @@ function toRow(s, now = Date.now()) {
|
|
|
4549
4579
|
return {
|
|
4550
4580
|
session: stripHydraSessionPrefix(s.sessionId),
|
|
4551
4581
|
upstream: formatUpstreamCell(s.upstreamSessionId, s.importedFromMachine),
|
|
4552
|
-
state: formatState(s.status, s.
|
|
4582
|
+
state: formatState(s.status, s.busy),
|
|
4553
4583
|
agent: formatAgentCell(s.agentId, s.currentUsage),
|
|
4554
4584
|
age: formatRelativeAge(s.updatedAt, now),
|
|
4555
4585
|
title: s.title ?? "-",
|
|
@@ -4565,11 +4595,11 @@ function formatUpstreamCell(upstreamSessionId, importedFromMachine) {
|
|
|
4565
4595
|
}
|
|
4566
4596
|
return "-";
|
|
4567
4597
|
}
|
|
4568
|
-
function formatState(status,
|
|
4598
|
+
function formatState(status, busy) {
|
|
4569
4599
|
if (status === "cold") {
|
|
4570
4600
|
return "COLD";
|
|
4571
4601
|
}
|
|
4572
|
-
return
|
|
4602
|
+
return busy ? "LIVE\u2022" : "LIVE";
|
|
4573
4603
|
}
|
|
4574
4604
|
function computeWidths(rows) {
|
|
4575
4605
|
return {
|
|
@@ -7005,6 +7035,10 @@ uncaught: ${err.stack ?? err.message}
|
|
|
7005
7035
|
this.handleCsi27Stdin(text);
|
|
7006
7036
|
return;
|
|
7007
7037
|
}
|
|
7038
|
+
if (text.includes("\x1B[200~")) {
|
|
7039
|
+
this.handleRawStdinSegment(text);
|
|
7040
|
+
return;
|
|
7041
|
+
}
|
|
7008
7042
|
if (text.includes("\n")) {
|
|
7009
7043
|
const parts = text.split("\n");
|
|
7010
7044
|
for (let i = 0; i < parts.length; i++) {
|
|
@@ -7353,24 +7387,22 @@ uncaught: ${err.stack ?? err.message}
|
|
|
7353
7387
|
this.writeProgressIndicator(this.banner.status === "busy" ? 3 : 0);
|
|
7354
7388
|
this.syncedPartialRepaint(() => this.drawBanner());
|
|
7355
7389
|
}
|
|
7356
|
-
// Wrap a partial repaint (banner-only,
|
|
7390
|
+
// Wrap a partial repaint (banner-only, prompt-only, etc.) in a
|
|
7357
7391
|
// synchronized-output bracket so the row swap is atomic on terminals
|
|
7358
|
-
// that support DEC 2026
|
|
7359
|
-
//
|
|
7360
|
-
//
|
|
7361
|
-
//
|
|
7362
|
-
//
|
|
7392
|
+
// that support DEC 2026. Cursor movement (moveTo) is buffered inside
|
|
7393
|
+
// BSU/ESU, so the cursor appears at its final placeCursor position
|
|
7394
|
+
// without visibly visiting intermediate rows. We intentionally do NOT
|
|
7395
|
+
// hide the cursor here: ?25l/h (cursor visibility) is terminal *state*
|
|
7396
|
+
// applied immediately rather than buffered, so hiding inside a BSU/ESU
|
|
7397
|
+
// block causes a visible blink (cursor disappears → frame commits →
|
|
7398
|
+
// cursor reappears) on every banner tick — worse than any skitter.
|
|
7363
7399
|
syncedPartialRepaint(paint) {
|
|
7364
7400
|
if (!this.started) {
|
|
7365
7401
|
return;
|
|
7366
7402
|
}
|
|
7367
7403
|
withSync(() => {
|
|
7368
|
-
this.term.hideCursor();
|
|
7369
7404
|
paint();
|
|
7370
7405
|
this.placeCursor();
|
|
7371
|
-
if (this.permissionPrompt || this.confirmPrompt || this.helpPrompt) {
|
|
7372
|
-
this.term.hideCursor(false);
|
|
7373
|
-
}
|
|
7374
7406
|
});
|
|
7375
7407
|
}
|
|
7376
7408
|
currentModeId() {
|
|
@@ -8462,7 +8494,7 @@ uncaught: ${err.stack ?? err.message}
|
|
|
8462
8494
|
const body = ` ${marker} ${i + 1}. ${truncate(opt.label, w - 8)}`;
|
|
8463
8495
|
writeRow(`perm|o|${w}|${i}|${isSel ? "1" : "0"}|${opt.label}`, () => {
|
|
8464
8496
|
if (isSel) {
|
|
8465
|
-
this.term.
|
|
8497
|
+
this.term.brightYellow(body);
|
|
8466
8498
|
} else {
|
|
8467
8499
|
this.term.dim(body);
|
|
8468
8500
|
}
|
|
@@ -8771,7 +8803,7 @@ async function pickSession(term, opts) {
|
|
|
8771
8803
|
if (tier !== 0) {
|
|
8772
8804
|
return tier;
|
|
8773
8805
|
}
|
|
8774
|
-
return b.updatedAt.localeCompare(a.updatedAt);
|
|
8806
|
+
return b.updatedAt.slice(0, 16).localeCompare(a.updatedAt.slice(0, 16));
|
|
8775
8807
|
});
|
|
8776
8808
|
};
|
|
8777
8809
|
let cwdOnly = false;
|
|
@@ -9178,6 +9210,8 @@ async function pickSession(term, opts) {
|
|
|
9178
9210
|
renderFromScratch();
|
|
9179
9211
|
return await new Promise((resolve6) => {
|
|
9180
9212
|
let resolved = false;
|
|
9213
|
+
let autoRefreshTimer = null;
|
|
9214
|
+
let autoRefreshInFlight = false;
|
|
9181
9215
|
const onResize = () => {
|
|
9182
9216
|
if (resolved) {
|
|
9183
9217
|
return;
|
|
@@ -9189,6 +9223,10 @@ async function pickSession(term, opts) {
|
|
|
9189
9223
|
return;
|
|
9190
9224
|
}
|
|
9191
9225
|
resolved = true;
|
|
9226
|
+
if (autoRefreshTimer) {
|
|
9227
|
+
clearInterval(autoRefreshTimer);
|
|
9228
|
+
autoRefreshTimer = null;
|
|
9229
|
+
}
|
|
9192
9230
|
term.off("key", onKey);
|
|
9193
9231
|
term.off("resize", onResize);
|
|
9194
9232
|
process.stdout.write("\x1B[?2004l");
|
|
@@ -9205,8 +9243,16 @@ async function pickSession(term, opts) {
|
|
|
9205
9243
|
term.moveTo(1, indicatorRow() + 1);
|
|
9206
9244
|
term("\n");
|
|
9207
9245
|
};
|
|
9208
|
-
const
|
|
9246
|
+
const renderFingerprint = () => {
|
|
9247
|
+
const cells = rows.map(
|
|
9248
|
+
(r) => `${r.session}|${r.upstream}|${r.state}|${r.agent}|${r.age}|${r.title}|${r.cwd}`
|
|
9249
|
+
).join("\n");
|
|
9250
|
+
return `${selectedIdx}:${scrollOffset}:${transientStatus ?? ""}
|
|
9251
|
+
${cells}`;
|
|
9252
|
+
};
|
|
9253
|
+
const refresh = async (preferredId, refreshOpts = {}) => {
|
|
9209
9254
|
try {
|
|
9255
|
+
const beforeKey = refreshOpts.silent ? renderFingerprint() : "";
|
|
9210
9256
|
const next = await listSessions(opts.target);
|
|
9211
9257
|
allSessions = sortSessions(next);
|
|
9212
9258
|
applyFilter();
|
|
@@ -9223,8 +9269,14 @@ async function pickSession(term, opts) {
|
|
|
9223
9269
|
scrollOffset = Math.max(0, visible.length - viewportSize);
|
|
9224
9270
|
}
|
|
9225
9271
|
adjustScroll();
|
|
9272
|
+
if (refreshOpts.silent && renderFingerprint() === beforeKey) {
|
|
9273
|
+
return;
|
|
9274
|
+
}
|
|
9226
9275
|
renderFromScratch();
|
|
9227
9276
|
} catch (err) {
|
|
9277
|
+
if (refreshOpts.silent) {
|
|
9278
|
+
return;
|
|
9279
|
+
}
|
|
9228
9280
|
transientStatus = `refresh failed: ${err.message}`;
|
|
9229
9281
|
renderFromScratch();
|
|
9230
9282
|
}
|
|
@@ -9701,6 +9753,16 @@ async function pickSession(term, opts) {
|
|
|
9701
9753
|
}
|
|
9702
9754
|
term.on("key", onKey);
|
|
9703
9755
|
term.on("resize", onResize);
|
|
9756
|
+
autoRefreshTimer = setInterval(() => {
|
|
9757
|
+
if (resolved || mode !== "normal" || searchActive || autoRefreshInFlight) {
|
|
9758
|
+
return;
|
|
9759
|
+
}
|
|
9760
|
+
const currentId = selectedIdx > 0 ? visible[selectedIdx - 1]?.sessionId : void 0;
|
|
9761
|
+
autoRefreshInFlight = true;
|
|
9762
|
+
void refresh(currentId, { silent: true }).finally(() => {
|
|
9763
|
+
autoRefreshInFlight = false;
|
|
9764
|
+
});
|
|
9765
|
+
}, 3e3);
|
|
9704
9766
|
});
|
|
9705
9767
|
}
|
|
9706
9768
|
function readTermHeight(term) {
|
|
@@ -11019,11 +11081,13 @@ function toolIconStyle(status) {
|
|
|
11019
11081
|
}
|
|
11020
11082
|
function formatPlan(event) {
|
|
11021
11083
|
const stopped = event.stopped === true;
|
|
11084
|
+
const amended = event.amended === true;
|
|
11085
|
+
const stoppedStyle = amended ? "tool-status-cancelled" : "tool-status-fail";
|
|
11022
11086
|
if (event.entries.length === 0) {
|
|
11023
11087
|
return [
|
|
11024
11088
|
{
|
|
11025
11089
|
prefix: "\u25A3 ",
|
|
11026
|
-
prefixStyle: stopped ?
|
|
11090
|
+
prefixStyle: stopped ? stoppedStyle : "plan",
|
|
11027
11091
|
body: "(empty plan)",
|
|
11028
11092
|
bodyStyle: "dim"
|
|
11029
11093
|
}
|
|
@@ -11032,7 +11096,7 @@ function formatPlan(event) {
|
|
|
11032
11096
|
const allComplete = event.entries.every(
|
|
11033
11097
|
(e) => (e.status ?? "pending") === "completed"
|
|
11034
11098
|
);
|
|
11035
|
-
const headerStyle = allComplete ? "plan-done" : stopped ?
|
|
11099
|
+
const headerStyle = allComplete ? "plan-done" : stopped ? stoppedStyle : "plan";
|
|
11036
11100
|
const lines = [
|
|
11037
11101
|
{
|
|
11038
11102
|
prefix: "\u25A3 ",
|
|
@@ -11137,9 +11201,38 @@ async function runTuiApp(opts) {
|
|
|
11137
11201
|
const viewPrefs = {
|
|
11138
11202
|
showThoughts: config.tui.showThoughts
|
|
11139
11203
|
};
|
|
11204
|
+
let altScreenEngaged = false;
|
|
11205
|
+
const enterAltScreen = () => {
|
|
11206
|
+
if (altScreenEngaged) {
|
|
11207
|
+
return;
|
|
11208
|
+
}
|
|
11209
|
+
term.fullscreen(true);
|
|
11210
|
+
altScreenEngaged = true;
|
|
11211
|
+
};
|
|
11212
|
+
const leaveAltScreen = () => {
|
|
11213
|
+
if (!altScreenEngaged) {
|
|
11214
|
+
return;
|
|
11215
|
+
}
|
|
11216
|
+
term.fullscreen(false);
|
|
11217
|
+
altScreenEngaged = false;
|
|
11218
|
+
process.stdout.write("\n");
|
|
11219
|
+
};
|
|
11220
|
+
enterAltScreen();
|
|
11221
|
+
const altScreenCleanup = () => {
|
|
11222
|
+
if (altScreenEngaged) {
|
|
11223
|
+
term.fullscreen(false);
|
|
11224
|
+
altScreenEngaged = false;
|
|
11225
|
+
}
|
|
11226
|
+
};
|
|
11227
|
+
process.once("exit", altScreenCleanup);
|
|
11140
11228
|
let nextOpts = opts;
|
|
11141
|
-
|
|
11142
|
-
|
|
11229
|
+
try {
|
|
11230
|
+
while (nextOpts !== null) {
|
|
11231
|
+
nextOpts = await runSession(term, config, target, nextOpts, exitHint, viewPrefs);
|
|
11232
|
+
}
|
|
11233
|
+
} finally {
|
|
11234
|
+
leaveAltScreen();
|
|
11235
|
+
process.off("exit", altScreenCleanup);
|
|
11143
11236
|
}
|
|
11144
11237
|
const pendingUpdate = await getPendingUpdate();
|
|
11145
11238
|
if (pendingUpdate) {
|
|
@@ -11149,7 +11242,6 @@ async function runTuiApp(opts) {
|
|
|
11149
11242
|
if (exitHint.sessionId && process.stdout.isTTY) {
|
|
11150
11243
|
const short = stripHydraSessionPrefix(exitHint.sessionId);
|
|
11151
11244
|
const flags = exitHint.readonly ? " --readonly" : "";
|
|
11152
|
-
process.stdout.write("\x1B[2J\x1B[H");
|
|
11153
11245
|
process.stdout.write(
|
|
11154
11246
|
`To resume: ${invokedBinName()} tui --session ${short}${flags}
|
|
11155
11247
|
`
|
|
@@ -11160,7 +11252,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
|
|
|
11160
11252
|
const ctx = await resolveSession(term, config, target, opts);
|
|
11161
11253
|
if (!ctx) {
|
|
11162
11254
|
term.grabInput(false);
|
|
11163
|
-
|
|
11255
|
+
return null;
|
|
11164
11256
|
}
|
|
11165
11257
|
const launchLabelBase = ctx.sessionId === "__new__" ? "Starting new session\u2026" : "Resuming session\u2026";
|
|
11166
11258
|
const installStatus = createInstallStatusLine(term, launchLabelBase);
|
|
@@ -11656,9 +11748,35 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
|
|
|
11656
11748
|
initialQueue = hydraMeta.queue;
|
|
11657
11749
|
}
|
|
11658
11750
|
const historyFile = paths.tuiHistoryFile(resolvedSessionId);
|
|
11751
|
+
const globalHistoryFile = paths.globalTuiHistoryFile();
|
|
11659
11752
|
let history = await loadHistory(historyFile).catch(() => []);
|
|
11660
|
-
|
|
11753
|
+
let globalHistory = await loadHistory(globalHistoryFile).catch(() => []);
|
|
11754
|
+
if (globalHistory.length > GLOBAL_HISTORY_CAP) {
|
|
11755
|
+
globalHistory = globalHistory.slice(globalHistory.length - GLOBAL_HISTORY_CAP);
|
|
11756
|
+
}
|
|
11757
|
+
const dispatcher = new InputDispatcher({
|
|
11758
|
+
history: buildCombinedHistory(globalHistory, history)
|
|
11759
|
+
});
|
|
11661
11760
|
dispatcherRef = dispatcher;
|
|
11761
|
+
const recordHistoryEntry = (entry) => {
|
|
11762
|
+
const trimmed = entry.replace(/\n+$/, "");
|
|
11763
|
+
if (trimmed.length === 0) {
|
|
11764
|
+
return;
|
|
11765
|
+
}
|
|
11766
|
+
const nextSession = appendEntry(history, trimmed);
|
|
11767
|
+
const sessionChanged = nextSession !== history;
|
|
11768
|
+
history = nextSession;
|
|
11769
|
+
const nextGlobal = appendEntry(globalHistory, trimmed, GLOBAL_HISTORY_CAP);
|
|
11770
|
+
const globalChanged = nextGlobal !== globalHistory;
|
|
11771
|
+
globalHistory = nextGlobal;
|
|
11772
|
+
dispatcher.setHistory(buildCombinedHistory(globalHistory, history));
|
|
11773
|
+
if (sessionChanged) {
|
|
11774
|
+
saveHistory(historyFile, history).catch(() => void 0);
|
|
11775
|
+
}
|
|
11776
|
+
if (globalChanged) {
|
|
11777
|
+
appendHistoryLine(globalHistoryFile, trimmed).catch(() => void 0);
|
|
11778
|
+
}
|
|
11779
|
+
};
|
|
11662
11780
|
if (pendingTurns > 0) {
|
|
11663
11781
|
dispatcher.setTurnRunning(true);
|
|
11664
11782
|
}
|
|
@@ -11867,7 +11985,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
|
|
|
11867
11985
|
const sessionbarAgent = resolvedAgentId || agentInfoName || "?";
|
|
11868
11986
|
const usage = { ...initialUsage ?? {} };
|
|
11869
11987
|
installStatus.finalize();
|
|
11870
|
-
screen.start();
|
|
11988
|
+
screen.start({ skipFullscreen: true });
|
|
11871
11989
|
screen.setHideThoughts(!viewPrefs.showThoughts);
|
|
11872
11990
|
screen.setSessionbar({
|
|
11873
11991
|
agent: sessionbarAgent,
|
|
@@ -12015,7 +12133,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
|
|
|
12015
12133
|
sessionElapsedTimer = null;
|
|
12016
12134
|
}
|
|
12017
12135
|
screen.clearWindowTitle();
|
|
12018
|
-
screen.stop();
|
|
12136
|
+
screen.stop({ keepFullscreen: true });
|
|
12019
12137
|
saveHistory(historyFile, history).catch(() => void 0);
|
|
12020
12138
|
void stream.close().catch(() => void 0);
|
|
12021
12139
|
};
|
|
@@ -12035,8 +12153,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
|
|
|
12035
12153
|
}
|
|
12036
12154
|
const pendingDraft = dispatcher.state().buffer.join("\n");
|
|
12037
12155
|
if (pendingDraft.replace(/\s+$/, "").length > 0) {
|
|
12038
|
-
|
|
12039
|
-
dispatcher.setHistory(history);
|
|
12156
|
+
recordHistoryEntry(pendingDraft);
|
|
12040
12157
|
}
|
|
12041
12158
|
screen.pauseRepaint();
|
|
12042
12159
|
screen.stop({ keepFullscreen: true });
|
|
@@ -12415,9 +12532,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
|
|
|
12415
12532
|
if (handleBuiltinCommand(text)) {
|
|
12416
12533
|
return;
|
|
12417
12534
|
}
|
|
12418
|
-
|
|
12419
|
-
dispatcher.setHistory(history);
|
|
12420
|
-
saveHistory(historyFile, history).catch(() => void 0);
|
|
12535
|
+
recordHistoryEntry(text);
|
|
12421
12536
|
void runPrompt(text, attachments);
|
|
12422
12537
|
};
|
|
12423
12538
|
const amendPrompt = (text, attachments) => {
|
|
@@ -12425,9 +12540,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
|
|
|
12425
12540
|
if (handleBuiltinCommand(text)) {
|
|
12426
12541
|
return;
|
|
12427
12542
|
}
|
|
12428
|
-
|
|
12429
|
-
dispatcher.setHistory(history);
|
|
12430
|
-
saveHistory(historyFile, history).catch(() => void 0);
|
|
12543
|
+
recordHistoryEntry(text);
|
|
12431
12544
|
if (!daemonSupportsAmend || currentHeadMessageId === void 0) {
|
|
12432
12545
|
void runPrompt(text, attachments);
|
|
12433
12546
|
return;
|
|
@@ -12787,16 +12900,18 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
|
|
|
12787
12900
|
const end = toolsBlockEndedAt ?? Date.now();
|
|
12788
12901
|
const elapsed = end - toolsBlockStartedAt;
|
|
12789
12902
|
const stoppedReason = !inProgress && toolsBlockStopReason !== null && toolsBlockStopReason !== "end_turn" ? toolsBlockStopReason : null;
|
|
12903
|
+
const isAmended = stoppedReason === "amended";
|
|
12904
|
+
const stoppedLabel = isAmended ? `amended \xB7 ${formatElapsed(elapsed)}` : `stopped (${stoppedReason}) \xB7 ${formatElapsed(elapsed)}`;
|
|
12790
12905
|
let summary;
|
|
12791
12906
|
if (total === 0) {
|
|
12792
12907
|
if (stoppedReason !== null) {
|
|
12793
|
-
summary =
|
|
12908
|
+
summary = stoppedLabel;
|
|
12794
12909
|
} else {
|
|
12795
12910
|
summary = inProgress ? `thinking \xB7 ${formatElapsed(elapsed)}` : `thought \xB7 ${formatElapsed(elapsed)}`;
|
|
12796
12911
|
}
|
|
12797
12912
|
} else {
|
|
12798
12913
|
const noun = total === 1 ? "tool" : "tools";
|
|
12799
|
-
const timing = stoppedReason !== null ?
|
|
12914
|
+
const timing = stoppedReason !== null ? stoppedLabel : inProgress ? formatElapsed(elapsed) : `took ${formatElapsed(elapsed)}`;
|
|
12800
12915
|
const parts = [`${total} ${noun}`, timing];
|
|
12801
12916
|
if (inProgress) {
|
|
12802
12917
|
if (hidden > 0) {
|
|
@@ -12808,8 +12923,9 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
|
|
|
12808
12923
|
summary = parts.join(" \xB7 ");
|
|
12809
12924
|
}
|
|
12810
12925
|
const pureThinking = total === 0 && inProgress;
|
|
12811
|
-
const
|
|
12812
|
-
const
|
|
12926
|
+
const stoppedHeaderStyle = isAmended ? "tool-status-cancelled" : "tool-status-fail";
|
|
12927
|
+
const frozenStyle = stoppedReason !== null ? stoppedHeaderStyle : "tool";
|
|
12928
|
+
const frozenBodyStyle = stoppedReason !== null ? stoppedHeaderStyle : "dim";
|
|
12813
12929
|
const lines = [
|
|
12814
12930
|
{
|
|
12815
12931
|
prefix: "\u2699 ",
|
|
@@ -12985,7 +13101,11 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
|
|
|
12985
13101
|
effectiveStopReason = "error";
|
|
12986
13102
|
}
|
|
12987
13103
|
if (lastPlanEvent !== null && effectiveStopReason !== void 0 && effectiveStopReason !== "end_turn") {
|
|
12988
|
-
const lines = formatEvent({
|
|
13104
|
+
const lines = formatEvent({
|
|
13105
|
+
...lastPlanEvent,
|
|
13106
|
+
stopped: true,
|
|
13107
|
+
amended: event.amended === true
|
|
13108
|
+
});
|
|
12989
13109
|
if (lines.length > 0) {
|
|
12990
13110
|
screen.upsertLines("plan", lines);
|
|
12991
13111
|
}
|
|
@@ -12997,7 +13117,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
|
|
|
12997
13117
|
toolsBlockStopReason = effectiveStopReason ?? null;
|
|
12998
13118
|
renderToolsBlock();
|
|
12999
13119
|
screen.clearKey("tools");
|
|
13000
|
-
} else if (effectiveStopReason !== void 0 && effectiveStopReason !== "end_turn") {
|
|
13120
|
+
} else if (effectiveStopReason !== void 0 && effectiveStopReason !== "end_turn" && effectiveStopReason !== "amended") {
|
|
13001
13121
|
screen.appendLines([
|
|
13002
13122
|
{
|
|
13003
13123
|
prefix: "\u26A0 ",
|
|
@@ -14693,6 +14813,12 @@ var SessionRecord = z4.object({
|
|
|
14693
14813
|
agentCommands: z4.array(PersistedAgentCommand).optional(),
|
|
14694
14814
|
agentModes: z4.array(PersistedAgentMode).optional(),
|
|
14695
14815
|
agentModels: z4.array(PersistedAgentModel).optional(),
|
|
14816
|
+
// One-shot flag set when `hydra agent sync` mints a row from an
|
|
14817
|
+
// agent-side session/list entry: signals that the first resurrect
|
|
14818
|
+
// should *keep* the agent's session/load replay (instead of draining
|
|
14819
|
+
// it) so the local history.jsonl gets populated from the agent's
|
|
14820
|
+
// memory. Cleared after that first resurrect completes.
|
|
14821
|
+
pendingHistorySync: z4.boolean().optional(),
|
|
14696
14822
|
createdAt: z4.string(),
|
|
14697
14823
|
updatedAt: z4.string()
|
|
14698
14824
|
});
|
|
@@ -14813,6 +14939,7 @@ function recordFromMemorySession(args) {
|
|
|
14813
14939
|
agentCommands: args.agentCommands,
|
|
14814
14940
|
agentModes: args.agentModes,
|
|
14815
14941
|
agentModels: args.agentModels,
|
|
14942
|
+
pendingHistorySync: args.pendingHistorySync,
|
|
14816
14943
|
createdAt: args.createdAt ?? now,
|
|
14817
14944
|
updatedAt: args.updatedAt ?? now
|
|
14818
14945
|
};
|
|
@@ -15118,7 +15245,13 @@ var SessionManager = class {
|
|
|
15118
15245
|
await agent.kill().catch(() => void 0);
|
|
15119
15246
|
return this.doResurrectFromImport(params);
|
|
15120
15247
|
}
|
|
15121
|
-
|
|
15248
|
+
if (params.pendingHistorySync === true) {
|
|
15249
|
+
void this.clearPendingHistorySync(params.hydraSessionId).catch(
|
|
15250
|
+
() => void 0
|
|
15251
|
+
);
|
|
15252
|
+
} else {
|
|
15253
|
+
agent.connection.drainBuffered("session/update");
|
|
15254
|
+
}
|
|
15122
15255
|
const session = new Session({
|
|
15123
15256
|
sessionId: params.hydraSessionId,
|
|
15124
15257
|
cwd: params.cwd,
|
|
@@ -15208,6 +15341,133 @@ var SessionManager = class {
|
|
|
15208
15341
|
}
|
|
15209
15342
|
return os3.homedir();
|
|
15210
15343
|
}
|
|
15344
|
+
// Pull every session the agent itself remembers (across all cwds) and
|
|
15345
|
+
// persist a cold hydra record for each one we don't already track.
|
|
15346
|
+
// Used by `hydra agent sync <id>` to surface sessions created outside
|
|
15347
|
+
// hydra — or by other tools — in `hydra session list` so the picker
|
|
15348
|
+
// can resurrect them. Spawns a throwaway agent process for the
|
|
15349
|
+
// initialize + session/list pair, then kills it. Records are minted
|
|
15350
|
+
// with pendingHistorySync:true so the first resurrect records the
|
|
15351
|
+
// agent's session/load replay into history.jsonl rather than dropping
|
|
15352
|
+
// it.
|
|
15353
|
+
async syncFromAgent(agentId) {
|
|
15354
|
+
const agentDef = await this.registry.getAgent(agentId);
|
|
15355
|
+
if (!agentDef) {
|
|
15356
|
+
const err = new Error(
|
|
15357
|
+
`agent ${agentId} not found in registry`
|
|
15358
|
+
);
|
|
15359
|
+
err.code = JsonRpcErrorCodes.AgentNotInstalled;
|
|
15360
|
+
throw err;
|
|
15361
|
+
}
|
|
15362
|
+
const plan = await planSpawn(agentDef, [], {
|
|
15363
|
+
npmRegistry: this.npmRegistry
|
|
15364
|
+
});
|
|
15365
|
+
const agent = this.spawner({
|
|
15366
|
+
agentId,
|
|
15367
|
+
cwd: os3.homedir(),
|
|
15368
|
+
plan
|
|
15369
|
+
});
|
|
15370
|
+
let initResult;
|
|
15371
|
+
try {
|
|
15372
|
+
initResult = await agent.connection.request(
|
|
15373
|
+
"initialize",
|
|
15374
|
+
{
|
|
15375
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
15376
|
+
clientCapabilities: {},
|
|
15377
|
+
clientInfo: { name: "hydra", version: HYDRA_VERSION }
|
|
15378
|
+
}
|
|
15379
|
+
);
|
|
15380
|
+
} catch (err) {
|
|
15381
|
+
await agent.kill().catch(() => void 0);
|
|
15382
|
+
throw err;
|
|
15383
|
+
}
|
|
15384
|
+
const caps = initResult.agentCapabilities ?? {};
|
|
15385
|
+
if (caps.sessionCapabilities?.list === void 0) {
|
|
15386
|
+
await agent.kill().catch(() => void 0);
|
|
15387
|
+
throw new Error(
|
|
15388
|
+
`agent ${agentId} does not advertise sessionCapabilities.list; cannot sync`
|
|
15389
|
+
);
|
|
15390
|
+
}
|
|
15391
|
+
let entries;
|
|
15392
|
+
try {
|
|
15393
|
+
entries = await this.collectAgentSessions(agent);
|
|
15394
|
+
} catch (err) {
|
|
15395
|
+
await agent.kill().catch(() => void 0);
|
|
15396
|
+
throw err;
|
|
15397
|
+
}
|
|
15398
|
+
await agent.kill().catch(() => void 0);
|
|
15399
|
+
const existing = /* @__PURE__ */ new Set();
|
|
15400
|
+
for (const live of this.sessions.values()) {
|
|
15401
|
+
existing.add(`${live.agentId}::${live.upstreamSessionId}`);
|
|
15402
|
+
}
|
|
15403
|
+
const stored = await this.store.list().catch(() => []);
|
|
15404
|
+
for (const rec of stored) {
|
|
15405
|
+
existing.add(`${rec.agentId}::${rec.upstreamSessionId}`);
|
|
15406
|
+
}
|
|
15407
|
+
const synced = [];
|
|
15408
|
+
let skipped = 0;
|
|
15409
|
+
for (const entry of entries) {
|
|
15410
|
+
const dedupeKey = `${agentId}::${entry.sessionId}`;
|
|
15411
|
+
if (existing.has(dedupeKey)) {
|
|
15412
|
+
skipped += 1;
|
|
15413
|
+
continue;
|
|
15414
|
+
}
|
|
15415
|
+
existing.add(dedupeKey);
|
|
15416
|
+
const newId = `${HYDRA_SESSION_PREFIX}${generateRawSessionId()}`;
|
|
15417
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
15418
|
+
const ts = entry.updatedAt ?? now;
|
|
15419
|
+
const recordArgs = {
|
|
15420
|
+
sessionId: newId,
|
|
15421
|
+
lineageId: generateLineageId(),
|
|
15422
|
+
upstreamSessionId: entry.sessionId,
|
|
15423
|
+
agentId,
|
|
15424
|
+
cwd: entry.cwd,
|
|
15425
|
+
pendingHistorySync: true,
|
|
15426
|
+
createdAt: ts,
|
|
15427
|
+
updatedAt: ts
|
|
15428
|
+
};
|
|
15429
|
+
if (entry.title !== void 0) {
|
|
15430
|
+
recordArgs.title = entry.title;
|
|
15431
|
+
}
|
|
15432
|
+
const record = recordFromMemorySession(recordArgs);
|
|
15433
|
+
await this.store.write(record);
|
|
15434
|
+
synced.push({ version: 1, ...record });
|
|
15435
|
+
}
|
|
15436
|
+
return { synced, skipped };
|
|
15437
|
+
}
|
|
15438
|
+
// Paginate the agent's session/list, threading nextCursor until the
|
|
15439
|
+
// agent stops returning one. Each entry the spec guarantees has
|
|
15440
|
+
// { sessionId, cwd }; title and updatedAt are optional.
|
|
15441
|
+
async collectAgentSessions(agent) {
|
|
15442
|
+
const out = [];
|
|
15443
|
+
let cursor;
|
|
15444
|
+
for (let page = 0; page < 100; page += 1) {
|
|
15445
|
+
const params = {};
|
|
15446
|
+
if (cursor !== void 0) {
|
|
15447
|
+
params.cursor = cursor;
|
|
15448
|
+
}
|
|
15449
|
+
const result = await agent.connection.request("session/list", params);
|
|
15450
|
+
const rows = Array.isArray(result.sessions) ? result.sessions : [];
|
|
15451
|
+
for (const row of rows) {
|
|
15452
|
+
if (typeof row.sessionId !== "string" || typeof row.cwd !== "string") {
|
|
15453
|
+
continue;
|
|
15454
|
+
}
|
|
15455
|
+
const entry = { sessionId: row.sessionId, cwd: row.cwd };
|
|
15456
|
+
if (typeof row.title === "string") {
|
|
15457
|
+
entry.title = row.title;
|
|
15458
|
+
}
|
|
15459
|
+
if (typeof row.updatedAt === "string") {
|
|
15460
|
+
entry.updatedAt = row.updatedAt;
|
|
15461
|
+
}
|
|
15462
|
+
out.push(entry);
|
|
15463
|
+
}
|
|
15464
|
+
if (typeof result.nextCursor !== "string" || result.nextCursor.length === 0) {
|
|
15465
|
+
break;
|
|
15466
|
+
}
|
|
15467
|
+
cursor = result.nextCursor;
|
|
15468
|
+
}
|
|
15469
|
+
return out;
|
|
15470
|
+
}
|
|
15211
15471
|
// Bootstrap a fresh agent process: registry resolve → spawn → initialize
|
|
15212
15472
|
// → session/new. Shared by create() and the /hydra agent path so both
|
|
15213
15473
|
// go through the same env / capabilities / error-handling.
|
|
@@ -15400,9 +15660,21 @@ var SessionManager = class {
|
|
|
15400
15660
|
agentCommands: record.agentCommands,
|
|
15401
15661
|
agentModes: record.agentModes,
|
|
15402
15662
|
agentModels: record.agentModels,
|
|
15403
|
-
createdAt: record.createdAt
|
|
15663
|
+
createdAt: record.createdAt,
|
|
15664
|
+
pendingHistorySync: record.pendingHistorySync
|
|
15404
15665
|
};
|
|
15405
15666
|
}
|
|
15667
|
+
async clearPendingHistorySync(sessionId) {
|
|
15668
|
+
await this.enqueueMetaWrite(sessionId, async () => {
|
|
15669
|
+
const record = await this.store.read(sessionId);
|
|
15670
|
+
if (!record || record.pendingHistorySync !== true) {
|
|
15671
|
+
return;
|
|
15672
|
+
}
|
|
15673
|
+
const next = { ...record };
|
|
15674
|
+
delete next.pendingHistorySync;
|
|
15675
|
+
await this.store.write(next);
|
|
15676
|
+
});
|
|
15677
|
+
}
|
|
15406
15678
|
// Best-effort: peek at the persisted history's first prompt and use
|
|
15407
15679
|
// its first line (capped to 200 chars) as a session title. Returns
|
|
15408
15680
|
// undefined if no usable prompt is found or any I/O fails.
|
|
@@ -15488,7 +15760,8 @@ var SessionManager = class {
|
|
|
15488
15760
|
currentUsage: session.currentUsage,
|
|
15489
15761
|
updatedAt: used,
|
|
15490
15762
|
attachedClients: session.attachedCount,
|
|
15491
|
-
status: "live"
|
|
15763
|
+
status: "live",
|
|
15764
|
+
busy: session.turnStartedAt !== void 0
|
|
15492
15765
|
});
|
|
15493
15766
|
}
|
|
15494
15767
|
const records = await this.store.list().catch(() => []);
|
|
@@ -15512,7 +15785,8 @@ var SessionManager = class {
|
|
|
15512
15785
|
importedFromUpstreamSessionId: r.importedFromUpstreamSessionId,
|
|
15513
15786
|
updatedAt: used,
|
|
15514
15787
|
attachedClients: 0,
|
|
15515
|
-
status: "cold"
|
|
15788
|
+
status: "cold",
|
|
15789
|
+
busy: false
|
|
15516
15790
|
});
|
|
15517
15791
|
}
|
|
15518
15792
|
entries.sort((a, b) => a.updatedAt < b.updatedAt ? 1 : -1);
|
|
@@ -17451,7 +17725,8 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
17451
17725
|
}
|
|
17452
17726
|
|
|
17453
17727
|
// src/daemon/routes/agents.ts
|
|
17454
|
-
|
|
17728
|
+
init_types();
|
|
17729
|
+
function registerAgentRoutes(app, registry, manager, opts = {}) {
|
|
17455
17730
|
app.get("/v1/agents", async () => {
|
|
17456
17731
|
const doc = await registry.load();
|
|
17457
17732
|
return {
|
|
@@ -17472,6 +17747,61 @@ function registerAgentRoutes(app, registry) {
|
|
|
17472
17747
|
const doc = await registry.refresh();
|
|
17473
17748
|
return { version: doc.version, agentCount: doc.agents.length };
|
|
17474
17749
|
});
|
|
17750
|
+
app.post("/v1/agents/:id/install", async (request, reply) => {
|
|
17751
|
+
const id = request.params.id;
|
|
17752
|
+
const agent = await registry.getAgent(id);
|
|
17753
|
+
if (!agent) {
|
|
17754
|
+
reply.code(404).send({ error: `agent ${id} not found in registry` });
|
|
17755
|
+
return;
|
|
17756
|
+
}
|
|
17757
|
+
if (agent.distribution.uvx && !agent.distribution.npx && !agent.distribution.binary) {
|
|
17758
|
+
reply.send({
|
|
17759
|
+
agentId: agent.id,
|
|
17760
|
+
version: agent.version ?? "current",
|
|
17761
|
+
distribution: "uvx",
|
|
17762
|
+
installed: false,
|
|
17763
|
+
message: "uvx agents resolve on first run; nothing to pre-install."
|
|
17764
|
+
});
|
|
17765
|
+
return;
|
|
17766
|
+
}
|
|
17767
|
+
try {
|
|
17768
|
+
const plan = await planSpawn(agent, [], { npmRegistry: opts.npmRegistry });
|
|
17769
|
+
const distribution = agent.distribution.npx ? "npx" : agent.distribution.binary ? "binary" : "unknown";
|
|
17770
|
+
reply.send({
|
|
17771
|
+
agentId: agent.id,
|
|
17772
|
+
version: plan.version,
|
|
17773
|
+
distribution,
|
|
17774
|
+
installed: true,
|
|
17775
|
+
command: plan.command
|
|
17776
|
+
});
|
|
17777
|
+
} catch (err) {
|
|
17778
|
+
reply.code(500).send({ error: err.message });
|
|
17779
|
+
}
|
|
17780
|
+
});
|
|
17781
|
+
app.post("/v1/agents/:id/sync", async (request, reply) => {
|
|
17782
|
+
const agentId = request.params.id;
|
|
17783
|
+
try {
|
|
17784
|
+
const { synced, skipped } = await manager.syncFromAgent(agentId);
|
|
17785
|
+
return {
|
|
17786
|
+
synced: synced.map((r) => ({
|
|
17787
|
+
sessionId: r.sessionId,
|
|
17788
|
+
upstreamSessionId: r.upstreamSessionId,
|
|
17789
|
+
agentId: r.agentId,
|
|
17790
|
+
cwd: r.cwd,
|
|
17791
|
+
title: r.title,
|
|
17792
|
+
updatedAt: r.updatedAt
|
|
17793
|
+
})),
|
|
17794
|
+
skipped
|
|
17795
|
+
};
|
|
17796
|
+
} catch (err) {
|
|
17797
|
+
const e = err;
|
|
17798
|
+
if (e.code === JsonRpcErrorCodes.AgentNotInstalled) {
|
|
17799
|
+
reply.code(404).send({ error: e.message });
|
|
17800
|
+
return;
|
|
17801
|
+
}
|
|
17802
|
+
reply.code(409).send({ error: e.message });
|
|
17803
|
+
}
|
|
17804
|
+
});
|
|
17475
17805
|
}
|
|
17476
17806
|
|
|
17477
17807
|
// src/daemon/routes/health.ts
|
|
@@ -18542,7 +18872,7 @@ async function startDaemon(config, serviceToken) {
|
|
|
18542
18872
|
agentId: config.defaultAgent,
|
|
18543
18873
|
cwd: config.defaultCwd
|
|
18544
18874
|
});
|
|
18545
|
-
registerAgentRoutes(app, registry);
|
|
18875
|
+
registerAgentRoutes(app, registry, manager, { npmRegistry: config.npmRegistry });
|
|
18546
18876
|
registerExtensionRoutes(app, extensions);
|
|
18547
18877
|
registerConfigRoutes(app, {
|
|
18548
18878
|
defaultAgent: config.defaultAgent,
|
|
@@ -19886,6 +20216,136 @@ async function runAgentsList() {
|
|
|
19886
20216
|
Registry version: ${body.version}
|
|
19887
20217
|
`);
|
|
19888
20218
|
}
|
|
20219
|
+
async function runAgentsInstall(agentId) {
|
|
20220
|
+
if (!agentId) {
|
|
20221
|
+
process.stderr.write("Usage: hydra-acp agent install <agent-id>\n");
|
|
20222
|
+
process.exit(2);
|
|
20223
|
+
return;
|
|
20224
|
+
}
|
|
20225
|
+
const config = await loadConfig();
|
|
20226
|
+
const serviceToken = await loadServiceToken();
|
|
20227
|
+
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
20228
|
+
process.stdout.write(`Installing ${agentId}\u2026
|
|
20229
|
+
`);
|
|
20230
|
+
let body;
|
|
20231
|
+
try {
|
|
20232
|
+
const r = await fetch(
|
|
20233
|
+
`${baseUrl}/v1/agents/${encodeURIComponent(agentId)}/install`,
|
|
20234
|
+
{
|
|
20235
|
+
method: "POST",
|
|
20236
|
+
headers: { Authorization: `Bearer ${serviceToken}` }
|
|
20237
|
+
}
|
|
20238
|
+
);
|
|
20239
|
+
if (!r.ok) {
|
|
20240
|
+
let detail = `HTTP ${r.status}`;
|
|
20241
|
+
try {
|
|
20242
|
+
const j = await r.json();
|
|
20243
|
+
if (j.error) {
|
|
20244
|
+
detail = j.error;
|
|
20245
|
+
}
|
|
20246
|
+
} catch {
|
|
20247
|
+
}
|
|
20248
|
+
process.stderr.write(`hydra agent install ${agentId}: ${detail}
|
|
20249
|
+
`);
|
|
20250
|
+
process.exit(1);
|
|
20251
|
+
}
|
|
20252
|
+
body = await r.json();
|
|
20253
|
+
} catch (err) {
|
|
20254
|
+
process.stderr.write(
|
|
20255
|
+
`Could not reach daemon at ${baseUrl}: ${err.message}
|
|
20256
|
+
`
|
|
20257
|
+
);
|
|
20258
|
+
process.exit(1);
|
|
20259
|
+
return;
|
|
20260
|
+
}
|
|
20261
|
+
if (!body.installed) {
|
|
20262
|
+
process.stdout.write(
|
|
20263
|
+
`${body.agentId} (${body.version}, ${body.distribution}): ${body.message ?? "nothing to install"}
|
|
20264
|
+
`
|
|
20265
|
+
);
|
|
20266
|
+
return;
|
|
20267
|
+
}
|
|
20268
|
+
process.stdout.write(
|
|
20269
|
+
`Installed ${body.agentId} (${body.version}, ${body.distribution})
|
|
20270
|
+
`
|
|
20271
|
+
);
|
|
20272
|
+
if (body.command) {
|
|
20273
|
+
process.stdout.write(` \u2192 ${body.command}
|
|
20274
|
+
`);
|
|
20275
|
+
}
|
|
20276
|
+
}
|
|
20277
|
+
async function runAgentsSync(agentId) {
|
|
20278
|
+
if (!agentId) {
|
|
20279
|
+
process.stderr.write("Usage: hydra-acp agent sync <agent-id>\n");
|
|
20280
|
+
process.exit(2);
|
|
20281
|
+
return;
|
|
20282
|
+
}
|
|
20283
|
+
const config = await loadConfig();
|
|
20284
|
+
const serviceToken = await loadServiceToken();
|
|
20285
|
+
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
20286
|
+
let body;
|
|
20287
|
+
try {
|
|
20288
|
+
const r = await fetch(`${baseUrl}/v1/agents/${encodeURIComponent(agentId)}/sync`, {
|
|
20289
|
+
method: "POST",
|
|
20290
|
+
headers: { Authorization: `Bearer ${serviceToken}` }
|
|
20291
|
+
});
|
|
20292
|
+
if (!r.ok) {
|
|
20293
|
+
let detail = `HTTP ${r.status}`;
|
|
20294
|
+
try {
|
|
20295
|
+
const j = await r.json();
|
|
20296
|
+
if (j.error) {
|
|
20297
|
+
detail = j.error;
|
|
20298
|
+
}
|
|
20299
|
+
} catch {
|
|
20300
|
+
}
|
|
20301
|
+
process.stderr.write(`hydra agent sync ${agentId}: ${detail}
|
|
20302
|
+
`);
|
|
20303
|
+
process.exit(1);
|
|
20304
|
+
}
|
|
20305
|
+
body = await r.json();
|
|
20306
|
+
} catch (err) {
|
|
20307
|
+
process.stderr.write(
|
|
20308
|
+
`Could not reach daemon at ${baseUrl}: ${err.message}
|
|
20309
|
+
`
|
|
20310
|
+
);
|
|
20311
|
+
process.exit(1);
|
|
20312
|
+
return;
|
|
20313
|
+
}
|
|
20314
|
+
if (body.synced.length === 0) {
|
|
20315
|
+
process.stdout.write(
|
|
20316
|
+
`Nothing new to sync (${body.skipped} already tracked).
|
|
20317
|
+
`
|
|
20318
|
+
);
|
|
20319
|
+
return;
|
|
20320
|
+
}
|
|
20321
|
+
const rows = body.synced.map((s) => ({
|
|
20322
|
+
id: s.sessionId,
|
|
20323
|
+
upstream: s.upstreamSessionId,
|
|
20324
|
+
cwd: s.cwd,
|
|
20325
|
+
title: s.title ?? "-"
|
|
20326
|
+
}));
|
|
20327
|
+
const header = { id: "ID", upstream: "UPSTREAM", cwd: "CWD", title: "TITLE" };
|
|
20328
|
+
const widths = {
|
|
20329
|
+
id: maxLen3(header.id, rows.map((r) => r.id)),
|
|
20330
|
+
upstream: maxLen3(header.upstream, rows.map((r) => r.upstream)),
|
|
20331
|
+
cwd: maxLen3(header.cwd, rows.map((r) => r.cwd))
|
|
20332
|
+
};
|
|
20333
|
+
const fmt = (r) => [
|
|
20334
|
+
r.id.padEnd(widths.id),
|
|
20335
|
+
r.upstream.padEnd(widths.upstream),
|
|
20336
|
+
r.cwd.padEnd(widths.cwd),
|
|
20337
|
+
r.title
|
|
20338
|
+
].join(" ");
|
|
20339
|
+
process.stdout.write(fmt(header) + "\n");
|
|
20340
|
+
for (const r of rows) {
|
|
20341
|
+
process.stdout.write(fmt(r) + "\n");
|
|
20342
|
+
}
|
|
20343
|
+
process.stdout.write(
|
|
20344
|
+
`
|
|
20345
|
+
Synced ${body.synced.length} session(s); skipped ${body.skipped} already tracked.
|
|
20346
|
+
`
|
|
20347
|
+
);
|
|
20348
|
+
}
|
|
19889
20349
|
async function runAgentsRefresh() {
|
|
19890
20350
|
const config = await loadConfig();
|
|
19891
20351
|
const serviceToken = await loadServiceToken();
|
|
@@ -20942,7 +21402,11 @@ async function main() {
|
|
|
20942
21402
|
const daemonIdx = argv.indexOf("daemon");
|
|
20943
21403
|
const tail = argv.slice(daemonIdx + 1);
|
|
20944
21404
|
const sub = tail[0];
|
|
20945
|
-
if (sub ===
|
|
21405
|
+
if (sub === void 0 || sub === "status") {
|
|
21406
|
+
await runDaemonStatus();
|
|
21407
|
+
return;
|
|
21408
|
+
}
|
|
21409
|
+
if (sub === "start") {
|
|
20946
21410
|
await runDaemonStart(flags);
|
|
20947
21411
|
return;
|
|
20948
21412
|
}
|
|
@@ -20954,10 +21418,6 @@ async function main() {
|
|
|
20954
21418
|
await runDaemonRestart();
|
|
20955
21419
|
return;
|
|
20956
21420
|
}
|
|
20957
|
-
if (sub === "status") {
|
|
20958
|
-
await runDaemonStatus();
|
|
20959
|
-
return;
|
|
20960
|
-
}
|
|
20961
21421
|
if (sub === "logs") {
|
|
20962
21422
|
await runDaemonLogs(tail.slice(1));
|
|
20963
21423
|
return;
|
|
@@ -21070,6 +21530,14 @@ async function main() {
|
|
|
21070
21530
|
await runAgentsRefresh();
|
|
21071
21531
|
return;
|
|
21072
21532
|
}
|
|
21533
|
+
if (sub === "install") {
|
|
21534
|
+
await runAgentsInstall(positional[2]);
|
|
21535
|
+
return;
|
|
21536
|
+
}
|
|
21537
|
+
if (sub === "sync") {
|
|
21538
|
+
await runAgentsSync(positional[2]);
|
|
21539
|
+
return;
|
|
21540
|
+
}
|
|
21073
21541
|
process.stderr.write(`Unknown agent subcommand: ${sub}
|
|
21074
21542
|
`);
|
|
21075
21543
|
process.exit(2);
|
|
@@ -21226,8 +21694,9 @@ function printHelp() {
|
|
|
21226
21694
|
" --readonly Open a session as a transcript viewer (requires --session).",
|
|
21227
21695
|
" HYDRA_ACP_SESSION Env var equivalent of --session (flag wins).",
|
|
21228
21696
|
" hydra-acp init [--rotate-token] Initialize ~/.hydra-acp/config.json",
|
|
21697
|
+
" hydra-acp daemon [status] Show daemon pid/version (default when no subcommand)",
|
|
21229
21698
|
" hydra-acp daemon start [--foreground] Start daemon (detached by default; --foreground to attach)",
|
|
21230
|
-
" hydra-acp daemon stop|restart
|
|
21699
|
+
" hydra-acp daemon stop|restart",
|
|
21231
21700
|
" hydra-acp daemon logs [-f] [-n N] Tail or follow the daemon log",
|
|
21232
21701
|
" hydra-acp session [list] [--all] [--json] [--host=<host>]",
|
|
21233
21702
|
" List sessions (live + 20 most-recent cold; --all for everything; --json emits JSON for scripts).",
|
|
@@ -21249,6 +21718,8 @@ function printHelp() {
|
|
|
21249
21718
|
" hydra-acp extension logs <name> [-f] [-n N] Tail or follow an extension's log",
|
|
21250
21719
|
" hydra-acp agent [list] List agents in the cached registry",
|
|
21251
21720
|
" hydra-acp agent refresh Force a registry re-fetch",
|
|
21721
|
+
" hydra-acp agent install <id> Pre-install <id> from the registry (else lazy on first session)",
|
|
21722
|
+
" 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
21723
|
" hydra-acp auth password [--force] Set the daemon's master password",
|
|
21253
21724
|
" hydra-acp auth [list] List active session tokens",
|
|
21254
21725
|
" hydra-acp auth revoke <id> Revoke a session token",
|