@hydra-acp/cli 0.1.51 → 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/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((resolve3, reject) => {
564
+ await new Promise((resolve4, reject) => {
558
565
  nodeStream.on("error", reject);
559
566
  out.on("error", reject);
560
- out.on("finish", () => resolve3());
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((resolve3, reject) => {
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
- resolve3();
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((resolve3) => {
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", () => resolve3(false));
635
- child.on("exit", (code) => resolve3(code === 0));
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((resolve3, reject) => {
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
- resolve3();
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((resolve3, reject) => {
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
- resolve3();
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((resolve3, reject) => {
1844
+ const response = new Promise((resolve4, reject) => {
1791
1845
  this.pending.set(id, {
1792
- resolve: (result) => resolve3(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((resolve3) => {
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
- resolve3(outcome);
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
- resolve3("timeout");
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 path13 = this.filePath;
2438
- if (path13 === void 0) {
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(path13, slice)).catch((err) => {
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((resolve3) => {
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(resolve3);
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: () => resolve3(),
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((resolve3) => {
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(resolve3).catch(() => resolve3(defaultStopPayload(method)));
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: resolve3,
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 ? " \u25C0" : "";
4550
+ const marker = m.modelId === current ? "\u25B6 " : " ";
4497
4551
  const desc = m.name && m.name !== m.modelId ? ` ${m.name}` : "";
4498
- return `${m.modelId}${marker}${desc}`;
4552
+ return `${marker}${m.modelId}${desc}`;
4499
4553
  });
4500
4554
  if (!inList && current) {
4501
- lines.unshift(`${current} \u25C0`);
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 path13 = this.streamFilePath;
5055
+ const path14 = this.streamFilePath;
5002
5056
  this.streamBuffer = void 0;
5003
5057
  this.streamFilePath = void 0;
5004
5058
  buf.close();
5005
- if (path13 !== void 0) {
5006
- void buf.drainFileWrites().then(() => fsp4.unlink(path13).catch(() => void 0));
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((resolve3, reject) => {
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
- resolve3(result);
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((resolve3, reject) => {
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: resolve3,
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((resolve3, reject) => {
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: resolve3,
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 path4 from "path";
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 path5 from "path";
6108
+ import * as path6 from "path";
6055
6109
  async function saveHistory(file, history) {
6056
- await fs8.mkdir(path5.dirname(file), { recursive: true });
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 path6 from "path";
6117
+ import * as path7 from "path";
6064
6118
  import * as fs9 from "fs";
6065
6119
  function resolveVersion() {
6066
6120
  try {
6067
- let dir = path6.dirname(fileURLToPath(import.meta.url));
6121
+ let dir = path7.dirname(fileURLToPath(import.meta.url));
6068
6122
  for (let i = 0; i < 8; i += 1) {
6069
- const candidate = path6.join(dir, "package.json");
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 = path6.dirname(dir);
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 path7 from "path";
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((resolve3) => {
7661
+ new Promise((resolve4) => {
7608
7662
  if (child.exitCode !== null || child.signalCode !== null) {
7609
- resolve3();
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
- resolve3();
7671
+ resolve4();
7618
7672
  }, STOP_GRACE_MS);
7619
7673
  child.on("exit", () => {
7620
7674
  clearTimeout(timer);
7621
- resolve3();
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((resolve3) => {
7730
- entry.exitWaiters.push(resolve3);
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 = path7.join(paths.extensionsDir(), entry);
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 resolve3 of waiters) {
7938
- resolve3();
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 path8 from "path";
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((resolve3) => {
8124
+ new Promise((resolve4) => {
8071
8125
  if (child.exitCode !== null || child.signalCode !== null) {
8072
- resolve3();
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
- resolve3();
8134
+ resolve4();
8081
8135
  }, STOP_GRACE_MS2);
8082
8136
  child.on("exit", () => {
8083
8137
  clearTimeout(timer);
8084
- resolve3();
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((resolve3) => {
8190
- entry.exitWaiters.push(resolve3);
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 = path8.join(paths.transformersDir(), entry);
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 resolve3 of waiters) {
8398
- resolve3();
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 path9 from "path";
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 = path9.join(paths.agentsDir(), platformKey);
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 = path9.join(platformDir, agentId);
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 = path9.join(agentDir, version);
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 path10 from "path";
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 path10.join(paths.home(), "session-tokens.json");
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
- return {
10288
- version: doc.version,
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 path11 from "path";
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 path11.join(paths.home(), "password-hash");
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((resolve3, reject) => {
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
- resolve3();
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 path12 from "path";
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((resolve3) => {
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
- resolve3({ entries });
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.stdinMcpRegistry !== void 0 && deps.getDaemonOrigin !== void 0) {
11232
+ if (hydraMeta.mcpStdin === true && deps.mcpTokenRegistry !== void 0 && deps.getDaemonOrigin !== void 0) {
11064
11233
  stdinToken = randomBytes3(32).toString("hex");
11065
- stdinReservation = deps.stdinMcpRegistry.reserve(stdinToken);
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: "hydra_stdin",
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.stdinMcpRegistry !== void 0) {
11290
+ if (stdinToken !== void 0 && stdinReservation !== void 0 && deps.mcpTokenRegistry !== void 0) {
11097
11291
  const token2 = stdinToken;
11098
- const registry = deps.stdinMcpRegistry;
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) => path12.join(os4.tmpdir(), `hydra-stdin-${sid}.log`);
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/stdin-registry.ts
11825
- var StdinMcpRegistry = class {
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(`stdin MCP token already bound`);
12031
+ throw new Error("mcp token already bound");
11835
12032
  }
11836
12033
  let resolveSession;
11837
12034
  let rejectSession;
11838
- const sessionReady = new Promise((resolve3, reject) => {
11839
- resolveSession = resolve3;
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 = { session: void 0, sessionReady };
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("stdin MCP reservation abandoned"));
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
- attachTransport(token, server, transport) {
11866
- const ep = this.byToken.get(token);
11867
- if (!ep) {
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
- ep.server = server;
11871
- ep.transport = transport;
12075
+ entry.disposers.push(dispose);
11872
12076
  }
11873
12077
  async unbind(token) {
11874
- const ep = this.byToken.get(token);
11875
- if (!ep) {
12078
+ const entry = this.byToken.get(token);
12079
+ if (entry === void 0) {
11876
12080
  return;
11877
12081
  }
11878
12082
  this.byToken.delete(token);
11879
- if (ep.transport) {
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 ep.server.close();
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 `tail_stdin` for the latest N bytes (good for finding the end of a log), `head_stdin` for the first N bytes (good for headers/preamble), `read_stdin` for windowed reads against an absolute byte cursor, `wait_for_more` to block until new bytes arrive past a cursor, and `stdin_info` for the current cursors/capacity/closed status. Byte payloads come back base64-encoded."
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
- "tail_stdin",
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
- "head_stdin",
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
- "read_stdin",
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
- "grep_stdin",
12219
+ "grep",
12018
12220
  {
12019
- description: "Scan piped stdin line-by-line and return lines matching `pattern`. Prefer this over `read_stdin` 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.",
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
- "stdin_info",
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
- async function ensureTransport(token, session, registry) {
12092
- const existing = registry.lookup(token);
12093
- if (existing?.transport !== void 0) {
12094
- return existing.transport;
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
- const server = buildMcpServer(session);
12097
- const transport = new StreamableHTTPServerTransport({
12098
- sessionIdGenerator: () => randomUUID()
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
- var SESSION_READY_TIMEOUT_MS = 1e4;
12105
- async function handle(req, reply, registry) {
12106
- const token = extractBearer(req);
12107
- if (token === void 0) {
12108
- reply.code(401).send({ error: "missing bearer token" });
12109
- return;
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
- const ep = registry.lookup(token);
12112
- if (ep === void 0) {
12113
- reply.code(404).send({ error: "unknown stdin token" });
12114
- return;
12385
+ clear(extName) {
12386
+ if (this.byName.delete(extName)) {
12387
+ this.fireChanged(extName, "clear");
12388
+ }
12115
12389
  }
12116
- let session;
12117
- if (ep.session !== void 0) {
12118
- session = ep.session;
12119
- } else {
12120
- let timer;
12121
- const timeout = new Promise((resolve3) => {
12122
- timer = setTimeout(() => resolve3(void 0), SESSION_READY_TIMEOUT_MS);
12123
- });
12124
- const resolved = await Promise.race([
12125
- ep.sessionReady.catch(() => void 0),
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
- if (resolved === void 0) {
12132
- reply.code(503).send({ error: "session not ready" });
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
- session = resolved;
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/stdin", opts, async (req, reply) => {
12144
- await handle(req, reply, registry);
12616
+ app.post("/mcp/:name", opts, async (req, reply) => {
12617
+ await handle(req, reply);
12145
12618
  });
12146
- app.get("/mcp/stdin", opts, async (req, reply) => {
12147
- await handle(req, reply, registry);
12619
+ app.get("/mcp/:name", opts, async (req, reply) => {
12620
+ await handle(req, reply);
12148
12621
  });
12149
- app.delete("/mcp/stdin", opts, async (req, reply) => {
12150
- await handle(req, reply, registry);
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 stdinMcpRegistry = new StdinMcpRegistry();
12260
- registerStdinMcpRoutes(app, stdinMcpRegistry);
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
- stdinMcpRegistry,
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 { app, manager, registry, extensions, transformers, shutdown };
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({