agent-lookout 0.2.7 → 0.2.9

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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  sortSessions
3
- } from "./chunk-JSBQMY3Q.js";
3
+ } from "./chunk-CMDNRNYP.js";
4
4
  import {
5
5
  ANSWER_DECISIONS,
6
6
  CAPABILITIES,
@@ -34,6 +34,7 @@ import {
34
34
  isRemoteSource,
35
35
  isStatusEvent,
36
36
  loopbackAddress,
37
+ messageText,
37
38
  needsYou,
38
39
  noticeTitle,
39
40
  oneLine,
@@ -48,7 +49,7 @@ import {
48
49
  waitingPhrase,
49
50
  waitingText,
50
51
  withoutWaitingText
51
- } from "./chunk-I6Q35DHC.js";
52
+ } from "./chunk-XV7BYWVY.js";
52
53
 
53
54
  // src/collector/hosts/server.ts
54
55
  import { createServer } from "node:http";
@@ -132,14 +133,14 @@ function sessionChanges(memory, snapshot) {
132
133
  }
133
134
  const found = [];
134
135
  for (const session of snapshot.sessions) {
135
- const answer = answers2.get(session.source);
136
- if (!answer || answer.sessions.has(session.id)) continue;
137
- answer.sessions.set(session.id, withoutWaitingText(session));
136
+ const answer2 = answers2.get(session.source);
137
+ if (!answer2 || answer2.sessions.has(session.id)) continue;
138
+ answer2.sessions.set(session.id, withoutWaitingText(session));
138
139
  if (startedWaiting.has(session)) {
139
140
  found.push({ event: "needs-you", session });
140
141
  continue;
141
142
  }
142
- const before = memory.seen.get(session.source)?.get(answer.basis)?.get(session.id);
143
+ const before = memory.seen.get(session.source)?.get(answer2.basis)?.get(session.id);
143
144
  if (before && before.status !== session.status && isOverStatus(session.status)) {
144
145
  found.push({ event: session.status, session });
145
146
  }
@@ -186,6 +187,7 @@ function sessionChanges(memory, snapshot) {
186
187
 
187
188
  // src/core/api.ts
188
189
  var CLEAR_HISTORY_ACTION = "clear-history";
190
+ var LAST_MESSAGE_PATH = "/api/sessions/last-message";
189
191
  var TERMINAL_JUMP_TIMEOUT_MS = 6e4;
190
192
  var JUMP_INTERVAL_MS = 1e3;
191
193
  var STOP_ACTION = "stop";
@@ -217,6 +219,9 @@ var MAX_HOST_LENGTH = 253;
217
219
  function isWebhookHost(host) {
218
220
  return host.length <= MAX_HOST_LENGTH && WEBHOOK_HOST.test(host);
219
221
  }
222
+ var PHONE_CHANNELS = ["ntfy", "pushover"];
223
+ var PHONE_TEST_PATH = "/api/phone/test";
224
+ var PHONE_TEST_ACTION = "phone-test";
220
225
 
221
226
  // src/core/history.ts
222
227
  var DEFAULT_HISTORY_WINDOW_MS = 15 * 60 * 1e3;
@@ -583,7 +588,7 @@ function waitBegan(statusSince, seenAt) {
583
588
 
584
589
  // src/collector/outbound/outboundChannel.ts
585
590
  function createOutboundChannel(options) {
586
- const { afterMs, asking, waitMessage, overMessage, reminderMessage, summaryMessage, failure: failure2 } = options;
591
+ const { afterMs, asking, waitMessage: waitMessage2, overMessage: overMessage2, reminderMessage: reminderMessage2, summaryMessage: summaryMessage2, failure: failure2 } = options;
587
592
  const now = options.now ?? Date.now;
588
593
  const wanted = new Set(options.events);
589
594
  let memory = EMPTY_CHANGE_MEMORY;
@@ -595,15 +600,26 @@ function createOutboundChannel(options) {
595
600
  const hold = createQuietHold();
596
601
  let last = null;
597
602
  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
- }
603
+ function tryInTurn(content, after = () => {
604
+ }) {
605
+ const tried = queue.then(() => options.send(content)).then(
606
+ (outcome) => outcome,
607
+ // A sender should not reject. One that does is a failure like any other.
608
+ () => ({ sent: false, reason: failure2 })
609
+ ).then((outcome) => {
610
+ after(outcome);
611
+ return outcome;
612
+ });
613
+ queue = tried.then(
614
+ () => void 0,
615
+ () => void 0
606
616
  );
617
+ return tried;
618
+ }
619
+ function send2(content) {
620
+ void tryInTurn(content, (outcome) => {
621
+ last = outcome.sent ? { at: now(), sent: true } : { at: now(), sent: false, reason: outcome.reason };
622
+ });
607
623
  }
608
624
  return {
609
625
  handle(snapshot) {
@@ -626,7 +642,7 @@ function createOutboundChannel(options) {
626
642
  ...item,
627
643
  agent: agentName(item.session, snapshot.sources)
628
644
  }));
629
- ready.push((sentNow) => summaryMessage({ ...summary, items, now: sentNow }));
645
+ ready.push((sentNow) => summaryMessage2({ ...summary, items, now: sentNow }));
630
646
  }
631
647
  for (const { session, begunAt } of end.open) open2.set(session.id, begunAt);
632
648
  }
@@ -644,7 +660,7 @@ function createOutboundChannel(options) {
644
660
  agent: agentName(session, snapshot.sources),
645
661
  seenAt: at
646
662
  };
647
- ready.push((sentNow) => overMessage({ ...facts, now: sentNow }));
663
+ ready.push((sentNow) => overMessage2({ ...facts, now: sentNow }));
648
664
  continue;
649
665
  }
650
666
  const since = session.statusSince;
@@ -674,7 +690,7 @@ function createOutboundChannel(options) {
674
690
  open2.delete(id);
675
691
  sentAt.push(at);
676
692
  send2(
677
- waitMessage({
693
+ waitMessage2({
678
694
  session: withoutWaitingText(session),
679
695
  agent: agentName(session, snapshot.sources),
680
696
  // Read now, from this poll's snapshot, and only when it is to go.
@@ -695,7 +711,7 @@ function createOutboundChannel(options) {
695
711
  sentAt.push(at);
696
712
  reminders.told(due.session.id, at);
697
713
  send2(
698
- reminderMessage({
714
+ reminderMessage2({
699
715
  session: withoutWaitingText(due.session),
700
716
  agent: agentName(due.session, snapshot.sources),
701
717
  asking: asking ? waitingText(due.session.waitingText) ?? null : null,
@@ -718,7 +734,15 @@ function createOutboundChannel(options) {
718
734
  },
719
735
  last: () => last,
720
736
  limitedUntil: () => limitLiftsAt(sentAt, now()),
721
- settled: () => queue
737
+ settled: () => queue,
738
+ async test(content) {
739
+ const at = now();
740
+ sentAt = sendsInLastHour(sentAt, at);
741
+ const limitedUntil = limitLiftsAt(sentAt, at);
742
+ if (limitedUntil !== null) return { tried: false, limitedUntil };
743
+ sentAt.push(at);
744
+ return { tried: true, at, outcome: await tryInTurn(content) };
745
+ }
722
746
  };
723
747
  }
724
748
 
@@ -1286,6 +1310,116 @@ function pullRequestsOffStatus(problem) {
1286
1310
  return { on: false, problem, gh: null, last: null };
1287
1311
  }
1288
1312
 
1313
+ // src/collector/outbound/phoneMessage.ts
1314
+ var MAX_PHONE_TITLE = 250;
1315
+ var MAX_PHONE_MESSAGE = 1024;
1316
+ function isWaitKind(kind) {
1317
+ return kind === "needs-you" || kind === "reminder" || kind === "test";
1318
+ }
1319
+ function cutTo(text4, max) {
1320
+ const characters = Array.from(text4);
1321
+ if (characters.length <= max) return text4;
1322
+ return `${characters.slice(0, max - 1).join("").trimEnd()}\u2026`;
1323
+ }
1324
+ function phoneMessage(kind, title, lines) {
1325
+ const said = lines.filter((line) => line !== "");
1326
+ return {
1327
+ kind,
1328
+ title: cutTo(title, MAX_PHONE_TITLE),
1329
+ message: cutTo(said.length > 0 ? said.join("\n") : title, MAX_PHONE_MESSAGE)
1330
+ };
1331
+ }
1332
+ function factsLine(first, session, agent) {
1333
+ const facts = [
1334
+ first,
1335
+ session.project ? oneLine(session.project) : "",
1336
+ surfaceLabel(session.surface) ?? "",
1337
+ agent ? oneLine(agent) : ""
1338
+ ];
1339
+ return facts.filter((fact) => fact !== "").join(" \xB7 ");
1340
+ }
1341
+ function askingLine(asking) {
1342
+ const said = waitingText(asking);
1343
+ return said ? `Asking: ${said}` : "";
1344
+ }
1345
+ function waitMessage(facts) {
1346
+ const { session, begunAt, now } = facts;
1347
+ return phoneMessage("needs-you", `${oneLine(sessionTitle(session))} ${waitingPhrase(session)}`, [
1348
+ askingLine(facts.asking),
1349
+ factsLine(formatDuration(Math.max(0, now - begunAt)), session, facts.agent)
1350
+ ]);
1351
+ }
1352
+ function reminderMessage(facts) {
1353
+ const { session, begunAt, now } = facts;
1354
+ return phoneMessage("reminder", reminderSentence(session, now - begunAt), [
1355
+ askingLine(facts.asking),
1356
+ factsLine(`Since ${clockAt(begunAt, now)}`, session, facts.agent)
1357
+ ]);
1358
+ }
1359
+ function summaryMessage(facts) {
1360
+ return phoneMessage("quiet-summary", WHILE_QUIET, [summaryLine(facts.items)]);
1361
+ }
1362
+ function overMessage(facts) {
1363
+ const { session, seenAt, now } = facts;
1364
+ return phoneMessage(facts.event, `${oneLine(sessionTitle(session))} ${overPhrase(facts.event)}`, [
1365
+ factsLine(`Seen at ${clockAt(seenAt, now)}`, session, facts.agent)
1366
+ ]);
1367
+ }
1368
+ function testMessage() {
1369
+ return phoneMessage("test", "Agent Lookout test", [
1370
+ "Pushes from Agent Lookout reach this device."
1371
+ ]);
1372
+ }
1373
+
1374
+ // src/collector/ntfy/ntfyNotifications.ts
1375
+ function ntfyOffStatus(problem) {
1376
+ return {
1377
+ on: false,
1378
+ host: null,
1379
+ tokenSet: null,
1380
+ events: null,
1381
+ afterMs: null,
1382
+ asking: null,
1383
+ problem,
1384
+ last: null,
1385
+ limitedUntil: null
1386
+ };
1387
+ }
1388
+ function createNtfyNotifications(options) {
1389
+ const { settings, sender } = options;
1390
+ const channel = createOutboundChannel({
1391
+ events: settings.events,
1392
+ afterMs: settings.afterMs,
1393
+ asking: settings.asking,
1394
+ waitMessage,
1395
+ overMessage,
1396
+ reminderMessage,
1397
+ summaryMessage,
1398
+ send: (message) => sender.send(message),
1399
+ failure: "the push could not be sent",
1400
+ now: options.now
1401
+ });
1402
+ return {
1403
+ handle: (snapshot) => channel.handle(snapshot),
1404
+ status() {
1405
+ return {
1406
+ on: true,
1407
+ // The host alone. The topic is the secret anyone could read the pushes with.
1408
+ host: settings.server.hostname,
1409
+ tokenSet: settings.token !== null,
1410
+ events: [...settings.events],
1411
+ afterMs: settings.afterMs,
1412
+ asking: settings.asking,
1413
+ problem: null,
1414
+ last: channel.last(),
1415
+ limitedUntil: channel.limitedUntil()
1416
+ };
1417
+ },
1418
+ test: () => channel.test(testMessage()),
1419
+ settled: () => channel.settled()
1420
+ };
1421
+ }
1422
+
1289
1423
  // src/core/sessions/diff.ts
1290
1424
  function severityFor(to) {
1291
1425
  if (to === "needs-you") return "warning";
@@ -1638,6 +1772,50 @@ function createPoller(options) {
1638
1772
  };
1639
1773
  }
1640
1774
 
1775
+ // src/collector/pushover/pushoverNotifications.ts
1776
+ function pushoverOffStatus(problem) {
1777
+ return {
1778
+ on: false,
1779
+ events: null,
1780
+ afterMs: null,
1781
+ asking: null,
1782
+ problem,
1783
+ last: null,
1784
+ limitedUntil: null
1785
+ };
1786
+ }
1787
+ function createPushoverNotifications(options) {
1788
+ const { settings, sender } = options;
1789
+ const channel = createOutboundChannel({
1790
+ events: settings.events,
1791
+ afterMs: settings.afterMs,
1792
+ asking: settings.asking,
1793
+ waitMessage,
1794
+ overMessage,
1795
+ reminderMessage,
1796
+ summaryMessage,
1797
+ send: (message) => sender.send(message),
1798
+ failure: "the push could not be sent",
1799
+ now: options.now
1800
+ });
1801
+ return {
1802
+ handle: (snapshot) => channel.handle(snapshot),
1803
+ status() {
1804
+ return {
1805
+ on: true,
1806
+ events: [...settings.events],
1807
+ afterMs: settings.afterMs,
1808
+ asking: settings.asking,
1809
+ problem: null,
1810
+ last: channel.last(),
1811
+ limitedUntil: channel.limitedUntil()
1812
+ };
1813
+ },
1814
+ test: () => channel.test(testMessage()),
1815
+ settled: () => channel.settled()
1816
+ };
1817
+ }
1818
+
1641
1819
  // src/collector/webhook/webhookMessage.ts
1642
1820
  var MAX_SUMMARY_ITEMS = 100;
1643
1821
  function slackSafe(text4) {
@@ -1891,6 +2069,13 @@ function send(res, status, body, headers = {}) {
1891
2069
  function fail(res, status, error, headers = {}) {
1892
2070
  send(res, status, { error }, headers);
1893
2071
  }
2072
+ function sendWhenReady(res, answer2) {
2073
+ answer2.then((ready) => send(res, ready.status, ready.body, ready.headers)).catch(() => {
2074
+ if (res.headersSent) res.end();
2075
+ else fail(res, 500, "The collector ran into an unexpected problem.");
2076
+ }).catch(() => {
2077
+ });
2078
+ }
1894
2079
  function notificationsSaid(req) {
1895
2080
  const value = req.headers[NOTIFICATIONS_HEADER.toLowerCase()];
1896
2081
  if (typeof value !== "string") return null;
@@ -1913,6 +2098,7 @@ function createApiHandler(options) {
1913
2098
  if (options.answer) actions.set(ANSWER_PATH, options.answer);
1914
2099
  if (options.timeRules) actions.set(TIME_RULES_PATH, options.timeRules);
1915
2100
  if (options.permissionRules) actions.set(PERMISSION_RULES_PATH, options.permissionRules);
2101
+ if (options.phoneTest) actions.set(PHONE_TEST_PATH, options.phoneTest);
1916
2102
  const serveSnapshot = options.serveSnapshot ?? ((snapshot) => snapshot);
1917
2103
  function route(req, res) {
1918
2104
  const refusal3 = refusalFor(req);
@@ -1928,11 +2114,7 @@ function createApiHandler(options) {
1928
2114
  }
1929
2115
  const action = url === null ? void 0 : actions.get(url.pathname);
1930
2116
  if (action) {
1931
- action(req).then((answer2) => send(res, answer2.status, answer2.body, answer2.headers)).catch(() => {
1932
- if (res.headersSent) res.end();
1933
- else fail(res, 500, "The collector ran into an unexpected problem.");
1934
- }).catch(() => {
1935
- });
2117
+ sendWhenReady(res, action(req));
1936
2118
  return;
1937
2119
  }
1938
2120
  if (req.method !== "GET") {
@@ -1954,6 +2136,14 @@ function createApiHandler(options) {
1954
2136
  send(res, 200, serveSnapshot(poller.getSnapshot()));
1955
2137
  return;
1956
2138
  }
2139
+ case LAST_MESSAGE_PATH: {
2140
+ if (options.lastMessage) {
2141
+ sendWhenReady(res, options.lastMessage(req, url));
2142
+ return;
2143
+ }
2144
+ fail(res, 404, "There is nothing at that address.");
2145
+ return;
2146
+ }
1957
2147
  case "/api/events": {
1958
2148
  const since = numberParam(url.searchParams.get("since"));
1959
2149
  if (since === "invalid") {
@@ -1975,6 +2165,22 @@ function createApiHandler(options) {
1975
2165
  );
1976
2166
  return;
1977
2167
  }
2168
+ case "/api/ntfy": {
2169
+ send(
2170
+ res,
2171
+ 200,
2172
+ options.ntfy ? options.ntfy() : ntfyOffStatus(null)
2173
+ );
2174
+ return;
2175
+ }
2176
+ case "/api/pushover": {
2177
+ send(
2178
+ res,
2179
+ 200,
2180
+ options.pushover ? options.pushover() : pushoverOffStatus(null)
2181
+ );
2182
+ return;
2183
+ }
1978
2184
  case "/api/pull-requests": {
1979
2185
  send(
1980
2186
  res,
@@ -2023,7 +2229,7 @@ function createApiHandler(options) {
2023
2229
  }
2024
2230
  }
2025
2231
  }
2026
- function answer(req, res) {
2232
+ function answer2(req, res) {
2027
2233
  try {
2028
2234
  route(req, res);
2029
2235
  } catch {
@@ -2034,12 +2240,12 @@ function createApiHandler(options) {
2034
2240
  return (req, res) => {
2035
2241
  const reading = options.ready?.() ?? null;
2036
2242
  if (reading === null) {
2037
- answer(req, res);
2243
+ answer2(req, res);
2038
2244
  return;
2039
2245
  }
2040
2246
  void reading.then(
2041
- () => answer(req, res),
2042
- () => answer(req, res)
2247
+ () => answer2(req, res),
2248
+ () => answer2(req, res)
2043
2249
  );
2044
2250
  };
2045
2251
  }
@@ -2258,7 +2464,7 @@ function urlFor(address) {
2258
2464
 
2259
2465
  // src/collector/hosts/standalone.ts
2260
2466
  import { existsSync } from "node:fs";
2261
- import path21 from "node:path";
2467
+ import path26 from "node:path";
2262
2468
 
2263
2469
  // src/core/mapping/claudeCodeMapping.ts
2264
2470
  var LIVE_STATUS = /* @__PURE__ */ new Map([
@@ -2645,6 +2851,16 @@ import os2 from "node:os";
2645
2851
  // src/collector/adapters/claude-code/feed.ts
2646
2852
  import { spawn } from "node:child_process";
2647
2853
 
2854
+ // src/collector/processes/childEnvironment.ts
2855
+ var OWN_SETTING = /^AGENT_LOOKOUT_/i;
2856
+ function childEnvironment(env) {
2857
+ const given = {};
2858
+ for (const [name, value] of Object.entries(env)) {
2859
+ if (!OWN_SETTING.test(name)) given[name] = value;
2860
+ }
2861
+ return given;
2862
+ }
2863
+
2648
2864
  // src/collector/processes/pids.ts
2649
2865
  function validPid(value) {
2650
2866
  return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
@@ -2761,7 +2977,7 @@ var runProgram = (file, args, options) => new Promise((resolve) => {
2761
2977
  }, options.timeoutMs);
2762
2978
  try {
2763
2979
  child = spawn(file, [...args], {
2764
- env: options.env,
2980
+ env: childEnvironment(options.env),
2765
2981
  shell: false,
2766
2982
  // stdin is the null device, so a program that reads it gets end-of-file.
2767
2983
  // stderr is not wanted and goes the same way.
@@ -3068,6 +3284,12 @@ function createStopTargets() {
3068
3284
  };
3069
3285
  }
3070
3286
 
3287
+ // src/collector/messages/lastMessageSettings.ts
3288
+ var LAST_MESSAGE_ENV = "AGENT_LOOKOUT_LAST_MESSAGE";
3289
+ function lastMessageOff(env) {
3290
+ return env[LAST_MESSAGE_ENV]?.trim().toLowerCase() === "off";
3291
+ }
3292
+
3071
3293
  // src/collector/processes/ps.ts
3072
3294
  import { execFile } from "node:child_process";
3073
3295
  var PS_TIMEOUT_MS = 2e3;
@@ -3114,6 +3336,9 @@ function lstartMs(value) {
3114
3336
  const [day, hours, minutes, seconds, year] = match.slice(2).map(Number);
3115
3337
  return Date.UTC(year, month, day, hours, minutes, seconds);
3116
3338
  }
3339
+ function processStartTime(value) {
3340
+ return lstartMs(collapse(value)) ?? null;
3341
+ }
3117
3342
  function compareProcessStart(recorded, actual, withinMs = SAME_START_WITHIN_MS) {
3118
3343
  if (recorded === void 0 || actual === void 0) return "unknown";
3119
3344
  const before = collapse(recorded);
@@ -3138,8 +3363,8 @@ function parseProcessStarts(stdout) {
3138
3363
  }
3139
3364
  var readProcessStartsWithPs = async (pids) => {
3140
3365
  if (pids.length === 0) return /* @__PURE__ */ new Map();
3141
- const answer = await runPs(psStartArgs(pids), { utc: true });
3142
- return parseProcessStarts(answer.stdout);
3366
+ const answer2 = await runPs(psStartArgs(pids), { utc: true });
3367
+ return parseProcessStarts(answer2.stdout);
3143
3368
  };
3144
3369
  function createProcessStartCheck(read = readProcessStartsWithPs, now = Date.now, ttlMs = START_CHECK_TTL_MS) {
3145
3370
  const remembered = /* @__PURE__ */ new Map();
@@ -3480,13 +3705,13 @@ function uniqueById(sessions) {
3480
3705
  }
3481
3706
 
3482
3707
  // src/collector/adapters/claude-code/stopOffers.ts
3483
- function jobIdOf(entry, answer) {
3484
- const listed = answer.find(
3708
+ function jobIdOf(entry, answer2) {
3709
+ const listed = answer2.find(
3485
3710
  (candidate) => candidate.id !== void 0 && (candidate.sessionId === entry.sessionId || candidate.sessionId === void 0 && candidate.pid === entry.pid)
3486
3711
  );
3487
3712
  return listed?.id !== void 0 && JOB_ID.test(listed.id) ? listed.id : void 0;
3488
3713
  }
3489
- function targetFor(entry, answer, options) {
3714
+ function targetFor(entry, answer2, options) {
3490
3715
  if (entry.sessionId === void 0 || entry.procStart === void 0) return null;
3491
3716
  const surface = mapClaudeCodeSurface(entry.entrypoint);
3492
3717
  const found = {
@@ -3499,12 +3724,12 @@ function targetFor(entry, answer, options) {
3499
3724
  return surface === "terminal" || surface === "vscode" ? { ...found, how: "signal" } : null;
3500
3725
  }
3501
3726
  if (entry.kind === "bg" && options.background && surface !== "desktop") {
3502
- const jobId = answer === null ? void 0 : jobIdOf(entry, answer);
3727
+ const jobId = answer2 === null ? void 0 : jobIdOf(entry, answer2);
3503
3728
  return jobId === void 0 ? null : { ...found, how: "background", jobId };
3504
3729
  }
3505
3730
  return null;
3506
3731
  }
3507
- function withStopOffers(sessions, live2, answer, options) {
3732
+ function withStopOffers(sessions, live2, answer2, options) {
3508
3733
  const byId = /* @__PURE__ */ new Map();
3509
3734
  for (const entry of live2) {
3510
3735
  if (entry.sessionId !== void 0) byId.set(`${SOURCE_ID}:${entry.sessionId}`, entry);
@@ -3515,7 +3740,7 @@ function withStopOffers(sessions, live2, answer, options) {
3515
3740
  if (session.status === "finished" || session.status === "failed") return session;
3516
3741
  const entry = byId.get(session.id);
3517
3742
  if (entry === void 0 || entry.pid !== session.pid) return session;
3518
- const target = targetFor(entry, answer, options);
3743
+ const target = targetFor(entry, answer2, options);
3519
3744
  if (target === null) return session;
3520
3745
  targets.set(session.id, target);
3521
3746
  return { ...session, stop: { how: target.how } };
@@ -3523,8 +3748,8 @@ function withStopOffers(sessions, live2, answer, options) {
3523
3748
  return { sessions: offered, targets };
3524
3749
  }
3525
3750
 
3526
- // src/collector/adapters/claude-code/transcript/waitingTexts.ts
3527
- import path8 from "node:path";
3751
+ // src/collector/adapters/claude-code/transcript/lastMessages.ts
3752
+ import path9 from "node:path";
3528
3753
 
3529
3754
  // src/collector/files/readOnlyIo.ts
3530
3755
  import { constants as constants4 } from "node:fs";
@@ -3578,7 +3803,7 @@ function isMissing2(error) {
3578
3803
  return code === "ENOENT" || code === "ENOTDIR";
3579
3804
  }
3580
3805
 
3581
- // src/collector/adapters/claude-code/transcript/lastAsk.ts
3806
+ // src/collector/adapters/claude-code/transcript/transcriptLines.ts
3582
3807
  function isRecord3(value) {
3583
3808
  return typeof value === "object" && value !== null && !Array.isArray(value);
3584
3809
  }
@@ -3604,6 +3829,112 @@ function linesOf(tail, fromStart) {
3604
3829
  }
3605
3830
  return lines;
3606
3831
  }
3832
+
3833
+ // src/collector/adapters/claude-code/transcript/lastSaid.ts
3834
+ var PART_BREAK = "\n\n";
3835
+ function textsOf(line) {
3836
+ if (line.type !== "assistant" || line.isSidechain === true || line.isMeta === true) return [];
3837
+ const texts = [];
3838
+ for (const block of blocksOf(line)) {
3839
+ const text4 = block.type === "text" ? nonEmpty(block.text) : null;
3840
+ if (text4 !== null) texts.push(text4);
3841
+ }
3842
+ return texts;
3843
+ }
3844
+ function messageIdOf(line) {
3845
+ return isRecord3(line.message) ? nonEmpty(line.message.id) : null;
3846
+ }
3847
+ function lastSaidInTail(tail, fromStart) {
3848
+ const lines = linesOf(tail, fromStart);
3849
+ const newest2 = lines.findLastIndex((line) => textsOf(line).length > 0);
3850
+ if (newest2 === -1) return { text: null, reason: fromStart ? "nothing-yet" : "too-far-back" };
3851
+ const parts = [textsOf(lines[newest2])];
3852
+ const messageId = messageIdOf(lines[newest2]);
3853
+ if (messageId === null) return { text: parts[0].join(PART_BREAK) };
3854
+ let index = newest2 - 1;
3855
+ for (; index >= 0; index -= 1) {
3856
+ const line = lines[index];
3857
+ if (line.isSidechain === true) continue;
3858
+ if (line.type === "user") break;
3859
+ if (line.type !== "assistant") continue;
3860
+ if (messageIdOf(line) !== messageId) break;
3861
+ parts.unshift(textsOf(line));
3862
+ }
3863
+ const text4 = parts.flat().join(PART_BREAK);
3864
+ return index < 0 && !fromStart ? { text: text4, startCut: true } : { text: text4 };
3865
+ }
3866
+
3867
+ // src/collector/adapters/claude-code/transcript/transcriptFile.ts
3868
+ import path7 from "node:path";
3869
+ var TRANSCRIPT_TAIL_BYTES = 256 * 1024;
3870
+ var SESSION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
3871
+ function isSessionId(value) {
3872
+ return typeof value === "string" && SESSION_ID.test(value);
3873
+ }
3874
+ var ID_PREFIX = "claude-code:";
3875
+ function sessionIdOf(session) {
3876
+ if (session.source !== "claude-code" || !session.id.startsWith(ID_PREFIX)) return null;
3877
+ const id = session.id.slice(ID_PREFIX.length);
3878
+ return isSessionId(id) ? id : null;
3879
+ }
3880
+ function projectFolderName(cwd) {
3881
+ return cwd.replace(/[^A-Za-z0-9]/g, "-");
3882
+ }
3883
+ function fileName(sessionId) {
3884
+ return `${sessionId}.jsonl`;
3885
+ }
3886
+ async function findTranscript(projectsDir, sessionId, cwd, io) {
3887
+ if (!isSessionId(sessionId)) return null;
3888
+ const name = fileName(sessionId);
3889
+ const isFile2 = async (file) => {
3890
+ try {
3891
+ return (await io.lstat(file)).kind === "file";
3892
+ } catch {
3893
+ return false;
3894
+ }
3895
+ };
3896
+ if (cwd !== null && cwd !== "") {
3897
+ const likely = path7.join(projectsDir, projectFolderName(cwd), name);
3898
+ if (await isFile2(likely)) return likely;
3899
+ }
3900
+ let folders;
3901
+ try {
3902
+ folders = await io.readdir(projectsDir);
3903
+ } catch {
3904
+ return null;
3905
+ }
3906
+ const candidates = folders.filter((folder) => folder !== "" && !folder.startsWith(".")).sort().map((folder) => path7.join(projectsDir, folder, name));
3907
+ const found = await Promise.all(candidates.map(isFile2));
3908
+ return candidates.find((_, index) => found[index]) ?? null;
3909
+ }
3910
+ function stampOf(info) {
3911
+ return { size: info.size, mtimeMs: info.mtimeMs, ino: info.ino };
3912
+ }
3913
+ function sameStamp(a, b) {
3914
+ return a.size === b.size && a.mtimeMs === b.mtimeMs && a.ino === b.ino;
3915
+ }
3916
+ async function readTranscriptTail(file, io, known, maxBytes = TRANSCRIPT_TAIL_BYTES) {
3917
+ const open2 = await io.openRegular(file);
3918
+ try {
3919
+ const stamp = stampOf(open2.info);
3920
+ if (known !== null && sameStamp(known, stamp)) return { unchanged: true, stamp };
3921
+ const start = Math.max(0, stamp.size - maxBytes);
3922
+ const bytes3 = await open2.read(start, stamp.size - start);
3923
+ return {
3924
+ unchanged: false,
3925
+ stamp,
3926
+ tail: new TextDecoder("utf-8").decode(bytes3),
3927
+ fromStart: start === 0
3928
+ };
3929
+ } finally {
3930
+ await open2.close();
3931
+ }
3932
+ }
3933
+
3934
+ // src/collector/adapters/claude-code/transcript/waitingTexts.ts
3935
+ import path8 from "node:path";
3936
+
3937
+ // src/collector/adapters/claude-code/transcript/lastAsk.ts
3607
3938
  function isPrompt(line) {
3608
3939
  if (line.isMeta === true) return false;
3609
3940
  const message = isRecord3(line.message) ? line.message : null;
@@ -3702,91 +4033,24 @@ function askedInTail(tail, fromStart, cwd) {
3702
4033
  return waitingText(describeToolUse(pending.name, pending.input, cwd));
3703
4034
  }
3704
4035
 
3705
- // src/collector/adapters/claude-code/transcript/transcriptFile.ts
3706
- import path7 from "node:path";
3707
- var TRANSCRIPT_TAIL_BYTES = 256 * 1024;
3708
- var SESSION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
3709
- function isSessionId(value) {
3710
- return typeof value === "string" && SESSION_ID.test(value);
3711
- }
3712
- function projectFolderName(cwd) {
3713
- return cwd.replace(/[^A-Za-z0-9]/g, "-");
3714
- }
3715
- function fileName(sessionId) {
3716
- return `${sessionId}.jsonl`;
4036
+ // src/collector/adapters/claude-code/transcript/waitingTexts.ts
4037
+ var WAITING_TEXT_ENV = "AGENT_LOOKOUT_WAITING_TEXT";
4038
+ function waitingTextOff(env) {
4039
+ return env[WAITING_TEXT_ENV]?.trim().toLowerCase() === "off";
3717
4040
  }
3718
- async function findTranscript(projectsDir, sessionId, cwd, io) {
3719
- if (!isSessionId(sessionId)) return null;
3720
- const name = fileName(sessionId);
3721
- const isFile2 = async (file) => {
3722
- try {
3723
- return (await io.lstat(file)).kind === "file";
3724
- } catch {
3725
- return false;
3726
- }
3727
- };
3728
- if (cwd !== null && cwd !== "") {
3729
- const likely = path7.join(projectsDir, projectFolderName(cwd), name);
3730
- if (await isFile2(likely)) return likely;
3731
- }
3732
- let folders;
3733
- try {
3734
- folders = await io.readdir(projectsDir);
3735
- } catch {
3736
- return null;
3737
- }
3738
- const candidates = folders.filter((folder) => folder !== "" && !folder.startsWith(".")).sort().map((folder) => path7.join(projectsDir, folder, name));
3739
- const found = await Promise.all(candidates.map(isFile2));
3740
- return candidates.find((_, index) => found[index]) ?? null;
3741
- }
3742
- function stampOf(info) {
3743
- return { size: info.size, mtimeMs: info.mtimeMs, ino: info.ino };
3744
- }
3745
- function sameStamp(a, b) {
3746
- return a.size === b.size && a.mtimeMs === b.mtimeMs && a.ino === b.ino;
3747
- }
3748
- async function readTranscriptTail(file, io, known, maxBytes = TRANSCRIPT_TAIL_BYTES) {
3749
- const open2 = await io.openRegular(file);
3750
- try {
3751
- const stamp = stampOf(open2.info);
3752
- if (known !== null && sameStamp(known, stamp)) return { unchanged: true, stamp };
3753
- const start = Math.max(0, stamp.size - maxBytes);
3754
- const bytes2 = await open2.read(start, stamp.size - start);
3755
- return {
3756
- unchanged: false,
3757
- stamp,
3758
- tail: new TextDecoder("utf-8").decode(bytes2),
3759
- fromStart: start === 0
3760
- };
3761
- } finally {
3762
- await open2.close();
3763
- }
3764
- }
3765
-
3766
- // src/collector/adapters/claude-code/transcript/waitingTexts.ts
3767
- var WAITING_TEXT_ENV = "AGENT_LOOKOUT_WAITING_TEXT";
3768
- function waitingTextOff(env) {
3769
- return env[WAITING_TEXT_ENV]?.trim().toLowerCase() === "off";
3770
- }
3771
- var LOOK_AGAIN_MS = 1e4;
3772
- var ID_PREFIX = "claude-code:";
3773
- function sessionIdOf(session) {
3774
- if (session.source !== "claude-code" || !session.id.startsWith(ID_PREFIX)) return null;
3775
- const id = session.id.slice(ID_PREFIX.length);
3776
- return isSessionId(id) ? id : null;
3777
- }
3778
- function createWaitingTextReader(options) {
3779
- const io = options.io ?? nodeIo2;
3780
- const now = options.now ?? Date.now;
3781
- const projectsDir = path8.join(options.claudeHome, "projects");
3782
- const kept = /* @__PURE__ */ new Map();
3783
- async function textFor(session, sessionId) {
3784
- const at = now();
3785
- let wait = kept.get(session.id);
3786
- if (wait === void 0 || wait.statusSince !== session.statusSince || wait.file === null && at - wait.lookedAt >= LOOK_AGAIN_MS) {
3787
- const file = await findTranscript(projectsDir, sessionId, session.cwd, io);
3788
- wait = { statusSince: session.statusSince, file, lookedAt: at, stamp: null, text: void 0 };
3789
- kept.set(session.id, wait);
4041
+ var LOOK_AGAIN_MS = 1e4;
4042
+ function createWaitingTextReader(options) {
4043
+ const io = options.io ?? nodeIo2;
4044
+ const now = options.now ?? Date.now;
4045
+ const projectsDir = path8.join(options.claudeHome, "projects");
4046
+ const kept = /* @__PURE__ */ new Map();
4047
+ async function textFor(session, sessionId) {
4048
+ const at = now();
4049
+ let wait = kept.get(session.id);
4050
+ if (wait === void 0 || wait.statusSince !== session.statusSince || wait.file === null && at - wait.lookedAt >= LOOK_AGAIN_MS) {
4051
+ const file = await findTranscript(projectsDir, sessionId, session.cwd, io);
4052
+ wait = { statusSince: session.statusSince, file, lookedAt: at, stamp: null, text: void 0 };
4053
+ kept.set(session.id, wait);
3790
4054
  }
3791
4055
  if (wait.file === null) return void 0;
3792
4056
  try {
@@ -3832,6 +4096,160 @@ function createWaitingTextReader(options) {
3832
4096
  };
3833
4097
  }
3834
4098
 
4099
+ // src/collector/adapters/claude-code/transcript/lastMessages.ts
4100
+ var ANSWER_AGAIN_MS = 1e3;
4101
+ var READS_PER_SECOND = 4;
4102
+ var MAX_KEPT = 8;
4103
+ var KEEP_MS = 15e3;
4104
+ var NOT_FOUND = { message: null, reason: "not-found" };
4105
+ var UNREADABLE = { message: null, reason: "unreadable" };
4106
+ function runLater(run, ms) {
4107
+ setTimeout(run, ms).unref();
4108
+ }
4109
+ function answerFrom(tail, fromStart) {
4110
+ const said = lastSaidInTail(tail, fromStart);
4111
+ if (said.text === null) return { message: null, reason: said.reason };
4112
+ const shown = messageText(said.text);
4113
+ if (shown === void 0) {
4114
+ return { message: null, reason: fromStart ? "nothing-yet" : "too-far-back" };
4115
+ }
4116
+ return { message: { text: shown.text, cut: shown.cut || said.startCut === true } };
4117
+ }
4118
+ function createLastMessageReader(options) {
4119
+ const io = options.io ?? nodeIo2;
4120
+ const now = options.now ?? Date.now;
4121
+ const later = options.later ?? runLater;
4122
+ const projectsDir = path9.join(options.claudeHome, "projects");
4123
+ const listed = /* @__PURE__ */ new Map();
4124
+ const kept = /* @__PURE__ */ new Map();
4125
+ let reads = [];
4126
+ let dropSet = false;
4127
+ function dropOld(at) {
4128
+ for (const [id, entry] of kept) {
4129
+ if (at - entry.askedAt >= KEEP_MS) kept.delete(id);
4130
+ }
4131
+ }
4132
+ function dropOnTime(at) {
4133
+ if (dropSet || kept.size === 0) return;
4134
+ const oldest = Math.min(...[...kept.values()].map((entry) => entry.askedAt));
4135
+ dropSet = true;
4136
+ later(
4137
+ () => {
4138
+ dropSet = false;
4139
+ const then = now();
4140
+ dropOld(then);
4141
+ dropOnTime(then);
4142
+ },
4143
+ Math.max(0, oldest + KEEP_MS - at)
4144
+ );
4145
+ }
4146
+ function mayRead(at) {
4147
+ reads = reads.filter((started) => at - started < 1e3 && started <= at);
4148
+ if (reads.length >= READS_PER_SECOND) return false;
4149
+ reads.push(at);
4150
+ return true;
4151
+ }
4152
+ function makeRoom() {
4153
+ while (kept.size >= MAX_KEPT) {
4154
+ let leastRecent;
4155
+ for (const pair of kept) {
4156
+ if (leastRecent === void 0 || pair[1].askedAt < leastRecent[1].askedAt) {
4157
+ leastRecent = pair;
4158
+ }
4159
+ }
4160
+ if (leastRecent === void 0) return;
4161
+ kept.delete(leastRecent[0]);
4162
+ }
4163
+ }
4164
+ async function check(entry, target, at) {
4165
+ try {
4166
+ if (entry.file === null) {
4167
+ entry.lookedAt = at;
4168
+ entry.file = await findTranscript(projectsDir, target.sessionId, target.cwd, io);
4169
+ if (entry.file === null) return NOT_FOUND;
4170
+ }
4171
+ const read = await readTranscriptTail(entry.file, io, entry.last?.stamp ?? null);
4172
+ if (read.unchanged) return entry.last?.answer ?? UNREADABLE;
4173
+ const answer2 = answerFrom(read.tail, read.fromStart);
4174
+ entry.last = { stamp: read.stamp, answer: answer2 };
4175
+ return answer2;
4176
+ } catch (error) {
4177
+ entry.last = null;
4178
+ if (isMissing2(error)) {
4179
+ entry.file = null;
4180
+ entry.lookedAt = at;
4181
+ return NOT_FOUND;
4182
+ }
4183
+ return UNREADABLE;
4184
+ }
4185
+ }
4186
+ return {
4187
+ read(id) {
4188
+ const target = listed.get(id);
4189
+ if (target === void 0) return Promise.resolve(null);
4190
+ if (target === null) return Promise.resolve(NOT_FOUND);
4191
+ const at = now();
4192
+ dropOld(at);
4193
+ const entry = kept.get(id);
4194
+ if (entry !== void 0) {
4195
+ entry.askedAt = at;
4196
+ dropOnTime(at);
4197
+ if (entry.checking !== null) return entry.checking;
4198
+ if (entry.answer !== null && at - entry.checkedAt < ANSWER_AGAIN_MS) {
4199
+ return Promise.resolve(entry.answer);
4200
+ }
4201
+ if (entry.answer !== null && entry.file === null && entry.lookedAt !== null && at - entry.lookedAt < LOOK_AGAIN_MS) {
4202
+ return Promise.resolve(entry.answer);
4203
+ }
4204
+ }
4205
+ if (!mayRead(at)) return Promise.resolve("busy");
4206
+ let fresh = entry;
4207
+ if (fresh === void 0) {
4208
+ makeRoom();
4209
+ fresh = {
4210
+ file: null,
4211
+ lookedAt: null,
4212
+ last: null,
4213
+ answer: null,
4214
+ checkedAt: at,
4215
+ askedAt: at,
4216
+ checking: null
4217
+ };
4218
+ kept.set(id, fresh);
4219
+ dropOnTime(at);
4220
+ }
4221
+ const reading = fresh;
4222
+ const checking = check(reading, target, at).then((answer2) => {
4223
+ reading.answer = answer2;
4224
+ reading.checkedAt = at;
4225
+ reading.checking = null;
4226
+ return answer2;
4227
+ });
4228
+ reading.checking = checking;
4229
+ return checking;
4230
+ },
4231
+ keep(sessions) {
4232
+ listed.clear();
4233
+ for (const session of sessions) {
4234
+ const sessionId = sessionIdOf(session);
4235
+ listed.set(session.id, sessionId === null ? null : { sessionId, cwd: session.cwd });
4236
+ }
4237
+ for (const id of kept.keys()) {
4238
+ if (!listed.has(id)) kept.delete(id);
4239
+ }
4240
+ dropOld(now());
4241
+ },
4242
+ forget() {
4243
+ listed.clear();
4244
+ kept.clear();
4245
+ reads = [];
4246
+ },
4247
+ get size() {
4248
+ return kept.size;
4249
+ }
4250
+ };
4251
+ }
4252
+
3835
4253
  // src/collector/adapters/claude-code/index.ts
3836
4254
  var CLAUDE_HOME_ENV = "AGENT_LOOKOUT_CLAUDE_HOME";
3837
4255
  var CLAUDE_FEED_ENV = "AGENT_LOOKOUT_CLAUDE_FEED";
@@ -3932,6 +4350,8 @@ function createClaudeCodeAdapter(options = {}) {
3932
4350
  const homeName = tildify(claudeHome, homeDir, platform);
3933
4351
  const registryName = tildify(sessionsDir, homeDir, platform);
3934
4352
  const waitingTexts = transcriptsOff ? null : createWaitingTextReader({ claudeHome, io: options.transcriptIo, now });
4353
+ const lastMessagesOff = lastMessageOff(env);
4354
+ const lastMessages = transcriptsOff || lastMessagesOff ? null : createLastMessageReader({ claudeHome, io: options.transcriptIo, now });
3935
4355
  let lastDue = null;
3936
4356
  let lastRead = null;
3937
4357
  let feedSoon = false;
@@ -3953,7 +4373,7 @@ function createClaudeCodeAdapter(options = {}) {
3953
4373
  { label: "Command run", value: commandRun },
3954
4374
  {
3955
4375
  label: "Transcript read",
3956
- value: transcriptsOff ? "Off" : "last message of a waiting session"
4376
+ value: transcriptsOff ? "Off" : lastMessagesOff ? "last message of a waiting session" : "last message of a waiting session, and of one whose details are open"
3957
4377
  }
3958
4378
  ];
3959
4379
  }
@@ -4049,12 +4469,12 @@ function createClaudeCodeAdapter(options = {}) {
4049
4469
  }
4050
4470
  reading ??= await readRegistryNow(checkedAt);
4051
4471
  const read = lastRead;
4052
- const answer = read?.feed.ok ? read.feed.entries : null;
4472
+ const answer2 = read?.feed.ok ? read.feed.entries : null;
4053
4473
  const problem = problemWith(reading, read);
4054
4474
  const { registry, live: live2 } = reading;
4055
4475
  const offered = (result) => {
4056
4476
  if (!stops) return result;
4057
- const found = withStopOffers(result.sessions, live2, answer, {
4477
+ const found = withStopOffers(result.sessions, live2, answer2, {
4058
4478
  sessionsDir,
4059
4479
  background: !neverRun
4060
4480
  });
@@ -4063,7 +4483,7 @@ function createClaudeCodeAdapter(options = {}) {
4063
4483
  };
4064
4484
  if (options.panes || options.terminals) {
4065
4485
  const pids = new Set(live2.map((entry) => entry.pid));
4066
- for (const entry of answer ?? []) {
4486
+ for (const entry of answer2 ?? []) {
4067
4487
  if (entry.pid !== void 0 && isAlive(entry.pid)) pids.add(entry.pid);
4068
4488
  }
4069
4489
  await Promise.all([options.panes?.look([...pids]), options.terminals?.look([...pids])]);
@@ -4083,16 +4503,16 @@ function createClaudeCodeAdapter(options = {}) {
4083
4503
  ...advice2 !== void 0 && { advice: advice2 },
4084
4504
  checkedAt
4085
4505
  });
4086
- if (answer !== null && read !== null && problem === null) {
4087
- const listedPids = new Set(answer.map((entry) => entry.pid));
4088
- const listedSessionIds = new Set(answer.map((entry) => entry.sessionId));
4506
+ if (answer2 !== null && read !== null && problem === null) {
4507
+ const listedPids = new Set(answer2.map((entry) => entry.pid));
4508
+ const listedSessionIds = new Set(answer2.map((entry) => entry.sessionId));
4089
4509
  const tooNewSince = read.askedAt - NEW_SESSION_GRACE_MS;
4090
4510
  const fromRegistry = live2.filter(
4091
4511
  (entry) => listedPids.has(entry.pid) || listedSessionIds.has(entry.sessionId) || startedAfter(entry, tooNewSince, checkedAt)
4092
4512
  );
4093
4513
  const livePids = new Set(live2.map((entry) => entry.pid));
4094
4514
  const liveSessionIds = new Set(live2.map((entry) => entry.sessionId));
4095
- const jobs = answer.filter(
4515
+ const jobs = answer2.filter(
4096
4516
  (entry) => (entry.id !== void 0 || entry.state !== void 0) && !(entry.pid !== void 0 && livePids.has(entry.pid)) && !(entry.sessionId !== void 0 && liveSessionIds.has(entry.sessionId))
4097
4517
  ).map((entry) => {
4098
4518
  const gone = entry.pid !== void 0 && !isAlive(entry.pid);
@@ -4113,9 +4533,9 @@ function createClaudeCodeAdapter(options = {}) {
4113
4533
  basis: "registry+feed"
4114
4534
  });
4115
4535
  }
4116
- if (answer !== null && problem !== null) {
4536
+ if (answer2 !== null && problem !== null) {
4117
4537
  const entries = registry.readable ? registry.entries : void 0;
4118
- const sessions = answer.map(
4538
+ const sessions = answer2.map(
4119
4539
  (entry) => sessionFromFeed(
4120
4540
  entry,
4121
4541
  entry.pid !== void 0 ? entries?.get(entry.pid) : void 0,
@@ -4169,9 +4589,12 @@ function createClaudeCodeAdapter(options = {}) {
4169
4589
  lookingIn: neverRun ? `Looking for sessions in ${registryName}.` : `Looking for sessions in ${registryName} and with claude ${FEED_ARGS.join(" ")}.`,
4170
4590
  async poll() {
4171
4591
  try {
4172
- return await withWaitingText(await poll());
4592
+ const result = await withWaitingText(await poll());
4593
+ lastMessages?.keep(result.sessions);
4594
+ return result;
4173
4595
  } catch {
4174
4596
  await waitingTexts?.annotate([]);
4597
+ lastMessages?.forget();
4175
4598
  stops?.set(/* @__PURE__ */ new Map());
4176
4599
  return {
4177
4600
  health: {
@@ -4189,6 +4612,19 @@ function createClaudeCodeAdapter(options = {}) {
4189
4612
  sessions: []
4190
4613
  };
4191
4614
  }
4615
+ },
4616
+ async lastMessage(sessionId) {
4617
+ if (lastMessages === null) {
4618
+ return {
4619
+ message: null,
4620
+ reason: "off",
4621
+ setting: transcriptsOff ? WAITING_TEXT_ENV : LAST_MESSAGE_ENV
4622
+ };
4623
+ }
4624
+ return lastMessages.read(sessionId);
4625
+ },
4626
+ get lastMessagesKept() {
4627
+ return lastMessages?.size ?? 0;
4192
4628
  }
4193
4629
  };
4194
4630
  }
@@ -4204,8 +4640,8 @@ function parseProcessParents(stdout) {
4204
4640
  return parents;
4205
4641
  }
4206
4642
  var readProcessParentsWithPs = async () => {
4207
- const answer = await runPs(PS_PARENTS_ARGS);
4208
- return answer.ok ? parseProcessParents(answer.stdout) : /* @__PURE__ */ new Map();
4643
+ const answer2 = await runPs(PS_PARENTS_ARGS);
4644
+ return answer2.ok ? parseProcessParents(answer2.stdout) : /* @__PURE__ */ new Map();
4209
4645
  };
4210
4646
 
4211
4647
  // src/collector/actions/stopSession.ts
@@ -4659,9 +5095,21 @@ async function pollAfterAnswer(poller, sessionId) {
4659
5095
  function answerRefusalFor(req) {
4660
5096
  return actionRefusalFor(req, ANSWER_ACTION, MAX_ANSWER_BODY_BYTES);
4661
5097
  }
4662
- function createAnswerRoute(options) {
5098
+ function createAnswerer(options) {
4663
5099
  const { poller, events, asks } = options;
4664
5100
  const now = options.now ?? Date.now;
5101
+ return async (sessionId, requestId, decision) => {
5102
+ const session = poller.getSnapshot().sessions.find((listed) => listed.id === sessionId);
5103
+ if (!session) return "no-ask";
5104
+ const outcome = await asks.answer(sessionId, requestId, decision);
5105
+ if (outcome !== "answered") return outcome;
5106
+ events.add([answeredEvent(session, decision, now())]);
5107
+ await pollAfterAnswer(poller, sessionId);
5108
+ return "answered";
5109
+ };
5110
+ }
5111
+ function createAnswerRoute(options) {
5112
+ const answer2 = createAnswerer(options);
4665
5113
  return async function answerPermission(req) {
4666
5114
  const refused = answerRefusalFor(req);
4667
5115
  if (refused) {
@@ -4680,12 +5128,8 @@ function createAnswerRoute(options) {
4680
5128
  'The body must be {"sessionId": "...", "requestId": "...", "decision": "allow" or "deny"} and nothing else.'
4681
5129
  );
4682
5130
  }
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);
5131
+ const outcome = await answer2(asked.sessionId, asked.requestId, asked.decision);
4686
5132
  if (outcome !== "answered") return answerFailed(outcome);
4687
- events.add([answeredEvent(session, asked.decision, now())]);
4688
- await pollAfterAnswer(poller, asked.sessionId);
4689
5133
  return {
4690
5134
  status: 200,
4691
5135
  body: { ok: true, decision: asked.decision }
@@ -4983,7 +5427,7 @@ function createHeldAsks(options) {
4983
5427
  if (inFlight === run) inFlight = null;
4984
5428
  }
4985
5429
  }
4986
- async function answer(sessionId, requestId, decision) {
5430
+ async function answer2(sessionId, requestId, decision) {
4987
5431
  const entry = held.get(sessionId);
4988
5432
  if (!entry || entry.requestId !== requestId || !entry.confirmed) return "no-ask";
4989
5433
  if (decision === "allow" && !entry.shown.allow) return "not-allowable";
@@ -5015,7 +5459,7 @@ function createHeldAsks(options) {
5015
5459
  const verdict = verdictOf(entry);
5016
5460
  if (verdict === null) return;
5017
5461
  entry.byRule = true;
5018
- void answer(entry.sessionId, entry.requestId, verdict.decision).then((outcome) => {
5462
+ void answer2(entry.sessionId, entry.requestId, verdict.decision).then((outcome) => {
5019
5463
  if (outcome === "answered") rules.answered(request2, verdict);
5020
5464
  });
5021
5465
  }
@@ -5072,7 +5516,7 @@ function createHeldAsks(options) {
5072
5516
  void check();
5073
5517
  },
5074
5518
  check,
5075
- answer,
5519
+ answer: answer2,
5076
5520
  withAsks(snapshot) {
5077
5521
  const answering = status();
5078
5522
  if (held.size === 0) return { ...snapshot, answering };
@@ -5135,7 +5579,7 @@ function createHeldAsks(options) {
5135
5579
  import { chmod, lstat as lstat3, mkdir, stat as stat4, unlink } from "node:fs/promises";
5136
5580
  import http from "node:http";
5137
5581
  import net from "node:net";
5138
- import path9 from "node:path";
5582
+ import path10 from "node:path";
5139
5583
  var HOOK_PATH = "/hooks/permission-request";
5140
5584
  var HOOK_HEADER = "x-agent-lookout-hook";
5141
5585
  var HOOK_HEADER_VALUE = "permission-request";
@@ -5208,7 +5652,7 @@ function createHookSocket(options) {
5208
5652
  const maxBody = options.maxBodyBytes ?? MAX_HOOK_BODY_BYTES;
5209
5653
  const bodyTimeoutMs = options.bodyTimeoutMs ?? HOOK_BODY_TIMEOUT_MS;
5210
5654
  const uid = options.uid ?? process.getuid?.() ?? -1;
5211
- const folder = path9.dirname(socketPath);
5655
+ const folder = path10.dirname(socketPath);
5212
5656
  let server = null;
5213
5657
  let made = null;
5214
5658
  async function handle(req, res) {
@@ -5331,12 +5775,12 @@ function createHookSocket(options) {
5331
5775
 
5332
5776
  // src/collector/answers/registryStatus.ts
5333
5777
  import os4 from "node:os";
5334
- import path10 from "node:path";
5778
+ import path11 from "node:path";
5335
5779
  function createRegistryStatus(options) {
5336
5780
  const homeDir = options.homeDir ?? os4.homedir();
5337
5781
  const readFile2 = options.readFile ?? readRegularFile;
5338
- const home = options.env[CLAUDE_HOME_ENV]?.trim() || path10.join(homeDir, ".claude");
5339
- const sessionsDir = path10.join(path10.resolve(home), "sessions");
5782
+ const home = options.env[CLAUDE_HOME_ENV]?.trim() || path11.join(homeDir, ".claude");
5783
+ const sessionsDir = path11.join(path11.resolve(home), "sessions");
5340
5784
  return {
5341
5785
  async statusOf(sessionId) {
5342
5786
  const unknown = { status: "unknown" };
@@ -5347,7 +5791,7 @@ function createRegistryStatus(options) {
5347
5791
  const ownId = sessionId.slice("claude-code:".length);
5348
5792
  let content;
5349
5793
  try {
5350
- content = await readFile2(path10.join(sessionsDir, `${pid}.json`));
5794
+ content = await readFile2(path11.join(sessionsDir, `${pid}.json`));
5351
5795
  } catch {
5352
5796
  return unknown;
5353
5797
  }
@@ -5621,18 +6065,18 @@ function namesOwnFiles(words, ownPaths = [], folder) {
5621
6065
  (part) => from !== void 0 && !part.startsWith("/") && !part.startsWith("-") ? [part, `${from}/${part}`] : [part]
5622
6066
  );
5623
6067
  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}`));
6068
+ const path28 = plainPath(part);
6069
+ if (path28 === BY_NUMBER || path28.startsWith(`${BY_NUMBER}/`)) return true;
6070
+ if (own.some((name) => path28 === name || path28.startsWith(`${name}/`))) return true;
6071
+ if (above.has(path28)) return true;
6072
+ return ownNames.some((name) => path28 === name || path28.endsWith(`/${name}`));
5629
6073
  });
5630
6074
  });
5631
6075
  }
5632
6076
  function folderReachesOwnFiles(folder, ownPaths = []) {
5633
6077
  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}/`));
6078
+ const path28 = plainPath(folder);
6079
+ return own.some((name) => name === path28 || name.startsWith(`${path28 === "/" ? "" : path28}/`));
5636
6080
  }
5637
6081
  var PLAIN_WORD = /^[A-Za-z0-9_\-.,/:=+@%]+$/;
5638
6082
  var ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
@@ -5985,6 +6429,8 @@ function createAnswering(options) {
5985
6429
  observe: () => {
5986
6430
  },
5987
6431
  answeredIn: () => /* @__PURE__ */ new Set(),
6432
+ check: async () => {
6433
+ },
5988
6434
  ruleAnswers: () => []
5989
6435
  };
5990
6436
  }
@@ -6012,6 +6458,7 @@ function createAnswering(options) {
6012
6458
  let problem = null;
6013
6459
  let listening = false;
6014
6460
  const answerRoute = createAnswerRoute({ poller, events, asks, now: options.now });
6461
+ const answer2 = createAnswerer({ poller, events, asks, now: options.now });
6015
6462
  return {
6016
6463
  async start() {
6017
6464
  const started = await socket.start();
@@ -6044,6 +6491,9 @@ function createAnswering(options) {
6044
6491
  req.resume();
6045
6492
  return answerRefusalFor(req) ?? refusal(405, "This address only answers GET requests.", { Allow: "GET" });
6046
6493
  },
6494
+ // The same answer, from the host's own process. With no socket nothing is held.
6495
+ answer: async (sessionId, requestId, decision) => problem === null ? answer2(sessionId, requestId, decision) : "unavailable",
6496
+ check: () => asks.check(),
6047
6497
  ruleAnswers: () => ruleAnswers.recent()
6048
6498
  };
6049
6499
  }
@@ -6271,169 +6721,1532 @@ function waitTotals(input) {
6271
6721
  };
6272
6722
  }
6273
6723
 
6274
- // src/collector/adapters/codex/index.ts
6724
+ // src/collector/adapters/antigravity/index.ts
6275
6725
  import os5 from "node:os";
6276
- import path12 from "node:path";
6726
+ import path15 from "node:path";
6277
6727
 
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";
6728
+ // src/core/mapping/antigravityLiveness.ts
6729
+ function holdConversations(processes, conversations, listedAt = null) {
6730
+ const held = /* @__PURE__ */ new Set();
6731
+ let complete = true;
6732
+ for (const { startedAt, conversation } of processes) {
6733
+ if (conversation !== void 0) held.add(conversation);
6734
+ if (startedAt === null) {
6735
+ complete = false;
6736
+ continue;
6737
+ }
6738
+ let wrote = false;
6739
+ for (const { id, writtenAt } of conversations) {
6740
+ if (writtenAt === null || writtenAt < startedAt) continue;
6741
+ held.add(id);
6742
+ wrote = true;
6743
+ }
6744
+ if (!wrote && conversation === void 0) complete = false;
6294
6745
  }
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;
6746
+ if (listedAt !== null) {
6747
+ for (const { id, writtenAt } of conversations) {
6748
+ if (writtenAt !== null && writtenAt > listedAt) held.add(id);
6749
+ }
6307
6750
  }
6308
- return match[4] === void 0;
6751
+ return { held, complete };
6309
6752
  }
6310
- function codexLiveness(fields) {
6311
- if (!fields.lockFolder) return "unknown";
6312
- if (fields.locked) return true;
6313
- return keepsWriterLocks(fields.cliVersion) ? false : "unknown";
6753
+ function antigravityLiveness(id, holding) {
6754
+ if (holding === null) return "unknown";
6755
+ if (holding.held.has(id)) return true;
6756
+ return holding.complete ? false : "unknown";
6314
6757
  }
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);
6758
+
6759
+ // src/core/mapping/antigravityLog.ts
6760
+ var GLOG_LINE = /^[IWEF](\d{2})(\d{2}) (\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))? +\d+ ([\w.-]+):\d+\] (.*)$/;
6761
+ var SOURCE2 = {
6762
+ backend: "server.go",
6763
+ created: "server.go",
6764
+ streaming: "conversation_manager.go",
6765
+ asking: "tool_confirmation_manager.go",
6766
+ answered: "input_loop.go"
6767
+ };
6768
+ 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}";
6769
+ var OPENED = new RegExp(`^(Created|Streaming) conversation (${UUID})$`);
6770
+ var BACKEND = /^Creating CLI server backend: (?:.* )?workspaceDirs=\[([^\]\s]+)\](?: |$)/;
6771
+ var ASKING = /^Surfacing tool confirmation: "([A-Za-z]{1,64})" at step (\d{1,9})$/;
6772
+ var ANSWERED = new RegExp(
6773
+ `^Responding to tool confirmation: convID=(${UUID}), stepIdx=(\\d{1,9}), approved=(?:true|false)(?:, .*)?$`
6774
+ );
6775
+ var ABSOLUTE = /^(?:\/|[A-Za-z]:[\\/])/;
6776
+ var UNPRINTABLE = /[\p{Cc}\p{Cf}]/u;
6777
+ function readAgyLogLine(line) {
6778
+ const glog = GLOG_LINE.exec(line);
6779
+ if (glog === null) return null;
6780
+ const source = glog[7];
6781
+ const message = glog[8];
6782
+ const opened = OPENED.exec(message);
6783
+ if (opened !== null) {
6784
+ const from = opened[1] === "Created" ? SOURCE2.created : SOURCE2.streaming;
6785
+ return source === from ? { kind: "opened", conversation: opened[2].toLowerCase() } : null;
6786
+ }
6787
+ if (source === SOURCE2.backend) {
6788
+ const backend = BACKEND.exec(message);
6789
+ return backend !== null && ABSOLUTE.test(backend[1]) && !UNPRINTABLE.test(backend[1]) ? { kind: "folder", folder: backend[1] } : null;
6790
+ }
6791
+ if (source === SOURCE2.answered) {
6792
+ const answered = ANSWERED.exec(message);
6793
+ return answered === null ? null : { kind: "answered", conversation: answered[1].toLowerCase(), step: Number(answered[2]) };
6794
+ }
6795
+ if (source !== SOURCE2.asking) return null;
6796
+ const asking = ASKING.exec(message);
6797
+ if (asking !== null) {
6798
+ const fraction = (glog[6] ?? "0").padEnd(3, "0").slice(0, 3);
6799
+ return {
6800
+ kind: "asking",
6801
+ tool: asking[1],
6802
+ step: Number(asking[2]),
6803
+ at: {
6804
+ month: Number(glog[1]),
6805
+ day: Number(glog[2]),
6806
+ hour: Number(glog[3]),
6807
+ minute: Number(glog[4]),
6808
+ second: Number(glog[5]),
6809
+ millisecond: Number(fraction)
6810
+ }
6811
+ };
6812
+ }
6813
+ return null;
6318
6814
  }
6319
- function isRecord8(value) {
6320
- return typeof value === "object" && value !== null && !Array.isArray(value);
6815
+ function emptyAgyLogState() {
6816
+ return { folder: null, folderNamed: false, opened: /* @__PURE__ */ new Set(), current: null, ask: null };
6321
6817
  }
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";
6818
+ function addAgyLogLine(state, line) {
6819
+ switch (line.kind) {
6820
+ case "folder":
6821
+ if (!state.folderNamed) state.folder = line.folder;
6822
+ state.folderNamed = true;
6823
+ return;
6824
+ case "opened":
6825
+ state.opened.add(line.conversation);
6826
+ if (state.current !== line.conversation) state.ask = null;
6827
+ state.current = line.conversation;
6828
+ return;
6829
+ case "asking":
6830
+ state.ask = state.current === null ? null : { conversation: state.current, step: line.step, tool: line.tool, at: line.at };
6831
+ return;
6832
+ case "answered":
6833
+ if (state.ask?.conversation === line.conversation && state.ask.step === line.step) {
6834
+ state.ask = null;
6835
+ }
6836
+ return;
6837
+ }
6329
6838
  }
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;
6839
+ var ASK_STEPS_AHEAD = 2;
6840
+ function waitsForApproval(ask, conversation, lastIndex) {
6841
+ if (ask === null || ask.conversation !== conversation || lastIndex === null) return false;
6842
+ return ask.step > lastIndex && ask.step <= lastIndex + ASK_STEPS_AHEAD;
6843
+ }
6844
+ function agyLogTimeAt(time, near) {
6845
+ const year = new Date(near).getFullYear();
6846
+ let best = null;
6847
+ for (const y of [year - 1, year, year + 1]) {
6848
+ const at = new Date(
6849
+ y,
6850
+ time.month - 1,
6851
+ time.day,
6852
+ time.hour,
6853
+ time.minute,
6854
+ time.second,
6855
+ time.millisecond
6856
+ );
6857
+ if (at.getMonth() !== time.month - 1 || at.getDate() !== time.day) continue;
6858
+ const ms = at.getTime();
6859
+ if (best === null || Math.abs(ms - near) < Math.abs(best - near)) best = ms;
6336
6860
  }
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;
6861
+ return best;
6340
6862
  }
6341
6863
 
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;
6864
+ // src/collector/adapters/antigravity/agyLogs.ts
6865
+ import path12 from "node:path";
6866
+ var LOG_DIR = "log";
6867
+ var LOG_LIMIT_BYTES = 4 * 1024 * 1024;
6868
+ var LINE_LIMIT_BYTES = 64 * 1024;
6869
+ var LOG_NAMED_WITHIN_MS = 1e4;
6870
+ var PS_LATE_MS = 2e3;
6871
+ var RELIST_MS = 1e4;
6872
+ var LOG_NAME = /^cli-(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})\.log$/;
6347
6873
  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
- ])
6874
+ function logStartOf(name) {
6875
+ const match = LOG_NAME.exec(name);
6876
+ if (match === null) return null;
6877
+ const [year, month, day, hour, minute, second] = match.slice(1).map(Number);
6878
+ const at = new Date(year, month - 1, day, hour, minute, second);
6879
+ if (at.getMonth() !== month - 1 || at.getDate() !== day || at.getHours() !== hour) return null;
6880
+ return at.getTime();
6881
+ }
6882
+ function matchLogs(processes, logs) {
6883
+ const candidates = (startedAt) => startedAt === null ? [] : [...logs].filter(
6884
+ ([, at]) => at >= startedAt - PS_LATE_MS && at - startedAt <= LOG_NAMED_WITHIN_MS
6374
6885
  );
6886
+ const owners = /* @__PURE__ */ new Map();
6887
+ for (const { startedAt } of processes) {
6888
+ for (const [name] of candidates(startedAt)) owners.set(name, (owners.get(name) ?? 0) + 1);
6889
+ }
6890
+ return processes.map(({ startedAt }) => {
6891
+ const found = candidates(startedAt);
6892
+ if (found.length !== 1) return null;
6893
+ const [name] = found[0];
6894
+ return owners.get(name) === 1 ? name : null;
6895
+ });
6375
6896
  }
6376
- function parse(line) {
6377
- try {
6378
- const value = JSON.parse(line);
6379
- return isRecord9(value) ? value : null;
6380
- } catch {
6381
- return null;
6897
+ function createAgyLogReader(options) {
6898
+ const { io } = options;
6899
+ const now = options.now ?? Date.now;
6900
+ const logDir = path12.join(options.home, LOG_DIR);
6901
+ const cursors = /* @__PURE__ */ new Map();
6902
+ let listed = /* @__PURE__ */ new Map();
6903
+ let listedFor = "";
6904
+ let listedAt = -Infinity;
6905
+ async function list(processes) {
6906
+ const key = processes.map((p) => p.startedAt ?? "?").join(",");
6907
+ const since = now() - listedAt;
6908
+ if (key === listedFor && since >= 0 && since < RELIST_MS) return;
6909
+ listedFor = key;
6910
+ listedAt = now();
6911
+ let names;
6912
+ try {
6913
+ names = await io.readdir(logDir);
6914
+ } catch {
6915
+ listed = /* @__PURE__ */ new Map();
6916
+ return;
6917
+ }
6918
+ const next = /* @__PURE__ */ new Map();
6919
+ for (const name of names) {
6920
+ const at = logStartOf(name);
6921
+ if (at !== null) next.set(name, at);
6922
+ }
6923
+ listed = next;
6382
6924
  }
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];
6925
+ function addLines(cursor, data) {
6926
+ let buffer = cursor.pending.byteLength > 0 ? Buffer.concat([cursor.pending, data]) : data;
6927
+ if (cursor.skipping) {
6928
+ const end = buffer.indexOf(NEWLINE);
6929
+ if (end === -1) return;
6930
+ buffer = buffer.subarray(end + 1);
6931
+ cursor.skipping = false;
6932
+ }
6933
+ let start = 0;
6934
+ let newline = buffer.indexOf(NEWLINE);
6935
+ while (newline !== -1) {
6936
+ const line = readAgyLogLine(buffer.toString("utf8", start, newline));
6937
+ if (line !== null) addAgyLogLine(cursor.state, line);
6938
+ start = newline + 1;
6939
+ newline = buffer.indexOf(NEWLINE, start);
6940
+ }
6941
+ const rest = buffer.subarray(start);
6942
+ cursor.skipping = rest.byteLength > LINE_LIMIT_BYTES;
6943
+ cursor.pending = cursor.skipping ? Buffer.alloc(0) : Buffer.from(rest);
6401
6944
  }
6402
- return meta;
6403
- }
6404
- function readTurnLine(line) {
6405
- if (!line.includes('"event_msg"') || !TURN_HINT.test(line)) return null;
6406
- const value = parse(line);
6407
- if (!value || value.type !== "event_msg" || !isRecord9(value.payload)) return null;
6408
- const turn = value.payload.type;
6409
- if (typeof turn !== "string" || !TURN_TYPE.test(turn)) return null;
6410
- return {
6411
- turn,
6412
- at: parseCodexTime(value.timestamp),
6413
- imported: isImportedCodexTurn(value.payload.turn_id)
6414
- };
6945
+ async function readLog(file, info) {
6946
+ const known = cursors.get(file);
6947
+ if (known && known.ino === info.ino && known.size === info.size && known.mtimeMs === info.mtimeMs) {
6948
+ return known.state;
6949
+ }
6950
+ const handle = await io.openRegular(file);
6951
+ try {
6952
+ const opened = handle.info;
6953
+ const onward = known !== void 0 && known.ino === opened.ino && opened.size >= known.offset;
6954
+ const cursor = onward ? { ...known, state: known.state } : {
6955
+ ino: opened.ino,
6956
+ size: 0,
6957
+ mtimeMs: 0,
6958
+ offset: 0,
6959
+ pending: Buffer.alloc(0),
6960
+ skipping: false,
6961
+ state: emptyAgyLogState()
6962
+ };
6963
+ let from = cursor.offset;
6964
+ if (opened.size - from > LOG_LIMIT_BYTES) {
6965
+ from = opened.size - LOG_LIMIT_BYTES;
6966
+ cursor.state.ask = null;
6967
+ cursor.pending = Buffer.alloc(0);
6968
+ cursor.skipping = true;
6969
+ addLines(cursor, Buffer.from(await handle.read(from, opened.size - from)));
6970
+ } else if (opened.size > from) {
6971
+ addLines(cursor, Buffer.from(await handle.read(from, opened.size - from)));
6972
+ }
6973
+ cursor.offset = opened.size;
6974
+ cursor.size = opened.size;
6975
+ cursor.mtimeMs = opened.mtimeMs;
6976
+ cursor.ino = opened.ino;
6977
+ cursors.set(file, cursor);
6978
+ return cursor.state;
6979
+ } finally {
6980
+ await handle.close();
6981
+ }
6982
+ }
6983
+ async function read(processes) {
6984
+ if (processes.length === 0) {
6985
+ cursors.clear();
6986
+ return [];
6987
+ }
6988
+ await list(processes);
6989
+ const names = matchLogs(processes, listed);
6990
+ const kept = /* @__PURE__ */ new Set();
6991
+ const states = [];
6992
+ for (const name of names) {
6993
+ if (name === null) {
6994
+ states.push(null);
6995
+ continue;
6996
+ }
6997
+ const file = path12.join(logDir, name);
6998
+ kept.add(file);
6999
+ try {
7000
+ const info = await io.lstat(file);
7001
+ states.push(info.kind === "file" ? await readLog(file, info) : null);
7002
+ } catch {
7003
+ states.push(null);
7004
+ }
7005
+ }
7006
+ for (const file of cursors.keys()) {
7007
+ if (!kept.has(file)) cursors.delete(file);
7008
+ }
7009
+ return states;
7010
+ }
7011
+ return {
7012
+ async read(processes) {
7013
+ try {
7014
+ return await read(processes);
7015
+ } catch {
7016
+ return processes.map(() => null);
7017
+ }
7018
+ }
7019
+ };
7020
+ }
7021
+
7022
+ // src/collector/adapters/antigravity/agyProcesses.ts
7023
+ var AGY_TABLE_ARGS = ["-A", "-o", "pid=,ppid=,lstart=,comm="];
7024
+ function agyCommandLineArgs(pids) {
7025
+ return ["-o", "pid=,args=", "-p", pids.join(",")];
7026
+ }
7027
+ var AGY_COMMANDS = /* @__PURE__ */ new Set([
7028
+ "agent",
7029
+ "agents",
7030
+ "changelog",
7031
+ "help",
7032
+ "install",
7033
+ "mcp",
7034
+ "mic-serve",
7035
+ "models",
7036
+ "plugin",
7037
+ "plugins",
7038
+ "remote-control",
7039
+ "update"
7040
+ ]);
7041
+ var NOT_A_SESSION_OPTIONS = /* @__PURE__ */ new Set([
7042
+ "-h",
7043
+ "-help",
7044
+ "--help",
7045
+ "-version",
7046
+ "--version"
7047
+ ]);
7048
+ var UUID2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
7049
+ 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*$/;
7050
+ function isAgyProgram(program) {
7051
+ const name = program.split(/[\\/]/).pop()?.toLowerCase();
7052
+ return name === "agy" || name === "agy.exe";
7053
+ }
7054
+ function parseAgyTable(stdout) {
7055
+ const rows = [];
7056
+ for (const line of stdout.split("\n")) {
7057
+ const match = TABLE_LINE.exec(line);
7058
+ if (!match || !isAgyProgram(match[4])) continue;
7059
+ const start = match[3].replace(/\s+/g, " ");
7060
+ rows.push({
7061
+ pid: Number(match[1]),
7062
+ ppid: Number(match[2]),
7063
+ start,
7064
+ startedAt: processStartTime(start)
7065
+ });
7066
+ }
7067
+ return rows;
7068
+ }
7069
+ var PROGRAM_IN_LINE = /(?:^|[\\/])agy(?:\.exe)?(?=\s|$)/i;
7070
+ function readAgyCommandLine(line) {
7071
+ const program = PROGRAM_IN_LINE.exec(line);
7072
+ if (!program) return { session: true };
7073
+ const words = line.slice(program.index + program[0].length).split(/\s+/).filter((word2) => word2 !== "");
7074
+ const first = words[0];
7075
+ if (first !== void 0 && (AGY_COMMANDS.has(first) || NOT_A_SESSION_OPTIONS.has(first))) {
7076
+ return { session: false };
7077
+ }
7078
+ for (const [index, word2] of words.entries()) {
7079
+ let value;
7080
+ if (word2 === "--conversation" || word2 === "-conversation") value = words[index + 1];
7081
+ else if (word2.startsWith("--conversation=")) value = word2.slice("--conversation=".length);
7082
+ else if (word2.startsWith("-conversation=")) value = word2.slice("-conversation=".length);
7083
+ else continue;
7084
+ if (value !== void 0 && UUID2.test(value)) {
7085
+ return { session: true, conversation: value.toLowerCase() };
7086
+ }
7087
+ }
7088
+ return { session: true };
7089
+ }
7090
+ function parseCommandLines(stdout) {
7091
+ const lines = /* @__PURE__ */ new Map();
7092
+ for (const line of stdout.split("\n")) {
7093
+ const match = /^\s*(\d+)\s(.*)$/.exec(line);
7094
+ if (match) lines.set(Number(match[1]), match[2]);
7095
+ }
7096
+ return lines;
7097
+ }
7098
+ function createAgyProcessReader(options = {}) {
7099
+ const run = options.run ?? runPs;
7100
+ const platform = options.platform ?? process.platform;
7101
+ const known = /* @__PURE__ */ new Map();
7102
+ async function read() {
7103
+ if (platform === "win32") return { ok: false };
7104
+ const table = await run(AGY_TABLE_ARGS, { utc: true });
7105
+ if (!table.ok) return { ok: false };
7106
+ const rows = parseAgyTable(table.stdout);
7107
+ const keyOf = (row) => `${row.pid} ${row.start}`;
7108
+ const current = new Set(rows.map(keyOf));
7109
+ for (const key of known.keys()) {
7110
+ if (!current.has(key)) known.delete(key);
7111
+ }
7112
+ const unseen = rows.filter((row) => !known.has(keyOf(row)));
7113
+ const ended = /* @__PURE__ */ new Set();
7114
+ if (unseen.length > 0) {
7115
+ const answer2 = await run(agyCommandLineArgs(unseen.map((row) => row.pid)));
7116
+ const lines = parseCommandLines(answer2.stdout);
7117
+ for (const row of unseen) {
7118
+ const line = lines.get(row.pid);
7119
+ if (line !== void 0) known.set(keyOf(row), readAgyCommandLine(line));
7120
+ else if (answer2.ok || lines.size > 0) ended.add(row.pid);
7121
+ }
7122
+ }
7123
+ const running = /* @__PURE__ */ new Map();
7124
+ for (const row of rows) {
7125
+ if (!ended.has(row.pid)) running.set(row.pid, row);
7126
+ }
7127
+ const lineOf2 = (row) => known.get(keyOf(row)) ?? { session: true };
7128
+ const underSession = (row) => {
7129
+ const seen = /* @__PURE__ */ new Set([row.pid]);
7130
+ let parent = running.get(row.ppid);
7131
+ while (parent !== void 0 && !seen.has(parent.pid)) {
7132
+ if (lineOf2(parent).session) return true;
7133
+ seen.add(parent.pid);
7134
+ parent = running.get(parent.ppid);
7135
+ }
7136
+ return false;
7137
+ };
7138
+ const sessions = [];
7139
+ for (const row of running.values()) {
7140
+ const line = lineOf2(row);
7141
+ if (!line.session || underSession(row)) continue;
7142
+ sessions.push(
7143
+ line.conversation === void 0 ? { startedAt: row.startedAt } : { startedAt: row.startedAt, conversation: line.conversation }
7144
+ );
7145
+ }
7146
+ return { ok: true, sessions };
7147
+ }
7148
+ return {
7149
+ async read() {
7150
+ try {
7151
+ return await read();
7152
+ } catch {
7153
+ return { ok: false };
7154
+ }
7155
+ }
7156
+ };
7157
+ }
7158
+
7159
+ // src/collector/adapters/antigravity/conversations.ts
7160
+ import path13 from "node:path";
7161
+ var BRAIN_DIR = "brain";
7162
+ var CONVERSATIONS_DIR = "conversations";
7163
+ var TRANSCRIPT_IN_FOLDER = [".system_generated", "logs", "transcript.jsonl"];
7164
+ var OLD_REFRESH_MS = 3e4;
7165
+ var UUID3 = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
7166
+ var FOLDER_NAME = new RegExp(`^(${UUID3})$`, "i");
7167
+ var DATABASE_NAME = new RegExp(`^(${UUID3})\\.db(?:-wal)?$`, "i");
7168
+ function conversationIdOfFolder(name) {
7169
+ return FOLDER_NAME.exec(name)?.[1]?.toLowerCase() ?? null;
7170
+ }
7171
+ function conversationIdOfDatabase(name) {
7172
+ return DATABASE_NAME.exec(name)?.[1]?.toLowerCase() ?? null;
7173
+ }
7174
+ function createConversationFinder(options) {
7175
+ const { io } = options;
7176
+ const brainDir = path13.join(options.home, BRAIN_DIR);
7177
+ const databaseDir = path13.join(options.home, CONVERSATIONS_DIR);
7178
+ const oldRefreshMs = options.oldRefreshMs ?? OLD_REFRESH_MS;
7179
+ const known = /* @__PURE__ */ new Map();
7180
+ let oldCheckedAt = null;
7181
+ async function lookAt(id) {
7182
+ const transcript = path13.join(brainDir, id, ...TRANSCRIPT_IN_FOLDER);
7183
+ const infos = await Promise.all(
7184
+ [transcript, path13.join(databaseDir, `${id}.db`), path13.join(databaseDir, `${id}.db-wal`)].map(
7185
+ (file) => io.lstat(file).catch(() => null)
7186
+ )
7187
+ );
7188
+ const transcriptInfo = infos[0]?.kind === "file" ? infos[0] : null;
7189
+ const writtenAt = transcriptInfo === null ? null : Math.floor(transcriptInfo.mtimeMs);
7190
+ let lastWriteAt = null;
7191
+ for (const info of infos) {
7192
+ if (info?.kind !== "file") continue;
7193
+ const at = Math.floor(info.mtimeMs);
7194
+ lastWriteAt = lastWriteAt === null ? at : Math.max(lastWriteAt, at);
7195
+ }
7196
+ return { id, transcript, transcriptInfo, writtenAt, lastWriteAt };
7197
+ }
7198
+ return {
7199
+ async list(checkedAt, mayBeOpen) {
7200
+ let folders;
7201
+ try {
7202
+ folders = await io.readdir(brainDir);
7203
+ } catch (error) {
7204
+ return { state: isMissing2(error) ? "missing" : "unreadable" };
7205
+ }
7206
+ const databases = await io.readdir(databaseDir).catch(() => []);
7207
+ const ids = /* @__PURE__ */ new Set();
7208
+ for (const name of folders) {
7209
+ const id = conversationIdOfFolder(name);
7210
+ if (id !== null) ids.add(id);
7211
+ }
7212
+ for (const name of databases) {
7213
+ const id = conversationIdOfDatabase(name);
7214
+ if (id !== null) ids.add(id);
7215
+ }
7216
+ for (const id of known.keys()) {
7217
+ if (!ids.has(id)) known.delete(id);
7218
+ }
7219
+ const since = oldCheckedAt === null ? Infinity : checkedAt - oldCheckedAt;
7220
+ const refreshOld = since < 0 || since >= oldRefreshMs;
7221
+ if (refreshOld) oldCheckedAt = checkedAt;
7222
+ const cutoff = checkedAt - FINISHED_RETENTION_MS;
7223
+ await Promise.all(
7224
+ [...ids].map(async (id) => {
7225
+ const before = known.get(id);
7226
+ const recent = before === void 0 || before.lastWriteAt !== null && before.lastWriteAt >= cutoff || mayBeOpen(id);
7227
+ if (recent || refreshOld) known.set(id, await lookAt(id));
7228
+ })
7229
+ );
7230
+ const conversations = [...known.values()].sort((a, b) => a.id < b.id ? -1 : 1);
7231
+ return { state: "ok", conversations };
7232
+ }
7233
+ };
7234
+ }
7235
+ function writtenRecently(files, now) {
7236
+ return files.writtenAt !== null && isWithinRetention(files.writtenAt, now);
7237
+ }
7238
+
7239
+ // src/collector/adapters/antigravity/annotations.ts
7240
+ import path14 from "node:path";
7241
+ var ANNOTATIONS_DIR = "annotations";
7242
+ var ANNOTATION_LIMIT_BYTES = 4 * 1024;
7243
+ var TITLE_FIELD = /^title[ \t]*:[ \t]*"((?:[^"\\\n]|\\.)*)"[ \t]*$/m;
7244
+ var SIMPLE_ESCAPES = {
7245
+ n: 10,
7246
+ r: 13,
7247
+ t: 9,
7248
+ '"': 34,
7249
+ "'": 39,
7250
+ "\\": 92,
7251
+ "?": 63,
7252
+ a: 7,
7253
+ b: 8,
7254
+ f: 12,
7255
+ v: 11
7256
+ };
7257
+ function unescapeTextFormat(body) {
7258
+ const chars = Array.from(body);
7259
+ const bytes3 = [];
7260
+ for (let i = 0; i < chars.length; i += 1) {
7261
+ const char = chars[i];
7262
+ if (char !== "\\") {
7263
+ bytes3.push(...Buffer.from(char, "utf8"));
7264
+ continue;
7265
+ }
7266
+ const next = chars[i + 1];
7267
+ if (next === void 0) return null;
7268
+ const octal = /^[0-7]{1,3}/.exec(chars.slice(i + 1, i + 4).join(""));
7269
+ if (octal !== null) {
7270
+ const value = parseInt(octal[0], 8);
7271
+ if (value > 255) return null;
7272
+ bytes3.push(value);
7273
+ i += octal[0].length;
7274
+ continue;
7275
+ }
7276
+ if (next === "u" || next === "U") {
7277
+ const digits = next === "u" ? 4 : 8;
7278
+ const hex = chars.slice(i + 2, i + 2 + digits).join("");
7279
+ if (!new RegExp(`^[0-9a-fA-F]{${digits}}$`).test(hex)) return null;
7280
+ const point = parseInt(hex, 16);
7281
+ if (point > 1114111 || point >= 55296 && point <= 57343) return null;
7282
+ bytes3.push(...Buffer.from(String.fromCodePoint(point), "utf8"));
7283
+ i += 1 + digits;
7284
+ continue;
7285
+ }
7286
+ if (next === "x" || next === "X") {
7287
+ const hex = /^[0-9a-fA-F]{1,2}/.exec(chars.slice(i + 2, i + 4).join(""));
7288
+ if (hex === null) return null;
7289
+ bytes3.push(parseInt(hex[0], 16));
7290
+ i += 1 + hex[0].length;
7291
+ continue;
7292
+ }
7293
+ const simple = SIMPLE_ESCAPES[next];
7294
+ if (simple === void 0) return null;
7295
+ bytes3.push(simple);
7296
+ i += 1;
7297
+ }
7298
+ try {
7299
+ return new TextDecoder("utf-8", { fatal: true }).decode(Uint8Array.from(bytes3));
7300
+ } catch {
7301
+ return null;
7302
+ }
7303
+ }
7304
+ function readTitle(content) {
7305
+ const field2 = TITLE_FIELD.exec(content);
7306
+ if (field2 === null) return null;
7307
+ const title = unescapeTextFormat(field2[1]);
7308
+ return title === null ? null : sessionName(title) ?? null;
7309
+ }
7310
+ function createTitleReader(options) {
7311
+ const { io } = options;
7312
+ const dir = path14.join(options.home, ANNOTATIONS_DIR);
7313
+ const cache = /* @__PURE__ */ new Map();
7314
+ async function read(id) {
7315
+ const file = path14.join(dir, `${id}.pbtxt`);
7316
+ const info = await io.lstat(file);
7317
+ const known = cache.get(id);
7318
+ if (known && known.ino === info.ino && known.size === info.size && known.mtimeMs === info.mtimeMs) {
7319
+ return known.title;
7320
+ }
7321
+ let title = null;
7322
+ if (info.kind === "file" && info.size <= ANNOTATION_LIMIT_BYTES) {
7323
+ const handle = await io.openRegular(file);
7324
+ try {
7325
+ if (handle.info.size <= ANNOTATION_LIMIT_BYTES) {
7326
+ const data = await handle.read(0, handle.info.size);
7327
+ title = readTitle(Buffer.from(data).toString("utf8"));
7328
+ }
7329
+ } finally {
7330
+ await handle.close();
7331
+ }
7332
+ }
7333
+ cache.set(id, { ino: info.ino, size: info.size, mtimeMs: info.mtimeMs, title });
7334
+ return title;
7335
+ }
7336
+ return {
7337
+ async read(id) {
7338
+ try {
7339
+ return await read(id);
7340
+ } catch {
7341
+ cache.delete(id);
7342
+ return null;
7343
+ }
7344
+ },
7345
+ keepOnly(ids) {
7346
+ for (const id of cache.keys()) {
7347
+ if (!ids.has(id)) cache.delete(id);
7348
+ }
7349
+ }
7350
+ };
7351
+ }
7352
+
7353
+ // src/core/mapping/antigravityMapping.ts
7354
+ var ANTIGRAVITY_STEP_TYPES = [
7355
+ "UNSPECIFIED",
7356
+ "DUMMY",
7357
+ "FINISH",
7358
+ "PLAN_INPUT",
7359
+ "MQUERY",
7360
+ "CODE_ACTION",
7361
+ "GIT_COMMIT",
7362
+ "GREP_SEARCH",
7363
+ "VIEW_FILE",
7364
+ "LIST_DIRECTORY",
7365
+ "COMPILE",
7366
+ "VIEW_CODE_ITEM",
7367
+ "USER_INPUT",
7368
+ "PLANNER_RESPONSE",
7369
+ "ERROR_MESSAGE",
7370
+ "RUN_COMMAND",
7371
+ "CHECKPOINT",
7372
+ "PROPOSE_CODE",
7373
+ "FIND",
7374
+ "SUGGESTED_RESPONSES",
7375
+ "COMMAND_STATUS",
7376
+ "MEMORY",
7377
+ "READ_URL_CONTENT",
7378
+ "VIEW_CONTENT_CHUNK",
7379
+ "SEARCH_WEB",
7380
+ "RETRIEVE_MEMORY",
7381
+ "MCP_TOOL",
7382
+ "MANAGER_FEEDBACK",
7383
+ "TOOL_CALL_PROPOSAL",
7384
+ "TOOL_CALL_CHOICE",
7385
+ "TRAJECTORY_CHOICE",
7386
+ "CLIPBOARD",
7387
+ "VIEW_FILE_OUTLINE",
7388
+ "POST_PR_REVIEW",
7389
+ "LIST_RESOURCES",
7390
+ "READ_RESOURCE",
7391
+ "LINT_DIFF",
7392
+ "FIND_ALL_REFERENCES",
7393
+ "BRAIN_UPDATE",
7394
+ "OPEN_BROWSER_URL",
7395
+ "RUN_EXTENSION_CODE",
7396
+ "PROPOSAL_FEEDBACK",
7397
+ "TRAJECTORY_SEARCH",
7398
+ "EXECUTE_BROWSER_JAVASCRIPT",
7399
+ "LIST_BROWSER_PAGES",
7400
+ "CAPTURE_BROWSER_SCREENSHOT",
7401
+ "CLICK_BROWSER_PIXEL",
7402
+ "READ_TERMINAL",
7403
+ "CAPTURE_BROWSER_CONSOLE_LOGS",
7404
+ "READ_BROWSER_PAGE",
7405
+ "BROWSER_GET_DOM",
7406
+ "CODE_SEARCH",
7407
+ "BROWSER_INPUT",
7408
+ "BROWSER_MOVE_MOUSE",
7409
+ "BROWSER_SELECT_OPTION",
7410
+ "BROWSER_SCROLL_UP",
7411
+ "BROWSER_SCROLL_DOWN",
7412
+ "BROWSER_CLICK_ELEMENT",
7413
+ "BROWSER_PRESS_KEY",
7414
+ "TASK_BOUNDARY",
7415
+ "NOTIFY_USER",
7416
+ "CODE_ACKNOWLEDGEMENT",
7417
+ "INTERNAL_SEARCH",
7418
+ "BROWSER_SUBAGENT",
7419
+ "FILE_CHANGE",
7420
+ "MOVE",
7421
+ "BROWSER_SCROLL",
7422
+ "KNOWLEDGE_GENERATION",
7423
+ "EPHEMERAL_MESSAGE",
7424
+ "GENERATE_IMAGE",
7425
+ "DELETE_DIRECTORY",
7426
+ "COMPILE_APPLET",
7427
+ "INSTALL_APPLET_DEPENDENCIES",
7428
+ "INSTALL_APPLET_PACKAGE",
7429
+ "BROWSER_RESIZE_WINDOW",
7430
+ "BROWSER_DRAG_PIXEL_TO_PIXEL",
7431
+ "CONVERSATION_HISTORY",
7432
+ "KNOWLEDGE_ARTIFACTS",
7433
+ "SEND_COMMAND_INPUT",
7434
+ "SYSTEM_MESSAGE",
7435
+ "WAIT",
7436
+ "AGENCY_TOOL_CALL",
7437
+ "CIDER_AGENT_DUMMY",
7438
+ "BUILD_CLEANER",
7439
+ "BLAZE_BUILD_TARGETS",
7440
+ "BLAZE_TEST_TARGETS",
7441
+ "SET_UP_FIREBASE",
7442
+ "MOMA",
7443
+ "RESTART_DEV_SERVER",
7444
+ "DEPLOY_FIREBASE",
7445
+ "SHELL_EXEC",
7446
+ "BROWSER_MOUSE_WHEEL",
7447
+ "LINT_APPLET",
7448
+ "KI_INSERTION",
7449
+ "RETRIEVE_CONTENT",
7450
+ "CRITIQUE",
7451
+ "FINDINGS",
7452
+ "BROWSER_MOUSE_UP",
7453
+ "BROWSER_MOUSE_DOWN",
7454
+ "WORKSPACE_API",
7455
+ "BROWSER_LIST_NETWORK_REQUESTS",
7456
+ "BROWSER_GET_NETWORK_REQUEST",
7457
+ "BROWSER_REFRESH_PAGE",
7458
+ "EDIT_NOTEBOOK",
7459
+ "INVOKE_SUBAGENT",
7460
+ "WRITE_BLOB",
7461
+ "READ_NOTEBOOK",
7462
+ "PROPOSE_AI_COMMENTS",
7463
+ "START_CODE_REVIEW",
7464
+ "GENERIC",
7465
+ "SET_UP_CLOUD_SQL",
7466
+ "EXECUTE_NOTEBOOK",
7467
+ "CLOUD_SQL_UPDATE_SCHEMA",
7468
+ "RPC_ACTION",
7469
+ "CLOUD_SQL_EXECUTE_SQL",
7470
+ "ASK_QUESTION",
7471
+ "DIRECTORY_RULES",
7472
+ "TOOL_SEARCH"
7473
+ ];
7474
+ var ANTIGRAVITY_STEP_STATUSES = [
7475
+ "UNSPECIFIED",
7476
+ "PENDING",
7477
+ "RUNNING",
7478
+ "DONE",
7479
+ "INVALID",
7480
+ "CLEARED",
7481
+ "CANCELED",
7482
+ "ERROR",
7483
+ "GENERATING",
7484
+ "WAITING",
7485
+ "QUEUED",
7486
+ "INTERRUPTED"
7487
+ ];
7488
+ var KNOWN_TYPES = new Set(ANTIGRAVITY_STEP_TYPES);
7489
+ var KNOWN_STATUSES = new Set(ANTIGRAVITY_STEP_STATUSES);
7490
+ var TYPE_PREFIX = "CORTEX_STEP_TYPE_";
7491
+ var STATUS_PREFIX = "CORTEX_STEP_STATUS_";
7492
+ var NAME = /^[A-Z][A-Z0-9_]{0,63}$/;
7493
+ function nameOf(value, prefix) {
7494
+ if (typeof value !== "string") return null;
7495
+ const upper = value.trim().toUpperCase();
7496
+ const name = upper.startsWith(prefix) ? upper.slice(prefix.length) : upper;
7497
+ return NAME.test(name) ? name : null;
7498
+ }
7499
+ function stepTypeName(value) {
7500
+ return nameOf(value, TYPE_PREFIX);
7501
+ }
7502
+ function stepStatusName(value) {
7503
+ return nameOf(value, STATUS_PREFIX);
7504
+ }
7505
+ var ASIDE_TYPES = /* @__PURE__ */ new Set([
7506
+ "DUMMY",
7507
+ "CIDER_AGENT_DUMMY",
7508
+ "CHECKPOINT",
7509
+ "EPHEMERAL_MESSAGE",
7510
+ "CONVERSATION_HISTORY",
7511
+ "SYSTEM_MESSAGE",
7512
+ "SUGGESTED_RESPONSES",
7513
+ "KNOWLEDGE_ARTIFACTS",
7514
+ "KNOWLEDGE_GENERATION",
7515
+ "KI_INSERTION",
7516
+ "DIRECTORY_RULES",
7517
+ "BRAIN_UPDATE"
7518
+ ]);
7519
+ var GONE_STATUSES = /* @__PURE__ */ new Set(["CLEARED", "INVALID"]);
7520
+ var UNDER_WAY = /* @__PURE__ */ new Set([
7521
+ "PENDING",
7522
+ "RUNNING",
7523
+ "GENERATING",
7524
+ "QUEUED",
7525
+ "WAITING"
7526
+ ]);
7527
+ var STOPPED = /* @__PURE__ */ new Set(["CANCELED", "INTERRUPTED"]);
7528
+ var TURN_OVER = /* @__PURE__ */ new Set(["FINISH"]);
7529
+ function isAsideStep(step) {
7530
+ return step.type !== null && ASIDE_TYPES.has(step.type) || step.status !== null && GONE_STATUSES.has(step.status);
7531
+ }
7532
+ function openStatusOf(step) {
7533
+ const { type, status } = step;
7534
+ if (type === "ERROR_MESSAGE") return "failed";
7535
+ if (status === null || !KNOWN_STATUSES.has(status)) return "unknown";
7536
+ if (UNDER_WAY.has(status)) return "working";
7537
+ if (STOPPED.has(status)) return "idle";
7538
+ if (type === null || !KNOWN_TYPES.has(type) || type === "UNSPECIFIED") return "unknown";
7539
+ if (status === "ERROR") return type === "PLANNER_RESPONSE" ? "failed" : "working";
7540
+ if (status !== "DONE") return "unknown";
7541
+ if (type === "PLANNER_RESPONSE") return step.toolCalls ? "working" : "idle";
7542
+ if (TURN_OVER.has(type)) return "idle";
7543
+ return "working";
7544
+ }
7545
+ function mapAntigravityStatus(fields) {
7546
+ const { last, live: live2 } = fields;
7547
+ if (last === void 0) return "unknown";
7548
+ const open2 = last === null ? "idle" : openStatusOf(last);
7549
+ if (open2 === "failed" || open2 === "unknown") return open2;
7550
+ if (live2 === false) return "finished";
7551
+ return fields.asking === true ? "needs-you" : open2;
7552
+ }
7553
+
7554
+ // src/collector/adapters/antigravity/toSession.ts
7555
+ var SOURCE_ID2 = "antigravity-cli";
7556
+ var WRITE_ORDER_SLACK_MS2 = 1e3;
7557
+ function antigravitySession(input) {
7558
+ const { conversationId, state, live: live2, now } = input;
7559
+ const id = `${SOURCE_ID2}:${conversationId}`;
7560
+ const waiting = input.waiting ?? null;
7561
+ const status = mapAntigravityStatus({ last: state.last, live: live2, asking: waiting !== null });
7562
+ const startedAt = plausibleTime(state.firstAt, now);
7563
+ let statusSince = null;
7564
+ if (status === "working" || status === "idle" || status === "failed") {
7565
+ statusSince = plausibleTime(state.since, now);
7566
+ } else if (status === "needs-you") {
7567
+ statusSince = plausibleTime(waiting?.since ?? null, now) ?? plausibleTime(state.lastAt, now);
7568
+ } else if (status === "finished") {
7569
+ statusSince = plausibleTime(state.lastAt, now);
7570
+ }
7571
+ if (statusSince !== null && startedAt !== null && statusSince < startedAt - WRITE_ORDER_SLACK_MS2) {
7572
+ statusSince = null;
7573
+ }
7574
+ const cwd = input.folder ?? null;
7575
+ const project = projectOf(cwd);
7576
+ const session = {
7577
+ id,
7578
+ source: SOURCE_ID2,
7579
+ surface: "terminal",
7580
+ name: input.title ?? project ?? sessionName(conversationId) ?? id,
7581
+ cwd,
7582
+ project,
7583
+ status,
7584
+ startedAt,
7585
+ statusSince,
7586
+ links: {},
7587
+ stale: isStale({ status, statusSince }, now)
7588
+ };
7589
+ if (status === "needs-you" && waiting !== null) {
7590
+ session.waitingReason = "permission";
7591
+ session.waitingDetail = waiting.tool;
7592
+ }
7593
+ const writtenAt = plausibleTime(input.writtenAt, now);
7594
+ const beforeStart = writtenAt !== null && startedAt !== null && writtenAt < startedAt - WRITE_ORDER_SLACK_MS2;
7595
+ if (writtenAt !== null && !beforeStart) session.lastWriteAt = writtenAt;
7596
+ return session;
7597
+ }
7598
+
7599
+ // src/collector/adapters/antigravity/transcriptFile.ts
7600
+ import { createHash as createHash2 } from "node:crypto";
7601
+ var HEAD_LIMIT_BYTES = 256 * 1024;
7602
+ var TAIL_LIMIT_BYTES = 2 * 1024 * 1024;
7603
+ var TAIL_CHUNK_BYTES = 64 * 1024;
7604
+ var HEAD_HASH_BYTES = 4 * 1024;
7605
+ var NEWLINE2 = 10;
7606
+ var ISO_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
7607
+ function parseStepTime(value) {
7608
+ if (typeof value !== "string" || !ISO_TIME.test(value)) return null;
7609
+ const at = Date.parse(value);
7610
+ return Number.isSafeInteger(at) ? at : null;
7611
+ }
7612
+ function isRecord8(value) {
7613
+ return typeof value === "object" && value !== null && !Array.isArray(value);
7614
+ }
7615
+ function readStepLine(line) {
7616
+ let value;
7617
+ try {
7618
+ value = JSON.parse(line);
7619
+ } catch {
7620
+ return null;
7621
+ }
7622
+ if (!isRecord8(value)) return null;
7623
+ const type = stepTypeName(value.type);
7624
+ const status = stepStatusName(value.status);
7625
+ if (type === null && status === null) return null;
7626
+ const index = value.step_index;
7627
+ return {
7628
+ index: Number.isSafeInteger(index) && index >= 0 ? index : null,
7629
+ type,
7630
+ status,
7631
+ toolCalls: Array.isArray(value.tool_calls) && value.tool_calls.length > 0,
7632
+ at: parseStepTime(value.created_at)
7633
+ };
7634
+ }
7635
+ function emptyState() {
7636
+ return { firstAt: null, last: void 0, since: null, lastAt: null, lastIndex: null };
7637
+ }
7638
+ function countsForIndex(step) {
7639
+ return step.index !== null && step.status !== "CLEARED" && step.status !== "INVALID";
7640
+ }
7641
+ function addStep(state, step) {
7642
+ if (step.at !== null) state.lastAt = step.at;
7643
+ if (countsForIndex(step)) state.lastIndex = step.index;
7644
+ if (isAsideStep(step)) return;
7645
+ const was = state.last ? openStatusOf(state.last) : null;
7646
+ if (was !== openStatusOf(step)) state.since = step.at;
7647
+ state.last = step;
7648
+ }
7649
+ function bytes(data) {
7650
+ return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
7651
+ }
7652
+ async function readExactly(file, position, length) {
7653
+ const data = bytes(await file.read(position, length));
7654
+ if (data.byteLength !== length) throw new Error("The file changed while it was read.");
7655
+ return data;
7656
+ }
7657
+ async function headHash(file, size) {
7658
+ const head = await readExactly(file, 0, Math.min(HEAD_HASH_BYTES, size));
7659
+ return createHash2("sha256").update(head).digest("hex");
7660
+ }
7661
+ async function readFirstAt(file, size) {
7662
+ const head = await readExactly(file, 0, Math.min(HEAD_LIMIT_BYTES, size));
7663
+ const end = head.indexOf(NEWLINE2);
7664
+ if (end === -1 && head.byteLength < size) return null;
7665
+ const step = readStepLine(head.toString("utf8", 0, end === -1 ? head.byteLength : end));
7666
+ return step?.index === 0 ? step.at : null;
7667
+ }
7668
+ async function scanTail(file, size) {
7669
+ const state = emptyState();
7670
+ let position = size;
7671
+ let budget2 = TAIL_LIMIT_BYTES;
7672
+ let fragment = Buffer.alloc(0);
7673
+ let end = null;
7674
+ let settled = false;
7675
+ const take = (step) => {
7676
+ if (step === null) return false;
7677
+ if (state.lastAt === null && step.at !== null) state.lastAt = step.at;
7678
+ if (state.lastIndex === null && countsForIndex(step)) state.lastIndex = step.index;
7679
+ if (isAsideStep(step)) return false;
7680
+ if (state.last === void 0 || state.last === null) {
7681
+ state.last = step;
7682
+ state.since = step.at;
7683
+ return false;
7684
+ }
7685
+ if (openStatusOf(step) !== openStatusOf(state.last)) return true;
7686
+ if (step.at !== null) state.since = step.at;
7687
+ return false;
7688
+ };
7689
+ while (position > 0 && budget2 > 0 && !settled) {
7690
+ const length = Math.min(TAIL_CHUNK_BYTES, position, budget2);
7691
+ const start = position - length;
7692
+ const chunk = await readExactly(file, start, length);
7693
+ budget2 -= length;
7694
+ position = start;
7695
+ const buffer = fragment.byteLength > 0 ? Buffer.concat([chunk, fragment]) : chunk;
7696
+ let cut2 = buffer.byteLength;
7697
+ if (end === null) {
7698
+ const last = buffer.lastIndexOf(NEWLINE2);
7699
+ if (last === -1) {
7700
+ fragment = buffer;
7701
+ continue;
7702
+ }
7703
+ end = start + last + 1;
7704
+ cut2 = last;
7705
+ if (last + 1 < buffer.byteLength) {
7706
+ settled = take(readStepLine(buffer.toString("utf8", last + 1)));
7707
+ }
7708
+ }
7709
+ let newline = cut2 > 0 ? buffer.lastIndexOf(NEWLINE2, cut2 - 1) : -1;
7710
+ while (newline !== -1 && !settled) {
7711
+ settled = take(readStepLine(buffer.toString("utf8", newline + 1, cut2)));
7712
+ cut2 = newline;
7713
+ newline = cut2 > 0 ? buffer.lastIndexOf(NEWLINE2, cut2 - 1) : -1;
7714
+ }
7715
+ if (settled) break;
7716
+ if (start === 0) {
7717
+ take(readStepLine(buffer.toString("utf8", 0, cut2)));
7718
+ if (state.last === void 0) state.last = null;
7719
+ return { state, end };
7720
+ }
7721
+ fragment = buffer.subarray(0, cut2);
7722
+ }
7723
+ if (!settled && position === 0) {
7724
+ if (end === null) {
7725
+ take(readStepLine(fragment.toString("utf8")));
7726
+ end = 0;
7727
+ }
7728
+ if (state.last === void 0) state.last = null;
7729
+ return { state, end };
7730
+ }
7731
+ if (!settled && state.last) state.since = null;
7732
+ return { state, end };
7733
+ }
7734
+ function stateOf(cached) {
7735
+ return {
7736
+ firstAt: cached.firstAt,
7737
+ last: cached.last,
7738
+ since: cached.since,
7739
+ lastAt: cached.lastAt,
7740
+ lastIndex: cached.lastIndex
7741
+ };
7742
+ }
7743
+ function createTranscriptReader(io) {
7744
+ const cache = /* @__PURE__ */ new Map();
7745
+ async function readFresh(file, earlier) {
7746
+ const handle = await io.openRegular(file);
7747
+ try {
7748
+ const { info } = handle;
7749
+ const firstAt = await readFirstAt(handle, info.size);
7750
+ const tail = await scanTail(handle, info.size);
7751
+ return {
7752
+ ...tail.state,
7753
+ // A compacted file may begin later than the conversation did: the earliest seen stands.
7754
+ firstAt: earlier?.firstAt != null && (firstAt === null || earlier.firstAt < firstAt) ? earlier.firstAt : firstAt,
7755
+ ino: info.ino,
7756
+ size: info.size,
7757
+ mtimeMs: info.mtimeMs,
7758
+ headHash: await headHash(handle, info.size),
7759
+ offset: tail.end
7760
+ };
7761
+ } finally {
7762
+ await handle.close();
7763
+ }
7764
+ }
7765
+ async function readOnward(file, cached, info) {
7766
+ const offset = cached.offset;
7767
+ if (offset === null) return null;
7768
+ if (info.ino !== cached.ino || info.size <= cached.size || info.size < offset) return null;
7769
+ if (info.size - offset > TAIL_LIMIT_BYTES) return null;
7770
+ const handle = await io.openRegular(file);
7771
+ try {
7772
+ const opened = handle.info;
7773
+ if (opened.ino !== cached.ino || opened.size < offset || opened.size < cached.size) {
7774
+ return null;
7775
+ }
7776
+ if (opened.size - offset > TAIL_LIMIT_BYTES) return null;
7777
+ if (await headHash(handle, cached.size) !== cached.headHash) return null;
7778
+ const from = offset > 0 ? offset - 1 : 0;
7779
+ const read = await readExactly(handle, from, opened.size - from);
7780
+ if (offset > 0 && read[0] !== NEWLINE2) return null;
7781
+ const added = offset > 0 ? read.subarray(1) : read;
7782
+ const next = {
7783
+ ...cached,
7784
+ ino: opened.ino,
7785
+ size: opened.size,
7786
+ mtimeMs: opened.mtimeMs,
7787
+ headHash: await headHash(handle, opened.size)
7788
+ };
7789
+ let start = 0;
7790
+ let newline = added.indexOf(NEWLINE2);
7791
+ while (newline !== -1) {
7792
+ const step = readStepLine(added.toString("utf8", start, newline));
7793
+ if (step) addStep(next, step);
7794
+ start = newline + 1;
7795
+ newline = added.indexOf(NEWLINE2, start);
7796
+ }
7797
+ if (start < added.byteLength) {
7798
+ const step = readStepLine(added.toString("utf8", start));
7799
+ if (step) addStep(next, step);
7800
+ }
7801
+ next.offset = offset + start;
7802
+ if (next.firstAt === null && offset === 0) {
7803
+ next.firstAt = await readFirstAt(handle, opened.size);
7804
+ }
7805
+ return next;
7806
+ } finally {
7807
+ await handle.close();
7808
+ }
7809
+ }
7810
+ return {
7811
+ async read(file, info) {
7812
+ const cached = cache.get(file);
7813
+ if (cached && cached.ino === info.ino && cached.size === info.size && cached.mtimeMs === info.mtimeMs) {
7814
+ return stateOf(cached);
7815
+ }
7816
+ const next = cached && await readOnward(file, cached, info) || await readFresh(file, cached);
7817
+ cache.set(file, next);
7818
+ return stateOf(next);
7819
+ },
7820
+ keepOnly(files) {
7821
+ for (const file of cache.keys()) {
7822
+ if (!files.has(file)) cache.delete(file);
7823
+ }
7824
+ }
7825
+ };
7826
+ }
7827
+
7828
+ // src/collector/adapters/antigravity/index.ts
7829
+ var ANTIGRAVITY_HOME_ENV = "AGENT_LOOKOUT_ANTIGRAVITY_HOME";
7830
+ var DEFAULT_HOME = [".gemini", "antigravity-cli"];
7831
+ var RECHECK_MS = 6e4;
7832
+ var PROCESS_CHECK_MS = 1e4;
7833
+ var PROMPT_PROCESS_CHECK_MS = 2e3;
7834
+ var BASIS = "files";
7835
+ var BASIS_WITHOUT_PROCESSES = "files-without-processes";
7836
+ var BASIS_UNMATCHED = "files-unmatched";
7837
+ var LABEL2 = "Antigravity CLI";
7838
+ 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.";
7839
+ 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.";
7840
+ var ALWAYS_NOTES = `${NEEDS_YOU_NOTE} ${UNCHECKED_NOTE}`;
7841
+ 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.";
7842
+ 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.";
7843
+ var ANTIGRAVITY_CAPABILITIES = {
7844
+ "working-and-idle": { level: "yes" },
7845
+ "needs-you": {
7846
+ level: "partly",
7847
+ reason: "While agy asks you to approve a tool, from its own log. A question it asks you shows as working."
7848
+ },
7849
+ finished: {
7850
+ level: "partly",
7851
+ reason: "Once ps shows no agy program running that could have the conversation open. Not on Windows."
7852
+ },
7853
+ failed: {
7854
+ level: "partly",
7855
+ reason: "Only when agy records an error of its own or a failed reply. A tool that fails shows as working."
7856
+ },
7857
+ names: {
7858
+ level: "partly",
7859
+ reason: "The title agy gives a conversation once it has one, and the folder its program's log names. Until then, its conversation ID."
7860
+ },
7861
+ jump: {
7862
+ level: "no",
7863
+ reason: "The transcripts name no terminal, and agy documents no link that opens a conversation."
7864
+ },
7865
+ "quiet-for": { level: "yes" },
7866
+ stop: {
7867
+ level: "no",
7868
+ reason: "No agy program is tied to one conversation surely enough for Agent Lookout to confirm it and stop it."
7869
+ },
7870
+ answer: {
7871
+ level: "no",
7872
+ reason: "Its approval prompt takes an answer only in the terminal agy runs in."
7873
+ }
7874
+ };
7875
+ function withLoggedConversations(programs, logStates) {
7876
+ return programs.map((program, index) => {
7877
+ const current = logStates[index]?.current ?? null;
7878
+ return current === null ? program : { ...program, conversation: current };
7879
+ });
7880
+ }
7881
+ function folderOf(id, logs) {
7882
+ const open2 = logs.find((log) => log.current === id && log.folder !== null);
7883
+ if (open2) return open2.folder;
7884
+ return logs.find((log) => log.opened.has(id) && log.folder !== null)?.folder ?? null;
7885
+ }
7886
+ function createAntigravityAdapter(options = {}) {
7887
+ const env = options.env ?? process.env;
7888
+ const homeDir = options.homeDir ?? os5.homedir();
7889
+ const now = options.now ?? Date.now;
7890
+ const io = options.io ?? nodeIo2;
7891
+ const platform = options.platform ?? process.platform;
7892
+ const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
7893
+ const recheckMs = options.recheckMs ?? RECHECK_MS;
7894
+ const processCheckMs = options.processCheckMs ?? PROCESS_CHECK_MS;
7895
+ const processes = options.processes ?? createAgyProcessReader({ platform });
7896
+ const override = env[ANTIGRAVITY_HOME_ENV]?.trim() || void 0;
7897
+ const homeFrom = override ? "agent-lookout" : "default";
7898
+ const home = path15.resolve(override ?? path15.join(homeDir, ...DEFAULT_HOME));
7899
+ const brainDir = path15.join(home, BRAIN_DIR);
7900
+ const homeName = tildify(home, homeDir);
7901
+ const brainName = tildify(brainDir, homeDir);
7902
+ const finder = createConversationFinder({ home, io, oldRefreshMs: options.oldRefreshMs });
7903
+ const reader = createTranscriptReader(io);
7904
+ const logs = createAgyLogReader({ home, io, now });
7905
+ const titles = createTitleReader({ home, io });
7906
+ let absentAt = null;
7907
+ let homeSeen = false;
7908
+ let lastProcesses = null;
7909
+ let processesAt = null;
7910
+ let lastHolding = null;
7911
+ const commandRun = platform === "win32" ? "not run on Windows" : `${every(processCheckMs)} at most`;
7912
+ function watching(read) {
7913
+ return [
7914
+ { label: "Conversations folder", value: brainName },
7915
+ { label: "Read", value: read },
7916
+ { label: "Command", value: `ps ${AGY_TABLE_ARGS.join(" ")}` },
7917
+ { label: "Command run", value: commandRun }
7918
+ ];
7919
+ }
7920
+ function result(state, detail, read, checkedAt, basis = BASIS) {
7921
+ const health = {
7922
+ id: SOURCE_ID2,
7923
+ label: LABEL2,
7924
+ state,
7925
+ detail,
7926
+ watching: watching(read),
7927
+ checkedAt
7928
+ };
7929
+ return state === "ok" ? { health, sessions: [], basis } : { health, sessions: [] };
7930
+ }
7931
+ function notFound(checkedAt) {
7932
+ lastProcesses = null;
7933
+ processesAt = null;
7934
+ lastHolding = null;
7935
+ return result(
7936
+ "unavailable",
7937
+ `The Antigravity CLI was not found: there is no ${homeName} folder. Agent Lookout looks again ${every(recheckMs)}.`,
7938
+ "not found",
7939
+ checkedAt
7940
+ );
7941
+ }
7942
+ async function homeIsThere() {
7943
+ try {
7944
+ return (await io.stat(home)).kind === "directory";
7945
+ } catch (error) {
7946
+ return !isMissing2(error);
7947
+ }
7948
+ }
7949
+ async function processesFor(conversations, checkedAt) {
7950
+ const shown = conversations.some(
7951
+ (files) => writtenRecently(files, checkedAt) || lastHolding?.held.has(files.id)
7952
+ );
7953
+ if (!shown) return null;
7954
+ const since = processesAt === null ? Infinity : checkedAt - processesAt;
7955
+ let due = since < 0 || since >= processCheckMs;
7956
+ if (!due && since >= PROMPT_PROCESS_CHECK_MS && lastProcesses?.ok) {
7957
+ const holding = holdConversations(lastProcesses.sessions, conversations);
7958
+ due = conversations.some(
7959
+ (files) => files.writtenAt !== null && files.writtenAt > processesAt && !holding.held.has(files.id)
7960
+ );
7961
+ }
7962
+ if (due) {
7963
+ lastProcesses = await processes.read();
7964
+ processesAt = checkedAt;
7965
+ }
7966
+ return lastProcesses;
7967
+ }
7968
+ async function poll() {
7969
+ const checkedAt = now();
7970
+ if (homeFrom !== "agent-lookout") {
7971
+ if (absentAt !== null) {
7972
+ const since = checkedAt - absentAt;
7973
+ if (since >= 0 && since < recheckMs) return notFound(checkedAt);
7974
+ }
7975
+ if (!homeSeen) {
7976
+ if (!await homeIsThere()) {
7977
+ absentAt = checkedAt;
7978
+ return notFound(checkedAt);
7979
+ }
7980
+ absentAt = null;
7981
+ homeSeen = true;
7982
+ }
7983
+ }
7984
+ const listing = await finder.list(checkedAt, (id) => lastHolding?.held.has(id) ?? false);
7985
+ if (listing.state === "unreadable") {
7986
+ return result(
7987
+ "error",
7988
+ `Antigravity CLI conversations could not be read: the folder ${brainName} could not be listed.`,
7989
+ "cannot be read",
7990
+ checkedAt
7991
+ );
7992
+ }
7993
+ if (listing.state === "missing") {
7994
+ lastHolding = null;
7995
+ if (homeFrom === "agent-lookout") {
7996
+ return result(
7997
+ "ok",
7998
+ `${ANTIGRAVITY_HOME_ENV} is set to ${homeName}, which has no ${BRAIN_DIR} folder, so no Antigravity CLI sessions are listed. ${ALWAYS_NOTES}`,
7999
+ "not found",
8000
+ checkedAt
8001
+ );
8002
+ }
8003
+ homeSeen = false;
8004
+ if (!await homeIsThere()) {
8005
+ absentAt = checkedAt;
8006
+ return notFound(checkedAt);
8007
+ }
8008
+ homeSeen = true;
8009
+ return result(
8010
+ "ok",
8011
+ `The Antigravity CLI has not saved any conversations in ${brainName} yet. ${ALWAYS_NOTES}`,
8012
+ "not found",
8013
+ checkedAt
8014
+ );
8015
+ }
8016
+ const { conversations } = listing;
8017
+ const list = await processesFor(conversations, checkedAt);
8018
+ const logStates = list?.ok ? await logs.read(list.sessions) : [];
8019
+ const programs = list?.ok ? withLoggedConversations(list.sessions, logStates) : [];
8020
+ const holding = list?.ok ? holdConversations(programs, conversations, processesAt) : null;
8021
+ lastHolding = holding;
8022
+ const logged = logStates.filter((state) => state !== null);
8023
+ const read = /* @__PURE__ */ new Set();
8024
+ const sessions = [];
8025
+ let unknownStep = false;
8026
+ for (const files of conversations) {
8027
+ const open2 = holding?.held.has(files.id) ?? false;
8028
+ if (files.transcriptInfo === null || !(open2 || writtenRecently(files, checkedAt))) continue;
8029
+ let state;
8030
+ try {
8031
+ state = await reader.read(files.transcript, files.transcriptInfo);
8032
+ } catch {
8033
+ read.add(files.transcript);
8034
+ continue;
8035
+ }
8036
+ read.add(files.transcript);
8037
+ const ask = logged.find((log) => log.ask?.conversation === files.id)?.ask ?? null;
8038
+ const waiting = ask !== null && waitsForApproval(ask, files.id, state.lastIndex) ? { tool: ask.tool, since: agyLogTimeAt(ask.at, checkedAt) } : null;
8039
+ const session = antigravitySession({
8040
+ conversationId: files.id,
8041
+ state,
8042
+ live: antigravityLiveness(files.id, holding),
8043
+ waiting,
8044
+ title: await titles.read(files.id),
8045
+ folder: folderOf(files.id, logged),
8046
+ writtenAt: files.lastWriteAt,
8047
+ now: checkedAt
8048
+ });
8049
+ if (session.status === "unknown") unknownStep = true;
8050
+ sessions.push(session);
8051
+ }
8052
+ reader.keepOnly(read);
8053
+ titles.keepOnly(new Set(sessions.map((session) => session.id.slice(SOURCE_ID2.length + 1))));
8054
+ let detail = `Sessions are read from the transcripts the Antigravity CLI keeps in ${brainName}. ${ALWAYS_NOTES}`;
8055
+ let basis = BASIS;
8056
+ if (list !== null && !list.ok) {
8057
+ 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.";
8058
+ basis = BASIS_WITHOUT_PROCESSES;
8059
+ } else if (holding !== null && !holding.complete) {
8060
+ detail += ` ${UNMATCHED_NOTE}`;
8061
+ basis = BASIS_UNMATCHED;
8062
+ }
8063
+ if (unknownStep) detail += ` ${UNKNOWN_STEP_NOTE}`;
8064
+ const answer2 = result("ok", detail, every(pollIntervalMs), checkedAt, basis);
8065
+ return { ...answer2, sessions };
8066
+ }
8067
+ return {
8068
+ id: SOURCE_ID2,
8069
+ label: LABEL2,
8070
+ capabilities: ANTIGRAVITY_CAPABILITIES,
8071
+ lookingIn: `Looking for Antigravity CLI sessions in ${brainName}.`,
8072
+ async poll() {
8073
+ try {
8074
+ return await poll();
8075
+ } catch {
8076
+ return result(
8077
+ "error",
8078
+ "Something unexpected went wrong while reading Antigravity CLI sessions.",
8079
+ every(pollIntervalMs),
8080
+ now()
8081
+ );
8082
+ }
8083
+ }
8084
+ };
8085
+ }
8086
+
8087
+ // src/collector/adapters/codex/index.ts
8088
+ import os6 from "node:os";
8089
+ import path17 from "node:path";
8090
+
8091
+ // src/core/mapping/codexMapping.ts
8092
+ var TURN_UNDER_WAY = /* @__PURE__ */ new Set(["task_started", "turn_started"]);
8093
+ var TURN_OVER2 = /* @__PURE__ */ new Set(["task_complete", "turn_complete", "turn_aborted"]);
8094
+ function mapCodexStatus(fields) {
8095
+ const { lastTurn, live: live2 } = fields;
8096
+ let open2;
8097
+ if (lastTurn === null) {
8098
+ open2 = "idle";
8099
+ } else if (typeof lastTurn !== "string") {
8100
+ return "unknown";
8101
+ } else if (TURN_UNDER_WAY.has(lastTurn)) {
8102
+ open2 = "working";
8103
+ } else if (TURN_OVER2.has(lastTurn)) {
8104
+ open2 = "idle";
8105
+ } else {
8106
+ return "unknown";
8107
+ }
8108
+ return live2 === false ? "finished" : open2;
8109
+ }
8110
+ var WRITER_LOCKS_SINCE = [0, 155, 0];
8111
+ var VERSION = /^(\d{1,6})\.(\d{1,6})\.(\d{1,6})(-[0-9A-Za-z.-]{1,64})?(?:\+[0-9A-Za-z.-]{1,64})?$/;
8112
+ function keepsWriterLocks(cliVersion) {
8113
+ if (typeof cliVersion !== "string") return false;
8114
+ const match = VERSION.exec(cliVersion);
8115
+ if (!match) return false;
8116
+ const parts = [Number(match[1]), Number(match[2]), Number(match[3])];
8117
+ for (const [index, since] of WRITER_LOCKS_SINCE.entries()) {
8118
+ const part = parts[index];
8119
+ if (part !== since) return part > since;
8120
+ }
8121
+ return match[4] === void 0;
8122
+ }
8123
+ function codexLiveness(fields) {
8124
+ if (!fields.lockFolder) return "unknown";
8125
+ if (fields.locked) return true;
8126
+ return keepsWriterLocks(fields.cliVersion) ? false : "unknown";
8127
+ }
8128
+ var IMPORTED_TURN_ID = /^external-import-turn-\d{1,9}$/;
8129
+ function isImportedCodexTurn(turnId) {
8130
+ return typeof turnId === "string" && IMPORTED_TURN_ID.test(turnId);
8131
+ }
8132
+ function isRecord9(value) {
8133
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8134
+ }
8135
+ var CODEX_DESKTOP_ORIGINATOR = "Codex Desktop";
8136
+ function mapCodexSurface(source, originator) {
8137
+ if (originator === CODEX_DESKTOP_ORIGINATOR) return "desktop";
8138
+ if (source === "cli" || source === "exec") return "terminal";
8139
+ if (source === "vscode") return "vscode";
8140
+ if (isRecord9(source) && source.custom === "chatgpt") return "desktop";
8141
+ return "unknown";
8142
+ }
8143
+ var NOT_A_SESSION_SOURCE = /* @__PURE__ */ new Set(["subagent", "internal", "mcp"]);
8144
+ var NOT_A_SESSION_THREAD = /* @__PURE__ */ new Set(["subagent", "guardian_review", "memory_consolidation"]);
8145
+ function isCodexSessionSource(source, meta = {}) {
8146
+ if (typeof meta.parentThreadId === "string" && meta.parentThreadId !== "") return false;
8147
+ if (typeof meta.threadSource === "string" && NOT_A_SESSION_THREAD.has(meta.threadSource)) {
8148
+ return false;
8149
+ }
8150
+ if (typeof source === "string") return !NOT_A_SESSION_SOURCE.has(source);
8151
+ if (isRecord9(source)) return !Object.keys(source).some((kind) => NOT_A_SESSION_SOURCE.has(kind));
8152
+ return true;
8153
+ }
8154
+
8155
+ // src/collector/adapters/codex/rolloutFile.ts
8156
+ var HEAD_LIMIT_BYTES2 = 2 * 1024 * 1024;
8157
+ var TAIL_LIMIT_BYTES2 = 8 * 1024 * 1024;
8158
+ var FIRST_HEAD_BYTES = 256 * 1024;
8159
+ var TAIL_CHUNK_BYTES2 = 256 * 1024;
8160
+ var NEWLINE3 = 10;
8161
+ var CODEX_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/;
8162
+ function parseCodexTime(value) {
8163
+ if (typeof value !== "string" || !CODEX_TIME.test(value)) return null;
8164
+ const at = Date.parse(value);
8165
+ return Number.isSafeInteger(at) ? at : null;
8166
+ }
8167
+ var LINE_TIME = /^\{"timestamp":"([^"]{1,64})"/;
8168
+ var TURN_HINT = /"type":"(?:task|turn)_/;
8169
+ var TURN_TYPE = /^(?:task|turn)_/;
8170
+ function isRecord10(value) {
8171
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8172
+ }
8173
+ function text3(value) {
8174
+ return typeof value === "string" && value !== "" ? value : void 0;
8175
+ }
8176
+ function label(value) {
8177
+ return typeof value === "string" && value !== "" && value.length <= 64 ? value : void 0;
8178
+ }
8179
+ function compactSource(value) {
8180
+ if (typeof value === "string") return text3(value);
8181
+ if (!isRecord10(value)) return void 0;
8182
+ return Object.fromEntries(
8183
+ Object.entries(value).map(([kind, detail]) => [
8184
+ kind,
8185
+ typeof detail === "string" ? detail : true
8186
+ ])
8187
+ );
8188
+ }
8189
+ function parse(line) {
8190
+ try {
8191
+ const value = JSON.parse(line);
8192
+ return isRecord10(value) ? value : null;
8193
+ } catch {
8194
+ return null;
8195
+ }
8196
+ }
8197
+ function readMetaLine(line) {
8198
+ if (!line.includes('"session_meta"')) return null;
8199
+ const value = parse(line);
8200
+ if (!value || value.type !== "session_meta" || !isRecord10(value.payload)) return null;
8201
+ const payload = value.payload;
8202
+ const meta = {
8203
+ id: text3(payload.id),
8204
+ timestamp: text3(payload.timestamp),
8205
+ cwd: text3(payload.cwd),
8206
+ source: compactSource(payload.source),
8207
+ threadSource: text3(payload.thread_source),
8208
+ parentThreadId: text3(payload.parent_thread_id),
8209
+ originator: label(payload.originator),
8210
+ cliVersion: label(payload.cli_version)
8211
+ };
8212
+ for (const key of Object.keys(meta)) {
8213
+ if (meta[key] === void 0) delete meta[key];
8214
+ }
8215
+ return meta;
8216
+ }
8217
+ function readTurnLine(line) {
8218
+ if (!line.includes('"event_msg"') || !TURN_HINT.test(line)) return null;
8219
+ const value = parse(line);
8220
+ if (!value || value.type !== "event_msg" || !isRecord10(value.payload)) return null;
8221
+ const turn = value.payload.type;
8222
+ if (typeof turn !== "string" || !TURN_TYPE.test(turn)) return null;
8223
+ return {
8224
+ turn,
8225
+ at: parseCodexTime(value.timestamp),
8226
+ imported: isImportedCodexTurn(value.payload.turn_id)
8227
+ };
6415
8228
  }
6416
8229
  function lineTime(line) {
6417
8230
  const match = LINE_TIME.exec(line);
6418
8231
  return match ? parseCodexTime(match[1]) : null;
6419
8232
  }
6420
- function bytes(data) {
8233
+ function bytes2(data) {
6421
8234
  return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
6422
8235
  }
6423
- async function readExactly(file, position, length) {
6424
- const data = bytes(await file.read(position, length));
8236
+ async function readExactly2(file, position, length) {
8237
+ const data = bytes2(await file.read(position, length));
6425
8238
  if (data.byteLength !== length) throw new Error("The file changed while it was read.");
6426
8239
  return data;
6427
8240
  }
6428
8241
  async function readHead(file, size) {
6429
- for (const limit of [FIRST_HEAD_BYTES, HEAD_LIMIT_BYTES]) {
8242
+ for (const limit of [FIRST_HEAD_BYTES, HEAD_LIMIT_BYTES2]) {
6430
8243
  const length = Math.min(limit, size);
6431
- const head = await readExactly(file, 0, length);
8244
+ const head = await readExactly2(file, 0, length);
6432
8245
  let named = head.indexOf('"session_meta"');
6433
8246
  while (named !== -1) {
6434
- const end = head.indexOf(NEWLINE, named);
8247
+ const end = head.indexOf(NEWLINE3, named);
6435
8248
  if (end === -1) break;
6436
- const start = named > 0 ? head.lastIndexOf(NEWLINE, named - 1) + 1 : 0;
8249
+ const start = named > 0 ? head.lastIndexOf(NEWLINE3, named - 1) + 1 : 0;
6437
8250
  const meta = readMetaLine(head.toString("utf8", start, end));
6438
8251
  if (meta) return meta;
6439
8252
  named = head.indexOf('"session_meta"', end + 1);
@@ -6442,9 +8255,9 @@ async function readHead(file, size) {
6442
8255
  }
6443
8256
  return null;
6444
8257
  }
6445
- async function scanTail(file, size) {
8258
+ async function scanTail2(file, size) {
6446
8259
  let position = size;
6447
- let budget2 = TAIL_LIMIT_BYTES;
8260
+ let budget2 = TAIL_LIMIT_BYTES2;
6448
8261
  let fragment = Buffer.alloc(0);
6449
8262
  let end = null;
6450
8263
  let lastLineAt = null;
@@ -6462,15 +8275,15 @@ async function scanTail(file, size) {
6462
8275
  end
6463
8276
  });
6464
8277
  while (position > 0 && budget2 > 0) {
6465
- const length = Math.min(TAIL_CHUNK_BYTES, position, budget2);
8278
+ const length = Math.min(TAIL_CHUNK_BYTES2, position, budget2);
6466
8279
  const start = position - length;
6467
- const chunk = await readExactly(file, start, length);
8280
+ const chunk = await readExactly2(file, start, length);
6468
8281
  budget2 -= length;
6469
8282
  position = start;
6470
8283
  const buffer = fragment.byteLength > 0 ? Buffer.concat([chunk, fragment]) : chunk;
6471
8284
  let cut2 = buffer.byteLength;
6472
8285
  if (end === null) {
6473
- const last = buffer.lastIndexOf(NEWLINE);
8286
+ const last = buffer.lastIndexOf(NEWLINE3);
6474
8287
  if (last === -1) {
6475
8288
  fragment = buffer;
6476
8289
  continue;
@@ -6478,12 +8291,12 @@ async function scanTail(file, size) {
6478
8291
  end = start + last + 1;
6479
8292
  cut2 = last;
6480
8293
  }
6481
- let newline = cut2 > 0 ? buffer.lastIndexOf(NEWLINE, cut2 - 1) : -1;
8294
+ let newline = cut2 > 0 ? buffer.lastIndexOf(NEWLINE3, cut2 - 1) : -1;
6482
8295
  while (newline !== -1) {
6483
8296
  const turn = take(buffer.toString("utf8", newline + 1, cut2));
6484
8297
  if (turn) return found(turn);
6485
8298
  cut2 = newline;
6486
- newline = cut2 > 0 ? buffer.lastIndexOf(NEWLINE, cut2 - 1) : -1;
8299
+ newline = cut2 > 0 ? buffer.lastIndexOf(NEWLINE3, cut2 - 1) : -1;
6487
8300
  }
6488
8301
  if (start === 0) {
6489
8302
  const turn = take(buffer.toString("utf8", 0, cut2));
@@ -6497,7 +8310,7 @@ async function scanTail(file, size) {
6497
8310
  }
6498
8311
  return { lastTurn: void 0, lastTurnAt: null, lastTurnImported: false, lastLineAt, end };
6499
8312
  }
6500
- function stateOf(cached) {
8313
+ function stateOf2(cached) {
6501
8314
  return {
6502
8315
  meta: cached.meta,
6503
8316
  lastTurn: cached.lastTurn,
@@ -6513,7 +8326,7 @@ function createRolloutReader(io) {
6513
8326
  try {
6514
8327
  const { info } = handle;
6515
8328
  const meta = await readHead(handle, info.size);
6516
- const tail = await scanTail(handle, info.size);
8329
+ const tail = await scanTail2(handle, info.size);
6517
8330
  return {
6518
8331
  meta,
6519
8332
  lastTurn: tail.lastTurn,
@@ -6533,14 +8346,14 @@ function createRolloutReader(io) {
6533
8346
  const offset = cached.offset;
6534
8347
  if (cached.meta === null || offset === null) return null;
6535
8348
  if (info.ino !== cached.ino || info.size <= cached.size || info.size < offset) return null;
6536
- if (info.size - offset > TAIL_LIMIT_BYTES) return null;
8349
+ if (info.size - offset > TAIL_LIMIT_BYTES2) return null;
6537
8350
  const handle = await io.openRegular(file);
6538
8351
  try {
6539
8352
  const opened = handle.info;
6540
8353
  if (opened.ino !== cached.ino || opened.size < offset) return null;
6541
8354
  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;
8355
+ const read = await readExactly2(handle, from, opened.size - from);
8356
+ if (offset > 0 && read[0] !== NEWLINE3) return null;
6544
8357
  const added = offset > 0 ? read.subarray(1) : read;
6545
8358
  const next = {
6546
8359
  ...cached,
@@ -6549,7 +8362,7 @@ function createRolloutReader(io) {
6549
8362
  mtimeMs: opened.mtimeMs
6550
8363
  };
6551
8364
  let start = 0;
6552
- let newline = added.indexOf(NEWLINE);
8365
+ let newline = added.indexOf(NEWLINE3);
6553
8366
  while (newline !== -1) {
6554
8367
  const line = added.toString("utf8", start, newline);
6555
8368
  if (line.trim() !== "") {
@@ -6563,7 +8376,7 @@ function createRolloutReader(io) {
6563
8376
  }
6564
8377
  }
6565
8378
  start = newline + 1;
6566
- newline = added.indexOf(NEWLINE, start);
8379
+ newline = added.indexOf(NEWLINE3, start);
6567
8380
  }
6568
8381
  next.offset = offset + start;
6569
8382
  return next;
@@ -6575,11 +8388,11 @@ function createRolloutReader(io) {
6575
8388
  async read(file, info) {
6576
8389
  const cached = cache.get(file);
6577
8390
  if (cached && cached.ino === info.ino && cached.size === info.size && cached.mtimeMs === info.mtimeMs) {
6578
- return stateOf(cached);
8391
+ return stateOf2(cached);
6579
8392
  }
6580
8393
  const next = cached && await readOnward(file, cached, info) || await readFresh(file);
6581
8394
  cache.set(file, next);
6582
- return stateOf(next);
8395
+ return stateOf2(next);
6583
8396
  },
6584
8397
  keepOnly(files) {
6585
8398
  for (const file of cache.keys()) {
@@ -6590,10 +8403,10 @@ function createRolloutReader(io) {
6590
8403
  }
6591
8404
 
6592
8405
  // 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}";
8406
+ import path16 from "node:path";
8407
+ var UUID4 = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
6595
8408
  var ROLLOUT_FILE_NAME = new RegExp(
6596
- `^rollout-\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-(${UUID})(?:_(${UUID}))?\\.jsonl$`,
8409
+ `^rollout-\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-(${UUID4})(?:_(${UUID4}))?\\.jsonl$`,
6597
8410
  "i"
6598
8411
  );
6599
8412
  var INDEX_REFRESH_MS = 3e4;
@@ -6608,7 +8421,7 @@ function twoDigits(value) {
6608
8421
  function recentDayFolders(sessionsDir, now) {
6609
8422
  const folders = [now, now - 24 * 60 * 60 * 1e3].map((at) => {
6610
8423
  const date = new Date(at);
6611
- return path11.join(
8424
+ return path16.join(
6612
8425
  sessionsDir,
6613
8426
  String(date.getFullYear()),
6614
8427
  twoDigits(date.getMonth() + 1),
@@ -6637,12 +8450,12 @@ function createRolloutFinder(options) {
6637
8450
  const refs = [];
6638
8451
  for (const name of (await names(dir)).sort()) {
6639
8452
  const threadId = threadIdOfRollout(name);
6640
- if (threadId !== null) refs.push({ path: path11.join(dir, name), threadId });
8453
+ if (threadId !== null) refs.push({ path: path16.join(dir, name), threadId });
6641
8454
  }
6642
8455
  return refs;
6643
8456
  }
6644
8457
  async function subfolders(dir, pattern) {
6645
- return (await names(dir)).filter((name) => pattern.test(name)).sort().map((name) => path11.join(dir, name));
8458
+ return (await names(dir)).filter((name) => pattern.test(name)).sort().map((name) => path16.join(dir, name));
6646
8459
  }
6647
8460
  async function buildIndex() {
6648
8461
  const years = await subfolders(sessionsDir, YEAR);
@@ -6686,7 +8499,7 @@ function createRolloutFinder(options) {
6686
8499
  // src/collector/adapters/codex/sessionIndex.ts
6687
8500
  var SESSION_INDEX_FILE = "session_index.jsonl";
6688
8501
  var SESSION_INDEX_LIMIT_BYTES = 4 * 1024 * 1024;
6689
- function isRecord10(value) {
8502
+ function isRecord11(value) {
6690
8503
  return typeof value === "object" && value !== null && !Array.isArray(value);
6691
8504
  }
6692
8505
  function parseSessionIndex(content) {
@@ -6699,7 +8512,7 @@ function parseSessionIndex(content) {
6699
8512
  } catch {
6700
8513
  continue;
6701
8514
  }
6702
- if (!isRecord10(value)) continue;
8515
+ if (!isRecord11(value)) continue;
6703
8516
  const { id, thread_name: name } = value;
6704
8517
  if (typeof id !== "string" || id === "" || typeof name !== "string") continue;
6705
8518
  const trimmed = name.trim();
@@ -6750,14 +8563,14 @@ function createSessionIndexReader(file, io) {
6750
8563
  }
6751
8564
 
6752
8565
  // src/collector/adapters/codex/toSession.ts
6753
- var SOURCE_ID2 = "codex";
6754
- var WRITE_ORDER_SLACK_MS2 = 1e3;
8566
+ var SOURCE_ID3 = "codex";
8567
+ var WRITE_ORDER_SLACK_MS3 = 1e3;
6755
8568
  function lastActivity(state, now) {
6756
8569
  return plausibleTime(state.lastLineAt, now) ?? plausibleTime(state.lastTurnAt, now) ?? plausibleTime(parseCodexTime(state.meta?.timestamp), now);
6757
8570
  }
6758
8571
  function codexSession(input) {
6759
8572
  const { threadId, state, live: live2, now } = input;
6760
- const id = `${SOURCE_ID2}:${threadId}`;
8573
+ const id = `${SOURCE_ID3}:${threadId}`;
6761
8574
  const cwd = state.meta?.cwd ?? null;
6762
8575
  const project = projectOf(cwd);
6763
8576
  const status = mapCodexStatus({ lastTurn: state.lastTurn, live: live2 });
@@ -6768,12 +8581,12 @@ function codexSession(input) {
6768
8581
  } else if (status === "idle" || status === "finished") {
6769
8582
  statusSince = plausibleTime(state.lastLineAt, now);
6770
8583
  }
6771
- if (statusSince !== null && startedAt !== null && statusSince < startedAt - WRITE_ORDER_SLACK_MS2) {
8584
+ if (statusSince !== null && startedAt !== null && statusSince < startedAt - WRITE_ORDER_SLACK_MS3) {
6772
8585
  statusSince = null;
6773
8586
  }
6774
8587
  const session = {
6775
8588
  id,
6776
- source: SOURCE_ID2,
8589
+ source: SOURCE_ID3,
6777
8590
  surface: mapCodexSurface(state.meta?.source, state.meta?.originator),
6778
8591
  // A name made only of spaces is no name. It is cleaned and cut as every other agent's is.
6779
8592
  name: sessionName(input.name ?? "") || project || id,
@@ -6786,7 +8599,7 @@ function codexSession(input) {
6786
8599
  stale: isStale({ status, statusSince }, now)
6787
8600
  };
6788
8601
  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;
8602
+ const beforeStart = writtenAt !== null && startedAt !== null && writtenAt < startedAt - WRITE_ORDER_SLACK_MS3;
6790
8603
  if (writtenAt !== null && !beforeStart) session.lastWriteAt = writtenAt;
6791
8604
  return session;
6792
8605
  }
@@ -6816,11 +8629,11 @@ async function readWriterLocks(dir, io) {
6816
8629
  // src/collector/adapters/codex/index.ts
6817
8630
  var CODEX_HOME_ENV = "AGENT_LOOKOUT_CODEX_HOME";
6818
8631
  var CODEX_OWN_HOME_ENV = "CODEX_HOME";
6819
- var RECHECK_MS = 6e4;
6820
- var BASIS = "files";
8632
+ var RECHECK_MS2 = 6e4;
8633
+ var BASIS2 = "files";
6821
8634
  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.";
8635
+ var LABEL3 = "Codex";
8636
+ 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
8637
  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
8638
  var CODEX_CAPABILITIES = {
6826
8639
  "working-and-idle": { level: "yes" },
@@ -6853,18 +8666,18 @@ var CODEX_CAPABILITIES = {
6853
8666
  };
6854
8667
  function createCodexAdapter(options = {}) {
6855
8668
  const env = options.env ?? process.env;
6856
- const homeDir = options.homeDir ?? os5.homedir();
8669
+ const homeDir = options.homeDir ?? os6.homedir();
6857
8670
  const now = options.now ?? Date.now;
6858
8671
  const io = options.io ?? nodeIo2;
6859
8672
  const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
6860
- const recheckMs = options.recheckMs ?? RECHECK_MS;
8673
+ const recheckMs = options.recheckMs ?? RECHECK_MS2;
6861
8674
  const ownOverride = env[CODEX_HOME_ENV]?.trim() || void 0;
6862
8675
  const codexOverride = env[CODEX_OWN_HOME_ENV]?.trim() || void 0;
6863
8676
  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);
8677
+ const codexHome = path17.resolve(ownOverride ?? codexOverride ?? path17.join(homeDir, ".codex"));
8678
+ const sessionsDir = path17.join(codexHome, "sessions");
8679
+ const locksDir = path17.join(codexHome, WRITER_LOCK_DIR);
8680
+ const indexFile = path17.join(codexHome, SESSION_INDEX_FILE);
6868
8681
  const homeName = tildify(codexHome, homeDir);
6869
8682
  const sessionsName = tildify(sessionsDir, homeDir);
6870
8683
  const locksName = tildify(locksDir, homeDir);
@@ -6890,14 +8703,14 @@ function createCodexAdapter(options = {}) {
6890
8703
  }
6891
8704
  function result(state, detail, read, checkedAt) {
6892
8705
  const health = {
6893
- id: SOURCE_ID2,
6894
- label: LABEL2,
8706
+ id: SOURCE_ID3,
8707
+ label: LABEL3,
6895
8708
  state,
6896
8709
  detail,
6897
8710
  watching: watching(read),
6898
8711
  checkedAt
6899
8712
  };
6900
- return state === "ok" ? { health, sessions: [], basis: BASIS } : { health, sessions: [] };
8713
+ return state === "ok" ? { health, sessions: [], basis: BASIS2 } : { health, sessions: [] };
6901
8714
  }
6902
8715
  function notFound(checkedAt) {
6903
8716
  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 +8758,7 @@ function createCodexAdapter(options = {}) {
6945
8758
  if (homeFrom === "agent-lookout") {
6946
8759
  return result(
6947
8760
  "ok",
6948
- `${CODEX_HOME_ENV} is set to ${homeName}, which has no sessions folder, so no Codex sessions are listed. ${NEEDS_YOU_NOTE}`,
8761
+ `${CODEX_HOME_ENV} is set to ${homeName}, which has no sessions folder, so no Codex sessions are listed. ${NEEDS_YOU_NOTE2}`,
6949
8762
  "not found",
6950
8763
  checkedAt
6951
8764
  );
@@ -6958,7 +8771,7 @@ function createCodexAdapter(options = {}) {
6958
8771
  homeSeen = true;
6959
8772
  return result(
6960
8773
  "ok",
6961
- `Codex has not saved any sessions in ${sessionsName} yet. ${NEEDS_YOU_NOTE}`,
8774
+ `Codex has not saved any sessions in ${sessionsName} yet. ${NEEDS_YOU_NOTE2}`,
6962
8775
  "not found",
6963
8776
  checkedAt
6964
8777
  );
@@ -7040,19 +8853,19 @@ function createCodexAdapter(options = {}) {
7040
8853
  now: checkedAt
7041
8854
  });
7042
8855
  });
7043
- let detail = `Sessions are read from the files Codex saves in ${sessionsName}. ${NEEDS_YOU_NOTE}`;
8856
+ let detail = `Sessions are read from the files Codex saves in ${sessionsName}. ${NEEDS_YOU_NOTE2}`;
7044
8857
  if (!locks.supported) {
7045
8858
  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
8859
  detail += ` ${why}, so a session that has ended cannot be told from one that is idle, and none is shown as finished.`;
7047
8860
  } else if (olderCodex) {
7048
8861
  detail += ` ${OLDER_CODEX_NOTE}`;
7049
8862
  }
7050
- const answer = result("ok", detail, every(pollIntervalMs), checkedAt);
7051
- return { ...answer, sessions, basis: locks.supported ? BASIS : BASIS_WITHOUT_LOCKS };
8863
+ const answer2 = result("ok", detail, every(pollIntervalMs), checkedAt);
8864
+ return { ...answer2, sessions, basis: locks.supported ? BASIS2 : BASIS_WITHOUT_LOCKS };
7052
8865
  }
7053
8866
  return {
7054
- id: SOURCE_ID2,
7055
- label: LABEL2,
8867
+ id: SOURCE_ID3,
8868
+ label: LABEL3,
7056
8869
  capabilities: CODEX_CAPABILITIES,
7057
8870
  lookingIn: `Looking for Codex sessions in ${sessionsName}.`,
7058
8871
  async poll() {
@@ -7071,8 +8884,8 @@ function createCodexAdapter(options = {}) {
7071
8884
  }
7072
8885
 
7073
8886
  // src/collector/adapters/status-files/index.ts
7074
- import os6 from "node:os";
7075
- import path13 from "node:path";
8887
+ import os7 from "node:os";
8888
+ import path18 from "node:path";
7076
8889
 
7077
8890
  // src/core/mapping/statusFileMapping.ts
7078
8891
  var STATUS = /* @__PURE__ */ new Map([
@@ -7152,16 +8965,16 @@ function parseStatusFile(content) {
7152
8965
  }
7153
8966
 
7154
8967
  // src/collector/adapters/status-files/toSession.ts
7155
- var SOURCE_ID3 = "status-files";
7156
- var WRITE_ORDER_SLACK_MS3 = 1e3;
8968
+ var SOURCE_ID4 = "status-files";
8969
+ var WRITE_ORDER_SLACK_MS4 = 1e3;
7157
8970
  function statusFileSession(input) {
7158
8971
  const { fileName: fileName2, file, statusSince, now } = input;
7159
8972
  const cwd = file.cwd ?? null;
7160
8973
  const project = projectOf(cwd);
7161
8974
  const mapped = mapStatusFileStatus(file);
7162
8975
  const session = {
7163
- id: `${SOURCE_ID3}:${fileName2}`,
7164
- source: SOURCE_ID3,
8976
+ id: `${SOURCE_ID4}:${fileName2}`,
8977
+ source: SOURCE_ID4,
7165
8978
  agent: file.agent,
7166
8979
  surface: mapStatusFileApp(file.app),
7167
8980
  // The file's name is as untrusted as its contents, so it is cleaned and cut
@@ -7181,15 +8994,15 @@ function statusFileSession(input) {
7181
8994
  }
7182
8995
  const writtenAt = input.writtenAt === void 0 ? null : plausibleTime(Math.floor(input.writtenAt), now);
7183
8996
  const given = plausibleTime(file.since, now);
7184
- const beforeSince = writtenAt !== null && given !== null && writtenAt < given - WRITE_ORDER_SLACK_MS3;
8997
+ const beforeSince = writtenAt !== null && given !== null && writtenAt < given - WRITE_ORDER_SLACK_MS4;
7185
8998
  if (writtenAt !== null && !beforeSince) session.lastWriteAt = writtenAt;
7186
8999
  return session;
7187
9000
  }
7188
9001
 
7189
9002
  // src/collector/adapters/status-files/index.ts
7190
9003
  var STATUS_DIR_ENV = "AGENT_LOOKOUT_STATUS_DIR";
7191
- var BASIS2 = "files";
7192
- var LABEL3 = "Status files";
9004
+ var BASIS3 = "files";
9005
+ var LABEL4 = "Status files";
7193
9006
  var STATUS_FILE_CAPABILITIES = {
7194
9007
  "working-and-idle": { level: "partly", reason: "If the agent writes working and idle." },
7195
9008
  "needs-you": { level: "partly", reason: "If the agent writes waiting." },
@@ -7215,13 +9028,13 @@ function counted(count2, one, many) {
7215
9028
  }
7216
9029
  function createStatusFileAdapter(options = {}) {
7217
9030
  const env = options.env ?? process.env;
7218
- const homeDir = options.homeDir ?? os6.homedir();
9031
+ const homeDir = options.homeDir ?? os7.homedir();
7219
9032
  const now = options.now ?? Date.now;
7220
9033
  const io = options.io ?? nodeIo2;
7221
9034
  const isAlive = options.isAlive ?? isProcessAlive;
7222
9035
  const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
7223
9036
  const override = env[STATUS_DIR_ENV]?.trim() || void 0;
7224
- const dir = path13.resolve(override ?? path13.join(homeDir, ".agent-lookout", "sessions"));
9037
+ const dir = path18.resolve(override ?? path18.join(homeDir, ".agent-lookout", "sessions"));
7225
9038
  const dirName = tildify(dir, homeDir);
7226
9039
  const decoder2 = new TextDecoder();
7227
9040
  let lastRead = /* @__PURE__ */ new Map();
@@ -7242,15 +9055,15 @@ function createStatusFileAdapter(options = {}) {
7242
9055
  }
7243
9056
  function result(state, detail, facts, checkedAt, sessions = []) {
7244
9057
  const health = {
7245
- id: SOURCE_ID3,
7246
- label: LABEL3,
9058
+ id: SOURCE_ID4,
9059
+ label: LABEL4,
7247
9060
  state,
7248
9061
  detail,
7249
9062
  watching: facts,
7250
9063
  checkedAt
7251
9064
  };
7252
- if (state === "ok") return { health, sessions, basis: BASIS2 };
7253
- if (state === "not-set-up") return { health, sessions: [], basis: BASIS2 };
9065
+ if (state === "ok") return { health, sessions, basis: BASIS3 };
9066
+ if (state === "not-set-up") return { health, sessions: [], basis: BASIS3 };
7254
9067
  return { health, sessions: [] };
7255
9068
  }
7256
9069
  async function readOne(file) {
@@ -7266,9 +9079,9 @@ function createStatusFileAdapter(options = {}) {
7266
9079
  const tooLarge = { kind: "refused", why: "is over 16 KB" };
7267
9080
  try {
7268
9081
  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));
9082
+ const bytes3 = await open2.read(0, MAX_FILE_BYTES + 1);
9083
+ if (bytes3.byteLength > MAX_FILE_BYTES) return tooLarge;
9084
+ const parsed = parseStatusFile(decoder2.decode(bytes3));
7272
9085
  return parsed ? { kind: "read", file: parsed, mtimeMs: open2.info.mtimeMs } : { kind: "invalid" };
7273
9086
  } catch {
7274
9087
  return { kind: "refused", why: "could not be read" };
@@ -7281,7 +9094,7 @@ function createStatusFileAdapter(options = {}) {
7281
9094
  if (names.length <= MAX_FILES) return names;
7282
9095
  const written = await Promise.all(
7283
9096
  names.map(async (name) => {
7284
- const info = await io.lstat(path13.join(dir, name)).catch(() => null);
9097
+ const info = await io.lstat(path18.join(dir, name)).catch(() => null);
7285
9098
  return { name, mtimeMs: info?.mtimeMs ?? -Infinity };
7286
9099
  })
7287
9100
  );
@@ -7302,14 +9115,14 @@ function createStatusFileAdapter(options = {}) {
7302
9115
  names = await io.readdir(dir);
7303
9116
  } catch (error) {
7304
9117
  if (!isMissing2(error)) {
7305
- const answer = result(
9118
+ const answer2 = result(
7306
9119
  "error",
7307
9120
  `Status files could not be read: the folder ${dirName} could not be listed.`,
7308
9121
  watching("cannot be read"),
7309
9122
  checkedAt
7310
9123
  );
7311
- answer.health.advice = "Check that your user account can open the folder.";
7312
- return answer;
9124
+ answer2.health.advice = "Check that your user account can open the folder.";
9125
+ return answer2;
7313
9126
  }
7314
9127
  contentsKnown = true;
7315
9128
  lastRead = /* @__PURE__ */ new Map();
@@ -7331,7 +9144,7 @@ function createStatusFileAdapter(options = {}) {
7331
9144
  const nextRead = /* @__PURE__ */ new Map();
7332
9145
  const nextStatuses = /* @__PURE__ */ new Map();
7333
9146
  for (const fileName2 of await toRead(listed)) {
7334
- const reading = await readOne(path13.join(dir, fileName2));
9147
+ const reading = await readOne(path18.join(dir, fileName2));
7335
9148
  let kept;
7336
9149
  if (reading.kind === "read") {
7337
9150
  kept = reading;
@@ -7397,8 +9210,8 @@ function createStatusFileAdapter(options = {}) {
7397
9210
  );
7398
9211
  }
7399
9212
  return {
7400
- id: SOURCE_ID3,
7401
- label: LABEL3,
9213
+ id: SOURCE_ID4,
9214
+ label: LABEL4,
7402
9215
  capabilities: STATUS_FILE_CAPABILITIES,
7403
9216
  lookingIn: `Looking for status files in ${dirName}.`,
7404
9217
  async poll() {
@@ -7557,8 +9370,8 @@ function createEventStore(capacity = EVENT_CAPACITY) {
7557
9370
  }
7558
9371
 
7559
9372
  // src/collector/git/branchFinder.ts
7560
- import os7 from "node:os";
7561
- import path15 from "node:path";
9373
+ import os8 from "node:os";
9374
+ import path20 from "node:path";
7562
9375
 
7563
9376
  // src/collector/git/gitHead.ts
7564
9377
  var MAX_GIT_FILE_BYTES = 4 * 1024;
@@ -7597,19 +9410,19 @@ function parseCommonDir(content) {
7597
9410
  }
7598
9411
 
7599
9412
  // src/collector/git/repository.ts
7600
- import { createHash as createHash2 } from "node:crypto";
7601
- import path14 from "node:path";
9413
+ import { createHash as createHash3 } from "node:crypto";
9414
+ import path19 from "node:path";
7602
9415
  var REPOSITORY_ID_LENGTH = 16;
7603
9416
  function repositoryId(gitFolder) {
7604
- return createHash2("sha256").update(gitFolder).digest("hex").slice(0, REPOSITORY_ID_LENGTH);
9417
+ return createHash3("sha256").update(gitFolder).digest("hex").slice(0, REPOSITORY_ID_LENGTH);
7605
9418
  }
7606
9419
  function sharedName(common) {
7607
- const base = path14.basename(common);
7608
- if (base.startsWith(".")) return path14.basename(path14.dirname(common));
9420
+ const base = path19.basename(common);
9421
+ if (base.startsWith(".")) return path19.basename(path19.dirname(common));
7609
9422
  return base.endsWith(".git") ? base.slice(0, -".git".length) : base;
7610
9423
  }
7611
9424
  function repositoryOf({ dir, gitDir, commonDir }) {
7612
- const name = sessionName(commonDir === null ? path14.basename(dir) : sharedName(commonDir));
9425
+ const name = sessionName(commonDir === null ? path19.basename(dir) : sharedName(commonDir));
7613
9426
  return name === void 0 ? null : { id: repositoryId(commonDir ?? gitDir), name };
7614
9427
  }
7615
9428
 
@@ -7627,9 +9440,9 @@ async function readGitFile(io, file, maxBytes = MAX_GIT_FILE_BYTES) {
7627
9440
  }
7628
9441
  try {
7629
9442
  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);
9443
+ const bytes3 = await open2.read(0, maxBytes + 1);
9444
+ if (bytes3.byteLength > maxBytes) return null;
9445
+ return decoder.decode(bytes3);
7633
9446
  } catch {
7634
9447
  return null;
7635
9448
  } finally {
@@ -7638,13 +9451,13 @@ async function readGitFile(io, file, maxBytes = MAX_GIT_FILE_BYTES) {
7638
9451
  }
7639
9452
  }
7640
9453
  async function commonDirOf(io, gitDir) {
7641
- const content = await readGitFile(io, path15.join(gitDir, "commondir"));
9454
+ const content = await readGitFile(io, path20.join(gitDir, "commondir"));
7642
9455
  if (content === void 0) return null;
7643
9456
  const target = content === null ? null : parseCommonDir(content);
7644
- return target === null ? void 0 : path15.resolve(gitDir, target);
9457
+ return target === null ? void 0 : path20.resolve(gitDir, target);
7645
9458
  }
7646
9459
  async function headIn(dir, kind, io) {
7647
- const marker = path15.join(dir, ".git");
9460
+ const marker = path20.join(dir, ".git");
7648
9461
  let gitDir;
7649
9462
  if (kind === "directory") {
7650
9463
  gitDir = marker;
@@ -7652,11 +9465,11 @@ async function headIn(dir, kind, io) {
7652
9465
  const content2 = await readGitFile(io, marker);
7653
9466
  const target = typeof content2 === "string" ? parseGitFile(content2) : null;
7654
9467
  if (target === null) return null;
7655
- gitDir = path15.resolve(dir, target);
9468
+ gitDir = path20.resolve(dir, target);
7656
9469
  } else {
7657
9470
  return null;
7658
9471
  }
7659
- const content = await readGitFile(io, path15.join(gitDir, "HEAD"));
9472
+ const content = await readGitFile(io, path20.join(gitDir, "HEAD"));
7660
9473
  const head = typeof content === "string" ? parseHead(content) : null;
7661
9474
  if (head === null) return null;
7662
9475
  const commonDir = kind === "file" ? await commonDirOf(io, gitDir) : null;
@@ -7665,20 +9478,20 @@ async function headIn(dir, kind, io) {
7665
9478
  return { head: { ...head, repository }, gitFolder: commonDir ?? gitDir };
7666
9479
  }
7667
9480
  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);
9481
+ if (!path20.isAbsolute(cwd)) return null;
9482
+ const home = path20.resolve(homeDir);
9483
+ let dir = path20.resolve(cwd);
7671
9484
  try {
7672
9485
  if ((await io.stat(dir)).kind !== "directory") return null;
7673
9486
  } catch {
7674
9487
  return null;
7675
9488
  }
7676
9489
  for (let level = 0; level < MAX_LEVELS; level += 1) {
7677
- const parent = path15.dirname(dir);
9490
+ const parent = path20.dirname(dir);
7678
9491
  if (dir === home || dir === parent) return null;
7679
9492
  let kind;
7680
9493
  try {
7681
- kind = (await io.lstat(path15.join(dir, ".git"))).kind;
9494
+ kind = (await io.lstat(path20.join(dir, ".git"))).kind;
7682
9495
  } catch (error) {
7683
9496
  if (!isMissing2(error)) return null;
7684
9497
  dir = parent;
@@ -7699,7 +9512,7 @@ function within(work, ms) {
7699
9512
  });
7700
9513
  }
7701
9514
  function createBranchFinder(options = {}) {
7702
- const where = { io: options.io ?? nodeIo2, homeDir: options.homeDir ?? os7.homedir() };
9515
+ const where = { io: options.io ?? nodeIo2, homeDir: options.homeDir ?? os8.homedir() };
7703
9516
  const now = options.now ?? Date.now;
7704
9517
  const waitMs = options.waitMs ?? READ_WAIT_MS;
7705
9518
  const known = /* @__PURE__ */ new Map();
@@ -7761,14 +9574,14 @@ var STATUS_STATE = {
7761
9574
  PENDING: "pending",
7762
9575
  EXPECTED: "pending"
7763
9576
  };
7764
- function isRecord11(value) {
9577
+ function isRecord12(value) {
7765
9578
  return typeof value === "object" && value !== null && !Array.isArray(value);
7766
9579
  }
7767
9580
  function textOf(value) {
7768
9581
  return typeof value === "string" ? value : "";
7769
9582
  }
7770
9583
  function checkOf(value) {
7771
- if (!isRecord11(value)) return null;
9584
+ if (!isRecord12(value)) return null;
7772
9585
  const startedAt = Date.parse(textOf(value.startedAt));
7773
9586
  const at = Number.isNaN(startedAt) ? -Infinity : startedAt;
7774
9587
  if (value.__typename === "CheckRun") {
@@ -7804,7 +9617,7 @@ function checksOf(rollup) {
7804
9617
  for (const { state } of latest.values()) counts[state] += 1;
7805
9618
  return { state: checksStateOf(counts), ...counts };
7806
9619
  }
7807
- function stateOf2(state, isDraft) {
9620
+ function stateOf3(state, isDraft) {
7808
9621
  if (state === "OPEN") return isDraft === true ? "draft" : "open";
7809
9622
  if (state === "MERGED") return "merged";
7810
9623
  if (state === "CLOSED") return "closed";
@@ -7817,9 +9630,9 @@ function readGhAnswer(stdout, repository) {
7817
9630
  } catch {
7818
9631
  return null;
7819
9632
  }
7820
- if (!isRecord11(data)) return null;
9633
+ if (!isRecord12(data)) return null;
7821
9634
  const { number } = data;
7822
- const state = stateOf2(data.state, data.isDraft);
9635
+ const state = stateOf3(data.state, data.isDraft);
7823
9636
  const title = typeof data.title === "string" ? sessionName(data.title) : void 0;
7824
9637
  if (!isPullRequestNumber(number) || state === null || title === void 0) return null;
7825
9638
  return {
@@ -7863,11 +9676,10 @@ function ghArguments(target) {
7863
9676
  ];
7864
9677
  }
7865
9678
  var LEFT_OUT = ["GH_FORCE_TTY", "GH_DEBUG", "DEBUG", "GH_PAGER", "PAGER"];
7866
- var OWN_SETTING = /^AGENT_LOOKOUT_/;
7867
9679
  function ghEnvironment(env) {
7868
9680
  const given = {};
7869
- for (const [name, value] of Object.entries(env)) {
7870
- if (!OWN_SETTING.test(name) && !LEFT_OUT.includes(name)) given[name] = value;
9681
+ for (const [name, value] of Object.entries(childEnvironment(env))) {
9682
+ if (!LEFT_OUT.includes(name)) given[name] = value;
7871
9683
  }
7872
9684
  return {
7873
9685
  ...given,
@@ -7956,7 +9768,7 @@ function createGhAsker(options) {
7956
9768
  }
7957
9769
 
7958
9770
  // src/collector/git/pullRequestTarget.ts
7959
- import path16 from "node:path";
9771
+ import path21 from "node:path";
7960
9772
 
7961
9773
  // src/collector/git/gitConfig.ts
7962
9774
  var MAX_CONFIG_BYTES = 64 * 1024;
@@ -8214,7 +10026,7 @@ function targetOf(config, branch, base, defaultBranch) {
8214
10026
  async function readPullRequestTarget(gitFolder, branch, io = nodeIo2) {
8215
10027
  try {
8216
10028
  if (!isAskableBranch(branch)) return null;
8217
- const text4 = await readGitFile(io, path16.join(gitFolder, "config"), MAX_CONFIG_BYTES);
10029
+ const text4 = await readGitFile(io, path21.join(gitFolder, "config"), MAX_CONFIG_BYTES);
8218
10030
  if (typeof text4 !== "string") return null;
8219
10031
  const config = parseGitConfig(text4);
8220
10032
  const base = config === null ? null : baseRemoteOf(config);
@@ -8223,7 +10035,7 @@ async function readPullRequestTarget(gitFolder, branch, io = nodeIo2) {
8223
10035
  if (base.name !== null && isSafeRemoteName(base.name)) {
8224
10036
  const head = await readGitFile(
8225
10037
  io,
8226
- path16.join(gitFolder, "refs", "remotes", base.name, "HEAD")
10038
+ path21.join(gitFolder, "refs", "remotes", base.name, "HEAD")
8227
10039
  );
8228
10040
  if (typeof head === "string") defaultBranch = parseRemoteHead(head, base.name);
8229
10041
  }
@@ -8258,19 +10070,19 @@ function createPullRequestFinder(options) {
8258
10070
  let gh = "unknown";
8259
10071
  let last = null;
8260
10072
  const due = (at, since) => at - (since ?? -Infinity) >= intervalMs;
8261
- function take(question, answer, at) {
8262
- switch (answer.kind) {
10073
+ function take(question, answer2, at) {
10074
+ switch (answer2.kind) {
8263
10075
  case "found":
8264
10076
  case "none":
8265
10077
  gh = "ready";
8266
10078
  last = { at, ok: true };
8267
10079
  answers2.set(question, {
8268
- pullRequest: answer.kind === "found" ? answer.pullRequest : null,
10080
+ pullRequest: answer2.kind === "found" ? answer2.pullRequest : null,
8269
10081
  askedAt: at
8270
10082
  });
8271
10083
  return true;
8272
10084
  case "failed":
8273
- last = { at, ok: false, reason: answer.reason };
10085
+ last = { at, ok: false, reason: answer2.reason };
8274
10086
  answers2.set(question, {
8275
10087
  pullRequest: answers2.get(question)?.pullRequest ?? null,
8276
10088
  askedAt: at
@@ -8278,7 +10090,7 @@ function createPullRequestFinder(options) {
8278
10090
  return true;
8279
10091
  case "not-found":
8280
10092
  case "signed-out":
8281
- gh = answer.kind;
10093
+ gh = answer2.kind;
8282
10094
  answers2.clear();
8283
10095
  pausedUntil = at + intervalMs;
8284
10096
  return false;
@@ -8298,14 +10110,14 @@ function createPullRequestFinder(options) {
8298
10110
  const at = now();
8299
10111
  if (at < pausedUntil) return;
8300
10112
  if (!due(at, answers2.get(question)?.askedAt)) continue;
8301
- const answer = await ask(target).catch(() => ({
10113
+ const answer2 = await ask(target).catch(() => ({
8302
10114
  kind: "failed",
8303
10115
  reason: "gh could not be run"
8304
10116
  }));
8305
10117
  if (![...reads.values()].some((read) => read.target && questionOf(read.target) === question)) {
8306
10118
  continue;
8307
10119
  }
8308
- if (!take(question, answer, now())) return;
10120
+ if (!take(question, answer2, now())) return;
8309
10121
  }
8310
10122
  }
8311
10123
  return {
@@ -8394,7 +10206,7 @@ function createClearHistoryRoute(options) {
8394
10206
  }
8395
10207
 
8396
10208
  // src/collector/history/historyKeeper.ts
8397
- import path17 from "node:path";
10209
+ import path22 from "node:path";
8398
10210
 
8399
10211
  // src/collector/history/historyFiles.ts
8400
10212
  import {
@@ -8694,7 +10506,7 @@ function createHistoryKeeper(options) {
8694
10506
  });
8695
10507
  const lock = createWriterLock({
8696
10508
  fs,
8697
- file: path17.join(dir, LOCK_FILE),
10509
+ file: path22.join(dir, LOCK_FILE),
8698
10510
  pid: options.pid,
8699
10511
  now,
8700
10512
  isAlive: options.isAlive,
@@ -8720,7 +10532,7 @@ function createHistoryKeeper(options) {
8720
10532
  });
8721
10533
  return next;
8722
10534
  }
8723
- const full = (name) => path17.join(dir, name);
10535
+ const full = (name) => path22.join(dir, name);
8724
10536
  function hold(record) {
8725
10537
  if (unusable !== null || !running) return;
8726
10538
  const line = encodeRecord(record);
@@ -8887,13 +10699,13 @@ function createHistoryKeeper(options) {
8887
10699
  continue;
8888
10700
  }
8889
10701
  let text4 = lead;
8890
- let bytes2 = lead.length;
10702
+ let bytes3 = lead.length;
8891
10703
  let end = index;
8892
10704
  while (end < lines.length) {
8893
10705
  const lineBytes = byteLength(lines[end]);
8894
- if (bytes2 - lead.length + lineBytes > room) break;
10706
+ if (bytes3 - lead.length + lineBytes > room) break;
8895
10707
  text4 += lines[end];
8896
- bytes2 += lineBytes;
10708
+ bytes3 += lineBytes;
8897
10709
  end += 1;
8898
10710
  }
8899
10711
  const outcome = yield { name, text: text4 };
@@ -8906,7 +10718,7 @@ function createHistoryKeeper(options) {
8906
10718
  const parsed = readHistoryFileName(name);
8907
10719
  files.set(name, {
8908
10720
  ...parsed,
8909
- size: size + bytes2,
10721
+ size: size + bytes3,
8910
10722
  first: known?.first ?? (record && { at: recordAt(record), kind: record.kind })
8911
10723
  });
8912
10724
  touched.add(name);
@@ -9134,8 +10946,8 @@ function createHistoryKeeper(options) {
9134
10946
  return { at: oldest.first.at, by: BEGINNING[oldest.first.kind] };
9135
10947
  },
9136
10948
  status() {
9137
- let bytes2 = 0;
9138
- for (const file of files.values()) bytes2 += file.size;
10949
+ let bytes3 = 0;
10950
+ for (const file of files.values()) bytes3 += file.size;
9139
10951
  let problem = unusable ?? failing;
9140
10952
  if (problem === null && writer === false) {
9141
10953
  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 +10955,7 @@ function createHistoryKeeper(options) {
9143
10955
  return {
9144
10956
  where: "disk",
9145
10957
  folder,
9146
- bytes: bytes2,
10958
+ bytes: bytes3,
9147
10959
  maxBytes,
9148
10960
  maxAgeMs,
9149
10961
  canClear: unusable === null && writer === true && lock.held,
@@ -9180,15 +10992,15 @@ function keptHistoryStore(store, keeper) {
9180
10992
  }
9181
10993
 
9182
10994
  // 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";
10995
+ import { createHash as createHash4 } from "node:crypto";
10996
+ import os9 from "node:os";
10997
+ import path23 from "node:path";
9186
10998
  var HISTORY_ENV = "AGENT_LOOKOUT_HISTORY";
9187
10999
  var HISTORY_DIR_ENV = "AGENT_LOOKOUT_HISTORY_DIR";
9188
- function readHistorySetup(env, homeDir = os8.homedir()) {
11000
+ function readHistorySetup(env, homeDir = os9.homedir()) {
9189
11001
  if (env[HISTORY_ENV]?.trim().toLowerCase() === "off") return { on: false };
9190
11002
  const override = env[HISTORY_DIR_ENV]?.trim() || void 0;
9191
- const dir = path18.resolve(override ?? path18.join(homeDir, ".agent-lookout", "history"));
11003
+ const dir = path23.resolve(override ?? path23.join(homeDir, ".agent-lookout", "history"));
9192
11004
  return {
9193
11005
  on: true,
9194
11006
  dir,
@@ -9196,14 +11008,58 @@ function readHistorySetup(env, homeDir = os8.homedir()) {
9196
11008
  sources: sourcesFingerprint(env, homeDir)
9197
11009
  };
9198
11010
  }
9199
- function sourcesFingerprint(env, homeDir = os8.homedir()) {
11011
+ function sourcesFingerprint(env, homeDir = os9.homedir()) {
9200
11012
  const set = (name) => env[name]?.trim() || void 0;
9201
11013
  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"))
11014
+ path23.resolve(set(CLAUDE_HOME_ENV) ?? path23.join(homeDir, ".claude")),
11015
+ path23.resolve(set(CODEX_HOME_ENV) ?? set(CODEX_OWN_HOME_ENV) ?? path23.join(homeDir, ".codex")),
11016
+ path23.resolve(set(STATUS_DIR_ENV) ?? path23.join(homeDir, ".agent-lookout", "sessions")),
11017
+ path23.resolve(set(ANTIGRAVITY_HOME_ENV) ?? path23.join(homeDir, ...DEFAULT_HOME))
9205
11018
  ];
9206
- return createHash3("sha256").update(JSON.stringify(folders)).digest("hex").slice(0, 32);
11019
+ return createHash4("sha256").update(JSON.stringify(folders)).digest("hex").slice(0, 32);
11020
+ }
11021
+
11022
+ // src/collector/messages/lastMessageRoute.ts
11023
+ var answer = (body) => ({ status: 200, body });
11024
+ var failed3 = (status, error, headers) => ({
11025
+ status,
11026
+ body: { error },
11027
+ ...headers && { headers }
11028
+ });
11029
+ function idIn(query) {
11030
+ const pairs = [...query];
11031
+ if (pairs.length !== 1) return null;
11032
+ const [name, value] = pairs[0] ?? ["", ""];
11033
+ if (name !== "id" || value === "" || value.length > MAX_SESSION_ID_LENGTH) return null;
11034
+ return value;
11035
+ }
11036
+ function createLastMessageRoute(options) {
11037
+ const { poller, adapters } = options;
11038
+ const off = lastMessageOff(options.env);
11039
+ return async (req, url) => {
11040
+ const site = req.headers["sec-fetch-site"];
11041
+ if (site !== void 0 && site !== "same-origin") {
11042
+ return failed3(403, "This address only answers the dashboard page served from this machine.");
11043
+ }
11044
+ const id = idIn(url.searchParams);
11045
+ if (id === null) {
11046
+ return failed3(400, "Name one session as id, and nothing else.");
11047
+ }
11048
+ if (off) return answer({ message: null, reason: "off", setting: LAST_MESSAGE_ENV });
11049
+ const session = poller.getSnapshot().sessions.find((listed) => listed.id === id);
11050
+ const notListed = () => failed3(404, "No session with that id is listed.");
11051
+ if (session === void 0) return notListed();
11052
+ const adapter = adapters.find((candidate) => candidate.id === session.source);
11053
+ if (adapter?.lastMessage === void 0) return answer({ message: null, reason: "not-read" });
11054
+ const read = await adapter.lastMessage(session.id);
11055
+ if (read === null) return notListed();
11056
+ if (read === "busy") {
11057
+ return failed3(429, "Too many last messages were asked for at once. Ask again in a second.", {
11058
+ "Retry-After": "1"
11059
+ });
11060
+ }
11061
+ return answer(read);
11062
+ };
9207
11063
  }
9208
11064
 
9209
11065
  // src/collector/notifications/heldWait.ts
@@ -9244,9 +11100,9 @@ function createServerNotifications(options) {
9244
11100
  const held = /* @__PURE__ */ new Map();
9245
11101
  const reminders = createReminderWatch();
9246
11102
  const quiet = createQuietHold();
9247
- function show(notice) {
11103
+ function show(notice, about) {
9248
11104
  try {
9249
- notifier.show(notice);
11105
+ notifier.show(notice, about);
9250
11106
  } catch {
9251
11107
  }
9252
11108
  }
@@ -9343,82 +11199,506 @@ function createServerNotifications(options) {
9343
11199
  }
9344
11200
  continue;
9345
11201
  }
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));
11202
+ if (entry.kind === "change") {
11203
+ const { change } = entry;
11204
+ show(
11205
+ changeNotice(change),
11206
+ change.event === "needs-you" ? { sessionId: change.session.id } : void 0
11207
+ );
11208
+ } else if (entry.kind === "reminder") {
11209
+ const { session, begunAt } = entry.reminder;
11210
+ const waitedMs = at - begunAt;
11211
+ show(reminderNotice(session, waitedMs), { sessionId: session.id, waitedMs });
9349
11212
  } else show(summaryNotice(entry.summary.items));
9350
11213
  }
9351
11214
  }
9352
11215
  };
9353
11216
  }
9354
11217
 
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) => {
11218
+ // src/collector/processes/osascript.ts
11219
+ import { execFile as execFile3 } from "node:child_process";
11220
+ var OSASCRIPT = "/usr/bin/osascript";
11221
+ var MAX_OUTPUT_BYTES4 = 64 * 1024;
11222
+ var NOT_RUN2 = { ok: false, stderr: "", timedOut: false };
11223
+ function runOsascript(args, options = {}) {
11224
+ return new Promise((resolve) => {
11225
+ try {
11226
+ const child = execFile3(
11227
+ OSASCRIPT,
11228
+ [...args],
11229
+ {
11230
+ env: childEnvironment(process.env),
11231
+ timeout: options.timeoutMs ?? TERMINAL_JUMP_TIMEOUT_MS,
11232
+ maxBuffer: MAX_OUTPUT_BYTES4,
11233
+ encoding: "utf8",
11234
+ windowsHide: true
11235
+ },
11236
+ (error, stdout, stderr) => {
11237
+ if (!error) {
11238
+ resolve({ ok: true, stdout: String(stdout ?? "") });
11239
+ return;
11240
+ }
11241
+ const timedOut = error.killed === true;
11242
+ resolve({ ok: false, stderr: String(stderr ?? "").trim(), timedOut });
11243
+ }
11244
+ );
11245
+ child.stdin?.end();
11246
+ } catch {
11247
+ resolve(NOT_RUN2);
11248
+ }
11249
+ });
11250
+ }
11251
+
11252
+ // src/collector/notifications/systemNotifier.ts
11253
+ var OSASCRIPT_TIMEOUT_MS = 5e3;
11254
+ var SCRIPT = [
11255
+ "on run argv",
11256
+ "display notification (item 2 of argv) with title (item 1 of argv)",
11257
+ "end run"
11258
+ ];
11259
+ function osascriptArgs(notice) {
11260
+ return [
11261
+ ...SCRIPT.flatMap((line) => ["-e", line]),
11262
+ "--",
11263
+ oneLine(notice.title),
11264
+ oneLine(notice.body, MAX_NOTICE_TEXT_LENGTH)
11265
+ ];
11266
+ }
11267
+ function systemNotificationsShownOn(platform) {
11268
+ return platform === "darwin";
11269
+ }
11270
+ var runForNotification = (args) => runOsascript(args, { timeoutMs: OSASCRIPT_TIMEOUT_MS });
11271
+ function createSystemNotifier(options = {}) {
11272
+ const platform = options.platform ?? process.platform;
11273
+ const run = options.run ?? runForNotification;
11274
+ return {
11275
+ show(notice) {
11276
+ if (!systemNotificationsShownOn(platform)) return;
11277
+ try {
11278
+ run(osascriptArgs(notice)).catch(() => {
11279
+ });
11280
+ } catch {
11281
+ }
11282
+ }
11283
+ };
11284
+ }
11285
+
11286
+ // src/collector/outbound/httpPost.ts
11287
+ var POST_TIMEOUT_MS = 1e4;
11288
+ function jsonHeaders(body, version, extra = {}) {
11289
+ return {
11290
+ "Content-Type": "application/json",
11291
+ "Content-Length": String(Buffer.byteLength(body)),
11292
+ "User-Agent": `Agent Lookout/${version}`,
11293
+ ...extra
11294
+ };
11295
+ }
11296
+ function postOptions(body, version, extra = {}) {
11297
+ return {
11298
+ method: "POST",
11299
+ headers: jsonHeaders(body, version, extra),
11300
+ // A connection of its own, closed after the answer.
11301
+ agent: false,
11302
+ rejectUnauthorized: true
11303
+ };
11304
+ }
11305
+ function createJsonPoster(url, options) {
11306
+ const timeoutMs = options.timeoutMs ?? POST_TIMEOUT_MS;
11307
+ const extra = options.headers ?? {};
11308
+ let client = null;
11309
+ const load = () => {
11310
+ client ??= url.protocol === "https:" ? import("node:https").then(({ request: request2 }) => request2) : import("node:http").then(({ request: request2 }) => request2);
11311
+ return client;
11312
+ };
11313
+ return {
11314
+ async post(body) {
11315
+ let request2;
11316
+ try {
11317
+ request2 = await load();
11318
+ } catch {
11319
+ return { kind: "unsent" };
11320
+ }
11321
+ return new Promise((resolve) => {
11322
+ let finished = false;
11323
+ const finish = (result) => {
11324
+ if (finished) return;
11325
+ finished = true;
11326
+ resolve(result);
11327
+ };
11328
+ let req;
11329
+ try {
11330
+ req = request2(url, postOptions(body, options.version, extra));
11331
+ } catch {
11332
+ finish({ kind: "unsent" });
11333
+ return;
11334
+ }
11335
+ const timer = setTimeout(() => {
11336
+ finish({ kind: "timeout" });
11337
+ req.destroy();
11338
+ }, timeoutMs);
11339
+ timer.unref();
11340
+ req.on("response", (res) => {
11341
+ finish({ kind: "status", status: res.statusCode ?? 0 });
11342
+ res.on("close", () => clearTimeout(timer));
11343
+ res.resume();
11344
+ });
11345
+ req.on("error", (error) => {
11346
+ clearTimeout(timer);
11347
+ const { code } = error ?? {};
11348
+ finish({ kind: "error", code: typeof code === "string" ? code : null });
11349
+ });
11350
+ req.end(body);
11351
+ });
11352
+ }
11353
+ };
11354
+ }
11355
+ function connectionReason(code, words) {
11356
+ if (code === null) return words.notSent;
11357
+ if (code === "ECONNREFUSED") return `nothing answered at ${words.where}`;
11358
+ if (code === "ENOTFOUND" || code === "EAI_AGAIN") return `${words.whose} host could not be found`;
11359
+ if (code === "ETIMEDOUT") return `${words.where} did not answer in time`;
11360
+ if (/CERT|SELF_SIGNED|UNABLE_TO|^ERR_TLS|^ERR_SSL|^EPROTO$/.test(code)) {
11361
+ return `the secure connection to ${words.where} failed`;
11362
+ }
11363
+ return `the connection to ${words.where} failed`;
11364
+ }
11365
+ function outcomeOfPost(result, words) {
11366
+ switch (result.kind) {
11367
+ case "unsent":
11368
+ return { sent: false, reason: words.notSent };
11369
+ case "timeout":
11370
+ return { sent: false, reason: `${words.where} did not answer in time` };
11371
+ case "error":
11372
+ return { sent: false, reason: connectionReason(result.code, words) };
11373
+ case "status": {
11374
+ const { status } = result;
11375
+ if (status >= 200 && status < 300) return { sent: true };
11376
+ if (status >= 300 && status < 400) {
11377
+ return {
11378
+ sent: false,
11379
+ reason: `${words.where} answered with a redirect, which is not followed`
11380
+ };
11381
+ }
11382
+ if (status >= 400 && status < 500) return { sent: false, reason: words.refused(status) };
11383
+ if (status >= 500 && status < 600) return { sent: false, reason: words.troubled(status) };
11384
+ return { sent: false, reason: `${words.whose} answer could not be understood` };
11385
+ }
11386
+ }
11387
+ }
11388
+
11389
+ // src/collector/ntfy/ntfyMessage.ts
11390
+ var TAG = {
11391
+ "needs-you": "hourglass",
11392
+ reminder: "hourglass",
11393
+ test: "hourglass",
11394
+ finished: "white_check_mark",
11395
+ failed: "x",
11396
+ ended: "stop_sign",
11397
+ "quiet-summary": null
11398
+ };
11399
+ function ntfyBody(topic, message) {
11400
+ const tag = TAG[message.kind];
11401
+ return {
11402
+ topic,
11403
+ title: message.title,
11404
+ message: message.message,
11405
+ // A test sounds as a wait would, so it shows how one will arrive.
11406
+ priority: isWaitKind(message.kind) ? 4 : 3,
11407
+ ...tag !== null && { tags: [tag] }
11408
+ };
11409
+ }
11410
+
11411
+ // src/collector/ntfy/ntfySender.ts
11412
+ var NTFY_WORDS = {
11413
+ where: "the ntfy server",
11414
+ whose: "the ntfy server's",
11415
+ notSent: "the push could not be sent",
11416
+ refused(status) {
11417
+ if (status === 401 || status === 403) {
11418
+ return `the ntfy server refused the access token or the topic (status ${status})`;
11419
+ }
11420
+ if (status === 413) return `the ntfy server refused the push as too large (status ${status})`;
11421
+ if (status === 429) return `the ntfy server's rate limit was reached (status ${status})`;
11422
+ return `the ntfy server refused the push (status ${status})`;
11423
+ },
11424
+ troubled: (status) => `the ntfy server had a problem (status ${status})`
11425
+ };
11426
+ function ntfyHeaders(settings) {
11427
+ return settings.token === null ? {} : { Authorization: `Bearer ${settings.token}` };
11428
+ }
11429
+ function createNtfySender(settings, options) {
11430
+ const poster = createJsonPoster(settings.server, { ...options, headers: ntfyHeaders(settings) });
11431
+ return {
11432
+ async send(message) {
11433
+ let body;
11434
+ try {
11435
+ body = JSON.stringify(ntfyBody(settings.topic, message));
11436
+ } catch {
11437
+ return { sent: false, reason: NTFY_WORDS.notSent };
11438
+ }
11439
+ return outcomeOfPost(await poster.post(body), NTFY_WORDS);
11440
+ }
11441
+ };
11442
+ }
11443
+
11444
+ // src/collector/ntfy/ntfySettings.ts
11445
+ var NTFY_URL_ENV = "AGENT_LOOKOUT_NTFY_URL";
11446
+ var NTFY_TOKEN_ENV = "AGENT_LOOKOUT_NTFY_TOKEN";
11447
+ var NTFY_EVENTS_ENV = "AGENT_LOOKOUT_NTFY_EVENTS";
11448
+ var NTFY_AFTER_ENV = "AGENT_LOOKOUT_NTFY_AFTER";
11449
+ var NTFY_ASKING_ENV = "AGENT_LOOKOUT_NTFY_ASKING";
11450
+ var MAX_URL_LENGTH = 2048;
11451
+ var TOPIC = /^[-_A-Za-z0-9]{1,64}$/;
11452
+ var PATH_PART = /^[-_.~A-Za-z0-9]+$/;
11453
+ var TOKEN = /^[-A-Za-z0-9._~+/=]{1,128}$/;
11454
+ var URL_UNREADABLE2 = `${NTFY_URL_ENV} could not be read. Copy the topic's whole address, beginning https://.`;
11455
+ function readTopicUrl(value) {
11456
+ let url;
11457
+ try {
11458
+ if (value.length > MAX_URL_LENGTH) throw new Error();
11459
+ url = new URL(value);
11460
+ } catch {
11461
+ throw new SettingProblem(URL_UNREADABLE2);
11462
+ }
11463
+ const local = LOCAL_HOSTS.has(url.hostname);
11464
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && local)) {
11465
+ throw new SettingProblem(
11466
+ `${NTFY_URL_ENV} must begin with https://, or with http:// for a server on this computer, 127.0.0.1 or localhost.`
11467
+ );
11468
+ }
11469
+ if (url.hostname === "") throw new SettingProblem(URL_UNREADABLE2);
11470
+ if (!isWebhookHost(url.hostname)) {
11471
+ throw new SettingProblem(
11472
+ `${NTFY_URL_ENV} must name its host in letters, digits, dots and dashes, such as ntfy.sh.`
11473
+ );
11474
+ }
11475
+ if (url.username !== "" || url.password !== "") {
11476
+ throw new SettingProblem(
11477
+ `${NTFY_URL_ENV} must not hold a user name or a password. Put an access token in ${NTFY_TOKEN_ENV}.`
11478
+ );
11479
+ }
11480
+ if (url.search !== "" || url.hash !== "" || value.includes("?") || value.includes("#")) {
11481
+ throw new SettingProblem(
11482
+ `${NTFY_URL_ENV} must end with the topic, with no ? or # after it. Put an access token in ${NTFY_TOKEN_ENV}.`
11483
+ );
11484
+ }
11485
+ const parts = url.pathname.split("/").filter((part) => part !== "");
11486
+ const topic = parts.pop();
11487
+ if (topic === void 0 || !TOPIC.test(topic) || !parts.every((part) => PATH_PART.test(part))) {
11488
+ throw new SettingProblem(
11489
+ `${NTFY_URL_ENV} must end with the topic, up to 64 letters, digits, dashes and underscores, as in https://ntfy.sh/your-topic.`
11490
+ );
11491
+ }
11492
+ const server = new URL(url.origin);
11493
+ server.pathname = `/${parts.map((part) => `${part}/`).join("")}`;
11494
+ return { server, topic };
11495
+ }
11496
+ function readToken(value) {
11497
+ if (value === void 0) return null;
11498
+ if (!TOKEN.test(value)) {
11499
+ throw new SettingProblem(
11500
+ `${NTFY_TOKEN_ENV} must be an access token of letters, digits and . _ ~ + / = -, such as one that begins tk_.`
11501
+ );
11502
+ }
11503
+ return value;
11504
+ }
11505
+ function readNtfySetup(env) {
11506
+ const url = valueOf(env, NTFY_URL_ENV);
11507
+ if (url === void 0) return { on: false, problem: null };
11508
+ try {
11509
+ return {
11510
+ on: true,
11511
+ settings: {
11512
+ ...readTopicUrl(url),
11513
+ token: readToken(valueOf(env, NTFY_TOKEN_ENV)),
11514
+ events: readEvents(NTFY_EVENTS_ENV, valueOf(env, NTFY_EVENTS_ENV)),
11515
+ afterMs: readAfter(NTFY_AFTER_ENV, valueOf(env, NTFY_AFTER_ENV)),
11516
+ asking: readAsking(NTFY_ASKING_ENV, valueOf(env, NTFY_ASKING_ENV))
11517
+ }
11518
+ };
11519
+ } catch (error) {
11520
+ if (error instanceof SettingProblem) return { on: false, problem: error.message };
11521
+ return { on: false, problem: URL_UNREADABLE2 };
11522
+ }
11523
+ }
11524
+ function ntfyProblemLine(problem) {
11525
+ return `ntfy pushes are off: ${problem}`;
11526
+ }
11527
+
11528
+ // src/collector/outbound/testSendRoute.ts
11529
+ var MAX_TEST_BODY_BYTES = 64;
11530
+ var NAMED = {
11531
+ ntfy: { name: "ntfy", setting: "AGENT_LOOKOUT_NTFY_URL" },
11532
+ pushover: {
11533
+ name: "Pushover",
11534
+ setting: "AGENT_LOOKOUT_PUSHOVER_TOKEN and AGENT_LOOKOUT_PUSHOVER_USER"
11535
+ }
11536
+ };
11537
+ function channelIn(body) {
11538
+ let value;
11539
+ try {
11540
+ value = JSON.parse(body);
11541
+ } catch {
11542
+ return null;
11543
+ }
11544
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
11545
+ const keys = Object.keys(value);
11546
+ if (keys.length !== 1 || keys[0] !== "channel") return null;
11547
+ const { channel } = value;
11548
+ return PHONE_CHANNELS.find((known) => known === channel) ?? null;
11549
+ }
11550
+ function asSentence(reason) {
11551
+ return `${reason.charAt(0).toUpperCase()}${reason.slice(1)}.`;
11552
+ }
11553
+ var failed4 = (status, reason, error, more = {}) => ({
11554
+ status,
11555
+ body: {
11556
+ error,
11557
+ reason,
11558
+ ...more.limitedUntil !== void 0 && { limitedUntil: more.limitedUntil }
11559
+ },
11560
+ ...more.retryAfter !== void 0 && {
11561
+ headers: { "Retry-After": String(more.retryAfter) }
11562
+ }
11563
+ });
11564
+ function createTestSendRoute(options) {
11565
+ const { channels, limiter } = options;
11566
+ const now = options.now ?? Date.now;
11567
+ return async function answerTest(req) {
11568
+ const refused = actionRefusalFor(req, PHONE_TEST_ACTION, MAX_TEST_BODY_BYTES);
11569
+ if (refused) {
11570
+ req.resume();
11571
+ return refused;
11572
+ }
11573
+ const body = await readRequestBody(req, MAX_TEST_BODY_BYTES);
11574
+ if (!body.ok) {
11575
+ req.resume();
11576
+ 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.");
11577
+ }
11578
+ const channel = channelIn(body.text);
11579
+ if (channel === null) {
11580
+ return refusal(400, 'The body must be {"channel": "ntfy"} or {"channel": "pushover"}.');
11581
+ }
11582
+ const { name, setting } = NAMED[channel];
11583
+ const tester = channels[channel];
11584
+ if (tester === null) {
11585
+ return failed4(
11586
+ 409,
11587
+ "off",
11588
+ `${name} is not set up. Set ${setting} and start Agent Lookout again.`
11589
+ );
11590
+ }
11591
+ if (!limiter.begin()) {
11592
+ return failed4(429, "too-soon", "One test is sent a second. Try again in a moment.", {
11593
+ retryAfter: 1
11594
+ });
11595
+ }
9362
11596
  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;
11597
+ const result = await tester.test();
11598
+ if (!result.tried) {
11599
+ return failed4(
11600
+ 429,
11601
+ "limited",
11602
+ `${SENDS_PER_HOUR} ${name} pushes were tried in the last hour, the most it tries.`,
11603
+ {
11604
+ limitedUntil: result.limitedUntil,
11605
+ retryAfter: Math.max(1, Math.ceil((result.limitedUntil - now()) / 1e3))
9376
11606
  }
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);
11607
+ );
11608
+ }
11609
+ if (!result.outcome.sent) {
11610
+ return failed4(502, "not-sent", asSentence(result.outcome.reason));
11611
+ }
11612
+ return {
11613
+ status: 200,
11614
+ body: { ok: true, channel, sentAt: result.at }
11615
+ };
11616
+ } finally {
11617
+ limiter.end();
9384
11618
  }
9385
- });
11619
+ };
9386
11620
  }
9387
11621
 
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";
11622
+ // src/collector/pushover/pushoverMessage.ts
11623
+ function pushoverBody(token, user, message) {
11624
+ return {
11625
+ token,
11626
+ user,
11627
+ title: message.title,
11628
+ message: message.message,
11629
+ // A test sounds as a wait would, so it shows how one will arrive.
11630
+ priority: isWaitKind(message.kind) ? 0 : -1
11631
+ };
9405
11632
  }
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;
11633
+
11634
+ // src/collector/pushover/pushoverSender.ts
11635
+ var PUSHOVER_ENDPOINT = "https://api.pushover.net/1/messages.json";
11636
+ var PUSHOVER_WORDS = {
11637
+ where: "Pushover",
11638
+ whose: "Pushover's",
11639
+ notSent: "the push could not be sent",
11640
+ refused(status) {
11641
+ if (status === 400) {
11642
+ return `Pushover refused the push: check the application's token and the user key (status ${status})`;
11643
+ }
11644
+ if (status === 429)
11645
+ return `Pushover's monthly limit for your account was reached (status ${status})`;
11646
+ return `Pushover refused the push (status ${status})`;
11647
+ },
11648
+ troubled: (status) => `Pushover had a problem (status ${status})`
11649
+ };
11650
+ function createPushoverSender(settings, options) {
11651
+ const poster = createJsonPoster(options.endpoint ?? new URL(PUSHOVER_ENDPOINT), options);
9410
11652
  return {
9411
- show(notice) {
9412
- if (!systemNotificationsShownOn(platform)) return;
11653
+ async send(message) {
11654
+ let body;
9413
11655
  try {
9414
- run(osascriptArgs(notice)).catch(() => {
9415
- });
11656
+ body = JSON.stringify(pushoverBody(settings.token, settings.user, message));
9416
11657
  } catch {
11658
+ return { sent: false, reason: PUSHOVER_WORDS.notSent };
9417
11659
  }
11660
+ return outcomeOfPost(await poster.post(body), PUSHOVER_WORDS);
9418
11661
  }
9419
11662
  };
9420
11663
  }
9421
11664
 
11665
+ // src/collector/pushover/pushoverSettings.ts
11666
+ var PUSHOVER_TOKEN_ENV = "AGENT_LOOKOUT_PUSHOVER_TOKEN";
11667
+ var PUSHOVER_USER_ENV = "AGENT_LOOKOUT_PUSHOVER_USER";
11668
+ var PUSHOVER_EVENTS_ENV = "AGENT_LOOKOUT_PUSHOVER_EVENTS";
11669
+ var PUSHOVER_AFTER_ENV = "AGENT_LOOKOUT_PUSHOVER_AFTER";
11670
+ var PUSHOVER_ASKING_ENV = "AGENT_LOOKOUT_PUSHOVER_ASKING";
11671
+ var KEY = /^[A-Za-z0-9]{30}$/;
11672
+ function readKey(name, value, what) {
11673
+ if (!KEY.test(value)) throw new SettingProblem(`${name} must be ${what}: 30 letters and digits.`);
11674
+ return value;
11675
+ }
11676
+ function readPushoverSetup(env) {
11677
+ const token = valueOf(env, PUSHOVER_TOKEN_ENV);
11678
+ const user = valueOf(env, PUSHOVER_USER_ENV);
11679
+ if (token === void 0 && user === void 0) return { on: false, problem: null };
11680
+ if (token === void 0) return { on: false, problem: `${PUSHOVER_TOKEN_ENV} is not set.` };
11681
+ if (user === void 0) return { on: false, problem: `${PUSHOVER_USER_ENV} is not set.` };
11682
+ try {
11683
+ return {
11684
+ on: true,
11685
+ settings: {
11686
+ token: readKey(PUSHOVER_TOKEN_ENV, token, "the API token of your Pushover application"),
11687
+ user: readKey(PUSHOVER_USER_ENV, user, "your Pushover user key, or a group key"),
11688
+ events: readEvents(PUSHOVER_EVENTS_ENV, valueOf(env, PUSHOVER_EVENTS_ENV)),
11689
+ afterMs: readAfter(PUSHOVER_AFTER_ENV, valueOf(env, PUSHOVER_AFTER_ENV)),
11690
+ asking: readAsking(PUSHOVER_ASKING_ENV, valueOf(env, PUSHOVER_ASKING_ENV))
11691
+ }
11692
+ };
11693
+ } catch (error) {
11694
+ if (error instanceof SettingProblem) return { on: false, problem: error.message };
11695
+ return { on: false, problem: `${PUSHOVER_TOKEN_ENV} could not be read.` };
11696
+ }
11697
+ }
11698
+ function pushoverProblemLine(problem) {
11699
+ return `Pushover pushes are off: ${problem}`;
11700
+ }
11701
+
9422
11702
  // src/collector/remotes/remoteReader.ts
9423
11703
  import { request } from "node:http";
9424
11704
  var REMOTE_ANSWER_TIMEOUT_MS = 6e3;
@@ -9427,12 +11707,12 @@ function askRemote(port, route, timeoutMs = REMOTE_ANSWER_TIMEOUT_MS) {
9427
11707
  return new Promise((resolve) => {
9428
11708
  let connected = false;
9429
11709
  let settled = false;
9430
- const finish = (answer) => {
11710
+ const finish = (answer2) => {
9431
11711
  if (settled) return;
9432
11712
  settled = true;
9433
11713
  clearTimeout(timer);
9434
11714
  req.destroy();
9435
- resolve(answer);
11715
+ resolve(answer2);
9436
11716
  };
9437
11717
  const req = request({
9438
11718
  host: "127.0.0.1",
@@ -9495,7 +11775,7 @@ var MAX_REMOTE_SOURCES = 20;
9495
11775
  var MAX_ID_LENGTH = 512;
9496
11776
  var MAX_PATH_LENGTH = 4096;
9497
11777
  var MAX_WORD_LENGTH2 = 200;
9498
- function isRecord12(value) {
11778
+ function isRecord13(value) {
9499
11779
  return typeof value === "object" && value !== null && !Array.isArray(value);
9500
11780
  }
9501
11781
  function oneOf2(allowed, value) {
@@ -9509,7 +11789,7 @@ function exact(value, max) {
9509
11789
  var COMMIT_ID2 = /^[0-9a-f]{7,64}$/;
9510
11790
  var REPOSITORY_ID = /^[0-9a-f]{16,64}$/;
9511
11791
  function readGit(value, machine) {
9512
- if (!isRecord12(value)) return void 0;
11792
+ if (!isRecord13(value)) return void 0;
9513
11793
  let head;
9514
11794
  if (value.commit !== void 0) {
9515
11795
  if (value.branch !== void 0 || typeof value.commit !== "string") return void 0;
@@ -9521,7 +11801,7 @@ function readGit(value, machine) {
9521
11801
  head = { branch };
9522
11802
  }
9523
11803
  const repository = value.repository;
9524
- if (isRecord12(repository) && typeof repository.id === "string") {
11804
+ if (isRecord13(repository) && typeof repository.id === "string") {
9525
11805
  const name = sessionName(typeof repository.name === "string" ? repository.name : "");
9526
11806
  if (name !== void 0 && REPOSITORY_ID.test(repository.id)) {
9527
11807
  head.repository = { id: repositoryId(`${machine}
@@ -9531,7 +11811,7 @@ ${repository.id}`), name };
9531
11811
  return head;
9532
11812
  }
9533
11813
  function readSession(value, machine, sources, now, keepWaitingText) {
9534
- if (!isRecord12(value)) return null;
11814
+ if (!isRecord13(value)) return null;
9535
11815
  const id = exact(value.id, MAX_ID_LENGTH);
9536
11816
  const source = exact(value.source, MAX_WORD_LENGTH2);
9537
11817
  if (id === null || source === null) return null;
@@ -9571,7 +11851,7 @@ function readSession(value, machine, sources, now, keepWaitingText) {
9571
11851
  return session;
9572
11852
  }
9573
11853
  function readCell(value) {
9574
- if (!isRecord12(value)) return null;
11854
+ if (!isRecord13(value)) return null;
9575
11855
  if (value.level === "yes") return { level: "yes" };
9576
11856
  const level = oneOf2(["no", "partly"], value.level);
9577
11857
  const reason = cut(value.reason, MAX_WORD_LENGTH2);
@@ -9579,7 +11859,7 @@ function readCell(value) {
9579
11859
  }
9580
11860
  var ACTS_HERE = ["jump", "stop", "answer"];
9581
11861
  function readCapabilities(value, machine) {
9582
- if (!isRecord12(value)) return null;
11862
+ if (!isRecord13(value)) return null;
9583
11863
  const read = {};
9584
11864
  for (const capability of CAPABILITIES) {
9585
11865
  if (ACTS_HERE.includes(capability)) continue;
@@ -9595,7 +11875,7 @@ function readCapabilities(value, machine) {
9595
11875
  };
9596
11876
  }
9597
11877
  function readRemoteSnapshot(value, machine, now, options = {}) {
9598
- if (!isRecord12(value) || !Array.isArray(value.sessions) || !Array.isArray(value.sources)) {
11878
+ if (!isRecord13(value) || !Array.isArray(value.sessions) || !Array.isArray(value.sources)) {
9599
11879
  return null;
9600
11880
  }
9601
11881
  const named = /* @__PURE__ */ new Map();
@@ -9603,7 +11883,7 @@ function readRemoteSnapshot(value, machine, now, options = {}) {
9603
11883
  const agents = [];
9604
11884
  for (const item of value.sources) {
9605
11885
  if (sources.length >= MAX_REMOTE_SOURCES) break;
9606
- if (!isRecord12(item)) continue;
11886
+ if (!isRecord13(item)) continue;
9607
11887
  const id = exact(item.id, MAX_WORD_LENGTH2);
9608
11888
  if (id === null || isRemoteSource(id) || item.machine !== void 0) continue;
9609
11889
  const label2 = cut(item.label, MAX_WORD_LENGTH2) ?? id;
@@ -9628,7 +11908,7 @@ function readRemoteSnapshot(value, machine, now, options = {}) {
9628
11908
  }
9629
11909
 
9630
11910
  // src/collector/remotes/sshProgram.ts
9631
- import path19 from "node:path";
11911
+ import path24 from "node:path";
9632
11912
  var SSH_BIN_ENV = "AGENT_LOOKOUT_SSH_BIN";
9633
11913
  var SSH_LOCATIONS = ["/usr/bin/ssh", "/opt/homebrew/bin/ssh", "/usr/local/bin/ssh"];
9634
11914
  var SERVER_ALIVE_SECONDS = 15;
@@ -9650,7 +11930,7 @@ async function findSsh(env, isExecutable, platform = process.platform) {
9650
11930
  }
9651
11931
  return {
9652
11932
  found: false,
9653
- looked: `The ssh command was not found on PATH or in ${SSH_LOCATIONS.map((at) => path19.dirname(at)).join(", ")}`
11933
+ looked: `The ssh command was not found on PATH or in ${SSH_LOCATIONS.map((at) => path24.dirname(at)).join(", ")}`
9654
11934
  };
9655
11935
  }
9656
11936
  function sshArguments(forward) {
@@ -9674,7 +11954,7 @@ function sshArguments(forward) {
9674
11954
  }
9675
11955
 
9676
11956
  // src/collector/remotes/remoteAdapter.ts
9677
- var BASIS3 = "api";
11957
+ var BASIS4 = "api";
9678
11958
  var NOT_CONNECTED = "unavailable";
9679
11959
  var ANSWER_WAIT_MS = 500;
9680
11960
  var STILL_CONNECTING_MS = 1e4;
@@ -9740,7 +12020,7 @@ function createRemoteAdapter(options) {
9740
12020
  if (found.advice) health.advice = found.advice;
9741
12021
  if (agents.length > 0) health.agents = agents;
9742
12022
  const sessions = state === "ok" && snapshot ? snapshot.sessions : [];
9743
- return state === "ok" ? { health, sessions, basis: BASIS3 } : { health, sessions };
12023
+ return state === "ok" ? { health, sessions, basis: BASIS4 } : { health, sessions };
9744
12024
  }
9745
12025
  function connecting(at) {
9746
12026
  if (problem === null) {
@@ -9992,7 +12272,11 @@ function freeLoopbackPort() {
9992
12272
  });
9993
12273
  });
9994
12274
  }
9995
- var spawnSsh = (binary, args, env) => spawn2(binary, [...args], { env, stdio: ["ignore", "ignore", "pipe"], windowsHide: true });
12275
+ var spawnSsh = (binary, args, env) => spawn2(binary, [...args], {
12276
+ env: childEnvironment(env),
12277
+ stdio: ["ignore", "ignore", "pipe"],
12278
+ windowsHide: true
12279
+ });
9996
12280
  var live = /* @__PURE__ */ new Set();
9997
12281
  var endedOnExit = false;
9998
12282
  function endOnExit(child) {
@@ -10245,15 +12529,15 @@ import {
10245
12529
  unlinkSync as unlinkSync2,
10246
12530
  writeSync as writeSync2
10247
12531
  } from "node:fs";
10248
- import os9 from "node:os";
10249
- import path20 from "node:path";
12532
+ import os10 from "node:os";
12533
+ import path25 from "node:path";
10250
12534
  var SETTINGS_FILE_ENV = "AGENT_LOOKOUT_SETTINGS_FILE";
10251
12535
  var SETTINGS_FILE_MODE = 384;
10252
12536
  var SETTINGS_FOLDER_MODE = 448;
10253
12537
  var MAX_SETTINGS_BYTES = 64 * 1024;
10254
- function readSettingsSetup(env, homeDir = os9.homedir()) {
12538
+ function readSettingsSetup(env, homeDir = os10.homedir()) {
10255
12539
  const named = env[SETTINGS_FILE_ENV]?.trim() || void 0;
10256
- const file = path20.resolve(named ?? path20.join(homeDir, ".agent-lookout", "settings.json"));
12540
+ const file = path25.resolve(named ?? path25.join(homeDir, ".agent-lookout", "settings.json"));
10257
12541
  return { file, shown: tildify(file, homeDir) };
10258
12542
  }
10259
12543
  function codeOf(error) {
@@ -10298,7 +12582,7 @@ function readSettingsStamp(setup) {
10298
12582
  }
10299
12583
  function readSettingsText(setup) {
10300
12584
  const { file, shown } = setup;
10301
- const folder = path20.dirname(file);
12585
+ const folder = path25.dirname(file);
10302
12586
  try {
10303
12587
  const folderKind = kindAt(folder);
10304
12588
  if (folderKind === null) return { kind: "missing" };
@@ -10357,7 +12641,7 @@ function readSettingsText(setup) {
10357
12641
  }
10358
12642
  function writeSettingsText(setup, text4) {
10359
12643
  const { file, shown } = setup;
10360
- const folder = path20.dirname(file);
12644
+ const folder = path25.dirname(file);
10361
12645
  const notSaved = {
10362
12646
  ok: false,
10363
12647
  problem: `${shown} could not be written, so the change was not saved.`
@@ -10378,9 +12662,9 @@ function writeSettingsText(setup, text4) {
10378
12662
  problem: `${shown} is a link or not an ordinary file, which Agent Lookout does not write through, so the change was not saved.`
10379
12663
  };
10380
12664
  }
10381
- temporary = path20.join(
12665
+ temporary = path25.join(
10382
12666
  folder,
10383
- `.${path20.basename(file)}.${process.pid}.${randomBytes3(6).toString("hex")}.tmp`
12667
+ `.${path25.basename(file)}.${process.pid}.${randomBytes3(6).toString("hex")}.tmp`
10384
12668
  );
10385
12669
  const fd = openWithoutFollowingNow(
10386
12670
  temporary,
@@ -10420,7 +12704,7 @@ var RULE_WORDS = {
10420
12704
  idle: "the idle rule",
10421
12705
  quietHours: "the quiet hours"
10422
12706
  };
10423
- function isRecord13(value) {
12707
+ function isRecord14(value) {
10424
12708
  return typeof value === "object" && value !== null && !Array.isArray(value);
10425
12709
  }
10426
12710
  function listOf(names) {
@@ -10458,7 +12742,7 @@ function rulesIn(read, shown) {
10458
12742
  } catch {
10459
12743
  value = null;
10460
12744
  }
10461
- if (!isRecord13(value)) {
12745
+ if (!isRecord14(value)) {
10462
12746
  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
12747
  return {
10464
12748
  rules: off,
@@ -10473,7 +12757,7 @@ function rulesIn(read, shown) {
10473
12757
  const { timeRules, permissionRules: heldRules, ...rest } = value;
10474
12758
  const permissions = readPermissionRules(heldRules);
10475
12759
  const { rules, unread } = readTimeRules(timeRules);
10476
- const otherRules = isRecord13(timeRules) ? Object.fromEntries(
12760
+ const otherRules = isRecord14(timeRules) ? Object.fromEntries(
10477
12761
  Object.entries(timeRules).filter(
10478
12762
  ([name]) => !TIME_RULE_NAMES.includes(name)
10479
12763
  )
@@ -10575,15 +12859,15 @@ import { randomBytes as randomBytes4 } from "node:crypto";
10575
12859
 
10576
12860
  // src/core/permission-rules/rulesChange.ts
10577
12861
  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) {
12862
+ function isRecord15(value) {
10579
12863
  return typeof value === "object" && value !== null && !Array.isArray(value);
10580
12864
  }
10581
12865
  function rulesChangeIn(value) {
10582
12866
  const refused = { ok: false, problem: CHANGE_SHAPE };
10583
- if (!isRecord14(value) || Object.keys(value).length !== 1) return refused;
12867
+ if (!isRecord15(value) || Object.keys(value).length !== 1) return refused;
10584
12868
  const [kind] = Object.keys(value);
10585
12869
  const body = value[kind];
10586
- if (!isRecord14(body)) return refused;
12870
+ if (!isRecord15(body)) return refused;
10587
12871
  switch (kind) {
10588
12872
  case "add": {
10589
12873
  const read = ruleWordsIn(body);
@@ -10659,7 +12943,7 @@ function applyRulesChange(rules, change, newId) {
10659
12943
 
10660
12944
  // src/collector/settings/permissionRulesRoute.ts
10661
12945
  var MAX_PERMISSION_RULES_BODY_BYTES = 1024;
10662
- var failed3 = (status, reason, error) => ({
12946
+ var failed5 = (status, reason, error) => ({
10663
12947
  status,
10664
12948
  body: { error, reason }
10665
12949
  });
@@ -10686,14 +12970,14 @@ function createPermissionRulesRoute(options) {
10686
12970
  try {
10687
12971
  value = JSON.parse(body.text);
10688
12972
  } catch {
10689
- return failed3(400, "invalid", "The body must be JSON: one change to the permission rules.");
12973
+ return failed5(400, "invalid", "The body must be JSON: one change to the permission rules.");
10690
12974
  }
10691
12975
  const asked = rulesChangeIn(value);
10692
- if (!asked.ok) return failed3(400, "invalid", asked.problem);
12976
+ if (!asked.ok) return failed5(400, "invalid", asked.problem);
10693
12977
  const made = settings.changePermissionRules(
10694
12978
  (rules) => applyRulesChange(rules, asked.change, newId)
10695
12979
  );
10696
- if (!made.ok) return failed3(STATUS_OF[made.reason], made.reason, made.problem);
12980
+ if (!made.ok) return failed5(STATUS_OF[made.reason], made.reason, made.problem);
10697
12981
  return {
10698
12982
  status: 200,
10699
12983
  body: { ok: true, permissionRules: made.rules }
@@ -10703,7 +12987,7 @@ function createPermissionRulesRoute(options) {
10703
12987
 
10704
12988
  // src/collector/settings/timeRulesRoute.ts
10705
12989
  var MAX_TIME_RULES_BODY_BYTES = 2048;
10706
- var failed4 = (status, reason, error) => ({
12990
+ var failed6 = (status, reason, error) => ({
10707
12991
  status,
10708
12992
  body: { error, reason }
10709
12993
  });
@@ -10727,12 +13011,12 @@ function createTimeRulesRoute(options) {
10727
13011
  try {
10728
13012
  value = JSON.parse(body.text);
10729
13013
  } catch {
10730
- return failed4(400, "invalid", "The body must be JSON: the three time rules.");
13014
+ return failed6(400, "invalid", "The body must be JSON: the three time rules.");
10731
13015
  }
10732
13016
  const asked = timeRulesIn(value);
10733
- if (!asked.ok) return failed4(400, "invalid", asked.problem);
13017
+ if (!asked.ok) return failed6(400, "invalid", asked.problem);
10734
13018
  const saved = settings.changeTimeRules(asked.rules);
10735
- if (!saved.ok) return failed4(500, "not-saved", saved.problem);
13019
+ if (!saved.ok) return failed6(500, "not-saved", saved.problem);
10736
13020
  return {
10737
13021
  status: 200,
10738
13022
  body: { ok: true, timeRules: asked.rules }
@@ -10755,8 +13039,8 @@ function parseProcessTable(stdout) {
10755
13039
  }
10756
13040
  var readProcessTableWithPs = async () => {
10757
13041
  if (process.platform !== "darwin") return /* @__PURE__ */ new Map();
10758
- const answer = await runPs(PS_TABLE_ARGS);
10759
- return answer.ok ? parseProcessTable(answer.stdout) : /* @__PURE__ */ new Map();
13042
+ const answer2 = await runPs(PS_TABLE_ARGS);
13043
+ return answer2.ok ? parseProcessTable(answer2.stdout) : /* @__PURE__ */ new Map();
10760
13044
  };
10761
13045
  function parentsIn(table) {
10762
13046
  const parents = /* @__PURE__ */ new Map();
@@ -10776,11 +13060,11 @@ function createOsascriptRunner(options) {
10776
13060
  }
10777
13061
 
10778
13062
  // src/collector/terminal/tabFinder.ts
10779
- import os10 from "node:os";
13063
+ import os11 from "node:os";
10780
13064
  var TAB_LOOK_SOONEST_MS = 5e3;
10781
13065
  function createTabFinder(options) {
10782
13066
  const off = terminalJumpOff(options.env);
10783
- const homeDir = options.homeDir ?? os10.homedir();
13067
+ const homeDir = options.homeDir ?? os11.homedir();
10784
13068
  const readProcesses = options.readProcesses ?? readProcessTableWithPs;
10785
13069
  const now = options.now ?? Date.now;
10786
13070
  const soonestMs = options.soonestMs ?? TAB_LOOK_SOONEST_MS;
@@ -10900,7 +13184,7 @@ function runTmuxBinary(binary, args, options) {
10900
13184
  binary,
10901
13185
  [...args],
10902
13186
  {
10903
- env: options.env,
13187
+ env: childEnvironment(options.env),
10904
13188
  timeout: options.timeoutMs ?? TMUX_TIMEOUT_MS,
10905
13189
  maxBuffer: MAX_OUTPUT_BYTES5,
10906
13190
  encoding: "utf8",
@@ -11014,96 +13298,24 @@ function ledgerHistoryStore(store, ledger) {
11014
13298
  }
11015
13299
 
11016
13300
  // 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
- }
13301
+ var WORDS = {
13302
+ where: "the address",
13303
+ whose: "the address's",
13304
+ notSent: "the post could not be sent",
13305
+ refused: (status) => `the address refused the post (status ${status})`,
13306
+ troubled: (status) => `the receiving service had a problem (status ${status})`
13307
+ };
11059
13308
  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
- };
13309
+ const poster = createJsonPoster(settings.url, options);
11067
13310
  return {
11068
13311
  async send(post) {
11069
- let request2;
13312
+ let body;
11070
13313
  try {
11071
- request2 = await load();
13314
+ body = JSON.stringify(post);
11072
13315
  } catch {
11073
- return { sent: false, reason: "the post could not be sent" };
13316
+ return { sent: false, reason: WORDS.notSent };
11074
13317
  }
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
- });
13318
+ return outcomeOfPost(await poster.post(body), WORDS);
11107
13319
  }
11108
13320
  };
11109
13321
  }
@@ -11113,15 +13325,15 @@ var WEBHOOK_URL_ENV = "AGENT_LOOKOUT_WEBHOOK_URL";
11113
13325
  var WEBHOOK_EVENTS_ENV = "AGENT_LOOKOUT_WEBHOOK_EVENTS";
11114
13326
  var WEBHOOK_AFTER_ENV = "AGENT_LOOKOUT_WEBHOOK_AFTER";
11115
13327
  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://.`;
13328
+ var MAX_URL_LENGTH2 = 2048;
13329
+ var URL_UNREADABLE3 = `${WEBHOOK_URL_ENV} could not be read. Copy the whole address, beginning https://.`;
11118
13330
  function readUrl(value) {
11119
13331
  let url;
11120
13332
  try {
11121
- if (value.length > MAX_URL_LENGTH) throw new Error();
13333
+ if (value.length > MAX_URL_LENGTH2) throw new Error();
11122
13334
  url = new URL(value);
11123
13335
  } catch {
11124
- throw new SettingProblem(URL_UNREADABLE2);
13336
+ throw new SettingProblem(URL_UNREADABLE3);
11125
13337
  }
11126
13338
  const local = LOCAL_HOSTS.has(url.hostname);
11127
13339
  if (url.protocol !== "https:" && !(url.protocol === "http:" && local)) {
@@ -11129,7 +13341,7 @@ function readUrl(value) {
11129
13341
  `${WEBHOOK_URL_ENV} must begin with https://, or with http:// for an address on this computer, 127.0.0.1 or localhost.`
11130
13342
  );
11131
13343
  }
11132
- if (url.hostname === "") throw new SettingProblem(URL_UNREADABLE2);
13344
+ if (url.hostname === "") throw new SettingProblem(URL_UNREADABLE3);
11133
13345
  if (!isWebhookHost(url.hostname)) {
11134
13346
  throw new SettingProblem(
11135
13347
  `${WEBHOOK_URL_ENV} must name its host in letters, digits, dots and dashes, such as hooks.slack.com.`
@@ -11155,7 +13367,7 @@ function readWebhookSetup(env) {
11155
13367
  };
11156
13368
  } catch (error) {
11157
13369
  if (error instanceof SettingProblem) return { on: false, problem: error.message };
11158
- return { on: false, problem: URL_UNREADABLE2 };
13370
+ return { on: false, problem: URL_UNREADABLE3 };
11159
13371
  }
11160
13372
  }
11161
13373
  function webhookProblemLine(problem) {
@@ -11252,6 +13464,26 @@ function createCollector(options) {
11252
13464
  now
11253
13465
  }) : null;
11254
13466
  const webhookOff = webhookOffStatus(webhookSetup.on ? null : webhookSetup.problem);
13467
+ const ntfySetup = readNtfySetup(env);
13468
+ if (!ntfySetup.on && ntfySetup.problem !== null) warn(ntfyProblemLine(ntfySetup.problem));
13469
+ const makeNtfySender = options.createNtfySender ?? ((settings2) => createNtfySender(settings2, { version: options.version }));
13470
+ const ntfy = ntfySetup.on ? createNtfyNotifications({
13471
+ settings: ntfySetup.settings,
13472
+ sender: makeNtfySender(ntfySetup.settings),
13473
+ now
13474
+ }) : null;
13475
+ const ntfyOff = ntfyOffStatus(ntfySetup.on ? null : ntfySetup.problem);
13476
+ const pushoverSetup = readPushoverSetup(env);
13477
+ if (!pushoverSetup.on && pushoverSetup.problem !== null) {
13478
+ warn(pushoverProblemLine(pushoverSetup.problem));
13479
+ }
13480
+ const makePushoverSender = options.createPushoverSender ?? ((settings2) => createPushoverSender(settings2, { version: options.version }));
13481
+ const pushover = pushoverSetup.on ? createPushoverNotifications({
13482
+ settings: pushoverSetup.settings,
13483
+ sender: makePushoverSender(pushoverSetup.settings),
13484
+ now
13485
+ }) : null;
13486
+ const pushoverOff = pushoverOffStatus(pushoverSetup.on ? null : pushoverSetup.problem);
11255
13487
  const remotes = createRemotes({
11256
13488
  env,
11257
13489
  version: options.version,
@@ -11272,6 +13504,7 @@ function createCollector(options) {
11272
13504
  platform
11273
13505
  }),
11274
13506
  createCodexAdapter({ env: options.env, now, pollIntervalMs: intervalMs }),
13507
+ createAntigravityAdapter({ env: options.env, now, pollIntervalMs: intervalMs, platform }),
11275
13508
  createStatusFileAdapter({ env: options.env, now, pollIntervalMs: intervalMs })
11276
13509
  ];
11277
13510
  const poller = createPoller({
@@ -11306,6 +13539,8 @@ function createCollector(options) {
11306
13539
  answering?.observe(snapshot);
11307
13540
  email?.handle(snapshot);
11308
13541
  webhook?.handle(snapshot);
13542
+ ntfy?.handle(snapshot);
13543
+ pushover?.handle(snapshot);
11309
13544
  notifications.handle(snapshot);
11310
13545
  options.onSnapshot?.(snapshot);
11311
13546
  }
@@ -11348,8 +13583,19 @@ function createCollector(options) {
11348
13583
  notifications,
11349
13584
  email: () => email?.status() ?? emailOff,
11350
13585
  webhook: () => webhook?.status() ?? webhookOff,
13586
+ ntfy: () => ntfy?.status() ?? ntfyOff,
13587
+ pushover: () => pushover?.status() ?? pushoverOff,
13588
+ phoneTest: createTestSendRoute({
13589
+ channels: { ntfy, pushover },
13590
+ limiter: createActionLimiter(now),
13591
+ now
13592
+ }),
11351
13593
  pullRequests: () => pullRequests?.status() ?? pullRequestsOff,
11352
13594
  jump: createJumpRoute({ poller, panes, run: tmux, tabs, osascript, now }),
13595
+ // Only this computer's own adapters read what a session last said. A
13596
+ // session on another machine is answered as not read, and nothing is
13597
+ // asked of that machine.
13598
+ lastMessage: createLastMessageRoute({ env, poller, adapters: localAdapters }),
11353
13599
  stop: stopRoutes && createStopRoute(stopRoutes),
11354
13600
  cleanUp: stopRoutes && createCleanUpRoute(stopRoutes),
11355
13601
  settings: () => ({
@@ -11442,13 +13688,20 @@ function createCollector(options) {
11442
13688
  poller,
11443
13689
  email,
11444
13690
  webhook,
13691
+ ntfy,
13692
+ pushover,
11445
13693
  pullRequests,
11446
13694
  history: keeper,
11447
13695
  answering: {
11448
13696
  start: () => answerStart ??= answering?.start() ?? Promise.resolve(),
11449
13697
  stop: async () => {
11450
13698
  await answering?.stop();
11451
- }
13699
+ },
13700
+ serve: (snapshot) => answering?.serve(snapshot) ?? snapshot,
13701
+ check: async () => {
13702
+ await answering?.check();
13703
+ },
13704
+ ...answering?.answer && { answer: answering.answer }
11452
13705
  },
11453
13706
  remotes,
11454
13707
  settings
@@ -11472,7 +13725,7 @@ async function runStandalone(options) {
11472
13725
  } catch (error) {
11473
13726
  return stopWith(error.message);
11474
13727
  }
11475
- if (!existsSync(path21.join(distDir, "index.html"))) {
13728
+ if (!existsSync(path26.join(distDir, "index.html"))) {
11476
13729
  return stopWith(
11477
13730
  "The dashboard has not been built yet. Run `npm run build`, then start it again."
11478
13731
  );
@@ -11515,12 +13768,12 @@ async function runStandalone(options) {
11515
13768
 
11516
13769
  // src/cli/start/openBrowser.ts
11517
13770
  import { spawn as spawn3 } from "node:child_process";
11518
- import path22 from "node:path";
13771
+ import path27 from "node:path";
11519
13772
  var OPEN_WAIT_MS = 1e4;
11520
13773
  var WINDOWS_FOLDER = "C:\\Windows";
11521
13774
  function windowsFolder(env) {
11522
13775
  const named = env.SystemRoot?.trim() || env.SYSTEMROOT?.trim();
11523
- return named && path22.win32.isAbsolute(named) ? named : WINDOWS_FOLDER;
13776
+ return named && path27.win32.isAbsolute(named) ? named : WINDOWS_FOLDER;
11524
13777
  }
11525
13778
  function openCommand(platform, url, env = process.env) {
11526
13779
  const address = loopbackAddress(url);
@@ -11529,7 +13782,7 @@ function openCommand(platform, url, env = process.env) {
11529
13782
  if (platform === "linux") return { file: "xdg-open", args: [address] };
11530
13783
  if (platform === "win32") {
11531
13784
  return {
11532
- file: path22.win32.join(windowsFolder(env), "System32", "rundll32.exe"),
13785
+ file: path27.win32.join(windowsFolder(env), "System32", "rundll32.exe"),
11533
13786
  args: ["url.dll,FileProtocolHandler", address]
11534
13787
  };
11535
13788
  }
@@ -11543,7 +13796,12 @@ function runProgram2(file, args, waitMs = OPEN_WAIT_MS) {
11543
13796
  resolve(opened);
11544
13797
  };
11545
13798
  try {
11546
- const child = spawn3(file, [...args], { stdio: "ignore", detached: true, windowsHide: true });
13799
+ const child = spawn3(file, [...args], {
13800
+ env: childEnvironment(process.env),
13801
+ stdio: "ignore",
13802
+ detached: true,
13803
+ windowsHide: true
13804
+ });
11547
13805
  child.once("error", () => settle(false));
11548
13806
  child.once("exit", (code) => settle(code === 0));
11549
13807
  child.unref();