agent-lookout 0.2.6 → 0.2.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.
@@ -217,6 +217,9 @@ var MAX_HOST_LENGTH = 253;
217
217
  function isWebhookHost(host) {
218
218
  return host.length <= MAX_HOST_LENGTH && WEBHOOK_HOST.test(host);
219
219
  }
220
+ var PHONE_CHANNELS = ["ntfy", "pushover"];
221
+ var PHONE_TEST_PATH = "/api/phone/test";
222
+ var PHONE_TEST_ACTION = "phone-test";
220
223
 
221
224
  // src/core/history.ts
222
225
  var DEFAULT_HISTORY_WINDOW_MS = 15 * 60 * 1e3;
@@ -254,10 +257,30 @@ function createQuietHold() {
254
257
  const last = lastWaitOf(session.id);
255
258
  if (last?.begunAt === begunAt) {
256
259
  last.endedAt = null;
260
+ last.reminderOnly = false;
257
261
  return;
258
262
  }
259
263
  if (last && last.endedAt === null) last.endedAt = begunAt;
260
- waits.push({ session: withoutWaitingText(session), begunAt, endedAt: null });
264
+ waits.push({
265
+ session: withoutWaitingText(session),
266
+ begunAt,
267
+ endedAt: null,
268
+ reminderOnly: false
269
+ });
270
+ },
271
+ holdReminder(session, begunAt) {
272
+ const last = lastWaitOf(session.id);
273
+ if (last?.begunAt === begunAt) {
274
+ last.endedAt = null;
275
+ return;
276
+ }
277
+ if (last && last.endedAt === null) last.endedAt = begunAt;
278
+ waits.push({
279
+ session: withoutWaitingText(session),
280
+ begunAt,
281
+ endedAt: null,
282
+ reminderOnly: true
283
+ });
261
284
  },
262
285
  holdOver(event, session, at) {
263
286
  over.push({ event, session: withoutWaitingText(session), at });
@@ -279,6 +302,7 @@ function createQuietHold() {
279
302
  }
280
303
  const open2 = [];
281
304
  for (const [id, wait] of stillOpen) {
305
+ if (wait.reminderOnly) continue;
282
306
  open2.push({ session: listed.get(id), begunAt: wait.begunAt });
283
307
  }
284
308
  const answered = /* @__PURE__ */ new Map();
@@ -336,6 +360,7 @@ var DAY_MS = 24 * HOUR_MS;
336
360
  var WEEKDAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
337
361
  var TIME_RULE_NAMES = ["longWait", "idle", "quietHours"];
338
362
  var LONG_WAIT_MINUTES = { min: 1, max: 1440, default: 10 };
363
+ var REPEAT_MINUTES = { min: 5, max: 1440, default: 30 };
339
364
  var IDLE_HOURS = { min: 1, max: 720, default: 48 };
340
365
  var DEFAULT_TIME_RULES = {
341
366
  longWait: { on: false, minutes: LONG_WAIT_MINUTES.default },
@@ -365,12 +390,22 @@ function holds(value, keys, exact2) {
365
390
  function wholeNumberIn(value, range) {
366
391
  return Number.isSafeInteger(value) && value >= range.min && value <= range.max;
367
392
  }
368
- function readLongWait(value, exact2) {
393
+ function readRepeat(value, exact2) {
369
394
  if (!isRecord(value) || !holds(value, ["on", "minutes"], exact2)) return null;
370
395
  const { on, minutes } = value;
371
- if (typeof on !== "boolean" || !wholeNumberIn(minutes, LONG_WAIT_MINUTES)) return null;
396
+ if (typeof on !== "boolean" || !wholeNumberIn(minutes, REPEAT_MINUTES)) return null;
372
397
  return { on, minutes };
373
398
  }
399
+ function readLongWait(value, exact2) {
400
+ if (!isRecord(value)) return null;
401
+ const keys = value.repeat === void 0 ? ["on", "minutes"] : ["on", "minutes", "repeat"];
402
+ if (!holds(value, keys, exact2)) return null;
403
+ const { on, minutes } = value;
404
+ if (typeof on !== "boolean" || !wholeNumberIn(minutes, LONG_WAIT_MINUTES)) return null;
405
+ if (value.repeat === void 0) return { on, minutes };
406
+ const repeat = readRepeat(value.repeat, exact2);
407
+ return repeat === null ? null : { on, minutes, repeat };
408
+ }
374
409
  function readIdle(value, exact2) {
375
410
  if (!isRecord(value) || !holds(value, ["on", "hours"], exact2)) return null;
376
411
  const { on, hours } = value;
@@ -402,7 +437,7 @@ var READERS = {
402
437
  quietHours: readQuietHours
403
438
  };
404
439
  var SHAPES = {
405
- longWait: `longWait must be {"on", "minutes"}, with minutes a whole number from ${LONG_WAIT_MINUTES.min} to ${LONG_WAIT_MINUTES.max}.`,
440
+ longWait: `longWait must be {"on", "minutes"}, with minutes a whole number from ${LONG_WAIT_MINUTES.min} to ${LONG_WAIT_MINUTES.max}, and may have "repeat", {"on", "minutes"} with minutes a whole number from ${REPEAT_MINUTES.min} to ${REPEAT_MINUTES.max}.`,
406
441
  idle: `idle must be {"on", "hours"}, with hours a whole number from ${IDLE_HOURS.min} to ${IDLE_HOURS.max}.`,
407
442
  quietHours: 'quietHours must be {"on", "from", "to", "days", "leaveOutAnswered"}, with from and to two different times written as 22:00 is, and days any of mon, tue, wed, thu, fri, sat and sun, each once.'
408
443
  };
@@ -467,6 +502,18 @@ function quietOf(snapshot) {
467
502
  }
468
503
 
469
504
  // src/core/time-rules/reminders.ts
505
+ var MINUTE_MS = 6e4;
506
+ function reminderSchedule(rule) {
507
+ return {
508
+ thresholdMs: rule.minutes * MINUTE_MS,
509
+ everyMs: rule.repeat?.on ? rule.repeat.minutes * MINUTE_MS : null
510
+ };
511
+ }
512
+ function reminderNumber(waitedMs, schedule) {
513
+ if (waitedMs < schedule.thresholdMs) return -1;
514
+ if (schedule.everyMs === null) return 0;
515
+ return Math.floor((waitedMs - schedule.thresholdMs) / schedule.everyMs);
516
+ }
470
517
  function readable(snapshot) {
471
518
  return new Set(
472
519
  snapshot.sources.filter((source) => source.state === "ok" || source.state === "not-set-up").map((source) => source.id)
@@ -485,29 +532,33 @@ function createReminderWatch() {
485
532
  if (known && (since === null || since === known.begunAt)) continue;
486
533
  watched.set(session.id, {
487
534
  begunAt: since ?? at,
488
- firstSeenAfterMs: since === null ? 0 : at - since,
489
- reminded: /* @__PURE__ */ new Set()
535
+ seenAfterMs: since === null ? 0 : at - since,
536
+ toldAfterMs: null
490
537
  });
491
538
  }
492
539
  },
493
- due(snapshot, at, thresholdMs, eligible = () => true) {
540
+ due(snapshot, at, schedule, eligible = () => true) {
494
541
  const found = [];
495
542
  for (const session of snapshot.sessions) {
496
543
  if (!needsYou(session)) continue;
497
544
  const wait = watched.get(session.id);
498
- if (!wait || wait.reminded.has(thresholdMs) || !eligible(session.id)) continue;
545
+ if (!wait || !eligible(session.id)) continue;
499
546
  const waitedMs = at - wait.begunAt;
500
- if (waitedMs < thresholdMs || wait.firstSeenAfterMs >= thresholdMs) continue;
501
- found.push({ session, begunAt: wait.begunAt, waitedMs });
547
+ const repeat = reminderNumber(waitedMs, schedule);
548
+ if (repeat < 0 || repeat <= reminderNumber(wait.seenAfterMs, schedule)) continue;
549
+ const told = wait.toldAfterMs;
550
+ if (told !== null && repeat <= reminderNumber(told, schedule)) continue;
551
+ const { thresholdMs, everyMs } = schedule;
552
+ if (told !== null && everyMs !== null && repeat > 0) {
553
+ if (thresholdMs + repeat * everyMs - told < everyMs / 2) continue;
554
+ }
555
+ found.push({ session, begunAt: wait.begunAt, waitedMs, repeat });
502
556
  }
503
557
  return found;
504
558
  },
505
- reminded(sessionId, thresholdMs) {
506
- watched.get(sessionId)?.reminded.add(thresholdMs);
507
- },
508
- toldLate(sessionId, at, thresholdMs) {
559
+ told(sessionId, at) {
509
560
  const wait = watched.get(sessionId);
510
- if (wait && at - wait.begunAt >= thresholdMs) wait.reminded.add(thresholdMs);
561
+ if (wait) wait.toldAfterMs = Math.max(wait.toldAfterMs ?? 0, at - wait.begunAt);
511
562
  },
512
563
  clear() {
513
564
  watched.clear();
@@ -535,7 +586,7 @@ function waitBegan(statusSince, seenAt) {
535
586
 
536
587
  // src/collector/outbound/outboundChannel.ts
537
588
  function createOutboundChannel(options) {
538
- const { afterMs, asking, waitMessage, overMessage, reminderMessage, summaryMessage, failure: failure2 } = options;
589
+ const { afterMs, asking, waitMessage: waitMessage2, overMessage: overMessage2, reminderMessage: reminderMessage2, summaryMessage: summaryMessage2, failure: failure2 } = options;
539
590
  const now = options.now ?? Date.now;
540
591
  const wanted = new Set(options.events);
541
592
  let memory = EMPTY_CHANGE_MEMORY;
@@ -547,15 +598,26 @@ function createOutboundChannel(options) {
547
598
  const hold = createQuietHold();
548
599
  let last = null;
549
600
  let queue = Promise.resolve();
550
- function send2(content) {
551
- queue = queue.then(() => options.send(content)).then(
552
- (outcome) => {
553
- last = outcome.sent ? { at: now(), sent: true } : { at: now(), sent: false, reason: outcome.reason };
554
- },
555
- () => {
556
- last = { at: now(), sent: false, reason: failure2 };
557
- }
601
+ function tryInTurn(content, after = () => {
602
+ }) {
603
+ const tried = queue.then(() => options.send(content)).then(
604
+ (outcome) => outcome,
605
+ // A sender should not reject. One that does is a failure like any other.
606
+ () => ({ sent: false, reason: failure2 })
607
+ ).then((outcome) => {
608
+ after(outcome);
609
+ return outcome;
610
+ });
611
+ queue = tried.then(
612
+ () => void 0,
613
+ () => void 0
558
614
  );
615
+ return tried;
616
+ }
617
+ function send2(content) {
618
+ void tryInTurn(content, (outcome) => {
619
+ last = outcome.sent ? { at: now(), sent: true } : { at: now(), sent: false, reason: outcome.reason };
620
+ });
559
621
  }
560
622
  return {
561
623
  handle(snapshot) {
@@ -578,7 +640,7 @@ function createOutboundChannel(options) {
578
640
  ...item,
579
641
  agent: agentName(item.session, snapshot.sources)
580
642
  }));
581
- ready.push((sentNow) => summaryMessage({ ...summary, items, now: sentNow }));
643
+ ready.push((sentNow) => summaryMessage2({ ...summary, items, now: sentNow }));
582
644
  }
583
645
  for (const { session, begunAt } of end.open) open2.set(session.id, begunAt);
584
646
  }
@@ -596,7 +658,7 @@ function createOutboundChannel(options) {
596
658
  agent: agentName(session, snapshot.sources),
597
659
  seenAt: at
598
660
  };
599
- ready.push((sentNow) => overMessage({ ...facts, now: sentNow }));
661
+ ready.push((sentNow) => overMessage2({ ...facts, now: sentNow }));
600
662
  continue;
601
663
  }
602
664
  const since = session.statusSince;
@@ -609,7 +671,7 @@ function createOutboundChannel(options) {
609
671
  sentAt.push(at);
610
672
  send2(write(at));
611
673
  }
612
- const thresholdMs = rules.longWait.minutes * 6e4;
674
+ const schedule = reminderSchedule(rules.longWait);
613
675
  for (const [id, begunAt] of open2) {
614
676
  const session = snapshot.sessions.find(
615
677
  (candidate) => candidate.id === id && needsYou(candidate)
@@ -626,7 +688,7 @@ function createOutboundChannel(options) {
626
688
  open2.delete(id);
627
689
  sentAt.push(at);
628
690
  send2(
629
- waitMessage({
691
+ waitMessage2({
630
692
  session: withoutWaitingText(session),
631
693
  agent: agentName(session, snapshot.sources),
632
694
  // Read now, from this poll's snapshot, and only when it is to go.
@@ -635,21 +697,27 @@ function createOutboundChannel(options) {
635
697
  now: at
636
698
  })
637
699
  );
638
- if (rules.longWait.on) reminders.toldLate(id, at, thresholdMs);
700
+ if (rules.longWait.on) reminders.told(id, at);
639
701
  }
640
- if (!quiet && rules.longWait.on && wanted.has("needs-you")) {
641
- for (const due of reminders.due(snapshot, at, thresholdMs, (id) => !open2.has(id))) {
702
+ if (rules.longWait.on && wanted.has("needs-you")) {
703
+ for (const due of reminders.due(snapshot, at, schedule, (id) => !open2.has(id))) {
704
+ if (quiet) {
705
+ hold.holdReminder(due.session, due.begunAt);
706
+ continue;
707
+ }
642
708
  if (limitLiftsAt(sentAt, at) !== null) break;
643
709
  sentAt.push(at);
644
- reminders.reminded(due.session.id, thresholdMs);
710
+ reminders.told(due.session.id, at);
645
711
  send2(
646
- reminderMessage({
712
+ reminderMessage2({
647
713
  session: withoutWaitingText(due.session),
648
714
  agent: agentName(due.session, snapshot.sources),
649
715
  asking: asking ? waitingText(due.session.waitingText) ?? null : null,
650
716
  begunAt: due.begunAt,
651
717
  now: at,
652
- thresholdMs
718
+ thresholdMs: schedule.thresholdMs,
719
+ everyMs: schedule.everyMs,
720
+ repeat: due.repeat
653
721
  })
654
722
  );
655
723
  }
@@ -664,14 +732,22 @@ function createOutboundChannel(options) {
664
732
  },
665
733
  last: () => last,
666
734
  limitedUntil: () => limitLiftsAt(sentAt, now()),
667
- settled: () => queue
735
+ settled: () => queue,
736
+ async test(content) {
737
+ const at = now();
738
+ sentAt = sendsInLastHour(sentAt, at);
739
+ const limitedUntil = limitLiftsAt(sentAt, at);
740
+ if (limitedUntil !== null) return { tried: false, limitedUntil };
741
+ sentAt.push(at);
742
+ return { tried: true, at, outcome: await tryInTurn(content) };
743
+ }
668
744
  };
669
745
  }
670
746
 
671
747
  // src/core/time-rules/timeRulesWords.ts
672
- var MINUTE_MS = 6e4;
748
+ var MINUTE_MS2 = 6e4;
673
749
  function waitedInWords(ms) {
674
- if (ms < MINUTE_MS) return "under a minute";
750
+ if (ms < MINUTE_MS2) return "under a minute";
675
751
  return partsInWords(durationParts(ms).filter((part) => part.unit !== "s"));
676
752
  }
677
753
  var WAITED_FOR = {
@@ -873,9 +949,10 @@ function waitEmail(facts) {
873
949
  }
874
950
  function reminderEmail(facts) {
875
951
  const { session, begunAt, now } = facts;
952
+ const again = facts.everyMs === null ? "" : `, and again every ${waitedInWords(facts.everyMs)} while it goes on`;
876
953
  return emailOf(
877
954
  reminderSentence(session, now - begunAt),
878
- `It has waited since ${clockAt(begunAt, now)}. Agent Lookout reminds you once a wait lasts ${waitedInWords(facts.thresholdMs)}, as Time rules in Settings says.`,
955
+ `It has waited since ${clockAt(begunAt, now)}. Agent Lookout reminds you once a wait lasts ${waitedInWords(facts.thresholdMs)}${again}, as Time rules in Settings says.`,
879
956
  session,
880
957
  facts.agent,
881
958
  facts.asking
@@ -1231,6 +1308,116 @@ function pullRequestsOffStatus(problem) {
1231
1308
  return { on: false, problem, gh: null, last: null };
1232
1309
  }
1233
1310
 
1311
+ // src/collector/outbound/phoneMessage.ts
1312
+ var MAX_PHONE_TITLE = 250;
1313
+ var MAX_PHONE_MESSAGE = 1024;
1314
+ function isWaitKind(kind) {
1315
+ return kind === "needs-you" || kind === "reminder" || kind === "test";
1316
+ }
1317
+ function cutTo(text4, max) {
1318
+ const characters = Array.from(text4);
1319
+ if (characters.length <= max) return text4;
1320
+ return `${characters.slice(0, max - 1).join("").trimEnd()}\u2026`;
1321
+ }
1322
+ function phoneMessage(kind, title, lines) {
1323
+ const said = lines.filter((line) => line !== "");
1324
+ return {
1325
+ kind,
1326
+ title: cutTo(title, MAX_PHONE_TITLE),
1327
+ message: cutTo(said.length > 0 ? said.join("\n") : title, MAX_PHONE_MESSAGE)
1328
+ };
1329
+ }
1330
+ function factsLine(first, session, agent) {
1331
+ const facts = [
1332
+ first,
1333
+ session.project ? oneLine(session.project) : "",
1334
+ surfaceLabel(session.surface) ?? "",
1335
+ agent ? oneLine(agent) : ""
1336
+ ];
1337
+ return facts.filter((fact) => fact !== "").join(" \xB7 ");
1338
+ }
1339
+ function askingLine(asking) {
1340
+ const said = waitingText(asking);
1341
+ return said ? `Asking: ${said}` : "";
1342
+ }
1343
+ function waitMessage(facts) {
1344
+ const { session, begunAt, now } = facts;
1345
+ return phoneMessage("needs-you", `${oneLine(sessionTitle(session))} ${waitingPhrase(session)}`, [
1346
+ askingLine(facts.asking),
1347
+ factsLine(formatDuration(Math.max(0, now - begunAt)), session, facts.agent)
1348
+ ]);
1349
+ }
1350
+ function reminderMessage(facts) {
1351
+ const { session, begunAt, now } = facts;
1352
+ return phoneMessage("reminder", reminderSentence(session, now - begunAt), [
1353
+ askingLine(facts.asking),
1354
+ factsLine(`Since ${clockAt(begunAt, now)}`, session, facts.agent)
1355
+ ]);
1356
+ }
1357
+ function summaryMessage(facts) {
1358
+ return phoneMessage("quiet-summary", WHILE_QUIET, [summaryLine(facts.items)]);
1359
+ }
1360
+ function overMessage(facts) {
1361
+ const { session, seenAt, now } = facts;
1362
+ return phoneMessage(facts.event, `${oneLine(sessionTitle(session))} ${overPhrase(facts.event)}`, [
1363
+ factsLine(`Seen at ${clockAt(seenAt, now)}`, session, facts.agent)
1364
+ ]);
1365
+ }
1366
+ function testMessage() {
1367
+ return phoneMessage("test", "Agent Lookout test", [
1368
+ "Pushes from Agent Lookout reach this device."
1369
+ ]);
1370
+ }
1371
+
1372
+ // src/collector/ntfy/ntfyNotifications.ts
1373
+ function ntfyOffStatus(problem) {
1374
+ return {
1375
+ on: false,
1376
+ host: null,
1377
+ tokenSet: null,
1378
+ events: null,
1379
+ afterMs: null,
1380
+ asking: null,
1381
+ problem,
1382
+ last: null,
1383
+ limitedUntil: null
1384
+ };
1385
+ }
1386
+ function createNtfyNotifications(options) {
1387
+ const { settings, sender } = options;
1388
+ const channel = createOutboundChannel({
1389
+ events: settings.events,
1390
+ afterMs: settings.afterMs,
1391
+ asking: settings.asking,
1392
+ waitMessage,
1393
+ overMessage,
1394
+ reminderMessage,
1395
+ summaryMessage,
1396
+ send: (message) => sender.send(message),
1397
+ failure: "the push could not be sent",
1398
+ now: options.now
1399
+ });
1400
+ return {
1401
+ handle: (snapshot) => channel.handle(snapshot),
1402
+ status() {
1403
+ return {
1404
+ on: true,
1405
+ // The host alone. The topic is the secret anyone could read the pushes with.
1406
+ host: settings.server.hostname,
1407
+ tokenSet: settings.token !== null,
1408
+ events: [...settings.events],
1409
+ afterMs: settings.afterMs,
1410
+ asking: settings.asking,
1411
+ problem: null,
1412
+ last: channel.last(),
1413
+ limitedUntil: channel.limitedUntil()
1414
+ };
1415
+ },
1416
+ test: () => channel.test(testMessage()),
1417
+ settled: () => channel.settled()
1418
+ };
1419
+ }
1420
+
1234
1421
  // src/core/sessions/diff.ts
1235
1422
  function severityFor(to) {
1236
1423
  if (to === "needs-you") return "warning";
@@ -1583,6 +1770,50 @@ function createPoller(options) {
1583
1770
  };
1584
1771
  }
1585
1772
 
1773
+ // src/collector/pushover/pushoverNotifications.ts
1774
+ function pushoverOffStatus(problem) {
1775
+ return {
1776
+ on: false,
1777
+ events: null,
1778
+ afterMs: null,
1779
+ asking: null,
1780
+ problem,
1781
+ last: null,
1782
+ limitedUntil: null
1783
+ };
1784
+ }
1785
+ function createPushoverNotifications(options) {
1786
+ const { settings, sender } = options;
1787
+ const channel = createOutboundChannel({
1788
+ events: settings.events,
1789
+ afterMs: settings.afterMs,
1790
+ asking: settings.asking,
1791
+ waitMessage,
1792
+ overMessage,
1793
+ reminderMessage,
1794
+ summaryMessage,
1795
+ send: (message) => sender.send(message),
1796
+ failure: "the push could not be sent",
1797
+ now: options.now
1798
+ });
1799
+ return {
1800
+ handle: (snapshot) => channel.handle(snapshot),
1801
+ status() {
1802
+ return {
1803
+ on: true,
1804
+ events: [...settings.events],
1805
+ afterMs: settings.afterMs,
1806
+ asking: settings.asking,
1807
+ problem: null,
1808
+ last: channel.last(),
1809
+ limitedUntil: channel.limitedUntil()
1810
+ };
1811
+ },
1812
+ test: () => channel.test(testMessage()),
1813
+ settled: () => channel.settled()
1814
+ };
1815
+ }
1816
+
1586
1817
  // src/collector/webhook/webhookMessage.ts
1587
1818
  var MAX_SUMMARY_ITEMS = 100;
1588
1819
  function slackSafe(text4) {
@@ -1634,7 +1865,9 @@ function reminderPost(facts) {
1634
1865
  post.session.app,
1635
1866
  post.session.agent
1636
1867
  ]),
1637
- reminder: true
1868
+ reminder: true,
1869
+ // Left out on the first, so its post is the post it always was.
1870
+ ...facts.repeat > 0 && { repeat: facts.repeat }
1638
1871
  };
1639
1872
  }
1640
1873
  function summaryPost(facts) {
@@ -1856,6 +2089,7 @@ function createApiHandler(options) {
1856
2089
  if (options.answer) actions.set(ANSWER_PATH, options.answer);
1857
2090
  if (options.timeRules) actions.set(TIME_RULES_PATH, options.timeRules);
1858
2091
  if (options.permissionRules) actions.set(PERMISSION_RULES_PATH, options.permissionRules);
2092
+ if (options.phoneTest) actions.set(PHONE_TEST_PATH, options.phoneTest);
1859
2093
  const serveSnapshot = options.serveSnapshot ?? ((snapshot) => snapshot);
1860
2094
  function route(req, res) {
1861
2095
  const refusal3 = refusalFor(req);
@@ -1918,6 +2152,22 @@ function createApiHandler(options) {
1918
2152
  );
1919
2153
  return;
1920
2154
  }
2155
+ case "/api/ntfy": {
2156
+ send(
2157
+ res,
2158
+ 200,
2159
+ options.ntfy ? options.ntfy() : ntfyOffStatus(null)
2160
+ );
2161
+ return;
2162
+ }
2163
+ case "/api/pushover": {
2164
+ send(
2165
+ res,
2166
+ 200,
2167
+ options.pushover ? options.pushover() : pushoverOffStatus(null)
2168
+ );
2169
+ return;
2170
+ }
1921
2171
  case "/api/pull-requests": {
1922
2172
  send(
1923
2173
  res,
@@ -2201,7 +2451,7 @@ function urlFor(address) {
2201
2451
 
2202
2452
  // src/collector/hosts/standalone.ts
2203
2453
  import { existsSync } from "node:fs";
2204
- import path21 from "node:path";
2454
+ import path25 from "node:path";
2205
2455
 
2206
2456
  // src/core/mapping/claudeCodeMapping.ts
2207
2457
  var LIVE_STATUS = /* @__PURE__ */ new Map([
@@ -3057,6 +3307,9 @@ function lstartMs(value) {
3057
3307
  const [day, hours, minutes, seconds, year] = match.slice(2).map(Number);
3058
3308
  return Date.UTC(year, month, day, hours, minutes, seconds);
3059
3309
  }
3310
+ function processStartTime(value) {
3311
+ return lstartMs(collapse(value)) ?? null;
3312
+ }
3060
3313
  function compareProcessStart(recorded, actual, withinMs = SAME_START_WITHIN_MS) {
3061
3314
  if (recorded === void 0 || actual === void 0) return "unknown";
3062
3315
  const before = collapse(recorded);
@@ -3694,11 +3947,11 @@ async function readTranscriptTail(file, io, known, maxBytes = TRANSCRIPT_TAIL_BY
3694
3947
  const stamp = stampOf(open2.info);
3695
3948
  if (known !== null && sameStamp(known, stamp)) return { unchanged: true, stamp };
3696
3949
  const start = Math.max(0, stamp.size - maxBytes);
3697
- const bytes2 = await open2.read(start, stamp.size - start);
3950
+ const bytes3 = await open2.read(start, stamp.size - start);
3698
3951
  return {
3699
3952
  unchanged: false,
3700
3953
  stamp,
3701
- tail: new TextDecoder("utf-8").decode(bytes2),
3954
+ tail: new TextDecoder("utf-8").decode(bytes3),
3702
3955
  fromStart: start === 0
3703
3956
  };
3704
3957
  } finally {
@@ -4602,9 +4855,21 @@ async function pollAfterAnswer(poller, sessionId) {
4602
4855
  function answerRefusalFor(req) {
4603
4856
  return actionRefusalFor(req, ANSWER_ACTION, MAX_ANSWER_BODY_BYTES);
4604
4857
  }
4605
- function createAnswerRoute(options) {
4858
+ function createAnswerer(options) {
4606
4859
  const { poller, events, asks } = options;
4607
4860
  const now = options.now ?? Date.now;
4861
+ return async (sessionId, requestId, decision) => {
4862
+ const session = poller.getSnapshot().sessions.find((listed) => listed.id === sessionId);
4863
+ if (!session) return "no-ask";
4864
+ const outcome = await asks.answer(sessionId, requestId, decision);
4865
+ if (outcome !== "answered") return outcome;
4866
+ events.add([answeredEvent(session, decision, now())]);
4867
+ await pollAfterAnswer(poller, sessionId);
4868
+ return "answered";
4869
+ };
4870
+ }
4871
+ function createAnswerRoute(options) {
4872
+ const answer = createAnswerer(options);
4608
4873
  return async function answerPermission(req) {
4609
4874
  const refused = answerRefusalFor(req);
4610
4875
  if (refused) {
@@ -4623,12 +4888,8 @@ function createAnswerRoute(options) {
4623
4888
  'The body must be {"sessionId": "...", "requestId": "...", "decision": "allow" or "deny"} and nothing else.'
4624
4889
  );
4625
4890
  }
4626
- const session = poller.getSnapshot().sessions.find((listed) => listed.id === asked.sessionId);
4627
- if (!session) return answerFailed("no-ask");
4628
- const outcome = await asks.answer(asked.sessionId, asked.requestId, asked.decision);
4891
+ const outcome = await answer(asked.sessionId, asked.requestId, asked.decision);
4629
4892
  if (outcome !== "answered") return answerFailed(outcome);
4630
- events.add([answeredEvent(session, asked.decision, now())]);
4631
- await pollAfterAnswer(poller, asked.sessionId);
4632
4893
  return {
4633
4894
  status: 200,
4634
4895
  body: { ok: true, decision: asked.decision }
@@ -5564,18 +5825,18 @@ function namesOwnFiles(words, ownPaths = [], folder) {
5564
5825
  (part) => from !== void 0 && !part.startsWith("/") && !part.startsWith("-") ? [part, `${from}/${part}`] : [part]
5565
5826
  );
5566
5827
  return read.some((part) => {
5567
- const path23 = plainPath(part);
5568
- if (path23 === BY_NUMBER || path23.startsWith(`${BY_NUMBER}/`)) return true;
5569
- if (own.some((name) => path23 === name || path23.startsWith(`${name}/`))) return true;
5570
- if (above.has(path23)) return true;
5571
- return ownNames.some((name) => path23 === name || path23.endsWith(`/${name}`));
5828
+ const path27 = plainPath(part);
5829
+ if (path27 === BY_NUMBER || path27.startsWith(`${BY_NUMBER}/`)) return true;
5830
+ if (own.some((name) => path27 === name || path27.startsWith(`${name}/`))) return true;
5831
+ if (above.has(path27)) return true;
5832
+ return ownNames.some((name) => path27 === name || path27.endsWith(`/${name}`));
5572
5833
  });
5573
5834
  });
5574
5835
  }
5575
5836
  function folderReachesOwnFiles(folder, ownPaths = []) {
5576
5837
  const own = ownPaths.filter((name) => name.startsWith("/")).map(plainPath);
5577
- const path23 = plainPath(folder);
5578
- return own.some((name) => name === path23 || name.startsWith(`${path23 === "/" ? "" : path23}/`));
5838
+ const path27 = plainPath(folder);
5839
+ return own.some((name) => name === path27 || name.startsWith(`${path27 === "/" ? "" : path27}/`));
5579
5840
  }
5580
5841
  var PLAIN_WORD = /^[A-Za-z0-9_\-.,/:=+@%]+$/;
5581
5842
  var ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
@@ -5928,6 +6189,8 @@ function createAnswering(options) {
5928
6189
  observe: () => {
5929
6190
  },
5930
6191
  answeredIn: () => /* @__PURE__ */ new Set(),
6192
+ check: async () => {
6193
+ },
5931
6194
  ruleAnswers: () => []
5932
6195
  };
5933
6196
  }
@@ -5955,6 +6218,7 @@ function createAnswering(options) {
5955
6218
  let problem = null;
5956
6219
  let listening = false;
5957
6220
  const answerRoute = createAnswerRoute({ poller, events, asks, now: options.now });
6221
+ const answer = createAnswerer({ poller, events, asks, now: options.now });
5958
6222
  return {
5959
6223
  async start() {
5960
6224
  const started = await socket.start();
@@ -5987,6 +6251,9 @@ function createAnswering(options) {
5987
6251
  req.resume();
5988
6252
  return answerRefusalFor(req) ?? refusal(405, "This address only answers GET requests.", { Allow: "GET" });
5989
6253
  },
6254
+ // The same answer, from the host's own process. With no socket nothing is held.
6255
+ answer: async (sessionId, requestId, decision) => problem === null ? answer(sessionId, requestId, decision) : "unavailable",
6256
+ check: () => asks.check(),
5990
6257
  ruleAnswers: () => ruleAnswers.recent()
5991
6258
  };
5992
6259
  }
@@ -6214,120 +6481,1482 @@ function waitTotals(input) {
6214
6481
  };
6215
6482
  }
6216
6483
 
6217
- // src/collector/adapters/codex/index.ts
6484
+ // src/collector/adapters/antigravity/index.ts
6218
6485
  import os5 from "node:os";
6219
- import path12 from "node:path";
6486
+ import path14 from "node:path";
6220
6487
 
6221
- // src/core/mapping/codexMapping.ts
6222
- var TURN_UNDER_WAY = /* @__PURE__ */ new Set(["task_started", "turn_started"]);
6223
- var TURN_OVER = /* @__PURE__ */ new Set(["task_complete", "turn_complete", "turn_aborted"]);
6224
- function mapCodexStatus(fields) {
6225
- const { lastTurn, live: live2 } = fields;
6226
- let open2;
6227
- if (lastTurn === null) {
6228
- open2 = "idle";
6229
- } else if (typeof lastTurn !== "string") {
6230
- return "unknown";
6231
- } else if (TURN_UNDER_WAY.has(lastTurn)) {
6232
- open2 = "working";
6233
- } else if (TURN_OVER.has(lastTurn)) {
6234
- open2 = "idle";
6235
- } else {
6236
- return "unknown";
6488
+ // src/core/mapping/antigravityLiveness.ts
6489
+ function holdConversations(processes, conversations, listedAt = null) {
6490
+ const held = /* @__PURE__ */ new Set();
6491
+ let complete = true;
6492
+ for (const { startedAt, conversation } of processes) {
6493
+ if (conversation !== void 0) held.add(conversation);
6494
+ if (startedAt === null) {
6495
+ complete = false;
6496
+ continue;
6497
+ }
6498
+ let wrote = false;
6499
+ for (const { id, writtenAt } of conversations) {
6500
+ if (writtenAt === null || writtenAt < startedAt) continue;
6501
+ held.add(id);
6502
+ wrote = true;
6503
+ }
6504
+ if (!wrote && conversation === void 0) complete = false;
6237
6505
  }
6238
- return live2 === false ? "finished" : open2;
6239
- }
6240
- var WRITER_LOCKS_SINCE = [0, 155, 0];
6241
- var VERSION = /^(\d{1,6})\.(\d{1,6})\.(\d{1,6})(-[0-9A-Za-z.-]{1,64})?(?:\+[0-9A-Za-z.-]{1,64})?$/;
6242
- function keepsWriterLocks(cliVersion) {
6243
- if (typeof cliVersion !== "string") return false;
6244
- const match = VERSION.exec(cliVersion);
6245
- if (!match) return false;
6246
- const parts = [Number(match[1]), Number(match[2]), Number(match[3])];
6247
- for (const [index, since] of WRITER_LOCKS_SINCE.entries()) {
6248
- const part = parts[index];
6249
- if (part !== since) return part > since;
6506
+ if (listedAt !== null) {
6507
+ for (const { id, writtenAt } of conversations) {
6508
+ if (writtenAt !== null && writtenAt > listedAt) held.add(id);
6509
+ }
6250
6510
  }
6251
- return match[4] === void 0;
6511
+ return { held, complete };
6252
6512
  }
6253
- function codexLiveness(fields) {
6254
- if (!fields.lockFolder) return "unknown";
6255
- if (fields.locked) return true;
6256
- return keepsWriterLocks(fields.cliVersion) ? false : "unknown";
6513
+ function antigravityLiveness(id, holding) {
6514
+ if (holding === null) return "unknown";
6515
+ if (holding.held.has(id)) return true;
6516
+ return holding.complete ? false : "unknown";
6257
6517
  }
6258
- var IMPORTED_TURN_ID = /^external-import-turn-\d{1,9}$/;
6259
- function isImportedCodexTurn(turnId) {
6260
- return typeof turnId === "string" && IMPORTED_TURN_ID.test(turnId);
6518
+
6519
+ // src/core/mapping/antigravityLog.ts
6520
+ var GLOG_LINE = /^[IWEF](\d{2})(\d{2}) (\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))? +\d+ ([\w.-]+):\d+\] (.*)$/;
6521
+ var SOURCE2 = {
6522
+ backend: "server.go",
6523
+ created: "server.go",
6524
+ streaming: "conversation_manager.go",
6525
+ confirmation: "tool_confirmation_manager.go"
6526
+ };
6527
+ var UUID = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}";
6528
+ var OPENED = new RegExp(`^(Created|Streaming) conversation (${UUID})$`);
6529
+ var BACKEND = /^Creating CLI server backend: (?:.* )?workspaceDirs=\[([^\]\s]+)\](?: |$)/;
6530
+ var ASKING = /^Surfacing tool confirmation: "([A-Za-z]{1,64})" at step (\d{1,9})$/;
6531
+ var ANSWERED = new RegExp(
6532
+ `^Responding to tool confirmation: convID=(${UUID}), stepIdx=(\\d{1,9}), approved=(?:true|false)$`
6533
+ );
6534
+ var ABSOLUTE = /^(?:\/|[A-Za-z]:[\\/])/;
6535
+ var UNPRINTABLE = /[\p{Cc}\p{Cf}]/u;
6536
+ function readAgyLogLine(line) {
6537
+ const glog = GLOG_LINE.exec(line);
6538
+ if (glog === null) return null;
6539
+ const source = glog[7];
6540
+ const message = glog[8];
6541
+ const opened = OPENED.exec(message);
6542
+ if (opened !== null) {
6543
+ const from = opened[1] === "Created" ? SOURCE2.created : SOURCE2.streaming;
6544
+ return source === from ? { kind: "opened", conversation: opened[2].toLowerCase() } : null;
6545
+ }
6546
+ if (source === SOURCE2.backend) {
6547
+ const backend = BACKEND.exec(message);
6548
+ return backend !== null && ABSOLUTE.test(backend[1]) && !UNPRINTABLE.test(backend[1]) ? { kind: "folder", folder: backend[1] } : null;
6549
+ }
6550
+ if (source !== SOURCE2.confirmation) return null;
6551
+ const asking = ASKING.exec(message);
6552
+ if (asking !== null) {
6553
+ const fraction = (glog[6] ?? "0").padEnd(3, "0").slice(0, 3);
6554
+ return {
6555
+ kind: "asking",
6556
+ tool: asking[1],
6557
+ step: Number(asking[2]),
6558
+ at: {
6559
+ month: Number(glog[1]),
6560
+ day: Number(glog[2]),
6561
+ hour: Number(glog[3]),
6562
+ minute: Number(glog[4]),
6563
+ second: Number(glog[5]),
6564
+ millisecond: Number(fraction)
6565
+ }
6566
+ };
6567
+ }
6568
+ const answered = ANSWERED.exec(message);
6569
+ if (answered !== null) {
6570
+ return { kind: "answered", conversation: answered[1].toLowerCase(), step: Number(answered[2]) };
6571
+ }
6572
+ return null;
6261
6573
  }
6262
- function isRecord8(value) {
6263
- return typeof value === "object" && value !== null && !Array.isArray(value);
6574
+ function emptyAgyLogState() {
6575
+ return { folder: null, folderNamed: false, opened: /* @__PURE__ */ new Set(), current: null, ask: null };
6264
6576
  }
6265
- var CODEX_DESKTOP_ORIGINATOR = "Codex Desktop";
6266
- function mapCodexSurface(source, originator) {
6267
- if (originator === CODEX_DESKTOP_ORIGINATOR) return "desktop";
6268
- if (source === "cli" || source === "exec") return "terminal";
6269
- if (source === "vscode") return "vscode";
6270
- if (isRecord8(source) && source.custom === "chatgpt") return "desktop";
6271
- return "unknown";
6577
+ function addAgyLogLine(state, line) {
6578
+ switch (line.kind) {
6579
+ case "folder":
6580
+ if (!state.folderNamed) state.folder = line.folder;
6581
+ state.folderNamed = true;
6582
+ return;
6583
+ case "opened":
6584
+ state.opened.add(line.conversation);
6585
+ if (state.current !== line.conversation) state.ask = null;
6586
+ state.current = line.conversation;
6587
+ return;
6588
+ case "asking":
6589
+ state.ask = state.current === null ? null : { conversation: state.current, step: line.step, tool: line.tool, at: line.at };
6590
+ return;
6591
+ case "answered":
6592
+ if (state.ask?.conversation === line.conversation && state.ask.step === line.step) {
6593
+ state.ask = null;
6594
+ }
6595
+ return;
6596
+ }
6272
6597
  }
6273
- var NOT_A_SESSION_SOURCE = /* @__PURE__ */ new Set(["subagent", "internal", "mcp"]);
6274
- var NOT_A_SESSION_THREAD = /* @__PURE__ */ new Set(["subagent", "guardian_review", "memory_consolidation"]);
6275
- function isCodexSessionSource(source, meta = {}) {
6276
- if (typeof meta.parentThreadId === "string" && meta.parentThreadId !== "") return false;
6277
- if (typeof meta.threadSource === "string" && NOT_A_SESSION_THREAD.has(meta.threadSource)) {
6278
- return false;
6598
+ var ASK_STEPS_AHEAD = 2;
6599
+ function waitsForApproval(ask, conversation, lastIndex) {
6600
+ if (ask === null || ask.conversation !== conversation || lastIndex === null) return false;
6601
+ return ask.step > lastIndex && ask.step <= lastIndex + ASK_STEPS_AHEAD;
6602
+ }
6603
+ function agyLogTimeAt(time, near) {
6604
+ const year = new Date(near).getFullYear();
6605
+ let best = null;
6606
+ for (const y of [year - 1, year, year + 1]) {
6607
+ const at = new Date(
6608
+ y,
6609
+ time.month - 1,
6610
+ time.day,
6611
+ time.hour,
6612
+ time.minute,
6613
+ time.second,
6614
+ time.millisecond
6615
+ );
6616
+ if (at.getMonth() !== time.month - 1 || at.getDate() !== time.day) continue;
6617
+ const ms = at.getTime();
6618
+ if (best === null || Math.abs(ms - near) < Math.abs(best - near)) best = ms;
6279
6619
  }
6280
- if (typeof source === "string") return !NOT_A_SESSION_SOURCE.has(source);
6281
- if (isRecord8(source)) return !Object.keys(source).some((kind) => NOT_A_SESSION_SOURCE.has(kind));
6282
- return true;
6620
+ return best;
6283
6621
  }
6284
6622
 
6285
- // src/collector/adapters/codex/rolloutFile.ts
6286
- var HEAD_LIMIT_BYTES = 2 * 1024 * 1024;
6287
- var TAIL_LIMIT_BYTES = 8 * 1024 * 1024;
6288
- var FIRST_HEAD_BYTES = 256 * 1024;
6289
- var TAIL_CHUNK_BYTES = 256 * 1024;
6623
+ // src/collector/adapters/antigravity/agyLogs.ts
6624
+ import path11 from "node:path";
6625
+ var LOG_DIR = "log";
6626
+ var LOG_LIMIT_BYTES = 4 * 1024 * 1024;
6627
+ var LINE_LIMIT_BYTES = 64 * 1024;
6628
+ var LOG_NAMED_WITHIN_MS = 1e4;
6629
+ var PS_LATE_MS = 2e3;
6630
+ var RELIST_MS = 1e4;
6631
+ var LOG_NAME = /^cli-(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})\.log$/;
6290
6632
  var NEWLINE = 10;
6291
- var CODEX_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/;
6292
- function parseCodexTime(value) {
6293
- if (typeof value !== "string" || !CODEX_TIME.test(value)) return null;
6294
- const at = Date.parse(value);
6295
- return Number.isSafeInteger(at) ? at : null;
6296
- }
6297
- var LINE_TIME = /^\{"timestamp":"([^"]{1,64})"/;
6298
- var TURN_HINT = /"type":"(?:task|turn)_/;
6299
- var TURN_TYPE = /^(?:task|turn)_/;
6300
- function isRecord9(value) {
6301
- return typeof value === "object" && value !== null && !Array.isArray(value);
6302
- }
6303
- function text3(value) {
6304
- return typeof value === "string" && value !== "" ? value : void 0;
6305
- }
6306
- function label(value) {
6307
- return typeof value === "string" && value !== "" && value.length <= 64 ? value : void 0;
6308
- }
6309
- function compactSource(value) {
6310
- if (typeof value === "string") return text3(value);
6311
- if (!isRecord9(value)) return void 0;
6312
- return Object.fromEntries(
6313
- Object.entries(value).map(([kind, detail]) => [
6314
- kind,
6315
- typeof detail === "string" ? detail : true
6316
- ])
6633
+ function logStartOf(name) {
6634
+ const match = LOG_NAME.exec(name);
6635
+ if (match === null) return null;
6636
+ const [year, month, day, hour, minute, second] = match.slice(1).map(Number);
6637
+ const at = new Date(year, month - 1, day, hour, minute, second);
6638
+ if (at.getMonth() !== month - 1 || at.getDate() !== day || at.getHours() !== hour) return null;
6639
+ return at.getTime();
6640
+ }
6641
+ function matchLogs(processes, logs) {
6642
+ const candidates = (startedAt) => startedAt === null ? [] : [...logs].filter(
6643
+ ([, at]) => at >= startedAt - PS_LATE_MS && at - startedAt <= LOG_NAMED_WITHIN_MS
6317
6644
  );
6645
+ const owners = /* @__PURE__ */ new Map();
6646
+ for (const { startedAt } of processes) {
6647
+ for (const [name] of candidates(startedAt)) owners.set(name, (owners.get(name) ?? 0) + 1);
6648
+ }
6649
+ return processes.map(({ startedAt }) => {
6650
+ const found = candidates(startedAt);
6651
+ if (found.length !== 1) return null;
6652
+ const [name] = found[0];
6653
+ return owners.get(name) === 1 ? name : null;
6654
+ });
6655
+ }
6656
+ function createAgyLogReader(options) {
6657
+ const { io } = options;
6658
+ const now = options.now ?? Date.now;
6659
+ const logDir = path11.join(options.home, LOG_DIR);
6660
+ const cursors = /* @__PURE__ */ new Map();
6661
+ let listed = /* @__PURE__ */ new Map();
6662
+ let listedFor = "";
6663
+ let listedAt = -Infinity;
6664
+ async function list(processes) {
6665
+ const key = processes.map((p) => p.startedAt ?? "?").join(",");
6666
+ const since = now() - listedAt;
6667
+ if (key === listedFor && since >= 0 && since < RELIST_MS) return;
6668
+ listedFor = key;
6669
+ listedAt = now();
6670
+ let names;
6671
+ try {
6672
+ names = await io.readdir(logDir);
6673
+ } catch {
6674
+ listed = /* @__PURE__ */ new Map();
6675
+ return;
6676
+ }
6677
+ const next = /* @__PURE__ */ new Map();
6678
+ for (const name of names) {
6679
+ const at = logStartOf(name);
6680
+ if (at !== null) next.set(name, at);
6681
+ }
6682
+ listed = next;
6683
+ }
6684
+ function addLines(cursor, data) {
6685
+ let buffer = cursor.pending.byteLength > 0 ? Buffer.concat([cursor.pending, data]) : data;
6686
+ if (cursor.skipping) {
6687
+ const end = buffer.indexOf(NEWLINE);
6688
+ if (end === -1) return;
6689
+ buffer = buffer.subarray(end + 1);
6690
+ cursor.skipping = false;
6691
+ }
6692
+ let start = 0;
6693
+ let newline = buffer.indexOf(NEWLINE);
6694
+ while (newline !== -1) {
6695
+ const line = readAgyLogLine(buffer.toString("utf8", start, newline));
6696
+ if (line !== null) addAgyLogLine(cursor.state, line);
6697
+ start = newline + 1;
6698
+ newline = buffer.indexOf(NEWLINE, start);
6699
+ }
6700
+ const rest = buffer.subarray(start);
6701
+ cursor.skipping = rest.byteLength > LINE_LIMIT_BYTES;
6702
+ cursor.pending = cursor.skipping ? Buffer.alloc(0) : Buffer.from(rest);
6703
+ }
6704
+ async function readLog(file, info) {
6705
+ const known = cursors.get(file);
6706
+ if (known && known.ino === info.ino && known.size === info.size && known.mtimeMs === info.mtimeMs) {
6707
+ return known.state;
6708
+ }
6709
+ const handle = await io.openRegular(file);
6710
+ try {
6711
+ const opened = handle.info;
6712
+ const onward = known !== void 0 && known.ino === opened.ino && opened.size >= known.offset;
6713
+ const cursor = onward ? { ...known, state: known.state } : {
6714
+ ino: opened.ino,
6715
+ size: 0,
6716
+ mtimeMs: 0,
6717
+ offset: 0,
6718
+ pending: Buffer.alloc(0),
6719
+ skipping: false,
6720
+ state: emptyAgyLogState()
6721
+ };
6722
+ let from = cursor.offset;
6723
+ if (opened.size - from > LOG_LIMIT_BYTES) {
6724
+ from = opened.size - LOG_LIMIT_BYTES;
6725
+ cursor.state.ask = null;
6726
+ cursor.pending = Buffer.alloc(0);
6727
+ cursor.skipping = true;
6728
+ addLines(cursor, Buffer.from(await handle.read(from, opened.size - from)));
6729
+ } else if (opened.size > from) {
6730
+ addLines(cursor, Buffer.from(await handle.read(from, opened.size - from)));
6731
+ }
6732
+ cursor.offset = opened.size;
6733
+ cursor.size = opened.size;
6734
+ cursor.mtimeMs = opened.mtimeMs;
6735
+ cursor.ino = opened.ino;
6736
+ cursors.set(file, cursor);
6737
+ return cursor.state;
6738
+ } finally {
6739
+ await handle.close();
6740
+ }
6741
+ }
6742
+ async function read(processes) {
6743
+ if (processes.length === 0) {
6744
+ cursors.clear();
6745
+ return [];
6746
+ }
6747
+ await list(processes);
6748
+ const names = matchLogs(processes, listed);
6749
+ const kept = /* @__PURE__ */ new Set();
6750
+ const states = [];
6751
+ for (const name of names) {
6752
+ if (name === null) {
6753
+ states.push(null);
6754
+ continue;
6755
+ }
6756
+ const file = path11.join(logDir, name);
6757
+ kept.add(file);
6758
+ try {
6759
+ const info = await io.lstat(file);
6760
+ states.push(info.kind === "file" ? await readLog(file, info) : null);
6761
+ } catch {
6762
+ states.push(null);
6763
+ }
6764
+ }
6765
+ for (const file of cursors.keys()) {
6766
+ if (!kept.has(file)) cursors.delete(file);
6767
+ }
6768
+ return states;
6769
+ }
6770
+ return {
6771
+ async read(processes) {
6772
+ try {
6773
+ return await read(processes);
6774
+ } catch {
6775
+ return processes.map(() => null);
6776
+ }
6777
+ }
6778
+ };
6779
+ }
6780
+
6781
+ // src/collector/adapters/antigravity/agyProcesses.ts
6782
+ var AGY_TABLE_ARGS = ["-A", "-o", "pid=,ppid=,lstart=,comm="];
6783
+ function agyCommandLineArgs(pids) {
6784
+ return ["-o", "pid=,args=", "-p", pids.join(",")];
6785
+ }
6786
+ var AGY_COMMANDS = /* @__PURE__ */ new Set([
6787
+ "agent",
6788
+ "agents",
6789
+ "changelog",
6790
+ "help",
6791
+ "install",
6792
+ "mcp",
6793
+ "mic-serve",
6794
+ "models",
6795
+ "plugin",
6796
+ "plugins",
6797
+ "remote-control",
6798
+ "update"
6799
+ ]);
6800
+ var NOT_A_SESSION_OPTIONS = /* @__PURE__ */ new Set([
6801
+ "-h",
6802
+ "-help",
6803
+ "--help",
6804
+ "-version",
6805
+ "--version"
6806
+ ]);
6807
+ var UUID2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
6808
+ var TABLE_LINE = /^\s*(\d+)\s+(\d+)\s+([A-Z][a-z]{2}\s+[A-Z][a-z]{2}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}\s+\d{4})\s+(\S.*?)\s*$/;
6809
+ function isAgyProgram(program) {
6810
+ const name = program.split(/[\\/]/).pop()?.toLowerCase();
6811
+ return name === "agy" || name === "agy.exe";
6812
+ }
6813
+ function parseAgyTable(stdout) {
6814
+ const rows = [];
6815
+ for (const line of stdout.split("\n")) {
6816
+ const match = TABLE_LINE.exec(line);
6817
+ if (!match || !isAgyProgram(match[4])) continue;
6818
+ const start = match[3].replace(/\s+/g, " ");
6819
+ rows.push({
6820
+ pid: Number(match[1]),
6821
+ ppid: Number(match[2]),
6822
+ start,
6823
+ startedAt: processStartTime(start)
6824
+ });
6825
+ }
6826
+ return rows;
6827
+ }
6828
+ var PROGRAM_IN_LINE = /(?:^|[\\/])agy(?:\.exe)?(?=\s|$)/i;
6829
+ function readAgyCommandLine(line) {
6830
+ const program = PROGRAM_IN_LINE.exec(line);
6831
+ if (!program) return { session: true };
6832
+ const words = line.slice(program.index + program[0].length).split(/\s+/).filter((word2) => word2 !== "");
6833
+ const first = words[0];
6834
+ if (first !== void 0 && (AGY_COMMANDS.has(first) || NOT_A_SESSION_OPTIONS.has(first))) {
6835
+ return { session: false };
6836
+ }
6837
+ for (const [index, word2] of words.entries()) {
6838
+ let value;
6839
+ if (word2 === "--conversation" || word2 === "-conversation") value = words[index + 1];
6840
+ else if (word2.startsWith("--conversation=")) value = word2.slice("--conversation=".length);
6841
+ else if (word2.startsWith("-conversation=")) value = word2.slice("-conversation=".length);
6842
+ else continue;
6843
+ if (value !== void 0 && UUID2.test(value)) {
6844
+ return { session: true, conversation: value.toLowerCase() };
6845
+ }
6846
+ }
6847
+ return { session: true };
6848
+ }
6849
+ function parseCommandLines(stdout) {
6850
+ const lines = /* @__PURE__ */ new Map();
6851
+ for (const line of stdout.split("\n")) {
6852
+ const match = /^\s*(\d+)\s(.*)$/.exec(line);
6853
+ if (match) lines.set(Number(match[1]), match[2]);
6854
+ }
6855
+ return lines;
6856
+ }
6857
+ function createAgyProcessReader(options = {}) {
6858
+ const run = options.run ?? runPs;
6859
+ const platform = options.platform ?? process.platform;
6860
+ const known = /* @__PURE__ */ new Map();
6861
+ async function read() {
6862
+ if (platform === "win32") return { ok: false };
6863
+ const table = await run(AGY_TABLE_ARGS, { utc: true });
6864
+ if (!table.ok) return { ok: false };
6865
+ const rows = parseAgyTable(table.stdout);
6866
+ const keyOf = (row) => `${row.pid} ${row.start}`;
6867
+ const current = new Set(rows.map(keyOf));
6868
+ for (const key of known.keys()) {
6869
+ if (!current.has(key)) known.delete(key);
6870
+ }
6871
+ const unseen = rows.filter((row) => !known.has(keyOf(row)));
6872
+ const ended = /* @__PURE__ */ new Set();
6873
+ if (unseen.length > 0) {
6874
+ const answer = await run(agyCommandLineArgs(unseen.map((row) => row.pid)));
6875
+ const lines = parseCommandLines(answer.stdout);
6876
+ for (const row of unseen) {
6877
+ const line = lines.get(row.pid);
6878
+ if (line !== void 0) known.set(keyOf(row), readAgyCommandLine(line));
6879
+ else if (answer.ok || lines.size > 0) ended.add(row.pid);
6880
+ }
6881
+ }
6882
+ const running = /* @__PURE__ */ new Map();
6883
+ for (const row of rows) {
6884
+ if (!ended.has(row.pid)) running.set(row.pid, row);
6885
+ }
6886
+ const lineOf2 = (row) => known.get(keyOf(row)) ?? { session: true };
6887
+ const underSession = (row) => {
6888
+ const seen = /* @__PURE__ */ new Set([row.pid]);
6889
+ let parent = running.get(row.ppid);
6890
+ while (parent !== void 0 && !seen.has(parent.pid)) {
6891
+ if (lineOf2(parent).session) return true;
6892
+ seen.add(parent.pid);
6893
+ parent = running.get(parent.ppid);
6894
+ }
6895
+ return false;
6896
+ };
6897
+ const sessions = [];
6898
+ for (const row of running.values()) {
6899
+ const line = lineOf2(row);
6900
+ if (!line.session || underSession(row)) continue;
6901
+ sessions.push(
6902
+ line.conversation === void 0 ? { startedAt: row.startedAt } : { startedAt: row.startedAt, conversation: line.conversation }
6903
+ );
6904
+ }
6905
+ return { ok: true, sessions };
6906
+ }
6907
+ return {
6908
+ async read() {
6909
+ try {
6910
+ return await read();
6911
+ } catch {
6912
+ return { ok: false };
6913
+ }
6914
+ }
6915
+ };
6916
+ }
6917
+
6918
+ // src/collector/adapters/antigravity/conversations.ts
6919
+ import path12 from "node:path";
6920
+ var BRAIN_DIR = "brain";
6921
+ var CONVERSATIONS_DIR = "conversations";
6922
+ var TRANSCRIPT_IN_FOLDER = [".system_generated", "logs", "transcript.jsonl"];
6923
+ var OLD_REFRESH_MS = 3e4;
6924
+ var UUID3 = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
6925
+ var FOLDER_NAME = new RegExp(`^(${UUID3})$`, "i");
6926
+ var DATABASE_NAME = new RegExp(`^(${UUID3})\\.db(?:-wal)?$`, "i");
6927
+ function conversationIdOfFolder(name) {
6928
+ return FOLDER_NAME.exec(name)?.[1]?.toLowerCase() ?? null;
6929
+ }
6930
+ function conversationIdOfDatabase(name) {
6931
+ return DATABASE_NAME.exec(name)?.[1]?.toLowerCase() ?? null;
6932
+ }
6933
+ function createConversationFinder(options) {
6934
+ const { io } = options;
6935
+ const brainDir = path12.join(options.home, BRAIN_DIR);
6936
+ const databaseDir = path12.join(options.home, CONVERSATIONS_DIR);
6937
+ const oldRefreshMs = options.oldRefreshMs ?? OLD_REFRESH_MS;
6938
+ const known = /* @__PURE__ */ new Map();
6939
+ let oldCheckedAt = null;
6940
+ async function lookAt(id) {
6941
+ const transcript = path12.join(brainDir, id, ...TRANSCRIPT_IN_FOLDER);
6942
+ const infos = await Promise.all(
6943
+ [transcript, path12.join(databaseDir, `${id}.db`), path12.join(databaseDir, `${id}.db-wal`)].map(
6944
+ (file) => io.lstat(file).catch(() => null)
6945
+ )
6946
+ );
6947
+ const transcriptInfo = infos[0]?.kind === "file" ? infos[0] : null;
6948
+ const writtenAt = transcriptInfo === null ? null : Math.floor(transcriptInfo.mtimeMs);
6949
+ let lastWriteAt = null;
6950
+ for (const info of infos) {
6951
+ if (info?.kind !== "file") continue;
6952
+ const at = Math.floor(info.mtimeMs);
6953
+ lastWriteAt = lastWriteAt === null ? at : Math.max(lastWriteAt, at);
6954
+ }
6955
+ return { id, transcript, transcriptInfo, writtenAt, lastWriteAt };
6956
+ }
6957
+ return {
6958
+ async list(checkedAt, mayBeOpen) {
6959
+ let folders;
6960
+ try {
6961
+ folders = await io.readdir(brainDir);
6962
+ } catch (error) {
6963
+ return { state: isMissing2(error) ? "missing" : "unreadable" };
6964
+ }
6965
+ const databases = await io.readdir(databaseDir).catch(() => []);
6966
+ const ids = /* @__PURE__ */ new Set();
6967
+ for (const name of folders) {
6968
+ const id = conversationIdOfFolder(name);
6969
+ if (id !== null) ids.add(id);
6970
+ }
6971
+ for (const name of databases) {
6972
+ const id = conversationIdOfDatabase(name);
6973
+ if (id !== null) ids.add(id);
6974
+ }
6975
+ for (const id of known.keys()) {
6976
+ if (!ids.has(id)) known.delete(id);
6977
+ }
6978
+ const since = oldCheckedAt === null ? Infinity : checkedAt - oldCheckedAt;
6979
+ const refreshOld = since < 0 || since >= oldRefreshMs;
6980
+ if (refreshOld) oldCheckedAt = checkedAt;
6981
+ const cutoff = checkedAt - FINISHED_RETENTION_MS;
6982
+ await Promise.all(
6983
+ [...ids].map(async (id) => {
6984
+ const before = known.get(id);
6985
+ const recent = before === void 0 || before.lastWriteAt !== null && before.lastWriteAt >= cutoff || mayBeOpen(id);
6986
+ if (recent || refreshOld) known.set(id, await lookAt(id));
6987
+ })
6988
+ );
6989
+ const conversations = [...known.values()].sort((a, b) => a.id < b.id ? -1 : 1);
6990
+ return { state: "ok", conversations };
6991
+ }
6992
+ };
6993
+ }
6994
+ function writtenRecently(files, now) {
6995
+ return files.writtenAt !== null && isWithinRetention(files.writtenAt, now);
6996
+ }
6997
+
6998
+ // src/collector/adapters/antigravity/annotations.ts
6999
+ import path13 from "node:path";
7000
+ var ANNOTATIONS_DIR = "annotations";
7001
+ var ANNOTATION_LIMIT_BYTES = 4 * 1024;
7002
+ var TITLE_FIELD = /^title[ \t]*:[ \t]*"((?:[^"\\\n]|\\.)*)"[ \t]*$/m;
7003
+ var SIMPLE_ESCAPES = {
7004
+ n: 10,
7005
+ r: 13,
7006
+ t: 9,
7007
+ '"': 34,
7008
+ "'": 39,
7009
+ "\\": 92,
7010
+ "?": 63,
7011
+ a: 7,
7012
+ b: 8,
7013
+ f: 12,
7014
+ v: 11
7015
+ };
7016
+ function unescapeTextFormat(body) {
7017
+ const chars = Array.from(body);
7018
+ const bytes3 = [];
7019
+ for (let i = 0; i < chars.length; i += 1) {
7020
+ const char = chars[i];
7021
+ if (char !== "\\") {
7022
+ bytes3.push(...Buffer.from(char, "utf8"));
7023
+ continue;
7024
+ }
7025
+ const next = chars[i + 1];
7026
+ if (next === void 0) return null;
7027
+ const octal = /^[0-7]{1,3}/.exec(chars.slice(i + 1, i + 4).join(""));
7028
+ if (octal !== null) {
7029
+ const value = parseInt(octal[0], 8);
7030
+ if (value > 255) return null;
7031
+ bytes3.push(value);
7032
+ i += octal[0].length;
7033
+ continue;
7034
+ }
7035
+ if (next === "u" || next === "U") {
7036
+ const digits = next === "u" ? 4 : 8;
7037
+ const hex = chars.slice(i + 2, i + 2 + digits).join("");
7038
+ if (!new RegExp(`^[0-9a-fA-F]{${digits}}$`).test(hex)) return null;
7039
+ const point = parseInt(hex, 16);
7040
+ if (point > 1114111 || point >= 55296 && point <= 57343) return null;
7041
+ bytes3.push(...Buffer.from(String.fromCodePoint(point), "utf8"));
7042
+ i += 1 + digits;
7043
+ continue;
7044
+ }
7045
+ if (next === "x" || next === "X") {
7046
+ const hex = /^[0-9a-fA-F]{1,2}/.exec(chars.slice(i + 2, i + 4).join(""));
7047
+ if (hex === null) return null;
7048
+ bytes3.push(parseInt(hex[0], 16));
7049
+ i += 1 + hex[0].length;
7050
+ continue;
7051
+ }
7052
+ const simple = SIMPLE_ESCAPES[next];
7053
+ if (simple === void 0) return null;
7054
+ bytes3.push(simple);
7055
+ i += 1;
7056
+ }
7057
+ try {
7058
+ return new TextDecoder("utf-8", { fatal: true }).decode(Uint8Array.from(bytes3));
7059
+ } catch {
7060
+ return null;
7061
+ }
7062
+ }
7063
+ function readTitle(content) {
7064
+ const field2 = TITLE_FIELD.exec(content);
7065
+ if (field2 === null) return null;
7066
+ const title = unescapeTextFormat(field2[1]);
7067
+ return title === null ? null : sessionName(title) ?? null;
7068
+ }
7069
+ function createTitleReader(options) {
7070
+ const { io } = options;
7071
+ const dir = path13.join(options.home, ANNOTATIONS_DIR);
7072
+ const cache = /* @__PURE__ */ new Map();
7073
+ async function read(id) {
7074
+ const file = path13.join(dir, `${id}.pbtxt`);
7075
+ const info = await io.lstat(file);
7076
+ const known = cache.get(id);
7077
+ if (known && known.ino === info.ino && known.size === info.size && known.mtimeMs === info.mtimeMs) {
7078
+ return known.title;
7079
+ }
7080
+ let title = null;
7081
+ if (info.kind === "file" && info.size <= ANNOTATION_LIMIT_BYTES) {
7082
+ const handle = await io.openRegular(file);
7083
+ try {
7084
+ if (handle.info.size <= ANNOTATION_LIMIT_BYTES) {
7085
+ const data = await handle.read(0, handle.info.size);
7086
+ title = readTitle(Buffer.from(data).toString("utf8"));
7087
+ }
7088
+ } finally {
7089
+ await handle.close();
7090
+ }
7091
+ }
7092
+ cache.set(id, { ino: info.ino, size: info.size, mtimeMs: info.mtimeMs, title });
7093
+ return title;
7094
+ }
7095
+ return {
7096
+ async read(id) {
7097
+ try {
7098
+ return await read(id);
7099
+ } catch {
7100
+ cache.delete(id);
7101
+ return null;
7102
+ }
7103
+ },
7104
+ keepOnly(ids) {
7105
+ for (const id of cache.keys()) {
7106
+ if (!ids.has(id)) cache.delete(id);
7107
+ }
7108
+ }
7109
+ };
7110
+ }
7111
+
7112
+ // src/core/mapping/antigravityMapping.ts
7113
+ var ANTIGRAVITY_STEP_TYPES = [
7114
+ "UNSPECIFIED",
7115
+ "DUMMY",
7116
+ "FINISH",
7117
+ "PLAN_INPUT",
7118
+ "MQUERY",
7119
+ "CODE_ACTION",
7120
+ "GIT_COMMIT",
7121
+ "GREP_SEARCH",
7122
+ "VIEW_FILE",
7123
+ "LIST_DIRECTORY",
7124
+ "COMPILE",
7125
+ "VIEW_CODE_ITEM",
7126
+ "USER_INPUT",
7127
+ "PLANNER_RESPONSE",
7128
+ "ERROR_MESSAGE",
7129
+ "RUN_COMMAND",
7130
+ "CHECKPOINT",
7131
+ "PROPOSE_CODE",
7132
+ "FIND",
7133
+ "SUGGESTED_RESPONSES",
7134
+ "COMMAND_STATUS",
7135
+ "MEMORY",
7136
+ "READ_URL_CONTENT",
7137
+ "VIEW_CONTENT_CHUNK",
7138
+ "SEARCH_WEB",
7139
+ "RETRIEVE_MEMORY",
7140
+ "MCP_TOOL",
7141
+ "MANAGER_FEEDBACK",
7142
+ "TOOL_CALL_PROPOSAL",
7143
+ "TOOL_CALL_CHOICE",
7144
+ "TRAJECTORY_CHOICE",
7145
+ "CLIPBOARD",
7146
+ "VIEW_FILE_OUTLINE",
7147
+ "POST_PR_REVIEW",
7148
+ "LIST_RESOURCES",
7149
+ "READ_RESOURCE",
7150
+ "LINT_DIFF",
7151
+ "FIND_ALL_REFERENCES",
7152
+ "BRAIN_UPDATE",
7153
+ "OPEN_BROWSER_URL",
7154
+ "RUN_EXTENSION_CODE",
7155
+ "PROPOSAL_FEEDBACK",
7156
+ "TRAJECTORY_SEARCH",
7157
+ "EXECUTE_BROWSER_JAVASCRIPT",
7158
+ "LIST_BROWSER_PAGES",
7159
+ "CAPTURE_BROWSER_SCREENSHOT",
7160
+ "CLICK_BROWSER_PIXEL",
7161
+ "READ_TERMINAL",
7162
+ "CAPTURE_BROWSER_CONSOLE_LOGS",
7163
+ "READ_BROWSER_PAGE",
7164
+ "BROWSER_GET_DOM",
7165
+ "CODE_SEARCH",
7166
+ "BROWSER_INPUT",
7167
+ "BROWSER_MOVE_MOUSE",
7168
+ "BROWSER_SELECT_OPTION",
7169
+ "BROWSER_SCROLL_UP",
7170
+ "BROWSER_SCROLL_DOWN",
7171
+ "BROWSER_CLICK_ELEMENT",
7172
+ "BROWSER_PRESS_KEY",
7173
+ "TASK_BOUNDARY",
7174
+ "NOTIFY_USER",
7175
+ "CODE_ACKNOWLEDGEMENT",
7176
+ "INTERNAL_SEARCH",
7177
+ "BROWSER_SUBAGENT",
7178
+ "FILE_CHANGE",
7179
+ "MOVE",
7180
+ "BROWSER_SCROLL",
7181
+ "KNOWLEDGE_GENERATION",
7182
+ "EPHEMERAL_MESSAGE",
7183
+ "GENERATE_IMAGE",
7184
+ "DELETE_DIRECTORY",
7185
+ "COMPILE_APPLET",
7186
+ "INSTALL_APPLET_DEPENDENCIES",
7187
+ "INSTALL_APPLET_PACKAGE",
7188
+ "BROWSER_RESIZE_WINDOW",
7189
+ "BROWSER_DRAG_PIXEL_TO_PIXEL",
7190
+ "CONVERSATION_HISTORY",
7191
+ "KNOWLEDGE_ARTIFACTS",
7192
+ "SEND_COMMAND_INPUT",
7193
+ "SYSTEM_MESSAGE",
7194
+ "WAIT",
7195
+ "AGENCY_TOOL_CALL",
7196
+ "CIDER_AGENT_DUMMY",
7197
+ "BUILD_CLEANER",
7198
+ "BLAZE_BUILD_TARGETS",
7199
+ "BLAZE_TEST_TARGETS",
7200
+ "SET_UP_FIREBASE",
7201
+ "MOMA",
7202
+ "RESTART_DEV_SERVER",
7203
+ "DEPLOY_FIREBASE",
7204
+ "SHELL_EXEC",
7205
+ "BROWSER_MOUSE_WHEEL",
7206
+ "LINT_APPLET",
7207
+ "KI_INSERTION",
7208
+ "RETRIEVE_CONTENT",
7209
+ "CRITIQUE",
7210
+ "FINDINGS",
7211
+ "BROWSER_MOUSE_UP",
7212
+ "BROWSER_MOUSE_DOWN",
7213
+ "WORKSPACE_API",
7214
+ "BROWSER_LIST_NETWORK_REQUESTS",
7215
+ "BROWSER_GET_NETWORK_REQUEST",
7216
+ "BROWSER_REFRESH_PAGE",
7217
+ "EDIT_NOTEBOOK",
7218
+ "INVOKE_SUBAGENT",
7219
+ "WRITE_BLOB",
7220
+ "READ_NOTEBOOK",
7221
+ "PROPOSE_AI_COMMENTS",
7222
+ "START_CODE_REVIEW",
7223
+ "GENERIC",
7224
+ "SET_UP_CLOUD_SQL",
7225
+ "EXECUTE_NOTEBOOK",
7226
+ "CLOUD_SQL_UPDATE_SCHEMA",
7227
+ "RPC_ACTION",
7228
+ "CLOUD_SQL_EXECUTE_SQL",
7229
+ "ASK_QUESTION",
7230
+ "DIRECTORY_RULES",
7231
+ "TOOL_SEARCH"
7232
+ ];
7233
+ var ANTIGRAVITY_STEP_STATUSES = [
7234
+ "UNSPECIFIED",
7235
+ "PENDING",
7236
+ "RUNNING",
7237
+ "DONE",
7238
+ "INVALID",
7239
+ "CLEARED",
7240
+ "CANCELED",
7241
+ "ERROR",
7242
+ "GENERATING",
7243
+ "WAITING",
7244
+ "QUEUED",
7245
+ "INTERRUPTED"
7246
+ ];
7247
+ var KNOWN_TYPES = new Set(ANTIGRAVITY_STEP_TYPES);
7248
+ var KNOWN_STATUSES = new Set(ANTIGRAVITY_STEP_STATUSES);
7249
+ var TYPE_PREFIX = "CORTEX_STEP_TYPE_";
7250
+ var STATUS_PREFIX = "CORTEX_STEP_STATUS_";
7251
+ var NAME = /^[A-Z][A-Z0-9_]{0,63}$/;
7252
+ function nameOf(value, prefix) {
7253
+ if (typeof value !== "string") return null;
7254
+ const upper = value.trim().toUpperCase();
7255
+ const name = upper.startsWith(prefix) ? upper.slice(prefix.length) : upper;
7256
+ return NAME.test(name) ? name : null;
7257
+ }
7258
+ function stepTypeName(value) {
7259
+ return nameOf(value, TYPE_PREFIX);
7260
+ }
7261
+ function stepStatusName(value) {
7262
+ return nameOf(value, STATUS_PREFIX);
7263
+ }
7264
+ var ASIDE_TYPES = /* @__PURE__ */ new Set([
7265
+ "DUMMY",
7266
+ "CIDER_AGENT_DUMMY",
7267
+ "CHECKPOINT",
7268
+ "EPHEMERAL_MESSAGE",
7269
+ "CONVERSATION_HISTORY",
7270
+ "SYSTEM_MESSAGE",
7271
+ "SUGGESTED_RESPONSES",
7272
+ "KNOWLEDGE_ARTIFACTS",
7273
+ "KNOWLEDGE_GENERATION",
7274
+ "KI_INSERTION",
7275
+ "DIRECTORY_RULES",
7276
+ "BRAIN_UPDATE"
7277
+ ]);
7278
+ var GONE_STATUSES = /* @__PURE__ */ new Set(["CLEARED", "INVALID"]);
7279
+ var UNDER_WAY = /* @__PURE__ */ new Set([
7280
+ "PENDING",
7281
+ "RUNNING",
7282
+ "GENERATING",
7283
+ "QUEUED",
7284
+ "WAITING"
7285
+ ]);
7286
+ var STOPPED = /* @__PURE__ */ new Set(["CANCELED", "INTERRUPTED"]);
7287
+ var TURN_OVER = /* @__PURE__ */ new Set(["FINISH"]);
7288
+ function isAsideStep(step) {
7289
+ return step.type !== null && ASIDE_TYPES.has(step.type) || step.status !== null && GONE_STATUSES.has(step.status);
7290
+ }
7291
+ function openStatusOf(step) {
7292
+ const { type, status } = step;
7293
+ if (type === "ERROR_MESSAGE") return "failed";
7294
+ if (status === null || !KNOWN_STATUSES.has(status)) return "unknown";
7295
+ if (UNDER_WAY.has(status)) return "working";
7296
+ if (STOPPED.has(status)) return "idle";
7297
+ if (type === null || !KNOWN_TYPES.has(type) || type === "UNSPECIFIED") return "unknown";
7298
+ if (status === "ERROR") return type === "PLANNER_RESPONSE" ? "failed" : "working";
7299
+ if (status !== "DONE") return "unknown";
7300
+ if (type === "PLANNER_RESPONSE") return step.toolCalls ? "working" : "idle";
7301
+ if (TURN_OVER.has(type)) return "idle";
7302
+ return "working";
7303
+ }
7304
+ function mapAntigravityStatus(fields) {
7305
+ const { last, live: live2 } = fields;
7306
+ if (last === void 0) return "unknown";
7307
+ const open2 = last === null ? "idle" : openStatusOf(last);
7308
+ if (open2 === "failed" || open2 === "unknown") return open2;
7309
+ if (live2 === false) return "finished";
7310
+ return fields.asking === true ? "needs-you" : open2;
7311
+ }
7312
+
7313
+ // src/collector/adapters/antigravity/toSession.ts
7314
+ var SOURCE_ID2 = "antigravity-cli";
7315
+ var WRITE_ORDER_SLACK_MS2 = 1e3;
7316
+ function antigravitySession(input) {
7317
+ const { conversationId, state, live: live2, now } = input;
7318
+ const id = `${SOURCE_ID2}:${conversationId}`;
7319
+ const waiting = input.waiting ?? null;
7320
+ const status = mapAntigravityStatus({ last: state.last, live: live2, asking: waiting !== null });
7321
+ const startedAt = plausibleTime(state.firstAt, now);
7322
+ let statusSince = null;
7323
+ if (status === "working" || status === "idle" || status === "failed") {
7324
+ statusSince = plausibleTime(state.since, now);
7325
+ } else if (status === "needs-you") {
7326
+ statusSince = plausibleTime(waiting?.since ?? null, now) ?? plausibleTime(state.lastAt, now);
7327
+ } else if (status === "finished") {
7328
+ statusSince = plausibleTime(state.lastAt, now);
7329
+ }
7330
+ if (statusSince !== null && startedAt !== null && statusSince < startedAt - WRITE_ORDER_SLACK_MS2) {
7331
+ statusSince = null;
7332
+ }
7333
+ const cwd = input.folder ?? null;
7334
+ const project = projectOf(cwd);
7335
+ const session = {
7336
+ id,
7337
+ source: SOURCE_ID2,
7338
+ surface: "terminal",
7339
+ name: input.title ?? project ?? sessionName(conversationId) ?? id,
7340
+ cwd,
7341
+ project,
7342
+ status,
7343
+ startedAt,
7344
+ statusSince,
7345
+ links: {},
7346
+ stale: isStale({ status, statusSince }, now)
7347
+ };
7348
+ if (status === "needs-you" && waiting !== null) {
7349
+ session.waitingReason = "permission";
7350
+ session.waitingDetail = waiting.tool;
7351
+ }
7352
+ const writtenAt = plausibleTime(input.writtenAt, now);
7353
+ const beforeStart = writtenAt !== null && startedAt !== null && writtenAt < startedAt - WRITE_ORDER_SLACK_MS2;
7354
+ if (writtenAt !== null && !beforeStart) session.lastWriteAt = writtenAt;
7355
+ return session;
7356
+ }
7357
+
7358
+ // src/collector/adapters/antigravity/transcriptFile.ts
7359
+ import { createHash as createHash2 } from "node:crypto";
7360
+ var HEAD_LIMIT_BYTES = 256 * 1024;
7361
+ var TAIL_LIMIT_BYTES = 2 * 1024 * 1024;
7362
+ var TAIL_CHUNK_BYTES = 64 * 1024;
7363
+ var HEAD_HASH_BYTES = 4 * 1024;
7364
+ var NEWLINE2 = 10;
7365
+ var ISO_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
7366
+ function parseStepTime(value) {
7367
+ if (typeof value !== "string" || !ISO_TIME.test(value)) return null;
7368
+ const at = Date.parse(value);
7369
+ return Number.isSafeInteger(at) ? at : null;
7370
+ }
7371
+ function isRecord8(value) {
7372
+ return typeof value === "object" && value !== null && !Array.isArray(value);
7373
+ }
7374
+ function readStepLine(line) {
7375
+ let value;
7376
+ try {
7377
+ value = JSON.parse(line);
7378
+ } catch {
7379
+ return null;
7380
+ }
7381
+ if (!isRecord8(value)) return null;
7382
+ const type = stepTypeName(value.type);
7383
+ const status = stepStatusName(value.status);
7384
+ if (type === null && status === null) return null;
7385
+ const index = value.step_index;
7386
+ return {
7387
+ index: Number.isSafeInteger(index) && index >= 0 ? index : null,
7388
+ type,
7389
+ status,
7390
+ toolCalls: Array.isArray(value.tool_calls) && value.tool_calls.length > 0,
7391
+ at: parseStepTime(value.created_at)
7392
+ };
7393
+ }
7394
+ function emptyState() {
7395
+ return { firstAt: null, last: void 0, since: null, lastAt: null, lastIndex: null };
7396
+ }
7397
+ function countsForIndex(step) {
7398
+ return step.index !== null && step.status !== "CLEARED" && step.status !== "INVALID";
7399
+ }
7400
+ function addStep(state, step) {
7401
+ if (step.at !== null) state.lastAt = step.at;
7402
+ if (countsForIndex(step)) state.lastIndex = step.index;
7403
+ if (isAsideStep(step)) return;
7404
+ const was = state.last ? openStatusOf(state.last) : null;
7405
+ if (was !== openStatusOf(step)) state.since = step.at;
7406
+ state.last = step;
7407
+ }
7408
+ function bytes(data) {
7409
+ return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
7410
+ }
7411
+ async function readExactly(file, position, length) {
7412
+ const data = bytes(await file.read(position, length));
7413
+ if (data.byteLength !== length) throw new Error("The file changed while it was read.");
7414
+ return data;
7415
+ }
7416
+ async function headHash(file, size) {
7417
+ const head = await readExactly(file, 0, Math.min(HEAD_HASH_BYTES, size));
7418
+ return createHash2("sha256").update(head).digest("hex");
7419
+ }
7420
+ async function readFirstAt(file, size) {
7421
+ const head = await readExactly(file, 0, Math.min(HEAD_LIMIT_BYTES, size));
7422
+ const end = head.indexOf(NEWLINE2);
7423
+ if (end === -1 && head.byteLength < size) return null;
7424
+ const step = readStepLine(head.toString("utf8", 0, end === -1 ? head.byteLength : end));
7425
+ return step?.index === 0 ? step.at : null;
7426
+ }
7427
+ async function scanTail(file, size) {
7428
+ const state = emptyState();
7429
+ let position = size;
7430
+ let budget2 = TAIL_LIMIT_BYTES;
7431
+ let fragment = Buffer.alloc(0);
7432
+ let end = null;
7433
+ let settled = false;
7434
+ const take = (step) => {
7435
+ if (step === null) return false;
7436
+ if (state.lastAt === null && step.at !== null) state.lastAt = step.at;
7437
+ if (state.lastIndex === null && countsForIndex(step)) state.lastIndex = step.index;
7438
+ if (isAsideStep(step)) return false;
7439
+ if (state.last === void 0 || state.last === null) {
7440
+ state.last = step;
7441
+ state.since = step.at;
7442
+ return false;
7443
+ }
7444
+ if (openStatusOf(step) !== openStatusOf(state.last)) return true;
7445
+ if (step.at !== null) state.since = step.at;
7446
+ return false;
7447
+ };
7448
+ while (position > 0 && budget2 > 0 && !settled) {
7449
+ const length = Math.min(TAIL_CHUNK_BYTES, position, budget2);
7450
+ const start = position - length;
7451
+ const chunk = await readExactly(file, start, length);
7452
+ budget2 -= length;
7453
+ position = start;
7454
+ const buffer = fragment.byteLength > 0 ? Buffer.concat([chunk, fragment]) : chunk;
7455
+ let cut2 = buffer.byteLength;
7456
+ if (end === null) {
7457
+ const last = buffer.lastIndexOf(NEWLINE2);
7458
+ if (last === -1) {
7459
+ fragment = buffer;
7460
+ continue;
7461
+ }
7462
+ end = start + last + 1;
7463
+ cut2 = last;
7464
+ if (last + 1 < buffer.byteLength) {
7465
+ settled = take(readStepLine(buffer.toString("utf8", last + 1)));
7466
+ }
7467
+ }
7468
+ let newline = cut2 > 0 ? buffer.lastIndexOf(NEWLINE2, cut2 - 1) : -1;
7469
+ while (newline !== -1 && !settled) {
7470
+ settled = take(readStepLine(buffer.toString("utf8", newline + 1, cut2)));
7471
+ cut2 = newline;
7472
+ newline = cut2 > 0 ? buffer.lastIndexOf(NEWLINE2, cut2 - 1) : -1;
7473
+ }
7474
+ if (settled) break;
7475
+ if (start === 0) {
7476
+ take(readStepLine(buffer.toString("utf8", 0, cut2)));
7477
+ if (state.last === void 0) state.last = null;
7478
+ return { state, end };
7479
+ }
7480
+ fragment = buffer.subarray(0, cut2);
7481
+ }
7482
+ if (!settled && position === 0) {
7483
+ if (end === null) {
7484
+ take(readStepLine(fragment.toString("utf8")));
7485
+ end = 0;
7486
+ }
7487
+ if (state.last === void 0) state.last = null;
7488
+ return { state, end };
7489
+ }
7490
+ if (!settled && state.last) state.since = null;
7491
+ return { state, end };
7492
+ }
7493
+ function stateOf(cached) {
7494
+ return {
7495
+ firstAt: cached.firstAt,
7496
+ last: cached.last,
7497
+ since: cached.since,
7498
+ lastAt: cached.lastAt,
7499
+ lastIndex: cached.lastIndex
7500
+ };
7501
+ }
7502
+ function createTranscriptReader(io) {
7503
+ const cache = /* @__PURE__ */ new Map();
7504
+ async function readFresh(file, earlier) {
7505
+ const handle = await io.openRegular(file);
7506
+ try {
7507
+ const { info } = handle;
7508
+ const firstAt = await readFirstAt(handle, info.size);
7509
+ const tail = await scanTail(handle, info.size);
7510
+ return {
7511
+ ...tail.state,
7512
+ // A compacted file may begin later than the conversation did: the earliest seen stands.
7513
+ firstAt: earlier?.firstAt != null && (firstAt === null || earlier.firstAt < firstAt) ? earlier.firstAt : firstAt,
7514
+ ino: info.ino,
7515
+ size: info.size,
7516
+ mtimeMs: info.mtimeMs,
7517
+ headHash: await headHash(handle, info.size),
7518
+ offset: tail.end
7519
+ };
7520
+ } finally {
7521
+ await handle.close();
7522
+ }
7523
+ }
7524
+ async function readOnward(file, cached, info) {
7525
+ const offset = cached.offset;
7526
+ if (offset === null) return null;
7527
+ if (info.ino !== cached.ino || info.size <= cached.size || info.size < offset) return null;
7528
+ if (info.size - offset > TAIL_LIMIT_BYTES) return null;
7529
+ const handle = await io.openRegular(file);
7530
+ try {
7531
+ const opened = handle.info;
7532
+ if (opened.ino !== cached.ino || opened.size < offset || opened.size < cached.size) {
7533
+ return null;
7534
+ }
7535
+ if (opened.size - offset > TAIL_LIMIT_BYTES) return null;
7536
+ if (await headHash(handle, cached.size) !== cached.headHash) return null;
7537
+ const from = offset > 0 ? offset - 1 : 0;
7538
+ const read = await readExactly(handle, from, opened.size - from);
7539
+ if (offset > 0 && read[0] !== NEWLINE2) return null;
7540
+ const added = offset > 0 ? read.subarray(1) : read;
7541
+ const next = {
7542
+ ...cached,
7543
+ ino: opened.ino,
7544
+ size: opened.size,
7545
+ mtimeMs: opened.mtimeMs,
7546
+ headHash: await headHash(handle, opened.size)
7547
+ };
7548
+ let start = 0;
7549
+ let newline = added.indexOf(NEWLINE2);
7550
+ while (newline !== -1) {
7551
+ const step = readStepLine(added.toString("utf8", start, newline));
7552
+ if (step) addStep(next, step);
7553
+ start = newline + 1;
7554
+ newline = added.indexOf(NEWLINE2, start);
7555
+ }
7556
+ if (start < added.byteLength) {
7557
+ const step = readStepLine(added.toString("utf8", start));
7558
+ if (step) addStep(next, step);
7559
+ }
7560
+ next.offset = offset + start;
7561
+ if (next.firstAt === null && offset === 0) {
7562
+ next.firstAt = await readFirstAt(handle, opened.size);
7563
+ }
7564
+ return next;
7565
+ } finally {
7566
+ await handle.close();
7567
+ }
7568
+ }
7569
+ return {
7570
+ async read(file, info) {
7571
+ const cached = cache.get(file);
7572
+ if (cached && cached.ino === info.ino && cached.size === info.size && cached.mtimeMs === info.mtimeMs) {
7573
+ return stateOf(cached);
7574
+ }
7575
+ const next = cached && await readOnward(file, cached, info) || await readFresh(file, cached);
7576
+ cache.set(file, next);
7577
+ return stateOf(next);
7578
+ },
7579
+ keepOnly(files) {
7580
+ for (const file of cache.keys()) {
7581
+ if (!files.has(file)) cache.delete(file);
7582
+ }
7583
+ }
7584
+ };
7585
+ }
7586
+
7587
+ // src/collector/adapters/antigravity/index.ts
7588
+ var ANTIGRAVITY_HOME_ENV = "AGENT_LOOKOUT_ANTIGRAVITY_HOME";
7589
+ var DEFAULT_HOME = [".gemini", "antigravity-cli"];
7590
+ var RECHECK_MS = 6e4;
7591
+ var PROCESS_CHECK_MS = 1e4;
7592
+ var PROMPT_PROCESS_CHECK_MS = 2e3;
7593
+ var BASIS = "files";
7594
+ var BASIS_WITHOUT_PROCESSES = "files-without-processes";
7595
+ var BASIS_UNMATCHED = "files-unmatched";
7596
+ var LABEL2 = "Antigravity CLI";
7597
+ var NEEDS_YOU_NOTE = "A session needs you while agy asks you to approve a tool, which its own log records. A question it asks you is not known to be recorded, so it shows as working.";
7598
+ var UNCHECKED_NOTE = "This was checked against one agy 1.3.1 conversation, and parts of it, such as /resume, are not yet checked, so a status can be wrong.";
7599
+ var ALWAYS_NOTES = `${NEEDS_YOU_NOTE} ${UNCHECKED_NOTE}`;
7600
+ var UNKNOWN_STEP_NOTE = "Some conversations end with a step Agent Lookout does not know, perhaps from a newer agy, so they show as unknown.";
7601
+ var UNMATCHED_NOTE = "An agy program is running that has not written to a conversation since it started, so none is shown as finished until it does.";
7602
+ var ANTIGRAVITY_CAPABILITIES = {
7603
+ "working-and-idle": { level: "yes" },
7604
+ "needs-you": {
7605
+ level: "partly",
7606
+ reason: "While agy asks you to approve a tool, from its own log. A question it asks you shows as working."
7607
+ },
7608
+ finished: {
7609
+ level: "partly",
7610
+ reason: "Once ps shows no agy program running that could have the conversation open. Not on Windows."
7611
+ },
7612
+ failed: {
7613
+ level: "partly",
7614
+ reason: "Only when agy records an error of its own or a failed reply. A tool that fails shows as working."
7615
+ },
7616
+ names: {
7617
+ level: "partly",
7618
+ reason: "The title agy gives a conversation once it has one, and the folder its program's log names. Until then, its conversation ID."
7619
+ },
7620
+ jump: {
7621
+ level: "no",
7622
+ reason: "The transcripts name no terminal, and agy documents no link that opens a conversation."
7623
+ },
7624
+ "quiet-for": { level: "yes" },
7625
+ stop: {
7626
+ level: "no",
7627
+ reason: "No agy program is tied to one conversation surely enough for Agent Lookout to confirm it and stop it."
7628
+ },
7629
+ answer: {
7630
+ level: "no",
7631
+ reason: "Its approval prompt takes an answer only in the terminal agy runs in."
7632
+ }
7633
+ };
7634
+ function withLoggedConversations(programs, logStates) {
7635
+ return programs.map((program, index) => {
7636
+ const current = logStates[index]?.current ?? null;
7637
+ return current === null ? program : { ...program, conversation: current };
7638
+ });
7639
+ }
7640
+ function folderOf(id, logs) {
7641
+ const open2 = logs.find((log) => log.current === id && log.folder !== null);
7642
+ if (open2) return open2.folder;
7643
+ return logs.find((log) => log.opened.has(id) && log.folder !== null)?.folder ?? null;
7644
+ }
7645
+ function createAntigravityAdapter(options = {}) {
7646
+ const env = options.env ?? process.env;
7647
+ const homeDir = options.homeDir ?? os5.homedir();
7648
+ const now = options.now ?? Date.now;
7649
+ const io = options.io ?? nodeIo2;
7650
+ const platform = options.platform ?? process.platform;
7651
+ const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
7652
+ const recheckMs = options.recheckMs ?? RECHECK_MS;
7653
+ const processCheckMs = options.processCheckMs ?? PROCESS_CHECK_MS;
7654
+ const processes = options.processes ?? createAgyProcessReader({ platform });
7655
+ const override = env[ANTIGRAVITY_HOME_ENV]?.trim() || void 0;
7656
+ const homeFrom = override ? "agent-lookout" : "default";
7657
+ const home = path14.resolve(override ?? path14.join(homeDir, ...DEFAULT_HOME));
7658
+ const brainDir = path14.join(home, BRAIN_DIR);
7659
+ const homeName = tildify(home, homeDir);
7660
+ const brainName = tildify(brainDir, homeDir);
7661
+ const finder = createConversationFinder({ home, io, oldRefreshMs: options.oldRefreshMs });
7662
+ const reader = createTranscriptReader(io);
7663
+ const logs = createAgyLogReader({ home, io, now });
7664
+ const titles = createTitleReader({ home, io });
7665
+ let absentAt = null;
7666
+ let homeSeen = false;
7667
+ let lastProcesses = null;
7668
+ let processesAt = null;
7669
+ let lastHolding = null;
7670
+ const commandRun = platform === "win32" ? "not run on Windows" : `${every(processCheckMs)} at most`;
7671
+ function watching(read) {
7672
+ return [
7673
+ { label: "Conversations folder", value: brainName },
7674
+ { label: "Read", value: read },
7675
+ { label: "Command", value: `ps ${AGY_TABLE_ARGS.join(" ")}` },
7676
+ { label: "Command run", value: commandRun }
7677
+ ];
7678
+ }
7679
+ function result(state, detail, read, checkedAt, basis = BASIS) {
7680
+ const health = {
7681
+ id: SOURCE_ID2,
7682
+ label: LABEL2,
7683
+ state,
7684
+ detail,
7685
+ watching: watching(read),
7686
+ checkedAt
7687
+ };
7688
+ return state === "ok" ? { health, sessions: [], basis } : { health, sessions: [] };
7689
+ }
7690
+ function notFound(checkedAt) {
7691
+ lastProcesses = null;
7692
+ processesAt = null;
7693
+ lastHolding = null;
7694
+ return result(
7695
+ "unavailable",
7696
+ `The Antigravity CLI was not found: there is no ${homeName} folder. Agent Lookout looks again ${every(recheckMs)}.`,
7697
+ "not found",
7698
+ checkedAt
7699
+ );
7700
+ }
7701
+ async function homeIsThere() {
7702
+ try {
7703
+ return (await io.stat(home)).kind === "directory";
7704
+ } catch (error) {
7705
+ return !isMissing2(error);
7706
+ }
7707
+ }
7708
+ async function processesFor(conversations, checkedAt) {
7709
+ const shown = conversations.some(
7710
+ (files) => writtenRecently(files, checkedAt) || lastHolding?.held.has(files.id)
7711
+ );
7712
+ if (!shown) return null;
7713
+ const since = processesAt === null ? Infinity : checkedAt - processesAt;
7714
+ let due = since < 0 || since >= processCheckMs;
7715
+ if (!due && since >= PROMPT_PROCESS_CHECK_MS && lastProcesses?.ok) {
7716
+ const holding = holdConversations(lastProcesses.sessions, conversations);
7717
+ due = conversations.some(
7718
+ (files) => files.writtenAt !== null && files.writtenAt > processesAt && !holding.held.has(files.id)
7719
+ );
7720
+ }
7721
+ if (due) {
7722
+ lastProcesses = await processes.read();
7723
+ processesAt = checkedAt;
7724
+ }
7725
+ return lastProcesses;
7726
+ }
7727
+ async function poll() {
7728
+ const checkedAt = now();
7729
+ if (homeFrom !== "agent-lookout") {
7730
+ if (absentAt !== null) {
7731
+ const since = checkedAt - absentAt;
7732
+ if (since >= 0 && since < recheckMs) return notFound(checkedAt);
7733
+ }
7734
+ if (!homeSeen) {
7735
+ if (!await homeIsThere()) {
7736
+ absentAt = checkedAt;
7737
+ return notFound(checkedAt);
7738
+ }
7739
+ absentAt = null;
7740
+ homeSeen = true;
7741
+ }
7742
+ }
7743
+ const listing = await finder.list(checkedAt, (id) => lastHolding?.held.has(id) ?? false);
7744
+ if (listing.state === "unreadable") {
7745
+ return result(
7746
+ "error",
7747
+ `Antigravity CLI conversations could not be read: the folder ${brainName} could not be listed.`,
7748
+ "cannot be read",
7749
+ checkedAt
7750
+ );
7751
+ }
7752
+ if (listing.state === "missing") {
7753
+ lastHolding = null;
7754
+ if (homeFrom === "agent-lookout") {
7755
+ return result(
7756
+ "ok",
7757
+ `${ANTIGRAVITY_HOME_ENV} is set to ${homeName}, which has no ${BRAIN_DIR} folder, so no Antigravity CLI sessions are listed. ${ALWAYS_NOTES}`,
7758
+ "not found",
7759
+ checkedAt
7760
+ );
7761
+ }
7762
+ homeSeen = false;
7763
+ if (!await homeIsThere()) {
7764
+ absentAt = checkedAt;
7765
+ return notFound(checkedAt);
7766
+ }
7767
+ homeSeen = true;
7768
+ return result(
7769
+ "ok",
7770
+ `The Antigravity CLI has not saved any conversations in ${brainName} yet. ${ALWAYS_NOTES}`,
7771
+ "not found",
7772
+ checkedAt
7773
+ );
7774
+ }
7775
+ const { conversations } = listing;
7776
+ const list = await processesFor(conversations, checkedAt);
7777
+ const logStates = list?.ok ? await logs.read(list.sessions) : [];
7778
+ const programs = list?.ok ? withLoggedConversations(list.sessions, logStates) : [];
7779
+ const holding = list?.ok ? holdConversations(programs, conversations, processesAt) : null;
7780
+ lastHolding = holding;
7781
+ const logged = logStates.filter((state) => state !== null);
7782
+ const read = /* @__PURE__ */ new Set();
7783
+ const sessions = [];
7784
+ let unknownStep = false;
7785
+ for (const files of conversations) {
7786
+ const open2 = holding?.held.has(files.id) ?? false;
7787
+ if (files.transcriptInfo === null || !(open2 || writtenRecently(files, checkedAt))) continue;
7788
+ let state;
7789
+ try {
7790
+ state = await reader.read(files.transcript, files.transcriptInfo);
7791
+ } catch {
7792
+ read.add(files.transcript);
7793
+ continue;
7794
+ }
7795
+ read.add(files.transcript);
7796
+ const ask = logged.find((log) => log.ask?.conversation === files.id)?.ask ?? null;
7797
+ const waiting = ask !== null && waitsForApproval(ask, files.id, state.lastIndex) ? { tool: ask.tool, since: agyLogTimeAt(ask.at, checkedAt) } : null;
7798
+ const session = antigravitySession({
7799
+ conversationId: files.id,
7800
+ state,
7801
+ live: antigravityLiveness(files.id, holding),
7802
+ waiting,
7803
+ title: await titles.read(files.id),
7804
+ folder: folderOf(files.id, logged),
7805
+ writtenAt: files.lastWriteAt,
7806
+ now: checkedAt
7807
+ });
7808
+ if (session.status === "unknown") unknownStep = true;
7809
+ sessions.push(session);
7810
+ }
7811
+ reader.keepOnly(read);
7812
+ titles.keepOnly(new Set(sessions.map((session) => session.id.slice(SOURCE_ID2.length + 1))));
7813
+ let detail = `Sessions are read from the transcripts the Antigravity CLI keeps in ${brainName}. ${ALWAYS_NOTES}`;
7814
+ let basis = BASIS;
7815
+ if (list !== null && !list.ok) {
7816
+ detail += platform === "win32" ? " Windows has no ps to tell which conversations agy has open, so none is shown as finished." : " ps could not be run to tell which conversations agy has open, so none is shown as finished.";
7817
+ basis = BASIS_WITHOUT_PROCESSES;
7818
+ } else if (holding !== null && !holding.complete) {
7819
+ detail += ` ${UNMATCHED_NOTE}`;
7820
+ basis = BASIS_UNMATCHED;
7821
+ }
7822
+ if (unknownStep) detail += ` ${UNKNOWN_STEP_NOTE}`;
7823
+ const answer = result("ok", detail, every(pollIntervalMs), checkedAt, basis);
7824
+ return { ...answer, sessions };
7825
+ }
7826
+ return {
7827
+ id: SOURCE_ID2,
7828
+ label: LABEL2,
7829
+ capabilities: ANTIGRAVITY_CAPABILITIES,
7830
+ lookingIn: `Looking for Antigravity CLI sessions in ${brainName}.`,
7831
+ async poll() {
7832
+ try {
7833
+ return await poll();
7834
+ } catch {
7835
+ return result(
7836
+ "error",
7837
+ "Something unexpected went wrong while reading Antigravity CLI sessions.",
7838
+ every(pollIntervalMs),
7839
+ now()
7840
+ );
7841
+ }
7842
+ }
7843
+ };
7844
+ }
7845
+
7846
+ // src/collector/adapters/codex/index.ts
7847
+ import os6 from "node:os";
7848
+ import path16 from "node:path";
7849
+
7850
+ // src/core/mapping/codexMapping.ts
7851
+ var TURN_UNDER_WAY = /* @__PURE__ */ new Set(["task_started", "turn_started"]);
7852
+ var TURN_OVER2 = /* @__PURE__ */ new Set(["task_complete", "turn_complete", "turn_aborted"]);
7853
+ function mapCodexStatus(fields) {
7854
+ const { lastTurn, live: live2 } = fields;
7855
+ let open2;
7856
+ if (lastTurn === null) {
7857
+ open2 = "idle";
7858
+ } else if (typeof lastTurn !== "string") {
7859
+ return "unknown";
7860
+ } else if (TURN_UNDER_WAY.has(lastTurn)) {
7861
+ open2 = "working";
7862
+ } else if (TURN_OVER2.has(lastTurn)) {
7863
+ open2 = "idle";
7864
+ } else {
7865
+ return "unknown";
7866
+ }
7867
+ return live2 === false ? "finished" : open2;
7868
+ }
7869
+ var WRITER_LOCKS_SINCE = [0, 155, 0];
7870
+ var VERSION = /^(\d{1,6})\.(\d{1,6})\.(\d{1,6})(-[0-9A-Za-z.-]{1,64})?(?:\+[0-9A-Za-z.-]{1,64})?$/;
7871
+ function keepsWriterLocks(cliVersion) {
7872
+ if (typeof cliVersion !== "string") return false;
7873
+ const match = VERSION.exec(cliVersion);
7874
+ if (!match) return false;
7875
+ const parts = [Number(match[1]), Number(match[2]), Number(match[3])];
7876
+ for (const [index, since] of WRITER_LOCKS_SINCE.entries()) {
7877
+ const part = parts[index];
7878
+ if (part !== since) return part > since;
7879
+ }
7880
+ return match[4] === void 0;
7881
+ }
7882
+ function codexLiveness(fields) {
7883
+ if (!fields.lockFolder) return "unknown";
7884
+ if (fields.locked) return true;
7885
+ return keepsWriterLocks(fields.cliVersion) ? false : "unknown";
7886
+ }
7887
+ var IMPORTED_TURN_ID = /^external-import-turn-\d{1,9}$/;
7888
+ function isImportedCodexTurn(turnId) {
7889
+ return typeof turnId === "string" && IMPORTED_TURN_ID.test(turnId);
7890
+ }
7891
+ function isRecord9(value) {
7892
+ return typeof value === "object" && value !== null && !Array.isArray(value);
7893
+ }
7894
+ var CODEX_DESKTOP_ORIGINATOR = "Codex Desktop";
7895
+ function mapCodexSurface(source, originator) {
7896
+ if (originator === CODEX_DESKTOP_ORIGINATOR) return "desktop";
7897
+ if (source === "cli" || source === "exec") return "terminal";
7898
+ if (source === "vscode") return "vscode";
7899
+ if (isRecord9(source) && source.custom === "chatgpt") return "desktop";
7900
+ return "unknown";
7901
+ }
7902
+ var NOT_A_SESSION_SOURCE = /* @__PURE__ */ new Set(["subagent", "internal", "mcp"]);
7903
+ var NOT_A_SESSION_THREAD = /* @__PURE__ */ new Set(["subagent", "guardian_review", "memory_consolidation"]);
7904
+ function isCodexSessionSource(source, meta = {}) {
7905
+ if (typeof meta.parentThreadId === "string" && meta.parentThreadId !== "") return false;
7906
+ if (typeof meta.threadSource === "string" && NOT_A_SESSION_THREAD.has(meta.threadSource)) {
7907
+ return false;
7908
+ }
7909
+ if (typeof source === "string") return !NOT_A_SESSION_SOURCE.has(source);
7910
+ if (isRecord9(source)) return !Object.keys(source).some((kind) => NOT_A_SESSION_SOURCE.has(kind));
7911
+ return true;
7912
+ }
7913
+
7914
+ // src/collector/adapters/codex/rolloutFile.ts
7915
+ var HEAD_LIMIT_BYTES2 = 2 * 1024 * 1024;
7916
+ var TAIL_LIMIT_BYTES2 = 8 * 1024 * 1024;
7917
+ var FIRST_HEAD_BYTES = 256 * 1024;
7918
+ var TAIL_CHUNK_BYTES2 = 256 * 1024;
7919
+ var NEWLINE3 = 10;
7920
+ var CODEX_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/;
7921
+ function parseCodexTime(value) {
7922
+ if (typeof value !== "string" || !CODEX_TIME.test(value)) return null;
7923
+ const at = Date.parse(value);
7924
+ return Number.isSafeInteger(at) ? at : null;
7925
+ }
7926
+ var LINE_TIME = /^\{"timestamp":"([^"]{1,64})"/;
7927
+ var TURN_HINT = /"type":"(?:task|turn)_/;
7928
+ var TURN_TYPE = /^(?:task|turn)_/;
7929
+ function isRecord10(value) {
7930
+ return typeof value === "object" && value !== null && !Array.isArray(value);
7931
+ }
7932
+ function text3(value) {
7933
+ return typeof value === "string" && value !== "" ? value : void 0;
7934
+ }
7935
+ function label(value) {
7936
+ return typeof value === "string" && value !== "" && value.length <= 64 ? value : void 0;
7937
+ }
7938
+ function compactSource(value) {
7939
+ if (typeof value === "string") return text3(value);
7940
+ if (!isRecord10(value)) return void 0;
7941
+ return Object.fromEntries(
7942
+ Object.entries(value).map(([kind, detail]) => [
7943
+ kind,
7944
+ typeof detail === "string" ? detail : true
7945
+ ])
7946
+ );
7947
+ }
7948
+ function parse(line) {
7949
+ try {
7950
+ const value = JSON.parse(line);
7951
+ return isRecord10(value) ? value : null;
7952
+ } catch {
7953
+ return null;
7954
+ }
6318
7955
  }
6319
- function parse(line) {
6320
- try {
6321
- const value = JSON.parse(line);
6322
- return isRecord9(value) ? value : null;
6323
- } catch {
6324
- return null;
6325
- }
6326
- }
6327
7956
  function readMetaLine(line) {
6328
7957
  if (!line.includes('"session_meta"')) return null;
6329
7958
  const value = parse(line);
6330
- if (!value || value.type !== "session_meta" || !isRecord9(value.payload)) return null;
7959
+ if (!value || value.type !== "session_meta" || !isRecord10(value.payload)) return null;
6331
7960
  const payload = value.payload;
6332
7961
  const meta = {
6333
7962
  id: text3(payload.id),
@@ -6347,7 +7976,7 @@ function readMetaLine(line) {
6347
7976
  function readTurnLine(line) {
6348
7977
  if (!line.includes('"event_msg"') || !TURN_HINT.test(line)) return null;
6349
7978
  const value = parse(line);
6350
- if (!value || value.type !== "event_msg" || !isRecord9(value.payload)) return null;
7979
+ if (!value || value.type !== "event_msg" || !isRecord10(value.payload)) return null;
6351
7980
  const turn = value.payload.type;
6352
7981
  if (typeof turn !== "string" || !TURN_TYPE.test(turn)) return null;
6353
7982
  return {
@@ -6360,23 +7989,23 @@ function lineTime(line) {
6360
7989
  const match = LINE_TIME.exec(line);
6361
7990
  return match ? parseCodexTime(match[1]) : null;
6362
7991
  }
6363
- function bytes(data) {
7992
+ function bytes2(data) {
6364
7993
  return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
6365
7994
  }
6366
- async function readExactly(file, position, length) {
6367
- const data = bytes(await file.read(position, length));
7995
+ async function readExactly2(file, position, length) {
7996
+ const data = bytes2(await file.read(position, length));
6368
7997
  if (data.byteLength !== length) throw new Error("The file changed while it was read.");
6369
7998
  return data;
6370
7999
  }
6371
8000
  async function readHead(file, size) {
6372
- for (const limit of [FIRST_HEAD_BYTES, HEAD_LIMIT_BYTES]) {
8001
+ for (const limit of [FIRST_HEAD_BYTES, HEAD_LIMIT_BYTES2]) {
6373
8002
  const length = Math.min(limit, size);
6374
- const head = await readExactly(file, 0, length);
8003
+ const head = await readExactly2(file, 0, length);
6375
8004
  let named = head.indexOf('"session_meta"');
6376
8005
  while (named !== -1) {
6377
- const end = head.indexOf(NEWLINE, named);
8006
+ const end = head.indexOf(NEWLINE3, named);
6378
8007
  if (end === -1) break;
6379
- const start = named > 0 ? head.lastIndexOf(NEWLINE, named - 1) + 1 : 0;
8008
+ const start = named > 0 ? head.lastIndexOf(NEWLINE3, named - 1) + 1 : 0;
6380
8009
  const meta = readMetaLine(head.toString("utf8", start, end));
6381
8010
  if (meta) return meta;
6382
8011
  named = head.indexOf('"session_meta"', end + 1);
@@ -6385,9 +8014,9 @@ async function readHead(file, size) {
6385
8014
  }
6386
8015
  return null;
6387
8016
  }
6388
- async function scanTail(file, size) {
8017
+ async function scanTail2(file, size) {
6389
8018
  let position = size;
6390
- let budget2 = TAIL_LIMIT_BYTES;
8019
+ let budget2 = TAIL_LIMIT_BYTES2;
6391
8020
  let fragment = Buffer.alloc(0);
6392
8021
  let end = null;
6393
8022
  let lastLineAt = null;
@@ -6405,15 +8034,15 @@ async function scanTail(file, size) {
6405
8034
  end
6406
8035
  });
6407
8036
  while (position > 0 && budget2 > 0) {
6408
- const length = Math.min(TAIL_CHUNK_BYTES, position, budget2);
8037
+ const length = Math.min(TAIL_CHUNK_BYTES2, position, budget2);
6409
8038
  const start = position - length;
6410
- const chunk = await readExactly(file, start, length);
8039
+ const chunk = await readExactly2(file, start, length);
6411
8040
  budget2 -= length;
6412
8041
  position = start;
6413
8042
  const buffer = fragment.byteLength > 0 ? Buffer.concat([chunk, fragment]) : chunk;
6414
8043
  let cut2 = buffer.byteLength;
6415
8044
  if (end === null) {
6416
- const last = buffer.lastIndexOf(NEWLINE);
8045
+ const last = buffer.lastIndexOf(NEWLINE3);
6417
8046
  if (last === -1) {
6418
8047
  fragment = buffer;
6419
8048
  continue;
@@ -6421,12 +8050,12 @@ async function scanTail(file, size) {
6421
8050
  end = start + last + 1;
6422
8051
  cut2 = last;
6423
8052
  }
6424
- let newline = cut2 > 0 ? buffer.lastIndexOf(NEWLINE, cut2 - 1) : -1;
8053
+ let newline = cut2 > 0 ? buffer.lastIndexOf(NEWLINE3, cut2 - 1) : -1;
6425
8054
  while (newline !== -1) {
6426
8055
  const turn = take(buffer.toString("utf8", newline + 1, cut2));
6427
8056
  if (turn) return found(turn);
6428
8057
  cut2 = newline;
6429
- newline = cut2 > 0 ? buffer.lastIndexOf(NEWLINE, cut2 - 1) : -1;
8058
+ newline = cut2 > 0 ? buffer.lastIndexOf(NEWLINE3, cut2 - 1) : -1;
6430
8059
  }
6431
8060
  if (start === 0) {
6432
8061
  const turn = take(buffer.toString("utf8", 0, cut2));
@@ -6440,7 +8069,7 @@ async function scanTail(file, size) {
6440
8069
  }
6441
8070
  return { lastTurn: void 0, lastTurnAt: null, lastTurnImported: false, lastLineAt, end };
6442
8071
  }
6443
- function stateOf(cached) {
8072
+ function stateOf2(cached) {
6444
8073
  return {
6445
8074
  meta: cached.meta,
6446
8075
  lastTurn: cached.lastTurn,
@@ -6456,7 +8085,7 @@ function createRolloutReader(io) {
6456
8085
  try {
6457
8086
  const { info } = handle;
6458
8087
  const meta = await readHead(handle, info.size);
6459
- const tail = await scanTail(handle, info.size);
8088
+ const tail = await scanTail2(handle, info.size);
6460
8089
  return {
6461
8090
  meta,
6462
8091
  lastTurn: tail.lastTurn,
@@ -6476,14 +8105,14 @@ function createRolloutReader(io) {
6476
8105
  const offset = cached.offset;
6477
8106
  if (cached.meta === null || offset === null) return null;
6478
8107
  if (info.ino !== cached.ino || info.size <= cached.size || info.size < offset) return null;
6479
- if (info.size - offset > TAIL_LIMIT_BYTES) return null;
8108
+ if (info.size - offset > TAIL_LIMIT_BYTES2) return null;
6480
8109
  const handle = await io.openRegular(file);
6481
8110
  try {
6482
8111
  const opened = handle.info;
6483
8112
  if (opened.ino !== cached.ino || opened.size < offset) return null;
6484
8113
  const from = offset > 0 ? offset - 1 : 0;
6485
- const read = await readExactly(handle, from, opened.size - from);
6486
- if (offset > 0 && read[0] !== NEWLINE) return null;
8114
+ const read = await readExactly2(handle, from, opened.size - from);
8115
+ if (offset > 0 && read[0] !== NEWLINE3) return null;
6487
8116
  const added = offset > 0 ? read.subarray(1) : read;
6488
8117
  const next = {
6489
8118
  ...cached,
@@ -6492,7 +8121,7 @@ function createRolloutReader(io) {
6492
8121
  mtimeMs: opened.mtimeMs
6493
8122
  };
6494
8123
  let start = 0;
6495
- let newline = added.indexOf(NEWLINE);
8124
+ let newline = added.indexOf(NEWLINE3);
6496
8125
  while (newline !== -1) {
6497
8126
  const line = added.toString("utf8", start, newline);
6498
8127
  if (line.trim() !== "") {
@@ -6506,7 +8135,7 @@ function createRolloutReader(io) {
6506
8135
  }
6507
8136
  }
6508
8137
  start = newline + 1;
6509
- newline = added.indexOf(NEWLINE, start);
8138
+ newline = added.indexOf(NEWLINE3, start);
6510
8139
  }
6511
8140
  next.offset = offset + start;
6512
8141
  return next;
@@ -6518,11 +8147,11 @@ function createRolloutReader(io) {
6518
8147
  async read(file, info) {
6519
8148
  const cached = cache.get(file);
6520
8149
  if (cached && cached.ino === info.ino && cached.size === info.size && cached.mtimeMs === info.mtimeMs) {
6521
- return stateOf(cached);
8150
+ return stateOf2(cached);
6522
8151
  }
6523
8152
  const next = cached && await readOnward(file, cached, info) || await readFresh(file);
6524
8153
  cache.set(file, next);
6525
- return stateOf(next);
8154
+ return stateOf2(next);
6526
8155
  },
6527
8156
  keepOnly(files) {
6528
8157
  for (const file of cache.keys()) {
@@ -6533,10 +8162,10 @@ function createRolloutReader(io) {
6533
8162
  }
6534
8163
 
6535
8164
  // src/collector/adapters/codex/rollouts.ts
6536
- import path11 from "node:path";
6537
- var UUID = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
8165
+ import path15 from "node:path";
8166
+ var UUID4 = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
6538
8167
  var ROLLOUT_FILE_NAME = new RegExp(
6539
- `^rollout-\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-(${UUID})(?:_(${UUID}))?\\.jsonl$`,
8168
+ `^rollout-\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-(${UUID4})(?:_(${UUID4}))?\\.jsonl$`,
6540
8169
  "i"
6541
8170
  );
6542
8171
  var INDEX_REFRESH_MS = 3e4;
@@ -6551,7 +8180,7 @@ function twoDigits(value) {
6551
8180
  function recentDayFolders(sessionsDir, now) {
6552
8181
  const folders = [now, now - 24 * 60 * 60 * 1e3].map((at) => {
6553
8182
  const date = new Date(at);
6554
- return path11.join(
8183
+ return path15.join(
6555
8184
  sessionsDir,
6556
8185
  String(date.getFullYear()),
6557
8186
  twoDigits(date.getMonth() + 1),
@@ -6580,12 +8209,12 @@ function createRolloutFinder(options) {
6580
8209
  const refs = [];
6581
8210
  for (const name of (await names(dir)).sort()) {
6582
8211
  const threadId = threadIdOfRollout(name);
6583
- if (threadId !== null) refs.push({ path: path11.join(dir, name), threadId });
8212
+ if (threadId !== null) refs.push({ path: path15.join(dir, name), threadId });
6584
8213
  }
6585
8214
  return refs;
6586
8215
  }
6587
8216
  async function subfolders(dir, pattern) {
6588
- return (await names(dir)).filter((name) => pattern.test(name)).sort().map((name) => path11.join(dir, name));
8217
+ return (await names(dir)).filter((name) => pattern.test(name)).sort().map((name) => path15.join(dir, name));
6589
8218
  }
6590
8219
  async function buildIndex() {
6591
8220
  const years = await subfolders(sessionsDir, YEAR);
@@ -6629,7 +8258,7 @@ function createRolloutFinder(options) {
6629
8258
  // src/collector/adapters/codex/sessionIndex.ts
6630
8259
  var SESSION_INDEX_FILE = "session_index.jsonl";
6631
8260
  var SESSION_INDEX_LIMIT_BYTES = 4 * 1024 * 1024;
6632
- function isRecord10(value) {
8261
+ function isRecord11(value) {
6633
8262
  return typeof value === "object" && value !== null && !Array.isArray(value);
6634
8263
  }
6635
8264
  function parseSessionIndex(content) {
@@ -6642,7 +8271,7 @@ function parseSessionIndex(content) {
6642
8271
  } catch {
6643
8272
  continue;
6644
8273
  }
6645
- if (!isRecord10(value)) continue;
8274
+ if (!isRecord11(value)) continue;
6646
8275
  const { id, thread_name: name } = value;
6647
8276
  if (typeof id !== "string" || id === "" || typeof name !== "string") continue;
6648
8277
  const trimmed = name.trim();
@@ -6693,14 +8322,14 @@ function createSessionIndexReader(file, io) {
6693
8322
  }
6694
8323
 
6695
8324
  // src/collector/adapters/codex/toSession.ts
6696
- var SOURCE_ID2 = "codex";
6697
- var WRITE_ORDER_SLACK_MS2 = 1e3;
8325
+ var SOURCE_ID3 = "codex";
8326
+ var WRITE_ORDER_SLACK_MS3 = 1e3;
6698
8327
  function lastActivity(state, now) {
6699
8328
  return plausibleTime(state.lastLineAt, now) ?? plausibleTime(state.lastTurnAt, now) ?? plausibleTime(parseCodexTime(state.meta?.timestamp), now);
6700
8329
  }
6701
8330
  function codexSession(input) {
6702
8331
  const { threadId, state, live: live2, now } = input;
6703
- const id = `${SOURCE_ID2}:${threadId}`;
8332
+ const id = `${SOURCE_ID3}:${threadId}`;
6704
8333
  const cwd = state.meta?.cwd ?? null;
6705
8334
  const project = projectOf(cwd);
6706
8335
  const status = mapCodexStatus({ lastTurn: state.lastTurn, live: live2 });
@@ -6711,12 +8340,12 @@ function codexSession(input) {
6711
8340
  } else if (status === "idle" || status === "finished") {
6712
8341
  statusSince = plausibleTime(state.lastLineAt, now);
6713
8342
  }
6714
- if (statusSince !== null && startedAt !== null && statusSince < startedAt - WRITE_ORDER_SLACK_MS2) {
8343
+ if (statusSince !== null && startedAt !== null && statusSince < startedAt - WRITE_ORDER_SLACK_MS3) {
6715
8344
  statusSince = null;
6716
8345
  }
6717
8346
  const session = {
6718
8347
  id,
6719
- source: SOURCE_ID2,
8348
+ source: SOURCE_ID3,
6720
8349
  surface: mapCodexSurface(state.meta?.source, state.meta?.originator),
6721
8350
  // A name made only of spaces is no name. It is cleaned and cut as every other agent's is.
6722
8351
  name: sessionName(input.name ?? "") || project || id,
@@ -6729,7 +8358,7 @@ function codexSession(input) {
6729
8358
  stale: isStale({ status, statusSince }, now)
6730
8359
  };
6731
8360
  const writtenAt = input.writtenAt === void 0 ? null : plausibleTime(Math.floor(input.writtenAt), now);
6732
- const beforeStart = writtenAt !== null && startedAt !== null && writtenAt < startedAt - WRITE_ORDER_SLACK_MS2;
8361
+ const beforeStart = writtenAt !== null && startedAt !== null && writtenAt < startedAt - WRITE_ORDER_SLACK_MS3;
6733
8362
  if (writtenAt !== null && !beforeStart) session.lastWriteAt = writtenAt;
6734
8363
  return session;
6735
8364
  }
@@ -6759,11 +8388,11 @@ async function readWriterLocks(dir, io) {
6759
8388
  // src/collector/adapters/codex/index.ts
6760
8389
  var CODEX_HOME_ENV = "AGENT_LOOKOUT_CODEX_HOME";
6761
8390
  var CODEX_OWN_HOME_ENV = "CODEX_HOME";
6762
- var RECHECK_MS = 6e4;
6763
- var BASIS = "files";
8391
+ var RECHECK_MS2 = 6e4;
8392
+ var BASIS2 = "files";
6764
8393
  var BASIS_WITHOUT_LOCKS = "files-without-locks";
6765
- var LABEL2 = "Codex";
6766
- var NEEDS_YOU_NOTE = "Codex's session files do not record when it is waiting for your approval, so a Codex session that is waiting for you shows as working.";
8394
+ var LABEL3 = "Codex";
8395
+ var NEEDS_YOU_NOTE2 = "Codex's session files do not record when it is waiting for your approval, so a Codex session that is waiting for you shows as working.";
6767
8396
  var OLDER_CODEX_NOTE = "Some sessions were started by a Codex older than 0.155, which does not record which sessions are open, so those are never shown as finished.";
6768
8397
  var CODEX_CAPABILITIES = {
6769
8398
  "working-and-idle": { level: "yes" },
@@ -6796,18 +8425,18 @@ var CODEX_CAPABILITIES = {
6796
8425
  };
6797
8426
  function createCodexAdapter(options = {}) {
6798
8427
  const env = options.env ?? process.env;
6799
- const homeDir = options.homeDir ?? os5.homedir();
8428
+ const homeDir = options.homeDir ?? os6.homedir();
6800
8429
  const now = options.now ?? Date.now;
6801
8430
  const io = options.io ?? nodeIo2;
6802
8431
  const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
6803
- const recheckMs = options.recheckMs ?? RECHECK_MS;
8432
+ const recheckMs = options.recheckMs ?? RECHECK_MS2;
6804
8433
  const ownOverride = env[CODEX_HOME_ENV]?.trim() || void 0;
6805
8434
  const codexOverride = env[CODEX_OWN_HOME_ENV]?.trim() || void 0;
6806
8435
  const homeFrom = ownOverride ? "agent-lookout" : codexOverride ? "codex" : "default";
6807
- const codexHome = path12.resolve(ownOverride ?? codexOverride ?? path12.join(homeDir, ".codex"));
6808
- const sessionsDir = path12.join(codexHome, "sessions");
6809
- const locksDir = path12.join(codexHome, WRITER_LOCK_DIR);
6810
- const indexFile = path12.join(codexHome, SESSION_INDEX_FILE);
8436
+ const codexHome = path16.resolve(ownOverride ?? codexOverride ?? path16.join(homeDir, ".codex"));
8437
+ const sessionsDir = path16.join(codexHome, "sessions");
8438
+ const locksDir = path16.join(codexHome, WRITER_LOCK_DIR);
8439
+ const indexFile = path16.join(codexHome, SESSION_INDEX_FILE);
6811
8440
  const homeName = tildify(codexHome, homeDir);
6812
8441
  const sessionsName = tildify(sessionsDir, homeDir);
6813
8442
  const locksName = tildify(locksDir, homeDir);
@@ -6833,14 +8462,14 @@ function createCodexAdapter(options = {}) {
6833
8462
  }
6834
8463
  function result(state, detail, read, checkedAt) {
6835
8464
  const health = {
6836
- id: SOURCE_ID2,
6837
- label: LABEL2,
8465
+ id: SOURCE_ID3,
8466
+ label: LABEL3,
6838
8467
  state,
6839
8468
  detail,
6840
8469
  watching: watching(read),
6841
8470
  checkedAt
6842
8471
  };
6843
- return state === "ok" ? { health, sessions: [], basis: BASIS } : { health, sessions: [] };
8472
+ return state === "ok" ? { health, sessions: [], basis: BASIS2 } : { health, sessions: [] };
6844
8473
  }
6845
8474
  function notFound(checkedAt) {
6846
8475
  const where = homeFrom === "codex" ? `${CODEX_OWN_HOME_ENV} is set to ${homeName}, and there is no folder there` : `there is no ${homeName} folder`;
@@ -6888,7 +8517,7 @@ function createCodexAdapter(options = {}) {
6888
8517
  if (homeFrom === "agent-lookout") {
6889
8518
  return result(
6890
8519
  "ok",
6891
- `${CODEX_HOME_ENV} is set to ${homeName}, which has no sessions folder, so no Codex sessions are listed. ${NEEDS_YOU_NOTE}`,
8520
+ `${CODEX_HOME_ENV} is set to ${homeName}, which has no sessions folder, so no Codex sessions are listed. ${NEEDS_YOU_NOTE2}`,
6892
8521
  "not found",
6893
8522
  checkedAt
6894
8523
  );
@@ -6901,7 +8530,7 @@ function createCodexAdapter(options = {}) {
6901
8530
  homeSeen = true;
6902
8531
  return result(
6903
8532
  "ok",
6904
- `Codex has not saved any sessions in ${sessionsName} yet. ${NEEDS_YOU_NOTE}`,
8533
+ `Codex has not saved any sessions in ${sessionsName} yet. ${NEEDS_YOU_NOTE2}`,
6905
8534
  "not found",
6906
8535
  checkedAt
6907
8536
  );
@@ -6983,7 +8612,7 @@ function createCodexAdapter(options = {}) {
6983
8612
  now: checkedAt
6984
8613
  });
6985
8614
  });
6986
- let detail = `Sessions are read from the files Codex saves in ${sessionsName}. ${NEEDS_YOU_NOTE}`;
8615
+ let detail = `Sessions are read from the files Codex saves in ${sessionsName}. ${NEEDS_YOU_NOTE2}`;
6987
8616
  if (!locks.supported) {
6988
8617
  const why = locks.missing ? `There is no list of open sessions at ${locksName}, which Codex keeps from version 0.155 on` : `The list of open sessions at ${locksName} could not be read`;
6989
8618
  detail += ` ${why}, so a session that has ended cannot be told from one that is idle, and none is shown as finished.`;
@@ -6991,11 +8620,11 @@ function createCodexAdapter(options = {}) {
6991
8620
  detail += ` ${OLDER_CODEX_NOTE}`;
6992
8621
  }
6993
8622
  const answer = result("ok", detail, every(pollIntervalMs), checkedAt);
6994
- return { ...answer, sessions, basis: locks.supported ? BASIS : BASIS_WITHOUT_LOCKS };
8623
+ return { ...answer, sessions, basis: locks.supported ? BASIS2 : BASIS_WITHOUT_LOCKS };
6995
8624
  }
6996
8625
  return {
6997
- id: SOURCE_ID2,
6998
- label: LABEL2,
8626
+ id: SOURCE_ID3,
8627
+ label: LABEL3,
6999
8628
  capabilities: CODEX_CAPABILITIES,
7000
8629
  lookingIn: `Looking for Codex sessions in ${sessionsName}.`,
7001
8630
  async poll() {
@@ -7014,8 +8643,8 @@ function createCodexAdapter(options = {}) {
7014
8643
  }
7015
8644
 
7016
8645
  // src/collector/adapters/status-files/index.ts
7017
- import os6 from "node:os";
7018
- import path13 from "node:path";
8646
+ import os7 from "node:os";
8647
+ import path17 from "node:path";
7019
8648
 
7020
8649
  // src/core/mapping/statusFileMapping.ts
7021
8650
  var STATUS = /* @__PURE__ */ new Map([
@@ -7095,16 +8724,16 @@ function parseStatusFile(content) {
7095
8724
  }
7096
8725
 
7097
8726
  // src/collector/adapters/status-files/toSession.ts
7098
- var SOURCE_ID3 = "status-files";
7099
- var WRITE_ORDER_SLACK_MS3 = 1e3;
8727
+ var SOURCE_ID4 = "status-files";
8728
+ var WRITE_ORDER_SLACK_MS4 = 1e3;
7100
8729
  function statusFileSession(input) {
7101
8730
  const { fileName: fileName2, file, statusSince, now } = input;
7102
8731
  const cwd = file.cwd ?? null;
7103
8732
  const project = projectOf(cwd);
7104
8733
  const mapped = mapStatusFileStatus(file);
7105
8734
  const session = {
7106
- id: `${SOURCE_ID3}:${fileName2}`,
7107
- source: SOURCE_ID3,
8735
+ id: `${SOURCE_ID4}:${fileName2}`,
8736
+ source: SOURCE_ID4,
7108
8737
  agent: file.agent,
7109
8738
  surface: mapStatusFileApp(file.app),
7110
8739
  // The file's name is as untrusted as its contents, so it is cleaned and cut
@@ -7124,15 +8753,15 @@ function statusFileSession(input) {
7124
8753
  }
7125
8754
  const writtenAt = input.writtenAt === void 0 ? null : plausibleTime(Math.floor(input.writtenAt), now);
7126
8755
  const given = plausibleTime(file.since, now);
7127
- const beforeSince = writtenAt !== null && given !== null && writtenAt < given - WRITE_ORDER_SLACK_MS3;
8756
+ const beforeSince = writtenAt !== null && given !== null && writtenAt < given - WRITE_ORDER_SLACK_MS4;
7128
8757
  if (writtenAt !== null && !beforeSince) session.lastWriteAt = writtenAt;
7129
8758
  return session;
7130
8759
  }
7131
8760
 
7132
8761
  // src/collector/adapters/status-files/index.ts
7133
8762
  var STATUS_DIR_ENV = "AGENT_LOOKOUT_STATUS_DIR";
7134
- var BASIS2 = "files";
7135
- var LABEL3 = "Status files";
8763
+ var BASIS3 = "files";
8764
+ var LABEL4 = "Status files";
7136
8765
  var STATUS_FILE_CAPABILITIES = {
7137
8766
  "working-and-idle": { level: "partly", reason: "If the agent writes working and idle." },
7138
8767
  "needs-you": { level: "partly", reason: "If the agent writes waiting." },
@@ -7158,13 +8787,13 @@ function counted(count2, one, many) {
7158
8787
  }
7159
8788
  function createStatusFileAdapter(options = {}) {
7160
8789
  const env = options.env ?? process.env;
7161
- const homeDir = options.homeDir ?? os6.homedir();
8790
+ const homeDir = options.homeDir ?? os7.homedir();
7162
8791
  const now = options.now ?? Date.now;
7163
8792
  const io = options.io ?? nodeIo2;
7164
8793
  const isAlive = options.isAlive ?? isProcessAlive;
7165
8794
  const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
7166
8795
  const override = env[STATUS_DIR_ENV]?.trim() || void 0;
7167
- const dir = path13.resolve(override ?? path13.join(homeDir, ".agent-lookout", "sessions"));
8796
+ const dir = path17.resolve(override ?? path17.join(homeDir, ".agent-lookout", "sessions"));
7168
8797
  const dirName = tildify(dir, homeDir);
7169
8798
  const decoder2 = new TextDecoder();
7170
8799
  let lastRead = /* @__PURE__ */ new Map();
@@ -7185,15 +8814,15 @@ function createStatusFileAdapter(options = {}) {
7185
8814
  }
7186
8815
  function result(state, detail, facts, checkedAt, sessions = []) {
7187
8816
  const health = {
7188
- id: SOURCE_ID3,
7189
- label: LABEL3,
8817
+ id: SOURCE_ID4,
8818
+ label: LABEL4,
7190
8819
  state,
7191
8820
  detail,
7192
8821
  watching: facts,
7193
8822
  checkedAt
7194
8823
  };
7195
- if (state === "ok") return { health, sessions, basis: BASIS2 };
7196
- if (state === "not-set-up") return { health, sessions: [], basis: BASIS2 };
8824
+ if (state === "ok") return { health, sessions, basis: BASIS3 };
8825
+ if (state === "not-set-up") return { health, sessions: [], basis: BASIS3 };
7197
8826
  return { health, sessions: [] };
7198
8827
  }
7199
8828
  async function readOne(file) {
@@ -7209,9 +8838,9 @@ function createStatusFileAdapter(options = {}) {
7209
8838
  const tooLarge = { kind: "refused", why: "is over 16 KB" };
7210
8839
  try {
7211
8840
  if (open2.info.size > MAX_FILE_BYTES) return tooLarge;
7212
- const bytes2 = await open2.read(0, MAX_FILE_BYTES + 1);
7213
- if (bytes2.byteLength > MAX_FILE_BYTES) return tooLarge;
7214
- const parsed = parseStatusFile(decoder2.decode(bytes2));
8841
+ const bytes3 = await open2.read(0, MAX_FILE_BYTES + 1);
8842
+ if (bytes3.byteLength > MAX_FILE_BYTES) return tooLarge;
8843
+ const parsed = parseStatusFile(decoder2.decode(bytes3));
7215
8844
  return parsed ? { kind: "read", file: parsed, mtimeMs: open2.info.mtimeMs } : { kind: "invalid" };
7216
8845
  } catch {
7217
8846
  return { kind: "refused", why: "could not be read" };
@@ -7224,7 +8853,7 @@ function createStatusFileAdapter(options = {}) {
7224
8853
  if (names.length <= MAX_FILES) return names;
7225
8854
  const written = await Promise.all(
7226
8855
  names.map(async (name) => {
7227
- const info = await io.lstat(path13.join(dir, name)).catch(() => null);
8856
+ const info = await io.lstat(path17.join(dir, name)).catch(() => null);
7228
8857
  return { name, mtimeMs: info?.mtimeMs ?? -Infinity };
7229
8858
  })
7230
8859
  );
@@ -7274,7 +8903,7 @@ function createStatusFileAdapter(options = {}) {
7274
8903
  const nextRead = /* @__PURE__ */ new Map();
7275
8904
  const nextStatuses = /* @__PURE__ */ new Map();
7276
8905
  for (const fileName2 of await toRead(listed)) {
7277
- const reading = await readOne(path13.join(dir, fileName2));
8906
+ const reading = await readOne(path17.join(dir, fileName2));
7278
8907
  let kept;
7279
8908
  if (reading.kind === "read") {
7280
8909
  kept = reading;
@@ -7340,8 +8969,8 @@ function createStatusFileAdapter(options = {}) {
7340
8969
  );
7341
8970
  }
7342
8971
  return {
7343
- id: SOURCE_ID3,
7344
- label: LABEL3,
8972
+ id: SOURCE_ID4,
8973
+ label: LABEL4,
7345
8974
  capabilities: STATUS_FILE_CAPABILITIES,
7346
8975
  lookingIn: `Looking for status files in ${dirName}.`,
7347
8976
  async poll() {
@@ -7500,8 +9129,8 @@ function createEventStore(capacity = EVENT_CAPACITY) {
7500
9129
  }
7501
9130
 
7502
9131
  // src/collector/git/branchFinder.ts
7503
- import os7 from "node:os";
7504
- import path15 from "node:path";
9132
+ import os8 from "node:os";
9133
+ import path19 from "node:path";
7505
9134
 
7506
9135
  // src/collector/git/gitHead.ts
7507
9136
  var MAX_GIT_FILE_BYTES = 4 * 1024;
@@ -7540,19 +9169,19 @@ function parseCommonDir(content) {
7540
9169
  }
7541
9170
 
7542
9171
  // src/collector/git/repository.ts
7543
- import { createHash as createHash2 } from "node:crypto";
7544
- import path14 from "node:path";
9172
+ import { createHash as createHash3 } from "node:crypto";
9173
+ import path18 from "node:path";
7545
9174
  var REPOSITORY_ID_LENGTH = 16;
7546
9175
  function repositoryId(gitFolder) {
7547
- return createHash2("sha256").update(gitFolder).digest("hex").slice(0, REPOSITORY_ID_LENGTH);
9176
+ return createHash3("sha256").update(gitFolder).digest("hex").slice(0, REPOSITORY_ID_LENGTH);
7548
9177
  }
7549
9178
  function sharedName(common) {
7550
- const base = path14.basename(common);
7551
- if (base.startsWith(".")) return path14.basename(path14.dirname(common));
9179
+ const base = path18.basename(common);
9180
+ if (base.startsWith(".")) return path18.basename(path18.dirname(common));
7552
9181
  return base.endsWith(".git") ? base.slice(0, -".git".length) : base;
7553
9182
  }
7554
9183
  function repositoryOf({ dir, gitDir, commonDir }) {
7555
- const name = sessionName(commonDir === null ? path14.basename(dir) : sharedName(commonDir));
9184
+ const name = sessionName(commonDir === null ? path18.basename(dir) : sharedName(commonDir));
7556
9185
  return name === void 0 ? null : { id: repositoryId(commonDir ?? gitDir), name };
7557
9186
  }
7558
9187
 
@@ -7570,9 +9199,9 @@ async function readGitFile(io, file, maxBytes = MAX_GIT_FILE_BYTES) {
7570
9199
  }
7571
9200
  try {
7572
9201
  if (open2.info.size > maxBytes) return null;
7573
- const bytes2 = await open2.read(0, maxBytes + 1);
7574
- if (bytes2.byteLength > maxBytes) return null;
7575
- return decoder.decode(bytes2);
9202
+ const bytes3 = await open2.read(0, maxBytes + 1);
9203
+ if (bytes3.byteLength > maxBytes) return null;
9204
+ return decoder.decode(bytes3);
7576
9205
  } catch {
7577
9206
  return null;
7578
9207
  } finally {
@@ -7581,13 +9210,13 @@ async function readGitFile(io, file, maxBytes = MAX_GIT_FILE_BYTES) {
7581
9210
  }
7582
9211
  }
7583
9212
  async function commonDirOf(io, gitDir) {
7584
- const content = await readGitFile(io, path15.join(gitDir, "commondir"));
9213
+ const content = await readGitFile(io, path19.join(gitDir, "commondir"));
7585
9214
  if (content === void 0) return null;
7586
9215
  const target = content === null ? null : parseCommonDir(content);
7587
- return target === null ? void 0 : path15.resolve(gitDir, target);
9216
+ return target === null ? void 0 : path19.resolve(gitDir, target);
7588
9217
  }
7589
9218
  async function headIn(dir, kind, io) {
7590
- const marker = path15.join(dir, ".git");
9219
+ const marker = path19.join(dir, ".git");
7591
9220
  let gitDir;
7592
9221
  if (kind === "directory") {
7593
9222
  gitDir = marker;
@@ -7595,11 +9224,11 @@ async function headIn(dir, kind, io) {
7595
9224
  const content2 = await readGitFile(io, marker);
7596
9225
  const target = typeof content2 === "string" ? parseGitFile(content2) : null;
7597
9226
  if (target === null) return null;
7598
- gitDir = path15.resolve(dir, target);
9227
+ gitDir = path19.resolve(dir, target);
7599
9228
  } else {
7600
9229
  return null;
7601
9230
  }
7602
- const content = await readGitFile(io, path15.join(gitDir, "HEAD"));
9231
+ const content = await readGitFile(io, path19.join(gitDir, "HEAD"));
7603
9232
  const head = typeof content === "string" ? parseHead(content) : null;
7604
9233
  if (head === null) return null;
7605
9234
  const commonDir = kind === "file" ? await commonDirOf(io, gitDir) : null;
@@ -7608,20 +9237,20 @@ async function headIn(dir, kind, io) {
7608
9237
  return { head: { ...head, repository }, gitFolder: commonDir ?? gitDir };
7609
9238
  }
7610
9239
  async function findHead(cwd, { io, homeDir }) {
7611
- if (!path15.isAbsolute(cwd)) return null;
7612
- const home = path15.resolve(homeDir);
7613
- let dir = path15.resolve(cwd);
9240
+ if (!path19.isAbsolute(cwd)) return null;
9241
+ const home = path19.resolve(homeDir);
9242
+ let dir = path19.resolve(cwd);
7614
9243
  try {
7615
9244
  if ((await io.stat(dir)).kind !== "directory") return null;
7616
9245
  } catch {
7617
9246
  return null;
7618
9247
  }
7619
9248
  for (let level = 0; level < MAX_LEVELS; level += 1) {
7620
- const parent = path15.dirname(dir);
9249
+ const parent = path19.dirname(dir);
7621
9250
  if (dir === home || dir === parent) return null;
7622
9251
  let kind;
7623
9252
  try {
7624
- kind = (await io.lstat(path15.join(dir, ".git"))).kind;
9253
+ kind = (await io.lstat(path19.join(dir, ".git"))).kind;
7625
9254
  } catch (error) {
7626
9255
  if (!isMissing2(error)) return null;
7627
9256
  dir = parent;
@@ -7642,7 +9271,7 @@ function within(work, ms) {
7642
9271
  });
7643
9272
  }
7644
9273
  function createBranchFinder(options = {}) {
7645
- const where = { io: options.io ?? nodeIo2, homeDir: options.homeDir ?? os7.homedir() };
9274
+ const where = { io: options.io ?? nodeIo2, homeDir: options.homeDir ?? os8.homedir() };
7646
9275
  const now = options.now ?? Date.now;
7647
9276
  const waitMs = options.waitMs ?? READ_WAIT_MS;
7648
9277
  const known = /* @__PURE__ */ new Map();
@@ -7704,14 +9333,14 @@ var STATUS_STATE = {
7704
9333
  PENDING: "pending",
7705
9334
  EXPECTED: "pending"
7706
9335
  };
7707
- function isRecord11(value) {
9336
+ function isRecord12(value) {
7708
9337
  return typeof value === "object" && value !== null && !Array.isArray(value);
7709
9338
  }
7710
9339
  function textOf(value) {
7711
9340
  return typeof value === "string" ? value : "";
7712
9341
  }
7713
9342
  function checkOf(value) {
7714
- if (!isRecord11(value)) return null;
9343
+ if (!isRecord12(value)) return null;
7715
9344
  const startedAt = Date.parse(textOf(value.startedAt));
7716
9345
  const at = Number.isNaN(startedAt) ? -Infinity : startedAt;
7717
9346
  if (value.__typename === "CheckRun") {
@@ -7747,7 +9376,7 @@ function checksOf(rollup) {
7747
9376
  for (const { state } of latest.values()) counts[state] += 1;
7748
9377
  return { state: checksStateOf(counts), ...counts };
7749
9378
  }
7750
- function stateOf2(state, isDraft) {
9379
+ function stateOf3(state, isDraft) {
7751
9380
  if (state === "OPEN") return isDraft === true ? "draft" : "open";
7752
9381
  if (state === "MERGED") return "merged";
7753
9382
  if (state === "CLOSED") return "closed";
@@ -7760,9 +9389,9 @@ function readGhAnswer(stdout, repository) {
7760
9389
  } catch {
7761
9390
  return null;
7762
9391
  }
7763
- if (!isRecord11(data)) return null;
9392
+ if (!isRecord12(data)) return null;
7764
9393
  const { number } = data;
7765
- const state = stateOf2(data.state, data.isDraft);
9394
+ const state = stateOf3(data.state, data.isDraft);
7766
9395
  const title = typeof data.title === "string" ? sessionName(data.title) : void 0;
7767
9396
  if (!isPullRequestNumber(number) || state === null || title === void 0) return null;
7768
9397
  return {
@@ -7899,7 +9528,7 @@ function createGhAsker(options) {
7899
9528
  }
7900
9529
 
7901
9530
  // src/collector/git/pullRequestTarget.ts
7902
- import path16 from "node:path";
9531
+ import path20 from "node:path";
7903
9532
 
7904
9533
  // src/collector/git/gitConfig.ts
7905
9534
  var MAX_CONFIG_BYTES = 64 * 1024;
@@ -8157,7 +9786,7 @@ function targetOf(config, branch, base, defaultBranch) {
8157
9786
  async function readPullRequestTarget(gitFolder, branch, io = nodeIo2) {
8158
9787
  try {
8159
9788
  if (!isAskableBranch(branch)) return null;
8160
- const text4 = await readGitFile(io, path16.join(gitFolder, "config"), MAX_CONFIG_BYTES);
9789
+ const text4 = await readGitFile(io, path20.join(gitFolder, "config"), MAX_CONFIG_BYTES);
8161
9790
  if (typeof text4 !== "string") return null;
8162
9791
  const config = parseGitConfig(text4);
8163
9792
  const base = config === null ? null : baseRemoteOf(config);
@@ -8166,7 +9795,7 @@ async function readPullRequestTarget(gitFolder, branch, io = nodeIo2) {
8166
9795
  if (base.name !== null && isSafeRemoteName(base.name)) {
8167
9796
  const head = await readGitFile(
8168
9797
  io,
8169
- path16.join(gitFolder, "refs", "remotes", base.name, "HEAD")
9798
+ path20.join(gitFolder, "refs", "remotes", base.name, "HEAD")
8170
9799
  );
8171
9800
  if (typeof head === "string") defaultBranch = parseRemoteHead(head, base.name);
8172
9801
  }
@@ -8337,7 +9966,7 @@ function createClearHistoryRoute(options) {
8337
9966
  }
8338
9967
 
8339
9968
  // src/collector/history/historyKeeper.ts
8340
- import path17 from "node:path";
9969
+ import path21 from "node:path";
8341
9970
 
8342
9971
  // src/collector/history/historyFiles.ts
8343
9972
  import {
@@ -8637,7 +10266,7 @@ function createHistoryKeeper(options) {
8637
10266
  });
8638
10267
  const lock = createWriterLock({
8639
10268
  fs,
8640
- file: path17.join(dir, LOCK_FILE),
10269
+ file: path21.join(dir, LOCK_FILE),
8641
10270
  pid: options.pid,
8642
10271
  now,
8643
10272
  isAlive: options.isAlive,
@@ -8663,7 +10292,7 @@ function createHistoryKeeper(options) {
8663
10292
  });
8664
10293
  return next;
8665
10294
  }
8666
- const full = (name) => path17.join(dir, name);
10295
+ const full = (name) => path21.join(dir, name);
8667
10296
  function hold(record) {
8668
10297
  if (unusable !== null || !running) return;
8669
10298
  const line = encodeRecord(record);
@@ -8830,13 +10459,13 @@ function createHistoryKeeper(options) {
8830
10459
  continue;
8831
10460
  }
8832
10461
  let text4 = lead;
8833
- let bytes2 = lead.length;
10462
+ let bytes3 = lead.length;
8834
10463
  let end = index;
8835
10464
  while (end < lines.length) {
8836
10465
  const lineBytes = byteLength(lines[end]);
8837
- if (bytes2 - lead.length + lineBytes > room) break;
10466
+ if (bytes3 - lead.length + lineBytes > room) break;
8838
10467
  text4 += lines[end];
8839
- bytes2 += lineBytes;
10468
+ bytes3 += lineBytes;
8840
10469
  end += 1;
8841
10470
  }
8842
10471
  const outcome = yield { name, text: text4 };
@@ -8849,7 +10478,7 @@ function createHistoryKeeper(options) {
8849
10478
  const parsed = readHistoryFileName(name);
8850
10479
  files.set(name, {
8851
10480
  ...parsed,
8852
- size: size + bytes2,
10481
+ size: size + bytes3,
8853
10482
  first: known?.first ?? (record && { at: recordAt(record), kind: record.kind })
8854
10483
  });
8855
10484
  touched.add(name);
@@ -9077,8 +10706,8 @@ function createHistoryKeeper(options) {
9077
10706
  return { at: oldest.first.at, by: BEGINNING[oldest.first.kind] };
9078
10707
  },
9079
10708
  status() {
9080
- let bytes2 = 0;
9081
- for (const file of files.values()) bytes2 += file.size;
10709
+ let bytes3 = 0;
10710
+ for (const file of files.values()) bytes3 += file.size;
9082
10711
  let problem = unusable ?? failing;
9083
10712
  if (problem === null && writer === false) {
9084
10713
  problem = lock.heldByOtherSources ? "Another copy of Agent Lookout on this computer is writing the history, and it reads sessions from other folders than this one. This one keeps what it sees in memory, and takes over when that one stops." : "Another copy of Agent Lookout on this computer is writing the history. This one keeps what it sees in memory, and takes over when that one stops.";
@@ -9086,7 +10715,7 @@ function createHistoryKeeper(options) {
9086
10715
  return {
9087
10716
  where: "disk",
9088
10717
  folder,
9089
- bytes: bytes2,
10718
+ bytes: bytes3,
9090
10719
  maxBytes,
9091
10720
  maxAgeMs,
9092
10721
  canClear: unusable === null && writer === true && lock.held,
@@ -9123,15 +10752,15 @@ function keptHistoryStore(store, keeper) {
9123
10752
  }
9124
10753
 
9125
10754
  // src/collector/history/historySettings.ts
9126
- import { createHash as createHash3 } from "node:crypto";
9127
- import os8 from "node:os";
9128
- import path18 from "node:path";
10755
+ import { createHash as createHash4 } from "node:crypto";
10756
+ import os9 from "node:os";
10757
+ import path22 from "node:path";
9129
10758
  var HISTORY_ENV = "AGENT_LOOKOUT_HISTORY";
9130
10759
  var HISTORY_DIR_ENV = "AGENT_LOOKOUT_HISTORY_DIR";
9131
- function readHistorySetup(env, homeDir = os8.homedir()) {
10760
+ function readHistorySetup(env, homeDir = os9.homedir()) {
9132
10761
  if (env[HISTORY_ENV]?.trim().toLowerCase() === "off") return { on: false };
9133
10762
  const override = env[HISTORY_DIR_ENV]?.trim() || void 0;
9134
- const dir = path18.resolve(override ?? path18.join(homeDir, ".agent-lookout", "history"));
10763
+ const dir = path22.resolve(override ?? path22.join(homeDir, ".agent-lookout", "history"));
9135
10764
  return {
9136
10765
  on: true,
9137
10766
  dir,
@@ -9139,14 +10768,15 @@ function readHistorySetup(env, homeDir = os8.homedir()) {
9139
10768
  sources: sourcesFingerprint(env, homeDir)
9140
10769
  };
9141
10770
  }
9142
- function sourcesFingerprint(env, homeDir = os8.homedir()) {
10771
+ function sourcesFingerprint(env, homeDir = os9.homedir()) {
9143
10772
  const set = (name) => env[name]?.trim() || void 0;
9144
10773
  const folders = [
9145
- path18.resolve(set(CLAUDE_HOME_ENV) ?? path18.join(homeDir, ".claude")),
9146
- path18.resolve(set(CODEX_HOME_ENV) ?? set(CODEX_OWN_HOME_ENV) ?? path18.join(homeDir, ".codex")),
9147
- path18.resolve(set(STATUS_DIR_ENV) ?? path18.join(homeDir, ".agent-lookout", "sessions"))
10774
+ path22.resolve(set(CLAUDE_HOME_ENV) ?? path22.join(homeDir, ".claude")),
10775
+ path22.resolve(set(CODEX_HOME_ENV) ?? set(CODEX_OWN_HOME_ENV) ?? path22.join(homeDir, ".codex")),
10776
+ path22.resolve(set(STATUS_DIR_ENV) ?? path22.join(homeDir, ".agent-lookout", "sessions")),
10777
+ path22.resolve(set(ANTIGRAVITY_HOME_ENV) ?? path22.join(homeDir, ...DEFAULT_HOME))
9148
10778
  ];
9149
- return createHash3("sha256").update(JSON.stringify(folders)).digest("hex").slice(0, 32);
10779
+ return createHash4("sha256").update(JSON.stringify(folders)).digest("hex").slice(0, 32);
9150
10780
  }
9151
10781
 
9152
10782
  // src/collector/notifications/heldWait.ts
@@ -9187,9 +10817,9 @@ function createServerNotifications(options) {
9187
10817
  const held = /* @__PURE__ */ new Map();
9188
10818
  const reminders = createReminderWatch();
9189
10819
  const quiet = createQuietHold();
9190
- function show(notice) {
10820
+ function show(notice, about) {
9191
10821
  try {
9192
- notifier.show(notice);
10822
+ notifier.show(notice, about);
9193
10823
  } catch {
9194
10824
  }
9195
10825
  }
@@ -9217,7 +10847,7 @@ function createServerNotifications(options) {
9217
10847
  const at = now();
9218
10848
  const rules = rulesOf(snapshot);
9219
10849
  const quietNow = quietOf(snapshot);
9220
- const thresholdMs = rules.longWait.minutes * 6e4;
10850
+ const schedule = reminderSchedule(rules.longWait);
9221
10851
  const result = sessionChanges(memory, snapshot);
9222
10852
  memory = result.memory;
9223
10853
  for (const id of result.stopped) {
@@ -9243,7 +10873,7 @@ function createServerNotifications(options) {
9243
10873
  change: { event: "needs-you", session },
9244
10874
  since: at
9245
10875
  });
9246
- if (rules.longWait.on) reminders.toldLate(session.id, at, thresholdMs);
10876
+ if (rules.longWait.on) reminders.told(session.id, at);
9247
10877
  }
9248
10878
  }
9249
10879
  if (quietNow) quiet.begin(at);
@@ -9253,9 +10883,13 @@ function createServerNotifications(options) {
9253
10883
  else
9254
10884
  held.set(heldKey(change.event, change.session.id), { kind: "change", change, since: at });
9255
10885
  }
9256
- if (!quietNow && rules.longWait.on && wants("needs-you")) {
9257
- for (const reminder of reminders.due(snapshot, at, thresholdMs)) {
9258
- reminders.reminded(reminder.session.id, thresholdMs);
10886
+ if (rules.longWait.on && wants("needs-you")) {
10887
+ for (const reminder of reminders.due(snapshot, at, schedule)) {
10888
+ if (quietNow) {
10889
+ quiet.holdReminder(reminder.session, reminder.begunAt);
10890
+ continue;
10891
+ }
10892
+ reminders.told(reminder.session.id, at);
9259
10893
  held.set(reminderKey(reminder.session.id), { kind: "reminder", reminder, since: at });
9260
10894
  }
9261
10895
  }
@@ -9282,82 +10916,505 @@ function createServerNotifications(options) {
9282
10916
  }
9283
10917
  continue;
9284
10918
  }
9285
- if (entry.kind === "change") show(changeNotice(entry.change));
9286
- else if (entry.kind === "reminder") {
9287
- show(reminderNotice(entry.reminder.session, at - entry.reminder.begunAt));
10919
+ if (entry.kind === "change") {
10920
+ const { change } = entry;
10921
+ show(
10922
+ changeNotice(change),
10923
+ change.event === "needs-you" ? { sessionId: change.session.id } : void 0
10924
+ );
10925
+ } else if (entry.kind === "reminder") {
10926
+ const { session, begunAt } = entry.reminder;
10927
+ const waitedMs = at - begunAt;
10928
+ show(reminderNotice(session, waitedMs), { sessionId: session.id, waitedMs });
9288
10929
  } else show(summaryNotice(entry.summary.items));
9289
10930
  }
9290
10931
  }
9291
10932
  };
9292
10933
  }
9293
10934
 
9294
- // src/collector/processes/osascript.ts
9295
- import { execFile as execFile3 } from "node:child_process";
9296
- var OSASCRIPT = "/usr/bin/osascript";
9297
- var MAX_OUTPUT_BYTES4 = 64 * 1024;
9298
- var NOT_RUN2 = { ok: false, stderr: "", timedOut: false };
9299
- function runOsascript(args, options = {}) {
9300
- return new Promise((resolve) => {
10935
+ // src/collector/processes/osascript.ts
10936
+ import { execFile as execFile3 } from "node:child_process";
10937
+ var OSASCRIPT = "/usr/bin/osascript";
10938
+ var MAX_OUTPUT_BYTES4 = 64 * 1024;
10939
+ var NOT_RUN2 = { ok: false, stderr: "", timedOut: false };
10940
+ function runOsascript(args, options = {}) {
10941
+ return new Promise((resolve) => {
10942
+ try {
10943
+ const child = execFile3(
10944
+ OSASCRIPT,
10945
+ [...args],
10946
+ {
10947
+ timeout: options.timeoutMs ?? TERMINAL_JUMP_TIMEOUT_MS,
10948
+ maxBuffer: MAX_OUTPUT_BYTES4,
10949
+ encoding: "utf8",
10950
+ windowsHide: true
10951
+ },
10952
+ (error, stdout, stderr) => {
10953
+ if (!error) {
10954
+ resolve({ ok: true, stdout: String(stdout ?? "") });
10955
+ return;
10956
+ }
10957
+ const timedOut = error.killed === true;
10958
+ resolve({ ok: false, stderr: String(stderr ?? "").trim(), timedOut });
10959
+ }
10960
+ );
10961
+ child.stdin?.end();
10962
+ } catch {
10963
+ resolve(NOT_RUN2);
10964
+ }
10965
+ });
10966
+ }
10967
+
10968
+ // src/collector/notifications/systemNotifier.ts
10969
+ var OSASCRIPT_TIMEOUT_MS = 5e3;
10970
+ var SCRIPT = [
10971
+ "on run argv",
10972
+ "display notification (item 2 of argv) with title (item 1 of argv)",
10973
+ "end run"
10974
+ ];
10975
+ function osascriptArgs(notice) {
10976
+ return [
10977
+ ...SCRIPT.flatMap((line) => ["-e", line]),
10978
+ "--",
10979
+ oneLine(notice.title),
10980
+ oneLine(notice.body, MAX_NOTICE_TEXT_LENGTH)
10981
+ ];
10982
+ }
10983
+ function systemNotificationsShownOn(platform) {
10984
+ return platform === "darwin";
10985
+ }
10986
+ var runForNotification = (args) => runOsascript(args, { timeoutMs: OSASCRIPT_TIMEOUT_MS });
10987
+ function createSystemNotifier(options = {}) {
10988
+ const platform = options.platform ?? process.platform;
10989
+ const run = options.run ?? runForNotification;
10990
+ return {
10991
+ show(notice) {
10992
+ if (!systemNotificationsShownOn(platform)) return;
10993
+ try {
10994
+ run(osascriptArgs(notice)).catch(() => {
10995
+ });
10996
+ } catch {
10997
+ }
10998
+ }
10999
+ };
11000
+ }
11001
+
11002
+ // src/collector/outbound/httpPost.ts
11003
+ var POST_TIMEOUT_MS = 1e4;
11004
+ function jsonHeaders(body, version, extra = {}) {
11005
+ return {
11006
+ "Content-Type": "application/json",
11007
+ "Content-Length": String(Buffer.byteLength(body)),
11008
+ "User-Agent": `Agent Lookout/${version}`,
11009
+ ...extra
11010
+ };
11011
+ }
11012
+ function postOptions(body, version, extra = {}) {
11013
+ return {
11014
+ method: "POST",
11015
+ headers: jsonHeaders(body, version, extra),
11016
+ // A connection of its own, closed after the answer.
11017
+ agent: false,
11018
+ rejectUnauthorized: true
11019
+ };
11020
+ }
11021
+ function createJsonPoster(url, options) {
11022
+ const timeoutMs = options.timeoutMs ?? POST_TIMEOUT_MS;
11023
+ const extra = options.headers ?? {};
11024
+ let client = null;
11025
+ const load = () => {
11026
+ client ??= url.protocol === "https:" ? import("node:https").then(({ request: request2 }) => request2) : import("node:http").then(({ request: request2 }) => request2);
11027
+ return client;
11028
+ };
11029
+ return {
11030
+ async post(body) {
11031
+ let request2;
11032
+ try {
11033
+ request2 = await load();
11034
+ } catch {
11035
+ return { kind: "unsent" };
11036
+ }
11037
+ return new Promise((resolve) => {
11038
+ let finished = false;
11039
+ const finish = (result) => {
11040
+ if (finished) return;
11041
+ finished = true;
11042
+ resolve(result);
11043
+ };
11044
+ let req;
11045
+ try {
11046
+ req = request2(url, postOptions(body, options.version, extra));
11047
+ } catch {
11048
+ finish({ kind: "unsent" });
11049
+ return;
11050
+ }
11051
+ const timer = setTimeout(() => {
11052
+ finish({ kind: "timeout" });
11053
+ req.destroy();
11054
+ }, timeoutMs);
11055
+ timer.unref();
11056
+ req.on("response", (res) => {
11057
+ finish({ kind: "status", status: res.statusCode ?? 0 });
11058
+ res.on("close", () => clearTimeout(timer));
11059
+ res.resume();
11060
+ });
11061
+ req.on("error", (error) => {
11062
+ clearTimeout(timer);
11063
+ const { code } = error ?? {};
11064
+ finish({ kind: "error", code: typeof code === "string" ? code : null });
11065
+ });
11066
+ req.end(body);
11067
+ });
11068
+ }
11069
+ };
11070
+ }
11071
+ function connectionReason(code, words) {
11072
+ if (code === null) return words.notSent;
11073
+ if (code === "ECONNREFUSED") return `nothing answered at ${words.where}`;
11074
+ if (code === "ENOTFOUND" || code === "EAI_AGAIN") return `${words.whose} host could not be found`;
11075
+ if (code === "ETIMEDOUT") return `${words.where} did not answer in time`;
11076
+ if (/CERT|SELF_SIGNED|UNABLE_TO|^ERR_TLS|^ERR_SSL|^EPROTO$/.test(code)) {
11077
+ return `the secure connection to ${words.where} failed`;
11078
+ }
11079
+ return `the connection to ${words.where} failed`;
11080
+ }
11081
+ function outcomeOfPost(result, words) {
11082
+ switch (result.kind) {
11083
+ case "unsent":
11084
+ return { sent: false, reason: words.notSent };
11085
+ case "timeout":
11086
+ return { sent: false, reason: `${words.where} did not answer in time` };
11087
+ case "error":
11088
+ return { sent: false, reason: connectionReason(result.code, words) };
11089
+ case "status": {
11090
+ const { status } = result;
11091
+ if (status >= 200 && status < 300) return { sent: true };
11092
+ if (status >= 300 && status < 400) {
11093
+ return {
11094
+ sent: false,
11095
+ reason: `${words.where} answered with a redirect, which is not followed`
11096
+ };
11097
+ }
11098
+ if (status >= 400 && status < 500) return { sent: false, reason: words.refused(status) };
11099
+ if (status >= 500 && status < 600) return { sent: false, reason: words.troubled(status) };
11100
+ return { sent: false, reason: `${words.whose} answer could not be understood` };
11101
+ }
11102
+ }
11103
+ }
11104
+
11105
+ // src/collector/ntfy/ntfyMessage.ts
11106
+ var TAG = {
11107
+ "needs-you": "hourglass",
11108
+ reminder: "hourglass",
11109
+ test: "hourglass",
11110
+ finished: "white_check_mark",
11111
+ failed: "x",
11112
+ ended: "stop_sign",
11113
+ "quiet-summary": null
11114
+ };
11115
+ function ntfyBody(topic, message) {
11116
+ const tag = TAG[message.kind];
11117
+ return {
11118
+ topic,
11119
+ title: message.title,
11120
+ message: message.message,
11121
+ // A test sounds as a wait would, so it shows how one will arrive.
11122
+ priority: isWaitKind(message.kind) ? 4 : 3,
11123
+ ...tag !== null && { tags: [tag] }
11124
+ };
11125
+ }
11126
+
11127
+ // src/collector/ntfy/ntfySender.ts
11128
+ var NTFY_WORDS = {
11129
+ where: "the ntfy server",
11130
+ whose: "the ntfy server's",
11131
+ notSent: "the push could not be sent",
11132
+ refused(status) {
11133
+ if (status === 401 || status === 403) {
11134
+ return `the ntfy server refused the access token or the topic (status ${status})`;
11135
+ }
11136
+ if (status === 413) return `the ntfy server refused the push as too large (status ${status})`;
11137
+ if (status === 429) return `the ntfy server's rate limit was reached (status ${status})`;
11138
+ return `the ntfy server refused the push (status ${status})`;
11139
+ },
11140
+ troubled: (status) => `the ntfy server had a problem (status ${status})`
11141
+ };
11142
+ function ntfyHeaders(settings) {
11143
+ return settings.token === null ? {} : { Authorization: `Bearer ${settings.token}` };
11144
+ }
11145
+ function createNtfySender(settings, options) {
11146
+ const poster = createJsonPoster(settings.server, { ...options, headers: ntfyHeaders(settings) });
11147
+ return {
11148
+ async send(message) {
11149
+ let body;
11150
+ try {
11151
+ body = JSON.stringify(ntfyBody(settings.topic, message));
11152
+ } catch {
11153
+ return { sent: false, reason: NTFY_WORDS.notSent };
11154
+ }
11155
+ return outcomeOfPost(await poster.post(body), NTFY_WORDS);
11156
+ }
11157
+ };
11158
+ }
11159
+
11160
+ // src/collector/ntfy/ntfySettings.ts
11161
+ var NTFY_URL_ENV = "AGENT_LOOKOUT_NTFY_URL";
11162
+ var NTFY_TOKEN_ENV = "AGENT_LOOKOUT_NTFY_TOKEN";
11163
+ var NTFY_EVENTS_ENV = "AGENT_LOOKOUT_NTFY_EVENTS";
11164
+ var NTFY_AFTER_ENV = "AGENT_LOOKOUT_NTFY_AFTER";
11165
+ var NTFY_ASKING_ENV = "AGENT_LOOKOUT_NTFY_ASKING";
11166
+ var MAX_URL_LENGTH = 2048;
11167
+ var TOPIC = /^[-_A-Za-z0-9]{1,64}$/;
11168
+ var PATH_PART = /^[-_.~A-Za-z0-9]+$/;
11169
+ var TOKEN = /^[-A-Za-z0-9._~+/=]{1,128}$/;
11170
+ var URL_UNREADABLE2 = `${NTFY_URL_ENV} could not be read. Copy the topic's whole address, beginning https://.`;
11171
+ function readTopicUrl(value) {
11172
+ let url;
11173
+ try {
11174
+ if (value.length > MAX_URL_LENGTH) throw new Error();
11175
+ url = new URL(value);
11176
+ } catch {
11177
+ throw new SettingProblem(URL_UNREADABLE2);
11178
+ }
11179
+ const local = LOCAL_HOSTS.has(url.hostname);
11180
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && local)) {
11181
+ throw new SettingProblem(
11182
+ `${NTFY_URL_ENV} must begin with https://, or with http:// for a server on this computer, 127.0.0.1 or localhost.`
11183
+ );
11184
+ }
11185
+ if (url.hostname === "") throw new SettingProblem(URL_UNREADABLE2);
11186
+ if (!isWebhookHost(url.hostname)) {
11187
+ throw new SettingProblem(
11188
+ `${NTFY_URL_ENV} must name its host in letters, digits, dots and dashes, such as ntfy.sh.`
11189
+ );
11190
+ }
11191
+ if (url.username !== "" || url.password !== "") {
11192
+ throw new SettingProblem(
11193
+ `${NTFY_URL_ENV} must not hold a user name or a password. Put an access token in ${NTFY_TOKEN_ENV}.`
11194
+ );
11195
+ }
11196
+ if (url.search !== "" || url.hash !== "" || value.includes("?") || value.includes("#")) {
11197
+ throw new SettingProblem(
11198
+ `${NTFY_URL_ENV} must end with the topic, with no ? or # after it. Put an access token in ${NTFY_TOKEN_ENV}.`
11199
+ );
11200
+ }
11201
+ const parts = url.pathname.split("/").filter((part) => part !== "");
11202
+ const topic = parts.pop();
11203
+ if (topic === void 0 || !TOPIC.test(topic) || !parts.every((part) => PATH_PART.test(part))) {
11204
+ throw new SettingProblem(
11205
+ `${NTFY_URL_ENV} must end with the topic, up to 64 letters, digits, dashes and underscores, as in https://ntfy.sh/your-topic.`
11206
+ );
11207
+ }
11208
+ const server = new URL(url.origin);
11209
+ server.pathname = `/${parts.map((part) => `${part}/`).join("")}`;
11210
+ return { server, topic };
11211
+ }
11212
+ function readToken(value) {
11213
+ if (value === void 0) return null;
11214
+ if (!TOKEN.test(value)) {
11215
+ throw new SettingProblem(
11216
+ `${NTFY_TOKEN_ENV} must be an access token of letters, digits and . _ ~ + / = -, such as one that begins tk_.`
11217
+ );
11218
+ }
11219
+ return value;
11220
+ }
11221
+ function readNtfySetup(env) {
11222
+ const url = valueOf(env, NTFY_URL_ENV);
11223
+ if (url === void 0) return { on: false, problem: null };
11224
+ try {
11225
+ return {
11226
+ on: true,
11227
+ settings: {
11228
+ ...readTopicUrl(url),
11229
+ token: readToken(valueOf(env, NTFY_TOKEN_ENV)),
11230
+ events: readEvents(NTFY_EVENTS_ENV, valueOf(env, NTFY_EVENTS_ENV)),
11231
+ afterMs: readAfter(NTFY_AFTER_ENV, valueOf(env, NTFY_AFTER_ENV)),
11232
+ asking: readAsking(NTFY_ASKING_ENV, valueOf(env, NTFY_ASKING_ENV))
11233
+ }
11234
+ };
11235
+ } catch (error) {
11236
+ if (error instanceof SettingProblem) return { on: false, problem: error.message };
11237
+ return { on: false, problem: URL_UNREADABLE2 };
11238
+ }
11239
+ }
11240
+ function ntfyProblemLine(problem) {
11241
+ return `ntfy pushes are off: ${problem}`;
11242
+ }
11243
+
11244
+ // src/collector/outbound/testSendRoute.ts
11245
+ var MAX_TEST_BODY_BYTES = 64;
11246
+ var NAMED = {
11247
+ ntfy: { name: "ntfy", setting: "AGENT_LOOKOUT_NTFY_URL" },
11248
+ pushover: {
11249
+ name: "Pushover",
11250
+ setting: "AGENT_LOOKOUT_PUSHOVER_TOKEN and AGENT_LOOKOUT_PUSHOVER_USER"
11251
+ }
11252
+ };
11253
+ function channelIn(body) {
11254
+ let value;
11255
+ try {
11256
+ value = JSON.parse(body);
11257
+ } catch {
11258
+ return null;
11259
+ }
11260
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
11261
+ const keys = Object.keys(value);
11262
+ if (keys.length !== 1 || keys[0] !== "channel") return null;
11263
+ const { channel } = value;
11264
+ return PHONE_CHANNELS.find((known) => known === channel) ?? null;
11265
+ }
11266
+ function asSentence(reason) {
11267
+ return `${reason.charAt(0).toUpperCase()}${reason.slice(1)}.`;
11268
+ }
11269
+ var failed3 = (status, reason, error, more = {}) => ({
11270
+ status,
11271
+ body: {
11272
+ error,
11273
+ reason,
11274
+ ...more.limitedUntil !== void 0 && { limitedUntil: more.limitedUntil }
11275
+ },
11276
+ ...more.retryAfter !== void 0 && {
11277
+ headers: { "Retry-After": String(more.retryAfter) }
11278
+ }
11279
+ });
11280
+ function createTestSendRoute(options) {
11281
+ const { channels, limiter } = options;
11282
+ const now = options.now ?? Date.now;
11283
+ return async function answerTest(req) {
11284
+ const refused = actionRefusalFor(req, PHONE_TEST_ACTION, MAX_TEST_BODY_BYTES);
11285
+ if (refused) {
11286
+ req.resume();
11287
+ return refused;
11288
+ }
11289
+ const body = await readRequestBody(req, MAX_TEST_BODY_BYTES);
11290
+ if (!body.ok) {
11291
+ req.resume();
11292
+ return body.tooLarge ? refusal(413, `The body must be no more than ${MAX_TEST_BODY_BYTES} bytes.`) : refusal(400, "The body could not be read.");
11293
+ }
11294
+ const channel = channelIn(body.text);
11295
+ if (channel === null) {
11296
+ return refusal(400, 'The body must be {"channel": "ntfy"} or {"channel": "pushover"}.');
11297
+ }
11298
+ const { name, setting } = NAMED[channel];
11299
+ const tester = channels[channel];
11300
+ if (tester === null) {
11301
+ return failed3(
11302
+ 409,
11303
+ "off",
11304
+ `${name} is not set up. Set ${setting} and start Agent Lookout again.`
11305
+ );
11306
+ }
11307
+ if (!limiter.begin()) {
11308
+ return failed3(429, "too-soon", "One test is sent a second. Try again in a moment.", {
11309
+ retryAfter: 1
11310
+ });
11311
+ }
9301
11312
  try {
9302
- const child = execFile3(
9303
- OSASCRIPT,
9304
- [...args],
9305
- {
9306
- timeout: options.timeoutMs ?? TERMINAL_JUMP_TIMEOUT_MS,
9307
- maxBuffer: MAX_OUTPUT_BYTES4,
9308
- encoding: "utf8",
9309
- windowsHide: true
9310
- },
9311
- (error, stdout, stderr) => {
9312
- if (!error) {
9313
- resolve({ ok: true, stdout: String(stdout ?? "") });
9314
- return;
11313
+ const result = await tester.test();
11314
+ if (!result.tried) {
11315
+ return failed3(
11316
+ 429,
11317
+ "limited",
11318
+ `${SENDS_PER_HOUR} ${name} pushes were tried in the last hour, the most it tries.`,
11319
+ {
11320
+ limitedUntil: result.limitedUntil,
11321
+ retryAfter: Math.max(1, Math.ceil((result.limitedUntil - now()) / 1e3))
9315
11322
  }
9316
- const timedOut = error.killed === true;
9317
- resolve({ ok: false, stderr: String(stderr ?? "").trim(), timedOut });
9318
- }
9319
- );
9320
- child.stdin?.end();
9321
- } catch {
9322
- resolve(NOT_RUN2);
11323
+ );
11324
+ }
11325
+ if (!result.outcome.sent) {
11326
+ return failed3(502, "not-sent", asSentence(result.outcome.reason));
11327
+ }
11328
+ return {
11329
+ status: 200,
11330
+ body: { ok: true, channel, sentAt: result.at }
11331
+ };
11332
+ } finally {
11333
+ limiter.end();
9323
11334
  }
9324
- });
11335
+ };
9325
11336
  }
9326
11337
 
9327
- // src/collector/notifications/systemNotifier.ts
9328
- var OSASCRIPT_TIMEOUT_MS = 5e3;
9329
- var SCRIPT = [
9330
- "on run argv",
9331
- "display notification (item 2 of argv) with title (item 1 of argv)",
9332
- "end run"
9333
- ];
9334
- function osascriptArgs(notice) {
9335
- return [
9336
- ...SCRIPT.flatMap((line) => ["-e", line]),
9337
- "--",
9338
- oneLine(notice.title),
9339
- oneLine(notice.body, MAX_NOTICE_TEXT_LENGTH)
9340
- ];
9341
- }
9342
- function systemNotificationsShownOn(platform) {
9343
- return platform === "darwin";
11338
+ // src/collector/pushover/pushoverMessage.ts
11339
+ function pushoverBody(token, user, message) {
11340
+ return {
11341
+ token,
11342
+ user,
11343
+ title: message.title,
11344
+ message: message.message,
11345
+ // A test sounds as a wait would, so it shows how one will arrive.
11346
+ priority: isWaitKind(message.kind) ? 0 : -1
11347
+ };
9344
11348
  }
9345
- var runForNotification = (args) => runOsascript(args, { timeoutMs: OSASCRIPT_TIMEOUT_MS });
9346
- function createSystemNotifier(options = {}) {
9347
- const platform = options.platform ?? process.platform;
9348
- const run = options.run ?? runForNotification;
11349
+
11350
+ // src/collector/pushover/pushoverSender.ts
11351
+ var PUSHOVER_ENDPOINT = "https://api.pushover.net/1/messages.json";
11352
+ var PUSHOVER_WORDS = {
11353
+ where: "Pushover",
11354
+ whose: "Pushover's",
11355
+ notSent: "the push could not be sent",
11356
+ refused(status) {
11357
+ if (status === 400) {
11358
+ return `Pushover refused the push: check the application's token and the user key (status ${status})`;
11359
+ }
11360
+ if (status === 429)
11361
+ return `Pushover's monthly limit for your account was reached (status ${status})`;
11362
+ return `Pushover refused the push (status ${status})`;
11363
+ },
11364
+ troubled: (status) => `Pushover had a problem (status ${status})`
11365
+ };
11366
+ function createPushoverSender(settings, options) {
11367
+ const poster = createJsonPoster(options.endpoint ?? new URL(PUSHOVER_ENDPOINT), options);
9349
11368
  return {
9350
- show(notice) {
9351
- if (!systemNotificationsShownOn(platform)) return;
11369
+ async send(message) {
11370
+ let body;
9352
11371
  try {
9353
- run(osascriptArgs(notice)).catch(() => {
9354
- });
11372
+ body = JSON.stringify(pushoverBody(settings.token, settings.user, message));
9355
11373
  } catch {
11374
+ return { sent: false, reason: PUSHOVER_WORDS.notSent };
9356
11375
  }
11376
+ return outcomeOfPost(await poster.post(body), PUSHOVER_WORDS);
9357
11377
  }
9358
11378
  };
9359
11379
  }
9360
11380
 
11381
+ // src/collector/pushover/pushoverSettings.ts
11382
+ var PUSHOVER_TOKEN_ENV = "AGENT_LOOKOUT_PUSHOVER_TOKEN";
11383
+ var PUSHOVER_USER_ENV = "AGENT_LOOKOUT_PUSHOVER_USER";
11384
+ var PUSHOVER_EVENTS_ENV = "AGENT_LOOKOUT_PUSHOVER_EVENTS";
11385
+ var PUSHOVER_AFTER_ENV = "AGENT_LOOKOUT_PUSHOVER_AFTER";
11386
+ var PUSHOVER_ASKING_ENV = "AGENT_LOOKOUT_PUSHOVER_ASKING";
11387
+ var KEY = /^[A-Za-z0-9]{30}$/;
11388
+ function readKey(name, value, what) {
11389
+ if (!KEY.test(value)) throw new SettingProblem(`${name} must be ${what}: 30 letters and digits.`);
11390
+ return value;
11391
+ }
11392
+ function readPushoverSetup(env) {
11393
+ const token = valueOf(env, PUSHOVER_TOKEN_ENV);
11394
+ const user = valueOf(env, PUSHOVER_USER_ENV);
11395
+ if (token === void 0 && user === void 0) return { on: false, problem: null };
11396
+ if (token === void 0) return { on: false, problem: `${PUSHOVER_TOKEN_ENV} is not set.` };
11397
+ if (user === void 0) return { on: false, problem: `${PUSHOVER_USER_ENV} is not set.` };
11398
+ try {
11399
+ return {
11400
+ on: true,
11401
+ settings: {
11402
+ token: readKey(PUSHOVER_TOKEN_ENV, token, "the API token of your Pushover application"),
11403
+ user: readKey(PUSHOVER_USER_ENV, user, "your Pushover user key, or a group key"),
11404
+ events: readEvents(PUSHOVER_EVENTS_ENV, valueOf(env, PUSHOVER_EVENTS_ENV)),
11405
+ afterMs: readAfter(PUSHOVER_AFTER_ENV, valueOf(env, PUSHOVER_AFTER_ENV)),
11406
+ asking: readAsking(PUSHOVER_ASKING_ENV, valueOf(env, PUSHOVER_ASKING_ENV))
11407
+ }
11408
+ };
11409
+ } catch (error) {
11410
+ if (error instanceof SettingProblem) return { on: false, problem: error.message };
11411
+ return { on: false, problem: `${PUSHOVER_TOKEN_ENV} could not be read.` };
11412
+ }
11413
+ }
11414
+ function pushoverProblemLine(problem) {
11415
+ return `Pushover pushes are off: ${problem}`;
11416
+ }
11417
+
9361
11418
  // src/collector/remotes/remoteReader.ts
9362
11419
  import { request } from "node:http";
9363
11420
  var REMOTE_ANSWER_TIMEOUT_MS = 6e3;
@@ -9434,7 +11491,7 @@ var MAX_REMOTE_SOURCES = 20;
9434
11491
  var MAX_ID_LENGTH = 512;
9435
11492
  var MAX_PATH_LENGTH = 4096;
9436
11493
  var MAX_WORD_LENGTH2 = 200;
9437
- function isRecord12(value) {
11494
+ function isRecord13(value) {
9438
11495
  return typeof value === "object" && value !== null && !Array.isArray(value);
9439
11496
  }
9440
11497
  function oneOf2(allowed, value) {
@@ -9448,7 +11505,7 @@ function exact(value, max) {
9448
11505
  var COMMIT_ID2 = /^[0-9a-f]{7,64}$/;
9449
11506
  var REPOSITORY_ID = /^[0-9a-f]{16,64}$/;
9450
11507
  function readGit(value, machine) {
9451
- if (!isRecord12(value)) return void 0;
11508
+ if (!isRecord13(value)) return void 0;
9452
11509
  let head;
9453
11510
  if (value.commit !== void 0) {
9454
11511
  if (value.branch !== void 0 || typeof value.commit !== "string") return void 0;
@@ -9460,7 +11517,7 @@ function readGit(value, machine) {
9460
11517
  head = { branch };
9461
11518
  }
9462
11519
  const repository = value.repository;
9463
- if (isRecord12(repository) && typeof repository.id === "string") {
11520
+ if (isRecord13(repository) && typeof repository.id === "string") {
9464
11521
  const name = sessionName(typeof repository.name === "string" ? repository.name : "");
9465
11522
  if (name !== void 0 && REPOSITORY_ID.test(repository.id)) {
9466
11523
  head.repository = { id: repositoryId(`${machine}
@@ -9470,7 +11527,7 @@ ${repository.id}`), name };
9470
11527
  return head;
9471
11528
  }
9472
11529
  function readSession(value, machine, sources, now, keepWaitingText) {
9473
- if (!isRecord12(value)) return null;
11530
+ if (!isRecord13(value)) return null;
9474
11531
  const id = exact(value.id, MAX_ID_LENGTH);
9475
11532
  const source = exact(value.source, MAX_WORD_LENGTH2);
9476
11533
  if (id === null || source === null) return null;
@@ -9510,7 +11567,7 @@ function readSession(value, machine, sources, now, keepWaitingText) {
9510
11567
  return session;
9511
11568
  }
9512
11569
  function readCell(value) {
9513
- if (!isRecord12(value)) return null;
11570
+ if (!isRecord13(value)) return null;
9514
11571
  if (value.level === "yes") return { level: "yes" };
9515
11572
  const level = oneOf2(["no", "partly"], value.level);
9516
11573
  const reason = cut(value.reason, MAX_WORD_LENGTH2);
@@ -9518,7 +11575,7 @@ function readCell(value) {
9518
11575
  }
9519
11576
  var ACTS_HERE = ["jump", "stop", "answer"];
9520
11577
  function readCapabilities(value, machine) {
9521
- if (!isRecord12(value)) return null;
11578
+ if (!isRecord13(value)) return null;
9522
11579
  const read = {};
9523
11580
  for (const capability of CAPABILITIES) {
9524
11581
  if (ACTS_HERE.includes(capability)) continue;
@@ -9534,7 +11591,7 @@ function readCapabilities(value, machine) {
9534
11591
  };
9535
11592
  }
9536
11593
  function readRemoteSnapshot(value, machine, now, options = {}) {
9537
- if (!isRecord12(value) || !Array.isArray(value.sessions) || !Array.isArray(value.sources)) {
11594
+ if (!isRecord13(value) || !Array.isArray(value.sessions) || !Array.isArray(value.sources)) {
9538
11595
  return null;
9539
11596
  }
9540
11597
  const named = /* @__PURE__ */ new Map();
@@ -9542,7 +11599,7 @@ function readRemoteSnapshot(value, machine, now, options = {}) {
9542
11599
  const agents = [];
9543
11600
  for (const item of value.sources) {
9544
11601
  if (sources.length >= MAX_REMOTE_SOURCES) break;
9545
- if (!isRecord12(item)) continue;
11602
+ if (!isRecord13(item)) continue;
9546
11603
  const id = exact(item.id, MAX_WORD_LENGTH2);
9547
11604
  if (id === null || isRemoteSource(id) || item.machine !== void 0) continue;
9548
11605
  const label2 = cut(item.label, MAX_WORD_LENGTH2) ?? id;
@@ -9567,7 +11624,7 @@ function readRemoteSnapshot(value, machine, now, options = {}) {
9567
11624
  }
9568
11625
 
9569
11626
  // src/collector/remotes/sshProgram.ts
9570
- import path19 from "node:path";
11627
+ import path23 from "node:path";
9571
11628
  var SSH_BIN_ENV = "AGENT_LOOKOUT_SSH_BIN";
9572
11629
  var SSH_LOCATIONS = ["/usr/bin/ssh", "/opt/homebrew/bin/ssh", "/usr/local/bin/ssh"];
9573
11630
  var SERVER_ALIVE_SECONDS = 15;
@@ -9589,7 +11646,7 @@ async function findSsh(env, isExecutable, platform = process.platform) {
9589
11646
  }
9590
11647
  return {
9591
11648
  found: false,
9592
- looked: `The ssh command was not found on PATH or in ${SSH_LOCATIONS.map((at) => path19.dirname(at)).join(", ")}`
11649
+ looked: `The ssh command was not found on PATH or in ${SSH_LOCATIONS.map((at) => path23.dirname(at)).join(", ")}`
9593
11650
  };
9594
11651
  }
9595
11652
  function sshArguments(forward) {
@@ -9613,7 +11670,7 @@ function sshArguments(forward) {
9613
11670
  }
9614
11671
 
9615
11672
  // src/collector/remotes/remoteAdapter.ts
9616
- var BASIS3 = "api";
11673
+ var BASIS4 = "api";
9617
11674
  var NOT_CONNECTED = "unavailable";
9618
11675
  var ANSWER_WAIT_MS = 500;
9619
11676
  var STILL_CONNECTING_MS = 1e4;
@@ -9679,7 +11736,7 @@ function createRemoteAdapter(options) {
9679
11736
  if (found.advice) health.advice = found.advice;
9680
11737
  if (agents.length > 0) health.agents = agents;
9681
11738
  const sessions = state === "ok" && snapshot ? snapshot.sessions : [];
9682
- return state === "ok" ? { health, sessions, basis: BASIS3 } : { health, sessions };
11739
+ return state === "ok" ? { health, sessions, basis: BASIS4 } : { health, sessions };
9683
11740
  }
9684
11741
  function connecting(at) {
9685
11742
  if (problem === null) {
@@ -10184,15 +12241,15 @@ import {
10184
12241
  unlinkSync as unlinkSync2,
10185
12242
  writeSync as writeSync2
10186
12243
  } from "node:fs";
10187
- import os9 from "node:os";
10188
- import path20 from "node:path";
12244
+ import os10 from "node:os";
12245
+ import path24 from "node:path";
10189
12246
  var SETTINGS_FILE_ENV = "AGENT_LOOKOUT_SETTINGS_FILE";
10190
12247
  var SETTINGS_FILE_MODE = 384;
10191
12248
  var SETTINGS_FOLDER_MODE = 448;
10192
12249
  var MAX_SETTINGS_BYTES = 64 * 1024;
10193
- function readSettingsSetup(env, homeDir = os9.homedir()) {
12250
+ function readSettingsSetup(env, homeDir = os10.homedir()) {
10194
12251
  const named = env[SETTINGS_FILE_ENV]?.trim() || void 0;
10195
- const file = path20.resolve(named ?? path20.join(homeDir, ".agent-lookout", "settings.json"));
12252
+ const file = path24.resolve(named ?? path24.join(homeDir, ".agent-lookout", "settings.json"));
10196
12253
  return { file, shown: tildify(file, homeDir) };
10197
12254
  }
10198
12255
  function codeOf(error) {
@@ -10237,7 +12294,7 @@ function readSettingsStamp(setup) {
10237
12294
  }
10238
12295
  function readSettingsText(setup) {
10239
12296
  const { file, shown } = setup;
10240
- const folder = path20.dirname(file);
12297
+ const folder = path24.dirname(file);
10241
12298
  try {
10242
12299
  const folderKind = kindAt(folder);
10243
12300
  if (folderKind === null) return { kind: "missing" };
@@ -10296,7 +12353,7 @@ function readSettingsText(setup) {
10296
12353
  }
10297
12354
  function writeSettingsText(setup, text4) {
10298
12355
  const { file, shown } = setup;
10299
- const folder = path20.dirname(file);
12356
+ const folder = path24.dirname(file);
10300
12357
  const notSaved = {
10301
12358
  ok: false,
10302
12359
  problem: `${shown} could not be written, so the change was not saved.`
@@ -10317,9 +12374,9 @@ function writeSettingsText(setup, text4) {
10317
12374
  problem: `${shown} is a link or not an ordinary file, which Agent Lookout does not write through, so the change was not saved.`
10318
12375
  };
10319
12376
  }
10320
- temporary = path20.join(
12377
+ temporary = path24.join(
10321
12378
  folder,
10322
- `.${path20.basename(file)}.${process.pid}.${randomBytes3(6).toString("hex")}.tmp`
12379
+ `.${path24.basename(file)}.${process.pid}.${randomBytes3(6).toString("hex")}.tmp`
10323
12380
  );
10324
12381
  const fd = openWithoutFollowingNow(
10325
12382
  temporary,
@@ -10359,7 +12416,7 @@ var RULE_WORDS = {
10359
12416
  idle: "the idle rule",
10360
12417
  quietHours: "the quiet hours"
10361
12418
  };
10362
- function isRecord13(value) {
12419
+ function isRecord14(value) {
10363
12420
  return typeof value === "object" && value !== null && !Array.isArray(value);
10364
12421
  }
10365
12422
  function listOf(names) {
@@ -10397,7 +12454,7 @@ function rulesIn(read, shown) {
10397
12454
  } catch {
10398
12455
  value = null;
10399
12456
  }
10400
- if (!isRecord13(value)) {
12457
+ if (!isRecord14(value)) {
10401
12458
  const notJson = `${shown} is not JSON that Agent Lookout can read, so the time rules and the permission rules are off. Changing one in Settings writes the file again.`;
10402
12459
  return {
10403
12460
  rules: off,
@@ -10412,7 +12469,7 @@ function rulesIn(read, shown) {
10412
12469
  const { timeRules, permissionRules: heldRules, ...rest } = value;
10413
12470
  const permissions = readPermissionRules(heldRules);
10414
12471
  const { rules, unread } = readTimeRules(timeRules);
10415
- const otherRules = isRecord13(timeRules) ? Object.fromEntries(
12472
+ const otherRules = isRecord14(timeRules) ? Object.fromEntries(
10416
12473
  Object.entries(timeRules).filter(
10417
12474
  ([name]) => !TIME_RULE_NAMES.includes(name)
10418
12475
  )
@@ -10514,15 +12571,15 @@ import { randomBytes as randomBytes4 } from "node:crypto";
10514
12571
 
10515
12572
  // src/core/permission-rules/rulesChange.ts
10516
12573
  var CHANGE_SHAPE = 'The body must be one change: {"add": rule}, {"edit": rule with its "id"}, {"move": {"id", "to": "up" or "down"}} or {"remove": {"id"}}, and nothing else.';
10517
- function isRecord14(value) {
12574
+ function isRecord15(value) {
10518
12575
  return typeof value === "object" && value !== null && !Array.isArray(value);
10519
12576
  }
10520
12577
  function rulesChangeIn(value) {
10521
12578
  const refused = { ok: false, problem: CHANGE_SHAPE };
10522
- if (!isRecord14(value) || Object.keys(value).length !== 1) return refused;
12579
+ if (!isRecord15(value) || Object.keys(value).length !== 1) return refused;
10523
12580
  const [kind] = Object.keys(value);
10524
12581
  const body = value[kind];
10525
- if (!isRecord14(body)) return refused;
12582
+ if (!isRecord15(body)) return refused;
10526
12583
  switch (kind) {
10527
12584
  case "add": {
10528
12585
  const read = ruleWordsIn(body);
@@ -10598,7 +12655,7 @@ function applyRulesChange(rules, change, newId) {
10598
12655
 
10599
12656
  // src/collector/settings/permissionRulesRoute.ts
10600
12657
  var MAX_PERMISSION_RULES_BODY_BYTES = 1024;
10601
- var failed3 = (status, reason, error) => ({
12658
+ var failed4 = (status, reason, error) => ({
10602
12659
  status,
10603
12660
  body: { error, reason }
10604
12661
  });
@@ -10625,14 +12682,14 @@ function createPermissionRulesRoute(options) {
10625
12682
  try {
10626
12683
  value = JSON.parse(body.text);
10627
12684
  } catch {
10628
- return failed3(400, "invalid", "The body must be JSON: one change to the permission rules.");
12685
+ return failed4(400, "invalid", "The body must be JSON: one change to the permission rules.");
10629
12686
  }
10630
12687
  const asked = rulesChangeIn(value);
10631
- if (!asked.ok) return failed3(400, "invalid", asked.problem);
12688
+ if (!asked.ok) return failed4(400, "invalid", asked.problem);
10632
12689
  const made = settings.changePermissionRules(
10633
12690
  (rules) => applyRulesChange(rules, asked.change, newId)
10634
12691
  );
10635
- if (!made.ok) return failed3(STATUS_OF[made.reason], made.reason, made.problem);
12692
+ if (!made.ok) return failed4(STATUS_OF[made.reason], made.reason, made.problem);
10636
12693
  return {
10637
12694
  status: 200,
10638
12695
  body: { ok: true, permissionRules: made.rules }
@@ -10642,7 +12699,7 @@ function createPermissionRulesRoute(options) {
10642
12699
 
10643
12700
  // src/collector/settings/timeRulesRoute.ts
10644
12701
  var MAX_TIME_RULES_BODY_BYTES = 2048;
10645
- var failed4 = (status, reason, error) => ({
12702
+ var failed5 = (status, reason, error) => ({
10646
12703
  status,
10647
12704
  body: { error, reason }
10648
12705
  });
@@ -10666,12 +12723,12 @@ function createTimeRulesRoute(options) {
10666
12723
  try {
10667
12724
  value = JSON.parse(body.text);
10668
12725
  } catch {
10669
- return failed4(400, "invalid", "The body must be JSON: the three time rules.");
12726
+ return failed5(400, "invalid", "The body must be JSON: the three time rules.");
10670
12727
  }
10671
12728
  const asked = timeRulesIn(value);
10672
- if (!asked.ok) return failed4(400, "invalid", asked.problem);
12729
+ if (!asked.ok) return failed5(400, "invalid", asked.problem);
10673
12730
  const saved = settings.changeTimeRules(asked.rules);
10674
- if (!saved.ok) return failed4(500, "not-saved", saved.problem);
12731
+ if (!saved.ok) return failed5(500, "not-saved", saved.problem);
10675
12732
  return {
10676
12733
  status: 200,
10677
12734
  body: { ok: true, timeRules: asked.rules }
@@ -10715,11 +12772,11 @@ function createOsascriptRunner(options) {
10715
12772
  }
10716
12773
 
10717
12774
  // src/collector/terminal/tabFinder.ts
10718
- import os10 from "node:os";
12775
+ import os11 from "node:os";
10719
12776
  var TAB_LOOK_SOONEST_MS = 5e3;
10720
12777
  function createTabFinder(options) {
10721
12778
  const off = terminalJumpOff(options.env);
10722
- const homeDir = options.homeDir ?? os10.homedir();
12779
+ const homeDir = options.homeDir ?? os11.homedir();
10723
12780
  const readProcesses = options.readProcesses ?? readProcessTableWithPs;
10724
12781
  const now = options.now ?? Date.now;
10725
12782
  const soonestMs = options.soonestMs ?? TAB_LOOK_SOONEST_MS;
@@ -10953,96 +13010,24 @@ function ledgerHistoryStore(store, ledger) {
10953
13010
  }
10954
13011
 
10955
13012
  // src/collector/webhook/webhookSender.ts
10956
- var WEBHOOK_TIMEOUT_MS = 1e4;
10957
- function postHeaders(body, version) {
10958
- return {
10959
- "Content-Type": "application/json",
10960
- "Content-Length": String(Buffer.byteLength(body)),
10961
- "User-Agent": `Agent Lookout/${version}`
10962
- };
10963
- }
10964
- var TIMED_OUT = "the address did not answer in time";
10965
- function outcomeOfStatus(status) {
10966
- if (status >= 200 && status < 300) return { sent: true };
10967
- if (status >= 300 && status < 400) {
10968
- return { sent: false, reason: "the address answered with a redirect, which is not followed" };
10969
- }
10970
- if (status >= 400 && status < 500) {
10971
- return { sent: false, reason: `the address refused the post (status ${status})` };
10972
- }
10973
- if (status >= 500 && status < 600) {
10974
- return { sent: false, reason: `the receiving service had a problem (status ${status})` };
10975
- }
10976
- return { sent: false, reason: "the address's answer could not be understood" };
10977
- }
10978
- function failureReason2(error) {
10979
- const { code } = error ?? {};
10980
- if (typeof code !== "string") return "the post could not be sent";
10981
- if (code === "ECONNREFUSED") return "nothing answered at the address";
10982
- if (code === "ENOTFOUND" || code === "EAI_AGAIN") return "the address's host could not be found";
10983
- if (code === "ETIMEDOUT") return TIMED_OUT;
10984
- if (/CERT|SELF_SIGNED|UNABLE_TO|^ERR_TLS|^ERR_SSL|^EPROTO$/.test(code)) {
10985
- return "the secure connection to the address failed";
10986
- }
10987
- return "the connection to the address failed";
10988
- }
10989
- function requestOptions(body, version) {
10990
- return {
10991
- method: "POST",
10992
- headers: postHeaders(body, version),
10993
- // A connection of its own, closed after the answer.
10994
- agent: false,
10995
- rejectUnauthorized: true
10996
- };
10997
- }
13013
+ var WORDS = {
13014
+ where: "the address",
13015
+ whose: "the address's",
13016
+ notSent: "the post could not be sent",
13017
+ refused: (status) => `the address refused the post (status ${status})`,
13018
+ troubled: (status) => `the receiving service had a problem (status ${status})`
13019
+ };
10998
13020
  function createHttpSender(settings, options) {
10999
- const { url } = settings;
11000
- const timeoutMs = options.timeoutMs ?? WEBHOOK_TIMEOUT_MS;
11001
- let client = null;
11002
- const load = () => {
11003
- client ??= url.protocol === "https:" ? import("node:https").then(({ request: request2 }) => request2) : import("node:http").then(({ request: request2 }) => request2);
11004
- return client;
11005
- };
13021
+ const poster = createJsonPoster(settings.url, options);
11006
13022
  return {
11007
13023
  async send(post) {
11008
- let request2;
13024
+ let body;
11009
13025
  try {
11010
- request2 = await load();
13026
+ body = JSON.stringify(post);
11011
13027
  } catch {
11012
- return { sent: false, reason: "the post could not be sent" };
13028
+ return { sent: false, reason: WORDS.notSent };
11013
13029
  }
11014
- return new Promise((resolve) => {
11015
- let finished = false;
11016
- const finish = (outcome) => {
11017
- if (finished) return;
11018
- finished = true;
11019
- resolve(outcome);
11020
- };
11021
- let body;
11022
- let req;
11023
- try {
11024
- body = JSON.stringify(post);
11025
- req = request2(url, requestOptions(body, options.version));
11026
- } catch {
11027
- finish({ sent: false, reason: "the post could not be sent" });
11028
- return;
11029
- }
11030
- const timer = setTimeout(() => {
11031
- finish({ sent: false, reason: TIMED_OUT });
11032
- req.destroy();
11033
- }, timeoutMs);
11034
- timer.unref();
11035
- req.on("response", (res) => {
11036
- finish(outcomeOfStatus(res.statusCode ?? 0));
11037
- res.on("close", () => clearTimeout(timer));
11038
- res.resume();
11039
- });
11040
- req.on("error", (error) => {
11041
- clearTimeout(timer);
11042
- finish({ sent: false, reason: failureReason2(error) });
11043
- });
11044
- req.end(body);
11045
- });
13030
+ return outcomeOfPost(await poster.post(body), WORDS);
11046
13031
  }
11047
13032
  };
11048
13033
  }
@@ -11052,15 +13037,15 @@ var WEBHOOK_URL_ENV = "AGENT_LOOKOUT_WEBHOOK_URL";
11052
13037
  var WEBHOOK_EVENTS_ENV = "AGENT_LOOKOUT_WEBHOOK_EVENTS";
11053
13038
  var WEBHOOK_AFTER_ENV = "AGENT_LOOKOUT_WEBHOOK_AFTER";
11054
13039
  var WEBHOOK_ASKING_ENV = "AGENT_LOOKOUT_WEBHOOK_ASKING";
11055
- var MAX_URL_LENGTH = 2048;
11056
- var URL_UNREADABLE2 = `${WEBHOOK_URL_ENV} could not be read. Copy the whole address, beginning https://.`;
13040
+ var MAX_URL_LENGTH2 = 2048;
13041
+ var URL_UNREADABLE3 = `${WEBHOOK_URL_ENV} could not be read. Copy the whole address, beginning https://.`;
11057
13042
  function readUrl(value) {
11058
13043
  let url;
11059
13044
  try {
11060
- if (value.length > MAX_URL_LENGTH) throw new Error();
13045
+ if (value.length > MAX_URL_LENGTH2) throw new Error();
11061
13046
  url = new URL(value);
11062
13047
  } catch {
11063
- throw new SettingProblem(URL_UNREADABLE2);
13048
+ throw new SettingProblem(URL_UNREADABLE3);
11064
13049
  }
11065
13050
  const local = LOCAL_HOSTS.has(url.hostname);
11066
13051
  if (url.protocol !== "https:" && !(url.protocol === "http:" && local)) {
@@ -11068,7 +13053,7 @@ function readUrl(value) {
11068
13053
  `${WEBHOOK_URL_ENV} must begin with https://, or with http:// for an address on this computer, 127.0.0.1 or localhost.`
11069
13054
  );
11070
13055
  }
11071
- if (url.hostname === "") throw new SettingProblem(URL_UNREADABLE2);
13056
+ if (url.hostname === "") throw new SettingProblem(URL_UNREADABLE3);
11072
13057
  if (!isWebhookHost(url.hostname)) {
11073
13058
  throw new SettingProblem(
11074
13059
  `${WEBHOOK_URL_ENV} must name its host in letters, digits, dots and dashes, such as hooks.slack.com.`
@@ -11094,7 +13079,7 @@ function readWebhookSetup(env) {
11094
13079
  };
11095
13080
  } catch (error) {
11096
13081
  if (error instanceof SettingProblem) return { on: false, problem: error.message };
11097
- return { on: false, problem: URL_UNREADABLE2 };
13082
+ return { on: false, problem: URL_UNREADABLE3 };
11098
13083
  }
11099
13084
  }
11100
13085
  function webhookProblemLine(problem) {
@@ -11191,6 +13176,26 @@ function createCollector(options) {
11191
13176
  now
11192
13177
  }) : null;
11193
13178
  const webhookOff = webhookOffStatus(webhookSetup.on ? null : webhookSetup.problem);
13179
+ const ntfySetup = readNtfySetup(env);
13180
+ if (!ntfySetup.on && ntfySetup.problem !== null) warn(ntfyProblemLine(ntfySetup.problem));
13181
+ const makeNtfySender = options.createNtfySender ?? ((settings2) => createNtfySender(settings2, { version: options.version }));
13182
+ const ntfy = ntfySetup.on ? createNtfyNotifications({
13183
+ settings: ntfySetup.settings,
13184
+ sender: makeNtfySender(ntfySetup.settings),
13185
+ now
13186
+ }) : null;
13187
+ const ntfyOff = ntfyOffStatus(ntfySetup.on ? null : ntfySetup.problem);
13188
+ const pushoverSetup = readPushoverSetup(env);
13189
+ if (!pushoverSetup.on && pushoverSetup.problem !== null) {
13190
+ warn(pushoverProblemLine(pushoverSetup.problem));
13191
+ }
13192
+ const makePushoverSender = options.createPushoverSender ?? ((settings2) => createPushoverSender(settings2, { version: options.version }));
13193
+ const pushover = pushoverSetup.on ? createPushoverNotifications({
13194
+ settings: pushoverSetup.settings,
13195
+ sender: makePushoverSender(pushoverSetup.settings),
13196
+ now
13197
+ }) : null;
13198
+ const pushoverOff = pushoverOffStatus(pushoverSetup.on ? null : pushoverSetup.problem);
11194
13199
  const remotes = createRemotes({
11195
13200
  env,
11196
13201
  version: options.version,
@@ -11211,6 +13216,7 @@ function createCollector(options) {
11211
13216
  platform
11212
13217
  }),
11213
13218
  createCodexAdapter({ env: options.env, now, pollIntervalMs: intervalMs }),
13219
+ createAntigravityAdapter({ env: options.env, now, pollIntervalMs: intervalMs, platform }),
11214
13220
  createStatusFileAdapter({ env: options.env, now, pollIntervalMs: intervalMs })
11215
13221
  ];
11216
13222
  const poller = createPoller({
@@ -11245,6 +13251,8 @@ function createCollector(options) {
11245
13251
  answering?.observe(snapshot);
11246
13252
  email?.handle(snapshot);
11247
13253
  webhook?.handle(snapshot);
13254
+ ntfy?.handle(snapshot);
13255
+ pushover?.handle(snapshot);
11248
13256
  notifications.handle(snapshot);
11249
13257
  options.onSnapshot?.(snapshot);
11250
13258
  }
@@ -11287,6 +13295,13 @@ function createCollector(options) {
11287
13295
  notifications,
11288
13296
  email: () => email?.status() ?? emailOff,
11289
13297
  webhook: () => webhook?.status() ?? webhookOff,
13298
+ ntfy: () => ntfy?.status() ?? ntfyOff,
13299
+ pushover: () => pushover?.status() ?? pushoverOff,
13300
+ phoneTest: createTestSendRoute({
13301
+ channels: { ntfy, pushover },
13302
+ limiter: createActionLimiter(now),
13303
+ now
13304
+ }),
11290
13305
  pullRequests: () => pullRequests?.status() ?? pullRequestsOff,
11291
13306
  jump: createJumpRoute({ poller, panes, run: tmux, tabs, osascript, now }),
11292
13307
  stop: stopRoutes && createStopRoute(stopRoutes),
@@ -11381,13 +13396,20 @@ function createCollector(options) {
11381
13396
  poller,
11382
13397
  email,
11383
13398
  webhook,
13399
+ ntfy,
13400
+ pushover,
11384
13401
  pullRequests,
11385
13402
  history: keeper,
11386
13403
  answering: {
11387
13404
  start: () => answerStart ??= answering?.start() ?? Promise.resolve(),
11388
13405
  stop: async () => {
11389
13406
  await answering?.stop();
11390
- }
13407
+ },
13408
+ serve: (snapshot) => answering?.serve(snapshot) ?? snapshot,
13409
+ check: async () => {
13410
+ await answering?.check();
13411
+ },
13412
+ ...answering?.answer && { answer: answering.answer }
11391
13413
  },
11392
13414
  remotes,
11393
13415
  settings
@@ -11411,7 +13433,7 @@ async function runStandalone(options) {
11411
13433
  } catch (error) {
11412
13434
  return stopWith(error.message);
11413
13435
  }
11414
- if (!existsSync(path21.join(distDir, "index.html"))) {
13436
+ if (!existsSync(path25.join(distDir, "index.html"))) {
11415
13437
  return stopWith(
11416
13438
  "The dashboard has not been built yet. Run `npm run build`, then start it again."
11417
13439
  );
@@ -11454,12 +13476,12 @@ async function runStandalone(options) {
11454
13476
 
11455
13477
  // src/cli/start/openBrowser.ts
11456
13478
  import { spawn as spawn3 } from "node:child_process";
11457
- import path22 from "node:path";
13479
+ import path26 from "node:path";
11458
13480
  var OPEN_WAIT_MS = 1e4;
11459
13481
  var WINDOWS_FOLDER = "C:\\Windows";
11460
13482
  function windowsFolder(env) {
11461
13483
  const named = env.SystemRoot?.trim() || env.SYSTEMROOT?.trim();
11462
- return named && path22.win32.isAbsolute(named) ? named : WINDOWS_FOLDER;
13484
+ return named && path26.win32.isAbsolute(named) ? named : WINDOWS_FOLDER;
11463
13485
  }
11464
13486
  function openCommand(platform, url, env = process.env) {
11465
13487
  const address = loopbackAddress(url);
@@ -11468,7 +13490,7 @@ function openCommand(platform, url, env = process.env) {
11468
13490
  if (platform === "linux") return { file: "xdg-open", args: [address] };
11469
13491
  if (platform === "win32") {
11470
13492
  return {
11471
- file: path22.win32.join(windowsFolder(env), "System32", "rundll32.exe"),
13493
+ file: path26.win32.join(windowsFolder(env), "System32", "rundll32.exe"),
11472
13494
  args: ["url.dll,FileProtocolHandler", address]
11473
13495
  };
11474
13496
  }