@usepowerplant/cli 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +401 -129
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -36252,7 +36252,7 @@ ${captureLines}` : capture.stack;
36252
36252
  });
36253
36253
 
36254
36254
  // src/cli.ts
36255
- import { spawnSync as spawnSync4 } from "node:child_process";
36255
+ import { spawnSync as spawnSync5 } from "node:child_process";
36256
36256
  import { existsSync as existsSync7, realpathSync as realpathSync2 } from "node:fs";
36257
36257
  import { parseArgs } from "node:util";
36258
36258
 
@@ -78652,7 +78652,7 @@ import os6 from "node:os";
78652
78652
  // ../../apps/collector/src/http.ts
78653
78653
  var import_undici = __toESM(require_undici(), 1);
78654
78654
  var CLI_VERSION_HEADER = "x-powerplant-cli-version";
78655
- var cliVersion = true ? "0.3.0" : "0.0.0-checkout";
78655
+ var cliVersion = true ? "0.3.2" : "0.0.0-checkout";
78656
78656
  function cliVersionHeaders() {
78657
78657
  return { [CLI_VERSION_HEADER]: cliVersion, "user-agent": `powerplant-cli/${cliVersion}` };
78658
78658
  }
@@ -78729,30 +78729,78 @@ import os5 from "node:os";
78729
78729
  import path3 from "node:path";
78730
78730
 
78731
78731
  // ../../apps/collector/src/gitOwner.ts
78732
+ import { execFileSync as execFileSync2 } from "node:child_process";
78732
78733
  import { readFileSync as readFileSync3, statSync } from "node:fs";
78733
78734
  import os4 from "node:os";
78734
78735
  import path2 from "node:path";
78735
- function parseGithubRepo(remoteUrl) {
78736
+ var GITHUB_HOSTS = /* @__PURE__ */ new Set(["github.com", "ssh.github.com"]);
78737
+ function parseRemoteUrl(remoteUrl) {
78736
78738
  const trimmed = remoteUrl.trim();
78737
- const separator = trimmed.indexOf(":");
78738
- if (separator > 0) {
78739
- const authority = trimmed.slice(0, separator).split("@").at(-1)?.toLowerCase();
78740
- const [owner, rawRepo, extra] = trimmed.slice(separator + 1).split("/");
78741
- if (authority === "github.com" && owner && rawRepo && extra === void 0) {
78742
- const repo = rawRepo.toLowerCase().endsWith(".git") ? rawRepo.slice(0, -4) : rawRepo;
78743
- return `${owner.toLowerCase()}/${repo}`;
78744
- }
78739
+ const colon = trimmed.indexOf(":");
78740
+ if (!trimmed.includes("://") && colon > 0 && !trimmed.slice(0, colon).includes("/")) {
78741
+ const host = trimmed.slice(0, colon).split("@").at(-1) ?? "";
78742
+ return { host: host.toLowerCase(), path: trimmed.slice(colon + 1), overSsh: true };
78745
78743
  }
78746
78744
  try {
78747
78745
  const url2 = new URL(trimmed);
78748
- if (url2.hostname.toLowerCase() !== "github.com") return null;
78749
- const [owner, repo] = url2.pathname.split("/").filter(Boolean);
78750
- if (!owner || !repo) return null;
78751
- return `${owner.toLowerCase()}/${repo.replace(/\.git$/i, "")}`;
78746
+ if (!url2.hostname) return null;
78747
+ const scheme = url2.protocol.slice(0, -1).toLowerCase();
78748
+ return {
78749
+ host: url2.hostname.toLowerCase(),
78750
+ path: url2.pathname,
78751
+ overSsh: scheme === "ssh" || scheme === "git+ssh"
78752
+ };
78752
78753
  } catch {
78753
78754
  return null;
78754
78755
  }
78755
78756
  }
78757
+ function parseGithubRepo(remoteUrl, sshHostnameOf = sshEffectiveHostname) {
78758
+ const remote = parseRemoteUrl(remoteUrl);
78759
+ if (!remote) return null;
78760
+ const host = remote.overSsh && !GITHUB_HOSTS.has(remote.host) ? sshHostnameOf(remote.host)?.toLowerCase() : remote.host;
78761
+ if (!host || !GITHUB_HOSTS.has(host)) return null;
78762
+ const [owner, rawRepo, extra] = remote.path.split("/").filter(Boolean);
78763
+ if (!owner || !rawRepo || extra !== void 0) return null;
78764
+ return `${owner.toLowerCase()}/${rawRepo.replace(/\.git$/i, "")}`;
78765
+ }
78766
+ var SSH_PROBE_TIMEOUT_MS = 2e3;
78767
+ var SSH_PROBE_CACHE_TTL_MS = 5 * 6e4;
78768
+ var SSH_HOST_PATTERN = /^[a-z0-9][a-z0-9._-]*$/i;
78769
+ var sshProbeCache = /* @__PURE__ */ new Map();
78770
+ function sshEffectiveHostname(host, options = {}) {
78771
+ if (!SSH_HOST_PATTERN.test(host)) return null;
78772
+ const key = `${options.configFile ?? ""}\0${host}`;
78773
+ const cached3 = sshProbeCache.get(key);
78774
+ const now = Date.now();
78775
+ if (cached3 && cached3.expiresAt > now) return cached3.hostname;
78776
+ const hostname4 = probeSshHostname(host, options);
78777
+ sshProbeCache.set(key, { hostname: hostname4, expiresAt: now + SSH_PROBE_CACHE_TTL_MS });
78778
+ return hostname4;
78779
+ }
78780
+ function probeSshHostname(host, options) {
78781
+ try {
78782
+ const dump = execFileSync2(
78783
+ "ssh",
78784
+ ["-G", ...options.configFile ? ["-F", options.configFile] : [], host],
78785
+ {
78786
+ encoding: "utf8",
78787
+ timeout: options.timeoutMs ?? SSH_PROBE_TIMEOUT_MS,
78788
+ stdio: ["ignore", "pipe", "ignore"],
78789
+ windowsHide: true
78790
+ }
78791
+ );
78792
+ return hostnameFromSshConfigDump(dump);
78793
+ } catch {
78794
+ return null;
78795
+ }
78796
+ }
78797
+ function hostnameFromSshConfigDump(dump) {
78798
+ for (const line of dump.split("\n")) {
78799
+ const match = /^hostname\s+(\S+)$/i.exec(line.trim());
78800
+ if (match?.[1]) return match[1];
78801
+ }
78802
+ return null;
78803
+ }
78756
78804
  function parseOriginUrl(configText) {
78757
78805
  let inOrigin = false;
78758
78806
  for (const raw of configText.split("\n")) {
@@ -78818,17 +78866,17 @@ function findGitConfigPath(cwd, environment = {}) {
78818
78866
  const dirs = resolveGitDirs(cwd, environment);
78819
78867
  return dirs ? path2.join(dirs.commonDir, "config") : null;
78820
78868
  }
78821
- function resolveGithubRepo(cwd, environment = {}) {
78869
+ function resolveGithubRepo(cwd, environment = {}, sshHostnameOf = sshEffectiveHostname) {
78822
78870
  const configPath = findGitConfigPath(cwd, environment);
78823
78871
  const configText = configPath ? readIfExists(configPath) : null;
78824
78872
  const remoteUrl = configText ? parseOriginUrl(configText) : null;
78825
- return remoteUrl ? parseGithubRepo(remoteUrl) : null;
78873
+ return remoteUrl ? parseGithubRepo(remoteUrl, sshHostnameOf) : null;
78826
78874
  }
78827
- function repoResolver(environment = {}) {
78875
+ function repoResolver(environment = {}, sshHostnameOf = sshEffectiveHostname) {
78828
78876
  const cache = /* @__PURE__ */ new Map();
78829
78877
  return (cwd) => {
78830
78878
  const key = path2.resolve(cwd);
78831
- if (!cache.has(key)) cache.set(key, resolveGithubRepo(key, environment));
78879
+ if (!cache.has(key)) cache.set(key, resolveGithubRepo(key, environment, sshHostnameOf));
78832
78880
  return cache.get(key);
78833
78881
  };
78834
78882
  }
@@ -78860,17 +78908,18 @@ var captureEverythingUnresolved = {
78860
78908
  function captureDecision(cwd, policy) {
78861
78909
  switch (policy.mode) {
78862
78910
  case "scope":
78863
- return cwd !== void 0 && isWithinScope(cwd, policy.scopePaths, policy.environment) ? memberChoice(cwd, policy) : { capture: false };
78911
+ if (cwd === void 0) return { capture: false, reason: "no-cwd" };
78912
+ return isWithinScope(cwd, policy.scopePaths, policy.environment) ? memberChoice(cwd, policy) : { capture: false, reason: "outside-policy" };
78864
78913
  case "org-repos": {
78865
- if (cwd === void 0) return { capture: false };
78914
+ if (cwd === void 0) return { capture: false, reason: "no-cwd" };
78866
78915
  if (policy.memberFolders.length > 0 && isWithinScope(cwd, policy.memberFolders, policy.environment)) {
78867
78916
  return memberChoice(cwd, policy);
78868
78917
  }
78869
78918
  const repoFullName = policy.repoOf(cwd);
78870
- if (repoFullName === null) return { capture: false };
78919
+ if (repoFullName === null) return { capture: false, reason: "no-github-repo" };
78871
78920
  const repo = repoFullName.toLowerCase();
78872
78921
  const admitted = policy.allowedRepos !== null ? policy.allowedRepos.includes(repo) : policy.allowedOwners.length > 0 && policy.allowedOwners.includes(ownerOfRepo(repo));
78873
- return admitted ? { capture: true, via: "org-policy", repoFullName } : { capture: false };
78922
+ return admitted ? { capture: true, via: "org-policy", repoFullName } : { capture: false, reason: "outside-policy" };
78874
78923
  }
78875
78924
  }
78876
78925
  }
@@ -78893,7 +78942,7 @@ function describeCapturePolicy(policy) {
78893
78942
  case "scope":
78894
78943
  return `sessions under ${policy.scopePaths.join(", ")}`;
78895
78944
  case "org-repos": {
78896
- const repoPart = policy.allowedRepos !== null ? policy.allowedRepos.length > 0 ? `sessions in the org's synced repos: ${listSome(policy.allowedRepos)}` : "the org's synced repo list is empty \u2014 no repo-based uploads" : policy.allowedOwners.length > 0 ? `sessions in GitHub repos owned by ${policy.allowedOwners.join(", ")}` : "org-owned GitHub repos \u2014 but the org has no GitHub integration, so nothing uploads";
78945
+ const repoPart = policy.allowedRepos !== null ? policy.allowedRepos.length > 0 ? `sessions in the org's allowlisted repos: ${listSome(policy.allowedRepos)}` : "the org's repo allowlist is empty \u2014 no repo-based uploads" : policy.allowedOwners.length > 0 ? `sessions in GitHub repos owned by ${policy.allowedOwners.join(", ")}` : "org-owned GitHub repos \u2014 but the org has no GitHub integration, so nothing uploads";
78897
78946
  return policy.memberFolders.length > 0 ? `${repoPart}, plus folders you added: ${policy.memberFolders.join(", ")}` : repoPart;
