@hydra-acp/cli 0.1.31 → 0.1.33

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
@@ -271,12 +271,12 @@ var init_config = __esm({
271
271
  // buffer. Oldest lines are dropped on overflow. The on-disk session
272
272
  // history is unaffected; this only bounds the TUI's local view buffer.
273
273
  maxScrollbackLines: z.number().int().positive().default(1e4),
274
- // When true (default), the TUI captures mouse events so the wheel can
275
- // drive scrollback. The cost: terminals route clicks to the app, so
276
- // text selection requires shift+drag to bypass mouse reporting. Set
277
- // false to disable capture — wheel scrollback stops working, but
278
- // plain click-drag selects text via the terminal emulator.
279
- mouse: z.boolean().default(true),
274
+ // When true, the TUI captures mouse events so the wheel can drive
275
+ // scrollback. The cost: terminals route clicks to the app, so text
276
+ // selection requires shift+drag to bypass mouse reporting. Default
277
+ // false — wheel scrollback stops working, but plain click-drag
278
+ // selects text via the terminal emulator. Set true to opt back in.
279
+ mouse: z.boolean().default(false),
280
280
  // Size at which the TUI's session/update debug log (tui.log) rotates
281
281
  // to tui.log.0 and resets. Bounds on-disk use at ~2x this value.
282
282
  logMaxBytes: z.number().int().positive().default(5 * 1024 * 1024),
@@ -291,13 +291,13 @@ var init_config = __esm({
291
291
  // just don't want it.
292
292
  progressIndicator: z.boolean().default(true),
293
293
  // What the unmodified Enter key does in the prompt composer.
294
- // "enqueue" (default) — Enter enqueues the prompt (sends immediately
295
- // when idle, queues behind an in-flight turn); Shift+Enter amends
296
- // the in-flight turn.
297
- // "amend" — flips the two: Enter amends the in-flight turn,
298
- // Shift+Enter enqueues. With no turn in flight either key just
299
- // enqueues, since there's nothing to amend.
300
- defaultEnterAction: z.enum(["enqueue", "amend"]).default("enqueue")
294
+ // "amend" (default) — Enter amends the in-flight turn; Shift+Enter
295
+ // enqueues. With no turn in flight either key just enqueues,
296
+ // since there's nothing to amend.
297
+ // "enqueue" — flips the two: Enter enqueues the prompt (sends
298
+ // immediately when idle, queues behind an in-flight turn);
299
+ // Shift+Enter amends the in-flight turn.
300
+ defaultEnterAction: z.enum(["enqueue", "amend"]).default("amend")
301
301
  });
302
302
  ExtensionName = z.string().min(1).regex(/^[A-Za-z0-9._-]+$/, "extension name must be filename-safe");
303
303
  ExtensionBody = z.object({
@@ -338,11 +338,11 @@ var init_config = __esm({
338
338
  tui: TuiConfig.default({
339
339
  repaintThrottleMs: 1e3,
340
340
  maxScrollbackLines: 1e4,
341
- mouse: true,
341
+ mouse: false,
342
342
  logMaxBytes: 5 * 1024 * 1024,
343
343
  cwdColumnMaxWidth: 24,
344
344
  progressIndicator: true,
345
- defaultEnterAction: "enqueue"
345
+ defaultEnterAction: "amend"
346
346
  })
347
347
  });
348
348
  }
@@ -819,9 +819,9 @@ var init_connection = __esm({
819
819
  }
820
820
  const id = nanoid();
821
821
  const message = { jsonrpc: "2.0", id, method, params };
822
- const response = new Promise((resolve5, reject) => {
822
+ const response = new Promise((resolve6, reject) => {
823
823
  this.pending.set(id, {
824
- resolve: (result) => resolve5(result),
824
+ resolve: (result) => resolve6(result),
825
825
  reject
826
826
  });
827
827
  this.stream.send(message).catch((err) => {
@@ -3074,7 +3074,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
3074
3074
  }
3075
3075
  const clientParams = this.rewriteForClient(params);
3076
3076
  const toolCallId = extractToolCallId(clientParams);
3077
- return new Promise((resolve5, reject) => {
3077
+ return new Promise((resolve6, reject) => {
3078
3078
  let settled = false;
3079
3079
  const outbound = [];
3080
3080
  const entry = { addClient: sendTo };
@@ -3113,7 +3113,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
3113
3113
  update
3114
3114
  }).catch(() => void 0);
3115
3115
  }
3116
- resolve5(result);
3116
+ resolve6(result);
3117
3117
  });
3118
3118
  }).catch((err) => {
3119
3119
  settle(() => reject(err));
@@ -3129,14 +3129,14 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
3129
3129
  // in flight, but doesn't emit prompt_queue_* broadcasts — clients
3130
3130
  // shouldn't see hydra's housekeeping in their chip list.
3131
3131
  async enqueuePrompt(task) {
3132
- return new Promise((resolve5, reject) => {
3132
+ return new Promise((resolve6, reject) => {
3133
3133
  const entry = {
3134
3134
  kind: "internal",
3135
3135
  messageId: generateMessageId(),
3136
3136
  enqueuedAt: Date.now(),
3137
3137
  cancelled: false,
3138
3138
  task,
3139
- resolve: resolve5,
3139
+ resolve: resolve6,
3140
3140
  reject
3141
3141
  };
3142
3142
  this.promptQueue.push(entry);
@@ -3155,7 +3155,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
3155
3155
  if (client.clientInfo?.name) originator.name = client.clientInfo.name;
3156
3156
  if (client.clientInfo?.version)
3157
3157
  originator.version = client.clientInfo.version;
3158
- return new Promise((resolve5, reject) => {
3158
+ return new Promise((resolve6, reject) => {
3159
3159
  const entry = {
3160
3160
  kind: "user",
3161
3161
  messageId,
@@ -3164,7 +3164,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
3164
3164
  prompt: promptArray,
3165
3165
  enqueuedAt: Date.now(),
3166
3166
  cancelled: false,
3167
- resolve: resolve5,
3167
+ resolve: resolve6,
3168
3168
  reject
3169
3169
  };
3170
3170
  this.promptQueue.push(entry);
@@ -4352,13 +4352,13 @@ function wsToMessageStream(ws) {
4352
4352
  throw new Error("ws is closed");
4353
4353
  }
4354
4354
  const text = JSON.stringify(message);
4355
- await new Promise((resolve5, reject) => {
4355
+ await new Promise((resolve6, reject) => {
4356
4356
  ws.send(text, (err) => {
4357
4357
  if (err) {
4358
4358
  reject(err);
4359
4359
  return;
4360
4360
  }
4361
- resolve5();
4361
+ resolve6();
4362
4362
  });
4363
4363
  });
4364
4364
  },
@@ -4846,8 +4846,8 @@ async function runSessionsTranscript(idOrFile, outPath) {
4846
4846
  }
4847
4847
  async function readBundleFileIfExists(arg) {
4848
4848
  try {
4849
- const stat4 = await fs17.stat(arg);
4850
- if (!stat4.isFile()) {
4849
+ const stat5 = await fs17.stat(arg);
4850
+ if (!stat5.isFile()) {
4851
4851
  return null;
4852
4852
  }
4853
4853
  } catch {
@@ -4882,8 +4882,8 @@ async function runSessionsImport(file, opts = {}) {
4882
4882
  if (opts.cwd !== void 0) {
4883
4883
  const resolved = path11.resolve(opts.cwd);
4884
4884
  try {
4885
- const stat4 = await fs17.stat(resolved);
4886
- if (!stat4.isDirectory()) {
4885
+ const stat5 = await fs17.stat(resolved);
4886
+ if (!stat5.isDirectory()) {
4887
4887
  process.stderr.write(`--cwd ${resolved} is not a directory
4888
4888
  `);
4889
4889
  process.exit(1);
@@ -5029,11 +5029,11 @@ function isResponse(msg) {
5029
5029
  return !("method" in msg) && "id" in msg && msg.id !== void 0;
5030
5030
  }
5031
5031
  async function openWs(url, subprotocols) {
5032
- return new Promise((resolve5, reject) => {
5032
+ return new Promise((resolve6, reject) => {
5033
5033
  const ws = new WebSocket(url, subprotocols);
5034
5034
  const onOpen = () => {
5035
5035
  ws.off("error", onError);
5036
- resolve5(wsToMessageStream(ws));
5036
+ resolve6(wsToMessageStream(ws));
5037
5037
  };
5038
5038
  const onError = (err) => {
5039
5039
  ws.off("open", onOpen);
@@ -5104,8 +5104,8 @@ var init_resilient_ws = __esm({
5104
5104
  throw new Error("resilient ws stream not connected");
5105
5105
  }
5106
5106
  const id = message.id;
5107
- const promise = new Promise((resolve5, reject) => {
5108
- this.pendingRequests.set(id, { resolve: resolve5, reject });
5107
+ const promise = new Promise((resolve6, reject) => {
5108
+ this.pendingRequests.set(id, { resolve: resolve6, reject });
5109
5109
  });
5110
5110
  try {
5111
5111
  await this.current.send(message);
@@ -5133,8 +5133,8 @@ var init_resilient_ws = __esm({
5133
5133
  this.bindStream(stream);
5134
5134
  const wasFirst = this.firstConnect;
5135
5135
  this.firstConnect = false;
5136
- this.connectGate = new Promise((resolve5) => {
5137
- this.releaseConnectGate = resolve5;
5136
+ this.connectGate = new Promise((resolve6) => {
5137
+ this.releaseConnectGate = resolve6;
5138
5138
  });
5139
5139
  try {
5140
5140
  if (this.opts.onConnect) {
@@ -5667,7 +5667,7 @@ async function pickSession(term, opts) {
5667
5667
  };
5668
5668
  renderFromScratch();
5669
5669
  term.hideCursor();
5670
- return await new Promise((resolve5) => {
5670
+ return await new Promise((resolve6) => {
5671
5671
  let resolved = false;
5672
5672
  const onResize = () => {
5673
5673
  if (resolved) {
@@ -5807,7 +5807,7 @@ async function pickSession(term, opts) {
5807
5807
  if (mode === "help") {
5808
5808
  if (name === "CTRL_C") {
5809
5809
  cleanup();
5810
- resolve5({ kind: "abort" });
5810
+ resolve6({ kind: "abort" });
5811
5811
  return;
5812
5812
  }
5813
5813
  mode = "normal";
@@ -5921,12 +5921,12 @@ async function pickSession(term, opts) {
5921
5921
  }
5922
5922
  if (name === "c" || name === "C") {
5923
5923
  cleanup();
5924
- resolve5({ kind: "new" });
5924
+ resolve6({ kind: "new" });
5925
5925
  return;
5926
5926
  }
5927
5927
  if (name === "q" || name === "Q") {
5928
5928
  cleanup();
5929
- resolve5({ kind: "abort" });
5929
+ resolve6({ kind: "abort" });
5930
5930
  return;
5931
5931
  }
5932
5932
  if (name === "o" || name === "O") {
@@ -5976,7 +5976,7 @@ async function pickSession(term, opts) {
5976
5976
  if (session.agentId !== void 0) {
5977
5977
  result.agentId = session.agentId;
5978
5978
  }
5979
- resolve5(result);
5979
+ resolve6(result);
5980
5980
  return;
5981
5981
  }
5982
5982
  if ((name === "k" || name === "K") && selectedIdx > 0) {
@@ -6062,12 +6062,12 @@ async function pickSession(term, opts) {
6062
6062
  case "KP_ENTER": {
6063
6063
  cleanup();
6064
6064
  if (selectedIdx === 0) {
6065
- resolve5({ kind: "new" });
6065
+ resolve6({ kind: "new" });
6066
6066
  return;
6067
6067
  }
6068
6068
  const session = visible[selectedIdx - 1];
6069
6069
  if (!session) {
6070
- resolve5({ kind: "abort" });
6070
+ resolve6({ kind: "abort" });
6071
6071
  return;
6072
6072
  }
6073
6073
  const result = {
@@ -6077,14 +6077,14 @@ async function pickSession(term, opts) {
6077
6077
  if (session.agentId !== void 0) {
6078
6078
  result.agentId = session.agentId;
6079
6079
  }
6080
- resolve5(result);
6080
+ resolve6(result);
6081
6081
  return;
6082
6082
  }
6083
6083
  case "ESCAPE":
6084
6084
  case "CTRL_C":
6085
6085
  case "CTRL_D":
6086
6086
  cleanup();
6087
- resolve5({ kind: "abort" });
6087
+ resolve6({ kind: "abort" });
6088
6088
  return;
6089
6089
  }
6090
6090
  };
@@ -6185,10 +6185,184 @@ var init_picker = __esm({
6185
6185
  }
6186
6186
  });
6187
6187
 
6188
+ // src/core/cwd.ts
6189
+ import * as fs18 from "fs/promises";
6190
+ import * as path12 from "path";
6191
+ async function validateLocalCwd(input) {
6192
+ const trimmed = input.trim();
6193
+ if (trimmed.length === 0) {
6194
+ return { ok: false, reason: "path is empty" };
6195
+ }
6196
+ const resolved = path12.resolve(expandHome(trimmed));
6197
+ let stat5;
6198
+ try {
6199
+ stat5 = await fs18.stat(resolved);
6200
+ } catch {
6201
+ return { ok: false, reason: `${resolved} does not exist` };
6202
+ }
6203
+ if (!stat5.isDirectory()) {
6204
+ return { ok: false, reason: `${resolved} is not a directory` };
6205
+ }
6206
+ return { ok: true, path: resolved };
6207
+ }
6208
+ var init_cwd = __esm({
6209
+ "src/core/cwd.ts"() {
6210
+ "use strict";
6211
+ init_config();
6212
+ }
6213
+ });
6214
+
6215
+ // src/tui/import-cwd-prompt.ts
6216
+ import * as os4 from "os";
6217
+ async function promptForImportCwd(term, session, opts = {}) {
6218
+ const defaultCwd = opts.defaultCwd ?? os4.homedir();
6219
+ process.stdout.write("\x1B[<u");
6220
+ process.stdout.write("\x1B[?2004l");
6221
+ process.stdout.write("\x1B[>4;0m");
6222
+ process.stdout.write("\x1B[>5;0m");
6223
+ process.stdout.write("\x1B[?1000l");
6224
+ process.stdout.write("\x1B[?1002l");
6225
+ process.stdout.write("\x1B[?1006l");
6226
+ process.stdout.write("\x1B[?1l");
6227
+ process.stdout.write("\x1B>");
6228
+ const shortId2 = stripHydraSessionPrefix(session.sessionId);
6229
+ const fromMachine = session.importedFromMachine ?? "another machine";
6230
+ const originalCwd = session.cwd;
6231
+ let buffer = defaultCwd;
6232
+ let errorLine = null;
6233
+ let busy = false;
6234
+ const render = () => {
6235
+ term("\n");
6236
+ term.bold.cyan("Imported session: ");
6237
+ term(`${shortId2}
6238
+ `);
6239
+ term.dim(` from machine: `);
6240
+ term(`${fromMachine}
6241
+ `);
6242
+ term.dim(` original cwd: `);
6243
+ term(`${shortenHomePath(originalCwd)}
6244
+ `);
6245
+ term("\n");
6246
+ term(
6247
+ "This session has never been launched on this machine. Pick a local\n"
6248
+ );
6249
+ term("cwd for the agent (Enter to accept, Esc to cancel):\n\n");
6250
+ paintInput();
6251
+ if (errorLine) {
6252
+ term("\n");
6253
+ term.red(` ${errorLine}
6254
+ `);
6255
+ }
6256
+ };
6257
+ const paintInput = () => {
6258
+ term.bold("cwd: ");
6259
+ term(buffer);
6260
+ if (!busy) {
6261
+ term.bgWhite(" ");
6262
+ }
6263
+ };
6264
+ const repaintInput = () => {
6265
+ term.column(1);
6266
+ term.eraseLine();
6267
+ paintInput();
6268
+ if (errorLine !== null) {
6269
+ term("\n");
6270
+ term.eraseLine();
6271
+ term.red(` ${errorLine}`);
6272
+ term.up(1);
6273
+ term.column(1);
6274
+ }
6275
+ };
6276
+ render();
6277
+ return await new Promise((resolve6) => {
6278
+ let resolved = false;
6279
+ const cleanup = () => {
6280
+ if (resolved) {
6281
+ return;
6282
+ }
6283
+ resolved = true;
6284
+ term.off("key", onKey);
6285
+ term.grabInput(false);
6286
+ term.hideCursor(false);
6287
+ term("\n\n");
6288
+ };
6289
+ const finish = (value) => {
6290
+ cleanup();
6291
+ resolve6(value);
6292
+ };
6293
+ const onKey = (name, _matches, data) => {
6294
+ if (busy) {
6295
+ return;
6296
+ }
6297
+ if (name === "ENTER" || name === "KP_ENTER") {
6298
+ const candidate = buffer;
6299
+ busy = true;
6300
+ errorLine = null;
6301
+ repaintInput();
6302
+ void validateLocalCwd(candidate).then((result) => {
6303
+ busy = false;
6304
+ if (result.ok) {
6305
+ finish(result.path);
6306
+ return;
6307
+ }
6308
+ errorLine = result.reason;
6309
+ repaintInput();
6310
+ });
6311
+ return;
6312
+ }
6313
+ if (name === "ESCAPE" || name === "CTRL_C" || name === "CTRL_D") {
6314
+ finish(null);
6315
+ return;
6316
+ }
6317
+ if (name === "BACKSPACE") {
6318
+ if (buffer.length > 0) {
6319
+ buffer = buffer.slice(0, -1);
6320
+ errorLine = null;
6321
+ repaintInput();
6322
+ }
6323
+ return;
6324
+ }
6325
+ if (name === "CTRL_U") {
6326
+ buffer = "";
6327
+ errorLine = null;
6328
+ repaintInput();
6329
+ return;
6330
+ }
6331
+ if (name === "CTRL_W") {
6332
+ const trimmedRight = buffer.replace(/[/\s]+$/, "");
6333
+ const lastSep = Math.max(
6334
+ trimmedRight.lastIndexOf("/"),
6335
+ trimmedRight.lastIndexOf(" ")
6336
+ );
6337
+ buffer = lastSep >= 0 ? trimmedRight.slice(0, lastSep + 1) : "";
6338
+ errorLine = null;
6339
+ repaintInput();
6340
+ return;
6341
+ }
6342
+ if (data?.isCharacter) {
6343
+ buffer += name;
6344
+ errorLine = null;
6345
+ repaintInput();
6346
+ return;
6347
+ }
6348
+ };
6349
+ term.grabInput({});
6350
+ term.on("key", onKey);
6351
+ });
6352
+ }
6353
+ var init_import_cwd_prompt = __esm({
6354
+ "src/tui/import-cwd-prompt.ts"() {
6355
+ "use strict";
6356
+ init_paths();
6357
+ init_session();
6358
+ init_cwd();
6359
+ }
6360
+ });
6361
+
6188
6362
  // src/tui/attachments.ts
6189
- import path12 from "path";
6363
+ import path13 from "path";
6190
6364
  function mimeFromExtension(p) {
6191
- return EXTENSION_TO_MIME[path12.extname(p).toLowerCase()] ?? null;
6365
+ return EXTENSION_TO_MIME[path13.extname(p).toLowerCase()] ?? null;
6192
6366
  }
6193
6367
  function isSupportedImagePath(p) {
6194
6368
  return mimeFromExtension(p) !== null;
@@ -6832,6 +7006,8 @@ function mapKeyName(name) {
6832
7006
  return "ctrl-v";
6833
7007
  case "CTRL_W":
6834
7008
  return "ctrl-w";
7009
+ case "CTRL_X":
7010
+ return "ctrl-x";
6835
7011
  case "CTRL_Y":
6836
7012
  return "ctrl-y";
6837
7013
  case "ESCAPE":
@@ -7097,7 +7273,7 @@ var init_screen = __esm({
7097
7273
  this.onKey = opts.onKey;
7098
7274
  this.contentRepaintThrottleMs = opts.repaintThrottleMs ?? DEFAULT_CONTENT_REPAINT_THROTTLE_MS;
7099
7275
  this.maxScrollbackLines = opts.maxScrollbackLines ?? DEFAULT_MAX_SCROLLBACK_LINES;
7100
- this.mouseEnabled = opts.mouse ?? true;
7276
+ this.mouseEnabled = opts.mouse ?? false;
7101
7277
  this.progressIndicatorEnabled = opts.progressIndicator ?? true;
7102
7278
  this.readonly = opts.readonly ?? false;
7103
7279
  this.resizeHandler = () => this.repaint();
@@ -7614,6 +7790,57 @@ uncaught: ${err.stack ?? err.message}
7614
7790
  this.drawBanner();
7615
7791
  this.placeCursor();
7616
7792
  }
7793
+ // Runtime toggle for terminal mouse capture. With capture on, the
7794
+ // wheel drives scrollback but text selection requires shift+drag
7795
+ // (terminals route mouse events to the app). With capture off, plain
7796
+ // click-drag selects text but the wheel does nothing in the app —
7797
+ // use PgUp/PgDn for scrollback instead. Bound to ^X so users can
7798
+ // flip on demand without a config reload + restart. Idempotent.
7799
+ //
7800
+ // Re-issuing grabInput() reinstalls terminal-kit's own stdin "data"
7801
+ // listener, so we have to redo the same listener swap that
7802
+ // installBracketedPaste() did at startup — otherwise our raw handler
7803
+ // and terminal-kit's both fire for every keystroke (each character
7804
+ // appears twice in the prompt).
7805
+ setMouseEnabled(enabled) {
7806
+ if (this.mouseEnabled === enabled) {
7807
+ return;
7808
+ }
7809
+ this.mouseEnabled = enabled;
7810
+ if (!this.started) {
7811
+ return;
7812
+ }
7813
+ if (enabled) {
7814
+ this.term.grabInput({ mouse: "button" });
7815
+ this.term.on("mouse", this.mouseHandler);
7816
+ } else {
7817
+ this.term.off("mouse", this.mouseHandler);
7818
+ this.term.grabInput(true);
7819
+ }
7820
+ this.reclaimStdinAfterGrabInput();
7821
+ }
7822
+ // After a grabInput() re-issue, terminal-kit has put its own "data"
7823
+ // listener back on stdin. Pull it back off and reinstall hydra's
7824
+ // rawStdinHandler — keeping the captured terminal-kit handler so our
7825
+ // bracketed-paste extractor can still delegate non-paste bytes to it.
7826
+ // No-op if installBracketedPaste() hasn't run yet (start() does it
7827
+ // before any toggle path can reach here).
7828
+ reclaimStdinAfterGrabInput() {
7829
+ if (this.terminalKitStdinHandler === null) {
7830
+ return;
7831
+ }
7832
+ const t = this.term;
7833
+ if (!t.stdin || typeof t.onStdin !== "function") {
7834
+ return;
7835
+ }
7836
+ this.terminalKitStdinHandler = t.onStdin;
7837
+ t.stdin.removeListener("data", t.onStdin);
7838
+ t.stdin.removeListener("data", this.rawStdinHandler);
7839
+ t.stdin.on("data", this.rawStdinHandler);
7840
+ }
7841
+ isMouseEnabled() {
7842
+ return this.mouseEnabled;
7843
+ }
7617
7844
  // Pushed by the app each onKey tick to reflect prompt-history
7618
7845
  // reverse-search state in the banner — the only place that mode's
7619
7846
  // query is visible. Pass null when not searching.
@@ -9101,6 +9328,8 @@ var init_input = __esm({
9101
9328
  case "ctrl-w":
9102
9329
  this.killWord();
9103
9330
  return [];
9331
+ case "ctrl-x":
9332
+ return [{ type: "toggle-mouse" }];
9104
9333
  case "ctrl-y":
9105
9334
  this.yank();
9106
9335
  return [];
@@ -9683,9 +9912,9 @@ var init_input = __esm({
9683
9912
 
9684
9913
  // src/tui/clipboard.ts
9685
9914
  import { spawn as nodeSpawn } from "child_process";
9686
- import fs18 from "fs/promises";
9687
- import os4 from "os";
9688
- import path13 from "path";
9915
+ import fs19 from "fs/promises";
9916
+ import os5 from "os";
9917
+ import path14 from "path";
9689
9918
  async function readClipboard(envIn = {}) {
9690
9919
  const env = { ...defaultEnv, ...envIn };
9691
9920
  if (env.platform === "darwin") {
@@ -9700,7 +9929,7 @@ async function readClipboard(envIn = {}) {
9700
9929
  };
9701
9930
  }
9702
9931
  async function readMacOS(env) {
9703
- const tmpPath = path13.join(
9932
+ const tmpPath = path14.join(
9704
9933
  env.tmpdir(),
9705
9934
  `hydra-clipboard-${Date.now()}-${process.pid}.png`
9706
9935
  );
@@ -9724,7 +9953,7 @@ async function readMacOS(env) {
9724
9953
  return img;
9725
9954
  }
9726
9955
  } catch {
9727
- await fs18.unlink(tmpPath).catch(() => void 0);
9956
+ await fs19.unlink(tmpPath).catch(() => void 0);
9728
9957
  }
9729
9958
  try {
9730
9959
  const buf = await runCapture(env.spawn, "pbpaste", []);
@@ -9839,9 +10068,9 @@ async function which(env, cmd) {
9839
10068
  }
9840
10069
  async function readFileAsAttachment(p, unlinkAfter) {
9841
10070
  try {
9842
- const buf = await fs18.readFile(p);
10071
+ const buf = await fs19.readFile(p);
9843
10072
  if (unlinkAfter) {
9844
- await fs18.unlink(p).catch(() => void 0);
10073
+ await fs19.unlink(p).catch(() => void 0);
9845
10074
  }
9846
10075
  if (buf.length === 0) {
9847
10076
  return { ok: false, reason: "no image on clipboard" };
@@ -9867,14 +10096,14 @@ async function readFileAsAttachment(p, unlinkAfter) {
9867
10096
  }
9868
10097
  }
9869
10098
  function run2(spawn6, cmd, args) {
9870
- return new Promise((resolve5, reject) => {
10099
+ return new Promise((resolve6, reject) => {
9871
10100
  const proc = spawn6(cmd, args);
9872
10101
  proc.stdout?.on("data", () => void 0);
9873
10102
  proc.stderr?.on("data", () => void 0);
9874
10103
  proc.on("error", reject);
9875
10104
  proc.on("close", (code) => {
9876
10105
  if (code === 0) {
9877
- resolve5();
10106
+ resolve6();
9878
10107
  } else {
9879
10108
  reject(new Error(`${cmd} exited ${code}`));
9880
10109
  }
@@ -9882,7 +10111,7 @@ function run2(spawn6, cmd, args) {
9882
10111
  });
9883
10112
  }
9884
10113
  function runCapture(spawn6, cmd, args) {
9885
- return new Promise((resolve5, reject) => {
10114
+ return new Promise((resolve6, reject) => {
9886
10115
  const proc = spawn6(cmd, args);
9887
10116
  const chunks = [];
9888
10117
  let stdoutEnded = proc.stdout === null;
@@ -9894,7 +10123,7 @@ function runCapture(spawn6, cmd, args) {
9894
10123
  }
9895
10124
  settled = true;
9896
10125
  if (closedCode === 0) {
9897
- resolve5(Buffer.concat(chunks));
10126
+ resolve6(Buffer.concat(chunks));
9898
10127
  } else {
9899
10128
  reject(new Error(`${cmd} exited ${closedCode}`));
9900
10129
  }
@@ -9929,7 +10158,7 @@ var init_clipboard = __esm({
9929
10158
  platform: process.platform,
9930
10159
  env: process.env,
9931
10160
  spawn: nodeSpawn,
9932
- tmpdir: os4.tmpdir
10161
+ tmpdir: os5.tmpdir
9933
10162
  };
9934
10163
  SUPPORTED_IMAGE_MIMES = [
9935
10164
  "image/png",
@@ -10446,8 +10675,8 @@ var init_format = __esm({
10446
10675
  import { appendFileSync, statSync, renameSync } from "fs";
10447
10676
  import { nanoid as nanoid3 } from "nanoid";
10448
10677
  import termkit from "terminal-kit";
10449
- import fs19 from "fs/promises";
10450
- import path14 from "path";
10678
+ import fs20 from "fs/promises";
10679
+ import path15 from "path";
10451
10680
  function isReadonlyForbiddenEffect(effect) {
10452
10681
  switch (effect.type) {
10453
10682
  case "send":
@@ -10775,10 +11004,10 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
10775
11004
  if (pendingPermission.toolCallId && toolCallId && pendingPermission.toolCallId !== toolCallId) {
10776
11005
  return;
10777
11006
  }
10778
- const resolve5 = pendingPermission.resolve;
11007
+ const resolve6 = pendingPermission.resolve;
10779
11008
  pendingPermission = null;
10780
11009
  screen.setPermissionPrompt(null);
10781
- resolve5(result ?? { outcome: { outcome: "cancelled" } });
11010
+ resolve6(result ?? { outcome: { outcome: "cancelled" } });
10782
11011
  };
10783
11012
  const maybeDismissPermissionByToolUpdate = (update) => {
10784
11013
  if (!pendingPermission?.toolCallId) {
@@ -10811,14 +11040,14 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
10811
11040
  if (!pendingPermission) {
10812
11041
  return;
10813
11042
  }
10814
- const { options, resolve: resolve5 } = pendingPermission;
11043
+ const { options, resolve: resolve6 } = pendingPermission;
10815
11044
  pendingPermission = null;
10816
11045
  screen.setPermissionPrompt(null);
10817
11046
  if (optionId === null) {
10818
- resolve5({ outcome: { outcome: "cancelled" } });
11047
+ resolve6({ outcome: { outcome: "cancelled" } });
10819
11048
  return;
10820
11049
  }
10821
- resolve5({ outcome: { outcome: "selected", optionId } });
11050
+ resolve6({ outcome: { outcome: "selected", optionId } });
10822
11051
  void options;
10823
11052
  };
10824
11053
  conn.onRequest("session/request_permission", async (params) => {
@@ -10845,12 +11074,12 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
10845
11074
  ]);
10846
11075
  return { outcome: { outcome: "cancelled" } };
10847
11076
  }
10848
- return new Promise((resolve5) => {
11077
+ return new Promise((resolve6) => {
10849
11078
  pendingPermission = {
10850
11079
  title,
10851
11080
  options,
10852
11081
  selectedIndex: 0,
10853
- resolve: resolve5,
11082
+ resolve: resolve6,
10854
11083
  toolCallId
10855
11084
  };
10856
11085
  refreshPermissionPrompt();
@@ -10939,7 +11168,23 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
10939
11168
  sessionId: ctx.sessionId,
10940
11169
  historyPolicy: "full",
10941
11170
  clientInfo: { name: "hydra-acp-tui", version: HYDRA_VERSION },
10942
- ...opts.readonly === true ? { readonly: true } : {}
11171
+ ...opts.readonly === true ? { readonly: true } : {},
11172
+ // Forward the user-chosen cwd for first-launch imported sessions
11173
+ // via a full resume hint. upstreamSessionId is empty so the
11174
+ // daemon routes through doResurrectFromImport (session-manager.ts)
11175
+ // with the user-supplied cwd instead of silently falling back to
11176
+ // $HOME in resolveImportCwd.
11177
+ ...ctx.importAttachHint !== void 0 ? {
11178
+ _meta: {
11179
+ [HYDRA_META_KEY]: {
11180
+ resume: {
11181
+ upstreamSessionId: "",
11182
+ agentId: ctx.importAttachHint.agentId,
11183
+ cwd: ctx.importAttachHint.cwd
11184
+ }
11185
+ }
11186
+ }
11187
+ } : {}
10943
11188
  });
10944
11189
  resolvedSessionId = attached.sessionId;
10945
11190
  if (attached.clientId) {
@@ -11200,8 +11445,8 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
11200
11445
  }
11201
11446
  });
11202
11447
  let finishSession = null;
11203
- const sessionDone = new Promise((resolve5) => {
11204
- finishSession = resolve5;
11448
+ const sessionDone = new Promise((resolve6) => {
11449
+ finishSession = resolve6;
11205
11450
  });
11206
11451
  const cancelRemoteTurn = () => {
11207
11452
  conn.notify("session/cancel", { sessionId: resolvedSessionId }).catch(() => void 0);
@@ -11518,6 +11763,14 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
11518
11763
  toolsExpanded = !toolsExpanded;
11519
11764
  renderToolsBlock();
11520
11765
  return;
11766
+ case "toggle-mouse": {
11767
+ const next = !screen.isMouseEnabled();
11768
+ screen.setMouseEnabled(next);
11769
+ screen.notify(
11770
+ next ? "mouse capture on \u2014 wheel scrolls; shift+drag to select text" : "mouse capture off \u2014 click-drag selects text; PgUp/PgDn scrolls"
11771
+ );
11772
+ return;
11773
+ }
11521
11774
  case "show-help":
11522
11775
  toggleHelpModal();
11523
11776
  return;
@@ -11560,11 +11813,11 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
11560
11813
  }
11561
11814
  const mimeType = mimeFromExtension(token);
11562
11815
  if (!mimeType) {
11563
- screen.notify(`unsupported image type: ${path14.basename(token)}`);
11816
+ screen.notify(`unsupported image type: ${path15.basename(token)}`);
11564
11817
  continue;
11565
11818
  }
11566
11819
  try {
11567
- const buf = await fs19.readFile(token);
11820
+ const buf = await fs20.readFile(token);
11568
11821
  if (buf.length > MAX_ATTACHMENT_BYTES) {
11569
11822
  screen.notify(
11570
11823
  `image too large (${formatSize(buf.length)}, max ${formatSize(MAX_ATTACHMENT_BYTES)})`
@@ -11574,13 +11827,13 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
11574
11827
  dispatcher.addAttachment({
11575
11828
  mimeType,
11576
11829
  data: buf.toString("base64"),
11577
- name: path14.basename(token),
11830
+ name: path15.basename(token),
11578
11831
  sizeBytes: buf.length
11579
11832
  });
11580
11833
  added++;
11581
11834
  } catch (err) {
11582
11835
  screen.notify(
11583
- `cannot read ${path14.basename(token)}: ${err.message}`
11836
+ `cannot read ${path15.basename(token)}: ${err.message}`
11584
11837
  );
11585
11838
  }
11586
11839
  }
@@ -12297,10 +12550,10 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
12297
12550
  }
12298
12551
  const resetInFlightUiState = () => {
12299
12552
  if (pendingPermission) {
12300
- const resolve5 = pendingPermission.resolve;
12553
+ const resolve6 = pendingPermission.resolve;
12301
12554
  pendingPermission = null;
12302
12555
  screen.setPermissionPrompt(null);
12303
- resolve5({ outcome: { outcome: "cancelled" } });
12556
+ resolve6({ outcome: { outcome: "cancelled" } });
12304
12557
  }
12305
12558
  closeAgentText();
12306
12559
  };
@@ -12465,6 +12718,21 @@ async function resolveSession(term, config, serviceToken, opts) {
12465
12718
  return newCtx(opts, cwd, config);
12466
12719
  }
12467
12720
  opts.readonly = choice.readonly === true;
12721
+ const chosen = sessions.find((s) => s.sessionId === choice.sessionId);
12722
+ const isImportedFirstLaunch = chosen !== void 0 && !!chosen.importedFromMachine && !chosen.upstreamSessionId && !opts.readonly;
12723
+ if (isImportedFirstLaunch) {
12724
+ const promptedCwd = await promptForImportCwd(term, chosen);
12725
+ if (promptedCwd === null) {
12726
+ return null;
12727
+ }
12728
+ const agentId = choice.agentId ?? chosen.agentId ?? "";
12729
+ return {
12730
+ sessionId: choice.sessionId,
12731
+ agentId,
12732
+ cwd: promptedCwd,
12733
+ importAttachHint: { agentId, cwd: promptedCwd }
12734
+ };
12735
+ }
12468
12736
  return {
12469
12737
  sessionId: choice.sessionId,
12470
12738
  agentId: choice.agentId ?? "",
@@ -12593,8 +12861,8 @@ function createInstallStatusLine(term, baseLabel) {
12593
12861
  }
12594
12862
  function rotateIfBig(target) {
12595
12863
  try {
12596
- const stat4 = statSync(target);
12597
- if (stat4.size < logMaxBytes) {
12864
+ const stat5 = statSync(target);
12865
+ if (stat5.size < logMaxBytes) {
12598
12866
  return;
12599
12867
  }
12600
12868
  renameSync(target, `${target}.0`);
@@ -12618,6 +12886,7 @@ var init_app = __esm({
12618
12886
  init_history();
12619
12887
  init_discovery();
12620
12888
  init_picker();
12889
+ init_import_cwd_prompt();
12621
12890
  init_screen();
12622
12891
  init_input();
12623
12892
  init_attachments();
@@ -12647,6 +12916,7 @@ var init_app = __esm({
12647
12916
  ["^R / ^S", "history reverse / forward search"],
12648
12917
  ["PgUp / PgDn", "scroll scrollback"],
12649
12918
  ["Mouse wheel", "scroll scrollback (when mouse capture is on)"],
12919
+ ["^X", "toggle mouse capture (wheel scroll vs. text selection)"],
12650
12920
  null,
12651
12921
  ["^C", "cancel turn (twice to exit)"],
12652
12922
  ["Esc", "cancel turn and prefill draft"],
@@ -12673,11 +12943,12 @@ var init_tui = __esm({
12673
12943
  // src/cli.ts
12674
12944
  import { readFileSync as readFileSync2 } from "fs";
12675
12945
  import { fileURLToPath as fileURLToPath2 } from "url";
12676
- import { dirname as dirname6, resolve as resolve4 } from "path";
12946
+ import { dirname as dirname6, resolve as resolve5 } from "path";
12677
12947
 
12678
12948
  // src/cli/parse-args.ts
12679
12949
  var KNOWN_BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
12680
12950
  "all",
12951
+ "detach",
12681
12952
  "foreground",
12682
12953
  "help",
12683
12954
  "info",
@@ -12971,10 +13242,10 @@ async function downloadTo(args) {
12971
13242
  logSink(formatProgress(args.agentId, received, total));
12972
13243
  }
12973
13244
  });
12974
- await new Promise((resolve5, reject) => {
13245
+ await new Promise((resolve6, reject) => {
12975
13246
  nodeStream.on("error", reject);
12976
13247
  out.on("error", reject);
12977
- out.on("finish", () => resolve5());
13248
+ out.on("finish", () => resolve6());
12978
13249
  nodeStream.pipe(out);
12979
13250
  });
12980
13251
  logSink(formatProgress(
@@ -13026,14 +13297,14 @@ async function extract(archivePath, dest) {
13026
13297
  throw new Error(`Unsupported archive format: ${archivePath}`);
13027
13298
  }
13028
13299
  function run(cmd, args) {
13029
- return new Promise((resolve5, reject) => {
13300
+ return new Promise((resolve6, reject) => {
13030
13301
  const child = spawn(cmd, args, {
13031
13302
  stdio: ["ignore", "ignore", "inherit"]
13032
13303
  });
13033
13304
  child.on("error", reject);
13034
13305
  child.on("exit", (code, signal) => {
13035
13306
  if (code === 0) {
13036
- resolve5();
13307
+ resolve6();
13037
13308
  return;
13038
13309
  }
13039
13310
  reject(
@@ -13045,11 +13316,11 @@ function run(cmd, args) {
13045
13316
  });
13046
13317
  }
13047
13318
  async function hasCommand(name) {
13048
- return new Promise((resolve5) => {
13319
+ return new Promise((resolve6) => {
13049
13320
  const finder = process.platform === "win32" ? "where" : "which";
13050
13321
  const child = spawn(finder, [name], { stdio: "ignore" });
13051
- child.on("error", () => resolve5(false));
13052
- child.on("exit", (code) => resolve5(code === 0));
13322
+ child.on("error", () => resolve6(false));
13323
+ child.on("exit", (code) => resolve6(code === 0));
13053
13324
  });
13054
13325
  }
13055
13326
  async function fileExists(p) {
@@ -13167,7 +13438,7 @@ function runNpmInstall(args) {
13167
13438
  }
13168
13439
  async function runNpmInstallOnce(args, attempt) {
13169
13440
  try {
13170
- await new Promise((resolve5, reject) => {
13441
+ await new Promise((resolve6, reject) => {
13171
13442
  const registryArgs = args.registry ? ["--registry", args.registry] : [];
13172
13443
  let child;
13173
13444
  try {
@@ -13209,7 +13480,7 @@ async function runNpmInstallOnce(args, attempt) {
13209
13480
  });
13210
13481
  child.on("exit", (code, signal) => {
13211
13482
  if (code === 0) {
13212
- resolve5();
13483
+ resolve6();
13213
13484
  return;
13214
13485
  }
13215
13486
  const reason = code !== null ? `exit code ${code}` : `signal ${signal ?? "unknown"}`;
@@ -13537,13 +13808,13 @@ function ndjsonStreamFromStdio(stdout, stdin) {
13537
13808
  throw new Error("stream is closed");
13538
13809
  }
13539
13810
  const line = JSON.stringify(message) + "\n";
13540
- await new Promise((resolve5, reject) => {
13811
+ await new Promise((resolve6, reject) => {
13541
13812
  stdin.write(line, (err) => {
13542
13813
  if (err) {
13543
13814
  reject(err);
13544
13815
  return;
13545
13816
  }
13546
- resolve5();
13817
+ resolve6();
13547
13818
  });
13548
13819
  });
13549
13820
  },
@@ -14058,8 +14329,8 @@ var SessionManager = class {
14058
14329
  }
14059
14330
  async resolveImportCwd(cwd) {
14060
14331
  try {
14061
- const stat4 = await fs11.stat(cwd);
14062
- if (stat4.isDirectory()) {
14332
+ const stat5 = await fs11.stat(cwd);
14333
+ if (stat5.isDirectory()) {
14063
14334
  return cwd;
14064
14335
  }
14065
14336
  } catch {
@@ -14982,9 +15253,9 @@ var ExtensionManager = class {
14982
15253
  } catch {
14983
15254
  }
14984
15255
  tasks.push(
14985
- new Promise((resolve5) => {
15256
+ new Promise((resolve6) => {
14986
15257
  if (child.exitCode !== null || child.signalCode !== null) {
14987
- resolve5();
15258
+ resolve6();
14988
15259
  return;
14989
15260
  }
14990
15261
  const timer = setTimeout(() => {
@@ -14992,11 +15263,11 @@ var ExtensionManager = class {
14992
15263
  child.kill("SIGKILL");
14993
15264
  } catch {
14994
15265
  }
14995
- resolve5();
15266
+ resolve6();
14996
15267
  }, STOP_GRACE_MS);
14997
15268
  child.on("exit", () => {
14998
15269
  clearTimeout(timer);
14999
- resolve5();
15270
+ resolve6();
15000
15271
  });
15001
15272
  })
15002
15273
  );
@@ -15104,8 +15375,8 @@ var ExtensionManager = class {
15104
15375
  if (child.exitCode !== null || child.signalCode !== null) {
15105
15376
  return;
15106
15377
  }
15107
- const exited = new Promise((resolve5) => {
15108
- entry.exitWaiters.push(resolve5);
15378
+ const exited = new Promise((resolve6) => {
15379
+ entry.exitWaiters.push(resolve6);
15109
15380
  });
15110
15381
  try {
15111
15382
  child.kill("SIGTERM");
@@ -15302,8 +15573,8 @@ var ExtensionManager = class {
15302
15573
  entry.pid = void 0;
15303
15574
  entry.lastExitCode = typeof code === "number" ? code : void 0;
15304
15575
  const waiters = entry.exitWaiters.splice(0);
15305
- for (const resolve5 of waiters) {
15306
- resolve5();
15576
+ for (const resolve6 of waiters) {
15577
+ resolve6();
15307
15578
  }
15308
15579
  if (this.stopping || entry.manuallyStopped) {
15309
15580
  try {
@@ -17174,9 +17445,9 @@ import * as fs16 from "fs";
17174
17445
  import * as fsp6 from "fs/promises";
17175
17446
  async function runLogTail(logPath, argv, notFoundMessage) {
17176
17447
  const opts = parseLogTailFlags(argv);
17177
- let stat4;
17448
+ let stat5;
17178
17449
  try {
17179
- stat4 = await fsp6.stat(logPath);
17450
+ stat5 = await fsp6.stat(logPath);
17180
17451
  } catch (err) {
17181
17452
  const e = err;
17182
17453
  if (e.code === "ENOENT") {
@@ -17187,7 +17458,7 @@ async function runLogTail(logPath, argv, notFoundMessage) {
17187
17458
  }
17188
17459
  throw err;
17189
17460
  }
17190
- let position = await printTail(logPath, stat4.size, opts.tail);
17461
+ let position = await printTail(logPath, stat5.size, opts.tail);
17191
17462
  if (!opts.follow) {
17192
17463
  return;
17193
17464
  }
@@ -17222,10 +17493,10 @@ async function runLogTail(logPath, argv, notFoundMessage) {
17222
17493
  }
17223
17494
  });
17224
17495
  });
17225
- await new Promise((resolve5) => {
17496
+ await new Promise((resolve6) => {
17226
17497
  const finish = () => {
17227
17498
  watcher.close();
17228
- resolve5();
17499
+ resolve6();
17229
17500
  };
17230
17501
  process.once("SIGINT", finish);
17231
17502
  process.once("SIGTERM", finish);
@@ -17305,6 +17576,7 @@ async function runDaemonStart(flags = {}) {
17305
17576
  return;
17306
17577
  }
17307
17578
  if (flagBool(flags, "foreground")) {
17579
+ process.title = "hydra-daemon";
17308
17580
  const handle = await startDaemon(config, serviceToken);
17309
17581
  process.stdout.write(
17310
17582
  `hydra-acp daemon listening on ${config.daemon.host}:${config.daemon.port}
@@ -18031,7 +18303,7 @@ async function promptPassword(prompt) {
18031
18303
  if (!process.stdin.isTTY) {
18032
18304
  return readLineFromStdin();
18033
18305
  }
18034
- return new Promise((resolve5, reject) => {
18306
+ return new Promise((resolve6, reject) => {
18035
18307
  const stdin = process.stdin;
18036
18308
  const wasRaw = stdin.isRaw === true;
18037
18309
  let buffer = "";
@@ -18048,7 +18320,7 @@ async function promptPassword(prompt) {
18048
18320
  if (byte === 10 || byte === 13) {
18049
18321
  process.stdout.write("\n");
18050
18322
  cleanup();
18051
- resolve5(buffer);
18323
+ resolve6(buffer);
18052
18324
  return;
18053
18325
  }
18054
18326
  if (byte === 3) {
@@ -18074,7 +18346,7 @@ async function promptPassword(prompt) {
18074
18346
  });
18075
18347
  }
18076
18348
  function readLineFromStdin() {
18077
- return new Promise((resolve5, reject) => {
18349
+ return new Promise((resolve6, reject) => {
18078
18350
  let buffer = "";
18079
18351
  process.stdin.setEncoding("utf8");
18080
18352
  const onData = (chunk) => {
@@ -18083,7 +18355,7 @@ function readLineFromStdin() {
18083
18355
  if (nl !== -1) {
18084
18356
  process.stdin.removeListener("data", onData);
18085
18357
  process.stdin.removeListener("error", onError);
18086
- resolve5(buffer.slice(0, nl).replace(/\r$/, ""));
18358
+ resolve6(buffer.slice(0, nl).replace(/\r$/, ""));
18087
18359
  }
18088
18360
  };
18089
18361
  const onError = (err) => {
@@ -18385,8 +18657,34 @@ function isResponse2(msg) {
18385
18657
  return !("method" in msg) && "id" in msg;
18386
18658
  }
18387
18659
 
18660
+ // src/core/process-title.ts
18661
+ import { writeFileSync as writeFileSync2 } from "fs";
18662
+ var COMM_ANCHOR = "hydra";
18663
+ var defaultWriteComm = (text) => {
18664
+ writeFileSync2("/proc/self/comm", text);
18665
+ };
18666
+ function setHydraProcessTitle(fullTitle, deps = {}) {
18667
+ process.title = fullTitle;
18668
+ const platform = deps.platform ?? process.platform;
18669
+ if (platform !== "linux") {
18670
+ return;
18671
+ }
18672
+ const writeComm = deps.writeComm ?? defaultWriteComm;
18673
+ try {
18674
+ writeComm(COMM_ANCHOR);
18675
+ } catch {
18676
+ }
18677
+ }
18678
+ function buildTitleFromArgv(argv) {
18679
+ if (argv.length === 0) {
18680
+ return COMM_ANCHOR;
18681
+ }
18682
+ return `${COMM_ANCHOR} ${argv.join(" ")}`;
18683
+ }
18684
+
18388
18685
  // src/shim/proxy.ts
18389
18686
  async function runShim(opts) {
18687
+ setHydraProcessTitle(buildTitleFromArgv(process.argv.slice(2)));
18390
18688
  const config = await loadConfig();
18391
18689
  const serviceToken = await ensureServiceToken();
18392
18690
  await ensureDaemonReachable(config);
@@ -18626,6 +18924,309 @@ function injectHydraMeta(msg, additions) {
18626
18924
  };
18627
18925
  }
18628
18926
 
18927
+ // src/cli/commands/cat.ts
18928
+ init_connection();
18929
+ init_ws_stream();
18930
+ init_config();
18931
+ init_service_token();
18932
+ init_daemon_bootstrap();
18933
+ init_render_update();
18934
+ init_types();
18935
+ init_hydra_version();
18936
+ import { WebSocket as WebSocket2 } from "ws";
18937
+
18938
+ // src/cli/commands/cat-chunker.ts
18939
+ function createChunker(opts) {
18940
+ let buffer = "";
18941
+ let dataArrivedSinceSchedule = false;
18942
+ let scheduled = false;
18943
+ let ended = false;
18944
+ const flush = () => {
18945
+ if (buffer.length === 0) {
18946
+ return;
18947
+ }
18948
+ const out = buffer;
18949
+ buffer = "";
18950
+ opts.onChunk(out);
18951
+ };
18952
+ const scheduleCheck = () => {
18953
+ if (scheduled) {
18954
+ return;
18955
+ }
18956
+ scheduled = true;
18957
+ dataArrivedSinceSchedule = false;
18958
+ opts.scheduleFlushCheck(() => {
18959
+ scheduled = false;
18960
+ if (dataArrivedSinceSchedule) {
18961
+ scheduleCheck();
18962
+ return;
18963
+ }
18964
+ flush();
18965
+ });
18966
+ };
18967
+ return {
18968
+ feed(data) {
18969
+ if (ended || data.length === 0) {
18970
+ return;
18971
+ }
18972
+ buffer += data;
18973
+ if (scheduled) {
18974
+ dataArrivedSinceSchedule = true;
18975
+ } else {
18976
+ scheduleCheck();
18977
+ }
18978
+ },
18979
+ eof() {
18980
+ if (ended) {
18981
+ return;
18982
+ }
18983
+ ended = true;
18984
+ flush();
18985
+ }
18986
+ };
18987
+ }
18988
+
18989
+ // src/cli/commands/cat.ts
18990
+ async function runCat(opts) {
18991
+ setHydraProcessTitle(buildTitleFromArgv(process.argv.slice(2)));
18992
+ if (process.stdin.isTTY && !opts.prompt && !opts.sessionId) {
18993
+ process.stderr.write(
18994
+ "hydra-acp cat: nothing to send. Pipe input on stdin, pass -p <text>, or attach to an existing session with --session-id.\n"
18995
+ );
18996
+ process.exit(2);
18997
+ return;
18998
+ }
18999
+ const config = await loadConfig();
19000
+ const serviceToken = await ensureServiceToken();
19001
+ await ensureDaemonReachable(config);
19002
+ const protocol = config.daemon.tls ? "wss" : "ws";
19003
+ const url = `${protocol}://${config.daemon.host}:${config.daemon.port}/acp`;
19004
+ const subprotocols = ["acp.v1", `hydra-acp-token.${serviceToken}`];
19005
+ const ws = await openWs2(url, subprotocols);
19006
+ const stream = wsToMessageStream(ws);
19007
+ const conn = new JsonRpcConnection(stream);
19008
+ const result = await runCatLoop({
19009
+ conn,
19010
+ opts,
19011
+ stdin: process.stdin,
19012
+ stdinIsTty: process.stdin.isTTY === true,
19013
+ stdout: (chunk) => process.stdout.write(chunk),
19014
+ stderr: (chunk) => {
19015
+ process.stderr.write(chunk);
19016
+ }
19017
+ });
19018
+ process.exit(result.exitCode);
19019
+ }
19020
+ async function runCatLoop(args) {
19021
+ const { conn, opts, stdin, stdinIsTty, stdout, stderr } = args;
19022
+ conn.setDefaultHandler(async () => {
19023
+ return { error: { code: -32601, message: "method not implemented" } };
19024
+ });
19025
+ try {
19026
+ await conn.request("initialize", {
19027
+ protocolVersion: ACP_PROTOCOL_VERSION,
19028
+ clientCapabilities: {
19029
+ fs: { readTextFile: false, writeTextFile: false },
19030
+ terminal: false
19031
+ },
19032
+ clientInfo: { name: "hydra-acp-cat", version: HYDRA_VERSION }
19033
+ });
19034
+ } catch {
19035
+ }
19036
+ const sessionId = await openOrAttachSession(conn, opts);
19037
+ let turnHadOutput = false;
19038
+ let lastCharWasNewline = true;
19039
+ const writeStdout = (text) => {
19040
+ if (text.length === 0) {
19041
+ return;
19042
+ }
19043
+ stdout(text);
19044
+ lastCharWasNewline = text.charCodeAt(text.length - 1) === 10;
19045
+ };
19046
+ const finalizeTurn = () => {
19047
+ if (turnHadOutput && !lastCharWasNewline) {
19048
+ writeStdout("\n");
19049
+ }
19050
+ turnHadOutput = false;
19051
+ };
19052
+ conn.onNotification("session/update", (params) => {
19053
+ const update = params?.update;
19054
+ const event = mapUpdate(update);
19055
+ if (!event) {
19056
+ return;
19057
+ }
19058
+ if (event.kind === "agent-text") {
19059
+ turnHadOutput = true;
19060
+ writeStdout(event.text);
19061
+ } else if (event.kind === "turn-complete") {
19062
+ finalizeTurn();
19063
+ }
19064
+ });
19065
+ const sendChunk = async (text) => {
19066
+ const promptBlocks = [];
19067
+ if (opts.prompt) {
19068
+ promptBlocks.push({ type: "text", text: opts.prompt });
19069
+ }
19070
+ if (text.length > 0) {
19071
+ promptBlocks.push({ type: "text", text });
19072
+ }
19073
+ if (promptBlocks.length === 0) {
19074
+ return;
19075
+ }
19076
+ try {
19077
+ await conn.request("session/prompt", {
19078
+ sessionId,
19079
+ prompt: promptBlocks
19080
+ });
19081
+ } catch (err) {
19082
+ stderr(`hydra-acp cat: prompt failed: ${err.message}
19083
+ `);
19084
+ return;
19085
+ }
19086
+ finalizeTurn();
19087
+ };
19088
+ let exitCode = 0;
19089
+ let resolveDone;
19090
+ const done = new Promise((resolve6) => {
19091
+ resolveDone = resolve6;
19092
+ });
19093
+ let settled = false;
19094
+ const settle = async (code) => {
19095
+ if (settled) {
19096
+ return;
19097
+ }
19098
+ settled = true;
19099
+ if (!opts.detach) {
19100
+ conn.request("session/detach", { sessionId }).catch(() => void 0);
19101
+ }
19102
+ await conn.close().catch(() => void 0);
19103
+ resolveDone({ exitCode: code });
19104
+ };
19105
+ conn.onClose((err) => {
19106
+ if (err) {
19107
+ stderr(`hydra-acp cat: ${err.message}
19108
+ `);
19109
+ exitCode = 1;
19110
+ }
19111
+ if (!settled) {
19112
+ settled = true;
19113
+ resolveDone({ exitCode });
19114
+ }
19115
+ });
19116
+ const chunkQueue = [];
19117
+ let draining = false;
19118
+ let stdinEnded = false;
19119
+ const drainQueue = async () => {
19120
+ if (draining) {
19121
+ return;
19122
+ }
19123
+ draining = true;
19124
+ try {
19125
+ while (chunkQueue.length > 0) {
19126
+ const next = chunkQueue.shift();
19127
+ if (next === void 0) {
19128
+ break;
19129
+ }
19130
+ await sendChunk(next);
19131
+ }
19132
+ } finally {
19133
+ draining = false;
19134
+ if (stdinEnded && chunkQueue.length === 0) {
19135
+ await settle(exitCode);
19136
+ }
19137
+ }
19138
+ };
19139
+ const chunker = createChunker({
19140
+ // setImmediate fires in the libuv "check" phase, after pending
19141
+ // I/O has been polled and any back-to-back "data" events have
19142
+ // been emitted. That makes it the natural hook for "the writer
19143
+ // has paused, time to flush": if more bytes were sitting in the
19144
+ // pipe buffer, Node would have emitted another "data" event
19145
+ // before this fires, and the chunker would detect that and defer.
19146
+ scheduleFlushCheck: (cb) => {
19147
+ const h = setImmediate(cb);
19148
+ return () => clearImmediate(h);
19149
+ },
19150
+ onChunk: (text) => {
19151
+ chunkQueue.push(text);
19152
+ void drainQueue();
19153
+ }
19154
+ });
19155
+ if (stdinIsTty && !opts.sessionId) {
19156
+ if (opts.prompt) {
19157
+ await sendChunk("");
19158
+ }
19159
+ await settle(0);
19160
+ return done;
19161
+ }
19162
+ if (typeof stdin.setEncoding === "function") {
19163
+ stdin.setEncoding("utf8");
19164
+ }
19165
+ stdin.on("data", (data) => {
19166
+ chunker.feed(typeof data === "string" ? data : data.toString("utf8"));
19167
+ });
19168
+ stdin.on("end", () => {
19169
+ chunker.eof();
19170
+ stdinEnded = true;
19171
+ if (!draining && chunkQueue.length === 0) {
19172
+ void settle(exitCode);
19173
+ }
19174
+ });
19175
+ stdin.on("error", (err) => {
19176
+ stderr(`hydra-acp cat: stdin error: ${err.message}
19177
+ `);
19178
+ exitCode = 1;
19179
+ stdinEnded = true;
19180
+ if (!draining && chunkQueue.length === 0) {
19181
+ void settle(exitCode);
19182
+ }
19183
+ });
19184
+ return done;
19185
+ }
19186
+ async function openOrAttachSession(conn, opts) {
19187
+ if (opts.sessionId) {
19188
+ const attached = await conn.request("session/attach", {
19189
+ sessionId: opts.sessionId,
19190
+ historyPolicy: "pending_only",
19191
+ clientInfo: { name: "hydra-acp-cat", version: HYDRA_VERSION }
19192
+ });
19193
+ return attached.sessionId;
19194
+ }
19195
+ const hydraMeta = {};
19196
+ if (opts.name) {
19197
+ hydraMeta.name = opts.name;
19198
+ }
19199
+ if (opts.model) {
19200
+ hydraMeta.model = opts.model;
19201
+ }
19202
+ const cwd = opts.cwd ?? process.cwd();
19203
+ const params = { cwd };
19204
+ if (opts.agentId) {
19205
+ params.agentId = opts.agentId;
19206
+ }
19207
+ if (Object.keys(hydraMeta).length > 0) {
19208
+ params._meta = { [HYDRA_META_KEY]: hydraMeta };
19209
+ }
19210
+ const created = await conn.request("session/new", params);
19211
+ void extractHydraMeta(created._meta);
19212
+ return created.sessionId;
19213
+ }
19214
+ async function openWs2(url, subprotocols) {
19215
+ return new Promise((resolve6, reject) => {
19216
+ const ws = new WebSocket2(url, subprotocols);
19217
+ const onOpen = () => {
19218
+ ws.off("error", onError);
19219
+ resolve6(ws);
19220
+ };
19221
+ const onError = (err) => {
19222
+ ws.off("open", onOpen);
19223
+ reject(err);
19224
+ };
19225
+ ws.once("open", onOpen);
19226
+ ws.once("error", onError);
19227
+ });
19228
+ }
19229
+
18629
19230
  // src/cli.ts
18630
19231
  init_update_check();
18631
19232
  var suppressUpdateNotice = false;
@@ -18703,6 +19304,26 @@ async function main() {
18703
19304
  suppressUpdateNotice = true;
18704
19305
  await runShim({ sessionId, name, agentId: agentIdFromFlag, model });
18705
19306
  return;
19307
+ case "cat": {
19308
+ const promptFromShort = readShortPrompt(argv);
19309
+ const longPrompt = typeof flags.prompt === "string" ? flags.prompt : void 0;
19310
+ const prompt = promptFromShort ?? longPrompt;
19311
+ const cwd = resolveOption(flags, "cwd");
19312
+ const catOpts = {
19313
+ prompt,
19314
+ sessionId,
19315
+ name,
19316
+ model,
19317
+ agentId: agentIdFromFlag,
19318
+ detach: flags.detach === true
19319
+ };
19320
+ if (cwd !== void 0) {
19321
+ catOpts.cwd = cwd;
19322
+ }
19323
+ suppressUpdateNotice = true;
19324
+ await runCat(catOpts);
19325
+ return;
19326
+ }
18706
19327
  case "init":
18707
19328
  await runInit(flags);
18708
19329
  return;
@@ -18887,6 +19508,7 @@ async function dispatchTui(flags, base) {
18887
19508
  );
18888
19509
  process.exit(2);
18889
19510
  }
19511
+ setHydraProcessTitle(buildTitleFromArgv(process.argv.slice(2)));
18890
19512
  const { runTui } = await Promise.resolve().then(() => (init_tui(), tui_exports));
18891
19513
  const tuiOpts = { resume, forceNew, readonly };
18892
19514
  if (base.sessionId !== void 0) {
@@ -18906,6 +19528,21 @@ async function dispatchTui(flags, base) {
18906
19528
  }
18907
19529
  await runTui(tuiOpts);
18908
19530
  }
19531
+ function readShortPrompt(argv) {
19532
+ for (let i = 0; i < argv.length; i += 1) {
19533
+ const tok = argv[i];
19534
+ if (tok === void 0) {
19535
+ continue;
19536
+ }
19537
+ if (tok === "-p") {
19538
+ return argv[i + 1];
19539
+ }
19540
+ if (tok.startsWith("-p") && !tok.startsWith("--")) {
19541
+ return tok.slice(2);
19542
+ }
19543
+ }
19544
+ return void 0;
19545
+ }
18909
19546
  function bareResumeError() {
18910
19547
  process.stderr.write(
18911
19548
  "hydra-acp: --resume requires a session id. Use --resume <id> to attach to a specific session, or --reattach to pick the most recent one in cwd.\n"
@@ -18916,7 +19553,7 @@ function readVersion() {
18916
19553
  try {
18917
19554
  const here = dirname6(fileURLToPath2(import.meta.url));
18918
19555
  const pkg = JSON.parse(
18919
- readFileSync2(resolve4(here, "../package.json"), "utf8")
19556
+ readFileSync2(resolve5(here, "../package.json"), "utf8")
18920
19557
  );
18921
19558
  return pkg.version ?? "unknown";
18922
19559
  } catch {
@@ -18932,6 +19569,19 @@ function printHelp() {
18932
19569
  " hydra-acp Auto: TUI when stdout is a TTY, shim otherwise (the editor-spawned case)",
18933
19570
  " hydra-acp shim Run as ACP shim explicitly (forces shim mode regardless of TTY)",
18934
19571
  " hydra-acp tui [opts] Run the terminal UI explicitly (see below for opts)",
19572
+ " hydra-acp cat [-p <prompt>] [--session-id <id>] [--detach] [--agent <id>] [--model <id>] [--name <label>]",
19573
+ " Pipe-friendly headless mode. Reads stdin and sends it",
19574
+ " as a prompt to a fresh session, streams the agent's",
19575
+ " response to stdout, exits when stdin closes. A bounded",
19576
+ ' input (e.g. `cat file.log | hydra cat -p "..."`) goes in',
19577
+ " as one turn; a streaming input (e.g. `tail -f`) is",
19578
+ " chunked by the natural pauses in the writer. -p is an",
19579
+ " optional standing instruction prepended to every chunk;",
19580
+ " if stdin already contains the question, -p is not needed.",
19581
+ " With --session-id, attach to an existing session instead",
19582
+ " of creating a new one. With --detach, the session",
19583
+ " survives in the daemon for slack/browser/notifier",
19584
+ " extensions.",
18935
19585
  " hydra-acp launch <agent> [agent-args...]",
18936
19586
  " Shim mode, force daemon to spawn <agent>",
18937
19587
  " from the registry. Args after <agent>",