@hydra-acp/cli 0.1.40 → 0.1.42

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
@@ -1415,6 +1415,7 @@ declare const SessionListEntry: z.ZodObject<{
1415
1415
  updatedAt: z.ZodString;
1416
1416
  attachedClients: z.ZodNumber;
1417
1417
  status: z.ZodDefault<z.ZodEnum<["live", "cold"]>>;
1418
+ busy: z.ZodDefault<z.ZodBoolean>;
1418
1419
  _meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
1419
1420
  }, "strip", z.ZodTypeAny, {
1420
1421
  sessionId: string;
@@ -1422,6 +1423,7 @@ declare const SessionListEntry: z.ZodObject<{
1422
1423
  status: "live" | "cold";
1423
1424
  updatedAt: string;
1424
1425
  attachedClients: number;
1426
+ busy: boolean;
1425
1427
  agentId?: string | undefined;
1426
1428
  upstreamSessionId?: string | undefined;
1427
1429
  title?: string | undefined;
@@ -1454,6 +1456,7 @@ declare const SessionListEntry: z.ZodObject<{
1454
1456
  } | undefined;
1455
1457
  importedFromMachine?: string | undefined;
1456
1458
  importedFromUpstreamSessionId?: string | undefined;
1459
+ busy?: boolean | undefined;
1457
1460
  }>;
1458
1461
  type SessionListEntry = z.infer<typeof SessionListEntry>;
1459
1462
  declare const SessionListResult: z.ZodObject<{
@@ -1984,6 +1987,7 @@ declare const SessionRecord: z.ZodObject<{
1984
1987
  name?: string | undefined;
1985
1988
  description?: string | undefined;
1986
1989
  }>, "many">>;
1990
+ pendingHistorySync: z.ZodOptional<z.ZodBoolean>;
1987
1991
  createdAt: z.ZodString;
1988
1992
  updatedAt: z.ZodString;
1989
1993
  }, "strip", z.ZodTypeAny, {
@@ -2022,6 +2026,7 @@ declare const SessionRecord: z.ZodObject<{
2022
2026
  name?: string | undefined;
2023
2027
  description?: string | undefined;
2024
2028
  }[] | undefined;
2029
+ pendingHistorySync?: boolean | undefined;
2025
2030
  }, {
2026
2031
  sessionId: string;
2027
2032
  version: 1;
@@ -2058,6 +2063,7 @@ declare const SessionRecord: z.ZodObject<{
2058
2063
  name?: string | undefined;
2059
2064
  description?: string | undefined;
2060
2065
  }[] | undefined;
2066
+ pendingHistorySync?: boolean | undefined;
2061
2067
  }>;
2062
2068
  type SessionRecord = z.infer<typeof SessionRecord>;
2063
2069
  declare class SessionStore {
@@ -2305,6 +2311,7 @@ interface ResurrectParams {
2305
2311
  agentModes?: AdvertisedMode[];
2306
2312
  agentModels?: AdvertisedModel[];
2307
2313
  createdAt?: string;
2314
+ pendingHistorySync?: boolean;
2308
2315
  }
2309
2316
  type AgentSpawner = (opts: AgentInstanceOptions) => AgentInstance;
2310
2317
  interface SessionManagerOptions {
@@ -2333,11 +2340,17 @@ declare class SessionManager {
2333
2340
  private doResurrect;
2334
2341
  private doResurrectFromImport;
2335
2342
  private resolveImportCwd;
2343
+ syncFromAgent(agentId: string): Promise<{
2344
+ synced: SessionRecord[];
2345
+ skipped: number;
2346
+ }>;
2347
+ private collectAgentSessions;
2336
2348
  private bootstrapAgent;
2337
2349
  private attachManagerHooks;
2338
2350
  getHistory(sessionId: string): Promise<HistoryEntry[] | undefined>;
2339
2351
  loadHistory(sessionId: string): Promise<HistoryEntry[]>;
2340
2352
  loadFromDisk(sessionId: string): Promise<ResurrectParams | undefined>;
2353
+ private clearPendingHistorySync;
2341
2354
  private deriveTitleFromHistory;
2342
2355
  get(sessionId: string): Session | undefined;
2343
2356
  activeAgentVersions(): Map<string, Set<string>>;
@@ -2459,6 +2472,7 @@ declare const paths: {
2459
2472
  extensionLogFile: (name: string) => string;
2460
2473
  extensionPidFile: (name: string) => string;
2461
2474
  tuiHistoryFile: (id: string) => string;
2475
+ globalTuiHistoryFile: () => string;
2462
2476
  tuiLogFile: () => string;
2463
2477
  };
2464
2478
 
package/dist/index.js CHANGED
@@ -87,6 +87,11 @@ var paths = {
87
87
  extensionLogFile: (name) => path.join(hydraHome(), "extensions", `${name}.log`),
88
88
  extensionPidFile: (name) => path.join(hydraHome(), "extensions", `${name}.pid`),
89
89
  tuiHistoryFile: (id) => path.join(hydraHome(), "sessions", id, "prompt-history"),
90
+ // Cross-session prompt history. Up-arrow / ^R fall through to this
91
+ // after the per-session list is exhausted. JSONL, one entry per
92
+ // line, append-only so concurrent TUIs don't lose each other's
93
+ // writes.
94
+ globalTuiHistoryFile: () => path.join(hydraHome(), "prompt-history"),
90
95
  tuiLogFile: () => path.join(hydraHome(), "tui.log")
91
96
  };
92
97
 
@@ -1383,6 +1388,10 @@ var SessionListEntry = z3.object({
1383
1388
  updatedAt: z3.string(),
1384
1389
  attachedClients: z3.number().int().nonnegative(),
1385
1390
  status: z3.enum(["live", "cold"]).default("live"),
1391
+ // True while the session is mid-turn (an agent prompt is in flight).
1392
+ // Always false for cold sessions. Lets pickers render a busy dot
1393
+ // without having to attach.
1394
+ busy: z3.boolean().default(false),
1386
1395
  _meta: z3.record(z3.unknown()).optional()
1387
1396
  });
1388
1397
  var SessionListEntryWire = z3.object({
@@ -1399,7 +1408,8 @@ var SessionListResult = z3.object({
1399
1408
  function sessionListEntryToWire(entry) {
1400
1409
  const hydraMeta = {
1401
1410
  attachedClients: entry.attachedClients,
1402
- status: entry.status
1411
+ status: entry.status,
1412
+ busy: entry.busy
1403
1413
  };
1404
1414
  if (entry.agentId !== void 0) {
1405
1415
  hydraMeta.agentId = entry.agentId;
@@ -4325,6 +4335,12 @@ var SessionRecord = z4.object({
4325
4335
  agentCommands: z4.array(PersistedAgentCommand).optional(),
4326
4336
  agentModes: z4.array(PersistedAgentMode).optional(),
4327
4337
  agentModels: z4.array(PersistedAgentModel).optional(),
4338
+ // One-shot flag set when `hydra agent sync` mints a row from an
4339
+ // agent-side session/list entry: signals that the first resurrect
4340
+ // should *keep* the agent's session/load replay (instead of draining
4341
+ // it) so the local history.jsonl gets populated from the agent's
4342
+ // memory. Cleared after that first resurrect completes.
4343
+ pendingHistorySync: z4.boolean().optional(),
4328
4344
  createdAt: z4.string(),
4329
4345
  updatedAt: z4.string()
4330
4346
  });
@@ -4445,6 +4461,7 @@ function recordFromMemorySession(args) {
4445
4461
  agentCommands: args.agentCommands,
4446
4462
  agentModes: args.agentModes,
4447
4463
  agentModels: args.agentModels,
4464
+ pendingHistorySync: args.pendingHistorySync,
4448
4465
  createdAt: args.createdAt ?? now,
4449
4466
  updatedAt: args.updatedAt ?? now
4450
4467
  };
@@ -4780,7 +4797,13 @@ var SessionManager = class {
4780
4797
  await agent.kill().catch(() => void 0);
4781
4798
  return this.doResurrectFromImport(params);
4782
4799
  }
4783
- agent.connection.drainBuffered("session/update");
4800
+ if (params.pendingHistorySync === true) {
4801
+ void this.clearPendingHistorySync(params.hydraSessionId).catch(
4802
+ () => void 0
4803
+ );
4804
+ } else {
4805
+ agent.connection.drainBuffered("session/update");
4806
+ }
4784
4807
  const session = new Session({
4785
4808
  sessionId: params.hydraSessionId,
4786
4809
  cwd: params.cwd,
@@ -4870,6 +4893,133 @@ var SessionManager = class {
4870
4893
  }
4871
4894
  return os2.homedir();
4872
4895
  }
4896
+ // Pull every session the agent itself remembers (across all cwds) and
4897
+ // persist a cold hydra record for each one we don't already track.
4898
+ // Used by `hydra agent sync <id>` to surface sessions created outside
4899
+ // hydra — or by other tools — in `hydra session list` so the picker
4900
+ // can resurrect them. Spawns a throwaway agent process for the
4901
+ // initialize + session/list pair, then kills it. Records are minted
4902
+ // with pendingHistorySync:true so the first resurrect records the
4903
+ // agent's session/load replay into history.jsonl rather than dropping
4904
+ // it.
4905
+ async syncFromAgent(agentId) {
4906
+ const agentDef = await this.registry.getAgent(agentId);
4907
+ if (!agentDef) {
4908
+ const err = new Error(
4909
+ `agent ${agentId} not found in registry`
4910
+ );
4911
+ err.code = JsonRpcErrorCodes.AgentNotInstalled;
4912
+ throw err;
4913
+ }
4914
+ const plan = await planSpawn(agentDef, [], {
4915
+ npmRegistry: this.npmRegistry
4916
+ });
4917
+ const agent = this.spawner({
4918
+ agentId,
4919
+ cwd: os2.homedir(),
4920
+ plan
4921
+ });
4922
+ let initResult;
4923
+ try {
4924
+ initResult = await agent.connection.request(
4925
+ "initialize",
4926
+ {
4927
+ protocolVersion: ACP_PROTOCOL_VERSION,
4928
+ clientCapabilities: {},
4929
+ clientInfo: { name: "hydra", version: HYDRA_VERSION }
4930
+ }
4931
+ );
4932
+ } catch (err) {
4933
+ await agent.kill().catch(() => void 0);
4934
+ throw err;
4935
+ }
4936
+ const caps = initResult.agentCapabilities ?? {};
4937
+ if (caps.sessionCapabilities?.list === void 0) {
4938
+ await agent.kill().catch(() => void 0);
4939
+ throw new Error(
4940
+ `agent ${agentId} does not advertise sessionCapabilities.list; cannot sync`
4941
+ );
4942
+ }
4943
+ let entries;
4944
+ try {
4945
+ entries = await this.collectAgentSessions(agent);
4946
+ } catch (err) {
4947
+ await agent.kill().catch(() => void 0);
4948
+ throw err;
4949
+ }
4950
+ await agent.kill().catch(() => void 0);
4951
+ const existing = /* @__PURE__ */ new Set();
4952
+ for (const live of this.sessions.values()) {
4953
+ existing.add(`${live.agentId}::${live.upstreamSessionId}`);
4954
+ }
4955
+ const stored = await this.store.list().catch(() => []);
4956
+ for (const rec of stored) {
4957
+ existing.add(`${rec.agentId}::${rec.upstreamSessionId}`);
4958
+ }
4959
+ const synced = [];
4960
+ let skipped = 0;
4961
+ for (const entry of entries) {
4962
+ const dedupeKey = `${agentId}::${entry.sessionId}`;
4963
+ if (existing.has(dedupeKey)) {
4964
+ skipped += 1;
4965
+ continue;
4966
+ }
4967
+ existing.add(dedupeKey);
4968
+ const newId = `${HYDRA_SESSION_PREFIX}${generateRawSessionId()}`;
4969
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4970
+ const ts = entry.updatedAt ?? now;
4971
+ const recordArgs = {
4972
+ sessionId: newId,
4973
+ lineageId: generateLineageId(),
4974
+ upstreamSessionId: entry.sessionId,
4975
+ agentId,
4976
+ cwd: entry.cwd,
4977
+ pendingHistorySync: true,
4978
+ createdAt: ts,
4979
+ updatedAt: ts
4980
+ };
4981
+ if (entry.title !== void 0) {
4982
+ recordArgs.title = entry.title;
4983
+ }
4984
+ const record = recordFromMemorySession(recordArgs);
4985
+ await this.store.write(record);
4986
+ synced.push({ version: 1, ...record });
4987
+ }
4988
+ return { synced, skipped };
4989
+ }
4990
+ // Paginate the agent's session/list, threading nextCursor until the
4991
+ // agent stops returning one. Each entry the spec guarantees has
4992
+ // { sessionId, cwd }; title and updatedAt are optional.
4993
+ async collectAgentSessions(agent) {
4994
+ const out = [];
4995
+ let cursor;
4996
+ for (let page = 0; page < 100; page += 1) {
4997
+ const params = {};
4998
+ if (cursor !== void 0) {
4999
+ params.cursor = cursor;
5000
+ }
5001
+ const result = await agent.connection.request("session/list", params);
5002
+ const rows = Array.isArray(result.sessions) ? result.sessions : [];
5003
+ for (const row of rows) {
5004
+ if (typeof row.sessionId !== "string" || typeof row.cwd !== "string") {
5005
+ continue;
5006
+ }
5007
+ const entry = { sessionId: row.sessionId, cwd: row.cwd };
5008
+ if (typeof row.title === "string") {
5009
+ entry.title = row.title;
5010
+ }
5011
+ if (typeof row.updatedAt === "string") {
5012
+ entry.updatedAt = row.updatedAt;
5013
+ }
5014
+ out.push(entry);
5015
+ }
5016
+ if (typeof result.nextCursor !== "string" || result.nextCursor.length === 0) {
5017
+ break;
5018
+ }
5019
+ cursor = result.nextCursor;
5020
+ }
5021
+ return out;
5022
+ }
4873
5023
  // Bootstrap a fresh agent process: registry resolve → spawn → initialize
4874
5024
  // → session/new. Shared by create() and the /hydra agent path so both
4875
5025
  // go through the same env / capabilities / error-handling.
@@ -5062,9 +5212,21 @@ var SessionManager = class {
5062
5212
  agentCommands: record.agentCommands,
5063
5213
  agentModes: record.agentModes,
5064
5214
  agentModels: record.agentModels,
5065
- createdAt: record.createdAt
5215
+ createdAt: record.createdAt,
5216
+ pendingHistorySync: record.pendingHistorySync
5066
5217
  };
5067
5218
  }
5219
+ async clearPendingHistorySync(sessionId) {
5220
+ await this.enqueueMetaWrite(sessionId, async () => {
5221
+ const record = await this.store.read(sessionId);
5222
+ if (!record || record.pendingHistorySync !== true) {
5223
+ return;
5224
+ }
5225
+ const next = { ...record };
5226
+ delete next.pendingHistorySync;
5227
+ await this.store.write(next);
5228
+ });
5229
+ }
5068
5230
  // Best-effort: peek at the persisted history's first prompt and use
5069
5231
  // its first line (capped to 200 chars) as a session title. Returns
5070
5232
  // undefined if no usable prompt is found or any I/O fails.
@@ -5150,7 +5312,8 @@ var SessionManager = class {
5150
5312
  currentUsage: session.currentUsage,
5151
5313
  updatedAt: used,
5152
5314
  attachedClients: session.attachedCount,
5153
- status: "live"
5315
+ status: "live",
5316
+ busy: session.turnStartedAt !== void 0
5154
5317
  });
5155
5318
  }
5156
5319
  const records = await this.store.list().catch(() => []);
@@ -5174,7 +5337,8 @@ var SessionManager = class {
5174
5337
  importedFromUpstreamSessionId: r.importedFromUpstreamSessionId,
5175
5338
  updatedAt: used,
5176
5339
  attachedClients: 0,
5177
- status: "cold"
5340
+ status: "cold",
5341
+ busy: false
5178
5342
  });
5179
5343
  }
5180
5344
  entries.sort((a, b) => a.updatedAt < b.updatedAt ? 1 : -1);
@@ -7461,7 +7625,7 @@ function registerSessionRoutes(app, manager, defaults) {
7461
7625
  }
7462
7626
 
7463
7627
  // src/daemon/routes/agents.ts
7464
- function registerAgentRoutes(app, registry) {
7628
+ function registerAgentRoutes(app, registry, manager, opts = {}) {
7465
7629
  app.get("/v1/agents", async () => {
7466
7630
  const doc = await registry.load();
7467
7631
  return {
@@ -7482,6 +7646,61 @@ function registerAgentRoutes(app, registry) {
7482
7646
  const doc = await registry.refresh();
7483
7647
  return { version: doc.version, agentCount: doc.agents.length };
7484
7648
  });
7649
+ app.post("/v1/agents/:id/install", async (request, reply) => {
7650
+ const id = request.params.id;
7651
+ const agent = await registry.getAgent(id);
7652
+ if (!agent) {
7653
+ reply.code(404).send({ error: `agent ${id} not found in registry` });
7654
+ return;
7655
+ }
7656
+ if (agent.distribution.uvx && !agent.distribution.npx && !agent.distribution.binary) {
7657
+ reply.send({
7658
+ agentId: agent.id,
7659
+ version: agent.version ?? "current",
7660
+ distribution: "uvx",
7661
+ installed: false,
7662
+ message: "uvx agents resolve on first run; nothing to pre-install."
7663
+ });
7664
+ return;
7665
+ }
7666
+ try {
7667
+ const plan = await planSpawn(agent, [], { npmRegistry: opts.npmRegistry });
7668
+ const distribution = agent.distribution.npx ? "npx" : agent.distribution.binary ? "binary" : "unknown";
7669
+ reply.send({
7670
+ agentId: agent.id,
7671
+ version: plan.version,
7672
+ distribution,
7673
+ installed: true,
7674
+ command: plan.command
7675
+ });
7676
+ } catch (err) {
7677
+ reply.code(500).send({ error: err.message });
7678
+ }
7679
+ });
7680
+ app.post("/v1/agents/:id/sync", async (request, reply) => {
7681
+ const agentId = request.params.id;
7682
+ try {
7683
+ const { synced, skipped } = await manager.syncFromAgent(agentId);
7684
+ return {
7685
+ synced: synced.map((r) => ({
7686
+ sessionId: r.sessionId,
7687
+ upstreamSessionId: r.upstreamSessionId,
7688
+ agentId: r.agentId,
7689
+ cwd: r.cwd,
7690
+ title: r.title,
7691
+ updatedAt: r.updatedAt
7692
+ })),
7693
+ skipped
7694
+ };
7695
+ } catch (err) {
7696
+ const e = err;
7697
+ if (e.code === JsonRpcErrorCodes.AgentNotInstalled) {
7698
+ reply.code(404).send({ error: e.message });
7699
+ return;
7700
+ }
7701
+ reply.code(409).send({ error: e.message });
7702
+ }
7703
+ });
7485
7704
  }
7486
7705
 
7487
7706
  // src/daemon/routes/health.ts
@@ -8597,7 +8816,7 @@ async function startDaemon(config, serviceToken) {
8597
8816
  agentId: config.defaultAgent,
8598
8817
  cwd: config.defaultCwd
8599
8818
  });
8600
- registerAgentRoutes(app, registry);
8819
+ registerAgentRoutes(app, registry, manager, { npmRegistry: config.npmRegistry });
8601
8820
  registerExtensionRoutes(app, extensions);
8602
8821
  registerConfigRoutes(app, {
8603
8822
  defaultAgent: config.defaultAgent,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hydra-acp/cli",
3
- "version": "0.1.40",
3
+ "version": "0.1.42",
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",