codeam-cli 2.20.2 → 2.21.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 +12 -0
  2. package/dist/index.js +641 -144
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -441,7 +441,7 @@ var import_qrcode_terminal = __toESM(require("qrcode-terminal"));
441
441
  // package.json
442
442
  var package_default = {
443
443
  name: "codeam-cli",
444
- version: "2.20.2",
444
+ version: "2.21.0",
445
445
  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.",
446
446
  type: "commonjs",
447
447
  main: "dist/index.js",
@@ -1026,8 +1026,8 @@ function createGetModuleFromFilename(basePath = process.argv[1] ? (0, import_pat
1026
1026
  return decodedFile;
1027
1027
  };
1028
1028
  }
1029
- function normalizeWindowsPath(path37) {
1030
- return path37.replace(/^[A-Z]:/, "").replace(/\\/g, "/");
1029
+ function normalizeWindowsPath(path39) {
1030
+ return path39.replace(/^[A-Z]:/, "").replace(/\\/g, "/");
1031
1031
  }
1032
1032
 
1033
1033
  // ../../node_modules/@posthog/core/dist/featureFlagUtils.mjs
@@ -3507,9 +3507,9 @@ async function addSourceContext(frames) {
3507
3507
  LRU_FILE_CONTENTS_CACHE.reduce();
3508
3508
  return frames;
3509
3509
  }
3510
- function getContextLinesFromFile(path37, ranges, output) {
3510
+ function getContextLinesFromFile(path39, ranges, output) {
3511
3511
  return new Promise((resolve4) => {
3512
- const stream = (0, import_node_fs.createReadStream)(path37);
3512
+ const stream = (0, import_node_fs.createReadStream)(path39);
3513
3513
  const lineReaded = (0, import_node_readline.createInterface)({
3514
3514
  input: stream
3515
3515
  });
@@ -3524,7 +3524,7 @@ function getContextLinesFromFile(path37, ranges, output) {
3524
3524
  let rangeStart = range[0];
3525
3525
  let rangeEnd = range[1];
3526
3526
  function onStreamError() {
3527
- LRU_FILE_CONTENTS_FS_READ_FAILED.set(path37, 1);
3527
+ LRU_FILE_CONTENTS_FS_READ_FAILED.set(path39, 1);
3528
3528
  lineReaded.close();
3529
3529
  lineReaded.removeAllListeners();
3530
3530
  destroyStreamAndResolve();
@@ -3585,8 +3585,8 @@ function clearLineContext(frame) {
3585
3585
  delete frame.context_line;
3586
3586
  delete frame.post_context;
3587
3587
  }
3588
- function shouldSkipContextLinesForFile(path37) {
3589
- return path37.startsWith("node:") || path37.endsWith(".min.js") || path37.endsWith(".min.cjs") || path37.endsWith(".min.mjs") || path37.startsWith("data:");
3588
+ function shouldSkipContextLinesForFile(path39) {
3589
+ return path39.startsWith("node:") || path39.endsWith(".min.js") || path39.endsWith(".min.cjs") || path39.endsWith(".min.mjs") || path39.startsWith("data:");
3590
3590
  }
3591
3591
  function shouldSkipContextLinesForFrame(frame) {
3592
3592
  if (void 0 !== frame.lineno && frame.lineno > MAX_CONTEXTLINES_LINENO) return true;
@@ -5740,7 +5740,7 @@ function readAnonId() {
5740
5740
  }
5741
5741
  function superProperties() {
5742
5742
  return {
5743
- cliVersion: true ? "2.20.2" : "0.0.0-dev",
5743
+ cliVersion: true ? "2.21.0" : "0.0.0-dev",
5744
5744
  nodeVersion: process.version,
5745
5745
  platform: process.platform,
5746
5746
  arch: process.arch,
@@ -10610,11 +10610,11 @@ function parseReview(stdout) {
10610
10610
  for (const line of lines) {
10611
10611
  const m = line.match(HUNK_LINE_RE);
10612
10612
  if (!m) continue;
10613
- const [, path37, lineNo, sevToken, message] = m;
10614
- if (!path37 || !lineNo || !message) continue;
10613
+ const [, path39, lineNo, sevToken, message] = m;
10614
+ if (!path39 || !lineNo || !message) continue;
10615
10615
  const cleanedMessage = message.trim().replace(/^[*-]\s+/, "");
10616
10616
  hunks.push({
10617
- path: path37.trim(),
10617
+ path: path39.trim(),
10618
10618
  line: Number(lineNo),
10619
10619
  severity: sevToken ? SEVERITY_MAP[sevToken.toLowerCase()] : void 0,
10620
10620
  message: cleanedMessage
@@ -12576,6 +12576,7 @@ var FileWatcherService = class {
12576
12576
  );
12577
12577
  return;
12578
12578
  }
12579
+ this.opts.onRepoDirty?.(gitRoot);
12579
12580
  const relPathInRepo = path25.relative(gitRoot, absPath);
12580
12581
  if (!relPathInRepo || relPathInRepo.startsWith("..")) return;
12581
12582
  const repoPath = path25.relative(this.opts.workingDir, gitRoot);
@@ -12769,9 +12770,493 @@ function _runGit(cwd, args2, opts = {}) {
12769
12770
  return _gitSeam.run(cwd, args2, opts);
12770
12771
  }
12771
12772
 
12772
- // src/services/streaming-emitter.service.ts
12773
+ // src/services/turn-files/turn-file-aggregator.ts
12773
12774
  var import_crypto2 = require("crypto");
12774
12775
 
12776
+ // src/services/turn-files/git-changeset.ts
12777
+ var import_child_process8 = require("child_process");
12778
+ var path26 = __toESM(require("path"));
12779
+ async function collectRepoChangeset(opts) {
12780
+ const status2 = await runGit2(opts.repoRoot, ["status", "--porcelain=v1", "-z"]);
12781
+ if (status2 === null) return null;
12782
+ const numstatRaw = await runGit2(opts.repoRoot, [
12783
+ "diff",
12784
+ "--numstat",
12785
+ "-z",
12786
+ "HEAD"
12787
+ ]).catch(() => null);
12788
+ const numstat = parseNumstat(numstatRaw ?? "");
12789
+ const entries = [];
12790
+ for (const row of parseStatus(status2)) {
12791
+ const stats = numstat.get(row.filePath) ?? { added: 0, removed: 0 };
12792
+ entries.push({
12793
+ filePath: row.filePath,
12794
+ fileStatus: row.fileStatus,
12795
+ linesAdded: stats.added,
12796
+ linesRemoved: stats.removed,
12797
+ // hunkCount isn't surfaced by --numstat. For the rail / drawer
12798
+ // it's only a count badge; defaulting to 1 when the file has
12799
+ // any non-zero stat is good enough until we wire a follow-up
12800
+ // per-file `git diff --shortstat` if we ever want exact hunks.
12801
+ hunkCount: stats.added + stats.removed > 0 ? 1 : 0,
12802
+ repoPath: opts.repoPath,
12803
+ repoName: opts.repoName
12804
+ });
12805
+ }
12806
+ return entries;
12807
+ }
12808
+ function parseStatus(raw) {
12809
+ const tokens = raw.split("\0");
12810
+ const rows = [];
12811
+ for (let i = 0; i < tokens.length; i++) {
12812
+ const token = tokens[i];
12813
+ if (!token || token.length < 3) continue;
12814
+ const code = token.slice(0, 2);
12815
+ const filePath = token.slice(3);
12816
+ if (!filePath) continue;
12817
+ const indexCode = code[0];
12818
+ const worktreeCode = code[1];
12819
+ if (indexCode === "R" || worktreeCode === "R") {
12820
+ rows.push({ filePath, fileStatus: "renamed" });
12821
+ i += 1;
12822
+ continue;
12823
+ }
12824
+ if (code === "??" || indexCode === "A" || worktreeCode === "A") {
12825
+ rows.push({ filePath, fileStatus: "added" });
12826
+ continue;
12827
+ }
12828
+ if (indexCode === "D" || worktreeCode === "D") {
12829
+ rows.push({ filePath, fileStatus: "deleted" });
12830
+ continue;
12831
+ }
12832
+ rows.push({ filePath, fileStatus: "modified" });
12833
+ }
12834
+ return rows;
12835
+ }
12836
+ function parseNumstat(raw) {
12837
+ const out2 = /* @__PURE__ */ new Map();
12838
+ for (const record of raw.split("\0")) {
12839
+ if (!record) continue;
12840
+ const parts = record.split(" ");
12841
+ if (parts.length < 3) continue;
12842
+ const added = parts[0] === "-" ? 0 : parseInt(parts[0], 10) || 0;
12843
+ const removed = parts[1] === "-" ? 0 : parseInt(parts[1], 10) || 0;
12844
+ const filePath = parts.slice(2).join(" ");
12845
+ if (!filePath) continue;
12846
+ out2.set(filePath, { added, removed });
12847
+ }
12848
+ return out2;
12849
+ }
12850
+ var _runGitImpl2 = {
12851
+ run: defaultRunGit
12852
+ };
12853
+ function runGit2(cwd, args2) {
12854
+ return _runGitImpl2.run(cwd, args2);
12855
+ }
12856
+ function defaultRunGit(cwd, args2) {
12857
+ return new Promise((resolve4) => {
12858
+ let proc;
12859
+ try {
12860
+ proc = (0, import_child_process8.spawn)("git", args2, { cwd, env: process.env });
12861
+ } catch {
12862
+ resolve4(null);
12863
+ return;
12864
+ }
12865
+ let stdout = "";
12866
+ let stderr = "";
12867
+ proc.stdout?.on("data", (c2) => {
12868
+ stdout += c2.toString();
12869
+ });
12870
+ proc.stderr?.on("data", (c2) => {
12871
+ stderr += c2.toString();
12872
+ });
12873
+ proc.on("error", () => resolve4(null));
12874
+ proc.on("close", (code) => {
12875
+ if (code === 0) {
12876
+ resolve4(stdout);
12877
+ } else {
12878
+ log.trace(
12879
+ "turnFiles",
12880
+ `git ${args2.join(" ")} exited ${code} stderr=${stderr.slice(0, 200)}`
12881
+ );
12882
+ resolve4(null);
12883
+ }
12884
+ });
12885
+ });
12886
+ }
12887
+ async function discoverRepos(workingDir, maxDepth = 4) {
12888
+ const fs30 = await import("fs/promises");
12889
+ const out2 = [];
12890
+ await walk(workingDir, 0);
12891
+ return out2;
12892
+ async function walk(dir, depth) {
12893
+ if (depth > maxDepth) return;
12894
+ let entries = [];
12895
+ try {
12896
+ const dirents = await fs30.readdir(dir, { withFileTypes: true });
12897
+ entries = dirents.filter((d3) => !d3.name.startsWith(".") || d3.name === ".git").map((d3) => ({ name: d3.name, isDirectory: d3.isDirectory() }));
12898
+ } catch {
12899
+ return;
12900
+ }
12901
+ const hasGit = entries.some(
12902
+ (e) => e.name === ".git" && (e.isDirectory || true)
12903
+ );
12904
+ if (hasGit) {
12905
+ out2.push({
12906
+ repoRoot: dir,
12907
+ repoPath: path26.relative(workingDir, dir),
12908
+ repoName: path26.basename(dir)
12909
+ });
12910
+ return;
12911
+ }
12912
+ for (const entry of entries) {
12913
+ if (!entry.isDirectory) continue;
12914
+ if (entry.name === "node_modules") continue;
12915
+ if (entry.name === "dist" || entry.name === "build") continue;
12916
+ await walk(path26.join(dir, entry.name), depth + 1);
12917
+ }
12918
+ }
12919
+ }
12920
+
12921
+ // src/services/turn-files/files-outbox.ts
12922
+ var fs22 = __toESM(require("fs/promises"));
12923
+ var path27 = __toESM(require("path"));
12924
+ var import_os6 = require("os");
12925
+ var HOME_OUTBOX_DIR = ".codeam/outbox";
12926
+ var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
12927
+ var BACKOFF_STEPS_MS = [
12928
+ 1e3,
12929
+ // 1 s
12930
+ 2e3,
12931
+ // 2 s
12932
+ 4e3,
12933
+ // 4 s
12934
+ 8e3,
12935
+ // 8 s
12936
+ 16e3,
12937
+ // 16 s
12938
+ 32e3,
12939
+ // 32 s
12940
+ 6e4,
12941
+ // 1 min
12942
+ 12e4,
12943
+ // 2 min
12944
+ 3e5
12945
+ // 5 min — cap
12946
+ ];
12947
+ var FilesOutbox = class {
12948
+ filePath;
12949
+ post;
12950
+ autoSchedule;
12951
+ flushTimer = null;
12952
+ flushing = false;
12953
+ backoffIndex = 0;
12954
+ stopped = false;
12955
+ constructor(opts) {
12956
+ const base = opts.baseDir ?? path27.join(homeDir(), HOME_OUTBOX_DIR);
12957
+ this.filePath = path27.join(base, `${opts.sessionId}.jsonl`);
12958
+ this.post = opts.post;
12959
+ this.autoSchedule = opts.autoSchedule !== false;
12960
+ }
12961
+ /** Persist the entry to disk and trigger a flush. Returns once the
12962
+ * line is durable on disk (not once the POST succeeds). */
12963
+ async enqueue(entry) {
12964
+ await fs22.mkdir(path27.dirname(this.filePath), { recursive: true });
12965
+ await fs22.appendFile(this.filePath, JSON.stringify(entry) + "\n", "utf8");
12966
+ this.backoffIndex = 0;
12967
+ if (this.autoSchedule) this.scheduleFlush(0);
12968
+ }
12969
+ /** Stop the scheduler. Idempotent. The on-disk file is left alone
12970
+ * so the next process pickup can flush whatever's pending. */
12971
+ stop() {
12972
+ this.stopped = true;
12973
+ if (this.flushTimer) {
12974
+ clearTimeout(this.flushTimer);
12975
+ this.flushTimer = null;
12976
+ }
12977
+ }
12978
+ /** Visible for tests. Forces a flush attempt right now. */
12979
+ async _flushNow() {
12980
+ return this.flush();
12981
+ }
12982
+ scheduleFlush(delayMs) {
12983
+ if (this.stopped) return;
12984
+ if (this.flushTimer) clearTimeout(this.flushTimer);
12985
+ const jittered = delayMs === 0 ? 0 : applyJitter(delayMs);
12986
+ this.flushTimer = setTimeout(() => {
12987
+ this.flushTimer = null;
12988
+ void this.flush();
12989
+ }, jittered);
12990
+ }
12991
+ async flush() {
12992
+ if (this.stopped) return;
12993
+ if (this.flushing) {
12994
+ return;
12995
+ }
12996
+ this.flushing = true;
12997
+ try {
12998
+ const entries = await this.readAll();
12999
+ if (entries.length === 0) return;
13000
+ const now = Date.now();
13001
+ const fresh = entries.filter((e) => now - e.enqueuedAt <= MAX_AGE_MS);
13002
+ if (fresh.length < entries.length) {
13003
+ log.warn(
13004
+ "turnFiles",
13005
+ `dropping ${entries.length - fresh.length} outbox entries older than ${MAX_AGE_MS / 1e3}s`
13006
+ );
13007
+ }
13008
+ const stillPending = [];
13009
+ let anyFailed = false;
13010
+ for (const entry of fresh) {
13011
+ if (this.stopped) {
13012
+ stillPending.push(entry);
13013
+ continue;
13014
+ }
13015
+ try {
13016
+ const res = await this.post(entry);
13017
+ if (res.ok) continue;
13018
+ if (res.statusCode === 404 || res.statusCode === 410) {
13019
+ log.warn(
13020
+ "turnFiles",
13021
+ `session dead (status=${res.statusCode}); dropping turnId=${entry.turnId.slice(0, 8)}`
13022
+ );
13023
+ continue;
13024
+ }
13025
+ anyFailed = true;
13026
+ stillPending.push(entry);
13027
+ } catch (err) {
13028
+ anyFailed = true;
13029
+ stillPending.push(entry);
13030
+ log.trace(
13031
+ "turnFiles",
13032
+ `outbox post threw for turnId=${entry.turnId.slice(0, 8)}: ${err.message}`
13033
+ );
13034
+ }
13035
+ }
13036
+ await this.rewrite(stillPending);
13037
+ if (anyFailed) {
13038
+ const delay = BACKOFF_STEPS_MS[Math.min(this.backoffIndex, BACKOFF_STEPS_MS.length - 1)];
13039
+ this.backoffIndex = Math.min(
13040
+ this.backoffIndex + 1,
13041
+ BACKOFF_STEPS_MS.length - 1
13042
+ );
13043
+ this.scheduleFlush(delay);
13044
+ } else {
13045
+ this.backoffIndex = 0;
13046
+ }
13047
+ } finally {
13048
+ this.flushing = false;
13049
+ }
13050
+ }
13051
+ async readAll() {
13052
+ let raw = "";
13053
+ try {
13054
+ raw = await fs22.readFile(this.filePath, "utf8");
13055
+ } catch {
13056
+ return [];
13057
+ }
13058
+ const out2 = [];
13059
+ for (const line of raw.split("\n")) {
13060
+ const trimmed = line.trim();
13061
+ if (!trimmed) continue;
13062
+ try {
13063
+ out2.push(JSON.parse(trimmed));
13064
+ } catch {
13065
+ }
13066
+ }
13067
+ return out2;
13068
+ }
13069
+ /**
13070
+ * Atomic compaction: write to `<file>.tmp`, fsync, rename over the
13071
+ * original. A crash between any of these steps leaves either the
13072
+ * original (no progress) or the new file (clean compaction) — never
13073
+ * a torn write.
13074
+ */
13075
+ async rewrite(entries) {
13076
+ const tmpPath = `${this.filePath}.${process.pid}.tmp`;
13077
+ if (entries.length === 0) {
13078
+ await fs22.unlink(this.filePath).catch(() => void 0);
13079
+ return;
13080
+ }
13081
+ const payload = entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
13082
+ await fs22.writeFile(tmpPath, payload, "utf8");
13083
+ await fs22.rename(tmpPath, this.filePath);
13084
+ }
13085
+ };
13086
+ function applyJitter(ms) {
13087
+ const factor = 0.8 + Math.random() * 0.4;
13088
+ return Math.round(ms * factor);
13089
+ }
13090
+ function homeDir() {
13091
+ return process.env.HOME ?? process.env.USERPROFILE ?? (0, import_os6.tmpdir)();
13092
+ }
13093
+
13094
+ // src/services/turn-files/turn-file-aggregator.ts
13095
+ var API_BASE6 = resolveApiBaseUrl();
13096
+ var ENDPOINT = "/api/files/batch";
13097
+ var MAX_BATCH_SIZE = 1e3;
13098
+ var TurnFileAggregator = class {
13099
+ constructor(opts) {
13100
+ this.opts = opts;
13101
+ this.apiBase = opts.apiBaseUrl ?? API_BASE6;
13102
+ this.outbox = new FilesOutbox({
13103
+ sessionId: opts.sessionId,
13104
+ baseDir: opts.outboxDir,
13105
+ post: (entry) => this.postEntry(entry),
13106
+ autoSchedule: opts.outboxAutoSchedule
13107
+ });
13108
+ this.discovering = discoverRepos(opts.workingDir).then((repos) => {
13109
+ this.repos = repos;
13110
+ opts.dirtyTracker?.markAllDirty(repos);
13111
+ log.info(
13112
+ "turnFiles",
13113
+ `discovered ${repos.length} repo(s) under ${opts.workingDir}: ${repos.map((r) => r.repoName || "<root>").join(", ")}`
13114
+ );
13115
+ });
13116
+ }
13117
+ opts;
13118
+ apiBase;
13119
+ outbox;
13120
+ repos = [];
13121
+ discovering = null;
13122
+ stopped = false;
13123
+ /** Stop the outbox scheduler. Idempotent. */
13124
+ stop() {
13125
+ this.stopped = true;
13126
+ this.outbox.stop();
13127
+ }
13128
+ /**
13129
+ * Run the discovery + git collection + POST pipeline for one
13130
+ * turn. Errors are swallowed (logged) so an agent never blocks on a
13131
+ * file-changeset failure; the outbox covers the network half.
13132
+ */
13133
+ async flushTurn() {
13134
+ if (this.stopped) return;
13135
+ try {
13136
+ if (this.discovering) {
13137
+ await this.discovering;
13138
+ this.discovering = null;
13139
+ }
13140
+ if (this.repos.length === 0) {
13141
+ log.trace("turnFiles", "no repos discovered \u2014 skipping flush");
13142
+ return;
13143
+ }
13144
+ const reposToScan = this.opts.dirtyTracker ? this.filterByDirty(this.opts.dirtyTracker) : this.repos;
13145
+ if (reposToScan.length === 0) {
13146
+ log.trace("turnFiles", "dirty set empty \u2014 skipping flush");
13147
+ return;
13148
+ }
13149
+ const files = [];
13150
+ for (const repo of reposToScan) {
13151
+ const entries = await collectRepoChangeset(repo);
13152
+ if (entries) files.push(...entries);
13153
+ }
13154
+ if (files.length === 0) {
13155
+ log.trace("turnFiles", "no changes detected this turn \u2014 skipping POST");
13156
+ return;
13157
+ }
13158
+ const chunks = chunkArray(files, MAX_BATCH_SIZE);
13159
+ for (const chunk of chunks) {
13160
+ const entry = {
13161
+ turnId: (0, import_crypto2.randomUUID)(),
13162
+ sessionId: this.opts.sessionId,
13163
+ pluginId: this.opts.pluginId,
13164
+ enqueuedAt: Date.now(),
13165
+ files: chunk
13166
+ };
13167
+ await this.outbox.enqueue(entry);
13168
+ }
13169
+ } catch (err) {
13170
+ log.warn(
13171
+ "turnFiles",
13172
+ `flushTurn failed: ${err.message ?? String(err)}`
13173
+ );
13174
+ }
13175
+ }
13176
+ /**
13177
+ * Consume the tracker's dirty set and intersect with the
13178
+ * discovered repos so we never spawn git for a path the watcher
13179
+ * marked outside our discovered set (e.g. a sibling repo that
13180
+ * appeared after construction). Unknown roots get dropped on the
13181
+ * floor — they'll re-mark themselves on the next event if they're
13182
+ * inside `workingDir`.
13183
+ */
13184
+ filterByDirty(tracker) {
13185
+ const dirty = tracker.consume();
13186
+ if (dirty.size === 0) return [];
13187
+ return this.repos.filter((repo) => dirty.has(repo.repoRoot));
13188
+ }
13189
+ async postEntry(entry) {
13190
+ const url = `${this.apiBase}${ENDPOINT}`;
13191
+ const headers = {
13192
+ "Content-Type": "application/json",
13193
+ "X-Codeam-Protocol-Version": PROTOCOL_VERSION,
13194
+ "X-Plugin-Auth-Token": this.opts.pluginAuthToken
13195
+ };
13196
+ const body = JSON.stringify({
13197
+ sessionId: entry.sessionId,
13198
+ pluginId: entry.pluginId,
13199
+ turnId: entry.turnId,
13200
+ files: entry.files
13201
+ });
13202
+ try {
13203
+ const res = await _transport3.post(url, headers, body);
13204
+ return { ok: res.statusCode >= 200 && res.statusCode < 300, statusCode: res.statusCode };
13205
+ } catch (err) {
13206
+ log.trace(
13207
+ "turnFiles",
13208
+ `batch POST threw turnId=${entry.turnId.slice(0, 8)}: ${err.message}`
13209
+ );
13210
+ return { ok: false, statusCode: 0 };
13211
+ }
13212
+ }
13213
+ };
13214
+ function chunkArray(arr, size) {
13215
+ if (arr.length <= size) return [arr];
13216
+ const out2 = [];
13217
+ for (let i = 0; i < arr.length; i += size) {
13218
+ out2.push(arr.slice(i, i + size));
13219
+ }
13220
+ return out2;
13221
+ }
13222
+
13223
+ // src/services/turn-files/repo-dirty-tracker.ts
13224
+ var RepoDirtyTracker = class {
13225
+ dirty = /* @__PURE__ */ new Set();
13226
+ /** Add a repo root to the dirty set. Idempotent. */
13227
+ markDirty(repoRoot) {
13228
+ this.dirty.add(repoRoot);
13229
+ }
13230
+ /** Seed every known repo as dirty — used right after `discoverRepos`
13231
+ * returns so the first end-of-turn flush captures worktree state
13232
+ * that predates the pairing. */
13233
+ markAllDirty(repoRoots) {
13234
+ for (const r of repoRoots) this.dirty.add(r.repoRoot);
13235
+ }
13236
+ /** Snapshot the current dirty set without clearing — useful for
13237
+ * diagnostic logs / tests. */
13238
+ peek() {
13239
+ return new Set(this.dirty);
13240
+ }
13241
+ /** Atomically read AND clear the dirty set. The aggregator calls
13242
+ * this on each `done:true`; subsequent filesystem events
13243
+ * re-populate it for the next turn. */
13244
+ consume() {
13245
+ const snapshot = new Set(this.dirty);
13246
+ this.dirty.clear();
13247
+ return snapshot;
13248
+ }
13249
+ /** True when the set is non-empty. Cheap pre-flight gate so the
13250
+ * aggregator can early-return on chat-only turns without
13251
+ * touching the dirty set. */
13252
+ hasDirty() {
13253
+ return this.dirty.size > 0;
13254
+ }
13255
+ };
13256
+
13257
+ // src/services/streaming-emitter.service.ts
13258
+ var import_crypto3 = require("crypto");
13259
+
12775
13260
  // src/services/streaming/transport.ts
12776
13261
  var http6 = __toESM(require("http"));
12777
13262
  var https6 = __toESM(require("https"));
@@ -12863,7 +13348,7 @@ function _get(url, headers) {
12863
13348
  }
12864
13349
 
12865
13350
  // src/services/streaming-emitter.service.ts
12866
- var API_BASE6 = resolveApiBaseUrl();
13351
+ var API_BASE7 = resolveApiBaseUrl();
12867
13352
  var TICK_MS = 50;
12868
13353
  var ANSWER_POLL_MS = 1500;
12869
13354
  var SELECTOR_STABLE_MS = 800;
@@ -12874,7 +13359,7 @@ var TAIL_KEEP_BYTES2 = 1.5 * 1024 * 1024;
12874
13359
  var StreamingEmitterService = class {
12875
13360
  constructor(opts) {
12876
13361
  this.opts = opts;
12877
- this.apiBase = opts.apiBaseUrl ?? API_BASE6;
13362
+ this.apiBase = opts.apiBaseUrl ?? API_BASE7;
12878
13363
  this.headers = {
12879
13364
  "Content-Type": "application/json",
12880
13365
  "X-Codeam-Protocol-Version": "2.0.0",
@@ -12993,7 +13478,7 @@ var StreamingEmitterService = class {
12993
13478
  });
12994
13479
  }
12995
13480
  this.activeChunk = {
12996
- chunkId: (0, import_crypto2.randomUUID)(),
13481
+ chunkId: (0, import_crypto3.randomUUID)(),
12997
13482
  kind: last.kind,
12998
13483
  emittedContent: "",
12999
13484
  currentContent: last.content,
@@ -13061,7 +13546,7 @@ var StreamingEmitterService = class {
13061
13546
  }
13062
13547
  if (this.pendingAnswer) return;
13063
13548
  if (now - this.selectorFirstSeenAt < SELECTOR_STABLE_MS) return;
13064
- const questionId = (0, import_crypto2.randomUUID)();
13549
+ const questionId = (0, import_crypto3.randomUUID)();
13065
13550
  this.pendingAnswer = {
13066
13551
  questionId,
13067
13552
  options: selector.options.length > 0 ? selector.options : void 0,
@@ -13224,13 +13709,13 @@ function fetchQuotaUsage(runtime, historySvc) {
13224
13709
  }
13225
13710
 
13226
13711
  // src/commands/start/keep-alive.ts
13227
- var import_child_process8 = require("child_process");
13712
+ var import_child_process9 = require("child_process");
13228
13713
  function buildKeepAlive(ctx) {
13229
13714
  let timer = null;
13230
13715
  async function setIdleTimeout(minutes) {
13231
13716
  if (!ctx.inCodespace || !ctx.codespaceName) return;
13232
13717
  await new Promise((resolve4) => {
13233
- const proc = (0, import_child_process8.spawn)(
13718
+ const proc = (0, import_child_process9.spawn)(
13234
13719
  "gh",
13235
13720
  [
13236
13721
  "api",
@@ -13267,11 +13752,11 @@ function buildKeepAlive(ctx) {
13267
13752
  }
13268
13753
 
13269
13754
  // src/commands/start/handlers.ts
13270
- var fs24 = __toESM(require("fs"));
13755
+ var fs25 = __toESM(require("fs"));
13271
13756
  var os23 = __toESM(require("os"));
13272
- var path29 = __toESM(require("path"));
13273
- var import_crypto4 = require("crypto");
13274
- var import_child_process11 = require("child_process");
13757
+ var path31 = __toESM(require("path"));
13758
+ var import_crypto5 = require("crypto");
13759
+ var import_child_process12 = require("child_process");
13275
13760
 
13276
13761
  // src/lib/payload.ts
13277
13762
  var import_zod2 = require("zod");
@@ -13327,8 +13812,8 @@ function parsePayload2(schema, raw) {
13327
13812
  }
13328
13813
 
13329
13814
  // src/services/file-ops.service.ts
13330
- var fs22 = __toESM(require("fs/promises"));
13331
- var path26 = __toESM(require("path"));
13815
+ var fs23 = __toESM(require("fs/promises"));
13816
+ var path28 = __toESM(require("path"));
13332
13817
  var MAX_FILE_BYTES = 5 * 1024 * 1024;
13333
13818
  var MAX_WALK_DEPTH = 6;
13334
13819
  var MAX_VISITED_DIRS = 5e3;
@@ -13363,12 +13848,12 @@ var SUBDIR_IGNORE = /* @__PURE__ */ new Set([
13363
13848
  "__pycache__"
13364
13849
  ]);
13365
13850
  function isUnder(parent, candidate) {
13366
- const rel = path26.relative(parent, candidate);
13367
- return rel === "" || !rel.startsWith("..") && !path26.isAbsolute(rel);
13851
+ const rel = path28.relative(parent, candidate);
13852
+ return rel === "" || !rel.startsWith("..") && !path28.isAbsolute(rel);
13368
13853
  }
13369
13854
  async function isExistingFile(absPath) {
13370
13855
  try {
13371
- const stat3 = await fs22.stat(absPath);
13856
+ const stat3 = await fs23.stat(absPath);
13372
13857
  return stat3.isFile();
13373
13858
  } catch {
13374
13859
  return false;
@@ -13381,13 +13866,13 @@ async function walkForSuffix(dir, needleVariants, depth, ctx) {
13381
13866
  ctx.visited++;
13382
13867
  let entries = [];
13383
13868
  try {
13384
- entries = await fs22.readdir(dir, { withFileTypes: true });
13869
+ entries = await fs23.readdir(dir, { withFileTypes: true });
13385
13870
  } catch {
13386
13871
  return;
13387
13872
  }
13388
13873
  for (const e of entries) {
13389
13874
  if (!e.isFile()) continue;
13390
- const full = path26.join(dir, e.name);
13875
+ const full = path28.join(dir, e.name);
13391
13876
  if (needleVariants.some((needle) => full.endsWith(needle))) {
13392
13877
  ctx.matches.push(full);
13393
13878
  if (ctx.matches.length >= ctx.cap) return;
@@ -13397,21 +13882,21 @@ async function walkForSuffix(dir, needleVariants, depth, ctx) {
13397
13882
  if (!e.isDirectory()) continue;
13398
13883
  if (SUBDIR_IGNORE.has(e.name)) continue;
13399
13884
  if (e.name.startsWith(".") && SUBDIR_IGNORE.has(e.name)) continue;
13400
- await walkForSuffix(path26.join(dir, e.name), needleVariants, depth + 1, ctx);
13885
+ await walkForSuffix(path28.join(dir, e.name), needleVariants, depth + 1, ctx);
13401
13886
  if (ctx.matches.length >= ctx.cap) return;
13402
13887
  }
13403
13888
  }
13404
13889
  async function findFile(rawPath) {
13405
13890
  const cwd = process.cwd();
13406
- if (path26.isAbsolute(rawPath)) {
13407
- const abs = path26.normalize(rawPath);
13891
+ if (path28.isAbsolute(rawPath)) {
13892
+ const abs = path28.normalize(rawPath);
13408
13893
  if (isUnder(cwd, abs) && await isExistingFile(abs)) return abs;
13409
13894
  }
13410
- const direct = path26.resolve(cwd, rawPath);
13895
+ const direct = path28.resolve(cwd, rawPath);
13411
13896
  if (isUnder(cwd, direct) && await isExistingFile(direct)) return direct;
13412
- const normalized = path26.normalize(rawPath).replace(/^[./\\]+/, "");
13897
+ const normalized = path28.normalize(rawPath).replace(/^[./\\]+/, "");
13413
13898
  const needles = [
13414
- `${path26.sep}${normalized}`,
13899
+ `${path28.sep}${normalized}`,
13415
13900
  `/${normalized}`
13416
13901
  ].filter((v, i, a) => a.indexOf(v) === i);
13417
13902
  const ctx = { visited: 0, matches: [], cap: 16 };
@@ -13425,7 +13910,7 @@ async function findWriteTarget(rawPath) {
13425
13910
  const found = await findFile(rawPath);
13426
13911
  if (found) return found;
13427
13912
  const cwd = process.cwd();
13428
- const fallback = path26.isAbsolute(rawPath) ? path26.normalize(rawPath) : path26.resolve(cwd, rawPath);
13913
+ const fallback = path28.isAbsolute(rawPath) ? path28.normalize(rawPath) : path28.resolve(cwd, rawPath);
13429
13914
  if (!isUnder(cwd, fallback)) return null;
13430
13915
  return fallback;
13431
13916
  }
@@ -13442,11 +13927,11 @@ async function readProjectFile(rawPath) {
13442
13927
  if (!abs) {
13443
13928
  return { error: `File not found in the project tree: ${rawPath}` };
13444
13929
  }
13445
- const stat3 = await fs22.stat(abs);
13930
+ const stat3 = await fs23.stat(abs);
13446
13931
  if (stat3.size > MAX_FILE_BYTES) {
13447
13932
  return { error: `File too large (${(stat3.size / 1024 / 1024).toFixed(1)} MB > ${MAX_FILE_BYTES / 1024 / 1024} MB).` };
13448
13933
  }
13449
- const buf = await fs22.readFile(abs);
13934
+ const buf = await fs23.readFile(abs);
13450
13935
  if (looksBinary(buf)) {
13451
13936
  return { error: "Binary file \u2014 refusing to open in a code editor." };
13452
13937
  }
@@ -13465,8 +13950,8 @@ async function writeProjectFile(rawPath, content) {
13465
13950
  if (Buffer.byteLength(content, "utf-8") > MAX_FILE_BYTES) {
13466
13951
  return { error: "Content too large." };
13467
13952
  }
13468
- await fs22.mkdir(path26.dirname(abs), { recursive: true });
13469
- await fs22.writeFile(abs, content, "utf-8");
13953
+ await fs23.mkdir(path28.dirname(abs), { recursive: true });
13954
+ await fs23.writeFile(abs, content, "utf-8");
13470
13955
  return { ok: true };
13471
13956
  } catch (e) {
13472
13957
  const msg = e instanceof Error ? e.message : "Write failed";
@@ -13475,11 +13960,11 @@ async function writeProjectFile(rawPath, content) {
13475
13960
  }
13476
13961
 
13477
13962
  // src/services/project-ops.service.ts
13478
- var import_child_process9 = require("child_process");
13963
+ var import_child_process10 = require("child_process");
13479
13964
  var import_util2 = require("util");
13480
- var fs23 = __toESM(require("fs/promises"));
13481
- var path27 = __toESM(require("path"));
13482
- var execFileP3 = (0, import_util2.promisify)(import_child_process9.execFile);
13965
+ var fs24 = __toESM(require("fs/promises"));
13966
+ var path29 = __toESM(require("path"));
13967
+ var execFileP3 = (0, import_util2.promisify)(import_child_process10.execFile);
13483
13968
  var PROJECT_IGNORE = /* @__PURE__ */ new Set([
13484
13969
  "node_modules",
13485
13970
  ".git",
@@ -13526,7 +14011,7 @@ async function listProjectFiles(opts = {}) {
13526
14011
  }
13527
14012
  let entries = [];
13528
14013
  try {
13529
- entries = await fs23.readdir(dir, { withFileTypes: true });
14014
+ entries = await fs24.readdir(dir, { withFileTypes: true });
13530
14015
  } catch {
13531
14016
  return;
13532
14017
  }
@@ -13536,18 +14021,18 @@ async function listProjectFiles(opts = {}) {
13536
14021
  return;
13537
14022
  }
13538
14023
  if (PROJECT_IGNORE.has(e.name)) continue;
13539
- const full = path27.join(dir, e.name);
14024
+ const full = path29.join(dir, e.name);
13540
14025
  if (e.isDirectory()) {
13541
14026
  if (depth >= 12) continue;
13542
14027
  await walk(full, depth + 1);
13543
14028
  } else if (e.isFile()) {
13544
- const rel = path27.relative(root, full);
14029
+ const rel = path29.relative(root, full);
13545
14030
  if (q2 && !rel.toLowerCase().includes(q2) && !e.name.toLowerCase().includes(q2)) {
13546
14031
  continue;
13547
14032
  }
13548
14033
  let size = 0;
13549
14034
  try {
13550
- const st3 = await fs23.stat(full);
14035
+ const st3 = await fs24.stat(full);
13551
14036
  size = st3.size;
13552
14037
  } catch {
13553
14038
  }
@@ -13649,8 +14134,8 @@ async function gitStatus(cwd) {
13649
14134
  let hasMergeInProgress = false;
13650
14135
  try {
13651
14136
  const gitDir = (await git(["rev-parse", "--git-dir"], root)).stdout.trim();
13652
- const mergeHead = path27.isAbsolute(gitDir) ? path27.join(gitDir, "MERGE_HEAD") : path27.join(root, gitDir, "MERGE_HEAD");
13653
- await fs23.access(mergeHead);
14137
+ const mergeHead = path29.isAbsolute(gitDir) ? path29.join(gitDir, "MERGE_HEAD") : path29.join(root, gitDir, "MERGE_HEAD");
14138
+ await fs24.access(mergeHead);
13654
14139
  hasMergeInProgress = true;
13655
14140
  } catch {
13656
14141
  }
@@ -13796,7 +14281,7 @@ async function jsSearchFiles(opts, cwd, cap) {
13796
14281
  }
13797
14282
  let content = "";
13798
14283
  try {
13799
- content = await fs23.readFile(path27.join(cwd, f.path), "utf8");
14284
+ content = await fs24.readFile(path29.join(cwd, f.path), "utf8");
13800
14285
  } catch {
13801
14286
  continue;
13802
14287
  }
@@ -13819,8 +14304,8 @@ async function jsSearchFiles(opts, cwd, cap) {
13819
14304
  }
13820
14305
 
13821
14306
  // src/services/terminal-ops.service.ts
13822
- var import_child_process10 = require("child_process");
13823
- var import_crypto3 = require("crypto");
14307
+ var import_child_process11 = require("child_process");
14308
+ var import_crypto4 = require("crypto");
13824
14309
  var import_path3 = __toESM(require("path"));
13825
14310
  var MAX_CONCURRENT_SESSIONS = 4;
13826
14311
  var nodePtyModule;
@@ -13941,7 +14426,7 @@ function createPythonSession(id, shell, cwd, env, cols, rows) {
13941
14426
  }
13942
14427
  let child;
13943
14428
  try {
13944
- child = (0, import_child_process10.spawn)(python, ["-c", PYTHON_TERMINAL_HELPER, shell], {
14429
+ child = (0, import_child_process11.spawn)(python, ["-c", PYTHON_TERMINAL_HELPER, shell], {
13945
14430
  cwd,
13946
14431
  env: { ...env, COLUMNS: String(cols), LINES: String(rows) },
13947
14432
  stdio: ["pipe", "pipe", "pipe"]
@@ -13996,7 +14481,7 @@ function openTerminal(opts) {
13996
14481
  };
13997
14482
  const cols = Math.max(1, Math.min(opts.cols ?? 80, 500));
13998
14483
  const rows = Math.max(1, Math.min(opts.rows ?? 24, 200));
13999
- const id = (0, import_crypto3.randomUUID)();
14484
+ const id = (0, import_crypto4.randomUUID)();
14000
14485
  const ptyMod = loadNodePty2();
14001
14486
  if (ptyMod) {
14002
14487
  try {
@@ -14080,7 +14565,7 @@ var pendingAttachmentFiles = /* @__PURE__ */ new Set();
14080
14565
  function cleanupAttachmentTempFiles() {
14081
14566
  for (const p2 of pendingAttachmentFiles) {
14082
14567
  try {
14083
- fs24.unlinkSync(p2);
14568
+ fs25.unlinkSync(p2);
14084
14569
  } catch {
14085
14570
  }
14086
14571
  }
@@ -14089,8 +14574,8 @@ function cleanupAttachmentTempFiles() {
14089
14574
  function saveFilesTemp(files) {
14090
14575
  return files.filter(({ base64 }) => base64 && base64.length > 0).map(({ filename, base64 }) => {
14091
14576
  const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80);
14092
- const tmpPath = path29.join(os23.tmpdir(), `codeam-${(0, import_crypto4.randomUUID)()}-${safeName}`);
14093
- fs24.writeFileSync(tmpPath, Buffer.from(base64, "base64"));
14577
+ const tmpPath = path31.join(os23.tmpdir(), `codeam-${(0, import_crypto5.randomUUID)()}-${safeName}`);
14578
+ fs25.writeFileSync(tmpPath, Buffer.from(base64, "base64"));
14094
14579
  pendingAttachmentFiles.add(tmpPath);
14095
14580
  return tmpPath;
14096
14581
  });
@@ -14110,7 +14595,7 @@ var startTask = (ctx, _cmd, parsed) => {
14110
14595
  setTimeout(() => {
14111
14596
  for (const p2 of paths) {
14112
14597
  try {
14113
- fs24.unlinkSync(p2);
14598
+ fs25.unlinkSync(p2);
14114
14599
  } catch {
14115
14600
  }
14116
14601
  pendingAttachmentFiles.delete(p2);
@@ -14223,7 +14708,7 @@ var sessionTerminated = (ctx) => {
14223
14708
  } catch {
14224
14709
  }
14225
14710
  try {
14226
- const proc = (0, import_child_process11.spawn)("bash", ["-lc", "pm2 delete codeam-pair >/dev/null 2>&1 || true"], {
14711
+ const proc = (0, import_child_process12.spawn)("bash", ["-lc", "pm2 delete codeam-pair >/dev/null 2>&1 || true"], {
14227
14712
  detached: true,
14228
14713
  stdio: "ignore"
14229
14714
  });
@@ -14245,7 +14730,7 @@ var shutdownSession = async (ctx, cmd) => {
14245
14730
  }
14246
14731
  if (ctx.keepAliveCtx.inCodespace && ctx.keepAliveCtx.codespaceName) {
14247
14732
  try {
14248
- const stopProc = (0, import_child_process11.spawn)(
14733
+ const stopProc = (0, import_child_process12.spawn)(
14249
14734
  "bash",
14250
14735
  ["-lc", `sleep 1; gh codespace stop -c ${JSON.stringify(ctx.keepAliveCtx.codespaceName)} >/dev/null 2>&1 || true`],
14251
14736
  { detached: true, stdio: "ignore" }
@@ -14255,7 +14740,7 @@ var shutdownSession = async (ctx, cmd) => {
14255
14740
  }
14256
14741
  }
14257
14742
  try {
14258
- const proc = (0, import_child_process11.spawn)("bash", ["-lc", "pm2 delete codeam-pair >/dev/null 2>&1 || true"], {
14743
+ const proc = (0, import_child_process12.spawn)("bash", ["-lc", "pm2 delete codeam-pair >/dev/null 2>&1 || true"], {
14259
14744
  detached: true,
14260
14745
  stdio: "ignore"
14261
14746
  });
@@ -14266,7 +14751,7 @@ var shutdownSession = async (ctx, cmd) => {
14266
14751
  ctx.relay.stop();
14267
14752
  process.exit(0);
14268
14753
  };
14269
- var readFile3 = async (ctx, cmd, parsed) => {
14754
+ var readFile4 = async (ctx, cmd, parsed) => {
14270
14755
  if (!parsed.path) {
14271
14756
  await ctx.relay.sendResult(cmd.id, "failed", { error: "Missing path" });
14272
14757
  return;
@@ -14274,7 +14759,7 @@ var readFile3 = async (ctx, cmd, parsed) => {
14274
14759
  const result = await readProjectFile(parsed.path);
14275
14760
  await ctx.relay.sendResult(cmd.id, "completed", result);
14276
14761
  };
14277
- var writeFile2 = async (ctx, cmd, parsed) => {
14762
+ var writeFile3 = async (ctx, cmd, parsed) => {
14278
14763
  if (!parsed.path || typeof parsed.content !== "string") {
14279
14764
  await ctx.relay.sendResult(cmd.id, "failed", { error: "Missing path or content" });
14280
14765
  return;
@@ -14393,8 +14878,8 @@ var handlers = {
14393
14878
  set_keep_alive: setKeepAlive,
14394
14879
  session_terminated: sessionTerminated,
14395
14880
  shutdown_session: shutdownSession,
14396
- read_file: readFile3,
14397
- write_file: writeFile2,
14881
+ read_file: readFile4,
14882
+ write_file: writeFile3,
14398
14883
  list_files: listFiles,
14399
14884
  search_files: searchFilesH,
14400
14885
  terminal_open: terminalOpenH,
@@ -14480,6 +14965,8 @@ async function start(requestedAgent) {
14480
14965
  historySvc.uploadDelta().catch(() => {
14481
14966
  });
14482
14967
  }, 400);
14968
+ turnFiles?.flushTurn().catch(() => {
14969
+ });
14483
14970
  },
14484
14971
  () => {
14485
14972
  const prevCount = historySvc.getCurrentMessageCount();
@@ -14488,11 +14975,20 @@ async function start(requestedAgent) {
14488
14975
  session.pluginAuthToken,
14489
14976
  runtime
14490
14977
  );
14978
+ const dirtyTracker = session.pluginAuthToken ? new RepoDirtyTracker() : null;
14491
14979
  const fileWatcher = session.pluginAuthToken ? new FileWatcherService({
14492
14980
  workingDir: cwd,
14493
14981
  sessionId: session.id,
14494
14982
  pluginId,
14495
- pluginAuthToken: session.pluginAuthToken
14983
+ pluginAuthToken: session.pluginAuthToken,
14984
+ onRepoDirty: dirtyTracker ? (repoRoot) => dirtyTracker.markDirty(repoRoot) : void 0
14985
+ }) : null;
14986
+ const turnFiles = session.pluginAuthToken ? new TurnFileAggregator({
14987
+ workingDir: cwd,
14988
+ sessionId: session.id,
14989
+ pluginId,
14990
+ pluginAuthToken: session.pluginAuthToken,
14991
+ dirtyTracker: dirtyTracker ?? void 0
14496
14992
  }) : null;
14497
14993
  let streamingEmitter = null;
14498
14994
  const agent = new AgentService(
@@ -14510,6 +15006,7 @@ async function start(requestedAgent) {
14510
15006
  outputSvc.dispose();
14511
15007
  relay.stop();
14512
15008
  void fileWatcher?.stop();
15009
+ turnFiles?.stop();
14513
15010
  void streamingEmitter?.stop();
14514
15011
  closeAllTerminals();
14515
15012
  cleanupAttachmentTempFiles();
@@ -14577,7 +15074,7 @@ async function start(requestedAgent) {
14577
15074
  }
14578
15075
 
14579
15076
  // src/commands/pair.ts
14580
- var import_crypto5 = require("crypto");
15077
+ var import_crypto6 = require("crypto");
14581
15078
  var import_picocolors3 = __toESM(require("picocolors"));
14582
15079
 
14583
15080
  // src/ui/prompts.ts
@@ -14641,7 +15138,7 @@ async function pair(args2 = []) {
14641
15138
  const flagAgent = parseAgentFlag(args2);
14642
15139
  const agentId = dryRun ? flagAgent ?? config.preferredAgent ?? "claude" : flagAgent ?? await promptForAgent(config.preferredAgent ?? "claude");
14643
15140
  showIntro();
14644
- const pluginId = (0, import_crypto5.randomUUID)();
15141
+ const pluginId = (0, import_crypto6.randomUUID)();
14645
15142
  capture("pair_started", { agentId, pluginId, dryRun });
14646
15143
  const spin = dist_exports.spinner();
14647
15144
  spin.start("Requesting pairing code...");
@@ -14732,10 +15229,10 @@ async function pair(args2 = []) {
14732
15229
  }
14733
15230
 
14734
15231
  // src/commands/pair-auto.ts
14735
- var fs25 = __toESM(require("fs"));
15232
+ var fs26 = __toESM(require("fs"));
14736
15233
  var os24 = __toESM(require("os"));
14737
- var import_crypto6 = require("crypto");
14738
- var API_BASE7 = resolveApiBaseUrl();
15234
+ var import_crypto7 = require("crypto");
15235
+ var API_BASE8 = resolveApiBaseUrl();
14739
15236
  function fail(msg) {
14740
15237
  console.error(`
14741
15238
  ${msg}
@@ -14750,12 +15247,12 @@ function readTokenFromArgs(args2) {
14750
15247
  }
14751
15248
  const fileFlag = args2.find((a) => a.startsWith("--token-file="));
14752
15249
  if (fileFlag) {
14753
- const path37 = fileFlag.slice("--token-file=".length);
15250
+ const path39 = fileFlag.slice("--token-file=".length);
14754
15251
  try {
14755
- const content = fs25.readFileSync(path37, "utf8").trim();
14756
- if (content.length === 0) fail(`--token-file ${path37} is empty`);
15252
+ const content = fs26.readFileSync(path39, "utf8").trim();
15253
+ if (content.length === 0) fail(`--token-file ${path39} is empty`);
14757
15254
  try {
14758
- fs25.unlinkSync(path37);
15255
+ fs26.unlinkSync(path39);
14759
15256
  } catch {
14760
15257
  }
14761
15258
  return content;
@@ -14775,7 +15272,7 @@ function networkError(msg, cause) {
14775
15272
  return err;
14776
15273
  }
14777
15274
  async function claimOnce(token, pluginId) {
14778
- const url = `${API_BASE7}/api/pairing/claim-auto-token`;
15275
+ const url = `${API_BASE8}/api/pairing/claim-auto-token`;
14779
15276
  const body = {
14780
15277
  token,
14781
15278
  pluginId,
@@ -14839,7 +15336,7 @@ async function claim(token, pluginId) {
14839
15336
  }
14840
15337
  async function pairAuto(args2) {
14841
15338
  const token = readTokenFromArgs(args2);
14842
- const pluginId = (0, import_crypto6.randomUUID)();
15339
+ const pluginId = (0, import_crypto7.randomUUID)();
14843
15340
  capture("pair_auto_started", { pluginId });
14844
15341
  console.log(" Claiming pairing token\u2026");
14845
15342
  const claimed = await claim(token, pluginId);
@@ -14965,7 +15462,7 @@ function status() {
14965
15462
 
14966
15463
  // src/commands/logout.ts
14967
15464
  var import_picocolors6 = __toESM(require("picocolors"));
14968
- var API_BASE8 = resolveApiBaseUrl();
15465
+ var API_BASE9 = resolveApiBaseUrl();
14969
15466
  async function notifyBackendOffline() {
14970
15467
  const cfg = loadCliConfig();
14971
15468
  const pluginIds = /* @__PURE__ */ new Set([
@@ -14977,7 +15474,7 @@ async function notifyBackendOffline() {
14977
15474
  try {
14978
15475
  await Promise.all(
14979
15476
  Array.from(pluginIds).map(
14980
- (pluginId) => _postJson(`${API_BASE8}/api/plugin/heartbeat`, {
15477
+ (pluginId) => _postJson(`${API_BASE9}/api/plugin/heartbeat`, {
14981
15478
  pluginId,
14982
15479
  online: false
14983
15480
  }).catch((err) => {
@@ -15005,11 +15502,11 @@ async function logout() {
15005
15502
  var import_picocolors9 = __toESM(require("picocolors"));
15006
15503
 
15007
15504
  // src/services/providers/github-codespaces.ts
15008
- var import_child_process12 = require("child_process");
15505
+ var import_child_process13 = require("child_process");
15009
15506
  var import_util3 = require("util");
15010
15507
  var import_picocolors7 = __toESM(require("picocolors"));
15011
- var path30 = __toESM(require("path"));
15012
- var execFileP4 = (0, import_util3.promisify)(import_child_process12.execFile);
15508
+ var path32 = __toESM(require("path"));
15509
+ var execFileP4 = (0, import_util3.promisify)(import_child_process13.execFile);
15013
15510
  var MAX_BUFFER = 8 * 1024 * 1024;
15014
15511
  function resetStdinForChild() {
15015
15512
  if (process.stdin.isTTY) {
@@ -15053,7 +15550,7 @@ var GitHubCodespacesProvider = class {
15053
15550
  if (!isAuthed) {
15054
15551
  resetStdinForChild();
15055
15552
  await new Promise((resolve4, reject) => {
15056
- const proc = (0, import_child_process12.spawn)("gh", ["auth", "login", "-s", "codespace,repo,read:user"], {
15553
+ const proc = (0, import_child_process13.spawn)("gh", ["auth", "login", "-s", "codespace,repo,read:user"], {
15057
15554
  stdio: "inherit"
15058
15555
  });
15059
15556
  proc.on("exit", (code) => {
@@ -15087,7 +15584,7 @@ var GitHubCodespacesProvider = class {
15087
15584
  wt(noteLines.join("\n"), "One more permission needed");
15088
15585
  resetStdinForChild();
15089
15586
  const refreshCode = await new Promise((resolve4, reject) => {
15090
- const proc = (0, import_child_process12.spawn)(
15587
+ const proc = (0, import_child_process13.spawn)(
15091
15588
  "gh",
15092
15589
  ["auth", "refresh", "-h", "github.com", "-s", "codespace"],
15093
15590
  { stdio: "inherit" }
@@ -15237,7 +15734,7 @@ var GitHubCodespacesProvider = class {
15237
15734
  O2.step(`Installing gh via ${installCmd.describe}\u2026`);
15238
15735
  resetStdinForChild();
15239
15736
  const ok = await new Promise((resolve4) => {
15240
- const proc = (0, import_child_process12.spawn)(installCmd.exe, installCmd.args, { stdio: "inherit" });
15737
+ const proc = (0, import_child_process13.spawn)(installCmd.exe, installCmd.args, { stdio: "inherit" });
15241
15738
  proc.on("exit", (code) => resolve4(code === 0));
15242
15739
  proc.on("error", () => resolve4(false));
15243
15740
  });
@@ -15264,7 +15761,7 @@ var GitHubCodespacesProvider = class {
15264
15761
  );
15265
15762
  resetStdinForChild();
15266
15763
  await new Promise((resolve4, reject) => {
15267
- const proc = (0, import_child_process12.spawn)(
15764
+ const proc = (0, import_child_process13.spawn)(
15268
15765
  "gh",
15269
15766
  ["auth", "refresh", "-h", "github.com", "-s", "repo,read:org"],
15270
15767
  { stdio: "inherit" }
@@ -15442,7 +15939,7 @@ var GitHubCodespacesProvider = class {
15442
15939
  async streamCommand(workspaceId, command2) {
15443
15940
  resetStdinForChild();
15444
15941
  return new Promise((resolve4, reject) => {
15445
- const proc = (0, import_child_process12.spawn)(
15942
+ const proc = (0, import_child_process13.spawn)(
15446
15943
  "gh",
15447
15944
  ["codespace", "ssh", "-c", workspaceId, "--", "-tt", command2],
15448
15945
  { stdio: "inherit" }
@@ -15469,11 +15966,11 @@ var GitHubCodespacesProvider = class {
15469
15966
  `mkdir -p ${shellQuote(remoteDir)} && tar -xzf - -C ${shellQuote(remoteDir)}`
15470
15967
  ];
15471
15968
  await new Promise((resolve4, reject) => {
15472
- const tar = (0, import_child_process12.spawn)("tar", tarArgs, {
15969
+ const tar = (0, import_child_process13.spawn)("tar", tarArgs, {
15473
15970
  stdio: ["ignore", "pipe", "pipe"],
15474
15971
  env: tarEnv
15475
15972
  });
15476
- const ssh = (0, import_child_process12.spawn)("gh", sshArgs, {
15973
+ const ssh = (0, import_child_process13.spawn)("gh", sshArgs, {
15477
15974
  stdio: [tar.stdout, "pipe", "pipe"]
15478
15975
  });
15479
15976
  let tarErr = "";
@@ -15497,7 +15994,7 @@ var GitHubCodespacesProvider = class {
15497
15994
  });
15498
15995
  }
15499
15996
  async uploadFile(workspaceId, remotePath, contents, options = {}) {
15500
- const remoteDir = path30.posix.dirname(remotePath);
15997
+ const remoteDir = path32.posix.dirname(remotePath);
15501
15998
  const parts = [
15502
15999
  `mkdir -p ${shellQuote(remoteDir)}`,
15503
16000
  `cat > ${shellQuote(remotePath)}`
@@ -15507,7 +16004,7 @@ var GitHubCodespacesProvider = class {
15507
16004
  }
15508
16005
  const cmd = parts.join(" && ");
15509
16006
  await new Promise((resolve4, reject) => {
15510
- const proc = (0, import_child_process12.spawn)(
16007
+ const proc = (0, import_child_process13.spawn)(
15511
16008
  "gh",
15512
16009
  ["codespace", "ssh", "-c", workspaceId, "--", cmd],
15513
16010
  { stdio: ["pipe", "pipe", "pipe"] }
@@ -15565,11 +16062,11 @@ function shellQuote(s) {
15565
16062
  }
15566
16063
 
15567
16064
  // src/services/providers/gitpod.ts
15568
- var import_child_process13 = require("child_process");
16065
+ var import_child_process14 = require("child_process");
15569
16066
  var import_util4 = require("util");
15570
- var path31 = __toESM(require("path"));
16067
+ var path33 = __toESM(require("path"));
15571
16068
  var import_picocolors8 = __toESM(require("picocolors"));
15572
- var execFileP5 = (0, import_util4.promisify)(import_child_process13.execFile);
16069
+ var execFileP5 = (0, import_util4.promisify)(import_child_process14.execFile);
15573
16070
  var MAX_BUFFER2 = 8 * 1024 * 1024;
15574
16071
  function resetStdinForChild2() {
15575
16072
  if (process.stdin.isTTY) {
@@ -15609,7 +16106,7 @@ var GitpodProvider = class {
15609
16106
  );
15610
16107
  resetStdinForChild2();
15611
16108
  await new Promise((resolve4, reject) => {
15612
- const proc = (0, import_child_process13.spawn)("gitpod", ["login"], { stdio: "inherit" });
16109
+ const proc = (0, import_child_process14.spawn)("gitpod", ["login"], { stdio: "inherit" });
15613
16110
  proc.on("exit", (code) => {
15614
16111
  if (code === 0) resolve4();
15615
16112
  else reject(new Error("gitpod login failed."));
@@ -15761,7 +16258,7 @@ var GitpodProvider = class {
15761
16258
  async streamCommand(workspaceId, command2) {
15762
16259
  resetStdinForChild2();
15763
16260
  return new Promise((resolve4, reject) => {
15764
- const proc = (0, import_child_process13.spawn)(
16261
+ const proc = (0, import_child_process14.spawn)(
15765
16262
  "gitpod",
15766
16263
  ["workspace", "ssh", workspaceId, "--", "-tt", command2],
15767
16264
  { stdio: "inherit" }
@@ -15781,11 +16278,11 @@ var GitpodProvider = class {
15781
16278
  const tarEnv = { ...process.env, COPYFILE_DISABLE: "1" };
15782
16279
  const remoteCmd = `mkdir -p ${shellQuote2(remoteDir)} && tar -xzf - -C ${shellQuote2(remoteDir)}`;
15783
16280
  await new Promise((resolve4, reject) => {
15784
- const tar = (0, import_child_process13.spawn)("tar", tarArgs, {
16281
+ const tar = (0, import_child_process14.spawn)("tar", tarArgs, {
15785
16282
  stdio: ["ignore", "pipe", "pipe"],
15786
16283
  env: tarEnv
15787
16284
  });
15788
- const ssh = (0, import_child_process13.spawn)(
16285
+ const ssh = (0, import_child_process14.spawn)(
15789
16286
  "gitpod",
15790
16287
  ["workspace", "ssh", workspaceId, "--", remoteCmd],
15791
16288
  { stdio: [tar.stdout, "pipe", "pipe"] }
@@ -15807,7 +16304,7 @@ var GitpodProvider = class {
15807
16304
  });
15808
16305
  }
15809
16306
  async uploadFile(workspaceId, remotePath, contents, options = {}) {
15810
- const remoteDir = path31.posix.dirname(remotePath);
16307
+ const remoteDir = path33.posix.dirname(remotePath);
15811
16308
  const parts = [
15812
16309
  `mkdir -p ${shellQuote2(remoteDir)}`,
15813
16310
  `cat > ${shellQuote2(remotePath)}`
@@ -15817,7 +16314,7 @@ var GitpodProvider = class {
15817
16314
  }
15818
16315
  const cmd = parts.join(" && ");
15819
16316
  await new Promise((resolve4, reject) => {
15820
- const proc = (0, import_child_process13.spawn)(
16317
+ const proc = (0, import_child_process14.spawn)(
15821
16318
  "gitpod",
15822
16319
  ["workspace", "ssh", workspaceId, "--", cmd],
15823
16320
  { stdio: ["pipe", "pipe", "pipe"] }
@@ -15841,10 +16338,10 @@ function shellQuote2(s) {
15841
16338
  }
15842
16339
 
15843
16340
  // src/services/providers/gitlab-workspaces.ts
15844
- var import_child_process14 = require("child_process");
16341
+ var import_child_process15 = require("child_process");
15845
16342
  var import_util5 = require("util");
15846
- var path32 = __toESM(require("path"));
15847
- var execFileP6 = (0, import_util5.promisify)(import_child_process14.execFile);
16343
+ var path34 = __toESM(require("path"));
16344
+ var execFileP6 = (0, import_util5.promisify)(import_child_process15.execFile);
15848
16345
  var MAX_BUFFER3 = 8 * 1024 * 1024;
15849
16346
  var GITLAB_API_BASE = process.env.CODEAM_GITLAB_API_URL ?? "https://gitlab.com/api/v4";
15850
16347
  function resetStdinForChild3() {
@@ -15886,7 +16383,7 @@ var GitLabWorkspacesProvider = class {
15886
16383
  );
15887
16384
  resetStdinForChild3();
15888
16385
  await new Promise((resolve4, reject) => {
15889
- const proc = (0, import_child_process14.spawn)(
16386
+ const proc = (0, import_child_process15.spawn)(
15890
16387
  "glab",
15891
16388
  ["auth", "login", "--scopes", "api,read_user,read_repository"],
15892
16389
  { stdio: "inherit" }
@@ -16058,7 +16555,7 @@ Docs: https://docs.gitlab.com/ee/user/workspace/configuration.html`
16058
16555
  const sshHost = process.env.CODEAM_GITLAB_SSH_HOST ?? "workspaces.gitlab.com";
16059
16556
  resetStdinForChild3();
16060
16557
  return new Promise((resolve4, reject) => {
16061
- const proc = (0, import_child_process14.spawn)(
16558
+ const proc = (0, import_child_process15.spawn)(
16062
16559
  "ssh",
16063
16560
  ["-tt", "-o", "StrictHostKeyChecking=accept-new", `${workspaceId}@${sshHost}`, command2],
16064
16561
  { stdio: "inherit" }
@@ -16079,8 +16576,8 @@ Docs: https://docs.gitlab.com/ee/user/workspace/configuration.html`
16079
16576
  const tarEnv = { ...process.env, COPYFILE_DISABLE: "1" };
16080
16577
  const remoteCmd = `mkdir -p ${shellQuote3(remoteDir)} && tar -xzf - -C ${shellQuote3(remoteDir)}`;
16081
16578
  await new Promise((resolve4, reject) => {
16082
- const tar = (0, import_child_process14.spawn)("tar", tarArgs, { stdio: ["ignore", "pipe", "pipe"], env: tarEnv });
16083
- const ssh = (0, import_child_process14.spawn)(
16579
+ const tar = (0, import_child_process15.spawn)("tar", tarArgs, { stdio: ["ignore", "pipe", "pipe"], env: tarEnv });
16580
+ const ssh = (0, import_child_process15.spawn)(
16084
16581
  "ssh",
16085
16582
  ["-o", "StrictHostKeyChecking=accept-new", `${workspaceId}@${sshHost}`, remoteCmd],
16086
16583
  { stdio: [tar.stdout, "pipe", "pipe"] }
@@ -16103,14 +16600,14 @@ Docs: https://docs.gitlab.com/ee/user/workspace/configuration.html`
16103
16600
  }
16104
16601
  async uploadFile(workspaceId, remotePath, contents, options = {}) {
16105
16602
  const sshHost = process.env.CODEAM_GITLAB_SSH_HOST ?? "workspaces.gitlab.com";
16106
- const remoteDir = path32.posix.dirname(remotePath);
16603
+ const remoteDir = path34.posix.dirname(remotePath);
16107
16604
  const parts = [`mkdir -p ${shellQuote3(remoteDir)}`, `cat > ${shellQuote3(remotePath)}`];
16108
16605
  if (options.mode != null) {
16109
16606
  parts.push(`chmod ${options.mode.toString(8)} ${shellQuote3(remotePath)}`);
16110
16607
  }
16111
16608
  const cmd = parts.join(" && ");
16112
16609
  await new Promise((resolve4, reject) => {
16113
- const proc = (0, import_child_process14.spawn)(
16610
+ const proc = (0, import_child_process15.spawn)(
16114
16611
  "ssh",
16115
16612
  ["-o", "StrictHostKeyChecking=accept-new", `${workspaceId}@${sshHost}`, cmd],
16116
16613
  { stdio: ["pipe", "pipe", "pipe"] }
@@ -16169,10 +16666,10 @@ function shellQuote3(s) {
16169
16666
  }
16170
16667
 
16171
16668
  // src/services/providers/railway.ts
16172
- var import_child_process15 = require("child_process");
16669
+ var import_child_process16 = require("child_process");
16173
16670
  var import_util6 = require("util");
16174
- var path33 = __toESM(require("path"));
16175
- var execFileP7 = (0, import_util6.promisify)(import_child_process15.execFile);
16671
+ var path35 = __toESM(require("path"));
16672
+ var execFileP7 = (0, import_util6.promisify)(import_child_process16.execFile);
16176
16673
  var MAX_BUFFER4 = 8 * 1024 * 1024;
16177
16674
  function resetStdinForChild4() {
16178
16675
  if (process.stdin.isTTY) {
@@ -16213,7 +16710,7 @@ var RailwayProvider = class {
16213
16710
  );
16214
16711
  resetStdinForChild4();
16215
16712
  await new Promise((resolve4, reject) => {
16216
- const proc = (0, import_child_process15.spawn)("railway", ["login"], { stdio: "inherit" });
16713
+ const proc = (0, import_child_process16.spawn)("railway", ["login"], { stdio: "inherit" });
16217
16714
  proc.on("exit", (code) => {
16218
16715
  if (code === 0) resolve4();
16219
16716
  else reject(new Error("railway login failed."));
@@ -16356,7 +16853,7 @@ var RailwayProvider = class {
16356
16853
  }
16357
16854
  resetStdinForChild4();
16358
16855
  return new Promise((resolve4, reject) => {
16359
- const proc = (0, import_child_process15.spawn)(
16856
+ const proc = (0, import_child_process16.spawn)(
16360
16857
  "railway",
16361
16858
  ["shell", "--project", projectId, "--service", serviceId, "--command", command2],
16362
16859
  { stdio: "inherit" }
@@ -16380,8 +16877,8 @@ var RailwayProvider = class {
16380
16877
  const tarEnv = { ...process.env, COPYFILE_DISABLE: "1" };
16381
16878
  const remoteCmd = `mkdir -p ${shellQuote4(remoteDir)} && tar -xzf - -C ${shellQuote4(remoteDir)}`;
16382
16879
  await new Promise((resolve4, reject) => {
16383
- const tar = (0, import_child_process15.spawn)("tar", tarArgs, { stdio: ["ignore", "pipe", "pipe"], env: tarEnv });
16384
- const sh = (0, import_child_process15.spawn)(
16880
+ const tar = (0, import_child_process16.spawn)("tar", tarArgs, { stdio: ["ignore", "pipe", "pipe"], env: tarEnv });
16881
+ const sh = (0, import_child_process16.spawn)(
16385
16882
  "railway",
16386
16883
  ["shell", "--project", projectId, "--service", serviceId, "--command", remoteCmd],
16387
16884
  { stdio: [tar.stdout, "pipe", "pipe"] }
@@ -16407,14 +16904,14 @@ var RailwayProvider = class {
16407
16904
  if (!projectId || !serviceId) {
16408
16905
  throw new Error("Invalid Railway workspace id (expected projectId/serviceId).");
16409
16906
  }
16410
- const remoteDir = path33.posix.dirname(remotePath);
16907
+ const remoteDir = path35.posix.dirname(remotePath);
16411
16908
  const parts = [`mkdir -p ${shellQuote4(remoteDir)}`, `cat > ${shellQuote4(remotePath)}`];
16412
16909
  if (options.mode != null) {
16413
16910
  parts.push(`chmod ${options.mode.toString(8)} ${shellQuote4(remotePath)}`);
16414
16911
  }
16415
16912
  const cmd = parts.join(" && ");
16416
16913
  await new Promise((resolve4, reject) => {
16417
- const proc = (0, import_child_process15.spawn)(
16914
+ const proc = (0, import_child_process16.spawn)(
16418
16915
  "railway",
16419
16916
  ["shell", "--project", projectId, "--service", serviceId, "--command", cmd],
16420
16917
  { stdio: ["pipe", "pipe", "pipe"] }
@@ -16948,8 +17445,8 @@ async function stopWorkspaceFromLocal(target) {
16948
17445
 
16949
17446
  // src/commands/link.ts
16950
17447
  var import_node_crypto4 = require("crypto");
16951
- var fs26 = __toESM(require("fs"));
16952
- var path34 = __toESM(require("path"));
17448
+ var fs27 = __toESM(require("fs"));
17449
+ var path36 = __toESM(require("path"));
16953
17450
  var import_chokidar = __toESM(require("chokidar"));
16954
17451
  var import_picocolors11 = __toESM(require("picocolors"));
16955
17452
  function buildLinkContext(agentId) {
@@ -16987,7 +17484,7 @@ function parseLinkArgs(args2) {
16987
17484
  if (apiKeyFileArg) {
16988
17485
  const filePath = apiKeyFileArg.slice("--api-key-file=".length);
16989
17486
  try {
16990
- apiKey = fs26.readFileSync(path34.resolve(filePath), "utf8").trim();
17487
+ apiKey = fs27.readFileSync(path36.resolve(filePath), "utf8").trim();
16991
17488
  } catch (err) {
16992
17489
  throw new Error(`Could not read --api-key-file ${filePath}: ${err.message}`);
16993
17490
  }
@@ -17081,7 +17578,7 @@ async function link(args2 = []) {
17081
17578
  return;
17082
17579
  }
17083
17580
  if (parsed.tokenFile) {
17084
- const credential = fs26.readFileSync(path34.resolve(parsed.tokenFile), "utf8").trim();
17581
+ const credential = fs27.readFileSync(path36.resolve(parsed.tokenFile), "utf8").trim();
17085
17582
  if (!credential) {
17086
17583
  showError(`--token-file ${parsed.tokenFile} is empty.`);
17087
17584
  process.exit(1);
@@ -17270,8 +17767,8 @@ async function linkDryRunPreflight(ctx) {
17270
17767
  var import_node_dns = require("dns");
17271
17768
  var import_node_util4 = require("util");
17272
17769
  var import_node_crypto5 = require("crypto");
17273
- var fs27 = __toESM(require("fs"));
17274
- var path35 = __toESM(require("path"));
17770
+ var fs28 = __toESM(require("fs"));
17771
+ var path37 = __toESM(require("path"));
17275
17772
  var import_picocolors12 = __toESM(require("picocolors"));
17276
17773
  var dnsResolveP = (0, import_node_util4.promisify)(import_node_dns.resolve);
17277
17774
  async function checkDns(apiBase) {
@@ -17327,13 +17824,13 @@ async function checkHealth(apiBase) {
17327
17824
  }
17328
17825
  }
17329
17826
  function checkConfigDir() {
17330
- const dir = path35.join(require("os").homedir(), ".codeam");
17827
+ const dir = path37.join(require("os").homedir(), ".codeam");
17331
17828
  try {
17332
- fs27.mkdirSync(dir, { recursive: true, mode: 448 });
17333
- const probe = path35.join(dir, ".doctor-probe");
17334
- fs27.writeFileSync(probe, "ok", { mode: 384 });
17335
- const read = fs27.readFileSync(probe, "utf8");
17336
- fs27.unlinkSync(probe);
17829
+ fs28.mkdirSync(dir, { recursive: true, mode: 448 });
17830
+ const probe = path37.join(dir, ".doctor-probe");
17831
+ fs28.writeFileSync(probe, "ok", { mode: 384 });
17832
+ const read = fs28.readFileSync(probe, "utf8");
17833
+ fs28.unlinkSync(probe);
17337
17834
  if (read !== "ok") throw new Error("write/read round-trip mismatch");
17338
17835
  return {
17339
17836
  id: "config-dir",
@@ -17397,7 +17894,7 @@ function checkNodePty() {
17397
17894
  detail: "not required on this platform"
17398
17895
  };
17399
17896
  }
17400
- const vendoredPath = path35.join(__dirname, "vendor", "node-pty");
17897
+ const vendoredPath = path37.join(__dirname, "vendor", "node-pty");
17401
17898
  for (const target of [vendoredPath, "node-pty"]) {
17402
17899
  try {
17403
17900
  require(target);
@@ -17439,7 +17936,7 @@ function checkChokidar() {
17439
17936
  }
17440
17937
  async function doctor(args2 = []) {
17441
17938
  const json = args2.includes("--json");
17442
- const cliVersion = true ? "2.20.2" : "0.0.0-dev";
17939
+ const cliVersion = true ? "2.21.0" : "0.0.0-dev";
17443
17940
  const apiBase = resolveApiBaseUrl();
17444
17941
  const diagnosticId = (0, import_node_crypto5.randomUUID)();
17445
17942
  log.info("doctor", `run id=${diagnosticId} cli=${cliVersion}`);
@@ -17638,7 +18135,7 @@ async function completion(args2) {
17638
18135
  // src/commands/version.ts
17639
18136
  var import_picocolors13 = __toESM(require("picocolors"));
17640
18137
  function version2() {
17641
- const v = true ? "2.20.2" : "unknown";
18138
+ const v = true ? "2.21.0" : "unknown";
17642
18139
  console.log(`${import_picocolors13.default.bold("codeam-cli")} ${import_picocolors13.default.cyan(v)}`);
17643
18140
  }
17644
18141
 
@@ -17766,9 +18263,9 @@ function tryShowSubcommandHelp(cmd, args2) {
17766
18263
  var _subcommandHelpKeys = Object.keys(HELPS);
17767
18264
 
17768
18265
  // src/lib/updateNotifier.ts
17769
- var fs28 = __toESM(require("fs"));
18266
+ var fs29 = __toESM(require("fs"));
17770
18267
  var os25 = __toESM(require("os"));
17771
- var path36 = __toESM(require("path"));
18268
+ var path38 = __toESM(require("path"));
17772
18269
  var https7 = __toESM(require("https"));
17773
18270
  var import_picocolors16 = __toESM(require("picocolors"));
17774
18271
  var PKG_NAME = "codeam-cli";
@@ -17776,12 +18273,12 @@ var REGISTRY_URL = `https://registry.npmjs.org/${PKG_NAME}/latest`;
17776
18273
  var TTL_MS = 24 * 60 * 60 * 1e3;
17777
18274
  var REQUEST_TIMEOUT_MS = 1500;
17778
18275
  function cachePath() {
17779
- const dir = path36.join(os25.homedir(), ".codeam");
17780
- return path36.join(dir, "update-check.json");
18276
+ const dir = path38.join(os25.homedir(), ".codeam");
18277
+ return path38.join(dir, "update-check.json");
17781
18278
  }
17782
18279
  function readCache() {
17783
18280
  try {
17784
- const raw = fs28.readFileSync(cachePath(), "utf8");
18281
+ const raw = fs29.readFileSync(cachePath(), "utf8");
17785
18282
  const parsed = JSON.parse(raw);
17786
18283
  if (typeof parsed.fetchedAt !== "number" || typeof parsed.latest !== "string") return null;
17787
18284
  return parsed;
@@ -17792,10 +18289,10 @@ function readCache() {
17792
18289
  function writeCache(cache) {
17793
18290
  try {
17794
18291
  const file = cachePath();
17795
- fs28.mkdirSync(path36.dirname(file), { recursive: true });
18292
+ fs29.mkdirSync(path38.dirname(file), { recursive: true });
17796
18293
  const tmp = `${file}.${process.pid}.tmp`;
17797
- fs28.writeFileSync(tmp, JSON.stringify(cache));
17798
- fs28.renameSync(tmp, file);
18294
+ fs29.writeFileSync(tmp, JSON.stringify(cache));
18295
+ fs29.renameSync(tmp, file);
17799
18296
  } catch {
17800
18297
  }
17801
18298
  }
@@ -17866,7 +18363,7 @@ function checkForUpdates() {
17866
18363
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
17867
18364
  if (process.env.CI) return;
17868
18365
  if (!process.stdout.isTTY) return;
17869
- const current = true ? "2.20.2" : null;
18366
+ const current = true ? "2.21.0" : null;
17870
18367
  if (!current) return;
17871
18368
  const cache = readCache();
17872
18369
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;