78898
78947
  }
78899
78948
  }
@@ -78914,10 +78963,17 @@ function describeStoredCapture(credentials) {
78914
78963
  repoOf: () => null
78915
78964
  });
78916
78965
  }
78917
- function describeSkipReason(policy, platform2 = process.platform) {
78966
+ function countCaptureSkip(skips, reason) {
78967
+ if (reason === "outside-policy") skips.skippedOutOfScope += 1;
78968
+ else skips.skippedNoRepo += 1;
78969
+ }
78970
+ function describeSkipReason(policy) {
78918
78971
  if (policy.mode !== "org-repos") return "outside the sync boundary, stayed local";
78919
- const boundary = policy.allowedRepos !== null ? "outside the org's synced repos" : "not in org-owned repos";
78920
- return platform2 === "darwin" ? `${boundary} or in macOS-protected folders, stayed local (run \`powerplant init\` to review Git repository scanning)` : `${boundary}, stayed local`;
78972
+ return policy.allowedRepos !== null ? "outside the org's allowlisted repos, stayed local" : "not in org-owned repos, stayed local";
78973
+ }
78974
+ function describeNoRepoSkipReason(policy, platform2 = process.platform) {
78975
+ if (policy.mode !== "org-repos") return "with no working directory recorded, stayed local";
78976
+ return platform2 === "darwin" ? "with no readable GitHub repo or in macOS-protected folders, stayed local (run `powerplant init` to review Git repository scanning)" : "with no readable GitHub repo, stayed local";
78921
78977
  }
78922
78978
  function normalizeScopePath(raw) {
78923
78979
  const expanded = raw === "~" || raw.startsWith("~/") ? path3.join(os5.homedir(), raw.slice(1)) : raw;
@@ -78939,6 +78995,37 @@ function isPathWithinScopes(resolved, scopePaths) {
78939
78995
  );
78940
78996
  }
78941
78997
 
78998
+ // ../../apps/collector/src/syncWindow.ts
78999
+ var STEADY_SYNC_WINDOW_DAYS = 3;
79000
+ var DEEP_FIRST_SYNC_DAYS = 90;
79001
+ var DEEP_FIRST_SYNC_MAX_SESSIONS = 100;
79002
+ function resolveSyncWindow(explicitSince, deepSyncedAt) {
79003
+ if (explicitSince !== void 0) {
79004
+ const days = Number(explicitSince);
79005
+ if (!Number.isFinite(days) || days <= 0) {
79006
+ throw new ExpectedCliError(`--since must be a positive number of days, got "${explicitSince}"`);
79007
+ }
79008
+ return { days, deepSweep: false };
79009
+ }
79010
+ if (deepSyncedAt === void 0) return { days: DEEP_FIRST_SYNC_DAYS, deepSweep: true };
79011
+ return { days: STEADY_SYNC_WINDOW_DAYS, deepSweep: false };
79012
+ }
79013
+ var DAY_MS = 24 * 60 * 60 * 1e3;
79014
+ function resolveScanFloor(window2, nowMs, narrowSinceMs) {
79015
+ if (window2.deepSweep || narrowSinceMs === void 0) return nowMs - window2.days * DAY_MS;
79016
+ return narrowSinceMs;
79017
+ }
79018
+ function rawShipFloorMs(appEnv, nowMs, explicitSince) {
79019
+ if (appEnv !== "development" || explicitSince !== void 0) return null;
79020
+ return nowMs - STEADY_SYNC_WINDOW_DAYS * DAY_MS;
79021
+ }
79022
+ function describeScanReach(window2, narrowSinceMs) {
79023
+ if (window2.deepSweep || narrowSinceMs === void 0) {
79024
+ return `from the last ${window2.days} day(s)`;
79025
+ }
79026
+ return `changed since ${new Date(narrowSinceMs).toISOString()}`;
79027
+ }
79028
+
78942
79029
  // ../../apps/collector/src/upload.ts
78943
79030
  import { readFileSync as readFileSync4 } from "node:fs";
78944
79031
  import { gzipSync } from "node:zlib";
@@ -79257,6 +79344,9 @@ async function runEnrollment(config2, options) {
79257
79344
  log3(`Enrolled "${result.machine.name}" (${result.machine.id}) into org "${result.org.name}".`);
79258
79345
  log3(`Machine token stored in ${location}.`);
79259
79346
  log3(`Syncing: ${describeCapturePolicy(policy)}.`);
79347
+ log3(
79348
+ `First sync backfills up to the ${DEEP_FIRST_SYNC_MAX_SESSIONS} most recently active of those sessions from the last ${DEEP_FIRST_SYNC_DAYS} days; later syncs cover the last ${STEADY_SYNC_WINDOW_DAYS} days.`
79349
+ );
79260
79350
  return credentials;
79261
79351
  }
79262
79352
  async function enroll(config2, options, createAuthClient = gotrueAuthClient) {
@@ -79453,7 +79543,7 @@ function addFolder(config2, rawPath, enrollCommand, log3, deps = defaultDeps) {
79453
79543
  });
79454
79544
  const org = credentials.orgName ?? "your org";
79455
79545
  log3(
79456
- credentials.captureMode === "org-repos" ? `Sessions under ${folder} will now upload to ${org} \u2014 even outside the org's synced repos.` : `Added ${folder} to this machine's sync boundary.`
79546
+ credentials.captureMode === "org-repos" ? `Sessions under ${folder} will now upload to ${org} \u2014 even outside the org's allowlisted repos.` : `Added ${folder} to this machine's sync boundary.`
79457
79547
  );
79458
79548
  log3("Takes effect on the next sync pass; run the sync command to apply now.");
79459
79549
  }
@@ -79492,18 +79582,53 @@ import { setImmediate as yieldToEventLoop } from "node:timers/promises";
79492
79582
  import path4 from "node:path";
79493
79583
 
79494
79584
  // ../../apps/collector/src/scan.ts
79495
- import { readdirSync, readFileSync as readFileSync5, statSync as statSync2 } from "node:fs";
79585
+ import { closeSync, openSync, readdirSync, readFileSync as readFileSync5, readSync, statSync as statSync2 } from "node:fs";
79586
+ import { StringDecoder } from "node:string_decoder";
79496
79587
  var ACTIVE_GRACE_MS = SESSION_ACTIVE_WINDOW_MS;
79497
79588
  var jsonObjectSchema = external_exports.record(external_exports.string(), external_exports.unknown());
79498
79589
  function* parseJsonl(text) {
79499
79590
  for (const line of text.split("\n")) {
79500
- if (!line.trim()) continue;
79501
- try {
79502
- const value = JSON.parse(line);
79503
- const parsed = jsonObjectSchema.safeParse(value);
79504
- if (parsed.success) yield parsed.data;
79505
- } catch {
79506
- }
79591
+ const record2 = parseJsonlLine(line);
79592
+ if (record2 !== null) yield record2;
79593
+ }
79594
+ }
79595
+ function parseJsonlLine(line) {
79596
+ if (!line.trim()) return null;
79597
+ try {
79598
+ const value = JSON.parse(line);
79599
+ const parsed = jsonObjectSchema.safeParse(value);
79600
+ return parsed.success ? parsed.data : null;
79601
+ } catch {
79602
+ return null;
79603
+ }
79604
+ }
79605
+ var READ_CHUNK_BYTES = 64 * 1024;
79606
+ function readFirstRecord(file2, matches) {
79607
+ let fd;
79608
+ try {
79609
+ fd = openSync(file2, "r");
79610
+ } catch (error52) {
79611
+ if (error52.code === "ENOENT") return null;
79612
+ throw error52;
79613
+ }
79614
+ const buffer = Buffer.alloc(READ_CHUNK_BYTES);
79615
+ const decoder = new StringDecoder("utf8");
79616
+ let carry = "";
79617
+ try {
79618
+ for (; ; ) {
79619
+ const bytes = readSync(fd, buffer, 0, buffer.length, null);
79620
+ if (bytes === 0) break;
79621
+ const lines = (carry + decoder.write(buffer.subarray(0, bytes))).split("\n");
79622
+ carry = lines.pop() ?? "";
79623
+ for (const line of lines) {
79624
+ const record2 = parseJsonlLine(line);
79625
+ if (record2 !== null && matches(record2)) return record2;
79626
+ }
79627
+ }
79628
+ const last = parseJsonlLine(carry + decoder.end());
79629
+ return last !== null && matches(last) ? last : null;
79630
+ } finally {
79631
+ closeSync(fd);
79507
79632
  }
79508
79633
  }
79509
79634
  function listDirs(dir) {
@@ -79535,6 +79660,10 @@ function readIfExists2(file2) {
79535
79660
  throw error52;
79536
79661
  }
79537
79662
  }
79663
+ var MAX_TRANSCRIPT_FILE_BYTES = 256 * 1024 * 1024;
79664
+ function describeReadError(error52) {
79665
+ return error52 instanceof Error ? error52.message : String(error52);
79666
+ }
79538
79667
 
79539
79668
  // ../../apps/collector/src/lifecycleJournal.ts
79540
79669
  var LIFECYCLE_JOURNAL_FILENAME = "session-lifecycle.jsonl";
