agent-lookout 0.2.5 → 0.2.7
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 +45 -0
- package/PRIVACY.md +44 -38
- package/README.md +187 -159
- package/SECURITY.md +14 -9
- package/dist/assets/{HistoryPanel-TXa0oPH4.js → HistoryPanel-oSXRXEdv.js} +1 -1
- package/dist/assets/SessionPanel-DcJvw0F8.js +1 -0
- package/dist/assets/index-B_1Its0R.css +1 -0
- package/dist/assets/index-DLPmOXDe.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-3LYUYPG4.js} +1635 -399
- package/dist/index.html +2 -2
- package/docs/API.md +100 -53
- package/docs/GUIDE.md +129 -79
- package/docs/INSTALL.md +175 -0
- package/package.json +2 -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
|
|
@@ -246,10 +254,30 @@ function createQuietHold() {
|
|
|
246
254
|
const last = lastWaitOf(session.id);
|
|
247
255
|
if (last?.begunAt === begunAt) {
|
|
248
256
|
last.endedAt = null;
|
|
257
|
+
last.reminderOnly = false;
|
|
249
258
|
return;
|
|
250
259
|
}
|
|
251
260
|
if (last && last.endedAt === null) last.endedAt = begunAt;
|
|
252
|
-
waits.push({
|
|
261
|
+
waits.push({
|
|
262
|
+
session: withoutWaitingText(session),
|
|
263
|
+
begunAt,
|
|
264
|
+
endedAt: null,
|
|
265
|
+
reminderOnly: false
|
|
266
|
+
});
|
|
267
|
+
},
|
|
268
|
+
holdReminder(session, begunAt) {
|
|
269
|
+
const last = lastWaitOf(session.id);
|
|
270
|
+
if (last?.begunAt === begunAt) {
|
|
271
|
+
last.endedAt = null;
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (last && last.endedAt === null) last.endedAt = begunAt;
|
|
275
|
+
waits.push({
|
|
276
|
+
session: withoutWaitingText(session),
|
|
277
|
+
begunAt,
|
|
278
|
+
endedAt: null,
|
|
279
|
+
reminderOnly: true
|
|
280
|
+
});
|
|
253
281
|
},
|
|
254
282
|
holdOver(event, session, at) {
|
|
255
283
|
over.push({ event, session: withoutWaitingText(session), at });
|
|
@@ -264,14 +292,15 @@ function createQuietHold() {
|
|
|
264
292
|
const stillOpen = /* @__PURE__ */ new Map();
|
|
265
293
|
for (const wait of waits) {
|
|
266
294
|
const now = listed.get(wait.session.id);
|
|
267
|
-
if (now
|
|
295
|
+
if (now === void 0 || !needsYou(now)) continue;
|
|
268
296
|
if (wait.endedAt === null || now.statusSince === wait.begunAt) {
|
|
269
297
|
stillOpen.set(wait.session.id, wait);
|
|
270
298
|
}
|
|
271
299
|
}
|
|
272
|
-
const
|
|
300
|
+
const open2 = [];
|
|
273
301
|
for (const [id, wait] of stillOpen) {
|
|
274
|
-
|
|
302
|
+
if (wait.reminderOnly) continue;
|
|
303
|
+
open2.push({ session: listed.get(id), begunAt: wait.begunAt });
|
|
275
304
|
}
|
|
276
305
|
const answered = /* @__PURE__ */ new Map();
|
|
277
306
|
const lastBegun = /* @__PURE__ */ new Map();
|
|
@@ -309,7 +338,7 @@ function createQuietHold() {
|
|
|
309
338
|
since = null;
|
|
310
339
|
waits = [];
|
|
311
340
|
over = [];
|
|
312
|
-
return { summary: items.length > 0 ? { from, to: at, items } : null, open:
|
|
341
|
+
return { summary: items.length > 0 ? { from, to: at, items } : null, open: open2 };
|
|
313
342
|
}
|
|
314
343
|
};
|
|
315
344
|
}
|
|
@@ -328,6 +357,7 @@ var DAY_MS = 24 * HOUR_MS;
|
|
|
328
357
|
var WEEKDAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
|
|
329
358
|
var TIME_RULE_NAMES = ["longWait", "idle", "quietHours"];
|
|
330
359
|
var LONG_WAIT_MINUTES = { min: 1, max: 1440, default: 10 };
|
|
360
|
+
var REPEAT_MINUTES = { min: 5, max: 1440, default: 30 };
|
|
331
361
|
var IDLE_HOURS = { min: 1, max: 720, default: 48 };
|
|
332
362
|
var DEFAULT_TIME_RULES = {
|
|
333
363
|
longWait: { on: false, minutes: LONG_WAIT_MINUTES.default },
|
|
@@ -357,12 +387,22 @@ function holds(value, keys, exact2) {
|
|
|
357
387
|
function wholeNumberIn(value, range) {
|
|
358
388
|
return Number.isSafeInteger(value) && value >= range.min && value <= range.max;
|
|
359
389
|
}
|
|
360
|
-
function
|
|
390
|
+
function readRepeat(value, exact2) {
|
|
361
391
|
if (!isRecord(value) || !holds(value, ["on", "minutes"], exact2)) return null;
|
|
362
392
|
const { on, minutes } = value;
|
|
363
|
-
if (typeof on !== "boolean" || !wholeNumberIn(minutes,
|
|
393
|
+
if (typeof on !== "boolean" || !wholeNumberIn(minutes, REPEAT_MINUTES)) return null;
|
|
364
394
|
return { on, minutes };
|
|
365
395
|
}
|
|
396
|
+
function readLongWait(value, exact2) {
|
|
397
|
+
if (!isRecord(value)) return null;
|
|
398
|
+
const keys = value.repeat === void 0 ? ["on", "minutes"] : ["on", "minutes", "repeat"];
|
|
399
|
+
if (!holds(value, keys, exact2)) return null;
|
|
400
|
+
const { on, minutes } = value;
|
|
401
|
+
if (typeof on !== "boolean" || !wholeNumberIn(minutes, LONG_WAIT_MINUTES)) return null;
|
|
402
|
+
if (value.repeat === void 0) return { on, minutes };
|
|
403
|
+
const repeat = readRepeat(value.repeat, exact2);
|
|
404
|
+
return repeat === null ? null : { on, minutes, repeat };
|
|
405
|
+
}
|
|
366
406
|
function readIdle(value, exact2) {
|
|
367
407
|
if (!isRecord(value) || !holds(value, ["on", "hours"], exact2)) return null;
|
|
368
408
|
const { on, hours } = value;
|
|
@@ -394,7 +434,7 @@ var READERS = {
|
|
|
394
434
|
quietHours: readQuietHours
|
|
395
435
|
};
|
|
396
436
|
var SHAPES = {
|
|
397
|
-
longWait: `longWait must be {"on", "minutes"}, with minutes a whole number from ${LONG_WAIT_MINUTES.min} to ${LONG_WAIT_MINUTES.max}.`,
|
|
437
|
+
longWait: `longWait must be {"on", "minutes"}, with minutes a whole number from ${LONG_WAIT_MINUTES.min} to ${LONG_WAIT_MINUTES.max}, and may have "repeat", {"on", "minutes"} with minutes a whole number from ${REPEAT_MINUTES.min} to ${REPEAT_MINUTES.max}.`,
|
|
398
438
|
idle: `idle must be {"on", "hours"}, with hours a whole number from ${IDLE_HOURS.min} to ${IDLE_HOURS.max}.`,
|
|
399
439
|
quietHours: 'quietHours must be {"on", "from", "to", "days", "leaveOutAnswered"}, with from and to two different times written as 22:00 is, and days any of mon, tue, wed, thu, fri, sat and sun, each once.'
|
|
400
440
|
};
|
|
@@ -459,6 +499,18 @@ function quietOf(snapshot) {
|
|
|
459
499
|
}
|
|
460
500
|
|
|
461
501
|
// src/core/time-rules/reminders.ts
|
|
502
|
+
var MINUTE_MS = 6e4;
|
|
503
|
+
function reminderSchedule(rule) {
|
|
504
|
+
return {
|
|
505
|
+
thresholdMs: rule.minutes * MINUTE_MS,
|
|
506
|
+
everyMs: rule.repeat?.on ? rule.repeat.minutes * MINUTE_MS : null
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
function reminderNumber(waitedMs, schedule) {
|
|
510
|
+
if (waitedMs < schedule.thresholdMs) return -1;
|
|
511
|
+
if (schedule.everyMs === null) return 0;
|
|
512
|
+
return Math.floor((waitedMs - schedule.thresholdMs) / schedule.everyMs);
|
|
513
|
+
}
|
|
462
514
|
function readable(snapshot) {
|
|
463
515
|
return new Set(
|
|
464
516
|
snapshot.sources.filter((source) => source.state === "ok" || source.state === "not-set-up").map((source) => source.id)
|
|
@@ -471,35 +523,39 @@ function createReminderWatch() {
|
|
|
471
523
|
for (const id of stopped) watched.delete(id);
|
|
472
524
|
const sources = readable(snapshot);
|
|
473
525
|
for (const session of snapshot.sessions) {
|
|
474
|
-
if (session
|
|
526
|
+
if (!needsYou(session) || !sources.has(session.source)) continue;
|
|
475
527
|
const since = session.statusSince !== null && session.statusSince <= at ? session.statusSince : null;
|
|
476
528
|
const known = watched.get(session.id);
|
|
477
529
|
if (known && (since === null || since === known.begunAt)) continue;
|
|
478
530
|
watched.set(session.id, {
|
|
479
531
|
begunAt: since ?? at,
|
|
480
|
-
|
|
481
|
-
|
|
532
|
+
seenAfterMs: since === null ? 0 : at - since,
|
|
533
|
+
toldAfterMs: null
|
|
482
534
|
});
|
|
483
535
|
}
|
|
484
536
|
},
|
|
485
|
-
due(snapshot, at,
|
|
537
|
+
due(snapshot, at, schedule, eligible = () => true) {
|
|
486
538
|
const found = [];
|
|
487
539
|
for (const session of snapshot.sessions) {
|
|
488
|
-
if (session
|
|
540
|
+
if (!needsYou(session)) continue;
|
|
489
541
|
const wait = watched.get(session.id);
|
|
490
|
-
if (!wait ||
|
|
542
|
+
if (!wait || !eligible(session.id)) continue;
|
|
491
543
|
const waitedMs = at - wait.begunAt;
|
|
492
|
-
|
|
493
|
-
|
|
544
|
+
const repeat = reminderNumber(waitedMs, schedule);
|
|
545
|
+
if (repeat < 0 || repeat <= reminderNumber(wait.seenAfterMs, schedule)) continue;
|
|
546
|
+
const told = wait.toldAfterMs;
|
|
547
|
+
if (told !== null && repeat <= reminderNumber(told, schedule)) continue;
|
|
548
|
+
const { thresholdMs, everyMs } = schedule;
|
|
549
|
+
if (told !== null && everyMs !== null && repeat > 0) {
|
|
550
|
+
if (thresholdMs + repeat * everyMs - told < everyMs / 2) continue;
|
|
551
|
+
}
|
|
552
|
+
found.push({ session, begunAt: wait.begunAt, waitedMs, repeat });
|
|
494
553
|
}
|
|
495
554
|
return found;
|
|
496
555
|
},
|
|
497
|
-
|
|
498
|
-
watched.get(sessionId)?.reminded.add(thresholdMs);
|
|
499
|
-
},
|
|
500
|
-
toldLate(sessionId, at, thresholdMs) {
|
|
556
|
+
told(sessionId, at) {
|
|
501
557
|
const wait = watched.get(sessionId);
|
|
502
|
-
if (wait
|
|
558
|
+
if (wait) wait.toldAfterMs = Math.max(wait.toldAfterMs ?? 0, at - wait.begunAt);
|
|
503
559
|
},
|
|
504
560
|
clear() {
|
|
505
561
|
watched.clear();
|
|
@@ -532,7 +588,7 @@ function createOutboundChannel(options) {
|
|
|
532
588
|
const wanted = new Set(options.events);
|
|
533
589
|
let memory = EMPTY_CHANGE_MEMORY;
|
|
534
590
|
const ready = [];
|
|
535
|
-
const
|
|
591
|
+
const open2 = /* @__PURE__ */ new Map();
|
|
536
592
|
const done = /* @__PURE__ */ new Map();
|
|
537
593
|
let sentAt = [];
|
|
538
594
|
const reminders = createReminderWatch();
|
|
@@ -558,7 +614,7 @@ function createOutboundChannel(options) {
|
|
|
558
614
|
const result = sessionChanges(memory, snapshot);
|
|
559
615
|
memory = result.memory;
|
|
560
616
|
for (const id of result.stopped) {
|
|
561
|
-
|
|
617
|
+
open2.delete(id);
|
|
562
618
|
hold.waitEnded(id, at);
|
|
563
619
|
}
|
|
564
620
|
reminders.observe(snapshot, result.stopped, at);
|
|
@@ -572,7 +628,7 @@ function createOutboundChannel(options) {
|
|
|
572
628
|
}));
|
|
573
629
|
ready.push((sentNow) => summaryMessage({ ...summary, items, now: sentNow }));
|
|
574
630
|
}
|
|
575
|
-
for (const { session, begunAt } of end.open)
|
|
631
|
+
for (const { session, begunAt } of end.open) open2.set(session.id, begunAt);
|
|
576
632
|
}
|
|
577
633
|
if (quiet) hold.begin(at);
|
|
578
634
|
for (const { event, session } of result.changes) {
|
|
@@ -593,7 +649,7 @@ function createOutboundChannel(options) {
|
|
|
593
649
|
}
|
|
594
650
|
const since = session.statusSince;
|
|
595
651
|
if (since !== null && done.get(session.id) === since) continue;
|
|
596
|
-
|
|
652
|
+
open2.set(session.id, waitBegan(since, at));
|
|
597
653
|
}
|
|
598
654
|
sentAt = sendsInLastHour(sentAt, at);
|
|
599
655
|
while (!quiet && ready.length > 0 && limitLiftsAt(sentAt, at) === null) {
|
|
@@ -601,21 +657,21 @@ function createOutboundChannel(options) {
|
|
|
601
657
|
sentAt.push(at);
|
|
602
658
|
send2(write(at));
|
|
603
659
|
}
|
|
604
|
-
const
|
|
605
|
-
for (const [id, begunAt] of
|
|
660
|
+
const schedule = reminderSchedule(rules.longWait);
|
|
661
|
+
for (const [id, begunAt] of open2) {
|
|
606
662
|
const session = snapshot.sessions.find(
|
|
607
|
-
(candidate) => candidate.id === id && candidate
|
|
663
|
+
(candidate) => candidate.id === id && needsYou(candidate)
|
|
608
664
|
);
|
|
609
665
|
if (!session) continue;
|
|
610
666
|
const timing = sendTiming(begunAt, afterMs, sentAt, at);
|
|
611
667
|
if (timing === "wait") continue;
|
|
612
668
|
if (quiet) {
|
|
613
|
-
|
|
669
|
+
open2.delete(id);
|
|
614
670
|
hold.holdWait(session, begunAt);
|
|
615
671
|
continue;
|
|
616
672
|
}
|
|
617
673
|
if (timing === "limited") continue;
|
|
618
|
-
|
|
674
|
+
open2.delete(id);
|
|
619
675
|
sentAt.push(at);
|
|
620
676
|
send2(
|
|
621
677
|
waitMessage({
|
|
@@ -627,13 +683,17 @@ function createOutboundChannel(options) {
|
|
|
627
683
|
now: at
|
|
628
684
|
})
|
|
629
685
|
);
|
|
630
|
-
if (rules.longWait.on) reminders.
|
|
686
|
+
if (rules.longWait.on) reminders.told(id, at);
|
|
631
687
|
}
|
|
632
|
-
if (
|
|
633
|
-
for (const due of reminders.due(snapshot, at,
|
|
688
|
+
if (rules.longWait.on && wanted.has("needs-you")) {
|
|
689
|
+
for (const due of reminders.due(snapshot, at, schedule, (id) => !open2.has(id))) {
|
|
690
|
+
if (quiet) {
|
|
691
|
+
hold.holdReminder(due.session, due.begunAt);
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
634
694
|
if (limitLiftsAt(sentAt, at) !== null) break;
|
|
635
695
|
sentAt.push(at);
|
|
636
|
-
reminders.
|
|
696
|
+
reminders.told(due.session.id, at);
|
|
637
697
|
send2(
|
|
638
698
|
reminderMessage({
|
|
639
699
|
session: withoutWaitingText(due.session),
|
|
@@ -641,13 +701,15 @@ function createOutboundChannel(options) {
|
|
|
641
701
|
asking: asking ? waitingText(due.session.waitingText) ?? null : null,
|
|
642
702
|
begunAt: due.begunAt,
|
|
643
703
|
now: at,
|
|
644
|
-
thresholdMs
|
|
704
|
+
thresholdMs: schedule.thresholdMs,
|
|
705
|
+
everyMs: schedule.everyMs,
|
|
706
|
+
repeat: due.repeat
|
|
645
707
|
})
|
|
646
708
|
);
|
|
647
709
|
}
|
|
648
710
|
}
|
|
649
711
|
for (const session of snapshot.sessions) {
|
|
650
|
-
if (session
|
|
712
|
+
if (needsYou(session) && session.statusSince !== null && !open2.has(session.id)) {
|
|
651
713
|
done.set(session.id, session.statusSince);
|
|
652
714
|
}
|
|
653
715
|
}
|
|
@@ -661,9 +723,9 @@ function createOutboundChannel(options) {
|
|
|
661
723
|
}
|
|
662
724
|
|
|
663
725
|
// src/core/time-rules/timeRulesWords.ts
|
|
664
|
-
var
|
|
726
|
+
var MINUTE_MS2 = 6e4;
|
|
665
727
|
function waitedInWords(ms) {
|
|
666
|
-
if (ms <
|
|
728
|
+
if (ms < MINUTE_MS2) return "under a minute";
|
|
667
729
|
return partsInWords(durationParts(ms).filter((part) => part.unit !== "s"));
|
|
668
730
|
}
|
|
669
731
|
var WAITED_FOR = {
|
|
@@ -865,9 +927,10 @@ function waitEmail(facts) {
|
|
|
865
927
|
}
|
|
866
928
|
function reminderEmail(facts) {
|
|
867
929
|
const { session, begunAt, now } = facts;
|
|
930
|
+
const again = facts.everyMs === null ? "" : `, and again every ${waitedInWords(facts.everyMs)} while it goes on`;
|
|
868
931
|
return emailOf(
|
|
869
932
|
reminderSentence(session, now - begunAt),
|
|
870
|
-
`It has waited since ${clockAt(begunAt, now)}. Agent Lookout reminds you once a wait lasts ${waitedInWords(facts.thresholdMs)}, as Time rules in Settings says.`,
|
|
933
|
+
`It has waited since ${clockAt(begunAt, now)}. Agent Lookout reminds you once a wait lasts ${waitedInWords(facts.thresholdMs)}${again}, as Time rules in Settings says.`,
|
|
871
934
|
session,
|
|
872
935
|
facts.agent,
|
|
873
936
|
facts.asking
|
|
@@ -998,15 +1061,15 @@ function recordValue(record) {
|
|
|
998
1061
|
case "cleared":
|
|
999
1062
|
return { cleared: record.at };
|
|
1000
1063
|
case "point": {
|
|
1001
|
-
const { at, needsYou, working, idle, total } = record.point;
|
|
1002
|
-
return { point: [at,
|
|
1064
|
+
const { at, needsYou: needsYou2, working, idle, total } = record.point;
|
|
1065
|
+
return { point: [at, needsYou2, working, idle, total] };
|
|
1003
1066
|
}
|
|
1004
1067
|
case "event":
|
|
1005
1068
|
return { event: eventFields(record.event) };
|
|
1006
1069
|
}
|
|
1007
1070
|
}
|
|
1008
1071
|
function eventFields(event) {
|
|
1009
|
-
const { id, at, sessionId, sessionName: sessionName2, kind, from, to, severity, by, decision } = event;
|
|
1072
|
+
const { id, at, sessionId, sessionName: sessionName2, kind, from, to, severity, by, decision, tool, rule } = event;
|
|
1010
1073
|
return {
|
|
1011
1074
|
id,
|
|
1012
1075
|
at,
|
|
@@ -1017,7 +1080,12 @@ function eventFields(event) {
|
|
|
1017
1080
|
...to !== void 0 && { to },
|
|
1018
1081
|
severity,
|
|
1019
1082
|
...by !== void 0 && { by },
|
|
1020
|
-
...decision !== void 0 && { decision }
|
|
1083
|
+
...decision !== void 0 && { decision },
|
|
1084
|
+
...tool !== void 0 && { tool },
|
|
1085
|
+
// The rule's own two fields, and nothing else it might carry.
|
|
1086
|
+
...rule !== void 0 && {
|
|
1087
|
+
rule: { tool: rule.tool, ...rule.command !== void 0 && { command: rule.command } }
|
|
1088
|
+
}
|
|
1021
1089
|
};
|
|
1022
1090
|
}
|
|
1023
1091
|
function isRecord2(value) {
|
|
@@ -1038,11 +1106,11 @@ function oneOf(list, value) {
|
|
|
1038
1106
|
function readPoint(value) {
|
|
1039
1107
|
if (!Array.isArray(value) || value.length !== 5) return null;
|
|
1040
1108
|
const at = moment(value[0]);
|
|
1041
|
-
const [
|
|
1042
|
-
if (at === null ||
|
|
1109
|
+
const [needsYou2, working, idle, total] = value.slice(1).map(count);
|
|
1110
|
+
if (at === null || needsYou2 == null || working == null || idle == null || total == null) {
|
|
1043
1111
|
return null;
|
|
1044
1112
|
}
|
|
1045
|
-
return { at, needsYou, working, idle, total };
|
|
1113
|
+
return { at, needsYou: needsYou2, working, idle, total };
|
|
1046
1114
|
}
|
|
1047
1115
|
function readEvent(value) {
|
|
1048
1116
|
if (!isRecord2(value)) return null;
|
|
@@ -1060,6 +1128,10 @@ function readEvent(value) {
|
|
|
1060
1128
|
if (value.by !== void 0 && !by) return null;
|
|
1061
1129
|
const decision = oneOf(ANSWER_DECISIONS, value.decision);
|
|
1062
1130
|
if (value.decision !== void 0 && !decision) return null;
|
|
1131
|
+
const tool = field(value.tool);
|
|
1132
|
+
if (value.tool !== void 0 && !tool) return null;
|
|
1133
|
+
const rule = value.rule === void 0 ? void 0 : readRule(value.rule);
|
|
1134
|
+
if (rule === null) return null;
|
|
1063
1135
|
return {
|
|
1064
1136
|
id,
|
|
1065
1137
|
at,
|
|
@@ -1070,9 +1142,20 @@ function readEvent(value) {
|
|
|
1070
1142
|
...to && { to },
|
|
1071
1143
|
severity,
|
|
1072
1144
|
...by && { by },
|
|
1073
|
-
...decision && { decision }
|
|
1145
|
+
...decision && { decision },
|
|
1146
|
+
...tool && { tool },
|
|
1147
|
+
...rule && { rule }
|
|
1074
1148
|
};
|
|
1075
1149
|
}
|
|
1150
|
+
function readRule(value) {
|
|
1151
|
+
if (!isRecord2(value)) return null;
|
|
1152
|
+
const keys = Object.keys(value);
|
|
1153
|
+
if (!keys.every((key) => key === "tool" || key === "command")) return null;
|
|
1154
|
+
const tool = field(value.tool);
|
|
1155
|
+
const command = value.command === void 0 ? void 0 : field(value.command);
|
|
1156
|
+
if (!tool || command === null) return null;
|
|
1157
|
+
return { tool, ...command !== void 0 && { command } };
|
|
1158
|
+
}
|
|
1076
1159
|
function parseRecord(line) {
|
|
1077
1160
|
if (line === "" || line.length > MAX_LINE_LENGTH) return null;
|
|
1078
1161
|
let value;
|
|
@@ -1291,6 +1374,34 @@ function staleBy(sessions, at, thresholdMs) {
|
|
|
1291
1374
|
return stale === session.stale ? session : { ...session, stale };
|
|
1292
1375
|
});
|
|
1293
1376
|
}
|
|
1377
|
+
function forEvents(sessions, previous, answered) {
|
|
1378
|
+
if (answered.size === 0) return sessions;
|
|
1379
|
+
const before = new Map(previous.map((session) => [session.id, session]));
|
|
1380
|
+
const taken = [];
|
|
1381
|
+
for (const session of sessions) {
|
|
1382
|
+
if (!answered.has(session.id)) {
|
|
1383
|
+
taken.push(session);
|
|
1384
|
+
continue;
|
|
1385
|
+
}
|
|
1386
|
+
const was = before.get(session.id);
|
|
1387
|
+
if (was) taken.push(was);
|
|
1388
|
+
}
|
|
1389
|
+
return taken;
|
|
1390
|
+
}
|
|
1391
|
+
function markAnswered(sessions, answered) {
|
|
1392
|
+
if (answered.size === 0) return sessions;
|
|
1393
|
+
return sessions.map(
|
|
1394
|
+
(session) => answered.has(session.id) ? { ...session, answered: true } : session
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
var NONE_ANSWERED = /* @__PURE__ */ new Set();
|
|
1398
|
+
function withAnsweredThere(answered, sessions) {
|
|
1399
|
+
const there = sessions.filter(
|
|
1400
|
+
(session) => session.machine !== void 0 && session.answered === true
|
|
1401
|
+
);
|
|
1402
|
+
if (there.length === 0) return answered;
|
|
1403
|
+
return /* @__PURE__ */ new Set([...answered, ...there.map((session) => session.id)]);
|
|
1404
|
+
}
|
|
1294
1405
|
function declared(adapter) {
|
|
1295
1406
|
return adapter.capabilities === void 0 ? {} : { capabilities: adapter.capabilities };
|
|
1296
1407
|
}
|
|
@@ -1345,11 +1456,11 @@ function createPoller(options) {
|
|
|
1345
1456
|
};
|
|
1346
1457
|
}
|
|
1347
1458
|
function ask(adapter) {
|
|
1348
|
-
const
|
|
1349
|
-
if (
|
|
1350
|
-
if (
|
|
1459
|
+
const open2 = overdue.get(adapter);
|
|
1460
|
+
if (open2) {
|
|
1461
|
+
if (open2.late === void 0) return Promise.resolve(stillWaiting(adapter, open2.since));
|
|
1351
1462
|
overdue.delete(adapter);
|
|
1352
|
-
return Promise.resolve(
|
|
1463
|
+
return Promise.resolve(open2.late);
|
|
1353
1464
|
}
|
|
1354
1465
|
const since = now();
|
|
1355
1466
|
return new Promise((resolve) => {
|
|
@@ -1393,10 +1504,24 @@ function createPoller(options) {
|
|
|
1393
1504
|
return sessions;
|
|
1394
1505
|
}
|
|
1395
1506
|
}
|
|
1507
|
+
function answeredIn(snapshot) {
|
|
1508
|
+
if (!options.answered) return NONE_ANSWERED;
|
|
1509
|
+
try {
|
|
1510
|
+
return options.answered(snapshot);
|
|
1511
|
+
} catch {
|
|
1512
|
+
return NONE_ANSWERED;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1396
1515
|
async function poll() {
|
|
1397
1516
|
const results = await Promise.all(adapters.map((adapter) => ask(adapter)));
|
|
1398
1517
|
const found = await annotated(results.flatMap((result) => result.sessions));
|
|
1399
1518
|
const at = now();
|
|
1519
|
+
const sources = results.map((result, index) => ({
|
|
1520
|
+
...result.health,
|
|
1521
|
+
...result.basis !== void 0 && { basis: result.basis },
|
|
1522
|
+
...declared(adapters[index])
|
|
1523
|
+
}));
|
|
1524
|
+
const answered = withAnsweredThere(answeredIn({ sources, sessions: found }), found);
|
|
1400
1525
|
const changes = [];
|
|
1401
1526
|
results.forEach((result, index) => {
|
|
1402
1527
|
const { state } = result.health;
|
|
@@ -1413,35 +1538,29 @@ function createPoller(options) {
|
|
|
1413
1538
|
const { baselines, reported } = remembered;
|
|
1414
1539
|
const basis = result.basis ?? "";
|
|
1415
1540
|
const previous = baselines.get(basis);
|
|
1541
|
+
const sessions = previous ? forEvents(result.sessions, previous, answered) : result.sessions;
|
|
1416
1542
|
if (previous) {
|
|
1417
|
-
changes.push(...withoutRepeats(diffSessions(previous,
|
|
1543
|
+
changes.push(...withoutRepeats(diffSessions(previous, sessions, at), reported));
|
|
1418
1544
|
} else {
|
|
1419
1545
|
if (lastSeen) {
|
|
1420
1546
|
const known = new Set(lastSeen.map((session) => session.id));
|
|
1421
1547
|
const before = lastSeen.filter(
|
|
1422
1548
|
(session) => session.status !== null
|
|
1423
1549
|
);
|
|
1424
|
-
const after =
|
|
1550
|
+
const after = sessions.filter((session) => known.has(session.id));
|
|
1425
1551
|
changes.push(...diffSessions(before, after, at));
|
|
1426
1552
|
}
|
|
1427
|
-
for (const session of
|
|
1553
|
+
for (const session of sessions) {
|
|
1428
1554
|
if (!reported.has(session.id)) reported.set(session.id, session.status);
|
|
1429
1555
|
}
|
|
1430
1556
|
}
|
|
1431
|
-
baselines.set(basis,
|
|
1557
|
+
baselines.set(basis, sessions.map(withoutWaitingText));
|
|
1432
1558
|
});
|
|
1433
1559
|
const rules = options.timeRules?.();
|
|
1434
1560
|
const snapshot = {
|
|
1435
1561
|
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))),
|
|
1562
|
+
sources,
|
|
1563
|
+
sessions: sortSessions(staleBy(markAnswered(found, answered), at, staleAfterMs(rules))),
|
|
1445
1564
|
...rules && { timeRules: rules, quiet: isQuietAt(rules.quietHours, at) }
|
|
1446
1565
|
};
|
|
1447
1566
|
latest = snapshot;
|
|
@@ -1570,7 +1689,9 @@ function reminderPost(facts) {
|
|
|
1570
1689
|
post.session.app,
|
|
1571
1690
|
post.session.agent
|
|
1572
1691
|
]),
|
|
1573
|
-
reminder: true
|
|
1692
|
+
reminder: true,
|
|
1693
|
+
// Left out on the first, so its post is the post it always was.
|
|
1694
|
+
...facts.repeat > 0 && { repeat: facts.repeat }
|
|
1574
1695
|
};
|
|
1575
1696
|
}
|
|
1576
1697
|
function summaryPost(facts) {
|
|
@@ -1791,11 +1912,12 @@ function createApiHandler(options) {
|
|
|
1791
1912
|
if (options.cleanUp) actions.set(CLEAN_UP_PATH, options.cleanUp);
|
|
1792
1913
|
if (options.answer) actions.set(ANSWER_PATH, options.answer);
|
|
1793
1914
|
if (options.timeRules) actions.set(TIME_RULES_PATH, options.timeRules);
|
|
1915
|
+
if (options.permissionRules) actions.set(PERMISSION_RULES_PATH, options.permissionRules);
|
|
1794
1916
|
const serveSnapshot = options.serveSnapshot ?? ((snapshot) => snapshot);
|
|
1795
1917
|
function route(req, res) {
|
|
1796
|
-
const
|
|
1797
|
-
if (
|
|
1798
|
-
fail(res, 403,
|
|
1918
|
+
const refusal3 = refusalFor(req);
|
|
1919
|
+
if (refusal3) {
|
|
1920
|
+
fail(res, 403, refusal3);
|
|
1799
1921
|
return;
|
|
1800
1922
|
}
|
|
1801
1923
|
let url;
|
|
@@ -2136,7 +2258,7 @@ function urlFor(address) {
|
|
|
2136
2258
|
|
|
2137
2259
|
// src/collector/hosts/standalone.ts
|
|
2138
2260
|
import { existsSync } from "node:fs";
|
|
2139
|
-
import
|
|
2261
|
+
import path21 from "node:path";
|
|
2140
2262
|
|
|
2141
2263
|
// src/core/mapping/claudeCodeMapping.ts
|
|
2142
2264
|
var LIVE_STATUS = /* @__PURE__ */ new Map([
|
|
@@ -2195,7 +2317,7 @@ function claudeCodeOpenLink(surface, sessionId) {
|
|
|
2195
2317
|
}
|
|
2196
2318
|
|
|
2197
2319
|
// src/collector/terminal/terminalTabs.ts
|
|
2198
|
-
import path2 from "node:path";
|
|
2320
|
+
import path2 from "node:path/posix";
|
|
2199
2321
|
var TERMINAL_PATH = "/System/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal";
|
|
2200
2322
|
var ITERM_PATH = "/Applications/iTerm.app/Contents/MacOS/iTerm2";
|
|
2201
2323
|
var ITERM_SERVER_FOLDER = "Library/Application Support/iTerm2";
|
|
@@ -2785,7 +2907,15 @@ import path4 from "node:path";
|
|
|
2785
2907
|
import { constants } from "node:fs";
|
|
2786
2908
|
import { access, stat as stat2 } from "node:fs/promises";
|
|
2787
2909
|
import path3 from "node:path";
|
|
2788
|
-
|
|
2910
|
+
function pathsOf(platform) {
|
|
2911
|
+
return platform === "win32" ? path3.win32 : path3.posix;
|
|
2912
|
+
}
|
|
2913
|
+
var WINDOWS_PROGRAM_ENDINGS = [".com", ".exe"];
|
|
2914
|
+
function isWindowsProgram(file) {
|
|
2915
|
+
return WINDOWS_PROGRAM_ENDINGS.includes(path3.win32.extname(file).toLowerCase());
|
|
2916
|
+
}
|
|
2917
|
+
async function isExecutableFile(candidate, platform = process.platform) {
|
|
2918
|
+
if (platform === "win32" && !isWindowsProgram(candidate)) return false;
|
|
2789
2919
|
try {
|
|
2790
2920
|
const info = await stat2(candidate);
|
|
2791
2921
|
if (!info.isFile()) return false;
|
|
@@ -2795,44 +2925,103 @@ async function isExecutableFile(candidate) {
|
|
|
2795
2925
|
return false;
|
|
2796
2926
|
}
|
|
2797
2927
|
}
|
|
2798
|
-
function
|
|
2799
|
-
|
|
2928
|
+
function variable(env, name, platform = process.platform) {
|
|
2929
|
+
if (platform !== "win32" || env[name] !== void 0) return env[name];
|
|
2930
|
+
const key = Object.keys(env).find((each) => each.toUpperCase() === name);
|
|
2931
|
+
return key === void 0 ? void 0 : env[key];
|
|
2800
2932
|
}
|
|
2801
|
-
function
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2933
|
+
function programEndings(env) {
|
|
2934
|
+
const listed = (variable(env, "PATHEXT", "win32") ?? "").split(";").map((ending) => ending.trim().toLowerCase()).filter((ending) => WINDOWS_PROGRAM_ENDINGS.includes(ending));
|
|
2935
|
+
return listed.length > 0 ? [...new Set(listed)] : WINDOWS_PROGRAM_ENDINGS;
|
|
2936
|
+
}
|
|
2937
|
+
function pathFolders(env, platform = process.platform) {
|
|
2938
|
+
const paths = pathsOf(platform);
|
|
2939
|
+
return (variable(env, "PATH", platform) ?? "").split(paths.delimiter).map((dir) => platform === "win32" ? dir.trim().replace(/^"(.*)"$/, "$1") : dir).filter((dir) => dir !== "" && paths.isAbsolute(dir));
|
|
2940
|
+
}
|
|
2941
|
+
function programNames(env, program, platform = process.platform) {
|
|
2942
|
+
if (platform !== "win32" || isWindowsProgram(program)) return [program];
|
|
2943
|
+
return programEndings(env).map((ending) => `${program}${ending}`);
|
|
2944
|
+
}
|
|
2945
|
+
function pathCandidates(env, program, platform = process.platform) {
|
|
2946
|
+
const paths = pathsOf(platform);
|
|
2947
|
+
const names = programNames(env, program, platform);
|
|
2948
|
+
return pathFolders(env, platform).flatMap((dir) => names.map((name) => paths.join(dir, name)));
|
|
2949
|
+
}
|
|
2950
|
+
function tildify(target, homeDir, platform = process.platform) {
|
|
2951
|
+
if (homeDir === "") return target;
|
|
2952
|
+
if (platform !== "win32") {
|
|
2953
|
+
if (homeDir === "/") return target;
|
|
2954
|
+
if (target === homeDir) return "~";
|
|
2955
|
+
if (target.startsWith(`${homeDir}/`)) return `~${target.slice(homeDir.length)}`;
|
|
2956
|
+
return target;
|
|
2957
|
+
}
|
|
2958
|
+
const home = path3.win32.resolve(homeDir);
|
|
2959
|
+
if (path3.win32.parse(home).root === home) return target;
|
|
2960
|
+
const inside = path3.win32.relative(home, path3.win32.resolve(target));
|
|
2961
|
+
if (inside === "") return "~";
|
|
2962
|
+
if (inside === ".." || inside.startsWith("..\\") || path3.win32.isAbsolute(inside)) return target;
|
|
2963
|
+
return `~/${inside.replaceAll("\\", "/")}`;
|
|
2806
2964
|
}
|
|
2807
2965
|
|
|
2808
2966
|
// src/collector/adapters/claude-code/findBinary.ts
|
|
2809
2967
|
var CLAUDE_BIN_ENV = "AGENT_LOOKOUT_CLAUDE_BIN";
|
|
2810
|
-
|
|
2968
|
+
var NPM_PROGRAM = ["node_modules", "@anthropic-ai", "claude-code", "bin", "claude.exe"];
|
|
2969
|
+
function npmFolder(env, homeDir) {
|
|
2970
|
+
const appData = variable(env, "APPDATA", "win32")?.trim();
|
|
2971
|
+
return appData && path4.win32.isAbsolute(appData) ? path4.win32.join(appData, "npm") : path4.win32.join(homeDir, "AppData", "Roaming", "npm");
|
|
2972
|
+
}
|
|
2973
|
+
function fixedLocations(homeDir, platform = process.platform, env = {}) {
|
|
2974
|
+
if (platform === "win32") {
|
|
2975
|
+
return [
|
|
2976
|
+
path4.win32.join(homeDir, ".local", "bin", "claude.exe"),
|
|
2977
|
+
path4.win32.join(npmFolder(env, homeDir), ...NPM_PROGRAM)
|
|
2978
|
+
];
|
|
2979
|
+
}
|
|
2811
2980
|
return [
|
|
2812
|
-
path4.join(homeDir, ".local", "bin", "claude"),
|
|
2981
|
+
path4.posix.join(homeDir, ".local", "bin", "claude"),
|
|
2813
2982
|
"/opt/homebrew/bin/claude",
|
|
2814
2983
|
"/usr/local/bin/claude",
|
|
2815
|
-
path4.join(homeDir, ".npm-global", "bin", "claude"),
|
|
2984
|
+
path4.posix.join(homeDir, ".npm-global", "bin", "claude"),
|
|
2816
2985
|
"/usr/bin/claude"
|
|
2817
2986
|
];
|
|
2818
2987
|
}
|
|
2988
|
+
function onPath(env, platform) {
|
|
2989
|
+
if (platform !== "win32") return pathCandidates(env, "claude", platform);
|
|
2990
|
+
const names = programNames(env, "claude", platform);
|
|
2991
|
+
return pathFolders(env, platform).flatMap((dir) => [
|
|
2992
|
+
...names.map((name) => path4.win32.join(dir, name)),
|
|
2993
|
+
path4.win32.join(dir, ...NPM_PROGRAM)
|
|
2994
|
+
]);
|
|
2995
|
+
}
|
|
2996
|
+
var WINDOWS_SCRIPT = /\.(bat|cmd|ps1)$/i;
|
|
2819
2997
|
async function findClaudeBinary(options) {
|
|
2820
|
-
const
|
|
2998
|
+
const platform = options.platform ?? process.platform;
|
|
2999
|
+
const paths = pathsOf(platform);
|
|
3000
|
+
const isExecutable = options.isExecutable ?? ((candidate) => isExecutableFile(candidate, platform));
|
|
2821
3001
|
const named = options.env[CLAUDE_BIN_ENV]?.trim();
|
|
2822
3002
|
if (named) {
|
|
2823
|
-
const resolved =
|
|
3003
|
+
const resolved = paths.resolve(named);
|
|
2824
3004
|
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
|
-
};
|
|
3005
|
+
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";
|
|
3006
|
+
return { found: false, looked: `${CLAUDE_BIN_ENV} is set to ${named}, which is ${why}` };
|
|
2829
3007
|
}
|
|
2830
|
-
const fixed = fixedLocations(options.homeDir);
|
|
2831
|
-
const candidates = [.../* @__PURE__ */ new Set([...
|
|
3008
|
+
const fixed = fixedLocations(options.homeDir, platform, options.env);
|
|
3009
|
+
const candidates = [.../* @__PURE__ */ new Set([...onPath(options.env, platform), ...fixed])];
|
|
2832
3010
|
for (const candidate of candidates) {
|
|
2833
3011
|
if (await isExecutable(candidate)) return { found: true, path: candidate };
|
|
2834
3012
|
}
|
|
2835
|
-
|
|
3013
|
+
if (platform === "win32") {
|
|
3014
|
+
const places = [paths.dirname(fixed[0]), npmFolder(options.env, options.homeDir)].map(
|
|
3015
|
+
(place) => tildify(place, options.homeDir, platform)
|
|
3016
|
+
);
|
|
3017
|
+
return {
|
|
3018
|
+
found: false,
|
|
3019
|
+
looked: `The claude command, claude.exe, was not found on PATH or in ${listWithOr(places)}`
|
|
3020
|
+
};
|
|
3021
|
+
}
|
|
3022
|
+
const fixedDirs = fixed.map(
|
|
3023
|
+
(candidate) => tildify(paths.dirname(candidate), options.homeDir, platform)
|
|
3024
|
+
);
|
|
2836
3025
|
return {
|
|
2837
3026
|
found: false,
|
|
2838
3027
|
looked: `The claude command was not found on PATH or in ${listWithOr(fixedDirs)}`
|
|
@@ -2845,7 +3034,6 @@ function listWithOr(items) {
|
|
|
2845
3034
|
|
|
2846
3035
|
// src/collector/adapters/claude-code/index.ts
|
|
2847
3036
|
import os from "node:os";
|
|
2848
|
-
import path9 from "node:path";
|
|
2849
3037
|
|
|
2850
3038
|
// src/core/time.ts
|
|
2851
3039
|
var EARLIEST_PLAUSIBLE_TIME_MS = Date.UTC(2020, 0, 1);
|
|
@@ -3044,13 +3232,73 @@ function createFinishedTracker(retentionMs = FINISHED_RETENTION_MS) {
|
|
|
3044
3232
|
}
|
|
3045
3233
|
|
|
3046
3234
|
// src/collector/adapters/claude-code/registry.ts
|
|
3047
|
-
import { constants as
|
|
3048
|
-
import {
|
|
3235
|
+
import { constants as constants3 } from "node:fs";
|
|
3236
|
+
import { readdir } from "node:fs/promises";
|
|
3049
3237
|
import path5 from "node:path";
|
|
3238
|
+
|
|
3239
|
+
// src/collector/files/noFollow.ts
|
|
3240
|
+
import { closeSync, constants as constants2, fstatSync, lstatSync, openSync } from "node:fs";
|
|
3241
|
+
import { lstat, open } from "node:fs/promises";
|
|
3242
|
+
var OPEN_FOLLOWS_LINKS = constants2.O_NOFOLLOW === void 0;
|
|
3243
|
+
var NO_FOLLOW = (constants2.O_NOFOLLOW ?? 0) | (constants2.O_NONBLOCK ?? 0);
|
|
3244
|
+
function notOrdinary() {
|
|
3245
|
+
return new Error("Not an ordinary file.");
|
|
3246
|
+
}
|
|
3247
|
+
function refusal2(named) {
|
|
3248
|
+
if (!named.isSymbolicLink()) return notOrdinary();
|
|
3249
|
+
return Object.assign(new Error("A symbolic link, which is not followed."), { code: "ELOOP" });
|
|
3250
|
+
}
|
|
3251
|
+
function sameFile(named, opened) {
|
|
3252
|
+
return named.isFile() && named.dev === opened.dev && named.ino === opened.ino;
|
|
3253
|
+
}
|
|
3254
|
+
function isMissing(error) {
|
|
3255
|
+
return error?.code === "ENOENT";
|
|
3256
|
+
}
|
|
3257
|
+
var creates = (flags) => (flags & constants2.O_CREAT) !== 0;
|
|
3258
|
+
async function openWithoutFollowing(file, flags, mode, follows = OPEN_FOLLOWS_LINKS) {
|
|
3259
|
+
if (!follows) return open(file, flags | NO_FOLLOW, mode);
|
|
3260
|
+
let before = null;
|
|
3261
|
+
try {
|
|
3262
|
+
before = await lstat(file, { bigint: true });
|
|
3263
|
+
} catch (error) {
|
|
3264
|
+
if (!(isMissing(error) && creates(flags))) throw error;
|
|
3265
|
+
}
|
|
3266
|
+
if (before !== null && !before.isFile()) throw refusal2(before);
|
|
3267
|
+
const handle = await open(file, flags, mode);
|
|
3268
|
+
try {
|
|
3269
|
+
const opened = await handle.stat({ bigint: true });
|
|
3270
|
+
const named = before ?? await lstat(file, { bigint: true });
|
|
3271
|
+
if (!sameFile(named, opened)) throw notOrdinary();
|
|
3272
|
+
return handle;
|
|
3273
|
+
} catch (error) {
|
|
3274
|
+
await handle.close();
|
|
3275
|
+
throw error;
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
function openWithoutFollowingNow(file, flags, mode, follows = OPEN_FOLLOWS_LINKS) {
|
|
3279
|
+
if (!follows) return openSync(file, flags | NO_FOLLOW, mode);
|
|
3280
|
+
let before = null;
|
|
3281
|
+
try {
|
|
3282
|
+
before = lstatSync(file, { bigint: true });
|
|
3283
|
+
} catch (error) {
|
|
3284
|
+
if (!(isMissing(error) && creates(flags))) throw error;
|
|
3285
|
+
}
|
|
3286
|
+
if (before !== null && !before.isFile()) throw refusal2(before);
|
|
3287
|
+
const fd = openSync(file, flags, mode);
|
|
3288
|
+
try {
|
|
3289
|
+
const named = before ?? lstatSync(file, { bigint: true });
|
|
3290
|
+
if (!sameFile(named, fstatSync(fd, { bigint: true }))) throw notOrdinary();
|
|
3291
|
+
return fd;
|
|
3292
|
+
} catch (error) {
|
|
3293
|
+
closeSync(fd);
|
|
3294
|
+
throw error;
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3297
|
+
|
|
3298
|
+
// src/collector/adapters/claude-code/registry.ts
|
|
3050
3299
|
var MAX_ENTRY_BYTES = 256 * 1024;
|
|
3051
3300
|
async function readRegularFile(file) {
|
|
3052
|
-
const
|
|
3053
|
-
const handle = await open(file, flags);
|
|
3301
|
+
const handle = await openWithoutFollowing(file, constants3.O_RDONLY);
|
|
3054
3302
|
try {
|
|
3055
3303
|
const info = await handle.stat();
|
|
3056
3304
|
if (!info.isFile()) throw new Error("Not an ordinary file.");
|
|
@@ -3108,7 +3356,7 @@ function parseRegistryEntry(content) {
|
|
|
3108
3356
|
procStart: text2(raw.procStart)
|
|
3109
3357
|
};
|
|
3110
3358
|
}
|
|
3111
|
-
async function readRegistry(sessionsDir, io = nodeIo) {
|
|
3359
|
+
async function readRegistry(sessionsDir, io = nodeIo, previous) {
|
|
3112
3360
|
let names;
|
|
3113
3361
|
try {
|
|
3114
3362
|
names = await io.readdir(sessionsDir);
|
|
@@ -3117,22 +3365,28 @@ async function readRegistry(sessionsDir, io = nodeIo) {
|
|
|
3117
3365
|
return { readable: false, missing: code === "ENOENT" || code === "ENOTDIR" };
|
|
3118
3366
|
}
|
|
3119
3367
|
const files = names.filter(isRegistryFileName).sort();
|
|
3120
|
-
const parsed =
|
|
3368
|
+
const parsed = /* @__PURE__ */ new Map();
|
|
3369
|
+
const found = await Promise.all(
|
|
3121
3370
|
files.map(async (name) => {
|
|
3371
|
+
let content;
|
|
3122
3372
|
try {
|
|
3123
|
-
|
|
3373
|
+
content = await io.readFile(path5.join(sessionsDir, name));
|
|
3124
3374
|
} catch {
|
|
3125
3375
|
return null;
|
|
3126
3376
|
}
|
|
3377
|
+
const entry = parseRegistryEntry(content);
|
|
3378
|
+
if (entry === null) return previous?.get(name) ?? null;
|
|
3379
|
+
parsed.set(name, entry);
|
|
3380
|
+
return entry;
|
|
3127
3381
|
})
|
|
3128
3382
|
);
|
|
3129
3383
|
const entries = /* @__PURE__ */ new Map();
|
|
3130
|
-
|
|
3384
|
+
found.forEach((entry, index) => {
|
|
3131
3385
|
if (!entry) return;
|
|
3132
3386
|
const isNamedForPid = files[index] === `${entry.pid}.json`;
|
|
3133
3387
|
if (isNamedForPid || !entries.has(entry.pid)) entries.set(entry.pid, entry);
|
|
3134
3388
|
});
|
|
3135
|
-
return { readable: true, entries };
|
|
3389
|
+
return { readable: true, entries, parsed };
|
|
3136
3390
|
}
|
|
3137
3391
|
|
|
3138
3392
|
// src/collector/adapters/claude-code/stopOffers.ts
|
|
@@ -3153,7 +3407,7 @@ function build(fields, origin, context) {
|
|
|
3153
3407
|
const project = projectOf(fields.cwd);
|
|
3154
3408
|
const surface = mapClaudeCodeSurface(fields.entrypoint);
|
|
3155
3409
|
const mapped = mapClaudeCodeStatus(fields, origin);
|
|
3156
|
-
const
|
|
3410
|
+
const open2 = claudeCodeOpenLink(surface, fields.sessionId);
|
|
3157
3411
|
const startedAt = plausibleTime(fields.startedAt, context.now);
|
|
3158
3412
|
let statusSince = plausibleTime(fields.statusUpdatedAt, context.now);
|
|
3159
3413
|
if (statusSince !== null && startedAt !== null && statusSince < startedAt - WRITE_ORDER_SLACK_MS) {
|
|
@@ -3170,7 +3424,7 @@ function build(fields, origin, context) {
|
|
|
3170
3424
|
...mapped,
|
|
3171
3425
|
startedAt,
|
|
3172
3426
|
statusSince,
|
|
3173
|
-
links:
|
|
3427
|
+
links: open2 ? { open: open2 } : {},
|
|
3174
3428
|
stale: isStale({ status: mapped.status, statusSince }, context.now)
|
|
3175
3429
|
};
|
|
3176
3430
|
if (fields.pid !== void 0) {
|
|
@@ -3273,8 +3527,8 @@ function withStopOffers(sessions, live2, answer, options) {
|
|
|
3273
3527
|
import path8 from "node:path";
|
|
3274
3528
|
|
|
3275
3529
|
// src/collector/files/readOnlyIo.ts
|
|
3276
|
-
import { constants as
|
|
3277
|
-
import { lstat
|
|
3530
|
+
import { constants as constants4 } from "node:fs";
|
|
3531
|
+
import { lstat as lstat2, readdir as readdir2, stat as stat3 } from "node:fs/promises";
|
|
3278
3532
|
function infoOf(stats) {
|
|
3279
3533
|
return {
|
|
3280
3534
|
kind: stats.isFile() ? "file" : stats.isDirectory() ? "directory" : "other",
|
|
@@ -3284,8 +3538,7 @@ function infoOf(stats) {
|
|
|
3284
3538
|
};
|
|
3285
3539
|
}
|
|
3286
3540
|
async function openRegularFile(file) {
|
|
3287
|
-
const
|
|
3288
|
-
const handle = await open2(file, flags);
|
|
3541
|
+
const handle = await openWithoutFollowing(file, constants4.O_RDONLY);
|
|
3289
3542
|
let info;
|
|
3290
3543
|
try {
|
|
3291
3544
|
info = infoOf(await handle.stat());
|
|
@@ -3317,10 +3570,10 @@ async function openRegularFile(file) {
|
|
|
3317
3570
|
var nodeIo2 = {
|
|
3318
3571
|
readdir: (dir) => readdir2(dir),
|
|
3319
3572
|
stat: async (target) => infoOf(await stat3(target)),
|
|
3320
|
-
lstat: async (target) => infoOf(await
|
|
3573
|
+
lstat: async (target) => infoOf(await lstat2(target)),
|
|
3321
3574
|
openRegular: openRegularFile
|
|
3322
3575
|
};
|
|
3323
|
-
function
|
|
3576
|
+
function isMissing2(error) {
|
|
3324
3577
|
const code = error?.code;
|
|
3325
3578
|
return code === "ENOENT" || code === "ENOTDIR";
|
|
3326
3579
|
}
|
|
@@ -3493,12 +3746,12 @@ function sameStamp(a, b) {
|
|
|
3493
3746
|
return a.size === b.size && a.mtimeMs === b.mtimeMs && a.ino === b.ino;
|
|
3494
3747
|
}
|
|
3495
3748
|
async function readTranscriptTail(file, io, known, maxBytes = TRANSCRIPT_TAIL_BYTES) {
|
|
3496
|
-
const
|
|
3749
|
+
const open2 = await io.openRegular(file);
|
|
3497
3750
|
try {
|
|
3498
|
-
const stamp = stampOf(
|
|
3751
|
+
const stamp = stampOf(open2.info);
|
|
3499
3752
|
if (known !== null && sameStamp(known, stamp)) return { unchanged: true, stamp };
|
|
3500
3753
|
const start = Math.max(0, stamp.size - maxBytes);
|
|
3501
|
-
const bytes2 = await
|
|
3754
|
+
const bytes2 = await open2.read(start, stamp.size - start);
|
|
3502
3755
|
return {
|
|
3503
3756
|
unchanged: false,
|
|
3504
3757
|
stamp,
|
|
@@ -3506,7 +3759,7 @@ async function readTranscriptTail(file, io, known, maxBytes = TRANSCRIPT_TAIL_BY
|
|
|
3506
3759
|
fromStart: start === 0
|
|
3507
3760
|
};
|
|
3508
3761
|
} finally {
|
|
3509
|
-
await
|
|
3762
|
+
await open2.close();
|
|
3510
3763
|
}
|
|
3511
3764
|
}
|
|
3512
3765
|
|
|
@@ -3544,7 +3797,7 @@ function createWaitingTextReader(options) {
|
|
|
3544
3797
|
}
|
|
3545
3798
|
return wait.text;
|
|
3546
3799
|
} catch (error) {
|
|
3547
|
-
if (
|
|
3800
|
+
if (isMissing2(error)) wait.file = null;
|
|
3548
3801
|
wait.lookedAt = at;
|
|
3549
3802
|
wait.stamp = null;
|
|
3550
3803
|
wait.text = void 0;
|
|
@@ -3613,7 +3866,22 @@ var CLAUDE_CODE_CAPABILITIES = {
|
|
|
3613
3866
|
},
|
|
3614
3867
|
answer: {
|
|
3615
3868
|
level: "partly",
|
|
3616
|
-
reason: "With the Agent Lookout plugin
|
|
3869
|
+
reason: "With the Agent Lookout plugin, by Allow, Deny or a permission rule. Allow only if all is shown: never edits, plans or questions."
|
|
3870
|
+
}
|
|
3871
|
+
};
|
|
3872
|
+
var CLAUDE_CODE_WINDOWS_CAPABILITIES = {
|
|
3873
|
+
...CLAUDE_CODE_CAPABILITIES,
|
|
3874
|
+
jump: {
|
|
3875
|
+
level: "partly",
|
|
3876
|
+
reason: "Only in VS Code. On Windows there is no tmux pane or Terminal tab to reach, and the desktop app has no link."
|
|
3877
|
+
},
|
|
3878
|
+
stop: {
|
|
3879
|
+
level: "no",
|
|
3880
|
+
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."
|
|
3881
|
+
},
|
|
3882
|
+
answer: {
|
|
3883
|
+
level: "no",
|
|
3884
|
+
reason: "Not on Windows: the plugin's hook is a POSIX sh script, and it reaches Agent Lookout through a Unix socket."
|
|
3617
3885
|
}
|
|
3618
3886
|
};
|
|
3619
3887
|
var STOP_TURNED_OFF = {
|
|
@@ -3649,21 +3917,25 @@ function createClaudeCodeAdapter(options = {}) {
|
|
|
3649
3917
|
const transcriptsOff = waitingTextOff(env);
|
|
3650
3918
|
const feedWithheld = homeOverride !== void 0 && !binaryNamed;
|
|
3651
3919
|
const neverRun = feedOff || feedWithheld;
|
|
3652
|
-
const
|
|
3920
|
+
const platform = options.platform ?? process.platform;
|
|
3921
|
+
const onWindows = platform === "win32";
|
|
3922
|
+
const paths = pathsOf(platform);
|
|
3923
|
+
const stops = stopOff(env) || onWindows ? void 0 : options.stops;
|
|
3653
3924
|
const answersOff = env.AGENT_LOOKOUT_ANSWER?.trim().toLowerCase() === "off";
|
|
3654
|
-
const capabilities = stopOff(env) || answersOff ? {
|
|
3925
|
+
const capabilities = onWindows ? CLAUDE_CODE_WINDOWS_CAPABILITIES : stopOff(env) || answersOff ? {
|
|
3655
3926
|
...CLAUDE_CODE_CAPABILITIES,
|
|
3656
3927
|
...stopOff(env) && { stop: STOP_TURNED_OFF },
|
|
3657
3928
|
...answersOff && { answer: ANSWER_TURNED_OFF }
|
|
3658
3929
|
} : CLAUDE_CODE_CAPABILITIES;
|
|
3659
|
-
const claudeHome =
|
|
3660
|
-
const sessionsDir =
|
|
3661
|
-
const homeName = tildify(claudeHome, homeDir);
|
|
3662
|
-
const registryName = tildify(sessionsDir, homeDir);
|
|
3930
|
+
const claudeHome = paths.resolve(homeOverride ?? paths.join(homeDir, ".claude"));
|
|
3931
|
+
const sessionsDir = paths.join(claudeHome, "sessions");
|
|
3932
|
+
const homeName = tildify(claudeHome, homeDir, platform);
|
|
3933
|
+
const registryName = tildify(sessionsDir, homeDir, platform);
|
|
3663
3934
|
const waitingTexts = transcriptsOff ? null : createWaitingTextReader({ claudeHome, io: options.transcriptIo, now });
|
|
3664
3935
|
let lastDue = null;
|
|
3665
3936
|
let lastRead = null;
|
|
3666
3937
|
let feedSoon = false;
|
|
3938
|
+
let lastParsed;
|
|
3667
3939
|
stops?.onAskFeedSoon(() => {
|
|
3668
3940
|
feedSoon = true;
|
|
3669
3941
|
});
|
|
@@ -3686,7 +3958,8 @@ function createClaudeCodeAdapter(options = {}) {
|
|
|
3686
3958
|
];
|
|
3687
3959
|
}
|
|
3688
3960
|
async function readRegistryNow(at) {
|
|
3689
|
-
const registry = await readRegistry(sessionsDir, options.registryIo);
|
|
3961
|
+
const registry = await readRegistry(sessionsDir, options.registryIo, lastParsed);
|
|
3962
|
+
lastParsed = registry.readable ? registry.parsed : void 0;
|
|
3690
3963
|
if (!registry.readable) return { registry, live: [], unknownStatus: false };
|
|
3691
3964
|
const candidates = [...registry.entries.values()].filter(
|
|
3692
3965
|
(entry) => entry.sessionId !== void 0 && isClaudeCodeSessionKind(entry.kind) && isAlive(entry.pid)
|
|
@@ -3720,7 +3993,12 @@ function createClaudeCodeAdapter(options = {}) {
|
|
|
3720
3993
|
return at - due < intervalMs ? due : at;
|
|
3721
3994
|
}
|
|
3722
3995
|
async function runCommand(askedAt) {
|
|
3723
|
-
const binary = await findClaudeBinary({
|
|
3996
|
+
const binary = await findClaudeBinary({
|
|
3997
|
+
env,
|
|
3998
|
+
homeDir,
|
|
3999
|
+
isExecutable: options.isExecutable,
|
|
4000
|
+
platform
|
|
4001
|
+
});
|
|
3724
4002
|
if (!binary.found) {
|
|
3725
4003
|
return {
|
|
3726
4004
|
askedAt,
|
|
@@ -4119,8 +4397,10 @@ function stopFailed(reason) {
|
|
|
4119
4397
|
function stopRefusalFor(req) {
|
|
4120
4398
|
return actionRefusalFor(req, STOP_ACTION, MAX_STOP_BODY_BYTES);
|
|
4121
4399
|
}
|
|
4122
|
-
function
|
|
4123
|
-
return snapshot.sessions.some(
|
|
4400
|
+
function notSeenToStop(snapshot, stopped) {
|
|
4401
|
+
return snapshot.sessions.some(
|
|
4402
|
+
(session) => session.id === stopped.id && (session.stop !== void 0 || stopped.stop?.how === "background" && session.status !== "finished" && session.status !== "failed")
|
|
4403
|
+
);
|
|
4124
4404
|
}
|
|
4125
4405
|
async function lookAgain(options, stopped) {
|
|
4126
4406
|
if (stopped.length === 0) return;
|
|
@@ -4129,7 +4409,7 @@ async function lookAgain(options, stopped) {
|
|
|
4129
4409
|
}
|
|
4130
4410
|
try {
|
|
4131
4411
|
const first = await options.poller.pollOnce();
|
|
4132
|
-
if (stopped.some((session) =>
|
|
4412
|
+
if (stopped.some((session) => notSeenToStop(first, session))) {
|
|
4133
4413
|
await options.poller.pollOnce();
|
|
4134
4414
|
}
|
|
4135
4415
|
} catch {
|
|
@@ -4148,7 +4428,9 @@ function createStopRoute(options) {
|
|
|
4148
4428
|
if (acted === "signalled") {
|
|
4149
4429
|
const running = await stopper.waitForEnd([target.pid]);
|
|
4150
4430
|
if (running.size > 0) return stopFailed("still-running");
|
|
4151
|
-
} else if (acted
|
|
4431
|
+
} else if (acted === "stopped") {
|
|
4432
|
+
await stopper.waitForEnd([target.pid]);
|
|
4433
|
+
} else {
|
|
4152
4434
|
return stopFailed(acted);
|
|
4153
4435
|
}
|
|
4154
4436
|
events.add([stoppedEvent(session, stopper.now())]);
|
|
@@ -4266,9 +4548,7 @@ function createCleanUpRoute(options) {
|
|
|
4266
4548
|
outcomes[index] = acted;
|
|
4267
4549
|
}
|
|
4268
4550
|
}
|
|
4269
|
-
const running = await stopper.waitForEnd(
|
|
4270
|
-
sent.filter((one) => one.signalled).map((one) => one.target.pid)
|
|
4271
|
-
);
|
|
4551
|
+
const running = await stopper.waitForEnd(sent.map((one) => one.target.pid));
|
|
4272
4552
|
const ended = [];
|
|
4273
4553
|
for (const one of sent) {
|
|
4274
4554
|
if (one.signalled && running.has(one.target.pid)) outcomes[one.index] = "still-running";
|
|
@@ -4367,6 +4647,15 @@ function answeredEvent(session, decision, at) {
|
|
|
4367
4647
|
decision
|
|
4368
4648
|
};
|
|
4369
4649
|
}
|
|
4650
|
+
async function pollAfterAnswer(poller, sessionId) {
|
|
4651
|
+
try {
|
|
4652
|
+
const first = await poller.pollOnce();
|
|
4653
|
+
if (first.sessions.some((session) => session.id === sessionId && needsYou(session))) {
|
|
4654
|
+
await poller.pollOnce();
|
|
4655
|
+
}
|
|
4656
|
+
} catch {
|
|
4657
|
+
}
|
|
4658
|
+
}
|
|
4370
4659
|
function answerRefusalFor(req) {
|
|
4371
4660
|
return actionRefusalFor(req, ANSWER_ACTION, MAX_ANSWER_BODY_BYTES);
|
|
4372
4661
|
}
|
|
@@ -4396,8 +4685,7 @@ function createAnswerRoute(options) {
|
|
|
4396
4685
|
const outcome = await asks.answer(asked.sessionId, asked.requestId, asked.decision);
|
|
4397
4686
|
if (outcome !== "answered") return answerFailed(outcome);
|
|
4398
4687
|
events.add([answeredEvent(session, asked.decision, now())]);
|
|
4399
|
-
|
|
4400
|
-
});
|
|
4688
|
+
await pollAfterAnswer(poller, asked.sessionId);
|
|
4401
4689
|
return {
|
|
4402
4690
|
status: 200,
|
|
4403
4691
|
body: { ok: true, decision: asked.decision }
|
|
@@ -4407,7 +4695,6 @@ function createAnswerRoute(options) {
|
|
|
4407
4695
|
|
|
4408
4696
|
// src/collector/answers/answerSettings.ts
|
|
4409
4697
|
import os3 from "node:os";
|
|
4410
|
-
import path10 from "node:path";
|
|
4411
4698
|
var ANSWER_ENV = "AGENT_LOOKOUT_ANSWER";
|
|
4412
4699
|
var ANSWER_SOCKET_ENV = "AGENT_LOOKOUT_ANSWER_SOCKET";
|
|
4413
4700
|
var ANSWER_WAIT_ENV = "AGENT_LOOKOUT_ANSWER_WAIT";
|
|
@@ -4415,6 +4702,7 @@ var DEFAULT_HOLD_MS = 3e5;
|
|
|
4415
4702
|
var MIN_HOLD_MS = 5e3;
|
|
4416
4703
|
var MAX_HOLD_MS = 54e4;
|
|
4417
4704
|
var MAX_SOCKET_PATH_BYTES = 103;
|
|
4705
|
+
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
4706
|
function answerOff(env) {
|
|
4419
4707
|
return env[ANSWER_ENV]?.trim().toLowerCase() === "off";
|
|
4420
4708
|
}
|
|
@@ -4435,7 +4723,8 @@ function readAnswerSetup(env, homeDir = os3.homedir(), platform = process.platfo
|
|
|
4435
4723
|
on: false,
|
|
4436
4724
|
socketPath: null,
|
|
4437
4725
|
holdMs: holdMs ?? DEFAULT_HOLD_MS,
|
|
4438
|
-
problem:
|
|
4726
|
+
problem: WINDOWS_PROBLEM,
|
|
4727
|
+
quiet: true
|
|
4439
4728
|
};
|
|
4440
4729
|
}
|
|
4441
4730
|
if (holdMs === null) {
|
|
@@ -4456,8 +4745,9 @@ function readAnswerSetup(env, homeDir = os3.homedir(), platform = process.platfo
|
|
|
4456
4745
|
quiet: true
|
|
4457
4746
|
};
|
|
4458
4747
|
}
|
|
4459
|
-
const
|
|
4460
|
-
const
|
|
4748
|
+
const paths = pathsOf(platform);
|
|
4749
|
+
const ownDir = paths.resolve(homeDir, ".agent-lookout");
|
|
4750
|
+
const socketPath = paths.resolve(named || paths.join(ownDir, "answer.sock"));
|
|
4461
4751
|
if (Buffer.byteLength(socketPath) > MAX_SOCKET_PATH_BYTES) {
|
|
4462
4752
|
return {
|
|
4463
4753
|
on: false,
|
|
@@ -4471,7 +4761,7 @@ function readAnswerSetup(env, homeDir = os3.homedir(), platform = process.platfo
|
|
|
4471
4761
|
socketPath,
|
|
4472
4762
|
holdMs,
|
|
4473
4763
|
problem: null,
|
|
4474
|
-
ownFolder:
|
|
4764
|
+
ownFolder: paths.dirname(socketPath) === ownDir
|
|
4475
4765
|
};
|
|
4476
4766
|
}
|
|
4477
4767
|
|
|
@@ -4485,8 +4775,6 @@ var MAX_BLANK_RUN = 2;
|
|
|
4485
4775
|
var MAX_INPUTS = 30;
|
|
4486
4776
|
var MAX_TOOL_NAME_CHARS = 200;
|
|
4487
4777
|
var MAX_DESCRIPTION_CHARS = 200;
|
|
4488
|
-
var EDIT_TOOLS = ["Edit", "Write", "MultiEdit", "NotebookEdit"];
|
|
4489
|
-
var NOT_YES_OR_NO_TOOLS = ["ExitPlanMode", "AskUserQuestion"];
|
|
4490
4778
|
var EDIT_INPUTS = ["file_path", "notebook_path"];
|
|
4491
4779
|
var HIDDEN = /[\p{Cc}\p{Cf}\p{Co}\p{Cn}\p{Cs}\p{Default_Ignorable_Code_Point}\u2028\u2029]/gu;
|
|
4492
4780
|
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 +4930,7 @@ function createHeldAsks(options) {
|
|
|
4642
4930
|
const newId = options.newId ?? (() => randomBytes(16).toString("hex"));
|
|
4643
4931
|
const startedAt = now();
|
|
4644
4932
|
const held = /* @__PURE__ */ new Map();
|
|
4933
|
+
let answers2 = /* @__PURE__ */ new Map();
|
|
4645
4934
|
const lastRequest = /* @__PURE__ */ new Map();
|
|
4646
4935
|
let seenAt = null;
|
|
4647
4936
|
let missedAt = null;
|
|
@@ -4673,6 +4962,8 @@ function createHeldAsks(options) {
|
|
|
4673
4962
|
if (!entry.confirmed) {
|
|
4674
4963
|
entry.confirmed = true;
|
|
4675
4964
|
entry.wait = reading.wait;
|
|
4965
|
+
entry.since = reading.since ?? null;
|
|
4966
|
+
answerByRule(entry);
|
|
4676
4967
|
return;
|
|
4677
4968
|
}
|
|
4678
4969
|
if (reading.wait !== entry.wait) drop(entry, "left-waiting");
|
|
@@ -4692,6 +4983,55 @@ function createHeldAsks(options) {
|
|
|
4692
4983
|
if (inFlight === run) inFlight = null;
|
|
4693
4984
|
}
|
|
4694
4985
|
}
|
|
4986
|
+
async function answer(sessionId, requestId, decision) {
|
|
4987
|
+
const entry = held.get(sessionId);
|
|
4988
|
+
if (!entry || entry.requestId !== requestId || !entry.confirmed) return "no-ask";
|
|
4989
|
+
if (decision === "allow" && !entry.shown.allow) return "not-allowable";
|
|
4990
|
+
if (entry.answering) return "too-soon";
|
|
4991
|
+
entry.answering = true;
|
|
4992
|
+
const reading = await read(sessionId);
|
|
4993
|
+
if (held.get(sessionId) !== entry || !entry.reply.isOpen()) {
|
|
4994
|
+
if (held.get(sessionId) === entry) drop(entry, "closed");
|
|
4995
|
+
return "gone";
|
|
4996
|
+
}
|
|
4997
|
+
const sameWait = reading.status === "waiting" && reading.wait === entry.wait;
|
|
4998
|
+
if (!sameWait || now() >= entry.until) {
|
|
4999
|
+
drop(entry, sameWait ? "limit" : "left-waiting");
|
|
5000
|
+
return "gone";
|
|
5001
|
+
}
|
|
5002
|
+
held.delete(sessionId);
|
|
5003
|
+
entry.reply.send(decisionOutput(decision));
|
|
5004
|
+
answers2.set(sessionId, { source: SOURCE, since: entry.since, at: now() });
|
|
5005
|
+
if (held.size === 0 && stopTimer !== null) {
|
|
5006
|
+
stopTimer();
|
|
5007
|
+
stopTimer = null;
|
|
5008
|
+
}
|
|
5009
|
+
return "answered";
|
|
5010
|
+
}
|
|
5011
|
+
function answerByRule(entry) {
|
|
5012
|
+
const rules = options.rules;
|
|
5013
|
+
if (rules === void 0) return;
|
|
5014
|
+
const request2 = { sessionId: entry.sessionId, shown: entry.shown };
|
|
5015
|
+
const verdict = verdictOf(entry);
|
|
5016
|
+
if (verdict === null) return;
|
|
5017
|
+
entry.byRule = true;
|
|
5018
|
+
void answer(entry.sessionId, entry.requestId, verdict.decision).then((outcome) => {
|
|
5019
|
+
if (outcome === "answered") rules.answered(request2, verdict);
|
|
5020
|
+
});
|
|
5021
|
+
}
|
|
5022
|
+
function verdictOf(entry) {
|
|
5023
|
+
if (options.rules === void 0) return null;
|
|
5024
|
+
try {
|
|
5025
|
+
return options.rules.verdictFor({ sessionId: entry.sessionId, shown: entry.shown });
|
|
5026
|
+
} catch {
|
|
5027
|
+
return null;
|
|
5028
|
+
}
|
|
5029
|
+
}
|
|
5030
|
+
function answeringByRule(entry) {
|
|
5031
|
+
if (!entry.reply.isOpen()) return false;
|
|
5032
|
+
if (entry.confirmed) return entry.answering && entry.byRule;
|
|
5033
|
+
return verdictOf(entry) !== null;
|
|
5034
|
+
}
|
|
4695
5035
|
function status() {
|
|
4696
5036
|
const plugin = seenAt !== null && (missedAt === null || seenAt > missedAt) ? "seen" : missedAt !== null ? "missed" : "unknown";
|
|
4697
5037
|
return { state: "on", plugin, holdMs };
|
|
@@ -4704,6 +5044,7 @@ function createHeldAsks(options) {
|
|
|
4704
5044
|
const at = now();
|
|
4705
5045
|
const older = held.get(request2.sessionId);
|
|
4706
5046
|
if (older) drop(older, "newer");
|
|
5047
|
+
answers2.delete(request2.sessionId);
|
|
4707
5048
|
const entry = {
|
|
4708
5049
|
requestId: newId(),
|
|
4709
5050
|
sessionId: request2.sessionId,
|
|
@@ -4713,7 +5054,9 @@ function createHeldAsks(options) {
|
|
|
4713
5054
|
reply,
|
|
4714
5055
|
confirmed: false,
|
|
4715
5056
|
wait: null,
|
|
4716
|
-
|
|
5057
|
+
since: null,
|
|
5058
|
+
answering: false,
|
|
5059
|
+
byRule: false
|
|
4717
5060
|
};
|
|
4718
5061
|
if (held.size >= MAX_HELD) {
|
|
4719
5062
|
drop(entry, "full");
|
|
@@ -4729,30 +5072,7 @@ function createHeldAsks(options) {
|
|
|
4729
5072
|
void check();
|
|
4730
5073
|
},
|
|
4731
5074
|
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
|
-
},
|
|
5075
|
+
answer,
|
|
4756
5076
|
withAsks(snapshot) {
|
|
4757
5077
|
const answering = status();
|
|
4758
5078
|
if (held.size === 0) return { ...snapshot, answering };
|
|
@@ -4765,6 +5085,17 @@ function createHeldAsks(options) {
|
|
|
4765
5085
|
});
|
|
4766
5086
|
return { ...snapshot, sessions, answering };
|
|
4767
5087
|
},
|
|
5088
|
+
answeredIn(snapshot) {
|
|
5089
|
+
const { ids, kept } = answeredNow(answers2, snapshot, now());
|
|
5090
|
+
answers2 = kept;
|
|
5091
|
+
for (const entry of held.values()) {
|
|
5092
|
+
if (!answeringByRule(entry)) continue;
|
|
5093
|
+
const session = snapshot.sessions.find((listed) => listed.id === entry.sessionId);
|
|
5094
|
+
const since = entry.confirmed ? entry.since : null;
|
|
5095
|
+
if (session && inAnsweredWait(session, { since })) ids.add(entry.sessionId);
|
|
5096
|
+
}
|
|
5097
|
+
return ids;
|
|
5098
|
+
},
|
|
4768
5099
|
noteRequest(sessionId) {
|
|
4769
5100
|
const at = now();
|
|
4770
5101
|
seenAt = at;
|
|
@@ -4801,10 +5132,10 @@ function createHeldAsks(options) {
|
|
|
4801
5132
|
}
|
|
4802
5133
|
|
|
4803
5134
|
// src/collector/answers/hookSocket.ts
|
|
4804
|
-
import { chmod, lstat as
|
|
5135
|
+
import { chmod, lstat as lstat3, mkdir, stat as stat4, unlink } from "node:fs/promises";
|
|
4805
5136
|
import http from "node:http";
|
|
4806
5137
|
import net from "node:net";
|
|
4807
|
-
import
|
|
5138
|
+
import path9 from "node:path";
|
|
4808
5139
|
var HOOK_PATH = "/hooks/permission-request";
|
|
4809
5140
|
var HOOK_HEADER = "x-agent-lookout-hook";
|
|
4810
5141
|
var HOOK_HEADER_VALUE = "permission-request";
|
|
@@ -4877,7 +5208,7 @@ function createHookSocket(options) {
|
|
|
4877
5208
|
const maxBody = options.maxBodyBytes ?? MAX_HOOK_BODY_BYTES;
|
|
4878
5209
|
const bodyTimeoutMs = options.bodyTimeoutMs ?? HOOK_BODY_TIMEOUT_MS;
|
|
4879
5210
|
const uid = options.uid ?? process.getuid?.() ?? -1;
|
|
4880
|
-
const folder =
|
|
5211
|
+
const folder = path9.dirname(socketPath);
|
|
4881
5212
|
let server = null;
|
|
4882
5213
|
let made = null;
|
|
4883
5214
|
async function handle(req, res) {
|
|
@@ -4911,7 +5242,7 @@ function createHookSocket(options) {
|
|
|
4911
5242
|
async function readyFolder() {
|
|
4912
5243
|
try {
|
|
4913
5244
|
const madeHere = await mkdir(folder, { recursive: true, mode: 448 });
|
|
4914
|
-
const info = await
|
|
5245
|
+
const info = await lstat3(folder);
|
|
4915
5246
|
if (!info.isDirectory()) return `${folder} is not a folder, so answering is off.`;
|
|
4916
5247
|
if (uid >= 0 && info.uid !== uid) {
|
|
4917
5248
|
return `${folder} belongs to another user, so answering is off.`;
|
|
@@ -4930,7 +5261,7 @@ function createHookSocket(options) {
|
|
|
4930
5261
|
async function freePath() {
|
|
4931
5262
|
let info;
|
|
4932
5263
|
try {
|
|
4933
|
-
info = await
|
|
5264
|
+
info = await lstat3(socketPath);
|
|
4934
5265
|
} catch (error) {
|
|
4935
5266
|
return errorCode2(error) === "ENOENT" ? null : `${socketPath} could not be read.`;
|
|
4936
5267
|
}
|
|
@@ -5000,12 +5331,12 @@ function createHookSocket(options) {
|
|
|
5000
5331
|
|
|
5001
5332
|
// src/collector/answers/registryStatus.ts
|
|
5002
5333
|
import os4 from "node:os";
|
|
5003
|
-
import
|
|
5334
|
+
import path10 from "node:path";
|
|
5004
5335
|
function createRegistryStatus(options) {
|
|
5005
5336
|
const homeDir = options.homeDir ?? os4.homedir();
|
|
5006
5337
|
const readFile2 = options.readFile ?? readRegularFile;
|
|
5007
|
-
const home = options.env[CLAUDE_HOME_ENV]?.trim() ||
|
|
5008
|
-
const sessionsDir =
|
|
5338
|
+
const home = options.env[CLAUDE_HOME_ENV]?.trim() || path10.join(homeDir, ".claude");
|
|
5339
|
+
const sessionsDir = path10.join(path10.resolve(home), "sessions");
|
|
5009
5340
|
return {
|
|
5010
5341
|
async statusOf(sessionId) {
|
|
5011
5342
|
const unknown = { status: "unknown" };
|
|
@@ -5016,7 +5347,7 @@ function createRegistryStatus(options) {
|
|
|
5016
5347
|
const ownId = sessionId.slice("claude-code:".length);
|
|
5017
5348
|
let content;
|
|
5018
5349
|
try {
|
|
5019
|
-
content = await readFile2(
|
|
5350
|
+
content = await readFile2(path10.join(sessionsDir, `${pid}.json`));
|
|
5020
5351
|
} catch {
|
|
5021
5352
|
return unknown;
|
|
5022
5353
|
}
|
|
@@ -5026,12 +5357,618 @@ function createRegistryStatus(options) {
|
|
|
5026
5357
|
if (!forPermission) return { status: "not-waiting" };
|
|
5027
5358
|
return {
|
|
5028
5359
|
status: "waiting",
|
|
5029
|
-
wait: `${entry.statusUpdatedAt ?? ""}|${entry.waitingFor ?? ""}
|
|
5360
|
+
wait: `${entry.statusUpdatedAt ?? ""}|${entry.waitingFor ?? ""}`,
|
|
5361
|
+
...entry.statusUpdatedAt !== void 0 && { since: entry.statusUpdatedAt }
|
|
5030
5362
|
};
|
|
5031
5363
|
}
|
|
5032
5364
|
};
|
|
5033
5365
|
}
|
|
5034
5366
|
|
|
5367
|
+
// src/core/permission-rules/commandWords.ts
|
|
5368
|
+
var COMMAND_RUNNERS = [
|
|
5369
|
+
// The shell's own words.
|
|
5370
|
+
".",
|
|
5371
|
+
"builtin",
|
|
5372
|
+
"command",
|
|
5373
|
+
"coproc",
|
|
5374
|
+
"eval",
|
|
5375
|
+
"exec",
|
|
5376
|
+
"nocorrect",
|
|
5377
|
+
"noglob",
|
|
5378
|
+
"source",
|
|
5379
|
+
"time",
|
|
5380
|
+
// Programs that run the command after their own words.
|
|
5381
|
+
"arch",
|
|
5382
|
+
"bunx",
|
|
5383
|
+
"caffeinate",
|
|
5384
|
+
"chroot",
|
|
5385
|
+
"doas",
|
|
5386
|
+
"env",
|
|
5387
|
+
"flock",
|
|
5388
|
+
"gtimeout",
|
|
5389
|
+
"ionice",
|
|
5390
|
+
"nice",
|
|
5391
|
+
"nix-shell",
|
|
5392
|
+
"nohup",
|
|
5393
|
+
"npx",
|
|
5394
|
+
"parallel",
|
|
5395
|
+
"pnpx",
|
|
5396
|
+
"sandbox-exec",
|
|
5397
|
+
"script",
|
|
5398
|
+
"setsid",
|
|
5399
|
+
"stdbuf",
|
|
5400
|
+
"su",
|
|
5401
|
+
"sudo",
|
|
5402
|
+
"taskset",
|
|
5403
|
+
"timeout",
|
|
5404
|
+
"unbuffer",
|
|
5405
|
+
"uvx",
|
|
5406
|
+
"watch",
|
|
5407
|
+
"xargs",
|
|
5408
|
+
"xcrun",
|
|
5409
|
+
// Shells, which run the command they are given or a file.
|
|
5410
|
+
"ash",
|
|
5411
|
+
"bash",
|
|
5412
|
+
"busybox",
|
|
5413
|
+
"cmd",
|
|
5414
|
+
"csh",
|
|
5415
|
+
"dash",
|
|
5416
|
+
"fish",
|
|
5417
|
+
"ksh",
|
|
5418
|
+
"powershell",
|
|
5419
|
+
"pwsh",
|
|
5420
|
+
"sh",
|
|
5421
|
+
"tcsh",
|
|
5422
|
+
"zsh"
|
|
5423
|
+
];
|
|
5424
|
+
var NETWORK_CLIENTS_AND_INTERPRETERS = [
|
|
5425
|
+
// Programs that send requests, or open a connection, to an address.
|
|
5426
|
+
"aria2c",
|
|
5427
|
+
"curl",
|
|
5428
|
+
"elinks",
|
|
5429
|
+
"ftp",
|
|
5430
|
+
"grpcurl",
|
|
5431
|
+
"http",
|
|
5432
|
+
"https",
|
|
5433
|
+
"httpx",
|
|
5434
|
+
"links",
|
|
5435
|
+
"lynx",
|
|
5436
|
+
"nc",
|
|
5437
|
+
"ncat",
|
|
5438
|
+
"netcat",
|
|
5439
|
+
"scp",
|
|
5440
|
+
"sftp",
|
|
5441
|
+
"socat",
|
|
5442
|
+
"ssh",
|
|
5443
|
+
"telnet",
|
|
5444
|
+
"w3m",
|
|
5445
|
+
"websocat",
|
|
5446
|
+
"wget",
|
|
5447
|
+
"xh",
|
|
5448
|
+
"xhs",
|
|
5449
|
+
// Interpreters, which run code given in their words or in a file.
|
|
5450
|
+
"awk",
|
|
5451
|
+
"bpython",
|
|
5452
|
+
"bun",
|
|
5453
|
+
"clj",
|
|
5454
|
+
"clojure",
|
|
5455
|
+
"dart",
|
|
5456
|
+
"deno",
|
|
5457
|
+
"elixir",
|
|
5458
|
+
"erl",
|
|
5459
|
+
"escript",
|
|
5460
|
+
"esno",
|
|
5461
|
+
"expect",
|
|
5462
|
+
"gawk",
|
|
5463
|
+
"ghci",
|
|
5464
|
+
"groovy",
|
|
5465
|
+
"guile",
|
|
5466
|
+
"iex",
|
|
5467
|
+
"ipython",
|
|
5468
|
+
"irb",
|
|
5469
|
+
"java",
|
|
5470
|
+
"jshell",
|
|
5471
|
+
"jrunscript",
|
|
5472
|
+
"julia",
|
|
5473
|
+
"kotlin",
|
|
5474
|
+
"lua",
|
|
5475
|
+
"luajit",
|
|
5476
|
+
"mawk",
|
|
5477
|
+
"nawk",
|
|
5478
|
+
"node",
|
|
5479
|
+
"nodejs",
|
|
5480
|
+
"ocaml",
|
|
5481
|
+
"osascript",
|
|
5482
|
+
"perl",
|
|
5483
|
+
"php",
|
|
5484
|
+
"pypy",
|
|
5485
|
+
"python",
|
|
5486
|
+
"r",
|
|
5487
|
+
"racket",
|
|
5488
|
+
"raku",
|
|
5489
|
+
"rakudo",
|
|
5490
|
+
"rscript",
|
|
5491
|
+
"ruby",
|
|
5492
|
+
"runghc",
|
|
5493
|
+
"runhaskell",
|
|
5494
|
+
"sbcl",
|
|
5495
|
+
"scala",
|
|
5496
|
+
"swift",
|
|
5497
|
+
"tclsh",
|
|
5498
|
+
"ts-node",
|
|
5499
|
+
"tsx",
|
|
5500
|
+
"vite-node",
|
|
5501
|
+
"wish",
|
|
5502
|
+
"zx"
|
|
5503
|
+
];
|
|
5504
|
+
var WINDOWS_PROGRAM_END = /\.(?:exe|com)$/;
|
|
5505
|
+
var LETTER = /[a-z]/;
|
|
5506
|
+
function programOf(word2) {
|
|
5507
|
+
return word2.slice(word2.lastIndexOf("/") + 1).toLowerCase().replace(WINDOWS_PROGRAM_END, "");
|
|
5508
|
+
}
|
|
5509
|
+
function nameIn(program, names) {
|
|
5510
|
+
let found;
|
|
5511
|
+
for (const name of names) {
|
|
5512
|
+
if (program === name) return name;
|
|
5513
|
+
const fits = program.startsWith(name) && !LETTER.test(program.charAt(name.length));
|
|
5514
|
+
if (fits && name.length > (found?.length ?? 0)) found = name;
|
|
5515
|
+
}
|
|
5516
|
+
return found;
|
|
5517
|
+
}
|
|
5518
|
+
function runsAnotherCommand(word2) {
|
|
5519
|
+
return nameIn(programOf(word2), COMMAND_RUNNERS) !== void 0;
|
|
5520
|
+
}
|
|
5521
|
+
function sendsOrRunsCode(word2) {
|
|
5522
|
+
return nameIn(programOf(word2), NETWORK_CLIENTS_AND_INTERPRETERS) !== void 0;
|
|
5523
|
+
}
|
|
5524
|
+
var SUBCOMMANDS_THAT_RUN_CODE = {
|
|
5525
|
+
bundle: ["e", "ex", "exe", "exec"],
|
|
5526
|
+
bundler: ["e", "ex", "exe", "exec"],
|
|
5527
|
+
cargo: ["r", "run"],
|
|
5528
|
+
docker: ["build", "buildx", "compose", "container", "create", "exec", "run", "start"],
|
|
5529
|
+
"docker-compose": ["create", "exec", "run", "start", "up"],
|
|
5530
|
+
dotnet: ["exec", "run", "tool"],
|
|
5531
|
+
go: ["generate", "run", "tool"],
|
|
5532
|
+
nix: ["develop", "run", "shell"],
|
|
5533
|
+
npm: ["create", "exec", "explore", "init", "innit", "x"],
|
|
5534
|
+
pipx: ["run"],
|
|
5535
|
+
pnpm: ["create", "dlx", "exec", "x"],
|
|
5536
|
+
podman: ["build", "compose", "container", "create", "exec", "run", "start"],
|
|
5537
|
+
poetry: ["run"],
|
|
5538
|
+
uv: ["run", "tool"],
|
|
5539
|
+
yarn: ["create", "dlx", "exec", "node"],
|
|
5540
|
+
yarnpkg: ["create", "dlx", "exec", "node"]
|
|
5541
|
+
};
|
|
5542
|
+
var TAKES_A_SHORTENED_SUBCOMMAND = ["npm"];
|
|
5543
|
+
function subcommandToolOf(word2) {
|
|
5544
|
+
return nameIn(programOf(word2), Object.keys(SUBCOMMANDS_THAT_RUN_CODE));
|
|
5545
|
+
}
|
|
5546
|
+
function needsItsSubcommand(words) {
|
|
5547
|
+
const [first, second] = words;
|
|
5548
|
+
if (first === void 0 || subcommandToolOf(first) === void 0) return false;
|
|
5549
|
+
return second === void 0 || second.startsWith("-");
|
|
5550
|
+
}
|
|
5551
|
+
function runsCodeBySubcommand(words) {
|
|
5552
|
+
const [first, ...rest] = words;
|
|
5553
|
+
if (first === void 0) return false;
|
|
5554
|
+
const tool = subcommandToolOf(first);
|
|
5555
|
+
if (tool === void 0) return false;
|
|
5556
|
+
const listed = SUBCOMMANDS_THAT_RUN_CODE[tool] ?? [];
|
|
5557
|
+
const shortened = TAKES_A_SHORTENED_SUBCOMMAND.includes(tool);
|
|
5558
|
+
let pastOptions = false;
|
|
5559
|
+
for (const word2 of rest) {
|
|
5560
|
+
if (word2 === "--") {
|
|
5561
|
+
if (pastOptions) return false;
|
|
5562
|
+
continue;
|
|
5563
|
+
}
|
|
5564
|
+
if (!word2.startsWith("-")) pastOptions = true;
|
|
5565
|
+
const lower = word2.toLowerCase();
|
|
5566
|
+
if (listed.includes(lower)) return true;
|
|
5567
|
+
if (shortened && lower.length >= 2 && listed.some((name) => name.startsWith(lower)))
|
|
5568
|
+
return true;
|
|
5569
|
+
}
|
|
5570
|
+
return false;
|
|
5571
|
+
}
|
|
5572
|
+
var OPTIONS_THAT_RUN_A_PROGRAM = {
|
|
5573
|
+
git: ["c", "config-env", "exec-path", "receive-pack", "upload-pack"],
|
|
5574
|
+
go: ["exec", "toolexec", "vettool"],
|
|
5575
|
+
npm: ["call", "node-options", "script-shell"]
|
|
5576
|
+
};
|
|
5577
|
+
function runsAProgramByOption(words) {
|
|
5578
|
+
const [first, ...rest] = words;
|
|
5579
|
+
if (first === void 0) return false;
|
|
5580
|
+
const program = nameIn(programOf(first), Object.keys(OPTIONS_THAT_RUN_A_PROGRAM));
|
|
5581
|
+
if (program === void 0) return false;
|
|
5582
|
+
const options = OPTIONS_THAT_RUN_A_PROGRAM[program] ?? [];
|
|
5583
|
+
return rest.some((word2) => {
|
|
5584
|
+
if (!word2.startsWith("-")) return false;
|
|
5585
|
+
const [name = ""] = word2.replace(/^-+/, "").split("=");
|
|
5586
|
+
return options.includes(name);
|
|
5587
|
+
});
|
|
5588
|
+
}
|
|
5589
|
+
var OWN_FOLDER_NAME = ".agent-lookout";
|
|
5590
|
+
var BY_NUMBER = "/.vol";
|
|
5591
|
+
function plainPath(word2) {
|
|
5592
|
+
const lower = word2.toLowerCase();
|
|
5593
|
+
if (!lower.startsWith("/")) return lower;
|
|
5594
|
+
const parts = [];
|
|
5595
|
+
for (const part of lower.split("/")) {
|
|
5596
|
+
if (part === "" || part === ".") continue;
|
|
5597
|
+
if (part === "..") parts.pop();
|
|
5598
|
+
else parts.push(part);
|
|
5599
|
+
}
|
|
5600
|
+
return `/${parts.join("/")}`;
|
|
5601
|
+
}
|
|
5602
|
+
function foldersAbove(file) {
|
|
5603
|
+
const folders = [];
|
|
5604
|
+
let at = file;
|
|
5605
|
+
while (at !== "/" && at !== "") {
|
|
5606
|
+
at = at.slice(0, at.lastIndexOf("/")) || "/";
|
|
5607
|
+
folders.push(at);
|
|
5608
|
+
}
|
|
5609
|
+
return folders;
|
|
5610
|
+
}
|
|
5611
|
+
function namesOwnFiles(words, ownPaths = [], folder) {
|
|
5612
|
+
const own = ownPaths.filter((name) => name.startsWith("/")).map(plainPath);
|
|
5613
|
+
const above = new Set(own.flatMap(foldersAbove));
|
|
5614
|
+
const ownNames = own.filter((name) => !name.includes(OWN_FOLDER_NAME)).map((name) => name.slice(name.lastIndexOf("/") + 1));
|
|
5615
|
+
const from = folder?.startsWith("/") ? folder : void 0;
|
|
5616
|
+
return words.some((word2) => {
|
|
5617
|
+
const lower = word2.toLowerCase();
|
|
5618
|
+
if (lower.includes(OWN_FOLDER_NAME)) return true;
|
|
5619
|
+
const parts = [lower, ...lower.split("=").slice(1)];
|
|
5620
|
+
const read = parts.flatMap(
|
|
5621
|
+
(part) => from !== void 0 && !part.startsWith("/") && !part.startsWith("-") ? [part, `${from}/${part}`] : [part]
|
|
5622
|
+
);
|
|
5623
|
+
return read.some((part) => {
|
|
5624
|
+
const path23 = plainPath(part);
|
|
5625
|
+
if (path23 === BY_NUMBER || path23.startsWith(`${BY_NUMBER}/`)) return true;
|
|
5626
|
+
if (own.some((name) => path23 === name || path23.startsWith(`${name}/`))) return true;
|
|
5627
|
+
if (above.has(path23)) return true;
|
|
5628
|
+
return ownNames.some((name) => path23 === name || path23.endsWith(`/${name}`));
|
|
5629
|
+
});
|
|
5630
|
+
});
|
|
5631
|
+
}
|
|
5632
|
+
function folderReachesOwnFiles(folder, ownPaths = []) {
|
|
5633
|
+
const own = ownPaths.filter((name) => name.startsWith("/")).map(plainPath);
|
|
5634
|
+
const path23 = plainPath(folder);
|
|
5635
|
+
return own.some((name) => name === path23 || name.startsWith(`${path23 === "/" ? "" : path23}/`));
|
|
5636
|
+
}
|
|
5637
|
+
var PLAIN_WORD = /^[A-Za-z0-9_\-.,/:=+@%]+$/;
|
|
5638
|
+
var ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
5639
|
+
function plainCommandWords(command) {
|
|
5640
|
+
if (command === "") return null;
|
|
5641
|
+
const words = command.split(" ");
|
|
5642
|
+
for (const word2 of words) {
|
|
5643
|
+
if (word2 === "" || !PLAIN_WORD.test(word2)) return null;
|
|
5644
|
+
if (word2.startsWith("=")) return null;
|
|
5645
|
+
}
|
|
5646
|
+
if (ASSIGNMENT.test(words[0])) return null;
|
|
5647
|
+
return words;
|
|
5648
|
+
}
|
|
5649
|
+
var SEPARATORS = /[;&|\n\r()`{}<>]/;
|
|
5650
|
+
var DOLLAR_QUOTE = /\$(?=['"])/g;
|
|
5651
|
+
var QUOTING = /['"\\]/g;
|
|
5652
|
+
var CLAUSE_WORDS = ["!", "if", "then", "else", "elif", "do", "while", "until"];
|
|
5653
|
+
function looseCommandWords(command) {
|
|
5654
|
+
const commands = [];
|
|
5655
|
+
for (const part of command.split(SEPARATORS)) {
|
|
5656
|
+
const words = part.split(/\s+/u).map((word2) => word2.replace(DOLLAR_QUOTE, "").replace(QUOTING, "")).filter((word2) => word2 !== "");
|
|
5657
|
+
let first = 0;
|
|
5658
|
+
for (; ; ) {
|
|
5659
|
+
const word2 = words[first];
|
|
5660
|
+
if (word2 === void 0) break;
|
|
5661
|
+
if (!ASSIGNMENT.test(word2) && !CLAUSE_WORDS.includes(word2)) break;
|
|
5662
|
+
first += 1;
|
|
5663
|
+
}
|
|
5664
|
+
if (first < words.length) commands.push(words.slice(first));
|
|
5665
|
+
}
|
|
5666
|
+
return commands;
|
|
5667
|
+
}
|
|
5668
|
+
var PREFIX_MARK = ":*";
|
|
5669
|
+
function ruleCommandOf(command) {
|
|
5670
|
+
const prefix = command.endsWith(PREFIX_MARK);
|
|
5671
|
+
const words = (prefix ? command.slice(0, -PREFIX_MARK.length) : command).split(" ");
|
|
5672
|
+
return { words, prefix };
|
|
5673
|
+
}
|
|
5674
|
+
function wordsMatch(rule, words) {
|
|
5675
|
+
if (rule.prefix ? words.length < rule.words.length : words.length !== rule.words.length) {
|
|
5676
|
+
return false;
|
|
5677
|
+
}
|
|
5678
|
+
return rule.words.every((word2, index) => words[index] === word2);
|
|
5679
|
+
}
|
|
5680
|
+
function inOrder(wanted, words, from, to) {
|
|
5681
|
+
let at = from;
|
|
5682
|
+
for (const word2 of wanted) {
|
|
5683
|
+
while (at < to && words[at] !== word2) at += 1;
|
|
5684
|
+
if (at >= to) return -1;
|
|
5685
|
+
at += 1;
|
|
5686
|
+
}
|
|
5687
|
+
return at;
|
|
5688
|
+
}
|
|
5689
|
+
function looselyFrom(rule, words, start) {
|
|
5690
|
+
const [program, ...rest] = rule.words;
|
|
5691
|
+
if (program === void 0 || programOf(words[start]) !== programOf(program)) {
|
|
5692
|
+
return false;
|
|
5693
|
+
}
|
|
5694
|
+
if (rule.prefix) return inOrder(rest, words, start + 1, words.length) >= 0;
|
|
5695
|
+
const last = rest.at(-1);
|
|
5696
|
+
if (last === void 0) return words.length === start + 1;
|
|
5697
|
+
const end = words.length - 1;
|
|
5698
|
+
return end > start && words[end] === last && inOrder(rest.slice(0, -1), words, start + 1, end) >= 0;
|
|
5699
|
+
}
|
|
5700
|
+
function looselyMatches(rule, words) {
|
|
5701
|
+
const first = words[0];
|
|
5702
|
+
if (first === void 0) return false;
|
|
5703
|
+
const starts = runsAnotherCommand(first) ? words.length : 1;
|
|
5704
|
+
for (let start = 0; start < starts; start += 1) {
|
|
5705
|
+
if (looselyFrom(rule, words, start)) return true;
|
|
5706
|
+
}
|
|
5707
|
+
return false;
|
|
5708
|
+
}
|
|
5709
|
+
|
|
5710
|
+
// src/core/permission-rules/permissionRules.ts
|
|
5711
|
+
var RULE_DECISIONS = ["allow", "ask", "deny"];
|
|
5712
|
+
var EVERY_TOOL = "*";
|
|
5713
|
+
var COMMAND_TOOL = "Bash";
|
|
5714
|
+
var MCP_PREFIX = "mcp__";
|
|
5715
|
+
var MCP_SEPARATOR = "__";
|
|
5716
|
+
function isServerName(tool) {
|
|
5717
|
+
if (!tool.startsWith(MCP_PREFIX)) return false;
|
|
5718
|
+
const server = tool.slice(MCP_PREFIX.length);
|
|
5719
|
+
return server !== "" && !server.includes(MCP_SEPARATOR);
|
|
5720
|
+
}
|
|
5721
|
+
var MAX_RULES = 100;
|
|
5722
|
+
var MAX_RULE_TOOL_CHARS = 128;
|
|
5723
|
+
var MAX_RULE_COMMAND_CHARS = 200;
|
|
5724
|
+
var TOOL_NAME = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
5725
|
+
var RULE_ID = /^[A-Za-z0-9-]{1,40}$/;
|
|
5726
|
+
var ASCII_WORDS = /^[\x21-\x7e]+(?: [\x21-\x7e]+)*$/;
|
|
5727
|
+
var NOT_IN_A_RULE = /[;&|<>()`'"\\{}$*]/;
|
|
5728
|
+
function isRecord7(value) {
|
|
5729
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5730
|
+
}
|
|
5731
|
+
function holdsOnly(value, keys, optional = []) {
|
|
5732
|
+
const there = Object.keys(value);
|
|
5733
|
+
return keys.every((key) => there.includes(key)) && there.every((key) => keys.includes(key) || optional.includes(key));
|
|
5734
|
+
}
|
|
5735
|
+
function toolProblem(tool) {
|
|
5736
|
+
if (tool === EVERY_TOOL) return null;
|
|
5737
|
+
if (tool === "") return "Name a tool, such as Bash or WebFetch, or * for every tool.";
|
|
5738
|
+
if (tool.includes("(")) {
|
|
5739
|
+
return "Write the tool's name alone, such as Bash, and the command in its own field.";
|
|
5740
|
+
}
|
|
5741
|
+
if (tool !== COMMAND_TOOL && tool.toLowerCase() === COMMAND_TOOL.toLowerCase()) {
|
|
5742
|
+
return `Claude Code writes this tool's name ${COMMAND_TOOL}.`;
|
|
5743
|
+
}
|
|
5744
|
+
if (tool.length > MAX_RULE_TOOL_CHARS || !TOOL_NAME.test(tool) || tool === MCP_PREFIX || tool.startsWith(MCP_PREFIX) && tool.endsWith(MCP_SEPARATOR)) {
|
|
5745
|
+
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.";
|
|
5746
|
+
}
|
|
5747
|
+
return null;
|
|
5748
|
+
}
|
|
5749
|
+
function commandProblem(command, decision) {
|
|
5750
|
+
if (command.length > MAX_RULE_COMMAND_CHARS) {
|
|
5751
|
+
return `A command in a rule is ${MAX_RULE_COMMAND_CHARS} characters at most.`;
|
|
5752
|
+
}
|
|
5753
|
+
if (!ASCII_WORDS.test(command)) {
|
|
5754
|
+
return "Write the command in plain ASCII letters, digits and marks, with one space between words and none at either end.";
|
|
5755
|
+
}
|
|
5756
|
+
const prefix = command.endsWith(PREFIX_MARK);
|
|
5757
|
+
const words = prefix ? command.slice(0, -PREFIX_MARK.length) : command;
|
|
5758
|
+
if (words === "" || words.endsWith(" ")) {
|
|
5759
|
+
return "Write :* straight after the words a command begins with, as in npm test:*.";
|
|
5760
|
+
}
|
|
5761
|
+
if (command.endsWith(" *")) {
|
|
5762
|
+
return "Write a prefix with :* straight after its words, as in npm test:*.";
|
|
5763
|
+
}
|
|
5764
|
+
if (NOT_IN_A_RULE.test(words)) {
|
|
5765
|
+
return "A rule names one command by its words: no ;, &, |, <, >, ( ), quotes, backslashes, $, { }, or * other than the :* that ends a prefix.";
|
|
5766
|
+
}
|
|
5767
|
+
if (decision === "allow" && plainCommandWords(words) === null) {
|
|
5768
|
+
return "An allow rule names one plain command: no ~, !, ^, #, ?, [ ], no word starting with =, and no variable set before it.";
|
|
5769
|
+
}
|
|
5770
|
+
const [program = ""] = words.split(" ");
|
|
5771
|
+
if (ASSIGNMENT.test(program)) {
|
|
5772
|
+
return "Begin the command with the program's name: the variables set before a command are passed over when it is matched.";
|
|
5773
|
+
}
|
|
5774
|
+
if (decision === "allow" && runsAnotherCommand(program)) {
|
|
5775
|
+
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.`;
|
|
5776
|
+
}
|
|
5777
|
+
if (decision === "allow" && sendsOrRunsCode(program)) {
|
|
5778
|
+
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.`;
|
|
5779
|
+
}
|
|
5780
|
+
if (decision === "allow" && needsItsSubcommand(words.split(" "))) {
|
|
5781
|
+
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:*.`;
|
|
5782
|
+
}
|
|
5783
|
+
if (decision === "allow" && runsAProgramByOption(words.split(" "))) {
|
|
5784
|
+
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.`;
|
|
5785
|
+
}
|
|
5786
|
+
if (decision === "allow" && runsCodeBySubcommand(words.split(" "))) {
|
|
5787
|
+
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.`;
|
|
5788
|
+
}
|
|
5789
|
+
if (decision === "allow" && namesOwnFiles(words.split(" "))) {
|
|
5790
|
+
return "An allow rule may not name Agent Lookout's own folder, ~/.agent-lookout, which holds its settings and its rules.";
|
|
5791
|
+
}
|
|
5792
|
+
return null;
|
|
5793
|
+
}
|
|
5794
|
+
function ruleProblem(words) {
|
|
5795
|
+
const { decision, tool, command } = words;
|
|
5796
|
+
const toolWrong = toolProblem(tool);
|
|
5797
|
+
if (toolWrong !== null) return toolWrong;
|
|
5798
|
+
if (command !== void 0) {
|
|
5799
|
+
if (tool !== COMMAND_TOOL)
|
|
5800
|
+
return "A command goes with Bash only. Leave it empty for any other tool.";
|
|
5801
|
+
const commandWrong = commandProblem(command, decision);
|
|
5802
|
+
if (commandWrong !== null) return commandWrong;
|
|
5803
|
+
}
|
|
5804
|
+
if (decision !== "allow") return null;
|
|
5805
|
+
if (tool === EVERY_TOOL) {
|
|
5806
|
+
return "An allow rule names one tool. Allowing every tool would let Claude Code run anything without asking you.";
|
|
5807
|
+
}
|
|
5808
|
+
if (tool === COMMAND_TOOL && command === void 0) {
|
|
5809
|
+
return "An allow rule for Bash names a command. Allowing every command would let Claude Code run anything without asking you.";
|
|
5810
|
+
}
|
|
5811
|
+
if (isServerName(tool)) {
|
|
5812
|
+
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.`;
|
|
5813
|
+
}
|
|
5814
|
+
if (EDIT_TOOLS.includes(tool)) {
|
|
5815
|
+
return `Agent Lookout does not show the change ${tool} makes, so it never allows one, by hand or by a rule.`;
|
|
5816
|
+
}
|
|
5817
|
+
if (NOT_YES_OR_NO_TOOLS.includes(tool)) {
|
|
5818
|
+
return `${tool} is answered with more than yes or no, so Agent Lookout never allows it, by hand or by a rule.`;
|
|
5819
|
+
}
|
|
5820
|
+
return null;
|
|
5821
|
+
}
|
|
5822
|
+
function ruleWordsIn(value) {
|
|
5823
|
+
const shape = 'A rule is {"decision": "allow", "ask" or "deny", "tool": "...", "command": "..."}, with the command for Bash only, and nothing else.';
|
|
5824
|
+
if (!isRecord7(value) || !holdsOnly(value, ["decision", "tool"], ["command"])) {
|
|
5825
|
+
return { ok: false, problem: shape };
|
|
5826
|
+
}
|
|
5827
|
+
const { decision, tool, command } = value;
|
|
5828
|
+
if (!RULE_DECISIONS.includes(decision))
|
|
5829
|
+
return { ok: false, problem: shape };
|
|
5830
|
+
if (typeof tool !== "string" || command !== void 0 && typeof command !== "string") {
|
|
5831
|
+
return { ok: false, problem: shape };
|
|
5832
|
+
}
|
|
5833
|
+
const words = {
|
|
5834
|
+
decision,
|
|
5835
|
+
tool,
|
|
5836
|
+
...command !== void 0 && { command }
|
|
5837
|
+
};
|
|
5838
|
+
const problem = ruleProblem(words);
|
|
5839
|
+
return problem === null ? { ok: true, words } : { ok: false, problem };
|
|
5840
|
+
}
|
|
5841
|
+
function isRuleId(value) {
|
|
5842
|
+
return typeof value === "string" && RULE_ID.test(value);
|
|
5843
|
+
}
|
|
5844
|
+
function readRule2(value) {
|
|
5845
|
+
if (!isRecord7(value)) return null;
|
|
5846
|
+
const { id, ...rest } = value;
|
|
5847
|
+
if (!isRuleId(id)) return null;
|
|
5848
|
+
const read = ruleWordsIn(rest);
|
|
5849
|
+
return read.ok ? { id, ...read.words } : null;
|
|
5850
|
+
}
|
|
5851
|
+
function readPermissionRules(value) {
|
|
5852
|
+
if (value === void 0) return { ok: true, rules: [] };
|
|
5853
|
+
if (!Array.isArray(value) || value.length > MAX_RULES) return { ok: false };
|
|
5854
|
+
const rules = [];
|
|
5855
|
+
const ids = /* @__PURE__ */ new Set();
|
|
5856
|
+
for (const item of value) {
|
|
5857
|
+
const rule = readRule2(item);
|
|
5858
|
+
if (rule === null || ids.has(rule.id)) return { ok: false };
|
|
5859
|
+
ids.add(rule.id);
|
|
5860
|
+
rules.push(rule);
|
|
5861
|
+
}
|
|
5862
|
+
return { ok: true, rules };
|
|
5863
|
+
}
|
|
5864
|
+
function sameWords(a, b) {
|
|
5865
|
+
return a.decision === b.decision && a.tool === b.tool && a.command === b.command;
|
|
5866
|
+
}
|
|
5867
|
+
|
|
5868
|
+
// src/core/permission-rules/ruleDecision.ts
|
|
5869
|
+
var INPUTS_AN_ALLOW_RULE_TAKES = ["timeout", "run_in_background"];
|
|
5870
|
+
var ORDER = ["deny", "ask", "allow"];
|
|
5871
|
+
function toolMatches(rule, request2) {
|
|
5872
|
+
if (rule.tool === EVERY_TOOL || rule.tool === request2.tool) return true;
|
|
5873
|
+
return isServerName(rule.tool) && request2.tool.startsWith(`${rule.tool}${MCP_SEPARATOR}`);
|
|
5874
|
+
}
|
|
5875
|
+
function holdsBack(rule, request2) {
|
|
5876
|
+
if (!toolMatches(rule, request2)) return false;
|
|
5877
|
+
if (rule.command === void 0) return true;
|
|
5878
|
+
if (request2.tool !== COMMAND_TOOL || request2.command === void 0) return false;
|
|
5879
|
+
const wanted = ruleCommandOf(rule.command);
|
|
5880
|
+
return looseCommandWords(request2.command).some((words) => looselyMatches(wanted, words));
|
|
5881
|
+
}
|
|
5882
|
+
function lets(rule, request2) {
|
|
5883
|
+
if (!request2.allowable || rule.tool === EVERY_TOOL || isServerName(rule.tool)) return false;
|
|
5884
|
+
if (rule.tool !== request2.tool) return false;
|
|
5885
|
+
if (request2.tool !== COMMAND_TOOL) return rule.command === void 0;
|
|
5886
|
+
if (rule.command === void 0 || request2.command === void 0) return false;
|
|
5887
|
+
if (!request2.inputNames.every((name) => INPUTS_AN_ALLOW_RULE_TAKES.includes(name))) return false;
|
|
5888
|
+
const wanted = ruleCommandOf(rule.command);
|
|
5889
|
+
const [program = ""] = wanted.words;
|
|
5890
|
+
if (runsAnotherCommand(program) || sendsOrRunsCode(program)) return false;
|
|
5891
|
+
if (needsItsSubcommand(wanted.words)) return false;
|
|
5892
|
+
const words = plainCommandWords(request2.command);
|
|
5893
|
+
if (words === null || runsCodeBySubcommand(words) || runsAProgramByOption(words)) return false;
|
|
5894
|
+
const own = request2.ownPaths ?? [];
|
|
5895
|
+
if (request2.folder !== void 0 && folderReachesOwnFiles(request2.folder, own)) return false;
|
|
5896
|
+
if (namesOwnFiles(words, own, request2.folder)) return false;
|
|
5897
|
+
return wordsMatch(wanted, words);
|
|
5898
|
+
}
|
|
5899
|
+
function decideByRules(rules, request2) {
|
|
5900
|
+
for (const decision of ORDER) {
|
|
5901
|
+
const matches = decision === "allow" ? lets : holdsBack;
|
|
5902
|
+
const rule = rules.find(
|
|
5903
|
+
(candidate) => candidate.decision === decision && matches(candidate, request2)
|
|
5904
|
+
);
|
|
5905
|
+
if (rule !== void 0) return { decision, rule };
|
|
5906
|
+
}
|
|
5907
|
+
return null;
|
|
5908
|
+
}
|
|
5909
|
+
|
|
5910
|
+
// src/collector/answers/ruleAnswers.ts
|
|
5911
|
+
function ruleAnsweredEvent(session, sessionId, tool, verdict, at) {
|
|
5912
|
+
const { rule } = verdict;
|
|
5913
|
+
return {
|
|
5914
|
+
id: `${sessionId}@${at}:answered`,
|
|
5915
|
+
at,
|
|
5916
|
+
sessionId,
|
|
5917
|
+
sessionName: session?.name ?? sessionId,
|
|
5918
|
+
kind: "answered",
|
|
5919
|
+
...session && { from: session.status },
|
|
5920
|
+
severity: "advisory",
|
|
5921
|
+
by: "agent-lookout",
|
|
5922
|
+
decision: verdict.decision,
|
|
5923
|
+
tool,
|
|
5924
|
+
rule: { tool: rule.tool, ...rule.command !== void 0 && { command: rule.command } }
|
|
5925
|
+
};
|
|
5926
|
+
}
|
|
5927
|
+
function createRuleAnswers(options) {
|
|
5928
|
+
const { poller, events } = options;
|
|
5929
|
+
const now = options.now ?? Date.now;
|
|
5930
|
+
let recent = [];
|
|
5931
|
+
return {
|
|
5932
|
+
verdictFor({ sessionId, shown }) {
|
|
5933
|
+
const rules = options.rules();
|
|
5934
|
+
if (rules.length === 0) return null;
|
|
5935
|
+
const folder = poller.getSnapshot().sessions.find((listed) => listed.id === sessionId)?.cwd;
|
|
5936
|
+
const verdict = decideByRules(rules, {
|
|
5937
|
+
tool: shown.tool,
|
|
5938
|
+
...shown.command !== void 0 && { command: shown.command },
|
|
5939
|
+
inputNames: shown.command === void 0 ? [] : (shown.inputs ?? []).map((input) => input.name),
|
|
5940
|
+
// The one rule for what may be allowed: what the dashboard would offer Allow for.
|
|
5941
|
+
allowable: shown.allow,
|
|
5942
|
+
ownPaths: options.ownPaths ?? [],
|
|
5943
|
+
...folder && { folder }
|
|
5944
|
+
});
|
|
5945
|
+
if (verdict === null || verdict.decision === "ask") return null;
|
|
5946
|
+
return { decision: verdict.decision, rule: verdict.rule };
|
|
5947
|
+
},
|
|
5948
|
+
answered({ sessionId, shown }, verdict) {
|
|
5949
|
+
const at = now();
|
|
5950
|
+
const session = poller.getSnapshot().sessions.find((listed) => listed.id === sessionId) ?? null;
|
|
5951
|
+
const event = ruleAnsweredEvent(session, sessionId, shown.tool, verdict, at);
|
|
5952
|
+
events.add([event]);
|
|
5953
|
+
const { id: _id, ...rule } = verdict.rule;
|
|
5954
|
+
recent = [
|
|
5955
|
+
{
|
|
5956
|
+
at,
|
|
5957
|
+
sessionId,
|
|
5958
|
+
sessionName: event.sessionName,
|
|
5959
|
+
tool: shown.tool,
|
|
5960
|
+
decision: verdict.decision,
|
|
5961
|
+
rule
|
|
5962
|
+
},
|
|
5963
|
+
...recent
|
|
5964
|
+
].slice(0, MAX_RULE_ANSWERS);
|
|
5965
|
+
void poller.pollOnce().catch(() => {
|
|
5966
|
+
});
|
|
5967
|
+
},
|
|
5968
|
+
recent: () => [...recent]
|
|
5969
|
+
};
|
|
5970
|
+
}
|
|
5971
|
+
|
|
5035
5972
|
// src/collector/answers/answering.ts
|
|
5036
5973
|
function createAnswering(options) {
|
|
5037
5974
|
const { env, poller, events } = options;
|
|
@@ -5046,14 +5983,26 @@ function createAnswering(options) {
|
|
|
5046
5983
|
},
|
|
5047
5984
|
serve: (snapshot) => ({ ...snapshot, answering: off }),
|
|
5048
5985
|
observe: () => {
|
|
5049
|
-
}
|
|
5986
|
+
},
|
|
5987
|
+
answeredIn: () => /* @__PURE__ */ new Set(),
|
|
5988
|
+
ruleAnswers: () => []
|
|
5050
5989
|
};
|
|
5051
5990
|
}
|
|
5991
|
+
const ruleAnswers = createRuleAnswers({
|
|
5992
|
+
rules: options.rules ?? (() => []),
|
|
5993
|
+
// A command that names the settings file or the socket could change the
|
|
5994
|
+
// rules or answer prompts, so no rule allows it, wherever they are.
|
|
5995
|
+
ownPaths: [options.settingsFile ?? "", setup.socketPath],
|
|
5996
|
+
poller,
|
|
5997
|
+
events,
|
|
5998
|
+
now: options.now
|
|
5999
|
+
});
|
|
5052
6000
|
const asks = createHeldAsks({
|
|
5053
6001
|
status: options.status ?? createRegistryStatus({ snapshot: poller.getSnapshot, env }),
|
|
5054
6002
|
holdMs: setup.holdMs,
|
|
5055
6003
|
now: options.now,
|
|
5056
|
-
every: options.every
|
|
6004
|
+
every: options.every,
|
|
6005
|
+
rules: ruleAnswers
|
|
5057
6006
|
});
|
|
5058
6007
|
const socket = createHookSocket({
|
|
5059
6008
|
socketPath: setup.socketPath,
|
|
@@ -5062,6 +6011,7 @@ function createAnswering(options) {
|
|
|
5062
6011
|
});
|
|
5063
6012
|
let problem = null;
|
|
5064
6013
|
let listening = false;
|
|
6014
|
+
const answerRoute = createAnswerRoute({ poller, events, asks, now: options.now });
|
|
5065
6015
|
return {
|
|
5066
6016
|
async start() {
|
|
5067
6017
|
const started = await socket.start();
|
|
@@ -5085,7 +6035,16 @@ function createAnswering(options) {
|
|
|
5085
6035
|
observe(snapshot) {
|
|
5086
6036
|
if (listening) asks.observe(snapshot);
|
|
5087
6037
|
},
|
|
5088
|
-
|
|
6038
|
+
answeredIn: (snapshot) => asks.answeredIn(snapshot),
|
|
6039
|
+
// A socket that could not be opened holds no request, so there is nothing
|
|
6040
|
+
// to answer. Past the route's own checks, a POST is told what it would be
|
|
6041
|
+
// told with answering off, where there is no such route.
|
|
6042
|
+
async route(req) {
|
|
6043
|
+
if (problem === null) return answerRoute(req);
|
|
6044
|
+
req.resume();
|
|
6045
|
+
return answerRefusalFor(req) ?? refusal(405, "This address only answers GET requests.", { Allow: "GET" });
|
|
6046
|
+
},
|
|
6047
|
+
ruleAnswers: () => ruleAnswers.recent()
|
|
5089
6048
|
};
|
|
5090
6049
|
}
|
|
5091
6050
|
|
|
@@ -5221,8 +6180,8 @@ function waitsOf(input) {
|
|
|
5221
6180
|
names.set(sessionId, event.sessionName);
|
|
5222
6181
|
}
|
|
5223
6182
|
if (began !== null) {
|
|
5224
|
-
const
|
|
5225
|
-
waits.push({ sessionId, from: began, to:
|
|
6183
|
+
const open2 = waitingNow.has(sessionId);
|
|
6184
|
+
waits.push({ sessionId, from: began, to: open2 || began >= thisRun ? now : thisRun, open: open2 });
|
|
5226
6185
|
unended.add(sessionId);
|
|
5227
6186
|
}
|
|
5228
6187
|
}
|
|
@@ -5314,26 +6273,26 @@ function waitTotals(input) {
|
|
|
5314
6273
|
|
|
5315
6274
|
// src/collector/adapters/codex/index.ts
|
|
5316
6275
|
import os5 from "node:os";
|
|
5317
|
-
import
|
|
6276
|
+
import path12 from "node:path";
|
|
5318
6277
|
|
|
5319
6278
|
// src/core/mapping/codexMapping.ts
|
|
5320
6279
|
var TURN_UNDER_WAY = /* @__PURE__ */ new Set(["task_started", "turn_started"]);
|
|
5321
6280
|
var TURN_OVER = /* @__PURE__ */ new Set(["task_complete", "turn_complete", "turn_aborted"]);
|
|
5322
6281
|
function mapCodexStatus(fields) {
|
|
5323
6282
|
const { lastTurn, live: live2 } = fields;
|
|
5324
|
-
let
|
|
6283
|
+
let open2;
|
|
5325
6284
|
if (lastTurn === null) {
|
|
5326
|
-
|
|
6285
|
+
open2 = "idle";
|
|
5327
6286
|
} else if (typeof lastTurn !== "string") {
|
|
5328
6287
|
return "unknown";
|
|
5329
6288
|
} else if (TURN_UNDER_WAY.has(lastTurn)) {
|
|
5330
|
-
|
|
6289
|
+
open2 = "working";
|
|
5331
6290
|
} else if (TURN_OVER.has(lastTurn)) {
|
|
5332
|
-
|
|
6291
|
+
open2 = "idle";
|
|
5333
6292
|
} else {
|
|
5334
6293
|
return "unknown";
|
|
5335
6294
|
}
|
|
5336
|
-
return live2 === false ? "finished" :
|
|
6295
|
+
return live2 === false ? "finished" : open2;
|
|
5337
6296
|
}
|
|
5338
6297
|
var WRITER_LOCKS_SINCE = [0, 155, 0];
|
|
5339
6298
|
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 +6316,7 @@ var IMPORTED_TURN_ID = /^external-import-turn-\d{1,9}$/;
|
|
|
5357
6316
|
function isImportedCodexTurn(turnId) {
|
|
5358
6317
|
return typeof turnId === "string" && IMPORTED_TURN_ID.test(turnId);
|
|
5359
6318
|
}
|
|
5360
|
-
function
|
|
6319
|
+
function isRecord8(value) {
|
|
5361
6320
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5362
6321
|
}
|
|
5363
6322
|
var CODEX_DESKTOP_ORIGINATOR = "Codex Desktop";
|
|
@@ -5365,7 +6324,7 @@ function mapCodexSurface(source, originator) {
|
|
|
5365
6324
|
if (originator === CODEX_DESKTOP_ORIGINATOR) return "desktop";
|
|
5366
6325
|
if (source === "cli" || source === "exec") return "terminal";
|
|
5367
6326
|
if (source === "vscode") return "vscode";
|
|
5368
|
-
if (
|
|
6327
|
+
if (isRecord8(source) && source.custom === "chatgpt") return "desktop";
|
|
5369
6328
|
return "unknown";
|
|
5370
6329
|
}
|
|
5371
6330
|
var NOT_A_SESSION_SOURCE = /* @__PURE__ */ new Set(["subagent", "internal", "mcp"]);
|
|
@@ -5376,7 +6335,7 @@ function isCodexSessionSource(source, meta = {}) {
|
|
|
5376
6335
|
return false;
|
|
5377
6336
|
}
|
|
5378
6337
|
if (typeof source === "string") return !NOT_A_SESSION_SOURCE.has(source);
|
|
5379
|
-
if (
|
|
6338
|
+
if (isRecord8(source)) return !Object.keys(source).some((kind) => NOT_A_SESSION_SOURCE.has(kind));
|
|
5380
6339
|
return true;
|
|
5381
6340
|
}
|
|
5382
6341
|
|
|
@@ -5395,7 +6354,7 @@ function parseCodexTime(value) {
|
|
|
5395
6354
|
var LINE_TIME = /^\{"timestamp":"([^"]{1,64})"/;
|
|
5396
6355
|
var TURN_HINT = /"type":"(?:task|turn)_/;
|
|
5397
6356
|
var TURN_TYPE = /^(?:task|turn)_/;
|
|
5398
|
-
function
|
|
6357
|
+
function isRecord9(value) {
|
|
5399
6358
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5400
6359
|
}
|
|
5401
6360
|
function text3(value) {
|
|
@@ -5406,7 +6365,7 @@ function label(value) {
|
|
|
5406
6365
|
}
|
|
5407
6366
|
function compactSource(value) {
|
|
5408
6367
|
if (typeof value === "string") return text3(value);
|
|
5409
|
-
if (!
|
|
6368
|
+
if (!isRecord9(value)) return void 0;
|
|
5410
6369
|
return Object.fromEntries(
|
|
5411
6370
|
Object.entries(value).map(([kind, detail]) => [
|
|
5412
6371
|
kind,
|
|
@@ -5417,7 +6376,7 @@ function compactSource(value) {
|
|
|
5417
6376
|
function parse(line) {
|
|
5418
6377
|
try {
|
|
5419
6378
|
const value = JSON.parse(line);
|
|
5420
|
-
return
|
|
6379
|
+
return isRecord9(value) ? value : null;
|
|
5421
6380
|
} catch {
|
|
5422
6381
|
return null;
|
|
5423
6382
|
}
|
|
@@ -5425,7 +6384,7 @@ function parse(line) {
|
|
|
5425
6384
|
function readMetaLine(line) {
|
|
5426
6385
|
if (!line.includes('"session_meta"')) return null;
|
|
5427
6386
|
const value = parse(line);
|
|
5428
|
-
if (!value || value.type !== "session_meta" || !
|
|
6387
|
+
if (!value || value.type !== "session_meta" || !isRecord9(value.payload)) return null;
|
|
5429
6388
|
const payload = value.payload;
|
|
5430
6389
|
const meta = {
|
|
5431
6390
|
id: text3(payload.id),
|
|
@@ -5445,7 +6404,7 @@ function readMetaLine(line) {
|
|
|
5445
6404
|
function readTurnLine(line) {
|
|
5446
6405
|
if (!line.includes('"event_msg"') || !TURN_HINT.test(line)) return null;
|
|
5447
6406
|
const value = parse(line);
|
|
5448
|
-
if (!value || value.type !== "event_msg" || !
|
|
6407
|
+
if (!value || value.type !== "event_msg" || !isRecord9(value.payload)) return null;
|
|
5449
6408
|
const turn = value.payload.type;
|
|
5450
6409
|
if (typeof turn !== "string" || !TURN_TYPE.test(turn)) return null;
|
|
5451
6410
|
return {
|
|
@@ -5631,7 +6590,7 @@ function createRolloutReader(io) {
|
|
|
5631
6590
|
}
|
|
5632
6591
|
|
|
5633
6592
|
// src/collector/adapters/codex/rollouts.ts
|
|
5634
|
-
import
|
|
6593
|
+
import path11 from "node:path";
|
|
5635
6594
|
var UUID = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
|
|
5636
6595
|
var ROLLOUT_FILE_NAME = new RegExp(
|
|
5637
6596
|
`^rollout-\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-(${UUID})(?:_(${UUID}))?\\.jsonl$`,
|
|
@@ -5649,7 +6608,7 @@ function twoDigits(value) {
|
|
|
5649
6608
|
function recentDayFolders(sessionsDir, now) {
|
|
5650
6609
|
const folders = [now, now - 24 * 60 * 60 * 1e3].map((at) => {
|
|
5651
6610
|
const date = new Date(at);
|
|
5652
|
-
return
|
|
6611
|
+
return path11.join(
|
|
5653
6612
|
sessionsDir,
|
|
5654
6613
|
String(date.getFullYear()),
|
|
5655
6614
|
twoDigits(date.getMonth() + 1),
|
|
@@ -5678,12 +6637,12 @@ function createRolloutFinder(options) {
|
|
|
5678
6637
|
const refs = [];
|
|
5679
6638
|
for (const name of (await names(dir)).sort()) {
|
|
5680
6639
|
const threadId = threadIdOfRollout(name);
|
|
5681
|
-
if (threadId !== null) refs.push({ path:
|
|
6640
|
+
if (threadId !== null) refs.push({ path: path11.join(dir, name), threadId });
|
|
5682
6641
|
}
|
|
5683
6642
|
return refs;
|
|
5684
6643
|
}
|
|
5685
6644
|
async function subfolders(dir, pattern) {
|
|
5686
|
-
return (await names(dir)).filter((name) => pattern.test(name)).sort().map((name) =>
|
|
6645
|
+
return (await names(dir)).filter((name) => pattern.test(name)).sort().map((name) => path11.join(dir, name));
|
|
5687
6646
|
}
|
|
5688
6647
|
async function buildIndex() {
|
|
5689
6648
|
const years = await subfolders(sessionsDir, YEAR);
|
|
@@ -5727,7 +6686,7 @@ function createRolloutFinder(options) {
|
|
|
5727
6686
|
// src/collector/adapters/codex/sessionIndex.ts
|
|
5728
6687
|
var SESSION_INDEX_FILE = "session_index.jsonl";
|
|
5729
6688
|
var SESSION_INDEX_LIMIT_BYTES = 4 * 1024 * 1024;
|
|
5730
|
-
function
|
|
6689
|
+
function isRecord10(value) {
|
|
5731
6690
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5732
6691
|
}
|
|
5733
6692
|
function parseSessionIndex(content) {
|
|
@@ -5740,7 +6699,7 @@ function parseSessionIndex(content) {
|
|
|
5740
6699
|
} catch {
|
|
5741
6700
|
continue;
|
|
5742
6701
|
}
|
|
5743
|
-
if (!
|
|
6702
|
+
if (!isRecord10(value)) continue;
|
|
5744
6703
|
const { id, thread_name: name } = value;
|
|
5745
6704
|
if (typeof id !== "string" || id === "" || typeof name !== "string") continue;
|
|
5746
6705
|
const trimmed = name.trim();
|
|
@@ -5780,7 +6739,7 @@ function createSessionIndexReader(file, io) {
|
|
|
5780
6739
|
}
|
|
5781
6740
|
names = await readNames();
|
|
5782
6741
|
} catch (error) {
|
|
5783
|
-
if (
|
|
6742
|
+
if (isMissing2(error)) {
|
|
5784
6743
|
seen = null;
|
|
5785
6744
|
names = /* @__PURE__ */ new Map();
|
|
5786
6745
|
}
|
|
@@ -5844,14 +6803,14 @@ async function readWriterLocks(dir, io) {
|
|
|
5844
6803
|
try {
|
|
5845
6804
|
names = await io.readdir(dir);
|
|
5846
6805
|
} catch (error) {
|
|
5847
|
-
return { supported: false, missing:
|
|
6806
|
+
return { supported: false, missing: isMissing2(error) };
|
|
5848
6807
|
}
|
|
5849
|
-
const
|
|
6808
|
+
const open2 = /* @__PURE__ */ new Set();
|
|
5850
6809
|
for (const name of names) {
|
|
5851
6810
|
const threadId = threadIdOfLock(name);
|
|
5852
|
-
if (threadId !== null)
|
|
6811
|
+
if (threadId !== null) open2.add(threadId);
|
|
5853
6812
|
}
|
|
5854
|
-
return { supported: true, open:
|
|
6813
|
+
return { supported: true, open: open2 };
|
|
5855
6814
|
}
|
|
5856
6815
|
|
|
5857
6816
|
// src/collector/adapters/codex/index.ts
|
|
@@ -5889,7 +6848,7 @@ var CODEX_CAPABILITIES = {
|
|
|
5889
6848
|
},
|
|
5890
6849
|
answer: {
|
|
5891
6850
|
level: "no",
|
|
5892
|
-
reason: "Codex records no approval waits, so there is nothing to answer from here."
|
|
6851
|
+
reason: "Codex records no approval waits, so there is nothing to answer from here, by hand or by a permission rule."
|
|
5893
6852
|
}
|
|
5894
6853
|
};
|
|
5895
6854
|
function createCodexAdapter(options = {}) {
|
|
@@ -5902,10 +6861,10 @@ function createCodexAdapter(options = {}) {
|
|
|
5902
6861
|
const ownOverride = env[CODEX_HOME_ENV]?.trim() || void 0;
|
|
5903
6862
|
const codexOverride = env[CODEX_OWN_HOME_ENV]?.trim() || void 0;
|
|
5904
6863
|
const homeFrom = ownOverride ? "agent-lookout" : codexOverride ? "codex" : "default";
|
|
5905
|
-
const codexHome =
|
|
5906
|
-
const sessionsDir =
|
|
5907
|
-
const locksDir =
|
|
5908
|
-
const indexFile =
|
|
6864
|
+
const codexHome = path12.resolve(ownOverride ?? codexOverride ?? path12.join(homeDir, ".codex"));
|
|
6865
|
+
const sessionsDir = path12.join(codexHome, "sessions");
|
|
6866
|
+
const locksDir = path12.join(codexHome, WRITER_LOCK_DIR);
|
|
6867
|
+
const indexFile = path12.join(codexHome, SESSION_INDEX_FILE);
|
|
5909
6868
|
const homeName = tildify(codexHome, homeDir);
|
|
5910
6869
|
const sessionsName = tildify(sessionsDir, homeDir);
|
|
5911
6870
|
const locksName = tildify(locksDir, homeDir);
|
|
@@ -5953,7 +6912,7 @@ function createCodexAdapter(options = {}) {
|
|
|
5953
6912
|
try {
|
|
5954
6913
|
return (await io.stat(codexHome)).kind === "directory";
|
|
5955
6914
|
} catch (error) {
|
|
5956
|
-
return !
|
|
6915
|
+
return !isMissing2(error);
|
|
5957
6916
|
}
|
|
5958
6917
|
}
|
|
5959
6918
|
async function poll() {
|
|
@@ -5975,7 +6934,7 @@ function createCodexAdapter(options = {}) {
|
|
|
5975
6934
|
try {
|
|
5976
6935
|
await io.readdir(sessionsDir);
|
|
5977
6936
|
} catch (error) {
|
|
5978
|
-
if (!
|
|
6937
|
+
if (!isMissing2(error)) {
|
|
5979
6938
|
return result(
|
|
5980
6939
|
"error",
|
|
5981
6940
|
`Codex sessions could not be read: the folder ${sessionsName} could not be listed.`,
|
|
@@ -6023,7 +6982,7 @@ function createCodexAdapter(options = {}) {
|
|
|
6023
6982
|
const chosen = /* @__PURE__ */ new Map();
|
|
6024
6983
|
const subagentWrites = /* @__PURE__ */ new Map();
|
|
6025
6984
|
for (const ref of [...candidates.values()].sort((a, b) => a.path < b.path ? -1 : 1)) {
|
|
6026
|
-
const
|
|
6985
|
+
const open2 = locked(ref.threadId);
|
|
6027
6986
|
let info;
|
|
6028
6987
|
try {
|
|
6029
6988
|
info = await io.lstat(ref.path);
|
|
@@ -6031,7 +6990,7 @@ function createCodexAdapter(options = {}) {
|
|
|
6031
6990
|
tracked.delete(ref.path);
|
|
6032
6991
|
continue;
|
|
6033
6992
|
}
|
|
6034
|
-
if (info.kind !== "file" || !
|
|
6993
|
+
if (info.kind !== "file" || !open2 && info.mtimeMs < cutoff) {
|
|
6035
6994
|
tracked.delete(ref.path);
|
|
6036
6995
|
continue;
|
|
6037
6996
|
}
|
|
@@ -6054,7 +7013,7 @@ function createCodexAdapter(options = {}) {
|
|
|
6054
7013
|
}
|
|
6055
7014
|
if (state.lastTurnImported) continue;
|
|
6056
7015
|
const at = lastActivity(state, checkedAt);
|
|
6057
|
-
if (!
|
|
7016
|
+
if (!open2 && (at === null || !isWithinRetention(at, checkedAt))) {
|
|
6058
7017
|
tracked.delete(ref.path);
|
|
6059
7018
|
continue;
|
|
6060
7019
|
}
|
|
@@ -6113,7 +7072,7 @@ function createCodexAdapter(options = {}) {
|
|
|
6113
7072
|
|
|
6114
7073
|
// src/collector/adapters/status-files/index.ts
|
|
6115
7074
|
import os6 from "node:os";
|
|
6116
|
-
import
|
|
7075
|
+
import path13 from "node:path";
|
|
6117
7076
|
|
|
6118
7077
|
// src/core/mapping/statusFileMapping.ts
|
|
6119
7078
|
var STATUS = /* @__PURE__ */ new Map([
|
|
@@ -6248,7 +7207,7 @@ var STATUS_FILE_CAPABILITIES = {
|
|
|
6248
7207
|
},
|
|
6249
7208
|
answer: {
|
|
6250
7209
|
level: "no",
|
|
6251
|
-
reason: "A status file only says a session waits, and holds nothing to answer it through."
|
|
7210
|
+
reason: "A status file only says a session waits, and holds nothing to answer it through, by hand or by a permission rule."
|
|
6252
7211
|
}
|
|
6253
7212
|
};
|
|
6254
7213
|
function counted(count2, one, many) {
|
|
@@ -6262,7 +7221,7 @@ function createStatusFileAdapter(options = {}) {
|
|
|
6262
7221
|
const isAlive = options.isAlive ?? isProcessAlive;
|
|
6263
7222
|
const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
6264
7223
|
const override = env[STATUS_DIR_ENV]?.trim() || void 0;
|
|
6265
|
-
const dir =
|
|
7224
|
+
const dir = path13.resolve(override ?? path13.join(homeDir, ".agent-lookout", "sessions"));
|
|
6266
7225
|
const dirName = tildify(dir, homeDir);
|
|
6267
7226
|
const decoder2 = new TextDecoder();
|
|
6268
7227
|
let lastRead = /* @__PURE__ */ new Map();
|
|
@@ -6295,26 +7254,26 @@ function createStatusFileAdapter(options = {}) {
|
|
|
6295
7254
|
return { health, sessions: [] };
|
|
6296
7255
|
}
|
|
6297
7256
|
async function readOne(file) {
|
|
6298
|
-
let
|
|
7257
|
+
let open2;
|
|
6299
7258
|
try {
|
|
6300
|
-
|
|
7259
|
+
open2 = await io.openRegular(file);
|
|
6301
7260
|
} catch (error) {
|
|
6302
|
-
if (
|
|
7261
|
+
if (isMissing2(error)) return { kind: "gone" };
|
|
6303
7262
|
const code = error?.code;
|
|
6304
7263
|
const notAFile = code === void 0 || code === "ELOOP" || code === "EISDIR";
|
|
6305
7264
|
return { kind: "refused", why: notAFile ? "is not an ordinary file" : "could not be opened" };
|
|
6306
7265
|
}
|
|
6307
7266
|
const tooLarge = { kind: "refused", why: "is over 16 KB" };
|
|
6308
7267
|
try {
|
|
6309
|
-
if (
|
|
6310
|
-
const bytes2 = await
|
|
7268
|
+
if (open2.info.size > MAX_FILE_BYTES) return tooLarge;
|
|
7269
|
+
const bytes2 = await open2.read(0, MAX_FILE_BYTES + 1);
|
|
6311
7270
|
if (bytes2.byteLength > MAX_FILE_BYTES) return tooLarge;
|
|
6312
7271
|
const parsed = parseStatusFile(decoder2.decode(bytes2));
|
|
6313
|
-
return parsed ? { kind: "read", file: parsed, mtimeMs:
|
|
7272
|
+
return parsed ? { kind: "read", file: parsed, mtimeMs: open2.info.mtimeMs } : { kind: "invalid" };
|
|
6314
7273
|
} catch {
|
|
6315
7274
|
return { kind: "refused", why: "could not be read" };
|
|
6316
7275
|
} finally {
|
|
6317
|
-
await
|
|
7276
|
+
await open2.close().catch(() => {
|
|
6318
7277
|
});
|
|
6319
7278
|
}
|
|
6320
7279
|
}
|
|
@@ -6322,7 +7281,7 @@ function createStatusFileAdapter(options = {}) {
|
|
|
6322
7281
|
if (names.length <= MAX_FILES) return names;
|
|
6323
7282
|
const written = await Promise.all(
|
|
6324
7283
|
names.map(async (name) => {
|
|
6325
|
-
const info = await io.lstat(
|
|
7284
|
+
const info = await io.lstat(path13.join(dir, name)).catch(() => null);
|
|
6326
7285
|
return { name, mtimeMs: info?.mtimeMs ?? -Infinity };
|
|
6327
7286
|
})
|
|
6328
7287
|
);
|
|
@@ -6342,7 +7301,7 @@ function createStatusFileAdapter(options = {}) {
|
|
|
6342
7301
|
try {
|
|
6343
7302
|
names = await io.readdir(dir);
|
|
6344
7303
|
} catch (error) {
|
|
6345
|
-
if (!
|
|
7304
|
+
if (!isMissing2(error)) {
|
|
6346
7305
|
const answer = result(
|
|
6347
7306
|
"error",
|
|
6348
7307
|
`Status files could not be read: the folder ${dirName} could not be listed.`,
|
|
@@ -6372,7 +7331,7 @@ function createStatusFileAdapter(options = {}) {
|
|
|
6372
7331
|
const nextRead = /* @__PURE__ */ new Map();
|
|
6373
7332
|
const nextStatuses = /* @__PURE__ */ new Map();
|
|
6374
7333
|
for (const fileName2 of await toRead(listed)) {
|
|
6375
|
-
const reading = await readOne(
|
|
7334
|
+
const reading = await readOne(path13.join(dir, fileName2));
|
|
6376
7335
|
let kept;
|
|
6377
7336
|
if (reading.kind === "read") {
|
|
6378
7337
|
kept = reading;
|
|
@@ -6469,6 +7428,7 @@ function transportOptions(settings, timeoutMs = MAIL_TIMEOUT_MS) {
|
|
|
6469
7428
|
secure: security === "tls",
|
|
6470
7429
|
requireTLS: security === "starttls",
|
|
6471
7430
|
ignoreTLS: security === "plain",
|
|
7431
|
+
tls: { rejectUnauthorized: true },
|
|
6472
7432
|
...auth ? { auth: { user: auth.user, pass: auth.pass } } : {},
|
|
6473
7433
|
name: GREETING_NAME,
|
|
6474
7434
|
connectionTimeout: timeoutMs,
|
|
@@ -6497,7 +7457,7 @@ function failureReason(error) {
|
|
|
6497
7457
|
return "the mail server refused the email";
|
|
6498
7458
|
case "ECONNECTION":
|
|
6499
7459
|
case "ESOCKET":
|
|
6500
|
-
if (errno === "ECONNREFUSED" || errno === -61 || errno === -111) {
|
|
7460
|
+
if (errno === "ECONNREFUSED" || errno === -61 || errno === -111 || errno === -4078) {
|
|
6501
7461
|
return "nothing answered at the mail server's address";
|
|
6502
7462
|
}
|
|
6503
7463
|
return "the connection to the mail server failed";
|
|
@@ -6598,7 +7558,7 @@ function createEventStore(capacity = EVENT_CAPACITY) {
|
|
|
6598
7558
|
|
|
6599
7559
|
// src/collector/git/branchFinder.ts
|
|
6600
7560
|
import os7 from "node:os";
|
|
6601
|
-
import
|
|
7561
|
+
import path15 from "node:path";
|
|
6602
7562
|
|
|
6603
7563
|
// src/collector/git/gitHead.ts
|
|
6604
7564
|
var MAX_GIT_FILE_BYTES = 4 * 1024;
|
|
@@ -6638,18 +7598,18 @@ function parseCommonDir(content) {
|
|
|
6638
7598
|
|
|
6639
7599
|
// src/collector/git/repository.ts
|
|
6640
7600
|
import { createHash as createHash2 } from "node:crypto";
|
|
6641
|
-
import
|
|
7601
|
+
import path14 from "node:path";
|
|
6642
7602
|
var REPOSITORY_ID_LENGTH = 16;
|
|
6643
7603
|
function repositoryId(gitFolder) {
|
|
6644
7604
|
return createHash2("sha256").update(gitFolder).digest("hex").slice(0, REPOSITORY_ID_LENGTH);
|
|
6645
7605
|
}
|
|
6646
7606
|
function sharedName(common) {
|
|
6647
|
-
const base =
|
|
6648
|
-
if (base.startsWith(".")) return
|
|
7607
|
+
const base = path14.basename(common);
|
|
7608
|
+
if (base.startsWith(".")) return path14.basename(path14.dirname(common));
|
|
6649
7609
|
return base.endsWith(".git") ? base.slice(0, -".git".length) : base;
|
|
6650
7610
|
}
|
|
6651
7611
|
function repositoryOf({ dir, gitDir, commonDir }) {
|
|
6652
|
-
const name = sessionName(commonDir === null ?
|
|
7612
|
+
const name = sessionName(commonDir === null ? path14.basename(dir) : sharedName(commonDir));
|
|
6653
7613
|
return name === void 0 ? null : { id: repositoryId(commonDir ?? gitDir), name };
|
|
6654
7614
|
}
|
|
6655
7615
|
|
|
@@ -6659,32 +7619,32 @@ var MAX_LEVELS = 24;
|
|
|
6659
7619
|
var READ_WAIT_MS = 1e3;
|
|
6660
7620
|
var decoder = new TextDecoder("utf-8", { fatal: true });
|
|
6661
7621
|
async function readGitFile(io, file, maxBytes = MAX_GIT_FILE_BYTES) {
|
|
6662
|
-
let
|
|
7622
|
+
let open2;
|
|
6663
7623
|
try {
|
|
6664
|
-
|
|
7624
|
+
open2 = await io.openRegular(file);
|
|
6665
7625
|
} catch (error) {
|
|
6666
|
-
return
|
|
7626
|
+
return isMissing2(error) ? void 0 : null;
|
|
6667
7627
|
}
|
|
6668
7628
|
try {
|
|
6669
|
-
if (
|
|
6670
|
-
const bytes2 = await
|
|
7629
|
+
if (open2.info.size > maxBytes) return null;
|
|
7630
|
+
const bytes2 = await open2.read(0, maxBytes + 1);
|
|
6671
7631
|
if (bytes2.byteLength > maxBytes) return null;
|
|
6672
7632
|
return decoder.decode(bytes2);
|
|
6673
7633
|
} catch {
|
|
6674
7634
|
return null;
|
|
6675
7635
|
} finally {
|
|
6676
|
-
await
|
|
7636
|
+
await open2.close().catch(() => {
|
|
6677
7637
|
});
|
|
6678
7638
|
}
|
|
6679
7639
|
}
|
|
6680
7640
|
async function commonDirOf(io, gitDir) {
|
|
6681
|
-
const content = await readGitFile(io,
|
|
7641
|
+
const content = await readGitFile(io, path15.join(gitDir, "commondir"));
|
|
6682
7642
|
if (content === void 0) return null;
|
|
6683
7643
|
const target = content === null ? null : parseCommonDir(content);
|
|
6684
|
-
return target === null ? void 0 :
|
|
7644
|
+
return target === null ? void 0 : path15.resolve(gitDir, target);
|
|
6685
7645
|
}
|
|
6686
7646
|
async function headIn(dir, kind, io) {
|
|
6687
|
-
const marker =
|
|
7647
|
+
const marker = path15.join(dir, ".git");
|
|
6688
7648
|
let gitDir;
|
|
6689
7649
|
if (kind === "directory") {
|
|
6690
7650
|
gitDir = marker;
|
|
@@ -6692,11 +7652,11 @@ async function headIn(dir, kind, io) {
|
|
|
6692
7652
|
const content2 = await readGitFile(io, marker);
|
|
6693
7653
|
const target = typeof content2 === "string" ? parseGitFile(content2) : null;
|
|
6694
7654
|
if (target === null) return null;
|
|
6695
|
-
gitDir =
|
|
7655
|
+
gitDir = path15.resolve(dir, target);
|
|
6696
7656
|
} else {
|
|
6697
7657
|
return null;
|
|
6698
7658
|
}
|
|
6699
|
-
const content = await readGitFile(io,
|
|
7659
|
+
const content = await readGitFile(io, path15.join(gitDir, "HEAD"));
|
|
6700
7660
|
const head = typeof content === "string" ? parseHead(content) : null;
|
|
6701
7661
|
if (head === null) return null;
|
|
6702
7662
|
const commonDir = kind === "file" ? await commonDirOf(io, gitDir) : null;
|
|
@@ -6705,22 +7665,22 @@ async function headIn(dir, kind, io) {
|
|
|
6705
7665
|
return { head: { ...head, repository }, gitFolder: commonDir ?? gitDir };
|
|
6706
7666
|
}
|
|
6707
7667
|
async function findHead(cwd, { io, homeDir }) {
|
|
6708
|
-
if (!
|
|
6709
|
-
const home =
|
|
6710
|
-
let dir =
|
|
7668
|
+
if (!path15.isAbsolute(cwd)) return null;
|
|
7669
|
+
const home = path15.resolve(homeDir);
|
|
7670
|
+
let dir = path15.resolve(cwd);
|
|
6711
7671
|
try {
|
|
6712
7672
|
if ((await io.stat(dir)).kind !== "directory") return null;
|
|
6713
7673
|
} catch {
|
|
6714
7674
|
return null;
|
|
6715
7675
|
}
|
|
6716
7676
|
for (let level = 0; level < MAX_LEVELS; level += 1) {
|
|
6717
|
-
const parent =
|
|
7677
|
+
const parent = path15.dirname(dir);
|
|
6718
7678
|
if (dir === home || dir === parent) return null;
|
|
6719
7679
|
let kind;
|
|
6720
7680
|
try {
|
|
6721
|
-
kind = (await io.lstat(
|
|
7681
|
+
kind = (await io.lstat(path15.join(dir, ".git"))).kind;
|
|
6722
7682
|
} catch (error) {
|
|
6723
|
-
if (!
|
|
7683
|
+
if (!isMissing2(error)) return null;
|
|
6724
7684
|
dir = parent;
|
|
6725
7685
|
continue;
|
|
6726
7686
|
}
|
|
@@ -6801,14 +7761,14 @@ var STATUS_STATE = {
|
|
|
6801
7761
|
PENDING: "pending",
|
|
6802
7762
|
EXPECTED: "pending"
|
|
6803
7763
|
};
|
|
6804
|
-
function
|
|
7764
|
+
function isRecord11(value) {
|
|
6805
7765
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6806
7766
|
}
|
|
6807
7767
|
function textOf(value) {
|
|
6808
7768
|
return typeof value === "string" ? value : "";
|
|
6809
7769
|
}
|
|
6810
7770
|
function checkOf(value) {
|
|
6811
|
-
if (!
|
|
7771
|
+
if (!isRecord11(value)) return null;
|
|
6812
7772
|
const startedAt = Date.parse(textOf(value.startedAt));
|
|
6813
7773
|
const at = Number.isNaN(startedAt) ? -Infinity : startedAt;
|
|
6814
7774
|
if (value.__typename === "CheckRun") {
|
|
@@ -6857,7 +7817,7 @@ function readGhAnswer(stdout, repository) {
|
|
|
6857
7817
|
} catch {
|
|
6858
7818
|
return null;
|
|
6859
7819
|
}
|
|
6860
|
-
if (!
|
|
7820
|
+
if (!isRecord11(data)) return null;
|
|
6861
7821
|
const { number } = data;
|
|
6862
7822
|
const state = stateOf2(data.state, data.isDraft);
|
|
6863
7823
|
const title = typeof data.title === "string" ? sessionName(data.title) : void 0;
|
|
@@ -6881,8 +7841,9 @@ var GH_LOCATIONS = [
|
|
|
6881
7841
|
"/usr/bin/gh"
|
|
6882
7842
|
];
|
|
6883
7843
|
var GH_FIELDS = "number,title,state,isDraft,statusCheckRollup";
|
|
6884
|
-
async function findGhBinary(env, isExecutable =
|
|
6885
|
-
|
|
7844
|
+
async function findGhBinary(env, isExecutable, locations = GH_LOCATIONS, platform = process.platform) {
|
|
7845
|
+
isExecutable ??= (candidate) => isExecutableFile(candidate, platform);
|
|
7846
|
+
const candidates = [.../* @__PURE__ */ new Set([...pathCandidates(env, "gh", platform), ...locations])];
|
|
6886
7847
|
for (const candidate of candidates) {
|
|
6887
7848
|
if (await isExecutable(candidate)) return candidate;
|
|
6888
7849
|
}
|
|
@@ -6995,7 +7956,7 @@ function createGhAsker(options) {
|
|
|
6995
7956
|
}
|
|
6996
7957
|
|
|
6997
7958
|
// src/collector/git/pullRequestTarget.ts
|
|
6998
|
-
import
|
|
7959
|
+
import path16 from "node:path";
|
|
6999
7960
|
|
|
7000
7961
|
// src/collector/git/gitConfig.ts
|
|
7001
7962
|
var MAX_CONFIG_BYTES = 64 * 1024;
|
|
@@ -7253,7 +8214,7 @@ function targetOf(config, branch, base, defaultBranch) {
|
|
|
7253
8214
|
async function readPullRequestTarget(gitFolder, branch, io = nodeIo2) {
|
|
7254
8215
|
try {
|
|
7255
8216
|
if (!isAskableBranch(branch)) return null;
|
|
7256
|
-
const text4 = await readGitFile(io,
|
|
8217
|
+
const text4 = await readGitFile(io, path16.join(gitFolder, "config"), MAX_CONFIG_BYTES);
|
|
7257
8218
|
if (typeof text4 !== "string") return null;
|
|
7258
8219
|
const config = parseGitConfig(text4);
|
|
7259
8220
|
const base = config === null ? null : baseRemoteOf(config);
|
|
@@ -7262,7 +8223,7 @@ async function readPullRequestTarget(gitFolder, branch, io = nodeIo2) {
|
|
|
7262
8223
|
if (base.name !== null && isSafeRemoteName(base.name)) {
|
|
7263
8224
|
const head = await readGitFile(
|
|
7264
8225
|
io,
|
|
7265
|
-
|
|
8226
|
+
path16.join(gitFolder, "refs", "remotes", base.name, "HEAD")
|
|
7266
8227
|
);
|
|
7267
8228
|
if (typeof head === "string") defaultBranch = parseRemoteHead(head, base.name);
|
|
7268
8229
|
}
|
|
@@ -7433,40 +8394,38 @@ function createClearHistoryRoute(options) {
|
|
|
7433
8394
|
}
|
|
7434
8395
|
|
|
7435
8396
|
// src/collector/history/historyKeeper.ts
|
|
7436
|
-
import
|
|
8397
|
+
import path17 from "node:path";
|
|
7437
8398
|
|
|
7438
8399
|
// src/collector/history/historyFiles.ts
|
|
7439
8400
|
import {
|
|
7440
|
-
closeSync,
|
|
7441
|
-
constants as
|
|
7442
|
-
fstatSync,
|
|
7443
|
-
lstatSync,
|
|
7444
|
-
openSync,
|
|
8401
|
+
closeSync as closeSync2,
|
|
8402
|
+
constants as constants5,
|
|
8403
|
+
fstatSync as fstatSync2,
|
|
8404
|
+
lstatSync as lstatSync2,
|
|
7445
8405
|
readSync,
|
|
7446
8406
|
unlinkSync,
|
|
7447
8407
|
writeSync
|
|
7448
8408
|
} from "node:fs";
|
|
7449
|
-
import { lstat as
|
|
8409
|
+
import { lstat as lstat4, lutimes, mkdir as mkdir2, readdir as readdir3, unlink as unlink2 } from "node:fs/promises";
|
|
7450
8410
|
function infoOf2(info) {
|
|
7451
8411
|
return { kind: info.isFile() ? "file" : "other", size: info.size, mtimeMs: info.mtimeMs };
|
|
7452
8412
|
}
|
|
7453
8413
|
var FILE_MODE = 384;
|
|
7454
8414
|
var FOLDER_MODE = 448;
|
|
7455
|
-
var
|
|
7456
|
-
var
|
|
7457
|
-
var
|
|
7458
|
-
|
|
7459
|
-
function notOrdinary() {
|
|
8415
|
+
var READ = constants5.O_RDONLY;
|
|
8416
|
+
var APPEND = constants5.O_WRONLY | constants5.O_APPEND | constants5.O_CREAT;
|
|
8417
|
+
var CREATE = constants5.O_WRONLY | constants5.O_CREAT | constants5.O_EXCL;
|
|
8418
|
+
function notOrdinary2() {
|
|
7460
8419
|
return new Error("Not an ordinary file.");
|
|
7461
8420
|
}
|
|
7462
|
-
function
|
|
8421
|
+
function isMissing3(error) {
|
|
7463
8422
|
return error?.code === "ENOENT";
|
|
7464
8423
|
}
|
|
7465
8424
|
async function readRegular(file, maxBytes) {
|
|
7466
|
-
const handle = await
|
|
8425
|
+
const handle = await openWithoutFollowing(file, READ);
|
|
7467
8426
|
try {
|
|
7468
8427
|
const info = await handle.stat();
|
|
7469
|
-
if (!info.isFile()) throw
|
|
8428
|
+
if (!info.isFile()) throw notOrdinary2();
|
|
7470
8429
|
if (info.size > maxBytes) throw new Error("Too large to be a history file.");
|
|
7471
8430
|
const buffer = Buffer.alloc(info.size);
|
|
7472
8431
|
let filled = 0;
|
|
@@ -7481,10 +8440,10 @@ async function readRegular(file, maxBytes) {
|
|
|
7481
8440
|
}
|
|
7482
8441
|
}
|
|
7483
8442
|
function readRegularNow(file, maxBytes) {
|
|
7484
|
-
const fd =
|
|
8443
|
+
const fd = openWithoutFollowingNow(file, READ);
|
|
7485
8444
|
try {
|
|
7486
|
-
const info =
|
|
7487
|
-
if (!info.isFile()) throw
|
|
8445
|
+
const info = fstatSync2(fd);
|
|
8446
|
+
if (!info.isFile()) throw notOrdinary2();
|
|
7488
8447
|
if (info.size > maxBytes) throw new Error("Too large to be a history file.");
|
|
7489
8448
|
const buffer = Buffer.alloc(info.size);
|
|
7490
8449
|
let filled = 0;
|
|
@@ -7495,22 +8454,22 @@ function readRegularNow(file, maxBytes) {
|
|
|
7495
8454
|
}
|
|
7496
8455
|
return buffer.toString("utf8", 0, filled);
|
|
7497
8456
|
} finally {
|
|
7498
|
-
|
|
8457
|
+
closeSync2(fd);
|
|
7499
8458
|
}
|
|
7500
8459
|
}
|
|
7501
8460
|
function writeNow(file, flags, text4) {
|
|
7502
|
-
const fd =
|
|
8461
|
+
const fd = openWithoutFollowingNow(file, flags, FILE_MODE);
|
|
7503
8462
|
try {
|
|
7504
|
-
if (!
|
|
8463
|
+
if (!fstatSync2(fd).isFile()) throw notOrdinary2();
|
|
7505
8464
|
writeSync(fd, text4);
|
|
7506
8465
|
} finally {
|
|
7507
|
-
|
|
8466
|
+
closeSync2(fd);
|
|
7508
8467
|
}
|
|
7509
8468
|
}
|
|
7510
8469
|
async function writeTo(file, flags, text4) {
|
|
7511
|
-
const handle = await
|
|
8470
|
+
const handle = await openWithoutFollowing(file, flags, FILE_MODE);
|
|
7512
8471
|
try {
|
|
7513
|
-
if (!(await handle.stat()).isFile()) throw
|
|
8472
|
+
if (!(await handle.stat()).isFile()) throw notOrdinary2();
|
|
7514
8473
|
await handle.writeFile(text4, "utf8");
|
|
7515
8474
|
} finally {
|
|
7516
8475
|
await handle.close();
|
|
@@ -7521,8 +8480,8 @@ var nodeHistoryFs = {
|
|
|
7521
8480
|
await mkdir2(dir, { recursive: true, mode: FOLDER_MODE });
|
|
7522
8481
|
},
|
|
7523
8482
|
list: (dir) => readdir3(dir),
|
|
7524
|
-
lstat: async (file) => infoOf2(await
|
|
7525
|
-
lstatNow: (file) => infoOf2(
|
|
8483
|
+
lstat: async (file) => infoOf2(await lstat4(file)),
|
|
8484
|
+
lstatNow: (file) => infoOf2(lstatSync2(file)),
|
|
7526
8485
|
readRegular,
|
|
7527
8486
|
readRegularNow,
|
|
7528
8487
|
append: (file, text4) => writeTo(file, APPEND, text4),
|
|
@@ -7537,14 +8496,14 @@ var nodeHistoryFs = {
|
|
|
7537
8496
|
try {
|
|
7538
8497
|
await unlink2(file);
|
|
7539
8498
|
} catch (error) {
|
|
7540
|
-
if (!
|
|
8499
|
+
if (!isMissing3(error)) throw error;
|
|
7541
8500
|
}
|
|
7542
8501
|
},
|
|
7543
8502
|
removeNow(file) {
|
|
7544
8503
|
try {
|
|
7545
8504
|
unlinkSync(file);
|
|
7546
8505
|
} catch (error) {
|
|
7547
|
-
if (!
|
|
8506
|
+
if (!isMissing3(error)) throw error;
|
|
7548
8507
|
}
|
|
7549
8508
|
}
|
|
7550
8509
|
};
|
|
@@ -7735,7 +8694,7 @@ function createHistoryKeeper(options) {
|
|
|
7735
8694
|
});
|
|
7736
8695
|
const lock = createWriterLock({
|
|
7737
8696
|
fs,
|
|
7738
|
-
file:
|
|
8697
|
+
file: path17.join(dir, LOCK_FILE),
|
|
7739
8698
|
pid: options.pid,
|
|
7740
8699
|
now,
|
|
7741
8700
|
isAlive: options.isAlive,
|
|
@@ -7761,7 +8720,7 @@ function createHistoryKeeper(options) {
|
|
|
7761
8720
|
});
|
|
7762
8721
|
return next;
|
|
7763
8722
|
}
|
|
7764
|
-
const full = (name) =>
|
|
8723
|
+
const full = (name) => path17.join(dir, name);
|
|
7765
8724
|
function hold(record) {
|
|
7766
8725
|
if (unusable !== null || !running) return;
|
|
7767
8726
|
const line = encodeRecord(record);
|
|
@@ -8223,13 +9182,13 @@ function keptHistoryStore(store, keeper) {
|
|
|
8223
9182
|
// src/collector/history/historySettings.ts
|
|
8224
9183
|
import { createHash as createHash3 } from "node:crypto";
|
|
8225
9184
|
import os8 from "node:os";
|
|
8226
|
-
import
|
|
9185
|
+
import path18 from "node:path";
|
|
8227
9186
|
var HISTORY_ENV = "AGENT_LOOKOUT_HISTORY";
|
|
8228
9187
|
var HISTORY_DIR_ENV = "AGENT_LOOKOUT_HISTORY_DIR";
|
|
8229
9188
|
function readHistorySetup(env, homeDir = os8.homedir()) {
|
|
8230
9189
|
if (env[HISTORY_ENV]?.trim().toLowerCase() === "off") return { on: false };
|
|
8231
9190
|
const override = env[HISTORY_DIR_ENV]?.trim() || void 0;
|
|
8232
|
-
const dir =
|
|
9191
|
+
const dir = path18.resolve(override ?? path18.join(homeDir, ".agent-lookout", "history"));
|
|
8233
9192
|
return {
|
|
8234
9193
|
on: true,
|
|
8235
9194
|
dir,
|
|
@@ -8240,9 +9199,9 @@ function readHistorySetup(env, homeDir = os8.homedir()) {
|
|
|
8240
9199
|
function sourcesFingerprint(env, homeDir = os8.homedir()) {
|
|
8241
9200
|
const set = (name) => env[name]?.trim() || void 0;
|
|
8242
9201
|
const folders = [
|
|
8243
|
-
|
|
8244
|
-
|
|
8245
|
-
|
|
9202
|
+
path18.resolve(set(CLAUDE_HOME_ENV) ?? path18.join(homeDir, ".claude")),
|
|
9203
|
+
path18.resolve(set(CODEX_HOME_ENV) ?? set(CODEX_OWN_HOME_ENV) ?? path18.join(homeDir, ".codex")),
|
|
9204
|
+
path18.resolve(set(STATUS_DIR_ENV) ?? path18.join(homeDir, ".agent-lookout", "sessions"))
|
|
8246
9205
|
];
|
|
8247
9206
|
return createHash3("sha256").update(JSON.stringify(folders)).digest("hex").slice(0, 32);
|
|
8248
9207
|
}
|
|
@@ -8315,7 +9274,7 @@ function createServerNotifications(options) {
|
|
|
8315
9274
|
const at = now();
|
|
8316
9275
|
const rules = rulesOf(snapshot);
|
|
8317
9276
|
const quietNow = quietOf(snapshot);
|
|
8318
|
-
const
|
|
9277
|
+
const schedule = reminderSchedule(rules.longWait);
|
|
8319
9278
|
const result = sessionChanges(memory, snapshot);
|
|
8320
9279
|
memory = result.memory;
|
|
8321
9280
|
for (const id of result.stopped) {
|
|
@@ -8341,7 +9300,7 @@ function createServerNotifications(options) {
|
|
|
8341
9300
|
change: { event: "needs-you", session },
|
|
8342
9301
|
since: at
|
|
8343
9302
|
});
|
|
8344
|
-
if (rules.longWait.on) reminders.
|
|
9303
|
+
if (rules.longWait.on) reminders.told(session.id, at);
|
|
8345
9304
|
}
|
|
8346
9305
|
}
|
|
8347
9306
|
if (quietNow) quiet.begin(at);
|
|
@@ -8351,9 +9310,13 @@ function createServerNotifications(options) {
|
|
|
8351
9310
|
else
|
|
8352
9311
|
held.set(heldKey(change.event, change.session.id), { kind: "change", change, since: at });
|
|
8353
9312
|
}
|
|
8354
|
-
if (
|
|
8355
|
-
for (const reminder of reminders.due(snapshot, at,
|
|
8356
|
-
|
|
9313
|
+
if (rules.longWait.on && wants("needs-you")) {
|
|
9314
|
+
for (const reminder of reminders.due(snapshot, at, schedule)) {
|
|
9315
|
+
if (quietNow) {
|
|
9316
|
+
quiet.holdReminder(reminder.session, reminder.begunAt);
|
|
9317
|
+
continue;
|
|
9318
|
+
}
|
|
9319
|
+
reminders.told(reminder.session.id, at);
|
|
8357
9320
|
held.set(reminderKey(reminder.session.id), { kind: "reminder", reminder, since: at });
|
|
8358
9321
|
}
|
|
8359
9322
|
}
|
|
@@ -8362,7 +9325,7 @@ function createServerNotifications(options) {
|
|
|
8362
9325
|
if (entry.kind === "summary") continue;
|
|
8363
9326
|
const session = entry.kind === "change" ? entry.change.session : entry.reminder.session;
|
|
8364
9327
|
const current = listed.get(session.id);
|
|
8365
|
-
if (current
|
|
9328
|
+
if (current === void 0 || !needsYou(current)) continue;
|
|
8366
9329
|
if (entry.kind === "reminder") entry.reminder = { ...entry.reminder, session: current };
|
|
8367
9330
|
else if (entry.change.event === "needs-you")
|
|
8368
9331
|
entry.change = { ...entry.change, session: current };
|
|
@@ -8532,7 +9495,7 @@ var MAX_REMOTE_SOURCES = 20;
|
|
|
8532
9495
|
var MAX_ID_LENGTH = 512;
|
|
8533
9496
|
var MAX_PATH_LENGTH = 4096;
|
|
8534
9497
|
var MAX_WORD_LENGTH2 = 200;
|
|
8535
|
-
function
|
|
9498
|
+
function isRecord12(value) {
|
|
8536
9499
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8537
9500
|
}
|
|
8538
9501
|
function oneOf2(allowed, value) {
|
|
@@ -8546,7 +9509,7 @@ function exact(value, max) {
|
|
|
8546
9509
|
var COMMIT_ID2 = /^[0-9a-f]{7,64}$/;
|
|
8547
9510
|
var REPOSITORY_ID = /^[0-9a-f]{16,64}$/;
|
|
8548
9511
|
function readGit(value, machine) {
|
|
8549
|
-
if (!
|
|
9512
|
+
if (!isRecord12(value)) return void 0;
|
|
8550
9513
|
let head;
|
|
8551
9514
|
if (value.commit !== void 0) {
|
|
8552
9515
|
if (value.branch !== void 0 || typeof value.commit !== "string") return void 0;
|
|
@@ -8558,7 +9521,7 @@ function readGit(value, machine) {
|
|
|
8558
9521
|
head = { branch };
|
|
8559
9522
|
}
|
|
8560
9523
|
const repository = value.repository;
|
|
8561
|
-
if (
|
|
9524
|
+
if (isRecord12(repository) && typeof repository.id === "string") {
|
|
8562
9525
|
const name = sessionName(typeof repository.name === "string" ? repository.name : "");
|
|
8563
9526
|
if (name !== void 0 && REPOSITORY_ID.test(repository.id)) {
|
|
8564
9527
|
head.repository = { id: repositoryId(`${machine}
|
|
@@ -8568,7 +9531,7 @@ ${repository.id}`), name };
|
|
|
8568
9531
|
return head;
|
|
8569
9532
|
}
|
|
8570
9533
|
function readSession(value, machine, sources, now, keepWaitingText) {
|
|
8571
|
-
if (!
|
|
9534
|
+
if (!isRecord12(value)) return null;
|
|
8572
9535
|
const id = exact(value.id, MAX_ID_LENGTH);
|
|
8573
9536
|
const source = exact(value.source, MAX_WORD_LENGTH2);
|
|
8574
9537
|
if (id === null || source === null) return null;
|
|
@@ -8598,6 +9561,7 @@ function readSession(value, machine, sources, now, keepWaitingText) {
|
|
|
8598
9561
|
if (detail !== void 0) session.waitingDetail = detail;
|
|
8599
9562
|
const asking = keepWaitingText ? waitingText(value.waitingText) : void 0;
|
|
8600
9563
|
if (asking !== void 0) session.waitingText = asking;
|
|
9564
|
+
if (value.answered === true) session.answered = true;
|
|
8601
9565
|
}
|
|
8602
9566
|
const lastWriteAt = plausibleTime(value.lastWriteAt, now);
|
|
8603
9567
|
if (lastWriteAt !== null) session.lastWriteAt = lastWriteAt;
|
|
@@ -8607,7 +9571,7 @@ function readSession(value, machine, sources, now, keepWaitingText) {
|
|
|
8607
9571
|
return session;
|
|
8608
9572
|
}
|
|
8609
9573
|
function readCell(value) {
|
|
8610
|
-
if (!
|
|
9574
|
+
if (!isRecord12(value)) return null;
|
|
8611
9575
|
if (value.level === "yes") return { level: "yes" };
|
|
8612
9576
|
const level = oneOf2(["no", "partly"], value.level);
|
|
8613
9577
|
const reason = cut(value.reason, MAX_WORD_LENGTH2);
|
|
@@ -8615,7 +9579,7 @@ function readCell(value) {
|
|
|
8615
9579
|
}
|
|
8616
9580
|
var ACTS_HERE = ["jump", "stop", "answer"];
|
|
8617
9581
|
function readCapabilities(value, machine) {
|
|
8618
|
-
if (!
|
|
9582
|
+
if (!isRecord12(value)) return null;
|
|
8619
9583
|
const read = {};
|
|
8620
9584
|
for (const capability of CAPABILITIES) {
|
|
8621
9585
|
if (ACTS_HERE.includes(capability)) continue;
|
|
@@ -8631,7 +9595,7 @@ function readCapabilities(value, machine) {
|
|
|
8631
9595
|
};
|
|
8632
9596
|
}
|
|
8633
9597
|
function readRemoteSnapshot(value, machine, now, options = {}) {
|
|
8634
|
-
if (!
|
|
9598
|
+
if (!isRecord12(value) || !Array.isArray(value.sessions) || !Array.isArray(value.sources)) {
|
|
8635
9599
|
return null;
|
|
8636
9600
|
}
|
|
8637
9601
|
const named = /* @__PURE__ */ new Map();
|
|
@@ -8639,7 +9603,7 @@ function readRemoteSnapshot(value, machine, now, options = {}) {
|
|
|
8639
9603
|
const agents = [];
|
|
8640
9604
|
for (const item of value.sources) {
|
|
8641
9605
|
if (sources.length >= MAX_REMOTE_SOURCES) break;
|
|
8642
|
-
if (!
|
|
9606
|
+
if (!isRecord12(item)) continue;
|
|
8643
9607
|
const id = exact(item.id, MAX_WORD_LENGTH2);
|
|
8644
9608
|
if (id === null || isRemoteSource(id) || item.machine !== void 0) continue;
|
|
8645
9609
|
const label2 = cut(item.label, MAX_WORD_LENGTH2) ?? id;
|
|
@@ -8664,14 +9628,15 @@ function readRemoteSnapshot(value, machine, now, options = {}) {
|
|
|
8664
9628
|
}
|
|
8665
9629
|
|
|
8666
9630
|
// src/collector/remotes/sshProgram.ts
|
|
8667
|
-
import
|
|
9631
|
+
import path19 from "node:path";
|
|
8668
9632
|
var SSH_BIN_ENV = "AGENT_LOOKOUT_SSH_BIN";
|
|
8669
9633
|
var SSH_LOCATIONS = ["/usr/bin/ssh", "/opt/homebrew/bin/ssh", "/usr/local/bin/ssh"];
|
|
8670
9634
|
var SERVER_ALIVE_SECONDS = 15;
|
|
8671
|
-
async function findSsh(env, isExecutable =
|
|
9635
|
+
async function findSsh(env, isExecutable, platform = process.platform) {
|
|
9636
|
+
isExecutable ??= (candidate) => isExecutableFile(candidate, platform);
|
|
8672
9637
|
const named = env[SSH_BIN_ENV]?.trim();
|
|
8673
9638
|
if (named) {
|
|
8674
|
-
const resolved =
|
|
9639
|
+
const resolved = pathsOf(platform).resolve(named);
|
|
8675
9640
|
if (await isExecutable(resolved)) return { found: true, path: resolved };
|
|
8676
9641
|
return {
|
|
8677
9642
|
found: false,
|
|
@@ -8679,13 +9644,13 @@ async function findSsh(env, isExecutable = isExecutableFile) {
|
|
|
8679
9644
|
named: true
|
|
8680
9645
|
};
|
|
8681
9646
|
}
|
|
8682
|
-
const candidates = [.../* @__PURE__ */ new Set([...pathCandidates(env, "ssh"), ...SSH_LOCATIONS])];
|
|
9647
|
+
const candidates = [.../* @__PURE__ */ new Set([...pathCandidates(env, "ssh", platform), ...SSH_LOCATIONS])];
|
|
8683
9648
|
for (const candidate of candidates) {
|
|
8684
9649
|
if (await isExecutable(candidate)) return { found: true, path: candidate };
|
|
8685
9650
|
}
|
|
8686
9651
|
return {
|
|
8687
9652
|
found: false,
|
|
8688
|
-
looked: `The ssh command was not found on PATH or in ${SSH_LOCATIONS.map((at) =>
|
|
9653
|
+
looked: `The ssh command was not found on PATH or in ${SSH_LOCATIONS.map((at) => path19.dirname(at)).join(", ")}`
|
|
8689
9654
|
};
|
|
8690
9655
|
}
|
|
8691
9656
|
function sshArguments(forward) {
|
|
@@ -9268,11 +10233,11 @@ function createRemotes(options) {
|
|
|
9268
10233
|
// src/collector/settings/settingsFile.ts
|
|
9269
10234
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
9270
10235
|
import {
|
|
9271
|
-
closeSync as
|
|
9272
|
-
constants as
|
|
9273
|
-
fstatSync as
|
|
10236
|
+
closeSync as closeSync3,
|
|
10237
|
+
constants as constants6,
|
|
10238
|
+
fstatSync as fstatSync3,
|
|
9274
10239
|
fsyncSync,
|
|
9275
|
-
lstatSync as
|
|
10240
|
+
lstatSync as lstatSync3,
|
|
9276
10241
|
mkdirSync,
|
|
9277
10242
|
openSync as openSync2,
|
|
9278
10243
|
readSync as readSync2,
|
|
@@ -9281,23 +10246,22 @@ import {
|
|
|
9281
10246
|
writeSync as writeSync2
|
|
9282
10247
|
} from "node:fs";
|
|
9283
10248
|
import os9 from "node:os";
|
|
9284
|
-
import
|
|
10249
|
+
import path20 from "node:path";
|
|
9285
10250
|
var SETTINGS_FILE_ENV = "AGENT_LOOKOUT_SETTINGS_FILE";
|
|
9286
10251
|
var SETTINGS_FILE_MODE = 384;
|
|
9287
10252
|
var SETTINGS_FOLDER_MODE = 448;
|
|
9288
10253
|
var MAX_SETTINGS_BYTES = 64 * 1024;
|
|
9289
10254
|
function readSettingsSetup(env, homeDir = os9.homedir()) {
|
|
9290
10255
|
const named = env[SETTINGS_FILE_ENV]?.trim() || void 0;
|
|
9291
|
-
const file =
|
|
10256
|
+
const file = path20.resolve(named ?? path20.join(homeDir, ".agent-lookout", "settings.json"));
|
|
9292
10257
|
return { file, shown: tildify(file, homeDir) };
|
|
9293
10258
|
}
|
|
9294
|
-
var NO_FOLLOW2 = (constants5.O_NOFOLLOW ?? 0) | (constants5.O_NONBLOCK ?? 0);
|
|
9295
10259
|
function codeOf(error) {
|
|
9296
10260
|
return error?.code;
|
|
9297
10261
|
}
|
|
9298
10262
|
function kindAt(target) {
|
|
9299
10263
|
try {
|
|
9300
|
-
const info =
|
|
10264
|
+
const info = lstatSync3(target);
|
|
9301
10265
|
if (info.isSymbolicLink()) return "link";
|
|
9302
10266
|
if (info.isFile()) return "file";
|
|
9303
10267
|
return info.isDirectory() ? "folder" : "other";
|
|
@@ -9309,54 +10273,69 @@ function kindAt(target) {
|
|
|
9309
10273
|
function syncFolder(folder) {
|
|
9310
10274
|
let fd = null;
|
|
9311
10275
|
try {
|
|
9312
|
-
fd = openSync2(folder,
|
|
10276
|
+
fd = openSync2(folder, constants6.O_RDONLY);
|
|
9313
10277
|
fsyncSync(fd);
|
|
9314
10278
|
} catch {
|
|
9315
10279
|
} finally {
|
|
9316
|
-
if (fd !== null)
|
|
10280
|
+
if (fd !== null) closeSync3(fd);
|
|
10281
|
+
}
|
|
10282
|
+
}
|
|
10283
|
+
var linkRefused = (shown) => ({
|
|
10284
|
+
kind: "refused",
|
|
10285
|
+
problem: `${shown} is a link, which Agent Lookout does not follow, so the time rules and the permission rules are off.`
|
|
10286
|
+
});
|
|
10287
|
+
var notOrdinary3 = (shown) => ({
|
|
10288
|
+
kind: "refused",
|
|
10289
|
+
problem: `${shown} is not an ordinary file, so the time rules and the permission rules are off.`
|
|
10290
|
+
});
|
|
10291
|
+
function readSettingsStamp(setup) {
|
|
10292
|
+
try {
|
|
10293
|
+
const info = lstatSync3(setup.file, { bigint: true });
|
|
10294
|
+
return `${info.ino}:${info.size}:${info.mtimeNs}:${info.ctimeNs}`;
|
|
10295
|
+
} catch {
|
|
10296
|
+
return null;
|
|
9317
10297
|
}
|
|
9318
10298
|
}
|
|
9319
10299
|
function readSettingsText(setup) {
|
|
9320
10300
|
const { file, shown } = setup;
|
|
9321
|
-
const folder =
|
|
10301
|
+
const folder = path20.dirname(file);
|
|
9322
10302
|
try {
|
|
9323
10303
|
const folderKind = kindAt(folder);
|
|
9324
10304
|
if (folderKind === null) return { kind: "missing" };
|
|
9325
10305
|
if (folderKind !== "folder") {
|
|
9326
10306
|
return {
|
|
9327
10307
|
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.`
|
|
10308
|
+
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
10309
|
};
|
|
9330
10310
|
}
|
|
10311
|
+
const fileKind = kindAt(file);
|
|
10312
|
+
if (fileKind === "link") return linkRefused(shown);
|
|
10313
|
+
if (fileKind === "folder" || fileKind === "other") return notOrdinary3(shown);
|
|
9331
10314
|
} catch {
|
|
9332
|
-
return {
|
|
10315
|
+
return {
|
|
10316
|
+
kind: "refused",
|
|
10317
|
+
problem: `${shown} could not be read, so the time rules and the permission rules are off.`
|
|
10318
|
+
};
|
|
9333
10319
|
}
|
|
9334
10320
|
let fd;
|
|
9335
10321
|
try {
|
|
9336
|
-
fd =
|
|
10322
|
+
fd = openWithoutFollowingNow(file, constants6.O_RDONLY);
|
|
9337
10323
|
} catch (error) {
|
|
9338
10324
|
const code = codeOf(error);
|
|
9339
10325
|
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.` };
|
|
10326
|
+
if (code === "ELOOP") return linkRefused(shown);
|
|
10327
|
+
return {
|
|
10328
|
+
kind: "refused",
|
|
10329
|
+
problem: `${shown} could not be read, so the time rules and the permission rules are off.`
|
|
10330
|
+
};
|
|
9347
10331
|
}
|
|
9348
10332
|
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
|
-
}
|
|
10333
|
+
const info = fstatSync3(fd);
|
|
10334
|
+
if (!info.isFile()) return notOrdinary3(shown);
|
|
9356
10335
|
if (info.size > MAX_SETTINGS_BYTES) {
|
|
9357
10336
|
return {
|
|
9358
10337
|
kind: "refused",
|
|
9359
|
-
problem: `${shown} is larger than 64 KB, so it was not read and the time rules are off.`
|
|
10338
|
+
problem: `${shown} is larger than 64 KB, so it was not read and the time rules and the permission rules are off.`
|
|
9360
10339
|
};
|
|
9361
10340
|
}
|
|
9362
10341
|
const buffer = Buffer.alloc(info.size);
|
|
@@ -9368,14 +10347,17 @@ function readSettingsText(setup) {
|
|
|
9368
10347
|
}
|
|
9369
10348
|
return { kind: "read", text: buffer.toString("utf8", 0, filled) };
|
|
9370
10349
|
} catch {
|
|
9371
|
-
return {
|
|
10350
|
+
return {
|
|
10351
|
+
kind: "refused",
|
|
10352
|
+
problem: `${shown} could not be read, so the time rules and the permission rules are off.`
|
|
10353
|
+
};
|
|
9372
10354
|
} finally {
|
|
9373
|
-
|
|
10355
|
+
closeSync3(fd);
|
|
9374
10356
|
}
|
|
9375
10357
|
}
|
|
9376
10358
|
function writeSettingsText(setup, text4) {
|
|
9377
10359
|
const { file, shown } = setup;
|
|
9378
|
-
const folder =
|
|
10360
|
+
const folder = path20.dirname(file);
|
|
9379
10361
|
const notSaved = {
|
|
9380
10362
|
ok: false,
|
|
9381
10363
|
problem: `${shown} could not be written, so the change was not saved.`
|
|
@@ -9396,20 +10378,20 @@ function writeSettingsText(setup, text4) {
|
|
|
9396
10378
|
problem: `${shown} is a link or not an ordinary file, which Agent Lookout does not write through, so the change was not saved.`
|
|
9397
10379
|
};
|
|
9398
10380
|
}
|
|
9399
|
-
temporary =
|
|
10381
|
+
temporary = path20.join(
|
|
9400
10382
|
folder,
|
|
9401
|
-
`.${
|
|
10383
|
+
`.${path20.basename(file)}.${process.pid}.${randomBytes3(6).toString("hex")}.tmp`
|
|
9402
10384
|
);
|
|
9403
|
-
const fd =
|
|
10385
|
+
const fd = openWithoutFollowingNow(
|
|
9404
10386
|
temporary,
|
|
9405
|
-
|
|
10387
|
+
constants6.O_WRONLY | constants6.O_CREAT | constants6.O_EXCL,
|
|
9406
10388
|
SETTINGS_FILE_MODE
|
|
9407
10389
|
);
|
|
9408
10390
|
try {
|
|
9409
10391
|
writeSync2(fd, text4);
|
|
9410
10392
|
fsyncSync(fd);
|
|
9411
10393
|
} finally {
|
|
9412
|
-
|
|
10394
|
+
closeSync3(fd);
|
|
9413
10395
|
}
|
|
9414
10396
|
renameSync(temporary, file);
|
|
9415
10397
|
temporary = null;
|
|
@@ -9430,14 +10412,15 @@ function writeSettingsText(setup, text4) {
|
|
|
9430
10412
|
// src/collector/settings/collectorSettings.ts
|
|
9431
10413
|
var fileSettingsStore = {
|
|
9432
10414
|
read: readSettingsText,
|
|
9433
|
-
write: writeSettingsText
|
|
10415
|
+
write: writeSettingsText,
|
|
10416
|
+
stamp: readSettingsStamp
|
|
9434
10417
|
};
|
|
9435
10418
|
var RULE_WORDS = {
|
|
9436
10419
|
longWait: "the long wait reminder",
|
|
9437
10420
|
idle: "the idle rule",
|
|
9438
10421
|
quietHours: "the quiet hours"
|
|
9439
10422
|
};
|
|
9440
|
-
function
|
|
10423
|
+
function isRecord13(value) {
|
|
9441
10424
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9442
10425
|
}
|
|
9443
10426
|
function listOf(names) {
|
|
@@ -9446,11 +10429,28 @@ function listOf(names) {
|
|
|
9446
10429
|
}
|
|
9447
10430
|
function rulesIn(read, shown) {
|
|
9448
10431
|
const off = readTimeRules(void 0).rules;
|
|
10432
|
+
const none = { permissionRules: [], heldRules: void 0 };
|
|
9449
10433
|
if (read.kind === "missing") {
|
|
9450
|
-
return {
|
|
10434
|
+
return {
|
|
10435
|
+
rules: off,
|
|
10436
|
+
...none,
|
|
10437
|
+
rest: {},
|
|
10438
|
+
otherRules: {},
|
|
10439
|
+
problem: null,
|
|
10440
|
+
rulesProblem: null,
|
|
10441
|
+
writable: true
|
|
10442
|
+
};
|
|
9451
10443
|
}
|
|
9452
10444
|
if (read.kind === "refused") {
|
|
9453
|
-
return {
|
|
10445
|
+
return {
|
|
10446
|
+
rules: off,
|
|
10447
|
+
...none,
|
|
10448
|
+
rest: {},
|
|
10449
|
+
otherRules: {},
|
|
10450
|
+
problem: read.problem,
|
|
10451
|
+
rulesProblem: read.problem,
|
|
10452
|
+
writable: false
|
|
10453
|
+
};
|
|
9454
10454
|
}
|
|
9455
10455
|
let value;
|
|
9456
10456
|
try {
|
|
@@ -9458,60 +10458,252 @@ function rulesIn(read, shown) {
|
|
|
9458
10458
|
} catch {
|
|
9459
10459
|
value = null;
|
|
9460
10460
|
}
|
|
9461
|
-
if (!
|
|
10461
|
+
if (!isRecord13(value)) {
|
|
10462
|
+
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
10463
|
return {
|
|
9463
10464
|
rules: off,
|
|
10465
|
+
...none,
|
|
9464
10466
|
rest: {},
|
|
9465
10467
|
otherRules: {},
|
|
9466
|
-
problem:
|
|
10468
|
+
problem: notJson,
|
|
10469
|
+
rulesProblem: notJson,
|
|
9467
10470
|
writable: true
|
|
9468
10471
|
};
|
|
9469
10472
|
}
|
|
9470
|
-
const { timeRules, ...rest } = value;
|
|
10473
|
+
const { timeRules, permissionRules: heldRules, ...rest } = value;
|
|
10474
|
+
const permissions = readPermissionRules(heldRules);
|
|
9471
10475
|
const { rules, unread } = readTimeRules(timeRules);
|
|
9472
|
-
const otherRules =
|
|
10476
|
+
const otherRules = isRecord13(timeRules) ? Object.fromEntries(
|
|
9473
10477
|
Object.entries(timeRules).filter(
|
|
9474
10478
|
([name]) => !TIME_RULE_NAMES.includes(name)
|
|
9475
10479
|
)
|
|
9476
10480
|
) : {};
|
|
9477
10481
|
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 {
|
|
10482
|
+
return {
|
|
10483
|
+
rules,
|
|
10484
|
+
permissionRules: permissions.ok ? permissions.rules : null,
|
|
10485
|
+
heldRules,
|
|
10486
|
+
rest,
|
|
10487
|
+
otherRules,
|
|
10488
|
+
problem,
|
|
10489
|
+
rulesProblem: permissions.ok ? null : unreadRulesLine(shown),
|
|
10490
|
+
writable: true
|
|
10491
|
+
};
|
|
10492
|
+
}
|
|
10493
|
+
function unreadRulesLine(shown) {
|
|
10494
|
+
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
10495
|
}
|
|
9480
10496
|
function createCollectorSettings(options) {
|
|
9481
10497
|
const { setup } = options;
|
|
9482
10498
|
const store = options.store ?? fileSettingsStore;
|
|
9483
|
-
|
|
9484
|
-
let
|
|
9485
|
-
const
|
|
9486
|
-
let
|
|
10499
|
+
let stamp = store.stamp(setup);
|
|
10500
|
+
let held = rulesIn(store.read(setup), setup.shown);
|
|
10501
|
+
const first = held;
|
|
10502
|
+
let rules = held.rules;
|
|
10503
|
+
let problem = held.problem;
|
|
10504
|
+
let permissionRules = held.permissionRules ?? [];
|
|
10505
|
+
let keepHeld = held.permissionRules === null;
|
|
10506
|
+
let rulesProblem = held.rulesProblem;
|
|
10507
|
+
function read() {
|
|
10508
|
+
stamp = store.stamp(setup);
|
|
10509
|
+
held = rulesIn(store.read(setup), setup.shown);
|
|
10510
|
+
rules = held.rules;
|
|
10511
|
+
problem = held.problem;
|
|
10512
|
+
permissionRules = held.permissionRules ?? [];
|
|
10513
|
+
keepHeld = held.permissionRules === null;
|
|
10514
|
+
rulesProblem = held.rulesProblem;
|
|
10515
|
+
}
|
|
10516
|
+
const notWritable = () => ({
|
|
10517
|
+
ok: false,
|
|
10518
|
+
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.`
|
|
10519
|
+
});
|
|
10520
|
+
function textWith(time, permissions, keep) {
|
|
10521
|
+
const listed = keep ? { permissionRules: held.heldRules } : permissions.length > 0 || held.heldRules !== void 0 ? { permissionRules: permissions } : {};
|
|
10522
|
+
const whole = { ...held.rest, timeRules: { ...held.otherRules, ...time }, ...listed };
|
|
10523
|
+
return `${JSON.stringify(whole, null, 2)}
|
|
10524
|
+
`;
|
|
10525
|
+
}
|
|
9487
10526
|
return {
|
|
9488
10527
|
timeRules: () => rules,
|
|
9489
|
-
|
|
10528
|
+
permissionRules: () => permissionRules,
|
|
10529
|
+
status: () => ({
|
|
10530
|
+
timeRules: rules,
|
|
10531
|
+
file: setup.shown,
|
|
10532
|
+
problem,
|
|
10533
|
+
permissionRules: [...permissionRules],
|
|
10534
|
+
permissionRulesProblem: rulesProblem
|
|
10535
|
+
}),
|
|
10536
|
+
readAgain() {
|
|
10537
|
+
if (store.stamp(setup) !== stamp) read();
|
|
10538
|
+
},
|
|
9490
10539
|
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);
|
|
10540
|
+
read();
|
|
10541
|
+
if (!held.writable) return notWritable();
|
|
10542
|
+
const written = store.write(setup, textWith(next, permissionRules, keepHeld));
|
|
9500
10543
|
if (!written.ok) {
|
|
9501
10544
|
problem = written.problem;
|
|
9502
10545
|
return written;
|
|
9503
10546
|
}
|
|
9504
10547
|
rules = next;
|
|
9505
10548
|
problem = null;
|
|
10549
|
+
if (!keepHeld) rulesProblem = null;
|
|
9506
10550
|
return written;
|
|
9507
10551
|
},
|
|
9508
|
-
|
|
10552
|
+
changePermissionRules(change) {
|
|
10553
|
+
read();
|
|
10554
|
+
if (!held.writable) return { reason: "not-saved", ...notWritable() };
|
|
10555
|
+
const made = change(permissionRules);
|
|
10556
|
+
if (!made.ok || !made.changed) return made;
|
|
10557
|
+
const written = store.write(setup, textWith(rules, made.rules, false));
|
|
10558
|
+
if (!written.ok) {
|
|
10559
|
+
rulesProblem = written.problem;
|
|
10560
|
+
return { ok: false, reason: "not-saved", problem: written.problem };
|
|
10561
|
+
}
|
|
10562
|
+
permissionRules = [...made.rules];
|
|
10563
|
+
keepHeld = false;
|
|
10564
|
+
rulesProblem = null;
|
|
10565
|
+
problem = null;
|
|
10566
|
+
return made;
|
|
10567
|
+
},
|
|
10568
|
+
problemAtStart: first.problem,
|
|
10569
|
+
rulesProblemAtStart: first.rulesProblem === first.problem ? null : first.rulesProblem
|
|
10570
|
+
};
|
|
10571
|
+
}
|
|
10572
|
+
|
|
10573
|
+
// src/collector/settings/permissionRulesRoute.ts
|
|
10574
|
+
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
10575
|
+
|
|
10576
|
+
// src/core/permission-rules/rulesChange.ts
|
|
10577
|
+
var CHANGE_SHAPE = 'The body must be one change: {"add": rule}, {"edit": rule with its "id"}, {"move": {"id", "to": "up" or "down"}} or {"remove": {"id"}}, and nothing else.';
|
|
10578
|
+
function isRecord14(value) {
|
|
10579
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10580
|
+
}
|
|
10581
|
+
function rulesChangeIn(value) {
|
|
10582
|
+
const refused = { ok: false, problem: CHANGE_SHAPE };
|
|
10583
|
+
if (!isRecord14(value) || Object.keys(value).length !== 1) return refused;
|
|
10584
|
+
const [kind] = Object.keys(value);
|
|
10585
|
+
const body = value[kind];
|
|
10586
|
+
if (!isRecord14(body)) return refused;
|
|
10587
|
+
switch (kind) {
|
|
10588
|
+
case "add": {
|
|
10589
|
+
const read = ruleWordsIn(body);
|
|
10590
|
+
return read.ok ? { ok: true, change: { kind, words: read.words } } : read;
|
|
10591
|
+
}
|
|
10592
|
+
case "edit": {
|
|
10593
|
+
const { id, ...words } = body;
|
|
10594
|
+
if (!isRuleId(id)) return refused;
|
|
10595
|
+
const read = ruleWordsIn(words);
|
|
10596
|
+
return read.ok ? { ok: true, change: { kind, id, words: read.words } } : read;
|
|
10597
|
+
}
|
|
10598
|
+
case "move": {
|
|
10599
|
+
const { id, to } = body;
|
|
10600
|
+
if (Object.keys(body).length !== 2 || !isRuleId(id)) return refused;
|
|
10601
|
+
if (to !== "up" && to !== "down") return refused;
|
|
10602
|
+
return { ok: true, change: { kind, id, to } };
|
|
10603
|
+
}
|
|
10604
|
+
case "remove": {
|
|
10605
|
+
const { id } = body;
|
|
10606
|
+
if (Object.keys(body).length !== 1 || !isRuleId(id)) return refused;
|
|
10607
|
+
return { ok: true, change: { kind, id } };
|
|
10608
|
+
}
|
|
10609
|
+
default:
|
|
10610
|
+
return refused;
|
|
10611
|
+
}
|
|
10612
|
+
}
|
|
10613
|
+
var NO_RULE = {
|
|
10614
|
+
ok: false,
|
|
10615
|
+
reason: "no-rule",
|
|
10616
|
+
problem: "The list holds no such rule. It may have been changed in another tab."
|
|
10617
|
+
};
|
|
10618
|
+
var DUPLICATE = {
|
|
10619
|
+
ok: false,
|
|
10620
|
+
reason: "duplicate",
|
|
10621
|
+
problem: "The list holds that rule already."
|
|
10622
|
+
};
|
|
10623
|
+
function applyRulesChange(rules, change, newId) {
|
|
10624
|
+
if (change.kind === "add") {
|
|
10625
|
+
if (rules.length >= MAX_RULES) {
|
|
10626
|
+
return {
|
|
10627
|
+
ok: false,
|
|
10628
|
+
reason: "full",
|
|
10629
|
+
problem: `The list holds ${MAX_RULES} rules, the most it can. Remove one to add another.`
|
|
10630
|
+
};
|
|
10631
|
+
}
|
|
10632
|
+
if (rules.some((rule2) => sameWords(rule2, change.words))) return DUPLICATE;
|
|
10633
|
+
let id = newId();
|
|
10634
|
+
while (rules.some((rule2) => rule2.id === id)) id = newId();
|
|
10635
|
+
return { ok: true, rules: [...rules, { id, ...change.words }], changed: true };
|
|
10636
|
+
}
|
|
10637
|
+
const at = rules.findIndex((rule2) => rule2.id === change.id);
|
|
10638
|
+
if (at < 0) return NO_RULE;
|
|
10639
|
+
const rule = rules[at];
|
|
10640
|
+
if (change.kind === "remove") {
|
|
10641
|
+
return { ok: true, rules: rules.filter((_, index) => index !== at), changed: true };
|
|
10642
|
+
}
|
|
10643
|
+
if (change.kind === "edit") {
|
|
10644
|
+
if (sameWords(rule, change.words)) return { ok: true, rules: [...rules], changed: false };
|
|
10645
|
+
if (rules.some((other) => other.id !== rule.id && sameWords(other, change.words))) {
|
|
10646
|
+
return DUPLICATE;
|
|
10647
|
+
}
|
|
10648
|
+
const next2 = [...rules];
|
|
10649
|
+
next2[at] = { id: rule.id, ...change.words };
|
|
10650
|
+
return { ok: true, rules: next2, changed: true };
|
|
10651
|
+
}
|
|
10652
|
+
const to = change.to === "up" ? at - 1 : at + 1;
|
|
10653
|
+
if (to < 0 || to >= rules.length) return { ok: true, rules: [...rules], changed: false };
|
|
10654
|
+
const next = [...rules];
|
|
10655
|
+
next[at] = next[to];
|
|
10656
|
+
next[to] = rule;
|
|
10657
|
+
return { ok: true, rules: next, changed: true };
|
|
10658
|
+
}
|
|
10659
|
+
|
|
10660
|
+
// src/collector/settings/permissionRulesRoute.ts
|
|
10661
|
+
var MAX_PERMISSION_RULES_BODY_BYTES = 1024;
|
|
10662
|
+
var failed3 = (status, reason, error) => ({
|
|
10663
|
+
status,
|
|
10664
|
+
body: { error, reason }
|
|
10665
|
+
});
|
|
10666
|
+
var STATUS_OF = { "no-rule": 404, full: 409, duplicate: 409, "not-saved": 500 };
|
|
10667
|
+
function permissionRulesRefusalFor(req) {
|
|
10668
|
+
return actionRefusalFor(req, PERMISSION_RULES_ACTION, MAX_PERMISSION_RULES_BODY_BYTES);
|
|
10669
|
+
}
|
|
10670
|
+
var newRuleId = () => randomBytes4(6).toString("hex");
|
|
10671
|
+
function createPermissionRulesRoute(options) {
|
|
10672
|
+
const { settings } = options;
|
|
10673
|
+
const newId = options.newId ?? newRuleId;
|
|
10674
|
+
return async function answerPermissionRules(req) {
|
|
10675
|
+
const refused = permissionRulesRefusalFor(req);
|
|
10676
|
+
if (refused) {
|
|
10677
|
+
req.resume();
|
|
10678
|
+
return refused;
|
|
10679
|
+
}
|
|
10680
|
+
const body = await readRequestBody(req, MAX_PERMISSION_RULES_BODY_BYTES);
|
|
10681
|
+
if (!body.ok) {
|
|
10682
|
+
req.resume();
|
|
10683
|
+
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.");
|
|
10684
|
+
}
|
|
10685
|
+
let value;
|
|
10686
|
+
try {
|
|
10687
|
+
value = JSON.parse(body.text);
|
|
10688
|
+
} catch {
|
|
10689
|
+
return failed3(400, "invalid", "The body must be JSON: one change to the permission rules.");
|
|
10690
|
+
}
|
|
10691
|
+
const asked = rulesChangeIn(value);
|
|
10692
|
+
if (!asked.ok) return failed3(400, "invalid", asked.problem);
|
|
10693
|
+
const made = settings.changePermissionRules(
|
|
10694
|
+
(rules) => applyRulesChange(rules, asked.change, newId)
|
|
10695
|
+
);
|
|
10696
|
+
if (!made.ok) return failed3(STATUS_OF[made.reason], made.reason, made.problem);
|
|
10697
|
+
return {
|
|
10698
|
+
status: 200,
|
|
10699
|
+
body: { ok: true, permissionRules: made.rules }
|
|
10700
|
+
};
|
|
9509
10701
|
};
|
|
9510
10702
|
}
|
|
9511
10703
|
|
|
9512
10704
|
// src/collector/settings/timeRulesRoute.ts
|
|
9513
10705
|
var MAX_TIME_RULES_BODY_BYTES = 2048;
|
|
9514
|
-
var
|
|
10706
|
+
var failed4 = (status, reason, error) => ({
|
|
9515
10707
|
status,
|
|
9516
10708
|
body: { error, reason }
|
|
9517
10709
|
});
|
|
@@ -9535,12 +10727,12 @@ function createTimeRulesRoute(options) {
|
|
|
9535
10727
|
try {
|
|
9536
10728
|
value = JSON.parse(body.text);
|
|
9537
10729
|
} catch {
|
|
9538
|
-
return
|
|
10730
|
+
return failed4(400, "invalid", "The body must be JSON: the three time rules.");
|
|
9539
10731
|
}
|
|
9540
10732
|
const asked = timeRulesIn(value);
|
|
9541
|
-
if (!asked.ok) return
|
|
10733
|
+
if (!asked.ok) return failed4(400, "invalid", asked.problem);
|
|
9542
10734
|
const saved = settings.changeTimeRules(asked.rules);
|
|
9543
|
-
if (!saved.ok) return
|
|
10735
|
+
if (!saved.ok) return failed4(500, "not-saved", saved.problem);
|
|
9544
10736
|
return {
|
|
9545
10737
|
status: 200,
|
|
9546
10738
|
body: { ok: true, timeRules: asked.rules }
|
|
@@ -9692,8 +10884,9 @@ var TMUX_LOCATIONS = [
|
|
|
9692
10884
|
"/opt/local/bin/tmux",
|
|
9693
10885
|
"/usr/bin/tmux"
|
|
9694
10886
|
];
|
|
9695
|
-
async function findTmuxBinary(env, isExecutable =
|
|
9696
|
-
|
|
10887
|
+
async function findTmuxBinary(env, isExecutable, platform = process.platform) {
|
|
10888
|
+
isExecutable ??= (candidate) => isExecutableFile(candidate, platform);
|
|
10889
|
+
const candidates = [.../* @__PURE__ */ new Set([...pathCandidates(env, "tmux", platform), ...TMUX_LOCATIONS])];
|
|
9697
10890
|
for (const candidate of candidates) {
|
|
9698
10891
|
if (await isExecutable(candidate)) return candidate;
|
|
9699
10892
|
}
|
|
@@ -9727,9 +10920,10 @@ function runTmuxBinary(binary, args, options) {
|
|
|
9727
10920
|
function createTmuxRunner(options) {
|
|
9728
10921
|
const { env } = options;
|
|
9729
10922
|
const off = env[TMUX_ENV]?.trim().toLowerCase() === "off";
|
|
10923
|
+
const platform = options.platform ?? process.platform;
|
|
9730
10924
|
return async (args) => {
|
|
9731
|
-
if (off ||
|
|
9732
|
-
const binary = await findTmuxBinary(env, options.isExecutable);
|
|
10925
|
+
if (off || platform === "win32") return NOT_RUN3;
|
|
10926
|
+
const binary = await findTmuxBinary(env, options.isExecutable, platform);
|
|
9733
10927
|
if (binary === null) return NOT_RUN3;
|
|
9734
10928
|
return runTmuxBinary(binary, args, { env, timeoutMs: options.timeoutMs });
|
|
9735
10929
|
};
|
|
@@ -9782,7 +10976,8 @@ function createWaitLedger() {
|
|
|
9782
10976
|
events,
|
|
9783
10977
|
measured: spans,
|
|
9784
10978
|
runStarts,
|
|
9785
|
-
|
|
10979
|
+
// Not a session in a wait Agent Lookout answered, whatever its source still says.
|
|
10980
|
+
waitingNow: snapshot.sessions.filter(needsYou).map((session) => ({ id: session.id, name: session.name })),
|
|
9786
10981
|
names: new Map(snapshot.sessions.map((session) => [session.id, session.name])),
|
|
9787
10982
|
since: since.at,
|
|
9788
10983
|
now
|
|
@@ -9973,11 +11168,13 @@ function createCollector(options) {
|
|
|
9973
11168
|
const env = options.env ?? process.env;
|
|
9974
11169
|
const intervalMs = options.intervalMs ?? POLL_INTERVAL_MS;
|
|
9975
11170
|
const warn = options.warn ?? console.error;
|
|
11171
|
+
const settingsSetup = readSettingsSetup(env);
|
|
9976
11172
|
const settings = createCollectorSettings({
|
|
9977
|
-
setup:
|
|
11173
|
+
setup: settingsSetup,
|
|
9978
11174
|
store: options.settingsStore
|
|
9979
11175
|
});
|
|
9980
11176
|
if (settings.problemAtStart !== null) warn(settings.problemAtStart);
|
|
11177
|
+
if (settings.rulesProblemAtStart !== null) warn(settings.rulesProblemAtStart);
|
|
9981
11178
|
const memoryEvents = createEventStore();
|
|
9982
11179
|
const memoryHistory = createHistoryStore();
|
|
9983
11180
|
const historySetup = readHistorySetup(env);
|
|
@@ -10008,9 +11205,9 @@ function createCollector(options) {
|
|
|
10008
11205
|
});
|
|
10009
11206
|
const tabs = createTabFinder({ env, readProcesses: options.readProcesses, now });
|
|
10010
11207
|
const osascript = options.osascript ?? createOsascriptRunner({ env });
|
|
10011
|
-
const stopsOn = !stopOff(env);
|
|
10012
|
-
const stopTargets = stopsOn ? createStopTargets() : void 0;
|
|
10013
11208
|
const platform = options.platform ?? process.platform;
|
|
11209
|
+
const stopsOn = !stopOff(env) && platform !== "win32";
|
|
11210
|
+
const stopTargets = stopsOn ? createStopTargets() : void 0;
|
|
10014
11211
|
const notifications = createServerNotifications({
|
|
10015
11212
|
notifier: options.notifier ?? createSystemNotifier({ platform }),
|
|
10016
11213
|
onAtStart: notificationsOnAtStart(env),
|
|
@@ -10071,7 +11268,8 @@ function createCollector(options) {
|
|
|
10071
11268
|
panes,
|
|
10072
11269
|
terminals: tabs,
|
|
10073
11270
|
readProcessStarts: options.readProcessStarts,
|
|
10074
|
-
stops: stopTargets
|
|
11271
|
+
stops: stopTargets,
|
|
11272
|
+
platform
|
|
10075
11273
|
}),
|
|
10076
11274
|
createCodexAdapter({ env: options.env, now, pollIntervalMs: intervalMs }),
|
|
10077
11275
|
createStatusFileAdapter({ env: options.env, now, pollIntervalMs: intervalMs })
|
|
@@ -10087,7 +11285,17 @@ function createCollector(options) {
|
|
|
10087
11285
|
// Every source's sessions alike are given the branch of their folder, and
|
|
10088
11286
|
// with pull requests on, the branch's pull request, as last learnt.
|
|
10089
11287
|
// Every snapshot carries the time rules in force, and is stale by them.
|
|
10090
|
-
|
|
11288
|
+
// The settings file is looked at again here, once a poll, so a change
|
|
11289
|
+
// another copy of Agent Lookout saved is in force by the next one.
|
|
11290
|
+
timeRules: () => {
|
|
11291
|
+
settings.readAgain();
|
|
11292
|
+
return settings.timeRules();
|
|
11293
|
+
},
|
|
11294
|
+
// And marks each session still in a wait whose permission request was
|
|
11295
|
+
// answered, so every channel, the page's too, takes that wait as over at
|
|
11296
|
+
// the same poll, and the Events log and the waits never count a wait a
|
|
11297
|
+
// rule answered before a poll saw it.
|
|
11298
|
+
answered: (snapshot) => answering?.answeredIn(snapshot) ?? /* @__PURE__ */ new Set(),
|
|
10091
11299
|
annotate: async (sessions) => {
|
|
10092
11300
|
const onBranches = await branches.annotate(sessions);
|
|
10093
11301
|
return pullRequests ? pullRequests.annotate(onBranches) : onBranches;
|
|
@@ -10103,7 +11311,18 @@ function createCollector(options) {
|
|
|
10103
11311
|
}
|
|
10104
11312
|
});
|
|
10105
11313
|
const answers2 = options.adapters === void 0 || options.answering !== void 0;
|
|
10106
|
-
const answering = answers2 ? createAnswering({
|
|
11314
|
+
const answering = answers2 ? createAnswering({
|
|
11315
|
+
env,
|
|
11316
|
+
poller,
|
|
11317
|
+
events,
|
|
11318
|
+
now,
|
|
11319
|
+
warn,
|
|
11320
|
+
platform,
|
|
11321
|
+
// Read for each request, so a change in Settings holds for the next one.
|
|
11322
|
+
rules: () => settings.permissionRules(),
|
|
11323
|
+
settingsFile: settingsSetup.file,
|
|
11324
|
+
...options.answering
|
|
11325
|
+
}) : null;
|
|
10107
11326
|
const stopper = createStopper({
|
|
10108
11327
|
env,
|
|
10109
11328
|
now,
|
|
@@ -10133,8 +11352,13 @@ function createCollector(options) {
|
|
|
10133
11352
|
jump: createJumpRoute({ poller, panes, run: tmux, tabs, osascript, now }),
|
|
10134
11353
|
stop: stopRoutes && createStopRoute(stopRoutes),
|
|
10135
11354
|
cleanUp: stopRoutes && createCleanUpRoute(stopRoutes),
|
|
10136
|
-
settings: () =>
|
|
11355
|
+
settings: () => ({
|
|
11356
|
+
...settings.status(),
|
|
11357
|
+
ruleAnswers: answering?.ruleAnswers() ?? [],
|
|
11358
|
+
ruleAnswersSince: poller.startedAt
|
|
11359
|
+
}),
|
|
10137
11360
|
timeRules: createTimeRulesRoute({ settings }),
|
|
11361
|
+
permissionRules: createPermissionRulesRoute({ settings }),
|
|
10138
11362
|
clearHistory: createClearHistoryRoute({
|
|
10139
11363
|
keeper,
|
|
10140
11364
|
forget: () => {
|
|
@@ -10248,7 +11472,7 @@ async function runStandalone(options) {
|
|
|
10248
11472
|
} catch (error) {
|
|
10249
11473
|
return stopWith(error.message);
|
|
10250
11474
|
}
|
|
10251
|
-
if (!existsSync(
|
|
11475
|
+
if (!existsSync(path21.join(distDir, "index.html"))) {
|
|
10252
11476
|
return stopWith(
|
|
10253
11477
|
"The dashboard has not been built yet. Run `npm run build`, then start it again."
|
|
10254
11478
|
);
|
|
@@ -10291,12 +11515,24 @@ async function runStandalone(options) {
|
|
|
10291
11515
|
|
|
10292
11516
|
// src/cli/start/openBrowser.ts
|
|
10293
11517
|
import { spawn as spawn3 } from "node:child_process";
|
|
11518
|
+
import path22 from "node:path";
|
|
10294
11519
|
var OPEN_WAIT_MS = 1e4;
|
|
10295
|
-
|
|
11520
|
+
var WINDOWS_FOLDER = "C:\\Windows";
|
|
11521
|
+
function windowsFolder(env) {
|
|
11522
|
+
const named = env.SystemRoot?.trim() || env.SYSTEMROOT?.trim();
|
|
11523
|
+
return named && path22.win32.isAbsolute(named) ? named : WINDOWS_FOLDER;
|
|
11524
|
+
}
|
|
11525
|
+
function openCommand(platform, url, env = process.env) {
|
|
10296
11526
|
const address = loopbackAddress(url);
|
|
10297
11527
|
if (address === null) return null;
|
|
10298
11528
|
if (platform === "darwin") return { file: "/usr/bin/open", args: [address] };
|
|
10299
11529
|
if (platform === "linux") return { file: "xdg-open", args: [address] };
|
|
11530
|
+
if (platform === "win32") {
|
|
11531
|
+
return {
|
|
11532
|
+
file: path22.win32.join(windowsFolder(env), "System32", "rundll32.exe"),
|
|
11533
|
+
args: ["url.dll,FileProtocolHandler", address]
|
|
11534
|
+
};
|
|
11535
|
+
}
|
|
10300
11536
|
return null;
|
|
10301
11537
|
}
|
|
10302
11538
|
function runProgram2(file, args, waitMs = OPEN_WAIT_MS) {
|
|
@@ -10307,7 +11543,7 @@ function runProgram2(file, args, waitMs = OPEN_WAIT_MS) {
|
|
|
10307
11543
|
resolve(opened);
|
|
10308
11544
|
};
|
|
10309
11545
|
try {
|
|
10310
|
-
const child = spawn3(file, [...args], { stdio: "ignore", detached: true });
|
|
11546
|
+
const child = spawn3(file, [...args], { stdio: "ignore", detached: true, windowsHide: true });
|
|
10311
11547
|
child.once("error", () => settle(false));
|
|
10312
11548
|
child.once("exit", (code) => settle(code === 0));
|
|
10313
11549
|
child.unref();
|
|
@@ -10318,7 +11554,7 @@ function runProgram2(file, args, waitMs = OPEN_WAIT_MS) {
|
|
|
10318
11554
|
});
|
|
10319
11555
|
}
|
|
10320
11556
|
async function openInBrowser(url, options = {}) {
|
|
10321
|
-
const command = openCommand(options.platform ?? process.platform, url);
|
|
11557
|
+
const command = openCommand(options.platform ?? process.platform, url, options.env);
|
|
10322
11558
|
if (command === null) return false;
|
|
10323
11559
|
return (options.run ?? runProgram2)(command.file, command.args);
|
|
10324
11560
|
}
|