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.
- package/CHANGELOG.md +30 -0
- package/PRIVACY.md +41 -35
- package/README.md +26 -25
- package/SECURITY.md +14 -9
- package/dist/assets/{HistoryPanel-TXa0oPH4.js → HistoryPanel-Cwmn7YtJ.js} +1 -1
- package/dist/assets/SessionPanel-BJ5t_CPi.js +1 -0
- package/dist/assets/index-B_1Its0R.css +1 -0
- package/dist/assets/index-QSfCI-L_.js +3 -0
- package/dist/cli/agent-lookout.js +4 -4
- package/dist/cli/{chunk-OT3EGOTY.js → chunk-I6Q35DHC.js} +38 -1
- package/dist/cli/{chunk-Q73JDXXW.js → chunk-JSBQMY3Q.js} +1 -1
- package/dist/cli/{chunk-NWVEVCRT.js → chunk-LZ62RW3N.js} +8 -3
- package/dist/cli/{mcpServer-IFB2FEOZ.js → mcpServer-HJSH3WIS.js} +10 -7
- package/dist/cli/{startCommand-J3QS26SZ.js → startCommand-PR5JPWCT.js} +1545 -370
- package/dist/index.html +2 -2
- package/docs/API.md +97 -51
- package/docs/GUIDE.md +125 -28
- package/package.json +1 -1
- package/dist/assets/SessionPanel-BStoG7uq.js +0 -1
- package/dist/assets/index-BsTJp5-3.css +0 -1
- package/dist/assets/index-a7ubgAmE.js +0 -3
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
import {
|
|
2
2
|
sortSessions
|
|
3
|
-
} from "./chunk-
|
|
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-
|
|
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
|
|
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
|
|
224
|
+
let needsYou2 = 0;
|
|
217
225
|
let working = 0;
|
|
218
226
|
let idle = 0;
|
|
219
227
|
for (const session of sessions) {
|
|
220
|
-
if (session
|
|
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
|
|
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
|
|
280
|
+
const open2 = [];
|
|
273
281
|
for (const [id, wait] of stillOpen) {
|
|
274
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
|
613
|
+
for (const [id, begunAt] of open2) {
|
|
606
614
|
const session = snapshot.sessions.find(
|
|
607
|
-
(candidate) => candidate.id === id && candidate
|
|
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
|
-
|
|
621
|
+
open2.delete(id);
|
|
614
622
|
hold.holdWait(session, begunAt);
|
|
615
623
|
continue;
|
|
616
624
|
}
|
|
617
625
|
if (timing === "limited") continue;
|
|
618
|
-
|
|
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) => !
|
|
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
|
|
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,
|
|
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 [
|
|
1042
|
-
if (at === 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
|
|
1349
|
-
if (
|
|
1350
|
-
if (
|
|
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(
|
|
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,
|
|
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 =
|
|
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
|
|
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,
|
|
1502
|
+
baselines.set(basis, sessions.map(withoutWaitingText));
|
|
1432
1503
|
});
|
|
1433
1504
|
const rules = options.timeRules?.();
|
|
1434
1505
|
const snapshot = {
|
|
1435
1506
|
generatedAt: at,
|
|
1436
|
-
|
|
1437
|
-
|
|
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
|
|
1797
|
-
if (
|
|
1798
|
-
fail(res, 403,
|
|
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
|
|
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
|
-
|
|
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
|
|
2799
|
-
|
|
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 === ""
|
|
2803
|
-
if (
|
|
2804
|
-
|
|
2805
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
2946
|
+
const resolved = paths.resolve(named);
|
|
2824
2947
|
if (await isExecutable(resolved)) return { found: true, path: resolved };
|
|
2825
|
-
|
|
2826
|
-
|
|
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([...
|
|
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
|
-
|
|
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
|
|
3048
|
-
import {
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
3277
|
-
import { lstat
|
|
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
|
|
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
|
|
3516
|
+
lstat: async (target) => infoOf(await lstat2(target)),
|
|
3321
3517
|
openRegular: openRegularFile
|
|
3322
3518
|
};
|
|
3323
|
-
function
|
|
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
|
|
3692
|
+
const open2 = await io.openRegular(file);
|
|
3497
3693
|
try {
|
|
3498
|
-
const stamp = stampOf(
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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 =
|
|
3660
|
-
const sessionsDir =
|
|
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({
|
|
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
|
|
4123
|
-
return snapshot.sessions.some(
|
|
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) =>
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
4460
|
-
const
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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() ||
|
|
5008
|
-
const sessionsDir =
|
|
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(
|
|
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
|
-
|
|
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
|
|
5225
|
-
waits.push({ sessionId, from: began, to:
|
|
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
|
|
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
|
|
6226
|
+
let open2;
|
|
5325
6227
|
if (lastTurn === null) {
|
|
5326
|
-
|
|
6228
|
+
open2 = "idle";
|
|
5327
6229
|
} else if (typeof lastTurn !== "string") {
|
|
5328
6230
|
return "unknown";
|
|
5329
6231
|
} else if (TURN_UNDER_WAY.has(lastTurn)) {
|
|
5330
|
-
|
|
6232
|
+
open2 = "working";
|
|
5331
6233
|
} else if (TURN_OVER.has(lastTurn)) {
|
|
5332
|
-
|
|
6234
|
+
open2 = "idle";
|
|
5333
6235
|
} else {
|
|
5334
6236
|
return "unknown";
|
|
5335
6237
|
}
|
|
5336
|
-
return live2 === false ? "finished" :
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
|
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 (!
|
|
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
|
|
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" || !
|
|
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" || !
|
|
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
|
|
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
|
|
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:
|
|
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) =>
|
|
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
|
|
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 (!
|
|
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 (
|
|
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:
|
|
6749
|
+
return { supported: false, missing: isMissing2(error) };
|
|
5848
6750
|
}
|
|
5849
|
-
const
|
|
6751
|
+
const open2 = /* @__PURE__ */ new Set();
|
|
5850
6752
|
for (const name of names) {
|
|
5851
6753
|
const threadId = threadIdOfLock(name);
|
|
5852
|
-
if (threadId !== null)
|
|
6754
|
+
if (threadId !== null) open2.add(threadId);
|
|
5853
6755
|
}
|
|
5854
|
-
return { supported: true, open:
|
|
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 =
|
|
5906
|
-
const sessionsDir =
|
|
5907
|
-
const locksDir =
|
|
5908
|
-
const indexFile =
|
|
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 !
|
|
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 (!
|
|
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
|
|
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" || !
|
|
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 (!
|
|
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
|
|
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 =
|
|
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
|
|
7200
|
+
let open2;
|
|
6299
7201
|
try {
|
|
6300
|
-
|
|
7202
|
+
open2 = await io.openRegular(file);
|
|
6301
7203
|
} catch (error) {
|
|
6302
|
-
if (
|
|
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 (
|
|
6310
|
-
const bytes2 = await
|
|
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:
|
|
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
|
|
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(
|
|
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 (!
|
|
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(
|
|
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
|
|
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
|
|
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 =
|
|
6648
|
-
if (base.startsWith(".")) return
|
|
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 ?
|
|
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
|
|
7565
|
+
let open2;
|
|
6663
7566
|
try {
|
|
6664
|
-
|
|
7567
|
+
open2 = await io.openRegular(file);
|
|
6665
7568
|
} catch (error) {
|
|
6666
|
-
return
|
|
7569
|
+
return isMissing2(error) ? void 0 : null;
|
|
6667
7570
|
}
|
|
6668
7571
|
try {
|
|
6669
|
-
if (
|
|
6670
|
-
const bytes2 = await
|
|
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
|
|
7579
|
+
await open2.close().catch(() => {
|
|
6677
7580
|
});
|
|
6678
7581
|
}
|
|
6679
7582
|
}
|
|
6680
7583
|
async function commonDirOf(io, gitDir) {
|
|
6681
|
-
const content = await readGitFile(io,
|
|
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 :
|
|
7587
|
+
return target === null ? void 0 : path15.resolve(gitDir, target);
|
|
6685
7588
|
}
|
|
6686
7589
|
async function headIn(dir, kind, io) {
|
|
6687
|
-
const marker =
|
|
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 =
|
|
7598
|
+
gitDir = path15.resolve(dir, target);
|
|
6696
7599
|
} else {
|
|
6697
7600
|
return null;
|
|
6698
7601
|
}
|
|
6699
|
-
const content = await readGitFile(io,
|
|
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 (!
|
|
6709
|
-
const home =
|
|
6710
|
-
let dir =
|
|
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 =
|
|
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(
|
|
7624
|
+
kind = (await io.lstat(path15.join(dir, ".git"))).kind;
|
|
6722
7625
|
} catch (error) {
|
|
6723
|
-
if (!
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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 =
|
|
6885
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
8340
|
+
import path17 from "node:path";
|
|
7437
8341
|
|
|
7438
8342
|
// src/collector/history/historyFiles.ts
|
|
7439
8343
|
import {
|
|
7440
|
-
closeSync,
|
|
7441
|
-
constants as
|
|
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
|
|
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
|
|
7456
|
-
var
|
|
7457
|
-
var
|
|
7458
|
-
|
|
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
|
|
8364
|
+
function isMissing3(error) {
|
|
7463
8365
|
return error?.code === "ENOENT";
|
|
7464
8366
|
}
|
|
7465
8367
|
async function readRegular(file, maxBytes) {
|
|
7466
|
-
const handle = await
|
|
8368
|
+
const handle = await openWithoutFollowing(file, READ);
|
|
7467
8369
|
try {
|
|
7468
8370
|
const info = await handle.stat();
|
|
7469
|
-
if (!info.isFile()) throw
|
|
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 =
|
|
8386
|
+
const fd = openWithoutFollowingNow(file, READ);
|
|
7485
8387
|
try {
|
|
7486
|
-
const info =
|
|
7487
|
-
if (!info.isFile()) throw
|
|
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
|
-
|
|
8400
|
+
closeSync2(fd);
|
|
7499
8401
|
}
|
|
7500
8402
|
}
|
|
7501
8403
|
function writeNow(file, flags, text4) {
|
|
7502
|
-
const fd =
|
|
8404
|
+
const fd = openWithoutFollowingNow(file, flags, FILE_MODE);
|
|
7503
8405
|
try {
|
|
7504
|
-
if (!
|
|
8406
|
+
if (!fstatSync2(fd).isFile()) throw notOrdinary2();
|
|
7505
8407
|
writeSync(fd, text4);
|
|
7506
8408
|
} finally {
|
|
7507
|
-
|
|
8409
|
+
closeSync2(fd);
|
|
7508
8410
|
}
|
|
7509
8411
|
}
|
|
7510
8412
|
async function writeTo(file, flags, text4) {
|
|
7511
|
-
const handle = await
|
|
8413
|
+
const handle = await openWithoutFollowing(file, flags, FILE_MODE);
|
|
7512
8414
|
try {
|
|
7513
|
-
if (!(await handle.stat()).isFile()) throw
|
|
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
|
|
7525
|
-
lstatNow: (file) => infoOf2(
|
|
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 (!
|
|
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 (!
|
|
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:
|
|
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) =>
|
|
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
|
|
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 =
|
|
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
|
-
|
|
8244
|
-
|
|
8245
|
-
|
|
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
|
|
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
|
|
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 (!
|
|
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 (
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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
|
|
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 =
|
|
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 =
|
|
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) =>
|
|
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
|
|
9272
|
-
constants as
|
|
9273
|
-
fstatSync as
|
|
10175
|
+
closeSync as closeSync3,
|
|
10176
|
+
constants as constants6,
|
|
10177
|
+
fstatSync as fstatSync3,
|
|
9274
10178
|
fsyncSync,
|
|
9275
|
-
lstatSync as
|
|
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
|
|
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 =
|
|
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 =
|
|
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,
|
|
10215
|
+
fd = openSync2(folder, constants6.O_RDONLY);
|
|
9313
10216
|
fsyncSync(fd);
|
|
9314
10217
|
} catch {
|
|
9315
10218
|
} finally {
|
|
9316
|
-
if (fd !== null)
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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
|
-
|
|
9342
|
-
|
|
9343
|
-
|
|
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 =
|
|
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 {
|
|
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
|
-
|
|
10294
|
+
closeSync3(fd);
|
|
9374
10295
|
}
|
|
9375
10296
|
}
|
|
9376
10297
|
function writeSettingsText(setup, text4) {
|
|
9377
10298
|
const { file, shown } = setup;
|
|
9378
|
-
const folder =
|
|
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 =
|
|
10320
|
+
temporary = path20.join(
|
|
9400
10321
|
folder,
|
|
9401
|
-
`.${
|
|
10322
|
+
`.${path20.basename(file)}.${process.pid}.${randomBytes3(6).toString("hex")}.tmp`
|
|
9402
10323
|
);
|
|
9403
|
-
const fd =
|
|
10324
|
+
const fd = openWithoutFollowingNow(
|
|
9404
10325
|
temporary,
|
|
9405
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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 {
|
|
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 (!
|
|
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:
|
|
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 =
|
|
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 {
|
|
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
|
-
|
|
9484
|
-
let
|
|
9485
|
-
const
|
|
9486
|
-
let
|
|
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
|
-
|
|
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
|
-
|
|
9492
|
-
|
|
9493
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
10672
|
+
if (!asked.ok) return failed4(400, "invalid", asked.problem);
|
|
9542
10673
|
const saved = settings.changeTimeRules(asked.rules);
|
|
9543
|
-
if (!saved.ok) return
|
|
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 =
|
|
9696
|
-
|
|
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 ||
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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({
|
|
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: () =>
|
|
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(
|
|
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
|
-
|
|
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
|
}
|