agent-lookout 0.2.7 → 0.2.9
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 +37 -0
- package/PRIVACY.md +164 -25
- package/README.md +46 -43
- package/SECURITY.md +29 -10
- package/dist/assets/{HistoryPanel-oSXRXEdv.js → HistoryPanel-Cr6VQlz-.js} +1 -1
- package/dist/assets/SessionPanel-YVGY4vbo.js +1 -0
- package/dist/assets/index-BRNSMV9A.css +1 -0
- package/dist/assets/index-DlDz48ci.js +9 -0
- package/dist/assets/motion-DlYOyClL.js +9 -0
- package/dist/cli/agent-lookout.js +4 -4
- package/dist/cli/{chunk-LZ62RW3N.js → chunk-AHCLJEYU.js} +1 -1
- package/dist/cli/{chunk-JSBQMY3Q.js → chunk-CMDNRNYP.js} +1 -1
- package/dist/cli/{chunk-I6Q35DHC.js → chunk-XV7BYWVY.js} +17 -0
- package/dist/cli/{mcpServer-HJSH3WIS.js → mcpServer-33QB6COH.js} +5 -5
- package/dist/cli/{startCommand-3LYUYPG4.js → startCommand-XUZHHWZG.js} +2881 -623
- package/dist/index.html +3 -3
- package/docs/API.md +150 -68
- package/docs/GUIDE.md +248 -94
- package/docs/INSTALL.md +8 -6
- package/package.json +5 -1
- package/dist/assets/SessionPanel-DcJvw0F8.js +0 -1
- package/dist/assets/index-B_1Its0R.css +0 -1
- package/dist/assets/index-DLPmOXDe.js +0 -3
- package/dist/assets/motion-BAhdRuCw.js +0 -9
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
sortSessions
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-CMDNRNYP.js";
|
|
4
4
|
import {
|
|
5
5
|
ANSWER_DECISIONS,
|
|
6
6
|
CAPABILITIES,
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
isRemoteSource,
|
|
35
35
|
isStatusEvent,
|
|
36
36
|
loopbackAddress,
|
|
37
|
+
messageText,
|
|
37
38
|
needsYou,
|
|
38
39
|
noticeTitle,
|
|
39
40
|
oneLine,
|
|
@@ -48,7 +49,7 @@ import {
|
|
|
48
49
|
waitingPhrase,
|
|
49
50
|
waitingText,
|
|
50
51
|
withoutWaitingText
|
|
51
|
-
} from "./chunk-
|
|
52
|
+
} from "./chunk-XV7BYWVY.js";
|
|
52
53
|
|
|
53
54
|
// src/collector/hosts/server.ts
|
|
54
55
|
import { createServer } from "node:http";
|
|
@@ -132,14 +133,14 @@ function sessionChanges(memory, snapshot) {
|
|
|
132
133
|
}
|
|
133
134
|
const found = [];
|
|
134
135
|
for (const session of snapshot.sessions) {
|
|
135
|
-
const
|
|
136
|
-
if (!
|
|
137
|
-
|
|
136
|
+
const answer2 = answers2.get(session.source);
|
|
137
|
+
if (!answer2 || answer2.sessions.has(session.id)) continue;
|
|
138
|
+
answer2.sessions.set(session.id, withoutWaitingText(session));
|
|
138
139
|
if (startedWaiting.has(session)) {
|
|
139
140
|
found.push({ event: "needs-you", session });
|
|
140
141
|
continue;
|
|
141
142
|
}
|
|
142
|
-
const before = memory.seen.get(session.source)?.get(
|
|
143
|
+
const before = memory.seen.get(session.source)?.get(answer2.basis)?.get(session.id);
|
|
143
144
|
if (before && before.status !== session.status && isOverStatus(session.status)) {
|
|
144
145
|
found.push({ event: session.status, session });
|
|
145
146
|
}
|
|
@@ -186,6 +187,7 @@ function sessionChanges(memory, snapshot) {
|
|
|
186
187
|
|
|
187
188
|
// src/core/api.ts
|
|
188
189
|
var CLEAR_HISTORY_ACTION = "clear-history";
|
|
190
|
+
var LAST_MESSAGE_PATH = "/api/sessions/last-message";
|
|
189
191
|
var TERMINAL_JUMP_TIMEOUT_MS = 6e4;
|
|
190
192
|
var JUMP_INTERVAL_MS = 1e3;
|
|
191
193
|
var STOP_ACTION = "stop";
|
|
@@ -217,6 +219,9 @@ var MAX_HOST_LENGTH = 253;
|
|
|
217
219
|
function isWebhookHost(host) {
|
|
218
220
|
return host.length <= MAX_HOST_LENGTH && WEBHOOK_HOST.test(host);
|
|
219
221
|
}
|
|
222
|
+
var PHONE_CHANNELS = ["ntfy", "pushover"];
|
|
223
|
+
var PHONE_TEST_PATH = "/api/phone/test";
|
|
224
|
+
var PHONE_TEST_ACTION = "phone-test";
|
|
220
225
|
|
|
221
226
|
// src/core/history.ts
|
|
222
227
|
var DEFAULT_HISTORY_WINDOW_MS = 15 * 60 * 1e3;
|
|
@@ -583,7 +588,7 @@ function waitBegan(statusSince, seenAt) {
|
|
|
583
588
|
|
|
584
589
|
// src/collector/outbound/outboundChannel.ts
|
|
585
590
|
function createOutboundChannel(options) {
|
|
586
|
-
const { afterMs, asking, waitMessage, overMessage, reminderMessage, summaryMessage, failure: failure2 } = options;
|
|
591
|
+
const { afterMs, asking, waitMessage: waitMessage2, overMessage: overMessage2, reminderMessage: reminderMessage2, summaryMessage: summaryMessage2, failure: failure2 } = options;
|
|
587
592
|
const now = options.now ?? Date.now;
|
|
588
593
|
const wanted = new Set(options.events);
|
|
589
594
|
let memory = EMPTY_CHANGE_MEMORY;
|
|
@@ -595,15 +600,26 @@ function createOutboundChannel(options) {
|
|
|
595
600
|
const hold = createQuietHold();
|
|
596
601
|
let last = null;
|
|
597
602
|
let queue = Promise.resolve();
|
|
598
|
-
function
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
() => {
|
|
604
|
-
|
|
605
|
-
|
|
603
|
+
function tryInTurn(content, after = () => {
|
|
604
|
+
}) {
|
|
605
|
+
const tried = queue.then(() => options.send(content)).then(
|
|
606
|
+
(outcome) => outcome,
|
|
607
|
+
// A sender should not reject. One that does is a failure like any other.
|
|
608
|
+
() => ({ sent: false, reason: failure2 })
|
|
609
|
+
).then((outcome) => {
|
|
610
|
+
after(outcome);
|
|
611
|
+
return outcome;
|
|
612
|
+
});
|
|
613
|
+
queue = tried.then(
|
|
614
|
+
() => void 0,
|
|
615
|
+
() => void 0
|
|
606
616
|
);
|
|
617
|
+
return tried;
|
|
618
|
+
}
|
|
619
|
+
function send2(content) {
|
|
620
|
+
void tryInTurn(content, (outcome) => {
|
|
621
|
+
last = outcome.sent ? { at: now(), sent: true } : { at: now(), sent: false, reason: outcome.reason };
|
|
622
|
+
});
|
|
607
623
|
}
|
|
608
624
|
return {
|
|
609
625
|
handle(snapshot) {
|
|
@@ -626,7 +642,7 @@ function createOutboundChannel(options) {
|
|
|
626
642
|
...item,
|
|
627
643
|
agent: agentName(item.session, snapshot.sources)
|
|
628
644
|
}));
|
|
629
|
-
ready.push((sentNow) =>
|
|
645
|
+
ready.push((sentNow) => summaryMessage2({ ...summary, items, now: sentNow }));
|
|
630
646
|
}
|
|
631
647
|
for (const { session, begunAt } of end.open) open2.set(session.id, begunAt);
|
|
632
648
|
}
|
|
@@ -644,7 +660,7 @@ function createOutboundChannel(options) {
|
|
|
644
660
|
agent: agentName(session, snapshot.sources),
|
|
645
661
|
seenAt: at
|
|
646
662
|
};
|
|
647
|
-
ready.push((sentNow) =>
|
|
663
|
+
ready.push((sentNow) => overMessage2({ ...facts, now: sentNow }));
|
|
648
664
|
continue;
|
|
649
665
|
}
|
|
650
666
|
const since = session.statusSince;
|
|
@@ -674,7 +690,7 @@ function createOutboundChannel(options) {
|
|
|
674
690
|
open2.delete(id);
|
|
675
691
|
sentAt.push(at);
|
|
676
692
|
send2(
|
|
677
|
-
|
|
693
|
+
waitMessage2({
|
|
678
694
|
session: withoutWaitingText(session),
|
|
679
695
|
agent: agentName(session, snapshot.sources),
|
|
680
696
|
// Read now, from this poll's snapshot, and only when it is to go.
|
|
@@ -695,7 +711,7 @@ function createOutboundChannel(options) {
|
|
|
695
711
|
sentAt.push(at);
|
|
696
712
|
reminders.told(due.session.id, at);
|
|
697
713
|
send2(
|
|
698
|
-
|
|
714
|
+
reminderMessage2({
|
|
699
715
|
session: withoutWaitingText(due.session),
|
|
700
716
|
agent: agentName(due.session, snapshot.sources),
|
|
701
717
|
asking: asking ? waitingText(due.session.waitingText) ?? null : null,
|
|
@@ -718,7 +734,15 @@ function createOutboundChannel(options) {
|
|
|
718
734
|
},
|
|
719
735
|
last: () => last,
|
|
720
736
|
limitedUntil: () => limitLiftsAt(sentAt, now()),
|
|
721
|
-
settled: () => queue
|
|
737
|
+
settled: () => queue,
|
|
738
|
+
async test(content) {
|
|
739
|
+
const at = now();
|
|
740
|
+
sentAt = sendsInLastHour(sentAt, at);
|
|
741
|
+
const limitedUntil = limitLiftsAt(sentAt, at);
|
|
742
|
+
if (limitedUntil !== null) return { tried: false, limitedUntil };
|
|
743
|
+
sentAt.push(at);
|
|
744
|
+
return { tried: true, at, outcome: await tryInTurn(content) };
|
|
745
|
+
}
|
|
722
746
|
};
|
|
723
747
|
}
|
|
724
748
|
|
|
@@ -1286,6 +1310,116 @@ function pullRequestsOffStatus(problem) {
|
|
|
1286
1310
|
return { on: false, problem, gh: null, last: null };
|
|
1287
1311
|
}
|
|
1288
1312
|
|
|
1313
|
+
// src/collector/outbound/phoneMessage.ts
|
|
1314
|
+
var MAX_PHONE_TITLE = 250;
|
|
1315
|
+
var MAX_PHONE_MESSAGE = 1024;
|
|
1316
|
+
function isWaitKind(kind) {
|
|
1317
|
+
return kind === "needs-you" || kind === "reminder" || kind === "test";
|
|
1318
|
+
}
|
|
1319
|
+
function cutTo(text4, max) {
|
|
1320
|
+
const characters = Array.from(text4);
|
|
1321
|
+
if (characters.length <= max) return text4;
|
|
1322
|
+
return `${characters.slice(0, max - 1).join("").trimEnd()}\u2026`;
|
|
1323
|
+
}
|
|
1324
|
+
function phoneMessage(kind, title, lines) {
|
|
1325
|
+
const said = lines.filter((line) => line !== "");
|
|
1326
|
+
return {
|
|
1327
|
+
kind,
|
|
1328
|
+
title: cutTo(title, MAX_PHONE_TITLE),
|
|
1329
|
+
message: cutTo(said.length > 0 ? said.join("\n") : title, MAX_PHONE_MESSAGE)
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
function factsLine(first, session, agent) {
|
|
1333
|
+
const facts = [
|
|
1334
|
+
first,
|
|
1335
|
+
session.project ? oneLine(session.project) : "",
|
|
1336
|
+
surfaceLabel(session.surface) ?? "",
|
|
1337
|
+
agent ? oneLine(agent) : ""
|
|
1338
|
+
];
|
|
1339
|
+
return facts.filter((fact) => fact !== "").join(" \xB7 ");
|
|
1340
|
+
}
|
|
1341
|
+
function askingLine(asking) {
|
|
1342
|
+
const said = waitingText(asking);
|
|
1343
|
+
return said ? `Asking: ${said}` : "";
|
|
1344
|
+
}
|
|
1345
|
+
function waitMessage(facts) {
|
|
1346
|
+
const { session, begunAt, now } = facts;
|
|
1347
|
+
return phoneMessage("needs-you", `${oneLine(sessionTitle(session))} ${waitingPhrase(session)}`, [
|
|
1348
|
+
askingLine(facts.asking),
|
|
1349
|
+
factsLine(formatDuration(Math.max(0, now - begunAt)), session, facts.agent)
|
|
1350
|
+
]);
|
|
1351
|
+
}
|
|
1352
|
+
function reminderMessage(facts) {
|
|
1353
|
+
const { session, begunAt, now } = facts;
|
|
1354
|
+
return phoneMessage("reminder", reminderSentence(session, now - begunAt), [
|
|
1355
|
+
askingLine(facts.asking),
|
|
1356
|
+
factsLine(`Since ${clockAt(begunAt, now)}`, session, facts.agent)
|
|
1357
|
+
]);
|
|
1358
|
+
}
|
|
1359
|
+
function summaryMessage(facts) {
|
|
1360
|
+
return phoneMessage("quiet-summary", WHILE_QUIET, [summaryLine(facts.items)]);
|
|
1361
|
+
}
|
|
1362
|
+
function overMessage(facts) {
|
|
1363
|
+
const { session, seenAt, now } = facts;
|
|
1364
|
+
return phoneMessage(facts.event, `${oneLine(sessionTitle(session))} ${overPhrase(facts.event)}`, [
|
|
1365
|
+
factsLine(`Seen at ${clockAt(seenAt, now)}`, session, facts.agent)
|
|
1366
|
+
]);
|
|
1367
|
+
}
|
|
1368
|
+
function testMessage() {
|
|
1369
|
+
return phoneMessage("test", "Agent Lookout test", [
|
|
1370
|
+
"Pushes from Agent Lookout reach this device."
|
|
1371
|
+
]);
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// src/collector/ntfy/ntfyNotifications.ts
|
|
1375
|
+
function ntfyOffStatus(problem) {
|
|
1376
|
+
return {
|
|
1377
|
+
on: false,
|
|
1378
|
+
host: null,
|
|
1379
|
+
tokenSet: null,
|
|
1380
|
+
events: null,
|
|
1381
|
+
afterMs: null,
|
|
1382
|
+
asking: null,
|
|
1383
|
+
problem,
|
|
1384
|
+
last: null,
|
|
1385
|
+
limitedUntil: null
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
function createNtfyNotifications(options) {
|
|
1389
|
+
const { settings, sender } = options;
|
|
1390
|
+
const channel = createOutboundChannel({
|
|
1391
|
+
events: settings.events,
|
|
1392
|
+
afterMs: settings.afterMs,
|
|
1393
|
+
asking: settings.asking,
|
|
1394
|
+
waitMessage,
|
|
1395
|
+
overMessage,
|
|
1396
|
+
reminderMessage,
|
|
1397
|
+
summaryMessage,
|
|
1398
|
+
send: (message) => sender.send(message),
|
|
1399
|
+
failure: "the push could not be sent",
|
|
1400
|
+
now: options.now
|
|
1401
|
+
});
|
|
1402
|
+
return {
|
|
1403
|
+
handle: (snapshot) => channel.handle(snapshot),
|
|
1404
|
+
status() {
|
|
1405
|
+
return {
|
|
1406
|
+
on: true,
|
|
1407
|
+
// The host alone. The topic is the secret anyone could read the pushes with.
|
|
1408
|
+
host: settings.server.hostname,
|
|
1409
|
+
tokenSet: settings.token !== null,
|
|
1410
|
+
events: [...settings.events],
|
|
1411
|
+
afterMs: settings.afterMs,
|
|
1412
|
+
asking: settings.asking,
|
|
1413
|
+
problem: null,
|
|
1414
|
+
last: channel.last(),
|
|
1415
|
+
limitedUntil: channel.limitedUntil()
|
|
1416
|
+
};
|
|
1417
|
+
},
|
|
1418
|
+
test: () => channel.test(testMessage()),
|
|
1419
|
+
settled: () => channel.settled()
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1289
1423
|
// src/core/sessions/diff.ts
|
|
1290
1424
|
function severityFor(to) {
|
|
1291
1425
|
if (to === "needs-you") return "warning";
|
|
@@ -1638,6 +1772,50 @@ function createPoller(options) {
|
|
|
1638
1772
|
};
|
|
1639
1773
|
}
|
|
1640
1774
|
|
|
1775
|
+
// src/collector/pushover/pushoverNotifications.ts
|
|
1776
|
+
function pushoverOffStatus(problem) {
|
|
1777
|
+
return {
|
|
1778
|
+
on: false,
|
|
1779
|
+
events: null,
|
|
1780
|
+
afterMs: null,
|
|
1781
|
+
asking: null,
|
|
1782
|
+
problem,
|
|
1783
|
+
last: null,
|
|
1784
|
+
limitedUntil: null
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1787
|
+
function createPushoverNotifications(options) {
|
|
1788
|
+
const { settings, sender } = options;
|
|
1789
|
+
const channel = createOutboundChannel({
|
|
1790
|
+
events: settings.events,
|
|
1791
|
+
afterMs: settings.afterMs,
|
|
1792
|
+
asking: settings.asking,
|
|
1793
|
+
waitMessage,
|
|
1794
|
+
overMessage,
|
|
1795
|
+
reminderMessage,
|
|
1796
|
+
summaryMessage,
|
|
1797
|
+
send: (message) => sender.send(message),
|
|
1798
|
+
failure: "the push could not be sent",
|
|
1799
|
+
now: options.now
|
|
1800
|
+
});
|
|
1801
|
+
return {
|
|
1802
|
+
handle: (snapshot) => channel.handle(snapshot),
|
|
1803
|
+
status() {
|
|
1804
|
+
return {
|
|
1805
|
+
on: true,
|
|
1806
|
+
events: [...settings.events],
|
|
1807
|
+
afterMs: settings.afterMs,
|
|
1808
|
+
asking: settings.asking,
|
|
1809
|
+
problem: null,
|
|
1810
|
+
last: channel.last(),
|
|
1811
|
+
limitedUntil: channel.limitedUntil()
|
|
1812
|
+
};
|
|
1813
|
+
},
|
|
1814
|
+
test: () => channel.test(testMessage()),
|
|
1815
|
+
settled: () => channel.settled()
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1641
1819
|
// src/collector/webhook/webhookMessage.ts
|
|
1642
1820
|
var MAX_SUMMARY_ITEMS = 100;
|
|
1643
1821
|
function slackSafe(text4) {
|
|
@@ -1891,6 +2069,13 @@ function send(res, status, body, headers = {}) {
|
|
|
1891
2069
|
function fail(res, status, error, headers = {}) {
|
|
1892
2070
|
send(res, status, { error }, headers);
|
|
1893
2071
|
}
|
|
2072
|
+
function sendWhenReady(res, answer2) {
|
|
2073
|
+
answer2.then((ready) => send(res, ready.status, ready.body, ready.headers)).catch(() => {
|
|
2074
|
+
if (res.headersSent) res.end();
|
|
2075
|
+
else fail(res, 500, "The collector ran into an unexpected problem.");
|
|
2076
|
+
}).catch(() => {
|
|
2077
|
+
});
|
|
2078
|
+
}
|
|
1894
2079
|
function notificationsSaid(req) {
|
|
1895
2080
|
const value = req.headers[NOTIFICATIONS_HEADER.toLowerCase()];
|
|
1896
2081
|
if (typeof value !== "string") return null;
|
|
@@ -1913,6 +2098,7 @@ function createApiHandler(options) {
|
|
|
1913
2098
|
if (options.answer) actions.set(ANSWER_PATH, options.answer);
|
|
1914
2099
|
if (options.timeRules) actions.set(TIME_RULES_PATH, options.timeRules);
|
|
1915
2100
|
if (options.permissionRules) actions.set(PERMISSION_RULES_PATH, options.permissionRules);
|
|
2101
|
+
if (options.phoneTest) actions.set(PHONE_TEST_PATH, options.phoneTest);
|
|
1916
2102
|
const serveSnapshot = options.serveSnapshot ?? ((snapshot) => snapshot);
|
|
1917
2103
|
function route(req, res) {
|
|
1918
2104
|
const refusal3 = refusalFor(req);
|
|
@@ -1928,11 +2114,7 @@ function createApiHandler(options) {
|
|
|
1928
2114
|
}
|
|
1929
2115
|
const action = url === null ? void 0 : actions.get(url.pathname);
|
|
1930
2116
|
if (action) {
|
|
1931
|
-
|
|
1932
|
-
if (res.headersSent) res.end();
|
|
1933
|
-
else fail(res, 500, "The collector ran into an unexpected problem.");
|
|
1934
|
-
}).catch(() => {
|
|
1935
|
-
});
|
|
2117
|
+
sendWhenReady(res, action(req));
|
|
1936
2118
|
return;
|
|
1937
2119
|
}
|
|
1938
2120
|
if (req.method !== "GET") {
|
|
@@ -1954,6 +2136,14 @@ function createApiHandler(options) {
|
|
|
1954
2136
|
send(res, 200, serveSnapshot(poller.getSnapshot()));
|
|
1955
2137
|
return;
|
|
1956
2138
|
}
|
|
2139
|
+
case LAST_MESSAGE_PATH: {
|
|
2140
|
+
if (options.lastMessage) {
|
|
2141
|
+
sendWhenReady(res, options.lastMessage(req, url));
|
|
2142
|
+
return;
|
|
2143
|
+
}
|
|
2144
|
+
fail(res, 404, "There is nothing at that address.");
|
|
2145
|
+
return;
|
|
2146
|
+
}
|
|
1957
2147
|
case "/api/events": {
|
|
1958
2148
|
const since = numberParam(url.searchParams.get("since"));
|
|
1959
2149
|
if (since === "invalid") {
|
|
@@ -1975,6 +2165,22 @@ function createApiHandler(options) {
|
|
|
1975
2165
|
);
|
|
1976
2166
|
return;
|
|
1977
2167
|
}
|
|
2168
|
+
case "/api/ntfy": {
|
|
2169
|
+
send(
|
|
2170
|
+
res,
|
|
2171
|
+
200,
|
|
2172
|
+
options.ntfy ? options.ntfy() : ntfyOffStatus(null)
|
|
2173
|
+
);
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
case "/api/pushover": {
|
|
2177
|
+
send(
|
|
2178
|
+
res,
|
|
2179
|
+
200,
|
|
2180
|
+
options.pushover ? options.pushover() : pushoverOffStatus(null)
|
|
2181
|
+
);
|
|
2182
|
+
return;
|
|
2183
|
+
}
|
|
1978
2184
|
case "/api/pull-requests": {
|
|
1979
2185
|
send(
|
|
1980
2186
|
res,
|
|
@@ -2023,7 +2229,7 @@ function createApiHandler(options) {
|
|
|
2023
2229
|
}
|
|
2024
2230
|
}
|
|
2025
2231
|
}
|
|
2026
|
-
function
|
|
2232
|
+
function answer2(req, res) {
|
|
2027
2233
|
try {
|
|
2028
2234
|
route(req, res);
|
|
2029
2235
|
} catch {
|
|
@@ -2034,12 +2240,12 @@ function createApiHandler(options) {
|
|
|
2034
2240
|
return (req, res) => {
|
|
2035
2241
|
const reading = options.ready?.() ?? null;
|
|
2036
2242
|
if (reading === null) {
|
|
2037
|
-
|
|
2243
|
+
answer2(req, res);
|
|
2038
2244
|
return;
|
|
2039
2245
|
}
|
|
2040
2246
|
void reading.then(
|
|
2041
|
-
() =>
|
|
2042
|
-
() =>
|
|
2247
|
+
() => answer2(req, res),
|
|
2248
|
+
() => answer2(req, res)
|
|
2043
2249
|
);
|
|
2044
2250
|
};
|
|
2045
2251
|
}
|
|
@@ -2258,7 +2464,7 @@ function urlFor(address) {
|
|
|
2258
2464
|
|
|
2259
2465
|
// src/collector/hosts/standalone.ts
|
|
2260
2466
|
import { existsSync } from "node:fs";
|
|
2261
|
-
import
|
|
2467
|
+
import path26 from "node:path";
|
|
2262
2468
|
|
|
2263
2469
|
// src/core/mapping/claudeCodeMapping.ts
|
|
2264
2470
|
var LIVE_STATUS = /* @__PURE__ */ new Map([
|
|
@@ -2645,6 +2851,16 @@ import os2 from "node:os";
|
|
|
2645
2851
|
// src/collector/adapters/claude-code/feed.ts
|
|
2646
2852
|
import { spawn } from "node:child_process";
|
|
2647
2853
|
|
|
2854
|
+
// src/collector/processes/childEnvironment.ts
|
|
2855
|
+
var OWN_SETTING = /^AGENT_LOOKOUT_/i;
|
|
2856
|
+
function childEnvironment(env) {
|
|
2857
|
+
const given = {};
|
|
2858
|
+
for (const [name, value] of Object.entries(env)) {
|
|
2859
|
+
if (!OWN_SETTING.test(name)) given[name] = value;
|
|
2860
|
+
}
|
|
2861
|
+
return given;
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2648
2864
|
// src/collector/processes/pids.ts
|
|
2649
2865
|
function validPid(value) {
|
|
2650
2866
|
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
|
|
@@ -2761,7 +2977,7 @@ var runProgram = (file, args, options) => new Promise((resolve) => {
|
|
|
2761
2977
|
}, options.timeoutMs);
|
|
2762
2978
|
try {
|
|
2763
2979
|
child = spawn(file, [...args], {
|
|
2764
|
-
env: options.env,
|
|
2980
|
+
env: childEnvironment(options.env),
|
|
2765
2981
|
shell: false,
|
|
2766
2982
|
// stdin is the null device, so a program that reads it gets end-of-file.
|
|
2767
2983
|
// stderr is not wanted and goes the same way.
|
|
@@ -3068,6 +3284,12 @@ function createStopTargets() {
|
|
|
3068
3284
|
};
|
|
3069
3285
|
}
|
|
3070
3286
|
|
|
3287
|
+
// src/collector/messages/lastMessageSettings.ts
|
|
3288
|
+
var LAST_MESSAGE_ENV = "AGENT_LOOKOUT_LAST_MESSAGE";
|
|
3289
|
+
function lastMessageOff(env) {
|
|
3290
|
+
return env[LAST_MESSAGE_ENV]?.trim().toLowerCase() === "off";
|
|
3291
|
+
}
|
|
3292
|
+
|
|
3071
3293
|
// src/collector/processes/ps.ts
|
|
3072
3294
|
import { execFile } from "node:child_process";
|
|
3073
3295
|
var PS_TIMEOUT_MS = 2e3;
|
|
@@ -3114,6 +3336,9 @@ function lstartMs(value) {
|
|
|
3114
3336
|
const [day, hours, minutes, seconds, year] = match.slice(2).map(Number);
|
|
3115
3337
|
return Date.UTC(year, month, day, hours, minutes, seconds);
|
|
3116
3338
|
}
|
|
3339
|
+
function processStartTime(value) {
|
|
3340
|
+
return lstartMs(collapse(value)) ?? null;
|
|
3341
|
+
}
|
|
3117
3342
|
function compareProcessStart(recorded, actual, withinMs = SAME_START_WITHIN_MS) {
|
|
3118
3343
|
if (recorded === void 0 || actual === void 0) return "unknown";
|
|
3119
3344
|
const before = collapse(recorded);
|
|
@@ -3138,8 +3363,8 @@ function parseProcessStarts(stdout) {
|
|
|
3138
3363
|
}
|
|
3139
3364
|
var readProcessStartsWithPs = async (pids) => {
|
|
3140
3365
|
if (pids.length === 0) return /* @__PURE__ */ new Map();
|
|
3141
|
-
const
|
|
3142
|
-
return parseProcessStarts(
|
|
3366
|
+
const answer2 = await runPs(psStartArgs(pids), { utc: true });
|
|
3367
|
+
return parseProcessStarts(answer2.stdout);
|
|
3143
3368
|
};
|
|
3144
3369
|
function createProcessStartCheck(read = readProcessStartsWithPs, now = Date.now, ttlMs = START_CHECK_TTL_MS) {
|
|
3145
3370
|
const remembered = /* @__PURE__ */ new Map();
|
|
@@ -3480,13 +3705,13 @@ function uniqueById(sessions) {
|
|
|
3480
3705
|
}
|
|
3481
3706
|
|
|
3482
3707
|
// src/collector/adapters/claude-code/stopOffers.ts
|
|
3483
|
-
function jobIdOf(entry,
|
|
3484
|
-
const listed =
|
|
3708
|
+
function jobIdOf(entry, answer2) {
|
|
3709
|
+
const listed = answer2.find(
|
|
3485
3710
|
(candidate) => candidate.id !== void 0 && (candidate.sessionId === entry.sessionId || candidate.sessionId === void 0 && candidate.pid === entry.pid)
|
|
3486
3711
|
);
|
|
3487
3712
|
return listed?.id !== void 0 && JOB_ID.test(listed.id) ? listed.id : void 0;
|
|
3488
3713
|
}
|
|
3489
|
-
function targetFor(entry,
|
|
3714
|
+
function targetFor(entry, answer2, options) {
|
|
3490
3715
|
if (entry.sessionId === void 0 || entry.procStart === void 0) return null;
|
|
3491
3716
|
const surface = mapClaudeCodeSurface(entry.entrypoint);
|
|
3492
3717
|
const found = {
|
|
@@ -3499,12 +3724,12 @@ function targetFor(entry, answer, options) {
|
|
|
3499
3724
|
return surface === "terminal" || surface === "vscode" ? { ...found, how: "signal" } : null;
|
|
3500
3725
|
}
|
|
3501
3726
|
if (entry.kind === "bg" && options.background && surface !== "desktop") {
|
|
3502
|
-
const jobId =
|
|
3727
|
+
const jobId = answer2 === null ? void 0 : jobIdOf(entry, answer2);
|
|
3503
3728
|
return jobId === void 0 ? null : { ...found, how: "background", jobId };
|
|
3504
3729
|
}
|
|
3505
3730
|
return null;
|
|
3506
3731
|
}
|
|
3507
|
-
function withStopOffers(sessions, live2,
|
|
3732
|
+
function withStopOffers(sessions, live2, answer2, options) {
|
|
3508
3733
|
const byId = /* @__PURE__ */ new Map();
|
|
3509
3734
|
for (const entry of live2) {
|
|
3510
3735
|
if (entry.sessionId !== void 0) byId.set(`${SOURCE_ID}:${entry.sessionId}`, entry);
|
|
@@ -3515,7 +3740,7 @@ function withStopOffers(sessions, live2, answer, options) {
|
|
|
3515
3740
|
if (session.status === "finished" || session.status === "failed") return session;
|
|
3516
3741
|
const entry = byId.get(session.id);
|
|
3517
3742
|
if (entry === void 0 || entry.pid !== session.pid) return session;
|
|
3518
|
-
const target = targetFor(entry,
|
|
3743
|
+
const target = targetFor(entry, answer2, options);
|
|
3519
3744
|
if (target === null) return session;
|
|
3520
3745
|
targets.set(session.id, target);
|
|
3521
3746
|
return { ...session, stop: { how: target.how } };
|
|
@@ -3523,8 +3748,8 @@ function withStopOffers(sessions, live2, answer, options) {
|
|
|
3523
3748
|
return { sessions: offered, targets };
|
|
3524
3749
|
}
|
|
3525
3750
|
|
|
3526
|
-
// src/collector/adapters/claude-code/transcript/
|
|
3527
|
-
import
|
|
3751
|
+
// src/collector/adapters/claude-code/transcript/lastMessages.ts
|
|
3752
|
+
import path9 from "node:path";
|
|
3528
3753
|
|
|
3529
3754
|
// src/collector/files/readOnlyIo.ts
|
|
3530
3755
|
import { constants as constants4 } from "node:fs";
|
|
@@ -3578,7 +3803,7 @@ function isMissing2(error) {
|
|
|
3578
3803
|
return code === "ENOENT" || code === "ENOTDIR";
|
|
3579
3804
|
}
|
|
3580
3805
|
|
|
3581
|
-
// src/collector/adapters/claude-code/transcript/
|
|
3806
|
+
// src/collector/adapters/claude-code/transcript/transcriptLines.ts
|
|
3582
3807
|
function isRecord3(value) {
|
|
3583
3808
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3584
3809
|
}
|
|
@@ -3604,6 +3829,112 @@ function linesOf(tail, fromStart) {
|
|
|
3604
3829
|
}
|
|
3605
3830
|
return lines;
|
|
3606
3831
|
}
|
|
3832
|
+
|
|
3833
|
+
// src/collector/adapters/claude-code/transcript/lastSaid.ts
|
|
3834
|
+
var PART_BREAK = "\n\n";
|
|
3835
|
+
function textsOf(line) {
|
|
3836
|
+
if (line.type !== "assistant" || line.isSidechain === true || line.isMeta === true) return [];
|
|
3837
|
+
const texts = [];
|
|
3838
|
+
for (const block of blocksOf(line)) {
|
|
3839
|
+
const text4 = block.type === "text" ? nonEmpty(block.text) : null;
|
|
3840
|
+
if (text4 !== null) texts.push(text4);
|
|
3841
|
+
}
|
|
3842
|
+
return texts;
|
|
3843
|
+
}
|
|
3844
|
+
function messageIdOf(line) {
|
|
3845
|
+
return isRecord3(line.message) ? nonEmpty(line.message.id) : null;
|
|
3846
|
+
}
|
|
3847
|
+
function lastSaidInTail(tail, fromStart) {
|
|
3848
|
+
const lines = linesOf(tail, fromStart);
|
|
3849
|
+
const newest2 = lines.findLastIndex((line) => textsOf(line).length > 0);
|
|
3850
|
+
if (newest2 === -1) return { text: null, reason: fromStart ? "nothing-yet" : "too-far-back" };
|
|
3851
|
+
const parts = [textsOf(lines[newest2])];
|
|
3852
|
+
const messageId = messageIdOf(lines[newest2]);
|
|
3853
|
+
if (messageId === null) return { text: parts[0].join(PART_BREAK) };
|
|
3854
|
+
let index = newest2 - 1;
|
|
3855
|
+
for (; index >= 0; index -= 1) {
|
|
3856
|
+
const line = lines[index];
|
|
3857
|
+
if (line.isSidechain === true) continue;
|
|
3858
|
+
if (line.type === "user") break;
|
|
3859
|
+
if (line.type !== "assistant") continue;
|
|
3860
|
+
if (messageIdOf(line) !== messageId) break;
|
|
3861
|
+
parts.unshift(textsOf(line));
|
|
3862
|
+
}
|
|
3863
|
+
const text4 = parts.flat().join(PART_BREAK);
|
|
3864
|
+
return index < 0 && !fromStart ? { text: text4, startCut: true } : { text: text4 };
|
|
3865
|
+
}
|
|
3866
|
+
|
|
3867
|
+
// src/collector/adapters/claude-code/transcript/transcriptFile.ts
|
|
3868
|
+
import path7 from "node:path";
|
|
3869
|
+
var TRANSCRIPT_TAIL_BYTES = 256 * 1024;
|
|
3870
|
+
var SESSION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3871
|
+
function isSessionId(value) {
|
|
3872
|
+
return typeof value === "string" && SESSION_ID.test(value);
|
|
3873
|
+
}
|
|
3874
|
+
var ID_PREFIX = "claude-code:";
|
|
3875
|
+
function sessionIdOf(session) {
|
|
3876
|
+
if (session.source !== "claude-code" || !session.id.startsWith(ID_PREFIX)) return null;
|
|
3877
|
+
const id = session.id.slice(ID_PREFIX.length);
|
|
3878
|
+
return isSessionId(id) ? id : null;
|
|
3879
|
+
}
|
|
3880
|
+
function projectFolderName(cwd) {
|
|
3881
|
+
return cwd.replace(/[^A-Za-z0-9]/g, "-");
|
|
3882
|
+
}
|
|
3883
|
+
function fileName(sessionId) {
|
|
3884
|
+
return `${sessionId}.jsonl`;
|
|
3885
|
+
}
|
|
3886
|
+
async function findTranscript(projectsDir, sessionId, cwd, io) {
|
|
3887
|
+
if (!isSessionId(sessionId)) return null;
|
|
3888
|
+
const name = fileName(sessionId);
|
|
3889
|
+
const isFile2 = async (file) => {
|
|
3890
|
+
try {
|
|
3891
|
+
return (await io.lstat(file)).kind === "file";
|
|
3892
|
+
} catch {
|
|
3893
|
+
return false;
|
|
3894
|
+
}
|
|
3895
|
+
};
|
|
3896
|
+
if (cwd !== null && cwd !== "") {
|
|
3897
|
+
const likely = path7.join(projectsDir, projectFolderName(cwd), name);
|
|
3898
|
+
if (await isFile2(likely)) return likely;
|
|
3899
|
+
}
|
|
3900
|
+
let folders;
|
|
3901
|
+
try {
|
|
3902
|
+
folders = await io.readdir(projectsDir);
|
|
3903
|
+
} catch {
|
|
3904
|
+
return null;
|
|
3905
|
+
}
|
|
3906
|
+
const candidates = folders.filter((folder) => folder !== "" && !folder.startsWith(".")).sort().map((folder) => path7.join(projectsDir, folder, name));
|
|
3907
|
+
const found = await Promise.all(candidates.map(isFile2));
|
|
3908
|
+
return candidates.find((_, index) => found[index]) ?? null;
|
|
3909
|
+
}
|
|
3910
|
+
function stampOf(info) {
|
|
3911
|
+
return { size: info.size, mtimeMs: info.mtimeMs, ino: info.ino };
|
|
3912
|
+
}
|
|
3913
|
+
function sameStamp(a, b) {
|
|
3914
|
+
return a.size === b.size && a.mtimeMs === b.mtimeMs && a.ino === b.ino;
|
|
3915
|
+
}
|
|
3916
|
+
async function readTranscriptTail(file, io, known, maxBytes = TRANSCRIPT_TAIL_BYTES) {
|
|
3917
|
+
const open2 = await io.openRegular(file);
|
|
3918
|
+
try {
|
|
3919
|
+
const stamp = stampOf(open2.info);
|
|
3920
|
+
if (known !== null && sameStamp(known, stamp)) return { unchanged: true, stamp };
|
|
3921
|
+
const start = Math.max(0, stamp.size - maxBytes);
|
|
3922
|
+
const bytes3 = await open2.read(start, stamp.size - start);
|
|
3923
|
+
return {
|
|
3924
|
+
unchanged: false,
|
|
3925
|
+
stamp,
|
|
3926
|
+
tail: new TextDecoder("utf-8").decode(bytes3),
|
|
3927
|
+
fromStart: start === 0
|
|
3928
|
+
};
|
|
3929
|
+
} finally {
|
|
3930
|
+
await open2.close();
|
|
3931
|
+
}
|
|
3932
|
+
}
|
|
3933
|
+
|
|
3934
|
+
// src/collector/adapters/claude-code/transcript/waitingTexts.ts
|
|
3935
|
+
import path8 from "node:path";
|
|
3936
|
+
|
|
3937
|
+
// src/collector/adapters/claude-code/transcript/lastAsk.ts
|
|
3607
3938
|
function isPrompt(line) {
|
|
3608
3939
|
if (line.isMeta === true) return false;
|
|
3609
3940
|
const message = isRecord3(line.message) ? line.message : null;
|
|
@@ -3702,91 +4033,24 @@ function askedInTail(tail, fromStart, cwd) {
|
|
|
3702
4033
|
return waitingText(describeToolUse(pending.name, pending.input, cwd));
|
|
3703
4034
|
}
|
|
3704
4035
|
|
|
3705
|
-
// src/collector/adapters/claude-code/transcript/
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
function isSessionId(value) {
|
|
3710
|
-
return typeof value === "string" && SESSION_ID.test(value);
|
|
3711
|
-
}
|
|
3712
|
-
function projectFolderName(cwd) {
|
|
3713
|
-
return cwd.replace(/[^A-Za-z0-9]/g, "-");
|
|
3714
|
-
}
|
|
3715
|
-
function fileName(sessionId) {
|
|
3716
|
-
return `${sessionId}.jsonl`;
|
|
4036
|
+
// src/collector/adapters/claude-code/transcript/waitingTexts.ts
|
|
4037
|
+
var WAITING_TEXT_ENV = "AGENT_LOOKOUT_WAITING_TEXT";
|
|
4038
|
+
function waitingTextOff(env) {
|
|
4039
|
+
return env[WAITING_TEXT_ENV]?.trim().toLowerCase() === "off";
|
|
3717
4040
|
}
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
const
|
|
3721
|
-
const
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
}
|
|
3732
|
-
let folders;
|
|
3733
|
-
try {
|
|
3734
|
-
folders = await io.readdir(projectsDir);
|
|
3735
|
-
} catch {
|
|
3736
|
-
return null;
|
|
3737
|
-
}
|
|
3738
|
-
const candidates = folders.filter((folder) => folder !== "" && !folder.startsWith(".")).sort().map((folder) => path7.join(projectsDir, folder, name));
|
|
3739
|
-
const found = await Promise.all(candidates.map(isFile2));
|
|
3740
|
-
return candidates.find((_, index) => found[index]) ?? null;
|
|
3741
|
-
}
|
|
3742
|
-
function stampOf(info) {
|
|
3743
|
-
return { size: info.size, mtimeMs: info.mtimeMs, ino: info.ino };
|
|
3744
|
-
}
|
|
3745
|
-
function sameStamp(a, b) {
|
|
3746
|
-
return a.size === b.size && a.mtimeMs === b.mtimeMs && a.ino === b.ino;
|
|
3747
|
-
}
|
|
3748
|
-
async function readTranscriptTail(file, io, known, maxBytes = TRANSCRIPT_TAIL_BYTES) {
|
|
3749
|
-
const open2 = await io.openRegular(file);
|
|
3750
|
-
try {
|
|
3751
|
-
const stamp = stampOf(open2.info);
|
|
3752
|
-
if (known !== null && sameStamp(known, stamp)) return { unchanged: true, stamp };
|
|
3753
|
-
const start = Math.max(0, stamp.size - maxBytes);
|
|
3754
|
-
const bytes2 = await open2.read(start, stamp.size - start);
|
|
3755
|
-
return {
|
|
3756
|
-
unchanged: false,
|
|
3757
|
-
stamp,
|
|
3758
|
-
tail: new TextDecoder("utf-8").decode(bytes2),
|
|
3759
|
-
fromStart: start === 0
|
|
3760
|
-
};
|
|
3761
|
-
} finally {
|
|
3762
|
-
await open2.close();
|
|
3763
|
-
}
|
|
3764
|
-
}
|
|
3765
|
-
|
|
3766
|
-
// src/collector/adapters/claude-code/transcript/waitingTexts.ts
|
|
3767
|
-
var WAITING_TEXT_ENV = "AGENT_LOOKOUT_WAITING_TEXT";
|
|
3768
|
-
function waitingTextOff(env) {
|
|
3769
|
-
return env[WAITING_TEXT_ENV]?.trim().toLowerCase() === "off";
|
|
3770
|
-
}
|
|
3771
|
-
var LOOK_AGAIN_MS = 1e4;
|
|
3772
|
-
var ID_PREFIX = "claude-code:";
|
|
3773
|
-
function sessionIdOf(session) {
|
|
3774
|
-
if (session.source !== "claude-code" || !session.id.startsWith(ID_PREFIX)) return null;
|
|
3775
|
-
const id = session.id.slice(ID_PREFIX.length);
|
|
3776
|
-
return isSessionId(id) ? id : null;
|
|
3777
|
-
}
|
|
3778
|
-
function createWaitingTextReader(options) {
|
|
3779
|
-
const io = options.io ?? nodeIo2;
|
|
3780
|
-
const now = options.now ?? Date.now;
|
|
3781
|
-
const projectsDir = path8.join(options.claudeHome, "projects");
|
|
3782
|
-
const kept = /* @__PURE__ */ new Map();
|
|
3783
|
-
async function textFor(session, sessionId) {
|
|
3784
|
-
const at = now();
|
|
3785
|
-
let wait = kept.get(session.id);
|
|
3786
|
-
if (wait === void 0 || wait.statusSince !== session.statusSince || wait.file === null && at - wait.lookedAt >= LOOK_AGAIN_MS) {
|
|
3787
|
-
const file = await findTranscript(projectsDir, sessionId, session.cwd, io);
|
|
3788
|
-
wait = { statusSince: session.statusSince, file, lookedAt: at, stamp: null, text: void 0 };
|
|
3789
|
-
kept.set(session.id, wait);
|
|
4041
|
+
var LOOK_AGAIN_MS = 1e4;
|
|
4042
|
+
function createWaitingTextReader(options) {
|
|
4043
|
+
const io = options.io ?? nodeIo2;
|
|
4044
|
+
const now = options.now ?? Date.now;
|
|
4045
|
+
const projectsDir = path8.join(options.claudeHome, "projects");
|
|
4046
|
+
const kept = /* @__PURE__ */ new Map();
|
|
4047
|
+
async function textFor(session, sessionId) {
|
|
4048
|
+
const at = now();
|
|
4049
|
+
let wait = kept.get(session.id);
|
|
4050
|
+
if (wait === void 0 || wait.statusSince !== session.statusSince || wait.file === null && at - wait.lookedAt >= LOOK_AGAIN_MS) {
|
|
4051
|
+
const file = await findTranscript(projectsDir, sessionId, session.cwd, io);
|
|
4052
|
+
wait = { statusSince: session.statusSince, file, lookedAt: at, stamp: null, text: void 0 };
|
|
4053
|
+
kept.set(session.id, wait);
|
|
3790
4054
|
}
|
|
3791
4055
|
if (wait.file === null) return void 0;
|
|
3792
4056
|
try {
|
|
@@ -3832,6 +4096,160 @@ function createWaitingTextReader(options) {
|
|
|
3832
4096
|
};
|
|
3833
4097
|
}
|
|
3834
4098
|
|
|
4099
|
+
// src/collector/adapters/claude-code/transcript/lastMessages.ts
|
|
4100
|
+
var ANSWER_AGAIN_MS = 1e3;
|
|
4101
|
+
var READS_PER_SECOND = 4;
|
|
4102
|
+
var MAX_KEPT = 8;
|
|
4103
|
+
var KEEP_MS = 15e3;
|
|
4104
|
+
var NOT_FOUND = { message: null, reason: "not-found" };
|
|
4105
|
+
var UNREADABLE = { message: null, reason: "unreadable" };
|
|
4106
|
+
function runLater(run, ms) {
|
|
4107
|
+
setTimeout(run, ms).unref();
|
|
4108
|
+
}
|
|
4109
|
+
function answerFrom(tail, fromStart) {
|
|
4110
|
+
const said = lastSaidInTail(tail, fromStart);
|
|
4111
|
+
if (said.text === null) return { message: null, reason: said.reason };
|
|
4112
|
+
const shown = messageText(said.text);
|
|
4113
|
+
if (shown === void 0) {
|
|
4114
|
+
return { message: null, reason: fromStart ? "nothing-yet" : "too-far-back" };
|
|
4115
|
+
}
|
|
4116
|
+
return { message: { text: shown.text, cut: shown.cut || said.startCut === true } };
|
|
4117
|
+
}
|
|
4118
|
+
function createLastMessageReader(options) {
|
|
4119
|
+
const io = options.io ?? nodeIo2;
|
|
4120
|
+
const now = options.now ?? Date.now;
|
|
4121
|
+
const later = options.later ?? runLater;
|
|
4122
|
+
const projectsDir = path9.join(options.claudeHome, "projects");
|
|
4123
|
+
const listed = /* @__PURE__ */ new Map();
|
|
4124
|
+
const kept = /* @__PURE__ */ new Map();
|
|
4125
|
+
let reads = [];
|
|
4126
|
+
let dropSet = false;
|
|
4127
|
+
function dropOld(at) {
|
|
4128
|
+
for (const [id, entry] of kept) {
|
|
4129
|
+
if (at - entry.askedAt >= KEEP_MS) kept.delete(id);
|
|
4130
|
+
}
|
|
4131
|
+
}
|
|
4132
|
+
function dropOnTime(at) {
|
|
4133
|
+
if (dropSet || kept.size === 0) return;
|
|
4134
|
+
const oldest = Math.min(...[...kept.values()].map((entry) => entry.askedAt));
|
|
4135
|
+
dropSet = true;
|
|
4136
|
+
later(
|
|
4137
|
+
() => {
|
|
4138
|
+
dropSet = false;
|
|
4139
|
+
const then = now();
|
|
4140
|
+
dropOld(then);
|
|
4141
|
+
dropOnTime(then);
|
|
4142
|
+
},
|
|
4143
|
+
Math.max(0, oldest + KEEP_MS - at)
|
|
4144
|
+
);
|
|
4145
|
+
}
|
|
4146
|
+
function mayRead(at) {
|
|
4147
|
+
reads = reads.filter((started) => at - started < 1e3 && started <= at);
|
|
4148
|
+
if (reads.length >= READS_PER_SECOND) return false;
|
|
4149
|
+
reads.push(at);
|
|
4150
|
+
return true;
|
|
4151
|
+
}
|
|
4152
|
+
function makeRoom() {
|
|
4153
|
+
while (kept.size >= MAX_KEPT) {
|
|
4154
|
+
let leastRecent;
|
|
4155
|
+
for (const pair of kept) {
|
|
4156
|
+
if (leastRecent === void 0 || pair[1].askedAt < leastRecent[1].askedAt) {
|
|
4157
|
+
leastRecent = pair;
|
|
4158
|
+
}
|
|
4159
|
+
}
|
|
4160
|
+
if (leastRecent === void 0) return;
|
|
4161
|
+
kept.delete(leastRecent[0]);
|
|
4162
|
+
}
|
|
4163
|
+
}
|
|
4164
|
+
async function check(entry, target, at) {
|
|
4165
|
+
try {
|
|
4166
|
+
if (entry.file === null) {
|
|
4167
|
+
entry.lookedAt = at;
|
|
4168
|
+
entry.file = await findTranscript(projectsDir, target.sessionId, target.cwd, io);
|
|
4169
|
+
if (entry.file === null) return NOT_FOUND;
|
|
4170
|
+
}
|
|
4171
|
+
const read = await readTranscriptTail(entry.file, io, entry.last?.stamp ?? null);
|
|
4172
|
+
if (read.unchanged) return entry.last?.answer ?? UNREADABLE;
|
|
4173
|
+
const answer2 = answerFrom(read.tail, read.fromStart);
|
|
4174
|
+
entry.last = { stamp: read.stamp, answer: answer2 };
|
|
4175
|
+
return answer2;
|
|
4176
|
+
} catch (error) {
|
|
4177
|
+
entry.last = null;
|
|
4178
|
+
if (isMissing2(error)) {
|
|
4179
|
+
entry.file = null;
|
|
4180
|
+
entry.lookedAt = at;
|
|
4181
|
+
return NOT_FOUND;
|
|
4182
|
+
}
|
|
4183
|
+
return UNREADABLE;
|
|
4184
|
+
}
|
|
4185
|
+
}
|
|
4186
|
+
return {
|
|
4187
|
+
read(id) {
|
|
4188
|
+
const target = listed.get(id);
|
|
4189
|
+
if (target === void 0) return Promise.resolve(null);
|
|
4190
|
+
if (target === null) return Promise.resolve(NOT_FOUND);
|
|
4191
|
+
const at = now();
|
|
4192
|
+
dropOld(at);
|
|
4193
|
+
const entry = kept.get(id);
|
|
4194
|
+
if (entry !== void 0) {
|
|
4195
|
+
entry.askedAt = at;
|
|
4196
|
+
dropOnTime(at);
|
|
4197
|
+
if (entry.checking !== null) return entry.checking;
|
|
4198
|
+
if (entry.answer !== null && at - entry.checkedAt < ANSWER_AGAIN_MS) {
|
|
4199
|
+
return Promise.resolve(entry.answer);
|
|
4200
|
+
}
|
|
4201
|
+
if (entry.answer !== null && entry.file === null && entry.lookedAt !== null && at - entry.lookedAt < LOOK_AGAIN_MS) {
|
|
4202
|
+
return Promise.resolve(entry.answer);
|
|
4203
|
+
}
|
|
4204
|
+
}
|
|
4205
|
+
if (!mayRead(at)) return Promise.resolve("busy");
|
|
4206
|
+
let fresh = entry;
|
|
4207
|
+
if (fresh === void 0) {
|
|
4208
|
+
makeRoom();
|
|
4209
|
+
fresh = {
|
|
4210
|
+
file: null,
|
|
4211
|
+
lookedAt: null,
|
|
4212
|
+
last: null,
|
|
4213
|
+
answer: null,
|
|
4214
|
+
checkedAt: at,
|
|
4215
|
+
askedAt: at,
|
|
4216
|
+
checking: null
|
|
4217
|
+
};
|
|
4218
|
+
kept.set(id, fresh);
|
|
4219
|
+
dropOnTime(at);
|
|
4220
|
+
}
|
|
4221
|
+
const reading = fresh;
|
|
4222
|
+
const checking = check(reading, target, at).then((answer2) => {
|
|
4223
|
+
reading.answer = answer2;
|
|
4224
|
+
reading.checkedAt = at;
|
|
4225
|
+
reading.checking = null;
|
|
4226
|
+
return answer2;
|
|
4227
|
+
});
|
|
4228
|
+
reading.checking = checking;
|
|
4229
|
+
return checking;
|
|
4230
|
+
},
|
|
4231
|
+
keep(sessions) {
|
|
4232
|
+
listed.clear();
|
|
4233
|
+
for (const session of sessions) {
|
|
4234
|
+
const sessionId = sessionIdOf(session);
|
|
4235
|
+
listed.set(session.id, sessionId === null ? null : { sessionId, cwd: session.cwd });
|
|
4236
|
+
}
|
|
4237
|
+
for (const id of kept.keys()) {
|
|
4238
|
+
if (!listed.has(id)) kept.delete(id);
|
|
4239
|
+
}
|
|
4240
|
+
dropOld(now());
|
|
4241
|
+
},
|
|
4242
|
+
forget() {
|
|
4243
|
+
listed.clear();
|
|
4244
|
+
kept.clear();
|
|
4245
|
+
reads = [];
|
|
4246
|
+
},
|
|
4247
|
+
get size() {
|
|
4248
|
+
return kept.size;
|
|
4249
|
+
}
|
|
4250
|
+
};
|
|
4251
|
+
}
|
|
4252
|
+
|
|
3835
4253
|
// src/collector/adapters/claude-code/index.ts
|
|
3836
4254
|
var CLAUDE_HOME_ENV = "AGENT_LOOKOUT_CLAUDE_HOME";
|
|
3837
4255
|
var CLAUDE_FEED_ENV = "AGENT_LOOKOUT_CLAUDE_FEED";
|
|
@@ -3932,6 +4350,8 @@ function createClaudeCodeAdapter(options = {}) {
|
|
|
3932
4350
|
const homeName = tildify(claudeHome, homeDir, platform);
|
|
3933
4351
|
const registryName = tildify(sessionsDir, homeDir, platform);
|
|
3934
4352
|
const waitingTexts = transcriptsOff ? null : createWaitingTextReader({ claudeHome, io: options.transcriptIo, now });
|
|
4353
|
+
const lastMessagesOff = lastMessageOff(env);
|
|
4354
|
+
const lastMessages = transcriptsOff || lastMessagesOff ? null : createLastMessageReader({ claudeHome, io: options.transcriptIo, now });
|
|
3935
4355
|
let lastDue = null;
|
|
3936
4356
|
let lastRead = null;
|
|
3937
4357
|
let feedSoon = false;
|
|
@@ -3953,7 +4373,7 @@ function createClaudeCodeAdapter(options = {}) {
|
|
|
3953
4373
|
{ label: "Command run", value: commandRun },
|
|
3954
4374
|
{
|
|
3955
4375
|
label: "Transcript read",
|
|
3956
|
-
value: transcriptsOff ? "Off" : "last message of a waiting session"
|
|
4376
|
+
value: transcriptsOff ? "Off" : lastMessagesOff ? "last message of a waiting session" : "last message of a waiting session, and of one whose details are open"
|
|
3957
4377
|
}
|
|
3958
4378
|
];
|
|
3959
4379
|
}
|
|
@@ -4049,12 +4469,12 @@ function createClaudeCodeAdapter(options = {}) {
|
|
|
4049
4469
|
}
|
|
4050
4470
|
reading ??= await readRegistryNow(checkedAt);
|
|
4051
4471
|
const read = lastRead;
|
|
4052
|
-
const
|
|
4472
|
+
const answer2 = read?.feed.ok ? read.feed.entries : null;
|
|
4053
4473
|
const problem = problemWith(reading, read);
|
|
4054
4474
|
const { registry, live: live2 } = reading;
|
|
4055
4475
|
const offered = (result) => {
|
|
4056
4476
|
if (!stops) return result;
|
|
4057
|
-
const found = withStopOffers(result.sessions, live2,
|
|
4477
|
+
const found = withStopOffers(result.sessions, live2, answer2, {
|
|
4058
4478
|
sessionsDir,
|
|
4059
4479
|
background: !neverRun
|
|
4060
4480
|
});
|
|
@@ -4063,7 +4483,7 @@ function createClaudeCodeAdapter(options = {}) {
|
|
|
4063
4483
|
};
|
|
4064
4484
|
if (options.panes || options.terminals) {
|
|
4065
4485
|
const pids = new Set(live2.map((entry) => entry.pid));
|
|
4066
|
-
for (const entry of
|
|
4486
|
+
for (const entry of answer2 ?? []) {
|
|
4067
4487
|
if (entry.pid !== void 0 && isAlive(entry.pid)) pids.add(entry.pid);
|
|
4068
4488
|
}
|
|
4069
4489
|
await Promise.all([options.panes?.look([...pids]), options.terminals?.look([...pids])]);
|
|
@@ -4083,16 +4503,16 @@ function createClaudeCodeAdapter(options = {}) {
|
|
|
4083
4503
|
...advice2 !== void 0 && { advice: advice2 },
|
|
4084
4504
|
checkedAt
|
|
4085
4505
|
});
|
|
4086
|
-
if (
|
|
4087
|
-
const listedPids = new Set(
|
|
4088
|
-
const listedSessionIds = new Set(
|
|
4506
|
+
if (answer2 !== null && read !== null && problem === null) {
|
|
4507
|
+
const listedPids = new Set(answer2.map((entry) => entry.pid));
|
|
4508
|
+
const listedSessionIds = new Set(answer2.map((entry) => entry.sessionId));
|
|
4089
4509
|
const tooNewSince = read.askedAt - NEW_SESSION_GRACE_MS;
|
|
4090
4510
|
const fromRegistry = live2.filter(
|
|
4091
4511
|
(entry) => listedPids.has(entry.pid) || listedSessionIds.has(entry.sessionId) || startedAfter(entry, tooNewSince, checkedAt)
|
|
4092
4512
|
);
|
|
4093
4513
|
const livePids = new Set(live2.map((entry) => entry.pid));
|
|
4094
4514
|
const liveSessionIds = new Set(live2.map((entry) => entry.sessionId));
|
|
4095
|
-
const jobs =
|
|
4515
|
+
const jobs = answer2.filter(
|
|
4096
4516
|
(entry) => (entry.id !== void 0 || entry.state !== void 0) && !(entry.pid !== void 0 && livePids.has(entry.pid)) && !(entry.sessionId !== void 0 && liveSessionIds.has(entry.sessionId))
|
|
4097
4517
|
).map((entry) => {
|
|
4098
4518
|
const gone = entry.pid !== void 0 && !isAlive(entry.pid);
|
|
@@ -4113,9 +4533,9 @@ function createClaudeCodeAdapter(options = {}) {
|
|
|
4113
4533
|
basis: "registry+feed"
|
|
4114
4534
|
});
|
|
4115
4535
|
}
|
|
4116
|
-
if (
|
|
4536
|
+
if (answer2 !== null && problem !== null) {
|
|
4117
4537
|
const entries = registry.readable ? registry.entries : void 0;
|
|
4118
|
-
const sessions =
|
|
4538
|
+
const sessions = answer2.map(
|
|
4119
4539
|
(entry) => sessionFromFeed(
|
|
4120
4540
|
entry,
|
|
4121
4541
|
entry.pid !== void 0 ? entries?.get(entry.pid) : void 0,
|
|
@@ -4169,9 +4589,12 @@ function createClaudeCodeAdapter(options = {}) {
|
|
|
4169
4589
|
lookingIn: neverRun ? `Looking for sessions in ${registryName}.` : `Looking for sessions in ${registryName} and with claude ${FEED_ARGS.join(" ")}.`,
|
|
4170
4590
|
async poll() {
|
|
4171
4591
|
try {
|
|
4172
|
-
|
|
4592
|
+
const result = await withWaitingText(await poll());
|
|
4593
|
+
lastMessages?.keep(result.sessions);
|
|
4594
|
+
return result;
|
|
4173
4595
|
} catch {
|
|
4174
4596
|
await waitingTexts?.annotate([]);
|
|
4597
|
+
lastMessages?.forget();
|
|
4175
4598
|
stops?.set(/* @__PURE__ */ new Map());
|
|
4176
4599
|
return {
|
|
4177
4600
|
health: {
|
|
@@ -4189,6 +4612,19 @@ function createClaudeCodeAdapter(options = {}) {
|
|
|
4189
4612
|
sessions: []
|
|
4190
4613
|
};
|
|
4191
4614
|
}
|
|
4615
|
+
},
|
|
4616
|
+
async lastMessage(sessionId) {
|
|
4617
|
+
if (lastMessages === null) {
|
|
4618
|
+
return {
|
|
4619
|
+
message: null,
|
|
4620
|
+
reason: "off",
|
|
4621
|
+
setting: transcriptsOff ? WAITING_TEXT_ENV : LAST_MESSAGE_ENV
|
|
4622
|
+
};
|
|
4623
|
+
}
|
|
4624
|
+
return lastMessages.read(sessionId);
|
|
4625
|
+
},
|
|
4626
|
+
get lastMessagesKept() {
|
|
4627
|
+
return lastMessages?.size ?? 0;
|
|
4192
4628
|
}
|
|
4193
4629
|
};
|
|
4194
4630
|
}
|
|
@@ -4204,8 +4640,8 @@ function parseProcessParents(stdout) {
|
|
|
4204
4640
|
return parents;
|
|
4205
4641
|
}
|
|
4206
4642
|
var readProcessParentsWithPs = async () => {
|
|
4207
|
-
const
|
|
4208
|
-
return
|
|
4643
|
+
const answer2 = await runPs(PS_PARENTS_ARGS);
|
|
4644
|
+
return answer2.ok ? parseProcessParents(answer2.stdout) : /* @__PURE__ */ new Map();
|
|
4209
4645
|
};
|
|
4210
4646
|
|
|
4211
4647
|
// src/collector/actions/stopSession.ts
|
|
@@ -4659,9 +5095,21 @@ async function pollAfterAnswer(poller, sessionId) {
|
|
|
4659
5095
|
function answerRefusalFor(req) {
|
|
4660
5096
|
return actionRefusalFor(req, ANSWER_ACTION, MAX_ANSWER_BODY_BYTES);
|
|
4661
5097
|
}
|
|
4662
|
-
function
|
|
5098
|
+
function createAnswerer(options) {
|
|
4663
5099
|
const { poller, events, asks } = options;
|
|
4664
5100
|
const now = options.now ?? Date.now;
|
|
5101
|
+
return async (sessionId, requestId, decision) => {
|
|
5102
|
+
const session = poller.getSnapshot().sessions.find((listed) => listed.id === sessionId);
|
|
5103
|
+
if (!session) return "no-ask";
|
|
5104
|
+
const outcome = await asks.answer(sessionId, requestId, decision);
|
|
5105
|
+
if (outcome !== "answered") return outcome;
|
|
5106
|
+
events.add([answeredEvent(session, decision, now())]);
|
|
5107
|
+
await pollAfterAnswer(poller, sessionId);
|
|
5108
|
+
return "answered";
|
|
5109
|
+
};
|
|
5110
|
+
}
|
|
5111
|
+
function createAnswerRoute(options) {
|
|
5112
|
+
const answer2 = createAnswerer(options);
|
|
4665
5113
|
return async function answerPermission(req) {
|
|
4666
5114
|
const refused = answerRefusalFor(req);
|
|
4667
5115
|
if (refused) {
|
|
@@ -4680,12 +5128,8 @@ function createAnswerRoute(options) {
|
|
|
4680
5128
|
'The body must be {"sessionId": "...", "requestId": "...", "decision": "allow" or "deny"} and nothing else.'
|
|
4681
5129
|
);
|
|
4682
5130
|
}
|
|
4683
|
-
const
|
|
4684
|
-
if (!session) return answerFailed("no-ask");
|
|
4685
|
-
const outcome = await asks.answer(asked.sessionId, asked.requestId, asked.decision);
|
|
5131
|
+
const outcome = await answer2(asked.sessionId, asked.requestId, asked.decision);
|
|
4686
5132
|
if (outcome !== "answered") return answerFailed(outcome);
|
|
4687
|
-
events.add([answeredEvent(session, asked.decision, now())]);
|
|
4688
|
-
await pollAfterAnswer(poller, asked.sessionId);
|
|
4689
5133
|
return {
|
|
4690
5134
|
status: 200,
|
|
4691
5135
|
body: { ok: true, decision: asked.decision }
|
|
@@ -4983,7 +5427,7 @@ function createHeldAsks(options) {
|
|
|
4983
5427
|
if (inFlight === run) inFlight = null;
|
|
4984
5428
|
}
|
|
4985
5429
|
}
|
|
4986
|
-
async function
|
|
5430
|
+
async function answer2(sessionId, requestId, decision) {
|
|
4987
5431
|
const entry = held.get(sessionId);
|
|
4988
5432
|
if (!entry || entry.requestId !== requestId || !entry.confirmed) return "no-ask";
|
|
4989
5433
|
if (decision === "allow" && !entry.shown.allow) return "not-allowable";
|
|
@@ -5015,7 +5459,7 @@ function createHeldAsks(options) {
|
|
|
5015
5459
|
const verdict = verdictOf(entry);
|
|
5016
5460
|
if (verdict === null) return;
|
|
5017
5461
|
entry.byRule = true;
|
|
5018
|
-
void
|
|
5462
|
+
void answer2(entry.sessionId, entry.requestId, verdict.decision).then((outcome) => {
|
|
5019
5463
|
if (outcome === "answered") rules.answered(request2, verdict);
|
|
5020
5464
|
});
|
|
5021
5465
|
}
|
|
@@ -5072,7 +5516,7 @@ function createHeldAsks(options) {
|
|
|
5072
5516
|
void check();
|
|
5073
5517
|
},
|
|
5074
5518
|
check,
|
|
5075
|
-
answer,
|
|
5519
|
+
answer: answer2,
|
|
5076
5520
|
withAsks(snapshot) {
|
|
5077
5521
|
const answering = status();
|
|
5078
5522
|
if (held.size === 0) return { ...snapshot, answering };
|
|
@@ -5135,7 +5579,7 @@ function createHeldAsks(options) {
|
|
|
5135
5579
|
import { chmod, lstat as lstat3, mkdir, stat as stat4, unlink } from "node:fs/promises";
|
|
5136
5580
|
import http from "node:http";
|
|
5137
5581
|
import net from "node:net";
|
|
5138
|
-
import
|
|
5582
|
+
import path10 from "node:path";
|
|
5139
5583
|
var HOOK_PATH = "/hooks/permission-request";
|
|
5140
5584
|
var HOOK_HEADER = "x-agent-lookout-hook";
|
|
5141
5585
|
var HOOK_HEADER_VALUE = "permission-request";
|
|
@@ -5208,7 +5652,7 @@ function createHookSocket(options) {
|
|
|
5208
5652
|
const maxBody = options.maxBodyBytes ?? MAX_HOOK_BODY_BYTES;
|
|
5209
5653
|
const bodyTimeoutMs = options.bodyTimeoutMs ?? HOOK_BODY_TIMEOUT_MS;
|
|
5210
5654
|
const uid = options.uid ?? process.getuid?.() ?? -1;
|
|
5211
|
-
const folder =
|
|
5655
|
+
const folder = path10.dirname(socketPath);
|
|
5212
5656
|
let server = null;
|
|
5213
5657
|
let made = null;
|
|
5214
5658
|
async function handle(req, res) {
|
|
@@ -5331,12 +5775,12 @@ function createHookSocket(options) {
|
|
|
5331
5775
|
|
|
5332
5776
|
// src/collector/answers/registryStatus.ts
|
|
5333
5777
|
import os4 from "node:os";
|
|
5334
|
-
import
|
|
5778
|
+
import path11 from "node:path";
|
|
5335
5779
|
function createRegistryStatus(options) {
|
|
5336
5780
|
const homeDir = options.homeDir ?? os4.homedir();
|
|
5337
5781
|
const readFile2 = options.readFile ?? readRegularFile;
|
|
5338
|
-
const home = options.env[CLAUDE_HOME_ENV]?.trim() ||
|
|
5339
|
-
const sessionsDir =
|
|
5782
|
+
const home = options.env[CLAUDE_HOME_ENV]?.trim() || path11.join(homeDir, ".claude");
|
|
5783
|
+
const sessionsDir = path11.join(path11.resolve(home), "sessions");
|
|
5340
5784
|
return {
|
|
5341
5785
|
async statusOf(sessionId) {
|
|
5342
5786
|
const unknown = { status: "unknown" };
|
|
@@ -5347,7 +5791,7 @@ function createRegistryStatus(options) {
|
|
|
5347
5791
|
const ownId = sessionId.slice("claude-code:".length);
|
|
5348
5792
|
let content;
|
|
5349
5793
|
try {
|
|
5350
|
-
content = await readFile2(
|
|
5794
|
+
content = await readFile2(path11.join(sessionsDir, `${pid}.json`));
|
|
5351
5795
|
} catch {
|
|
5352
5796
|
return unknown;
|
|
5353
5797
|
}
|
|
@@ -5621,18 +6065,18 @@ function namesOwnFiles(words, ownPaths = [], folder) {
|
|
|
5621
6065
|
(part) => from !== void 0 && !part.startsWith("/") && !part.startsWith("-") ? [part, `${from}/${part}`] : [part]
|
|
5622
6066
|
);
|
|
5623
6067
|
return read.some((part) => {
|
|
5624
|
-
const
|
|
5625
|
-
if (
|
|
5626
|
-
if (own.some((name) =>
|
|
5627
|
-
if (above.has(
|
|
5628
|
-
return ownNames.some((name) =>
|
|
6068
|
+
const path28 = plainPath(part);
|
|
6069
|
+
if (path28 === BY_NUMBER || path28.startsWith(`${BY_NUMBER}/`)) return true;
|
|
6070
|
+
if (own.some((name) => path28 === name || path28.startsWith(`${name}/`))) return true;
|
|
6071
|
+
if (above.has(path28)) return true;
|
|
6072
|
+
return ownNames.some((name) => path28 === name || path28.endsWith(`/${name}`));
|
|
5629
6073
|
});
|
|
5630
6074
|
});
|
|
5631
6075
|
}
|
|
5632
6076
|
function folderReachesOwnFiles(folder, ownPaths = []) {
|
|
5633
6077
|
const own = ownPaths.filter((name) => name.startsWith("/")).map(plainPath);
|
|
5634
|
-
const
|
|
5635
|
-
return own.some((name) => name ===
|
|
6078
|
+
const path28 = plainPath(folder);
|
|
6079
|
+
return own.some((name) => name === path28 || name.startsWith(`${path28 === "/" ? "" : path28}/`));
|
|
5636
6080
|
}
|
|
5637
6081
|
var PLAIN_WORD = /^[A-Za-z0-9_\-.,/:=+@%]+$/;
|
|
5638
6082
|
var ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
@@ -5985,6 +6429,8 @@ function createAnswering(options) {
|
|
|
5985
6429
|
observe: () => {
|
|
5986
6430
|
},
|
|
5987
6431
|
answeredIn: () => /* @__PURE__ */ new Set(),
|
|
6432
|
+
check: async () => {
|
|
6433
|
+
},
|
|
5988
6434
|
ruleAnswers: () => []
|
|
5989
6435
|
};
|
|
5990
6436
|
}
|
|
@@ -6012,6 +6458,7 @@ function createAnswering(options) {
|
|
|
6012
6458
|
let problem = null;
|
|
6013
6459
|
let listening = false;
|
|
6014
6460
|
const answerRoute = createAnswerRoute({ poller, events, asks, now: options.now });
|
|
6461
|
+
const answer2 = createAnswerer({ poller, events, asks, now: options.now });
|
|
6015
6462
|
return {
|
|
6016
6463
|
async start() {
|
|
6017
6464
|
const started = await socket.start();
|
|
@@ -6044,6 +6491,9 @@ function createAnswering(options) {
|
|
|
6044
6491
|
req.resume();
|
|
6045
6492
|
return answerRefusalFor(req) ?? refusal(405, "This address only answers GET requests.", { Allow: "GET" });
|
|
6046
6493
|
},
|
|
6494
|
+
// The same answer, from the host's own process. With no socket nothing is held.
|
|
6495
|
+
answer: async (sessionId, requestId, decision) => problem === null ? answer2(sessionId, requestId, decision) : "unavailable",
|
|
6496
|
+
check: () => asks.check(),
|
|
6047
6497
|
ruleAnswers: () => ruleAnswers.recent()
|
|
6048
6498
|
};
|
|
6049
6499
|
}
|
|
@@ -6271,169 +6721,1532 @@ function waitTotals(input) {
|
|
|
6271
6721
|
};
|
|
6272
6722
|
}
|
|
6273
6723
|
|
|
6274
|
-
// src/collector/adapters/
|
|
6724
|
+
// src/collector/adapters/antigravity/index.ts
|
|
6275
6725
|
import os5 from "node:os";
|
|
6276
|
-
import
|
|
6726
|
+
import path15 from "node:path";
|
|
6277
6727
|
|
|
6278
|
-
// src/core/mapping/
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
const {
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
|
|
6728
|
+
// src/core/mapping/antigravityLiveness.ts
|
|
6729
|
+
function holdConversations(processes, conversations, listedAt = null) {
|
|
6730
|
+
const held = /* @__PURE__ */ new Set();
|
|
6731
|
+
let complete = true;
|
|
6732
|
+
for (const { startedAt, conversation } of processes) {
|
|
6733
|
+
if (conversation !== void 0) held.add(conversation);
|
|
6734
|
+
if (startedAt === null) {
|
|
6735
|
+
complete = false;
|
|
6736
|
+
continue;
|
|
6737
|
+
}
|
|
6738
|
+
let wrote = false;
|
|
6739
|
+
for (const { id, writtenAt } of conversations) {
|
|
6740
|
+
if (writtenAt === null || writtenAt < startedAt) continue;
|
|
6741
|
+
held.add(id);
|
|
6742
|
+
wrote = true;
|
|
6743
|
+
}
|
|
6744
|
+
if (!wrote && conversation === void 0) complete = false;
|
|
6294
6745
|
}
|
|
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;
|
|
6746
|
+
if (listedAt !== null) {
|
|
6747
|
+
for (const { id, writtenAt } of conversations) {
|
|
6748
|
+
if (writtenAt !== null && writtenAt > listedAt) held.add(id);
|
|
6749
|
+
}
|
|
6307
6750
|
}
|
|
6308
|
-
return
|
|
6751
|
+
return { held, complete };
|
|
6309
6752
|
}
|
|
6310
|
-
function
|
|
6311
|
-
if (
|
|
6312
|
-
if (
|
|
6313
|
-
return
|
|
6753
|
+
function antigravityLiveness(id, holding) {
|
|
6754
|
+
if (holding === null) return "unknown";
|
|
6755
|
+
if (holding.held.has(id)) return true;
|
|
6756
|
+
return holding.complete ? false : "unknown";
|
|
6314
6757
|
}
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
6758
|
+
|
|
6759
|
+
// src/core/mapping/antigravityLog.ts
|
|
6760
|
+
var GLOG_LINE = /^[IWEF](\d{2})(\d{2}) (\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))? +\d+ ([\w.-]+):\d+\] (.*)$/;
|
|
6761
|
+
var SOURCE2 = {
|
|
6762
|
+
backend: "server.go",
|
|
6763
|
+
created: "server.go",
|
|
6764
|
+
streaming: "conversation_manager.go",
|
|
6765
|
+
asking: "tool_confirmation_manager.go",
|
|
6766
|
+
answered: "input_loop.go"
|
|
6767
|
+
};
|
|
6768
|
+
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}";
|
|
6769
|
+
var OPENED = new RegExp(`^(Created|Streaming) conversation (${UUID})$`);
|
|
6770
|
+
var BACKEND = /^Creating CLI server backend: (?:.* )?workspaceDirs=\[([^\]\s]+)\](?: |$)/;
|
|
6771
|
+
var ASKING = /^Surfacing tool confirmation: "([A-Za-z]{1,64})" at step (\d{1,9})$/;
|
|
6772
|
+
var ANSWERED = new RegExp(
|
|
6773
|
+
`^Responding to tool confirmation: convID=(${UUID}), stepIdx=(\\d{1,9}), approved=(?:true|false)(?:, .*)?$`
|
|
6774
|
+
);
|
|
6775
|
+
var ABSOLUTE = /^(?:\/|[A-Za-z]:[\\/])/;
|
|
6776
|
+
var UNPRINTABLE = /[\p{Cc}\p{Cf}]/u;
|
|
6777
|
+
function readAgyLogLine(line) {
|
|
6778
|
+
const glog = GLOG_LINE.exec(line);
|
|
6779
|
+
if (glog === null) return null;
|
|
6780
|
+
const source = glog[7];
|
|
6781
|
+
const message = glog[8];
|
|
6782
|
+
const opened = OPENED.exec(message);
|
|
6783
|
+
if (opened !== null) {
|
|
6784
|
+
const from = opened[1] === "Created" ? SOURCE2.created : SOURCE2.streaming;
|
|
6785
|
+
return source === from ? { kind: "opened", conversation: opened[2].toLowerCase() } : null;
|
|
6786
|
+
}
|
|
6787
|
+
if (source === SOURCE2.backend) {
|
|
6788
|
+
const backend = BACKEND.exec(message);
|
|
6789
|
+
return backend !== null && ABSOLUTE.test(backend[1]) && !UNPRINTABLE.test(backend[1]) ? { kind: "folder", folder: backend[1] } : null;
|
|
6790
|
+
}
|
|
6791
|
+
if (source === SOURCE2.answered) {
|
|
6792
|
+
const answered = ANSWERED.exec(message);
|
|
6793
|
+
return answered === null ? null : { kind: "answered", conversation: answered[1].toLowerCase(), step: Number(answered[2]) };
|
|
6794
|
+
}
|
|
6795
|
+
if (source !== SOURCE2.asking) return null;
|
|
6796
|
+
const asking = ASKING.exec(message);
|
|
6797
|
+
if (asking !== null) {
|
|
6798
|
+
const fraction = (glog[6] ?? "0").padEnd(3, "0").slice(0, 3);
|
|
6799
|
+
return {
|
|
6800
|
+
kind: "asking",
|
|
6801
|
+
tool: asking[1],
|
|
6802
|
+
step: Number(asking[2]),
|
|
6803
|
+
at: {
|
|
6804
|
+
month: Number(glog[1]),
|
|
6805
|
+
day: Number(glog[2]),
|
|
6806
|
+
hour: Number(glog[3]),
|
|
6807
|
+
minute: Number(glog[4]),
|
|
6808
|
+
second: Number(glog[5]),
|
|
6809
|
+
millisecond: Number(fraction)
|
|
6810
|
+
}
|
|
6811
|
+
};
|
|
6812
|
+
}
|
|
6813
|
+
return null;
|
|
6318
6814
|
}
|
|
6319
|
-
function
|
|
6320
|
-
return
|
|
6815
|
+
function emptyAgyLogState() {
|
|
6816
|
+
return { folder: null, folderNamed: false, opened: /* @__PURE__ */ new Set(), current: null, ask: null };
|
|
6321
6817
|
}
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
|
|
6818
|
+
function addAgyLogLine(state, line) {
|
|
6819
|
+
switch (line.kind) {
|
|
6820
|
+
case "folder":
|
|
6821
|
+
if (!state.folderNamed) state.folder = line.folder;
|
|
6822
|
+
state.folderNamed = true;
|
|
6823
|
+
return;
|
|
6824
|
+
case "opened":
|
|
6825
|
+
state.opened.add(line.conversation);
|
|
6826
|
+
if (state.current !== line.conversation) state.ask = null;
|
|
6827
|
+
state.current = line.conversation;
|
|
6828
|
+
return;
|
|
6829
|
+
case "asking":
|
|
6830
|
+
state.ask = state.current === null ? null : { conversation: state.current, step: line.step, tool: line.tool, at: line.at };
|
|
6831
|
+
return;
|
|
6832
|
+
case "answered":
|
|
6833
|
+
if (state.ask?.conversation === line.conversation && state.ask.step === line.step) {
|
|
6834
|
+
state.ask = null;
|
|
6835
|
+
}
|
|
6836
|
+
return;
|
|
6837
|
+
}
|
|
6329
6838
|
}
|
|
6330
|
-
var
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
|
|
6839
|
+
var ASK_STEPS_AHEAD = 2;
|
|
6840
|
+
function waitsForApproval(ask, conversation, lastIndex) {
|
|
6841
|
+
if (ask === null || ask.conversation !== conversation || lastIndex === null) return false;
|
|
6842
|
+
return ask.step > lastIndex && ask.step <= lastIndex + ASK_STEPS_AHEAD;
|
|
6843
|
+
}
|
|
6844
|
+
function agyLogTimeAt(time, near) {
|
|
6845
|
+
const year = new Date(near).getFullYear();
|
|
6846
|
+
let best = null;
|
|
6847
|
+
for (const y of [year - 1, year, year + 1]) {
|
|
6848
|
+
const at = new Date(
|
|
6849
|
+
y,
|
|
6850
|
+
time.month - 1,
|
|
6851
|
+
time.day,
|
|
6852
|
+
time.hour,
|
|
6853
|
+
time.minute,
|
|
6854
|
+
time.second,
|
|
6855
|
+
time.millisecond
|
|
6856
|
+
);
|
|
6857
|
+
if (at.getMonth() !== time.month - 1 || at.getDate() !== time.day) continue;
|
|
6858
|
+
const ms = at.getTime();
|
|
6859
|
+
if (best === null || Math.abs(ms - near) < Math.abs(best - near)) best = ms;
|
|
6336
6860
|
}
|
|
6337
|
-
|
|
6338
|
-
if (isRecord8(source)) return !Object.keys(source).some((kind) => NOT_A_SESSION_SOURCE.has(kind));
|
|
6339
|
-
return true;
|
|
6861
|
+
return best;
|
|
6340
6862
|
}
|
|
6341
6863
|
|
|
6342
|
-
// src/collector/adapters/
|
|
6343
|
-
|
|
6344
|
-
var
|
|
6345
|
-
var
|
|
6346
|
-
var
|
|
6864
|
+
// src/collector/adapters/antigravity/agyLogs.ts
|
|
6865
|
+
import path12 from "node:path";
|
|
6866
|
+
var LOG_DIR = "log";
|
|
6867
|
+
var LOG_LIMIT_BYTES = 4 * 1024 * 1024;
|
|
6868
|
+
var LINE_LIMIT_BYTES = 64 * 1024;
|
|
6869
|
+
var LOG_NAMED_WITHIN_MS = 1e4;
|
|
6870
|
+
var PS_LATE_MS = 2e3;
|
|
6871
|
+
var RELIST_MS = 1e4;
|
|
6872
|
+
var LOG_NAME = /^cli-(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})\.log$/;
|
|
6347
6873
|
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
|
-
])
|
|
6874
|
+
function logStartOf(name) {
|
|
6875
|
+
const match = LOG_NAME.exec(name);
|
|
6876
|
+
if (match === null) return null;
|
|
6877
|
+
const [year, month, day, hour, minute, second] = match.slice(1).map(Number);
|
|
6878
|
+
const at = new Date(year, month - 1, day, hour, minute, second);
|
|
6879
|
+
if (at.getMonth() !== month - 1 || at.getDate() !== day || at.getHours() !== hour) return null;
|
|
6880
|
+
return at.getTime();
|
|
6881
|
+
}
|
|
6882
|
+
function matchLogs(processes, logs) {
|
|
6883
|
+
const candidates = (startedAt) => startedAt === null ? [] : [...logs].filter(
|
|
6884
|
+
([, at]) => at >= startedAt - PS_LATE_MS && at - startedAt <= LOG_NAMED_WITHIN_MS
|
|
6374
6885
|
);
|
|
6886
|
+
const owners = /* @__PURE__ */ new Map();
|
|
6887
|
+
for (const { startedAt } of processes) {
|
|
6888
|
+
for (const [name] of candidates(startedAt)) owners.set(name, (owners.get(name) ?? 0) + 1);
|
|
6889
|
+
}
|
|
6890
|
+
return processes.map(({ startedAt }) => {
|
|
6891
|
+
const found = candidates(startedAt);
|
|
6892
|
+
if (found.length !== 1) return null;
|
|
6893
|
+
const [name] = found[0];
|
|
6894
|
+
return owners.get(name) === 1 ? name : null;
|
|
6895
|
+
});
|
|
6375
6896
|
}
|
|
6376
|
-
function
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
|
|
6897
|
+
function createAgyLogReader(options) {
|
|
6898
|
+
const { io } = options;
|
|
6899
|
+
const now = options.now ?? Date.now;
|
|
6900
|
+
const logDir = path12.join(options.home, LOG_DIR);
|
|
6901
|
+
const cursors = /* @__PURE__ */ new Map();
|
|
6902
|
+
let listed = /* @__PURE__ */ new Map();
|
|
6903
|
+
let listedFor = "";
|
|
6904
|
+
let listedAt = -Infinity;
|
|
6905
|
+
async function list(processes) {
|
|
6906
|
+
const key = processes.map((p) => p.startedAt ?? "?").join(",");
|
|
6907
|
+
const since = now() - listedAt;
|
|
6908
|
+
if (key === listedFor && since >= 0 && since < RELIST_MS) return;
|
|
6909
|
+
listedFor = key;
|
|
6910
|
+
listedAt = now();
|
|
6911
|
+
let names;
|
|
6912
|
+
try {
|
|
6913
|
+
names = await io.readdir(logDir);
|
|
6914
|
+
} catch {
|
|
6915
|
+
listed = /* @__PURE__ */ new Map();
|
|
6916
|
+
return;
|
|
6917
|
+
}
|
|
6918
|
+
const next = /* @__PURE__ */ new Map();
|
|
6919
|
+
for (const name of names) {
|
|
6920
|
+
const at = logStartOf(name);
|
|
6921
|
+
if (at !== null) next.set(name, at);
|
|
6922
|
+
}
|
|
6923
|
+
listed = next;
|
|
6382
6924
|
}
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
|
|
6393
|
-
|
|
6394
|
-
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
|
|
6399
|
-
|
|
6400
|
-
|
|
6925
|
+
function addLines(cursor, data) {
|
|
6926
|
+
let buffer = cursor.pending.byteLength > 0 ? Buffer.concat([cursor.pending, data]) : data;
|
|
6927
|
+
if (cursor.skipping) {
|
|
6928
|
+
const end = buffer.indexOf(NEWLINE);
|
|
6929
|
+
if (end === -1) return;
|
|
6930
|
+
buffer = buffer.subarray(end + 1);
|
|
6931
|
+
cursor.skipping = false;
|
|
6932
|
+
}
|
|
6933
|
+
let start = 0;
|
|
6934
|
+
let newline = buffer.indexOf(NEWLINE);
|
|
6935
|
+
while (newline !== -1) {
|
|
6936
|
+
const line = readAgyLogLine(buffer.toString("utf8", start, newline));
|
|
6937
|
+
if (line !== null) addAgyLogLine(cursor.state, line);
|
|
6938
|
+
start = newline + 1;
|
|
6939
|
+
newline = buffer.indexOf(NEWLINE, start);
|
|
6940
|
+
}
|
|
6941
|
+
const rest = buffer.subarray(start);
|
|
6942
|
+
cursor.skipping = rest.byteLength > LINE_LIMIT_BYTES;
|
|
6943
|
+
cursor.pending = cursor.skipping ? Buffer.alloc(0) : Buffer.from(rest);
|
|
6401
6944
|
}
|
|
6402
|
-
|
|
6403
|
-
|
|
6404
|
-
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
6408
|
-
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
6414
|
-
|
|
6945
|
+
async function readLog(file, info) {
|
|
6946
|
+
const known = cursors.get(file);
|
|
6947
|
+
if (known && known.ino === info.ino && known.size === info.size && known.mtimeMs === info.mtimeMs) {
|
|
6948
|
+
return known.state;
|
|
6949
|
+
}
|
|
6950
|
+
const handle = await io.openRegular(file);
|
|
6951
|
+
try {
|
|
6952
|
+
const opened = handle.info;
|
|
6953
|
+
const onward = known !== void 0 && known.ino === opened.ino && opened.size >= known.offset;
|
|
6954
|
+
const cursor = onward ? { ...known, state: known.state } : {
|
|
6955
|
+
ino: opened.ino,
|
|
6956
|
+
size: 0,
|
|
6957
|
+
mtimeMs: 0,
|
|
6958
|
+
offset: 0,
|
|
6959
|
+
pending: Buffer.alloc(0),
|
|
6960
|
+
skipping: false,
|
|
6961
|
+
state: emptyAgyLogState()
|
|
6962
|
+
};
|
|
6963
|
+
let from = cursor.offset;
|
|
6964
|
+
if (opened.size - from > LOG_LIMIT_BYTES) {
|
|
6965
|
+
from = opened.size - LOG_LIMIT_BYTES;
|
|
6966
|
+
cursor.state.ask = null;
|
|
6967
|
+
cursor.pending = Buffer.alloc(0);
|
|
6968
|
+
cursor.skipping = true;
|
|
6969
|
+
addLines(cursor, Buffer.from(await handle.read(from, opened.size - from)));
|
|
6970
|
+
} else if (opened.size > from) {
|
|
6971
|
+
addLines(cursor, Buffer.from(await handle.read(from, opened.size - from)));
|
|
6972
|
+
}
|
|
6973
|
+
cursor.offset = opened.size;
|
|
6974
|
+
cursor.size = opened.size;
|
|
6975
|
+
cursor.mtimeMs = opened.mtimeMs;
|
|
6976
|
+
cursor.ino = opened.ino;
|
|
6977
|
+
cursors.set(file, cursor);
|
|
6978
|
+
return cursor.state;
|
|
6979
|
+
} finally {
|
|
6980
|
+
await handle.close();
|
|
6981
|
+
}
|
|
6982
|
+
}
|
|
6983
|
+
async function read(processes) {
|
|
6984
|
+
if (processes.length === 0) {
|
|
6985
|
+
cursors.clear();
|
|
6986
|
+
return [];
|
|
6987
|
+
}
|
|
6988
|
+
await list(processes);
|
|
6989
|
+
const names = matchLogs(processes, listed);
|
|
6990
|
+
const kept = /* @__PURE__ */ new Set();
|
|
6991
|
+
const states = [];
|
|
6992
|
+
for (const name of names) {
|
|
6993
|
+
if (name === null) {
|
|
6994
|
+
states.push(null);
|
|
6995
|
+
continue;
|
|
6996
|
+
}
|
|
6997
|
+
const file = path12.join(logDir, name);
|
|
6998
|
+
kept.add(file);
|
|
6999
|
+
try {
|
|
7000
|
+
const info = await io.lstat(file);
|
|
7001
|
+
states.push(info.kind === "file" ? await readLog(file, info) : null);
|
|
7002
|
+
} catch {
|
|
7003
|
+
states.push(null);
|
|
7004
|
+
}
|
|
7005
|
+
}
|
|
7006
|
+
for (const file of cursors.keys()) {
|
|
7007
|
+
if (!kept.has(file)) cursors.delete(file);
|
|
7008
|
+
}
|
|
7009
|
+
return states;
|
|
7010
|
+
}
|
|
7011
|
+
return {
|
|
7012
|
+
async read(processes) {
|
|
7013
|
+
try {
|
|
7014
|
+
return await read(processes);
|
|
7015
|
+
} catch {
|
|
7016
|
+
return processes.map(() => null);
|
|
7017
|
+
}
|
|
7018
|
+
}
|
|
7019
|
+
};
|
|
7020
|
+
}
|
|
7021
|
+
|
|
7022
|
+
// src/collector/adapters/antigravity/agyProcesses.ts
|
|
7023
|
+
var AGY_TABLE_ARGS = ["-A", "-o", "pid=,ppid=,lstart=,comm="];
|
|
7024
|
+
function agyCommandLineArgs(pids) {
|
|
7025
|
+
return ["-o", "pid=,args=", "-p", pids.join(",")];
|
|
7026
|
+
}
|
|
7027
|
+
var AGY_COMMANDS = /* @__PURE__ */ new Set([
|
|
7028
|
+
"agent",
|
|
7029
|
+
"agents",
|
|
7030
|
+
"changelog",
|
|
7031
|
+
"help",
|
|
7032
|
+
"install",
|
|
7033
|
+
"mcp",
|
|
7034
|
+
"mic-serve",
|
|
7035
|
+
"models",
|
|
7036
|
+
"plugin",
|
|
7037
|
+
"plugins",
|
|
7038
|
+
"remote-control",
|
|
7039
|
+
"update"
|
|
7040
|
+
]);
|
|
7041
|
+
var NOT_A_SESSION_OPTIONS = /* @__PURE__ */ new Set([
|
|
7042
|
+
"-h",
|
|
7043
|
+
"-help",
|
|
7044
|
+
"--help",
|
|
7045
|
+
"-version",
|
|
7046
|
+
"--version"
|
|
7047
|
+
]);
|
|
7048
|
+
var UUID2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
7049
|
+
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*$/;
|
|
7050
|
+
function isAgyProgram(program) {
|
|
7051
|
+
const name = program.split(/[\\/]/).pop()?.toLowerCase();
|
|
7052
|
+
return name === "agy" || name === "agy.exe";
|
|
7053
|
+
}
|
|
7054
|
+
function parseAgyTable(stdout) {
|
|
7055
|
+
const rows = [];
|
|
7056
|
+
for (const line of stdout.split("\n")) {
|
|
7057
|
+
const match = TABLE_LINE.exec(line);
|
|
7058
|
+
if (!match || !isAgyProgram(match[4])) continue;
|
|
7059
|
+
const start = match[3].replace(/\s+/g, " ");
|
|
7060
|
+
rows.push({
|
|
7061
|
+
pid: Number(match[1]),
|
|
7062
|
+
ppid: Number(match[2]),
|
|
7063
|
+
start,
|
|
7064
|
+
startedAt: processStartTime(start)
|
|
7065
|
+
});
|
|
7066
|
+
}
|
|
7067
|
+
return rows;
|
|
7068
|
+
}
|
|
7069
|
+
var PROGRAM_IN_LINE = /(?:^|[\\/])agy(?:\.exe)?(?=\s|$)/i;
|
|
7070
|
+
function readAgyCommandLine(line) {
|
|
7071
|
+
const program = PROGRAM_IN_LINE.exec(line);
|
|
7072
|
+
if (!program) return { session: true };
|
|
7073
|
+
const words = line.slice(program.index + program[0].length).split(/\s+/).filter((word2) => word2 !== "");
|
|
7074
|
+
const first = words[0];
|
|
7075
|
+
if (first !== void 0 && (AGY_COMMANDS.has(first) || NOT_A_SESSION_OPTIONS.has(first))) {
|
|
7076
|
+
return { session: false };
|
|
7077
|
+
}
|
|
7078
|
+
for (const [index, word2] of words.entries()) {
|
|
7079
|
+
let value;
|
|
7080
|
+
if (word2 === "--conversation" || word2 === "-conversation") value = words[index + 1];
|
|
7081
|
+
else if (word2.startsWith("--conversation=")) value = word2.slice("--conversation=".length);
|
|
7082
|
+
else if (word2.startsWith("-conversation=")) value = word2.slice("-conversation=".length);
|
|
7083
|
+
else continue;
|
|
7084
|
+
if (value !== void 0 && UUID2.test(value)) {
|
|
7085
|
+
return { session: true, conversation: value.toLowerCase() };
|
|
7086
|
+
}
|
|
7087
|
+
}
|
|
7088
|
+
return { session: true };
|
|
7089
|
+
}
|
|
7090
|
+
function parseCommandLines(stdout) {
|
|
7091
|
+
const lines = /* @__PURE__ */ new Map();
|
|
7092
|
+
for (const line of stdout.split("\n")) {
|
|
7093
|
+
const match = /^\s*(\d+)\s(.*)$/.exec(line);
|
|
7094
|
+
if (match) lines.set(Number(match[1]), match[2]);
|
|
7095
|
+
}
|
|
7096
|
+
return lines;
|
|
7097
|
+
}
|
|
7098
|
+
function createAgyProcessReader(options = {}) {
|
|
7099
|
+
const run = options.run ?? runPs;
|
|
7100
|
+
const platform = options.platform ?? process.platform;
|
|
7101
|
+
const known = /* @__PURE__ */ new Map();
|
|
7102
|
+
async function read() {
|
|
7103
|
+
if (platform === "win32") return { ok: false };
|
|
7104
|
+
const table = await run(AGY_TABLE_ARGS, { utc: true });
|
|
7105
|
+
if (!table.ok) return { ok: false };
|
|
7106
|
+
const rows = parseAgyTable(table.stdout);
|
|
7107
|
+
const keyOf = (row) => `${row.pid} ${row.start}`;
|
|
7108
|
+
const current = new Set(rows.map(keyOf));
|
|
7109
|
+
for (const key of known.keys()) {
|
|
7110
|
+
if (!current.has(key)) known.delete(key);
|
|
7111
|
+
}
|
|
7112
|
+
const unseen = rows.filter((row) => !known.has(keyOf(row)));
|
|
7113
|
+
const ended = /* @__PURE__ */ new Set();
|
|
7114
|
+
if (unseen.length > 0) {
|
|
7115
|
+
const answer2 = await run(agyCommandLineArgs(unseen.map((row) => row.pid)));
|
|
7116
|
+
const lines = parseCommandLines(answer2.stdout);
|
|
7117
|
+
for (const row of unseen) {
|
|
7118
|
+
const line = lines.get(row.pid);
|
|
7119
|
+
if (line !== void 0) known.set(keyOf(row), readAgyCommandLine(line));
|
|
7120
|
+
else if (answer2.ok || lines.size > 0) ended.add(row.pid);
|
|
7121
|
+
}
|
|
7122
|
+
}
|
|
7123
|
+
const running = /* @__PURE__ */ new Map();
|
|
7124
|
+
for (const row of rows) {
|
|
7125
|
+
if (!ended.has(row.pid)) running.set(row.pid, row);
|
|
7126
|
+
}
|
|
7127
|
+
const lineOf2 = (row) => known.get(keyOf(row)) ?? { session: true };
|
|
7128
|
+
const underSession = (row) => {
|
|
7129
|
+
const seen = /* @__PURE__ */ new Set([row.pid]);
|
|
7130
|
+
let parent = running.get(row.ppid);
|
|
7131
|
+
while (parent !== void 0 && !seen.has(parent.pid)) {
|
|
7132
|
+
if (lineOf2(parent).session) return true;
|
|
7133
|
+
seen.add(parent.pid);
|
|
7134
|
+
parent = running.get(parent.ppid);
|
|
7135
|
+
}
|
|
7136
|
+
return false;
|
|
7137
|
+
};
|
|
7138
|
+
const sessions = [];
|
|
7139
|
+
for (const row of running.values()) {
|
|
7140
|
+
const line = lineOf2(row);
|
|
7141
|
+
if (!line.session || underSession(row)) continue;
|
|
7142
|
+
sessions.push(
|
|
7143
|
+
line.conversation === void 0 ? { startedAt: row.startedAt } : { startedAt: row.startedAt, conversation: line.conversation }
|
|
7144
|
+
);
|
|
7145
|
+
}
|
|
7146
|
+
return { ok: true, sessions };
|
|
7147
|
+
}
|
|
7148
|
+
return {
|
|
7149
|
+
async read() {
|
|
7150
|
+
try {
|
|
7151
|
+
return await read();
|
|
7152
|
+
} catch {
|
|
7153
|
+
return { ok: false };
|
|
7154
|
+
}
|
|
7155
|
+
}
|
|
7156
|
+
};
|
|
7157
|
+
}
|
|
7158
|
+
|
|
7159
|
+
// src/collector/adapters/antigravity/conversations.ts
|
|
7160
|
+
import path13 from "node:path";
|
|
7161
|
+
var BRAIN_DIR = "brain";
|
|
7162
|
+
var CONVERSATIONS_DIR = "conversations";
|
|
7163
|
+
var TRANSCRIPT_IN_FOLDER = [".system_generated", "logs", "transcript.jsonl"];
|
|
7164
|
+
var OLD_REFRESH_MS = 3e4;
|
|
7165
|
+
var UUID3 = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
|
|
7166
|
+
var FOLDER_NAME = new RegExp(`^(${UUID3})$`, "i");
|
|
7167
|
+
var DATABASE_NAME = new RegExp(`^(${UUID3})\\.db(?:-wal)?$`, "i");
|
|
7168
|
+
function conversationIdOfFolder(name) {
|
|
7169
|
+
return FOLDER_NAME.exec(name)?.[1]?.toLowerCase() ?? null;
|
|
7170
|
+
}
|
|
7171
|
+
function conversationIdOfDatabase(name) {
|
|
7172
|
+
return DATABASE_NAME.exec(name)?.[1]?.toLowerCase() ?? null;
|
|
7173
|
+
}
|
|
7174
|
+
function createConversationFinder(options) {
|
|
7175
|
+
const { io } = options;
|
|
7176
|
+
const brainDir = path13.join(options.home, BRAIN_DIR);
|
|
7177
|
+
const databaseDir = path13.join(options.home, CONVERSATIONS_DIR);
|
|
7178
|
+
const oldRefreshMs = options.oldRefreshMs ?? OLD_REFRESH_MS;
|
|
7179
|
+
const known = /* @__PURE__ */ new Map();
|
|
7180
|
+
let oldCheckedAt = null;
|
|
7181
|
+
async function lookAt(id) {
|
|
7182
|
+
const transcript = path13.join(brainDir, id, ...TRANSCRIPT_IN_FOLDER);
|
|
7183
|
+
const infos = await Promise.all(
|
|
7184
|
+
[transcript, path13.join(databaseDir, `${id}.db`), path13.join(databaseDir, `${id}.db-wal`)].map(
|
|
7185
|
+
(file) => io.lstat(file).catch(() => null)
|
|
7186
|
+
)
|
|
7187
|
+
);
|
|
7188
|
+
const transcriptInfo = infos[0]?.kind === "file" ? infos[0] : null;
|
|
7189
|
+
const writtenAt = transcriptInfo === null ? null : Math.floor(transcriptInfo.mtimeMs);
|
|
7190
|
+
let lastWriteAt = null;
|
|
7191
|
+
for (const info of infos) {
|
|
7192
|
+
if (info?.kind !== "file") continue;
|
|
7193
|
+
const at = Math.floor(info.mtimeMs);
|
|
7194
|
+
lastWriteAt = lastWriteAt === null ? at : Math.max(lastWriteAt, at);
|
|
7195
|
+
}
|
|
7196
|
+
return { id, transcript, transcriptInfo, writtenAt, lastWriteAt };
|
|
7197
|
+
}
|
|
7198
|
+
return {
|
|
7199
|
+
async list(checkedAt, mayBeOpen) {
|
|
7200
|
+
let folders;
|
|
7201
|
+
try {
|
|
7202
|
+
folders = await io.readdir(brainDir);
|
|
7203
|
+
} catch (error) {
|
|
7204
|
+
return { state: isMissing2(error) ? "missing" : "unreadable" };
|
|
7205
|
+
}
|
|
7206
|
+
const databases = await io.readdir(databaseDir).catch(() => []);
|
|
7207
|
+
const ids = /* @__PURE__ */ new Set();
|
|
7208
|
+
for (const name of folders) {
|
|
7209
|
+
const id = conversationIdOfFolder(name);
|
|
7210
|
+
if (id !== null) ids.add(id);
|
|
7211
|
+
}
|
|
7212
|
+
for (const name of databases) {
|
|
7213
|
+
const id = conversationIdOfDatabase(name);
|
|
7214
|
+
if (id !== null) ids.add(id);
|
|
7215
|
+
}
|
|
7216
|
+
for (const id of known.keys()) {
|
|
7217
|
+
if (!ids.has(id)) known.delete(id);
|
|
7218
|
+
}
|
|
7219
|
+
const since = oldCheckedAt === null ? Infinity : checkedAt - oldCheckedAt;
|
|
7220
|
+
const refreshOld = since < 0 || since >= oldRefreshMs;
|
|
7221
|
+
if (refreshOld) oldCheckedAt = checkedAt;
|
|
7222
|
+
const cutoff = checkedAt - FINISHED_RETENTION_MS;
|
|
7223
|
+
await Promise.all(
|
|
7224
|
+
[...ids].map(async (id) => {
|
|
7225
|
+
const before = known.get(id);
|
|
7226
|
+
const recent = before === void 0 || before.lastWriteAt !== null && before.lastWriteAt >= cutoff || mayBeOpen(id);
|
|
7227
|
+
if (recent || refreshOld) known.set(id, await lookAt(id));
|
|
7228
|
+
})
|
|
7229
|
+
);
|
|
7230
|
+
const conversations = [...known.values()].sort((a, b) => a.id < b.id ? -1 : 1);
|
|
7231
|
+
return { state: "ok", conversations };
|
|
7232
|
+
}
|
|
7233
|
+
};
|
|
7234
|
+
}
|
|
7235
|
+
function writtenRecently(files, now) {
|
|
7236
|
+
return files.writtenAt !== null && isWithinRetention(files.writtenAt, now);
|
|
7237
|
+
}
|
|
7238
|
+
|
|
7239
|
+
// src/collector/adapters/antigravity/annotations.ts
|
|
7240
|
+
import path14 from "node:path";
|
|
7241
|
+
var ANNOTATIONS_DIR = "annotations";
|
|
7242
|
+
var ANNOTATION_LIMIT_BYTES = 4 * 1024;
|
|
7243
|
+
var TITLE_FIELD = /^title[ \t]*:[ \t]*"((?:[^"\\\n]|\\.)*)"[ \t]*$/m;
|
|
7244
|
+
var SIMPLE_ESCAPES = {
|
|
7245
|
+
n: 10,
|
|
7246
|
+
r: 13,
|
|
7247
|
+
t: 9,
|
|
7248
|
+
'"': 34,
|
|
7249
|
+
"'": 39,
|
|
7250
|
+
"\\": 92,
|
|
7251
|
+
"?": 63,
|
|
7252
|
+
a: 7,
|
|
7253
|
+
b: 8,
|
|
7254
|
+
f: 12,
|
|
7255
|
+
v: 11
|
|
7256
|
+
};
|
|
7257
|
+
function unescapeTextFormat(body) {
|
|
7258
|
+
const chars = Array.from(body);
|
|
7259
|
+
const bytes3 = [];
|
|
7260
|
+
for (let i = 0; i < chars.length; i += 1) {
|
|
7261
|
+
const char = chars[i];
|
|
7262
|
+
if (char !== "\\") {
|
|
7263
|
+
bytes3.push(...Buffer.from(char, "utf8"));
|
|
7264
|
+
continue;
|
|
7265
|
+
}
|
|
7266
|
+
const next = chars[i + 1];
|
|
7267
|
+
if (next === void 0) return null;
|
|
7268
|
+
const octal = /^[0-7]{1,3}/.exec(chars.slice(i + 1, i + 4).join(""));
|
|
7269
|
+
if (octal !== null) {
|
|
7270
|
+
const value = parseInt(octal[0], 8);
|
|
7271
|
+
if (value > 255) return null;
|
|
7272
|
+
bytes3.push(value);
|
|
7273
|
+
i += octal[0].length;
|
|
7274
|
+
continue;
|
|
7275
|
+
}
|
|
7276
|
+
if (next === "u" || next === "U") {
|
|
7277
|
+
const digits = next === "u" ? 4 : 8;
|
|
7278
|
+
const hex = chars.slice(i + 2, i + 2 + digits).join("");
|
|
7279
|
+
if (!new RegExp(`^[0-9a-fA-F]{${digits}}$`).test(hex)) return null;
|
|
7280
|
+
const point = parseInt(hex, 16);
|
|
7281
|
+
if (point > 1114111 || point >= 55296 && point <= 57343) return null;
|
|
7282
|
+
bytes3.push(...Buffer.from(String.fromCodePoint(point), "utf8"));
|
|
7283
|
+
i += 1 + digits;
|
|
7284
|
+
continue;
|
|
7285
|
+
}
|
|
7286
|
+
if (next === "x" || next === "X") {
|
|
7287
|
+
const hex = /^[0-9a-fA-F]{1,2}/.exec(chars.slice(i + 2, i + 4).join(""));
|
|
7288
|
+
if (hex === null) return null;
|
|
7289
|
+
bytes3.push(parseInt(hex[0], 16));
|
|
7290
|
+
i += 1 + hex[0].length;
|
|
7291
|
+
continue;
|
|
7292
|
+
}
|
|
7293
|
+
const simple = SIMPLE_ESCAPES[next];
|
|
7294
|
+
if (simple === void 0) return null;
|
|
7295
|
+
bytes3.push(simple);
|
|
7296
|
+
i += 1;
|
|
7297
|
+
}
|
|
7298
|
+
try {
|
|
7299
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(Uint8Array.from(bytes3));
|
|
7300
|
+
} catch {
|
|
7301
|
+
return null;
|
|
7302
|
+
}
|
|
7303
|
+
}
|
|
7304
|
+
function readTitle(content) {
|
|
7305
|
+
const field2 = TITLE_FIELD.exec(content);
|
|
7306
|
+
if (field2 === null) return null;
|
|
7307
|
+
const title = unescapeTextFormat(field2[1]);
|
|
7308
|
+
return title === null ? null : sessionName(title) ?? null;
|
|
7309
|
+
}
|
|
7310
|
+
function createTitleReader(options) {
|
|
7311
|
+
const { io } = options;
|
|
7312
|
+
const dir = path14.join(options.home, ANNOTATIONS_DIR);
|
|
7313
|
+
const cache = /* @__PURE__ */ new Map();
|
|
7314
|
+
async function read(id) {
|
|
7315
|
+
const file = path14.join(dir, `${id}.pbtxt`);
|
|
7316
|
+
const info = await io.lstat(file);
|
|
7317
|
+
const known = cache.get(id);
|
|
7318
|
+
if (known && known.ino === info.ino && known.size === info.size && known.mtimeMs === info.mtimeMs) {
|
|
7319
|
+
return known.title;
|
|
7320
|
+
}
|
|
7321
|
+
let title = null;
|
|
7322
|
+
if (info.kind === "file" && info.size <= ANNOTATION_LIMIT_BYTES) {
|
|
7323
|
+
const handle = await io.openRegular(file);
|
|
7324
|
+
try {
|
|
7325
|
+
if (handle.info.size <= ANNOTATION_LIMIT_BYTES) {
|
|
7326
|
+
const data = await handle.read(0, handle.info.size);
|
|
7327
|
+
title = readTitle(Buffer.from(data).toString("utf8"));
|
|
7328
|
+
}
|
|
7329
|
+
} finally {
|
|
7330
|
+
await handle.close();
|
|
7331
|
+
}
|
|
7332
|
+
}
|
|
7333
|
+
cache.set(id, { ino: info.ino, size: info.size, mtimeMs: info.mtimeMs, title });
|
|
7334
|
+
return title;
|
|
7335
|
+
}
|
|
7336
|
+
return {
|
|
7337
|
+
async read(id) {
|
|
7338
|
+
try {
|
|
7339
|
+
return await read(id);
|
|
7340
|
+
} catch {
|
|
7341
|
+
cache.delete(id);
|
|
7342
|
+
return null;
|
|
7343
|
+
}
|
|
7344
|
+
},
|
|
7345
|
+
keepOnly(ids) {
|
|
7346
|
+
for (const id of cache.keys()) {
|
|
7347
|
+
if (!ids.has(id)) cache.delete(id);
|
|
7348
|
+
}
|
|
7349
|
+
}
|
|
7350
|
+
};
|
|
7351
|
+
}
|
|
7352
|
+
|
|
7353
|
+
// src/core/mapping/antigravityMapping.ts
|
|
7354
|
+
var ANTIGRAVITY_STEP_TYPES = [
|
|
7355
|
+
"UNSPECIFIED",
|
|
7356
|
+
"DUMMY",
|
|
7357
|
+
"FINISH",
|
|
7358
|
+
"PLAN_INPUT",
|
|
7359
|
+
"MQUERY",
|
|
7360
|
+
"CODE_ACTION",
|
|
7361
|
+
"GIT_COMMIT",
|
|
7362
|
+
"GREP_SEARCH",
|
|
7363
|
+
"VIEW_FILE",
|
|
7364
|
+
"LIST_DIRECTORY",
|
|
7365
|
+
"COMPILE",
|
|
7366
|
+
"VIEW_CODE_ITEM",
|
|
7367
|
+
"USER_INPUT",
|
|
7368
|
+
"PLANNER_RESPONSE",
|
|
7369
|
+
"ERROR_MESSAGE",
|
|
7370
|
+
"RUN_COMMAND",
|
|
7371
|
+
"CHECKPOINT",
|
|
7372
|
+
"PROPOSE_CODE",
|
|
7373
|
+
"FIND",
|
|
7374
|
+
"SUGGESTED_RESPONSES",
|
|
7375
|
+
"COMMAND_STATUS",
|
|
7376
|
+
"MEMORY",
|
|
7377
|
+
"READ_URL_CONTENT",
|
|
7378
|
+
"VIEW_CONTENT_CHUNK",
|
|
7379
|
+
"SEARCH_WEB",
|
|
7380
|
+
"RETRIEVE_MEMORY",
|
|
7381
|
+
"MCP_TOOL",
|
|
7382
|
+
"MANAGER_FEEDBACK",
|
|
7383
|
+
"TOOL_CALL_PROPOSAL",
|
|
7384
|
+
"TOOL_CALL_CHOICE",
|
|
7385
|
+
"TRAJECTORY_CHOICE",
|
|
7386
|
+
"CLIPBOARD",
|
|
7387
|
+
"VIEW_FILE_OUTLINE",
|
|
7388
|
+
"POST_PR_REVIEW",
|
|
7389
|
+
"LIST_RESOURCES",
|
|
7390
|
+
"READ_RESOURCE",
|
|
7391
|
+
"LINT_DIFF",
|
|
7392
|
+
"FIND_ALL_REFERENCES",
|
|
7393
|
+
"BRAIN_UPDATE",
|
|
7394
|
+
"OPEN_BROWSER_URL",
|
|
7395
|
+
"RUN_EXTENSION_CODE",
|
|
7396
|
+
"PROPOSAL_FEEDBACK",
|
|
7397
|
+
"TRAJECTORY_SEARCH",
|
|
7398
|
+
"EXECUTE_BROWSER_JAVASCRIPT",
|
|
7399
|
+
"LIST_BROWSER_PAGES",
|
|
7400
|
+
"CAPTURE_BROWSER_SCREENSHOT",
|
|
7401
|
+
"CLICK_BROWSER_PIXEL",
|
|
7402
|
+
"READ_TERMINAL",
|
|
7403
|
+
"CAPTURE_BROWSER_CONSOLE_LOGS",
|
|
7404
|
+
"READ_BROWSER_PAGE",
|
|
7405
|
+
"BROWSER_GET_DOM",
|
|
7406
|
+
"CODE_SEARCH",
|
|
7407
|
+
"BROWSER_INPUT",
|
|
7408
|
+
"BROWSER_MOVE_MOUSE",
|
|
7409
|
+
"BROWSER_SELECT_OPTION",
|
|
7410
|
+
"BROWSER_SCROLL_UP",
|
|
7411
|
+
"BROWSER_SCROLL_DOWN",
|
|
7412
|
+
"BROWSER_CLICK_ELEMENT",
|
|
7413
|
+
"BROWSER_PRESS_KEY",
|
|
7414
|
+
"TASK_BOUNDARY",
|
|
7415
|
+
"NOTIFY_USER",
|
|
7416
|
+
"CODE_ACKNOWLEDGEMENT",
|
|
7417
|
+
"INTERNAL_SEARCH",
|
|
7418
|
+
"BROWSER_SUBAGENT",
|
|
7419
|
+
"FILE_CHANGE",
|
|
7420
|
+
"MOVE",
|
|
7421
|
+
"BROWSER_SCROLL",
|
|
7422
|
+
"KNOWLEDGE_GENERATION",
|
|
7423
|
+
"EPHEMERAL_MESSAGE",
|
|
7424
|
+
"GENERATE_IMAGE",
|
|
7425
|
+
"DELETE_DIRECTORY",
|
|
7426
|
+
"COMPILE_APPLET",
|
|
7427
|
+
"INSTALL_APPLET_DEPENDENCIES",
|
|
7428
|
+
"INSTALL_APPLET_PACKAGE",
|
|
7429
|
+
"BROWSER_RESIZE_WINDOW",
|
|
7430
|
+
"BROWSER_DRAG_PIXEL_TO_PIXEL",
|
|
7431
|
+
"CONVERSATION_HISTORY",
|
|
7432
|
+
"KNOWLEDGE_ARTIFACTS",
|
|
7433
|
+
"SEND_COMMAND_INPUT",
|
|
7434
|
+
"SYSTEM_MESSAGE",
|
|
7435
|
+
"WAIT",
|
|
7436
|
+
"AGENCY_TOOL_CALL",
|
|
7437
|
+
"CIDER_AGENT_DUMMY",
|
|
7438
|
+
"BUILD_CLEANER",
|
|
7439
|
+
"BLAZE_BUILD_TARGETS",
|
|
7440
|
+
"BLAZE_TEST_TARGETS",
|
|
7441
|
+
"SET_UP_FIREBASE",
|
|
7442
|
+
"MOMA",
|
|
7443
|
+
"RESTART_DEV_SERVER",
|
|
7444
|
+
"DEPLOY_FIREBASE",
|
|
7445
|
+
"SHELL_EXEC",
|
|
7446
|
+
"BROWSER_MOUSE_WHEEL",
|
|
7447
|
+
"LINT_APPLET",
|
|
7448
|
+
"KI_INSERTION",
|
|
7449
|
+
"RETRIEVE_CONTENT",
|
|
7450
|
+
"CRITIQUE",
|
|
7451
|
+
"FINDINGS",
|
|
7452
|
+
"BROWSER_MOUSE_UP",
|
|
7453
|
+
"BROWSER_MOUSE_DOWN",
|
|
7454
|
+
"WORKSPACE_API",
|
|
7455
|
+
"BROWSER_LIST_NETWORK_REQUESTS",
|
|
7456
|
+
"BROWSER_GET_NETWORK_REQUEST",
|
|
7457
|
+
"BROWSER_REFRESH_PAGE",
|
|
7458
|
+
"EDIT_NOTEBOOK",
|
|
7459
|
+
"INVOKE_SUBAGENT",
|
|
7460
|
+
"WRITE_BLOB",
|
|
7461
|
+
"READ_NOTEBOOK",
|
|
7462
|
+
"PROPOSE_AI_COMMENTS",
|
|
7463
|
+
"START_CODE_REVIEW",
|
|
7464
|
+
"GENERIC",
|
|
7465
|
+
"SET_UP_CLOUD_SQL",
|
|
7466
|
+
"EXECUTE_NOTEBOOK",
|
|
7467
|
+
"CLOUD_SQL_UPDATE_SCHEMA",
|
|
7468
|
+
"RPC_ACTION",
|
|
7469
|
+
"CLOUD_SQL_EXECUTE_SQL",
|
|
7470
|
+
"ASK_QUESTION",
|
|
7471
|
+
"DIRECTORY_RULES",
|
|
7472
|
+
"TOOL_SEARCH"
|
|
7473
|
+
];
|
|
7474
|
+
var ANTIGRAVITY_STEP_STATUSES = [
|
|
7475
|
+
"UNSPECIFIED",
|
|
7476
|
+
"PENDING",
|
|
7477
|
+
"RUNNING",
|
|
7478
|
+
"DONE",
|
|
7479
|
+
"INVALID",
|
|
7480
|
+
"CLEARED",
|
|
7481
|
+
"CANCELED",
|
|
7482
|
+
"ERROR",
|
|
7483
|
+
"GENERATING",
|
|
7484
|
+
"WAITING",
|
|
7485
|
+
"QUEUED",
|
|
7486
|
+
"INTERRUPTED"
|
|
7487
|
+
];
|
|
7488
|
+
var KNOWN_TYPES = new Set(ANTIGRAVITY_STEP_TYPES);
|
|
7489
|
+
var KNOWN_STATUSES = new Set(ANTIGRAVITY_STEP_STATUSES);
|
|
7490
|
+
var TYPE_PREFIX = "CORTEX_STEP_TYPE_";
|
|
7491
|
+
var STATUS_PREFIX = "CORTEX_STEP_STATUS_";
|
|
7492
|
+
var NAME = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
7493
|
+
function nameOf(value, prefix) {
|
|
7494
|
+
if (typeof value !== "string") return null;
|
|
7495
|
+
const upper = value.trim().toUpperCase();
|
|
7496
|
+
const name = upper.startsWith(prefix) ? upper.slice(prefix.length) : upper;
|
|
7497
|
+
return NAME.test(name) ? name : null;
|
|
7498
|
+
}
|
|
7499
|
+
function stepTypeName(value) {
|
|
7500
|
+
return nameOf(value, TYPE_PREFIX);
|
|
7501
|
+
}
|
|
7502
|
+
function stepStatusName(value) {
|
|
7503
|
+
return nameOf(value, STATUS_PREFIX);
|
|
7504
|
+
}
|
|
7505
|
+
var ASIDE_TYPES = /* @__PURE__ */ new Set([
|
|
7506
|
+
"DUMMY",
|
|
7507
|
+
"CIDER_AGENT_DUMMY",
|
|
7508
|
+
"CHECKPOINT",
|
|
7509
|
+
"EPHEMERAL_MESSAGE",
|
|
7510
|
+
"CONVERSATION_HISTORY",
|
|
7511
|
+
"SYSTEM_MESSAGE",
|
|
7512
|
+
"SUGGESTED_RESPONSES",
|
|
7513
|
+
"KNOWLEDGE_ARTIFACTS",
|
|
7514
|
+
"KNOWLEDGE_GENERATION",
|
|
7515
|
+
"KI_INSERTION",
|
|
7516
|
+
"DIRECTORY_RULES",
|
|
7517
|
+
"BRAIN_UPDATE"
|
|
7518
|
+
]);
|
|
7519
|
+
var GONE_STATUSES = /* @__PURE__ */ new Set(["CLEARED", "INVALID"]);
|
|
7520
|
+
var UNDER_WAY = /* @__PURE__ */ new Set([
|
|
7521
|
+
"PENDING",
|
|
7522
|
+
"RUNNING",
|
|
7523
|
+
"GENERATING",
|
|
7524
|
+
"QUEUED",
|
|
7525
|
+
"WAITING"
|
|
7526
|
+
]);
|
|
7527
|
+
var STOPPED = /* @__PURE__ */ new Set(["CANCELED", "INTERRUPTED"]);
|
|
7528
|
+
var TURN_OVER = /* @__PURE__ */ new Set(["FINISH"]);
|
|
7529
|
+
function isAsideStep(step) {
|
|
7530
|
+
return step.type !== null && ASIDE_TYPES.has(step.type) || step.status !== null && GONE_STATUSES.has(step.status);
|
|
7531
|
+
}
|
|
7532
|
+
function openStatusOf(step) {
|
|
7533
|
+
const { type, status } = step;
|
|
7534
|
+
if (type === "ERROR_MESSAGE") return "failed";
|
|
7535
|
+
if (status === null || !KNOWN_STATUSES.has(status)) return "unknown";
|
|
7536
|
+
if (UNDER_WAY.has(status)) return "working";
|
|
7537
|
+
if (STOPPED.has(status)) return "idle";
|
|
7538
|
+
if (type === null || !KNOWN_TYPES.has(type) || type === "UNSPECIFIED") return "unknown";
|
|
7539
|
+
if (status === "ERROR") return type === "PLANNER_RESPONSE" ? "failed" : "working";
|
|
7540
|
+
if (status !== "DONE") return "unknown";
|
|
7541
|
+
if (type === "PLANNER_RESPONSE") return step.toolCalls ? "working" : "idle";
|
|
7542
|
+
if (TURN_OVER.has(type)) return "idle";
|
|
7543
|
+
return "working";
|
|
7544
|
+
}
|
|
7545
|
+
function mapAntigravityStatus(fields) {
|
|
7546
|
+
const { last, live: live2 } = fields;
|
|
7547
|
+
if (last === void 0) return "unknown";
|
|
7548
|
+
const open2 = last === null ? "idle" : openStatusOf(last);
|
|
7549
|
+
if (open2 === "failed" || open2 === "unknown") return open2;
|
|
7550
|
+
if (live2 === false) return "finished";
|
|
7551
|
+
return fields.asking === true ? "needs-you" : open2;
|
|
7552
|
+
}
|
|
7553
|
+
|
|
7554
|
+
// src/collector/adapters/antigravity/toSession.ts
|
|
7555
|
+
var SOURCE_ID2 = "antigravity-cli";
|
|
7556
|
+
var WRITE_ORDER_SLACK_MS2 = 1e3;
|
|
7557
|
+
function antigravitySession(input) {
|
|
7558
|
+
const { conversationId, state, live: live2, now } = input;
|
|
7559
|
+
const id = `${SOURCE_ID2}:${conversationId}`;
|
|
7560
|
+
const waiting = input.waiting ?? null;
|
|
7561
|
+
const status = mapAntigravityStatus({ last: state.last, live: live2, asking: waiting !== null });
|
|
7562
|
+
const startedAt = plausibleTime(state.firstAt, now);
|
|
7563
|
+
let statusSince = null;
|
|
7564
|
+
if (status === "working" || status === "idle" || status === "failed") {
|
|
7565
|
+
statusSince = plausibleTime(state.since, now);
|
|
7566
|
+
} else if (status === "needs-you") {
|
|
7567
|
+
statusSince = plausibleTime(waiting?.since ?? null, now) ?? plausibleTime(state.lastAt, now);
|
|
7568
|
+
} else if (status === "finished") {
|
|
7569
|
+
statusSince = plausibleTime(state.lastAt, now);
|
|
7570
|
+
}
|
|
7571
|
+
if (statusSince !== null && startedAt !== null && statusSince < startedAt - WRITE_ORDER_SLACK_MS2) {
|
|
7572
|
+
statusSince = null;
|
|
7573
|
+
}
|
|
7574
|
+
const cwd = input.folder ?? null;
|
|
7575
|
+
const project = projectOf(cwd);
|
|
7576
|
+
const session = {
|
|
7577
|
+
id,
|
|
7578
|
+
source: SOURCE_ID2,
|
|
7579
|
+
surface: "terminal",
|
|
7580
|
+
name: input.title ?? project ?? sessionName(conversationId) ?? id,
|
|
7581
|
+
cwd,
|
|
7582
|
+
project,
|
|
7583
|
+
status,
|
|
7584
|
+
startedAt,
|
|
7585
|
+
statusSince,
|
|
7586
|
+
links: {},
|
|
7587
|
+
stale: isStale({ status, statusSince }, now)
|
|
7588
|
+
};
|
|
7589
|
+
if (status === "needs-you" && waiting !== null) {
|
|
7590
|
+
session.waitingReason = "permission";
|
|
7591
|
+
session.waitingDetail = waiting.tool;
|
|
7592
|
+
}
|
|
7593
|
+
const writtenAt = plausibleTime(input.writtenAt, now);
|
|
7594
|
+
const beforeStart = writtenAt !== null && startedAt !== null && writtenAt < startedAt - WRITE_ORDER_SLACK_MS2;
|
|
7595
|
+
if (writtenAt !== null && !beforeStart) session.lastWriteAt = writtenAt;
|
|
7596
|
+
return session;
|
|
7597
|
+
}
|
|
7598
|
+
|
|
7599
|
+
// src/collector/adapters/antigravity/transcriptFile.ts
|
|
7600
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
7601
|
+
var HEAD_LIMIT_BYTES = 256 * 1024;
|
|
7602
|
+
var TAIL_LIMIT_BYTES = 2 * 1024 * 1024;
|
|
7603
|
+
var TAIL_CHUNK_BYTES = 64 * 1024;
|
|
7604
|
+
var HEAD_HASH_BYTES = 4 * 1024;
|
|
7605
|
+
var NEWLINE2 = 10;
|
|
7606
|
+
var ISO_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
7607
|
+
function parseStepTime(value) {
|
|
7608
|
+
if (typeof value !== "string" || !ISO_TIME.test(value)) return null;
|
|
7609
|
+
const at = Date.parse(value);
|
|
7610
|
+
return Number.isSafeInteger(at) ? at : null;
|
|
7611
|
+
}
|
|
7612
|
+
function isRecord8(value) {
|
|
7613
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7614
|
+
}
|
|
7615
|
+
function readStepLine(line) {
|
|
7616
|
+
let value;
|
|
7617
|
+
try {
|
|
7618
|
+
value = JSON.parse(line);
|
|
7619
|
+
} catch {
|
|
7620
|
+
return null;
|
|
7621
|
+
}
|
|
7622
|
+
if (!isRecord8(value)) return null;
|
|
7623
|
+
const type = stepTypeName(value.type);
|
|
7624
|
+
const status = stepStatusName(value.status);
|
|
7625
|
+
if (type === null && status === null) return null;
|
|
7626
|
+
const index = value.step_index;
|
|
7627
|
+
return {
|
|
7628
|
+
index: Number.isSafeInteger(index) && index >= 0 ? index : null,
|
|
7629
|
+
type,
|
|
7630
|
+
status,
|
|
7631
|
+
toolCalls: Array.isArray(value.tool_calls) && value.tool_calls.length > 0,
|
|
7632
|
+
at: parseStepTime(value.created_at)
|
|
7633
|
+
};
|
|
7634
|
+
}
|
|
7635
|
+
function emptyState() {
|
|
7636
|
+
return { firstAt: null, last: void 0, since: null, lastAt: null, lastIndex: null };
|
|
7637
|
+
}
|
|
7638
|
+
function countsForIndex(step) {
|
|
7639
|
+
return step.index !== null && step.status !== "CLEARED" && step.status !== "INVALID";
|
|
7640
|
+
}
|
|
7641
|
+
function addStep(state, step) {
|
|
7642
|
+
if (step.at !== null) state.lastAt = step.at;
|
|
7643
|
+
if (countsForIndex(step)) state.lastIndex = step.index;
|
|
7644
|
+
if (isAsideStep(step)) return;
|
|
7645
|
+
const was = state.last ? openStatusOf(state.last) : null;
|
|
7646
|
+
if (was !== openStatusOf(step)) state.since = step.at;
|
|
7647
|
+
state.last = step;
|
|
7648
|
+
}
|
|
7649
|
+
function bytes(data) {
|
|
7650
|
+
return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
7651
|
+
}
|
|
7652
|
+
async function readExactly(file, position, length) {
|
|
7653
|
+
const data = bytes(await file.read(position, length));
|
|
7654
|
+
if (data.byteLength !== length) throw new Error("The file changed while it was read.");
|
|
7655
|
+
return data;
|
|
7656
|
+
}
|
|
7657
|
+
async function headHash(file, size) {
|
|
7658
|
+
const head = await readExactly(file, 0, Math.min(HEAD_HASH_BYTES, size));
|
|
7659
|
+
return createHash2("sha256").update(head).digest("hex");
|
|
7660
|
+
}
|
|
7661
|
+
async function readFirstAt(file, size) {
|
|
7662
|
+
const head = await readExactly(file, 0, Math.min(HEAD_LIMIT_BYTES, size));
|
|
7663
|
+
const end = head.indexOf(NEWLINE2);
|
|
7664
|
+
if (end === -1 && head.byteLength < size) return null;
|
|
7665
|
+
const step = readStepLine(head.toString("utf8", 0, end === -1 ? head.byteLength : end));
|
|
7666
|
+
return step?.index === 0 ? step.at : null;
|
|
7667
|
+
}
|
|
7668
|
+
async function scanTail(file, size) {
|
|
7669
|
+
const state = emptyState();
|
|
7670
|
+
let position = size;
|
|
7671
|
+
let budget2 = TAIL_LIMIT_BYTES;
|
|
7672
|
+
let fragment = Buffer.alloc(0);
|
|
7673
|
+
let end = null;
|
|
7674
|
+
let settled = false;
|
|
7675
|
+
const take = (step) => {
|
|
7676
|
+
if (step === null) return false;
|
|
7677
|
+
if (state.lastAt === null && step.at !== null) state.lastAt = step.at;
|
|
7678
|
+
if (state.lastIndex === null && countsForIndex(step)) state.lastIndex = step.index;
|
|
7679
|
+
if (isAsideStep(step)) return false;
|
|
7680
|
+
if (state.last === void 0 || state.last === null) {
|
|
7681
|
+
state.last = step;
|
|
7682
|
+
state.since = step.at;
|
|
7683
|
+
return false;
|
|
7684
|
+
}
|
|
7685
|
+
if (openStatusOf(step) !== openStatusOf(state.last)) return true;
|
|
7686
|
+
if (step.at !== null) state.since = step.at;
|
|
7687
|
+
return false;
|
|
7688
|
+
};
|
|
7689
|
+
while (position > 0 && budget2 > 0 && !settled) {
|
|
7690
|
+
const length = Math.min(TAIL_CHUNK_BYTES, position, budget2);
|
|
7691
|
+
const start = position - length;
|
|
7692
|
+
const chunk = await readExactly(file, start, length);
|
|
7693
|
+
budget2 -= length;
|
|
7694
|
+
position = start;
|
|
7695
|
+
const buffer = fragment.byteLength > 0 ? Buffer.concat([chunk, fragment]) : chunk;
|
|
7696
|
+
let cut2 = buffer.byteLength;
|
|
7697
|
+
if (end === null) {
|
|
7698
|
+
const last = buffer.lastIndexOf(NEWLINE2);
|
|
7699
|
+
if (last === -1) {
|
|
7700
|
+
fragment = buffer;
|
|
7701
|
+
continue;
|
|
7702
|
+
}
|
|
7703
|
+
end = start + last + 1;
|
|
7704
|
+
cut2 = last;
|
|
7705
|
+
if (last + 1 < buffer.byteLength) {
|
|
7706
|
+
settled = take(readStepLine(buffer.toString("utf8", last + 1)));
|
|
7707
|
+
}
|
|
7708
|
+
}
|
|
7709
|
+
let newline = cut2 > 0 ? buffer.lastIndexOf(NEWLINE2, cut2 - 1) : -1;
|
|
7710
|
+
while (newline !== -1 && !settled) {
|
|
7711
|
+
settled = take(readStepLine(buffer.toString("utf8", newline + 1, cut2)));
|
|
7712
|
+
cut2 = newline;
|
|
7713
|
+
newline = cut2 > 0 ? buffer.lastIndexOf(NEWLINE2, cut2 - 1) : -1;
|
|
7714
|
+
}
|
|
7715
|
+
if (settled) break;
|
|
7716
|
+
if (start === 0) {
|
|
7717
|
+
take(readStepLine(buffer.toString("utf8", 0, cut2)));
|
|
7718
|
+
if (state.last === void 0) state.last = null;
|
|
7719
|
+
return { state, end };
|
|
7720
|
+
}
|
|
7721
|
+
fragment = buffer.subarray(0, cut2);
|
|
7722
|
+
}
|
|
7723
|
+
if (!settled && position === 0) {
|
|
7724
|
+
if (end === null) {
|
|
7725
|
+
take(readStepLine(fragment.toString("utf8")));
|
|
7726
|
+
end = 0;
|
|
7727
|
+
}
|
|
7728
|
+
if (state.last === void 0) state.last = null;
|
|
7729
|
+
return { state, end };
|
|
7730
|
+
}
|
|
7731
|
+
if (!settled && state.last) state.since = null;
|
|
7732
|
+
return { state, end };
|
|
7733
|
+
}
|
|
7734
|
+
function stateOf(cached) {
|
|
7735
|
+
return {
|
|
7736
|
+
firstAt: cached.firstAt,
|
|
7737
|
+
last: cached.last,
|
|
7738
|
+
since: cached.since,
|
|
7739
|
+
lastAt: cached.lastAt,
|
|
7740
|
+
lastIndex: cached.lastIndex
|
|
7741
|
+
};
|
|
7742
|
+
}
|
|
7743
|
+
function createTranscriptReader(io) {
|
|
7744
|
+
const cache = /* @__PURE__ */ new Map();
|
|
7745
|
+
async function readFresh(file, earlier) {
|
|
7746
|
+
const handle = await io.openRegular(file);
|
|
7747
|
+
try {
|
|
7748
|
+
const { info } = handle;
|
|
7749
|
+
const firstAt = await readFirstAt(handle, info.size);
|
|
7750
|
+
const tail = await scanTail(handle, info.size);
|
|
7751
|
+
return {
|
|
7752
|
+
...tail.state,
|
|
7753
|
+
// A compacted file may begin later than the conversation did: the earliest seen stands.
|
|
7754
|
+
firstAt: earlier?.firstAt != null && (firstAt === null || earlier.firstAt < firstAt) ? earlier.firstAt : firstAt,
|
|
7755
|
+
ino: info.ino,
|
|
7756
|
+
size: info.size,
|
|
7757
|
+
mtimeMs: info.mtimeMs,
|
|
7758
|
+
headHash: await headHash(handle, info.size),
|
|
7759
|
+
offset: tail.end
|
|
7760
|
+
};
|
|
7761
|
+
} finally {
|
|
7762
|
+
await handle.close();
|
|
7763
|
+
}
|
|
7764
|
+
}
|
|
7765
|
+
async function readOnward(file, cached, info) {
|
|
7766
|
+
const offset = cached.offset;
|
|
7767
|
+
if (offset === null) return null;
|
|
7768
|
+
if (info.ino !== cached.ino || info.size <= cached.size || info.size < offset) return null;
|
|
7769
|
+
if (info.size - offset > TAIL_LIMIT_BYTES) return null;
|
|
7770
|
+
const handle = await io.openRegular(file);
|
|
7771
|
+
try {
|
|
7772
|
+
const opened = handle.info;
|
|
7773
|
+
if (opened.ino !== cached.ino || opened.size < offset || opened.size < cached.size) {
|
|
7774
|
+
return null;
|
|
7775
|
+
}
|
|
7776
|
+
if (opened.size - offset > TAIL_LIMIT_BYTES) return null;
|
|
7777
|
+
if (await headHash(handle, cached.size) !== cached.headHash) return null;
|
|
7778
|
+
const from = offset > 0 ? offset - 1 : 0;
|
|
7779
|
+
const read = await readExactly(handle, from, opened.size - from);
|
|
7780
|
+
if (offset > 0 && read[0] !== NEWLINE2) return null;
|
|
7781
|
+
const added = offset > 0 ? read.subarray(1) : read;
|
|
7782
|
+
const next = {
|
|
7783
|
+
...cached,
|
|
7784
|
+
ino: opened.ino,
|
|
7785
|
+
size: opened.size,
|
|
7786
|
+
mtimeMs: opened.mtimeMs,
|
|
7787
|
+
headHash: await headHash(handle, opened.size)
|
|
7788
|
+
};
|
|
7789
|
+
let start = 0;
|
|
7790
|
+
let newline = added.indexOf(NEWLINE2);
|
|
7791
|
+
while (newline !== -1) {
|
|
7792
|
+
const step = readStepLine(added.toString("utf8", start, newline));
|
|
7793
|
+
if (step) addStep(next, step);
|
|
7794
|
+
start = newline + 1;
|
|
7795
|
+
newline = added.indexOf(NEWLINE2, start);
|
|
7796
|
+
}
|
|
7797
|
+
if (start < added.byteLength) {
|
|
7798
|
+
const step = readStepLine(added.toString("utf8", start));
|
|
7799
|
+
if (step) addStep(next, step);
|
|
7800
|
+
}
|
|
7801
|
+
next.offset = offset + start;
|
|
7802
|
+
if (next.firstAt === null && offset === 0) {
|
|
7803
|
+
next.firstAt = await readFirstAt(handle, opened.size);
|
|
7804
|
+
}
|
|
7805
|
+
return next;
|
|
7806
|
+
} finally {
|
|
7807
|
+
await handle.close();
|
|
7808
|
+
}
|
|
7809
|
+
}
|
|
7810
|
+
return {
|
|
7811
|
+
async read(file, info) {
|
|
7812
|
+
const cached = cache.get(file);
|
|
7813
|
+
if (cached && cached.ino === info.ino && cached.size === info.size && cached.mtimeMs === info.mtimeMs) {
|
|
7814
|
+
return stateOf(cached);
|
|
7815
|
+
}
|
|
7816
|
+
const next = cached && await readOnward(file, cached, info) || await readFresh(file, cached);
|
|
7817
|
+
cache.set(file, next);
|
|
7818
|
+
return stateOf(next);
|
|
7819
|
+
},
|
|
7820
|
+
keepOnly(files) {
|
|
7821
|
+
for (const file of cache.keys()) {
|
|
7822
|
+
if (!files.has(file)) cache.delete(file);
|
|
7823
|
+
}
|
|
7824
|
+
}
|
|
7825
|
+
};
|
|
7826
|
+
}
|
|
7827
|
+
|
|
7828
|
+
// src/collector/adapters/antigravity/index.ts
|
|
7829
|
+
var ANTIGRAVITY_HOME_ENV = "AGENT_LOOKOUT_ANTIGRAVITY_HOME";
|
|
7830
|
+
var DEFAULT_HOME = [".gemini", "antigravity-cli"];
|
|
7831
|
+
var RECHECK_MS = 6e4;
|
|
7832
|
+
var PROCESS_CHECK_MS = 1e4;
|
|
7833
|
+
var PROMPT_PROCESS_CHECK_MS = 2e3;
|
|
7834
|
+
var BASIS = "files";
|
|
7835
|
+
var BASIS_WITHOUT_PROCESSES = "files-without-processes";
|
|
7836
|
+
var BASIS_UNMATCHED = "files-unmatched";
|
|
7837
|
+
var LABEL2 = "Antigravity CLI";
|
|
7838
|
+
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.";
|
|
7839
|
+
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.";
|
|
7840
|
+
var ALWAYS_NOTES = `${NEEDS_YOU_NOTE} ${UNCHECKED_NOTE}`;
|
|
7841
|
+
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.";
|
|
7842
|
+
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.";
|
|
7843
|
+
var ANTIGRAVITY_CAPABILITIES = {
|
|
7844
|
+
"working-and-idle": { level: "yes" },
|
|
7845
|
+
"needs-you": {
|
|
7846
|
+
level: "partly",
|
|
7847
|
+
reason: "While agy asks you to approve a tool, from its own log. A question it asks you shows as working."
|
|
7848
|
+
},
|
|
7849
|
+
finished: {
|
|
7850
|
+
level: "partly",
|
|
7851
|
+
reason: "Once ps shows no agy program running that could have the conversation open. Not on Windows."
|
|
7852
|
+
},
|
|
7853
|
+
failed: {
|
|
7854
|
+
level: "partly",
|
|
7855
|
+
reason: "Only when agy records an error of its own or a failed reply. A tool that fails shows as working."
|
|
7856
|
+
},
|
|
7857
|
+
names: {
|
|
7858
|
+
level: "partly",
|
|
7859
|
+
reason: "The title agy gives a conversation once it has one, and the folder its program's log names. Until then, its conversation ID."
|
|
7860
|
+
},
|
|
7861
|
+
jump: {
|
|
7862
|
+
level: "no",
|
|
7863
|
+
reason: "The transcripts name no terminal, and agy documents no link that opens a conversation."
|
|
7864
|
+
},
|
|
7865
|
+
"quiet-for": { level: "yes" },
|
|
7866
|
+
stop: {
|
|
7867
|
+
level: "no",
|
|
7868
|
+
reason: "No agy program is tied to one conversation surely enough for Agent Lookout to confirm it and stop it."
|
|
7869
|
+
},
|
|
7870
|
+
answer: {
|
|
7871
|
+
level: "no",
|
|
7872
|
+
reason: "Its approval prompt takes an answer only in the terminal agy runs in."
|
|
7873
|
+
}
|
|
7874
|
+
};
|
|
7875
|
+
function withLoggedConversations(programs, logStates) {
|
|
7876
|
+
return programs.map((program, index) => {
|
|
7877
|
+
const current = logStates[index]?.current ?? null;
|
|
7878
|
+
return current === null ? program : { ...program, conversation: current };
|
|
7879
|
+
});
|
|
7880
|
+
}
|
|
7881
|
+
function folderOf(id, logs) {
|
|
7882
|
+
const open2 = logs.find((log) => log.current === id && log.folder !== null);
|
|
7883
|
+
if (open2) return open2.folder;
|
|
7884
|
+
return logs.find((log) => log.opened.has(id) && log.folder !== null)?.folder ?? null;
|
|
7885
|
+
}
|
|
7886
|
+
function createAntigravityAdapter(options = {}) {
|
|
7887
|
+
const env = options.env ?? process.env;
|
|
7888
|
+
const homeDir = options.homeDir ?? os5.homedir();
|
|
7889
|
+
const now = options.now ?? Date.now;
|
|
7890
|
+
const io = options.io ?? nodeIo2;
|
|
7891
|
+
const platform = options.platform ?? process.platform;
|
|
7892
|
+
const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
7893
|
+
const recheckMs = options.recheckMs ?? RECHECK_MS;
|
|
7894
|
+
const processCheckMs = options.processCheckMs ?? PROCESS_CHECK_MS;
|
|
7895
|
+
const processes = options.processes ?? createAgyProcessReader({ platform });
|
|
7896
|
+
const override = env[ANTIGRAVITY_HOME_ENV]?.trim() || void 0;
|
|
7897
|
+
const homeFrom = override ? "agent-lookout" : "default";
|
|
7898
|
+
const home = path15.resolve(override ?? path15.join(homeDir, ...DEFAULT_HOME));
|
|
7899
|
+
const brainDir = path15.join(home, BRAIN_DIR);
|
|
7900
|
+
const homeName = tildify(home, homeDir);
|
|
7901
|
+
const brainName = tildify(brainDir, homeDir);
|
|
7902
|
+
const finder = createConversationFinder({ home, io, oldRefreshMs: options.oldRefreshMs });
|
|
7903
|
+
const reader = createTranscriptReader(io);
|
|
7904
|
+
const logs = createAgyLogReader({ home, io, now });
|
|
7905
|
+
const titles = createTitleReader({ home, io });
|
|
7906
|
+
let absentAt = null;
|
|
7907
|
+
let homeSeen = false;
|
|
7908
|
+
let lastProcesses = null;
|
|
7909
|
+
let processesAt = null;
|
|
7910
|
+
let lastHolding = null;
|
|
7911
|
+
const commandRun = platform === "win32" ? "not run on Windows" : `${every(processCheckMs)} at most`;
|
|
7912
|
+
function watching(read) {
|
|
7913
|
+
return [
|
|
7914
|
+
{ label: "Conversations folder", value: brainName },
|
|
7915
|
+
{ label: "Read", value: read },
|
|
7916
|
+
{ label: "Command", value: `ps ${AGY_TABLE_ARGS.join(" ")}` },
|
|
7917
|
+
{ label: "Command run", value: commandRun }
|
|
7918
|
+
];
|
|
7919
|
+
}
|
|
7920
|
+
function result(state, detail, read, checkedAt, basis = BASIS) {
|
|
7921
|
+
const health = {
|
|
7922
|
+
id: SOURCE_ID2,
|
|
7923
|
+
label: LABEL2,
|
|
7924
|
+
state,
|
|
7925
|
+
detail,
|
|
7926
|
+
watching: watching(read),
|
|
7927
|
+
checkedAt
|
|
7928
|
+
};
|
|
7929
|
+
return state === "ok" ? { health, sessions: [], basis } : { health, sessions: [] };
|
|
7930
|
+
}
|
|
7931
|
+
function notFound(checkedAt) {
|
|
7932
|
+
lastProcesses = null;
|
|
7933
|
+
processesAt = null;
|
|
7934
|
+
lastHolding = null;
|
|
7935
|
+
return result(
|
|
7936
|
+
"unavailable",
|
|
7937
|
+
`The Antigravity CLI was not found: there is no ${homeName} folder. Agent Lookout looks again ${every(recheckMs)}.`,
|
|
7938
|
+
"not found",
|
|
7939
|
+
checkedAt
|
|
7940
|
+
);
|
|
7941
|
+
}
|
|
7942
|
+
async function homeIsThere() {
|
|
7943
|
+
try {
|
|
7944
|
+
return (await io.stat(home)).kind === "directory";
|
|
7945
|
+
} catch (error) {
|
|
7946
|
+
return !isMissing2(error);
|
|
7947
|
+
}
|
|
7948
|
+
}
|
|
7949
|
+
async function processesFor(conversations, checkedAt) {
|
|
7950
|
+
const shown = conversations.some(
|
|
7951
|
+
(files) => writtenRecently(files, checkedAt) || lastHolding?.held.has(files.id)
|
|
7952
|
+
);
|
|
7953
|
+
if (!shown) return null;
|
|
7954
|
+
const since = processesAt === null ? Infinity : checkedAt - processesAt;
|
|
7955
|
+
let due = since < 0 || since >= processCheckMs;
|
|
7956
|
+
if (!due && since >= PROMPT_PROCESS_CHECK_MS && lastProcesses?.ok) {
|
|
7957
|
+
const holding = holdConversations(lastProcesses.sessions, conversations);
|
|
7958
|
+
due = conversations.some(
|
|
7959
|
+
(files) => files.writtenAt !== null && files.writtenAt > processesAt && !holding.held.has(files.id)
|
|
7960
|
+
);
|
|
7961
|
+
}
|
|
7962
|
+
if (due) {
|
|
7963
|
+
lastProcesses = await processes.read();
|
|
7964
|
+
processesAt = checkedAt;
|
|
7965
|
+
}
|
|
7966
|
+
return lastProcesses;
|
|
7967
|
+
}
|
|
7968
|
+
async function poll() {
|
|
7969
|
+
const checkedAt = now();
|
|
7970
|
+
if (homeFrom !== "agent-lookout") {
|
|
7971
|
+
if (absentAt !== null) {
|
|
7972
|
+
const since = checkedAt - absentAt;
|
|
7973
|
+
if (since >= 0 && since < recheckMs) return notFound(checkedAt);
|
|
7974
|
+
}
|
|
7975
|
+
if (!homeSeen) {
|
|
7976
|
+
if (!await homeIsThere()) {
|
|
7977
|
+
absentAt = checkedAt;
|
|
7978
|
+
return notFound(checkedAt);
|
|
7979
|
+
}
|
|
7980
|
+
absentAt = null;
|
|
7981
|
+
homeSeen = true;
|
|
7982
|
+
}
|
|
7983
|
+
}
|
|
7984
|
+
const listing = await finder.list(checkedAt, (id) => lastHolding?.held.has(id) ?? false);
|
|
7985
|
+
if (listing.state === "unreadable") {
|
|
7986
|
+
return result(
|
|
7987
|
+
"error",
|
|
7988
|
+
`Antigravity CLI conversations could not be read: the folder ${brainName} could not be listed.`,
|
|
7989
|
+
"cannot be read",
|
|
7990
|
+
checkedAt
|
|
7991
|
+
);
|
|
7992
|
+
}
|
|
7993
|
+
if (listing.state === "missing") {
|
|
7994
|
+
lastHolding = null;
|
|
7995
|
+
if (homeFrom === "agent-lookout") {
|
|
7996
|
+
return result(
|
|
7997
|
+
"ok",
|
|
7998
|
+
`${ANTIGRAVITY_HOME_ENV} is set to ${homeName}, which has no ${BRAIN_DIR} folder, so no Antigravity CLI sessions are listed. ${ALWAYS_NOTES}`,
|
|
7999
|
+
"not found",
|
|
8000
|
+
checkedAt
|
|
8001
|
+
);
|
|
8002
|
+
}
|
|
8003
|
+
homeSeen = false;
|
|
8004
|
+
if (!await homeIsThere()) {
|
|
8005
|
+
absentAt = checkedAt;
|
|
8006
|
+
return notFound(checkedAt);
|
|
8007
|
+
}
|
|
8008
|
+
homeSeen = true;
|
|
8009
|
+
return result(
|
|
8010
|
+
"ok",
|
|
8011
|
+
`The Antigravity CLI has not saved any conversations in ${brainName} yet. ${ALWAYS_NOTES}`,
|
|
8012
|
+
"not found",
|
|
8013
|
+
checkedAt
|
|
8014
|
+
);
|
|
8015
|
+
}
|
|
8016
|
+
const { conversations } = listing;
|
|
8017
|
+
const list = await processesFor(conversations, checkedAt);
|
|
8018
|
+
const logStates = list?.ok ? await logs.read(list.sessions) : [];
|
|
8019
|
+
const programs = list?.ok ? withLoggedConversations(list.sessions, logStates) : [];
|
|
8020
|
+
const holding = list?.ok ? holdConversations(programs, conversations, processesAt) : null;
|
|
8021
|
+
lastHolding = holding;
|
|
8022
|
+
const logged = logStates.filter((state) => state !== null);
|
|
8023
|
+
const read = /* @__PURE__ */ new Set();
|
|
8024
|
+
const sessions = [];
|
|
8025
|
+
let unknownStep = false;
|
|
8026
|
+
for (const files of conversations) {
|
|
8027
|
+
const open2 = holding?.held.has(files.id) ?? false;
|
|
8028
|
+
if (files.transcriptInfo === null || !(open2 || writtenRecently(files, checkedAt))) continue;
|
|
8029
|
+
let state;
|
|
8030
|
+
try {
|
|
8031
|
+
state = await reader.read(files.transcript, files.transcriptInfo);
|
|
8032
|
+
} catch {
|
|
8033
|
+
read.add(files.transcript);
|
|
8034
|
+
continue;
|
|
8035
|
+
}
|
|
8036
|
+
read.add(files.transcript);
|
|
8037
|
+
const ask = logged.find((log) => log.ask?.conversation === files.id)?.ask ?? null;
|
|
8038
|
+
const waiting = ask !== null && waitsForApproval(ask, files.id, state.lastIndex) ? { tool: ask.tool, since: agyLogTimeAt(ask.at, checkedAt) } : null;
|
|
8039
|
+
const session = antigravitySession({
|
|
8040
|
+
conversationId: files.id,
|
|
8041
|
+
state,
|
|
8042
|
+
live: antigravityLiveness(files.id, holding),
|
|
8043
|
+
waiting,
|
|
8044
|
+
title: await titles.read(files.id),
|
|
8045
|
+
folder: folderOf(files.id, logged),
|
|
8046
|
+
writtenAt: files.lastWriteAt,
|
|
8047
|
+
now: checkedAt
|
|
8048
|
+
});
|
|
8049
|
+
if (session.status === "unknown") unknownStep = true;
|
|
8050
|
+
sessions.push(session);
|
|
8051
|
+
}
|
|
8052
|
+
reader.keepOnly(read);
|
|
8053
|
+
titles.keepOnly(new Set(sessions.map((session) => session.id.slice(SOURCE_ID2.length + 1))));
|
|
8054
|
+
let detail = `Sessions are read from the transcripts the Antigravity CLI keeps in ${brainName}. ${ALWAYS_NOTES}`;
|
|
8055
|
+
let basis = BASIS;
|
|
8056
|
+
if (list !== null && !list.ok) {
|
|
8057
|
+
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.";
|
|
8058
|
+
basis = BASIS_WITHOUT_PROCESSES;
|
|
8059
|
+
} else if (holding !== null && !holding.complete) {
|
|
8060
|
+
detail += ` ${UNMATCHED_NOTE}`;
|
|
8061
|
+
basis = BASIS_UNMATCHED;
|
|
8062
|
+
}
|
|
8063
|
+
if (unknownStep) detail += ` ${UNKNOWN_STEP_NOTE}`;
|
|
8064
|
+
const answer2 = result("ok", detail, every(pollIntervalMs), checkedAt, basis);
|
|
8065
|
+
return { ...answer2, sessions };
|
|
8066
|
+
}
|
|
8067
|
+
return {
|
|
8068
|
+
id: SOURCE_ID2,
|
|
8069
|
+
label: LABEL2,
|
|
8070
|
+
capabilities: ANTIGRAVITY_CAPABILITIES,
|
|
8071
|
+
lookingIn: `Looking for Antigravity CLI sessions in ${brainName}.`,
|
|
8072
|
+
async poll() {
|
|
8073
|
+
try {
|
|
8074
|
+
return await poll();
|
|
8075
|
+
} catch {
|
|
8076
|
+
return result(
|
|
8077
|
+
"error",
|
|
8078
|
+
"Something unexpected went wrong while reading Antigravity CLI sessions.",
|
|
8079
|
+
every(pollIntervalMs),
|
|
8080
|
+
now()
|
|
8081
|
+
);
|
|
8082
|
+
}
|
|
8083
|
+
}
|
|
8084
|
+
};
|
|
8085
|
+
}
|
|
8086
|
+
|
|
8087
|
+
// src/collector/adapters/codex/index.ts
|
|
8088
|
+
import os6 from "node:os";
|
|
8089
|
+
import path17 from "node:path";
|
|
8090
|
+
|
|
8091
|
+
// src/core/mapping/codexMapping.ts
|
|
8092
|
+
var TURN_UNDER_WAY = /* @__PURE__ */ new Set(["task_started", "turn_started"]);
|
|
8093
|
+
var TURN_OVER2 = /* @__PURE__ */ new Set(["task_complete", "turn_complete", "turn_aborted"]);
|
|
8094
|
+
function mapCodexStatus(fields) {
|
|
8095
|
+
const { lastTurn, live: live2 } = fields;
|
|
8096
|
+
let open2;
|
|
8097
|
+
if (lastTurn === null) {
|
|
8098
|
+
open2 = "idle";
|
|
8099
|
+
} else if (typeof lastTurn !== "string") {
|
|
8100
|
+
return "unknown";
|
|
8101
|
+
} else if (TURN_UNDER_WAY.has(lastTurn)) {
|
|
8102
|
+
open2 = "working";
|
|
8103
|
+
} else if (TURN_OVER2.has(lastTurn)) {
|
|
8104
|
+
open2 = "idle";
|
|
8105
|
+
} else {
|
|
8106
|
+
return "unknown";
|
|
8107
|
+
}
|
|
8108
|
+
return live2 === false ? "finished" : open2;
|
|
8109
|
+
}
|
|
8110
|
+
var WRITER_LOCKS_SINCE = [0, 155, 0];
|
|
8111
|
+
var VERSION = /^(\d{1,6})\.(\d{1,6})\.(\d{1,6})(-[0-9A-Za-z.-]{1,64})?(?:\+[0-9A-Za-z.-]{1,64})?$/;
|
|
8112
|
+
function keepsWriterLocks(cliVersion) {
|
|
8113
|
+
if (typeof cliVersion !== "string") return false;
|
|
8114
|
+
const match = VERSION.exec(cliVersion);
|
|
8115
|
+
if (!match) return false;
|
|
8116
|
+
const parts = [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
8117
|
+
for (const [index, since] of WRITER_LOCKS_SINCE.entries()) {
|
|
8118
|
+
const part = parts[index];
|
|
8119
|
+
if (part !== since) return part > since;
|
|
8120
|
+
}
|
|
8121
|
+
return match[4] === void 0;
|
|
8122
|
+
}
|
|
8123
|
+
function codexLiveness(fields) {
|
|
8124
|
+
if (!fields.lockFolder) return "unknown";
|
|
8125
|
+
if (fields.locked) return true;
|
|
8126
|
+
return keepsWriterLocks(fields.cliVersion) ? false : "unknown";
|
|
8127
|
+
}
|
|
8128
|
+
var IMPORTED_TURN_ID = /^external-import-turn-\d{1,9}$/;
|
|
8129
|
+
function isImportedCodexTurn(turnId) {
|
|
8130
|
+
return typeof turnId === "string" && IMPORTED_TURN_ID.test(turnId);
|
|
8131
|
+
}
|
|
8132
|
+
function isRecord9(value) {
|
|
8133
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8134
|
+
}
|
|
8135
|
+
var CODEX_DESKTOP_ORIGINATOR = "Codex Desktop";
|
|
8136
|
+
function mapCodexSurface(source, originator) {
|
|
8137
|
+
if (originator === CODEX_DESKTOP_ORIGINATOR) return "desktop";
|
|
8138
|
+
if (source === "cli" || source === "exec") return "terminal";
|
|
8139
|
+
if (source === "vscode") return "vscode";
|
|
8140
|
+
if (isRecord9(source) && source.custom === "chatgpt") return "desktop";
|
|
8141
|
+
return "unknown";
|
|
8142
|
+
}
|
|
8143
|
+
var NOT_A_SESSION_SOURCE = /* @__PURE__ */ new Set(["subagent", "internal", "mcp"]);
|
|
8144
|
+
var NOT_A_SESSION_THREAD = /* @__PURE__ */ new Set(["subagent", "guardian_review", "memory_consolidation"]);
|
|
8145
|
+
function isCodexSessionSource(source, meta = {}) {
|
|
8146
|
+
if (typeof meta.parentThreadId === "string" && meta.parentThreadId !== "") return false;
|
|
8147
|
+
if (typeof meta.threadSource === "string" && NOT_A_SESSION_THREAD.has(meta.threadSource)) {
|
|
8148
|
+
return false;
|
|
8149
|
+
}
|
|
8150
|
+
if (typeof source === "string") return !NOT_A_SESSION_SOURCE.has(source);
|
|
8151
|
+
if (isRecord9(source)) return !Object.keys(source).some((kind) => NOT_A_SESSION_SOURCE.has(kind));
|
|
8152
|
+
return true;
|
|
8153
|
+
}
|
|
8154
|
+
|
|
8155
|
+
// src/collector/adapters/codex/rolloutFile.ts
|
|
8156
|
+
var HEAD_LIMIT_BYTES2 = 2 * 1024 * 1024;
|
|
8157
|
+
var TAIL_LIMIT_BYTES2 = 8 * 1024 * 1024;
|
|
8158
|
+
var FIRST_HEAD_BYTES = 256 * 1024;
|
|
8159
|
+
var TAIL_CHUNK_BYTES2 = 256 * 1024;
|
|
8160
|
+
var NEWLINE3 = 10;
|
|
8161
|
+
var CODEX_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/;
|
|
8162
|
+
function parseCodexTime(value) {
|
|
8163
|
+
if (typeof value !== "string" || !CODEX_TIME.test(value)) return null;
|
|
8164
|
+
const at = Date.parse(value);
|
|
8165
|
+
return Number.isSafeInteger(at) ? at : null;
|
|
8166
|
+
}
|
|
8167
|
+
var LINE_TIME = /^\{"timestamp":"([^"]{1,64})"/;
|
|
8168
|
+
var TURN_HINT = /"type":"(?:task|turn)_/;
|
|
8169
|
+
var TURN_TYPE = /^(?:task|turn)_/;
|
|
8170
|
+
function isRecord10(value) {
|
|
8171
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8172
|
+
}
|
|
8173
|
+
function text3(value) {
|
|
8174
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
8175
|
+
}
|
|
8176
|
+
function label(value) {
|
|
8177
|
+
return typeof value === "string" && value !== "" && value.length <= 64 ? value : void 0;
|
|
8178
|
+
}
|
|
8179
|
+
function compactSource(value) {
|
|
8180
|
+
if (typeof value === "string") return text3(value);
|
|
8181
|
+
if (!isRecord10(value)) return void 0;
|
|
8182
|
+
return Object.fromEntries(
|
|
8183
|
+
Object.entries(value).map(([kind, detail]) => [
|
|
8184
|
+
kind,
|
|
8185
|
+
typeof detail === "string" ? detail : true
|
|
8186
|
+
])
|
|
8187
|
+
);
|
|
8188
|
+
}
|
|
8189
|
+
function parse(line) {
|
|
8190
|
+
try {
|
|
8191
|
+
const value = JSON.parse(line);
|
|
8192
|
+
return isRecord10(value) ? value : null;
|
|
8193
|
+
} catch {
|
|
8194
|
+
return null;
|
|
8195
|
+
}
|
|
8196
|
+
}
|
|
8197
|
+
function readMetaLine(line) {
|
|
8198
|
+
if (!line.includes('"session_meta"')) return null;
|
|
8199
|
+
const value = parse(line);
|
|
8200
|
+
if (!value || value.type !== "session_meta" || !isRecord10(value.payload)) return null;
|
|
8201
|
+
const payload = value.payload;
|
|
8202
|
+
const meta = {
|
|
8203
|
+
id: text3(payload.id),
|
|
8204
|
+
timestamp: text3(payload.timestamp),
|
|
8205
|
+
cwd: text3(payload.cwd),
|
|
8206
|
+
source: compactSource(payload.source),
|
|
8207
|
+
threadSource: text3(payload.thread_source),
|
|
8208
|
+
parentThreadId: text3(payload.parent_thread_id),
|
|
8209
|
+
originator: label(payload.originator),
|
|
8210
|
+
cliVersion: label(payload.cli_version)
|
|
8211
|
+
};
|
|
8212
|
+
for (const key of Object.keys(meta)) {
|
|
8213
|
+
if (meta[key] === void 0) delete meta[key];
|
|
8214
|
+
}
|
|
8215
|
+
return meta;
|
|
8216
|
+
}
|
|
8217
|
+
function readTurnLine(line) {
|
|
8218
|
+
if (!line.includes('"event_msg"') || !TURN_HINT.test(line)) return null;
|
|
8219
|
+
const value = parse(line);
|
|
8220
|
+
if (!value || value.type !== "event_msg" || !isRecord10(value.payload)) return null;
|
|
8221
|
+
const turn = value.payload.type;
|
|
8222
|
+
if (typeof turn !== "string" || !TURN_TYPE.test(turn)) return null;
|
|
8223
|
+
return {
|
|
8224
|
+
turn,
|
|
8225
|
+
at: parseCodexTime(value.timestamp),
|
|
8226
|
+
imported: isImportedCodexTurn(value.payload.turn_id)
|
|
8227
|
+
};
|
|
6415
8228
|
}
|
|
6416
8229
|
function lineTime(line) {
|
|
6417
8230
|
const match = LINE_TIME.exec(line);
|
|
6418
8231
|
return match ? parseCodexTime(match[1]) : null;
|
|
6419
8232
|
}
|
|
6420
|
-
function
|
|
8233
|
+
function bytes2(data) {
|
|
6421
8234
|
return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
6422
8235
|
}
|
|
6423
|
-
async function
|
|
6424
|
-
const data =
|
|
8236
|
+
async function readExactly2(file, position, length) {
|
|
8237
|
+
const data = bytes2(await file.read(position, length));
|
|
6425
8238
|
if (data.byteLength !== length) throw new Error("The file changed while it was read.");
|
|
6426
8239
|
return data;
|
|
6427
8240
|
}
|
|
6428
8241
|
async function readHead(file, size) {
|
|
6429
|
-
for (const limit of [FIRST_HEAD_BYTES,
|
|
8242
|
+
for (const limit of [FIRST_HEAD_BYTES, HEAD_LIMIT_BYTES2]) {
|
|
6430
8243
|
const length = Math.min(limit, size);
|
|
6431
|
-
const head = await
|
|
8244
|
+
const head = await readExactly2(file, 0, length);
|
|
6432
8245
|
let named = head.indexOf('"session_meta"');
|
|
6433
8246
|
while (named !== -1) {
|
|
6434
|
-
const end = head.indexOf(
|
|
8247
|
+
const end = head.indexOf(NEWLINE3, named);
|
|
6435
8248
|
if (end === -1) break;
|
|
6436
|
-
const start = named > 0 ? head.lastIndexOf(
|
|
8249
|
+
const start = named > 0 ? head.lastIndexOf(NEWLINE3, named - 1) + 1 : 0;
|
|
6437
8250
|
const meta = readMetaLine(head.toString("utf8", start, end));
|
|
6438
8251
|
if (meta) return meta;
|
|
6439
8252
|
named = head.indexOf('"session_meta"', end + 1);
|
|
@@ -6442,9 +8255,9 @@ async function readHead(file, size) {
|
|
|
6442
8255
|
}
|
|
6443
8256
|
return null;
|
|
6444
8257
|
}
|
|
6445
|
-
async function
|
|
8258
|
+
async function scanTail2(file, size) {
|
|
6446
8259
|
let position = size;
|
|
6447
|
-
let budget2 =
|
|
8260
|
+
let budget2 = TAIL_LIMIT_BYTES2;
|
|
6448
8261
|
let fragment = Buffer.alloc(0);
|
|
6449
8262
|
let end = null;
|
|
6450
8263
|
let lastLineAt = null;
|
|
@@ -6462,15 +8275,15 @@ async function scanTail(file, size) {
|
|
|
6462
8275
|
end
|
|
6463
8276
|
});
|
|
6464
8277
|
while (position > 0 && budget2 > 0) {
|
|
6465
|
-
const length = Math.min(
|
|
8278
|
+
const length = Math.min(TAIL_CHUNK_BYTES2, position, budget2);
|
|
6466
8279
|
const start = position - length;
|
|
6467
|
-
const chunk = await
|
|
8280
|
+
const chunk = await readExactly2(file, start, length);
|
|
6468
8281
|
budget2 -= length;
|
|
6469
8282
|
position = start;
|
|
6470
8283
|
const buffer = fragment.byteLength > 0 ? Buffer.concat([chunk, fragment]) : chunk;
|
|
6471
8284
|
let cut2 = buffer.byteLength;
|
|
6472
8285
|
if (end === null) {
|
|
6473
|
-
const last = buffer.lastIndexOf(
|
|
8286
|
+
const last = buffer.lastIndexOf(NEWLINE3);
|
|
6474
8287
|
if (last === -1) {
|
|
6475
8288
|
fragment = buffer;
|
|
6476
8289
|
continue;
|
|
@@ -6478,12 +8291,12 @@ async function scanTail(file, size) {
|
|
|
6478
8291
|
end = start + last + 1;
|
|
6479
8292
|
cut2 = last;
|
|
6480
8293
|
}
|
|
6481
|
-
let newline = cut2 > 0 ? buffer.lastIndexOf(
|
|
8294
|
+
let newline = cut2 > 0 ? buffer.lastIndexOf(NEWLINE3, cut2 - 1) : -1;
|
|
6482
8295
|
while (newline !== -1) {
|
|
6483
8296
|
const turn = take(buffer.toString("utf8", newline + 1, cut2));
|
|
6484
8297
|
if (turn) return found(turn);
|
|
6485
8298
|
cut2 = newline;
|
|
6486
|
-
newline = cut2 > 0 ? buffer.lastIndexOf(
|
|
8299
|
+
newline = cut2 > 0 ? buffer.lastIndexOf(NEWLINE3, cut2 - 1) : -1;
|
|
6487
8300
|
}
|
|
6488
8301
|
if (start === 0) {
|
|
6489
8302
|
const turn = take(buffer.toString("utf8", 0, cut2));
|
|
@@ -6497,7 +8310,7 @@ async function scanTail(file, size) {
|
|
|
6497
8310
|
}
|
|
6498
8311
|
return { lastTurn: void 0, lastTurnAt: null, lastTurnImported: false, lastLineAt, end };
|
|
6499
8312
|
}
|
|
6500
|
-
function
|
|
8313
|
+
function stateOf2(cached) {
|
|
6501
8314
|
return {
|
|
6502
8315
|
meta: cached.meta,
|
|
6503
8316
|
lastTurn: cached.lastTurn,
|
|
@@ -6513,7 +8326,7 @@ function createRolloutReader(io) {
|
|
|
6513
8326
|
try {
|
|
6514
8327
|
const { info } = handle;
|
|
6515
8328
|
const meta = await readHead(handle, info.size);
|
|
6516
|
-
const tail = await
|
|
8329
|
+
const tail = await scanTail2(handle, info.size);
|
|
6517
8330
|
return {
|
|
6518
8331
|
meta,
|
|
6519
8332
|
lastTurn: tail.lastTurn,
|
|
@@ -6533,14 +8346,14 @@ function createRolloutReader(io) {
|
|
|
6533
8346
|
const offset = cached.offset;
|
|
6534
8347
|
if (cached.meta === null || offset === null) return null;
|
|
6535
8348
|
if (info.ino !== cached.ino || info.size <= cached.size || info.size < offset) return null;
|
|
6536
|
-
if (info.size - offset >
|
|
8349
|
+
if (info.size - offset > TAIL_LIMIT_BYTES2) return null;
|
|
6537
8350
|
const handle = await io.openRegular(file);
|
|
6538
8351
|
try {
|
|
6539
8352
|
const opened = handle.info;
|
|
6540
8353
|
if (opened.ino !== cached.ino || opened.size < offset) return null;
|
|
6541
8354
|
const from = offset > 0 ? offset - 1 : 0;
|
|
6542
|
-
const read = await
|
|
6543
|
-
if (offset > 0 && read[0] !==
|
|
8355
|
+
const read = await readExactly2(handle, from, opened.size - from);
|
|
8356
|
+
if (offset > 0 && read[0] !== NEWLINE3) return null;
|
|
6544
8357
|
const added = offset > 0 ? read.subarray(1) : read;
|
|
6545
8358
|
const next = {
|
|
6546
8359
|
...cached,
|
|
@@ -6549,7 +8362,7 @@ function createRolloutReader(io) {
|
|
|
6549
8362
|
mtimeMs: opened.mtimeMs
|
|
6550
8363
|
};
|
|
6551
8364
|
let start = 0;
|
|
6552
|
-
let newline = added.indexOf(
|
|
8365
|
+
let newline = added.indexOf(NEWLINE3);
|
|
6553
8366
|
while (newline !== -1) {
|
|
6554
8367
|
const line = added.toString("utf8", start, newline);
|
|
6555
8368
|
if (line.trim() !== "") {
|
|
@@ -6563,7 +8376,7 @@ function createRolloutReader(io) {
|
|
|
6563
8376
|
}
|
|
6564
8377
|
}
|
|
6565
8378
|
start = newline + 1;
|
|
6566
|
-
newline = added.indexOf(
|
|
8379
|
+
newline = added.indexOf(NEWLINE3, start);
|
|
6567
8380
|
}
|
|
6568
8381
|
next.offset = offset + start;
|
|
6569
8382
|
return next;
|
|
@@ -6575,11 +8388,11 @@ function createRolloutReader(io) {
|
|
|
6575
8388
|
async read(file, info) {
|
|
6576
8389
|
const cached = cache.get(file);
|
|
6577
8390
|
if (cached && cached.ino === info.ino && cached.size === info.size && cached.mtimeMs === info.mtimeMs) {
|
|
6578
|
-
return
|
|
8391
|
+
return stateOf2(cached);
|
|
6579
8392
|
}
|
|
6580
8393
|
const next = cached && await readOnward(file, cached, info) || await readFresh(file);
|
|
6581
8394
|
cache.set(file, next);
|
|
6582
|
-
return
|
|
8395
|
+
return stateOf2(next);
|
|
6583
8396
|
},
|
|
6584
8397
|
keepOnly(files) {
|
|
6585
8398
|
for (const file of cache.keys()) {
|
|
@@ -6590,10 +8403,10 @@ function createRolloutReader(io) {
|
|
|
6590
8403
|
}
|
|
6591
8404
|
|
|
6592
8405
|
// src/collector/adapters/codex/rollouts.ts
|
|
6593
|
-
import
|
|
6594
|
-
var
|
|
8406
|
+
import path16 from "node:path";
|
|
8407
|
+
var UUID4 = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
|
|
6595
8408
|
var ROLLOUT_FILE_NAME = new RegExp(
|
|
6596
|
-
`^rollout-\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-(${
|
|
8409
|
+
`^rollout-\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-(${UUID4})(?:_(${UUID4}))?\\.jsonl$`,
|
|
6597
8410
|
"i"
|
|
6598
8411
|
);
|
|
6599
8412
|
var INDEX_REFRESH_MS = 3e4;
|
|
@@ -6608,7 +8421,7 @@ function twoDigits(value) {
|
|
|
6608
8421
|
function recentDayFolders(sessionsDir, now) {
|
|
6609
8422
|
const folders = [now, now - 24 * 60 * 60 * 1e3].map((at) => {
|
|
6610
8423
|
const date = new Date(at);
|
|
6611
|
-
return
|
|
8424
|
+
return path16.join(
|
|
6612
8425
|
sessionsDir,
|
|
6613
8426
|
String(date.getFullYear()),
|
|
6614
8427
|
twoDigits(date.getMonth() + 1),
|
|
@@ -6637,12 +8450,12 @@ function createRolloutFinder(options) {
|
|
|
6637
8450
|
const refs = [];
|
|
6638
8451
|
for (const name of (await names(dir)).sort()) {
|
|
6639
8452
|
const threadId = threadIdOfRollout(name);
|
|
6640
|
-
if (threadId !== null) refs.push({ path:
|
|
8453
|
+
if (threadId !== null) refs.push({ path: path16.join(dir, name), threadId });
|
|
6641
8454
|
}
|
|
6642
8455
|
return refs;
|
|
6643
8456
|
}
|
|
6644
8457
|
async function subfolders(dir, pattern) {
|
|
6645
|
-
return (await names(dir)).filter((name) => pattern.test(name)).sort().map((name) =>
|
|
8458
|
+
return (await names(dir)).filter((name) => pattern.test(name)).sort().map((name) => path16.join(dir, name));
|
|
6646
8459
|
}
|
|
6647
8460
|
async function buildIndex() {
|
|
6648
8461
|
const years = await subfolders(sessionsDir, YEAR);
|
|
@@ -6686,7 +8499,7 @@ function createRolloutFinder(options) {
|
|
|
6686
8499
|
// src/collector/adapters/codex/sessionIndex.ts
|
|
6687
8500
|
var SESSION_INDEX_FILE = "session_index.jsonl";
|
|
6688
8501
|
var SESSION_INDEX_LIMIT_BYTES = 4 * 1024 * 1024;
|
|
6689
|
-
function
|
|
8502
|
+
function isRecord11(value) {
|
|
6690
8503
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6691
8504
|
}
|
|
6692
8505
|
function parseSessionIndex(content) {
|
|
@@ -6699,7 +8512,7 @@ function parseSessionIndex(content) {
|
|
|
6699
8512
|
} catch {
|
|
6700
8513
|
continue;
|
|
6701
8514
|
}
|
|
6702
|
-
if (!
|
|
8515
|
+
if (!isRecord11(value)) continue;
|
|
6703
8516
|
const { id, thread_name: name } = value;
|
|
6704
8517
|
if (typeof id !== "string" || id === "" || typeof name !== "string") continue;
|
|
6705
8518
|
const trimmed = name.trim();
|
|
@@ -6750,14 +8563,14 @@ function createSessionIndexReader(file, io) {
|
|
|
6750
8563
|
}
|
|
6751
8564
|
|
|
6752
8565
|
// src/collector/adapters/codex/toSession.ts
|
|
6753
|
-
var
|
|
6754
|
-
var
|
|
8566
|
+
var SOURCE_ID3 = "codex";
|
|
8567
|
+
var WRITE_ORDER_SLACK_MS3 = 1e3;
|
|
6755
8568
|
function lastActivity(state, now) {
|
|
6756
8569
|
return plausibleTime(state.lastLineAt, now) ?? plausibleTime(state.lastTurnAt, now) ?? plausibleTime(parseCodexTime(state.meta?.timestamp), now);
|
|
6757
8570
|
}
|
|
6758
8571
|
function codexSession(input) {
|
|
6759
8572
|
const { threadId, state, live: live2, now } = input;
|
|
6760
|
-
const id = `${
|
|
8573
|
+
const id = `${SOURCE_ID3}:${threadId}`;
|
|
6761
8574
|
const cwd = state.meta?.cwd ?? null;
|
|
6762
8575
|
const project = projectOf(cwd);
|
|
6763
8576
|
const status = mapCodexStatus({ lastTurn: state.lastTurn, live: live2 });
|
|
@@ -6768,12 +8581,12 @@ function codexSession(input) {
|
|
|
6768
8581
|
} else if (status === "idle" || status === "finished") {
|
|
6769
8582
|
statusSince = plausibleTime(state.lastLineAt, now);
|
|
6770
8583
|
}
|
|
6771
|
-
if (statusSince !== null && startedAt !== null && statusSince < startedAt -
|
|
8584
|
+
if (statusSince !== null && startedAt !== null && statusSince < startedAt - WRITE_ORDER_SLACK_MS3) {
|
|
6772
8585
|
statusSince = null;
|
|
6773
8586
|
}
|
|
6774
8587
|
const session = {
|
|
6775
8588
|
id,
|
|
6776
|
-
source:
|
|
8589
|
+
source: SOURCE_ID3,
|
|
6777
8590
|
surface: mapCodexSurface(state.meta?.source, state.meta?.originator),
|
|
6778
8591
|
// A name made only of spaces is no name. It is cleaned and cut as every other agent's is.
|
|
6779
8592
|
name: sessionName(input.name ?? "") || project || id,
|
|
@@ -6786,7 +8599,7 @@ function codexSession(input) {
|
|
|
6786
8599
|
stale: isStale({ status, statusSince }, now)
|
|
6787
8600
|
};
|
|
6788
8601
|
const writtenAt = input.writtenAt === void 0 ? null : plausibleTime(Math.floor(input.writtenAt), now);
|
|
6789
|
-
const beforeStart = writtenAt !== null && startedAt !== null && writtenAt < startedAt -
|
|
8602
|
+
const beforeStart = writtenAt !== null && startedAt !== null && writtenAt < startedAt - WRITE_ORDER_SLACK_MS3;
|
|
6790
8603
|
if (writtenAt !== null && !beforeStart) session.lastWriteAt = writtenAt;
|
|
6791
8604
|
return session;
|
|
6792
8605
|
}
|
|
@@ -6816,11 +8629,11 @@ async function readWriterLocks(dir, io) {
|
|
|
6816
8629
|
// src/collector/adapters/codex/index.ts
|
|
6817
8630
|
var CODEX_HOME_ENV = "AGENT_LOOKOUT_CODEX_HOME";
|
|
6818
8631
|
var CODEX_OWN_HOME_ENV = "CODEX_HOME";
|
|
6819
|
-
var
|
|
6820
|
-
var
|
|
8632
|
+
var RECHECK_MS2 = 6e4;
|
|
8633
|
+
var BASIS2 = "files";
|
|
6821
8634
|
var BASIS_WITHOUT_LOCKS = "files-without-locks";
|
|
6822
|
-
var
|
|
6823
|
-
var
|
|
8635
|
+
var LABEL3 = "Codex";
|
|
8636
|
+
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
8637
|
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
8638
|
var CODEX_CAPABILITIES = {
|
|
6826
8639
|
"working-and-idle": { level: "yes" },
|
|
@@ -6853,18 +8666,18 @@ var CODEX_CAPABILITIES = {
|
|
|
6853
8666
|
};
|
|
6854
8667
|
function createCodexAdapter(options = {}) {
|
|
6855
8668
|
const env = options.env ?? process.env;
|
|
6856
|
-
const homeDir = options.homeDir ??
|
|
8669
|
+
const homeDir = options.homeDir ?? os6.homedir();
|
|
6857
8670
|
const now = options.now ?? Date.now;
|
|
6858
8671
|
const io = options.io ?? nodeIo2;
|
|
6859
8672
|
const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
6860
|
-
const recheckMs = options.recheckMs ??
|
|
8673
|
+
const recheckMs = options.recheckMs ?? RECHECK_MS2;
|
|
6861
8674
|
const ownOverride = env[CODEX_HOME_ENV]?.trim() || void 0;
|
|
6862
8675
|
const codexOverride = env[CODEX_OWN_HOME_ENV]?.trim() || void 0;
|
|
6863
8676
|
const homeFrom = ownOverride ? "agent-lookout" : codexOverride ? "codex" : "default";
|
|
6864
|
-
const codexHome =
|
|
6865
|
-
const sessionsDir =
|
|
6866
|
-
const locksDir =
|
|
6867
|
-
const indexFile =
|
|
8677
|
+
const codexHome = path17.resolve(ownOverride ?? codexOverride ?? path17.join(homeDir, ".codex"));
|
|
8678
|
+
const sessionsDir = path17.join(codexHome, "sessions");
|
|
8679
|
+
const locksDir = path17.join(codexHome, WRITER_LOCK_DIR);
|
|
8680
|
+
const indexFile = path17.join(codexHome, SESSION_INDEX_FILE);
|
|
6868
8681
|
const homeName = tildify(codexHome, homeDir);
|
|
6869
8682
|
const sessionsName = tildify(sessionsDir, homeDir);
|
|
6870
8683
|
const locksName = tildify(locksDir, homeDir);
|
|
@@ -6890,14 +8703,14 @@ function createCodexAdapter(options = {}) {
|
|
|
6890
8703
|
}
|
|
6891
8704
|
function result(state, detail, read, checkedAt) {
|
|
6892
8705
|
const health = {
|
|
6893
|
-
id:
|
|
6894
|
-
label:
|
|
8706
|
+
id: SOURCE_ID3,
|
|
8707
|
+
label: LABEL3,
|
|
6895
8708
|
state,
|
|
6896
8709
|
detail,
|
|
6897
8710
|
watching: watching(read),
|
|
6898
8711
|
checkedAt
|
|
6899
8712
|
};
|
|
6900
|
-
return state === "ok" ? { health, sessions: [], basis:
|
|
8713
|
+
return state === "ok" ? { health, sessions: [], basis: BASIS2 } : { health, sessions: [] };
|
|
6901
8714
|
}
|
|
6902
8715
|
function notFound(checkedAt) {
|
|
6903
8716
|
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 +8758,7 @@ function createCodexAdapter(options = {}) {
|
|
|
6945
8758
|
if (homeFrom === "agent-lookout") {
|
|
6946
8759
|
return result(
|
|
6947
8760
|
"ok",
|
|
6948
|
-
`${CODEX_HOME_ENV} is set to ${homeName}, which has no sessions folder, so no Codex sessions are listed. ${
|
|
8761
|
+
`${CODEX_HOME_ENV} is set to ${homeName}, which has no sessions folder, so no Codex sessions are listed. ${NEEDS_YOU_NOTE2}`,
|
|
6949
8762
|
"not found",
|
|
6950
8763
|
checkedAt
|
|
6951
8764
|
);
|
|
@@ -6958,7 +8771,7 @@ function createCodexAdapter(options = {}) {
|
|
|
6958
8771
|
homeSeen = true;
|
|
6959
8772
|
return result(
|
|
6960
8773
|
"ok",
|
|
6961
|
-
`Codex has not saved any sessions in ${sessionsName} yet. ${
|
|
8774
|
+
`Codex has not saved any sessions in ${sessionsName} yet. ${NEEDS_YOU_NOTE2}`,
|
|
6962
8775
|
"not found",
|
|
6963
8776
|
checkedAt
|
|
6964
8777
|
);
|
|
@@ -7040,19 +8853,19 @@ function createCodexAdapter(options = {}) {
|
|
|
7040
8853
|
now: checkedAt
|
|
7041
8854
|
});
|
|
7042
8855
|
});
|
|
7043
|
-
let detail = `Sessions are read from the files Codex saves in ${sessionsName}. ${
|
|
8856
|
+
let detail = `Sessions are read from the files Codex saves in ${sessionsName}. ${NEEDS_YOU_NOTE2}`;
|
|
7044
8857
|
if (!locks.supported) {
|
|
7045
8858
|
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
8859
|
detail += ` ${why}, so a session that has ended cannot be told from one that is idle, and none is shown as finished.`;
|
|
7047
8860
|
} else if (olderCodex) {
|
|
7048
8861
|
detail += ` ${OLDER_CODEX_NOTE}`;
|
|
7049
8862
|
}
|
|
7050
|
-
const
|
|
7051
|
-
return { ...
|
|
8863
|
+
const answer2 = result("ok", detail, every(pollIntervalMs), checkedAt);
|
|
8864
|
+
return { ...answer2, sessions, basis: locks.supported ? BASIS2 : BASIS_WITHOUT_LOCKS };
|
|
7052
8865
|
}
|
|
7053
8866
|
return {
|
|
7054
|
-
id:
|
|
7055
|
-
label:
|
|
8867
|
+
id: SOURCE_ID3,
|
|
8868
|
+
label: LABEL3,
|
|
7056
8869
|
capabilities: CODEX_CAPABILITIES,
|
|
7057
8870
|
lookingIn: `Looking for Codex sessions in ${sessionsName}.`,
|
|
7058
8871
|
async poll() {
|
|
@@ -7071,8 +8884,8 @@ function createCodexAdapter(options = {}) {
|
|
|
7071
8884
|
}
|
|
7072
8885
|
|
|
7073
8886
|
// src/collector/adapters/status-files/index.ts
|
|
7074
|
-
import
|
|
7075
|
-
import
|
|
8887
|
+
import os7 from "node:os";
|
|
8888
|
+
import path18 from "node:path";
|
|
7076
8889
|
|
|
7077
8890
|
// src/core/mapping/statusFileMapping.ts
|
|
7078
8891
|
var STATUS = /* @__PURE__ */ new Map([
|
|
@@ -7152,16 +8965,16 @@ function parseStatusFile(content) {
|
|
|
7152
8965
|
}
|
|
7153
8966
|
|
|
7154
8967
|
// src/collector/adapters/status-files/toSession.ts
|
|
7155
|
-
var
|
|
7156
|
-
var
|
|
8968
|
+
var SOURCE_ID4 = "status-files";
|
|
8969
|
+
var WRITE_ORDER_SLACK_MS4 = 1e3;
|
|
7157
8970
|
function statusFileSession(input) {
|
|
7158
8971
|
const { fileName: fileName2, file, statusSince, now } = input;
|
|
7159
8972
|
const cwd = file.cwd ?? null;
|
|
7160
8973
|
const project = projectOf(cwd);
|
|
7161
8974
|
const mapped = mapStatusFileStatus(file);
|
|
7162
8975
|
const session = {
|
|
7163
|
-
id: `${
|
|
7164
|
-
source:
|
|
8976
|
+
id: `${SOURCE_ID4}:${fileName2}`,
|
|
8977
|
+
source: SOURCE_ID4,
|
|
7165
8978
|
agent: file.agent,
|
|
7166
8979
|
surface: mapStatusFileApp(file.app),
|
|
7167
8980
|
// The file's name is as untrusted as its contents, so it is cleaned and cut
|
|
@@ -7181,15 +8994,15 @@ function statusFileSession(input) {
|
|
|
7181
8994
|
}
|
|
7182
8995
|
const writtenAt = input.writtenAt === void 0 ? null : plausibleTime(Math.floor(input.writtenAt), now);
|
|
7183
8996
|
const given = plausibleTime(file.since, now);
|
|
7184
|
-
const beforeSince = writtenAt !== null && given !== null && writtenAt < given -
|
|
8997
|
+
const beforeSince = writtenAt !== null && given !== null && writtenAt < given - WRITE_ORDER_SLACK_MS4;
|
|
7185
8998
|
if (writtenAt !== null && !beforeSince) session.lastWriteAt = writtenAt;
|
|
7186
8999
|
return session;
|
|
7187
9000
|
}
|
|
7188
9001
|
|
|
7189
9002
|
// src/collector/adapters/status-files/index.ts
|
|
7190
9003
|
var STATUS_DIR_ENV = "AGENT_LOOKOUT_STATUS_DIR";
|
|
7191
|
-
var
|
|
7192
|
-
var
|
|
9004
|
+
var BASIS3 = "files";
|
|
9005
|
+
var LABEL4 = "Status files";
|
|
7193
9006
|
var STATUS_FILE_CAPABILITIES = {
|
|
7194
9007
|
"working-and-idle": { level: "partly", reason: "If the agent writes working and idle." },
|
|
7195
9008
|
"needs-you": { level: "partly", reason: "If the agent writes waiting." },
|
|
@@ -7215,13 +9028,13 @@ function counted(count2, one, many) {
|
|
|
7215
9028
|
}
|
|
7216
9029
|
function createStatusFileAdapter(options = {}) {
|
|
7217
9030
|
const env = options.env ?? process.env;
|
|
7218
|
-
const homeDir = options.homeDir ??
|
|
9031
|
+
const homeDir = options.homeDir ?? os7.homedir();
|
|
7219
9032
|
const now = options.now ?? Date.now;
|
|
7220
9033
|
const io = options.io ?? nodeIo2;
|
|
7221
9034
|
const isAlive = options.isAlive ?? isProcessAlive;
|
|
7222
9035
|
const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
7223
9036
|
const override = env[STATUS_DIR_ENV]?.trim() || void 0;
|
|
7224
|
-
const dir =
|
|
9037
|
+
const dir = path18.resolve(override ?? path18.join(homeDir, ".agent-lookout", "sessions"));
|
|
7225
9038
|
const dirName = tildify(dir, homeDir);
|
|
7226
9039
|
const decoder2 = new TextDecoder();
|
|
7227
9040
|
let lastRead = /* @__PURE__ */ new Map();
|
|
@@ -7242,15 +9055,15 @@ function createStatusFileAdapter(options = {}) {
|
|
|
7242
9055
|
}
|
|
7243
9056
|
function result(state, detail, facts, checkedAt, sessions = []) {
|
|
7244
9057
|
const health = {
|
|
7245
|
-
id:
|
|
7246
|
-
label:
|
|
9058
|
+
id: SOURCE_ID4,
|
|
9059
|
+
label: LABEL4,
|
|
7247
9060
|
state,
|
|
7248
9061
|
detail,
|
|
7249
9062
|
watching: facts,
|
|
7250
9063
|
checkedAt
|
|
7251
9064
|
};
|
|
7252
|
-
if (state === "ok") return { health, sessions, basis:
|
|
7253
|
-
if (state === "not-set-up") return { health, sessions: [], basis:
|
|
9065
|
+
if (state === "ok") return { health, sessions, basis: BASIS3 };
|
|
9066
|
+
if (state === "not-set-up") return { health, sessions: [], basis: BASIS3 };
|
|
7254
9067
|
return { health, sessions: [] };
|
|
7255
9068
|
}
|
|
7256
9069
|
async function readOne(file) {
|
|
@@ -7266,9 +9079,9 @@ function createStatusFileAdapter(options = {}) {
|
|
|
7266
9079
|
const tooLarge = { kind: "refused", why: "is over 16 KB" };
|
|
7267
9080
|
try {
|
|
7268
9081
|
if (open2.info.size > MAX_FILE_BYTES) return tooLarge;
|
|
7269
|
-
const
|
|
7270
|
-
if (
|
|
7271
|
-
const parsed = parseStatusFile(decoder2.decode(
|
|
9082
|
+
const bytes3 = await open2.read(0, MAX_FILE_BYTES + 1);
|
|
9083
|
+
if (bytes3.byteLength > MAX_FILE_BYTES) return tooLarge;
|
|
9084
|
+
const parsed = parseStatusFile(decoder2.decode(bytes3));
|
|
7272
9085
|
return parsed ? { kind: "read", file: parsed, mtimeMs: open2.info.mtimeMs } : { kind: "invalid" };
|
|
7273
9086
|
} catch {
|
|
7274
9087
|
return { kind: "refused", why: "could not be read" };
|
|
@@ -7281,7 +9094,7 @@ function createStatusFileAdapter(options = {}) {
|
|
|
7281
9094
|
if (names.length <= MAX_FILES) return names;
|
|
7282
9095
|
const written = await Promise.all(
|
|
7283
9096
|
names.map(async (name) => {
|
|
7284
|
-
const info = await io.lstat(
|
|
9097
|
+
const info = await io.lstat(path18.join(dir, name)).catch(() => null);
|
|
7285
9098
|
return { name, mtimeMs: info?.mtimeMs ?? -Infinity };
|
|
7286
9099
|
})
|
|
7287
9100
|
);
|
|
@@ -7302,14 +9115,14 @@ function createStatusFileAdapter(options = {}) {
|
|
|
7302
9115
|
names = await io.readdir(dir);
|
|
7303
9116
|
} catch (error) {
|
|
7304
9117
|
if (!isMissing2(error)) {
|
|
7305
|
-
const
|
|
9118
|
+
const answer2 = result(
|
|
7306
9119
|
"error",
|
|
7307
9120
|
`Status files could not be read: the folder ${dirName} could not be listed.`,
|
|
7308
9121
|
watching("cannot be read"),
|
|
7309
9122
|
checkedAt
|
|
7310
9123
|
);
|
|
7311
|
-
|
|
7312
|
-
return
|
|
9124
|
+
answer2.health.advice = "Check that your user account can open the folder.";
|
|
9125
|
+
return answer2;
|
|
7313
9126
|
}
|
|
7314
9127
|
contentsKnown = true;
|
|
7315
9128
|
lastRead = /* @__PURE__ */ new Map();
|
|
@@ -7331,7 +9144,7 @@ function createStatusFileAdapter(options = {}) {
|
|
|
7331
9144
|
const nextRead = /* @__PURE__ */ new Map();
|
|
7332
9145
|
const nextStatuses = /* @__PURE__ */ new Map();
|
|
7333
9146
|
for (const fileName2 of await toRead(listed)) {
|
|
7334
|
-
const reading = await readOne(
|
|
9147
|
+
const reading = await readOne(path18.join(dir, fileName2));
|
|
7335
9148
|
let kept;
|
|
7336
9149
|
if (reading.kind === "read") {
|
|
7337
9150
|
kept = reading;
|
|
@@ -7397,8 +9210,8 @@ function createStatusFileAdapter(options = {}) {
|
|
|
7397
9210
|
);
|
|
7398
9211
|
}
|
|
7399
9212
|
return {
|
|
7400
|
-
id:
|
|
7401
|
-
label:
|
|
9213
|
+
id: SOURCE_ID4,
|
|
9214
|
+
label: LABEL4,
|
|
7402
9215
|
capabilities: STATUS_FILE_CAPABILITIES,
|
|
7403
9216
|
lookingIn: `Looking for status files in ${dirName}.`,
|
|
7404
9217
|
async poll() {
|
|
@@ -7557,8 +9370,8 @@ function createEventStore(capacity = EVENT_CAPACITY) {
|
|
|
7557
9370
|
}
|
|
7558
9371
|
|
|
7559
9372
|
// src/collector/git/branchFinder.ts
|
|
7560
|
-
import
|
|
7561
|
-
import
|
|
9373
|
+
import os8 from "node:os";
|
|
9374
|
+
import path20 from "node:path";
|
|
7562
9375
|
|
|
7563
9376
|
// src/collector/git/gitHead.ts
|
|
7564
9377
|
var MAX_GIT_FILE_BYTES = 4 * 1024;
|
|
@@ -7597,19 +9410,19 @@ function parseCommonDir(content) {
|
|
|
7597
9410
|
}
|
|
7598
9411
|
|
|
7599
9412
|
// src/collector/git/repository.ts
|
|
7600
|
-
import { createHash as
|
|
7601
|
-
import
|
|
9413
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
9414
|
+
import path19 from "node:path";
|
|
7602
9415
|
var REPOSITORY_ID_LENGTH = 16;
|
|
7603
9416
|
function repositoryId(gitFolder) {
|
|
7604
|
-
return
|
|
9417
|
+
return createHash3("sha256").update(gitFolder).digest("hex").slice(0, REPOSITORY_ID_LENGTH);
|
|
7605
9418
|
}
|
|
7606
9419
|
function sharedName(common) {
|
|
7607
|
-
const base =
|
|
7608
|
-
if (base.startsWith(".")) return
|
|
9420
|
+
const base = path19.basename(common);
|
|
9421
|
+
if (base.startsWith(".")) return path19.basename(path19.dirname(common));
|
|
7609
9422
|
return base.endsWith(".git") ? base.slice(0, -".git".length) : base;
|
|
7610
9423
|
}
|
|
7611
9424
|
function repositoryOf({ dir, gitDir, commonDir }) {
|
|
7612
|
-
const name = sessionName(commonDir === null ?
|
|
9425
|
+
const name = sessionName(commonDir === null ? path19.basename(dir) : sharedName(commonDir));
|
|
7613
9426
|
return name === void 0 ? null : { id: repositoryId(commonDir ?? gitDir), name };
|
|
7614
9427
|
}
|
|
7615
9428
|
|
|
@@ -7627,9 +9440,9 @@ async function readGitFile(io, file, maxBytes = MAX_GIT_FILE_BYTES) {
|
|
|
7627
9440
|
}
|
|
7628
9441
|
try {
|
|
7629
9442
|
if (open2.info.size > maxBytes) return null;
|
|
7630
|
-
const
|
|
7631
|
-
if (
|
|
7632
|
-
return decoder.decode(
|
|
9443
|
+
const bytes3 = await open2.read(0, maxBytes + 1);
|
|
9444
|
+
if (bytes3.byteLength > maxBytes) return null;
|
|
9445
|
+
return decoder.decode(bytes3);
|
|
7633
9446
|
} catch {
|
|
7634
9447
|
return null;
|
|
7635
9448
|
} finally {
|
|
@@ -7638,13 +9451,13 @@ async function readGitFile(io, file, maxBytes = MAX_GIT_FILE_BYTES) {
|
|
|
7638
9451
|
}
|
|
7639
9452
|
}
|
|
7640
9453
|
async function commonDirOf(io, gitDir) {
|
|
7641
|
-
const content = await readGitFile(io,
|
|
9454
|
+
const content = await readGitFile(io, path20.join(gitDir, "commondir"));
|
|
7642
9455
|
if (content === void 0) return null;
|
|
7643
9456
|
const target = content === null ? null : parseCommonDir(content);
|
|
7644
|
-
return target === null ? void 0 :
|
|
9457
|
+
return target === null ? void 0 : path20.resolve(gitDir, target);
|
|
7645
9458
|
}
|
|
7646
9459
|
async function headIn(dir, kind, io) {
|
|
7647
|
-
const marker =
|
|
9460
|
+
const marker = path20.join(dir, ".git");
|
|
7648
9461
|
let gitDir;
|
|
7649
9462
|
if (kind === "directory") {
|
|
7650
9463
|
gitDir = marker;
|
|
@@ -7652,11 +9465,11 @@ async function headIn(dir, kind, io) {
|
|
|
7652
9465
|
const content2 = await readGitFile(io, marker);
|
|
7653
9466
|
const target = typeof content2 === "string" ? parseGitFile(content2) : null;
|
|
7654
9467
|
if (target === null) return null;
|
|
7655
|
-
gitDir =
|
|
9468
|
+
gitDir = path20.resolve(dir, target);
|
|
7656
9469
|
} else {
|
|
7657
9470
|
return null;
|
|
7658
9471
|
}
|
|
7659
|
-
const content = await readGitFile(io,
|
|
9472
|
+
const content = await readGitFile(io, path20.join(gitDir, "HEAD"));
|
|
7660
9473
|
const head = typeof content === "string" ? parseHead(content) : null;
|
|
7661
9474
|
if (head === null) return null;
|
|
7662
9475
|
const commonDir = kind === "file" ? await commonDirOf(io, gitDir) : null;
|
|
@@ -7665,20 +9478,20 @@ async function headIn(dir, kind, io) {
|
|
|
7665
9478
|
return { head: { ...head, repository }, gitFolder: commonDir ?? gitDir };
|
|
7666
9479
|
}
|
|
7667
9480
|
async function findHead(cwd, { io, homeDir }) {
|
|
7668
|
-
if (!
|
|
7669
|
-
const home =
|
|
7670
|
-
let dir =
|
|
9481
|
+
if (!path20.isAbsolute(cwd)) return null;
|
|
9482
|
+
const home = path20.resolve(homeDir);
|
|
9483
|
+
let dir = path20.resolve(cwd);
|
|
7671
9484
|
try {
|
|
7672
9485
|
if ((await io.stat(dir)).kind !== "directory") return null;
|
|
7673
9486
|
} catch {
|
|
7674
9487
|
return null;
|
|
7675
9488
|
}
|
|
7676
9489
|
for (let level = 0; level < MAX_LEVELS; level += 1) {
|
|
7677
|
-
const parent =
|
|
9490
|
+
const parent = path20.dirname(dir);
|
|
7678
9491
|
if (dir === home || dir === parent) return null;
|
|
7679
9492
|
let kind;
|
|
7680
9493
|
try {
|
|
7681
|
-
kind = (await io.lstat(
|
|
9494
|
+
kind = (await io.lstat(path20.join(dir, ".git"))).kind;
|
|
7682
9495
|
} catch (error) {
|
|
7683
9496
|
if (!isMissing2(error)) return null;
|
|
7684
9497
|
dir = parent;
|
|
@@ -7699,7 +9512,7 @@ function within(work, ms) {
|
|
|
7699
9512
|
});
|
|
7700
9513
|
}
|
|
7701
9514
|
function createBranchFinder(options = {}) {
|
|
7702
|
-
const where = { io: options.io ?? nodeIo2, homeDir: options.homeDir ??
|
|
9515
|
+
const where = { io: options.io ?? nodeIo2, homeDir: options.homeDir ?? os8.homedir() };
|
|
7703
9516
|
const now = options.now ?? Date.now;
|
|
7704
9517
|
const waitMs = options.waitMs ?? READ_WAIT_MS;
|
|
7705
9518
|
const known = /* @__PURE__ */ new Map();
|
|
@@ -7761,14 +9574,14 @@ var STATUS_STATE = {
|
|
|
7761
9574
|
PENDING: "pending",
|
|
7762
9575
|
EXPECTED: "pending"
|
|
7763
9576
|
};
|
|
7764
|
-
function
|
|
9577
|
+
function isRecord12(value) {
|
|
7765
9578
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7766
9579
|
}
|
|
7767
9580
|
function textOf(value) {
|
|
7768
9581
|
return typeof value === "string" ? value : "";
|
|
7769
9582
|
}
|
|
7770
9583
|
function checkOf(value) {
|
|
7771
|
-
if (!
|
|
9584
|
+
if (!isRecord12(value)) return null;
|
|
7772
9585
|
const startedAt = Date.parse(textOf(value.startedAt));
|
|
7773
9586
|
const at = Number.isNaN(startedAt) ? -Infinity : startedAt;
|
|
7774
9587
|
if (value.__typename === "CheckRun") {
|
|
@@ -7804,7 +9617,7 @@ function checksOf(rollup) {
|
|
|
7804
9617
|
for (const { state } of latest.values()) counts[state] += 1;
|
|
7805
9618
|
return { state: checksStateOf(counts), ...counts };
|
|
7806
9619
|
}
|
|
7807
|
-
function
|
|
9620
|
+
function stateOf3(state, isDraft) {
|
|
7808
9621
|
if (state === "OPEN") return isDraft === true ? "draft" : "open";
|
|
7809
9622
|
if (state === "MERGED") return "merged";
|
|
7810
9623
|
if (state === "CLOSED") return "closed";
|
|
@@ -7817,9 +9630,9 @@ function readGhAnswer(stdout, repository) {
|
|
|
7817
9630
|
} catch {
|
|
7818
9631
|
return null;
|
|
7819
9632
|
}
|
|
7820
|
-
if (!
|
|
9633
|
+
if (!isRecord12(data)) return null;
|
|
7821
9634
|
const { number } = data;
|
|
7822
|
-
const state =
|
|
9635
|
+
const state = stateOf3(data.state, data.isDraft);
|
|
7823
9636
|
const title = typeof data.title === "string" ? sessionName(data.title) : void 0;
|
|
7824
9637
|
if (!isPullRequestNumber(number) || state === null || title === void 0) return null;
|
|
7825
9638
|
return {
|
|
@@ -7863,11 +9676,10 @@ function ghArguments(target) {
|
|
|
7863
9676
|
];
|
|
7864
9677
|
}
|
|
7865
9678
|
var LEFT_OUT = ["GH_FORCE_TTY", "GH_DEBUG", "DEBUG", "GH_PAGER", "PAGER"];
|
|
7866
|
-
var OWN_SETTING = /^AGENT_LOOKOUT_/;
|
|
7867
9679
|
function ghEnvironment(env) {
|
|
7868
9680
|
const given = {};
|
|
7869
|
-
for (const [name, value] of Object.entries(env)) {
|
|
7870
|
-
if (!
|
|
9681
|
+
for (const [name, value] of Object.entries(childEnvironment(env))) {
|
|
9682
|
+
if (!LEFT_OUT.includes(name)) given[name] = value;
|
|
7871
9683
|
}
|
|
7872
9684
|
return {
|
|
7873
9685
|
...given,
|
|
@@ -7956,7 +9768,7 @@ function createGhAsker(options) {
|
|
|
7956
9768
|
}
|
|
7957
9769
|
|
|
7958
9770
|
// src/collector/git/pullRequestTarget.ts
|
|
7959
|
-
import
|
|
9771
|
+
import path21 from "node:path";
|
|
7960
9772
|
|
|
7961
9773
|
// src/collector/git/gitConfig.ts
|
|
7962
9774
|
var MAX_CONFIG_BYTES = 64 * 1024;
|
|
@@ -8214,7 +10026,7 @@ function targetOf(config, branch, base, defaultBranch) {
|
|
|
8214
10026
|
async function readPullRequestTarget(gitFolder, branch, io = nodeIo2) {
|
|
8215
10027
|
try {
|
|
8216
10028
|
if (!isAskableBranch(branch)) return null;
|
|
8217
|
-
const text4 = await readGitFile(io,
|
|
10029
|
+
const text4 = await readGitFile(io, path21.join(gitFolder, "config"), MAX_CONFIG_BYTES);
|
|
8218
10030
|
if (typeof text4 !== "string") return null;
|
|
8219
10031
|
const config = parseGitConfig(text4);
|
|
8220
10032
|
const base = config === null ? null : baseRemoteOf(config);
|
|
@@ -8223,7 +10035,7 @@ async function readPullRequestTarget(gitFolder, branch, io = nodeIo2) {
|
|
|
8223
10035
|
if (base.name !== null && isSafeRemoteName(base.name)) {
|
|
8224
10036
|
const head = await readGitFile(
|
|
8225
10037
|
io,
|
|
8226
|
-
|
|
10038
|
+
path21.join(gitFolder, "refs", "remotes", base.name, "HEAD")
|
|
8227
10039
|
);
|
|
8228
10040
|
if (typeof head === "string") defaultBranch = parseRemoteHead(head, base.name);
|
|
8229
10041
|
}
|
|
@@ -8258,19 +10070,19 @@ function createPullRequestFinder(options) {
|
|
|
8258
10070
|
let gh = "unknown";
|
|
8259
10071
|
let last = null;
|
|
8260
10072
|
const due = (at, since) => at - (since ?? -Infinity) >= intervalMs;
|
|
8261
|
-
function take(question,
|
|
8262
|
-
switch (
|
|
10073
|
+
function take(question, answer2, at) {
|
|
10074
|
+
switch (answer2.kind) {
|
|
8263
10075
|
case "found":
|
|
8264
10076
|
case "none":
|
|
8265
10077
|
gh = "ready";
|
|
8266
10078
|
last = { at, ok: true };
|
|
8267
10079
|
answers2.set(question, {
|
|
8268
|
-
pullRequest:
|
|
10080
|
+
pullRequest: answer2.kind === "found" ? answer2.pullRequest : null,
|
|
8269
10081
|
askedAt: at
|
|
8270
10082
|
});
|
|
8271
10083
|
return true;
|
|
8272
10084
|
case "failed":
|
|
8273
|
-
last = { at, ok: false, reason:
|
|
10085
|
+
last = { at, ok: false, reason: answer2.reason };
|
|
8274
10086
|
answers2.set(question, {
|
|
8275
10087
|
pullRequest: answers2.get(question)?.pullRequest ?? null,
|
|
8276
10088
|
askedAt: at
|
|
@@ -8278,7 +10090,7 @@ function createPullRequestFinder(options) {
|
|
|
8278
10090
|
return true;
|
|
8279
10091
|
case "not-found":
|
|
8280
10092
|
case "signed-out":
|
|
8281
|
-
gh =
|
|
10093
|
+
gh = answer2.kind;
|
|
8282
10094
|
answers2.clear();
|
|
8283
10095
|
pausedUntil = at + intervalMs;
|
|
8284
10096
|
return false;
|
|
@@ -8298,14 +10110,14 @@ function createPullRequestFinder(options) {
|
|
|
8298
10110
|
const at = now();
|
|
8299
10111
|
if (at < pausedUntil) return;
|
|
8300
10112
|
if (!due(at, answers2.get(question)?.askedAt)) continue;
|
|
8301
|
-
const
|
|
10113
|
+
const answer2 = await ask(target).catch(() => ({
|
|
8302
10114
|
kind: "failed",
|
|
8303
10115
|
reason: "gh could not be run"
|
|
8304
10116
|
}));
|
|
8305
10117
|
if (![...reads.values()].some((read) => read.target && questionOf(read.target) === question)) {
|
|
8306
10118
|
continue;
|
|
8307
10119
|
}
|
|
8308
|
-
if (!take(question,
|
|
10120
|
+
if (!take(question, answer2, now())) return;
|
|
8309
10121
|
}
|
|
8310
10122
|
}
|
|
8311
10123
|
return {
|
|
@@ -8394,7 +10206,7 @@ function createClearHistoryRoute(options) {
|
|
|
8394
10206
|
}
|
|
8395
10207
|
|
|
8396
10208
|
// src/collector/history/historyKeeper.ts
|
|
8397
|
-
import
|
|
10209
|
+
import path22 from "node:path";
|
|
8398
10210
|
|
|
8399
10211
|
// src/collector/history/historyFiles.ts
|
|
8400
10212
|
import {
|
|
@@ -8694,7 +10506,7 @@ function createHistoryKeeper(options) {
|
|
|
8694
10506
|
});
|
|
8695
10507
|
const lock = createWriterLock({
|
|
8696
10508
|
fs,
|
|
8697
|
-
file:
|
|
10509
|
+
file: path22.join(dir, LOCK_FILE),
|
|
8698
10510
|
pid: options.pid,
|
|
8699
10511
|
now,
|
|
8700
10512
|
isAlive: options.isAlive,
|
|
@@ -8720,7 +10532,7 @@ function createHistoryKeeper(options) {
|
|
|
8720
10532
|
});
|
|
8721
10533
|
return next;
|
|
8722
10534
|
}
|
|
8723
|
-
const full = (name) =>
|
|
10535
|
+
const full = (name) => path22.join(dir, name);
|
|
8724
10536
|
function hold(record) {
|
|
8725
10537
|
if (unusable !== null || !running) return;
|
|
8726
10538
|
const line = encodeRecord(record);
|
|
@@ -8887,13 +10699,13 @@ function createHistoryKeeper(options) {
|
|
|
8887
10699
|
continue;
|
|
8888
10700
|
}
|
|
8889
10701
|
let text4 = lead;
|
|
8890
|
-
let
|
|
10702
|
+
let bytes3 = lead.length;
|
|
8891
10703
|
let end = index;
|
|
8892
10704
|
while (end < lines.length) {
|
|
8893
10705
|
const lineBytes = byteLength(lines[end]);
|
|
8894
|
-
if (
|
|
10706
|
+
if (bytes3 - lead.length + lineBytes > room) break;
|
|
8895
10707
|
text4 += lines[end];
|
|
8896
|
-
|
|
10708
|
+
bytes3 += lineBytes;
|
|
8897
10709
|
end += 1;
|
|
8898
10710
|
}
|
|
8899
10711
|
const outcome = yield { name, text: text4 };
|
|
@@ -8906,7 +10718,7 @@ function createHistoryKeeper(options) {
|
|
|
8906
10718
|
const parsed = readHistoryFileName(name);
|
|
8907
10719
|
files.set(name, {
|
|
8908
10720
|
...parsed,
|
|
8909
|
-
size: size +
|
|
10721
|
+
size: size + bytes3,
|
|
8910
10722
|
first: known?.first ?? (record && { at: recordAt(record), kind: record.kind })
|
|
8911
10723
|
});
|
|
8912
10724
|
touched.add(name);
|
|
@@ -9134,8 +10946,8 @@ function createHistoryKeeper(options) {
|
|
|
9134
10946
|
return { at: oldest.first.at, by: BEGINNING[oldest.first.kind] };
|
|
9135
10947
|
},
|
|
9136
10948
|
status() {
|
|
9137
|
-
let
|
|
9138
|
-
for (const file of files.values())
|
|
10949
|
+
let bytes3 = 0;
|
|
10950
|
+
for (const file of files.values()) bytes3 += file.size;
|
|
9139
10951
|
let problem = unusable ?? failing;
|
|
9140
10952
|
if (problem === null && writer === false) {
|
|
9141
10953
|
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 +10955,7 @@ function createHistoryKeeper(options) {
|
|
|
9143
10955
|
return {
|
|
9144
10956
|
where: "disk",
|
|
9145
10957
|
folder,
|
|
9146
|
-
bytes:
|
|
10958
|
+
bytes: bytes3,
|
|
9147
10959
|
maxBytes,
|
|
9148
10960
|
maxAgeMs,
|
|
9149
10961
|
canClear: unusable === null && writer === true && lock.held,
|
|
@@ -9180,15 +10992,15 @@ function keptHistoryStore(store, keeper) {
|
|
|
9180
10992
|
}
|
|
9181
10993
|
|
|
9182
10994
|
// src/collector/history/historySettings.ts
|
|
9183
|
-
import { createHash as
|
|
9184
|
-
import
|
|
9185
|
-
import
|
|
10995
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
10996
|
+
import os9 from "node:os";
|
|
10997
|
+
import path23 from "node:path";
|
|
9186
10998
|
var HISTORY_ENV = "AGENT_LOOKOUT_HISTORY";
|
|
9187
10999
|
var HISTORY_DIR_ENV = "AGENT_LOOKOUT_HISTORY_DIR";
|
|
9188
|
-
function readHistorySetup(env, homeDir =
|
|
11000
|
+
function readHistorySetup(env, homeDir = os9.homedir()) {
|
|
9189
11001
|
if (env[HISTORY_ENV]?.trim().toLowerCase() === "off") return { on: false };
|
|
9190
11002
|
const override = env[HISTORY_DIR_ENV]?.trim() || void 0;
|
|
9191
|
-
const dir =
|
|
11003
|
+
const dir = path23.resolve(override ?? path23.join(homeDir, ".agent-lookout", "history"));
|
|
9192
11004
|
return {
|
|
9193
11005
|
on: true,
|
|
9194
11006
|
dir,
|
|
@@ -9196,14 +11008,58 @@ function readHistorySetup(env, homeDir = os8.homedir()) {
|
|
|
9196
11008
|
sources: sourcesFingerprint(env, homeDir)
|
|
9197
11009
|
};
|
|
9198
11010
|
}
|
|
9199
|
-
function sourcesFingerprint(env, homeDir =
|
|
11011
|
+
function sourcesFingerprint(env, homeDir = os9.homedir()) {
|
|
9200
11012
|
const set = (name) => env[name]?.trim() || void 0;
|
|
9201
11013
|
const folders = [
|
|
9202
|
-
|
|
9203
|
-
|
|
9204
|
-
|
|
11014
|
+
path23.resolve(set(CLAUDE_HOME_ENV) ?? path23.join(homeDir, ".claude")),
|
|
11015
|
+
path23.resolve(set(CODEX_HOME_ENV) ?? set(CODEX_OWN_HOME_ENV) ?? path23.join(homeDir, ".codex")),
|
|
11016
|
+
path23.resolve(set(STATUS_DIR_ENV) ?? path23.join(homeDir, ".agent-lookout", "sessions")),
|
|
11017
|
+
path23.resolve(set(ANTIGRAVITY_HOME_ENV) ?? path23.join(homeDir, ...DEFAULT_HOME))
|
|
9205
11018
|
];
|
|
9206
|
-
return
|
|
11019
|
+
return createHash4("sha256").update(JSON.stringify(folders)).digest("hex").slice(0, 32);
|
|
11020
|
+
}
|
|
11021
|
+
|
|
11022
|
+
// src/collector/messages/lastMessageRoute.ts
|
|
11023
|
+
var answer = (body) => ({ status: 200, body });
|
|
11024
|
+
var failed3 = (status, error, headers) => ({
|
|
11025
|
+
status,
|
|
11026
|
+
body: { error },
|
|
11027
|
+
...headers && { headers }
|
|
11028
|
+
});
|
|
11029
|
+
function idIn(query) {
|
|
11030
|
+
const pairs = [...query];
|
|
11031
|
+
if (pairs.length !== 1) return null;
|
|
11032
|
+
const [name, value] = pairs[0] ?? ["", ""];
|
|
11033
|
+
if (name !== "id" || value === "" || value.length > MAX_SESSION_ID_LENGTH) return null;
|
|
11034
|
+
return value;
|
|
11035
|
+
}
|
|
11036
|
+
function createLastMessageRoute(options) {
|
|
11037
|
+
const { poller, adapters } = options;
|
|
11038
|
+
const off = lastMessageOff(options.env);
|
|
11039
|
+
return async (req, url) => {
|
|
11040
|
+
const site = req.headers["sec-fetch-site"];
|
|
11041
|
+
if (site !== void 0 && site !== "same-origin") {
|
|
11042
|
+
return failed3(403, "This address only answers the dashboard page served from this machine.");
|
|
11043
|
+
}
|
|
11044
|
+
const id = idIn(url.searchParams);
|
|
11045
|
+
if (id === null) {
|
|
11046
|
+
return failed3(400, "Name one session as id, and nothing else.");
|
|
11047
|
+
}
|
|
11048
|
+
if (off) return answer({ message: null, reason: "off", setting: LAST_MESSAGE_ENV });
|
|
11049
|
+
const session = poller.getSnapshot().sessions.find((listed) => listed.id === id);
|
|
11050
|
+
const notListed = () => failed3(404, "No session with that id is listed.");
|
|
11051
|
+
if (session === void 0) return notListed();
|
|
11052
|
+
const adapter = adapters.find((candidate) => candidate.id === session.source);
|
|
11053
|
+
if (adapter?.lastMessage === void 0) return answer({ message: null, reason: "not-read" });
|
|
11054
|
+
const read = await adapter.lastMessage(session.id);
|
|
11055
|
+
if (read === null) return notListed();
|
|
11056
|
+
if (read === "busy") {
|
|
11057
|
+
return failed3(429, "Too many last messages were asked for at once. Ask again in a second.", {
|
|
11058
|
+
"Retry-After": "1"
|
|
11059
|
+
});
|
|
11060
|
+
}
|
|
11061
|
+
return answer(read);
|
|
11062
|
+
};
|
|
9207
11063
|
}
|
|
9208
11064
|
|
|
9209
11065
|
// src/collector/notifications/heldWait.ts
|
|
@@ -9244,9 +11100,9 @@ function createServerNotifications(options) {
|
|
|
9244
11100
|
const held = /* @__PURE__ */ new Map();
|
|
9245
11101
|
const reminders = createReminderWatch();
|
|
9246
11102
|
const quiet = createQuietHold();
|
|
9247
|
-
function show(notice) {
|
|
11103
|
+
function show(notice, about) {
|
|
9248
11104
|
try {
|
|
9249
|
-
notifier.show(notice);
|
|
11105
|
+
notifier.show(notice, about);
|
|
9250
11106
|
} catch {
|
|
9251
11107
|
}
|
|
9252
11108
|
}
|
|
@@ -9343,82 +11199,506 @@ function createServerNotifications(options) {
|
|
|
9343
11199
|
}
|
|
9344
11200
|
continue;
|
|
9345
11201
|
}
|
|
9346
|
-
if (entry.kind === "change")
|
|
9347
|
-
|
|
9348
|
-
show(
|
|
11202
|
+
if (entry.kind === "change") {
|
|
11203
|
+
const { change } = entry;
|
|
11204
|
+
show(
|
|
11205
|
+
changeNotice(change),
|
|
11206
|
+
change.event === "needs-you" ? { sessionId: change.session.id } : void 0
|
|
11207
|
+
);
|
|
11208
|
+
} else if (entry.kind === "reminder") {
|
|
11209
|
+
const { session, begunAt } = entry.reminder;
|
|
11210
|
+
const waitedMs = at - begunAt;
|
|
11211
|
+
show(reminderNotice(session, waitedMs), { sessionId: session.id, waitedMs });
|
|
9349
11212
|
} else show(summaryNotice(entry.summary.items));
|
|
9350
11213
|
}
|
|
9351
11214
|
}
|
|
9352
11215
|
};
|
|
9353
11216
|
}
|
|
9354
11217
|
|
|
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) => {
|
|
11218
|
+
// src/collector/processes/osascript.ts
|
|
11219
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
11220
|
+
var OSASCRIPT = "/usr/bin/osascript";
|
|
11221
|
+
var MAX_OUTPUT_BYTES4 = 64 * 1024;
|
|
11222
|
+
var NOT_RUN2 = { ok: false, stderr: "", timedOut: false };
|
|
11223
|
+
function runOsascript(args, options = {}) {
|
|
11224
|
+
return new Promise((resolve) => {
|
|
11225
|
+
try {
|
|
11226
|
+
const child = execFile3(
|
|
11227
|
+
OSASCRIPT,
|
|
11228
|
+
[...args],
|
|
11229
|
+
{
|
|
11230
|
+
env: childEnvironment(process.env),
|
|
11231
|
+
timeout: options.timeoutMs ?? TERMINAL_JUMP_TIMEOUT_MS,
|
|
11232
|
+
maxBuffer: MAX_OUTPUT_BYTES4,
|
|
11233
|
+
encoding: "utf8",
|
|
11234
|
+
windowsHide: true
|
|
11235
|
+
},
|
|
11236
|
+
(error, stdout, stderr) => {
|
|
11237
|
+
if (!error) {
|
|
11238
|
+
resolve({ ok: true, stdout: String(stdout ?? "") });
|
|
11239
|
+
return;
|
|
11240
|
+
}
|
|
11241
|
+
const timedOut = error.killed === true;
|
|
11242
|
+
resolve({ ok: false, stderr: String(stderr ?? "").trim(), timedOut });
|
|
11243
|
+
}
|
|
11244
|
+
);
|
|
11245
|
+
child.stdin?.end();
|
|
11246
|
+
} catch {
|
|
11247
|
+
resolve(NOT_RUN2);
|
|
11248
|
+
}
|
|
11249
|
+
});
|
|
11250
|
+
}
|
|
11251
|
+
|
|
11252
|
+
// src/collector/notifications/systemNotifier.ts
|
|
11253
|
+
var OSASCRIPT_TIMEOUT_MS = 5e3;
|
|
11254
|
+
var SCRIPT = [
|
|
11255
|
+
"on run argv",
|
|
11256
|
+
"display notification (item 2 of argv) with title (item 1 of argv)",
|
|
11257
|
+
"end run"
|
|
11258
|
+
];
|
|
11259
|
+
function osascriptArgs(notice) {
|
|
11260
|
+
return [
|
|
11261
|
+
...SCRIPT.flatMap((line) => ["-e", line]),
|
|
11262
|
+
"--",
|
|
11263
|
+
oneLine(notice.title),
|
|
11264
|
+
oneLine(notice.body, MAX_NOTICE_TEXT_LENGTH)
|
|
11265
|
+
];
|
|
11266
|
+
}
|
|
11267
|
+
function systemNotificationsShownOn(platform) {
|
|
11268
|
+
return platform === "darwin";
|
|
11269
|
+
}
|
|
11270
|
+
var runForNotification = (args) => runOsascript(args, { timeoutMs: OSASCRIPT_TIMEOUT_MS });
|
|
11271
|
+
function createSystemNotifier(options = {}) {
|
|
11272
|
+
const platform = options.platform ?? process.platform;
|
|
11273
|
+
const run = options.run ?? runForNotification;
|
|
11274
|
+
return {
|
|
11275
|
+
show(notice) {
|
|
11276
|
+
if (!systemNotificationsShownOn(platform)) return;
|
|
11277
|
+
try {
|
|
11278
|
+
run(osascriptArgs(notice)).catch(() => {
|
|
11279
|
+
});
|
|
11280
|
+
} catch {
|
|
11281
|
+
}
|
|
11282
|
+
}
|
|
11283
|
+
};
|
|
11284
|
+
}
|
|
11285
|
+
|
|
11286
|
+
// src/collector/outbound/httpPost.ts
|
|
11287
|
+
var POST_TIMEOUT_MS = 1e4;
|
|
11288
|
+
function jsonHeaders(body, version, extra = {}) {
|
|
11289
|
+
return {
|
|
11290
|
+
"Content-Type": "application/json",
|
|
11291
|
+
"Content-Length": String(Buffer.byteLength(body)),
|
|
11292
|
+
"User-Agent": `Agent Lookout/${version}`,
|
|
11293
|
+
...extra
|
|
11294
|
+
};
|
|
11295
|
+
}
|
|
11296
|
+
function postOptions(body, version, extra = {}) {
|
|
11297
|
+
return {
|
|
11298
|
+
method: "POST",
|
|
11299
|
+
headers: jsonHeaders(body, version, extra),
|
|
11300
|
+
// A connection of its own, closed after the answer.
|
|
11301
|
+
agent: false,
|
|
11302
|
+
rejectUnauthorized: true
|
|
11303
|
+
};
|
|
11304
|
+
}
|
|
11305
|
+
function createJsonPoster(url, options) {
|
|
11306
|
+
const timeoutMs = options.timeoutMs ?? POST_TIMEOUT_MS;
|
|
11307
|
+
const extra = options.headers ?? {};
|
|
11308
|
+
let client = null;
|
|
11309
|
+
const load = () => {
|
|
11310
|
+
client ??= url.protocol === "https:" ? import("node:https").then(({ request: request2 }) => request2) : import("node:http").then(({ request: request2 }) => request2);
|
|
11311
|
+
return client;
|
|
11312
|
+
};
|
|
11313
|
+
return {
|
|
11314
|
+
async post(body) {
|
|
11315
|
+
let request2;
|
|
11316
|
+
try {
|
|
11317
|
+
request2 = await load();
|
|
11318
|
+
} catch {
|
|
11319
|
+
return { kind: "unsent" };
|
|
11320
|
+
}
|
|
11321
|
+
return new Promise((resolve) => {
|
|
11322
|
+
let finished = false;
|
|
11323
|
+
const finish = (result) => {
|
|
11324
|
+
if (finished) return;
|
|
11325
|
+
finished = true;
|
|
11326
|
+
resolve(result);
|
|
11327
|
+
};
|
|
11328
|
+
let req;
|
|
11329
|
+
try {
|
|
11330
|
+
req = request2(url, postOptions(body, options.version, extra));
|
|
11331
|
+
} catch {
|
|
11332
|
+
finish({ kind: "unsent" });
|
|
11333
|
+
return;
|
|
11334
|
+
}
|
|
11335
|
+
const timer = setTimeout(() => {
|
|
11336
|
+
finish({ kind: "timeout" });
|
|
11337
|
+
req.destroy();
|
|
11338
|
+
}, timeoutMs);
|
|
11339
|
+
timer.unref();
|
|
11340
|
+
req.on("response", (res) => {
|
|
11341
|
+
finish({ kind: "status", status: res.statusCode ?? 0 });
|
|
11342
|
+
res.on("close", () => clearTimeout(timer));
|
|
11343
|
+
res.resume();
|
|
11344
|
+
});
|
|
11345
|
+
req.on("error", (error) => {
|
|
11346
|
+
clearTimeout(timer);
|
|
11347
|
+
const { code } = error ?? {};
|
|
11348
|
+
finish({ kind: "error", code: typeof code === "string" ? code : null });
|
|
11349
|
+
});
|
|
11350
|
+
req.end(body);
|
|
11351
|
+
});
|
|
11352
|
+
}
|
|
11353
|
+
};
|
|
11354
|
+
}
|
|
11355
|
+
function connectionReason(code, words) {
|
|
11356
|
+
if (code === null) return words.notSent;
|
|
11357
|
+
if (code === "ECONNREFUSED") return `nothing answered at ${words.where}`;
|
|
11358
|
+
if (code === "ENOTFOUND" || code === "EAI_AGAIN") return `${words.whose} host could not be found`;
|
|
11359
|
+
if (code === "ETIMEDOUT") return `${words.where} did not answer in time`;
|
|
11360
|
+
if (/CERT|SELF_SIGNED|UNABLE_TO|^ERR_TLS|^ERR_SSL|^EPROTO$/.test(code)) {
|
|
11361
|
+
return `the secure connection to ${words.where} failed`;
|
|
11362
|
+
}
|
|
11363
|
+
return `the connection to ${words.where} failed`;
|
|
11364
|
+
}
|
|
11365
|
+
function outcomeOfPost(result, words) {
|
|
11366
|
+
switch (result.kind) {
|
|
11367
|
+
case "unsent":
|
|
11368
|
+
return { sent: false, reason: words.notSent };
|
|
11369
|
+
case "timeout":
|
|
11370
|
+
return { sent: false, reason: `${words.where} did not answer in time` };
|
|
11371
|
+
case "error":
|
|
11372
|
+
return { sent: false, reason: connectionReason(result.code, words) };
|
|
11373
|
+
case "status": {
|
|
11374
|
+
const { status } = result;
|
|
11375
|
+
if (status >= 200 && status < 300) return { sent: true };
|
|
11376
|
+
if (status >= 300 && status < 400) {
|
|
11377
|
+
return {
|
|
11378
|
+
sent: false,
|
|
11379
|
+
reason: `${words.where} answered with a redirect, which is not followed`
|
|
11380
|
+
};
|
|
11381
|
+
}
|
|
11382
|
+
if (status >= 400 && status < 500) return { sent: false, reason: words.refused(status) };
|
|
11383
|
+
if (status >= 500 && status < 600) return { sent: false, reason: words.troubled(status) };
|
|
11384
|
+
return { sent: false, reason: `${words.whose} answer could not be understood` };
|
|
11385
|
+
}
|
|
11386
|
+
}
|
|
11387
|
+
}
|
|
11388
|
+
|
|
11389
|
+
// src/collector/ntfy/ntfyMessage.ts
|
|
11390
|
+
var TAG = {
|
|
11391
|
+
"needs-you": "hourglass",
|
|
11392
|
+
reminder: "hourglass",
|
|
11393
|
+
test: "hourglass",
|
|
11394
|
+
finished: "white_check_mark",
|
|
11395
|
+
failed: "x",
|
|
11396
|
+
ended: "stop_sign",
|
|
11397
|
+
"quiet-summary": null
|
|
11398
|
+
};
|
|
11399
|
+
function ntfyBody(topic, message) {
|
|
11400
|
+
const tag = TAG[message.kind];
|
|
11401
|
+
return {
|
|
11402
|
+
topic,
|
|
11403
|
+
title: message.title,
|
|
11404
|
+
message: message.message,
|
|
11405
|
+
// A test sounds as a wait would, so it shows how one will arrive.
|
|
11406
|
+
priority: isWaitKind(message.kind) ? 4 : 3,
|
|
11407
|
+
...tag !== null && { tags: [tag] }
|
|
11408
|
+
};
|
|
11409
|
+
}
|
|
11410
|
+
|
|
11411
|
+
// src/collector/ntfy/ntfySender.ts
|
|
11412
|
+
var NTFY_WORDS = {
|
|
11413
|
+
where: "the ntfy server",
|
|
11414
|
+
whose: "the ntfy server's",
|
|
11415
|
+
notSent: "the push could not be sent",
|
|
11416
|
+
refused(status) {
|
|
11417
|
+
if (status === 401 || status === 403) {
|
|
11418
|
+
return `the ntfy server refused the access token or the topic (status ${status})`;
|
|
11419
|
+
}
|
|
11420
|
+
if (status === 413) return `the ntfy server refused the push as too large (status ${status})`;
|
|
11421
|
+
if (status === 429) return `the ntfy server's rate limit was reached (status ${status})`;
|
|
11422
|
+
return `the ntfy server refused the push (status ${status})`;
|
|
11423
|
+
},
|
|
11424
|
+
troubled: (status) => `the ntfy server had a problem (status ${status})`
|
|
11425
|
+
};
|
|
11426
|
+
function ntfyHeaders(settings) {
|
|
11427
|
+
return settings.token === null ? {} : { Authorization: `Bearer ${settings.token}` };
|
|
11428
|
+
}
|
|
11429
|
+
function createNtfySender(settings, options) {
|
|
11430
|
+
const poster = createJsonPoster(settings.server, { ...options, headers: ntfyHeaders(settings) });
|
|
11431
|
+
return {
|
|
11432
|
+
async send(message) {
|
|
11433
|
+
let body;
|
|
11434
|
+
try {
|
|
11435
|
+
body = JSON.stringify(ntfyBody(settings.topic, message));
|
|
11436
|
+
} catch {
|
|
11437
|
+
return { sent: false, reason: NTFY_WORDS.notSent };
|
|
11438
|
+
}
|
|
11439
|
+
return outcomeOfPost(await poster.post(body), NTFY_WORDS);
|
|
11440
|
+
}
|
|
11441
|
+
};
|
|
11442
|
+
}
|
|
11443
|
+
|
|
11444
|
+
// src/collector/ntfy/ntfySettings.ts
|
|
11445
|
+
var NTFY_URL_ENV = "AGENT_LOOKOUT_NTFY_URL";
|
|
11446
|
+
var NTFY_TOKEN_ENV = "AGENT_LOOKOUT_NTFY_TOKEN";
|
|
11447
|
+
var NTFY_EVENTS_ENV = "AGENT_LOOKOUT_NTFY_EVENTS";
|
|
11448
|
+
var NTFY_AFTER_ENV = "AGENT_LOOKOUT_NTFY_AFTER";
|
|
11449
|
+
var NTFY_ASKING_ENV = "AGENT_LOOKOUT_NTFY_ASKING";
|
|
11450
|
+
var MAX_URL_LENGTH = 2048;
|
|
11451
|
+
var TOPIC = /^[-_A-Za-z0-9]{1,64}$/;
|
|
11452
|
+
var PATH_PART = /^[-_.~A-Za-z0-9]+$/;
|
|
11453
|
+
var TOKEN = /^[-A-Za-z0-9._~+/=]{1,128}$/;
|
|
11454
|
+
var URL_UNREADABLE2 = `${NTFY_URL_ENV} could not be read. Copy the topic's whole address, beginning https://.`;
|
|
11455
|
+
function readTopicUrl(value) {
|
|
11456
|
+
let url;
|
|
11457
|
+
try {
|
|
11458
|
+
if (value.length > MAX_URL_LENGTH) throw new Error();
|
|
11459
|
+
url = new URL(value);
|
|
11460
|
+
} catch {
|
|
11461
|
+
throw new SettingProblem(URL_UNREADABLE2);
|
|
11462
|
+
}
|
|
11463
|
+
const local = LOCAL_HOSTS.has(url.hostname);
|
|
11464
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && local)) {
|
|
11465
|
+
throw new SettingProblem(
|
|
11466
|
+
`${NTFY_URL_ENV} must begin with https://, or with http:// for a server on this computer, 127.0.0.1 or localhost.`
|
|
11467
|
+
);
|
|
11468
|
+
}
|
|
11469
|
+
if (url.hostname === "") throw new SettingProblem(URL_UNREADABLE2);
|
|
11470
|
+
if (!isWebhookHost(url.hostname)) {
|
|
11471
|
+
throw new SettingProblem(
|
|
11472
|
+
`${NTFY_URL_ENV} must name its host in letters, digits, dots and dashes, such as ntfy.sh.`
|
|
11473
|
+
);
|
|
11474
|
+
}
|
|
11475
|
+
if (url.username !== "" || url.password !== "") {
|
|
11476
|
+
throw new SettingProblem(
|
|
11477
|
+
`${NTFY_URL_ENV} must not hold a user name or a password. Put an access token in ${NTFY_TOKEN_ENV}.`
|
|
11478
|
+
);
|
|
11479
|
+
}
|
|
11480
|
+
if (url.search !== "" || url.hash !== "" || value.includes("?") || value.includes("#")) {
|
|
11481
|
+
throw new SettingProblem(
|
|
11482
|
+
`${NTFY_URL_ENV} must end with the topic, with no ? or # after it. Put an access token in ${NTFY_TOKEN_ENV}.`
|
|
11483
|
+
);
|
|
11484
|
+
}
|
|
11485
|
+
const parts = url.pathname.split("/").filter((part) => part !== "");
|
|
11486
|
+
const topic = parts.pop();
|
|
11487
|
+
if (topic === void 0 || !TOPIC.test(topic) || !parts.every((part) => PATH_PART.test(part))) {
|
|
11488
|
+
throw new SettingProblem(
|
|
11489
|
+
`${NTFY_URL_ENV} must end with the topic, up to 64 letters, digits, dashes and underscores, as in https://ntfy.sh/your-topic.`
|
|
11490
|
+
);
|
|
11491
|
+
}
|
|
11492
|
+
const server = new URL(url.origin);
|
|
11493
|
+
server.pathname = `/${parts.map((part) => `${part}/`).join("")}`;
|
|
11494
|
+
return { server, topic };
|
|
11495
|
+
}
|
|
11496
|
+
function readToken(value) {
|
|
11497
|
+
if (value === void 0) return null;
|
|
11498
|
+
if (!TOKEN.test(value)) {
|
|
11499
|
+
throw new SettingProblem(
|
|
11500
|
+
`${NTFY_TOKEN_ENV} must be an access token of letters, digits and . _ ~ + / = -, such as one that begins tk_.`
|
|
11501
|
+
);
|
|
11502
|
+
}
|
|
11503
|
+
return value;
|
|
11504
|
+
}
|
|
11505
|
+
function readNtfySetup(env) {
|
|
11506
|
+
const url = valueOf(env, NTFY_URL_ENV);
|
|
11507
|
+
if (url === void 0) return { on: false, problem: null };
|
|
11508
|
+
try {
|
|
11509
|
+
return {
|
|
11510
|
+
on: true,
|
|
11511
|
+
settings: {
|
|
11512
|
+
...readTopicUrl(url),
|
|
11513
|
+
token: readToken(valueOf(env, NTFY_TOKEN_ENV)),
|
|
11514
|
+
events: readEvents(NTFY_EVENTS_ENV, valueOf(env, NTFY_EVENTS_ENV)),
|
|
11515
|
+
afterMs: readAfter(NTFY_AFTER_ENV, valueOf(env, NTFY_AFTER_ENV)),
|
|
11516
|
+
asking: readAsking(NTFY_ASKING_ENV, valueOf(env, NTFY_ASKING_ENV))
|
|
11517
|
+
}
|
|
11518
|
+
};
|
|
11519
|
+
} catch (error) {
|
|
11520
|
+
if (error instanceof SettingProblem) return { on: false, problem: error.message };
|
|
11521
|
+
return { on: false, problem: URL_UNREADABLE2 };
|
|
11522
|
+
}
|
|
11523
|
+
}
|
|
11524
|
+
function ntfyProblemLine(problem) {
|
|
11525
|
+
return `ntfy pushes are off: ${problem}`;
|
|
11526
|
+
}
|
|
11527
|
+
|
|
11528
|
+
// src/collector/outbound/testSendRoute.ts
|
|
11529
|
+
var MAX_TEST_BODY_BYTES = 64;
|
|
11530
|
+
var NAMED = {
|
|
11531
|
+
ntfy: { name: "ntfy", setting: "AGENT_LOOKOUT_NTFY_URL" },
|
|
11532
|
+
pushover: {
|
|
11533
|
+
name: "Pushover",
|
|
11534
|
+
setting: "AGENT_LOOKOUT_PUSHOVER_TOKEN and AGENT_LOOKOUT_PUSHOVER_USER"
|
|
11535
|
+
}
|
|
11536
|
+
};
|
|
11537
|
+
function channelIn(body) {
|
|
11538
|
+
let value;
|
|
11539
|
+
try {
|
|
11540
|
+
value = JSON.parse(body);
|
|
11541
|
+
} catch {
|
|
11542
|
+
return null;
|
|
11543
|
+
}
|
|
11544
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
11545
|
+
const keys = Object.keys(value);
|
|
11546
|
+
if (keys.length !== 1 || keys[0] !== "channel") return null;
|
|
11547
|
+
const { channel } = value;
|
|
11548
|
+
return PHONE_CHANNELS.find((known) => known === channel) ?? null;
|
|
11549
|
+
}
|
|
11550
|
+
function asSentence(reason) {
|
|
11551
|
+
return `${reason.charAt(0).toUpperCase()}${reason.slice(1)}.`;
|
|
11552
|
+
}
|
|
11553
|
+
var failed4 = (status, reason, error, more = {}) => ({
|
|
11554
|
+
status,
|
|
11555
|
+
body: {
|
|
11556
|
+
error,
|
|
11557
|
+
reason,
|
|
11558
|
+
...more.limitedUntil !== void 0 && { limitedUntil: more.limitedUntil }
|
|
11559
|
+
},
|
|
11560
|
+
...more.retryAfter !== void 0 && {
|
|
11561
|
+
headers: { "Retry-After": String(more.retryAfter) }
|
|
11562
|
+
}
|
|
11563
|
+
});
|
|
11564
|
+
function createTestSendRoute(options) {
|
|
11565
|
+
const { channels, limiter } = options;
|
|
11566
|
+
const now = options.now ?? Date.now;
|
|
11567
|
+
return async function answerTest(req) {
|
|
11568
|
+
const refused = actionRefusalFor(req, PHONE_TEST_ACTION, MAX_TEST_BODY_BYTES);
|
|
11569
|
+
if (refused) {
|
|
11570
|
+
req.resume();
|
|
11571
|
+
return refused;
|
|
11572
|
+
}
|
|
11573
|
+
const body = await readRequestBody(req, MAX_TEST_BODY_BYTES);
|
|
11574
|
+
if (!body.ok) {
|
|
11575
|
+
req.resume();
|
|
11576
|
+
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.");
|
|
11577
|
+
}
|
|
11578
|
+
const channel = channelIn(body.text);
|
|
11579
|
+
if (channel === null) {
|
|
11580
|
+
return refusal(400, 'The body must be {"channel": "ntfy"} or {"channel": "pushover"}.');
|
|
11581
|
+
}
|
|
11582
|
+
const { name, setting } = NAMED[channel];
|
|
11583
|
+
const tester = channels[channel];
|
|
11584
|
+
if (tester === null) {
|
|
11585
|
+
return failed4(
|
|
11586
|
+
409,
|
|
11587
|
+
"off",
|
|
11588
|
+
`${name} is not set up. Set ${setting} and start Agent Lookout again.`
|
|
11589
|
+
);
|
|
11590
|
+
}
|
|
11591
|
+
if (!limiter.begin()) {
|
|
11592
|
+
return failed4(429, "too-soon", "One test is sent a second. Try again in a moment.", {
|
|
11593
|
+
retryAfter: 1
|
|
11594
|
+
});
|
|
11595
|
+
}
|
|
9362
11596
|
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;
|
|
11597
|
+
const result = await tester.test();
|
|
11598
|
+
if (!result.tried) {
|
|
11599
|
+
return failed4(
|
|
11600
|
+
429,
|
|
11601
|
+
"limited",
|
|
11602
|
+
`${SENDS_PER_HOUR} ${name} pushes were tried in the last hour, the most it tries.`,
|
|
11603
|
+
{
|
|
11604
|
+
limitedUntil: result.limitedUntil,
|
|
11605
|
+
retryAfter: Math.max(1, Math.ceil((result.limitedUntil - now()) / 1e3))
|
|
9376
11606
|
}
|
|
9377
|
-
|
|
9378
|
-
|
|
9379
|
-
|
|
9380
|
-
|
|
9381
|
-
|
|
9382
|
-
|
|
9383
|
-
|
|
11607
|
+
);
|
|
11608
|
+
}
|
|
11609
|
+
if (!result.outcome.sent) {
|
|
11610
|
+
return failed4(502, "not-sent", asSentence(result.outcome.reason));
|
|
11611
|
+
}
|
|
11612
|
+
return {
|
|
11613
|
+
status: 200,
|
|
11614
|
+
body: { ok: true, channel, sentAt: result.at }
|
|
11615
|
+
};
|
|
11616
|
+
} finally {
|
|
11617
|
+
limiter.end();
|
|
9384
11618
|
}
|
|
9385
|
-
}
|
|
11619
|
+
};
|
|
9386
11620
|
}
|
|
9387
11621
|
|
|
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";
|
|
11622
|
+
// src/collector/pushover/pushoverMessage.ts
|
|
11623
|
+
function pushoverBody(token, user, message) {
|
|
11624
|
+
return {
|
|
11625
|
+
token,
|
|
11626
|
+
user,
|
|
11627
|
+
title: message.title,
|
|
11628
|
+
message: message.message,
|
|
11629
|
+
// A test sounds as a wait would, so it shows how one will arrive.
|
|
11630
|
+
priority: isWaitKind(message.kind) ? 0 : -1
|
|
11631
|
+
};
|
|
9405
11632
|
}
|
|
9406
|
-
|
|
9407
|
-
|
|
9408
|
-
|
|
9409
|
-
|
|
11633
|
+
|
|
11634
|
+
// src/collector/pushover/pushoverSender.ts
|
|
11635
|
+
var PUSHOVER_ENDPOINT = "https://api.pushover.net/1/messages.json";
|
|
11636
|
+
var PUSHOVER_WORDS = {
|
|
11637
|
+
where: "Pushover",
|
|
11638
|
+
whose: "Pushover's",
|
|
11639
|
+
notSent: "the push could not be sent",
|
|
11640
|
+
refused(status) {
|
|
11641
|
+
if (status === 400) {
|
|
11642
|
+
return `Pushover refused the push: check the application's token and the user key (status ${status})`;
|
|
11643
|
+
}
|
|
11644
|
+
if (status === 429)
|
|
11645
|
+
return `Pushover's monthly limit for your account was reached (status ${status})`;
|
|
11646
|
+
return `Pushover refused the push (status ${status})`;
|
|
11647
|
+
},
|
|
11648
|
+
troubled: (status) => `Pushover had a problem (status ${status})`
|
|
11649
|
+
};
|
|
11650
|
+
function createPushoverSender(settings, options) {
|
|
11651
|
+
const poster = createJsonPoster(options.endpoint ?? new URL(PUSHOVER_ENDPOINT), options);
|
|
9410
11652
|
return {
|
|
9411
|
-
|
|
9412
|
-
|
|
11653
|
+
async send(message) {
|
|
11654
|
+
let body;
|
|
9413
11655
|
try {
|
|
9414
|
-
|
|
9415
|
-
});
|
|
11656
|
+
body = JSON.stringify(pushoverBody(settings.token, settings.user, message));
|
|
9416
11657
|
} catch {
|
|
11658
|
+
return { sent: false, reason: PUSHOVER_WORDS.notSent };
|
|
9417
11659
|
}
|
|
11660
|
+
return outcomeOfPost(await poster.post(body), PUSHOVER_WORDS);
|
|
9418
11661
|
}
|
|
9419
11662
|
};
|
|
9420
11663
|
}
|
|
9421
11664
|
|
|
11665
|
+
// src/collector/pushover/pushoverSettings.ts
|
|
11666
|
+
var PUSHOVER_TOKEN_ENV = "AGENT_LOOKOUT_PUSHOVER_TOKEN";
|
|
11667
|
+
var PUSHOVER_USER_ENV = "AGENT_LOOKOUT_PUSHOVER_USER";
|
|
11668
|
+
var PUSHOVER_EVENTS_ENV = "AGENT_LOOKOUT_PUSHOVER_EVENTS";
|
|
11669
|
+
var PUSHOVER_AFTER_ENV = "AGENT_LOOKOUT_PUSHOVER_AFTER";
|
|
11670
|
+
var PUSHOVER_ASKING_ENV = "AGENT_LOOKOUT_PUSHOVER_ASKING";
|
|
11671
|
+
var KEY = /^[A-Za-z0-9]{30}$/;
|
|
11672
|
+
function readKey(name, value, what) {
|
|
11673
|
+
if (!KEY.test(value)) throw new SettingProblem(`${name} must be ${what}: 30 letters and digits.`);
|
|
11674
|
+
return value;
|
|
11675
|
+
}
|
|
11676
|
+
function readPushoverSetup(env) {
|
|
11677
|
+
const token = valueOf(env, PUSHOVER_TOKEN_ENV);
|
|
11678
|
+
const user = valueOf(env, PUSHOVER_USER_ENV);
|
|
11679
|
+
if (token === void 0 && user === void 0) return { on: false, problem: null };
|
|
11680
|
+
if (token === void 0) return { on: false, problem: `${PUSHOVER_TOKEN_ENV} is not set.` };
|
|
11681
|
+
if (user === void 0) return { on: false, problem: `${PUSHOVER_USER_ENV} is not set.` };
|
|
11682
|
+
try {
|
|
11683
|
+
return {
|
|
11684
|
+
on: true,
|
|
11685
|
+
settings: {
|
|
11686
|
+
token: readKey(PUSHOVER_TOKEN_ENV, token, "the API token of your Pushover application"),
|
|
11687
|
+
user: readKey(PUSHOVER_USER_ENV, user, "your Pushover user key, or a group key"),
|
|
11688
|
+
events: readEvents(PUSHOVER_EVENTS_ENV, valueOf(env, PUSHOVER_EVENTS_ENV)),
|
|
11689
|
+
afterMs: readAfter(PUSHOVER_AFTER_ENV, valueOf(env, PUSHOVER_AFTER_ENV)),
|
|
11690
|
+
asking: readAsking(PUSHOVER_ASKING_ENV, valueOf(env, PUSHOVER_ASKING_ENV))
|
|
11691
|
+
}
|
|
11692
|
+
};
|
|
11693
|
+
} catch (error) {
|
|
11694
|
+
if (error instanceof SettingProblem) return { on: false, problem: error.message };
|
|
11695
|
+
return { on: false, problem: `${PUSHOVER_TOKEN_ENV} could not be read.` };
|
|
11696
|
+
}
|
|
11697
|
+
}
|
|
11698
|
+
function pushoverProblemLine(problem) {
|
|
11699
|
+
return `Pushover pushes are off: ${problem}`;
|
|
11700
|
+
}
|
|
11701
|
+
|
|
9422
11702
|
// src/collector/remotes/remoteReader.ts
|
|
9423
11703
|
import { request } from "node:http";
|
|
9424
11704
|
var REMOTE_ANSWER_TIMEOUT_MS = 6e3;
|
|
@@ -9427,12 +11707,12 @@ function askRemote(port, route, timeoutMs = REMOTE_ANSWER_TIMEOUT_MS) {
|
|
|
9427
11707
|
return new Promise((resolve) => {
|
|
9428
11708
|
let connected = false;
|
|
9429
11709
|
let settled = false;
|
|
9430
|
-
const finish = (
|
|
11710
|
+
const finish = (answer2) => {
|
|
9431
11711
|
if (settled) return;
|
|
9432
11712
|
settled = true;
|
|
9433
11713
|
clearTimeout(timer);
|
|
9434
11714
|
req.destroy();
|
|
9435
|
-
resolve(
|
|
11715
|
+
resolve(answer2);
|
|
9436
11716
|
};
|
|
9437
11717
|
const req = request({
|
|
9438
11718
|
host: "127.0.0.1",
|
|
@@ -9495,7 +11775,7 @@ var MAX_REMOTE_SOURCES = 20;
|
|
|
9495
11775
|
var MAX_ID_LENGTH = 512;
|
|
9496
11776
|
var MAX_PATH_LENGTH = 4096;
|
|
9497
11777
|
var MAX_WORD_LENGTH2 = 200;
|
|
9498
|
-
function
|
|
11778
|
+
function isRecord13(value) {
|
|
9499
11779
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9500
11780
|
}
|
|
9501
11781
|
function oneOf2(allowed, value) {
|
|
@@ -9509,7 +11789,7 @@ function exact(value, max) {
|
|
|
9509
11789
|
var COMMIT_ID2 = /^[0-9a-f]{7,64}$/;
|
|
9510
11790
|
var REPOSITORY_ID = /^[0-9a-f]{16,64}$/;
|
|
9511
11791
|
function readGit(value, machine) {
|
|
9512
|
-
if (!
|
|
11792
|
+
if (!isRecord13(value)) return void 0;
|
|
9513
11793
|
let head;
|
|
9514
11794
|
if (value.commit !== void 0) {
|
|
9515
11795
|
if (value.branch !== void 0 || typeof value.commit !== "string") return void 0;
|
|
@@ -9521,7 +11801,7 @@ function readGit(value, machine) {
|
|
|
9521
11801
|
head = { branch };
|
|
9522
11802
|
}
|
|
9523
11803
|
const repository = value.repository;
|
|
9524
|
-
if (
|
|
11804
|
+
if (isRecord13(repository) && typeof repository.id === "string") {
|
|
9525
11805
|
const name = sessionName(typeof repository.name === "string" ? repository.name : "");
|
|
9526
11806
|
if (name !== void 0 && REPOSITORY_ID.test(repository.id)) {
|
|
9527
11807
|
head.repository = { id: repositoryId(`${machine}
|
|
@@ -9531,7 +11811,7 @@ ${repository.id}`), name };
|
|
|
9531
11811
|
return head;
|
|
9532
11812
|
}
|
|
9533
11813
|
function readSession(value, machine, sources, now, keepWaitingText) {
|
|
9534
|
-
if (!
|
|
11814
|
+
if (!isRecord13(value)) return null;
|
|
9535
11815
|
const id = exact(value.id, MAX_ID_LENGTH);
|
|
9536
11816
|
const source = exact(value.source, MAX_WORD_LENGTH2);
|
|
9537
11817
|
if (id === null || source === null) return null;
|
|
@@ -9571,7 +11851,7 @@ function readSession(value, machine, sources, now, keepWaitingText) {
|
|
|
9571
11851
|
return session;
|
|
9572
11852
|
}
|
|
9573
11853
|
function readCell(value) {
|
|
9574
|
-
if (!
|
|
11854
|
+
if (!isRecord13(value)) return null;
|
|
9575
11855
|
if (value.level === "yes") return { level: "yes" };
|
|
9576
11856
|
const level = oneOf2(["no", "partly"], value.level);
|
|
9577
11857
|
const reason = cut(value.reason, MAX_WORD_LENGTH2);
|
|
@@ -9579,7 +11859,7 @@ function readCell(value) {
|
|
|
9579
11859
|
}
|
|
9580
11860
|
var ACTS_HERE = ["jump", "stop", "answer"];
|
|
9581
11861
|
function readCapabilities(value, machine) {
|
|
9582
|
-
if (!
|
|
11862
|
+
if (!isRecord13(value)) return null;
|
|
9583
11863
|
const read = {};
|
|
9584
11864
|
for (const capability of CAPABILITIES) {
|
|
9585
11865
|
if (ACTS_HERE.includes(capability)) continue;
|
|
@@ -9595,7 +11875,7 @@ function readCapabilities(value, machine) {
|
|
|
9595
11875
|
};
|
|
9596
11876
|
}
|
|
9597
11877
|
function readRemoteSnapshot(value, machine, now, options = {}) {
|
|
9598
|
-
if (!
|
|
11878
|
+
if (!isRecord13(value) || !Array.isArray(value.sessions) || !Array.isArray(value.sources)) {
|
|
9599
11879
|
return null;
|
|
9600
11880
|
}
|
|
9601
11881
|
const named = /* @__PURE__ */ new Map();
|
|
@@ -9603,7 +11883,7 @@ function readRemoteSnapshot(value, machine, now, options = {}) {
|
|
|
9603
11883
|
const agents = [];
|
|
9604
11884
|
for (const item of value.sources) {
|
|
9605
11885
|
if (sources.length >= MAX_REMOTE_SOURCES) break;
|
|
9606
|
-
if (!
|
|
11886
|
+
if (!isRecord13(item)) continue;
|
|
9607
11887
|
const id = exact(item.id, MAX_WORD_LENGTH2);
|
|
9608
11888
|
if (id === null || isRemoteSource(id) || item.machine !== void 0) continue;
|
|
9609
11889
|
const label2 = cut(item.label, MAX_WORD_LENGTH2) ?? id;
|
|
@@ -9628,7 +11908,7 @@ function readRemoteSnapshot(value, machine, now, options = {}) {
|
|
|
9628
11908
|
}
|
|
9629
11909
|
|
|
9630
11910
|
// src/collector/remotes/sshProgram.ts
|
|
9631
|
-
import
|
|
11911
|
+
import path24 from "node:path";
|
|
9632
11912
|
var SSH_BIN_ENV = "AGENT_LOOKOUT_SSH_BIN";
|
|
9633
11913
|
var SSH_LOCATIONS = ["/usr/bin/ssh", "/opt/homebrew/bin/ssh", "/usr/local/bin/ssh"];
|
|
9634
11914
|
var SERVER_ALIVE_SECONDS = 15;
|
|
@@ -9650,7 +11930,7 @@ async function findSsh(env, isExecutable, platform = process.platform) {
|
|
|
9650
11930
|
}
|
|
9651
11931
|
return {
|
|
9652
11932
|
found: false,
|
|
9653
|
-
looked: `The ssh command was not found on PATH or in ${SSH_LOCATIONS.map((at) =>
|
|
11933
|
+
looked: `The ssh command was not found on PATH or in ${SSH_LOCATIONS.map((at) => path24.dirname(at)).join(", ")}`
|
|
9654
11934
|
};
|
|
9655
11935
|
}
|
|
9656
11936
|
function sshArguments(forward) {
|
|
@@ -9674,7 +11954,7 @@ function sshArguments(forward) {
|
|
|
9674
11954
|
}
|
|
9675
11955
|
|
|
9676
11956
|
// src/collector/remotes/remoteAdapter.ts
|
|
9677
|
-
var
|
|
11957
|
+
var BASIS4 = "api";
|
|
9678
11958
|
var NOT_CONNECTED = "unavailable";
|
|
9679
11959
|
var ANSWER_WAIT_MS = 500;
|
|
9680
11960
|
var STILL_CONNECTING_MS = 1e4;
|
|
@@ -9740,7 +12020,7 @@ function createRemoteAdapter(options) {
|
|
|
9740
12020
|
if (found.advice) health.advice = found.advice;
|
|
9741
12021
|
if (agents.length > 0) health.agents = agents;
|
|
9742
12022
|
const sessions = state === "ok" && snapshot ? snapshot.sessions : [];
|
|
9743
|
-
return state === "ok" ? { health, sessions, basis:
|
|
12023
|
+
return state === "ok" ? { health, sessions, basis: BASIS4 } : { health, sessions };
|
|
9744
12024
|
}
|
|
9745
12025
|
function connecting(at) {
|
|
9746
12026
|
if (problem === null) {
|
|
@@ -9992,7 +12272,11 @@ function freeLoopbackPort() {
|
|
|
9992
12272
|
});
|
|
9993
12273
|
});
|
|
9994
12274
|
}
|
|
9995
|
-
var spawnSsh = (binary, args, env) => spawn2(binary, [...args], {
|
|
12275
|
+
var spawnSsh = (binary, args, env) => spawn2(binary, [...args], {
|
|
12276
|
+
env: childEnvironment(env),
|
|
12277
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
12278
|
+
windowsHide: true
|
|
12279
|
+
});
|
|
9996
12280
|
var live = /* @__PURE__ */ new Set();
|
|
9997
12281
|
var endedOnExit = false;
|
|
9998
12282
|
function endOnExit(child) {
|
|
@@ -10245,15 +12529,15 @@ import {
|
|
|
10245
12529
|
unlinkSync as unlinkSync2,
|
|
10246
12530
|
writeSync as writeSync2
|
|
10247
12531
|
} from "node:fs";
|
|
10248
|
-
import
|
|
10249
|
-
import
|
|
12532
|
+
import os10 from "node:os";
|
|
12533
|
+
import path25 from "node:path";
|
|
10250
12534
|
var SETTINGS_FILE_ENV = "AGENT_LOOKOUT_SETTINGS_FILE";
|
|
10251
12535
|
var SETTINGS_FILE_MODE = 384;
|
|
10252
12536
|
var SETTINGS_FOLDER_MODE = 448;
|
|
10253
12537
|
var MAX_SETTINGS_BYTES = 64 * 1024;
|
|
10254
|
-
function readSettingsSetup(env, homeDir =
|
|
12538
|
+
function readSettingsSetup(env, homeDir = os10.homedir()) {
|
|
10255
12539
|
const named = env[SETTINGS_FILE_ENV]?.trim() || void 0;
|
|
10256
|
-
const file =
|
|
12540
|
+
const file = path25.resolve(named ?? path25.join(homeDir, ".agent-lookout", "settings.json"));
|
|
10257
12541
|
return { file, shown: tildify(file, homeDir) };
|
|
10258
12542
|
}
|
|
10259
12543
|
function codeOf(error) {
|
|
@@ -10298,7 +12582,7 @@ function readSettingsStamp(setup) {
|
|
|
10298
12582
|
}
|
|
10299
12583
|
function readSettingsText(setup) {
|
|
10300
12584
|
const { file, shown } = setup;
|
|
10301
|
-
const folder =
|
|
12585
|
+
const folder = path25.dirname(file);
|
|
10302
12586
|
try {
|
|
10303
12587
|
const folderKind = kindAt(folder);
|
|
10304
12588
|
if (folderKind === null) return { kind: "missing" };
|
|
@@ -10357,7 +12641,7 @@ function readSettingsText(setup) {
|
|
|
10357
12641
|
}
|
|
10358
12642
|
function writeSettingsText(setup, text4) {
|
|
10359
12643
|
const { file, shown } = setup;
|
|
10360
|
-
const folder =
|
|
12644
|
+
const folder = path25.dirname(file);
|
|
10361
12645
|
const notSaved = {
|
|
10362
12646
|
ok: false,
|
|
10363
12647
|
problem: `${shown} could not be written, so the change was not saved.`
|
|
@@ -10378,9 +12662,9 @@ function writeSettingsText(setup, text4) {
|
|
|
10378
12662
|
problem: `${shown} is a link or not an ordinary file, which Agent Lookout does not write through, so the change was not saved.`
|
|
10379
12663
|
};
|
|
10380
12664
|
}
|
|
10381
|
-
temporary =
|
|
12665
|
+
temporary = path25.join(
|
|
10382
12666
|
folder,
|
|
10383
|
-
`.${
|
|
12667
|
+
`.${path25.basename(file)}.${process.pid}.${randomBytes3(6).toString("hex")}.tmp`
|
|
10384
12668
|
);
|
|
10385
12669
|
const fd = openWithoutFollowingNow(
|
|
10386
12670
|
temporary,
|
|
@@ -10420,7 +12704,7 @@ var RULE_WORDS = {
|
|
|
10420
12704
|
idle: "the idle rule",
|
|
10421
12705
|
quietHours: "the quiet hours"
|
|
10422
12706
|
};
|
|
10423
|
-
function
|
|
12707
|
+
function isRecord14(value) {
|
|
10424
12708
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10425
12709
|
}
|
|
10426
12710
|
function listOf(names) {
|
|
@@ -10458,7 +12742,7 @@ function rulesIn(read, shown) {
|
|
|
10458
12742
|
} catch {
|
|
10459
12743
|
value = null;
|
|
10460
12744
|
}
|
|
10461
|
-
if (!
|
|
12745
|
+
if (!isRecord14(value)) {
|
|
10462
12746
|
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
12747
|
return {
|
|
10464
12748
|
rules: off,
|
|
@@ -10473,7 +12757,7 @@ function rulesIn(read, shown) {
|
|
|
10473
12757
|
const { timeRules, permissionRules: heldRules, ...rest } = value;
|
|
10474
12758
|
const permissions = readPermissionRules(heldRules);
|
|
10475
12759
|
const { rules, unread } = readTimeRules(timeRules);
|
|
10476
|
-
const otherRules =
|
|
12760
|
+
const otherRules = isRecord14(timeRules) ? Object.fromEntries(
|
|
10477
12761
|
Object.entries(timeRules).filter(
|
|
10478
12762
|
([name]) => !TIME_RULE_NAMES.includes(name)
|
|
10479
12763
|
)
|
|
@@ -10575,15 +12859,15 @@ import { randomBytes as randomBytes4 } from "node:crypto";
|
|
|
10575
12859
|
|
|
10576
12860
|
// src/core/permission-rules/rulesChange.ts
|
|
10577
12861
|
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
|
|
12862
|
+
function isRecord15(value) {
|
|
10579
12863
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10580
12864
|
}
|
|
10581
12865
|
function rulesChangeIn(value) {
|
|
10582
12866
|
const refused = { ok: false, problem: CHANGE_SHAPE };
|
|
10583
|
-
if (!
|
|
12867
|
+
if (!isRecord15(value) || Object.keys(value).length !== 1) return refused;
|
|
10584
12868
|
const [kind] = Object.keys(value);
|
|
10585
12869
|
const body = value[kind];
|
|
10586
|
-
if (!
|
|
12870
|
+
if (!isRecord15(body)) return refused;
|
|
10587
12871
|
switch (kind) {
|
|
10588
12872
|
case "add": {
|
|
10589
12873
|
const read = ruleWordsIn(body);
|
|
@@ -10659,7 +12943,7 @@ function applyRulesChange(rules, change, newId) {
|
|
|
10659
12943
|
|
|
10660
12944
|
// src/collector/settings/permissionRulesRoute.ts
|
|
10661
12945
|
var MAX_PERMISSION_RULES_BODY_BYTES = 1024;
|
|
10662
|
-
var
|
|
12946
|
+
var failed5 = (status, reason, error) => ({
|
|
10663
12947
|
status,
|
|
10664
12948
|
body: { error, reason }
|
|
10665
12949
|
});
|
|
@@ -10686,14 +12970,14 @@ function createPermissionRulesRoute(options) {
|
|
|
10686
12970
|
try {
|
|
10687
12971
|
value = JSON.parse(body.text);
|
|
10688
12972
|
} catch {
|
|
10689
|
-
return
|
|
12973
|
+
return failed5(400, "invalid", "The body must be JSON: one change to the permission rules.");
|
|
10690
12974
|
}
|
|
10691
12975
|
const asked = rulesChangeIn(value);
|
|
10692
|
-
if (!asked.ok) return
|
|
12976
|
+
if (!asked.ok) return failed5(400, "invalid", asked.problem);
|
|
10693
12977
|
const made = settings.changePermissionRules(
|
|
10694
12978
|
(rules) => applyRulesChange(rules, asked.change, newId)
|
|
10695
12979
|
);
|
|
10696
|
-
if (!made.ok) return
|
|
12980
|
+
if (!made.ok) return failed5(STATUS_OF[made.reason], made.reason, made.problem);
|
|
10697
12981
|
return {
|
|
10698
12982
|
status: 200,
|
|
10699
12983
|
body: { ok: true, permissionRules: made.rules }
|
|
@@ -10703,7 +12987,7 @@ function createPermissionRulesRoute(options) {
|
|
|
10703
12987
|
|
|
10704
12988
|
// src/collector/settings/timeRulesRoute.ts
|
|
10705
12989
|
var MAX_TIME_RULES_BODY_BYTES = 2048;
|
|
10706
|
-
var
|
|
12990
|
+
var failed6 = (status, reason, error) => ({
|
|
10707
12991
|
status,
|
|
10708
12992
|
body: { error, reason }
|
|
10709
12993
|
});
|
|
@@ -10727,12 +13011,12 @@ function createTimeRulesRoute(options) {
|
|
|
10727
13011
|
try {
|
|
10728
13012
|
value = JSON.parse(body.text);
|
|
10729
13013
|
} catch {
|
|
10730
|
-
return
|
|
13014
|
+
return failed6(400, "invalid", "The body must be JSON: the three time rules.");
|
|
10731
13015
|
}
|
|
10732
13016
|
const asked = timeRulesIn(value);
|
|
10733
|
-
if (!asked.ok) return
|
|
13017
|
+
if (!asked.ok) return failed6(400, "invalid", asked.problem);
|
|
10734
13018
|
const saved = settings.changeTimeRules(asked.rules);
|
|
10735
|
-
if (!saved.ok) return
|
|
13019
|
+
if (!saved.ok) return failed6(500, "not-saved", saved.problem);
|
|
10736
13020
|
return {
|
|
10737
13021
|
status: 200,
|
|
10738
13022
|
body: { ok: true, timeRules: asked.rules }
|
|
@@ -10755,8 +13039,8 @@ function parseProcessTable(stdout) {
|
|
|
10755
13039
|
}
|
|
10756
13040
|
var readProcessTableWithPs = async () => {
|
|
10757
13041
|
if (process.platform !== "darwin") return /* @__PURE__ */ new Map();
|
|
10758
|
-
const
|
|
10759
|
-
return
|
|
13042
|
+
const answer2 = await runPs(PS_TABLE_ARGS);
|
|
13043
|
+
return answer2.ok ? parseProcessTable(answer2.stdout) : /* @__PURE__ */ new Map();
|
|
10760
13044
|
};
|
|
10761
13045
|
function parentsIn(table) {
|
|
10762
13046
|
const parents = /* @__PURE__ */ new Map();
|
|
@@ -10776,11 +13060,11 @@ function createOsascriptRunner(options) {
|
|
|
10776
13060
|
}
|
|
10777
13061
|
|
|
10778
13062
|
// src/collector/terminal/tabFinder.ts
|
|
10779
|
-
import
|
|
13063
|
+
import os11 from "node:os";
|
|
10780
13064
|
var TAB_LOOK_SOONEST_MS = 5e3;
|
|
10781
13065
|
function createTabFinder(options) {
|
|
10782
13066
|
const off = terminalJumpOff(options.env);
|
|
10783
|
-
const homeDir = options.homeDir ??
|
|
13067
|
+
const homeDir = options.homeDir ?? os11.homedir();
|
|
10784
13068
|
const readProcesses = options.readProcesses ?? readProcessTableWithPs;
|
|
10785
13069
|
const now = options.now ?? Date.now;
|
|
10786
13070
|
const soonestMs = options.soonestMs ?? TAB_LOOK_SOONEST_MS;
|
|
@@ -10900,7 +13184,7 @@ function runTmuxBinary(binary, args, options) {
|
|
|
10900
13184
|
binary,
|
|
10901
13185
|
[...args],
|
|
10902
13186
|
{
|
|
10903
|
-
env: options.env,
|
|
13187
|
+
env: childEnvironment(options.env),
|
|
10904
13188
|
timeout: options.timeoutMs ?? TMUX_TIMEOUT_MS,
|
|
10905
13189
|
maxBuffer: MAX_OUTPUT_BYTES5,
|
|
10906
13190
|
encoding: "utf8",
|
|
@@ -11014,96 +13298,24 @@ function ledgerHistoryStore(store, ledger) {
|
|
|
11014
13298
|
}
|
|
11015
13299
|
|
|
11016
13300
|
// 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
|
-
}
|
|
13301
|
+
var WORDS = {
|
|
13302
|
+
where: "the address",
|
|
13303
|
+
whose: "the address's",
|
|
13304
|
+
notSent: "the post could not be sent",
|
|
13305
|
+
refused: (status) => `the address refused the post (status ${status})`,
|
|
13306
|
+
troubled: (status) => `the receiving service had a problem (status ${status})`
|
|
13307
|
+
};
|
|
11059
13308
|
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
|
-
};
|
|
13309
|
+
const poster = createJsonPoster(settings.url, options);
|
|
11067
13310
|
return {
|
|
11068
13311
|
async send(post) {
|
|
11069
|
-
let
|
|
13312
|
+
let body;
|
|
11070
13313
|
try {
|
|
11071
|
-
|
|
13314
|
+
body = JSON.stringify(post);
|
|
11072
13315
|
} catch {
|
|
11073
|
-
return { sent: false, reason:
|
|
13316
|
+
return { sent: false, reason: WORDS.notSent };
|
|
11074
13317
|
}
|
|
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
|
-
});
|
|
13318
|
+
return outcomeOfPost(await poster.post(body), WORDS);
|
|
11107
13319
|
}
|
|
11108
13320
|
};
|
|
11109
13321
|
}
|
|
@@ -11113,15 +13325,15 @@ var WEBHOOK_URL_ENV = "AGENT_LOOKOUT_WEBHOOK_URL";
|
|
|
11113
13325
|
var WEBHOOK_EVENTS_ENV = "AGENT_LOOKOUT_WEBHOOK_EVENTS";
|
|
11114
13326
|
var WEBHOOK_AFTER_ENV = "AGENT_LOOKOUT_WEBHOOK_AFTER";
|
|
11115
13327
|
var WEBHOOK_ASKING_ENV = "AGENT_LOOKOUT_WEBHOOK_ASKING";
|
|
11116
|
-
var
|
|
11117
|
-
var
|
|
13328
|
+
var MAX_URL_LENGTH2 = 2048;
|
|
13329
|
+
var URL_UNREADABLE3 = `${WEBHOOK_URL_ENV} could not be read. Copy the whole address, beginning https://.`;
|
|
11118
13330
|
function readUrl(value) {
|
|
11119
13331
|
let url;
|
|
11120
13332
|
try {
|
|
11121
|
-
if (value.length >
|
|
13333
|
+
if (value.length > MAX_URL_LENGTH2) throw new Error();
|
|
11122
13334
|
url = new URL(value);
|
|
11123
13335
|
} catch {
|
|
11124
|
-
throw new SettingProblem(
|
|
13336
|
+
throw new SettingProblem(URL_UNREADABLE3);
|
|
11125
13337
|
}
|
|
11126
13338
|
const local = LOCAL_HOSTS.has(url.hostname);
|
|
11127
13339
|
if (url.protocol !== "https:" && !(url.protocol === "http:" && local)) {
|
|
@@ -11129,7 +13341,7 @@ function readUrl(value) {
|
|
|
11129
13341
|
`${WEBHOOK_URL_ENV} must begin with https://, or with http:// for an address on this computer, 127.0.0.1 or localhost.`
|
|
11130
13342
|
);
|
|
11131
13343
|
}
|
|
11132
|
-
if (url.hostname === "") throw new SettingProblem(
|
|
13344
|
+
if (url.hostname === "") throw new SettingProblem(URL_UNREADABLE3);
|
|
11133
13345
|
if (!isWebhookHost(url.hostname)) {
|
|
11134
13346
|
throw new SettingProblem(
|
|
11135
13347
|
`${WEBHOOK_URL_ENV} must name its host in letters, digits, dots and dashes, such as hooks.slack.com.`
|
|
@@ -11155,7 +13367,7 @@ function readWebhookSetup(env) {
|
|
|
11155
13367
|
};
|
|
11156
13368
|
} catch (error) {
|
|
11157
13369
|
if (error instanceof SettingProblem) return { on: false, problem: error.message };
|
|
11158
|
-
return { on: false, problem:
|
|
13370
|
+
return { on: false, problem: URL_UNREADABLE3 };
|
|
11159
13371
|
}
|
|
11160
13372
|
}
|
|
11161
13373
|
function webhookProblemLine(problem) {
|
|
@@ -11252,6 +13464,26 @@ function createCollector(options) {
|
|
|
11252
13464
|
now
|
|
11253
13465
|
}) : null;
|
|
11254
13466
|
const webhookOff = webhookOffStatus(webhookSetup.on ? null : webhookSetup.problem);
|
|
13467
|
+
const ntfySetup = readNtfySetup(env);
|
|
13468
|
+
if (!ntfySetup.on && ntfySetup.problem !== null) warn(ntfyProblemLine(ntfySetup.problem));
|
|
13469
|
+
const makeNtfySender = options.createNtfySender ?? ((settings2) => createNtfySender(settings2, { version: options.version }));
|
|
13470
|
+
const ntfy = ntfySetup.on ? createNtfyNotifications({
|
|
13471
|
+
settings: ntfySetup.settings,
|
|
13472
|
+
sender: makeNtfySender(ntfySetup.settings),
|
|
13473
|
+
now
|
|
13474
|
+
}) : null;
|
|
13475
|
+
const ntfyOff = ntfyOffStatus(ntfySetup.on ? null : ntfySetup.problem);
|
|
13476
|
+
const pushoverSetup = readPushoverSetup(env);
|
|
13477
|
+
if (!pushoverSetup.on && pushoverSetup.problem !== null) {
|
|
13478
|
+
warn(pushoverProblemLine(pushoverSetup.problem));
|
|
13479
|
+
}
|
|
13480
|
+
const makePushoverSender = options.createPushoverSender ?? ((settings2) => createPushoverSender(settings2, { version: options.version }));
|
|
13481
|
+
const pushover = pushoverSetup.on ? createPushoverNotifications({
|
|
13482
|
+
settings: pushoverSetup.settings,
|
|
13483
|
+
sender: makePushoverSender(pushoverSetup.settings),
|
|
13484
|
+
now
|
|
13485
|
+
}) : null;
|
|
13486
|
+
const pushoverOff = pushoverOffStatus(pushoverSetup.on ? null : pushoverSetup.problem);
|
|
11255
13487
|
const remotes = createRemotes({
|
|
11256
13488
|
env,
|
|
11257
13489
|
version: options.version,
|
|
@@ -11272,6 +13504,7 @@ function createCollector(options) {
|
|
|
11272
13504
|
platform
|
|
11273
13505
|
}),
|
|
11274
13506
|
createCodexAdapter({ env: options.env, now, pollIntervalMs: intervalMs }),
|
|
13507
|
+
createAntigravityAdapter({ env: options.env, now, pollIntervalMs: intervalMs, platform }),
|
|
11275
13508
|
createStatusFileAdapter({ env: options.env, now, pollIntervalMs: intervalMs })
|
|
11276
13509
|
];
|
|
11277
13510
|
const poller = createPoller({
|
|
@@ -11306,6 +13539,8 @@ function createCollector(options) {
|
|
|
11306
13539
|
answering?.observe(snapshot);
|
|
11307
13540
|
email?.handle(snapshot);
|
|
11308
13541
|
webhook?.handle(snapshot);
|
|
13542
|
+
ntfy?.handle(snapshot);
|
|
13543
|
+
pushover?.handle(snapshot);
|
|
11309
13544
|
notifications.handle(snapshot);
|
|
11310
13545
|
options.onSnapshot?.(snapshot);
|
|
11311
13546
|
}
|
|
@@ -11348,8 +13583,19 @@ function createCollector(options) {
|
|
|
11348
13583
|
notifications,
|
|
11349
13584
|
email: () => email?.status() ?? emailOff,
|
|
11350
13585
|
webhook: () => webhook?.status() ?? webhookOff,
|
|
13586
|
+
ntfy: () => ntfy?.status() ?? ntfyOff,
|
|
13587
|
+
pushover: () => pushover?.status() ?? pushoverOff,
|
|
13588
|
+
phoneTest: createTestSendRoute({
|
|
13589
|
+
channels: { ntfy, pushover },
|
|
13590
|
+
limiter: createActionLimiter(now),
|
|
13591
|
+
now
|
|
13592
|
+
}),
|
|
11351
13593
|
pullRequests: () => pullRequests?.status() ?? pullRequestsOff,
|
|
11352
13594
|
jump: createJumpRoute({ poller, panes, run: tmux, tabs, osascript, now }),
|
|
13595
|
+
// Only this computer's own adapters read what a session last said. A
|
|
13596
|
+
// session on another machine is answered as not read, and nothing is
|
|
13597
|
+
// asked of that machine.
|
|
13598
|
+
lastMessage: createLastMessageRoute({ env, poller, adapters: localAdapters }),
|
|
11353
13599
|
stop: stopRoutes && createStopRoute(stopRoutes),
|
|
11354
13600
|
cleanUp: stopRoutes && createCleanUpRoute(stopRoutes),
|
|
11355
13601
|
settings: () => ({
|
|
@@ -11442,13 +13688,20 @@ function createCollector(options) {
|
|
|
11442
13688
|
poller,
|
|
11443
13689
|
email,
|
|
11444
13690
|
webhook,
|
|
13691
|
+
ntfy,
|
|
13692
|
+
pushover,
|
|
11445
13693
|
pullRequests,
|
|
11446
13694
|
history: keeper,
|
|
11447
13695
|
answering: {
|
|
11448
13696
|
start: () => answerStart ??= answering?.start() ?? Promise.resolve(),
|
|
11449
13697
|
stop: async () => {
|
|
11450
13698
|
await answering?.stop();
|
|
11451
|
-
}
|
|
13699
|
+
},
|
|
13700
|
+
serve: (snapshot) => answering?.serve(snapshot) ?? snapshot,
|
|
13701
|
+
check: async () => {
|
|
13702
|
+
await answering?.check();
|
|
13703
|
+
},
|
|
13704
|
+
...answering?.answer && { answer: answering.answer }
|
|
11452
13705
|
},
|
|
11453
13706
|
remotes,
|
|
11454
13707
|
settings
|
|
@@ -11472,7 +13725,7 @@ async function runStandalone(options) {
|
|
|
11472
13725
|
} catch (error) {
|
|
11473
13726
|
return stopWith(error.message);
|
|
11474
13727
|
}
|
|
11475
|
-
if (!existsSync(
|
|
13728
|
+
if (!existsSync(path26.join(distDir, "index.html"))) {
|
|
11476
13729
|
return stopWith(
|
|
11477
13730
|
"The dashboard has not been built yet. Run `npm run build`, then start it again."
|
|
11478
13731
|
);
|
|
@@ -11515,12 +13768,12 @@ async function runStandalone(options) {
|
|
|
11515
13768
|
|
|
11516
13769
|
// src/cli/start/openBrowser.ts
|
|
11517
13770
|
import { spawn as spawn3 } from "node:child_process";
|
|
11518
|
-
import
|
|
13771
|
+
import path27 from "node:path";
|
|
11519
13772
|
var OPEN_WAIT_MS = 1e4;
|
|
11520
13773
|
var WINDOWS_FOLDER = "C:\\Windows";
|
|
11521
13774
|
function windowsFolder(env) {
|
|
11522
13775
|
const named = env.SystemRoot?.trim() || env.SYSTEMROOT?.trim();
|
|
11523
|
-
return named &&
|
|
13776
|
+
return named && path27.win32.isAbsolute(named) ? named : WINDOWS_FOLDER;
|
|
11524
13777
|
}
|
|
11525
13778
|
function openCommand(platform, url, env = process.env) {
|
|
11526
13779
|
const address = loopbackAddress(url);
|
|
@@ -11529,7 +13782,7 @@ function openCommand(platform, url, env = process.env) {
|
|
|
11529
13782
|
if (platform === "linux") return { file: "xdg-open", args: [address] };
|
|
11530
13783
|
if (platform === "win32") {
|
|
11531
13784
|
return {
|
|
11532
|
-
file:
|
|
13785
|
+
file: path27.win32.join(windowsFolder(env), "System32", "rundll32.exe"),
|
|
11533
13786
|
args: ["url.dll,FileProtocolHandler", address]
|
|
11534
13787
|
};
|
|
11535
13788
|
}
|
|
@@ -11543,7 +13796,12 @@ function runProgram2(file, args, waitMs = OPEN_WAIT_MS) {
|
|
|
11543
13796
|
resolve(opened);
|
|
11544
13797
|
};
|
|
11545
13798
|
try {
|
|
11546
|
-
const child = spawn3(file, [...args], {
|
|
13799
|
+
const child = spawn3(file, [...args], {
|
|
13800
|
+
env: childEnvironment(process.env),
|
|
13801
|
+
stdio: "ignore",
|
|
13802
|
+
detached: true,
|
|
13803
|
+
windowsHide: true
|
|
13804
|
+
});
|
|
11547
13805
|
child.once("error", () => settle(false));
|
|
11548
13806
|
child.once("exit", (code) => settle(code === 0));
|
|
11549
13807
|
child.unref();
|