@hydra-acp/cli 0.1.7 → 0.1.8
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 +1 -1
- package/dist/cli.js +1701 -483
- package/dist/index.d.ts +59 -4
- package/dist/index.js +347 -75
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/daemon/server.ts
|
|
2
|
-
import * as
|
|
3
|
-
import * as
|
|
2
|
+
import * as fs10 from "fs";
|
|
3
|
+
import * as fsp4 from "fs/promises";
|
|
4
4
|
import Fastify from "fastify";
|
|
5
5
|
import websocketPlugin from "@fastify/websocket";
|
|
6
6
|
import pino from "pino";
|
|
@@ -43,6 +43,18 @@ var paths = {
|
|
|
43
43
|
// machine's binaries cleanly separated. `ls agents/` immediately
|
|
44
44
|
// shows which platforms have ever installed anything.
|
|
45
45
|
agentInstallDir: (id, platformKey, version) => path.join(hydraHome(), "agents", platformKey, id, version),
|
|
46
|
+
// npm install cache for npx-distributed agents. The trailing
|
|
47
|
+
// node<ABI> segment keys on process.versions.modules so a Node
|
|
48
|
+
// major bump (different ABI → native modules incompatible) yields
|
|
49
|
+
// a fresh install rather than failing at require() time.
|
|
50
|
+
agentNpmInstallDir: (id, platformKey, version) => path.join(
|
|
51
|
+
hydraHome(),
|
|
52
|
+
"agents",
|
|
53
|
+
platformKey,
|
|
54
|
+
id,
|
|
55
|
+
version,
|
|
56
|
+
`node${process.versions.modules}`
|
|
57
|
+
),
|
|
46
58
|
sessionsDir: () => path.join(hydraHome(), "sessions"),
|
|
47
59
|
// One directory per session id under sessions/. Co-locates the
|
|
48
60
|
// session record, its transcript, and any future per-session state
|
|
@@ -69,7 +81,16 @@ var DaemonConfig = z.object({
|
|
|
69
81
|
authToken: z.string().min(16),
|
|
70
82
|
logLevel: z.enum(["debug", "info", "warn", "error"]).default("info"),
|
|
71
83
|
tls: TlsConfig.optional(),
|
|
72
|
-
sessionIdleTimeoutSeconds: z.number().int().nonnegative().default(3600)
|
|
84
|
+
sessionIdleTimeoutSeconds: z.number().int().nonnegative().default(3600),
|
|
85
|
+
// Cap on entries kept in a session's on-disk replay log (history.jsonl).
|
|
86
|
+
// Compaction trims to this many on a periodic basis; reads also slice
|
|
87
|
+
// to the tail at this length as a defensive measure against older
|
|
88
|
+
// daemons that may have written unbounded files.
|
|
89
|
+
sessionHistoryMaxEntries: z.number().int().positive().default(1e3),
|
|
90
|
+
// Bytes of trailing agent stderr buffered per AgentInstance so the
|
|
91
|
+
// daemon can include it in the diagnostic message when a spawn fails.
|
|
92
|
+
// Bump if your agents emit large tracebacks you want surfaced.
|
|
93
|
+
agentStderrTailBytes: z.number().int().positive().default(4096)
|
|
73
94
|
});
|
|
74
95
|
var RegistryConfig = z.object({
|
|
75
96
|
url: z.string().url().default(REGISTRY_URL_DEFAULT),
|
|
@@ -92,7 +113,14 @@ var TuiConfig = z.object({
|
|
|
92
113
|
// text selection requires shift+drag to bypass mouse reporting. Set
|
|
93
114
|
// false to disable capture — wheel scrollback stops working, but
|
|
94
115
|
// plain click-drag selects text via the terminal emulator.
|
|
95
|
-
mouse: z.boolean().default(true)
|
|
116
|
+
mouse: z.boolean().default(true),
|
|
117
|
+
// Size at which the TUI's session/update debug log (tui.log) rotates
|
|
118
|
+
// to tui.log.0 and resets. Bounds on-disk use at ~2x this value.
|
|
119
|
+
logMaxBytes: z.number().int().positive().default(5 * 1024 * 1024),
|
|
120
|
+
// Width cap on the cwd column in the `sessions list` output and the
|
|
121
|
+
// TUI picker. Set higher if you keep deeply-nested working directories
|
|
122
|
+
// and want them visible; the elastic title column shrinks to make room.
|
|
123
|
+
cwdColumnMaxWidth: z.number().int().positive().default(24)
|
|
96
124
|
});
|
|
97
125
|
var ExtensionName = z.string().min(1).regex(/^[A-Za-z0-9._-]+$/, "extension name must be filename-safe");
|
|
98
126
|
var ExtensionBody = z.object({
|
|
@@ -128,9 +156,14 @@ var HydraConfig = z.object({
|
|
|
128
156
|
tui: TuiConfig.default({
|
|
129
157
|
repaintThrottleMs: 1e3,
|
|
130
158
|
maxScrollbackLines: 1e4,
|
|
131
|
-
mouse: true
|
|
159
|
+
mouse: true,
|
|
160
|
+
logMaxBytes: 5 * 1024 * 1024,
|
|
161
|
+
cwdColumnMaxWidth: 24
|
|
132
162
|
})
|
|
133
163
|
});
|
|
164
|
+
var HydraConfigReadOnly = HydraConfig.extend({
|
|
165
|
+
daemon: DaemonConfig.omit({ authToken: true })
|
|
166
|
+
});
|
|
134
167
|
function extensionList(config) {
|
|
135
168
|
return Object.entries(config.extensions).map(([name, body]) => ({
|
|
136
169
|
name,
|
|
@@ -472,9 +505,129 @@ async function fileExists(p) {
|
|
|
472
505
|
}
|
|
473
506
|
}
|
|
474
507
|
|
|
508
|
+
// src/core/npm-install.ts
|
|
509
|
+
import * as fsp2 from "fs/promises";
|
|
510
|
+
import * as path3 from "path";
|
|
511
|
+
import { spawn as spawn2 } from "child_process";
|
|
512
|
+
var logSink2 = (msg) => {
|
|
513
|
+
process.stderr.write(msg + "\n");
|
|
514
|
+
};
|
|
515
|
+
function setNpmInstallLogger(log) {
|
|
516
|
+
logSink2 = log ?? ((msg) => process.stderr.write(msg + "\n"));
|
|
517
|
+
}
|
|
518
|
+
async function ensureNpmPackage(args) {
|
|
519
|
+
const platformKey = currentPlatformKey();
|
|
520
|
+
if (!platformKey) {
|
|
521
|
+
throw new Error(
|
|
522
|
+
`Agent ${args.agentId}: cannot determine platform key for ${process.platform}/${process.arch}`
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
const installDir = paths.agentNpmInstallDir(
|
|
526
|
+
args.agentId,
|
|
527
|
+
platformKey,
|
|
528
|
+
args.version
|
|
529
|
+
);
|
|
530
|
+
const binPath = path3.join(installDir, "node_modules", ".bin", args.bin);
|
|
531
|
+
if (await fileExists2(binPath)) {
|
|
532
|
+
return binPath;
|
|
533
|
+
}
|
|
534
|
+
await installInto({
|
|
535
|
+
agentId: args.agentId,
|
|
536
|
+
packageSpec: args.packageSpec,
|
|
537
|
+
installDir
|
|
538
|
+
});
|
|
539
|
+
if (!await fileExists2(binPath)) {
|
|
540
|
+
throw new Error(
|
|
541
|
+
`Agent ${args.agentId}: npm install of ${args.packageSpec} did not produce bin ${args.bin} (looked in ${installDir}/node_modules/.bin/)`
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
return binPath;
|
|
545
|
+
}
|
|
546
|
+
async function installInto(args) {
|
|
547
|
+
await fsp2.mkdir(path3.dirname(args.installDir), { recursive: true });
|
|
548
|
+
const tempDir = await fsp2.mkdtemp(`${args.installDir}.partial-`);
|
|
549
|
+
try {
|
|
550
|
+
logSink2(
|
|
551
|
+
`hydra-acp: installing ${args.packageSpec} for ${args.agentId} into ${tempDir}`
|
|
552
|
+
);
|
|
553
|
+
await runNpmInstall({
|
|
554
|
+
packageSpec: args.packageSpec,
|
|
555
|
+
cwd: tempDir
|
|
556
|
+
});
|
|
557
|
+
try {
|
|
558
|
+
await fsp2.rename(tempDir, args.installDir);
|
|
559
|
+
} catch (err) {
|
|
560
|
+
const e = err;
|
|
561
|
+
if ((e.code === "EEXIST" || e.code === "ENOTEMPTY") && await fileExists2(args.installDir)) {
|
|
562
|
+
await fsp2.rm(tempDir, { recursive: true, force: true }).catch(
|
|
563
|
+
() => void 0
|
|
564
|
+
);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
throw err;
|
|
568
|
+
}
|
|
569
|
+
logSink2(`hydra-acp: installed ${args.agentId} to ${args.installDir}`);
|
|
570
|
+
} catch (err) {
|
|
571
|
+
await fsp2.rm(tempDir, { recursive: true, force: true }).catch(
|
|
572
|
+
() => void 0
|
|
573
|
+
);
|
|
574
|
+
throw err;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
function runNpmInstall(args) {
|
|
578
|
+
return new Promise((resolve3, reject) => {
|
|
579
|
+
const child = spawn2(
|
|
580
|
+
"npm",
|
|
581
|
+
["install", "--no-audit", "--no-fund", "--silent", args.packageSpec],
|
|
582
|
+
{
|
|
583
|
+
cwd: args.cwd,
|
|
584
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
585
|
+
}
|
|
586
|
+
);
|
|
587
|
+
let stderrTail = "";
|
|
588
|
+
child.stdout?.on("data", (chunk) => {
|
|
589
|
+
void chunk;
|
|
590
|
+
});
|
|
591
|
+
child.stderr?.setEncoding("utf8");
|
|
592
|
+
child.stderr?.on("data", (chunk) => {
|
|
593
|
+
stderrTail = (stderrTail + chunk).slice(-4096);
|
|
594
|
+
});
|
|
595
|
+
child.on("error", (err) => {
|
|
596
|
+
const msg = err.code === "ENOENT" ? `npm not found on PATH (install Node.js / npm, or use a binary-distributed agent)` : err.message;
|
|
597
|
+
reject(new Error(msg));
|
|
598
|
+
});
|
|
599
|
+
child.on("exit", (code, signal) => {
|
|
600
|
+
if (code === 0) {
|
|
601
|
+
resolve3();
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
const reason = code !== null ? `exit code ${code}` : `signal ${signal ?? "unknown"}`;
|
|
605
|
+
const tail = stderrTail.trim();
|
|
606
|
+
reject(
|
|
607
|
+
new Error(
|
|
608
|
+
tail ? `npm install ${args.packageSpec} failed (${reason})
|
|
609
|
+
stderr: ${tail}` : `npm install ${args.packageSpec} failed (${reason})`
|
|
610
|
+
)
|
|
611
|
+
);
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
async function fileExists2(p) {
|
|
616
|
+
try {
|
|
617
|
+
await fsp2.access(p);
|
|
618
|
+
return true;
|
|
619
|
+
} catch {
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
475
624
|
// src/core/registry.ts
|
|
476
625
|
var NpxDistribution = z2.object({
|
|
477
626
|
package: z2.string(),
|
|
627
|
+
// The bin to invoke after install. Defaults to the package basename
|
|
628
|
+
// (e.g. "claude-code" for "@anthropic-ai/claude-code"). Required when
|
|
629
|
+
// the package exposes a bin name that differs from its basename.
|
|
630
|
+
bin: z2.string().optional(),
|
|
478
631
|
args: z2.array(z2.string()).optional(),
|
|
479
632
|
env: z2.record(z2.string()).optional()
|
|
480
633
|
});
|
|
@@ -638,9 +791,23 @@ async function planSpawn(agent, callerArgs = []) {
|
|
|
638
791
|
if (agent.distribution.npx) {
|
|
639
792
|
const npx = agent.distribution.npx;
|
|
640
793
|
const tail = callerArgs.length > 0 ? callerArgs : npx.args ?? [];
|
|
794
|
+
if (process.env.HYDRA_ACP_SKIP_NPM_PREFETCH) {
|
|
795
|
+
return {
|
|
796
|
+
command: "npx",
|
|
797
|
+
args: ["-y", npx.package, ...tail],
|
|
798
|
+
env: npx.env ?? {}
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
const bin = npx.bin ?? npxPackageBasename(agent) ?? npx.package;
|
|
802
|
+
const binPath = await ensureNpmPackage({
|
|
803
|
+
agentId: agent.id,
|
|
804
|
+
version: agent.version ?? "current",
|
|
805
|
+
packageSpec: npx.package,
|
|
806
|
+
bin
|
|
807
|
+
});
|
|
641
808
|
return {
|
|
642
|
-
command:
|
|
643
|
-
args:
|
|
809
|
+
command: binPath,
|
|
810
|
+
args: tail,
|
|
644
811
|
env: npx.env ?? {}
|
|
645
812
|
};
|
|
646
813
|
}
|
|
@@ -675,15 +842,12 @@ async function planSpawn(agent, callerArgs = []) {
|
|
|
675
842
|
throw new Error(`Agent ${agent.id} has no usable distribution method.`);
|
|
676
843
|
}
|
|
677
844
|
|
|
678
|
-
// src/core/session-manager.ts
|
|
679
|
-
import * as fs7 from "fs/promises";
|
|
680
|
-
import { customAlphabet as customAlphabet3 } from "nanoid";
|
|
681
|
-
|
|
682
845
|
// src/core/agent-instance.ts
|
|
683
|
-
import { spawn as
|
|
846
|
+
import { spawn as spawn3 } from "child_process";
|
|
684
847
|
|
|
685
848
|
// src/acp/types.ts
|
|
686
849
|
import { z as z3 } from "zod";
|
|
850
|
+
var ACP_PROTOCOL_VERSION = 1;
|
|
687
851
|
var JsonRpcErrorCodes = {
|
|
688
852
|
ParseError: -32700,
|
|
689
853
|
InvalidRequest: -32600,
|
|
@@ -1013,6 +1177,13 @@ var JsonRpcConnection = class _JsonRpcConnection {
|
|
|
1013
1177
|
}
|
|
1014
1178
|
await this.stream.close();
|
|
1015
1179
|
}
|
|
1180
|
+
// Force-close with an error. Rejects all pending requests and fires
|
|
1181
|
+
// close handlers carrying `err`. Used by transports that detect a
|
|
1182
|
+
// failure (e.g. child process crash, spawn ENOENT) the stream itself
|
|
1183
|
+
// can't surface as a stdout/stdin error.
|
|
1184
|
+
fail(err) {
|
|
1185
|
+
this.handleClose(err);
|
|
1186
|
+
}
|
|
1016
1187
|
handleIncoming(message) {
|
|
1017
1188
|
if ("method" in message) {
|
|
1018
1189
|
if ("id" in message && message.id !== void 0) {
|
|
@@ -1105,17 +1276,22 @@ var JsonRpcConnection = class _JsonRpcConnection {
|
|
|
1105
1276
|
};
|
|
1106
1277
|
|
|
1107
1278
|
// src/core/agent-instance.ts
|
|
1279
|
+
var DEFAULT_STDERR_TAIL_BYTES = 4096;
|
|
1108
1280
|
var AgentInstance = class _AgentInstance {
|
|
1109
1281
|
agentId;
|
|
1110
1282
|
cwd;
|
|
1111
1283
|
connection;
|
|
1112
1284
|
child;
|
|
1113
1285
|
exited = false;
|
|
1286
|
+
killed = false;
|
|
1287
|
+
stderrTail = "";
|
|
1288
|
+
stderrTailBytes;
|
|
1114
1289
|
exitHandlers = [];
|
|
1115
1290
|
constructor(opts, child) {
|
|
1116
1291
|
this.agentId = opts.agentId;
|
|
1117
1292
|
this.cwd = opts.cwd;
|
|
1118
1293
|
this.child = child;
|
|
1294
|
+
this.stderrTailBytes = opts.stderrTailBytes ?? DEFAULT_STDERR_TAIL_BYTES;
|
|
1119
1295
|
if (!child.stdout || !child.stdin) {
|
|
1120
1296
|
throw new Error("agent subprocess missing stdio");
|
|
1121
1297
|
}
|
|
@@ -1123,22 +1299,36 @@ var AgentInstance = class _AgentInstance {
|
|
|
1123
1299
|
this.connection = new JsonRpcConnection(stream);
|
|
1124
1300
|
child.stderr?.setEncoding("utf8");
|
|
1125
1301
|
child.stderr?.on("data", (chunk) => {
|
|
1302
|
+
this.stderrTail = (this.stderrTail + chunk).slice(-this.stderrTailBytes);
|
|
1126
1303
|
process.stderr.write(`[${opts.agentId}] ${chunk}`);
|
|
1127
1304
|
});
|
|
1305
|
+
child.on("error", (err) => {
|
|
1306
|
+
const msg = this.formatFailure(err.message);
|
|
1307
|
+
this.connection.fail(new Error(msg));
|
|
1308
|
+
});
|
|
1128
1309
|
child.on("exit", (code, signal) => {
|
|
1129
1310
|
this.exited = true;
|
|
1311
|
+
if (!this.killed) {
|
|
1312
|
+
const reason = `agent ${opts.agentId} exited before responding (code=${code} signal=${signal})`;
|
|
1313
|
+
this.connection.fail(new Error(this.formatFailure(reason)));
|
|
1314
|
+
}
|
|
1130
1315
|
for (const handler of this.exitHandlers) {
|
|
1131
1316
|
handler(code, signal);
|
|
1132
1317
|
}
|
|
1133
1318
|
});
|
|
1134
1319
|
}
|
|
1320
|
+
formatFailure(reason) {
|
|
1321
|
+
const tail = this.stderrTail.trim();
|
|
1322
|
+
return tail ? `${reason}
|
|
1323
|
+
stderr: ${tail}` : reason;
|
|
1324
|
+
}
|
|
1135
1325
|
static spawn(opts) {
|
|
1136
1326
|
const env = {
|
|
1137
1327
|
...process.env,
|
|
1138
1328
|
...opts.plan.env,
|
|
1139
1329
|
...opts.extraEnv ?? {}
|
|
1140
1330
|
};
|
|
1141
|
-
const child =
|
|
1331
|
+
const child = spawn3(opts.plan.command, opts.plan.args, {
|
|
1142
1332
|
cwd: opts.cwd,
|
|
1143
1333
|
env,
|
|
1144
1334
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1155,11 +1345,17 @@ var AgentInstance = class _AgentInstance {
|
|
|
1155
1345
|
if (this.exited) {
|
|
1156
1346
|
return;
|
|
1157
1347
|
}
|
|
1348
|
+
this.killed = true;
|
|
1158
1349
|
await this.connection.close().catch(() => void 0);
|
|
1159
1350
|
this.child.kill(signal);
|
|
1160
1351
|
}
|
|
1161
1352
|
};
|
|
1162
1353
|
|
|
1354
|
+
// src/core/session-manager.ts
|
|
1355
|
+
import * as fs8 from "fs/promises";
|
|
1356
|
+
import * as os2 from "os";
|
|
1357
|
+
import { customAlphabet as customAlphabet3 } from "nanoid";
|
|
1358
|
+
|
|
1163
1359
|
// src/core/session.ts
|
|
1164
1360
|
import { customAlphabet } from "nanoid";
|
|
1165
1361
|
|
|
@@ -1189,8 +1385,7 @@ function hydraCommandsAsAdvertised() {
|
|
|
1189
1385
|
var HYDRA_ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
1190
1386
|
var generateHydraId = customAlphabet(HYDRA_ID_ALPHABET, 16);
|
|
1191
1387
|
var HYDRA_SESSION_PREFIX = "hydra_session_";
|
|
1192
|
-
var
|
|
1193
|
-
var COMPACT_EVERY = 200;
|
|
1388
|
+
var DEFAULT_HISTORY_MAX_ENTRIES = 1e3;
|
|
1194
1389
|
var Session = class {
|
|
1195
1390
|
sessionId;
|
|
1196
1391
|
cwd;
|
|
@@ -1232,11 +1427,13 @@ var Session = class {
|
|
|
1232
1427
|
// Bumped by broadcastPromptReceived, cleared by broadcastTurnComplete.
|
|
1233
1428
|
// Drives the mid-turn elapsed counter delivered to fresh attachers.
|
|
1234
1429
|
promptStartedAt;
|
|
1235
|
-
// Counts appends since the last compaction. When it hits
|
|
1430
|
+
// Counts appends since the last compaction. When it hits compactEvery
|
|
1236
1431
|
// we ask the history store to trim the file to the most recent
|
|
1237
|
-
//
|
|
1432
|
+
// historyMaxEntries. Keeps file growth bounded without per-append
|
|
1238
1433
|
// file-size checks.
|
|
1239
1434
|
appendCount = 0;
|
|
1435
|
+
historyMaxEntries;
|
|
1436
|
+
compactEvery;
|
|
1240
1437
|
// Permission requests that have been broadcast to one or more
|
|
1241
1438
|
// clients but have not yet resolved. Replayed to clients that
|
|
1242
1439
|
// attach mid-flight so a late joiner sees the prompt instead of an
|
|
@@ -1293,6 +1490,8 @@ var Session = class {
|
|
|
1293
1490
|
this.firstPromptSeeded = true;
|
|
1294
1491
|
}
|
|
1295
1492
|
this.historyStore = init.historyStore;
|
|
1493
|
+
this.historyMaxEntries = init.historyMaxEntries ?? DEFAULT_HISTORY_MAX_ENTRIES;
|
|
1494
|
+
this.compactEvery = Math.max(1, Math.floor(this.historyMaxEntries * 0.2));
|
|
1296
1495
|
this.updatedAt = Date.now();
|
|
1297
1496
|
this.createdAt = init.createdAt ?? this.updatedAt;
|
|
1298
1497
|
this.lastRecordedAt = this.updatedAt;
|
|
@@ -2131,9 +2330,9 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
2131
2330
|
if (this.historyStore) {
|
|
2132
2331
|
const store = this.historyStore;
|
|
2133
2332
|
void store.append(this.sessionId, entry).catch(() => void 0);
|
|
2134
|
-
if (this.appendCount >=
|
|
2333
|
+
if (this.appendCount >= this.compactEvery) {
|
|
2135
2334
|
this.appendCount = 0;
|
|
2136
|
-
void store.compact(this.sessionId,
|
|
2335
|
+
void store.compact(this.sessionId, this.historyMaxEntries).catch(
|
|
2137
2336
|
() => void 0
|
|
2138
2337
|
);
|
|
2139
2338
|
}
|
|
@@ -2335,7 +2534,7 @@ function firstLine(text, max) {
|
|
|
2335
2534
|
|
|
2336
2535
|
// src/core/session-store.ts
|
|
2337
2536
|
import * as fs4 from "fs/promises";
|
|
2338
|
-
import * as
|
|
2537
|
+
import * as path4 from "path";
|
|
2339
2538
|
import { customAlphabet as customAlphabet2 } from "nanoid";
|
|
2340
2539
|
import { z as z4 } from "zod";
|
|
2341
2540
|
var HYDRA_ID_ALPHABET2 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
@@ -2507,12 +2706,16 @@ function recordFromMemorySession(args) {
|
|
|
2507
2706
|
// src/core/history-store.ts
|
|
2508
2707
|
import * as fs5 from "fs/promises";
|
|
2509
2708
|
var SESSION_ID_PATTERN2 = /^[A-Za-z0-9_-]+$/;
|
|
2510
|
-
var
|
|
2709
|
+
var DEFAULT_MAX_ENTRIES = 1e3;
|
|
2511
2710
|
var HistoryStore = class {
|
|
2512
2711
|
// Serialize writes per session id so appends and rewrites don't
|
|
2513
2712
|
// interleave JSONL lines on disk. The chain swallows errors so one
|
|
2514
2713
|
// failed append doesn't poison every subsequent write.
|
|
2515
2714
|
writeQueues = /* @__PURE__ */ new Map();
|
|
2715
|
+
maxEntries;
|
|
2716
|
+
constructor(options = {}) {
|
|
2717
|
+
this.maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
2718
|
+
}
|
|
2516
2719
|
async append(sessionId, entry) {
|
|
2517
2720
|
if (!SESSION_ID_PATTERN2.test(sessionId)) {
|
|
2518
2721
|
return;
|
|
@@ -2614,8 +2817,8 @@ var HistoryStore = class {
|
|
|
2614
2817
|
recordedAt: obj.recordedAt
|
|
2615
2818
|
});
|
|
2616
2819
|
}
|
|
2617
|
-
if (out.length >
|
|
2618
|
-
return out.slice(-
|
|
2820
|
+
if (out.length > this.maxEntries) {
|
|
2821
|
+
return out.slice(-this.maxEntries);
|
|
2619
2822
|
}
|
|
2620
2823
|
return out;
|
|
2621
2824
|
}
|
|
@@ -2658,13 +2861,40 @@ var HistoryStore = class {
|
|
|
2658
2861
|
|
|
2659
2862
|
// src/tui/history.ts
|
|
2660
2863
|
import { promises as fs6 } from "fs";
|
|
2661
|
-
import * as
|
|
2864
|
+
import * as path5 from "path";
|
|
2662
2865
|
async function saveHistory(file, history) {
|
|
2663
|
-
await fs6.mkdir(
|
|
2866
|
+
await fs6.mkdir(path5.dirname(file), { recursive: true });
|
|
2664
2867
|
const lines = history.map((entry) => JSON.stringify(entry));
|
|
2665
2868
|
await fs6.writeFile(file, lines.length > 0 ? lines.join("\n") + "\n" : "");
|
|
2666
2869
|
}
|
|
2667
2870
|
|
|
2871
|
+
// src/core/hydra-version.ts
|
|
2872
|
+
import { fileURLToPath } from "url";
|
|
2873
|
+
import * as path6 from "path";
|
|
2874
|
+
import * as fs7 from "fs";
|
|
2875
|
+
function resolveVersion() {
|
|
2876
|
+
try {
|
|
2877
|
+
let dir = path6.dirname(fileURLToPath(import.meta.url));
|
|
2878
|
+
for (let i = 0; i < 8; i += 1) {
|
|
2879
|
+
const candidate = path6.join(dir, "package.json");
|
|
2880
|
+
if (fs7.existsSync(candidate)) {
|
|
2881
|
+
const pkg = JSON.parse(fs7.readFileSync(candidate, "utf8"));
|
|
2882
|
+
if (typeof pkg.version === "string" && pkg.version.length > 0 && (typeof pkg.name !== "string" || pkg.name.includes("hydra-acp"))) {
|
|
2883
|
+
return pkg.version;
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
const parent = path6.dirname(dir);
|
|
2887
|
+
if (parent === dir) {
|
|
2888
|
+
break;
|
|
2889
|
+
}
|
|
2890
|
+
dir = parent;
|
|
2891
|
+
}
|
|
2892
|
+
} catch {
|
|
2893
|
+
}
|
|
2894
|
+
return "0.0.0";
|
|
2895
|
+
}
|
|
2896
|
+
var HYDRA_VERSION = resolveVersion();
|
|
2897
|
+
|
|
2668
2898
|
// src/core/session-manager.ts
|
|
2669
2899
|
var HYDRA_ID_ALPHABET3 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
2670
2900
|
var generateRawSessionId = customAlphabet3(HYDRA_ID_ALPHABET3, 16);
|
|
@@ -2673,7 +2903,8 @@ var SessionManager = class {
|
|
|
2673
2903
|
this.registry = registry;
|
|
2674
2904
|
this.spawner = spawner ?? ((opts) => AgentInstance.spawn(opts));
|
|
2675
2905
|
this.store = store ?? new SessionStore();
|
|
2676
|
-
this.
|
|
2906
|
+
this.sessionHistoryMaxEntries = options.sessionHistoryMaxEntries ?? 1e3;
|
|
2907
|
+
this.histories = new HistoryStore({ maxEntries: this.sessionHistoryMaxEntries });
|
|
2677
2908
|
this.idleTimeoutMs = options.idleTimeoutMs ?? 0;
|
|
2678
2909
|
this.defaultModels = options.defaultModels ?? {};
|
|
2679
2910
|
}
|
|
@@ -2685,6 +2916,7 @@ var SessionManager = class {
|
|
|
2685
2916
|
histories;
|
|
2686
2917
|
idleTimeoutMs;
|
|
2687
2918
|
defaultModels;
|
|
2919
|
+
sessionHistoryMaxEntries;
|
|
2688
2920
|
// Serialize meta.json read-modify-write operations per session id so
|
|
2689
2921
|
// concurrent snapshot updates (e.g. an agent emitting model + mode
|
|
2690
2922
|
// back-to-back) don't lose writes via interleaved reads.
|
|
@@ -2708,6 +2940,7 @@ var SessionManager = class {
|
|
|
2708
2940
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
2709
2941
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
2710
2942
|
historyStore: this.histories,
|
|
2943
|
+
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
2711
2944
|
currentModel: fresh.initialModel
|
|
2712
2945
|
});
|
|
2713
2946
|
await this.attachManagerHooks(session);
|
|
@@ -2759,11 +2992,16 @@ var SessionManager = class {
|
|
|
2759
2992
|
cwd: params.cwd,
|
|
2760
2993
|
plan
|
|
2761
2994
|
});
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2995
|
+
try {
|
|
2996
|
+
await agent.connection.request("initialize", {
|
|
2997
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
2998
|
+
clientCapabilities: {},
|
|
2999
|
+
clientInfo: { name: "hydra", version: HYDRA_VERSION }
|
|
3000
|
+
});
|
|
3001
|
+
} catch (err) {
|
|
3002
|
+
await agent.kill().catch(() => void 0);
|
|
3003
|
+
throw err;
|
|
3004
|
+
}
|
|
2767
3005
|
let loadResult;
|
|
2768
3006
|
try {
|
|
2769
3007
|
loadResult = await agent.connection.request(
|
|
@@ -2775,10 +3013,12 @@ var SessionManager = class {
|
|
|
2775
3013
|
}
|
|
2776
3014
|
);
|
|
2777
3015
|
} catch (err) {
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
3016
|
+
process.stderr.write(
|
|
3017
|
+
`session/load failed for upstream ${params.upstreamSessionId} on ${params.agentId} (${err.message}); recovering via import-reseed
|
|
3018
|
+
`
|
|
2781
3019
|
);
|
|
3020
|
+
await agent.kill().catch(() => void 0);
|
|
3021
|
+
return this.doResurrectFromImport(params);
|
|
2782
3022
|
}
|
|
2783
3023
|
const session = new Session({
|
|
2784
3024
|
sessionId: params.hydraSessionId,
|
|
@@ -2792,6 +3032,7 @@ var SessionManager = class {
|
|
|
2792
3032
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
2793
3033
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
2794
3034
|
historyStore: this.histories,
|
|
3035
|
+
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
2795
3036
|
// Prefer what we previously stored from a current_model_update; if
|
|
2796
3037
|
// we never captured one (e.g. old opencode sessions on disk before
|
|
2797
3038
|
// this fix), fall back to the model the agent ships in its
|
|
@@ -2818,15 +3059,16 @@ var SessionManager = class {
|
|
|
2818
3059
|
// so subsequent resurrects of this session use the normal session/load
|
|
2819
3060
|
// path.
|
|
2820
3061
|
async doResurrectFromImport(params) {
|
|
3062
|
+
const cwd = await this.resolveImportCwd(params.cwd);
|
|
2821
3063
|
const fresh = await this.bootstrapAgent({
|
|
2822
3064
|
agentId: params.agentId,
|
|
2823
|
-
cwd
|
|
3065
|
+
cwd,
|
|
2824
3066
|
agentArgs: params.agentArgs,
|
|
2825
3067
|
mcpServers: []
|
|
2826
3068
|
});
|
|
2827
3069
|
const session = new Session({
|
|
2828
3070
|
sessionId: params.hydraSessionId,
|
|
2829
|
-
cwd
|
|
3071
|
+
cwd,
|
|
2830
3072
|
agentId: params.agentId,
|
|
2831
3073
|
agent: fresh.agent,
|
|
2832
3074
|
upstreamSessionId: fresh.upstreamSessionId,
|
|
@@ -2836,6 +3078,7 @@ var SessionManager = class {
|
|
|
2836
3078
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
2837
3079
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
2838
3080
|
historyStore: this.histories,
|
|
3081
|
+
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
2839
3082
|
// Prefer the stored value (set by a previous current_model_update);
|
|
2840
3083
|
// fall back to whatever the agent ships in its session/new response.
|
|
2841
3084
|
currentModel: params.currentModel ?? fresh.initialModel,
|
|
@@ -2849,6 +3092,16 @@ var SessionManager = class {
|
|
|
2849
3092
|
void session.seedFromImport().catch(() => void 0);
|
|
2850
3093
|
return session;
|
|
2851
3094
|
}
|
|
3095
|
+
async resolveImportCwd(cwd) {
|
|
3096
|
+
try {
|
|
3097
|
+
const stat2 = await fs8.stat(cwd);
|
|
3098
|
+
if (stat2.isDirectory()) {
|
|
3099
|
+
return cwd;
|
|
3100
|
+
}
|
|
3101
|
+
} catch {
|
|
3102
|
+
}
|
|
3103
|
+
return os2.homedir();
|
|
3104
|
+
}
|
|
2852
3105
|
// Bootstrap a fresh agent process: registry resolve → spawn → initialize
|
|
2853
3106
|
// → session/new. Shared by create() and the /hydra agent path so both
|
|
2854
3107
|
// go through the same env / capabilities / error-handling.
|
|
@@ -2869,9 +3122,9 @@ var SessionManager = class {
|
|
|
2869
3122
|
});
|
|
2870
3123
|
try {
|
|
2871
3124
|
await agent.connection.request("initialize", {
|
|
2872
|
-
protocolVersion:
|
|
3125
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
2873
3126
|
clientCapabilities: {},
|
|
2874
|
-
clientInfo: { name: "hydra", version:
|
|
3127
|
+
clientInfo: { name: "hydra", version: HYDRA_VERSION }
|
|
2875
3128
|
});
|
|
2876
3129
|
const newResult = await agent.connection.request(
|
|
2877
3130
|
"session/new",
|
|
@@ -3151,7 +3404,8 @@ var SessionManager = class {
|
|
|
3151
3404
|
await this.writeImportedRecord({
|
|
3152
3405
|
sessionId: existing.sessionId,
|
|
3153
3406
|
bundle,
|
|
3154
|
-
preservedCreatedAt: existing.createdAt
|
|
3407
|
+
preservedCreatedAt: existing.createdAt,
|
|
3408
|
+
cwd: opts.cwd
|
|
3155
3409
|
});
|
|
3156
3410
|
return {
|
|
3157
3411
|
sessionId: existing.sessionId,
|
|
@@ -3160,7 +3414,11 @@ var SessionManager = class {
|
|
|
3160
3414
|
};
|
|
3161
3415
|
}
|
|
3162
3416
|
const newId = `${HYDRA_SESSION_PREFIX}${generateRawSessionId()}`;
|
|
3163
|
-
await this.writeImportedRecord({
|
|
3417
|
+
await this.writeImportedRecord({
|
|
3418
|
+
sessionId: newId,
|
|
3419
|
+
bundle,
|
|
3420
|
+
cwd: opts.cwd
|
|
3421
|
+
});
|
|
3164
3422
|
return {
|
|
3165
3423
|
sessionId: newId,
|
|
3166
3424
|
importedFromSessionId: bundle.session.sessionId,
|
|
@@ -3190,7 +3448,7 @@ var SessionManager = class {
|
|
|
3190
3448
|
upstreamSessionId: "",
|
|
3191
3449
|
importedFromSessionId: args.bundle.session.sessionId,
|
|
3192
3450
|
agentId: args.bundle.session.agentId,
|
|
3193
|
-
cwd: args.bundle.session.cwd,
|
|
3451
|
+
cwd: args.cwd ?? args.bundle.session.cwd,
|
|
3194
3452
|
title: args.bundle.session.title,
|
|
3195
3453
|
currentModel: args.bundle.session.currentModel,
|
|
3196
3454
|
currentMode: args.bundle.session.currentMode,
|
|
@@ -3383,7 +3641,7 @@ function asString(value) {
|
|
|
3383
3641
|
}
|
|
3384
3642
|
async function loadPromptHistorySafely(sessionId) {
|
|
3385
3643
|
try {
|
|
3386
|
-
const raw = await
|
|
3644
|
+
const raw = await fs8.readFile(paths.tuiHistoryFile(sessionId), "utf8");
|
|
3387
3645
|
const out = [];
|
|
3388
3646
|
for (const line of raw.split("\n")) {
|
|
3389
3647
|
if (line.length === 0) {
|
|
@@ -3404,7 +3662,7 @@ async function loadPromptHistorySafely(sessionId) {
|
|
|
3404
3662
|
}
|
|
3405
3663
|
async function historyMtimeIso(sessionId) {
|
|
3406
3664
|
try {
|
|
3407
|
-
const st = await
|
|
3665
|
+
const st = await fs8.stat(paths.historyFile(sessionId));
|
|
3408
3666
|
return new Date(st.mtimeMs).toISOString();
|
|
3409
3667
|
} catch {
|
|
3410
3668
|
return void 0;
|
|
@@ -3412,10 +3670,10 @@ async function historyMtimeIso(sessionId) {
|
|
|
3412
3670
|
}
|
|
3413
3671
|
|
|
3414
3672
|
// src/core/extensions.ts
|
|
3415
|
-
import { spawn as
|
|
3416
|
-
import * as
|
|
3417
|
-
import * as
|
|
3418
|
-
import * as
|
|
3673
|
+
import { spawn as spawn4 } from "child_process";
|
|
3674
|
+
import * as fs9 from "fs";
|
|
3675
|
+
import * as fsp3 from "fs/promises";
|
|
3676
|
+
import * as path7 from "path";
|
|
3419
3677
|
var RESTART_BASE_MS = 1e3;
|
|
3420
3678
|
var RESTART_CAP_MS = 6e4;
|
|
3421
3679
|
var STOP_GRACE_MS = 3e3;
|
|
@@ -3436,7 +3694,7 @@ var ExtensionManager = class {
|
|
|
3436
3694
|
if (!this.context) {
|
|
3437
3695
|
throw new Error("ExtensionManager: setContext must be called before start");
|
|
3438
3696
|
}
|
|
3439
|
-
await
|
|
3697
|
+
await fsp3.mkdir(paths.extensionsDir(), { recursive: true });
|
|
3440
3698
|
await this.reapOrphans();
|
|
3441
3699
|
for (const entry of this.entries.values()) {
|
|
3442
3700
|
if (!entry.config.enabled) {
|
|
@@ -3645,7 +3903,7 @@ var ExtensionManager = class {
|
|
|
3645
3903
|
async reapOrphans() {
|
|
3646
3904
|
let entries;
|
|
3647
3905
|
try {
|
|
3648
|
-
entries = await
|
|
3906
|
+
entries = await fsp3.readdir(paths.extensionsDir());
|
|
3649
3907
|
} catch (err) {
|
|
3650
3908
|
const e = err;
|
|
3651
3909
|
if (e.code === "ENOENT") {
|
|
@@ -3657,10 +3915,10 @@ var ExtensionManager = class {
|
|
|
3657
3915
|
if (!entry.endsWith(".pid")) {
|
|
3658
3916
|
continue;
|
|
3659
3917
|
}
|
|
3660
|
-
const pidPath =
|
|
3918
|
+
const pidPath = path7.join(paths.extensionsDir(), entry);
|
|
3661
3919
|
let pid;
|
|
3662
3920
|
try {
|
|
3663
|
-
const raw = await
|
|
3921
|
+
const raw = await fsp3.readFile(pidPath, "utf8");
|
|
3664
3922
|
const parsed = Number.parseInt(raw.trim(), 10);
|
|
3665
3923
|
if (Number.isInteger(parsed) && parsed > 0) {
|
|
3666
3924
|
pid = parsed;
|
|
@@ -3683,7 +3941,7 @@ var ExtensionManager = class {
|
|
|
3683
3941
|
}
|
|
3684
3942
|
}
|
|
3685
3943
|
}
|
|
3686
|
-
await
|
|
3944
|
+
await fsp3.unlink(pidPath).catch(() => void 0);
|
|
3687
3945
|
}
|
|
3688
3946
|
}
|
|
3689
3947
|
spawn(entry, attempt) {
|
|
@@ -3696,7 +3954,7 @@ var ExtensionManager = class {
|
|
|
3696
3954
|
}
|
|
3697
3955
|
const ext = entry.config;
|
|
3698
3956
|
const command = ext.command.length > 0 ? ext.command : [ext.name];
|
|
3699
|
-
const logStream =
|
|
3957
|
+
const logStream = fs9.createWriteStream(paths.extensionLogFile(ext.name), {
|
|
3700
3958
|
flags: "a"
|
|
3701
3959
|
});
|
|
3702
3960
|
logStream.write(
|
|
@@ -3724,7 +3982,7 @@ var ExtensionManager = class {
|
|
|
3724
3982
|
const args = [...baseArgs, ...ext.args];
|
|
3725
3983
|
let child;
|
|
3726
3984
|
try {
|
|
3727
|
-
child =
|
|
3985
|
+
child = spawn4(cmd, args, {
|
|
3728
3986
|
env,
|
|
3729
3987
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3730
3988
|
detached: false
|
|
@@ -3746,7 +4004,7 @@ var ExtensionManager = class {
|
|
|
3746
4004
|
}
|
|
3747
4005
|
if (typeof child.pid === "number") {
|
|
3748
4006
|
try {
|
|
3749
|
-
|
|
4007
|
+
fs9.writeFileSync(paths.extensionPidFile(ext.name), `${child.pid}
|
|
3750
4008
|
`, {
|
|
3751
4009
|
encoding: "utf8",
|
|
3752
4010
|
mode: 384
|
|
@@ -3771,7 +4029,7 @@ var ExtensionManager = class {
|
|
|
3771
4029
|
});
|
|
3772
4030
|
child.on("exit", (code, signal) => {
|
|
3773
4031
|
try {
|
|
3774
|
-
|
|
4032
|
+
fs9.unlinkSync(paths.extensionPidFile(ext.name));
|
|
3775
4033
|
} catch {
|
|
3776
4034
|
}
|
|
3777
4035
|
logStream.write(
|
|
@@ -3880,7 +4138,7 @@ function constantTimeEqual(a, b) {
|
|
|
3880
4138
|
}
|
|
3881
4139
|
|
|
3882
4140
|
// src/daemon/routes/sessions.ts
|
|
3883
|
-
import * as
|
|
4141
|
+
import * as os3 from "os";
|
|
3884
4142
|
|
|
3885
4143
|
// src/core/bundle.ts
|
|
3886
4144
|
import { z as z5 } from "zod";
|
|
@@ -3950,7 +4208,6 @@ function decodeBundle(raw) {
|
|
|
3950
4208
|
}
|
|
3951
4209
|
|
|
3952
4210
|
// src/daemon/routes/sessions.ts
|
|
3953
|
-
var HYDRA_VERSION = "0.1.0";
|
|
3954
4211
|
function registerSessionRoutes(app, manager, defaults) {
|
|
3955
4212
|
app.get("/v1/sessions", async (request) => {
|
|
3956
4213
|
const query = request.query;
|
|
@@ -4021,7 +4278,7 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
4021
4278
|
history: exported.history,
|
|
4022
4279
|
promptHistory: exported.promptHistory.length > 0 ? exported.promptHistory : void 0,
|
|
4023
4280
|
hydraVersion: HYDRA_VERSION,
|
|
4024
|
-
machine:
|
|
4281
|
+
machine: os3.hostname()
|
|
4025
4282
|
});
|
|
4026
4283
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
4027
4284
|
reply.header(
|
|
@@ -4036,6 +4293,14 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
4036
4293
|
reply.code(400).send({ error: "missing bundle" });
|
|
4037
4294
|
return;
|
|
4038
4295
|
}
|
|
4296
|
+
let cwdOverride;
|
|
4297
|
+
if (body.cwd !== void 0) {
|
|
4298
|
+
if (typeof body.cwd !== "string" || body.cwd.length === 0) {
|
|
4299
|
+
reply.code(400).send({ error: "cwd must be a non-empty string" });
|
|
4300
|
+
return;
|
|
4301
|
+
}
|
|
4302
|
+
cwdOverride = body.cwd;
|
|
4303
|
+
}
|
|
4039
4304
|
let bundle;
|
|
4040
4305
|
try {
|
|
4041
4306
|
bundle = decodeBundle(body.bundle);
|
|
@@ -4048,7 +4313,8 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
4048
4313
|
}
|
|
4049
4314
|
try {
|
|
4050
4315
|
const result = await manager.importBundle(bundle, {
|
|
4051
|
-
replace: body.replace === true
|
|
4316
|
+
replace: body.replace === true,
|
|
4317
|
+
...cwdOverride !== void 0 ? { cwd: cwdOverride } : {}
|
|
4052
4318
|
});
|
|
4053
4319
|
reply.code(201).send(result);
|
|
4054
4320
|
} catch (err) {
|
|
@@ -4359,8 +4625,6 @@ function wsToMessageStream(ws) {
|
|
|
4359
4625
|
}
|
|
4360
4626
|
|
|
4361
4627
|
// src/daemon/acp-ws.ts
|
|
4362
|
-
var HYDRA_VERSION2 = "0.1.0";
|
|
4363
|
-
var HYDRA_PROTOCOL_VERSION = 1;
|
|
4364
4628
|
function registerAcpWsEndpoint(app, deps) {
|
|
4365
4629
|
app.get("/acp", { websocket: true }, (socket, request) => {
|
|
4366
4630
|
const token = tokenFromUpgradeRequest({
|
|
@@ -4623,8 +4887,8 @@ function buildResponseMeta(session) {
|
|
|
4623
4887
|
}
|
|
4624
4888
|
function buildInitializeResult() {
|
|
4625
4889
|
return {
|
|
4626
|
-
protocolVersion:
|
|
4627
|
-
agentInfo: { name: "hydra", version:
|
|
4890
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
4891
|
+
agentInfo: { name: "hydra", version: HYDRA_VERSION },
|
|
4628
4892
|
agentCapabilities: {
|
|
4629
4893
|
// hydra is a transparent proxy: prompt blocks and MCP server configs are
|
|
4630
4894
|
// forwarded to the underlying agent unchanged. We claim the union of
|
|
@@ -4663,14 +4927,13 @@ function bindClientToSession(connection, session, state, clientInfo) {
|
|
|
4663
4927
|
}
|
|
4664
4928
|
|
|
4665
4929
|
// src/daemon/server.ts
|
|
4666
|
-
var HYDRA_VERSION3 = "0.1.0";
|
|
4667
4930
|
async function startDaemon(config) {
|
|
4668
4931
|
ensureLoopbackOrTls(config);
|
|
4669
4932
|
const httpsOptions = config.daemon.tls ? {
|
|
4670
|
-
key: await
|
|
4671
|
-
cert: await
|
|
4933
|
+
key: await fsp4.readFile(config.daemon.tls.key),
|
|
4934
|
+
cert: await fsp4.readFile(config.daemon.tls.cert)
|
|
4672
4935
|
} : void 0;
|
|
4673
|
-
await
|
|
4936
|
+
await fsp4.mkdir(paths.home(), { recursive: true });
|
|
4674
4937
|
const { stream: logStream, fileStream } = await buildLogStream(
|
|
4675
4938
|
config.daemon.logLevel
|
|
4676
4939
|
);
|
|
@@ -4679,12 +4942,18 @@ async function startDaemon(config) {
|
|
|
4679
4942
|
level: config.daemon.logLevel,
|
|
4680
4943
|
stream: logStream
|
|
4681
4944
|
},
|
|
4682
|
-
https: httpsOptions ?? null
|
|
4945
|
+
https: httpsOptions ?? null,
|
|
4946
|
+
// Session bundles can be large (full history + tool output);
|
|
4947
|
+
// the 1MB Fastify default rejects ordinary imports.
|
|
4948
|
+
bodyLimit: 256 * 1024 * 1024
|
|
4683
4949
|
});
|
|
4684
4950
|
await app.register(websocketPlugin);
|
|
4685
4951
|
setBinaryInstallLogger((msg) => {
|
|
4686
4952
|
app.log.info(msg);
|
|
4687
4953
|
});
|
|
4954
|
+
setNpmInstallLogger((msg) => {
|
|
4955
|
+
app.log.info(msg);
|
|
4956
|
+
});
|
|
4688
4957
|
const auth = bearerAuth({ config });
|
|
4689
4958
|
app.addHook("onRequest", async (request, reply) => {
|
|
4690
4959
|
if (request.routeOptions.config?.skipAuth) {
|
|
@@ -4696,12 +4965,14 @@ async function startDaemon(config) {
|
|
|
4696
4965
|
await auth(request, reply);
|
|
4697
4966
|
});
|
|
4698
4967
|
const registry = new Registry(config);
|
|
4699
|
-
const
|
|
4968
|
+
const spawner = (opts) => AgentInstance.spawn({ ...opts, stderrTailBytes: config.daemon.agentStderrTailBytes });
|
|
4969
|
+
const manager = new SessionManager(registry, spawner, void 0, {
|
|
4700
4970
|
idleTimeoutMs: config.daemon.sessionIdleTimeoutSeconds * 1e3,
|
|
4701
|
-
defaultModels: config.defaultModels
|
|
4971
|
+
defaultModels: config.defaultModels,
|
|
4972
|
+
sessionHistoryMaxEntries: config.daemon.sessionHistoryMaxEntries
|
|
4702
4973
|
});
|
|
4703
4974
|
const extensions = new ExtensionManager(extensionList(config));
|
|
4704
|
-
registerHealthRoutes(app,
|
|
4975
|
+
registerHealthRoutes(app, HYDRA_VERSION);
|
|
4705
4976
|
registerSessionRoutes(app, manager, {
|
|
4706
4977
|
agentId: config.defaultAgent,
|
|
4707
4978
|
cwd: config.defaultCwd
|
|
@@ -4720,8 +4991,8 @@ async function startDaemon(config) {
|
|
|
4720
4991
|
await app.listen({ host: config.daemon.host, port: config.daemon.port });
|
|
4721
4992
|
const address = app.server.address();
|
|
4722
4993
|
const boundPort = address && typeof address === "object" ? address.port : config.daemon.port;
|
|
4723
|
-
await
|
|
4724
|
-
await
|
|
4994
|
+
await fsp4.mkdir(paths.home(), { recursive: true });
|
|
4995
|
+
await fsp4.writeFile(
|
|
4725
4996
|
paths.pidFile(),
|
|
4726
4997
|
JSON.stringify({
|
|
4727
4998
|
pid: process.pid,
|
|
@@ -4747,9 +5018,10 @@ async function startDaemon(config) {
|
|
|
4747
5018
|
await manager.closeAll();
|
|
4748
5019
|
await manager.flushMetaWrites();
|
|
4749
5020
|
setBinaryInstallLogger(null);
|
|
5021
|
+
setNpmInstallLogger(null);
|
|
4750
5022
|
await app.close();
|
|
4751
5023
|
try {
|
|
4752
|
-
|
|
5024
|
+
fs10.unlinkSync(paths.pidFile());
|
|
4753
5025
|
} catch {
|
|
4754
5026
|
}
|
|
4755
5027
|
try {
|