@usepowerplant/cli 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +687 -217
  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
 
@@ -50833,6 +50833,17 @@ function capturedSessionEvents(turns, anchor) {
50833
50833
  return events;
50834
50834
  }
50835
50835
 
50836
+ // ../core/src/workspaceClaims.ts
50837
+ function workspaceClaimsRepo(repoFullName, policy) {
50838
+ const repo = repoFullName.toLowerCase();
50839
+ if (policy.repos !== null) return policy.repos.includes(repo);
50840
+ const owner = repo.split("/")[0] ?? "";
50841
+ return policy.owners.length > 0 && policy.owners.includes(owner);
50842
+ }
50843
+ function workspaceClaimsAnyRepo(policy) {
50844
+ return policy.repos !== null ? policy.repos.length > 0 : policy.owners.length > 0;
50845
+ }
50846
+
50836
50847
  // ../core/src/transcriptImages.ts
50837
50848
  var IMAGE_DATA_URI_RE = /^data:(image\/[\w.+-]+);base64,([A-Za-z0-9+/=_-]*)$/;
50838
50849
  function isImageDataUri(value) {
@@ -51693,6 +51704,8 @@ var SESSION_SOURCES = [
51693
51704
  "cursor-cloud",
51694
51705
  "github-pr"
51695
51706
  ];
51707
+ var SESSION_SPEED_MODES = ["standard", "fast"];
51708
+ var SESSION_SPEED_OBSERVATIONS = [...SESSION_SPEED_MODES, "unknown"];
51696
51709
  function isSessionSource(source) {
51697
51710
  return typeof source === "string" && SESSION_SOURCES.includes(source);
51698
51711
  }
@@ -78558,6 +78571,23 @@ var storedCredentialsSchema = external_exports.object({
78558
78571
  * alongside the owners. Absent = the org has no list — the owner check
78559
78572
  * decides (and pre-list enrollments read the same way). */
78560
78573
  allowedRepos: external_exports.array(external_exports.string()).optional(),
78574
+ /** org-repos mode: per-workspace claim policies across every workspace
78575
+ * the member belongs to (home first), refreshed every sync — the union
78576
+ * an offline pass still filters with. Absent = cached before routing;
78577
+ * the flat fields above are the whole boundary. */
78578
+ workspacePolicies: external_exports.array(
78579
+ external_exports.object({
78580
+ orgId: external_exports.string().min(1),
78581
+ name: external_exports.string(),
78582
+ owners: external_exports.array(external_exports.string()),
78583
+ repos: external_exports.array(external_exports.string()).nullable(),
78584
+ /** This entry's claims were UNRESOLVED when cached (a linked
78585
+ * installation whose account failed to resolve, nothing usable to
78586
+ * stand in) — unknown, never "claims nothing". A pass reading only
78587
+ * the cache must not stamp a deep sweep done over it. */
78588
+ unresolved: external_exports.literal(true).optional()
78589
+ })
78590
+ ).optional(),
78561
78591
  /** Folders this member explicitly chose. Under scope mode they are the
78562
78592
  * whole boundary (never empty — enrollment requires at least one); under
78563
78593
  * org-repos they are additions on top of the org policy
@@ -78652,7 +78682,7 @@ import os6 from "node:os";
78652
78682
  // ../../apps/collector/src/http.ts
78653
78683
  var import_undici = __toESM(require_undici(), 1);
78654
78684
  var CLI_VERSION_HEADER = "x-powerplant-cli-version";
78655
- var cliVersion = true ? "0.3.1" : "0.0.0-checkout";
78685
+ var cliVersion = true ? "0.4.0" : "0.0.0-checkout";
78656
78686
  function cliVersionHeaders() {
78657
78687
  return { [CLI_VERSION_HEADER]: cliVersion, "user-agent": `powerplant-cli/${cliVersion}` };
78658
78688
  }
@@ -78729,30 +78759,78 @@ import os5 from "node:os";
78729
78759
  import path3 from "node:path";
78730
78760
 
78731
78761
  // ../../apps/collector/src/gitOwner.ts
78762
+ import { execFileSync as execFileSync2 } from "node:child_process";
78732
78763
  import { readFileSync as readFileSync3, statSync } from "node:fs";
78733
78764
  import os4 from "node:os";
78734
78765
  import path2 from "node:path";
78735
- function parseGithubRepo(remoteUrl) {
78766
+ var GITHUB_HOSTS = /* @__PURE__ */ new Set(["github.com", "ssh.github.com"]);
78767
+ function parseRemoteUrl(remoteUrl) {
78736
78768
  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
- }
78769
+ const colon = trimmed.indexOf(":");
78770
+ if (!trimmed.includes("://") && colon > 0 && !trimmed.slice(0, colon).includes("/")) {
78771
+ const host = trimmed.slice(0, colon).split("@").at(-1) ?? "";
78772
+ return { host: host.toLowerCase(), path: trimmed.slice(colon + 1), overSsh: true };
78745
78773
  }
78746
78774
  try {
78747
78775
  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, "")}`;
78776
+ if (!url2.hostname) return null;
78777
+ const scheme = url2.protocol.slice(0, -1).toLowerCase();
78778
+ return {
78779
+ host: url2.hostname.toLowerCase(),
78780
+ path: url2.pathname,
78781
+ overSsh: scheme === "ssh" || scheme === "git+ssh"
78782
+ };
78783
+ } catch {
78784
+ return null;
78785
+ }
78786
+ }
78787
+ function parseGithubRepo(remoteUrl, sshHostnameOf = sshEffectiveHostname) {
78788
+ const remote = parseRemoteUrl(remoteUrl);
78789
+ if (!remote) return null;
78790
+ const host = remote.overSsh && !GITHUB_HOSTS.has(remote.host) ? sshHostnameOf(remote.host)?.toLowerCase() : remote.host;
78791
+ if (!host || !GITHUB_HOSTS.has(host)) return null;
78792
+ const [owner, rawRepo, extra] = remote.path.split("/").filter(Boolean);
78793
+ if (!owner || !rawRepo || extra !== void 0) return null;
78794
+ return `${owner.toLowerCase()}/${rawRepo.replace(/\.git$/i, "")}`;
78795
+ }
78796
+ var SSH_PROBE_TIMEOUT_MS = 2e3;
78797
+ var SSH_PROBE_CACHE_TTL_MS = 5 * 6e4;
78798
+ var SSH_HOST_PATTERN = /^[a-z0-9][a-z0-9._-]*$/i;
78799
+ var sshProbeCache = /* @__PURE__ */ new Map();
78800
+ function sshEffectiveHostname(host, options = {}) {
78801
+ if (!SSH_HOST_PATTERN.test(host)) return null;
78802
+ const key = `${options.configFile ?? ""}\0${host}`;
78803
+ const cached3 = sshProbeCache.get(key);
78804
+ const now = Date.now();
78805
+ if (cached3 && cached3.expiresAt > now) return cached3.hostname;
78806
+ const hostname4 = probeSshHostname(host, options);
78807
+ sshProbeCache.set(key, { hostname: hostname4, expiresAt: now + SSH_PROBE_CACHE_TTL_MS });
78808
+ return hostname4;
78809
+ }
78810
+ function probeSshHostname(host, options) {
78811
+ try {
78812
+ const dump = execFileSync2(
78813
+ "ssh",
78814
+ ["-G", ...options.configFile ? ["-F", options.configFile] : [], host],
78815
+ {
78816
+ encoding: "utf8",
78817
+ timeout: options.timeoutMs ?? SSH_PROBE_TIMEOUT_MS,
78818
+ stdio: ["ignore", "pipe", "ignore"],
78819
+ windowsHide: true
78820
+ }
78821
+ );
78822
+ return hostnameFromSshConfigDump(dump);
78752
78823
  } catch {
78753
78824
  return null;
78754
78825
  }
78755
78826
  }
78827
+ function hostnameFromSshConfigDump(dump) {
78828
+ for (const line of dump.split("\n")) {
78829
+ const match = /^hostname\s+(\S+)$/i.exec(line.trim());
78830
+ if (match?.[1]) return match[1];
78831
+ }
78832
+ return null;
78833
+ }
78756
78834
  function parseOriginUrl(configText) {
78757
78835
  let inOrigin = false;
78758
78836
  for (const raw of configText.split("\n")) {
@@ -78818,17 +78896,17 @@ function findGitConfigPath(cwd, environment = {}) {
78818
78896
  const dirs = resolveGitDirs(cwd, environment);
78819
78897
  return dirs ? path2.join(dirs.commonDir, "config") : null;
78820
78898
  }
78821
- function resolveGithubRepo(cwd, environment = {}) {
78899
+ function resolveGithubRepo(cwd, environment = {}, sshHostnameOf = sshEffectiveHostname) {
78822
78900
  const configPath = findGitConfigPath(cwd, environment);
78823
78901
  const configText = configPath ? readIfExists(configPath) : null;
78824
78902
  const remoteUrl = configText ? parseOriginUrl(configText) : null;
78825
- return remoteUrl ? parseGithubRepo(remoteUrl) : null;
78903
+ return remoteUrl ? parseGithubRepo(remoteUrl, sshHostnameOf) : null;
78826
78904
  }
78827
- function repoResolver(environment = {}) {
78905
+ function repoResolver(environment = {}, sshHostnameOf = sshEffectiveHostname) {
78828
78906
  const cache = /* @__PURE__ */ new Map();
78829
78907
  return (cwd) => {
78830
78908
  const key = path2.resolve(cwd);
78831
- if (!cache.has(key)) cache.set(key, resolveGithubRepo(key, environment));
78909
+ if (!cache.has(key)) cache.set(key, resolveGithubRepo(key, environment, sshHostnameOf));
78832
78910
  return cache.get(key);
78833
78911
  };
78834
78912
  }
@@ -78860,17 +78938,19 @@ var captureEverythingUnresolved = {
78860
78938
  function captureDecision(cwd, policy) {
78861
78939
  switch (policy.mode) {
78862
78940
  case "scope":
78863
- return cwd !== void 0 && isWithinScope(cwd, policy.scopePaths, policy.environment) ? memberChoice(cwd, policy) : { capture: false };
78941
+ if (cwd === void 0) return { capture: false, reason: "no-cwd" };
78942
+ return isWithinScope(cwd, policy.scopePaths, policy.environment) ? memberChoice(cwd, policy) : { capture: false, reason: "outside-policy" };
78864
78943
  case "org-repos": {
78865
- if (cwd === void 0) return { capture: false };
78944
+ if (cwd === void 0) return { capture: false, reason: "no-cwd" };
78866
78945
  if (policy.memberFolders.length > 0 && isWithinScope(cwd, policy.memberFolders, policy.environment)) {
78867
78946
  return memberChoice(cwd, policy);
78868
78947
  }
78869
78948
  const repoFullName = policy.repoOf(cwd);
78870
- if (repoFullName === null) return { capture: false };
78871
- const repo = repoFullName.toLowerCase();
78872
- 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 };
78949
+ if (repoFullName === null) return { capture: false, reason: "no-github-repo" };
78950
+ const claims = policy.workspaceClaims ?? [
78951
+ { owners: policy.allowedOwners, repos: policy.allowedRepos }
78952
+ ];
78953
+ return claims.some((workspace) => workspaceClaimsRepo(repoFullName, workspace)) ? { capture: true, via: "org-policy", repoFullName } : { capture: false, reason: "outside-policy" };
78874
78954
  }
78875
78955
  }
78876
78956
  }
@@ -78882,9 +78962,6 @@ function memberChoice(cwd, policy) {
78882
78962
  ...repoFullName !== null && { repoFullName }
78883
78963
  };
78884
78964
  }
78885
- function ownerOfRepo(repoFullName) {
78886
- return repoFullName.split("/")[0] ?? "";
78887
- }
78888
78965
  function listSome(values, max = 6) {
78889
78966
  return values.length <= max ? values.join(", ") : `${values.slice(0, max).join(", ")} and ${values.length - max} more`;
78890
78967
  }
@@ -78893,8 +78970,11 @@ function describeCapturePolicy(policy) {
78893
78970
  case "scope":
78894
78971
  return `sessions under ${policy.scopePaths.join(", ")}`;
78895
78972
  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";
78897
- return policy.memberFolders.length > 0 ? `${repoPart}, plus folders you added: ${policy.memberFolders.join(", ")}` : repoPart;
78973
+ 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";
78974
+ const claiming = (policy.workspaceClaims ?? []).filter(workspaceClaimsAnyRepo);
78975
+ const severalWorkspaces = (policy.workspaceClaims?.length ?? 0) > 1;
78976
+ const withOthers = severalWorkspaces && claiming.length > 0 ? `${repoPart}, routing each repo to the workspace that syncs it (${listSome(claiming.map((workspace) => workspace.name))})` : repoPart;
78977
+ return policy.memberFolders.length > 0 ? `${withOthers}, plus folders you added: ${policy.memberFolders.join(", ")}` : withOthers;
78898
78978
  }
78899
78979
  }
78900
78980
  }
@@ -78910,14 +78990,24 @@ function describeStoredCapture(credentials) {
78910
78990
  mode: "org-repos",
78911
78991
  allowedOwners: credentials.allowedOwners ?? [],
78912
78992
  allowedRepos: credentials.allowedRepos ?? null,
78993
+ ...credentials.workspacePolicies !== void 0 && {
78994
+ workspaceClaims: credentials.workspacePolicies
78995
+ },
78913
78996
  memberFolders: credentials.scopePaths ?? [],
78914
78997
  repoOf: () => null
78915
78998
  });
78916
78999
  }
78917
- function describeSkipReason(policy, platform2 = process.platform) {
79000
+ function countCaptureSkip(skips, reason) {
79001
+ if (reason === "outside-policy") skips.skippedOutOfScope += 1;
79002
+ else skips.skippedNoRepo += 1;
79003
+ }
79004
+ function describeSkipReason(policy) {
78918
79005
  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`;
79006
+ return policy.allowedRepos !== null ? "outside the org's allowlisted repos, stayed local" : "not in org-owned repos, stayed local";
79007
+ }
79008
+ function describeNoRepoSkipReason(policy, platform2 = process.platform) {
79009
+ if (policy.mode !== "org-repos") return "with no working directory recorded, stayed local";
79010
+ 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
79011
  }
78922
79012
  function normalizeScopePath(raw) {
78923
79013
  const expanded = raw === "~" || raw.startsWith("~/") ? path3.join(os5.homedir(), raw.slice(1)) : raw;
@@ -78939,6 +79029,37 @@ function isPathWithinScopes(resolved, scopePaths) {
78939
79029
  );
78940
79030
  }
78941
79031
 
79032
+ // ../../apps/collector/src/syncWindow.ts
79033
+ var STEADY_SYNC_WINDOW_DAYS = 3;
79034
+ var DEEP_FIRST_SYNC_DAYS = 90;
79035
+ var DEEP_FIRST_SYNC_MAX_SESSIONS = 100;
79036
+ function resolveSyncWindow(explicitSince, deepSyncedAt) {
79037
+ if (explicitSince !== void 0) {
79038
+ const days = Number(explicitSince);
79039
+ if (!Number.isFinite(days) || days <= 0) {
79040
+ throw new ExpectedCliError(`--since must be a positive number of days, got "${explicitSince}"`);
79041
+ }
79042
+ return { days, deepSweep: false };
79043
+ }
79044
+ if (deepSyncedAt === void 0) return { days: DEEP_FIRST_SYNC_DAYS, deepSweep: true };
79045
+ return { days: STEADY_SYNC_WINDOW_DAYS, deepSweep: false };
79046
+ }
79047
+ var DAY_MS = 24 * 60 * 60 * 1e3;
79048
+ function resolveScanFloor(window2, nowMs, narrowSinceMs) {
79049
+ if (window2.deepSweep || narrowSinceMs === void 0) return nowMs - window2.days * DAY_MS;
79050
+ return narrowSinceMs;
79051
+ }
79052
+ function rawShipFloorMs(appEnv, nowMs, explicitSince) {
79053
+ if (appEnv !== "development" || explicitSince !== void 0) return null;
79054
+ return nowMs - STEADY_SYNC_WINDOW_DAYS * DAY_MS;
79055
+ }
79056
+ function describeScanReach(window2, narrowSinceMs) {
79057
+ if (window2.deepSweep || narrowSinceMs === void 0) {
79058
+ return `from the last ${window2.days} day(s)`;
79059
+ }
79060
+ return `changed since ${new Date(narrowSinceMs).toISOString()}`;
79061
+ }
79062
+
78942
79063
  // ../../apps/collector/src/upload.ts
78943
79064
  import { readFileSync as readFileSync4 } from "node:fs";
78944
79065
  import { gzipSync } from "node:zlib";
@@ -78996,7 +79117,16 @@ async function fetchCapturePolicy(apiUrl, machineToken) {
78996
79117
  data: external_exports.object({
78997
79118
  owners: external_exports.array(external_exports.string()),
78998
79119
  repos: external_exports.array(external_exports.string()).nullable(),
78999
- integration: external_exports.enum(["none", "linked"])
79120
+ integration: external_exports.enum(["none", "linked"]),
79121
+ workspaces: external_exports.array(
79122
+ external_exports.object({
79123
+ orgId: external_exports.string().min(1),
79124
+ name: external_exports.string(),
79125
+ owners: external_exports.array(external_exports.string()),
79126
+ repos: external_exports.array(external_exports.string()).nullable(),
79127
+ integration: external_exports.enum(["none", "linked"])
79128
+ })
79129
+ ).optional()
79000
79130
  })
79001
79131
  }),
