@usepowerplant/cli 0.4.6 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -55485,20 +55485,210 @@ function workspaceClaimsAnyRepo(policy) {
55485
55485
  return policy.repos !== null ? policy.repos.length > 0 : policy.owners.length > 0;
55486
55486
  }
55487
55487
 
55488
+ // ../core/src/encodedImages.ts
55489
+ var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
55490
+ function isEncodedImage(data, mediaType) {
55491
+ if (data.length < 16 || data.length % 4 !== 0) return false;
55492
+ const padding = data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0;
55493
+ const body = data.slice(0, data.length - padding);
55494
+ if (/[^A-Za-z0-9+/]/.test(body)) return false;
55495
+ const last = BASE64_ALPHABET.indexOf(body.at(-1));
55496
+ if (padding === 2 && (last & 15) !== 0 || padding === 1 && (last & 3) !== 0) return false;
55497
+ switch (mediaType) {
55498
+ case "image/png":
55499
+ return isPng(atob(data));
55500
+ case "image/jpeg":
55501
+ return isJpeg(atob(data));
55502
+ case "image/gif":
55503
+ return isGif(atob(data));
55504
+ case "image/webp":
55505
+ return isWebp(atob(data));
55506
+ default:
55507
+ return false;
55508
+ }
55509
+ }
55510
+ function u16(data, offset) {
55511
+ return data.charCodeAt(offset) << 8 | data.charCodeAt(offset + 1);
55512
+ }
55513
+ function u32(data, offset) {
55514
+ return u16(data, offset) * 65536 + u16(data, offset + 2) >>> 0;
55515
+ }
55516
+ function little32(data, offset) {
55517
+ return (data.charCodeAt(offset) | data.charCodeAt(offset + 1) << 8 | data.charCodeAt(offset + 2) << 16 | data.charCodeAt(offset + 3) << 24) >>> 0;
55518
+ }
55519
+ var CRC_TABLE = Array.from({ length: 256 }, (_, value) => {
55520
+ for (let bit = 0; bit < 8; bit++) value = value & 1 ? 3988292384 ^ value >>> 1 : value >>> 1;
55521
+ return value >>> 0;
55522
+ });
55523
+ function crc32(data, start, end) {
55524
+ let crc = 4294967295;
55525
+ for (let i = start; i < end; i++)
55526
+ crc = CRC_TABLE[(crc ^ data.charCodeAt(i)) & 255] ^ crc >>> 8;
55527
+ return (crc ^ 4294967295) >>> 0;
55528
+ }
55529
+ function isPng(data) {
55530
+ if (!data.startsWith("\x89PNG\r\n\n")) return false;
55531
+ let offset = 8;
55532
+ let imageData = false;
55533
+ let dataEnded = false;
55534
+ let palette = false;
55535
+ let indexed = false;
55536
+ while (offset + 12 <= data.length) {
55537
+ const size = u32(data, offset);
55538
+ const type = data.slice(offset + 4, offset + 8);
55539
+ const end = offset + 8 + size;
55540
+ if (size > 2147483647 || end + 4 > data.length || !/^[A-Za-z]{2}[A-Z][A-Za-z]$/.test(type))
55541
+ return false;
55542
+ if (u32(data, end) !== crc32(data, offset + 4, end)) return false;
55543
+ if (offset === 8) {
55544
+ if (type !== "IHDR" || size !== 13 || !u32(data, offset + 8) || !u32(data, offset + 12))
55545
+ return false;
55546
+ const depth = data.charCodeAt(offset + 16);
55547
+ const color = data.charCodeAt(offset + 17);
55548
+ const validDepth = color === 0 ? [1, 2, 4, 8, 16] : color === 3 ? [1, 2, 4, 8] : [8, 16];
55549
+ if (![0, 2, 3, 4, 6].includes(color) || !validDepth.includes(depth)) return false;
55550
+ if (data.charCodeAt(offset + 18) !== 0 || data.charCodeAt(offset + 19) !== 0 || data.charCodeAt(offset + 20) > 1)
55551
+ return false;
55552
+ indexed = color === 3;
55553
+ } else if (type === "IHDR") return false;
55554
+ else if (type === "PLTE") {
55555
+ if (imageData || palette || size === 0 || size > 768 || size % 3 !== 0) return false;
55556
+ palette = true;
55557
+ } else if (type === "IDAT") {
55558
+ if (dataEnded || indexed && !palette) return false;
55559
+ imageData = imageData || size > 0;
55560
+ } else if (type === "IEND") return imageData && size === 0 && end + 4 === data.length;
55561
+ else if (type[0] === type[0].toUpperCase()) return false;
55562
+ if (imageData && type !== "IDAT") dataEnded = true;
55563
+ offset = end + 4;
55564
+ }
55565
+ return false;
55566
+ }
55567
+ function isJpeg(data) {
55568
+ if (!data.startsWith("\xFF\xD8")) return false;
55569
+ let offset = 2;
55570
+ let frame = false;
55571
+ let scan = false;
55572
+ while (offset < data.length) {
55573
+ if (data.charCodeAt(offset++) !== 255) return false;
55574
+ while (data.charCodeAt(offset) === 255) offset++;
55575
+ const marker = data.charCodeAt(offset++);
55576
+ if (marker === 217) return frame && scan && offset === data.length;
55577
+ if (marker === 0 || marker === 216 || marker >= 208 && marker <= 215) return false;
55578
+ if (marker === 1) continue;
55579
+ if (offset + 2 > data.length) return false;
55580
+ const size = u16(data, offset);
55581
+ if (size < 2 || offset + size > data.length) return false;
55582
+ if (marker >= 192 && marker <= 207 && ![196, 200, 204].includes(marker)) {
55583
+ if (size < 11 || size !== 8 + 3 * data.charCodeAt(offset + 7) || !u16(data, offset + 3) || !u16(data, offset + 5))
55584
+ return false;
55585
+ frame = true;
55586
+ }
55587
+ if (marker !== 218) {
55588
+ offset += size;
55589
+ continue;
55590
+ }
55591
+ if (!frame || size < 8 || size !== 6 + 2 * data.charCodeAt(offset + 2)) return false;
55592
+ offset += size;
55593
+ const start = offset;
55594
+ for (; ; ) {
55595
+ const next = data.indexOf("\xFF", offset);
55596
+ if (next < 0 || next + 1 >= data.length) return false;
55597
+ const following = data.charCodeAt(next + 1);
55598
+ if (following === 0 || following >= 208 && following <= 215) offset = next + 2;
55599
+ else if (following === 255) offset = next + 1;
55600
+ else {
55601
+ offset = next;
55602
+ break;
55603
+ }
55604
+ }
55605
+ if (offset <= start) return false;
55606
+ scan = true;
55607
+ }
55608
+ return false;
55609
+ }
55610
+ function skipGifBlocks(data, offset) {
55611
+ while (offset < data.length) {
55612
+ const size = data.charCodeAt(offset++);
55613
+ if (size === 0) return offset;
55614
+ offset += size;
55615
+ }
55616
+ return -1;
55617
+ }
55618
+ function isGif(data) {
55619
+ if (!data.startsWith("GIF87a") && !data.startsWith("GIF89a") || data.length < 13) return false;
55620
+ if (!(data.charCodeAt(6) | data.charCodeAt(7)) || !(data.charCodeAt(8) | data.charCodeAt(9)))
55621
+ return false;
55622
+ const packed = data.charCodeAt(10);
55623
+ let offset = 13 + (packed & 128 ? 3 * (2 << (packed & 7)) : 0);
55624
+ let image = false;
55625
+ while (offset < data.length) {
55626
+ const marker = data.charCodeAt(offset++);
55627
+ if (marker === 59) return image && offset === data.length;
55628
+ if (marker === 33) offset = skipGifBlocks(data, offset + 1);
55629
+ else if (marker === 44) {
55630
+ if (offset + 9 > data.length || !(data.charCodeAt(offset + 4) | data.charCodeAt(offset + 5)) || !(data.charCodeAt(offset + 6) | data.charCodeAt(offset + 7)))
55631
+ return false;
55632
+ const local = data.charCodeAt(offset + 8);
55633
+ offset += 9 + (local & 128 ? 3 * (2 << (local & 7)) : 0);
55634
+ const codeSize = data.charCodeAt(offset++);
55635
+ if (codeSize < 2 || codeSize > 8 || offset >= data.length || data.charCodeAt(offset) === 0)
55636
+ return false;
55637
+ offset = skipGifBlocks(data, offset);
55638
+ image = true;
55639
+ } else return false;
55640
+ if (offset < 0) return false;
55641
+ }
55642
+ return false;
55643
+ }
55644
+ function isWebp(data) {
55645
+ return data.startsWith("RIFF") && data.slice(8, 12) === "WEBP" && little32(data, 4) + 8 === data.length && webpChunks(data, 12, data.length, true);
55646
+ }
55647
+ function webpChunks(data, start, end, allowFrames) {
55648
+ let image = false;
55649
+ let offset = start;
55650
+ while (offset + 8 <= end) {
55651
+ const type = data.slice(offset, offset + 4);
55652
+ const size = little32(data, offset + 4);
55653
+ const body = offset + 8;
55654
+ const next = body + size + size % 2;
55655
+ if (next > end) return false;
55656
+ if (type === "VP8 ") {
55657
+ if (size < 11 || data.charCodeAt(body) & 1 || data.slice(body + 3, body + 6) !== "\x9D*")
55658
+ return false;
55659
+ if (!((data.charCodeAt(body + 6) | data.charCodeAt(body + 7) << 8) & 16383) || !((data.charCodeAt(body + 8) | data.charCodeAt(body + 9) << 8) & 16383))
55660
+ return false;
55661
+ image = true;
55662
+ } else if (type === "VP8L") {
55663
+ if (size < 6 || data.charCodeAt(body) !== 47 || data.charCodeAt(body + 4) & 224)
55664
+ return false;
55665
+ image = true;
55666
+ } else if (type === "VP8X") {
55667
+ if (size !== 10 || !allowFrames) return false;
55668
+ } else if (type === "ANMF") {
55669
+ if (!allowFrames || size < 16 || !webpChunks(data, body + 16, body + size, false))
55670
+ return false;
55671
+ image = true;
55672
+ }
55673
+ offset = next;
55674
+ }
55675
+ return image && offset === end;
55676
+ }
55677
+
55488
55678
  // ../core/src/transcriptImages.ts
55489
55679
  var IMAGE_DATA_URI_RE = /^data:(image\/[\w.+-]+);base64,([A-Za-z0-9+/=_-]*)$/;
55490
55680
  function isImageDataUri(value) {
55491
- return value.startsWith("data:image/") && IMAGE_DATA_URI_RE.test(value);
55681
+ if (!value.startsWith("data:image/")) return false;
55682
+ const match = IMAGE_DATA_URI_RE.exec(value);
55683
+ return match !== null && isEncodedImage(match[2], match[1]);
55492
55684
  }
55493
55685
  function isImageBase64Payload(record2, key) {
55494
- if (typeof record2[key] !== "string") return false;
55495
- if (key === "data") return record2.type === "base64" && isImageMediaType(record2.media_type);
55496
- if (key === "base64") return isImageMediaType(record2.type);
55686
+ const data = record2[key];
55687
+ if (typeof data !== "string") return false;
55688
+ if (key === "data" && record2.type === "base64") return isEncodedImage(data, record2.media_type);
55689
+ if (key === "base64") return isEncodedImage(data, record2.type);
55497
55690
  return false;
55498
55691
  }
55499
- function isImageMediaType(value) {
55500
- return typeof value === "string" && value.startsWith("image/");
55501
- }
55502
55692
  function describeImageBlock(block2) {
55503
55693
  if (block2.type !== "image") return null;
55504
55694
  const source = asRecord(block2.source);
@@ -55700,10 +55890,21 @@ function createDigestTurnParser(options = {}) {
55700
55890
  const turns = [];
55701
55891
  let pendingTools = [];
55702
55892
  let pendingUsage = null;
55893
+ let pendingAt = null;
55703
55894
  let currentId = null;
55704
55895
  let currentCarrier = null;
55705
55896
  let currentBase = null;
55706
55897
  let currentCounted = false;
55898
+ const envelopes = [];
55899
+ let pendingEnvelopes = [];
55900
+ let currentEnvelope = null;
55901
+ const landPending = (turn) => {
55902
+ for (const envelope of pendingEnvelopes) {
55903
+ envelope.turn = turn;
55904
+ envelopes.push(envelope);
55905
+ }
55906
+ pendingEnvelopes = [];
55907
+ };
55707
55908
  return {
55708
55909
  pushLine(line) {
55709
55910
  const trimmed = line.trim();
@@ -55717,14 +55918,16 @@ function createDigestTurnParser(options = {}) {
55717
55918
  return;
55718
55919
  }
55719
55920
  if (record2.isMeta === true) return;
55921
+ const at = recordTime(record2);
55720
55922
  if (record2.type === "user") {
55721
55923
  const text = userText(record2);
55722
- if (text) turns.push({ kind: "human_prompt", text, toolCalls: [], usage: null });
55924
+ if (text) turns.push({ kind: "human_prompt", text, toolCalls: [], usage: null, at });
55723
55925
  return;
55724
55926
  }
55725
55927
  if (record2.type !== "assistant") return;
55726
55928
  const message2 = record2.message;
55727
55929
  if (!message2 || message2.model === "<synthetic>" || !Array.isArray(message2.content)) return;
55930
+ pendingAt ??= at;
55728
55931
  const id = messageIdOf(record2, message2);
55729
55932
  if (id === null || id !== currentId) {
55730
55933
  currentId = id;
@@ -55738,12 +55941,17 @@ function createDigestTurnParser(options = {}) {
55738
55941
  const updated = mergeUsage(currentBase, parsedUsage);
55739
55942
  if (currentCarrier) currentCarrier.usage = updated;
55740
55943
  else pendingUsage = updated;
55944
+ if (currentEnvelope) currentEnvelope.usage = parsedUsage;
55741
55945
  } else if (currentCarrier) {
55742
55946
  currentCarrier.usage = mergeUsage(currentCarrier.usage, parsedUsage);
55743
55947
  currentCounted = true;
55948
+ currentEnvelope = { turn: turns.indexOf(currentCarrier), at, usage: parsedUsage };
55949
+ envelopes.push(currentEnvelope);
55744
55950
  } else {
55745
55951
  pendingUsage = mergeUsage(pendingUsage, parsedUsage);
55746
55952
  currentCounted = true;
55953
+ currentEnvelope = { turn: -1, at, usage: parsedUsage };
55954
+ pendingEnvelopes.push(currentEnvelope);
55747
55955
  }
55748
55956
  }
55749
55957
  const model = typeof message2.model === "string" ? message2.model : null;
@@ -55758,32 +55966,50 @@ function createDigestTurnParser(options = {}) {
55758
55966
  }
55759
55967
  if (part.type === "text" && typeof part.text === "string" && part.text.trim() !== "") {
55760
55968
  if (currentCarrier === null) {
55761
- const turn = { kind: "agent_text", text: part.text, toolCalls: pendingTools, usage: pendingUsage };
55969
+ const turn = {
55970
+ kind: "agent_text",
55971
+ text: part.text,
55972
+ toolCalls: pendingTools,
55973
+ usage: pendingUsage,
55974
+ at: pendingAt
55975
+ };
55762
55976
  turns.push(turn);
55763
55977
  currentCarrier = turn;
55764
55978
  pendingUsage = null;
55979
+ landPending(turns.length - 1);
55765
55980
  } else {
55766
55981
  turns.push({
55767
55982
  kind: "agent_text",
55768
55983
  text: part.text,
55769
55984
  toolCalls: pendingTools,
55770
- usage: currentCounted ? zeroUsage(model) : null
55985
+ usage: currentCounted ? zeroUsage(model) : null,
55986
+ // A sibling block of the same record began when the record did.
55987
+ at: pendingAt ?? at
55771
55988
  });
55772
55989
  }
55773
55990
  pendingTools = [];
55991
+ pendingAt = null;
55774
55992
  }
55775
55993
  }
55776
55994
  },
55777
55995
  finish() {
55778
55996
  if (pendingTools.length > 0 || pendingUsage !== null) {
55779
- turns.push({ kind: "agent_text", text: "", toolCalls: pendingTools, usage: pendingUsage });
55997
+ turns.push({ kind: "agent_text", text: "", toolCalls: pendingTools, usage: pendingUsage, at: pendingAt });
55998
+ landPending(turns.length - 1);
55780
55999
  pendingTools = [];
55781
56000
  pendingUsage = null;
56001
+ pendingAt = null;
55782
56002
  }
55783
56003
  return turns;
56004
+ },
56005
+ usageEnvelopes() {
56006
+ return envelopes;
55784
56007
  }
55785
56008
  };
55786
56009
  }
56010
+ function recordTime(record2) {
56011
+ return typeof record2.timestamp === "string" && record2.timestamp !== "" ? record2.timestamp : null;
56012
+ }
55787
56013
  function isRecord(value) {
55788
56014
  return value !== null && typeof value === "object" && !Array.isArray(value);
55789
56015
  }
@@ -55920,10 +56146,12 @@ function promptText(content) {
55920
56146
  function createCodexTurnParser(options = {}) {
55921
56147
  const turns = [];
55922
56148
  let pendingTools = [];
56149
+ let pendingAt = null;
55923
56150
  const flushPendingTools = () => {
55924
56151
  if (pendingTools.length === 0) return;
55925
- turns.push({ kind: "agent_text", text: "", toolCalls: pendingTools, usage: null });
56152
+ turns.push({ kind: "agent_text", text: "", toolCalls: pendingTools, usage: null, at: pendingAt });
55926
56153
  pendingTools = [];
56154
+ pendingAt = null;
55927
56155
  };
55928
56156
  return {
55929
56157
  pushLine(line) {
@@ -55938,6 +56166,7 @@ function createCodexTurnParser(options = {}) {
55938
56166
  return;
55939
56167
  }
55940
56168
  if (record2.type !== "response_item") return;
56169
+ const at = typeof record2.timestamp === "string" && record2.timestamp !== "" ? record2.timestamp : null;
55941
56170
  const item = record2.payload;
55942
56171
  if (item === null || typeof item !== "object" || Array.isArray(item)) return;
55943
56172
  const responseItem = item;
@@ -55949,6 +56178,7 @@ function createCodexTurnParser(options = {}) {
55949
56178
  if (args !== null) call.input = boundedToolInput(args);
55950
56179
  }
55951
56180
  pendingTools.push(call);
56181
+ pendingAt ??= at;
55952
56182
  return;
55953
56183
  }
55954
56184
  if (responseItem.type !== "message") return;
@@ -55956,15 +56186,16 @@ function createCodexTurnParser(options = {}) {
55956
56186
  const text2 = promptText(responseItem.content);
55957
56187
  if (text2 !== null) {
55958
56188
  flushPendingTools();
55959
- turns.push({ kind: "human_prompt", text: text2, toolCalls: [], usage: null });
56189
+ turns.push({ kind: "human_prompt", text: text2, toolCalls: [], usage: null, at });
55960
56190
  }
55961
56191
  return;
55962
56192
  }
55963
56193
  if (responseItem.role !== "assistant") return;
55964
56194
  const text = outputTextBlocks(responseItem.content).join("\n");
55965
56195
  if (text.trim() === "") return;
55966
- turns.push({ kind: "agent_text", text, toolCalls: pendingTools, usage: null });
56196
+ turns.push({ kind: "agent_text", text, toolCalls: pendingTools, usage: null, at: pendingAt ?? at });
55967
56197
  pendingTools = [];
56198
+ pendingAt = null;
55968
56199
  },
55969
56200
  finish() {
55970
56201
  flushPendingTools();
@@ -55977,10 +56208,12 @@ function createCodexTurnParser(options = {}) {
55977
56208
  function createCursorTurnParser(options = {}) {
55978
56209
  const turns = [];
55979
56210
  let pendingTools = [];
56211
+ let pendingAt = null;
55980
56212
  const flushPendingTools = () => {
55981
56213
  if (pendingTools.length === 0) return;
55982
- turns.push({ kind: "agent_text", text: "", toolCalls: pendingTools, usage: null });
56214
+ turns.push({ kind: "agent_text", text: "", toolCalls: pendingTools, usage: null, at: pendingAt });
55983
56215
  pendingTools = [];
56216
+ pendingAt = null;
55984
56217
  };
55985
56218
  return {
55986
56219
  pushLine(line) {
@@ -55994,9 +56227,10 @@ function createCursorTurnParser(options = {}) {
55994
56227
  } catch {
55995
56228
  return;
55996
56229
  }
56230
+ const at = typeof record2.createdAt === "string" && record2.createdAt !== "" ? record2.createdAt : null;
55997
56231
  if (record2.type === "user" && typeof record2.text === "string" && record2.text.trim() !== "") {
55998
56232
  flushPendingTools();
55999
- turns.push({ kind: "human_prompt", text: record2.text, toolCalls: [], usage: null });
56233
+ turns.push({ kind: "human_prompt", text: record2.text, toolCalls: [], usage: null, at });
56000
56234
  return;
56001
56235
  }
56002
56236
  if (record2.type === "tool" && typeof record2.name === "string") {
@@ -56006,11 +56240,13 @@ function createCursorTurnParser(options = {}) {
56006
56240
  call.input = boundedToolInput(record2.args);
56007
56241
  }
56008
56242
  pendingTools.push(call);
56243
+ pendingAt ??= at;
56009
56244
  return;
56010
56245
  }
56011
56246
  if (record2.type === "agent" && typeof record2.text === "string" && record2.text.trim() !== "") {
56012
- turns.push({ kind: "agent_text", text: record2.text, toolCalls: pendingTools, usage: null });
56247
+ turns.push({ kind: "agent_text", text: record2.text, toolCalls: pendingTools, usage: null, at: pendingAt ?? at });
56013
56248
  pendingTools = [];
56249
+ pendingAt = null;
56014
56250
  }
56015
56251
  },
56016
56252
  finish() {
@@ -56076,7 +56312,7 @@ function parseCursorRunEventsDetailed(runs, options = {}) {
56076
56312
  let text = "";
56077
56313
  const flush2 = () => {
56078
56314
  if (text !== "" || pendingTools.length > 0 || pendingTokens > 0) {
56079
- turns.push({ kind: "agent_text", text, toolCalls: pendingTools, usage: null });
56315
+ turns.push({ kind: "agent_text", text, toolCalls: pendingTools, usage: null, at: null });
56080
56316
  turnTokens.push(pendingTokens);
56081
56317
  }
56082
56318
  text = "";
@@ -56126,7 +56362,7 @@ function parseCursorRunEventsDetailed(runs, options = {}) {
56126
56362
  const prompt = stringField(asRecord3(data?.userMessage), "text");
56127
56363
  if (!prompt) continue;
56128
56364
  if (text !== "") flush2();
56129
- turns.push({ kind: "human_prompt", text: prompt, toolCalls: [], usage: null });
56365
+ turns.push({ kind: "human_prompt", text: prompt, toolCalls: [], usage: null, at: null });
56130
56366
  turnTokens.push(0);
56131
56367
  continue;
56132
56368
  }
@@ -56183,10 +56419,16 @@ function normalizeUrl(raw) {
56183
56419
 
56184
56420
  // ../core/src/sessionTurns.ts
56185
56421
  function parseSessionTurns(source, transcript, options = {}) {
56186
- if (source === "cursor-cloud") return parseCursorRunEvents(envelopeRuns(transcript), options);
56422
+ return parseSessionTurnsDetailed(source, transcript, options).turns;
56423
+ }
56424
+ function parseSessionTurnsDetailed(source, transcript, options = {}) {
56425
+ if (source === "cursor-cloud") {
56426
+ return { turns: parseCursorRunEvents(envelopeRuns(transcript), options), envelopes: [] };
56427
+ }
56187
56428
  const parser = createSessionTurnParser(source, options);
56188
56429
  for (const line of transcript.split("\n")) parser.pushLine(line);
56189
- return parser.finish();
56430
+ const turns = parser.finish();
56431
+ return { turns, envelopes: parser.usageEnvelopes?.() ?? [] };
56190
56432
  }
56191
56433
  function createSessionTurnParser(source, options = {}) {
56192
56434
  if (source === "codex") return createCodexTurnParser(options);
@@ -56336,6 +56578,38 @@ function scrubBreadcrumb(breadcrumb) {
56336
56578
  return breadcrumb;
56337
56579
  }
56338
56580
 
56581
+ // ../core/src/freshness.ts
56582
+ function describeDataAge(generatedAt, now) {
56583
+ const ageMs = now.getTime() - generatedAt.getTime();
56584
+ if (ageMs < 0) return "just now";
56585
+ const minutes = Math.floor(ageMs / 6e4);
56586
+ if (minutes < 1) return "just now";
56587
+ if (minutes < 60) return `${minutes}m ago`;
56588
+ const hours = Math.floor(minutes / 60);
56589
+ if (hours < 24) return `${hours}h ago`;
56590
+ const days = Math.floor(hours / 24);
56591
+ return `${days}d ago`;
56592
+ }
56593
+
56594
+ // ../core/src/onboarding.ts
56595
+ var SAME_HOUR_MS = 60 * 60 * 1e3;
56596
+ function describeSyncSignals(member, now, noUpload = "no sessions uploaded yet") {
56597
+ const session = member.lastSessionActivityAt;
56598
+ const poll = member.lastCheckedInAt;
56599
+ const error62 = member.lastSyncErrorAt;
56600
+ const paused = member.syncPaused === true;
56601
+ if (session === null && poll === null && error62 === null) return null;
56602
+ const age = (at) => describeDataAge(new Date(at), now);
56603
+ const apart = (a, b2) => b2 === null || Math.abs(Date.parse(a) - Date.parse(b2)) > SAME_HOUR_MS;
56604
+ const parts = [session === null ? noUpload : `last uploaded session ${age(session)}`];
56605
+ if (error62 !== null) parts.push(`reported sync error ${age(error62)}`);
56606
+ if (paused) parts.push("syncing paused");
56607
+ const pollSays = error62 !== null ? apart(poll ?? error62, error62) : paused || apart(poll ?? "", session);
56608
+ if (poll !== null && pollSays) parts.push(`last poll ${age(poll)}`);
56609
+ const line = parts.join(", ");
56610
+ return `${line.charAt(0).toUpperCase()}${line.slice(1)}.`;
56611
+ }
56612
+
56339
56613
  // ../core/src/sessionIngest.ts
56340
56614
  var SESSION_SOURCES = [
56341
56615
  "claude-code",
@@ -83283,9 +83557,20 @@ var storedCredentialsSchema = external_exports.object({
83283
83557
  * (`powerplant folders add`). */
83284
83558
  scopePaths: external_exports.array(external_exports.string()).optional(),
83285
83559
  /** macOS privacy roots the member explicitly allowed Powerplant to inspect
83286
- * for Git metadata. This does not widen capture: org policy still decides
83287
- * which discovered repositories upload. */
83560
+ * for Git metadata, and that macOS let it read. This does not widen
83561
+ * capture: org policy still decides which discovered repositories upload. */
83288
83562
  repoDiscoveryRoots: external_exports.array(external_exports.string()).optional(),
83563
+ /** When the member answered yes to Git metadata scans of macOS-protected
83564
+ * session folders (ISO 8601). One answer for this enrollment: later init
83565
+ * runs scan a newly seen root after an explanation, without asking again.
83566
+ * Absent = never asked, or never answered yes. */
83567
+ repoDiscoveryConsentedAt: external_exports.string().optional(),
83568
+ /** Protected roots the member allowed but macOS refused to let Powerplant
83569
+ * read (a declined Files and Folders dialog). macOS remembers that refusal
83570
+ * and shows no second dialog, so init re-reads these roots on every run
83571
+ * without a question and moves a root to repoDiscoveryRoots once the
83572
+ * member enables it in System Settings. */
83573
+ repoDiscoveryBlockedRoots: external_exports.array(external_exports.string()).optional(),
83289
83574
  /** When this enrollment's deep first sync completed; absent = still owed,
83290
83575
  * so the next sync sweeps DEEP_FIRST_SYNC_DAYS instead of the rolling window. */
83291
83576
  deepSyncedAt: external_exports.string().optional()
@@ -83371,7 +83656,7 @@ import os6 from "node:os";
83371
83656
  // ../../apps/collector/src/http.ts
83372
83657
  var import_undici = __toESM(require_undici(), 1);
83373
83658
  var CLI_VERSION_HEADER = "x-powerplant-cli-version";
83374
- var cliVersion = true ? "0.4.6" : "0.0.0-checkout";
83659
+ var cliVersion = true ? "0.4.8" : "0.0.0-checkout";
83375
83660
  function cliVersionHeaders() {
83376
83661
  return { [CLI_VERSION_HEADER]: cliVersion, "user-agent": `powerplant-cli/${cliVersion}` };
83377
83662
  }
@@ -83556,8 +83841,8 @@ function unconsentedProtectedRoot(candidate, environment = {}) {
83556
83841
  );
83557
83842
  return consented ? null : protectedRoot;
83558
83843
  }
83559
- var DENIED = { kind: "denied" };
83560
83844
  var NONE = { kind: "none" };
83845
+ var deniedAt = (p) => ({ kind: "denied", path: p });
83561
83846
  function consentGate(candidate, environment) {
83562
83847
  const root = unconsentedProtectedRoot(candidate, environment);
83563
83848
  return root === null ? null : { kind: "unconsented", root, path: path2.resolve(candidate) };
@@ -83566,7 +83851,7 @@ function readProbe(p) {
83566
83851
  try {
83567
83852
  return { kind: "found", value: readFileSync3(p, "utf8") };
83568
83853
  } catch (cause) {
83569
- return isPermissionError(cause) ? DENIED : NONE;
83854
+ return isPermissionError(cause) ? deniedAt(p) : NONE;
83570
83855
  }
83571
83856
  }
83572
83857
  function gitDirReferenceProbe(cwd, environment) {
@@ -83576,7 +83861,7 @@ function gitDirReferenceProbe(cwd, environment) {
83576
83861
  for (; ; ) {
83577
83862
  const gitPath = path2.join(dir, ".git");
83578
83863
  const kind = statKind(gitPath);
83579
- if (kind === "denied") return DENIED;
83864
+ if (kind === "denied") return deniedAt(gitPath);
83580
83865
  if (kind === "dir") return { kind: "found", value: gitPath };
83581
83866
  if (kind === "file") {
83582
83867
  const pointer = readProbe(gitPath);
@@ -83600,7 +83885,7 @@ function gitDirsProbe(cwd, environment) {
83600
83885
  const gitDirGate = consentGate(gitDir, environment);
83601
83886
  if (gitDirGate) return gitDirGate;
83602
83887
  const commondir = readProbe(path2.join(gitDir, "commondir"));
83603
- if (commondir.kind === "denied") return DENIED;
83888
+ if (commondir.kind === "denied") return commondir;
83604
83889
  const pointer = commondir.kind === "found" ? commondir.value.trim() : "";
83605
83890
  const commonDir = pointer ? path2.resolve(gitDir, pointer) : gitDir;
83606
83891
  const commonDirGate = consentGate(commonDir, environment);
@@ -83835,10 +84120,15 @@ function tokenRejected(route) {
83835
84120
  `${route} rejected this machine's token (revoked or unknown). Re-enroll this machine (powerplant enroll, or pnpm collector enroll from the checkout).`
83836
84121
  );
83837
84122
  }
83838
- async function fetchCapturePolicy(apiUrl, machineToken) {
84123
+ function capturePolicyUrl(apiUrl, request2) {
84124
+ const url2 = new URL(`${apiUrl}/api/collector/capture-policy`);
84125
+ if (request2.paused !== void 0) url2.searchParams.set("paused", String(request2.paused));
84126
+ return url2.toString();
84127
+ }
84128
+ async function fetchCapturePolicy(apiUrl, machineToken, request2 = {}) {
83839
84129
  const response = await fetchWithRetry(
83840
84130
  "fetching the org's capture policy",
83841
- `${apiUrl}/api/collector/capture-policy`,
84131
+ capturePolicyUrl(apiUrl, request2),
83842
84132
  { headers: { authorization: `Bearer ${machineToken}` } }
83843
84133
  );
83844
84134
  if (response.status === 401) {
@@ -83869,6 +84159,19 @@ async function fetchCapturePolicy(apiUrl, machineToken) {
83869
84159
  );
83870
84160
  return result.data;
83871
84161
  }
84162
+ var CHECK_IN_INTERVAL_MS = 30 * 6e4;
84163
+ async function reportCheckIn(apiUrl, machineToken, paused, fetcher = fetch) {
84164
+ try {
84165
+ const response = await fetcher(capturePolicyUrl(apiUrl, { paused }), {
84166
+ headers: { ...cliVersionHeaders(), authorization: `Bearer ${machineToken}` },
84167
+ signal: AbortSignal.timeout(3e3)
84168
+ });
84169
+ await response.body?.cancel();
84170
+ return response.ok;
84171
+ } catch {
84172
+ return false;
84173
+ }
84174
+ }
83872
84175
  async function uploadSessions(apiUrl, machineToken, items) {
83873
84176
  let upserted = 0;
83874
84177
  let unchanged = 0;
@@ -84409,7 +84712,6 @@ function removeFolder(config2, rawPath, enrollCommand, log3, deps = defaultDeps)
84409
84712
 
84410
84713
  // ../../apps/collector/src/diagnostics.ts
84411
84714
  import { createHmac } from "node:crypto";
84412
- var DIAGNOSTIC_INTERVAL_MS = 30 * 6e4;
84413
84715
  function observeFileDiagnostic(observer, file2) {
84414
84716
  try {
84415
84717
  observer?.(file2);
@@ -84527,8 +84829,7 @@ function createDiagnosticsReporter(credentials, send = createDiagnosticSender())
84527
84829
  let pending = Promise.resolve();
84528
84830
  let enrollment = null;
84529
84831
  let lastIssue = null;
84530
- let paused = false;
84531
- const report2 = (detailed) => {
84832
+ const report2 = () => {
84532
84833
  try {
84533
84834
  const current = credentials();
84534
84835
  if (!current) return;
@@ -84537,29 +84838,20 @@ function createDiagnosticsReporter(credentials, send = createDiagnosticSender())
84537
84838
  lastIssue = null;
84538
84839
  }
84539
84840
  const matching = pass !== null && passEnrollments.get(pass) === current.token ? pass : null;
84540
- if (detailed) {
84541
- if (matching === null) return;
84542
- const issue2 = issueSignature(matching);
84543
- if (issue2 === lastIssue) return;
84544
- lastIssue = issue2;
84545
- }
84546
- const body = collectorDiagnosticSchema.parse({
84547
- state: matching?.outcome === "running" ? "syncing" : paused ? "paused" : "idle",
84548
- pass: detailed ? matching : null
84549
- });
84841
+ if (matching === null) return;
84842
+ const issue2 = issueSignature(matching);
84843
+ if (issue2 === lastIssue) return;
84844
+ lastIssue = issue2;
84845
+ const body = collectorDiagnosticSchema.parse({ state: "idle", pass: matching });
84550
84846
  pending = send(current, body).catch(() => void 0);
84551
84847
  } catch {
84552
84848
  }
84553
84849
  };
84554
84850
  return {
84555
- heartbeat: (isPaused = false) => {
84556
- paused = isPaused;
84557
- report2(false);
84558
- },
84559
84851
  flush: () => pending,
84560
84852
  onDiagnostics: (diagnostic) => {
84561
84853
  pass = diagnostic;
84562
- if (diagnostic.outcome !== "running") report2(true);
84854
+ if (diagnostic.outcome !== "running") report2();
84563
84855
  }
84564
84856
  };
84565
84857
  }
@@ -85071,6 +85363,7 @@ function readCodexTranscriptHints(home, roots) {
85071
85363
  }
85072
85364
 
85073
85365
  // ../../apps/collector/src/codexCli.ts
85366
+ import { closeSync as closeSync3, mkdirSync as mkdirSync2, openSync as openSync3, renameSync, rmSync, utimesSync, writeSync } from "node:fs";
85074
85367
  import os8 from "node:os";
85075
85368
  import path7 from "node:path";
85076
85369
  import { setImmediate as yieldToEventLoop2 } from "node:timers/promises";
@@ -85145,19 +85438,96 @@ function parseCodexRollout(text) {
85145
85438
  ...typeof git?.repository_url === "string" && repoFullNameFrom(git.repository_url)
85146
85439
  };
85147
85440
  }
85148
- function parseRolloutFile(rolloutPath) {
85149
- const text = readIfExists(rolloutPath);
85150
- return text === null ? null : parseCodexRollout(text);
85151
- }
85441
+ function foldRolloutPages(pages) {
85442
+ const first = (key) => pages.find((page) => page[key] !== void 0)?.[key];
85443
+ const last = (key) => [...pages].reverse().find((page) => page[key] !== void 0)?.[key];
85444
+ const sum2 = (measured2) => measured2.length === 0 ? null : measured2.reduce((total, page) => addCodexTotals(total, page));
85445
+ const measured = pages.map((page) => page.totals).filter((totals) => totals !== null);
85446
+ const measuredFast = pages.filter((page) => page.totals !== null).map((page) => page.fastTotals);
85447
+ const sessionId = first("sessionId");
85448
+ const startedAt = first("startedAt");
85449
+ const endedAt = last("endedAt");
85450
+ const model = last("model");
85451
+ const speedMode = last("speedMode");
85452
+ const cwd = first("cwd");
85453
+ const gitBranch = first("gitBranch");
85454
+ const repoName = first("repoName");
85455
+ const repoFullName = first("repoFullName");
85456
+ return {
85457
+ kind: pages[0].kind,
85458
+ ...sessionId !== void 0 && { sessionId },
85459
+ ...startedAt !== void 0 && { startedAt },
85460
+ ...endedAt !== void 0 && { endedAt },
85461
+ ...model !== void 0 && { model },
85462
+ totals: sum2(measured),
85463
+ fastTotals: measuredFast.every((fast) => fast !== null) ? sum2(measuredFast) : null,
85464
+ ...speedMode !== void 0 && { speedMode },
85465
+ ...cwd !== void 0 && { cwd },
85466
+ ...gitBranch !== void 0 && { gitBranch },
85467
+ ...repoName !== void 0 && { repoName },
85468
+ ...repoFullName !== void 0 && { repoFullName }
85469
+ };
85470
+ }
85471
+ function rolloutIdentity(rolloutPath) {
85472
+ const name = path7.basename(rolloutPath, ".jsonl");
85473
+ const [head, pageId, ...rest] = name.split("_");
85474
+ const threadId2 = head.slice(-36);
85475
+ const conventional = rest.length === 0 && head.startsWith("rollout-") && head.at(-37) === "-" && SESSION_UUID.test(threadId2) && (pageId === void 0 || SESSION_UUID.test(pageId));
85476
+ if (!conventional) return { threadId: name, page: false };
85477
+ return { threadId: threadId2.toLowerCase(), page: pageId !== void 0 };
85478
+ }
85479
+ function codexSpoolDir(home) {
85480
+ return path7.join(home, ".config", "powerplant", "codex");
85481
+ }
85482
+ function mergedSpoolIsFresh(spoolPath, mtimeMs) {
85483
+ const existing = statIfExists(spoolPath);
85484
+ return existing !== null && Math.abs(existing.mtimeMs - mtimeMs) < 1;
85485
+ }
85486
+ var MergedSpoolWriter = class {
85487
+ constructor(spoolPath) {
85488
+ this.spoolPath = spoolPath;
85489
+ mkdirSync2(path7.dirname(spoolPath), { recursive: true, mode: 448 });
85490
+ this.tmpPath = `${spoolPath}.${process.pid}.tmp`;
85491
+ this.fd = openSync3(this.tmpPath, "w", 384);
85492
+ }
85493
+ spoolPath;
85494
+ tmpPath;
85495
+ fd;
85496
+ bytes = 0;
85497
+ append(pageText) {
85498
+ if (this.fd === void 0) throw new Error("merged transcript already closed");
85499
+ const text = pageText.endsWith("\n") ? pageText : `${pageText}
85500
+ `;
85501
+ this.bytes += writeSync(this.fd, text);
85502
+ }
85503
+ /** Stamps the newest page's mtime and publishes the file; returns its size. */
85504
+ finish(mtimeMs) {
85505
+ this.close();
85506
+ utimesSync(this.tmpPath, mtimeMs / 1e3, mtimeMs / 1e3);
85507
+ renameSync(this.tmpPath, this.spoolPath);
85508
+ return this.bytes;
85509
+ }
85510
+ abandon() {
85511
+ this.close();
85512
+ rmSync(this.tmpPath, { force: true });
85513
+ }
85514
+ close() {
85515
+ if (this.fd === void 0) return;
85516
+ closeSync3(this.fd);
85517
+ this.fd = void 0;
85518
+ }
85519
+ };
85152
85520
  function rolloutLocation(rolloutPath) {
85153
85521
  const record2 = readFirstRecord(rolloutPath, (candidate) => candidate.type === "session_meta");
85154
85522
  const meta3 = record2?.payload;
85155
85523
  if (!meta3 || typeof meta3 !== "object" || Array.isArray(meta3)) return { kind: "session", totals: null, fastTotals: null };
85156
85524
  const { cwd, id, session_id: fallbackId } = meta3;
85157
85525
  const sessionId = id ?? fallbackId;
85526
+ const startedAt = record2?.timestamp;
85158
85527
  return {
85159
85528
  kind: isSidecarThread(meta3) ? "subagent" : "session",
85160
85529
  ...typeof sessionId === "string" && { sessionId },
85530
+ ...typeof startedAt === "string" && { startedAt },
85161
85531
  totals: null,
85162
85532
  fastTotals: null,
85163
85533
  ...typeof cwd === "string" && cwd !== "" && { cwd }
@@ -85212,89 +85582,128 @@ async function collectCodexSessions(options = {}) {
85212
85582
  const now = options.now ?? Date.now();
85213
85583
  const capture = options.capture ?? captureEverythingUnresolved;
85214
85584
  const maxBytes = options.maxTranscriptBytes ?? MAX_TRANSCRIPT_FILE_BYTES;
85585
+ const spoolDir = options.spoolDir ?? codexSpoolDir(options.homeDir ?? os8.homedir());
85215
85586
  const items = [];
85216
85587
  const candidates = [];
85217
85588
  const unreadable = [];
85218
85589
  const skips = { skippedOutOfScope: 0, skippedNoRepo: 0 };
85219
85590
  let skippedSubagentThreads = 0;
85220
85591
  let skippedTooLarge = 0;
85592
+ let mergedThreads = 0;
85593
+ let largestMergedBytes = 0;
85221
85594
  const externalIdOf = (rolloutPath) => path7.basename(rolloutPath, ".jsonl");
85222
- const seen = /* @__PURE__ */ new Set();
85223
- const admitFrom = async (files, archived, sessionId) => {
85224
- for (const rolloutPath of files) {
85225
- if (seen.has(rolloutPath)) continue;
85595
+ const files = /* @__PURE__ */ new Map();
85596
+ const observe = async (paths, archived, hintSessionId) => {
85597
+ for (const rolloutPath of paths) {
85598
+ const known = files.get(rolloutPath);
85599
+ if (known !== void 0) {
85600
+ if (hintSessionId === void 0) delete known.hintSessionIds;
85601
+ else known.hintSessionIds?.push(hintSessionId);
85602
+ continue;
85603
+ }
85226
85604
  await yieldToEventLoop2();
85227
85605
  const stat = statIfExists(rolloutPath);
85228
85606
  if (!stat) continue;
85229
- seen.add(rolloutPath);
85230
- if (options.sinceMs !== void 0 && Math.max(stat.mtimeMs, stat.ctimeMs) < options.sinceMs)
85231
- continue;
85232
- if (stat.size > maxBytes) {
85607
+ files.set(rolloutPath, {
85608
+ rolloutPath,
85609
+ archived,
85610
+ identity: rolloutIdentity(rolloutPath),
85611
+ mtimeMs: stat.mtimeMs,
85612
+ changedMs: Math.max(stat.mtimeMs, stat.ctimeMs),
85613
+ sizeBytes: stat.size,
85614
+ ...hintSessionId !== void 0 && { hintSessionIds: [hintSessionId] }
85615
+ });
85616
+ }
85617
+ };
85618
+ const archiveDir = codexArchivedSessionsDir(sessionsDir);
85619
+ const hintHome = options.homeDir ?? (options.codexDir === void 0 ? os8.homedir() : void 0);
85620
+ if (hintHome !== void 0) {
85621
+ for (const hint of readCodexTranscriptHints(hintHome, [sessionsDir, archiveDir])) {
85622
+ await observe([hint.rolloutPath], hint.archived, hint.sessionId);
85623
+ }
85624
+ }
85625
+ await observe(rolloutFiles(sessionsDir), false);
85626
+ await observe(rolloutFiles(archiveDir), true);
85627
+ const byThread = /* @__PURE__ */ new Map();
85628
+ for (const file2 of files.values()) {
85629
+ const thread = byThread.get(file2.identity.threadId);
85630
+ if (thread === void 0) byThread.set(file2.identity.threadId, [file2]);
85631
+ else thread.push(file2);
85632
+ }
85633
+ for (const [threadId2, observed] of byThread) {
85634
+ const stored = dedupeStoreCopies(observed);
85635
+ let externalId = externalIdOf((stored.find((file2) => !file2.identity.page) ?? stored[0]).rolloutPath);
85636
+ try {
85637
+ const spoolPath = stored.length > 1 ? path7.join(spoolDir, `${threadId2}.jsonl`) : void 0;
85638
+ const changedMs = Math.max(...stored.map((file2) => file2.changedMs));
85639
+ if (options.sinceMs !== void 0 && changedMs < options.sinceMs) {
85640
+ const neverMerged = spoolPath !== void 0 && !options.locationsOnly && statIfExists(spoolPath) === null;
85641
+ if (!neverMerged) continue;
85642
+ }
85643
+ await yieldToEventLoop2();
85644
+ const named = stored.filter((file2) => {
85645
+ if (file2.hintSessionIds === void 0) return true;
85646
+ const { sessionId } = rolloutLocation(file2.rolloutPath);
85647
+ return sessionId !== void 0 && file2.hintSessionIds.includes(sessionId);
85648
+ });
85649
+ if (named.length === 0) continue;
85650
+ const threadFiles = named.length > 1 ? orderPages(named) : named;
85651
+ const base = threadFiles[0];
85652
+ externalId = externalIdOf(base.rolloutPath);
85653
+ const mtimeMs = Math.max(...threadFiles.map((file2) => file2.mtimeMs));
85654
+ const sizeBytes = threadFiles.reduce((sum2, file2) => sum2 + file2.sizeBytes, 0);
85655
+ if (sizeBytes > maxBytes) {
85233
85656
  skippedTooLarge += 1;
85234
85657
  observeFileDiagnostic(options.onFileDiagnostic, {
85235
- path: rolloutPath,
85658
+ path: base.rolloutPath,
85236
85659
  reason: "too_large",
85237
- sizeBytes: stat.size
85660
+ sizeBytes
85238
85661
  });
85239
85662
  continue;
85240
85663
  }
85241
- try {
85242
- const location = rolloutLocation(rolloutPath);
85243
- if (sessionId !== void 0 && location.sessionId !== sessionId) {
85244
- seen.delete(rolloutPath);
85245
- continue;
85246
- }
85247
- if (location.kind === "subagent") {
85248
- skippedSubagentThreads += 1;
85249
- continue;
85250
- }
85251
- const decision = captureDecision(location.cwd, capture);
85252
- if (!decision.capture) {
85253
- countCaptureSkip(skips, decision.reason);
85254
- continue;
85255
- }
85256
- if (options.locationsOnly) {
85257
- items.push({
85258
- mtimeMs: stat.mtimeMs,
85259
- transcriptPath: rolloutPath,
85260
- ...location.cwd && { cwd: location.cwd },
85261
- item: {
85262
- source: "codex",
85263
- externalId: externalIdOf(rolloutPath),
85264
- status: now - stat.mtimeMs < ACTIVE_GRACE_MS ? "active" : "inactive",
85265
- archived,
85266
- capturedVia: decision.via
85267
- }
85268
- });
85269
- continue;
85270
- }
85271
- candidates.push({
85272
- rolloutPath,
85273
- archived,
85274
- mtimeMs: stat.mtimeMs,
85664
+ const location = rolloutLocation(base.rolloutPath);
85665
+ if (location.kind === "subagent") {
85666
+ skippedSubagentThreads += 1;
85667
+ continue;
85668
+ }
85669
+ const decision = captureDecision(location.cwd, capture);
85670
+ if (!decision.capture) {
85671
+ countCaptureSkip(skips, decision.reason);
85672
+ continue;
85673
+ }
85674
+ const thread = {
85675
+ externalId,
85676
+ files: threadFiles,
85677
+ ...threadFiles.length > 1 && spoolPath !== void 0 && { spoolPath },
85678
+ mtimeMs,
85679
+ archived: threadFiles.every((file2) => file2.archived)
85680
+ };
85681
+ if (options.locationsOnly) {
85682
+ items.push({
85683
+ mtimeMs,
85684
+ transcriptPath: base.rolloutPath,
85275
85685
  ...location.cwd && { cwd: location.cwd },
85276
- decision
85277
- });
85278
- } catch (error62) {
85279
- unreadable.push(`${externalIdOf(rolloutPath)}: ${describeReadError(error62)}`);
85280
- observeFileDiagnostic(options.onFileDiagnostic, {
85281
- path: rolloutPath,
85282
- reason: "unreadable",
85283
- sizeBytes: null,
85284
- errorCode: diagnosticErrorCode(error62)
85686
+ item: {
85687
+ source: "codex",
85688
+ externalId,
85689
+ status: now - mtimeMs < ACTIVE_GRACE_MS ? "active" : "inactive",
85690
+ archived: thread.archived,
85691
+ capturedVia: decision.via
85692
+ }
85285
85693
  });
85694
+ continue;
85286
85695
  }
85287
- }
85288
- };
85289
- const archiveDir = codexArchivedSessionsDir(sessionsDir);
85290
- const hintHome = options.homeDir ?? (options.codexDir === void 0 ? os8.homedir() : void 0);
85291
- if (hintHome !== void 0) {
85292
- for (const hint of readCodexTranscriptHints(hintHome, [sessionsDir, archiveDir])) {
85293
- await admitFrom([hint.rolloutPath], hint.archived, hint.sessionId);
85696
+ candidates.push({ thread, ...location.cwd && { cwd: location.cwd }, decision });
85697
+ } catch (error62) {
85698
+ unreadable.push(`${externalId}: ${describeReadError(error62)}`);
85699
+ observeFileDiagnostic(options.onFileDiagnostic, {
85700
+ path: stored[0].rolloutPath,
85701
+ reason: "unreadable",
85702
+ sizeBytes: null,
85703
+ errorCode: diagnosticErrorCode(error62)
85704
+ });
85294
85705
  }
85295
85706
  }
85296
- await admitFrom(rolloutFiles(sessionsDir), false);
85297
- await admitFrom(rolloutFiles(archiveDir), true);
85298
85707
  if (options.locationsOnly) {
85299
85708
  return {
85300
85709
  sessions: items.sort((a, b2) => b2.mtimeMs - a.mtimeMs),
@@ -85302,30 +85711,59 @@ async function collectCodexSessions(options = {}) {
85302
85711
  skippedSubagentThreads,
85303
85712
  skippedTooLarge,
85304
85713
  unreadable,
85305
- heldBack: 0
85714
+ heldBack: 0,
85715
+ mergedThreads: 0,
85716
+ largestMergedBytes: 0
85306
85717
  };
85307
85718
  }
85308
- candidates.sort((a, b2) => b2.mtimeMs - a.mtimeMs);
85719
+ candidates.sort((a, b2) => b2.thread.mtimeMs - a.thread.mtimeMs);
85309
85720
  const chosen = options.maxSessions === void 0 ? candidates : candidates.slice(0, options.maxSessions);
85310
85721
  const threadNames = chosen.length > 0 ? loadThreadNames(sessionsDir) : /* @__PURE__ */ new Map();
85311
85722
  for (const candidate of chosen) {
85312
85723
  await yieldToEventLoop2();
85313
- const { rolloutPath, archived, mtimeMs, cwd, decision } = candidate;
85724
+ const { thread, cwd, decision } = candidate;
85725
+ let writer;
85314
85726
  try {
85315
- const summary = parseRolloutFile(rolloutPath);
85316
- if (!summary) continue;
85727
+ const rewrite = thread.spoolPath !== void 0 && !mergedSpoolIsFresh(thread.spoolPath, thread.mtimeMs);
85728
+ if (thread.spoolPath !== void 0 && rewrite) writer = new MergedSpoolWriter(thread.spoolPath);
85729
+ const pages = [];
85730
+ let vanished = false;
85731
+ for (const file2 of thread.files) {
85732
+ const text = readIfExists(file2.rolloutPath);
85733
+ if (text === null) {
85734
+ vanished = true;
85735
+ break;
85736
+ }
85737
+ const page = parseCodexRollout(text);
85738
+ if (page === null) continue;
85739
+ pages.push(page);
85740
+ writer?.append(text);
85741
+ }
85742
+ if (vanished || pages.length === 0) {
85743
+ writer?.abandon();
85744
+ continue;
85745
+ }
85746
+ const summary = foldRolloutPages(pages);
85747
+ let transcriptPath = thread.files[0].rolloutPath;
85748
+ if (thread.spoolPath !== void 0) {
85749
+ const mergedBytes = writer ? writer.finish(thread.mtimeMs) : statIfExists(thread.spoolPath)?.size ?? 0;
85750
+ writer = void 0;
85751
+ transcriptPath = thread.spoolPath;
85752
+ mergedThreads += 1;
85753
+ largestMergedBytes = Math.max(largestMergedBytes, mergedBytes);
85754
+ }
85317
85755
  const repoDir = summary.repoName ?? (cwd ? path7.basename(cwd) : void 0);
85318
85756
  const repoFullName = decision.repoFullName ?? summary.repoFullName;
85319
85757
  const title = summary.sessionId ? threadNames.get(summary.sessionId) : void 0;
85320
85758
  items.push({
85321
- mtimeMs,
85322
- transcriptPath: rolloutPath,
85759
+ mtimeMs: thread.mtimeMs,
85760
+ transcriptPath,
85323
85761
  ...cwd && { cwd },
85324
85762
  item: {
85325
85763
  source: "codex",
85326
- externalId: externalIdOf(rolloutPath),
85327
- status: now - mtimeMs < ACTIVE_GRACE_MS ? "active" : "inactive",
85328
- archived,
85764
+ externalId: thread.externalId,
85765
+ status: now - thread.mtimeMs < ACTIVE_GRACE_MS ? "active" : "inactive",
85766
+ archived: thread.archived,
85329
85767
  ...summary.startedAt && { startedAt: summary.startedAt },
85330
85768
  ...summary.endedAt && { endedAt: summary.endedAt },
85331
85769
  ...summary.model && { model: summary.model },
@@ -85350,9 +85788,10 @@ async function collectCodexSessions(options = {}) {
85350
85788
  }
85351
85789
  });
85352
85790
  } catch (error62) {
85353
- unreadable.push(`${externalIdOf(rolloutPath)}: ${describeReadError(error62)}`);
85791
+ writer?.abandon();
85792
+ unreadable.push(`${thread.externalId}: ${describeReadError(error62)}`);
85354
85793
  observeFileDiagnostic(options.onFileDiagnostic, {
85355
- path: rolloutPath,
85794
+ path: thread.files[0].rolloutPath,
85356
85795
  reason: "unreadable",
85357
85796
  sizeBytes: null,
85358
85797
  errorCode: diagnosticErrorCode(error62)
@@ -85365,15 +85804,32 @@ async function collectCodexSessions(options = {}) {
85365
85804
  skippedSubagentThreads,
85366
85805
  skippedTooLarge,
85367
85806
  unreadable,
85368
- heldBack: candidates.length - chosen.length
85807
+ heldBack: candidates.length - chosen.length,
85808
+ mergedThreads,
85809
+ largestMergedBytes
85369
85810
  };
85370
85811
  }
85812
+ function dedupeStoreCopies(threadFiles) {
85813
+ const byName = /* @__PURE__ */ new Map();
85814
+ for (const file2 of threadFiles) {
85815
+ const name = path7.basename(file2.rolloutPath);
85816
+ const known = byName.get(name);
85817
+ if (known === void 0 || file2.archived) byName.set(name, file2);
85818
+ }
85819
+ return [...byName.values()];
85820
+ }
85821
+ function orderPages(threadFiles) {
85822
+ const orderOf = (file2) => rolloutLocation(file2.rolloutPath).startedAt ?? "";
85823
+ return threadFiles.map((file2) => ({ file: file2, order: orderOf(file2) })).sort(
85824
+ (a, b2) => Number(a.file.identity.page) - Number(b2.file.identity.page) || a.order.localeCompare(b2.order) || a.file.rolloutPath.localeCompare(b2.file.rolloutPath)
85825
+ ).map((entry) => entry.file);
85826
+ }
85371
85827
  function codexThreadCwd(threadId2, sessionsDir = path7.join(os8.homedir(), ".codex", "sessions")) {
85372
85828
  if (!SESSION_UUID.test(threadId2)) return void 0;
85373
- const suffix = `-${threadId2.toLowerCase()}.jsonl`;
85829
+ const wanted = threadId2.toLowerCase();
85374
85830
  for (const root of [sessionsDir, codexArchivedSessionsDir(sessionsDir)]) {
85375
85831
  for (const rolloutPath of rolloutFiles(root)) {
85376
- if (!path7.basename(rolloutPath).toLowerCase().endsWith(suffix)) continue;
85832
+ if (rolloutIdentity(rolloutPath).threadId !== wanted) continue;
85377
85833
  const cwd = rolloutLocation(rolloutPath).cwd;
85378
85834
  if (cwd !== void 0) return cwd;
85379
85835
  }
@@ -85387,7 +85843,7 @@ import path10 from "node:path";
85387
85843
 
85388
85844
  // ../../apps/collector/src/cursorLocal.ts
85389
85845
  import { createHash as createHash4 } from "node:crypto";
85390
- import { existsSync as existsSync2, mkdirSync as mkdirSync3, utimesSync as utimesSync2, writeFileSync as writeFileSync3 } from "node:fs";
85846
+ import { existsSync as existsSync2, mkdirSync as mkdirSync4, utimesSync as utimesSync3, writeFileSync as writeFileSync3 } from "node:fs";
85391
85847
  import os10 from "node:os";
85392
85848
  import path9 from "node:path";
85393
85849
  import { fileURLToPath } from "node:url";
@@ -85395,15 +85851,15 @@ import { fileURLToPath } from "node:url";
85395
85851
  // ../../apps/collector/src/cursorTranscriptFile.ts
85396
85852
  import { createHash as createHash3 } from "node:crypto";
85397
85853
  import {
85398
- closeSync as closeSync3,
85854
+ closeSync as closeSync4,
85399
85855
  existsSync,
85400
85856
  lstatSync as lstatSync2,
85401
- mkdirSync as mkdirSync2,
85402
- openSync as openSync3,
85857
+ mkdirSync as mkdirSync3,
85858
+ openSync as openSync4,
85403
85859
  readSync as readSync3,
85404
85860
  readdirSync as readdirSync3,
85405
85861
  realpathSync as realpathSync2,
85406
- utimesSync,
85862
+ utimesSync as utimesSync2,
85407
85863
  writeFileSync as writeFileSync2
85408
85864
  } from "node:fs";
85409
85865
  import os9 from "node:os";
@@ -85472,7 +85928,7 @@ var hintSchema2 = external_exports.object({
85472
85928
  model: external_exports.string().max(256).optional()
85473
85929
  });
85474
85930
  function readBounded(file2, maxBytes) {
85475
- const fd = openSync3(file2, "r");
85931
+ const fd = openSync4(file2, "r");
85476
85932
  try {
85477
85933
  const chunks = [];
85478
85934
  let bytes = 0;
@@ -85485,7 +85941,7 @@ function readBounded(file2, maxBytes) {
85485
85941
  }
85486
85942
  return null;
85487
85943
  } finally {
85488
- closeSync3(fd);
85944
+ closeSync4(fd);
85489
85945
  }
85490
85946
  }
85491
85947
  function collectCursorTranscriptFiles(options, isDatabaseSession, result, databaseSpoolDir, spoolName) {
@@ -85589,9 +86045,9 @@ function collectCursorTranscriptFiles(options, isDatabaseSession, result, databa
85589
86045
  result.skippedTooLarge++;
85590
86046
  continue;
85591
86047
  }
85592
- mkdirSync2(spoolDir, { recursive: true, mode: 448 });
86048
+ mkdirSync3(spoolDir, { recursive: true, mode: 448 });
85593
86049
  writeFileSync2(spool, body, { mode: 384 });
85594
- utimesSync(spool, stat.atime, stat.mtime);
86050
+ utimesSync2(spool, stat.atime, stat.mtime);
85595
86051
  }
85596
86052
  result.sessions.push({
85597
86053
  cwd,
@@ -85942,13 +86398,13 @@ function collectFromStore(db, userDir, options, result, knownIds) {
85942
86398
  ...transcript.bubblesOmitted > 0 && { bubblesOmitted: transcript.bubblesOmitted }
85943
86399
  };
85944
86400
  if (!spoolDirReady) {
85945
- mkdirSync3(spoolDir, { recursive: true });
86401
+ mkdirSync4(spoolDir, { recursive: true });
85946
86402
  spoolDirReady = true;
85947
86403
  }
85948
86404
  const body = [meta3, ...transcript.lines].map((line) => JSON.stringify(line)).join("\n");
85949
86405
  writeFileSync3(spoolPath, `${body}
85950
86406
  `, "utf8");
85951
- utimesSync2(spoolPath, new Date(lastUpdatedAt), new Date(lastUpdatedAt));
86407
+ utimesSync3(spoolPath, new Date(lastUpdatedAt), new Date(lastUpdatedAt));
85952
86408
  }
85953
86409
  const repoSourceDir = facts.repoPath ?? cwd;
85954
86410
  const startBranch = facts.branches[0];
@@ -86080,25 +86536,33 @@ function groupProtectedSessions(sessions, environment = {}) {
86080
86536
  if (!session.cwd) continue;
86081
86537
  const cwd = path10.resolve(session.cwd);
86082
86538
  const directRoot = macPrivacyProtectedRoot(cwd, environment);
86083
- const gitDir = directRoot === null ? resolveGitDirReference(cwd, environment) : null;
86084
- const root = directRoot ?? (gitDir ? macPrivacyProtectedRoot(gitDir, environment) : null);
86085
- if (root === null) continue;
86086
- const folder = directRoot !== null || gitDir === null ? cwd : checkoutOfGitDir(gitDir);
86087
- tally.add(root, folder, cwd);
86539
+ if (directRoot !== null) tally.add(directRoot, cwd, cwd);
86540
+ const gitDir = resolveGitDirReference(cwd, environment);
86541
+ const checkoutRoot = gitDir ? macPrivacyProtectedRoot(gitDir, environment) : null;
86542
+ if (checkoutRoot !== null && checkoutRoot !== directRoot) {
86543
+ tally.add(checkoutRoot, checkoutOfGitDir(gitDir), cwd);
86544
+ }
86088
86545
  }
86089
86546
  return tally.entries();
86090
86547
  }
86091
86548
  var defaultAllowDeps = {
86092
86549
  load: loadCredentials,
86093
86550
  persist: saveCredentials,
86094
- probeRepo: repoProber
86551
+ probeRepo: repoProber,
86552
+ now: Date.now
86095
86553
  };
86554
+ function protectedRootReview(credentials, root) {
86555
+ const resolved = path10.resolve(root);
86556
+ if ((credentials.repoDiscoveryRoots ?? []).includes(resolved)) return "allowed";
86557
+ if ((credentials.repoDiscoveryBlockedRoots ?? []).includes(resolved)) return "blocked";
86558
+ return "unreviewed";
86559
+ }
86096
86560
  function allowProtectedRepoDiscovery(config2, found, environment = {}, deps = defaultAllowDeps) {
86097
86561
  const enrollment = deps.load(config2.appEnv);
86098
86562
  if (enrollment === null) {
86099
86563
  throw new ExpectedCliError("This machine must be enrolled before protected folders can be reviewed.");
86100
86564
  }
86101
- const candidateRoots = found.map((entry) => path10.resolve(entry.root));
86565
+ const candidateRoots = rootsOf(found);
86102
86566
  const invalid = candidateRoots.find(
86103
86567
  (root) => macPrivacyProtectedRoot(root, environment) !== root
86104
86568
  );
@@ -86119,53 +86583,92 @@ function allowProtectedRepoDiscovery(config2, found, environment = {}, deps = de
86119
86583
  needsReview: []
86120
86584
  };
86121
86585
  const needsReview = new ProtectedRootTally(environment);
86586
+ const refusedElsewhere = new ProtectedRootTally(environment);
86122
86587
  for (const entry of found) {
86588
+ const root = path10.resolve(entry.root);
86123
86589
  const probes = entry.folders.flatMap(
86124
86590
  (folder) => folder.cwds.map((cwd) => ({ cwd, probe: probeOf(cwd), folder }))
86125
86591
  );
86592
+ let deniedHere = false;
86126
86593
  let unconsented = 0;
86127
86594
  const countedFolderTargets = /* @__PURE__ */ new Set();
86595
+ const countOnce = (target, sessionCount) => {
86596
+ const counted = countedFolderTargets.has(target);
86597
+ countedFolderTargets.add(target);
86598
+ return counted ? 0 : sessionCount;
86599
+ };
86128
86600
  for (const { cwd, probe, folder } of probes) {
86601
+ if (probe.kind === "denied") {
86602
+ const refusedRoot = macPrivacyProtectedRoot(probe.path, environment);
86603
+ if (refusedRoot === root) deniedHere = true;
86604
+ else if (refusedRoot !== null) {
86605
+ const checkout2 = checkoutOfGitDir(probe.path);
86606
+ const target2 = [folder.path, refusedRoot, checkout2].join("\0");
86607
+ refusedElsewhere.add(refusedRoot, checkout2, cwd, countOnce(target2, folder.sessionCount));
86608
+ }
86609
+ continue;
86610
+ }
86129
86611
  if (probe.kind !== "unconsented") continue;
86130
86612
  unconsented += 1;
86131
86613
  const checkout = checkoutOfGitDir(probe.path);
86132
86614
  const target = [folder.path, probe.root, checkout].join("\0");
86133
- needsReview.add(
86134
- probe.root,
86135
- checkout,
86136
- cwd,
86137
- countedFolderTargets.has(target) ? 0 : folder.sessionCount
86138
- );
86139
- countedFolderTargets.add(target);
86615
+ needsReview.add(probe.root, checkout, cwd, countOnce(target, folder.sessionCount));
86140
86616
  }
86141
86617
  if (probes.some(({ probe }) => probe.kind === "found")) outcome.enabled.push(entry);
86142
- else if (probes.some(({ probe }) => probe.kind === "denied")) outcome.blocked.push(entry);
86618
+ else if (deniedHere) outcome.blocked.push(entry);
86143
86619
  else if (unconsented > 0) outcome.unreviewed.push(entry);
86144
86620
  else outcome.unmatched.push(entry);
86145
86621
  }
86622
+ const blockedRoots = new Set(rootsOf(outcome.blocked));
86623
+ outcome.blocked.push(
86624
+ ...refusedElsewhere.entries().filter((entry) => !blockedRoots.has(entry.root))
86625
+ );
86146
86626
  outcome.needsReview = needsReview.entries();
86147
- const consented = [...outcome.enabled, ...outcome.unmatched];
86148
- if (consented.length === 0) return outcome;
86149
86627
  const credentials = deps.load(config2.appEnv);
86150
86628
  if (credentials === null || credentials.token !== enrollment.token || credentials.enrolledAt !== enrollment.enrolledAt) {
86151
86629
  throw new ExpectedCliError(
86152
- "This machine's enrollment changed while the folders were being reviewed, so the approval was not saved, run `powerplant init` again to review them."
86630
+ "This machine's enrollment changed while the folders were being reviewed, so nothing was saved, run `powerplant init` again to review them."
86153
86631
  );
86154
86632
  }
86155
- const allowedProtectedRoots = [
86633
+ const next = credentialsAfterRepoDiscovery(credentials, outcome, deps.now);
86634
+ if (next !== null) deps.persist(config2.appEnv, next);
86635
+ return outcome;
86636
+ }
86637
+ function rootsOf(entries) {
86638
+ return entries.map((entry) => path10.resolve(entry.root));
86639
+ }
86640
+ function credentialsAfterRepoDiscovery(credentials, outcome, now) {
86641
+ const readable = rootsOf([...outcome.enabled, ...outcome.unmatched]);
86642
+ const denied = rootsOf(outcome.blocked).filter((root) => !readable.includes(root));
86643
+ const allowedRoots = [
86156
86644
  .../* @__PURE__ */ new Set([
86157
- ...credentials.repoDiscoveryRoots ?? [],
86158
- ...consented.map((entry) => path10.resolve(entry.root))
86645
+ ...(credentials.repoDiscoveryRoots ?? []).filter((root) => !denied.includes(root)),
86646
+ ...readable
86159
86647
  ])
86160
86648
  ];
86161
- if (allowedProtectedRoots.length !== (credentials.repoDiscoveryRoots ?? []).length) {
86162
- deps.persist(config2.appEnv, { ...credentials, repoDiscoveryRoots: allowedProtectedRoots });
86163
- }
86164
- return outcome;
86649
+ const blockedRoots = [
86650
+ .../* @__PURE__ */ new Set([
86651
+ ...(credentials.repoDiscoveryBlockedRoots ?? []).filter((root) => !readable.includes(root)),
86652
+ ...denied
86653
+ ])
86654
+ ];
86655
+ const unchanged = credentials.repoDiscoveryConsentedAt !== void 0 && sameList(allowedRoots, credentials.repoDiscoveryRoots ?? []) && sameList(blockedRoots, credentials.repoDiscoveryBlockedRoots ?? []);
86656
+ if (unchanged) return null;
86657
+ return {
86658
+ ...credentials,
86659
+ repoDiscoveryConsentedAt: credentials.repoDiscoveryConsentedAt ?? new Date(now()).toISOString(),
86660
+ // Both lists clear on empty: keeping a stale allowed list would read a
86661
+ // root as consent after macOS refused it.
86662
+ repoDiscoveryRoots: allowedRoots.length > 0 ? allowedRoots : void 0,
86663
+ repoDiscoveryBlockedRoots: blockedRoots.length > 0 ? blockedRoots : void 0
86664
+ };
86665
+ }
86666
+ function sameList(a, b2) {
86667
+ return a.length === b2.length && a.every((root, index) => root === b2[index]);
86165
86668
  }
86166
86669
 
86167
86670
  // ../../apps/collector/src/hookInstall.ts
86168
- import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync4 } from "node:fs";
86671
+ import { existsSync as existsSync3, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "node:fs";
86169
86672
  import os12 from "node:os";
86170
86673
  import path11 from "node:path";
86171
86674
  import { fileURLToPath as fileURLToPath2 } from "node:url";
@@ -86233,7 +86736,7 @@ function hookSourcePath(script) {
86233
86736
  }
86234
86737
  function installCollectorHooks(homeDir = os12.homedir()) {
86235
86738
  const hookDir = path11.join(homeDir, ".config", "powerplant", "hooks");
86236
- mkdirSync4(hookDir, { recursive: true });
86739
+ mkdirSync5(hookDir, { recursive: true });
86237
86740
  for (const script of HOOK_SCRIPTS) {
86238
86741
  writeFileSync4(path11.join(hookDir, script), readFileSync6(hookSourcePath(script), "utf8"), {
86239
86742
  mode: 493
@@ -86249,12 +86752,12 @@ function installCollectorHooks(homeDir = os12.homedir()) {
86249
86752
  }
86250
86753
  const merged = mergeCollectorHooks(settingsText, (script) => `node "${path11.join(hookDir, script)}"`);
86251
86754
  if (merged.action === "write") {
86252
- mkdirSync4(settingsDir, { recursive: true });
86755
+ mkdirSync5(settingsDir, { recursive: true });
86253
86756
  writeFileSync4(settingsPath, merged.text);
86254
86757
  }
86255
86758
  if (merged.action !== "skip") {
86256
86759
  for (const retired of RETIRED_HOOK_WIRINGS) {
86257
- rmSync(path11.join(hookDir, retired.script), { force: true });
86760
+ rmSync2(path11.join(hookDir, retired.script), { force: true });
86258
86761
  }
86259
86762
  }
86260
86763
  return {
@@ -86266,7 +86769,7 @@ function installCollectorHooks(homeDir = os12.homedir()) {
86266
86769
 
86267
86770
  // ../../apps/collector/src/service.ts
86268
86771
  import { execFileSync as execFileSync3 } from "node:child_process";
86269
- import { chmodSync as chmodSync2, existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync7, rmSync as rmSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "node:fs";
86772
+ import { chmodSync as chmodSync2, existsSync as existsSync4, mkdirSync as mkdirSync6, readFileSync as readFileSync7, rmSync as rmSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "node:fs";
86270
86773
  import os13 from "node:os";
86271
86774
  import path12 from "node:path";
86272
86775
  var RESTART_THROTTLE_SECONDS = 60;
@@ -86346,11 +86849,11 @@ function installService(config2, command) {
86346
86849
  );
86347
86850
  }
86348
86851
  const launcher = launcherPath(config2.appEnv);
86349
- mkdirSync5(path12.dirname(launcher), { recursive: true });
86852
+ mkdirSync6(path12.dirname(launcher), { recursive: true });
86350
86853
  writeFileSync5(launcher, buildLauncherScript(command));
86351
86854
  chmodSync2(launcher, 493);
86352
86855
  const file2 = plistPath(config2.appEnv);
86353
- mkdirSync5(path12.dirname(file2), { recursive: true });
86856
+ mkdirSync6(path12.dirname(file2), { recursive: true });
86354
86857
  writeFileSync5(file2, buildLaunchdPlist(config2.appEnv, installerPathValue()));
86355
86858
  runLaunchctl(["bootstrap", `gui/${uid()}`, file2]);
86356
86859
  return `${file2} (watching for session changes; log: ${logPath(config2.appEnv)})`;
@@ -86388,7 +86891,7 @@ function serviceUpToDate(config2, command) {
86388
86891
  function uninstallService(config2) {
86389
86892
  requireMacos();
86390
86893
  const existed = bootout(config2.appEnv);
86391
- rmSync2(launcherPath(config2.appEnv), { force: true });
86894
+ rmSync3(launcherPath(config2.appEnv), { force: true });
86392
86895
  if (existsSync4(plistPath(config2.appEnv))) {
86393
86896
  unlinkSync2(plistPath(config2.appEnv));
86394
86897
  return true;
@@ -86458,7 +86961,7 @@ function xmlEscape(value) {
86458
86961
  }
86459
86962
 
86460
86963
  // ../../apps/collector/src/status.ts
86461
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync8, renameSync, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "node:fs";
86964
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync8, renameSync as renameSync2, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "node:fs";
86462
86965
  import os14 from "node:os";
86463
86966
  import path13 from "node:path";
86464
86967
  var HEARTBEAT_INTERVAL_MS = 6e4;
@@ -86493,6 +86996,9 @@ var collectorStatusSchema = external_exports.object({
86493
86996
  /** Last pass that completed without error — everything eligible had been
86494
86997
  * uploaded as of this time. */
86495
86998
  lastSyncAt: external_exports.string().optional(),
86999
+ /** Last pass that upserted at least one session; missing = no session
87000
+ * has uploaded in this daemon's lifetime, not "never". */
87001
+ lastUploadAt: external_exports.string().optional(),
86496
87002
  consecutiveFailures: external_exports.number().int().nonnegative()
86497
87003
  });
86498
87004
  var controlSchema = external_exports.object({
@@ -86509,7 +87015,8 @@ var controlSchema = external_exports.object({
86509
87015
  function interpretControlChange(previous, current) {
86510
87016
  const transitioned = previous.paused !== current.paused;
86511
87017
  const nudge = !current.paused && (transitioned || current.resumedAt !== previous.resumedAt);
86512
- return { transitioned, nudge };
87018
+ const updateOptIn = current.autoUpdate === true && previous.autoUpdate !== true;
87019
+ return { transitioned, nudge, updateOptIn };
86513
87020
  }
86514
87021
  function collectorStateDir(homeDir = os14.homedir()) {
86515
87022
  return path13.join(homeDir, ".config", "powerplant", "state");
@@ -86524,14 +87031,18 @@ function controlFilePath(appEnv, homeDir) {
86524
87031
  return path13.join(collectorStateDir(homeDir), `collector-control-${appEnv}.json`);
86525
87032
  }
86526
87033
  function writeAtomically(file2, payload) {
86527
- mkdirSync6(path13.dirname(file2), { recursive: true });
87034
+ mkdirSync7(path13.dirname(file2), { recursive: true });
86528
87035
  const tmp = `${file2}.${process.pid}.tmp`;
86529
87036
  writeFileSync6(tmp, payload);
86530
- renameSync(tmp, file2);
87037
+ renameSync2(tmp, file2);
86531
87038
  }
86532
87039
  function writeCollectorStatus(appEnv, status, homeDir) {
86533
87040
  writeAtomically(statusFilePath(appEnv, homeDir), JSON.stringify(status, null, 2));
86534
87041
  }
87042
+ function carriedUploadClock(previous, machineId) {
87043
+ if (previous.file !== "ok" || machineId === void 0) return void 0;
87044
+ return previous.status.machineId === machineId ? previous.status.lastUploadAt : void 0;
87045
+ }
86535
87046
  function readCollectorStatus(appEnv, homeDir) {
86536
87047
  let raw;
86537
87048
  try {
@@ -86568,10 +87079,10 @@ function writeCollectorControl(appEnv, control, homeDir) {
86568
87079
  writeAtomically(controlFilePath(appEnv, homeDir), JSON.stringify(control, null, 2));
86569
87080
  }
86570
87081
  function clearCollectorControl(appEnv, homeDir) {
86571
- rmSync3(controlFilePath(appEnv, homeDir), { force: true });
87082
+ rmSync4(controlFilePath(appEnv, homeDir), { force: true });
86572
87083
  }
86573
87084
  function clearCollectorStatus(appEnv, homeDir) {
86574
- rmSync3(statusFilePath(appEnv, homeDir), { force: true });
87085
+ rmSync4(statusFilePath(appEnv, homeDir), { force: true });
86575
87086
  }
86576
87087
  function describeDaemonState(read, probes = {}) {
86577
87088
  if (read.file === "invalid") return { state: "unreadable" };
@@ -86944,7 +87455,10 @@ async function runSyncPassInternal(config2, options, diagnostic) {
86944
87455
  const active = items.filter((s) => s.status === "active").length;
86945
87456
  const tooLarge = (count2, what) => count2 > 0 ? ` \xB7 ${count2} ${what} skipped (over ${MAX_TRANSCRIPT_FILE_BYTES / (1024 * 1024)}MB, too large to read)` : "";
86946
87457
  log3(
86947
- `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, the first sync backfills the ${DEEP_FIRST_SYNC_MAX_SESSIONS} most recently active` : "") + "."
87458
+ `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` : "") + // The measure for POW-79's follow-up: a merged thread re-uploads whole
87459
+ // on every change, so once the largest nears the cap, per-page upload
87460
+ // is due.
87461
+ (codex.mergedThreads > 0 ? ` \xB7 ${codex.mergedThreads} codex thread(s) merged from split rollouts (largest ${(codex.largestMergedBytes / (1024 * 1024)).toFixed(1)}MB of the ${MAX_TRANSCRIPT_FILE_BYTES / (1024 * 1024)}MB cap)` : "") + 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, the first sync backfills the ${DEEP_FIRST_SYNC_MAX_SESSIONS} most recently active` : "") + "."
86948
87462
  );
86949
87463
  for (const failure of claude.unreadable) log3(` claude transcript skipped, ${failure}`);
86950
87464
  for (const failure of codex.unreadable) log3(` codex rollout skipped, ${failure}`);
@@ -87191,10 +87705,10 @@ import path16 from "node:path";
87191
87705
  import {
87192
87706
  existsSync as existsSync5,
87193
87707
  lstatSync as lstatSync3,
87194
- mkdirSync as mkdirSync7,
87708
+ mkdirSync as mkdirSync8,
87195
87709
  readFileSync as readFileSync10,
87196
- renameSync as renameSync2,
87197
- rmSync as rmSync4,
87710
+ renameSync as renameSync3,
87711
+ rmSync as rmSync5,
87198
87712
  writeFileSync as writeFileSync7
87199
87713
  } from "node:fs";
87200
87714
  import path15 from "node:path";
@@ -87275,7 +87789,7 @@ function installProviderHook(provider, home) {
87275
87789
  `${shellQuote2(process.execPath)} ${shellQuote2(scriptPath)}`
87276
87790
  );
87277
87791
  if (merged.action === "skip") return { provider, settings: "skip", detail: merged.reason };
87278
- mkdirSync7(hookDir, { recursive: true });
87792
+ mkdirSync8(hookDir, { recursive: true });
87279
87793
  writeAtomic(scriptPath, readFileSync10(hookSourcePath(script), "utf8"), 448);
87280
87794
  if (merged.action === "write") {
87281
87795
  const current = existsSync5(configPath) ? readFileSync10(configPath, "utf8") : null;
@@ -87293,9 +87807,9 @@ function writeAtomic(file2, text, mode) {
87293
87807
  const temporary = `${file2}.${randomUUID()}.tmp`;
87294
87808
  try {
87295
87809
  writeFileSync7(temporary, text, { mode, flag: "wx" });
87296
- renameSync2(temporary, file2);
87810
+ renameSync3(temporary, file2);
87297
87811
  } finally {
87298
- rmSync4(temporary, { force: true });
87812
+ rmSync5(temporary, { force: true });
87299
87813
  }
87300
87814
  }
87301
87815
  function installProviderHooks(home) {
@@ -87637,6 +88151,10 @@ function startWatch(config2, homeDir, runtime = { version: "checkout" }) {
87637
88151
  const presentRoots = storeRoots.filter(({ dir }) => existsSync6(dir)).map(({ dir }) => dir);
87638
88152
  const initialControl = readCollectorControl(config2.appEnv, home);
87639
88153
  const pausedAtMs = initialControl.paused ? Date.parse(initialControl.pausedAt ?? "") : Number.NaN;
88154
+ const lastUploadAt = carriedUploadClock(
88155
+ readCollectorStatus(config2.appEnv, home),
88156
+ credentials?.machineId
88157
+ );
87640
88158
  let status = {
87641
88159
  pid: process.pid,
87642
88160
  version: runtime.version,
@@ -87652,6 +88170,7 @@ function startWatch(config2, homeDir, runtime = { version: "checkout" }) {
87652
88170
  },
87653
88171
  watchedRoots: presentRoots,
87654
88172
  paused: initialControl.paused,
88173
+ ...lastUploadAt !== void 0 && { lastUploadAt },
87655
88174
  consecutiveFailures: 0
87656
88175
  };
87657
88176
  let statusWriteFailed = false;
@@ -87670,9 +88189,19 @@ function startWatch(config2, homeDir, runtime = { version: "checkout" }) {
87670
88189
  };
87671
88190
  writeStatus({});
87672
88191
  const diagnostics = createDiagnosticsReporter(() => loadCredentials(config2.appEnv));
87673
- diagnostics.heartbeat(initialControl.paused);
87674
- const diagnosticHeartbeat = setInterval(() => diagnostics.heartbeat(status.paused), DIAGNOSTIC_INTERVAL_MS);
87675
- diagnosticHeartbeat.unref?.();
88192
+ const checkIn = () => {
88193
+ let current;
88194
+ try {
88195
+ current = loadCredentials(config2.appEnv);
88196
+ } catch {
88197
+ return;
88198
+ }
88199
+ if (current === null) return;
88200
+ void reportCheckIn(current.apiUrl, current.token, status.paused);
88201
+ };
88202
+ checkIn();
88203
+ const checkInTimer = setInterval(checkIn, CHECK_IN_INTERVAL_MS);
88204
+ checkInTimer.unref?.();
87676
88205
  const heartbeat = setInterval(() => writeStatus({}), HEARTBEAT_INTERVAL_MS);
87677
88206
  heartbeat.unref?.();
87678
88207
  const noticeIfUnsupported = async () => {
@@ -87770,6 +88299,7 @@ function startWatch(config2, homeDir, runtime = { version: "checkout" }) {
87770
88299
  ...result.diagnostic && { diagnostic: result.diagnostic }
87771
88300
  },
87772
88301
  ...result.ok && { lastSyncAt: new Date(result.at).toISOString() },
88302
+ ...result.ok && result.upserted > 0 && { lastUploadAt: new Date(result.at).toISOString() },
87773
88303
  consecutiveFailures: result.ok ? 0 : status.consecutiveFailures + 1
87774
88304
  }),
87775
88305
  reportError: reportCollectorError,
@@ -87801,16 +88331,21 @@ function startWatch(config2, homeDir, runtime = { version: "checkout" }) {
87801
88331
  let lastControl = initialControl;
87802
88332
  const controlPoll = setInterval(() => {
87803
88333
  const control = readCollectorControl(config2.appEnv, home);
87804
- const { transitioned, nudge } = interpretControlChange(lastControl, control);
88334
+ const { transitioned, nudge, updateOptIn } = interpretControlChange(lastControl, control);
87805
88335
  lastControl = control;
87806
88336
  if (transitioned) {
87807
88337
  log3(control.paused ? "syncing paused via control file" : "syncing resumed");
87808
88338
  writeStatus({ paused: control.paused });
88339
+ checkIn();
87809
88340
  }
87810
88341
  if (nudge) {
87811
88342
  log3("running a catch-up pass to cover the pause");
87812
88343
  loop.nudge();
87813
88344
  }
88345
+ if (updateOptIn) {
88346
+ log3("automatic updates turned on, checking for a release now");
88347
+ void checkForUpdate();
88348
+ }
87814
88349
  }, CONTROL_POLL_MS);
87815
88350
  controlPoll.unref?.();
87816
88351
  if (runtime.entryPath) {
@@ -87844,7 +88379,7 @@ import os16 from "node:os";
87844
88379
  import path18 from "node:path";
87845
88380
 
87846
88381
  // ../../apps/collector/src/codexReviewFacts.ts
87847
- import { closeSync as closeSync4, lstatSync as lstatSync4, openSync as openSync4, readSync as readSync4, realpathSync as realpathSync3 } from "node:fs";
88382
+ import { closeSync as closeSync5, lstatSync as lstatSync4, openSync as openSync5, readSync as readSync4, realpathSync as realpathSync3 } from "node:fs";
87848
88383
  import path17 from "node:path";
87849
88384
  var pointerSchema = external_exports.object({
87850
88385
  source: external_exports.unknown().optional(),
@@ -87882,7 +88417,7 @@ function readCodexReviewFacts(row, id, home) {
87882
88417
  }
87883
88418
  }
87884
88419
  function readPrefix(file2) {
87885
- const fd = openSync4(file2, "r");
88420
+ const fd = openSync5(file2, "r");
87886
88421
  const buffer = Buffer.alloc(MAX_PREFIX_BYTES);
87887
88422
  let used = 0;
87888
88423
  let first = -1;
@@ -87903,7 +88438,7 @@ function readPrefix(file2) {
87903
88438
  used = end;
87904
88439
  }
87905
88440
  } finally {
87906
- closeSync4(fd);
88441
+ closeSync5(fd);
87907
88442
  }
87908
88443
  if (first < 0 || second < 0) return null;
87909
88444
  try {
@@ -88283,7 +88818,7 @@ async function probeClaudeDesktopSessions(wanted, installed, reportFault, root =
88283
88818
 
88284
88819
  // src/cli.ts
88285
88820
  import { spawn as spawn2, spawnSync as spawnSync5 } from "node:child_process";
88286
- import { existsSync as existsSync12, mkdirSync as mkdirSync11, openSync as openSync6, realpathSync as realpathSync5, rmSync as rmSync6 } from "node:fs";
88821
+ import { existsSync as existsSync12, mkdirSync as mkdirSync12, openSync as openSync7, realpathSync as realpathSync5, rmSync as rmSync7 } from "node:fs";
88287
88822
  import path28 from "node:path";
88288
88823
  import { parseArgs } from "node:util";
88289
88824
 
@@ -88291,7 +88826,7 @@ import { parseArgs } from "node:util";
88291
88826
  import { execFileSync as execFileSync5, spawnSync } from "node:child_process";
88292
88827
  import {
88293
88828
  existsSync as existsSync9,
88294
- mkdirSync as mkdirSync8,
88829
+ mkdirSync as mkdirSync9,
88295
88830
  mkdtempSync,
88296
88831
  readdirSync as readdirSync5,
88297
88832
  readFileSync as readFileSync11,
@@ -89330,7 +89865,7 @@ function realReadTextFileStrict(filePath) {
89330
89865
  }
89331
89866
  }
89332
89867
  function realWriteTextFile(filePath, content) {
89333
- mkdirSync8(path21.dirname(filePath), { recursive: true });
89868
+ mkdirSync9(path21.dirname(filePath), { recursive: true });
89334
89869
  writeFileSync8(filePath, content);
89335
89870
  }
89336
89871
  function codexConfigPath(homeDir = os19.homedir()) {
@@ -89395,7 +89930,7 @@ function realRunInteractive(bin, args, env) {
89395
89930
 
89396
89931
  // src/connections.ts
89397
89932
  import { execFileSync as execFileSync6, spawnSync as spawnSync2 } from "node:child_process";
89398
- import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "node:fs";
89933
+ import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync9 } from "node:fs";
89399
89934
  import path22 from "node:path";
89400
89935
  function probeConnections(clients, deps) {
89401
89936
  const reports = [];
@@ -89524,7 +90059,7 @@ function detectedClientsPath(homeDir) {
89524
90059
  function saveDetectedClients(clients, pathValue, homeDir) {
89525
90060
  try {
89526
90061
  const file2 = detectedClientsPath(homeDir);
89527
- mkdirSync9(path22.dirname(file2), { recursive: true });
90062
+ mkdirSync10(path22.dirname(file2), { recursive: true });
89528
90063
  const record2 = {
89529
90064
  detectedAt: (/* @__PURE__ */ new Date()).toISOString(),
89530
90065
  path: pathValue,
@@ -89608,7 +90143,7 @@ function whichAllWith(pathValue) {
89608
90143
  }
89609
90144
 
89610
90145
  // src/doctor.ts
89611
- import { closeSync as closeSync5, fstatSync, openSync as openSync5, readSync as readSync5 } from "node:fs";
90146
+ import { closeSync as closeSync6, fstatSync, openSync as openSync6, readSync as readSync5 } from "node:fs";
89612
90147
 
89613
90148
  // src/update.ts
89614
90149
  import path24 from "node:path";
@@ -89633,7 +90168,7 @@ import { accessSync, constants as constants2 } from "node:fs";
89633
90168
  import path23 from "node:path";
89634
90169
 
89635
90170
  // src/version.ts
89636
- var CLI_VERSION = true ? "0.4.6" : "0.0.0-checkout";
90171
+ var CLI_VERSION = true ? "0.4.8" : "0.0.0-checkout";
89637
90172
  var CLI_RELEASE_BUILD = true ? true : false;
89638
90173
  var CLI_PACKAGE_NAME = "@usepowerplant/cli";
89639
90174
 
@@ -89937,7 +90472,7 @@ function redactedLogTail(raw) {
89937
90472
  function readDoctorLogTail(path29) {
89938
90473
  let fd;
89939
90474
  try {
89940
- fd = openSync5(path29, "r");
90475
+ fd = openSync6(path29, "r");
89941
90476
  } catch (error62) {
89942
90477
  if (error62.code === "ENOENT") return { path: path29, lines: null };
89943
90478
  return { path: path29, lines: null, readError: error62 instanceof Error ? error62.message : String(error62) };
@@ -89953,7 +90488,7 @@ function readDoctorLogTail(path29) {
89953
90488
  } catch (error62) {
89954
90489
  return { path: path29, lines: null, readError: error62 instanceof Error ? error62.message : String(error62) };
89955
90490
  } finally {
89956
- closeSync5(fd);
90491
+ closeSync6(fd);
89957
90492
  }
89958
90493
  }
89959
90494
  function renderDoctorReport(i) {
@@ -90205,6 +90740,9 @@ async function confirmResume(deps, log3, indent) {
90205
90740
  }
90206
90741
  }
90207
90742
 
90743
+ // src/protectedFolders.ts
90744
+ var MACOS_BLOCKED_FOLDER_REMEDY = "To allow it, open System Settings > Privacy & Security > Files and Folders, enable the folder for node, then run `powerplant init`.";
90745
+
90208
90746
  // src/init.ts
90209
90747
  function syncNote(credentials) {
90210
90748
  const org = credentials.orgName ?? "your organization";
@@ -90249,7 +90787,7 @@ async function runInit(deps) {
90249
90787
  }
90250
90788
  log3("");
90251
90789
  const orgName = credentials.orgName;
90252
- let repoDiscoveryEnabled = false;
90790
+ let repoDiscoveryDidWork = false;
90253
90791
  let protectedFoldersNeedAttention = false;
90254
90792
  let installed = false;
90255
90793
  let menubarDidWork = false;
@@ -90262,56 +90800,68 @@ async function runInit(deps) {
90262
90800
  "Checking recent session locations\u2026",
90263
90801
  deps.findProtectedSessions
90264
90802
  );
90265
- const alreadyAllowed = new Set(credentials.repoDiscoveryRoots ?? []);
90266
- const needsReview = protectedSessions.filter((entry) => !alreadyAllowed.has(entry.root));
90267
- if (needsReview.length > 0) {
90268
- protectedFoldersNeedAttention = true;
90269
- log3(" Coding agents recorded recent sessions in folders macOS is currently hiding from Powerplant:");
90270
- logProtectedFolders(log3, needsReview);
90271
- log3(" Powerplant will scan only Git metadata in those recorded session folders.");
90272
- if (deps.promptAllowed) {
90273
- const allowed = await confirmProtectedRepoDiscovery(deps);
90274
- if (allowed) {
90275
- log3(
90276
- ' macOS may label Powerplant as "node" because Powerplant runs on Node.js.'
90277
- );
90803
+ const reviewOf = (entry) => protectedRootReview(credentials, entry.root);
90804
+ const blocked = protectedSessions.filter((entry) => reviewOf(entry) === "blocked");
90805
+ const fresh = protectedSessions.filter((entry) => reviewOf(entry) === "unreviewed");
90806
+ if (blocked.length > 0 || fresh.length > 0) {
90807
+ const org = orgName ?? "your organization";
90808
+ const consented = credentials.repoDiscoveryConsentedAt !== void 0;
90809
+ const toProbe = [];
90810
+ let needsAttention = false;
90811
+ if (fresh.length > 0) {
90812
+ log3(" Coding agents recorded recent sessions in folders macOS is currently hiding from Powerplant:");
90813
+ logProtectedFolders(log3, fresh);
90814
+ if (!deps.promptAllowed) {
90815
+ needsAttention = true;
90816
+ logProtectedSessionsStayLocal(log3, fresh);
90817
+ log3(` Run \`powerplant init\` in a terminal to ${consented ? "scan" : "review"} them.`);
90818
+ } else {
90278
90819
  log3(
90279
- ` If permission is added, Powerplant will only read Git metadata in these folders to check for ${orgName ?? "your organization's"} sessions.`
90820
+ consented ? ` You already allowed Git metadata scans. Checking those folders for ${org}'s work repositories.` : ` If you allow it, Powerplant reads only Git metadata there, to find ${org}'s work repositories.`
90280
90821
  );
90281
- const result = await deps.activity(
90822
+ if (consented || await confirmProtectedRepoDiscovery(deps)) toProbe.push(...fresh);
90823
+ else {
90824
+ needsAttention = true;
90825
+ logProtectedSessionsStayLocal(log3, fresh);
90826
+ }
90827
+ }
90828
+ }
90829
+ if (blocked.length > 0) {
90830
+ if (deps.promptAllowed) {
90831
+ log3(` Checking whether macOS still blocks ${displayRoots(blocked)}.`);
90832
+ toProbe.push(...blocked);
90833
+ } else {
90834
+ needsAttention = true;
90835
+ logBlockedByMacOS(log3, blocked);
90836
+ }
90837
+ }
90838
+ if (toProbe.length > 0) {
90839
+ log3(' macOS may label Powerplant as "node" because Powerplant runs on Node.js.');
90840
+ const probe = async (roots) => {
90841
+ const result2 = await deps.activity(
90282
90842
  "Scanning recorded session folders for Git repositories\u2026",
90283
- () => deps.allowProtectedRepoDiscovery(needsReview)
90843
+ () => deps.allowProtectedRepoDiscovery(roots)
90284
90844
  );
90285
- let needsAttention = reportProtectedRepoDiscovery(log3, result);
90286
- if (result.enabled.length > 0) repoDiscoveryEnabled = true;
90287
- if (result.needsReview.length > 0) {
90288
- log3(" Some of those sessions use Git checkouts in other folders macOS is hiding from Powerplant:");
90289
- logProtectedFolders(log3, result.needsReview);
90290
- const again = await confirmProtectedRepoDiscovery(deps);
90291
- if (again) {
90292
- const second = await deps.activity(
90293
- "Scanning recorded session folders for Git repositories\u2026",
90294
- () => deps.allowProtectedRepoDiscovery([...result.unreviewed, ...result.needsReview])
90295
- );
90296
- needsAttention = reportProtectedRepoDiscovery(log3, second) || needsAttention;
90297
- if (second.enabled.length > 0) repoDiscoveryEnabled = true;
90298
- if (second.unreviewed.length > 0) {
90299
- logProtectedSessionsStayLocal(log3, second.unreviewed);
90300
- needsAttention = true;
90301
- }
90302
- } else {
90303
- logProtectedSessionsStayLocal(log3, result.needsReview);
90304
- needsAttention = true;
90305
- }
90845
+ needsAttention = reportProtectedRepoDiscovery(log3, result2) || needsAttention;
90846
+ const unblocked = result2.unmatched.filter((entry) => reviewOf(entry) === "blocked");
90847
+ if (unblocked.length > 0) {
90848
+ log3(` \u2713 macOS now lets Powerplant read ${displayRoots(unblocked)}. No work repositories were found there.`);
90849
+ }
90850
+ if (result2.enabled.length > 0 || unblocked.length > 0) repoDiscoveryDidWork = true;
90851
+ return result2;
90852
+ };
90853
+ const result = await probe(toProbe);
90854
+ if (result.needsReview.length > 0) {
90855
+ log3(" Some of those sessions use Git checkouts in other folders macOS is hiding from Powerplant:");
90856
+ logProtectedFolders(log3, result.needsReview);
90857
+ const second = await probe([...result.unreviewed, ...result.needsReview]);
90858
+ if (second.unreviewed.length > 0) {
90859
+ logProtectedSessionsStayLocal(log3, second.unreviewed);
90860
+ needsAttention = true;
90306
90861
  }
90307
- protectedFoldersNeedAttention = needsAttention;
90308
- } else {
90309
- logProtectedSessionsStayLocal(log3, needsReview);
90310
90862
  }
90311
- } else {
90312
- logProtectedSessionsStayLocal(log3, needsReview);
90313
- log3(" Run `powerplant init` in a terminal to review them.");
90314
90863
  }
90864
+ protectedFoldersNeedAttention = needsAttention;
90315
90865
  }
90316
90866
  }
90317
90867
  if (deps.platform !== "darwin") {
@@ -90351,7 +90901,7 @@ async function runInit(deps) {
90351
90901
  log3("");
90352
90902
  const connectDidWork = outcome !== null && outcome.connected + outcome.configured > outcome.unchanged;
90353
90903
  const connectSettled = outcome !== null && outcome.found > 0 && outcome.pending === 0 && outcome.failed === 0 && outcome.skipped === 0 && outcome.unchanged === outcome.found && !connectDidWork;
90354
- if (!enrolled && !installed && !menubarDidWork && !repoDiscoveryEnabled && !protectedFoldersNeedAttention && pauseOffer === "not-paused" && connectSettled) {
90904
+ if (!enrolled && !installed && !menubarDidWork && !repoDiscoveryDidWork && !protectedFoldersNeedAttention && pauseOffer === "not-paused" && connectSettled) {
90355
90905
  log3("Everything is already set up.");
90356
90906
  return;
90357
90907
  }
@@ -90428,24 +90978,22 @@ function logProtectedFolders(log3, roots) {
90428
90978
  }
90429
90979
  function reportProtectedRepoDiscovery(log3, result) {
90430
90980
  if (result.enabled.length > 0) {
90431
- log3(
90432
- ` \u2713 Repository scanning enabled for ${result.enabled.map((entry) => entry.displayRoot).join(", ")}`
90433
- );
90434
- }
90435
- if (result.blocked.length > 0) {
90436
- log3(
90437
- ` ! macOS blocked Powerplant from reading ${result.blocked.map((entry) => entry.displayRoot).join(", ")}.`
90438
- );
90439
- log3(
90440
- " Sessions there will stay local. To allow it, open System Settings > Privacy & Security > Files and Folders, enable the folder for node, then rerun `powerplant init`."
90441
- );
90981
+ log3(` \u2713 Repository scanning enabled for ${displayRoots(result.enabled)}`);
90442
90982
  }
90983
+ if (result.blocked.length > 0) logBlockedByMacOS(log3, result.blocked);
90443
90984
  return result.blocked.length > 0;
90444
90985
  }
90986
+ function logBlockedByMacOS(log3, roots) {
90987
+ log3(` ! macOS blocked Powerplant from reading ${displayRoots(roots)}.`);
90988
+ log3(` Sessions there will stay local. ${MACOS_BLOCKED_FOLDER_REMEDY}`);
90989
+ }
90990
+ function displayRoots(roots) {
90991
+ return roots.map((entry) => entry.displayRoot).join(", ");
90992
+ }
90445
90993
  function logProtectedSessionsStayLocal(log3, found) {
90446
90994
  const total = found.reduce((sum2, entry) => sum2 + entry.sessionCount, 0);
90447
90995
  log3(
90448
- ` ! Repository matching for ${total} recent session${total === 1 ? "" : "s"} under ${found.map((entry) => entry.displayRoot).join(", ")} was not enabled.`
90996
+ ` ! Repository matching for ${total} recent session${total === 1 ? "" : "s"} under ${displayRoots(found)} was not enabled.`
90449
90997
  );
90450
90998
  log3(" Sessions outside folders you explicitly added will stay local.");
90451
90999
  }
@@ -90489,6 +91037,8 @@ function renderMenu(i) {
90489
91037
  }
90490
91038
  if (items.length > 0) items.push({ type: "separator" });
90491
91039
  const progress = updateProgressView(i.update.progress, i.cliVersion, i.now);
91040
+ let updateStart;
91041
+ let updateCount = 0;
90492
91042
  const unsupported = isUnsupportedVersion(i.cliVersion, i.oldestSupportedVersion);
90493
91043
  if (unsupported) {
90494
91044
  items.push(
@@ -90497,7 +91047,9 @@ function renderMenu(i) {
90497
91047
  `You have ${i.cliVersion}; the oldest supported is ${i.oldestSupportedVersion}. Update now.`
90498
91048
  )
90499
91049
  );
91050
+ updateStart ??= items.length;
90500
91051
  items.push(...updateItems(i, progress, envArgs, problem));
91052
+ updateCount = items.length - updateStart;
90501
91053
  items.push({ type: "separator" });
90502
91054
  }
90503
91055
  if (i.credentialsError !== void 0) {
@@ -90530,6 +91082,7 @@ function renderMenu(i) {
90530
91082
  }
90531
91083
  if (!unsupported && (isUpdateAvailable(i.cliVersion, i.latestVersion) || progress.kind !== "none")) {
90532
91084
  items.push({ type: "separator" });
91085
+ updateStart = items.length;
90533
91086
  if (progress.kind === "none") {
90534
91087
  items.push({
90535
91088
  type: "status",
@@ -90539,6 +91092,7 @@ function renderMenu(i) {
90539
91092
  });
90540
91093
  }
90541
91094
  items.push(...updateItems(i, progress, envArgs, problem));
91095
+ updateCount = items.length - updateStart;
90542
91096
  }
90543
91097
  items.push({ type: "separator" });
90544
91098
  items.push({ type: "action", title: "Open Powerplant", url: i.config.webUrl });
@@ -90548,6 +91102,17 @@ function renderMenu(i) {
90548
91102
  const icon = attention ? "attention" : i.control.paused ? "paused" : "running";
90549
91103
  return {
90550
91104
  contract: MENU_CONTRACT,
91105
+ ...i.update.inPlace && updateStart !== void 0 && {
91106
+ cliUpdate: {
91107
+ startIndex: updateStart,
91108
+ itemCount: updateCount,
91109
+ state: progress.kind === "none" ? "available" : progress.kind,
91110
+ target: progress.kind === "none" ? i.latestVersion : progress.target,
91111
+ ...progress.kind === "failed" && { detail: progress.detail },
91112
+ run: ["update", "--background", ...envArgs],
91113
+ logPath: updateLogPath()
91114
+ }
91115
+ },
90551
91116
  ...i.desktopDiagnostic && { desktopDiagnostic: i.desktopDiagnostic },
90552
91117
  appEnv: i.config.appEnv,
90553
91118
  cliVersion: i.cliVersion,
@@ -91077,8 +91642,9 @@ function renderStatus(i) {
91077
91642
  );
91078
91643
  lines.push(`Syncing: ${describeStoredCapture(i.credentials)}.`);
91079
91644
  for (const entry of i.protectedSessions ?? []) {
91645
+ const sessions = `${entry.sessionCount} recent agent session${entry.sessionCount === 1 ? "" : "s"}`;
91080
91646
  lines.push(
91081
- `\u26A0 Repository matching is not enabled for ${entry.sessionCount} recent agent session${entry.sessionCount === 1 ? "" : "s"} under ${entry.displayRoot}. Sessions outside folders you explicitly added are staying local, run \`powerplant init\` to review Git repository scanning.`
91647
+ protectedRootReview(i.credentials, entry.root) === "blocked" ? `\u26A0 macOS is blocking Powerplant from reading ${entry.displayRoot} (${sessions}). Those sessions are staying local. ${MACOS_BLOCKED_FOLDER_REMEDY}` : `\u26A0 Repository matching is not enabled for ${sessions} under ${entry.displayRoot}. Sessions outside folders you explicitly added are staying local, run \`powerplant init\` to review Git repository scanning.`
91082
91648
  );
91083
91649
  }
91084
91650
  }
@@ -91130,9 +91696,17 @@ function renderDaemon(i) {
91130
91696
  ` The daemon is on ${status.version}, this CLI is ${i.cliVersion}, it restarts onto the installed version within minutes, or now via: powerplant restart`
91131
91697
  );
91132
91698
  }
91133
- lines.push(
91134
- status.lastSyncAt !== void 0 ? ` Last successful sync: ${ago(i.now - Date.parse(status.lastSyncAt))}.` : " No successful sync yet in this run."
91699
+ const signals = describeSyncSignals(
91700
+ {
91701
+ lastSessionActivityAt: status.lastUploadAt ?? null,
91702
+ lastCheckedInAt: status.lastPass?.at ?? null,
91703
+ syncPaused: null,
91704
+ lastSyncErrorAt: status.lastPass && !status.lastPass.ok ? status.lastPass.at : null
91705
+ },
91706
+ new Date(i.now),
91707
+ "last upload unmeasured"
91135
91708
  );
91709
+ lines.push(signals === null ? " No pass has run yet in this run." : ` ${signals}`);
91136
91710
  if (status.lastPass && !status.lastPass.ok) {
91137
91711
  lines.push(
91138
91712
  ` \u26A0 Last pass failed: ${status.lastPass.error ?? "unknown error"} (${status.consecutiveFailures} consecutive failure${status.consecutiveFailures === 1 ? "" : "s"}).`
@@ -91159,7 +91733,10 @@ function statusJson(i) {
91159
91733
  },
91160
91734
  protectedRepoDiscoveryNeeded: (i.protectedSessions ?? []).map((entry) => ({
91161
91735
  displayRoot: entry.displayRoot,
91162
- sessionCount: entry.sessionCount
91736
+ sessionCount: entry.sessionCount,
91737
+ // The member said yes and macOS refused: System Settings, not init,
91738
+ // is the fix, so a tool rendering this must not say "review".
91739
+ blockedByMacOS: i.credentials !== null && protectedRootReview(i.credentials, entry.root) === "blocked"
91163
91740
  })),
91164
91741
  ...i.credentialsError !== void 0 && { credentialsError: i.credentialsError },
91165
91742
  service: i.service,
@@ -91315,7 +91892,7 @@ function createTerminalOutput(options) {
91315
91892
  }
91316
91893
 
91317
91894
  // src/menubarApp.ts
91318
- import { existsSync as existsSync11, mkdirSync as mkdirSync10, renameSync as renameSync3, rmSync as rmSync5 } from "node:fs";
91895
+ import { existsSync as existsSync11, mkdirSync as mkdirSync11, renameSync as renameSync4, rmSync as rmSync6 } from "node:fs";
91319
91896
  import os21 from "node:os";
91320
91897
  import path27 from "node:path";
91321
91898
  import { spawnSync as spawnSync4 } from "node:child_process";
@@ -91383,11 +91960,11 @@ function realMenubarAppOps(input2) {
91383
91960
  },
91384
91961
  replace: (zip, appPath) => {
91385
91962
  const parent = path27.dirname(appPath);
91386
- mkdirSync10(parent, { recursive: true });
91963
+ mkdirSync11(parent, { recursive: true });
91387
91964
  const staging = path27.join(parent, `.${MENUBAR_APP_NAME}.swap-${process.pid}`);
91388
91965
  const retired = path27.join(parent, `.${MENUBAR_APP_NAME}.old-${process.pid}`);
91389
- rmSync5(staging, { recursive: true, force: true });
91390
- rmSync5(retired, { recursive: true, force: true });
91966
+ rmSync6(staging, { recursive: true, force: true });
91967
+ rmSync6(retired, { recursive: true, force: true });
91391
91968
  try {
91392
91969
  const extract = spawnSync4("ditto", ["-xk", zip, staging]);
91393
91970
  if (extract.status !== 0) {
@@ -91400,16 +91977,16 @@ function realMenubarAppOps(input2) {
91400
91977
  throw new Error(`${zip} does not contain ${MENUBAR_APP_NAME}`);
91401
91978
  }
91402
91979
  const hadOld = existsSync11(appPath);
91403
- if (hadOld) renameSync3(appPath, retired);
91980
+ if (hadOld) renameSync4(appPath, retired);
91404
91981
  try {
91405
- renameSync3(extracted, appPath);
91982
+ renameSync4(extracted, appPath);
91406
91983
  } catch (error62) {
91407
- if (hadOld) renameSync3(retired, appPath);
91984
+ if (hadOld) renameSync4(retired, appPath);
91408
91985
  throw error62;
91409
91986
  }
91410
91987
  } finally {
91411
- rmSync5(staging, { recursive: true, force: true });
91412
- rmSync5(retired, { recursive: true, force: true });
91988
+ rmSync6(staging, { recursive: true, force: true });
91989
+ rmSync6(retired, { recursive: true, force: true });
91413
91990
  }
91414
91991
  },
91415
91992
  launch: (appPath) => {
@@ -91754,7 +92331,11 @@ ${USAGE}`);
91754
92331
  // Enrollment's own narration (org, token location, capture boundary)
91755
92332
  // indents under stage 1.
91756
92333
  enroll: () => enroll2((line) => terminal.log(` ${line}`)),
91757
- findProtectedSessions: () => findRecentProtectedSessions(),
92334
+ // Discovery may read a worktree pointer under a root the store
92335
+ // already allows, so a revoked checkout root still surfaces.
92336
+ findProtectedSessions: () => findRecentProtectedSessions({
92337
+ environment: { allowedProtectedRoots: loadCredentials(config2.appEnv)?.repoDiscoveryRoots ?? [] }
92338
+ }),
91758
92339
  promptAllowed: pickerOn,
91759
92340
  promptLine: realPromptLine,
91760
92341
  allowProtectedRepoDiscovery: (found) => allowProtectedRepoDiscovery(config2, found),
@@ -91828,12 +92409,14 @@ The access you granted these agents stays on your Powerplant account until you r
91828
92409
  }
91829
92410
  case "status": {
91830
92411
  const stored = loadCredentialsSafely(config2);
91831
- const allowedProtectedRoots = new Set(stored.credentials?.repoDiscoveryRoots ?? []);
92412
+ const credentials = stored.credentials;
91832
92413
  const [latestVersion, oldestSupportedVersion, protectedSessions] = await Promise.all([
91833
- fetchAvailableVersion(config2, stored.credentials),
91834
- oldestSupportedFor(stored.credentials),
91835
- process.platform === "darwin" && stored.credentials ? findRecentProtectedSessions().then(
91836
- (found) => found.filter((entry) => !allowedProtectedRoots.has(entry.root))
92414
+ fetchAvailableVersion(config2, credentials),
92415
+ oldestSupportedFor(credentials),
92416
+ process.platform === "darwin" && credentials ? findRecentProtectedSessions({
92417
+ environment: { allowedProtectedRoots: credentials.repoDiscoveryRoots ?? [] }
92418
+ }).then(
92419
+ (found) => found.filter((entry) => protectedRootReview(credentials, entry.root) !== "allowed")
91837
92420
  ) : []
91838
92421
  ]);
91839
92422
  const inputs = {
@@ -91976,7 +92559,7 @@ The access you granted these agents stays on your Powerplant account until you r
91976
92559
  const on = parseAutomaticFlag(values.automatic);
91977
92560
  setAutomaticUpdates(config2.appEnv, on);
91978
92561
  console.log(
91979
- on ? "Automatic updates on: the background sync installs each new release within an hour of publication." : "Automatic updates off: powerplant status and the menu bar still say when a release is available."
92562
+ on ? "Automatic updates on: the background sync checks for a release now and installs each new one within an hour of publication." : "Automatic updates off: powerplant status and the menu bar still say when a release is available."
91980
92563
  );
91981
92564
  return;
91982
92565
  }
@@ -92139,8 +92722,8 @@ function startBackgroundUpdate(config2, target) {
92139
92722
  readProgress: updateProgress.read,
92140
92723
  writeProgress: updateProgress.write,
92141
92724
  spawnChild: () => {
92142
- mkdirSync11(path28.dirname(updateLogPath()), { recursive: true });
92143
- const log3 = openSync6(updateLogPath(), "a");
92725
+ mkdirSync12(path28.dirname(updateLogPath()), { recursive: true });
92726
+ const log3 = openSync7(updateLogPath(), "a");
92144
92727
  const child = spawn2(process.execPath, [entryPath(), "update", ...envArgsFor(config2)], {
92145
92728
  detached: true,
92146
92729
  stdio: ["ignore", log3, log3],
@@ -92202,7 +92785,7 @@ function restartSyncOnNewCode(config2) {
92202
92785
  var updateProgress = {
92203
92786
  read: () => readUpdateProgress(realReadTextFile),
92204
92787
  write: (record2) => realWriteTextFile(updateProgressPath(), JSON.stringify(record2, null, 2)),
92205
- clear: () => rmSync6(updateProgressPath(), { force: true })
92788
+ clear: () => rmSync7(updateProgressPath(), { force: true })
92206
92789
  };
92207
92790
  function envArgsFor(config2) {
92208
92791
  return config2.appEnv === "production" ? [] : ["--env", config2.appEnv];