codeam-cli 2.27.16 → 2.28.1

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 +23 -0
  2. package/dist/index.js +1094 -824
  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.1",
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",
@@ -691,12 +692,13 @@ function computePollDelay({ baseMs, failures }) {
691
692
 
692
693
  // src/services/pairing.service.ts
693
694
  var API_BASE = resolveApiBaseUrl();
695
+ var REQUEST_CODE_TIMEOUT_MS = 1e4;
694
696
  async function requestCode(pluginId) {
695
697
  try {
696
698
  const runtime = process.env.CODESPACES === "true" ? "github-codespaces" : "local";
697
699
  const codespaceName = process.env.CODESPACE_NAME;
698
700
  const branch = detectCurrentBranch();
699
- const result = await _transport.postJson(`${API_BASE}/api/pairing/code`, {
701
+ const post2 = _transport.postJson(`${API_BASE}/api/pairing/code`, {
700
702
  pluginId,
701
703
  ideName: "Terminal (codeam-cli)",
702
704
  ideVersion: package_default.version,
@@ -705,6 +707,14 @@ async function requestCode(pluginId) {
705
707
  branch,
706
708
  ...codespaceName ? { codespaceName } : {}
707
709
  });
710
+ let timer;
711
+ const timeoutSentinel = /* @__PURE__ */ Symbol("request-code-timeout");
712
+ const timeoutPromise = new Promise((resolve6) => {
713
+ timer = setTimeout(() => resolve6(timeoutSentinel), REQUEST_CODE_TIMEOUT_MS);
714
+ });
715
+ const result = await Promise.race([post2, timeoutPromise]);
716
+ clearTimeout(timer);
717
+ if (result === timeoutSentinel) return null;
708
718
  const data = result?.data;
709
719
  if (!data?.code) return null;
710
720
  return { code: data.code, expiresAt: data.expiresAt };
@@ -712,6 +722,20 @@ async function requestCode(pluginId) {
712
722
  return null;
713
723
  }
714
724
  }
725
+ async function fetchCurrentPluginAuthToken(sessionId, pluginId) {
726
+ try {
727
+ const result = await _transport.postJson(
728
+ `${API_BASE}/api/pairing/reconnect`,
729
+ { sessionId, pluginId }
730
+ );
731
+ const data = result?.data;
732
+ if (!data?.paired) return null;
733
+ const token = data.pluginAuthToken;
734
+ return typeof token === "string" && token.length > 0 ? token : null;
735
+ } catch {
736
+ return null;
737
+ }
738
+ }
715
739
  function pollStatus(pluginId, onPaired, onTimeout) {
716
740
  let stopped = false;
717
741
  let pollTimer = null;
@@ -5873,7 +5897,7 @@ function readAnonId() {
5873
5897
  }
5874
5898
  function superProperties() {
5875
5899
  return {
5876
- cliVersion: true ? "2.27.16" : "0.0.0-dev",
5900
+ cliVersion: true ? "2.28.1" : "0.0.0-dev",
5877
5901
  nodeVersion: process.version,
5878
5902
  platform: process.platform,
5879
5903
  arch: process.arch,
@@ -14973,9 +14997,10 @@ var AcpPublisher = class {
14973
14997
  this.envelope(body)
14974
14998
  );
14975
14999
  if (statusCode < 200 || statusCode >= 300) {
15000
+ const tok = this.opts.pluginAuthToken;
14976
15001
  log.warn(
14977
15002
  "acpPublisher",
14978
- `output type=${String(body.type)} done=${body.done === true} status=${statusCode} body=${resBody.slice(0, 200)}`
15003
+ `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
15004
  );
14980
15005
  }
14981
15006
  } catch (err) {
@@ -15113,151 +15138,420 @@ var AcpPublisher = class {
15113
15138
  }
15114
15139
  };
15115
15140
 
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 }];
15141
+ // src/services/terminal-ops.service.ts
15142
+ var import_child_process8 = require("child_process");
15143
+ var import_crypto2 = require("crypto");
15144
+ var import_path3 = __toESM(require("path"));
15145
+ var MAX_CONCURRENT_SESSIONS = 4;
15146
+ var nodePtyModule;
15147
+ function loadNodePty2() {
15148
+ if (nodePtyModule !== void 0) return nodePtyModule;
15149
+ const vendoredPath = import_path3.default.join(__dirname, "vendor", "node-pty");
15150
+ try {
15151
+ nodePtyModule = require(vendoredPath);
15152
+ return nodePtyModule;
15153
+ } catch {
15154
+ try {
15155
+ nodePtyModule = require("node-pty");
15156
+ return nodePtyModule;
15157
+ } catch {
15158
+ nodePtyModule = null;
15159
+ return nodePtyModule;
15144
15160
  }
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
15161
  }
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
15162
  }
15182
- function messageChunkId(messageId) {
15183
- if (typeof messageId === "string" && messageId.length > 0) return messageId;
15184
- return (0, import_node_crypto5.randomUUID)();
15163
+ var sessions = /* @__PURE__ */ new Map();
15164
+ var onDataHandler = null;
15165
+ var onExitHandler = null;
15166
+ function registerTerminalHandlers(opts) {
15167
+ onDataHandler = opts.onData;
15168
+ onExitHandler = opts.onExit;
15185
15169
  }
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;
15170
+ function defaultShell() {
15171
+ if (process.platform === "win32") {
15172
+ return process.env.COMSPEC ?? "powershell.exe";
15191
15173
  }
15192
- return null;
15174
+ return process.env.SHELL ?? "/bin/bash";
15193
15175
  }
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") {
15176
+ var PYTHON_TERMINAL_HELPER = `import os,pty,sys,select,signal,struct,fcntl,termios,errno,re
15177
+ m,s=pty.openpty()
15178
+ try:
15179
+ cols=int(os.environ.get('COLUMNS','80'))
15180
+ rows=int(os.environ.get('LINES','24'))
15181
+ fcntl.ioctl(s,termios.TIOCSWINSZ,struct.pack('HHHH',rows,cols,0,0))
15182
+ except Exception:pass
15183
+ pid=os.fork()
15184
+ if pid==0:
15185
+ os.close(m)
15186
+ os.setsid()
15187
+ try:fcntl.ioctl(s,termios.TIOCSCTTY,0)
15188
+ except Exception:pass
15189
+ for fd in[0,1,2]:os.dup2(s,fd)
15190
+ if s>2:os.close(s)
15191
+ os.execvp(sys.argv[1],sys.argv[1:])
15192
+ sys.exit(127)
15193
+ os.close(s)
15194
+ done=[False]
15195
+ def onchld(n,f):
15196
+ try:os.waitpid(pid,os.WNOHANG)
15197
+ except Exception:pass
15198
+ done[0]=True
15199
+ signal.signal(signal.SIGCHLD,onchld)
15200
+ signal.signal(signal.SIGHUP,signal.SIG_IGN)
15201
+ i=sys.stdin.fileno()
15202
+ o=sys.stdout.fileno()
15203
+ in_buf=b''
15204
+ resize_re=re.compile(rb'\\x00CW (\\d+) (\\d+)\\n')
15205
+ while not done[0]:
15206
+ try:r,_,_=select.select([i,m],[],[],0.1)
15207
+ except OSError as e:
15208
+ if e.errno==errno.EINTR:continue
15209
+ break
15210
+ if i in r:
15211
+ try:
15212
+ d=os.read(i,4096)
15213
+ if not d:break
15214
+ in_buf+=d
15215
+ while True:
15216
+ mo=resize_re.search(in_buf)
15217
+ if not mo:break
15218
+ try:
15219
+ rows=int(mo.group(1));cols=int(mo.group(2))
15220
+ fcntl.ioctl(m,termios.TIOCSWINSZ,struct.pack('HHHH',rows,cols,0,0))
15221
+ except Exception:pass
15222
+ in_buf=in_buf[:mo.start()]+in_buf[mo.end():]
15223
+ if in_buf:
15224
+ # Don't forward a dangling NUL that might be the
15225
+ # start of an incomplete resize marker \u2014 hold it
15226
+ # until the next read so the regex matches.
15227
+ nul=in_buf.rfind(b'\\x00')
15228
+ if nul>=0 and len(in_buf)-nul<32:
15229
+ tail=in_buf[nul:];body=in_buf[:nul]
15230
+ if body:os.write(m,body)
15231
+ in_buf=tail
15232
+ else:
15233
+ os.write(m,in_buf);in_buf=b''
15234
+ except OSError:break
15235
+ if m in r:
15236
+ try:
15237
+ d=os.read(m,4096)
15238
+ if d:os.write(o,d)
15239
+ except OSError:done[0]=True
15240
+ try:os.kill(pid,signal.SIGTERM)
15241
+ except Exception:pass
15242
+ try:
15243
+ _,st=os.waitpid(pid,0)
15244
+ sys.exit((st>>8)&0xFF)
15245
+ except Exception:sys.exit(0)
15246
+ `;
15247
+ function findPython3() {
15248
+ for (const name of ["python3", "python"]) {
15200
15249
  try {
15201
- const summary = JSON.stringify(call.rawInput);
15202
- if (summary.length > 240) return `${summary.slice(0, 240)}\u2026`;
15203
- return summary;
15250
+ const out2 = require("child_process").spawnSync("which", [name], { encoding: "utf8" });
15251
+ if (out2.status === 0 && out2.stdout?.trim()) return out2.stdout.trim();
15204
15252
  } catch {
15205
- return null;
15206
15253
  }
15207
15254
  }
15208
15255
  return null;
15209
15256
  }
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");
15224
- }
15225
- }
15257
+ function createPythonSession(id, shell, cwd, env, cols, rows) {
15258
+ const python = findPython3();
15259
+ if (!python) {
15260
+ return { error: "python3 not found on PATH \u2014 required for terminal sessions on Linux/macOS without node-pty." };
15226
15261
  }
15227
- if (parts.length > 0) return parts.join("\n");
15228
- const title = update.title?.trim();
15229
- return title && title.length > 0 ? title : null;
15230
- }
15231
- function humanizeKind(kind) {
15232
- switch (kind) {
15233
- case "allow_once":
15234
- return "Allow once";
15235
- case "allow_always":
15236
- return "Always allow";
15237
- case "reject_once":
15238
- return "Reject";
15239
- case "reject_always":
15240
- return "Always reject";
15241
- default:
15242
- return kind;
15262
+ log.trace("terminal", `python helper spawn python=${python} shell=${shell} cwd=${cwd}`);
15263
+ let child;
15264
+ try {
15265
+ child = (0, import_child_process8.spawn)(python, ["-c", PYTHON_TERMINAL_HELPER, shell], {
15266
+ cwd,
15267
+ env: { ...env, COLUMNS: String(cols), LINES: String(rows) },
15268
+ stdio: ["pipe", "pipe", "pipe"]
15269
+ });
15270
+ } catch (e) {
15271
+ const msg = e instanceof Error ? e.message : "python spawn failed";
15272
+ log.warn("terminal", `python helper spawn failed: ${msg}`);
15273
+ return { error: msg };
15243
15274
  }
15244
- }
15245
-
15246
- // src/agents/acp/selectPromptExtractor.ts
15247
- var MAX_OPTIONS = 6;
15248
- var MAX_OPTION_BODY_CHARS = 200;
15249
- var NUMBERED_LINE_RE = /^\s*(\d+)[.)\]]\s+(.+)$/;
15250
- function extractSelectPrompt(text) {
15251
- if (text.length === 0) return null;
15252
- const lines = text.split("\n");
15253
- let endIdx = lines.length - 1;
15254
- while (endIdx >= 0 && lines[endIdx].trim() === "") endIdx--;
15255
- if (endIdx < 0) return null;
15256
- const items = [];
15257
- let i = endIdx;
15258
- while (i >= 0) {
15259
- const m = lines[i].match(NUMBERED_LINE_RE);
15260
- if (!m) break;
15275
+ child.stdout.on("data", (buf) => {
15276
+ onDataHandler?.({ sessionId: id, data: buf.toString("utf8") });
15277
+ });
15278
+ child.stderr.on("data", (buf) => {
15279
+ onDataHandler?.({ sessionId: id, data: buf.toString("utf8") });
15280
+ });
15281
+ child.on("exit", (code) => {
15282
+ onExitHandler?.({ sessionId: id, exitCode: code ?? 0 });
15283
+ sessions.delete(id);
15284
+ });
15285
+ return {
15286
+ id,
15287
+ write(data) {
15288
+ try {
15289
+ child.stdin.write(data);
15290
+ } catch {
15291
+ }
15292
+ },
15293
+ resize(cs, rs) {
15294
+ try {
15295
+ child.stdin.write(`\0CW ${rs} ${cs}
15296
+ `);
15297
+ } catch {
15298
+ }
15299
+ },
15300
+ kill() {
15301
+ try {
15302
+ child.kill("SIGTERM");
15303
+ } catch {
15304
+ }
15305
+ }
15306
+ };
15307
+ }
15308
+ function openTerminal(opts) {
15309
+ if (sessions.size >= MAX_CONCURRENT_SESSIONS) {
15310
+ return { error: `Too many open terminals (max ${MAX_CONCURRENT_SESSIONS})` };
15311
+ }
15312
+ const shell = opts.shell ?? defaultShell();
15313
+ const cwd = opts.cwd ?? process.cwd();
15314
+ const env = {
15315
+ ...process.env,
15316
+ TERM: "xterm-256color",
15317
+ COLORTERM: "truecolor",
15318
+ FORCE_COLOR: "1"
15319
+ };
15320
+ const cols = Math.max(1, Math.min(opts.cols ?? 80, 500));
15321
+ const rows = Math.max(1, Math.min(opts.rows ?? 24, 200));
15322
+ const id = (0, import_crypto2.randomUUID)();
15323
+ const ptyMod = loadNodePty2();
15324
+ if (ptyMod) {
15325
+ try {
15326
+ log.trace("terminal", `node-pty spawn shell=${shell} cwd=${cwd} cols=${cols} rows=${rows}`);
15327
+ const term = ptyMod.spawn(shell, [], {
15328
+ name: "xterm-256color",
15329
+ cols,
15330
+ rows,
15331
+ cwd,
15332
+ env,
15333
+ useConpty: process.platform === "win32" ? true : void 0
15334
+ });
15335
+ const dataListener = term.onData((data) => {
15336
+ onDataHandler?.({ sessionId: id, data });
15337
+ });
15338
+ const exitListener = term.onExit(({ exitCode }) => {
15339
+ onExitHandler?.({ sessionId: id, exitCode });
15340
+ sessions.delete(id);
15341
+ });
15342
+ sessions.set(id, {
15343
+ id,
15344
+ write: (d3) => term.write(d3),
15345
+ resize: (cs, rs) => term.resize(cs, rs),
15346
+ kill: () => {
15347
+ dataListener.dispose();
15348
+ exitListener.dispose();
15349
+ term.kill();
15350
+ }
15351
+ });
15352
+ return { sessionId: id };
15353
+ } catch (e) {
15354
+ const msg = e instanceof Error ? e.message : "spawn failed";
15355
+ log.warn("terminal", `node-pty spawn failed: ${msg} (falling back to Python helper on ${process.platform})`);
15356
+ if (process.platform === "win32") {
15357
+ return { error: msg };
15358
+ }
15359
+ }
15360
+ } else {
15361
+ log.warn("terminal", `node-pty unavailable on ${process.platform}-${process.arch}; falling back to Python helper`);
15362
+ if (process.platform === "win32") {
15363
+ return {
15364
+ error: `node-pty native module unavailable on ${process.platform}-${process.arch}; terminal feature disabled for this platform`
15365
+ };
15366
+ }
15367
+ }
15368
+ const sess = createPythonSession(id, shell, cwd, env, cols, rows);
15369
+ if ("error" in sess) {
15370
+ log.warn("terminal", `createPythonSession failed: ${sess.error}`);
15371
+ return { error: sess.error };
15372
+ }
15373
+ sessions.set(id, sess);
15374
+ return { sessionId: id };
15375
+ }
15376
+ function writeTerminal(sessionId, data) {
15377
+ const s = sessions.get(sessionId);
15378
+ if (!s) return { ok: false, error: "No such session" };
15379
+ try {
15380
+ s.write(data);
15381
+ return { ok: true };
15382
+ } catch (e) {
15383
+ return { ok: false, error: e instanceof Error ? e.message : "write failed" };
15384
+ }
15385
+ }
15386
+ function resizeTerminal(sessionId, cols, rows) {
15387
+ const s = sessions.get(sessionId);
15388
+ if (!s) return { ok: false, error: "No such session" };
15389
+ try {
15390
+ s.resize?.(Math.max(1, Math.min(cols, 500)), Math.max(1, Math.min(rows, 200)));
15391
+ return { ok: true };
15392
+ } catch (e) {
15393
+ return { ok: false, error: e instanceof Error ? e.message : "resize failed" };
15394
+ }
15395
+ }
15396
+ function closeTerminal(sessionId) {
15397
+ const s = sessions.get(sessionId);
15398
+ if (!s) return { ok: true };
15399
+ try {
15400
+ s.kill();
15401
+ } catch {
15402
+ }
15403
+ sessions.delete(sessionId);
15404
+ return { ok: true };
15405
+ }
15406
+ function closeAllTerminals() {
15407
+ for (const id of Array.from(sessions.keys())) closeTerminal(id);
15408
+ }
15409
+
15410
+ // src/agents/acp/mappers.ts
15411
+ var import_node_crypto5 = require("crypto");
15412
+ function mapSessionUpdate(notification) {
15413
+ const update = notification.update;
15414
+ switch (update.sessionUpdate) {
15415
+ case "agent_message_chunk": {
15416
+ const text = extractText2(update.content);
15417
+ if (!text) return [];
15418
+ return [{ chunkId: messageChunkId(update.messageId), kind: "text", delta: text }];
15419
+ }
15420
+ case "agent_thought_chunk": {
15421
+ const text = extractText2(update.content);
15422
+ if (!text) return [];
15423
+ return [{ chunkId: messageChunkId(update.messageId), kind: "thinking", delta: text }];
15424
+ }
15425
+ case "tool_call": {
15426
+ const summary = describeToolCall(update);
15427
+ if (!summary) return [];
15428
+ return [{ chunkId: update.toolCallId, kind: "tool_use", delta: summary }];
15429
+ }
15430
+ case "tool_call_update": {
15431
+ if (update.status !== "completed" && update.status !== "failed") {
15432
+ return [];
15433
+ }
15434
+ const body = describeToolCallUpdate(update);
15435
+ if (!body) return [];
15436
+ const prefix = update.status === "failed" ? "[failed] " : "";
15437
+ return [{ chunkId: update.toolCallId, kind: "tool_result", delta: prefix + body }];
15438
+ }
15439
+ case "user_message_chunk":
15440
+ return [];
15441
+ case "plan":
15442
+ case "plan_update":
15443
+ case "plan_removed":
15444
+ case "available_commands_update":
15445
+ case "current_mode_update":
15446
+ case "config_option_update":
15447
+ case "session_info_update":
15448
+ case "usage_update":
15449
+ return [];
15450
+ default:
15451
+ return [];
15452
+ }
15453
+ }
15454
+ function mapPermissionRequest(request) {
15455
+ const prompt = describeToolCall(request.toolCall) ?? "The agent requested permission to continue.";
15456
+ const optionIdByLabel = {};
15457
+ const kindByLabel = {};
15458
+ const labels = [];
15459
+ for (const opt of request.options) {
15460
+ const label = opt.name?.trim() || humanizeKind(opt.kind);
15461
+ if (label in optionIdByLabel) continue;
15462
+ optionIdByLabel[label] = opt.optionId;
15463
+ kindByLabel[label] = opt.kind;
15464
+ labels.push(label);
15465
+ }
15466
+ return {
15467
+ event: {
15468
+ questionId: (0, import_node_crypto5.randomUUID)(),
15469
+ prompt,
15470
+ options: labels.length > 0 ? labels : void 0
15471
+ },
15472
+ optionIdByLabel,
15473
+ kindByLabel
15474
+ };
15475
+ }
15476
+ function messageChunkId(messageId) {
15477
+ if (typeof messageId === "string" && messageId.length > 0) return messageId;
15478
+ return (0, import_node_crypto5.randomUUID)();
15479
+ }
15480
+ function extractText2(content) {
15481
+ if (!content || typeof content !== "object") return null;
15482
+ if ("type" in content && content.type === "text") {
15483
+ const t2 = content.text;
15484
+ return typeof t2 === "string" && t2.length > 0 ? t2 : null;
15485
+ }
15486
+ return null;
15487
+ }
15488
+ function describeToolCall(call) {
15489
+ const title = call.title?.trim();
15490
+ const kind = call.kind?.trim();
15491
+ if (title && title.length > 0) return title;
15492
+ if (kind && kind.length > 0) return kind;
15493
+ if (call.rawInput && typeof call.rawInput === "object") {
15494
+ try {
15495
+ const summary = JSON.stringify(call.rawInput);
15496
+ if (summary.length > 240) return `${summary.slice(0, 240)}\u2026`;
15497
+ return summary;
15498
+ } catch {
15499
+ return null;
15500
+ }
15501
+ }
15502
+ return null;
15503
+ }
15504
+ function describeToolCallUpdate(update) {
15505
+ const parts = [];
15506
+ if (Array.isArray(update.content)) {
15507
+ for (const item of update.content) {
15508
+ if (!item || typeof item !== "object") continue;
15509
+ if (item.type === "content" && item.content) {
15510
+ const text = extractText2(item.content);
15511
+ if (text) parts.push(text);
15512
+ } else if (item.type === "diff") {
15513
+ const p2 = item.path;
15514
+ parts.push(p2 ? `diff: ${p2}` : "diff");
15515
+ } else if (item.type === "terminal") {
15516
+ const id = item.terminalId;
15517
+ parts.push(id ? `terminal: ${id}` : "terminal");
15518
+ }
15519
+ }
15520
+ }
15521
+ if (parts.length > 0) return parts.join("\n");
15522
+ const title = update.title?.trim();
15523
+ return title && title.length > 0 ? title : null;
15524
+ }
15525
+ function humanizeKind(kind) {
15526
+ switch (kind) {
15527
+ case "allow_once":
15528
+ return "Allow once";
15529
+ case "allow_always":
15530
+ return "Always allow";
15531
+ case "reject_once":
15532
+ return "Reject";
15533
+ case "reject_always":
15534
+ return "Always reject";
15535
+ default:
15536
+ return kind;
15537
+ }
15538
+ }
15539
+
15540
+ // src/agents/acp/selectPromptExtractor.ts
15541
+ var MAX_OPTIONS = 6;
15542
+ var MAX_OPTION_BODY_CHARS = 200;
15543
+ var NUMBERED_LINE_RE = /^\s*(\d+)[.)\]]\s+(.+)$/;
15544
+ function extractSelectPrompt(text) {
15545
+ if (text.length === 0) return null;
15546
+ const lines = text.split("\n");
15547
+ let endIdx = lines.length - 1;
15548
+ while (endIdx >= 0 && lines[endIdx].trim() === "") endIdx--;
15549
+ if (endIdx < 0) return null;
15550
+ const items = [];
15551
+ let i = endIdx;
15552
+ while (i >= 0) {
15553
+ const m = lines[i].match(NUMBERED_LINE_RE);
15554
+ if (!m) break;
15261
15555
  const body = m[2].trim();
15262
15556
  if (body.length === 0 || body.length > MAX_OPTION_BODY_CHARS) {
15263
15557
  return null;
@@ -15420,159 +15714,11 @@ function parsePayload2(schema, raw) {
15420
15714
 
15421
15715
  // src/services/file-ops.service.ts
15422
15716
  var fs22 = __toESM(require("fs/promises"));
15423
- var path26 = __toESM(require("path"));
15424
- var MAX_FILE_BYTES = 5 * 1024 * 1024;
15425
- var MAX_WALK_DEPTH = 6;
15426
- var MAX_VISITED_DIRS = 5e3;
15427
- var SUBDIR_IGNORE = /* @__PURE__ */ new Set([
15428
- "node_modules",
15429
- ".git",
15430
- ".next",
15431
- ".expo",
15432
- "dist",
15433
- "build",
15434
- "out",
15435
- ".cache",
15436
- "coverage",
15437
- ".turbo",
15438
- ".parcel-cache",
15439
- ".idea",
15440
- ".vscode",
15441
- ".vscode-test",
15442
- "ios",
15443
- "android",
15444
- // expo-managed native dirs are huge and rarely interesting
15445
- ".gradle",
15446
- ".cxx",
15447
- ".intellijPlatform",
15448
- ".kotlin",
15449
- "tmp",
15450
- "target",
15451
- "venv",
15452
- ".venv",
15453
- ".mypy_cache",
15454
- ".pytest_cache",
15455
- "__pycache__"
15456
- ]);
15457
- function isUnder(parent, candidate) {
15458
- const rel = path26.relative(parent, candidate);
15459
- return rel === "" || !rel.startsWith("..") && !path26.isAbsolute(rel);
15460
- }
15461
- async function isExistingFile(absPath) {
15462
- try {
15463
- const stat3 = await fs22.stat(absPath);
15464
- return stat3.isFile();
15465
- } catch {
15466
- return false;
15467
- }
15468
- }
15469
- async function walkForSuffix(dir, needleVariants, depth, ctx) {
15470
- if (depth > MAX_WALK_DEPTH) return;
15471
- if (ctx.visited > MAX_VISITED_DIRS) return;
15472
- if (ctx.matches.length >= ctx.cap) return;
15473
- ctx.visited++;
15474
- let entries = [];
15475
- try {
15476
- entries = await fs22.readdir(dir, { withFileTypes: true });
15477
- } catch {
15478
- return;
15479
- }
15480
- for (const e of entries) {
15481
- if (!e.isFile()) continue;
15482
- const full = path26.join(dir, e.name);
15483
- if (needleVariants.some((needle) => full.endsWith(needle))) {
15484
- ctx.matches.push(full);
15485
- if (ctx.matches.length >= ctx.cap) return;
15486
- }
15487
- }
15488
- for (const e of entries) {
15489
- if (!e.isDirectory()) continue;
15490
- if (SUBDIR_IGNORE.has(e.name)) continue;
15491
- if (e.name.startsWith(".") && SUBDIR_IGNORE.has(e.name)) continue;
15492
- await walkForSuffix(path26.join(dir, e.name), needleVariants, depth + 1, ctx);
15493
- if (ctx.matches.length >= ctx.cap) return;
15494
- }
15495
- }
15496
- async function findFile(rawPath) {
15497
- const cwd = process.cwd();
15498
- if (path26.isAbsolute(rawPath)) {
15499
- const abs = path26.normalize(rawPath);
15500
- if (isUnder(cwd, abs) && await isExistingFile(abs)) return abs;
15501
- }
15502
- const direct = path26.resolve(cwd, rawPath);
15503
- if (isUnder(cwd, direct) && await isExistingFile(direct)) return direct;
15504
- const normalized = path26.normalize(rawPath).replace(/^[./\\]+/, "");
15505
- const needles = [
15506
- `${path26.sep}${normalized}`,
15507
- `/${normalized}`
15508
- ].filter((v, i, a) => a.indexOf(v) === i);
15509
- const ctx = { visited: 0, matches: [], cap: 16 };
15510
- await walkForSuffix(cwd, needles, 0, ctx);
15511
- const candidates = ctx.matches.filter((c2) => isUnder(cwd, c2));
15512
- if (candidates.length === 0) return null;
15513
- candidates.sort((a, b) => a.length - b.length);
15514
- return candidates[0];
15515
- }
15516
- async function findWriteTarget(rawPath) {
15517
- const found = await findFile(rawPath);
15518
- if (found) return found;
15519
- const cwd = process.cwd();
15520
- const fallback = path26.isAbsolute(rawPath) ? path26.normalize(rawPath) : path26.resolve(cwd, rawPath);
15521
- if (!isUnder(cwd, fallback)) return null;
15522
- return fallback;
15523
- }
15524
- function looksBinary(buf) {
15525
- const sample = buf.subarray(0, Math.min(8192, buf.length));
15526
- for (let i = 0; i < sample.length; i++) {
15527
- if (sample[i] === 0) return true;
15528
- }
15529
- return false;
15530
- }
15531
- async function readProjectFile(rawPath) {
15532
- try {
15533
- const abs = await findFile(rawPath);
15534
- if (!abs) {
15535
- return { error: `File not found in the project tree: ${rawPath}` };
15536
- }
15537
- const stat3 = await fs22.stat(abs);
15538
- if (stat3.size > MAX_FILE_BYTES) {
15539
- return { error: `File too large (${(stat3.size / 1024 / 1024).toFixed(1)} MB > ${MAX_FILE_BYTES / 1024 / 1024} MB).` };
15540
- }
15541
- const buf = await fs22.readFile(abs);
15542
- if (looksBinary(buf)) {
15543
- return { error: "Binary file \u2014 refusing to open in a code editor." };
15544
- }
15545
- return { content: buf.toString("utf-8") };
15546
- } catch (e) {
15547
- const msg = e instanceof Error ? e.message : "Read failed";
15548
- return { error: msg };
15549
- }
15550
- }
15551
- async function writeProjectFile(rawPath, content) {
15552
- try {
15553
- const abs = await findWriteTarget(rawPath);
15554
- if (!abs) {
15555
- return { error: `Path escapes the project root: ${rawPath}` };
15556
- }
15557
- if (Buffer.byteLength(content, "utf-8") > MAX_FILE_BYTES) {
15558
- return { error: "Content too large." };
15559
- }
15560
- await fs22.mkdir(path26.dirname(abs), { recursive: true });
15561
- await fs22.writeFile(abs, content, "utf-8");
15562
- return { ok: true };
15563
- } catch (e) {
15564
- const msg = e instanceof Error ? e.message : "Write failed";
15565
- return { error: msg };
15566
- }
15567
- }
15568
-
15569
- // src/services/project-ops.service.ts
15570
- var import_child_process8 = require("child_process");
15571
- var import_util2 = require("util");
15572
- var fs23 = __toESM(require("fs/promises"));
15573
15717
  var path27 = __toESM(require("path"));
15574
- var execFileP3 = (0, import_util2.promisify)(import_child_process8.execFile);
15575
- var PROJECT_IGNORE = /* @__PURE__ */ new Set([
15718
+ var MAX_FILE_BYTES = 5 * 1024 * 1024;
15719
+ var MAX_WALK_DEPTH = 6;
15720
+ var MAX_VISITED_DIRS = 5e3;
15721
+ var SUBDIR_IGNORE = /* @__PURE__ */ new Set([
15576
15722
  "node_modules",
15577
15723
  ".git",
15578
15724
  ".next",
@@ -15589,6 +15735,7 @@ var PROJECT_IGNORE = /* @__PURE__ */ new Set([
15589
15735
  ".vscode-test",
15590
15736
  "ios",
15591
15737
  "android",
15738
+ // expo-managed native dirs are huge and rarely interesting
15592
15739
  ".gradle",
15593
15740
  ".cxx",
15594
15741
  ".intellijPlatform",
@@ -15599,572 +15746,462 @@ var PROJECT_IGNORE = /* @__PURE__ */ new Set([
15599
15746
  ".venv",
15600
15747
  ".mypy_cache",
15601
15748
  ".pytest_cache",
15602
- "__pycache__",
15603
- ".DS_Store"
15749
+ "__pycache__"
15604
15750
  ]);
15605
- var MAX_TREE_FILES = 5e4;
15606
- var MAX_DIFF_BYTES = 512 * 1024;
15607
- var MAX_GIT_OUTPUT = 256 * 1024;
15608
- async function listProjectFiles(opts = {}) {
15609
- const root = opts.cwd ?? process.cwd();
15610
- const cap = opts.cap ?? MAX_TREE_FILES;
15611
- const q2 = (opts.query ?? "").trim().toLowerCase();
15612
- const out2 = [];
15613
- let truncated = false;
15614
- async function walk(dir, depth) {
15615
- if (out2.length >= cap) {
15616
- truncated = true;
15617
- return;
15618
- }
15619
- let entries = [];
15620
- try {
15621
- entries = await fs23.readdir(dir, { withFileTypes: true });
15622
- } catch {
15623
- return;
15624
- }
15625
- for (const e of entries) {
15626
- if (out2.length >= cap) {
15627
- truncated = true;
15628
- return;
15629
- }
15630
- if (PROJECT_IGNORE.has(e.name)) continue;
15631
- const full = path27.join(dir, e.name);
15632
- if (e.isDirectory()) {
15633
- if (depth >= 12) continue;
15634
- await walk(full, depth + 1);
15635
- } else if (e.isFile()) {
15636
- const rel = path27.relative(root, full);
15637
- if (q2 && !rel.toLowerCase().includes(q2) && !e.name.toLowerCase().includes(q2)) {
15638
- continue;
15639
- }
15640
- let size = 0;
15641
- try {
15642
- const st3 = await fs23.stat(full);
15643
- size = st3.size;
15644
- } catch {
15645
- }
15646
- out2.push({ path: rel, name: e.name, size });
15647
- }
15648
- }
15649
- }
15650
- await walk(root, 0);
15651
- out2.sort((a, b) => a.path.localeCompare(b.path));
15652
- return { files: out2, truncated, root };
15751
+ function isUnder(parent, candidate) {
15752
+ const rel = path27.relative(parent, candidate);
15753
+ return rel === "" || !rel.startsWith("..") && !path27.isAbsolute(rel);
15653
15754
  }
15654
- async function git(args2, cwd) {
15755
+ async function isExistingFile(absPath) {
15655
15756
  try {
15656
- const { stdout, stderr } = await execFileP3("git", args2, {
15657
- cwd: cwd ?? process.cwd(),
15658
- maxBuffer: MAX_GIT_OUTPUT,
15659
- timeout: 3e4
15660
- });
15661
- return { stdout, stderr, code: 0 };
15662
- } catch (err) {
15663
- const e = err;
15664
- return {
15665
- stdout: e.stdout ?? "",
15666
- stderr: e.stderr ?? e.message ?? "git failed",
15667
- code: typeof e.code === "number" ? e.code : 1
15668
- };
15757
+ const stat3 = await fs22.stat(absPath);
15758
+ return stat3.isFile();
15759
+ } catch {
15760
+ return false;
15669
15761
  }
15670
15762
  }
15671
- async function gitStatus(cwd) {
15672
- const root = cwd ?? process.cwd();
15673
- const r = await git(["status", "--porcelain=v2", "--branch"], root);
15674
- if (r.code !== 0) {
15675
- return {
15676
- branch: null,
15677
- upstream: null,
15678
- ahead: 0,
15679
- behind: 0,
15680
- entries: [],
15681
- hasMergeInProgress: false,
15682
- error: r.stderr.trim()
15683
- };
15684
- }
15685
- const lines = r.stdout.split("\n").filter(Boolean);
15686
- let branch = null;
15687
- let upstream = null;
15688
- let ahead = 0;
15689
- let behind = 0;
15690
- const entries = [];
15691
- for (const line of lines) {
15692
- if (line.startsWith("# branch.head ")) branch = line.slice("# branch.head ".length).trim();
15693
- else if (line.startsWith("# branch.upstream ")) upstream = line.slice("# branch.upstream ".length).trim();
15694
- else if (line.startsWith("# branch.ab ")) {
15695
- const m = line.match(/\+(\d+)\s+-(\d+)/);
15696
- if (m) {
15697
- ahead = parseInt(m[1], 10);
15698
- behind = parseInt(m[2], 10);
15699
- }
15700
- } else if (line.startsWith("1 ")) {
15701
- const parts = line.split(" ");
15702
- const xy = parts[1];
15703
- const p2 = parts.slice(8).join(" ");
15704
- entries.push({
15705
- code: xy,
15706
- path: p2,
15707
- staged: xy[0] !== ".",
15708
- conflict: false
15709
- });
15710
- } else if (line.startsWith("2 ")) {
15711
- const parts = line.split(" ");
15712
- const xy = parts[1];
15713
- const tail = parts.slice(9).join(" ");
15714
- const [newPath, oldPath] = tail.split(" ");
15715
- entries.push({
15716
- code: xy,
15717
- path: newPath ?? "",
15718
- oldPath: oldPath ?? void 0,
15719
- staged: xy[0] !== ".",
15720
- conflict: false
15721
- });
15722
- } else if (line.startsWith("? ")) {
15723
- entries.push({
15724
- code: "??",
15725
- path: line.slice(2),
15726
- staged: false,
15727
- conflict: false
15728
- });
15729
- } else if (line.startsWith("u ")) {
15730
- const parts = line.split(" ");
15731
- const xy = parts[1];
15732
- const p2 = parts.slice(10).join(" ");
15733
- entries.push({
15734
- code: xy,
15735
- path: p2,
15736
- staged: false,
15737
- conflict: true
15738
- });
15739
- }
15740
- }
15741
- let hasMergeInProgress = false;
15763
+ async function walkForSuffix(dir, needleVariants, depth, ctx) {
15764
+ if (depth > MAX_WALK_DEPTH) return;
15765
+ if (ctx.visited > MAX_VISITED_DIRS) return;
15766
+ if (ctx.matches.length >= ctx.cap) return;
15767
+ ctx.visited++;
15768
+ let entries = [];
15742
15769
  try {
15743
- 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");
15745
- await fs23.access(mergeHead);
15746
- hasMergeInProgress = true;
15770
+ entries = await fs22.readdir(dir, { withFileTypes: true });
15747
15771
  } catch {
15772
+ return;
15748
15773
  }
15749
- return { branch, upstream, ahead, behind, entries, hasMergeInProgress };
15750
- }
15751
- async function gitDiff(file, cwd) {
15752
- const args2 = ["diff", "--no-color", "--patch"];
15753
- if (file) args2.push("--", file);
15754
- const r = await git(args2, cwd);
15755
- if (r.code !== 0 && !r.stdout) {
15756
- return { diff: "", truncated: false, error: r.stderr.trim() };
15757
- }
15758
- const truncated = r.stdout.length >= MAX_DIFF_BYTES;
15759
- return { diff: r.stdout.slice(0, MAX_DIFF_BYTES), truncated };
15760
- }
15761
- async function gitDiffStaged(file, cwd) {
15762
- const args2 = ["diff", "--cached", "--no-color", "--patch"];
15763
- if (file) args2.push("--", file);
15764
- const r = await git(args2, cwd);
15765
- if (r.code !== 0 && !r.stdout) {
15766
- return { diff: "", truncated: false, error: r.stderr.trim() };
15767
- }
15768
- const truncated = r.stdout.length >= MAX_DIFF_BYTES;
15769
- return { diff: r.stdout.slice(0, MAX_DIFF_BYTES), truncated };
15770
- }
15771
- async function gitLog(limit = 30, cwd) {
15772
- const SEP = "";
15773
- const fmt = `%H${SEP}%s${SEP}%an${SEP}%ct${SEP}%D`;
15774
- const r = await git(["log", `-n${Math.min(limit, 200)}`, `--pretty=format:${fmt}`], cwd);
15775
- if (r.code !== 0) return { commits: [], error: r.stderr.trim() };
15776
- const commits = r.stdout.split("\n").filter(Boolean).map((line) => {
15777
- const [sha, subject, author, ts, refs] = line.split(SEP);
15778
- return {
15779
- sha: sha ?? "",
15780
- subject: subject ?? "",
15781
- author: author ?? "",
15782
- timestamp: parseInt(ts ?? "0", 10) * 1e3,
15783
- refs: (refs ?? "").split(",").map((s) => s.trim().replace(/^HEAD -> /, "")).filter((s) => s.length > 0)
15784
- };
15785
- });
15786
- return { commits };
15787
- }
15788
- async function gitCommit(message, files, cwd) {
15789
- if (!message || message.trim().length === 0) {
15790
- return { error: "Commit message is required." };
15791
- }
15792
- 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" };
15774
+ for (const e of entries) {
15775
+ if (!e.isFile()) continue;
15776
+ const full = path27.join(dir, e.name);
15777
+ if (needleVariants.some((needle) => full.endsWith(needle))) {
15778
+ ctx.matches.push(full);
15779
+ if (ctx.matches.length >= ctx.cap) return;
15780
+ }
15802
15781
  }
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(".");
15782
+ for (const e of entries) {
15783
+ if (!e.isDirectory()) continue;
15784
+ if (SUBDIR_IGNORE.has(e.name)) continue;
15785
+ if (e.name.startsWith(".") && SUBDIR_IGNORE.has(e.name)) continue;
15786
+ await walkForSuffix(path27.join(dir, e.name), needleVariants, depth + 1, ctx);
15787
+ if (ctx.matches.length >= ctx.cap) return;
15841
15788
  }
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);
15789
+ }
15790
+ async function findFile(rawPath) {
15791
+ const cwd = process.cwd();
15792
+ if (path27.isAbsolute(rawPath)) {
15793
+ const abs = path27.normalize(rawPath);
15794
+ if (isUnder(cwd, abs) && await isExistingFile(abs)) return abs;
15846
15795
  }
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
- });
15796
+ const direct = path27.resolve(cwd, rawPath);
15797
+ if (isUnder(cwd, direct) && await isExistingFile(direct)) return direct;
15798
+ const normalized = path27.normalize(rawPath).replace(/^[./\\]+/, "");
15799
+ const needles = [
15800
+ `${path27.sep}${normalized}`,
15801
+ `/${normalized}`
15802
+ ].filter((v, i, a) => a.indexOf(v) === i);
15803
+ const ctx = { visited: 0, matches: [], cap: 16 };
15804
+ await walkForSuffix(cwd, needles, 0, ctx);
15805
+ const candidates = ctx.matches.filter((c2) => isUnder(cwd, c2));
15806
+ if (candidates.length === 0) return null;
15807
+ candidates.sort((a, b) => a.length - b.length);
15808
+ return candidates[0];
15809
+ }
15810
+ async function findWriteTarget(rawPath) {
15811
+ const found = await findFile(rawPath);
15812
+ if (found) return found;
15813
+ const cwd = process.cwd();
15814
+ const fallback = path27.isAbsolute(rawPath) ? path27.normalize(rawPath) : path27.resolve(cwd, rawPath);
15815
+ if (!isUnder(cwd, fallback)) return null;
15816
+ return fallback;
15817
+ }
15818
+ function looksBinary(buf) {
15819
+ const sample = buf.subarray(0, Math.min(8192, buf.length));
15820
+ for (let i = 0; i < sample.length; i++) {
15821
+ if (sample[i] === 0) return true;
15876
15822
  }
15877
- return { hits, total: hits.length, truncated };
15823
+ return false;
15878
15824
  }
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;
15825
+ async function readProjectFile(rawPath) {
15826
+ try {
15827
+ const abs = await findFile(rawPath);
15828
+ if (!abs) {
15829
+ return { error: `File not found in the project tree: ${rawPath}` };
15888
15830
  }
15889
- let content = "";
15890
- try {
15891
- content = await fs23.readFile(path27.join(cwd, f.path), "utf8");
15892
- } catch {
15893
- continue;
15831
+ const stat3 = await fs22.stat(abs);
15832
+ if (stat3.size > MAX_FILE_BYTES) {
15833
+ return { error: `File too large (${(stat3.size / 1024 / 1024).toFixed(1)} MB > ${MAX_FILE_BYTES / 1024 / 1024} MB).` };
15894
15834
  }
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
- });
15835
+ const buf = await fs22.readFile(abs);
15836
+ if (looksBinary(buf)) {
15837
+ return { error: "Binary file \u2014 refusing to open in a code editor." };
15908
15838
  }
15839
+ return { content: buf.toString("utf-8") };
15840
+ } catch (e) {
15841
+ const msg = e instanceof Error ? e.message : "Read failed";
15842
+ return { error: msg };
15909
15843
  }
15910
- return { hits, total: hits.length, truncated };
15911
15844
  }
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");
15845
+ async function writeProjectFile(rawPath, content) {
15922
15846
  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;
15847
+ const abs = await findWriteTarget(rawPath);
15848
+ if (!abs) {
15849
+ return { error: `Path escapes the project root: ${rawPath}` };
15932
15850
  }
15851
+ if (Buffer.byteLength(content, "utf-8") > MAX_FILE_BYTES) {
15852
+ return { error: "Content too large." };
15853
+ }
15854
+ await fs22.mkdir(path27.dirname(abs), { recursive: true });
15855
+ await fs22.writeFile(abs, content, "utf-8");
15856
+ return { ok: true };
15857
+ } catch (e) {
15858
+ const msg = e instanceof Error ? e.message : "Write failed";
15859
+ return { error: msg };
15933
15860
  }
15934
15861
  }
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"]) {
15862
+
15863
+ // src/services/project-ops.service.ts
15864
+ var import_child_process9 = require("child_process");
15865
+ var import_util2 = require("util");
15866
+ var fs23 = __toESM(require("fs/promises"));
15867
+ var path28 = __toESM(require("path"));
15868
+ var execFileP3 = (0, import_util2.promisify)(import_child_process9.execFile);
15869
+ var PROJECT_IGNORE = /* @__PURE__ */ new Set([
15870
+ "node_modules",
15871
+ ".git",
15872
+ ".next",
15873
+ ".expo",
15874
+ "dist",
15875
+ "build",
15876
+ "out",
15877
+ ".cache",
15878
+ "coverage",
15879
+ ".turbo",
15880
+ ".parcel-cache",
15881
+ ".idea",
15882
+ ".vscode",
15883
+ ".vscode-test",
15884
+ "ios",
15885
+ "android",
15886
+ ".gradle",
15887
+ ".cxx",
15888
+ ".intellijPlatform",
15889
+ ".kotlin",
15890
+ "tmp",
15891
+ "target",
15892
+ "venv",
15893
+ ".venv",
15894
+ ".mypy_cache",
15895
+ ".pytest_cache",
15896
+ "__pycache__",
15897
+ ".DS_Store"
15898
+ ]);
15899
+ var MAX_TREE_FILES = 5e4;
15900
+ var MAX_DIFF_BYTES = 512 * 1024;
15901
+ var MAX_GIT_OUTPUT = 256 * 1024;
15902
+ async function listProjectFiles(opts = {}) {
15903
+ const root = opts.cwd ?? process.cwd();
15904
+ const cap = opts.cap ?? MAX_TREE_FILES;
15905
+ const q2 = (opts.query ?? "").trim().toLowerCase();
15906
+ const out2 = [];
15907
+ let truncated = false;
15908
+ async function walk(dir, depth) {
15909
+ if (out2.length >= cap) {
15910
+ truncated = true;
15911
+ return;
15912
+ }
15913
+ let entries = [];
16021
15914
  try {
16022
- const out2 = require("child_process").spawnSync("which", [name], { encoding: "utf8" });
16023
- if (out2.status === 0 && out2.stdout?.trim()) return out2.stdout.trim();
15915
+ entries = await fs23.readdir(dir, { withFileTypes: true });
16024
15916
  } catch {
15917
+ return;
15918
+ }
15919
+ for (const e of entries) {
15920
+ if (out2.length >= cap) {
15921
+ truncated = true;
15922
+ return;
15923
+ }
15924
+ if (PROJECT_IGNORE.has(e.name)) continue;
15925
+ const full = path28.join(dir, e.name);
15926
+ if (e.isDirectory()) {
15927
+ if (depth >= 12) continue;
15928
+ await walk(full, depth + 1);
15929
+ } else if (e.isFile()) {
15930
+ const rel = path28.relative(root, full);
15931
+ if (q2 && !rel.toLowerCase().includes(q2) && !e.name.toLowerCase().includes(q2)) {
15932
+ continue;
15933
+ }
15934
+ let size = 0;
15935
+ try {
15936
+ const st3 = await fs23.stat(full);
15937
+ size = st3.size;
15938
+ } catch {
15939
+ }
15940
+ out2.push({ path: rel, name: e.name, size });
15941
+ }
16025
15942
  }
16026
15943
  }
16027
- return null;
15944
+ await walk(root, 0);
15945
+ out2.sort((a, b) => a.path.localeCompare(b.path));
15946
+ return { files: out2, truncated, root };
16028
15947
  }
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;
15948
+ async function git(args2, cwd) {
16035
15949
  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"]
15950
+ const { stdout, stderr } = await execFileP3("git", args2, {
15951
+ cwd: cwd ?? process.cwd(),
15952
+ maxBuffer: MAX_GIT_OUTPUT,
15953
+ timeout: 3e4
16040
15954
  });
16041
- } catch (e) {
16042
- return { error: e instanceof Error ? e.message : "python spawn failed" };
15955
+ return { stdout, stderr, code: 0 };
15956
+ } catch (err) {
15957
+ const e = err;
15958
+ return {
15959
+ stdout: e.stdout ?? "",
15960
+ stderr: e.stderr ?? e.message ?? "git failed",
15961
+ code: typeof e.code === "number" ? e.code : 1
15962
+ };
16043
15963
  }
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
15964
  }
16077
- function openTerminal(opts) {
16078
- if (sessions.size >= MAX_CONCURRENT_SESSIONS) {
16079
- return { error: `Too many open terminals (max ${MAX_CONCURRENT_SESSIONS})` };
15965
+ async function gitStatus(cwd) {
15966
+ const root = cwd ?? process.cwd();
15967
+ const r = await git(["status", "--porcelain=v2", "--branch"], root);
15968
+ if (r.code !== 0) {
15969
+ return {
15970
+ branch: null,
15971
+ upstream: null,
15972
+ ahead: 0,
15973
+ behind: 0,
15974
+ entries: [],
15975
+ hasMergeInProgress: false,
15976
+ error: r.stderr.trim()
15977
+ };
16080
15978
  }
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
15979
+ const lines = r.stdout.split("\n").filter(Boolean);
15980
+ let branch = null;
15981
+ let upstream = null;
15982
+ let ahead = 0;
15983
+ let behind = 0;
15984
+ const entries = [];
15985
+ for (const line of lines) {
15986
+ if (line.startsWith("# branch.head ")) branch = line.slice("# branch.head ".length).trim();
15987
+ else if (line.startsWith("# branch.upstream ")) upstream = line.slice("# branch.upstream ".length).trim();
15988
+ else if (line.startsWith("# branch.ab ")) {
15989
+ const m = line.match(/\+(\d+)\s+-(\d+)/);
15990
+ if (m) {
15991
+ ahead = parseInt(m[1], 10);
15992
+ behind = parseInt(m[2], 10);
15993
+ }
15994
+ } else if (line.startsWith("1 ")) {
15995
+ const parts = line.split(" ");
15996
+ const xy = parts[1];
15997
+ const p2 = parts.slice(8).join(" ");
15998
+ entries.push({
15999
+ code: xy,
16000
+ path: p2,
16001
+ staged: xy[0] !== ".",
16002
+ conflict: false
16102
16003
  });
16103
- const dataListener = term.onData((data) => {
16104
- onDataHandler?.({ sessionId: id, data });
16004
+ } else if (line.startsWith("2 ")) {
16005
+ const parts = line.split(" ");
16006
+ const xy = parts[1];
16007
+ const tail = parts.slice(9).join(" ");
16008
+ const [newPath, oldPath] = tail.split(" ");
16009
+ entries.push({
16010
+ code: xy,
16011
+ path: newPath ?? "",
16012
+ oldPath: oldPath ?? void 0,
16013
+ staged: xy[0] !== ".",
16014
+ conflict: false
16105
16015
  });
16106
- const exitListener = term.onExit(({ exitCode }) => {
16107
- onExitHandler?.({ sessionId: id, exitCode });
16108
- sessions.delete(id);
16016
+ } else if (line.startsWith("? ")) {
16017
+ entries.push({
16018
+ code: "??",
16019
+ path: line.slice(2),
16020
+ staged: false,
16021
+ conflict: false
16109
16022
  });
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
- }
16023
+ } else if (line.startsWith("u ")) {
16024
+ const parts = line.split(" ");
16025
+ const xy = parts[1];
16026
+ const p2 = parts.slice(10).join(" ");
16027
+ entries.push({
16028
+ code: xy,
16029
+ path: p2,
16030
+ staged: false,
16031
+ conflict: true
16119
16032
  });
16120
- return { sessionId: id };
16121
- } catch (e) {
16122
- if (process.platform === "win32") {
16123
- return { error: e instanceof Error ? e.message : "spawn failed" };
16124
- }
16125
16033
  }
16126
- } else if (process.platform === "win32") {
16034
+ }
16035
+ let hasMergeInProgress = false;
16036
+ try {
16037
+ const gitDir = (await git(["rev-parse", "--git-dir"], root)).stdout.trim();
16038
+ const mergeHead = path28.isAbsolute(gitDir) ? path28.join(gitDir, "MERGE_HEAD") : path28.join(root, gitDir, "MERGE_HEAD");
16039
+ await fs23.access(mergeHead);
16040
+ hasMergeInProgress = true;
16041
+ } catch {
16042
+ }
16043
+ return { branch, upstream, ahead, behind, entries, hasMergeInProgress };
16044
+ }
16045
+ async function gitDiff(file, cwd) {
16046
+ const args2 = ["diff", "--no-color", "--patch"];
16047
+ if (file) args2.push("--", file);
16048
+ const r = await git(args2, cwd);
16049
+ if (r.code !== 0 && !r.stdout) {
16050
+ return { diff: "", truncated: false, error: r.stderr.trim() };
16051
+ }
16052
+ const truncated = r.stdout.length >= MAX_DIFF_BYTES;
16053
+ return { diff: r.stdout.slice(0, MAX_DIFF_BYTES), truncated };
16054
+ }
16055
+ async function gitDiffStaged(file, cwd) {
16056
+ const args2 = ["diff", "--cached", "--no-color", "--patch"];
16057
+ if (file) args2.push("--", file);
16058
+ const r = await git(args2, cwd);
16059
+ if (r.code !== 0 && !r.stdout) {
16060
+ return { diff: "", truncated: false, error: r.stderr.trim() };
16061
+ }
16062
+ const truncated = r.stdout.length >= MAX_DIFF_BYTES;
16063
+ return { diff: r.stdout.slice(0, MAX_DIFF_BYTES), truncated };
16064
+ }
16065
+ async function gitLog(limit = 30, cwd) {
16066
+ const SEP = "";
16067
+ const fmt = `%H${SEP}%s${SEP}%an${SEP}%ct${SEP}%D`;
16068
+ const r = await git(["log", `-n${Math.min(limit, 200)}`, `--pretty=format:${fmt}`], cwd);
16069
+ if (r.code !== 0) return { commits: [], error: r.stderr.trim() };
16070
+ const commits = r.stdout.split("\n").filter(Boolean).map((line) => {
16071
+ const [sha, subject, author, ts, refs] = line.split(SEP);
16127
16072
  return {
16128
- error: `node-pty native module unavailable on ${process.platform}-${process.arch}; terminal feature disabled for this platform`
16073
+ sha: sha ?? "",
16074
+ subject: subject ?? "",
16075
+ author: author ?? "",
16076
+ timestamp: parseInt(ts ?? "0", 10) * 1e3,
16077
+ refs: (refs ?? "").split(",").map((s) => s.trim().replace(/^HEAD -> /, "")).filter((s) => s.length > 0)
16129
16078
  };
16079
+ });
16080
+ return { commits };
16081
+ }
16082
+ async function gitCommit(message, files, cwd) {
16083
+ if (!message || message.trim().length === 0) {
16084
+ return { error: "Commit message is required." };
16085
+ }
16086
+ if (files && files.length > 0) {
16087
+ const r2 = await git(["add", "--", ...files], cwd);
16088
+ if (r2.code !== 0) return { error: `git add failed: ${r2.stderr.trim()}` };
16089
+ } else {
16090
+ const r2 = await git(["add", "-A"], cwd);
16091
+ if (r2.code !== 0) return { error: `git add failed: ${r2.stderr.trim()}` };
16130
16092
  }
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" };
16093
+ const r = await git(["commit", "-m", message], cwd);
16094
+ if (r.code !== 0) {
16095
+ return { error: r.stderr.trim() || "git commit failed" };
16144
16096
  }
16097
+ const head = await git(["rev-parse", "HEAD"], cwd);
16098
+ return { ok: true, commit: head.stdout.trim() };
16145
16099
  }
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
- }
16100
+ async function gitPush(cwd) {
16101
+ const r = await git(["push"], cwd);
16102
+ if (r.code !== 0) return { error: r.stderr.trim() || "git push failed" };
16103
+ return { ok: true, output: (r.stdout + r.stderr).trim() };
16155
16104
  }
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);
16105
+ async function gitPull(cwd) {
16106
+ const r = await git(["pull", "--ff-only"], cwd);
16107
+ if (r.code !== 0) return { error: r.stderr.trim() || "git pull failed" };
16108
+ return { ok: true, output: (r.stdout + r.stderr).trim() };
16109
+ }
16110
+ async function gitResolve(file, side, cwd) {
16111
+ const r = await git(["checkout", `--${side}`, "--", file], cwd);
16112
+ if (r.code !== 0) return { error: r.stderr.trim() || `git checkout --${side} failed` };
16113
+ const add = await git(["add", "--", file], cwd);
16114
+ if (add.code !== 0) return { error: add.stderr.trim() || "git add (resolve) failed" };
16164
16115
  return { ok: true };
16165
16116
  }
16166
- function closeAllTerminals() {
16167
- for (const id of Array.from(sessions.keys())) closeTerminal(id);
16117
+ var MAX_SEARCH_HITS = 500;
16118
+ var MAX_SEARCH_BYTES = 256 * 1024;
16119
+ async function searchFiles(opts) {
16120
+ const cwd = opts.cwd ?? process.cwd();
16121
+ const cap = Math.min(opts.maxResults ?? MAX_SEARCH_HITS, MAX_SEARCH_HITS);
16122
+ if (!opts.query.trim()) return { hits: [], total: 0, truncated: false };
16123
+ const args2 = ["grep", "-n", "--column", "-I"];
16124
+ if (!opts.caseSensitive) args2.push("-i");
16125
+ if (opts.wholeWord) args2.push("-w");
16126
+ if (opts.regex) args2.push("-E");
16127
+ else args2.push("-F");
16128
+ args2.push(opts.query);
16129
+ if (opts.include && opts.include.length > 0) {
16130
+ args2.push("--");
16131
+ for (const p2 of opts.include) args2.push(p2);
16132
+ } else if (opts.exclude && opts.exclude.length > 0) {
16133
+ args2.push("--");
16134
+ args2.push(".");
16135
+ }
16136
+ for (const p2 of opts.exclude ?? []) args2.push(`:!${p2}`);
16137
+ const r = await git(args2, cwd);
16138
+ if (r.code !== 0 && r.code !== 1) {
16139
+ return jsSearchFiles(opts, cwd, cap);
16140
+ }
16141
+ const hits = [];
16142
+ const lines = r.stdout.split("\n");
16143
+ let truncated = false;
16144
+ let byteBudget = MAX_SEARCH_BYTES;
16145
+ for (const line of lines) {
16146
+ if (!line) continue;
16147
+ if (hits.length >= cap) {
16148
+ truncated = true;
16149
+ break;
16150
+ }
16151
+ if (byteBudget <= 0) {
16152
+ truncated = true;
16153
+ break;
16154
+ }
16155
+ byteBudget -= line.length;
16156
+ const m = line.match(/^([^]+?):(\d+):(\d+):(.*)$/);
16157
+ if (!m) continue;
16158
+ const filePath = m[1] ?? "";
16159
+ const lineNo = parseInt(m[2] ?? "0", 10);
16160
+ const col = parseInt(m[3] ?? "1", 10);
16161
+ const text = (m[4] ?? "").slice(0, 400);
16162
+ if (!filePath) continue;
16163
+ hits.push({
16164
+ path: filePath,
16165
+ line: lineNo,
16166
+ column: col,
16167
+ text,
16168
+ matchLength: opts.query.length
16169
+ });
16170
+ }
16171
+ return { hits, total: hits.length, truncated };
16172
+ }
16173
+ async function jsSearchFiles(opts, cwd, cap) {
16174
+ const files = await listProjectFiles({ cwd, cap: 2e3 });
16175
+ const hits = [];
16176
+ const needle = opts.caseSensitive ? opts.query : opts.query.toLowerCase();
16177
+ let truncated = files.truncated;
16178
+ for (const f of files.files) {
16179
+ if (hits.length >= cap) {
16180
+ truncated = true;
16181
+ break;
16182
+ }
16183
+ let content = "";
16184
+ try {
16185
+ content = await fs23.readFile(path28.join(cwd, f.path), "utf8");
16186
+ } catch {
16187
+ continue;
16188
+ }
16189
+ const lines = content.split("\n");
16190
+ for (let i = 0; i < lines.length && hits.length < cap; i++) {
16191
+ const line = lines[i] ?? "";
16192
+ const hay = opts.caseSensitive ? line : line.toLowerCase();
16193
+ const idx = hay.indexOf(needle);
16194
+ if (idx === -1) continue;
16195
+ hits.push({
16196
+ path: f.path,
16197
+ line: i + 1,
16198
+ column: idx + 1,
16199
+ text: line.slice(0, 400),
16200
+ matchLength: opts.query.length
16201
+ });
16202
+ }
16203
+ }
16204
+ return { hits, total: hits.length, truncated };
16168
16205
  }
16169
16206
 
16170
16207
  // src/services/apply-file-review.service.ts
@@ -17876,6 +17913,7 @@ var import_child_process13 = require("child_process");
17876
17913
  var fs30 = __toESM(require("fs"));
17877
17914
  var os24 = __toESM(require("os"));
17878
17915
  var path35 = __toESM(require("path"));
17916
+ var import_ignore = __toESM(require("ignore"));
17879
17917
 
17880
17918
  // src/services/file-watcher/diff-parser.ts
17881
17919
  var HUNK_HEADER_RE = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/;
@@ -18099,6 +18137,17 @@ var FileWatcherService = class {
18099
18137
  * doesn't hammer `fs.statSync` for every event.
18100
18138
  */
18101
18139
  gitRootByDir = /* @__PURE__ */ new Map();
18140
+ /**
18141
+ * Per-repo `.gitignore` matcher. On first encounter of a git root we
18142
+ * collect every `.gitignore` file under it, parse them through the
18143
+ * `ignore` package, and store the resulting matcher here keyed by
18144
+ * absolute repo path. Subsequent file events in the same repo reuse
18145
+ * the matcher in O(1). The hard-coded IGNORED_PATH_PATTERN above
18146
+ * catches conventional dirs (node_modules, dist, Pods, …); this
18147
+ * matcher layers the repo's own ignore rules on top so per-project
18148
+ * artifacts (ios/, .env*, build outputs) stop polluting the queue.
18149
+ */
18150
+ gitIgnoreMatcherByRoot = /* @__PURE__ */ new Map();
18102
18151
  stopped = false;
18103
18152
  /**
18104
18153
  * Cross-file coalescing buffer. Keyed by absPath so multiple
@@ -18324,9 +18373,17 @@ var FileWatcherService = class {
18324
18373
  );
18325
18374
  return;
18326
18375
  }
18327
- this.opts.onRepoDirty?.(gitRoot);
18328
18376
  const relPathInRepo = path35.relative(gitRoot, absPath);
18329
18377
  if (!relPathInRepo || relPathInRepo.startsWith("..")) return;
18378
+ const matcher = this.getGitIgnoreMatcher(gitRoot);
18379
+ if (matcher && matcher.ignores(relPathInRepo)) {
18380
+ log.trace(
18381
+ "fileWatcher",
18382
+ `${relPathInRepo} ignored by ${path35.basename(gitRoot)}/.gitignore \u2014 suppressing emit`
18383
+ );
18384
+ return;
18385
+ }
18386
+ this.opts.onRepoDirty?.(gitRoot);
18330
18387
  const repoPath = path35.relative(this.opts.workingDir, gitRoot);
18331
18388
  const repoName = path35.basename(gitRoot);
18332
18389
  let diffText = "";
@@ -18462,6 +18519,79 @@ var FileWatcherService = class {
18462
18519
  async postReviewBlame(body) {
18463
18520
  await this.postWithRetries(`${this.apiBase}/api/review/blame`, body);
18464
18521
  }
18522
+ /**
18523
+ * Lazily build and cache a per-repo `.gitignore` matcher. We walk the
18524
+ * repo collecting every `.gitignore` file (skipping the same dirs
18525
+ * IGNORED_PATH_PATTERN already filters at chokidar level, so we
18526
+ * don't read inside node_modules / Pods / etc.) and feed each file
18527
+ * into a single `ignore` matcher anchored at the git root. Subsequent
18528
+ * calls return the cached matcher; failures fall back to `null`,
18529
+ * which the caller treats as "no extra filtering" — so a malformed
18530
+ * .gitignore degrades to the prior pre-fix behaviour rather than
18531
+ * silently dropping every event.
18532
+ */
18533
+ getGitIgnoreMatcher(gitRoot) {
18534
+ if (this.gitIgnoreMatcherByRoot.has(gitRoot)) {
18535
+ return this.gitIgnoreMatcherByRoot.get(gitRoot) ?? null;
18536
+ }
18537
+ const matcher = (0, import_ignore.default)();
18538
+ let added = 0;
18539
+ try {
18540
+ this.collectGitignoreFiles(gitRoot, gitRoot, matcher);
18541
+ added = 1;
18542
+ } catch (err) {
18543
+ log.warn(
18544
+ "fileWatcher",
18545
+ `failed to build gitignore matcher for ${gitRoot}: ${err.message}`
18546
+ );
18547
+ }
18548
+ const result = added > 0 ? matcher : null;
18549
+ this.gitIgnoreMatcherByRoot.set(gitRoot, result);
18550
+ return result;
18551
+ }
18552
+ /**
18553
+ * Walk the repo recursively collecting every `.gitignore` file and
18554
+ * add its rules to `matcher`, with the path prefix that anchors them
18555
+ * to the right subdirectory (so a `.gitignore` inside `apps/api`
18556
+ * scopes to `apps/api/*`, not the whole repo). Skips heavy dirs the
18557
+ * static IGNORED_PATH_PATTERN already filters — we don't want to
18558
+ * stat into `node_modules/` looking for buried .gitignore files.
18559
+ */
18560
+ collectGitignoreFiles(repoRoot, dir, matcher) {
18561
+ let entries;
18562
+ try {
18563
+ entries = fs30.readdirSync(dir, { withFileTypes: true });
18564
+ } catch {
18565
+ return;
18566
+ }
18567
+ const gitignoreEntry = entries.find(
18568
+ (e) => e.isFile() && e.name === ".gitignore"
18569
+ );
18570
+ if (gitignoreEntry) {
18571
+ try {
18572
+ const body = fs30.readFileSync(path35.join(dir, ".gitignore"), "utf8");
18573
+ const rel = path35.relative(repoRoot, dir).replace(/\\/g, "/");
18574
+ const prefixed = body.split(/\r?\n/).map((line) => {
18575
+ const trimmed = line.trim();
18576
+ if (!trimmed || trimmed.startsWith("#")) return line;
18577
+ if (!rel) return line;
18578
+ if (trimmed.startsWith("!")) {
18579
+ return "!" + path35.posix.join(rel, trimmed.slice(1));
18580
+ }
18581
+ return path35.posix.join(rel, trimmed);
18582
+ }).join("\n");
18583
+ matcher.add(prefixed);
18584
+ } catch {
18585
+ }
18586
+ }
18587
+ for (const entry of entries) {
18588
+ if (!entry.isDirectory()) continue;
18589
+ if (entry.name === ".git") continue;
18590
+ const childAbs = path35.join(dir, entry.name);
18591
+ if (isIgnoredFilePath(childAbs)) continue;
18592
+ this.collectGitignoreFiles(repoRoot, childAbs, matcher);
18593
+ }
18594
+ }
18465
18595
  async postWithRetries(url, body) {
18466
18596
  const payload = JSON.stringify(body);
18467
18597
  const headers = {
@@ -19425,6 +19555,24 @@ async function runAcpSession(opts) {
19425
19555
  pluginAuthToken: opts.pluginAuthToken
19426
19556
  });
19427
19557
  const streaming = new StreamingState(publisher);
19558
+ registerTerminalHandlers({
19559
+ onData: ({ sessionId, data }) => {
19560
+ void publisher.publishOutput({
19561
+ type: "terminal_data",
19562
+ terminalSessionId: sessionId,
19563
+ data,
19564
+ done: false
19565
+ });
19566
+ },
19567
+ onExit: ({ sessionId, exitCode }) => {
19568
+ void publisher.publishOutput({
19569
+ type: "terminal_exit",
19570
+ terminalSessionId: sessionId,
19571
+ exitCode,
19572
+ done: true
19573
+ });
19574
+ }
19575
+ });
19428
19576
  let updateCount = 0;
19429
19577
  const client2 = new AcpClient({
19430
19578
  adapter: opts.adapter,
@@ -19502,20 +19650,37 @@ async function runAcpSession(opts) {
19502
19650
  const runtime = createInteractiveAgentStrategy(opts.agent, createOsStrategy());
19503
19651
  const models = await runtime.listModels();
19504
19652
  const history = new AcpHistory(publisher, { agent: opts.agent, acpSessionId });
19505
- const fileWatcher = new FileWatcherService({
19653
+ const turnFiles = new TurnFileAggregator({
19506
19654
  workingDir: opts.cwd,
19507
19655
  sessionId: opts.sessionId,
19508
19656
  pluginId: opts.pluginId,
19509
- pluginAuthToken: opts.pluginAuthToken
19657
+ pluginAuthToken: opts.pluginAuthToken,
19658
+ agentId: opts.agent
19510
19659
  });
19511
- const turnFiles = new TurnFileAggregator({
19660
+ const REPO_DIRTY_FLUSH_DEBOUNCE_MS = 2e3;
19661
+ let repoDirtyTimer = null;
19662
+ const fileWatcher = new FileWatcherService({
19512
19663
  workingDir: opts.cwd,
19513
19664
  sessionId: opts.sessionId,
19514
19665
  pluginId: opts.pluginId,
19515
19666
  pluginAuthToken: opts.pluginAuthToken,
19516
- agentId: opts.agent
19667
+ onRepoDirty: () => {
19668
+ if (repoDirtyTimer) clearTimeout(repoDirtyTimer);
19669
+ repoDirtyTimer = setTimeout(() => {
19670
+ repoDirtyTimer = null;
19671
+ log.info("acpRunner", "onRepoDirty debounce fired \u2014 running flushTurn");
19672
+ turnFiles.flushTurn().catch((err) => {
19673
+ log.warn("acpRunner", `flushTurn from onRepoDirty failed: ${describeError(err)}`);
19674
+ });
19675
+ }, REPO_DIRTY_FLUSH_DEBOUNCE_MS);
19676
+ }
19517
19677
  });
19518
- fileWatcher.start().catch((err) => {
19678
+ fileWatcher.start().then(() => {
19679
+ log.info(
19680
+ "acpRunner",
19681
+ `fileWatcher started \u2014 watching cwd=${opts.cwd} for file changes (debounce ${REPO_DIRTY_FLUSH_DEBOUNCE_MS}ms before flushTurn)`
19682
+ );
19683
+ }).catch((err) => {
19519
19684
  log.warn("acpRunner", `fileWatcher.start failed: ${describeError(err)}`);
19520
19685
  });
19521
19686
  const relay = new CommandRelayService(
@@ -19542,6 +19707,7 @@ async function runAcpSession(opts) {
19542
19707
  relay.stop();
19543
19708
  void fileWatcher.stop();
19544
19709
  turnFiles.stop();
19710
+ closeAllTerminals();
19545
19711
  await client2.stop();
19546
19712
  process.exit(0);
19547
19713
  };
@@ -19756,6 +19922,7 @@ async function handleCommand(cmd, client2, relay, acpSessionId, models, streamin
19756
19922
  }
19757
19923
  }
19758
19924
  relay.stop();
19925
+ closeAllTerminals();
19759
19926
  await client2.stop();
19760
19927
  process.exit(0);
19761
19928
  return;
@@ -19765,26 +19932,58 @@ async function handleCommand(cmd, client2, relay, acpSessionId, models, streamin
19765
19932
  case "preview_stop":
19766
19933
  case "save_preview_config": {
19767
19934
  const runtime = createInteractiveAgentStrategy(opts.agent, createOsStrategy());
19768
- const ctx = buildLegacyContextForACP(opts, relay, runtime);
19935
+ let previewHandlerAcked = false;
19936
+ const ackingRelay = new Proxy(relay, {
19937
+ get(target, prop, receiver) {
19938
+ if (prop === "sendResult") {
19939
+ return (commandId, status2, result) => {
19940
+ if (commandId === cmd.id) previewHandlerAcked = true;
19941
+ return target.sendResult(commandId, status2, result);
19942
+ };
19943
+ }
19944
+ return Reflect.get(target, prop, receiver);
19945
+ }
19946
+ });
19947
+ const ctx = buildLegacyContextForACP(opts, ackingRelay, runtime);
19769
19948
  try {
19770
19949
  await dispatchCommand(ctx, cmd);
19771
- await relay.sendResult(cmd.id, "completed", {});
19950
+ if (!previewHandlerAcked) {
19951
+ await relay.sendResult(cmd.id, "completed", {});
19952
+ }
19772
19953
  } catch (err) {
19773
19954
  log.warn("acpRunner", `${cmd.type} failed: ${describeError(err)}`);
19774
- await relay.sendResult(cmd.id, "failed", { error: describeError(err) });
19955
+ if (!previewHandlerAcked) {
19956
+ await relay.sendResult(cmd.id, "failed", { error: describeError(err) });
19957
+ }
19775
19958
  }
19776
19959
  return;
19777
19960
  }
19778
19961
  default:
19779
19962
  if (handlers[cmd.type]) {
19780
19963
  const runtime = createInteractiveAgentStrategy(opts.agent, createOsStrategy());
19781
- const legacyCtx = buildLegacyContextForACP(opts, relay, runtime);
19964
+ let handlerAcked = false;
19965
+ const ackingRelay = new Proxy(relay, {
19966
+ get(target, prop, receiver) {
19967
+ if (prop === "sendResult") {
19968
+ return (commandId, status2, result) => {
19969
+ if (commandId === cmd.id) handlerAcked = true;
19970
+ return target.sendResult(commandId, status2, result);
19971
+ };
19972
+ }
19973
+ return Reflect.get(target, prop, receiver);
19974
+ }
19975
+ });
19976
+ const legacyCtx = buildLegacyContextForACP(opts, ackingRelay, runtime);
19782
19977
  try {
19783
19978
  await dispatchCommand(legacyCtx, cmd);
19784
- await relay.sendResult(cmd.id, "completed", {});
19979
+ if (!handlerAcked) {
19980
+ await relay.sendResult(cmd.id, "completed", {});
19981
+ }
19785
19982
  } catch (err) {
19786
19983
  log.warn("acpRunner", `legacy handler "${cmd.type}" threw: ${describeError(err)}`);
19787
- await relay.sendResult(cmd.id, "failed", { error: describeError(err) });
19984
+ if (!handlerAcked) {
19985
+ await relay.sendResult(cmd.id, "failed", { error: describeError(err) });
19986
+ }
19788
19987
  }
19789
19988
  return;
19790
19989
  }
@@ -21551,6 +21750,19 @@ async function start(requestedAgent) {
21551
21750
  requestedAgent: requestedAgent ?? null
21552
21751
  });
21553
21752
  const cwd = process.cwd();
21753
+ const refreshed = await fetchCurrentPluginAuthToken(session.id, pluginId);
21754
+ if (refreshed && refreshed !== session.pluginAuthToken) {
21755
+ addSession({ ...session, pluginAuthToken: refreshed });
21756
+ session.pluginAuthToken = refreshed;
21757
+ showInfo("Reconnected \u2014 refreshed plugin auth token.");
21758
+ } else if (refreshed) {
21759
+ showInfo("Reconnected previous session.");
21760
+ }
21761
+ const tokenForLog = session.pluginAuthToken ?? "(unset)";
21762
+ log.trace(
21763
+ "pluginAuth",
21764
+ `boot triple sessionId=${session.id} pluginId=${pluginId} tokenLen=${tokenForLog.length} tokenHead=${tokenForLog.slice(0, 12)} tokenTail=${tokenForLog.slice(-8)} mintedEqualsCached=${refreshed === session.pluginAuthToken}`
21765
+ );
21554
21766
  const acpDisabled = process.env.CODEAM_ACP_DISABLED === "1";
21555
21767
  if (!acpDisabled && session.pluginAuthToken) {
21556
21768
  const adapter = getAcpAdapter(session.agent);
@@ -24467,7 +24679,7 @@ function checkChokidar() {
24467
24679
  }
24468
24680
  async function doctor(args2 = []) {
24469
24681
  const json = args2.includes("--json");
24470
- const cliVersion = true ? "2.27.16" : "0.0.0-dev";
24682
+ const cliVersion = true ? "2.28.1" : "0.0.0-dev";
24471
24683
  const apiBase = resolveApiBaseUrl();
24472
24684
  const diagnosticId = (0, import_node_crypto8.randomUUID)();
24473
24685
  log.info("doctor", `run id=${diagnosticId} cli=${cliVersion}`);
@@ -24666,7 +24878,7 @@ async function completion(args2) {
24666
24878
  // src/commands/version.ts
24667
24879
  var import_picocolors13 = __toESM(require("picocolors"));
24668
24880
  function version2() {
24669
- const v = true ? "2.27.16" : "unknown";
24881
+ const v = true ? "2.28.1" : "unknown";
24670
24882
  console.log(`${import_picocolors13.default.bold("codeam-cli")} ${import_picocolors13.default.cyan(v)}`);
24671
24883
  }
24672
24884
 
@@ -24798,6 +25010,7 @@ var fs35 = __toESM(require("fs"));
24798
25010
  var os27 = __toESM(require("os"));
24799
25011
  var path44 = __toESM(require("path"));
24800
25012
  var https7 = __toESM(require("https"));
25013
+ var import_node_child_process12 = require("child_process");
24801
25014
  var import_picocolors16 = __toESM(require("picocolors"));
24802
25015
  var PKG_NAME = "codeam-cli";
24803
25016
  var REGISTRY_URL = `https://registry.npmjs.org/${PKG_NAME}/latest`;
@@ -24889,17 +25102,74 @@ function notifyIfStale(currentVersion, latest) {
24889
25102
  ];
24890
25103
  process.stderr.write(lines.join("\n"));
24891
25104
  }
25105
+ function isLinkedInstall() {
25106
+ try {
25107
+ const root = (0, import_node_child_process12.execSync)("npm root -g", {
25108
+ encoding: "utf8",
25109
+ stdio: ["ignore", "pipe", "ignore"],
25110
+ timeout: 2e3
25111
+ }).trim();
25112
+ if (!root) return false;
25113
+ const pkgPath = path44.join(root, PKG_NAME);
25114
+ return fs35.lstatSync(pkgPath).isSymbolicLink();
25115
+ } catch {
25116
+ return false;
25117
+ }
25118
+ }
25119
+ function maybeAutoUpdate(currentVersion, latest) {
25120
+ if (compareSemver(latest, currentVersion) <= 0) return;
25121
+ if (process.env.CODEAM_NO_AUTO_UPDATE === "1") {
25122
+ notifyIfStale(currentVersion, latest);
25123
+ return;
25124
+ }
25125
+ if (isLinkedInstall()) {
25126
+ notifyIfStale(currentVersion, latest);
25127
+ return;
25128
+ }
25129
+ process.stderr.write(
25130
+ `
25131
+ ${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)}...
25132
+
25133
+ `
25134
+ );
25135
+ const install = (0, import_node_child_process12.spawnSync)("npm", ["install", "-g", `${PKG_NAME}@latest`], {
25136
+ stdio: "inherit",
25137
+ env: process.env
25138
+ });
25139
+ if (install.status !== 0) {
25140
+ process.stderr.write(
25141
+ `
25142
+ ${import_picocolors16.default.red("!")} Update failed (exit ${install.status ?? "?"}). Continuing on ${currentVersion}.
25143
+ Run ${import_picocolors16.default.cyan("npm install -g codeam-cli")} manually to retry.
25144
+
25145
+ `
25146
+ );
25147
+ return;
25148
+ }
25149
+ try {
25150
+ fs35.unlinkSync(cachePath());
25151
+ } catch {
25152
+ }
25153
+ process.stderr.write(` ${import_picocolors16.default.green("\u2713")} Updated. Resuming session...
25154
+
25155
+ `);
25156
+ const child = (0, import_node_child_process12.spawnSync)("codeam", process.argv.slice(2), {
25157
+ stdio: "inherit",
25158
+ env: process.env
25159
+ });
25160
+ process.exit(child.status ?? 0);
25161
+ }
24892
25162
  function checkForUpdates() {
24893
25163
  if (process.env.NODE_ENV === "test") return;
24894
25164
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
24895
25165
  if (process.env.CI) return;
24896
25166
  if (!process.stdout.isTTY) return;
24897
- const current = true ? "2.27.16" : null;
25167
+ const current = true ? "2.28.1" : null;
24898
25168
  if (!current) return;
24899
25169
  const cache = readCache();
24900
25170
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
24901
25171
  if (fresh && cache) {
24902
- notifyIfStale(current, cache.latest);
25172
+ maybeAutoUpdate(current, cache.latest);
24903
25173
  return;
24904
25174
  }
24905
25175
  void fetchLatest().then((latest) => {