@usepowerplant/cli 0.4.12 → 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/README.md CHANGED
@@ -1,3 +1,9 @@
1
+ <!--
2
+ This file is the public npm page of @usepowerplant/cli. Write only what a
3
+ customer needs. Team procedures go in RELEASING.md, which npm does not publish.
4
+ Rule: docs/client-engineering-rules.md, "Releases".
5
+ -->
6
+
1
7
  # @usepowerplant/cli
2
8
 
3
9
  Set up a machine for [Powerplant](https://powerplant.sh): enroll it into
@@ -19,7 +25,7 @@ re-run at any time — finished steps are skipped with a note:
19
25
  1. **Enroll this machine.** Your browser opens so you can approve the
20
26
  machine into your org. On macOS, the credential is encrypted in a local
21
27
  file with its key in Keychain; other platforms use a file only your user
22
- can read. See [credential storage and reset](../../README.md#quick-staging-reset).
28
+ can read.
23
29
  2. **Install the background service.** The sync service watches your local
24
30
  coding-agent sessions (Claude Code, Codex, and Cursor) and uploads the
25
31
  ones in scope. If recent sessions name repositories under a macOS-protected
@@ -119,8 +125,10 @@ powerplant update
119
125
  That reinstalls `@usepowerplant/cli` with whichever package manager put it
120
126
  on this machine (npm, pnpm, bun, or yarn), so the copy that is running is the
121
127
  copy that gets replaced, then restarts the background sync onto the new
122
- version. Running the manager's own global install yourself works too: the
123
- service notices the new version on disk and restarts itself within moments.
128
+ version. When this machine already has the newest version available to it,
129
+ `update` says so and changes nothing. Running the manager's own global
130
+ install yourself works too: the service notices the new version on disk and
131
+ restarts itself within moments.
124
132
  When the install can't be replaced this way (a folder only an administrator
125
133
  can write, or a layout the CLI doesn't recognise), `update` says why and
126
134
  prints the command to run instead. `powerplant status` tells you when a
@@ -144,134 +152,17 @@ The one exception is a registry that does not serve the release yet (npm's
144
152
  cache can lag a publish by a few minutes): the sync tries that release
145
153
  again on its next hourly checks, up to three times, and the menu says so.
146
154
 
155
+ Powerplant offers your machine a release only after the team has tested
156
+ it, so the version it offers can be older than the newest version listed on
157
+ npm. Powerplant never offers or automatically installs a version older
158
+ than the one you have.
159
+
147
160
  Powerplant also publishes the oldest version it still supports. When yours
148
161
  is older, `powerplant status`, `powerplant doctor`, the menu bar app, and
149
162
  the sync service log all say so and point you at `powerplant update`. Nothing
150
163
  stops working on that notice — an unsupported version keeps syncing, but
151
164
  it is no longer tested or fixed, so update when you see it.
152
165
 
153
- ### How a release reaches production
154
-
155
- The same CLI package serves every environment. Staging, development, and
156
- agent installs follow npm's `next` tag; enrolled production installs follow
157
- the exact version promoted through the production API. Fresh installs
158
- without a tag resolve npm's `latest`. Use a separate staging install below
159
- to test `next` while keeping your production install on its promoted version.
160
-
161
- 1. **Release.** The "Release CLI to next" workflow (GitHub Actions,
162
- manual) builds the CLI, signs and notarizes the menu bar app into the
163
- package, and publishes to the `next` tag. Staging, development, and
164
- agent installs with automatic updates enabled check at boot and hourly
165
- while the service is running. Sleeping machines, disabled updates, and
166
- failed installs can leave a machine behind. Verify the installed staging
167
- version and exercise sync and the menu actions you changed before
168
- promoting; elapsed time alone is not evidence of testing. Promote only
169
- after the workflow's "Wait until npm serves the release" step is green:
170
- npm's CDN caches the package document for about five minutes after a
171
- publish, and a production machine that checks inside that window gets
172
- "no matching version" and waits an hour to try again.
173
- 2. **Promote.** From a repo checkout with npm package access and `gh`
174
- signed in, name the version you tested:
175
-
176
- ```bash
177
- pnpm cli:promote:release 0.5.0
178
- ```
179
-
180
- Or, if `next` still points at that tested version:
181
-
182
- ```bash
183
- pnpm cli:promote:release
184
- ```
185
-
186
- Omitting the version resolves `next` when the command runs.
187
- The script moves npm's `latest` from your terminal, with your security
188
- key in the browser (npm requires a live second factor for every
189
- dist-tag move, so no CI credential can do this half), then dispatches
190
- the Promote CLI workflow, which verifies the tag and points production
191
- machines at the same artifact. The script watches the workflow; confirm
192
- the pointer write succeeded before calling the promotion complete.
193
- If the tag moved but that write failed, fresh installs are ahead of
194
- enrolled machines. Re-dispatch **the same version**, then check that run:
195
-
196
- ```bash
197
- gh workflow run promote-cli.yml -f version=0.5.0
198
- ```
199
-
200
- A failed Slack announcement after a successful pointer write does not
201
- undo promotion. Read the failed step before retrying.
202
- 3. **Adopt.** Running production services with automatic updates enabled
203
- check for the promoted version at boot and hourly. Other users can
204
- update from the menu bar or `powerplant update`; `powerplant status`
205
- reports the available version. Until something is promoted, production
206
- sees no update target, only the supported-version floor.
207
-
208
- Promoting an older version is the brake: production holds there, `latest`
209
- moves back for new installs, and no enrolled machine ever downgrades. A
210
- bad release is undone by a new release and a new promotion.
211
-
212
- ### Staging beside production on one Mac
213
-
214
- Use production for day-to-day team work. Staging is for testing enrollment,
215
- onboarding, and releases. See the [internal machine setup guide](../../README.md#set-up-your-machine)
216
- for access and data boundaries: running sync in both environments uploads
217
- and processes eligible sessions twice.
218
-
219
- A machine has one global `powerplant` by default, and the background sync
220
- for each environment runs whatever copy `init` was run from. To try
221
- releases on staging while production keeps the promoted version, give
222
- staging its own copy under its own npm prefix and set it up from there:
223
-
224
- ```bash
225
- npm install -g --prefix ~/.powerplant-staging @usepowerplant/cli@next
226
- ~/.powerplant-staging/bin/powerplant init --env staging
227
- ```
228
-
229
- `init --env staging` registers its own MCP entry, `powerplant-staging`, in each
230
- selected agent, beside the production `powerplant` entry. Agents see its tools
231
- as `mcp__powerplant-staging__*`. A CLI older than this change registered
232
- staging as `powerplant`: if that entry still points at staging, run a bare
233
- `powerplant connect` from your production install once to restore it. Include
234
- `--env staging` on every staging command.
235
-
236
- The `@next` pin matters: a bare install resolves `latest`, the production
237
- channel, so the new staging copy would start on the promoted release and
238
- test nothing until its first update.
239
-
240
- The staging daemon runs that copy and updates it in place (the updater
241
- pins npm to the prefix the running copy lives under), so
242
- `~/.powerplant-staging` follows the `next` channel while the production
243
- install follows the promoted one. Run staging commands through that path,
244
- `~/.powerplant-staging/bin/powerplant status --env staging`.
245
- Use `~/.powerplant-staging/bin/powerplant update --env staging` to update
246
- now, then `~/.powerplant-staging/bin/powerplant doctor --env staging` to
247
- check the installed version, service, and install location.
248
-
249
- ### Menu bar apps during release testing
250
-
251
- The npm package, including `@next`, contains **Powerplant.app**, the
252
- production app. Only production `init` installs it and the production
253
- service refreshes it after CLI updates. Staging `init` does neither.
254
-
255
- Run **Powerplant Staging.app** separately, alongside production. Its icon
256
- has an "S", and it uses the CLI at `~/.powerplant-staging/bin/powerplant`
257
- when present. Follow the menu bar app's [local build instructions](../../apps/menubar/README.md#development)
258
- or [signed release instructions](../../apps/menubar/README.md#release).
259
- Rebuild or replace that staging app when testing native app changes;
260
- updating the staging CLI does not update it. This tests the staging variant,
261
- not the exact signed production bundle embedded in the npm package.
262
-
263
- ### Web and API promotion is separate
264
-
265
- Merges to `main` deploy to staging automatically. The
266
- [Promote to Production workflow](../../.github/workflows/promote.yml) can
267
- promote the live staging API commit after its E2E check passes, with the
268
- production schema and web build from that commit. Automatic runs happen
269
- on CI completion and an hourly catch-up during weekdays, 09:00 to before
270
- 16:00 America/Vancouver, unless `AUTO_PROMOTE_PAUSED=true`. A manual
271
- workflow dispatch can promote outside that window. Staging does not wait
272
- indefinitely for manual QA. This workflow does not promote the CLI;
273
- `pnpm cli:promote:release` does not deploy the web or API.
274
-
275
166
  ### Moving from @powerplant-sh/cli
276
167
 
277
168
  Releases up to 0.2.1 were published as `@powerplant-sh/cli`. That package
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
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]="7d73a97a-938c-5f0f-8b6b-c4076b82abad")}catch(e){}}();
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){}}();
4
4
  const __cliNodeMajor = Number(process.versions.node.split('.')[0]);
5
5
  if (__cliNodeMajor < 20) {
6
6
  console.error(`powerplant needs Node 20 or newer — this is Node ${process.versions.node}.`);
@@ -85883,11 +85883,11 @@ import path12 from "node:path";
85883
85883
  import { existsSync as existsSync6, lstatSync as lstatSync4 } from "node:fs";
85884
85884
  import path11 from "node:path";
85885
85885
 
85886
- // ../../apps/collector/src/codexTranscriptHints.ts
85886
+ // ../../apps/collector/src/codexTranscriptDiscovery.ts
85887
85887
  import { createHash } from "node:crypto";
85888
85888
  import { closeSync, lstatSync, openSync, readSync, readdirSync, realpathSync } from "node:fs";
85889
85889
  import path2 from "node:path";
85890
- var hintSchema = external_exports.object({
85890
+ var transcriptDiscoveryRecordSchema = external_exports.object({
85891
85891
  sessionId: external_exports.string().regex(/^[a-zA-Z0-9_-]{1,200}$/),
85892
85892
  transcriptPath: external_exports.string().max(4096).nullable()
85893
85893
  });
@@ -85900,7 +85900,7 @@ function codexTranscriptRecordPath(home, sessionId) {
85900
85900
  `${createHash("sha256").update(sessionId).digest("hex")}.json`
85901
85901
  );
85902
85902
  }
85903
- function readCodexTranscriptHints(home, roots) {
85903
+ function readCodexTranscriptDiscoveryRecords(home, roots) {
85904
85904
  const dir = codexTranscriptRecordsDir(home);
85905
85905
  let files;
85906
85906
  try {
@@ -85912,9 +85912,9 @@ function readCodexTranscriptHints(home, roots) {
85912
85912
  for (const file2 of files) {
85913
85913
  if (!/^[a-f0-9]{64}\.json$/.test(file2)) continue;
85914
85914
  try {
85915
- const hintPath = path2.join(dir, file2);
85916
- if (!lstatSync(hintPath).isFile()) continue;
85917
- const fd = openSync(hintPath, "r");
85915
+ const recordPath = path2.join(dir, file2);
85916
+ if (!lstatSync(recordPath).isFile()) continue;
85917
+ const fd = openSync(recordPath, "r");
85918
85918
  let text;
85919
85919
  try {
85920
85920
  const buffer = Buffer.alloc(32 * 1024 + 1);
@@ -85924,10 +85924,10 @@ function readCodexTranscriptHints(home, roots) {
85924
85924
  } finally {
85925
85925
  closeSync(fd);
85926
85926
  }
85927
- const hint = hintSchema.parse(JSON.parse(text));
85928
- if (file2 !== `${createHash("sha256").update(hint.sessionId).digest("hex")}.json`) continue;
85929
- if (hint.transcriptPath === null || !path2.isAbsolute(hint.transcriptPath)) continue;
85930
- 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);
85931
85931
  if (path2.extname(rolloutPath) !== ".jsonl") continue;
85932
85932
  const rootIndex = roots.findIndex((root2) => {
85933
85933
  const relative2 = path2.relative(root2, rolloutPath);
@@ -85937,7 +85937,7 @@ function readCodexTranscriptHints(home, roots) {
85937
85937
  const root = roots[rootIndex];
85938
85938
  if (realpathSync(rolloutPath) !== path2.join(realpathSync(root), path2.relative(root, rolloutPath)))
85939
85939
  continue;
85940
- result.push({ rolloutPath, sessionId: hint.sessionId, archived: rootIndex === 1 });
85940
+ result.push({ rolloutPath, sessionId: record2.sessionId, archived: rootIndex === 1 });
85941
85941
  } catch {
85942
85942
  }
85943
85943
  }
@@ -85950,7 +85950,7 @@ import { createHmac } from "node:crypto";
85950
85950
  // ../../apps/collector/src/http.ts
85951
85951
  var import_undici = __toESM(require_undici(), 1);
85952
85952
  var CLI_VERSION_HEADER = "x-powerplant-cli-version";
85953
- var cliVersion = true ? "0.4.12" : "0.0.0-checkout";
85953
+ var cliVersion = true ? "0.4.13" : "0.0.0-checkout";
85954
85954
  function cliVersionHeaders() {
85955
85955
  return { [CLI_VERSION_HEADER]: cliVersion, "user-agent": `powerplant-cli/${cliVersion}` };
85956
85956
  }
@@ -86257,7 +86257,7 @@ function bindDiagnosticEnrollment(diagnostic, token) {
86257
86257
  passEnrollments.set(diagnostic, token);
86258
86258
  }
86259
86259
 
86260
- // ../../apps/collector/src/codexCli.ts
86260
+ // ../../apps/collector/src/codexSessions.ts
86261
86261
  import { closeSync as closeSync3, mkdirSync as mkdirSync2, openSync as openSync3, renameSync, rmSync as rmSync2, utimesSync, writeSync } from "node:fs";
86262
86262
  import os6 from "node:os";
86263
86263
  import path5 from "node:path";
@@ -86697,13 +86697,17 @@ function isPathWithinScopes(resolved, scopePaths) {
86697
86697
  );
86698
86698
  }
86699
86699
 
86700
- // ../../apps/collector/src/codexCli.ts
86700
+ // ../../apps/collector/src/codexSessions.ts
86701
86701
  function parseCodexRollout(text, sharedUsage) {
86702
86702
  let meta3 = null;
86703
+ let initialEvent;
86704
+ let records = 0;
86703
86705
  let model;
86704
86706
  const timestamps = [];
86705
86707
  const usage = sharedUsage ?? createCodexUsageReader();
86706
86708
  for (const line of parseJsonl(text)) {
86709
+ records += 1;
86710
+ if (records === 2 && meta3 !== null) initialEvent = line;
86707
86711
  if (typeof line.timestamp === "string") timestamps.push(line.timestamp);
86708
86712
  usage.pushRecord(line);
86709
86713
  const payload = line.payload;
@@ -86719,8 +86723,10 @@ function parseCodexRollout(text, sharedUsage) {
86719
86723
  if (meta3 === null && timestamps.length === 0) return null;
86720
86724
  const git = meta3?.git ?? null;
86721
86725
  const sessionId = meta3?.id ?? meta3?.session_id;
86726
+ const facts = meta3 === null ? {} : harnessSessionFacts("codex", { ...meta3, initialEvent });
86722
86727
  return {
86723
- kind: meta3 !== null && isSidecarThread(meta3) ? "subagent" : "session",
86728
+ kind: facts.subagent ? "subagent" : "session",
86729
+ ...facts.standaloneReview && { standaloneReview: true },
86724
86730
  ...typeof sessionId === "string" && sessionId !== "" && { sessionId },
86725
86731
  startedAt: timestamps[0],
86726
86732
  endedAt: timestamps[timestamps.length - 1],
@@ -86756,6 +86762,7 @@ function foldRolloutPages(pages, usage) {
86756
86762
  const repoFullName = first("repoFullName");
86757
86763
  return {
86758
86764
  kind: pages[0].kind,
86765
+ ...pages[0].standaloneReview && { standaloneReview: true },
86759
86766
  ...sessionId !== void 0 && { sessionId },
86760
86767
  ...startedAt !== void 0 && { startedAt },
86761
86768
  ...endedAt !== void 0 && { endedAt },
@@ -86777,21 +86784,21 @@ function rolloutIdentity(rolloutPath) {
86777
86784
  if (!conventional) return { threadId: name, page: false };
86778
86785
  return { threadId: threadId2.toLowerCase(), page: pageId !== void 0 };
86779
86786
  }
86780
- function codexSpoolDir(home) {
86787
+ function codexMergedTranscriptsDir(home) {
86781
86788
  return path5.join(home, ".config", "powerplant", "codex");
86782
86789
  }
86783
- function mergedSpoolIsFresh(spoolPath, mtimeMs) {
86784
- const existing = statIfExists(spoolPath);
86790
+ function mergedTranscriptIsFresh(mergedTranscriptPath, mtimeMs) {
86791
+ const existing = statIfExists(mergedTranscriptPath);
86785
86792
  return existing !== null && Math.abs(existing.mtimeMs - mtimeMs) < 1;
86786
86793
  }
86787
- var MergedSpoolWriter = class {
86788
- constructor(spoolPath) {
86789
- this.spoolPath = spoolPath;
86790
- mkdirSync2(path5.dirname(spoolPath), { recursive: true, mode: 448 });
86791
- 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`;
86792
86799
  this.fd = openSync3(this.tmpPath, "w", 384);
86793
86800
  }
86794
- spoolPath;
86801
+ mergedTranscriptPath;
86795
86802
  tmpPath;
86796
86803
  fd;
86797
86804
  bytes = 0;
@@ -86805,7 +86812,7 @@ var MergedSpoolWriter = class {
86805
86812
  finish(mtimeMs) {
86806
86813
  this.close();
86807
86814
  utimesSync(this.tmpPath, mtimeMs / 1e3, mtimeMs / 1e3);
86808
- renameSync(this.tmpPath, this.spoolPath);
86815
+ renameSync(this.tmpPath, this.mergedTranscriptPath);
86809
86816
  return this.bytes;
86810
86817
  }
86811
86818
  abandon() {
@@ -86875,7 +86882,7 @@ async function collectCodexSessions(options = {}) {
86875
86882
  const now = options.now ?? Date.now();
86876
86883
  const capture = options.capture ?? captureEverythingUnresolved;
86877
86884
  const maxBytes = options.maxTranscriptBytes ?? MAX_TRANSCRIPT_FILE_BYTES;
86878
- const spoolDir = options.spoolDir ?? codexSpoolDir(options.homeDir ?? os6.homedir());
86885
+ const mergedTranscriptsDir = options.mergedTranscriptsDir ?? codexMergedTranscriptsDir(options.homeDir ?? os6.homedir());
86879
86886
  const items = [];
86880
86887
  const candidates = [];
86881
86888
  const unreadable = [];
@@ -86886,12 +86893,12 @@ async function collectCodexSessions(options = {}) {
86886
86893
  let largestMergedBytes = 0;
86887
86894
  const externalIdOf = (rolloutPath) => path5.basename(rolloutPath, ".jsonl");
86888
86895
  const files = /* @__PURE__ */ new Map();
86889
- const observe = async (paths, archived, hintSessionId) => {
86896
+ const observe = async (paths, archived, discoverySessionId) => {
86890
86897
  for (const rolloutPath of paths) {
86891
86898
  const known = files.get(rolloutPath);
86892
86899
  if (known !== void 0) {
86893
- if (hintSessionId === void 0) delete known.hintSessionIds;
86894
- else known.hintSessionIds?.push(hintSessionId);
86900
+ if (discoverySessionId === void 0) delete known.discoverySessionIds;
86901
+ else known.discoverySessionIds?.push(discoverySessionId);
86895
86902
  continue;
86896
86903
  }
86897
86904
  await yieldToEventLoop();
@@ -86904,15 +86911,15 @@ async function collectCodexSessions(options = {}) {
86904
86911
  mtimeMs: stat.mtimeMs,
86905
86912
  changedMs: Math.max(stat.mtimeMs, stat.ctimeMs),
86906
86913
  sizeBytes: stat.size,
86907
- ...hintSessionId !== void 0 && { hintSessionIds: [hintSessionId] }
86914
+ ...discoverySessionId !== void 0 && { discoverySessionIds: [discoverySessionId] }
86908
86915
  });
86909
86916
  }
86910
86917
  };
86911
86918
  const archiveDir = codexArchivedSessionsDir(sessionsDir);
86912
- const hintHome = options.homeDir ?? (options.codexDir === void 0 ? os6.homedir() : void 0);
86913
- if (hintHome !== void 0) {
86914
- for (const hint of readCodexTranscriptHints(hintHome, [sessionsDir, archiveDir])) {
86915
- 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);
86916
86923
  }
86917
86924
  }
86918
86925
  await observe(rolloutFiles(sessionsDir), false);
@@ -86927,17 +86934,17 @@ async function collectCodexSessions(options = {}) {
86927
86934
  const stored = dedupeStoreCopies(observed);
86928
86935
  let externalId = externalIdOf((stored.find((file2) => !file2.identity.page) ?? stored[0]).rolloutPath);
86929
86936
  try {
86930
- 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;
86931
86938
  const changedMs = Math.max(...stored.map((file2) => file2.changedMs));
86932
86939
  if (options.sinceMs !== void 0 && changedMs < options.sinceMs) {
86933
- const neverMerged = spoolPath !== void 0 && !options.locationsOnly && statIfExists(spoolPath) === null;
86940
+ const neverMerged = mergedTranscriptPath !== void 0 && !options.locationsOnly && statIfExists(mergedTranscriptPath) === null;
86934
86941
  if (!neverMerged) continue;
86935
86942
  }
86936
86943
  await yieldToEventLoop();
86937
86944
  const named = stored.filter((file2) => {
86938
- if (file2.hintSessionIds === void 0) return true;
86945
+ if (file2.discoverySessionIds === void 0) return true;
86939
86946
  const { sessionId } = rolloutLocation(file2.rolloutPath);
86940
- return sessionId !== void 0 && file2.hintSessionIds.includes(sessionId);
86947
+ return sessionId !== void 0 && file2.discoverySessionIds.includes(sessionId);
86941
86948
  });
86942
86949
  if (named.length === 0) continue;
86943
86950
  const threadFiles = named.length > 1 ? orderPages(named) : named;
@@ -86967,7 +86974,7 @@ async function collectCodexSessions(options = {}) {
86967
86974
  const thread = {
86968
86975
  externalId,
86969
86976
  files: threadFiles,
86970
- ...threadFiles.length > 1 && spoolPath !== void 0 && { spoolPath },
86977
+ ...threadFiles.length > 1 && mergedTranscriptPath !== void 0 && { mergedTranscriptPath },
86971
86978
  mtimeMs,
86972
86979
  archived: threadFiles.every((file2) => file2.archived)
86973
86980
  };
@@ -87017,8 +87024,8 @@ async function collectCodexSessions(options = {}) {
87017
87024
  const { thread, cwd, decision } = candidate;
87018
87025
  let writer;
87019
87026
  try {
87020
- const rewrite = thread.spoolPath !== void 0 && !mergedSpoolIsFresh(thread.spoolPath, thread.mtimeMs);
87021
- 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);
87022
87029
  const pages = [];
87023
87030
  const usage = createCodexUsageReader();
87024
87031
  let vanished = false;
@@ -87039,11 +87046,11 @@ async function collectCodexSessions(options = {}) {
87039
87046
  continue;
87040
87047
  }
87041
87048
  const summary = foldRolloutPages(pages, usage.finish());
87042
- let transcriptPath = thread.files[0].rolloutPath;
87043
- if (thread.spoolPath !== void 0) {
87044
- 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;
87045
87052
  writer = void 0;
87046
- transcriptPath = thread.spoolPath;
87053
+ transcriptUploadSourcePath = thread.mergedTranscriptPath;
87047
87054
  mergedThreads += 1;
87048
87055
  largestMergedBytes = Math.max(largestMergedBytes, mergedBytes);
87049
87056
  }
@@ -87052,13 +87059,14 @@ async function collectCodexSessions(options = {}) {
87052
87059
  const title = summary.sessionId ? threadNames.get(summary.sessionId) : void 0;
87053
87060
  items.push({
87054
87061
  mtimeMs: thread.mtimeMs,
87055
- transcriptPath,
87062
+ transcriptPath: transcriptUploadSourcePath,
87056
87063
  ...cwd && { cwd },
87057
87064
  item: {
87058
87065
  source: "codex",
87059
87066
  externalId: thread.externalId,
87060
87067
  status: now - thread.mtimeMs < ACTIVE_GRACE_MS ? "active" : "inactive",
87061
87068
  archived: thread.archived,
87069
+ ...summary.standaloneReview && { automation: true },
87062
87070
  ...summary.startedAt && { startedAt: summary.startedAt },
87063
87071
  ...summary.endedAt && { endedAt: summary.endedAt },
87064
87072
  ...summary.model && { model: summary.model },
@@ -87219,7 +87227,7 @@ function defaultCursorUserDir(home) {
87219
87227
  function cursorGlobalStorageDir(home) {
87220
87228
  return path6.join(defaultCursorUserDir(home), "globalStorage");
87221
87229
  }
87222
- function cursorSpoolDir(home) {
87230
+ function cursorExtractedTranscriptsDir(home) {
87223
87231
  return path6.join(home, ".config", "powerplant", "cursor");
87224
87232
  }
87225
87233
  var emptyResult = () => ({
@@ -87275,7 +87283,7 @@ var cachedMetaSchema = external_exports.object({
87275
87283
  model: external_exports.string().optional(),
87276
87284
  ...cursorTranscriptMetaDisclosureSchema.shape
87277
87285
  });
87278
- function spoolFileName(composerId) {
87286
+ function extractedTranscriptFileName(composerId) {
87279
87287
  const sanitized = composerId.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 80);
87280
87288
  const hash2 = createHash2("sha256").update(composerId).digest("hex").slice(0, 8);
87281
87289
  return `${sanitized}-${hash2}.jsonl`;
@@ -87338,7 +87346,7 @@ function toolFilePathOf(args) {
87338
87346
  }
87339
87347
  return void 0;
87340
87348
  }
87341
- var SPOOL_META_RESERVE_BYTES = 64 * 1024;
87349
+ var EXTRACTED_TRANSCRIPT_META_RESERVE_BYTES = 64 * 1024;
87342
87350
  var BUBBLE_FIELDS_SHARED = ["_v", "type", "bubbleId", "serverBubbleId", "text", "createdAt"];
87343
87351
  var USER_BUBBLE_FIELDS = /* @__PURE__ */ new Set([...BUBBLE_FIELDS_SHARED, "context"]);
87344
87352
  var ASSISTANT_BUBBLE_FIELDS = /* @__PURE__ */ new Set([
@@ -87582,7 +87590,7 @@ function extractTranscript(db, composerId, maxBytes) {
87582
87590
  const spine = Array.isArray(data?.fullConversationHeadersOnly) ? data.fullConversationHeadersOnly : [];
87583
87591
  if (spine.length === 0) return null;
87584
87592
  const recorder = createOmissionRecorder();
87585
- const budget = maxBytes - SPOOL_META_RESERVE_BYTES;
87593
+ const budget = maxBytes - EXTRACTED_TRANSCRIPT_META_RESERVE_BYTES;
87586
87594
  const serialized = [];
87587
87595
  const modelCounts = /* @__PURE__ */ new Map();
87588
87596
  let bytes = 0;
@@ -87663,7 +87671,7 @@ async function collectCursorSessions(options = {}) {
87663
87671
  collectCursorTranscriptFiles(options, (id) => {
87664
87672
  if (knownIds.has(id)) return true;
87665
87673
  return db?.prepare("select composerId from composerHeaders where composerId = ?").get(id) != null;
87666
- }, result, options.spoolDir ?? cursorSpoolDir(options.homeDir ?? os7.homedir()), spoolFileName);
87674
+ }, result, options.extractedTranscriptsDir ?? cursorExtractedTranscriptsDir(options.homeDir ?? os7.homedir()), extractedTranscriptFileName);
87667
87675
  }
87668
87676
  } finally {
87669
87677
  try {
@@ -87677,9 +87685,9 @@ async function collectCursorSessions(options = {}) {
87677
87685
  function collectFromStore(db, userDir, options, result, knownIds) {
87678
87686
  const now = options.now ?? Date.now();
87679
87687
  const capture = options.capture ?? captureEverythingUnresolved;
87680
- const spoolDir = options.spoolDir ?? cursorSpoolDir(options.homeDir ?? os7.homedir());
87688
+ const extractedTranscriptsDir = options.extractedTranscriptsDir ?? cursorExtractedTranscriptsDir(options.homeDir ?? os7.homedir());
87681
87689
  const maxBytes = options.maxTranscriptBytes ?? MAX_TRANSCRIPT_FILE_BYTES;
87682
- let spoolDirReady = false;
87690
+ let extractedTranscriptsDirReady = false;
87683
87691
  const columns = new Set(
87684
87692
  db.prepare("pragma table_info(composerHeaders)").all().map((row) => isRecord2(row) ? row.name : void 0)
87685
87693
  );
@@ -87729,15 +87737,15 @@ function collectFromStore(db, userDir, options, result, knownIds) {
87729
87737
  });
87730
87738
  continue;
87731
87739
  }
87732
- const spoolPath = path6.join(spoolDir, spoolFileName(raw.composerId));
87733
- const spoolStat = statIfExists(spoolPath);
87740
+ const transcriptUploadSourcePath = path6.join(extractedTranscriptsDir, extractedTranscriptFileName(raw.composerId));
87741
+ const extractedTranscriptStat = statIfExists(transcriptUploadSourcePath);
87734
87742
  let model;
87735
- const sourceUnchanged = spoolStat !== null && Math.abs(spoolStat.mtimeMs - lastUpdatedAt) < 1;
87736
- if (sourceUnchanged && spoolStat.size > maxBytes) {
87743
+ const sourceUnchanged = extractedTranscriptStat !== null && Math.abs(extractedTranscriptStat.mtimeMs - lastUpdatedAt) < 1;
87744
+ if (sourceUnchanged && extractedTranscriptStat.size > maxBytes) {
87737
87745
  result.skippedTooLarge += 1;
87738
87746
  continue;
87739
87747
  }
87740
- const cached3 = sourceUnchanged ? readFirstRecord(spoolPath, () => true) : null;
87748
+ const cached3 = sourceUnchanged ? readFirstRecord(transcriptUploadSourcePath, () => true) : null;
87741
87749
  const cachedMeta = cachedMetaSchema.safeParse(cached3);
87742
87750
  const preparationCurrent = cachedMeta.success && cachedMeta.data.composerId === raw.composerId;
87743
87751
  let omissions;
@@ -87770,15 +87778,15 @@ function collectFromStore(db, userDir, options, result, knownIds) {
87770
87778
  ...counted && { omissions: transcript.omissions },
87771
87779
  ...unhandled.length > 0 && { unhandled }
87772
87780
  };
87773
- if (!spoolDirReady) {
87774
- mkdirSync3(spoolDir, { recursive: true, mode: 448 });
87775
- chmodSync2(spoolDir, 448);
87776
- spoolDirReady = true;
87781
+ if (!extractedTranscriptsDirReady) {
87782
+ mkdirSync3(extractedTranscriptsDir, { recursive: true, mode: 448 });
87783
+ chmodSync2(extractedTranscriptsDir, 448);
87784
+ extractedTranscriptsDirReady = true;
87777
87785
  }
87778
87786
  const body = [JSON.stringify(meta3), ...transcript.serialized, ""].join("\n");
87779
- if (spoolStat !== null) chmodSync2(spoolPath, 384);
87780
- writeFileSync2(spoolPath, body, { encoding: "utf8", mode: 384 });
87781
- 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));
87782
87790
  }
87783
87791
  if (hasCaptureGaps(omissions)) result.partiallyInterpreted += 1;
87784
87792
  for (const observation of unhandledObservations(unhandled, raw.composerId)) {
@@ -87797,7 +87805,7 @@ function collectFromStore(db, userDir, options, result, knownIds) {
87797
87805
  const startBranch = facts.branches[0];
87798
87806
  result.sessions.push({
87799
87807
  mtimeMs: lastUpdatedAt,
87800
- transcriptPath: spoolPath,
87808
+ transcriptPath: transcriptUploadSourcePath,
87801
87809
  ...cwd && { cwd },
87802
87810
  item: {
87803
87811
  source: "cursor",
@@ -87931,7 +87939,7 @@ function parseCursorTranscriptFile(text) {
87931
87939
  if (!lines.some((line) => CONTENT_LINE_TYPES2.has(line.type))) return null;
87932
87940
  return { lines, omissions: recorder.omissions(), unhandled: recorder.unhandled() };
87933
87941
  }
87934
- function spoolBody(metaFor, lines, maxBytes) {
87942
+ function extractedTranscriptBody(metaFor, lines, maxBytes) {
87935
87943
  for (const keepPayloads of [true, false]) {
87936
87944
  const payloadsDropped = keepPayloads ? 0 : lines.filter((line) => line.type === "unsupported" && line.payload !== void 0).length;
87937
87945
  const records = [
@@ -87950,7 +87958,7 @@ function spoolBody(metaFor, lines, maxBytes) {
87950
87958
  }
87951
87959
  return null;
87952
87960
  }
87953
- var spoolMetaSchema = external_exports.object({
87961
+ var extractedTranscriptMetaSchema = external_exports.object({
87954
87962
  type: external_exports.literal("meta"),
87955
87963
  format: external_exports.literal("cursor-local-transcript"),
87956
87964
  version: external_exports.literal(1),
@@ -87962,7 +87970,7 @@ var spoolMetaSchema = external_exports.object({
87962
87970
  model: external_exports.string().optional(),
87963
87971
  ...cursorTranscriptMetaDisclosureSchema.shape
87964
87972
  });
87965
- var hintSchema2 = external_exports.object({
87973
+ var transcriptDiscoveryRecordSchema2 = external_exports.object({
87966
87974
  sessionId: external_exports.string().regex(/^[a-zA-Z0-9_-]{1,200}$/),
87967
87975
  transcriptPath: external_exports.string().max(4096).nullable(),
87968
87976
  workspaceRoots: external_exports.array(external_exports.string().max(4096)).max(8),
@@ -87994,15 +88002,15 @@ function cursorTranscriptRecordPath(home, sessionId) {
87994
88002
  `${createHash3("sha256").update(sessionId).digest("hex")}.json`
87995
88003
  );
87996
88004
  }
87997
- function collectCursorTranscriptFiles(options, isDatabaseSession, result, databaseSpoolDir, spoolName) {
88005
+ function collectCursorTranscriptFiles(options, isDatabaseSession, result, databaseExtractedTranscriptsDir, extractedTranscriptFileName2) {
87998
88006
  const home = options.homeDir ?? os8.homedir();
87999
- const hintsDir = cursorTranscriptRecordsDir(home);
88000
- if (!existsSync3(hintsDir)) return;
88007
+ const recordsDir = cursorTranscriptRecordsDir(home);
88008
+ if (!existsSync3(recordsDir)) return;
88001
88009
  const maxBytes = options.maxTranscriptBytes ?? MAX_TRANSCRIPT_FILE_BYTES;
88002
- const spoolDir = path7.join(databaseSpoolDir, "native");
88010
+ const nativeExtractedTranscriptsDir = path7.join(databaseExtractedTranscriptsDir, "native");
88003
88011
  let files;
88004
88012
  try {
88005
- files = readdirSync3(hintsDir);
88013
+ files = readdirSync3(recordsDir);
88006
88014
  } catch {
88007
88015
  result.skippedUnreadable++;
88008
88016
  return;
@@ -88010,15 +88018,15 @@ function collectCursorTranscriptFiles(options, isDatabaseSession, result, databa
88010
88018
  for (const file2 of files) {
88011
88019
  if (!/^[a-f0-9]{64}\.json$/.test(file2)) continue;
88012
88020
  try {
88013
- const hintPath = path7.join(hintsDir, file2);
88014
- if (!lstatSync2(hintPath).isFile()) continue;
88015
- const text = readBounded(hintPath, 40 * 1024);
88016
- const hint = hintSchema2.parse(text === null ? null : JSON.parse(text));
88017
- if (file2 !== `${createHash3("sha256").update(hint.sessionId).digest("hex")}.json`) continue;
88018
- 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))))
88019
88027
  continue;
88020
- if (hint.transcriptPath === null || hint.workspaceRoots.length !== 1) continue;
88021
- const cwd = hint.workspaceRoots[0];
88028
+ if (record2.transcriptPath === null || record2.workspaceRoots.length !== 1) continue;
88029
+ const cwd = record2.workspaceRoots[0];
88022
88030
  if (!path7.isAbsolute(cwd)) continue;
88023
88031
  const decision = captureDecision(cwd, options.capture ?? captureEverythingUnresolved);
88024
88032
  if (!decision.capture) {
@@ -88028,14 +88036,14 @@ function collectCursorTranscriptFiles(options, isDatabaseSession, result, databa
88028
88036
  const projects = path7.join(home, ".cursor", "projects");
88029
88037
  const project = cwd.replace(/[^a-zA-Z0-9]/g, "-").replace(/^-+/, "");
88030
88038
  const base = path7.join(projects, project, "agent-transcripts");
88031
- const basename2 = `${hint.sessionId}.jsonl`;
88032
- const allowed = [path7.join(base, basename2), path7.join(base, hint.sessionId, basename2)];
88033
- 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)) {
88034
88042
  result.skippedUnreadable++;
88035
88043
  continue;
88036
88044
  }
88037
- const stat = lstatSync2(hint.transcriptPath);
88038
- 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))) {
88039
88047
  result.skippedUnreadable++;
88040
88048
  continue;
88041
88049
  }
@@ -88046,7 +88054,7 @@ function collectCursorTranscriptFiles(options, isDatabaseSession, result, databa
88046
88054
  }
88047
88055
  const common = {
88048
88056
  source: "cursor",
88049
- externalId: hint.sessionId,
88057
+ externalId: record2.sessionId,
88050
88058
  status: (options.now ?? Date.now()) - stat.mtimeMs < ACTIVE_GRACE_MS ? "active" : "inactive",
88051
88059
  ...decision.repoFullName && { repoFullName: decision.repoFullName },
88052
88060
  capturedVia: decision.via
@@ -88055,15 +88063,15 @@ function collectCursorTranscriptFiles(options, isDatabaseSession, result, databa
88055
88063
  result.sessions.push({
88056
88064
  cwd,
88057
88065
  mtimeMs: stat.mtimeMs,
88058
- transcriptPath: hint.transcriptPath,
88066
+ transcriptPath: record2.transcriptPath,
88059
88067
  item: common
88060
88068
  });
88061
88069
  continue;
88062
88070
  }
88063
- const spool = path7.join(spoolDir, spoolName(hint.sessionId));
88064
- const spoolStat = statIfExists(spool);
88065
- const cached3 = spoolStat !== null && spoolStat.size <= maxBytes && Math.abs(spoolStat.mtimeMs - stat.mtimeMs) < 1 ? spoolMetaSchema.safeParse(readFirstRecord(spool, () => true)) : null;
88066
- 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;
88067
88075
  let title;
88068
88076
  let omissions;
88069
88077
  let unhandled = [];
@@ -88072,7 +88080,7 @@ function collectCursorTranscriptFiles(options, isDatabaseSession, result, databa
88072
88080
  omissions = cached3.data.omissions;
88073
88081
  unhandled = cached3.data.unhandled ?? [];
88074
88082
  } else {
88075
- const raw = readBounded(hint.transcriptPath, maxBytes);
88083
+ const raw = readBounded(record2.transcriptPath, maxBytes);
88076
88084
  if (raw === null) {
88077
88085
  result.skippedTooLarge++;
88078
88086
  continue;
@@ -88093,40 +88101,40 @@ function collectCursorTranscriptFiles(options, isDatabaseSession, result, databa
88093
88101
  format: "cursor-local-transcript",
88094
88102
  version: 1,
88095
88103
  preparationVersion: CURSOR_TRANSCRIPT_PREPARATION_VERSION,
88096
- composerId: hint.sessionId,
88104
+ composerId: record2.sessionId,
88097
88105
  workspacePath: cwd,
88098
88106
  nativeSourceSize: stat.size,
88099
88107
  ...title !== void 0 && { name: title },
88100
- ...hint.model && { model: hint.model },
88108
+ ...record2.model && { model: record2.model },
88101
88109
  ...Object.keys(written).length > 0 && { omissions: written },
88102
88110
  ...prepared.unhandled.length > 0 && { unhandled: prepared.unhandled }
88103
88111
  };
88104
88112
  };
88105
- const body = spoolBody(metaFor, prepared.lines, maxBytes);
88113
+ const body = extractedTranscriptBody(metaFor, prepared.lines, maxBytes);
88106
88114
  if (body === null) {
88107
88115
  result.skippedTooLarge++;
88108
88116
  continue;
88109
88117
  }
88110
- mkdirSync4(spoolDir, { recursive: true, mode: 448 });
88111
- if (spoolStat !== null) chmodSync3(spool, 384);
88112
- writeFileSync3(spool, body, { mode: 384 });
88113
- 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);
88114
88122
  omissions = Object.keys(written).length > 0 ? written : void 0;
88115
88123
  unhandled = prepared.unhandled;
88116
88124
  }
88117
88125
  if (hasCaptureGaps(omissions)) result.partiallyInterpreted++;
88118
- for (const observation of unhandledObservations(unhandled, hint.sessionId)) {
88126
+ for (const observation of unhandledObservations(unhandled, record2.sessionId)) {
88119
88127
  observeStructure(options.onStructureDiagnostic, observation);
88120
88128
  }
88121
88129
  result.sessions.push({
88122
88130
  cwd,
88123
88131
  mtimeMs: stat.mtimeMs,
88124
- transcriptPath: spool,
88132
+ transcriptPath: transcriptUploadSourcePath,
88125
88133
  item: {
88126
88134
  ...common,
88127
88135
  endedAt: stat.mtime.toISOString(),
88128
88136
  repoDir: path7.basename(cwd),
88129
- ...hint.model && { model: hint.model },
88137
+ ...record2.model && { model: record2.model },
88130
88138
  ...title !== void 0 && { title }
88131
88139
  }
88132
88140
  });
@@ -88290,7 +88298,7 @@ function observedSessionEnd(entry, mtimeMs) {
88290
88298
  return entry.at;
88291
88299
  }
88292
88300
 
88293
- // ../../apps/collector/src/providerHooks.ts
88301
+ // ../../apps/collector/src/harnessHooks.ts
88294
88302
  import {
88295
88303
  existsSync as existsSync5,
88296
88304
  lstatSync as lstatSync3,
@@ -88306,26 +88314,39 @@ var SESSION_CHANGE_HOOK = "powerplant-session-change.mjs";
88306
88314
  var CODEX_TRANSCRIPT_HOOK = "powerplant-codex-transcript.mjs";
88307
88315
  var CURSOR_TRANSCRIPT_HOOK = "powerplant-cursor-transcript.mjs";
88308
88316
  var SESSION_CHANGE_FILENAME = "session-change";
88317
+ var SUPPORTED_HOOK_HARNESSES = ["codex-local", "cursor-local"];
88309
88318
  var jsonObject = external_exports.record(external_exports.string(), external_exports.json());
88310
- function providerEvents(provider) {
88311
- 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
+ }
88312
88328
  }
88313
- function providerConfigPath(provider, home) {
88314
- return path10.join(home, `.${provider}`, "hooks.json");
88329
+ function harnessEvents(harness) {
88330
+ return nativeHarnessName(harness) === "cursor" ? ["sessionStart", "sessionEnd", "stop"] : ["SessionStart", "SessionEnd", "Stop"];
88331
+ }
88332
+ function harnessHookConfigPath(harness, home) {
88333
+ const nativeName = nativeHarnessName(harness);
88334
+ return path10.join(home, `.${nativeName}`, "hooks.json");
88315
88335
  }
88316
88336
  var UNUSABLE_CONFIG = "hooks.json contains invalid JSON or an unsupported hook structure";
88317
88337
  var NON_REGULAR_CONFIG = "hooks.json is not a regular file; configure the hook manually";
88318
- function providerHookCommands(provider, text) {
88338
+ function harnessHookCommands(harness, text) {
88319
88339
  try {
88340
+ const nativeName = nativeHarnessName(harness);
88320
88341
  const settings = jsonObject.parse(text === null ? {} : JSON.parse(text));
88321
- if (provider === "cursor" && settings.version !== void 0 && settings.version !== 1) {
88342
+ if (nativeName === "cursor" && settings.version !== void 0 && settings.version !== 1) {
88322
88343
  return { kind: "unusable", reason: "Unsupported Cursor hooks version" };
88323
88344
  }
88324
88345
  const hooks = jsonObject.parse(settings.hooks === void 0 ? {} : settings.hooks);
88325
88346
  const commands = [];
88326
- for (const event of providerEvents(provider)) {
88347
+ for (const event of harnessEvents(harness)) {
88327
88348
  const entries = external_exports.array(jsonObject).parse(hooks[event] === void 0 ? [] : hooks[event]);
88328
- 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));
88329
88350
  const ours = handlers2.filter(isOurHook);
88330
88351
  if (ours.length === 0) return { kind: "missing" };
88331
88352
  for (const hook of ours) {
@@ -88341,20 +88362,21 @@ function isOurHook(hook) {
88341
88362
  const command = hook.command;
88342
88363
  return typeof command === "string" && [SESSION_CHANGE_HOOK, CODEX_TRANSCRIPT_HOOK, CURSOR_TRANSCRIPT_HOOK].some((script) => command.endsWith(`/${script}'`) || command.endsWith(`/${script}"`));
88343
88364
  }
88344
- function mergeProviderHooks(provider, text, command) {
88365
+ function mergeHarnessHooks(harness, text, command) {
88345
88366
  try {
88367
+ const nativeName = nativeHarnessName(harness);
88346
88368
  const settings = jsonObject.parse(text === null ? {} : JSON.parse(text));
88347
- if (provider === "cursor" && settings.version !== void 0 && settings.version !== 1) {
88369
+ if (nativeName === "cursor" && settings.version !== void 0 && settings.version !== 1) {
88348
88370
  return { action: "skip", reason: "Unsupported Cursor hooks version" };
88349
88371
  }
88350
88372
  const hooks = jsonObject.parse(settings.hooks === void 0 ? {} : settings.hooks);
88351
- for (const event of providerEvents(provider)) {
88373
+ for (const event of harnessEvents(harness)) {
88352
88374
  const entries = external_exports.array(jsonObject).parse(hooks[event] === void 0 ? [] : hooks[event]);
88353
- const handler = provider === "cursor" ? { command, timeout: 2 } : { type: "command", command, timeout: 2 };
88354
- 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] };
88355
88377
  const kept = [];
88356
88378
  for (const entry of entries) {
88357
- if (provider === "cursor") {
88379
+ if (nativeName === "cursor") {
88358
88380
  if (!isOurHook(entry)) kept.push(entry);
88359
88381
  continue;
88360
88382
  }
@@ -88365,7 +88387,7 @@ function mergeProviderHooks(provider, text, command) {
88365
88387
  }
88366
88388
  hooks[event] = [...kept, ours];
88367
88389
  }
88368
- const next = { ...settings, ...provider === "cursor" ? { version: 1 } : {}, hooks };
88390
+ const next = { ...settings, ...nativeName === "cursor" ? { version: 1 } : {}, hooks };
88369
88391
  if (JSON.stringify(next) === JSON.stringify(settings)) return { action: "unchanged" };
88370
88392
  return { action: "write", text: `${JSON.stringify(next, null, 2)}
88371
88393
  ` };
@@ -88376,42 +88398,47 @@ function mergeProviderHooks(provider, text, command) {
88376
88398
  function shellQuote2(value) {
88377
88399
  return `'${value.replaceAll("'", "'\\''")}'`;
88378
88400
  }
88379
- function installProviderHook(provider, home) {
88380
- const configDir = path10.join(home, `.${provider}`);
88381
- if (!existsSync5(configDir)) return { provider, settings: "absent" };
88382
- 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);
88383
88406
  let original = null;
88384
88407
  let mode = 384;
88385
88408
  try {
88386
88409
  const stat = lstatSync3(configPath);
88387
- 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
+ }
88388
88413
  mode = stat.mode & 511;
88389
88414
  original = readFileSync6(configPath, "utf8");
88390
88415
  } catch (error63) {
88391
88416
  if (!(error63 instanceof Error && "code" in error63 && error63.code === "ENOENT")) throw error63;
88392
88417
  }
88393
88418
  const hookDir = path10.join(home, ".config", "powerplant", "hooks");
88394
- const script = provider === "cursor" ? CURSOR_TRANSCRIPT_HOOK : CODEX_TRANSCRIPT_HOOK;
88419
+ const script = nativeName === "cursor" ? CURSOR_TRANSCRIPT_HOOK : CODEX_TRANSCRIPT_HOOK;
88395
88420
  const scriptPath = path10.join(hookDir, script);
88396
- const merged = mergeProviderHooks(
88397
- provider,
88421
+ const merged = mergeHarnessHooks(
88422
+ harness,
88398
88423
  original,
88399
88424
  `${shellQuote2(process.execPath)} ${shellQuote2(scriptPath)}`
88400
88425
  );
88401
- 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
+ }
88402
88429
  mkdirSync6(hookDir, { recursive: true });
88403
88430
  writeAtomic(scriptPath, readFileSync6(hookSourcePath(script), "utf8"), 448);
88404
88431
  if (merged.action === "write") {
88405
88432
  const current = existsSync5(configPath) ? readFileSync6(configPath, "utf8") : null;
88406
88433
  if (current !== original)
88407
88434
  return {
88408
- provider,
88435
+ provider: nativeName,
88409
88436
  settings: "skip",
88410
88437
  detail: "hooks.json changed during installation; restart the collector to retry"
88411
88438
  };
88412
88439
  writeAtomic(configPath, merged.text, mode);
88413
88440
  }
88414
- return { provider, settings: merged.action };
88441
+ return { provider: nativeName, settings: merged.action };
88415
88442
  }
88416
88443
  function writeAtomic(file2, text, mode) {
88417
88444
  const temporary = `${file2}.${randomUUID()}.tmp`;
@@ -88422,13 +88449,13 @@ function writeAtomic(file2, text, mode) {
88422
88449
  rmSync4(temporary, { force: true });
88423
88450
  }
88424
88451
  }
88425
- function installProviderHooks(home) {
88426
- return ["codex", "cursor"].map((provider) => {
88452
+ function installHarnessHooks(home) {
88453
+ return SUPPORTED_HOOK_HARNESSES.map((harness) => {
88427
88454
  try {
88428
- return installProviderHook(provider, home);
88455
+ return installHarnessHook(harness, home);
88429
88456
  } catch (error63) {
88430
88457
  return {
88431
- provider,
88458
+ provider: nativeHarnessName(harness),
88432
88459
  settings: "skip",
88433
88460
  detail: error63 instanceof Error ? error63.message : String(error63)
88434
88461
  };
@@ -88440,7 +88467,7 @@ function installProviderHooks(home) {
88440
88467
  var HOOK_OBSERVATION_GRACE_MS = 2 * 6e4;
88441
88468
  var hookHealthSchema = external_exports.object({
88442
88469
  harness: hookHarnessSchema,
88443
- /** What this daemon's boot-time install did (installCollectorHooks / installProviderHooks). */
88470
+ /** What this daemon's boot-time install did (installCollectorHooks / installHarnessHooks). */
88444
88471
  install: hookInstallOutcomeSchema,
88445
88472
  /** unverified: no in-scope session started since the install has been
88446
88473
  * judged yet. firing: one carried a hook record. silent: one had none. */
@@ -88469,7 +88496,10 @@ var HARNESS_NAMES = {
88469
88496
  cursor: "Cursor"
88470
88497
  };
88471
88498
  function configPathOf(harness, home) {
88472
- 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";
88473
88503
  }
88474
88504
  function commandPaths(command) {
88475
88505
  const paths = [];
@@ -88504,7 +88534,7 @@ function inspectHookConfig(harness, home) {
88504
88534
  detail: error63 instanceof Error ? error63.message : String(error63)
88505
88535
  };
88506
88536
  }
88507
- const read = harness === "claude" ? collectorHookCommands(text) : providerHookCommands(harness, text);
88537
+ const read = harness === "claude" ? collectorHookCommands(text) : harnessHookCommands(localHookHarness(harness), text);
88508
88538
  if (read.kind === "unusable") return { kind: "config-unusable", configPath, detail: read.reason };
88509
88539
  if (read.kind === "missing") return { kind: "entries-missing", configPath };
88510
88540
  for (const command of read.commands) {
@@ -89406,7 +89436,7 @@ async function uploadPreviewRegistration(apiUrl, machineToken, registration) {
89406
89436
  function encodeRawTranscript(text) {
89407
89437
  return gzipSync(redactJsonl(text));
89408
89438
  }
89409
- 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) {
89410
89440
  const summary = {
89411
89441
  uploaded: 0,
89412
89442
  skippedTooLarge: 0,
@@ -89417,18 +89447,19 @@ async function uploadRawSessions(apiUrl, machineToken, collected, readTranscript
89417
89447
  };
89418
89448
  const initiallyShipped = new Map(shipped);
89419
89449
  let rateLimited;
89420
- for (const session of collected) {
89450
+ for (const session of transcriptUploadSources) {
89421
89451
  summary.uploaded += await progress?.beforeUpload?.() ?? 0;
89422
- const acknowledged = shipped?.get(session.transcriptPath);
89423
- 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)) {
89424
89455
  summary.skippedUnchanged += 1;
89425
89456
  continue;
89426
89457
  }
89427
89458
  try {
89428
- const body = encodeRawTranscript(readTranscript(session.transcriptPath));
89459
+ const body = encodeRawTranscript(readTranscript(transcriptUploadSourcePath));
89429
89460
  if (body.byteLength > MAX_RAW_UPLOAD_BYTES) {
89430
89461
  summary.skippedTooLarge += 1;
89431
- shipped?.set(session.transcriptPath, session.mtimeMs);
89462
+ shipped?.set(transcriptUploadSourcePath, session.mtimeMs);
89432
89463
  continue;
89433
89464
  }
89434
89465
  const query = `source=${session.item.source}&externalId=${encodeURIComponent(session.item.externalId)}`;
@@ -89461,7 +89492,7 @@ async function uploadRawSessions(apiUrl, machineToken, collected, readTranscript
89461
89492
  }
89462
89493
  summary.uploaded += 1;
89463
89494
  progress?.onUploaded?.();
89464
- shipped?.set(session.transcriptPath, session.mtimeMs);
89495
+ shipped?.set(transcriptUploadSourcePath, session.mtimeMs);
89465
89496
  } catch (error63) {
89466
89497
  if (error63 instanceof RateLimitedError) rateLimited ??= error63;
89467
89498
  summary.failed.push(
@@ -91332,7 +91363,7 @@ function startWatch(config2, homeDir, runtime = { version: "checkout" }) {
91332
91363
  );
91333
91364
  reportCollectorError(error63, "watch-hook-install");
91334
91365
  }
91335
- for (const hook of installProviderHooks(home)) {
91366
+ for (const hook of installHarnessHooks(home)) {
91336
91367
  hookInstalls[hook.provider] = hook.settings;
91337
91368
  if (hook.settings === "absent") continue;
91338
91369
  if (hook.settings === "skip") {
@@ -93378,7 +93409,7 @@ import { accessSync, constants as constants2, readFileSync as readFileSync12 } f
93378
93409
  import path25 from "node:path";
93379
93410
 
93380
93411
  // src/version.ts
93381
- var CLI_VERSION = true ? "0.4.12" : "0.0.0-checkout";
93412
+ var CLI_VERSION = true ? "0.4.13" : "0.0.0-checkout";
93382
93413
  var CLI_RELEASE_BUILD = true ? true : false;
93383
93414
  var CLI_PACKAGE_NAME = "@usepowerplant/cli";
93384
93415
 
@@ -93498,7 +93529,13 @@ function realInspectEnvironment() {
93498
93529
 
93499
93530
  // src/update.ts
93500
93531
  var updateProgressSchema = external_exports.object({
93501
- 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"]),
93502
93539
  /** The release aimed at; null when the registry couldn't be asked (its latest). */
93503
93540
  target: external_exports.string().nullable(),
93504
93541
  startedAt: external_exports.string(),
@@ -93538,7 +93575,7 @@ function readUpdateProgress(readTextFile, file2 = updateProgressPath()) {
93538
93575
  }
93539
93576
  }
93540
93577
  function updateProgressView(progress, cliVersion2, now) {
93541
- if (progress === null) return { kind: "none" };
93578
+ if (progress === null || progress.state === "up-to-date") return { kind: "none" };
93542
93579
  const reached = progress.target === null ? null : compareReleaseVersions(cliVersion2, progress.target);
93543
93580
  if (reached === 0 || reached === 1) return { kind: "none" };
93544
93581
  if (progress.state === "failed") {
@@ -93571,13 +93608,29 @@ async function runUpdate(deps) {
93571
93608
  const reserved = running?.state === "installing" ? running.target : null;
93572
93609
  const attempts = (release2) => running?.target === release2 && running.attempts !== void 0 ? { attempts: running.attempts } : {};
93573
93610
  deps.writeProgress({ state: "installing", target: reserved, startedAt, pid: deps.pid, ...attempts(reserved) });
93574
- const target = await deps.resolveTarget() ?? reserved;
93611
+ const resolved = await deps.resolveTarget();
93612
+ if (resolved !== null) deps.recordAvailableVersion(resolved);
93613
+ const target = resolved ?? reserved;
93575
93614
  if (target === null && deps.targetRequired) {
93576
93615
  deps.clearProgress();
93577
93616
  throw new ExpectedCliError(
93578
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."
93579
93618
  );
93580
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
+ }
93581
93634
  deps.writeProgress({ state: "installing", target, startedAt, pid: deps.pid, ...attempts(target) });
93582
93635
  const { bin, args } = updateCommand(manager, target, inspection.prefix, deps.channel);
93583
93636
  deps.log(`Updating ${CLI_PACKAGE_NAME}${target === null ? "" : ` to ${target}`} via ${manager}\u2026`);
@@ -94422,6 +94475,7 @@ function renderMenu(i) {
94422
94475
  logPath: updateLogPath()
94423
94476
  }
94424
94477
  },
94478
+ ...i.update.progress?.state === "up-to-date" && { cliUpToDateAt: i.update.progress.finishedAt },
94425
94479
  ...i.desktopDiagnostic && { desktopDiagnostic: i.desktopDiagnostic },
94426
94480
  appEnv: i.config.appEnv,
94427
94481
  cliVersion: i.cliVersion,
@@ -94725,14 +94779,17 @@ async function cachedLatestVersion(options) {
94725
94779
  }
94726
94780
  }
94727
94781
  const version2 = await options.fetchLatest();
94782
+ writeVersionCache({ ...options, version: version2 });
94783
+ return version2;
94784
+ }
94785
+ function writeVersionCache(options) {
94728
94786
  try {
94729
94787
  options.writeTextFile(
94730
94788
  options.cacheFile,
94731
- JSON.stringify({ checkedAt: new Date(options.now).toISOString(), version: version2 })
94789
+ JSON.stringify({ checkedAt: new Date(options.now).toISOString(), version: options.version })
94732
94790
  );
94733
94791
  } catch {
94734
94792
  }
94735
- return version2;
94736
94793
  }
94737
94794
 
94738
94795
  // src/liveTerminalSessions.ts
@@ -96111,7 +96168,14 @@ The access you granted these agents stays on your Powerplant account until you r
96111
96168
  }
96112
96169
  await runUpdate({
96113
96170
  inspection,
96171
+ cliVersion: CLI_VERSION,
96114
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
+ }),
96115
96179
  channel: releaseSource(config2.appEnv) === "npm" ? "next" : "latest",
96116
96180
  targetRequired: releaseSource(config2.appEnv) === "promoted",
96117
96181
  claimsReservation: process.env[UPDATE_CHILD_ENV] === "1",
@@ -96398,4 +96462,4 @@ undici/lib/web/websocket/frame.js:
96398
96462
  (*! ws. MIT License. Einar Otto Stangvik <einaros@gmail.com> *)
96399
96463
  */
96400
96464
 
96401
- //# debugId=7d73a97a-938c-5f0f-8b6b-c4076b82abad
96465
+ //# debugId=66f773d5-e0b2-5538-a184-3ee15c3a469a
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usepowerplant/cli",
3
- "version": "0.4.12",
3
+ "version": "0.4.13",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },