@hydra-acp/cli 0.1.6 → 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 +3 -1
- package/dist/cli.js +2185 -634
- package/dist/index.d.ts +68 -5
- package/dist/index.js +471 -106
- 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";
|
|
@@ -30,6 +30,9 @@ function hydraHome() {
|
|
|
30
30
|
var paths = {
|
|
31
31
|
home: hydraHome,
|
|
32
32
|
config: () => path.join(hydraHome(), "config.json"),
|
|
33
|
+
// Auth token lives in its own file so config.json can be version-
|
|
34
|
+
// controlled without leaking the secret. Raw string contents, mode 0600.
|
|
35
|
+
authToken: () => path.join(hydraHome(), "auth-token"),
|
|
33
36
|
pidFile: () => path.join(hydraHome(), "daemon.pid"),
|
|
34
37
|
logFile: () => path.join(hydraHome(), "daemon.log"),
|
|
35
38
|
currentLogFile: () => path.join(hydraHome(), "current.log"),
|
|
@@ -40,6 +43,18 @@ var paths = {
|
|
|
40
43
|
// machine's binaries cleanly separated. `ls agents/` immediately
|
|
41
44
|
// shows which platforms have ever installed anything.
|
|
42
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
|
+
),
|
|
43
58
|
sessionsDir: () => path.join(hydraHome(), "sessions"),
|
|
44
59
|
// One directory per session id under sessions/. Co-locates the
|
|
45
60
|
// session record, its transcript, and any future per-session state
|
|
@@ -66,7 +81,16 @@ var DaemonConfig = z.object({
|
|
|
66
81
|
authToken: z.string().min(16),
|
|
67
82
|
logLevel: z.enum(["debug", "info", "warn", "error"]).default("info"),
|
|
68
83
|
tls: TlsConfig.optional(),
|
|
69
|
-
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)
|
|
70
94
|
});
|
|
71
95
|
var RegistryConfig = z.object({
|
|
72
96
|
url: z.string().url().default(REGISTRY_URL_DEFAULT),
|
|
@@ -83,7 +107,20 @@ var TuiConfig = z.object({
|
|
|
83
107
|
// Cap on logical lines retained in the in-memory scrollback render
|
|
84
108
|
// buffer. Oldest lines are dropped on overflow. The on-disk session
|
|
85
109
|
// history is unaffected; this only bounds the TUI's local view buffer.
|
|
86
|
-
maxScrollbackLines: z.number().int().positive().default(1e4)
|
|
110
|
+
maxScrollbackLines: z.number().int().positive().default(1e4),
|
|
111
|
+
// When true (default), the TUI captures mouse events so the wheel can
|
|
112
|
+
// drive scrollback. The cost: terminals route clicks to the app, so
|
|
113
|
+
// text selection requires shift+drag to bypass mouse reporting. Set
|
|
114
|
+
// false to disable capture — wheel scrollback stops working, but
|
|
115
|
+
// plain click-drag selects text via the terminal emulator.
|
|
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)
|
|
87
124
|
});
|
|
88
125
|
var ExtensionName = z.string().min(1).regex(/^[A-Za-z0-9._-]+$/, "extension name must be filename-safe");
|
|
89
126
|
var ExtensionBody = z.object({
|
|
@@ -116,7 +153,16 @@ var HydraConfig = z.object({
|
|
|
116
153
|
// recency and truncated to this count. `--all` overrides in the CLI.
|
|
117
154
|
sessionListColdLimit: z.number().int().nonnegative().default(20),
|
|
118
155
|
extensions: z.record(ExtensionName, ExtensionBody).default({}),
|
|
119
|
-
tui: TuiConfig.default({
|
|
156
|
+
tui: TuiConfig.default({
|
|
157
|
+
repaintThrottleMs: 1e3,
|
|
158
|
+
maxScrollbackLines: 1e4,
|
|
159
|
+
mouse: true,
|
|
160
|
+
logMaxBytes: 5 * 1024 * 1024,
|
|
161
|
+
cwdColumnMaxWidth: 24
|
|
162
|
+
})
|
|
163
|
+
});
|
|
164
|
+
var HydraConfigReadOnly = HydraConfig.extend({
|
|
165
|
+
daemon: DaemonConfig.omit({ authToken: true })
|
|
120
166
|
});
|
|
121
167
|
function extensionList(config) {
|
|
122
168
|
return Object.entries(config.extensions).map(([name, body]) => ({
|
|
@@ -124,56 +170,104 @@ function extensionList(config) {
|
|
|
124
170
|
...body
|
|
125
171
|
}));
|
|
126
172
|
}
|
|
127
|
-
async function
|
|
128
|
-
const configPath = paths.config();
|
|
173
|
+
async function readConfigFile() {
|
|
129
174
|
let raw;
|
|
130
175
|
try {
|
|
131
|
-
raw = await fs.readFile(
|
|
176
|
+
raw = await fs.readFile(paths.config(), "utf8");
|
|
132
177
|
} catch (err) {
|
|
133
178
|
const e = err;
|
|
134
179
|
if (e.code === "ENOENT") {
|
|
135
|
-
|
|
136
|
-
`No config found at ${configPath}. Run \`hydra-acp init\` to create one.`
|
|
137
|
-
);
|
|
180
|
+
return {};
|
|
138
181
|
}
|
|
139
182
|
throw err;
|
|
140
183
|
}
|
|
141
|
-
|
|
142
|
-
return HydraConfig.parse(parsed);
|
|
184
|
+
return JSON.parse(raw);
|
|
143
185
|
}
|
|
144
|
-
async function
|
|
186
|
+
async function loadAuthToken() {
|
|
187
|
+
let tokenFile;
|
|
145
188
|
try {
|
|
146
|
-
await fs.
|
|
189
|
+
const text = await fs.readFile(paths.authToken(), "utf8");
|
|
190
|
+
const trimmed = text.trim();
|
|
191
|
+
if (trimmed.length > 0) {
|
|
192
|
+
tokenFile = trimmed;
|
|
193
|
+
}
|
|
147
194
|
} catch (err) {
|
|
148
195
|
const e = err;
|
|
149
196
|
if (e.code !== "ENOENT") {
|
|
150
197
|
throw err;
|
|
151
198
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
199
|
+
}
|
|
200
|
+
const raw = await readConfigFile();
|
|
201
|
+
const daemon = raw.daemon;
|
|
202
|
+
const legacy = daemon && typeof daemon.authToken === "string" ? daemon.authToken : void 0;
|
|
203
|
+
if (tokenFile && legacy) {
|
|
204
|
+
throw new Error(
|
|
205
|
+
`Auth token present in both ${paths.authToken()} and ${paths.config()} (daemon.authToken). Remove daemon.authToken from config.json to resolve.`
|
|
156
206
|
);
|
|
157
|
-
return config;
|
|
158
207
|
}
|
|
159
|
-
|
|
208
|
+
if (tokenFile) {
|
|
209
|
+
return tokenFile;
|
|
210
|
+
}
|
|
211
|
+
if (legacy) {
|
|
212
|
+
await migrateLegacyAuthToken(raw, daemon, legacy);
|
|
213
|
+
return legacy;
|
|
214
|
+
}
|
|
215
|
+
return void 0;
|
|
160
216
|
}
|
|
161
|
-
async function
|
|
217
|
+
async function migrateLegacyAuthToken(raw, daemon, token) {
|
|
218
|
+
await writeAuthToken(token);
|
|
219
|
+
delete daemon.authToken;
|
|
220
|
+
if (Object.keys(daemon).length === 0) {
|
|
221
|
+
delete raw.daemon;
|
|
222
|
+
}
|
|
223
|
+
await fs.writeFile(paths.config(), JSON.stringify(raw, null, 2) + "\n", {
|
|
224
|
+
encoding: "utf8",
|
|
225
|
+
mode: 384
|
|
226
|
+
});
|
|
227
|
+
process.stderr.write(
|
|
228
|
+
`hydra-acp: migrated auth token from ${paths.config()} to ${paths.authToken()}.
|
|
229
|
+
`
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
async function writeAuthToken(token) {
|
|
162
233
|
await fs.mkdir(paths.home(), { recursive: true });
|
|
163
|
-
await fs.writeFile(paths.
|
|
234
|
+
await fs.writeFile(paths.authToken(), token + "\n", {
|
|
164
235
|
encoding: "utf8",
|
|
165
236
|
mode: 384
|
|
166
237
|
});
|
|
167
238
|
}
|
|
168
|
-
async function
|
|
169
|
-
const token =
|
|
170
|
-
|
|
239
|
+
async function loadConfig() {
|
|
240
|
+
const token = await loadAuthToken();
|
|
241
|
+
if (!token) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
`No auth token found at ${paths.authToken()}. Run \`hydra-acp init\` to create one.`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
const raw = await readConfigFile();
|
|
247
|
+
const daemon = raw.daemon ??= {};
|
|
248
|
+
daemon.authToken = token;
|
|
249
|
+
return HydraConfig.parse(raw);
|
|
250
|
+
}
|
|
251
|
+
async function ensureConfig() {
|
|
252
|
+
if (!await loadAuthToken()) {
|
|
253
|
+
const token = generateAuthToken();
|
|
254
|
+
await writeAuthToken(token);
|
|
255
|
+
process.stderr.write(
|
|
256
|
+
`hydra-acp: initialized ${paths.authToken()} with a fresh auth token.
|
|
257
|
+
`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
return loadConfig();
|
|
261
|
+
}
|
|
262
|
+
async function writeConfig(config) {
|
|
171
263
|
await fs.mkdir(paths.home(), { recursive: true });
|
|
172
|
-
|
|
264
|
+
const { daemon, ...rest } = config;
|
|
265
|
+
const { authToken: _authToken, ...daemonRest } = daemon;
|
|
266
|
+
const onDisk = { ...rest, daemon: daemonRest };
|
|
267
|
+
await fs.writeFile(paths.config(), JSON.stringify(onDisk, null, 2) + "\n", {
|
|
173
268
|
encoding: "utf8",
|
|
174
269
|
mode: 384
|
|
175
270
|
});
|
|
176
|
-
return HydraConfig.parse(minimal);
|
|
177
271
|
}
|
|
178
272
|
function generateAuthToken() {
|
|
179
273
|
const bytes = new Uint8Array(32);
|
|
@@ -411,9 +505,129 @@ async function fileExists(p) {
|
|
|
411
505
|
}
|
|
412
506
|
}
|
|
413
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
|
+
|
|
414
624
|
// src/core/registry.ts
|
|
415
625
|
var NpxDistribution = z2.object({
|
|
416
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(),
|
|
417
631
|
args: z2.array(z2.string()).optional(),
|
|
418
632
|
env: z2.record(z2.string()).optional()
|
|
419
633
|
});
|
|
@@ -573,13 +787,27 @@ function npxPackageBasename(agent) {
|
|
|
573
787
|
const atIdx = afterSlash.lastIndexOf("@");
|
|
574
788
|
return atIdx <= 0 ? afterSlash : afterSlash.slice(0, atIdx);
|
|
575
789
|
}
|
|
576
|
-
async function planSpawn(agent,
|
|
790
|
+
async function planSpawn(agent, callerArgs = []) {
|
|
577
791
|
if (agent.distribution.npx) {
|
|
578
792
|
const npx = agent.distribution.npx;
|
|
579
|
-
const
|
|
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
|
+
});
|
|
580
808
|
return {
|
|
581
|
-
command:
|
|
582
|
-
args,
|
|
809
|
+
command: binPath,
|
|
810
|
+
args: tail,
|
|
583
811
|
env: npx.env ?? {}
|
|
584
812
|
};
|
|
585
813
|
}
|
|
@@ -595,33 +823,31 @@ async function planSpawn(agent, extraArgs = []) {
|
|
|
595
823
|
version: agent.version ?? "current",
|
|
596
824
|
target
|
|
597
825
|
});
|
|
826
|
+
const tail = callerArgs.length > 0 ? callerArgs : target.args ?? [];
|
|
598
827
|
return {
|
|
599
828
|
command: cmdPath,
|
|
600
|
-
args:
|
|
829
|
+
args: tail,
|
|
601
830
|
env: target.env ?? {}
|
|
602
831
|
};
|
|
603
832
|
}
|
|
604
833
|
if (agent.distribution.uvx) {
|
|
605
834
|
const uvx = agent.distribution.uvx;
|
|
606
|
-
const
|
|
835
|
+
const tail = callerArgs.length > 0 ? callerArgs : uvx.args ?? [];
|
|
607
836
|
return {
|
|
608
837
|
command: "uvx",
|
|
609
|
-
args,
|
|
838
|
+
args: [uvx.package, ...tail],
|
|
610
839
|
env: uvx.env ?? {}
|
|
611
840
|
};
|
|
612
841
|
}
|
|
613
842
|
throw new Error(`Agent ${agent.id} has no usable distribution method.`);
|
|
614
843
|
}
|
|
615
844
|
|
|
616
|
-
// src/core/session-manager.ts
|
|
617
|
-
import * as fs7 from "fs/promises";
|
|
618
|
-
import { customAlphabet as customAlphabet3 } from "nanoid";
|
|
619
|
-
|
|
620
845
|
// src/core/agent-instance.ts
|
|
621
|
-
import { spawn as
|
|
846
|
+
import { spawn as spawn3 } from "child_process";
|
|
622
847
|
|
|
623
848
|
// src/acp/types.ts
|
|
624
849
|
import { z as z3 } from "zod";
|
|
850
|
+
var ACP_PROTOCOL_VERSION = 1;
|
|
625
851
|
var JsonRpcErrorCodes = {
|
|
626
852
|
ParseError: -32700,
|
|
627
853
|
InvalidRequest: -32600,
|
|
@@ -865,7 +1091,7 @@ function ndjsonStreamFromStdio(stdout, stdin) {
|
|
|
865
1091
|
|
|
866
1092
|
// src/acp/connection.ts
|
|
867
1093
|
import { nanoid } from "nanoid";
|
|
868
|
-
var JsonRpcConnection = class {
|
|
1094
|
+
var JsonRpcConnection = class _JsonRpcConnection {
|
|
869
1095
|
constructor(stream) {
|
|
870
1096
|
this.stream = stream;
|
|
871
1097
|
this.stream.onMessage((m) => this.handleIncoming(m));
|
|
@@ -875,6 +1101,16 @@ var JsonRpcConnection = class {
|
|
|
875
1101
|
requestHandlers = /* @__PURE__ */ new Map();
|
|
876
1102
|
defaultRequestHandler;
|
|
877
1103
|
notificationHandlers = /* @__PURE__ */ new Map();
|
|
1104
|
+
// Notifications received before a handler was registered. Some agents
|
|
1105
|
+
// (e.g. claude-acp) advertise their command list in the same chunk as
|
|
1106
|
+
// the `session/new` response, which is processed before the consumer
|
|
1107
|
+
// can attach its `session/update` handler. Without this buffer those
|
|
1108
|
+
// notifications would be silently dropped, so e.g. `/model` would
|
|
1109
|
+
// never appear in the TUI's slash-completion palette. Capped per
|
|
1110
|
+
// method to keep the buffer from growing unboundedly when nothing
|
|
1111
|
+
// ever subscribes.
|
|
1112
|
+
bufferedNotifications = /* @__PURE__ */ new Map();
|
|
1113
|
+
static MAX_BUFFERED_PER_METHOD = 64;
|
|
878
1114
|
pending = /* @__PURE__ */ new Map();
|
|
879
1115
|
closed = false;
|
|
880
1116
|
closeHandlers = [];
|
|
@@ -886,6 +1122,17 @@ var JsonRpcConnection = class {
|
|
|
886
1122
|
}
|
|
887
1123
|
onNotification(method, handler) {
|
|
888
1124
|
this.notificationHandlers.set(method, handler);
|
|
1125
|
+
const queued = this.bufferedNotifications.get(method);
|
|
1126
|
+
if (!queued) {
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
this.bufferedNotifications.delete(method);
|
|
1130
|
+
for (const note of queued) {
|
|
1131
|
+
try {
|
|
1132
|
+
handler(note.params, note.method);
|
|
1133
|
+
} catch {
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
889
1136
|
}
|
|
890
1137
|
onClose(handler) {
|
|
891
1138
|
this.closeHandlers.push(handler);
|
|
@@ -930,6 +1177,13 @@ var JsonRpcConnection = class {
|
|
|
930
1177
|
}
|
|
931
1178
|
await this.stream.close();
|
|
932
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
|
+
}
|
|
933
1187
|
handleIncoming(message) {
|
|
934
1188
|
if ("method" in message) {
|
|
935
1189
|
if ("id" in message && message.id !== void 0) {
|
|
@@ -971,6 +1225,16 @@ var JsonRpcConnection = class {
|
|
|
971
1225
|
const handler = this.notificationHandlers.get(note.method);
|
|
972
1226
|
if (handler) {
|
|
973
1227
|
handler(note.params, note.method);
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
let queued = this.bufferedNotifications.get(note.method);
|
|
1231
|
+
if (!queued) {
|
|
1232
|
+
queued = [];
|
|
1233
|
+
this.bufferedNotifications.set(note.method, queued);
|
|
1234
|
+
}
|
|
1235
|
+
queued.push(note);
|
|
1236
|
+
if (queued.length > _JsonRpcConnection.MAX_BUFFERED_PER_METHOD) {
|
|
1237
|
+
queued.shift();
|
|
974
1238
|
}
|
|
975
1239
|
}
|
|
976
1240
|
handleResponse(res) {
|
|
@@ -1012,17 +1276,22 @@ var JsonRpcConnection = class {
|
|
|
1012
1276
|
};
|
|
1013
1277
|
|
|
1014
1278
|
// src/core/agent-instance.ts
|
|
1279
|
+
var DEFAULT_STDERR_TAIL_BYTES = 4096;
|
|
1015
1280
|
var AgentInstance = class _AgentInstance {
|
|
1016
1281
|
agentId;
|
|
1017
1282
|
cwd;
|
|
1018
1283
|
connection;
|
|
1019
1284
|
child;
|
|
1020
1285
|
exited = false;
|
|
1286
|
+
killed = false;
|
|
1287
|
+
stderrTail = "";
|
|
1288
|
+
stderrTailBytes;
|
|
1021
1289
|
exitHandlers = [];
|
|
1022
1290
|
constructor(opts, child) {
|
|
1023
1291
|
this.agentId = opts.agentId;
|
|
1024
1292
|
this.cwd = opts.cwd;
|
|
1025
1293
|
this.child = child;
|
|
1294
|
+
this.stderrTailBytes = opts.stderrTailBytes ?? DEFAULT_STDERR_TAIL_BYTES;
|
|
1026
1295
|
if (!child.stdout || !child.stdin) {
|
|
1027
1296
|
throw new Error("agent subprocess missing stdio");
|
|
1028
1297
|
}
|
|
@@ -1030,22 +1299,36 @@ var AgentInstance = class _AgentInstance {
|
|
|
1030
1299
|
this.connection = new JsonRpcConnection(stream);
|
|
1031
1300
|
child.stderr?.setEncoding("utf8");
|
|
1032
1301
|
child.stderr?.on("data", (chunk) => {
|
|
1302
|
+
this.stderrTail = (this.stderrTail + chunk).slice(-this.stderrTailBytes);
|
|
1033
1303
|
process.stderr.write(`[${opts.agentId}] ${chunk}`);
|
|
1034
1304
|
});
|
|
1305
|
+
child.on("error", (err) => {
|
|
1306
|
+
const msg = this.formatFailure(err.message);
|
|
1307
|
+
this.connection.fail(new Error(msg));
|
|
1308
|
+
});
|
|
1035
1309
|
child.on("exit", (code, signal) => {
|
|
1036
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
|
+
}
|
|
1037
1315
|
for (const handler of this.exitHandlers) {
|
|
1038
1316
|
handler(code, signal);
|
|
1039
1317
|
}
|
|
1040
1318
|
});
|
|
1041
1319
|
}
|
|
1320
|
+
formatFailure(reason) {
|
|
1321
|
+
const tail = this.stderrTail.trim();
|
|
1322
|
+
return tail ? `${reason}
|
|
1323
|
+
stderr: ${tail}` : reason;
|
|
1324
|
+
}
|
|
1042
1325
|
static spawn(opts) {
|
|
1043
1326
|
const env = {
|
|
1044
1327
|
...process.env,
|
|
1045
1328
|
...opts.plan.env,
|
|
1046
1329
|
...opts.extraEnv ?? {}
|
|
1047
1330
|
};
|
|
1048
|
-
const child =
|
|
1331
|
+
const child = spawn3(opts.plan.command, opts.plan.args, {
|
|
1049
1332
|
cwd: opts.cwd,
|
|
1050
1333
|
env,
|
|
1051
1334
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1062,11 +1345,17 @@ var AgentInstance = class _AgentInstance {
|
|
|
1062
1345
|
if (this.exited) {
|
|
1063
1346
|
return;
|
|
1064
1347
|
}
|
|
1348
|
+
this.killed = true;
|
|
1065
1349
|
await this.connection.close().catch(() => void 0);
|
|
1066
1350
|
this.child.kill(signal);
|
|
1067
1351
|
}
|
|
1068
1352
|
};
|
|
1069
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
|
+
|
|
1070
1359
|
// src/core/session.ts
|
|
1071
1360
|
import { customAlphabet } from "nanoid";
|
|
1072
1361
|
|
|
@@ -1074,12 +1363,12 @@ import { customAlphabet } from "nanoid";
|
|
|
1074
1363
|
var HYDRA_COMMANDS = [
|
|
1075
1364
|
{
|
|
1076
1365
|
verb: "title",
|
|
1077
|
-
name: "
|
|
1366
|
+
name: "hydra title",
|
|
1078
1367
|
description: "Regenerate the session title via the agent (or set manually with an arg)"
|
|
1079
1368
|
},
|
|
1080
1369
|
{
|
|
1081
1370
|
verb: "agent",
|
|
1082
|
-
name: "
|
|
1371
|
+
name: "hydra agent",
|
|
1083
1372
|
argsHint: "<agent>",
|
|
1084
1373
|
description: "Swap the agent backing this session, preserving context"
|
|
1085
1374
|
}
|
|
@@ -1096,8 +1385,7 @@ function hydraCommandsAsAdvertised() {
|
|
|
1096
1385
|
var HYDRA_ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
1097
1386
|
var generateHydraId = customAlphabet(HYDRA_ID_ALPHABET, 16);
|
|
1098
1387
|
var HYDRA_SESSION_PREFIX = "hydra_session_";
|
|
1099
|
-
var
|
|
1100
|
-
var COMPACT_EVERY = 200;
|
|
1388
|
+
var DEFAULT_HISTORY_MAX_ENTRIES = 1e3;
|
|
1101
1389
|
var Session = class {
|
|
1102
1390
|
sessionId;
|
|
1103
1391
|
cwd;
|
|
@@ -1139,11 +1427,13 @@ var Session = class {
|
|
|
1139
1427
|
// Bumped by broadcastPromptReceived, cleared by broadcastTurnComplete.
|
|
1140
1428
|
// Drives the mid-turn elapsed counter delivered to fresh attachers.
|
|
1141
1429
|
promptStartedAt;
|
|
1142
|
-
// Counts appends since the last compaction. When it hits
|
|
1430
|
+
// Counts appends since the last compaction. When it hits compactEvery
|
|
1143
1431
|
// we ask the history store to trim the file to the most recent
|
|
1144
|
-
//
|
|
1432
|
+
// historyMaxEntries. Keeps file growth bounded without per-append
|
|
1145
1433
|
// file-size checks.
|
|
1146
1434
|
appendCount = 0;
|
|
1435
|
+
historyMaxEntries;
|
|
1436
|
+
compactEvery;
|
|
1147
1437
|
// Permission requests that have been broadcast to one or more
|
|
1148
1438
|
// clients but have not yet resolved. Replayed to clients that
|
|
1149
1439
|
// attach mid-flight so a late joiner sees the prompt instead of an
|
|
@@ -1200,6 +1490,8 @@ var Session = class {
|
|
|
1200
1490
|
this.firstPromptSeeded = true;
|
|
1201
1491
|
}
|
|
1202
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));
|
|
1203
1495
|
this.updatedAt = Date.now();
|
|
1204
1496
|
this.createdAt = init.createdAt ?? this.updatedAt;
|
|
1205
1497
|
this.lastRecordedAt = this.updatedAt;
|
|
@@ -2038,9 +2330,9 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
2038
2330
|
if (this.historyStore) {
|
|
2039
2331
|
const store = this.historyStore;
|
|
2040
2332
|
void store.append(this.sessionId, entry).catch(() => void 0);
|
|
2041
|
-
if (this.appendCount >=
|
|
2333
|
+
if (this.appendCount >= this.compactEvery) {
|
|
2042
2334
|
this.appendCount = 0;
|
|
2043
|
-
void store.compact(this.sessionId,
|
|
2335
|
+
void store.compact(this.sessionId, this.historyMaxEntries).catch(
|
|
2044
2336
|
() => void 0
|
|
2045
2337
|
);
|
|
2046
2338
|
}
|
|
@@ -2242,7 +2534,7 @@ function firstLine(text, max) {
|
|
|
2242
2534
|
|
|
2243
2535
|
// src/core/session-store.ts
|
|
2244
2536
|
import * as fs4 from "fs/promises";
|
|
2245
|
-
import * as
|
|
2537
|
+
import * as path4 from "path";
|
|
2246
2538
|
import { customAlphabet as customAlphabet2 } from "nanoid";
|
|
2247
2539
|
import { z as z4 } from "zod";
|
|
2248
2540
|
var HYDRA_ID_ALPHABET2 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
@@ -2414,12 +2706,16 @@ function recordFromMemorySession(args) {
|
|
|
2414
2706
|
// src/core/history-store.ts
|
|
2415
2707
|
import * as fs5 from "fs/promises";
|
|
2416
2708
|
var SESSION_ID_PATTERN2 = /^[A-Za-z0-9_-]+$/;
|
|
2417
|
-
var
|
|
2709
|
+
var DEFAULT_MAX_ENTRIES = 1e3;
|
|
2418
2710
|
var HistoryStore = class {
|
|
2419
2711
|
// Serialize writes per session id so appends and rewrites don't
|
|
2420
2712
|
// interleave JSONL lines on disk. The chain swallows errors so one
|
|
2421
2713
|
// failed append doesn't poison every subsequent write.
|
|
2422
2714
|
writeQueues = /* @__PURE__ */ new Map();
|
|
2715
|
+
maxEntries;
|
|
2716
|
+
constructor(options = {}) {
|
|
2717
|
+
this.maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
2718
|
+
}
|
|
2423
2719
|
async append(sessionId, entry) {
|
|
2424
2720
|
if (!SESSION_ID_PATTERN2.test(sessionId)) {
|
|
2425
2721
|
return;
|
|
@@ -2521,8 +2817,8 @@ var HistoryStore = class {
|
|
|
2521
2817
|
recordedAt: obj.recordedAt
|
|
2522
2818
|
});
|
|
2523
2819
|
}
|
|
2524
|
-
if (out.length >
|
|
2525
|
-
return out.slice(-
|
|
2820
|
+
if (out.length > this.maxEntries) {
|
|
2821
|
+
return out.slice(-this.maxEntries);
|
|
2526
2822
|
}
|
|
2527
2823
|
return out;
|
|
2528
2824
|
}
|
|
@@ -2565,13 +2861,40 @@ var HistoryStore = class {
|
|
|
2565
2861
|
|
|
2566
2862
|
// src/tui/history.ts
|
|
2567
2863
|
import { promises as fs6 } from "fs";
|
|
2568
|
-
import * as
|
|
2864
|
+
import * as path5 from "path";
|
|
2569
2865
|
async function saveHistory(file, history) {
|
|
2570
|
-
await fs6.mkdir(
|
|
2866
|
+
await fs6.mkdir(path5.dirname(file), { recursive: true });
|
|
2571
2867
|
const lines = history.map((entry) => JSON.stringify(entry));
|
|
2572
2868
|
await fs6.writeFile(file, lines.length > 0 ? lines.join("\n") + "\n" : "");
|
|
2573
2869
|
}
|
|
2574
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
|
+
|
|
2575
2898
|
// src/core/session-manager.ts
|
|
2576
2899
|
var HYDRA_ID_ALPHABET3 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
2577
2900
|
var generateRawSessionId = customAlphabet3(HYDRA_ID_ALPHABET3, 16);
|
|
@@ -2580,7 +2903,8 @@ var SessionManager = class {
|
|
|
2580
2903
|
this.registry = registry;
|
|
2581
2904
|
this.spawner = spawner ?? ((opts) => AgentInstance.spawn(opts));
|
|
2582
2905
|
this.store = store ?? new SessionStore();
|
|
2583
|
-
this.
|
|
2906
|
+
this.sessionHistoryMaxEntries = options.sessionHistoryMaxEntries ?? 1e3;
|
|
2907
|
+
this.histories = new HistoryStore({ maxEntries: this.sessionHistoryMaxEntries });
|
|
2584
2908
|
this.idleTimeoutMs = options.idleTimeoutMs ?? 0;
|
|
2585
2909
|
this.defaultModels = options.defaultModels ?? {};
|
|
2586
2910
|
}
|
|
@@ -2592,6 +2916,7 @@ var SessionManager = class {
|
|
|
2592
2916
|
histories;
|
|
2593
2917
|
idleTimeoutMs;
|
|
2594
2918
|
defaultModels;
|
|
2919
|
+
sessionHistoryMaxEntries;
|
|
2595
2920
|
// Serialize meta.json read-modify-write operations per session id so
|
|
2596
2921
|
// concurrent snapshot updates (e.g. an agent emitting model + mode
|
|
2597
2922
|
// back-to-back) don't lose writes via interleaved reads.
|
|
@@ -2615,6 +2940,7 @@ var SessionManager = class {
|
|
|
2615
2940
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
2616
2941
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
2617
2942
|
historyStore: this.histories,
|
|
2943
|
+
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
2618
2944
|
currentModel: fresh.initialModel
|
|
2619
2945
|
});
|
|
2620
2946
|
await this.attachManagerHooks(session);
|
|
@@ -2666,11 +2992,16 @@ var SessionManager = class {
|
|
|
2666
2992
|
cwd: params.cwd,
|
|
2667
2993
|
plan
|
|
2668
2994
|
});
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
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
|
+
}
|
|
2674
3005
|
let loadResult;
|
|
2675
3006
|
try {
|
|
2676
3007
|
loadResult = await agent.connection.request(
|
|
@@ -2682,10 +3013,12 @@ var SessionManager = class {
|
|
|
2682
3013
|
}
|
|
2683
3014
|
);
|
|
2684
3015
|
} catch (err) {
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
3016
|
+
process.stderr.write(
|
|
3017
|
+
`session/load failed for upstream ${params.upstreamSessionId} on ${params.agentId} (${err.message}); recovering via import-reseed
|
|
3018
|
+
`
|
|
2688
3019
|
);
|
|
3020
|
+
await agent.kill().catch(() => void 0);
|
|
3021
|
+
return this.doResurrectFromImport(params);
|
|
2689
3022
|
}
|
|
2690
3023
|
const session = new Session({
|
|
2691
3024
|
sessionId: params.hydraSessionId,
|
|
@@ -2699,6 +3032,7 @@ var SessionManager = class {
|
|
|
2699
3032
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
2700
3033
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
2701
3034
|
historyStore: this.histories,
|
|
3035
|
+
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
2702
3036
|
// Prefer what we previously stored from a current_model_update; if
|
|
2703
3037
|
// we never captured one (e.g. old opencode sessions on disk before
|
|
2704
3038
|
// this fix), fall back to the model the agent ships in its
|
|
@@ -2725,15 +3059,16 @@ var SessionManager = class {
|
|
|
2725
3059
|
// so subsequent resurrects of this session use the normal session/load
|
|
2726
3060
|
// path.
|
|
2727
3061
|
async doResurrectFromImport(params) {
|
|
3062
|
+
const cwd = await this.resolveImportCwd(params.cwd);
|
|
2728
3063
|
const fresh = await this.bootstrapAgent({
|
|
2729
3064
|
agentId: params.agentId,
|
|
2730
|
-
cwd
|
|
3065
|
+
cwd,
|
|
2731
3066
|
agentArgs: params.agentArgs,
|
|
2732
3067
|
mcpServers: []
|
|
2733
3068
|
});
|
|
2734
3069
|
const session = new Session({
|
|
2735
3070
|
sessionId: params.hydraSessionId,
|
|
2736
|
-
cwd
|
|
3071
|
+
cwd,
|
|
2737
3072
|
agentId: params.agentId,
|
|
2738
3073
|
agent: fresh.agent,
|
|
2739
3074
|
upstreamSessionId: fresh.upstreamSessionId,
|
|
@@ -2743,6 +3078,7 @@ var SessionManager = class {
|
|
|
2743
3078
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
2744
3079
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
2745
3080
|
historyStore: this.histories,
|
|
3081
|
+
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
2746
3082
|
// Prefer the stored value (set by a previous current_model_update);
|
|
2747
3083
|
// fall back to whatever the agent ships in its session/new response.
|
|
2748
3084
|
currentModel: params.currentModel ?? fresh.initialModel,
|
|
@@ -2756,6 +3092,16 @@ var SessionManager = class {
|
|
|
2756
3092
|
void session.seedFromImport().catch(() => void 0);
|
|
2757
3093
|
return session;
|
|
2758
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
|
+
}
|
|
2759
3105
|
// Bootstrap a fresh agent process: registry resolve → spawn → initialize
|
|
2760
3106
|
// → session/new. Shared by create() and the /hydra agent path so both
|
|
2761
3107
|
// go through the same env / capabilities / error-handling.
|
|
@@ -2776,9 +3122,9 @@ var SessionManager = class {
|
|
|
2776
3122
|
});
|
|
2777
3123
|
try {
|
|
2778
3124
|
await agent.connection.request("initialize", {
|
|
2779
|
-
protocolVersion:
|
|
3125
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
2780
3126
|
clientCapabilities: {},
|
|
2781
|
-
clientInfo: { name: "hydra", version:
|
|
3127
|
+
clientInfo: { name: "hydra", version: HYDRA_VERSION }
|
|
2782
3128
|
});
|
|
2783
3129
|
const newResult = await agent.connection.request(
|
|
2784
3130
|
"session/new",
|
|
@@ -3058,7 +3404,8 @@ var SessionManager = class {
|
|
|
3058
3404
|
await this.writeImportedRecord({
|
|
3059
3405
|
sessionId: existing.sessionId,
|
|
3060
3406
|
bundle,
|
|
3061
|
-
preservedCreatedAt: existing.createdAt
|
|
3407
|
+
preservedCreatedAt: existing.createdAt,
|
|
3408
|
+
cwd: opts.cwd
|
|
3062
3409
|
});
|
|
3063
3410
|
return {
|
|
3064
3411
|
sessionId: existing.sessionId,
|
|
@@ -3067,7 +3414,11 @@ var SessionManager = class {
|
|
|
3067
3414
|
};
|
|
3068
3415
|
}
|
|
3069
3416
|
const newId = `${HYDRA_SESSION_PREFIX}${generateRawSessionId()}`;
|
|
3070
|
-
await this.writeImportedRecord({
|
|
3417
|
+
await this.writeImportedRecord({
|
|
3418
|
+
sessionId: newId,
|
|
3419
|
+
bundle,
|
|
3420
|
+
cwd: opts.cwd
|
|
3421
|
+
});
|
|
3071
3422
|
return {
|
|
3072
3423
|
sessionId: newId,
|
|
3073
3424
|
importedFromSessionId: bundle.session.sessionId,
|
|
@@ -3097,7 +3448,7 @@ var SessionManager = class {
|
|
|
3097
3448
|
upstreamSessionId: "",
|
|
3098
3449
|
importedFromSessionId: args.bundle.session.sessionId,
|
|
3099
3450
|
agentId: args.bundle.session.agentId,
|
|
3100
|
-
cwd: args.bundle.session.cwd,
|
|
3451
|
+
cwd: args.cwd ?? args.bundle.session.cwd,
|
|
3101
3452
|
title: args.bundle.session.title,
|
|
3102
3453
|
currentModel: args.bundle.session.currentModel,
|
|
3103
3454
|
currentMode: args.bundle.session.currentMode,
|
|
@@ -3290,7 +3641,7 @@ function asString(value) {
|
|
|
3290
3641
|
}
|
|
3291
3642
|
async function loadPromptHistorySafely(sessionId) {
|
|
3292
3643
|
try {
|
|
3293
|
-
const raw = await
|
|
3644
|
+
const raw = await fs8.readFile(paths.tuiHistoryFile(sessionId), "utf8");
|
|
3294
3645
|
const out = [];
|
|
3295
3646
|
for (const line of raw.split("\n")) {
|
|
3296
3647
|
if (line.length === 0) {
|
|
@@ -3311,7 +3662,7 @@ async function loadPromptHistorySafely(sessionId) {
|
|
|
3311
3662
|
}
|
|
3312
3663
|
async function historyMtimeIso(sessionId) {
|
|
3313
3664
|
try {
|
|
3314
|
-
const st = await
|
|
3665
|
+
const st = await fs8.stat(paths.historyFile(sessionId));
|
|
3315
3666
|
return new Date(st.mtimeMs).toISOString();
|
|
3316
3667
|
} catch {
|
|
3317
3668
|
return void 0;
|
|
@@ -3319,10 +3670,10 @@ async function historyMtimeIso(sessionId) {
|
|
|
3319
3670
|
}
|
|
3320
3671
|
|
|
3321
3672
|
// src/core/extensions.ts
|
|
3322
|
-
import { spawn as
|
|
3323
|
-
import * as
|
|
3324
|
-
import * as
|
|
3325
|
-
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";
|
|
3326
3677
|
var RESTART_BASE_MS = 1e3;
|
|
3327
3678
|
var RESTART_CAP_MS = 6e4;
|
|
3328
3679
|
var STOP_GRACE_MS = 3e3;
|
|
@@ -3343,7 +3694,7 @@ var ExtensionManager = class {
|
|
|
3343
3694
|
if (!this.context) {
|
|
3344
3695
|
throw new Error("ExtensionManager: setContext must be called before start");
|
|
3345
3696
|
}
|
|
3346
|
-
await
|
|
3697
|
+
await fsp3.mkdir(paths.extensionsDir(), { recursive: true });
|
|
3347
3698
|
await this.reapOrphans();
|
|
3348
3699
|
for (const entry of this.entries.values()) {
|
|
3349
3700
|
if (!entry.config.enabled) {
|
|
@@ -3552,7 +3903,7 @@ var ExtensionManager = class {
|
|
|
3552
3903
|
async reapOrphans() {
|
|
3553
3904
|
let entries;
|
|
3554
3905
|
try {
|
|
3555
|
-
entries = await
|
|
3906
|
+
entries = await fsp3.readdir(paths.extensionsDir());
|
|
3556
3907
|
} catch (err) {
|
|
3557
3908
|
const e = err;
|
|
3558
3909
|
if (e.code === "ENOENT") {
|
|
@@ -3564,10 +3915,10 @@ var ExtensionManager = class {
|
|
|
3564
3915
|
if (!entry.endsWith(".pid")) {
|
|
3565
3916
|
continue;
|
|
3566
3917
|
}
|
|
3567
|
-
const pidPath =
|
|
3918
|
+
const pidPath = path7.join(paths.extensionsDir(), entry);
|
|
3568
3919
|
let pid;
|
|
3569
3920
|
try {
|
|
3570
|
-
const raw = await
|
|
3921
|
+
const raw = await fsp3.readFile(pidPath, "utf8");
|
|
3571
3922
|
const parsed = Number.parseInt(raw.trim(), 10);
|
|
3572
3923
|
if (Number.isInteger(parsed) && parsed > 0) {
|
|
3573
3924
|
pid = parsed;
|
|
@@ -3590,7 +3941,7 @@ var ExtensionManager = class {
|
|
|
3590
3941
|
}
|
|
3591
3942
|
}
|
|
3592
3943
|
}
|
|
3593
|
-
await
|
|
3944
|
+
await fsp3.unlink(pidPath).catch(() => void 0);
|
|
3594
3945
|
}
|
|
3595
3946
|
}
|
|
3596
3947
|
spawn(entry, attempt) {
|
|
@@ -3603,7 +3954,7 @@ var ExtensionManager = class {
|
|
|
3603
3954
|
}
|
|
3604
3955
|
const ext = entry.config;
|
|
3605
3956
|
const command = ext.command.length > 0 ? ext.command : [ext.name];
|
|
3606
|
-
const logStream =
|
|
3957
|
+
const logStream = fs9.createWriteStream(paths.extensionLogFile(ext.name), {
|
|
3607
3958
|
flags: "a"
|
|
3608
3959
|
});
|
|
3609
3960
|
logStream.write(
|
|
@@ -3631,7 +3982,7 @@ var ExtensionManager = class {
|
|
|
3631
3982
|
const args = [...baseArgs, ...ext.args];
|
|
3632
3983
|
let child;
|
|
3633
3984
|
try {
|
|
3634
|
-
child =
|
|
3985
|
+
child = spawn4(cmd, args, {
|
|
3635
3986
|
env,
|
|
3636
3987
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3637
3988
|
detached: false
|
|
@@ -3653,7 +4004,7 @@ var ExtensionManager = class {
|
|
|
3653
4004
|
}
|
|
3654
4005
|
if (typeof child.pid === "number") {
|
|
3655
4006
|
try {
|
|
3656
|
-
|
|
4007
|
+
fs9.writeFileSync(paths.extensionPidFile(ext.name), `${child.pid}
|
|
3657
4008
|
`, {
|
|
3658
4009
|
encoding: "utf8",
|
|
3659
4010
|
mode: 384
|
|
@@ -3678,7 +4029,7 @@ var ExtensionManager = class {
|
|
|
3678
4029
|
});
|
|
3679
4030
|
child.on("exit", (code, signal) => {
|
|
3680
4031
|
try {
|
|
3681
|
-
|
|
4032
|
+
fs9.unlinkSync(paths.extensionPidFile(ext.name));
|
|
3682
4033
|
} catch {
|
|
3683
4034
|
}
|
|
3684
4035
|
logStream.write(
|
|
@@ -3787,7 +4138,7 @@ function constantTimeEqual(a, b) {
|
|
|
3787
4138
|
}
|
|
3788
4139
|
|
|
3789
4140
|
// src/daemon/routes/sessions.ts
|
|
3790
|
-
import * as
|
|
4141
|
+
import * as os3 from "os";
|
|
3791
4142
|
|
|
3792
4143
|
// src/core/bundle.ts
|
|
3793
4144
|
import { z as z5 } from "zod";
|
|
@@ -3857,7 +4208,6 @@ function decodeBundle(raw) {
|
|
|
3857
4208
|
}
|
|
3858
4209
|
|
|
3859
4210
|
// src/daemon/routes/sessions.ts
|
|
3860
|
-
var HYDRA_VERSION = "0.1.0";
|
|
3861
4211
|
function registerSessionRoutes(app, manager, defaults) {
|
|
3862
4212
|
app.get("/v1/sessions", async (request) => {
|
|
3863
4213
|
const query = request.query;
|
|
@@ -3928,7 +4278,7 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
3928
4278
|
history: exported.history,
|
|
3929
4279
|
promptHistory: exported.promptHistory.length > 0 ? exported.promptHistory : void 0,
|
|
3930
4280
|
hydraVersion: HYDRA_VERSION,
|
|
3931
|
-
machine:
|
|
4281
|
+
machine: os3.hostname()
|
|
3932
4282
|
});
|
|
3933
4283
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3934
4284
|
reply.header(
|
|
@@ -3943,6 +4293,14 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
3943
4293
|
reply.code(400).send({ error: "missing bundle" });
|
|
3944
4294
|
return;
|
|
3945
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
|
+
}
|
|
3946
4304
|
let bundle;
|
|
3947
4305
|
try {
|
|
3948
4306
|
bundle = decodeBundle(body.bundle);
|
|
@@ -3955,7 +4313,8 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
3955
4313
|
}
|
|
3956
4314
|
try {
|
|
3957
4315
|
const result = await manager.importBundle(bundle, {
|
|
3958
|
-
replace: body.replace === true
|
|
4316
|
+
replace: body.replace === true,
|
|
4317
|
+
...cwdOverride !== void 0 ? { cwd: cwdOverride } : {}
|
|
3959
4318
|
});
|
|
3960
4319
|
reply.code(201).send(result);
|
|
3961
4320
|
} catch (err) {
|
|
@@ -4266,8 +4625,6 @@ function wsToMessageStream(ws) {
|
|
|
4266
4625
|
}
|
|
4267
4626
|
|
|
4268
4627
|
// src/daemon/acp-ws.ts
|
|
4269
|
-
var HYDRA_VERSION2 = "0.1.0";
|
|
4270
|
-
var HYDRA_PROTOCOL_VERSION = 1;
|
|
4271
4628
|
function registerAcpWsEndpoint(app, deps) {
|
|
4272
4629
|
app.get("/acp", { websocket: true }, (socket, request) => {
|
|
4273
4630
|
const token = tokenFromUpgradeRequest({
|
|
@@ -4530,8 +4887,8 @@ function buildResponseMeta(session) {
|
|
|
4530
4887
|
}
|
|
4531
4888
|
function buildInitializeResult() {
|
|
4532
4889
|
return {
|
|
4533
|
-
protocolVersion:
|
|
4534
|
-
agentInfo: { name: "hydra", version:
|
|
4890
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
4891
|
+
agentInfo: { name: "hydra", version: HYDRA_VERSION },
|
|
4535
4892
|
agentCapabilities: {
|
|
4536
4893
|
// hydra is a transparent proxy: prompt blocks and MCP server configs are
|
|
4537
4894
|
// forwarded to the underlying agent unchanged. We claim the union of
|
|
@@ -4570,14 +4927,13 @@ function bindClientToSession(connection, session, state, clientInfo) {
|
|
|
4570
4927
|
}
|
|
4571
4928
|
|
|
4572
4929
|
// src/daemon/server.ts
|
|
4573
|
-
var HYDRA_VERSION3 = "0.1.0";
|
|
4574
4930
|
async function startDaemon(config) {
|
|
4575
4931
|
ensureLoopbackOrTls(config);
|
|
4576
4932
|
const httpsOptions = config.daemon.tls ? {
|
|
4577
|
-
key: await
|
|
4578
|
-
cert: await
|
|
4933
|
+
key: await fsp4.readFile(config.daemon.tls.key),
|
|
4934
|
+
cert: await fsp4.readFile(config.daemon.tls.cert)
|
|
4579
4935
|
} : void 0;
|
|
4580
|
-
await
|
|
4936
|
+
await fsp4.mkdir(paths.home(), { recursive: true });
|
|
4581
4937
|
const { stream: logStream, fileStream } = await buildLogStream(
|
|
4582
4938
|
config.daemon.logLevel
|
|
4583
4939
|
);
|
|
@@ -4586,12 +4942,18 @@ async function startDaemon(config) {
|
|
|
4586
4942
|
level: config.daemon.logLevel,
|
|
4587
4943
|
stream: logStream
|
|
4588
4944
|
},
|
|
4589
|
-
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
|
|
4590
4949
|
});
|
|
4591
4950
|
await app.register(websocketPlugin);
|
|
4592
4951
|
setBinaryInstallLogger((msg) => {
|
|
4593
4952
|
app.log.info(msg);
|
|
4594
4953
|
});
|
|
4954
|
+
setNpmInstallLogger((msg) => {
|
|
4955
|
+
app.log.info(msg);
|
|
4956
|
+
});
|
|
4595
4957
|
const auth = bearerAuth({ config });
|
|
4596
4958
|
app.addHook("onRequest", async (request, reply) => {
|
|
4597
4959
|
if (request.routeOptions.config?.skipAuth) {
|
|
@@ -4603,12 +4965,14 @@ async function startDaemon(config) {
|
|
|
4603
4965
|
await auth(request, reply);
|
|
4604
4966
|
});
|
|
4605
4967
|
const registry = new Registry(config);
|
|
4606
|
-
const
|
|
4968
|
+
const spawner = (opts) => AgentInstance.spawn({ ...opts, stderrTailBytes: config.daemon.agentStderrTailBytes });
|
|
4969
|
+
const manager = new SessionManager(registry, spawner, void 0, {
|
|
4607
4970
|
idleTimeoutMs: config.daemon.sessionIdleTimeoutSeconds * 1e3,
|
|
4608
|
-
defaultModels: config.defaultModels
|
|
4971
|
+
defaultModels: config.defaultModels,
|
|
4972
|
+
sessionHistoryMaxEntries: config.daemon.sessionHistoryMaxEntries
|
|
4609
4973
|
});
|
|
4610
4974
|
const extensions = new ExtensionManager(extensionList(config));
|
|
4611
|
-
registerHealthRoutes(app,
|
|
4975
|
+
registerHealthRoutes(app, HYDRA_VERSION);
|
|
4612
4976
|
registerSessionRoutes(app, manager, {
|
|
4613
4977
|
agentId: config.defaultAgent,
|
|
4614
4978
|
cwd: config.defaultCwd
|
|
@@ -4627,8 +4991,8 @@ async function startDaemon(config) {
|
|
|
4627
4991
|
await app.listen({ host: config.daemon.host, port: config.daemon.port });
|
|
4628
4992
|
const address = app.server.address();
|
|
4629
4993
|
const boundPort = address && typeof address === "object" ? address.port : config.daemon.port;
|
|
4630
|
-
await
|
|
4631
|
-
await
|
|
4994
|
+
await fsp4.mkdir(paths.home(), { recursive: true });
|
|
4995
|
+
await fsp4.writeFile(
|
|
4632
4996
|
paths.pidFile(),
|
|
4633
4997
|
JSON.stringify({
|
|
4634
4998
|
pid: process.pid,
|
|
@@ -4654,9 +5018,10 @@ async function startDaemon(config) {
|
|
|
4654
5018
|
await manager.closeAll();
|
|
4655
5019
|
await manager.flushMetaWrites();
|
|
4656
5020
|
setBinaryInstallLogger(null);
|
|
5021
|
+
setNpmInstallLogger(null);
|
|
4657
5022
|
await app.close();
|
|
4658
5023
|
try {
|
|
4659
|
-
|
|
5024
|
+
fs10.unlinkSync(paths.pidFile());
|
|
4660
5025
|
} catch {
|
|
4661
5026
|
}
|
|
4662
5027
|
try {
|