@hydra-acp/cli 0.1.23 → 0.1.24

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.d.ts CHANGED
@@ -1203,10 +1203,14 @@ declare const RegistryDocument: z.ZodObject<{
1203
1203
  extensions?: unknown[] | undefined;
1204
1204
  }>;
1205
1205
  type RegistryDocument = z.infer<typeof RegistryDocument>;
1206
+ interface RegistryOptions {
1207
+ onFetched?: (doc: RegistryDocument) => void | Promise<void>;
1208
+ }
1206
1209
  declare class Registry {
1207
1210
  private config;
1211
+ private options;
1208
1212
  private cache;
1209
- constructor(config: HydraConfig);
1213
+ constructor(config: HydraConfig, options?: RegistryOptions);
1210
1214
  load(): Promise<RegistryDocument>;
1211
1215
  refresh(): Promise<RegistryDocument>;
1212
1216
  getAgent(id: string): Promise<RegistryAgent | undefined>;
@@ -1219,6 +1223,7 @@ interface SpawnPlan {
1219
1223
  command: string;
1220
1224
  args: string[];
1221
1225
  env: Record<string, string>;
1226
+ version: string;
1222
1227
  }
1223
1228
  declare function planSpawn(agent: RegistryAgent, callerArgs?: string[], options?: {
1224
1229
  npmRegistry?: string;
@@ -1618,6 +1623,7 @@ interface AgentLogger {
1618
1623
  }
1619
1624
  declare class AgentInstance {
1620
1625
  readonly agentId: string;
1626
+ readonly version: string;
1621
1627
  readonly cwd: string;
1622
1628
  readonly connection: JsonRpcConnection;
1623
1629
  private child;
@@ -2263,6 +2269,7 @@ declare class SessionManager {
2263
2269
  loadFromDisk(sessionId: string): Promise<ResurrectParams | undefined>;
2264
2270
  private deriveTitleFromHistory;
2265
2271
  get(sessionId: string): Session | undefined;
2272
+ activeAgentVersions(): Map<string, Set<string>>;
2266
2273
  resolveCanonicalId(input: string): Promise<string | undefined>;
2267
2274
  require(sessionId: string): Session;
2268
2275
  list(filter?: {
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/daemon/server.ts
2
2
  import * as fs14 from "fs";
3
- import * as fsp4 from "fs/promises";
3
+ import * as fsp5 from "fs/promises";
4
4
  import Fastify from "fastify";
5
5
  import websocketPlugin from "@fastify/websocket";
6
6
  import pino from "pino";
@@ -693,10 +693,12 @@ var RegistryDocument = z2.object({
693
693
  extensions: z2.array(z2.unknown()).optional()
694
694
  });
695
695
  var Registry = class {
696
- constructor(config) {
696
+ constructor(config, options = {}) {
697
697
  this.config = config;
698
+ this.options = options;
698
699
  }
699
700
  config;
701
+ options;
700
702
  cache;
701
703
  async load() {
702
704
  if (this.cache && this.isFresh(this.cache.fetchedAt)) {
@@ -746,7 +748,12 @@ var Registry = class {
746
748
  }
747
749
  const raw = await response.json();
748
750
  const data = RegistryDocument.parse(raw);
749
- return { fetchedAt: Date.now(), raw, data };
751
+ const cached = { fetchedAt: Date.now(), raw, data };
752
+ const hook = this.options.onFetched;
753
+ if (hook) {
754
+ void Promise.resolve().then(() => hook(data)).catch(() => void 0);
755
+ }
756
+ return cached;
750
757
  }
751
758
  async readDiskCache() {
752
759
  let text;
@@ -808,6 +815,7 @@ function npxPackageBasename(agent) {
808
815
  return atIdx <= 0 ? afterSlash : afterSlash.slice(0, atIdx);
809
816
  }
810
817
  async function planSpawn(agent, callerArgs = [], options = {}) {
818
+ const version = agent.version ?? "current";
811
819
  if (agent.distribution.npx) {
812
820
  const npx = agent.distribution.npx;
813
821
  const tail = callerArgs.length > 0 ? callerArgs : npx.args ?? [];
@@ -815,13 +823,14 @@ async function planSpawn(agent, callerArgs = [], options = {}) {
815
823
  return {
816
824
  command: "npx",
817
825
  args: ["-y", npx.package, ...tail],
818
- env: npx.env ?? {}
826
+ env: npx.env ?? {},
827
+ version
819
828
  };
820
829
  }
821
830
  const bin = npx.bin ?? npxPackageBasename(agent) ?? npx.package;
822
831
  const binPath = await ensureNpmPackage({
823
832
  agentId: agent.id,
824
- version: agent.version ?? "current",
833
+ version,
825
834
  packageSpec: npx.package,
826
835
  bin,
827
836
  registry: options.npmRegistry
@@ -829,7 +838,8 @@ async function planSpawn(agent, callerArgs = [], options = {}) {
829
838
  return {
830
839
  command: binPath,
831
840
  args: tail,
832
- env: npx.env ?? {}
841
+ env: npx.env ?? {},
842
+ version
833
843
  };
834
844
  }
835
845
  if (agent.distribution.binary) {
@@ -841,14 +851,15 @@ async function planSpawn(agent, callerArgs = [], options = {}) {
841
851
  }
842
852
  const cmdPath = await ensureBinary({
843
853
  agentId: agent.id,
844
- version: agent.version ?? "current",
854
+ version,
845
855
  target
846
856
  });
847
857
  const tail = callerArgs.length > 0 ? callerArgs : target.args ?? [];
848
858
  return {
849
859
  command: cmdPath,
850
860
  args: tail,
851
- env: target.env ?? {}
861
+ env: target.env ?? {},
862
+ version
852
863
  };
853
864
  }
854
865
  if (agent.distribution.uvx) {
@@ -857,7 +868,8 @@ async function planSpawn(agent, callerArgs = [], options = {}) {
857
868
  return {
858
869
  command: "uvx",
859
870
  args: [uvx.package, ...tail],
860
- env: uvx.env ?? {}
871
+ env: uvx.env ?? {},
872
+ version
861
873
  };
862
874
  }
863
875
  throw new Error(`Agent ${agent.id} has no usable distribution method.`);
@@ -1443,6 +1455,9 @@ var JsonRpcConnection = class _JsonRpcConnection {
1443
1455
  var DEFAULT_STDERR_TAIL_BYTES = 4096;
1444
1456
  var AgentInstance = class _AgentInstance {
1445
1457
  agentId;
1458
+ // Version this process was spawned from — used by the registry-fetch
1459
+ // prune sweep to skip install dirs belonging to a live agent.
1460
+ version;
1446
1461
  cwd;
1447
1462
  connection;
1448
1463
  child;
@@ -1454,6 +1469,7 @@ var AgentInstance = class _AgentInstance {
1454
1469
  exitHandlers = [];
1455
1470
  constructor(opts, child) {
1456
1471
  this.agentId = opts.agentId;
1472
+ this.version = opts.plan.version;
1457
1473
  this.cwd = opts.cwd;
1458
1474
  this.child = child;
1459
1475
  this.stderrTailBytes = opts.stderrTailBytes ?? DEFAULT_STDERR_TAIL_BYTES;
@@ -4257,6 +4273,23 @@ var SessionManager = class {
4257
4273
  get(sessionId) {
4258
4274
  return this.sessions.get(sessionId);
4259
4275
  }
4276
+ // Snapshot of which agent versions are currently in use by live
4277
+ // sessions, keyed by agentId. Read by the registry-fetch prune sweep
4278
+ // so it can skip install dirs that still back a running process.
4279
+ activeAgentVersions() {
4280
+ const out = /* @__PURE__ */ new Map();
4281
+ for (const session of this.sessions.values()) {
4282
+ const id = session.agent.agentId;
4283
+ const version = session.agent.version;
4284
+ let set = out.get(id);
4285
+ if (!set) {
4286
+ set = /* @__PURE__ */ new Set();
4287
+ out.set(id, set);
4288
+ }
4289
+ set.add(version);
4290
+ }
4291
+ return out;
4292
+ }
4260
4293
  // Resolve a user-typed session id (which may have the hydra_session_
4261
4294
  // prefix stripped — that's what `sessions list` and the picker show) to
4262
4295
  // the canonical form that actually exists. Tries the input as-given
@@ -5230,9 +5263,85 @@ function withCode2(err, code) {
5230
5263
  return err;
5231
5264
  }
5232
5265
 
5266
+ // src/core/agent-prune.ts
5267
+ import * as fsp4 from "fs/promises";
5268
+ import * as path8 from "path";
5269
+ var logSink3 = (msg) => {
5270
+ process.stderr.write(msg + "\n");
5271
+ };
5272
+ function setAgentPruneLogger(log) {
5273
+ logSink3 = log ?? ((msg) => process.stderr.write(msg + "\n"));
5274
+ }
5275
+ async function pruneStaleAgentVersions(registry, sessionManager) {
5276
+ const platformKey = currentPlatformKey();
5277
+ if (!platformKey) {
5278
+ return;
5279
+ }
5280
+ const doc = await registry.load();
5281
+ const desiredByAgent = /* @__PURE__ */ new Map();
5282
+ for (const a of doc.agents) {
5283
+ desiredByAgent.set(a.id, a.version ?? "current");
5284
+ }
5285
+ const activeByAgent = sessionManager.activeAgentVersions();
5286
+ const platformDir = path8.join(paths.agentsDir(), platformKey);
5287
+ let agentEntries;
5288
+ try {
5289
+ agentEntries = await fsp4.readdir(platformDir, { withFileTypes: true });
5290
+ } catch (err) {
5291
+ const e = err;
5292
+ if (e.code === "ENOENT") {
5293
+ return;
5294
+ }
5295
+ logSink3(`hydra-acp: prune: failed to read ${platformDir}: ${e.message}`);
5296
+ return;
5297
+ }
5298
+ for (const agentEntry of agentEntries) {
5299
+ if (!agentEntry.isDirectory()) {
5300
+ continue;
5301
+ }
5302
+ const agentId = agentEntry.name;
5303
+ const desired = desiredByAgent.get(agentId);
5304
+ if (desired === void 0) {
5305
+ continue;
5306
+ }
5307
+ const activeVersions = activeByAgent.get(agentId) ?? /* @__PURE__ */ new Set();
5308
+ const agentDir = path8.join(platformDir, agentId);
5309
+ let versionEntries;
5310
+ try {
5311
+ versionEntries = await fsp4.readdir(agentDir, { withFileTypes: true });
5312
+ } catch (err) {
5313
+ logSink3(
5314
+ `hydra-acp: prune: failed to read ${agentDir}: ${err.message}`
5315
+ );
5316
+ continue;
5317
+ }
5318
+ for (const versionEntry of versionEntries) {
5319
+ if (!versionEntry.isDirectory()) {
5320
+ continue;
5321
+ }
5322
+ const version = versionEntry.name;
5323
+ if (version === desired) {
5324
+ continue;
5325
+ }
5326
+ if (activeVersions.has(version)) {
5327
+ continue;
5328
+ }
5329
+ const versionDir = path8.join(agentDir, version);
5330
+ try {
5331
+ await fsp4.rm(versionDir, { recursive: true, force: true });
5332
+ logSink3(`hydra-acp: pruned stale ${agentId} ${version} (${versionDir})`);
5333
+ } catch (err) {
5334
+ logSink3(
5335
+ `hydra-acp: prune: failed to remove ${versionDir}: ${err.message}`
5336
+ );
5337
+ }
5338
+ }
5339
+ }
5340
+ }
5341
+
5233
5342
  // src/core/session-tokens.ts
5234
5343
  import * as fs12 from "fs/promises";
5235
- import * as path8 from "path";
5344
+ import * as path9 from "path";
5236
5345
  import { createHash, randomBytes, timingSafeEqual } from "crypto";
5237
5346
  var TOKEN_PREFIX = "hydra_session_";
5238
5347
  var DEFAULT_TTL_SEC = 60 * 60 * 24 * 30;
@@ -5240,7 +5349,7 @@ var ID_LENGTH = 12;
5240
5349
  var TOKEN_BYTES = 32;
5241
5350
  var WRITE_DEBOUNCE_MS = 50;
5242
5351
  function tokensFilePath() {
5243
- return path8.join(paths.home(), "session-tokens.json");
5352
+ return path9.join(paths.home(), "session-tokens.json");
5244
5353
  }
5245
5354
  function sha256Hex(input) {
5246
5355
  return createHash("sha256").update(input).digest("hex");
@@ -6536,12 +6645,12 @@ import { z as z6 } from "zod";
6536
6645
 
6537
6646
  // src/core/password.ts
6538
6647
  import * as fs13 from "fs/promises";
6539
- import * as path9 from "path";
6648
+ import * as path10 from "path";
6540
6649
  import { randomBytes as randomBytes2, scrypt, timingSafeEqual as timingSafeEqual2 } from "crypto";
6541
6650
  import { promisify } from "util";
6542
6651
  var scryptAsync = promisify(scrypt);
6543
6652
  function passwordHashPath() {
6544
- return path9.join(paths.home(), "password-hash");
6653
+ return path10.join(paths.home(), "password-hash");
6545
6654
  }
6546
6655
  var DEFAULT_N = 1 << 15;
6547
6656
  var MAX_MEM = 128 * 1024 * 1024;
@@ -7174,10 +7283,10 @@ function bindClientToSession(connection, session, state, clientInfo, callerClien
7174
7283
  async function startDaemon(config, serviceToken) {
7175
7284
  ensureLoopbackOrTls(config);
7176
7285
  const httpsOptions = config.daemon.tls ? {
7177
- key: await fsp4.readFile(config.daemon.tls.key),
7178
- cert: await fsp4.readFile(config.daemon.tls.cert)
7286
+ key: await fsp5.readFile(config.daemon.tls.key),
7287
+ cert: await fsp5.readFile(config.daemon.tls.cert)
7179
7288
  } : void 0;
7180
- await fsp4.mkdir(paths.home(), { recursive: true });
7289
+ await fsp5.mkdir(paths.home(), { recursive: true });
7181
7290
  const { stream: logStream, fileStream } = await buildLogStream(
7182
7291
  config.daemon.logLevel
7183
7292
  );
@@ -7221,7 +7330,12 @@ async function startDaemon(config, serviceToken) {
7221
7330
  5 * 60 * 1e3
7222
7331
  );
7223
7332
  sweepInterval.unref();
7224
- const registry = new Registry(config);
7333
+ const registry = new Registry(config, {
7334
+ onFetched: () => {
7335
+ void pruneStaleAgentVersions(registry, manager);
7336
+ }
7337
+ });
7338
+ setAgentPruneLogger((msg) => app.log.info(msg));
7225
7339
  const agentLogger = {
7226
7340
  info: (msg) => app.log.info(msg),
7227
7341
  warn: (msg) => app.log.warn(msg)
@@ -7262,8 +7376,8 @@ async function startDaemon(config, serviceToken) {
7262
7376
  await app.listen({ host: config.daemon.host, port: config.daemon.port });
7263
7377
  const address = app.server.address();
7264
7378
  const boundPort = address && typeof address === "object" ? address.port : config.daemon.port;
7265
- await fsp4.mkdir(paths.home(), { recursive: true });
7266
- await fsp4.writeFile(
7379
+ await fsp5.mkdir(paths.home(), { recursive: true });
7380
+ await fsp5.writeFile(
7267
7381
  paths.pidFile(),
7268
7382
  JSON.stringify({
7269
7383
  pid: process.pid,
@@ -7297,6 +7411,7 @@ async function startDaemon(config, serviceToken) {
7297
7411
  await manager.flushMetaWrites();
7298
7412
  setBinaryInstallLogger(null);
7299
7413
  setNpmInstallLogger(null);
7414
+ setAgentPruneLogger(null);
7300
7415
  await app.close();
7301
7416
  try {
7302
7417
  fs14.unlinkSync(paths.pidFile());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hydra-acp/cli",
3
- "version": "0.1.23",
3
+ "version": "0.1.24",
4
4
  "description": "Multi-client ACP session daemon: spawn agents, attach over WSS, multiplex sessions across editors.",
5
5
  "license": "MIT",
6
6
  "type": "module",