@hydra-acp/cli 0.1.50 → 0.1.52
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 +46 -50
- package/dist/cli.js +7562 -6908
- package/dist/index.d.ts +51 -0
- package/dist/index.js +668 -172
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -184,7 +184,13 @@ var DaemonConfig = z.object({
|
|
|
184
184
|
// tunnel (ngrok) or VPN (Tailscale) under a different name. The
|
|
185
185
|
// `--host` flag on `share` overrides this; omitting both falls
|
|
186
186
|
// back to `daemon.host`, then to "127.0.0.1" with a stderr warning.
|
|
187
|
-
publicHost: z.string().optional()
|
|
187
|
+
publicHost: z.string().optional(),
|
|
188
|
+
// How often (minutes) the daemon runs `agent sync` against every
|
|
189
|
+
// installed (non-uvx) agent in the background, picking up sessions
|
|
190
|
+
// created outside hydra so the picker can resurrect them. Spawns
|
|
191
|
+
// are staggered across the window — N agents on a 60-minute interval
|
|
192
|
+
// mean one agent spawn every 60/N minutes. Set 0 to disable entirely.
|
|
193
|
+
agentSyncIntervalMinutes: z.number().nonnegative().default(60)
|
|
188
194
|
});
|
|
189
195
|
var RegistryConfig = z.object({
|
|
190
196
|
url: z.string().url().default(REGISTRY_URL_DEFAULT),
|
|
@@ -387,6 +393,7 @@ function expandHome(p) {
|
|
|
387
393
|
|
|
388
394
|
// src/core/registry.ts
|
|
389
395
|
import * as fs4 from "fs/promises";
|
|
396
|
+
import * as path4 from "path";
|
|
390
397
|
import { z as z2 } from "zod";
|
|
391
398
|
|
|
392
399
|
// src/core/binary-install.ts
|
|
@@ -554,10 +561,10 @@ async function downloadTo(args) {
|
|
|
554
561
|
logSink(formatProgress(args.agentId, received, total));
|
|
555
562
|
}
|
|
556
563
|
});
|
|
557
|
-
await new Promise((
|
|
564
|
+
await new Promise((resolve4, reject) => {
|
|
558
565
|
nodeStream.on("error", reject);
|
|
559
566
|
out.on("error", reject);
|
|
560
|
-
out.on("finish", () =>
|
|
567
|
+
out.on("finish", () => resolve4());
|
|
561
568
|
nodeStream.pipe(out);
|
|
562
569
|
});
|
|
563
570
|
logSink(formatProgress(
|
|
@@ -609,14 +616,14 @@ async function extract(archivePath, dest) {
|
|
|
609
616
|
throw new Error(`Unsupported archive format: ${archivePath}`);
|
|
610
617
|
}
|
|
611
618
|
function run(cmd, args) {
|
|
612
|
-
return new Promise((
|
|
619
|
+
return new Promise((resolve4, reject) => {
|
|
613
620
|
const child = spawn(cmd, args, {
|
|
614
621
|
stdio: ["ignore", "ignore", "inherit"]
|
|
615
622
|
});
|
|
616
623
|
child.on("error", reject);
|
|
617
624
|
child.on("exit", (code, signal) => {
|
|
618
625
|
if (code === 0) {
|
|
619
|
-
|
|
626
|
+
resolve4();
|
|
620
627
|
return;
|
|
621
628
|
}
|
|
622
629
|
reject(
|
|
@@ -628,11 +635,11 @@ function run(cmd, args) {
|
|
|
628
635
|
});
|
|
629
636
|
}
|
|
630
637
|
async function hasCommand(name) {
|
|
631
|
-
return new Promise((
|
|
638
|
+
return new Promise((resolve4) => {
|
|
632
639
|
const finder = process.platform === "win32" ? "where" : "which";
|
|
633
640
|
const child = spawn(finder, [name], { stdio: "ignore" });
|
|
634
|
-
child.on("error", () =>
|
|
635
|
-
child.on("exit", (code) =>
|
|
641
|
+
child.on("error", () => resolve4(false));
|
|
642
|
+
child.on("exit", (code) => resolve4(code === 0));
|
|
636
643
|
});
|
|
637
644
|
}
|
|
638
645
|
async function fileExists(p) {
|
|
@@ -759,7 +766,7 @@ function runNpmInstall(args) {
|
|
|
759
766
|
}
|
|
760
767
|
async function runNpmInstallOnce(args, attempt) {
|
|
761
768
|
try {
|
|
762
|
-
await new Promise((
|
|
769
|
+
await new Promise((resolve4, reject) => {
|
|
763
770
|
const registryArgs = args.registry ? ["--registry", args.registry] : [];
|
|
764
771
|
let child;
|
|
765
772
|
try {
|
|
@@ -801,7 +808,7 @@ async function runNpmInstallOnce(args, attempt) {
|
|
|
801
808
|
});
|
|
802
809
|
child.on("exit", (code, signal) => {
|
|
803
810
|
if (code === 0) {
|
|
804
|
-
|
|
811
|
+
resolve4();
|
|
805
812
|
return;
|
|
806
813
|
}
|
|
807
814
|
const reason = code !== null ? `exit code ${code}` : `signal ${signal ?? "unknown"}`;
|
|
@@ -988,6 +995,13 @@ var Registry = class {
|
|
|
988
995
|
await this.writeDiskCache(fresh);
|
|
989
996
|
return fresh.data;
|
|
990
997
|
}
|
|
998
|
+
// Epoch ms of the last successful registry fetch (in-memory or
|
|
999
|
+
// disk). Returns undefined before load()/refresh() has populated the
|
|
1000
|
+
// cache. Used by `/v1/agents` to surface "synced N minutes ago" in
|
|
1001
|
+
// the CLI without exposing the full cache shape.
|
|
1002
|
+
lastFetchedAt() {
|
|
1003
|
+
return this.cache?.fetchedAt;
|
|
1004
|
+
}
|
|
991
1005
|
async getAgent(id) {
|
|
992
1006
|
const doc = await this.load();
|
|
993
1007
|
const exact = doc.agents.find((a) => a.id === id);
|
|
@@ -1074,6 +1088,46 @@ function npxPackageBasename(agent) {
|
|
|
1074
1088
|
const atIdx = afterSlash.lastIndexOf("@");
|
|
1075
1089
|
return atIdx <= 0 ? afterSlash : afterSlash.slice(0, atIdx);
|
|
1076
1090
|
}
|
|
1091
|
+
async function agentInstallState(agent) {
|
|
1092
|
+
const platformKey = currentPlatformKey();
|
|
1093
|
+
if (!platformKey) {
|
|
1094
|
+
return "no";
|
|
1095
|
+
}
|
|
1096
|
+
const version = agent.version ?? "current";
|
|
1097
|
+
if (agent.distribution.binary) {
|
|
1098
|
+
const target = pickBinaryTarget(agent.distribution.binary, platformKey);
|
|
1099
|
+
if (target?.cmd) {
|
|
1100
|
+
const cmdPath = path4.resolve(
|
|
1101
|
+
paths.agentInstallDir(agent.id, platformKey, version),
|
|
1102
|
+
target.cmd
|
|
1103
|
+
);
|
|
1104
|
+
if (await fileExists3(cmdPath)) {
|
|
1105
|
+
return "yes";
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
if (agent.distribution.npx) {
|
|
1110
|
+
const npx = agent.distribution.npx;
|
|
1111
|
+
const bin = npx.bin ?? npxPackageBasename(agent) ?? npx.package;
|
|
1112
|
+
const installDir = paths.agentNpmInstallDir(agent.id, platformKey, version);
|
|
1113
|
+
const binPath = path4.join(installDir, "node_modules", ".bin", bin);
|
|
1114
|
+
if (await fileExists3(binPath)) {
|
|
1115
|
+
return "yes";
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
if (!agent.distribution.npx && !agent.distribution.binary && agent.distribution.uvx) {
|
|
1119
|
+
return "lazy";
|
|
1120
|
+
}
|
|
1121
|
+
return "no";
|
|
1122
|
+
}
|
|
1123
|
+
async function fileExists3(p) {
|
|
1124
|
+
try {
|
|
1125
|
+
await fs4.access(p);
|
|
1126
|
+
return true;
|
|
1127
|
+
} catch {
|
|
1128
|
+
return false;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1077
1131
|
async function planSpawn(agent, callerArgs = [], options = {}) {
|
|
1078
1132
|
const version = agent.version ?? "current";
|
|
1079
1133
|
if (agent.distribution.npx) {
|
|
@@ -1689,13 +1743,13 @@ function ndjsonStreamFromStdio(stdout, stdin) {
|
|
|
1689
1743
|
throw new Error("stream is closed");
|
|
1690
1744
|
}
|
|
1691
1745
|
const line = JSON.stringify(message) + "\n";
|
|
1692
|
-
await new Promise((
|
|
1746
|
+
await new Promise((resolve4, reject) => {
|
|
1693
1747
|
stdin.write(line, (err) => {
|
|
1694
1748
|
if (err) {
|
|
1695
1749
|
reject(err);
|
|
1696
1750
|
return;
|
|
1697
1751
|
}
|
|
1698
|
-
|
|
1752
|
+
resolve4();
|
|
1699
1753
|
});
|
|
1700
1754
|
});
|
|
1701
1755
|
},
|
|
@@ -1787,9 +1841,9 @@ var JsonRpcConnection = class _JsonRpcConnection {
|
|
|
1787
1841
|
}
|
|
1788
1842
|
const id = nanoid();
|
|
1789
1843
|
const message = { jsonrpc: "2.0", id, method, params };
|
|
1790
|
-
const response = new Promise((
|
|
1844
|
+
const response = new Promise((resolve4, reject) => {
|
|
1791
1845
|
this.pending.set(id, {
|
|
1792
|
-
resolve: (result) =>
|
|
1846
|
+
resolve: (result) => resolve4(result),
|
|
1793
1847
|
reject
|
|
1794
1848
|
});
|
|
1795
1849
|
this.stream.send(message).catch((err) => {
|
|
@@ -2216,14 +2270,14 @@ var SessionStreamBuffer = class {
|
|
|
2216
2270
|
if (cap === 0) {
|
|
2217
2271
|
return Promise.resolve("timeout");
|
|
2218
2272
|
}
|
|
2219
|
-
return new Promise((
|
|
2273
|
+
return new Promise((resolve4) => {
|
|
2220
2274
|
const waiter = {
|
|
2221
2275
|
resolve: (outcome) => {
|
|
2222
2276
|
if (waiter.timer !== void 0) {
|
|
2223
2277
|
clearTimeout(waiter.timer);
|
|
2224
2278
|
waiter.timer = void 0;
|
|
2225
2279
|
}
|
|
2226
|
-
|
|
2280
|
+
resolve4(outcome);
|
|
2227
2281
|
},
|
|
2228
2282
|
timer: setTimeout(() => {
|
|
2229
2283
|
const idx = this.waiters.indexOf(waiter);
|
|
@@ -2231,7 +2285,7 @@ var SessionStreamBuffer = class {
|
|
|
2231
2285
|
this.waiters.splice(idx, 1);
|
|
2232
2286
|
}
|
|
2233
2287
|
waiter.timer = void 0;
|
|
2234
|
-
|
|
2288
|
+
resolve4("timeout");
|
|
2235
2289
|
}, cap)
|
|
2236
2290
|
};
|
|
2237
2291
|
this.waiters.push(waiter);
|
|
@@ -2434,8 +2488,8 @@ var SessionStreamBuffer = class {
|
|
|
2434
2488
|
return out;
|
|
2435
2489
|
}
|
|
2436
2490
|
scheduleFileWrite(chunk) {
|
|
2437
|
-
const
|
|
2438
|
-
if (
|
|
2491
|
+
const path14 = this.filePath;
|
|
2492
|
+
if (path14 === void 0) {
|
|
2439
2493
|
return;
|
|
2440
2494
|
}
|
|
2441
2495
|
if (this.fileCapReached) {
|
|
@@ -2450,7 +2504,7 @@ var SessionStreamBuffer = class {
|
|
|
2450
2504
|
const slice = chunk.length <= remaining ? chunk : chunk.subarray(0, remaining);
|
|
2451
2505
|
this.fileBytesWritten += slice.length;
|
|
2452
2506
|
const willHitCap = this.fileBytesWritten >= this.fileCapBytes;
|
|
2453
|
-
this.fileWriteChain = this.fileWriteChain.then(() => fsp3.appendFile(
|
|
2507
|
+
this.fileWriteChain = this.fileWriteChain.then(() => fsp3.appendFile(path14, slice)).catch((err) => {
|
|
2454
2508
|
this.logWriteError?.(err);
|
|
2455
2509
|
});
|
|
2456
2510
|
if (willHitCap && !this.fileCapReached) {
|
|
@@ -2863,7 +2917,7 @@ var Session = class {
|
|
|
2863
2917
|
const claimIdx = i;
|
|
2864
2918
|
const claimEnvelope = envelope;
|
|
2865
2919
|
const claimOriginatedBy = new Set(originatedBy);
|
|
2866
|
-
await new Promise((
|
|
2920
|
+
await new Promise((resolve4) => {
|
|
2867
2921
|
const timer = setTimeout(() => {
|
|
2868
2922
|
if (this.pendingClaims.delete(token)) {
|
|
2869
2923
|
this.broadcastQueueNotification(
|
|
@@ -2874,14 +2928,14 @@ var Session = class {
|
|
|
2874
2928
|
claimEnvelope,
|
|
2875
2929
|
/* @__PURE__ */ new Set([...claimOriginatedBy, t.name]),
|
|
2876
2930
|
claimIdx + 1
|
|
2877
|
-
).then(
|
|
2931
|
+
).then(resolve4);
|
|
2878
2932
|
}
|
|
2879
2933
|
}, TRANSFORMER_CLAIM_TIMEOUT_MS);
|
|
2880
2934
|
if (typeof timer.unref === "function") {
|
|
2881
2935
|
timer.unref();
|
|
2882
2936
|
}
|
|
2883
2937
|
this.pendingClaims.set(token, {
|
|
2884
|
-
resolve: () =>
|
|
2938
|
+
resolve: () => resolve4(),
|
|
2885
2939
|
timer,
|
|
2886
2940
|
transformerName: t.name,
|
|
2887
2941
|
method: "session/update",
|
|
@@ -3721,7 +3775,7 @@ var Session = class {
|
|
|
3721
3775
|
const claimIdx = i;
|
|
3722
3776
|
const claimEnvelope = envelope;
|
|
3723
3777
|
const claimOriginatedBy = new Set(originatedBy);
|
|
3724
|
-
return new Promise((
|
|
3778
|
+
return new Promise((resolve4) => {
|
|
3725
3779
|
const timer = setTimeout(() => {
|
|
3726
3780
|
if (this.pendingClaims.delete(token)) {
|
|
3727
3781
|
this.broadcastQueueNotification(
|
|
@@ -3733,14 +3787,14 @@ var Session = class {
|
|
|
3733
3787
|
claimEnvelope,
|
|
3734
3788
|
/* @__PURE__ */ new Set([...claimOriginatedBy, t.name]),
|
|
3735
3789
|
claimIdx + 1
|
|
3736
|
-
).then(
|
|
3790
|
+
).then(resolve4).catch(() => resolve4(defaultStopPayload(method)));
|
|
3737
3791
|
}
|
|
3738
3792
|
}, TRANSFORMER_CLAIM_TIMEOUT_MS);
|
|
3739
3793
|
if (typeof timer.unref === "function") {
|
|
3740
3794
|
timer.unref();
|
|
3741
3795
|
}
|
|
3742
3796
|
this.pendingClaims.set(token, {
|
|
3743
|
-
resolve:
|
|
3797
|
+
resolve: resolve4,
|
|
3744
3798
|
timer,
|
|
3745
3799
|
transformerName: t.name,
|
|
3746
3800
|
method,
|
|
@@ -4493,12 +4547,12 @@ ${text}
|
|
|
4493
4547
|
} else {
|
|
4494
4548
|
const inList = current ? models.some((m) => m.modelId === current) : true;
|
|
4495
4549
|
const lines = models.map((m) => {
|
|
4496
|
-
const marker = m.modelId === current ? "
|
|
4550
|
+
const marker = m.modelId === current ? "\u25B6 " : " ";
|
|
4497
4551
|
const desc = m.name && m.name !== m.modelId ? ` ${m.name}` : "";
|
|
4498
|
-
return `${m.modelId}${
|
|
4552
|
+
return `${marker}${m.modelId}${desc}`;
|
|
4499
4553
|
});
|
|
4500
4554
|
if (!inList && current) {
|
|
4501
|
-
lines.unshift(
|
|
4555
|
+
lines.unshift(`\u25B6 ${current}`);
|
|
4502
4556
|
}
|
|
4503
4557
|
body = lines.join("\n");
|
|
4504
4558
|
}
|
|
@@ -4998,12 +5052,12 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
4998
5052
|
this.clients.clear();
|
|
4999
5053
|
if (this.streamBuffer !== void 0) {
|
|
5000
5054
|
const buf = this.streamBuffer;
|
|
5001
|
-
const
|
|
5055
|
+
const path14 = this.streamFilePath;
|
|
5002
5056
|
this.streamBuffer = void 0;
|
|
5003
5057
|
this.streamFilePath = void 0;
|
|
5004
5058
|
buf.close();
|
|
5005
|
-
if (
|
|
5006
|
-
void buf.drainFileWrites().then(() => fsp4.unlink(
|
|
5059
|
+
if (path14 !== void 0) {
|
|
5060
|
+
void buf.drainFileWrites().then(() => fsp4.unlink(path14).catch(() => void 0));
|
|
5007
5061
|
}
|
|
5008
5062
|
}
|
|
5009
5063
|
for (const handler of this.closeHandlers) {
|
|
@@ -5165,7 +5219,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
5165
5219
|
}
|
|
5166
5220
|
const clientParams = this.rewriteForClient(params);
|
|
5167
5221
|
const toolCallId = extractToolCallId(clientParams);
|
|
5168
|
-
return new Promise((
|
|
5222
|
+
return new Promise((resolve4, reject) => {
|
|
5169
5223
|
let settled = false;
|
|
5170
5224
|
const outbound = [];
|
|
5171
5225
|
const entry = { addClient: sendTo };
|
|
@@ -5204,7 +5258,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
5204
5258
|
update
|
|
5205
5259
|
}).catch(() => void 0);
|
|
5206
5260
|
}
|
|
5207
|
-
|
|
5261
|
+
resolve4(result);
|
|
5208
5262
|
});
|
|
5209
5263
|
}).catch((err) => {
|
|
5210
5264
|
settle(() => reject(err));
|
|
@@ -5220,14 +5274,14 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
5220
5274
|
// in flight, but doesn't emit prompt_queue_* broadcasts — clients
|
|
5221
5275
|
// shouldn't see hydra's housekeeping in their chip list.
|
|
5222
5276
|
async enqueuePrompt(task) {
|
|
5223
|
-
return new Promise((
|
|
5277
|
+
return new Promise((resolve4, reject) => {
|
|
5224
5278
|
const entry = {
|
|
5225
5279
|
kind: "internal",
|
|
5226
5280
|
messageId: generateMessageId(),
|
|
5227
5281
|
enqueuedAt: Date.now(),
|
|
5228
5282
|
cancelled: false,
|
|
5229
5283
|
task,
|
|
5230
|
-
resolve:
|
|
5284
|
+
resolve: resolve4,
|
|
5231
5285
|
reject
|
|
5232
5286
|
};
|
|
5233
5287
|
this.promptQueue.push(entry);
|
|
@@ -5246,7 +5300,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
5246
5300
|
if (client.clientInfo?.name) originator.name = client.clientInfo.name;
|
|
5247
5301
|
if (client.clientInfo?.version)
|
|
5248
5302
|
originator.version = client.clientInfo.version;
|
|
5249
|
-
return new Promise((
|
|
5303
|
+
return new Promise((resolve4, reject) => {
|
|
5250
5304
|
const entry = {
|
|
5251
5305
|
kind: "user",
|
|
5252
5306
|
messageId,
|
|
@@ -5255,7 +5309,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
5255
5309
|
prompt: promptArray,
|
|
5256
5310
|
enqueuedAt: Date.now(),
|
|
5257
5311
|
cancelled: false,
|
|
5258
|
-
resolve:
|
|
5312
|
+
resolve: resolve4,
|
|
5259
5313
|
reject
|
|
5260
5314
|
};
|
|
5261
5315
|
this.promptQueue.push(entry);
|
|
@@ -5677,7 +5731,7 @@ function firstLine(text, max) {
|
|
|
5677
5731
|
|
|
5678
5732
|
// src/core/session-store.ts
|
|
5679
5733
|
import * as fs6 from "fs/promises";
|
|
5680
|
-
import * as
|
|
5734
|
+
import * as path5 from "path";
|
|
5681
5735
|
import { customAlphabet as customAlphabet2 } from "nanoid";
|
|
5682
5736
|
import { z as z4 } from "zod";
|
|
5683
5737
|
var HYDRA_ID_ALPHABET2 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
@@ -6051,29 +6105,29 @@ var HistoryStore = class {
|
|
|
6051
6105
|
|
|
6052
6106
|
// src/tui/history.ts
|
|
6053
6107
|
import { promises as fs8 } from "fs";
|
|
6054
|
-
import * as
|
|
6108
|
+
import * as path6 from "path";
|
|
6055
6109
|
async function saveHistory(file, history) {
|
|
6056
|
-
await fs8.mkdir(
|
|
6110
|
+
await fs8.mkdir(path6.dirname(file), { recursive: true });
|
|
6057
6111
|
const lines = history.map((entry) => JSON.stringify(entry));
|
|
6058
6112
|
await fs8.writeFile(file, lines.length > 0 ? lines.join("\n") + "\n" : "");
|
|
6059
6113
|
}
|
|
6060
6114
|
|
|
6061
6115
|
// src/core/hydra-version.ts
|
|
6062
6116
|
import { fileURLToPath } from "url";
|
|
6063
|
-
import * as
|
|
6117
|
+
import * as path7 from "path";
|
|
6064
6118
|
import * as fs9 from "fs";
|
|
6065
6119
|
function resolveVersion() {
|
|
6066
6120
|
try {
|
|
6067
|
-
let dir =
|
|
6121
|
+
let dir = path7.dirname(fileURLToPath(import.meta.url));
|
|
6068
6122
|
for (let i = 0; i < 8; i += 1) {
|
|
6069
|
-
const candidate =
|
|
6123
|
+
const candidate = path7.join(dir, "package.json");
|
|
6070
6124
|
if (fs9.existsSync(candidate)) {
|
|
6071
6125
|
const pkg = JSON.parse(fs9.readFileSync(candidate, "utf8"));
|
|
6072
6126
|
if (typeof pkg.version === "string" && pkg.version.length > 0 && (typeof pkg.name !== "string" || pkg.name.includes("hydra-acp"))) {
|
|
6073
6127
|
return pkg.version;
|
|
6074
6128
|
}
|
|
6075
6129
|
}
|
|
6076
|
-
const parent =
|
|
6130
|
+
const parent = path7.dirname(dir);
|
|
6077
6131
|
if (parent === dir) {
|
|
6078
6132
|
break;
|
|
6079
6133
|
}
|
|
@@ -7547,7 +7601,7 @@ async function historyMtimeIso(sessionId) {
|
|
|
7547
7601
|
import { spawn as spawn4 } from "child_process";
|
|
7548
7602
|
import * as fs11 from "fs";
|
|
7549
7603
|
import * as fsp5 from "fs/promises";
|
|
7550
|
-
import * as
|
|
7604
|
+
import * as path8 from "path";
|
|
7551
7605
|
var RESTART_BASE_MS = 1e3;
|
|
7552
7606
|
var RESTART_CAP_MS = 6e4;
|
|
7553
7607
|
var STOP_GRACE_MS = 3e3;
|
|
@@ -7604,9 +7658,9 @@ var ExtensionManager = class {
|
|
|
7604
7658
|
} catch {
|
|
7605
7659
|
}
|
|
7606
7660
|
tasks.push(
|
|
7607
|
-
new Promise((
|
|
7661
|
+
new Promise((resolve4) => {
|
|
7608
7662
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
7609
|
-
|
|
7663
|
+
resolve4();
|
|
7610
7664
|
return;
|
|
7611
7665
|
}
|
|
7612
7666
|
const timer = setTimeout(() => {
|
|
@@ -7614,11 +7668,11 @@ var ExtensionManager = class {
|
|
|
7614
7668
|
child.kill("SIGKILL");
|
|
7615
7669
|
} catch {
|
|
7616
7670
|
}
|
|
7617
|
-
|
|
7671
|
+
resolve4();
|
|
7618
7672
|
}, STOP_GRACE_MS);
|
|
7619
7673
|
child.on("exit", () => {
|
|
7620
7674
|
clearTimeout(timer);
|
|
7621
|
-
|
|
7675
|
+
resolve4();
|
|
7622
7676
|
});
|
|
7623
7677
|
})
|
|
7624
7678
|
);
|
|
@@ -7726,8 +7780,8 @@ var ExtensionManager = class {
|
|
|
7726
7780
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
7727
7781
|
return;
|
|
7728
7782
|
}
|
|
7729
|
-
const exited = new Promise((
|
|
7730
|
-
entry.exitWaiters.push(
|
|
7783
|
+
const exited = new Promise((resolve4) => {
|
|
7784
|
+
entry.exitWaiters.push(resolve4);
|
|
7731
7785
|
});
|
|
7732
7786
|
try {
|
|
7733
7787
|
child.kill("SIGTERM");
|
|
@@ -7802,7 +7856,7 @@ var ExtensionManager = class {
|
|
|
7802
7856
|
if (!entry.endsWith(".pid")) {
|
|
7803
7857
|
continue;
|
|
7804
7858
|
}
|
|
7805
|
-
const pidPath =
|
|
7859
|
+
const pidPath = path8.join(paths.extensionsDir(), entry);
|
|
7806
7860
|
let pid;
|
|
7807
7861
|
try {
|
|
7808
7862
|
const raw = await fsp5.readFile(pidPath, "utf8");
|
|
@@ -7934,8 +7988,8 @@ var ExtensionManager = class {
|
|
|
7934
7988
|
entry.processToken = void 0;
|
|
7935
7989
|
}
|
|
7936
7990
|
const waiters = entry.exitWaiters.splice(0);
|
|
7937
|
-
for (const
|
|
7938
|
-
|
|
7991
|
+
for (const resolve4 of waiters) {
|
|
7992
|
+
resolve4();
|
|
7939
7993
|
}
|
|
7940
7994
|
if (this.stopping || entry.manuallyStopped) {
|
|
7941
7995
|
try {
|
|
@@ -7983,7 +8037,7 @@ function withCode2(err, code) {
|
|
|
7983
8037
|
import { spawn as spawn5 } from "child_process";
|
|
7984
8038
|
import * as fs12 from "fs";
|
|
7985
8039
|
import * as fsp6 from "fs/promises";
|
|
7986
|
-
import * as
|
|
8040
|
+
import * as path9 from "path";
|
|
7987
8041
|
var RESTART_BASE_MS2 = 1e3;
|
|
7988
8042
|
var RESTART_CAP_MS2 = 6e4;
|
|
7989
8043
|
var STOP_GRACE_MS2 = 3e3;
|
|
@@ -8067,9 +8121,9 @@ var TransformerManager = class {
|
|
|
8067
8121
|
} catch {
|
|
8068
8122
|
}
|
|
8069
8123
|
tasks.push(
|
|
8070
|
-
new Promise((
|
|
8124
|
+
new Promise((resolve4) => {
|
|
8071
8125
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
8072
|
-
|
|
8126
|
+
resolve4();
|
|
8073
8127
|
return;
|
|
8074
8128
|
}
|
|
8075
8129
|
const timer = setTimeout(() => {
|
|
@@ -8077,11 +8131,11 @@ var TransformerManager = class {
|
|
|
8077
8131
|
child.kill("SIGKILL");
|
|
8078
8132
|
} catch {
|
|
8079
8133
|
}
|
|
8080
|
-
|
|
8134
|
+
resolve4();
|
|
8081
8135
|
}, STOP_GRACE_MS2);
|
|
8082
8136
|
child.on("exit", () => {
|
|
8083
8137
|
clearTimeout(timer);
|
|
8084
|
-
|
|
8138
|
+
resolve4();
|
|
8085
8139
|
});
|
|
8086
8140
|
})
|
|
8087
8141
|
);
|
|
@@ -8186,8 +8240,8 @@ var TransformerManager = class {
|
|
|
8186
8240
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
8187
8241
|
return;
|
|
8188
8242
|
}
|
|
8189
|
-
const exited = new Promise((
|
|
8190
|
-
entry.exitWaiters.push(
|
|
8243
|
+
const exited = new Promise((resolve4) => {
|
|
8244
|
+
entry.exitWaiters.push(resolve4);
|
|
8191
8245
|
});
|
|
8192
8246
|
try {
|
|
8193
8247
|
child.kill("SIGTERM");
|
|
@@ -8262,7 +8316,7 @@ var TransformerManager = class {
|
|
|
8262
8316
|
if (!entry.endsWith(".pid")) {
|
|
8263
8317
|
continue;
|
|
8264
8318
|
}
|
|
8265
|
-
const pidPath =
|
|
8319
|
+
const pidPath = path9.join(paths.transformersDir(), entry);
|
|
8266
8320
|
let pid;
|
|
8267
8321
|
try {
|
|
8268
8322
|
const raw = await fsp6.readFile(pidPath, "utf8");
|
|
@@ -8394,8 +8448,8 @@ var TransformerManager = class {
|
|
|
8394
8448
|
entry.processToken = void 0;
|
|
8395
8449
|
}
|
|
8396
8450
|
const waiters = entry.exitWaiters.splice(0);
|
|
8397
|
-
for (const
|
|
8398
|
-
|
|
8451
|
+
for (const resolve4 of waiters) {
|
|
8452
|
+
resolve4();
|
|
8399
8453
|
}
|
|
8400
8454
|
if (this.stopping || entry.manuallyStopped) {
|
|
8401
8455
|
try {
|
|
@@ -8490,7 +8544,7 @@ var ExtensionCommandRegistry = class {
|
|
|
8490
8544
|
|
|
8491
8545
|
// src/core/agent-prune.ts
|
|
8492
8546
|
import * as fsp7 from "fs/promises";
|
|
8493
|
-
import * as
|
|
8547
|
+
import * as path10 from "path";
|
|
8494
8548
|
var logSink3 = (msg) => {
|
|
8495
8549
|
process.stderr.write(msg + "\n");
|
|
8496
8550
|
};
|
|
@@ -8508,7 +8562,7 @@ async function pruneStaleAgentVersions(registry, sessionManager) {
|
|
|
8508
8562
|
desiredByAgent.set(a.id, a.version ?? "current");
|
|
8509
8563
|
}
|
|
8510
8564
|
const activeByAgent = sessionManager.activeAgentVersions();
|
|
8511
|
-
const platformDir =
|
|
8565
|
+
const platformDir = path10.join(paths.agentsDir(), platformKey);
|
|
8512
8566
|
let agentEntries;
|
|
8513
8567
|
try {
|
|
8514
8568
|
agentEntries = await fsp7.readdir(platformDir, { withFileTypes: true });
|
|
@@ -8530,7 +8584,7 @@ async function pruneStaleAgentVersions(registry, sessionManager) {
|
|
|
8530
8584
|
continue;
|
|
8531
8585
|
}
|
|
8532
8586
|
const activeVersions = activeByAgent.get(agentId) ?? /* @__PURE__ */ new Set();
|
|
8533
|
-
const agentDir =
|
|
8587
|
+
const agentDir = path10.join(platformDir, agentId);
|
|
8534
8588
|
let versionEntries;
|
|
8535
8589
|
try {
|
|
8536
8590
|
versionEntries = await fsp7.readdir(agentDir, { withFileTypes: true });
|
|
@@ -8554,7 +8608,7 @@ async function pruneStaleAgentVersions(registry, sessionManager) {
|
|
|
8554
8608
|
if (version.includes(".partial-")) {
|
|
8555
8609
|
continue;
|
|
8556
8610
|
}
|
|
8557
|
-
const versionDir =
|
|
8611
|
+
const versionDir = path10.join(agentDir, version);
|
|
8558
8612
|
try {
|
|
8559
8613
|
await fsp7.rm(versionDir, { recursive: true, force: true });
|
|
8560
8614
|
logSink3(`hydra-acp: pruned stale ${agentId} ${version} (${versionDir})`);
|
|
@@ -8567,9 +8621,75 @@ async function pruneStaleAgentVersions(registry, sessionManager) {
|
|
|
8567
8621
|
}
|
|
8568
8622
|
}
|
|
8569
8623
|
|
|
8624
|
+
// src/core/agent-sync-scheduler.ts
|
|
8625
|
+
function startAgentSyncScheduler(opts) {
|
|
8626
|
+
let timer;
|
|
8627
|
+
let stopped = false;
|
|
8628
|
+
let cursor = 0;
|
|
8629
|
+
const log = (level, msg) => {
|
|
8630
|
+
if (!opts.logger) {
|
|
8631
|
+
return;
|
|
8632
|
+
}
|
|
8633
|
+
opts.logger[level](`agent-sync: ${msg}`);
|
|
8634
|
+
};
|
|
8635
|
+
const tick = async () => {
|
|
8636
|
+
const installed = [];
|
|
8637
|
+
try {
|
|
8638
|
+
const doc = await opts.registry.load();
|
|
8639
|
+
for (const a of doc.agents) {
|
|
8640
|
+
const state = await agentInstallState(a);
|
|
8641
|
+
if (state === "yes") {
|
|
8642
|
+
installed.push(a.id);
|
|
8643
|
+
}
|
|
8644
|
+
}
|
|
8645
|
+
} catch (err) {
|
|
8646
|
+
log("warn", `registry load failed: ${err.message}`);
|
|
8647
|
+
return opts.intervalMs;
|
|
8648
|
+
}
|
|
8649
|
+
if (installed.length === 0) {
|
|
8650
|
+
return opts.intervalMs;
|
|
8651
|
+
}
|
|
8652
|
+
const idx = cursor % installed.length;
|
|
8653
|
+
cursor = (cursor + 1) % installed.length;
|
|
8654
|
+
const agentId = installed[idx];
|
|
8655
|
+
try {
|
|
8656
|
+
const { synced, skipped } = await opts.manager.syncFromAgent(agentId);
|
|
8657
|
+
log(
|
|
8658
|
+
"info",
|
|
8659
|
+
`${agentId}: synced ${synced.length}, skipped ${skipped}`
|
|
8660
|
+
);
|
|
8661
|
+
} catch (err) {
|
|
8662
|
+
log("warn", `${agentId}: ${err.message}`);
|
|
8663
|
+
}
|
|
8664
|
+
return Math.max(1, Math.floor(opts.intervalMs / installed.length));
|
|
8665
|
+
};
|
|
8666
|
+
const scheduleNext = (delayMs) => {
|
|
8667
|
+
if (stopped) {
|
|
8668
|
+
return;
|
|
8669
|
+
}
|
|
8670
|
+
timer = setTimeout(() => {
|
|
8671
|
+
tick().then((nextDelay) => {
|
|
8672
|
+
scheduleNext(nextDelay);
|
|
8673
|
+
}).catch((err) => {
|
|
8674
|
+
log("warn", `tick crashed: ${err.message}`);
|
|
8675
|
+
scheduleNext(opts.intervalMs);
|
|
8676
|
+
});
|
|
8677
|
+
}, delayMs);
|
|
8678
|
+
timer.unref();
|
|
8679
|
+
};
|
|
8680
|
+
scheduleNext(opts.intervalMs);
|
|
8681
|
+
return () => {
|
|
8682
|
+
stopped = true;
|
|
8683
|
+
if (timer) {
|
|
8684
|
+
clearTimeout(timer);
|
|
8685
|
+
timer = void 0;
|
|
8686
|
+
}
|
|
8687
|
+
};
|
|
8688
|
+
}
|
|
8689
|
+
|
|
8570
8690
|
// src/core/session-tokens.ts
|
|
8571
8691
|
import * as fs13 from "fs/promises";
|
|
8572
|
-
import * as
|
|
8692
|
+
import * as path11 from "path";
|
|
8573
8693
|
import { createHash, randomBytes, timingSafeEqual } from "crypto";
|
|
8574
8694
|
var TOKEN_PREFIX = "hydra_session_";
|
|
8575
8695
|
var DEFAULT_TTL_SEC = 60 * 60 * 24 * 30;
|
|
@@ -8577,7 +8697,7 @@ var ID_LENGTH = 12;
|
|
|
8577
8697
|
var TOKEN_BYTES = 32;
|
|
8578
8698
|
var WRITE_DEBOUNCE_MS = 50;
|
|
8579
8699
|
function tokensFilePath() {
|
|
8580
|
-
return
|
|
8700
|
+
return path11.join(paths.home(), "session-tokens.json");
|
|
8581
8701
|
}
|
|
8582
8702
|
function sha256Hex(input) {
|
|
8583
8703
|
return createHash("sha256").update(input).digest("hex");
|
|
@@ -10284,15 +10404,20 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
10284
10404
|
function registerAgentRoutes(app, registry, manager, opts = {}) {
|
|
10285
10405
|
app.get("/v1/agents", async () => {
|
|
10286
10406
|
const doc = await registry.load();
|
|
10287
|
-
|
|
10288
|
-
|
|
10289
|
-
agents: doc.agents.map((a) => ({
|
|
10407
|
+
const agents = await Promise.all(
|
|
10408
|
+
doc.agents.map(async (a) => ({
|
|
10290
10409
|
id: a.id,
|
|
10291
10410
|
name: a.name,
|
|
10292
10411
|
version: a.version,
|
|
10293
10412
|
description: a.description,
|
|
10294
|
-
distributions: Object.keys(a.distribution)
|
|
10413
|
+
distributions: Object.keys(a.distribution),
|
|
10414
|
+
installed: await agentInstallState(a)
|
|
10295
10415
|
}))
|
|
10416
|
+
);
|
|
10417
|
+
return {
|
|
10418
|
+
version: doc.version,
|
|
10419
|
+
fetchedAt: registry.lastFetchedAt(),
|
|
10420
|
+
agents
|
|
10296
10421
|
};
|
|
10297
10422
|
});
|
|
10298
10423
|
app.get("/v1/registry", async () => {
|
|
@@ -10611,12 +10736,12 @@ import { z as z6 } from "zod";
|
|
|
10611
10736
|
|
|
10612
10737
|
// src/core/password.ts
|
|
10613
10738
|
import * as fs14 from "fs/promises";
|
|
10614
|
-
import * as
|
|
10739
|
+
import * as path12 from "path";
|
|
10615
10740
|
import { randomBytes as randomBytes2, scrypt, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
10616
10741
|
import { promisify } from "util";
|
|
10617
10742
|
var scryptAsync = promisify(scrypt);
|
|
10618
10743
|
function passwordHashPath() {
|
|
10619
|
-
return
|
|
10744
|
+
return path12.join(paths.home(), "password-hash");
|
|
10620
10745
|
}
|
|
10621
10746
|
var DEFAULT_N = 1 << 15;
|
|
10622
10747
|
var MAX_MEM = 128 * 1024 * 1024;
|
|
@@ -10805,13 +10930,13 @@ function wsToMessageStream(ws) {
|
|
|
10805
10930
|
throw new Error("ws is closed");
|
|
10806
10931
|
}
|
|
10807
10932
|
const text = JSON.stringify(message);
|
|
10808
|
-
await new Promise((
|
|
10933
|
+
await new Promise((resolve4, reject) => {
|
|
10809
10934
|
ws.send(text, (err) => {
|
|
10810
10935
|
if (err) {
|
|
10811
10936
|
reject(err);
|
|
10812
10937
|
return;
|
|
10813
10938
|
}
|
|
10814
|
-
|
|
10939
|
+
resolve4();
|
|
10815
10940
|
});
|
|
10816
10941
|
});
|
|
10817
10942
|
},
|
|
@@ -10833,7 +10958,7 @@ function wsToMessageStream(ws) {
|
|
|
10833
10958
|
|
|
10834
10959
|
// src/daemon/acp-ws.ts
|
|
10835
10960
|
import * as os4 from "os";
|
|
10836
|
-
import * as
|
|
10961
|
+
import * as path13 from "path";
|
|
10837
10962
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
10838
10963
|
function registerAcpWsEndpoint(app, deps) {
|
|
10839
10964
|
app.get("/acp", { websocket: true }, async (socket, request) => {
|
|
@@ -10916,6 +11041,50 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
10916
11041
|
registry.clear(processIdentity.name);
|
|
10917
11042
|
});
|
|
10918
11043
|
}
|
|
11044
|
+
if (processIdentity && deps.extensionMcp) {
|
|
11045
|
+
const mcpRegistry = deps.extensionMcp;
|
|
11046
|
+
connection.onRequest("hydra-acp/register_mcp_tools", async (raw) => {
|
|
11047
|
+
const params = raw ?? {};
|
|
11048
|
+
const instructions = typeof params.instructions === "string" ? params.instructions : void 0;
|
|
11049
|
+
const tools = Array.isArray(params.tools) ? params.tools.map((t) => {
|
|
11050
|
+
if (!t || typeof t !== "object") {
|
|
11051
|
+
return void 0;
|
|
11052
|
+
}
|
|
11053
|
+
const obj = t;
|
|
11054
|
+
if (typeof obj.name !== "string" || obj.name.length === 0) {
|
|
11055
|
+
return void 0;
|
|
11056
|
+
}
|
|
11057
|
+
if (typeof obj.description !== "string") {
|
|
11058
|
+
return void 0;
|
|
11059
|
+
}
|
|
11060
|
+
if (obj.inputSchema === null || typeof obj.inputSchema !== "object") {
|
|
11061
|
+
return void 0;
|
|
11062
|
+
}
|
|
11063
|
+
const spec = {
|
|
11064
|
+
name: obj.name,
|
|
11065
|
+
description: obj.description,
|
|
11066
|
+
inputSchema: obj.inputSchema
|
|
11067
|
+
};
|
|
11068
|
+
if (obj.outputSchema !== null && typeof obj.outputSchema === "object") {
|
|
11069
|
+
spec.outputSchema = obj.outputSchema;
|
|
11070
|
+
}
|
|
11071
|
+
return spec;
|
|
11072
|
+
}).filter((s) => s !== void 0) : [];
|
|
11073
|
+
if (tools.length === 0) {
|
|
11074
|
+
throw new Error("register_mcp_tools requires at least one tool");
|
|
11075
|
+
}
|
|
11076
|
+
mcpRegistry.register(
|
|
11077
|
+
processIdentity.name,
|
|
11078
|
+
connection,
|
|
11079
|
+
instructions,
|
|
11080
|
+
tools
|
|
11081
|
+
);
|
|
11082
|
+
return { ok: true, registered: tools.length };
|
|
11083
|
+
});
|
|
11084
|
+
connection.onClose(() => {
|
|
11085
|
+
mcpRegistry.clear(processIdentity.name);
|
|
11086
|
+
});
|
|
11087
|
+
}
|
|
10919
11088
|
if (processIdentity?.kind === "transformer") {
|
|
10920
11089
|
connection.onRequest("transformer/initialize", async (raw) => {
|
|
10921
11090
|
const params = raw ?? {};
|
|
@@ -11002,13 +11171,13 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
11002
11171
|
{ code: JsonRpcErrorCodes.SessionNotFound }
|
|
11003
11172
|
);
|
|
11004
11173
|
}
|
|
11005
|
-
return new Promise((
|
|
11174
|
+
return new Promise((resolve4) => {
|
|
11006
11175
|
const entries = [];
|
|
11007
11176
|
let unsubscribe;
|
|
11008
11177
|
const finish = () => {
|
|
11009
11178
|
clearTimeout(timer);
|
|
11010
11179
|
unsubscribe?.();
|
|
11011
|
-
|
|
11180
|
+
resolve4({ entries });
|
|
11012
11181
|
};
|
|
11013
11182
|
unsubscribe = child.onBroadcast((entry) => {
|
|
11014
11183
|
entries.push(entry);
|
|
@@ -11060,12 +11229,12 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
11060
11229
|
let stdinToken;
|
|
11061
11230
|
let stdinReservation;
|
|
11062
11231
|
let augmentedMcpServers = params.mcpServers;
|
|
11063
|
-
if (hydraMeta.mcpStdin === true && deps.
|
|
11232
|
+
if (hydraMeta.mcpStdin === true && deps.mcpTokenRegistry !== void 0 && deps.getDaemonOrigin !== void 0) {
|
|
11064
11233
|
stdinToken = randomBytes3(32).toString("hex");
|
|
11065
|
-
stdinReservation = deps.
|
|
11066
|
-
const url = `${deps.getDaemonOrigin()}/mcp/stdin`;
|
|
11234
|
+
stdinReservation = deps.mcpTokenRegistry.reserve(stdinToken);
|
|
11235
|
+
const url = `${deps.getDaemonOrigin()}/mcp/hydra-acp-stdin`;
|
|
11067
11236
|
const descriptor = {
|
|
11068
|
-
name: "
|
|
11237
|
+
name: "hydra-acp-stdin",
|
|
11069
11238
|
type: "http",
|
|
11070
11239
|
url,
|
|
11071
11240
|
headers: [
|
|
@@ -11074,6 +11243,28 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
11074
11243
|
};
|
|
11075
11244
|
augmentedMcpServers = [...params.mcpServers ?? [], descriptor];
|
|
11076
11245
|
}
|
|
11246
|
+
let extMcpToken;
|
|
11247
|
+
let extMcpReservation;
|
|
11248
|
+
if (deps.extensionMcp !== void 0 && deps.mcpTokenRegistry !== void 0 && deps.getDaemonOrigin !== void 0) {
|
|
11249
|
+
const extNames = deps.extensionMcp.list();
|
|
11250
|
+
if (extNames.length > 0) {
|
|
11251
|
+
extMcpToken = randomBytes3(32).toString("hex");
|
|
11252
|
+
extMcpReservation = deps.mcpTokenRegistry.reserve(extMcpToken);
|
|
11253
|
+
const origin = deps.getDaemonOrigin();
|
|
11254
|
+
const descriptors = extNames.map((name) => ({
|
|
11255
|
+
name,
|
|
11256
|
+
type: "http",
|
|
11257
|
+
url: `${origin}/mcp/${name}`,
|
|
11258
|
+
headers: [
|
|
11259
|
+
{ name: "Authorization", value: `Bearer ${extMcpToken}` }
|
|
11260
|
+
]
|
|
11261
|
+
}));
|
|
11262
|
+
augmentedMcpServers = [
|
|
11263
|
+
...augmentedMcpServers ?? [],
|
|
11264
|
+
...descriptors
|
|
11265
|
+
];
|
|
11266
|
+
}
|
|
11267
|
+
}
|
|
11077
11268
|
let session;
|
|
11078
11269
|
try {
|
|
11079
11270
|
session = await deps.manager.create({
|
|
@@ -11091,16 +11282,27 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
11091
11282
|
if (stdinReservation !== void 0) {
|
|
11092
11283
|
stdinReservation.abandon(err instanceof Error ? err : void 0);
|
|
11093
11284
|
}
|
|
11285
|
+
if (extMcpReservation !== void 0) {
|
|
11286
|
+
extMcpReservation.abandon(err instanceof Error ? err : void 0);
|
|
11287
|
+
}
|
|
11094
11288
|
throw err;
|
|
11095
11289
|
}
|
|
11096
|
-
if (stdinToken !== void 0 && stdinReservation !== void 0 && deps.
|
|
11290
|
+
if (stdinToken !== void 0 && stdinReservation !== void 0 && deps.mcpTokenRegistry !== void 0) {
|
|
11097
11291
|
const token2 = stdinToken;
|
|
11098
|
-
const registry = deps.
|
|
11292
|
+
const registry = deps.mcpTokenRegistry;
|
|
11099
11293
|
stdinReservation.complete(session);
|
|
11100
11294
|
session.onClose(() => {
|
|
11101
11295
|
void registry.unbind(token2);
|
|
11102
11296
|
});
|
|
11103
11297
|
}
|
|
11298
|
+
if (extMcpToken !== void 0 && extMcpReservation !== void 0 && deps.mcpTokenRegistry !== void 0) {
|
|
11299
|
+
const token2 = extMcpToken;
|
|
11300
|
+
const registry = deps.mcpTokenRegistry;
|
|
11301
|
+
extMcpReservation.complete(session);
|
|
11302
|
+
session.onClose(() => {
|
|
11303
|
+
void registry.unbind(token2);
|
|
11304
|
+
});
|
|
11305
|
+
}
|
|
11104
11306
|
const client = bindClientToSession(connection, session, state);
|
|
11105
11307
|
const { entries: replay } = await session.attach(client, "full");
|
|
11106
11308
|
state.attached.set(session.sessionId, {
|
|
@@ -11403,7 +11605,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
11403
11605
|
openOpts.fileCapBytes = params.fileCapBytes;
|
|
11404
11606
|
}
|
|
11405
11607
|
if ((params.mode ?? "memory") === "file") {
|
|
11406
|
-
openOpts.filePathFor = (sid) =>
|
|
11608
|
+
openOpts.filePathFor = (sid) => path13.join(os4.tmpdir(), `hydra-acp-stdin-${sid}.log`);
|
|
11407
11609
|
}
|
|
11408
11610
|
return session.openStream(openOpts);
|
|
11409
11611
|
});
|
|
@@ -11821,26 +12023,25 @@ function bindClientToSession(connection, session, state, clientInfo, callerClien
|
|
|
11821
12023
|
};
|
|
11822
12024
|
}
|
|
11823
12025
|
|
|
11824
|
-
// src/daemon/mcp/
|
|
11825
|
-
var
|
|
12026
|
+
// src/daemon/mcp/token-registry.ts
|
|
12027
|
+
var McpTokenRegistry = class {
|
|
11826
12028
|
byToken = /* @__PURE__ */ new Map();
|
|
11827
|
-
// Reserve a token slot before the session exists. Used by acp-ws when
|
|
11828
|
-
// we need to inject the bearer into the agent's mcpServers BEFORE
|
|
11829
|
-
// manager.create() returns — claude-acp connects to /mcp/stdin during
|
|
11830
|
-
// session/new initialization (eagerly), so the route handler must be
|
|
11831
|
-
// able to find the token by the time the agent's first request lands.
|
|
11832
12029
|
reserve(token) {
|
|
11833
12030
|
if (this.byToken.has(token)) {
|
|
11834
|
-
throw new Error(
|
|
12031
|
+
throw new Error("mcp token already bound");
|
|
11835
12032
|
}
|
|
11836
12033
|
let resolveSession;
|
|
11837
12034
|
let rejectSession;
|
|
11838
|
-
const sessionReady = new Promise((
|
|
11839
|
-
resolveSession =
|
|
12035
|
+
const sessionReady = new Promise((resolve4, reject) => {
|
|
12036
|
+
resolveSession = resolve4;
|
|
11840
12037
|
rejectSession = reject;
|
|
11841
12038
|
});
|
|
11842
12039
|
sessionReady.catch(() => void 0);
|
|
11843
|
-
const entry = {
|
|
12040
|
+
const entry = {
|
|
12041
|
+
session: void 0,
|
|
12042
|
+
sessionReady,
|
|
12043
|
+
disposers: []
|
|
12044
|
+
};
|
|
11844
12045
|
this.byToken.set(token, entry);
|
|
11845
12046
|
return {
|
|
11846
12047
|
complete: (session) => {
|
|
@@ -11849,7 +12050,7 @@ var StdinMcpRegistry = class {
|
|
|
11849
12050
|
},
|
|
11850
12051
|
abandon: (reason) => {
|
|
11851
12052
|
this.byToken.delete(token);
|
|
11852
|
-
rejectSession(reason ?? new Error("
|
|
12053
|
+
rejectSession(reason ?? new Error("mcp token reservation abandoned"));
|
|
11853
12054
|
}
|
|
11854
12055
|
};
|
|
11855
12056
|
}
|
|
@@ -11862,29 +12063,26 @@ var StdinMcpRegistry = class {
|
|
|
11862
12063
|
lookup(token) {
|
|
11863
12064
|
return this.byToken.get(token);
|
|
11864
12065
|
}
|
|
11865
|
-
|
|
11866
|
-
|
|
11867
|
-
|
|
12066
|
+
// Register a cleanup callback for this token. No-op if the token is
|
|
12067
|
+
// not currently bound — late additions after unbind() would never fire
|
|
12068
|
+
// anyway, so dropping them silently is safer than throwing into an
|
|
12069
|
+
// unrelated cleanup path.
|
|
12070
|
+
addDisposer(token, dispose) {
|
|
12071
|
+
const entry = this.byToken.get(token);
|
|
12072
|
+
if (entry === void 0) {
|
|
11868
12073
|
return;
|
|
11869
12074
|
}
|
|
11870
|
-
|
|
11871
|
-
ep.transport = transport;
|
|
12075
|
+
entry.disposers.push(dispose);
|
|
11872
12076
|
}
|
|
11873
12077
|
async unbind(token) {
|
|
11874
|
-
const
|
|
11875
|
-
if (
|
|
12078
|
+
const entry = this.byToken.get(token);
|
|
12079
|
+
if (entry === void 0) {
|
|
11876
12080
|
return;
|
|
11877
12081
|
}
|
|
11878
12082
|
this.byToken.delete(token);
|
|
11879
|
-
|
|
11880
|
-
try {
|
|
11881
|
-
await ep.transport.close();
|
|
11882
|
-
} catch {
|
|
11883
|
-
}
|
|
11884
|
-
}
|
|
11885
|
-
if (ep.server) {
|
|
12083
|
+
for (const dispose of entry.disposers) {
|
|
11886
12084
|
try {
|
|
11887
|
-
await
|
|
12085
|
+
await dispose();
|
|
11888
12086
|
} catch {
|
|
11889
12087
|
}
|
|
11890
12088
|
}
|
|
@@ -11899,6 +12097,8 @@ import { randomUUID } from "crypto";
|
|
|
11899
12097
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11900
12098
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
11901
12099
|
import { z as z7 } from "zod";
|
|
12100
|
+
|
|
12101
|
+
// src/daemon/mcp/bearer.ts
|
|
11902
12102
|
var BEARER_PREFIX2 = "Bearer ";
|
|
11903
12103
|
function extractBearer(req) {
|
|
11904
12104
|
const header = req.headers.authorization;
|
|
@@ -11911,15 +12111,17 @@ function extractBearer(req) {
|
|
|
11911
12111
|
const token = header.slice(BEARER_PREFIX2.length).trim();
|
|
11912
12112
|
return token.length > 0 ? token : void 0;
|
|
11913
12113
|
}
|
|
12114
|
+
|
|
12115
|
+
// src/daemon/mcp/stdin-server.ts
|
|
11914
12116
|
function buildMcpServer(session) {
|
|
11915
12117
|
const server = new McpServer(
|
|
11916
|
-
{ name: "hydra-stdin", version: "1.0.0" },
|
|
12118
|
+
{ name: "hydra-acp-stdin", version: "1.0.0" },
|
|
11917
12119
|
{
|
|
11918
|
-
instructions: "Piped input from `hydra cat --stream` is exposed here as a byte stream. Use `
|
|
12120
|
+
instructions: "Piped input from `hydra cat --stream` is exposed here as a byte stream. Use `tail` for the latest N bytes (good for finding the end of a log), `head` for the first N bytes (good for headers/preamble), `read` for windowed reads against an absolute byte cursor, `wait_for_more` to block until new bytes arrive past a cursor, and `info` for the current cursors/capacity/closed status. Byte payloads come back base64-encoded."
|
|
11919
12121
|
}
|
|
11920
12122
|
);
|
|
11921
12123
|
server.registerTool(
|
|
11922
|
-
"
|
|
12124
|
+
"tail",
|
|
11923
12125
|
{
|
|
11924
12126
|
description: "Return the most recent `bytes` bytes of piped stdin (capped server-side, default 64 KiB max). `truncated:true` means older bytes existed but have been evicted from the ring.",
|
|
11925
12127
|
inputSchema: {
|
|
@@ -11940,7 +12142,7 @@ function buildMcpServer(session) {
|
|
|
11940
12142
|
}
|
|
11941
12143
|
);
|
|
11942
12144
|
server.registerTool(
|
|
11943
|
-
"
|
|
12145
|
+
"head",
|
|
11944
12146
|
{
|
|
11945
12147
|
description: "Return the first `bytes` bytes of piped stdin (capped server-side, default 64 KiB max). `truncated:true` means the head has already been evicted from the ring and the returned bytes start at the oldest still-resident cursor.",
|
|
11946
12148
|
inputSchema: {
|
|
@@ -11961,7 +12163,7 @@ function buildMcpServer(session) {
|
|
|
11961
12163
|
}
|
|
11962
12164
|
);
|
|
11963
12165
|
server.registerTool(
|
|
11964
|
-
"
|
|
12166
|
+
"read",
|
|
11965
12167
|
{
|
|
11966
12168
|
description: "Read up to `max_bytes` bytes starting at absolute byte `cursor`. Returns `{bytes, nextCursor, gap?, eof?}` \u2014 `gap` is the number of bytes silently skipped because the ring had evicted them; `eof:true` means the producer closed and there is nothing left to read.",
|
|
11967
12169
|
inputSchema: {
|
|
@@ -12014,9 +12216,9 @@ function buildMcpServer(session) {
|
|
|
12014
12216
|
}
|
|
12015
12217
|
);
|
|
12016
12218
|
server.registerTool(
|
|
12017
|
-
"
|
|
12219
|
+
"grep",
|
|
12018
12220
|
{
|
|
12019
|
-
description: "Scan piped stdin line-by-line and return lines matching `pattern`. Prefer this over `
|
|
12221
|
+
description: "Scan piped stdin line-by-line and return lines matching `pattern`. Prefer this over `read` when the question is 'find lines that mention X' \u2014 it filters server-side so you don't pull and decode 64 KiB base64 windows. Returns `{matches: [{cursor, line, before?, after?}], truncated, nextCursor, gap?, scannedBytes, eof?}`. Lines come back as decoded UTF-8 strings (not base64). When `truncated:true`, re-call with `cursor: nextCursor` to resume.",
|
|
12020
12222
|
inputSchema: {
|
|
12021
12223
|
pattern: z7.string().min(1).describe(
|
|
12022
12224
|
"Search pattern. Treated as a JavaScript regular expression by default (set `regex:false` for a literal substring match)."
|
|
@@ -12068,7 +12270,7 @@ function buildMcpServer(session) {
|
|
|
12068
12270
|
}
|
|
12069
12271
|
);
|
|
12070
12272
|
server.registerTool(
|
|
12071
|
-
"
|
|
12273
|
+
"info",
|
|
12072
12274
|
{
|
|
12073
12275
|
description: "Report cursor / capacity / closed state of the stdin ring. Cheap; safe to call repeatedly.",
|
|
12074
12276
|
inputSchema: {}
|
|
@@ -12088,66 +12290,337 @@ function buildMcpServer(session) {
|
|
|
12088
12290
|
);
|
|
12089
12291
|
return server;
|
|
12090
12292
|
}
|
|
12091
|
-
|
|
12092
|
-
|
|
12093
|
-
|
|
12094
|
-
|
|
12293
|
+
var SESSION_READY_TIMEOUT_MS = 1e4;
|
|
12294
|
+
function registerStdinMcpRoutes(app, tokenRegistry) {
|
|
12295
|
+
const builtPerToken = /* @__PURE__ */ new Map();
|
|
12296
|
+
async function ensureTransport(token, session) {
|
|
12297
|
+
const existing = builtPerToken.get(token);
|
|
12298
|
+
if (existing !== void 0) {
|
|
12299
|
+
return existing.transport;
|
|
12300
|
+
}
|
|
12301
|
+
const server = buildMcpServer(session);
|
|
12302
|
+
const transport = new StreamableHTTPServerTransport({
|
|
12303
|
+
sessionIdGenerator: () => randomUUID()
|
|
12304
|
+
});
|
|
12305
|
+
await server.connect(transport);
|
|
12306
|
+
const pair = { server, transport };
|
|
12307
|
+
builtPerToken.set(token, pair);
|
|
12308
|
+
tokenRegistry.addDisposer(token, async () => {
|
|
12309
|
+
builtPerToken.delete(token);
|
|
12310
|
+
try {
|
|
12311
|
+
await transport.close();
|
|
12312
|
+
} catch {
|
|
12313
|
+
}
|
|
12314
|
+
try {
|
|
12315
|
+
await server.close();
|
|
12316
|
+
} catch {
|
|
12317
|
+
}
|
|
12318
|
+
});
|
|
12319
|
+
return transport;
|
|
12095
12320
|
}
|
|
12096
|
-
|
|
12097
|
-
|
|
12098
|
-
|
|
12321
|
+
async function handle(req, reply) {
|
|
12322
|
+
const token = extractBearer(req);
|
|
12323
|
+
if (token === void 0) {
|
|
12324
|
+
reply.code(401).send({ error: "missing bearer token" });
|
|
12325
|
+
return;
|
|
12326
|
+
}
|
|
12327
|
+
const entry = tokenRegistry.lookup(token);
|
|
12328
|
+
if (entry === void 0) {
|
|
12329
|
+
reply.code(404).send({ error: "unknown stdin token" });
|
|
12330
|
+
return;
|
|
12331
|
+
}
|
|
12332
|
+
let session;
|
|
12333
|
+
if (entry.session !== void 0) {
|
|
12334
|
+
session = entry.session;
|
|
12335
|
+
} else {
|
|
12336
|
+
let timer;
|
|
12337
|
+
const timeout = new Promise((resolve4) => {
|
|
12338
|
+
timer = setTimeout(() => resolve4(void 0), SESSION_READY_TIMEOUT_MS);
|
|
12339
|
+
});
|
|
12340
|
+
const resolved = await Promise.race([
|
|
12341
|
+
entry.sessionReady.catch(() => void 0),
|
|
12342
|
+
timeout
|
|
12343
|
+
]);
|
|
12344
|
+
if (timer !== void 0) {
|
|
12345
|
+
clearTimeout(timer);
|
|
12346
|
+
}
|
|
12347
|
+
if (resolved === void 0) {
|
|
12348
|
+
reply.code(503).send({ error: "session not ready" });
|
|
12349
|
+
return;
|
|
12350
|
+
}
|
|
12351
|
+
session = resolved;
|
|
12352
|
+
}
|
|
12353
|
+
const transport = await ensureTransport(token, session);
|
|
12354
|
+
reply.hijack();
|
|
12355
|
+
await transport.handleRequest(req.raw, reply.raw, req.body);
|
|
12356
|
+
}
|
|
12357
|
+
const opts = { config: { skipAuth: true } };
|
|
12358
|
+
app.post("/mcp/hydra-acp-stdin", opts, async (req, reply) => {
|
|
12359
|
+
await handle(req, reply);
|
|
12360
|
+
});
|
|
12361
|
+
app.get("/mcp/hydra-acp-stdin", opts, async (req, reply) => {
|
|
12362
|
+
await handle(req, reply);
|
|
12363
|
+
});
|
|
12364
|
+
app.delete("/mcp/hydra-acp-stdin", opts, async (req, reply) => {
|
|
12365
|
+
await handle(req, reply);
|
|
12099
12366
|
});
|
|
12100
|
-
await server.connect(transport);
|
|
12101
|
-
registry.attachTransport(token, server, transport);
|
|
12102
|
-
return transport;
|
|
12103
12367
|
}
|
|
12104
|
-
|
|
12105
|
-
|
|
12106
|
-
|
|
12107
|
-
|
|
12108
|
-
|
|
12109
|
-
|
|
12368
|
+
|
|
12369
|
+
// src/core/extension-mcp.ts
|
|
12370
|
+
var ExtensionMcpRegistry = class {
|
|
12371
|
+
byName = /* @__PURE__ */ new Map();
|
|
12372
|
+
changeHandlers = [];
|
|
12373
|
+
// Set-the-whole-spec semantics, same as ExtensionCommandRegistry. A
|
|
12374
|
+
// second register for the same extName overwrites tools + instructions
|
|
12375
|
+
// wholesale; the change notification lets the route evict any cached
|
|
12376
|
+
// transports built against the old spec.
|
|
12377
|
+
register(extName, connection, instructions, tools) {
|
|
12378
|
+
this.byName.set(extName, {
|
|
12379
|
+
connection,
|
|
12380
|
+
instructions,
|
|
12381
|
+
tools: [...tools]
|
|
12382
|
+
});
|
|
12383
|
+
this.fireChanged(extName, "register");
|
|
12110
12384
|
}
|
|
12111
|
-
|
|
12112
|
-
|
|
12113
|
-
|
|
12114
|
-
|
|
12385
|
+
clear(extName) {
|
|
12386
|
+
if (this.byName.delete(extName)) {
|
|
12387
|
+
this.fireChanged(extName, "clear");
|
|
12388
|
+
}
|
|
12115
12389
|
}
|
|
12116
|
-
|
|
12117
|
-
|
|
12118
|
-
|
|
12119
|
-
|
|
12120
|
-
|
|
12121
|
-
|
|
12122
|
-
|
|
12123
|
-
|
|
12124
|
-
|
|
12125
|
-
|
|
12390
|
+
lookup(extName) {
|
|
12391
|
+
return this.byName.get(extName);
|
|
12392
|
+
}
|
|
12393
|
+
// List of currently-registered extension names. Used by session-create
|
|
12394
|
+
// to decide whether to mint an extension-MCP token and which mcpServers
|
|
12395
|
+
// entries to emit.
|
|
12396
|
+
list() {
|
|
12397
|
+
return Array.from(this.byName.keys());
|
|
12398
|
+
}
|
|
12399
|
+
onChange(handler) {
|
|
12400
|
+
this.changeHandlers.push(handler);
|
|
12401
|
+
return () => {
|
|
12402
|
+
const i = this.changeHandlers.indexOf(handler);
|
|
12403
|
+
if (i >= 0) {
|
|
12404
|
+
this.changeHandlers.splice(i, 1);
|
|
12405
|
+
}
|
|
12406
|
+
};
|
|
12407
|
+
}
|
|
12408
|
+
fireChanged(extName, kind) {
|
|
12409
|
+
for (const h of this.changeHandlers) {
|
|
12410
|
+
try {
|
|
12411
|
+
h(extName, kind);
|
|
12412
|
+
} catch {
|
|
12413
|
+
}
|
|
12414
|
+
}
|
|
12415
|
+
}
|
|
12416
|
+
};
|
|
12417
|
+
|
|
12418
|
+
// src/daemon/mcp/extension-route.ts
|
|
12419
|
+
import { StreamableHTTPServerTransport as StreamableHTTPServerTransport2 } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
12420
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
12421
|
+
|
|
12422
|
+
// src/daemon/mcp/build-extension-server.ts
|
|
12423
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
12424
|
+
import {
|
|
12425
|
+
CallToolRequestSchema,
|
|
12426
|
+
ListToolsRequestSchema
|
|
12427
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
12428
|
+
var DEFAULT_INVOKE_TIMEOUT_MS = 6e4;
|
|
12429
|
+
function buildExtensionServer(extensionName, entry, options = {}) {
|
|
12430
|
+
const invokeTimeoutMs = options.invokeTimeoutMs ?? DEFAULT_INVOKE_TIMEOUT_MS;
|
|
12431
|
+
const server = new Server(
|
|
12432
|
+
{ name: extensionName, version: "1.0.0" },
|
|
12433
|
+
{
|
|
12434
|
+
capabilities: {
|
|
12435
|
+
// listChanged: false matches the v1 strategy — the daemon closes
|
|
12436
|
+
// transports on re-register; agents reconnect and re-list against
|
|
12437
|
+
// the new spec naturally. Flipping to true is the upgrade path
|
|
12438
|
+
// if any supported agent caches tools/list across reconnects.
|
|
12439
|
+
tools: { listChanged: false }
|
|
12440
|
+
},
|
|
12441
|
+
...entry.instructions !== void 0 ? { instructions: entry.instructions } : {}
|
|
12442
|
+
}
|
|
12443
|
+
);
|
|
12444
|
+
const toolsByName = new Map(
|
|
12445
|
+
entry.tools.map((t) => [t.name, t])
|
|
12446
|
+
);
|
|
12447
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
12448
|
+
tools: entry.tools.map((t) => ({
|
|
12449
|
+
name: t.name,
|
|
12450
|
+
description: t.description,
|
|
12451
|
+
inputSchema: t.inputSchema,
|
|
12452
|
+
...t.outputSchema !== void 0 ? { outputSchema: t.outputSchema } : {}
|
|
12453
|
+
}))
|
|
12454
|
+
}));
|
|
12455
|
+
server.setRequestHandler(
|
|
12456
|
+
CallToolRequestSchema,
|
|
12457
|
+
async (req) => {
|
|
12458
|
+
const toolName = req.params.name;
|
|
12459
|
+
if (!toolsByName.has(toolName)) {
|
|
12460
|
+
return errorResult(`unknown tool: ${toolName}`);
|
|
12461
|
+
}
|
|
12462
|
+
try {
|
|
12463
|
+
const raw = await invokeWithTimeout(
|
|
12464
|
+
entry.connection,
|
|
12465
|
+
extensionName,
|
|
12466
|
+
toolName,
|
|
12467
|
+
req.params.arguments ?? {},
|
|
12468
|
+
invokeTimeoutMs
|
|
12469
|
+
);
|
|
12470
|
+
return normalizeToolResult(raw, toolName);
|
|
12471
|
+
} catch (err) {
|
|
12472
|
+
return errorResult(
|
|
12473
|
+
err instanceof Error ? err.message : String(err)
|
|
12474
|
+
);
|
|
12475
|
+
}
|
|
12476
|
+
}
|
|
12477
|
+
);
|
|
12478
|
+
return server;
|
|
12479
|
+
}
|
|
12480
|
+
async function invokeWithTimeout(connection, server, tool, args, timeoutMs) {
|
|
12481
|
+
let timer;
|
|
12482
|
+
const timeout = new Promise((_, reject) => {
|
|
12483
|
+
timer = setTimeout(
|
|
12484
|
+
() => reject(new Error(`extension timeout after ${timeoutMs}ms`)),
|
|
12485
|
+
timeoutMs
|
|
12486
|
+
);
|
|
12487
|
+
});
|
|
12488
|
+
try {
|
|
12489
|
+
return await Promise.race([
|
|
12490
|
+
connection.request("hydra-acp/invoke_mcp_tool", {
|
|
12491
|
+
server,
|
|
12492
|
+
tool,
|
|
12493
|
+
args
|
|
12494
|
+
}),
|
|
12126
12495
|
timeout
|
|
12127
12496
|
]);
|
|
12497
|
+
} finally {
|
|
12128
12498
|
if (timer !== void 0) {
|
|
12129
12499
|
clearTimeout(timer);
|
|
12130
12500
|
}
|
|
12131
|
-
|
|
12132
|
-
|
|
12501
|
+
}
|
|
12502
|
+
}
|
|
12503
|
+
function normalizeToolResult(raw, toolName) {
|
|
12504
|
+
if (raw === null || typeof raw !== "object") {
|
|
12505
|
+
return errorResult(`extension ${toolName} returned non-object`);
|
|
12506
|
+
}
|
|
12507
|
+
const obj = raw;
|
|
12508
|
+
if (!Array.isArray(obj.content)) {
|
|
12509
|
+
return errorResult(`extension ${toolName} omitted content array`);
|
|
12510
|
+
}
|
|
12511
|
+
return obj;
|
|
12512
|
+
}
|
|
12513
|
+
function errorResult(message) {
|
|
12514
|
+
return {
|
|
12515
|
+
content: [{ type: "text", text: message }],
|
|
12516
|
+
isError: true
|
|
12517
|
+
};
|
|
12518
|
+
}
|
|
12519
|
+
|
|
12520
|
+
// src/daemon/mcp/extension-route.ts
|
|
12521
|
+
var SESSION_READY_TIMEOUT_MS2 = 1e4;
|
|
12522
|
+
function registerExtensionMcpRoutes(app, tokenRegistry, extensionMcp, options = {}) {
|
|
12523
|
+
const built = /* @__PURE__ */ new Map();
|
|
12524
|
+
async function disposeBuiltPair(pair) {
|
|
12525
|
+
try {
|
|
12526
|
+
await pair.transport.close();
|
|
12527
|
+
} catch {
|
|
12528
|
+
}
|
|
12529
|
+
try {
|
|
12530
|
+
await pair.server.close();
|
|
12531
|
+
} catch {
|
|
12532
|
+
}
|
|
12533
|
+
}
|
|
12534
|
+
function evictExtension(extName) {
|
|
12535
|
+
for (const tokenScope of built.values()) {
|
|
12536
|
+
const pair = tokenScope.get(extName);
|
|
12537
|
+
if (pair !== void 0) {
|
|
12538
|
+
tokenScope.delete(extName);
|
|
12539
|
+
void disposeBuiltPair(pair);
|
|
12540
|
+
}
|
|
12541
|
+
}
|
|
12542
|
+
}
|
|
12543
|
+
extensionMcp.onChange((extName) => {
|
|
12544
|
+
evictExtension(extName);
|
|
12545
|
+
});
|
|
12546
|
+
async function ensureTransport(token, extName) {
|
|
12547
|
+
let tokenScope = built.get(token);
|
|
12548
|
+
if (tokenScope === void 0) {
|
|
12549
|
+
tokenScope = /* @__PURE__ */ new Map();
|
|
12550
|
+
built.set(token, tokenScope);
|
|
12551
|
+
tokenRegistry.addDisposer(token, async () => {
|
|
12552
|
+
const scope = built.get(token);
|
|
12553
|
+
if (scope === void 0) {
|
|
12554
|
+
return;
|
|
12555
|
+
}
|
|
12556
|
+
built.delete(token);
|
|
12557
|
+
for (const pair of scope.values()) {
|
|
12558
|
+
await disposeBuiltPair(pair);
|
|
12559
|
+
}
|
|
12560
|
+
});
|
|
12561
|
+
}
|
|
12562
|
+
const existing = tokenScope.get(extName);
|
|
12563
|
+
if (existing !== void 0) {
|
|
12564
|
+
return existing.transport;
|
|
12565
|
+
}
|
|
12566
|
+
const entry = extensionMcp.lookup(extName);
|
|
12567
|
+
if (entry === void 0) {
|
|
12568
|
+
return void 0;
|
|
12569
|
+
}
|
|
12570
|
+
const server = buildExtensionServer(extName, entry, options.buildOptions);
|
|
12571
|
+
const transport = new StreamableHTTPServerTransport2({
|
|
12572
|
+
sessionIdGenerator: () => randomUUID2()
|
|
12573
|
+
});
|
|
12574
|
+
await server.connect(transport);
|
|
12575
|
+
tokenScope.set(extName, { server, transport });
|
|
12576
|
+
return transport;
|
|
12577
|
+
}
|
|
12578
|
+
async function handle(req, reply) {
|
|
12579
|
+
const token = extractBearer(req);
|
|
12580
|
+
if (token === void 0) {
|
|
12581
|
+
reply.code(401).send({ error: "missing bearer token" });
|
|
12582
|
+
return;
|
|
12583
|
+
}
|
|
12584
|
+
const entry = tokenRegistry.lookup(token);
|
|
12585
|
+
if (entry === void 0) {
|
|
12586
|
+
reply.code(404).send({ error: "unknown mcp token" });
|
|
12587
|
+
return;
|
|
12588
|
+
}
|
|
12589
|
+
if (entry.session === void 0) {
|
|
12590
|
+
let timer;
|
|
12591
|
+
const timeout = new Promise((resolve4) => {
|
|
12592
|
+
timer = setTimeout(() => resolve4(void 0), SESSION_READY_TIMEOUT_MS2);
|
|
12593
|
+
});
|
|
12594
|
+
const resolved = await Promise.race([
|
|
12595
|
+
entry.sessionReady.catch(() => void 0),
|
|
12596
|
+
timeout
|
|
12597
|
+
]);
|
|
12598
|
+
if (timer !== void 0) {
|
|
12599
|
+
clearTimeout(timer);
|
|
12600
|
+
}
|
|
12601
|
+
if (resolved === void 0) {
|
|
12602
|
+
reply.code(503).send({ error: "session not ready" });
|
|
12603
|
+
return;
|
|
12604
|
+
}
|
|
12605
|
+
}
|
|
12606
|
+
const extName = req.params.name;
|
|
12607
|
+
const transport = await ensureTransport(token, extName);
|
|
12608
|
+
if (transport === void 0) {
|
|
12609
|
+
reply.code(404).send({ error: `unknown mcp server: ${extName}` });
|
|
12133
12610
|
return;
|
|
12134
12611
|
}
|
|
12135
|
-
|
|
12612
|
+
reply.hijack();
|
|
12613
|
+
await transport.handleRequest(req.raw, reply.raw, req.body);
|
|
12136
12614
|
}
|
|
12137
|
-
const transport = await ensureTransport(token, session, registry);
|
|
12138
|
-
reply.hijack();
|
|
12139
|
-
await transport.handleRequest(req.raw, reply.raw, req.body);
|
|
12140
|
-
}
|
|
12141
|
-
function registerStdinMcpRoutes(app, registry) {
|
|
12142
12615
|
const opts = { config: { skipAuth: true } };
|
|
12143
|
-
app.post("/mcp
|
|
12144
|
-
await handle(req, reply
|
|
12616
|
+
app.post("/mcp/:name", opts, async (req, reply) => {
|
|
12617
|
+
await handle(req, reply);
|
|
12145
12618
|
});
|
|
12146
|
-
app.get("/mcp
|
|
12147
|
-
await handle(req, reply
|
|
12619
|
+
app.get("/mcp/:name", opts, async (req, reply) => {
|
|
12620
|
+
await handle(req, reply);
|
|
12148
12621
|
});
|
|
12149
|
-
app.delete("/mcp
|
|
12150
|
-
await handle(req, reply
|
|
12622
|
+
app.delete("/mcp/:name", opts, async (req, reply) => {
|
|
12623
|
+
await handle(req, reply);
|
|
12151
12624
|
});
|
|
12152
12625
|
}
|
|
12153
12626
|
|
|
@@ -12256,8 +12729,10 @@ async function startDaemon(config, serviceToken) {
|
|
|
12256
12729
|
store: sessionTokenStore,
|
|
12257
12730
|
rateLimiter: authRateLimiter
|
|
12258
12731
|
});
|
|
12259
|
-
const
|
|
12260
|
-
|
|
12732
|
+
const mcpTokenRegistry = new McpTokenRegistry();
|
|
12733
|
+
const extensionMcp = new ExtensionMcpRegistry();
|
|
12734
|
+
registerStdinMcpRoutes(app, mcpTokenRegistry);
|
|
12735
|
+
registerExtensionMcpRoutes(app, mcpTokenRegistry, extensionMcp);
|
|
12261
12736
|
let daemonOriginCached;
|
|
12262
12737
|
const getDaemonOrigin = () => {
|
|
12263
12738
|
if (daemonOriginCached !== void 0) {
|
|
@@ -12278,7 +12753,8 @@ async function startDaemon(config, serviceToken) {
|
|
|
12278
12753
|
onTransformerVersion: (name, version) => transformers.reportVersion(name, version),
|
|
12279
12754
|
transformers,
|
|
12280
12755
|
extensionCommands,
|
|
12281
|
-
|
|
12756
|
+
mcpTokenRegistry,
|
|
12757
|
+
extensionMcp,
|
|
12282
12758
|
getDaemonOrigin
|
|
12283
12759
|
});
|
|
12284
12760
|
await app.listen({ host: config.daemon.host, port: config.daemon.port });
|
|
@@ -12314,7 +12790,17 @@ async function startDaemon(config, serviceToken) {
|
|
|
12314
12790
|
`queue replay scan failed: ${err.message}`
|
|
12315
12791
|
);
|
|
12316
12792
|
});
|
|
12793
|
+
const intervalMs = config.daemon.agentSyncIntervalMinutes * 60 * 1e3;
|
|
12794
|
+
const stopAgentSync = intervalMs > 0 ? startAgentSyncScheduler({
|
|
12795
|
+
registry,
|
|
12796
|
+
manager,
|
|
12797
|
+
intervalMs,
|
|
12798
|
+
logger: agentLogger
|
|
12799
|
+
}) : void 0;
|
|
12317
12800
|
const shutdown = async () => {
|
|
12801
|
+
if (stopAgentSync) {
|
|
12802
|
+
stopAgentSync();
|
|
12803
|
+
}
|
|
12318
12804
|
clearInterval(sweepInterval);
|
|
12319
12805
|
await sessionTokenStore.flush();
|
|
12320
12806
|
await extensions.stop();
|
|
@@ -12334,7 +12820,17 @@ async function startDaemon(config, serviceToken) {
|
|
|
12334
12820
|
} catch {
|
|
12335
12821
|
}
|
|
12336
12822
|
};
|
|
12337
|
-
return {
|
|
12823
|
+
return {
|
|
12824
|
+
app,
|
|
12825
|
+
manager,
|
|
12826
|
+
registry,
|
|
12827
|
+
extensions,
|
|
12828
|
+
transformers,
|
|
12829
|
+
mcpTokenRegistry,
|
|
12830
|
+
extensionMcp,
|
|
12831
|
+
processRegistry,
|
|
12832
|
+
shutdown
|
|
12833
|
+
};
|
|
12338
12834
|
}
|
|
12339
12835
|
async function buildLogStream(level) {
|
|
12340
12836
|
const fileStream = await createPinoRoll({
|