agent-lookout 0.2.8 → 0.2.10
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 +35 -1
- package/PRIVACY.md +39 -23
- package/README.md +7 -6
- package/SECURITY.md +12 -7
- package/dist/assets/{HistoryPanel-CQ7zovsM.js → HistoryPanel-C5-cd24R.js} +1 -1
- package/dist/assets/SessionPanel-CO5qG2uH.js +1 -0
- package/dist/assets/index-BRNSMV9A.css +1 -0
- package/dist/assets/index-dMRHt2Y8.js +9 -0
- package/dist/cli/agent-lookout.js +4 -4
- package/dist/cli/{chunk-JSBQMY3Q.js → chunk-HHZWG5HI.js} +1 -1
- package/dist/cli/{chunk-I6Q35DHC.js → chunk-ME7TBH3B.js} +19 -0
- package/dist/cli/{chunk-LZ62RW3N.js → chunk-WKRBVBCR.js} +1 -1
- package/dist/cli/{mcpServer-BMSF2TXC.js → mcpServer-7VLC6LZN.js} +6 -5
- package/dist/cli/{startCommand-ANC7NVAO.js → startCommand-FV2NXVQR.js} +657 -287
- package/dist/index.html +2 -2
- package/docs/API.md +71 -41
- package/docs/GUIDE.md +100 -64
- package/docs/INSTALL.md +5 -1
- package/package.json +1 -1
- package/dist/assets/SessionPanel-DS29Z04P.js +0 -1
- package/dist/assets/index-BWFkRW4g.js +0 -3
- package/dist/assets/index-B_1Its0R.css +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
sortSessions
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-HHZWG5HI.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-ME7TBH3B.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";
|
|
@@ -2067,6 +2069,13 @@ function send(res, status, body, headers = {}) {
|
|
|
2067
2069
|
function fail(res, status, error, headers = {}) {
|
|
2068
2070
|
send(res, status, { error }, headers);
|
|
2069
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
|
+
}
|
|
2070
2079
|
function notificationsSaid(req) {
|
|
2071
2080
|
const value = req.headers[NOTIFICATIONS_HEADER.toLowerCase()];
|
|
2072
2081
|
if (typeof value !== "string") return null;
|
|
@@ -2105,11 +2114,7 @@ function createApiHandler(options) {
|
|
|
2105
2114
|
}
|
|
2106
2115
|
const action = url === null ? void 0 : actions.get(url.pathname);
|
|
2107
2116
|
if (action) {
|
|
2108
|
-
|
|
2109
|
-
if (res.headersSent) res.end();
|
|
2110
|
-
else fail(res, 500, "The collector ran into an unexpected problem.");
|
|
2111
|
-
}).catch(() => {
|
|
2112
|
-
});
|
|
2117
|
+
sendWhenReady(res, action(req));
|
|
2113
2118
|
return;
|
|
2114
2119
|
}
|
|
2115
2120
|
if (req.method !== "GET") {
|
|
@@ -2131,6 +2136,14 @@ function createApiHandler(options) {
|
|
|
2131
2136
|
send(res, 200, serveSnapshot(poller.getSnapshot()));
|
|
2132
2137
|
return;
|
|
2133
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
|
+
}
|
|
2134
2147
|
case "/api/events": {
|
|
2135
2148
|
const since = numberParam(url.searchParams.get("since"));
|
|
2136
2149
|
if (since === "invalid") {
|
|
@@ -2216,7 +2229,7 @@ function createApiHandler(options) {
|
|
|
2216
2229
|
}
|
|
2217
2230
|
}
|
|
2218
2231
|
}
|
|
2219
|
-
function
|
|
2232
|
+
function answer2(req, res) {
|
|
2220
2233
|
try {
|
|
2221
2234
|
route(req, res);
|
|
2222
2235
|
} catch {
|
|
@@ -2227,12 +2240,12 @@ function createApiHandler(options) {
|
|
|
2227
2240
|
return (req, res) => {
|
|
2228
2241
|
const reading = options.ready?.() ?? null;
|
|
2229
2242
|
if (reading === null) {
|
|
2230
|
-
|
|
2243
|
+
answer2(req, res);
|
|
2231
2244
|
return;
|
|
2232
2245
|
}
|
|
2233
2246
|
void reading.then(
|
|
2234
|
-
() =>
|
|
2235
|
-
() =>
|
|
2247
|
+
() => answer2(req, res),
|
|
2248
|
+
() => answer2(req, res)
|
|
2236
2249
|
);
|
|
2237
2250
|
};
|
|
2238
2251
|
}
|
|
@@ -2451,7 +2464,7 @@ function urlFor(address) {
|
|
|
2451
2464
|
|
|
2452
2465
|
// src/collector/hosts/standalone.ts
|
|
2453
2466
|
import { existsSync } from "node:fs";
|
|
2454
|
-
import
|
|
2467
|
+
import path26 from "node:path";
|
|
2455
2468
|
|
|
2456
2469
|
// src/core/mapping/claudeCodeMapping.ts
|
|
2457
2470
|
var LIVE_STATUS = /* @__PURE__ */ new Map([
|
|
@@ -2838,6 +2851,16 @@ import os2 from "node:os";
|
|
|
2838
2851
|
// src/collector/adapters/claude-code/feed.ts
|
|
2839
2852
|
import { spawn } from "node:child_process";
|
|
2840
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
|
+
|
|
2841
2864
|
// src/collector/processes/pids.ts
|
|
2842
2865
|
function validPid(value) {
|
|
2843
2866
|
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
|
|
@@ -2954,7 +2977,7 @@ var runProgram = (file, args, options) => new Promise((resolve) => {
|
|
|
2954
2977
|
}, options.timeoutMs);
|
|
2955
2978
|
try {
|
|
2956
2979
|
child = spawn(file, [...args], {
|
|
2957
|
-
env: options.env,
|
|
2980
|
+
env: childEnvironment(options.env),
|
|
2958
2981
|
shell: false,
|
|
2959
2982
|
// stdin is the null device, so a program that reads it gets end-of-file.
|
|
2960
2983
|
// stderr is not wanted and goes the same way.
|
|
@@ -3261,6 +3284,12 @@ function createStopTargets() {
|
|
|
3261
3284
|
};
|
|
3262
3285
|
}
|
|
3263
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
|
+
|
|
3264
3293
|
// src/collector/processes/ps.ts
|
|
3265
3294
|
import { execFile } from "node:child_process";
|
|
3266
3295
|
var PS_TIMEOUT_MS = 2e3;
|
|
@@ -3334,8 +3363,8 @@ function parseProcessStarts(stdout) {
|
|
|
3334
3363
|
}
|
|
3335
3364
|
var readProcessStartsWithPs = async (pids) => {
|
|
3336
3365
|
if (pids.length === 0) return /* @__PURE__ */ new Map();
|
|
3337
|
-
const
|
|
3338
|
-
return parseProcessStarts(
|
|
3366
|
+
const answer2 = await runPs(psStartArgs(pids), { utc: true });
|
|
3367
|
+
return parseProcessStarts(answer2.stdout);
|
|
3339
3368
|
};
|
|
3340
3369
|
function createProcessStartCheck(read = readProcessStartsWithPs, now = Date.now, ttlMs = START_CHECK_TTL_MS) {
|
|
3341
3370
|
const remembered = /* @__PURE__ */ new Map();
|
|
@@ -3676,13 +3705,13 @@ function uniqueById(sessions) {
|
|
|
3676
3705
|
}
|
|
3677
3706
|
|
|
3678
3707
|
// src/collector/adapters/claude-code/stopOffers.ts
|
|
3679
|
-
function jobIdOf(entry,
|
|
3680
|
-
const listed =
|
|
3708
|
+
function jobIdOf(entry, answer2) {
|
|
3709
|
+
const listed = answer2.find(
|
|
3681
3710
|
(candidate) => candidate.id !== void 0 && (candidate.sessionId === entry.sessionId || candidate.sessionId === void 0 && candidate.pid === entry.pid)
|
|
3682
3711
|
);
|
|
3683
3712
|
return listed?.id !== void 0 && JOB_ID.test(listed.id) ? listed.id : void 0;
|
|
3684
3713
|
}
|
|
3685
|
-
function targetFor(entry,
|
|
3714
|
+
function targetFor(entry, answer2, options) {
|
|
3686
3715
|
if (entry.sessionId === void 0 || entry.procStart === void 0) return null;
|
|
3687
3716
|
const surface = mapClaudeCodeSurface(entry.entrypoint);
|
|
3688
3717
|
const found = {
|
|
@@ -3695,12 +3724,12 @@ function targetFor(entry, answer, options) {
|
|
|
3695
3724
|
return surface === "terminal" || surface === "vscode" ? { ...found, how: "signal" } : null;
|
|
3696
3725
|
}
|
|
3697
3726
|
if (entry.kind === "bg" && options.background && surface !== "desktop") {
|
|
3698
|
-
const jobId =
|
|
3727
|
+
const jobId = answer2 === null ? void 0 : jobIdOf(entry, answer2);
|
|
3699
3728
|
return jobId === void 0 ? null : { ...found, how: "background", jobId };
|
|
3700
3729
|
}
|
|
3701
3730
|
return null;
|
|
3702
3731
|
}
|
|
3703
|
-
function withStopOffers(sessions, live2,
|
|
3732
|
+
function withStopOffers(sessions, live2, answer2, options) {
|
|
3704
3733
|
const byId = /* @__PURE__ */ new Map();
|
|
3705
3734
|
for (const entry of live2) {
|
|
3706
3735
|
if (entry.sessionId !== void 0) byId.set(`${SOURCE_ID}:${entry.sessionId}`, entry);
|
|
@@ -3711,7 +3740,7 @@ function withStopOffers(sessions, live2, answer, options) {
|
|
|
3711
3740
|
if (session.status === "finished" || session.status === "failed") return session;
|
|
3712
3741
|
const entry = byId.get(session.id);
|
|
3713
3742
|
if (entry === void 0 || entry.pid !== session.pid) return session;
|
|
3714
|
-
const target = targetFor(entry,
|
|
3743
|
+
const target = targetFor(entry, answer2, options);
|
|
3715
3744
|
if (target === null) return session;
|
|
3716
3745
|
targets.set(session.id, target);
|
|
3717
3746
|
return { ...session, stop: { how: target.how } };
|
|
@@ -3719,8 +3748,8 @@ function withStopOffers(sessions, live2, answer, options) {
|
|
|
3719
3748
|
return { sessions: offered, targets };
|
|
3720
3749
|
}
|
|
3721
3750
|
|
|
3722
|
-
// src/collector/adapters/claude-code/transcript/
|
|
3723
|
-
import
|
|
3751
|
+
// src/collector/adapters/claude-code/transcript/lastMessages.ts
|
|
3752
|
+
import path9 from "node:path";
|
|
3724
3753
|
|
|
3725
3754
|
// src/collector/files/readOnlyIo.ts
|
|
3726
3755
|
import { constants as constants4 } from "node:fs";
|
|
@@ -3774,7 +3803,7 @@ function isMissing2(error) {
|
|
|
3774
3803
|
return code === "ENOENT" || code === "ENOTDIR";
|
|
3775
3804
|
}
|
|
3776
3805
|
|
|
3777
|
-
// src/collector/adapters/claude-code/transcript/
|
|
3806
|
+
// src/collector/adapters/claude-code/transcript/transcriptLines.ts
|
|
3778
3807
|
function isRecord3(value) {
|
|
3779
3808
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3780
3809
|
}
|
|
@@ -3800,6 +3829,112 @@ function linesOf(tail, fromStart) {
|
|
|
3800
3829
|
}
|
|
3801
3830
|
return lines;
|
|
3802
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
|
|
3803
3938
|
function isPrompt(line) {
|
|
3804
3939
|
if (line.isMeta === true) return false;
|
|
3805
3940
|
const message = isRecord3(line.message) ? line.message : null;
|
|
@@ -3898,79 +4033,12 @@ function askedInTail(tail, fromStart, cwd) {
|
|
|
3898
4033
|
return waitingText(describeToolUse(pending.name, pending.input, cwd));
|
|
3899
4034
|
}
|
|
3900
4035
|
|
|
3901
|
-
// src/collector/adapters/claude-code/transcript/transcriptFile.ts
|
|
3902
|
-
import path7 from "node:path";
|
|
3903
|
-
var TRANSCRIPT_TAIL_BYTES = 256 * 1024;
|
|
3904
|
-
var SESSION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3905
|
-
function isSessionId(value) {
|
|
3906
|
-
return typeof value === "string" && SESSION_ID.test(value);
|
|
3907
|
-
}
|
|
3908
|
-
function projectFolderName(cwd) {
|
|
3909
|
-
return cwd.replace(/[^A-Za-z0-9]/g, "-");
|
|
3910
|
-
}
|
|
3911
|
-
function fileName(sessionId) {
|
|
3912
|
-
return `${sessionId}.jsonl`;
|
|
3913
|
-
}
|
|
3914
|
-
async function findTranscript(projectsDir, sessionId, cwd, io) {
|
|
3915
|
-
if (!isSessionId(sessionId)) return null;
|
|
3916
|
-
const name = fileName(sessionId);
|
|
3917
|
-
const isFile2 = async (file) => {
|
|
3918
|
-
try {
|
|
3919
|
-
return (await io.lstat(file)).kind === "file";
|
|
3920
|
-
} catch {
|
|
3921
|
-
return false;
|
|
3922
|
-
}
|
|
3923
|
-
};
|
|
3924
|
-
if (cwd !== null && cwd !== "") {
|
|
3925
|
-
const likely = path7.join(projectsDir, projectFolderName(cwd), name);
|
|
3926
|
-
if (await isFile2(likely)) return likely;
|
|
3927
|
-
}
|
|
3928
|
-
let folders;
|
|
3929
|
-
try {
|
|
3930
|
-
folders = await io.readdir(projectsDir);
|
|
3931
|
-
} catch {
|
|
3932
|
-
return null;
|
|
3933
|
-
}
|
|
3934
|
-
const candidates = folders.filter((folder) => folder !== "" && !folder.startsWith(".")).sort().map((folder) => path7.join(projectsDir, folder, name));
|
|
3935
|
-
const found = await Promise.all(candidates.map(isFile2));
|
|
3936
|
-
return candidates.find((_, index) => found[index]) ?? null;
|
|
3937
|
-
}
|
|
3938
|
-
function stampOf(info) {
|
|
3939
|
-
return { size: info.size, mtimeMs: info.mtimeMs, ino: info.ino };
|
|
3940
|
-
}
|
|
3941
|
-
function sameStamp(a, b) {
|
|
3942
|
-
return a.size === b.size && a.mtimeMs === b.mtimeMs && a.ino === b.ino;
|
|
3943
|
-
}
|
|
3944
|
-
async function readTranscriptTail(file, io, known, maxBytes = TRANSCRIPT_TAIL_BYTES) {
|
|
3945
|
-
const open2 = await io.openRegular(file);
|
|
3946
|
-
try {
|
|
3947
|
-
const stamp = stampOf(open2.info);
|
|
3948
|
-
if (known !== null && sameStamp(known, stamp)) return { unchanged: true, stamp };
|
|
3949
|
-
const start = Math.max(0, stamp.size - maxBytes);
|
|
3950
|
-
const bytes3 = await open2.read(start, stamp.size - start);
|
|
3951
|
-
return {
|
|
3952
|
-
unchanged: false,
|
|
3953
|
-
stamp,
|
|
3954
|
-
tail: new TextDecoder("utf-8").decode(bytes3),
|
|
3955
|
-
fromStart: start === 0
|
|
3956
|
-
};
|
|
3957
|
-
} finally {
|
|
3958
|
-
await open2.close();
|
|
3959
|
-
}
|
|
3960
|
-
}
|
|
3961
|
-
|
|
3962
4036
|
// src/collector/adapters/claude-code/transcript/waitingTexts.ts
|
|
3963
4037
|
var WAITING_TEXT_ENV = "AGENT_LOOKOUT_WAITING_TEXT";
|
|
3964
4038
|
function waitingTextOff(env) {
|
|
3965
4039
|
return env[WAITING_TEXT_ENV]?.trim().toLowerCase() === "off";
|
|
3966
4040
|
}
|
|
3967
4041
|
var LOOK_AGAIN_MS = 1e4;
|
|
3968
|
-
var ID_PREFIX = "claude-code:";
|
|
3969
|
-
function sessionIdOf(session) {
|
|
3970
|
-
if (session.source !== "claude-code" || !session.id.startsWith(ID_PREFIX)) return null;
|
|
3971
|
-
const id = session.id.slice(ID_PREFIX.length);
|
|
3972
|
-
return isSessionId(id) ? id : null;
|
|
3973
|
-
}
|
|
3974
4042
|
function createWaitingTextReader(options) {
|
|
3975
4043
|
const io = options.io ?? nodeIo2;
|
|
3976
4044
|
const now = options.now ?? Date.now;
|
|
@@ -4028,6 +4096,160 @@ function createWaitingTextReader(options) {
|
|
|
4028
4096
|
};
|
|
4029
4097
|
}
|
|
4030
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
|
+
|
|
4031
4253
|
// src/collector/adapters/claude-code/index.ts
|
|
4032
4254
|
var CLAUDE_HOME_ENV = "AGENT_LOOKOUT_CLAUDE_HOME";
|
|
4033
4255
|
var CLAUDE_FEED_ENV = "AGENT_LOOKOUT_CLAUDE_FEED";
|
|
@@ -4056,6 +4278,10 @@ var CLAUDE_CODE_CAPABILITIES = {
|
|
|
4056
4278
|
level: "no",
|
|
4057
4279
|
reason: "The file Agent Lookout reads is not rewritten as a session works."
|
|
4058
4280
|
},
|
|
4281
|
+
tokens: {
|
|
4282
|
+
level: "no",
|
|
4283
|
+
reason: "Agent Lookout does not read the token counts in its transcripts yet."
|
|
4284
|
+
},
|
|
4059
4285
|
stop: {
|
|
4060
4286
|
level: "partly",
|
|
4061
4287
|
reason: "In a terminal, in VS Code and for background jobs. Not in the desktop app."
|
|
@@ -4128,6 +4354,8 @@ function createClaudeCodeAdapter(options = {}) {
|
|
|
4128
4354
|
const homeName = tildify(claudeHome, homeDir, platform);
|
|
4129
4355
|
const registryName = tildify(sessionsDir, homeDir, platform);
|
|
4130
4356
|
const waitingTexts = transcriptsOff ? null : createWaitingTextReader({ claudeHome, io: options.transcriptIo, now });
|
|
4357
|
+
const lastMessagesOff = lastMessageOff(env);
|
|
4358
|
+
const lastMessages = transcriptsOff || lastMessagesOff ? null : createLastMessageReader({ claudeHome, io: options.transcriptIo, now });
|
|
4131
4359
|
let lastDue = null;
|
|
4132
4360
|
let lastRead = null;
|
|
4133
4361
|
let feedSoon = false;
|
|
@@ -4149,7 +4377,7 @@ function createClaudeCodeAdapter(options = {}) {
|
|
|
4149
4377
|
{ label: "Command run", value: commandRun },
|
|
4150
4378
|
{
|
|
4151
4379
|
label: "Transcript read",
|
|
4152
|
-
value: transcriptsOff ? "Off" : "last message of a waiting session"
|
|
4380
|
+
value: transcriptsOff ? "Off" : lastMessagesOff ? "last message of a waiting session" : "last message of a waiting session, and of one whose details are open"
|
|
4153
4381
|
}
|
|
4154
4382
|
];
|
|
4155
4383
|
}
|
|
@@ -4245,12 +4473,12 @@ function createClaudeCodeAdapter(options = {}) {
|
|
|
4245
4473
|
}
|
|
4246
4474
|
reading ??= await readRegistryNow(checkedAt);
|
|
4247
4475
|
const read = lastRead;
|
|
4248
|
-
const
|
|
4476
|
+
const answer2 = read?.feed.ok ? read.feed.entries : null;
|
|
4249
4477
|
const problem = problemWith(reading, read);
|
|
4250
4478
|
const { registry, live: live2 } = reading;
|
|
4251
4479
|
const offered = (result) => {
|
|
4252
4480
|
if (!stops) return result;
|
|
4253
|
-
const found = withStopOffers(result.sessions, live2,
|
|
4481
|
+
const found = withStopOffers(result.sessions, live2, answer2, {
|
|
4254
4482
|
sessionsDir,
|
|
4255
4483
|
background: !neverRun
|
|
4256
4484
|
});
|
|
@@ -4259,7 +4487,7 @@ function createClaudeCodeAdapter(options = {}) {
|
|
|
4259
4487
|
};
|
|
4260
4488
|
if (options.panes || options.terminals) {
|
|
4261
4489
|
const pids = new Set(live2.map((entry) => entry.pid));
|
|
4262
|
-
for (const entry of
|
|
4490
|
+
for (const entry of answer2 ?? []) {
|
|
4263
4491
|
if (entry.pid !== void 0 && isAlive(entry.pid)) pids.add(entry.pid);
|
|
4264
4492
|
}
|
|
4265
4493
|
await Promise.all([options.panes?.look([...pids]), options.terminals?.look([...pids])]);
|
|
@@ -4279,16 +4507,16 @@ function createClaudeCodeAdapter(options = {}) {
|
|
|
4279
4507
|
...advice2 !== void 0 && { advice: advice2 },
|
|
4280
4508
|
checkedAt
|
|
4281
4509
|
});
|
|
4282
|
-
if (
|
|
4283
|
-
const listedPids = new Set(
|
|
4284
|
-
const listedSessionIds = new Set(
|
|
4510
|
+
if (answer2 !== null && read !== null && problem === null) {
|
|
4511
|
+
const listedPids = new Set(answer2.map((entry) => entry.pid));
|
|
4512
|
+
const listedSessionIds = new Set(answer2.map((entry) => entry.sessionId));
|
|
4285
4513
|
const tooNewSince = read.askedAt - NEW_SESSION_GRACE_MS;
|
|
4286
4514
|
const fromRegistry = live2.filter(
|
|
4287
4515
|
(entry) => listedPids.has(entry.pid) || listedSessionIds.has(entry.sessionId) || startedAfter(entry, tooNewSince, checkedAt)
|
|
4288
4516
|
);
|
|
4289
4517
|
const livePids = new Set(live2.map((entry) => entry.pid));
|
|
4290
4518
|
const liveSessionIds = new Set(live2.map((entry) => entry.sessionId));
|
|
4291
|
-
const jobs =
|
|
4519
|
+
const jobs = answer2.filter(
|
|
4292
4520
|
(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))
|
|
4293
4521
|
).map((entry) => {
|
|
4294
4522
|
const gone = entry.pid !== void 0 && !isAlive(entry.pid);
|
|
@@ -4309,9 +4537,9 @@ function createClaudeCodeAdapter(options = {}) {
|
|
|
4309
4537
|
basis: "registry+feed"
|
|
4310
4538
|
});
|
|
4311
4539
|
}
|
|
4312
|
-
if (
|
|
4540
|
+
if (answer2 !== null && problem !== null) {
|
|
4313
4541
|
const entries = registry.readable ? registry.entries : void 0;
|
|
4314
|
-
const sessions =
|
|
4542
|
+
const sessions = answer2.map(
|
|
4315
4543
|
(entry) => sessionFromFeed(
|
|
4316
4544
|
entry,
|
|
4317
4545
|
entry.pid !== void 0 ? entries?.get(entry.pid) : void 0,
|
|
@@ -4365,9 +4593,12 @@ function createClaudeCodeAdapter(options = {}) {
|
|
|
4365
4593
|
lookingIn: neverRun ? `Looking for sessions in ${registryName}.` : `Looking for sessions in ${registryName} and with claude ${FEED_ARGS.join(" ")}.`,
|
|
4366
4594
|
async poll() {
|
|
4367
4595
|
try {
|
|
4368
|
-
|
|
4596
|
+
const result = await withWaitingText(await poll());
|
|
4597
|
+
lastMessages?.keep(result.sessions);
|
|
4598
|
+
return result;
|
|
4369
4599
|
} catch {
|
|
4370
4600
|
await waitingTexts?.annotate([]);
|
|
4601
|
+
lastMessages?.forget();
|
|
4371
4602
|
stops?.set(/* @__PURE__ */ new Map());
|
|
4372
4603
|
return {
|
|
4373
4604
|
health: {
|
|
@@ -4385,6 +4616,19 @@ function createClaudeCodeAdapter(options = {}) {
|
|
|
4385
4616
|
sessions: []
|
|
4386
4617
|
};
|
|
4387
4618
|
}
|
|
4619
|
+
},
|
|
4620
|
+
async lastMessage(sessionId) {
|
|
4621
|
+
if (lastMessages === null) {
|
|
4622
|
+
return {
|
|
4623
|
+
message: null,
|
|
4624
|
+
reason: "off",
|
|
4625
|
+
setting: transcriptsOff ? WAITING_TEXT_ENV : LAST_MESSAGE_ENV
|
|
4626
|
+
};
|
|
4627
|
+
}
|
|
4628
|
+
return lastMessages.read(sessionId);
|
|
4629
|
+
},
|
|
4630
|
+
get lastMessagesKept() {
|
|
4631
|
+
return lastMessages?.size ?? 0;
|
|
4388
4632
|
}
|
|
4389
4633
|
};
|
|
4390
4634
|
}
|
|
@@ -4400,8 +4644,8 @@ function parseProcessParents(stdout) {
|
|
|
4400
4644
|
return parents;
|
|
4401
4645
|
}
|
|
4402
4646
|
var readProcessParentsWithPs = async () => {
|
|
4403
|
-
const
|
|
4404
|
-
return
|
|
4647
|
+
const answer2 = await runPs(PS_PARENTS_ARGS);
|
|
4648
|
+
return answer2.ok ? parseProcessParents(answer2.stdout) : /* @__PURE__ */ new Map();
|
|
4405
4649
|
};
|
|
4406
4650
|
|
|
4407
4651
|
// src/collector/actions/stopSession.ts
|
|
@@ -4869,7 +5113,7 @@ function createAnswerer(options) {
|
|
|
4869
5113
|
};
|
|
4870
5114
|
}
|
|
4871
5115
|
function createAnswerRoute(options) {
|
|
4872
|
-
const
|
|
5116
|
+
const answer2 = createAnswerer(options);
|
|
4873
5117
|
return async function answerPermission(req) {
|
|
4874
5118
|
const refused = answerRefusalFor(req);
|
|
4875
5119
|
if (refused) {
|
|
@@ -4888,7 +5132,7 @@ function createAnswerRoute(options) {
|
|
|
4888
5132
|
'The body must be {"sessionId": "...", "requestId": "...", "decision": "allow" or "deny"} and nothing else.'
|
|
4889
5133
|
);
|
|
4890
5134
|
}
|
|
4891
|
-
const outcome = await
|
|
5135
|
+
const outcome = await answer2(asked.sessionId, asked.requestId, asked.decision);
|
|
4892
5136
|
if (outcome !== "answered") return answerFailed(outcome);
|
|
4893
5137
|
return {
|
|
4894
5138
|
status: 200,
|
|
@@ -5187,7 +5431,7 @@ function createHeldAsks(options) {
|
|
|
5187
5431
|
if (inFlight === run) inFlight = null;
|
|
5188
5432
|
}
|
|
5189
5433
|
}
|
|
5190
|
-
async function
|
|
5434
|
+
async function answer2(sessionId, requestId, decision) {
|
|
5191
5435
|
const entry = held.get(sessionId);
|
|
5192
5436
|
if (!entry || entry.requestId !== requestId || !entry.confirmed) return "no-ask";
|
|
5193
5437
|
if (decision === "allow" && !entry.shown.allow) return "not-allowable";
|
|
@@ -5219,7 +5463,7 @@ function createHeldAsks(options) {
|
|
|
5219
5463
|
const verdict = verdictOf(entry);
|
|
5220
5464
|
if (verdict === null) return;
|
|
5221
5465
|
entry.byRule = true;
|
|
5222
|
-
void
|
|
5466
|
+
void answer2(entry.sessionId, entry.requestId, verdict.decision).then((outcome) => {
|
|
5223
5467
|
if (outcome === "answered") rules.answered(request2, verdict);
|
|
5224
5468
|
});
|
|
5225
5469
|
}
|
|
@@ -5276,7 +5520,7 @@ function createHeldAsks(options) {
|
|
|
5276
5520
|
void check();
|
|
5277
5521
|
},
|
|
5278
5522
|
check,
|
|
5279
|
-
answer,
|
|
5523
|
+
answer: answer2,
|
|
5280
5524
|
withAsks(snapshot) {
|
|
5281
5525
|
const answering = status();
|
|
5282
5526
|
if (held.size === 0) return { ...snapshot, answering };
|
|
@@ -5339,7 +5583,7 @@ function createHeldAsks(options) {
|
|
|
5339
5583
|
import { chmod, lstat as lstat3, mkdir, stat as stat4, unlink } from "node:fs/promises";
|
|
5340
5584
|
import http from "node:http";
|
|
5341
5585
|
import net from "node:net";
|
|
5342
|
-
import
|
|
5586
|
+
import path10 from "node:path";
|
|
5343
5587
|
var HOOK_PATH = "/hooks/permission-request";
|
|
5344
5588
|
var HOOK_HEADER = "x-agent-lookout-hook";
|
|
5345
5589
|
var HOOK_HEADER_VALUE = "permission-request";
|
|
@@ -5412,7 +5656,7 @@ function createHookSocket(options) {
|
|
|
5412
5656
|
const maxBody = options.maxBodyBytes ?? MAX_HOOK_BODY_BYTES;
|
|
5413
5657
|
const bodyTimeoutMs = options.bodyTimeoutMs ?? HOOK_BODY_TIMEOUT_MS;
|
|
5414
5658
|
const uid = options.uid ?? process.getuid?.() ?? -1;
|
|
5415
|
-
const folder =
|
|
5659
|
+
const folder = path10.dirname(socketPath);
|
|
5416
5660
|
let server = null;
|
|
5417
5661
|
let made = null;
|
|
5418
5662
|
async function handle(req, res) {
|
|
@@ -5535,12 +5779,12 @@ function createHookSocket(options) {
|
|
|
5535
5779
|
|
|
5536
5780
|
// src/collector/answers/registryStatus.ts
|
|
5537
5781
|
import os4 from "node:os";
|
|
5538
|
-
import
|
|
5782
|
+
import path11 from "node:path";
|
|
5539
5783
|
function createRegistryStatus(options) {
|
|
5540
5784
|
const homeDir = options.homeDir ?? os4.homedir();
|
|
5541
5785
|
const readFile2 = options.readFile ?? readRegularFile;
|
|
5542
|
-
const home = options.env[CLAUDE_HOME_ENV]?.trim() ||
|
|
5543
|
-
const sessionsDir =
|
|
5786
|
+
const home = options.env[CLAUDE_HOME_ENV]?.trim() || path11.join(homeDir, ".claude");
|
|
5787
|
+
const sessionsDir = path11.join(path11.resolve(home), "sessions");
|
|
5544
5788
|
return {
|
|
5545
5789
|
async statusOf(sessionId) {
|
|
5546
5790
|
const unknown = { status: "unknown" };
|
|
@@ -5551,7 +5795,7 @@ function createRegistryStatus(options) {
|
|
|
5551
5795
|
const ownId = sessionId.slice("claude-code:".length);
|
|
5552
5796
|
let content;
|
|
5553
5797
|
try {
|
|
5554
|
-
content = await readFile2(
|
|
5798
|
+
content = await readFile2(path11.join(sessionsDir, `${pid}.json`));
|
|
5555
5799
|
} catch {
|
|
5556
5800
|
return unknown;
|
|
5557
5801
|
}
|
|
@@ -5825,18 +6069,18 @@ function namesOwnFiles(words, ownPaths = [], folder) {
|
|
|
5825
6069
|
(part) => from !== void 0 && !part.startsWith("/") && !part.startsWith("-") ? [part, `${from}/${part}`] : [part]
|
|
5826
6070
|
);
|
|
5827
6071
|
return read.some((part) => {
|
|
5828
|
-
const
|
|
5829
|
-
if (
|
|
5830
|
-
if (own.some((name) =>
|
|
5831
|
-
if (above.has(
|
|
5832
|
-
return ownNames.some((name) =>
|
|
6072
|
+
const path28 = plainPath(part);
|
|
6073
|
+
if (path28 === BY_NUMBER || path28.startsWith(`${BY_NUMBER}/`)) return true;
|
|
6074
|
+
if (own.some((name) => path28 === name || path28.startsWith(`${name}/`))) return true;
|
|
6075
|
+
if (above.has(path28)) return true;
|
|
6076
|
+
return ownNames.some((name) => path28 === name || path28.endsWith(`/${name}`));
|
|
5833
6077
|
});
|
|
5834
6078
|
});
|
|
5835
6079
|
}
|
|
5836
6080
|
function folderReachesOwnFiles(folder, ownPaths = []) {
|
|
5837
6081
|
const own = ownPaths.filter((name) => name.startsWith("/")).map(plainPath);
|
|
5838
|
-
const
|
|
5839
|
-
return own.some((name) => name ===
|
|
6082
|
+
const path28 = plainPath(folder);
|
|
6083
|
+
return own.some((name) => name === path28 || name.startsWith(`${path28 === "/" ? "" : path28}/`));
|
|
5840
6084
|
}
|
|
5841
6085
|
var PLAIN_WORD = /^[A-Za-z0-9_\-.,/:=+@%]+$/;
|
|
5842
6086
|
var ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
@@ -6218,7 +6462,7 @@ function createAnswering(options) {
|
|
|
6218
6462
|
let problem = null;
|
|
6219
6463
|
let listening = false;
|
|
6220
6464
|
const answerRoute = createAnswerRoute({ poller, events, asks, now: options.now });
|
|
6221
|
-
const
|
|
6465
|
+
const answer2 = createAnswerer({ poller, events, asks, now: options.now });
|
|
6222
6466
|
return {
|
|
6223
6467
|
async start() {
|
|
6224
6468
|
const started = await socket.start();
|
|
@@ -6252,7 +6496,7 @@ function createAnswering(options) {
|
|
|
6252
6496
|
return answerRefusalFor(req) ?? refusal(405, "This address only answers GET requests.", { Allow: "GET" });
|
|
6253
6497
|
},
|
|
6254
6498
|
// The same answer, from the host's own process. With no socket nothing is held.
|
|
6255
|
-
answer: async (sessionId, requestId, decision) => problem === null ?
|
|
6499
|
+
answer: async (sessionId, requestId, decision) => problem === null ? answer2(sessionId, requestId, decision) : "unavailable",
|
|
6256
6500
|
check: () => asks.check(),
|
|
6257
6501
|
ruleAnswers: () => ruleAnswers.recent()
|
|
6258
6502
|
};
|
|
@@ -6483,7 +6727,7 @@ function waitTotals(input) {
|
|
|
6483
6727
|
|
|
6484
6728
|
// src/collector/adapters/antigravity/index.ts
|
|
6485
6729
|
import os5 from "node:os";
|
|
6486
|
-
import
|
|
6730
|
+
import path15 from "node:path";
|
|
6487
6731
|
|
|
6488
6732
|
// src/core/mapping/antigravityLiveness.ts
|
|
6489
6733
|
function holdConversations(processes, conversations, listedAt = null) {
|
|
@@ -6522,14 +6766,15 @@ var SOURCE2 = {
|
|
|
6522
6766
|
backend: "server.go",
|
|
6523
6767
|
created: "server.go",
|
|
6524
6768
|
streaming: "conversation_manager.go",
|
|
6525
|
-
|
|
6769
|
+
asking: "tool_confirmation_manager.go",
|
|
6770
|
+
answered: "input_loop.go"
|
|
6526
6771
|
};
|
|
6527
6772
|
var UUID = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}";
|
|
6528
6773
|
var OPENED = new RegExp(`^(Created|Streaming) conversation (${UUID})$`);
|
|
6529
6774
|
var BACKEND = /^Creating CLI server backend: (?:.* )?workspaceDirs=\[([^\]\s]+)\](?: |$)/;
|
|
6530
6775
|
var ASKING = /^Surfacing tool confirmation: "([A-Za-z]{1,64})" at step (\d{1,9})$/;
|
|
6531
6776
|
var ANSWERED = new RegExp(
|
|
6532
|
-
`^Responding to tool confirmation: convID=(${UUID}), stepIdx=(\\d{1,9}), approved=(?:true|false)
|
|
6777
|
+
`^Responding to tool confirmation: convID=(${UUID}), stepIdx=(\\d{1,9}), approved=(?:true|false)(?:, .*)?$`
|
|
6533
6778
|
);
|
|
6534
6779
|
var ABSOLUTE = /^(?:\/|[A-Za-z]:[\\/])/;
|
|
6535
6780
|
var UNPRINTABLE = /[\p{Cc}\p{Cf}]/u;
|
|
@@ -6547,7 +6792,11 @@ function readAgyLogLine(line) {
|
|
|
6547
6792
|
const backend = BACKEND.exec(message);
|
|
6548
6793
|
return backend !== null && ABSOLUTE.test(backend[1]) && !UNPRINTABLE.test(backend[1]) ? { kind: "folder", folder: backend[1] } : null;
|
|
6549
6794
|
}
|
|
6550
|
-
if (source
|
|
6795
|
+
if (source === SOURCE2.answered) {
|
|
6796
|
+
const answered = ANSWERED.exec(message);
|
|
6797
|
+
return answered === null ? null : { kind: "answered", conversation: answered[1].toLowerCase(), step: Number(answered[2]) };
|
|
6798
|
+
}
|
|
6799
|
+
if (source !== SOURCE2.asking) return null;
|
|
6551
6800
|
const asking = ASKING.exec(message);
|
|
6552
6801
|
if (asking !== null) {
|
|
6553
6802
|
const fraction = (glog[6] ?? "0").padEnd(3, "0").slice(0, 3);
|
|
@@ -6565,10 +6814,6 @@ function readAgyLogLine(line) {
|
|
|
6565
6814
|
}
|
|
6566
6815
|
};
|
|
6567
6816
|
}
|
|
6568
|
-
const answered = ANSWERED.exec(message);
|
|
6569
|
-
if (answered !== null) {
|
|
6570
|
-
return { kind: "answered", conversation: answered[1].toLowerCase(), step: Number(answered[2]) };
|
|
6571
|
-
}
|
|
6572
6817
|
return null;
|
|
6573
6818
|
}
|
|
6574
6819
|
function emptyAgyLogState() {
|
|
@@ -6621,7 +6866,7 @@ function agyLogTimeAt(time, near) {
|
|
|
6621
6866
|
}
|
|
6622
6867
|
|
|
6623
6868
|
// src/collector/adapters/antigravity/agyLogs.ts
|
|
6624
|
-
import
|
|
6869
|
+
import path12 from "node:path";
|
|
6625
6870
|
var LOG_DIR = "log";
|
|
6626
6871
|
var LOG_LIMIT_BYTES = 4 * 1024 * 1024;
|
|
6627
6872
|
var LINE_LIMIT_BYTES = 64 * 1024;
|
|
@@ -6656,7 +6901,7 @@ function matchLogs(processes, logs) {
|
|
|
6656
6901
|
function createAgyLogReader(options) {
|
|
6657
6902
|
const { io } = options;
|
|
6658
6903
|
const now = options.now ?? Date.now;
|
|
6659
|
-
const logDir =
|
|
6904
|
+
const logDir = path12.join(options.home, LOG_DIR);
|
|
6660
6905
|
const cursors = /* @__PURE__ */ new Map();
|
|
6661
6906
|
let listed = /* @__PURE__ */ new Map();
|
|
6662
6907
|
let listedFor = "";
|
|
@@ -6753,7 +6998,7 @@ function createAgyLogReader(options) {
|
|
|
6753
6998
|
states.push(null);
|
|
6754
6999
|
continue;
|
|
6755
7000
|
}
|
|
6756
|
-
const file =
|
|
7001
|
+
const file = path12.join(logDir, name);
|
|
6757
7002
|
kept.add(file);
|
|
6758
7003
|
try {
|
|
6759
7004
|
const info = await io.lstat(file);
|
|
@@ -6871,12 +7116,12 @@ function createAgyProcessReader(options = {}) {
|
|
|
6871
7116
|
const unseen = rows.filter((row) => !known.has(keyOf(row)));
|
|
6872
7117
|
const ended = /* @__PURE__ */ new Set();
|
|
6873
7118
|
if (unseen.length > 0) {
|
|
6874
|
-
const
|
|
6875
|
-
const lines = parseCommandLines(
|
|
7119
|
+
const answer2 = await run(agyCommandLineArgs(unseen.map((row) => row.pid)));
|
|
7120
|
+
const lines = parseCommandLines(answer2.stdout);
|
|
6876
7121
|
for (const row of unseen) {
|
|
6877
7122
|
const line = lines.get(row.pid);
|
|
6878
7123
|
if (line !== void 0) known.set(keyOf(row), readAgyCommandLine(line));
|
|
6879
|
-
else if (
|
|
7124
|
+
else if (answer2.ok || lines.size > 0) ended.add(row.pid);
|
|
6880
7125
|
}
|
|
6881
7126
|
}
|
|
6882
7127
|
const running = /* @__PURE__ */ new Map();
|
|
@@ -6916,7 +7161,7 @@ function createAgyProcessReader(options = {}) {
|
|
|
6916
7161
|
}
|
|
6917
7162
|
|
|
6918
7163
|
// src/collector/adapters/antigravity/conversations.ts
|
|
6919
|
-
import
|
|
7164
|
+
import path13 from "node:path";
|
|
6920
7165
|
var BRAIN_DIR = "brain";
|
|
6921
7166
|
var CONVERSATIONS_DIR = "conversations";
|
|
6922
7167
|
var TRANSCRIPT_IN_FOLDER = [".system_generated", "logs", "transcript.jsonl"];
|
|
@@ -6932,15 +7177,15 @@ function conversationIdOfDatabase(name) {
|
|
|
6932
7177
|
}
|
|
6933
7178
|
function createConversationFinder(options) {
|
|
6934
7179
|
const { io } = options;
|
|
6935
|
-
const brainDir =
|
|
6936
|
-
const databaseDir =
|
|
7180
|
+
const brainDir = path13.join(options.home, BRAIN_DIR);
|
|
7181
|
+
const databaseDir = path13.join(options.home, CONVERSATIONS_DIR);
|
|
6937
7182
|
const oldRefreshMs = options.oldRefreshMs ?? OLD_REFRESH_MS;
|
|
6938
7183
|
const known = /* @__PURE__ */ new Map();
|
|
6939
7184
|
let oldCheckedAt = null;
|
|
6940
7185
|
async function lookAt(id) {
|
|
6941
|
-
const transcript =
|
|
7186
|
+
const transcript = path13.join(brainDir, id, ...TRANSCRIPT_IN_FOLDER);
|
|
6942
7187
|
const infos = await Promise.all(
|
|
6943
|
-
[transcript,
|
|
7188
|
+
[transcript, path13.join(databaseDir, `${id}.db`), path13.join(databaseDir, `${id}.db-wal`)].map(
|
|
6944
7189
|
(file) => io.lstat(file).catch(() => null)
|
|
6945
7190
|
)
|
|
6946
7191
|
);
|
|
@@ -6996,7 +7241,7 @@ function writtenRecently(files, now) {
|
|
|
6996
7241
|
}
|
|
6997
7242
|
|
|
6998
7243
|
// src/collector/adapters/antigravity/annotations.ts
|
|
6999
|
-
import
|
|
7244
|
+
import path14 from "node:path";
|
|
7000
7245
|
var ANNOTATIONS_DIR = "annotations";
|
|
7001
7246
|
var ANNOTATION_LIMIT_BYTES = 4 * 1024;
|
|
7002
7247
|
var TITLE_FIELD = /^title[ \t]*:[ \t]*"((?:[^"\\\n]|\\.)*)"[ \t]*$/m;
|
|
@@ -7068,10 +7313,10 @@ function readTitle(content) {
|
|
|
7068
7313
|
}
|
|
7069
7314
|
function createTitleReader(options) {
|
|
7070
7315
|
const { io } = options;
|
|
7071
|
-
const dir =
|
|
7316
|
+
const dir = path14.join(options.home, ANNOTATIONS_DIR);
|
|
7072
7317
|
const cache = /* @__PURE__ */ new Map();
|
|
7073
7318
|
async function read(id) {
|
|
7074
|
-
const file =
|
|
7319
|
+
const file = path14.join(dir, `${id}.pbtxt`);
|
|
7075
7320
|
const info = await io.lstat(file);
|
|
7076
7321
|
const known = cache.get(id);
|
|
7077
7322
|
if (known && known.ino === info.ino && known.size === info.size && known.mtimeMs === info.mtimeMs) {
|
|
@@ -7622,6 +7867,10 @@ var ANTIGRAVITY_CAPABILITIES = {
|
|
|
7622
7867
|
reason: "The transcripts name no terminal, and agy documents no link that opens a conversation."
|
|
7623
7868
|
},
|
|
7624
7869
|
"quiet-for": { level: "yes" },
|
|
7870
|
+
tokens: {
|
|
7871
|
+
level: "no",
|
|
7872
|
+
reason: "Agent Lookout does not read token counts from agy's transcripts yet."
|
|
7873
|
+
},
|
|
7625
7874
|
stop: {
|
|
7626
7875
|
level: "no",
|
|
7627
7876
|
reason: "No agy program is tied to one conversation surely enough for Agent Lookout to confirm it and stop it."
|
|
@@ -7654,8 +7903,8 @@ function createAntigravityAdapter(options = {}) {
|
|
|
7654
7903
|
const processes = options.processes ?? createAgyProcessReader({ platform });
|
|
7655
7904
|
const override = env[ANTIGRAVITY_HOME_ENV]?.trim() || void 0;
|
|
7656
7905
|
const homeFrom = override ? "agent-lookout" : "default";
|
|
7657
|
-
const home =
|
|
7658
|
-
const brainDir =
|
|
7906
|
+
const home = path15.resolve(override ?? path15.join(homeDir, ...DEFAULT_HOME));
|
|
7907
|
+
const brainDir = path15.join(home, BRAIN_DIR);
|
|
7659
7908
|
const homeName = tildify(home, homeDir);
|
|
7660
7909
|
const brainName = tildify(brainDir, homeDir);
|
|
7661
7910
|
const finder = createConversationFinder({ home, io, oldRefreshMs: options.oldRefreshMs });
|
|
@@ -7820,8 +8069,8 @@ function createAntigravityAdapter(options = {}) {
|
|
|
7820
8069
|
basis = BASIS_UNMATCHED;
|
|
7821
8070
|
}
|
|
7822
8071
|
if (unknownStep) detail += ` ${UNKNOWN_STEP_NOTE}`;
|
|
7823
|
-
const
|
|
7824
|
-
return { ...
|
|
8072
|
+
const answer2 = result("ok", detail, every(pollIntervalMs), checkedAt, basis);
|
|
8073
|
+
return { ...answer2, sessions };
|
|
7825
8074
|
}
|
|
7826
8075
|
return {
|
|
7827
8076
|
id: SOURCE_ID2,
|
|
@@ -7845,7 +8094,7 @@ function createAntigravityAdapter(options = {}) {
|
|
|
7845
8094
|
|
|
7846
8095
|
// src/collector/adapters/codex/index.ts
|
|
7847
8096
|
import os6 from "node:os";
|
|
7848
|
-
import
|
|
8097
|
+
import path17 from "node:path";
|
|
7849
8098
|
|
|
7850
8099
|
// src/core/mapping/codexMapping.ts
|
|
7851
8100
|
var TURN_UNDER_WAY = /* @__PURE__ */ new Set(["task_started", "turn_started"]);
|
|
@@ -7911,6 +8160,22 @@ function isCodexSessionSource(source, meta = {}) {
|
|
|
7911
8160
|
return true;
|
|
7912
8161
|
}
|
|
7913
8162
|
|
|
8163
|
+
// src/core/tokens/tokenCounts.ts
|
|
8164
|
+
function isRecord10(value) {
|
|
8165
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8166
|
+
}
|
|
8167
|
+
function isCount(value) {
|
|
8168
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
8169
|
+
}
|
|
8170
|
+
function tokenCountsOf(value) {
|
|
8171
|
+
if (!isRecord10(value)) return null;
|
|
8172
|
+
const { input, cached, output } = value;
|
|
8173
|
+
if (!isCount(input) || input === 0 || !isCount(output)) return null;
|
|
8174
|
+
if (cached === void 0) return { input, output };
|
|
8175
|
+
if (!isCount(cached) || cached > input) return null;
|
|
8176
|
+
return { input, cached, output };
|
|
8177
|
+
}
|
|
8178
|
+
|
|
7914
8179
|
// src/collector/adapters/codex/rolloutFile.ts
|
|
7915
8180
|
var HEAD_LIMIT_BYTES2 = 2 * 1024 * 1024;
|
|
7916
8181
|
var TAIL_LIMIT_BYTES2 = 8 * 1024 * 1024;
|
|
@@ -7926,7 +8191,8 @@ function parseCodexTime(value) {
|
|
|
7926
8191
|
var LINE_TIME = /^\{"timestamp":"([^"]{1,64})"/;
|
|
7927
8192
|
var TURN_HINT = /"type":"(?:task|turn)_/;
|
|
7928
8193
|
var TURN_TYPE = /^(?:task|turn)_/;
|
|
7929
|
-
|
|
8194
|
+
var TOKEN_HINT = '"type":"token_count"';
|
|
8195
|
+
function isRecord11(value) {
|
|
7930
8196
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7931
8197
|
}
|
|
7932
8198
|
function text3(value) {
|
|
@@ -7937,7 +8203,7 @@ function label(value) {
|
|
|
7937
8203
|
}
|
|
7938
8204
|
function compactSource(value) {
|
|
7939
8205
|
if (typeof value === "string") return text3(value);
|
|
7940
|
-
if (!
|
|
8206
|
+
if (!isRecord11(value)) return void 0;
|
|
7941
8207
|
return Object.fromEntries(
|
|
7942
8208
|
Object.entries(value).map(([kind, detail]) => [
|
|
7943
8209
|
kind,
|
|
@@ -7948,7 +8214,7 @@ function compactSource(value) {
|
|
|
7948
8214
|
function parse(line) {
|
|
7949
8215
|
try {
|
|
7950
8216
|
const value = JSON.parse(line);
|
|
7951
|
-
return
|
|
8217
|
+
return isRecord11(value) ? value : null;
|
|
7952
8218
|
} catch {
|
|
7953
8219
|
return null;
|
|
7954
8220
|
}
|
|
@@ -7956,7 +8222,7 @@ function parse(line) {
|
|
|
7956
8222
|
function readMetaLine(line) {
|
|
7957
8223
|
if (!line.includes('"session_meta"')) return null;
|
|
7958
8224
|
const value = parse(line);
|
|
7959
|
-
if (!value || value.type !== "session_meta" || !
|
|
8225
|
+
if (!value || value.type !== "session_meta" || !isRecord11(value.payload)) return null;
|
|
7960
8226
|
const payload = value.payload;
|
|
7961
8227
|
const meta = {
|
|
7962
8228
|
id: text3(payload.id),
|
|
@@ -7976,7 +8242,7 @@ function readMetaLine(line) {
|
|
|
7976
8242
|
function readTurnLine(line) {
|
|
7977
8243
|
if (!line.includes('"event_msg"') || !TURN_HINT.test(line)) return null;
|
|
7978
8244
|
const value = parse(line);
|
|
7979
|
-
if (!value || value.type !== "event_msg" || !
|
|
8245
|
+
if (!value || value.type !== "event_msg" || !isRecord11(value.payload)) return null;
|
|
7980
8246
|
const turn = value.payload.type;
|
|
7981
8247
|
if (typeof turn !== "string" || !TURN_TYPE.test(turn)) return null;
|
|
7982
8248
|
return {
|
|
@@ -7985,6 +8251,21 @@ function readTurnLine(line) {
|
|
|
7985
8251
|
imported: isImportedCodexTurn(value.payload.turn_id)
|
|
7986
8252
|
};
|
|
7987
8253
|
}
|
|
8254
|
+
function readTokenLine(line) {
|
|
8255
|
+
if (!line.includes('"event_msg"') || !line.includes(TOKEN_HINT)) return null;
|
|
8256
|
+
const value = parse(line);
|
|
8257
|
+
if (!value || value.type !== "event_msg" || !isRecord11(value.payload)) return null;
|
|
8258
|
+
if (value.payload.type !== "token_count" || value.payload.info === null) return null;
|
|
8259
|
+
const last = isRecord11(value.payload.info) ? value.payload.info.last_token_usage : void 0;
|
|
8260
|
+
if (!isRecord11(last)) return { tokens: null };
|
|
8261
|
+
return {
|
|
8262
|
+
tokens: tokenCountsOf({
|
|
8263
|
+
input: last.input_tokens,
|
|
8264
|
+
cached: last.cached_input_tokens,
|
|
8265
|
+
output: last.output_tokens
|
|
8266
|
+
})
|
|
8267
|
+
};
|
|
8268
|
+
}
|
|
7988
8269
|
function lineTime(line) {
|
|
7989
8270
|
const match = LINE_TIME.exec(line);
|
|
7990
8271
|
return match ? parseCodexTime(match[1]) : null;
|
|
@@ -8020,20 +8301,31 @@ async function scanTail2(file, size) {
|
|
|
8020
8301
|
let fragment = Buffer.alloc(0);
|
|
8021
8302
|
let end = null;
|
|
8022
8303
|
let lastLineAt = null;
|
|
8304
|
+
let turn = null;
|
|
8305
|
+
let counted2 = null;
|
|
8306
|
+
let lookedBack = false;
|
|
8023
8307
|
const take = (line) => {
|
|
8024
|
-
if (line.trim() === "") return
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8029
|
-
|
|
8030
|
-
|
|
8031
|
-
|
|
8032
|
-
|
|
8308
|
+
if (line.trim() === "") return false;
|
|
8309
|
+
if (turn === null) {
|
|
8310
|
+
turn = readTurnLine(line);
|
|
8311
|
+
lastLineAt ??= lineTime(line) ?? turn?.at ?? null;
|
|
8312
|
+
}
|
|
8313
|
+
counted2 ??= readTokenLine(line);
|
|
8314
|
+
return turn !== null && counted2 !== null;
|
|
8315
|
+
};
|
|
8316
|
+
const found = (whole) => ({
|
|
8317
|
+
lastTurn: turn !== null ? turn.turn : whole ? null : void 0,
|
|
8318
|
+
lastTurnAt: turn?.at ?? null,
|
|
8319
|
+
lastTurnImported: turn?.imported ?? false,
|
|
8033
8320
|
lastLineAt,
|
|
8321
|
+
tokens: counted2?.tokens ?? null,
|
|
8034
8322
|
end
|
|
8035
8323
|
});
|
|
8036
8324
|
while (position > 0 && budget2 > 0) {
|
|
8325
|
+
if (turn !== null) {
|
|
8326
|
+
if (lookedBack) break;
|
|
8327
|
+
lookedBack = true;
|
|
8328
|
+
}
|
|
8037
8329
|
const length = Math.min(TAIL_CHUNK_BYTES2, position, budget2);
|
|
8038
8330
|
const start = position - length;
|
|
8039
8331
|
const chunk = await readExactly2(file, start, length);
|
|
@@ -8052,22 +8344,18 @@ async function scanTail2(file, size) {
|
|
|
8052
8344
|
}
|
|
8053
8345
|
let newline = cut2 > 0 ? buffer.lastIndexOf(NEWLINE3, cut2 - 1) : -1;
|
|
8054
8346
|
while (newline !== -1) {
|
|
8055
|
-
|
|
8056
|
-
if (turn) return found(turn);
|
|
8347
|
+
if (take(buffer.toString("utf8", newline + 1, cut2))) return found(false);
|
|
8057
8348
|
cut2 = newline;
|
|
8058
8349
|
newline = cut2 > 0 ? buffer.lastIndexOf(NEWLINE3, cut2 - 1) : -1;
|
|
8059
8350
|
}
|
|
8060
8351
|
if (start === 0) {
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
return { lastTurn: null, lastTurnAt: null, lastTurnImported: false, lastLineAt, end };
|
|
8352
|
+
take(buffer.toString("utf8", 0, cut2));
|
|
8353
|
+
return found(true);
|
|
8064
8354
|
}
|
|
8065
8355
|
fragment = buffer.subarray(0, cut2);
|
|
8066
8356
|
}
|
|
8067
|
-
if (position === 0) {
|
|
8068
|
-
|
|
8069
|
-
}
|
|
8070
|
-
return { lastTurn: void 0, lastTurnAt: null, lastTurnImported: false, lastLineAt, end };
|
|
8357
|
+
if (position === 0) return { ...found(true), end: end ?? 0 };
|
|
8358
|
+
return found(false);
|
|
8071
8359
|
}
|
|
8072
8360
|
function stateOf2(cached) {
|
|
8073
8361
|
return {
|
|
@@ -8075,7 +8363,8 @@ function stateOf2(cached) {
|
|
|
8075
8363
|
lastTurn: cached.lastTurn,
|
|
8076
8364
|
lastTurnAt: cached.lastTurnAt,
|
|
8077
8365
|
lastTurnImported: cached.lastTurnImported,
|
|
8078
|
-
lastLineAt: cached.lastLineAt
|
|
8366
|
+
lastLineAt: cached.lastLineAt,
|
|
8367
|
+
tokens: cached.tokens
|
|
8079
8368
|
};
|
|
8080
8369
|
}
|
|
8081
8370
|
function createRolloutReader(io) {
|
|
@@ -8092,6 +8381,7 @@ function createRolloutReader(io) {
|
|
|
8092
8381
|
lastTurnAt: tail.lastTurnAt,
|
|
8093
8382
|
lastTurnImported: tail.lastTurnImported,
|
|
8094
8383
|
lastLineAt: tail.lastLineAt,
|
|
8384
|
+
tokens: tail.tokens,
|
|
8095
8385
|
ino: info.ino,
|
|
8096
8386
|
size: info.size,
|
|
8097
8387
|
mtimeMs: info.mtimeMs,
|
|
@@ -8133,6 +8423,8 @@ function createRolloutReader(io) {
|
|
|
8133
8423
|
next.lastTurnAt = turn.at;
|
|
8134
8424
|
next.lastTurnImported = turn.imported;
|
|
8135
8425
|
}
|
|
8426
|
+
const counted2 = turn ? null : readTokenLine(line);
|
|
8427
|
+
if (counted2) next.tokens = counted2.tokens;
|
|
8136
8428
|
}
|
|
8137
8429
|
start = newline + 1;
|
|
8138
8430
|
newline = added.indexOf(NEWLINE3, start);
|
|
@@ -8162,7 +8454,7 @@ function createRolloutReader(io) {
|
|
|
8162
8454
|
}
|
|
8163
8455
|
|
|
8164
8456
|
// src/collector/adapters/codex/rollouts.ts
|
|
8165
|
-
import
|
|
8457
|
+
import path16 from "node:path";
|
|
8166
8458
|
var UUID4 = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
|
|
8167
8459
|
var ROLLOUT_FILE_NAME = new RegExp(
|
|
8168
8460
|
`^rollout-\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-(${UUID4})(?:_(${UUID4}))?\\.jsonl$`,
|
|
@@ -8180,7 +8472,7 @@ function twoDigits(value) {
|
|
|
8180
8472
|
function recentDayFolders(sessionsDir, now) {
|
|
8181
8473
|
const folders = [now, now - 24 * 60 * 60 * 1e3].map((at) => {
|
|
8182
8474
|
const date = new Date(at);
|
|
8183
|
-
return
|
|
8475
|
+
return path16.join(
|
|
8184
8476
|
sessionsDir,
|
|
8185
8477
|
String(date.getFullYear()),
|
|
8186
8478
|
twoDigits(date.getMonth() + 1),
|
|
@@ -8209,12 +8501,12 @@ function createRolloutFinder(options) {
|
|
|
8209
8501
|
const refs = [];
|
|
8210
8502
|
for (const name of (await names(dir)).sort()) {
|
|
8211
8503
|
const threadId = threadIdOfRollout(name);
|
|
8212
|
-
if (threadId !== null) refs.push({ path:
|
|
8504
|
+
if (threadId !== null) refs.push({ path: path16.join(dir, name), threadId });
|
|
8213
8505
|
}
|
|
8214
8506
|
return refs;
|
|
8215
8507
|
}
|
|
8216
8508
|
async function subfolders(dir, pattern) {
|
|
8217
|
-
return (await names(dir)).filter((name) => pattern.test(name)).sort().map((name) =>
|
|
8509
|
+
return (await names(dir)).filter((name) => pattern.test(name)).sort().map((name) => path16.join(dir, name));
|
|
8218
8510
|
}
|
|
8219
8511
|
async function buildIndex() {
|
|
8220
8512
|
const years = await subfolders(sessionsDir, YEAR);
|
|
@@ -8258,7 +8550,7 @@ function createRolloutFinder(options) {
|
|
|
8258
8550
|
// src/collector/adapters/codex/sessionIndex.ts
|
|
8259
8551
|
var SESSION_INDEX_FILE = "session_index.jsonl";
|
|
8260
8552
|
var SESSION_INDEX_LIMIT_BYTES = 4 * 1024 * 1024;
|
|
8261
|
-
function
|
|
8553
|
+
function isRecord12(value) {
|
|
8262
8554
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8263
8555
|
}
|
|
8264
8556
|
function parseSessionIndex(content) {
|
|
@@ -8271,7 +8563,7 @@ function parseSessionIndex(content) {
|
|
|
8271
8563
|
} catch {
|
|
8272
8564
|
continue;
|
|
8273
8565
|
}
|
|
8274
|
-
if (!
|
|
8566
|
+
if (!isRecord12(value)) continue;
|
|
8275
8567
|
const { id, thread_name: name } = value;
|
|
8276
8568
|
if (typeof id !== "string" || id === "" || typeof name !== "string") continue;
|
|
8277
8569
|
const trimmed = name.trim();
|
|
@@ -8360,6 +8652,7 @@ function codexSession(input) {
|
|
|
8360
8652
|
const writtenAt = input.writtenAt === void 0 ? null : plausibleTime(Math.floor(input.writtenAt), now);
|
|
8361
8653
|
const beforeStart = writtenAt !== null && startedAt !== null && writtenAt < startedAt - WRITE_ORDER_SLACK_MS3;
|
|
8362
8654
|
if (writtenAt !== null && !beforeStart) session.lastWriteAt = writtenAt;
|
|
8655
|
+
if (state.tokens) session.tokens = { ...state.tokens };
|
|
8363
8656
|
return session;
|
|
8364
8657
|
}
|
|
8365
8658
|
|
|
@@ -8414,6 +8707,7 @@ var CODEX_CAPABILITIES = {
|
|
|
8414
8707
|
reason: "Codex's files name no process to find, and Codex documents no link to a session."
|
|
8415
8708
|
},
|
|
8416
8709
|
"quiet-for": { level: "yes" },
|
|
8710
|
+
tokens: { level: "yes" },
|
|
8417
8711
|
stop: {
|
|
8418
8712
|
level: "no",
|
|
8419
8713
|
reason: "Codex's files name no process that Agent Lookout could confirm and stop."
|
|
@@ -8433,10 +8727,10 @@ function createCodexAdapter(options = {}) {
|
|
|
8433
8727
|
const ownOverride = env[CODEX_HOME_ENV]?.trim() || void 0;
|
|
8434
8728
|
const codexOverride = env[CODEX_OWN_HOME_ENV]?.trim() || void 0;
|
|
8435
8729
|
const homeFrom = ownOverride ? "agent-lookout" : codexOverride ? "codex" : "default";
|
|
8436
|
-
const codexHome =
|
|
8437
|
-
const sessionsDir =
|
|
8438
|
-
const locksDir =
|
|
8439
|
-
const indexFile =
|
|
8730
|
+
const codexHome = path17.resolve(ownOverride ?? codexOverride ?? path17.join(homeDir, ".codex"));
|
|
8731
|
+
const sessionsDir = path17.join(codexHome, "sessions");
|
|
8732
|
+
const locksDir = path17.join(codexHome, WRITER_LOCK_DIR);
|
|
8733
|
+
const indexFile = path17.join(codexHome, SESSION_INDEX_FILE);
|
|
8440
8734
|
const homeName = tildify(codexHome, homeDir);
|
|
8441
8735
|
const sessionsName = tildify(sessionsDir, homeDir);
|
|
8442
8736
|
const locksName = tildify(locksDir, homeDir);
|
|
@@ -8619,8 +8913,8 @@ function createCodexAdapter(options = {}) {
|
|
|
8619
8913
|
} else if (olderCodex) {
|
|
8620
8914
|
detail += ` ${OLDER_CODEX_NOTE}`;
|
|
8621
8915
|
}
|
|
8622
|
-
const
|
|
8623
|
-
return { ...
|
|
8916
|
+
const answer2 = result("ok", detail, every(pollIntervalMs), checkedAt);
|
|
8917
|
+
return { ...answer2, sessions, basis: locks.supported ? BASIS2 : BASIS_WITHOUT_LOCKS };
|
|
8624
8918
|
}
|
|
8625
8919
|
return {
|
|
8626
8920
|
id: SOURCE_ID3,
|
|
@@ -8644,7 +8938,7 @@ function createCodexAdapter(options = {}) {
|
|
|
8644
8938
|
|
|
8645
8939
|
// src/collector/adapters/status-files/index.ts
|
|
8646
8940
|
import os7 from "node:os";
|
|
8647
|
-
import
|
|
8941
|
+
import path18 from "node:path";
|
|
8648
8942
|
|
|
8649
8943
|
// src/core/mapping/statusFileMapping.ts
|
|
8650
8944
|
var STATUS = /* @__PURE__ */ new Map([
|
|
@@ -8773,6 +9067,7 @@ var STATUS_FILE_CAPABILITIES = {
|
|
|
8773
9067
|
},
|
|
8774
9068
|
jump: { level: "no", reason: "Nothing in a status file is used to reach a session." },
|
|
8775
9069
|
"quiet-for": { level: "partly", reason: "If the agent writes its file again as it works." },
|
|
9070
|
+
tokens: { level: "no", reason: "A status file has no field for token counts." },
|
|
8776
9071
|
stop: {
|
|
8777
9072
|
level: "no",
|
|
8778
9073
|
reason: "Any program can write a status file, so nothing in one is used to stop a session."
|
|
@@ -8793,7 +9088,7 @@ function createStatusFileAdapter(options = {}) {
|
|
|
8793
9088
|
const isAlive = options.isAlive ?? isProcessAlive;
|
|
8794
9089
|
const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
8795
9090
|
const override = env[STATUS_DIR_ENV]?.trim() || void 0;
|
|
8796
|
-
const dir =
|
|
9091
|
+
const dir = path18.resolve(override ?? path18.join(homeDir, ".agent-lookout", "sessions"));
|
|
8797
9092
|
const dirName = tildify(dir, homeDir);
|
|
8798
9093
|
const decoder2 = new TextDecoder();
|
|
8799
9094
|
let lastRead = /* @__PURE__ */ new Map();
|
|
@@ -8853,7 +9148,7 @@ function createStatusFileAdapter(options = {}) {
|
|
|
8853
9148
|
if (names.length <= MAX_FILES) return names;
|
|
8854
9149
|
const written = await Promise.all(
|
|
8855
9150
|
names.map(async (name) => {
|
|
8856
|
-
const info = await io.lstat(
|
|
9151
|
+
const info = await io.lstat(path18.join(dir, name)).catch(() => null);
|
|
8857
9152
|
return { name, mtimeMs: info?.mtimeMs ?? -Infinity };
|
|
8858
9153
|
})
|
|
8859
9154
|
);
|
|
@@ -8874,14 +9169,14 @@ function createStatusFileAdapter(options = {}) {
|
|
|
8874
9169
|
names = await io.readdir(dir);
|
|
8875
9170
|
} catch (error) {
|
|
8876
9171
|
if (!isMissing2(error)) {
|
|
8877
|
-
const
|
|
9172
|
+
const answer2 = result(
|
|
8878
9173
|
"error",
|
|
8879
9174
|
`Status files could not be read: the folder ${dirName} could not be listed.`,
|
|
8880
9175
|
watching("cannot be read"),
|
|
8881
9176
|
checkedAt
|
|
8882
9177
|
);
|
|
8883
|
-
|
|
8884
|
-
return
|
|
9178
|
+
answer2.health.advice = "Check that your user account can open the folder.";
|
|
9179
|
+
return answer2;
|
|
8885
9180
|
}
|
|
8886
9181
|
contentsKnown = true;
|
|
8887
9182
|
lastRead = /* @__PURE__ */ new Map();
|
|
@@ -8903,7 +9198,7 @@ function createStatusFileAdapter(options = {}) {
|
|
|
8903
9198
|
const nextRead = /* @__PURE__ */ new Map();
|
|
8904
9199
|
const nextStatuses = /* @__PURE__ */ new Map();
|
|
8905
9200
|
for (const fileName2 of await toRead(listed)) {
|
|
8906
|
-
const reading = await readOne(
|
|
9201
|
+
const reading = await readOne(path18.join(dir, fileName2));
|
|
8907
9202
|
let kept;
|
|
8908
9203
|
if (reading.kind === "read") {
|
|
8909
9204
|
kept = reading;
|
|
@@ -9130,7 +9425,7 @@ function createEventStore(capacity = EVENT_CAPACITY) {
|
|
|
9130
9425
|
|
|
9131
9426
|
// src/collector/git/branchFinder.ts
|
|
9132
9427
|
import os8 from "node:os";
|
|
9133
|
-
import
|
|
9428
|
+
import path20 from "node:path";
|
|
9134
9429
|
|
|
9135
9430
|
// src/collector/git/gitHead.ts
|
|
9136
9431
|
var MAX_GIT_FILE_BYTES = 4 * 1024;
|
|
@@ -9170,18 +9465,18 @@ function parseCommonDir(content) {
|
|
|
9170
9465
|
|
|
9171
9466
|
// src/collector/git/repository.ts
|
|
9172
9467
|
import { createHash as createHash3 } from "node:crypto";
|
|
9173
|
-
import
|
|
9468
|
+
import path19 from "node:path";
|
|
9174
9469
|
var REPOSITORY_ID_LENGTH = 16;
|
|
9175
9470
|
function repositoryId(gitFolder) {
|
|
9176
9471
|
return createHash3("sha256").update(gitFolder).digest("hex").slice(0, REPOSITORY_ID_LENGTH);
|
|
9177
9472
|
}
|
|
9178
9473
|
function sharedName(common) {
|
|
9179
|
-
const base =
|
|
9180
|
-
if (base.startsWith(".")) return
|
|
9474
|
+
const base = path19.basename(common);
|
|
9475
|
+
if (base.startsWith(".")) return path19.basename(path19.dirname(common));
|
|
9181
9476
|
return base.endsWith(".git") ? base.slice(0, -".git".length) : base;
|
|
9182
9477
|
}
|
|
9183
9478
|
function repositoryOf({ dir, gitDir, commonDir }) {
|
|
9184
|
-
const name = sessionName(commonDir === null ?
|
|
9479
|
+
const name = sessionName(commonDir === null ? path19.basename(dir) : sharedName(commonDir));
|
|
9185
9480
|
return name === void 0 ? null : { id: repositoryId(commonDir ?? gitDir), name };
|
|
9186
9481
|
}
|
|
9187
9482
|
|
|
@@ -9210,13 +9505,13 @@ async function readGitFile(io, file, maxBytes = MAX_GIT_FILE_BYTES) {
|
|
|
9210
9505
|
}
|
|
9211
9506
|
}
|
|
9212
9507
|
async function commonDirOf(io, gitDir) {
|
|
9213
|
-
const content = await readGitFile(io,
|
|
9508
|
+
const content = await readGitFile(io, path20.join(gitDir, "commondir"));
|
|
9214
9509
|
if (content === void 0) return null;
|
|
9215
9510
|
const target = content === null ? null : parseCommonDir(content);
|
|
9216
|
-
return target === null ? void 0 :
|
|
9511
|
+
return target === null ? void 0 : path20.resolve(gitDir, target);
|
|
9217
9512
|
}
|
|
9218
9513
|
async function headIn(dir, kind, io) {
|
|
9219
|
-
const marker =
|
|
9514
|
+
const marker = path20.join(dir, ".git");
|
|
9220
9515
|
let gitDir;
|
|
9221
9516
|
if (kind === "directory") {
|
|
9222
9517
|
gitDir = marker;
|
|
@@ -9224,11 +9519,11 @@ async function headIn(dir, kind, io) {
|
|
|
9224
9519
|
const content2 = await readGitFile(io, marker);
|
|
9225
9520
|
const target = typeof content2 === "string" ? parseGitFile(content2) : null;
|
|
9226
9521
|
if (target === null) return null;
|
|
9227
|
-
gitDir =
|
|
9522
|
+
gitDir = path20.resolve(dir, target);
|
|
9228
9523
|
} else {
|
|
9229
9524
|
return null;
|
|
9230
9525
|
}
|
|
9231
|
-
const content = await readGitFile(io,
|
|
9526
|
+
const content = await readGitFile(io, path20.join(gitDir, "HEAD"));
|
|
9232
9527
|
const head = typeof content === "string" ? parseHead(content) : null;
|
|
9233
9528
|
if (head === null) return null;
|
|
9234
9529
|
const commonDir = kind === "file" ? await commonDirOf(io, gitDir) : null;
|
|
@@ -9237,20 +9532,20 @@ async function headIn(dir, kind, io) {
|
|
|
9237
9532
|
return { head: { ...head, repository }, gitFolder: commonDir ?? gitDir };
|
|
9238
9533
|
}
|
|
9239
9534
|
async function findHead(cwd, { io, homeDir }) {
|
|
9240
|
-
if (!
|
|
9241
|
-
const home =
|
|
9242
|
-
let dir =
|
|
9535
|
+
if (!path20.isAbsolute(cwd)) return null;
|
|
9536
|
+
const home = path20.resolve(homeDir);
|
|
9537
|
+
let dir = path20.resolve(cwd);
|
|
9243
9538
|
try {
|
|
9244
9539
|
if ((await io.stat(dir)).kind !== "directory") return null;
|
|
9245
9540
|
} catch {
|
|
9246
9541
|
return null;
|
|
9247
9542
|
}
|
|
9248
9543
|
for (let level = 0; level < MAX_LEVELS; level += 1) {
|
|
9249
|
-
const parent =
|
|
9544
|
+
const parent = path20.dirname(dir);
|
|
9250
9545
|
if (dir === home || dir === parent) return null;
|
|
9251
9546
|
let kind;
|
|
9252
9547
|
try {
|
|
9253
|
-
kind = (await io.lstat(
|
|
9548
|
+
kind = (await io.lstat(path20.join(dir, ".git"))).kind;
|
|
9254
9549
|
} catch (error) {
|
|
9255
9550
|
if (!isMissing2(error)) return null;
|
|
9256
9551
|
dir = parent;
|
|
@@ -9333,14 +9628,14 @@ var STATUS_STATE = {
|
|
|
9333
9628
|
PENDING: "pending",
|
|
9334
9629
|
EXPECTED: "pending"
|
|
9335
9630
|
};
|
|
9336
|
-
function
|
|
9631
|
+
function isRecord13(value) {
|
|
9337
9632
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9338
9633
|
}
|
|
9339
9634
|
function textOf(value) {
|
|
9340
9635
|
return typeof value === "string" ? value : "";
|
|
9341
9636
|
}
|
|
9342
9637
|
function checkOf(value) {
|
|
9343
|
-
if (!
|
|
9638
|
+
if (!isRecord13(value)) return null;
|
|
9344
9639
|
const startedAt = Date.parse(textOf(value.startedAt));
|
|
9345
9640
|
const at = Number.isNaN(startedAt) ? -Infinity : startedAt;
|
|
9346
9641
|
if (value.__typename === "CheckRun") {
|
|
@@ -9389,7 +9684,7 @@ function readGhAnswer(stdout, repository) {
|
|
|
9389
9684
|
} catch {
|
|
9390
9685
|
return null;
|
|
9391
9686
|
}
|
|
9392
|
-
if (!
|
|
9687
|
+
if (!isRecord13(data)) return null;
|
|
9393
9688
|
const { number } = data;
|
|
9394
9689
|
const state = stateOf3(data.state, data.isDraft);
|
|
9395
9690
|
const title = typeof data.title === "string" ? sessionName(data.title) : void 0;
|
|
@@ -9435,11 +9730,10 @@ function ghArguments(target) {
|
|
|
9435
9730
|
];
|
|
9436
9731
|
}
|
|
9437
9732
|
var LEFT_OUT = ["GH_FORCE_TTY", "GH_DEBUG", "DEBUG", "GH_PAGER", "PAGER"];
|
|
9438
|
-
var OWN_SETTING = /^AGENT_LOOKOUT_/;
|
|
9439
9733
|
function ghEnvironment(env) {
|
|
9440
9734
|
const given = {};
|
|
9441
|
-
for (const [name, value] of Object.entries(env)) {
|
|
9442
|
-
if (!
|
|
9735
|
+
for (const [name, value] of Object.entries(childEnvironment(env))) {
|
|
9736
|
+
if (!LEFT_OUT.includes(name)) given[name] = value;
|
|
9443
9737
|
}
|
|
9444
9738
|
return {
|
|
9445
9739
|
...given,
|
|
@@ -9528,7 +9822,7 @@ function createGhAsker(options) {
|
|
|
9528
9822
|
}
|
|
9529
9823
|
|
|
9530
9824
|
// src/collector/git/pullRequestTarget.ts
|
|
9531
|
-
import
|
|
9825
|
+
import path21 from "node:path";
|
|
9532
9826
|
|
|
9533
9827
|
// src/collector/git/gitConfig.ts
|
|
9534
9828
|
var MAX_CONFIG_BYTES = 64 * 1024;
|
|
@@ -9786,7 +10080,7 @@ function targetOf(config, branch, base, defaultBranch) {
|
|
|
9786
10080
|
async function readPullRequestTarget(gitFolder, branch, io = nodeIo2) {
|
|
9787
10081
|
try {
|
|
9788
10082
|
if (!isAskableBranch(branch)) return null;
|
|
9789
|
-
const text4 = await readGitFile(io,
|
|
10083
|
+
const text4 = await readGitFile(io, path21.join(gitFolder, "config"), MAX_CONFIG_BYTES);
|
|
9790
10084
|
if (typeof text4 !== "string") return null;
|
|
9791
10085
|
const config = parseGitConfig(text4);
|
|
9792
10086
|
const base = config === null ? null : baseRemoteOf(config);
|
|
@@ -9795,7 +10089,7 @@ async function readPullRequestTarget(gitFolder, branch, io = nodeIo2) {
|
|
|
9795
10089
|
if (base.name !== null && isSafeRemoteName(base.name)) {
|
|
9796
10090
|
const head = await readGitFile(
|
|
9797
10091
|
io,
|
|
9798
|
-
|
|
10092
|
+
path21.join(gitFolder, "refs", "remotes", base.name, "HEAD")
|
|
9799
10093
|
);
|
|
9800
10094
|
if (typeof head === "string") defaultBranch = parseRemoteHead(head, base.name);
|
|
9801
10095
|
}
|
|
@@ -9830,19 +10124,19 @@ function createPullRequestFinder(options) {
|
|
|
9830
10124
|
let gh = "unknown";
|
|
9831
10125
|
let last = null;
|
|
9832
10126
|
const due = (at, since) => at - (since ?? -Infinity) >= intervalMs;
|
|
9833
|
-
function take(question,
|
|
9834
|
-
switch (
|
|
10127
|
+
function take(question, answer2, at) {
|
|
10128
|
+
switch (answer2.kind) {
|
|
9835
10129
|
case "found":
|
|
9836
10130
|
case "none":
|
|
9837
10131
|
gh = "ready";
|
|
9838
10132
|
last = { at, ok: true };
|
|
9839
10133
|
answers2.set(question, {
|
|
9840
|
-
pullRequest:
|
|
10134
|
+
pullRequest: answer2.kind === "found" ? answer2.pullRequest : null,
|
|
9841
10135
|
askedAt: at
|
|
9842
10136
|
});
|
|
9843
10137
|
return true;
|
|
9844
10138
|
case "failed":
|
|
9845
|
-
last = { at, ok: false, reason:
|
|
10139
|
+
last = { at, ok: false, reason: answer2.reason };
|
|
9846
10140
|
answers2.set(question, {
|
|
9847
10141
|
pullRequest: answers2.get(question)?.pullRequest ?? null,
|
|
9848
10142
|
askedAt: at
|
|
@@ -9850,7 +10144,7 @@ function createPullRequestFinder(options) {
|
|
|
9850
10144
|
return true;
|
|
9851
10145
|
case "not-found":
|
|
9852
10146
|
case "signed-out":
|
|
9853
|
-
gh =
|
|
10147
|
+
gh = answer2.kind;
|
|
9854
10148
|
answers2.clear();
|
|
9855
10149
|
pausedUntil = at + intervalMs;
|
|
9856
10150
|
return false;
|
|
@@ -9870,14 +10164,14 @@ function createPullRequestFinder(options) {
|
|
|
9870
10164
|
const at = now();
|
|
9871
10165
|
if (at < pausedUntil) return;
|
|
9872
10166
|
if (!due(at, answers2.get(question)?.askedAt)) continue;
|
|
9873
|
-
const
|
|
10167
|
+
const answer2 = await ask(target).catch(() => ({
|
|
9874
10168
|
kind: "failed",
|
|
9875
10169
|
reason: "gh could not be run"
|
|
9876
10170
|
}));
|
|
9877
10171
|
if (![...reads.values()].some((read) => read.target && questionOf(read.target) === question)) {
|
|
9878
10172
|
continue;
|
|
9879
10173
|
}
|
|
9880
|
-
if (!take(question,
|
|
10174
|
+
if (!take(question, answer2, now())) return;
|
|
9881
10175
|
}
|
|
9882
10176
|
}
|
|
9883
10177
|
return {
|
|
@@ -9966,7 +10260,7 @@ function createClearHistoryRoute(options) {
|
|
|
9966
10260
|
}
|
|
9967
10261
|
|
|
9968
10262
|
// src/collector/history/historyKeeper.ts
|
|
9969
|
-
import
|
|
10263
|
+
import path22 from "node:path";
|
|
9970
10264
|
|
|
9971
10265
|
// src/collector/history/historyFiles.ts
|
|
9972
10266
|
import {
|
|
@@ -10266,7 +10560,7 @@ function createHistoryKeeper(options) {
|
|
|
10266
10560
|
});
|
|
10267
10561
|
const lock = createWriterLock({
|
|
10268
10562
|
fs,
|
|
10269
|
-
file:
|
|
10563
|
+
file: path22.join(dir, LOCK_FILE),
|
|
10270
10564
|
pid: options.pid,
|
|
10271
10565
|
now,
|
|
10272
10566
|
isAlive: options.isAlive,
|
|
@@ -10276,6 +10570,7 @@ function createHistoryKeeper(options) {
|
|
|
10276
10570
|
const blocked = /* @__PURE__ */ new Set();
|
|
10277
10571
|
const touched = /* @__PURE__ */ new Set();
|
|
10278
10572
|
let queue = [];
|
|
10573
|
+
let writing = null;
|
|
10279
10574
|
let startedAt = null;
|
|
10280
10575
|
let target = null;
|
|
10281
10576
|
let restored = false;
|
|
@@ -10292,7 +10587,7 @@ function createHistoryKeeper(options) {
|
|
|
10292
10587
|
});
|
|
10293
10588
|
return next;
|
|
10294
10589
|
}
|
|
10295
|
-
const full = (name) =>
|
|
10590
|
+
const full = (name) => path22.join(dir, name);
|
|
10296
10591
|
function hold(record) {
|
|
10297
10592
|
if (unusable !== null || !running) return;
|
|
10298
10593
|
const line = encodeRecord(record);
|
|
@@ -10468,7 +10763,7 @@ function createHistoryKeeper(options) {
|
|
|
10468
10763
|
bytes3 += lineBytes;
|
|
10469
10764
|
end += 1;
|
|
10470
10765
|
}
|
|
10471
|
-
const outcome = yield { name, text: text4 };
|
|
10766
|
+
const outcome = yield { name, text: text4, count: end - index };
|
|
10472
10767
|
if (outcome === "blocked") {
|
|
10473
10768
|
blocked.add(name);
|
|
10474
10769
|
part += 1;
|
|
@@ -10498,11 +10793,15 @@ function createHistoryKeeper(options) {
|
|
|
10498
10793
|
}
|
|
10499
10794
|
async function writeLines(lines) {
|
|
10500
10795
|
if (lines.length === 0) return true;
|
|
10796
|
+
const job = { lines: [...lines] };
|
|
10797
|
+
writing = job;
|
|
10501
10798
|
const steps = chunks(lines);
|
|
10502
10799
|
let step = steps.next("written");
|
|
10503
10800
|
while (!step.done) {
|
|
10504
|
-
|
|
10801
|
+
if (writing !== job) return false;
|
|
10802
|
+
const { name, text: text4, count: count2 } = step.value;
|
|
10505
10803
|
await prune(name, byteLength(text4));
|
|
10804
|
+
if (writing !== job) return false;
|
|
10506
10805
|
try {
|
|
10507
10806
|
await fs.append(full(name), text4);
|
|
10508
10807
|
} catch (error) {
|
|
@@ -10510,11 +10809,14 @@ function createHistoryKeeper(options) {
|
|
|
10510
10809
|
step = steps.next("blocked");
|
|
10511
10810
|
continue;
|
|
10512
10811
|
}
|
|
10812
|
+
writing = null;
|
|
10513
10813
|
writeFailed();
|
|
10514
10814
|
return false;
|
|
10515
10815
|
}
|
|
10816
|
+
job.lines = job.lines.slice(count2);
|
|
10516
10817
|
step = steps.next("written");
|
|
10517
10818
|
}
|
|
10819
|
+
writing = null;
|
|
10518
10820
|
wrote();
|
|
10519
10821
|
return true;
|
|
10520
10822
|
}
|
|
@@ -10643,8 +10945,10 @@ function createHistoryKeeper(options) {
|
|
|
10643
10945
|
running = false;
|
|
10644
10946
|
if (timer) clearInterval(timer);
|
|
10645
10947
|
timer = null;
|
|
10948
|
+
const left = [...writing?.lines ?? [], ...takeQueued()];
|
|
10949
|
+
writing = null;
|
|
10646
10950
|
if (writer === true && unusable === null && lock.stillHeldNow()) {
|
|
10647
|
-
writeLinesNow(
|
|
10951
|
+
writeLinesNow(left);
|
|
10648
10952
|
}
|
|
10649
10953
|
queue = [];
|
|
10650
10954
|
startedAt = null;
|
|
@@ -10754,13 +11058,13 @@ function keptHistoryStore(store, keeper) {
|
|
|
10754
11058
|
// src/collector/history/historySettings.ts
|
|
10755
11059
|
import { createHash as createHash4 } from "node:crypto";
|
|
10756
11060
|
import os9 from "node:os";
|
|
10757
|
-
import
|
|
11061
|
+
import path23 from "node:path";
|
|
10758
11062
|
var HISTORY_ENV = "AGENT_LOOKOUT_HISTORY";
|
|
10759
11063
|
var HISTORY_DIR_ENV = "AGENT_LOOKOUT_HISTORY_DIR";
|
|
10760
11064
|
function readHistorySetup(env, homeDir = os9.homedir()) {
|
|
10761
11065
|
if (env[HISTORY_ENV]?.trim().toLowerCase() === "off") return { on: false };
|
|
10762
11066
|
const override = env[HISTORY_DIR_ENV]?.trim() || void 0;
|
|
10763
|
-
const dir =
|
|
11067
|
+
const dir = path23.resolve(override ?? path23.join(homeDir, ".agent-lookout", "history"));
|
|
10764
11068
|
return {
|
|
10765
11069
|
on: true,
|
|
10766
11070
|
dir,
|
|
@@ -10771,14 +11075,57 @@ function readHistorySetup(env, homeDir = os9.homedir()) {
|
|
|
10771
11075
|
function sourcesFingerprint(env, homeDir = os9.homedir()) {
|
|
10772
11076
|
const set = (name) => env[name]?.trim() || void 0;
|
|
10773
11077
|
const folders = [
|
|
10774
|
-
|
|
10775
|
-
|
|
10776
|
-
|
|
10777
|
-
|
|
11078
|
+
path23.resolve(set(CLAUDE_HOME_ENV) ?? path23.join(homeDir, ".claude")),
|
|
11079
|
+
path23.resolve(set(CODEX_HOME_ENV) ?? set(CODEX_OWN_HOME_ENV) ?? path23.join(homeDir, ".codex")),
|
|
11080
|
+
path23.resolve(set(STATUS_DIR_ENV) ?? path23.join(homeDir, ".agent-lookout", "sessions")),
|
|
11081
|
+
path23.resolve(set(ANTIGRAVITY_HOME_ENV) ?? path23.join(homeDir, ...DEFAULT_HOME))
|
|
10778
11082
|
];
|
|
10779
11083
|
return createHash4("sha256").update(JSON.stringify(folders)).digest("hex").slice(0, 32);
|
|
10780
11084
|
}
|
|
10781
11085
|
|
|
11086
|
+
// src/collector/messages/lastMessageRoute.ts
|
|
11087
|
+
var answer = (body) => ({ status: 200, body });
|
|
11088
|
+
var failed3 = (status, error, headers) => ({
|
|
11089
|
+
status,
|
|
11090
|
+
body: { error },
|
|
11091
|
+
...headers && { headers }
|
|
11092
|
+
});
|
|
11093
|
+
function idIn(query) {
|
|
11094
|
+
const pairs = [...query];
|
|
11095
|
+
if (pairs.length !== 1) return null;
|
|
11096
|
+
const [name, value] = pairs[0] ?? ["", ""];
|
|
11097
|
+
if (name !== "id" || value === "" || value.length > MAX_SESSION_ID_LENGTH) return null;
|
|
11098
|
+
return value;
|
|
11099
|
+
}
|
|
11100
|
+
function createLastMessageRoute(options) {
|
|
11101
|
+
const { poller, adapters } = options;
|
|
11102
|
+
const off = lastMessageOff(options.env);
|
|
11103
|
+
return async (req, url) => {
|
|
11104
|
+
const site = req.headers["sec-fetch-site"];
|
|
11105
|
+
if (site !== void 0 && site !== "same-origin") {
|
|
11106
|
+
return failed3(403, "This address only answers the dashboard page served from this machine.");
|
|
11107
|
+
}
|
|
11108
|
+
const id = idIn(url.searchParams);
|
|
11109
|
+
if (id === null) {
|
|
11110
|
+
return failed3(400, "Name one session as id, and nothing else.");
|
|
11111
|
+
}
|
|
11112
|
+
if (off) return answer({ message: null, reason: "off", setting: LAST_MESSAGE_ENV });
|
|
11113
|
+
const session = poller.getSnapshot().sessions.find((listed) => listed.id === id);
|
|
11114
|
+
const notListed = () => failed3(404, "No session with that id is listed.");
|
|
11115
|
+
if (session === void 0) return notListed();
|
|
11116
|
+
const adapter = adapters.find((candidate) => candidate.id === session.source);
|
|
11117
|
+
if (adapter?.lastMessage === void 0) return answer({ message: null, reason: "not-read" });
|
|
11118
|
+
const read = await adapter.lastMessage(session.id);
|
|
11119
|
+
if (read === null) return notListed();
|
|
11120
|
+
if (read === "busy") {
|
|
11121
|
+
return failed3(429, "Too many last messages were asked for at once. Ask again in a second.", {
|
|
11122
|
+
"Retry-After": "1"
|
|
11123
|
+
});
|
|
11124
|
+
}
|
|
11125
|
+
return answer(read);
|
|
11126
|
+
};
|
|
11127
|
+
}
|
|
11128
|
+
|
|
10782
11129
|
// src/collector/notifications/heldWait.ts
|
|
10783
11130
|
var HANDOVER_GRACE_MS = 3e3;
|
|
10784
11131
|
var PAGE_GONE_AFTER_MS = 5e3;
|
|
@@ -10944,6 +11291,7 @@ function runOsascript(args, options = {}) {
|
|
|
10944
11291
|
OSASCRIPT,
|
|
10945
11292
|
[...args],
|
|
10946
11293
|
{
|
|
11294
|
+
env: childEnvironment(process.env),
|
|
10947
11295
|
timeout: options.timeoutMs ?? TERMINAL_JUMP_TIMEOUT_MS,
|
|
10948
11296
|
maxBuffer: MAX_OUTPUT_BYTES4,
|
|
10949
11297
|
encoding: "utf8",
|
|
@@ -11266,7 +11614,7 @@ function channelIn(body) {
|
|
|
11266
11614
|
function asSentence(reason) {
|
|
11267
11615
|
return `${reason.charAt(0).toUpperCase()}${reason.slice(1)}.`;
|
|
11268
11616
|
}
|
|
11269
|
-
var
|
|
11617
|
+
var failed4 = (status, reason, error, more = {}) => ({
|
|
11270
11618
|
status,
|
|
11271
11619
|
body: {
|
|
11272
11620
|
error,
|
|
@@ -11298,21 +11646,21 @@ function createTestSendRoute(options) {
|
|
|
11298
11646
|
const { name, setting } = NAMED[channel];
|
|
11299
11647
|
const tester = channels[channel];
|
|
11300
11648
|
if (tester === null) {
|
|
11301
|
-
return
|
|
11649
|
+
return failed4(
|
|
11302
11650
|
409,
|
|
11303
11651
|
"off",
|
|
11304
11652
|
`${name} is not set up. Set ${setting} and start Agent Lookout again.`
|
|
11305
11653
|
);
|
|
11306
11654
|
}
|
|
11307
11655
|
if (!limiter.begin()) {
|
|
11308
|
-
return
|
|
11656
|
+
return failed4(429, "too-soon", "One test is sent a second. Try again in a moment.", {
|
|
11309
11657
|
retryAfter: 1
|
|
11310
11658
|
});
|
|
11311
11659
|
}
|
|
11312
11660
|
try {
|
|
11313
11661
|
const result = await tester.test();
|
|
11314
11662
|
if (!result.tried) {
|
|
11315
|
-
return
|
|
11663
|
+
return failed4(
|
|
11316
11664
|
429,
|
|
11317
11665
|
"limited",
|
|
11318
11666
|
`${SENDS_PER_HOUR} ${name} pushes were tried in the last hour, the most it tries.`,
|
|
@@ -11323,7 +11671,7 @@ function createTestSendRoute(options) {
|
|
|
11323
11671
|
);
|
|
11324
11672
|
}
|
|
11325
11673
|
if (!result.outcome.sent) {
|
|
11326
|
-
return
|
|
11674
|
+
return failed4(502, "not-sent", asSentence(result.outcome.reason));
|
|
11327
11675
|
}
|
|
11328
11676
|
return {
|
|
11329
11677
|
status: 200,
|
|
@@ -11423,12 +11771,12 @@ function askRemote(port, route, timeoutMs = REMOTE_ANSWER_TIMEOUT_MS) {
|
|
|
11423
11771
|
return new Promise((resolve) => {
|
|
11424
11772
|
let connected = false;
|
|
11425
11773
|
let settled = false;
|
|
11426
|
-
const finish = (
|
|
11774
|
+
const finish = (answer2) => {
|
|
11427
11775
|
if (settled) return;
|
|
11428
11776
|
settled = true;
|
|
11429
11777
|
clearTimeout(timer);
|
|
11430
11778
|
req.destroy();
|
|
11431
|
-
resolve(
|
|
11779
|
+
resolve(answer2);
|
|
11432
11780
|
};
|
|
11433
11781
|
const req = request({
|
|
11434
11782
|
host: "127.0.0.1",
|
|
@@ -11491,7 +11839,7 @@ var MAX_REMOTE_SOURCES = 20;
|
|
|
11491
11839
|
var MAX_ID_LENGTH = 512;
|
|
11492
11840
|
var MAX_PATH_LENGTH = 4096;
|
|
11493
11841
|
var MAX_WORD_LENGTH2 = 200;
|
|
11494
|
-
function
|
|
11842
|
+
function isRecord14(value) {
|
|
11495
11843
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11496
11844
|
}
|
|
11497
11845
|
function oneOf2(allowed, value) {
|
|
@@ -11505,7 +11853,7 @@ function exact(value, max) {
|
|
|
11505
11853
|
var COMMIT_ID2 = /^[0-9a-f]{7,64}$/;
|
|
11506
11854
|
var REPOSITORY_ID = /^[0-9a-f]{16,64}$/;
|
|
11507
11855
|
function readGit(value, machine) {
|
|
11508
|
-
if (!
|
|
11856
|
+
if (!isRecord14(value)) return void 0;
|
|
11509
11857
|
let head;
|
|
11510
11858
|
if (value.commit !== void 0) {
|
|
11511
11859
|
if (value.branch !== void 0 || typeof value.commit !== "string") return void 0;
|
|
@@ -11517,7 +11865,7 @@ function readGit(value, machine) {
|
|
|
11517
11865
|
head = { branch };
|
|
11518
11866
|
}
|
|
11519
11867
|
const repository = value.repository;
|
|
11520
|
-
if (
|
|
11868
|
+
if (isRecord14(repository) && typeof repository.id === "string") {
|
|
11521
11869
|
const name = sessionName(typeof repository.name === "string" ? repository.name : "");
|
|
11522
11870
|
if (name !== void 0 && REPOSITORY_ID.test(repository.id)) {
|
|
11523
11871
|
head.repository = { id: repositoryId(`${machine}
|
|
@@ -11527,7 +11875,7 @@ ${repository.id}`), name };
|
|
|
11527
11875
|
return head;
|
|
11528
11876
|
}
|
|
11529
11877
|
function readSession(value, machine, sources, now, keepWaitingText) {
|
|
11530
|
-
if (!
|
|
11878
|
+
if (!isRecord14(value)) return null;
|
|
11531
11879
|
const id = exact(value.id, MAX_ID_LENGTH);
|
|
11532
11880
|
const source = exact(value.source, MAX_WORD_LENGTH2);
|
|
11533
11881
|
if (id === null || source === null) return null;
|
|
@@ -11561,13 +11909,15 @@ function readSession(value, machine, sources, now, keepWaitingText) {
|
|
|
11561
11909
|
}
|
|
11562
11910
|
const lastWriteAt = plausibleTime(value.lastWriteAt, now);
|
|
11563
11911
|
if (lastWriteAt !== null) session.lastWriteAt = lastWriteAt;
|
|
11912
|
+
const tokens = tokenCountsOf(value.tokens);
|
|
11913
|
+
if (tokens) session.tokens = tokens;
|
|
11564
11914
|
if (typeof value.alive === "boolean") session.alive = value.alive;
|
|
11565
11915
|
const git = readGit(value.git, machine);
|
|
11566
11916
|
if (git) session.git = git;
|
|
11567
11917
|
return session;
|
|
11568
11918
|
}
|
|
11569
11919
|
function readCell(value) {
|
|
11570
|
-
if (!
|
|
11920
|
+
if (!isRecord14(value)) return null;
|
|
11571
11921
|
if (value.level === "yes") return { level: "yes" };
|
|
11572
11922
|
const level = oneOf2(["no", "partly"], value.level);
|
|
11573
11923
|
const reason = cut(value.reason, MAX_WORD_LENGTH2);
|
|
@@ -11575,10 +11925,17 @@ function readCell(value) {
|
|
|
11575
11925
|
}
|
|
11576
11926
|
var ACTS_HERE = ["jump", "stop", "answer"];
|
|
11577
11927
|
function readCapabilities(value, machine) {
|
|
11578
|
-
if (!
|
|
11928
|
+
if (!isRecord14(value)) return null;
|
|
11579
11929
|
const read = {};
|
|
11580
11930
|
for (const capability of CAPABILITIES) {
|
|
11581
11931
|
if (ACTS_HERE.includes(capability)) continue;
|
|
11932
|
+
if (capability === "tokens" && value.tokens === void 0) {
|
|
11933
|
+
read.tokens = {
|
|
11934
|
+
level: "no",
|
|
11935
|
+
reason: `The Agent Lookout on ${machine} does not send token counts.`
|
|
11936
|
+
};
|
|
11937
|
+
continue;
|
|
11938
|
+
}
|
|
11582
11939
|
const cell = readCell(value[capability]);
|
|
11583
11940
|
if (!cell) return null;
|
|
11584
11941
|
read[capability] = cell;
|
|
@@ -11591,7 +11948,7 @@ function readCapabilities(value, machine) {
|
|
|
11591
11948
|
};
|
|
11592
11949
|
}
|
|
11593
11950
|
function readRemoteSnapshot(value, machine, now, options = {}) {
|
|
11594
|
-
if (!
|
|
11951
|
+
if (!isRecord14(value) || !Array.isArray(value.sessions) || !Array.isArray(value.sources)) {
|
|
11595
11952
|
return null;
|
|
11596
11953
|
}
|
|
11597
11954
|
const named = /* @__PURE__ */ new Map();
|
|
@@ -11599,7 +11956,7 @@ function readRemoteSnapshot(value, machine, now, options = {}) {
|
|
|
11599
11956
|
const agents = [];
|
|
11600
11957
|
for (const item of value.sources) {
|
|
11601
11958
|
if (sources.length >= MAX_REMOTE_SOURCES) break;
|
|
11602
|
-
if (!
|
|
11959
|
+
if (!isRecord14(item)) continue;
|
|
11603
11960
|
const id = exact(item.id, MAX_WORD_LENGTH2);
|
|
11604
11961
|
if (id === null || isRemoteSource(id) || item.machine !== void 0) continue;
|
|
11605
11962
|
const label2 = cut(item.label, MAX_WORD_LENGTH2) ?? id;
|
|
@@ -11624,7 +11981,7 @@ function readRemoteSnapshot(value, machine, now, options = {}) {
|
|
|
11624
11981
|
}
|
|
11625
11982
|
|
|
11626
11983
|
// src/collector/remotes/sshProgram.ts
|
|
11627
|
-
import
|
|
11984
|
+
import path24 from "node:path";
|
|
11628
11985
|
var SSH_BIN_ENV = "AGENT_LOOKOUT_SSH_BIN";
|
|
11629
11986
|
var SSH_LOCATIONS = ["/usr/bin/ssh", "/opt/homebrew/bin/ssh", "/usr/local/bin/ssh"];
|
|
11630
11987
|
var SERVER_ALIVE_SECONDS = 15;
|
|
@@ -11646,7 +12003,7 @@ async function findSsh(env, isExecutable, platform = process.platform) {
|
|
|
11646
12003
|
}
|
|
11647
12004
|
return {
|
|
11648
12005
|
found: false,
|
|
11649
|
-
looked: `The ssh command was not found on PATH or in ${SSH_LOCATIONS.map((at) =>
|
|
12006
|
+
looked: `The ssh command was not found on PATH or in ${SSH_LOCATIONS.map((at) => path24.dirname(at)).join(", ")}`
|
|
11650
12007
|
};
|
|
11651
12008
|
}
|
|
11652
12009
|
function sshArguments(forward) {
|
|
@@ -11988,7 +12345,11 @@ function freeLoopbackPort() {
|
|
|
11988
12345
|
});
|
|
11989
12346
|
});
|
|
11990
12347
|
}
|
|
11991
|
-
var spawnSsh = (binary, args, env) => spawn2(binary, [...args], {
|
|
12348
|
+
var spawnSsh = (binary, args, env) => spawn2(binary, [...args], {
|
|
12349
|
+
env: childEnvironment(env),
|
|
12350
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
12351
|
+
windowsHide: true
|
|
12352
|
+
});
|
|
11992
12353
|
var live = /* @__PURE__ */ new Set();
|
|
11993
12354
|
var endedOnExit = false;
|
|
11994
12355
|
function endOnExit(child) {
|
|
@@ -12242,14 +12603,14 @@ import {
|
|
|
12242
12603
|
writeSync as writeSync2
|
|
12243
12604
|
} from "node:fs";
|
|
12244
12605
|
import os10 from "node:os";
|
|
12245
|
-
import
|
|
12606
|
+
import path25 from "node:path";
|
|
12246
12607
|
var SETTINGS_FILE_ENV = "AGENT_LOOKOUT_SETTINGS_FILE";
|
|
12247
12608
|
var SETTINGS_FILE_MODE = 384;
|
|
12248
12609
|
var SETTINGS_FOLDER_MODE = 448;
|
|
12249
12610
|
var MAX_SETTINGS_BYTES = 64 * 1024;
|
|
12250
12611
|
function readSettingsSetup(env, homeDir = os10.homedir()) {
|
|
12251
12612
|
const named = env[SETTINGS_FILE_ENV]?.trim() || void 0;
|
|
12252
|
-
const file =
|
|
12613
|
+
const file = path25.resolve(named ?? path25.join(homeDir, ".agent-lookout", "settings.json"));
|
|
12253
12614
|
return { file, shown: tildify(file, homeDir) };
|
|
12254
12615
|
}
|
|
12255
12616
|
function codeOf(error) {
|
|
@@ -12294,7 +12655,7 @@ function readSettingsStamp(setup) {
|
|
|
12294
12655
|
}
|
|
12295
12656
|
function readSettingsText(setup) {
|
|
12296
12657
|
const { file, shown } = setup;
|
|
12297
|
-
const folder =
|
|
12658
|
+
const folder = path25.dirname(file);
|
|
12298
12659
|
try {
|
|
12299
12660
|
const folderKind = kindAt(folder);
|
|
12300
12661
|
if (folderKind === null) return { kind: "missing" };
|
|
@@ -12353,7 +12714,7 @@ function readSettingsText(setup) {
|
|
|
12353
12714
|
}
|
|
12354
12715
|
function writeSettingsText(setup, text4) {
|
|
12355
12716
|
const { file, shown } = setup;
|
|
12356
|
-
const folder =
|
|
12717
|
+
const folder = path25.dirname(file);
|
|
12357
12718
|
const notSaved = {
|
|
12358
12719
|
ok: false,
|
|
12359
12720
|
problem: `${shown} could not be written, so the change was not saved.`
|
|
@@ -12374,9 +12735,9 @@ function writeSettingsText(setup, text4) {
|
|
|
12374
12735
|
problem: `${shown} is a link or not an ordinary file, which Agent Lookout does not write through, so the change was not saved.`
|
|
12375
12736
|
};
|
|
12376
12737
|
}
|
|
12377
|
-
temporary =
|
|
12738
|
+
temporary = path25.join(
|
|
12378
12739
|
folder,
|
|
12379
|
-
`.${
|
|
12740
|
+
`.${path25.basename(file)}.${process.pid}.${randomBytes3(6).toString("hex")}.tmp`
|
|
12380
12741
|
);
|
|
12381
12742
|
const fd = openWithoutFollowingNow(
|
|
12382
12743
|
temporary,
|
|
@@ -12416,7 +12777,7 @@ var RULE_WORDS = {
|
|
|
12416
12777
|
idle: "the idle rule",
|
|
12417
12778
|
quietHours: "the quiet hours"
|
|
12418
12779
|
};
|
|
12419
|
-
function
|
|
12780
|
+
function isRecord15(value) {
|
|
12420
12781
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12421
12782
|
}
|
|
12422
12783
|
function listOf(names) {
|
|
@@ -12454,7 +12815,7 @@ function rulesIn(read, shown) {
|
|
|
12454
12815
|
} catch {
|
|
12455
12816
|
value = null;
|
|
12456
12817
|
}
|
|
12457
|
-
if (!
|
|
12818
|
+
if (!isRecord15(value)) {
|
|
12458
12819
|
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.`;
|
|
12459
12820
|
return {
|
|
12460
12821
|
rules: off,
|
|
@@ -12469,7 +12830,7 @@ function rulesIn(read, shown) {
|
|
|
12469
12830
|
const { timeRules, permissionRules: heldRules, ...rest } = value;
|
|
12470
12831
|
const permissions = readPermissionRules(heldRules);
|
|
12471
12832
|
const { rules, unread } = readTimeRules(timeRules);
|
|
12472
|
-
const otherRules =
|
|
12833
|
+
const otherRules = isRecord15(timeRules) ? Object.fromEntries(
|
|
12473
12834
|
Object.entries(timeRules).filter(
|
|
12474
12835
|
([name]) => !TIME_RULE_NAMES.includes(name)
|
|
12475
12836
|
)
|
|
@@ -12571,15 +12932,15 @@ import { randomBytes as randomBytes4 } from "node:crypto";
|
|
|
12571
12932
|
|
|
12572
12933
|
// src/core/permission-rules/rulesChange.ts
|
|
12573
12934
|
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.';
|
|
12574
|
-
function
|
|
12935
|
+
function isRecord16(value) {
|
|
12575
12936
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12576
12937
|
}
|
|
12577
12938
|
function rulesChangeIn(value) {
|
|
12578
12939
|
const refused = { ok: false, problem: CHANGE_SHAPE };
|
|
12579
|
-
if (!
|
|
12940
|
+
if (!isRecord16(value) || Object.keys(value).length !== 1) return refused;
|
|
12580
12941
|
const [kind] = Object.keys(value);
|
|
12581
12942
|
const body = value[kind];
|
|
12582
|
-
if (!
|
|
12943
|
+
if (!isRecord16(body)) return refused;
|
|
12583
12944
|
switch (kind) {
|
|
12584
12945
|
case "add": {
|
|
12585
12946
|
const read = ruleWordsIn(body);
|
|
@@ -12655,7 +13016,7 @@ function applyRulesChange(rules, change, newId) {
|
|
|
12655
13016
|
|
|
12656
13017
|
// src/collector/settings/permissionRulesRoute.ts
|
|
12657
13018
|
var MAX_PERMISSION_RULES_BODY_BYTES = 1024;
|
|
12658
|
-
var
|
|
13019
|
+
var failed5 = (status, reason, error) => ({
|
|
12659
13020
|
status,
|
|
12660
13021
|
body: { error, reason }
|
|
12661
13022
|
});
|
|
@@ -12682,14 +13043,14 @@ function createPermissionRulesRoute(options) {
|
|
|
12682
13043
|
try {
|
|
12683
13044
|
value = JSON.parse(body.text);
|
|
12684
13045
|
} catch {
|
|
12685
|
-
return
|
|
13046
|
+
return failed5(400, "invalid", "The body must be JSON: one change to the permission rules.");
|
|
12686
13047
|
}
|
|
12687
13048
|
const asked = rulesChangeIn(value);
|
|
12688
|
-
if (!asked.ok) return
|
|
13049
|
+
if (!asked.ok) return failed5(400, "invalid", asked.problem);
|
|
12689
13050
|
const made = settings.changePermissionRules(
|
|
12690
13051
|
(rules) => applyRulesChange(rules, asked.change, newId)
|
|
12691
13052
|
);
|
|
12692
|
-
if (!made.ok) return
|
|
13053
|
+
if (!made.ok) return failed5(STATUS_OF[made.reason], made.reason, made.problem);
|
|
12693
13054
|
return {
|
|
12694
13055
|
status: 200,
|
|
12695
13056
|
body: { ok: true, permissionRules: made.rules }
|
|
@@ -12699,7 +13060,7 @@ function createPermissionRulesRoute(options) {
|
|
|
12699
13060
|
|
|
12700
13061
|
// src/collector/settings/timeRulesRoute.ts
|
|
12701
13062
|
var MAX_TIME_RULES_BODY_BYTES = 2048;
|
|
12702
|
-
var
|
|
13063
|
+
var failed6 = (status, reason, error) => ({
|
|
12703
13064
|
status,
|
|
12704
13065
|
body: { error, reason }
|
|
12705
13066
|
});
|
|
@@ -12723,12 +13084,12 @@ function createTimeRulesRoute(options) {
|
|
|
12723
13084
|
try {
|
|
12724
13085
|
value = JSON.parse(body.text);
|
|
12725
13086
|
} catch {
|
|
12726
|
-
return
|
|
13087
|
+
return failed6(400, "invalid", "The body must be JSON: the three time rules.");
|
|
12727
13088
|
}
|
|
12728
13089
|
const asked = timeRulesIn(value);
|
|
12729
|
-
if (!asked.ok) return
|
|
13090
|
+
if (!asked.ok) return failed6(400, "invalid", asked.problem);
|
|
12730
13091
|
const saved = settings.changeTimeRules(asked.rules);
|
|
12731
|
-
if (!saved.ok) return
|
|
13092
|
+
if (!saved.ok) return failed6(500, "not-saved", saved.problem);
|
|
12732
13093
|
return {
|
|
12733
13094
|
status: 200,
|
|
12734
13095
|
body: { ok: true, timeRules: asked.rules }
|
|
@@ -12751,8 +13112,8 @@ function parseProcessTable(stdout) {
|
|
|
12751
13112
|
}
|
|
12752
13113
|
var readProcessTableWithPs = async () => {
|
|
12753
13114
|
if (process.platform !== "darwin") return /* @__PURE__ */ new Map();
|
|
12754
|
-
const
|
|
12755
|
-
return
|
|
13115
|
+
const answer2 = await runPs(PS_TABLE_ARGS);
|
|
13116
|
+
return answer2.ok ? parseProcessTable(answer2.stdout) : /* @__PURE__ */ new Map();
|
|
12756
13117
|
};
|
|
12757
13118
|
function parentsIn(table) {
|
|
12758
13119
|
const parents = /* @__PURE__ */ new Map();
|
|
@@ -12896,7 +13257,7 @@ function runTmuxBinary(binary, args, options) {
|
|
|
12896
13257
|
binary,
|
|
12897
13258
|
[...args],
|
|
12898
13259
|
{
|
|
12899
|
-
env: options.env,
|
|
13260
|
+
env: childEnvironment(options.env),
|
|
12900
13261
|
timeout: options.timeoutMs ?? TMUX_TIMEOUT_MS,
|
|
12901
13262
|
maxBuffer: MAX_OUTPUT_BYTES5,
|
|
12902
13263
|
encoding: "utf8",
|
|
@@ -13304,6 +13665,10 @@ function createCollector(options) {
|
|
|
13304
13665
|
}),
|
|
13305
13666
|
pullRequests: () => pullRequests?.status() ?? pullRequestsOff,
|
|
13306
13667
|
jump: createJumpRoute({ poller, panes, run: tmux, tabs, osascript, now }),
|
|
13668
|
+
// Only this computer's own adapters read what a session last said. A
|
|
13669
|
+
// session on another machine is answered as not read, and nothing is
|
|
13670
|
+
// asked of that machine.
|
|
13671
|
+
lastMessage: createLastMessageRoute({ env, poller, adapters: localAdapters }),
|
|
13307
13672
|
stop: stopRoutes && createStopRoute(stopRoutes),
|
|
13308
13673
|
cleanUp: stopRoutes && createCleanUpRoute(stopRoutes),
|
|
13309
13674
|
settings: () => ({
|
|
@@ -13433,7 +13798,7 @@ async function runStandalone(options) {
|
|
|
13433
13798
|
} catch (error) {
|
|
13434
13799
|
return stopWith(error.message);
|
|
13435
13800
|
}
|
|
13436
|
-
if (!existsSync(
|
|
13801
|
+
if (!existsSync(path26.join(distDir, "index.html"))) {
|
|
13437
13802
|
return stopWith(
|
|
13438
13803
|
"The dashboard has not been built yet. Run `npm run build`, then start it again."
|
|
13439
13804
|
);
|
|
@@ -13476,12 +13841,12 @@ async function runStandalone(options) {
|
|
|
13476
13841
|
|
|
13477
13842
|
// src/cli/start/openBrowser.ts
|
|
13478
13843
|
import { spawn as spawn3 } from "node:child_process";
|
|
13479
|
-
import
|
|
13844
|
+
import path27 from "node:path";
|
|
13480
13845
|
var OPEN_WAIT_MS = 1e4;
|
|
13481
13846
|
var WINDOWS_FOLDER = "C:\\Windows";
|
|
13482
13847
|
function windowsFolder(env) {
|
|
13483
13848
|
const named = env.SystemRoot?.trim() || env.SYSTEMROOT?.trim();
|
|
13484
|
-
return named &&
|
|
13849
|
+
return named && path27.win32.isAbsolute(named) ? named : WINDOWS_FOLDER;
|
|
13485
13850
|
}
|
|
13486
13851
|
function openCommand(platform, url, env = process.env) {
|
|
13487
13852
|
const address = loopbackAddress(url);
|
|
@@ -13490,7 +13855,7 @@ function openCommand(platform, url, env = process.env) {
|
|
|
13490
13855
|
if (platform === "linux") return { file: "xdg-open", args: [address] };
|
|
13491
13856
|
if (platform === "win32") {
|
|
13492
13857
|
return {
|
|
13493
|
-
file:
|
|
13858
|
+
file: path27.win32.join(windowsFolder(env), "System32", "rundll32.exe"),
|
|
13494
13859
|
args: ["url.dll,FileProtocolHandler", address]
|
|
13495
13860
|
};
|
|
13496
13861
|
}
|
|
@@ -13504,7 +13869,12 @@ function runProgram2(file, args, waitMs = OPEN_WAIT_MS) {
|
|
|
13504
13869
|
resolve(opened);
|
|
13505
13870
|
};
|
|
13506
13871
|
try {
|
|
13507
|
-
const child = spawn3(file, [...args], {
|
|
13872
|
+
const child = spawn3(file, [...args], {
|
|
13873
|
+
env: childEnvironment(process.env),
|
|
13874
|
+
stdio: "ignore",
|
|
13875
|
+
detached: true,
|
|
13876
|
+
windowsHide: true
|
|
13877
|
+
});
|
|
13508
13878
|
child.once("error", () => settle(false));
|
|
13509
13879
|
child.once("exit", (code) => settle(code === 0));
|
|
13510
13880
|
child.unref();
|