agent-lookout 0.2.5 → 0.2.6

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,21 +1,24 @@
1
1
  import {
2
2
  sortSessions
3
- } from "./chunk-Q73JDXXW.js";
3
+ } from "./chunk-JSBQMY3Q.js";
4
4
  import {
5
5
  ANSWER_DECISIONS,
6
6
  CAPABILITIES,
7
+ EDIT_TOOLS,
7
8
  EVENT_ACTORS,
8
9
  EVENT_KINDS,
9
10
  EVENT_SEVERITIES,
10
11
  MAX_MACHINE_NAME_LENGTH,
11
12
  MAX_NAME_LENGTH,
12
13
  MAX_NOTICE_TEXT_LENGTH,
14
+ NOT_YES_OR_NO_TOOLS,
13
15
  SESSION_STATUSES,
14
16
  SOURCE_STATES,
15
17
  SOURCE_STATE_LABEL,
16
18
  SURFACES,
17
19
  WAITING_REASONS,
18
20
  agentName,
21
+ answeredNow,
19
22
  changeNotice,
20
23
  checksStateOf,
21
24
  clean,
@@ -24,12 +27,14 @@ import {
24
27
  durationInWords,
25
28
  durationParts,
26
29
  formatDuration,
30
+ inAnsweredWait,
27
31
  isGitHubRepository,
28
32
  isMachineName,
29
33
  isPullRequestNumber,
30
34
  isRemoteSource,
31
35
  isStatusEvent,
32
36
  loopbackAddress,
37
+ needsYou,
33
38
  noticeTitle,
34
39
  oneLine,
35
40
  overPhrase,
@@ -43,7 +48,7 @@ import {
43
48
  waitingPhrase,
44
49
  waitingText,
45
50
  withoutWaitingText
46
- } from "./chunk-OT3EGOTY.js";
51
+ } from "./chunk-I6Q35DHC.js";
47
52
 
48
53
  // src/collector/hosts/server.ts
49
54
  import { createServer } from "node:http";
@@ -66,7 +71,7 @@ function waitChanges(memory, snapshot) {
66
71
  const started = [];
67
72
  const stopped = [];
68
73
  for (const session of snapshot.sessions) {
69
- if (session.status !== "needs-you") continue;
74
+ if (!needsYou(session)) continue;
70
75
  const waiting = waitingNow.get(session.source);
71
76
  if (!waiting || waiting.has(session.id)) continue;
72
77
  const remembered = memory.get(session.source);
@@ -192,6 +197,9 @@ var ANSWER_ACTION = "answer";
192
197
  var SETTINGS_PATH = "/api/settings";
193
198
  var TIME_RULES_PATH = "/api/settings/time-rules";
194
199
  var TIME_RULES_ACTION = "time-rules";
200
+ var PERMISSION_RULES_PATH = "/api/settings/permission-rules";
201
+ var PERMISSION_RULES_ACTION = "permission-rules";
202
+ var MAX_RULE_ANSWERS = 100;
195
203
  var ACTION_HEADER = "X-Agent-Lookout-Action";
196
204
  var MAX_EVENTS_PER_RESPONSE = 200;
197
205
  var NOTIFICATIONS_HEADER = "X-Agent-Lookout-Notifications";
@@ -213,15 +221,15 @@ function isWebhookHost(host) {
213
221
  // src/core/history.ts
214
222
  var DEFAULT_HISTORY_WINDOW_MS = 15 * 60 * 1e3;
215
223
  function historyPointFor(sessions, at) {
216
- let needsYou = 0;
224
+ let needsYou2 = 0;
217
225
  let working = 0;
218
226
  let idle = 0;
219
227
  for (const session of sessions) {
220
- if (session.status === "needs-you") needsYou += 1;
221
- else if (session.status === "working") working += 1;
228
+ if (needsYou(session)) needsYou2 += 1;
229
+ else if (session.status === "working" || session.status === "needs-you") working += 1;
222
230
  else if (session.status === "idle" && !session.stale) idle += 1;
223
231
  }
224
- return { at, needsYou, working, idle, total: sessions.length };
232
+ return { at, needsYou: needsYou2, working, idle, total: sessions.length };
225
233
  }
226
234
 
227
235
  // src/core/time-rules/quietHold.ts
@@ -264,14 +272,14 @@ function createQuietHold() {
264
272
  const stillOpen = /* @__PURE__ */ new Map();
265
273
  for (const wait of waits) {
266
274
  const now = listed.get(wait.session.id);
267
- if (now?.status !== "needs-you") continue;
275
+ if (now === void 0 || !needsYou(now)) continue;
268
276
  if (wait.endedAt === null || now.statusSince === wait.begunAt) {
269
277
  stillOpen.set(wait.session.id, wait);
270
278
  }
271
279
  }
272
- const open4 = [];
280
+ const open2 = [];
273
281
  for (const [id, wait] of stillOpen) {
274
- open4.push({ session: listed.get(id), begunAt: wait.begunAt });
282
+ open2.push({ session: listed.get(id), begunAt: wait.begunAt });
275
283
  }
276
284
  const answered = /* @__PURE__ */ new Map();
277
285
  const lastBegun = /* @__PURE__ */ new Map();
@@ -309,7 +317,7 @@ function createQuietHold() {
309
317
  since = null;
310
318
  waits = [];
311
319
  over = [];
312
- return { summary: items.length > 0 ? { from, to: at, items } : null, open: open4 };
320
+ return { summary: items.length > 0 ? { from, to: at, items } : null, open: open2 };
313
321
  }
314
322
  };
315
323
  }
@@ -471,7 +479,7 @@ function createReminderWatch() {
471
479
  for (const id of stopped) watched.delete(id);
472
480
  const sources = readable(snapshot);
473
481
  for (const session of snapshot.sessions) {
474
- if (session.status !== "needs-you" || !sources.has(session.source)) continue;
482
+ if (!needsYou(session) || !sources.has(session.source)) continue;
475
483
  const since = session.statusSince !== null && session.statusSince <= at ? session.statusSince : null;
476
484
  const known = watched.get(session.id);
477
485
  if (known && (since === null || since === known.begunAt)) continue;
@@ -485,7 +493,7 @@ function createReminderWatch() {
485
493
  due(snapshot, at, thresholdMs, eligible = () => true) {
486
494
  const found = [];
487
495
  for (const session of snapshot.sessions) {
488
- if (session.status !== "needs-you") continue;
496
+ if (!needsYou(session)) continue;
489
497
  const wait = watched.get(session.id);
490
498
  if (!wait || wait.reminded.has(thresholdMs) || !eligible(session.id)) continue;
491
499
  const waitedMs = at - wait.begunAt;
@@ -532,7 +540,7 @@ function createOutboundChannel(options) {
532
540
  const wanted = new Set(options.events);
533
541
  let memory = EMPTY_CHANGE_MEMORY;
534
542
  const ready = [];
535
- const open4 = /* @__PURE__ */ new Map();
543
+ const open2 = /* @__PURE__ */ new Map();
536
544
  const done = /* @__PURE__ */ new Map();
537
545
  let sentAt = [];
538
546
  const reminders = createReminderWatch();
@@ -558,7 +566,7 @@ function createOutboundChannel(options) {
558
566
  const result = sessionChanges(memory, snapshot);
559
567
  memory = result.memory;
560
568
  for (const id of result.stopped) {
561
- open4.delete(id);
569
+ open2.delete(id);
562
570
  hold.waitEnded(id, at);
563
571
  }
564
572
  reminders.observe(snapshot, result.stopped, at);
@@ -572,7 +580,7 @@ function createOutboundChannel(options) {
572
580
  }));
573
581
  ready.push((sentNow) => summaryMessage({ ...summary, items, now: sentNow }));
574
582
  }
575
- for (const { session, begunAt } of end.open) open4.set(session.id, begunAt);
583
+ for (const { session, begunAt } of end.open) open2.set(session.id, begunAt);
576
584
  }
577
585
  if (quiet) hold.begin(at);
578
586
  for (const { event, session } of result.changes) {
@@ -593,7 +601,7 @@ function createOutboundChannel(options) {
593
601
  }
594
602
  const since = session.statusSince;
595
603
  if (since !== null && done.get(session.id) === since) continue;
596
- open4.set(session.id, waitBegan(since, at));
604
+ open2.set(session.id, waitBegan(since, at));
597
605
  }
598
606
  sentAt = sendsInLastHour(sentAt, at);
599
607
  while (!quiet && ready.length > 0 && limitLiftsAt(sentAt, at) === null) {
@@ -602,20 +610,20 @@ function createOutboundChannel(options) {
602
610
  send2(write(at));
603
611
  }
604
612
  const thresholdMs = rules.longWait.minutes * 6e4;
605
- for (const [id, begunAt] of open4) {
613
+ for (const [id, begunAt] of open2) {
606
614
  const session = snapshot.sessions.find(
607
- (candidate) => candidate.id === id && candidate.status === "needs-you"
615
+ (candidate) => candidate.id === id && needsYou(candidate)
608
616
  );
609
617
  if (!session) continue;
610
618
  const timing = sendTiming(begunAt, afterMs, sentAt, at);
611
619
  if (timing === "wait") continue;
612
620
  if (quiet) {
613
- open4.delete(id);
621
+ open2.delete(id);
614
622
  hold.holdWait(session, begunAt);
615
623
  continue;
616
624
  }
617
625
  if (timing === "limited") continue;
618
- open4.delete(id);
626
+ open2.delete(id);
619
627
  sentAt.push(at);
620
628
  send2(
621
629
  waitMessage({
@@ -630,7 +638,7 @@ function createOutboundChannel(options) {
630
638
  if (rules.longWait.on) reminders.toldLate(id, at, thresholdMs);
631
639
  }
632
640
  if (!quiet && rules.longWait.on && wanted.has("needs-you")) {
633
- for (const due of reminders.due(snapshot, at, thresholdMs, (id) => !open4.has(id))) {
641
+ for (const due of reminders.due(snapshot, at, thresholdMs, (id) => !open2.has(id))) {
634
642
  if (limitLiftsAt(sentAt, at) !== null) break;
635
643
  sentAt.push(at);
636
644
  reminders.reminded(due.session.id, thresholdMs);
@@ -647,7 +655,7 @@ function createOutboundChannel(options) {
647
655
  }
648
656
  }
649
657
  for (const session of snapshot.sessions) {
650
- if (session.status === "needs-you" && session.statusSince !== null && !open4.has(session.id)) {
658
+ if (needsYou(session) && session.statusSince !== null && !open2.has(session.id)) {
651
659
  done.set(session.id, session.statusSince);
652
660
  }
653
661
  }
@@ -998,15 +1006,15 @@ function recordValue(record) {
998
1006
  case "cleared":
999
1007
  return { cleared: record.at };
1000
1008
  case "point": {
1001
- const { at, needsYou, working, idle, total } = record.point;
1002
- return { point: [at, needsYou, working, idle, total] };
1009
+ const { at, needsYou: needsYou2, working, idle, total } = record.point;
1010
+ return { point: [at, needsYou2, working, idle, total] };
1003
1011
  }
1004
1012
  case "event":
1005
1013
  return { event: eventFields(record.event) };
1006
1014
  }
1007
1015
  }
1008
1016
  function eventFields(event) {
1009
- const { id, at, sessionId, sessionName: sessionName2, kind, from, to, severity, by, decision } = event;
1017
+ const { id, at, sessionId, sessionName: sessionName2, kind, from, to, severity, by, decision, tool, rule } = event;
1010
1018
  return {
1011
1019
  id,
1012
1020
  at,
@@ -1017,7 +1025,12 @@ function eventFields(event) {
1017
1025
  ...to !== void 0 && { to },
1018
1026
  severity,
1019
1027
  ...by !== void 0 && { by },
1020
- ...decision !== void 0 && { decision }
1028
+ ...decision !== void 0 && { decision },
1029
+ ...tool !== void 0 && { tool },
1030
+ // The rule's own two fields, and nothing else it might carry.
1031
+ ...rule !== void 0 && {
1032
+ rule: { tool: rule.tool, ...rule.command !== void 0 && { command: rule.command } }
1033
+ }
1021
1034
  };
1022
1035
  }
1023
1036
  function isRecord2(value) {
@@ -1038,11 +1051,11 @@ function oneOf(list, value) {
1038
1051
  function readPoint(value) {
1039
1052
  if (!Array.isArray(value) || value.length !== 5) return null;
1040
1053
  const at = moment(value[0]);
1041
- const [needsYou, working, idle, total] = value.slice(1).map(count);
1042
- if (at === null || needsYou == null || working == null || idle == null || total == null) {
1054
+ const [needsYou2, working, idle, total] = value.slice(1).map(count);
1055
+ if (at === null || needsYou2 == null || working == null || idle == null || total == null) {
1043
1056
  return null;
1044
1057
  }
1045
- return { at, needsYou, working, idle, total };
1058
+ return { at, needsYou: needsYou2, working, idle, total };
1046
1059
  }
1047
1060
  function readEvent(value) {
1048
1061
  if (!isRecord2(value)) return null;
@@ -1060,6 +1073,10 @@ function readEvent(value) {
1060
1073
  if (value.by !== void 0 && !by) return null;
1061
1074
  const decision = oneOf(ANSWER_DECISIONS, value.decision);
1062
1075
  if (value.decision !== void 0 && !decision) return null;
1076
+ const tool = field(value.tool);
1077
+ if (value.tool !== void 0 && !tool) return null;
1078
+ const rule = value.rule === void 0 ? void 0 : readRule(value.rule);
1079
+ if (rule === null) return null;
1063
1080
  return {
1064
1081
  id,
1065
1082
  at,
@@ -1070,9 +1087,20 @@ function readEvent(value) {
1070
1087
  ...to && { to },
1071
1088
  severity,
1072
1089
  ...by && { by },
1073
- ...decision && { decision }
1090
+ ...decision && { decision },
1091
+ ...tool && { tool },
1092
+ ...rule && { rule }
1074
1093
  };
1075
1094
  }
1095
+ function readRule(value) {
1096
+ if (!isRecord2(value)) return null;
1097
+ const keys = Object.keys(value);
1098
+ if (!keys.every((key) => key === "tool" || key === "command")) return null;
1099
+ const tool = field(value.tool);
1100
+ const command = value.command === void 0 ? void 0 : field(value.command);
1101
+ if (!tool || command === null) return null;
1102
+ return { tool, ...command !== void 0 && { command } };
1103
+ }
1076
1104
  function parseRecord(line) {
1077
1105
  if (line === "" || line.length > MAX_LINE_LENGTH) return null;
1078
1106
  let value;
@@ -1291,6 +1319,34 @@ function staleBy(sessions, at, thresholdMs) {
1291
1319
  return stale === session.stale ? session : { ...session, stale };
1292
1320
  });
1293
1321
  }
1322
+ function forEvents(sessions, previous, answered) {
1323
+ if (answered.size === 0) return sessions;
1324
+ const before = new Map(previous.map((session) => [session.id, session]));
1325
+ const taken = [];
1326
+ for (const session of sessions) {
1327
+ if (!answered.has(session.id)) {
1328
+ taken.push(session);
1329
+ continue;
1330
+ }
1331
+ const was = before.get(session.id);
1332
+ if (was) taken.push(was);
1333
+ }
1334
+ return taken;
1335
+ }
1336
+ function markAnswered(sessions, answered) {
1337
+ if (answered.size === 0) return sessions;
1338
+ return sessions.map(
1339
+ (session) => answered.has(session.id) ? { ...session, answered: true } : session
1340
+ );
1341
+ }
1342
+ var NONE_ANSWERED = /* @__PURE__ */ new Set();
1343
+ function withAnsweredThere(answered, sessions) {
1344
+ const there = sessions.filter(
1345
+ (session) => session.machine !== void 0 && session.answered === true
1346
+ );
1347
+ if (there.length === 0) return answered;
1348
+ return /* @__PURE__ */ new Set([...answered, ...there.map((session) => session.id)]);
1349
+ }
1294
1350
  function declared(adapter) {
1295
1351
  return adapter.capabilities === void 0 ? {} : { capabilities: adapter.capabilities };
1296
1352
  }
@@ -1345,11 +1401,11 @@ function createPoller(options) {
1345
1401
  };
1346
1402
  }
1347
1403
  function ask(adapter) {
1348
- const open4 = overdue.get(adapter);
1349
- if (open4) {
1350
- if (open4.late === void 0) return Promise.resolve(stillWaiting(adapter, open4.since));
1404
+ const open2 = overdue.get(adapter);
1405
+ if (open2) {
1406
+ if (open2.late === void 0) return Promise.resolve(stillWaiting(adapter, open2.since));
1351
1407
  overdue.delete(adapter);
1352
- return Promise.resolve(open4.late);
1408
+ return Promise.resolve(open2.late);
1353
1409
  }
1354
1410
  const since = now();
1355
1411
  return new Promise((resolve) => {
@@ -1393,10 +1449,24 @@ function createPoller(options) {
1393
1449
  return sessions;
1394
1450
  }
1395
1451
  }
1452
+ function answeredIn(snapshot) {
1453
+ if (!options.answered) return NONE_ANSWERED;
1454
+ try {
1455
+ return options.answered(snapshot);
1456
+ } catch {
1457
+ return NONE_ANSWERED;
1458
+ }
1459
+ }
1396
1460
  async function poll() {
1397
1461
  const results = await Promise.all(adapters.map((adapter) => ask(adapter)));
1398
1462
  const found = await annotated(results.flatMap((result) => result.sessions));
1399
1463
  const at = now();
1464
+ const sources = results.map((result, index) => ({
1465
+ ...result.health,
1466
+ ...result.basis !== void 0 && { basis: result.basis },
1467
+ ...declared(adapters[index])
1468
+ }));
1469
+ const answered = withAnsweredThere(answeredIn({ sources, sessions: found }), found);
1400
1470
  const changes = [];
1401
1471
  results.forEach((result, index) => {
1402
1472
  const { state } = result.health;
@@ -1413,35 +1483,29 @@ function createPoller(options) {
1413
1483
  const { baselines, reported } = remembered;
1414
1484
  const basis = result.basis ?? "";
1415
1485
  const previous = baselines.get(basis);
1486
+ const sessions = previous ? forEvents(result.sessions, previous, answered) : result.sessions;
1416
1487
  if (previous) {
1417
- changes.push(...withoutRepeats(diffSessions(previous, result.sessions, at), reported));
1488
+ changes.push(...withoutRepeats(diffSessions(previous, sessions, at), reported));
1418
1489
  } else {
1419
1490
  if (lastSeen) {
1420
1491
  const known = new Set(lastSeen.map((session) => session.id));
1421
1492
  const before = lastSeen.filter(
1422
1493
  (session) => session.status !== null
1423
1494
  );
1424
- const after = result.sessions.filter((session) => known.has(session.id));
1495
+ const after = sessions.filter((session) => known.has(session.id));
1425
1496
  changes.push(...diffSessions(before, after, at));
1426
1497
  }
1427
- for (const session of result.sessions) {
1498
+ for (const session of sessions) {
1428
1499
  if (!reported.has(session.id)) reported.set(session.id, session.status);
1429
1500
  }
1430
1501
  }
1431
- baselines.set(basis, result.sessions.map(withoutWaitingText));
1502
+ baselines.set(basis, sessions.map(withoutWaitingText));
1432
1503
  });
1433
1504
  const rules = options.timeRules?.();
1434
1505
  const snapshot = {
1435
1506
  generatedAt: at,
1436
- // The way each was read goes with it, so what follows the snapshots
1437
- // compares answers the way the event log does. So does what its agent
1438
- // can report, whatever the answer was.
1439
- sources: results.map((result, index) => ({
1440
- ...result.health,
1441
- ...result.basis !== void 0 && { basis: result.basis },
1442
- ...declared(adapters[index])
1443
- })),
1444
- sessions: sortSessions(staleBy(found, at, staleAfterMs(rules))),
1507
+ sources,
1508
+ sessions: sortSessions(staleBy(markAnswered(found, answered), at, staleAfterMs(rules))),
1445
1509
  ...rules && { timeRules: rules, quiet: isQuietAt(rules.quietHours, at) }
1446
1510
  };
1447
1511
  latest = snapshot;
@@ -1791,11 +1855,12 @@ function createApiHandler(options) {
1791
1855
  if (options.cleanUp) actions.set(CLEAN_UP_PATH, options.cleanUp);
1792
1856
  if (options.answer) actions.set(ANSWER_PATH, options.answer);
1793
1857
  if (options.timeRules) actions.set(TIME_RULES_PATH, options.timeRules);
1858
+ if (options.permissionRules) actions.set(PERMISSION_RULES_PATH, options.permissionRules);
1794
1859
  const serveSnapshot = options.serveSnapshot ?? ((snapshot) => snapshot);
1795
1860
  function route(req, res) {
1796
- const refusal2 = refusalFor(req);
1797
- if (refusal2) {
1798
- fail(res, 403, refusal2);
1861
+ const refusal3 = refusalFor(req);
1862
+ if (refusal3) {
1863
+ fail(res, 403, refusal3);
1799
1864
  return;
1800
1865
  }
1801
1866
  let url;
@@ -2136,7 +2201,7 @@ function urlFor(address) {
2136
2201
 
2137
2202
  // src/collector/hosts/standalone.ts
2138
2203
  import { existsSync } from "node:fs";
2139
- import path23 from "node:path";
2204
+ import path21 from "node:path";
2140
2205
 
2141
2206
  // src/core/mapping/claudeCodeMapping.ts
2142
2207
  var LIVE_STATUS = /* @__PURE__ */ new Map([
@@ -2195,7 +2260,7 @@ function claudeCodeOpenLink(surface, sessionId) {
2195
2260
  }
2196
2261
 
2197
2262
  // src/collector/terminal/terminalTabs.ts
2198
- import path2 from "node:path";
2263
+ import path2 from "node:path/posix";
2199
2264
  var TERMINAL_PATH = "/System/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal";
2200
2265
  var ITERM_PATH = "/Applications/iTerm.app/Contents/MacOS/iTerm2";
2201
2266
  var ITERM_SERVER_FOLDER = "Library/Application Support/iTerm2";
@@ -2785,7 +2850,15 @@ import path4 from "node:path";
2785
2850
  import { constants } from "node:fs";
2786
2851
  import { access, stat as stat2 } from "node:fs/promises";
2787
2852
  import path3 from "node:path";
2788
- async function isExecutableFile(candidate) {
2853
+ function pathsOf(platform) {
2854
+ return platform === "win32" ? path3.win32 : path3.posix;
2855
+ }
2856
+ var WINDOWS_PROGRAM_ENDINGS = [".com", ".exe"];
2857
+ function isWindowsProgram(file) {
2858
+ return WINDOWS_PROGRAM_ENDINGS.includes(path3.win32.extname(file).toLowerCase());
2859
+ }
2860
+ async function isExecutableFile(candidate, platform = process.platform) {
2861
+ if (platform === "win32" && !isWindowsProgram(candidate)) return false;
2789
2862
  try {
2790
2863
  const info = await stat2(candidate);
2791
2864
  if (!info.isFile()) return false;
@@ -2795,44 +2868,103 @@ async function isExecutableFile(candidate) {
2795
2868
  return false;
2796
2869
  }
2797
2870
  }
2798
- function pathCandidates(env, program) {
2799
- return (env.PATH ?? "").split(path3.delimiter).filter((dir) => dir !== "" && path3.isAbsolute(dir)).map((dir) => path3.join(dir, program));
2871
+ function variable(env, name, platform = process.platform) {
2872
+ if (platform !== "win32" || env[name] !== void 0) return env[name];
2873
+ const key = Object.keys(env).find((each) => each.toUpperCase() === name);
2874
+ return key === void 0 ? void 0 : env[key];
2875
+ }
2876
+ function programEndings(env) {
2877
+ const listed = (variable(env, "PATHEXT", "win32") ?? "").split(";").map((ending) => ending.trim().toLowerCase()).filter((ending) => WINDOWS_PROGRAM_ENDINGS.includes(ending));
2878
+ return listed.length > 0 ? [...new Set(listed)] : WINDOWS_PROGRAM_ENDINGS;
2879
+ }
2880
+ function pathFolders(env, platform = process.platform) {
2881
+ const paths = pathsOf(platform);
2882
+ return (variable(env, "PATH", platform) ?? "").split(paths.delimiter).map((dir) => platform === "win32" ? dir.trim().replace(/^"(.*)"$/, "$1") : dir).filter((dir) => dir !== "" && paths.isAbsolute(dir));
2883
+ }
2884
+ function programNames(env, program, platform = process.platform) {
2885
+ if (platform !== "win32" || isWindowsProgram(program)) return [program];
2886
+ return programEndings(env).map((ending) => `${program}${ending}`);
2887
+ }
2888
+ function pathCandidates(env, program, platform = process.platform) {
2889
+ const paths = pathsOf(platform);
2890
+ const names = programNames(env, program, platform);
2891
+ return pathFolders(env, platform).flatMap((dir) => names.map((name) => paths.join(dir, name)));
2800
2892
  }
2801
- function tildify(target, homeDir) {
2802
- if (homeDir === "" || homeDir === path3.sep) return target;
2803
- if (target === homeDir) return "~";
2804
- if (target.startsWith(homeDir + path3.sep)) return `~${target.slice(homeDir.length)}`;
2805
- return target;
2893
+ function tildify(target, homeDir, platform = process.platform) {
2894
+ if (homeDir === "") return target;
2895
+ if (platform !== "win32") {
2896
+ if (homeDir === "/") return target;
2897
+ if (target === homeDir) return "~";
2898
+ if (target.startsWith(`${homeDir}/`)) return `~${target.slice(homeDir.length)}`;
2899
+ return target;
2900
+ }
2901
+ const home = path3.win32.resolve(homeDir);
2902
+ if (path3.win32.parse(home).root === home) return target;
2903
+ const inside = path3.win32.relative(home, path3.win32.resolve(target));
2904
+ if (inside === "") return "~";
2905
+ if (inside === ".." || inside.startsWith("..\\") || path3.win32.isAbsolute(inside)) return target;
2906
+ return `~/${inside.replaceAll("\\", "/")}`;
2806
2907
  }
2807
2908
 
2808
2909
  // src/collector/adapters/claude-code/findBinary.ts
2809
2910
  var CLAUDE_BIN_ENV = "AGENT_LOOKOUT_CLAUDE_BIN";
2810
- function fixedLocations(homeDir) {
2911
+ var NPM_PROGRAM = ["node_modules", "@anthropic-ai", "claude-code", "bin", "claude.exe"];
2912
+ function npmFolder(env, homeDir) {
2913
+ const appData = variable(env, "APPDATA", "win32")?.trim();
2914
+ return appData && path4.win32.isAbsolute(appData) ? path4.win32.join(appData, "npm") : path4.win32.join(homeDir, "AppData", "Roaming", "npm");
2915
+ }
2916
+ function fixedLocations(homeDir, platform = process.platform, env = {}) {
2917
+ if (platform === "win32") {
2918
+ return [
2919
+ path4.win32.join(homeDir, ".local", "bin", "claude.exe"),
2920
+ path4.win32.join(npmFolder(env, homeDir), ...NPM_PROGRAM)
2921
+ ];
2922
+ }
2811
2923
  return [
2812
- path4.join(homeDir, ".local", "bin", "claude"),
2924
+ path4.posix.join(homeDir, ".local", "bin", "claude"),
2813
2925
  "/opt/homebrew/bin/claude",
2814
2926
  "/usr/local/bin/claude",
2815
- path4.join(homeDir, ".npm-global", "bin", "claude"),
2927
+ path4.posix.join(homeDir, ".npm-global", "bin", "claude"),
2816
2928
  "/usr/bin/claude"
2817
2929
  ];
2818
2930
  }
2931
+ function onPath(env, platform) {
2932
+ if (platform !== "win32") return pathCandidates(env, "claude", platform);
2933
+ const names = programNames(env, "claude", platform);
2934
+ return pathFolders(env, platform).flatMap((dir) => [
2935
+ ...names.map((name) => path4.win32.join(dir, name)),
2936
+ path4.win32.join(dir, ...NPM_PROGRAM)
2937
+ ]);
2938
+ }
2939
+ var WINDOWS_SCRIPT = /\.(bat|cmd|ps1)$/i;
2819
2940
  async function findClaudeBinary(options) {
2820
- const isExecutable = options.isExecutable ?? isExecutableFile;
2941
+ const platform = options.platform ?? process.platform;
2942
+ const paths = pathsOf(platform);
2943
+ const isExecutable = options.isExecutable ?? ((candidate) => isExecutableFile(candidate, platform));
2821
2944
  const named = options.env[CLAUDE_BIN_ENV]?.trim();
2822
2945
  if (named) {
2823
- const resolved = path4.resolve(named);
2946
+ const resolved = paths.resolve(named);
2824
2947
  if (await isExecutable(resolved)) return { found: true, path: resolved };
2825
- return {
2826
- found: false,
2827
- looked: `${CLAUDE_BIN_ENV} is set to ${named}, which is not a program this user can run`
2828
- };
2948
+ const why = platform === "win32" && WINDOWS_SCRIPT.test(resolved) ? "a script that needs a shell to run, and Agent Lookout runs no shell" : "not a program this user can run";
2949
+ return { found: false, looked: `${CLAUDE_BIN_ENV} is set to ${named}, which is ${why}` };
2829
2950
  }
2830
- const fixed = fixedLocations(options.homeDir);
2831
- const candidates = [.../* @__PURE__ */ new Set([...pathCandidates(options.env, "claude"), ...fixed])];
2951
+ const fixed = fixedLocations(options.homeDir, platform, options.env);
2952
+ const candidates = [.../* @__PURE__ */ new Set([...onPath(options.env, platform), ...fixed])];
2832
2953
  for (const candidate of candidates) {
2833
2954
  if (await isExecutable(candidate)) return { found: true, path: candidate };
2834
2955
  }
2835
- const fixedDirs = fixed.map((candidate) => tildify(path4.dirname(candidate), options.homeDir));
2956
+ if (platform === "win32") {
2957
+ const places = [paths.dirname(fixed[0]), npmFolder(options.env, options.homeDir)].map(
2958
+ (place) => tildify(place, options.homeDir, platform)
2959
+ );
2960
+ return {
2961
+ found: false,
2962
+ looked: `The claude command, claude.exe, was not found on PATH or in ${listWithOr(places)}`
2963
+ };
2964
+ }
2965
+ const fixedDirs = fixed.map(
2966
+ (candidate) => tildify(paths.dirname(candidate), options.homeDir, platform)
2967
+ );
2836
2968
  return {
2837
2969
  found: false,
2838
2970
  looked: `The claude command was not found on PATH or in ${listWithOr(fixedDirs)}`
@@ -2845,7 +2977,6 @@ function listWithOr(items) {
2845
2977
 
2846
2978
  // src/collector/adapters/claude-code/index.ts
2847
2979
  import os from "node:os";
2848
- import path9 from "node:path";
2849
2980
 
2850
2981
  // src/core/time.ts
2851
2982
  var EARLIEST_PLAUSIBLE_TIME_MS = Date.UTC(2020, 0, 1);
@@ -3044,13 +3175,73 @@ function createFinishedTracker(retentionMs = FINISHED_RETENTION_MS) {
3044
3175
  }
3045
3176
 
3046
3177
  // src/collector/adapters/claude-code/registry.ts
3047
- import { constants as constants2 } from "node:fs";
3048
- import { open, readdir } from "node:fs/promises";
3178
+ import { constants as constants3 } from "node:fs";
3179
+ import { readdir } from "node:fs/promises";
3049
3180
  import path5 from "node:path";
3181
+
3182
+ // src/collector/files/noFollow.ts
3183
+ import { closeSync, constants as constants2, fstatSync, lstatSync, openSync } from "node:fs";
3184
+ import { lstat, open } from "node:fs/promises";
3185
+ var OPEN_FOLLOWS_LINKS = constants2.O_NOFOLLOW === void 0;
3186
+ var NO_FOLLOW = (constants2.O_NOFOLLOW ?? 0) | (constants2.O_NONBLOCK ?? 0);
3187
+ function notOrdinary() {
3188
+ return new Error("Not an ordinary file.");
3189
+ }
3190
+ function refusal2(named) {
3191
+ if (!named.isSymbolicLink()) return notOrdinary();
3192
+ return Object.assign(new Error("A symbolic link, which is not followed."), { code: "ELOOP" });
3193
+ }
3194
+ function sameFile(named, opened) {
3195
+ return named.isFile() && named.dev === opened.dev && named.ino === opened.ino;
3196
+ }
3197
+ function isMissing(error) {
3198
+ return error?.code === "ENOENT";
3199
+ }
3200
+ var creates = (flags) => (flags & constants2.O_CREAT) !== 0;
3201
+ async function openWithoutFollowing(file, flags, mode, follows = OPEN_FOLLOWS_LINKS) {
3202
+ if (!follows) return open(file, flags | NO_FOLLOW, mode);
3203
+ let before = null;
3204
+ try {
3205
+ before = await lstat(file, { bigint: true });
3206
+ } catch (error) {
3207
+ if (!(isMissing(error) && creates(flags))) throw error;
3208
+ }
3209
+ if (before !== null && !before.isFile()) throw refusal2(before);
3210
+ const handle = await open(file, flags, mode);
3211
+ try {
3212
+ const opened = await handle.stat({ bigint: true });
3213
+ const named = before ?? await lstat(file, { bigint: true });
3214
+ if (!sameFile(named, opened)) throw notOrdinary();
3215
+ return handle;
3216
+ } catch (error) {
3217
+ await handle.close();
3218
+ throw error;
3219
+ }
3220
+ }
3221
+ function openWithoutFollowingNow(file, flags, mode, follows = OPEN_FOLLOWS_LINKS) {
3222
+ if (!follows) return openSync(file, flags | NO_FOLLOW, mode);
3223
+ let before = null;
3224
+ try {
3225
+ before = lstatSync(file, { bigint: true });
3226
+ } catch (error) {
3227
+ if (!(isMissing(error) && creates(flags))) throw error;
3228
+ }
3229
+ if (before !== null && !before.isFile()) throw refusal2(before);
3230
+ const fd = openSync(file, flags, mode);
3231
+ try {
3232
+ const named = before ?? lstatSync(file, { bigint: true });
3233
+ if (!sameFile(named, fstatSync(fd, { bigint: true }))) throw notOrdinary();
3234
+ return fd;
3235
+ } catch (error) {
3236
+ closeSync(fd);
3237
+ throw error;
3238
+ }
3239
+ }
3240
+
3241
+ // src/collector/adapters/claude-code/registry.ts
3050
3242
  var MAX_ENTRY_BYTES = 256 * 1024;
3051
3243
  async function readRegularFile(file) {
3052
- const flags = constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0) | (constants2.O_NONBLOCK ?? 0);
3053
- const handle = await open(file, flags);
3244
+ const handle = await openWithoutFollowing(file, constants3.O_RDONLY);
3054
3245
  try {
3055
3246
  const info = await handle.stat();
3056
3247
  if (!info.isFile()) throw new Error("Not an ordinary file.");
@@ -3108,7 +3299,7 @@ function parseRegistryEntry(content) {
3108
3299
  procStart: text2(raw.procStart)
3109
3300
  };
3110
3301
  }
3111
- async function readRegistry(sessionsDir, io = nodeIo) {
3302
+ async function readRegistry(sessionsDir, io = nodeIo, previous) {
3112
3303
  let names;
3113
3304
  try {
3114
3305
  names = await io.readdir(sessionsDir);
@@ -3117,22 +3308,28 @@ async function readRegistry(sessionsDir, io = nodeIo) {
3117
3308
  return { readable: false, missing: code === "ENOENT" || code === "ENOTDIR" };
3118
3309
  }
3119
3310
  const files = names.filter(isRegistryFileName).sort();
3120
- const parsed = await Promise.all(
3311
+ const parsed = /* @__PURE__ */ new Map();
3312
+ const found = await Promise.all(
3121
3313
  files.map(async (name) => {
3314
+ let content;
3122
3315
  try {
3123
- return parseRegistryEntry(await io.readFile(path5.join(sessionsDir, name)));
3316
+ content = await io.readFile(path5.join(sessionsDir, name));
3124
3317
  } catch {
3125
3318
  return null;
3126
3319
  }
3320
+ const entry = parseRegistryEntry(content);
3321
+ if (entry === null) return previous?.get(name) ?? null;
3322
+ parsed.set(name, entry);
3323
+ return entry;
3127
3324
  })
3128
3325
  );
3129
3326
  const entries = /* @__PURE__ */ new Map();
3130
- parsed.forEach((entry, index) => {
3327
+ found.forEach((entry, index) => {
3131
3328
  if (!entry) return;
3132
3329
  const isNamedForPid = files[index] === `${entry.pid}.json`;
3133
3330
  if (isNamedForPid || !entries.has(entry.pid)) entries.set(entry.pid, entry);
3134
3331
  });
3135
- return { readable: true, entries };
3332
+ return { readable: true, entries, parsed };
3136
3333
  }
3137
3334
 
3138
3335
  // src/collector/adapters/claude-code/stopOffers.ts
@@ -3153,7 +3350,7 @@ function build(fields, origin, context) {
3153
3350
  const project = projectOf(fields.cwd);
3154
3351
  const surface = mapClaudeCodeSurface(fields.entrypoint);
3155
3352
  const mapped = mapClaudeCodeStatus(fields, origin);
3156
- const open4 = claudeCodeOpenLink(surface, fields.sessionId);
3353
+ const open2 = claudeCodeOpenLink(surface, fields.sessionId);
3157
3354
  const startedAt = plausibleTime(fields.startedAt, context.now);
3158
3355
  let statusSince = plausibleTime(fields.statusUpdatedAt, context.now);
3159
3356
  if (statusSince !== null && startedAt !== null && statusSince < startedAt - WRITE_ORDER_SLACK_MS) {
@@ -3170,7 +3367,7 @@ function build(fields, origin, context) {
3170
3367
  ...mapped,
3171
3368
  startedAt,
3172
3369
  statusSince,
3173
- links: open4 ? { open: open4 } : {},
3370
+ links: open2 ? { open: open2 } : {},
3174
3371
  stale: isStale({ status: mapped.status, statusSince }, context.now)
3175
3372
  };
3176
3373
  if (fields.pid !== void 0) {
@@ -3273,8 +3470,8 @@ function withStopOffers(sessions, live2, answer, options) {
3273
3470
  import path8 from "node:path";
3274
3471
 
3275
3472
  // src/collector/files/readOnlyIo.ts
3276
- import { constants as constants3 } from "node:fs";
3277
- import { lstat, open as open2, readdir as readdir2, stat as stat3 } from "node:fs/promises";
3473
+ import { constants as constants4 } from "node:fs";
3474
+ import { lstat as lstat2, readdir as readdir2, stat as stat3 } from "node:fs/promises";
3278
3475
  function infoOf(stats) {
3279
3476
  return {
3280
3477
  kind: stats.isFile() ? "file" : stats.isDirectory() ? "directory" : "other",
@@ -3284,8 +3481,7 @@ function infoOf(stats) {
3284
3481
  };
3285
3482
  }
3286
3483
  async function openRegularFile(file) {
3287
- const flags = constants3.O_RDONLY | (constants3.O_NOFOLLOW ?? 0) | (constants3.O_NONBLOCK ?? 0);
3288
- const handle = await open2(file, flags);
3484
+ const handle = await openWithoutFollowing(file, constants4.O_RDONLY);
3289
3485
  let info;
3290
3486
  try {
3291
3487
  info = infoOf(await handle.stat());
@@ -3317,10 +3513,10 @@ async function openRegularFile(file) {
3317
3513
  var nodeIo2 = {
3318
3514
  readdir: (dir) => readdir2(dir),
3319
3515
  stat: async (target) => infoOf(await stat3(target)),
3320
- lstat: async (target) => infoOf(await lstat(target)),
3516
+ lstat: async (target) => infoOf(await lstat2(target)),
3321
3517
  openRegular: openRegularFile
3322
3518
  };
3323
- function isMissing(error) {
3519
+ function isMissing2(error) {
3324
3520
  const code = error?.code;
3325
3521
  return code === "ENOENT" || code === "ENOTDIR";
3326
3522
  }
@@ -3493,12 +3689,12 @@ function sameStamp(a, b) {
3493
3689
  return a.size === b.size && a.mtimeMs === b.mtimeMs && a.ino === b.ino;
3494
3690
  }
3495
3691
  async function readTranscriptTail(file, io, known, maxBytes = TRANSCRIPT_TAIL_BYTES) {
3496
- const open4 = await io.openRegular(file);
3692
+ const open2 = await io.openRegular(file);
3497
3693
  try {
3498
- const stamp = stampOf(open4.info);
3694
+ const stamp = stampOf(open2.info);
3499
3695
  if (known !== null && sameStamp(known, stamp)) return { unchanged: true, stamp };
3500
3696
  const start = Math.max(0, stamp.size - maxBytes);
3501
- const bytes2 = await open4.read(start, stamp.size - start);
3697
+ const bytes2 = await open2.read(start, stamp.size - start);
3502
3698
  return {
3503
3699
  unchanged: false,
3504
3700
  stamp,
@@ -3506,7 +3702,7 @@ async function readTranscriptTail(file, io, known, maxBytes = TRANSCRIPT_TAIL_BY
3506
3702
  fromStart: start === 0
3507
3703
  };
3508
3704
  } finally {
3509
- await open4.close();
3705
+ await open2.close();
3510
3706
  }
3511
3707
  }
3512
3708
 
@@ -3544,7 +3740,7 @@ function createWaitingTextReader(options) {
3544
3740
  }
3545
3741
  return wait.text;
3546
3742
  } catch (error) {
3547
- if (isMissing(error)) wait.file = null;
3743
+ if (isMissing2(error)) wait.file = null;
3548
3744
  wait.lookedAt = at;
3549
3745
  wait.stamp = null;
3550
3746
  wait.text = void 0;
@@ -3613,7 +3809,22 @@ var CLAUDE_CODE_CAPABILITIES = {
3613
3809
  },
3614
3810
  answer: {
3615
3811
  level: "partly",
3616
- reason: "With the Agent Lookout plugin installed. Allow only when all it allows is shown, so edits, plans and questions can only be denied."
3812
+ reason: "With the Agent Lookout plugin, by Allow, Deny or a permission rule. Allow only if all is shown: never edits, plans or questions."
3813
+ }
3814
+ };
3815
+ var CLAUDE_CODE_WINDOWS_CAPABILITIES = {
3816
+ ...CLAUDE_CODE_CAPABILITIES,
3817
+ jump: {
3818
+ level: "partly",
3819
+ reason: "Only in VS Code. On Windows there is no tmux pane or Terminal tab to reach, and the desktop app has no link."
3820
+ },
3821
+ stop: {
3822
+ level: "no",
3823
+ reason: "Not on Windows, where there is no ps to confirm a session's process by its start time, and no POSIX signal to stop it."
3824
+ },
3825
+ answer: {
3826
+ level: "no",
3827
+ reason: "Not on Windows: the plugin's hook is a POSIX sh script, and it reaches Agent Lookout through a Unix socket."
3617
3828
  }
3618
3829
  };
3619
3830
  var STOP_TURNED_OFF = {
@@ -3649,21 +3860,25 @@ function createClaudeCodeAdapter(options = {}) {
3649
3860
  const transcriptsOff = waitingTextOff(env);
3650
3861
  const feedWithheld = homeOverride !== void 0 && !binaryNamed;
3651
3862
  const neverRun = feedOff || feedWithheld;
3652
- const stops = stopOff(env) ? void 0 : options.stops;
3863
+ const platform = options.platform ?? process.platform;
3864
+ const onWindows = platform === "win32";
3865
+ const paths = pathsOf(platform);
3866
+ const stops = stopOff(env) || onWindows ? void 0 : options.stops;
3653
3867
  const answersOff = env.AGENT_LOOKOUT_ANSWER?.trim().toLowerCase() === "off";
3654
- const capabilities = stopOff(env) || answersOff ? {
3868
+ const capabilities = onWindows ? CLAUDE_CODE_WINDOWS_CAPABILITIES : stopOff(env) || answersOff ? {
3655
3869
  ...CLAUDE_CODE_CAPABILITIES,
3656
3870
  ...stopOff(env) && { stop: STOP_TURNED_OFF },
3657
3871
  ...answersOff && { answer: ANSWER_TURNED_OFF }
3658
3872
  } : CLAUDE_CODE_CAPABILITIES;
3659
- const claudeHome = path9.resolve(homeOverride ?? path9.join(homeDir, ".claude"));
3660
- const sessionsDir = path9.join(claudeHome, "sessions");
3661
- const homeName = tildify(claudeHome, homeDir);
3662
- const registryName = tildify(sessionsDir, homeDir);
3873
+ const claudeHome = paths.resolve(homeOverride ?? paths.join(homeDir, ".claude"));
3874
+ const sessionsDir = paths.join(claudeHome, "sessions");
3875
+ const homeName = tildify(claudeHome, homeDir, platform);
3876
+ const registryName = tildify(sessionsDir, homeDir, platform);
3663
3877
  const waitingTexts = transcriptsOff ? null : createWaitingTextReader({ claudeHome, io: options.transcriptIo, now });
3664
3878
  let lastDue = null;
3665
3879
  let lastRead = null;
3666
3880
  let feedSoon = false;
3881
+ let lastParsed;
3667
3882
  stops?.onAskFeedSoon(() => {
3668
3883
  feedSoon = true;
3669
3884
  });
@@ -3686,7 +3901,8 @@ function createClaudeCodeAdapter(options = {}) {
3686
3901
  ];
3687
3902
  }
3688
3903
  async function readRegistryNow(at) {
3689
- const registry = await readRegistry(sessionsDir, options.registryIo);
3904
+ const registry = await readRegistry(sessionsDir, options.registryIo, lastParsed);
3905
+ lastParsed = registry.readable ? registry.parsed : void 0;
3690
3906
  if (!registry.readable) return { registry, live: [], unknownStatus: false };
3691
3907
  const candidates = [...registry.entries.values()].filter(
3692
3908
  (entry) => entry.sessionId !== void 0 && isClaudeCodeSessionKind(entry.kind) && isAlive(entry.pid)
@@ -3720,7 +3936,12 @@ function createClaudeCodeAdapter(options = {}) {
3720
3936
  return at - due < intervalMs ? due : at;
3721
3937
  }
3722
3938
  async function runCommand(askedAt) {
3723
- const binary = await findClaudeBinary({ env, homeDir, isExecutable: options.isExecutable });
3939
+ const binary = await findClaudeBinary({
3940
+ env,
3941
+ homeDir,
3942
+ isExecutable: options.isExecutable,
3943
+ platform
3944
+ });
3724
3945
  if (!binary.found) {
3725
3946
  return {
3726
3947
  askedAt,
@@ -4119,8 +4340,10 @@ function stopFailed(reason) {
4119
4340
  function stopRefusalFor(req) {
4120
4341
  return actionRefusalFor(req, STOP_ACTION, MAX_STOP_BODY_BYTES);
4121
4342
  }
4122
- function stillOffered(snapshot, sessionId) {
4123
- return snapshot.sessions.some((session) => session.id === sessionId && session.stop);
4343
+ function notSeenToStop(snapshot, stopped) {
4344
+ return snapshot.sessions.some(
4345
+ (session) => session.id === stopped.id && (session.stop !== void 0 || stopped.stop?.how === "background" && session.status !== "finished" && session.status !== "failed")
4346
+ );
4124
4347
  }
4125
4348
  async function lookAgain(options, stopped) {
4126
4349
  if (stopped.length === 0) return;
@@ -4129,7 +4352,7 @@ async function lookAgain(options, stopped) {
4129
4352
  }
4130
4353
  try {
4131
4354
  const first = await options.poller.pollOnce();
4132
- if (stopped.some((session) => stillOffered(first, session.id))) {
4355
+ if (stopped.some((session) => notSeenToStop(first, session))) {
4133
4356
  await options.poller.pollOnce();
4134
4357
  }
4135
4358
  } catch {
@@ -4148,7 +4371,9 @@ function createStopRoute(options) {
4148
4371
  if (acted === "signalled") {
4149
4372
  const running = await stopper.waitForEnd([target.pid]);
4150
4373
  if (running.size > 0) return stopFailed("still-running");
4151
- } else if (acted !== "stopped") {
4374
+ } else if (acted === "stopped") {
4375
+ await stopper.waitForEnd([target.pid]);
4376
+ } else {
4152
4377
  return stopFailed(acted);
4153
4378
  }
4154
4379
  events.add([stoppedEvent(session, stopper.now())]);
@@ -4266,9 +4491,7 @@ function createCleanUpRoute(options) {
4266
4491
  outcomes[index] = acted;
4267
4492
  }
4268
4493
  }
4269
- const running = await stopper.waitForEnd(
4270
- sent.filter((one) => one.signalled).map((one) => one.target.pid)
4271
- );
4494
+ const running = await stopper.waitForEnd(sent.map((one) => one.target.pid));
4272
4495
  const ended = [];
4273
4496
  for (const one of sent) {
4274
4497
  if (one.signalled && running.has(one.target.pid)) outcomes[one.index] = "still-running";
@@ -4367,6 +4590,15 @@ function answeredEvent(session, decision, at) {
4367
4590
  decision
4368
4591
  };
4369
4592
  }
4593
+ async function pollAfterAnswer(poller, sessionId) {
4594
+ try {
4595
+ const first = await poller.pollOnce();
4596
+ if (first.sessions.some((session) => session.id === sessionId && needsYou(session))) {
4597
+ await poller.pollOnce();
4598
+ }
4599
+ } catch {
4600
+ }
4601
+ }
4370
4602
  function answerRefusalFor(req) {
4371
4603
  return actionRefusalFor(req, ANSWER_ACTION, MAX_ANSWER_BODY_BYTES);
4372
4604
  }
@@ -4396,8 +4628,7 @@ function createAnswerRoute(options) {
4396
4628
  const outcome = await asks.answer(asked.sessionId, asked.requestId, asked.decision);
4397
4629
  if (outcome !== "answered") return answerFailed(outcome);
4398
4630
  events.add([answeredEvent(session, asked.decision, now())]);
4399
- void poller.pollOnce().catch(() => {
4400
- });
4631
+ await pollAfterAnswer(poller, asked.sessionId);
4401
4632
  return {
4402
4633
  status: 200,
4403
4634
  body: { ok: true, decision: asked.decision }
@@ -4407,7 +4638,6 @@ function createAnswerRoute(options) {
4407
4638
 
4408
4639
  // src/collector/answers/answerSettings.ts
4409
4640
  import os3 from "node:os";
4410
- import path10 from "node:path";
4411
4641
  var ANSWER_ENV = "AGENT_LOOKOUT_ANSWER";
4412
4642
  var ANSWER_SOCKET_ENV = "AGENT_LOOKOUT_ANSWER_SOCKET";
4413
4643
  var ANSWER_WAIT_ENV = "AGENT_LOOKOUT_ANSWER_WAIT";
@@ -4415,6 +4645,7 @@ var DEFAULT_HOLD_MS = 3e5;
4415
4645
  var MIN_HOLD_MS = 5e3;
4416
4646
  var MAX_HOLD_MS = 54e4;
4417
4647
  var MAX_SOCKET_PATH_BYTES = 103;
4648
+ var WINDOWS_PROBLEM = "Permission prompts are not answered from Agent Lookout on Windows: the plugin's hook is a POSIX sh script, and it reaches Agent Lookout through a Unix socket.";
4418
4649
  function answerOff(env) {
4419
4650
  return env[ANSWER_ENV]?.trim().toLowerCase() === "off";
4420
4651
  }
@@ -4435,7 +4666,8 @@ function readAnswerSetup(env, homeDir = os3.homedir(), platform = process.platfo
4435
4666
  on: false,
4436
4667
  socketPath: null,
4437
4668
  holdMs: holdMs ?? DEFAULT_HOLD_MS,
4438
- problem: "Answering permission prompts is not offered on Windows."
4669
+ problem: WINDOWS_PROBLEM,
4670
+ quiet: true
4439
4671
  };
4440
4672
  }
4441
4673
  if (holdMs === null) {
@@ -4456,8 +4688,9 @@ function readAnswerSetup(env, homeDir = os3.homedir(), platform = process.platfo
4456
4688
  quiet: true
4457
4689
  };
4458
4690
  }
4459
- const ownDir = path10.resolve(homeDir, ".agent-lookout");
4460
- const socketPath = path10.resolve(named || path10.join(ownDir, "answer.sock"));
4691
+ const paths = pathsOf(platform);
4692
+ const ownDir = paths.resolve(homeDir, ".agent-lookout");
4693
+ const socketPath = paths.resolve(named || paths.join(ownDir, "answer.sock"));
4461
4694
  if (Buffer.byteLength(socketPath) > MAX_SOCKET_PATH_BYTES) {
4462
4695
  return {
4463
4696
  on: false,
@@ -4471,7 +4704,7 @@ function readAnswerSetup(env, homeDir = os3.homedir(), platform = process.platfo
4471
4704
  socketPath,
4472
4705
  holdMs,
4473
4706
  problem: null,
4474
- ownFolder: path10.dirname(socketPath) === ownDir
4707
+ ownFolder: paths.dirname(socketPath) === ownDir
4475
4708
  };
4476
4709
  }
4477
4710
 
@@ -4485,8 +4718,6 @@ var MAX_BLANK_RUN = 2;
4485
4718
  var MAX_INPUTS = 30;
4486
4719
  var MAX_TOOL_NAME_CHARS = 200;
4487
4720
  var MAX_DESCRIPTION_CHARS = 200;
4488
- var EDIT_TOOLS = ["Edit", "Write", "MultiEdit", "NotebookEdit"];
4489
- var NOT_YES_OR_NO_TOOLS = ["ExitPlanMode", "AskUserQuestion"];
4490
4721
  var EDIT_INPUTS = ["file_path", "notebook_path"];
4491
4722
  var HIDDEN = /[\p{Cc}\p{Cf}\p{Co}\p{Cn}\p{Cs}\p{Default_Ignorable_Code_Point}\u2028\u2029]/gu;
4492
4723
  var RIGHT_TO_LEFT = /[\p{Script=Hebrew}\p{Script=Arabic}\p{Script=Syriac}\p{Script=Thaana}\p{Script=Nko}\p{Script=Samaritan}\p{Script=Mandaic}\p{Script=Adlam}\p{Script=Hanifi_Rohingya}\p{Script=Mende_Kikakui}\p{Script=Old_Hungarian}\p{Script=Kharoshthi}\p{Script=Phoenician}\p{Script=Imperial_Aramaic}\p{Script=Avestan}\p{Script=Old_South_Arabian}\p{Script=Old_North_Arabian}\p{Script=Nabataean}\p{Script=Palmyrene}\p{Script=Manichaean}\p{Script=Inscriptional_Pahlavi}\p{Script=Inscriptional_Parthian}\p{Script=Psalter_Pahlavi}\p{Script=Sogdian}\p{Script=Old_Sogdian}\p{Script=Elymaic}\p{Script=Lydian}\p{Script=Cypriot}]/u;
@@ -4642,6 +4873,7 @@ function createHeldAsks(options) {
4642
4873
  const newId = options.newId ?? (() => randomBytes(16).toString("hex"));
4643
4874
  const startedAt = now();
4644
4875
  const held = /* @__PURE__ */ new Map();
4876
+ let answers2 = /* @__PURE__ */ new Map();
4645
4877
  const lastRequest = /* @__PURE__ */ new Map();
4646
4878
  let seenAt = null;
4647
4879
  let missedAt = null;
@@ -4673,6 +4905,8 @@ function createHeldAsks(options) {
4673
4905
  if (!entry.confirmed) {
4674
4906
  entry.confirmed = true;
4675
4907
  entry.wait = reading.wait;
4908
+ entry.since = reading.since ?? null;
4909
+ answerByRule(entry);
4676
4910
  return;
4677
4911
  }
4678
4912
  if (reading.wait !== entry.wait) drop(entry, "left-waiting");
@@ -4692,6 +4926,55 @@ function createHeldAsks(options) {
4692
4926
  if (inFlight === run) inFlight = null;
4693
4927
  }
4694
4928
  }
4929
+ async function answer(sessionId, requestId, decision) {
4930
+ const entry = held.get(sessionId);
4931
+ if (!entry || entry.requestId !== requestId || !entry.confirmed) return "no-ask";
4932
+ if (decision === "allow" && !entry.shown.allow) return "not-allowable";
4933
+ if (entry.answering) return "too-soon";
4934
+ entry.answering = true;
4935
+ const reading = await read(sessionId);
4936
+ if (held.get(sessionId) !== entry || !entry.reply.isOpen()) {
4937
+ if (held.get(sessionId) === entry) drop(entry, "closed");
4938
+ return "gone";
4939
+ }
4940
+ const sameWait = reading.status === "waiting" && reading.wait === entry.wait;
4941
+ if (!sameWait || now() >= entry.until) {
4942
+ drop(entry, sameWait ? "limit" : "left-waiting");
4943
+ return "gone";
4944
+ }
4945
+ held.delete(sessionId);
4946
+ entry.reply.send(decisionOutput(decision));
4947
+ answers2.set(sessionId, { source: SOURCE, since: entry.since, at: now() });
4948
+ if (held.size === 0 && stopTimer !== null) {
4949
+ stopTimer();
4950
+ stopTimer = null;
4951
+ }
4952
+ return "answered";
4953
+ }
4954
+ function answerByRule(entry) {
4955
+ const rules = options.rules;
4956
+ if (rules === void 0) return;
4957
+ const request2 = { sessionId: entry.sessionId, shown: entry.shown };
4958
+ const verdict = verdictOf(entry);
4959
+ if (verdict === null) return;
4960
+ entry.byRule = true;
4961
+ void answer(entry.sessionId, entry.requestId, verdict.decision).then((outcome) => {
4962
+ if (outcome === "answered") rules.answered(request2, verdict);
4963
+ });
4964
+ }
4965
+ function verdictOf(entry) {
4966
+ if (options.rules === void 0) return null;
4967
+ try {
4968
+ return options.rules.verdictFor({ sessionId: entry.sessionId, shown: entry.shown });
4969
+ } catch {
4970
+ return null;
4971
+ }
4972
+ }
4973
+ function answeringByRule(entry) {
4974
+ if (!entry.reply.isOpen()) return false;
4975
+ if (entry.confirmed) return entry.answering && entry.byRule;
4976
+ return verdictOf(entry) !== null;
4977
+ }
4695
4978
  function status() {
4696
4979
  const plugin = seenAt !== null && (missedAt === null || seenAt > missedAt) ? "seen" : missedAt !== null ? "missed" : "unknown";
4697
4980
  return { state: "on", plugin, holdMs };
@@ -4704,6 +4987,7 @@ function createHeldAsks(options) {
4704
4987
  const at = now();
4705
4988
  const older = held.get(request2.sessionId);
4706
4989
  if (older) drop(older, "newer");
4990
+ answers2.delete(request2.sessionId);
4707
4991
  const entry = {
4708
4992
  requestId: newId(),
4709
4993
  sessionId: request2.sessionId,
@@ -4713,7 +4997,9 @@ function createHeldAsks(options) {
4713
4997
  reply,
4714
4998
  confirmed: false,
4715
4999
  wait: null,
4716
- answering: false
5000
+ since: null,
5001
+ answering: false,
5002
+ byRule: false
4717
5003
  };
4718
5004
  if (held.size >= MAX_HELD) {
4719
5005
  drop(entry, "full");
@@ -4729,30 +5015,7 @@ function createHeldAsks(options) {
4729
5015
  void check();
4730
5016
  },
4731
5017
  check,
4732
- async answer(sessionId, requestId, decision) {
4733
- const entry = held.get(sessionId);
4734
- if (!entry || entry.requestId !== requestId || !entry.confirmed) return "no-ask";
4735
- if (decision === "allow" && !entry.shown.allow) return "not-allowable";
4736
- if (entry.answering) return "too-soon";
4737
- entry.answering = true;
4738
- const reading = await read(sessionId);
4739
- if (held.get(sessionId) !== entry || !entry.reply.isOpen()) {
4740
- if (held.get(sessionId) === entry) drop(entry, "closed");
4741
- return "gone";
4742
- }
4743
- const sameWait = reading.status === "waiting" && reading.wait === entry.wait;
4744
- if (!sameWait || now() >= entry.until) {
4745
- drop(entry, sameWait ? "limit" : "left-waiting");
4746
- return "gone";
4747
- }
4748
- held.delete(sessionId);
4749
- entry.reply.send(decisionOutput(decision));
4750
- if (held.size === 0 && stopTimer !== null) {
4751
- stopTimer();
4752
- stopTimer = null;
4753
- }
4754
- return "answered";
4755
- },
5018
+ answer,
4756
5019
  withAsks(snapshot) {
4757
5020
  const answering = status();
4758
5021
  if (held.size === 0) return { ...snapshot, answering };
@@ -4765,6 +5028,17 @@ function createHeldAsks(options) {
4765
5028
  });
4766
5029
  return { ...snapshot, sessions, answering };
4767
5030
  },
5031
+ answeredIn(snapshot) {
5032
+ const { ids, kept } = answeredNow(answers2, snapshot, now());
5033
+ answers2 = kept;
5034
+ for (const entry of held.values()) {
5035
+ if (!answeringByRule(entry)) continue;
5036
+ const session = snapshot.sessions.find((listed) => listed.id === entry.sessionId);
5037
+ const since = entry.confirmed ? entry.since : null;
5038
+ if (session && inAnsweredWait(session, { since })) ids.add(entry.sessionId);
5039
+ }
5040
+ return ids;
5041
+ },
4768
5042
  noteRequest(sessionId) {
4769
5043
  const at = now();
4770
5044
  seenAt = at;
@@ -4801,10 +5075,10 @@ function createHeldAsks(options) {
4801
5075
  }
4802
5076
 
4803
5077
  // src/collector/answers/hookSocket.ts
4804
- import { chmod, lstat as lstat2, mkdir, stat as stat4, unlink } from "node:fs/promises";
5078
+ import { chmod, lstat as lstat3, mkdir, stat as stat4, unlink } from "node:fs/promises";
4805
5079
  import http from "node:http";
4806
5080
  import net from "node:net";
4807
- import path11 from "node:path";
5081
+ import path9 from "node:path";
4808
5082
  var HOOK_PATH = "/hooks/permission-request";
4809
5083
  var HOOK_HEADER = "x-agent-lookout-hook";
4810
5084
  var HOOK_HEADER_VALUE = "permission-request";
@@ -4877,7 +5151,7 @@ function createHookSocket(options) {
4877
5151
  const maxBody = options.maxBodyBytes ?? MAX_HOOK_BODY_BYTES;
4878
5152
  const bodyTimeoutMs = options.bodyTimeoutMs ?? HOOK_BODY_TIMEOUT_MS;
4879
5153
  const uid = options.uid ?? process.getuid?.() ?? -1;
4880
- const folder = path11.dirname(socketPath);
5154
+ const folder = path9.dirname(socketPath);
4881
5155
  let server = null;
4882
5156
  let made = null;
4883
5157
  async function handle(req, res) {
@@ -4911,7 +5185,7 @@ function createHookSocket(options) {
4911
5185
  async function readyFolder() {
4912
5186
  try {
4913
5187
  const madeHere = await mkdir(folder, { recursive: true, mode: 448 });
4914
- const info = await lstat2(folder);
5188
+ const info = await lstat3(folder);
4915
5189
  if (!info.isDirectory()) return `${folder} is not a folder, so answering is off.`;
4916
5190
  if (uid >= 0 && info.uid !== uid) {
4917
5191
  return `${folder} belongs to another user, so answering is off.`;
@@ -4930,7 +5204,7 @@ function createHookSocket(options) {
4930
5204
  async function freePath() {
4931
5205
  let info;
4932
5206
  try {
4933
- info = await lstat2(socketPath);
5207
+ info = await lstat3(socketPath);
4934
5208
  } catch (error) {
4935
5209
  return errorCode2(error) === "ENOENT" ? null : `${socketPath} could not be read.`;
4936
5210
  }
@@ -5000,12 +5274,12 @@ function createHookSocket(options) {
5000
5274
 
5001
5275
  // src/collector/answers/registryStatus.ts
5002
5276
  import os4 from "node:os";
5003
- import path12 from "node:path";
5277
+ import path10 from "node:path";
5004
5278
  function createRegistryStatus(options) {
5005
5279
  const homeDir = options.homeDir ?? os4.homedir();
5006
5280
  const readFile2 = options.readFile ?? readRegularFile;
5007
- const home = options.env[CLAUDE_HOME_ENV]?.trim() || path12.join(homeDir, ".claude");
5008
- const sessionsDir = path12.join(path12.resolve(home), "sessions");
5281
+ const home = options.env[CLAUDE_HOME_ENV]?.trim() || path10.join(homeDir, ".claude");
5282
+ const sessionsDir = path10.join(path10.resolve(home), "sessions");
5009
5283
  return {
5010
5284
  async statusOf(sessionId) {
5011
5285
  const unknown = { status: "unknown" };
@@ -5016,7 +5290,7 @@ function createRegistryStatus(options) {
5016
5290
  const ownId = sessionId.slice("claude-code:".length);
5017
5291
  let content;
5018
5292
  try {
5019
- content = await readFile2(path12.join(sessionsDir, `${pid}.json`));
5293
+ content = await readFile2(path10.join(sessionsDir, `${pid}.json`));
5020
5294
  } catch {
5021
5295
  return unknown;
5022
5296
  }
@@ -5026,12 +5300,618 @@ function createRegistryStatus(options) {
5026
5300
  if (!forPermission) return { status: "not-waiting" };
5027
5301
  return {
5028
5302
  status: "waiting",
5029
- wait: `${entry.statusUpdatedAt ?? ""}|${entry.waitingFor ?? ""}`
5303
+ wait: `${entry.statusUpdatedAt ?? ""}|${entry.waitingFor ?? ""}`,
5304
+ ...entry.statusUpdatedAt !== void 0 && { since: entry.statusUpdatedAt }
5030
5305
  };
5031
5306
  }
5032
5307
  };
5033
5308
  }
5034
5309
 
5310
+ // src/core/permission-rules/commandWords.ts
5311
+ var COMMAND_RUNNERS = [
5312
+ // The shell's own words.
5313
+ ".",
5314
+ "builtin",
5315
+ "command",
5316
+ "coproc",
5317
+ "eval",
5318
+ "exec",
5319
+ "nocorrect",
5320
+ "noglob",
5321
+ "source",
5322
+ "time",
5323
+ // Programs that run the command after their own words.
5324
+ "arch",
5325
+ "bunx",
5326
+ "caffeinate",
5327
+ "chroot",
5328
+ "doas",
5329
+ "env",
5330
+ "flock",
5331
+ "gtimeout",
5332
+ "ionice",
5333
+ "nice",
5334
+ "nix-shell",
5335
+ "nohup",
5336
+ "npx",
5337
+ "parallel",
5338
+ "pnpx",
5339
+ "sandbox-exec",
5340
+ "script",
5341
+ "setsid",
5342
+ "stdbuf",
5343
+ "su",
5344
+ "sudo",
5345
+ "taskset",
5346
+ "timeout",
5347
+ "unbuffer",
5348
+ "uvx",
5349
+ "watch",
5350
+ "xargs",
5351
+ "xcrun",
5352
+ // Shells, which run the command they are given or a file.
5353
+ "ash",
5354
+ "bash",
5355
+ "busybox",
5356
+ "cmd",
5357
+ "csh",
5358
+ "dash",
5359
+ "fish",
5360
+ "ksh",
5361
+ "powershell",
5362
+ "pwsh",
5363
+ "sh",
5364
+ "tcsh",
5365
+ "zsh"
5366
+ ];
5367
+ var NETWORK_CLIENTS_AND_INTERPRETERS = [
5368
+ // Programs that send requests, or open a connection, to an address.
5369
+ "aria2c",
5370
+ "curl",
5371
+ "elinks",
5372
+ "ftp",
5373
+ "grpcurl",
5374
+ "http",
5375
+ "https",
5376
+ "httpx",
5377
+ "links",
5378
+ "lynx",
5379
+ "nc",
5380
+ "ncat",
5381
+ "netcat",
5382
+ "scp",
5383
+ "sftp",
5384
+ "socat",
5385
+ "ssh",
5386
+ "telnet",
5387
+ "w3m",
5388
+ "websocat",
5389
+ "wget",
5390
+ "xh",
5391
+ "xhs",
5392
+ // Interpreters, which run code given in their words or in a file.
5393
+ "awk",
5394
+ "bpython",
5395
+ "bun",
5396
+ "clj",
5397
+ "clojure",
5398
+ "dart",
5399
+ "deno",
5400
+ "elixir",
5401
+ "erl",
5402
+ "escript",
5403
+ "esno",
5404
+ "expect",
5405
+ "gawk",
5406
+ "ghci",
5407
+ "groovy",
5408
+ "guile",
5409
+ "iex",
5410
+ "ipython",
5411
+ "irb",
5412
+ "java",
5413
+ "jshell",
5414
+ "jrunscript",
5415
+ "julia",
5416
+ "kotlin",
5417
+ "lua",
5418
+ "luajit",
5419
+ "mawk",
5420
+ "nawk",
5421
+ "node",
5422
+ "nodejs",
5423
+ "ocaml",
5424
+ "osascript",
5425
+ "perl",
5426
+ "php",
5427
+ "pypy",
5428
+ "python",
5429
+ "r",
5430
+ "racket",
5431
+ "raku",
5432
+ "rakudo",
5433
+ "rscript",
5434
+ "ruby",
5435
+ "runghc",
5436
+ "runhaskell",
5437
+ "sbcl",
5438
+ "scala",
5439
+ "swift",
5440
+ "tclsh",
5441
+ "ts-node",
5442
+ "tsx",
5443
+ "vite-node",
5444
+ "wish",
5445
+ "zx"
5446
+ ];
5447
+ var WINDOWS_PROGRAM_END = /\.(?:exe|com)$/;
5448
+ var LETTER = /[a-z]/;
5449
+ function programOf(word2) {
5450
+ return word2.slice(word2.lastIndexOf("/") + 1).toLowerCase().replace(WINDOWS_PROGRAM_END, "");
5451
+ }
5452
+ function nameIn(program, names) {
5453
+ let found;
5454
+ for (const name of names) {
5455
+ if (program === name) return name;
5456
+ const fits = program.startsWith(name) && !LETTER.test(program.charAt(name.length));
5457
+ if (fits && name.length > (found?.length ?? 0)) found = name;
5458
+ }
5459
+ return found;
5460
+ }
5461
+ function runsAnotherCommand(word2) {
5462
+ return nameIn(programOf(word2), COMMAND_RUNNERS) !== void 0;
5463
+ }
5464
+ function sendsOrRunsCode(word2) {
5465
+ return nameIn(programOf(word2), NETWORK_CLIENTS_AND_INTERPRETERS) !== void 0;
5466
+ }
5467
+ var SUBCOMMANDS_THAT_RUN_CODE = {
5468
+ bundle: ["e", "ex", "exe", "exec"],
5469
+ bundler: ["e", "ex", "exe", "exec"],
5470
+ cargo: ["r", "run"],
5471
+ docker: ["build", "buildx", "compose", "container", "create", "exec", "run", "start"],
5472
+ "docker-compose": ["create", "exec", "run", "start", "up"],
5473
+ dotnet: ["exec", "run", "tool"],
5474
+ go: ["generate", "run", "tool"],
5475
+ nix: ["develop", "run", "shell"],
5476
+ npm: ["create", "exec", "explore", "init", "innit", "x"],
5477
+ pipx: ["run"],
5478
+ pnpm: ["create", "dlx", "exec", "x"],
5479
+ podman: ["build", "compose", "container", "create", "exec", "run", "start"],
5480
+ poetry: ["run"],
5481
+ uv: ["run", "tool"],
5482
+ yarn: ["create", "dlx", "exec", "node"],
5483
+ yarnpkg: ["create", "dlx", "exec", "node"]
5484
+ };
5485
+ var TAKES_A_SHORTENED_SUBCOMMAND = ["npm"];
5486
+ function subcommandToolOf(word2) {
5487
+ return nameIn(programOf(word2), Object.keys(SUBCOMMANDS_THAT_RUN_CODE));
5488
+ }
5489
+ function needsItsSubcommand(words) {
5490
+ const [first, second] = words;
5491
+ if (first === void 0 || subcommandToolOf(first) === void 0) return false;
5492
+ return second === void 0 || second.startsWith("-");
5493
+ }
5494
+ function runsCodeBySubcommand(words) {
5495
+ const [first, ...rest] = words;
5496
+ if (first === void 0) return false;
5497
+ const tool = subcommandToolOf(first);
5498
+ if (tool === void 0) return false;
5499
+ const listed = SUBCOMMANDS_THAT_RUN_CODE[tool] ?? [];
5500
+ const shortened = TAKES_A_SHORTENED_SUBCOMMAND.includes(tool);
5501
+ let pastOptions = false;
5502
+ for (const word2 of rest) {
5503
+ if (word2 === "--") {
5504
+ if (pastOptions) return false;
5505
+ continue;
5506
+ }
5507
+ if (!word2.startsWith("-")) pastOptions = true;
5508
+ const lower = word2.toLowerCase();
5509
+ if (listed.includes(lower)) return true;
5510
+ if (shortened && lower.length >= 2 && listed.some((name) => name.startsWith(lower)))
5511
+ return true;
5512
+ }
5513
+ return false;
5514
+ }
5515
+ var OPTIONS_THAT_RUN_A_PROGRAM = {
5516
+ git: ["c", "config-env", "exec-path", "receive-pack", "upload-pack"],
5517
+ go: ["exec", "toolexec", "vettool"],
5518
+ npm: ["call", "node-options", "script-shell"]
5519
+ };
5520
+ function runsAProgramByOption(words) {
5521
+ const [first, ...rest] = words;
5522
+ if (first === void 0) return false;
5523
+ const program = nameIn(programOf(first), Object.keys(OPTIONS_THAT_RUN_A_PROGRAM));
5524
+ if (program === void 0) return false;
5525
+ const options = OPTIONS_THAT_RUN_A_PROGRAM[program] ?? [];
5526
+ return rest.some((word2) => {
5527
+ if (!word2.startsWith("-")) return false;
5528
+ const [name = ""] = word2.replace(/^-+/, "").split("=");
5529
+ return options.includes(name);
5530
+ });
5531
+ }
5532
+ var OWN_FOLDER_NAME = ".agent-lookout";
5533
+ var BY_NUMBER = "/.vol";
5534
+ function plainPath(word2) {
5535
+ const lower = word2.toLowerCase();
5536
+ if (!lower.startsWith("/")) return lower;
5537
+ const parts = [];
5538
+ for (const part of lower.split("/")) {
5539
+ if (part === "" || part === ".") continue;
5540
+ if (part === "..") parts.pop();
5541
+ else parts.push(part);
5542
+ }
5543
+ return `/${parts.join("/")}`;
5544
+ }
5545
+ function foldersAbove(file) {
5546
+ const folders = [];
5547
+ let at = file;
5548
+ while (at !== "/" && at !== "") {
5549
+ at = at.slice(0, at.lastIndexOf("/")) || "/";
5550
+ folders.push(at);
5551
+ }
5552
+ return folders;
5553
+ }
5554
+ function namesOwnFiles(words, ownPaths = [], folder) {
5555
+ const own = ownPaths.filter((name) => name.startsWith("/")).map(plainPath);
5556
+ const above = new Set(own.flatMap(foldersAbove));
5557
+ const ownNames = own.filter((name) => !name.includes(OWN_FOLDER_NAME)).map((name) => name.slice(name.lastIndexOf("/") + 1));
5558
+ const from = folder?.startsWith("/") ? folder : void 0;
5559
+ return words.some((word2) => {
5560
+ const lower = word2.toLowerCase();
5561
+ if (lower.includes(OWN_FOLDER_NAME)) return true;
5562
+ const parts = [lower, ...lower.split("=").slice(1)];
5563
+ const read = parts.flatMap(
5564
+ (part) => from !== void 0 && !part.startsWith("/") && !part.startsWith("-") ? [part, `${from}/${part}`] : [part]
5565
+ );
5566
+ return read.some((part) => {
5567
+ const path23 = plainPath(part);
5568
+ if (path23 === BY_NUMBER || path23.startsWith(`${BY_NUMBER}/`)) return true;
5569
+ if (own.some((name) => path23 === name || path23.startsWith(`${name}/`))) return true;
5570
+ if (above.has(path23)) return true;
5571
+ return ownNames.some((name) => path23 === name || path23.endsWith(`/${name}`));
5572
+ });
5573
+ });
5574
+ }
5575
+ function folderReachesOwnFiles(folder, ownPaths = []) {
5576
+ const own = ownPaths.filter((name) => name.startsWith("/")).map(plainPath);
5577
+ const path23 = plainPath(folder);
5578
+ return own.some((name) => name === path23 || name.startsWith(`${path23 === "/" ? "" : path23}/`));
5579
+ }
5580
+ var PLAIN_WORD = /^[A-Za-z0-9_\-.,/:=+@%]+$/;
5581
+ var ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
5582
+ function plainCommandWords(command) {
5583
+ if (command === "") return null;
5584
+ const words = command.split(" ");
5585
+ for (const word2 of words) {
5586
+ if (word2 === "" || !PLAIN_WORD.test(word2)) return null;
5587
+ if (word2.startsWith("=")) return null;
5588
+ }
5589
+ if (ASSIGNMENT.test(words[0])) return null;
5590
+ return words;
5591
+ }
5592
+ var SEPARATORS = /[;&|\n\r()`{}<>]/;
5593
+ var DOLLAR_QUOTE = /\$(?=['"])/g;
5594
+ var QUOTING = /['"\\]/g;
5595
+ var CLAUSE_WORDS = ["!", "if", "then", "else", "elif", "do", "while", "until"];
5596
+ function looseCommandWords(command) {
5597
+ const commands = [];
5598
+ for (const part of command.split(SEPARATORS)) {
5599
+ const words = part.split(/\s+/u).map((word2) => word2.replace(DOLLAR_QUOTE, "").replace(QUOTING, "")).filter((word2) => word2 !== "");
5600
+ let first = 0;
5601
+ for (; ; ) {
5602
+ const word2 = words[first];
5603
+ if (word2 === void 0) break;
5604
+ if (!ASSIGNMENT.test(word2) && !CLAUSE_WORDS.includes(word2)) break;
5605
+ first += 1;
5606
+ }
5607
+ if (first < words.length) commands.push(words.slice(first));
5608
+ }
5609
+ return commands;
5610
+ }
5611
+ var PREFIX_MARK = ":*";
5612
+ function ruleCommandOf(command) {
5613
+ const prefix = command.endsWith(PREFIX_MARK);
5614
+ const words = (prefix ? command.slice(0, -PREFIX_MARK.length) : command).split(" ");
5615
+ return { words, prefix };
5616
+ }
5617
+ function wordsMatch(rule, words) {
5618
+ if (rule.prefix ? words.length < rule.words.length : words.length !== rule.words.length) {
5619
+ return false;
5620
+ }
5621
+ return rule.words.every((word2, index) => words[index] === word2);
5622
+ }
5623
+ function inOrder(wanted, words, from, to) {
5624
+ let at = from;
5625
+ for (const word2 of wanted) {
5626
+ while (at < to && words[at] !== word2) at += 1;
5627
+ if (at >= to) return -1;
5628
+ at += 1;
5629
+ }
5630
+ return at;
5631
+ }
5632
+ function looselyFrom(rule, words, start) {
5633
+ const [program, ...rest] = rule.words;
5634
+ if (program === void 0 || programOf(words[start]) !== programOf(program)) {
5635
+ return false;
5636
+ }
5637
+ if (rule.prefix) return inOrder(rest, words, start + 1, words.length) >= 0;
5638
+ const last = rest.at(-1);
5639
+ if (last === void 0) return words.length === start + 1;
5640
+ const end = words.length - 1;
5641
+ return end > start && words[end] === last && inOrder(rest.slice(0, -1), words, start + 1, end) >= 0;
5642
+ }
5643
+ function looselyMatches(rule, words) {
5644
+ const first = words[0];
5645
+ if (first === void 0) return false;
5646
+ const starts = runsAnotherCommand(first) ? words.length : 1;
5647
+ for (let start = 0; start < starts; start += 1) {
5648
+ if (looselyFrom(rule, words, start)) return true;
5649
+ }
5650
+ return false;
5651
+ }
5652
+
5653
+ // src/core/permission-rules/permissionRules.ts
5654
+ var RULE_DECISIONS = ["allow", "ask", "deny"];
5655
+ var EVERY_TOOL = "*";
5656
+ var COMMAND_TOOL = "Bash";
5657
+ var MCP_PREFIX = "mcp__";
5658
+ var MCP_SEPARATOR = "__";
5659
+ function isServerName(tool) {
5660
+ if (!tool.startsWith(MCP_PREFIX)) return false;
5661
+ const server = tool.slice(MCP_PREFIX.length);
5662
+ return server !== "" && !server.includes(MCP_SEPARATOR);
5663
+ }
5664
+ var MAX_RULES = 100;
5665
+ var MAX_RULE_TOOL_CHARS = 128;
5666
+ var MAX_RULE_COMMAND_CHARS = 200;
5667
+ var TOOL_NAME = /^[A-Za-z][A-Za-z0-9_-]*$/;
5668
+ var RULE_ID = /^[A-Za-z0-9-]{1,40}$/;
5669
+ var ASCII_WORDS = /^[\x21-\x7e]+(?: [\x21-\x7e]+)*$/;
5670
+ var NOT_IN_A_RULE = /[;&|<>()`'"\\{}$*]/;
5671
+ function isRecord7(value) {
5672
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5673
+ }
5674
+ function holdsOnly(value, keys, optional = []) {
5675
+ const there = Object.keys(value);
5676
+ return keys.every((key) => there.includes(key)) && there.every((key) => keys.includes(key) || optional.includes(key));
5677
+ }
5678
+ function toolProblem(tool) {
5679
+ if (tool === EVERY_TOOL) return null;
5680
+ if (tool === "") return "Name a tool, such as Bash or WebFetch, or * for every tool.";
5681
+ if (tool.includes("(")) {
5682
+ return "Write the tool's name alone, such as Bash, and the command in its own field.";
5683
+ }
5684
+ if (tool !== COMMAND_TOOL && tool.toLowerCase() === COMMAND_TOOL.toLowerCase()) {
5685
+ return `Claude Code writes this tool's name ${COMMAND_TOOL}.`;
5686
+ }
5687
+ if (tool.length > MAX_RULE_TOOL_CHARS || !TOOL_NAME.test(tool) || tool === MCP_PREFIX || tool.startsWith(MCP_PREFIX) && tool.endsWith(MCP_SEPARATOR)) {
5688
+ return "A tool's name is written as Claude Code writes it: a letter, then letters, digits, _ and -, such as Bash, WebFetch or mcp__docs__search.";
5689
+ }
5690
+ return null;
5691
+ }
5692
+ function commandProblem(command, decision) {
5693
+ if (command.length > MAX_RULE_COMMAND_CHARS) {
5694
+ return `A command in a rule is ${MAX_RULE_COMMAND_CHARS} characters at most.`;
5695
+ }
5696
+ if (!ASCII_WORDS.test(command)) {
5697
+ return "Write the command in plain ASCII letters, digits and marks, with one space between words and none at either end.";
5698
+ }
5699
+ const prefix = command.endsWith(PREFIX_MARK);
5700
+ const words = prefix ? command.slice(0, -PREFIX_MARK.length) : command;
5701
+ if (words === "" || words.endsWith(" ")) {
5702
+ return "Write :* straight after the words a command begins with, as in npm test:*.";
5703
+ }
5704
+ if (command.endsWith(" *")) {
5705
+ return "Write a prefix with :* straight after its words, as in npm test:*.";
5706
+ }
5707
+ if (NOT_IN_A_RULE.test(words)) {
5708
+ return "A rule names one command by its words: no ;, &, |, <, >, ( ), quotes, backslashes, $, { }, or * other than the :* that ends a prefix.";
5709
+ }
5710
+ if (decision === "allow" && plainCommandWords(words) === null) {
5711
+ return "An allow rule names one plain command: no ~, !, ^, #, ?, [ ], no word starting with =, and no variable set before it.";
5712
+ }
5713
+ const [program = ""] = words.split(" ");
5714
+ if (ASSIGNMENT.test(program)) {
5715
+ return "Begin the command with the program's name: the variables set before a command are passed over when it is matched.";
5716
+ }
5717
+ if (decision === "allow" && runsAnotherCommand(program)) {
5718
+ return `${program} runs another command, so an allow rule for it would let Claude Code run anything without asking you. Name the command it would run instead.`;
5719
+ }
5720
+ if (decision === "allow" && sendsOrRunsCode(program)) {
5721
+ return `${program} is, or is named like, a program that can send requests or run code that does, so an allow rule for it would let Claude Code reach Agent Lookout on this computer and add a rule or answer its own prompts without asking you. Answer such commands by hand, or allow a script the project owns, such as ./scripts/test.sh, knowing Claude can edit it.`;
5722
+ }
5723
+ if (decision === "allow" && needsItsSubcommand(words.split(" "))) {
5724
+ return `${program} does something different for each subcommand, and some run any code they are pointed at, so an allow rule for it names the subcommand too, such as ${program} test:*.`;
5725
+ }
5726
+ if (decision === "allow" && runsAProgramByOption(words.split(" "))) {
5727
+ return `${words} names an option through which ${program} runs another program, so an allow rule for it would let Claude Code run anything without asking you.`;
5728
+ }
5729
+ if (decision === "allow" && runsCodeBySubcommand(words.split(" "))) {
5730
+ return `${words} fetches or runs code that Claude Code can choose, so an allow rule for it would let it run anything without asking you. Name the script it should run instead, or answer such commands by hand.`;
5731
+ }
5732
+ if (decision === "allow" && namesOwnFiles(words.split(" "))) {
5733
+ return "An allow rule may not name Agent Lookout's own folder, ~/.agent-lookout, which holds its settings and its rules.";
5734
+ }
5735
+ return null;
5736
+ }
5737
+ function ruleProblem(words) {
5738
+ const { decision, tool, command } = words;
5739
+ const toolWrong = toolProblem(tool);
5740
+ if (toolWrong !== null) return toolWrong;
5741
+ if (command !== void 0) {
5742
+ if (tool !== COMMAND_TOOL)
5743
+ return "A command goes with Bash only. Leave it empty for any other tool.";
5744
+ const commandWrong = commandProblem(command, decision);
5745
+ if (commandWrong !== null) return commandWrong;
5746
+ }
5747
+ if (decision !== "allow") return null;
5748
+ if (tool === EVERY_TOOL) {
5749
+ return "An allow rule names one tool. Allowing every tool would let Claude Code run anything without asking you.";
5750
+ }
5751
+ if (tool === COMMAND_TOOL && command === void 0) {
5752
+ return "An allow rule for Bash names a command. Allowing every command would let Claude Code run anything without asking you.";
5753
+ }
5754
+ if (isServerName(tool)) {
5755
+ return `An allow rule names one tool of a server, such as ${tool}${MCP_SEPARATOR}search. Allowing every tool of ${tool} would let Claude Code use each without asking you.`;
5756
+ }
5757
+ if (EDIT_TOOLS.includes(tool)) {
5758
+ return `Agent Lookout does not show the change ${tool} makes, so it never allows one, by hand or by a rule.`;
5759
+ }
5760
+ if (NOT_YES_OR_NO_TOOLS.includes(tool)) {
5761
+ return `${tool} is answered with more than yes or no, so Agent Lookout never allows it, by hand or by a rule.`;
5762
+ }
5763
+ return null;
5764
+ }
5765
+ function ruleWordsIn(value) {
5766
+ const shape = 'A rule is {"decision": "allow", "ask" or "deny", "tool": "...", "command": "..."}, with the command for Bash only, and nothing else.';
5767
+ if (!isRecord7(value) || !holdsOnly(value, ["decision", "tool"], ["command"])) {
5768
+ return { ok: false, problem: shape };
5769
+ }
5770
+ const { decision, tool, command } = value;
5771
+ if (!RULE_DECISIONS.includes(decision))
5772
+ return { ok: false, problem: shape };
5773
+ if (typeof tool !== "string" || command !== void 0 && typeof command !== "string") {
5774
+ return { ok: false, problem: shape };
5775
+ }
5776
+ const words = {
5777
+ decision,
5778
+ tool,
5779
+ ...command !== void 0 && { command }
5780
+ };
5781
+ const problem = ruleProblem(words);
5782
+ return problem === null ? { ok: true, words } : { ok: false, problem };
5783
+ }
5784
+ function isRuleId(value) {
5785
+ return typeof value === "string" && RULE_ID.test(value);
5786
+ }
5787
+ function readRule2(value) {
5788
+ if (!isRecord7(value)) return null;
5789
+ const { id, ...rest } = value;
5790
+ if (!isRuleId(id)) return null;
5791
+ const read = ruleWordsIn(rest);
5792
+ return read.ok ? { id, ...read.words } : null;
5793
+ }
5794
+ function readPermissionRules(value) {
5795
+ if (value === void 0) return { ok: true, rules: [] };
5796
+ if (!Array.isArray(value) || value.length > MAX_RULES) return { ok: false };
5797
+ const rules = [];
5798
+ const ids = /* @__PURE__ */ new Set();
5799
+ for (const item of value) {
5800
+ const rule = readRule2(item);
5801
+ if (rule === null || ids.has(rule.id)) return { ok: false };
5802
+ ids.add(rule.id);
5803
+ rules.push(rule);
5804
+ }
5805
+ return { ok: true, rules };
5806
+ }
5807
+ function sameWords(a, b) {
5808
+ return a.decision === b.decision && a.tool === b.tool && a.command === b.command;
5809
+ }
5810
+
5811
+ // src/core/permission-rules/ruleDecision.ts
5812
+ var INPUTS_AN_ALLOW_RULE_TAKES = ["timeout", "run_in_background"];
5813
+ var ORDER = ["deny", "ask", "allow"];
5814
+ function toolMatches(rule, request2) {
5815
+ if (rule.tool === EVERY_TOOL || rule.tool === request2.tool) return true;
5816
+ return isServerName(rule.tool) && request2.tool.startsWith(`${rule.tool}${MCP_SEPARATOR}`);
5817
+ }
5818
+ function holdsBack(rule, request2) {
5819
+ if (!toolMatches(rule, request2)) return false;
5820
+ if (rule.command === void 0) return true;
5821
+ if (request2.tool !== COMMAND_TOOL || request2.command === void 0) return false;
5822
+ const wanted = ruleCommandOf(rule.command);
5823
+ return looseCommandWords(request2.command).some((words) => looselyMatches(wanted, words));
5824
+ }
5825
+ function lets(rule, request2) {
5826
+ if (!request2.allowable || rule.tool === EVERY_TOOL || isServerName(rule.tool)) return false;
5827
+ if (rule.tool !== request2.tool) return false;
5828
+ if (request2.tool !== COMMAND_TOOL) return rule.command === void 0;
5829
+ if (rule.command === void 0 || request2.command === void 0) return false;
5830
+ if (!request2.inputNames.every((name) => INPUTS_AN_ALLOW_RULE_TAKES.includes(name))) return false;
5831
+ const wanted = ruleCommandOf(rule.command);
5832
+ const [program = ""] = wanted.words;
5833
+ if (runsAnotherCommand(program) || sendsOrRunsCode(program)) return false;
5834
+ if (needsItsSubcommand(wanted.words)) return false;
5835
+ const words = plainCommandWords(request2.command);
5836
+ if (words === null || runsCodeBySubcommand(words) || runsAProgramByOption(words)) return false;
5837
+ const own = request2.ownPaths ?? [];
5838
+ if (request2.folder !== void 0 && folderReachesOwnFiles(request2.folder, own)) return false;
5839
+ if (namesOwnFiles(words, own, request2.folder)) return false;
5840
+ return wordsMatch(wanted, words);
5841
+ }
5842
+ function decideByRules(rules, request2) {
5843
+ for (const decision of ORDER) {
5844
+ const matches = decision === "allow" ? lets : holdsBack;
5845
+ const rule = rules.find(
5846
+ (candidate) => candidate.decision === decision && matches(candidate, request2)
5847
+ );
5848
+ if (rule !== void 0) return { decision, rule };
5849
+ }
5850
+ return null;
5851
+ }
5852
+
5853
+ // src/collector/answers/ruleAnswers.ts
5854
+ function ruleAnsweredEvent(session, sessionId, tool, verdict, at) {
5855
+ const { rule } = verdict;
5856
+ return {
5857
+ id: `${sessionId}@${at}:answered`,
5858
+ at,
5859
+ sessionId,
5860
+ sessionName: session?.name ?? sessionId,
5861
+ kind: "answered",
5862
+ ...session && { from: session.status },
5863
+ severity: "advisory",
5864
+ by: "agent-lookout",
5865
+ decision: verdict.decision,
5866
+ tool,
5867
+ rule: { tool: rule.tool, ...rule.command !== void 0 && { command: rule.command } }
5868
+ };
5869
+ }
5870
+ function createRuleAnswers(options) {
5871
+ const { poller, events } = options;
5872
+ const now = options.now ?? Date.now;
5873
+ let recent = [];
5874
+ return {
5875
+ verdictFor({ sessionId, shown }) {
5876
+ const rules = options.rules();
5877
+ if (rules.length === 0) return null;
5878
+ const folder = poller.getSnapshot().sessions.find((listed) => listed.id === sessionId)?.cwd;
5879
+ const verdict = decideByRules(rules, {
5880
+ tool: shown.tool,
5881
+ ...shown.command !== void 0 && { command: shown.command },
5882
+ inputNames: shown.command === void 0 ? [] : (shown.inputs ?? []).map((input) => input.name),
5883
+ // The one rule for what may be allowed: what the dashboard would offer Allow for.
5884
+ allowable: shown.allow,
5885
+ ownPaths: options.ownPaths ?? [],
5886
+ ...folder && { folder }
5887
+ });
5888
+ if (verdict === null || verdict.decision === "ask") return null;
5889
+ return { decision: verdict.decision, rule: verdict.rule };
5890
+ },
5891
+ answered({ sessionId, shown }, verdict) {
5892
+ const at = now();
5893
+ const session = poller.getSnapshot().sessions.find((listed) => listed.id === sessionId) ?? null;
5894
+ const event = ruleAnsweredEvent(session, sessionId, shown.tool, verdict, at);
5895
+ events.add([event]);
5896
+ const { id: _id, ...rule } = verdict.rule;
5897
+ recent = [
5898
+ {
5899
+ at,
5900
+ sessionId,
5901
+ sessionName: event.sessionName,
5902
+ tool: shown.tool,
5903
+ decision: verdict.decision,
5904
+ rule
5905
+ },
5906
+ ...recent
5907
+ ].slice(0, MAX_RULE_ANSWERS);
5908
+ void poller.pollOnce().catch(() => {
5909
+ });
5910
+ },
5911
+ recent: () => [...recent]
5912
+ };
5913
+ }
5914
+
5035
5915
  // src/collector/answers/answering.ts
5036
5916
  function createAnswering(options) {
5037
5917
  const { env, poller, events } = options;
@@ -5046,14 +5926,26 @@ function createAnswering(options) {
5046
5926
  },
5047
5927
  serve: (snapshot) => ({ ...snapshot, answering: off }),
5048
5928
  observe: () => {
5049
- }
5929
+ },
5930
+ answeredIn: () => /* @__PURE__ */ new Set(),
5931
+ ruleAnswers: () => []
5050
5932
  };
5051
5933
  }
5934
+ const ruleAnswers = createRuleAnswers({
5935
+ rules: options.rules ?? (() => []),
5936
+ // A command that names the settings file or the socket could change the
5937
+ // rules or answer prompts, so no rule allows it, wherever they are.
5938
+ ownPaths: [options.settingsFile ?? "", setup.socketPath],
5939
+ poller,
5940
+ events,
5941
+ now: options.now
5942
+ });
5052
5943
  const asks = createHeldAsks({
5053
5944
  status: options.status ?? createRegistryStatus({ snapshot: poller.getSnapshot, env }),
5054
5945
  holdMs: setup.holdMs,
5055
5946
  now: options.now,
5056
- every: options.every
5947
+ every: options.every,
5948
+ rules: ruleAnswers
5057
5949
  });
5058
5950
  const socket = createHookSocket({
5059
5951
  socketPath: setup.socketPath,
@@ -5062,6 +5954,7 @@ function createAnswering(options) {
5062
5954
  });
5063
5955
  let problem = null;
5064
5956
  let listening = false;
5957
+ const answerRoute = createAnswerRoute({ poller, events, asks, now: options.now });
5065
5958
  return {
5066
5959
  async start() {
5067
5960
  const started = await socket.start();
@@ -5085,7 +5978,16 @@ function createAnswering(options) {
5085
5978
  observe(snapshot) {
5086
5979
  if (listening) asks.observe(snapshot);
5087
5980
  },
5088
- route: createAnswerRoute({ poller, events, asks, now: options.now })
5981
+ answeredIn: (snapshot) => asks.answeredIn(snapshot),
5982
+ // A socket that could not be opened holds no request, so there is nothing
5983
+ // to answer. Past the route's own checks, a POST is told what it would be
5984
+ // told with answering off, where there is no such route.
5985
+ async route(req) {
5986
+ if (problem === null) return answerRoute(req);
5987
+ req.resume();
5988
+ return answerRefusalFor(req) ?? refusal(405, "This address only answers GET requests.", { Allow: "GET" });
5989
+ },
5990
+ ruleAnswers: () => ruleAnswers.recent()
5089
5991
  };
5090
5992
  }
5091
5993
 
@@ -5221,8 +6123,8 @@ function waitsOf(input) {
5221
6123
  names.set(sessionId, event.sessionName);
5222
6124
  }
5223
6125
  if (began !== null) {
5224
- const open4 = waitingNow.has(sessionId);
5225
- waits.push({ sessionId, from: began, to: open4 || began >= thisRun ? now : thisRun, open: open4 });
6126
+ const open2 = waitingNow.has(sessionId);
6127
+ waits.push({ sessionId, from: began, to: open2 || began >= thisRun ? now : thisRun, open: open2 });
5226
6128
  unended.add(sessionId);
5227
6129
  }
5228
6130
  }
@@ -5314,26 +6216,26 @@ function waitTotals(input) {
5314
6216
 
5315
6217
  // src/collector/adapters/codex/index.ts
5316
6218
  import os5 from "node:os";
5317
- import path14 from "node:path";
6219
+ import path12 from "node:path";
5318
6220
 
5319
6221
  // src/core/mapping/codexMapping.ts
5320
6222
  var TURN_UNDER_WAY = /* @__PURE__ */ new Set(["task_started", "turn_started"]);
5321
6223
  var TURN_OVER = /* @__PURE__ */ new Set(["task_complete", "turn_complete", "turn_aborted"]);
5322
6224
  function mapCodexStatus(fields) {
5323
6225
  const { lastTurn, live: live2 } = fields;
5324
- let open4;
6226
+ let open2;
5325
6227
  if (lastTurn === null) {
5326
- open4 = "idle";
6228
+ open2 = "idle";
5327
6229
  } else if (typeof lastTurn !== "string") {
5328
6230
  return "unknown";
5329
6231
  } else if (TURN_UNDER_WAY.has(lastTurn)) {
5330
- open4 = "working";
6232
+ open2 = "working";
5331
6233
  } else if (TURN_OVER.has(lastTurn)) {
5332
- open4 = "idle";
6234
+ open2 = "idle";
5333
6235
  } else {
5334
6236
  return "unknown";
5335
6237
  }
5336
- return live2 === false ? "finished" : open4;
6238
+ return live2 === false ? "finished" : open2;
5337
6239
  }
5338
6240
  var WRITER_LOCKS_SINCE = [0, 155, 0];
5339
6241
  var VERSION = /^(\d{1,6})\.(\d{1,6})\.(\d{1,6})(-[0-9A-Za-z.-]{1,64})?(?:\+[0-9A-Za-z.-]{1,64})?$/;
@@ -5357,7 +6259,7 @@ var IMPORTED_TURN_ID = /^external-import-turn-\d{1,9}$/;
5357
6259
  function isImportedCodexTurn(turnId) {
5358
6260
  return typeof turnId === "string" && IMPORTED_TURN_ID.test(turnId);
5359
6261
  }
5360
- function isRecord7(value) {
6262
+ function isRecord8(value) {
5361
6263
  return typeof value === "object" && value !== null && !Array.isArray(value);
5362
6264
  }
5363
6265
  var CODEX_DESKTOP_ORIGINATOR = "Codex Desktop";
@@ -5365,7 +6267,7 @@ function mapCodexSurface(source, originator) {
5365
6267
  if (originator === CODEX_DESKTOP_ORIGINATOR) return "desktop";
5366
6268
  if (source === "cli" || source === "exec") return "terminal";
5367
6269
  if (source === "vscode") return "vscode";
5368
- if (isRecord7(source) && source.custom === "chatgpt") return "desktop";
6270
+ if (isRecord8(source) && source.custom === "chatgpt") return "desktop";
5369
6271
  return "unknown";
5370
6272
  }
5371
6273
  var NOT_A_SESSION_SOURCE = /* @__PURE__ */ new Set(["subagent", "internal", "mcp"]);
@@ -5376,7 +6278,7 @@ function isCodexSessionSource(source, meta = {}) {
5376
6278
  return false;
5377
6279
  }
5378
6280
  if (typeof source === "string") return !NOT_A_SESSION_SOURCE.has(source);
5379
- if (isRecord7(source)) return !Object.keys(source).some((kind) => NOT_A_SESSION_SOURCE.has(kind));
6281
+ if (isRecord8(source)) return !Object.keys(source).some((kind) => NOT_A_SESSION_SOURCE.has(kind));
5380
6282
  return true;
5381
6283
  }
5382
6284
 
@@ -5395,7 +6297,7 @@ function parseCodexTime(value) {
5395
6297
  var LINE_TIME = /^\{"timestamp":"([^"]{1,64})"/;
5396
6298
  var TURN_HINT = /"type":"(?:task|turn)_/;
5397
6299
  var TURN_TYPE = /^(?:task|turn)_/;
5398
- function isRecord8(value) {
6300
+ function isRecord9(value) {
5399
6301
  return typeof value === "object" && value !== null && !Array.isArray(value);
5400
6302
  }
5401
6303
  function text3(value) {
@@ -5406,7 +6308,7 @@ function label(value) {
5406
6308
  }
5407
6309
  function compactSource(value) {
5408
6310
  if (typeof value === "string") return text3(value);
5409
- if (!isRecord8(value)) return void 0;
6311
+ if (!isRecord9(value)) return void 0;
5410
6312
  return Object.fromEntries(
5411
6313
  Object.entries(value).map(([kind, detail]) => [
5412
6314
  kind,
@@ -5417,7 +6319,7 @@ function compactSource(value) {
5417
6319
  function parse(line) {
5418
6320
  try {
5419
6321
  const value = JSON.parse(line);
5420
- return isRecord8(value) ? value : null;
6322
+ return isRecord9(value) ? value : null;
5421
6323
  } catch {
5422
6324
  return null;
5423
6325
  }
@@ -5425,7 +6327,7 @@ function parse(line) {
5425
6327
  function readMetaLine(line) {
5426
6328
  if (!line.includes('"session_meta"')) return null;
5427
6329
  const value = parse(line);
5428
- if (!value || value.type !== "session_meta" || !isRecord8(value.payload)) return null;
6330
+ if (!value || value.type !== "session_meta" || !isRecord9(value.payload)) return null;
5429
6331
  const payload = value.payload;
5430
6332
  const meta = {
5431
6333
  id: text3(payload.id),
@@ -5445,7 +6347,7 @@ function readMetaLine(line) {
5445
6347
  function readTurnLine(line) {
5446
6348
  if (!line.includes('"event_msg"') || !TURN_HINT.test(line)) return null;
5447
6349
  const value = parse(line);
5448
- if (!value || value.type !== "event_msg" || !isRecord8(value.payload)) return null;
6350
+ if (!value || value.type !== "event_msg" || !isRecord9(value.payload)) return null;
5449
6351
  const turn = value.payload.type;
5450
6352
  if (typeof turn !== "string" || !TURN_TYPE.test(turn)) return null;
5451
6353
  return {
@@ -5631,7 +6533,7 @@ function createRolloutReader(io) {
5631
6533
  }
5632
6534
 
5633
6535
  // src/collector/adapters/codex/rollouts.ts
5634
- import path13 from "node:path";
6536
+ import path11 from "node:path";
5635
6537
  var UUID = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
5636
6538
  var ROLLOUT_FILE_NAME = new RegExp(
5637
6539
  `^rollout-\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-(${UUID})(?:_(${UUID}))?\\.jsonl$`,
@@ -5649,7 +6551,7 @@ function twoDigits(value) {
5649
6551
  function recentDayFolders(sessionsDir, now) {
5650
6552
  const folders = [now, now - 24 * 60 * 60 * 1e3].map((at) => {
5651
6553
  const date = new Date(at);
5652
- return path13.join(
6554
+ return path11.join(
5653
6555
  sessionsDir,
5654
6556
  String(date.getFullYear()),
5655
6557
  twoDigits(date.getMonth() + 1),
@@ -5678,12 +6580,12 @@ function createRolloutFinder(options) {
5678
6580
  const refs = [];
5679
6581
  for (const name of (await names(dir)).sort()) {
5680
6582
  const threadId = threadIdOfRollout(name);
5681
- if (threadId !== null) refs.push({ path: path13.join(dir, name), threadId });
6583
+ if (threadId !== null) refs.push({ path: path11.join(dir, name), threadId });
5682
6584
  }
5683
6585
  return refs;
5684
6586
  }
5685
6587
  async function subfolders(dir, pattern) {
5686
- return (await names(dir)).filter((name) => pattern.test(name)).sort().map((name) => path13.join(dir, name));
6588
+ return (await names(dir)).filter((name) => pattern.test(name)).sort().map((name) => path11.join(dir, name));
5687
6589
  }
5688
6590
  async function buildIndex() {
5689
6591
  const years = await subfolders(sessionsDir, YEAR);
@@ -5727,7 +6629,7 @@ function createRolloutFinder(options) {
5727
6629
  // src/collector/adapters/codex/sessionIndex.ts
5728
6630
  var SESSION_INDEX_FILE = "session_index.jsonl";
5729
6631
  var SESSION_INDEX_LIMIT_BYTES = 4 * 1024 * 1024;
5730
- function isRecord9(value) {
6632
+ function isRecord10(value) {
5731
6633
  return typeof value === "object" && value !== null && !Array.isArray(value);
5732
6634
  }
5733
6635
  function parseSessionIndex(content) {
@@ -5740,7 +6642,7 @@ function parseSessionIndex(content) {
5740
6642
  } catch {
5741
6643
  continue;
5742
6644
  }
5743
- if (!isRecord9(value)) continue;
6645
+ if (!isRecord10(value)) continue;
5744
6646
  const { id, thread_name: name } = value;
5745
6647
  if (typeof id !== "string" || id === "" || typeof name !== "string") continue;
5746
6648
  const trimmed = name.trim();
@@ -5780,7 +6682,7 @@ function createSessionIndexReader(file, io) {
5780
6682
  }
5781
6683
  names = await readNames();
5782
6684
  } catch (error) {
5783
- if (isMissing(error)) {
6685
+ if (isMissing2(error)) {
5784
6686
  seen = null;
5785
6687
  names = /* @__PURE__ */ new Map();
5786
6688
  }
@@ -5844,14 +6746,14 @@ async function readWriterLocks(dir, io) {
5844
6746
  try {
5845
6747
  names = await io.readdir(dir);
5846
6748
  } catch (error) {
5847
- return { supported: false, missing: isMissing(error) };
6749
+ return { supported: false, missing: isMissing2(error) };
5848
6750
  }
5849
- const open4 = /* @__PURE__ */ new Set();
6751
+ const open2 = /* @__PURE__ */ new Set();
5850
6752
  for (const name of names) {
5851
6753
  const threadId = threadIdOfLock(name);
5852
- if (threadId !== null) open4.add(threadId);
6754
+ if (threadId !== null) open2.add(threadId);
5853
6755
  }
5854
- return { supported: true, open: open4 };
6756
+ return { supported: true, open: open2 };
5855
6757
  }
5856
6758
 
5857
6759
  // src/collector/adapters/codex/index.ts
@@ -5889,7 +6791,7 @@ var CODEX_CAPABILITIES = {
5889
6791
  },
5890
6792
  answer: {
5891
6793
  level: "no",
5892
- reason: "Codex records no approval waits, so there is nothing to answer from here."
6794
+ reason: "Codex records no approval waits, so there is nothing to answer from here, by hand or by a permission rule."
5893
6795
  }
5894
6796
  };
5895
6797
  function createCodexAdapter(options = {}) {
@@ -5902,10 +6804,10 @@ function createCodexAdapter(options = {}) {
5902
6804
  const ownOverride = env[CODEX_HOME_ENV]?.trim() || void 0;
5903
6805
  const codexOverride = env[CODEX_OWN_HOME_ENV]?.trim() || void 0;
5904
6806
  const homeFrom = ownOverride ? "agent-lookout" : codexOverride ? "codex" : "default";
5905
- const codexHome = path14.resolve(ownOverride ?? codexOverride ?? path14.join(homeDir, ".codex"));
5906
- const sessionsDir = path14.join(codexHome, "sessions");
5907
- const locksDir = path14.join(codexHome, WRITER_LOCK_DIR);
5908
- const indexFile = path14.join(codexHome, SESSION_INDEX_FILE);
6807
+ const codexHome = path12.resolve(ownOverride ?? codexOverride ?? path12.join(homeDir, ".codex"));
6808
+ const sessionsDir = path12.join(codexHome, "sessions");
6809
+ const locksDir = path12.join(codexHome, WRITER_LOCK_DIR);
6810
+ const indexFile = path12.join(codexHome, SESSION_INDEX_FILE);
5909
6811
  const homeName = tildify(codexHome, homeDir);
5910
6812
  const sessionsName = tildify(sessionsDir, homeDir);
5911
6813
  const locksName = tildify(locksDir, homeDir);
@@ -5953,7 +6855,7 @@ function createCodexAdapter(options = {}) {
5953
6855
  try {
5954
6856
  return (await io.stat(codexHome)).kind === "directory";
5955
6857
  } catch (error) {
5956
- return !isMissing(error);
6858
+ return !isMissing2(error);
5957
6859
  }
5958
6860
  }
5959
6861
  async function poll() {
@@ -5975,7 +6877,7 @@ function createCodexAdapter(options = {}) {
5975
6877
  try {
5976
6878
  await io.readdir(sessionsDir);
5977
6879
  } catch (error) {
5978
- if (!isMissing(error)) {
6880
+ if (!isMissing2(error)) {
5979
6881
  return result(
5980
6882
  "error",
5981
6883
  `Codex sessions could not be read: the folder ${sessionsName} could not be listed.`,
@@ -6023,7 +6925,7 @@ function createCodexAdapter(options = {}) {
6023
6925
  const chosen = /* @__PURE__ */ new Map();
6024
6926
  const subagentWrites = /* @__PURE__ */ new Map();
6025
6927
  for (const ref of [...candidates.values()].sort((a, b) => a.path < b.path ? -1 : 1)) {
6026
- const open4 = locked(ref.threadId);
6928
+ const open2 = locked(ref.threadId);
6027
6929
  let info;
6028
6930
  try {
6029
6931
  info = await io.lstat(ref.path);
@@ -6031,7 +6933,7 @@ function createCodexAdapter(options = {}) {
6031
6933
  tracked.delete(ref.path);
6032
6934
  continue;
6033
6935
  }
6034
- if (info.kind !== "file" || !open4 && info.mtimeMs < cutoff) {
6936
+ if (info.kind !== "file" || !open2 && info.mtimeMs < cutoff) {
6035
6937
  tracked.delete(ref.path);
6036
6938
  continue;
6037
6939
  }
@@ -6054,7 +6956,7 @@ function createCodexAdapter(options = {}) {
6054
6956
  }
6055
6957
  if (state.lastTurnImported) continue;
6056
6958
  const at = lastActivity(state, checkedAt);
6057
- if (!open4 && (at === null || !isWithinRetention(at, checkedAt))) {
6959
+ if (!open2 && (at === null || !isWithinRetention(at, checkedAt))) {
6058
6960
  tracked.delete(ref.path);
6059
6961
  continue;
6060
6962
  }
@@ -6113,7 +7015,7 @@ function createCodexAdapter(options = {}) {
6113
7015
 
6114
7016
  // src/collector/adapters/status-files/index.ts
6115
7017
  import os6 from "node:os";
6116
- import path15 from "node:path";
7018
+ import path13 from "node:path";
6117
7019
 
6118
7020
  // src/core/mapping/statusFileMapping.ts
6119
7021
  var STATUS = /* @__PURE__ */ new Map([
@@ -6248,7 +7150,7 @@ var STATUS_FILE_CAPABILITIES = {
6248
7150
  },
6249
7151
  answer: {
6250
7152
  level: "no",
6251
- reason: "A status file only says a session waits, and holds nothing to answer it through."
7153
+ reason: "A status file only says a session waits, and holds nothing to answer it through, by hand or by a permission rule."
6252
7154
  }
6253
7155
  };
6254
7156
  function counted(count2, one, many) {
@@ -6262,7 +7164,7 @@ function createStatusFileAdapter(options = {}) {
6262
7164
  const isAlive = options.isAlive ?? isProcessAlive;
6263
7165
  const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
6264
7166
  const override = env[STATUS_DIR_ENV]?.trim() || void 0;
6265
- const dir = path15.resolve(override ?? path15.join(homeDir, ".agent-lookout", "sessions"));
7167
+ const dir = path13.resolve(override ?? path13.join(homeDir, ".agent-lookout", "sessions"));
6266
7168
  const dirName = tildify(dir, homeDir);
6267
7169
  const decoder2 = new TextDecoder();
6268
7170
  let lastRead = /* @__PURE__ */ new Map();
@@ -6295,26 +7197,26 @@ function createStatusFileAdapter(options = {}) {
6295
7197
  return { health, sessions: [] };
6296
7198
  }
6297
7199
  async function readOne(file) {
6298
- let open4;
7200
+ let open2;
6299
7201
  try {
6300
- open4 = await io.openRegular(file);
7202
+ open2 = await io.openRegular(file);
6301
7203
  } catch (error) {
6302
- if (isMissing(error)) return { kind: "gone" };
7204
+ if (isMissing2(error)) return { kind: "gone" };
6303
7205
  const code = error?.code;
6304
7206
  const notAFile = code === void 0 || code === "ELOOP" || code === "EISDIR";
6305
7207
  return { kind: "refused", why: notAFile ? "is not an ordinary file" : "could not be opened" };
6306
7208
  }
6307
7209
  const tooLarge = { kind: "refused", why: "is over 16 KB" };
6308
7210
  try {
6309
- if (open4.info.size > MAX_FILE_BYTES) return tooLarge;
6310
- const bytes2 = await open4.read(0, MAX_FILE_BYTES + 1);
7211
+ if (open2.info.size > MAX_FILE_BYTES) return tooLarge;
7212
+ const bytes2 = await open2.read(0, MAX_FILE_BYTES + 1);
6311
7213
  if (bytes2.byteLength > MAX_FILE_BYTES) return tooLarge;
6312
7214
  const parsed = parseStatusFile(decoder2.decode(bytes2));
6313
- return parsed ? { kind: "read", file: parsed, mtimeMs: open4.info.mtimeMs } : { kind: "invalid" };
7215
+ return parsed ? { kind: "read", file: parsed, mtimeMs: open2.info.mtimeMs } : { kind: "invalid" };
6314
7216
  } catch {
6315
7217
  return { kind: "refused", why: "could not be read" };
6316
7218
  } finally {
6317
- await open4.close().catch(() => {
7219
+ await open2.close().catch(() => {
6318
7220
  });
6319
7221
  }
6320
7222
  }
@@ -6322,7 +7224,7 @@ function createStatusFileAdapter(options = {}) {
6322
7224
  if (names.length <= MAX_FILES) return names;
6323
7225
  const written = await Promise.all(
6324
7226
  names.map(async (name) => {
6325
- const info = await io.lstat(path15.join(dir, name)).catch(() => null);
7227
+ const info = await io.lstat(path13.join(dir, name)).catch(() => null);
6326
7228
  return { name, mtimeMs: info?.mtimeMs ?? -Infinity };
6327
7229
  })
6328
7230
  );
@@ -6342,7 +7244,7 @@ function createStatusFileAdapter(options = {}) {
6342
7244
  try {
6343
7245
  names = await io.readdir(dir);
6344
7246
  } catch (error) {
6345
- if (!isMissing(error)) {
7247
+ if (!isMissing2(error)) {
6346
7248
  const answer = result(
6347
7249
  "error",
6348
7250
  `Status files could not be read: the folder ${dirName} could not be listed.`,
@@ -6372,7 +7274,7 @@ function createStatusFileAdapter(options = {}) {
6372
7274
  const nextRead = /* @__PURE__ */ new Map();
6373
7275
  const nextStatuses = /* @__PURE__ */ new Map();
6374
7276
  for (const fileName2 of await toRead(listed)) {
6375
- const reading = await readOne(path15.join(dir, fileName2));
7277
+ const reading = await readOne(path13.join(dir, fileName2));
6376
7278
  let kept;
6377
7279
  if (reading.kind === "read") {
6378
7280
  kept = reading;
@@ -6469,6 +7371,7 @@ function transportOptions(settings, timeoutMs = MAIL_TIMEOUT_MS) {
6469
7371
  secure: security === "tls",
6470
7372
  requireTLS: security === "starttls",
6471
7373
  ignoreTLS: security === "plain",
7374
+ tls: { rejectUnauthorized: true },
6472
7375
  ...auth ? { auth: { user: auth.user, pass: auth.pass } } : {},
6473
7376
  name: GREETING_NAME,
6474
7377
  connectionTimeout: timeoutMs,
@@ -6497,7 +7400,7 @@ function failureReason(error) {
6497
7400
  return "the mail server refused the email";
6498
7401
  case "ECONNECTION":
6499
7402
  case "ESOCKET":
6500
- if (errno === "ECONNREFUSED" || errno === -61 || errno === -111) {
7403
+ if (errno === "ECONNREFUSED" || errno === -61 || errno === -111 || errno === -4078) {
6501
7404
  return "nothing answered at the mail server's address";
6502
7405
  }
6503
7406
  return "the connection to the mail server failed";
@@ -6598,7 +7501,7 @@ function createEventStore(capacity = EVENT_CAPACITY) {
6598
7501
 
6599
7502
  // src/collector/git/branchFinder.ts
6600
7503
  import os7 from "node:os";
6601
- import path17 from "node:path";
7504
+ import path15 from "node:path";
6602
7505
 
6603
7506
  // src/collector/git/gitHead.ts
6604
7507
  var MAX_GIT_FILE_BYTES = 4 * 1024;
@@ -6638,18 +7541,18 @@ function parseCommonDir(content) {
6638
7541
 
6639
7542
  // src/collector/git/repository.ts
6640
7543
  import { createHash as createHash2 } from "node:crypto";
6641
- import path16 from "node:path";
7544
+ import path14 from "node:path";
6642
7545
  var REPOSITORY_ID_LENGTH = 16;
6643
7546
  function repositoryId(gitFolder) {
6644
7547
  return createHash2("sha256").update(gitFolder).digest("hex").slice(0, REPOSITORY_ID_LENGTH);
6645
7548
  }
6646
7549
  function sharedName(common) {
6647
- const base = path16.basename(common);
6648
- if (base.startsWith(".")) return path16.basename(path16.dirname(common));
7550
+ const base = path14.basename(common);
7551
+ if (base.startsWith(".")) return path14.basename(path14.dirname(common));
6649
7552
  return base.endsWith(".git") ? base.slice(0, -".git".length) : base;
6650
7553
  }
6651
7554
  function repositoryOf({ dir, gitDir, commonDir }) {
6652
- const name = sessionName(commonDir === null ? path16.basename(dir) : sharedName(commonDir));
7555
+ const name = sessionName(commonDir === null ? path14.basename(dir) : sharedName(commonDir));
6653
7556
  return name === void 0 ? null : { id: repositoryId(commonDir ?? gitDir), name };
6654
7557
  }
6655
7558
 
@@ -6659,32 +7562,32 @@ var MAX_LEVELS = 24;
6659
7562
  var READ_WAIT_MS = 1e3;
6660
7563
  var decoder = new TextDecoder("utf-8", { fatal: true });
6661
7564
  async function readGitFile(io, file, maxBytes = MAX_GIT_FILE_BYTES) {
6662
- let open4;
7565
+ let open2;
6663
7566
  try {
6664
- open4 = await io.openRegular(file);
7567
+ open2 = await io.openRegular(file);
6665
7568
  } catch (error) {
6666
- return isMissing(error) ? void 0 : null;
7569
+ return isMissing2(error) ? void 0 : null;
6667
7570
  }
6668
7571
  try {
6669
- if (open4.info.size > maxBytes) return null;
6670
- const bytes2 = await open4.read(0, maxBytes + 1);
7572
+ if (open2.info.size > maxBytes) return null;
7573
+ const bytes2 = await open2.read(0, maxBytes + 1);
6671
7574
  if (bytes2.byteLength > maxBytes) return null;
6672
7575
  return decoder.decode(bytes2);
6673
7576
  } catch {
6674
7577
  return null;
6675
7578
  } finally {
6676
- await open4.close().catch(() => {
7579
+ await open2.close().catch(() => {
6677
7580
  });
6678
7581
  }
6679
7582
  }
6680
7583
  async function commonDirOf(io, gitDir) {
6681
- const content = await readGitFile(io, path17.join(gitDir, "commondir"));
7584
+ const content = await readGitFile(io, path15.join(gitDir, "commondir"));
6682
7585
  if (content === void 0) return null;
6683
7586
  const target = content === null ? null : parseCommonDir(content);
6684
- return target === null ? void 0 : path17.resolve(gitDir, target);
7587
+ return target === null ? void 0 : path15.resolve(gitDir, target);
6685
7588
  }
6686
7589
  async function headIn(dir, kind, io) {
6687
- const marker = path17.join(dir, ".git");
7590
+ const marker = path15.join(dir, ".git");
6688
7591
  let gitDir;
6689
7592
  if (kind === "directory") {
6690
7593
  gitDir = marker;
@@ -6692,11 +7595,11 @@ async function headIn(dir, kind, io) {
6692
7595
  const content2 = await readGitFile(io, marker);
6693
7596
  const target = typeof content2 === "string" ? parseGitFile(content2) : null;
6694
7597
  if (target === null) return null;
6695
- gitDir = path17.resolve(dir, target);
7598
+ gitDir = path15.resolve(dir, target);
6696
7599
  } else {
6697
7600
  return null;
6698
7601
  }
6699
- const content = await readGitFile(io, path17.join(gitDir, "HEAD"));
7602
+ const content = await readGitFile(io, path15.join(gitDir, "HEAD"));
6700
7603
  const head = typeof content === "string" ? parseHead(content) : null;
6701
7604
  if (head === null) return null;
6702
7605
  const commonDir = kind === "file" ? await commonDirOf(io, gitDir) : null;
@@ -6705,22 +7608,22 @@ async function headIn(dir, kind, io) {
6705
7608
  return { head: { ...head, repository }, gitFolder: commonDir ?? gitDir };
6706
7609
  }
6707
7610
  async function findHead(cwd, { io, homeDir }) {
6708
- if (!path17.isAbsolute(cwd)) return null;
6709
- const home = path17.resolve(homeDir);
6710
- let dir = path17.resolve(cwd);
7611
+ if (!path15.isAbsolute(cwd)) return null;
7612
+ const home = path15.resolve(homeDir);
7613
+ let dir = path15.resolve(cwd);
6711
7614
  try {
6712
7615
  if ((await io.stat(dir)).kind !== "directory") return null;
6713
7616
  } catch {
6714
7617
  return null;
6715
7618
  }
6716
7619
  for (let level = 0; level < MAX_LEVELS; level += 1) {
6717
- const parent = path17.dirname(dir);
7620
+ const parent = path15.dirname(dir);
6718
7621
  if (dir === home || dir === parent) return null;
6719
7622
  let kind;
6720
7623
  try {
6721
- kind = (await io.lstat(path17.join(dir, ".git"))).kind;
7624
+ kind = (await io.lstat(path15.join(dir, ".git"))).kind;
6722
7625
  } catch (error) {
6723
- if (!isMissing(error)) return null;
7626
+ if (!isMissing2(error)) return null;
6724
7627
  dir = parent;
6725
7628
  continue;
6726
7629
  }
@@ -6801,14 +7704,14 @@ var STATUS_STATE = {
6801
7704
  PENDING: "pending",
6802
7705
  EXPECTED: "pending"
6803
7706
  };
6804
- function isRecord10(value) {
7707
+ function isRecord11(value) {
6805
7708
  return typeof value === "object" && value !== null && !Array.isArray(value);
6806
7709
  }
6807
7710
  function textOf(value) {
6808
7711
  return typeof value === "string" ? value : "";
6809
7712
  }
6810
7713
  function checkOf(value) {
6811
- if (!isRecord10(value)) return null;
7714
+ if (!isRecord11(value)) return null;
6812
7715
  const startedAt = Date.parse(textOf(value.startedAt));
6813
7716
  const at = Number.isNaN(startedAt) ? -Infinity : startedAt;
6814
7717
  if (value.__typename === "CheckRun") {
@@ -6857,7 +7760,7 @@ function readGhAnswer(stdout, repository) {
6857
7760
  } catch {
6858
7761
  return null;
6859
7762
  }
6860
- if (!isRecord10(data)) return null;
7763
+ if (!isRecord11(data)) return null;
6861
7764
  const { number } = data;
6862
7765
  const state = stateOf2(data.state, data.isDraft);
6863
7766
  const title = typeof data.title === "string" ? sessionName(data.title) : void 0;
@@ -6881,8 +7784,9 @@ var GH_LOCATIONS = [
6881
7784
  "/usr/bin/gh"
6882
7785
  ];
6883
7786
  var GH_FIELDS = "number,title,state,isDraft,statusCheckRollup";
6884
- async function findGhBinary(env, isExecutable = isExecutableFile, locations = GH_LOCATIONS) {
6885
- const candidates = [.../* @__PURE__ */ new Set([...pathCandidates(env, "gh"), ...locations])];
7787
+ async function findGhBinary(env, isExecutable, locations = GH_LOCATIONS, platform = process.platform) {
7788
+ isExecutable ??= (candidate) => isExecutableFile(candidate, platform);
7789
+ const candidates = [.../* @__PURE__ */ new Set([...pathCandidates(env, "gh", platform), ...locations])];
6886
7790
  for (const candidate of candidates) {
6887
7791
  if (await isExecutable(candidate)) return candidate;
6888
7792
  }
@@ -6995,7 +7899,7 @@ function createGhAsker(options) {
6995
7899
  }
6996
7900
 
6997
7901
  // src/collector/git/pullRequestTarget.ts
6998
- import path18 from "node:path";
7902
+ import path16 from "node:path";
6999
7903
 
7000
7904
  // src/collector/git/gitConfig.ts
7001
7905
  var MAX_CONFIG_BYTES = 64 * 1024;
@@ -7253,7 +8157,7 @@ function targetOf(config, branch, base, defaultBranch) {
7253
8157
  async function readPullRequestTarget(gitFolder, branch, io = nodeIo2) {
7254
8158
  try {
7255
8159
  if (!isAskableBranch(branch)) return null;
7256
- const text4 = await readGitFile(io, path18.join(gitFolder, "config"), MAX_CONFIG_BYTES);
8160
+ const text4 = await readGitFile(io, path16.join(gitFolder, "config"), MAX_CONFIG_BYTES);
7257
8161
  if (typeof text4 !== "string") return null;
7258
8162
  const config = parseGitConfig(text4);
7259
8163
  const base = config === null ? null : baseRemoteOf(config);
@@ -7262,7 +8166,7 @@ async function readPullRequestTarget(gitFolder, branch, io = nodeIo2) {
7262
8166
  if (base.name !== null && isSafeRemoteName(base.name)) {
7263
8167
  const head = await readGitFile(
7264
8168
  io,
7265
- path18.join(gitFolder, "refs", "remotes", base.name, "HEAD")
8169
+ path16.join(gitFolder, "refs", "remotes", base.name, "HEAD")
7266
8170
  );
7267
8171
  if (typeof head === "string") defaultBranch = parseRemoteHead(head, base.name);
7268
8172
  }
@@ -7433,40 +8337,38 @@ function createClearHistoryRoute(options) {
7433
8337
  }
7434
8338
 
7435
8339
  // src/collector/history/historyKeeper.ts
7436
- import path19 from "node:path";
8340
+ import path17 from "node:path";
7437
8341
 
7438
8342
  // src/collector/history/historyFiles.ts
7439
8343
  import {
7440
- closeSync,
7441
- constants as constants4,
7442
- fstatSync,
7443
- lstatSync,
7444
- openSync,
8344
+ closeSync as closeSync2,
8345
+ constants as constants5,
8346
+ fstatSync as fstatSync2,
8347
+ lstatSync as lstatSync2,
7445
8348
  readSync,
7446
8349
  unlinkSync,
7447
8350
  writeSync
7448
8351
  } from "node:fs";
7449
- import { lstat as lstat3, lutimes, mkdir as mkdir2, open as open3, readdir as readdir3, unlink as unlink2 } from "node:fs/promises";
8352
+ import { lstat as lstat4, lutimes, mkdir as mkdir2, readdir as readdir3, unlink as unlink2 } from "node:fs/promises";
7450
8353
  function infoOf2(info) {
7451
8354
  return { kind: info.isFile() ? "file" : "other", size: info.size, mtimeMs: info.mtimeMs };
7452
8355
  }
7453
8356
  var FILE_MODE = 384;
7454
8357
  var FOLDER_MODE = 448;
7455
- var NO_FOLLOW = (constants4.O_NOFOLLOW ?? 0) | (constants4.O_NONBLOCK ?? 0);
7456
- var READ = constants4.O_RDONLY | NO_FOLLOW;
7457
- var APPEND = constants4.O_WRONLY | constants4.O_APPEND | constants4.O_CREAT | NO_FOLLOW;
7458
- var CREATE = constants4.O_WRONLY | constants4.O_CREAT | constants4.O_EXCL | NO_FOLLOW;
7459
- function notOrdinary() {
8358
+ var READ = constants5.O_RDONLY;
8359
+ var APPEND = constants5.O_WRONLY | constants5.O_APPEND | constants5.O_CREAT;
8360
+ var CREATE = constants5.O_WRONLY | constants5.O_CREAT | constants5.O_EXCL;
8361
+ function notOrdinary2() {
7460
8362
  return new Error("Not an ordinary file.");
7461
8363
  }
7462
- function isMissing2(error) {
8364
+ function isMissing3(error) {
7463
8365
  return error?.code === "ENOENT";
7464
8366
  }
7465
8367
  async function readRegular(file, maxBytes) {
7466
- const handle = await open3(file, READ);
8368
+ const handle = await openWithoutFollowing(file, READ);
7467
8369
  try {
7468
8370
  const info = await handle.stat();
7469
- if (!info.isFile()) throw notOrdinary();
8371
+ if (!info.isFile()) throw notOrdinary2();
7470
8372
  if (info.size > maxBytes) throw new Error("Too large to be a history file.");
7471
8373
  const buffer = Buffer.alloc(info.size);
7472
8374
  let filled = 0;
@@ -7481,10 +8383,10 @@ async function readRegular(file, maxBytes) {
7481
8383
  }
7482
8384
  }
7483
8385
  function readRegularNow(file, maxBytes) {
7484
- const fd = openSync(file, READ);
8386
+ const fd = openWithoutFollowingNow(file, READ);
7485
8387
  try {
7486
- const info = fstatSync(fd);
7487
- if (!info.isFile()) throw notOrdinary();
8388
+ const info = fstatSync2(fd);
8389
+ if (!info.isFile()) throw notOrdinary2();
7488
8390
  if (info.size > maxBytes) throw new Error("Too large to be a history file.");
7489
8391
  const buffer = Buffer.alloc(info.size);
7490
8392
  let filled = 0;
@@ -7495,22 +8397,22 @@ function readRegularNow(file, maxBytes) {
7495
8397
  }
7496
8398
  return buffer.toString("utf8", 0, filled);
7497
8399
  } finally {
7498
- closeSync(fd);
8400
+ closeSync2(fd);
7499
8401
  }
7500
8402
  }
7501
8403
  function writeNow(file, flags, text4) {
7502
- const fd = openSync(file, flags, FILE_MODE);
8404
+ const fd = openWithoutFollowingNow(file, flags, FILE_MODE);
7503
8405
  try {
7504
- if (!fstatSync(fd).isFile()) throw notOrdinary();
8406
+ if (!fstatSync2(fd).isFile()) throw notOrdinary2();
7505
8407
  writeSync(fd, text4);
7506
8408
  } finally {
7507
- closeSync(fd);
8409
+ closeSync2(fd);
7508
8410
  }
7509
8411
  }
7510
8412
  async function writeTo(file, flags, text4) {
7511
- const handle = await open3(file, flags, FILE_MODE);
8413
+ const handle = await openWithoutFollowing(file, flags, FILE_MODE);
7512
8414
  try {
7513
- if (!(await handle.stat()).isFile()) throw notOrdinary();
8415
+ if (!(await handle.stat()).isFile()) throw notOrdinary2();
7514
8416
  await handle.writeFile(text4, "utf8");
7515
8417
  } finally {
7516
8418
  await handle.close();
@@ -7521,8 +8423,8 @@ var nodeHistoryFs = {
7521
8423
  await mkdir2(dir, { recursive: true, mode: FOLDER_MODE });
7522
8424
  },
7523
8425
  list: (dir) => readdir3(dir),
7524
- lstat: async (file) => infoOf2(await lstat3(file)),
7525
- lstatNow: (file) => infoOf2(lstatSync(file)),
8426
+ lstat: async (file) => infoOf2(await lstat4(file)),
8427
+ lstatNow: (file) => infoOf2(lstatSync2(file)),
7526
8428
  readRegular,
7527
8429
  readRegularNow,
7528
8430
  append: (file, text4) => writeTo(file, APPEND, text4),
@@ -7537,14 +8439,14 @@ var nodeHistoryFs = {
7537
8439
  try {
7538
8440
  await unlink2(file);
7539
8441
  } catch (error) {
7540
- if (!isMissing2(error)) throw error;
8442
+ if (!isMissing3(error)) throw error;
7541
8443
  }
7542
8444
  },
7543
8445
  removeNow(file) {
7544
8446
  try {
7545
8447
  unlinkSync(file);
7546
8448
  } catch (error) {
7547
- if (!isMissing2(error)) throw error;
8449
+ if (!isMissing3(error)) throw error;
7548
8450
  }
7549
8451
  }
7550
8452
  };
@@ -7735,7 +8637,7 @@ function createHistoryKeeper(options) {
7735
8637
  });
7736
8638
  const lock = createWriterLock({
7737
8639
  fs,
7738
- file: path19.join(dir, LOCK_FILE),
8640
+ file: path17.join(dir, LOCK_FILE),
7739
8641
  pid: options.pid,
7740
8642
  now,
7741
8643
  isAlive: options.isAlive,
@@ -7761,7 +8663,7 @@ function createHistoryKeeper(options) {
7761
8663
  });
7762
8664
  return next;
7763
8665
  }
7764
- const full = (name) => path19.join(dir, name);
8666
+ const full = (name) => path17.join(dir, name);
7765
8667
  function hold(record) {
7766
8668
  if (unusable !== null || !running) return;
7767
8669
  const line = encodeRecord(record);
@@ -8223,13 +9125,13 @@ function keptHistoryStore(store, keeper) {
8223
9125
  // src/collector/history/historySettings.ts
8224
9126
  import { createHash as createHash3 } from "node:crypto";
8225
9127
  import os8 from "node:os";
8226
- import path20 from "node:path";
9128
+ import path18 from "node:path";
8227
9129
  var HISTORY_ENV = "AGENT_LOOKOUT_HISTORY";
8228
9130
  var HISTORY_DIR_ENV = "AGENT_LOOKOUT_HISTORY_DIR";
8229
9131
  function readHistorySetup(env, homeDir = os8.homedir()) {
8230
9132
  if (env[HISTORY_ENV]?.trim().toLowerCase() === "off") return { on: false };
8231
9133
  const override = env[HISTORY_DIR_ENV]?.trim() || void 0;
8232
- const dir = path20.resolve(override ?? path20.join(homeDir, ".agent-lookout", "history"));
9134
+ const dir = path18.resolve(override ?? path18.join(homeDir, ".agent-lookout", "history"));
8233
9135
  return {
8234
9136
  on: true,
8235
9137
  dir,
@@ -8240,9 +9142,9 @@ function readHistorySetup(env, homeDir = os8.homedir()) {
8240
9142
  function sourcesFingerprint(env, homeDir = os8.homedir()) {
8241
9143
  const set = (name) => env[name]?.trim() || void 0;
8242
9144
  const folders = [
8243
- path20.resolve(set(CLAUDE_HOME_ENV) ?? path20.join(homeDir, ".claude")),
8244
- path20.resolve(set(CODEX_HOME_ENV) ?? set(CODEX_OWN_HOME_ENV) ?? path20.join(homeDir, ".codex")),
8245
- path20.resolve(set(STATUS_DIR_ENV) ?? path20.join(homeDir, ".agent-lookout", "sessions"))
9145
+ path18.resolve(set(CLAUDE_HOME_ENV) ?? path18.join(homeDir, ".claude")),
9146
+ path18.resolve(set(CODEX_HOME_ENV) ?? set(CODEX_OWN_HOME_ENV) ?? path18.join(homeDir, ".codex")),
9147
+ path18.resolve(set(STATUS_DIR_ENV) ?? path18.join(homeDir, ".agent-lookout", "sessions"))
8246
9148
  ];
8247
9149
  return createHash3("sha256").update(JSON.stringify(folders)).digest("hex").slice(0, 32);
8248
9150
  }
@@ -8362,7 +9264,7 @@ function createServerNotifications(options) {
8362
9264
  if (entry.kind === "summary") continue;
8363
9265
  const session = entry.kind === "change" ? entry.change.session : entry.reminder.session;
8364
9266
  const current = listed.get(session.id);
8365
- if (current?.status !== "needs-you") continue;
9267
+ if (current === void 0 || !needsYou(current)) continue;
8366
9268
  if (entry.kind === "reminder") entry.reminder = { ...entry.reminder, session: current };
8367
9269
  else if (entry.change.event === "needs-you")
8368
9270
  entry.change = { ...entry.change, session: current };
@@ -8532,7 +9434,7 @@ var MAX_REMOTE_SOURCES = 20;
8532
9434
  var MAX_ID_LENGTH = 512;
8533
9435
  var MAX_PATH_LENGTH = 4096;
8534
9436
  var MAX_WORD_LENGTH2 = 200;
8535
- function isRecord11(value) {
9437
+ function isRecord12(value) {
8536
9438
  return typeof value === "object" && value !== null && !Array.isArray(value);
8537
9439
  }
8538
9440
  function oneOf2(allowed, value) {
@@ -8546,7 +9448,7 @@ function exact(value, max) {
8546
9448
  var COMMIT_ID2 = /^[0-9a-f]{7,64}$/;
8547
9449
  var REPOSITORY_ID = /^[0-9a-f]{16,64}$/;
8548
9450
  function readGit(value, machine) {
8549
- if (!isRecord11(value)) return void 0;
9451
+ if (!isRecord12(value)) return void 0;
8550
9452
  let head;
8551
9453
  if (value.commit !== void 0) {
8552
9454
  if (value.branch !== void 0 || typeof value.commit !== "string") return void 0;
@@ -8558,7 +9460,7 @@ function readGit(value, machine) {
8558
9460
  head = { branch };
8559
9461
  }
8560
9462
  const repository = value.repository;
8561
- if (isRecord11(repository) && typeof repository.id === "string") {
9463
+ if (isRecord12(repository) && typeof repository.id === "string") {
8562
9464
  const name = sessionName(typeof repository.name === "string" ? repository.name : "");
8563
9465
  if (name !== void 0 && REPOSITORY_ID.test(repository.id)) {
8564
9466
  head.repository = { id: repositoryId(`${machine}
@@ -8568,7 +9470,7 @@ ${repository.id}`), name };
8568
9470
  return head;
8569
9471
  }
8570
9472
  function readSession(value, machine, sources, now, keepWaitingText) {
8571
- if (!isRecord11(value)) return null;
9473
+ if (!isRecord12(value)) return null;
8572
9474
  const id = exact(value.id, MAX_ID_LENGTH);
8573
9475
  const source = exact(value.source, MAX_WORD_LENGTH2);
8574
9476
  if (id === null || source === null) return null;
@@ -8598,6 +9500,7 @@ function readSession(value, machine, sources, now, keepWaitingText) {
8598
9500
  if (detail !== void 0) session.waitingDetail = detail;
8599
9501
  const asking = keepWaitingText ? waitingText(value.waitingText) : void 0;
8600
9502
  if (asking !== void 0) session.waitingText = asking;
9503
+ if (value.answered === true) session.answered = true;
8601
9504
  }
8602
9505
  const lastWriteAt = plausibleTime(value.lastWriteAt, now);
8603
9506
  if (lastWriteAt !== null) session.lastWriteAt = lastWriteAt;
@@ -8607,7 +9510,7 @@ function readSession(value, machine, sources, now, keepWaitingText) {
8607
9510
  return session;
8608
9511
  }
8609
9512
  function readCell(value) {
8610
- if (!isRecord11(value)) return null;
9513
+ if (!isRecord12(value)) return null;
8611
9514
  if (value.level === "yes") return { level: "yes" };
8612
9515
  const level = oneOf2(["no", "partly"], value.level);
8613
9516
  const reason = cut(value.reason, MAX_WORD_LENGTH2);
@@ -8615,7 +9518,7 @@ function readCell(value) {
8615
9518
  }
8616
9519
  var ACTS_HERE = ["jump", "stop", "answer"];
8617
9520
  function readCapabilities(value, machine) {
8618
- if (!isRecord11(value)) return null;
9521
+ if (!isRecord12(value)) return null;
8619
9522
  const read = {};
8620
9523
  for (const capability of CAPABILITIES) {
8621
9524
  if (ACTS_HERE.includes(capability)) continue;
@@ -8631,7 +9534,7 @@ function readCapabilities(value, machine) {
8631
9534
  };
8632
9535
  }
8633
9536
  function readRemoteSnapshot(value, machine, now, options = {}) {
8634
- if (!isRecord11(value) || !Array.isArray(value.sessions) || !Array.isArray(value.sources)) {
9537
+ if (!isRecord12(value) || !Array.isArray(value.sessions) || !Array.isArray(value.sources)) {
8635
9538
  return null;
8636
9539
  }
8637
9540
  const named = /* @__PURE__ */ new Map();
@@ -8639,7 +9542,7 @@ function readRemoteSnapshot(value, machine, now, options = {}) {
8639
9542
  const agents = [];
8640
9543
  for (const item of value.sources) {
8641
9544
  if (sources.length >= MAX_REMOTE_SOURCES) break;
8642
- if (!isRecord11(item)) continue;
9545
+ if (!isRecord12(item)) continue;
8643
9546
  const id = exact(item.id, MAX_WORD_LENGTH2);
8644
9547
  if (id === null || isRemoteSource(id) || item.machine !== void 0) continue;
8645
9548
  const label2 = cut(item.label, MAX_WORD_LENGTH2) ?? id;
@@ -8664,14 +9567,15 @@ function readRemoteSnapshot(value, machine, now, options = {}) {
8664
9567
  }
8665
9568
 
8666
9569
  // src/collector/remotes/sshProgram.ts
8667
- import path21 from "node:path";
9570
+ import path19 from "node:path";
8668
9571
  var SSH_BIN_ENV = "AGENT_LOOKOUT_SSH_BIN";
8669
9572
  var SSH_LOCATIONS = ["/usr/bin/ssh", "/opt/homebrew/bin/ssh", "/usr/local/bin/ssh"];
8670
9573
  var SERVER_ALIVE_SECONDS = 15;
8671
- async function findSsh(env, isExecutable = isExecutableFile) {
9574
+ async function findSsh(env, isExecutable, platform = process.platform) {
9575
+ isExecutable ??= (candidate) => isExecutableFile(candidate, platform);
8672
9576
  const named = env[SSH_BIN_ENV]?.trim();
8673
9577
  if (named) {
8674
- const resolved = path21.resolve(named);
9578
+ const resolved = pathsOf(platform).resolve(named);
8675
9579
  if (await isExecutable(resolved)) return { found: true, path: resolved };
8676
9580
  return {
8677
9581
  found: false,
@@ -8679,13 +9583,13 @@ async function findSsh(env, isExecutable = isExecutableFile) {
8679
9583
  named: true
8680
9584
  };
8681
9585
  }
8682
- const candidates = [.../* @__PURE__ */ new Set([...pathCandidates(env, "ssh"), ...SSH_LOCATIONS])];
9586
+ const candidates = [.../* @__PURE__ */ new Set([...pathCandidates(env, "ssh", platform), ...SSH_LOCATIONS])];
8683
9587
  for (const candidate of candidates) {
8684
9588
  if (await isExecutable(candidate)) return { found: true, path: candidate };
8685
9589
  }
8686
9590
  return {
8687
9591
  found: false,
8688
- looked: `The ssh command was not found on PATH or in ${SSH_LOCATIONS.map((at) => path21.dirname(at)).join(", ")}`
9592
+ looked: `The ssh command was not found on PATH or in ${SSH_LOCATIONS.map((at) => path19.dirname(at)).join(", ")}`
8689
9593
  };
8690
9594
  }
8691
9595
  function sshArguments(forward) {
@@ -9268,11 +10172,11 @@ function createRemotes(options) {
9268
10172
  // src/collector/settings/settingsFile.ts
9269
10173
  import { randomBytes as randomBytes3 } from "node:crypto";
9270
10174
  import {
9271
- closeSync as closeSync2,
9272
- constants as constants5,
9273
- fstatSync as fstatSync2,
10175
+ closeSync as closeSync3,
10176
+ constants as constants6,
10177
+ fstatSync as fstatSync3,
9274
10178
  fsyncSync,
9275
- lstatSync as lstatSync2,
10179
+ lstatSync as lstatSync3,
9276
10180
  mkdirSync,
9277
10181
  openSync as openSync2,
9278
10182
  readSync as readSync2,
@@ -9281,23 +10185,22 @@ import {
9281
10185
  writeSync as writeSync2
9282
10186
  } from "node:fs";
9283
10187
  import os9 from "node:os";
9284
- import path22 from "node:path";
10188
+ import path20 from "node:path";
9285
10189
  var SETTINGS_FILE_ENV = "AGENT_LOOKOUT_SETTINGS_FILE";
9286
10190
  var SETTINGS_FILE_MODE = 384;
9287
10191
  var SETTINGS_FOLDER_MODE = 448;
9288
10192
  var MAX_SETTINGS_BYTES = 64 * 1024;
9289
10193
  function readSettingsSetup(env, homeDir = os9.homedir()) {
9290
10194
  const named = env[SETTINGS_FILE_ENV]?.trim() || void 0;
9291
- const file = path22.resolve(named ?? path22.join(homeDir, ".agent-lookout", "settings.json"));
10195
+ const file = path20.resolve(named ?? path20.join(homeDir, ".agent-lookout", "settings.json"));
9292
10196
  return { file, shown: tildify(file, homeDir) };
9293
10197
  }
9294
- var NO_FOLLOW2 = (constants5.O_NOFOLLOW ?? 0) | (constants5.O_NONBLOCK ?? 0);
9295
10198
  function codeOf(error) {
9296
10199
  return error?.code;
9297
10200
  }
9298
10201
  function kindAt(target) {
9299
10202
  try {
9300
- const info = lstatSync2(target);
10203
+ const info = lstatSync3(target);
9301
10204
  if (info.isSymbolicLink()) return "link";
9302
10205
  if (info.isFile()) return "file";
9303
10206
  return info.isDirectory() ? "folder" : "other";
@@ -9309,54 +10212,69 @@ function kindAt(target) {
9309
10212
  function syncFolder(folder) {
9310
10213
  let fd = null;
9311
10214
  try {
9312
- fd = openSync2(folder, constants5.O_RDONLY);
10215
+ fd = openSync2(folder, constants6.O_RDONLY);
9313
10216
  fsyncSync(fd);
9314
10217
  } catch {
9315
10218
  } finally {
9316
- if (fd !== null) closeSync2(fd);
10219
+ if (fd !== null) closeSync3(fd);
10220
+ }
10221
+ }
10222
+ var linkRefused = (shown) => ({
10223
+ kind: "refused",
10224
+ problem: `${shown} is a link, which Agent Lookout does not follow, so the time rules and the permission rules are off.`
10225
+ });
10226
+ var notOrdinary3 = (shown) => ({
10227
+ kind: "refused",
10228
+ problem: `${shown} is not an ordinary file, so the time rules and the permission rules are off.`
10229
+ });
10230
+ function readSettingsStamp(setup) {
10231
+ try {
10232
+ const info = lstatSync3(setup.file, { bigint: true });
10233
+ return `${info.ino}:${info.size}:${info.mtimeNs}:${info.ctimeNs}`;
10234
+ } catch {
10235
+ return null;
9317
10236
  }
9318
10237
  }
9319
10238
  function readSettingsText(setup) {
9320
10239
  const { file, shown } = setup;
9321
- const folder = path22.dirname(file);
10240
+ const folder = path20.dirname(file);
9322
10241
  try {
9323
10242
  const folderKind = kindAt(folder);
9324
10243
  if (folderKind === null) return { kind: "missing" };
9325
10244
  if (folderKind !== "folder") {
9326
10245
  return {
9327
10246
  kind: "refused",
9328
- problem: `The folder of ${shown} is a link or not a folder, which Agent Lookout does not follow, so the time rules are off.`
10247
+ problem: `The folder of ${shown} is a link or not a folder, which Agent Lookout does not follow, so the time rules and the permission rules are off.`
9329
10248
  };
9330
10249
  }
10250
+ const fileKind = kindAt(file);
10251
+ if (fileKind === "link") return linkRefused(shown);
10252
+ if (fileKind === "folder" || fileKind === "other") return notOrdinary3(shown);
9331
10253
  } catch {
9332
- return { kind: "refused", problem: `${shown} could not be read, so the time rules are off.` };
10254
+ return {
10255
+ kind: "refused",
10256
+ problem: `${shown} could not be read, so the time rules and the permission rules are off.`
10257
+ };
9333
10258
  }
9334
10259
  let fd;
9335
10260
  try {
9336
- fd = openSync2(file, constants5.O_RDONLY | NO_FOLLOW2);
10261
+ fd = openWithoutFollowingNow(file, constants6.O_RDONLY);
9337
10262
  } catch (error) {
9338
10263
  const code = codeOf(error);
9339
10264
  if (code === "ENOENT") return { kind: "missing" };
9340
- if (code === "ELOOP") {
9341
- return {
9342
- kind: "refused",
9343
- problem: `${shown} is a link, which Agent Lookout does not follow, so the time rules are off.`
9344
- };
9345
- }
9346
- return { kind: "refused", problem: `${shown} could not be read, so the time rules are off.` };
10265
+ if (code === "ELOOP") return linkRefused(shown);
10266
+ return {
10267
+ kind: "refused",
10268
+ problem: `${shown} could not be read, so the time rules and the permission rules are off.`
10269
+ };
9347
10270
  }
9348
10271
  try {
9349
- const info = fstatSync2(fd);
9350
- if (!info.isFile()) {
9351
- return {
9352
- kind: "refused",
9353
- problem: `${shown} is not an ordinary file, so the time rules are off.`
9354
- };
9355
- }
10272
+ const info = fstatSync3(fd);
10273
+ if (!info.isFile()) return notOrdinary3(shown);
9356
10274
  if (info.size > MAX_SETTINGS_BYTES) {
9357
10275
  return {
9358
10276
  kind: "refused",
9359
- problem: `${shown} is larger than 64 KB, so it was not read and the time rules are off.`
10277
+ problem: `${shown} is larger than 64 KB, so it was not read and the time rules and the permission rules are off.`
9360
10278
  };
9361
10279
  }
9362
10280
  const buffer = Buffer.alloc(info.size);
@@ -9368,14 +10286,17 @@ function readSettingsText(setup) {
9368
10286
  }
9369
10287
  return { kind: "read", text: buffer.toString("utf8", 0, filled) };
9370
10288
  } catch {
9371
- return { kind: "refused", problem: `${shown} could not be read, so the time rules are off.` };
10289
+ return {
10290
+ kind: "refused",
10291
+ problem: `${shown} could not be read, so the time rules and the permission rules are off.`
10292
+ };
9372
10293
  } finally {
9373
- closeSync2(fd);
10294
+ closeSync3(fd);
9374
10295
  }
9375
10296
  }
9376
10297
  function writeSettingsText(setup, text4) {
9377
10298
  const { file, shown } = setup;
9378
- const folder = path22.dirname(file);
10299
+ const folder = path20.dirname(file);
9379
10300
  const notSaved = {
9380
10301
  ok: false,
9381
10302
  problem: `${shown} could not be written, so the change was not saved.`
@@ -9396,20 +10317,20 @@ function writeSettingsText(setup, text4) {
9396
10317
  problem: `${shown} is a link or not an ordinary file, which Agent Lookout does not write through, so the change was not saved.`
9397
10318
  };
9398
10319
  }
9399
- temporary = path22.join(
10320
+ temporary = path20.join(
9400
10321
  folder,
9401
- `.${path22.basename(file)}.${process.pid}.${randomBytes3(6).toString("hex")}.tmp`
10322
+ `.${path20.basename(file)}.${process.pid}.${randomBytes3(6).toString("hex")}.tmp`
9402
10323
  );
9403
- const fd = openSync2(
10324
+ const fd = openWithoutFollowingNow(
9404
10325
  temporary,
9405
- constants5.O_WRONLY | constants5.O_CREAT | constants5.O_EXCL | NO_FOLLOW2,
10326
+ constants6.O_WRONLY | constants6.O_CREAT | constants6.O_EXCL,
9406
10327
  SETTINGS_FILE_MODE
9407
10328
  );
9408
10329
  try {
9409
10330
  writeSync2(fd, text4);
9410
10331
  fsyncSync(fd);
9411
10332
  } finally {
9412
- closeSync2(fd);
10333
+ closeSync3(fd);
9413
10334
  }
9414
10335
  renameSync(temporary, file);
9415
10336
  temporary = null;
@@ -9430,14 +10351,15 @@ function writeSettingsText(setup, text4) {
9430
10351
  // src/collector/settings/collectorSettings.ts
9431
10352
  var fileSettingsStore = {
9432
10353
  read: readSettingsText,
9433
- write: writeSettingsText
10354
+ write: writeSettingsText,
10355
+ stamp: readSettingsStamp
9434
10356
  };
9435
10357
  var RULE_WORDS = {
9436
10358
  longWait: "the long wait reminder",
9437
10359
  idle: "the idle rule",
9438
10360
  quietHours: "the quiet hours"
9439
10361
  };
9440
- function isRecord12(value) {
10362
+ function isRecord13(value) {
9441
10363
  return typeof value === "object" && value !== null && !Array.isArray(value);
9442
10364
  }
9443
10365
  function listOf(names) {
@@ -9446,11 +10368,28 @@ function listOf(names) {
9446
10368
  }
9447
10369
  function rulesIn(read, shown) {
9448
10370
  const off = readTimeRules(void 0).rules;
10371
+ const none = { permissionRules: [], heldRules: void 0 };
9449
10372
  if (read.kind === "missing") {
9450
- return { rules: off, rest: {}, otherRules: {}, problem: null, writable: true };
10373
+ return {
10374
+ rules: off,
10375
+ ...none,
10376
+ rest: {},
10377
+ otherRules: {},
10378
+ problem: null,
10379
+ rulesProblem: null,
10380
+ writable: true
10381
+ };
9451
10382
  }
9452
10383
  if (read.kind === "refused") {
9453
- return { rules: off, rest: {}, otherRules: {}, problem: read.problem, writable: false };
10384
+ return {
10385
+ rules: off,
10386
+ ...none,
10387
+ rest: {},
10388
+ otherRules: {},
10389
+ problem: read.problem,
10390
+ rulesProblem: read.problem,
10391
+ writable: false
10392
+ };
9454
10393
  }
9455
10394
  let value;
9456
10395
  try {
@@ -9458,60 +10397,252 @@ function rulesIn(read, shown) {
9458
10397
  } catch {
9459
10398
  value = null;
9460
10399
  }
9461
- if (!isRecord12(value)) {
10400
+ if (!isRecord13(value)) {
10401
+ 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.`;
9462
10402
  return {
9463
10403
  rules: off,
10404
+ ...none,
9464
10405
  rest: {},
9465
10406
  otherRules: {},
9466
- problem: `${shown} is not JSON that Agent Lookout can read, so the time rules are off. Changing one in Settings writes the file again.`,
10407
+ problem: notJson,
10408
+ rulesProblem: notJson,
9467
10409
  writable: true
9468
10410
  };
9469
10411
  }
9470
- const { timeRules, ...rest } = value;
10412
+ const { timeRules, permissionRules: heldRules, ...rest } = value;
10413
+ const permissions = readPermissionRules(heldRules);
9471
10414
  const { rules, unread } = readTimeRules(timeRules);
9472
- const otherRules = isRecord12(timeRules) ? Object.fromEntries(
10415
+ const otherRules = isRecord13(timeRules) ? Object.fromEntries(
9473
10416
  Object.entries(timeRules).filter(
9474
10417
  ([name]) => !TIME_RULE_NAMES.includes(name)
9475
10418
  )
9476
10419
  ) : {};
9477
10420
  const problem = unread.length === 0 ? null : `${shown} holds ${listOf(unread)} in a form Agent Lookout cannot read, so ${unread.length === 1 ? "it is" : "they are"} off.`;
9478
- return { rules, rest, otherRules, problem, writable: true };
10421
+ return {
10422
+ rules,
10423
+ permissionRules: permissions.ok ? permissions.rules : null,
10424
+ heldRules,
10425
+ rest,
10426
+ otherRules,
10427
+ problem,
10428
+ rulesProblem: permissions.ok ? null : unreadRulesLine(shown),
10429
+ writable: true
10430
+ };
10431
+ }
10432
+ function unreadRulesLine(shown) {
10433
+ return `${shown} holds permission rules Agent Lookout cannot read, or a rule it refuses, so no rule is used. Changing a rule in Settings writes the list again with only the rules shown there.`;
9479
10434
  }
9480
10435
  function createCollectorSettings(options) {
9481
10436
  const { setup } = options;
9482
10437
  const store = options.store ?? fileSettingsStore;
9483
- const first = rulesIn(store.read(setup), setup.shown);
9484
- let rules = first.rules;
9485
- const { rest, otherRules } = first;
9486
- let problem = first.problem;
10438
+ let stamp = store.stamp(setup);
10439
+ let held = rulesIn(store.read(setup), setup.shown);
10440
+ const first = held;
10441
+ let rules = held.rules;
10442
+ let problem = held.problem;
10443
+ let permissionRules = held.permissionRules ?? [];
10444
+ let keepHeld = held.permissionRules === null;
10445
+ let rulesProblem = held.rulesProblem;
10446
+ function read() {
10447
+ stamp = store.stamp(setup);
10448
+ held = rulesIn(store.read(setup), setup.shown);
10449
+ rules = held.rules;
10450
+ problem = held.problem;
10451
+ permissionRules = held.permissionRules ?? [];
10452
+ keepHeld = held.permissionRules === null;
10453
+ rulesProblem = held.rulesProblem;
10454
+ }
10455
+ const notWritable = () => ({
10456
+ ok: false,
10457
+ problem: `${setup.shown} could not be read, so it is not written over and the change was not saved. Mend or remove the file, then make the change again.`
10458
+ });
10459
+ function textWith(time, permissions, keep) {
10460
+ const listed = keep ? { permissionRules: held.heldRules } : permissions.length > 0 || held.heldRules !== void 0 ? { permissionRules: permissions } : {};
10461
+ const whole = { ...held.rest, timeRules: { ...held.otherRules, ...time }, ...listed };
10462
+ return `${JSON.stringify(whole, null, 2)}
10463
+ `;
10464
+ }
9487
10465
  return {
9488
10466
  timeRules: () => rules,
9489
- status: () => ({ timeRules: rules, file: setup.shown, problem }),
10467
+ permissionRules: () => permissionRules,
10468
+ status: () => ({
10469
+ timeRules: rules,
10470
+ file: setup.shown,
10471
+ problem,
10472
+ permissionRules: [...permissionRules],
10473
+ permissionRulesProblem: rulesProblem
10474
+ }),
10475
+ readAgain() {
10476
+ if (store.stamp(setup) !== stamp) read();
10477
+ },
9490
10478
  changeTimeRules(next) {
9491
- if (!first.writable) {
9492
- return {
9493
- ok: false,
9494
- problem: `${setup.shown} could not be read when Agent Lookout started, so it is not written over and the change was not saved. Mend or remove the file, then start Agent Lookout again.`
9495
- };
9496
- }
9497
- const text4 = `${JSON.stringify({ ...rest, timeRules: { ...otherRules, ...next } }, null, 2)}
9498
- `;
9499
- const written = store.write(setup, text4);
10479
+ read();
10480
+ if (!held.writable) return notWritable();
10481
+ const written = store.write(setup, textWith(next, permissionRules, keepHeld));
9500
10482
  if (!written.ok) {
9501
10483
  problem = written.problem;
9502
10484
  return written;
9503
10485
  }
9504
10486
  rules = next;
9505
10487
  problem = null;
10488
+ if (!keepHeld) rulesProblem = null;
9506
10489
  return written;
9507
10490
  },
9508
- problemAtStart: first.problem
10491
+ changePermissionRules(change) {
10492
+ read();
10493
+ if (!held.writable) return { reason: "not-saved", ...notWritable() };
10494
+ const made = change(permissionRules);
10495
+ if (!made.ok || !made.changed) return made;
10496
+ const written = store.write(setup, textWith(rules, made.rules, false));
10497
+ if (!written.ok) {
10498
+ rulesProblem = written.problem;
10499
+ return { ok: false, reason: "not-saved", problem: written.problem };
10500
+ }
10501
+ permissionRules = [...made.rules];
10502
+ keepHeld = false;
10503
+ rulesProblem = null;
10504
+ problem = null;
10505
+ return made;
10506
+ },
10507
+ problemAtStart: first.problem,
10508
+ rulesProblemAtStart: first.rulesProblem === first.problem ? null : first.rulesProblem
10509
+ };
10510
+ }
10511
+
10512
+ // src/collector/settings/permissionRulesRoute.ts
10513
+ import { randomBytes as randomBytes4 } from "node:crypto";
10514
+
10515
+ // src/core/permission-rules/rulesChange.ts
10516
+ var CHANGE_SHAPE = 'The body must be one change: {"add": rule}, {"edit": rule with its "id"}, {"move": {"id", "to": "up" or "down"}} or {"remove": {"id"}}, and nothing else.';
10517
+ function isRecord14(value) {
10518
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10519
+ }
10520
+ function rulesChangeIn(value) {
10521
+ const refused = { ok: false, problem: CHANGE_SHAPE };
10522
+ if (!isRecord14(value) || Object.keys(value).length !== 1) return refused;
10523
+ const [kind] = Object.keys(value);
10524
+ const body = value[kind];
10525
+ if (!isRecord14(body)) return refused;
10526
+ switch (kind) {
10527
+ case "add": {
10528
+ const read = ruleWordsIn(body);
10529
+ return read.ok ? { ok: true, change: { kind, words: read.words } } : read;
10530
+ }
10531
+ case "edit": {
10532
+ const { id, ...words } = body;
10533
+ if (!isRuleId(id)) return refused;
10534
+ const read = ruleWordsIn(words);
10535
+ return read.ok ? { ok: true, change: { kind, id, words: read.words } } : read;
10536
+ }
10537
+ case "move": {
10538
+ const { id, to } = body;
10539
+ if (Object.keys(body).length !== 2 || !isRuleId(id)) return refused;
10540
+ if (to !== "up" && to !== "down") return refused;
10541
+ return { ok: true, change: { kind, id, to } };
10542
+ }
10543
+ case "remove": {
10544
+ const { id } = body;
10545
+ if (Object.keys(body).length !== 1 || !isRuleId(id)) return refused;
10546
+ return { ok: true, change: { kind, id } };
10547
+ }
10548
+ default:
10549
+ return refused;
10550
+ }
10551
+ }
10552
+ var NO_RULE = {
10553
+ ok: false,
10554
+ reason: "no-rule",
10555
+ problem: "The list holds no such rule. It may have been changed in another tab."
10556
+ };
10557
+ var DUPLICATE = {
10558
+ ok: false,
10559
+ reason: "duplicate",
10560
+ problem: "The list holds that rule already."
10561
+ };
10562
+ function applyRulesChange(rules, change, newId) {
10563
+ if (change.kind === "add") {
10564
+ if (rules.length >= MAX_RULES) {
10565
+ return {
10566
+ ok: false,
10567
+ reason: "full",
10568
+ problem: `The list holds ${MAX_RULES} rules, the most it can. Remove one to add another.`
10569
+ };
10570
+ }
10571
+ if (rules.some((rule2) => sameWords(rule2, change.words))) return DUPLICATE;
10572
+ let id = newId();
10573
+ while (rules.some((rule2) => rule2.id === id)) id = newId();
10574
+ return { ok: true, rules: [...rules, { id, ...change.words }], changed: true };
10575
+ }
10576
+ const at = rules.findIndex((rule2) => rule2.id === change.id);
10577
+ if (at < 0) return NO_RULE;
10578
+ const rule = rules[at];
10579
+ if (change.kind === "remove") {
10580
+ return { ok: true, rules: rules.filter((_, index) => index !== at), changed: true };
10581
+ }
10582
+ if (change.kind === "edit") {
10583
+ if (sameWords(rule, change.words)) return { ok: true, rules: [...rules], changed: false };
10584
+ if (rules.some((other) => other.id !== rule.id && sameWords(other, change.words))) {
10585
+ return DUPLICATE;
10586
+ }
10587
+ const next2 = [...rules];
10588
+ next2[at] = { id: rule.id, ...change.words };
10589
+ return { ok: true, rules: next2, changed: true };
10590
+ }
10591
+ const to = change.to === "up" ? at - 1 : at + 1;
10592
+ if (to < 0 || to >= rules.length) return { ok: true, rules: [...rules], changed: false };
10593
+ const next = [...rules];
10594
+ next[at] = next[to];
10595
+ next[to] = rule;
10596
+ return { ok: true, rules: next, changed: true };
10597
+ }
10598
+
10599
+ // src/collector/settings/permissionRulesRoute.ts
10600
+ var MAX_PERMISSION_RULES_BODY_BYTES = 1024;
10601
+ var failed3 = (status, reason, error) => ({
10602
+ status,
10603
+ body: { error, reason }
10604
+ });
10605
+ var STATUS_OF = { "no-rule": 404, full: 409, duplicate: 409, "not-saved": 500 };
10606
+ function permissionRulesRefusalFor(req) {
10607
+ return actionRefusalFor(req, PERMISSION_RULES_ACTION, MAX_PERMISSION_RULES_BODY_BYTES);
10608
+ }
10609
+ var newRuleId = () => randomBytes4(6).toString("hex");
10610
+ function createPermissionRulesRoute(options) {
10611
+ const { settings } = options;
10612
+ const newId = options.newId ?? newRuleId;
10613
+ return async function answerPermissionRules(req) {
10614
+ const refused = permissionRulesRefusalFor(req);
10615
+ if (refused) {
10616
+ req.resume();
10617
+ return refused;
10618
+ }
10619
+ const body = await readRequestBody(req, MAX_PERMISSION_RULES_BODY_BYTES);
10620
+ if (!body.ok) {
10621
+ req.resume();
10622
+ return body.tooLarge ? refusal(413, `The body must be no more than ${MAX_PERMISSION_RULES_BODY_BYTES} bytes.`) : refusal(400, "The body could not be read.");
10623
+ }
10624
+ let value;
10625
+ try {
10626
+ value = JSON.parse(body.text);
10627
+ } catch {
10628
+ return failed3(400, "invalid", "The body must be JSON: one change to the permission rules.");
10629
+ }
10630
+ const asked = rulesChangeIn(value);
10631
+ if (!asked.ok) return failed3(400, "invalid", asked.problem);
10632
+ const made = settings.changePermissionRules(
10633
+ (rules) => applyRulesChange(rules, asked.change, newId)
10634
+ );
10635
+ if (!made.ok) return failed3(STATUS_OF[made.reason], made.reason, made.problem);
10636
+ return {
10637
+ status: 200,
10638
+ body: { ok: true, permissionRules: made.rules }
10639
+ };
9509
10640
  };
9510
10641
  }
9511
10642
 
9512
10643
  // src/collector/settings/timeRulesRoute.ts
9513
10644
  var MAX_TIME_RULES_BODY_BYTES = 2048;
9514
- var failed3 = (status, reason, error) => ({
10645
+ var failed4 = (status, reason, error) => ({
9515
10646
  status,
9516
10647
  body: { error, reason }
9517
10648
  });
@@ -9535,12 +10666,12 @@ function createTimeRulesRoute(options) {
9535
10666
  try {
9536
10667
  value = JSON.parse(body.text);
9537
10668
  } catch {
9538
- return failed3(400, "invalid", "The body must be JSON: the three time rules.");
10669
+ return failed4(400, "invalid", "The body must be JSON: the three time rules.");
9539
10670
  }
9540
10671
  const asked = timeRulesIn(value);
9541
- if (!asked.ok) return failed3(400, "invalid", asked.problem);
10672
+ if (!asked.ok) return failed4(400, "invalid", asked.problem);
9542
10673
  const saved = settings.changeTimeRules(asked.rules);
9543
- if (!saved.ok) return failed3(500, "not-saved", saved.problem);
10674
+ if (!saved.ok) return failed4(500, "not-saved", saved.problem);
9544
10675
  return {
9545
10676
  status: 200,
9546
10677
  body: { ok: true, timeRules: asked.rules }
@@ -9692,8 +10823,9 @@ var TMUX_LOCATIONS = [
9692
10823
  "/opt/local/bin/tmux",
9693
10824
  "/usr/bin/tmux"
9694
10825
  ];
9695
- async function findTmuxBinary(env, isExecutable = isExecutableFile) {
9696
- const candidates = [.../* @__PURE__ */ new Set([...pathCandidates(env, "tmux"), ...TMUX_LOCATIONS])];
10826
+ async function findTmuxBinary(env, isExecutable, platform = process.platform) {
10827
+ isExecutable ??= (candidate) => isExecutableFile(candidate, platform);
10828
+ const candidates = [.../* @__PURE__ */ new Set([...pathCandidates(env, "tmux", platform), ...TMUX_LOCATIONS])];
9697
10829
  for (const candidate of candidates) {
9698
10830
  if (await isExecutable(candidate)) return candidate;
9699
10831
  }
@@ -9727,9 +10859,10 @@ function runTmuxBinary(binary, args, options) {
9727
10859
  function createTmuxRunner(options) {
9728
10860
  const { env } = options;
9729
10861
  const off = env[TMUX_ENV]?.trim().toLowerCase() === "off";
10862
+ const platform = options.platform ?? process.platform;
9730
10863
  return async (args) => {
9731
- if (off || process.platform === "win32") return NOT_RUN3;
9732
- const binary = await findTmuxBinary(env, options.isExecutable);
10864
+ if (off || platform === "win32") return NOT_RUN3;
10865
+ const binary = await findTmuxBinary(env, options.isExecutable, platform);
9733
10866
  if (binary === null) return NOT_RUN3;
9734
10867
  return runTmuxBinary(binary, args, { env, timeoutMs: options.timeoutMs });
9735
10868
  };
@@ -9782,7 +10915,8 @@ function createWaitLedger() {
9782
10915
  events,
9783
10916
  measured: spans,
9784
10917
  runStarts,
9785
- waitingNow: snapshot.sessions.filter((session) => session.status === "needs-you").map((session) => ({ id: session.id, name: session.name })),
10918
+ // Not a session in a wait Agent Lookout answered, whatever its source still says.
10919
+ waitingNow: snapshot.sessions.filter(needsYou).map((session) => ({ id: session.id, name: session.name })),
9786
10920
  names: new Map(snapshot.sessions.map((session) => [session.id, session.name])),
9787
10921
  since: since.at,
9788
10922
  now
@@ -9973,11 +11107,13 @@ function createCollector(options) {
9973
11107
  const env = options.env ?? process.env;
9974
11108
  const intervalMs = options.intervalMs ?? POLL_INTERVAL_MS;
9975
11109
  const warn = options.warn ?? console.error;
11110
+ const settingsSetup = readSettingsSetup(env);
9976
11111
  const settings = createCollectorSettings({
9977
- setup: readSettingsSetup(env),
11112
+ setup: settingsSetup,
9978
11113
  store: options.settingsStore
9979
11114
  });
9980
11115
  if (settings.problemAtStart !== null) warn(settings.problemAtStart);
11116
+ if (settings.rulesProblemAtStart !== null) warn(settings.rulesProblemAtStart);
9981
11117
  const memoryEvents = createEventStore();
9982
11118
  const memoryHistory = createHistoryStore();
9983
11119
  const historySetup = readHistorySetup(env);
@@ -10008,9 +11144,9 @@ function createCollector(options) {
10008
11144
  });
10009
11145
  const tabs = createTabFinder({ env, readProcesses: options.readProcesses, now });
10010
11146
  const osascript = options.osascript ?? createOsascriptRunner({ env });
10011
- const stopsOn = !stopOff(env);
10012
- const stopTargets = stopsOn ? createStopTargets() : void 0;
10013
11147
  const platform = options.platform ?? process.platform;
11148
+ const stopsOn = !stopOff(env) && platform !== "win32";
11149
+ const stopTargets = stopsOn ? createStopTargets() : void 0;
10014
11150
  const notifications = createServerNotifications({
10015
11151
  notifier: options.notifier ?? createSystemNotifier({ platform }),
10016
11152
  onAtStart: notificationsOnAtStart(env),
@@ -10071,7 +11207,8 @@ function createCollector(options) {
10071
11207
  panes,
10072
11208
  terminals: tabs,
10073
11209
  readProcessStarts: options.readProcessStarts,
10074
- stops: stopTargets
11210
+ stops: stopTargets,
11211
+ platform
10075
11212
  }),
10076
11213
  createCodexAdapter({ env: options.env, now, pollIntervalMs: intervalMs }),
10077
11214
  createStatusFileAdapter({ env: options.env, now, pollIntervalMs: intervalMs })
@@ -10087,7 +11224,17 @@ function createCollector(options) {
10087
11224
  // Every source's sessions alike are given the branch of their folder, and
10088
11225
  // with pull requests on, the branch's pull request, as last learnt.
10089
11226
  // Every snapshot carries the time rules in force, and is stale by them.
10090
- timeRules: () => settings.timeRules(),
11227
+ // The settings file is looked at again here, once a poll, so a change
11228
+ // another copy of Agent Lookout saved is in force by the next one.
11229
+ timeRules: () => {
11230
+ settings.readAgain();
11231
+ return settings.timeRules();
11232
+ },
11233
+ // And marks each session still in a wait whose permission request was
11234
+ // answered, so every channel, the page's too, takes that wait as over at
11235
+ // the same poll, and the Events log and the waits never count a wait a
11236
+ // rule answered before a poll saw it.
11237
+ answered: (snapshot) => answering?.answeredIn(snapshot) ?? /* @__PURE__ */ new Set(),
10091
11238
  annotate: async (sessions) => {
10092
11239
  const onBranches = await branches.annotate(sessions);
10093
11240
  return pullRequests ? pullRequests.annotate(onBranches) : onBranches;
@@ -10103,7 +11250,18 @@ function createCollector(options) {
10103
11250
  }
10104
11251
  });
10105
11252
  const answers2 = options.adapters === void 0 || options.answering !== void 0;
10106
- const answering = answers2 ? createAnswering({ env, poller, events, now, warn, ...options.answering }) : null;
11253
+ const answering = answers2 ? createAnswering({
11254
+ env,
11255
+ poller,
11256
+ events,
11257
+ now,
11258
+ warn,
11259
+ platform,
11260
+ // Read for each request, so a change in Settings holds for the next one.
11261
+ rules: () => settings.permissionRules(),
11262
+ settingsFile: settingsSetup.file,
11263
+ ...options.answering
11264
+ }) : null;
10107
11265
  const stopper = createStopper({
10108
11266
  env,
10109
11267
  now,
@@ -10133,8 +11291,13 @@ function createCollector(options) {
10133
11291
  jump: createJumpRoute({ poller, panes, run: tmux, tabs, osascript, now }),
10134
11292
  stop: stopRoutes && createStopRoute(stopRoutes),
10135
11293
  cleanUp: stopRoutes && createCleanUpRoute(stopRoutes),
10136
- settings: () => settings.status(),
11294
+ settings: () => ({
11295
+ ...settings.status(),
11296
+ ruleAnswers: answering?.ruleAnswers() ?? [],
11297
+ ruleAnswersSince: poller.startedAt
11298
+ }),
10137
11299
  timeRules: createTimeRulesRoute({ settings }),
11300
+ permissionRules: createPermissionRulesRoute({ settings }),
10138
11301
  clearHistory: createClearHistoryRoute({
10139
11302
  keeper,
10140
11303
  forget: () => {
@@ -10248,7 +11411,7 @@ async function runStandalone(options) {
10248
11411
  } catch (error) {
10249
11412
  return stopWith(error.message);
10250
11413
  }
10251
- if (!existsSync(path23.join(distDir, "index.html"))) {
11414
+ if (!existsSync(path21.join(distDir, "index.html"))) {
10252
11415
  return stopWith(
10253
11416
  "The dashboard has not been built yet. Run `npm run build`, then start it again."
10254
11417
  );
@@ -10291,12 +11454,24 @@ async function runStandalone(options) {
10291
11454
 
10292
11455
  // src/cli/start/openBrowser.ts
10293
11456
  import { spawn as spawn3 } from "node:child_process";
11457
+ import path22 from "node:path";
10294
11458
  var OPEN_WAIT_MS = 1e4;
10295
- function openCommand(platform, url) {
11459
+ var WINDOWS_FOLDER = "C:\\Windows";
11460
+ function windowsFolder(env) {
11461
+ const named = env.SystemRoot?.trim() || env.SYSTEMROOT?.trim();
11462
+ return named && path22.win32.isAbsolute(named) ? named : WINDOWS_FOLDER;
11463
+ }
11464
+ function openCommand(platform, url, env = process.env) {
10296
11465
  const address = loopbackAddress(url);
10297
11466
  if (address === null) return null;
10298
11467
  if (platform === "darwin") return { file: "/usr/bin/open", args: [address] };
10299
11468
  if (platform === "linux") return { file: "xdg-open", args: [address] };
11469
+ if (platform === "win32") {
11470
+ return {
11471
+ file: path22.win32.join(windowsFolder(env), "System32", "rundll32.exe"),
11472
+ args: ["url.dll,FileProtocolHandler", address]
11473
+ };
11474
+ }
10300
11475
  return null;
10301
11476
  }
10302
11477
  function runProgram2(file, args, waitMs = OPEN_WAIT_MS) {
@@ -10307,7 +11482,7 @@ function runProgram2(file, args, waitMs = OPEN_WAIT_MS) {
10307
11482
  resolve(opened);
10308
11483
  };
10309
11484
  try {
10310
- const child = spawn3(file, [...args], { stdio: "ignore", detached: true });
11485
+ const child = spawn3(file, [...args], { stdio: "ignore", detached: true, windowsHide: true });
10311
11486
  child.once("error", () => settle(false));
10312
11487
  child.once("exit", (code) => settle(code === 0));
10313
11488
  child.unref();
@@ -10318,7 +11493,7 @@ function runProgram2(file, args, waitMs = OPEN_WAIT_MS) {
10318
11493
  });
10319
11494
  }
10320
11495
  async function openInBrowser(url, options = {}) {
10321
- const command = openCommand(options.platform ?? process.platform, url);
11496
+ const command = openCommand(options.platform ?? process.platform, url, options.env);
10322
11497
  if (command === null) return false;
10323
11498
  return (options.run ?? runProgram2)(command.file, command.args);
10324
11499
  }