codeam-cli 2.27.16 → 2.28.0

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/index.js +768 -507
  3. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -498,7 +498,7 @@ var import_qrcode_terminal = __toESM(require("qrcode-terminal"));
498
498
  // package.json
499
499
  var package_default = {
500
500
  name: "codeam-cli",
501
- version: "2.27.16",
501
+ version: "2.28.0",
502
502
  description: "Workflow-continuity bridge for AI coding agents. Wrap Claude Code or Codex in a PTY and supervise, approve, and redirect the session from any device \u2014 async. The terminal companion for CodeAgent Mobile.",
503
503
  type: "commonjs",
504
504
  main: "dist/index.js",
@@ -573,6 +573,7 @@ var package_default = {
573
573
  "@agentclientprotocol/sdk": "^0.25.0",
574
574
  "@clack/prompts": "^1.2.0",
575
575
  chokidar: "^3.6.0",
576
+ ignore: "^5.3.2",
576
577
  picocolors: "^1.1.0",
577
578
  "qrcode-terminal": "^0.12.0",
578
579
  which: "^5.0.0",
@@ -712,6 +713,20 @@ async function requestCode(pluginId) {
712
713
  return null;
713
714
  }
714
715
  }
716
+ async function fetchCurrentPluginAuthToken(sessionId, pluginId) {
717
+ try {
718
+ const result = await _transport.postJson(
719
+ `${API_BASE}/api/pairing/reconnect`,
720
+ { sessionId, pluginId }
721
+ );
722
+ const data = result?.data;
723
+ if (!data?.paired) return null;
724
+ const token = data.pluginAuthToken;
725
+ return typeof token === "string" && token.length > 0 ? token : null;
726
+ } catch {
727
+ return null;
728
+ }
729
+ }
715
730
  function pollStatus(pluginId, onPaired, onTimeout) {
716
731
  let stopped = false;
717
732
  let pollTimer = null;
@@ -5873,7 +5888,7 @@ function readAnonId() {
5873
5888
  }
5874
5889
  function superProperties() {
5875
5890
  return {
5876
- cliVersion: true ? "2.27.16" : "0.0.0-dev",
5891
+ cliVersion: true ? "2.28.0" : "0.0.0-dev",
5877
5892
  nodeVersion: process.version,
5878
5893
  platform: process.platform,
5879
5894
  arch: process.arch,
@@ -14973,9 +14988,10 @@ var AcpPublisher = class {
14973
14988
  this.envelope(body)
14974
14989
  );
14975
14990
  if (statusCode < 200 || statusCode >= 300) {
14991
+ const tok = this.opts.pluginAuthToken;
14976
14992
  log.warn(
14977
14993
  "acpPublisher",
14978
- `output type=${String(body.type)} done=${body.done === true} status=${statusCode} body=${resBody.slice(0, 200)}`
14994
+ `output type=${String(body.type)} done=${body.done === true} status=${statusCode} body=${resBody.slice(0, 200)} | sentSessionId=${this.opts.sessionId} sentPluginId=${this.opts.pluginId} tokenLen=${tok.length} tokenHead=${tok.slice(0, 12)} tokenTail=${tok.slice(-8)}`
14979
14995
  );
14980
14996
  }
14981
14997
  } catch (err) {
@@ -15113,122 +15129,391 @@ var AcpPublisher = class {
15113
15129
  }
15114
15130
  };
15115
15131
 
15116
- // src/agents/acp/mappers.ts
15117
- var import_node_crypto5 = require("crypto");
15118
- function mapSessionUpdate(notification) {
15119
- const update = notification.update;
15120
- switch (update.sessionUpdate) {
15121
- case "agent_message_chunk": {
15122
- const text = extractText2(update.content);
15123
- if (!text) return [];
15124
- return [{ chunkId: messageChunkId(update.messageId), kind: "text", delta: text }];
15125
- }
15126
- case "agent_thought_chunk": {
15127
- const text = extractText2(update.content);
15128
- if (!text) return [];
15129
- return [{ chunkId: messageChunkId(update.messageId), kind: "thinking", delta: text }];
15130
- }
15131
- case "tool_call": {
15132
- const summary = describeToolCall(update);
15133
- if (!summary) return [];
15134
- return [{ chunkId: update.toolCallId, kind: "tool_use", delta: summary }];
15135
- }
15136
- case "tool_call_update": {
15137
- if (update.status !== "completed" && update.status !== "failed") {
15138
- return [];
15139
- }
15140
- const body = describeToolCallUpdate(update);
15141
- if (!body) return [];
15142
- const prefix = update.status === "failed" ? "[failed] " : "";
15143
- return [{ chunkId: update.toolCallId, kind: "tool_result", delta: prefix + body }];
15132
+ // src/services/terminal-ops.service.ts
15133
+ var import_child_process8 = require("child_process");
15134
+ var import_crypto2 = require("crypto");
15135
+ var import_path3 = __toESM(require("path"));
15136
+ var MAX_CONCURRENT_SESSIONS = 4;
15137
+ var nodePtyModule;
15138
+ function loadNodePty2() {
15139
+ if (nodePtyModule !== void 0) return nodePtyModule;
15140
+ const vendoredPath = import_path3.default.join(__dirname, "vendor", "node-pty");
15141
+ try {
15142
+ nodePtyModule = require(vendoredPath);
15143
+ return nodePtyModule;
15144
+ } catch {
15145
+ try {
15146
+ nodePtyModule = require("node-pty");
15147
+ return nodePtyModule;
15148
+ } catch {
15149
+ nodePtyModule = null;
15150
+ return nodePtyModule;
15144
15151
  }
15145
- case "user_message_chunk":
15146
- return [];
15147
- case "plan":
15148
- case "plan_update":
15149
- case "plan_removed":
15150
- case "available_commands_update":
15151
- case "current_mode_update":
15152
- case "config_option_update":
15153
- case "session_info_update":
15154
- case "usage_update":
15155
- return [];
15156
- default:
15157
- return [];
15158
- }
15159
- }
15160
- function mapPermissionRequest(request) {
15161
- const prompt = describeToolCall(request.toolCall) ?? "The agent requested permission to continue.";
15162
- const optionIdByLabel = {};
15163
- const kindByLabel = {};
15164
- const labels = [];
15165
- for (const opt of request.options) {
15166
- const label = opt.name?.trim() || humanizeKind(opt.kind);
15167
- if (label in optionIdByLabel) continue;
15168
- optionIdByLabel[label] = opt.optionId;
15169
- kindByLabel[label] = opt.kind;
15170
- labels.push(label);
15171
15152
  }
15172
- return {
15173
- event: {
15174
- questionId: (0, import_node_crypto5.randomUUID)(),
15175
- prompt,
15176
- options: labels.length > 0 ? labels : void 0
15177
- },
15178
- optionIdByLabel,
15179
- kindByLabel
15180
- };
15181
15153
  }
15182
- function messageChunkId(messageId) {
15183
- if (typeof messageId === "string" && messageId.length > 0) return messageId;
15184
- return (0, import_node_crypto5.randomUUID)();
15154
+ var sessions = /* @__PURE__ */ new Map();
15155
+ var onDataHandler = null;
15156
+ var onExitHandler = null;
15157
+ function registerTerminalHandlers(opts) {
15158
+ onDataHandler = opts.onData;
15159
+ onExitHandler = opts.onExit;
15185
15160
  }
15186
- function extractText2(content) {
15187
- if (!content || typeof content !== "object") return null;
15188
- if ("type" in content && content.type === "text") {
15189
- const t2 = content.text;
15190
- return typeof t2 === "string" && t2.length > 0 ? t2 : null;
15161
+ function defaultShell() {
15162
+ if (process.platform === "win32") {
15163
+ return process.env.COMSPEC ?? "powershell.exe";
15191
15164
  }
15192
- return null;
15165
+ return process.env.SHELL ?? "/bin/bash";
15193
15166
  }
15194
- function describeToolCall(call) {
15195
- const title = call.title?.trim();
15196
- const kind = call.kind?.trim();
15197
- if (title && title.length > 0) return title;
15198
- if (kind && kind.length > 0) return kind;
15199
- if (call.rawInput && typeof call.rawInput === "object") {
15167
+ var PYTHON_TERMINAL_HELPER = `import os,pty,sys,select,signal,struct,fcntl,termios,errno,re
15168
+ m,s=pty.openpty()
15169
+ try:
15170
+ cols=int(os.environ.get('COLUMNS','80'))
15171
+ rows=int(os.environ.get('LINES','24'))
15172
+ fcntl.ioctl(s,termios.TIOCSWINSZ,struct.pack('HHHH',rows,cols,0,0))
15173
+ except Exception:pass
15174
+ pid=os.fork()
15175
+ if pid==0:
15176
+ os.close(m)
15177
+ os.setsid()
15178
+ try:fcntl.ioctl(s,termios.TIOCSCTTY,0)
15179
+ except Exception:pass
15180
+ for fd in[0,1,2]:os.dup2(s,fd)
15181
+ if s>2:os.close(s)
15182
+ os.execvp(sys.argv[1],sys.argv[1:])
15183
+ sys.exit(127)
15184
+ os.close(s)
15185
+ done=[False]
15186
+ def onchld(n,f):
15187
+ try:os.waitpid(pid,os.WNOHANG)
15188
+ except Exception:pass
15189
+ done[0]=True
15190
+ signal.signal(signal.SIGCHLD,onchld)
15191
+ signal.signal(signal.SIGHUP,signal.SIG_IGN)
15192
+ i=sys.stdin.fileno()
15193
+ o=sys.stdout.fileno()
15194
+ in_buf=b''
15195
+ resize_re=re.compile(rb'\\x00CW (\\d+) (\\d+)\\n')
15196
+ while not done[0]:
15197
+ try:r,_,_=select.select([i,m],[],[],0.1)
15198
+ except OSError as e:
15199
+ if e.errno==errno.EINTR:continue
15200
+ break
15201
+ if i in r:
15202
+ try:
15203
+ d=os.read(i,4096)
15204
+ if not d:break
15205
+ in_buf+=d
15206
+ while True:
15207
+ mo=resize_re.search(in_buf)
15208
+ if not mo:break
15209
+ try:
15210
+ rows=int(mo.group(1));cols=int(mo.group(2))
15211
+ fcntl.ioctl(m,termios.TIOCSWINSZ,struct.pack('HHHH',rows,cols,0,0))
15212
+ except Exception:pass
15213
+ in_buf=in_buf[:mo.start()]+in_buf[mo.end():]
15214
+ if in_buf:
15215
+ # Don't forward a dangling NUL that might be the
15216
+ # start of an incomplete resize marker \u2014 hold it
15217
+ # until the next read so the regex matches.
15218
+ nul=in_buf.rfind(b'\\x00')
15219
+ if nul>=0 and len(in_buf)-nul<32:
15220
+ tail=in_buf[nul:];body=in_buf[:nul]
15221
+ if body:os.write(m,body)
15222
+ in_buf=tail
15223
+ else:
15224
+ os.write(m,in_buf);in_buf=b''
15225
+ except OSError:break
15226
+ if m in r:
15227
+ try:
15228
+ d=os.read(m,4096)
15229
+ if d:os.write(o,d)
15230
+ except OSError:done[0]=True
15231
+ try:os.kill(pid,signal.SIGTERM)
15232
+ except Exception:pass
15233
+ try:
15234
+ _,st=os.waitpid(pid,0)
15235
+ sys.exit((st>>8)&0xFF)
15236
+ except Exception:sys.exit(0)
15237
+ `;
15238
+ function findPython3() {
15239
+ for (const name of ["python3", "python"]) {
15200
15240
  try {
15201
- const summary = JSON.stringify(call.rawInput);
15202
- if (summary.length > 240) return `${summary.slice(0, 240)}\u2026`;
15203
- return summary;
15241
+ const out2 = require("child_process").spawnSync("which", [name], { encoding: "utf8" });
15242
+ if (out2.status === 0 && out2.stdout?.trim()) return out2.stdout.trim();
15204
15243
  } catch {
15205
- return null;
15206
15244
  }
15207
15245
  }
15208
15246
  return null;
15209
15247
  }
15210
- function describeToolCallUpdate(update) {
15211
- const parts = [];
15212
- if (Array.isArray(update.content)) {
15213
- for (const item of update.content) {
15214
- if (!item || typeof item !== "object") continue;
15215
- if (item.type === "content" && item.content) {
15216
- const text = extractText2(item.content);
15217
- if (text) parts.push(text);
15218
- } else if (item.type === "diff") {
15219
- const p2 = item.path;
15220
- parts.push(p2 ? `diff: ${p2}` : "diff");
15221
- } else if (item.type === "terminal") {
15222
- const id = item.terminalId;
15223
- parts.push(id ? `terminal: ${id}` : "terminal");
15248
+ function createPythonSession(id, shell, cwd, env, cols, rows) {
15249
+ const python = findPython3();
15250
+ if (!python) {
15251
+ return { error: "python3 not found on PATH \u2014 required for terminal sessions on Linux/macOS without node-pty." };
15252
+ }
15253
+ log.trace("terminal", `python helper spawn python=${python} shell=${shell} cwd=${cwd}`);
15254
+ let child;
15255
+ try {
15256
+ child = (0, import_child_process8.spawn)(python, ["-c", PYTHON_TERMINAL_HELPER, shell], {
15257
+ cwd,
15258
+ env: { ...env, COLUMNS: String(cols), LINES: String(rows) },
15259
+ stdio: ["pipe", "pipe", "pipe"]
15260
+ });
15261
+ } catch (e) {
15262
+ const msg = e instanceof Error ? e.message : "python spawn failed";
15263
+ log.warn("terminal", `python helper spawn failed: ${msg}`);
15264
+ return { error: msg };
15265
+ }
15266
+ child.stdout.on("data", (buf) => {
15267
+ onDataHandler?.({ sessionId: id, data: buf.toString("utf8") });
15268
+ });
15269
+ child.stderr.on("data", (buf) => {
15270
+ onDataHandler?.({ sessionId: id, data: buf.toString("utf8") });
15271
+ });
15272
+ child.on("exit", (code) => {
15273
+ onExitHandler?.({ sessionId: id, exitCode: code ?? 0 });
15274
+ sessions.delete(id);
15275
+ });
15276
+ return {
15277
+ id,
15278
+ write(data) {
15279
+ try {
15280
+ child.stdin.write(data);
15281
+ } catch {
15282
+ }
15283
+ },
15284
+ resize(cs, rs) {
15285
+ try {
15286
+ child.stdin.write(`\0CW ${rs} ${cs}
15287
+ `);
15288
+ } catch {
15289
+ }
15290
+ },
15291
+ kill() {
15292
+ try {
15293
+ child.kill("SIGTERM");
15294
+ } catch {
15224
15295
  }
15225
15296
  }
15226
- }
15227
- if (parts.length > 0) return parts.join("\n");
15228
- const title = update.title?.trim();
15229
- return title && title.length > 0 ? title : null;
15297
+ };
15230
15298
  }
15231
- function humanizeKind(kind) {
15299
+ function openTerminal(opts) {
15300
+ if (sessions.size >= MAX_CONCURRENT_SESSIONS) {
15301
+ return { error: `Too many open terminals (max ${MAX_CONCURRENT_SESSIONS})` };
15302
+ }
15303
+ const shell = opts.shell ?? defaultShell();
15304
+ const cwd = opts.cwd ?? process.cwd();
15305
+ const env = {
15306
+ ...process.env,
15307
+ TERM: "xterm-256color",
15308
+ COLORTERM: "truecolor",
15309
+ FORCE_COLOR: "1"
15310
+ };
15311
+ const cols = Math.max(1, Math.min(opts.cols ?? 80, 500));
15312
+ const rows = Math.max(1, Math.min(opts.rows ?? 24, 200));
15313
+ const id = (0, import_crypto2.randomUUID)();
15314
+ const ptyMod = loadNodePty2();
15315
+ if (ptyMod) {
15316
+ try {
15317
+ log.trace("terminal", `node-pty spawn shell=${shell} cwd=${cwd} cols=${cols} rows=${rows}`);
15318
+ const term = ptyMod.spawn(shell, [], {
15319
+ name: "xterm-256color",
15320
+ cols,
15321
+ rows,
15322
+ cwd,
15323
+ env,
15324
+ useConpty: process.platform === "win32" ? true : void 0
15325
+ });
15326
+ const dataListener = term.onData((data) => {
15327
+ onDataHandler?.({ sessionId: id, data });
15328
+ });
15329
+ const exitListener = term.onExit(({ exitCode }) => {
15330
+ onExitHandler?.({ sessionId: id, exitCode });
15331
+ sessions.delete(id);
15332
+ });
15333
+ sessions.set(id, {
15334
+ id,
15335
+ write: (d3) => term.write(d3),
15336
+ resize: (cs, rs) => term.resize(cs, rs),
15337
+ kill: () => {
15338
+ dataListener.dispose();
15339
+ exitListener.dispose();
15340
+ term.kill();
15341
+ }
15342
+ });
15343
+ return { sessionId: id };
15344
+ } catch (e) {
15345
+ const msg = e instanceof Error ? e.message : "spawn failed";
15346
+ log.warn("terminal", `node-pty spawn failed: ${msg} (falling back to Python helper on ${process.platform})`);
15347
+ if (process.platform === "win32") {
15348
+ return { error: msg };
15349
+ }
15350
+ }
15351
+ } else {
15352
+ log.warn("terminal", `node-pty unavailable on ${process.platform}-${process.arch}; falling back to Python helper`);
15353
+ if (process.platform === "win32") {
15354
+ return {
15355
+ error: `node-pty native module unavailable on ${process.platform}-${process.arch}; terminal feature disabled for this platform`
15356
+ };
15357
+ }
15358
+ }
15359
+ const sess = createPythonSession(id, shell, cwd, env, cols, rows);
15360
+ if ("error" in sess) {
15361
+ log.warn("terminal", `createPythonSession failed: ${sess.error}`);
15362
+ return { error: sess.error };
15363
+ }
15364
+ sessions.set(id, sess);
15365
+ return { sessionId: id };
15366
+ }
15367
+ function writeTerminal(sessionId, data) {
15368
+ const s = sessions.get(sessionId);
15369
+ if (!s) return { ok: false, error: "No such session" };
15370
+ try {
15371
+ s.write(data);
15372
+ return { ok: true };
15373
+ } catch (e) {
15374
+ return { ok: false, error: e instanceof Error ? e.message : "write failed" };
15375
+ }
15376
+ }
15377
+ function resizeTerminal(sessionId, cols, rows) {
15378
+ const s = sessions.get(sessionId);
15379
+ if (!s) return { ok: false, error: "No such session" };
15380
+ try {
15381
+ s.resize?.(Math.max(1, Math.min(cols, 500)), Math.max(1, Math.min(rows, 200)));
15382
+ return { ok: true };
15383
+ } catch (e) {
15384
+ return { ok: false, error: e instanceof Error ? e.message : "resize failed" };
15385
+ }
15386
+ }
15387
+ function closeTerminal(sessionId) {
15388
+ const s = sessions.get(sessionId);
15389
+ if (!s) return { ok: true };
15390
+ try {
15391
+ s.kill();
15392
+ } catch {
15393
+ }
15394
+ sessions.delete(sessionId);
15395
+ return { ok: true };
15396
+ }
15397
+ function closeAllTerminals() {
15398
+ for (const id of Array.from(sessions.keys())) closeTerminal(id);
15399
+ }
15400
+
15401
+ // src/agents/acp/mappers.ts
15402
+ var import_node_crypto5 = require("crypto");
15403
+ function mapSessionUpdate(notification) {
15404
+ const update = notification.update;
15405
+ switch (update.sessionUpdate) {
15406
+ case "agent_message_chunk": {
15407
+ const text = extractText2(update.content);
15408
+ if (!text) return [];
15409
+ return [{ chunkId: messageChunkId(update.messageId), kind: "text", delta: text }];
15410
+ }
15411
+ case "agent_thought_chunk": {
15412
+ const text = extractText2(update.content);
15413
+ if (!text) return [];
15414
+ return [{ chunkId: messageChunkId(update.messageId), kind: "thinking", delta: text }];
15415
+ }
15416
+ case "tool_call": {
15417
+ const summary = describeToolCall(update);
15418
+ if (!summary) return [];
15419
+ return [{ chunkId: update.toolCallId, kind: "tool_use", delta: summary }];
15420
+ }
15421
+ case "tool_call_update": {
15422
+ if (update.status !== "completed" && update.status !== "failed") {
15423
+ return [];
15424
+ }
15425
+ const body = describeToolCallUpdate(update);
15426
+ if (!body) return [];
15427
+ const prefix = update.status === "failed" ? "[failed] " : "";
15428
+ return [{ chunkId: update.toolCallId, kind: "tool_result", delta: prefix + body }];
15429
+ }
15430
+ case "user_message_chunk":
15431
+ return [];
15432
+ case "plan":
15433
+ case "plan_update":
15434
+ case "plan_removed":
15435
+ case "available_commands_update":
15436
+ case "current_mode_update":
15437
+ case "config_option_update":
15438
+ case "session_info_update":
15439
+ case "usage_update":
15440
+ return [];
15441
+ default:
15442
+ return [];
15443
+ }
15444
+ }
15445
+ function mapPermissionRequest(request) {
15446
+ const prompt = describeToolCall(request.toolCall) ?? "The agent requested permission to continue.";
15447
+ const optionIdByLabel = {};
15448
+ const kindByLabel = {};
15449
+ const labels = [];
15450
+ for (const opt of request.options) {
15451
+ const label = opt.name?.trim() || humanizeKind(opt.kind);
15452
+ if (label in optionIdByLabel) continue;
15453
+ optionIdByLabel[label] = opt.optionId;
15454
+ kindByLabel[label] = opt.kind;
15455
+ labels.push(label);
15456
+ }
15457
+ return {
15458
+ event: {
15459
+ questionId: (0, import_node_crypto5.randomUUID)(),
15460
+ prompt,
15461
+ options: labels.length > 0 ? labels : void 0
15462
+ },
15463
+ optionIdByLabel,
15464
+ kindByLabel
15465
+ };
15466
+ }
15467
+ function messageChunkId(messageId) {
15468
+ if (typeof messageId === "string" && messageId.length > 0) return messageId;
15469
+ return (0, import_node_crypto5.randomUUID)();
15470
+ }
15471
+ function extractText2(content) {
15472
+ if (!content || typeof content !== "object") return null;
15473
+ if ("type" in content && content.type === "text") {
15474
+ const t2 = content.text;
15475
+ return typeof t2 === "string" && t2.length > 0 ? t2 : null;
15476
+ }
15477
+ return null;
15478
+ }
15479
+ function describeToolCall(call) {
15480
+ const title = call.title?.trim();
15481
+ const kind = call.kind?.trim();
15482
+ if (title && title.length > 0) return title;
15483
+ if (kind && kind.length > 0) return kind;
15484
+ if (call.rawInput && typeof call.rawInput === "object") {
15485
+ try {
15486
+ const summary = JSON.stringify(call.rawInput);
15487
+ if (summary.length > 240) return `${summary.slice(0, 240)}\u2026`;
15488
+ return summary;
15489
+ } catch {
15490
+ return null;
15491
+ }
15492
+ }
15493
+ return null;
15494
+ }
15495
+ function describeToolCallUpdate(update) {
15496
+ const parts = [];
15497
+ if (Array.isArray(update.content)) {
15498
+ for (const item of update.content) {
15499
+ if (!item || typeof item !== "object") continue;
15500
+ if (item.type === "content" && item.content) {
15501
+ const text = extractText2(item.content);
15502
+ if (text) parts.push(text);
15503
+ } else if (item.type === "diff") {
15504
+ const p2 = item.path;
15505
+ parts.push(p2 ? `diff: ${p2}` : "diff");
15506
+ } else if (item.type === "terminal") {
15507
+ const id = item.terminalId;
15508
+ parts.push(id ? `terminal: ${id}` : "terminal");
15509
+ }
15510
+ }
15511
+ }
15512
+ if (parts.length > 0) return parts.join("\n");
15513
+ const title = update.title?.trim();
15514
+ return title && title.length > 0 ? title : null;
15515
+ }
15516
+ function humanizeKind(kind) {
15232
15517
  switch (kind) {
15233
15518
  case "allow_once":
15234
15519
  return "Allow once";
@@ -15420,7 +15705,7 @@ function parsePayload2(schema, raw) {
15420
15705
 
15421
15706
  // src/services/file-ops.service.ts
15422
15707
  var fs22 = __toESM(require("fs/promises"));
15423
- var path26 = __toESM(require("path"));
15708
+ var path27 = __toESM(require("path"));
15424
15709
  var MAX_FILE_BYTES = 5 * 1024 * 1024;
15425
15710
  var MAX_WALK_DEPTH = 6;
15426
15711
  var MAX_VISITED_DIRS = 5e3;
@@ -15455,8 +15740,8 @@ var SUBDIR_IGNORE = /* @__PURE__ */ new Set([
15455
15740
  "__pycache__"
15456
15741
  ]);
15457
15742
  function isUnder(parent, candidate) {
15458
- const rel = path26.relative(parent, candidate);
15459
- return rel === "" || !rel.startsWith("..") && !path26.isAbsolute(rel);
15743
+ const rel = path27.relative(parent, candidate);
15744
+ return rel === "" || !rel.startsWith("..") && !path27.isAbsolute(rel);
15460
15745
  }
15461
15746
  async function isExistingFile(absPath) {
15462
15747
  try {
@@ -15479,7 +15764,7 @@ async function walkForSuffix(dir, needleVariants, depth, ctx) {
15479
15764
  }
15480
15765
  for (const e of entries) {
15481
15766
  if (!e.isFile()) continue;
15482
- const full = path26.join(dir, e.name);
15767
+ const full = path27.join(dir, e.name);
15483
15768
  if (needleVariants.some((needle) => full.endsWith(needle))) {
15484
15769
  ctx.matches.push(full);
15485
15770
  if (ctx.matches.length >= ctx.cap) return;
@@ -15489,21 +15774,21 @@ async function walkForSuffix(dir, needleVariants, depth, ctx) {
15489
15774
  if (!e.isDirectory()) continue;
15490
15775
  if (SUBDIR_IGNORE.has(e.name)) continue;
15491
15776
  if (e.name.startsWith(".") && SUBDIR_IGNORE.has(e.name)) continue;
15492
- await walkForSuffix(path26.join(dir, e.name), needleVariants, depth + 1, ctx);
15777
+ await walkForSuffix(path27.join(dir, e.name), needleVariants, depth + 1, ctx);
15493
15778
  if (ctx.matches.length >= ctx.cap) return;
15494
15779
  }
15495
15780
  }
15496
15781
  async function findFile(rawPath) {
15497
15782
  const cwd = process.cwd();
15498
- if (path26.isAbsolute(rawPath)) {
15499
- const abs = path26.normalize(rawPath);
15783
+ if (path27.isAbsolute(rawPath)) {
15784
+ const abs = path27.normalize(rawPath);
15500
15785
  if (isUnder(cwd, abs) && await isExistingFile(abs)) return abs;
15501
15786
  }
15502
- const direct = path26.resolve(cwd, rawPath);
15787
+ const direct = path27.resolve(cwd, rawPath);
15503
15788
  if (isUnder(cwd, direct) && await isExistingFile(direct)) return direct;
15504
- const normalized = path26.normalize(rawPath).replace(/^[./\\]+/, "");
15789
+ const normalized = path27.normalize(rawPath).replace(/^[./\\]+/, "");
15505
15790
  const needles = [
15506
- `${path26.sep}${normalized}`,
15791
+ `${path27.sep}${normalized}`,
15507
15792
  `/${normalized}`
15508
15793
  ].filter((v, i, a) => a.indexOf(v) === i);
15509
15794
  const ctx = { visited: 0, matches: [], cap: 16 };
@@ -15517,7 +15802,7 @@ async function findWriteTarget(rawPath) {
15517
15802
  const found = await findFile(rawPath);
15518
15803
  if (found) return found;
15519
15804
  const cwd = process.cwd();
15520
- const fallback = path26.isAbsolute(rawPath) ? path26.normalize(rawPath) : path26.resolve(cwd, rawPath);
15805
+ const fallback = path27.isAbsolute(rawPath) ? path27.normalize(rawPath) : path27.resolve(cwd, rawPath);
15521
15806
  if (!isUnder(cwd, fallback)) return null;
15522
15807
  return fallback;
15523
15808
  }
@@ -15557,7 +15842,7 @@ async function writeProjectFile(rawPath, content) {
15557
15842
  if (Buffer.byteLength(content, "utf-8") > MAX_FILE_BYTES) {
15558
15843
  return { error: "Content too large." };
15559
15844
  }
15560
- await fs22.mkdir(path26.dirname(abs), { recursive: true });
15845
+ await fs22.mkdir(path27.dirname(abs), { recursive: true });
15561
15846
  await fs22.writeFile(abs, content, "utf-8");
15562
15847
  return { ok: true };
15563
15848
  } catch (e) {
@@ -15567,11 +15852,11 @@ async function writeProjectFile(rawPath, content) {
15567
15852
  }
15568
15853
 
15569
15854
  // src/services/project-ops.service.ts
15570
- var import_child_process8 = require("child_process");
15855
+ var import_child_process9 = require("child_process");
15571
15856
  var import_util2 = require("util");
15572
15857
  var fs23 = __toESM(require("fs/promises"));
15573
- var path27 = __toESM(require("path"));
15574
- var execFileP3 = (0, import_util2.promisify)(import_child_process8.execFile);
15858
+ var path28 = __toESM(require("path"));
15859
+ var execFileP3 = (0, import_util2.promisify)(import_child_process9.execFile);
15575
15860
  var PROJECT_IGNORE = /* @__PURE__ */ new Set([
15576
15861
  "node_modules",
15577
15862
  ".git",
@@ -15628,12 +15913,12 @@ async function listProjectFiles(opts = {}) {
15628
15913
  return;
15629
15914
  }
15630
15915
  if (PROJECT_IGNORE.has(e.name)) continue;
15631
- const full = path27.join(dir, e.name);
15916
+ const full = path28.join(dir, e.name);
15632
15917
  if (e.isDirectory()) {
15633
15918
  if (depth >= 12) continue;
15634
15919
  await walk(full, depth + 1);
15635
15920
  } else if (e.isFile()) {
15636
- const rel = path27.relative(root, full);
15921
+ const rel = path28.relative(root, full);
15637
15922
  if (q2 && !rel.toLowerCase().includes(q2) && !e.name.toLowerCase().includes(q2)) {
15638
15923
  continue;
15639
15924
  }
@@ -15741,7 +16026,7 @@ async function gitStatus(cwd) {
15741
16026
  let hasMergeInProgress = false;
15742
16027
  try {
15743
16028
  const gitDir = (await git(["rev-parse", "--git-dir"], root)).stdout.trim();
15744
- const mergeHead = path27.isAbsolute(gitDir) ? path27.join(gitDir, "MERGE_HEAD") : path27.join(root, gitDir, "MERGE_HEAD");
16029
+ const mergeHead = path28.isAbsolute(gitDir) ? path28.join(gitDir, "MERGE_HEAD") : path28.join(root, gitDir, "MERGE_HEAD");
15745
16030
  await fs23.access(mergeHead);
15746
16031
  hasMergeInProgress = true;
15747
16032
  } catch {
@@ -15790,381 +16075,124 @@ async function gitCommit(message, files, cwd) {
15790
16075
  return { error: "Commit message is required." };
15791
16076
  }
15792
16077
  if (files && files.length > 0) {
15793
- const r2 = await git(["add", "--", ...files], cwd);
15794
- if (r2.code !== 0) return { error: `git add failed: ${r2.stderr.trim()}` };
15795
- } else {
15796
- const r2 = await git(["add", "-A"], cwd);
15797
- if (r2.code !== 0) return { error: `git add failed: ${r2.stderr.trim()}` };
15798
- }
15799
- const r = await git(["commit", "-m", message], cwd);
15800
- if (r.code !== 0) {
15801
- return { error: r.stderr.trim() || "git commit failed" };
15802
- }
15803
- const head = await git(["rev-parse", "HEAD"], cwd);
15804
- return { ok: true, commit: head.stdout.trim() };
15805
- }
15806
- async function gitPush(cwd) {
15807
- const r = await git(["push"], cwd);
15808
- if (r.code !== 0) return { error: r.stderr.trim() || "git push failed" };
15809
- return { ok: true, output: (r.stdout + r.stderr).trim() };
15810
- }
15811
- async function gitPull(cwd) {
15812
- const r = await git(["pull", "--ff-only"], cwd);
15813
- if (r.code !== 0) return { error: r.stderr.trim() || "git pull failed" };
15814
- return { ok: true, output: (r.stdout + r.stderr).trim() };
15815
- }
15816
- async function gitResolve(file, side, cwd) {
15817
- const r = await git(["checkout", `--${side}`, "--", file], cwd);
15818
- if (r.code !== 0) return { error: r.stderr.trim() || `git checkout --${side} failed` };
15819
- const add = await git(["add", "--", file], cwd);
15820
- if (add.code !== 0) return { error: add.stderr.trim() || "git add (resolve) failed" };
15821
- return { ok: true };
15822
- }
15823
- var MAX_SEARCH_HITS = 500;
15824
- var MAX_SEARCH_BYTES = 256 * 1024;
15825
- async function searchFiles(opts) {
15826
- const cwd = opts.cwd ?? process.cwd();
15827
- const cap = Math.min(opts.maxResults ?? MAX_SEARCH_HITS, MAX_SEARCH_HITS);
15828
- if (!opts.query.trim()) return { hits: [], total: 0, truncated: false };
15829
- const args2 = ["grep", "-n", "--column", "-I"];
15830
- if (!opts.caseSensitive) args2.push("-i");
15831
- if (opts.wholeWord) args2.push("-w");
15832
- if (opts.regex) args2.push("-E");
15833
- else args2.push("-F");
15834
- args2.push(opts.query);
15835
- if (opts.include && opts.include.length > 0) {
15836
- args2.push("--");
15837
- for (const p2 of opts.include) args2.push(p2);
15838
- } else if (opts.exclude && opts.exclude.length > 0) {
15839
- args2.push("--");
15840
- args2.push(".");
15841
- }
15842
- for (const p2 of opts.exclude ?? []) args2.push(`:!${p2}`);
15843
- const r = await git(args2, cwd);
15844
- if (r.code !== 0 && r.code !== 1) {
15845
- return jsSearchFiles(opts, cwd, cap);
15846
- }
15847
- const hits = [];
15848
- const lines = r.stdout.split("\n");
15849
- let truncated = false;
15850
- let byteBudget = MAX_SEARCH_BYTES;
15851
- for (const line of lines) {
15852
- if (!line) continue;
15853
- if (hits.length >= cap) {
15854
- truncated = true;
15855
- break;
15856
- }
15857
- if (byteBudget <= 0) {
15858
- truncated = true;
15859
- break;
15860
- }
15861
- byteBudget -= line.length;
15862
- const m = line.match(/^([^]+?):(\d+):(\d+):(.*)$/);
15863
- if (!m) continue;
15864
- const filePath = m[1] ?? "";
15865
- const lineNo = parseInt(m[2] ?? "0", 10);
15866
- const col = parseInt(m[3] ?? "1", 10);
15867
- const text = (m[4] ?? "").slice(0, 400);
15868
- if (!filePath) continue;
15869
- hits.push({
15870
- path: filePath,
15871
- line: lineNo,
15872
- column: col,
15873
- text,
15874
- matchLength: opts.query.length
15875
- });
15876
- }
15877
- return { hits, total: hits.length, truncated };
15878
- }
15879
- async function jsSearchFiles(opts, cwd, cap) {
15880
- const files = await listProjectFiles({ cwd, cap: 2e3 });
15881
- const hits = [];
15882
- const needle = opts.caseSensitive ? opts.query : opts.query.toLowerCase();
15883
- let truncated = files.truncated;
15884
- for (const f of files.files) {
15885
- if (hits.length >= cap) {
15886
- truncated = true;
15887
- break;
15888
- }
15889
- let content = "";
15890
- try {
15891
- content = await fs23.readFile(path27.join(cwd, f.path), "utf8");
15892
- } catch {
15893
- continue;
15894
- }
15895
- const lines = content.split("\n");
15896
- for (let i = 0; i < lines.length && hits.length < cap; i++) {
15897
- const line = lines[i] ?? "";
15898
- const hay = opts.caseSensitive ? line : line.toLowerCase();
15899
- const idx = hay.indexOf(needle);
15900
- if (idx === -1) continue;
15901
- hits.push({
15902
- path: f.path,
15903
- line: i + 1,
15904
- column: idx + 1,
15905
- text: line.slice(0, 400),
15906
- matchLength: opts.query.length
15907
- });
15908
- }
15909
- }
15910
- return { hits, total: hits.length, truncated };
15911
- }
15912
-
15913
- // src/services/terminal-ops.service.ts
15914
- var import_child_process9 = require("child_process");
15915
- var import_crypto2 = require("crypto");
15916
- var import_path3 = __toESM(require("path"));
15917
- var MAX_CONCURRENT_SESSIONS = 4;
15918
- var nodePtyModule;
15919
- function loadNodePty2() {
15920
- if (nodePtyModule !== void 0) return nodePtyModule;
15921
- const vendoredPath = import_path3.default.join(__dirname, "vendor", "node-pty");
15922
- try {
15923
- nodePtyModule = require(vendoredPath);
15924
- return nodePtyModule;
15925
- } catch {
15926
- try {
15927
- nodePtyModule = require("node-pty");
15928
- return nodePtyModule;
15929
- } catch {
15930
- nodePtyModule = null;
15931
- return nodePtyModule;
15932
- }
15933
- }
15934
- }
15935
- var sessions = /* @__PURE__ */ new Map();
15936
- var onDataHandler = null;
15937
- var onExitHandler = null;
15938
- function registerTerminalHandlers(opts) {
15939
- onDataHandler = opts.onData;
15940
- onExitHandler = opts.onExit;
15941
- }
15942
- function defaultShell() {
15943
- if (process.platform === "win32") {
15944
- return process.env.COMSPEC ?? "powershell.exe";
15945
- }
15946
- return process.env.SHELL ?? "/bin/bash";
15947
- }
15948
- var PYTHON_TERMINAL_HELPER = `import os,pty,sys,select,signal,struct,fcntl,termios,errno,re
15949
- m,s=pty.openpty()
15950
- try:
15951
- cols=int(os.environ.get('COLUMNS','80'))
15952
- rows=int(os.environ.get('LINES','24'))
15953
- fcntl.ioctl(s,termios.TIOCSWINSZ,struct.pack('HHHH',rows,cols,0,0))
15954
- except Exception:pass
15955
- pid=os.fork()
15956
- if pid==0:
15957
- os.close(m)
15958
- os.setsid()
15959
- try:fcntl.ioctl(s,termios.TIOCSCTTY,0)
15960
- except Exception:pass
15961
- for fd in[0,1,2]:os.dup2(s,fd)
15962
- if s>2:os.close(s)
15963
- os.execvp(sys.argv[1],sys.argv[1:])
15964
- sys.exit(127)
15965
- os.close(s)
15966
- done=[False]
15967
- def onchld(n,f):
15968
- try:os.waitpid(pid,os.WNOHANG)
15969
- except Exception:pass
15970
- done[0]=True
15971
- signal.signal(signal.SIGCHLD,onchld)
15972
- signal.signal(signal.SIGHUP,signal.SIG_IGN)
15973
- i=sys.stdin.fileno()
15974
- o=sys.stdout.fileno()
15975
- in_buf=b''
15976
- resize_re=re.compile(rb'\\x00CW (\\d+) (\\d+)\\n')
15977
- while not done[0]:
15978
- try:r,_,_=select.select([i,m],[],[],0.1)
15979
- except OSError as e:
15980
- if e.errno==errno.EINTR:continue
15981
- break
15982
- if i in r:
15983
- try:
15984
- d=os.read(i,4096)
15985
- if not d:break
15986
- in_buf+=d
15987
- while True:
15988
- mo=resize_re.search(in_buf)
15989
- if not mo:break
15990
- try:
15991
- rows=int(mo.group(1));cols=int(mo.group(2))
15992
- fcntl.ioctl(m,termios.TIOCSWINSZ,struct.pack('HHHH',rows,cols,0,0))
15993
- except Exception:pass
15994
- in_buf=in_buf[:mo.start()]+in_buf[mo.end():]
15995
- if in_buf:
15996
- # Don't forward a dangling NUL that might be the
15997
- # start of an incomplete resize marker \u2014 hold it
15998
- # until the next read so the regex matches.
15999
- nul=in_buf.rfind(b'\\x00')
16000
- if nul>=0 and len(in_buf)-nul<32:
16001
- tail=in_buf[nul:];body=in_buf[:nul]
16002
- if body:os.write(m,body)
16003
- in_buf=tail
16004
- else:
16005
- os.write(m,in_buf);in_buf=b''
16006
- except OSError:break
16007
- if m in r:
16008
- try:
16009
- d=os.read(m,4096)
16010
- if d:os.write(o,d)
16011
- except OSError:done[0]=True
16012
- try:os.kill(pid,signal.SIGTERM)
16013
- except Exception:pass
16014
- try:
16015
- _,st=os.waitpid(pid,0)
16016
- sys.exit((st>>8)&0xFF)
16017
- except Exception:sys.exit(0)
16018
- `;
16019
- function findPython3() {
16020
- for (const name of ["python3", "python"]) {
16021
- try {
16022
- const out2 = require("child_process").spawnSync("which", [name], { encoding: "utf8" });
16023
- if (out2.status === 0 && out2.stdout?.trim()) return out2.stdout.trim();
16024
- } catch {
16025
- }
16026
- }
16027
- return null;
16028
- }
16029
- function createPythonSession(id, shell, cwd, env, cols, rows) {
16030
- const python = findPython3();
16031
- if (!python) {
16032
- return { error: "python3 not found on PATH \u2014 required for terminal sessions on Linux/macOS without node-pty." };
16033
- }
16034
- let child;
16035
- try {
16036
- child = (0, import_child_process9.spawn)(python, ["-c", PYTHON_TERMINAL_HELPER, shell], {
16037
- cwd,
16038
- env: { ...env, COLUMNS: String(cols), LINES: String(rows) },
16039
- stdio: ["pipe", "pipe", "pipe"]
16040
- });
16041
- } catch (e) {
16042
- return { error: e instanceof Error ? e.message : "python spawn failed" };
16043
- }
16044
- child.stdout.on("data", (buf) => {
16045
- onDataHandler?.({ sessionId: id, data: buf.toString("utf8") });
16046
- });
16047
- child.stderr.on("data", (buf) => {
16048
- onDataHandler?.({ sessionId: id, data: buf.toString("utf8") });
16049
- });
16050
- child.on("exit", (code) => {
16051
- onExitHandler?.({ sessionId: id, exitCode: code ?? 0 });
16052
- sessions.delete(id);
16053
- });
16054
- return {
16055
- id,
16056
- write(data) {
16057
- try {
16058
- child.stdin.write(data);
16059
- } catch {
16060
- }
16061
- },
16062
- resize(cs, rs) {
16063
- try {
16064
- child.stdin.write(`\0CW ${rs} ${cs}
16065
- `);
16066
- } catch {
16067
- }
16068
- },
16069
- kill() {
16070
- try {
16071
- child.kill("SIGTERM");
16072
- } catch {
16073
- }
16074
- }
16075
- };
16076
- }
16077
- function openTerminal(opts) {
16078
- if (sessions.size >= MAX_CONCURRENT_SESSIONS) {
16079
- return { error: `Too many open terminals (max ${MAX_CONCURRENT_SESSIONS})` };
16080
- }
16081
- const shell = opts.shell ?? defaultShell();
16082
- const cwd = opts.cwd ?? process.cwd();
16083
- const env = {
16084
- ...process.env,
16085
- TERM: "xterm-256color",
16086
- COLORTERM: "truecolor",
16087
- FORCE_COLOR: "1"
16088
- };
16089
- const cols = Math.max(1, Math.min(opts.cols ?? 80, 500));
16090
- const rows = Math.max(1, Math.min(opts.rows ?? 24, 200));
16091
- const id = (0, import_crypto2.randomUUID)();
16092
- const ptyMod = loadNodePty2();
16093
- if (ptyMod) {
16094
- try {
16095
- const term = ptyMod.spawn(shell, [], {
16096
- name: "xterm-256color",
16097
- cols,
16098
- rows,
16099
- cwd,
16100
- env,
16101
- useConpty: process.platform === "win32" ? true : void 0
16102
- });
16103
- const dataListener = term.onData((data) => {
16104
- onDataHandler?.({ sessionId: id, data });
16105
- });
16106
- const exitListener = term.onExit(({ exitCode }) => {
16107
- onExitHandler?.({ sessionId: id, exitCode });
16108
- sessions.delete(id);
16109
- });
16110
- sessions.set(id, {
16111
- id,
16112
- write: (d3) => term.write(d3),
16113
- resize: (cs, rs) => term.resize(cs, rs),
16114
- kill: () => {
16115
- dataListener.dispose();
16116
- exitListener.dispose();
16117
- term.kill();
16118
- }
16119
- });
16120
- return { sessionId: id };
16121
- } catch (e) {
16122
- if (process.platform === "win32") {
16123
- return { error: e instanceof Error ? e.message : "spawn failed" };
16124
- }
16125
- }
16126
- } else if (process.platform === "win32") {
16127
- return {
16128
- error: `node-pty native module unavailable on ${process.platform}-${process.arch}; terminal feature disabled for this platform`
16129
- };
16078
+ const r2 = await git(["add", "--", ...files], cwd);
16079
+ if (r2.code !== 0) return { error: `git add failed: ${r2.stderr.trim()}` };
16080
+ } else {
16081
+ const r2 = await git(["add", "-A"], cwd);
16082
+ if (r2.code !== 0) return { error: `git add failed: ${r2.stderr.trim()}` };
16130
16083
  }
16131
- const sess = createPythonSession(id, shell, cwd, env, cols, rows);
16132
- if ("error" in sess) return { error: sess.error };
16133
- sessions.set(id, sess);
16134
- return { sessionId: id };
16135
- }
16136
- function writeTerminal(sessionId, data) {
16137
- const s = sessions.get(sessionId);
16138
- if (!s) return { ok: false, error: "No such session" };
16139
- try {
16140
- s.write(data);
16141
- return { ok: true };
16142
- } catch (e) {
16143
- return { ok: false, error: e instanceof Error ? e.message : "write failed" };
16084
+ const r = await git(["commit", "-m", message], cwd);
16085
+ if (r.code !== 0) {
16086
+ return { error: r.stderr.trim() || "git commit failed" };
16144
16087
  }
16088
+ const head = await git(["rev-parse", "HEAD"], cwd);
16089
+ return { ok: true, commit: head.stdout.trim() };
16145
16090
  }
16146
- function resizeTerminal(sessionId, cols, rows) {
16147
- const s = sessions.get(sessionId);
16148
- if (!s) return { ok: false, error: "No such session" };
16149
- try {
16150
- s.resize?.(Math.max(1, Math.min(cols, 500)), Math.max(1, Math.min(rows, 200)));
16151
- return { ok: true };
16152
- } catch (e) {
16153
- return { ok: false, error: e instanceof Error ? e.message : "resize failed" };
16154
- }
16091
+ async function gitPush(cwd) {
16092
+ const r = await git(["push"], cwd);
16093
+ if (r.code !== 0) return { error: r.stderr.trim() || "git push failed" };
16094
+ return { ok: true, output: (r.stdout + r.stderr).trim() };
16155
16095
  }
16156
- function closeTerminal(sessionId) {
16157
- const s = sessions.get(sessionId);
16158
- if (!s) return { ok: true };
16159
- try {
16160
- s.kill();
16161
- } catch {
16162
- }
16163
- sessions.delete(sessionId);
16096
+ async function gitPull(cwd) {
16097
+ const r = await git(["pull", "--ff-only"], cwd);
16098
+ if (r.code !== 0) return { error: r.stderr.trim() || "git pull failed" };
16099
+ return { ok: true, output: (r.stdout + r.stderr).trim() };
16100
+ }
16101
+ async function gitResolve(file, side, cwd) {
16102
+ const r = await git(["checkout", `--${side}`, "--", file], cwd);
16103
+ if (r.code !== 0) return { error: r.stderr.trim() || `git checkout --${side} failed` };
16104
+ const add = await git(["add", "--", file], cwd);
16105
+ if (add.code !== 0) return { error: add.stderr.trim() || "git add (resolve) failed" };
16164
16106
  return { ok: true };
16165
16107
  }
16166
- function closeAllTerminals() {
16167
- for (const id of Array.from(sessions.keys())) closeTerminal(id);
16108
+ var MAX_SEARCH_HITS = 500;
16109
+ var MAX_SEARCH_BYTES = 256 * 1024;
16110
+ async function searchFiles(opts) {
16111
+ const cwd = opts.cwd ?? process.cwd();
16112
+ const cap = Math.min(opts.maxResults ?? MAX_SEARCH_HITS, MAX_SEARCH_HITS);
16113
+ if (!opts.query.trim()) return { hits: [], total: 0, truncated: false };
16114
+ const args2 = ["grep", "-n", "--column", "-I"];
16115
+ if (!opts.caseSensitive) args2.push("-i");
16116
+ if (opts.wholeWord) args2.push("-w");
16117
+ if (opts.regex) args2.push("-E");
16118
+ else args2.push("-F");
16119
+ args2.push(opts.query);
16120
+ if (opts.include && opts.include.length > 0) {
16121
+ args2.push("--");
16122
+ for (const p2 of opts.include) args2.push(p2);
16123
+ } else if (opts.exclude && opts.exclude.length > 0) {
16124
+ args2.push("--");
16125
+ args2.push(".");
16126
+ }
16127
+ for (const p2 of opts.exclude ?? []) args2.push(`:!${p2}`);
16128
+ const r = await git(args2, cwd);
16129
+ if (r.code !== 0 && r.code !== 1) {
16130
+ return jsSearchFiles(opts, cwd, cap);
16131
+ }
16132
+ const hits = [];
16133
+ const lines = r.stdout.split("\n");
16134
+ let truncated = false;
16135
+ let byteBudget = MAX_SEARCH_BYTES;
16136
+ for (const line of lines) {
16137
+ if (!line) continue;
16138
+ if (hits.length >= cap) {
16139
+ truncated = true;
16140
+ break;
16141
+ }
16142
+ if (byteBudget <= 0) {
16143
+ truncated = true;
16144
+ break;
16145
+ }
16146
+ byteBudget -= line.length;
16147
+ const m = line.match(/^([^]+?):(\d+):(\d+):(.*)$/);
16148
+ if (!m) continue;
16149
+ const filePath = m[1] ?? "";
16150
+ const lineNo = parseInt(m[2] ?? "0", 10);
16151
+ const col = parseInt(m[3] ?? "1", 10);
16152
+ const text = (m[4] ?? "").slice(0, 400);
16153
+ if (!filePath) continue;
16154
+ hits.push({
16155
+ path: filePath,
16156
+ line: lineNo,
16157
+ column: col,
16158
+ text,
16159
+ matchLength: opts.query.length
16160
+ });
16161
+ }
16162
+ return { hits, total: hits.length, truncated };
16163
+ }
16164
+ async function jsSearchFiles(opts, cwd, cap) {
16165
+ const files = await listProjectFiles({ cwd, cap: 2e3 });
16166
+ const hits = [];
16167
+ const needle = opts.caseSensitive ? opts.query : opts.query.toLowerCase();
16168
+ let truncated = files.truncated;
16169
+ for (const f of files.files) {
16170
+ if (hits.length >= cap) {
16171
+ truncated = true;
16172
+ break;
16173
+ }
16174
+ let content = "";
16175
+ try {
16176
+ content = await fs23.readFile(path28.join(cwd, f.path), "utf8");
16177
+ } catch {
16178
+ continue;
16179
+ }
16180
+ const lines = content.split("\n");
16181
+ for (let i = 0; i < lines.length && hits.length < cap; i++) {
16182
+ const line = lines[i] ?? "";
16183
+ const hay = opts.caseSensitive ? line : line.toLowerCase();
16184
+ const idx = hay.indexOf(needle);
16185
+ if (idx === -1) continue;
16186
+ hits.push({
16187
+ path: f.path,
16188
+ line: i + 1,
16189
+ column: idx + 1,
16190
+ text: line.slice(0, 400),
16191
+ matchLength: opts.query.length
16192
+ });
16193
+ }
16194
+ }
16195
+ return { hits, total: hits.length, truncated };
16168
16196
  }
16169
16197
 
16170
16198
  // src/services/apply-file-review.service.ts
@@ -17876,6 +17904,7 @@ var import_child_process13 = require("child_process");
17876
17904
  var fs30 = __toESM(require("fs"));
17877
17905
  var os24 = __toESM(require("os"));
17878
17906
  var path35 = __toESM(require("path"));
17907
+ var import_ignore = __toESM(require("ignore"));
17879
17908
 
17880
17909
  // src/services/file-watcher/diff-parser.ts
17881
17910
  var HUNK_HEADER_RE = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/;
@@ -18099,6 +18128,17 @@ var FileWatcherService = class {
18099
18128
  * doesn't hammer `fs.statSync` for every event.
18100
18129
  */
18101
18130
  gitRootByDir = /* @__PURE__ */ new Map();
18131
+ /**
18132
+ * Per-repo `.gitignore` matcher. On first encounter of a git root we
18133
+ * collect every `.gitignore` file under it, parse them through the
18134
+ * `ignore` package, and store the resulting matcher here keyed by
18135
+ * absolute repo path. Subsequent file events in the same repo reuse
18136
+ * the matcher in O(1). The hard-coded IGNORED_PATH_PATTERN above
18137
+ * catches conventional dirs (node_modules, dist, Pods, …); this
18138
+ * matcher layers the repo's own ignore rules on top so per-project
18139
+ * artifacts (ios/, .env*, build outputs) stop polluting the queue.
18140
+ */
18141
+ gitIgnoreMatcherByRoot = /* @__PURE__ */ new Map();
18102
18142
  stopped = false;
18103
18143
  /**
18104
18144
  * Cross-file coalescing buffer. Keyed by absPath so multiple
@@ -18324,9 +18364,17 @@ var FileWatcherService = class {
18324
18364
  );
18325
18365
  return;
18326
18366
  }
18327
- this.opts.onRepoDirty?.(gitRoot);
18328
18367
  const relPathInRepo = path35.relative(gitRoot, absPath);
18329
18368
  if (!relPathInRepo || relPathInRepo.startsWith("..")) return;
18369
+ const matcher = this.getGitIgnoreMatcher(gitRoot);
18370
+ if (matcher && matcher.ignores(relPathInRepo)) {
18371
+ log.trace(
18372
+ "fileWatcher",
18373
+ `${relPathInRepo} ignored by ${path35.basename(gitRoot)}/.gitignore \u2014 suppressing emit`
18374
+ );
18375
+ return;
18376
+ }
18377
+ this.opts.onRepoDirty?.(gitRoot);
18330
18378
  const repoPath = path35.relative(this.opts.workingDir, gitRoot);
18331
18379
  const repoName = path35.basename(gitRoot);
18332
18380
  let diffText = "";
@@ -18462,6 +18510,79 @@ var FileWatcherService = class {
18462
18510
  async postReviewBlame(body) {
18463
18511
  await this.postWithRetries(`${this.apiBase}/api/review/blame`, body);
18464
18512
  }
18513
+ /**
18514
+ * Lazily build and cache a per-repo `.gitignore` matcher. We walk the
18515
+ * repo collecting every `.gitignore` file (skipping the same dirs
18516
+ * IGNORED_PATH_PATTERN already filters at chokidar level, so we
18517
+ * don't read inside node_modules / Pods / etc.) and feed each file
18518
+ * into a single `ignore` matcher anchored at the git root. Subsequent
18519
+ * calls return the cached matcher; failures fall back to `null`,
18520
+ * which the caller treats as "no extra filtering" — so a malformed
18521
+ * .gitignore degrades to the prior pre-fix behaviour rather than
18522
+ * silently dropping every event.
18523
+ */
18524
+ getGitIgnoreMatcher(gitRoot) {
18525
+ if (this.gitIgnoreMatcherByRoot.has(gitRoot)) {
18526
+ return this.gitIgnoreMatcherByRoot.get(gitRoot) ?? null;
18527
+ }
18528
+ const matcher = (0, import_ignore.default)();
18529
+ let added = 0;
18530
+ try {
18531
+ this.collectGitignoreFiles(gitRoot, gitRoot, matcher);
18532
+ added = 1;
18533
+ } catch (err) {
18534
+ log.warn(
18535
+ "fileWatcher",
18536
+ `failed to build gitignore matcher for ${gitRoot}: ${err.message}`
18537
+ );
18538
+ }
18539
+ const result = added > 0 ? matcher : null;
18540
+ this.gitIgnoreMatcherByRoot.set(gitRoot, result);
18541
+ return result;
18542
+ }
18543
+ /**
18544
+ * Walk the repo recursively collecting every `.gitignore` file and
18545
+ * add its rules to `matcher`, with the path prefix that anchors them
18546
+ * to the right subdirectory (so a `.gitignore` inside `apps/api`
18547
+ * scopes to `apps/api/*`, not the whole repo). Skips heavy dirs the
18548
+ * static IGNORED_PATH_PATTERN already filters — we don't want to
18549
+ * stat into `node_modules/` looking for buried .gitignore files.
18550
+ */
18551
+ collectGitignoreFiles(repoRoot, dir, matcher) {
18552
+ let entries;
18553
+ try {
18554
+ entries = fs30.readdirSync(dir, { withFileTypes: true });
18555
+ } catch {
18556
+ return;
18557
+ }
18558
+ const gitignoreEntry = entries.find(
18559
+ (e) => e.isFile() && e.name === ".gitignore"
18560
+ );
18561
+ if (gitignoreEntry) {
18562
+ try {
18563
+ const body = fs30.readFileSync(path35.join(dir, ".gitignore"), "utf8");
18564
+ const rel = path35.relative(repoRoot, dir).replace(/\\/g, "/");
18565
+ const prefixed = body.split(/\r?\n/).map((line) => {
18566
+ const trimmed = line.trim();
18567
+ if (!trimmed || trimmed.startsWith("#")) return line;
18568
+ if (!rel) return line;
18569
+ if (trimmed.startsWith("!")) {
18570
+ return "!" + path35.posix.join(rel, trimmed.slice(1));
18571
+ }
18572
+ return path35.posix.join(rel, trimmed);
18573
+ }).join("\n");
18574
+ matcher.add(prefixed);
18575
+ } catch {
18576
+ }
18577
+ }
18578
+ for (const entry of entries) {
18579
+ if (!entry.isDirectory()) continue;
18580
+ if (entry.name === ".git") continue;
18581
+ const childAbs = path35.join(dir, entry.name);
18582
+ if (isIgnoredFilePath(childAbs)) continue;
18583
+ this.collectGitignoreFiles(repoRoot, childAbs, matcher);
18584
+ }
18585
+ }
18465
18586
  async postWithRetries(url, body) {
18466
18587
  const payload = JSON.stringify(body);
18467
18588
  const headers = {
@@ -19425,6 +19546,24 @@ async function runAcpSession(opts) {
19425
19546
  pluginAuthToken: opts.pluginAuthToken
19426
19547
  });
19427
19548
  const streaming = new StreamingState(publisher);
19549
+ registerTerminalHandlers({
19550
+ onData: ({ sessionId, data }) => {
19551
+ void publisher.publishOutput({
19552
+ type: "terminal_data",
19553
+ terminalSessionId: sessionId,
19554
+ data,
19555
+ done: false
19556
+ });
19557
+ },
19558
+ onExit: ({ sessionId, exitCode }) => {
19559
+ void publisher.publishOutput({
19560
+ type: "terminal_exit",
19561
+ terminalSessionId: sessionId,
19562
+ exitCode,
19563
+ done: true
19564
+ });
19565
+ }
19566
+ });
19428
19567
  let updateCount = 0;
19429
19568
  const client2 = new AcpClient({
19430
19569
  adapter: opts.adapter,
@@ -19502,20 +19641,37 @@ async function runAcpSession(opts) {
19502
19641
  const runtime = createInteractiveAgentStrategy(opts.agent, createOsStrategy());
19503
19642
  const models = await runtime.listModels();
19504
19643
  const history = new AcpHistory(publisher, { agent: opts.agent, acpSessionId });
19505
- const fileWatcher = new FileWatcherService({
19644
+ const turnFiles = new TurnFileAggregator({
19506
19645
  workingDir: opts.cwd,
19507
19646
  sessionId: opts.sessionId,
19508
19647
  pluginId: opts.pluginId,
19509
- pluginAuthToken: opts.pluginAuthToken
19648
+ pluginAuthToken: opts.pluginAuthToken,
19649
+ agentId: opts.agent
19510
19650
  });
19511
- const turnFiles = new TurnFileAggregator({
19651
+ const REPO_DIRTY_FLUSH_DEBOUNCE_MS = 2e3;
19652
+ let repoDirtyTimer = null;
19653
+ const fileWatcher = new FileWatcherService({
19512
19654
  workingDir: opts.cwd,
19513
19655
  sessionId: opts.sessionId,
19514
19656
  pluginId: opts.pluginId,
19515
19657
  pluginAuthToken: opts.pluginAuthToken,
19516
- agentId: opts.agent
19658
+ onRepoDirty: () => {
19659
+ if (repoDirtyTimer) clearTimeout(repoDirtyTimer);
19660
+ repoDirtyTimer = setTimeout(() => {
19661
+ repoDirtyTimer = null;
19662
+ log.info("acpRunner", "onRepoDirty debounce fired \u2014 running flushTurn");
19663
+ turnFiles.flushTurn().catch((err) => {
19664
+ log.warn("acpRunner", `flushTurn from onRepoDirty failed: ${describeError(err)}`);
19665
+ });
19666
+ }, REPO_DIRTY_FLUSH_DEBOUNCE_MS);
19667
+ }
19517
19668
  });
19518
- fileWatcher.start().catch((err) => {
19669
+ fileWatcher.start().then(() => {
19670
+ log.info(
19671
+ "acpRunner",
19672
+ `fileWatcher started \u2014 watching cwd=${opts.cwd} for file changes (debounce ${REPO_DIRTY_FLUSH_DEBOUNCE_MS}ms before flushTurn)`
19673
+ );
19674
+ }).catch((err) => {
19519
19675
  log.warn("acpRunner", `fileWatcher.start failed: ${describeError(err)}`);
19520
19676
  });
19521
19677
  const relay = new CommandRelayService(
@@ -19542,6 +19698,7 @@ async function runAcpSession(opts) {
19542
19698
  relay.stop();
19543
19699
  void fileWatcher.stop();
19544
19700
  turnFiles.stop();
19701
+ closeAllTerminals();
19545
19702
  await client2.stop();
19546
19703
  process.exit(0);
19547
19704
  };
@@ -19756,6 +19913,7 @@ async function handleCommand(cmd, client2, relay, acpSessionId, models, streamin
19756
19913
  }
19757
19914
  }
19758
19915
  relay.stop();
19916
+ closeAllTerminals();
19759
19917
  await client2.stop();
19760
19918
  process.exit(0);
19761
19919
  return;
@@ -19765,26 +19923,58 @@ async function handleCommand(cmd, client2, relay, acpSessionId, models, streamin
19765
19923
  case "preview_stop":
19766
19924
  case "save_preview_config": {
19767
19925
  const runtime = createInteractiveAgentStrategy(opts.agent, createOsStrategy());
19768
- const ctx = buildLegacyContextForACP(opts, relay, runtime);
19926
+ let previewHandlerAcked = false;
19927
+ const ackingRelay = new Proxy(relay, {
19928
+ get(target, prop, receiver) {
19929
+ if (prop === "sendResult") {
19930
+ return (commandId, status2, result) => {
19931
+ if (commandId === cmd.id) previewHandlerAcked = true;
19932
+ return target.sendResult(commandId, status2, result);
19933
+ };
19934
+ }
19935
+ return Reflect.get(target, prop, receiver);
19936
+ }
19937
+ });
19938
+ const ctx = buildLegacyContextForACP(opts, ackingRelay, runtime);
19769
19939
  try {
19770
19940
  await dispatchCommand(ctx, cmd);
19771
- await relay.sendResult(cmd.id, "completed", {});
19941
+ if (!previewHandlerAcked) {
19942
+ await relay.sendResult(cmd.id, "completed", {});
19943
+ }
19772
19944
  } catch (err) {
19773
19945
  log.warn("acpRunner", `${cmd.type} failed: ${describeError(err)}`);
19774
- await relay.sendResult(cmd.id, "failed", { error: describeError(err) });
19946
+ if (!previewHandlerAcked) {
19947
+ await relay.sendResult(cmd.id, "failed", { error: describeError(err) });
19948
+ }
19775
19949
  }
19776
19950
  return;
19777
19951
  }
19778
19952
  default:
19779
19953
  if (handlers[cmd.type]) {
19780
19954
  const runtime = createInteractiveAgentStrategy(opts.agent, createOsStrategy());
19781
- const legacyCtx = buildLegacyContextForACP(opts, relay, runtime);
19955
+ let handlerAcked = false;
19956
+ const ackingRelay = new Proxy(relay, {
19957
+ get(target, prop, receiver) {
19958
+ if (prop === "sendResult") {
19959
+ return (commandId, status2, result) => {
19960
+ if (commandId === cmd.id) handlerAcked = true;
19961
+ return target.sendResult(commandId, status2, result);
19962
+ };
19963
+ }
19964
+ return Reflect.get(target, prop, receiver);
19965
+ }
19966
+ });
19967
+ const legacyCtx = buildLegacyContextForACP(opts, ackingRelay, runtime);
19782
19968
  try {
19783
19969
  await dispatchCommand(legacyCtx, cmd);
19784
- await relay.sendResult(cmd.id, "completed", {});
19970
+ if (!handlerAcked) {
19971
+ await relay.sendResult(cmd.id, "completed", {});
19972
+ }
19785
19973
  } catch (err) {
19786
19974
  log.warn("acpRunner", `legacy handler "${cmd.type}" threw: ${describeError(err)}`);
19787
- await relay.sendResult(cmd.id, "failed", { error: describeError(err) });
19975
+ if (!handlerAcked) {
19976
+ await relay.sendResult(cmd.id, "failed", { error: describeError(err) });
19977
+ }
19788
19978
  }
19789
19979
  return;
19790
19980
  }
@@ -21551,6 +21741,19 @@ async function start(requestedAgent) {
21551
21741
  requestedAgent: requestedAgent ?? null
21552
21742
  });
21553
21743
  const cwd = process.cwd();
21744
+ const refreshed = await fetchCurrentPluginAuthToken(session.id, pluginId);
21745
+ if (refreshed && refreshed !== session.pluginAuthToken) {
21746
+ addSession({ ...session, pluginAuthToken: refreshed });
21747
+ session.pluginAuthToken = refreshed;
21748
+ showInfo("Reconnected \u2014 refreshed plugin auth token.");
21749
+ } else if (refreshed) {
21750
+ showInfo("Reconnected previous session.");
21751
+ }
21752
+ const tokenForLog = session.pluginAuthToken ?? "(unset)";
21753
+ log.trace(
21754
+ "pluginAuth",
21755
+ `boot triple sessionId=${session.id} pluginId=${pluginId} tokenLen=${tokenForLog.length} tokenHead=${tokenForLog.slice(0, 12)} tokenTail=${tokenForLog.slice(-8)} mintedEqualsCached=${refreshed === session.pluginAuthToken}`
21756
+ );
21554
21757
  const acpDisabled = process.env.CODEAM_ACP_DISABLED === "1";
21555
21758
  if (!acpDisabled && session.pluginAuthToken) {
21556
21759
  const adapter = getAcpAdapter(session.agent);
@@ -24467,7 +24670,7 @@ function checkChokidar() {
24467
24670
  }
24468
24671
  async function doctor(args2 = []) {
24469
24672
  const json = args2.includes("--json");
24470
- const cliVersion = true ? "2.27.16" : "0.0.0-dev";
24673
+ const cliVersion = true ? "2.28.0" : "0.0.0-dev";
24471
24674
  const apiBase = resolveApiBaseUrl();
24472
24675
  const diagnosticId = (0, import_node_crypto8.randomUUID)();
24473
24676
  log.info("doctor", `run id=${diagnosticId} cli=${cliVersion}`);
@@ -24666,7 +24869,7 @@ async function completion(args2) {
24666
24869
  // src/commands/version.ts
24667
24870
  var import_picocolors13 = __toESM(require("picocolors"));
24668
24871
  function version2() {
24669
- const v = true ? "2.27.16" : "unknown";
24872
+ const v = true ? "2.28.0" : "unknown";
24670
24873
  console.log(`${import_picocolors13.default.bold("codeam-cli")} ${import_picocolors13.default.cyan(v)}`);
24671
24874
  }
24672
24875
 
@@ -24798,6 +25001,7 @@ var fs35 = __toESM(require("fs"));
24798
25001
  var os27 = __toESM(require("os"));
24799
25002
  var path44 = __toESM(require("path"));
24800
25003
  var https7 = __toESM(require("https"));
25004
+ var import_node_child_process12 = require("child_process");
24801
25005
  var import_picocolors16 = __toESM(require("picocolors"));
24802
25006
  var PKG_NAME = "codeam-cli";
24803
25007
  var REGISTRY_URL = `https://registry.npmjs.org/${PKG_NAME}/latest`;
@@ -24889,17 +25093,74 @@ function notifyIfStale(currentVersion, latest) {
24889
25093
  ];
24890
25094
  process.stderr.write(lines.join("\n"));
24891
25095
  }
25096
+ function isLinkedInstall() {
25097
+ try {
25098
+ const root = (0, import_node_child_process12.execSync)("npm root -g", {
25099
+ encoding: "utf8",
25100
+ stdio: ["ignore", "pipe", "ignore"],
25101
+ timeout: 2e3
25102
+ }).trim();
25103
+ if (!root) return false;
25104
+ const pkgPath = path44.join(root, PKG_NAME);
25105
+ return fs35.lstatSync(pkgPath).isSymbolicLink();
25106
+ } catch {
25107
+ return false;
25108
+ }
25109
+ }
25110
+ function maybeAutoUpdate(currentVersion, latest) {
25111
+ if (compareSemver(latest, currentVersion) <= 0) return;
25112
+ if (process.env.CODEAM_NO_AUTO_UPDATE === "1") {
25113
+ notifyIfStale(currentVersion, latest);
25114
+ return;
25115
+ }
25116
+ if (isLinkedInstall()) {
25117
+ notifyIfStale(currentVersion, latest);
25118
+ return;
25119
+ }
25120
+ process.stderr.write(
25121
+ `
25122
+ ${import_picocolors16.default.yellow("\u25CF")} ${import_picocolors16.default.bold("Updating codeam-cli")} ${import_picocolors16.default.dim(currentVersion)} ${import_picocolors16.default.dim("\u2192")} ${import_picocolors16.default.green(latest)}...
25123
+
25124
+ `
25125
+ );
25126
+ const install = (0, import_node_child_process12.spawnSync)("npm", ["install", "-g", `${PKG_NAME}@latest`], {
25127
+ stdio: "inherit",
25128
+ env: process.env
25129
+ });
25130
+ if (install.status !== 0) {
25131
+ process.stderr.write(
25132
+ `
25133
+ ${import_picocolors16.default.red("!")} Update failed (exit ${install.status ?? "?"}). Continuing on ${currentVersion}.
25134
+ Run ${import_picocolors16.default.cyan("npm install -g codeam-cli")} manually to retry.
25135
+
25136
+ `
25137
+ );
25138
+ return;
25139
+ }
25140
+ try {
25141
+ fs35.unlinkSync(cachePath());
25142
+ } catch {
25143
+ }
25144
+ process.stderr.write(` ${import_picocolors16.default.green("\u2713")} Updated. Resuming session...
25145
+
25146
+ `);
25147
+ const child = (0, import_node_child_process12.spawnSync)("codeam", process.argv.slice(2), {
25148
+ stdio: "inherit",
25149
+ env: process.env
25150
+ });
25151
+ process.exit(child.status ?? 0);
25152
+ }
24892
25153
  function checkForUpdates() {
24893
25154
  if (process.env.NODE_ENV === "test") return;
24894
25155
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
24895
25156
  if (process.env.CI) return;
24896
25157
  if (!process.stdout.isTTY) return;
24897
- const current = true ? "2.27.16" : null;
25158
+ const current = true ? "2.28.0" : null;
24898
25159
  if (!current) return;
24899
25160
  const cache = readCache();
24900
25161
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
24901
25162
  if (fresh && cache) {
24902
- notifyIfStale(current, cache.latest);
25163
+ maybeAutoUpdate(current, cache.latest);
24903
25164
  return;
24904
25165
  }
24905
25166
  void fetchLatest().then((latest) => {