@usepowerplant/cli 0.4.11 → 0.4.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
+
3
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="66f773d5-e0b2-5538-a184-3ee15c3a469a")}catch(e){}}();
2
4
  const __cliNodeMajor = Number(process.versions.node.split('.')[0]);
3
5
  if (__cliNodeMajor < 20) {
4
6
  console.error(`powerplant needs Node 20 or newer — this is Node ${process.versions.node}.`);
@@ -56698,12 +56700,13 @@ function boundRedactedText(raw, maxBytes) {
56698
56700
  return boundText(redactSecrets(raw), maxBytes);
56699
56701
  }
56700
56702
  function boundText(redacted, maxBytes) {
56701
- const bytes = utf8Encoder.encode(redacted);
56703
+ const text = redacted.includes("\0") ? redacted.replaceAll("\0", "\uFFFD") : redacted;
56704
+ const bytes = utf8Encoder.encode(text);
56702
56705
  if (bytes.length <= maxBytes) {
56703
- return { text: redacted, truncated: false, originalBytes: bytes.length };
56706
+ return { text, truncated: false, originalBytes: bytes.length };
56704
56707
  }
56705
- const text = utf8Decoder.decode(bytes.subarray(0, maxBytes)).replace(/�+$/u, "");
56706
- return { text, truncated: true, originalBytes: bytes.length };
56708
+ const cut = utf8Decoder.decode(bytes.subarray(0, maxBytes)).replace(/�+$/u, "");
56709
+ return { text: cut, truncated: true, originalBytes: bytes.length };
56707
56710
  }
56708
56711
 
56709
56712
  // ../core/src/codeHost.ts
@@ -56755,18 +56758,35 @@ function pullRequestRefsInText(text) {
56755
56758
  }
56756
56759
  return refs;
56757
56760
  }
56761
+ var PULL_REQUEST_ALREADY_EXISTS = /a pull request for branch \S+ into branch \S+ already exists:/;
56762
+ function pullRequestAlreadyExistsInText(text) {
56763
+ return PULL_REQUEST_ALREADY_EXISTS.test(text);
56764
+ }
56758
56765
  function pullRequestRefFromUrl(url2) {
56759
56766
  return pullRequestRefsInText(url2)[0] ?? null;
56760
56767
  }
56761
56768
  var MAY_TOUCH_HOST = /\b(?:git|gh)\b/;
