@usepowerplant/cli 0.3.1 → 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 +264 -122
  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.1" : "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
  }
@@ -79570,6 +79660,10 @@ function readIfExists2(file2) {
79570
79660
  throw error52;
79571
79661
  }
79572
79662
  }
79663
+ var MAX_TRANSCRIPT_FILE_BYTES = 256 * 1024 * 1024;
79664
+ function describeReadError(error52) {
79665
+ return error52 instanceof Error ? error52.message : String(error52);
79666
+ }
79573
79667
 
79574
79668
  // ../../apps/collector/src/lifecycleJournal.ts
79575
79669
  var LIFECYCLE_JOURNAL_FILENAME = "session-lifecycle.jsonl";
@@ -79660,7 +79754,7 @@ async function collectClaudeCodeSessions(options = {}) {
79660
79754
  const capture = options.capture ?? captureEverythingUnresolved;
79661
79755
  const projectsDir = path5.join(claudeDir, "projects");
79662
79756
  const items = [];
79663
- let skippedOutOfScope = 0;
79757
+ const skips = { skippedOutOfScope: 0, skippedNoRepo: 0 };
79664
79758
  const home = path5.dirname(claudeDir);
79665
79759
  const lifecycle = options.lifecycle ?? readLifecycleJournal(home);
79666
79760
  for (const projectDir of listDirs(projectsDir)) {
@@ -79677,7 +79771,7 @@ async function collectClaudeCodeSessions(options = {}) {
79677
79771
  const cwd = transcriptCwd(transcriptPath);
79678
79772
  const decision2 = captureDecision(cwd, capture);
79679
79773
  if (!decision2.capture) {
79680
- skippedOutOfScope += 1;
79774
+ countCaptureSkip(skips, decision2.reason);
79681
79775
  continue;
79682
79776
  }
79683
79777
  items.push({
@@ -79694,7 +79788,7 @@ async function collectClaudeCodeSessions(options = {}) {
79694
79788
  if (!summary) continue;
79695
79789
  const decision = captureDecision(summary.cwd, capture);
79696
79790
  if (!decision.capture) {
79697
- skippedOutOfScope += 1;
79791
+ countCaptureSkip(skips, decision.reason);
79698
79792
  continue;
79699
79793
  }
79700
79794
  const usage = addSubagentUsage(summary.usage, path5.join(projectDir, sessionId, "subagents"));
@@ -79731,7 +79825,7 @@ async function collectClaudeCodeSessions(options = {}) {
79731
79825
  }
79732
79826
  return {
79733
79827
  sessions: items.sort((a, b2) => b2.mtimeMs - a.mtimeMs),
79734
- skippedOutOfScope
79828
+ ...skips
79735
79829
  };
79736
79830
  }
79737
79831
  function transcriptCwd(transcriptPath) {
@@ -79875,54 +79969,95 @@ async function collectCodexSessions(options = {}) {
79875
79969
  const sessionsDir = options.codexDir ?? path6.join(os8.homedir(), ".codex", "sessions");
79876
79970
  const now = options.now ?? Date.now();
79877
79971
  const capture = options.capture ?? captureEverythingUnresolved;
79878
- const threadNames = options.locationsOnly ? /* @__PURE__ */ new Map() : loadThreadNames(sessionsDir);
79972
+ const maxBytes = options.maxTranscriptBytes ?? MAX_TRANSCRIPT_FILE_BYTES;
79879
79973
  const items = [];
79880
- let skippedOutOfScope = 0;
79974
+ const candidates = [];
79975
+ const unreadable = [];
79976
+ const skips = { skippedOutOfScope: 0, skippedNoRepo: 0 };
79881
79977
  let skippedSubagentThreads = 0;
79882
- const collectFrom = async (rootDir, archived) => {
79978
+ let skippedTooLarge = 0;
79979
+ const externalIdOf = (rolloutPath) => path6.basename(rolloutPath, ".jsonl");
79980
+ const admitFrom = async (rootDir, archived) => {
79883
79981
  for (const rolloutPath of rolloutFiles(rootDir)) {
79884
79982
  await yieldToEventLoop2();
79885
79983
  const stat = statIfExists(rolloutPath);
79886
79984
  if (!stat) continue;
79887
79985
  if (options.sinceMs !== void 0 && Math.max(stat.mtimeMs, stat.ctimeMs) < options.sinceMs)
79888
79986
  continue;
79889
- const summary = options.locationsOnly ? rolloutLocation(rolloutPath) : parseRolloutFile(rolloutPath);
79890
- if (!summary) continue;
79891
- if (summary.kind === "subagent") {
79892
- skippedSubagentThreads += 1;
79893
- continue;
79894
- }
79895
- const decision = captureDecision(summary.cwd, capture);
79896
- if (!decision.capture) {
79897
- skippedOutOfScope += 1;
79987
+ if (stat.size > maxBytes) {
79988
+ skippedTooLarge += 1;
79898
79989
  continue;
79899
79990
  }
79900
- if (options.locationsOnly) {
79901
- items.push({
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,
79902
80020
  mtimeMs: stat.mtimeMs,
79903
- transcriptPath: rolloutPath,
79904
- ...summary.cwd && { cwd: summary.cwd },
79905
- item: {
79906
- source: "codex",
79907
- externalId: path6.basename(rolloutPath, ".jsonl"),
79908
- status: now - stat.mtimeMs < ACTIVE_GRACE_MS ? "active" : "inactive",
79909
- archived,
79910
- capturedVia: decision.via
79911
- }
80021
+ ...location.cwd && { cwd: location.cwd },
80022
+ decision
79912
80023
  });
79913
- continue;
80024
+ } catch (error52) {
80025
+ unreadable.push(`${externalIdOf(rolloutPath)}: ${describeReadError(error52)}`);
79914
80026
  }
79915
- 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);
79916
80051
  const repoFullName = decision.repoFullName ?? summary.repoFullName;
79917
80052
  const title = summary.sessionId ? threadNames.get(summary.sessionId) : void 0;
79918
80053
  items.push({
79919
- mtimeMs: stat.mtimeMs,
80054
+ mtimeMs,
79920
80055
  transcriptPath: rolloutPath,
79921
- ...summary.cwd && { cwd: summary.cwd },
80056
+ ...cwd && { cwd },
79922
80057
  item: {
79923
80058
  source: "codex",
79924
- externalId: path6.basename(rolloutPath, ".jsonl"),
79925
- status: now - stat.mtimeMs < ACTIVE_GRACE_MS ? "active" : "inactive",
80059
+ externalId: externalIdOf(rolloutPath),
80060
+ status: now - mtimeMs < ACTIVE_GRACE_MS ? "active" : "inactive",
79926
80061
  archived,
79927
80062
  ...summary.startedAt && { startedAt: summary.startedAt },
79928
80063
  ...summary.endedAt && { endedAt: summary.endedAt },
@@ -79940,14 +80075,17 @@ async function collectCodexSessions(options = {}) {
79940
80075
  capturedVia: decision.via
79941
80076
  }
79942
80077
  });
80078
+ } catch (error52) {
80079
+ unreadable.push(`${externalIdOf(rolloutPath)}: ${describeReadError(error52)}`);
79943
80080
  }
79944
- };
79945
- await collectFrom(sessionsDir, false);
79946
- await collectFrom(codexArchivedSessionsDir(sessionsDir), true);
80081
+ }
79947
80082
  return {
79948
80083
  sessions: items.sort((a, b2) => b2.mtimeMs - a.mtimeMs),
79949
- skippedOutOfScope,
79950
- skippedSubagentThreads
80084
+ ...skips,
80085
+ skippedSubagentThreads,
80086
+ skippedTooLarge,
80087
+ unreadable,
80088
+ heldBack: candidates.length - chosen.length
79951
80089
  };
79952
80090
  }
79953
80091
 
@@ -79975,6 +80113,7 @@ function cursorSpoolDir(home) {
79975
80113
  var emptyResult = () => ({
79976
80114
  sessions: [],
79977
80115
  skippedOutOfScope: 0,
80116
+ skippedNoRepo: 0,
79978
80117
  skippedSubagentComposers: 0,
79979
80118
  skippedNonLocalComposers: 0,
79980
80119
  skippedUnreadable: 0,
@@ -80225,7 +80364,7 @@ function collectFromStore(db, userDir, options, result) {
80225
80364
  const cwd = facts.workspacePath ?? workspaceFolderOf(userDir, workspaceId);
80226
80365
  const decision = captureDecision(cwd, capture);
80227
80366
  if (!decision.capture) {
80228
- result.skippedOutOfScope += 1;
80367
+ countCaptureSkip(result, decision.reason);
80229
80368
  continue;
80230
80369
  }
80231
80370
  if (options.locationsOnly) {
@@ -80306,36 +80445,6 @@ function collectFromStore(db, userDir, options, result) {
80306
80445
  }
80307
80446
  }
80308
80447
 
80309
- // ../../apps/collector/src/syncWindow.ts
80310
- var STEADY_SYNC_WINDOW_DAYS = 3;
80311
- var DEEP_FIRST_SYNC_DAYS = 90;
80312
- function resolveSyncWindow(explicitSince, deepSyncedAt) {
80313
- if (explicitSince !== void 0) {
80314
- const days = Number(explicitSince);
80315
- if (!Number.isFinite(days) || days <= 0) {
80316
- throw new ExpectedCliError(`--since must be a positive number of days, got "${explicitSince}"`);
80317
- }
80318
- return { days, deepSweep: false };
80319
- }
80320
- if (deepSyncedAt === void 0) return { days: DEEP_FIRST_SYNC_DAYS, deepSweep: true };
80321
- return { days: STEADY_SYNC_WINDOW_DAYS, deepSweep: false };
80322
- }
80323
- var DAY_MS = 24 * 60 * 60 * 1e3;
80324
- function resolveScanFloor(window2, nowMs, narrowSinceMs) {
80325
- if (window2.deepSweep || narrowSinceMs === void 0) return nowMs - window2.days * DAY_MS;
80326
- return narrowSinceMs;
80327
- }
80328
- function rawShipFloorMs(appEnv, nowMs, explicitSince) {
80329
- if (appEnv !== "development" || explicitSince !== void 0) return null;
80330
- return nowMs - STEADY_SYNC_WINDOW_DAYS * DAY_MS;
80331
- }
80332
- function describeScanReach(window2, narrowSinceMs) {
80333
- if (window2.deepSweep || narrowSinceMs === void 0) {
80334
- return `from the last ${window2.days} day(s)`;
80335
- }
80336
- return `changed since ${new Date(narrowSinceMs).toISOString()}`;
80337
- }
80338
-
80339
80448
  // ../../apps/collector/src/protectedRepoDiscovery.ts
80340
80449
  var defaultDiscoveryDeps = {
80341
80450
  collectClaude: collectClaudeCodeSessions,
@@ -80512,7 +80621,7 @@ function installCollectorHooks(homeDir = os11.homedir()) {
80512
80621
  }
80513
80622
 
80514
80623
  // ../../apps/collector/src/service.ts
80515
- import { execFileSync as execFileSync2 } from "node:child_process";
80624
+ import { execFileSync as execFileSync3 } from "node:child_process";
80516
80625
  import { chmodSync as chmodSync2, existsSync as existsSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync7, rmSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "node:fs";
80517
80626
  import os12 from "node:os";
80518
80627
  import path10 from "node:path";
@@ -80660,7 +80769,7 @@ function restartService(config2) {
80660
80769
  }
80661
80770
  function runLaunchctl(args) {
80662
80771
  try {
80663
- execFileSync2("launchctl", args, { stdio: ["ignore", "ignore", "pipe"], encoding: "utf8" });
80772
+ execFileSync3("launchctl", args, { stdio: ["ignore", "ignore", "pipe"], encoding: "utf8" });
80664
80773
  } catch (cause) {
80665
80774
  throw new Error(launchctlFailureMessage(args[0] ?? "", cause), { cause });
80666
80775
  }
@@ -80675,7 +80784,7 @@ function launchctlFailureMessage(subcommand, cause) {
80675
80784
  }
80676
80785
  function isLoaded(appEnv) {
80677
80786
  try {
80678
- execFileSync2("launchctl", ["print", `gui/${uid()}/${label(appEnv)}`], { stdio: "ignore" });
80787
+ execFileSync3("launchctl", ["print", `gui/${uid()}/${label(appEnv)}`], { stdio: "ignore" });
80679
80788
  return true;
80680
80789
  } catch {
80681
80790
  return false;
@@ -80683,7 +80792,7 @@ function isLoaded(appEnv) {
80683
80792
  }
80684
80793
  function bootout(appEnv) {
80685
80794
  try {
80686
- execFileSync2("launchctl", ["bootout", `gui/${uid()}/${label(appEnv)}`], { stdio: "ignore" });
80795
+ execFileSync3("launchctl", ["bootout", `gui/${uid()}/${label(appEnv)}`], { stdio: "ignore" });
80687
80796
  return true;
80688
80797
  } catch {
80689
80798
  return false;
@@ -81014,7 +81123,7 @@ async function resolveCapturePolicy(config2, credentials, log3, deps = defaultPo
81014
81123
  }
81015
81124
  if (repos !== null && repos.length === 0) {
81016
81125
  log3(
81017
- "\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." : ".")
81018
81127
  );
81019
81128
  } else if (repos === null && owners.length === 0 && !ownersUnresolved) {
81020
81129
  log3(
@@ -81041,26 +81150,31 @@ async function runSyncPass(config2, options = {}) {
81041
81150
  const window2 = resolveSyncWindow(options.explicitSince, credentials.deepSyncedAt);
81042
81151
  if (window2.deepSweep) {
81043
81152
  log3(
81044
- `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).`
81045
81154
  );
81046
81155
  }
81047
81156
  const capture = await resolveCapturePolicy(config2, credentials, log3);
81048
81157
  const sinceMs = resolveScanFloor(window2, Date.now(), options.narrowSinceMs);
81158
+ const maxSessions = window2.deepSweep ? DEEP_FIRST_SYNC_MAX_SESSIONS : void 0;
81049
81159
  const claude = await collectClaudeCodeSessions({ sinceMs, capture });
81050
- const codex = await collectCodexSessions({ sinceMs, capture });
81160
+ const codex = await collectCodexSessions({ sinceMs, capture, maxSessions });
81051
81161
  const cursor = await collectCursorSessions({ sinceMs, capture });
81052
81162
  if (cursor.unavailable !== null) {
81053
81163
  log3(`\u26A0 Cursor sessions skipped this pass \u2014 ${cursor.unavailable}. Other syncing is unaffected.`);
81054
81164
  }
81055
- const collected = [...claude.sessions, ...codex.sessions, ...cursor.sessions].sort(
81165
+ const inWindow = [...claude.sessions, ...codex.sessions, ...cursor.sessions].sort(
81056
81166
  (a, b2) => b2.mtimeMs - a.mtimeMs
81057
81167
  );
81168
+ const collected = maxSessions === void 0 ? inWindow : inWindow.slice(0, maxSessions);
81169
+ const heldBack = inWindow.length - collected.length + codex.heldBack;
81058
81170
  const skippedOutOfScope = claude.skippedOutOfScope + codex.skippedOutOfScope + cursor.skippedOutOfScope;
81171
+ const skippedNoRepo = claude.skippedNoRepo + codex.skippedNoRepo + cursor.skippedNoRepo;
81059
81172
  const items = collected.map((c) => c.item);
81060
81173
  const active = items.filter((s) => s.status === "active").length;
81061
81174
  log3(
81062
- `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` : "") + "."
81063
81176
  );
81177
+ for (const failure of codex.unreadable) log3(` codex rollout skipped \u2014 ${failure}`);
81064
81178
  if (options.dryRun) {
81065
81179
  for (const s of items.slice(0, 10)) {
81066
81180
  log3(` ${s.externalId} ${s.model ?? "model unmeasured"} ${s.title ?? "(untitled)"}`);
@@ -81198,7 +81312,7 @@ import os14 from "node:os";
81198
81312
  import path14 from "node:path";
81199
81313
 
81200
81314
  // ../../apps/collector/src/wake.ts
81201
- import { execFile as execFile2, execFileSync as execFileSync3 } from "node:child_process";
81315
+ import { execFile as execFile2, execFileSync as execFileSync4 } from "node:child_process";
81202
81316
  import { existsSync as existsSync3 } from "node:fs";
81203
81317
  import path13 from "node:path";
81204
81318
  var WAKE_POLL_MS = 5e3;
@@ -81300,7 +81414,7 @@ function resolveClaudeBin(homeDir, env = process.env) {
81300
81414
  const found = candidates.find((candidate) => existsSync3(candidate));
81301
81415
  if (found) return found;
81302
81416
  try {
81303
- const fromPath = execFileSync3("which", ["claude"], { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
81417
+ const fromPath = execFileSync4("which", ["claude"], { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
81304
81418
  return fromPath === "" ? null : fromPath;
81305
81419
  } catch {
81306
81420
  return null;
@@ -81322,7 +81436,7 @@ function versionAtLeast(version2, floor) {
81322
81436
  return true;
81323
81437
  }
81324
81438
  function execClaudeVersion(claudeBin) {
81325
- return execFileSync3(claudeBin, ["--version"], {
81439
+ return execFileSync4(claudeBin, ["--version"], {
81326
81440
  stdio: ["ignore", "pipe", "ignore"],
81327
81441
  timeout: VERSION_TIMEOUT_MS
81328
81442
  }).toString();
@@ -81896,7 +82010,7 @@ function describeError3(error52) {
81896
82010
  }
81897
82011
 
81898
82012
  // src/connect.ts
81899
- import { execFileSync as execFileSync4, spawnSync } from "node:child_process";
82013
+ import { execFileSync as execFileSync5, spawnSync } from "node:child_process";
81900
82014
  import {
81901
82015
  existsSync as existsSync5,
81902
82016
  mkdirSync as mkdirSync6,
@@ -82122,7 +82236,7 @@ function desktopClaudeBinaries(homeDir) {
82122
82236
  }
82123
82237
  function defaultWhichAll(name) {
82124
82238
  try {
82125
- 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);
82126
82240
  } catch {
82127
82241
  return [];
82128
82242
  }
@@ -82915,7 +83029,7 @@ function realRunInteractive(bin, args, env) {
82915
83029
  }
82916
83030
 
82917
83031
  // src/connections.ts
82918
- import { execFileSync as execFileSync5, spawnSync as spawnSync2 } from "node:child_process";
83032
+ import { execFileSync as execFileSync6, spawnSync as spawnSync2 } from "node:child_process";
82919
83033
  import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "node:fs";
82920
83034
  import path16 from "node:path";
82921
83035
  function probeConnections(clients, deps) {
@@ -83118,7 +83232,7 @@ function runWithPath(pathValue, budget = UNLIMITED_TIME_BUDGET) {
83118
83232
  function whichAllWith(pathValue) {
83119
83233
  return (name) => {
83120
83234
  try {
83121
- return execFileSync5("which", ["-a", name], {
83235
+ return execFileSync6("which", ["-a", name], {
83122
83236
  encoding: "utf8",
83123
83237
  env: { ...process.env, PATH: pathValue }
83124
83238
  }).split("\n").map((line) => line.trim()).filter(Boolean);
@@ -83136,7 +83250,7 @@ function renderStatus(i) {
83136
83250
  const lines = [];
83137
83251
  lines.push(`Environment: ${i.config.appEnv} (API ${i.config.apiUrl})`);
83138
83252
  lines.push(
83139
- `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` : "")
83140
83254
  );
83141
83255
  const unsupported = unsupportedVersionNotice(i.cliVersion, i.oldestSupportedVersion);
83142
83256
  if (unsupported !== null) lines.push(unsupported);
@@ -83607,7 +83721,7 @@ async function realPromptLine(question) {
83607
83721
  }
83608
83722
 
83609
83723
  // src/memberSessions.ts
83610
- import { execFileSync as execFileSync6 } from "node:child_process";
83724
+ import { execFileSync as execFileSync7 } from "node:child_process";
83611
83725
  import { existsSync as existsSync6 } from "node:fs";
83612
83726
  import os16 from "node:os";
83613
83727
  import path17 from "node:path";
@@ -83682,7 +83796,7 @@ function detectCodexDesktop(probe = { exists: existsSync6, readUrlSchemes: realR
83682
83796
  function realReadUrlSchemes(bundlePath) {
83683
83797
  if (process.platform !== "darwin") return [];
83684
83798
  try {
83685
- const json2 = execFileSync6(
83799
+ const json2 = execFileSync7(
83686
83800
  "plutil",
83687
83801
  ["-extract", "CFBundleURLTypes", "json", "-o", "-", path17.join(bundlePath, "Contents", "Info.plist")],
83688
83802
  { stdio: ["ignore", "pipe", "ignore"] }
@@ -84139,8 +84253,38 @@ function createTerminalOutput(options) {
84139
84253
  return { log: log3, activity };
84140
84254
  }
84141
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
+
84142
84286
  // src/version.ts
84143
- var CLI_VERSION = true ? "0.3.1" : "0.0.0-checkout";
84287
+ var CLI_VERSION = true ? "0.3.2" : "0.0.0-checkout";
84144
84288
  var CLI_PACKAGE_NAME = "@usepowerplant/cli";
84145
84289
 
84146
84290
  // src/workspace.ts
@@ -84261,7 +84405,7 @@ Commands:
84261
84405
  sync Run one sync pass in the foreground [--since <days>] [--dry-run].
84262
84406
  folders The folders you explicitly sync on this machine:
84263
84407
  powerplant folders [add <path> | remove <path>]. Additions on
84264
- 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.
84265
84409
  workspace Your workspaces, and which one Powerplant and your connected
84266
84410
  agents use: powerplant workspace [<name>] switches to <name>.
84267
84411
  resume-session Reopen a synced session in its own CLI, right here in
@@ -84573,7 +84717,7 @@ The access you granted these agents stays on your Powerplant account until you r
84573
84717
  }
84574
84718
  case "update": {
84575
84719
  console.log(`Updating ${CLI_PACKAGE_NAME} via npm\u2026`);
84576
- const npm = spawnSync4("npm", ["install", "-g", CLI_PACKAGE_NAME], { stdio: "inherit" });
84720
+ const npm = spawnSync5("npm", ["install", "-g", CLI_PACKAGE_NAME], { stdio: "inherit" });
84577
84721
  if (npm.error) {
84578
84722
  throw new ExpectedCliError(
84579
84723
  `Could not run npm (${npm.error.message}) \u2014 update by hand: npm install -g ${CLI_PACKAGE_NAME}`
@@ -84582,9 +84726,7 @@ The access you granted these agents stays on your Powerplant account until you r
84582
84726
  if (npm.status !== 0) {
84583
84727
  throw new ExpectedCliError("npm install failed \u2014 see the output above.");
84584
84728
  }
84585
- console.log(
84586
- "Updated. The background service restarts itself onto the new version within minutes (or now: powerplant restart)."
84587
- );
84729
+ console.log(updatedMessage(installedGlobalVersion(CLI_PACKAGE_NAME)));
84588
84730
  return;
84589
84731
  }
84590
84732
  case "restart": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usepowerplant/cli",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },