agent-lookout 0.2.7 → 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;
@@ -583,7 +586,7 @@ function waitBegan(statusSince, seenAt) {
583
586
 
584
587
  // src/collector/outbound/outboundChannel.ts
585
588
  function createOutboundChannel(options) {
586
- 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;
587
590
  const now = options.now ?? Date.now;
588
591
  const wanted = new Set(options.events);
589
592
  let memory = EMPTY_CHANGE_MEMORY;
@@ -595,15 +598,26 @@ function createOutboundChannel(options) {
595
598
  const hold = createQuietHold();
596
599
  let last = null;
597
600
  let queue = Promise.resolve();
598
- function send2(content) {
599
- queue = queue.then(() => options.send(content)).then(
600
- (outcome) => {
601
- last = outcome.sent ? { at: now(), sent: true } : { at: now(), sent: false, reason: outcome.reason };
602
- },
603
- () => {
604
- last = { at: now(), sent: false, reason: failure2 };
605
- }
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
606
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
+ });
607
621
  }
608
622
  return {
609
623
  handle(snapshot) {
@@ -626,7 +640,7 @@ function createOutboundChannel(options) {
626
640
  ...item,
627
641
  agent: agentName(item.session, snapshot.sources)
628
642
  }));
629
- ready.push((sentNow) => summaryMessage({ ...summary, items, now: sentNow }));
643
+ ready.push((sentNow) => summaryMessage2({ ...summary, items, now: sentNow }));
630
644
  }
631
645
  for (const { session, begunAt } of end.open) open2.set(session.id, begunAt);
632
646
  }
@@ -644,7 +658,7 @@ function createOutboundChannel(options) {
644
658
  agent: agentName(session, snapshot.sources),
645
659
  seenAt: at
646
660
  };
647
- ready.push((sentNow) => overMessage({ ...facts, now: sentNow }));
661
+ ready.push((sentNow) => overMessage2({ ...facts, now: sentNow }));
648
662
  continue;
649
663
  }
650
664
  const since = session.statusSince;
@@ -674,7 +688,7 @@ function createOutboundChannel(options) {
674
688
  open2.delete(id);
675
689
  sentAt.push(at);
676
690
  send2(
677
- waitMessage({
691
+ waitMessage2({
678
692
  session: withoutWaitingText(session),
679
693
  agent: agentName(session, snapshot.sources),
680
694
  // Read now, from this poll's snapshot, and only when it is to go.
@@ -695,7 +709,7 @@ function createOutboundChannel(options) {
695
709
  sentAt.push(at);
696
710
  reminders.told(due.session.id, at);
697
711
  send2(
698
- reminderMessage({
712
+ reminderMessage2({
699
713
  session: withoutWaitingText(due.session),
700
714
  agent: agentName(due.session, snapshot.sources),
701
715
  asking: asking ? waitingText(due.session.waitingText) ?? null : null,
@@ -718,7 +732,15 @@ function createOutboundChannel(options) {
718
732
  },
719
733
  last: () => last,
720
734
  limitedUntil: () => limitLiftsAt(sentAt, now()),
721
- 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
+ }
722
744
  };
723
745
  }
724
746
 
@@ -1286,6 +1308,116 @@ function pullRequestsOffStatus(problem) {
1286
1308
  return { on: false, problem, gh: null, last: null };
1287
1309
  }
1288
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
+
1289
1421
  // src/core/sessions/diff.ts
1290
1422
  function severityFor(to) {
1291
1423
  if (to === "needs-you") return "warning";
@@ -1638,6 +1770,50 @@ function createPoller(options) {
1638
1770
  };
1639
1771
  }
1640
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
+
1641
1817
  // src/collector/webhook/webhookMessage.ts
1642
1818
  var MAX_SUMMARY_ITEMS = 100;
1643
1819
  function slackSafe(text4) {
@@ -1913,6 +2089,7 @@ function createApiHandler(options) {
1913
2089
  if (options.answer) actions.set(ANSWER_PATH, options.answer);
1914
2090
  if (options.timeRules) actions.set(TIME_RULES_PATH, options.timeRules);
1915
2091
  if (options.permissionRules) actions.set(PERMISSION_RULES_PATH, options.permissionRules);
2092
+ if (options.phoneTest) actions.set(PHONE_TEST_PATH, options.phoneTest);
1916
2093
  const serveSnapshot = options.serveSnapshot ?? ((snapshot) => snapshot);
1917
2094
  function route(req, res) {
1918
2095
  const refusal3 = refusalFor(req);
@@ -1975,6 +2152,22 @@ function createApiHandler(options) {
1975
2152
  );
1976
2153
  return;
1977
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
+ }
1978
2171
  case "/api/pull-requests": {
1979
2172
  send(
1980
2173
  res,
@@ -2258,7 +2451,7 @@ function urlFor(address) {
2258
2451
 
2259
2452
  // src/collector/hosts/standalone.ts
2260
2453
  import { existsSync } from "node:fs";
2261
- import path21 from "node:path";
2454
+ import path25 from "node:path";
2262
2455
 
2263
2456
  // src/core/mapping/claudeCodeMapping.ts
2264
2457
  var LIVE_STATUS = /* @__PURE__ */ new Map([
@@ -3114,6 +3307,9 @@ function lstartMs(value) {
3114
3307
  const [day, hours, minutes, seconds, year] = match.slice(2).map(Number);
3115
3308
  return Date.UTC(year, month, day, hours, minutes, seconds);
3116
3309
  }
3310
+ function processStartTime(value) {
3311
+ return lstartMs(collapse(value)) ?? null;
3312
+ }
3117
3313
  function compareProcessStart(recorded, actual, withinMs = SAME_START_WITHIN_MS) {
3118
3314
  if (recorded === void 0 || actual === void 0) return "unknown";
3119
3315
  const before = collapse(recorded);
@@ -3751,11 +3947,11 @@ async function readTranscriptTail(file, io, known, maxBytes = TRANSCRIPT_TAIL_BY
3751
3947
  const stamp = stampOf(open2.info);
3752
3948
  if (known !== null && sameStamp(known, stamp)) return { unchanged: true, stamp };
3753
3949
  const start = Math.max(0, stamp.size - maxBytes);
3754
- const bytes2 = await open2.read(start, stamp.size - start);
3950
+ const bytes3 = await open2.read(start, stamp.size - start);
3755
3951
  return {
3756
3952
  unchanged: false,
3757
3953
  stamp,
3758
- tail: new TextDecoder("utf-8").decode(bytes2),
3954
+ tail: new TextDecoder("utf-8").decode(bytes3),
3759
3955
  fromStart: start === 0
3760
3956
  };
3761
3957
  } finally {
@@ -4659,9 +4855,21 @@ async function pollAfterAnswer(poller, sessionId) {
4659
4855
  function answerRefusalFor(req) {
4660
4856
  return actionRefusalFor(req, ANSWER_ACTION, MAX_ANSWER_BODY_BYTES);
4661
4857
  }
4662
- function createAnswerRoute(options) {
4858
+ function createAnswerer(options) {
4663
4859
  const { poller, events, asks } = options;
4664
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);
4665
4873
  return async function answerPermission(req) {
4666
4874
  const refused = answerRefusalFor(req);
4667
4875
  if (refused) {
@@ -4680,12 +4888,8 @@ function createAnswerRoute(options) {
4680
4888
  'The body must be {"sessionId": "...", "requestId": "...", "decision": "allow" or "deny"} and nothing else.'
4681
4889
  );
4682
4890
  }
4683
- const session = poller.getSnapshot().sessions.find((listed) => listed.id === asked.sessionId);
4684
- if (!session) return answerFailed("no-ask");
4685
- const outcome = await asks.answer(asked.sessionId, asked.requestId, asked.decision);
4891
+ const outcome = await answer(asked.sessionId, asked.requestId, asked.decision);
4686
4892
  if (outcome !== "answered") return answerFailed(outcome);
4687
- events.add([answeredEvent(session, asked.decision, now())]);
4688
- await pollAfterAnswer(poller, asked.sessionId);
4689
4893
  return {
4690
4894
  status: 200,
4691
4895
  body: { ok: true, decision: asked.decision }
@@ -5621,18 +5825,18 @@ function namesOwnFiles(words, ownPaths = [], folder) {
5621
5825
  (part) => from !== void 0 && !part.startsWith("/") && !part.startsWith("-") ? [part, `${from}/${part}`] : [part]
5622
5826
  );
5623
5827
  return read.some((part) => {
5624
- const path23 = plainPath(part);
5625
- if (path23 === BY_NUMBER || path23.startsWith(`${BY_NUMBER}/`)) return true;
5626
- if (own.some((name) => path23 === name || path23.startsWith(`${name}/`))) return true;
5627
- if (above.has(path23)) return true;
5628
- 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}`));
5629
5833
  });
5630
5834
  });
5631
5835
  }
5632
5836
  function folderReachesOwnFiles(folder, ownPaths = []) {
5633
5837
  const own = ownPaths.filter((name) => name.startsWith("/")).map(plainPath);
5634
- const path23 = plainPath(folder);
5635
- 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}/`));
5636
5840
  }
5637
5841
  var PLAIN_WORD = /^[A-Za-z0-9_\-.,/:=+@%]+$/;
5638
5842
  var ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
@@ -5985,6 +6189,8 @@ function createAnswering(options) {
5985
6189
  observe: () => {
5986
6190
  },
5987
6191
  answeredIn: () => /* @__PURE__ */ new Set(),
6192
+ check: async () => {
6193
+ },
5988
6194
  ruleAnswers: () => []
5989
6195
  };
5990
6196
  }
@@ -6012,6 +6218,7 @@ function createAnswering(options) {
6012
6218
  let problem = null;
6013
6219
  let listening = false;
6014
6220
  const answerRoute = createAnswerRoute({ poller, events, asks, now: options.now });
6221
+ const answer = createAnswerer({ poller, events, asks, now: options.now });
6015
6222
  return {
6016
6223
  async start() {
6017
6224
  const started = await socket.start();
@@ -6044,6 +6251,9 @@ function createAnswering(options) {
6044
6251
  req.resume();
6045
6252
  return answerRefusalFor(req) ?? refusal(405, "This address only answers GET requests.", { Allow: "GET" });
6046
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(),
6047
6257
  ruleAnswers: () => ruleAnswers.recent()
6048
6258
  };
6049
6259
  }
@@ -6271,140 +6481,1502 @@ function waitTotals(input) {
6271
6481
  };
6272
6482
  }
6273
6483
 
6274
- // src/collector/adapters/codex/index.ts
6484
+ // src/collector/adapters/antigravity/index.ts
6275
6485
  import os5 from "node:os";
6276
- import path12 from "node:path";
6486
+ import path14 from "node:path";
6277
6487
 
6278
- // src/core/mapping/codexMapping.ts
6279
- var TURN_UNDER_WAY = /* @__PURE__ */ new Set(["task_started", "turn_started"]);
6280
- var TURN_OVER = /* @__PURE__ */ new Set(["task_complete", "turn_complete", "turn_aborted"]);
6281
- function mapCodexStatus(fields) {
6282
- const { lastTurn, live: live2 } = fields;
6283
- let open2;
6284
- if (lastTurn === null) {
6285
- open2 = "idle";
6286
- } else if (typeof lastTurn !== "string") {
6287
- return "unknown";
6288
- } else if (TURN_UNDER_WAY.has(lastTurn)) {
6289
- open2 = "working";
6290
- } else if (TURN_OVER.has(lastTurn)) {
6291
- open2 = "idle";
6292
- } else {
6293
- 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;
6294
6505
  }
6295
- return live2 === false ? "finished" : open2;
6296
- }
6297
- var WRITER_LOCKS_SINCE = [0, 155, 0];
6298
- var VERSION = /^(\d{1,6})\.(\d{1,6})\.(\d{1,6})(-[0-9A-Za-z.-]{1,64})?(?:\+[0-9A-Za-z.-]{1,64})?$/;
6299
- function keepsWriterLocks(cliVersion) {
6300
- if (typeof cliVersion !== "string") return false;
6301
- const match = VERSION.exec(cliVersion);
6302
- if (!match) return false;
6303
- const parts = [Number(match[1]), Number(match[2]), Number(match[3])];
6304
- for (const [index, since] of WRITER_LOCKS_SINCE.entries()) {
6305
- const part = parts[index];
6306
- 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
+ }
6307
6510
  }
6308
- return match[4] === void 0;
6511
+ return { held, complete };
6309
6512
  }
6310
- function codexLiveness(fields) {
6311
- if (!fields.lockFolder) return "unknown";
6312
- if (fields.locked) return true;
6313
- 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";
6314
6517
  }
6315
- var IMPORTED_TURN_ID = /^external-import-turn-\d{1,9}$/;
6316
- function isImportedCodexTurn(turnId) {
6317
- 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;
6318
6573
  }
6319
- function isRecord8(value) {
6320
- 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 };
6321
6576
  }
6322
- var CODEX_DESKTOP_ORIGINATOR = "Codex Desktop";
6323
- function mapCodexSurface(source, originator) {
6324
- if (originator === CODEX_DESKTOP_ORIGINATOR) return "desktop";
6325
- if (source === "cli" || source === "exec") return "terminal";
6326
- if (source === "vscode") return "vscode";
6327
- if (isRecord8(source) && source.custom === "chatgpt") return "desktop";
6328
- 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
+ }
6329
6597
  }
6330
- var NOT_A_SESSION_SOURCE = /* @__PURE__ */ new Set(["subagent", "internal", "mcp"]);
6331
- var NOT_A_SESSION_THREAD = /* @__PURE__ */ new Set(["subagent", "guardian_review", "memory_consolidation"]);
6332
- function isCodexSessionSource(source, meta = {}) {
6333
- if (typeof meta.parentThreadId === "string" && meta.parentThreadId !== "") return false;
6334
- if (typeof meta.threadSource === "string" && NOT_A_SESSION_THREAD.has(meta.threadSource)) {
6335
- 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;
6336
6619
  }
6337
- if (typeof source === "string") return !NOT_A_SESSION_SOURCE.has(source);
6338
- if (isRecord8(source)) return !Object.keys(source).some((kind) => NOT_A_SESSION_SOURCE.has(kind));
6339
- return true;
6620
+ return best;
6340
6621
  }
6341
6622
 
6342
- // src/collector/adapters/codex/rolloutFile.ts
6343
- var HEAD_LIMIT_BYTES = 2 * 1024 * 1024;
6344
- var TAIL_LIMIT_BYTES = 8 * 1024 * 1024;
6345
- var FIRST_HEAD_BYTES = 256 * 1024;
6346
- 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$/;
6347
6632
  var NEWLINE = 10;
6348
- var CODEX_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/;
6349
- function parseCodexTime(value) {
6350
- if (typeof value !== "string" || !CODEX_TIME.test(value)) return null;
6351
- const at = Date.parse(value);
6352
- return Number.isSafeInteger(at) ? at : null;
6353
- }
6354
- var LINE_TIME = /^\{"timestamp":"([^"]{1,64})"/;
6355
- var TURN_HINT = /"type":"(?:task|turn)_/;
6356
- var TURN_TYPE = /^(?:task|turn)_/;
6357
- function isRecord9(value) {
6358
- return typeof value === "object" && value !== null && !Array.isArray(value);
6359
- }
6360
- function text3(value) {
6361
- return typeof value === "string" && value !== "" ? value : void 0;
6362
- }
6363
- function label(value) {
6364
- return typeof value === "string" && value !== "" && value.length <= 64 ? value : void 0;
6365
- }
6366
- function compactSource(value) {
6367
- if (typeof value === "string") return text3(value);
6368
- if (!isRecord9(value)) return void 0;
6369
- return Object.fromEntries(
6370
- Object.entries(value).map(([kind, detail]) => [
6371
- kind,
6372
- typeof detail === "string" ? detail : true
6373
- ])
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
6374
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
+ });
6375
6655
  }
6376
- function parse(line) {
6377
- try {
6378
- const value = JSON.parse(line);
6379
- return isRecord9(value) ? value : null;
6380
- } catch {
6381
- return null;
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;
6382
6683
  }
6383
- }
6384
- function readMetaLine(line) {
6385
- if (!line.includes('"session_meta"')) return null;
6386
- const value = parse(line);
6387
- if (!value || value.type !== "session_meta" || !isRecord9(value.payload)) return null;
6388
- const payload = value.payload;
6389
- const meta = {
6390
- id: text3(payload.id),
6391
- timestamp: text3(payload.timestamp),
6392
- cwd: text3(payload.cwd),
6393
- source: compactSource(payload.source),
6394
- threadSource: text3(payload.thread_source),
6395
- parentThreadId: text3(payload.parent_thread_id),
6396
- originator: label(payload.originator),
6397
- cliVersion: label(payload.cli_version)
6398
- };
6399
- for (const key of Object.keys(meta)) {
6400
- if (meta[key] === void 0) delete meta[key];
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
+ }
7955
+ }
7956
+ function readMetaLine(line) {
7957
+ if (!line.includes('"session_meta"')) return null;
7958
+ const value = parse(line);
7959
+ if (!value || value.type !== "session_meta" || !isRecord10(value.payload)) return null;
7960
+ const payload = value.payload;
7961
+ const meta = {
7962
+ id: text3(payload.id),
7963
+ timestamp: text3(payload.timestamp),
7964
+ cwd: text3(payload.cwd),
7965
+ source: compactSource(payload.source),
7966
+ threadSource: text3(payload.thread_source),
7967
+ parentThreadId: text3(payload.parent_thread_id),
7968
+ originator: label(payload.originator),
7969
+ cliVersion: label(payload.cli_version)
7970
+ };
7971
+ for (const key of Object.keys(meta)) {
7972
+ if (meta[key] === void 0) delete meta[key];
6401
7973
  }
6402
7974
  return meta;
6403
7975
  }
6404
7976
  function readTurnLine(line) {
6405
7977
  if (!line.includes('"event_msg"') || !TURN_HINT.test(line)) return null;
6406
7978
  const value = parse(line);
6407
- if (!value || value.type !== "event_msg" || !isRecord9(value.payload)) return null;
7979
+ if (!value || value.type !== "event_msg" || !isRecord10(value.payload)) return null;
6408
7980
  const turn = value.payload.type;
6409
7981
  if (typeof turn !== "string" || !TURN_TYPE.test(turn)) return null;
6410
7982
  return {
@@ -6417,23 +7989,23 @@ function lineTime(line) {
6417
7989
  const match = LINE_TIME.exec(line);
6418
7990
  return match ? parseCodexTime(match[1]) : null;
6419
7991
  }
6420
- function bytes(data) {
7992
+ function bytes2(data) {
6421
7993
  return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
6422
7994
  }
6423
- async function readExactly(file, position, length) {
6424
- const data = bytes(await file.read(position, length));
7995
+ async function readExactly2(file, position, length) {
7996
+ const data = bytes2(await file.read(position, length));
6425
7997
  if (data.byteLength !== length) throw new Error("The file changed while it was read.");
6426
7998
  return data;
6427
7999
  }
6428
8000
  async function readHead(file, size) {
6429
- for (const limit of [FIRST_HEAD_BYTES, HEAD_LIMIT_BYTES]) {
8001
+ for (const limit of [FIRST_HEAD_BYTES, HEAD_LIMIT_BYTES2]) {
6430
8002
  const length = Math.min(limit, size);
6431
- const head = await readExactly(file, 0, length);
8003
+ const head = await readExactly2(file, 0, length);
6432
8004
  let named = head.indexOf('"session_meta"');
6433
8005
  while (named !== -1) {
6434
- const end = head.indexOf(NEWLINE, named);
8006
+ const end = head.indexOf(NEWLINE3, named);
6435
8007
  if (end === -1) break;
6436
- const start = named > 0 ? head.lastIndexOf(NEWLINE, named - 1) + 1 : 0;
8008
+ const start = named > 0 ? head.lastIndexOf(NEWLINE3, named - 1) + 1 : 0;
6437
8009
  const meta = readMetaLine(head.toString("utf8", start, end));
6438
8010
  if (meta) return meta;
6439
8011
  named = head.indexOf('"session_meta"', end + 1);
@@ -6442,9 +8014,9 @@ async function readHead(file, size) {
6442
8014
  }
6443
8015
  return null;
6444
8016
  }
6445
- async function scanTail(file, size) {
8017
+ async function scanTail2(file, size) {
6446
8018
  let position = size;
6447
- let budget2 = TAIL_LIMIT_BYTES;
8019
+ let budget2 = TAIL_LIMIT_BYTES2;
6448
8020
  let fragment = Buffer.alloc(0);
6449
8021
  let end = null;
6450
8022
  let lastLineAt = null;
@@ -6462,15 +8034,15 @@ async function scanTail(file, size) {
6462
8034
  end
6463
8035
  });
6464
8036
  while (position > 0 && budget2 > 0) {
6465
- const length = Math.min(TAIL_CHUNK_BYTES, position, budget2);
8037
+ const length = Math.min(TAIL_CHUNK_BYTES2, position, budget2);
6466
8038
  const start = position - length;
6467
- const chunk = await readExactly(file, start, length);
8039
+ const chunk = await readExactly2(file, start, length);
6468
8040
  budget2 -= length;
6469
8041
  position = start;
6470
8042
  const buffer = fragment.byteLength > 0 ? Buffer.concat([chunk, fragment]) : chunk;
6471
8043
  let cut2 = buffer.byteLength;
6472
8044
  if (end === null) {
6473
- const last = buffer.lastIndexOf(NEWLINE);
8045
+ const last = buffer.lastIndexOf(NEWLINE3);
6474
8046
  if (last === -1) {
6475
8047
  fragment = buffer;
6476
8048
  continue;
@@ -6478,12 +8050,12 @@ async function scanTail(file, size) {
6478
8050
  end = start + last + 1;
6479
8051
  cut2 = last;
6480
8052
  }
6481
- let newline = cut2 > 0 ? buffer.lastIndexOf(NEWLINE, cut2 - 1) : -1;
8053
+ let newline = cut2 > 0 ? buffer.lastIndexOf(NEWLINE3, cut2 - 1) : -1;
6482
8054
  while (newline !== -1) {
6483
8055
  const turn = take(buffer.toString("utf8", newline + 1, cut2));
6484
8056
  if (turn) return found(turn);
6485
8057
  cut2 = newline;
6486
- newline = cut2 > 0 ? buffer.lastIndexOf(NEWLINE, cut2 - 1) : -1;
8058
+ newline = cut2 > 0 ? buffer.lastIndexOf(NEWLINE3, cut2 - 1) : -1;
6487
8059
  }
6488
8060
  if (start === 0) {
6489
8061
  const turn = take(buffer.toString("utf8", 0, cut2));
@@ -6497,7 +8069,7 @@ async function scanTail(file, size) {
6497
8069
  }
6498
8070
  return { lastTurn: void 0, lastTurnAt: null, lastTurnImported: false, lastLineAt, end };
6499
8071
  }
6500
- function stateOf(cached) {
8072
+ function stateOf2(cached) {
6501
8073
  return {
6502
8074
  meta: cached.meta,
6503
8075
  lastTurn: cached.lastTurn,
@@ -6513,7 +8085,7 @@ function createRolloutReader(io) {
6513
8085
  try {
6514
8086
  const { info } = handle;
6515
8087
  const meta = await readHead(handle, info.size);
6516
- const tail = await scanTail(handle, info.size);
8088
+ const tail = await scanTail2(handle, info.size);
6517
8089
  return {
6518
8090
  meta,
6519
8091
  lastTurn: tail.lastTurn,
@@ -6533,14 +8105,14 @@ function createRolloutReader(io) {
6533
8105
  const offset = cached.offset;
6534
8106
  if (cached.meta === null || offset === null) return null;
6535
8107
  if (info.ino !== cached.ino || info.size <= cached.size || info.size < offset) return null;
6536
- if (info.size - offset > TAIL_LIMIT_BYTES) return null;
8108
+ if (info.size - offset > TAIL_LIMIT_BYTES2) return null;
6537
8109
  const handle = await io.openRegular(file);
6538
8110
  try {
6539
8111
  const opened = handle.info;
6540
8112
  if (opened.ino !== cached.ino || opened.size < offset) return null;
6541
8113
  const from = offset > 0 ? offset - 1 : 0;
6542
- const read = await readExactly(handle, from, opened.size - from);
6543
- 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;
6544
8116
  const added = offset > 0 ? read.subarray(1) : read;
6545
8117
  const next = {
6546
8118
  ...cached,
@@ -6549,7 +8121,7 @@ function createRolloutReader(io) {
6549
8121
  mtimeMs: opened.mtimeMs
6550
8122
  };
6551
8123
  let start = 0;
6552
- let newline = added.indexOf(NEWLINE);
8124
+ let newline = added.indexOf(NEWLINE3);
6553
8125
  while (newline !== -1) {
6554
8126
  const line = added.toString("utf8", start, newline);
6555
8127
  if (line.trim() !== "") {
@@ -6563,7 +8135,7 @@ function createRolloutReader(io) {
6563
8135
  }
6564
8136
  }
6565
8137
  start = newline + 1;
6566
- newline = added.indexOf(NEWLINE, start);
8138
+ newline = added.indexOf(NEWLINE3, start);
6567
8139
  }
6568
8140
  next.offset = offset + start;
6569
8141
  return next;
@@ -6575,11 +8147,11 @@ function createRolloutReader(io) {
6575
8147
  async read(file, info) {
6576
8148
  const cached = cache.get(file);
6577
8149
  if (cached && cached.ino === info.ino && cached.size === info.size && cached.mtimeMs === info.mtimeMs) {
6578
- return stateOf(cached);
8150
+ return stateOf2(cached);
6579
8151
  }
6580
8152
  const next = cached && await readOnward(file, cached, info) || await readFresh(file);
6581
8153
  cache.set(file, next);
6582
- return stateOf(next);
8154
+ return stateOf2(next);
6583
8155
  },
6584
8156
  keepOnly(files) {
6585
8157
  for (const file of cache.keys()) {
@@ -6590,10 +8162,10 @@ function createRolloutReader(io) {
6590
8162
  }
6591
8163
 
6592
8164
  // src/collector/adapters/codex/rollouts.ts
6593
- import path11 from "node:path";
6594
- 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}";
6595
8167
  var ROLLOUT_FILE_NAME = new RegExp(
6596
- `^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$`,
6597
8169
  "i"
6598
8170
  );
6599
8171
  var INDEX_REFRESH_MS = 3e4;
@@ -6608,7 +8180,7 @@ function twoDigits(value) {
6608
8180
  function recentDayFolders(sessionsDir, now) {
6609
8181
  const folders = [now, now - 24 * 60 * 60 * 1e3].map((at) => {
6610
8182
  const date = new Date(at);
6611
- return path11.join(
8183
+ return path15.join(
6612
8184
  sessionsDir,
6613
8185
  String(date.getFullYear()),
6614
8186
  twoDigits(date.getMonth() + 1),
@@ -6637,12 +8209,12 @@ function createRolloutFinder(options) {
6637
8209
  const refs = [];
6638
8210
  for (const name of (await names(dir)).sort()) {
6639
8211
  const threadId = threadIdOfRollout(name);
6640
- if (threadId !== null) refs.push({ path: path11.join(dir, name), threadId });
8212
+ if (threadId !== null) refs.push({ path: path15.join(dir, name), threadId });
6641
8213
  }
6642
8214
  return refs;
6643
8215
  }
6644
8216
  async function subfolders(dir, pattern) {
6645
- 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));
6646
8218
  }
6647
8219
  async function buildIndex() {
6648
8220
  const years = await subfolders(sessionsDir, YEAR);
@@ -6686,7 +8258,7 @@ function createRolloutFinder(options) {
6686
8258
  // src/collector/adapters/codex/sessionIndex.ts
6687
8259
  var SESSION_INDEX_FILE = "session_index.jsonl";
6688
8260
  var SESSION_INDEX_LIMIT_BYTES = 4 * 1024 * 1024;
6689
- function isRecord10(value) {
8261
+ function isRecord11(value) {
6690
8262
  return typeof value === "object" && value !== null && !Array.isArray(value);
6691
8263
  }
6692
8264
  function parseSessionIndex(content) {
@@ -6699,7 +8271,7 @@ function parseSessionIndex(content) {
6699
8271
  } catch {
6700
8272
  continue;
6701
8273
  }
6702
- if (!isRecord10(value)) continue;
8274
+ if (!isRecord11(value)) continue;
6703
8275
  const { id, thread_name: name } = value;
6704
8276
  if (typeof id !== "string" || id === "" || typeof name !== "string") continue;
6705
8277
  const trimmed = name.trim();
@@ -6750,14 +8322,14 @@ function createSessionIndexReader(file, io) {
6750
8322
  }
6751
8323
 
6752
8324
  // src/collector/adapters/codex/toSession.ts
6753
- var SOURCE_ID2 = "codex";
6754
- var WRITE_ORDER_SLACK_MS2 = 1e3;
8325
+ var SOURCE_ID3 = "codex";
8326
+ var WRITE_ORDER_SLACK_MS3 = 1e3;
6755
8327
  function lastActivity(state, now) {
6756
8328
  return plausibleTime(state.lastLineAt, now) ?? plausibleTime(state.lastTurnAt, now) ?? plausibleTime(parseCodexTime(state.meta?.timestamp), now);
6757
8329
  }
6758
8330
  function codexSession(input) {
6759
8331
  const { threadId, state, live: live2, now } = input;
6760
- const id = `${SOURCE_ID2}:${threadId}`;
8332
+ const id = `${SOURCE_ID3}:${threadId}`;
6761
8333
  const cwd = state.meta?.cwd ?? null;
6762
8334
  const project = projectOf(cwd);
6763
8335
  const status = mapCodexStatus({ lastTurn: state.lastTurn, live: live2 });
@@ -6768,12 +8340,12 @@ function codexSession(input) {
6768
8340
  } else if (status === "idle" || status === "finished") {
6769
8341
  statusSince = plausibleTime(state.lastLineAt, now);
6770
8342
  }
6771
- if (statusSince !== null && startedAt !== null && statusSince < startedAt - WRITE_ORDER_SLACK_MS2) {
8343
+ if (statusSince !== null && startedAt !== null && statusSince < startedAt - WRITE_ORDER_SLACK_MS3) {
6772
8344
  statusSince = null;
6773
8345
  }
6774
8346
  const session = {
6775
8347
  id,
6776
- source: SOURCE_ID2,
8348
+ source: SOURCE_ID3,
6777
8349
  surface: mapCodexSurface(state.meta?.source, state.meta?.originator),
6778
8350
  // A name made only of spaces is no name. It is cleaned and cut as every other agent's is.
6779
8351
  name: sessionName(input.name ?? "") || project || id,
@@ -6786,7 +8358,7 @@ function codexSession(input) {
6786
8358
  stale: isStale({ status, statusSince }, now)
6787
8359
  };
6788
8360
  const writtenAt = input.writtenAt === void 0 ? null : plausibleTime(Math.floor(input.writtenAt), now);
6789
- 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;
6790
8362
  if (writtenAt !== null && !beforeStart) session.lastWriteAt = writtenAt;
6791
8363
  return session;
6792
8364
  }
@@ -6816,11 +8388,11 @@ async function readWriterLocks(dir, io) {
6816
8388
  // src/collector/adapters/codex/index.ts
6817
8389
  var CODEX_HOME_ENV = "AGENT_LOOKOUT_CODEX_HOME";
6818
8390
  var CODEX_OWN_HOME_ENV = "CODEX_HOME";
6819
- var RECHECK_MS = 6e4;
6820
- var BASIS = "files";
8391
+ var RECHECK_MS2 = 6e4;
8392
+ var BASIS2 = "files";
6821
8393
  var BASIS_WITHOUT_LOCKS = "files-without-locks";
6822
- var LABEL2 = "Codex";
6823
- 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.";
6824
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.";
6825
8397
  var CODEX_CAPABILITIES = {
6826
8398
  "working-and-idle": { level: "yes" },
@@ -6853,18 +8425,18 @@ var CODEX_CAPABILITIES = {
6853
8425
  };
6854
8426
  function createCodexAdapter(options = {}) {
6855
8427
  const env = options.env ?? process.env;
6856
- const homeDir = options.homeDir ?? os5.homedir();
8428
+ const homeDir = options.homeDir ?? os6.homedir();
6857
8429
  const now = options.now ?? Date.now;
6858
8430
  const io = options.io ?? nodeIo2;
6859
8431
  const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
6860
- const recheckMs = options.recheckMs ?? RECHECK_MS;
8432
+ const recheckMs = options.recheckMs ?? RECHECK_MS2;
6861
8433
  const ownOverride = env[CODEX_HOME_ENV]?.trim() || void 0;
6862
8434
  const codexOverride = env[CODEX_OWN_HOME_ENV]?.trim() || void 0;
6863
8435
  const homeFrom = ownOverride ? "agent-lookout" : codexOverride ? "codex" : "default";
6864
- const codexHome = path12.resolve(ownOverride ?? codexOverride ?? path12.join(homeDir, ".codex"));
6865
- const sessionsDir = path12.join(codexHome, "sessions");
6866
- const locksDir = path12.join(codexHome, WRITER_LOCK_DIR);
6867
- 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);
6868
8440
  const homeName = tildify(codexHome, homeDir);
6869
8441
  const sessionsName = tildify(sessionsDir, homeDir);
6870
8442
  const locksName = tildify(locksDir, homeDir);
@@ -6890,14 +8462,14 @@ function createCodexAdapter(options = {}) {
6890
8462
  }
6891
8463
  function result(state, detail, read, checkedAt) {
6892
8464
  const health = {
6893
- id: SOURCE_ID2,
6894
- label: LABEL2,
8465
+ id: SOURCE_ID3,
8466
+ label: LABEL3,
6895
8467
  state,
6896
8468
  detail,
6897
8469
  watching: watching(read),
6898
8470
  checkedAt
6899
8471
  };
6900
- return state === "ok" ? { health, sessions: [], basis: BASIS } : { health, sessions: [] };
8472
+ return state === "ok" ? { health, sessions: [], basis: BASIS2 } : { health, sessions: [] };
6901
8473
  }
6902
8474
  function notFound(checkedAt) {
6903
8475
  const where = homeFrom === "codex" ? `${CODEX_OWN_HOME_ENV} is set to ${homeName}, and there is no folder there` : `there is no ${homeName} folder`;
@@ -6945,7 +8517,7 @@ function createCodexAdapter(options = {}) {
6945
8517
  if (homeFrom === "agent-lookout") {
6946
8518
  return result(
6947
8519
  "ok",
6948
- `${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}`,
6949
8521
  "not found",
6950
8522
  checkedAt
6951
8523
  );
@@ -6958,7 +8530,7 @@ function createCodexAdapter(options = {}) {
6958
8530
  homeSeen = true;
6959
8531
  return result(
6960
8532
  "ok",
6961
- `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}`,
6962
8534
  "not found",
6963
8535
  checkedAt
6964
8536
  );
@@ -7040,7 +8612,7 @@ function createCodexAdapter(options = {}) {
7040
8612
  now: checkedAt
7041
8613
  });
7042
8614
  });
7043
- 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}`;
7044
8616
  if (!locks.supported) {
7045
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`;
7046
8618
  detail += ` ${why}, so a session that has ended cannot be told from one that is idle, and none is shown as finished.`;
@@ -7048,11 +8620,11 @@ function createCodexAdapter(options = {}) {
7048
8620
  detail += ` ${OLDER_CODEX_NOTE}`;
7049
8621
  }
7050
8622
  const answer = result("ok", detail, every(pollIntervalMs), checkedAt);
7051
- return { ...answer, sessions, basis: locks.supported ? BASIS : BASIS_WITHOUT_LOCKS };
8623
+ return { ...answer, sessions, basis: locks.supported ? BASIS2 : BASIS_WITHOUT_LOCKS };
7052
8624
  }
7053
8625
  return {
7054
- id: SOURCE_ID2,
7055
- label: LABEL2,
8626
+ id: SOURCE_ID3,
8627
+ label: LABEL3,
7056
8628
  capabilities: CODEX_CAPABILITIES,
7057
8629
  lookingIn: `Looking for Codex sessions in ${sessionsName}.`,
7058
8630
  async poll() {
@@ -7071,8 +8643,8 @@ function createCodexAdapter(options = {}) {
7071
8643
  }
7072
8644
 
7073
8645
  // src/collector/adapters/status-files/index.ts
7074
- import os6 from "node:os";
7075
- import path13 from "node:path";
8646
+ import os7 from "node:os";
8647
+ import path17 from "node:path";
7076
8648
 
7077
8649
  // src/core/mapping/statusFileMapping.ts
7078
8650
  var STATUS = /* @__PURE__ */ new Map([
@@ -7152,16 +8724,16 @@ function parseStatusFile(content) {
7152
8724
  }
7153
8725
 
7154
8726
  // src/collector/adapters/status-files/toSession.ts
7155
- var SOURCE_ID3 = "status-files";
7156
- var WRITE_ORDER_SLACK_MS3 = 1e3;
8727
+ var SOURCE_ID4 = "status-files";
8728
+ var WRITE_ORDER_SLACK_MS4 = 1e3;
7157
8729
  function statusFileSession(input) {
7158
8730
  const { fileName: fileName2, file, statusSince, now } = input;
7159
8731
  const cwd = file.cwd ?? null;
7160
8732
  const project = projectOf(cwd);
7161
8733
  const mapped = mapStatusFileStatus(file);
7162
8734
  const session = {
7163
- id: `${SOURCE_ID3}:${fileName2}`,
7164
- source: SOURCE_ID3,
8735
+ id: `${SOURCE_ID4}:${fileName2}`,
8736
+ source: SOURCE_ID4,
7165
8737
  agent: file.agent,
7166
8738
  surface: mapStatusFileApp(file.app),
7167
8739
  // The file's name is as untrusted as its contents, so it is cleaned and cut
@@ -7181,15 +8753,15 @@ function statusFileSession(input) {
7181
8753
  }
7182
8754
  const writtenAt = input.writtenAt === void 0 ? null : plausibleTime(Math.floor(input.writtenAt), now);
7183
8755
  const given = plausibleTime(file.since, now);
7184
- 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;
7185
8757
  if (writtenAt !== null && !beforeSince) session.lastWriteAt = writtenAt;
7186
8758
  return session;
7187
8759
  }
7188
8760
 
7189
8761
  // src/collector/adapters/status-files/index.ts
7190
8762
  var STATUS_DIR_ENV = "AGENT_LOOKOUT_STATUS_DIR";
7191
- var BASIS2 = "files";
7192
- var LABEL3 = "Status files";
8763
+ var BASIS3 = "files";
8764
+ var LABEL4 = "Status files";
7193
8765
  var STATUS_FILE_CAPABILITIES = {
7194
8766
  "working-and-idle": { level: "partly", reason: "If the agent writes working and idle." },
7195
8767
  "needs-you": { level: "partly", reason: "If the agent writes waiting." },
@@ -7215,13 +8787,13 @@ function counted(count2, one, many) {
7215
8787
  }
7216
8788
  function createStatusFileAdapter(options = {}) {
7217
8789
  const env = options.env ?? process.env;
7218
- const homeDir = options.homeDir ?? os6.homedir();
8790
+ const homeDir = options.homeDir ?? os7.homedir();
7219
8791
  const now = options.now ?? Date.now;
7220
8792
  const io = options.io ?? nodeIo2;
7221
8793
  const isAlive = options.isAlive ?? isProcessAlive;
7222
8794
  const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
7223
8795
  const override = env[STATUS_DIR_ENV]?.trim() || void 0;
7224
- const dir = path13.resolve(override ?? path13.join(homeDir, ".agent-lookout", "sessions"));
8796
+ const dir = path17.resolve(override ?? path17.join(homeDir, ".agent-lookout", "sessions"));
7225
8797
  const dirName = tildify(dir, homeDir);
7226
8798
  const decoder2 = new TextDecoder();
7227
8799
  let lastRead = /* @__PURE__ */ new Map();
@@ -7242,15 +8814,15 @@ function createStatusFileAdapter(options = {}) {
7242
8814
  }
7243
8815
  function result(state, detail, facts, checkedAt, sessions = []) {
7244
8816
  const health = {
7245
- id: SOURCE_ID3,
7246
- label: LABEL3,
8817
+ id: SOURCE_ID4,
8818
+ label: LABEL4,
7247
8819
  state,
7248
8820
  detail,
7249
8821
  watching: facts,
7250
8822
  checkedAt
7251
8823
  };
7252
- if (state === "ok") return { health, sessions, basis: BASIS2 };
7253
- 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 };
7254
8826
  return { health, sessions: [] };
7255
8827
  }
7256
8828
  async function readOne(file) {
@@ -7266,9 +8838,9 @@ function createStatusFileAdapter(options = {}) {
7266
8838
  const tooLarge = { kind: "refused", why: "is over 16 KB" };
7267
8839
  try {
7268
8840
  if (open2.info.size > MAX_FILE_BYTES) return tooLarge;
7269
- const bytes2 = await open2.read(0, MAX_FILE_BYTES + 1);
7270
- if (bytes2.byteLength > MAX_FILE_BYTES) return tooLarge;
7271
- 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));
7272
8844
  return parsed ? { kind: "read", file: parsed, mtimeMs: open2.info.mtimeMs } : { kind: "invalid" };
7273
8845
  } catch {
7274
8846
  return { kind: "refused", why: "could not be read" };
@@ -7281,7 +8853,7 @@ function createStatusFileAdapter(options = {}) {
7281
8853
  if (names.length <= MAX_FILES) return names;
7282
8854
  const written = await Promise.all(
7283
8855
  names.map(async (name) => {
7284
- const info = await io.lstat(path13.join(dir, name)).catch(() => null);
8856
+ const info = await io.lstat(path17.join(dir, name)).catch(() => null);
7285
8857
  return { name, mtimeMs: info?.mtimeMs ?? -Infinity };
7286
8858
  })
7287
8859
  );
@@ -7331,7 +8903,7 @@ function createStatusFileAdapter(options = {}) {
7331
8903
  const nextRead = /* @__PURE__ */ new Map();
7332
8904
  const nextStatuses = /* @__PURE__ */ new Map();
7333
8905
  for (const fileName2 of await toRead(listed)) {
7334
- const reading = await readOne(path13.join(dir, fileName2));
8906
+ const reading = await readOne(path17.join(dir, fileName2));
7335
8907
  let kept;
7336
8908
  if (reading.kind === "read") {
7337
8909
  kept = reading;
@@ -7397,8 +8969,8 @@ function createStatusFileAdapter(options = {}) {
7397
8969
  );
7398
8970
  }
7399
8971
  return {
7400
- id: SOURCE_ID3,
7401
- label: LABEL3,
8972
+ id: SOURCE_ID4,
8973
+ label: LABEL4,
7402
8974
  capabilities: STATUS_FILE_CAPABILITIES,
7403
8975
  lookingIn: `Looking for status files in ${dirName}.`,
7404
8976
  async poll() {
@@ -7557,8 +9129,8 @@ function createEventStore(capacity = EVENT_CAPACITY) {
7557
9129
  }
7558
9130
 
7559
9131
  // src/collector/git/branchFinder.ts
7560
- import os7 from "node:os";
7561
- import path15 from "node:path";
9132
+ import os8 from "node:os";
9133
+ import path19 from "node:path";
7562
9134
 
7563
9135
  // src/collector/git/gitHead.ts
7564
9136
  var MAX_GIT_FILE_BYTES = 4 * 1024;
@@ -7597,19 +9169,19 @@ function parseCommonDir(content) {
7597
9169
  }
7598
9170
 
7599
9171
  // src/collector/git/repository.ts
7600
- import { createHash as createHash2 } from "node:crypto";
7601
- import path14 from "node:path";
9172
+ import { createHash as createHash3 } from "node:crypto";
9173
+ import path18 from "node:path";
7602
9174
  var REPOSITORY_ID_LENGTH = 16;
7603
9175
  function repositoryId(gitFolder) {
7604
- 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);
7605
9177
  }
7606
9178
  function sharedName(common) {
7607
- const base = path14.basename(common);
7608
- 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));
7609
9181
  return base.endsWith(".git") ? base.slice(0, -".git".length) : base;
7610
9182
  }
7611
9183
  function repositoryOf({ dir, gitDir, commonDir }) {
7612
- const name = sessionName(commonDir === null ? path14.basename(dir) : sharedName(commonDir));
9184
+ const name = sessionName(commonDir === null ? path18.basename(dir) : sharedName(commonDir));
7613
9185
  return name === void 0 ? null : { id: repositoryId(commonDir ?? gitDir), name };
7614
9186
  }
7615
9187
 
@@ -7627,9 +9199,9 @@ async function readGitFile(io, file, maxBytes = MAX_GIT_FILE_BYTES) {
7627
9199
  }
7628
9200
  try {
7629
9201
  if (open2.info.size > maxBytes) return null;
7630
- const bytes2 = await open2.read(0, maxBytes + 1);
7631
- if (bytes2.byteLength > maxBytes) return null;
7632
- 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);
7633
9205
  } catch {
7634
9206
  return null;
7635
9207
  } finally {
@@ -7638,13 +9210,13 @@ async function readGitFile(io, file, maxBytes = MAX_GIT_FILE_BYTES) {
7638
9210
  }
7639
9211
  }
7640
9212
  async function commonDirOf(io, gitDir) {
7641
- const content = await readGitFile(io, path15.join(gitDir, "commondir"));
9213
+ const content = await readGitFile(io, path19.join(gitDir, "commondir"));
7642
9214
  if (content === void 0) return null;
7643
9215
  const target = content === null ? null : parseCommonDir(content);
7644
- return target === null ? void 0 : path15.resolve(gitDir, target);
9216
+ return target === null ? void 0 : path19.resolve(gitDir, target);
7645
9217
  }
7646
9218
  async function headIn(dir, kind, io) {
7647
- const marker = path15.join(dir, ".git");
9219
+ const marker = path19.join(dir, ".git");
7648
9220
  let gitDir;
7649
9221
  if (kind === "directory") {
7650
9222
  gitDir = marker;
@@ -7652,11 +9224,11 @@ async function headIn(dir, kind, io) {
7652
9224
  const content2 = await readGitFile(io, marker);
7653
9225
  const target = typeof content2 === "string" ? parseGitFile(content2) : null;
7654
9226
  if (target === null) return null;
7655
- gitDir = path15.resolve(dir, target);
9227
+ gitDir = path19.resolve(dir, target);
7656
9228
  } else {
7657
9229
  return null;
7658
9230
  }
7659
- const content = await readGitFile(io, path15.join(gitDir, "HEAD"));
9231
+ const content = await readGitFile(io, path19.join(gitDir, "HEAD"));
7660
9232
  const head = typeof content === "string" ? parseHead(content) : null;
7661
9233
  if (head === null) return null;
7662
9234
  const commonDir = kind === "file" ? await commonDirOf(io, gitDir) : null;
@@ -7665,20 +9237,20 @@ async function headIn(dir, kind, io) {
7665
9237
  return { head: { ...head, repository }, gitFolder: commonDir ?? gitDir };
7666
9238
  }
7667
9239
  async function findHead(cwd, { io, homeDir }) {
7668
- if (!path15.isAbsolute(cwd)) return null;
7669
- const home = path15.resolve(homeDir);
7670
- let dir = path15.resolve(cwd);
9240
+ if (!path19.isAbsolute(cwd)) return null;
9241
+ const home = path19.resolve(homeDir);
9242
+ let dir = path19.resolve(cwd);
7671
9243
  try {
7672
9244
  if ((await io.stat(dir)).kind !== "directory") return null;
7673
9245
  } catch {
7674
9246
  return null;
7675
9247
  }
7676
9248
  for (let level = 0; level < MAX_LEVELS; level += 1) {
7677
- const parent = path15.dirname(dir);
9249
+ const parent = path19.dirname(dir);
7678
9250
  if (dir === home || dir === parent) return null;
7679
9251
  let kind;
7680
9252
  try {
7681
- kind = (await io.lstat(path15.join(dir, ".git"))).kind;
9253
+ kind = (await io.lstat(path19.join(dir, ".git"))).kind;
7682
9254
  } catch (error) {
7683
9255
  if (!isMissing2(error)) return null;
7684
9256
  dir = parent;
@@ -7699,7 +9271,7 @@ function within(work, ms) {
7699
9271
  });
7700
9272
  }
7701
9273
  function createBranchFinder(options = {}) {
7702
- const where = { io: options.io ?? nodeIo2, homeDir: options.homeDir ?? os7.homedir() };
9274
+ const where = { io: options.io ?? nodeIo2, homeDir: options.homeDir ?? os8.homedir() };
7703
9275
  const now = options.now ?? Date.now;
7704
9276
  const waitMs = options.waitMs ?? READ_WAIT_MS;
7705
9277
  const known = /* @__PURE__ */ new Map();
@@ -7761,14 +9333,14 @@ var STATUS_STATE = {
7761
9333
  PENDING: "pending",
7762
9334
  EXPECTED: "pending"
7763
9335
  };
7764
- function isRecord11(value) {
9336
+ function isRecord12(value) {
7765
9337
  return typeof value === "object" && value !== null && !Array.isArray(value);
7766
9338
  }
7767
9339
  function textOf(value) {
7768
9340
  return typeof value === "string" ? value : "";
7769
9341
  }
7770
9342
  function checkOf(value) {
7771
- if (!isRecord11(value)) return null;
9343
+ if (!isRecord12(value)) return null;
7772
9344
  const startedAt = Date.parse(textOf(value.startedAt));
7773
9345
  const at = Number.isNaN(startedAt) ? -Infinity : startedAt;
7774
9346
  if (value.__typename === "CheckRun") {
@@ -7804,7 +9376,7 @@ function checksOf(rollup) {
7804
9376
  for (const { state } of latest.values()) counts[state] += 1;
7805
9377
  return { state: checksStateOf(counts), ...counts };
7806
9378
  }
7807
- function stateOf2(state, isDraft) {
9379
+ function stateOf3(state, isDraft) {
7808
9380
  if (state === "OPEN") return isDraft === true ? "draft" : "open";
7809
9381
  if (state === "MERGED") return "merged";
7810
9382
  if (state === "CLOSED") return "closed";
@@ -7817,9 +9389,9 @@ function readGhAnswer(stdout, repository) {
7817
9389
  } catch {
7818
9390
  return null;
7819
9391
  }
7820
- if (!isRecord11(data)) return null;
9392
+ if (!isRecord12(data)) return null;
7821
9393
  const { number } = data;
7822
- const state = stateOf2(data.state, data.isDraft);
9394
+ const state = stateOf3(data.state, data.isDraft);
7823
9395
  const title = typeof data.title === "string" ? sessionName(data.title) : void 0;
7824
9396
  if (!isPullRequestNumber(number) || state === null || title === void 0) return null;
7825
9397
  return {
@@ -7956,7 +9528,7 @@ function createGhAsker(options) {
7956
9528
  }
7957
9529
 
7958
9530
  // src/collector/git/pullRequestTarget.ts
7959
- import path16 from "node:path";
9531
+ import path20 from "node:path";
7960
9532
 
7961
9533
  // src/collector/git/gitConfig.ts
7962
9534
  var MAX_CONFIG_BYTES = 64 * 1024;
@@ -8214,7 +9786,7 @@ function targetOf(config, branch, base, defaultBranch) {
8214
9786
  async function readPullRequestTarget(gitFolder, branch, io = nodeIo2) {
8215
9787
  try {
8216
9788
  if (!isAskableBranch(branch)) return null;
8217
- 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);
8218
9790
  if (typeof text4 !== "string") return null;
8219
9791
  const config = parseGitConfig(text4);
8220
9792
  const base = config === null ? null : baseRemoteOf(config);
@@ -8223,7 +9795,7 @@ async function readPullRequestTarget(gitFolder, branch, io = nodeIo2) {
8223
9795
  if (base.name !== null && isSafeRemoteName(base.name)) {
8224
9796
  const head = await readGitFile(
8225
9797
  io,
8226
- path16.join(gitFolder, "refs", "remotes", base.name, "HEAD")
9798
+ path20.join(gitFolder, "refs", "remotes", base.name, "HEAD")
8227
9799
  );
8228
9800
  if (typeof head === "string") defaultBranch = parseRemoteHead(head, base.name);
8229
9801
  }
@@ -8394,7 +9966,7 @@ function createClearHistoryRoute(options) {
8394
9966
  }
8395
9967
 
8396
9968
  // src/collector/history/historyKeeper.ts
8397
- import path17 from "node:path";
9969
+ import path21 from "node:path";
8398
9970
 
8399
9971
  // src/collector/history/historyFiles.ts
8400
9972
  import {
@@ -8694,7 +10266,7 @@ function createHistoryKeeper(options) {
8694
10266
  });
8695
10267
  const lock = createWriterLock({
8696
10268
  fs,
8697
- file: path17.join(dir, LOCK_FILE),
10269
+ file: path21.join(dir, LOCK_FILE),
8698
10270
  pid: options.pid,
8699
10271
  now,
8700
10272
  isAlive: options.isAlive,
@@ -8720,7 +10292,7 @@ function createHistoryKeeper(options) {
8720
10292
  });
8721
10293
  return next;
8722
10294
  }
8723
- const full = (name) => path17.join(dir, name);
10295
+ const full = (name) => path21.join(dir, name);
8724
10296
  function hold(record) {
8725
10297
  if (unusable !== null || !running) return;
8726
10298
  const line = encodeRecord(record);
@@ -8887,13 +10459,13 @@ function createHistoryKeeper(options) {
8887
10459
  continue;
8888
10460
  }
8889
10461
  let text4 = lead;
8890
- let bytes2 = lead.length;
10462
+ let bytes3 = lead.length;
8891
10463
  let end = index;
8892
10464
  while (end < lines.length) {
8893
10465
  const lineBytes = byteLength(lines[end]);
8894
- if (bytes2 - lead.length + lineBytes > room) break;
10466
+ if (bytes3 - lead.length + lineBytes > room) break;
8895
10467
  text4 += lines[end];
8896
- bytes2 += lineBytes;
10468
+ bytes3 += lineBytes;
8897
10469
  end += 1;
8898
10470
  }
8899
10471
  const outcome = yield { name, text: text4 };
@@ -8906,7 +10478,7 @@ function createHistoryKeeper(options) {
8906
10478
  const parsed = readHistoryFileName(name);
8907
10479
  files.set(name, {
8908
10480
  ...parsed,
8909
- size: size + bytes2,
10481
+ size: size + bytes3,
8910
10482
  first: known?.first ?? (record && { at: recordAt(record), kind: record.kind })
8911
10483
  });
8912
10484
  touched.add(name);
@@ -9134,8 +10706,8 @@ function createHistoryKeeper(options) {
9134
10706
  return { at: oldest.first.at, by: BEGINNING[oldest.first.kind] };
9135
10707
  },
9136
10708
  status() {
9137
- let bytes2 = 0;
9138
- for (const file of files.values()) bytes2 += file.size;
10709
+ let bytes3 = 0;
10710
+ for (const file of files.values()) bytes3 += file.size;
9139
10711
  let problem = unusable ?? failing;
9140
10712
  if (problem === null && writer === false) {
9141
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.";
@@ -9143,7 +10715,7 @@ function createHistoryKeeper(options) {
9143
10715
  return {
9144
10716
  where: "disk",
9145
10717
  folder,
9146
- bytes: bytes2,
10718
+ bytes: bytes3,
9147
10719
  maxBytes,
9148
10720
  maxAgeMs,
9149
10721
  canClear: unusable === null && writer === true && lock.held,
@@ -9180,15 +10752,15 @@ function keptHistoryStore(store, keeper) {
9180
10752
  }
9181
10753
 
9182
10754
  // src/collector/history/historySettings.ts
9183
- import { createHash as createHash3 } from "node:crypto";
9184
- import os8 from "node:os";
9185
- 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";
9186
10758
  var HISTORY_ENV = "AGENT_LOOKOUT_HISTORY";
9187
10759
  var HISTORY_DIR_ENV = "AGENT_LOOKOUT_HISTORY_DIR";
9188
- function readHistorySetup(env, homeDir = os8.homedir()) {
10760
+ function readHistorySetup(env, homeDir = os9.homedir()) {
9189
10761
  if (env[HISTORY_ENV]?.trim().toLowerCase() === "off") return { on: false };
9190
10762
  const override = env[HISTORY_DIR_ENV]?.trim() || void 0;
9191
- const dir = path18.resolve(override ?? path18.join(homeDir, ".agent-lookout", "history"));
10763
+ const dir = path22.resolve(override ?? path22.join(homeDir, ".agent-lookout", "history"));
9192
10764
  return {
9193
10765
  on: true,
9194
10766
  dir,
@@ -9196,14 +10768,15 @@ function readHistorySetup(env, homeDir = os8.homedir()) {
9196
10768
  sources: sourcesFingerprint(env, homeDir)
9197
10769
  };
9198
10770
  }
9199
- function sourcesFingerprint(env, homeDir = os8.homedir()) {
10771
+ function sourcesFingerprint(env, homeDir = os9.homedir()) {
9200
10772
  const set = (name) => env[name]?.trim() || void 0;
9201
10773
  const folders = [
9202
- path18.resolve(set(CLAUDE_HOME_ENV) ?? path18.join(homeDir, ".claude")),
9203
- path18.resolve(set(CODEX_HOME_ENV) ?? set(CODEX_OWN_HOME_ENV) ?? path18.join(homeDir, ".codex")),
9204
- 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))
9205
10778
  ];
9206
- 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);
9207
10780
  }
9208
10781
 
9209
10782
  // src/collector/notifications/heldWait.ts
@@ -9244,9 +10817,9 @@ function createServerNotifications(options) {
9244
10817
  const held = /* @__PURE__ */ new Map();
9245
10818
  const reminders = createReminderWatch();
9246
10819
  const quiet = createQuietHold();
9247
- function show(notice) {
10820
+ function show(notice, about) {
9248
10821
  try {
9249
- notifier.show(notice);
10822
+ notifier.show(notice, about);
9250
10823
  } catch {
9251
10824
  }
9252
10825
  }
@@ -9343,82 +10916,505 @@ function createServerNotifications(options) {
9343
10916
  }
9344
10917
  continue;
9345
10918
  }
9346
- if (entry.kind === "change") show(changeNotice(entry.change));
9347
- else if (entry.kind === "reminder") {
9348
- 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 });
9349
10929
  } else show(summaryNotice(entry.summary.items));
9350
10930
  }
9351
10931
  }
9352
10932
  };
9353
10933
  }
9354
10934
 
9355
- // src/collector/processes/osascript.ts
9356
- import { execFile as execFile3 } from "node:child_process";
9357
- var OSASCRIPT = "/usr/bin/osascript";
9358
- var MAX_OUTPUT_BYTES4 = 64 * 1024;
9359
- var NOT_RUN2 = { ok: false, stderr: "", timedOut: false };
9360
- function runOsascript(args, options = {}) {
9361
- 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
+ }
9362
11312
  try {
9363
- const child = execFile3(
9364
- OSASCRIPT,
9365
- [...args],
9366
- {
9367
- timeout: options.timeoutMs ?? TERMINAL_JUMP_TIMEOUT_MS,
9368
- maxBuffer: MAX_OUTPUT_BYTES4,
9369
- encoding: "utf8",
9370
- windowsHide: true
9371
- },
9372
- (error, stdout, stderr) => {
9373
- if (!error) {
9374
- resolve({ ok: true, stdout: String(stdout ?? "") });
9375
- 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))
9376
11322
  }
9377
- const timedOut = error.killed === true;
9378
- resolve({ ok: false, stderr: String(stderr ?? "").trim(), timedOut });
9379
- }
9380
- );
9381
- child.stdin?.end();
9382
- } catch {
9383
- 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();
9384
11334
  }
9385
- });
11335
+ };
9386
11336
  }
9387
11337
 
9388
- // src/collector/notifications/systemNotifier.ts
9389
- var OSASCRIPT_TIMEOUT_MS = 5e3;
9390
- var SCRIPT = [
9391
- "on run argv",
9392
- "display notification (item 2 of argv) with title (item 1 of argv)",
9393
- "end run"
9394
- ];
9395
- function osascriptArgs(notice) {
9396
- return [
9397
- ...SCRIPT.flatMap((line) => ["-e", line]),
9398
- "--",
9399
- oneLine(notice.title),
9400
- oneLine(notice.body, MAX_NOTICE_TEXT_LENGTH)
9401
- ];
9402
- }
9403
- function systemNotificationsShownOn(platform) {
9404
- 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
+ };
9405
11348
  }
9406
- var runForNotification = (args) => runOsascript(args, { timeoutMs: OSASCRIPT_TIMEOUT_MS });
9407
- function createSystemNotifier(options = {}) {
9408
- const platform = options.platform ?? process.platform;
9409
- 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);
9410
11368
  return {
9411
- show(notice) {
9412
- if (!systemNotificationsShownOn(platform)) return;
11369
+ async send(message) {
11370
+ let body;
9413
11371
  try {
9414
- run(osascriptArgs(notice)).catch(() => {
9415
- });
11372
+ body = JSON.stringify(pushoverBody(settings.token, settings.user, message));
9416
11373
  } catch {
11374
+ return { sent: false, reason: PUSHOVER_WORDS.notSent };
9417
11375
  }
11376
+ return outcomeOfPost(await poster.post(body), PUSHOVER_WORDS);
9418
11377
  }
9419
11378
  };
9420
11379
  }
9421
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
+
9422
11418
  // src/collector/remotes/remoteReader.ts
9423
11419
  import { request } from "node:http";
9424
11420
  var REMOTE_ANSWER_TIMEOUT_MS = 6e3;
@@ -9495,7 +11491,7 @@ var MAX_REMOTE_SOURCES = 20;
9495
11491
  var MAX_ID_LENGTH = 512;
9496
11492
  var MAX_PATH_LENGTH = 4096;
9497
11493
  var MAX_WORD_LENGTH2 = 200;
9498
- function isRecord12(value) {
11494
+ function isRecord13(value) {
9499
11495
  return typeof value === "object" && value !== null && !Array.isArray(value);
9500
11496
  }
9501
11497
  function oneOf2(allowed, value) {
@@ -9509,7 +11505,7 @@ function exact(value, max) {
9509
11505
  var COMMIT_ID2 = /^[0-9a-f]{7,64}$/;
9510
11506
  var REPOSITORY_ID = /^[0-9a-f]{16,64}$/;
9511
11507
  function readGit(value, machine) {
9512
- if (!isRecord12(value)) return void 0;
11508
+ if (!isRecord13(value)) return void 0;
9513
11509
  let head;
9514
11510
  if (value.commit !== void 0) {
9515
11511
  if (value.branch !== void 0 || typeof value.commit !== "string") return void 0;
@@ -9521,7 +11517,7 @@ function readGit(value, machine) {
9521
11517
  head = { branch };
9522
11518
  }
9523
11519
  const repository = value.repository;
9524
- if (isRecord12(repository) && typeof repository.id === "string") {
11520
+ if (isRecord13(repository) && typeof repository.id === "string") {
9525
11521
  const name = sessionName(typeof repository.name === "string" ? repository.name : "");
9526
11522
  if (name !== void 0 && REPOSITORY_ID.test(repository.id)) {
9527
11523
  head.repository = { id: repositoryId(`${machine}
@@ -9531,7 +11527,7 @@ ${repository.id}`), name };
9531
11527
  return head;
9532
11528
  }
9533
11529
  function readSession(value, machine, sources, now, keepWaitingText) {
9534
- if (!isRecord12(value)) return null;
11530
+ if (!isRecord13(value)) return null;
9535
11531
  const id = exact(value.id, MAX_ID_LENGTH);
9536
11532
  const source = exact(value.source, MAX_WORD_LENGTH2);
9537
11533
  if (id === null || source === null) return null;
@@ -9571,7 +11567,7 @@ function readSession(value, machine, sources, now, keepWaitingText) {
9571
11567
  return session;
9572
11568
  }
9573
11569
  function readCell(value) {
9574
- if (!isRecord12(value)) return null;
11570
+ if (!isRecord13(value)) return null;
9575
11571
  if (value.level === "yes") return { level: "yes" };
9576
11572
  const level = oneOf2(["no", "partly"], value.level);
9577
11573
  const reason = cut(value.reason, MAX_WORD_LENGTH2);
@@ -9579,7 +11575,7 @@ function readCell(value) {
9579
11575
  }
9580
11576
  var ACTS_HERE = ["jump", "stop", "answer"];
9581
11577
  function readCapabilities(value, machine) {
9582
- if (!isRecord12(value)) return null;
11578
+ if (!isRecord13(value)) return null;
9583
11579
  const read = {};
9584
11580
  for (const capability of CAPABILITIES) {
9585
11581
  if (ACTS_HERE.includes(capability)) continue;
@@ -9595,7 +11591,7 @@ function readCapabilities(value, machine) {
9595
11591
  };
9596
11592
  }
9597
11593
  function readRemoteSnapshot(value, machine, now, options = {}) {
9598
- if (!isRecord12(value) || !Array.isArray(value.sessions) || !Array.isArray(value.sources)) {
11594
+ if (!isRecord13(value) || !Array.isArray(value.sessions) || !Array.isArray(value.sources)) {
9599
11595
  return null;
9600
11596
  }
9601
11597
  const named = /* @__PURE__ */ new Map();
@@ -9603,7 +11599,7 @@ function readRemoteSnapshot(value, machine, now, options = {}) {
9603
11599
  const agents = [];
9604
11600
  for (const item of value.sources) {
9605
11601
  if (sources.length >= MAX_REMOTE_SOURCES) break;
9606
- if (!isRecord12(item)) continue;
11602
+ if (!isRecord13(item)) continue;
9607
11603
  const id = exact(item.id, MAX_WORD_LENGTH2);
9608
11604
  if (id === null || isRemoteSource(id) || item.machine !== void 0) continue;
9609
11605
  const label2 = cut(item.label, MAX_WORD_LENGTH2) ?? id;
@@ -9628,7 +11624,7 @@ function readRemoteSnapshot(value, machine, now, options = {}) {
9628
11624
  }
9629
11625
 
9630
11626
  // src/collector/remotes/sshProgram.ts
9631
- import path19 from "node:path";
11627
+ import path23 from "node:path";
9632
11628
  var SSH_BIN_ENV = "AGENT_LOOKOUT_SSH_BIN";
9633
11629
  var SSH_LOCATIONS = ["/usr/bin/ssh", "/opt/homebrew/bin/ssh", "/usr/local/bin/ssh"];
9634
11630
  var SERVER_ALIVE_SECONDS = 15;
@@ -9650,7 +11646,7 @@ async function findSsh(env, isExecutable, platform = process.platform) {
9650
11646
  }
9651
11647
  return {
9652
11648
  found: false,
9653
- 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(", ")}`
9654
11650
  };
9655
11651
  }
9656
11652
  function sshArguments(forward) {
@@ -9674,7 +11670,7 @@ function sshArguments(forward) {
9674
11670
  }
9675
11671
 
9676
11672
  // src/collector/remotes/remoteAdapter.ts
9677
- var BASIS3 = "api";
11673
+ var BASIS4 = "api";
9678
11674
  var NOT_CONNECTED = "unavailable";
9679
11675
  var ANSWER_WAIT_MS = 500;
9680
11676
  var STILL_CONNECTING_MS = 1e4;
@@ -9740,7 +11736,7 @@ function createRemoteAdapter(options) {
9740
11736
  if (found.advice) health.advice = found.advice;
9741
11737
  if (agents.length > 0) health.agents = agents;
9742
11738
  const sessions = state === "ok" && snapshot ? snapshot.sessions : [];
9743
- return state === "ok" ? { health, sessions, basis: BASIS3 } : { health, sessions };
11739
+ return state === "ok" ? { health, sessions, basis: BASIS4 } : { health, sessions };
9744
11740
  }
9745
11741
  function connecting(at) {
9746
11742
  if (problem === null) {
@@ -10245,15 +12241,15 @@ import {
10245
12241
  unlinkSync as unlinkSync2,
10246
12242
  writeSync as writeSync2
10247
12243
  } from "node:fs";
10248
- import os9 from "node:os";
10249
- import path20 from "node:path";
12244
+ import os10 from "node:os";
12245
+ import path24 from "node:path";
10250
12246
  var SETTINGS_FILE_ENV = "AGENT_LOOKOUT_SETTINGS_FILE";
10251
12247
  var SETTINGS_FILE_MODE = 384;
10252
12248
  var SETTINGS_FOLDER_MODE = 448;
10253
12249
  var MAX_SETTINGS_BYTES = 64 * 1024;
10254
- function readSettingsSetup(env, homeDir = os9.homedir()) {
12250
+ function readSettingsSetup(env, homeDir = os10.homedir()) {
10255
12251
  const named = env[SETTINGS_FILE_ENV]?.trim() || void 0;
10256
- 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"));
10257
12253
  return { file, shown: tildify(file, homeDir) };
10258
12254
  }
10259
12255
  function codeOf(error) {
@@ -10298,7 +12294,7 @@ function readSettingsStamp(setup) {
10298
12294
  }
10299
12295
  function readSettingsText(setup) {
10300
12296
  const { file, shown } = setup;
10301
- const folder = path20.dirname(file);
12297
+ const folder = path24.dirname(file);
10302
12298
  try {
10303
12299
  const folderKind = kindAt(folder);
10304
12300
  if (folderKind === null) return { kind: "missing" };
@@ -10357,7 +12353,7 @@ function readSettingsText(setup) {
10357
12353
  }
10358
12354
  function writeSettingsText(setup, text4) {
10359
12355
  const { file, shown } = setup;
10360
- const folder = path20.dirname(file);
12356
+ const folder = path24.dirname(file);
10361
12357
  const notSaved = {
10362
12358
  ok: false,
10363
12359
  problem: `${shown} could not be written, so the change was not saved.`
@@ -10378,9 +12374,9 @@ function writeSettingsText(setup, text4) {
10378
12374
  problem: `${shown} is a link or not an ordinary file, which Agent Lookout does not write through, so the change was not saved.`
10379
12375
  };
10380
12376
  }
10381
- temporary = path20.join(
12377
+ temporary = path24.join(
10382
12378
  folder,
10383
- `.${path20.basename(file)}.${process.pid}.${randomBytes3(6).toString("hex")}.tmp`
12379
+ `.${path24.basename(file)}.${process.pid}.${randomBytes3(6).toString("hex")}.tmp`
10384
12380
  );
10385
12381
  const fd = openWithoutFollowingNow(
10386
12382
  temporary,
@@ -10420,7 +12416,7 @@ var RULE_WORDS = {
10420
12416
  idle: "the idle rule",
10421
12417
  quietHours: "the quiet hours"
10422
12418
  };
10423
- function isRecord13(value) {
12419
+ function isRecord14(value) {
10424
12420
  return typeof value === "object" && value !== null && !Array.isArray(value);
10425
12421
  }
10426
12422
  function listOf(names) {
@@ -10458,7 +12454,7 @@ function rulesIn(read, shown) {
10458
12454
  } catch {
10459
12455
  value = null;
10460
12456
  }
10461
- if (!isRecord13(value)) {
12457
+ if (!isRecord14(value)) {
10462
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.`;
10463
12459
  return {
10464
12460
  rules: off,
@@ -10473,7 +12469,7 @@ function rulesIn(read, shown) {
10473
12469
  const { timeRules, permissionRules: heldRules, ...rest } = value;
10474
12470
  const permissions = readPermissionRules(heldRules);
10475
12471
  const { rules, unread } = readTimeRules(timeRules);
10476
- const otherRules = isRecord13(timeRules) ? Object.fromEntries(
12472
+ const otherRules = isRecord14(timeRules) ? Object.fromEntries(
10477
12473
  Object.entries(timeRules).filter(
10478
12474
  ([name]) => !TIME_RULE_NAMES.includes(name)
10479
12475
  )
@@ -10575,15 +12571,15 @@ import { randomBytes as randomBytes4 } from "node:crypto";
10575
12571
 
10576
12572
  // src/core/permission-rules/rulesChange.ts
10577
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.';
10578
- function isRecord14(value) {
12574
+ function isRecord15(value) {
10579
12575
  return typeof value === "object" && value !== null && !Array.isArray(value);
10580
12576
  }
10581
12577
  function rulesChangeIn(value) {
10582
12578
  const refused = { ok: false, problem: CHANGE_SHAPE };
10583
- if (!isRecord14(value) || Object.keys(value).length !== 1) return refused;
12579
+ if (!isRecord15(value) || Object.keys(value).length !== 1) return refused;
10584
12580
  const [kind] = Object.keys(value);
10585
12581
  const body = value[kind];
10586
- if (!isRecord14(body)) return refused;
12582
+ if (!isRecord15(body)) return refused;
10587
12583
  switch (kind) {
10588
12584
  case "add": {
10589
12585
  const read = ruleWordsIn(body);
@@ -10659,7 +12655,7 @@ function applyRulesChange(rules, change, newId) {
10659
12655
 
10660
12656
  // src/collector/settings/permissionRulesRoute.ts
10661
12657
  var MAX_PERMISSION_RULES_BODY_BYTES = 1024;
10662
- var failed3 = (status, reason, error) => ({
12658
+ var failed4 = (status, reason, error) => ({
10663
12659
  status,
10664
12660
  body: { error, reason }
10665
12661
  });
@@ -10686,14 +12682,14 @@ function createPermissionRulesRoute(options) {
10686
12682
  try {
10687
12683
  value = JSON.parse(body.text);
10688
12684
  } catch {
10689
- 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.");
10690
12686
  }
10691
12687
  const asked = rulesChangeIn(value);
10692
- if (!asked.ok) return failed3(400, "invalid", asked.problem);
12688
+ if (!asked.ok) return failed4(400, "invalid", asked.problem);
10693
12689
  const made = settings.changePermissionRules(
10694
12690
  (rules) => applyRulesChange(rules, asked.change, newId)
10695
12691
  );
10696
- 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);
10697
12693
  return {
10698
12694
  status: 200,
10699
12695
  body: { ok: true, permissionRules: made.rules }
@@ -10703,7 +12699,7 @@ function createPermissionRulesRoute(options) {
10703
12699
 
10704
12700
  // src/collector/settings/timeRulesRoute.ts
10705
12701
  var MAX_TIME_RULES_BODY_BYTES = 2048;
10706
- var failed4 = (status, reason, error) => ({
12702
+ var failed5 = (status, reason, error) => ({
10707
12703
  status,
10708
12704
  body: { error, reason }
10709
12705
  });
@@ -10727,12 +12723,12 @@ function createTimeRulesRoute(options) {
10727
12723
  try {
10728
12724
  value = JSON.parse(body.text);
10729
12725
  } catch {
10730
- 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.");
10731
12727
  }
10732
12728
  const asked = timeRulesIn(value);
10733
- if (!asked.ok) return failed4(400, "invalid", asked.problem);
12729
+ if (!asked.ok) return failed5(400, "invalid", asked.problem);
10734
12730
  const saved = settings.changeTimeRules(asked.rules);
10735
- if (!saved.ok) return failed4(500, "not-saved", saved.problem);
12731
+ if (!saved.ok) return failed5(500, "not-saved", saved.problem);
10736
12732
  return {
10737
12733
  status: 200,
10738
12734
  body: { ok: true, timeRules: asked.rules }
@@ -10776,11 +12772,11 @@ function createOsascriptRunner(options) {
10776
12772
  }
10777
12773
 
10778
12774
  // src/collector/terminal/tabFinder.ts
10779
- import os10 from "node:os";
12775
+ import os11 from "node:os";
10780
12776
  var TAB_LOOK_SOONEST_MS = 5e3;
10781
12777
  function createTabFinder(options) {
10782
12778
  const off = terminalJumpOff(options.env);
10783
- const homeDir = options.homeDir ?? os10.homedir();
12779
+ const homeDir = options.homeDir ?? os11.homedir();
10784
12780
  const readProcesses = options.readProcesses ?? readProcessTableWithPs;
10785
12781
  const now = options.now ?? Date.now;
10786
12782
  const soonestMs = options.soonestMs ?? TAB_LOOK_SOONEST_MS;
@@ -11014,96 +13010,24 @@ function ledgerHistoryStore(store, ledger) {
11014
13010
  }
11015
13011
 
11016
13012
  // src/collector/webhook/webhookSender.ts
11017
- var WEBHOOK_TIMEOUT_MS = 1e4;
11018
- function postHeaders(body, version) {
11019
- return {
11020
- "Content-Type": "application/json",
11021
- "Content-Length": String(Buffer.byteLength(body)),
11022
- "User-Agent": `Agent Lookout/${version}`
11023
- };
11024
- }
11025
- var TIMED_OUT = "the address did not answer in time";
11026
- function outcomeOfStatus(status) {
11027
- if (status >= 200 && status < 300) return { sent: true };
11028
- if (status >= 300 && status < 400) {
11029
- return { sent: false, reason: "the address answered with a redirect, which is not followed" };
11030
- }
11031
- if (status >= 400 && status < 500) {
11032
- return { sent: false, reason: `the address refused the post (status ${status})` };
11033
- }
11034
- if (status >= 500 && status < 600) {
11035
- return { sent: false, reason: `the receiving service had a problem (status ${status})` };
11036
- }
11037
- return { sent: false, reason: "the address's answer could not be understood" };
11038
- }
11039
- function failureReason2(error) {
11040
- const { code } = error ?? {};
11041
- if (typeof code !== "string") return "the post could not be sent";
11042
- if (code === "ECONNREFUSED") return "nothing answered at the address";
11043
- if (code === "ENOTFOUND" || code === "EAI_AGAIN") return "the address's host could not be found";
11044
- if (code === "ETIMEDOUT") return TIMED_OUT;
11045
- if (/CERT|SELF_SIGNED|UNABLE_TO|^ERR_TLS|^ERR_SSL|^EPROTO$/.test(code)) {
11046
- return "the secure connection to the address failed";
11047
- }
11048
- return "the connection to the address failed";
11049
- }
11050
- function requestOptions(body, version) {
11051
- return {
11052
- method: "POST",
11053
- headers: postHeaders(body, version),
11054
- // A connection of its own, closed after the answer.
11055
- agent: false,
11056
- rejectUnauthorized: true
11057
- };
11058
- }
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
+ };
11059
13020
  function createHttpSender(settings, options) {
11060
- const { url } = settings;
11061
- const timeoutMs = options.timeoutMs ?? WEBHOOK_TIMEOUT_MS;
11062
- let client = null;
11063
- const load = () => {
11064
- client ??= url.protocol === "https:" ? import("node:https").then(({ request: request2 }) => request2) : import("node:http").then(({ request: request2 }) => request2);
11065
- return client;
11066
- };
13021
+ const poster = createJsonPoster(settings.url, options);
11067
13022
  return {
11068
13023
  async send(post) {
11069
- let request2;
13024
+ let body;
11070
13025
  try {
11071
- request2 = await load();
13026
+ body = JSON.stringify(post);
11072
13027
  } catch {
11073
- return { sent: false, reason: "the post could not be sent" };
13028
+ return { sent: false, reason: WORDS.notSent };
11074
13029
  }
11075
- return new Promise((resolve) => {
11076
- let finished = false;
11077
- const finish = (outcome) => {
11078
- if (finished) return;
11079
- finished = true;
11080
- resolve(outcome);
11081
- };
11082
- let body;
11083
- let req;
11084
- try {
11085
- body = JSON.stringify(post);
11086
- req = request2(url, requestOptions(body, options.version));
11087
- } catch {
11088
- finish({ sent: false, reason: "the post could not be sent" });
11089
- return;
11090
- }
11091
- const timer = setTimeout(() => {
11092
- finish({ sent: false, reason: TIMED_OUT });
11093
- req.destroy();
11094
- }, timeoutMs);
11095
- timer.unref();
11096
- req.on("response", (res) => {
11097
- finish(outcomeOfStatus(res.statusCode ?? 0));
11098
- res.on("close", () => clearTimeout(timer));
11099
- res.resume();
11100
- });
11101
- req.on("error", (error) => {
11102
- clearTimeout(timer);
11103
- finish({ sent: false, reason: failureReason2(error) });
11104
- });
11105
- req.end(body);
11106
- });
13030
+ return outcomeOfPost(await poster.post(body), WORDS);
11107
13031
  }
11108
13032
  };
11109
13033
  }
@@ -11113,15 +13037,15 @@ var WEBHOOK_URL_ENV = "AGENT_LOOKOUT_WEBHOOK_URL";
11113
13037
  var WEBHOOK_EVENTS_ENV = "AGENT_LOOKOUT_WEBHOOK_EVENTS";
11114
13038
  var WEBHOOK_AFTER_ENV = "AGENT_LOOKOUT_WEBHOOK_AFTER";
11115
13039
  var WEBHOOK_ASKING_ENV = "AGENT_LOOKOUT_WEBHOOK_ASKING";
11116
- var MAX_URL_LENGTH = 2048;
11117
- 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://.`;
11118
13042
  function readUrl(value) {
11119
13043
  let url;
11120
13044
  try {
11121
- if (value.length > MAX_URL_LENGTH) throw new Error();
13045
+ if (value.length > MAX_URL_LENGTH2) throw new Error();
11122
13046
  url = new URL(value);
11123
13047
  } catch {
11124
- throw new SettingProblem(URL_UNREADABLE2);
13048
+ throw new SettingProblem(URL_UNREADABLE3);
11125
13049
  }
11126
13050
  const local = LOCAL_HOSTS.has(url.hostname);
11127
13051
  if (url.protocol !== "https:" && !(url.protocol === "http:" && local)) {
@@ -11129,7 +13053,7 @@ function readUrl(value) {
11129
13053
  `${WEBHOOK_URL_ENV} must begin with https://, or with http:// for an address on this computer, 127.0.0.1 or localhost.`
11130
13054
  );
11131
13055
  }
11132
- if (url.hostname === "") throw new SettingProblem(URL_UNREADABLE2);
13056
+ if (url.hostname === "") throw new SettingProblem(URL_UNREADABLE3);
11133
13057
  if (!isWebhookHost(url.hostname)) {
11134
13058
  throw new SettingProblem(
11135
13059
  `${WEBHOOK_URL_ENV} must name its host in letters, digits, dots and dashes, such as hooks.slack.com.`
@@ -11155,7 +13079,7 @@ function readWebhookSetup(env) {
11155
13079
  };
11156
13080
  } catch (error) {
11157
13081
  if (error instanceof SettingProblem) return { on: false, problem: error.message };
11158
- return { on: false, problem: URL_UNREADABLE2 };
13082
+ return { on: false, problem: URL_UNREADABLE3 };
11159
13083
  }
11160
13084
  }
11161
13085
  function webhookProblemLine(problem) {
@@ -11252,6 +13176,26 @@ function createCollector(options) {
11252
13176
  now
11253
13177
  }) : null;
11254
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);
11255
13199
  const remotes = createRemotes({
11256
13200
  env,
11257
13201
  version: options.version,
@@ -11272,6 +13216,7 @@ function createCollector(options) {
11272
13216
  platform
11273
13217
  }),
11274
13218
  createCodexAdapter({ env: options.env, now, pollIntervalMs: intervalMs }),
13219
+ createAntigravityAdapter({ env: options.env, now, pollIntervalMs: intervalMs, platform }),
11275
13220
  createStatusFileAdapter({ env: options.env, now, pollIntervalMs: intervalMs })
11276
13221
  ];
11277
13222
  const poller = createPoller({
@@ -11306,6 +13251,8 @@ function createCollector(options) {
11306
13251
  answering?.observe(snapshot);
11307
13252
  email?.handle(snapshot);
11308
13253
  webhook?.handle(snapshot);
13254
+ ntfy?.handle(snapshot);
13255
+ pushover?.handle(snapshot);
11309
13256
  notifications.handle(snapshot);
11310
13257
  options.onSnapshot?.(snapshot);
11311
13258
  }
@@ -11348,6 +13295,13 @@ function createCollector(options) {
11348
13295
  notifications,
11349
13296
  email: () => email?.status() ?? emailOff,
11350
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
+ }),
11351
13305
  pullRequests: () => pullRequests?.status() ?? pullRequestsOff,
11352
13306
  jump: createJumpRoute({ poller, panes, run: tmux, tabs, osascript, now }),
11353
13307
  stop: stopRoutes && createStopRoute(stopRoutes),
@@ -11442,13 +13396,20 @@ function createCollector(options) {
11442
13396
  poller,
11443
13397
  email,
11444
13398
  webhook,
13399
+ ntfy,
13400
+ pushover,
11445
13401
  pullRequests,
11446
13402
  history: keeper,
11447
13403
  answering: {
11448
13404
  start: () => answerStart ??= answering?.start() ?? Promise.resolve(),
11449
13405
  stop: async () => {
11450
13406
  await answering?.stop();
11451
- }
13407
+ },
13408
+ serve: (snapshot) => answering?.serve(snapshot) ?? snapshot,
13409
+ check: async () => {
13410
+ await answering?.check();
13411
+ },
13412
+ ...answering?.answer && { answer: answering.answer }
11452
13413
  },
11453
13414
  remotes,
11454
13415
  settings
@@ -11472,7 +13433,7 @@ async function runStandalone(options) {
11472
13433
  } catch (error) {
11473
13434
  return stopWith(error.message);
11474
13435
  }
11475
- if (!existsSync(path21.join(distDir, "index.html"))) {
13436
+ if (!existsSync(path25.join(distDir, "index.html"))) {
11476
13437
  return stopWith(
11477
13438
  "The dashboard has not been built yet. Run `npm run build`, then start it again."
11478
13439
  );
@@ -11515,12 +13476,12 @@ async function runStandalone(options) {
11515
13476
 
11516
13477
  // src/cli/start/openBrowser.ts
11517
13478
  import { spawn as spawn3 } from "node:child_process";
11518
- import path22 from "node:path";
13479
+ import path26 from "node:path";
11519
13480
  var OPEN_WAIT_MS = 1e4;
11520
13481
  var WINDOWS_FOLDER = "C:\\Windows";
11521
13482
  function windowsFolder(env) {
11522
13483
  const named = env.SystemRoot?.trim() || env.SYSTEMROOT?.trim();
11523
- return named && path22.win32.isAbsolute(named) ? named : WINDOWS_FOLDER;
13484
+ return named && path26.win32.isAbsolute(named) ? named : WINDOWS_FOLDER;
11524
13485
  }
11525
13486
  function openCommand(platform, url, env = process.env) {
11526
13487
  const address = loopbackAddress(url);
@@ -11529,7 +13490,7 @@ function openCommand(platform, url, env = process.env) {
11529
13490
  if (platform === "linux") return { file: "xdg-open", args: [address] };
11530
13491
  if (platform === "win32") {
11531
13492
  return {
11532
- file: path22.win32.join(windowsFolder(env), "System32", "rundll32.exe"),
13493
+ file: path26.win32.join(windowsFolder(env), "System32", "rundll32.exe"),
11533
13494
  args: ["url.dll,FileProtocolHandler", address]
11534
13495
  };
11535
13496
  }