79002
79132
  await response.json().catch(() => null),
@@ -79008,6 +79138,7 @@ async function uploadSessions(apiUrl, machineToken, items) {
79008
79138
  let upserted = 0;
79009
79139
  let unchanged = 0;
79010
79140
  let outOfPolicy = 0;
79141
+ let deferred = 0;
79011
79142
  for (let offset = 0; offset < items.length; offset += MAX_INGEST_BATCH) {
79012
79143
  const batch = items.slice(offset, offset + MAX_INGEST_BATCH);
79013
79144
  const response = await fetchWithRetry("uploading session metadata", `${apiUrl}/api/ingest/sessions`, {
@@ -79026,7 +79157,8 @@ async function uploadSessions(apiUrl, machineToken, items) {
79026
79157
  data: external_exports.object({
79027
79158
  upserted: external_exports.number(),
79028
79159
  unchanged: external_exports.number(),
79029
- outOfPolicy: external_exports.number()
79160
+ outOfPolicy: external_exports.number(),
79161
+ deferred: external_exports.number().optional()
79030
79162
  })
79031
79163
  }),
79032
79164
  await response.json().catch(() => null),
@@ -79035,8 +79167,9 @@ async function uploadSessions(apiUrl, machineToken, items) {
79035
79167
  upserted += result.data.upserted;
79036
79168
  unchanged += result.data.unchanged;
79037
79169
  outOfPolicy += result.data.outOfPolicy;
79170
+ deferred += result.data.deferred ?? 0;
79038
79171
  }
79039
- return { upserted, unchanged, outOfPolicy };
79172
+ return { upserted, unchanged, outOfPolicy, deferred };
79040
79173
  }
79041
79174
  async function uploadBranchFacts(apiUrl, machineToken, facts) {
79042
79175
  let recorded = 0;
@@ -79086,6 +79219,7 @@ async function uploadRawSessions(apiUrl, machineToken, collected, readTranscript
79086
79219
  uploaded: 0,
79087
79220
  skippedTooLarge: 0,
79088
79221
  skippedUnchanged: 0,
79222
+ missingMetadata: 0,
79089
79223
  failed: [],
79090
79224
  unconfigured: false
79091
79225
  };
@@ -79118,6 +79252,10 @@ async function uploadRawSessions(apiUrl, machineToken, collected, readTranscript
79118
79252
  summary.unconfigured = true;
79119
79253
  return summary;
79120
79254
  }
79255
+ if (response.status === 404) {
79256
+ summary.missingMetadata += 1;
79257
+ continue;
79258
+ }
79121
79259
  if (!response.ok) {
79122
79260
  throw new Error(`${response.status}: ${await errorDetail(response)}`);
79123
79261
  }
@@ -79201,7 +79339,8 @@ async function runEnrollment(config2, options) {
79201
79339
  log: log3
79202
79340
  });
79203
79341
  let orgPolicy = captureMode === "org-repos" ? await fetchCapturePolicy(config2.apiUrl, result.token) : void 0;
79204
- if (orgPolicy !== void 0 && orgPolicy.owners.length === 0 && orgPolicy.repos === null) {
79342
+ const anyWorkspaceClaims = (orgPolicy?.workspaces ?? []).some(workspaceClaimsAnyRepo);
79343
+ if (orgPolicy !== void 0 && orgPolicy.owners.length === 0 && orgPolicy.repos === null && !anyWorkspaceClaims) {
79205
79344
  if (config2.appEnv === "development") {
79206
79345
  const home = normalizeScopePath(os6.homedir());
79207
79346
  log3(
@@ -79233,10 +79372,20 @@ async function runEnrollment(config2, options) {
79233
79372
  );
79234
79373
  }
79235
79374
  }
79375
+ const workspacePolicies = orgPolicy?.workspaces?.map((workspace) => ({
79376
+ orgId: workspace.orgId,
79377
+ name: workspace.name,
79378
+ owners: workspace.owners,
79379
+ repos: workspace.repos,
79380
+ // Cached unknownness survives enrollment: an unresolved workspace's
79381
+ // claims are unknown, and the first sweep must not stamp over them.
79382
+ ...workspace.integration === "linked" && workspace.owners.length === 0 && workspace.repos === null && { unresolved: true }
79383
+ }));
79236
79384
  const policy = captureMode === "org-repos" ? {
79237
79385
  mode: "org-repos",
79238
79386
  allowedOwners: orgPolicy?.owners ?? [],
79239
79387
  allowedRepos: orgPolicy?.repos ?? null,
79388
+ ...workspacePolicies !== void 0 && { workspaceClaims: workspacePolicies },
79240
79389
  memberFolders: [],
79241
79390
  repoOf: () => null
79242
79391
  } : { mode: "scope", scopePaths, repoOf: () => null };
@@ -79251,12 +79400,16 @@ async function runEnrollment(config2, options) {
79251
79400
  captureMode,
79252
79401
  ...orgPolicy && { allowedOwners: orgPolicy.owners },
79253
79402
  ...orgPolicy?.repos != null && { allowedRepos: orgPolicy.repos },
79403
+ ...captureMode === "org-repos" && workspacePolicies !== void 0 && { workspacePolicies },
79254
79404
  scopePaths
79255
79405
  };
79256
79406
  const location = saveCredentials(config2.appEnv, credentials);
79257
79407
  log3(`Enrolled "${result.machine.name}" (${result.machine.id}) into org "${result.org.name}".`);
79258
79408
  log3(`Machine token stored in ${location}.`);
79259
79409
  log3(`Syncing: ${describeCapturePolicy(policy)}.`);
79410
+ log3(
79411
+ `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.`
79412
+ );
79260
79413
  return credentials;
79261
79414
  }
79262
79415
  async function enroll(config2, options, createAuthClient = gotrueAuthClient) {
@@ -79453,7 +79606,7 @@ function addFolder(config2, rawPath, enrollCommand, log3, deps = defaultDeps) {
79453
79606
  });
79454
79607
  const org = credentials.orgName ?? "your org";
79455
79608
  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.`
79609
+ 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
79610
  );
79458
79611
  log3("Takes effect on the next sync pass; run the sync command to apply now.");
79459
79612
  }
@@ -79570,6 +79723,10 @@ function readIfExists2(file2) {
79570
79723
  throw error52;
79571
79724
  }
79572
79725
  }
79726
+ var MAX_TRANSCRIPT_FILE_BYTES = 256 * 1024 * 1024;
79727
+ function describeReadError(error52) {
79728
+ return error52 instanceof Error ? error52.message : String(error52);
79729
+ }
79573
79730
 
79574
79731
  // ../../apps/collector/src/lifecycleJournal.ts
79575
79732
  var LIFECYCLE_JOURNAL_FILENAME = "session-lifecycle.jsonl";
@@ -79611,6 +79768,7 @@ function parseClaudeCodeTranscript(text) {
79611
79768
  let cwd;
79612
79769
  let gitBranch;
79613
79770
  let anonCounter = 0;
79771
+ let speedMode;
79614
79772
  for (const record2 of parseJsonl(text)) {
79615
79773
  if (record2.type === "custom-title" && typeof record2.customTitle === "string") {
79616
79774
  title = record2.customTitle;
@@ -79629,25 +79787,42 @@ function parseClaudeCodeTranscript(text) {
79629
79787
  const message = record2.message;
79630
79788
  if (!message || message.model === "<synthetic>") continue;
79631
79789
  const id = message.id ?? (typeof record2.requestId === "string" ? record2.requestId : void 0) ?? (typeof record2.uuid === "string" ? record2.uuid : void 0) ?? `anon-${anonCounter++}`;
79632
- const isNewMessage = !usageByMessage.has(id);
79633
- if (message.usage) usageByMessage.set(id, toUsage(message.usage));
79634
- else if (isNewMessage) usageByMessage.set(id, { input: 0, cacheRead: 0, cacheCreation: 0, output: 0 });
79790
+ const previousMessage = usageByMessage.get(id);
79791
+ const isNewMessage = previousMessage === void 0;
79792
+ const speedValue = message.usage?.speed;
79793
+ const observedSpeed = speedModeOf(speedValue);
79794
+ const messageSpeed = observedSpeed ?? previousMessage?.speedMode;
79795
+ if (speedValue !== void 0) speedMode = observedSpeed ?? "unknown";
79796
+ if (message.usage) {
79797
+ usageByMessage.set(id, {
79798
+ usage: toUsage(message.usage),
79799
+ ...messageSpeed && { speedMode: messageSpeed }
79800
+ });
79801
+ } else if (isNewMessage) {
79802
+ usageByMessage.set(id, {
79803
+ usage: { input: 0, cacheRead: 0, cacheCreation: 0, output: 0 }
79804
+ });
79805
+ }
79635
79806
  if (isNewMessage && typeof message.model === "string") {
79636
79807
  modelCounts.set(message.model, (modelCounts.get(message.model) ?? 0) + 1);
79637
79808
  }
79638
79809
  }
79639
79810
  if (usageByMessage.size === 0 && timestamps.length === 0) return null;
79640
- const summed = [...usageByMessage.values()].reduce(addUsage, {
79811
+ const summed = [...usageByMessage.values()].map((entry) => entry.usage).reduce(addUsage, {
79641
79812
  input: 0,
79642
79813
  cacheRead: 0,
79643
79814
  cacheCreation: 0,
79644
79815
  output: 0
79645
79816
  });
79817
+ const speedFullyMeasured = usageByMessage.size > 0 && [...usageByMessage.values()].every((entry) => entry.speedMode !== void 0);
79818
+ const fastUsage = speedFullyMeasured ? [...usageByMessage.values()].filter((entry) => entry.speedMode === "fast").map((entry) => entry.usage).reduce(addUsage, { input: 0, cacheRead: 0, cacheCreation: 0, output: 0 }) : null;
79646
79819
  return {
79647
79820
  startedAt: timestamps[0],
79648
79821
  endedAt: timestamps[timestamps.length - 1],
79649
79822
  model: dominantModel(modelCounts),
79650
79823
  usage: usageByMessage.size > 0 ? summed : null,
79824
+ fastUsage,
79825
+ ...speedMode && { speedMode },
79651
79826
  title,
79652
79827
  turnCount: usageByMessage.size,
79653
79828
  cwd,
@@ -79658,9 +79833,13 @@ async function collectClaudeCodeSessions(options = {}) {
79658
79833
  const claudeDir = options.claudeDir ?? path5.join(os7.homedir(), ".claude");
79659
79834
  const now = options.now ?? Date.now();
79660
79835
  const capture = options.capture ?? captureEverythingUnresolved;
79836
+ const maxBytes = options.maxTranscriptBytes ?? MAX_TRANSCRIPT_FILE_BYTES;
79661
79837
  const projectsDir = path5.join(claudeDir, "projects");
79662
79838
  const items = [];
79663
- let skippedOutOfScope = 0;
79839
+ const skips = { skippedOutOfScope: 0, skippedNoRepo: 0 };
79840
+ const candidates = [];
79841
+ const unreadable = [];
79842
+ let skippedTooLarge = 0;
79664
79843
  const home = path5.dirname(claudeDir);
79665
79844
  const lifecycle = options.lifecycle ?? readLifecycleJournal(home);
79666
79845
  for (const projectDir of listDirs(projectsDir)) {
@@ -79670,39 +79849,76 @@ async function collectClaudeCodeSessions(options = {}) {
79670
79849
  const stat = statIfExists(transcriptPath);
79671
79850
  if (!stat) continue;
79672
79851
  if (options.sinceMs !== void 0 && stat.mtimeMs < options.sinceMs) continue;
79852
+ if (stat.size > maxBytes) {
79853
+ skippedTooLarge += 1;
79854
+ continue;
79855
+ }
79673
79856
  const sessionId = path5.basename(entry, ".jsonl");
79674
79857
  const endObservedAt = observedSessionEnd(lifecycle.get(sessionId), stat.mtimeMs);
79675
79858
  const status = endObservedAt === null && now - stat.mtimeMs < ACTIVE_GRACE_MS ? "active" : "inactive";
79676
- if (options.locationsOnly) {
79859
+ try {
79677
79860
  const cwd = transcriptCwd(transcriptPath);
79678
- const decision2 = captureDecision(cwd, capture);
79679
- if (!decision2.capture) {
79680
- skippedOutOfScope += 1;
79861
+ const decision = captureDecision(cwd, capture);
79862
+ if (!decision.capture) {
79863
+ countCaptureSkip(skips, decision.reason);
79681
79864
  continue;
79682
79865
  }
79683
- items.push({
79684
- mtimeMs: stat.mtimeMs,
79866
+ if (options.locationsOnly) {
79867
+ items.push({
79868
+ mtimeMs: stat.mtimeMs,
79869
+ transcriptPath,
79870
+ ...cwd && { cwd },
79871
+ item: { source: "claude-code", externalId: sessionId, status, capturedVia: decision.via }
79872
+ });
79873
+ continue;
79874
+ }
79875
+ candidates.push({
79685
79876
  transcriptPath,
79877
+ projectDir,
79878
+ sessionId,
79879
+ mtimeMs: stat.mtimeMs,
79880
+ status,
79881
+ endObservedAt,
79686
79882
  ...cwd && { cwd },
79687
- item: { source: "claude-code", externalId: sessionId, status, capturedVia: decision2.via }
79883
+ decision
79688
79884
  });
79689
- continue;
79885
+ } catch (error52) {
79886
+ unreadable.push(`${sessionId}: ${describeReadError(error52)}`);
79690
79887
  }
79888
+ }
79889
+ }
79890
+ if (options.locationsOnly) {
79891
+ return {
79892
+ sessions: items.sort((a, b2) => b2.mtimeMs - a.mtimeMs),
79893
+ ...skips,
79894
+ skippedTooLarge,
79895
+ unreadable,
79896
+ heldBack: 0
79897
+ };
79898
+ }
79899
+ candidates.sort((a, b2) => b2.mtimeMs - a.mtimeMs);
79900
+ const chosen = options.maxSessions === void 0 ? candidates : candidates.slice(0, options.maxSessions);
79901
+ for (const candidate of chosen) {
79902
+ await yieldToEventLoop();
79903
+ const { transcriptPath, projectDir, sessionId, mtimeMs, status, endObservedAt, cwd, decision } = candidate;
79904
+ try {
79691
79905
  const text = readIfExists2(transcriptPath);
79692
79906
  if (text === null) continue;
79693
79907
  const summary = parseClaudeCodeTranscript(text);
79694
79908
  if (!summary) continue;
79695
- const decision = captureDecision(summary.cwd, capture);
79696
- if (!decision.capture) {
79697
- skippedOutOfScope += 1;
79698
- continue;
79699
- }
79700
- const usage = addSubagentUsage(summary.usage, path5.join(projectDir, sessionId, "subagents"));
79909
+ const usage = addSubagentUsage(
79910
+ summary.usage,
79911
+ summary.fastUsage,
79912
+ path5.join(projectDir, sessionId, "subagents"),
79913
+ { maxBytes, sessionId }
79914
+ );
79915
+ skippedTooLarge += usage.skippedTooLarge;
79916
+ unreadable.push(...usage.unreadable);
79701
79917
  const endedAt = endObservedAt ?? summary.endedAt;
79702
79918
  items.push({
79703
- mtimeMs: stat.mtimeMs,
79919
+ mtimeMs,
79704
79920
  transcriptPath,
79705
- ...summary.cwd && { cwd: summary.cwd },
79921
+ ...cwd && { cwd },
79706
79922
  item: {
79707
79923
  source: "claude-code",
79708
79924
  externalId: sessionId,
@@ -79710,28 +79926,42 @@ async function collectClaudeCodeSessions(options = {}) {
79710
79926
  ...summary.startedAt && { startedAt: summary.startedAt },
79711
79927
  ...endedAt && { endedAt },
79712
79928
  ...summary.model && { model: summary.model },
79929
+ ...summary.speedMode && { speedMode: summary.speedMode },
79713
79930
  // Total input-class tokens the source reported (uncached + cache
79714
79931
  // reads + cache writes) — a real reported total, not an estimate.
79715
79932
  // The cache breakdown rides along: pricing bills reads at 0.1×
79716
79933
  // and writes at 1.25×, so collapsing it would inflate estimates.
79717
- ...usage && { inputTokens: usage.input + usage.cacheRead + usage.cacheCreation },
79718
- ...usage && { outputTokens: usage.output },
79719
- ...usage && { cacheReadTokens: usage.cacheRead },
79720
- ...usage && { cacheWriteTokens: usage.cacheCreation },
79934
+ ...usage.total && {
79935
+ inputTokens: usage.total.input + usage.total.cacheRead + usage.total.cacheCreation
79936
+ },
79937
+ ...usage.total && { outputTokens: usage.total.output },
79938
+ ...usage.total && { cacheReadTokens: usage.total.cacheRead },
79939
+ ...usage.total && { cacheWriteTokens: usage.total.cacheCreation },
79940
+ ...usage.fast && {
79941
+ fastInputTokens: usage.fast.input + usage.fast.cacheRead + usage.fast.cacheCreation,
79942
+ fastOutputTokens: usage.fast.output,
79943
+ fastCacheReadTokens: usage.fast.cacheRead,
79944
+ fastCacheWriteTokens: usage.fast.cacheCreation
79945
+ },
79721
79946
  ...summary.title && { title: redactText(summary.title).slice(0, 500) },
79722
79947
  // The PR-join coordinates: the checkout's directory name (bare
79723
79948
  // repo, not the local path) and the branch the work started on.
79724
- ...summary.cwd && { repoDir: path5.basename(summary.cwd) },
79949
+ ...cwd && { repoDir: path5.basename(cwd) },
79725
79950
  ...summary.gitBranch && { gitBranch: summary.gitBranch },
79726
79951
  ...decision.repoFullName && { repoFullName: decision.repoFullName },
79727
79952
  capturedVia: decision.via
79728
79953
  }
79729
79954
  });
79955
+ } catch (error52) {
79956
+ unreadable.push(`${sessionId}: ${describeReadError(error52)}`);
79730
79957
  }
79731
79958
  }
79732
79959
  return {
79733
79960
  sessions: items.sort((a, b2) => b2.mtimeMs - a.mtimeMs),
79734
- skippedOutOfScope
79961
+ ...skips,
79962
+ skippedTooLarge,
79963
+ unreadable,
79964
+ heldBack: candidates.length - chosen.length
79735
79965
  };
79736
79966
  }
79737
79967
  function transcriptCwd(transcriptPath) {
@@ -79750,6 +79980,9 @@ function toUsage(u) {
79750
79980
  output: n(u.output_tokens)
79751
79981
  };
79752
79982
  }
79983
+ function speedModeOf(value) {
79984
+ return value === "fast" || value === "standard" ? value : void 0;
79985
+ }
79753
79986
  function addUsage(a, b2) {
79754
79987
  return {
79755
79988
  input: a.input + b2.input,
@@ -79758,15 +79991,36 @@ function addUsage(a, b2) {
79758
79991
  output: a.output + b2.output
79759
79992
  };
79760
79993
  }
79761
- function addSubagentUsage(main2, subagentsDir) {
79762
- let usage = main2;
79994
+ function addSubagentUsage(main2, mainFast, subagentsDir, bounds) {
79995
+ let total = main2;
79996
+ let fast = mainFast;
79997
+ let fastCoverageComplete = main2 === null || mainFast !== null;
79998
+ let skippedTooLarge = 0;
79999
+ const unreadable = [];
79763
80000
  for (const entry of listFiles(subagentsDir, ".jsonl")) {
79764
- const text = readIfExists2(path5.join(subagentsDir, entry));
79765
- if (text === null) continue;
79766
- const summary = parseClaudeCodeTranscript(text);
79767
- if (summary?.usage) usage = addUsage(usage ?? { input: 0, cacheRead: 0, cacheCreation: 0, output: 0 }, summary.usage);
80001
+ const sidecarPath = path5.join(subagentsDir, entry);
80002
+ try {
80003
+ const stat = statIfExists(sidecarPath);
80004
+ if (stat === null) continue;
80005
+ if (stat.size > bounds.maxBytes) {
80006
+ skippedTooLarge += 1;
80007
+ continue;
80008
+ }
80009
+ const text = readIfExists2(sidecarPath);
80010
+ if (text === null) continue;
80011
+ const summary = parseClaudeCodeTranscript(text);
80012
+ if (summary?.usage) {
80013
+ total = addUsage(total ?? { input: 0, cacheRead: 0, cacheCreation: 0, output: 0 }, summary.usage);
80014
+ if (summary.fastUsage === null) fastCoverageComplete = false;
80015
+ }
80016
+ if (summary?.fastUsage) {
80017
+ fast = addUsage(fast ?? { input: 0, cacheRead: 0, cacheCreation: 0, output: 0 }, summary.fastUsage);
80018
+ }
80019
+ } catch (error52) {
80020
+ unreadable.push(`${bounds.sessionId} (subagent ${entry}): ${describeReadError(error52)}`);
80021
+ }
79768
80022
  }
79769
- return usage;
80023
+ return { total, fast: fastCoverageComplete ? fast : null, skippedTooLarge, unreadable };
79770
80024
  }
79771
80025
  function dominantModel(counts) {
79772
80026
  let best;
@@ -79785,6 +80039,10 @@ function parseCodexRollout(text) {
79785
80039
  let meta3 = null;
79786
80040
  let model;
79787
80041
  let totals = null;
80042
+ let fastTotals = { input: 0, cached: 0, output: 0 };
80043
+ let speedMode;
80044
+ let fastCoverageComplete = true;
80045
+ let classifiedUsage = false;
79788
80046
  const timestamps = [];
79789
80047
  for (const line of parseJsonl(text)) {
79790
80048
  if (typeof line.timestamp === "string") timestamps.push(line.timestamp);
@@ -79795,15 +80053,38 @@ function parseCodexRollout(text) {
79795
80053
  meta3 ??= record2;
79796
80054
  } else if (line.type === "turn_context" && typeof record2.model === "string") {
79797
80055
  model = record2.model;
80056
+ } else if (line.type === "event_msg" && record2.type === "thread_settings_applied") {
80057
+ const settings = record2.thread_settings;
80058
+ if (settings && typeof settings === "object" && !Array.isArray(settings)) {
80059
+ const tier = settings.service_tier;
80060
+ if (tier !== void 0) speedMode = codexSpeedMode(tier) ?? "unknown";
80061
+ }
79798
80062
  } else if (line.type === "event_msg" && record2.type === "token_count") {
79799
80063
  const info = record2.info;
79800
80064
  const usage = info?.total_token_usage;
79801
80065
  if (usage) {
79802
80066
  const n = (value) => typeof value === "number" ? value : 0;
79803
- totals ??= { input: 0, cached: 0, output: 0 };
79804
- totals.input = Math.max(totals.input, n(usage.input_tokens));
79805
- totals.cached = Math.max(totals.cached, n(usage.cached_input_tokens));
79806
- totals.output = Math.max(totals.output, n(usage.output_tokens));
80067
+ const next = {
80068
+ input: n(usage.input_tokens),
80069
+ cached: n(usage.cached_input_tokens),
80070
+ output: n(usage.output_tokens)
80071
+ };
80072
+ const previous = totals ?? { input: 0, cached: 0, output: 0 };
80073
+ const delta = {
80074
+ input: Math.max(0, next.input - previous.input),
80075
+ cached: Math.max(0, next.cached - previous.cached),
80076
+ output: Math.max(0, next.output - previous.output)
80077
+ };
80078
+ if (delta.input > 0 || delta.cached > 0 || delta.output > 0) {
80079
+ if (speedMode === "fast" || speedMode === "standard") classifiedUsage = true;
80080
+ else fastCoverageComplete = false;
80081
+ if (speedMode === "fast") fastTotals = addCodexTotals(fastTotals, delta);
80082
+ }
80083
+ totals = {
80084
+ input: Math.max(previous.input, next.input),
80085
+ cached: Math.max(previous.cached, next.cached),
80086
+ output: Math.max(previous.output, next.output)
80087
+ };
79807
80088
  }
79808
80089
  }
79809
80090
  }
@@ -79817,6 +80098,8 @@ function parseCodexRollout(text) {
79817
80098
  endedAt: timestamps[timestamps.length - 1],
79818
80099
  ...model && { model },
79819
80100
  totals,
80101
+ fastTotals: fastCoverageComplete && classifiedUsage ? fastTotals : null,
80102
+ ...speedMode && { speedMode },
79820
80103
  ...typeof meta3?.cwd === "string" && meta3.cwd !== "" && { cwd: meta3.cwd },
79821
80104
  ...typeof git?.branch === "string" && git.branch !== "" && { gitBranch: git.branch },
79822
80105
  ...repoNameFromUrl(git?.repository_url) ?? {},
@@ -79830,14 +80113,23 @@ function parseRolloutFile(rolloutPath) {
79830
80113
  function rolloutLocation(rolloutPath) {
79831
80114
  const record2 = readFirstRecord(rolloutPath, (candidate) => candidate.type === "session_meta");
79832
80115
  const meta3 = record2?.payload;
79833
- if (!meta3 || typeof meta3 !== "object" || Array.isArray(meta3)) return { kind: "session", totals: null };
80116
+ if (!meta3 || typeof meta3 !== "object" || Array.isArray(meta3)) return { kind: "session", totals: null, fastTotals: null };
79834
80117
  const { thread_source: threadSource, cwd } = meta3;
79835
80118
  return {
79836
80119
  kind: threadSource === "subagent" ? "subagent" : "session",
79837
80120
  totals: null,
80121
+ fastTotals: null,
79838
80122
  ...typeof cwd === "string" && cwd !== "" && { cwd }
79839
80123
  };
79840
80124
  }
80125
+ function codexSpeedMode(value) {
80126
+ if (value === "priority" || value === "fast") return "fast";
80127
+ if (value === "default") return "standard";
80128
+ return void 0;
80129
+ }
80130
+ function addCodexTotals(a, b2) {
80131
+ return { input: a.input + b2.input, cached: a.cached + b2.cached, output: a.output + b2.output };
80132
+ }
79841
80133
  function repoFullNameFrom(url2) {
79842
80134
  const fullName = parseGithubRepo(url2);
79843
80135
  return fullName ? { repoFullName: fullName } : {};
@@ -79875,58 +80167,100 @@ async function collectCodexSessions(options = {}) {
79875
80167
  const sessionsDir = options.codexDir ?? path6.join(os8.homedir(), ".codex", "sessions");
79876
80168
  const now = options.now ?? Date.now();
79877
80169
  const capture = options.capture ?? captureEverythingUnresolved;
79878
- const threadNames = options.locationsOnly ? /* @__PURE__ */ new Map() : loadThreadNames(sessionsDir);
80170
+ const maxBytes = options.maxTranscriptBytes ?? MAX_TRANSCRIPT_FILE_BYTES;
79879
80171
  const items = [];
79880
- let skippedOutOfScope = 0;
80172
+ const candidates = [];
80173
+ const unreadable = [];
80174
+ const skips = { skippedOutOfScope: 0, skippedNoRepo: 0 };
79881
80175
  let skippedSubagentThreads = 0;
79882
- const collectFrom = async (rootDir, archived) => {
80176
+ let skippedTooLarge = 0;
80177
+ const externalIdOf = (rolloutPath) => path6.basename(rolloutPath, ".jsonl");
80178
+ const admitFrom = async (rootDir, archived) => {
79883
80179
  for (const rolloutPath of rolloutFiles(rootDir)) {
79884
80180
  await yieldToEventLoop2();
79885
80181
  const stat = statIfExists(rolloutPath);
79886
80182
  if (!stat) continue;
79887
80183
  if (options.sinceMs !== void 0 && Math.max(stat.mtimeMs, stat.ctimeMs) < options.sinceMs)
79888
80184
  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;
80185
+ if (stat.size > maxBytes) {
80186
+ skippedTooLarge += 1;
79898
80187
  continue;
79899
80188
  }
79900
- if (options.locationsOnly) {
79901
- items.push({
80189
+ try {
80190
+ const location = rolloutLocation(rolloutPath);
80191
+ if (location.kind === "subagent") {
80192
+ skippedSubagentThreads += 1;
80193
+ continue;
80194
+ }
80195
+ const decision = captureDecision(location.cwd, capture);
80196
+ if (!decision.capture) {
80197
+ countCaptureSkip(skips, decision.reason);
80198
+ continue;
80199
+ }
80200
+ if (options.locationsOnly) {
80201
+ items.push({
80202
+ mtimeMs: stat.mtimeMs,
80203
+ transcriptPath: rolloutPath,
80204
+ ...location.cwd && { cwd: location.cwd },
80205
+ item: {
80206
+ source: "codex",
80207
+ externalId: externalIdOf(rolloutPath),
80208
+ status: now - stat.mtimeMs < ACTIVE_GRACE_MS ? "active" : "inactive",
80209
+ archived,
80210
+ capturedVia: decision.via
80211
+ }
80212
+ });
80213
+ continue;
80214
+ }
80215
+ candidates.push({
80216
+ rolloutPath,
80217
+ archived,
79902
80218
  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
- }
80219
+ ...location.cwd && { cwd: location.cwd },
80220
+ decision
79912
80221
  });
79913
- continue;
80222
+ } catch (error52) {
80223
+ unreadable.push(`${externalIdOf(rolloutPath)}: ${describeReadError(error52)}`);
79914
80224
  }
79915
- const repoDir = summary.repoName ?? (summary.cwd ? path6.basename(summary.cwd) : void 0);
80225
+ }
80226
+ };
80227
+ await admitFrom(sessionsDir, false);
80228
+ await admitFrom(codexArchivedSessionsDir(sessionsDir), true);
80229
+ if (options.locationsOnly) {
80230
+ return {
80231
+ sessions: items.sort((a, b2) => b2.mtimeMs - a.mtimeMs),
80232
+ ...skips,
80233
+ skippedSubagentThreads,
80234
+ skippedTooLarge,
80235
+ unreadable,
80236
+ heldBack: 0
80237
+ };
80238
+ }
80239
+ candidates.sort((a, b2) => b2.mtimeMs - a.mtimeMs);
80240
+ const chosen = options.maxSessions === void 0 ? candidates : candidates.slice(0, options.maxSessions);
80241
+ const threadNames = chosen.length > 0 ? loadThreadNames(sessionsDir) : /* @__PURE__ */ new Map();
80242
+ for (const candidate of chosen) {
80243
+ await yieldToEventLoop2();
80244
+ const { rolloutPath, archived, mtimeMs, cwd, decision } = candidate;
80245
+ try {
80246
+ const summary = parseRolloutFile(rolloutPath);
80247
+ if (!summary) continue;
80248
+ const repoDir = summary.repoName ?? (cwd ? path6.basename(cwd) : void 0);
79916
80249
  const repoFullName = decision.repoFullName ?? summary.repoFullName;
79917
80250
  const title = summary.sessionId ? threadNames.get(summary.sessionId) : void 0;
79918
80251
  items.push({
79919
- mtimeMs: stat.mtimeMs,
80252
+ mtimeMs,
79920
80253
  transcriptPath: rolloutPath,
79921
- ...summary.cwd && { cwd: summary.cwd },
80254
+ ...cwd && { cwd },
79922
80255
  item: {
79923
80256
  source: "codex",
79924
- externalId: path6.basename(rolloutPath, ".jsonl"),
79925
- status: now - stat.mtimeMs < ACTIVE_GRACE_MS ? "active" : "inactive",
80257
+ externalId: externalIdOf(rolloutPath),
80258
+ status: now - mtimeMs < ACTIVE_GRACE_MS ? "active" : "inactive",
79926
80259
  archived,
79927
80260
  ...summary.startedAt && { startedAt: summary.startedAt },
79928
80261
  ...summary.endedAt && { endedAt: summary.endedAt },
79929
80262
  ...summary.model && { model: summary.model },
80263
+ ...summary.speedMode && { speedMode: summary.speedMode },
79930
80264
  ...title && { title: redactText(title).slice(0, 500) },
79931
80265
  // input_tokens already includes the cached subset — it IS the
79932
80266
  // all-in input-class total. No cache-write figure exists, so
@@ -79934,20 +80268,29 @@ async function collectCodexSessions(options = {}) {
79934
80268
  ...summary.totals && { inputTokens: summary.totals.input },
79935
80269
  ...summary.totals && { outputTokens: summary.totals.output },
79936
80270
  ...summary.totals && { cacheReadTokens: summary.totals.cached },
80271
+ ...summary.fastTotals && { fastInputTokens: summary.fastTotals.input },
80272
+ ...summary.fastTotals && { fastOutputTokens: summary.fastTotals.output },
80273
+ ...summary.fastTotals && { fastCacheReadTokens: summary.fastTotals.cached },
80274
+ // OpenAI prompt-cache writes have no separate price in the current
80275
+ // session cost model; zero is measured here, not an invented count.
80276
+ ...summary.fastTotals && { fastCacheWriteTokens: 0 },
79937
80277
  ...repoDir && { repoDir },
79938
80278
  ...summary.gitBranch && { gitBranch: summary.gitBranch },
79939
80279
  ...repoFullName && { repoFullName },
79940
80280
  capturedVia: decision.via
79941
80281
  }
79942
80282
  });
80283
+ } catch (error52) {
80284
+ unreadable.push(`${externalIdOf(rolloutPath)}: ${describeReadError(error52)}`);
79943
80285
  }
79944
- };
79945
- await collectFrom(sessionsDir, false);
79946
- await collectFrom(codexArchivedSessionsDir(sessionsDir), true);
80286
+ }
79947
80287
  return {
79948
80288
  sessions: items.sort((a, b2) => b2.mtimeMs - a.mtimeMs),
79949
- skippedOutOfScope,
79950
- skippedSubagentThreads
80289
+ ...skips,
80290
+ skippedSubagentThreads,
80291
+ skippedTooLarge,
80292
+ unreadable,
80293
+ heldBack: candidates.length - chosen.length
79951
80294
  };
79952
80295
  }
79953
80296
 
@@ -79975,9 +80318,12 @@ function cursorSpoolDir(home) {
79975
80318
  var emptyResult = () => ({
79976
80319
  sessions: [],
79977
80320
  skippedOutOfScope: 0,
80321
+ skippedNoRepo: 0,
79978
80322
  skippedSubagentComposers: 0,
79979
80323
  skippedNonLocalComposers: 0,
79980
80324
  skippedUnreadable: 0,
80325
+ skippedTooLarge: 0,
80326
+ truncatedTranscripts: 0,
79981
80327
  unavailable: null
79982
80328
  });
79983
80329
  var SQLITE_EXPERIMENTAL_WARNING = "SQLite is an experimental feature and might change at any time";
@@ -80005,9 +80351,6 @@ async function openCursorDatabase(dbPath) {
80005
80351
  const sqlite = await loadSqlite();
80006
80352
  return new sqlite.DatabaseSync(dbPath, { readOnly: true });
80007
80353
  }
80008
- function describeError(error52) {
80009
- return error52 instanceof Error ? error52.message : String(error52);
80010
- }
80011
80354
  function isRecord3(value) {
80012
80355
  return value !== null && typeof value === "object" && !Array.isArray(value);
80013
80356
  }
@@ -80082,6 +80425,8 @@ function toolFilePathOf(args) {
80082
80425
  }
80083
80426
  return void 0;
80084
80427
  }
80428
+ var SPOOL_META_RESERVE_BYTES = 64 * 1024;
80429
+ var lineBytes = (line) => Buffer.byteLength(JSON.stringify(line)) + 1;
80085
80430
  function bubbleLines(bubble) {
80086
80431
  const lines = [];
80087
80432
  const createdAt = typeof bubble.createdAt === "string" ? { createdAt: bubble.createdAt } : {};
@@ -80121,43 +80466,50 @@ function bubbleLines(bubble) {
80121
80466
  }
80122
80467
  return lines;
80123
80468
  }
80124
- function extractTranscript(db, composerId) {
80469
+ function extractTranscript(db, composerId, maxBytes) {
80125
80470
  const valueByKey = db.prepare("select value from cursorDiskKV where key = ?");
80126
80471
  const dataRow = valueByKey.get(`composerData:${composerId}`);
80127
80472
  const dataText = isRecord3(dataRow) && typeof dataRow.value === "string" ? dataRow.value : null;
80128
80473
  const data = dataText === null ? null : safeJsonRecord2(dataText);
80129
80474
  const spine = Array.isArray(data?.fullConversationHeadersOnly) ? data.fullConversationHeadersOnly : [];
80130
80475
  if (spine.length === 0) return null;
80476
+ const budget = maxBytes - SPOOL_META_RESERVE_BYTES;
80131
80477
  const lines = [];
80132
80478
  const modelCounts = /* @__PURE__ */ new Map();
80479
+ let bytes = 0;
80133
80480
  let bubbleCount = 0;
80134
- for (const entry of spine) {
80481
+ let bubblesOmitted = 0;
80482
+ for (const [index, entry] of spine.entries()) {
80135
80483
  if (!isRecord3(entry) || typeof entry.bubbleId !== "string") continue;
80136
80484
  const row = valueByKey.get(`bubbleId:${composerId}:${entry.bubbleId}`);
80137
80485
  const text = isRecord3(row) && typeof row.value === "string" ? row.value : null;
80138
80486
  const bubble = text === null ? null : safeJsonRecord2(text);
80139
80487
  if (bubble === null) continue;
80488
+ const bubbleLinesOut = bubbleLines(bubble);
80489
+ const bubbleBytes = bubbleLinesOut.reduce((sum2, line) => sum2 + lineBytes(line), 0);
80490
+ if (bytes + bubbleBytes > budget) {
80491
+ bubblesOmitted = spine.length - index;
80492
+ break;
80493
+ }
80494
+ bytes += bubbleBytes;
80140
80495
  bubbleCount += 1;
80141
- for (const line of bubbleLines(bubble)) {
80496
+ for (const line of bubbleLinesOut) {
80142
80497
  lines.push(line);
80143
80498
  if (line.type === "agent" && line.model !== void 0) {
80144
80499
  modelCounts.set(line.model, (modelCounts.get(line.model) ?? 0) + 1);
80145
80500
  }
80146
80501
  }
80147
80502
  }
80148
- if (bubbleCount === 0) return null;
80503
+ if (bubbleCount === 0 && bubblesOmitted === 0) return null;
80149
80504
  let model;
80150
80505
  let best = 0;
80151
80506
  for (const [name, count] of modelCounts) {
80152
80507
  if (count > best) [model, best] = [name, count];
80153
80508
  }
80154
- return { lines, ...model && { model }, bubbleCount };
80509
+ return { lines, ...model && { model }, bubbleCount, bubblesOmitted };
80155
80510
  }
80156
80511
  function readSpoolModel(spoolPath) {
80157
- const text = readIfExists2(spoolPath);
80158
- if (text === null) return void 0;
80159
- const newline = text.indexOf("\n");
80160
- const record2 = safeJsonRecord2(newline === -1 ? text : text.slice(0, newline));
80512
+ const record2 = readFirstRecord(spoolPath, () => true);
80161
80513
  if (record2 === null || record2.type !== "meta" || record2.format !== "cursor-local-transcript") {
80162
80514
  return void 0;
80163
80515
  }
@@ -80170,20 +80522,20 @@ async function collectCursorSessions(options = {}) {
80170
80522
  try {
80171
80523
  if (statIfExists(dbPath) === null) return result;
80172
80524
  } catch (error52) {
80173
- result.unavailable = `Cursor's session store isn't readable (${describeError(error52)})`;
80525
+ result.unavailable = `Cursor's session store isn't readable (${describeReadError(error52)})`;
80174
80526
  return result;
80175
80527
  }
80176
80528
  let db;
80177
80529
  try {
80178
80530
  db = await openCursorDatabase(dbPath);
80179
80531
  } catch (error52) {
80180
- result.unavailable = `Cursor's session store couldn't be opened (${describeError(error52)})`;
80532
+ result.unavailable = `Cursor's session store couldn't be opened (${describeReadError(error52)})`;
80181
80533
  return result;
80182
80534
  }
80183
80535
  try {
80184
80536
  collectFromStore(db, userDir, options, result);
80185
80537
  } catch (error52) {
80186
- result.unavailable = `reading Cursor's session store failed (${describeError(error52)})`;
80538
+ result.unavailable = `reading Cursor's session store failed (${describeReadError(error52)})`;
80187
80539
  } finally {
80188
80540
  try {
80189
80541
  db.close();
@@ -80197,6 +80549,7 @@ function collectFromStore(db, userDir, options, result) {
80197
80549
  const now = options.now ?? Date.now();
80198
80550
  const capture = options.capture ?? captureEverythingUnresolved;
80199
80551
  const spoolDir = options.spoolDir ?? cursorSpoolDir(os9.homedir());
80552
+ const maxBytes = options.maxTranscriptBytes ?? MAX_TRANSCRIPT_FILE_BYTES;
80200
80553
  let spoolDirReady = false;
80201
80554
  const headerRows = db.prepare(
80202
80555
  "select composerId, workspaceId, createdAt, lastUpdatedAt, isArchived, isSubagent, value from composerHeaders where lastUpdatedAt >= ? order by lastUpdatedAt desc"
@@ -80225,7 +80578,7 @@ function collectFromStore(db, userDir, options, result) {
80225
80578
  const cwd = facts.workspacePath ?? workspaceFolderOf(userDir, workspaceId);
80226
80579
  const decision = captureDecision(cwd, capture);
80227
80580
  if (!decision.capture) {
80228
- result.skippedOutOfScope += 1;
80581
+ countCaptureSkip(result, decision.reason);
80229
80582
  continue;
80230
80583
  }
80231
80584
  if (options.locationsOnly) {
@@ -80246,10 +80599,15 @@ function collectFromStore(db, userDir, options, result) {
80246
80599
  const spoolStat = statIfExists(spoolPath);
80247
80600
  let model;
80248
80601
  if (spoolStat !== null && Math.abs(spoolStat.mtimeMs - lastUpdatedAt) < 1) {
80602
+ if (spoolStat.size > maxBytes) {
80603
+ result.skippedTooLarge += 1;
80604
+ continue;
80605
+ }
80249
80606
  model = readSpoolModel(spoolPath);
80250
80607
  } else {
80251
- const transcript = extractTranscript(db, raw.composerId);
80608
+ const transcript = extractTranscript(db, raw.composerId, maxBytes);
80252
80609
  if (transcript === null) continue;
80610
+ if (transcript.bubblesOmitted > 0) result.truncatedTranscripts += 1;
80253
80611
  model = transcript.model;
80254
80612
  const meta3 = {
80255
80613
  type: "meta",
@@ -80261,7 +80619,8 @@ function collectFromStore(db, userDir, options, result) {
80261
80619
  ...model && { model },
80262
80620
  ...typeof raw.createdAt === "number" && { createdAt: raw.createdAt },
80263
80621
  lastUpdatedAt,
80264
- ...cwd && { workspacePath: cwd }
80622
+ ...cwd && { workspacePath: cwd },
80623
+ ...transcript.bubblesOmitted > 0 && { bubblesOmitted: transcript.bubblesOmitted }
80265
80624
  };
80266
80625
  if (!spoolDirReady) {
80267
80626
  mkdirSync2(spoolDir, { recursive: true });
@@ -80306,36 +80665,6 @@ function collectFromStore(db, userDir, options, result) {
80306
80665
  }
80307
80666
  }
80308
80667
 
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
80668
  // ../../apps/collector/src/protectedRepoDiscovery.ts
80340
80669
  var defaultDiscoveryDeps = {
80341
80670
  collectClaude: collectClaudeCodeSessions,
@@ -80512,7 +80841,7 @@ function installCollectorHooks(homeDir = os11.homedir()) {
80512
80841
  }
80513
80842
 
80514
80843
  // ../../apps/collector/src/service.ts
80515
- import { execFileSync as execFileSync2 } from "node:child_process";
80844
+ import { execFileSync as execFileSync3 } from "node:child_process";
80516
80845
  import { chmodSync as chmodSync2, existsSync as existsSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync7, rmSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "node:fs";
80517
80846
  import os12 from "node:os";
80518
80847
  import path10 from "node:path";
@@ -80660,7 +80989,7 @@ function restartService(config2) {
80660
80989
  }
80661
80990
  function runLaunchctl(args) {
80662
80991
  try {
80663
- execFileSync2("launchctl", args, { stdio: ["ignore", "ignore", "pipe"], encoding: "utf8" });
80992
+ execFileSync3("launchctl", args, { stdio: ["ignore", "ignore", "pipe"], encoding: "utf8" });
80664
80993
  } catch (cause) {
80665
80994
  throw new Error(launchctlFailureMessage(args[0] ?? "", cause), { cause });
80666
80995
  }
@@ -80675,7 +81004,7 @@ function launchctlFailureMessage(subcommand, cause) {
80675
81004
  }
80676
81005
  function isLoaded(appEnv) {
80677
81006
  try {
80678
- execFileSync2("launchctl", ["print", `gui/${uid()}/${label(appEnv)}`], { stdio: "ignore" });
81007
+ execFileSync3("launchctl", ["print", `gui/${uid()}/${label(appEnv)}`], { stdio: "ignore" });
80679
81008
  return true;
80680
81009
  } catch {
80681
81010
  return false;
@@ -80683,7 +81012,7 @@ function isLoaded(appEnv) {
80683
81012
  }
80684
81013
  function bootout(appEnv) {
80685
81014
  try {
80686
- execFileSync2("launchctl", ["bootout", `gui/${uid()}/${label(appEnv)}`], { stdio: "ignore" });
81015
+ execFileSync3("launchctl", ["bootout", `gui/${uid()}/${label(appEnv)}`], { stdio: "ignore" });
80687
81016
  return true;
80688
81017
  } catch {
80689
81018
  return false;
@@ -80984,12 +81313,33 @@ async function resolveCapturePolicy(config2, credentials, log3, deps = defaultPo
80984
81313
  }
80985
81314
  let owners = credentials.allowedOwners ?? [];
80986
81315
  let repos = credentials.allowedRepos ?? null;
81316
+ let workspacePolicies = credentials.workspacePolicies;
80987
81317
  let ownersUnresolved = false;
81318
+ let claimsUnresolved = false;
80988
81319
  try {
80989
81320
  const fresh = await deps.fetchPolicy(credentials.apiUrl, credentials.token);
80990
81321
  ownersUnresolved = fresh.integration === "linked" && fresh.owners.length === 0;
80991
81322
  const nextOwners = ownersUnresolved ? owners : fresh.owners;
80992
- const changed = nextOwners.join(",") !== owners.join(",") || (fresh.repos?.join(",") ?? null) !== (repos?.join(",") ?? null);
81323
+ const nextWorkspaces = fresh.workspaces?.map((workspace) => {
81324
+ const unresolved = workspace.integration === "linked" && workspace.owners.length === 0 && workspace.repos === null;
81325
+ const cached3 = workspacePolicies?.find((entry) => entry.orgId === workspace.orgId) ?? (credentials.orgId !== void 0 && workspace.orgId === credentials.orgId ? { orgId: workspace.orgId, name: workspace.name, owners, repos } : void 0);
81326
+ const usableCache = cached3 !== void 0 && workspaceClaimsAnyRepo(cached3);
81327
+ const effective = unresolved && usableCache ? cached3 : unresolved ? {
81328
+ orgId: workspace.orgId,
81329
+ name: workspace.name,
81330
+ owners: workspace.owners,
81331
+ repos: workspace.repos,
81332
+ unresolved: true
81333
+ } : {
81334
+ orgId: workspace.orgId,
81335
+ name: workspace.name,
81336
+ owners: workspace.owners,
81337
+ repos: workspace.repos
81338
+ };
81339
+ if (effective.unresolved) claimsUnresolved = true;
81340
+ return effective;
81341
+ });
81342
+ const changed = nextOwners.join(",") !== owners.join(",") || (fresh.repos?.join(",") ?? null) !== (repos?.join(",") ?? null) || JSON.stringify(nextWorkspaces ?? null) !== JSON.stringify(workspacePolicies ?? null);
80993
81343
  if (changed) {
80994
81344
  const next = {
80995
81345
  ...credentials,
@@ -80997,34 +81347,48 @@ async function resolveCapturePolicy(config2, credentials, log3, deps = defaultPo
80997
81347
  };
80998
81348
  if (fresh.repos !== null) next.allowedRepos = fresh.repos;
80999
81349
  else delete next.allowedRepos;
81350
+ if (nextWorkspaces !== void 0) next.workspacePolicies = nextWorkspaces;
81351
+ else delete next.workspacePolicies;
81000
81352
  deps.persist(config2.appEnv, next);
81001
81353
  }
81002
81354
  owners = nextOwners;
81003
81355
  repos = fresh.repos;
81356
+ workspacePolicies = nextWorkspaces;
81004
81357
  } catch (cause) {
81358
+ const cachedCoversSomething = owners.length > 0 || repos !== null || (workspacePolicies ?? []).some((entry) => workspaceClaimsAnyRepo(entry));
81359
+ if (!cachedCoversSomething || (workspacePolicies ?? []).some((entry) => entry.unresolved === true)) {
81360
+ claimsUnresolved = true;
81361
+ }
81005
81362
  log3(
81006
- `\u26A0 Couldn't refresh the org's repo sync policy (${describeSyncError(cause)}) \u2014 ` + (owners.length > 0 || repos !== null ? "using the cached policy." : "nothing will upload this pass.")
81363
+ `\u26A0 Couldn't refresh the org's repo sync policy (${describeSyncError(cause)}) \u2014 ` + (cachedCoversSomething ? "using the cached policy." : "nothing will upload this pass.")
81007
81364
  );
81008
81365
  }
81009
81366
  const memberFolders = credentials.scopePaths ?? [];
81367
+ if (ownersUnresolved && owners.length === 0 && repos === null) claimsUnresolved = true;
81010
81368
  if (ownersUnresolved) {
81011
81369
  log3(
81012
81370
  "\u26A0 The org's GitHub is connected, but its account couldn't be resolved just now \u2014 " + (owners.length > 0 ? `using the cached owner list: ${owners.join(", ")}.` : "no owner-based uploads this pass. This is usually temporary; syncing resumes when it resolves.")
81013
81371
  );
81014
81372
  }
81373
+ const homeOrgId = credentials.orgId ?? workspacePolicies?.[0]?.orgId;
81374
+ const foreignClaims = (workspacePolicies ?? []).filter(
81375
+ (entry) => entry.orgId !== homeOrgId && workspaceClaimsAnyRepo(entry)
81376
+ );
81015
81377
  if (repos !== null && repos.length === 0) {
81016
81378
  log3(
81017
- "\u26A0 The org's synced repo list is empty \u2014 no repo-based uploads" + (memberFolders.length > 0 ? "; only folders you added upload." : ".")
81379
+ "\u26A0 The org's repo allowlist is empty \u2014 no repo-based uploads for it" + (foreignClaims.length > 0 ? `; repos synced by ${foreignClaims.map((entry) => entry.name).join(", ")} still upload.` : memberFolders.length > 0 ? "; only folders you added upload." : ".")
81018
81380
  );
81019
81381
  } else if (repos === null && owners.length === 0 && !ownersUnresolved) {
81020
81382
  log3(
81021
- "\u26A0 Syncing is gated on org-owned GitHub repos, but the org has no GitHub integration \u2014 nothing uploads. Connect GitHub in Settings, or add folders with `powerplant folders add`."
81383
+ foreignClaims.length > 0 ? `Your home workspace has no GitHub integration \u2014 syncing repos for ${foreignClaims.map((entry) => entry.name).join(", ")}.` : "\u26A0 Syncing is gated on org-owned GitHub repos, but the org has no GitHub integration \u2014 nothing uploads. Connect GitHub in Settings, or add folders with `powerplant folders add`."
81022
81384
  );
81023
81385
  }
81024
81386
  return {
81025
81387
  mode: "org-repos",
81026
81388
  allowedOwners: owners,
81027
81389
  allowedRepos: repos,
81390
+ ...workspacePolicies !== void 0 && { workspaceClaims: workspacePolicies },
81391
+ ...claimsUnresolved && { claimsUnresolved },
81028
81392
  memberFolders,
81029
81393
  repoOf,
81030
81394
  environment
@@ -81041,26 +81405,33 @@ async function runSyncPass(config2, options = {}) {
81041
81405
  const window2 = resolveSyncWindow(options.explicitSince, credentials.deepSyncedAt);
81042
81406
  if (window2.deepSweep) {
81043
81407
  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}).`
81408
+ `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
81409
  );
81046
81410
  }
81047
81411
  const capture = await resolveCapturePolicy(config2, credentials, log3);
81048
81412
  const sinceMs = resolveScanFloor(window2, Date.now(), options.narrowSinceMs);
81049
- const claude = await collectClaudeCodeSessions({ sinceMs, capture });
81050
- const codex = await collectCodexSessions({ sinceMs, capture });
81413
+ const maxSessions = window2.deepSweep ? DEEP_FIRST_SYNC_MAX_SESSIONS : void 0;
81414
+ const claude = await collectClaudeCodeSessions({ sinceMs, capture, maxSessions });
81415
+ const codex = await collectCodexSessions({ sinceMs, capture, maxSessions });
81051
81416
  const cursor = await collectCursorSessions({ sinceMs, capture });
81052
81417
  if (cursor.unavailable !== null) {
81053
81418
  log3(`\u26A0 Cursor sessions skipped this pass \u2014 ${cursor.unavailable}. Other syncing is unaffected.`);
81054
81419
  }
81055
- const collected = [...claude.sessions, ...codex.sessions, ...cursor.sessions].sort(
81420
+ const inWindow = [...claude.sessions, ...codex.sessions, ...cursor.sessions].sort(
81056
81421
  (a, b2) => b2.mtimeMs - a.mtimeMs
81057
81422
  );
81423
+ const collected = maxSessions === void 0 ? inWindow : inWindow.slice(0, maxSessions);
81424
+ const heldBack = inWindow.length - collected.length + claude.heldBack + codex.heldBack;
81058
81425
  const skippedOutOfScope = claude.skippedOutOfScope + codex.skippedOutOfScope + cursor.skippedOutOfScope;
81426
+ const skippedNoRepo = claude.skippedNoRepo + codex.skippedNoRepo + cursor.skippedNoRepo;
81059
81427
  const items = collected.map((c) => c.item);
81060
81428
  const active = items.filter((s) => s.status === "active").length;
81429
+ const tooLarge = (count, what) => count > 0 ? ` \xB7 ${count} ${what} skipped (over ${MAX_TRANSCRIPT_FILE_BYTES / (1024 * 1024)}MB \u2014 too large to read)` : "";
81061
81430
  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` : "") + "."
81431
+ `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` : "") + tooLarge(claude.skippedTooLarge, "claude transcript(s)") + (claude.unreadable.length > 0 ? ` \xB7 ${claude.unreadable.length} claude transcript(s) unreadable, skipped` : "") + tooLarge(codex.skippedTooLarge, "codex rollout(s)") + (codex.unreadable.length > 0 ? ` \xB7 ${codex.unreadable.length} codex rollout(s) unreadable, skipped` : "") + tooLarge(cursor.skippedTooLarge, "cursor transcript(s)") + (cursor.truncatedTranscripts > 0 ? ` \xB7 ${cursor.truncatedTranscripts} cursor transcript(s) cut at ${MAX_TRANSCRIPT_FILE_BYTES / (1024 * 1024)}MB (the rest of the conversation stays local)` : "") + (heldBack > 0 ? ` \xB7 ${heldBack} older session(s) held back \u2014 the first sync backfills the ${DEEP_FIRST_SYNC_MAX_SESSIONS} most recently active` : "") + "."
81063
81432
  );
81433
+ for (const failure of claude.unreadable) log3(` claude transcript skipped \u2014 ${failure}`);
81434
+ for (const failure of codex.unreadable) log3(` codex rollout skipped \u2014 ${failure}`);
81064
81435
  if (options.dryRun) {
81065
81436
  for (const s of items.slice(0, 10)) {
81066
81437
  log3(` ${s.externalId} ${s.model ?? "model unmeasured"} ${s.title ?? "(untitled)"}`);
@@ -81085,11 +81456,18 @@ async function runSyncPass(config2, options = {}) {
81085
81456
  saveCredentials(config2.appEnv, { ...current, deepSyncedAt: (/* @__PURE__ */ new Date()).toISOString() });
81086
81457
  log3(`Deep sync complete \u2014 future syncs cover the last ${STEADY_SYNC_WINDOW_DAYS} days.`);
81087
81458
  };
81459
+ const claimsUnresolved = capture.mode === "org-repos" && capture.claimsUnresolved === true;
81088
81460
  if (items.length === 0) {
81089
- markDeepSynced();
81461
+ if (claimsUnresolved) {
81462
+ if (window2.deepSweep) {
81463
+ log3("Deep sync not stamped \u2014 a workspace's claims are unresolved; the sweep re-runs next pass.");
81464
+ }
81465
+ } else {
81466
+ markDeepSynced();
81467
+ }
81090
81468
  return { sessions: collected, upserted: 0 };
81091
81469
  }
81092
- const { upserted, unchanged, outOfPolicy } = await uploadSessions(
81470
+ const { upserted, unchanged, outOfPolicy, deferred } = await uploadSessions(
81093
81471
  credentials.apiUrl,
81094
81472
  credentials.token,
81095
81473
  items
@@ -81097,6 +81475,11 @@ async function runSyncPass(config2, options = {}) {
81097
81475
  log3(
81098
81476
  `Uploaded: ${upserted} session(s) upserted` + (unchanged > 0 ? `, ${unchanged} unchanged (no writes issued)` : "") + "."
81099
81477
  );
81478
+ if (deferred > 0) {
81479
+ log3(
81480
+ `\u26A0 ${deferred} session(s) deferred \u2014 a workspace's GitHub owners couldn't be resolved just now, so routing waited rather than guessing a workspace. They upload on a later pass.`
81481
+ );
81482
+ }
81100
81483
  if (outOfPolicy > 0) {
81101
81484
  log3(
81102
81485
  `\u26A0 ${outOfPolicy} session(s) refused by the org's repo sync policy \u2014 they stayed out of Powerplant. The next pass refreshes the policy and stops offering them.`
@@ -81152,11 +81535,24 @@ async function runSyncPass(config2, options = {}) {
81152
81535
  if (raw.failed.length > 0) parts.push(`${raw.failed.length} failed`);
81153
81536
  log3(`Raw content: ${parts.join(", ")}.`);
81154
81537
  for (const failure of raw.failed) log3(` raw upload failed \u2014 ${failure}`);
81155
- const nothingElseAccountedFor = raw.skippedUnchanged === 0 && raw.skippedTooLarge === 0 && rawWindowSkipped === 0;
81538
+ if (raw.missingMetadata > 0) {
81539
+ log3(
81540
+ `${raw.missingMetadata} raw transcript(s) waiting on their session metadata \u2014 they re-send once it lands.`
81541
+ );
81542
+ }
81543
+ const nothingElseAccountedFor = raw.skippedUnchanged === 0 && raw.skippedTooLarge === 0 && rawWindowSkipped === 0 && raw.missingMetadata === 0;
81156
81544
  if (raw.uploaded === 0 && (raw.failed.length > 1 || raw.failed.length === 1 && nothingElseAccountedFor)) {
81157
81545
  throw new Error(`all ${raw.failed.length} raw upload(s) failed this pass \u2014 ${raw.failed[0]}`);
81158
81546
  }
81159
- markDeepSynced();
81547
+ if (deferred > 0 || claimsUnresolved) {
81548
+ if (window2.deepSweep) {
81549
+ log3(
81550
+ deferred > 0 ? "Deep sync not stamped \u2014 deferred sessions still owe their upload; the sweep re-runs next pass." : "Deep sync not stamped \u2014 a workspace's claims are unresolved; the sweep re-runs next pass."
81551
+ );
81552
+ }
81553
+ } else {
81554
+ markDeepSynced();
81555
+ }
81160
81556
  }
81161
81557
  return { sessions: collected, upserted };
81162
81558
  }
@@ -81198,7 +81594,7 @@ import os14 from "node:os";
81198
81594
  import path14 from "node:path";
81199
81595
 
81200
81596
  // ../../apps/collector/src/wake.ts
81201
- import { execFile as execFile2, execFileSync as execFileSync3 } from "node:child_process";
81597
+ import { execFile as execFile2, execFileSync as execFileSync4 } from "node:child_process";
81202
81598
  import { existsSync as existsSync3 } from "node:fs";
81203
81599
  import path13 from "node:path";
81204
81600
  var WAKE_POLL_MS = 5e3;
@@ -81210,14 +81606,11 @@ function findClaudeTranscript(homeDir, externalId) {
81210
81606
  const transcript = path13.join(projectDir, `${externalId}.jsonl`);
81211
81607
  const stat = statIfExists(transcript);
81212
81608
  if (stat === null) continue;
81213
- const text = readIfExists2(transcript);
81214
- if (text === null) continue;
81215
- for (const record2 of parseJsonl(text)) {
81216
- if (typeof record2.cwd === "string" && record2.cwd !== "") {
81217
- return { cwd: record2.cwd, mtimeMs: stat.mtimeMs };
81218
- }
81219
- }
81220
- return null;
81609
+ const record2 = readFirstRecord(
81610
+ transcript,
81611
+ (candidate) => typeof candidate.cwd === "string" && candidate.cwd !== ""
81612
+ );
81613
+ return typeof record2?.cwd === "string" ? { cwd: record2.cwd, mtimeMs: stat.mtimeMs } : null;
81221
81614
  }
81222
81615
  return null;
81223
81616
  }
@@ -81242,7 +81635,7 @@ function createWakePoller(deps) {
81242
81635
  try {
81243
81636
  for (const target of await deps.listPending()) {
81244
81637
  if (inFlight.has(target.externalId)) continue;
81245
- const home = deps.inspect(target);
81638
+ const home = inspectTarget(deps, target);
81246
81639
  if (home === null) continue;
81247
81640
  if (deps.now() - home.mtimeMs < WAKE_QUIET_MS) continue;
81248
81641
  const messages = await deps.claim(target);
@@ -81252,13 +81645,13 @@ function createWakePoller(deps) {
81252
81645
  `waking session ${target.externalId} with ${messages.length} message(s) in ${home.cwd}`
81253
81646
  );
81254
81647
  void deps.runClaude(target, formatWakePrompt(messages), home.cwd).then(() => deps.log(`wake finished for session ${target.externalId}`)).catch(
81255
- (error52) => reportFailedWake(deps, target, messages, describeError2(error52))
81648
+ (error52) => reportFailedWake(deps, target, messages, describeError(error52))
81256
81649
  ).finally(() => inFlight.delete(target.externalId));
81257
81650
  }
81258
81651
  consecutivePollFailures = 0;
81259
81652
  } catch (error52) {
81260
81653
  consecutivePollFailures += 1;
81261
- deps.log(`\u26A0 wake poll failed (${describeError2(error52)}) \u2014 next poll retries`);
81654
+ deps.log(`\u26A0 wake poll failed (${describeError(error52)}) \u2014 next poll retries`);
81262
81655
  if (consecutivePollFailures === REPORT_AFTER_CONSECUTIVE_FAILURES) {
81263
81656
  deps.reportError?.(error52, "wake-poll", { consecutivePollFailures });
81264
81657
  }
@@ -81268,6 +81661,16 @@ function createWakePoller(deps) {
81268
81661
  }
81269
81662
  };
81270
81663
  }
81664
+ function inspectTarget(deps, target) {
81665
+ try {
81666
+ return deps.inspect(target);
81667
+ } catch (error52) {
81668
+ deps.log(
81669
+ `\u26A0 wake skipped for session ${target.externalId} \u2014 its transcript could not be read (${describeError(error52)}); next poll retries`
81670
+ );
81671
+ return null;
81672
+ }
81673
+ }
81271
81674
  async function reportFailedWake(deps, target, messages, reason) {
81272
81675
  deps.log(
81273
81676
  `\u26A0 wake failed for session ${target.externalId} AFTER its messages were claimed (${reason})`
@@ -81280,7 +81683,7 @@ async function reportFailedWake(deps, target, messages, reason) {
81280
81683
  );
81281
81684
  } catch (reportError) {
81282
81685
  deps.log(
81283
- `\u26A0 wake failure report for session ${target.externalId} ALSO failed \u2014 its messages read Delivered but no agent ran (${describeError2(reportError)})`
81686
+ `\u26A0 wake failure report for session ${target.externalId} ALSO failed \u2014 its messages read Delivered but no agent ran (${describeError(reportError)})`
81284
81687
  );
81285
81688
  deps.reportError?.(reportError, "wake-failure-report", {
81286
81689
  externalId: target.externalId,
@@ -81300,7 +81703,7 @@ function resolveClaudeBin(homeDir, env = process.env) {
81300
81703
  const found = candidates.find((candidate) => existsSync3(candidate));
81301
81704
  if (found) return found;
81302
81705
  try {
81303
- const fromPath = execFileSync3("which", ["claude"], { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
81706
+ const fromPath = execFileSync4("which", ["claude"], { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
81304
81707
  return fromPath === "" ? null : fromPath;
81305
81708
  } catch {
81306
81709
  return null;
@@ -81322,7 +81725,7 @@ function versionAtLeast(version2, floor) {
81322
81725
  return true;
81323
81726
  }
81324
81727
  function execClaudeVersion(claudeBin) {
81325
- return execFileSync3(claudeBin, ["--version"], {
81728
+ return execFileSync4(claudeBin, ["--version"], {
81326
81729
  stdio: ["ignore", "pipe", "ignore"],
81327
81730
  timeout: VERSION_TIMEOUT_MS
81328
81731
  }).toString();
@@ -81332,7 +81735,7 @@ function preflightClaudeVersion(claudeBin, readVersionOutput = execClaudeVersion
81332
81735
  try {
81333
81736
  output = readVersionOutput(claudeBin);
81334
81737
  } catch (error52) {
81335
- return { ok: false, problem: `\`${claudeBin} --version\` failed (${describeError2(error52)})` };
81738
+ return { ok: false, problem: `\`${claudeBin} --version\` failed (${describeError(error52)})` };
81336
81739
  }
81337
81740
  const version2 = /\d+\.\d+\.\d+/.exec(output)?.[0] ?? null;
81338
81741
  if (version2 === null) {
@@ -81477,7 +81880,7 @@ function startWakeService(config2, homeDir, log3) {
81477
81880
  log3(`wake-on-message armed: polling every ${WAKE_POLL_MS / 1e3}s (${config2.appEnv})`);
81478
81881
  return () => clearInterval(timer);
81479
81882
  }
81480
- function describeError2(error52) {
81883
+ function describeError(error52) {
81481
81884
  return error52 instanceof Error ? error52.message : String(error52);
81482
81885
  }
81483
81886
 
@@ -81517,6 +81920,8 @@ var SAFETY_NET_INTERVAL_MS = 60 * 60 * 1e3;
81517
81920
  var NARROW_OVERLAP_MS = 6e4;
81518
81921
  var WATCHER_RETRY_MS = 6e4;
81519
81922
  var FAILED_PASS_RETRY_MS = 6e4;
81923
+ var FAILED_PASS_BACKOFF_CAP_MS = 15 * 6e4;
81924
+ var STREAK_REREPORT_INTERVAL_MS = 60 * 6e4;
81520
81925
  var VERSION_NOTICE_INTERVAL_MS = 60 * 6e4;
81521
81926
  var COMPLETION_SLACK_MS = 3e4;
81522
81927
  function createWatchLoop(deps) {
@@ -81529,6 +81934,10 @@ function createWatchLoop(deps) {
81529
81934
  let completionTimer = null;
81530
81935
  let running = false;
81531
81936
  let consecutiveFailures = 0;
81937
+ let streakSignature = null;
81938
+ let streakCount = 0;
81939
+ let streakStartedAt = 0;
81940
+ let streakReportedAt = 0;
81532
81941
  let pausedLogged = false;
81533
81942
  let queued = null;
81534
81943
  let retryNotBefore = 0;
@@ -81555,7 +81964,7 @@ function createWatchLoop(deps) {
81555
81964
  if (stopped) return;
81556
81965
  try {
81557
81966
  closeWatcher = deps.watch(onEvent, (error52) => {
81558
- deps.log(`watcher lost (${describeError3(error52)}) \u2014 re-arming and re-scanning`);
81967
+ deps.log(`watcher lost (${describeError2(error52)}) \u2014 re-arming and re-scanning`);
81559
81968
  closeWatcher?.();
81560
81969
  closeWatcher = null;
81561
81970
  arm();
@@ -81563,7 +81972,7 @@ function createWatchLoop(deps) {
81563
81972
  });
81564
81973
  } catch (error52) {
81565
81974
  deps.log(
81566
- `cannot watch yet (${describeError3(error52)}) \u2014 retrying in ${WATCHER_RETRY_MS / 1e3}s`
81975
+ `cannot watch yet (${describeError2(error52)}) \u2014 retrying in ${WATCHER_RETRY_MS / 1e3}s`
81567
81976
  );
81568
81977
  watcherRetry = setTimeout(arm, WATCHER_RETRY_MS);
81569
81978
  }
@@ -81620,6 +82029,8 @@ function createWatchLoop(deps) {
81620
82029
  }
81621
82030
  }
81622
82031
  consecutiveFailures = 0;
82032
+ streakSignature = null;
82033
+ streakCount = 0;
81623
82034
  deps.onPassResult?.({
81624
82035
  kind,
81625
82036
  at: startedAt,
@@ -81629,17 +82040,33 @@ function createWatchLoop(deps) {
81629
82040
  });
81630
82041
  } catch (error52) {
81631
82042
  consecutiveFailures += 1;
81632
- retryNotBefore = now() + Math.max(FAILED_PASS_RETRY_MS, retryAfterMs(error52) ?? 0);
82043
+ const signature = errorSignature(error52);
82044
+ if (signature === streakSignature) {
82045
+ streakCount += 1;
82046
+ } else {
82047
+ streakSignature = signature;
82048
+ streakCount = 1;
82049
+ streakStartedAt = startedAt;
82050
+ streakReportedAt = 0;
82051
+ }
82052
+ const holdMs = Math.max(
82053
+ Math.min(FAILED_PASS_RETRY_MS * 2 ** (streakCount - 1), FAILED_PASS_BACKOFF_CAP_MS),
82054
+ retryAfterMs(error52) ?? 0
82055
+ );
82056
+ retryNotBefore = now() + holdMs;
81633
82057
  lifecycleChanged ||= consultLifecycle;
81634
82058
  deps.onPassResult?.({
81635
82059
  kind,
81636
82060
  at: startedAt,
81637
82061
  ok: false,
81638
- error: describeError3(error52),
82062
+ error: describeError2(error52),
81639
82063
  sessions: 0,
81640
82064
  upserted: 0
81641
82065
  });
81642
- deps.log(`${kind} pass failed (${describeError3(error52)}) \u2014 next pass re-covers this window`);
82066
+ const failLine = `${kind} pass failed (${describeError2(error52)})`;
82067
+ deps.log(
82068
+ streakCount > 1 ? `${failLine}: same failure ${streakCount} times in a row, next attempt in ${describeDelay(holdMs)}` : `${failLine}, next attempt in ${describeDelay(holdMs)} re-covers this window`
82069
+ );
81643
82070
  if (error52 instanceof Error && !(error52 instanceof ExpectedCliError) && error52.stack) {
81644
82071
  deps.log(error52.stack);
81645
82072
  }
@@ -81650,10 +82077,13 @@ function createWatchLoop(deps) {
81650
82077
  );
81651
82078
  return;
81652
82079
  }
81653
- if (consecutiveFailures >= REPORT_AFTER_CONSECUTIVE_FAILURES) {
82080
+ if (consecutiveFailures >= REPORT_AFTER_CONSECUTIVE_FAILURES && (streakReportedAt === 0 || now() - streakReportedAt >= STREAK_REREPORT_INTERVAL_MS)) {
82081
+ streakReportedAt = now();
81654
82082
  const report2 = error52 instanceof ExpectedCliError ? new Error(`${consecutiveFailures} consecutive failed passes: ${error52.message}`) : error52;
81655
82083
  deps.reportError?.(report2, `watch-pass-${kind}`, {
81656
82084
  consecutiveFailures,
82085
+ sameFailureCount: streakCount,
82086
+ streakMinutes: Math.round((now() - streakStartedAt) / 6e4),
81657
82087
  pendingCompletions: pendingCompletions.size
81658
82088
  });
81659
82089
  }
@@ -81741,7 +82171,7 @@ function startWatch(config2, homeDir, runtime = { version: "checkout" }) {
81741
82171
  try {
81742
82172
  return loadCredentials(config2.appEnv);
81743
82173
  } catch (error52) {
81744
- log3(`\u26A0 credential store unreadable (${describeError3(error52)}) \u2014 identity omitted from status`);
82174
+ log3(`\u26A0 credential store unreadable (${describeError2(error52)}) \u2014 identity omitted from status`);
81745
82175
  return null;
81746
82176
  }
81747
82177
  })();
@@ -81755,14 +82185,14 @@ function startWatch(config2, homeDir, runtime = { version: "checkout" }) {
81755
82185
  );
81756
82186
  } catch (error52) {
81757
82187
  log3(
81758
- `\u26A0 Claude Code hook install failed (${describeError3(error52)}) \u2014 syncing continues without them`
82188
+ `\u26A0 Claude Code hook install failed (${describeError2(error52)}) \u2014 syncing continues without them`
81759
82189
  );
81760
82190
  reportCollectorError(error52, "watch-hook-install");
81761
82191
  }
81762
82192
  try {
81763
82193
  startWakeService(config2, home, log3);
81764
82194
  } catch (error52) {
81765
- log3(`\u26A0 wake-on-message failed to start (${describeError3(error52)}) \u2014 syncing continues`);
82195
+ log3(`\u26A0 wake-on-message failed to start (${describeError2(error52)}) \u2014 syncing continues`);
81766
82196
  reportCollectorError(error52, "watch-wake-start");
81767
82197
  }
81768
82198
  const presentRoots = storeRoots.filter(({ dir }) => existsSync4(dir)).map(({ dir }) => dir);
@@ -81793,7 +82223,7 @@ function startWatch(config2, homeDir, runtime = { version: "checkout" }) {
81793
82223
  statusWriteFailed = false;
81794
82224
  } catch (error52) {
81795
82225
  if (!statusWriteFailed) {
81796
- log3(`\u26A0 status file write failed (${describeError3(error52)}) \u2014 syncing continues`);
82226
+ log3(`\u26A0 status file write failed (${describeError2(error52)}) \u2014 syncing continues`);
81797
82227
  reportCollectorError(error52, "watch-status-write");
81798
82228
  }
81799
82229
  statusWriteFailed = true;
@@ -81891,12 +82321,24 @@ function startWatch(config2, homeDir, runtime = { version: "checkout" }) {
81891
82321
  return new Promise(() => {
81892
82322
  });
81893
82323
  }
81894
- function describeError3(error52) {
82324
+ function describeError2(error52) {
81895
82325
  return error52 instanceof Error ? error52.message : String(error52);
81896
82326
  }
82327
+ function errorSignature(error52) {
82328
+ const parts = [];
82329
+ for (let cause = error52; cause instanceof Error; cause = cause.cause) {
82330
+ parts.push(`${cause.name}: ${cause.message}`);
82331
+ }
82332
+ if (parts.length === 0) parts.push(String(error52));
82333
+ return parts.join(" | ").replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "#").replace(/[0-9a-f]{8,}/gi, "#").replace(/\d+/g, "#");
82334
+ }
82335
+ function describeDelay(ms) {
82336
+ const seconds = Math.round(ms / 1e3);
82337
+ return seconds < 120 ? `${seconds}s` : `${Math.round(seconds / 60)} minutes`;
82338
+ }
81897
82339
 
81898
82340
  // src/connect.ts
81899
- import { execFileSync as execFileSync4, spawnSync } from "node:child_process";
82341
+ import { execFileSync as execFileSync5, spawnSync } from "node:child_process";
81900
82342
  import {
81901
82343
  existsSync as existsSync5,
81902
82344
  mkdirSync as mkdirSync6,
@@ -82122,7 +82564,7 @@ function desktopClaudeBinaries(homeDir) {
82122
82564
  }
82123
82565
  function defaultWhichAll(name) {
82124
82566
  try {
82125
- return execFileSync4("which", ["-a", name], { encoding: "utf8" }).split("\n").map((line) => line.trim()).filter(Boolean);
82567
+ return execFileSync5("which", ["-a", name], { encoding: "utf8" }).split("\n").map((line) => line.trim()).filter(Boolean);
82126
82568
  } catch {
82127
82569
  return [];
82128
82570
  }
@@ -82915,7 +83357,7 @@ function realRunInteractive(bin, args, env) {
82915
83357
  }
82916
83358
 
82917
83359
  // src/connections.ts
82918
- import { execFileSync as execFileSync5, spawnSync as spawnSync2 } from "node:child_process";
83360
+ import { execFileSync as execFileSync6, spawnSync as spawnSync2 } from "node:child_process";
82919
83361
  import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "node:fs";
82920
83362
  import path16 from "node:path";
82921
83363
  function probeConnections(clients, deps) {
@@ -83118,7 +83560,7 @@ function runWithPath(pathValue, budget = UNLIMITED_TIME_BUDGET) {
83118
83560
  function whichAllWith(pathValue) {
83119
83561
  return (name) => {
83120
83562
  try {
83121
- return execFileSync5("which", ["-a", name], {
83563
+ return execFileSync6("which", ["-a", name], {
83122
83564
  encoding: "utf8",
83123
83565
  env: { ...process.env, PATH: pathValue }
83124
83566
  }).split("\n").map((line) => line.trim()).filter(Boolean);
@@ -83136,7 +83578,7 @@ function renderStatus(i) {
83136
83578
  const lines = [];
83137
83579
  lines.push(`Environment: ${i.config.appEnv} (API ${i.config.apiUrl})`);
83138
83580
  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)` : "")
83581
+ `CLI: powerplant ${i.cliVersion}` + (i.latestVersion !== null && i.latestVersion !== i.cliVersion ? ` \u2014 ${i.latestVersion} is available: powerplant update` : "")
83140
83582
  );
83141
83583
  const unsupported = unsupportedVersionNotice(i.cliVersion, i.oldestSupportedVersion);
83142
83584
  if (unsupported !== null) lines.push(unsupported);
@@ -83607,7 +84049,7 @@ async function realPromptLine(question) {
83607
84049
  }
83608
84050
 
83609
84051
  // src/memberSessions.ts
83610
- import { execFileSync as execFileSync6 } from "node:child_process";
84052
+ import { execFileSync as execFileSync7 } from "node:child_process";
83611
84053
  import { existsSync as existsSync6 } from "node:fs";
83612
84054
  import os16 from "node:os";
83613
84055
  import path17 from "node:path";
@@ -83682,7 +84124,7 @@ function detectCodexDesktop(probe = { exists: existsSync6, readUrlSchemes: realR
83682
84124
  function realReadUrlSchemes(bundlePath) {
83683
84125
  if (process.platform !== "darwin") return [];
83684
84126
  try {
83685
- const json2 = execFileSync6(
84127
+ const json2 = execFileSync7(
83686
84128
  "plutil",
83687
84129
  ["-extract", "CFBundleURLTypes", "json", "-o", "-", path17.join(bundlePath, "Contents", "Info.plist")],
83688
84130
  { stdio: ["ignore", "pipe", "ignore"] }
@@ -84139,8 +84581,38 @@ function createTerminalOutput(options) {
84139
84581
  return { log: log3, activity };
84140
84582
  }
84141
84583
 
84584
+ // src/update.ts
84585
+ import { spawnSync as spawnSync4 } from "node:child_process";
84586
+ var npmLsSchema = external_exports.object({
84587
+ dependencies: external_exports.record(external_exports.string(), external_exports.object({ version: external_exports.string().min(1) })).optional()
84588
+ });
84589
+ function runCapturing(command, args) {
84590
+ const result = spawnSync4(command, args, {
84591
+ encoding: "utf8",
84592
+ stdio: ["ignore", "pipe", "ignore"]
84593
+ });
84594
+ return { stdout: result.stdout ?? "" };
84595
+ }
84596
+ function installedGlobalVersion(packageName, run = runCapturing) {
84597
+ const { stdout } = run("npm", ["ls", "-g", packageName, "--json"]);
84598
+ try {
84599
+ const parsed = npmLsSchema.safeParse(JSON.parse(stdout));
84600
+ return parsed.success ? parsed.data.dependencies?.[packageName]?.version ?? null : null;
84601
+ } catch {
84602
+ return null;
84603
+ }
84604
+ }
84605
+ var RESTART_NOTE = "Sync service will restart within minutes. (You can do this now with powerplant restart.)";
84606
+ function updatedMessage(version2) {
84607
+ if (version2 === null) {
84608
+ return `Updated, but could not read the installed version from npm. Check with: powerplant status
84609
+ ${RESTART_NOTE}`;
84610
+ }
84611
+ return `Updated to v${version2}. ${RESTART_NOTE}`;
84612
+ }
84613
+
84142
84614
  // src/version.ts
84143
- var CLI_VERSION = true ? "0.3.1" : "0.0.0-checkout";
84615
+ var CLI_VERSION = true ? "0.4.0" : "0.0.0-checkout";
84144
84616
  var CLI_PACKAGE_NAME = "@usepowerplant/cli";
84145
84617
 
84146
84618
  // src/workspace.ts
@@ -84261,7 +84733,7 @@ Commands:
84261
84733
  sync Run one sync pass in the foreground [--since <days>] [--dry-run].
84262
84734
  folders The folders you explicitly sync on this machine:
84263
84735
  powerplant folders [add <path> | remove <path>]. Additions on
84264
- top of your org's synced repos \u2014 no re-enroll needed.
84736
+ top of your org's allowlisted repos \u2014 no re-enroll needed.
84265
84737
  workspace Your workspaces, and which one Powerplant and your connected
84266
84738
  agents use: powerplant workspace [<name>] switches to <name>.
84267
84739
  resume-session Reopen a synced session in its own CLI, right here in
@@ -84573,7 +85045,7 @@ The access you granted these agents stays on your Powerplant account until you r
84573
85045
  }
84574
85046
  case "update": {
84575
85047
  console.log(`Updating ${CLI_PACKAGE_NAME} via npm\u2026`);
84576
- const npm = spawnSync4("npm", ["install", "-g", CLI_PACKAGE_NAME], { stdio: "inherit" });
85048
+ const npm = spawnSync5("npm", ["install", "-g", CLI_PACKAGE_NAME], { stdio: "inherit" });
84577
85049
  if (npm.error) {
84578
85050
  throw new ExpectedCliError(
84579
85051
  `Could not run npm (${npm.error.message}) \u2014 update by hand: npm install -g ${CLI_PACKAGE_NAME}`
@@ -84582,9 +85054,7 @@ The access you granted these agents stays on your Powerplant account until you r
84582
85054
  if (npm.status !== 0) {
84583
85055
  throw new ExpectedCliError("npm install failed \u2014 see the output above.");
84584
85056
  }
84585
- console.log(
84586
- "Updated. The background service restarts itself onto the new version within minutes (or now: powerplant restart)."
84587
- );
85057
+ console.log(updatedMessage(installedGlobalVersion(CLI_PACKAGE_NAME)));
84588
85058
  return;
84589
85059
  }
84590
85060
  case "restart": {