@hydra-acp/cli 0.1.21 → 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
2197
2647
  };
2198
- this.promptQueue.push(run3);
2648
+ this.promptQueue.push(entry);
2199
2649
  void this.drainQueue();
2200
2650
  });
2201
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
2675
+ };
2676
+ this.promptQueue.push(entry);
2677
+ this.persistRewrite();
2678
+ this.broadcastQueueAdded(entry);
2679
+ void this.drainQueue();
2680
+ });
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 [];
@@ -8269,11 +8889,12 @@ function toolIconStyle(status) {
8269
8889
  }
8270
8890
  }
8271
8891
  function formatPlan(event) {
8892
+ const stopped = event.stopped === true;
8272
8893
  if (event.entries.length === 0) {
8273
8894
  return [
8274
8895
  {
8275
8896
  prefix: "\u25A3 ",
8276
- prefixStyle: "plan",
8897
+ prefixStyle: stopped ? "tool-status-fail" : "plan",
8277
8898
  body: "(empty plan)",
8278
8899
  bodyStyle: "dim"
8279
8900
  }
@@ -8282,7 +8903,7 @@ function formatPlan(event) {
8282
8903
  const allComplete = event.entries.every(
8283
8904
  (e) => (e.status ?? "pending") === "completed"
8284
8905
  );
8285
- const headerStyle = allComplete ? "plan-done" : "plan";
8906
+ const headerStyle = allComplete ? "plan-done" : stopped ? "tool-status-fail" : "plan";
8286
8907
  const lines = [
8287
8908
  {
8288
8909
  prefix: "\u25A3 ",
@@ -8294,7 +8915,7 @@ function formatPlan(event) {
8294
8915
  for (const entry of event.entries) {
8295
8916
  const status = entry.status ?? "pending";
8296
8917
  const marker = status === "completed" ? "[x]" : status === "in_progress" ? "[~]" : "[ ]";
8297
- const style = status === "completed" ? "plan-done" : status === "in_progress" ? "plan" : "plan-pending";
8918
+ const style = status === "completed" ? "plan-done" : status === "in_progress" ? stopped ? "plan-pending" : "plan" : "plan-pending";
8298
8919
  lines.push({
8299
8920
  prefix: " ",
8300
8921
  body: `${marker} ${entry.content}`,
@@ -8359,17 +8980,18 @@ var init_format = __esm({
8359
8980
  import { appendFileSync, statSync, renameSync } from "fs";
8360
8981
  import { nanoid as nanoid3 } from "nanoid";
8361
8982
  import termkit from "terminal-kit";
8362
- import fs15 from "fs/promises";
8363
- import path11 from "path";
8983
+ import fs19 from "fs/promises";
8984
+ import path13 from "path";
8364
8985
  async function runTuiApp(opts) {
8365
- const config = await ensureConfig();
8986
+ const config = await loadConfig();
8987
+ const serviceToken = await ensureServiceToken();
8366
8988
  logMaxBytes = config.tui.logMaxBytes;
8367
8989
  await ensureDaemonReachable(config);
8368
8990
  const term = termkit.terminal;
8369
8991
  const exitHint = {};
8370
8992
  let nextOpts = opts;
8371
8993
  while (nextOpts !== null) {
8372
- nextOpts = await runSession(term, config, nextOpts, exitHint);
8994
+ nextOpts = await runSession(term, config, serviceToken, nextOpts, exitHint);
8373
8995
  }
8374
8996
  const pendingUpdate = await getPendingUpdate();
8375
8997
  if (pendingUpdate) {
@@ -8382,8 +9004,8 @@ async function runTuiApp(opts) {
8382
9004
  `);
8383
9005
  }
8384
9006
  }
8385
- async function runSession(term, config, opts, exitHint) {
8386
- 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);
8387
9009
  if (!ctx) {
8388
9010
  term.grabInput(false);
8389
9011
  process.exit(0);
@@ -8392,7 +9014,7 @@ async function runSession(term, config, opts, exitHint) {
8392
9014
  term.brightYellow(launchLabel)("\n");
8393
9015
  const protocol = config.daemon.tls ? "wss" : "ws";
8394
9016
  const wsUrl = `${protocol}://${config.daemon.host}:${config.daemon.port}/acp`;
8395
- const subprotocols = ["acp.v1", `hydra-acp-token.${config.daemon.authToken}`];
9017
+ const subprotocols = ["acp.v1", `hydra-acp-token.${serviceToken}`];
8396
9018
  let onReconnect = null;
8397
9019
  let onDisconnectHook = null;
8398
9020
  const stream = new ResilientWsStream({
@@ -8461,9 +9083,7 @@ async function runSession(term, config, opts, exitHint) {
8461
9083
  screenRef.setBanner({ status: "ready", elapsedMs: void 0 });
8462
9084
  }
8463
9085
  }
8464
- if (delta < 0) {
8465
- tickWorker();
8466
- }
9086
+ void delta;
8467
9087
  };
8468
9088
  let screenRef = null;
8469
9089
  let dispatcherRef = null;
@@ -8480,23 +9100,91 @@ async function runSession(term, config, opts, exitHint) {
8480
9100
  } else if (event?.kind === "turn-complete") {
8481
9101
  adjustPendingTurns(-1);
8482
9102
  }
8483
- if (rawTag === "permission_resolved") {
8484
- handlePermissionResolved(update);
8485
- 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();
8486
9167
  }
8487
- appendRender(event);
8488
- maybeDismissPermissionByToolUpdate(update);
8489
9168
  });
8490
- conn.onNotification("hydra-acp/session_closed", () => {
8491
- if (teardownStarted) {
8492
- return;
8493
- }
8494
- if (pendingTurns > 0) {
8495
- 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();
8496
9176
  }
8497
- const screenReady = typeof screenRef !== "undefined" && screenRef !== null;
8498
- if (screenReady) {
8499
- 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
+ }
8500
9188
  }
8501
9189
  });
8502
9190
  const handlePermissionResolved = (update) => {
@@ -8626,9 +9314,12 @@ async function runSession(term, config, opts, exitHint) {
8626
9314
  let resolvedAgentId = ctx.agentId;
8627
9315
  let resolvedCwd = ctx.cwd;
8628
9316
  let resolvedTitle;
9317
+ let ownClientId;
8629
9318
  let initialModel;
8630
9319
  let initialMode;
8631
9320
  let initialCommands;
9321
+ let initialModes;
9322
+ let initialQueue;
8632
9323
  let initialUsage;
8633
9324
  let initialTurnStartedAt;
8634
9325
  if (ctx.sessionId === "__new__") {
@@ -8645,6 +9336,9 @@ async function runSession(term, config, opts, exitHint) {
8645
9336
  ...Object.keys(hydraNewMeta).length > 0 ? { _meta: { [HYDRA_META_KEY]: hydraNewMeta } } : {}
8646
9337
  });
8647
9338
  resolvedSessionId = created.sessionId;
9339
+ if (created.clientId) {
9340
+ ownClientId = created.clientId;
9341
+ }
8648
9342
  exitHint.sessionId = resolvedSessionId;
8649
9343
  const hydraMeta = extractHydraMeta(created._meta ?? void 0);
8650
9344
  upstreamSessionId = hydraMeta.upstreamSessionId;
@@ -8664,6 +9358,10 @@ async function runSession(term, config, opts, exitHint) {
8664
9358
  if (hydraMeta.availableCommands) {
8665
9359
  initialCommands = normalizeAdvertisedCommands(hydraMeta.availableCommands);
8666
9360
  }
9361
+ if (hydraMeta.availableModes) {
9362
+ initialModes = hydraMeta.availableModes;
9363
+ }
9364
+ initialQueue = hydraMeta.queue;
8667
9365
  } else {
8668
9366
  const attached = await conn.request("session/attach", {
8669
9367
  sessionId: ctx.sessionId,
@@ -8671,6 +9369,9 @@ async function runSession(term, config, opts, exitHint) {
8671
9369
  clientInfo: { name: "hydra-acp-tui", version: HYDRA_VERSION }
8672
9370
  });
8673
9371
  resolvedSessionId = attached.sessionId;
9372
+ if (attached.clientId) {
9373
+ ownClientId = attached.clientId;
9374
+ }
8674
9375
  exitHint.sessionId = resolvedSessionId;
8675
9376
  const hydraMeta = extractHydraMeta(attached._meta ?? void 0);
8676
9377
  upstreamSessionId = hydraMeta.upstreamSessionId;
@@ -8690,6 +9391,10 @@ async function runSession(term, config, opts, exitHint) {
8690
9391
  if (hydraMeta.availableCommands) {
8691
9392
  initialCommands = normalizeAdvertisedCommands(hydraMeta.availableCommands);
8692
9393
  }
9394
+ if (hydraMeta.availableModes) {
9395
+ initialModes = hydraMeta.availableModes;
9396
+ }
9397
+ initialQueue = hydraMeta.queue;
8693
9398
  }
8694
9399
  const historyFile = paths.tuiHistoryFile(resolvedSessionId);
8695
9400
  let history = await loadHistory(historyFile).catch(() => []);
@@ -8749,6 +9454,7 @@ async function runSession(term, config, opts, exitHint) {
8749
9454
  { name: "/demo-tool", description: "Inject a synthetic tool-call sequence (UI test)" }
8750
9455
  ];
8751
9456
  let agentCommands = initialCommands ?? [];
9457
+ let agentModes = initialModes ?? [];
8752
9458
  const allCommands = () => {
8753
9459
  const seen = /* @__PURE__ */ new Set();
8754
9460
  const out = [];
@@ -8904,7 +9610,7 @@ async function runSession(term, config, opts, exitHint) {
8904
9610
  usage: { ...usage }
8905
9611
  });
8906
9612
  if (initialMode) {
8907
- screen.appendLines(formatEvent({ kind: "mode-changed", mode: initialMode }));
9613
+ screen.setBanner({ currentMode: initialMode });
8908
9614
  }
8909
9615
  void getPendingUpdate().then((info) => {
8910
9616
  if (info) {
@@ -8941,7 +9647,7 @@ async function runSession(term, config, opts, exitHint) {
8941
9647
  }
8942
9648
  let onlyClient = false;
8943
9649
  try {
8944
- const sessions = await listSessions(config);
9650
+ const sessions = await listSessions(config, serviceToken);
8945
9651
  const me = sessions.find((s) => s.sessionId === resolvedSessionId);
8946
9652
  onlyClient = !me || me.attachedClients <= 1;
8947
9653
  } catch {
@@ -9033,11 +9739,12 @@ async function runSession(term, config, opts, exitHint) {
9033
9739
  screen.pauseRepaint();
9034
9740
  screen.stop();
9035
9741
  saveHistory(historyFile, history).catch(() => void 0);
9036
- const sessions = await listSessions(config);
9742
+ const sessions = await listSessions(config, serviceToken);
9037
9743
  const choice = await pickSession(term, {
9038
9744
  cwd: resolvedCwd,
9039
9745
  sessions,
9040
9746
  config,
9747
+ serviceToken,
9041
9748
  currentSessionId: resolvedSessionId
9042
9749
  });
9043
9750
  if (choice.kind === "abort") {
@@ -9065,37 +9772,76 @@ async function runSession(term, config, opts, exitHint) {
9065
9772
  }
9066
9773
  resume(nextOpts);
9067
9774
  };
9068
- 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
+ };
9069
9793
  const handleEffect = (effect) => {
9070
9794
  switch (effect.type) {
9071
9795
  case "send":
9072
- enqueuePrompt(effect.text, effect.planMode, effect.attachments);
9796
+ enqueuePrompt(effect.text, effect.attachments);
9073
9797
  return;
9074
9798
  case "queue-edit": {
9075
- const realIdx = effect.index + queueHeadOffset();
9076
- const existing = promptQueue[realIdx];
9077
- if (existing) {
9078
- promptQueue[realIdx] = {
9079
- text: effect.text,
9080
- planMode: existing.planMode,
9081
- attachments: effect.attachments
9082
- };
9083
- 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 });
9084
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
+ });
9085
9822
  return;
9086
9823
  }
9087
9824
  case "queue-remove": {
9088
- const realIdx = effect.index + queueHeadOffset();
9089
- if (realIdx >= 0 && realIdx < promptQueue.length) {
9090
- promptQueue.splice(realIdx, 1);
9091
- refreshQueueDisplay();
9825
+ const mid = queueMessageIdAt(effect.index);
9826
+ if (!mid) {
9827
+ return;
9092
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
+ });
9093
9840
  return;
9094
9841
  }
9095
9842
  case "cancel": {
9096
9843
  if (effect.prefill && turnInFlight) {
9097
- const headOffset = workerActive ? 1 : 0;
9098
- const waitingEmpty = promptQueue.length <= headOffset;
9844
+ const waitingEmpty = queueCache.size === 0;
9099
9845
  const bufferEmpty = dispatcher.state().buffer.every((line) => line === "");
9100
9846
  if (waitingEmpty && bufferEmpty) {
9101
9847
  pendingPrefill = {
@@ -9115,7 +9861,7 @@ async function runSession(term, config, opts, exitHint) {
9115
9861
  void requestExit();
9116
9862
  return;
9117
9863
  case "plan-toggle":
9118
- screen.setBanner({ planMode: effect.on });
9864
+ void handleModeToggle(effect.on);
9119
9865
  return;
9120
9866
  case "redraw-banner":
9121
9867
  screen.setBanner({});
@@ -9132,6 +9878,9 @@ async function runSession(term, config, opts, exitHint) {
9132
9878
  case "switch-session":
9133
9879
  void switchSession();
9134
9880
  return;
9881
+ case "next-live-session":
9882
+ void cycleLiveSession();
9883
+ return;
9135
9884
  case "toggle-tools":
9136
9885
  toolsExpanded = !toolsExpanded;
9137
9886
  renderToolsBlock();
@@ -9175,11 +9924,11 @@ async function runSession(term, config, opts, exitHint) {
9175
9924
  }
9176
9925
  const mimeType = mimeFromExtension(token);
9177
9926
  if (!mimeType) {
9178
- screen.notify(`unsupported image type: ${path11.basename(token)}`);
9927
+ screen.notify(`unsupported image type: ${path13.basename(token)}`);
9179
9928
  continue;
9180
9929
  }
9181
9930
  try {
9182
- const buf = await fs15.readFile(token);
9931
+ const buf = await fs19.readFile(token);
9183
9932
  if (buf.length > MAX_ATTACHMENT_BYTES) {
9184
9933
  screen.notify(
9185
9934
  `image too large (${formatSize(buf.length)}, max ${formatSize(MAX_ATTACHMENT_BYTES)})`
@@ -9189,13 +9938,13 @@ async function runSession(term, config, opts, exitHint) {
9189
9938
  dispatcher.addAttachment({
9190
9939
  mimeType,
9191
9940
  data: buf.toString("base64"),
9192
- name: path11.basename(token),
9941
+ name: path13.basename(token),
9193
9942
  sizeBytes: buf.length
9194
9943
  });
9195
9944
  added++;
9196
9945
  } catch (err) {
9197
9946
  screen.notify(
9198
- `cannot read ${path11.basename(token)}: ${err.message}`
9947
+ `cannot read ${path13.basename(token)}: ${err.message}`
9199
9948
  );
9200
9949
  }
9201
9950
  }
@@ -9226,18 +9975,53 @@ async function runSession(term, config, opts, exitHint) {
9226
9975
  }
9227
9976
  screen.refreshPrompt();
9228
9977
  };
9229
- const promptQueue = [];
9230
- 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();
9231
10001
  const refreshQueueDisplay = () => {
9232
- const waiting = promptQueue.slice(workerActive ? 1 : 0);
9233
- const displayTexts = waiting.map(
9234
- (p) => p.attachments.length > 0 ? `${p.text} \xB7 \u{1F4CE}\xD7${p.attachments.length}` : p.text
9235
- );
10002
+ const entries = [...queueCache.values()];
10003
+ const displayTexts = entries.map(formatQueueChipText);
9236
10004
  screen.setQueuedPrompts(displayTexts);
9237
- screen.setBanner({ queued: waiting.length });
9238
- 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;
9239
10011
  };
9240
- 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) => {
9241
10025
  screen.scrollToBottom();
9242
10026
  if (handleBuiltinCommand(text)) {
9243
10027
  return;
@@ -9245,15 +10029,29 @@ async function runSession(term, config, opts, exitHint) {
9245
10029
  history = appendEntry(history, text);
9246
10030
  dispatcher.setHistory(history);
9247
10031
  saveHistory(historyFile, history).catch(() => void 0);
9248
- promptQueue.push({ text, planMode, attachments });
9249
- refreshQueueDisplay();
9250
- tickWorker();
10032
+ void runPrompt(text, attachments);
9251
10033
  };
9252
- const tickWorker = () => {
9253
- 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) {
9254
10044
  return;
9255
10045
  }
9256
- 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
+ }
9257
10055
  };
9258
10056
  const handleBuiltinCommand = (text) => {
9259
10057
  const trimmed = text.trim();
@@ -9403,33 +10201,7 @@ async function runSession(term, config, opts, exitHint) {
9403
10201
  return false;
9404
10202
  }
9405
10203
  };
9406
- const runQueueWorker = async () => {
9407
- workerActive = true;
9408
- try {
9409
- while (promptQueue.length > 0 && pendingTurns === 0) {
9410
- const next = promptQueue[0];
9411
- if (!next) {
9412
- break;
9413
- }
9414
- refreshQueueDisplay();
9415
- await processPrompt(next.text, next.planMode, next.attachments);
9416
- promptQueue.shift();
9417
- }
9418
- } finally {
9419
- workerActive = false;
9420
- refreshQueueDisplay();
9421
- if (pendingPrefill !== null) {
9422
- const { text, attachments } = pendingPrefill;
9423
- pendingPrefill = null;
9424
- const bufferEmpty = dispatcher.state().buffer.every((line) => line === "");
9425
- if (bufferEmpty) {
9426
- dispatcher.setBuffer(text, attachments);
9427
- screen.refreshPrompt();
9428
- }
9429
- }
9430
- }
9431
- };
9432
- const processPrompt = async (text, planMode, attachments) => {
10204
+ const runPrompt = async (text, attachments) => {
9433
10205
  const userBlocks = [];
9434
10206
  if (text.length > 0) {
9435
10207
  userBlocks.push({ type: "text", text });
@@ -9437,9 +10209,9 @@ async function runSession(term, config, opts, exitHint) {
9437
10209
  for (const a of attachments) {
9438
10210
  userBlocks.push({ type: "image", data: a.data, mimeType: a.mimeType });
9439
10211
  }
9440
- const promptArr = planMode ? [{ type: "text", text: PLAN_PREFIX_TEXT }, ...userBlocks] : userBlocks;
9441
10212
  adjustPendingTurns(1);
9442
- appendRender({ kind: "user-text", text, attachments });
10213
+ const echo = { text, attachments, flushed: false };
10214
+ pendingEchoes.push(echo);
9443
10215
  let cancelled = false;
9444
10216
  turnInFlight = {
9445
10217
  text,
@@ -9458,23 +10230,44 @@ async function runSession(term, config, opts, exitHint) {
9458
10230
  try {
9459
10231
  const response = await conn.request("session/prompt", {
9460
10232
  sessionId: resolvedSessionId,
9461
- prompt: promptArr
10233
+ prompt: userBlocks
9462
10234
  });
9463
10235
  if (response && typeof response.stopReason === "string") {
9464
10236
  stopReason = response.stopReason;
9465
10237
  }
9466
10238
  } catch (err) {
9467
- appendRender({
9468
- kind: "unknown",
9469
- sessionUpdate: "error",
9470
- raw: { error: err.message }
9471
- });
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
+ ]);
9472
10254
  } finally {
9473
10255
  turnInFlight = null;
9474
10256
  adjustPendingTurns(-1);
9475
- appendRender(
9476
- stopReason !== void 0 ? { kind: "turn-complete", stopReason } : { kind: "turn-complete" }
9477
- );
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
+ }
9478
10271
  }
9479
10272
  };
9480
10273
  const toolStates = /* @__PURE__ */ new Map();
@@ -9483,6 +10276,7 @@ async function runSession(term, config, opts, exitHint) {
9483
10276
  let toolsBlockStartedAt = null;
9484
10277
  let toolsBlockEndedAt = null;
9485
10278
  let toolsBlockStopReason = null;
10279
+ let lastPlanEvent = null;
9486
10280
  const TOOLS_COLLAPSED_LIMIT = 5;
9487
10281
  let agentBuffer = "";
9488
10282
  let agentKey = null;
@@ -9603,6 +10397,14 @@ async function runSession(term, config, opts, exitHint) {
9603
10397
  refreshCompletions();
9604
10398
  return;
9605
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
+ }
9606
10408
  if (event.kind === "session-info") {
9607
10409
  if (event.title !== void 0) {
9608
10410
  screen.setSessionbar({ title: event.title });
@@ -9645,6 +10447,7 @@ async function runSession(term, config, opts, exitHint) {
9645
10447
  }
9646
10448
  screen.clearKey("tools");
9647
10449
  screen.clearKey("plan");
10450
+ lastPlanEvent = null;
9648
10451
  toolStates.clear();
9649
10452
  toolCallOrder.length = 0;
9650
10453
  toolsExpanded = false;
@@ -9670,6 +10473,7 @@ async function runSession(term, config, opts, exitHint) {
9670
10473
  }
9671
10474
  if (event.kind === "plan") {
9672
10475
  closeAgentText();
10476
+ lastPlanEvent = event;
9673
10477
  const lines = formatEvent(event);
9674
10478
  if (lines.length > 0) {
9675
10479
  screen.upsertLines("plan", lines);
@@ -9691,6 +10495,13 @@ async function runSession(term, config, opts, exitHint) {
9691
10495
  }
9692
10496
  if (event.kind === "turn-complete") {
9693
10497
  closeAgentText();
10498
+ if (lastPlanEvent !== null && event.stopReason !== void 0 && event.stopReason !== "end_turn") {
10499
+ const lines = formatEvent({ ...lastPlanEvent, stopped: true });
10500
+ if (lines.length > 0) {
10501
+ screen.upsertLines("plan", lines);
10502
+ }
10503
+ }
10504
+ lastPlanEvent = null;
9694
10505
  screen.clearKey("plan");
9695
10506
  if (toolsBlockStartedAt !== null) {
9696
10507
  toolsBlockEndedAt = Date.now();
@@ -9757,6 +10568,7 @@ async function runSession(term, config, opts, exitHint) {
9757
10568
  toolsExpanded = false;
9758
10569
  }
9759
10570
  screen.clearKey("plan");
10571
+ lastPlanEvent = null;
9760
10572
  if (pendingTurns > 0) {
9761
10573
  adjustPendingTurns(-pendingTurns);
9762
10574
  }
@@ -9834,7 +10646,7 @@ connection lost: ${err.message}
9834
10646
  process.on("SIGINT", sigintHandler);
9835
10647
  return await sessionDone;
9836
10648
  }
9837
- async function resolveSession(term, config, opts) {
10649
+ async function resolveSession(term, config, serviceToken, opts) {
9838
10650
  const cwd = opts.cwd ?? process.cwd();
9839
10651
  if (opts.sessionId) {
9840
10652
  return {
@@ -9847,7 +10659,7 @@ async function resolveSession(term, config, opts) {
9847
10659
  return newCtx(opts, cwd, config);
9848
10660
  }
9849
10661
  if (opts.resume) {
9850
- const sessions2 = await listSessions(config, { cwd, all: true });
10662
+ const sessions2 = await listSessions(config, serviceToken, { cwd, all: true });
9851
10663
  const target = pickMostRecent(sessions2, cwd);
9852
10664
  if (!target) {
9853
10665
  term.yellow(`No sessions found for ${cwd}.
@@ -9860,14 +10672,15 @@ async function resolveSession(term, config, opts) {
9860
10672
  cwd
9861
10673
  };
9862
10674
  }
9863
- const sessions = await listSessions(config);
10675
+ const sessions = await listSessions(config, serviceToken);
9864
10676
  if (sessions.length === 0) {
9865
10677
  return newCtx(opts, cwd, config);
9866
10678
  }
9867
10679
  const choice = await pickSession(term, {
9868
10680
  cwd,
9869
10681
  sessions,
9870
- config
10682
+ config,
10683
+ serviceToken
9871
10684
  });
9872
10685
  if (choice.kind === "abort") {
9873
10686
  return null;
@@ -9922,7 +10735,7 @@ function rotateIfBig(target) {
9922
10735
  } catch {
9923
10736
  }
9924
10737
  }
9925
- var PLAN_PREFIX_TEXT, logMaxBytes;
10738
+ var logMaxBytes;
9926
10739
  var init_app = __esm({
9927
10740
  "src/tui/app.ts"() {
9928
10741
  "use strict";
@@ -9930,6 +10743,7 @@ var init_app = __esm({
9930
10743
  init_types();
9931
10744
  init_resilient_ws();
9932
10745
  init_config();
10746
+ init_service_token();
9933
10747
  init_daemon_bootstrap();
9934
10748
  init_session();
9935
10749
  init_paths();
@@ -9945,7 +10759,6 @@ var init_app = __esm({
9945
10759
  init_completion();
9946
10760
  init_render_update();
9947
10761
  init_format();
9948
- 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.";
9949
10762
  logMaxBytes = 5 * 1024 * 1024;
9950
10763
  }
9951
10764
  });
@@ -10043,23 +10856,25 @@ function resolveOption(flags, key) {
10043
10856
  // src/cli/commands/init.ts
10044
10857
  init_paths();
10045
10858
  init_config();
10046
- import * as fs2 from "fs/promises";
10859
+ init_service_token();
10860
+ import * as fs3 from "fs/promises";
10047
10861
  async function runInit(flags) {
10048
- await fs2.mkdir(paths.home(), { recursive: true });
10049
- const existingToken = await loadAuthToken();
10862
+ await fs3.mkdir(paths.home(), { recursive: true });
10863
+ await migrateLegacyAuthToken();
10864
+ const existingToken = await readServiceToken();
10050
10865
  if (!existingToken) {
10051
- const token = generateAuthToken();
10052
- await writeAuthToken(token);
10866
+ const token = generateServiceToken();
10867
+ await writeServiceToken(token);
10053
10868
  process.stdout.write(
10054
10869
  `Initialized ${paths.authToken()}
10055
- Auth token: ${token}
10870
+ Service token: ${token}
10056
10871
  `
10057
10872
  );
10058
10873
  return;
10059
10874
  }
10060
10875
  if (flagBool(flags, "rotate-token")) {
10061
- const newToken = generateAuthToken();
10062
- await writeAuthToken(newToken);
10876
+ const newToken = generateServiceToken();
10877
+ await writeServiceToken(newToken);
10063
10878
  process.stdout.write(
10064
10879
  `Rotated token in ${paths.authToken()}
10065
10880
  New token: ${newToken}
@@ -10067,20 +10882,21 @@ New token: ${newToken}
10067
10882
  );
10068
10883
  return;
10069
10884
  }
10070
- process.stdout.write(`Auth token already exists at ${paths.authToken()}.
10885
+ process.stdout.write(`Service token already exists at ${paths.authToken()}.
10071
10886
  `);
10072
- 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");
10073
10888
  }
10074
10889
 
10075
10890
  // src/cli/commands/daemon.ts
10076
10891
  init_paths();
10077
10892
  init_config();
10893
+ init_service_token();
10078
10894
  import * as fsp6 from "fs/promises";
10079
10895
  import { setTimeout as sleep2 } from "timers/promises";
10080
10896
 
10081
10897
  // src/daemon/server.ts
10082
10898
  init_config();
10083
- import * as fs11 from "fs";
10899
+ import * as fs15 from "fs";
10084
10900
  import * as fsp4 from "fs/promises";
10085
10901
  import Fastify from "fastify";
10086
10902
  import websocketPlugin from "@fastify/websocket";
@@ -10089,12 +10905,12 @@ import createPinoRoll from "pino-roll";
10089
10905
 
10090
10906
  // src/core/registry.ts
10091
10907
  init_paths();
10092
- import * as fs4 from "fs/promises";
10908
+ import * as fs5 from "fs/promises";
10093
10909
  import { z as z2 } from "zod";
10094
10910
 
10095
10911
  // src/core/binary-install.ts
10096
10912
  init_paths();
10097
- import * as fs3 from "fs";
10913
+ import * as fs4 from "fs";
10098
10914
  import * as fsp from "fs/promises";
10099
10915
  import * as path2 from "path";
10100
10916
  import { spawn } from "child_process";
@@ -10199,7 +11015,7 @@ async function downloadTo(args) {
10199
11015
  );
10200
11016
  }
10201
11017
  const total = Number(response.headers.get("content-length") ?? "0");
10202
- const out = fs3.createWriteStream(dest);
11018
+ const out = fs4.createWriteStream(dest);
10203
11019
  const nodeStream = Readable.fromWeb(response.body);
10204
11020
  let received = 0;
10205
11021
  let lastEmit = Date.now();
@@ -10326,7 +11142,8 @@ async function ensureNpmPackage(args) {
10326
11142
  await installInto({
10327
11143
  agentId: args.agentId,
10328
11144
  packageSpec: args.packageSpec,
10329
- installDir
11145
+ installDir,
11146
+ registry: args.registry
10330
11147
  });
10331
11148
  if (!await fileExists2(binPath)) {
10332
11149
  throw new Error(
@@ -10344,7 +11161,8 @@ async function installInto(args) {
10344
11161
  );
10345
11162
  await runNpmInstall({
10346
11163
  packageSpec: args.packageSpec,
10347
- cwd: tempDir
11164
+ cwd: tempDir,
11165
+ registry: args.registry
10348
11166
  });
10349
11167
  try {
10350
11168
  await fsp2.rename(tempDir, args.installDir);
@@ -10368,9 +11186,10 @@ async function installInto(args) {
10368
11186
  }
10369
11187
  function runNpmInstall(args) {
10370
11188
  return new Promise((resolve5, reject) => {
11189
+ const registryArgs = args.registry ? ["--registry", args.registry] : [];
10371
11190
  const child = spawn2(
10372
11191
  "npm",
10373
- ["install", "--no-audit", "--no-fund", "--silent", args.packageSpec],
11192
+ ["install", "--no-audit", "--no-fund", "--silent", ...registryArgs, args.packageSpec],
10374
11193
  {
10375
11194
  cwd: args.cwd,
10376
11195
  stdio: ["ignore", "pipe", "pipe"]
@@ -10523,7 +11342,7 @@ var Registry = class {
10523
11342
  async readDiskCache() {
10524
11343
  let text;
10525
11344
  try {
10526
- text = await fs4.readFile(paths.registryCache(), "utf8");
11345
+ text = await fs5.readFile(paths.registryCache(), "utf8");
10527
11346
  } catch (err) {
10528
11347
  const e = err;
10529
11348
  if (e.code === "ENOENT") {
@@ -10549,7 +11368,7 @@ var Registry = class {
10549
11368
  // without a lock file: the loser of the rename race just gets its
10550
11369
  // version replaced by the winner's.
10551
11370
  async writeDiskCache(cache) {
10552
- await fs4.mkdir(paths.home(), { recursive: true });
11371
+ await fs5.mkdir(paths.home(), { recursive: true });
10553
11372
  const final = paths.registryCache();
10554
11373
  const tmp = `${final}.tmp-${process.pid}-${randSuffix()}`;
10555
11374
  const body = JSON.stringify(
@@ -10558,10 +11377,10 @@ var Registry = class {
10558
11377
  2
10559
11378
  ) + "\n";
10560
11379
  try {
10561
- await fs4.writeFile(tmp, body, "utf8");
10562
- await fs4.rename(tmp, final);
11380
+ await fs5.writeFile(tmp, body, "utf8");
11381
+ await fs5.rename(tmp, final);
10563
11382
  } catch (err) {
10564
- await fs4.unlink(tmp).catch(() => void 0);
11383
+ await fs5.unlink(tmp).catch(() => void 0);
10565
11384
  throw err;
10566
11385
  }
10567
11386
  }
@@ -10579,7 +11398,7 @@ function npxPackageBasename(agent) {
10579
11398
  const atIdx = afterSlash.lastIndexOf("@");
10580
11399
  return atIdx <= 0 ? afterSlash : afterSlash.slice(0, atIdx);
10581
11400
  }
10582
- async function planSpawn(agent, callerArgs = []) {
11401
+ async function planSpawn(agent, callerArgs = [], options = {}) {
10583
11402
  if (agent.distribution.npx) {
10584
11403
  const npx = agent.distribution.npx;
10585
11404
  const tail = callerArgs.length > 0 ? callerArgs : npx.args ?? [];
@@ -10595,7 +11414,8 @@ async function planSpawn(agent, callerArgs = []) {
10595
11414
  agentId: agent.id,
10596
11415
  version: agent.version ?? "current",
10597
11416
  packageSpec: npx.package,
10598
- bin
11417
+ bin,
11418
+ registry: options.npmRegistry
10599
11419
  });
10600
11420
  return {
10601
11421
  command: binPath,
@@ -10820,7 +11640,7 @@ stderr: ${tail}` : reason;
10820
11640
  };
10821
11641
 
10822
11642
  // src/core/session-manager.ts
10823
- import * as fs9 from "fs/promises";
11643
+ import * as fs11 from "fs/promises";
10824
11644
  import * as os2 from "os";
10825
11645
  import { customAlphabet as customAlphabet3 } from "nanoid";
10826
11646
  init_session();
@@ -10828,7 +11648,7 @@ init_session_store();
10828
11648
 
10829
11649
  // src/core/history-store.ts
10830
11650
  init_paths();
10831
- import * as fs6 from "fs/promises";
11651
+ import * as fs8 from "fs/promises";
10832
11652
  var SESSION_ID_PATTERN2 = /^[A-Za-z0-9_-]+$/;
10833
11653
  var DEFAULT_MAX_ENTRIES = 1e3;
10834
11654
  var HistoryStore = class {
@@ -10845,9 +11665,9 @@ var HistoryStore = class {
10845
11665
  return;
10846
11666
  }
10847
11667
  return this.enqueue(sessionId, async () => {
10848
- await fs6.mkdir(paths.sessionDir(sessionId), { recursive: true });
11668
+ await fs8.mkdir(paths.sessionDir(sessionId), { recursive: true });
10849
11669
  const line = JSON.stringify(entry) + "\n";
10850
- await fs6.appendFile(paths.historyFile(sessionId), line, {
11670
+ await fs8.appendFile(paths.historyFile(sessionId), line, {
10851
11671
  encoding: "utf8",
10852
11672
  mode: 384
10853
11673
  });
@@ -10858,9 +11678,9 @@ var HistoryStore = class {
10858
11678
  return;
10859
11679
  }
10860
11680
  return this.enqueue(sessionId, async () => {
10861
- await fs6.mkdir(paths.sessionDir(sessionId), { recursive: true });
11681
+ await fs8.mkdir(paths.sessionDir(sessionId), { recursive: true });
10862
11682
  const body = entries.length === 0 ? "" : entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
10863
- await fs6.writeFile(paths.historyFile(sessionId), body, {
11683
+ await fs8.writeFile(paths.historyFile(sessionId), body, {
10864
11684
  encoding: "utf8",
10865
11685
  mode: 384
10866
11686
  });
@@ -10877,7 +11697,7 @@ var HistoryStore = class {
10877
11697
  return this.enqueue(sessionId, async () => {
10878
11698
  let raw;
10879
11699
  try {
10880
- raw = await fs6.readFile(paths.historyFile(sessionId), "utf8");
11700
+ raw = await fs8.readFile(paths.historyFile(sessionId), "utf8");
10881
11701
  } catch (err) {
10882
11702
  const e = err;
10883
11703
  if (e.code === "ENOENT") {
@@ -10890,7 +11710,7 @@ var HistoryStore = class {
10890
11710
  return;
10891
11711
  }
10892
11712
  const trimmed = lines.slice(-maxEntries);
10893
- await fs6.writeFile(paths.historyFile(sessionId), trimmed.join("\n") + "\n", {
11713
+ await fs8.writeFile(paths.historyFile(sessionId), trimmed.join("\n") + "\n", {
10894
11714
  encoding: "utf8",
10895
11715
  mode: 384
10896
11716
  });
@@ -10906,7 +11726,7 @@ var HistoryStore = class {
10906
11726
  }
10907
11727
  let raw;
10908
11728
  try {
10909
- raw = await fs6.readFile(paths.historyFile(sessionId), "utf8");
11729
+ raw = await fs8.readFile(paths.historyFile(sessionId), "utf8");
10910
11730
  } catch (err) {
10911
11731
  const e = err;
10912
11732
  if (e.code === "ENOENT") {
@@ -10952,7 +11772,7 @@ var HistoryStore = class {
10952
11772
  }
10953
11773
  return this.enqueue(sessionId, async () => {
10954
11774
  try {
10955
- await fs6.unlink(paths.historyFile(sessionId));
11775
+ await fs8.unlink(paths.historyFile(sessionId));
10956
11776
  } catch (err) {
10957
11777
  const e = err;
10958
11778
  if (e.code !== "ENOENT") {
@@ -10960,7 +11780,7 @@ var HistoryStore = class {
10960
11780
  }
10961
11781
  }
10962
11782
  try {
10963
- await fs6.rmdir(paths.sessionDir(sessionId));
11783
+ await fs8.rmdir(paths.sessionDir(sessionId));
10964
11784
  } catch (err) {
10965
11785
  const e = err;
10966
11786
  if (e.code !== "ENOENT" && e.code !== "ENOTEMPTY") {
@@ -10988,6 +11808,8 @@ init_paths();
10988
11808
  init_history();
10989
11809
  init_types();
10990
11810
  init_hydra_version();
11811
+ init_queue_store();
11812
+ var QUEUE_REPLAY_TTL_MS = 15 * 60 * 1e3;
10991
11813
  var HYDRA_ID_ALPHABET3 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
10992
11814
  var generateRawSessionId = customAlphabet3(HYDRA_ID_ALPHABET3, 16);
10993
11815
  var SessionManager = class {
@@ -11000,6 +11822,7 @@ var SessionManager = class {
11000
11822
  this.idleTimeoutMs = options.idleTimeoutMs ?? 0;
11001
11823
  this.defaultModels = options.defaultModels ?? {};
11002
11824
  this.logger = options.logger;
11825
+ this.npmRegistry = options.npmRegistry;
11003
11826
  }
11004
11827
  registry;
11005
11828
  sessions = /* @__PURE__ */ new Map();
@@ -11015,6 +11838,7 @@ var SessionManager = class {
11015
11838
  // back-to-back) don't lose writes via interleaved reads.
11016
11839
  metaWriteQueues = /* @__PURE__ */ new Map();
11017
11840
  logger;
11841
+ npmRegistry;
11018
11842
  async create(params) {
11019
11843
  const fresh = await this.bootstrapAgent({
11020
11844
  agentId: params.agentId,
@@ -11036,7 +11860,9 @@ var SessionManager = class {
11036
11860
  spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
11037
11861
  historyStore: this.histories,
11038
11862
  historyMaxEntries: this.sessionHistoryMaxEntries,
11039
- currentModel: fresh.initialModel
11863
+ currentModel: fresh.initialModel,
11864
+ currentMode: fresh.initialMode,
11865
+ agentModes: fresh.initialModes
11040
11866
  });
11041
11867
  await this.attachManagerHooks(session);
11042
11868
  return session;
@@ -11081,7 +11907,7 @@ var SessionManager = class {
11081
11907
  if (params.upstreamSessionId === "") {
11082
11908
  return this.doResurrectFromImport(params);
11083
11909
  }
11084
- const plan = await planSpawn(agentDef, params.agentArgs ?? []);
11910
+ const plan = await planSpawn(agentDef, params.agentArgs ?? [], { npmRegistry: this.npmRegistry });
11085
11911
  const agent = this.spawner({
11086
11912
  agentId: params.agentId,
11087
11913
  cwd: params.cwd,
@@ -11135,9 +11961,10 @@ var SessionManager = class {
11135
11961
  // this fix), fall back to the model the agent ships in its
11136
11962
  // session/load response body.
11137
11963
  currentModel: params.currentModel ?? extractInitialModel(loadResult ?? {}),
11138
- currentMode: params.currentMode,
11964
+ currentMode: params.currentMode ?? extractInitialCurrentMode(loadResult ?? {}),
11139
11965
  currentUsage: params.currentUsage,
11140
11966
  agentCommands: params.agentCommands,
11967
+ agentModes: params.agentModes ?? nonEmptyOrUndefined(extractInitialModes(loadResult ?? {})),
11141
11968
  // Only gate the first-prompt title heuristic when we actually have
11142
11969
  // a title to preserve. A title-less session (lost to a write race
11143
11970
  // or never seeded) should re-derive from the next prompt rather
@@ -11180,9 +12007,10 @@ var SessionManager = class {
11180
12007
  // Prefer the stored value (set by a previous current_model_update);
11181
12008
  // fall back to whatever the agent ships in its session/new response.
11182
12009
  currentModel: params.currentModel ?? fresh.initialModel,
11183
- currentMode: params.currentMode,
12010
+ currentMode: params.currentMode ?? fresh.initialMode,
11184
12011
  currentUsage: params.currentUsage,
11185
12012
  agentCommands: params.agentCommands,
12013
+ agentModes: params.agentModes ?? fresh.initialModes,
11186
12014
  firstPromptSeeded: !!params.title,
11187
12015
  createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0
11188
12016
  });
@@ -11192,7 +12020,7 @@ var SessionManager = class {
11192
12020
  }
11193
12021
  async resolveImportCwd(cwd) {
11194
12022
  try {
11195
- const stat4 = await fs9.stat(cwd);
12023
+ const stat4 = await fs11.stat(cwd);
11196
12024
  if (stat4.isDirectory()) {
11197
12025
  return cwd;
11198
12026
  }
@@ -11212,7 +12040,7 @@ var SessionManager = class {
11212
12040
  err.code = JsonRpcErrorCodes.AgentNotInstalled;
11213
12041
  throw err;
11214
12042
  }
11215
- const plan = await planSpawn(agentDef, params.agentArgs ?? []);
12043
+ const plan = await planSpawn(agentDef, params.agentArgs ?? [], { npmRegistry: this.npmRegistry });
11216
12044
  const agent = this.spawner({
11217
12045
  agentId: params.agentId,
11218
12046
  cwd: params.cwd,
@@ -11249,11 +12077,15 @@ var SessionManager = class {
11249
12077
  } catch {
11250
12078
  }
11251
12079
  }
12080
+ const initialModes = extractInitialModes(newResult);
12081
+ const initialMode = extractInitialCurrentMode(newResult);
11252
12082
  return {
11253
12083
  agent,
11254
12084
  upstreamSessionId: sessionIdRaw,
11255
12085
  agentMeta: newResult._meta,
11256
- initialModel
12086
+ initialModel,
12087
+ initialModes: initialModes.length > 0 ? initialModes : void 0,
12088
+ initialMode
11257
12089
  };
11258
12090
  } catch (err) {
11259
12091
  await agent.kill().catch(() => void 0);
@@ -11305,6 +12137,15 @@ var SessionManager = class {
11305
12137
  }))
11306
12138
  }).catch(() => void 0);
11307
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
+ });
11308
12149
  this.sessions.set(session.sessionId, session);
11309
12150
  await this.enqueueMetaWrite(session.sessionId, async () => {
11310
12151
  const existing = await this.store.read(session.sessionId);
@@ -11347,6 +12188,7 @@ var SessionManager = class {
11347
12188
  currentMode: record.currentMode,
11348
12189
  currentUsage: persistedUsageToSnapshot(record.currentUsage),
11349
12190
  agentCommands: record.agentCommands,
12191
+ agentModes: record.agentModes,
11350
12192
  createdAt: record.createdAt
11351
12193
  };
11352
12194
  }
@@ -11624,6 +12466,7 @@ var SessionManager = class {
11624
12466
  ...update.currentMode !== void 0 ? { currentMode: update.currentMode } : {},
11625
12467
  ...update.currentUsage !== void 0 ? { currentUsage: update.currentUsage } : {},
11626
12468
  ...update.agentCommands !== void 0 ? { agentCommands: update.agentCommands } : {},
12469
+ ...update.agentModes !== void 0 ? { agentModes: update.agentModes } : {},
11627
12470
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
11628
12471
  });
11629
12472
  });
@@ -11657,6 +12500,53 @@ var SessionManager = class {
11657
12500
  }
11658
12501
  await Promise.allSettled(pending);
11659
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
+ }
11660
12550
  };
11661
12551
  function mergeForPersistence(session, existing) {
11662
12552
  const persistedCommands = session.mergedAvailableCommands().length > 0 ? session.agentOnlyAdvertisedCommands().map((c) => {
@@ -11666,6 +12556,18 @@ function mergeForPersistence(session, existing) {
11666
12556
  return { name: c.name };
11667
12557
  }) : void 0;
11668
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;
11669
12571
  return recordFromMemorySession({
11670
12572
  sessionId: session.sessionId,
11671
12573
  lineageId: existing?.lineageId ?? generateLineageId(),
@@ -11681,6 +12583,7 @@ function mergeForPersistence(session, existing) {
11681
12583
  currentMode: session.currentMode ?? existing?.currentMode,
11682
12584
  currentUsage: usageSnapshotToPersisted(session.currentUsage) ?? existing?.currentUsage,
11683
12585
  agentCommands,
12586
+ agentModes,
11684
12587
  createdAt: existing?.createdAt ?? new Date(session.createdAt).toISOString()
11685
12588
  });
11686
12589
  }
@@ -11743,9 +12646,103 @@ function asString(value) {
11743
12646
  const trimmed = value.trim();
11744
12647
  return trimmed.length > 0 ? trimmed : void 0;
11745
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
+ }
11746
12743
  async function loadPromptHistorySafely(sessionId) {
11747
12744
  try {
11748
- const raw = await fs9.readFile(paths.tuiHistoryFile(sessionId), "utf8");
12745
+ const raw = await fs11.readFile(paths.tuiHistoryFile(sessionId), "utf8");
11749
12746
  const out = [];
11750
12747
  for (const line of raw.split("\n")) {
11751
12748
  if (line.length === 0) {
@@ -11766,7 +12763,7 @@ async function loadPromptHistorySafely(sessionId) {
11766
12763
  }
11767
12764
  async function historyMtimeIso(sessionId) {
11768
12765
  try {
11769
- const st = await fs9.stat(paths.historyFile(sessionId));
12766
+ const st = await fs11.stat(paths.historyFile(sessionId));
11770
12767
  return new Date(st.mtimeMs).toISOString();
11771
12768
  } catch {
11772
12769
  return void 0;
@@ -11776,7 +12773,7 @@ async function historyMtimeIso(sessionId) {
11776
12773
  // src/core/extensions.ts
11777
12774
  init_paths();
11778
12775
  import { spawn as spawn4 } from "child_process";
11779
- import * as fs10 from "fs";
12776
+ import * as fs12 from "fs";
11780
12777
  import * as fsp3 from "fs/promises";
11781
12778
  import * as path7 from "path";
11782
12779
  var RESTART_BASE_MS = 1e3;
@@ -12059,7 +13056,7 @@ var ExtensionManager = class {
12059
13056
  }
12060
13057
  const ext = entry.config;
12061
13058
  const command = ext.command.length > 0 ? ext.command : [ext.name];
12062
- const logStream = fs10.createWriteStream(paths.extensionLogFile(ext.name), {
13059
+ const logStream = fs12.createWriteStream(paths.extensionLogFile(ext.name), {
12063
13060
  flags: "a"
12064
13061
  });
12065
13062
  logStream.write(
@@ -12071,7 +13068,7 @@ var ExtensionManager = class {
12071
13068
  HYDRA_ACP_DAEMON_URL: ctx.daemonUrl,
12072
13069
  HYDRA_ACP_DAEMON_HOST: ctx.daemonHost,
12073
13070
  HYDRA_ACP_DAEMON_PORT: String(ctx.daemonPort),
12074
- HYDRA_ACP_TOKEN: ctx.daemonToken,
13071
+ HYDRA_ACP_TOKEN: ctx.serviceToken,
12075
13072
  HYDRA_ACP_WS_URL: ctx.daemonWsUrl,
12076
13073
  HYDRA_ACP_HOME: ctx.hydraHome,
12077
13074
  HYDRA_ACP_EXTENSION_NAME: ext.name,
@@ -12109,7 +13106,7 @@ var ExtensionManager = class {
12109
13106
  }
12110
13107
  if (typeof child.pid === "number") {
12111
13108
  try {
12112
- fs10.writeFileSync(paths.extensionPidFile(ext.name), `${child.pid}
13109
+ fs12.writeFileSync(paths.extensionPidFile(ext.name), `${child.pid}
12113
13110
  `, {
12114
13111
  encoding: "utf8",
12115
13112
  mode: 384
@@ -12134,7 +13131,7 @@ var ExtensionManager = class {
12134
13131
  });
12135
13132
  child.on("exit", (code, signal) => {
12136
13133
  try {
12137
- fs10.unlinkSync(paths.extensionPidFile(ext.name));
13134
+ fs12.unlinkSync(paths.extensionPidFile(ext.name));
12138
13135
  } catch {
12139
13136
  }
12140
13137
  logStream.write(
@@ -12177,25 +13174,245 @@ var ExtensionManager = class {
12177
13174
  }
12178
13175
  }
12179
13176
  };
12180
- function isAlive(pid) {
12181
- try {
12182
- process.kill(pid, 0);
12183
- return true;
12184
- } 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") {
12185
13375
  return false;
12186
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");
12187
13379
  }
12188
- function withCode2(err, code) {
12189
- err.code = code;
12190
- return err;
12191
- }
12192
-
12193
- // src/daemon/server.ts
12194
- init_paths();
12195
- init_hydra_version();
12196
13380
 
12197
13381
  // src/daemon/auth.ts
12198
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
+ };
12199
13416
  function bearerAuth(opts) {
12200
13417
  return async function authMiddleware(request, reply) {
12201
13418
  const header = request.headers.authorization;
@@ -12204,10 +13421,12 @@ function bearerAuth(opts) {
12204
13421
  return;
12205
13422
  }
12206
13423
  const token = header.slice(BEARER_PREFIX.length).trim();
12207
- if (!constantTimeEqual(token, opts.config.daemon.authToken)) {
13424
+ const identity = await opts.validator.validate(token);
13425
+ if (!identity) {
12208
13426
  reply.code(403).send({ error: "Invalid token" });
12209
13427
  return;
12210
13428
  }
13429
+ request.authIdentity = identity;
12211
13430
  };
12212
13431
  }
12213
13432
  function tokenFromUpgradeRequest(req) {
@@ -12246,6 +13465,40 @@ function constantTimeEqual(a, b) {
12246
13465
  return mismatch === 0;
12247
13466
  }
12248
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
+
12249
13502
  // src/daemon/routes/sessions.ts
12250
13503
  init_config();
12251
13504
  init_bundle();
@@ -12613,6 +13866,181 @@ function registerConfigRoutes(app, defaults) {
12613
13866
  });
12614
13867
  }
12615
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
+
12616
14044
  // src/daemon/acp-ws.ts
12617
14045
  init_connection();
12618
14046
  init_ws_stream();
@@ -12620,12 +14048,12 @@ init_types();
12620
14048
  import { nanoid as nanoid2 } from "nanoid";
12621
14049
  init_hydra_version();
12622
14050
  function registerAcpWsEndpoint(app, deps) {
12623
- app.get("/acp", { websocket: true }, (socket, request) => {
14051
+ app.get("/acp", { websocket: true }, async (socket, request) => {
12624
14052
  const token = tokenFromUpgradeRequest({
12625
14053
  headers: request.headers,
12626
14054
  url: request.url
12627
14055
  });
12628
- if (!token || !constantTimeEqual(token, deps.config.daemon.authToken)) {
14056
+ if (!token || !await deps.validator.validate(token)) {
12629
14057
  socket.close(4401, "Unauthorized");
12630
14058
  return;
12631
14059
  }
@@ -12676,8 +14104,15 @@ function registerAcpWsEndpoint(app, deps) {
12676
14104
  }
12677
14105
  })();
12678
14106
  });
14107
+ const modesPayload = buildModesPayload(session);
12679
14108
  return {
12680
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 } : {},
12681
14116
  _meta: buildResponseMeta(session)
12682
14117
  };
12683
14118
  });
@@ -12738,6 +14173,7 @@ function registerAcpWsEndpoint(app, deps) {
12738
14173
  await connection.notify(note.method, note.params);
12739
14174
  }
12740
14175
  session.replayPendingPermissions(client);
14176
+ const modesPayload = buildModesPayload(session);
12741
14177
  return {
12742
14178
  sessionId: session.sessionId,
12743
14179
  clientId: client.clientId,
@@ -12748,6 +14184,7 @@ function registerAcpWsEndpoint(app, deps) {
12748
14184
  // ran, not what was asked for.
12749
14185
  historyPolicy: appliedPolicy,
12750
14186
  replayed: replay.length,
14187
+ ...modesPayload ? { modes: modesPayload } : {},
12751
14188
  _meta: buildResponseMeta(session)
12752
14189
  };
12753
14190
  });
@@ -12781,7 +14218,29 @@ function registerAcpWsEndpoint(app, deps) {
12781
14218
  err.code = JsonRpcErrorCodes.SessionNotFound;
12782
14219
  throw err;
12783
14220
  }
12784
- 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
+ }
12785
14244
  return session.prompt(att.clientId, params);
12786
14245
  });
12787
14246
  const handleCancelParams = (raw) => {
@@ -12813,6 +14272,26 @@ function registerAcpWsEndpoint(app, deps) {
12813
14272
  handleCancelParams(raw);
12814
14273
  return null;
12815
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
+ });
12816
14295
  connection.onRequest("session/load", async (raw) => {
12817
14296
  const rawObj = raw ?? {};
12818
14297
  const rawSessionId = typeof rawObj.sessionId === "string" ? rawObj.sessionId : void 0;
@@ -12844,8 +14323,13 @@ function registerAcpWsEndpoint(app, deps) {
12844
14323
  await connection.notify(note.method, note.params);
12845
14324
  }
12846
14325
  session.replayPendingPermissions(client);
14326
+ const modesPayload = buildModesPayload(session);
12847
14327
  return {
12848
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 } : {},
12849
14333
  _meta: buildResponseMeta(session)
12850
14334
  };
12851
14335
  });
@@ -12871,6 +14355,26 @@ function registerAcpWsEndpoint(app, deps) {
12871
14355
  });
12872
14356
  });
12873
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
+ }
12874
14378
  function buildResponseMeta(session) {
12875
14379
  const ours = {
12876
14380
  upstreamSessionId: session.upstreamSessionId,
@@ -12896,9 +14400,17 @@ function buildResponseMeta(session) {
12896
14400
  if (commands.length > 0) {
12897
14401
  ours.availableCommands = commands;
12898
14402
  }
14403
+ const modes = session.availableModes();
14404
+ if (modes.length > 0) {
14405
+ ours.availableModes = modes;
14406
+ }
12899
14407
  if (session.turnStartedAt !== void 0) {
12900
14408
  ours.turnStartedAt = session.turnStartedAt;
12901
14409
  }
14410
+ const queue = session.queueSnapshot();
14411
+ if (queue.length > 0) {
14412
+ ours.queue = queue;
14413
+ }
12902
14414
  return mergeMeta(session.agentMeta, ours);
12903
14415
  }
12904
14416
  function buildInitializeResult() {
@@ -12929,7 +14441,13 @@ function buildInitializeResult() {
12929
14441
  id: "bearer-token",
12930
14442
  description: "Bearer token presented at WS upgrade"
12931
14443
  }
12932
- ]
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 })
12933
14451
  };
12934
14452
  }
12935
14453
  function bindClientToSession(connection, session, state, clientInfo, callerClientId) {
@@ -12943,7 +14461,7 @@ function bindClientToSession(connection, session, state, clientInfo, callerClien
12943
14461
  }
12944
14462
 
12945
14463
  // src/daemon/server.ts
12946
- async function startDaemon(config) {
14464
+ async function startDaemon(config, serviceToken) {
12947
14465
  ensureLoopbackOrTls(config);
12948
14466
  const httpsOptions = config.daemon.tls ? {
12949
14467
  key: await fsp4.readFile(config.daemon.tls.key),
@@ -12970,7 +14488,13 @@ async function startDaemon(config) {
12970
14488
  setNpmInstallLogger((msg) => {
12971
14489
  app.log.info(msg);
12972
14490
  });
12973
- 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 });
12974
14498
  app.addHook("onRequest", async (request, reply) => {
12975
14499
  if (request.routeOptions.config?.skipAuth) {
12976
14500
  return;
@@ -12980,6 +14504,13 @@ async function startDaemon(config) {
12980
14504
  }
12981
14505
  await auth(request, reply);
12982
14506
  });
14507
+ const sweepInterval = setInterval(
14508
+ () => {
14509
+ sessionTokenStore.sweepExpired();
14510
+ },
14511
+ 5 * 60 * 1e3
14512
+ );
14513
+ sweepInterval.unref();
12983
14514
  const registry = new Registry(config);
12984
14515
  const agentLogger = {
12985
14516
  info: (msg) => app.log.info(msg),
@@ -12994,7 +14525,8 @@ async function startDaemon(config) {
12994
14525
  idleTimeoutMs: config.daemon.sessionIdleTimeoutSeconds * 1e3,
12995
14526
  defaultModels: config.defaultModels,
12996
14527
  sessionHistoryMaxEntries: config.daemon.sessionHistoryMaxEntries,
12997
- logger: agentLogger
14528
+ logger: agentLogger,
14529
+ npmRegistry: config.npmRegistry
12998
14530
  });
12999
14531
  const extensions = new ExtensionManager(extensionList(config));
13000
14532
  registerHealthRoutes(app, HYDRA_VERSION);
@@ -13008,8 +14540,12 @@ async function startDaemon(config) {
13008
14540
  defaultAgent: config.defaultAgent,
13009
14541
  defaultCwd: config.defaultCwd
13010
14542
  });
14543
+ registerAuthRoutes(app, {
14544
+ store: sessionTokenStore,
14545
+ rateLimiter: authRateLimiter
14546
+ });
13011
14547
  registerAcpWsEndpoint(app, {
13012
- config,
14548
+ validator,
13013
14549
  manager,
13014
14550
  defaultAgent: config.defaultAgent
13015
14551
  });
@@ -13033,12 +14569,19 @@ async function startDaemon(config) {
13033
14569
  daemonUrl: `${scheme}://${config.daemon.host}:${boundPort}`,
13034
14570
  daemonHost: config.daemon.host,
13035
14571
  daemonPort: boundPort,
13036
- daemonToken: config.daemon.authToken,
14572
+ serviceToken,
13037
14573
  daemonWsUrl: `${wsScheme}://${config.daemon.host}:${boundPort}/acp`,
13038
14574
  hydraHome: paths.home()
13039
14575
  });
13040
14576
  await extensions.start();
14577
+ void manager.resurrectPendingQueues().catch((err) => {
14578
+ app.log.warn(
14579
+ `queue replay scan failed: ${err.message}`
14580
+ );
14581
+ });
13041
14582
  const shutdown = async () => {
14583
+ clearInterval(sweepInterval);
14584
+ await sessionTokenStore.flush();
13042
14585
  await extensions.stop();
13043
14586
  await manager.closeAll();
13044
14587
  await manager.flushMetaWrites();
@@ -13046,7 +14589,7 @@ async function startDaemon(config) {
13046
14589
  setNpmInstallLogger(null);
13047
14590
  await app.close();
13048
14591
  try {
13049
- fs11.unlinkSync(paths.pidFile());
14592
+ fs15.unlinkSync(paths.pidFile());
13050
14593
  } catch {
13051
14594
  }
13052
14595
  try {
@@ -13085,7 +14628,7 @@ function ensureLoopbackOrTls(config) {
13085
14628
  init_daemon_bootstrap();
13086
14629
 
13087
14630
  // src/cli/commands/log-tail.ts
13088
- import * as fs12 from "fs";
14631
+ import * as fs16 from "fs";
13089
14632
  import * as fsp5 from "fs/promises";
13090
14633
  async function runLogTail(logPath, argv, notFoundMessage) {
13091
14634
  const opts = parseLogTailFlags(argv);
@@ -13109,7 +14652,7 @@ async function runLogTail(logPath, argv, notFoundMessage) {
13109
14652
  process.stdout.write(`-- following ${logPath} --
13110
14653
  `);
13111
14654
  let pending = false;
13112
- const watcher = fs12.watch(logPath, () => {
14655
+ const watcher = fs16.watch(logPath, () => {
13113
14656
  if (pending) {
13114
14657
  return;
13115
14658
  }
@@ -13209,7 +14752,8 @@ function parseLogTailFlags(argv) {
13209
14752
 
13210
14753
  // src/cli/commands/daemon.ts
13211
14754
  async function runDaemonStart(flags = {}) {
13212
- const config = await ensureConfig();
14755
+ const config = await loadConfig();
14756
+ const serviceToken = await ensureServiceToken();
13213
14757
  if (await pingHealth(config)) {
13214
14758
  const info2 = await readPidFile();
13215
14759
  process.stdout.write(
@@ -13219,7 +14763,7 @@ async function runDaemonStart(flags = {}) {
13219
14763
  return;
13220
14764
  }
13221
14765
  if (flagBool(flags, "foreground")) {
13222
- const handle = await startDaemon(config);
14766
+ const handle = await startDaemon(config, serviceToken);
13223
14767
  process.stdout.write(
13224
14768
  `hydra-acp daemon listening on ${config.daemon.host}:${config.daemon.port}
13225
14769
  `
@@ -13257,7 +14801,8 @@ async function runDaemonStop() {
13257
14801
  }
13258
14802
  }
13259
14803
  async function runDaemonRestart() {
13260
- const config = await ensureConfig();
14804
+ const config = await loadConfig();
14805
+ await ensureServiceToken();
13261
14806
  const info = await readPidFile();
13262
14807
  if (info && isProcessAlive(info.pid)) {
13263
14808
  process.stdout.write(`Stopping daemon pid ${info.pid}...
@@ -13345,16 +14890,18 @@ init_sessions();
13345
14890
 
13346
14891
  // src/cli/commands/extensions.ts
13347
14892
  init_config();
14893
+ init_service_token();
13348
14894
  init_paths();
13349
14895
  import * as fsp7 from "fs/promises";
13350
14896
  init_sessions();
13351
14897
  async function runExtensionsList() {
13352
14898
  const config = await loadConfig();
14899
+ const serviceToken = await loadServiceToken();
13353
14900
  const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
13354
14901
  let body;
13355
14902
  try {
13356
14903
  const r = await fetch(`${baseUrl}/v1/extensions`, {
13357
- headers: { Authorization: `Bearer ${config.daemon.authToken}` }
14904
+ headers: { Authorization: `Bearer ${serviceToken}` }
13358
14905
  });
13359
14906
  if (!r.ok) {
13360
14907
  process.stderr.write(`Daemon returned HTTP ${r.status}
@@ -13457,13 +15004,14 @@ async function runExtensionsAdd(name, argv) {
13457
15004
  process.stdout.write(`Added extension '${name}' to ${paths.config()}
13458
15005
  `);
13459
15006
  const config = await loadConfig();
15007
+ const serviceToken = await loadServiceToken();
13460
15008
  const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
13461
15009
  const registerBody = { name, ...body };
13462
15010
  try {
13463
15011
  const r = await fetch(`${baseUrl}/v1/extensions`, {
13464
15012
  method: "POST",
13465
15013
  headers: {
13466
- Authorization: `Bearer ${config.daemon.authToken}`,
15014
+ Authorization: `Bearer ${serviceToken}`,
13467
15015
  "Content-Type": "application/json"
13468
15016
  },
13469
15017
  body: JSON.stringify(registerBody)
@@ -13511,11 +15059,12 @@ async function runExtensionsRemove(name) {
13511
15059
  process.stdout.write(`Removed extension '${name}' from ${paths.config()}
13512
15060
  `);
13513
15061
  const config = await loadConfig();
15062
+ const serviceToken = await loadServiceToken();
13514
15063
  const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
13515
15064
  try {
13516
15065
  const r = await fetch(`${baseUrl}/v1/extensions/${encodeURIComponent(name)}`, {
13517
15066
  method: "DELETE",
13518
- headers: { Authorization: `Bearer ${config.daemon.authToken}` }
15067
+ headers: { Authorization: `Bearer ${serviceToken}` }
13519
15068
  });
13520
15069
  if (r.status === 204 || r.status === 404) {
13521
15070
  process.stdout.write(`${name}: stopped
@@ -13575,12 +15124,13 @@ async function postLifecycle(name, verb) {
13575
15124
  return;
13576
15125
  }
13577
15126
  const config = await loadConfig();
15127
+ const serviceToken = await loadServiceToken();
13578
15128
  const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
13579
15129
  let r;
13580
15130
  try {
13581
15131
  r = await fetch(`${baseUrl}/v1/extensions/${encodeURIComponent(name)}/${verb}`, {
13582
15132
  method: "POST",
13583
- headers: { Authorization: `Bearer ${config.daemon.authToken}` }
15133
+ headers: { Authorization: `Bearer ${serviceToken}` }
13584
15134
  });
13585
15135
  } catch (err) {
13586
15136
  process.stderr.write(
@@ -13611,8 +15161,9 @@ async function postLifecycle(name, verb) {
13611
15161
  }
13612
15162
  async function postLifecycleAll(verb) {
13613
15163
  const config = await loadConfig();
15164
+ const serviceToken = await loadServiceToken();
13614
15165
  const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
13615
- const auth = { Authorization: `Bearer ${config.daemon.authToken}` };
15166
+ const auth = { Authorization: `Bearer ${serviceToken}` };
13616
15167
  let listBody;
13617
15168
  try {
13618
15169
  const r = await fetch(`${baseUrl}/v1/extensions`, { headers: auth });
@@ -13778,14 +15329,16 @@ function maxLen2(headerCell, values) {
13778
15329
 
13779
15330
  // src/cli/commands/agents.ts
13780
15331
  init_config();
15332
+ init_service_token();
13781
15333
  init_sessions();
13782
15334
  async function runAgentsList() {
13783
15335
  const config = await loadConfig();
15336
+ const serviceToken = await loadServiceToken();
13784
15337
  const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
13785
15338
  let body;
13786
15339
  try {
13787
15340
  const r = await fetch(`${baseUrl}/v1/agents`, {
13788
- headers: { Authorization: `Bearer ${config.daemon.authToken}` }
15341
+ headers: { Authorization: `Bearer ${serviceToken}` }
13789
15342
  });
13790
15343
  if (!r.ok) {
13791
15344
  process.stderr.write(`Daemon returned HTTP ${r.status}
@@ -13842,12 +15395,13 @@ Registry version: ${body.version}
13842
15395
  }
13843
15396
  async function runAgentsRefresh() {
13844
15397
  const config = await loadConfig();
15398
+ const serviceToken = await loadServiceToken();
13845
15399
  const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
13846
15400
  let body;
13847
15401
  try {
13848
15402
  const r = await fetch(`${baseUrl}/v1/registry/refresh`, {
13849
15403
  method: "POST",
13850
- headers: { Authorization: `Bearer ${config.daemon.authToken}` }
15404
+ headers: { Authorization: `Bearer ${serviceToken}` }
13851
15405
  });
13852
15406
  if (!r.ok) {
13853
15407
  process.stderr.write(`Daemon returned HTTP ${r.status}
@@ -13878,8 +15432,197 @@ function maxLen3(headerCell, values) {
13878
15432
  return max;
13879
15433
  }
13880
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
+
13881
15623
  // src/shim/proxy.ts
13882
15624
  init_config();
15625
+ init_service_token();
13883
15626
  init_daemon_bootstrap();
13884
15627
  init_resilient_ws();
13885
15628
 
@@ -14054,13 +15797,14 @@ function isResponse2(msg) {
14054
15797
 
14055
15798
  // src/shim/proxy.ts
14056
15799
  async function runShim(opts) {
14057
- const config = await ensureConfig();
15800
+ const config = await loadConfig();
15801
+ const serviceToken = await ensureServiceToken();
14058
15802
  await ensureDaemonReachable(config);
14059
15803
  const tracker = new SessionTracker();
14060
15804
  const downstream = ndjsonStreamFromStdio(process.stdin, process.stdout);
14061
15805
  const protocol = config.daemon.tls ? "wss" : "ws";
14062
15806
  const url = `${protocol}://${config.daemon.host}:${config.daemon.port}/acp`;
14063
- const subprotocols = ["acp.v1", `hydra-acp-token.${config.daemon.authToken}`];
15807
+ const subprotocols = ["acp.v1", `hydra-acp-token.${serviceToken}`];
14064
15808
  const upstream = new ResilientWsStream({
14065
15809
  url,
14066
15810
  subprotocols,
@@ -14401,6 +16145,7 @@ async function main() {
14401
16145
  process.exit(2);
14402
16146
  return;
14403
16147
  }
16148
+ case "session":
14404
16149
  case "sessions": {
14405
16150
  const sub = positional[1];
14406
16151
  if (sub === void 0 || sub === "list") {
@@ -14437,13 +16182,14 @@ async function main() {
14437
16182
  });
14438
16183
  return;
14439
16184
  }
14440
- process.stderr.write(`Unknown sessions subcommand: ${sub}
16185
+ process.stderr.write(`Unknown session subcommand: ${sub}
14441
16186
  `);
14442
16187
  process.exit(2);
14443
16188
  return;
14444
16189
  }
16190
+ case "extension":
14445
16191
  case "extensions": {
14446
- const extIdx = argv.indexOf("extensions");
16192
+ const extIdx = argv.indexOf(subcommand);
14447
16193
  const tail = argv.slice(extIdx + 1);
14448
16194
  const sub = tail[0];
14449
16195
  const name2 = tail[1];
@@ -14476,11 +16222,12 @@ async function main() {
14476
16222
  await runExtensionsLogs(name2, rest);
14477
16223
  return;
14478
16224
  }
14479
- process.stderr.write(`Unknown extensions subcommand: ${sub}
16225
+ process.stderr.write(`Unknown extension subcommand: ${sub}
14480
16226
  `);
14481
16227
  process.exit(2);
14482
16228
  return;
14483
16229
  }
16230
+ case "agent":
14484
16231
  case "agents": {
14485
16232
  const sub = positional[1];
14486
16233
  if (sub === void 0 || sub === "list") {
@@ -14491,7 +16238,33 @@ async function main() {
14491
16238
  await runAgentsRefresh();
14492
16239
  return;
14493
16240
  }
14494
- 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}
14495
16268
  `);
14496
16269
  process.exit(2);
14497
16270
  return;
@@ -14571,23 +16344,26 @@ function printHelp() {
14571
16344
  " hydra-acp daemon start [--foreground] Start daemon (detached by default; --foreground to attach)",
14572
16345
  " hydra-acp daemon stop|restart|status",
14573
16346
  " hydra-acp daemon logs [-f] [-n N] Tail or follow the daemon log",
14574
- " hydra-acp sessions [list] [--all] [--json]",
16347
+ " hydra-acp session [list] [--all] [--json]",
14575
16348
  " List sessions (live + 20 most-recent cold; --all for everything; --json emits the raw daemon response as JSON for scripts)",
14576
- " hydra-acp sessions kill <id> Demote a live session to cold (keeps the on-disk record)",
14577
- " hydra-acp sessions remove <id> Remove a session entirely (live or cold)",
14578
- " 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>|.]",
14579
16352
  " Write a session bundle to <file>, to a default-named file when --out=., or to stdout",
14580
- " hydra-acp sessions transcript <id>|<file> [--out <file>|.]",
16353
+ " hydra-acp session transcript <id>|<file> [--out <file>|.]",
14581
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",
14582
- " hydra-acp sessions import <file>|- [--replace] [--cwd <path>] [--info]",
16355
+ " hydra-acp session import <file>|- [--replace] [--cwd <path>] [--info]",
14583
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",
14584
- " hydra-acp extensions list List configured extensions and live state",
14585
- " hydra-acp extensions add <name> [opts] Add an extension to config",
14586
- " hydra-acp extensions remove <name> Remove an extension from config",
14587
- " hydra-acp extensions start|stop|restart <n>|all Lifecycle on one or all",
14588
- " hydra-acp extensions logs <name> [-f] [-n N]Tail or follow an extension's log",
14589
- " hydra-acp agents [list] List agents in the cached registry",
14590
- " 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",
14591
16367
  " hydra-acp tui flags: [--resume <id>] [--reattach] [--new] [--agent <id>] [--model <id>] [--cwd <path>] [--name <label>]",
14592
16368
  " --resume <id> attaches to a specific session; --reattach picks the most-recent in cwd.",
14593
16369
  " Smart default (no flags): shows a picker when sessions exist, else new.",