agent-lookout 0.2.6 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/PRIVACY.md +151 -26
- package/README.md +190 -160
- package/SECURITY.md +24 -9
- package/dist/assets/{HistoryPanel-Cwmn7YtJ.js → HistoryPanel-CQ7zovsM.js} +1 -1
- package/dist/assets/{SessionPanel-BJ5t_CPi.js → SessionPanel-DS29Z04P.js} +1 -1
- package/dist/assets/index-BWFkRW4g.js +3 -0
- package/dist/assets/motion-DlYOyClL.js +9 -0
- package/dist/cli/agent-lookout.js +2 -2
- package/dist/cli/{mcpServer-HJSH3WIS.js → mcpServer-BMSF2TXC.js} +2 -2
- package/dist/cli/{startCommand-PR5JPWCT.js → startCommand-ANC7NVAO.js} +2496 -474
- package/dist/index.html +2 -2
- package/docs/API.md +102 -46
- package/docs/GUIDE.md +250 -147
- package/docs/INSTALL.md +177 -0
- package/package.json +6 -1
- package/dist/assets/index-QSfCI-L_.js +0 -3
- package/dist/assets/motion-BAhdRuCw.js +0 -9
|
@@ -217,6 +217,9 @@ var MAX_HOST_LENGTH = 253;
|
|
|
217
217
|
function isWebhookHost(host) {
|
|
218
218
|
return host.length <= MAX_HOST_LENGTH && WEBHOOK_HOST.test(host);
|
|
219
219
|
}
|
|
220
|
+
var PHONE_CHANNELS = ["ntfy", "pushover"];
|
|
221
|
+
var PHONE_TEST_PATH = "/api/phone/test";
|
|
222
|
+
var PHONE_TEST_ACTION = "phone-test";
|
|
220
223
|
|
|
221
224
|
// src/core/history.ts
|
|
222
225
|
var DEFAULT_HISTORY_WINDOW_MS = 15 * 60 * 1e3;
|
|
@@ -254,10 +257,30 @@ function createQuietHold() {
|
|
|
254
257
|
const last = lastWaitOf(session.id);
|
|
255
258
|
if (last?.begunAt === begunAt) {
|
|
256
259
|
last.endedAt = null;
|
|
260
|
+
last.reminderOnly = false;
|
|
257
261
|
return;
|
|
258
262
|
}
|
|
259
263
|
if (last && last.endedAt === null) last.endedAt = begunAt;
|
|
260
|
-
waits.push({
|
|
264
|
+
waits.push({
|
|
265
|
+
session: withoutWaitingText(session),
|
|
266
|
+
begunAt,
|
|
267
|
+
endedAt: null,
|
|
268
|
+
reminderOnly: false
|
|
269
|
+
});
|
|
270
|
+
},
|
|
271
|
+
holdReminder(session, begunAt) {
|
|
272
|
+
const last = lastWaitOf(session.id);
|
|
273
|
+
if (last?.begunAt === begunAt) {
|
|
274
|
+
last.endedAt = null;
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (last && last.endedAt === null) last.endedAt = begunAt;
|
|
278
|
+
waits.push({
|
|
279
|
+
session: withoutWaitingText(session),
|
|
280
|
+
begunAt,
|
|
281
|
+
endedAt: null,
|
|
282
|
+
reminderOnly: true
|
|
283
|
+
});
|
|
261
284
|
},
|
|
262
285
|
holdOver(event, session, at) {
|
|
263
286
|
over.push({ event, session: withoutWaitingText(session), at });
|
|
@@ -279,6 +302,7 @@ function createQuietHold() {
|
|
|
279
302
|
}
|
|
280
303
|
const open2 = [];
|
|
281
304
|
for (const [id, wait] of stillOpen) {
|
|
305
|
+
if (wait.reminderOnly) continue;
|
|
282
306
|
open2.push({ session: listed.get(id), begunAt: wait.begunAt });
|
|
283
307
|
}
|
|
284
308
|
const answered = /* @__PURE__ */ new Map();
|
|
@@ -336,6 +360,7 @@ var DAY_MS = 24 * HOUR_MS;
|
|
|
336
360
|
var WEEKDAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
|
|
337
361
|
var TIME_RULE_NAMES = ["longWait", "idle", "quietHours"];
|
|
338
362
|
var LONG_WAIT_MINUTES = { min: 1, max: 1440, default: 10 };
|
|
363
|
+
var REPEAT_MINUTES = { min: 5, max: 1440, default: 30 };
|
|
339
364
|
var IDLE_HOURS = { min: 1, max: 720, default: 48 };
|
|
340
365
|
var DEFAULT_TIME_RULES = {
|
|
341
366
|
longWait: { on: false, minutes: LONG_WAIT_MINUTES.default },
|
|
@@ -365,12 +390,22 @@ function holds(value, keys, exact2) {
|
|
|
365
390
|
function wholeNumberIn(value, range) {
|
|
366
391
|
return Number.isSafeInteger(value) && value >= range.min && value <= range.max;
|
|
367
392
|
}
|
|
368
|
-
function
|
|
393
|
+
function readRepeat(value, exact2) {
|
|
369
394
|
if (!isRecord(value) || !holds(value, ["on", "minutes"], exact2)) return null;
|
|
370
395
|
const { on, minutes } = value;
|
|
371
|
-
if (typeof on !== "boolean" || !wholeNumberIn(minutes,
|
|
396
|
+
if (typeof on !== "boolean" || !wholeNumberIn(minutes, REPEAT_MINUTES)) return null;
|
|
372
397
|
return { on, minutes };
|
|
373
398
|
}
|
|
399
|
+
function readLongWait(value, exact2) {
|
|
400
|
+
if (!isRecord(value)) return null;
|
|
401
|
+
const keys = value.repeat === void 0 ? ["on", "minutes"] : ["on", "minutes", "repeat"];
|
|
402
|
+
if (!holds(value, keys, exact2)) return null;
|
|
403
|
+
const { on, minutes } = value;
|
|
404
|
+
if (typeof on !== "boolean" || !wholeNumberIn(minutes, LONG_WAIT_MINUTES)) return null;
|
|
405
|
+
if (value.repeat === void 0) return { on, minutes };
|
|
406
|
+
const repeat = readRepeat(value.repeat, exact2);
|
|
407
|
+
return repeat === null ? null : { on, minutes, repeat };
|
|
408
|
+
}
|
|
374
409
|
function readIdle(value, exact2) {
|
|
375
410
|
if (!isRecord(value) || !holds(value, ["on", "hours"], exact2)) return null;
|
|
376
411
|
const { on, hours } = value;
|
|
@@ -402,7 +437,7 @@ var READERS = {
|
|
|
402
437
|
quietHours: readQuietHours
|
|
403
438
|
};
|
|
404
439
|
var SHAPES = {
|
|
405
|
-
longWait: `longWait must be {"on", "minutes"}, with minutes a whole number from ${LONG_WAIT_MINUTES.min} to ${LONG_WAIT_MINUTES.max}.`,
|
|
440
|
+
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}.`,
|
|
406
441
|
idle: `idle must be {"on", "hours"}, with hours a whole number from ${IDLE_HOURS.min} to ${IDLE_HOURS.max}.`,
|
|
407
442
|
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.'
|
|
408
443
|
};
|
|
@@ -467,6 +502,18 @@ function quietOf(snapshot) {
|
|
|
467
502
|
}
|
|
468
503
|
|
|
469
504
|
// src/core/time-rules/reminders.ts
|
|
505
|
+
var MINUTE_MS = 6e4;
|
|
506
|
+
function reminderSchedule(rule) {
|
|
507
|
+
return {
|
|
508
|
+
thresholdMs: rule.minutes * MINUTE_MS,
|
|
509
|
+
everyMs: rule.repeat?.on ? rule.repeat.minutes * MINUTE_MS : null
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
function reminderNumber(waitedMs, schedule) {
|
|
513
|
+
if (waitedMs < schedule.thresholdMs) return -1;
|
|
514
|
+
if (schedule.everyMs === null) return 0;
|
|
515
|
+
return Math.floor((waitedMs - schedule.thresholdMs) / schedule.everyMs);
|
|
516
|
+
}
|
|
470
517
|
function readable(snapshot) {
|
|
471
518
|
return new Set(
|
|
472
519
|
snapshot.sources.filter((source) => source.state === "ok" || source.state === "not-set-up").map((source) => source.id)
|
|
@@ -485,29 +532,33 @@ function createReminderWatch() {
|
|
|
485
532
|
if (known && (since === null || since === known.begunAt)) continue;
|
|
486
533
|
watched.set(session.id, {
|
|
487
534
|
begunAt: since ?? at,
|
|
488
|
-
|
|
489
|
-
|
|
535
|
+
seenAfterMs: since === null ? 0 : at - since,
|
|
536
|
+
toldAfterMs: null
|
|
490
537
|
});
|
|
491
538
|
}
|
|
492
539
|
},
|
|
493
|
-
due(snapshot, at,
|
|
540
|
+
due(snapshot, at, schedule, eligible = () => true) {
|
|
494
541
|
const found = [];
|
|
495
542
|
for (const session of snapshot.sessions) {
|
|
496
543
|
if (!needsYou(session)) continue;
|
|
497
544
|
const wait = watched.get(session.id);
|
|
498
|
-
if (!wait ||
|
|
545
|
+
if (!wait || !eligible(session.id)) continue;
|
|
499
546
|
const waitedMs = at - wait.begunAt;
|
|
500
|
-
|
|
501
|
-
|
|
547
|
+
const repeat = reminderNumber(waitedMs, schedule);
|
|
548
|
+
if (repeat < 0 || repeat <= reminderNumber(wait.seenAfterMs, schedule)) continue;
|
|
549
|
+
const told = wait.toldAfterMs;
|
|
550
|
+
if (told !== null && repeat <= reminderNumber(told, schedule)) continue;
|
|
551
|
+
const { thresholdMs, everyMs } = schedule;
|
|
552
|
+
if (told !== null && everyMs !== null && repeat > 0) {
|
|
553
|
+
if (thresholdMs + repeat * everyMs - told < everyMs / 2) continue;
|
|
554
|
+
}
|
|
555
|
+
found.push({ session, begunAt: wait.begunAt, waitedMs, repeat });
|
|
502
556
|
}
|
|
503
557
|
return found;
|
|
504
558
|
},
|
|
505
|
-
|
|
506
|
-
watched.get(sessionId)?.reminded.add(thresholdMs);
|
|
507
|
-
},
|
|
508
|
-
toldLate(sessionId, at, thresholdMs) {
|
|
559
|
+
told(sessionId, at) {
|
|
509
560
|
const wait = watched.get(sessionId);
|
|
510
|
-
if (wait
|
|
561
|
+
if (wait) wait.toldAfterMs = Math.max(wait.toldAfterMs ?? 0, at - wait.begunAt);
|
|
511
562
|
},
|
|
512
563
|
clear() {
|
|
513
564
|
watched.clear();
|
|
@@ -535,7 +586,7 @@ function waitBegan(statusSince, seenAt) {
|
|
|
535
586
|
|
|
536
587
|
// src/collector/outbound/outboundChannel.ts
|
|
537
588
|
function createOutboundChannel(options) {
|
|
538
|
-
const { afterMs, asking, waitMessage, overMessage, reminderMessage, summaryMessage, failure: failure2 } = options;
|
|
589
|
+
const { afterMs, asking, waitMessage: waitMessage2, overMessage: overMessage2, reminderMessage: reminderMessage2, summaryMessage: summaryMessage2, failure: failure2 } = options;
|
|
539
590
|
const now = options.now ?? Date.now;
|
|
540
591
|
const wanted = new Set(options.events);
|
|
541
592
|
let memory = EMPTY_CHANGE_MEMORY;
|
|
@@ -547,15 +598,26 @@ function createOutboundChannel(options) {
|
|
|
547
598
|
const hold = createQuietHold();
|
|
548
599
|
let last = null;
|
|
549
600
|
let queue = Promise.resolve();
|
|
550
|
-
function
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
() => {
|
|
556
|
-
|
|
557
|
-
|
|
601
|
+
function tryInTurn(content, after = () => {
|
|
602
|
+
}) {
|
|
603
|
+
const tried = queue.then(() => options.send(content)).then(
|
|
604
|
+
(outcome) => outcome,
|
|
605
|
+
// A sender should not reject. One that does is a failure like any other.
|
|
606
|
+
() => ({ sent: false, reason: failure2 })
|
|
607
|
+
).then((outcome) => {
|
|
608
|
+
after(outcome);
|
|
609
|
+
return outcome;
|
|
610
|
+
});
|
|
611
|
+
queue = tried.then(
|
|
612
|
+
() => void 0,
|
|
613
|
+
() => void 0
|
|
558
614
|
);
|
|
615
|
+
return tried;
|
|
616
|
+
}
|
|
617
|
+
function send2(content) {
|
|
618
|
+
void tryInTurn(content, (outcome) => {
|
|
619
|
+
last = outcome.sent ? { at: now(), sent: true } : { at: now(), sent: false, reason: outcome.reason };
|
|
620
|
+
});
|
|
559
621
|
}
|
|
560
622
|
return {
|
|
561
623
|
handle(snapshot) {
|
|
@@ -578,7 +640,7 @@ function createOutboundChannel(options) {
|
|
|
578
640
|
...item,
|
|
579
641
|
agent: agentName(item.session, snapshot.sources)
|
|
580
642
|
}));
|
|
581
|
-
ready.push((sentNow) =>
|
|
643
|
+
ready.push((sentNow) => summaryMessage2({ ...summary, items, now: sentNow }));
|
|
582
644
|
}
|
|
583
645
|
for (const { session, begunAt } of end.open) open2.set(session.id, begunAt);
|
|
584
646
|
}
|
|
@@ -596,7 +658,7 @@ function createOutboundChannel(options) {
|
|
|
596
658
|
agent: agentName(session, snapshot.sources),
|
|
597
659
|
seenAt: at
|
|
598
660
|
};
|
|
599
|
-
ready.push((sentNow) =>
|
|
661
|
+
ready.push((sentNow) => overMessage2({ ...facts, now: sentNow }));
|
|
600
662
|
continue;
|
|
601
663
|
}
|
|
602
664
|
const since = session.statusSince;
|
|
@@ -609,7 +671,7 @@ function createOutboundChannel(options) {
|
|
|
609
671
|
sentAt.push(at);
|
|
610
672
|
send2(write(at));
|
|
611
673
|
}
|
|
612
|
-
const
|
|
674
|
+
const schedule = reminderSchedule(rules.longWait);
|
|
613
675
|
for (const [id, begunAt] of open2) {
|
|
614
676
|
const session = snapshot.sessions.find(
|
|
615
677
|
(candidate) => candidate.id === id && needsYou(candidate)
|
|
@@ -626,7 +688,7 @@ function createOutboundChannel(options) {
|
|
|
626
688
|
open2.delete(id);
|
|
627
689
|
sentAt.push(at);
|
|
628
690
|
send2(
|
|
629
|
-
|
|
691
|
+
waitMessage2({
|
|
630
692
|
session: withoutWaitingText(session),
|
|
631
693
|
agent: agentName(session, snapshot.sources),
|
|
632
694
|
// Read now, from this poll's snapshot, and only when it is to go.
|
|
@@ -635,21 +697,27 @@ function createOutboundChannel(options) {
|
|
|
635
697
|
now: at
|
|
636
698
|
})
|
|
637
699
|
);
|
|
638
|
-
if (rules.longWait.on) reminders.
|
|
700
|
+
if (rules.longWait.on) reminders.told(id, at);
|
|
639
701
|
}
|
|
640
|
-
if (
|
|
641
|
-
for (const due of reminders.due(snapshot, at,
|
|
702
|
+
if (rules.longWait.on && wanted.has("needs-you")) {
|
|
703
|
+
for (const due of reminders.due(snapshot, at, schedule, (id) => !open2.has(id))) {
|
|
704
|
+
if (quiet) {
|
|
705
|
+
hold.holdReminder(due.session, due.begunAt);
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
642
708
|
if (limitLiftsAt(sentAt, at) !== null) break;
|
|
643
709
|
sentAt.push(at);
|
|
644
|
-
reminders.
|
|
710
|
+
reminders.told(due.session.id, at);
|
|
645
711
|
send2(
|
|
646
|
-
|
|
712
|
+
reminderMessage2({
|
|
647
713
|
session: withoutWaitingText(due.session),
|
|
648
714
|
agent: agentName(due.session, snapshot.sources),
|
|
649
715
|
asking: asking ? waitingText(due.session.waitingText) ?? null : null,
|
|
650
716
|
begunAt: due.begunAt,
|
|
651
717
|
now: at,
|
|
652
|
-
thresholdMs
|
|
718
|
+
thresholdMs: schedule.thresholdMs,
|
|
719
|
+
everyMs: schedule.everyMs,
|
|
720
|
+
repeat: due.repeat
|
|
653
721
|
})
|
|
654
722
|
);
|
|
655
723
|
}
|
|
@@ -664,14 +732,22 @@ function createOutboundChannel(options) {
|
|
|
664
732
|
},
|
|
665
733
|
last: () => last,
|
|
666
734
|
limitedUntil: () => limitLiftsAt(sentAt, now()),
|
|
667
|
-
settled: () => queue
|
|
735
|
+
settled: () => queue,
|
|
736
|
+
async test(content) {
|
|
737
|
+
const at = now();
|
|
738
|
+
sentAt = sendsInLastHour(sentAt, at);
|
|
739
|
+
const limitedUntil = limitLiftsAt(sentAt, at);
|
|
740
|
+
if (limitedUntil !== null) return { tried: false, limitedUntil };
|
|
741
|
+
sentAt.push(at);
|
|
742
|
+
return { tried: true, at, outcome: await tryInTurn(content) };
|
|
743
|
+
}
|
|
668
744
|
};
|
|
669
745
|
}
|
|
670
746
|
|
|
671
747
|
// src/core/time-rules/timeRulesWords.ts
|
|
672
|
-
var
|
|
748
|
+
var MINUTE_MS2 = 6e4;
|
|
673
749
|
function waitedInWords(ms) {
|
|
674
|
-
if (ms <
|
|
750
|
+
if (ms < MINUTE_MS2) return "under a minute";
|
|
675
751
|
return partsInWords(durationParts(ms).filter((part) => part.unit !== "s"));
|
|
676
752
|
}
|
|
677
753
|
var WAITED_FOR = {
|
|
@@ -873,9 +949,10 @@ function waitEmail(facts) {
|
|
|
873
949
|
}
|
|
874
950
|
function reminderEmail(facts) {
|
|
875
951
|
const { session, begunAt, now } = facts;
|
|
952
|
+
const again = facts.everyMs === null ? "" : `, and again every ${waitedInWords(facts.everyMs)} while it goes on`;
|
|
876
953
|
return emailOf(
|
|
877
954
|
reminderSentence(session, now - begunAt),
|
|
878
|
-
`It has waited since ${clockAt(begunAt, now)}. Agent Lookout reminds you once a wait lasts ${waitedInWords(facts.thresholdMs)}, as Time rules in Settings says.`,
|
|
955
|
+
`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.`,
|
|
879
956
|
session,
|
|
880
957
|
facts.agent,
|
|
881
958
|
facts.asking
|
|
@@ -1231,6 +1308,116 @@ function pullRequestsOffStatus(problem) {
|
|
|
1231
1308
|
return { on: false, problem, gh: null, last: null };
|
|
1232
1309
|
}
|
|
1233
1310
|
|
|
1311
|
+
// src/collector/outbound/phoneMessage.ts
|
|
1312
|
+
var MAX_PHONE_TITLE = 250;
|
|
1313
|
+
var MAX_PHONE_MESSAGE = 1024;
|
|
1314
|
+
function isWaitKind(kind) {
|
|
1315
|
+
return kind === "needs-you" || kind === "reminder" || kind === "test";
|
|
1316
|
+
}
|
|
1317
|
+
function cutTo(text4, max) {
|
|
1318
|
+
const characters = Array.from(text4);
|
|
1319
|
+
if (characters.length <= max) return text4;
|
|
1320
|
+
return `${characters.slice(0, max - 1).join("").trimEnd()}\u2026`;
|
|
1321
|
+
}
|
|
1322
|
+
function phoneMessage(kind, title, lines) {
|
|
1323
|
+
const said = lines.filter((line) => line !== "");
|
|
1324
|
+
return {
|
|
1325
|
+
kind,
|
|
1326
|
+
title: cutTo(title, MAX_PHONE_TITLE),
|
|
1327
|
+
message: cutTo(said.length > 0 ? said.join("\n") : title, MAX_PHONE_MESSAGE)
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
function factsLine(first, session, agent) {
|
|
1331
|
+
const facts = [
|
|
1332
|
+
first,
|
|
1333
|
+
session.project ? oneLine(session.project) : "",
|
|
1334
|
+
surfaceLabel(session.surface) ?? "",
|
|
1335
|
+
agent ? oneLine(agent) : ""
|
|
1336
|
+
];
|
|
1337
|
+
return facts.filter((fact) => fact !== "").join(" \xB7 ");
|
|
1338
|
+
}
|
|
1339
|
+
function askingLine(asking) {
|
|
1340
|
+
const said = waitingText(asking);
|
|
1341
|
+
return said ? `Asking: ${said}` : "";
|
|
1342
|
+
}
|
|
1343
|
+
function waitMessage(facts) {
|
|
1344
|
+
const { session, begunAt, now } = facts;
|
|
1345
|
+
return phoneMessage("needs-you", `${oneLine(sessionTitle(session))} ${waitingPhrase(session)}`, [
|
|
1346
|
+
askingLine(facts.asking),
|
|
1347
|
+
factsLine(formatDuration(Math.max(0, now - begunAt)), session, facts.agent)
|
|
1348
|
+
]);
|
|
1349
|
+
}
|
|
1350
|
+
function reminderMessage(facts) {
|
|
1351
|
+
const { session, begunAt, now } = facts;
|
|
1352
|
+
return phoneMessage("reminder", reminderSentence(session, now - begunAt), [
|
|
1353
|
+
askingLine(facts.asking),
|
|
1354
|
+
factsLine(`Since ${clockAt(begunAt, now)}`, session, facts.agent)
|
|
1355
|
+
]);
|
|
1356
|
+
}
|
|
1357
|
+
function summaryMessage(facts) {
|
|
1358
|
+
return phoneMessage("quiet-summary", WHILE_QUIET, [summaryLine(facts.items)]);
|
|
1359
|
+
}
|
|
1360
|
+
function overMessage(facts) {
|
|
1361
|
+
const { session, seenAt, now } = facts;
|
|
1362
|
+
return phoneMessage(facts.event, `${oneLine(sessionTitle(session))} ${overPhrase(facts.event)}`, [
|
|
1363
|
+
factsLine(`Seen at ${clockAt(seenAt, now)}`, session, facts.agent)
|
|
1364
|
+
]);
|
|
1365
|
+
}
|
|
1366
|
+
function testMessage() {
|
|
1367
|
+
return phoneMessage("test", "Agent Lookout test", [
|
|
1368
|
+
"Pushes from Agent Lookout reach this device."
|
|
1369
|
+
]);
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// src/collector/ntfy/ntfyNotifications.ts
|
|
1373
|
+
function ntfyOffStatus(problem) {
|
|
1374
|
+
return {
|
|
1375
|
+
on: false,
|
|
1376
|
+
host: null,
|
|
1377
|
+
tokenSet: null,
|
|
1378
|
+
events: null,
|
|
1379
|
+
afterMs: null,
|
|
1380
|
+
asking: null,
|
|
1381
|
+
problem,
|
|
1382
|
+
last: null,
|
|
1383
|
+
limitedUntil: null
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
function createNtfyNotifications(options) {
|
|
1387
|
+
const { settings, sender } = options;
|
|
1388
|
+
const channel = createOutboundChannel({
|
|
1389
|
+
events: settings.events,
|
|
1390
|
+
afterMs: settings.afterMs,
|
|
1391
|
+
asking: settings.asking,
|
|
1392
|
+
waitMessage,
|
|
1393
|
+
overMessage,
|
|
1394
|
+
reminderMessage,
|
|
1395
|
+
summaryMessage,
|
|
1396
|
+
send: (message) => sender.send(message),
|
|
1397
|
+
failure: "the push could not be sent",
|
|
1398
|
+
now: options.now
|
|
1399
|
+
});
|
|
1400
|
+
return {
|
|
1401
|
+
handle: (snapshot) => channel.handle(snapshot),
|
|
1402
|
+
status() {
|
|
1403
|
+
return {
|
|
1404
|
+
on: true,
|
|
1405
|
+
// The host alone. The topic is the secret anyone could read the pushes with.
|
|
1406
|
+
host: settings.server.hostname,
|
|
1407
|
+
tokenSet: settings.token !== null,
|
|
1408
|
+
events: [...settings.events],
|
|
1409
|
+
afterMs: settings.afterMs,
|
|
1410
|
+
asking: settings.asking,
|
|
1411
|
+
problem: null,
|
|
1412
|
+
last: channel.last(),
|
|
1413
|
+
limitedUntil: channel.limitedUntil()
|
|
1414
|
+
};
|
|
1415
|
+
},
|
|
1416
|
+
test: () => channel.test(testMessage()),
|
|
1417
|
+
settled: () => channel.settled()
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1234
1421
|
// src/core/sessions/diff.ts
|
|
1235
1422
|
function severityFor(to) {
|
|
1236
1423
|
if (to === "needs-you") return "warning";
|
|
@@ -1583,6 +1770,50 @@ function createPoller(options) {
|
|
|
1583
1770
|
};
|
|
1584
1771
|
}
|
|
1585
1772
|
|
|
1773
|
+
// src/collector/pushover/pushoverNotifications.ts
|
|
1774
|
+
function pushoverOffStatus(problem) {
|
|
1775
|
+
return {
|
|
1776
|
+
on: false,
|
|
1777
|
+
events: null,
|
|
1778
|
+
afterMs: null,
|
|
1779
|
+
asking: null,
|
|
1780
|
+
problem,
|
|
1781
|
+
last: null,
|
|
1782
|
+
limitedUntil: null
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
function createPushoverNotifications(options) {
|
|
1786
|
+
const { settings, sender } = options;
|
|
1787
|
+
const channel = createOutboundChannel({
|
|
1788
|
+
events: settings.events,
|
|
1789
|
+
afterMs: settings.afterMs,
|
|
1790
|
+
asking: settings.asking,
|
|
1791
|
+
waitMessage,
|
|
1792
|
+
overMessage,
|
|
1793
|
+
reminderMessage,
|
|
1794
|
+
summaryMessage,
|
|
1795
|
+
send: (message) => sender.send(message),
|
|
1796
|
+
failure: "the push could not be sent",
|
|
1797
|
+
now: options.now
|
|
1798
|
+
});
|
|
1799
|
+
return {
|
|
1800
|
+
handle: (snapshot) => channel.handle(snapshot),
|
|
1801
|
+
status() {
|
|
1802
|
+
return {
|
|
1803
|
+
on: true,
|
|
1804
|
+
events: [...settings.events],
|
|
1805
|
+
afterMs: settings.afterMs,
|
|
1806
|
+
asking: settings.asking,
|
|
1807
|
+
problem: null,
|
|
1808
|
+
last: channel.last(),
|
|
1809
|
+
limitedUntil: channel.limitedUntil()
|
|
1810
|
+
};
|
|
1811
|
+
},
|
|
1812
|
+
test: () => channel.test(testMessage()),
|
|
1813
|
+
settled: () => channel.settled()
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1586
1817
|
// src/collector/webhook/webhookMessage.ts
|
|
1587
1818
|
var MAX_SUMMARY_ITEMS = 100;
|
|
1588
1819
|
function slackSafe(text4) {
|
|
@@ -1634,7 +1865,9 @@ function reminderPost(facts) {
|
|
|
1634
1865
|
post.session.app,
|
|
1635
1866
|
post.session.agent
|
|
1636
1867
|
]),
|
|
1637
|
-
reminder: true
|
|
1868
|
+
reminder: true,
|
|
1869
|
+
// Left out on the first, so its post is the post it always was.
|
|
1870
|
+
...facts.repeat > 0 && { repeat: facts.repeat }
|
|
1638
1871
|
};
|
|
1639
1872
|
}
|
|
1640
1873
|
function summaryPost(facts) {
|
|
@@ -1856,6 +2089,7 @@ function createApiHandler(options) {
|
|
|
1856
2089
|
if (options.answer) actions.set(ANSWER_PATH, options.answer);
|
|
1857
2090
|
if (options.timeRules) actions.set(TIME_RULES_PATH, options.timeRules);
|
|
1858
2091
|
if (options.permissionRules) actions.set(PERMISSION_RULES_PATH, options.permissionRules);
|
|
2092
|
+
if (options.phoneTest) actions.set(PHONE_TEST_PATH, options.phoneTest);
|
|
1859
2093
|
const serveSnapshot = options.serveSnapshot ?? ((snapshot) => snapshot);
|
|
1860
2094
|
function route(req, res) {
|
|
1861
2095
|
const refusal3 = refusalFor(req);
|
|
@@ -1918,6 +2152,22 @@ function createApiHandler(options) {
|
|
|
1918
2152
|
);
|
|
1919
2153
|
return;
|
|
1920
2154
|
}
|
|
2155
|
+
case "/api/ntfy": {
|
|
2156
|
+
send(
|
|
2157
|
+
res,
|
|
2158
|
+
200,
|
|
2159
|
+
options.ntfy ? options.ntfy() : ntfyOffStatus(null)
|
|
2160
|
+
);
|
|
2161
|
+
return;
|
|
2162
|
+
}
|
|
2163
|
+
case "/api/pushover": {
|
|
2164
|
+
send(
|
|
2165
|
+
res,
|
|
2166
|
+
200,
|
|
2167
|
+
options.pushover ? options.pushover() : pushoverOffStatus(null)
|
|
2168
|
+
);
|
|
2169
|
+
return;
|
|
2170
|
+
}
|
|
1921
2171
|
case "/api/pull-requests": {
|
|
1922
2172
|
send(
|
|
1923
2173
|
res,
|
|
@@ -2201,7 +2451,7 @@ function urlFor(address) {
|
|
|
2201
2451
|
|
|
2202
2452
|
// src/collector/hosts/standalone.ts
|
|
2203
2453
|
import { existsSync } from "node:fs";
|
|
2204
|
-
import
|
|
2454
|
+
import path25 from "node:path";
|
|
2205
2455
|
|
|
2206
2456
|
// src/core/mapping/claudeCodeMapping.ts
|
|
2207
2457
|
var LIVE_STATUS = /* @__PURE__ */ new Map([
|
|
@@ -3057,6 +3307,9 @@ function lstartMs(value) {
|
|
|
3057
3307
|
const [day, hours, minutes, seconds, year] = match.slice(2).map(Number);
|
|
3058
3308
|
return Date.UTC(year, month, day, hours, minutes, seconds);
|
|
3059
3309
|
}
|
|
3310
|
+
function processStartTime(value) {
|
|
3311
|
+
return lstartMs(collapse(value)) ?? null;
|
|
3312
|
+
}
|
|
3060
3313
|
function compareProcessStart(recorded, actual, withinMs = SAME_START_WITHIN_MS) {
|
|
3061
3314
|
if (recorded === void 0 || actual === void 0) return "unknown";
|
|
3062
3315
|
const before = collapse(recorded);
|
|
@@ -3694,11 +3947,11 @@ async function readTranscriptTail(file, io, known, maxBytes = TRANSCRIPT_TAIL_BY
|
|
|
3694
3947
|
const stamp = stampOf(open2.info);
|
|
3695
3948
|
if (known !== null && sameStamp(known, stamp)) return { unchanged: true, stamp };
|
|
3696
3949
|
const start = Math.max(0, stamp.size - maxBytes);
|
|
3697
|
-
const
|
|
3950
|
+
const bytes3 = await open2.read(start, stamp.size - start);
|
|
3698
3951
|
return {
|
|
3699
3952
|
unchanged: false,
|
|
3700
3953
|
stamp,
|
|
3701
|
-
tail: new TextDecoder("utf-8").decode(
|
|
3954
|
+
tail: new TextDecoder("utf-8").decode(bytes3),
|
|
3702
3955
|
fromStart: start === 0
|
|
3703
3956
|
};
|
|
3704
3957
|
} finally {
|
|
@@ -4602,9 +4855,21 @@ async function pollAfterAnswer(poller, sessionId) {
|
|
|
4602
4855
|
function answerRefusalFor(req) {
|
|
4603
4856
|
return actionRefusalFor(req, ANSWER_ACTION, MAX_ANSWER_BODY_BYTES);
|
|
4604
4857
|
}
|
|
4605
|
-
function
|
|
4858
|
+
function createAnswerer(options) {
|
|
4606
4859
|
const { poller, events, asks } = options;
|
|
4607
4860
|
const now = options.now ?? Date.now;
|
|
4861
|
+
return async (sessionId, requestId, decision) => {
|
|
4862
|
+
const session = poller.getSnapshot().sessions.find((listed) => listed.id === sessionId);
|
|
4863
|
+
if (!session) return "no-ask";
|
|
4864
|
+
const outcome = await asks.answer(sessionId, requestId, decision);
|
|
4865
|
+
if (outcome !== "answered") return outcome;
|
|
4866
|
+
events.add([answeredEvent(session, decision, now())]);
|
|
4867
|
+
await pollAfterAnswer(poller, sessionId);
|
|
4868
|
+
return "answered";
|
|
4869
|
+
};
|
|
4870
|
+
}
|
|
4871
|
+
function createAnswerRoute(options) {
|
|
4872
|
+
const answer = createAnswerer(options);
|
|
4608
4873
|
return async function answerPermission(req) {
|
|
4609
4874
|
const refused = answerRefusalFor(req);
|
|
4610
4875
|
if (refused) {
|
|
@@ -4623,12 +4888,8 @@ function createAnswerRoute(options) {
|
|
|
4623
4888
|
'The body must be {"sessionId": "...", "requestId": "...", "decision": "allow" or "deny"} and nothing else.'
|
|
4624
4889
|
);
|
|
4625
4890
|
}
|
|
4626
|
-
const
|
|
4627
|
-
if (!session) return answerFailed("no-ask");
|
|
4628
|
-
const outcome = await asks.answer(asked.sessionId, asked.requestId, asked.decision);
|
|
4891
|
+
const outcome = await answer(asked.sessionId, asked.requestId, asked.decision);
|
|
4629
4892
|
if (outcome !== "answered") return answerFailed(outcome);
|
|
4630
|
-
events.add([answeredEvent(session, asked.decision, now())]);
|
|
4631
|
-
await pollAfterAnswer(poller, asked.sessionId);
|
|
4632
4893
|
return {
|
|
4633
4894
|
status: 200,
|
|
4634
4895
|
body: { ok: true, decision: asked.decision }
|
|
@@ -5564,18 +5825,18 @@ function namesOwnFiles(words, ownPaths = [], folder) {
|
|
|
5564
5825
|
(part) => from !== void 0 && !part.startsWith("/") && !part.startsWith("-") ? [part, `${from}/${part}`] : [part]
|
|
5565
5826
|
);
|
|
5566
5827
|
return read.some((part) => {
|
|
5567
|
-
const
|
|
5568
|
-
if (
|
|
5569
|
-
if (own.some((name) =>
|
|
5570
|
-
if (above.has(
|
|
5571
|
-
return ownNames.some((name) =>
|
|
5828
|
+
const path27 = plainPath(part);
|
|
5829
|
+
if (path27 === BY_NUMBER || path27.startsWith(`${BY_NUMBER}/`)) return true;
|
|
5830
|
+
if (own.some((name) => path27 === name || path27.startsWith(`${name}/`))) return true;
|
|
5831
|
+
if (above.has(path27)) return true;
|
|
5832
|
+
return ownNames.some((name) => path27 === name || path27.endsWith(`/${name}`));
|
|
5572
5833
|
});
|
|
5573
5834
|
});
|
|
5574
5835
|
}
|
|
5575
5836
|
function folderReachesOwnFiles(folder, ownPaths = []) {
|
|
5576
5837
|
const own = ownPaths.filter((name) => name.startsWith("/")).map(plainPath);
|
|
5577
|
-
const
|
|
5578
|
-
return own.some((name) => name ===
|
|
5838
|
+
const path27 = plainPath(folder);
|
|
5839
|
+
return own.some((name) => name === path27 || name.startsWith(`${path27 === "/" ? "" : path27}/`));
|
|
5579
5840
|
}
|
|
5580
5841
|
var PLAIN_WORD = /^[A-Za-z0-9_\-.,/:=+@%]+$/;
|
|
5581
5842
|
var ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
@@ -5928,6 +6189,8 @@ function createAnswering(options) {
|
|
|
5928
6189
|
observe: () => {
|
|
5929
6190
|
},
|
|
5930
6191
|
answeredIn: () => /* @__PURE__ */ new Set(),
|
|
6192
|
+
check: async () => {
|
|
6193
|
+
},
|
|
5931
6194
|
ruleAnswers: () => []
|
|
5932
6195
|
};
|
|
5933
6196
|
}
|
|
@@ -5955,6 +6218,7 @@ function createAnswering(options) {
|
|
|
5955
6218
|
let problem = null;
|
|
5956
6219
|
let listening = false;
|
|
5957
6220
|
const answerRoute = createAnswerRoute({ poller, events, asks, now: options.now });
|
|
6221
|
+
const answer = createAnswerer({ poller, events, asks, now: options.now });
|
|
5958
6222
|
return {
|
|
5959
6223
|
async start() {
|
|
5960
6224
|
const started = await socket.start();
|
|
@@ -5987,6 +6251,9 @@ function createAnswering(options) {
|
|
|
5987
6251
|
req.resume();
|
|
5988
6252
|
return answerRefusalFor(req) ?? refusal(405, "This address only answers GET requests.", { Allow: "GET" });
|
|
5989
6253
|
},
|
|
6254
|
+
// The same answer, from the host's own process. With no socket nothing is held.
|
|
6255
|
+
answer: async (sessionId, requestId, decision) => problem === null ? answer(sessionId, requestId, decision) : "unavailable",
|
|
6256
|
+
check: () => asks.check(),
|
|
5990
6257
|
ruleAnswers: () => ruleAnswers.recent()
|
|
5991
6258
|
};
|
|
5992
6259
|
}
|
|
@@ -6214,120 +6481,1482 @@ function waitTotals(input) {
|
|
|
6214
6481
|
};
|
|
6215
6482
|
}
|
|
6216
6483
|
|
|
6217
|
-
// src/collector/adapters/
|
|
6484
|
+
// src/collector/adapters/antigravity/index.ts
|
|
6218
6485
|
import os5 from "node:os";
|
|
6219
|
-
import
|
|
6486
|
+
import path14 from "node:path";
|
|
6220
6487
|
|
|
6221
|
-
// src/core/mapping/
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
const {
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
|
|
6488
|
+
// src/core/mapping/antigravityLiveness.ts
|
|
6489
|
+
function holdConversations(processes, conversations, listedAt = null) {
|
|
6490
|
+
const held = /* @__PURE__ */ new Set();
|
|
6491
|
+
let complete = true;
|
|
6492
|
+
for (const { startedAt, conversation } of processes) {
|
|
6493
|
+
if (conversation !== void 0) held.add(conversation);
|
|
6494
|
+
if (startedAt === null) {
|
|
6495
|
+
complete = false;
|
|
6496
|
+
continue;
|
|
6497
|
+
}
|
|
6498
|
+
let wrote = false;
|
|
6499
|
+
for (const { id, writtenAt } of conversations) {
|
|
6500
|
+
if (writtenAt === null || writtenAt < startedAt) continue;
|
|
6501
|
+
held.add(id);
|
|
6502
|
+
wrote = true;
|
|
6503
|
+
}
|
|
6504
|
+
if (!wrote && conversation === void 0) complete = false;
|
|
6237
6505
|
}
|
|
6238
|
-
|
|
6239
|
-
}
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
function keepsWriterLocks(cliVersion) {
|
|
6243
|
-
if (typeof cliVersion !== "string") return false;
|
|
6244
|
-
const match = VERSION.exec(cliVersion);
|
|
6245
|
-
if (!match) return false;
|
|
6246
|
-
const parts = [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
6247
|
-
for (const [index, since] of WRITER_LOCKS_SINCE.entries()) {
|
|
6248
|
-
const part = parts[index];
|
|
6249
|
-
if (part !== since) return part > since;
|
|
6506
|
+
if (listedAt !== null) {
|
|
6507
|
+
for (const { id, writtenAt } of conversations) {
|
|
6508
|
+
if (writtenAt !== null && writtenAt > listedAt) held.add(id);
|
|
6509
|
+
}
|
|
6250
6510
|
}
|
|
6251
|
-
return
|
|
6511
|
+
return { held, complete };
|
|
6252
6512
|
}
|
|
6253
|
-
function
|
|
6254
|
-
if (
|
|
6255
|
-
if (
|
|
6256
|
-
return
|
|
6513
|
+
function antigravityLiveness(id, holding) {
|
|
6514
|
+
if (holding === null) return "unknown";
|
|
6515
|
+
if (holding.held.has(id)) return true;
|
|
6516
|
+
return holding.complete ? false : "unknown";
|
|
6257
6517
|
}
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6518
|
+
|
|
6519
|
+
// src/core/mapping/antigravityLog.ts
|
|
6520
|
+
var GLOG_LINE = /^[IWEF](\d{2})(\d{2}) (\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))? +\d+ ([\w.-]+):\d+\] (.*)$/;
|
|
6521
|
+
var SOURCE2 = {
|
|
6522
|
+
backend: "server.go",
|
|
6523
|
+
created: "server.go",
|
|
6524
|
+
streaming: "conversation_manager.go",
|
|
6525
|
+
confirmation: "tool_confirmation_manager.go"
|
|
6526
|
+
};
|
|
6527
|
+
var UUID = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}";
|
|
6528
|
+
var OPENED = new RegExp(`^(Created|Streaming) conversation (${UUID})$`);
|
|
6529
|
+
var BACKEND = /^Creating CLI server backend: (?:.* )?workspaceDirs=\[([^\]\s]+)\](?: |$)/;
|
|
6530
|
+
var ASKING = /^Surfacing tool confirmation: "([A-Za-z]{1,64})" at step (\d{1,9})$/;
|
|
6531
|
+
var ANSWERED = new RegExp(
|
|
6532
|
+
`^Responding to tool confirmation: convID=(${UUID}), stepIdx=(\\d{1,9}), approved=(?:true|false)$`
|
|
6533
|
+
);
|
|
6534
|
+
var ABSOLUTE = /^(?:\/|[A-Za-z]:[\\/])/;
|
|
6535
|
+
var UNPRINTABLE = /[\p{Cc}\p{Cf}]/u;
|
|
6536
|
+
function readAgyLogLine(line) {
|
|
6537
|
+
const glog = GLOG_LINE.exec(line);
|
|
6538
|
+
if (glog === null) return null;
|
|
6539
|
+
const source = glog[7];
|
|
6540
|
+
const message = glog[8];
|
|
6541
|
+
const opened = OPENED.exec(message);
|
|
6542
|
+
if (opened !== null) {
|
|
6543
|
+
const from = opened[1] === "Created" ? SOURCE2.created : SOURCE2.streaming;
|
|
6544
|
+
return source === from ? { kind: "opened", conversation: opened[2].toLowerCase() } : null;
|
|
6545
|
+
}
|
|
6546
|
+
if (source === SOURCE2.backend) {
|
|
6547
|
+
const backend = BACKEND.exec(message);
|
|
6548
|
+
return backend !== null && ABSOLUTE.test(backend[1]) && !UNPRINTABLE.test(backend[1]) ? { kind: "folder", folder: backend[1] } : null;
|
|
6549
|
+
}
|
|
6550
|
+
if (source !== SOURCE2.confirmation) return null;
|
|
6551
|
+
const asking = ASKING.exec(message);
|
|
6552
|
+
if (asking !== null) {
|
|
6553
|
+
const fraction = (glog[6] ?? "0").padEnd(3, "0").slice(0, 3);
|
|
6554
|
+
return {
|
|
6555
|
+
kind: "asking",
|
|
6556
|
+
tool: asking[1],
|
|
6557
|
+
step: Number(asking[2]),
|
|
6558
|
+
at: {
|
|
6559
|
+
month: Number(glog[1]),
|
|
6560
|
+
day: Number(glog[2]),
|
|
6561
|
+
hour: Number(glog[3]),
|
|
6562
|
+
minute: Number(glog[4]),
|
|
6563
|
+
second: Number(glog[5]),
|
|
6564
|
+
millisecond: Number(fraction)
|
|
6565
|
+
}
|
|
6566
|
+
};
|
|
6567
|
+
}
|
|
6568
|
+
const answered = ANSWERED.exec(message);
|
|
6569
|
+
if (answered !== null) {
|
|
6570
|
+
return { kind: "answered", conversation: answered[1].toLowerCase(), step: Number(answered[2]) };
|
|
6571
|
+
}
|
|
6572
|
+
return null;
|
|
6261
6573
|
}
|
|
6262
|
-
function
|
|
6263
|
-
return
|
|
6574
|
+
function emptyAgyLogState() {
|
|
6575
|
+
return { folder: null, folderNamed: false, opened: /* @__PURE__ */ new Set(), current: null, ask: null };
|
|
6264
6576
|
}
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
|
|
6268
|
-
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6577
|
+
function addAgyLogLine(state, line) {
|
|
6578
|
+
switch (line.kind) {
|
|
6579
|
+
case "folder":
|
|
6580
|
+
if (!state.folderNamed) state.folder = line.folder;
|
|
6581
|
+
state.folderNamed = true;
|
|
6582
|
+
return;
|
|
6583
|
+
case "opened":
|
|
6584
|
+
state.opened.add(line.conversation);
|
|
6585
|
+
if (state.current !== line.conversation) state.ask = null;
|
|
6586
|
+
state.current = line.conversation;
|
|
6587
|
+
return;
|
|
6588
|
+
case "asking":
|
|
6589
|
+
state.ask = state.current === null ? null : { conversation: state.current, step: line.step, tool: line.tool, at: line.at };
|
|
6590
|
+
return;
|
|
6591
|
+
case "answered":
|
|
6592
|
+
if (state.ask?.conversation === line.conversation && state.ask.step === line.step) {
|
|
6593
|
+
state.ask = null;
|
|
6594
|
+
}
|
|
6595
|
+
return;
|
|
6596
|
+
}
|
|
6272
6597
|
}
|
|
6273
|
-
var
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
|
|
6277
|
-
|
|
6278
|
-
|
|
6598
|
+
var ASK_STEPS_AHEAD = 2;
|
|
6599
|
+
function waitsForApproval(ask, conversation, lastIndex) {
|
|
6600
|
+
if (ask === null || ask.conversation !== conversation || lastIndex === null) return false;
|
|
6601
|
+
return ask.step > lastIndex && ask.step <= lastIndex + ASK_STEPS_AHEAD;
|
|
6602
|
+
}
|
|
6603
|
+
function agyLogTimeAt(time, near) {
|
|
6604
|
+
const year = new Date(near).getFullYear();
|
|
6605
|
+
let best = null;
|
|
6606
|
+
for (const y of [year - 1, year, year + 1]) {
|
|
6607
|
+
const at = new Date(
|
|
6608
|
+
y,
|
|
6609
|
+
time.month - 1,
|
|
6610
|
+
time.day,
|
|
6611
|
+
time.hour,
|
|
6612
|
+
time.minute,
|
|
6613
|
+
time.second,
|
|
6614
|
+
time.millisecond
|
|
6615
|
+
);
|
|
6616
|
+
if (at.getMonth() !== time.month - 1 || at.getDate() !== time.day) continue;
|
|
6617
|
+
const ms = at.getTime();
|
|
6618
|
+
if (best === null || Math.abs(ms - near) < Math.abs(best - near)) best = ms;
|
|
6279
6619
|
}
|
|
6280
|
-
|
|
6281
|
-
if (isRecord8(source)) return !Object.keys(source).some((kind) => NOT_A_SESSION_SOURCE.has(kind));
|
|
6282
|
-
return true;
|
|
6620
|
+
return best;
|
|
6283
6621
|
}
|
|
6284
6622
|
|
|
6285
|
-
// src/collector/adapters/
|
|
6286
|
-
|
|
6287
|
-
var
|
|
6288
|
-
var
|
|
6289
|
-
var
|
|
6623
|
+
// src/collector/adapters/antigravity/agyLogs.ts
|
|
6624
|
+
import path11 from "node:path";
|
|
6625
|
+
var LOG_DIR = "log";
|
|
6626
|
+
var LOG_LIMIT_BYTES = 4 * 1024 * 1024;
|
|
6627
|
+
var LINE_LIMIT_BYTES = 64 * 1024;
|
|
6628
|
+
var LOG_NAMED_WITHIN_MS = 1e4;
|
|
6629
|
+
var PS_LATE_MS = 2e3;
|
|
6630
|
+
var RELIST_MS = 1e4;
|
|
6631
|
+
var LOG_NAME = /^cli-(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})\.log$/;
|
|
6290
6632
|
var NEWLINE = 10;
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
if (
|
|
6294
|
-
const
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
}
|
|
6303
|
-
function text3(value) {
|
|
6304
|
-
return typeof value === "string" && value !== "" ? value : void 0;
|
|
6305
|
-
}
|
|
6306
|
-
function label(value) {
|
|
6307
|
-
return typeof value === "string" && value !== "" && value.length <= 64 ? value : void 0;
|
|
6308
|
-
}
|
|
6309
|
-
function compactSource(value) {
|
|
6310
|
-
if (typeof value === "string") return text3(value);
|
|
6311
|
-
if (!isRecord9(value)) return void 0;
|
|
6312
|
-
return Object.fromEntries(
|
|
6313
|
-
Object.entries(value).map(([kind, detail]) => [
|
|
6314
|
-
kind,
|
|
6315
|
-
typeof detail === "string" ? detail : true
|
|
6316
|
-
])
|
|
6633
|
+
function logStartOf(name) {
|
|
6634
|
+
const match = LOG_NAME.exec(name);
|
|
6635
|
+
if (match === null) return null;
|
|
6636
|
+
const [year, month, day, hour, minute, second] = match.slice(1).map(Number);
|
|
6637
|
+
const at = new Date(year, month - 1, day, hour, minute, second);
|
|
6638
|
+
if (at.getMonth() !== month - 1 || at.getDate() !== day || at.getHours() !== hour) return null;
|
|
6639
|
+
return at.getTime();
|
|
6640
|
+
}
|
|
6641
|
+
function matchLogs(processes, logs) {
|
|
6642
|
+
const candidates = (startedAt) => startedAt === null ? [] : [...logs].filter(
|
|
6643
|
+
([, at]) => at >= startedAt - PS_LATE_MS && at - startedAt <= LOG_NAMED_WITHIN_MS
|
|
6317
6644
|
);
|
|
6645
|
+
const owners = /* @__PURE__ */ new Map();
|
|
6646
|
+
for (const { startedAt } of processes) {
|
|
6647
|
+
for (const [name] of candidates(startedAt)) owners.set(name, (owners.get(name) ?? 0) + 1);
|
|
6648
|
+
}
|
|
6649
|
+
return processes.map(({ startedAt }) => {
|
|
6650
|
+
const found = candidates(startedAt);
|
|
6651
|
+
if (found.length !== 1) return null;
|
|
6652
|
+
const [name] = found[0];
|
|
6653
|
+
return owners.get(name) === 1 ? name : null;
|
|
6654
|
+
});
|
|
6655
|
+
}
|
|
6656
|
+
function createAgyLogReader(options) {
|
|
6657
|
+
const { io } = options;
|
|
6658
|
+
const now = options.now ?? Date.now;
|
|
6659
|
+
const logDir = path11.join(options.home, LOG_DIR);
|
|
6660
|
+
const cursors = /* @__PURE__ */ new Map();
|
|
6661
|
+
let listed = /* @__PURE__ */ new Map();
|
|
6662
|
+
let listedFor = "";
|
|
6663
|
+
let listedAt = -Infinity;
|
|
6664
|
+
async function list(processes) {
|
|
6665
|
+
const key = processes.map((p) => p.startedAt ?? "?").join(",");
|
|
6666
|
+
const since = now() - listedAt;
|
|
6667
|
+
if (key === listedFor && since >= 0 && since < RELIST_MS) return;
|
|
6668
|
+
listedFor = key;
|
|
6669
|
+
listedAt = now();
|
|
6670
|
+
let names;
|
|
6671
|
+
try {
|
|
6672
|
+
names = await io.readdir(logDir);
|
|
6673
|
+
} catch {
|
|
6674
|
+
listed = /* @__PURE__ */ new Map();
|
|
6675
|
+
return;
|
|
6676
|
+
}
|
|
6677
|
+
const next = /* @__PURE__ */ new Map();
|
|
6678
|
+
for (const name of names) {
|
|
6679
|
+
const at = logStartOf(name);
|
|
6680
|
+
if (at !== null) next.set(name, at);
|
|
6681
|
+
}
|
|
6682
|
+
listed = next;
|
|
6683
|
+
}
|
|
6684
|
+
function addLines(cursor, data) {
|
|
6685
|
+
let buffer = cursor.pending.byteLength > 0 ? Buffer.concat([cursor.pending, data]) : data;
|
|
6686
|
+
if (cursor.skipping) {
|
|
6687
|
+
const end = buffer.indexOf(NEWLINE);
|
|
6688
|
+
if (end === -1) return;
|
|
6689
|
+
buffer = buffer.subarray(end + 1);
|
|
6690
|
+
cursor.skipping = false;
|
|
6691
|
+
}
|
|
6692
|
+
let start = 0;
|
|
6693
|
+
let newline = buffer.indexOf(NEWLINE);
|
|
6694
|
+
while (newline !== -1) {
|
|
6695
|
+
const line = readAgyLogLine(buffer.toString("utf8", start, newline));
|
|
6696
|
+
if (line !== null) addAgyLogLine(cursor.state, line);
|
|
6697
|
+
start = newline + 1;
|
|
6698
|
+
newline = buffer.indexOf(NEWLINE, start);
|
|
6699
|
+
}
|
|
6700
|
+
const rest = buffer.subarray(start);
|
|
6701
|
+
cursor.skipping = rest.byteLength > LINE_LIMIT_BYTES;
|
|
6702
|
+
cursor.pending = cursor.skipping ? Buffer.alloc(0) : Buffer.from(rest);
|
|
6703
|
+
}
|
|
6704
|
+
async function readLog(file, info) {
|
|
6705
|
+
const known = cursors.get(file);
|
|
6706
|
+
if (known && known.ino === info.ino && known.size === info.size && known.mtimeMs === info.mtimeMs) {
|
|
6707
|
+
return known.state;
|
|
6708
|
+
}
|
|
6709
|
+
const handle = await io.openRegular(file);
|
|
6710
|
+
try {
|
|
6711
|
+
const opened = handle.info;
|
|
6712
|
+
const onward = known !== void 0 && known.ino === opened.ino && opened.size >= known.offset;
|
|
6713
|
+
const cursor = onward ? { ...known, state: known.state } : {
|
|
6714
|
+
ino: opened.ino,
|
|
6715
|
+
size: 0,
|
|
6716
|
+
mtimeMs: 0,
|
|
6717
|
+
offset: 0,
|
|
6718
|
+
pending: Buffer.alloc(0),
|
|
6719
|
+
skipping: false,
|
|
6720
|
+
state: emptyAgyLogState()
|
|
6721
|
+
};
|
|
6722
|
+
let from = cursor.offset;
|
|
6723
|
+
if (opened.size - from > LOG_LIMIT_BYTES) {
|
|
6724
|
+
from = opened.size - LOG_LIMIT_BYTES;
|
|
6725
|
+
cursor.state.ask = null;
|
|
6726
|
+
cursor.pending = Buffer.alloc(0);
|
|
6727
|
+
cursor.skipping = true;
|
|
6728
|
+
addLines(cursor, Buffer.from(await handle.read(from, opened.size - from)));
|
|
6729
|
+
} else if (opened.size > from) {
|
|
6730
|
+
addLines(cursor, Buffer.from(await handle.read(from, opened.size - from)));
|
|
6731
|
+
}
|
|
6732
|
+
cursor.offset = opened.size;
|
|
6733
|
+
cursor.size = opened.size;
|
|
6734
|
+
cursor.mtimeMs = opened.mtimeMs;
|
|
6735
|
+
cursor.ino = opened.ino;
|
|
6736
|
+
cursors.set(file, cursor);
|
|
6737
|
+
return cursor.state;
|
|
6738
|
+
} finally {
|
|
6739
|
+
await handle.close();
|
|
6740
|
+
}
|
|
6741
|
+
}
|
|
6742
|
+
async function read(processes) {
|
|
6743
|
+
if (processes.length === 0) {
|
|
6744
|
+
cursors.clear();
|
|
6745
|
+
return [];
|
|
6746
|
+
}
|
|
6747
|
+
await list(processes);
|
|
6748
|
+
const names = matchLogs(processes, listed);
|
|
6749
|
+
const kept = /* @__PURE__ */ new Set();
|
|
6750
|
+
const states = [];
|
|
6751
|
+
for (const name of names) {
|
|
6752
|
+
if (name === null) {
|
|
6753
|
+
states.push(null);
|
|
6754
|
+
continue;
|
|
6755
|
+
}
|
|
6756
|
+
const file = path11.join(logDir, name);
|
|
6757
|
+
kept.add(file);
|
|
6758
|
+
try {
|
|
6759
|
+
const info = await io.lstat(file);
|
|
6760
|
+
states.push(info.kind === "file" ? await readLog(file, info) : null);
|
|
6761
|
+
} catch {
|
|
6762
|
+
states.push(null);
|
|
6763
|
+
}
|
|
6764
|
+
}
|
|
6765
|
+
for (const file of cursors.keys()) {
|
|
6766
|
+
if (!kept.has(file)) cursors.delete(file);
|
|
6767
|
+
}
|
|
6768
|
+
return states;
|
|
6769
|
+
}
|
|
6770
|
+
return {
|
|
6771
|
+
async read(processes) {
|
|
6772
|
+
try {
|
|
6773
|
+
return await read(processes);
|
|
6774
|
+
} catch {
|
|
6775
|
+
return processes.map(() => null);
|
|
6776
|
+
}
|
|
6777
|
+
}
|
|
6778
|
+
};
|
|
6779
|
+
}
|
|
6780
|
+
|
|
6781
|
+
// src/collector/adapters/antigravity/agyProcesses.ts
|
|
6782
|
+
var AGY_TABLE_ARGS = ["-A", "-o", "pid=,ppid=,lstart=,comm="];
|
|
6783
|
+
function agyCommandLineArgs(pids) {
|
|
6784
|
+
return ["-o", "pid=,args=", "-p", pids.join(",")];
|
|
6785
|
+
}
|
|
6786
|
+
var AGY_COMMANDS = /* @__PURE__ */ new Set([
|
|
6787
|
+
"agent",
|
|
6788
|
+
"agents",
|
|
6789
|
+
"changelog",
|
|
6790
|
+
"help",
|
|
6791
|
+
"install",
|
|
6792
|
+
"mcp",
|
|
6793
|
+
"mic-serve",
|
|
6794
|
+
"models",
|
|
6795
|
+
"plugin",
|
|
6796
|
+
"plugins",
|
|
6797
|
+
"remote-control",
|
|
6798
|
+
"update"
|
|
6799
|
+
]);
|
|
6800
|
+
var NOT_A_SESSION_OPTIONS = /* @__PURE__ */ new Set([
|
|
6801
|
+
"-h",
|
|
6802
|
+
"-help",
|
|
6803
|
+
"--help",
|
|
6804
|
+
"-version",
|
|
6805
|
+
"--version"
|
|
6806
|
+
]);
|
|
6807
|
+
var UUID2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
6808
|
+
var TABLE_LINE = /^\s*(\d+)\s+(\d+)\s+([A-Z][a-z]{2}\s+[A-Z][a-z]{2}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}\s+\d{4})\s+(\S.*?)\s*$/;
|
|
6809
|
+
function isAgyProgram(program) {
|
|
6810
|
+
const name = program.split(/[\\/]/).pop()?.toLowerCase();
|
|
6811
|
+
return name === "agy" || name === "agy.exe";
|
|
6812
|
+
}
|
|
6813
|
+
function parseAgyTable(stdout) {
|
|
6814
|
+
const rows = [];
|
|
6815
|
+
for (const line of stdout.split("\n")) {
|
|
6816
|
+
const match = TABLE_LINE.exec(line);
|
|
6817
|
+
if (!match || !isAgyProgram(match[4])) continue;
|
|
6818
|
+
const start = match[3].replace(/\s+/g, " ");
|
|
6819
|
+
rows.push({
|
|
6820
|
+
pid: Number(match[1]),
|
|
6821
|
+
ppid: Number(match[2]),
|
|
6822
|
+
start,
|
|
6823
|
+
startedAt: processStartTime(start)
|
|
6824
|
+
});
|
|
6825
|
+
}
|
|
6826
|
+
return rows;
|
|
6827
|
+
}
|
|
6828
|
+
var PROGRAM_IN_LINE = /(?:^|[\\/])agy(?:\.exe)?(?=\s|$)/i;
|
|
6829
|
+
function readAgyCommandLine(line) {
|
|
6830
|
+
const program = PROGRAM_IN_LINE.exec(line);
|
|
6831
|
+
if (!program) return { session: true };
|
|
6832
|
+
const words = line.slice(program.index + program[0].length).split(/\s+/).filter((word2) => word2 !== "");
|
|
6833
|
+
const first = words[0];
|
|
6834
|
+
if (first !== void 0 && (AGY_COMMANDS.has(first) || NOT_A_SESSION_OPTIONS.has(first))) {
|
|
6835
|
+
return { session: false };
|
|
6836
|
+
}
|
|
6837
|
+
for (const [index, word2] of words.entries()) {
|
|
6838
|
+
let value;
|
|
6839
|
+
if (word2 === "--conversation" || word2 === "-conversation") value = words[index + 1];
|
|
6840
|
+
else if (word2.startsWith("--conversation=")) value = word2.slice("--conversation=".length);
|
|
6841
|
+
else if (word2.startsWith("-conversation=")) value = word2.slice("-conversation=".length);
|
|
6842
|
+
else continue;
|
|
6843
|
+
if (value !== void 0 && UUID2.test(value)) {
|
|
6844
|
+
return { session: true, conversation: value.toLowerCase() };
|
|
6845
|
+
}
|
|
6846
|
+
}
|
|
6847
|
+
return { session: true };
|
|
6848
|
+
}
|
|
6849
|
+
function parseCommandLines(stdout) {
|
|
6850
|
+
const lines = /* @__PURE__ */ new Map();
|
|
6851
|
+
for (const line of stdout.split("\n")) {
|
|
6852
|
+
const match = /^\s*(\d+)\s(.*)$/.exec(line);
|
|
6853
|
+
if (match) lines.set(Number(match[1]), match[2]);
|
|
6854
|
+
}
|
|
6855
|
+
return lines;
|
|
6856
|
+
}
|
|
6857
|
+
function createAgyProcessReader(options = {}) {
|
|
6858
|
+
const run = options.run ?? runPs;
|
|
6859
|
+
const platform = options.platform ?? process.platform;
|
|
6860
|
+
const known = /* @__PURE__ */ new Map();
|
|
6861
|
+
async function read() {
|
|
6862
|
+
if (platform === "win32") return { ok: false };
|
|
6863
|
+
const table = await run(AGY_TABLE_ARGS, { utc: true });
|
|
6864
|
+
if (!table.ok) return { ok: false };
|
|
6865
|
+
const rows = parseAgyTable(table.stdout);
|
|
6866
|
+
const keyOf = (row) => `${row.pid} ${row.start}`;
|
|
6867
|
+
const current = new Set(rows.map(keyOf));
|
|
6868
|
+
for (const key of known.keys()) {
|
|
6869
|
+
if (!current.has(key)) known.delete(key);
|
|
6870
|
+
}
|
|
6871
|
+
const unseen = rows.filter((row) => !known.has(keyOf(row)));
|
|
6872
|
+
const ended = /* @__PURE__ */ new Set();
|
|
6873
|
+
if (unseen.length > 0) {
|
|
6874
|
+
const answer = await run(agyCommandLineArgs(unseen.map((row) => row.pid)));
|
|
6875
|
+
const lines = parseCommandLines(answer.stdout);
|
|
6876
|
+
for (const row of unseen) {
|
|
6877
|
+
const line = lines.get(row.pid);
|
|
6878
|
+
if (line !== void 0) known.set(keyOf(row), readAgyCommandLine(line));
|
|
6879
|
+
else if (answer.ok || lines.size > 0) ended.add(row.pid);
|
|
6880
|
+
}
|
|
6881
|
+
}
|
|
6882
|
+
const running = /* @__PURE__ */ new Map();
|
|
6883
|
+
for (const row of rows) {
|
|
6884
|
+
if (!ended.has(row.pid)) running.set(row.pid, row);
|
|
6885
|
+
}
|
|
6886
|
+
const lineOf2 = (row) => known.get(keyOf(row)) ?? { session: true };
|
|
6887
|
+
const underSession = (row) => {
|
|
6888
|
+
const seen = /* @__PURE__ */ new Set([row.pid]);
|
|
6889
|
+
let parent = running.get(row.ppid);
|
|
6890
|
+
while (parent !== void 0 && !seen.has(parent.pid)) {
|
|
6891
|
+
if (lineOf2(parent).session) return true;
|
|
6892
|
+
seen.add(parent.pid);
|
|
6893
|
+
parent = running.get(parent.ppid);
|
|
6894
|
+
}
|
|
6895
|
+
return false;
|
|
6896
|
+
};
|
|
6897
|
+
const sessions = [];
|
|
6898
|
+
for (const row of running.values()) {
|
|
6899
|
+
const line = lineOf2(row);
|
|
6900
|
+
if (!line.session || underSession(row)) continue;
|
|
6901
|
+
sessions.push(
|
|
6902
|
+
line.conversation === void 0 ? { startedAt: row.startedAt } : { startedAt: row.startedAt, conversation: line.conversation }
|
|
6903
|
+
);
|
|
6904
|
+
}
|
|
6905
|
+
return { ok: true, sessions };
|
|
6906
|
+
}
|
|
6907
|
+
return {
|
|
6908
|
+
async read() {
|
|
6909
|
+
try {
|
|
6910
|
+
return await read();
|
|
6911
|
+
} catch {
|
|
6912
|
+
return { ok: false };
|
|
6913
|
+
}
|
|
6914
|
+
}
|
|
6915
|
+
};
|
|
6916
|
+
}
|
|
6917
|
+
|
|
6918
|
+
// src/collector/adapters/antigravity/conversations.ts
|
|
6919
|
+
import path12 from "node:path";
|
|
6920
|
+
var BRAIN_DIR = "brain";
|
|
6921
|
+
var CONVERSATIONS_DIR = "conversations";
|
|
6922
|
+
var TRANSCRIPT_IN_FOLDER = [".system_generated", "logs", "transcript.jsonl"];
|
|
6923
|
+
var OLD_REFRESH_MS = 3e4;
|
|
6924
|
+
var UUID3 = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
|
|
6925
|
+
var FOLDER_NAME = new RegExp(`^(${UUID3})$`, "i");
|
|
6926
|
+
var DATABASE_NAME = new RegExp(`^(${UUID3})\\.db(?:-wal)?$`, "i");
|
|
6927
|
+
function conversationIdOfFolder(name) {
|
|
6928
|
+
return FOLDER_NAME.exec(name)?.[1]?.toLowerCase() ?? null;
|
|
6929
|
+
}
|
|
6930
|
+
function conversationIdOfDatabase(name) {
|
|
6931
|
+
return DATABASE_NAME.exec(name)?.[1]?.toLowerCase() ?? null;
|
|
6932
|
+
}
|
|
6933
|
+
function createConversationFinder(options) {
|
|
6934
|
+
const { io } = options;
|
|
6935
|
+
const brainDir = path12.join(options.home, BRAIN_DIR);
|
|
6936
|
+
const databaseDir = path12.join(options.home, CONVERSATIONS_DIR);
|
|
6937
|
+
const oldRefreshMs = options.oldRefreshMs ?? OLD_REFRESH_MS;
|
|
6938
|
+
const known = /* @__PURE__ */ new Map();
|
|
6939
|
+
let oldCheckedAt = null;
|
|
6940
|
+
async function lookAt(id) {
|
|
6941
|
+
const transcript = path12.join(brainDir, id, ...TRANSCRIPT_IN_FOLDER);
|
|
6942
|
+
const infos = await Promise.all(
|
|
6943
|
+
[transcript, path12.join(databaseDir, `${id}.db`), path12.join(databaseDir, `${id}.db-wal`)].map(
|
|
6944
|
+
(file) => io.lstat(file).catch(() => null)
|
|
6945
|
+
)
|
|
6946
|
+
);
|
|
6947
|
+
const transcriptInfo = infos[0]?.kind === "file" ? infos[0] : null;
|
|
6948
|
+
const writtenAt = transcriptInfo === null ? null : Math.floor(transcriptInfo.mtimeMs);
|
|
6949
|
+
let lastWriteAt = null;
|
|
6950
|
+
for (const info of infos) {
|
|
6951
|
+
if (info?.kind !== "file") continue;
|
|
6952
|
+
const at = Math.floor(info.mtimeMs);
|
|
6953
|
+
lastWriteAt = lastWriteAt === null ? at : Math.max(lastWriteAt, at);
|
|
6954
|
+
}
|
|
6955
|
+
return { id, transcript, transcriptInfo, writtenAt, lastWriteAt };
|
|
6956
|
+
}
|
|
6957
|
+
return {
|
|
6958
|
+
async list(checkedAt, mayBeOpen) {
|
|
6959
|
+
let folders;
|
|
6960
|
+
try {
|
|
6961
|
+
folders = await io.readdir(brainDir);
|
|
6962
|
+
} catch (error) {
|
|
6963
|
+
return { state: isMissing2(error) ? "missing" : "unreadable" };
|
|
6964
|
+
}
|
|
6965
|
+
const databases = await io.readdir(databaseDir).catch(() => []);
|
|
6966
|
+
const ids = /* @__PURE__ */ new Set();
|
|
6967
|
+
for (const name of folders) {
|
|
6968
|
+
const id = conversationIdOfFolder(name);
|
|
6969
|
+
if (id !== null) ids.add(id);
|
|
6970
|
+
}
|
|
6971
|
+
for (const name of databases) {
|
|
6972
|
+
const id = conversationIdOfDatabase(name);
|
|
6973
|
+
if (id !== null) ids.add(id);
|
|
6974
|
+
}
|
|
6975
|
+
for (const id of known.keys()) {
|
|
6976
|
+
if (!ids.has(id)) known.delete(id);
|
|
6977
|
+
}
|
|
6978
|
+
const since = oldCheckedAt === null ? Infinity : checkedAt - oldCheckedAt;
|
|
6979
|
+
const refreshOld = since < 0 || since >= oldRefreshMs;
|
|
6980
|
+
if (refreshOld) oldCheckedAt = checkedAt;
|
|
6981
|
+
const cutoff = checkedAt - FINISHED_RETENTION_MS;
|
|
6982
|
+
await Promise.all(
|
|
6983
|
+
[...ids].map(async (id) => {
|
|
6984
|
+
const before = known.get(id);
|
|
6985
|
+
const recent = before === void 0 || before.lastWriteAt !== null && before.lastWriteAt >= cutoff || mayBeOpen(id);
|
|
6986
|
+
if (recent || refreshOld) known.set(id, await lookAt(id));
|
|
6987
|
+
})
|
|
6988
|
+
);
|
|
6989
|
+
const conversations = [...known.values()].sort((a, b) => a.id < b.id ? -1 : 1);
|
|
6990
|
+
return { state: "ok", conversations };
|
|
6991
|
+
}
|
|
6992
|
+
};
|
|
6993
|
+
}
|
|
6994
|
+
function writtenRecently(files, now) {
|
|
6995
|
+
return files.writtenAt !== null && isWithinRetention(files.writtenAt, now);
|
|
6996
|
+
}
|
|
6997
|
+
|
|
6998
|
+
// src/collector/adapters/antigravity/annotations.ts
|
|
6999
|
+
import path13 from "node:path";
|
|
7000
|
+
var ANNOTATIONS_DIR = "annotations";
|
|
7001
|
+
var ANNOTATION_LIMIT_BYTES = 4 * 1024;
|
|
7002
|
+
var TITLE_FIELD = /^title[ \t]*:[ \t]*"((?:[^"\\\n]|\\.)*)"[ \t]*$/m;
|
|
7003
|
+
var SIMPLE_ESCAPES = {
|
|
7004
|
+
n: 10,
|
|
7005
|
+
r: 13,
|
|
7006
|
+
t: 9,
|
|
7007
|
+
'"': 34,
|
|
7008
|
+
"'": 39,
|
|
7009
|
+
"\\": 92,
|
|
7010
|
+
"?": 63,
|
|
7011
|
+
a: 7,
|
|
7012
|
+
b: 8,
|
|
7013
|
+
f: 12,
|
|
7014
|
+
v: 11
|
|
7015
|
+
};
|
|
7016
|
+
function unescapeTextFormat(body) {
|
|
7017
|
+
const chars = Array.from(body);
|
|
7018
|
+
const bytes3 = [];
|
|
7019
|
+
for (let i = 0; i < chars.length; i += 1) {
|
|
7020
|
+
const char = chars[i];
|
|
7021
|
+
if (char !== "\\") {
|
|
7022
|
+
bytes3.push(...Buffer.from(char, "utf8"));
|
|
7023
|
+
continue;
|
|
7024
|
+
}
|
|
7025
|
+
const next = chars[i + 1];
|
|
7026
|
+
if (next === void 0) return null;
|
|
7027
|
+
const octal = /^[0-7]{1,3}/.exec(chars.slice(i + 1, i + 4).join(""));
|
|
7028
|
+
if (octal !== null) {
|
|
7029
|
+
const value = parseInt(octal[0], 8);
|
|
7030
|
+
if (value > 255) return null;
|
|
7031
|
+
bytes3.push(value);
|
|
7032
|
+
i += octal[0].length;
|
|
7033
|
+
continue;
|
|
7034
|
+
}
|
|
7035
|
+
if (next === "u" || next === "U") {
|
|
7036
|
+
const digits = next === "u" ? 4 : 8;
|
|
7037
|
+
const hex = chars.slice(i + 2, i + 2 + digits).join("");
|
|
7038
|
+
if (!new RegExp(`^[0-9a-fA-F]{${digits}}$`).test(hex)) return null;
|
|
7039
|
+
const point = parseInt(hex, 16);
|
|
7040
|
+
if (point > 1114111 || point >= 55296 && point <= 57343) return null;
|
|
7041
|
+
bytes3.push(...Buffer.from(String.fromCodePoint(point), "utf8"));
|
|
7042
|
+
i += 1 + digits;
|
|
7043
|
+
continue;
|
|
7044
|
+
}
|
|
7045
|
+
if (next === "x" || next === "X") {
|
|
7046
|
+
const hex = /^[0-9a-fA-F]{1,2}/.exec(chars.slice(i + 2, i + 4).join(""));
|
|
7047
|
+
if (hex === null) return null;
|
|
7048
|
+
bytes3.push(parseInt(hex[0], 16));
|
|
7049
|
+
i += 1 + hex[0].length;
|
|
7050
|
+
continue;
|
|
7051
|
+
}
|
|
7052
|
+
const simple = SIMPLE_ESCAPES[next];
|
|
7053
|
+
if (simple === void 0) return null;
|
|
7054
|
+
bytes3.push(simple);
|
|
7055
|
+
i += 1;
|
|
7056
|
+
}
|
|
7057
|
+
try {
|
|
7058
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(Uint8Array.from(bytes3));
|
|
7059
|
+
} catch {
|
|
7060
|
+
return null;
|
|
7061
|
+
}
|
|
7062
|
+
}
|
|
7063
|
+
function readTitle(content) {
|
|
7064
|
+
const field2 = TITLE_FIELD.exec(content);
|
|
7065
|
+
if (field2 === null) return null;
|
|
7066
|
+
const title = unescapeTextFormat(field2[1]);
|
|
7067
|
+
return title === null ? null : sessionName(title) ?? null;
|
|
7068
|
+
}
|
|
7069
|
+
function createTitleReader(options) {
|
|
7070
|
+
const { io } = options;
|
|
7071
|
+
const dir = path13.join(options.home, ANNOTATIONS_DIR);
|
|
7072
|
+
const cache = /* @__PURE__ */ new Map();
|
|
7073
|
+
async function read(id) {
|
|
7074
|
+
const file = path13.join(dir, `${id}.pbtxt`);
|
|
7075
|
+
const info = await io.lstat(file);
|
|
7076
|
+
const known = cache.get(id);
|
|
7077
|
+
if (known && known.ino === info.ino && known.size === info.size && known.mtimeMs === info.mtimeMs) {
|
|
7078
|
+
return known.title;
|
|
7079
|
+
}
|
|
7080
|
+
let title = null;
|
|
7081
|
+
if (info.kind === "file" && info.size <= ANNOTATION_LIMIT_BYTES) {
|
|
7082
|
+
const handle = await io.openRegular(file);
|
|
7083
|
+
try {
|
|
7084
|
+
if (handle.info.size <= ANNOTATION_LIMIT_BYTES) {
|
|
7085
|
+
const data = await handle.read(0, handle.info.size);
|
|
7086
|
+
title = readTitle(Buffer.from(data).toString("utf8"));
|
|
7087
|
+
}
|
|
7088
|
+
} finally {
|
|
7089
|
+
await handle.close();
|
|
7090
|
+
}
|
|
7091
|
+
}
|
|
7092
|
+
cache.set(id, { ino: info.ino, size: info.size, mtimeMs: info.mtimeMs, title });
|
|
7093
|
+
return title;
|
|
7094
|
+
}
|
|
7095
|
+
return {
|
|
7096
|
+
async read(id) {
|
|
7097
|
+
try {
|
|
7098
|
+
return await read(id);
|
|
7099
|
+
} catch {
|
|
7100
|
+
cache.delete(id);
|
|
7101
|
+
return null;
|
|
7102
|
+
}
|
|
7103
|
+
},
|
|
7104
|
+
keepOnly(ids) {
|
|
7105
|
+
for (const id of cache.keys()) {
|
|
7106
|
+
if (!ids.has(id)) cache.delete(id);
|
|
7107
|
+
}
|
|
7108
|
+
}
|
|
7109
|
+
};
|
|
7110
|
+
}
|
|
7111
|
+
|
|
7112
|
+
// src/core/mapping/antigravityMapping.ts
|
|
7113
|
+
var ANTIGRAVITY_STEP_TYPES = [
|
|
7114
|
+
"UNSPECIFIED",
|
|
7115
|
+
"DUMMY",
|
|
7116
|
+
"FINISH",
|
|
7117
|
+
"PLAN_INPUT",
|
|
7118
|
+
"MQUERY",
|
|
7119
|
+
"CODE_ACTION",
|
|
7120
|
+
"GIT_COMMIT",
|
|
7121
|
+
"GREP_SEARCH",
|
|
7122
|
+
"VIEW_FILE",
|
|
7123
|
+
"LIST_DIRECTORY",
|
|
7124
|
+
"COMPILE",
|
|
7125
|
+
"VIEW_CODE_ITEM",
|
|
7126
|
+
"USER_INPUT",
|
|
7127
|
+
"PLANNER_RESPONSE",
|
|
7128
|
+
"ERROR_MESSAGE",
|
|
7129
|
+
"RUN_COMMAND",
|
|
7130
|
+
"CHECKPOINT",
|
|
7131
|
+
"PROPOSE_CODE",
|
|
7132
|
+
"FIND",
|
|
7133
|
+
"SUGGESTED_RESPONSES",
|
|
7134
|
+
"COMMAND_STATUS",
|
|
7135
|
+
"MEMORY",
|
|
7136
|
+
"READ_URL_CONTENT",
|
|
7137
|
+
"VIEW_CONTENT_CHUNK",
|
|
7138
|
+
"SEARCH_WEB",
|
|
7139
|
+
"RETRIEVE_MEMORY",
|
|
7140
|
+
"MCP_TOOL",
|
|
7141
|
+
"MANAGER_FEEDBACK",
|
|
7142
|
+
"TOOL_CALL_PROPOSAL",
|
|
7143
|
+
"TOOL_CALL_CHOICE",
|
|
7144
|
+
"TRAJECTORY_CHOICE",
|
|
7145
|
+
"CLIPBOARD",
|
|
7146
|
+
"VIEW_FILE_OUTLINE",
|
|
7147
|
+
"POST_PR_REVIEW",
|
|
7148
|
+
"LIST_RESOURCES",
|
|
7149
|
+
"READ_RESOURCE",
|
|
7150
|
+
"LINT_DIFF",
|
|
7151
|
+
"FIND_ALL_REFERENCES",
|
|
7152
|
+
"BRAIN_UPDATE",
|
|
7153
|
+
"OPEN_BROWSER_URL",
|
|
7154
|
+
"RUN_EXTENSION_CODE",
|
|
7155
|
+
"PROPOSAL_FEEDBACK",
|
|
7156
|
+
"TRAJECTORY_SEARCH",
|
|
7157
|
+
"EXECUTE_BROWSER_JAVASCRIPT",
|
|
7158
|
+
"LIST_BROWSER_PAGES",
|
|
7159
|
+
"CAPTURE_BROWSER_SCREENSHOT",
|
|
7160
|
+
"CLICK_BROWSER_PIXEL",
|
|
7161
|
+
"READ_TERMINAL",
|
|
7162
|
+
"CAPTURE_BROWSER_CONSOLE_LOGS",
|
|
7163
|
+
"READ_BROWSER_PAGE",
|
|
7164
|
+
"BROWSER_GET_DOM",
|
|
7165
|
+
"CODE_SEARCH",
|
|
7166
|
+
"BROWSER_INPUT",
|
|
7167
|
+
"BROWSER_MOVE_MOUSE",
|
|
7168
|
+
"BROWSER_SELECT_OPTION",
|
|
7169
|
+
"BROWSER_SCROLL_UP",
|
|
7170
|
+
"BROWSER_SCROLL_DOWN",
|
|
7171
|
+
"BROWSER_CLICK_ELEMENT",
|
|
7172
|
+
"BROWSER_PRESS_KEY",
|
|
7173
|
+
"TASK_BOUNDARY",
|
|
7174
|
+
"NOTIFY_USER",
|
|
7175
|
+
"CODE_ACKNOWLEDGEMENT",
|
|
7176
|
+
"INTERNAL_SEARCH",
|
|
7177
|
+
"BROWSER_SUBAGENT",
|
|
7178
|
+
"FILE_CHANGE",
|
|
7179
|
+
"MOVE",
|
|
7180
|
+
"BROWSER_SCROLL",
|
|
7181
|
+
"KNOWLEDGE_GENERATION",
|
|
7182
|
+
"EPHEMERAL_MESSAGE",
|
|
7183
|
+
"GENERATE_IMAGE",
|
|
7184
|
+
"DELETE_DIRECTORY",
|
|
7185
|
+
"COMPILE_APPLET",
|
|
7186
|
+
"INSTALL_APPLET_DEPENDENCIES",
|
|
7187
|
+
"INSTALL_APPLET_PACKAGE",
|
|
7188
|
+
"BROWSER_RESIZE_WINDOW",
|
|
7189
|
+
"BROWSER_DRAG_PIXEL_TO_PIXEL",
|
|
7190
|
+
"CONVERSATION_HISTORY",
|
|
7191
|
+
"KNOWLEDGE_ARTIFACTS",
|
|
7192
|
+
"SEND_COMMAND_INPUT",
|
|
7193
|
+
"SYSTEM_MESSAGE",
|
|
7194
|
+
"WAIT",
|
|
7195
|
+
"AGENCY_TOOL_CALL",
|
|
7196
|
+
"CIDER_AGENT_DUMMY",
|
|
7197
|
+
"BUILD_CLEANER",
|
|
7198
|
+
"BLAZE_BUILD_TARGETS",
|
|
7199
|
+
"BLAZE_TEST_TARGETS",
|
|
7200
|
+
"SET_UP_FIREBASE",
|
|
7201
|
+
"MOMA",
|
|
7202
|
+
"RESTART_DEV_SERVER",
|
|
7203
|
+
"DEPLOY_FIREBASE",
|
|
7204
|
+
"SHELL_EXEC",
|
|
7205
|
+
"BROWSER_MOUSE_WHEEL",
|
|
7206
|
+
"LINT_APPLET",
|
|
7207
|
+
"KI_INSERTION",
|
|
7208
|
+
"RETRIEVE_CONTENT",
|
|
7209
|
+
"CRITIQUE",
|
|
7210
|
+
"FINDINGS",
|
|
7211
|
+
"BROWSER_MOUSE_UP",
|
|
7212
|
+
"BROWSER_MOUSE_DOWN",
|
|
7213
|
+
"WORKSPACE_API",
|
|
7214
|
+
"BROWSER_LIST_NETWORK_REQUESTS",
|
|
7215
|
+
"BROWSER_GET_NETWORK_REQUEST",
|
|
7216
|
+
"BROWSER_REFRESH_PAGE",
|
|
7217
|
+
"EDIT_NOTEBOOK",
|
|
7218
|
+
"INVOKE_SUBAGENT",
|
|
7219
|
+
"WRITE_BLOB",
|
|
7220
|
+
"READ_NOTEBOOK",
|
|
7221
|
+
"PROPOSE_AI_COMMENTS",
|
|
7222
|
+
"START_CODE_REVIEW",
|
|
7223
|
+
"GENERIC",
|
|
7224
|
+
"SET_UP_CLOUD_SQL",
|
|
7225
|
+
"EXECUTE_NOTEBOOK",
|
|
7226
|
+
"CLOUD_SQL_UPDATE_SCHEMA",
|
|
7227
|
+
"RPC_ACTION",
|
|
7228
|
+
"CLOUD_SQL_EXECUTE_SQL",
|
|
7229
|
+
"ASK_QUESTION",
|
|
7230
|
+
"DIRECTORY_RULES",
|
|
7231
|
+
"TOOL_SEARCH"
|
|
7232
|
+
];
|
|
7233
|
+
var ANTIGRAVITY_STEP_STATUSES = [
|
|
7234
|
+
"UNSPECIFIED",
|
|
7235
|
+
"PENDING",
|
|
7236
|
+
"RUNNING",
|
|
7237
|
+
"DONE",
|
|
7238
|
+
"INVALID",
|
|
7239
|
+
"CLEARED",
|
|
7240
|
+
"CANCELED",
|
|
7241
|
+
"ERROR",
|
|
7242
|
+
"GENERATING",
|
|
7243
|
+
"WAITING",
|
|
7244
|
+
"QUEUED",
|
|
7245
|
+
"INTERRUPTED"
|
|
7246
|
+
];
|
|
7247
|
+
var KNOWN_TYPES = new Set(ANTIGRAVITY_STEP_TYPES);
|
|
7248
|
+
var KNOWN_STATUSES = new Set(ANTIGRAVITY_STEP_STATUSES);
|
|
7249
|
+
var TYPE_PREFIX = "CORTEX_STEP_TYPE_";
|
|
7250
|
+
var STATUS_PREFIX = "CORTEX_STEP_STATUS_";
|
|
7251
|
+
var NAME = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
7252
|
+
function nameOf(value, prefix) {
|
|
7253
|
+
if (typeof value !== "string") return null;
|
|
7254
|
+
const upper = value.trim().toUpperCase();
|
|
7255
|
+
const name = upper.startsWith(prefix) ? upper.slice(prefix.length) : upper;
|
|
7256
|
+
return NAME.test(name) ? name : null;
|
|
7257
|
+
}
|
|
7258
|
+
function stepTypeName(value) {
|
|
7259
|
+
return nameOf(value, TYPE_PREFIX);
|
|
7260
|
+
}
|
|
7261
|
+
function stepStatusName(value) {
|
|
7262
|
+
return nameOf(value, STATUS_PREFIX);
|
|
7263
|
+
}
|
|
7264
|
+
var ASIDE_TYPES = /* @__PURE__ */ new Set([
|
|
7265
|
+
"DUMMY",
|
|
7266
|
+
"CIDER_AGENT_DUMMY",
|
|
7267
|
+
"CHECKPOINT",
|
|
7268
|
+
"EPHEMERAL_MESSAGE",
|
|
7269
|
+
"CONVERSATION_HISTORY",
|
|
7270
|
+
"SYSTEM_MESSAGE",
|
|
7271
|
+
"SUGGESTED_RESPONSES",
|
|
7272
|
+
"KNOWLEDGE_ARTIFACTS",
|
|
7273
|
+
"KNOWLEDGE_GENERATION",
|
|
7274
|
+
"KI_INSERTION",
|
|
7275
|
+
"DIRECTORY_RULES",
|
|
7276
|
+
"BRAIN_UPDATE"
|
|
7277
|
+
]);
|
|
7278
|
+
var GONE_STATUSES = /* @__PURE__ */ new Set(["CLEARED", "INVALID"]);
|
|
7279
|
+
var UNDER_WAY = /* @__PURE__ */ new Set([
|
|
7280
|
+
"PENDING",
|
|
7281
|
+
"RUNNING",
|
|
7282
|
+
"GENERATING",
|
|
7283
|
+
"QUEUED",
|
|
7284
|
+
"WAITING"
|
|
7285
|
+
]);
|
|
7286
|
+
var STOPPED = /* @__PURE__ */ new Set(["CANCELED", "INTERRUPTED"]);
|
|
7287
|
+
var TURN_OVER = /* @__PURE__ */ new Set(["FINISH"]);
|
|
7288
|
+
function isAsideStep(step) {
|
|
7289
|
+
return step.type !== null && ASIDE_TYPES.has(step.type) || step.status !== null && GONE_STATUSES.has(step.status);
|
|
7290
|
+
}
|
|
7291
|
+
function openStatusOf(step) {
|
|
7292
|
+
const { type, status } = step;
|
|
7293
|
+
if (type === "ERROR_MESSAGE") return "failed";
|
|
7294
|
+
if (status === null || !KNOWN_STATUSES.has(status)) return "unknown";
|
|
7295
|
+
if (UNDER_WAY.has(status)) return "working";
|
|
7296
|
+
if (STOPPED.has(status)) return "idle";
|
|
7297
|
+
if (type === null || !KNOWN_TYPES.has(type) || type === "UNSPECIFIED") return "unknown";
|
|
7298
|
+
if (status === "ERROR") return type === "PLANNER_RESPONSE" ? "failed" : "working";
|
|
7299
|
+
if (status !== "DONE") return "unknown";
|
|
7300
|
+
if (type === "PLANNER_RESPONSE") return step.toolCalls ? "working" : "idle";
|
|
7301
|
+
if (TURN_OVER.has(type)) return "idle";
|
|
7302
|
+
return "working";
|
|
7303
|
+
}
|
|
7304
|
+
function mapAntigravityStatus(fields) {
|
|
7305
|
+
const { last, live: live2 } = fields;
|
|
7306
|
+
if (last === void 0) return "unknown";
|
|
7307
|
+
const open2 = last === null ? "idle" : openStatusOf(last);
|
|
7308
|
+
if (open2 === "failed" || open2 === "unknown") return open2;
|
|
7309
|
+
if (live2 === false) return "finished";
|
|
7310
|
+
return fields.asking === true ? "needs-you" : open2;
|
|
7311
|
+
}
|
|
7312
|
+
|
|
7313
|
+
// src/collector/adapters/antigravity/toSession.ts
|
|
7314
|
+
var SOURCE_ID2 = "antigravity-cli";
|
|
7315
|
+
var WRITE_ORDER_SLACK_MS2 = 1e3;
|
|
7316
|
+
function antigravitySession(input) {
|
|
7317
|
+
const { conversationId, state, live: live2, now } = input;
|
|
7318
|
+
const id = `${SOURCE_ID2}:${conversationId}`;
|
|
7319
|
+
const waiting = input.waiting ?? null;
|
|
7320
|
+
const status = mapAntigravityStatus({ last: state.last, live: live2, asking: waiting !== null });
|
|
7321
|
+
const startedAt = plausibleTime(state.firstAt, now);
|
|
7322
|
+
let statusSince = null;
|
|
7323
|
+
if (status === "working" || status === "idle" || status === "failed") {
|
|
7324
|
+
statusSince = plausibleTime(state.since, now);
|
|
7325
|
+
} else if (status === "needs-you") {
|
|
7326
|
+
statusSince = plausibleTime(waiting?.since ?? null, now) ?? plausibleTime(state.lastAt, now);
|
|
7327
|
+
} else if (status === "finished") {
|
|
7328
|
+
statusSince = plausibleTime(state.lastAt, now);
|
|
7329
|
+
}
|
|
7330
|
+
if (statusSince !== null && startedAt !== null && statusSince < startedAt - WRITE_ORDER_SLACK_MS2) {
|
|
7331
|
+
statusSince = null;
|
|
7332
|
+
}
|
|
7333
|
+
const cwd = input.folder ?? null;
|
|
7334
|
+
const project = projectOf(cwd);
|
|
7335
|
+
const session = {
|
|
7336
|
+
id,
|
|
7337
|
+
source: SOURCE_ID2,
|
|
7338
|
+
surface: "terminal",
|
|
7339
|
+
name: input.title ?? project ?? sessionName(conversationId) ?? id,
|
|
7340
|
+
cwd,
|
|
7341
|
+
project,
|
|
7342
|
+
status,
|
|
7343
|
+
startedAt,
|
|
7344
|
+
statusSince,
|
|
7345
|
+
links: {},
|
|
7346
|
+
stale: isStale({ status, statusSince }, now)
|
|
7347
|
+
};
|
|
7348
|
+
if (status === "needs-you" && waiting !== null) {
|
|
7349
|
+
session.waitingReason = "permission";
|
|
7350
|
+
session.waitingDetail = waiting.tool;
|
|
7351
|
+
}
|
|
7352
|
+
const writtenAt = plausibleTime(input.writtenAt, now);
|
|
7353
|
+
const beforeStart = writtenAt !== null && startedAt !== null && writtenAt < startedAt - WRITE_ORDER_SLACK_MS2;
|
|
7354
|
+
if (writtenAt !== null && !beforeStart) session.lastWriteAt = writtenAt;
|
|
7355
|
+
return session;
|
|
7356
|
+
}
|
|
7357
|
+
|
|
7358
|
+
// src/collector/adapters/antigravity/transcriptFile.ts
|
|
7359
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
7360
|
+
var HEAD_LIMIT_BYTES = 256 * 1024;
|
|
7361
|
+
var TAIL_LIMIT_BYTES = 2 * 1024 * 1024;
|
|
7362
|
+
var TAIL_CHUNK_BYTES = 64 * 1024;
|
|
7363
|
+
var HEAD_HASH_BYTES = 4 * 1024;
|
|
7364
|
+
var NEWLINE2 = 10;
|
|
7365
|
+
var ISO_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
7366
|
+
function parseStepTime(value) {
|
|
7367
|
+
if (typeof value !== "string" || !ISO_TIME.test(value)) return null;
|
|
7368
|
+
const at = Date.parse(value);
|
|
7369
|
+
return Number.isSafeInteger(at) ? at : null;
|
|
7370
|
+
}
|
|
7371
|
+
function isRecord8(value) {
|
|
7372
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7373
|
+
}
|
|
7374
|
+
function readStepLine(line) {
|
|
7375
|
+
let value;
|
|
7376
|
+
try {
|
|
7377
|
+
value = JSON.parse(line);
|
|
7378
|
+
} catch {
|
|
7379
|
+
return null;
|
|
7380
|
+
}
|
|
7381
|
+
if (!isRecord8(value)) return null;
|
|
7382
|
+
const type = stepTypeName(value.type);
|
|
7383
|
+
const status = stepStatusName(value.status);
|
|
7384
|
+
if (type === null && status === null) return null;
|
|
7385
|
+
const index = value.step_index;
|
|
7386
|
+
return {
|
|
7387
|
+
index: Number.isSafeInteger(index) && index >= 0 ? index : null,
|
|
7388
|
+
type,
|
|
7389
|
+
status,
|
|
7390
|
+
toolCalls: Array.isArray(value.tool_calls) && value.tool_calls.length > 0,
|
|
7391
|
+
at: parseStepTime(value.created_at)
|
|
7392
|
+
};
|
|
7393
|
+
}
|
|
7394
|
+
function emptyState() {
|
|
7395
|
+
return { firstAt: null, last: void 0, since: null, lastAt: null, lastIndex: null };
|
|
7396
|
+
}
|
|
7397
|
+
function countsForIndex(step) {
|
|
7398
|
+
return step.index !== null && step.status !== "CLEARED" && step.status !== "INVALID";
|
|
7399
|
+
}
|
|
7400
|
+
function addStep(state, step) {
|
|
7401
|
+
if (step.at !== null) state.lastAt = step.at;
|
|
7402
|
+
if (countsForIndex(step)) state.lastIndex = step.index;
|
|
7403
|
+
if (isAsideStep(step)) return;
|
|
7404
|
+
const was = state.last ? openStatusOf(state.last) : null;
|
|
7405
|
+
if (was !== openStatusOf(step)) state.since = step.at;
|
|
7406
|
+
state.last = step;
|
|
7407
|
+
}
|
|
7408
|
+
function bytes(data) {
|
|
7409
|
+
return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
7410
|
+
}
|
|
7411
|
+
async function readExactly(file, position, length) {
|
|
7412
|
+
const data = bytes(await file.read(position, length));
|
|
7413
|
+
if (data.byteLength !== length) throw new Error("The file changed while it was read.");
|
|
7414
|
+
return data;
|
|
7415
|
+
}
|
|
7416
|
+
async function headHash(file, size) {
|
|
7417
|
+
const head = await readExactly(file, 0, Math.min(HEAD_HASH_BYTES, size));
|
|
7418
|
+
return createHash2("sha256").update(head).digest("hex");
|
|
7419
|
+
}
|
|
7420
|
+
async function readFirstAt(file, size) {
|
|
7421
|
+
const head = await readExactly(file, 0, Math.min(HEAD_LIMIT_BYTES, size));
|
|
7422
|
+
const end = head.indexOf(NEWLINE2);
|
|
7423
|
+
if (end === -1 && head.byteLength < size) return null;
|
|
7424
|
+
const step = readStepLine(head.toString("utf8", 0, end === -1 ? head.byteLength : end));
|
|
7425
|
+
return step?.index === 0 ? step.at : null;
|
|
7426
|
+
}
|
|
7427
|
+
async function scanTail(file, size) {
|
|
7428
|
+
const state = emptyState();
|
|
7429
|
+
let position = size;
|
|
7430
|
+
let budget2 = TAIL_LIMIT_BYTES;
|
|
7431
|
+
let fragment = Buffer.alloc(0);
|
|
7432
|
+
let end = null;
|
|
7433
|
+
let settled = false;
|
|
7434
|
+
const take = (step) => {
|
|
7435
|
+
if (step === null) return false;
|
|
7436
|
+
if (state.lastAt === null && step.at !== null) state.lastAt = step.at;
|
|
7437
|
+
if (state.lastIndex === null && countsForIndex(step)) state.lastIndex = step.index;
|
|
7438
|
+
if (isAsideStep(step)) return false;
|
|
7439
|
+
if (state.last === void 0 || state.last === null) {
|
|
7440
|
+
state.last = step;
|
|
7441
|
+
state.since = step.at;
|
|
7442
|
+
return false;
|
|
7443
|
+
}
|
|
7444
|
+
if (openStatusOf(step) !== openStatusOf(state.last)) return true;
|
|
7445
|
+
if (step.at !== null) state.since = step.at;
|
|
7446
|
+
return false;
|
|
7447
|
+
};
|
|
7448
|
+
while (position > 0 && budget2 > 0 && !settled) {
|
|
7449
|
+
const length = Math.min(TAIL_CHUNK_BYTES, position, budget2);
|
|
7450
|
+
const start = position - length;
|
|
7451
|
+
const chunk = await readExactly(file, start, length);
|
|
7452
|
+
budget2 -= length;
|
|
7453
|
+
position = start;
|
|
7454
|
+
const buffer = fragment.byteLength > 0 ? Buffer.concat([chunk, fragment]) : chunk;
|
|
7455
|
+
let cut2 = buffer.byteLength;
|
|
7456
|
+
if (end === null) {
|
|
7457
|
+
const last = buffer.lastIndexOf(NEWLINE2);
|
|
7458
|
+
if (last === -1) {
|
|
7459
|
+
fragment = buffer;
|
|
7460
|
+
continue;
|
|
7461
|
+
}
|
|
7462
|
+
end = start + last + 1;
|
|
7463
|
+
cut2 = last;
|
|
7464
|
+
if (last + 1 < buffer.byteLength) {
|
|
7465
|
+
settled = take(readStepLine(buffer.toString("utf8", last + 1)));
|
|
7466
|
+
}
|
|
7467
|
+
}
|
|
7468
|
+
let newline = cut2 > 0 ? buffer.lastIndexOf(NEWLINE2, cut2 - 1) : -1;
|
|
7469
|
+
while (newline !== -1 && !settled) {
|
|
7470
|
+
settled = take(readStepLine(buffer.toString("utf8", newline + 1, cut2)));
|
|
7471
|
+
cut2 = newline;
|
|
7472
|
+
newline = cut2 > 0 ? buffer.lastIndexOf(NEWLINE2, cut2 - 1) : -1;
|
|
7473
|
+
}
|
|
7474
|
+
if (settled) break;
|
|
7475
|
+
if (start === 0) {
|
|
7476
|
+
take(readStepLine(buffer.toString("utf8", 0, cut2)));
|
|
7477
|
+
if (state.last === void 0) state.last = null;
|
|
7478
|
+
return { state, end };
|
|
7479
|
+
}
|
|
7480
|
+
fragment = buffer.subarray(0, cut2);
|
|
7481
|
+
}
|
|
7482
|
+
if (!settled && position === 0) {
|
|
7483
|
+
if (end === null) {
|
|
7484
|
+
take(readStepLine(fragment.toString("utf8")));
|
|
7485
|
+
end = 0;
|
|
7486
|
+
}
|
|
7487
|
+
if (state.last === void 0) state.last = null;
|
|
7488
|
+
return { state, end };
|
|
7489
|
+
}
|
|
7490
|
+
if (!settled && state.last) state.since = null;
|
|
7491
|
+
return { state, end };
|
|
7492
|
+
}
|
|
7493
|
+
function stateOf(cached) {
|
|
7494
|
+
return {
|
|
7495
|
+
firstAt: cached.firstAt,
|
|
7496
|
+
last: cached.last,
|
|
7497
|
+
since: cached.since,
|
|
7498
|
+
lastAt: cached.lastAt,
|
|
7499
|
+
lastIndex: cached.lastIndex
|
|
7500
|
+
};
|
|
7501
|
+
}
|
|
7502
|
+
function createTranscriptReader(io) {
|
|
7503
|
+
const cache = /* @__PURE__ */ new Map();
|
|
7504
|
+
async function readFresh(file, earlier) {
|
|
7505
|
+
const handle = await io.openRegular(file);
|
|
7506
|
+
try {
|
|
7507
|
+
const { info } = handle;
|
|
7508
|
+
const firstAt = await readFirstAt(handle, info.size);
|
|
7509
|
+
const tail = await scanTail(handle, info.size);
|
|
7510
|
+
return {
|
|
7511
|
+
...tail.state,
|
|
7512
|
+
// A compacted file may begin later than the conversation did: the earliest seen stands.
|
|
7513
|
+
firstAt: earlier?.firstAt != null && (firstAt === null || earlier.firstAt < firstAt) ? earlier.firstAt : firstAt,
|
|
7514
|
+
ino: info.ino,
|
|
7515
|
+
size: info.size,
|
|
7516
|
+
mtimeMs: info.mtimeMs,
|
|
7517
|
+
headHash: await headHash(handle, info.size),
|
|
7518
|
+
offset: tail.end
|
|
7519
|
+
};
|
|
7520
|
+
} finally {
|
|
7521
|
+
await handle.close();
|
|
7522
|
+
}
|
|
7523
|
+
}
|
|
7524
|
+
async function readOnward(file, cached, info) {
|
|
7525
|
+
const offset = cached.offset;
|
|
7526
|
+
if (offset === null) return null;
|
|
7527
|
+
if (info.ino !== cached.ino || info.size <= cached.size || info.size < offset) return null;
|
|
7528
|
+
if (info.size - offset > TAIL_LIMIT_BYTES) return null;
|
|
7529
|
+
const handle = await io.openRegular(file);
|
|
7530
|
+
try {
|
|
7531
|
+
const opened = handle.info;
|
|
7532
|
+
if (opened.ino !== cached.ino || opened.size < offset || opened.size < cached.size) {
|
|
7533
|
+
return null;
|
|
7534
|
+
}
|
|
7535
|
+
if (opened.size - offset > TAIL_LIMIT_BYTES) return null;
|
|
7536
|
+
if (await headHash(handle, cached.size) !== cached.headHash) return null;
|
|
7537
|
+
const from = offset > 0 ? offset - 1 : 0;
|
|
7538
|
+
const read = await readExactly(handle, from, opened.size - from);
|
|
7539
|
+
if (offset > 0 && read[0] !== NEWLINE2) return null;
|
|
7540
|
+
const added = offset > 0 ? read.subarray(1) : read;
|
|
7541
|
+
const next = {
|
|
7542
|
+
...cached,
|
|
7543
|
+
ino: opened.ino,
|
|
7544
|
+
size: opened.size,
|
|
7545
|
+
mtimeMs: opened.mtimeMs,
|
|
7546
|
+
headHash: await headHash(handle, opened.size)
|
|
7547
|
+
};
|
|
7548
|
+
let start = 0;
|
|
7549
|
+
let newline = added.indexOf(NEWLINE2);
|
|
7550
|
+
while (newline !== -1) {
|
|
7551
|
+
const step = readStepLine(added.toString("utf8", start, newline));
|
|
7552
|
+
if (step) addStep(next, step);
|
|
7553
|
+
start = newline + 1;
|
|
7554
|
+
newline = added.indexOf(NEWLINE2, start);
|
|
7555
|
+
}
|
|
7556
|
+
if (start < added.byteLength) {
|
|
7557
|
+
const step = readStepLine(added.toString("utf8", start));
|
|
7558
|
+
if (step) addStep(next, step);
|
|
7559
|
+
}
|
|
7560
|
+
next.offset = offset + start;
|
|
7561
|
+
if (next.firstAt === null && offset === 0) {
|
|
7562
|
+
next.firstAt = await readFirstAt(handle, opened.size);
|
|
7563
|
+
}
|
|
7564
|
+
return next;
|
|
7565
|
+
} finally {
|
|
7566
|
+
await handle.close();
|
|
7567
|
+
}
|
|
7568
|
+
}
|
|
7569
|
+
return {
|
|
7570
|
+
async read(file, info) {
|
|
7571
|
+
const cached = cache.get(file);
|
|
7572
|
+
if (cached && cached.ino === info.ino && cached.size === info.size && cached.mtimeMs === info.mtimeMs) {
|
|
7573
|
+
return stateOf(cached);
|
|
7574
|
+
}
|
|
7575
|
+
const next = cached && await readOnward(file, cached, info) || await readFresh(file, cached);
|
|
7576
|
+
cache.set(file, next);
|
|
7577
|
+
return stateOf(next);
|
|
7578
|
+
},
|
|
7579
|
+
keepOnly(files) {
|
|
7580
|
+
for (const file of cache.keys()) {
|
|
7581
|
+
if (!files.has(file)) cache.delete(file);
|
|
7582
|
+
}
|
|
7583
|
+
}
|
|
7584
|
+
};
|
|
7585
|
+
}
|
|
7586
|
+
|
|
7587
|
+
// src/collector/adapters/antigravity/index.ts
|
|
7588
|
+
var ANTIGRAVITY_HOME_ENV = "AGENT_LOOKOUT_ANTIGRAVITY_HOME";
|
|
7589
|
+
var DEFAULT_HOME = [".gemini", "antigravity-cli"];
|
|
7590
|
+
var RECHECK_MS = 6e4;
|
|
7591
|
+
var PROCESS_CHECK_MS = 1e4;
|
|
7592
|
+
var PROMPT_PROCESS_CHECK_MS = 2e3;
|
|
7593
|
+
var BASIS = "files";
|
|
7594
|
+
var BASIS_WITHOUT_PROCESSES = "files-without-processes";
|
|
7595
|
+
var BASIS_UNMATCHED = "files-unmatched";
|
|
7596
|
+
var LABEL2 = "Antigravity CLI";
|
|
7597
|
+
var NEEDS_YOU_NOTE = "A session needs you while agy asks you to approve a tool, which its own log records. A question it asks you is not known to be recorded, so it shows as working.";
|
|
7598
|
+
var UNCHECKED_NOTE = "This was checked against one agy 1.3.1 conversation, and parts of it, such as /resume, are not yet checked, so a status can be wrong.";
|
|
7599
|
+
var ALWAYS_NOTES = `${NEEDS_YOU_NOTE} ${UNCHECKED_NOTE}`;
|
|
7600
|
+
var UNKNOWN_STEP_NOTE = "Some conversations end with a step Agent Lookout does not know, perhaps from a newer agy, so they show as unknown.";
|
|
7601
|
+
var UNMATCHED_NOTE = "An agy program is running that has not written to a conversation since it started, so none is shown as finished until it does.";
|
|
7602
|
+
var ANTIGRAVITY_CAPABILITIES = {
|
|
7603
|
+
"working-and-idle": { level: "yes" },
|
|
7604
|
+
"needs-you": {
|
|
7605
|
+
level: "partly",
|
|
7606
|
+
reason: "While agy asks you to approve a tool, from its own log. A question it asks you shows as working."
|
|
7607
|
+
},
|
|
7608
|
+
finished: {
|
|
7609
|
+
level: "partly",
|
|
7610
|
+
reason: "Once ps shows no agy program running that could have the conversation open. Not on Windows."
|
|
7611
|
+
},
|
|
7612
|
+
failed: {
|
|
7613
|
+
level: "partly",
|
|
7614
|
+
reason: "Only when agy records an error of its own or a failed reply. A tool that fails shows as working."
|
|
7615
|
+
},
|
|
7616
|
+
names: {
|
|
7617
|
+
level: "partly",
|
|
7618
|
+
reason: "The title agy gives a conversation once it has one, and the folder its program's log names. Until then, its conversation ID."
|
|
7619
|
+
},
|
|
7620
|
+
jump: {
|
|
7621
|
+
level: "no",
|
|
7622
|
+
reason: "The transcripts name no terminal, and agy documents no link that opens a conversation."
|
|
7623
|
+
},
|
|
7624
|
+
"quiet-for": { level: "yes" },
|
|
7625
|
+
stop: {
|
|
7626
|
+
level: "no",
|
|
7627
|
+
reason: "No agy program is tied to one conversation surely enough for Agent Lookout to confirm it and stop it."
|
|
7628
|
+
},
|
|
7629
|
+
answer: {
|
|
7630
|
+
level: "no",
|
|
7631
|
+
reason: "Its approval prompt takes an answer only in the terminal agy runs in."
|
|
7632
|
+
}
|
|
7633
|
+
};
|
|
7634
|
+
function withLoggedConversations(programs, logStates) {
|
|
7635
|
+
return programs.map((program, index) => {
|
|
7636
|
+
const current = logStates[index]?.current ?? null;
|
|
7637
|
+
return current === null ? program : { ...program, conversation: current };
|
|
7638
|
+
});
|
|
7639
|
+
}
|
|
7640
|
+
function folderOf(id, logs) {
|
|
7641
|
+
const open2 = logs.find((log) => log.current === id && log.folder !== null);
|
|
7642
|
+
if (open2) return open2.folder;
|
|
7643
|
+
return logs.find((log) => log.opened.has(id) && log.folder !== null)?.folder ?? null;
|
|
7644
|
+
}
|
|
7645
|
+
function createAntigravityAdapter(options = {}) {
|
|
7646
|
+
const env = options.env ?? process.env;
|
|
7647
|
+
const homeDir = options.homeDir ?? os5.homedir();
|
|
7648
|
+
const now = options.now ?? Date.now;
|
|
7649
|
+
const io = options.io ?? nodeIo2;
|
|
7650
|
+
const platform = options.platform ?? process.platform;
|
|
7651
|
+
const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
7652
|
+
const recheckMs = options.recheckMs ?? RECHECK_MS;
|
|
7653
|
+
const processCheckMs = options.processCheckMs ?? PROCESS_CHECK_MS;
|
|
7654
|
+
const processes = options.processes ?? createAgyProcessReader({ platform });
|
|
7655
|
+
const override = env[ANTIGRAVITY_HOME_ENV]?.trim() || void 0;
|
|
7656
|
+
const homeFrom = override ? "agent-lookout" : "default";
|
|
7657
|
+
const home = path14.resolve(override ?? path14.join(homeDir, ...DEFAULT_HOME));
|
|
7658
|
+
const brainDir = path14.join(home, BRAIN_DIR);
|
|
7659
|
+
const homeName = tildify(home, homeDir);
|
|
7660
|
+
const brainName = tildify(brainDir, homeDir);
|
|
7661
|
+
const finder = createConversationFinder({ home, io, oldRefreshMs: options.oldRefreshMs });
|
|
7662
|
+
const reader = createTranscriptReader(io);
|
|
7663
|
+
const logs = createAgyLogReader({ home, io, now });
|
|
7664
|
+
const titles = createTitleReader({ home, io });
|
|
7665
|
+
let absentAt = null;
|
|
7666
|
+
let homeSeen = false;
|
|
7667
|
+
let lastProcesses = null;
|
|
7668
|
+
let processesAt = null;
|
|
7669
|
+
let lastHolding = null;
|
|
7670
|
+
const commandRun = platform === "win32" ? "not run on Windows" : `${every(processCheckMs)} at most`;
|
|
7671
|
+
function watching(read) {
|
|
7672
|
+
return [
|
|
7673
|
+
{ label: "Conversations folder", value: brainName },
|
|
7674
|
+
{ label: "Read", value: read },
|
|
7675
|
+
{ label: "Command", value: `ps ${AGY_TABLE_ARGS.join(" ")}` },
|
|
7676
|
+
{ label: "Command run", value: commandRun }
|
|
7677
|
+
];
|
|
7678
|
+
}
|
|
7679
|
+
function result(state, detail, read, checkedAt, basis = BASIS) {
|
|
7680
|
+
const health = {
|
|
7681
|
+
id: SOURCE_ID2,
|
|
7682
|
+
label: LABEL2,
|
|
7683
|
+
state,
|
|
7684
|
+
detail,
|
|
7685
|
+
watching: watching(read),
|
|
7686
|
+
checkedAt
|
|
7687
|
+
};
|
|
7688
|
+
return state === "ok" ? { health, sessions: [], basis } : { health, sessions: [] };
|
|
7689
|
+
}
|
|
7690
|
+
function notFound(checkedAt) {
|
|
7691
|
+
lastProcesses = null;
|
|
7692
|
+
processesAt = null;
|
|
7693
|
+
lastHolding = null;
|
|
7694
|
+
return result(
|
|
7695
|
+
"unavailable",
|
|
7696
|
+
`The Antigravity CLI was not found: there is no ${homeName} folder. Agent Lookout looks again ${every(recheckMs)}.`,
|
|
7697
|
+
"not found",
|
|
7698
|
+
checkedAt
|
|
7699
|
+
);
|
|
7700
|
+
}
|
|
7701
|
+
async function homeIsThere() {
|
|
7702
|
+
try {
|
|
7703
|
+
return (await io.stat(home)).kind === "directory";
|
|
7704
|
+
} catch (error) {
|
|
7705
|
+
return !isMissing2(error);
|
|
7706
|
+
}
|
|
7707
|
+
}
|
|
7708
|
+
async function processesFor(conversations, checkedAt) {
|
|
7709
|
+
const shown = conversations.some(
|
|
7710
|
+
(files) => writtenRecently(files, checkedAt) || lastHolding?.held.has(files.id)
|
|
7711
|
+
);
|
|
7712
|
+
if (!shown) return null;
|
|
7713
|
+
const since = processesAt === null ? Infinity : checkedAt - processesAt;
|
|
7714
|
+
let due = since < 0 || since >= processCheckMs;
|
|
7715
|
+
if (!due && since >= PROMPT_PROCESS_CHECK_MS && lastProcesses?.ok) {
|
|
7716
|
+
const holding = holdConversations(lastProcesses.sessions, conversations);
|
|
7717
|
+
due = conversations.some(
|
|
7718
|
+
(files) => files.writtenAt !== null && files.writtenAt > processesAt && !holding.held.has(files.id)
|
|
7719
|
+
);
|
|
7720
|
+
}
|
|
7721
|
+
if (due) {
|
|
7722
|
+
lastProcesses = await processes.read();
|
|
7723
|
+
processesAt = checkedAt;
|
|
7724
|
+
}
|
|
7725
|
+
return lastProcesses;
|
|
7726
|
+
}
|
|
7727
|
+
async function poll() {
|
|
7728
|
+
const checkedAt = now();
|
|
7729
|
+
if (homeFrom !== "agent-lookout") {
|
|
7730
|
+
if (absentAt !== null) {
|
|
7731
|
+
const since = checkedAt - absentAt;
|
|
7732
|
+
if (since >= 0 && since < recheckMs) return notFound(checkedAt);
|
|
7733
|
+
}
|
|
7734
|
+
if (!homeSeen) {
|
|
7735
|
+
if (!await homeIsThere()) {
|
|
7736
|
+
absentAt = checkedAt;
|
|
7737
|
+
return notFound(checkedAt);
|
|
7738
|
+
}
|
|
7739
|
+
absentAt = null;
|
|
7740
|
+
homeSeen = true;
|
|
7741
|
+
}
|
|
7742
|
+
}
|
|
7743
|
+
const listing = await finder.list(checkedAt, (id) => lastHolding?.held.has(id) ?? false);
|
|
7744
|
+
if (listing.state === "unreadable") {
|
|
7745
|
+
return result(
|
|
7746
|
+
"error",
|
|
7747
|
+
`Antigravity CLI conversations could not be read: the folder ${brainName} could not be listed.`,
|
|
7748
|
+
"cannot be read",
|
|
7749
|
+
checkedAt
|
|
7750
|
+
);
|
|
7751
|
+
}
|
|
7752
|
+
if (listing.state === "missing") {
|
|
7753
|
+
lastHolding = null;
|
|
7754
|
+
if (homeFrom === "agent-lookout") {
|
|
7755
|
+
return result(
|
|
7756
|
+
"ok",
|
|
7757
|
+
`${ANTIGRAVITY_HOME_ENV} is set to ${homeName}, which has no ${BRAIN_DIR} folder, so no Antigravity CLI sessions are listed. ${ALWAYS_NOTES}`,
|
|
7758
|
+
"not found",
|
|
7759
|
+
checkedAt
|
|
7760
|
+
);
|
|
7761
|
+
}
|
|
7762
|
+
homeSeen = false;
|
|
7763
|
+
if (!await homeIsThere()) {
|
|
7764
|
+
absentAt = checkedAt;
|
|
7765
|
+
return notFound(checkedAt);
|
|
7766
|
+
}
|
|
7767
|
+
homeSeen = true;
|
|
7768
|
+
return result(
|
|
7769
|
+
"ok",
|
|
7770
|
+
`The Antigravity CLI has not saved any conversations in ${brainName} yet. ${ALWAYS_NOTES}`,
|
|
7771
|
+
"not found",
|
|
7772
|
+
checkedAt
|
|
7773
|
+
);
|
|
7774
|
+
}
|
|
7775
|
+
const { conversations } = listing;
|
|
7776
|
+
const list = await processesFor(conversations, checkedAt);
|
|
7777
|
+
const logStates = list?.ok ? await logs.read(list.sessions) : [];
|
|
7778
|
+
const programs = list?.ok ? withLoggedConversations(list.sessions, logStates) : [];
|
|
7779
|
+
const holding = list?.ok ? holdConversations(programs, conversations, processesAt) : null;
|
|
7780
|
+
lastHolding = holding;
|
|
7781
|
+
const logged = logStates.filter((state) => state !== null);
|
|
7782
|
+
const read = /* @__PURE__ */ new Set();
|
|
7783
|
+
const sessions = [];
|
|
7784
|
+
let unknownStep = false;
|
|
7785
|
+
for (const files of conversations) {
|
|
7786
|
+
const open2 = holding?.held.has(files.id) ?? false;
|
|
7787
|
+
if (files.transcriptInfo === null || !(open2 || writtenRecently(files, checkedAt))) continue;
|
|
7788
|
+
let state;
|
|
7789
|
+
try {
|
|
7790
|
+
state = await reader.read(files.transcript, files.transcriptInfo);
|
|
7791
|
+
} catch {
|
|
7792
|
+
read.add(files.transcript);
|
|
7793
|
+
continue;
|
|
7794
|
+
}
|
|
7795
|
+
read.add(files.transcript);
|
|
7796
|
+
const ask = logged.find((log) => log.ask?.conversation === files.id)?.ask ?? null;
|
|
7797
|
+
const waiting = ask !== null && waitsForApproval(ask, files.id, state.lastIndex) ? { tool: ask.tool, since: agyLogTimeAt(ask.at, checkedAt) } : null;
|
|
7798
|
+
const session = antigravitySession({
|
|
7799
|
+
conversationId: files.id,
|
|
7800
|
+
state,
|
|
7801
|
+
live: antigravityLiveness(files.id, holding),
|
|
7802
|
+
waiting,
|
|
7803
|
+
title: await titles.read(files.id),
|
|
7804
|
+
folder: folderOf(files.id, logged),
|
|
7805
|
+
writtenAt: files.lastWriteAt,
|
|
7806
|
+
now: checkedAt
|
|
7807
|
+
});
|
|
7808
|
+
if (session.status === "unknown") unknownStep = true;
|
|
7809
|
+
sessions.push(session);
|
|
7810
|
+
}
|
|
7811
|
+
reader.keepOnly(read);
|
|
7812
|
+
titles.keepOnly(new Set(sessions.map((session) => session.id.slice(SOURCE_ID2.length + 1))));
|
|
7813
|
+
let detail = `Sessions are read from the transcripts the Antigravity CLI keeps in ${brainName}. ${ALWAYS_NOTES}`;
|
|
7814
|
+
let basis = BASIS;
|
|
7815
|
+
if (list !== null && !list.ok) {
|
|
7816
|
+
detail += platform === "win32" ? " Windows has no ps to tell which conversations agy has open, so none is shown as finished." : " ps could not be run to tell which conversations agy has open, so none is shown as finished.";
|
|
7817
|
+
basis = BASIS_WITHOUT_PROCESSES;
|
|
7818
|
+
} else if (holding !== null && !holding.complete) {
|
|
7819
|
+
detail += ` ${UNMATCHED_NOTE}`;
|
|
7820
|
+
basis = BASIS_UNMATCHED;
|
|
7821
|
+
}
|
|
7822
|
+
if (unknownStep) detail += ` ${UNKNOWN_STEP_NOTE}`;
|
|
7823
|
+
const answer = result("ok", detail, every(pollIntervalMs), checkedAt, basis);
|
|
7824
|
+
return { ...answer, sessions };
|
|
7825
|
+
}
|
|
7826
|
+
return {
|
|
7827
|
+
id: SOURCE_ID2,
|
|
7828
|
+
label: LABEL2,
|
|
7829
|
+
capabilities: ANTIGRAVITY_CAPABILITIES,
|
|
7830
|
+
lookingIn: `Looking for Antigravity CLI sessions in ${brainName}.`,
|
|
7831
|
+
async poll() {
|
|
7832
|
+
try {
|
|
7833
|
+
return await poll();
|
|
7834
|
+
} catch {
|
|
7835
|
+
return result(
|
|
7836
|
+
"error",
|
|
7837
|
+
"Something unexpected went wrong while reading Antigravity CLI sessions.",
|
|
7838
|
+
every(pollIntervalMs),
|
|
7839
|
+
now()
|
|
7840
|
+
);
|
|
7841
|
+
}
|
|
7842
|
+
}
|
|
7843
|
+
};
|
|
7844
|
+
}
|
|
7845
|
+
|
|
7846
|
+
// src/collector/adapters/codex/index.ts
|
|
7847
|
+
import os6 from "node:os";
|
|
7848
|
+
import path16 from "node:path";
|
|
7849
|
+
|
|
7850
|
+
// src/core/mapping/codexMapping.ts
|
|
7851
|
+
var TURN_UNDER_WAY = /* @__PURE__ */ new Set(["task_started", "turn_started"]);
|
|
7852
|
+
var TURN_OVER2 = /* @__PURE__ */ new Set(["task_complete", "turn_complete", "turn_aborted"]);
|
|
7853
|
+
function mapCodexStatus(fields) {
|
|
7854
|
+
const { lastTurn, live: live2 } = fields;
|
|
7855
|
+
let open2;
|
|
7856
|
+
if (lastTurn === null) {
|
|
7857
|
+
open2 = "idle";
|
|
7858
|
+
} else if (typeof lastTurn !== "string") {
|
|
7859
|
+
return "unknown";
|
|
7860
|
+
} else if (TURN_UNDER_WAY.has(lastTurn)) {
|
|
7861
|
+
open2 = "working";
|
|
7862
|
+
} else if (TURN_OVER2.has(lastTurn)) {
|
|
7863
|
+
open2 = "idle";
|
|
7864
|
+
} else {
|
|
7865
|
+
return "unknown";
|
|
7866
|
+
}
|
|
7867
|
+
return live2 === false ? "finished" : open2;
|
|
7868
|
+
}
|
|
7869
|
+
var WRITER_LOCKS_SINCE = [0, 155, 0];
|
|
7870
|
+
var VERSION = /^(\d{1,6})\.(\d{1,6})\.(\d{1,6})(-[0-9A-Za-z.-]{1,64})?(?:\+[0-9A-Za-z.-]{1,64})?$/;
|
|
7871
|
+
function keepsWriterLocks(cliVersion) {
|
|
7872
|
+
if (typeof cliVersion !== "string") return false;
|
|
7873
|
+
const match = VERSION.exec(cliVersion);
|
|
7874
|
+
if (!match) return false;
|
|
7875
|
+
const parts = [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
7876
|
+
for (const [index, since] of WRITER_LOCKS_SINCE.entries()) {
|
|
7877
|
+
const part = parts[index];
|
|
7878
|
+
if (part !== since) return part > since;
|
|
7879
|
+
}
|
|
7880
|
+
return match[4] === void 0;
|
|
7881
|
+
}
|
|
7882
|
+
function codexLiveness(fields) {
|
|
7883
|
+
if (!fields.lockFolder) return "unknown";
|
|
7884
|
+
if (fields.locked) return true;
|
|
7885
|
+
return keepsWriterLocks(fields.cliVersion) ? false : "unknown";
|
|
7886
|
+
}
|
|
7887
|
+
var IMPORTED_TURN_ID = /^external-import-turn-\d{1,9}$/;
|
|
7888
|
+
function isImportedCodexTurn(turnId) {
|
|
7889
|
+
return typeof turnId === "string" && IMPORTED_TURN_ID.test(turnId);
|
|
7890
|
+
}
|
|
7891
|
+
function isRecord9(value) {
|
|
7892
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7893
|
+
}
|
|
7894
|
+
var CODEX_DESKTOP_ORIGINATOR = "Codex Desktop";
|
|
7895
|
+
function mapCodexSurface(source, originator) {
|
|
7896
|
+
if (originator === CODEX_DESKTOP_ORIGINATOR) return "desktop";
|
|
7897
|
+
if (source === "cli" || source === "exec") return "terminal";
|
|
7898
|
+
if (source === "vscode") return "vscode";
|
|
7899
|
+
if (isRecord9(source) && source.custom === "chatgpt") return "desktop";
|
|
7900
|
+
return "unknown";
|
|
7901
|
+
}
|
|
7902
|
+
var NOT_A_SESSION_SOURCE = /* @__PURE__ */ new Set(["subagent", "internal", "mcp"]);
|
|
7903
|
+
var NOT_A_SESSION_THREAD = /* @__PURE__ */ new Set(["subagent", "guardian_review", "memory_consolidation"]);
|
|
7904
|
+
function isCodexSessionSource(source, meta = {}) {
|
|
7905
|
+
if (typeof meta.parentThreadId === "string" && meta.parentThreadId !== "") return false;
|
|
7906
|
+
if (typeof meta.threadSource === "string" && NOT_A_SESSION_THREAD.has(meta.threadSource)) {
|
|
7907
|
+
return false;
|
|
7908
|
+
}
|
|
7909
|
+
if (typeof source === "string") return !NOT_A_SESSION_SOURCE.has(source);
|
|
7910
|
+
if (isRecord9(source)) return !Object.keys(source).some((kind) => NOT_A_SESSION_SOURCE.has(kind));
|
|
7911
|
+
return true;
|
|
7912
|
+
}
|
|
7913
|
+
|
|
7914
|
+
// src/collector/adapters/codex/rolloutFile.ts
|
|
7915
|
+
var HEAD_LIMIT_BYTES2 = 2 * 1024 * 1024;
|
|
7916
|
+
var TAIL_LIMIT_BYTES2 = 8 * 1024 * 1024;
|
|
7917
|
+
var FIRST_HEAD_BYTES = 256 * 1024;
|
|
7918
|
+
var TAIL_CHUNK_BYTES2 = 256 * 1024;
|
|
7919
|
+
var NEWLINE3 = 10;
|
|
7920
|
+
var CODEX_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/;
|
|
7921
|
+
function parseCodexTime(value) {
|
|
7922
|
+
if (typeof value !== "string" || !CODEX_TIME.test(value)) return null;
|
|
7923
|
+
const at = Date.parse(value);
|
|
7924
|
+
return Number.isSafeInteger(at) ? at : null;
|
|
7925
|
+
}
|
|
7926
|
+
var LINE_TIME = /^\{"timestamp":"([^"]{1,64})"/;
|
|
7927
|
+
var TURN_HINT = /"type":"(?:task|turn)_/;
|
|
7928
|
+
var TURN_TYPE = /^(?:task|turn)_/;
|
|
7929
|
+
function isRecord10(value) {
|
|
7930
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7931
|
+
}
|
|
7932
|
+
function text3(value) {
|
|
7933
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
7934
|
+
}
|
|
7935
|
+
function label(value) {
|
|
7936
|
+
return typeof value === "string" && value !== "" && value.length <= 64 ? value : void 0;
|
|
7937
|
+
}
|
|
7938
|
+
function compactSource(value) {
|
|
7939
|
+
if (typeof value === "string") return text3(value);
|
|
7940
|
+
if (!isRecord10(value)) return void 0;
|
|
7941
|
+
return Object.fromEntries(
|
|
7942
|
+
Object.entries(value).map(([kind, detail]) => [
|
|
7943
|
+
kind,
|
|
7944
|
+
typeof detail === "string" ? detail : true
|
|
7945
|
+
])
|
|
7946
|
+
);
|
|
7947
|
+
}
|
|
7948
|
+
function parse(line) {
|
|
7949
|
+
try {
|
|
7950
|
+
const value = JSON.parse(line);
|
|
7951
|
+
return isRecord10(value) ? value : null;
|
|
7952
|
+
} catch {
|
|
7953
|
+
return null;
|
|
7954
|
+
}
|
|
6318
7955
|
}
|
|
6319
|
-
function parse(line) {
|
|
6320
|
-
try {
|
|
6321
|
-
const value = JSON.parse(line);
|
|
6322
|
-
return isRecord9(value) ? value : null;
|
|
6323
|
-
} catch {
|
|
6324
|
-
return null;
|
|
6325
|
-
}
|
|
6326
|
-
}
|
|
6327
7956
|
function readMetaLine(line) {
|
|
6328
7957
|
if (!line.includes('"session_meta"')) return null;
|
|
6329
7958
|
const value = parse(line);
|
|
6330
|
-
if (!value || value.type !== "session_meta" || !
|
|
7959
|
+
if (!value || value.type !== "session_meta" || !isRecord10(value.payload)) return null;
|
|
6331
7960
|
const payload = value.payload;
|
|
6332
7961
|
const meta = {
|
|
6333
7962
|
id: text3(payload.id),
|
|
@@ -6347,7 +7976,7 @@ function readMetaLine(line) {
|
|
|
6347
7976
|
function readTurnLine(line) {
|
|
6348
7977
|
if (!line.includes('"event_msg"') || !TURN_HINT.test(line)) return null;
|
|
6349
7978
|
const value = parse(line);
|
|
6350
|
-
if (!value || value.type !== "event_msg" || !
|
|
7979
|
+
if (!value || value.type !== "event_msg" || !isRecord10(value.payload)) return null;
|
|
6351
7980
|
const turn = value.payload.type;
|
|
6352
7981
|
if (typeof turn !== "string" || !TURN_TYPE.test(turn)) return null;
|
|
6353
7982
|
return {
|
|
@@ -6360,23 +7989,23 @@ function lineTime(line) {
|
|
|
6360
7989
|
const match = LINE_TIME.exec(line);
|
|
6361
7990
|
return match ? parseCodexTime(match[1]) : null;
|
|
6362
7991
|
}
|
|
6363
|
-
function
|
|
7992
|
+
function bytes2(data) {
|
|
6364
7993
|
return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
6365
7994
|
}
|
|
6366
|
-
async function
|
|
6367
|
-
const data =
|
|
7995
|
+
async function readExactly2(file, position, length) {
|
|
7996
|
+
const data = bytes2(await file.read(position, length));
|
|
6368
7997
|
if (data.byteLength !== length) throw new Error("The file changed while it was read.");
|
|
6369
7998
|
return data;
|
|
6370
7999
|
}
|
|
6371
8000
|
async function readHead(file, size) {
|
|
6372
|
-
for (const limit of [FIRST_HEAD_BYTES,
|
|
8001
|
+
for (const limit of [FIRST_HEAD_BYTES, HEAD_LIMIT_BYTES2]) {
|
|
6373
8002
|
const length = Math.min(limit, size);
|
|
6374
|
-
const head = await
|
|
8003
|
+
const head = await readExactly2(file, 0, length);
|
|
6375
8004
|
let named = head.indexOf('"session_meta"');
|
|
6376
8005
|
while (named !== -1) {
|
|
6377
|
-
const end = head.indexOf(
|
|
8006
|
+
const end = head.indexOf(NEWLINE3, named);
|
|
6378
8007
|
if (end === -1) break;
|
|
6379
|
-
const start = named > 0 ? head.lastIndexOf(
|
|
8008
|
+
const start = named > 0 ? head.lastIndexOf(NEWLINE3, named - 1) + 1 : 0;
|
|
6380
8009
|
const meta = readMetaLine(head.toString("utf8", start, end));
|
|
6381
8010
|
if (meta) return meta;
|
|
6382
8011
|
named = head.indexOf('"session_meta"', end + 1);
|
|
@@ -6385,9 +8014,9 @@ async function readHead(file, size) {
|
|
|
6385
8014
|
}
|
|
6386
8015
|
return null;
|
|
6387
8016
|
}
|
|
6388
|
-
async function
|
|
8017
|
+
async function scanTail2(file, size) {
|
|
6389
8018
|
let position = size;
|
|
6390
|
-
let budget2 =
|
|
8019
|
+
let budget2 = TAIL_LIMIT_BYTES2;
|
|
6391
8020
|
let fragment = Buffer.alloc(0);
|
|
6392
8021
|
let end = null;
|
|
6393
8022
|
let lastLineAt = null;
|
|
@@ -6405,15 +8034,15 @@ async function scanTail(file, size) {
|
|
|
6405
8034
|
end
|
|
6406
8035
|
});
|
|
6407
8036
|
while (position > 0 && budget2 > 0) {
|
|
6408
|
-
const length = Math.min(
|
|
8037
|
+
const length = Math.min(TAIL_CHUNK_BYTES2, position, budget2);
|
|
6409
8038
|
const start = position - length;
|
|
6410
|
-
const chunk = await
|
|
8039
|
+
const chunk = await readExactly2(file, start, length);
|
|
6411
8040
|
budget2 -= length;
|
|
6412
8041
|
position = start;
|
|
6413
8042
|
const buffer = fragment.byteLength > 0 ? Buffer.concat([chunk, fragment]) : chunk;
|
|
6414
8043
|
let cut2 = buffer.byteLength;
|
|
6415
8044
|
if (end === null) {
|
|
6416
|
-
const last = buffer.lastIndexOf(
|
|
8045
|
+
const last = buffer.lastIndexOf(NEWLINE3);
|
|
6417
8046
|
if (last === -1) {
|
|
6418
8047
|
fragment = buffer;
|
|
6419
8048
|
continue;
|
|
@@ -6421,12 +8050,12 @@ async function scanTail(file, size) {
|
|
|
6421
8050
|
end = start + last + 1;
|
|
6422
8051
|
cut2 = last;
|
|
6423
8052
|
}
|
|
6424
|
-
let newline = cut2 > 0 ? buffer.lastIndexOf(
|
|
8053
|
+
let newline = cut2 > 0 ? buffer.lastIndexOf(NEWLINE3, cut2 - 1) : -1;
|
|
6425
8054
|
while (newline !== -1) {
|
|
6426
8055
|
const turn = take(buffer.toString("utf8", newline + 1, cut2));
|
|
6427
8056
|
if (turn) return found(turn);
|
|
6428
8057
|
cut2 = newline;
|
|
6429
|
-
newline = cut2 > 0 ? buffer.lastIndexOf(
|
|
8058
|
+
newline = cut2 > 0 ? buffer.lastIndexOf(NEWLINE3, cut2 - 1) : -1;
|
|
6430
8059
|
}
|
|
6431
8060
|
if (start === 0) {
|
|
6432
8061
|
const turn = take(buffer.toString("utf8", 0, cut2));
|
|
@@ -6440,7 +8069,7 @@ async function scanTail(file, size) {
|
|
|
6440
8069
|
}
|
|
6441
8070
|
return { lastTurn: void 0, lastTurnAt: null, lastTurnImported: false, lastLineAt, end };
|
|
6442
8071
|
}
|
|
6443
|
-
function
|
|
8072
|
+
function stateOf2(cached) {
|
|
6444
8073
|
return {
|
|
6445
8074
|
meta: cached.meta,
|
|
6446
8075
|
lastTurn: cached.lastTurn,
|
|
@@ -6456,7 +8085,7 @@ function createRolloutReader(io) {
|
|
|
6456
8085
|
try {
|
|
6457
8086
|
const { info } = handle;
|
|
6458
8087
|
const meta = await readHead(handle, info.size);
|
|
6459
|
-
const tail = await
|
|
8088
|
+
const tail = await scanTail2(handle, info.size);
|
|
6460
8089
|
return {
|
|
6461
8090
|
meta,
|
|
6462
8091
|
lastTurn: tail.lastTurn,
|
|
@@ -6476,14 +8105,14 @@ function createRolloutReader(io) {
|
|
|
6476
8105
|
const offset = cached.offset;
|
|
6477
8106
|
if (cached.meta === null || offset === null) return null;
|
|
6478
8107
|
if (info.ino !== cached.ino || info.size <= cached.size || info.size < offset) return null;
|
|
6479
|
-
if (info.size - offset >
|
|
8108
|
+
if (info.size - offset > TAIL_LIMIT_BYTES2) return null;
|
|
6480
8109
|
const handle = await io.openRegular(file);
|
|
6481
8110
|
try {
|
|
6482
8111
|
const opened = handle.info;
|
|
6483
8112
|
if (opened.ino !== cached.ino || opened.size < offset) return null;
|
|
6484
8113
|
const from = offset > 0 ? offset - 1 : 0;
|
|
6485
|
-
const read = await
|
|
6486
|
-
if (offset > 0 && read[0] !==
|
|
8114
|
+
const read = await readExactly2(handle, from, opened.size - from);
|
|
8115
|
+
if (offset > 0 && read[0] !== NEWLINE3) return null;
|
|
6487
8116
|
const added = offset > 0 ? read.subarray(1) : read;
|
|
6488
8117
|
const next = {
|
|
6489
8118
|
...cached,
|
|
@@ -6492,7 +8121,7 @@ function createRolloutReader(io) {
|
|
|
6492
8121
|
mtimeMs: opened.mtimeMs
|
|
6493
8122
|
};
|
|
6494
8123
|
let start = 0;
|
|
6495
|
-
let newline = added.indexOf(
|
|
8124
|
+
let newline = added.indexOf(NEWLINE3);
|
|
6496
8125
|
while (newline !== -1) {
|
|
6497
8126
|
const line = added.toString("utf8", start, newline);
|
|
6498
8127
|
if (line.trim() !== "") {
|
|
@@ -6506,7 +8135,7 @@ function createRolloutReader(io) {
|
|
|
6506
8135
|
}
|
|
6507
8136
|
}
|
|
6508
8137
|
start = newline + 1;
|
|
6509
|
-
newline = added.indexOf(
|
|
8138
|
+
newline = added.indexOf(NEWLINE3, start);
|
|
6510
8139
|
}
|
|
6511
8140
|
next.offset = offset + start;
|
|
6512
8141
|
return next;
|
|
@@ -6518,11 +8147,11 @@ function createRolloutReader(io) {
|
|
|
6518
8147
|
async read(file, info) {
|
|
6519
8148
|
const cached = cache.get(file);
|
|
6520
8149
|
if (cached && cached.ino === info.ino && cached.size === info.size && cached.mtimeMs === info.mtimeMs) {
|
|
6521
|
-
return
|
|
8150
|
+
return stateOf2(cached);
|
|
6522
8151
|
}
|
|
6523
8152
|
const next = cached && await readOnward(file, cached, info) || await readFresh(file);
|
|
6524
8153
|
cache.set(file, next);
|
|
6525
|
-
return
|
|
8154
|
+
return stateOf2(next);
|
|
6526
8155
|
},
|
|
6527
8156
|
keepOnly(files) {
|
|
6528
8157
|
for (const file of cache.keys()) {
|
|
@@ -6533,10 +8162,10 @@ function createRolloutReader(io) {
|
|
|
6533
8162
|
}
|
|
6534
8163
|
|
|
6535
8164
|
// src/collector/adapters/codex/rollouts.ts
|
|
6536
|
-
import
|
|
6537
|
-
var
|
|
8165
|
+
import path15 from "node:path";
|
|
8166
|
+
var UUID4 = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
|
|
6538
8167
|
var ROLLOUT_FILE_NAME = new RegExp(
|
|
6539
|
-
`^rollout-\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-(${
|
|
8168
|
+
`^rollout-\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-(${UUID4})(?:_(${UUID4}))?\\.jsonl$`,
|
|
6540
8169
|
"i"
|
|
6541
8170
|
);
|
|
6542
8171
|
var INDEX_REFRESH_MS = 3e4;
|
|
@@ -6551,7 +8180,7 @@ function twoDigits(value) {
|
|
|
6551
8180
|
function recentDayFolders(sessionsDir, now) {
|
|
6552
8181
|
const folders = [now, now - 24 * 60 * 60 * 1e3].map((at) => {
|
|
6553
8182
|
const date = new Date(at);
|
|
6554
|
-
return
|
|
8183
|
+
return path15.join(
|
|
6555
8184
|
sessionsDir,
|
|
6556
8185
|
String(date.getFullYear()),
|
|
6557
8186
|
twoDigits(date.getMonth() + 1),
|
|
@@ -6580,12 +8209,12 @@ function createRolloutFinder(options) {
|
|
|
6580
8209
|
const refs = [];
|
|
6581
8210
|
for (const name of (await names(dir)).sort()) {
|
|
6582
8211
|
const threadId = threadIdOfRollout(name);
|
|
6583
|
-
if (threadId !== null) refs.push({ path:
|
|
8212
|
+
if (threadId !== null) refs.push({ path: path15.join(dir, name), threadId });
|
|
6584
8213
|
}
|
|
6585
8214
|
return refs;
|
|
6586
8215
|
}
|
|
6587
8216
|
async function subfolders(dir, pattern) {
|
|
6588
|
-
return (await names(dir)).filter((name) => pattern.test(name)).sort().map((name) =>
|
|
8217
|
+
return (await names(dir)).filter((name) => pattern.test(name)).sort().map((name) => path15.join(dir, name));
|
|
6589
8218
|
}
|
|
6590
8219
|
async function buildIndex() {
|
|
6591
8220
|
const years = await subfolders(sessionsDir, YEAR);
|
|
@@ -6629,7 +8258,7 @@ function createRolloutFinder(options) {
|
|
|
6629
8258
|
// src/collector/adapters/codex/sessionIndex.ts
|
|
6630
8259
|
var SESSION_INDEX_FILE = "session_index.jsonl";
|
|
6631
8260
|
var SESSION_INDEX_LIMIT_BYTES = 4 * 1024 * 1024;
|
|
6632
|
-
function
|
|
8261
|
+
function isRecord11(value) {
|
|
6633
8262
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6634
8263
|
}
|
|
6635
8264
|
function parseSessionIndex(content) {
|
|
@@ -6642,7 +8271,7 @@ function parseSessionIndex(content) {
|
|
|
6642
8271
|
} catch {
|
|
6643
8272
|
continue;
|
|
6644
8273
|
}
|
|
6645
|
-
if (!
|
|
8274
|
+
if (!isRecord11(value)) continue;
|
|
6646
8275
|
const { id, thread_name: name } = value;
|
|
6647
8276
|
if (typeof id !== "string" || id === "" || typeof name !== "string") continue;
|
|
6648
8277
|
const trimmed = name.trim();
|
|
@@ -6693,14 +8322,14 @@ function createSessionIndexReader(file, io) {
|
|
|
6693
8322
|
}
|
|
6694
8323
|
|
|
6695
8324
|
// src/collector/adapters/codex/toSession.ts
|
|
6696
|
-
var
|
|
6697
|
-
var
|
|
8325
|
+
var SOURCE_ID3 = "codex";
|
|
8326
|
+
var WRITE_ORDER_SLACK_MS3 = 1e3;
|
|
6698
8327
|
function lastActivity(state, now) {
|
|
6699
8328
|
return plausibleTime(state.lastLineAt, now) ?? plausibleTime(state.lastTurnAt, now) ?? plausibleTime(parseCodexTime(state.meta?.timestamp), now);
|
|
6700
8329
|
}
|
|
6701
8330
|
function codexSession(input) {
|
|
6702
8331
|
const { threadId, state, live: live2, now } = input;
|
|
6703
|
-
const id = `${
|
|
8332
|
+
const id = `${SOURCE_ID3}:${threadId}`;
|
|
6704
8333
|
const cwd = state.meta?.cwd ?? null;
|
|
6705
8334
|
const project = projectOf(cwd);
|
|
6706
8335
|
const status = mapCodexStatus({ lastTurn: state.lastTurn, live: live2 });
|
|
@@ -6711,12 +8340,12 @@ function codexSession(input) {
|
|
|
6711
8340
|
} else if (status === "idle" || status === "finished") {
|
|
6712
8341
|
statusSince = plausibleTime(state.lastLineAt, now);
|
|
6713
8342
|
}
|
|
6714
|
-
if (statusSince !== null && startedAt !== null && statusSince < startedAt -
|
|
8343
|
+
if (statusSince !== null && startedAt !== null && statusSince < startedAt - WRITE_ORDER_SLACK_MS3) {
|
|
6715
8344
|
statusSince = null;
|
|
6716
8345
|
}
|
|
6717
8346
|
const session = {
|
|
6718
8347
|
id,
|
|
6719
|
-
source:
|
|
8348
|
+
source: SOURCE_ID3,
|
|
6720
8349
|
surface: mapCodexSurface(state.meta?.source, state.meta?.originator),
|
|
6721
8350
|
// A name made only of spaces is no name. It is cleaned and cut as every other agent's is.
|
|
6722
8351
|
name: sessionName(input.name ?? "") || project || id,
|
|
@@ -6729,7 +8358,7 @@ function codexSession(input) {
|
|
|
6729
8358
|
stale: isStale({ status, statusSince }, now)
|
|
6730
8359
|
};
|
|
6731
8360
|
const writtenAt = input.writtenAt === void 0 ? null : plausibleTime(Math.floor(input.writtenAt), now);
|
|
6732
|
-
const beforeStart = writtenAt !== null && startedAt !== null && writtenAt < startedAt -
|
|
8361
|
+
const beforeStart = writtenAt !== null && startedAt !== null && writtenAt < startedAt - WRITE_ORDER_SLACK_MS3;
|
|
6733
8362
|
if (writtenAt !== null && !beforeStart) session.lastWriteAt = writtenAt;
|
|
6734
8363
|
return session;
|
|
6735
8364
|
}
|
|
@@ -6759,11 +8388,11 @@ async function readWriterLocks(dir, io) {
|
|
|
6759
8388
|
// src/collector/adapters/codex/index.ts
|
|
6760
8389
|
var CODEX_HOME_ENV = "AGENT_LOOKOUT_CODEX_HOME";
|
|
6761
8390
|
var CODEX_OWN_HOME_ENV = "CODEX_HOME";
|
|
6762
|
-
var
|
|
6763
|
-
var
|
|
8391
|
+
var RECHECK_MS2 = 6e4;
|
|
8392
|
+
var BASIS2 = "files";
|
|
6764
8393
|
var BASIS_WITHOUT_LOCKS = "files-without-locks";
|
|
6765
|
-
var
|
|
6766
|
-
var
|
|
8394
|
+
var LABEL3 = "Codex";
|
|
8395
|
+
var NEEDS_YOU_NOTE2 = "Codex's session files do not record when it is waiting for your approval, so a Codex session that is waiting for you shows as working.";
|
|
6767
8396
|
var OLDER_CODEX_NOTE = "Some sessions were started by a Codex older than 0.155, which does not record which sessions are open, so those are never shown as finished.";
|
|
6768
8397
|
var CODEX_CAPABILITIES = {
|
|
6769
8398
|
"working-and-idle": { level: "yes" },
|
|
@@ -6796,18 +8425,18 @@ var CODEX_CAPABILITIES = {
|
|
|
6796
8425
|
};
|
|
6797
8426
|
function createCodexAdapter(options = {}) {
|
|
6798
8427
|
const env = options.env ?? process.env;
|
|
6799
|
-
const homeDir = options.homeDir ??
|
|
8428
|
+
const homeDir = options.homeDir ?? os6.homedir();
|
|
6800
8429
|
const now = options.now ?? Date.now;
|
|
6801
8430
|
const io = options.io ?? nodeIo2;
|
|
6802
8431
|
const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
6803
|
-
const recheckMs = options.recheckMs ??
|
|
8432
|
+
const recheckMs = options.recheckMs ?? RECHECK_MS2;
|
|
6804
8433
|
const ownOverride = env[CODEX_HOME_ENV]?.trim() || void 0;
|
|
6805
8434
|
const codexOverride = env[CODEX_OWN_HOME_ENV]?.trim() || void 0;
|
|
6806
8435
|
const homeFrom = ownOverride ? "agent-lookout" : codexOverride ? "codex" : "default";
|
|
6807
|
-
const codexHome =
|
|
6808
|
-
const sessionsDir =
|
|
6809
|
-
const locksDir =
|
|
6810
|
-
const indexFile =
|
|
8436
|
+
const codexHome = path16.resolve(ownOverride ?? codexOverride ?? path16.join(homeDir, ".codex"));
|
|
8437
|
+
const sessionsDir = path16.join(codexHome, "sessions");
|
|
8438
|
+
const locksDir = path16.join(codexHome, WRITER_LOCK_DIR);
|
|
8439
|
+
const indexFile = path16.join(codexHome, SESSION_INDEX_FILE);
|
|
6811
8440
|
const homeName = tildify(codexHome, homeDir);
|
|
6812
8441
|
const sessionsName = tildify(sessionsDir, homeDir);
|
|
6813
8442
|
const locksName = tildify(locksDir, homeDir);
|
|
@@ -6833,14 +8462,14 @@ function createCodexAdapter(options = {}) {
|
|
|
6833
8462
|
}
|
|
6834
8463
|
function result(state, detail, read, checkedAt) {
|
|
6835
8464
|
const health = {
|
|
6836
|
-
id:
|
|
6837
|
-
label:
|
|
8465
|
+
id: SOURCE_ID3,
|
|
8466
|
+
label: LABEL3,
|
|
6838
8467
|
state,
|
|
6839
8468
|
detail,
|
|
6840
8469
|
watching: watching(read),
|
|
6841
8470
|
checkedAt
|
|
6842
8471
|
};
|
|
6843
|
-
return state === "ok" ? { health, sessions: [], basis:
|
|
8472
|
+
return state === "ok" ? { health, sessions: [], basis: BASIS2 } : { health, sessions: [] };
|
|
6844
8473
|
}
|
|
6845
8474
|
function notFound(checkedAt) {
|
|
6846
8475
|
const where = homeFrom === "codex" ? `${CODEX_OWN_HOME_ENV} is set to ${homeName}, and there is no folder there` : `there is no ${homeName} folder`;
|
|
@@ -6888,7 +8517,7 @@ function createCodexAdapter(options = {}) {
|
|
|
6888
8517
|
if (homeFrom === "agent-lookout") {
|
|
6889
8518
|
return result(
|
|
6890
8519
|
"ok",
|
|
6891
|
-
`${CODEX_HOME_ENV} is set to ${homeName}, which has no sessions folder, so no Codex sessions are listed. ${
|
|
8520
|
+
`${CODEX_HOME_ENV} is set to ${homeName}, which has no sessions folder, so no Codex sessions are listed. ${NEEDS_YOU_NOTE2}`,
|
|
6892
8521
|
"not found",
|
|
6893
8522
|
checkedAt
|
|
6894
8523
|
);
|
|
@@ -6901,7 +8530,7 @@ function createCodexAdapter(options = {}) {
|
|
|
6901
8530
|
homeSeen = true;
|
|
6902
8531
|
return result(
|
|
6903
8532
|
"ok",
|
|
6904
|
-
`Codex has not saved any sessions in ${sessionsName} yet. ${
|
|
8533
|
+
`Codex has not saved any sessions in ${sessionsName} yet. ${NEEDS_YOU_NOTE2}`,
|
|
6905
8534
|
"not found",
|
|
6906
8535
|
checkedAt
|
|
6907
8536
|
);
|
|
@@ -6983,7 +8612,7 @@ function createCodexAdapter(options = {}) {
|
|
|
6983
8612
|
now: checkedAt
|
|
6984
8613
|
});
|
|
6985
8614
|
});
|
|
6986
|
-
let detail = `Sessions are read from the files Codex saves in ${sessionsName}. ${
|
|
8615
|
+
let detail = `Sessions are read from the files Codex saves in ${sessionsName}. ${NEEDS_YOU_NOTE2}`;
|
|
6987
8616
|
if (!locks.supported) {
|
|
6988
8617
|
const why = locks.missing ? `There is no list of open sessions at ${locksName}, which Codex keeps from version 0.155 on` : `The list of open sessions at ${locksName} could not be read`;
|
|
6989
8618
|
detail += ` ${why}, so a session that has ended cannot be told from one that is idle, and none is shown as finished.`;
|
|
@@ -6991,11 +8620,11 @@ function createCodexAdapter(options = {}) {
|
|
|
6991
8620
|
detail += ` ${OLDER_CODEX_NOTE}`;
|
|
6992
8621
|
}
|
|
6993
8622
|
const answer = result("ok", detail, every(pollIntervalMs), checkedAt);
|
|
6994
|
-
return { ...answer, sessions, basis: locks.supported ?
|
|
8623
|
+
return { ...answer, sessions, basis: locks.supported ? BASIS2 : BASIS_WITHOUT_LOCKS };
|
|
6995
8624
|
}
|
|
6996
8625
|
return {
|
|
6997
|
-
id:
|
|
6998
|
-
label:
|
|
8626
|
+
id: SOURCE_ID3,
|
|
8627
|
+
label: LABEL3,
|
|
6999
8628
|
capabilities: CODEX_CAPABILITIES,
|
|
7000
8629
|
lookingIn: `Looking for Codex sessions in ${sessionsName}.`,
|
|
7001
8630
|
async poll() {
|
|
@@ -7014,8 +8643,8 @@ function createCodexAdapter(options = {}) {
|
|
|
7014
8643
|
}
|
|
7015
8644
|
|
|
7016
8645
|
// src/collector/adapters/status-files/index.ts
|
|
7017
|
-
import
|
|
7018
|
-
import
|
|
8646
|
+
import os7 from "node:os";
|
|
8647
|
+
import path17 from "node:path";
|
|
7019
8648
|
|
|
7020
8649
|
// src/core/mapping/statusFileMapping.ts
|
|
7021
8650
|
var STATUS = /* @__PURE__ */ new Map([
|
|
@@ -7095,16 +8724,16 @@ function parseStatusFile(content) {
|
|
|
7095
8724
|
}
|
|
7096
8725
|
|
|
7097
8726
|
// src/collector/adapters/status-files/toSession.ts
|
|
7098
|
-
var
|
|
7099
|
-
var
|
|
8727
|
+
var SOURCE_ID4 = "status-files";
|
|
8728
|
+
var WRITE_ORDER_SLACK_MS4 = 1e3;
|
|
7100
8729
|
function statusFileSession(input) {
|
|
7101
8730
|
const { fileName: fileName2, file, statusSince, now } = input;
|
|
7102
8731
|
const cwd = file.cwd ?? null;
|
|
7103
8732
|
const project = projectOf(cwd);
|
|
7104
8733
|
const mapped = mapStatusFileStatus(file);
|
|
7105
8734
|
const session = {
|
|
7106
|
-
id: `${
|
|
7107
|
-
source:
|
|
8735
|
+
id: `${SOURCE_ID4}:${fileName2}`,
|
|
8736
|
+
source: SOURCE_ID4,
|
|
7108
8737
|
agent: file.agent,
|
|
7109
8738
|
surface: mapStatusFileApp(file.app),
|
|
7110
8739
|
// The file's name is as untrusted as its contents, so it is cleaned and cut
|
|
@@ -7124,15 +8753,15 @@ function statusFileSession(input) {
|
|
|
7124
8753
|
}
|
|
7125
8754
|
const writtenAt = input.writtenAt === void 0 ? null : plausibleTime(Math.floor(input.writtenAt), now);
|
|
7126
8755
|
const given = plausibleTime(file.since, now);
|
|
7127
|
-
const beforeSince = writtenAt !== null && given !== null && writtenAt < given -
|
|
8756
|
+
const beforeSince = writtenAt !== null && given !== null && writtenAt < given - WRITE_ORDER_SLACK_MS4;
|
|
7128
8757
|
if (writtenAt !== null && !beforeSince) session.lastWriteAt = writtenAt;
|
|
7129
8758
|
return session;
|
|
7130
8759
|
}
|
|
7131
8760
|
|
|
7132
8761
|
// src/collector/adapters/status-files/index.ts
|
|
7133
8762
|
var STATUS_DIR_ENV = "AGENT_LOOKOUT_STATUS_DIR";
|
|
7134
|
-
var
|
|
7135
|
-
var
|
|
8763
|
+
var BASIS3 = "files";
|
|
8764
|
+
var LABEL4 = "Status files";
|
|
7136
8765
|
var STATUS_FILE_CAPABILITIES = {
|
|
7137
8766
|
"working-and-idle": { level: "partly", reason: "If the agent writes working and idle." },
|
|
7138
8767
|
"needs-you": { level: "partly", reason: "If the agent writes waiting." },
|
|
@@ -7158,13 +8787,13 @@ function counted(count2, one, many) {
|
|
|
7158
8787
|
}
|
|
7159
8788
|
function createStatusFileAdapter(options = {}) {
|
|
7160
8789
|
const env = options.env ?? process.env;
|
|
7161
|
-
const homeDir = options.homeDir ??
|
|
8790
|
+
const homeDir = options.homeDir ?? os7.homedir();
|
|
7162
8791
|
const now = options.now ?? Date.now;
|
|
7163
8792
|
const io = options.io ?? nodeIo2;
|
|
7164
8793
|
const isAlive = options.isAlive ?? isProcessAlive;
|
|
7165
8794
|
const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
7166
8795
|
const override = env[STATUS_DIR_ENV]?.trim() || void 0;
|
|
7167
|
-
const dir =
|
|
8796
|
+
const dir = path17.resolve(override ?? path17.join(homeDir, ".agent-lookout", "sessions"));
|
|
7168
8797
|
const dirName = tildify(dir, homeDir);
|
|
7169
8798
|
const decoder2 = new TextDecoder();
|
|
7170
8799
|
let lastRead = /* @__PURE__ */ new Map();
|
|
@@ -7185,15 +8814,15 @@ function createStatusFileAdapter(options = {}) {
|
|
|
7185
8814
|
}
|
|
7186
8815
|
function result(state, detail, facts, checkedAt, sessions = []) {
|
|
7187
8816
|
const health = {
|
|
7188
|
-
id:
|
|
7189
|
-
label:
|
|
8817
|
+
id: SOURCE_ID4,
|
|
8818
|
+
label: LABEL4,
|
|
7190
8819
|
state,
|
|
7191
8820
|
detail,
|
|
7192
8821
|
watching: facts,
|
|
7193
8822
|
checkedAt
|
|
7194
8823
|
};
|
|
7195
|
-
if (state === "ok") return { health, sessions, basis:
|
|
7196
|
-
if (state === "not-set-up") return { health, sessions: [], basis:
|
|
8824
|
+
if (state === "ok") return { health, sessions, basis: BASIS3 };
|
|
8825
|
+
if (state === "not-set-up") return { health, sessions: [], basis: BASIS3 };
|
|
7197
8826
|
return { health, sessions: [] };
|
|
7198
8827
|
}
|
|
7199
8828
|
async function readOne(file) {
|
|
@@ -7209,9 +8838,9 @@ function createStatusFileAdapter(options = {}) {
|
|
|
7209
8838
|
const tooLarge = { kind: "refused", why: "is over 16 KB" };
|
|
7210
8839
|
try {
|
|
7211
8840
|
if (open2.info.size > MAX_FILE_BYTES) return tooLarge;
|
|
7212
|
-
const
|
|
7213
|
-
if (
|
|
7214
|
-
const parsed = parseStatusFile(decoder2.decode(
|
|
8841
|
+
const bytes3 = await open2.read(0, MAX_FILE_BYTES + 1);
|
|
8842
|
+
if (bytes3.byteLength > MAX_FILE_BYTES) return tooLarge;
|
|
8843
|
+
const parsed = parseStatusFile(decoder2.decode(bytes3));
|
|
7215
8844
|
return parsed ? { kind: "read", file: parsed, mtimeMs: open2.info.mtimeMs } : { kind: "invalid" };
|
|
7216
8845
|
} catch {
|
|
7217
8846
|
return { kind: "refused", why: "could not be read" };
|
|
@@ -7224,7 +8853,7 @@ function createStatusFileAdapter(options = {}) {
|
|
|
7224
8853
|
if (names.length <= MAX_FILES) return names;
|
|
7225
8854
|
const written = await Promise.all(
|
|
7226
8855
|
names.map(async (name) => {
|
|
7227
|
-
const info = await io.lstat(
|
|
8856
|
+
const info = await io.lstat(path17.join(dir, name)).catch(() => null);
|
|
7228
8857
|
return { name, mtimeMs: info?.mtimeMs ?? -Infinity };
|
|
7229
8858
|
})
|
|
7230
8859
|
);
|
|
@@ -7274,7 +8903,7 @@ function createStatusFileAdapter(options = {}) {
|
|
|
7274
8903
|
const nextRead = /* @__PURE__ */ new Map();
|
|
7275
8904
|
const nextStatuses = /* @__PURE__ */ new Map();
|
|
7276
8905
|
for (const fileName2 of await toRead(listed)) {
|
|
7277
|
-
const reading = await readOne(
|
|
8906
|
+
const reading = await readOne(path17.join(dir, fileName2));
|
|
7278
8907
|
let kept;
|
|
7279
8908
|
if (reading.kind === "read") {
|
|
7280
8909
|
kept = reading;
|
|
@@ -7340,8 +8969,8 @@ function createStatusFileAdapter(options = {}) {
|
|
|
7340
8969
|
);
|
|
7341
8970
|
}
|
|
7342
8971
|
return {
|
|
7343
|
-
id:
|
|
7344
|
-
label:
|
|
8972
|
+
id: SOURCE_ID4,
|
|
8973
|
+
label: LABEL4,
|
|
7345
8974
|
capabilities: STATUS_FILE_CAPABILITIES,
|
|
7346
8975
|
lookingIn: `Looking for status files in ${dirName}.`,
|
|
7347
8976
|
async poll() {
|
|
@@ -7500,8 +9129,8 @@ function createEventStore(capacity = EVENT_CAPACITY) {
|
|
|
7500
9129
|
}
|
|
7501
9130
|
|
|
7502
9131
|
// src/collector/git/branchFinder.ts
|
|
7503
|
-
import
|
|
7504
|
-
import
|
|
9132
|
+
import os8 from "node:os";
|
|
9133
|
+
import path19 from "node:path";
|
|
7505
9134
|
|
|
7506
9135
|
// src/collector/git/gitHead.ts
|
|
7507
9136
|
var MAX_GIT_FILE_BYTES = 4 * 1024;
|
|
@@ -7540,19 +9169,19 @@ function parseCommonDir(content) {
|
|
|
7540
9169
|
}
|
|
7541
9170
|
|
|
7542
9171
|
// src/collector/git/repository.ts
|
|
7543
|
-
import { createHash as
|
|
7544
|
-
import
|
|
9172
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
9173
|
+
import path18 from "node:path";
|
|
7545
9174
|
var REPOSITORY_ID_LENGTH = 16;
|
|
7546
9175
|
function repositoryId(gitFolder) {
|
|
7547
|
-
return
|
|
9176
|
+
return createHash3("sha256").update(gitFolder).digest("hex").slice(0, REPOSITORY_ID_LENGTH);
|
|
7548
9177
|
}
|
|
7549
9178
|
function sharedName(common) {
|
|
7550
|
-
const base =
|
|
7551
|
-
if (base.startsWith(".")) return
|
|
9179
|
+
const base = path18.basename(common);
|
|
9180
|
+
if (base.startsWith(".")) return path18.basename(path18.dirname(common));
|
|
7552
9181
|
return base.endsWith(".git") ? base.slice(0, -".git".length) : base;
|
|
7553
9182
|
}
|
|
7554
9183
|
function repositoryOf({ dir, gitDir, commonDir }) {
|
|
7555
|
-
const name = sessionName(commonDir === null ?
|
|
9184
|
+
const name = sessionName(commonDir === null ? path18.basename(dir) : sharedName(commonDir));
|
|
7556
9185
|
return name === void 0 ? null : { id: repositoryId(commonDir ?? gitDir), name };
|
|
7557
9186
|
}
|
|
7558
9187
|
|
|
@@ -7570,9 +9199,9 @@ async function readGitFile(io, file, maxBytes = MAX_GIT_FILE_BYTES) {
|
|
|
7570
9199
|
}
|
|
7571
9200
|
try {
|
|
7572
9201
|
if (open2.info.size > maxBytes) return null;
|
|
7573
|
-
const
|
|
7574
|
-
if (
|
|
7575
|
-
return decoder.decode(
|
|
9202
|
+
const bytes3 = await open2.read(0, maxBytes + 1);
|
|
9203
|
+
if (bytes3.byteLength > maxBytes) return null;
|
|
9204
|
+
return decoder.decode(bytes3);
|
|
7576
9205
|
} catch {
|
|
7577
9206
|
return null;
|
|
7578
9207
|
} finally {
|
|
@@ -7581,13 +9210,13 @@ async function readGitFile(io, file, maxBytes = MAX_GIT_FILE_BYTES) {
|
|
|
7581
9210
|
}
|
|
7582
9211
|
}
|
|
7583
9212
|
async function commonDirOf(io, gitDir) {
|
|
7584
|
-
const content = await readGitFile(io,
|
|
9213
|
+
const content = await readGitFile(io, path19.join(gitDir, "commondir"));
|
|
7585
9214
|
if (content === void 0) return null;
|
|
7586
9215
|
const target = content === null ? null : parseCommonDir(content);
|
|
7587
|
-
return target === null ? void 0 :
|
|
9216
|
+
return target === null ? void 0 : path19.resolve(gitDir, target);
|
|
7588
9217
|
}
|
|
7589
9218
|
async function headIn(dir, kind, io) {
|
|
7590
|
-
const marker =
|
|
9219
|
+
const marker = path19.join(dir, ".git");
|
|
7591
9220
|
let gitDir;
|
|
7592
9221
|
if (kind === "directory") {
|
|
7593
9222
|
gitDir = marker;
|
|
@@ -7595,11 +9224,11 @@ async function headIn(dir, kind, io) {
|
|
|
7595
9224
|
const content2 = await readGitFile(io, marker);
|
|
7596
9225
|
const target = typeof content2 === "string" ? parseGitFile(content2) : null;
|
|
7597
9226
|
if (target === null) return null;
|
|
7598
|
-
gitDir =
|
|
9227
|
+
gitDir = path19.resolve(dir, target);
|
|
7599
9228
|
} else {
|
|
7600
9229
|
return null;
|
|
7601
9230
|
}
|
|
7602
|
-
const content = await readGitFile(io,
|
|
9231
|
+
const content = await readGitFile(io, path19.join(gitDir, "HEAD"));
|
|
7603
9232
|
const head = typeof content === "string" ? parseHead(content) : null;
|
|
7604
9233
|
if (head === null) return null;
|
|
7605
9234
|
const commonDir = kind === "file" ? await commonDirOf(io, gitDir) : null;
|
|
@@ -7608,20 +9237,20 @@ async function headIn(dir, kind, io) {
|
|
|
7608
9237
|
return { head: { ...head, repository }, gitFolder: commonDir ?? gitDir };
|
|
7609
9238
|
}
|
|
7610
9239
|
async function findHead(cwd, { io, homeDir }) {
|
|
7611
|
-
if (!
|
|
7612
|
-
const home =
|
|
7613
|
-
let dir =
|
|
9240
|
+
if (!path19.isAbsolute(cwd)) return null;
|
|
9241
|
+
const home = path19.resolve(homeDir);
|
|
9242
|
+
let dir = path19.resolve(cwd);
|
|
7614
9243
|
try {
|
|
7615
9244
|
if ((await io.stat(dir)).kind !== "directory") return null;
|
|
7616
9245
|
} catch {
|
|
7617
9246
|
return null;
|
|
7618
9247
|
}
|
|
7619
9248
|
for (let level = 0; level < MAX_LEVELS; level += 1) {
|
|
7620
|
-
const parent =
|
|
9249
|
+
const parent = path19.dirname(dir);
|
|
7621
9250
|
if (dir === home || dir === parent) return null;
|
|
7622
9251
|
let kind;
|
|
7623
9252
|
try {
|
|
7624
|
-
kind = (await io.lstat(
|
|
9253
|
+
kind = (await io.lstat(path19.join(dir, ".git"))).kind;
|
|
7625
9254
|
} catch (error) {
|
|
7626
9255
|
if (!isMissing2(error)) return null;
|
|
7627
9256
|
dir = parent;
|
|
@@ -7642,7 +9271,7 @@ function within(work, ms) {
|
|
|
7642
9271
|
});
|
|
7643
9272
|
}
|
|
7644
9273
|
function createBranchFinder(options = {}) {
|
|
7645
|
-
const where = { io: options.io ?? nodeIo2, homeDir: options.homeDir ??
|
|
9274
|
+
const where = { io: options.io ?? nodeIo2, homeDir: options.homeDir ?? os8.homedir() };
|
|
7646
9275
|
const now = options.now ?? Date.now;
|
|
7647
9276
|
const waitMs = options.waitMs ?? READ_WAIT_MS;
|
|
7648
9277
|
const known = /* @__PURE__ */ new Map();
|
|
@@ -7704,14 +9333,14 @@ var STATUS_STATE = {
|
|
|
7704
9333
|
PENDING: "pending",
|
|
7705
9334
|
EXPECTED: "pending"
|
|
7706
9335
|
};
|
|
7707
|
-
function
|
|
9336
|
+
function isRecord12(value) {
|
|
7708
9337
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7709
9338
|
}
|
|
7710
9339
|
function textOf(value) {
|
|
7711
9340
|
return typeof value === "string" ? value : "";
|
|
7712
9341
|
}
|
|
7713
9342
|
function checkOf(value) {
|
|
7714
|
-
if (!
|
|
9343
|
+
if (!isRecord12(value)) return null;
|
|
7715
9344
|
const startedAt = Date.parse(textOf(value.startedAt));
|
|
7716
9345
|
const at = Number.isNaN(startedAt) ? -Infinity : startedAt;
|
|
7717
9346
|
if (value.__typename === "CheckRun") {
|
|
@@ -7747,7 +9376,7 @@ function checksOf(rollup) {
|
|
|
7747
9376
|
for (const { state } of latest.values()) counts[state] += 1;
|
|
7748
9377
|
return { state: checksStateOf(counts), ...counts };
|
|
7749
9378
|
}
|
|
7750
|
-
function
|
|
9379
|
+
function stateOf3(state, isDraft) {
|
|
7751
9380
|
if (state === "OPEN") return isDraft === true ? "draft" : "open";
|
|
7752
9381
|
if (state === "MERGED") return "merged";
|
|
7753
9382
|
if (state === "CLOSED") return "closed";
|
|
@@ -7760,9 +9389,9 @@ function readGhAnswer(stdout, repository) {
|
|
|
7760
9389
|
} catch {
|
|
7761
9390
|
return null;
|
|
7762
9391
|
}
|
|
7763
|
-
if (!
|
|
9392
|
+
if (!isRecord12(data)) return null;
|
|
7764
9393
|
const { number } = data;
|
|
7765
|
-
const state =
|
|
9394
|
+
const state = stateOf3(data.state, data.isDraft);
|
|
7766
9395
|
const title = typeof data.title === "string" ? sessionName(data.title) : void 0;
|
|
7767
9396
|
if (!isPullRequestNumber(number) || state === null || title === void 0) return null;
|
|
7768
9397
|
return {
|
|
@@ -7899,7 +9528,7 @@ function createGhAsker(options) {
|
|
|
7899
9528
|
}
|
|
7900
9529
|
|
|
7901
9530
|
// src/collector/git/pullRequestTarget.ts
|
|
7902
|
-
import
|
|
9531
|
+
import path20 from "node:path";
|
|
7903
9532
|
|
|
7904
9533
|
// src/collector/git/gitConfig.ts
|
|
7905
9534
|
var MAX_CONFIG_BYTES = 64 * 1024;
|
|
@@ -8157,7 +9786,7 @@ function targetOf(config, branch, base, defaultBranch) {
|
|
|
8157
9786
|
async function readPullRequestTarget(gitFolder, branch, io = nodeIo2) {
|
|
8158
9787
|
try {
|
|
8159
9788
|
if (!isAskableBranch(branch)) return null;
|
|
8160
|
-
const text4 = await readGitFile(io,
|
|
9789
|
+
const text4 = await readGitFile(io, path20.join(gitFolder, "config"), MAX_CONFIG_BYTES);
|
|
8161
9790
|
if (typeof text4 !== "string") return null;
|
|
8162
9791
|
const config = parseGitConfig(text4);
|
|
8163
9792
|
const base = config === null ? null : baseRemoteOf(config);
|
|
@@ -8166,7 +9795,7 @@ async function readPullRequestTarget(gitFolder, branch, io = nodeIo2) {
|
|
|
8166
9795
|
if (base.name !== null && isSafeRemoteName(base.name)) {
|
|
8167
9796
|
const head = await readGitFile(
|
|
8168
9797
|
io,
|
|
8169
|
-
|
|
9798
|
+
path20.join(gitFolder, "refs", "remotes", base.name, "HEAD")
|
|
8170
9799
|
);
|
|
8171
9800
|
if (typeof head === "string") defaultBranch = parseRemoteHead(head, base.name);
|
|
8172
9801
|
}
|
|
@@ -8337,7 +9966,7 @@ function createClearHistoryRoute(options) {
|
|
|
8337
9966
|
}
|
|
8338
9967
|
|
|
8339
9968
|
// src/collector/history/historyKeeper.ts
|
|
8340
|
-
import
|
|
9969
|
+
import path21 from "node:path";
|
|
8341
9970
|
|
|
8342
9971
|
// src/collector/history/historyFiles.ts
|
|
8343
9972
|
import {
|
|
@@ -8637,7 +10266,7 @@ function createHistoryKeeper(options) {
|
|
|
8637
10266
|
});
|
|
8638
10267
|
const lock = createWriterLock({
|
|
8639
10268
|
fs,
|
|
8640
|
-
file:
|
|
10269
|
+
file: path21.join(dir, LOCK_FILE),
|
|
8641
10270
|
pid: options.pid,
|
|
8642
10271
|
now,
|
|
8643
10272
|
isAlive: options.isAlive,
|
|
@@ -8663,7 +10292,7 @@ function createHistoryKeeper(options) {
|
|
|
8663
10292
|
});
|
|
8664
10293
|
return next;
|
|
8665
10294
|
}
|
|
8666
|
-
const full = (name) =>
|
|
10295
|
+
const full = (name) => path21.join(dir, name);
|
|
8667
10296
|
function hold(record) {
|
|
8668
10297
|
if (unusable !== null || !running) return;
|
|
8669
10298
|
const line = encodeRecord(record);
|
|
@@ -8830,13 +10459,13 @@ function createHistoryKeeper(options) {
|
|
|
8830
10459
|
continue;
|
|
8831
10460
|
}
|
|
8832
10461
|
let text4 = lead;
|
|
8833
|
-
let
|
|
10462
|
+
let bytes3 = lead.length;
|
|
8834
10463
|
let end = index;
|
|
8835
10464
|
while (end < lines.length) {
|
|
8836
10465
|
const lineBytes = byteLength(lines[end]);
|
|
8837
|
-
if (
|
|
10466
|
+
if (bytes3 - lead.length + lineBytes > room) break;
|
|
8838
10467
|
text4 += lines[end];
|
|
8839
|
-
|
|
10468
|
+
bytes3 += lineBytes;
|
|
8840
10469
|
end += 1;
|
|
8841
10470
|
}
|
|
8842
10471
|
const outcome = yield { name, text: text4 };
|
|
@@ -8849,7 +10478,7 @@ function createHistoryKeeper(options) {
|
|
|
8849
10478
|
const parsed = readHistoryFileName(name);
|
|
8850
10479
|
files.set(name, {
|
|
8851
10480
|
...parsed,
|
|
8852
|
-
size: size +
|
|
10481
|
+
size: size + bytes3,
|
|
8853
10482
|
first: known?.first ?? (record && { at: recordAt(record), kind: record.kind })
|
|
8854
10483
|
});
|
|
8855
10484
|
touched.add(name);
|
|
@@ -9077,8 +10706,8 @@ function createHistoryKeeper(options) {
|
|
|
9077
10706
|
return { at: oldest.first.at, by: BEGINNING[oldest.first.kind] };
|
|
9078
10707
|
},
|
|
9079
10708
|
status() {
|
|
9080
|
-
let
|
|
9081
|
-
for (const file of files.values())
|
|
10709
|
+
let bytes3 = 0;
|
|
10710
|
+
for (const file of files.values()) bytes3 += file.size;
|
|
9082
10711
|
let problem = unusable ?? failing;
|
|
9083
10712
|
if (problem === null && writer === false) {
|
|
9084
10713
|
problem = lock.heldByOtherSources ? "Another copy of Agent Lookout on this computer is writing the history, and it reads sessions from other folders than this one. This one keeps what it sees in memory, and takes over when that one stops." : "Another copy of Agent Lookout on this computer is writing the history. This one keeps what it sees in memory, and takes over when that one stops.";
|
|
@@ -9086,7 +10715,7 @@ function createHistoryKeeper(options) {
|
|
|
9086
10715
|
return {
|
|
9087
10716
|
where: "disk",
|
|
9088
10717
|
folder,
|
|
9089
|
-
bytes:
|
|
10718
|
+
bytes: bytes3,
|
|
9090
10719
|
maxBytes,
|
|
9091
10720
|
maxAgeMs,
|
|
9092
10721
|
canClear: unusable === null && writer === true && lock.held,
|
|
@@ -9123,15 +10752,15 @@ function keptHistoryStore(store, keeper) {
|
|
|
9123
10752
|
}
|
|
9124
10753
|
|
|
9125
10754
|
// src/collector/history/historySettings.ts
|
|
9126
|
-
import { createHash as
|
|
9127
|
-
import
|
|
9128
|
-
import
|
|
10755
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
10756
|
+
import os9 from "node:os";
|
|
10757
|
+
import path22 from "node:path";
|
|
9129
10758
|
var HISTORY_ENV = "AGENT_LOOKOUT_HISTORY";
|
|
9130
10759
|
var HISTORY_DIR_ENV = "AGENT_LOOKOUT_HISTORY_DIR";
|
|
9131
|
-
function readHistorySetup(env, homeDir =
|
|
10760
|
+
function readHistorySetup(env, homeDir = os9.homedir()) {
|
|
9132
10761
|
if (env[HISTORY_ENV]?.trim().toLowerCase() === "off") return { on: false };
|
|
9133
10762
|
const override = env[HISTORY_DIR_ENV]?.trim() || void 0;
|
|
9134
|
-
const dir =
|
|
10763
|
+
const dir = path22.resolve(override ?? path22.join(homeDir, ".agent-lookout", "history"));
|
|
9135
10764
|
return {
|
|
9136
10765
|
on: true,
|
|
9137
10766
|
dir,
|
|
@@ -9139,14 +10768,15 @@ function readHistorySetup(env, homeDir = os8.homedir()) {
|
|
|
9139
10768
|
sources: sourcesFingerprint(env, homeDir)
|
|
9140
10769
|
};
|
|
9141
10770
|
}
|
|
9142
|
-
function sourcesFingerprint(env, homeDir =
|
|
10771
|
+
function sourcesFingerprint(env, homeDir = os9.homedir()) {
|
|
9143
10772
|
const set = (name) => env[name]?.trim() || void 0;
|
|
9144
10773
|
const folders = [
|
|
9145
|
-
|
|
9146
|
-
|
|
9147
|
-
|
|
10774
|
+
path22.resolve(set(CLAUDE_HOME_ENV) ?? path22.join(homeDir, ".claude")),
|
|
10775
|
+
path22.resolve(set(CODEX_HOME_ENV) ?? set(CODEX_OWN_HOME_ENV) ?? path22.join(homeDir, ".codex")),
|
|
10776
|
+
path22.resolve(set(STATUS_DIR_ENV) ?? path22.join(homeDir, ".agent-lookout", "sessions")),
|
|
10777
|
+
path22.resolve(set(ANTIGRAVITY_HOME_ENV) ?? path22.join(homeDir, ...DEFAULT_HOME))
|
|
9148
10778
|
];
|
|
9149
|
-
return
|
|
10779
|
+
return createHash4("sha256").update(JSON.stringify(folders)).digest("hex").slice(0, 32);
|
|
9150
10780
|
}
|
|
9151
10781
|
|
|
9152
10782
|
// src/collector/notifications/heldWait.ts
|
|
@@ -9187,9 +10817,9 @@ function createServerNotifications(options) {
|
|
|
9187
10817
|
const held = /* @__PURE__ */ new Map();
|
|
9188
10818
|
const reminders = createReminderWatch();
|
|
9189
10819
|
const quiet = createQuietHold();
|
|
9190
|
-
function show(notice) {
|
|
10820
|
+
function show(notice, about) {
|
|
9191
10821
|
try {
|
|
9192
|
-
notifier.show(notice);
|
|
10822
|
+
notifier.show(notice, about);
|
|
9193
10823
|
} catch {
|
|
9194
10824
|
}
|
|
9195
10825
|
}
|
|
@@ -9217,7 +10847,7 @@ function createServerNotifications(options) {
|
|
|
9217
10847
|
const at = now();
|
|
9218
10848
|
const rules = rulesOf(snapshot);
|
|
9219
10849
|
const quietNow = quietOf(snapshot);
|
|
9220
|
-
const
|
|
10850
|
+
const schedule = reminderSchedule(rules.longWait);
|
|
9221
10851
|
const result = sessionChanges(memory, snapshot);
|
|
9222
10852
|
memory = result.memory;
|
|
9223
10853
|
for (const id of result.stopped) {
|
|
@@ -9243,7 +10873,7 @@ function createServerNotifications(options) {
|
|
|
9243
10873
|
change: { event: "needs-you", session },
|
|
9244
10874
|
since: at
|
|
9245
10875
|
});
|
|
9246
|
-
if (rules.longWait.on) reminders.
|
|
10876
|
+
if (rules.longWait.on) reminders.told(session.id, at);
|
|
9247
10877
|
}
|
|
9248
10878
|
}
|
|
9249
10879
|
if (quietNow) quiet.begin(at);
|
|
@@ -9253,9 +10883,13 @@ function createServerNotifications(options) {
|
|
|
9253
10883
|
else
|
|
9254
10884
|
held.set(heldKey(change.event, change.session.id), { kind: "change", change, since: at });
|
|
9255
10885
|
}
|
|
9256
|
-
if (
|
|
9257
|
-
for (const reminder of reminders.due(snapshot, at,
|
|
9258
|
-
|
|
10886
|
+
if (rules.longWait.on && wants("needs-you")) {
|
|
10887
|
+
for (const reminder of reminders.due(snapshot, at, schedule)) {
|
|
10888
|
+
if (quietNow) {
|
|
10889
|
+
quiet.holdReminder(reminder.session, reminder.begunAt);
|
|
10890
|
+
continue;
|
|
10891
|
+
}
|
|
10892
|
+
reminders.told(reminder.session.id, at);
|
|
9259
10893
|
held.set(reminderKey(reminder.session.id), { kind: "reminder", reminder, since: at });
|
|
9260
10894
|
}
|
|
9261
10895
|
}
|
|
@@ -9282,82 +10916,505 @@ function createServerNotifications(options) {
|
|
|
9282
10916
|
}
|
|
9283
10917
|
continue;
|
|
9284
10918
|
}
|
|
9285
|
-
if (entry.kind === "change")
|
|
9286
|
-
|
|
9287
|
-
show(
|
|
10919
|
+
if (entry.kind === "change") {
|
|
10920
|
+
const { change } = entry;
|
|
10921
|
+
show(
|
|
10922
|
+
changeNotice(change),
|
|
10923
|
+
change.event === "needs-you" ? { sessionId: change.session.id } : void 0
|
|
10924
|
+
);
|
|
10925
|
+
} else if (entry.kind === "reminder") {
|
|
10926
|
+
const { session, begunAt } = entry.reminder;
|
|
10927
|
+
const waitedMs = at - begunAt;
|
|
10928
|
+
show(reminderNotice(session, waitedMs), { sessionId: session.id, waitedMs });
|
|
9288
10929
|
} else show(summaryNotice(entry.summary.items));
|
|
9289
10930
|
}
|
|
9290
10931
|
}
|
|
9291
10932
|
};
|
|
9292
10933
|
}
|
|
9293
10934
|
|
|
9294
|
-
// src/collector/processes/osascript.ts
|
|
9295
|
-
import { execFile as execFile3 } from "node:child_process";
|
|
9296
|
-
var OSASCRIPT = "/usr/bin/osascript";
|
|
9297
|
-
var MAX_OUTPUT_BYTES4 = 64 * 1024;
|
|
9298
|
-
var NOT_RUN2 = { ok: false, stderr: "", timedOut: false };
|
|
9299
|
-
function runOsascript(args, options = {}) {
|
|
9300
|
-
return new Promise((resolve) => {
|
|
10935
|
+
// src/collector/processes/osascript.ts
|
|
10936
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
10937
|
+
var OSASCRIPT = "/usr/bin/osascript";
|
|
10938
|
+
var MAX_OUTPUT_BYTES4 = 64 * 1024;
|
|
10939
|
+
var NOT_RUN2 = { ok: false, stderr: "", timedOut: false };
|
|
10940
|
+
function runOsascript(args, options = {}) {
|
|
10941
|
+
return new Promise((resolve) => {
|
|
10942
|
+
try {
|
|
10943
|
+
const child = execFile3(
|
|
10944
|
+
OSASCRIPT,
|
|
10945
|
+
[...args],
|
|
10946
|
+
{
|
|
10947
|
+
timeout: options.timeoutMs ?? TERMINAL_JUMP_TIMEOUT_MS,
|
|
10948
|
+
maxBuffer: MAX_OUTPUT_BYTES4,
|
|
10949
|
+
encoding: "utf8",
|
|
10950
|
+
windowsHide: true
|
|
10951
|
+
},
|
|
10952
|
+
(error, stdout, stderr) => {
|
|
10953
|
+
if (!error) {
|
|
10954
|
+
resolve({ ok: true, stdout: String(stdout ?? "") });
|
|
10955
|
+
return;
|
|
10956
|
+
}
|
|
10957
|
+
const timedOut = error.killed === true;
|
|
10958
|
+
resolve({ ok: false, stderr: String(stderr ?? "").trim(), timedOut });
|
|
10959
|
+
}
|
|
10960
|
+
);
|
|
10961
|
+
child.stdin?.end();
|
|
10962
|
+
} catch {
|
|
10963
|
+
resolve(NOT_RUN2);
|
|
10964
|
+
}
|
|
10965
|
+
});
|
|
10966
|
+
}
|
|
10967
|
+
|
|
10968
|
+
// src/collector/notifications/systemNotifier.ts
|
|
10969
|
+
var OSASCRIPT_TIMEOUT_MS = 5e3;
|
|
10970
|
+
var SCRIPT = [
|
|
10971
|
+
"on run argv",
|
|
10972
|
+
"display notification (item 2 of argv) with title (item 1 of argv)",
|
|
10973
|
+
"end run"
|
|
10974
|
+
];
|
|
10975
|
+
function osascriptArgs(notice) {
|
|
10976
|
+
return [
|
|
10977
|
+
...SCRIPT.flatMap((line) => ["-e", line]),
|
|
10978
|
+
"--",
|
|
10979
|
+
oneLine(notice.title),
|
|
10980
|
+
oneLine(notice.body, MAX_NOTICE_TEXT_LENGTH)
|
|
10981
|
+
];
|
|
10982
|
+
}
|
|
10983
|
+
function systemNotificationsShownOn(platform) {
|
|
10984
|
+
return platform === "darwin";
|
|
10985
|
+
}
|
|
10986
|
+
var runForNotification = (args) => runOsascript(args, { timeoutMs: OSASCRIPT_TIMEOUT_MS });
|
|
10987
|
+
function createSystemNotifier(options = {}) {
|
|
10988
|
+
const platform = options.platform ?? process.platform;
|
|
10989
|
+
const run = options.run ?? runForNotification;
|
|
10990
|
+
return {
|
|
10991
|
+
show(notice) {
|
|
10992
|
+
if (!systemNotificationsShownOn(platform)) return;
|
|
10993
|
+
try {
|
|
10994
|
+
run(osascriptArgs(notice)).catch(() => {
|
|
10995
|
+
});
|
|
10996
|
+
} catch {
|
|
10997
|
+
}
|
|
10998
|
+
}
|
|
10999
|
+
};
|
|
11000
|
+
}
|
|
11001
|
+
|
|
11002
|
+
// src/collector/outbound/httpPost.ts
|
|
11003
|
+
var POST_TIMEOUT_MS = 1e4;
|
|
11004
|
+
function jsonHeaders(body, version, extra = {}) {
|
|
11005
|
+
return {
|
|
11006
|
+
"Content-Type": "application/json",
|
|
11007
|
+
"Content-Length": String(Buffer.byteLength(body)),
|
|
11008
|
+
"User-Agent": `Agent Lookout/${version}`,
|
|
11009
|
+
...extra
|
|
11010
|
+
};
|
|
11011
|
+
}
|
|
11012
|
+
function postOptions(body, version, extra = {}) {
|
|
11013
|
+
return {
|
|
11014
|
+
method: "POST",
|
|
11015
|
+
headers: jsonHeaders(body, version, extra),
|
|
11016
|
+
// A connection of its own, closed after the answer.
|
|
11017
|
+
agent: false,
|
|
11018
|
+
rejectUnauthorized: true
|
|
11019
|
+
};
|
|
11020
|
+
}
|
|
11021
|
+
function createJsonPoster(url, options) {
|
|
11022
|
+
const timeoutMs = options.timeoutMs ?? POST_TIMEOUT_MS;
|
|
11023
|
+
const extra = options.headers ?? {};
|
|
11024
|
+
let client = null;
|
|
11025
|
+
const load = () => {
|
|
11026
|
+
client ??= url.protocol === "https:" ? import("node:https").then(({ request: request2 }) => request2) : import("node:http").then(({ request: request2 }) => request2);
|
|
11027
|
+
return client;
|
|
11028
|
+
};
|
|
11029
|
+
return {
|
|
11030
|
+
async post(body) {
|
|
11031
|
+
let request2;
|
|
11032
|
+
try {
|
|
11033
|
+
request2 = await load();
|
|
11034
|
+
} catch {
|
|
11035
|
+
return { kind: "unsent" };
|
|
11036
|
+
}
|
|
11037
|
+
return new Promise((resolve) => {
|
|
11038
|
+
let finished = false;
|
|
11039
|
+
const finish = (result) => {
|
|
11040
|
+
if (finished) return;
|
|
11041
|
+
finished = true;
|
|
11042
|
+
resolve(result);
|
|
11043
|
+
};
|
|
11044
|
+
let req;
|
|
11045
|
+
try {
|
|
11046
|
+
req = request2(url, postOptions(body, options.version, extra));
|
|
11047
|
+
} catch {
|
|
11048
|
+
finish({ kind: "unsent" });
|
|
11049
|
+
return;
|
|
11050
|
+
}
|
|
11051
|
+
const timer = setTimeout(() => {
|
|
11052
|
+
finish({ kind: "timeout" });
|
|
11053
|
+
req.destroy();
|
|
11054
|
+
}, timeoutMs);
|
|
11055
|
+
timer.unref();
|
|
11056
|
+
req.on("response", (res) => {
|
|
11057
|
+
finish({ kind: "status", status: res.statusCode ?? 0 });
|
|
11058
|
+
res.on("close", () => clearTimeout(timer));
|
|
11059
|
+
res.resume();
|
|
11060
|
+
});
|
|
11061
|
+
req.on("error", (error) => {
|
|
11062
|
+
clearTimeout(timer);
|
|
11063
|
+
const { code } = error ?? {};
|
|
11064
|
+
finish({ kind: "error", code: typeof code === "string" ? code : null });
|
|
11065
|
+
});
|
|
11066
|
+
req.end(body);
|
|
11067
|
+
});
|
|
11068
|
+
}
|
|
11069
|
+
};
|
|
11070
|
+
}
|
|
11071
|
+
function connectionReason(code, words) {
|
|
11072
|
+
if (code === null) return words.notSent;
|
|
11073
|
+
if (code === "ECONNREFUSED") return `nothing answered at ${words.where}`;
|
|
11074
|
+
if (code === "ENOTFOUND" || code === "EAI_AGAIN") return `${words.whose} host could not be found`;
|
|
11075
|
+
if (code === "ETIMEDOUT") return `${words.where} did not answer in time`;
|
|
11076
|
+
if (/CERT|SELF_SIGNED|UNABLE_TO|^ERR_TLS|^ERR_SSL|^EPROTO$/.test(code)) {
|
|
11077
|
+
return `the secure connection to ${words.where} failed`;
|
|
11078
|
+
}
|
|
11079
|
+
return `the connection to ${words.where} failed`;
|
|
11080
|
+
}
|
|
11081
|
+
function outcomeOfPost(result, words) {
|
|
11082
|
+
switch (result.kind) {
|
|
11083
|
+
case "unsent":
|
|
11084
|
+
return { sent: false, reason: words.notSent };
|
|
11085
|
+
case "timeout":
|
|
11086
|
+
return { sent: false, reason: `${words.where} did not answer in time` };
|
|
11087
|
+
case "error":
|
|
11088
|
+
return { sent: false, reason: connectionReason(result.code, words) };
|
|
11089
|
+
case "status": {
|
|
11090
|
+
const { status } = result;
|
|
11091
|
+
if (status >= 200 && status < 300) return { sent: true };
|
|
11092
|
+
if (status >= 300 && status < 400) {
|
|
11093
|
+
return {
|
|
11094
|
+
sent: false,
|
|
11095
|
+
reason: `${words.where} answered with a redirect, which is not followed`
|
|
11096
|
+
};
|
|
11097
|
+
}
|
|
11098
|
+
if (status >= 400 && status < 500) return { sent: false, reason: words.refused(status) };
|
|
11099
|
+
if (status >= 500 && status < 600) return { sent: false, reason: words.troubled(status) };
|
|
11100
|
+
return { sent: false, reason: `${words.whose} answer could not be understood` };
|
|
11101
|
+
}
|
|
11102
|
+
}
|
|
11103
|
+
}
|
|
11104
|
+
|
|
11105
|
+
// src/collector/ntfy/ntfyMessage.ts
|
|
11106
|
+
var TAG = {
|
|
11107
|
+
"needs-you": "hourglass",
|
|
11108
|
+
reminder: "hourglass",
|
|
11109
|
+
test: "hourglass",
|
|
11110
|
+
finished: "white_check_mark",
|
|
11111
|
+
failed: "x",
|
|
11112
|
+
ended: "stop_sign",
|
|
11113
|
+
"quiet-summary": null
|
|
11114
|
+
};
|
|
11115
|
+
function ntfyBody(topic, message) {
|
|
11116
|
+
const tag = TAG[message.kind];
|
|
11117
|
+
return {
|
|
11118
|
+
topic,
|
|
11119
|
+
title: message.title,
|
|
11120
|
+
message: message.message,
|
|
11121
|
+
// A test sounds as a wait would, so it shows how one will arrive.
|
|
11122
|
+
priority: isWaitKind(message.kind) ? 4 : 3,
|
|
11123
|
+
...tag !== null && { tags: [tag] }
|
|
11124
|
+
};
|
|
11125
|
+
}
|
|
11126
|
+
|
|
11127
|
+
// src/collector/ntfy/ntfySender.ts
|
|
11128
|
+
var NTFY_WORDS = {
|
|
11129
|
+
where: "the ntfy server",
|
|
11130
|
+
whose: "the ntfy server's",
|
|
11131
|
+
notSent: "the push could not be sent",
|
|
11132
|
+
refused(status) {
|
|
11133
|
+
if (status === 401 || status === 403) {
|
|
11134
|
+
return `the ntfy server refused the access token or the topic (status ${status})`;
|
|
11135
|
+
}
|
|
11136
|
+
if (status === 413) return `the ntfy server refused the push as too large (status ${status})`;
|
|
11137
|
+
if (status === 429) return `the ntfy server's rate limit was reached (status ${status})`;
|
|
11138
|
+
return `the ntfy server refused the push (status ${status})`;
|
|
11139
|
+
},
|
|
11140
|
+
troubled: (status) => `the ntfy server had a problem (status ${status})`
|
|
11141
|
+
};
|
|
11142
|
+
function ntfyHeaders(settings) {
|
|
11143
|
+
return settings.token === null ? {} : { Authorization: `Bearer ${settings.token}` };
|
|
11144
|
+
}
|
|
11145
|
+
function createNtfySender(settings, options) {
|
|
11146
|
+
const poster = createJsonPoster(settings.server, { ...options, headers: ntfyHeaders(settings) });
|
|
11147
|
+
return {
|
|
11148
|
+
async send(message) {
|
|
11149
|
+
let body;
|
|
11150
|
+
try {
|
|
11151
|
+
body = JSON.stringify(ntfyBody(settings.topic, message));
|
|
11152
|
+
} catch {
|
|
11153
|
+
return { sent: false, reason: NTFY_WORDS.notSent };
|
|
11154
|
+
}
|
|
11155
|
+
return outcomeOfPost(await poster.post(body), NTFY_WORDS);
|
|
11156
|
+
}
|
|
11157
|
+
};
|
|
11158
|
+
}
|
|
11159
|
+
|
|
11160
|
+
// src/collector/ntfy/ntfySettings.ts
|
|
11161
|
+
var NTFY_URL_ENV = "AGENT_LOOKOUT_NTFY_URL";
|
|
11162
|
+
var NTFY_TOKEN_ENV = "AGENT_LOOKOUT_NTFY_TOKEN";
|
|
11163
|
+
var NTFY_EVENTS_ENV = "AGENT_LOOKOUT_NTFY_EVENTS";
|
|
11164
|
+
var NTFY_AFTER_ENV = "AGENT_LOOKOUT_NTFY_AFTER";
|
|
11165
|
+
var NTFY_ASKING_ENV = "AGENT_LOOKOUT_NTFY_ASKING";
|
|
11166
|
+
var MAX_URL_LENGTH = 2048;
|
|
11167
|
+
var TOPIC = /^[-_A-Za-z0-9]{1,64}$/;
|
|
11168
|
+
var PATH_PART = /^[-_.~A-Za-z0-9]+$/;
|
|
11169
|
+
var TOKEN = /^[-A-Za-z0-9._~+/=]{1,128}$/;
|
|
11170
|
+
var URL_UNREADABLE2 = `${NTFY_URL_ENV} could not be read. Copy the topic's whole address, beginning https://.`;
|
|
11171
|
+
function readTopicUrl(value) {
|
|
11172
|
+
let url;
|
|
11173
|
+
try {
|
|
11174
|
+
if (value.length > MAX_URL_LENGTH) throw new Error();
|
|
11175
|
+
url = new URL(value);
|
|
11176
|
+
} catch {
|
|
11177
|
+
throw new SettingProblem(URL_UNREADABLE2);
|
|
11178
|
+
}
|
|
11179
|
+
const local = LOCAL_HOSTS.has(url.hostname);
|
|
11180
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && local)) {
|
|
11181
|
+
throw new SettingProblem(
|
|
11182
|
+
`${NTFY_URL_ENV} must begin with https://, or with http:// for a server on this computer, 127.0.0.1 or localhost.`
|
|
11183
|
+
);
|
|
11184
|
+
}
|
|
11185
|
+
if (url.hostname === "") throw new SettingProblem(URL_UNREADABLE2);
|
|
11186
|
+
if (!isWebhookHost(url.hostname)) {
|
|
11187
|
+
throw new SettingProblem(
|
|
11188
|
+
`${NTFY_URL_ENV} must name its host in letters, digits, dots and dashes, such as ntfy.sh.`
|
|
11189
|
+
);
|
|
11190
|
+
}
|
|
11191
|
+
if (url.username !== "" || url.password !== "") {
|
|
11192
|
+
throw new SettingProblem(
|
|
11193
|
+
`${NTFY_URL_ENV} must not hold a user name or a password. Put an access token in ${NTFY_TOKEN_ENV}.`
|
|
11194
|
+
);
|
|
11195
|
+
}
|
|
11196
|
+
if (url.search !== "" || url.hash !== "" || value.includes("?") || value.includes("#")) {
|
|
11197
|
+
throw new SettingProblem(
|
|
11198
|
+
`${NTFY_URL_ENV} must end with the topic, with no ? or # after it. Put an access token in ${NTFY_TOKEN_ENV}.`
|
|
11199
|
+
);
|
|
11200
|
+
}
|
|
11201
|
+
const parts = url.pathname.split("/").filter((part) => part !== "");
|
|
11202
|
+
const topic = parts.pop();
|
|
11203
|
+
if (topic === void 0 || !TOPIC.test(topic) || !parts.every((part) => PATH_PART.test(part))) {
|
|
11204
|
+
throw new SettingProblem(
|
|
11205
|
+
`${NTFY_URL_ENV} must end with the topic, up to 64 letters, digits, dashes and underscores, as in https://ntfy.sh/your-topic.`
|
|
11206
|
+
);
|
|
11207
|
+
}
|
|
11208
|
+
const server = new URL(url.origin);
|
|
11209
|
+
server.pathname = `/${parts.map((part) => `${part}/`).join("")}`;
|
|
11210
|
+
return { server, topic };
|
|
11211
|
+
}
|
|
11212
|
+
function readToken(value) {
|
|
11213
|
+
if (value === void 0) return null;
|
|
11214
|
+
if (!TOKEN.test(value)) {
|
|
11215
|
+
throw new SettingProblem(
|
|
11216
|
+
`${NTFY_TOKEN_ENV} must be an access token of letters, digits and . _ ~ + / = -, such as one that begins tk_.`
|
|
11217
|
+
);
|
|
11218
|
+
}
|
|
11219
|
+
return value;
|
|
11220
|
+
}
|
|
11221
|
+
function readNtfySetup(env) {
|
|
11222
|
+
const url = valueOf(env, NTFY_URL_ENV);
|
|
11223
|
+
if (url === void 0) return { on: false, problem: null };
|
|
11224
|
+
try {
|
|
11225
|
+
return {
|
|
11226
|
+
on: true,
|
|
11227
|
+
settings: {
|
|
11228
|
+
...readTopicUrl(url),
|
|
11229
|
+
token: readToken(valueOf(env, NTFY_TOKEN_ENV)),
|
|
11230
|
+
events: readEvents(NTFY_EVENTS_ENV, valueOf(env, NTFY_EVENTS_ENV)),
|
|
11231
|
+
afterMs: readAfter(NTFY_AFTER_ENV, valueOf(env, NTFY_AFTER_ENV)),
|
|
11232
|
+
asking: readAsking(NTFY_ASKING_ENV, valueOf(env, NTFY_ASKING_ENV))
|
|
11233
|
+
}
|
|
11234
|
+
};
|
|
11235
|
+
} catch (error) {
|
|
11236
|
+
if (error instanceof SettingProblem) return { on: false, problem: error.message };
|
|
11237
|
+
return { on: false, problem: URL_UNREADABLE2 };
|
|
11238
|
+
}
|
|
11239
|
+
}
|
|
11240
|
+
function ntfyProblemLine(problem) {
|
|
11241
|
+
return `ntfy pushes are off: ${problem}`;
|
|
11242
|
+
}
|
|
11243
|
+
|
|
11244
|
+
// src/collector/outbound/testSendRoute.ts
|
|
11245
|
+
var MAX_TEST_BODY_BYTES = 64;
|
|
11246
|
+
var NAMED = {
|
|
11247
|
+
ntfy: { name: "ntfy", setting: "AGENT_LOOKOUT_NTFY_URL" },
|
|
11248
|
+
pushover: {
|
|
11249
|
+
name: "Pushover",
|
|
11250
|
+
setting: "AGENT_LOOKOUT_PUSHOVER_TOKEN and AGENT_LOOKOUT_PUSHOVER_USER"
|
|
11251
|
+
}
|
|
11252
|
+
};
|
|
11253
|
+
function channelIn(body) {
|
|
11254
|
+
let value;
|
|
11255
|
+
try {
|
|
11256
|
+
value = JSON.parse(body);
|
|
11257
|
+
} catch {
|
|
11258
|
+
return null;
|
|
11259
|
+
}
|
|
11260
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
11261
|
+
const keys = Object.keys(value);
|
|
11262
|
+
if (keys.length !== 1 || keys[0] !== "channel") return null;
|
|
11263
|
+
const { channel } = value;
|
|
11264
|
+
return PHONE_CHANNELS.find((known) => known === channel) ?? null;
|
|
11265
|
+
}
|
|
11266
|
+
function asSentence(reason) {
|
|
11267
|
+
return `${reason.charAt(0).toUpperCase()}${reason.slice(1)}.`;
|
|
11268
|
+
}
|
|
11269
|
+
var failed3 = (status, reason, error, more = {}) => ({
|
|
11270
|
+
status,
|
|
11271
|
+
body: {
|
|
11272
|
+
error,
|
|
11273
|
+
reason,
|
|
11274
|
+
...more.limitedUntil !== void 0 && { limitedUntil: more.limitedUntil }
|
|
11275
|
+
},
|
|
11276
|
+
...more.retryAfter !== void 0 && {
|
|
11277
|
+
headers: { "Retry-After": String(more.retryAfter) }
|
|
11278
|
+
}
|
|
11279
|
+
});
|
|
11280
|
+
function createTestSendRoute(options) {
|
|
11281
|
+
const { channels, limiter } = options;
|
|
11282
|
+
const now = options.now ?? Date.now;
|
|
11283
|
+
return async function answerTest(req) {
|
|
11284
|
+
const refused = actionRefusalFor(req, PHONE_TEST_ACTION, MAX_TEST_BODY_BYTES);
|
|
11285
|
+
if (refused) {
|
|
11286
|
+
req.resume();
|
|
11287
|
+
return refused;
|
|
11288
|
+
}
|
|
11289
|
+
const body = await readRequestBody(req, MAX_TEST_BODY_BYTES);
|
|
11290
|
+
if (!body.ok) {
|
|
11291
|
+
req.resume();
|
|
11292
|
+
return body.tooLarge ? refusal(413, `The body must be no more than ${MAX_TEST_BODY_BYTES} bytes.`) : refusal(400, "The body could not be read.");
|
|
11293
|
+
}
|
|
11294
|
+
const channel = channelIn(body.text);
|
|
11295
|
+
if (channel === null) {
|
|
11296
|
+
return refusal(400, 'The body must be {"channel": "ntfy"} or {"channel": "pushover"}.');
|
|
11297
|
+
}
|
|
11298
|
+
const { name, setting } = NAMED[channel];
|
|
11299
|
+
const tester = channels[channel];
|
|
11300
|
+
if (tester === null) {
|
|
11301
|
+
return failed3(
|
|
11302
|
+
409,
|
|
11303
|
+
"off",
|
|
11304
|
+
`${name} is not set up. Set ${setting} and start Agent Lookout again.`
|
|
11305
|
+
);
|
|
11306
|
+
}
|
|
11307
|
+
if (!limiter.begin()) {
|
|
11308
|
+
return failed3(429, "too-soon", "One test is sent a second. Try again in a moment.", {
|
|
11309
|
+
retryAfter: 1
|
|
11310
|
+
});
|
|
11311
|
+
}
|
|
9301
11312
|
try {
|
|
9302
|
-
const
|
|
9303
|
-
|
|
9304
|
-
|
|
9305
|
-
|
|
9306
|
-
|
|
9307
|
-
|
|
9308
|
-
|
|
9309
|
-
|
|
9310
|
-
|
|
9311
|
-
(error, stdout, stderr) => {
|
|
9312
|
-
if (!error) {
|
|
9313
|
-
resolve({ ok: true, stdout: String(stdout ?? "") });
|
|
9314
|
-
return;
|
|
11313
|
+
const result = await tester.test();
|
|
11314
|
+
if (!result.tried) {
|
|
11315
|
+
return failed3(
|
|
11316
|
+
429,
|
|
11317
|
+
"limited",
|
|
11318
|
+
`${SENDS_PER_HOUR} ${name} pushes were tried in the last hour, the most it tries.`,
|
|
11319
|
+
{
|
|
11320
|
+
limitedUntil: result.limitedUntil,
|
|
11321
|
+
retryAfter: Math.max(1, Math.ceil((result.limitedUntil - now()) / 1e3))
|
|
9315
11322
|
}
|
|
9316
|
-
|
|
9317
|
-
|
|
9318
|
-
|
|
9319
|
-
|
|
9320
|
-
|
|
9321
|
-
|
|
9322
|
-
|
|
11323
|
+
);
|
|
11324
|
+
}
|
|
11325
|
+
if (!result.outcome.sent) {
|
|
11326
|
+
return failed3(502, "not-sent", asSentence(result.outcome.reason));
|
|
11327
|
+
}
|
|
11328
|
+
return {
|
|
11329
|
+
status: 200,
|
|
11330
|
+
body: { ok: true, channel, sentAt: result.at }
|
|
11331
|
+
};
|
|
11332
|
+
} finally {
|
|
11333
|
+
limiter.end();
|
|
9323
11334
|
}
|
|
9324
|
-
}
|
|
11335
|
+
};
|
|
9325
11336
|
}
|
|
9326
11337
|
|
|
9327
|
-
// src/collector/
|
|
9328
|
-
|
|
9329
|
-
|
|
9330
|
-
|
|
9331
|
-
|
|
9332
|
-
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
"--",
|
|
9338
|
-
oneLine(notice.title),
|
|
9339
|
-
oneLine(notice.body, MAX_NOTICE_TEXT_LENGTH)
|
|
9340
|
-
];
|
|
9341
|
-
}
|
|
9342
|
-
function systemNotificationsShownOn(platform) {
|
|
9343
|
-
return platform === "darwin";
|
|
11338
|
+
// src/collector/pushover/pushoverMessage.ts
|
|
11339
|
+
function pushoverBody(token, user, message) {
|
|
11340
|
+
return {
|
|
11341
|
+
token,
|
|
11342
|
+
user,
|
|
11343
|
+
title: message.title,
|
|
11344
|
+
message: message.message,
|
|
11345
|
+
// A test sounds as a wait would, so it shows how one will arrive.
|
|
11346
|
+
priority: isWaitKind(message.kind) ? 0 : -1
|
|
11347
|
+
};
|
|
9344
11348
|
}
|
|
9345
|
-
|
|
9346
|
-
|
|
9347
|
-
|
|
9348
|
-
|
|
11349
|
+
|
|
11350
|
+
// src/collector/pushover/pushoverSender.ts
|
|
11351
|
+
var PUSHOVER_ENDPOINT = "https://api.pushover.net/1/messages.json";
|
|
11352
|
+
var PUSHOVER_WORDS = {
|
|
11353
|
+
where: "Pushover",
|
|
11354
|
+
whose: "Pushover's",
|
|
11355
|
+
notSent: "the push could not be sent",
|
|
11356
|
+
refused(status) {
|
|
11357
|
+
if (status === 400) {
|
|
11358
|
+
return `Pushover refused the push: check the application's token and the user key (status ${status})`;
|
|
11359
|
+
}
|
|
11360
|
+
if (status === 429)
|
|
11361
|
+
return `Pushover's monthly limit for your account was reached (status ${status})`;
|
|
11362
|
+
return `Pushover refused the push (status ${status})`;
|
|
11363
|
+
},
|
|
11364
|
+
troubled: (status) => `Pushover had a problem (status ${status})`
|
|
11365
|
+
};
|
|
11366
|
+
function createPushoverSender(settings, options) {
|
|
11367
|
+
const poster = createJsonPoster(options.endpoint ?? new URL(PUSHOVER_ENDPOINT), options);
|
|
9349
11368
|
return {
|
|
9350
|
-
|
|
9351
|
-
|
|
11369
|
+
async send(message) {
|
|
11370
|
+
let body;
|
|
9352
11371
|
try {
|
|
9353
|
-
|
|
9354
|
-
});
|
|
11372
|
+
body = JSON.stringify(pushoverBody(settings.token, settings.user, message));
|
|
9355
11373
|
} catch {
|
|
11374
|
+
return { sent: false, reason: PUSHOVER_WORDS.notSent };
|
|
9356
11375
|
}
|
|
11376
|
+
return outcomeOfPost(await poster.post(body), PUSHOVER_WORDS);
|
|
9357
11377
|
}
|
|
9358
11378
|
};
|
|
9359
11379
|
}
|
|
9360
11380
|
|
|
11381
|
+
// src/collector/pushover/pushoverSettings.ts
|
|
11382
|
+
var PUSHOVER_TOKEN_ENV = "AGENT_LOOKOUT_PUSHOVER_TOKEN";
|
|
11383
|
+
var PUSHOVER_USER_ENV = "AGENT_LOOKOUT_PUSHOVER_USER";
|
|
11384
|
+
var PUSHOVER_EVENTS_ENV = "AGENT_LOOKOUT_PUSHOVER_EVENTS";
|
|
11385
|
+
var PUSHOVER_AFTER_ENV = "AGENT_LOOKOUT_PUSHOVER_AFTER";
|
|
11386
|
+
var PUSHOVER_ASKING_ENV = "AGENT_LOOKOUT_PUSHOVER_ASKING";
|
|
11387
|
+
var KEY = /^[A-Za-z0-9]{30}$/;
|
|
11388
|
+
function readKey(name, value, what) {
|
|
11389
|
+
if (!KEY.test(value)) throw new SettingProblem(`${name} must be ${what}: 30 letters and digits.`);
|
|
11390
|
+
return value;
|
|
11391
|
+
}
|
|
11392
|
+
function readPushoverSetup(env) {
|
|
11393
|
+
const token = valueOf(env, PUSHOVER_TOKEN_ENV);
|
|
11394
|
+
const user = valueOf(env, PUSHOVER_USER_ENV);
|
|
11395
|
+
if (token === void 0 && user === void 0) return { on: false, problem: null };
|
|
11396
|
+
if (token === void 0) return { on: false, problem: `${PUSHOVER_TOKEN_ENV} is not set.` };
|
|
11397
|
+
if (user === void 0) return { on: false, problem: `${PUSHOVER_USER_ENV} is not set.` };
|
|
11398
|
+
try {
|
|
11399
|
+
return {
|
|
11400
|
+
on: true,
|
|
11401
|
+
settings: {
|
|
11402
|
+
token: readKey(PUSHOVER_TOKEN_ENV, token, "the API token of your Pushover application"),
|
|
11403
|
+
user: readKey(PUSHOVER_USER_ENV, user, "your Pushover user key, or a group key"),
|
|
11404
|
+
events: readEvents(PUSHOVER_EVENTS_ENV, valueOf(env, PUSHOVER_EVENTS_ENV)),
|
|
11405
|
+
afterMs: readAfter(PUSHOVER_AFTER_ENV, valueOf(env, PUSHOVER_AFTER_ENV)),
|
|
11406
|
+
asking: readAsking(PUSHOVER_ASKING_ENV, valueOf(env, PUSHOVER_ASKING_ENV))
|
|
11407
|
+
}
|
|
11408
|
+
};
|
|
11409
|
+
} catch (error) {
|
|
11410
|
+
if (error instanceof SettingProblem) return { on: false, problem: error.message };
|
|
11411
|
+
return { on: false, problem: `${PUSHOVER_TOKEN_ENV} could not be read.` };
|
|
11412
|
+
}
|
|
11413
|
+
}
|
|
11414
|
+
function pushoverProblemLine(problem) {
|
|
11415
|
+
return `Pushover pushes are off: ${problem}`;
|
|
11416
|
+
}
|
|
11417
|
+
|
|
9361
11418
|
// src/collector/remotes/remoteReader.ts
|
|
9362
11419
|
import { request } from "node:http";
|
|
9363
11420
|
var REMOTE_ANSWER_TIMEOUT_MS = 6e3;
|
|
@@ -9434,7 +11491,7 @@ var MAX_REMOTE_SOURCES = 20;
|
|
|
9434
11491
|
var MAX_ID_LENGTH = 512;
|
|
9435
11492
|
var MAX_PATH_LENGTH = 4096;
|
|
9436
11493
|
var MAX_WORD_LENGTH2 = 200;
|
|
9437
|
-
function
|
|
11494
|
+
function isRecord13(value) {
|
|
9438
11495
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9439
11496
|
}
|
|
9440
11497
|
function oneOf2(allowed, value) {
|
|
@@ -9448,7 +11505,7 @@ function exact(value, max) {
|
|
|
9448
11505
|
var COMMIT_ID2 = /^[0-9a-f]{7,64}$/;
|
|
9449
11506
|
var REPOSITORY_ID = /^[0-9a-f]{16,64}$/;
|
|
9450
11507
|
function readGit(value, machine) {
|
|
9451
|
-
if (!
|
|
11508
|
+
if (!isRecord13(value)) return void 0;
|
|
9452
11509
|
let head;
|
|
9453
11510
|
if (value.commit !== void 0) {
|
|
9454
11511
|
if (value.branch !== void 0 || typeof value.commit !== "string") return void 0;
|
|
@@ -9460,7 +11517,7 @@ function readGit(value, machine) {
|
|
|
9460
11517
|
head = { branch };
|
|
9461
11518
|
}
|
|
9462
11519
|
const repository = value.repository;
|
|
9463
|
-
if (
|
|
11520
|
+
if (isRecord13(repository) && typeof repository.id === "string") {
|
|
9464
11521
|
const name = sessionName(typeof repository.name === "string" ? repository.name : "");
|
|
9465
11522
|
if (name !== void 0 && REPOSITORY_ID.test(repository.id)) {
|
|
9466
11523
|
head.repository = { id: repositoryId(`${machine}
|
|
@@ -9470,7 +11527,7 @@ ${repository.id}`), name };
|
|
|
9470
11527
|
return head;
|
|
9471
11528
|
}
|
|
9472
11529
|
function readSession(value, machine, sources, now, keepWaitingText) {
|
|
9473
|
-
if (!
|
|
11530
|
+
if (!isRecord13(value)) return null;
|
|
9474
11531
|
const id = exact(value.id, MAX_ID_LENGTH);
|
|
9475
11532
|
const source = exact(value.source, MAX_WORD_LENGTH2);
|
|
9476
11533
|
if (id === null || source === null) return null;
|
|
@@ -9510,7 +11567,7 @@ function readSession(value, machine, sources, now, keepWaitingText) {
|
|
|
9510
11567
|
return session;
|
|
9511
11568
|
}
|
|
9512
11569
|
function readCell(value) {
|
|
9513
|
-
if (!
|
|
11570
|
+
if (!isRecord13(value)) return null;
|
|
9514
11571
|
if (value.level === "yes") return { level: "yes" };
|
|
9515
11572
|
const level = oneOf2(["no", "partly"], value.level);
|
|
9516
11573
|
const reason = cut(value.reason, MAX_WORD_LENGTH2);
|
|
@@ -9518,7 +11575,7 @@ function readCell(value) {
|
|
|
9518
11575
|
}
|
|
9519
11576
|
var ACTS_HERE = ["jump", "stop", "answer"];
|
|
9520
11577
|
function readCapabilities(value, machine) {
|
|
9521
|
-
if (!
|
|
11578
|
+
if (!isRecord13(value)) return null;
|
|
9522
11579
|
const read = {};
|
|
9523
11580
|
for (const capability of CAPABILITIES) {
|
|
9524
11581
|
if (ACTS_HERE.includes(capability)) continue;
|
|
@@ -9534,7 +11591,7 @@ function readCapabilities(value, machine) {
|
|
|
9534
11591
|
};
|
|
9535
11592
|
}
|
|
9536
11593
|
function readRemoteSnapshot(value, machine, now, options = {}) {
|
|
9537
|
-
if (!
|
|
11594
|
+
if (!isRecord13(value) || !Array.isArray(value.sessions) || !Array.isArray(value.sources)) {
|
|
9538
11595
|
return null;
|
|
9539
11596
|
}
|
|
9540
11597
|
const named = /* @__PURE__ */ new Map();
|
|
@@ -9542,7 +11599,7 @@ function readRemoteSnapshot(value, machine, now, options = {}) {
|
|
|
9542
11599
|
const agents = [];
|
|
9543
11600
|
for (const item of value.sources) {
|
|
9544
11601
|
if (sources.length >= MAX_REMOTE_SOURCES) break;
|
|
9545
|
-
if (!
|
|
11602
|
+
if (!isRecord13(item)) continue;
|
|
9546
11603
|
const id = exact(item.id, MAX_WORD_LENGTH2);
|
|
9547
11604
|
if (id === null || isRemoteSource(id) || item.machine !== void 0) continue;
|
|
9548
11605
|
const label2 = cut(item.label, MAX_WORD_LENGTH2) ?? id;
|
|
@@ -9567,7 +11624,7 @@ function readRemoteSnapshot(value, machine, now, options = {}) {
|
|
|
9567
11624
|
}
|
|
9568
11625
|
|
|
9569
11626
|
// src/collector/remotes/sshProgram.ts
|
|
9570
|
-
import
|
|
11627
|
+
import path23 from "node:path";
|
|
9571
11628
|
var SSH_BIN_ENV = "AGENT_LOOKOUT_SSH_BIN";
|
|
9572
11629
|
var SSH_LOCATIONS = ["/usr/bin/ssh", "/opt/homebrew/bin/ssh", "/usr/local/bin/ssh"];
|
|
9573
11630
|
var SERVER_ALIVE_SECONDS = 15;
|
|
@@ -9589,7 +11646,7 @@ async function findSsh(env, isExecutable, platform = process.platform) {
|
|
|
9589
11646
|
}
|
|
9590
11647
|
return {
|
|
9591
11648
|
found: false,
|
|
9592
|
-
looked: `The ssh command was not found on PATH or in ${SSH_LOCATIONS.map((at) =>
|
|
11649
|
+
looked: `The ssh command was not found on PATH or in ${SSH_LOCATIONS.map((at) => path23.dirname(at)).join(", ")}`
|
|
9593
11650
|
};
|
|
9594
11651
|
}
|
|
9595
11652
|
function sshArguments(forward) {
|
|
@@ -9613,7 +11670,7 @@ function sshArguments(forward) {
|
|
|
9613
11670
|
}
|
|
9614
11671
|
|
|
9615
11672
|
// src/collector/remotes/remoteAdapter.ts
|
|
9616
|
-
var
|
|
11673
|
+
var BASIS4 = "api";
|
|
9617
11674
|
var NOT_CONNECTED = "unavailable";
|
|
9618
11675
|
var ANSWER_WAIT_MS = 500;
|
|
9619
11676
|
var STILL_CONNECTING_MS = 1e4;
|
|
@@ -9679,7 +11736,7 @@ function createRemoteAdapter(options) {
|
|
|
9679
11736
|
if (found.advice) health.advice = found.advice;
|
|
9680
11737
|
if (agents.length > 0) health.agents = agents;
|
|
9681
11738
|
const sessions = state === "ok" && snapshot ? snapshot.sessions : [];
|
|
9682
|
-
return state === "ok" ? { health, sessions, basis:
|
|
11739
|
+
return state === "ok" ? { health, sessions, basis: BASIS4 } : { health, sessions };
|
|
9683
11740
|
}
|
|
9684
11741
|
function connecting(at) {
|
|
9685
11742
|
if (problem === null) {
|
|
@@ -10184,15 +12241,15 @@ import {
|
|
|
10184
12241
|
unlinkSync as unlinkSync2,
|
|
10185
12242
|
writeSync as writeSync2
|
|
10186
12243
|
} from "node:fs";
|
|
10187
|
-
import
|
|
10188
|
-
import
|
|
12244
|
+
import os10 from "node:os";
|
|
12245
|
+
import path24 from "node:path";
|
|
10189
12246
|
var SETTINGS_FILE_ENV = "AGENT_LOOKOUT_SETTINGS_FILE";
|
|
10190
12247
|
var SETTINGS_FILE_MODE = 384;
|
|
10191
12248
|
var SETTINGS_FOLDER_MODE = 448;
|
|
10192
12249
|
var MAX_SETTINGS_BYTES = 64 * 1024;
|
|
10193
|
-
function readSettingsSetup(env, homeDir =
|
|
12250
|
+
function readSettingsSetup(env, homeDir = os10.homedir()) {
|
|
10194
12251
|
const named = env[SETTINGS_FILE_ENV]?.trim() || void 0;
|
|
10195
|
-
const file =
|
|
12252
|
+
const file = path24.resolve(named ?? path24.join(homeDir, ".agent-lookout", "settings.json"));
|
|
10196
12253
|
return { file, shown: tildify(file, homeDir) };
|
|
10197
12254
|
}
|
|
10198
12255
|
function codeOf(error) {
|
|
@@ -10237,7 +12294,7 @@ function readSettingsStamp(setup) {
|
|
|
10237
12294
|
}
|
|
10238
12295
|
function readSettingsText(setup) {
|
|
10239
12296
|
const { file, shown } = setup;
|
|
10240
|
-
const folder =
|
|
12297
|
+
const folder = path24.dirname(file);
|
|
10241
12298
|
try {
|
|
10242
12299
|
const folderKind = kindAt(folder);
|
|
10243
12300
|
if (folderKind === null) return { kind: "missing" };
|
|
@@ -10296,7 +12353,7 @@ function readSettingsText(setup) {
|
|
|
10296
12353
|
}
|
|
10297
12354
|
function writeSettingsText(setup, text4) {
|
|
10298
12355
|
const { file, shown } = setup;
|
|
10299
|
-
const folder =
|
|
12356
|
+
const folder = path24.dirname(file);
|
|
10300
12357
|
const notSaved = {
|
|
10301
12358
|
ok: false,
|
|
10302
12359
|
problem: `${shown} could not be written, so the change was not saved.`
|
|
@@ -10317,9 +12374,9 @@ function writeSettingsText(setup, text4) {
|
|
|
10317
12374
|
problem: `${shown} is a link or not an ordinary file, which Agent Lookout does not write through, so the change was not saved.`
|
|
10318
12375
|
};
|
|
10319
12376
|
}
|
|
10320
|
-
temporary =
|
|
12377
|
+
temporary = path24.join(
|
|
10321
12378
|
folder,
|
|
10322
|
-
`.${
|
|
12379
|
+
`.${path24.basename(file)}.${process.pid}.${randomBytes3(6).toString("hex")}.tmp`
|
|
10323
12380
|
);
|
|
10324
12381
|
const fd = openWithoutFollowingNow(
|
|
10325
12382
|
temporary,
|
|
@@ -10359,7 +12416,7 @@ var RULE_WORDS = {
|
|
|
10359
12416
|
idle: "the idle rule",
|
|
10360
12417
|
quietHours: "the quiet hours"
|
|
10361
12418
|
};
|
|
10362
|
-
function
|
|
12419
|
+
function isRecord14(value) {
|
|
10363
12420
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10364
12421
|
}
|
|
10365
12422
|
function listOf(names) {
|
|
@@ -10397,7 +12454,7 @@ function rulesIn(read, shown) {
|
|
|
10397
12454
|
} catch {
|
|
10398
12455
|
value = null;
|
|
10399
12456
|
}
|
|
10400
|
-
if (!
|
|
12457
|
+
if (!isRecord14(value)) {
|
|
10401
12458
|
const notJson = `${shown} is not JSON that Agent Lookout can read, so the time rules and the permission rules are off. Changing one in Settings writes the file again.`;
|
|
10402
12459
|
return {
|
|
10403
12460
|
rules: off,
|
|
@@ -10412,7 +12469,7 @@ function rulesIn(read, shown) {
|
|
|
10412
12469
|
const { timeRules, permissionRules: heldRules, ...rest } = value;
|
|
10413
12470
|
const permissions = readPermissionRules(heldRules);
|
|
10414
12471
|
const { rules, unread } = readTimeRules(timeRules);
|
|
10415
|
-
const otherRules =
|
|
12472
|
+
const otherRules = isRecord14(timeRules) ? Object.fromEntries(
|
|
10416
12473
|
Object.entries(timeRules).filter(
|
|
10417
12474
|
([name]) => !TIME_RULE_NAMES.includes(name)
|
|
10418
12475
|
)
|
|
@@ -10514,15 +12571,15 @@ import { randomBytes as randomBytes4 } from "node:crypto";
|
|
|
10514
12571
|
|
|
10515
12572
|
// src/core/permission-rules/rulesChange.ts
|
|
10516
12573
|
var CHANGE_SHAPE = 'The body must be one change: {"add": rule}, {"edit": rule with its "id"}, {"move": {"id", "to": "up" or "down"}} or {"remove": {"id"}}, and nothing else.';
|
|
10517
|
-
function
|
|
12574
|
+
function isRecord15(value) {
|
|
10518
12575
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10519
12576
|
}
|
|
10520
12577
|
function rulesChangeIn(value) {
|
|
10521
12578
|
const refused = { ok: false, problem: CHANGE_SHAPE };
|
|
10522
|
-
if (!
|
|
12579
|
+
if (!isRecord15(value) || Object.keys(value).length !== 1) return refused;
|
|
10523
12580
|
const [kind] = Object.keys(value);
|
|
10524
12581
|
const body = value[kind];
|
|
10525
|
-
if (!
|
|
12582
|
+
if (!isRecord15(body)) return refused;
|
|
10526
12583
|
switch (kind) {
|
|
10527
12584
|
case "add": {
|
|
10528
12585
|
const read = ruleWordsIn(body);
|
|
@@ -10598,7 +12655,7 @@ function applyRulesChange(rules, change, newId) {
|
|
|
10598
12655
|
|
|
10599
12656
|
// src/collector/settings/permissionRulesRoute.ts
|
|
10600
12657
|
var MAX_PERMISSION_RULES_BODY_BYTES = 1024;
|
|
10601
|
-
var
|
|
12658
|
+
var failed4 = (status, reason, error) => ({
|
|
10602
12659
|
status,
|
|
10603
12660
|
body: { error, reason }
|
|
10604
12661
|
});
|
|
@@ -10625,14 +12682,14 @@ function createPermissionRulesRoute(options) {
|
|
|
10625
12682
|
try {
|
|
10626
12683
|
value = JSON.parse(body.text);
|
|
10627
12684
|
} catch {
|
|
10628
|
-
return
|
|
12685
|
+
return failed4(400, "invalid", "The body must be JSON: one change to the permission rules.");
|
|
10629
12686
|
}
|
|
10630
12687
|
const asked = rulesChangeIn(value);
|
|
10631
|
-
if (!asked.ok) return
|
|
12688
|
+
if (!asked.ok) return failed4(400, "invalid", asked.problem);
|
|
10632
12689
|
const made = settings.changePermissionRules(
|
|
10633
12690
|
(rules) => applyRulesChange(rules, asked.change, newId)
|
|
10634
12691
|
);
|
|
10635
|
-
if (!made.ok) return
|
|
12692
|
+
if (!made.ok) return failed4(STATUS_OF[made.reason], made.reason, made.problem);
|
|
10636
12693
|
return {
|
|
10637
12694
|
status: 200,
|
|
10638
12695
|
body: { ok: true, permissionRules: made.rules }
|
|
@@ -10642,7 +12699,7 @@ function createPermissionRulesRoute(options) {
|
|
|
10642
12699
|
|
|
10643
12700
|
// src/collector/settings/timeRulesRoute.ts
|
|
10644
12701
|
var MAX_TIME_RULES_BODY_BYTES = 2048;
|
|
10645
|
-
var
|
|
12702
|
+
var failed5 = (status, reason, error) => ({
|
|
10646
12703
|
status,
|
|
10647
12704
|
body: { error, reason }
|
|
10648
12705
|
});
|
|
@@ -10666,12 +12723,12 @@ function createTimeRulesRoute(options) {
|
|
|
10666
12723
|
try {
|
|
10667
12724
|
value = JSON.parse(body.text);
|
|
10668
12725
|
} catch {
|
|
10669
|
-
return
|
|
12726
|
+
return failed5(400, "invalid", "The body must be JSON: the three time rules.");
|
|
10670
12727
|
}
|
|
10671
12728
|
const asked = timeRulesIn(value);
|
|
10672
|
-
if (!asked.ok) return
|
|
12729
|
+
if (!asked.ok) return failed5(400, "invalid", asked.problem);
|
|
10673
12730
|
const saved = settings.changeTimeRules(asked.rules);
|
|
10674
|
-
if (!saved.ok) return
|
|
12731
|
+
if (!saved.ok) return failed5(500, "not-saved", saved.problem);
|
|
10675
12732
|
return {
|
|
10676
12733
|
status: 200,
|
|
10677
12734
|
body: { ok: true, timeRules: asked.rules }
|
|
@@ -10715,11 +12772,11 @@ function createOsascriptRunner(options) {
|
|
|
10715
12772
|
}
|
|
10716
12773
|
|
|
10717
12774
|
// src/collector/terminal/tabFinder.ts
|
|
10718
|
-
import
|
|
12775
|
+
import os11 from "node:os";
|
|
10719
12776
|
var TAB_LOOK_SOONEST_MS = 5e3;
|
|
10720
12777
|
function createTabFinder(options) {
|
|
10721
12778
|
const off = terminalJumpOff(options.env);
|
|
10722
|
-
const homeDir = options.homeDir ??
|
|
12779
|
+
const homeDir = options.homeDir ?? os11.homedir();
|
|
10723
12780
|
const readProcesses = options.readProcesses ?? readProcessTableWithPs;
|
|
10724
12781
|
const now = options.now ?? Date.now;
|
|
10725
12782
|
const soonestMs = options.soonestMs ?? TAB_LOOK_SOONEST_MS;
|
|
@@ -10953,96 +13010,24 @@ function ledgerHistoryStore(store, ledger) {
|
|
|
10953
13010
|
}
|
|
10954
13011
|
|
|
10955
13012
|
// src/collector/webhook/webhookSender.ts
|
|
10956
|
-
var
|
|
10957
|
-
|
|
10958
|
-
|
|
10959
|
-
|
|
10960
|
-
|
|
10961
|
-
|
|
10962
|
-
|
|
10963
|
-
}
|
|
10964
|
-
var TIMED_OUT = "the address did not answer in time";
|
|
10965
|
-
function outcomeOfStatus(status) {
|
|
10966
|
-
if (status >= 200 && status < 300) return { sent: true };
|
|
10967
|
-
if (status >= 300 && status < 400) {
|
|
10968
|
-
return { sent: false, reason: "the address answered with a redirect, which is not followed" };
|
|
10969
|
-
}
|
|
10970
|
-
if (status >= 400 && status < 500) {
|
|
10971
|
-
return { sent: false, reason: `the address refused the post (status ${status})` };
|
|
10972
|
-
}
|
|
10973
|
-
if (status >= 500 && status < 600) {
|
|
10974
|
-
return { sent: false, reason: `the receiving service had a problem (status ${status})` };
|
|
10975
|
-
}
|
|
10976
|
-
return { sent: false, reason: "the address's answer could not be understood" };
|
|
10977
|
-
}
|
|
10978
|
-
function failureReason2(error) {
|
|
10979
|
-
const { code } = error ?? {};
|
|
10980
|
-
if (typeof code !== "string") return "the post could not be sent";
|
|
10981
|
-
if (code === "ECONNREFUSED") return "nothing answered at the address";
|
|
10982
|
-
if (code === "ENOTFOUND" || code === "EAI_AGAIN") return "the address's host could not be found";
|
|
10983
|
-
if (code === "ETIMEDOUT") return TIMED_OUT;
|
|
10984
|
-
if (/CERT|SELF_SIGNED|UNABLE_TO|^ERR_TLS|^ERR_SSL|^EPROTO$/.test(code)) {
|
|
10985
|
-
return "the secure connection to the address failed";
|
|
10986
|
-
}
|
|
10987
|
-
return "the connection to the address failed";
|
|
10988
|
-
}
|
|
10989
|
-
function requestOptions(body, version) {
|
|
10990
|
-
return {
|
|
10991
|
-
method: "POST",
|
|
10992
|
-
headers: postHeaders(body, version),
|
|
10993
|
-
// A connection of its own, closed after the answer.
|
|
10994
|
-
agent: false,
|
|
10995
|
-
rejectUnauthorized: true
|
|
10996
|
-
};
|
|
10997
|
-
}
|
|
13013
|
+
var WORDS = {
|
|
13014
|
+
where: "the address",
|
|
13015
|
+
whose: "the address's",
|
|
13016
|
+
notSent: "the post could not be sent",
|
|
13017
|
+
refused: (status) => `the address refused the post (status ${status})`,
|
|
13018
|
+
troubled: (status) => `the receiving service had a problem (status ${status})`
|
|
13019
|
+
};
|
|
10998
13020
|
function createHttpSender(settings, options) {
|
|
10999
|
-
const
|
|
11000
|
-
const timeoutMs = options.timeoutMs ?? WEBHOOK_TIMEOUT_MS;
|
|
11001
|
-
let client = null;
|
|
11002
|
-
const load = () => {
|
|
11003
|
-
client ??= url.protocol === "https:" ? import("node:https").then(({ request: request2 }) => request2) : import("node:http").then(({ request: request2 }) => request2);
|
|
11004
|
-
return client;
|
|
11005
|
-
};
|
|
13021
|
+
const poster = createJsonPoster(settings.url, options);
|
|
11006
13022
|
return {
|
|
11007
13023
|
async send(post) {
|
|
11008
|
-
let
|
|
13024
|
+
let body;
|
|
11009
13025
|
try {
|
|
11010
|
-
|
|
13026
|
+
body = JSON.stringify(post);
|
|
11011
13027
|
} catch {
|
|
11012
|
-
return { sent: false, reason:
|
|
13028
|
+
return { sent: false, reason: WORDS.notSent };
|
|
11013
13029
|
}
|
|
11014
|
-
return
|
|
11015
|
-
let finished = false;
|
|
11016
|
-
const finish = (outcome) => {
|
|
11017
|
-
if (finished) return;
|
|
11018
|
-
finished = true;
|
|
11019
|
-
resolve(outcome);
|
|
11020
|
-
};
|
|
11021
|
-
let body;
|
|
11022
|
-
let req;
|
|
11023
|
-
try {
|
|
11024
|
-
body = JSON.stringify(post);
|
|
11025
|
-
req = request2(url, requestOptions(body, options.version));
|
|
11026
|
-
} catch {
|
|
11027
|
-
finish({ sent: false, reason: "the post could not be sent" });
|
|
11028
|
-
return;
|
|
11029
|
-
}
|
|
11030
|
-
const timer = setTimeout(() => {
|
|
11031
|
-
finish({ sent: false, reason: TIMED_OUT });
|
|
11032
|
-
req.destroy();
|
|
11033
|
-
}, timeoutMs);
|
|
11034
|
-
timer.unref();
|
|
11035
|
-
req.on("response", (res) => {
|
|
11036
|
-
finish(outcomeOfStatus(res.statusCode ?? 0));
|
|
11037
|
-
res.on("close", () => clearTimeout(timer));
|
|
11038
|
-
res.resume();
|
|
11039
|
-
});
|
|
11040
|
-
req.on("error", (error) => {
|
|
11041
|
-
clearTimeout(timer);
|
|
11042
|
-
finish({ sent: false, reason: failureReason2(error) });
|
|
11043
|
-
});
|
|
11044
|
-
req.end(body);
|
|
11045
|
-
});
|
|
13030
|
+
return outcomeOfPost(await poster.post(body), WORDS);
|
|
11046
13031
|
}
|
|
11047
13032
|
};
|
|
11048
13033
|
}
|
|
@@ -11052,15 +13037,15 @@ var WEBHOOK_URL_ENV = "AGENT_LOOKOUT_WEBHOOK_URL";
|
|
|
11052
13037
|
var WEBHOOK_EVENTS_ENV = "AGENT_LOOKOUT_WEBHOOK_EVENTS";
|
|
11053
13038
|
var WEBHOOK_AFTER_ENV = "AGENT_LOOKOUT_WEBHOOK_AFTER";
|
|
11054
13039
|
var WEBHOOK_ASKING_ENV = "AGENT_LOOKOUT_WEBHOOK_ASKING";
|
|
11055
|
-
var
|
|
11056
|
-
var
|
|
13040
|
+
var MAX_URL_LENGTH2 = 2048;
|
|
13041
|
+
var URL_UNREADABLE3 = `${WEBHOOK_URL_ENV} could not be read. Copy the whole address, beginning https://.`;
|
|
11057
13042
|
function readUrl(value) {
|
|
11058
13043
|
let url;
|
|
11059
13044
|
try {
|
|
11060
|
-
if (value.length >
|
|
13045
|
+
if (value.length > MAX_URL_LENGTH2) throw new Error();
|
|
11061
13046
|
url = new URL(value);
|
|
11062
13047
|
} catch {
|
|
11063
|
-
throw new SettingProblem(
|
|
13048
|
+
throw new SettingProblem(URL_UNREADABLE3);
|
|
11064
13049
|
}
|
|
11065
13050
|
const local = LOCAL_HOSTS.has(url.hostname);
|
|
11066
13051
|
if (url.protocol !== "https:" && !(url.protocol === "http:" && local)) {
|
|
@@ -11068,7 +13053,7 @@ function readUrl(value) {
|
|
|
11068
13053
|
`${WEBHOOK_URL_ENV} must begin with https://, or with http:// for an address on this computer, 127.0.0.1 or localhost.`
|
|
11069
13054
|
);
|
|
11070
13055
|
}
|
|
11071
|
-
if (url.hostname === "") throw new SettingProblem(
|
|
13056
|
+
if (url.hostname === "") throw new SettingProblem(URL_UNREADABLE3);
|
|
11072
13057
|
if (!isWebhookHost(url.hostname)) {
|
|
11073
13058
|
throw new SettingProblem(
|
|
11074
13059
|
`${WEBHOOK_URL_ENV} must name its host in letters, digits, dots and dashes, such as hooks.slack.com.`
|
|
@@ -11094,7 +13079,7 @@ function readWebhookSetup(env) {
|
|
|
11094
13079
|
};
|
|
11095
13080
|
} catch (error) {
|
|
11096
13081
|
if (error instanceof SettingProblem) return { on: false, problem: error.message };
|
|
11097
|
-
return { on: false, problem:
|
|
13082
|
+
return { on: false, problem: URL_UNREADABLE3 };
|
|
11098
13083
|
}
|
|
11099
13084
|
}
|
|
11100
13085
|
function webhookProblemLine(problem) {
|
|
@@ -11191,6 +13176,26 @@ function createCollector(options) {
|
|
|
11191
13176
|
now
|
|
11192
13177
|
}) : null;
|
|
11193
13178
|
const webhookOff = webhookOffStatus(webhookSetup.on ? null : webhookSetup.problem);
|
|
13179
|
+
const ntfySetup = readNtfySetup(env);
|
|
13180
|
+
if (!ntfySetup.on && ntfySetup.problem !== null) warn(ntfyProblemLine(ntfySetup.problem));
|
|
13181
|
+
const makeNtfySender = options.createNtfySender ?? ((settings2) => createNtfySender(settings2, { version: options.version }));
|
|
13182
|
+
const ntfy = ntfySetup.on ? createNtfyNotifications({
|
|
13183
|
+
settings: ntfySetup.settings,
|
|
13184
|
+
sender: makeNtfySender(ntfySetup.settings),
|
|
13185
|
+
now
|
|
13186
|
+
}) : null;
|
|
13187
|
+
const ntfyOff = ntfyOffStatus(ntfySetup.on ? null : ntfySetup.problem);
|
|
13188
|
+
const pushoverSetup = readPushoverSetup(env);
|
|
13189
|
+
if (!pushoverSetup.on && pushoverSetup.problem !== null) {
|
|
13190
|
+
warn(pushoverProblemLine(pushoverSetup.problem));
|
|
13191
|
+
}
|
|
13192
|
+
const makePushoverSender = options.createPushoverSender ?? ((settings2) => createPushoverSender(settings2, { version: options.version }));
|
|
13193
|
+
const pushover = pushoverSetup.on ? createPushoverNotifications({
|
|
13194
|
+
settings: pushoverSetup.settings,
|
|
13195
|
+
sender: makePushoverSender(pushoverSetup.settings),
|
|
13196
|
+
now
|
|
13197
|
+
}) : null;
|
|
13198
|
+
const pushoverOff = pushoverOffStatus(pushoverSetup.on ? null : pushoverSetup.problem);
|
|
11194
13199
|
const remotes = createRemotes({
|
|
11195
13200
|
env,
|
|
11196
13201
|
version: options.version,
|
|
@@ -11211,6 +13216,7 @@ function createCollector(options) {
|
|
|
11211
13216
|
platform
|
|
11212
13217
|
}),
|
|
11213
13218
|
createCodexAdapter({ env: options.env, now, pollIntervalMs: intervalMs }),
|
|
13219
|
+
createAntigravityAdapter({ env: options.env, now, pollIntervalMs: intervalMs, platform }),
|
|
11214
13220
|
createStatusFileAdapter({ env: options.env, now, pollIntervalMs: intervalMs })
|
|
11215
13221
|
];
|
|
11216
13222
|
const poller = createPoller({
|
|
@@ -11245,6 +13251,8 @@ function createCollector(options) {
|
|
|
11245
13251
|
answering?.observe(snapshot);
|
|
11246
13252
|
email?.handle(snapshot);
|
|
11247
13253
|
webhook?.handle(snapshot);
|
|
13254
|
+
ntfy?.handle(snapshot);
|
|
13255
|
+
pushover?.handle(snapshot);
|
|
11248
13256
|
notifications.handle(snapshot);
|
|
11249
13257
|
options.onSnapshot?.(snapshot);
|
|
11250
13258
|
}
|
|
@@ -11287,6 +13295,13 @@ function createCollector(options) {
|
|
|
11287
13295
|
notifications,
|
|
11288
13296
|
email: () => email?.status() ?? emailOff,
|
|
11289
13297
|
webhook: () => webhook?.status() ?? webhookOff,
|
|
13298
|
+
ntfy: () => ntfy?.status() ?? ntfyOff,
|
|
13299
|
+
pushover: () => pushover?.status() ?? pushoverOff,
|
|
13300
|
+
phoneTest: createTestSendRoute({
|
|
13301
|
+
channels: { ntfy, pushover },
|
|
13302
|
+
limiter: createActionLimiter(now),
|
|
13303
|
+
now
|
|
13304
|
+
}),
|
|
11290
13305
|
pullRequests: () => pullRequests?.status() ?? pullRequestsOff,
|
|
11291
13306
|
jump: createJumpRoute({ poller, panes, run: tmux, tabs, osascript, now }),
|
|
11292
13307
|
stop: stopRoutes && createStopRoute(stopRoutes),
|
|
@@ -11381,13 +13396,20 @@ function createCollector(options) {
|
|
|
11381
13396
|
poller,
|
|
11382
13397
|
email,
|
|
11383
13398
|
webhook,
|
|
13399
|
+
ntfy,
|
|
13400
|
+
pushover,
|
|
11384
13401
|
pullRequests,
|
|
11385
13402
|
history: keeper,
|
|
11386
13403
|
answering: {
|
|
11387
13404
|
start: () => answerStart ??= answering?.start() ?? Promise.resolve(),
|
|
11388
13405
|
stop: async () => {
|
|
11389
13406
|
await answering?.stop();
|
|
11390
|
-
}
|
|
13407
|
+
},
|
|
13408
|
+
serve: (snapshot) => answering?.serve(snapshot) ?? snapshot,
|
|
13409
|
+
check: async () => {
|
|
13410
|
+
await answering?.check();
|
|
13411
|
+
},
|
|
13412
|
+
...answering?.answer && { answer: answering.answer }
|
|
11391
13413
|
},
|
|
11392
13414
|
remotes,
|
|
11393
13415
|
settings
|
|
@@ -11411,7 +13433,7 @@ async function runStandalone(options) {
|
|
|
11411
13433
|
} catch (error) {
|
|
11412
13434
|
return stopWith(error.message);
|
|
11413
13435
|
}
|
|
11414
|
-
if (!existsSync(
|
|
13436
|
+
if (!existsSync(path25.join(distDir, "index.html"))) {
|
|
11415
13437
|
return stopWith(
|
|
11416
13438
|
"The dashboard has not been built yet. Run `npm run build`, then start it again."
|
|
11417
13439
|
);
|
|
@@ -11454,12 +13476,12 @@ async function runStandalone(options) {
|
|
|
11454
13476
|
|
|
11455
13477
|
// src/cli/start/openBrowser.ts
|
|
11456
13478
|
import { spawn as spawn3 } from "node:child_process";
|
|
11457
|
-
import
|
|
13479
|
+
import path26 from "node:path";
|
|
11458
13480
|
var OPEN_WAIT_MS = 1e4;
|
|
11459
13481
|
var WINDOWS_FOLDER = "C:\\Windows";
|
|
11460
13482
|
function windowsFolder(env) {
|
|
11461
13483
|
const named = env.SystemRoot?.trim() || env.SYSTEMROOT?.trim();
|
|
11462
|
-
return named &&
|
|
13484
|
+
return named && path26.win32.isAbsolute(named) ? named : WINDOWS_FOLDER;
|
|
11463
13485
|
}
|
|
11464
13486
|
function openCommand(platform, url, env = process.env) {
|
|
11465
13487
|
const address = loopbackAddress(url);
|
|
@@ -11468,7 +13490,7 @@ function openCommand(platform, url, env = process.env) {
|
|
|
11468
13490
|
if (platform === "linux") return { file: "xdg-open", args: [address] };
|
|
11469
13491
|
if (platform === "win32") {
|
|
11470
13492
|
return {
|
|
11471
|
-
file:
|
|
13493
|
+
file: path26.win32.join(windowsFolder(env), "System32", "rundll32.exe"),
|
|
11472
13494
|
args: ["url.dll,FileProtocolHandler", address]
|
|
11473
13495
|
};
|
|
11474
13496
|
}
|