@hydra-acp/cli 0.1.22 → 0.1.23

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/cli.js CHANGED
@@ -77,6 +77,12 @@ var init_paths = __esm({
77
77
  sessionDir: (id) => path.join(hydraHome(), "sessions", id),
78
78
  sessionFile: (id) => path.join(hydraHome(), "sessions", id, "meta.json"),
79
79
  historyFile: (id) => path.join(hydraHome(), "sessions", id, "history.jsonl"),
80
+ // Persisted prompt queue for a session. ndjson, one record per
81
+ // entry. Survives daemon restarts so queued prompts get a chance to
82
+ // run rather than being silently lost. Entries are removed BEFORE
83
+ // the agent invocation (see Session.drainQueue) so a crash mid-
84
+ // generation doesn't double-run on restart.
85
+ queueFile: (id) => path.join(hydraHome(), "sessions", id, "queue.ndjson"),
80
86
  extensionsDir: () => path.join(hydraHome(), "extensions"),
81
87
  extensionLogFile: (name) => path.join(hydraHome(), "extensions", `${name}.log`),
82
88
  extensionPidFile: (name) => path.join(hydraHome(), "extensions", `${name}.pid`),
@@ -86,8 +92,68 @@ var init_paths = __esm({
86
92
  }
87
93
  });
88
94
 
89
- // src/core/config.ts
95
+ // src/core/service-token.ts
90
96
  import * as fs from "fs/promises";
97
+ function generateServiceToken() {
98
+ const bytes = new Uint8Array(32);
99
+ crypto.getRandomValues(bytes);
100
+ let hex = "";
101
+ for (const b of bytes) {
102
+ hex += b.toString(16).padStart(2, "0");
103
+ }
104
+ return `hydra_token_${hex}`;
105
+ }
106
+ async function readServiceToken() {
107
+ try {
108
+ const text = await fs.readFile(paths.authToken(), "utf8");
109
+ const trimmed = text.trim();
110
+ return trimmed.length > 0 ? trimmed : void 0;
111
+ } catch (err) {
112
+ const e = err;
113
+ if (e.code === "ENOENT") {
114
+ return void 0;
115
+ }
116
+ throw err;
117
+ }
118
+ }
119
+ async function loadServiceToken() {
120
+ const token = await readServiceToken();
121
+ if (!token) {
122
+ throw new Error(
123
+ `No service token found at ${paths.authToken()}. Run \`hydra-acp init\` to create one.`
124
+ );
125
+ }
126
+ return token;
127
+ }
128
+ async function writeServiceToken(token) {
129
+ await fs.mkdir(paths.home(), { recursive: true });
130
+ await fs.writeFile(paths.authToken(), token + "\n", {
131
+ encoding: "utf8",
132
+ mode: 384
133
+ });
134
+ }
135
+ async function ensureServiceToken() {
136
+ const existing = await readServiceToken();
137
+ if (existing) {
138
+ return existing;
139
+ }
140
+ const token = generateServiceToken();
141
+ await writeServiceToken(token);
142
+ process.stderr.write(
143
+ `hydra-acp: initialized ${paths.authToken()} with a fresh service token.
144
+ `
145
+ );
146
+ return token;
147
+ }
148
+ var init_service_token = __esm({
149
+ "src/core/service-token.ts"() {
150
+ "use strict";
151
+ init_paths();
152
+ }
153
+ });
154
+
155
+ // src/core/config.ts
156
+ import * as fs2 from "fs/promises";
91
157
  import { homedir as homedir2 } from "os";
92
158
  import { z } from "zod";
93
159
  function extensionList(config) {
@@ -99,7 +165,7 @@ function extensionList(config) {
99
165
  async function readConfigFile() {
100
166
  let raw;
101
167
  try {
102
- raw = await fs.readFile(paths.config(), "utf8");
168
+ raw = await fs2.readFile(paths.config(), "utf8");
103
169
  } catch (err) {
104
170
  const e = err;
105
171
  if (e.code === "ENOENT") {
@@ -109,44 +175,34 @@ async function readConfigFile() {
109
175
  }
110
176
  return JSON.parse(raw);
111
177
  }
112
- async function loadAuthToken() {
113
- let tokenFile;
178
+ async function migrateLegacyAuthToken() {
179
+ const raw = await readConfigFile();
180
+ const daemon = raw.daemon;
181
+ const legacy = daemon && typeof daemon.authToken === "string" ? daemon.authToken : void 0;
182
+ if (!legacy) {
183
+ return;
184
+ }
185
+ let tokenFileExists = false;
114
186
  try {
115
- const text = await fs.readFile(paths.authToken(), "utf8");
116
- const trimmed = text.trim();
117
- if (trimmed.length > 0) {
118
- tokenFile = trimmed;
119
- }
187
+ await fs2.access(paths.authToken());
188
+ tokenFileExists = true;
120
189
  } catch (err) {
121
190
  const e = err;
122
191
  if (e.code !== "ENOENT") {
123
192
  throw err;
124
193
  }
125
194
  }
126
- const raw = await readConfigFile();
127
- const daemon = raw.daemon;
128
- const legacy = daemon && typeof daemon.authToken === "string" ? daemon.authToken : void 0;
129
- if (tokenFile && legacy) {
195
+ if (tokenFileExists) {
130
196
  throw new Error(
131
197
  `Auth token present in both ${paths.authToken()} and ${paths.config()} (daemon.authToken). Remove daemon.authToken from config.json to resolve.`
132
198
  );
133
199
  }
134
- if (tokenFile) {
135
- return tokenFile;
136
- }
137
- if (legacy) {
138
- await migrateLegacyAuthToken(raw, daemon, legacy);
139
- return legacy;
140
- }
141
- return void 0;
142
- }
143
- async function migrateLegacyAuthToken(raw, daemon, token) {
144
- await writeAuthToken(token);
200
+ await writeServiceToken(legacy);
145
201
  delete daemon.authToken;
146
202
  if (Object.keys(daemon).length === 0) {
147
203
  delete raw.daemon;
148
204
  }
149
- await fs.writeFile(paths.config(), JSON.stringify(raw, null, 2) + "\n", {
205
+ await fs2.writeFile(paths.config(), JSON.stringify(raw, null, 2) + "\n", {
150
206
  encoding: "utf8",
151
207
  mode: 384
152
208
  });
@@ -155,47 +211,9 @@ async function migrateLegacyAuthToken(raw, daemon, token) {
155
211
  `
156
212
  );
157
213
  }
158
- async function writeAuthToken(token) {
159
- await fs.mkdir(paths.home(), { recursive: true });
160
- await fs.writeFile(paths.authToken(), token + "\n", {
161
- encoding: "utf8",
162
- mode: 384
163
- });
164
- }
165
214
  async function loadConfig() {
166
- const token = await loadAuthToken();
167
- if (!token) {
168
- throw new Error(
169
- `No auth token found at ${paths.authToken()}. Run \`hydra-acp init\` to create one.`
170
- );
171
- }
172
- const raw = await readConfigFile();
173
- const daemon = raw.daemon ??= {};
174
- daemon.authToken = token;
175
- return HydraConfig.parse(raw);
176
- }
177
- async function loadConfigReadOnly() {
178
- return HydraConfigReadOnly.parse(await readConfigFile());
179
- }
180
- async function ensureConfig() {
181
- if (!await loadAuthToken()) {
182
- const token = generateAuthToken();
183
- await writeAuthToken(token);
184
- process.stderr.write(
185
- `hydra-acp: initialized ${paths.authToken()} with a fresh auth token.
186
- `
187
- );
188
- }
189
- return loadConfig();
190
- }
191
- function generateAuthToken() {
192
- const bytes = new Uint8Array(32);
193
- crypto.getRandomValues(bytes);
194
- let hex = "";
195
- for (const b of bytes) {
196
- hex += b.toString(16).padStart(2, "0");
197
- }
198
- return `hydra_token_${hex}`;
215
+ await migrateLegacyAuthToken();
216
+ return HydraConfig.parse(await readConfigFile());
199
217
  }
200
218
  function expandHome(p) {
201
219
  if (p === "~" || p === "$HOME") {
@@ -209,20 +227,21 @@ function expandHome(p) {
209
227
  }
210
228
  return p;
211
229
  }
212
- var REGISTRY_URL_DEFAULT, TlsConfig, DaemonConfig, RegistryConfig, TuiConfig, ExtensionName, ExtensionBody, HydraConfig, HydraConfigReadOnly;
230
+ var REGISTRY_URL_DEFAULT, TlsConfig, DEFAULT_DAEMON_PORT, DaemonConfig, RegistryConfig, TuiConfig, ExtensionName, ExtensionBody, HydraConfig;
213
231
  var init_config = __esm({
214
232
  "src/core/config.ts"() {
215
233
  "use strict";
216
234
  init_paths();
235
+ init_service_token();
217
236
  REGISTRY_URL_DEFAULT = "https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json";
218
237
  TlsConfig = z.object({
219
238
  cert: z.string(),
220
239
  key: z.string()
221
240
  });
241
+ DEFAULT_DAEMON_PORT = 55514;
222
242
  DaemonConfig = z.object({
223
243
  host: z.string().default("127.0.0.1"),
224
- port: z.number().int().positive().default(8765),
225
- authToken: z.string().min(16),
244
+ port: z.number().int().positive().default(DEFAULT_DAEMON_PORT),
226
245
  logLevel: z.enum(["debug", "info", "warn", "error"]).default("info"),
227
246
  tls: TlsConfig.optional(),
228
247
  sessionIdleTimeoutSeconds: z.number().int().nonnegative().default(3600),
@@ -283,7 +302,7 @@ var init_config = __esm({
283
302
  enabled: z.boolean().default(true)
284
303
  });
285
304
  HydraConfig = z.object({
286
- daemon: DaemonConfig,
305
+ daemon: DaemonConfig.default({}),
287
306
  registry: RegistryConfig.default({ url: REGISTRY_URL_DEFAULT, ttlHours: 24 }),
288
307
  defaultAgent: z.string().default("claude-acp"),
289
308
  // Optional per-agent default model id. When a brand-new agent process
@@ -303,6 +322,11 @@ var init_config = __esm({
303
322
  // recency and truncated to this count. `--all` overrides in the CLI.
304
323
  sessionListColdLimit: z.number().int().nonnegative().default(20),
305
324
  extensions: z.record(ExtensionName, ExtensionBody).default({}),
325
+ // npm registry URL used when installing npm-distributed agents into
326
+ // ~/.hydra-acp/agents. Overrides the global ~/.npmrc registry so a
327
+ // corporate .npmrc pointing at an internal registry doesn't break
328
+ // public-package installs. Omit to let npm use its own defaults.
329
+ npmRegistry: z.string().url().optional(),
306
330
  tui: TuiConfig.default({
307
331
  repaintThrottleMs: 1e3,
308
332
  maxScrollbackLines: 1e4,
@@ -312,9 +336,6 @@ var init_config = __esm({
312
336
  progressIndicator: true
313
337
  })
314
338
  });
315
- HydraConfigReadOnly = HydraConfig.extend({
316
- daemon: DaemonConfig.omit({ authToken: true }).default({})
317
- });
318
339
  }
319
340
  });
320
341
 
@@ -389,12 +410,64 @@ function extractHydraMeta(meta) {
389
410
  out.availableCommands = cmds;
390
411
  }
391
412
  }
413
+ if (typeof obj.promptQueueing === "boolean") {
414
+ out.promptQueueing = obj.promptQueueing;
415
+ }
416
+ if (Array.isArray(obj.queue)) {
417
+ const entries = [];
418
+ for (const raw of obj.queue) {
419
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
420
+ continue;
421
+ }
422
+ const r = raw;
423
+ const orig = r.originator;
424
+ if (typeof r.messageId !== "string" || !orig || typeof orig.clientId !== "string" || !Array.isArray(r.prompt) || typeof r.position !== "number" || typeof r.enqueuedAt !== "number") {
425
+ continue;
426
+ }
427
+ const originator = { clientId: orig.clientId };
428
+ if (typeof orig.name === "string") originator.name = orig.name;
429
+ if (typeof orig.version === "string") originator.version = orig.version;
430
+ entries.push({
431
+ messageId: r.messageId,
432
+ originator,
433
+ prompt: r.prompt,
434
+ position: r.position,
435
+ enqueuedAt: r.enqueuedAt
436
+ });
437
+ }
438
+ if (entries.length > 0) {
439
+ out.queue = entries;
440
+ }
441
+ }
442
+ if (Array.isArray(obj.availableModes)) {
443
+ const modes = [];
444
+ for (const raw of obj.availableModes) {
445
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
446
+ continue;
447
+ }
448
+ const m = raw;
449
+ if (typeof m.id !== "string") {
450
+ continue;
451
+ }
452
+ const mode = { id: m.id };
453
+ if (typeof m.name === "string") {
454
+ mode.name = m.name;
455
+ }
456
+ if (typeof m.description === "string") {
457
+ mode.description = m.description;
458
+ }
459
+ modes.push(mode);
460
+ }
461
+ if (modes.length > 0) {
462
+ out.availableModes = modes;
463
+ }
464
+ }
392
465
  return out;
393
466
  }
394
467
  function mergeMeta(passthrough, ours) {
395
468
  return { ...passthrough ?? {}, [HYDRA_META_KEY]: ours };
396
469
  }
397
- var ACP_PROTOCOL_VERSION, JsonRpcErrorCodes, InitializeParams, HistoryPolicy, SessionNewParams, SessionResumeHints, SessionAttachParams, HYDRA_META_KEY, SessionDetachParams, SessionListParams, SessionListUsage, SessionListEntry, SessionListResult, SessionPromptParams, SessionCancelParams, ProxyInitializeParams;
470
+ var ACP_PROTOCOL_VERSION, JsonRpcErrorCodes, InitializeParams, HistoryPolicy, SessionNewParams, SessionResumeHints, SessionAttachParams, HYDRA_META_KEY, SessionDetachParams, SessionListParams, SessionListUsage, SessionListEntry, SessionListResult, SessionPromptParams, SessionCancelParams, PromptOriginatorSchema, PromptQueueAddedParams, PromptQueueUpdatedParams, PromptQueueRemovedParams, CancelPromptParams, CancelPromptResult, UpdatePromptParams, UpdatePromptResult, ProxyInitializeParams;
398
471
  var init_types = __esm({
399
472
  "src/acp/types.ts"() {
400
473
  "use strict";
@@ -517,6 +590,49 @@ var init_types = __esm({
517
590
  SessionCancelParams = z3.object({
518
591
  sessionId: z3.string()
519
592
  });
593
+ PromptOriginatorSchema = z3.object({
594
+ clientId: z3.string(),
595
+ name: z3.string().optional(),
596
+ version: z3.string().optional()
597
+ });
598
+ PromptQueueAddedParams = z3.object({
599
+ sessionId: z3.string(),
600
+ messageId: z3.string(),
601
+ originator: PromptOriginatorSchema,
602
+ prompt: z3.array(z3.unknown()),
603
+ // 0 = head (currently in-flight). At enqueue time the new entry's
604
+ // position equals the count of entries already ahead of it.
605
+ position: z3.number().int().nonnegative(),
606
+ queueDepth: z3.number().int().positive(),
607
+ enqueuedAt: z3.number()
608
+ });
609
+ PromptQueueUpdatedParams = z3.object({
610
+ sessionId: z3.string(),
611
+ messageId: z3.string(),
612
+ prompt: z3.array(z3.unknown())
613
+ });
614
+ PromptQueueRemovedParams = z3.object({
615
+ sessionId: z3.string(),
616
+ messageId: z3.string(),
617
+ reason: z3.enum(["started", "cancelled", "abandoned"])
618
+ });
619
+ CancelPromptParams = z3.object({
620
+ sessionId: z3.string(),
621
+ messageId: z3.string()
622
+ });
623
+ CancelPromptResult = z3.object({
624
+ cancelled: z3.boolean(),
625
+ reason: z3.enum(["ok", "not_found", "already_running"])
626
+ });
627
+ UpdatePromptParams = z3.object({
628
+ sessionId: z3.string(),
629
+ messageId: z3.string(),
630
+ prompt: z3.array(z3.unknown())
631
+ });
632
+ UpdatePromptResult = z3.object({
633
+ updated: z3.boolean(),
634
+ reason: z3.enum(["ok", "not_found", "already_running"])
635
+ });
520
636
  ProxyInitializeParams = z3.object({
521
637
  protocolVersion: z3.number().optional(),
522
638
  proxyInfo: z3.object({
@@ -766,6 +882,53 @@ var init_hydra_commands = __esm({
766
882
  }
767
883
  });
768
884
 
885
+ // src/core/queue-store.ts
886
+ import * as fs6 from "fs/promises";
887
+ async function rewriteQueue(sessionId, entries) {
888
+ const file = paths.queueFile(sessionId);
889
+ if (entries.length === 0) {
890
+ await fs6.unlink(file).catch(() => void 0);
891
+ return;
892
+ }
893
+ await fs6.mkdir(paths.sessionDir(sessionId), { recursive: true });
894
+ const body = entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
895
+ await fs6.writeFile(file, body, "utf8");
896
+ }
897
+ async function loadQueue(sessionId) {
898
+ const file = paths.queueFile(sessionId);
899
+ let text;
900
+ try {
901
+ text = await fs6.readFile(file, "utf8");
902
+ } catch (err) {
903
+ if (err.code === "ENOENT") {
904
+ return [];
905
+ }
906
+ throw err;
907
+ }
908
+ const out = [];
909
+ for (const line of text.split("\n")) {
910
+ if (!line.trim()) continue;
911
+ try {
912
+ const parsed = JSON.parse(line);
913
+ if (parsed && typeof parsed.messageId === "string" && Array.isArray(parsed.prompt) && typeof parsed.enqueuedAt === "number") {
914
+ out.push(parsed);
915
+ }
916
+ } catch {
917
+ }
918
+ }
919
+ return out;
920
+ }
921
+ async function deleteQueue(sessionId) {
922
+ const file = paths.queueFile(sessionId);
923
+ await fs6.unlink(file).catch(() => void 0);
924
+ }
925
+ var init_queue_store = __esm({
926
+ "src/core/queue-store.ts"() {
927
+ "use strict";
928
+ init_paths();
929
+ }
930
+ });
931
+
769
932
  // src/core/session.ts
770
933
  import { customAlphabet } from "nanoid";
771
934
  function generateMessageId() {
@@ -797,6 +960,47 @@ function sameAdvertisedCommands(a, b) {
797
960
  }
798
961
  return true;
799
962
  }
963
+ function sameAdvertisedModes(a, b) {
964
+ if (a.length !== b.length) {
965
+ return false;
966
+ }
967
+ for (let i = 0; i < a.length; i++) {
968
+ if (a[i]?.id !== b[i]?.id || a[i]?.name !== b[i]?.name || a[i]?.description !== b[i]?.description) {
969
+ return false;
970
+ }
971
+ }
972
+ return true;
973
+ }
974
+ function extractAdvertisedModes(params) {
975
+ const obj = params ?? {};
976
+ const update = obj.update ?? {};
977
+ if (update.sessionUpdate !== "available_modes_update") {
978
+ return null;
979
+ }
980
+ const list = update.availableModes;
981
+ if (!Array.isArray(list)) {
982
+ return [];
983
+ }
984
+ const out = [];
985
+ for (const raw of list) {
986
+ if (!raw || typeof raw !== "object") {
987
+ continue;
988
+ }
989
+ const m = raw;
990
+ if (typeof m.id !== "string" || m.id.length === 0) {
991
+ continue;
992
+ }
993
+ const mode = { id: m.id };
994
+ if (typeof m.name === "string") {
995
+ mode.name = m.name;
996
+ }
997
+ if (typeof m.description === "string") {
998
+ mode.description = m.description;
999
+ }
1000
+ out.push(mode);
1001
+ }
1002
+ return out;
1003
+ }
800
1004
  function captureInternalChunk(capture, params) {
801
1005
  const obj = params ?? {};
802
1006
  const update = obj.update ?? {};
@@ -955,6 +1159,7 @@ var init_session = __esm({
955
1159
  "src/core/session.ts"() {
956
1160
  "use strict";
957
1161
  init_hydra_commands();
1162
+ init_queue_store();
958
1163
  init_types();
959
1164
  HYDRA_ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
960
1165
  generateHydraId = customAlphabet(HYDRA_ID_ALPHABET, 16);
@@ -984,7 +1189,18 @@ var init_session = __esm({
984
1189
  clients = /* @__PURE__ */ new Map();
985
1190
  historyStore;
986
1191
  promptQueue = [];
1192
+ // The entry that drainQueue is currently awaiting. Distinct from
1193
+ // promptQueue[0] (which is the *next* one to dequeue): once shifted
1194
+ // off, the entry lives here for the duration of its task() so
1195
+ // cancelQueuedPrompt can distinguish "still in line" from "running"
1196
+ // and return already_running for the latter.
1197
+ currentEntry;
987
1198
  promptInFlight = false;
1199
+ // Serialize disk writes to the persisted queue file. Without this
1200
+ // chain, fire-and-forget appends/rewrites can interleave (e.g.
1201
+ // drainQueue's rewrite-to-empty races a sibling's append-on-
1202
+ // enqueue) and leave the file out of sync with in-memory state.
1203
+ queueWriteChain = Promise.resolve();
988
1204
  closed = false;
989
1205
  closeHandlers = [];
990
1206
  titleHandlers = [];
@@ -1037,10 +1253,14 @@ var init_session = __esm({
1037
1253
  // can deliver the merged list via _meta without depending on history
1038
1254
  // replay.
1039
1255
  agentAdvertisedCommands = [];
1256
+ // Last available_modes_update we observed from the agent. Same
1257
+ // pattern as commands: cache, persist, broadcast on change.
1258
+ agentAdvertisedModes = [];
1040
1259
  // Persist hooks for snapshot-shaped state. SessionManager hooks these
1041
1260
  // to mirror changes into meta.json so cold-resurrect attaches can
1042
1261
  // surface the latest snapshot via the attach response _meta.
1043
1262
  agentCommandsHandlers = [];
1263
+ agentModesHandlers = [];
1044
1264
  modelHandlers = [];
1045
1265
  modeHandlers = [];
1046
1266
  usageHandlers = [];
@@ -1059,6 +1279,9 @@ var init_session = __esm({
1059
1279
  if (init.agentCommands && init.agentCommands.length > 0) {
1060
1280
  this.agentAdvertisedCommands = [...init.agentCommands];
1061
1281
  }
1282
+ if (init.agentModes && init.agentModes.length > 0) {
1283
+ this.agentAdvertisedModes = [...init.agentModes];
1284
+ }
1062
1285
  this.idleTimeoutMs = init.idleTimeoutMs ?? 0;
1063
1286
  this.spawnReplacementAgent = init.spawnReplacementAgent;
1064
1287
  this.logger = init.logger;
@@ -1087,6 +1310,15 @@ var init_session = __esm({
1087
1310
  }
1088
1311
  });
1089
1312
  }
1313
+ broadcastAvailableModes() {
1314
+ this.recordAndBroadcast("session/update", {
1315
+ sessionId: this.upstreamSessionId,
1316
+ update: {
1317
+ sessionUpdate: "available_modes_update",
1318
+ availableModes: this.agentAdvertisedModes
1319
+ }
1320
+ });
1321
+ }
1090
1322
  // Register session/update, session/request_permission, and onExit
1091
1323
  // handlers on an agent connection. Re-run on every /hydra agent so
1092
1324
  // the new agent is plumbed identically. The exit handler's identity
@@ -1104,6 +1336,11 @@ var init_session = __esm({
1104
1336
  this.setAgentAdvertisedCommands(agentCmds);
1105
1337
  return;
1106
1338
  }
1339
+ const agentModes = extractAdvertisedModes(params);
1340
+ if (agentModes !== null) {
1341
+ this.setAgentAdvertisedModes(agentModes);
1342
+ return;
1343
+ }
1107
1344
  if (this.maybeApplyAgentModel(params)) {
1108
1345
  this.recordAndBroadcast("session/update", params);
1109
1346
  return;
@@ -1280,7 +1517,7 @@ var init_session = __esm({
1280
1517
  sessionId,
1281
1518
  update: {
1282
1519
  sessionUpdate: "current_mode_update",
1283
- currentMode: this.currentMode
1520
+ currentModeId: this.currentMode
1284
1521
  }
1285
1522
  },
1286
1523
  recordedAt
@@ -1300,6 +1537,19 @@ var init_session = __esm({
1300
1537
  recordedAt
1301
1538
  });
1302
1539
  }
1540
+ if (this.agentAdvertisedModes.length > 0) {
1541
+ out.push({
1542
+ method: "session/update",
1543
+ params: {
1544
+ sessionId,
1545
+ update: {
1546
+ sessionUpdate: "available_modes_update",
1547
+ availableModes: [...this.agentAdvertisedModes]
1548
+ }
1549
+ },
1550
+ recordedAt
1551
+ });
1552
+ }
1303
1553
  if (this.currentUsage !== void 0) {
1304
1554
  const u = this.currentUsage;
1305
1555
  const update = {
@@ -1390,34 +1640,28 @@ var init_session = __esm({
1390
1640
  if (promptText.startsWith("/hydra")) {
1391
1641
  return this.handleSlashCommand(promptText);
1392
1642
  }
1393
- this.broadcastPromptReceived(client, params);
1643
+ const messageId = generateMessageId();
1394
1644
  this.maybeSeedTitleFromPrompt(params);
1395
- return this.enqueuePrompt(async () => {
1396
- let response;
1397
- try {
1398
- response = await this.agent.connection.request(
1399
- "session/prompt",
1400
- {
1401
- ...params,
1402
- sessionId: this.upstreamSessionId
1403
- }
1404
- );
1405
- } catch (err) {
1406
- this.broadcastTurnComplete(client.clientId, { stopReason: "error" });
1407
- throw err;
1408
- }
1409
- this.broadcastTurnComplete(client.clientId, response);
1410
- return response;
1411
- });
1412
- }
1413
- broadcastPromptReceived(client, params) {
1414
- const promptParams = params ?? {};
1415
- const sentBy = { clientId: client.clientId };
1416
- if (client.clientInfo?.name) {
1417
- sentBy.name = client.clientInfo.name;
1418
- }
1419
- if (client.clientInfo?.version) {
1420
- sentBy.version = client.clientInfo.version;
1645
+ return this.enqueueUserPrompt(client, params, messageId);
1646
+ }
1647
+ // DEVIATION FROM RFD #533: this broadcast is deliberately deferred
1648
+ // until the prompt actually becomes the active turn (i.e. drainQueue
1649
+ // is about to forward it to the agent), NOT when hydra first accepts
1650
+ // the request. The literal RFD doesn't pin the timing — it just says
1651
+ // peers should learn about the turn — but it was authored before
1652
+ // prompt queueing existed, so accept-time and start-time were the
1653
+ // same moment. With hydra's per-session FIFO, deferring gives
1654
+ // prompt_received a single, useful meaning ("the agent is now taking
1655
+ // a turn on this prompt"), which is how attached clients (notably
1656
+ // agent-shell) consume it. The accept-time signal that peers can use
1657
+ // for queue chip rendering is hydra-acp/prompt_queue_added instead.
1658
+ broadcastPromptReceived(entry) {
1659
+ const sentBy = { clientId: entry.originator.clientId };
1660
+ if (entry.originator.name) {
1661
+ sentBy.name = entry.originator.name;
1662
+ }
1663
+ if (entry.originator.version) {
1664
+ sentBy.version = entry.originator.version;
1421
1665
  }
1422
1666
  this.promptStartedAt = Date.now();
1423
1667
  this.recordAndBroadcast(
@@ -1426,14 +1670,14 @@ var init_session = __esm({
1426
1670
  sessionId: this.sessionId,
1427
1671
  update: {
1428
1672
  sessionUpdate: "prompt_received",
1429
- messageId: generateMessageId(),
1430
- prompt: promptParams.prompt,
1673
+ messageId: entry.messageId,
1674
+ prompt: entry.prompt,
1431
1675
  sentBy
1432
1676
  }
1433
1677
  },
1434
- client.clientId
1678
+ entry.clientId
1435
1679
  );
1436
- const text = extractPromptText(promptParams.prompt);
1680
+ const text = extractPromptText(entry.prompt);
1437
1681
  if (text.length > 0) {
1438
1682
  this.recordAndBroadcast(
1439
1683
  "session/update",
@@ -1445,7 +1689,7 @@ var init_session = __esm({
1445
1689
  _meta: { "hydra-acp": { compatFor: "prompt_received" } }
1446
1690
  }
1447
1691
  },
1448
- client.clientId
1692
+ entry.clientId
1449
1693
  );
1450
1694
  }
1451
1695
  }
@@ -1468,6 +1712,172 @@ var init_session = __esm({
1468
1712
  originatorClientId
1469
1713
  );
1470
1714
  }
1715
+ // Total visible-or-running entries: the in-flight head (if any) plus
1716
+ // the queue's user-visible waiting entries. Internal entries don't
1717
+ // count — they're an implementation detail and the wire never
1718
+ // surfaces them.
1719
+ visibleQueueDepth() {
1720
+ let count = this.currentEntry?.kind === "user" && !this.currentEntry.cancelled ? 1 : 0;
1721
+ for (const entry of this.promptQueue) {
1722
+ if (entry.kind === "user" && !entry.cancelled) count += 1;
1723
+ }
1724
+ return count;
1725
+ }
1726
+ broadcastQueueAdded(entry) {
1727
+ const depth = this.visibleQueueDepth();
1728
+ const position = Math.max(0, depth - 1);
1729
+ const params = {
1730
+ sessionId: this.sessionId,
1731
+ messageId: entry.messageId,
1732
+ originator: entry.originator,
1733
+ prompt: entry.prompt,
1734
+ position,
1735
+ queueDepth: depth,
1736
+ enqueuedAt: entry.enqueuedAt
1737
+ };
1738
+ this.broadcastQueueNotification("hydra-acp/prompt_queue_added", params);
1739
+ }
1740
+ broadcastQueueUpdated(messageId, prompt) {
1741
+ this.broadcastQueueNotification("hydra-acp/prompt_queue_updated", {
1742
+ sessionId: this.sessionId,
1743
+ messageId,
1744
+ prompt
1745
+ });
1746
+ }
1747
+ broadcastQueueRemoved(messageId, reason) {
1748
+ this.broadcastQueueNotification("hydra-acp/prompt_queue_removed", {
1749
+ sessionId: this.sessionId,
1750
+ messageId,
1751
+ reason
1752
+ });
1753
+ }
1754
+ // Fan-out for queue lifecycle notifications. Ephemeral by design —
1755
+ // these signals describe transient daemon state, not conversation
1756
+ // content, so we deliberately bypass recordAndBroadcast (no history,
1757
+ // no idle-timer arm, no rewrite-for-client since we already emit the
1758
+ // hydra sessionId).
1759
+ broadcastQueueNotification(method, params) {
1760
+ for (const client of this.clients.values()) {
1761
+ void client.connection.notify(method, params).catch(() => void 0);
1762
+ }
1763
+ }
1764
+ // Snapshot of user-visible queue state at this moment. Surfaced to
1765
+ // late-attaching clients via the session/attach response _meta so
1766
+ // they boot with the same chip list as their peers without waiting
1767
+ // for new prompt_queue_added notifications. Internal entries are
1768
+ // omitted (they're not surfaced on the wire at all).
1769
+ queueSnapshot() {
1770
+ const out = [];
1771
+ let position = 0;
1772
+ if (this.currentEntry?.kind === "user" && !this.currentEntry.cancelled) {
1773
+ out.push({
1774
+ messageId: this.currentEntry.messageId,
1775
+ originator: this.currentEntry.originator,
1776
+ prompt: this.currentEntry.prompt,
1777
+ position: position++,
1778
+ enqueuedAt: this.currentEntry.enqueuedAt
1779
+ });
1780
+ }
1781
+ for (const entry of this.promptQueue) {
1782
+ if (entry.kind !== "user" || entry.cancelled) continue;
1783
+ out.push({
1784
+ messageId: entry.messageId,
1785
+ originator: entry.originator,
1786
+ prompt: entry.prompt,
1787
+ position: position++,
1788
+ enqueuedAt: entry.enqueuedAt
1789
+ });
1790
+ }
1791
+ return out;
1792
+ }
1793
+ // Wait for any pending queue-file writes to settle. Test hook so
1794
+ // assertions about on-disk state don't race with fire-and-forget
1795
+ // rewrites. Production code doesn't need this — the chain
1796
+ // self-serializes.
1797
+ async flushPersistWrites() {
1798
+ await this.queueWriteChain.catch(() => void 0);
1799
+ }
1800
+ // Push pre-existing queue entries back through the daemon-side
1801
+ // pipeline on startup. Called by SessionManager after resurrecting
1802
+ // a session that had a non-empty queue.ndjson on disk. Each entry
1803
+ // gets a synthetic UserPromptQueueEntry with no real caller
1804
+ // (resolve/reject are no-ops since the original WS is long gone),
1805
+ // then drainQueue picks it up like any other entry. Late-attaching
1806
+ // clients see the entries via prompt_queue_added broadcasts and the
1807
+ // attach-response snapshot.
1808
+ replayPersistedQueue(entries) {
1809
+ for (const persisted of entries) {
1810
+ const originator = {
1811
+ clientId: `hydra-resurrected_${persisted.messageId}`
1812
+ };
1813
+ if (persisted.originator.clientInfo.name !== void 0) {
1814
+ originator.name = persisted.originator.clientInfo.name;
1815
+ }
1816
+ if (persisted.originator.clientInfo.version !== void 0) {
1817
+ originator.version = persisted.originator.clientInfo.version;
1818
+ }
1819
+ const entry = {
1820
+ kind: "user",
1821
+ messageId: persisted.messageId,
1822
+ originator,
1823
+ // Synthetic clientId. broadcastTurnComplete uses this as
1824
+ // excludeClientId for the peer-only broadcast; with a synthetic
1825
+ // id no real attached client matches the exclude, so everyone
1826
+ // sees turn_complete — which is what we want, since none of
1827
+ // them originated this restart-replayed prompt.
1828
+ clientId: originator.clientId,
1829
+ prompt: persisted.prompt,
1830
+ enqueuedAt: persisted.enqueuedAt,
1831
+ cancelled: false,
1832
+ resolve: () => void 0,
1833
+ reject: () => void 0
1834
+ };
1835
+ this.promptQueue.push(entry);
1836
+ this.broadcastQueueAdded(entry);
1837
+ }
1838
+ void this.drainQueue();
1839
+ }
1840
+ // Drop a queued prompt by messageId. Returns already_running when
1841
+ // the messageId names the in-flight entry — callers should fall back
1842
+ // to session/cancel for that case. Originator-agnostic: any attached
1843
+ // client may cancel any queued prompt (matches the existing slack
1844
+ // :stop_sign: reaction UX and the TUI's queue-edit dispatcher).
1845
+ cancelQueuedPrompt(messageId) {
1846
+ if (this.currentEntry?.messageId === messageId) {
1847
+ return { cancelled: false, reason: "already_running" };
1848
+ }
1849
+ const idx = this.promptQueue.findIndex((e) => e.messageId === messageId);
1850
+ if (idx < 0) {
1851
+ return { cancelled: false, reason: "not_found" };
1852
+ }
1853
+ const entry = this.promptQueue[idx];
1854
+ entry.cancelled = true;
1855
+ this.promptQueue.splice(idx, 1);
1856
+ if (entry.kind === "user") {
1857
+ this.broadcastQueueRemoved(messageId, "cancelled");
1858
+ this.persistRewrite();
1859
+ }
1860
+ entry.resolve({ stopReason: "cancelled" });
1861
+ return { cancelled: true, reason: "ok" };
1862
+ }
1863
+ // Replace the prompt payload of a queued (not-yet-running) entry.
1864
+ // Returns already_running for the in-flight head; not_found for
1865
+ // unknown messageIds or for internal queue entries (internal tasks
1866
+ // don't expose a mutable prompt). Broadcasts prompt_queue_updated on
1867
+ // success so every attached client refreshes its chip.
1868
+ updateQueuedPrompt(messageId, prompt) {
1869
+ if (this.currentEntry?.messageId === messageId) {
1870
+ return { updated: false, reason: "already_running" };
1871
+ }
1872
+ const entry = this.promptQueue.find((e) => e.messageId === messageId);
1873
+ if (!entry || entry.kind !== "user") {
1874
+ return { updated: false, reason: "not_found" };
1875
+ }
1876
+ entry.prompt = prompt;
1877
+ this.broadcastQueueUpdated(messageId, prompt);
1878
+ this.persistRewrite();
1879
+ return { updated: true, reason: "ok" };
1880
+ }
1471
1881
  async cancel(clientId) {
1472
1882
  const client = this.clients.get(clientId);
1473
1883
  if (!client) {
@@ -1594,7 +2004,7 @@ var init_session = __esm({
1594
2004
  if (update.sessionUpdate !== "current_mode_update") {
1595
2005
  return false;
1596
2006
  }
1597
- const raw = typeof update.currentMode === "string" ? update.currentMode : typeof update.mode === "string" ? update.mode : void 0;
2007
+ const raw = typeof update.currentModeId === "string" ? update.currentModeId : typeof update.currentMode === "string" ? update.currentMode : typeof update.mode === "string" ? update.mode : void 0;
1598
2008
  if (raw === void 0) {
1599
2009
  return true;
1600
2010
  }
@@ -1672,12 +2082,29 @@ var init_session = __esm({
1672
2082
  }
1673
2083
  this.broadcastMergedCommands();
1674
2084
  }
2085
+ setAgentAdvertisedModes(modes) {
2086
+ if (sameAdvertisedModes(this.agentAdvertisedModes, modes)) {
2087
+ this.broadcastAvailableModes();
2088
+ return;
2089
+ }
2090
+ this.agentAdvertisedModes = modes;
2091
+ for (const handler of this.agentModesHandlers) {
2092
+ try {
2093
+ handler(modes);
2094
+ } catch {
2095
+ }
2096
+ }
2097
+ this.broadcastAvailableModes();
2098
+ }
1675
2099
  // Subscribe to snapshot-state updates. SessionManager wires these to
1676
2100
  // persist the new value into meta.json so cold resurrect can restore
1677
2101
  // them via the attach response _meta.
1678
2102
  onAgentCommandsChange(handler) {
1679
2103
  this.agentCommandsHandlers.push(handler);
1680
2104
  }
2105
+ onAgentModesChange(handler) {
2106
+ this.agentModesHandlers.push(handler);
2107
+ }
1681
2108
  onModelChange(handler) {
1682
2109
  this.modelHandlers.push(handler);
1683
2110
  }
@@ -1699,6 +2126,10 @@ var init_session = __esm({
1699
2126
  agentOnlyAdvertisedCommands() {
1700
2127
  return [...this.agentAdvertisedCommands];
1701
2128
  }
2129
+ // The agent's advertised modes list, for callers that need a snapshot.
2130
+ availableModes() {
2131
+ return [...this.agentAdvertisedModes];
2132
+ }
1702
2133
  // Pick up an agent-emitted session_info_update and store its title
1703
2134
  // as our canonical record. The notification is also forwarded to
1704
2135
  // clients via the surrounding recordAndBroadcast call. Authoritative
@@ -2011,6 +2442,20 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
2011
2442
  }
2012
2443
  this.closed = true;
2013
2444
  this.cancelIdleTimer();
2445
+ const stranded = this.promptQueue;
2446
+ this.promptQueue = [];
2447
+ for (const entry of stranded) {
2448
+ entry.cancelled = true;
2449
+ if (entry.kind === "user") {
2450
+ this.broadcastQueueRemoved(entry.messageId, "abandoned");
2451
+ }
2452
+ try {
2453
+ entry.resolve({ stopReason: "cancelled" });
2454
+ } catch {
2455
+ }
2456
+ }
2457
+ const sessionId = this.sessionId;
2458
+ this.queueWriteChain = this.queueWriteChain.catch(() => void 0).then(() => deleteQueue(sessionId).catch(() => void 0));
2014
2459
  for (const client of this.clients.values()) {
2015
2460
  void client.connection.notify("hydra-acp/session_closed", { sessionId: this.sessionId }).catch(() => void 0);
2016
2461
  }
@@ -2056,7 +2501,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
2056
2501
  if (this.closed || this.idleTimeoutMs <= 0) {
2057
2502
  return;
2058
2503
  }
2059
- if (this.turnStartedAt !== void 0 || this.inFlightPermissions.size > 0) {
2504
+ if (this.turnStartedAt !== void 0 || this.inFlightPermissions.size > 0 || this.promptQueue.length > 0) {
2060
2505
  this.armIdleTimer(this.idleTimeoutMs);
2061
2506
  return;
2062
2507
  }
@@ -2185,20 +2630,88 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
2185
2630
  }
2186
2631
  });
2187
2632
  }
2633
+ // Schedule an internal task (title regen, agent swap transcript
2634
+ // injection, import seed). Serializes behind any user prompts already
2635
+ // in flight, but doesn't emit prompt_queue_* broadcasts — clients
2636
+ // shouldn't see hydra's housekeeping in their chip list.
2188
2637
  async enqueuePrompt(task) {
2189
2638
  return new Promise((resolve5, reject) => {
2190
- const run3 = async () => {
2191
- try {
2192
- const result = await task();
2193
- resolve5(result);
2194
- } catch (err) {
2195
- reject(err);
2196
- }
2639
+ const entry = {
2640
+ kind: "internal",
2641
+ messageId: generateMessageId(),
2642
+ enqueuedAt: Date.now(),
2643
+ cancelled: false,
2644
+ task,
2645
+ resolve: resolve5,
2646
+ reject
2647
+ };
2648
+ this.promptQueue.push(entry);
2649
+ void this.drainQueue();
2650
+ });
2651
+ }
2652
+ // Schedule a user-originated session/prompt. Emits prompt_queue_added
2653
+ // immediately on enqueue (so peer clients can render the queued chip)
2654
+ // and prompt_queue_removed when the entry leaves the queue. The
2655
+ // returned promise resolves with the upstream agent's session/prompt
2656
+ // result, or { stopReason: "cancelled" } if the entry is dropped via
2657
+ // cancelQueuedPrompt before reaching the head.
2658
+ async enqueueUserPrompt(client, params, messageId) {
2659
+ const promptArray = (params ?? {}).prompt ?? [];
2660
+ const originator = { clientId: client.clientId };
2661
+ if (client.clientInfo?.name) originator.name = client.clientInfo.name;
2662
+ if (client.clientInfo?.version)
2663
+ originator.version = client.clientInfo.version;
2664
+ return new Promise((resolve5, reject) => {
2665
+ const entry = {
2666
+ kind: "user",
2667
+ messageId,
2668
+ originator,
2669
+ clientId: client.clientId,
2670
+ prompt: promptArray,
2671
+ enqueuedAt: Date.now(),
2672
+ cancelled: false,
2673
+ resolve: resolve5,
2674
+ reject
2197
2675
  };
2198
- this.promptQueue.push(run3);
2676
+ this.promptQueue.push(entry);
2677
+ this.persistRewrite();
2678
+ this.broadcastQueueAdded(entry);
2199
2679
  void this.drainQueue();
2200
2680
  });
2201
2681
  }
2682
+ // Rewrite the on-disk queue to reflect the current set of WAITING
2683
+ // entries (excluding currentEntry, the in-flight head). Excluding
2684
+ // the head is the key idempotency choice: once drainQueue shifts an
2685
+ // entry off and calls persistRewrite, a daemon crash mid-generation
2686
+ // will NOT re-run it on restart. Partial output (if any streamed
2687
+ // before the crash) stays in history; the prompt itself is lost
2688
+ // and the user can re-submit if they care.
2689
+ //
2690
+ // Snapshots in-memory state synchronously (so subsequent mutations
2691
+ // can't perturb what we're about to write) and chains the write
2692
+ // onto queueWriteChain so all persists are serialized.
2693
+ persistRewrite() {
2694
+ const entries = [];
2695
+ for (const entry of this.promptQueue) {
2696
+ if (entry.kind !== "user" || entry.cancelled) continue;
2697
+ entries.push(this.persistedFromEntry(entry));
2698
+ }
2699
+ const sessionId = this.sessionId;
2700
+ this.queueWriteChain = this.queueWriteChain.catch(() => void 0).then(() => rewriteQueue(sessionId, entries).catch(() => void 0));
2701
+ }
2702
+ persistedFromEntry(entry) {
2703
+ return {
2704
+ messageId: entry.messageId,
2705
+ originator: {
2706
+ clientInfo: {
2707
+ ...entry.originator.name !== void 0 ? { name: entry.originator.name } : {},
2708
+ ...entry.originator.version !== void 0 ? { version: entry.originator.version } : {}
2709
+ }
2710
+ },
2711
+ prompt: entry.prompt,
2712
+ enqueuedAt: entry.enqueuedAt
2713
+ };
2714
+ }
2202
2715
  async drainQueue() {
2203
2716
  if (this.promptInFlight) {
2204
2717
  return;
@@ -2207,27 +2720,78 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
2207
2720
  try {
2208
2721
  while (this.promptQueue.length > 0) {
2209
2722
  const next = this.promptQueue.shift();
2210
- if (next) {
2211
- await next();
2723
+ if (!next) {
2724
+ break;
2725
+ }
2726
+ if (next.cancelled) {
2727
+ continue;
2728
+ }
2729
+ this.currentEntry = next;
2730
+ if (next.kind === "user") {
2731
+ this.persistRewrite();
2732
+ }
2733
+ if (next.kind === "user") {
2734
+ this.broadcastQueueRemoved(next.messageId, "started");
2735
+ }
2736
+ try {
2737
+ const result = await this.runQueueEntry(next);
2738
+ next.resolve(result);
2739
+ } catch (err) {
2740
+ next.reject(err);
2741
+ } finally {
2742
+ this.currentEntry = void 0;
2212
2743
  }
2213
2744
  }
2214
2745
  } finally {
2215
2746
  this.promptInFlight = false;
2216
2747
  }
2217
2748
  }
2749
+ // Execute a queue entry. User-prompt entries forward to the upstream
2750
+ // agent and pair with broadcastTurnComplete; internal entries run
2751
+ // their captured task closure. Reads entry.prompt at dispatch time
2752
+ // so updateQueuedPrompt's mutations are honoured.
2753
+ //
2754
+ // For user entries, broadcastPromptReceived fires HERE — not in
2755
+ // Session.prompt — so peer clients see prompt_received only when the
2756
+ // turn actually starts (a deliberate deviation from a naive reading
2757
+ // of RFD #533; see the comment on broadcastPromptReceived). Order on
2758
+ // the wire: prompt_queue_removed{started} (already emitted by
2759
+ // drainQueue) → prompt_received → upstream session/prompt.
2760
+ async runQueueEntry(entry) {
2761
+ if (entry.kind === "internal") {
2762
+ return entry.task();
2763
+ }
2764
+ this.broadcastPromptReceived(entry);
2765
+ let response;
2766
+ try {
2767
+ response = await this.agent.connection.request(
2768
+ "session/prompt",
2769
+ {
2770
+ sessionId: this.upstreamSessionId,
2771
+ prompt: entry.prompt
2772
+ }
2773
+ );
2774
+ } catch (err) {
2775
+ this.broadcastTurnComplete(entry.clientId, { stopReason: "error" });
2776
+ throw err;
2777
+ }
2778
+ this.broadcastTurnComplete(entry.clientId, response);
2779
+ return response;
2780
+ }
2218
2781
  };
2219
2782
  STATE_UPDATE_KINDS = /* @__PURE__ */ new Set([
2220
2783
  "session_info_update",
2221
2784
  "current_model_update",
2222
2785
  "current_mode_update",
2223
2786
  "available_commands_update",
2787
+ "available_modes_update",
2224
2788
  "usage_update"
2225
2789
  ]);
2226
2790
  }
2227
2791
  });
2228
2792
 
2229
2793
  // src/core/session-store.ts
2230
- import * as fs5 from "fs/promises";
2794
+ import * as fs7 from "fs/promises";
2231
2795
  import * as path4 from "path";
2232
2796
  import { customAlphabet as customAlphabet2 } from "nanoid";
2233
2797
  import { z as z4 } from "zod";
@@ -2256,11 +2820,12 @@ function recordFromMemorySession(args) {
2256
2820
  currentMode: args.currentMode,
2257
2821
  currentUsage: args.currentUsage,
2258
2822
  agentCommands: args.agentCommands,
2823
+ agentModes: args.agentModes,
2259
2824
  createdAt: args.createdAt ?? now,
2260
2825
  updatedAt: args.updatedAt ?? now
2261
2826
  };
2262
2827
  }
2263
- var HYDRA_ID_ALPHABET2, generateRawId, HYDRA_LINEAGE_PREFIX, PersistedAgentCommand, PersistedUsage, SessionRecord, SESSION_ID_PATTERN, SessionStore;
2828
+ var HYDRA_ID_ALPHABET2, generateRawId, HYDRA_LINEAGE_PREFIX, PersistedAgentCommand, PersistedAgentMode, PersistedUsage, SessionRecord, SESSION_ID_PATTERN, SessionStore;
2264
2829
  var init_session_store = __esm({
2265
2830
  "src/core/session-store.ts"() {
2266
2831
  "use strict";
@@ -2272,6 +2837,11 @@ var init_session_store = __esm({
2272
2837
  name: z4.string(),
2273
2838
  description: z4.string().optional()
2274
2839
  });
2840
+ PersistedAgentMode = z4.object({
2841
+ id: z4.string(),
2842
+ name: z4.string().optional(),
2843
+ description: z4.string().optional()
2844
+ });
2275
2845
  PersistedUsage = z4.object({
2276
2846
  used: z4.number().optional(),
2277
2847
  size: z4.number().optional(),
@@ -2317,6 +2887,7 @@ var init_session_store = __esm({
2317
2887
  currentMode: z4.string().optional(),
2318
2888
  currentUsage: PersistedUsage.optional(),
2319
2889
  agentCommands: z4.array(PersistedAgentCommand).optional(),
2890
+ agentModes: z4.array(PersistedAgentMode).optional(),
2320
2891
  createdAt: z4.string(),
2321
2892
  updatedAt: z4.string()
2322
2893
  });
@@ -2324,9 +2895,9 @@ var init_session_store = __esm({
2324
2895
  SessionStore = class {
2325
2896
  async write(record) {
2326
2897
  assertSafeId(record.sessionId);
2327
- await fs5.mkdir(paths.sessionDir(record.sessionId), { recursive: true });
2898
+ await fs7.mkdir(paths.sessionDir(record.sessionId), { recursive: true });
2328
2899
  const full = { version: 1, ...record };
2329
- await fs5.writeFile(
2900
+ await fs7.writeFile(
2330
2901
  paths.sessionFile(record.sessionId),
2331
2902
  JSON.stringify(full, null, 2) + "\n",
2332
2903
  { encoding: "utf8", mode: 384 }
@@ -2338,7 +2909,7 @@ var init_session_store = __esm({
2338
2909
  }
2339
2910
  let raw;
2340
2911
  try {
2341
- raw = await fs5.readFile(paths.sessionFile(sessionId), "utf8");
2912
+ raw = await fs7.readFile(paths.sessionFile(sessionId), "utf8");
2342
2913
  } catch (err) {
2343
2914
  const e = err;
2344
2915
  if (e.code === "ENOENT") {
@@ -2357,7 +2928,7 @@ var init_session_store = __esm({
2357
2928
  return;
2358
2929
  }
2359
2930
  try {
2360
- await fs5.unlink(paths.sessionFile(sessionId));
2931
+ await fs7.unlink(paths.sessionFile(sessionId));
2361
2932
  } catch (err) {
2362
2933
  const e = err;
2363
2934
  if (e.code !== "ENOENT") {
@@ -2365,7 +2936,7 @@ var init_session_store = __esm({
2365
2936
  }
2366
2937
  }
2367
2938
  try {
2368
- await fs5.rmdir(paths.sessionDir(sessionId));
2939
+ await fs7.rmdir(paths.sessionDir(sessionId));
2369
2940
  } catch (err) {
2370
2941
  const e = err;
2371
2942
  if (e.code !== "ENOENT" && e.code !== "ENOTEMPTY") {
@@ -2395,7 +2966,7 @@ var init_session_store = __esm({
2395
2966
  async list() {
2396
2967
  let entries;
2397
2968
  try {
2398
- entries = await fs5.readdir(paths.sessionsDir());
2969
+ entries = await fs7.readdir(paths.sessionsDir());
2399
2970
  } catch (err) {
2400
2971
  const e = err;
2401
2972
  if (e.code === "ENOENT") {
@@ -2417,12 +2988,12 @@ var init_session_store = __esm({
2417
2988
  });
2418
2989
 
2419
2990
  // src/tui/history.ts
2420
- import { promises as fs7 } from "fs";
2991
+ import { promises as fs9 } from "fs";
2421
2992
  import * as path5 from "path";
2422
2993
  async function loadHistory(file) {
2423
2994
  let text;
2424
2995
  try {
2425
- text = await fs7.readFile(file, "utf8");
2996
+ text = await fs9.readFile(file, "utf8");
2426
2997
  } catch (err) {
2427
2998
  if (err.code === "ENOENT") {
2428
2999
  return [];
@@ -2462,9 +3033,9 @@ function appendEntry(history, entry) {
2462
3033
  return out;
2463
3034
  }
2464
3035
  async function saveHistory(file, history) {
2465
- await fs7.mkdir(path5.dirname(file), { recursive: true });
3036
+ await fs9.mkdir(path5.dirname(file), { recursive: true });
2466
3037
  const lines = history.map((entry) => JSON.stringify(entry));
2467
- await fs7.writeFile(file, lines.length > 0 ? lines.join("\n") + "\n" : "");
3038
+ await fs9.writeFile(file, lines.length > 0 ? lines.join("\n") + "\n" : "");
2468
3039
  }
2469
3040
  var HISTORY_CAP;
2470
3041
  var init_history = __esm({
@@ -2477,14 +3048,14 @@ var init_history = __esm({
2477
3048
  // src/core/hydra-version.ts
2478
3049
  import { fileURLToPath } from "url";
2479
3050
  import * as path6 from "path";
2480
- import * as fs8 from "fs";
3051
+ import * as fs10 from "fs";
2481
3052
  function resolveVersion() {
2482
3053
  try {
2483
3054
  let dir = path6.dirname(fileURLToPath(import.meta.url));
2484
3055
  for (let i = 0; i < 8; i += 1) {
2485
3056
  const candidate = path6.join(dir, "package.json");
2486
- if (fs8.existsSync(candidate)) {
2487
- const pkg = JSON.parse(fs8.readFileSync(candidate, "utf8"));
3057
+ if (fs10.existsSync(candidate)) {
3058
+ const pkg = JSON.parse(fs10.readFileSync(candidate, "utf8"));
2488
3059
  if (typeof pkg.version === "string" && pkg.version.length > 0 && (typeof pkg.name !== "string" || pkg.name.includes("hydra-acp"))) {
2489
3060
  return pkg.version;
2490
3061
  }
@@ -2528,6 +3099,7 @@ function encodeBundle(params) {
2528
3099
  ...params.record.currentMode !== void 0 ? { currentMode: params.record.currentMode } : {},
2529
3100
  ...params.record.currentUsage !== void 0 ? { currentUsage: params.record.currentUsage } : {},
2530
3101
  ...params.record.agentCommands !== void 0 ? { agentCommands: params.record.agentCommands } : {},
3102
+ ...params.record.agentModes !== void 0 ? { agentModes: params.record.agentModes } : {},
2531
3103
  createdAt: params.record.createdAt,
2532
3104
  updatedAt: params.record.updatedAt
2533
3105
  },
@@ -2571,6 +3143,7 @@ var init_bundle = __esm({
2571
3143
  currentMode: z5.string().optional(),
2572
3144
  currentUsage: PersistedUsage.optional(),
2573
3145
  agentCommands: z5.array(PersistedAgentCommand).optional(),
3146
+ agentModes: z5.array(PersistedAgentMode).optional(),
2574
3147
  createdAt: z5.string(),
2575
3148
  updatedAt: z5.string()
2576
3149
  });
@@ -2631,6 +3204,8 @@ function mapUpdate(update) {
2631
3204
  return mapUsage(u);
2632
3205
  case "available_commands_update":
2633
3206
  return mapAvailableCommands(u);
3207
+ case "available_modes_update":
3208
+ return mapAvailableModes(u);
2634
3209
  case "session_info_update":
2635
3210
  return mapSessionInfo(u);
2636
3211
  default:
@@ -2692,6 +3267,31 @@ function mapAvailableCommands(u) {
2692
3267
  }
2693
3268
  return { kind: "available-commands", commands: normalizeAdvertisedCommands(list) };
2694
3269
  }
3270
+ function mapAvailableModes(u) {
3271
+ const list = u.availableModes;
3272
+ if (!Array.isArray(list)) {
3273
+ return null;
3274
+ }
3275
+ const modes = [];
3276
+ for (const raw of list) {
3277
+ if (!raw || typeof raw !== "object") {
3278
+ continue;
3279
+ }
3280
+ const m = raw;
3281
+ if (typeof m.id !== "string" || m.id.length === 0) {
3282
+ continue;
3283
+ }
3284
+ const mode = { id: sanitizeSingleLine(m.id) };
3285
+ if (typeof m.name === "string") {
3286
+ mode.name = sanitizeSingleLine(m.name);
3287
+ }
3288
+ if (typeof m.description === "string") {
3289
+ mode.description = sanitizeSingleLine(m.description);
3290
+ }
3291
+ modes.push(mode);
3292
+ }
3293
+ return { kind: "available-modes", modes };
3294
+ }
2695
3295
  function mapUsage(u) {
2696
3296
  const event = { kind: "usage-update" };
2697
3297
  if (typeof u.used === "number") {
@@ -2812,7 +3412,7 @@ function mapPlan(u) {
2812
3412
  return { kind: "plan", entries: normalized };
2813
3413
  }
2814
3414
  function mapMode(u) {
2815
- const mode = readString(u, "currentMode") ?? readString(u, "mode");
3415
+ const mode = readString(u, "currentModeId") ?? readString(u, "currentMode") ?? readString(u, "mode");
2816
3416
  if (!mode) {
2817
3417
  return null;
2818
3418
  }
@@ -3466,14 +4066,15 @@ var init_session_row = __esm({
3466
4066
  });
3467
4067
 
3468
4068
  // src/cli/commands/sessions.ts
3469
- import * as fs13 from "fs/promises";
3470
- import * as path8 from "path";
4069
+ import * as fs17 from "fs/promises";
4070
+ import * as path10 from "path";
3471
4071
  async function runSessionsList(opts = {}) {
3472
4072
  const config = await loadConfig();
4073
+ const serviceToken = await loadServiceToken();
3473
4074
  const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
3474
4075
  const url = new URL(`${baseUrl}/v1/sessions`);
3475
4076
  const response = await fetch(url.toString(), {
3476
- headers: { Authorization: `Bearer ${config.daemon.authToken}` }
4077
+ headers: { Authorization: `Bearer ${serviceToken}` }
3477
4078
  });
3478
4079
  if (!response.ok) {
3479
4080
  process.stderr.write(`Daemon returned HTTP ${response.status}
@@ -3529,10 +4130,11 @@ async function runSessionsKill(id) {
3529
4130
  process.exit(2);
3530
4131
  }
3531
4132
  const config = await loadConfig();
4133
+ const serviceToken = await loadServiceToken();
3532
4134
  const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
3533
4135
  const response = await fetch(`${baseUrl}/v1/sessions/${id}/kill`, {
3534
4136
  method: "POST",
3535
- headers: { Authorization: `Bearer ${config.daemon.authToken}` }
4137
+ headers: { Authorization: `Bearer ${serviceToken}` }
3536
4138
  });
3537
4139
  if (!response.ok && response.status !== 204) {
3538
4140
  process.stderr.write(`Daemon returned HTTP ${response.status}
@@ -3548,10 +4150,11 @@ async function runSessionsRemove(id) {
3548
4150
  process.exit(2);
3549
4151
  }
3550
4152
  const config = await loadConfig();
4153
+ const serviceToken = await loadServiceToken();
3551
4154
  const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
3552
4155
  const response = await fetch(`${baseUrl}/v1/sessions/${id}`, {
3553
4156
  method: "DELETE",
3554
- headers: { Authorization: `Bearer ${config.daemon.authToken}` }
4157
+ headers: { Authorization: `Bearer ${serviceToken}` }
3555
4158
  });
3556
4159
  if (!response.ok && response.status !== 204) {
3557
4160
  process.stderr.write(`Daemon returned HTTP ${response.status}
@@ -3569,11 +4172,12 @@ async function runSessionsExport(id, outPath) {
3569
4172
  process.exit(2);
3570
4173
  }
3571
4174
  const config = await loadConfig();
4175
+ const serviceToken = await loadServiceToken();
3572
4176
  const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
3573
4177
  const response = await fetch(
3574
4178
  `${baseUrl}/v1/sessions/${encodeURIComponent(id)}/export`,
3575
4179
  {
3576
- headers: { Authorization: `Bearer ${config.daemon.authToken}` }
4180
+ headers: { Authorization: `Bearer ${serviceToken}` }
3577
4181
  }
3578
4182
  );
3579
4183
  if (!response.ok) {
@@ -3591,8 +4195,8 @@ async function runSessionsExport(id, outPath) {
3591
4195
  return;
3592
4196
  }
3593
4197
  const resolved = outPath === "." ? deriveFilenameFrom(response, id) : outPath;
3594
- await fs13.mkdir(path8.dirname(path8.resolve(resolved)), { recursive: true });
3595
- await fs13.writeFile(resolved, body, { encoding: "utf8", mode: 384 });
4198
+ await fs17.mkdir(path10.dirname(path10.resolve(resolved)), { recursive: true });
4199
+ await fs17.writeFile(resolved, body, { encoding: "utf8", mode: 384 });
3596
4200
  process.stdout.write(`Wrote ${resolved}
3597
4201
  `);
3598
4202
  }
@@ -3610,14 +4214,15 @@ async function runSessionsTranscript(idOrFile, outPath) {
3610
4214
  const bundle = decodeBundleOrExit(localFile.raw);
3611
4215
  body = bundleToMarkdown(bundle);
3612
4216
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3613
- defaultName = `${path8.basename(idOrFile, path8.extname(idOrFile))}-${stamp}.md`;
4217
+ defaultName = `${path10.basename(idOrFile, path10.extname(idOrFile))}-${stamp}.md`;
3614
4218
  } else {
3615
4219
  const config = await loadConfig();
4220
+ const serviceToken = await loadServiceToken();
3616
4221
  const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
3617
4222
  const response = await fetch(
3618
4223
  `${baseUrl}/v1/sessions/${encodeURIComponent(idOrFile)}/transcript`,
3619
4224
  {
3620
- headers: { Authorization: `Bearer ${config.daemon.authToken}` }
4225
+ headers: { Authorization: `Bearer ${serviceToken}` }
3621
4226
  }
3622
4227
  );
3623
4228
  if (!response.ok) {
@@ -3638,21 +4243,21 @@ async function runSessionsTranscript(idOrFile, outPath) {
3638
4243
  return;
3639
4244
  }
3640
4245
  const resolved = outPath === "." ? defaultName : outPath;
3641
- await fs13.mkdir(path8.dirname(path8.resolve(resolved)), { recursive: true });
3642
- await fs13.writeFile(resolved, body, { encoding: "utf8", mode: 384 });
4246
+ await fs17.mkdir(path10.dirname(path10.resolve(resolved)), { recursive: true });
4247
+ await fs17.writeFile(resolved, body, { encoding: "utf8", mode: 384 });
3643
4248
  process.stdout.write(`Wrote ${resolved}
3644
4249
  `);
3645
4250
  }
3646
4251
  async function readBundleFileIfExists(arg) {
3647
4252
  try {
3648
- const stat4 = await fs13.stat(arg);
4253
+ const stat4 = await fs17.stat(arg);
3649
4254
  if (!stat4.isFile()) {
3650
4255
  return null;
3651
4256
  }
3652
4257
  } catch {
3653
4258
  return null;
3654
4259
  }
3655
- const text = await fs13.readFile(arg, "utf8");
4260
+ const text = await fs17.readFile(arg, "utf8");
3656
4261
  try {
3657
4262
  return { raw: JSON.parse(text) };
3658
4263
  } catch (err) {
@@ -3679,9 +4284,9 @@ async function runSessionsImport(file, opts = {}) {
3679
4284
  }
3680
4285
  let cwdOverride;
3681
4286
  if (opts.cwd !== void 0) {
3682
- const resolved = path8.resolve(opts.cwd);
4287
+ const resolved = path10.resolve(opts.cwd);
3683
4288
  try {
3684
- const stat4 = await fs13.stat(resolved);
4289
+ const stat4 = await fs17.stat(resolved);
3685
4290
  if (!stat4.isDirectory()) {
3686
4291
  process.stderr.write(`--cwd ${resolved} is not a directory
3687
4292
  `);
@@ -3698,7 +4303,7 @@ async function runSessionsImport(file, opts = {}) {
3698
4303
  if (file === "-") {
3699
4304
  body = await readStdin();
3700
4305
  } else {
3701
- body = await fs13.readFile(file, "utf8");
4306
+ body = await fs17.readFile(file, "utf8");
3702
4307
  }
3703
4308
  let bundle;
3704
4309
  try {
@@ -3709,17 +4314,18 @@ async function runSessionsImport(file, opts = {}) {
3709
4314
  process.exit(1);
3710
4315
  }
3711
4316
  if (opts.info === true) {
3712
- const inspectConfig = await loadConfigReadOnly();
4317
+ const inspectConfig = await loadConfig();
3713
4318
  printBundleInfo(bundle, inspectConfig.tui.cwdColumnMaxWidth);
3714
4319
  return;
3715
4320
  }
3716
4321
  const config = await loadConfig();
4322
+ const serviceToken = await loadServiceToken();
3717
4323
  const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
3718
4324
  const response = await fetch(`${baseUrl}/v1/sessions/import`, {
3719
4325
  method: "POST",
3720
4326
  headers: {
3721
4327
  "Content-Type": "application/json",
3722
- Authorization: `Bearer ${config.daemon.authToken}`
4328
+ Authorization: `Bearer ${serviceToken}`
3723
4329
  },
3724
4330
  body: JSON.stringify({
3725
4331
  bundle,
@@ -3813,6 +4419,7 @@ var init_sessions = __esm({
3813
4419
  "src/cli/commands/sessions.ts"() {
3814
4420
  "use strict";
3815
4421
  init_config();
4422
+ init_service_token();
3816
4423
  init_bundle();
3817
4424
  init_transcript();
3818
4425
  init_session_row();
@@ -4110,7 +4717,7 @@ var init_update_check = __esm({
4110
4717
  });
4111
4718
 
4112
4719
  // src/tui/discovery.ts
4113
- async function listSessions(config, opts = {}, fetchImpl = fetch) {
4720
+ async function listSessions(config, serviceToken, opts = {}, fetchImpl = fetch) {
4114
4721
  const base = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
4115
4722
  const url = new URL(`${base}/v1/sessions`);
4116
4723
  if (opts.cwd) {
@@ -4120,7 +4727,7 @@ async function listSessions(config, opts = {}, fetchImpl = fetch) {
4120
4727
  url.searchParams.set("all", "true");
4121
4728
  }
4122
4729
  const response = await fetchImpl(url.toString(), {
4123
- headers: { Authorization: `Bearer ${config.daemon.authToken}` }
4730
+ headers: { Authorization: `Bearer ${serviceToken}` }
4124
4731
  });
4125
4732
  if (!response.ok) {
4126
4733
  throw new Error(`daemon returned HTTP ${response.status}`);
@@ -4144,21 +4751,21 @@ async function listSessions(config, opts = {}, fetchImpl = fetch) {
4144
4751
  importedFromUpstreamSessionId: s.importedFromUpstreamSessionId
4145
4752
  }));
4146
4753
  }
4147
- async function killSession(config, id, fetchImpl = fetch) {
4754
+ async function killSession(config, serviceToken, id, fetchImpl = fetch) {
4148
4755
  const base = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
4149
4756
  const response = await fetchImpl(`${base}/v1/sessions/${id}/kill`, {
4150
4757
  method: "POST",
4151
- headers: { Authorization: `Bearer ${config.daemon.authToken}` }
4758
+ headers: { Authorization: `Bearer ${serviceToken}` }
4152
4759
  });
4153
4760
  if (!response.ok && response.status !== 204 && response.status !== 404) {
4154
4761
  throw new Error(`daemon returned HTTP ${response.status}`);
4155
4762
  }
4156
4763
  }
4157
- async function deleteSession(config, id, fetchImpl = fetch) {
4764
+ async function deleteSession(config, serviceToken, id, fetchImpl = fetch) {
4158
4765
  const base = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
4159
4766
  const response = await fetchImpl(`${base}/v1/sessions/${id}`, {
4160
4767
  method: "DELETE",
4161
- headers: { Authorization: `Bearer ${config.daemon.authToken}` }
4768
+ headers: { Authorization: `Bearer ${serviceToken}` }
4162
4769
  });
4163
4770
  if (!response.ok && response.status !== 204 && response.status !== 404) {
4164
4771
  throw new Error(`daemon returned HTTP ${response.status}`);
@@ -4408,7 +5015,7 @@ async function pickSession(term, opts) {
4408
5015
  };
4409
5016
  const refresh = async (preferredId) => {
4410
5017
  try {
4411
- const next = await listSessions(opts.config);
5018
+ const next = await listSessions(opts.config, opts.serviceToken);
4412
5019
  allSessions = sortSessions(next);
4413
5020
  applyFilter();
4414
5021
  if (preferredId !== void 0) {
@@ -4439,9 +5046,9 @@ async function pickSession(term, opts) {
4439
5046
  paintIndicator();
4440
5047
  try {
4441
5048
  if (kind === "kill") {
4442
- await killSession(opts.config, target.sessionId);
5049
+ await killSession(opts.config, opts.serviceToken, target.sessionId);
4443
5050
  } else {
4444
- await deleteSession(opts.config, target.sessionId);
5051
+ await deleteSession(opts.config, opts.serviceToken, target.sessionId);
4445
5052
  }
4446
5053
  mode = "normal";
4447
5054
  pendingAction = null;
@@ -4709,9 +5316,9 @@ var init_picker = __esm({
4709
5316
  });
4710
5317
 
4711
5318
  // src/tui/attachments.ts
4712
- import path9 from "path";
5319
+ import path11 from "path";
4713
5320
  function mimeFromExtension(p) {
4714
- return EXTENSION_TO_MIME[path9.extname(p).toLowerCase()] ?? null;
5321
+ return EXTENSION_TO_MIME[path11.extname(p).toLowerCase()] ?? null;
4715
5322
  }
4716
5323
  function isSupportedImagePath(p) {
4717
5324
  return mimeFromExtension(p) !== null;
@@ -4820,6 +5427,10 @@ var init_attachments = __esm({
4820
5427
  // src/tui/screen.ts
4821
5428
  import stringWidth from "string-width";
4822
5429
  import wrapAnsi from "wrap-ansi";
5430
+ function matchBareUrl(text) {
5431
+ const stripped = text.replace(/\r\n?$|\n$/, "");
5432
+ return BARE_URL_RE.test(stripped) ? stripped : null;
5433
+ }
4823
5434
  function formattedLineSig(zone, width, line, highlight2 = null, activeCol = null) {
4824
5435
  const active = activeCol === null ? "" : `a${activeCol}`;
4825
5436
  if (!line) {
@@ -5283,6 +5894,8 @@ function mapKeyName(name) {
5283
5894
  case "ALT_ENTER":
5284
5895
  case "META_ENTER":
5285
5896
  return "alt-enter";
5897
+ case "CTRL_T":
5898
+ return "ctrl-t";
5286
5899
  case "SHIFT_TAB":
5287
5900
  return "shift-tab";
5288
5901
  case "TAB":
@@ -5343,7 +5956,7 @@ function mapKeyName(name) {
5343
5956
  return null;
5344
5957
  }
5345
5958
  }
5346
- var SESSIONBAR_ROWS, BANNER_ROWS, SEPARATOR_ROWS, MAX_PROMPT_ROWS, MAX_QUEUED_ROWS, MAX_PERMISSION_ROWS, MAX_COMPLETION_ROWS, MAX_CHIP_ROWS, CONFIRM_PROMPT_ROWS, DEFAULT_CONTENT_REPAINT_THROTTLE_MS, DEFAULT_MAX_SCROLLBACK_LINES, Screen, NON_ASCII, SEGMENTER, TK_MARKUP_STYLE_CHAR, shortId;
5959
+ var SESSIONBAR_ROWS, BANNER_ROWS, SEPARATOR_ROWS, MAX_PROMPT_ROWS, MAX_QUEUED_ROWS, MAX_PERMISSION_ROWS, MAX_COMPLETION_ROWS, MAX_CHIP_ROWS, CONFIRM_PROMPT_ROWS, DEFAULT_CONTENT_REPAINT_THROTTLE_MS, DEFAULT_MAX_SCROLLBACK_LINES, BARE_URL_RE, Screen, NON_ASCII, SEGMENTER, TK_MARKUP_STYLE_CHAR, shortId;
5347
5960
  var init_screen = __esm({
5348
5961
  "src/tui/screen.ts"() {
5349
5962
  "use strict";
@@ -5362,6 +5975,7 @@ var init_screen = __esm({
5362
5975
  CONFIRM_PROMPT_ROWS = 2;
5363
5976
  DEFAULT_CONTENT_REPAINT_THROTTLE_MS = 1e3;
5364
5977
  DEFAULT_MAX_SCROLLBACK_LINES = 1e4;
5978
+ BARE_URL_RE = /^(https?|ftp):\/\/\S+$/;
5365
5979
  Screen = class {
5366
5980
  term;
5367
5981
  dispatcher;
@@ -5445,8 +6059,8 @@ var init_screen = __esm({
5445
6059
  bannerSearchIndicator = null;
5446
6060
  banner = {
5447
6061
  status: "ready",
5448
- planMode: false,
5449
- hint: "\u21E7\u21E5 plan \xB7 \u2325\u23CE newline \xB7 \u2303V paste \xB7 \u2303P pick \xB7 \u2303C cancel \xB7 \u2303D detach",
6062
+ currentMode: void 0,
6063
+ hint: "\u21E7\u21E5 mode \xB7 \u2303V paste \xB7 \u2303P pick \xB7 \u2303C cancel \xB7 \u2303D detach",
5450
6064
  queued: 0
5451
6065
  };
5452
6066
  sessionbar = { agent: "?", cwd: "?", sessionId: "?" };
@@ -5591,7 +6205,10 @@ var init_screen = __esm({
5591
6205
  }
5592
6206
  const startIdx = text.indexOf(startMarker);
5593
6207
  if (startIdx === -1) {
5594
- if (this.terminalKitStdinHandler) {
6208
+ const url = matchBareUrl(text);
6209
+ if (url !== null) {
6210
+ this.onKey([{ type: "paste", text: url }]);
6211
+ } else if (this.terminalKitStdinHandler) {
5595
6212
  this.terminalKitStdinHandler(Buffer.from(text, "binary"));
5596
6213
  }
5597
6214
  return;
@@ -5821,11 +6438,11 @@ var init_screen = __esm({
5821
6438
  return;
5822
6439
  }
5823
6440
  this.lastWindowTitle = clean;
5824
- process.stdout.write(`\x1B]2;${clean}\x1B\\`);
6441
+ process.stdout.write(`\x1B]0;${clean}\x1B\\`);
5825
6442
  }
5826
6443
  clearWindowTitle() {
5827
6444
  this.lastWindowTitle = null;
5828
- process.stdout.write("\x1B]2;\x1B\\");
6445
+ process.stdout.write("\x1B]0;\x1B\\");
5829
6446
  }
5830
6447
  setBanner(banner) {
5831
6448
  this.banner = { ...this.banner, ...banner };
@@ -5833,6 +6450,9 @@ var init_screen = __esm({
5833
6450
  this.drawBanner();
5834
6451
  this.placeCursor();
5835
6452
  }
6453
+ currentModeId() {
6454
+ return this.banner.currentMode;
6455
+ }
5836
6456
  // OSC 9;4 progress-bar control. State 3 = indeterminate (pulsing
5837
6457
  // taskbar / dock badge while a turn is running); state 0 = remove.
5838
6458
  // ConEmu-flavor sequence — supported by Windows Terminal, WezTerm,
@@ -6785,10 +7405,9 @@ var init_screen = __esm({
6785
7405
  const elapsedStr = this.banner.status === "busy" && this.banner.elapsedMs !== void 0 && this.banner.elapsedMs >= 1e3 ? formatElapsed(this.banner.elapsedMs) : "";
6786
7406
  const right = this.bannerRightContent();
6787
7407
  const rightSig = right ? `${right.kind}|${right.text}` : "";
6788
- const sig = `bnr|${w}|${this.banner.status}|${elapsedStr}|${this.banner.queued}|${this.scrollOffset}|${this.banner.planMode ? "1" : "0"}|${this.banner.hint}|` + rightSig;
7408
+ const sig = `bnr|${w}|${this.banner.status}|${elapsedStr}|${this.banner.queued}|${this.scrollOffset}|${this.banner.currentMode ?? ""}|${this.banner.hint}|` + rightSig;
6789
7409
  this.paintRow(row, sig, () => {
6790
7410
  const dot = this.banner.status === "busy" ? "\u25CF" : "\u25CB";
6791
- const planLabel = this.banner.planMode ? "plan: ON " : "plan: off";
6792
7411
  if (this.banner.status === "busy") {
6793
7412
  this.term.brightYellow(`${dot} ${this.banner.status}`);
6794
7413
  if (elapsedStr) {
@@ -6807,13 +7426,11 @@ var init_screen = __esm({
6807
7426
  if (this.scrollOffset > 0) {
6808
7427
  this.term(" \xB7 ").brightCyan(`\u2191 ${this.scrollOffset}`);
6809
7428
  }
6810
- this.term(" \xB7 ");
6811
- if (this.banner.planMode) {
6812
- this.term.brightYellow(planLabel);
6813
- } else {
6814
- this.term.dim(planLabel);
6815
- }
6816
- this.term(" \xB7 ").dim(this.banner.hint);
7429
+ const hint = this.banner.currentMode ? this.banner.hint.replace(
7430
+ "\u21E7\u21E5 mode",
7431
+ `\u21E7\u21E5 mode(${this.banner.currentMode})`
7432
+ ) : this.banner.hint;
7433
+ this.term(" \xB7 ").dim(hint);
6817
7434
  if (right) {
6818
7435
  const visibleWidth = stringWidth(right.text);
6819
7436
  const col = Math.max(1, w - visibleWidth + 1);
@@ -7156,7 +7773,7 @@ var init_input = __esm({
7156
7773
  this.retreatHistorySearch();
7157
7774
  return [];
7158
7775
  }
7159
- if (event.name === "escape") {
7776
+ if (event.name === "escape" || event.name === "ctrl-c") {
7160
7777
  this.cancelHistorySearch();
7161
7778
  return [];
7162
7779
  }
@@ -7252,6 +7869,8 @@ var init_input = __esm({
7252
7869
  return [{ type: "redraw" }];
7253
7870
  case "ctrl-p":
7254
7871
  return [{ type: "switch-session" }];
7872
+ case "ctrl-t":
7873
+ return [{ type: "next-live-session" }];
7255
7874
  case "ctrl-r":
7256
7875
  return this.startHistorySearch();
7257
7876
  case "ctrl-s":
@@ -7741,9 +8360,9 @@ var init_input = __esm({
7741
8360
 
7742
8361
  // src/tui/clipboard.ts
7743
8362
  import { spawn as nodeSpawn } from "child_process";
7744
- import fs14 from "fs/promises";
8363
+ import fs18 from "fs/promises";
7745
8364
  import os4 from "os";
7746
- import path10 from "path";
8365
+ import path12 from "path";
7747
8366
  async function readClipboard(envIn = {}) {
7748
8367
  const env = { ...defaultEnv, ...envIn };
7749
8368
  if (env.platform === "darwin") {
@@ -7758,7 +8377,7 @@ async function readClipboard(envIn = {}) {
7758
8377
  };
7759
8378
  }
7760
8379
  async function readMacOS(env) {
7761
- const tmpPath = path10.join(
8380
+ const tmpPath = path12.join(
7762
8381
  env.tmpdir(),
7763
8382
  `hydra-clipboard-${Date.now()}-${process.pid}.png`
7764
8383
  );
@@ -7782,7 +8401,7 @@ async function readMacOS(env) {
7782
8401
  return img;
7783
8402
  }
7784
8403
  } catch {
7785
- await fs14.unlink(tmpPath).catch(() => void 0);
8404
+ await fs18.unlink(tmpPath).catch(() => void 0);
7786
8405
  }
7787
8406
  try {
7788
8407
  const buf = await runCapture(env.spawn, "pbpaste", []);
@@ -7870,9 +8489,9 @@ async function which(env, cmd) {
7870
8489
  }
7871
8490
  async function readFileAsAttachment(p, unlinkAfter) {
7872
8491
  try {
7873
- const buf = await fs14.readFile(p);
8492
+ const buf = await fs18.readFile(p);
7874
8493
  if (unlinkAfter) {
7875
- await fs14.unlink(p).catch(() => void 0);
8494
+ await fs18.unlink(p).catch(() => void 0);
7876
8495
  }
7877
8496
  if (buf.length === 0) {
7878
8497
  return { ok: false, reason: "no image on clipboard" };
@@ -8062,6 +8681,7 @@ function formatEvent(event) {
8062
8681
  case "usage-update":
8063
8682
  return [];
8064
8683
  case "available-commands":
8684
+ case "available-modes":
8065
8685
  return [];
8066
8686
  case "session-info":
8067
8687
  return [];
@@ -8360,17 +8980,18 @@ var init_format = __esm({
8360
8980
  import { appendFileSync, statSync, renameSync } from "fs";
8361
8981
  import { nanoid as nanoid3 } from "nanoid";
8362
8982
  import termkit from "terminal-kit";
8363
- import fs15 from "fs/promises";
8364
- import path11 from "path";
8983
+ import fs19 from "fs/promises";
8984
+ import path13 from "path";
8365
8985
  async function runTuiApp(opts) {
8366
- const config = await ensureConfig();
8986
+ const config = await loadConfig();
8987
+ const serviceToken = await ensureServiceToken();
8367
8988
  logMaxBytes = config.tui.logMaxBytes;
8368
8989
  await ensureDaemonReachable(config);
8369
8990
  const term = termkit.terminal;
8370
8991
  const exitHint = {};
8371
8992
  let nextOpts = opts;
8372
8993
  while (nextOpts !== null) {
8373
- nextOpts = await runSession(term, config, nextOpts, exitHint);
8994
+ nextOpts = await runSession(term, config, serviceToken, nextOpts, exitHint);
8374
8995
  }
8375
8996
  const pendingUpdate = await getPendingUpdate();
8376
8997
  if (pendingUpdate) {
@@ -8383,8 +9004,8 @@ async function runTuiApp(opts) {
8383
9004
  `);
8384
9005
  }
8385
9006
  }
8386
- async function runSession(term, config, opts, exitHint) {
8387
- const ctx = await resolveSession(term, config, opts);
9007
+ async function runSession(term, config, serviceToken, opts, exitHint) {
9008
+ const ctx = await resolveSession(term, config, serviceToken, opts);
8388
9009
  if (!ctx) {
8389
9010
  term.grabInput(false);
8390
9011
  process.exit(0);
@@ -8393,7 +9014,7 @@ async function runSession(term, config, opts, exitHint) {
8393
9014
  term.brightYellow(launchLabel)("\n");
8394
9015
  const protocol = config.daemon.tls ? "wss" : "ws";
8395
9016
  const wsUrl = `${protocol}://${config.daemon.host}:${config.daemon.port}/acp`;
8396
- const subprotocols = ["acp.v1", `hydra-acp-token.${config.daemon.authToken}`];
9017
+ const subprotocols = ["acp.v1", `hydra-acp-token.${serviceToken}`];
8397
9018
  let onReconnect = null;
8398
9019
  let onDisconnectHook = null;
8399
9020
  const stream = new ResilientWsStream({
@@ -8462,9 +9083,7 @@ async function runSession(term, config, opts, exitHint) {
8462
9083
  screenRef.setBanner({ status: "ready", elapsedMs: void 0 });
8463
9084
  }
8464
9085
  }
8465
- if (delta < 0) {
8466
- tickWorker();
8467
- }
9086
+ void delta;
8468
9087
  };
8469
9088
  let screenRef = null;
8470
9089
  let dispatcherRef = null;
@@ -8481,23 +9100,91 @@ async function runSession(term, config, opts, exitHint) {
8481
9100
  } else if (event?.kind === "turn-complete") {
8482
9101
  adjustPendingTurns(-1);
8483
9102
  }
8484
- if (rawTag === "permission_resolved") {
8485
- handlePermissionResolved(update);
8486
- return;
9103
+ if (rawTag === "permission_resolved") {
9104
+ handlePermissionResolved(update);
9105
+ return;
9106
+ }
9107
+ appendRender(event);
9108
+ maybeDismissPermissionByToolUpdate(update);
9109
+ });
9110
+ conn.onNotification("hydra-acp/session_closed", () => {
9111
+ if (teardownStarted) {
9112
+ return;
9113
+ }
9114
+ if (pendingTurns > 0) {
9115
+ adjustPendingTurns(-pendingTurns);
9116
+ }
9117
+ const screenReady = typeof screenRef !== "undefined" && screenRef !== null;
9118
+ if (screenReady) {
9119
+ screenRef.setBanner({ status: "cold", elapsedMs: void 0 });
9120
+ }
9121
+ });
9122
+ conn.onNotification("hydra-acp/prompt_queue_added", (params) => {
9123
+ if (teardownStarted) return;
9124
+ const p = params ?? {};
9125
+ if (typeof p.messageId !== "string") return;
9126
+ queueCache.set(p.messageId, chipFromPrompt(p.messageId, p.prompt));
9127
+ if (screenRef && dispatcherRef) {
9128
+ refreshQueueDisplay();
9129
+ }
9130
+ if (ownClientId !== void 0 && p.originator?.clientId === ownClientId) {
9131
+ const echo = pendingEchoes.shift();
9132
+ if (echo) {
9133
+ echo.messageId = p.messageId;
9134
+ ownPendingByMid.set(p.messageId, echo);
9135
+ }
9136
+ }
9137
+ });
9138
+ conn.onNotification("hydra-acp/prompt_queue_updated", (params) => {
9139
+ if (teardownStarted) return;
9140
+ const p = params ?? {};
9141
+ if (typeof p.messageId !== "string") return;
9142
+ if (!queueCache.has(p.messageId)) return;
9143
+ queueCache.set(p.messageId, chipFromPrompt(p.messageId, p.prompt));
9144
+ const pending = ownPendingByMid.get(p.messageId);
9145
+ if (pending) {
9146
+ const blocks = Array.isArray(p.prompt) ? p.prompt : [];
9147
+ let text = "";
9148
+ const attachments = [];
9149
+ for (const raw of blocks) {
9150
+ if (!raw || typeof raw !== "object") continue;
9151
+ const b = raw;
9152
+ if (b.type === "text" && typeof b.text === "string") {
9153
+ text += b.text;
9154
+ } else if (b.type === "image" && typeof b.data === "string" && typeof b.mimeType === "string") {
9155
+ attachments.push({
9156
+ data: b.data,
9157
+ mimeType: b.mimeType,
9158
+ sizeBytes: Math.floor(b.data.length * 3 / 4)
9159
+ });
9160
+ }
9161
+ }
9162
+ pending.text = text;
9163
+ pending.attachments = attachments;
9164
+ }
9165
+ if (screenRef && dispatcherRef) {
9166
+ refreshQueueDisplay();
8487
9167
  }
8488
- appendRender(event);
8489
- maybeDismissPermissionByToolUpdate(update);
8490
9168
  });
8491
- conn.onNotification("hydra-acp/session_closed", () => {
8492
- if (teardownStarted) {
8493
- return;
8494
- }
8495
- if (pendingTurns > 0) {
8496
- adjustPendingTurns(-pendingTurns);
9169
+ conn.onNotification("hydra-acp/prompt_queue_removed", (params) => {
9170
+ if (teardownStarted) return;
9171
+ const p = params ?? {};
9172
+ if (typeof p.messageId !== "string") return;
9173
+ const hadChip = queueCache.delete(p.messageId);
9174
+ if (hadChip && screenRef && dispatcherRef) {
9175
+ refreshQueueDisplay();
8497
9176
  }
8498
- const screenReady = typeof screenRef !== "undefined" && screenRef !== null;
8499
- if (screenReady) {
8500
- screenRef.setBanner({ status: "cold", elapsedMs: void 0 });
9177
+ const echo = ownPendingByMid.get(p.messageId);
9178
+ if (echo) {
9179
+ ownPendingByMid.delete(p.messageId);
9180
+ if (p.reason === "started") {
9181
+ echo.flushed = true;
9182
+ appendRender({
9183
+ kind: "user-text",
9184
+ text: echo.text,
9185
+ attachments: echo.attachments
9186
+ });
9187
+ }
8501
9188
  }
8502
9189
  });
8503
9190
  const handlePermissionResolved = (update) => {
@@ -8627,9 +9314,12 @@ async function runSession(term, config, opts, exitHint) {
8627
9314
  let resolvedAgentId = ctx.agentId;
8628
9315
  let resolvedCwd = ctx.cwd;
8629
9316
  let resolvedTitle;
9317
+ let ownClientId;
8630
9318
  let initialModel;
8631
9319
  let initialMode;
8632
9320
  let initialCommands;
9321
+ let initialModes;
9322
+ let initialQueue;
8633
9323
  let initialUsage;
8634
9324
  let initialTurnStartedAt;
8635
9325
  if (ctx.sessionId === "__new__") {
@@ -8646,6 +9336,9 @@ async function runSession(term, config, opts, exitHint) {
8646
9336
  ...Object.keys(hydraNewMeta).length > 0 ? { _meta: { [HYDRA_META_KEY]: hydraNewMeta } } : {}
8647
9337
  });
8648
9338
  resolvedSessionId = created.sessionId;
9339
+ if (created.clientId) {
9340
+ ownClientId = created.clientId;
9341
+ }
8649
9342
  exitHint.sessionId = resolvedSessionId;
8650
9343
  const hydraMeta = extractHydraMeta(created._meta ?? void 0);
8651
9344
  upstreamSessionId = hydraMeta.upstreamSessionId;
@@ -8665,6 +9358,10 @@ async function runSession(term, config, opts, exitHint) {
8665
9358
  if (hydraMeta.availableCommands) {
8666
9359
  initialCommands = normalizeAdvertisedCommands(hydraMeta.availableCommands);
8667
9360
  }
9361
+ if (hydraMeta.availableModes) {
9362
+ initialModes = hydraMeta.availableModes;
9363
+ }
9364
+ initialQueue = hydraMeta.queue;
8668
9365
  } else {
8669
9366
  const attached = await conn.request("session/attach", {
8670
9367
  sessionId: ctx.sessionId,
@@ -8672,6 +9369,9 @@ async function runSession(term, config, opts, exitHint) {
8672
9369
  clientInfo: { name: "hydra-acp-tui", version: HYDRA_VERSION }
8673
9370
  });
8674
9371
  resolvedSessionId = attached.sessionId;
9372
+ if (attached.clientId) {
9373
+ ownClientId = attached.clientId;
9374
+ }
8675
9375
  exitHint.sessionId = resolvedSessionId;
8676
9376
  const hydraMeta = extractHydraMeta(attached._meta ?? void 0);
8677
9377
  upstreamSessionId = hydraMeta.upstreamSessionId;
@@ -8691,6 +9391,10 @@ async function runSession(term, config, opts, exitHint) {
8691
9391
  if (hydraMeta.availableCommands) {
8692
9392
  initialCommands = normalizeAdvertisedCommands(hydraMeta.availableCommands);
8693
9393
  }
9394
+ if (hydraMeta.availableModes) {
9395
+ initialModes = hydraMeta.availableModes;
9396
+ }
9397
+ initialQueue = hydraMeta.queue;
8694
9398
  }
8695
9399
  const historyFile = paths.tuiHistoryFile(resolvedSessionId);
8696
9400
  let history = await loadHistory(historyFile).catch(() => []);
@@ -8750,6 +9454,7 @@ async function runSession(term, config, opts, exitHint) {
8750
9454
  { name: "/demo-tool", description: "Inject a synthetic tool-call sequence (UI test)" }
8751
9455
  ];
8752
9456
  let agentCommands = initialCommands ?? [];
9457
+ let agentModes = initialModes ?? [];
8753
9458
  const allCommands = () => {
8754
9459
  const seen = /* @__PURE__ */ new Set();
8755
9460
  const out = [];
@@ -8905,7 +9610,7 @@ async function runSession(term, config, opts, exitHint) {
8905
9610
  usage: { ...usage }
8906
9611
  });
8907
9612
  if (initialMode) {
8908
- screen.appendLines(formatEvent({ kind: "mode-changed", mode: initialMode }));
9613
+ screen.setBanner({ currentMode: initialMode });
8909
9614
  }
8910
9615
  void getPendingUpdate().then((info) => {
8911
9616
  if (info) {
@@ -8942,7 +9647,7 @@ async function runSession(term, config, opts, exitHint) {
8942
9647
  }
8943
9648
  let onlyClient = false;
8944
9649
  try {
8945
- const sessions = await listSessions(config);
9650
+ const sessions = await listSessions(config, serviceToken);
8946
9651
  const me = sessions.find((s) => s.sessionId === resolvedSessionId);
8947
9652
  onlyClient = !me || me.attachedClients <= 1;
8948
9653
  } catch {
@@ -9034,11 +9739,12 @@ async function runSession(term, config, opts, exitHint) {
9034
9739
  screen.pauseRepaint();
9035
9740
  screen.stop();
9036
9741
  saveHistory(historyFile, history).catch(() => void 0);
9037
- const sessions = await listSessions(config);
9742
+ const sessions = await listSessions(config, serviceToken);
9038
9743
  const choice = await pickSession(term, {
9039
9744
  cwd: resolvedCwd,
9040
9745
  sessions,
9041
9746
  config,
9747
+ serviceToken,
9042
9748
  currentSessionId: resolvedSessionId
9043
9749
  });
9044
9750
  if (choice.kind === "abort") {
@@ -9066,37 +9772,76 @@ async function runSession(term, config, opts, exitHint) {
9066
9772
  }
9067
9773
  resume(nextOpts);
9068
9774
  };
9069
- const queueHeadOffset = () => workerActive ? 1 : 0;
9775
+ const cycleLiveSession = async () => {
9776
+ if (!finishSession)
9777
+ return;
9778
+ const sessions = await listSessions(config, serviceToken);
9779
+ const live = sessions.filter((s) => s.status === "live");
9780
+ if (live.length <= 1)
9781
+ return;
9782
+ const idx = live.findIndex((s) => s.sessionId === resolvedSessionId);
9783
+ const next = live[(idx + 1) % live.length];
9784
+ const resume = finishSession;
9785
+ finishSession = null;
9786
+ process.off("SIGINT", sigintHandler);
9787
+ void stream.close().catch(() => void 0);
9788
+ const nextOpts = { ...opts, sessionId: next.sessionId, cwd: resolvedCwd };
9789
+ if (next.agentId !== void 0)
9790
+ nextOpts.agentId = next.agentId;
9791
+ resume(nextOpts);
9792
+ };
9070
9793
  const handleEffect = (effect) => {
9071
9794
  switch (effect.type) {
9072
9795
  case "send":
9073
- enqueuePrompt(effect.text, effect.planMode, effect.attachments);
9796
+ enqueuePrompt(effect.text, effect.attachments);
9074
9797
  return;
9075
9798
  case "queue-edit": {
9076
- const realIdx = effect.index + queueHeadOffset();
9077
- const existing = promptQueue[realIdx];
9078
- if (existing) {
9079
- promptQueue[realIdx] = {
9080
- text: effect.text,
9081
- planMode: existing.planMode,
9082
- attachments: effect.attachments
9083
- };
9084
- refreshQueueDisplay();
9799
+ const mid = queueMessageIdAt(effect.index);
9800
+ if (!mid) {
9801
+ return;
9802
+ }
9803
+ const blocks = [];
9804
+ if (effect.text.length > 0) {
9805
+ blocks.push({ type: "text", text: effect.text });
9085
9806
  }
9807
+ for (const a of effect.attachments) {
9808
+ blocks.push({ type: "image", data: a.data, mimeType: a.mimeType });
9809
+ }
9810
+ conn.request("hydra-acp/update_prompt", {
9811
+ sessionId: resolvedSessionId,
9812
+ messageId: mid,
9813
+ prompt: blocks
9814
+ }).then((raw) => {
9815
+ const res = raw;
9816
+ if (!res.updated && res.reason !== "ok") {
9817
+ screen.notify(`queue edit skipped (${res.reason})`);
9818
+ }
9819
+ }).catch((err) => {
9820
+ screen.notify(`queue edit failed: ${err.message}`);
9821
+ });
9086
9822
  return;
9087
9823
  }
9088
9824
  case "queue-remove": {
9089
- const realIdx = effect.index + queueHeadOffset();
9090
- if (realIdx >= 0 && realIdx < promptQueue.length) {
9091
- promptQueue.splice(realIdx, 1);
9092
- refreshQueueDisplay();
9825
+ const mid = queueMessageIdAt(effect.index);
9826
+ if (!mid) {
9827
+ return;
9093
9828
  }
9829
+ conn.request("hydra-acp/cancel_prompt", {
9830
+ sessionId: resolvedSessionId,
9831
+ messageId: mid
9832
+ }).then((raw) => {
9833
+ const res = raw;
9834
+ if (!res.cancelled && res.reason !== "ok") {
9835
+ screen.notify(`queue cancel skipped (${res.reason})`);
9836
+ }
9837
+ }).catch((err) => {
9838
+ screen.notify(`queue cancel failed: ${err.message}`);
9839
+ });
9094
9840
  return;
9095
9841
  }
9096
9842
  case "cancel": {
9097
9843
  if (effect.prefill && turnInFlight) {
9098
- const headOffset = workerActive ? 1 : 0;
9099
- const waitingEmpty = promptQueue.length <= headOffset;
9844
+ const waitingEmpty = queueCache.size === 0;
9100
9845
  const bufferEmpty = dispatcher.state().buffer.every((line) => line === "");
9101
9846
  if (waitingEmpty && bufferEmpty) {
9102
9847
  pendingPrefill = {
@@ -9116,7 +9861,7 @@ async function runSession(term, config, opts, exitHint) {
9116
9861
  void requestExit();
9117
9862
  return;
9118
9863
  case "plan-toggle":
9119
- screen.setBanner({ planMode: effect.on });
9864
+ void handleModeToggle(effect.on);
9120
9865
  return;
9121
9866
  case "redraw-banner":
9122
9867
  screen.setBanner({});
@@ -9133,6 +9878,9 @@ async function runSession(term, config, opts, exitHint) {
9133
9878
  case "switch-session":
9134
9879
  void switchSession();
9135
9880
  return;
9881
+ case "next-live-session":
9882
+ void cycleLiveSession();
9883
+ return;
9136
9884
  case "toggle-tools":
9137
9885
  toolsExpanded = !toolsExpanded;
9138
9886
  renderToolsBlock();
@@ -9176,11 +9924,11 @@ async function runSession(term, config, opts, exitHint) {
9176
9924
  }
9177
9925
  const mimeType = mimeFromExtension(token);
9178
9926
  if (!mimeType) {
9179
- screen.notify(`unsupported image type: ${path11.basename(token)}`);
9927
+ screen.notify(`unsupported image type: ${path13.basename(token)}`);
9180
9928
  continue;
9181
9929
  }
9182
9930
  try {
9183
- const buf = await fs15.readFile(token);
9931
+ const buf = await fs19.readFile(token);
9184
9932
  if (buf.length > MAX_ATTACHMENT_BYTES) {
9185
9933
  screen.notify(
9186
9934
  `image too large (${formatSize(buf.length)}, max ${formatSize(MAX_ATTACHMENT_BYTES)})`
@@ -9190,13 +9938,13 @@ async function runSession(term, config, opts, exitHint) {
9190
9938
  dispatcher.addAttachment({
9191
9939
  mimeType,
9192
9940
  data: buf.toString("base64"),
9193
- name: path11.basename(token),
9941
+ name: path13.basename(token),
9194
9942
  sizeBytes: buf.length
9195
9943
  });
9196
9944
  added++;
9197
9945
  } catch (err) {
9198
9946
  screen.notify(
9199
- `cannot read ${path11.basename(token)}: ${err.message}`
9947
+ `cannot read ${path13.basename(token)}: ${err.message}`
9200
9948
  );
9201
9949
  }
9202
9950
  }
@@ -9227,18 +9975,53 @@ async function runSession(term, config, opts, exitHint) {
9227
9975
  }
9228
9976
  screen.refreshPrompt();
9229
9977
  };
9230
- const promptQueue = [];
9231
- let workerActive = false;
9978
+ const formatQueueChipText = (entry) => entry.attachmentCount > 0 ? `${entry.text} \xB7 \u{1F4CE}\xD7${entry.attachmentCount}` : entry.text;
9979
+ const chipFromPrompt = (messageId, prompt) => {
9980
+ const blocks = Array.isArray(prompt) ? prompt : [];
9981
+ let text = "";
9982
+ let attachmentCount = 0;
9983
+ for (const raw of blocks) {
9984
+ if (!raw || typeof raw !== "object") continue;
9985
+ const b = raw;
9986
+ if (b.type === "text" && typeof b.text === "string") {
9987
+ text += b.text;
9988
+ } else if (b.type === "image") {
9989
+ attachmentCount += 1;
9990
+ }
9991
+ }
9992
+ return {
9993
+ messageId,
9994
+ text: sanitizeSingleLine(text),
9995
+ attachmentCount
9996
+ };
9997
+ };
9998
+ const queueCache = /* @__PURE__ */ new Map();
9999
+ const pendingEchoes = [];
10000
+ const ownPendingByMid = /* @__PURE__ */ new Map();
9232
10001
  const refreshQueueDisplay = () => {
9233
- const waiting = promptQueue.slice(workerActive ? 1 : 0);
9234
- const displayTexts = waiting.map(
9235
- (p) => p.attachments.length > 0 ? `${p.text} \xB7 \u{1F4CE}\xD7${p.attachments.length}` : p.text
9236
- );
10002
+ const entries = [...queueCache.values()];
10003
+ const displayTexts = entries.map(formatQueueChipText);
9237
10004
  screen.setQueuedPrompts(displayTexts);
9238
- screen.setBanner({ queued: waiting.length });
9239
- dispatcher.setQueue(waiting.map((p) => p.text));
10005
+ screen.setBanner({ queued: entries.length });
10006
+ dispatcher.setQueue(entries.map((e) => e.text));
10007
+ };
10008
+ const queueMessageIdAt = (index) => {
10009
+ const entries = [...queueCache.values()];
10010
+ return entries[index]?.messageId;
9240
10011
  };
9241
- const enqueuePrompt = (text, planMode, attachments) => {
10012
+ if (initialQueue && initialQueue.length > 0) {
10013
+ for (const entry of initialQueue) {
10014
+ if (entry.position === 0) continue;
10015
+ queueCache.set(
10016
+ entry.messageId,
10017
+ chipFromPrompt(entry.messageId, entry.prompt)
10018
+ );
10019
+ }
10020
+ if (queueCache.size > 0) {
10021
+ refreshQueueDisplay();
10022
+ }
10023
+ }
10024
+ const enqueuePrompt = (text, attachments) => {
9242
10025
  screen.scrollToBottom();
9243
10026
  if (handleBuiltinCommand(text)) {
9244
10027
  return;
@@ -9246,15 +10029,29 @@ async function runSession(term, config, opts, exitHint) {
9246
10029
  history = appendEntry(history, text);
9247
10030
  dispatcher.setHistory(history);
9248
10031
  saveHistory(historyFile, history).catch(() => void 0);
9249
- promptQueue.push({ text, planMode, attachments });
9250
- refreshQueueDisplay();
9251
- tickWorker();
10032
+ void runPrompt(text, attachments);
9252
10033
  };
9253
- const tickWorker = () => {
9254
- if (workerActive || pendingTurns > 0 || promptQueue.length === 0) {
10034
+ const handleModeToggle = async (_on) => {
10035
+ if (agentModes.length === 0) {
10036
+ screen.notify("no modes advertised by agent");
10037
+ return;
10038
+ }
10039
+ const currentMode = screen.currentModeId();
10040
+ const idx = agentModes.findIndex((m) => m.id === currentMode);
10041
+ const nextIdx = idx === -1 ? 0 : (idx + 1) % agentModes.length;
10042
+ const newModeId = agentModes[nextIdx]?.id;
10043
+ if (!newModeId) {
9255
10044
  return;
9256
10045
  }
9257
- void runQueueWorker();
10046
+ screen.setBanner({ currentMode: newModeId });
10047
+ try {
10048
+ await conn.request("session/set_mode", {
10049
+ sessionId: resolvedSessionId,
10050
+ modeId: newModeId
10051
+ });
10052
+ } catch (err) {
10053
+ screen.notify(`set_mode failed: ${err.message}`);
10054
+ }
9258
10055
  };
9259
10056
  const handleBuiltinCommand = (text) => {
9260
10057
  const trimmed = text.trim();
@@ -9404,33 +10201,7 @@ async function runSession(term, config, opts, exitHint) {
9404
10201
  return false;
9405
10202
  }
9406
10203
  };
9407
- const runQueueWorker = async () => {
9408
- workerActive = true;
9409
- try {
9410
- while (promptQueue.length > 0 && pendingTurns === 0) {
9411
- const next = promptQueue[0];
9412
- if (!next) {
9413
- break;
9414
- }
9415
- refreshQueueDisplay();
9416
- await processPrompt(next.text, next.planMode, next.attachments);
9417
- promptQueue.shift();
9418
- }
9419
- } finally {
9420
- workerActive = false;
9421
- refreshQueueDisplay();
9422
- if (pendingPrefill !== null) {
9423
- const { text, attachments } = pendingPrefill;
9424
- pendingPrefill = null;
9425
- const bufferEmpty = dispatcher.state().buffer.every((line) => line === "");
9426
- if (bufferEmpty) {
9427
- dispatcher.setBuffer(text, attachments);
9428
- screen.refreshPrompt();
9429
- }
9430
- }
9431
- }
9432
- };
9433
- const processPrompt = async (text, planMode, attachments) => {
10204
+ const runPrompt = async (text, attachments) => {
9434
10205
  const userBlocks = [];
9435
10206
  if (text.length > 0) {
9436
10207
  userBlocks.push({ type: "text", text });
@@ -9438,9 +10209,9 @@ async function runSession(term, config, opts, exitHint) {
9438
10209
  for (const a of attachments) {
9439
10210
  userBlocks.push({ type: "image", data: a.data, mimeType: a.mimeType });
9440
10211
  }
9441
- const promptArr = planMode ? [{ type: "text", text: PLAN_PREFIX_TEXT }, ...userBlocks] : userBlocks;
9442
10212
  adjustPendingTurns(1);
9443
- appendRender({ kind: "user-text", text, attachments });
10213
+ const echo = { text, attachments, flushed: false };
10214
+ pendingEchoes.push(echo);
9444
10215
  let cancelled = false;
9445
10216
  turnInFlight = {
9446
10217
  text,
@@ -9459,23 +10230,44 @@ async function runSession(term, config, opts, exitHint) {
9459
10230
  try {
9460
10231
  const response = await conn.request("session/prompt", {
9461
10232
  sessionId: resolvedSessionId,
9462
- prompt: promptArr
10233
+ prompt: userBlocks
9463
10234
  });
9464
10235
  if (response && typeof response.stopReason === "string") {
9465
10236
  stopReason = response.stopReason;
9466
10237
  }
9467
10238
  } catch (err) {
9468
- appendRender({
9469
- kind: "unknown",
9470
- sessionUpdate: "error",
9471
- raw: { error: err.message }
9472
- });
10239
+ const idx = pendingEchoes.indexOf(echo);
10240
+ if (idx >= 0) {
10241
+ pendingEchoes.splice(idx, 1);
10242
+ }
10243
+ if (echo.messageId !== void 0) {
10244
+ ownPendingByMid.delete(echo.messageId);
10245
+ }
10246
+ screen.appendLines([
10247
+ {
10248
+ prefix: "\u2717 ",
10249
+ prefixStyle: "tool-status-fail",
10250
+ body: err.message,
10251
+ bodyStyle: "tool-status-fail"
10252
+ }
10253
+ ]);
9473
10254
  } finally {
9474
10255
  turnInFlight = null;
9475
10256
  adjustPendingTurns(-1);
9476
- appendRender(
9477
- stopReason !== void 0 ? { kind: "turn-complete", stopReason } : { kind: "turn-complete" }
9478
- );
10257
+ if (echo.flushed) {
10258
+ appendRender(
10259
+ stopReason !== void 0 ? { kind: "turn-complete", stopReason } : { kind: "turn-complete" }
10260
+ );
10261
+ }
10262
+ if (pendingPrefill !== null) {
10263
+ const { text: pt, attachments: pa } = pendingPrefill;
10264
+ pendingPrefill = null;
10265
+ const bufferEmpty = dispatcher.state().buffer.every((line) => line === "");
10266
+ if (bufferEmpty) {
10267
+ dispatcher.setBuffer(pt, pa);
10268
+ screen.refreshPrompt();
10269
+ }
10270
+ }
9479
10271
  }
9480
10272
  };
9481
10273
  const toolStates = /* @__PURE__ */ new Map();
@@ -9605,6 +10397,14 @@ async function runSession(term, config, opts, exitHint) {
9605
10397
  refreshCompletions();
9606
10398
  return;
9607
10399
  }
10400
+ if (event.kind === "available-modes") {
10401
+ agentModes = event.modes;
10402
+ return;
10403
+ }
10404
+ if (event.kind === "mode-changed") {
10405
+ screen.setBanner({ currentMode: event.mode || void 0 });
10406
+ return;
10407
+ }
9608
10408
  if (event.kind === "session-info") {
9609
10409
  if (event.title !== void 0) {
9610
10410
  screen.setSessionbar({ title: event.title });
@@ -9846,7 +10646,7 @@ connection lost: ${err.message}
9846
10646
  process.on("SIGINT", sigintHandler);
9847
10647
  return await sessionDone;
9848
10648
  }
9849
- async function resolveSession(term, config, opts) {
10649
+ async function resolveSession(term, config, serviceToken, opts) {
9850
10650
  const cwd = opts.cwd ?? process.cwd();
9851
10651
  if (opts.sessionId) {
9852
10652
  return {
@@ -9859,7 +10659,7 @@ async function resolveSession(term, config, opts) {
9859
10659
  return newCtx(opts, cwd, config);
9860
10660
  }
9861
10661
  if (opts.resume) {
9862
- const sessions2 = await listSessions(config, { cwd, all: true });
10662
+ const sessions2 = await listSessions(config, serviceToken, { cwd, all: true });
9863
10663
  const target = pickMostRecent(sessions2, cwd);
9864
10664
  if (!target) {
9865
10665
  term.yellow(`No sessions found for ${cwd}.
@@ -9872,14 +10672,15 @@ async function resolveSession(term, config, opts) {
9872
10672
  cwd
9873
10673
  };
9874
10674
  }
9875
- const sessions = await listSessions(config);
10675
+ const sessions = await listSessions(config, serviceToken);
9876
10676
  if (sessions.length === 0) {
9877
10677
  return newCtx(opts, cwd, config);
9878
10678
  }
9879
10679
  const choice = await pickSession(term, {
9880
10680
  cwd,
9881
10681
  sessions,
9882
- config
10682
+ config,
10683
+ serviceToken
9883
10684
  });
9884
10685
  if (choice.kind === "abort") {
9885
10686
  return null;
@@ -9934,7 +10735,7 @@ function rotateIfBig(target) {
9934
10735
  } catch {
9935
10736
  }
9936
10737
  }
9937
- var PLAN_PREFIX_TEXT, logMaxBytes;
10738
+ var logMaxBytes;
9938
10739
  var init_app = __esm({
9939
10740
  "src/tui/app.ts"() {
9940
10741
  "use strict";
@@ -9942,6 +10743,7 @@ var init_app = __esm({
9942
10743
  init_types();
9943
10744
  init_resilient_ws();
9944
10745
  init_config();
10746
+ init_service_token();
9945
10747
  init_daemon_bootstrap();
9946
10748
  init_session();
9947
10749
  init_paths();
@@ -9957,7 +10759,6 @@ var init_app = __esm({
9957
10759
  init_completion();
9958
10760
  init_render_update();
9959
10761
  init_format();
9960
- PLAN_PREFIX_TEXT = "Plan mode is on. Outline what you would do without making any changes. Do not edit files, run shell commands, or otherwise execute side effects; produce a plan only.";
9961
10762
  logMaxBytes = 5 * 1024 * 1024;
9962
10763
  }
9963
10764
  });
@@ -10055,23 +10856,25 @@ function resolveOption(flags, key) {
10055
10856
  // src/cli/commands/init.ts
10056
10857
  init_paths();
10057
10858
  init_config();
10058
- import * as fs2 from "fs/promises";
10859
+ init_service_token();
10860
+ import * as fs3 from "fs/promises";
10059
10861
  async function runInit(flags) {
10060
- await fs2.mkdir(paths.home(), { recursive: true });
10061
- const existingToken = await loadAuthToken();
10862
+ await fs3.mkdir(paths.home(), { recursive: true });
10863
+ await migrateLegacyAuthToken();
10864
+ const existingToken = await readServiceToken();
10062
10865
  if (!existingToken) {
10063
- const token = generateAuthToken();
10064
- await writeAuthToken(token);
10866
+ const token = generateServiceToken();
10867
+ await writeServiceToken(token);
10065
10868
  process.stdout.write(
10066
10869
  `Initialized ${paths.authToken()}
10067
- Auth token: ${token}
10870
+ Service token: ${token}
10068
10871
  `
10069
10872
  );
10070
10873
  return;
10071
10874
  }
10072
10875
  if (flagBool(flags, "rotate-token")) {
10073
- const newToken = generateAuthToken();
10074
- await writeAuthToken(newToken);
10876
+ const newToken = generateServiceToken();
10877
+ await writeServiceToken(newToken);
10075
10878
  process.stdout.write(
10076
10879
  `Rotated token in ${paths.authToken()}
10077
10880
  New token: ${newToken}
@@ -10079,20 +10882,21 @@ New token: ${newToken}
10079
10882
  );
10080
10883
  return;
10081
10884
  }
10082
- process.stdout.write(`Auth token already exists at ${paths.authToken()}.
10885
+ process.stdout.write(`Service token already exists at ${paths.authToken()}.
10083
10886
  `);
10084
- process.stdout.write("Pass --rotate-token to generate a new auth token.\n");
10887
+ process.stdout.write("Pass --rotate-token to generate a new service token.\n");
10085
10888
  }
10086
10889
 
10087
10890
  // src/cli/commands/daemon.ts
10088
10891
  init_paths();
10089
10892
  init_config();
10893
+ init_service_token();
10090
10894
  import * as fsp6 from "fs/promises";
10091
10895
  import { setTimeout as sleep2 } from "timers/promises";
10092
10896
 
10093
10897
  // src/daemon/server.ts
10094
10898
  init_config();
10095
- import * as fs11 from "fs";
10899
+ import * as fs15 from "fs";
10096
10900
  import * as fsp4 from "fs/promises";
10097
10901
  import Fastify from "fastify";
10098
10902
  import websocketPlugin from "@fastify/websocket";
@@ -10101,12 +10905,12 @@ import createPinoRoll from "pino-roll";
10101
10905
 
10102
10906
  // src/core/registry.ts
10103
10907
  init_paths();
10104
- import * as fs4 from "fs/promises";
10908
+ import * as fs5 from "fs/promises";
10105
10909
  import { z as z2 } from "zod";
10106
10910
 
10107
10911
  // src/core/binary-install.ts
10108
10912
  init_paths();
10109
- import * as fs3 from "fs";
10913
+ import * as fs4 from "fs";
10110
10914
  import * as fsp from "fs/promises";
10111
10915
  import * as path2 from "path";
10112
10916
  import { spawn } from "child_process";
@@ -10211,7 +11015,7 @@ async function downloadTo(args) {
10211
11015
  );
10212
11016
  }
10213
11017
  const total = Number(response.headers.get("content-length") ?? "0");
10214
- const out = fs3.createWriteStream(dest);
11018
+ const out = fs4.createWriteStream(dest);
10215
11019
  const nodeStream = Readable.fromWeb(response.body);
10216
11020
  let received = 0;
10217
11021
  let lastEmit = Date.now();
@@ -10338,7 +11142,8 @@ async function ensureNpmPackage(args) {
10338
11142
  await installInto({
10339
11143
  agentId: args.agentId,
10340
11144
  packageSpec: args.packageSpec,
10341
- installDir
11145
+ installDir,
11146
+ registry: args.registry
10342
11147
  });
10343
11148
  if (!await fileExists2(binPath)) {
10344
11149
  throw new Error(
@@ -10356,7 +11161,8 @@ async function installInto(args) {
10356
11161
  );
10357
11162
  await runNpmInstall({
10358
11163
  packageSpec: args.packageSpec,
10359
- cwd: tempDir
11164
+ cwd: tempDir,
11165
+ registry: args.registry
10360
11166
  });
10361
11167
  try {
10362
11168
  await fsp2.rename(tempDir, args.installDir);
@@ -10380,9 +11186,10 @@ async function installInto(args) {
10380
11186
  }
10381
11187
  function runNpmInstall(args) {
10382
11188
  return new Promise((resolve5, reject) => {
11189
+ const registryArgs = args.registry ? ["--registry", args.registry] : [];
10383
11190
  const child = spawn2(
10384
11191
  "npm",
10385
- ["install", "--no-audit", "--no-fund", "--silent", args.packageSpec],
11192
+ ["install", "--no-audit", "--no-fund", "--silent", ...registryArgs, args.packageSpec],
10386
11193
  {
10387
11194
  cwd: args.cwd,
10388
11195
  stdio: ["ignore", "pipe", "pipe"]
@@ -10535,7 +11342,7 @@ var Registry = class {
10535
11342
  async readDiskCache() {
10536
11343
  let text;
10537
11344
  try {
10538
- text = await fs4.readFile(paths.registryCache(), "utf8");
11345
+ text = await fs5.readFile(paths.registryCache(), "utf8");
10539
11346
  } catch (err) {
10540
11347
  const e = err;
10541
11348
  if (e.code === "ENOENT") {
@@ -10561,7 +11368,7 @@ var Registry = class {
10561
11368
  // without a lock file: the loser of the rename race just gets its
10562
11369
  // version replaced by the winner's.
10563
11370
  async writeDiskCache(cache) {
10564
- await fs4.mkdir(paths.home(), { recursive: true });
11371
+ await fs5.mkdir(paths.home(), { recursive: true });
10565
11372
  const final = paths.registryCache();
10566
11373
  const tmp = `${final}.tmp-${process.pid}-${randSuffix()}`;
10567
11374
  const body = JSON.stringify(
@@ -10570,10 +11377,10 @@ var Registry = class {
10570
11377
  2
10571
11378
  ) + "\n";
10572
11379
  try {
10573
- await fs4.writeFile(tmp, body, "utf8");
10574
- await fs4.rename(tmp, final);
11380
+ await fs5.writeFile(tmp, body, "utf8");
11381
+ await fs5.rename(tmp, final);
10575
11382
  } catch (err) {
10576
- await fs4.unlink(tmp).catch(() => void 0);
11383
+ await fs5.unlink(tmp).catch(() => void 0);
10577
11384
  throw err;
10578
11385
  }
10579
11386
  }
@@ -10591,7 +11398,7 @@ function npxPackageBasename(agent) {
10591
11398
  const atIdx = afterSlash.lastIndexOf("@");
10592
11399
  return atIdx <= 0 ? afterSlash : afterSlash.slice(0, atIdx);
10593
11400
  }
10594
- async function planSpawn(agent, callerArgs = []) {
11401
+ async function planSpawn(agent, callerArgs = [], options = {}) {
10595
11402
  if (agent.distribution.npx) {
10596
11403
  const npx = agent.distribution.npx;
10597
11404
  const tail = callerArgs.length > 0 ? callerArgs : npx.args ?? [];
@@ -10607,7 +11414,8 @@ async function planSpawn(agent, callerArgs = []) {
10607
11414
  agentId: agent.id,
10608
11415
  version: agent.version ?? "current",
10609
11416
  packageSpec: npx.package,
10610
- bin
11417
+ bin,
11418
+ registry: options.npmRegistry
10611
11419
  });
10612
11420
  return {
10613
11421
  command: binPath,
@@ -10832,7 +11640,7 @@ stderr: ${tail}` : reason;
10832
11640
  };
10833
11641
 
10834
11642
  // src/core/session-manager.ts
10835
- import * as fs9 from "fs/promises";
11643
+ import * as fs11 from "fs/promises";
10836
11644
  import * as os2 from "os";
10837
11645
  import { customAlphabet as customAlphabet3 } from "nanoid";
10838
11646
  init_session();
@@ -10840,7 +11648,7 @@ init_session_store();
10840
11648
 
10841
11649
  // src/core/history-store.ts
10842
11650
  init_paths();
10843
- import * as fs6 from "fs/promises";
11651
+ import * as fs8 from "fs/promises";
10844
11652
  var SESSION_ID_PATTERN2 = /^[A-Za-z0-9_-]+$/;
10845
11653
  var DEFAULT_MAX_ENTRIES = 1e3;
10846
11654
  var HistoryStore = class {
@@ -10857,9 +11665,9 @@ var HistoryStore = class {
10857
11665
  return;
10858
11666
  }
10859
11667
  return this.enqueue(sessionId, async () => {
10860
- await fs6.mkdir(paths.sessionDir(sessionId), { recursive: true });
11668
+ await fs8.mkdir(paths.sessionDir(sessionId), { recursive: true });
10861
11669
  const line = JSON.stringify(entry) + "\n";
10862
- await fs6.appendFile(paths.historyFile(sessionId), line, {
11670
+ await fs8.appendFile(paths.historyFile(sessionId), line, {
10863
11671
  encoding: "utf8",
10864
11672
  mode: 384
10865
11673
  });
@@ -10870,9 +11678,9 @@ var HistoryStore = class {
10870
11678
  return;
10871
11679
  }
10872
11680
  return this.enqueue(sessionId, async () => {
10873
- await fs6.mkdir(paths.sessionDir(sessionId), { recursive: true });
11681
+ await fs8.mkdir(paths.sessionDir(sessionId), { recursive: true });
10874
11682
  const body = entries.length === 0 ? "" : entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
10875
- await fs6.writeFile(paths.historyFile(sessionId), body, {
11683
+ await fs8.writeFile(paths.historyFile(sessionId), body, {
10876
11684
  encoding: "utf8",
10877
11685
  mode: 384
10878
11686
  });
@@ -10889,7 +11697,7 @@ var HistoryStore = class {
10889
11697
  return this.enqueue(sessionId, async () => {
10890
11698
  let raw;
10891
11699
  try {
10892
- raw = await fs6.readFile(paths.historyFile(sessionId), "utf8");
11700
+ raw = await fs8.readFile(paths.historyFile(sessionId), "utf8");
10893
11701
  } catch (err) {
10894
11702
  const e = err;
10895
11703
  if (e.code === "ENOENT") {
@@ -10902,7 +11710,7 @@ var HistoryStore = class {
10902
11710
  return;
10903
11711
  }
10904
11712
  const trimmed = lines.slice(-maxEntries);
10905
- await fs6.writeFile(paths.historyFile(sessionId), trimmed.join("\n") + "\n", {
11713
+ await fs8.writeFile(paths.historyFile(sessionId), trimmed.join("\n") + "\n", {
10906
11714
  encoding: "utf8",
10907
11715
  mode: 384
10908
11716
  });
@@ -10918,7 +11726,7 @@ var HistoryStore = class {
10918
11726
  }
10919
11727
  let raw;
10920
11728
  try {
10921
- raw = await fs6.readFile(paths.historyFile(sessionId), "utf8");
11729
+ raw = await fs8.readFile(paths.historyFile(sessionId), "utf8");
10922
11730
  } catch (err) {
10923
11731
  const e = err;
10924
11732
  if (e.code === "ENOENT") {
@@ -10964,7 +11772,7 @@ var HistoryStore = class {
10964
11772
  }
10965
11773
  return this.enqueue(sessionId, async () => {
10966
11774
  try {
10967
- await fs6.unlink(paths.historyFile(sessionId));
11775
+ await fs8.unlink(paths.historyFile(sessionId));
10968
11776
  } catch (err) {
10969
11777
  const e = err;
10970
11778
  if (e.code !== "ENOENT") {
@@ -10972,7 +11780,7 @@ var HistoryStore = class {
10972
11780
  }
10973
11781
  }
10974
11782
  try {
10975
- await fs6.rmdir(paths.sessionDir(sessionId));
11783
+ await fs8.rmdir(paths.sessionDir(sessionId));
10976
11784
  } catch (err) {
10977
11785
  const e = err;
10978
11786
  if (e.code !== "ENOENT" && e.code !== "ENOTEMPTY") {
@@ -11000,6 +11808,8 @@ init_paths();
11000
11808
  init_history();
11001
11809
  init_types();
11002
11810
  init_hydra_version();
11811
+ init_queue_store();
11812
+ var QUEUE_REPLAY_TTL_MS = 15 * 60 * 1e3;
11003
11813
  var HYDRA_ID_ALPHABET3 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
11004
11814
  var generateRawSessionId = customAlphabet3(HYDRA_ID_ALPHABET3, 16);
11005
11815
  var SessionManager = class {
@@ -11012,6 +11822,7 @@ var SessionManager = class {
11012
11822
  this.idleTimeoutMs = options.idleTimeoutMs ?? 0;
11013
11823
  this.defaultModels = options.defaultModels ?? {};
11014
11824
  this.logger = options.logger;
11825
+ this.npmRegistry = options.npmRegistry;
11015
11826
  }
11016
11827
  registry;
11017
11828
  sessions = /* @__PURE__ */ new Map();
@@ -11027,6 +11838,7 @@ var SessionManager = class {
11027
11838
  // back-to-back) don't lose writes via interleaved reads.
11028
11839
  metaWriteQueues = /* @__PURE__ */ new Map();
11029
11840
  logger;
11841
+ npmRegistry;
11030
11842
  async create(params) {
11031
11843
  const fresh = await this.bootstrapAgent({
11032
11844
  agentId: params.agentId,
@@ -11048,7 +11860,9 @@ var SessionManager = class {
11048
11860
  spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
11049
11861
  historyStore: this.histories,
11050
11862
  historyMaxEntries: this.sessionHistoryMaxEntries,
11051
- currentModel: fresh.initialModel
11863
+ currentModel: fresh.initialModel,
11864
+ currentMode: fresh.initialMode,
11865
+ agentModes: fresh.initialModes
11052
11866
  });
11053
11867
  await this.attachManagerHooks(session);
11054
11868
  return session;
@@ -11093,7 +11907,7 @@ var SessionManager = class {
11093
11907
  if (params.upstreamSessionId === "") {
11094
11908
  return this.doResurrectFromImport(params);
11095
11909
  }
11096
- const plan = await planSpawn(agentDef, params.agentArgs ?? []);
11910
+ const plan = await planSpawn(agentDef, params.agentArgs ?? [], { npmRegistry: this.npmRegistry });
11097
11911
  const agent = this.spawner({
11098
11912
  agentId: params.agentId,
11099
11913
  cwd: params.cwd,
@@ -11147,9 +11961,10 @@ var SessionManager = class {
11147
11961
  // this fix), fall back to the model the agent ships in its
11148
11962
  // session/load response body.
11149
11963
  currentModel: params.currentModel ?? extractInitialModel(loadResult ?? {}),
11150
- currentMode: params.currentMode,
11964
+ currentMode: params.currentMode ?? extractInitialCurrentMode(loadResult ?? {}),
11151
11965
  currentUsage: params.currentUsage,
11152
11966
  agentCommands: params.agentCommands,
11967
+ agentModes: params.agentModes ?? nonEmptyOrUndefined(extractInitialModes(loadResult ?? {})),
11153
11968
  // Only gate the first-prompt title heuristic when we actually have
11154
11969
  // a title to preserve. A title-less session (lost to a write race
11155
11970
  // or never seeded) should re-derive from the next prompt rather
@@ -11192,9 +12007,10 @@ var SessionManager = class {
11192
12007
  // Prefer the stored value (set by a previous current_model_update);
11193
12008
  // fall back to whatever the agent ships in its session/new response.
11194
12009
  currentModel: params.currentModel ?? fresh.initialModel,
11195
- currentMode: params.currentMode,
12010
+ currentMode: params.currentMode ?? fresh.initialMode,
11196
12011
  currentUsage: params.currentUsage,
11197
12012
  agentCommands: params.agentCommands,
12013
+ agentModes: params.agentModes ?? fresh.initialModes,
11198
12014
  firstPromptSeeded: !!params.title,
11199
12015
  createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0
11200
12016
  });
@@ -11204,7 +12020,7 @@ var SessionManager = class {
11204
12020
  }
11205
12021
  async resolveImportCwd(cwd) {
11206
12022
  try {
11207
- const stat4 = await fs9.stat(cwd);
12023
+ const stat4 = await fs11.stat(cwd);
11208
12024
  if (stat4.isDirectory()) {
11209
12025
  return cwd;
11210
12026
  }
@@ -11224,7 +12040,7 @@ var SessionManager = class {
11224
12040
  err.code = JsonRpcErrorCodes.AgentNotInstalled;
11225
12041
  throw err;
11226
12042
  }
11227
- const plan = await planSpawn(agentDef, params.agentArgs ?? []);
12043
+ const plan = await planSpawn(agentDef, params.agentArgs ?? [], { npmRegistry: this.npmRegistry });
11228
12044
  const agent = this.spawner({
11229
12045
  agentId: params.agentId,
11230
12046
  cwd: params.cwd,
@@ -11261,11 +12077,15 @@ var SessionManager = class {
11261
12077
  } catch {
11262
12078
  }
11263
12079
  }
12080
+ const initialModes = extractInitialModes(newResult);
12081
+ const initialMode = extractInitialCurrentMode(newResult);
11264
12082
  return {
11265
12083
  agent,
11266
12084
  upstreamSessionId: sessionIdRaw,
11267
12085
  agentMeta: newResult._meta,
11268
- initialModel
12086
+ initialModel,
12087
+ initialModes: initialModes.length > 0 ? initialModes : void 0,
12088
+ initialMode
11269
12089
  };
11270
12090
  } catch (err) {
11271
12091
  await agent.kill().catch(() => void 0);
@@ -11317,6 +12137,15 @@ var SessionManager = class {
11317
12137
  }))
11318
12138
  }).catch(() => void 0);
11319
12139
  });
12140
+ session.onAgentModesChange((modes) => {
12141
+ void this.persistSnapshot(session.sessionId, {
12142
+ agentModes: modes.map((m) => ({
12143
+ id: m.id,
12144
+ ...m.name !== void 0 ? { name: m.name } : {},
12145
+ ...m.description !== void 0 ? { description: m.description } : {}
12146
+ }))
12147
+ }).catch(() => void 0);
12148
+ });
11320
12149
  this.sessions.set(session.sessionId, session);
11321
12150
  await this.enqueueMetaWrite(session.sessionId, async () => {
11322
12151
  const existing = await this.store.read(session.sessionId);
@@ -11359,6 +12188,7 @@ var SessionManager = class {
11359
12188
  currentMode: record.currentMode,
11360
12189
  currentUsage: persistedUsageToSnapshot(record.currentUsage),
11361
12190
  agentCommands: record.agentCommands,
12191
+ agentModes: record.agentModes,
11362
12192
  createdAt: record.createdAt
11363
12193
  };
11364
12194
  }
@@ -11636,6 +12466,7 @@ var SessionManager = class {
11636
12466
  ...update.currentMode !== void 0 ? { currentMode: update.currentMode } : {},
11637
12467
  ...update.currentUsage !== void 0 ? { currentUsage: update.currentUsage } : {},
11638
12468
  ...update.agentCommands !== void 0 ? { agentCommands: update.agentCommands } : {},
12469
+ ...update.agentModes !== void 0 ? { agentModes: update.agentModes } : {},
11639
12470
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
11640
12471
  });
11641
12472
  });
@@ -11669,6 +12500,53 @@ var SessionManager = class {
11669
12500
  }
11670
12501
  await Promise.allSettled(pending);
11671
12502
  }
12503
+ // Startup hook: scan persisted sessions for non-empty queue files,
12504
+ // apply the TTL, resurrect anything with surviving entries, and
12505
+ // replay them through the normal queue path. Called from the daemon
12506
+ // boot sequence; failures per session are logged and don't block
12507
+ // the boot.
12508
+ //
12509
+ // Concurrency is deliberately sequential — resurrect each session
12510
+ // one at a time so a runaway daemon with 100 queued sessions
12511
+ // doesn't burst-spawn 100 agents on startup. Inside a single
12512
+ // session, the queue still drains in parallel-friendly fashion via
12513
+ // drainQueue once resurrect() completes.
12514
+ async resurrectPendingQueues() {
12515
+ const records = await this.store.list().catch(() => []);
12516
+ for (const rec of records) {
12517
+ const queue = await loadQueue(rec.sessionId).catch(() => []);
12518
+ if (queue.length === 0) continue;
12519
+ const now = Date.now();
12520
+ const fresh = queue.filter((e) => now - e.enqueuedAt < QUEUE_REPLAY_TTL_MS);
12521
+ const dropped = queue.length - fresh.length;
12522
+ if (dropped > 0) {
12523
+ this.logger?.info(
12524
+ `queue replay: dropping ${dropped} stale prompt(s) for ${rec.sessionId} (TTL ${QUEUE_REPLAY_TTL_MS / 1e3}s)`
12525
+ );
12526
+ await rewriteQueue(rec.sessionId, fresh).catch(() => void 0);
12527
+ }
12528
+ if (fresh.length === 0) continue;
12529
+ const fromDisk = await this.loadFromDisk(rec.sessionId).catch(() => void 0);
12530
+ if (!fromDisk) {
12531
+ this.logger?.warn(
12532
+ `queue replay: no meta for ${rec.sessionId}; discarding ${fresh.length} entr${fresh.length === 1 ? "y" : "ies"}`
12533
+ );
12534
+ await rewriteQueue(rec.sessionId, []).catch(() => void 0);
12535
+ continue;
12536
+ }
12537
+ try {
12538
+ const session = await this.resurrect(fromDisk);
12539
+ this.logger?.info(
12540
+ `queue replay: resurrected ${rec.sessionId} and replaying ${fresh.length} prompt(s)`
12541
+ );
12542
+ session.replayPersistedQueue(fresh);
12543
+ } catch (err) {
12544
+ this.logger?.warn(
12545
+ `queue replay: failed to resurrect ${rec.sessionId}: ${err.message}`
12546
+ );
12547
+ }
12548
+ }
12549
+ }
11672
12550
  };
11673
12551
  function mergeForPersistence(session, existing) {
11674
12552
  const persistedCommands = session.mergedAvailableCommands().length > 0 ? session.agentOnlyAdvertisedCommands().map((c) => {
@@ -11678,6 +12556,18 @@ function mergeForPersistence(session, existing) {
11678
12556
  return { name: c.name };
11679
12557
  }) : void 0;
11680
12558
  const agentCommands = persistedCommands ?? existing?.agentCommands;
12559
+ const sessionModes = session.availableModes();
12560
+ const persistedModes = sessionModes.length > 0 ? sessionModes.map((m) => {
12561
+ const out = { id: m.id };
12562
+ if (m.name !== void 0) {
12563
+ out.name = m.name;
12564
+ }
12565
+ if (m.description !== void 0) {
12566
+ out.description = m.description;
12567
+ }
12568
+ return out;
12569
+ }) : void 0;
12570
+ const agentModes = persistedModes ?? existing?.agentModes;
11681
12571
  return recordFromMemorySession({
11682
12572
  sessionId: session.sessionId,
11683
12573
  lineageId: existing?.lineageId ?? generateLineageId(),
@@ -11693,6 +12583,7 @@ function mergeForPersistence(session, existing) {
11693
12583
  currentMode: session.currentMode ?? existing?.currentMode,
11694
12584
  currentUsage: usageSnapshotToPersisted(session.currentUsage) ?? existing?.currentUsage,
11695
12585
  agentCommands,
12586
+ agentModes,
11696
12587
  createdAt: existing?.createdAt ?? new Date(session.createdAt).toISOString()
11697
12588
  });
11698
12589
  }
@@ -11755,9 +12646,103 @@ function asString(value) {
11755
12646
  const trimmed = value.trim();
11756
12647
  return trimmed.length > 0 ? trimmed : void 0;
11757
12648
  }
12649
+ function nonEmptyOrUndefined(arr) {
12650
+ return arr.length > 0 ? arr : void 0;
12651
+ }
12652
+ function extractInitialModes(result) {
12653
+ const direct = parseModesList(result.availableModes);
12654
+ if (direct.length > 0) {
12655
+ return direct;
12656
+ }
12657
+ const modes = result.modes;
12658
+ if (modes && typeof modes === "object" && !Array.isArray(modes)) {
12659
+ const fromModesObj = parseModesList(
12660
+ modes.availableModes
12661
+ );
12662
+ if (fromModesObj.length > 0) {
12663
+ return fromModesObj;
12664
+ }
12665
+ }
12666
+ const meta = result._meta;
12667
+ if (meta && typeof meta === "object" && !Array.isArray(meta)) {
12668
+ for (const [key, value] of Object.entries(
12669
+ meta
12670
+ )) {
12671
+ if (key === "hydra-acp") {
12672
+ continue;
12673
+ }
12674
+ if (value && typeof value === "object" && !Array.isArray(value)) {
12675
+ const fromMeta = parseModesList(
12676
+ value.availableModes
12677
+ );
12678
+ if (fromMeta.length > 0) {
12679
+ return fromMeta;
12680
+ }
12681
+ }
12682
+ }
12683
+ }
12684
+ return [];
12685
+ }
12686
+ function extractInitialCurrentMode(result) {
12687
+ const direct = asString(result.currentModeId) ?? asString(result.currentMode) ?? asString(result.modeId) ?? asString(result.mode);
12688
+ if (direct) {
12689
+ return direct;
12690
+ }
12691
+ const modes = result.modes;
12692
+ if (modes && typeof modes === "object" && !Array.isArray(modes)) {
12693
+ const m = asString(modes.currentModeId) ?? asString(modes.currentMode);
12694
+ if (m) {
12695
+ return m;
12696
+ }
12697
+ }
12698
+ const meta = result._meta;
12699
+ if (meta && typeof meta === "object" && !Array.isArray(meta)) {
12700
+ for (const [key, value] of Object.entries(
12701
+ meta
12702
+ )) {
12703
+ if (key === "hydra-acp") {
12704
+ continue;
12705
+ }
12706
+ if (value && typeof value === "object" && !Array.isArray(value)) {
12707
+ const m = asString(value.currentModeId) ?? asString(value.currentMode) ?? asString(value.modeId);
12708
+ if (m) {
12709
+ return m;
12710
+ }
12711
+ }
12712
+ }
12713
+ }
12714
+ return void 0;
12715
+ }
12716
+ function parseModesList(list) {
12717
+ if (!Array.isArray(list)) {
12718
+ return [];
12719
+ }
12720
+ const out = [];
12721
+ for (const raw of list) {
12722
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
12723
+ continue;
12724
+ }
12725
+ const r = raw;
12726
+ const id = asString(r.id) ?? asString(r.modeId);
12727
+ if (!id) {
12728
+ continue;
12729
+ }
12730
+ const mode = { id };
12731
+ const name = asString(r.name);
12732
+ if (name) {
12733
+ mode.name = name;
12734
+ }
12735
+ const description = asString(r.description);
12736
+ if (description) {
12737
+ mode.description = description;
12738
+ }
12739
+ out.push(mode);
12740
+ }
12741
+ return out;
12742
+ }
11758
12743
  async function loadPromptHistorySafely(sessionId) {
11759
12744
  try {
11760
- const raw = await fs9.readFile(paths.tuiHistoryFile(sessionId), "utf8");
12745
+ const raw = await fs11.readFile(paths.tuiHistoryFile(sessionId), "utf8");
11761
12746
  const out = [];
11762
12747
  for (const line of raw.split("\n")) {
11763
12748
  if (line.length === 0) {
@@ -11778,7 +12763,7 @@ async function loadPromptHistorySafely(sessionId) {
11778
12763
  }
11779
12764
  async function historyMtimeIso(sessionId) {
11780
12765
  try {
11781
- const st = await fs9.stat(paths.historyFile(sessionId));
12766
+ const st = await fs11.stat(paths.historyFile(sessionId));
11782
12767
  return new Date(st.mtimeMs).toISOString();
11783
12768
  } catch {
11784
12769
  return void 0;
@@ -11788,7 +12773,7 @@ async function historyMtimeIso(sessionId) {
11788
12773
  // src/core/extensions.ts
11789
12774
  init_paths();
11790
12775
  import { spawn as spawn4 } from "child_process";
11791
- import * as fs10 from "fs";
12776
+ import * as fs12 from "fs";
11792
12777
  import * as fsp3 from "fs/promises";
11793
12778
  import * as path7 from "path";
11794
12779
  var RESTART_BASE_MS = 1e3;
@@ -12071,7 +13056,7 @@ var ExtensionManager = class {
12071
13056
  }
12072
13057
  const ext = entry.config;
12073
13058
  const command = ext.command.length > 0 ? ext.command : [ext.name];
12074
- const logStream = fs10.createWriteStream(paths.extensionLogFile(ext.name), {
13059
+ const logStream = fs12.createWriteStream(paths.extensionLogFile(ext.name), {
12075
13060
  flags: "a"
12076
13061
  });
12077
13062
  logStream.write(
@@ -12083,7 +13068,7 @@ var ExtensionManager = class {
12083
13068
  HYDRA_ACP_DAEMON_URL: ctx.daemonUrl,
12084
13069
  HYDRA_ACP_DAEMON_HOST: ctx.daemonHost,
12085
13070
  HYDRA_ACP_DAEMON_PORT: String(ctx.daemonPort),
12086
- HYDRA_ACP_TOKEN: ctx.daemonToken,
13071
+ HYDRA_ACP_TOKEN: ctx.serviceToken,
12087
13072
  HYDRA_ACP_WS_URL: ctx.daemonWsUrl,
12088
13073
  HYDRA_ACP_HOME: ctx.hydraHome,
12089
13074
  HYDRA_ACP_EXTENSION_NAME: ext.name,
@@ -12121,7 +13106,7 @@ var ExtensionManager = class {
12121
13106
  }
12122
13107
  if (typeof child.pid === "number") {
12123
13108
  try {
12124
- fs10.writeFileSync(paths.extensionPidFile(ext.name), `${child.pid}
13109
+ fs12.writeFileSync(paths.extensionPidFile(ext.name), `${child.pid}
12125
13110
  `, {
12126
13111
  encoding: "utf8",
12127
13112
  mode: 384
@@ -12146,7 +13131,7 @@ var ExtensionManager = class {
12146
13131
  });
12147
13132
  child.on("exit", (code, signal) => {
12148
13133
  try {
12149
- fs10.unlinkSync(paths.extensionPidFile(ext.name));
13134
+ fs12.unlinkSync(paths.extensionPidFile(ext.name));
12150
13135
  } catch {
12151
13136
  }
12152
13137
  logStream.write(
@@ -12189,25 +13174,245 @@ var ExtensionManager = class {
12189
13174
  }
12190
13175
  }
12191
13176
  };
12192
- function isAlive(pid) {
12193
- try {
12194
- process.kill(pid, 0);
12195
- return true;
12196
- } catch {
13177
+ function isAlive(pid) {
13178
+ try {
13179
+ process.kill(pid, 0);
13180
+ return true;
13181
+ } catch {
13182
+ return false;
13183
+ }
13184
+ }
13185
+ function withCode2(err, code) {
13186
+ err.code = code;
13187
+ return err;
13188
+ }
13189
+
13190
+ // src/daemon/server.ts
13191
+ init_paths();
13192
+ init_hydra_version();
13193
+
13194
+ // src/core/session-tokens.ts
13195
+ init_paths();
13196
+ import * as fs13 from "fs/promises";
13197
+ import * as path8 from "path";
13198
+ import { createHash, randomBytes, timingSafeEqual } from "crypto";
13199
+ var TOKEN_PREFIX = "hydra_session_";
13200
+ var DEFAULT_TTL_SEC = 60 * 60 * 24 * 30;
13201
+ var ID_LENGTH = 12;
13202
+ var TOKEN_BYTES = 32;
13203
+ var WRITE_DEBOUNCE_MS = 50;
13204
+ function tokensFilePath() {
13205
+ return path8.join(paths.home(), "session-tokens.json");
13206
+ }
13207
+ function sha256Hex(input) {
13208
+ return createHash("sha256").update(input).digest("hex");
13209
+ }
13210
+ function randomHex(bytes) {
13211
+ return randomBytes(bytes).toString("hex");
13212
+ }
13213
+ function generateId() {
13214
+ return randomHex(ID_LENGTH).slice(0, ID_LENGTH * 2);
13215
+ }
13216
+ function generateToken() {
13217
+ return `${TOKEN_PREFIX}${randomHex(TOKEN_BYTES)}`;
13218
+ }
13219
+ var SessionTokenStore = class _SessionTokenStore {
13220
+ records = /* @__PURE__ */ new Map();
13221
+ // keyed by hash
13222
+ writeTimer = null;
13223
+ writeInflight = null;
13224
+ constructor(records) {
13225
+ for (const r of records) {
13226
+ this.records.set(r.hash, r);
13227
+ }
13228
+ }
13229
+ static async load() {
13230
+ let records = [];
13231
+ try {
13232
+ const raw = await fs13.readFile(tokensFilePath(), "utf8");
13233
+ const parsed = JSON.parse(raw);
13234
+ if (parsed && Array.isArray(parsed.records)) {
13235
+ records = parsed.records.filter(isRecord);
13236
+ }
13237
+ } catch (err) {
13238
+ const e = err;
13239
+ if (e.code !== "ENOENT") {
13240
+ throw err;
13241
+ }
13242
+ }
13243
+ const store = new _SessionTokenStore(records);
13244
+ const removed = store.sweepExpired(/* @__PURE__ */ new Date());
13245
+ if (removed > 0) {
13246
+ await store.flush();
13247
+ }
13248
+ return store;
13249
+ }
13250
+ async issue(opts = {}) {
13251
+ const token = generateToken();
13252
+ const hash = sha256Hex(token);
13253
+ const id = generateId();
13254
+ const now = /* @__PURE__ */ new Date();
13255
+ const ttlSec = opts.ttlSec && opts.ttlSec > 0 ? opts.ttlSec : DEFAULT_TTL_SEC;
13256
+ const expiresAt = new Date(now.getTime() + ttlSec * 1e3);
13257
+ const record = {
13258
+ id,
13259
+ hash,
13260
+ label: opts.label,
13261
+ createdAt: now.toISOString(),
13262
+ expiresAt: expiresAt.toISOString(),
13263
+ lastUsedAt: now.toISOString()
13264
+ };
13265
+ this.records.set(hash, record);
13266
+ this.scheduleWrite();
13267
+ return { id, token, expiresAt: record.expiresAt };
13268
+ }
13269
+ // Verifies a presented token. Returns the matching record id (so the
13270
+ // caller can revoke it on logout) and bumps lastUsedAt; returns
13271
+ // undefined when no record matches or when the matched record has
13272
+ // expired.
13273
+ async verify(token) {
13274
+ if (typeof token !== "string" || !token.startsWith(TOKEN_PREFIX)) {
13275
+ return void 0;
13276
+ }
13277
+ const hash = sha256Hex(token);
13278
+ const record = this.records.get(hash);
13279
+ if (!record) {
13280
+ return void 0;
13281
+ }
13282
+ const expected = Buffer.from(record.hash, "hex");
13283
+ const actual = Buffer.from(hash, "hex");
13284
+ if (expected.length !== actual.length || !timingSafeEqual(expected, actual)) {
13285
+ return void 0;
13286
+ }
13287
+ const now = /* @__PURE__ */ new Date();
13288
+ if (new Date(record.expiresAt).getTime() <= now.getTime()) {
13289
+ this.records.delete(hash);
13290
+ this.scheduleWrite();
13291
+ return void 0;
13292
+ }
13293
+ record.lastUsedAt = now.toISOString();
13294
+ this.scheduleWrite();
13295
+ return record.id;
13296
+ }
13297
+ async revoke(id) {
13298
+ for (const [hash, r] of this.records) {
13299
+ if (r.id === id) {
13300
+ this.records.delete(hash);
13301
+ this.scheduleWrite();
13302
+ return true;
13303
+ }
13304
+ }
13305
+ return false;
13306
+ }
13307
+ async revokeAll() {
13308
+ const n = this.records.size;
13309
+ this.records.clear();
13310
+ this.scheduleWrite();
13311
+ return n;
13312
+ }
13313
+ list() {
13314
+ return Array.from(this.records.values()).map(({ id, label, createdAt, expiresAt, lastUsedAt }) => ({
13315
+ id,
13316
+ label,
13317
+ createdAt,
13318
+ expiresAt,
13319
+ lastUsedAt
13320
+ })).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
13321
+ }
13322
+ sweepExpired(now = /* @__PURE__ */ new Date()) {
13323
+ let removed = 0;
13324
+ for (const [hash, r] of this.records) {
13325
+ if (new Date(r.expiresAt).getTime() <= now.getTime()) {
13326
+ this.records.delete(hash);
13327
+ removed += 1;
13328
+ }
13329
+ }
13330
+ if (removed > 0) {
13331
+ this.scheduleWrite();
13332
+ }
13333
+ return removed;
13334
+ }
13335
+ // Force any pending write to complete. Useful in tests and at shutdown.
13336
+ async flush() {
13337
+ if (this.writeTimer) {
13338
+ clearTimeout(this.writeTimer);
13339
+ this.writeTimer = null;
13340
+ }
13341
+ await this.persist();
13342
+ }
13343
+ scheduleWrite() {
13344
+ if (this.writeTimer) {
13345
+ return;
13346
+ }
13347
+ this.writeTimer = setTimeout(() => {
13348
+ this.writeTimer = null;
13349
+ this.persist().catch(() => {
13350
+ });
13351
+ }, WRITE_DEBOUNCE_MS);
13352
+ }
13353
+ async persist() {
13354
+ if (this.writeInflight) {
13355
+ await this.writeInflight;
13356
+ }
13357
+ const records = Array.from(this.records.values());
13358
+ const payload = JSON.stringify({ records }, null, 2) + "\n";
13359
+ this.writeInflight = (async () => {
13360
+ await fs13.mkdir(paths.home(), { recursive: true });
13361
+ await fs13.writeFile(tokensFilePath(), payload, {
13362
+ encoding: "utf8",
13363
+ mode: 384
13364
+ });
13365
+ })();
13366
+ try {
13367
+ await this.writeInflight;
13368
+ } finally {
13369
+ this.writeInflight = null;
13370
+ }
13371
+ }
13372
+ };
13373
+ function isRecord(value) {
13374
+ if (!value || typeof value !== "object") {
12197
13375
  return false;
12198
13376
  }
13377
+ const v = value;
13378
+ return typeof v.id === "string" && typeof v.hash === "string" && typeof v.createdAt === "string" && typeof v.expiresAt === "string" && typeof v.lastUsedAt === "string" && (v.label === void 0 || typeof v.label === "string");
12199
13379
  }
12200
- function withCode2(err, code) {
12201
- err.code = code;
12202
- return err;
12203
- }
12204
-
12205
- // src/daemon/server.ts
12206
- init_paths();
12207
- init_hydra_version();
12208
13380
 
12209
13381
  // src/daemon/auth.ts
12210
13382
  var BEARER_PREFIX = "Bearer ";
13383
+ var StaticTokenValidator = class {
13384
+ constructor(token) {
13385
+ this.token = token;
13386
+ }
13387
+ token;
13388
+ async validate(token) {
13389
+ return constantTimeEqual(token, this.token) ? "service" : void 0;
13390
+ }
13391
+ };
13392
+ var SessionTokenValidator = class {
13393
+ constructor(store) {
13394
+ this.store = store;
13395
+ }
13396
+ store;
13397
+ async validate(token) {
13398
+ return this.store.verify(token);
13399
+ }
13400
+ };
13401
+ var CompositeTokenValidator = class {
13402
+ constructor(validators) {
13403
+ this.validators = validators;
13404
+ }
13405
+ validators;
13406
+ async validate(token) {
13407
+ for (const v of this.validators) {
13408
+ const id = await v.validate(token);
13409
+ if (id !== void 0) {
13410
+ return id;
13411
+ }
13412
+ }
13413
+ return void 0;
13414
+ }
13415
+ };
12211
13416
  function bearerAuth(opts) {
12212
13417
  return async function authMiddleware(request, reply) {
12213
13418
  const header = request.headers.authorization;
@@ -12216,10 +13421,12 @@ function bearerAuth(opts) {
12216
13421
  return;
12217
13422
  }
12218
13423
  const token = header.slice(BEARER_PREFIX.length).trim();
12219
- if (!constantTimeEqual(token, opts.config.daemon.authToken)) {
13424
+ const identity = await opts.validator.validate(token);
13425
+ if (!identity) {
12220
13426
  reply.code(403).send({ error: "Invalid token" });
12221
13427
  return;
12222
13428
  }
13429
+ request.authIdentity = identity;
12223
13430
  };
12224
13431
  }
12225
13432
  function tokenFromUpgradeRequest(req) {
@@ -12258,6 +13465,40 @@ function constantTimeEqual(a, b) {
12258
13465
  return mismatch === 0;
12259
13466
  }
12260
13467
 
13468
+ // src/daemon/rate-limit.ts
13469
+ var AuthRateLimiter = class {
13470
+ entries = /* @__PURE__ */ new Map();
13471
+ maxFails;
13472
+ windowMs;
13473
+ constructor(maxFails = 10, windowMs = 15 * 60 * 1e3) {
13474
+ this.maxFails = maxFails;
13475
+ this.windowMs = windowMs;
13476
+ }
13477
+ isBlocked(ip) {
13478
+ const e = this.entries.get(ip);
13479
+ if (!e) {
13480
+ return false;
13481
+ }
13482
+ if (Date.now() - e.windowStart > this.windowMs) {
13483
+ this.entries.delete(ip);
13484
+ return false;
13485
+ }
13486
+ return e.fails >= this.maxFails;
13487
+ }
13488
+ recordFailure(ip) {
13489
+ const now = Date.now();
13490
+ const e = this.entries.get(ip);
13491
+ if (!e || now - e.windowStart > this.windowMs) {
13492
+ this.entries.set(ip, { fails: 1, windowStart: now });
13493
+ return;
13494
+ }
13495
+ e.fails += 1;
13496
+ }
13497
+ recordSuccess(ip) {
13498
+ this.entries.delete(ip);
13499
+ }
13500
+ };
13501
+
12261
13502
  // src/daemon/routes/sessions.ts
12262
13503
  init_config();
12263
13504
  init_bundle();
@@ -12625,6 +13866,181 @@ function registerConfigRoutes(app, defaults) {
12625
13866
  });
12626
13867
  }
12627
13868
 
13869
+ // src/daemon/routes/auth.ts
13870
+ import { z as z6 } from "zod";
13871
+
13872
+ // src/core/password.ts
13873
+ init_paths();
13874
+ import * as fs14 from "fs/promises";
13875
+ import * as path9 from "path";
13876
+ import { randomBytes as randomBytes2, scrypt, timingSafeEqual as timingSafeEqual2 } from "crypto";
13877
+ import { promisify } from "util";
13878
+ var scryptAsync = promisify(scrypt);
13879
+ function passwordHashPath() {
13880
+ return path9.join(paths.home(), "password-hash");
13881
+ }
13882
+ var DEFAULT_N = 1 << 15;
13883
+ var DEFAULT_R = 8;
13884
+ var DEFAULT_P = 1;
13885
+ var KEY_LEN = 64;
13886
+ var SALT_LEN = 16;
13887
+ var MAX_MEM = 128 * 1024 * 1024;
13888
+ async function setPassword(plaintext) {
13889
+ if (typeof plaintext !== "string" || plaintext.length === 0) {
13890
+ throw new Error("password must be a non-empty string");
13891
+ }
13892
+ const salt = randomBytes2(SALT_LEN);
13893
+ const key = await scryptAsync(plaintext, salt, KEY_LEN, {
13894
+ N: DEFAULT_N,
13895
+ r: DEFAULT_R,
13896
+ p: DEFAULT_P,
13897
+ maxmem: MAX_MEM
13898
+ });
13899
+ const encoded = `scrypt$${DEFAULT_N}$${DEFAULT_R}$${DEFAULT_P}$${salt.toString("hex")}$${key.toString("hex")}
13900
+ `;
13901
+ await fs14.mkdir(paths.home(), { recursive: true });
13902
+ await fs14.writeFile(passwordHashPath(), encoded, {
13903
+ encoding: "utf8",
13904
+ mode: 384
13905
+ });
13906
+ }
13907
+ async function hasPassword() {
13908
+ try {
13909
+ const text = await fs14.readFile(passwordHashPath(), "utf8");
13910
+ return text.trim().length > 0;
13911
+ } catch (err) {
13912
+ const e = err;
13913
+ if (e.code === "ENOENT") {
13914
+ return false;
13915
+ }
13916
+ throw err;
13917
+ }
13918
+ }
13919
+ async function verifyPassword(plaintext) {
13920
+ if (typeof plaintext !== "string" || plaintext.length === 0) {
13921
+ return false;
13922
+ }
13923
+ let line;
13924
+ try {
13925
+ line = (await fs14.readFile(passwordHashPath(), "utf8")).trim();
13926
+ } catch (err) {
13927
+ const e = err;
13928
+ if (e.code === "ENOENT") {
13929
+ return false;
13930
+ }
13931
+ throw err;
13932
+ }
13933
+ const parts = line.split("$");
13934
+ if (parts.length !== 6 || parts[0] !== "scrypt") {
13935
+ return false;
13936
+ }
13937
+ const N = parseInt(parts[1], 10);
13938
+ const r = parseInt(parts[2], 10);
13939
+ const p = parseInt(parts[3], 10);
13940
+ if (!Number.isFinite(N) || !Number.isFinite(r) || !Number.isFinite(p)) {
13941
+ return false;
13942
+ }
13943
+ const salt = Buffer.from(parts[4], "hex");
13944
+ const expected = Buffer.from(parts[5], "hex");
13945
+ if (salt.length === 0 || expected.length === 0) {
13946
+ return false;
13947
+ }
13948
+ const actual = await scryptAsync(plaintext, salt, expected.length, {
13949
+ N,
13950
+ r,
13951
+ p,
13952
+ maxmem: MAX_MEM
13953
+ });
13954
+ if (actual.length !== expected.length) {
13955
+ return false;
13956
+ }
13957
+ return timingSafeEqual2(actual, expected);
13958
+ }
13959
+
13960
+ // src/daemon/routes/auth.ts
13961
+ var LoginBody = z6.object({
13962
+ password: z6.string().min(1),
13963
+ label: z6.string().min(1).max(256).optional(),
13964
+ ttlSec: z6.number().int().positive().optional()
13965
+ });
13966
+ var LogoutBody = z6.object({
13967
+ id: z6.string().optional()
13968
+ }).optional();
13969
+ function registerAuthRoutes(app, deps) {
13970
+ app.post(
13971
+ "/v1/auth/login",
13972
+ { config: { skipAuth: true } },
13973
+ async (request, reply) => {
13974
+ const ip = remoteIp(request);
13975
+ if (deps.rateLimiter.isBlocked(ip)) {
13976
+ return reply.code(429).send({
13977
+ error: "Too many failed attempts; try again later."
13978
+ });
13979
+ }
13980
+ let body;
13981
+ try {
13982
+ body = LoginBody.parse(request.body);
13983
+ } catch {
13984
+ return reply.code(400).send({ error: "Invalid request body" });
13985
+ }
13986
+ if (!await hasPassword()) {
13987
+ return reply.code(403).send({
13988
+ error: "No password configured. Run `hydra-acp auth password` on the daemon host."
13989
+ });
13990
+ }
13991
+ const ok = await verifyPassword(body.password);
13992
+ if (!ok) {
13993
+ deps.rateLimiter.recordFailure(ip);
13994
+ return reply.code(401).send({ error: "Invalid password" });
13995
+ }
13996
+ deps.rateLimiter.recordSuccess(ip);
13997
+ const issued = await deps.store.issue({
13998
+ label: body.label,
13999
+ ttlSec: body.ttlSec
14000
+ });
14001
+ return reply.code(200).send({
14002
+ session_token: issued.token,
14003
+ id: issued.id,
14004
+ expires_at: issued.expiresAt
14005
+ });
14006
+ }
14007
+ );
14008
+ app.post("/v1/auth/logout", async (request, reply) => {
14009
+ let body = void 0;
14010
+ try {
14011
+ body = LogoutBody.parse(request.body ?? void 0);
14012
+ } catch {
14013
+ return reply.code(400).send({ error: "Invalid request body" });
14014
+ }
14015
+ const id = body?.id ?? request.authIdentity;
14016
+ if (!id || id === "service") {
14017
+ return reply.code(200).send({ revoked: false });
14018
+ }
14019
+ const revoked = await deps.store.revoke(id);
14020
+ return reply.code(200).send({ revoked });
14021
+ });
14022
+ app.get("/v1/auth/verify", async (_request, reply) => {
14023
+ return reply.code(200).send({ ok: true });
14024
+ });
14025
+ app.get("/v1/auth/sessions", async (_request, reply) => {
14026
+ return reply.code(200).send({ sessions: deps.store.list() });
14027
+ });
14028
+ app.delete(
14029
+ "/v1/auth/sessions/:id",
14030
+ async (request, reply) => {
14031
+ const id = request.params.id;
14032
+ const revoked = await deps.store.revoke(id);
14033
+ if (!revoked) {
14034
+ return reply.code(404).send({ error: "Not found" });
14035
+ }
14036
+ return reply.code(204).send();
14037
+ }
14038
+ );
14039
+ }
14040
+ function remoteIp(request) {
14041
+ return request.ip || "unknown";
14042
+ }
14043
+
12628
14044
  // src/daemon/acp-ws.ts
12629
14045
  init_connection();
12630
14046
  init_ws_stream();
@@ -12632,12 +14048,12 @@ init_types();
12632
14048
  import { nanoid as nanoid2 } from "nanoid";
12633
14049
  init_hydra_version();
12634
14050
  function registerAcpWsEndpoint(app, deps) {
12635
- app.get("/acp", { websocket: true }, (socket, request) => {
14051
+ app.get("/acp", { websocket: true }, async (socket, request) => {
12636
14052
  const token = tokenFromUpgradeRequest({
12637
14053
  headers: request.headers,
12638
14054
  url: request.url
12639
14055
  });
12640
- if (!token || !constantTimeEqual(token, deps.config.daemon.authToken)) {
14056
+ if (!token || !await deps.validator.validate(token)) {
12641
14057
  socket.close(4401, "Unauthorized");
12642
14058
  return;
12643
14059
  }
@@ -12688,8 +14104,15 @@ function registerAcpWsEndpoint(app, deps) {
12688
14104
  }
12689
14105
  })();
12690
14106
  });
14107
+ const modesPayload = buildModesPayload(session);
12691
14108
  return {
12692
14109
  sessionId: session.sessionId,
14110
+ // session/new is implicitly an attach; mirror session/attach's
14111
+ // shape by including the clientId so deferred-echo clients
14112
+ // (TUI's queue work) can recognize their own prompt_queue_added
14113
+ // events without an extra round-trip.
14114
+ clientId: client.clientId,
14115
+ ...modesPayload ? { modes: modesPayload } : {},
12693
14116
  _meta: buildResponseMeta(session)
12694
14117
  };
12695
14118
  });
@@ -12750,6 +14173,7 @@ function registerAcpWsEndpoint(app, deps) {
12750
14173
  await connection.notify(note.method, note.params);
12751
14174
  }
12752
14175
  session.replayPendingPermissions(client);
14176
+ const modesPayload = buildModesPayload(session);
12753
14177
  return {
12754
14178
  sessionId: session.sessionId,
12755
14179
  clientId: client.clientId,
@@ -12760,6 +14184,7 @@ function registerAcpWsEndpoint(app, deps) {
12760
14184
  // ran, not what was asked for.
12761
14185
  historyPolicy: appliedPolicy,
12762
14186
  replayed: replay.length,
14187
+ ...modesPayload ? { modes: modesPayload } : {},
12763
14188
  _meta: buildResponseMeta(session)
12764
14189
  };
12765
14190
  });
@@ -12793,7 +14218,29 @@ function registerAcpWsEndpoint(app, deps) {
12793
14218
  err.code = JsonRpcErrorCodes.SessionNotFound;
12794
14219
  throw err;
12795
14220
  }
12796
- const session = deps.manager.require(params.sessionId);
14221
+ let session = deps.manager.get(params.sessionId);
14222
+ if (!session) {
14223
+ const fromDisk = await deps.manager.loadFromDisk(params.sessionId);
14224
+ if (!fromDisk) {
14225
+ const err = new Error(
14226
+ `session ${params.sessionId} not found`
14227
+ );
14228
+ err.code = JsonRpcErrorCodes.SessionNotFound;
14229
+ throw err;
14230
+ }
14231
+ app.log.info(
14232
+ `session/prompt auto-resurrecting cold sessionId=${params.sessionId}`
14233
+ );
14234
+ session = await deps.manager.resurrect(fromDisk);
14235
+ const client = bindClientToSession(
14236
+ connection,
14237
+ session,
14238
+ state,
14239
+ void 0,
14240
+ att.clientId
14241
+ );
14242
+ await session.attach(client, "none");
14243
+ }
12797
14244
  return session.prompt(att.clientId, params);
12798
14245
  });
12799
14246
  const handleCancelParams = (raw) => {
@@ -12825,6 +14272,26 @@ function registerAcpWsEndpoint(app, deps) {
12825
14272
  handleCancelParams(raw);
12826
14273
  return null;
12827
14274
  });
14275
+ connection.onRequest("hydra-acp/cancel_prompt", async (raw) => {
14276
+ const params = CancelPromptParams.parse(raw);
14277
+ const session = deps.manager.get(params.sessionId);
14278
+ if (!session) {
14279
+ const err = new Error(`session ${params.sessionId} not found`);
14280
+ err.code = JsonRpcErrorCodes.SessionNotFound;
14281
+ throw err;
14282
+ }
14283
+ return session.cancelQueuedPrompt(params.messageId);
14284
+ });
14285
+ connection.onRequest("hydra-acp/update_prompt", async (raw) => {
14286
+ const params = UpdatePromptParams.parse(raw);
14287
+ const session = deps.manager.get(params.sessionId);
14288
+ if (!session) {
14289
+ const err = new Error(`session ${params.sessionId} not found`);
14290
+ err.code = JsonRpcErrorCodes.SessionNotFound;
14291
+ throw err;
14292
+ }
14293
+ return session.updateQueuedPrompt(params.messageId, params.prompt);
14294
+ });
12828
14295
  connection.onRequest("session/load", async (raw) => {
12829
14296
  const rawObj = raw ?? {};
12830
14297
  const rawSessionId = typeof rawObj.sessionId === "string" ? rawObj.sessionId : void 0;
@@ -12856,8 +14323,13 @@ function registerAcpWsEndpoint(app, deps) {
12856
14323
  await connection.notify(note.method, note.params);
12857
14324
  }
12858
14325
  session.replayPendingPermissions(client);
14326
+ const modesPayload = buildModesPayload(session);
12859
14327
  return {
12860
14328
  sessionId: session.sessionId,
14329
+ // Same as session/new: include clientId so the deferred-echo
14330
+ // path in queue-aware clients can recognize own broadcasts.
14331
+ clientId: client.clientId,
14332
+ ...modesPayload ? { modes: modesPayload } : {},
12861
14333
  _meta: buildResponseMeta(session)
12862
14334
  };
12863
14335
  });
@@ -12883,6 +14355,26 @@ function registerAcpWsEndpoint(app, deps) {
12883
14355
  });
12884
14356
  });
12885
14357
  }
14358
+ function buildModesPayload(session) {
14359
+ const modes = session.availableModes();
14360
+ if (modes.length === 0) {
14361
+ return void 0;
14362
+ }
14363
+ const availableModes = modes.map((m) => {
14364
+ const out = {
14365
+ id: m.id,
14366
+ // ACP spec requires `name` — fall back to id when the agent didn't
14367
+ // supply one so we never emit an invalid SessionMode.
14368
+ name: m.name ?? m.id
14369
+ };
14370
+ if (m.description !== void 0) {
14371
+ out.description = m.description;
14372
+ }
14373
+ return out;
14374
+ });
14375
+ const currentModeId = session.currentMode ?? modes[0].id;
14376
+ return { currentModeId, availableModes };
14377
+ }
12886
14378
  function buildResponseMeta(session) {
12887
14379
  const ours = {
12888
14380
  upstreamSessionId: session.upstreamSessionId,
@@ -12908,9 +14400,17 @@ function buildResponseMeta(session) {
12908
14400
  if (commands.length > 0) {
12909
14401
  ours.availableCommands = commands;
12910
14402
  }
14403
+ const modes = session.availableModes();
14404
+ if (modes.length > 0) {
14405
+ ours.availableModes = modes;
14406
+ }
12911
14407
  if (session.turnStartedAt !== void 0) {
12912
14408
  ours.turnStartedAt = session.turnStartedAt;
12913
14409
  }
14410
+ const queue = session.queueSnapshot();
14411
+ if (queue.length > 0) {
14412
+ ours.queue = queue;
14413
+ }
12914
14414
  return mergeMeta(session.agentMeta, ours);
12915
14415
  }
12916
14416
  function buildInitializeResult() {
@@ -12941,7 +14441,13 @@ function buildInitializeResult() {
12941
14441
  id: "bearer-token",
12942
14442
  description: "Bearer token presented at WS upgrade"
12943
14443
  }
12944
- ]
14444
+ ],
14445
+ // Advertise hydra-only capabilities via _meta["hydra-acp"]. Generic
14446
+ // ACP clients ignore the field; capability-aware clients learn here
14447
+ // that hydra accepts concurrent session/prompt requests and emits
14448
+ // prompt_queue_* notifications so they can stop running their own
14449
+ // local queue.
14450
+ _meta: mergeMeta(void 0, { promptQueueing: true })
12945
14451
  };
12946
14452
  }
12947
14453
  function bindClientToSession(connection, session, state, clientInfo, callerClientId) {
@@ -12955,7 +14461,7 @@ function bindClientToSession(connection, session, state, clientInfo, callerClien
12955
14461
  }
12956
14462
 
12957
14463
  // src/daemon/server.ts
12958
- async function startDaemon(config) {
14464
+ async function startDaemon(config, serviceToken) {
12959
14465
  ensureLoopbackOrTls(config);
12960
14466
  const httpsOptions = config.daemon.tls ? {
12961
14467
  key: await fsp4.readFile(config.daemon.tls.key),
@@ -12982,7 +14488,13 @@ async function startDaemon(config) {
12982
14488
  setNpmInstallLogger((msg) => {
12983
14489
  app.log.info(msg);
12984
14490
  });
12985
- const auth = bearerAuth({ config });
14491
+ const sessionTokenStore = await SessionTokenStore.load();
14492
+ const authRateLimiter = new AuthRateLimiter();
14493
+ const validator = new CompositeTokenValidator([
14494
+ new StaticTokenValidator(serviceToken),
14495
+ new SessionTokenValidator(sessionTokenStore)
14496
+ ]);
14497
+ const auth = bearerAuth({ validator });
12986
14498
  app.addHook("onRequest", async (request, reply) => {
12987
14499
  if (request.routeOptions.config?.skipAuth) {
12988
14500
  return;
@@ -12992,6 +14504,13 @@ async function startDaemon(config) {
12992
14504
  }
12993
14505
  await auth(request, reply);
12994
14506
  });
14507
+ const sweepInterval = setInterval(
14508
+ () => {
14509
+ sessionTokenStore.sweepExpired();
14510
+ },
14511
+ 5 * 60 * 1e3
14512
+ );
14513
+ sweepInterval.unref();
12995
14514
  const registry = new Registry(config);
12996
14515
  const agentLogger = {
12997
14516
  info: (msg) => app.log.info(msg),
@@ -13006,7 +14525,8 @@ async function startDaemon(config) {
13006
14525
  idleTimeoutMs: config.daemon.sessionIdleTimeoutSeconds * 1e3,
13007
14526
  defaultModels: config.defaultModels,
13008
14527
  sessionHistoryMaxEntries: config.daemon.sessionHistoryMaxEntries,
13009
- logger: agentLogger
14528
+ logger: agentLogger,
14529
+ npmRegistry: config.npmRegistry
13010
14530
  });
13011
14531
  const extensions = new ExtensionManager(extensionList(config));
13012
14532
  registerHealthRoutes(app, HYDRA_VERSION);
@@ -13020,8 +14540,12 @@ async function startDaemon(config) {
13020
14540
  defaultAgent: config.defaultAgent,
13021
14541
  defaultCwd: config.defaultCwd
13022
14542
  });
14543
+ registerAuthRoutes(app, {
14544
+ store: sessionTokenStore,
14545
+ rateLimiter: authRateLimiter
14546
+ });
13023
14547
  registerAcpWsEndpoint(app, {
13024
- config,
14548
+ validator,
13025
14549
  manager,
13026
14550
  defaultAgent: config.defaultAgent
13027
14551
  });
@@ -13045,12 +14569,19 @@ async function startDaemon(config) {
13045
14569
  daemonUrl: `${scheme}://${config.daemon.host}:${boundPort}`,
13046
14570
  daemonHost: config.daemon.host,
13047
14571
  daemonPort: boundPort,
13048
- daemonToken: config.daemon.authToken,
14572
+ serviceToken,
13049
14573
  daemonWsUrl: `${wsScheme}://${config.daemon.host}:${boundPort}/acp`,
13050
14574
  hydraHome: paths.home()
13051
14575
  });
13052
14576
  await extensions.start();
14577
+ void manager.resurrectPendingQueues().catch((err) => {
14578
+ app.log.warn(
14579
+ `queue replay scan failed: ${err.message}`
14580
+ );
14581
+ });
13053
14582
  const shutdown = async () => {
14583
+ clearInterval(sweepInterval);
14584
+ await sessionTokenStore.flush();
13054
14585
  await extensions.stop();
13055
14586
  await manager.closeAll();
13056
14587
  await manager.flushMetaWrites();
@@ -13058,7 +14589,7 @@ async function startDaemon(config) {
13058
14589
  setNpmInstallLogger(null);
13059
14590
  await app.close();
13060
14591
  try {
13061
- fs11.unlinkSync(paths.pidFile());
14592
+ fs15.unlinkSync(paths.pidFile());
13062
14593
  } catch {
13063
14594
  }
13064
14595
  try {
@@ -13097,7 +14628,7 @@ function ensureLoopbackOrTls(config) {
13097
14628
  init_daemon_bootstrap();
13098
14629
 
13099
14630
  // src/cli/commands/log-tail.ts
13100
- import * as fs12 from "fs";
14631
+ import * as fs16 from "fs";
13101
14632
  import * as fsp5 from "fs/promises";
13102
14633
  async function runLogTail(logPath, argv, notFoundMessage) {
13103
14634
  const opts = parseLogTailFlags(argv);
@@ -13121,7 +14652,7 @@ async function runLogTail(logPath, argv, notFoundMessage) {
13121
14652
  process.stdout.write(`-- following ${logPath} --
13122
14653
  `);
13123
14654
  let pending = false;
13124
- const watcher = fs12.watch(logPath, () => {
14655
+ const watcher = fs16.watch(logPath, () => {
13125
14656
  if (pending) {
13126
14657
  return;
13127
14658
  }
@@ -13221,7 +14752,8 @@ function parseLogTailFlags(argv) {
13221
14752
 
13222
14753
  // src/cli/commands/daemon.ts
13223
14754
  async function runDaemonStart(flags = {}) {
13224
- const config = await ensureConfig();
14755
+ const config = await loadConfig();
14756
+ const serviceToken = await ensureServiceToken();
13225
14757
  if (await pingHealth(config)) {
13226
14758
  const info2 = await readPidFile();
13227
14759
  process.stdout.write(
@@ -13231,7 +14763,7 @@ async function runDaemonStart(flags = {}) {
13231
14763
  return;
13232
14764
  }
13233
14765
  if (flagBool(flags, "foreground")) {
13234
- const handle = await startDaemon(config);
14766
+ const handle = await startDaemon(config, serviceToken);
13235
14767
  process.stdout.write(
13236
14768
  `hydra-acp daemon listening on ${config.daemon.host}:${config.daemon.port}
13237
14769
  `
@@ -13269,7 +14801,8 @@ async function runDaemonStop() {
13269
14801
  }
13270
14802
  }
13271
14803
  async function runDaemonRestart() {
13272
- const config = await ensureConfig();
14804
+ const config = await loadConfig();
14805
+ await ensureServiceToken();
13273
14806
  const info = await readPidFile();
13274
14807
  if (info && isProcessAlive(info.pid)) {
13275
14808
  process.stdout.write(`Stopping daemon pid ${info.pid}...
@@ -13357,16 +14890,18 @@ init_sessions();
13357
14890
 
13358
14891
  // src/cli/commands/extensions.ts
13359
14892
  init_config();
14893
+ init_service_token();
13360
14894
  init_paths();
13361
14895
  import * as fsp7 from "fs/promises";
13362
14896
  init_sessions();
13363
14897
  async function runExtensionsList() {
13364
14898
  const config = await loadConfig();
14899
+ const serviceToken = await loadServiceToken();
13365
14900
  const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
13366
14901
  let body;
13367
14902
  try {
13368
14903
  const r = await fetch(`${baseUrl}/v1/extensions`, {
13369
- headers: { Authorization: `Bearer ${config.daemon.authToken}` }
14904
+ headers: { Authorization: `Bearer ${serviceToken}` }
13370
14905
  });
13371
14906
  if (!r.ok) {
13372
14907
  process.stderr.write(`Daemon returned HTTP ${r.status}
@@ -13469,13 +15004,14 @@ async function runExtensionsAdd(name, argv) {
13469
15004
  process.stdout.write(`Added extension '${name}' to ${paths.config()}
13470
15005
  `);
13471
15006
  const config = await loadConfig();
15007
+ const serviceToken = await loadServiceToken();
13472
15008
  const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
13473
15009
  const registerBody = { name, ...body };
13474
15010
  try {
13475
15011
  const r = await fetch(`${baseUrl}/v1/extensions`, {
13476
15012
  method: "POST",
13477
15013
  headers: {
13478
- Authorization: `Bearer ${config.daemon.authToken}`,
15014
+ Authorization: `Bearer ${serviceToken}`,
13479
15015
  "Content-Type": "application/json"
13480
15016
  },
13481
15017
  body: JSON.stringify(registerBody)
@@ -13523,11 +15059,12 @@ async function runExtensionsRemove(name) {
13523
15059
  process.stdout.write(`Removed extension '${name}' from ${paths.config()}
13524
15060
  `);
13525
15061
  const config = await loadConfig();
15062
+ const serviceToken = await loadServiceToken();
13526
15063
  const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
13527
15064
  try {
13528
15065
  const r = await fetch(`${baseUrl}/v1/extensions/${encodeURIComponent(name)}`, {
13529
15066
  method: "DELETE",
13530
- headers: { Authorization: `Bearer ${config.daemon.authToken}` }
15067
+ headers: { Authorization: `Bearer ${serviceToken}` }
13531
15068
  });
13532
15069
  if (r.status === 204 || r.status === 404) {
13533
15070
  process.stdout.write(`${name}: stopped
@@ -13587,12 +15124,13 @@ async function postLifecycle(name, verb) {
13587
15124
  return;
13588
15125
  }
13589
15126
  const config = await loadConfig();
15127
+ const serviceToken = await loadServiceToken();
13590
15128
  const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
13591
15129
  let r;
13592
15130
  try {
13593
15131
  r = await fetch(`${baseUrl}/v1/extensions/${encodeURIComponent(name)}/${verb}`, {
13594
15132
  method: "POST",
13595
- headers: { Authorization: `Bearer ${config.daemon.authToken}` }
15133
+ headers: { Authorization: `Bearer ${serviceToken}` }
13596
15134
  });
13597
15135
  } catch (err) {
13598
15136
  process.stderr.write(
@@ -13623,8 +15161,9 @@ async function postLifecycle(name, verb) {
13623
15161
  }
13624
15162
  async function postLifecycleAll(verb) {
13625
15163
  const config = await loadConfig();
15164
+ const serviceToken = await loadServiceToken();
13626
15165
  const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
13627
- const auth = { Authorization: `Bearer ${config.daemon.authToken}` };
15166
+ const auth = { Authorization: `Bearer ${serviceToken}` };
13628
15167
  let listBody;
13629
15168
  try {
13630
15169
  const r = await fetch(`${baseUrl}/v1/extensions`, { headers: auth });
@@ -13790,14 +15329,16 @@ function maxLen2(headerCell, values) {
13790
15329
 
13791
15330
  // src/cli/commands/agents.ts
13792
15331
  init_config();
15332
+ init_service_token();
13793
15333
  init_sessions();
13794
15334
  async function runAgentsList() {
13795
15335
  const config = await loadConfig();
15336
+ const serviceToken = await loadServiceToken();
13796
15337
  const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
13797
15338
  let body;
13798
15339
  try {
13799
15340
  const r = await fetch(`${baseUrl}/v1/agents`, {
13800
- headers: { Authorization: `Bearer ${config.daemon.authToken}` }
15341
+ headers: { Authorization: `Bearer ${serviceToken}` }
13801
15342
  });
13802
15343
  if (!r.ok) {
13803
15344
  process.stderr.write(`Daemon returned HTTP ${r.status}
@@ -13854,12 +15395,13 @@ Registry version: ${body.version}
13854
15395
  }
13855
15396
  async function runAgentsRefresh() {
13856
15397
  const config = await loadConfig();
15398
+ const serviceToken = await loadServiceToken();
13857
15399
  const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
13858
15400
  let body;
13859
15401
  try {
13860
15402
  const r = await fetch(`${baseUrl}/v1/registry/refresh`, {
13861
15403
  method: "POST",
13862
- headers: { Authorization: `Bearer ${config.daemon.authToken}` }
15404
+ headers: { Authorization: `Bearer ${serviceToken}` }
13863
15405
  });
13864
15406
  if (!r.ok) {
13865
15407
  process.stderr.write(`Daemon returned HTTP ${r.status}
@@ -13890,8 +15432,197 @@ function maxLen3(headerCell, values) {
13890
15432
  return max;
13891
15433
  }
13892
15434
 
15435
+ // src/cli/commands/auth.ts
15436
+ init_config();
15437
+ init_service_token();
15438
+ init_sessions();
15439
+ async function promptPassword(prompt) {
15440
+ process.stdout.write(prompt);
15441
+ if (!process.stdin.isTTY) {
15442
+ return readLineFromStdin();
15443
+ }
15444
+ return new Promise((resolve5, reject) => {
15445
+ const stdin = process.stdin;
15446
+ const wasRaw = stdin.isRaw === true;
15447
+ let buffer = "";
15448
+ const cleanup = () => {
15449
+ stdin.removeListener("data", onData);
15450
+ stdin.removeListener("error", onError);
15451
+ if (!wasRaw) {
15452
+ stdin.setRawMode(false);
15453
+ }
15454
+ stdin.pause();
15455
+ };
15456
+ const onData = (chunk) => {
15457
+ for (const byte of chunk) {
15458
+ if (byte === 10 || byte === 13) {
15459
+ process.stdout.write("\n");
15460
+ cleanup();
15461
+ resolve5(buffer);
15462
+ return;
15463
+ }
15464
+ if (byte === 3) {
15465
+ cleanup();
15466
+ reject(new Error("password entry cancelled"));
15467
+ return;
15468
+ }
15469
+ if (byte === 127 || byte === 8) {
15470
+ buffer = buffer.slice(0, -1);
15471
+ continue;
15472
+ }
15473
+ buffer += String.fromCharCode(byte);
15474
+ }
15475
+ };
15476
+ const onError = (err) => {
15477
+ cleanup();
15478
+ reject(err);
15479
+ };
15480
+ stdin.setRawMode(true);
15481
+ stdin.resume();
15482
+ stdin.on("data", onData);
15483
+ stdin.on("error", onError);
15484
+ });
15485
+ }
15486
+ function readLineFromStdin() {
15487
+ return new Promise((resolve5, reject) => {
15488
+ let buffer = "";
15489
+ process.stdin.setEncoding("utf8");
15490
+ const onData = (chunk) => {
15491
+ buffer += chunk;
15492
+ const nl = buffer.indexOf("\n");
15493
+ if (nl !== -1) {
15494
+ process.stdin.removeListener("data", onData);
15495
+ process.stdin.removeListener("error", onError);
15496
+ resolve5(buffer.slice(0, nl).replace(/\r$/, ""));
15497
+ }
15498
+ };
15499
+ const onError = (err) => {
15500
+ process.stdin.removeListener("data", onData);
15501
+ process.stdin.removeListener("error", onError);
15502
+ reject(err);
15503
+ };
15504
+ process.stdin.on("data", onData);
15505
+ process.stdin.on("error", onError);
15506
+ });
15507
+ }
15508
+ async function runAuthPasswordSet(flags) {
15509
+ const force = flagBool(flags, "force");
15510
+ if (await hasPassword() && !force) {
15511
+ const current = await promptPassword("Current password: ");
15512
+ if (!await verifyPassword(current)) {
15513
+ process.stderr.write("Wrong password.\n");
15514
+ process.exit(1);
15515
+ }
15516
+ }
15517
+ const next = await promptPassword("New password: ");
15518
+ if (next.length === 0) {
15519
+ process.stderr.write("Password must not be empty.\n");
15520
+ process.exit(2);
15521
+ }
15522
+ const confirm = await promptPassword("Confirm new password: ");
15523
+ if (next !== confirm) {
15524
+ process.stderr.write("Passwords did not match.\n");
15525
+ process.exit(1);
15526
+ }
15527
+ await setPassword(next);
15528
+ process.stdout.write("Password set.\n");
15529
+ }
15530
+ async function runAuthList() {
15531
+ const config = await loadConfig();
15532
+ const token = await loadServiceToken();
15533
+ const baseUrl = httpBase(
15534
+ config.daemon.host,
15535
+ config.daemon.port,
15536
+ !!config.daemon.tls
15537
+ );
15538
+ const r = await fetch(`${baseUrl}/v1/auth/sessions`, {
15539
+ headers: { Authorization: `Bearer ${token}` }
15540
+ });
15541
+ if (!r.ok) {
15542
+ process.stderr.write(`Daemon returned HTTP ${r.status}
15543
+ `);
15544
+ process.exit(1);
15545
+ }
15546
+ const body = await r.json();
15547
+ if (body.sessions.length === 0) {
15548
+ process.stdout.write("No active session tokens.\n");
15549
+ return;
15550
+ }
15551
+ const header = {
15552
+ id: "ID",
15553
+ label: "LABEL",
15554
+ createdAt: "CREATED",
15555
+ expiresAt: "EXPIRES",
15556
+ lastUsedAt: "LAST USED"
15557
+ };
15558
+ const rows = body.sessions.map((s) => ({
15559
+ id: s.id,
15560
+ label: s.label ?? "-",
15561
+ createdAt: s.createdAt,
15562
+ expiresAt: s.expiresAt,
15563
+ lastUsedAt: s.lastUsedAt
15564
+ }));
15565
+ const widths = {
15566
+ id: maxLen4(header.id, rows.map((r2) => r2.id)),
15567
+ label: maxLen4(header.label, rows.map((r2) => r2.label)),
15568
+ createdAt: maxLen4(header.createdAt, rows.map((r2) => r2.createdAt)),
15569
+ expiresAt: maxLen4(header.expiresAt, rows.map((r2) => r2.expiresAt))
15570
+ };
15571
+ const fmt = (r2) => [
15572
+ r2.id.padEnd(widths.id),
15573
+ r2.label.padEnd(widths.label),
15574
+ r2.createdAt.padEnd(widths.createdAt),
15575
+ r2.expiresAt.padEnd(widths.expiresAt),
15576
+ r2.lastUsedAt
15577
+ ].join(" ");
15578
+ process.stdout.write(fmt(header) + "\n");
15579
+ for (const r2 of rows) {
15580
+ process.stdout.write(fmt(r2) + "\n");
15581
+ }
15582
+ }
15583
+ async function runAuthRevoke(id) {
15584
+ if (!id) {
15585
+ process.stderr.write("Usage: hydra-acp auth revoke <id>\n");
15586
+ process.exit(2);
15587
+ }
15588
+ const config = await loadConfig();
15589
+ const token = await loadServiceToken();
15590
+ const baseUrl = httpBase(
15591
+ config.daemon.host,
15592
+ config.daemon.port,
15593
+ !!config.daemon.tls
15594
+ );
15595
+ const r = await fetch(`${baseUrl}/v1/auth/sessions/${id}`, {
15596
+ method: "DELETE",
15597
+ headers: { Authorization: `Bearer ${token}` }
15598
+ });
15599
+ if (r.status === 204) {
15600
+ process.stdout.write(`Revoked ${id}
15601
+ `);
15602
+ return;
15603
+ }
15604
+ if (r.status === 404) {
15605
+ process.stderr.write(`No session token with id ${id}
15606
+ `);
15607
+ process.exit(1);
15608
+ }
15609
+ process.stderr.write(`Daemon returned HTTP ${r.status}
15610
+ `);
15611
+ process.exit(1);
15612
+ }
15613
+ function maxLen4(headerCell, values) {
15614
+ let max = headerCell.length;
15615
+ for (const v of values) {
15616
+ if (v.length > max) {
15617
+ max = v.length;
15618
+ }
15619
+ }
15620
+ return max;
15621
+ }
15622
+
13893
15623
  // src/shim/proxy.ts
13894
15624
  init_config();
15625
+ init_service_token();
13895
15626
  init_daemon_bootstrap();
13896
15627
  init_resilient_ws();
13897
15628
 
@@ -14066,13 +15797,14 @@ function isResponse2(msg) {
14066
15797
 
14067
15798
  // src/shim/proxy.ts
14068
15799
  async function runShim(opts) {
14069
- const config = await ensureConfig();
15800
+ const config = await loadConfig();
15801
+ const serviceToken = await ensureServiceToken();
14070
15802
  await ensureDaemonReachable(config);
14071
15803
  const tracker = new SessionTracker();
14072
15804
  const downstream = ndjsonStreamFromStdio(process.stdin, process.stdout);
14073
15805
  const protocol = config.daemon.tls ? "wss" : "ws";
14074
15806
  const url = `${protocol}://${config.daemon.host}:${config.daemon.port}/acp`;
14075
- const subprotocols = ["acp.v1", `hydra-acp-token.${config.daemon.authToken}`];
15807
+ const subprotocols = ["acp.v1", `hydra-acp-token.${serviceToken}`];
14076
15808
  const upstream = new ResilientWsStream({
14077
15809
  url,
14078
15810
  subprotocols,
@@ -14413,6 +16145,7 @@ async function main() {
14413
16145
  process.exit(2);
14414
16146
  return;
14415
16147
  }
16148
+ case "session":
14416
16149
  case "sessions": {
14417
16150
  const sub = positional[1];
14418
16151
  if (sub === void 0 || sub === "list") {
@@ -14449,13 +16182,14 @@ async function main() {
14449
16182
  });
14450
16183
  return;
14451
16184
  }
14452
- process.stderr.write(`Unknown sessions subcommand: ${sub}
16185
+ process.stderr.write(`Unknown session subcommand: ${sub}
14453
16186
  `);
14454
16187
  process.exit(2);
14455
16188
  return;
14456
16189
  }
16190
+ case "extension":
14457
16191
  case "extensions": {
14458
- const extIdx = argv.indexOf("extensions");
16192
+ const extIdx = argv.indexOf(subcommand);
14459
16193
  const tail = argv.slice(extIdx + 1);
14460
16194
  const sub = tail[0];
14461
16195
  const name2 = tail[1];
@@ -14488,11 +16222,12 @@ async function main() {
14488
16222
  await runExtensionsLogs(name2, rest);
14489
16223
  return;
14490
16224
  }
14491
- process.stderr.write(`Unknown extensions subcommand: ${sub}
16225
+ process.stderr.write(`Unknown extension subcommand: ${sub}
14492
16226
  `);
14493
16227
  process.exit(2);
14494
16228
  return;
14495
16229
  }
16230
+ case "agent":
14496
16231
  case "agents": {
14497
16232
  const sub = positional[1];
14498
16233
  if (sub === void 0 || sub === "list") {
@@ -14503,7 +16238,33 @@ async function main() {
14503
16238
  await runAgentsRefresh();
14504
16239
  return;
14505
16240
  }
14506
- process.stderr.write(`Unknown agents subcommand: ${sub}
16241
+ process.stderr.write(`Unknown agent subcommand: ${sub}
16242
+ `);
16243
+ process.exit(2);
16244
+ return;
16245
+ }
16246
+ case "auth": {
16247
+ const sub = positional[1];
16248
+ if (sub === "password") {
16249
+ const action = positional[2];
16250
+ if (action === void 0 || action === "set") {
16251
+ await runAuthPasswordSet(flags);
16252
+ return;
16253
+ }
16254
+ process.stderr.write(`Unknown auth password action: ${action}
16255
+ `);
16256
+ process.exit(2);
16257
+ return;
16258
+ }
16259
+ if (sub === void 0 || sub === "list") {
16260
+ await runAuthList();
16261
+ return;
16262
+ }
16263
+ if (sub === "revoke") {
16264
+ await runAuthRevoke(positional[2]);
16265
+ return;
16266
+ }
16267
+ process.stderr.write(`Unknown auth subcommand: ${sub}
14507
16268
  `);
14508
16269
  process.exit(2);
14509
16270
  return;
@@ -14583,23 +16344,26 @@ function printHelp() {
14583
16344
  " hydra-acp daemon start [--foreground] Start daemon (detached by default; --foreground to attach)",
14584
16345
  " hydra-acp daemon stop|restart|status",
14585
16346
  " hydra-acp daemon logs [-f] [-n N] Tail or follow the daemon log",
14586
- " hydra-acp sessions [list] [--all] [--json]",
16347
+ " hydra-acp session [list] [--all] [--json]",
14587
16348
  " List sessions (live + 20 most-recent cold; --all for everything; --json emits the raw daemon response as JSON for scripts)",
14588
- " hydra-acp sessions kill <id> Demote a live session to cold (keeps the on-disk record)",
14589
- " hydra-acp sessions remove <id> Remove a session entirely (live or cold)",
14590
- " hydra-acp sessions export <id> [--out <file>|.]",
16349
+ " hydra-acp session kill <id> Demote a live session to cold (keeps the on-disk record)",
16350
+ " hydra-acp session remove <id> Remove a session entirely (live or cold)",
16351
+ " hydra-acp session export <id> [--out <file>|.]",
14591
16352
  " Write a session bundle to <file>, to a default-named file when --out=., or to stdout",
14592
- " hydra-acp sessions transcript <id>|<file> [--out <file>|.]",
16353
+ " hydra-acp session transcript <id>|<file> [--out <file>|.]",
14593
16354
  " Render a session as a markdown transcript. Accepts a session id (renders via the daemon) or a local .hydra bundle file (rendered in-process). Writes to <file>, to a default-named file when --out=., or to stdout",
14594
- " hydra-acp sessions import <file>|- [--replace] [--cwd <path>] [--info]",
16355
+ " hydra-acp session import <file>|- [--replace] [--cwd <path>] [--info]",
14595
16356
  " Import a bundle from <file> or stdin (-); --replace overwrites a lineage match (kills it if live); --cwd overrides the bundle's recorded working directory; --info prints the bundle's meta without importing",
14596
- " hydra-acp extensions list List configured extensions and live state",
14597
- " hydra-acp extensions add <name> [opts] Add an extension to config",
14598
- " hydra-acp extensions remove <name> Remove an extension from config",
14599
- " hydra-acp extensions start|stop|restart <n>|all Lifecycle on one or all",
14600
- " hydra-acp extensions logs <name> [-f] [-n N]Tail or follow an extension's log",
14601
- " hydra-acp agents [list] List agents in the cached registry",
14602
- " hydra-acp agents refresh Force a registry re-fetch",
16357
+ " hydra-acp extension list List configured extensions and live state",
16358
+ " hydra-acp extension add <name> [opts] Add an extension to config",
16359
+ " hydra-acp extension remove <name> Remove an extension from config",
16360
+ " hydra-acp extension start|stop|restart <n>|all Lifecycle on one or all",
16361
+ " hydra-acp extension logs <name> [-f] [-n N] Tail or follow an extension's log",
16362
+ " hydra-acp agent [list] List agents in the cached registry",
16363
+ " hydra-acp agent refresh Force a registry re-fetch",
16364
+ " hydra-acp auth password [--force] Set the daemon's master password",
16365
+ " hydra-acp auth [list] List active session tokens",
16366
+ " hydra-acp auth revoke <id> Revoke a session token",
14603
16367
  " hydra-acp tui flags: [--resume <id>] [--reattach] [--new] [--agent <id>] [--model <id>] [--cwd <path>] [--name <label>]",
14604
16368
  " --resume <id> attaches to a specific session; --reattach picks the most-recent in cwd.",
14605
16369
  " Smart default (no flags): shows a picker when sessions exist, else new.",