@@ -79625,7 +79754,7 @@ async function collectClaudeCodeSessions(options = {}) {
79625
79754
  const capture = options.capture ?? captureEverythingUnresolved;
79626
79755
  const projectsDir = path5.join(claudeDir, "projects");
79627
79756
  const items = [];
79628
- let skippedOutOfScope = 0;
79757
+ const skips = { skippedOutOfScope: 0, skippedNoRepo: 0 };
79629
79758
  const home = path5.dirname(claudeDir);
79630
79759
  const lifecycle = options.lifecycle ?? readLifecycleJournal(home);
79631
79760
  for (const projectDir of listDirs(projectsDir)) {
@@ -79635,18 +79764,34 @@ async function collectClaudeCodeSessions(options = {}) {
79635
79764
  const stat = statIfExists(transcriptPath);
79636
79765
  if (!stat) continue;
79637
79766
  if (options.sinceMs !== void 0 && stat.mtimeMs < options.sinceMs) continue;
79767
+ const sessionId = path5.basename(entry, ".jsonl");
79768
+ const endObservedAt = observedSessionEnd(lifecycle.get(sessionId), stat.mtimeMs);
79769
+ const status = endObservedAt === null && now - stat.mtimeMs < ACTIVE_GRACE_MS ? "active" : "inactive";
79770
+ if (options.locationsOnly) {
79771
+ const cwd = transcriptCwd(transcriptPath);
79772
+ const decision2 = captureDecision(cwd, capture);
79773
+ if (!decision2.capture) {
79774
+ countCaptureSkip(skips, decision2.reason);
79775
+ continue;
79776
+ }
79777
+ items.push({
79778
+ mtimeMs: stat.mtimeMs,
79779
+ transcriptPath,
79780
+ ...cwd && { cwd },
79781
+ item: { source: "claude-code", externalId: sessionId, status, capturedVia: decision2.via }
79782
+ });
79783
+ continue;
79784
+ }
79638
79785
  const text = readIfExists2(transcriptPath);
79639
79786
  if (text === null) continue;
79640
79787
  const summary = parseClaudeCodeTranscript(text);
79641
79788
  if (!summary) continue;
79642
79789
  const decision = captureDecision(summary.cwd, capture);
79643
79790
  if (!decision.capture) {
79644
- skippedOutOfScope += 1;
79791
+ countCaptureSkip(skips, decision.reason);
79645
79792
  continue;
79646
79793
  }
79647
- const sessionId = path5.basename(entry, ".jsonl");
79648
79794
  const usage = addSubagentUsage(summary.usage, path5.join(projectDir, sessionId, "subagents"));
79649
- const endObservedAt = observedSessionEnd(lifecycle.get(sessionId), stat.mtimeMs);
79650
79795
  const endedAt = endObservedAt ?? summary.endedAt;
79651
79796
  items.push({
79652
79797
  mtimeMs: stat.mtimeMs,
@@ -79655,7 +79800,7 @@ async function collectClaudeCodeSessions(options = {}) {
79655
79800
  item: {
79656
79801
  source: "claude-code",
79657
79802
  externalId: sessionId,
79658
- status: endObservedAt === null && now - stat.mtimeMs < ACTIVE_GRACE_MS ? "active" : "inactive",
79803
+ status,
79659
79804
  ...summary.startedAt && { startedAt: summary.startedAt },
79660
79805
  ...endedAt && { endedAt },
79661
79806
  ...summary.model && { model: summary.model },
@@ -79680,9 +79825,16 @@ async function collectClaudeCodeSessions(options = {}) {
79680
79825
  }
79681
79826
  return {
79682
79827
  sessions: items.sort((a, b2) => b2.mtimeMs - a.mtimeMs),
79683
- skippedOutOfScope
79828
+ ...skips
79684
79829
  };
79685
79830
  }
79831
+ function transcriptCwd(transcriptPath) {
79832
+ const record2 = readFirstRecord(
79833
+ transcriptPath,
79834
+ (candidate) => typeof candidate.cwd === "string" && candidate.cwd !== ""
79835
+ );
79836
+ return typeof record2?.cwd === "string" ? record2.cwd : void 0;
79837
+ }
79686
79838
  function toUsage(u) {
79687
79839
  const n = (value) => typeof value === "number" ? value : 0;
79688
79840
  return {
@@ -79765,6 +79917,21 @@ function parseCodexRollout(text) {
79765
79917
  ...typeof git?.repository_url === "string" && repoFullNameFrom(git.repository_url)
79766
79918
  };
79767
79919
  }
79920
+ function parseRolloutFile(rolloutPath) {
79921
+ const text = readIfExists2(rolloutPath);
79922
+ return text === null ? null : parseCodexRollout(text);
79923
+ }
79924
+ function rolloutLocation(rolloutPath) {
79925
+ const record2 = readFirstRecord(rolloutPath, (candidate) => candidate.type === "session_meta");
79926
+ const meta3 = record2?.payload;
79927
+ if (!meta3 || typeof meta3 !== "object" || Array.isArray(meta3)) return { kind: "session", totals: null };
79928
+ const { thread_source: threadSource, cwd } = meta3;
79929
+ return {
79930
+ kind: threadSource === "subagent" ? "subagent" : "session",
79931
+ totals: null,
79932
+ ...typeof cwd === "string" && cwd !== "" && { cwd }
79933
+ };
79934
+ }
79768
79935
  function repoFullNameFrom(url2) {
79769
79936
  const fullName = parseGithubRepo(url2);
79770
79937
  return fullName ? { repoFullName: fullName } : {};
@@ -79802,41 +79969,95 @@ async function collectCodexSessions(options = {}) {
79802
79969
  const sessionsDir = options.codexDir ?? path6.join(os8.homedir(), ".codex", "sessions");
79803
79970
  const now = options.now ?? Date.now();
79804
79971
  const capture = options.capture ?? captureEverythingUnresolved;
79805
- const threadNames = loadThreadNames(sessionsDir);
79972
+ const maxBytes = options.maxTranscriptBytes ?? MAX_TRANSCRIPT_FILE_BYTES;
79806
79973
  const items = [];
79807
- let skippedOutOfScope = 0;
79974
+ const candidates = [];
79975
+ const unreadable = [];
79976
+ const skips = { skippedOutOfScope: 0, skippedNoRepo: 0 };
79808
79977
  let skippedSubagentThreads = 0;
79809
- const collectFrom = async (rootDir, archived) => {
79978
+ let skippedTooLarge = 0;
79979
+ const externalIdOf = (rolloutPath) => path6.basename(rolloutPath, ".jsonl");
79980
+ const admitFrom = async (rootDir, archived) => {
79810
79981
  for (const rolloutPath of rolloutFiles(rootDir)) {
79811
79982
  await yieldToEventLoop2();
79812
79983
  const stat = statIfExists(rolloutPath);
79813
79984
  if (!stat) continue;
79814
79985
  if (options.sinceMs !== void 0 && Math.max(stat.mtimeMs, stat.ctimeMs) < options.sinceMs)
79815
79986
  continue;
79816
- const text = readIfExists2(rolloutPath);
79817
- if (text === null) continue;
79818
- const summary = parseCodexRollout(text);
79819
- if (!summary) continue;
79820
- if (summary.kind === "subagent") {
79821
- skippedSubagentThreads += 1;
79987
+ if (stat.size > maxBytes) {
79988
+ skippedTooLarge += 1;
79822
79989
  continue;
79823
79990
  }
79824
- const decision = captureDecision(summary.cwd, capture);
79825
- if (!decision.capture) {
79826
- skippedOutOfScope += 1;
79827
- continue;
79991
+ try {
79992
+ const location = rolloutLocation(rolloutPath);
79993
+ if (location.kind === "subagent") {
79994
+ skippedSubagentThreads += 1;
79995
+ continue;
79996
+ }
79997
+ const decision = captureDecision(location.cwd, capture);
79998
+ if (!decision.capture) {
79999
+ countCaptureSkip(skips, decision.reason);
80000
+ continue;
80001
+ }
80002
+ if (options.locationsOnly) {
80003
+ items.push({
80004
+ mtimeMs: stat.mtimeMs,
80005
+ transcriptPath: rolloutPath,
80006
+ ...location.cwd && { cwd: location.cwd },
80007
+ item: {
80008
+ source: "codex",
80009
+ externalId: externalIdOf(rolloutPath),
80010
+ status: now - stat.mtimeMs < ACTIVE_GRACE_MS ? "active" : "inactive",
80011
+ archived,
80012
+ capturedVia: decision.via
80013
+ }
80014
+ });
80015
+ continue;
80016
+ }
80017
+ candidates.push({
80018
+ rolloutPath,
80019
+ archived,
80020
+ mtimeMs: stat.mtimeMs,
80021
+ ...location.cwd && { cwd: location.cwd },
80022
+ decision
80023
+ });
80024
+ } catch (error52) {
80025
+ unreadable.push(`${externalIdOf(rolloutPath)}: ${describeReadError(error52)}`);
79828
80026
  }
79829
- const repoDir = summary.repoName ?? (summary.cwd ? path6.basename(summary.cwd) : void 0);
80027
+ }
80028
+ };
80029
+ await admitFrom(sessionsDir, false);
80030
+ await admitFrom(codexArchivedSessionsDir(sessionsDir), true);
80031
+ if (options.locationsOnly) {
80032
+ return {
80033
+ sessions: items.sort((a, b2) => b2.mtimeMs - a.mtimeMs),
80034
+ ...skips,
80035
+ skippedSubagentThreads,
80036
+ skippedTooLarge,
80037
+ unreadable,
80038
+ heldBack: 0
80039
+ };
80040
+ }
80041
+ candidates.sort((a, b2) => b2.mtimeMs - a.mtimeMs);
80042
+ const chosen = options.maxSessions === void 0 ? candidates : candidates.slice(0, options.maxSessions);
80043
+ const threadNames = chosen.length > 0 ? loadThreadNames(sessionsDir) : /* @__PURE__ */ new Map();
80044
+ for (const candidate of chosen) {
80045
+ await yieldToEventLoop2();
80046
+ const { rolloutPath, archived, mtimeMs, cwd, decision } = candidate;
80047
+ try {
80048
+ const summary = parseRolloutFile(rolloutPath);
80049
+ if (!summary) continue;
80050
+ const repoDir = summary.repoName ?? (cwd ? path6.basename(cwd) : void 0);
79830
80051
  const repoFullName = decision.repoFullName ?? summary.repoFullName;
79831
80052
  const title = summary.sessionId ? threadNames.get(summary.sessionId) : void 0;
79832
80053
  items.push({
79833
- mtimeMs: stat.mtimeMs,
80054
+ mtimeMs,
79834
80055
  transcriptPath: rolloutPath,
79835
- ...summary.cwd && { cwd: summary.cwd },
80056
+ ...cwd && { cwd },
79836
80057
  item: {
79837
80058
  source: "codex",
79838
- externalId: path6.basename(rolloutPath, ".jsonl"),
79839
- status: now - stat.mtimeMs < ACTIVE_GRACE_MS ? "active" : "inactive",
80059
+ externalId: externalIdOf(rolloutPath),
80060
+ status: now - mtimeMs < ACTIVE_GRACE_MS ? "active" : "inactive",
79840
80061
  archived,
79841
80062
  ...summary.startedAt && { startedAt: summary.startedAt },
79842
80063
  ...summary.endedAt && { endedAt: summary.endedAt },
@@ -79854,14 +80075,17 @@ async function collectCodexSessions(options = {}) {
79854
80075
  capturedVia: decision.via
79855
80076
  }
79856
80077
  });
80078
+ } catch (error52) {
80079
+ unreadable.push(`${externalIdOf(rolloutPath)}: ${describeReadError(error52)}`);
79857
80080
  }
79858
- };
79859
- await collectFrom(sessionsDir, false);
79860
- await collectFrom(codexArchivedSessionsDir(sessionsDir), true);
80081
+ }
79861
80082
  return {
79862
80083
  sessions: items.sort((a, b2) => b2.mtimeMs - a.mtimeMs),
79863
- skippedOutOfScope,
79864
- skippedSubagentThreads
80084
+ ...skips,
80085
+ skippedSubagentThreads,
80086
+ skippedTooLarge,
80087
+ unreadable,
80088
+ heldBack: candidates.length - chosen.length
79865
80089
  };
79866
80090
  }
79867
80091
 
@@ -79889,13 +80113,35 @@ function cursorSpoolDir(home) {
79889
80113
  var emptyResult = () => ({
79890
80114
  sessions: [],
79891
80115
  skippedOutOfScope: 0,
80116
+ skippedNoRepo: 0,
79892
80117
  skippedSubagentComposers: 0,
79893
80118
  skippedNonLocalComposers: 0,
79894
80119
  skippedUnreadable: 0,
79895
80120
  unavailable: null
79896
80121
  });
80122
+ var SQLITE_EXPERIMENTAL_WARNING = "SQLite is an experimental feature and might change at any time";
80123
+ async function withSqliteExperimentalWarningSuppressed(load) {
80124
+ const forward = process.emitWarning.bind(process);
80125
+ process.emitWarning = (warning2, ...rest) => {
80126
+ const message = typeof warning2 === "string" ? warning2 : warning2.message;
80127
+ if (message === SQLITE_EXPERIMENTAL_WARNING) return;
80128
+ Reflect.apply(forward, process, [warning2, ...rest]);
80129
+ };
80130
+ try {
80131
+ return await load();
80132
+ } finally {
80133
+ process.emitWarning = forward;
80134
+ }
80135
+ }
80136
+ var sqliteModule;
80137
+ function loadSqlite() {
80138
+ sqliteModule ??= withSqliteExperimentalWarningSuppressed(
80139
+ () => import("node:sqlite")
80140
+ );
80141
+ return sqliteModule;
80142
+ }
79897
80143
  async function openCursorDatabase(dbPath) {
79898
- const sqlite = await import("node:sqlite");
80144
+ const sqlite = await loadSqlite();
79899
80145
  return new sqlite.DatabaseSync(dbPath, { readOnly: true });
79900
80146
  }
79901
80147
  function describeError(error52) {
@@ -80118,7 +80364,7 @@ function collectFromStore(db, userDir, options, result) {
80118
80364
  const cwd = facts.workspacePath ?? workspaceFolderOf(userDir, workspaceId);
80119
80365
  const decision = captureDecision(cwd, capture);
80120
80366
  if (!decision.capture) {
80121
- result.skippedOutOfScope += 1;
80367
+ countCaptureSkip(result, decision.reason);
80122
80368
  continue;
80123
80369
  }
80124
80370
  if (options.locationsOnly) {
@@ -80199,36 +80445,6 @@ function collectFromStore(db, userDir, options, result) {
80199
80445
  }
80200
80446
  }
80201
80447
 
80202
- // ../../apps/collector/src/syncWindow.ts
80203
- var STEADY_SYNC_WINDOW_DAYS = 3;
80204
- var DEEP_FIRST_SYNC_DAYS = 90;
80205
- function resolveSyncWindow(explicitSince, deepSyncedAt) {
80206
- if (explicitSince !== void 0) {
80207
- const days = Number(explicitSince);
80208
- if (!Number.isFinite(days) || days <= 0) {
80209
- throw new ExpectedCliError(`--since must be a positive number of days, got "${explicitSince}"`);
80210
- }
80211
- return { days, deepSweep: false };
80212
- }
80213
- if (deepSyncedAt === void 0) return { days: DEEP_FIRST_SYNC_DAYS, deepSweep: true };
80214
- return { days: STEADY_SYNC_WINDOW_DAYS, deepSweep: false };
80215
- }
80216
- var DAY_MS = 24 * 60 * 60 * 1e3;
80217
- function resolveScanFloor(window2, nowMs, narrowSinceMs) {
80218
- if (window2.deepSweep || narrowSinceMs === void 0) return nowMs - window2.days * DAY_MS;
80219
- return narrowSinceMs;
80220
- }
80221
- function rawShipFloorMs(appEnv, nowMs, explicitSince) {
80222
- if (appEnv !== "development" || explicitSince !== void 0) return null;
80223
- return nowMs - STEADY_SYNC_WINDOW_DAYS * DAY_MS;
80224
- }
80225
- function describeScanReach(window2, narrowSinceMs) {
80226
- if (window2.deepSweep || narrowSinceMs === void 0) {
80227
- return `from the last ${window2.days} day(s)`;
80228
- }
80229
- return `changed since ${new Date(narrowSinceMs).toISOString()}`;
80230
- }
80231
-
80232
80448
  // ../../apps/collector/src/protectedRepoDiscovery.ts
80233
80449
  var defaultDiscoveryDeps = {
80234
80450
  collectClaude: collectClaudeCodeSessions,
@@ -80240,8 +80456,8 @@ async function findRecentProtectedSessions(options = {}, deps = defaultDiscovery
80240
80456
  const sinceMs = now - DEEP_FIRST_SYNC_DAYS * 24 * 60 * 60 * 1e3;
80241
80457
  const capture = captureEverythingUnresolved;
80242
80458
  const results = [
80243
- await deps.collectClaude({ sinceMs, capture }),
80244
- await deps.collectCodex({ sinceMs, capture }),
80459
+ await deps.collectClaude({ sinceMs, capture, locationsOnly: true }),
80460
+ await deps.collectCodex({ sinceMs, capture, locationsOnly: true }),
80245
80461
  await deps.collectCursor({ sinceMs, capture, locationsOnly: true })
80246
80462
  ];
80247
80463
  return groupProtectedSessions(
@@ -80405,7 +80621,7 @@ function installCollectorHooks(homeDir = os11.homedir()) {
80405
80621
  }
80406
80622
 
80407
80623
  // ../../apps/collector/src/service.ts
80408
- import { execFileSync as execFileSync2 } from "node:child_process";
80624
+ import { execFileSync as execFileSync3 } from "node:child_process";
80409
80625
  import { chmodSync as chmodSync2, existsSync as existsSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync7, rmSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "node:fs";
80410
80626
  import os12 from "node:os";
80411
80627
  import path10 from "node:path";
@@ -80478,17 +80694,40 @@ function installerPathValue() {
80478
80694
  }
80479
80695
  function installService(config2, command) {
80480
80696
  requireMacos();
80697
+ bootout(config2.appEnv);
80698
+ if (!waitForUnload(() => isLoaded(config2.appEnv))) {
80699
+ const service = `gui/${uid()}/${label(config2.appEnv)}`;
80700
+ throw new Error(
80701
+ `launchd is still unloading ${label(config2.appEnv)} after ${UNLOAD_WAIT_MS / 1e3}s \u2014 re-run powerplant init, or unload it by hand first: launchctl bootout ${service}`
80702
+ );
80703
+ }
80481
80704
  const launcher = launcherPath(config2.appEnv);
80482
80705
  mkdirSync4(path10.dirname(launcher), { recursive: true });
80483
80706
  writeFileSync4(launcher, buildLauncherScript(command));
80484
80707
  chmodSync2(launcher, 493);
80485
80708
  const file2 = plistPath(config2.appEnv);
80486
80709
  mkdirSync4(path10.dirname(file2), { recursive: true });
80487
- bootout(config2.appEnv);
80488
80710
  writeFileSync4(file2, buildLaunchdPlist(config2.appEnv, installerPathValue()));
80489
80711
  runLaunchctl(["bootstrap", `gui/${uid()}`, file2]);
80490
80712
  return `${file2} (watching for session changes; log: ${logPath(config2.appEnv)})`;
80491
80713
  }
80714
+ var UNLOAD_WAIT_MS = 1e4;
80715
+ var UNLOAD_POLL_MS = 100;
80716
+ function waitForUnload(loaded, options = {}) {
80717
+ const deadlineMs = options.deadlineMs ?? UNLOAD_WAIT_MS;
80718
+ const pollMs = options.pollMs ?? UNLOAD_POLL_MS;
80719
+ const sleep = options.sleep ?? sleepSync;
80720
+ const now = options.now ?? Date.now;
80721
+ const deadline = now() + deadlineMs;
80722
+ while (loaded()) {
80723
+ if (now() >= deadline) return false;
80724
+ sleep(pollMs);
80725
+ }
80726
+ return true;
80727
+ }
80728
+ function sleepSync(ms) {
80729
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
80730
+ }
80492
80731
  function serviceUpToDate(config2, command) {
80493
80732
  if (process.platform !== "darwin") return false;
80494
80733
  const launcher = launcherPath(config2.appEnv);
@@ -80530,7 +80769,7 @@ function restartService(config2) {
80530
80769
  }
80531
80770
  function runLaunchctl(args) {
80532
80771
  try {
80533
- execFileSync2("launchctl", args, { stdio: ["ignore", "ignore", "pipe"], encoding: "utf8" });
80772
+ execFileSync3("launchctl", args, { stdio: ["ignore", "ignore", "pipe"], encoding: "utf8" });
80534
80773
  } catch (cause) {
80535
80774
  throw new Error(launchctlFailureMessage(args[0] ?? "", cause), { cause });
80536
80775
  }
@@ -80545,7 +80784,7 @@ function launchctlFailureMessage(subcommand, cause) {
80545
80784
  }
80546
80785
  function isLoaded(appEnv) {
80547
80786
  try {
80548
- execFileSync2("launchctl", ["print", `gui/${uid()}/${label(appEnv)}`], { stdio: "ignore" });
80787
+ execFileSync3("launchctl", ["print", `gui/${uid()}/${label(appEnv)}`], { stdio: "ignore" });
80549
80788
  return true;
80550
80789
  } catch {
80551
80790
  return false;
@@ -80553,7 +80792,7 @@ function isLoaded(appEnv) {
80553
80792
  }
80554
80793
  function bootout(appEnv) {
80555
80794
  try {
80556
- execFileSync2("launchctl", ["bootout", `gui/${uid()}/${label(appEnv)}`], { stdio: "ignore" });
80795
+ execFileSync3("launchctl", ["bootout", `gui/${uid()}/${label(appEnv)}`], { stdio: "ignore" });
80557
80796
  return true;
80558
80797
  } catch {
80559
80798
  return false;
@@ -80884,7 +81123,7 @@ async function resolveCapturePolicy(config2, credentials, log3, deps = defaultPo
80884
81123
  }
80885
81124
  if (repos !== null && repos.length === 0) {
80886
81125
  log3(
80887
- "\u26A0 The org's synced repo list is empty \u2014 no repo-based uploads" + (memberFolders.length > 0 ? "; only folders you added upload." : ".")
81126
+ "\u26A0 The org's repo allowlist is empty \u2014 no repo-based uploads" + (memberFolders.length > 0 ? "; only folders you added upload." : ".")
80888
81127
  );
80889
81128
  } else if (repos === null && owners.length === 0 && !ownersUnresolved) {
80890
81129
  log3(
@@ -80911,26 +81150,31 @@ async function runSyncPass(config2, options = {}) {
80911
81150
  const window2 = resolveSyncWindow(options.explicitSince, credentials.deepSyncedAt);
80912
81151
  if (window2.deepSweep) {
80913
81152
  log3(
80914
- `First sync for this enrollment \u2014 sweeping the last ${window2.days} days to backfill transcripts already on this machine (later syncs cover ${STEADY_SYNC_WINDOW_DAYS}).`
81153
+ `First sync for this enrollment \u2014 sweeping the last ${window2.days} days and backfilling up to the ${DEEP_FIRST_SYNC_MAX_SESSIONS} most recently active sessions already on this machine (later syncs cover ${STEADY_SYNC_WINDOW_DAYS} days).`
80915
81154
  );
80916
81155
  }
80917
81156
  const capture = await resolveCapturePolicy(config2, credentials, log3);
80918
81157
  const sinceMs = resolveScanFloor(window2, Date.now(), options.narrowSinceMs);
81158
+ const maxSessions = window2.deepSweep ? DEEP_FIRST_SYNC_MAX_SESSIONS : void 0;
80919
81159
  const claude = await collectClaudeCodeSessions({ sinceMs, capture });
80920
- const codex = await collectCodexSessions({ sinceMs, capture });
81160
+ const codex = await collectCodexSessions({ sinceMs, capture, maxSessions });
80921
81161
  const cursor = await collectCursorSessions({ sinceMs, capture });
80922
81162
  if (cursor.unavailable !== null) {
80923
81163
  log3(`\u26A0 Cursor sessions skipped this pass \u2014 ${cursor.unavailable}. Other syncing is unaffected.`);
80924
81164
  }
80925
- const collected = [...claude.sessions, ...codex.sessions, ...cursor.sessions].sort(
81165
+ const inWindow = [...claude.sessions, ...codex.sessions, ...cursor.sessions].sort(
80926
81166
  (a, b2) => b2.mtimeMs - a.mtimeMs
80927
81167
  );
81168
+ const collected = maxSessions === void 0 ? inWindow : inWindow.slice(0, maxSessions);
81169
+ const heldBack = inWindow.length - collected.length + codex.heldBack;
80928
81170
  const skippedOutOfScope = claude.skippedOutOfScope + codex.skippedOutOfScope + cursor.skippedOutOfScope;
81171
+ const skippedNoRepo = claude.skippedNoRepo + codex.skippedNoRepo + cursor.skippedNoRepo;
80929
81172
  const items = collected.map((c) => c.item);
80930
81173
  const active = items.filter((s) => s.status === "active").length;
80931
81174
  log3(
80932
- `Found ${items.length} local session(s) (${claude.sessions.length} claude, ${codex.sessions.length} codex, ${cursor.sessions.length} cursor) ` + describeScanReach(window2, options.narrowSinceMs) + (active > 0 ? ` (${active} still active)` : "") + (skippedOutOfScope > 0 ? ` \xB7 ${skippedOutOfScope} ${describeSkipReason(capture)}` : "") + (codex.skippedSubagentThreads > 0 ? ` \xB7 ${codex.skippedSubagentThreads} codex subagent thread(s) skipped (spend unmeasured)` : "") + (cursor.skippedSubagentComposers > 0 ? ` \xB7 ${cursor.skippedSubagentComposers} cursor subagent thread(s) skipped` : "") + (cursor.skippedNonLocalComposers > 0 ? ` \xB7 ${cursor.skippedNonLocalComposers} cursor session(s) skipped (not local to this machine)` : "") + (cursor.skippedUnreadable > 0 ? ` \xB7 ${cursor.skippedUnreadable} cursor session(s) unreadable, skipped` : "") + "."
81175
+ `Found ${inWindow.length} local session(s) (${claude.sessions.length} claude, ${codex.sessions.length} codex, ${cursor.sessions.length} cursor) ` + describeScanReach(window2, options.narrowSinceMs) + (active > 0 ? ` (${active} still active)` : "") + (skippedOutOfScope > 0 ? ` \xB7 ${skippedOutOfScope} ${describeSkipReason(capture)}` : "") + (skippedNoRepo > 0 ? ` \xB7 ${skippedNoRepo} ${describeNoRepoSkipReason(capture)}` : "") + (codex.skippedSubagentThreads > 0 ? ` \xB7 ${codex.skippedSubagentThreads} codex subagent thread(s) skipped (spend unmeasured)` : "") + (cursor.skippedSubagentComposers > 0 ? ` \xB7 ${cursor.skippedSubagentComposers} cursor subagent thread(s) skipped` : "") + (cursor.skippedNonLocalComposers > 0 ? ` \xB7 ${cursor.skippedNonLocalComposers} cursor session(s) skipped (not local to this machine)` : "") + (cursor.skippedUnreadable > 0 ? ` \xB7 ${cursor.skippedUnreadable} cursor session(s) unreadable, skipped` : "") + (codex.skippedTooLarge > 0 ? ` \xB7 ${codex.skippedTooLarge} codex rollout(s) skipped (over ${MAX_TRANSCRIPT_FILE_BYTES / (1024 * 1024)}MB \u2014 too large to read)` : "") + (codex.unreadable.length > 0 ? ` \xB7 ${codex.unreadable.length} codex rollout(s) unreadable, skipped` : "") + (heldBack > 0 ? ` \xB7 ${heldBack} older session(s) held back \u2014 the first sync backfills the ${DEEP_FIRST_SYNC_MAX_SESSIONS} most recently active` : "") + "."
80933
81176
  );
81177
+ for (const failure of codex.unreadable) log3(` codex rollout skipped \u2014 ${failure}`);
80934
81178
  if (options.dryRun) {
80935
81179
  for (const s of items.slice(0, 10)) {
80936
81180
  log3(` ${s.externalId} ${s.model ?? "model unmeasured"} ${s.title ?? "(untitled)"}`);
@@ -81068,7 +81312,7 @@ import os14 from "node:os";
81068
81312
  import path14 from "node:path";
81069
81313
 
81070
81314
  // ../../apps/collector/src/wake.ts
81071
- import { execFile as execFile2, execFileSync as execFileSync3 } from "node:child_process";
81315
+ import { execFile as execFile2, execFileSync as execFileSync4 } from "node:child_process";
81072
81316
  import { existsSync as existsSync3 } from "node:fs";
81073
81317
  import path13 from "node:path";
81074
81318
  var WAKE_POLL_MS = 5e3;
@@ -81170,7 +81414,7 @@ function resolveClaudeBin(homeDir, env = process.env) {
81170
81414
  const found = candidates.find((candidate) => existsSync3(candidate));
81171
81415
  if (found) return found;
81172
81416
  try {
81173
- const fromPath = execFileSync3("which", ["claude"], { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
81417
+ const fromPath = execFileSync4("which", ["claude"], { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
81174
81418
  return fromPath === "" ? null : fromPath;
81175
81419
  } catch {
81176
81420
  return null;
@@ -81192,7 +81436,7 @@ function versionAtLeast(version2, floor) {
81192
81436
  return true;
81193
81437
  }
81194
81438
  function execClaudeVersion(claudeBin) {
81195
- return execFileSync3(claudeBin, ["--version"], {
81439
+ return execFileSync4(claudeBin, ["--version"], {
81196
81440
  stdio: ["ignore", "pipe", "ignore"],
81197
81441
  timeout: VERSION_TIMEOUT_MS
81198
81442
  }).toString();
@@ -81766,7 +82010,7 @@ function describeError3(error52) {
81766
82010
  }
81767
82011
 
81768
82012
  // src/connect.ts
81769
- import { execFileSync as execFileSync4, spawnSync } from "node:child_process";
82013
+ import { execFileSync as execFileSync5, spawnSync } from "node:child_process";
81770
82014
  import {
81771
82015
  existsSync as existsSync5,
81772
82016
  mkdirSync as mkdirSync6,
@@ -81992,7 +82236,7 @@ function desktopClaudeBinaries(homeDir) {
81992
82236
  }
81993
82237
  function defaultWhichAll(name) {
81994
82238
  try {
81995
- return execFileSync4("which", ["-a", name], { encoding: "utf8" }).split("\n").map((line) => line.trim()).filter(Boolean);
82239
+ return execFileSync5("which", ["-a", name], { encoding: "utf8" }).split("\n").map((line) => line.trim()).filter(Boolean);
81996
82240
  } catch {
81997
82241
  return [];
81998
82242
  }
@@ -82785,7 +83029,7 @@ function realRunInteractive(bin, args, env) {
82785
83029
  }
82786
83030
 
82787
83031
  // src/connections.ts
82788
- import { execFileSync as execFileSync5, spawnSync as spawnSync2 } from "node:child_process";
83032
+ import { execFileSync as execFileSync6, spawnSync as spawnSync2 } from "node:child_process";
82789
83033
  import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "node:fs";
82790
83034
  import path16 from "node:path";
82791
83035
  function probeConnections(clients, deps) {
@@ -82988,7 +83232,7 @@ function runWithPath(pathValue, budget = UNLIMITED_TIME_BUDGET) {
82988
83232
  function whichAllWith(pathValue) {
82989
83233
  return (name) => {
82990
83234
  try {
82991
- return execFileSync5("which", ["-a", name], {
83235
+ return execFileSync6("which", ["-a", name], {
82992
83236
  encoding: "utf8",
82993
83237
  env: { ...process.env, PATH: pathValue }
82994
83238
  }).split("\n").map((line) => line.trim()).filter(Boolean);
@@ -82999,14 +83243,14 @@ function whichAllWith(pathValue) {
82999
83243
  }
83000
83244
 
83001
83245
  // src/doctor.ts
83002
- import { closeSync, fstatSync, openSync, readSync } from "node:fs";
83246
+ import { closeSync as closeSync2, fstatSync, openSync as openSync2, readSync as readSync2 } from "node:fs";
83003
83247
 
83004
83248
  // src/statusReport.ts
83005
83249
  function renderStatus(i) {
83006
83250
  const lines = [];
83007
83251
  lines.push(`Environment: ${i.config.appEnv} (API ${i.config.apiUrl})`);
83008
83252
  lines.push(
83009
- `CLI: powerplant ${i.cliVersion}` + (i.latestVersion !== null && i.latestVersion !== i.cliVersion ? ` \u2014 ${i.latestVersion} is available: npm install -g @usepowerplant/cli (the service restarts itself onto it)` : "")
83253
+ `CLI: powerplant ${i.cliVersion}` + (i.latestVersion !== null && i.latestVersion !== i.cliVersion ? ` \u2014 ${i.latestVersion} is available: powerplant update` : "")
83010
83254
  );
83011
83255
  const unsupported = unsupportedVersionNotice(i.cliVersion, i.oldestSupportedVersion);
83012
83256
  if (unsupported !== null) lines.push(unsupported);
@@ -83158,7 +83402,7 @@ function redactedLogTail(raw) {
83158
83402
  function readDoctorLogTail(path19) {
83159
83403
  let fd;
83160
83404
  try {
83161
- fd = openSync(path19, "r");
83405
+ fd = openSync2(path19, "r");
83162
83406
  } catch (error52) {
83163
83407
  if (error52.code === "ENOENT") return { path: path19, lines: null };
83164
83408
  return { path: path19, lines: null, readError: error52 instanceof Error ? error52.message : String(error52) };
@@ -83167,14 +83411,14 @@ function readDoctorLogTail(path19) {
83167
83411
  const size = fstatSync(fd).size;
83168
83412
  const offset = Math.max(0, size - LOG_TAIL_READ_BYTES);
83169
83413
  const buffer = Buffer.alloc(Math.min(size, LOG_TAIL_READ_BYTES));
83170
- readSync(fd, buffer, 0, buffer.length, offset);
83414
+ readSync2(fd, buffer, 0, buffer.length, offset);
83171
83415
  let text = buffer.toString("utf8");
83172
83416
  if (offset > 0) text = text.slice(text.indexOf("\n") + 1);
83173
83417
  return { path: path19, lines: redactedLogTail(text) };
83174
83418
  } catch (error52) {
83175
83419
  return { path: path19, lines: null, readError: error52 instanceof Error ? error52.message : String(error52) };
83176
83420
  } finally {
83177
- closeSync(fd);
83421
+ closeSync2(fd);
83178
83422
  }
83179
83423
  }
83180
83424
  function renderDoctorReport(i) {
@@ -83477,7 +83721,7 @@ async function realPromptLine(question) {
83477
83721
  }
83478
83722
 
83479
83723
  // src/memberSessions.ts
83480
- import { execFileSync as execFileSync6 } from "node:child_process";
83724
+ import { execFileSync as execFileSync7 } from "node:child_process";
83481
83725
  import { existsSync as existsSync6 } from "node:fs";
83482
83726
  import os16 from "node:os";
83483
83727
  import path17 from "node:path";
@@ -83552,7 +83796,7 @@ function detectCodexDesktop(probe = { exists: existsSync6, readUrlSchemes: realR
83552
83796
  function realReadUrlSchemes(bundlePath) {
83553
83797
  if (process.platform !== "darwin") return [];
83554
83798
  try {
83555
- const json2 = execFileSync6(
83799
+ const json2 = execFileSync7(
83556
83800
  "plutil",
83557
83801
  ["-extract", "CFBundleURLTypes", "json", "-o", "-", path17.join(bundlePath, "Contents", "Info.plist")],
83558
83802
  { stdio: ["ignore", "pipe", "ignore"] }
@@ -84009,8 +84253,38 @@ function createTerminalOutput(options) {
84009
84253
  return { log: log3, activity };
84010
84254
  }
84011
84255
 
84256
+ // src/update.ts
84257
+ import { spawnSync as spawnSync4 } from "node:child_process";
84258
+ var npmLsSchema = external_exports.object({
84259
+ dependencies: external_exports.record(external_exports.string(), external_exports.object({ version: external_exports.string().min(1) })).optional()
84260
+ });
84261
+ function runCapturing(command, args) {
84262
+ const result = spawnSync4(command, args, {
84263
+ encoding: "utf8",
84264
+ stdio: ["ignore", "pipe", "ignore"]
84265
+ });
84266
+ return { stdout: result.stdout ?? "" };
84267
+ }
84268
+ function installedGlobalVersion(packageName, run = runCapturing) {
84269
+ const { stdout } = run("npm", ["ls", "-g", packageName, "--json"]);
84270
+ try {
84271
+ const parsed = npmLsSchema.safeParse(JSON.parse(stdout));
84272
+ return parsed.success ? parsed.data.dependencies?.[packageName]?.version ?? null : null;
84273
+ } catch {
84274
+ return null;
84275
+ }
84276
+ }
84277
+ var RESTART_NOTE = "Sync service will restart within minutes. (You can do this now with powerplant restart.)";
84278
+ function updatedMessage(version2) {
84279
+ if (version2 === null) {
84280
+ return `Updated, but could not read the installed version from npm. Check with: powerplant status
84281
+ ${RESTART_NOTE}`;
84282
+ }
84283
+ return `Updated to v${version2}. ${RESTART_NOTE}`;
84284
+ }
84285
+
84012
84286
  // src/version.ts
84013
- var CLI_VERSION = true ? "0.3.0" : "0.0.0-checkout";
84287
+ var CLI_VERSION = true ? "0.3.2" : "0.0.0-checkout";
84014
84288
  var CLI_PACKAGE_NAME = "@usepowerplant/cli";
84015
84289
 
84016
84290
  // src/workspace.ts
@@ -84131,7 +84405,7 @@ Commands:
84131
84405
  sync Run one sync pass in the foreground [--since <days>] [--dry-run].
84132
84406
  folders The folders you explicitly sync on this machine:
84133
84407
  powerplant folders [add <path> | remove <path>]. Additions on
84134
- top of your org's synced repos \u2014 no re-enroll needed.
84408
+ top of your org's allowlisted repos \u2014 no re-enroll needed.
84135
84409
  workspace Your workspaces, and which one Powerplant and your connected
84136
84410
  agents use: powerplant workspace [<name>] switches to <name>.
84137
84411
  resume-session Reopen a synced session in its own CLI, right here in
@@ -84443,7 +84717,7 @@ The access you granted these agents stays on your Powerplant account until you r
84443
84717
  }
84444
84718
  case "update": {
84445
84719
  console.log(`Updating ${CLI_PACKAGE_NAME} via npm\u2026`);
84446
- const npm = spawnSync4("npm", ["install", "-g", CLI_PACKAGE_NAME], { stdio: "inherit" });
84720
+ const npm = spawnSync5("npm", ["install", "-g", CLI_PACKAGE_NAME], { stdio: "inherit" });
84447
84721
  if (npm.error) {
84448
84722
  throw new ExpectedCliError(
84449
84723
  `Could not run npm (${npm.error.message}) \u2014 update by hand: npm install -g ${CLI_PACKAGE_NAME}`
@@ -84452,9 +84726,7 @@ The access you granted these agents stays on your Powerplant account until you r
84452
84726
  if (npm.status !== 0) {
84453
84727
  throw new ExpectedCliError("npm install failed \u2014 see the output above.");
84454
84728
  }
84455
- console.log(
84456
- "Updated. The background service restarts itself onto the new version within minutes (or now: powerplant restart)."
84457
- );
84729
+ console.log(updatedMessage(installedGlobalVersion(CLI_PACKAGE_NAME)));
84458
84730
  return;
84459
84731
  }
84460
84732
  case "restart": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usepowerplant/cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },