agent-lookout 0.2.7 → 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 +19 -0
- package/PRIVACY.md +145 -20
- package/README.md +46 -43
- package/SECURITY.md +24 -9
- package/dist/assets/{HistoryPanel-oSXRXEdv.js → HistoryPanel-CQ7zovsM.js} +1 -1
- package/dist/assets/{SessionPanel-DcJvw0F8.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-3LYUYPG4.js → startCommand-ANC7NVAO.js} +2420 -459
- package/dist/index.html +2 -2
- package/docs/API.md +99 -44
- package/docs/GUIDE.md +242 -92
- package/docs/INSTALL.md +8 -6
- package/package.json +5 -1
- package/dist/assets/index-DLPmOXDe.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;
|
|
@@ -583,7 +586,7 @@ function waitBegan(statusSince, seenAt) {
|
|
|
583
586
|
|
|
584
587
|
// src/collector/outbound/outboundChannel.ts
|
|
585
588
|
function createOutboundChannel(options) {
|
|
586
|
-
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;
|
|
587
590
|
const now = options.now ?? Date.now;
|
|
588
591
|
const wanted = new Set(options.events);
|
|
589
592
|
let memory = EMPTY_CHANGE_MEMORY;
|
|
@@ -595,15 +598,26 @@ function createOutboundChannel(options) {
|
|
|
595
598
|
const hold = createQuietHold();
|
|
596
599
|
let last = null;
|
|
597
600
|
let queue = Promise.resolve();
|
|
598
|
-
function
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
() => {
|
|
604
|
-
|
|
605
|
-
|
|
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
|
|
606
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
|
+
});
|
|
607
621
|
}
|
|
608
622
|
return {
|
|
609
623
|
handle(snapshot) {
|
|
@@ -626,7 +640,7 @@ function createOutboundChannel(options) {
|
|
|
626
640
|
...item,
|
|
627
641
|
agent: agentName(item.session, snapshot.sources)
|
|
628
642
|
}));
|
|
629
|
-
ready.push((sentNow) =>
|
|
643
|
+
ready.push((sentNow) => summaryMessage2({ ...summary, items, now: sentNow }));
|
|
630
644
|
}
|
|
631
645
|
for (const { session, begunAt } of end.open) open2.set(session.id, begunAt);
|
|
632
646
|
}
|
|
@@ -644,7 +658,7 @@ function createOutboundChannel(options) {
|
|
|
644
658
|
agent: agentName(session, snapshot.sources),
|
|
645
659
|
seenAt: at
|
|
646
660
|
};
|
|
647
|
-
ready.push((sentNow) =>
|
|
661
|
+
ready.push((sentNow) => overMessage2({ ...facts, now: sentNow }));
|
|
648
662
|
continue;
|
|
649
663
|
}
|
|
650
664
|
const since = session.statusSince;
|
|
@@ -674,7 +688,7 @@ function createOutboundChannel(options) {
|
|
|
674
688
|
open2.delete(id);
|
|
675
689
|
sentAt.push(at);
|
|
676
690
|
send2(
|
|
677
|
-
|
|
691
|
+
waitMessage2({
|
|
678
692
|
session: withoutWaitingText(session),
|
|
679
693
|
agent: agentName(session, snapshot.sources),
|
|
680
694
|
// Read now, from this poll's snapshot, and only when it is to go.
|
|
@@ -695,7 +709,7 @@ function createOutboundChannel(options) {
|
|
|
695
709
|
sentAt.push(at);
|
|
696
710
|
reminders.told(due.session.id, at);
|
|
697
711
|
send2(
|
|
698
|
-
|
|
712
|
+
reminderMessage2({
|
|
699
713
|
session: withoutWaitingText(due.session),
|
|
700
714
|
agent: agentName(due.session, snapshot.sources),
|
|
701
715
|
asking: asking ? waitingText(due.session.waitingText) ?? null : null,
|
|
@@ -718,7 +732,15 @@ function createOutboundChannel(options) {
|
|
|
718
732
|
},
|
|
719
733
|
last: () => last,
|
|
720
734
|
limitedUntil: () => limitLiftsAt(sentAt, now()),
|
|
721
|
-
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
|
+
}
|
|
722
744
|
};
|
|
723
745
|
}
|
|
724
746
|
|
|
@@ -1286,6 +1308,116 @@ function pullRequestsOffStatus(problem) {
|
|
|
1286
1308
|
return { on: false, problem, gh: null, last: null };
|
|
1287
1309
|
}
|
|
1288
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
|
+
|
|
1289
1421
|
// src/core/sessions/diff.ts
|
|
1290
1422
|
function severityFor(to) {
|
|
1291
1423
|
if (to === "needs-you") return "warning";
|
|
@@ -1638,6 +1770,50 @@ function createPoller(options) {
|
|
|
1638
1770
|
};
|
|
1639
1771
|
}
|
|
1640
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
|
+
|
|
1641
1817
|
// src/collector/webhook/webhookMessage.ts
|
|
1642
1818
|
var MAX_SUMMARY_ITEMS = 100;
|
|
1643
1819
|
function slackSafe(text4) {
|
|
@@ -1913,6 +2089,7 @@ function createApiHandler(options) {
|
|
|
1913
2089
|
if (options.answer) actions.set(ANSWER_PATH, options.answer);
|
|
1914
2090
|
if (options.timeRules) actions.set(TIME_RULES_PATH, options.timeRules);
|
|
1915
2091
|
if (options.permissionRules) actions.set(PERMISSION_RULES_PATH, options.permissionRules);
|
|
2092
|
+
if (options.phoneTest) actions.set(PHONE_TEST_PATH, options.phoneTest);
|
|
1916
2093
|
const serveSnapshot = options.serveSnapshot ?? ((snapshot) => snapshot);
|
|
1917
2094
|
function route(req, res) {
|
|
1918
2095
|
const refusal3 = refusalFor(req);
|
|
@@ -1975,6 +2152,22 @@ function createApiHandler(options) {
|
|
|
1975
2152
|
);
|
|
1976
2153
|
return;
|
|
1977
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
|
+
}
|
|
1978
2171
|
case "/api/pull-requests": {
|
|
1979
2172
|
send(
|
|
1980
2173
|
res,
|
|
@@ -2258,7 +2451,7 @@ function urlFor(address) {
|
|
|
2258
2451
|
|
|
2259
2452
|
// src/collector/hosts/standalone.ts
|
|
2260
2453
|
import { existsSync } from "node:fs";
|
|
2261
|
-
import
|
|
2454
|
+
import path25 from "node:path";
|
|
2262
2455
|
|
|
2263
2456
|
// src/core/mapping/claudeCodeMapping.ts
|
|
2264
2457
|
var LIVE_STATUS = /* @__PURE__ */ new Map([
|
|
@@ -3114,6 +3307,9 @@ function lstartMs(value) {
|
|
|
3114
3307
|
const [day, hours, minutes, seconds, year] = match.slice(2).map(Number);
|
|
3115
3308
|
return Date.UTC(year, month, day, hours, minutes, seconds);
|
|
3116
3309
|
}
|
|
3310
|
+
function processStartTime(value) {
|
|
3311
|
+
return lstartMs(collapse(value)) ?? null;
|
|
3312
|
+
}
|
|
3117
3313
|
function compareProcessStart(recorded, actual, withinMs = SAME_START_WITHIN_MS) {
|
|
3118
3314
|
if (recorded === void 0 || actual === void 0) return "unknown";
|
|
3119
3315
|
const before = collapse(recorded);
|
|
@@ -3751,11 +3947,11 @@ async function readTranscriptTail(file, io, known, maxBytes = TRANSCRIPT_TAIL_BY
|
|
|
3751
3947
|
const stamp = stampOf(open2.info);
|
|
3752
3948
|
if (known !== null && sameStamp(known, stamp)) return { unchanged: true, stamp };
|
|
3753
3949
|
const start = Math.max(0, stamp.size - maxBytes);
|
|
3754
|
-
const
|
|
3950
|
+
const bytes3 = await open2.read(start, stamp.size - start);
|
|
3755
3951
|
return {
|
|
3756
3952
|
unchanged: false,
|
|
3757
3953
|
stamp,
|
|
3758
|
-
tail: new TextDecoder("utf-8").decode(
|
|
3954
|
+
tail: new TextDecoder("utf-8").decode(bytes3),
|
|
3759
3955
|
fromStart: start === 0
|
|
3760
3956
|
};
|
|
3761
3957
|
} finally {
|
|
@@ -4659,9 +4855,21 @@ async function pollAfterAnswer(poller, sessionId) {
|
|
|
4659
4855
|
function answerRefusalFor(req) {
|
|
4660
4856
|
return actionRefusalFor(req, ANSWER_ACTION, MAX_ANSWER_BODY_BYTES);
|
|
4661
4857
|
}
|
|
4662
|
-
function
|
|
4858
|
+
function createAnswerer(options) {
|
|
4663
4859
|
const { poller, events, asks } = options;
|
|
4664
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);
|
|
4665
4873
|
return async function answerPermission(req) {
|
|
4666
4874
|
const refused = answerRefusalFor(req);
|
|
4667
4875
|
if (refused) {
|
|
@@ -4680,12 +4888,8 @@ function createAnswerRoute(options) {
|
|
|
4680
4888
|
'The body must be {"sessionId": "...", "requestId": "...", "decision": "allow" or "deny"} and nothing else.'
|
|
4681
4889
|
);
|
|
4682
4890
|
}
|
|
4683
|
-
const
|
|
4684
|
-
if (!session) return answerFailed("no-ask");
|
|
4685
|
-
const outcome = await asks.answer(asked.sessionId, asked.requestId, asked.decision);
|
|
4891
|
+
const outcome = await answer(asked.sessionId, asked.requestId, asked.decision);
|
|
4686
4892
|
if (outcome !== "answered") return answerFailed(outcome);
|
|
4687
|
-
events.add([answeredEvent(session, asked.decision, now())]);
|
|
4688
|
-
await pollAfterAnswer(poller, asked.sessionId);
|
|
4689
4893
|
return {
|
|
4690
4894
|
status: 200,
|
|
4691
4895
|
body: { ok: true, decision: asked.decision }
|
|
@@ -5621,18 +5825,18 @@ function namesOwnFiles(words, ownPaths = [], folder) {
|
|
|
5621
5825
|
(part) => from !== void 0 && !part.startsWith("/") && !part.startsWith("-") ? [part, `${from}/${part}`] : [part]
|
|
5622
5826
|
);
|
|
5623
5827
|
return read.some((part) => {
|
|
5624
|
-
const
|
|
5625
|
-
if (
|
|
5626
|
-
if (own.some((name) =>
|
|
5627
|
-
if (above.has(
|
|
5628
|
-
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}`));
|
|
5629
5833
|
});
|
|
5630
5834
|
});
|
|
5631
5835
|
}
|
|
5632
5836
|
function folderReachesOwnFiles(folder, ownPaths = []) {
|
|
5633
5837
|
const own = ownPaths.filter((name) => name.startsWith("/")).map(plainPath);
|
|
5634
|
-
const
|
|
5635
|
-
return own.some((name) => name ===
|
|
5838
|
+
const path27 = plainPath(folder);
|
|
5839
|
+
return own.some((name) => name === path27 || name.startsWith(`${path27 === "/" ? "" : path27}/`));
|
|
5636
5840
|
}
|
|
5637
5841
|
var PLAIN_WORD = /^[A-Za-z0-9_\-.,/:=+@%]+$/;
|
|
5638
5842
|
var ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
@@ -5985,6 +6189,8 @@ function createAnswering(options) {
|
|
|
5985
6189
|
observe: () => {
|
|
5986
6190
|
},
|
|
5987
6191
|
answeredIn: () => /* @__PURE__ */ new Set(),
|
|
6192
|
+
check: async () => {
|
|
6193
|
+
},
|
|
5988
6194
|
ruleAnswers: () => []
|
|
5989
6195
|
};
|
|
5990
6196
|
}
|
|
@@ -6012,6 +6218,7 @@ function createAnswering(options) {
|
|
|
6012
6218
|
let problem = null;
|
|
6013
6219
|
let listening = false;
|
|
6014
6220
|
const answerRoute = createAnswerRoute({ poller, events, asks, now: options.now });
|
|
6221
|
+
const answer = createAnswerer({ poller, events, asks, now: options.now });
|
|
6015
6222
|
return {
|
|
6016
6223
|
async start() {
|
|
6017
6224
|
const started = await socket.start();
|
|
@@ -6044,6 +6251,9 @@ function createAnswering(options) {
|
|
|
6044
6251
|
req.resume();
|
|
6045
6252
|
return answerRefusalFor(req) ?? refusal(405, "This address only answers GET requests.", { Allow: "GET" });
|
|
6046
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(),
|
|
6047
6257
|
ruleAnswers: () => ruleAnswers.recent()
|
|
6048
6258
|
};
|
|
6049
6259
|
}
|
|
@@ -6271,140 +6481,1502 @@ function waitTotals(input) {
|
|
|
6271
6481
|
};
|
|
6272
6482
|
}
|
|
6273
6483
|
|
|
6274
|
-
// src/collector/adapters/
|
|
6484
|
+
// src/collector/adapters/antigravity/index.ts
|
|
6275
6485
|
import os5 from "node:os";
|
|
6276
|
-
import
|
|
6486
|
+
import path14 from "node:path";
|
|
6277
6487
|
|
|
6278
|
-
// src/core/mapping/
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
const {
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
|
|
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;
|
|
6294
6505
|
}
|
|
6295
|
-
|
|
6296
|
-
}
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
function keepsWriterLocks(cliVersion) {
|
|
6300
|
-
if (typeof cliVersion !== "string") return false;
|
|
6301
|
-
const match = VERSION.exec(cliVersion);
|
|
6302
|
-
if (!match) return false;
|
|
6303
|
-
const parts = [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
6304
|
-
for (const [index, since] of WRITER_LOCKS_SINCE.entries()) {
|
|
6305
|
-
const part = parts[index];
|
|
6306
|
-
if (part !== since) return part > since;
|
|
6506
|
+
if (listedAt !== null) {
|
|
6507
|
+
for (const { id, writtenAt } of conversations) {
|
|
6508
|
+
if (writtenAt !== null && writtenAt > listedAt) held.add(id);
|
|
6509
|
+
}
|
|
6307
6510
|
}
|
|
6308
|
-
return
|
|
6511
|
+
return { held, complete };
|
|
6309
6512
|
}
|
|
6310
|
-
function
|
|
6311
|
-
if (
|
|
6312
|
-
if (
|
|
6313
|
-
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";
|
|
6314
6517
|
}
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
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;
|
|
6318
6573
|
}
|
|
6319
|
-
function
|
|
6320
|
-
return
|
|
6574
|
+
function emptyAgyLogState() {
|
|
6575
|
+
return { folder: null, folderNamed: false, opened: /* @__PURE__ */ new Set(), current: null, ask: null };
|
|
6321
6576
|
}
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
|
|
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
|
+
}
|
|
6329
6597
|
}
|
|
6330
|
-
var
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
|
|
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;
|
|
6336
6619
|
}
|
|
6337
|
-
|
|
6338
|
-
if (isRecord8(source)) return !Object.keys(source).some((kind) => NOT_A_SESSION_SOURCE.has(kind));
|
|
6339
|
-
return true;
|
|
6620
|
+
return best;
|
|
6340
6621
|
}
|
|
6341
6622
|
|
|
6342
|
-
// src/collector/adapters/
|
|
6343
|
-
|
|
6344
|
-
var
|
|
6345
|
-
var
|
|
6346
|
-
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$/;
|
|
6347
6632
|
var NEWLINE = 10;
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
if (
|
|
6351
|
-
const
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
}
|
|
6360
|
-
function text3(value) {
|
|
6361
|
-
return typeof value === "string" && value !== "" ? value : void 0;
|
|
6362
|
-
}
|
|
6363
|
-
function label(value) {
|
|
6364
|
-
return typeof value === "string" && value !== "" && value.length <= 64 ? value : void 0;
|
|
6365
|
-
}
|
|
6366
|
-
function compactSource(value) {
|
|
6367
|
-
if (typeof value === "string") return text3(value);
|
|
6368
|
-
if (!isRecord9(value)) return void 0;
|
|
6369
|
-
return Object.fromEntries(
|
|
6370
|
-
Object.entries(value).map(([kind, detail]) => [
|
|
6371
|
-
kind,
|
|
6372
|
-
typeof detail === "string" ? detail : true
|
|
6373
|
-
])
|
|
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
|
|
6374
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
|
+
});
|
|
6375
6655
|
}
|
|
6376
|
-
function
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
|
|
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;
|
|
6382
6683
|
}
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
|
|
6393
|
-
|
|
6394
|
-
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
|
|
6399
|
-
|
|
6400
|
-
|
|
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
|
+
}
|
|
7955
|
+
}
|
|
7956
|
+
function readMetaLine(line) {
|
|
7957
|
+
if (!line.includes('"session_meta"')) return null;
|
|
7958
|
+
const value = parse(line);
|
|
7959
|
+
if (!value || value.type !== "session_meta" || !isRecord10(value.payload)) return null;
|
|
7960
|
+
const payload = value.payload;
|
|
7961
|
+
const meta = {
|
|
7962
|
+
id: text3(payload.id),
|
|
7963
|
+
timestamp: text3(payload.timestamp),
|
|
7964
|
+
cwd: text3(payload.cwd),
|
|
7965
|
+
source: compactSource(payload.source),
|
|
7966
|
+
threadSource: text3(payload.thread_source),
|
|
7967
|
+
parentThreadId: text3(payload.parent_thread_id),
|
|
7968
|
+
originator: label(payload.originator),
|
|
7969
|
+
cliVersion: label(payload.cli_version)
|
|
7970
|
+
};
|
|
7971
|
+
for (const key of Object.keys(meta)) {
|
|
7972
|
+
if (meta[key] === void 0) delete meta[key];
|
|
6401
7973
|
}
|
|
6402
7974
|
return meta;
|
|
6403
7975
|
}
|
|
6404
7976
|
function readTurnLine(line) {
|
|
6405
7977
|
if (!line.includes('"event_msg"') || !TURN_HINT.test(line)) return null;
|
|
6406
7978
|
const value = parse(line);
|
|
6407
|
-
if (!value || value.type !== "event_msg" || !
|
|
7979
|
+
if (!value || value.type !== "event_msg" || !isRecord10(value.payload)) return null;
|
|
6408
7980
|
const turn = value.payload.type;
|
|
6409
7981
|
if (typeof turn !== "string" || !TURN_TYPE.test(turn)) return null;
|
|
6410
7982
|
return {
|
|
@@ -6417,23 +7989,23 @@ function lineTime(line) {
|
|
|
6417
7989
|
const match = LINE_TIME.exec(line);
|
|
6418
7990
|
return match ? parseCodexTime(match[1]) : null;
|
|
6419
7991
|
}
|
|
6420
|
-
function
|
|
7992
|
+
function bytes2(data) {
|
|
6421
7993
|
return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
6422
7994
|
}
|
|
6423
|
-
async function
|
|
6424
|
-
const data =
|
|
7995
|
+
async function readExactly2(file, position, length) {
|
|
7996
|
+
const data = bytes2(await file.read(position, length));
|
|
6425
7997
|
if (data.byteLength !== length) throw new Error("The file changed while it was read.");
|
|
6426
7998
|
return data;
|
|
6427
7999
|
}
|
|
6428
8000
|
async function readHead(file, size) {
|
|
6429
|
-
for (const limit of [FIRST_HEAD_BYTES,
|
|
8001
|
+
for (const limit of [FIRST_HEAD_BYTES, HEAD_LIMIT_BYTES2]) {
|
|
6430
8002
|
const length = Math.min(limit, size);
|
|
6431
|
-
const head = await
|
|
8003
|
+
const head = await readExactly2(file, 0, length);
|
|
6432
8004
|
let named = head.indexOf('"session_meta"');
|
|
6433
8005
|
while (named !== -1) {
|
|
6434
|
-
const end = head.indexOf(
|
|
8006
|
+
const end = head.indexOf(NEWLINE3, named);
|
|
6435
8007
|
if (end === -1) break;
|
|
6436
|
-
const start = named > 0 ? head.lastIndexOf(
|
|
8008
|
+
const start = named > 0 ? head.lastIndexOf(NEWLINE3, named - 1) + 1 : 0;
|
|
6437
8009
|
const meta = readMetaLine(head.toString("utf8", start, end));
|
|
6438
8010
|
if (meta) return meta;
|
|
6439
8011
|
named = head.indexOf('"session_meta"', end + 1);
|
|
@@ -6442,9 +8014,9 @@ async function readHead(file, size) {
|
|
|
6442
8014
|
}
|
|
6443
8015
|
return null;
|
|
6444
8016
|
}
|
|
6445
|
-
async function
|
|
8017
|
+
async function scanTail2(file, size) {
|
|
6446
8018
|
let position = size;
|
|
6447
|
-
let budget2 =
|
|
8019
|
+
let budget2 = TAIL_LIMIT_BYTES2;
|
|
6448
8020
|
let fragment = Buffer.alloc(0);
|
|
6449
8021
|
let end = null;
|
|
6450
8022
|
let lastLineAt = null;
|
|
@@ -6462,15 +8034,15 @@ async function scanTail(file, size) {
|
|
|
6462
8034
|
end
|
|
6463
8035
|
});
|
|
6464
8036
|
while (position > 0 && budget2 > 0) {
|
|
6465
|
-
const length = Math.min(
|
|
8037
|
+
const length = Math.min(TAIL_CHUNK_BYTES2, position, budget2);
|
|
6466
8038
|
const start = position - length;
|
|
6467
|
-
const chunk = await
|
|
8039
|
+
const chunk = await readExactly2(file, start, length);
|
|
6468
8040
|
budget2 -= length;
|
|
6469
8041
|
position = start;
|
|
6470
8042
|
const buffer = fragment.byteLength > 0 ? Buffer.concat([chunk, fragment]) : chunk;
|
|
6471
8043
|
let cut2 = buffer.byteLength;
|
|
6472
8044
|
if (end === null) {
|
|
6473
|
-
const last = buffer.lastIndexOf(
|
|
8045
|
+
const last = buffer.lastIndexOf(NEWLINE3);
|
|
6474
8046
|
if (last === -1) {
|
|
6475
8047
|
fragment = buffer;
|
|
6476
8048
|
continue;
|
|
@@ -6478,12 +8050,12 @@ async function scanTail(file, size) {
|
|
|
6478
8050
|
end = start + last + 1;
|
|
6479
8051
|
cut2 = last;
|
|
6480
8052
|
}
|
|
6481
|
-
let newline = cut2 > 0 ? buffer.lastIndexOf(
|
|
8053
|
+
let newline = cut2 > 0 ? buffer.lastIndexOf(NEWLINE3, cut2 - 1) : -1;
|
|
6482
8054
|
while (newline !== -1) {
|
|
6483
8055
|
const turn = take(buffer.toString("utf8", newline + 1, cut2));
|
|
6484
8056
|
if (turn) return found(turn);
|
|
6485
8057
|
cut2 = newline;
|
|
6486
|
-
newline = cut2 > 0 ? buffer.lastIndexOf(
|
|
8058
|
+
newline = cut2 > 0 ? buffer.lastIndexOf(NEWLINE3, cut2 - 1) : -1;
|
|
6487
8059
|
}
|
|
6488
8060
|
if (start === 0) {
|
|
6489
8061
|
const turn = take(buffer.toString("utf8", 0, cut2));
|
|
@@ -6497,7 +8069,7 @@ async function scanTail(file, size) {
|
|
|
6497
8069
|
}
|
|
6498
8070
|
return { lastTurn: void 0, lastTurnAt: null, lastTurnImported: false, lastLineAt, end };
|
|
6499
8071
|
}
|
|
6500
|
-
function
|
|
8072
|
+
function stateOf2(cached) {
|
|
6501
8073
|
return {
|
|
6502
8074
|
meta: cached.meta,
|
|
6503
8075
|
lastTurn: cached.lastTurn,
|
|
@@ -6513,7 +8085,7 @@ function createRolloutReader(io) {
|
|
|
6513
8085
|
try {
|
|
6514
8086
|
const { info } = handle;
|
|
6515
8087
|
const meta = await readHead(handle, info.size);
|
|
6516
|
-
const tail = await
|
|
8088
|
+
const tail = await scanTail2(handle, info.size);
|
|
6517
8089
|
return {
|
|
6518
8090
|
meta,
|
|
6519
8091
|
lastTurn: tail.lastTurn,
|
|
@@ -6533,14 +8105,14 @@ function createRolloutReader(io) {
|
|
|
6533
8105
|
const offset = cached.offset;
|
|
6534
8106
|
if (cached.meta === null || offset === null) return null;
|
|
6535
8107
|
if (info.ino !== cached.ino || info.size <= cached.size || info.size < offset) return null;
|
|
6536
|
-
if (info.size - offset >
|
|
8108
|
+
if (info.size - offset > TAIL_LIMIT_BYTES2) return null;
|
|
6537
8109
|
const handle = await io.openRegular(file);
|
|
6538
8110
|
try {
|
|
6539
8111
|
const opened = handle.info;
|
|
6540
8112
|
if (opened.ino !== cached.ino || opened.size < offset) return null;
|
|
6541
8113
|
const from = offset > 0 ? offset - 1 : 0;
|
|
6542
|
-
const read = await
|
|
6543
|
-
if (offset > 0 && read[0] !==
|
|
8114
|
+
const read = await readExactly2(handle, from, opened.size - from);
|
|
8115
|
+
if (offset > 0 && read[0] !== NEWLINE3) return null;
|
|
6544
8116
|
const added = offset > 0 ? read.subarray(1) : read;
|
|
6545
8117
|
const next = {
|
|
6546
8118
|
...cached,
|
|
@@ -6549,7 +8121,7 @@ function createRolloutReader(io) {
|
|
|
6549
8121
|
mtimeMs: opened.mtimeMs
|
|
6550
8122
|
};
|
|
6551
8123
|
let start = 0;
|
|
6552
|
-
let newline = added.indexOf(
|
|
8124
|
+
let newline = added.indexOf(NEWLINE3);
|
|
6553
8125
|
while (newline !== -1) {
|
|
6554
8126
|
const line = added.toString("utf8", start, newline);
|
|
6555
8127
|
if (line.trim() !== "") {
|
|
@@ -6563,7 +8135,7 @@ function createRolloutReader(io) {
|
|
|
6563
8135
|
}
|
|
6564
8136
|
}
|
|
6565
8137
|
start = newline + 1;
|
|
6566
|
-
newline = added.indexOf(
|
|
8138
|
+
newline = added.indexOf(NEWLINE3, start);
|
|
6567
8139
|
}
|
|
6568
8140
|
next.offset = offset + start;
|
|
6569
8141
|
return next;
|
|
@@ -6575,11 +8147,11 @@ function createRolloutReader(io) {
|
|
|
6575
8147
|
async read(file, info) {
|
|
6576
8148
|
const cached = cache.get(file);
|
|
6577
8149
|
if (cached && cached.ino === info.ino && cached.size === info.size && cached.mtimeMs === info.mtimeMs) {
|
|
6578
|
-
return
|
|
8150
|
+
return stateOf2(cached);
|
|
6579
8151
|
}
|
|
6580
8152
|
const next = cached && await readOnward(file, cached, info) || await readFresh(file);
|
|
6581
8153
|
cache.set(file, next);
|
|
6582
|
-
return
|
|
8154
|
+
return stateOf2(next);
|
|
6583
8155
|
},
|
|
6584
8156
|
keepOnly(files) {
|
|
6585
8157
|
for (const file of cache.keys()) {
|
|
@@ -6590,10 +8162,10 @@ function createRolloutReader(io) {
|
|
|
6590
8162
|
}
|
|
6591
8163
|
|
|
6592
8164
|
// src/collector/adapters/codex/rollouts.ts
|
|
6593
|
-
import
|
|
6594
|
-
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}";
|
|
6595
8167
|
var ROLLOUT_FILE_NAME = new RegExp(
|
|
6596
|
-
`^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$`,
|
|
6597
8169
|
"i"
|
|
6598
8170
|
);
|
|
6599
8171
|
var INDEX_REFRESH_MS = 3e4;
|
|
@@ -6608,7 +8180,7 @@ function twoDigits(value) {
|
|
|
6608
8180
|
function recentDayFolders(sessionsDir, now) {
|
|
6609
8181
|
const folders = [now, now - 24 * 60 * 60 * 1e3].map((at) => {
|
|
6610
8182
|
const date = new Date(at);
|
|
6611
|
-
return
|
|
8183
|
+
return path15.join(
|
|
6612
8184
|
sessionsDir,
|
|
6613
8185
|
String(date.getFullYear()),
|
|
6614
8186
|
twoDigits(date.getMonth() + 1),
|
|
@@ -6637,12 +8209,12 @@ function createRolloutFinder(options) {
|
|
|
6637
8209
|
const refs = [];
|
|
6638
8210
|
for (const name of (await names(dir)).sort()) {
|
|
6639
8211
|
const threadId = threadIdOfRollout(name);
|
|
6640
|
-
if (threadId !== null) refs.push({ path:
|
|
8212
|
+
if (threadId !== null) refs.push({ path: path15.join(dir, name), threadId });
|
|
6641
8213
|
}
|
|
6642
8214
|
return refs;
|
|
6643
8215
|
}
|
|
6644
8216
|
async function subfolders(dir, pattern) {
|
|
6645
|
-
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));
|
|
6646
8218
|
}
|
|
6647
8219
|
async function buildIndex() {
|
|
6648
8220
|
const years = await subfolders(sessionsDir, YEAR);
|
|
@@ -6686,7 +8258,7 @@ function createRolloutFinder(options) {
|
|
|
6686
8258
|
// src/collector/adapters/codex/sessionIndex.ts
|
|
6687
8259
|
var SESSION_INDEX_FILE = "session_index.jsonl";
|
|
6688
8260
|
var SESSION_INDEX_LIMIT_BYTES = 4 * 1024 * 1024;
|
|
6689
|
-
function
|
|
8261
|
+
function isRecord11(value) {
|
|
6690
8262
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6691
8263
|
}
|
|
6692
8264
|
function parseSessionIndex(content) {
|
|
@@ -6699,7 +8271,7 @@ function parseSessionIndex(content) {
|
|
|
6699
8271
|
} catch {
|
|
6700
8272
|
continue;
|
|
6701
8273
|
}
|
|
6702
|
-
if (!
|
|
8274
|
+
if (!isRecord11(value)) continue;
|
|
6703
8275
|
const { id, thread_name: name } = value;
|
|
6704
8276
|
if (typeof id !== "string" || id === "" || typeof name !== "string") continue;
|
|
6705
8277
|
const trimmed = name.trim();
|
|
@@ -6750,14 +8322,14 @@ function createSessionIndexReader(file, io) {
|
|
|
6750
8322
|
}
|
|
6751
8323
|
|
|
6752
8324
|
// src/collector/adapters/codex/toSession.ts
|
|
6753
|
-
var
|
|
6754
|
-
var
|
|
8325
|
+
var SOURCE_ID3 = "codex";
|
|
8326
|
+
var WRITE_ORDER_SLACK_MS3 = 1e3;
|
|
6755
8327
|
function lastActivity(state, now) {
|
|
6756
8328
|
return plausibleTime(state.lastLineAt, now) ?? plausibleTime(state.lastTurnAt, now) ?? plausibleTime(parseCodexTime(state.meta?.timestamp), now);
|
|
6757
8329
|
}
|
|
6758
8330
|
function codexSession(input) {
|
|
6759
8331
|
const { threadId, state, live: live2, now } = input;
|
|
6760
|
-
const id = `${
|
|
8332
|
+
const id = `${SOURCE_ID3}:${threadId}`;
|
|
6761
8333
|
const cwd = state.meta?.cwd ?? null;
|
|
6762
8334
|
const project = projectOf(cwd);
|
|
6763
8335
|
const status = mapCodexStatus({ lastTurn: state.lastTurn, live: live2 });
|
|
@@ -6768,12 +8340,12 @@ function codexSession(input) {
|
|
|
6768
8340
|
} else if (status === "idle" || status === "finished") {
|
|
6769
8341
|
statusSince = plausibleTime(state.lastLineAt, now);
|
|
6770
8342
|
}
|
|
6771
|
-
if (statusSince !== null && startedAt !== null && statusSince < startedAt -
|
|
8343
|
+
if (statusSince !== null && startedAt !== null && statusSince < startedAt - WRITE_ORDER_SLACK_MS3) {
|
|
6772
8344
|
statusSince = null;
|
|
6773
8345
|
}
|
|
6774
8346
|
const session = {
|
|
6775
8347
|
id,
|
|
6776
|
-
source:
|
|
8348
|
+
source: SOURCE_ID3,
|
|
6777
8349
|
surface: mapCodexSurface(state.meta?.source, state.meta?.originator),
|
|
6778
8350
|
// A name made only of spaces is no name. It is cleaned and cut as every other agent's is.
|
|
6779
8351
|
name: sessionName(input.name ?? "") || project || id,
|
|
@@ -6786,7 +8358,7 @@ function codexSession(input) {
|
|
|
6786
8358
|
stale: isStale({ status, statusSince }, now)
|
|
6787
8359
|
};
|
|
6788
8360
|
const writtenAt = input.writtenAt === void 0 ? null : plausibleTime(Math.floor(input.writtenAt), now);
|
|
6789
|
-
const beforeStart = writtenAt !== null && startedAt !== null && writtenAt < startedAt -
|
|
8361
|
+
const beforeStart = writtenAt !== null && startedAt !== null && writtenAt < startedAt - WRITE_ORDER_SLACK_MS3;
|
|
6790
8362
|
if (writtenAt !== null && !beforeStart) session.lastWriteAt = writtenAt;
|
|
6791
8363
|
return session;
|
|
6792
8364
|
}
|
|
@@ -6816,11 +8388,11 @@ async function readWriterLocks(dir, io) {
|
|
|
6816
8388
|
// src/collector/adapters/codex/index.ts
|
|
6817
8389
|
var CODEX_HOME_ENV = "AGENT_LOOKOUT_CODEX_HOME";
|
|
6818
8390
|
var CODEX_OWN_HOME_ENV = "CODEX_HOME";
|
|
6819
|
-
var
|
|
6820
|
-
var
|
|
8391
|
+
var RECHECK_MS2 = 6e4;
|
|
8392
|
+
var BASIS2 = "files";
|
|
6821
8393
|
var BASIS_WITHOUT_LOCKS = "files-without-locks";
|
|
6822
|
-
var
|
|
6823
|
-
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.";
|
|
6824
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.";
|
|
6825
8397
|
var CODEX_CAPABILITIES = {
|
|
6826
8398
|
"working-and-idle": { level: "yes" },
|
|
@@ -6853,18 +8425,18 @@ var CODEX_CAPABILITIES = {
|
|
|
6853
8425
|
};
|
|
6854
8426
|
function createCodexAdapter(options = {}) {
|
|
6855
8427
|
const env = options.env ?? process.env;
|
|
6856
|
-
const homeDir = options.homeDir ??
|
|
8428
|
+
const homeDir = options.homeDir ?? os6.homedir();
|
|
6857
8429
|
const now = options.now ?? Date.now;
|
|
6858
8430
|
const io = options.io ?? nodeIo2;
|
|
6859
8431
|
const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
6860
|
-
const recheckMs = options.recheckMs ??
|
|
8432
|
+
const recheckMs = options.recheckMs ?? RECHECK_MS2;
|
|
6861
8433
|
const ownOverride = env[CODEX_HOME_ENV]?.trim() || void 0;
|
|
6862
8434
|
const codexOverride = env[CODEX_OWN_HOME_ENV]?.trim() || void 0;
|
|
6863
8435
|
const homeFrom = ownOverride ? "agent-lookout" : codexOverride ? "codex" : "default";
|
|
6864
|
-
const codexHome =
|
|
6865
|
-
const sessionsDir =
|
|
6866
|
-
const locksDir =
|
|
6867
|
-
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);
|
|
6868
8440
|
const homeName = tildify(codexHome, homeDir);
|
|
6869
8441
|
const sessionsName = tildify(sessionsDir, homeDir);
|
|
6870
8442
|
const locksName = tildify(locksDir, homeDir);
|
|
@@ -6890,14 +8462,14 @@ function createCodexAdapter(options = {}) {
|
|
|
6890
8462
|
}
|
|
6891
8463
|
function result(state, detail, read, checkedAt) {
|
|
6892
8464
|
const health = {
|
|
6893
|
-
id:
|
|
6894
|
-
label:
|
|
8465
|
+
id: SOURCE_ID3,
|
|
8466
|
+
label: LABEL3,
|
|
6895
8467
|
state,
|
|
6896
8468
|
detail,
|
|
6897
8469
|
watching: watching(read),
|
|
6898
8470
|
checkedAt
|
|
6899
8471
|
};
|
|
6900
|
-
return state === "ok" ? { health, sessions: [], basis:
|
|
8472
|
+
return state === "ok" ? { health, sessions: [], basis: BASIS2 } : { health, sessions: [] };
|
|
6901
8473
|
}
|
|
6902
8474
|
function notFound(checkedAt) {
|
|
6903
8475
|
const where = homeFrom === "codex" ? `${CODEX_OWN_HOME_ENV} is set to ${homeName}, and there is no folder there` : `there is no ${homeName} folder`;
|
|
@@ -6945,7 +8517,7 @@ function createCodexAdapter(options = {}) {
|
|
|
6945
8517
|
if (homeFrom === "agent-lookout") {
|
|
6946
8518
|
return result(
|
|
6947
8519
|
"ok",
|
|
6948
|
-
`${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}`,
|
|
6949
8521
|
"not found",
|
|
6950
8522
|
checkedAt
|
|
6951
8523
|
);
|
|
@@ -6958,7 +8530,7 @@ function createCodexAdapter(options = {}) {
|
|
|
6958
8530
|
homeSeen = true;
|
|
6959
8531
|
return result(
|
|
6960
8532
|
"ok",
|
|
6961
|
-
`Codex has not saved any sessions in ${sessionsName} yet. ${
|
|
8533
|
+
`Codex has not saved any sessions in ${sessionsName} yet. ${NEEDS_YOU_NOTE2}`,
|
|
6962
8534
|
"not found",
|
|
6963
8535
|
checkedAt
|
|
6964
8536
|
);
|
|
@@ -7040,7 +8612,7 @@ function createCodexAdapter(options = {}) {
|
|
|
7040
8612
|
now: checkedAt
|
|
7041
8613
|
});
|
|
7042
8614
|
});
|
|
7043
|
-
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}`;
|
|
7044
8616
|
if (!locks.supported) {
|
|
7045
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`;
|
|
7046
8618
|
detail += ` ${why}, so a session that has ended cannot be told from one that is idle, and none is shown as finished.`;
|
|
@@ -7048,11 +8620,11 @@ function createCodexAdapter(options = {}) {
|
|
|
7048
8620
|
detail += ` ${OLDER_CODEX_NOTE}`;
|
|
7049
8621
|
}
|
|
7050
8622
|
const answer = result("ok", detail, every(pollIntervalMs), checkedAt);
|
|
7051
|
-
return { ...answer, sessions, basis: locks.supported ?
|
|
8623
|
+
return { ...answer, sessions, basis: locks.supported ? BASIS2 : BASIS_WITHOUT_LOCKS };
|
|
7052
8624
|
}
|
|
7053
8625
|
return {
|
|
7054
|
-
id:
|
|
7055
|
-
label:
|
|
8626
|
+
id: SOURCE_ID3,
|
|
8627
|
+
label: LABEL3,
|
|
7056
8628
|
capabilities: CODEX_CAPABILITIES,
|
|
7057
8629
|
lookingIn: `Looking for Codex sessions in ${sessionsName}.`,
|
|
7058
8630
|
async poll() {
|
|
@@ -7071,8 +8643,8 @@ function createCodexAdapter(options = {}) {
|
|
|
7071
8643
|
}
|
|
7072
8644
|
|
|
7073
8645
|
// src/collector/adapters/status-files/index.ts
|
|
7074
|
-
import
|
|
7075
|
-
import
|
|
8646
|
+
import os7 from "node:os";
|
|
8647
|
+
import path17 from "node:path";
|
|
7076
8648
|
|
|
7077
8649
|
// src/core/mapping/statusFileMapping.ts
|
|
7078
8650
|
var STATUS = /* @__PURE__ */ new Map([
|
|
@@ -7152,16 +8724,16 @@ function parseStatusFile(content) {
|
|
|
7152
8724
|
}
|
|
7153
8725
|
|
|
7154
8726
|
// src/collector/adapters/status-files/toSession.ts
|
|
7155
|
-
var
|
|
7156
|
-
var
|
|
8727
|
+
var SOURCE_ID4 = "status-files";
|
|
8728
|
+
var WRITE_ORDER_SLACK_MS4 = 1e3;
|
|
7157
8729
|
function statusFileSession(input) {
|
|
7158
8730
|
const { fileName: fileName2, file, statusSince, now } = input;
|
|
7159
8731
|
const cwd = file.cwd ?? null;
|
|
7160
8732
|
const project = projectOf(cwd);
|
|
7161
8733
|
const mapped = mapStatusFileStatus(file);
|
|
7162
8734
|
const session = {
|
|
7163
|
-
id: `${
|
|
7164
|
-
source:
|
|
8735
|
+
id: `${SOURCE_ID4}:${fileName2}`,
|
|
8736
|
+
source: SOURCE_ID4,
|
|
7165
8737
|
agent: file.agent,
|
|
7166
8738
|
surface: mapStatusFileApp(file.app),
|
|
7167
8739
|
// The file's name is as untrusted as its contents, so it is cleaned and cut
|
|
@@ -7181,15 +8753,15 @@ function statusFileSession(input) {
|
|
|
7181
8753
|
}
|
|
7182
8754
|
const writtenAt = input.writtenAt === void 0 ? null : plausibleTime(Math.floor(input.writtenAt), now);
|
|
7183
8755
|
const given = plausibleTime(file.since, now);
|
|
7184
|
-
const beforeSince = writtenAt !== null && given !== null && writtenAt < given -
|
|
8756
|
+
const beforeSince = writtenAt !== null && given !== null && writtenAt < given - WRITE_ORDER_SLACK_MS4;
|
|
7185
8757
|
if (writtenAt !== null && !beforeSince) session.lastWriteAt = writtenAt;
|
|
7186
8758
|
return session;
|
|
7187
8759
|
}
|
|
7188
8760
|
|
|
7189
8761
|
// src/collector/adapters/status-files/index.ts
|
|
7190
8762
|
var STATUS_DIR_ENV = "AGENT_LOOKOUT_STATUS_DIR";
|
|
7191
|
-
var
|
|
7192
|
-
var
|
|
8763
|
+
var BASIS3 = "files";
|
|
8764
|
+
var LABEL4 = "Status files";
|
|
7193
8765
|
var STATUS_FILE_CAPABILITIES = {
|
|
7194
8766
|
"working-and-idle": { level: "partly", reason: "If the agent writes working and idle." },
|
|
7195
8767
|
"needs-you": { level: "partly", reason: "If the agent writes waiting." },
|
|
@@ -7215,13 +8787,13 @@ function counted(count2, one, many) {
|
|
|
7215
8787
|
}
|
|
7216
8788
|
function createStatusFileAdapter(options = {}) {
|
|
7217
8789
|
const env = options.env ?? process.env;
|
|
7218
|
-
const homeDir = options.homeDir ??
|
|
8790
|
+
const homeDir = options.homeDir ?? os7.homedir();
|
|
7219
8791
|
const now = options.now ?? Date.now;
|
|
7220
8792
|
const io = options.io ?? nodeIo2;
|
|
7221
8793
|
const isAlive = options.isAlive ?? isProcessAlive;
|
|
7222
8794
|
const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
7223
8795
|
const override = env[STATUS_DIR_ENV]?.trim() || void 0;
|
|
7224
|
-
const dir =
|
|
8796
|
+
const dir = path17.resolve(override ?? path17.join(homeDir, ".agent-lookout", "sessions"));
|
|
7225
8797
|
const dirName = tildify(dir, homeDir);
|
|
7226
8798
|
const decoder2 = new TextDecoder();
|
|
7227
8799
|
let lastRead = /* @__PURE__ */ new Map();
|
|
@@ -7242,15 +8814,15 @@ function createStatusFileAdapter(options = {}) {
|
|
|
7242
8814
|
}
|
|
7243
8815
|
function result(state, detail, facts, checkedAt, sessions = []) {
|
|
7244
8816
|
const health = {
|
|
7245
|
-
id:
|
|
7246
|
-
label:
|
|
8817
|
+
id: SOURCE_ID4,
|
|
8818
|
+
label: LABEL4,
|
|
7247
8819
|
state,
|
|
7248
8820
|
detail,
|
|
7249
8821
|
watching: facts,
|
|
7250
8822
|
checkedAt
|
|
7251
8823
|
};
|
|
7252
|
-
if (state === "ok") return { health, sessions, basis:
|
|
7253
|
-
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 };
|
|
7254
8826
|
return { health, sessions: [] };
|
|
7255
8827
|
}
|
|
7256
8828
|
async function readOne(file) {
|
|
@@ -7266,9 +8838,9 @@ function createStatusFileAdapter(options = {}) {
|
|
|
7266
8838
|
const tooLarge = { kind: "refused", why: "is over 16 KB" };
|
|
7267
8839
|
try {
|
|
7268
8840
|
if (open2.info.size > MAX_FILE_BYTES) return tooLarge;
|
|
7269
|
-
const
|
|
7270
|
-
if (
|
|
7271
|
-
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));
|
|
7272
8844
|
return parsed ? { kind: "read", file: parsed, mtimeMs: open2.info.mtimeMs } : { kind: "invalid" };
|
|
7273
8845
|
} catch {
|
|
7274
8846
|
return { kind: "refused", why: "could not be read" };
|
|
@@ -7281,7 +8853,7 @@ function createStatusFileAdapter(options = {}) {
|
|
|
7281
8853
|
if (names.length <= MAX_FILES) return names;
|
|
7282
8854
|
const written = await Promise.all(
|
|
7283
8855
|
names.map(async (name) => {
|
|
7284
|
-
const info = await io.lstat(
|
|
8856
|
+
const info = await io.lstat(path17.join(dir, name)).catch(() => null);
|
|
7285
8857
|
return { name, mtimeMs: info?.mtimeMs ?? -Infinity };
|
|
7286
8858
|
})
|
|
7287
8859
|
);
|
|
@@ -7331,7 +8903,7 @@ function createStatusFileAdapter(options = {}) {
|
|
|
7331
8903
|
const nextRead = /* @__PURE__ */ new Map();
|
|
7332
8904
|
const nextStatuses = /* @__PURE__ */ new Map();
|
|
7333
8905
|
for (const fileName2 of await toRead(listed)) {
|
|
7334
|
-
const reading = await readOne(
|
|
8906
|
+
const reading = await readOne(path17.join(dir, fileName2));
|
|
7335
8907
|
let kept;
|
|
7336
8908
|
if (reading.kind === "read") {
|
|
7337
8909
|
kept = reading;
|
|
@@ -7397,8 +8969,8 @@ function createStatusFileAdapter(options = {}) {
|
|
|
7397
8969
|
);
|
|
7398
8970
|
}
|
|
7399
8971
|
return {
|
|
7400
|
-
id:
|
|
7401
|
-
label:
|
|
8972
|
+
id: SOURCE_ID4,
|
|
8973
|
+
label: LABEL4,
|
|
7402
8974
|
capabilities: STATUS_FILE_CAPABILITIES,
|
|
7403
8975
|
lookingIn: `Looking for status files in ${dirName}.`,
|
|
7404
8976
|
async poll() {
|
|
@@ -7557,8 +9129,8 @@ function createEventStore(capacity = EVENT_CAPACITY) {
|
|
|
7557
9129
|
}
|
|
7558
9130
|
|
|
7559
9131
|
// src/collector/git/branchFinder.ts
|
|
7560
|
-
import
|
|
7561
|
-
import
|
|
9132
|
+
import os8 from "node:os";
|
|
9133
|
+
import path19 from "node:path";
|
|
7562
9134
|
|
|
7563
9135
|
// src/collector/git/gitHead.ts
|
|
7564
9136
|
var MAX_GIT_FILE_BYTES = 4 * 1024;
|
|
@@ -7597,19 +9169,19 @@ function parseCommonDir(content) {
|
|
|
7597
9169
|
}
|
|
7598
9170
|
|
|
7599
9171
|
// src/collector/git/repository.ts
|
|
7600
|
-
import { createHash as
|
|
7601
|
-
import
|
|
9172
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
9173
|
+
import path18 from "node:path";
|
|
7602
9174
|
var REPOSITORY_ID_LENGTH = 16;
|
|
7603
9175
|
function repositoryId(gitFolder) {
|
|
7604
|
-
return
|
|
9176
|
+
return createHash3("sha256").update(gitFolder).digest("hex").slice(0, REPOSITORY_ID_LENGTH);
|
|
7605
9177
|
}
|
|
7606
9178
|
function sharedName(common) {
|
|
7607
|
-
const base =
|
|
7608
|
-
if (base.startsWith(".")) return
|
|
9179
|
+
const base = path18.basename(common);
|
|
9180
|
+
if (base.startsWith(".")) return path18.basename(path18.dirname(common));
|
|
7609
9181
|
return base.endsWith(".git") ? base.slice(0, -".git".length) : base;
|
|
7610
9182
|
}
|
|
7611
9183
|
function repositoryOf({ dir, gitDir, commonDir }) {
|
|
7612
|
-
const name = sessionName(commonDir === null ?
|
|
9184
|
+
const name = sessionName(commonDir === null ? path18.basename(dir) : sharedName(commonDir));
|
|
7613
9185
|
return name === void 0 ? null : { id: repositoryId(commonDir ?? gitDir), name };
|
|
7614
9186
|
}
|
|
7615
9187
|
|
|
@@ -7627,9 +9199,9 @@ async function readGitFile(io, file, maxBytes = MAX_GIT_FILE_BYTES) {
|
|
|
7627
9199
|
}
|
|
7628
9200
|
try {
|
|
7629
9201
|
if (open2.info.size > maxBytes) return null;
|
|
7630
|
-
const
|
|
7631
|
-
if (
|
|
7632
|
-
return decoder.decode(
|
|
9202
|
+
const bytes3 = await open2.read(0, maxBytes + 1);
|
|
9203
|
+
if (bytes3.byteLength > maxBytes) return null;
|
|
9204
|
+
return decoder.decode(bytes3);
|
|
7633
9205
|
} catch {
|
|
7634
9206
|
return null;
|
|
7635
9207
|
} finally {
|
|
@@ -7638,13 +9210,13 @@ async function readGitFile(io, file, maxBytes = MAX_GIT_FILE_BYTES) {
|
|
|
7638
9210
|
}
|
|
7639
9211
|
}
|
|
7640
9212
|
async function commonDirOf(io, gitDir) {
|
|
7641
|
-
const content = await readGitFile(io,
|
|
9213
|
+
const content = await readGitFile(io, path19.join(gitDir, "commondir"));
|
|
7642
9214
|
if (content === void 0) return null;
|
|
7643
9215
|
const target = content === null ? null : parseCommonDir(content);
|
|
7644
|
-
return target === null ? void 0 :
|
|
9216
|
+
return target === null ? void 0 : path19.resolve(gitDir, target);
|
|
7645
9217
|
}
|
|
7646
9218
|
async function headIn(dir, kind, io) {
|
|
7647
|
-
const marker =
|
|
9219
|
+
const marker = path19.join(dir, ".git");
|
|
7648
9220
|
let gitDir;
|
|
7649
9221
|
if (kind === "directory") {
|
|
7650
9222
|
gitDir = marker;
|
|
@@ -7652,11 +9224,11 @@ async function headIn(dir, kind, io) {
|
|
|
7652
9224
|
const content2 = await readGitFile(io, marker);
|
|
7653
9225
|
const target = typeof content2 === "string" ? parseGitFile(content2) : null;
|
|
7654
9226
|
if (target === null) return null;
|
|
7655
|
-
gitDir =
|
|
9227
|
+
gitDir = path19.resolve(dir, target);
|
|
7656
9228
|
} else {
|
|
7657
9229
|
return null;
|
|
7658
9230
|
}
|
|
7659
|
-
const content = await readGitFile(io,
|
|
9231
|
+
const content = await readGitFile(io, path19.join(gitDir, "HEAD"));
|
|
7660
9232
|
const head = typeof content === "string" ? parseHead(content) : null;
|
|
7661
9233
|
if (head === null) return null;
|
|
7662
9234
|
const commonDir = kind === "file" ? await commonDirOf(io, gitDir) : null;
|
|
@@ -7665,20 +9237,20 @@ async function headIn(dir, kind, io) {
|
|
|
7665
9237
|
return { head: { ...head, repository }, gitFolder: commonDir ?? gitDir };
|
|
7666
9238
|
}
|
|
7667
9239
|
async function findHead(cwd, { io, homeDir }) {
|
|
7668
|
-
if (!
|
|
7669
|
-
const home =
|
|
7670
|
-
let dir =
|
|
9240
|
+
if (!path19.isAbsolute(cwd)) return null;
|
|
9241
|
+
const home = path19.resolve(homeDir);
|
|
9242
|
+
let dir = path19.resolve(cwd);
|
|
7671
9243
|
try {
|
|
7672
9244
|
if ((await io.stat(dir)).kind !== "directory") return null;
|
|
7673
9245
|
} catch {
|
|
7674
9246
|
return null;
|
|
7675
9247
|
}
|
|
7676
9248
|
for (let level = 0; level < MAX_LEVELS; level += 1) {
|
|
7677
|
-
const parent =
|
|
9249
|
+
const parent = path19.dirname(dir);
|
|
7678
9250
|
if (dir === home || dir === parent) return null;
|
|
7679
9251
|
let kind;
|
|
7680
9252
|
try {
|
|
7681
|
-
kind = (await io.lstat(
|
|
9253
|
+
kind = (await io.lstat(path19.join(dir, ".git"))).kind;
|
|
7682
9254
|
} catch (error) {
|
|
7683
9255
|
if (!isMissing2(error)) return null;
|
|
7684
9256
|
dir = parent;
|
|
@@ -7699,7 +9271,7 @@ function within(work, ms) {
|
|
|
7699
9271
|
});
|
|
7700
9272
|
}
|
|
7701
9273
|
function createBranchFinder(options = {}) {
|
|
7702
|
-
const where = { io: options.io ?? nodeIo2, homeDir: options.homeDir ??
|
|
9274
|
+
const where = { io: options.io ?? nodeIo2, homeDir: options.homeDir ?? os8.homedir() };
|
|
7703
9275
|
const now = options.now ?? Date.now;
|
|
7704
9276
|
const waitMs = options.waitMs ?? READ_WAIT_MS;
|
|
7705
9277
|
const known = /* @__PURE__ */ new Map();
|
|
@@ -7761,14 +9333,14 @@ var STATUS_STATE = {
|
|
|
7761
9333
|
PENDING: "pending",
|
|
7762
9334
|
EXPECTED: "pending"
|
|
7763
9335
|
};
|
|
7764
|
-
function
|
|
9336
|
+
function isRecord12(value) {
|
|
7765
9337
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7766
9338
|
}
|
|
7767
9339
|
function textOf(value) {
|
|
7768
9340
|
return typeof value === "string" ? value : "";
|
|
7769
9341
|
}
|
|
7770
9342
|
function checkOf(value) {
|
|
7771
|
-
if (!
|
|
9343
|
+
if (!isRecord12(value)) return null;
|
|
7772
9344
|
const startedAt = Date.parse(textOf(value.startedAt));
|
|
7773
9345
|
const at = Number.isNaN(startedAt) ? -Infinity : startedAt;
|
|
7774
9346
|
if (value.__typename === "CheckRun") {
|
|
@@ -7804,7 +9376,7 @@ function checksOf(rollup) {
|
|
|
7804
9376
|
for (const { state } of latest.values()) counts[state] += 1;
|
|
7805
9377
|
return { state: checksStateOf(counts), ...counts };
|
|
7806
9378
|
}
|
|
7807
|
-
function
|
|
9379
|
+
function stateOf3(state, isDraft) {
|
|
7808
9380
|
if (state === "OPEN") return isDraft === true ? "draft" : "open";
|
|
7809
9381
|
if (state === "MERGED") return "merged";
|
|
7810
9382
|
if (state === "CLOSED") return "closed";
|
|
@@ -7817,9 +9389,9 @@ function readGhAnswer(stdout, repository) {
|
|
|
7817
9389
|
} catch {
|
|
7818
9390
|
return null;
|
|
7819
9391
|
}
|
|
7820
|
-
if (!
|
|
9392
|
+
if (!isRecord12(data)) return null;
|
|
7821
9393
|
const { number } = data;
|
|
7822
|
-
const state =
|
|
9394
|
+
const state = stateOf3(data.state, data.isDraft);
|
|
7823
9395
|
const title = typeof data.title === "string" ? sessionName(data.title) : void 0;
|
|
7824
9396
|
if (!isPullRequestNumber(number) || state === null || title === void 0) return null;
|
|
7825
9397
|
return {
|
|
@@ -7956,7 +9528,7 @@ function createGhAsker(options) {
|
|
|
7956
9528
|
}
|
|
7957
9529
|
|
|
7958
9530
|
// src/collector/git/pullRequestTarget.ts
|
|
7959
|
-
import
|
|
9531
|
+
import path20 from "node:path";
|
|
7960
9532
|
|
|
7961
9533
|
// src/collector/git/gitConfig.ts
|
|
7962
9534
|
var MAX_CONFIG_BYTES = 64 * 1024;
|
|
@@ -8214,7 +9786,7 @@ function targetOf(config, branch, base, defaultBranch) {
|
|
|
8214
9786
|
async function readPullRequestTarget(gitFolder, branch, io = nodeIo2) {
|
|
8215
9787
|
try {
|
|
8216
9788
|
if (!isAskableBranch(branch)) return null;
|
|
8217
|
-
const text4 = await readGitFile(io,
|
|
9789
|
+
const text4 = await readGitFile(io, path20.join(gitFolder, "config"), MAX_CONFIG_BYTES);
|
|
8218
9790
|
if (typeof text4 !== "string") return null;
|
|
8219
9791
|
const config = parseGitConfig(text4);
|
|
8220
9792
|
const base = config === null ? null : baseRemoteOf(config);
|
|
@@ -8223,7 +9795,7 @@ async function readPullRequestTarget(gitFolder, branch, io = nodeIo2) {
|
|
|
8223
9795
|
if (base.name !== null && isSafeRemoteName(base.name)) {
|
|
8224
9796
|
const head = await readGitFile(
|
|
8225
9797
|
io,
|
|
8226
|
-
|
|
9798
|
+
path20.join(gitFolder, "refs", "remotes", base.name, "HEAD")
|
|
8227
9799
|
);
|
|
8228
9800
|
if (typeof head === "string") defaultBranch = parseRemoteHead(head, base.name);
|
|
8229
9801
|
}
|
|
@@ -8394,7 +9966,7 @@ function createClearHistoryRoute(options) {
|
|
|
8394
9966
|
}
|
|
8395
9967
|
|
|
8396
9968
|
// src/collector/history/historyKeeper.ts
|
|
8397
|
-
import
|
|
9969
|
+
import path21 from "node:path";
|
|
8398
9970
|
|
|
8399
9971
|
// src/collector/history/historyFiles.ts
|
|
8400
9972
|
import {
|
|
@@ -8694,7 +10266,7 @@ function createHistoryKeeper(options) {
|
|
|
8694
10266
|
});
|
|
8695
10267
|
const lock = createWriterLock({
|
|
8696
10268
|
fs,
|
|
8697
|
-
file:
|
|
10269
|
+
file: path21.join(dir, LOCK_FILE),
|
|
8698
10270
|
pid: options.pid,
|
|
8699
10271
|
now,
|
|
8700
10272
|
isAlive: options.isAlive,
|
|
@@ -8720,7 +10292,7 @@ function createHistoryKeeper(options) {
|
|
|
8720
10292
|
});
|
|
8721
10293
|
return next;
|
|
8722
10294
|
}
|
|
8723
|
-
const full = (name) =>
|
|
10295
|
+
const full = (name) => path21.join(dir, name);
|
|
8724
10296
|
function hold(record) {
|
|
8725
10297
|
if (unusable !== null || !running) return;
|
|
8726
10298
|
const line = encodeRecord(record);
|
|
@@ -8887,13 +10459,13 @@ function createHistoryKeeper(options) {
|
|
|
8887
10459
|
continue;
|
|
8888
10460
|
}
|
|
8889
10461
|
let text4 = lead;
|
|
8890
|
-
let
|
|
10462
|
+
let bytes3 = lead.length;
|
|
8891
10463
|
let end = index;
|
|
8892
10464
|
while (end < lines.length) {
|
|
8893
10465
|
const lineBytes = byteLength(lines[end]);
|
|
8894
|
-
if (
|
|
10466
|
+
if (bytes3 - lead.length + lineBytes > room) break;
|
|
8895
10467
|
text4 += lines[end];
|
|
8896
|
-
|
|
10468
|
+
bytes3 += lineBytes;
|
|
8897
10469
|
end += 1;
|
|
8898
10470
|
}
|
|
8899
10471
|
const outcome = yield { name, text: text4 };
|
|
@@ -8906,7 +10478,7 @@ function createHistoryKeeper(options) {
|
|
|
8906
10478
|
const parsed = readHistoryFileName(name);
|
|
8907
10479
|
files.set(name, {
|
|
8908
10480
|
...parsed,
|
|
8909
|
-
size: size +
|
|
10481
|
+
size: size + bytes3,
|
|
8910
10482
|
first: known?.first ?? (record && { at: recordAt(record), kind: record.kind })
|
|
8911
10483
|
});
|
|
8912
10484
|
touched.add(name);
|
|
@@ -9134,8 +10706,8 @@ function createHistoryKeeper(options) {
|
|
|
9134
10706
|
return { at: oldest.first.at, by: BEGINNING[oldest.first.kind] };
|
|
9135
10707
|
},
|
|
9136
10708
|
status() {
|
|
9137
|
-
let
|
|
9138
|
-
for (const file of files.values())
|
|
10709
|
+
let bytes3 = 0;
|
|
10710
|
+
for (const file of files.values()) bytes3 += file.size;
|
|
9139
10711
|
let problem = unusable ?? failing;
|
|
9140
10712
|
if (problem === null && writer === false) {
|
|
9141
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.";
|
|
@@ -9143,7 +10715,7 @@ function createHistoryKeeper(options) {
|
|
|
9143
10715
|
return {
|
|
9144
10716
|
where: "disk",
|
|
9145
10717
|
folder,
|
|
9146
|
-
bytes:
|
|
10718
|
+
bytes: bytes3,
|
|
9147
10719
|
maxBytes,
|
|
9148
10720
|
maxAgeMs,
|
|
9149
10721
|
canClear: unusable === null && writer === true && lock.held,
|
|
@@ -9180,15 +10752,15 @@ function keptHistoryStore(store, keeper) {
|
|
|
9180
10752
|
}
|
|
9181
10753
|
|
|
9182
10754
|
// src/collector/history/historySettings.ts
|
|
9183
|
-
import { createHash as
|
|
9184
|
-
import
|
|
9185
|
-
import
|
|
10755
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
10756
|
+
import os9 from "node:os";
|
|
10757
|
+
import path22 from "node:path";
|
|
9186
10758
|
var HISTORY_ENV = "AGENT_LOOKOUT_HISTORY";
|
|
9187
10759
|
var HISTORY_DIR_ENV = "AGENT_LOOKOUT_HISTORY_DIR";
|
|
9188
|
-
function readHistorySetup(env, homeDir =
|
|
10760
|
+
function readHistorySetup(env, homeDir = os9.homedir()) {
|
|
9189
10761
|
if (env[HISTORY_ENV]?.trim().toLowerCase() === "off") return { on: false };
|
|
9190
10762
|
const override = env[HISTORY_DIR_ENV]?.trim() || void 0;
|
|
9191
|
-
const dir =
|
|
10763
|
+
const dir = path22.resolve(override ?? path22.join(homeDir, ".agent-lookout", "history"));
|
|
9192
10764
|
return {
|
|
9193
10765
|
on: true,
|
|
9194
10766
|
dir,
|
|
@@ -9196,14 +10768,15 @@ function readHistorySetup(env, homeDir = os8.homedir()) {
|
|
|
9196
10768
|
sources: sourcesFingerprint(env, homeDir)
|
|
9197
10769
|
};
|
|
9198
10770
|
}
|
|
9199
|
-
function sourcesFingerprint(env, homeDir =
|
|
10771
|
+
function sourcesFingerprint(env, homeDir = os9.homedir()) {
|
|
9200
10772
|
const set = (name) => env[name]?.trim() || void 0;
|
|
9201
10773
|
const folders = [
|
|
9202
|
-
|
|
9203
|
-
|
|
9204
|
-
|
|
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))
|
|
9205
10778
|
];
|
|
9206
|
-
return
|
|
10779
|
+
return createHash4("sha256").update(JSON.stringify(folders)).digest("hex").slice(0, 32);
|
|
9207
10780
|
}
|
|
9208
10781
|
|
|
9209
10782
|
// src/collector/notifications/heldWait.ts
|
|
@@ -9244,9 +10817,9 @@ function createServerNotifications(options) {
|
|
|
9244
10817
|
const held = /* @__PURE__ */ new Map();
|
|
9245
10818
|
const reminders = createReminderWatch();
|
|
9246
10819
|
const quiet = createQuietHold();
|
|
9247
|
-
function show(notice) {
|
|
10820
|
+
function show(notice, about) {
|
|
9248
10821
|
try {
|
|
9249
|
-
notifier.show(notice);
|
|
10822
|
+
notifier.show(notice, about);
|
|
9250
10823
|
} catch {
|
|
9251
10824
|
}
|
|
9252
10825
|
}
|
|
@@ -9343,82 +10916,505 @@ function createServerNotifications(options) {
|
|
|
9343
10916
|
}
|
|
9344
10917
|
continue;
|
|
9345
10918
|
}
|
|
9346
|
-
if (entry.kind === "change")
|
|
9347
|
-
|
|
9348
|
-
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 });
|
|
9349
10929
|
} else show(summaryNotice(entry.summary.items));
|
|
9350
10930
|
}
|
|
9351
10931
|
}
|
|
9352
10932
|
};
|
|
9353
10933
|
}
|
|
9354
10934
|
|
|
9355
|
-
// src/collector/processes/osascript.ts
|
|
9356
|
-
import { execFile as execFile3 } from "node:child_process";
|
|
9357
|
-
var OSASCRIPT = "/usr/bin/osascript";
|
|
9358
|
-
var MAX_OUTPUT_BYTES4 = 64 * 1024;
|
|
9359
|
-
var NOT_RUN2 = { ok: false, stderr: "", timedOut: false };
|
|
9360
|
-
function runOsascript(args, options = {}) {
|
|
9361
|
-
return new Promise((resolve) => {
|
|
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
|
+
}
|
|
9362
11312
|
try {
|
|
9363
|
-
const
|
|
9364
|
-
|
|
9365
|
-
|
|
9366
|
-
|
|
9367
|
-
|
|
9368
|
-
|
|
9369
|
-
|
|
9370
|
-
|
|
9371
|
-
|
|
9372
|
-
(error, stdout, stderr) => {
|
|
9373
|
-
if (!error) {
|
|
9374
|
-
resolve({ ok: true, stdout: String(stdout ?? "") });
|
|
9375
|
-
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))
|
|
9376
11322
|
}
|
|
9377
|
-
|
|
9378
|
-
|
|
9379
|
-
|
|
9380
|
-
|
|
9381
|
-
|
|
9382
|
-
|
|
9383
|
-
|
|
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();
|
|
9384
11334
|
}
|
|
9385
|
-
}
|
|
11335
|
+
};
|
|
9386
11336
|
}
|
|
9387
11337
|
|
|
9388
|
-
// src/collector/
|
|
9389
|
-
|
|
9390
|
-
|
|
9391
|
-
|
|
9392
|
-
|
|
9393
|
-
|
|
9394
|
-
|
|
9395
|
-
|
|
9396
|
-
|
|
9397
|
-
|
|
9398
|
-
"--",
|
|
9399
|
-
oneLine(notice.title),
|
|
9400
|
-
oneLine(notice.body, MAX_NOTICE_TEXT_LENGTH)
|
|
9401
|
-
];
|
|
9402
|
-
}
|
|
9403
|
-
function systemNotificationsShownOn(platform) {
|
|
9404
|
-
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
|
+
};
|
|
9405
11348
|
}
|
|
9406
|
-
|
|
9407
|
-
|
|
9408
|
-
|
|
9409
|
-
|
|
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);
|
|
9410
11368
|
return {
|
|
9411
|
-
|
|
9412
|
-
|
|
11369
|
+
async send(message) {
|
|
11370
|
+
let body;
|
|
9413
11371
|
try {
|
|
9414
|
-
|
|
9415
|
-
});
|
|
11372
|
+
body = JSON.stringify(pushoverBody(settings.token, settings.user, message));
|
|
9416
11373
|
} catch {
|
|
11374
|
+
return { sent: false, reason: PUSHOVER_WORDS.notSent };
|
|
9417
11375
|
}
|
|
11376
|
+
return outcomeOfPost(await poster.post(body), PUSHOVER_WORDS);
|
|
9418
11377
|
}
|
|
9419
11378
|
};
|
|
9420
11379
|
}
|
|
9421
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
|
+
|
|
9422
11418
|
// src/collector/remotes/remoteReader.ts
|
|
9423
11419
|
import { request } from "node:http";
|
|
9424
11420
|
var REMOTE_ANSWER_TIMEOUT_MS = 6e3;
|
|
@@ -9495,7 +11491,7 @@ var MAX_REMOTE_SOURCES = 20;
|
|
|
9495
11491
|
var MAX_ID_LENGTH = 512;
|
|
9496
11492
|
var MAX_PATH_LENGTH = 4096;
|
|
9497
11493
|
var MAX_WORD_LENGTH2 = 200;
|
|
9498
|
-
function
|
|
11494
|
+
function isRecord13(value) {
|
|
9499
11495
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9500
11496
|
}
|
|
9501
11497
|
function oneOf2(allowed, value) {
|
|
@@ -9509,7 +11505,7 @@ function exact(value, max) {
|
|
|
9509
11505
|
var COMMIT_ID2 = /^[0-9a-f]{7,64}$/;
|
|
9510
11506
|
var REPOSITORY_ID = /^[0-9a-f]{16,64}$/;
|
|
9511
11507
|
function readGit(value, machine) {
|
|
9512
|
-
if (!
|
|
11508
|
+
if (!isRecord13(value)) return void 0;
|
|
9513
11509
|
let head;
|
|
9514
11510
|
if (value.commit !== void 0) {
|
|
9515
11511
|
if (value.branch !== void 0 || typeof value.commit !== "string") return void 0;
|
|
@@ -9521,7 +11517,7 @@ function readGit(value, machine) {
|
|
|
9521
11517
|
head = { branch };
|
|
9522
11518
|
}
|
|
9523
11519
|
const repository = value.repository;
|
|
9524
|
-
if (
|
|
11520
|
+
if (isRecord13(repository) && typeof repository.id === "string") {
|
|
9525
11521
|
const name = sessionName(typeof repository.name === "string" ? repository.name : "");
|
|
9526
11522
|
if (name !== void 0 && REPOSITORY_ID.test(repository.id)) {
|
|
9527
11523
|
head.repository = { id: repositoryId(`${machine}
|
|
@@ -9531,7 +11527,7 @@ ${repository.id}`), name };
|
|
|
9531
11527
|
return head;
|
|
9532
11528
|
}
|
|
9533
11529
|
function readSession(value, machine, sources, now, keepWaitingText) {
|
|
9534
|
-
if (!
|
|
11530
|
+
if (!isRecord13(value)) return null;
|
|
9535
11531
|
const id = exact(value.id, MAX_ID_LENGTH);
|
|
9536
11532
|
const source = exact(value.source, MAX_WORD_LENGTH2);
|
|
9537
11533
|
if (id === null || source === null) return null;
|
|
@@ -9571,7 +11567,7 @@ function readSession(value, machine, sources, now, keepWaitingText) {
|
|
|
9571
11567
|
return session;
|
|
9572
11568
|
}
|
|
9573
11569
|
function readCell(value) {
|
|
9574
|
-
if (!
|
|
11570
|
+
if (!isRecord13(value)) return null;
|
|
9575
11571
|
if (value.level === "yes") return { level: "yes" };
|
|
9576
11572
|
const level = oneOf2(["no", "partly"], value.level);
|
|
9577
11573
|
const reason = cut(value.reason, MAX_WORD_LENGTH2);
|
|
@@ -9579,7 +11575,7 @@ function readCell(value) {
|
|
|
9579
11575
|
}
|
|
9580
11576
|
var ACTS_HERE = ["jump", "stop", "answer"];
|
|
9581
11577
|
function readCapabilities(value, machine) {
|
|
9582
|
-
if (!
|
|
11578
|
+
if (!isRecord13(value)) return null;
|
|
9583
11579
|
const read = {};
|
|
9584
11580
|
for (const capability of CAPABILITIES) {
|
|
9585
11581
|
if (ACTS_HERE.includes(capability)) continue;
|
|
@@ -9595,7 +11591,7 @@ function readCapabilities(value, machine) {
|
|
|
9595
11591
|
};
|
|
9596
11592
|
}
|
|
9597
11593
|
function readRemoteSnapshot(value, machine, now, options = {}) {
|
|
9598
|
-
if (!
|
|
11594
|
+
if (!isRecord13(value) || !Array.isArray(value.sessions) || !Array.isArray(value.sources)) {
|
|
9599
11595
|
return null;
|
|
9600
11596
|
}
|
|
9601
11597
|
const named = /* @__PURE__ */ new Map();
|
|
@@ -9603,7 +11599,7 @@ function readRemoteSnapshot(value, machine, now, options = {}) {
|
|
|
9603
11599
|
const agents = [];
|
|
9604
11600
|
for (const item of value.sources) {
|
|
9605
11601
|
if (sources.length >= MAX_REMOTE_SOURCES) break;
|
|
9606
|
-
if (!
|
|
11602
|
+
if (!isRecord13(item)) continue;
|
|
9607
11603
|
const id = exact(item.id, MAX_WORD_LENGTH2);
|
|
9608
11604
|
if (id === null || isRemoteSource(id) || item.machine !== void 0) continue;
|
|
9609
11605
|
const label2 = cut(item.label, MAX_WORD_LENGTH2) ?? id;
|
|
@@ -9628,7 +11624,7 @@ function readRemoteSnapshot(value, machine, now, options = {}) {
|
|
|
9628
11624
|
}
|
|
9629
11625
|
|
|
9630
11626
|
// src/collector/remotes/sshProgram.ts
|
|
9631
|
-
import
|
|
11627
|
+
import path23 from "node:path";
|
|
9632
11628
|
var SSH_BIN_ENV = "AGENT_LOOKOUT_SSH_BIN";
|
|
9633
11629
|
var SSH_LOCATIONS = ["/usr/bin/ssh", "/opt/homebrew/bin/ssh", "/usr/local/bin/ssh"];
|
|
9634
11630
|
var SERVER_ALIVE_SECONDS = 15;
|
|
@@ -9650,7 +11646,7 @@ async function findSsh(env, isExecutable, platform = process.platform) {
|
|
|
9650
11646
|
}
|
|
9651
11647
|
return {
|
|
9652
11648
|
found: false,
|
|
9653
|
-
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(", ")}`
|
|
9654
11650
|
};
|
|
9655
11651
|
}
|
|
9656
11652
|
function sshArguments(forward) {
|
|
@@ -9674,7 +11670,7 @@ function sshArguments(forward) {
|
|
|
9674
11670
|
}
|
|
9675
11671
|
|
|
9676
11672
|
// src/collector/remotes/remoteAdapter.ts
|
|
9677
|
-
var
|
|
11673
|
+
var BASIS4 = "api";
|
|
9678
11674
|
var NOT_CONNECTED = "unavailable";
|
|
9679
11675
|
var ANSWER_WAIT_MS = 500;
|
|
9680
11676
|
var STILL_CONNECTING_MS = 1e4;
|
|
@@ -9740,7 +11736,7 @@ function createRemoteAdapter(options) {
|
|
|
9740
11736
|
if (found.advice) health.advice = found.advice;
|
|
9741
11737
|
if (agents.length > 0) health.agents = agents;
|
|
9742
11738
|
const sessions = state === "ok" && snapshot ? snapshot.sessions : [];
|
|
9743
|
-
return state === "ok" ? { health, sessions, basis:
|
|
11739
|
+
return state === "ok" ? { health, sessions, basis: BASIS4 } : { health, sessions };
|
|
9744
11740
|
}
|
|
9745
11741
|
function connecting(at) {
|
|
9746
11742
|
if (problem === null) {
|
|
@@ -10245,15 +12241,15 @@ import {
|
|
|
10245
12241
|
unlinkSync as unlinkSync2,
|
|
10246
12242
|
writeSync as writeSync2
|
|
10247
12243
|
} from "node:fs";
|
|
10248
|
-
import
|
|
10249
|
-
import
|
|
12244
|
+
import os10 from "node:os";
|
|
12245
|
+
import path24 from "node:path";
|
|
10250
12246
|
var SETTINGS_FILE_ENV = "AGENT_LOOKOUT_SETTINGS_FILE";
|
|
10251
12247
|
var SETTINGS_FILE_MODE = 384;
|
|
10252
12248
|
var SETTINGS_FOLDER_MODE = 448;
|
|
10253
12249
|
var MAX_SETTINGS_BYTES = 64 * 1024;
|
|
10254
|
-
function readSettingsSetup(env, homeDir =
|
|
12250
|
+
function readSettingsSetup(env, homeDir = os10.homedir()) {
|
|
10255
12251
|
const named = env[SETTINGS_FILE_ENV]?.trim() || void 0;
|
|
10256
|
-
const file =
|
|
12252
|
+
const file = path24.resolve(named ?? path24.join(homeDir, ".agent-lookout", "settings.json"));
|
|
10257
12253
|
return { file, shown: tildify(file, homeDir) };
|
|
10258
12254
|
}
|
|
10259
12255
|
function codeOf(error) {
|
|
@@ -10298,7 +12294,7 @@ function readSettingsStamp(setup) {
|
|
|
10298
12294
|
}
|
|
10299
12295
|
function readSettingsText(setup) {
|
|
10300
12296
|
const { file, shown } = setup;
|
|
10301
|
-
const folder =
|
|
12297
|
+
const folder = path24.dirname(file);
|
|
10302
12298
|
try {
|
|
10303
12299
|
const folderKind = kindAt(folder);
|
|
10304
12300
|
if (folderKind === null) return { kind: "missing" };
|
|
@@ -10357,7 +12353,7 @@ function readSettingsText(setup) {
|
|
|
10357
12353
|
}
|
|
10358
12354
|
function writeSettingsText(setup, text4) {
|
|
10359
12355
|
const { file, shown } = setup;
|
|
10360
|
-
const folder =
|
|
12356
|
+
const folder = path24.dirname(file);
|
|
10361
12357
|
const notSaved = {
|
|
10362
12358
|
ok: false,
|
|
10363
12359
|
problem: `${shown} could not be written, so the change was not saved.`
|
|
@@ -10378,9 +12374,9 @@ function writeSettingsText(setup, text4) {
|
|
|
10378
12374
|
problem: `${shown} is a link or not an ordinary file, which Agent Lookout does not write through, so the change was not saved.`
|
|
10379
12375
|
};
|
|
10380
12376
|
}
|
|
10381
|
-
temporary =
|
|
12377
|
+
temporary = path24.join(
|
|
10382
12378
|
folder,
|
|
10383
|
-
`.${
|
|
12379
|
+
`.${path24.basename(file)}.${process.pid}.${randomBytes3(6).toString("hex")}.tmp`
|
|
10384
12380
|
);
|
|
10385
12381
|
const fd = openWithoutFollowingNow(
|
|
10386
12382
|
temporary,
|
|
@@ -10420,7 +12416,7 @@ var RULE_WORDS = {
|
|
|
10420
12416
|
idle: "the idle rule",
|
|
10421
12417
|
quietHours: "the quiet hours"
|
|
10422
12418
|
};
|
|
10423
|
-
function
|
|
12419
|
+
function isRecord14(value) {
|
|
10424
12420
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10425
12421
|
}
|
|
10426
12422
|
function listOf(names) {
|
|
@@ -10458,7 +12454,7 @@ function rulesIn(read, shown) {
|
|
|
10458
12454
|
} catch {
|
|
10459
12455
|
value = null;
|
|
10460
12456
|
}
|
|
10461
|
-
if (!
|
|
12457
|
+
if (!isRecord14(value)) {
|
|
10462
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.`;
|
|
10463
12459
|
return {
|
|
10464
12460
|
rules: off,
|
|
@@ -10473,7 +12469,7 @@ function rulesIn(read, shown) {
|
|
|
10473
12469
|
const { timeRules, permissionRules: heldRules, ...rest } = value;
|
|
10474
12470
|
const permissions = readPermissionRules(heldRules);
|
|
10475
12471
|
const { rules, unread } = readTimeRules(timeRules);
|
|
10476
|
-
const otherRules =
|
|
12472
|
+
const otherRules = isRecord14(timeRules) ? Object.fromEntries(
|
|
10477
12473
|
Object.entries(timeRules).filter(
|
|
10478
12474
|
([name]) => !TIME_RULE_NAMES.includes(name)
|
|
10479
12475
|
)
|
|
@@ -10575,15 +12571,15 @@ import { randomBytes as randomBytes4 } from "node:crypto";
|
|
|
10575
12571
|
|
|
10576
12572
|
// src/core/permission-rules/rulesChange.ts
|
|
10577
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.';
|
|
10578
|
-
function
|
|
12574
|
+
function isRecord15(value) {
|
|
10579
12575
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10580
12576
|
}
|
|
10581
12577
|
function rulesChangeIn(value) {
|
|
10582
12578
|
const refused = { ok: false, problem: CHANGE_SHAPE };
|
|
10583
|
-
if (!
|
|
12579
|
+
if (!isRecord15(value) || Object.keys(value).length !== 1) return refused;
|
|
10584
12580
|
const [kind] = Object.keys(value);
|
|
10585
12581
|
const body = value[kind];
|
|
10586
|
-
if (!
|
|
12582
|
+
if (!isRecord15(body)) return refused;
|
|
10587
12583
|
switch (kind) {
|
|
10588
12584
|
case "add": {
|
|
10589
12585
|
const read = ruleWordsIn(body);
|
|
@@ -10659,7 +12655,7 @@ function applyRulesChange(rules, change, newId) {
|
|
|
10659
12655
|
|
|
10660
12656
|
// src/collector/settings/permissionRulesRoute.ts
|
|
10661
12657
|
var MAX_PERMISSION_RULES_BODY_BYTES = 1024;
|
|
10662
|
-
var
|
|
12658
|
+
var failed4 = (status, reason, error) => ({
|
|
10663
12659
|
status,
|
|
10664
12660
|
body: { error, reason }
|
|
10665
12661
|
});
|
|
@@ -10686,14 +12682,14 @@ function createPermissionRulesRoute(options) {
|
|
|
10686
12682
|
try {
|
|
10687
12683
|
value = JSON.parse(body.text);
|
|
10688
12684
|
} catch {
|
|
10689
|
-
return
|
|
12685
|
+
return failed4(400, "invalid", "The body must be JSON: one change to the permission rules.");
|
|
10690
12686
|
}
|
|
10691
12687
|
const asked = rulesChangeIn(value);
|
|
10692
|
-
if (!asked.ok) return
|
|
12688
|
+
if (!asked.ok) return failed4(400, "invalid", asked.problem);
|
|
10693
12689
|
const made = settings.changePermissionRules(
|
|
10694
12690
|
(rules) => applyRulesChange(rules, asked.change, newId)
|
|
10695
12691
|
);
|
|
10696
|
-
if (!made.ok) return
|
|
12692
|
+
if (!made.ok) return failed4(STATUS_OF[made.reason], made.reason, made.problem);
|
|
10697
12693
|
return {
|
|
10698
12694
|
status: 200,
|
|
10699
12695
|
body: { ok: true, permissionRules: made.rules }
|
|
@@ -10703,7 +12699,7 @@ function createPermissionRulesRoute(options) {
|
|
|
10703
12699
|
|
|
10704
12700
|
// src/collector/settings/timeRulesRoute.ts
|
|
10705
12701
|
var MAX_TIME_RULES_BODY_BYTES = 2048;
|
|
10706
|
-
var
|
|
12702
|
+
var failed5 = (status, reason, error) => ({
|
|
10707
12703
|
status,
|
|
10708
12704
|
body: { error, reason }
|
|
10709
12705
|
});
|
|
@@ -10727,12 +12723,12 @@ function createTimeRulesRoute(options) {
|
|
|
10727
12723
|
try {
|
|
10728
12724
|
value = JSON.parse(body.text);
|
|
10729
12725
|
} catch {
|
|
10730
|
-
return
|
|
12726
|
+
return failed5(400, "invalid", "The body must be JSON: the three time rules.");
|
|
10731
12727
|
}
|
|
10732
12728
|
const asked = timeRulesIn(value);
|
|
10733
|
-
if (!asked.ok) return
|
|
12729
|
+
if (!asked.ok) return failed5(400, "invalid", asked.problem);
|
|
10734
12730
|
const saved = settings.changeTimeRules(asked.rules);
|
|
10735
|
-
if (!saved.ok) return
|
|
12731
|
+
if (!saved.ok) return failed5(500, "not-saved", saved.problem);
|
|
10736
12732
|
return {
|
|
10737
12733
|
status: 200,
|
|
10738
12734
|
body: { ok: true, timeRules: asked.rules }
|
|
@@ -10776,11 +12772,11 @@ function createOsascriptRunner(options) {
|
|
|
10776
12772
|
}
|
|
10777
12773
|
|
|
10778
12774
|
// src/collector/terminal/tabFinder.ts
|
|
10779
|
-
import
|
|
12775
|
+
import os11 from "node:os";
|
|
10780
12776
|
var TAB_LOOK_SOONEST_MS = 5e3;
|
|
10781
12777
|
function createTabFinder(options) {
|
|
10782
12778
|
const off = terminalJumpOff(options.env);
|
|
10783
|
-
const homeDir = options.homeDir ??
|
|
12779
|
+
const homeDir = options.homeDir ?? os11.homedir();
|
|
10784
12780
|
const readProcesses = options.readProcesses ?? readProcessTableWithPs;
|
|
10785
12781
|
const now = options.now ?? Date.now;
|
|
10786
12782
|
const soonestMs = options.soonestMs ?? TAB_LOOK_SOONEST_MS;
|
|
@@ -11014,96 +13010,24 @@ function ledgerHistoryStore(store, ledger) {
|
|
|
11014
13010
|
}
|
|
11015
13011
|
|
|
11016
13012
|
// src/collector/webhook/webhookSender.ts
|
|
11017
|
-
var
|
|
11018
|
-
|
|
11019
|
-
|
|
11020
|
-
|
|
11021
|
-
|
|
11022
|
-
|
|
11023
|
-
|
|
11024
|
-
}
|
|
11025
|
-
var TIMED_OUT = "the address did not answer in time";
|
|
11026
|
-
function outcomeOfStatus(status) {
|
|
11027
|
-
if (status >= 200 && status < 300) return { sent: true };
|
|
11028
|
-
if (status >= 300 && status < 400) {
|
|
11029
|
-
return { sent: false, reason: "the address answered with a redirect, which is not followed" };
|
|
11030
|
-
}
|
|
11031
|
-
if (status >= 400 && status < 500) {
|
|
11032
|
-
return { sent: false, reason: `the address refused the post (status ${status})` };
|
|
11033
|
-
}
|
|
11034
|
-
if (status >= 500 && status < 600) {
|
|
11035
|
-
return { sent: false, reason: `the receiving service had a problem (status ${status})` };
|
|
11036
|
-
}
|
|
11037
|
-
return { sent: false, reason: "the address's answer could not be understood" };
|
|
11038
|
-
}
|
|
11039
|
-
function failureReason2(error) {
|
|
11040
|
-
const { code } = error ?? {};
|
|
11041
|
-
if (typeof code !== "string") return "the post could not be sent";
|
|
11042
|
-
if (code === "ECONNREFUSED") return "nothing answered at the address";
|
|
11043
|
-
if (code === "ENOTFOUND" || code === "EAI_AGAIN") return "the address's host could not be found";
|
|
11044
|
-
if (code === "ETIMEDOUT") return TIMED_OUT;
|
|
11045
|
-
if (/CERT|SELF_SIGNED|UNABLE_TO|^ERR_TLS|^ERR_SSL|^EPROTO$/.test(code)) {
|
|
11046
|
-
return "the secure connection to the address failed";
|
|
11047
|
-
}
|
|
11048
|
-
return "the connection to the address failed";
|
|
11049
|
-
}
|
|
11050
|
-
function requestOptions(body, version) {
|
|
11051
|
-
return {
|
|
11052
|
-
method: "POST",
|
|
11053
|
-
headers: postHeaders(body, version),
|
|
11054
|
-
// A connection of its own, closed after the answer.
|
|
11055
|
-
agent: false,
|
|
11056
|
-
rejectUnauthorized: true
|
|
11057
|
-
};
|
|
11058
|
-
}
|
|
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
|
+
};
|
|
11059
13020
|
function createHttpSender(settings, options) {
|
|
11060
|
-
const
|
|
11061
|
-
const timeoutMs = options.timeoutMs ?? WEBHOOK_TIMEOUT_MS;
|
|
11062
|
-
let client = null;
|
|
11063
|
-
const load = () => {
|
|
11064
|
-
client ??= url.protocol === "https:" ? import("node:https").then(({ request: request2 }) => request2) : import("node:http").then(({ request: request2 }) => request2);
|
|
11065
|
-
return client;
|
|
11066
|
-
};
|
|
13021
|
+
const poster = createJsonPoster(settings.url, options);
|
|
11067
13022
|
return {
|
|
11068
13023
|
async send(post) {
|
|
11069
|
-
let
|
|
13024
|
+
let body;
|
|
11070
13025
|
try {
|
|
11071
|
-
|
|
13026
|
+
body = JSON.stringify(post);
|
|
11072
13027
|
} catch {
|
|
11073
|
-
return { sent: false, reason:
|
|
13028
|
+
return { sent: false, reason: WORDS.notSent };
|
|
11074
13029
|
}
|
|
11075
|
-
return
|
|
11076
|
-
let finished = false;
|
|
11077
|
-
const finish = (outcome) => {
|
|
11078
|
-
if (finished) return;
|
|
11079
|
-
finished = true;
|
|
11080
|
-
resolve(outcome);
|
|
11081
|
-
};
|
|
11082
|
-
let body;
|
|
11083
|
-
let req;
|
|
11084
|
-
try {
|
|
11085
|
-
body = JSON.stringify(post);
|
|
11086
|
-
req = request2(url, requestOptions(body, options.version));
|
|
11087
|
-
} catch {
|
|
11088
|
-
finish({ sent: false, reason: "the post could not be sent" });
|
|
11089
|
-
return;
|
|
11090
|
-
}
|
|
11091
|
-
const timer = setTimeout(() => {
|
|
11092
|
-
finish({ sent: false, reason: TIMED_OUT });
|
|
11093
|
-
req.destroy();
|
|
11094
|
-
}, timeoutMs);
|
|
11095
|
-
timer.unref();
|
|
11096
|
-
req.on("response", (res) => {
|
|
11097
|
-
finish(outcomeOfStatus(res.statusCode ?? 0));
|
|
11098
|
-
res.on("close", () => clearTimeout(timer));
|
|
11099
|
-
res.resume();
|
|
11100
|
-
});
|
|
11101
|
-
req.on("error", (error) => {
|
|
11102
|
-
clearTimeout(timer);
|
|
11103
|
-
finish({ sent: false, reason: failureReason2(error) });
|
|
11104
|
-
});
|
|
11105
|
-
req.end(body);
|
|
11106
|
-
});
|
|
13030
|
+
return outcomeOfPost(await poster.post(body), WORDS);
|
|
11107
13031
|
}
|
|
11108
13032
|
};
|
|
11109
13033
|
}
|
|
@@ -11113,15 +13037,15 @@ var WEBHOOK_URL_ENV = "AGENT_LOOKOUT_WEBHOOK_URL";
|
|
|
11113
13037
|
var WEBHOOK_EVENTS_ENV = "AGENT_LOOKOUT_WEBHOOK_EVENTS";
|
|
11114
13038
|
var WEBHOOK_AFTER_ENV = "AGENT_LOOKOUT_WEBHOOK_AFTER";
|
|
11115
13039
|
var WEBHOOK_ASKING_ENV = "AGENT_LOOKOUT_WEBHOOK_ASKING";
|
|
11116
|
-
var
|
|
11117
|
-
var
|
|
13040
|
+
var MAX_URL_LENGTH2 = 2048;
|
|
13041
|
+
var URL_UNREADABLE3 = `${WEBHOOK_URL_ENV} could not be read. Copy the whole address, beginning https://.`;
|
|
11118
13042
|
function readUrl(value) {
|
|
11119
13043
|
let url;
|
|
11120
13044
|
try {
|
|
11121
|
-
if (value.length >
|
|
13045
|
+
if (value.length > MAX_URL_LENGTH2) throw new Error();
|
|
11122
13046
|
url = new URL(value);
|
|
11123
13047
|
} catch {
|
|
11124
|
-
throw new SettingProblem(
|
|
13048
|
+
throw new SettingProblem(URL_UNREADABLE3);
|
|
11125
13049
|
}
|
|
11126
13050
|
const local = LOCAL_HOSTS.has(url.hostname);
|
|
11127
13051
|
if (url.protocol !== "https:" && !(url.protocol === "http:" && local)) {
|
|
@@ -11129,7 +13053,7 @@ function readUrl(value) {
|
|
|
11129
13053
|
`${WEBHOOK_URL_ENV} must begin with https://, or with http:// for an address on this computer, 127.0.0.1 or localhost.`
|
|
11130
13054
|
);
|
|
11131
13055
|
}
|
|
11132
|
-
if (url.hostname === "") throw new SettingProblem(
|
|
13056
|
+
if (url.hostname === "") throw new SettingProblem(URL_UNREADABLE3);
|
|
11133
13057
|
if (!isWebhookHost(url.hostname)) {
|
|
11134
13058
|
throw new SettingProblem(
|
|
11135
13059
|
`${WEBHOOK_URL_ENV} must name its host in letters, digits, dots and dashes, such as hooks.slack.com.`
|
|
@@ -11155,7 +13079,7 @@ function readWebhookSetup(env) {
|
|
|
11155
13079
|
};
|
|
11156
13080
|
} catch (error) {
|
|
11157
13081
|
if (error instanceof SettingProblem) return { on: false, problem: error.message };
|
|
11158
|
-
return { on: false, problem:
|
|
13082
|
+
return { on: false, problem: URL_UNREADABLE3 };
|
|
11159
13083
|
}
|
|
11160
13084
|
}
|
|
11161
13085
|
function webhookProblemLine(problem) {
|
|
@@ -11252,6 +13176,26 @@ function createCollector(options) {
|
|
|
11252
13176
|
now
|
|
11253
13177
|
}) : null;
|
|
11254
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);
|
|
11255
13199
|
const remotes = createRemotes({
|
|
11256
13200
|
env,
|
|
11257
13201
|
version: options.version,
|
|
@@ -11272,6 +13216,7 @@ function createCollector(options) {
|
|
|
11272
13216
|
platform
|
|
11273
13217
|
}),
|
|
11274
13218
|
createCodexAdapter({ env: options.env, now, pollIntervalMs: intervalMs }),
|
|
13219
|
+
createAntigravityAdapter({ env: options.env, now, pollIntervalMs: intervalMs, platform }),
|
|
11275
13220
|
createStatusFileAdapter({ env: options.env, now, pollIntervalMs: intervalMs })
|
|
11276
13221
|
];
|
|
11277
13222
|
const poller = createPoller({
|
|
@@ -11306,6 +13251,8 @@ function createCollector(options) {
|
|
|
11306
13251
|
answering?.observe(snapshot);
|
|
11307
13252
|
email?.handle(snapshot);
|
|
11308
13253
|
webhook?.handle(snapshot);
|
|
13254
|
+
ntfy?.handle(snapshot);
|
|
13255
|
+
pushover?.handle(snapshot);
|
|
11309
13256
|
notifications.handle(snapshot);
|
|
11310
13257
|
options.onSnapshot?.(snapshot);
|
|
11311
13258
|
}
|
|
@@ -11348,6 +13295,13 @@ function createCollector(options) {
|
|
|
11348
13295
|
notifications,
|
|
11349
13296
|
email: () => email?.status() ?? emailOff,
|
|
11350
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
|
+
}),
|
|
11351
13305
|
pullRequests: () => pullRequests?.status() ?? pullRequestsOff,
|
|
11352
13306
|
jump: createJumpRoute({ poller, panes, run: tmux, tabs, osascript, now }),
|
|
11353
13307
|
stop: stopRoutes && createStopRoute(stopRoutes),
|
|
@@ -11442,13 +13396,20 @@ function createCollector(options) {
|
|
|
11442
13396
|
poller,
|
|
11443
13397
|
email,
|
|
11444
13398
|
webhook,
|
|
13399
|
+
ntfy,
|
|
13400
|
+
pushover,
|
|
11445
13401
|
pullRequests,
|
|
11446
13402
|
history: keeper,
|
|
11447
13403
|
answering: {
|
|
11448
13404
|
start: () => answerStart ??= answering?.start() ?? Promise.resolve(),
|
|
11449
13405
|
stop: async () => {
|
|
11450
13406
|
await answering?.stop();
|
|
11451
|
-
}
|
|
13407
|
+
},
|
|
13408
|
+
serve: (snapshot) => answering?.serve(snapshot) ?? snapshot,
|
|
13409
|
+
check: async () => {
|
|
13410
|
+
await answering?.check();
|
|
13411
|
+
},
|
|
13412
|
+
...answering?.answer && { answer: answering.answer }
|
|
11452
13413
|
},
|
|
11453
13414
|
remotes,
|
|
11454
13415
|
settings
|
|
@@ -11472,7 +13433,7 @@ async function runStandalone(options) {
|
|
|
11472
13433
|
} catch (error) {
|
|
11473
13434
|
return stopWith(error.message);
|
|
11474
13435
|
}
|
|
11475
|
-
if (!existsSync(
|
|
13436
|
+
if (!existsSync(path25.join(distDir, "index.html"))) {
|
|
11476
13437
|
return stopWith(
|
|
11477
13438
|
"The dashboard has not been built yet. Run `npm run build`, then start it again."
|
|
11478
13439
|
);
|
|
@@ -11515,12 +13476,12 @@ async function runStandalone(options) {
|
|
|
11515
13476
|
|
|
11516
13477
|
// src/cli/start/openBrowser.ts
|
|
11517
13478
|
import { spawn as spawn3 } from "node:child_process";
|
|
11518
|
-
import
|
|
13479
|
+
import path26 from "node:path";
|
|
11519
13480
|
var OPEN_WAIT_MS = 1e4;
|
|
11520
13481
|
var WINDOWS_FOLDER = "C:\\Windows";
|
|
11521
13482
|
function windowsFolder(env) {
|
|
11522
13483
|
const named = env.SystemRoot?.trim() || env.SYSTEMROOT?.trim();
|
|
11523
|
-
return named &&
|
|
13484
|
+
return named && path26.win32.isAbsolute(named) ? named : WINDOWS_FOLDER;
|
|
11524
13485
|
}
|
|
11525
13486
|
function openCommand(platform, url, env = process.env) {
|
|
11526
13487
|
const address = loopbackAddress(url);
|
|
@@ -11529,7 +13490,7 @@ function openCommand(platform, url, env = process.env) {
|
|
|
11529
13490
|
if (platform === "linux") return { file: "xdg-open", args: [address] };
|
|
11530
13491
|
if (platform === "win32") {
|
|
11531
13492
|
return {
|
|
11532
|
-
file:
|
|
13493
|
+
file: path26.win32.join(windowsFolder(env), "System32", "rundll32.exe"),
|
|
11533
13494
|
args: ["url.dll,FileProtocolHandler", address]
|
|
11534
13495
|
};
|
|
11535
13496
|
}
|