56762
56769
  function readCodeHostCommands(command, currentBranch) {
56770
+ return readCommands(command, currentBranch, 0);
56771
+ }
56772
+ var SUBSTITUTION_DEPTH_MAX = 4;
56773
+ function readCommands(command, currentBranch, depth) {
56763
56774
  const result = { pushes: [], pullRequestCreates: [] };
56764
56775
  if (!MAY_TOUCH_HOST.test(command)) return result;
56765
56776
  const chain = shellCommands(command);
56766
56777
  let branch = currentBranch;
56767
56778
  chain.forEach((simple, i) => {
56768
56779
  const program = programOf(simple.words);
56769
- if (program === null) return;
56780
+ if (program === null) {
56781
+ if (depth >= SUBSTITUTION_DEPTH_MAX) return;
56782
+ const alone = simple.words.length === 1 && simple.next === null;
56783
+ for (const substituted of simple.assignedSubstitutions) {
56784
+ for (const create of readCommands(substituted, branch, depth + 1).pullRequestCreates) {
56785
+ result.pullRequestCreates.push({ ...create, ownExitStatus: create.ownExitStatus && alone });
56786
+ }
56787
+ }
56788
+ return;
56789
+ }
56770
56790
  if (program.name === "git") {
56771
56791
  const git = gitSubcommand(program.args);
56772
56792
  if (git === null) return;
@@ -56799,6 +56819,7 @@ function pushOutcomeInText(text, branch) {
56799
56819
  return /Everything up-to-date/.test(text) ? true : null;
56800
56820
  }
56801
56821
  var ENV_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
56822
+ var ASSIGNMENT_NAME = /^[A-Za-z_][A-Za-z0-9_]*=$/;
56802
56823
  function programOf(words) {
56803
56824
  let i = 0;
56804
56825
  while (i < words.length && (ENV_ASSIGNMENT.test(words[i]) || words[i] === "env")) i += 1;
@@ -56895,6 +56916,7 @@ function exitStatusIsOwn(chain, index) {
56895
56916
  function shellCommands(text) {
56896
56917
  const commands = [];
56897
56918
  let words = [];
56919
+ let assignedSubstitutions = [];
56898
56920
  let word = "";
56899
56921
  let inWord = false;
56900
56922
  let quote = null;
@@ -56906,8 +56928,9 @@ function shellCommands(text) {
56906
56928
  };
56907
56929
  const endCommand = (next) => {
56908
56930
  endWord();
56909
- if (words.length > 0) commands.push({ words: withoutRedirections(words), next });
56931
+ if (words.length > 0) commands.push({ words: withoutRedirections(words), assignedSubstitutions, next });
56910
56932
  words = [];
56933
+ assignedSubstitutions = [];
56911
56934
  };
56912
56935
  for (let i = 0; i < text.length; i++) {
56913
56936
  const ch = text[i];
@@ -56927,6 +56950,11 @@ function shellCommands(text) {
56927
56950
  inWord = true;
56928
56951
  } else if (ch === "$" && text[i + 1] === "(" || ch === "`") {
56929
56952
  const end = substitutionEnd(text, i);
56953
+ const after = text[end + 1];
56954
+ const endsWord = after === void 0 || /[\s;&|)]/.test(after);
56955
+ if (ch === "$" && text[end] === ")" && ASSIGNMENT_NAME.test(word) && endsWord) {
56956
+ assignedSubstitutions.push(text.slice(i + 2, end));
56957
+ }
56930
56958
  word += text.slice(i, end + 1);
56931
56959
  inWord = true;
56932
56960
  i = end;
@@ -57037,6 +57065,9 @@ var PrEvidenceCollector = class {
57037
57065
  evidence = [];
57038
57066
  seen = /* @__PURE__ */ new Set();
57039
57067
  watched = /* @__PURE__ */ new Map();
57068
+ /** By process id: what a watched call left unsettled when its result said
57069
+ * the process still runs. */
57070
+ running = /* @__PURE__ */ new Map();
57040
57071
  dropped = 0;
57041
57072
  /** The harness recorded the pull request for this session. Repeats (a
57042
57073
  * record per turn while a PR stays bound) keep the first time. */
@@ -57045,15 +57076,50 @@ var PrEvidenceCollector = class {
57045
57076
  }
57046
57077
  /** A shell command the session ran: watched until its result arrives when
57047
57078
  * it pushes a branch or creates a pull request, ignored otherwise. */
57048
- command(callId, command, currentBranch) {
57049
- this.watch(callId, readCodeHostCommands(command, currentBranch));
57079
+ command(callId, command, currentBranch, startedAt = null) {
57080
+ this.watch(callId, readCodeHostCommands(command, currentBranch), startedAt);
57050
57081
  }
57051
57082
  /** A tool call that would push or create a pull request, in the code host
57052
57083
  * adapter's terms (a harness's own pull request tool, for example). */
57053
- watch(callId, commands) {
57084
+ watch(callId, commands, startedAt = null) {
57054
57085
  if (commands.pushes.length === 0 && commands.pullRequestCreates.length === 0) return;
57055
- this.watched.set(callId, commands);
57056
- if (this.watched.size > WATCHED_CALLS_MAX) this.watched.delete(this.watched.keys().next().value);
57086
+ keepBounded(this.watched, callId, { ...commands, startedAt });
57087
+ }
57088
+ /** A call that reads more output of a running process (Codex's
57089
+ * write_stdin poll; Claude Code's TaskOutput, or a read of a background
57090
+ * task's output file): its result is read as the rest of the watched call
57091
+ * that started the process. */
57092
+ follow(callId, process2) {
57093
+ const unsettled = this.running.get(process2);
57094
+ if (unsettled === void 0) return;
57095
+ this.running.delete(process2);
57096
+ this.watch(callId, unsettled, unsettled.startedAt);
57097
+ }
57098
+ /** Whether any process is followed, so a parser reads an unwatched
57099
+ * call's output only while it could matter. */
57100
+ following() {
57101
+ return this.running.size > 0;
57102
+ }
57103
+ /** Another call now runs a process under this id: a later poll of the id
57104
+ * is no longer the rest of the watched call that left it running. */
57105
+ forget(process2) {
57106
+ this.running.delete(process2);
57107
+ }
57108
+ /** Watches several watched calls as one, in the order given: the parts of
57109
+ * a call whose output cannot be told apart, read once as a whole. The
57110
+ * parts are one call's, so they started together and the first part's
57111
+ * time is the whole's. */
57112
+ merge(callIds, into) {
57113
+ const parts = callIds.flatMap((callId) => this.watched.get(callId) ?? []);
57114
+ for (const callId of callIds) this.watched.delete(callId);
57115
+ this.watch(
57116
+ into,
57117
+ {
57118
+ pushes: parts.flatMap((part) => part.pushes),
57119
+ pullRequestCreates: parts.flatMap((part) => part.pullRequestCreates)
57120
+ },
57121
+ parts[0]?.startedAt ?? null
57122
+ );
57057
57123
  }
57058
57124
  watching(callId) {
57059
57125
  return this.watched.has(callId);
@@ -57070,7 +57136,16 @@ var PrEvidenceCollector = class {
57070
57136
  * result names the pull request: gh prints the created PR's URL last, so
57071
57137
  * a call with one creation takes the last URL and a call with several
57072
57138
  * only when the text names exactly as many; a failed call whose status
57073
- * is the creation's own counts nothing.
57139
+ * is the creation's own counts nothing, and neither does a call whose
57140
+ * text carries gh's refusal because the pull request exists (the URL it
57141
+ * prints is that pull request's, and a result read from a file states no
57142
+ * status). A text cut to the URL alone (`| tail -1`, a `grep` of the
57143
+ * output file) hides the refusal, and the URL then counts.
57144
+ *
57145
+ * When the result says one process still runs, whatever it settled
57146
+ * neither way waits for that process's later output (`follow`). A push
57147
+ * git rejected, or a command whose own status failed, is settled: a
57148
+ * later output never revives it.
57074
57149
  */
57075
57150
  result(callId, outcome) {
57076
57151
  const commands = this.watched.get(callId);
@@ -57079,35 +57154,45 @@ var PrEvidenceCollector = class {
57079
57154
  const destinations = pushDestinationsInText(outcome.text);
57080
57155
  const oneRemote = new Set(commands.pushes.map((push) => push.remote)).size === 1;
57081
57156
  const shared = oneRemote && new Set(destinations).size === 1 ? destinations[0] : null;
57157
+ const unsettled = { pushes: [], pullRequestCreates: [], startedAt: commands.startedAt };
57082
57158
  for (const [i, push] of commands.pushes.entries()) {
57083
57159
  if (push.branch === null) continue;
57084
- const pushed = pushOutcomeInText(outcome.text, push.branch) ?? (push.ownExitStatus && outcome.succeeded === true);
57085
- if (!pushed) continue;
57160
+ const pushed = pushOutcomeInText(outcome.text, push.branch) ?? (push.ownExitStatus ? outcome.succeeded : null);
57161
+ if (pushed === null) unsettled.pushes.push(push);
57162
+ if (pushed !== true) continue;
57086
57163
  const repository = destinations.length === commands.pushes.length ? destinations[i] : shared;
57087
57164
  this.addPush({ kind: "branch-pushed", at: outcome.at, prId: null, repository, branch: push.branch, line: outcome.line });
57088
57165
  }
57089
57166
  const creates = commands.pullRequestCreates;
57090
- if (creates.length === 0) return;
57091
57167
  const refs = pullRequestRefsInText(outcome.text);
57092
57168
  const paired = creates.length === 1 ? [refs.at(-1)] : refs.length === creates.length ? refs : [];
57169
+ const refused = pullRequestAlreadyExistsInText(outcome.text);
57093
57170
  creates.forEach((create, i) => {
57094
57171
  const ref = paired[i];
57095
- if (ref === void 0 || outcome.succeeded === false && create.ownExitStatus) return;
57172
+ if (refused || outcome.succeeded === false && create.ownExitStatus) return;
57173
+ if (ref === void 0) {
57174
+ unsettled.pullRequestCreates.push(create);
57175
+ return;
57176
+ }
57096
57177
  this.add({
57097
57178
  kind: "pull-request-created",
57098
57179
  at: outcome.at,
57099
57180
  prId: pullRequestId(ref),
57100
57181
  repository: null,
57101
57182
  branch: create.head,
57102
- line: outcome.line
57183
+ line: outcome.line,
57184
+ commandStartedAt: commands.startedAt
57103
57185
  });
57104
57186
  });
57187
+ if (outcome.running === null || unsettled.pushes.length === 0 && unsettled.pullRequestCreates.length === 0) return;
57188
+ keepBounded(this.running, outcome.running, unsettled);
57105
57189
  }
57106
57190
  /** The evidence collected, in transcript order, with pushes past the cap
57107
- * dropped oldest first. Calls still watched are dropped: their results
57108
- * never arrived. */
57191
+ * dropped oldest first. Calls still watched and processes still followed
57192
+ * are dropped: their results never arrived. */
57109
57193
  finish() {
57110
57194
  this.watched.clear();
57195
+ this.running.clear();
57111
57196
  const pushes = this.evidence.filter((item) => item.kind === "branch-pushed");
57112
57197
  if (pushes.length <= PUSH_EVIDENCE_MAX) return this.evidence;
57113
57198
  const cut = new Set(pushes.slice(0, pushes.length - PUSH_EVIDENCE_MAX));
@@ -57119,10 +57204,13 @@ var PrEvidenceCollector = class {
57119
57204
  droppedPushes() {
57120
57205
  return this.dropped;
57121
57206
  }
57122
- /** A link or a creation is one fact per pull request: repeats keep the
57123
- * first. */
57207
+ /** A link is one fact per pull request, and so is a creation per command:
57208
+ * repeats keep the first. Two commands that each named the pull request
57209
+ * are two facts (a refused retry read before the first command's own
57210
+ * output), because the match keeps only the one whose command can have
57211
+ * opened it. */
57124
57212
  add(item) {
57125
- const key = `${item.kind}|${item.prId ?? ""}|${item.branch ?? ""}`;
57213
+ const key = `${item.kind}|${item.prId ?? ""}|${item.branch ?? ""}|${item.commandStartedAt ?? ""}`;
57126
57214
  if (this.seen.has(key)) return;
57127
57215
  this.seen.add(key);
57128
57216
  this.evidence.push(item);
@@ -57136,6 +57224,11 @@ var PrEvidenceCollector = class {
57136
57224
  this.evidence.push(item);
57137
57225
  }
57138
57226
  };
57227
+ function keepBounded(map2, key, value) {
57228
+ map2.delete(key);
57229
+ map2.set(key, value);
57230
+ if (map2.size > WATCHED_CALLS_MAX) map2.delete(map2.keys().next().value);
57231
+ }
57139
57232
 
57140
57233
  // ../core/src/digest.ts
57141
57234
  var TOOL_CALL_INPUT_MAX_BYTES = 65536;
@@ -57155,6 +57248,7 @@ function createDigestTurnParser(options = {}) {
57155
57248
  let pendingEnvelopes = [];
57156
57249
  let currentEnvelope = null;
57157
57250
  const evidence = new PrEvidenceCollector();
57251
+ const backgroundTasks = { started: /* @__PURE__ */ new Map(), reads: /* @__PURE__ */ new Map() };
57158
57252
  let line = -1;
57159
57253
  const landPending = (turn) => {
57160
57254
  for (const envelope of pendingEnvelopes) {
@@ -57184,7 +57278,7 @@ function createDigestTurnParser(options = {}) {
57184
57278
  if (record2.isMeta === true) return;
57185
57279
  const at = recordTime(record2);
57186
57280
  if (record2.type === "user") {
57187
- toolResults(record2, evidence, line);
57281
+ toolResults(record2, evidence, backgroundTasks, line);
57188
57282
  const text = userText(record2);
57189
57283
  if (text) {
57190
57284
  const origin = claudeCodeMessageOrigin(originFacts(record2), text);
@@ -57233,8 +57327,9 @@ function createDigestTurnParser(options = {}) {
57233
57327
  }
57234
57328
  pendingTools.push(call);
57235
57329
  if (part.name === "Bash" && typeof part.id === "string" && typeof part.input?.command === "string") {
57236
- evidence.command(part.id, part.input.command, recordBranch(record2));
57330
+ evidence.command(part.id, part.input.command, recordBranch(record2), at);
57237
57331
  }
57332
+ if (typeof part.id === "string") followBackgroundTask(part.id, part.name, part.input, evidence, backgroundTasks);
57238
57333
  }
57239
57334
  if (part.type === "text" && typeof part.text === "string" && part.text.trim() !== "") {
57240
57335
  const origin = claudeCodeMessageOrigin(facts, part.text);
@@ -57311,16 +57406,38 @@ function isRecord(value) {
57311
57406
  function recordBranch(record2) {
57312
57407
  return typeof record2.gitBranch === "string" && record2.gitBranch !== "" ? record2.gitBranch : null;
57313
57408
  }
57314
- function toolResults(record2, evidence, line) {
57409
+ function followBackgroundTask(callId, tool, input2, evidence, tasks) {
57410
+ if (tasks.started.size === 0 || evidence.watching(callId)) return;
57411
+ const named = tool === "TaskOutput" ? input2?.task_id : tool === "Read" ? input2?.file_path : tool === "Bash" ? input2?.command : void 0;
57412
+ if (typeof named !== "string" || tool === "Bash" && RUNS_GH.test(named)) return;
57413
+ const read = new Set(
57414
+ [...tasks.started].filter(([task]) => tool === "TaskOutput" ? named === task : named.includes(`/${task}.output`)).map(([, followed2]) => followed2)
57415
+ );
57416
+ if (read.size !== 1) return;
57417
+ const [followed] = read;
57418
+ evidence.follow(callId, followed);
57419
+ if (evidence.watching(callId)) keepBounded(tasks.reads, callId, followed);
57420
+ }
57421
+ var RUNS_GH = /(?:^|[\s;&|(])gh\s/;
57422
+ function toolResults(record2, evidence, tasks, line) {
57315
57423
  const content = isRecord(record2.message) ? record2.message.content : void 0;
57316
57424
  if (!Array.isArray(content)) return;
57317
57425
  const result = isRecord(record2.toolUseResult) ? record2.toolUseResult : {};
57426
+ const file2 = isRecord(result.file) ? result.file : {};
57427
+ const task = isRecord(result.task) ? result.task : {};
57318
57428
  for (const block of content) {
57319
57429
  if (!isRecord(block) || block.type !== "tool_result" || typeof block.tool_use_id !== "string") continue;
57320
57430
  if (!evidence.watching(block.tool_use_id)) continue;
57431
+ const reads = tasks.reads.get(block.tool_use_id);
57432
+ tasks.reads.delete(block.tool_use_id);
57433
+ const started = typeof result.backgroundTaskId === "string" ? result.backgroundTaskId : null;
57434
+ if (started !== null) keepBounded(tasks.started, started, reads ?? started);
57435
+ const failed2 = block.is_error === true || result.interrupted === true;
57436
+ const taskExited = typeof task.exitCode === "number" && task.task_id === reads;
57437
+ const status = reads !== void 0 ? { succeeded: taskExited ? task.exitCode === 0 : null, running: taskExited ? null : reads } : { succeeded: failed2 ? false : started !== null ? null : true, running: started };
57321
57438
  evidence.result(block.tool_use_id, {
57322
- succeeded: block.is_error !== true && result.interrupted !== true && result.backgroundTaskId === void 0,
57323
- text: [result.stdout, result.stderr].filter((part) => typeof part === "string").concat(contentText(block.content)).join("\n"),
57439
+ ...status,
57440
+ text: [result.stdout, result.stderr, file2.content, task.output].filter((part) => typeof part === "string").concat(contentText(block.content)).join("\n"),
57324
57441
  at: recordTime(record2),
57325
57442
  line
57326
57443
  });
@@ -57988,6 +58105,8 @@ function createCodexTurnParser(options = {}) {
57988
58105
  let pendingTools = [];
57989
58106
  let pendingAt = null;
57990
58107
  const evidence = new PrEvidenceCollector();
58108
+ const scripts = /* @__PURE__ */ new Map();
58109
+ const runningScripts = /* @__PURE__ */ new Map();
57991
58110
  let line = -1;
57992
58111
  const envelopes = [];
57993
58112
  let pendingEnvelopes = [];
@@ -58047,9 +58166,24 @@ function createCodexTurnParser(options = {}) {
58047
58166
  const responseItem = item;
58048
58167
  const callId = typeof responseItem.call_id === "string" ? responseItem.call_id : null;
58049
58168
  if (callId !== null && isToolOutput(responseItem)) {
58050
- if (evidence.watching(callId)) {
58169
+ const script = scripts.get(callId);
58170
+ scripts.delete(callId);
58171
+ if (script !== void 0) {
58172
+ const [header = "", ...blocks] = outputBlocks(responseItem.output);
58173
+ const cell = SCRIPT_STILL_RUNNING.exec(header)?.[1];
58174
+ if (cell === void 0) scriptResult(evidence, script, header, blocks, at, line);
58175
+ else keepBounded(runningScripts, cell, script);
58176
+ } else if (evidence.watching(callId)) {
58051
58177
  const text2 = contentText(responseItem.output);
58052
- evidence.result(callId, { succeeded: codexExitStatus(text2), text: text2, at, line });
58178
+ evidence.result(callId, { succeeded: codexExitStatus(text2), text: text2, running: codexRunningProcess(text2), at, line });
58179
+ } else if (runningScripts.size > 0 || evidence.following()) {
58180
+ const [header = "", ...blocks] = outputBlocks(responseItem.output);
58181
+ const cell = SCRIPT_STILL_RUNNING.exec(header)?.[1];
58182
+ if (cell !== void 0) runningScripts.delete(cell);
58183
+ for (const text2 of [header, ...blocks]) {
58184
+ const process2 = codexRunningProcess(text2);
58185
+ if (process2 !== null) evidence.forget(process2);
58186
+ }
58053
58187
  }
58054
58188
  return;
58055
58189
  }
@@ -58062,7 +58196,10 @@ function createCodexTurnParser(options = {}) {
58062
58196
  }
58063
58197
  pendingTools.push(call);
58064
58198
  pendingAt ??= at;
58065
- if (callId !== null) watchCodexCall(evidence, callId, responseItem);
58199
+ if (callId !== null) {
58200
+ const script = watchCodexCall(evidence, callId, responseItem, at, runningScripts);
58201
+ if (script !== null) keepBounded(scripts, callId, script);
58202
+ }
58066
58203
  return;
58067
58204
  }
58068
58205
  if (responseItem.type !== "message") return;
@@ -58105,27 +58242,76 @@ function createCodexTurnParser(options = {}) {
58105
58242
  }
58106
58243
  };
58107
58244
  }
58108
- function watchCodexCall(evidence, callId, item) {
58245
+ var stepKey = (callId, step) => `${callId}#${step}`;
58246
+ function watchCodexCall(evidence, callId, item, at, runningScripts) {
58109
58247
  if (item.type === "custom_tool_call" && item.name === "exec" && typeof item.input === "string") {
58110
- evidence.command(callId, execCommandStrings(item.input).join("\n"), null);
58111
- return;
58248
+ const steps = execScriptSteps(item.input);
58249
+ const commands = [];
58250
+ let seen = { pushes: 0, pullRequestCreates: 0 };
58251
+ steps.forEach((step, i) => {
58252
+ if (step.polls !== null) evidence.follow(stepKey(callId, i), step.polls);
58253
+ if (step.command === null) return;
58254
+ commands.push(step.command);
58255
+ const read = readCodeHostCommands(commands.join("\n"), null);
58256
+ evidence.watch(
58257
+ stepKey(callId, i),
58258
+ {
58259
+ pushes: read.pushes.slice(seen.pushes),
58260
+ pullRequestCreates: read.pullRequestCreates.slice(seen.pullRequestCreates)
58261
+ },
58262
+ at
58263
+ );
58264
+ seen = { pushes: read.pushes.length, pullRequestCreates: read.pullRequestCreates.length };
58265
+ });
58266
+ const watched = steps.some((_, i) => evidence.watching(stepKey(callId, i)));
58267
+ return watched ? { callId, polls: steps.map((step) => step.polls), printsEachStep: printsEachStep(item.input, steps) } : null;
58112
58268
  }
58113
58269
  if (item.type === "local_shell_call") {
58114
58270
  const command = argvCommand(asRecord2(item.action)?.command);
58115
- if (command !== null) evidence.command(callId, command, null);
58116
- return;
58271
+ if (command !== null) evidence.command(callId, command, null, at);
58272
+ return null;
58117
58273
  }
58118
- if (item.type !== "function_call" || typeof item.arguments !== "string") return;
58274
+ if (item.type !== "function_call" || typeof item.arguments !== "string") return null;
58119
58275
  const args = safeJsonRecord(item.arguments);
58120
58276
  if (item.name === "exec_command" && typeof args?.cmd === "string") {
58121
- evidence.command(callId, args.cmd, null);
58277
+ evidence.command(callId, args.cmd, null, at);
58122
58278
  } else if (item.name === "shell") {
58123
58279
  const command = argvCommand(args?.command);
58124
- if (command !== null) evidence.command(callId, command, null);
58280
+ if (command !== null) evidence.command(callId, command, null, at);
58281
+ } else if (item.name === "write_stdin" && typeof args?.session_id === "number") {
58282
+ evidence.follow(callId, String(args.session_id));
58283
+ } else if (item.name === "wait" && (typeof args?.cell_id === "string" || typeof args?.cell_id === "number")) {
58284
+ const script = runningScripts.get(String(args.cell_id)) ?? null;
58285
+ runningScripts.delete(String(args.cell_id));
58286
+ return script;
58125
58287
  } else if (item.name === "_create_pull_request") {
58126
58288
  const head = typeof args?.head === "string" ? args.head : null;
58127
- evidence.watch(callId, { pushes: [], pullRequestCreates: [{ head, ownExitStatus: true }] });
58289
+ evidence.watch(callId, { pushes: [], pullRequestCreates: [{ head, ownExitStatus: true }] }, at);
58128
58290
  }
58291
+ return null;
58292
+ }
58293
+ function scriptResult(evidence, script, header, blocks, at, line) {
58294
+ const paired = script.printsEachStep && blocks.length === script.polls.length;
58295
+ const keys = script.polls.map((_, i) => stepKey(script.callId, i));
58296
+ if (!paired) {
58297
+ evidence.merge(keys.filter((_, i) => script.polls[i] === null), script.callId);
58298
+ evidence.result(script.callId, { succeeded: null, text: [header, ...blocks].join("\n"), running: null, at, line });
58299
+ }
58300
+ script.polls.forEach((polls, i) => {
58301
+ if (!evidence.watching(keys[i])) return;
58302
+ const text = paired ? blocks[i] : "";
58303
+ const succeeded = paired ? codexExitStatus(text) : null;
58304
+ const running = (paired ? codexRunningProcess(text) : null) ?? (succeeded === null ? polls : null);
58305
+ evidence.result(keys[i], { succeeded, text, running, at, line });
58306
+ });
58307
+ }
58308
+ function outputBlocks(output2) {
58309
+ if (typeof output2 === "string") return [output2];
58310
+ if (!Array.isArray(output2)) return [];
58311
+ return output2.map((block) => {
58312
+ const text = asRecord2(block)?.text;
58313
+ return typeof text === "string" ? text : "";
58314
+ });
58129
58315
  }
58130
58316
  function argvCommand(argv) {
58131
58317
  if (typeof argv === "string") return argv;
@@ -58134,22 +58320,56 @@ function argvCommand(argv) {
58134
58320
  if (argv.length === 3 && /(?:^|\/)(?:ba|z|da|k)?sh$/.test(shell) && /^-[a-z]*c[a-z]*$/.test(flag)) return script;
58135
58321
  return argv.map((word) => /^[A-Za-z0-9._/:=@+-]+$/.test(word) ? word : `'${word.replaceAll("'", "'\\''")}'`).join(" ");
58136
58322
  }
58137
- var EXEC_COMMAND_CALL = /exec_command\(\{\s*"?cmd"?\s*:\s*("(?:[^"\\]|\\.)*")/g;
58138
- function execCommandStrings(script) {
58139
- const commands = [];
58140
- for (const match of script.matchAll(EXEC_COMMAND_CALL)) {
58141
- try {
58142
- const parsed = JSON.parse(match[1]);
58143
- if (typeof parsed === "string") commands.push(parsed);
58144
- } catch {
58145
- continue;
58146
- }
58323
+ var SCRIPT_TOOL_CALL = /tools\.(\w+)\(/g;
58324
+ var COMMAND_ARGUMENT = /\{\s*"?cmd"?\s*:\s*("(?:[^"\\]|\\.)*")/y;
58325
+ var SESSION_ARGUMENT = /\{\s*"?session_id"?\s*:\s*(\d+)/y;
58326
+ function execScriptSteps(script) {
58327
+ const steps = [];
58328
+ const call = new RegExp(SCRIPT_TOOL_CALL);
58329
+ for (let match = call.exec(script); match !== null; match = call.exec(script)) {
58330
+ const argument = match[1] === "exec_command" ? COMMAND_ARGUMENT : match[1] === "write_stdin" ? SESSION_ARGUMENT : null;
58331
+ if (argument !== null) argument.lastIndex = call.lastIndex;
58332
+ const value = argument?.exec(script) ?? null;
58333
+ if (value !== null) call.lastIndex += value[0].length;
58334
+ steps.push({
58335
+ command: value !== null && match[1] === "exec_command" ? jsonString(value[1]) : null,
58336
+ polls: value !== null && match[1] === "write_stdin" ? value[1] : null
58337
+ });
58338
+ }
58339
+ return steps;
58340
+ }
58341
+ var STRING_LITERAL = /"(?:[^"\\]|\\.)*"/g;
58342
+ var TEXT_CALL = /\btext\(/g;
58343
+ var TEXT_OF_TOOL_CALL = /\btext\(\s*await\s+tools\.\w+\(/g;
58344
+ var DEFERRED_CODE = /\bfunction\b|=>|\.then\(/;
58345
+ function printsEachStep(script, steps) {
58346
+ if (steps.length === 1) return true;
58347
+ const code = script.replace(STRING_LITERAL, '""');
58348
+ if (DEFERRED_CODE.test(code)) return false;
58349
+ const printed = [...code.matchAll(TEXT_OF_TOOL_CALL)].length;
58350
+ return printed === steps.length && [...code.matchAll(TEXT_CALL)].length === steps.length;
58351
+ }
58352
+ function jsonString(literal2) {
58353
+ try {
58354
+ const parsed = JSON.parse(literal2);
58355
+ return typeof parsed === "string" ? parsed : null;
58356
+ } catch {
58357
+ return null;
58147
58358
  }
58148
- return commands;
58149
58359
  }
58150
58360
  function isToolOutput(item) {
58151
58361
  return item.type === "custom_tool_call_output" || item.type === "function_call_output" || item.type === "local_shell_call_output";
58152
58362
  }
58363
+ var SCRIPT_STILL_RUNNING = /^Script running with cell ID (\d+)/;
58364
+ var RUNNING_CHUNK = /^\s*\{[^\n]*?"session_id"\s*:\s*(\d+)/;
58365
+ var RUNNING_PROCESS_LINE = /^Process running with session ID (\d+)$/m;
58366
+ function codexRunningProcess(output2) {
58367
+ const chunk = RUNNING_CHUNK.exec(output2);
58368
+ if (chunk !== null) return chunk[1];
58369
+ const outputLine = output2.search(/^Output:/m);
58370
+ const header = outputLine === -1 ? output2 : output2.slice(0, outputLine);
58371
+ return RUNNING_PROCESS_LINE.exec(header)?.[1] ?? null;
58372
+ }
58153
58373
  var EXIT_STATUS = /"exit_code"\s*:\s*(\d+)|Process exited with code (\d+)/g;
58154
58374
  function codexExitStatus(output2) {
58155
58375
  const codes = [...output2.matchAll(EXIT_STATUS)].map((match) => Number(match[1] ?? match[2]));
@@ -85663,11 +85883,11 @@ import path12 from "node:path";
85663
85883
  import { existsSync as existsSync6, lstatSync as lstatSync4 } from "node:fs";
85664
85884
  import path11 from "node:path";
85665
85885
 
85666
- // ../../apps/collector/src/codexTranscriptHints.ts
85886
+ // ../../apps/collector/src/codexTranscriptDiscovery.ts
85667
85887
  import { createHash } from "node:crypto";
85668
85888
  import { closeSync, lstatSync, openSync, readSync, readdirSync, realpathSync } from "node:fs";
85669
85889
  import path2 from "node:path";
85670
- var hintSchema = external_exports.object({
85890
+ var transcriptDiscoveryRecordSchema = external_exports.object({
85671
85891
  sessionId: external_exports.string().regex(/^[a-zA-Z0-9_-]{1,200}$/),
85672
85892
  transcriptPath: external_exports.string().max(4096).nullable()
85673
85893
  });
@@ -85680,7 +85900,7 @@ function codexTranscriptRecordPath(home, sessionId) {
85680
85900
  `${createHash("sha256").update(sessionId).digest("hex")}.json`
85681
85901
  );
85682
85902
  }
85683
- function readCodexTranscriptHints(home, roots) {
85903
+ function readCodexTranscriptDiscoveryRecords(home, roots) {
85684
85904
  const dir = codexTranscriptRecordsDir(home);
85685
85905
  let files;
85686
85906
  try {
@@ -85692,9 +85912,9 @@ function readCodexTranscriptHints(home, roots) {
85692
85912
  for (const file2 of files) {
85693
85913
  if (!/^[a-f0-9]{64}\.json$/.test(file2)) continue;
85694
85914
  try {
85695
- const hintPath = path2.join(dir, file2);
85696
- if (!lstatSync(hintPath).isFile()) continue;
85697
- const fd = openSync(hintPath, "r");
85915
+ const recordPath = path2.join(dir, file2);
85916
+ if (!lstatSync(recordPath).isFile()) continue;
85917
+ const fd = openSync(recordPath, "r");
85698
85918
  let text;
85699
85919
  try {
85700
85920
  const buffer = Buffer.alloc(32 * 1024 + 1);
@@ -85704,10 +85924,10 @@ function readCodexTranscriptHints(home, roots) {
85704
85924
  } finally {
85705
85925
  closeSync(fd);
85706
85926
  }
85707
- const hint = hintSchema.parse(JSON.parse(text));
85708
- if (file2 !== `${createHash("sha256").update(hint.sessionId).digest("hex")}.json`) continue;
85709
- if (hint.transcriptPath === null || !path2.isAbsolute(hint.transcriptPath)) continue;
85710
- const rolloutPath = path2.normalize(hint.transcriptPath);
85927
+ const record2 = transcriptDiscoveryRecordSchema.parse(JSON.parse(text));
85928
+ if (file2 !== `${createHash("sha256").update(record2.sessionId).digest("hex")}.json`) continue;
85929
+ if (record2.transcriptPath === null || !path2.isAbsolute(record2.transcriptPath)) continue;
85930
+ const rolloutPath = path2.normalize(record2.transcriptPath);
85711
85931
  if (path2.extname(rolloutPath) !== ".jsonl") continue;
85712
85932
  const rootIndex = roots.findIndex((root2) => {
85713
85933
  const relative2 = path2.relative(root2, rolloutPath);
@@ -85717,7 +85937,7 @@ function readCodexTranscriptHints(home, roots) {
85717
85937
  const root = roots[rootIndex];
85718
85938
  if (realpathSync(rolloutPath) !== path2.join(realpathSync(root), path2.relative(root, rolloutPath)))
85719
85939
  continue;
85720
- result.push({ rolloutPath, sessionId: hint.sessionId, archived: rootIndex === 1 });
85940
+ result.push({ rolloutPath, sessionId: record2.sessionId, archived: rootIndex === 1 });
85721
85941
  } catch {
85722
85942
  }
85723
85943
  }
@@ -85730,7 +85950,7 @@ import { createHmac } from "node:crypto";
85730
85950
  // ../../apps/collector/src/http.ts
85731
85951
  var import_undici = __toESM(require_undici(), 1);
85732
85952
  var CLI_VERSION_HEADER = "x-powerplant-cli-version";
85733
- var cliVersion = true ? "0.4.11" : "0.0.0-checkout";
85953
+ var cliVersion = true ? "0.4.13" : "0.0.0-checkout";
85734
85954
  function cliVersionHeaders() {
85735
85955
  return { [CLI_VERSION_HEADER]: cliVersion, "user-agent": `powerplant-cli/${cliVersion}` };
85736
85956
  }
@@ -86037,7 +86257,7 @@ function bindDiagnosticEnrollment(diagnostic, token) {
86037
86257
  passEnrollments.set(diagnostic, token);
86038
86258
  }
86039
86259
 
86040
- // ../../apps/collector/src/codexCli.ts
86260
+ // ../../apps/collector/src/codexSessions.ts
86041
86261
  import { closeSync as closeSync3, mkdirSync as mkdirSync2, openSync as openSync3, renameSync, rmSync as rmSync2, utimesSync, writeSync } from "node:fs";
86042
86262
  import os6 from "node:os";
86043
86263
  import path5 from "node:path";
@@ -86477,13 +86697,17 @@ function isPathWithinScopes(resolved, scopePaths) {
86477
86697
  );
86478
86698
  }
86479
86699
 
86480
- // ../../apps/collector/src/codexCli.ts
86700
+ // ../../apps/collector/src/codexSessions.ts
86481
86701
  function parseCodexRollout(text, sharedUsage) {
86482
86702
  let meta3 = null;
86703
+ let initialEvent;
86704
+ let records = 0;
86483
86705
  let model;
86484
86706
  const timestamps = [];
86485
86707
  const usage = sharedUsage ?? createCodexUsageReader();
86486
86708
  for (const line of parseJsonl(text)) {
86709
+ records += 1;
86710
+ if (records === 2 && meta3 !== null) initialEvent = line;
86487
86711
  if (typeof line.timestamp === "string") timestamps.push(line.timestamp);
86488
86712
  usage.pushRecord(line);
86489
86713
  const payload = line.payload;
@@ -86499,8 +86723,10 @@ function parseCodexRollout(text, sharedUsage) {
86499
86723
  if (meta3 === null && timestamps.length === 0) return null;
86500
86724
  const git = meta3?.git ?? null;
86501
86725
  const sessionId = meta3?.id ?? meta3?.session_id;
86726
+ const facts = meta3 === null ? {} : harnessSessionFacts("codex", { ...meta3, initialEvent });
86502
86727
  return {
86503
- kind: meta3 !== null && isSidecarThread(meta3) ? "subagent" : "session",
86728
+ kind: facts.subagent ? "subagent" : "session",
86729
+ ...facts.standaloneReview && { standaloneReview: true },
86504
86730
  ...typeof sessionId === "string" && sessionId !== "" && { sessionId },
86505
86731
  startedAt: timestamps[0],
86506
86732
  endedAt: timestamps[timestamps.length - 1],
@@ -86536,6 +86762,7 @@ function foldRolloutPages(pages, usage) {
86536
86762
  const repoFullName = first("repoFullName");
86537
86763
  return {
86538
86764
  kind: pages[0].kind,
86765
+ ...pages[0].standaloneReview && { standaloneReview: true },
86539
86766
  ...sessionId !== void 0 && { sessionId },
86540
86767
  ...startedAt !== void 0 && { startedAt },
86541
86768
  ...endedAt !== void 0 && { endedAt },
@@ -86557,27 +86784,27 @@ function rolloutIdentity(rolloutPath) {
86557
86784
  if (!conventional) return { threadId: name, page: false };
86558
86785
  return { threadId: threadId2.toLowerCase(), page: pageId !== void 0 };
86559
86786
  }
86560
- function codexSpoolDir(home) {
86787
+ function codexMergedTranscriptsDir(home) {
86561
86788
  return path5.join(home, ".config", "powerplant", "codex");
86562
86789
  }
86563
- function mergedSpoolIsFresh(spoolPath, mtimeMs) {
86564
- const existing = statIfExists(spoolPath);
86790
+ function mergedTranscriptIsFresh(mergedTranscriptPath, mtimeMs) {
86791
+ const existing = statIfExists(mergedTranscriptPath);
86565
86792
  return existing !== null && Math.abs(existing.mtimeMs - mtimeMs) < 1;
86566
86793
  }
86567
- var MergedSpoolWriter = class {
86568
- constructor(spoolPath) {
86569
- this.spoolPath = spoolPath;
86570
- mkdirSync2(path5.dirname(spoolPath), { recursive: true, mode: 448 });
86571
- this.tmpPath = `${spoolPath}.${process.pid}.tmp`;
86794
+ var MergedTranscriptWriter = class {
86795
+ constructor(mergedTranscriptPath) {
86796
+ this.mergedTranscriptPath = mergedTranscriptPath;
86797
+ mkdirSync2(path5.dirname(mergedTranscriptPath), { recursive: true, mode: 448 });
86798
+ this.tmpPath = `${mergedTranscriptPath}.${process.pid}.tmp`;
86572
86799
  this.fd = openSync3(this.tmpPath, "w", 384);
86573
86800
  }
86574
- spoolPath;
86801
+ mergedTranscriptPath;
86575
86802
  tmpPath;
86576
86803
  fd;
86577
86804
  bytes = 0;
86578
- append(pageText) {
86805
+ append(pageText2) {
86579
86806
  if (this.fd === void 0) throw new Error("merged transcript already closed");
86580
- const text = pageText.endsWith("\n") ? pageText : `${pageText}
86807
+ const text = pageText2.endsWith("\n") ? pageText2 : `${pageText2}
86581
86808
  `;
86582
86809
  this.bytes += writeSync(this.fd, text);
86583
86810
  }
@@ -86585,7 +86812,7 @@ var MergedSpoolWriter = class {
86585
86812
  finish(mtimeMs) {
86586
86813
  this.close();
86587
86814
  utimesSync(this.tmpPath, mtimeMs / 1e3, mtimeMs / 1e3);
86588
- renameSync(this.tmpPath, this.spoolPath);
86815
+ renameSync(this.tmpPath, this.mergedTranscriptPath);
86589
86816
  return this.bytes;
86590
86817
  }
86591
86818
  abandon() {
@@ -86655,7 +86882,7 @@ async function collectCodexSessions(options = {}) {
86655
86882
  const now = options.now ?? Date.now();
86656
86883
  const capture = options.capture ?? captureEverythingUnresolved;
86657
86884
  const maxBytes = options.maxTranscriptBytes ?? MAX_TRANSCRIPT_FILE_BYTES;
86658
- const spoolDir = options.spoolDir ?? codexSpoolDir(options.homeDir ?? os6.homedir());
86885
+ const mergedTranscriptsDir = options.mergedTranscriptsDir ?? codexMergedTranscriptsDir(options.homeDir ?? os6.homedir());
86659
86886
  const items = [];
86660
86887
  const candidates = [];
86661
86888
  const unreadable = [];
@@ -86666,12 +86893,12 @@ async function collectCodexSessions(options = {}) {
86666
86893
  let largestMergedBytes = 0;
86667
86894
  const externalIdOf = (rolloutPath) => path5.basename(rolloutPath, ".jsonl");
86668
86895
  const files = /* @__PURE__ */ new Map();
86669
- const observe = async (paths, archived, hintSessionId) => {
86896
+ const observe = async (paths, archived, discoverySessionId) => {
86670
86897
  for (const rolloutPath of paths) {
86671
86898
  const known = files.get(rolloutPath);
86672
86899
  if (known !== void 0) {
86673
- if (hintSessionId === void 0) delete known.hintSessionIds;
86674
- else known.hintSessionIds?.push(hintSessionId);
86900
+ if (discoverySessionId === void 0) delete known.discoverySessionIds;
86901
+ else known.discoverySessionIds?.push(discoverySessionId);
86675
86902
  continue;
86676
86903
  }
86677
86904
  await yieldToEventLoop();
@@ -86684,15 +86911,15 @@ async function collectCodexSessions(options = {}) {
86684
86911
  mtimeMs: stat.mtimeMs,
86685
86912
  changedMs: Math.max(stat.mtimeMs, stat.ctimeMs),
86686
86913
  sizeBytes: stat.size,
86687
- ...hintSessionId !== void 0 && { hintSessionIds: [hintSessionId] }
86914
+ ...discoverySessionId !== void 0 && { discoverySessionIds: [discoverySessionId] }
86688
86915
  });
86689
86916
  }
86690
86917
  };
86691
86918
  const archiveDir = codexArchivedSessionsDir(sessionsDir);
86692
- const hintHome = options.homeDir ?? (options.codexDir === void 0 ? os6.homedir() : void 0);
86693
- if (hintHome !== void 0) {
86694
- for (const hint of readCodexTranscriptHints(hintHome, [sessionsDir, archiveDir])) {
86695
- await observe([hint.rolloutPath], hint.archived, hint.sessionId);
86919
+ const discoveryHome = options.homeDir ?? (options.codexDir === void 0 ? os6.homedir() : void 0);
86920
+ if (discoveryHome !== void 0) {
86921
+ for (const record2 of readCodexTranscriptDiscoveryRecords(discoveryHome, [sessionsDir, archiveDir])) {
86922
+ await observe([record2.rolloutPath], record2.archived, record2.sessionId);
86696
86923
  }
86697
86924
  }
86698
86925
  await observe(rolloutFiles(sessionsDir), false);
@@ -86707,17 +86934,17 @@ async function collectCodexSessions(options = {}) {
86707
86934
  const stored = dedupeStoreCopies(observed);
86708
86935
  let externalId = externalIdOf((stored.find((file2) => !file2.identity.page) ?? stored[0]).rolloutPath);
86709
86936
  try {
86710
- const spoolPath = stored.length > 1 ? path5.join(spoolDir, `${threadId2}.jsonl`) : void 0;
86937
+ const mergedTranscriptPath = stored.length > 1 ? path5.join(mergedTranscriptsDir, `${threadId2}.jsonl`) : void 0;
86711
86938
  const changedMs = Math.max(...stored.map((file2) => file2.changedMs));
86712
86939
  if (options.sinceMs !== void 0 && changedMs < options.sinceMs) {
86713
- const neverMerged = spoolPath !== void 0 && !options.locationsOnly && statIfExists(spoolPath) === null;
86940
+ const neverMerged = mergedTranscriptPath !== void 0 && !options.locationsOnly && statIfExists(mergedTranscriptPath) === null;
86714
86941
  if (!neverMerged) continue;
86715
86942
  }
86716
86943
  await yieldToEventLoop();
86717
86944
  const named = stored.filter((file2) => {
86718
- if (file2.hintSessionIds === void 0) return true;
86945
+ if (file2.discoverySessionIds === void 0) return true;
86719
86946
  const { sessionId } = rolloutLocation(file2.rolloutPath);
86720
- return sessionId !== void 0 && file2.hintSessionIds.includes(sessionId);
86947
+ return sessionId !== void 0 && file2.discoverySessionIds.includes(sessionId);
86721
86948
  });
86722
86949
  if (named.length === 0) continue;
86723
86950
  const threadFiles = named.length > 1 ? orderPages(named) : named;
@@ -86747,7 +86974,7 @@ async function collectCodexSessions(options = {}) {
86747
86974
  const thread = {
86748
86975
  externalId,
86749
86976
  files: threadFiles,
86750
- ...threadFiles.length > 1 && spoolPath !== void 0 && { spoolPath },
86977
+ ...threadFiles.length > 1 && mergedTranscriptPath !== void 0 && { mergedTranscriptPath },
86751
86978
  mtimeMs,
86752
86979
  archived: threadFiles.every((file2) => file2.archived)
86753
86980
  };
@@ -86797,8 +87024,8 @@ async function collectCodexSessions(options = {}) {
86797
87024
  const { thread, cwd, decision } = candidate;
86798
87025
  let writer;
86799
87026
  try {
86800
- const rewrite = thread.spoolPath !== void 0 && !mergedSpoolIsFresh(thread.spoolPath, thread.mtimeMs);
86801
- if (thread.spoolPath !== void 0 && rewrite) writer = new MergedSpoolWriter(thread.spoolPath);
87027
+ const rewrite = thread.mergedTranscriptPath !== void 0 && !mergedTranscriptIsFresh(thread.mergedTranscriptPath, thread.mtimeMs);
87028
+ if (thread.mergedTranscriptPath !== void 0 && rewrite) writer = new MergedTranscriptWriter(thread.mergedTranscriptPath);
86802
87029
  const pages = [];
86803
87030
  const usage = createCodexUsageReader();
86804
87031
  let vanished = false;
@@ -86819,11 +87046,11 @@ async function collectCodexSessions(options = {}) {
86819
87046
  continue;
86820
87047
  }
86821
87048
  const summary = foldRolloutPages(pages, usage.finish());
86822
- let transcriptPath = thread.files[0].rolloutPath;
86823
- if (thread.spoolPath !== void 0) {
86824
- const mergedBytes = writer ? writer.finish(thread.mtimeMs) : statIfExists(thread.spoolPath)?.size ?? 0;
87049
+ let transcriptUploadSourcePath = thread.files[0].rolloutPath;
87050
+ if (thread.mergedTranscriptPath !== void 0) {
87051
+ const mergedBytes = writer ? writer.finish(thread.mtimeMs) : statIfExists(thread.mergedTranscriptPath)?.size ?? 0;
86825
87052
  writer = void 0;
86826
- transcriptPath = thread.spoolPath;
87053
+ transcriptUploadSourcePath = thread.mergedTranscriptPath;
86827
87054
  mergedThreads += 1;
86828
87055
  largestMergedBytes = Math.max(largestMergedBytes, mergedBytes);
86829
87056
  }
@@ -86832,13 +87059,14 @@ async function collectCodexSessions(options = {}) {
86832
87059
  const title = summary.sessionId ? threadNames.get(summary.sessionId) : void 0;
86833
87060
  items.push({
86834
87061
  mtimeMs: thread.mtimeMs,
86835
- transcriptPath,
87062
+ transcriptPath: transcriptUploadSourcePath,
86836
87063
  ...cwd && { cwd },
86837
87064
  item: {
86838
87065
  source: "codex",
86839
87066
  externalId: thread.externalId,
86840
87067
  status: now - thread.mtimeMs < ACTIVE_GRACE_MS ? "active" : "inactive",
86841
87068
  archived: thread.archived,
87069
+ ...summary.standaloneReview && { automation: true },
86842
87070
  ...summary.startedAt && { startedAt: summary.startedAt },
86843
87071
  ...summary.endedAt && { endedAt: summary.endedAt },
86844
87072
  ...summary.model && { model: summary.model },
@@ -86999,7 +87227,7 @@ function defaultCursorUserDir(home) {
86999
87227
  function cursorGlobalStorageDir(home) {
87000
87228
  return path6.join(defaultCursorUserDir(home), "globalStorage");
87001
87229
  }
87002
- function cursorSpoolDir(home) {
87230
+ function cursorExtractedTranscriptsDir(home) {
87003
87231
  return path6.join(home, ".config", "powerplant", "cursor");
87004
87232
  }
87005
87233
  var emptyResult = () => ({
@@ -87055,7 +87283,7 @@ var cachedMetaSchema = external_exports.object({
87055
87283
  model: external_exports.string().optional(),
87056
87284
  ...cursorTranscriptMetaDisclosureSchema.shape
87057
87285
  });
87058
- function spoolFileName(composerId) {
87286
+ function extractedTranscriptFileName(composerId) {
87059
87287
  const sanitized = composerId.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 80);
87060
87288
  const hash2 = createHash2("sha256").update(composerId).digest("hex").slice(0, 8);
87061
87289
  return `${sanitized}-${hash2}.jsonl`;
@@ -87118,7 +87346,7 @@ function toolFilePathOf(args) {
87118
87346
  }
87119
87347
  return void 0;
87120
87348
  }
87121
- var SPOOL_META_RESERVE_BYTES = 64 * 1024;
87349
+ var EXTRACTED_TRANSCRIPT_META_RESERVE_BYTES = 64 * 1024;
87122
87350
  var BUBBLE_FIELDS_SHARED = ["_v", "type", "bubbleId", "serverBubbleId", "text", "createdAt"];
87123
87351
  var USER_BUBBLE_FIELDS = /* @__PURE__ */ new Set([...BUBBLE_FIELDS_SHARED, "context"]);
87124
87352
  var ASSISTANT_BUBBLE_FIELDS = /* @__PURE__ */ new Set([
@@ -87362,7 +87590,7 @@ function extractTranscript(db, composerId, maxBytes) {
87362
87590
  const spine = Array.isArray(data?.fullConversationHeadersOnly) ? data.fullConversationHeadersOnly : [];
87363
87591
  if (spine.length === 0) return null;
87364
87592
  const recorder = createOmissionRecorder();
87365
- const budget = maxBytes - SPOOL_META_RESERVE_BYTES;
87593
+ const budget = maxBytes - EXTRACTED_TRANSCRIPT_META_RESERVE_BYTES;
87366
87594
  const serialized = [];
87367
87595
  const modelCounts = /* @__PURE__ */ new Map();
87368
87596
  let bytes = 0;
@@ -87443,7 +87671,7 @@ async function collectCursorSessions(options = {}) {
87443
87671
  collectCursorTranscriptFiles(options, (id) => {
87444
87672
  if (knownIds.has(id)) return true;
87445
87673
  return db?.prepare("select composerId from composerHeaders where composerId = ?").get(id) != null;
87446
- }, result, options.spoolDir ?? cursorSpoolDir(options.homeDir ?? os7.homedir()), spoolFileName);
87674
+ }, result, options.extractedTranscriptsDir ?? cursorExtractedTranscriptsDir(options.homeDir ?? os7.homedir()), extractedTranscriptFileName);
87447
87675
  }
87448
87676
  } finally {
87449
87677
  try {
@@ -87457,9 +87685,9 @@ async function collectCursorSessions(options = {}) {
87457
87685
  function collectFromStore(db, userDir, options, result, knownIds) {
87458
87686
  const now = options.now ?? Date.now();
87459
87687
  const capture = options.capture ?? captureEverythingUnresolved;
87460
- const spoolDir = options.spoolDir ?? cursorSpoolDir(options.homeDir ?? os7.homedir());
87688
+ const extractedTranscriptsDir = options.extractedTranscriptsDir ?? cursorExtractedTranscriptsDir(options.homeDir ?? os7.homedir());
87461
87689
  const maxBytes = options.maxTranscriptBytes ?? MAX_TRANSCRIPT_FILE_BYTES;
87462
- let spoolDirReady = false;
87690
+ let extractedTranscriptsDirReady = false;
87463
87691
  const columns = new Set(
87464
87692
  db.prepare("pragma table_info(composerHeaders)").all().map((row) => isRecord2(row) ? row.name : void 0)
87465
87693
  );
@@ -87509,15 +87737,15 @@ function collectFromStore(db, userDir, options, result, knownIds) {
87509
87737
  });
87510
87738
  continue;
87511
87739
  }
87512
- const spoolPath = path6.join(spoolDir, spoolFileName(raw.composerId));
87513
- const spoolStat = statIfExists(spoolPath);
87740
+ const transcriptUploadSourcePath = path6.join(extractedTranscriptsDir, extractedTranscriptFileName(raw.composerId));
87741
+ const extractedTranscriptStat = statIfExists(transcriptUploadSourcePath);
87514
87742
  let model;
87515
- const sourceUnchanged = spoolStat !== null && Math.abs(spoolStat.mtimeMs - lastUpdatedAt) < 1;
87516
- if (sourceUnchanged && spoolStat.size > maxBytes) {
87743
+ const sourceUnchanged = extractedTranscriptStat !== null && Math.abs(extractedTranscriptStat.mtimeMs - lastUpdatedAt) < 1;
87744
+ if (sourceUnchanged && extractedTranscriptStat.size > maxBytes) {
87517
87745
  result.skippedTooLarge += 1;
87518
87746
  continue;
87519
87747
  }
87520
- const cached3 = sourceUnchanged ? readFirstRecord(spoolPath, () => true) : null;
87748
+ const cached3 = sourceUnchanged ? readFirstRecord(transcriptUploadSourcePath, () => true) : null;
87521
87749
  const cachedMeta = cachedMetaSchema.safeParse(cached3);
87522
87750
  const preparationCurrent = cachedMeta.success && cachedMeta.data.composerId === raw.composerId;
87523
87751
  let omissions;
@@ -87550,15 +87778,15 @@ function collectFromStore(db, userDir, options, result, knownIds) {
87550
87778
  ...counted && { omissions: transcript.omissions },
87551
87779
  ...unhandled.length > 0 && { unhandled }
87552
87780
  };
87553
- if (!spoolDirReady) {
87554
- mkdirSync3(spoolDir, { recursive: true, mode: 448 });
87555
- chmodSync2(spoolDir, 448);
87556
- spoolDirReady = true;
87781
+ if (!extractedTranscriptsDirReady) {
87782
+ mkdirSync3(extractedTranscriptsDir, { recursive: true, mode: 448 });
87783
+ chmodSync2(extractedTranscriptsDir, 448);
87784
+ extractedTranscriptsDirReady = true;
87557
87785
  }
87558
87786
  const body = [JSON.stringify(meta3), ...transcript.serialized, ""].join("\n");
87559
- if (spoolStat !== null) chmodSync2(spoolPath, 384);
87560
- writeFileSync2(spoolPath, body, { encoding: "utf8", mode: 384 });
87561
- utimesSync2(spoolPath, new Date(lastUpdatedAt), new Date(lastUpdatedAt));
87787
+ if (extractedTranscriptStat !== null) chmodSync2(transcriptUploadSourcePath, 384);
87788
+ writeFileSync2(transcriptUploadSourcePath, body, { encoding: "utf8", mode: 384 });
87789
+ utimesSync2(transcriptUploadSourcePath, new Date(lastUpdatedAt), new Date(lastUpdatedAt));
87562
87790
  }
87563
87791
  if (hasCaptureGaps(omissions)) result.partiallyInterpreted += 1;
87564
87792
  for (const observation of unhandledObservations(unhandled, raw.composerId)) {
@@ -87577,7 +87805,7 @@ function collectFromStore(db, userDir, options, result, knownIds) {
87577
87805
  const startBranch = facts.branches[0];
87578
87806
  result.sessions.push({
87579
87807
  mtimeMs: lastUpdatedAt,
87580
- transcriptPath: spoolPath,
87808
+ transcriptPath: transcriptUploadSourcePath,
87581
87809
  ...cwd && { cwd },
87582
87810
  item: {
87583
87811
  source: "cursor",
@@ -87711,7 +87939,7 @@ function parseCursorTranscriptFile(text) {
87711
87939
  if (!lines.some((line) => CONTENT_LINE_TYPES2.has(line.type))) return null;
87712
87940
  return { lines, omissions: recorder.omissions(), unhandled: recorder.unhandled() };
87713
87941
  }
87714
- function spoolBody(metaFor, lines, maxBytes) {
87942
+ function extractedTranscriptBody(metaFor, lines, maxBytes) {
87715
87943
  for (const keepPayloads of [true, false]) {
87716
87944
  const payloadsDropped = keepPayloads ? 0 : lines.filter((line) => line.type === "unsupported" && line.payload !== void 0).length;
87717
87945
  const records = [
@@ -87730,7 +87958,7 @@ function spoolBody(metaFor, lines, maxBytes) {
87730
87958
  }
87731
87959
  return null;
87732
87960
  }
87733
- var spoolMetaSchema = external_exports.object({
87961
+ var extractedTranscriptMetaSchema = external_exports.object({
87734
87962
  type: external_exports.literal("meta"),
87735
87963
  format: external_exports.literal("cursor-local-transcript"),
87736
87964
  version: external_exports.literal(1),
@@ -87742,7 +87970,7 @@ var spoolMetaSchema = external_exports.object({
87742
87970
  model: external_exports.string().optional(),
87743
87971
  ...cursorTranscriptMetaDisclosureSchema.shape
87744
87972
  });
87745
- var hintSchema2 = external_exports.object({
87973
+ var transcriptDiscoveryRecordSchema2 = external_exports.object({
87746
87974
  sessionId: external_exports.string().regex(/^[a-zA-Z0-9_-]{1,200}$/),
87747
87975
  transcriptPath: external_exports.string().max(4096).nullable(),
87748
87976
  workspaceRoots: external_exports.array(external_exports.string().max(4096)).max(8),
@@ -87774,15 +88002,15 @@ function cursorTranscriptRecordPath(home, sessionId) {
87774
88002
  `${createHash3("sha256").update(sessionId).digest("hex")}.json`
87775
88003
  );
87776
88004
  }
87777
- function collectCursorTranscriptFiles(options, isDatabaseSession, result, databaseSpoolDir, spoolName) {
88005
+ function collectCursorTranscriptFiles(options, isDatabaseSession, result, databaseExtractedTranscriptsDir, extractedTranscriptFileName2) {
87778
88006
  const home = options.homeDir ?? os8.homedir();
87779
- const hintsDir = cursorTranscriptRecordsDir(home);
87780
- if (!existsSync3(hintsDir)) return;
88007
+ const recordsDir = cursorTranscriptRecordsDir(home);
88008
+ if (!existsSync3(recordsDir)) return;
87781
88009
  const maxBytes = options.maxTranscriptBytes ?? MAX_TRANSCRIPT_FILE_BYTES;
87782
- const spoolDir = path7.join(databaseSpoolDir, "native");
88010
+ const nativeExtractedTranscriptsDir = path7.join(databaseExtractedTranscriptsDir, "native");
87783
88011
  let files;
87784
88012
  try {
87785
- files = readdirSync3(hintsDir);
88013
+ files = readdirSync3(recordsDir);
87786
88014
  } catch {
87787
88015
  result.skippedUnreadable++;
87788
88016
  return;
@@ -87790,15 +88018,15 @@ function collectCursorTranscriptFiles(options, isDatabaseSession, result, databa
87790
88018
  for (const file2 of files) {
87791
88019
  if (!/^[a-f0-9]{64}\.json$/.test(file2)) continue;
87792
88020
  try {
87793
- const hintPath = path7.join(hintsDir, file2);
87794
- if (!lstatSync2(hintPath).isFile()) continue;
87795
- const text = readBounded(hintPath, 40 * 1024);
87796
- const hint = hintSchema2.parse(text === null ? null : JSON.parse(text));
87797
- if (file2 !== `${createHash3("sha256").update(hint.sessionId).digest("hex")}.json`) continue;
87798
- if (isDatabaseSession(hint.sessionId) || existsSync3(path7.join(databaseSpoolDir, spoolName(hint.sessionId))))
88021
+ const recordPath = path7.join(recordsDir, file2);
88022
+ if (!lstatSync2(recordPath).isFile()) continue;
88023
+ const text = readBounded(recordPath, 40 * 1024);
88024
+ const record2 = transcriptDiscoveryRecordSchema2.parse(text === null ? null : JSON.parse(text));
88025
+ if (file2 !== `${createHash3("sha256").update(record2.sessionId).digest("hex")}.json`) continue;
88026
+ if (isDatabaseSession(record2.sessionId) || existsSync3(path7.join(databaseExtractedTranscriptsDir, extractedTranscriptFileName2(record2.sessionId))))
87799
88027
  continue;
87800
- if (hint.transcriptPath === null || hint.workspaceRoots.length !== 1) continue;
87801
- const cwd = hint.workspaceRoots[0];
88028
+ if (record2.transcriptPath === null || record2.workspaceRoots.length !== 1) continue;
88029
+ const cwd = record2.workspaceRoots[0];
87802
88030
  if (!path7.isAbsolute(cwd)) continue;
87803
88031
  const decision = captureDecision(cwd, options.capture ?? captureEverythingUnresolved);
87804
88032
  if (!decision.capture) {
@@ -87808,14 +88036,14 @@ function collectCursorTranscriptFiles(options, isDatabaseSession, result, databa
87808
88036
  const projects = path7.join(home, ".cursor", "projects");
87809
88037
  const project = cwd.replace(/[^a-zA-Z0-9]/g, "-").replace(/^-+/, "");
87810
88038
  const base = path7.join(projects, project, "agent-transcripts");
87811
- const basename2 = `${hint.sessionId}.jsonl`;
87812
- const allowed = [path7.join(base, basename2), path7.join(base, hint.sessionId, basename2)];
87813
- if (!allowed.includes(hint.transcriptPath)) {
88039
+ const basename2 = `${record2.sessionId}.jsonl`;
88040
+ const allowed = [path7.join(base, basename2), path7.join(base, record2.sessionId, basename2)];
88041
+ if (!allowed.includes(record2.transcriptPath)) {
87814
88042
  result.skippedUnreadable++;
87815
88043
  continue;
87816
88044
  }
87817
- const stat = lstatSync2(hint.transcriptPath);
87818
- if (!stat.isFile() || realpathSync2(hint.transcriptPath) !== path7.join(realpathSync2(projects), path7.relative(projects, hint.transcriptPath))) {
88045
+ const stat = lstatSync2(record2.transcriptPath);
88046
+ if (!stat.isFile() || realpathSync2(record2.transcriptPath) !== path7.join(realpathSync2(projects), path7.relative(projects, record2.transcriptPath))) {
87819
88047
  result.skippedUnreadable++;
87820
88048
  continue;
87821
88049
  }
@@ -87826,7 +88054,7 @@ function collectCursorTranscriptFiles(options, isDatabaseSession, result, databa
87826
88054
  }
87827
88055
  const common = {
87828
88056
  source: "cursor",
87829
- externalId: hint.sessionId,
88057
+ externalId: record2.sessionId,
87830
88058
  status: (options.now ?? Date.now()) - stat.mtimeMs < ACTIVE_GRACE_MS ? "active" : "inactive",
87831
88059
  ...decision.repoFullName && { repoFullName: decision.repoFullName },
87832
88060
  capturedVia: decision.via
@@ -87835,15 +88063,15 @@ function collectCursorTranscriptFiles(options, isDatabaseSession, result, databa
87835
88063
  result.sessions.push({
87836
88064
  cwd,
87837
88065
  mtimeMs: stat.mtimeMs,
87838
- transcriptPath: hint.transcriptPath,
88066
+ transcriptPath: record2.transcriptPath,
87839
88067
  item: common
87840
88068
  });
87841
88069
  continue;
87842
88070
  }
87843
- const spool = path7.join(spoolDir, spoolName(hint.sessionId));
87844
- const spoolStat = statIfExists(spool);
87845
- const cached3 = spoolStat !== null && spoolStat.size <= maxBytes && Math.abs(spoolStat.mtimeMs - stat.mtimeMs) < 1 ? spoolMetaSchema.safeParse(readFirstRecord(spool, () => true)) : null;
87846
- const fresh = cached3?.success && cached3.data.composerId === hint.sessionId && cached3.data.workspacePath === cwd && cached3.data.nativeSourceSize === stat.size && cached3.data.model === hint.model;
88071
+ const transcriptUploadSourcePath = path7.join(nativeExtractedTranscriptsDir, extractedTranscriptFileName2(record2.sessionId));
88072
+ const extractedTranscriptStat = statIfExists(transcriptUploadSourcePath);
88073
+ const cached3 = extractedTranscriptStat !== null && extractedTranscriptStat.size <= maxBytes && Math.abs(extractedTranscriptStat.mtimeMs - stat.mtimeMs) < 1 ? extractedTranscriptMetaSchema.safeParse(readFirstRecord(transcriptUploadSourcePath, () => true)) : null;
88074
+ const fresh = cached3?.success && cached3.data.composerId === record2.sessionId && cached3.data.workspacePath === cwd && cached3.data.nativeSourceSize === stat.size && cached3.data.model === record2.model;
87847
88075
  let title;
87848
88076
  let omissions;
87849
88077
  let unhandled = [];
@@ -87852,7 +88080,7 @@ function collectCursorTranscriptFiles(options, isDatabaseSession, result, databa
87852
88080
  omissions = cached3.data.omissions;
87853
88081
  unhandled = cached3.data.unhandled ?? [];
87854
88082
  } else {
87855
- const raw = readBounded(hint.transcriptPath, maxBytes);
88083
+ const raw = readBounded(record2.transcriptPath, maxBytes);
87856
88084
  if (raw === null) {
87857
88085
  result.skippedTooLarge++;
87858
88086
  continue;
@@ -87873,40 +88101,40 @@ function collectCursorTranscriptFiles(options, isDatabaseSession, result, databa
87873
88101
  format: "cursor-local-transcript",
87874
88102
  version: 1,
87875
88103
  preparationVersion: CURSOR_TRANSCRIPT_PREPARATION_VERSION,
87876
- composerId: hint.sessionId,
88104
+ composerId: record2.sessionId,
87877
88105
  workspacePath: cwd,
87878
88106
  nativeSourceSize: stat.size,
87879
88107
  ...title !== void 0 && { name: title },
87880
- ...hint.model && { model: hint.model },
88108
+ ...record2.model && { model: record2.model },
87881
88109
  ...Object.keys(written).length > 0 && { omissions: written },
87882
88110
  ...prepared.unhandled.length > 0 && { unhandled: prepared.unhandled }
87883
88111
  };
87884
88112
  };
87885
- const body = spoolBody(metaFor, prepared.lines, maxBytes);
88113
+ const body = extractedTranscriptBody(metaFor, prepared.lines, maxBytes);
87886
88114
  if (body === null) {
87887
88115
  result.skippedTooLarge++;
87888
88116
  continue;
87889
88117
  }
87890
- mkdirSync4(spoolDir, { recursive: true, mode: 448 });
87891
- if (spoolStat !== null) chmodSync3(spool, 384);
87892
- writeFileSync3(spool, body, { mode: 384 });
87893
- utimesSync3(spool, stat.atime, stat.mtime);
88118
+ mkdirSync4(nativeExtractedTranscriptsDir, { recursive: true, mode: 448 });
88119
+ if (extractedTranscriptStat !== null) chmodSync3(transcriptUploadSourcePath, 384);
88120
+ writeFileSync3(transcriptUploadSourcePath, body, { mode: 384 });
88121
+ utimesSync3(transcriptUploadSourcePath, stat.atime, stat.mtime);
87894
88122
  omissions = Object.keys(written).length > 0 ? written : void 0;
87895
88123
  unhandled = prepared.unhandled;
87896
88124
  }
87897
88125
  if (hasCaptureGaps(omissions)) result.partiallyInterpreted++;
87898
- for (const observation of unhandledObservations(unhandled, hint.sessionId)) {
88126
+ for (const observation of unhandledObservations(unhandled, record2.sessionId)) {
87899
88127
  observeStructure(options.onStructureDiagnostic, observation);
87900
88128
  }
87901
88129
  result.sessions.push({
87902
88130
  cwd,
87903
88131
  mtimeMs: stat.mtimeMs,
87904
- transcriptPath: spool,
88132
+ transcriptPath: transcriptUploadSourcePath,
87905
88133
  item: {
87906
88134
  ...common,
87907
88135
  endedAt: stat.mtime.toISOString(),
87908
88136
  repoDir: path7.basename(cwd),
87909
- ...hint.model && { model: hint.model },
88137
+ ...record2.model && { model: record2.model },
87910
88138
  ...title !== void 0 && { title }
87911
88139
  }
87912
88140
  });
@@ -88070,7 +88298,7 @@ function observedSessionEnd(entry, mtimeMs) {
88070
88298
  return entry.at;
88071
88299
  }
88072
88300
 
88073
- // ../../apps/collector/src/providerHooks.ts
88301
+ // ../../apps/collector/src/harnessHooks.ts
88074
88302
  import {
88075
88303
  existsSync as existsSync5,
88076
88304
  lstatSync as lstatSync3,
@@ -88086,26 +88314,39 @@ var SESSION_CHANGE_HOOK = "powerplant-session-change.mjs";
88086
88314
  var CODEX_TRANSCRIPT_HOOK = "powerplant-codex-transcript.mjs";
88087
88315
  var CURSOR_TRANSCRIPT_HOOK = "powerplant-cursor-transcript.mjs";
88088
88316
  var SESSION_CHANGE_FILENAME = "session-change";
88317
+ var SUPPORTED_HOOK_HARNESSES = ["codex-local", "cursor-local"];
88089
88318
  var jsonObject = external_exports.record(external_exports.string(), external_exports.json());
88090
- function providerEvents(provider) {
88091
- return provider === "cursor" ? ["sessionStart", "sessionEnd", "stop"] : ["SessionStart", "SessionEnd", "Stop"];
88319
+ function nativeHarnessName(harness) {
88320
+ switch (harness) {
88321
+ case "codex-local":
88322
+ return "codex";
88323
+ case "cursor-local":
88324
+ return "cursor";
88325
+ default:
88326
+ throw new Error(`unsupported hook harness: ${String(harness)}`);
88327
+ }
88328
+ }
88329
+ function harnessEvents(harness) {
88330
+ return nativeHarnessName(harness) === "cursor" ? ["sessionStart", "sessionEnd", "stop"] : ["SessionStart", "SessionEnd", "Stop"];
88092
88331
  }
88093
- function providerConfigPath(provider, home) {
88094
- return path10.join(home, `.${provider}`, "hooks.json");
88332
+ function harnessHookConfigPath(harness, home) {
88333
+ const nativeName = nativeHarnessName(harness);
88334
+ return path10.join(home, `.${nativeName}`, "hooks.json");
88095
88335
  }
88096
88336
  var UNUSABLE_CONFIG = "hooks.json contains invalid JSON or an unsupported hook structure";
88097
88337
  var NON_REGULAR_CONFIG = "hooks.json is not a regular file; configure the hook manually";
88098
- function providerHookCommands(provider, text) {
88338
+ function harnessHookCommands(harness, text) {
88099
88339
  try {
88340
+ const nativeName = nativeHarnessName(harness);
88100
88341
  const settings = jsonObject.parse(text === null ? {} : JSON.parse(text));
88101
- if (provider === "cursor" && settings.version !== void 0 && settings.version !== 1) {
88342
+ if (nativeName === "cursor" && settings.version !== void 0 && settings.version !== 1) {
88102
88343
  return { kind: "unusable", reason: "Unsupported Cursor hooks version" };
88103
88344
  }
88104
88345
  const hooks = jsonObject.parse(settings.hooks === void 0 ? {} : settings.hooks);
88105
88346
  const commands = [];
88106
- for (const event of providerEvents(provider)) {
88347
+ for (const event of harnessEvents(harness)) {
88107
88348
  const entries = external_exports.array(jsonObject).parse(hooks[event] === void 0 ? [] : hooks[event]);
88108
- const handlers2 = provider === "cursor" ? entries : entries.flatMap((entry) => external_exports.array(jsonObject).parse(entry.hooks));
88349
+ const handlers2 = nativeName === "cursor" ? entries : entries.flatMap((entry) => external_exports.array(jsonObject).parse(entry.hooks));
88109
88350
  const ours = handlers2.filter(isOurHook);
88110
88351
  if (ours.length === 0) return { kind: "missing" };
88111
88352
  for (const hook of ours) {
@@ -88121,20 +88362,21 @@ function isOurHook(hook) {
88121
88362
  const command = hook.command;
88122
88363
  return typeof command === "string" && [SESSION_CHANGE_HOOK, CODEX_TRANSCRIPT_HOOK, CURSOR_TRANSCRIPT_HOOK].some((script) => command.endsWith(`/${script}'`) || command.endsWith(`/${script}"`));
88123
88364
  }
88124
- function mergeProviderHooks(provider, text, command) {
88365
+ function mergeHarnessHooks(harness, text, command) {
88125
88366
  try {
88367
+ const nativeName = nativeHarnessName(harness);
88126
88368
  const settings = jsonObject.parse(text === null ? {} : JSON.parse(text));
88127
- if (provider === "cursor" && settings.version !== void 0 && settings.version !== 1) {
88369
+ if (nativeName === "cursor" && settings.version !== void 0 && settings.version !== 1) {
88128
88370
  return { action: "skip", reason: "Unsupported Cursor hooks version" };
88129
88371
  }
88130
88372
  const hooks = jsonObject.parse(settings.hooks === void 0 ? {} : settings.hooks);
88131
- for (const event of providerEvents(provider)) {
88373
+ for (const event of harnessEvents(harness)) {
88132
88374
  const entries = external_exports.array(jsonObject).parse(hooks[event] === void 0 ? [] : hooks[event]);
88133
- const handler = provider === "cursor" ? { command, timeout: 2 } : { type: "command", command, timeout: 2 };
88134
- const ours = provider === "cursor" ? handler : { hooks: [handler] };
88375
+ const handler = nativeName === "cursor" ? { command, timeout: 2 } : { type: "command", command, timeout: 2 };
88376
+ const ours = nativeName === "cursor" ? handler : { hooks: [handler] };
88135
88377
  const kept = [];
88136
88378
  for (const entry of entries) {
88137
- if (provider === "cursor") {
88379
+ if (nativeName === "cursor") {
88138
88380
  if (!isOurHook(entry)) kept.push(entry);
88139
88381
  continue;
88140
88382
  }
@@ -88145,7 +88387,7 @@ function mergeProviderHooks(provider, text, command) {
88145
88387
  }
88146
88388
  hooks[event] = [...kept, ours];
88147
88389
  }
88148
- const next = { ...settings, ...provider === "cursor" ? { version: 1 } : {}, hooks };
88390
+ const next = { ...settings, ...nativeName === "cursor" ? { version: 1 } : {}, hooks };
88149
88391
  if (JSON.stringify(next) === JSON.stringify(settings)) return { action: "unchanged" };
88150
88392
  return { action: "write", text: `${JSON.stringify(next, null, 2)}
88151
88393
  ` };
@@ -88156,42 +88398,47 @@ function mergeProviderHooks(provider, text, command) {
88156
88398
  function shellQuote2(value) {
88157
88399
  return `'${value.replaceAll("'", "'\\''")}'`;
88158
88400
  }
88159
- function installProviderHook(provider, home) {
88160
- const configDir = path10.join(home, `.${provider}`);
88161
- if (!existsSync5(configDir)) return { provider, settings: "absent" };
88162
- const configPath = providerConfigPath(provider, home);
88401
+ function installHarnessHook(harness, home) {
88402
+ const nativeName = nativeHarnessName(harness);
88403
+ const configDir = path10.join(home, `.${nativeName}`);
88404
+ if (!existsSync5(configDir)) return { provider: nativeName, settings: "absent" };
88405
+ const configPath = harnessHookConfigPath(harness, home);
88163
88406
  let original = null;
88164
88407
  let mode = 384;
88165
88408
  try {
88166
88409
  const stat = lstatSync3(configPath);
88167
- if (!stat.isFile()) return { provider, settings: "skip", detail: NON_REGULAR_CONFIG };
88410
+ if (!stat.isFile()) {
88411
+ return { provider: nativeName, settings: "skip", detail: NON_REGULAR_CONFIG };
88412
+ }
88168
88413
  mode = stat.mode & 511;
88169
88414
  original = readFileSync6(configPath, "utf8");
88170
88415
  } catch (error63) {
88171
88416
  if (!(error63 instanceof Error && "code" in error63 && error63.code === "ENOENT")) throw error63;
88172
88417
  }
88173
88418
  const hookDir = path10.join(home, ".config", "powerplant", "hooks");
88174
- const script = provider === "cursor" ? CURSOR_TRANSCRIPT_HOOK : CODEX_TRANSCRIPT_HOOK;
88419
+ const script = nativeName === "cursor" ? CURSOR_TRANSCRIPT_HOOK : CODEX_TRANSCRIPT_HOOK;
88175
88420
  const scriptPath = path10.join(hookDir, script);
88176
- const merged = mergeProviderHooks(
88177
- provider,
88421
+ const merged = mergeHarnessHooks(
88422
+ harness,
88178
88423
  original,
88179
88424
  `${shellQuote2(process.execPath)} ${shellQuote2(scriptPath)}`
88180
88425
  );
88181
- if (merged.action === "skip") return { provider, settings: "skip", detail: merged.reason };
88426
+ if (merged.action === "skip") {
88427
+ return { provider: nativeName, settings: "skip", detail: merged.reason };
88428
+ }
88182
88429
  mkdirSync6(hookDir, { recursive: true });
88183
88430
  writeAtomic(scriptPath, readFileSync6(hookSourcePath(script), "utf8"), 448);
88184
88431
  if (merged.action === "write") {
88185
88432
  const current = existsSync5(configPath) ? readFileSync6(configPath, "utf8") : null;
88186
88433
  if (current !== original)
88187
88434
  return {
88188
- provider,
88435
+ provider: nativeName,
88189
88436
  settings: "skip",
88190
88437
  detail: "hooks.json changed during installation; restart the collector to retry"
88191
88438
  };
88192
88439
  writeAtomic(configPath, merged.text, mode);
88193
88440
  }
88194
- return { provider, settings: merged.action };
88441
+ return { provider: nativeName, settings: merged.action };
88195
88442
  }
88196
88443
  function writeAtomic(file2, text, mode) {
88197
88444
  const temporary = `${file2}.${randomUUID()}.tmp`;
@@ -88202,13 +88449,13 @@ function writeAtomic(file2, text, mode) {
88202
88449
  rmSync4(temporary, { force: true });
88203
88450
  }
88204
88451
  }
88205
- function installProviderHooks(home) {
88206
- return ["codex", "cursor"].map((provider) => {
88452
+ function installHarnessHooks(home) {
88453
+ return SUPPORTED_HOOK_HARNESSES.map((harness) => {
88207
88454
  try {
88208
- return installProviderHook(provider, home);
88455
+ return installHarnessHook(harness, home);
88209
88456
  } catch (error63) {
88210
88457
  return {
88211
- provider,
88458
+ provider: nativeHarnessName(harness),
88212
88459
  settings: "skip",
88213
88460
  detail: error63 instanceof Error ? error63.message : String(error63)
88214
88461
  };
@@ -88220,7 +88467,7 @@ function installProviderHooks(home) {
88220
88467
  var HOOK_OBSERVATION_GRACE_MS = 2 * 6e4;
88221
88468
  var hookHealthSchema = external_exports.object({
88222
88469
  harness: hookHarnessSchema,
88223
- /** What this daemon's boot-time install did (installCollectorHooks / installProviderHooks). */
88470
+ /** What this daemon's boot-time install did (installCollectorHooks / installHarnessHooks). */
88224
88471
  install: hookInstallOutcomeSchema,
88225
88472
  /** unverified: no in-scope session started since the install has been
88226
88473
  * judged yet. firing: one carried a hook record. silent: one had none. */
@@ -88249,7 +88496,10 @@ var HARNESS_NAMES = {
88249
88496
  cursor: "Cursor"
88250
88497
  };
88251
88498
  function configPathOf(harness, home) {
88252
- return harness === "claude" ? claudeSettingsPath(home) : providerConfigPath(harness, home);
88499
+ return harness === "claude" ? claudeSettingsPath(home) : harnessHookConfigPath(localHookHarness(harness), home);
88500
+ }
88501
+ function localHookHarness(harness) {
88502
+ return harness === "codex" ? "codex-local" : "cursor-local";
88253
88503
  }
88254
88504
  function commandPaths(command) {
88255
88505
  const paths = [];
@@ -88284,7 +88534,7 @@ function inspectHookConfig(harness, home) {
88284
88534
  detail: error63 instanceof Error ? error63.message : String(error63)
88285
88535
  };
88286
88536
  }
88287
- const read = harness === "claude" ? collectorHookCommands(text) : providerHookCommands(harness, text);
88537
+ const read = harness === "claude" ? collectorHookCommands(text) : harnessHookCommands(localHookHarness(harness), text);
88288
88538
  if (read.kind === "unusable") return { kind: "config-unusable", configPath, detail: read.reason };
88289
88539
  if (read.kind === "missing") return { kind: "entries-missing", configPath };
88290
88540
  for (const command of read.commands) {
@@ -89186,7 +89436,7 @@ async function uploadPreviewRegistration(apiUrl, machineToken, registration) {
89186
89436
  function encodeRawTranscript(text) {
89187
89437
  return gzipSync(redactJsonl(text));
89188
89438
  }
89189
- async function uploadRawSessions(apiUrl, machineToken, collected, readTranscript = (p) => readFileSync9(p, "utf8"), shipped, progress) {
89439
+ async function uploadRawSessions(apiUrl, machineToken, transcriptUploadSources, readTranscript = (p) => readFileSync9(p, "utf8"), shipped, progress) {
89190
89440
  const summary = {
89191
89441
  uploaded: 0,
89192
89442
  skippedTooLarge: 0,
@@ -89197,18 +89447,19 @@ async function uploadRawSessions(apiUrl, machineToken, collected, readTranscript
89197
89447
  };
89198
89448
  const initiallyShipped = new Map(shipped);
89199
89449
  let rateLimited;
89200
- for (const session of collected) {
89450
+ for (const session of transcriptUploadSources) {
89201
89451
  summary.uploaded += await progress?.beforeUpload?.() ?? 0;
89202
- const acknowledged = shipped?.get(session.transcriptPath);
89203
- if (acknowledged === session.mtimeMs || acknowledged !== void 0 && acknowledged !== initiallyShipped.get(session.transcriptPath)) {
89452
+ const transcriptUploadSourcePath = session.transcriptPath;
89453
+ const acknowledged = shipped?.get(transcriptUploadSourcePath);
89454
+ if (acknowledged === session.mtimeMs || acknowledged !== void 0 && acknowledged !== initiallyShipped.get(transcriptUploadSourcePath)) {
89204
89455
  summary.skippedUnchanged += 1;
89205
89456
  continue;
89206
89457
  }
89207
89458
  try {
89208
- const body = encodeRawTranscript(readTranscript(session.transcriptPath));
89459
+ const body = encodeRawTranscript(readTranscript(transcriptUploadSourcePath));
89209
89460
  if (body.byteLength > MAX_RAW_UPLOAD_BYTES) {
89210
89461
  summary.skippedTooLarge += 1;
89211
- shipped?.set(session.transcriptPath, session.mtimeMs);
89462
+ shipped?.set(transcriptUploadSourcePath, session.mtimeMs);
89212
89463
  continue;
89213
89464
  }
89214
89465
  const query = `source=${session.item.source}&externalId=${encodeURIComponent(session.item.externalId)}`;
@@ -89241,7 +89492,7 @@ async function uploadRawSessions(apiUrl, machineToken, collected, readTranscript
89241
89492
  }
89242
89493
  summary.uploaded += 1;
89243
89494
  progress?.onUploaded?.();
89244
- shipped?.set(session.transcriptPath, session.mtimeMs);
89495
+ shipped?.set(transcriptUploadSourcePath, session.mtimeMs);
89245
89496
  } catch (error63) {
89246
89497
  if (error63 instanceof RateLimitedError) rateLimited ??= error63;
89247
89498
  summary.failed.push(
@@ -91112,7 +91363,7 @@ function startWatch(config2, homeDir, runtime = { version: "checkout" }) {
91112
91363
  );
91113
91364
  reportCollectorError(error63, "watch-hook-install");
91114
91365
  }
91115
- for (const hook of installProviderHooks(home)) {
91366
+ for (const hook of installHarnessHooks(home)) {
91116
91367
  hookInstalls[hook.provider] = hook.settings;
91117
91368
  if (hook.settings === "absent") continue;
91118
91369
  if (hook.settings === "skip") {
@@ -91861,7 +92112,7 @@ function belowVersionFloor(client, versionText) {
91861
92112
  }
91862
92113
 
91863
92114
  // src/connect.ts
91864
- var MCP_SERVER_NAME = "powerplant";
92115
+ var mcpServerNameFor = (appEnv) => appEnv === "production" ? "powerplant" : `powerplant-${appEnv}`;
91865
92116
  var AGENT_DISPLAY_NAMES = {
91866
92117
  claude: "Claude Code",
91867
92118
  codex: "Codex",
@@ -92298,7 +92549,7 @@ function realCheckboxPrompt(question, choices, seams = {}) {
92298
92549
  function claudeAddArgs(deps) {
92299
92550
  const official = deps.officialClients?.["claude-code"];
92300
92551
  const credentialArgs = official ? ["--client-id", official.clientId, "--callback-port", String(official.callbackPort)] : [];
92301
- return ["mcp", "add", "--transport", "http", "--scope", "user", ...credentialArgs, MCP_SERVER_NAME, deps.mcpUrl];
92552
+ return ["mcp", "add", "--transport", "http", "--scope", "user", ...credentialArgs, deps.serverName, deps.mcpUrl];
92302
92553
  }
92303
92554
  function parseClaudeProbe(result) {
92304
92555
  const output2 = result.stdout + result.stderr;
@@ -92319,7 +92570,7 @@ function claudeEnv(client) {
92319
92570
  function connectClaude(client, name, deps) {
92320
92571
  const { bin } = client;
92321
92572
  const env = claudeEnv(client);
92322
- const probe = parseClaudeProbe(deps.run(bin, ["mcp", "get", MCP_SERVER_NAME], env));
92573
+ const probe = parseClaudeProbe(deps.run(bin, ["mcp", "get", deps.serverName], env));
92323
92574
  if (probe.broken) {
92324
92575
  return failed(`probing for the Powerplant MCP failed, ${probe.detail}`);
92325
92576
  }
@@ -92327,7 +92578,7 @@ function connectClaude(client, name, deps) {
92327
92578
  if (missing || registeredUrl !== void 0 && registeredUrl !== deps.mcpUrl) {
92328
92579
  if (!missing) {
92329
92580
  deps.log(`${name}: registered against ${registeredUrl}, re-registering for ${deps.mcpUrl}.`);
92330
- deps.run(bin, ["mcp", "remove", "--scope", "user", MCP_SERVER_NAME], env);
92581
+ deps.run(bin, ["mcp", "remove", "--scope", "user", deps.serverName], env);
92331
92582
  }
92332
92583
  const add = deps.run(bin, claudeAddArgs(deps), env);
92333
92584
  if (add.status !== 0) {
@@ -92339,7 +92590,7 @@ function connectClaude(client, name, deps) {
92339
92590
  return { state: "connected", unchanged: true };
92340
92591
  } else if (deps.officialClients?.["claude-code"]) {
92341
92592
  deps.log(`${name}: re-registering with the pre-registered client before sign-in.`);
92342
- deps.run(bin, ["mcp", "remove", "--scope", "user", MCP_SERVER_NAME], env);
92593
+ deps.run(bin, ["mcp", "remove", "--scope", "user", deps.serverName], env);
92343
92594
  const add = deps.run(bin, claudeAddArgs(deps), env);
92344
92595
  if (add.status !== 0) {
92345
92596
  return failed(`registering the Powerplant MCP failed, ${describeFailure(add)}`);
@@ -92347,7 +92598,7 @@ function connectClaude(client, name, deps) {
92347
92598
  } else {
92348
92599
  deps.log(`${name}: Powerplant MCP already registered.`);
92349
92600
  }
92350
- return login(bin, name, ["mcp", "login", MCP_SERVER_NAME], deps, env);
92601
+ return login(bin, name, ["mcp", "login", deps.serverName], deps, env);
92351
92602
  }
92352
92603
  function codexOauthHeaderPattern(serverName) {
92353
92604
  return new RegExp(
@@ -92514,7 +92765,7 @@ function parseCodexServers(stdout) {
92514
92765
  }
92515
92766
  }
92516
92767
  var CODEX_AUTHENTICATED_STATUSES = /* @__PURE__ */ new Set(["o_auth", "bearer_token"]);
92517
- function parseCodexProbe(result) {
92768
+ function parseCodexProbe(result, serverName) {
92518
92769
  if (result.status !== 0) {
92519
92770
  return {
92520
92771
  broken: true,
@@ -92532,7 +92783,7 @@ function parseCodexProbe(result) {
92532
92783
  authenticated: false
92533
92784
  };
92534
92785
  }
92535
- const existing = servers.find((server) => server.name === MCP_SERVER_NAME);
92786
+ const existing = servers.find((server) => server.name === serverName);
92536
92787
  if (!existing) return { broken: false, missing: true, authenticated: false };
92537
92788
  return {
92538
92789
  broken: false,
@@ -92544,7 +92795,7 @@ function parseCodexProbe(result) {
92544
92795
  function codexAddArgs(deps, options) {
92545
92796
  const official = deps.officialClients?.codex;
92546
92797
  const credentialArgs = options.withOfficialClientId && official ? ["--oauth-client-id", official.clientId] : [];
92547
- return ["mcp", "add", MCP_SERVER_NAME, ...credentialArgs, "--url", deps.mcpUrl];
92798
+ return ["mcp", "add", deps.serverName, ...credentialArgs, "--url", deps.mcpUrl];
92548
92799
  }
92549
92800
  function codexAddLogsIn(bin, deps) {
92550
92801
  const help = deps.run(bin, ["mcp", "add", "--help"]);
@@ -92559,9 +92810,9 @@ function codexServerEntryPattern(serverName) {
92559
92810
  function healOrphanCodexOauthTable(name, deps) {
92560
92811
  const existing = deps.readTextFile(deps.codexConfigPath);
92561
92812
  if (existing === null) return;
92562
- if (!codexOauthHeaderPattern(MCP_SERVER_NAME).test(existing)) return;
92563
- if (codexServerEntryPattern(MCP_SERVER_NAME).test(existing)) return;
92564
- const { content, changed } = unpatchCodexConfigToml(existing, MCP_SERVER_NAME);
92813
+ if (!codexOauthHeaderPattern(deps.serverName).test(existing)) return;
92814
+ if (codexServerEntryPattern(deps.serverName).test(existing)) return;
92815
+ const { content, changed } = unpatchCodexConfigToml(existing, deps.serverName);
92565
92816
  if (!changed) return;
92566
92817
  deps.writeTextFile(deps.codexConfigPath, content);
92567
92818
  deps.log(
@@ -92570,7 +92821,7 @@ function healOrphanCodexOauthTable(name, deps) {
92570
92821
  }
92571
92822
  function connectCodex(bin, name, deps) {
92572
92823
  healOrphanCodexOauthTable(name, deps);
92573
- const probe = parseCodexProbe(deps.run(bin, ["mcp", "list", "--json"]));
92824
+ const probe = parseCodexProbe(deps.run(bin, ["mcp", "list", "--json"]), deps.serverName);
92574
92825
  if (probe.broken) {
92575
92826
  return failed(probe.detail ?? "probing for the Powerplant MCP failed");
92576
92827
  }
@@ -92597,7 +92848,7 @@ function connectCodex(bin, name, deps) {
92597
92848
  }
92598
92849
  ensureCodexOfficialClient(name, deps);
92599
92850
  if (alreadyConnected) return { state: "connected", unchanged: true };
92600
- return login(bin, name, ["mcp", "login", MCP_SERVER_NAME], deps);
92851
+ return login(bin, name, ["mcp", "login", deps.serverName], deps);
92601
92852
  }
92602
92853
  function applyCodexConfigPatch(name, deps, patch, successLine) {
92603
92854
  if (patch.conflict !== void 0) {
@@ -92629,7 +92880,7 @@ function ensureCodexOfficialClient(name, deps) {
92629
92880
  deps,
92630
92881
  patchCodexConfigToml(
92631
92882
  deps.readTextFile(deps.codexConfigPath),
92632
- MCP_SERVER_NAME,
92883
+ deps.serverName,
92633
92884
  official.clientId,
92634
92885
  official.callbackPort
92635
92886
  ),
@@ -92640,7 +92891,7 @@ function connectCursor(mcpJsonPath, name, deps) {
92640
92891
  const official = deps.officialClients?.cursor;
92641
92892
  const patch = patchCursorMcpJson(
92642
92893
  deps.readTextFile(mcpJsonPath),
92643
- MCP_SERVER_NAME,
92894
+ deps.serverName,
92644
92895
  deps.mcpUrl,
92645
92896
  official?.clientId ?? null
92646
92897
  );
@@ -92651,12 +92902,12 @@ function connectCursor(mcpJsonPath, name, deps) {
92651
92902
  }
92652
92903
  if (patch.changed) deps.writeTextFile(mcpJsonPath, patch.content);
92653
92904
  const configured = patch.changed ? "configured" : "already configured";
92654
- if (deps.openUrl(cursorInstallDeeplink(MCP_SERVER_NAME, patch.entry))) {
92905
+ if (deps.openUrl(cursorInstallDeeplink(deps.serverName, patch.entry))) {
92655
92906
  deps.log(`\u2192 ${name} ${configured}, approve inside Cursor when it asks.`);
92656
92907
  return { state: "configured", unchanged: !patch.changed };
92657
92908
  }
92658
92909
  deps.log(
92659
- `\u2192 ${name} ${configured}, could not open Cursor; in Cursor's MCP settings, enable the powerplant server and sign in.`
92910
+ `\u2192 ${name} ${configured}, could not open Cursor; in Cursor's MCP settings, enable the ${deps.serverName} server and sign in.`
92660
92911
  );
92661
92912
  return { state: "pending", unchanged: false };
92662
92913
  }
@@ -92717,7 +92968,7 @@ function disconnectClaude(client, deps) {
92717
92968
  const { bin } = client;
92718
92969
  const name = describeClaudeTarget(client);
92719
92970
  const env = claudeEnv(client);
92720
- const get = deps.run(bin, ["mcp", "get", MCP_SERVER_NAME], env);
92971
+ const get = deps.run(bin, ["mcp", "get", deps.serverName], env);
92721
92972
  const output2 = get.stdout + get.stderr;
92722
92973
  if (/No MCP server named/i.test(output2)) {
92723
92974
  deps.log(`${name}: not connected.`);
@@ -92727,7 +92978,7 @@ function disconnectClaude(client, deps) {
92727
92978
  deps.log(`\u2717 ${name}: probing for the Powerplant MCP failed, ${describeFailure(get)}`);
92728
92979
  return "failed";
92729
92980
  }
92730
- const remove = deps.run(bin, ["mcp", "remove", "--scope", "user", MCP_SERVER_NAME], env);
92981
+ const remove = deps.run(bin, ["mcp", "remove", "--scope", "user", deps.serverName], env);
92731
92982
  if (remove.status !== 0) {
92732
92983
  deps.log(`\u2717 ${name}: removing the Powerplant MCP failed, ${describeFailure(remove)}`);
92733
92984
  return "failed";
@@ -92747,15 +92998,15 @@ function disconnectCodex(bin, deps) {
92747
92998
  deps.log(`\u2717 ${name}: could not read its MCP server list, is this codex version very old?`);
92748
92999
  return "failed";
92749
93000
  }
92750
- const registered = servers.some((server) => server.name === MCP_SERVER_NAME);
92751
- const patch = unpatchCodexConfigToml(deps.readTextFile(deps.codexConfigPath), MCP_SERVER_NAME);
93001
+ const registered = servers.some((server) => server.name === deps.serverName);
93002
+ const patch = unpatchCodexConfigToml(deps.readTextFile(deps.codexConfigPath), deps.serverName);
92752
93003
  if (patch.changed) deps.writeTextFile(deps.codexConfigPath, patch.content);
92753
93004
  if (!registered) {
92754
93005
  deps.log(`${name}: not connected.`);
92755
93006
  return "notConnected";
92756
93007
  }
92757
- deps.run(bin, ["mcp", "logout", MCP_SERVER_NAME]);
92758
- const remove = deps.run(bin, ["mcp", "remove", MCP_SERVER_NAME]);
93008
+ deps.run(bin, ["mcp", "logout", deps.serverName]);
93009
+ const remove = deps.run(bin, ["mcp", "remove", deps.serverName]);
92759
93010
  if (remove.status !== 0) {
92760
93011
  deps.log(`\u2717 ${name}: removing the Powerplant MCP failed, ${describeFailure(remove)}`);
92761
93012
  return "failed";
@@ -92765,9 +93016,9 @@ function disconnectCodex(bin, deps) {
92765
93016
  }
92766
93017
  function disconnectCursor(mcpJsonPath, deps) {
92767
93018
  const name = `Cursor (${mcpJsonPath.replace(os20.homedir(), "~")})`;
92768
- const patch = unpatchCursorMcpJson(deps.readTextFile(mcpJsonPath), MCP_SERVER_NAME);
93019
+ const patch = unpatchCursorMcpJson(deps.readTextFile(mcpJsonPath), deps.serverName);
92769
93020
  if (patch.error !== void 0) {
92770
- deps.log(`\u2717 ${name}: mcp.json is ${patch.error}, fix it by hand, then re-run powerplant disconnect.`);
93021
+ deps.log(`\u2717 ${name}: mcp.json is ${patch.error}, fix it by hand, then re-run ${deps.disconnectCommand}.`);
92771
93022
  return "failed";
92772
93023
  }
92773
93024
  if (!patch.changed) {
@@ -92781,15 +93032,15 @@ function disconnectCursor(mcpJsonPath, deps) {
92781
93032
  function anyClientStillConnected(clients, deps) {
92782
93033
  return clients.some((client) => {
92783
93034
  if (client.client === "claude") {
92784
- const get = deps.run(client.bin, ["mcp", "get", MCP_SERVER_NAME], claudeEnv(client));
93035
+ const get = deps.run(client.bin, ["mcp", "get", deps.serverName], claudeEnv(client));
92785
93036
  return get.status === 0 && !/No MCP server named/i.test(get.stdout + get.stderr);
92786
93037
  }
92787
93038
  if (client.client === "codex") {
92788
93039
  const list = deps.run(client.bin, ["mcp", "list", "--json"]);
92789
93040
  if (list.status !== 0) return false;
92790
- return parseCodexServers(list.stdout)?.some((server) => server.name === MCP_SERVER_NAME) ?? false;
93041
+ return parseCodexServers(list.stdout)?.some((server) => server.name === deps.serverName) ?? false;
92791
93042
  }
92792
- const patch = unpatchCursorMcpJson(deps.readTextFile(client.bin), MCP_SERVER_NAME);
93043
+ const patch = unpatchCursorMcpJson(deps.readTextFile(client.bin), deps.serverName);
92793
93044
  return patch.error === void 0 && patch.changed;
92794
93045
  });
92795
93046
  }
@@ -92974,10 +93225,10 @@ function probeClient(client, deps) {
92974
93225
  return report(
92975
93226
  kind,
92976
93227
  kind === "claude" ? parseClaudeProbe(
92977
- deps.run(bin, ["mcp", "get", MCP_SERVER_NAME], {
93228
+ deps.run(bin, ["mcp", "get", deps.serverName], {
92978
93229
  CLAUDE_CONFIG_DIR: client.configDir
92979
93230
  })
92980
- ) : parseCodexProbe(deps.run(bin, ["mcp", "list", "--json"])),
93231
+ ) : parseCodexProbe(deps.run(bin, ["mcp", "list", "--json"]), deps.serverName),
92981
93232
  deps.mcpUrl
92982
93233
  );
92983
93234
  }
@@ -93000,7 +93251,7 @@ function probeCursor(mcpJsonPath, deps) {
93000
93251
  detail: error63 instanceof Error ? error63.message : String(error63)
93001
93252
  };
93002
93253
  }
93003
- const registration = readCursorRegistration(raw, MCP_SERVER_NAME);
93254
+ const registration = readCursorRegistration(raw, deps.serverName);
93004
93255
  if (registration.error !== void 0) {
93005
93256
  return { client: "cursor", state: "check-failed", detail: `mcp.json is ${registration.error}` };
93006
93257
  }
@@ -93158,7 +93409,7 @@ import { accessSync, constants as constants2, readFileSync as readFileSync12 } f
93158
93409
  import path25 from "node:path";
93159
93410
 
93160
93411
  // src/version.ts
93161
- var CLI_VERSION = true ? "0.4.11" : "0.0.0-checkout";
93412
+ var CLI_VERSION = true ? "0.4.13" : "0.0.0-checkout";
93162
93413
  var CLI_RELEASE_BUILD = true ? true : false;
93163
93414
  var CLI_PACKAGE_NAME = "@usepowerplant/cli";
93164
93415
 
@@ -93278,7 +93529,13 @@ function realInspectEnvironment() {
93278
93529
 
93279
93530
  // src/update.ts
93280
93531
  var updateProgressSchema = external_exports.object({
93281
- state: external_exports.enum(["installing", "failed"]),
93532
+ /** "up-to-date": the last run installed nothing, because this build is
93533
+ * already at or past the release. That record is kept only so the menu
93534
+ * bar app that started the run can stop showing "Updating CLI…"
93535
+ * (menu.ts cliUpToDateAt); to every other reader it is no update at all.
93536
+ * A CLI from before this state rejects the record, which it reads as no
93537
+ * record. */
93538
+ state: external_exports.enum(["installing", "failed", "up-to-date"]),
93282
93539
  /** The release aimed at; null when the registry couldn't be asked (its latest). */
93283
93540
  target: external_exports.string().nullable(),
93284
93541
  startedAt: external_exports.string(),
@@ -93318,7 +93575,7 @@ function readUpdateProgress(readTextFile, file2 = updateProgressPath()) {
93318
93575
  }
93319
93576
  }
93320
93577
  function updateProgressView(progress, cliVersion2, now) {
93321
- if (progress === null) return { kind: "none" };
93578
+ if (progress === null || progress.state === "up-to-date") return { kind: "none" };
93322
93579
  const reached = progress.target === null ? null : compareReleaseVersions(cliVersion2, progress.target);
93323
93580
  if (reached === 0 || reached === 1) return { kind: "none" };
93324
93581
  if (progress.state === "failed") {
@@ -93351,13 +93608,29 @@ async function runUpdate(deps) {
93351
93608
  const reserved = running?.state === "installing" ? running.target : null;
93352
93609
  const attempts = (release2) => running?.target === release2 && running.attempts !== void 0 ? { attempts: running.attempts } : {};
93353
93610
  deps.writeProgress({ state: "installing", target: reserved, startedAt, pid: deps.pid, ...attempts(reserved) });
93354
- const target = await deps.resolveTarget() ?? reserved;
93611
+ const resolved = await deps.resolveTarget();
93612
+ if (resolved !== null) deps.recordAvailableVersion(resolved);
93613
+ const target = resolved ?? reserved;
93355
93614
  if (target === null && deps.targetRequired) {
93356
93615
  deps.clearProgress();
93357
93616
  throw new ExpectedCliError(
93358
93617
  "No release is promoted for production right now (or the API couldn't be reached), so there is nothing to update to. Production machines only install the promoted release."
93359
93618
  );
93360
93619
  }
93620
+ const reached = target === null ? null : compareReleaseVersions(deps.cliVersion, target);
93621
+ if (reached === 0 || reached === 1) {
93622
+ deps.writeProgress({
93623
+ state: "up-to-date",
93624
+ target,
93625
+ startedAt,
93626
+ pid: deps.pid,
93627
+ finishedAt: new Date(deps.now()).toISOString()
93628
+ });
93629
+ deps.log(
93630
+ reached === 0 ? `Already up to date (v${deps.cliVersion}).` : `Already on v${deps.cliVersion}, newer than the available release (v${target}). Nothing was installed.`
93631
+ );
93632
+ return;
93633
+ }
93361
93634
  deps.writeProgress({ state: "installing", target, startedAt, pid: deps.pid, ...attempts(target) });
93362
93635
  const { bin, args } = updateCommand(manager, target, inspection.prefix, deps.channel);
93363
93636
  deps.log(`Updating ${CLI_PACKAGE_NAME}${target === null ? "" : ` to ${target}`} via ${manager}\u2026`);
@@ -94202,6 +94475,7 @@ function renderMenu(i) {
94202
94475
  logPath: updateLogPath()
94203
94476
  }
94204
94477
  },
94478
+ ...i.update.progress?.state === "up-to-date" && { cliUpToDateAt: i.update.progress.finishedAt },
94205
94479
  ...i.desktopDiagnostic && { desktopDiagnostic: i.desktopDiagnostic },
94206
94480
  appEnv: i.config.appEnv,
94207
94481
  cliVersion: i.cliVersion,
@@ -94505,14 +94779,17 @@ async function cachedLatestVersion(options) {
94505
94779
  }
94506
94780
  }
94507
94781
  const version2 = await options.fetchLatest();
94782
+ writeVersionCache({ ...options, version: version2 });
94783
+ return version2;
94784
+ }
94785
+ function writeVersionCache(options) {
94508
94786
  try {
94509
94787
  options.writeTextFile(
94510
94788
  options.cacheFile,
94511
- JSON.stringify({ checkedAt: new Date(options.now).toISOString(), version: version2 })
94789
+ JSON.stringify({ checkedAt: new Date(options.now).toISOString(), version: options.version })
94512
94790
  );
94513
94791
  } catch {
94514
94792
  }
94515
- return version2;
94516
94793
  }
94517
94794
 
94518
94795
  // src/liveTerminalSessions.ts
@@ -95569,6 +95846,7 @@ ${USAGE}`);
95569
95846
  log: log3
95570
95847
  });
95571
95848
  const connectCommand = withEnv("powerplant connect");
95849
+ const disconnectCommand = withEnv("powerplant disconnect");
95572
95850
  const clientFilter = parseClientFilter(values.client);
95573
95851
  const pickerOn = pickerAllowed({
95574
95852
  yes: values.yes,
@@ -95618,6 +95896,7 @@ ${USAGE}`);
95618
95896
  }
95619
95897
  const outcome = runConnect(clients, {
95620
95898
  mcpUrl: `${config2.apiUrl}/mcp`,
95899
+ serverName: mcpServerNameFor(config2.appEnv),
95621
95900
  connectCommand,
95622
95901
  officialClients: await fetchOfficialClients(config2.apiUrl, log3),
95623
95902
  log: log3,
@@ -95705,6 +95984,8 @@ ${USAGE}`);
95705
95984
  }
95706
95985
  const outcome = runDisconnect(clients, {
95707
95986
  log: console.log,
95987
+ serverName: mcpServerNameFor(config2.appEnv),
95988
+ disconnectCommand,
95708
95989
  run: realRun,
95709
95990
  readTextFile: realReadTextFileStrict,
95710
95991
  writeTextFile: realWriteTextFile,
@@ -95837,6 +96118,7 @@ The access you granted these agents stays on your Powerplant account until you r
95837
96118
  oldestSupportedVersion,
95838
96119
  connections: probeConnections(clients, {
95839
96120
  mcpUrl: `${config2.apiUrl}/mcp`,
96121
+ serverName: mcpServerNameFor(config2.appEnv),
95840
96122
  run: runWithPath(probePath, budget),
95841
96123
  readTextFile: realReadTextFileStrict,
95842
96124
  grantedClientIds
@@ -95886,7 +96168,14 @@ The access you granted these agents stays on your Powerplant account until you r
95886
96168
  }
95887
96169
  await runUpdate({
95888
96170
  inspection,
96171
+ cliVersion: CLI_VERSION,
95889
96172
  resolveTarget: () => fetchAvailableVersion(config2, loadCredentialsSafely(config2).credentials),
96173
+ recordAvailableVersion: (version2) => writeVersionCache({
96174
+ now: Date.now(),
96175
+ cacheFile: versionCachePath(void 0, releaseSource(config2.appEnv)),
96176
+ writeTextFile: realWriteTextFile,
96177
+ version: version2
96178
+ }),
95890
96179
  channel: releaseSource(config2.appEnv) === "npm" ? "next" : "latest",
95891
96180
  targetRequired: releaseSource(config2.appEnv) === "promoted",
95892
96181
  claimsReservation: process.env[UPDATE_CHILD_ENV] === "1",
@@ -95984,9 +96273,13 @@ The access you granted these agents stays on your Powerplant account until you r
95984
96273
  "Credentials kept, `powerplant init` re-installs without re-enrolling. (--forget deletes them.)"
95985
96274
  );
95986
96275
  }
95987
- if (anyClientStillConnected(detectClients(), { run: realRun, readTextFile: realReadTextFile })) {
96276
+ if (anyClientStillConnected(detectClients(), {
96277
+ run: realRun,
96278
+ readTextFile: realReadTextFile,
96279
+ serverName: mcpServerNameFor(config2.appEnv)
96280
+ })) {
95988
96281
  console.log(
95989
- "Your coding agents are still connected to the Powerplant MCP server, remove those connections with: powerplant disconnect"
96282
+ `Your coding agents are still connected to the Powerplant MCP server, remove those connections with: ${disconnectCommand}`
95990
96283
  );
95991
96284
  }
95992
96285
  return;
@@ -96168,3 +96461,5 @@ undici/lib/web/fetch/body.js:
96168
96461
  undici/lib/web/websocket/frame.js:
96169
96462
  (*! ws. MIT License. Einar Otto Stangvik <einaros@gmail.com> *)
96170
96463
  */
96464
+
96465
+ //# debugId=66f773d5-e0b2-5538-a184-3ee15c3a469a