@faithfulalabi/agent-lens 0.1.0
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/LICENSE +21 -0
- package/README.md +124 -0
- package/bin/agent-lens.js +40 -0
- package/bin/package.json +4 -0
- package/dist/src/archive/cron-log.js +41 -0
- package/dist/src/archive/discover.js +89 -0
- package/dist/src/archive/index.js +7 -0
- package/dist/src/archive/lock.js +119 -0
- package/dist/src/archive/log.js +15 -0
- package/dist/src/archive/mirror.js +366 -0
- package/dist/src/archive/paths.js +159 -0
- package/dist/src/archive/read.js +133 -0
- package/dist/src/archive/report.js +272 -0
- package/dist/src/archive/seal.js +76 -0
- package/dist/src/archive/sidecar.js +39 -0
- package/dist/src/cli/args.js +47 -0
- package/dist/src/cli/commands/archive.js +65 -0
- package/dist/src/cli/commands/doctor.js +191 -0
- package/dist/src/cli/commands/prune.js +159 -0
- package/dist/src/cli/commands/rebuild.js +98 -0
- package/dist/src/cli/commands/schedule.js +325 -0
- package/dist/src/cli/commands/start.js +83 -0
- package/dist/src/cli/commands/warm.js +96 -0
- package/dist/src/cli/index.js +102 -0
- package/dist/src/content/resolve.js +163 -0
- package/dist/src/corpus/env.js +32 -0
- package/dist/src/corpus/paths.js +70 -0
- package/dist/src/corpus/scan.js +85 -0
- package/dist/src/corpus/watch.js +189 -0
- package/dist/src/db/freshness.js +82 -0
- package/dist/src/db/open.js +74 -0
- package/dist/src/db/read.js +266 -0
- package/dist/src/db/schema.js +312 -0
- package/dist/src/db/sidecars.js +216 -0
- package/dist/src/db/spill-index.js +68 -0
- package/dist/src/db/write.js +279 -0
- package/dist/src/project/pipeline.js +307 -0
- package/dist/src/project/subagents.js +41 -0
- package/dist/src/project/tools.js +94 -0
- package/dist/src/server/api.js +249 -0
- package/dist/src/server/app.js +28 -0
- package/dist/src/server/config.js +24 -0
- package/dist/src/server/drift-report.js +35 -0
- package/dist/src/server/index.js +1 -0
- package/dist/src/server/live.js +109 -0
- package/dist/src/server/middleware/host-guard.js +42 -0
- package/dist/src/server/middleware/token-auth.js +19 -0
- package/dist/src/server/start.js +150 -0
- package/dist/src/server/static-ui.js +97 -0
- package/dist/src/server/stream.js +50 -0
- package/dist/src/server/warm.js +48 -0
- package/dist/src/shared/api.js +1 -0
- package/dist/src/shared/entities.js +1 -0
- package/dist/src/shared/index.js +2 -0
- package/dist/src/shared/pricing.js +68 -0
- package/dist/src/shared/token.js +39 -0
- package/dist/src/transcript/accessors.js +28 -0
- package/dist/src/transcript/agents.js +44 -0
- package/dist/src/transcript/blocks.js +75 -0
- package/dist/src/transcript/drift.js +42 -0
- package/dist/src/transcript/human.js +65 -0
- package/dist/src/transcript/line.js +251 -0
- package/dist/src/transcript/raw-types.js +1 -0
- package/dist/src/transcript/spill.js +122 -0
- package/dist/src/transcript/usage.js +63 -0
- package/dist/src/transcript/version.js +1 -0
- package/package.json +70 -0
- package/ui/dist/assets/index-CKKoKUCq.js +254 -0
- package/ui/dist/assets/index-Chza4fL6.css +1 -0
- package/ui/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/ui/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/ui/dist/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/ui/dist/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- package/ui/dist/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/ui/dist/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
- package/ui/dist/index.html +21 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { serve } from '@hono/node-server';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { mkdirSync } from 'node:fs';
|
|
5
|
+
import { readOrCreateToken } from '../shared/index.js';
|
|
6
|
+
import { resolveArchiveRoot, resolveTranscriptRoot } from '../archive/index.js';
|
|
7
|
+
import { createArchiveReader } from '../archive/read.js';
|
|
8
|
+
import { createContentResolver, createContentEnv } from '../content/resolve.js';
|
|
9
|
+
import { createCorpusSweep } from '../corpus/watch.js';
|
|
10
|
+
import { createProjectionEnv } from '../corpus/env.js';
|
|
11
|
+
import { openDb } from '../db/open.js';
|
|
12
|
+
import { readEventArchivePath } from '../db/read.js';
|
|
13
|
+
import { buildApiApp } from './app.js';
|
|
14
|
+
import { clearConfig, writeConfig } from './config.js';
|
|
15
|
+
import { startLiveTick } from './live.js';
|
|
16
|
+
import { createStreamHub } from './stream.js';
|
|
17
|
+
import { createWarmQueue } from './warm.js';
|
|
18
|
+
const DEFAULT_PORT = 4470;
|
|
19
|
+
const MAX_PORT_TRIES = 20;
|
|
20
|
+
const DEFAULT_HOSTNAME = '127.0.0.1';
|
|
21
|
+
const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1', '[::1]']);
|
|
22
|
+
function resolveDataDir(dataDir) {
|
|
23
|
+
return dataDir ?? process.env.AGENT_LENS_DIR ?? join(homedir(), '.agent-lens');
|
|
24
|
+
}
|
|
25
|
+
function warnNetworkExposure(host, port) {
|
|
26
|
+
console.warn([
|
|
27
|
+
'',
|
|
28
|
+
' ############################################################',
|
|
29
|
+
' # WARNING: agent-lens is binding to a non-loopback host. #',
|
|
30
|
+
` # Listening on ${host}:${port} — reachable over the network.`,
|
|
31
|
+
' # Your traces contain source code and secrets. The token #',
|
|
32
|
+
' # is the ONLY thing guarding them. Do this on trusted LANs #',
|
|
33
|
+
' # only, and never on a public/untrusted network. #',
|
|
34
|
+
' ############################################################',
|
|
35
|
+
'',
|
|
36
|
+
].join('\n'));
|
|
37
|
+
}
|
|
38
|
+
function bind(fetch, host, port) {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const server = serve({ fetch, hostname: host, port }, () => {
|
|
41
|
+
resolve(server);
|
|
42
|
+
});
|
|
43
|
+
server.on('error', reject);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
export async function startServer(options = {}) {
|
|
47
|
+
const dataDir = resolveDataDir(options.dataDir);
|
|
48
|
+
mkdirSync(dataDir, { recursive: true });
|
|
49
|
+
const host = options.host ?? DEFAULT_HOSTNAME;
|
|
50
|
+
const token = readOrCreateToken(dataDir);
|
|
51
|
+
const opened = openDb({ dataDir, port: options.port });
|
|
52
|
+
const db = opened.db;
|
|
53
|
+
const reader = createArchiveReader();
|
|
54
|
+
const roots = {
|
|
55
|
+
archiveRoot: resolveArchiveRoot(dataDir),
|
|
56
|
+
transcriptRoot: resolveTranscriptRoot(options.transcriptRoot),
|
|
57
|
+
};
|
|
58
|
+
const sweep = createCorpusSweep({
|
|
59
|
+
db,
|
|
60
|
+
dataDir,
|
|
61
|
+
transcriptRoot: options.transcriptRoot,
|
|
62
|
+
reader,
|
|
63
|
+
...(options.sweepIntervalMs !== undefined && { intervalMs: options.sweepIntervalMs }),
|
|
64
|
+
});
|
|
65
|
+
const hub = createStreamHub();
|
|
66
|
+
const warm = createWarmQueue({ db, env: createProjectionEnv(reader, roots), hub });
|
|
67
|
+
const app = buildApiApp({
|
|
68
|
+
db,
|
|
69
|
+
token,
|
|
70
|
+
host,
|
|
71
|
+
uiDir: options.uiDir,
|
|
72
|
+
env: createProjectionEnv(reader, roots),
|
|
73
|
+
sweep,
|
|
74
|
+
hub,
|
|
75
|
+
warm,
|
|
76
|
+
resolveContent: createContentResolver((id) => readEventArchivePath(db, id)?.archive_path, createContentEnv(reader, [roots.archiveRoot, roots.transcriptRoot])),
|
|
77
|
+
});
|
|
78
|
+
const fetch = app.fetch;
|
|
79
|
+
const explicit = options.port !== undefined;
|
|
80
|
+
const startPort = explicit ? options.port : DEFAULT_PORT;
|
|
81
|
+
let server;
|
|
82
|
+
let boundPort = startPort;
|
|
83
|
+
for (let attempt = 0; attempt < (explicit ? 1 : MAX_PORT_TRIES); attempt++) {
|
|
84
|
+
const candidate = startPort + attempt;
|
|
85
|
+
try {
|
|
86
|
+
server = await bind(fetch, host, candidate);
|
|
87
|
+
boundPort = candidate;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
if (!explicit &&
|
|
92
|
+
err.code === 'EADDRINUSE' &&
|
|
93
|
+
attempt < MAX_PORT_TRIES - 1) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
opened.close();
|
|
97
|
+
throw err;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (server === undefined) {
|
|
101
|
+
opened.close();
|
|
102
|
+
throw new Error(`unable to bind a port starting at ${startPort}`);
|
|
103
|
+
}
|
|
104
|
+
if (startPort === 0) {
|
|
105
|
+
const address = server.address();
|
|
106
|
+
if (address !== null && typeof address === 'object') {
|
|
107
|
+
boundPort = address.port;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
let tick;
|
|
111
|
+
if (options.sweepIntervalMs !== 0) {
|
|
112
|
+
sweep.tick();
|
|
113
|
+
tick = startLiveTick({
|
|
114
|
+
db,
|
|
115
|
+
env: createProjectionEnv(reader, roots),
|
|
116
|
+
sweep,
|
|
117
|
+
hub,
|
|
118
|
+
...(options.sweepIntervalMs !== undefined && { intervalMs: options.sweepIntervalMs }),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
writeConfig(dataDir, {
|
|
122
|
+
port: boundPort,
|
|
123
|
+
pid: process.pid,
|
|
124
|
+
started_at: new Date().toISOString(),
|
|
125
|
+
});
|
|
126
|
+
if (!explicit && boundPort !== DEFAULT_PORT) {
|
|
127
|
+
console.log(`agent-lens: port ${DEFAULT_PORT} in use, listening on ${boundPort}`);
|
|
128
|
+
}
|
|
129
|
+
if (!LOOPBACK_HOSTS.has(host)) {
|
|
130
|
+
warnNetworkExposure(host, boundPort);
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
port: boundPort,
|
|
134
|
+
close: async () => {
|
|
135
|
+
tick?.close();
|
|
136
|
+
warm.close();
|
|
137
|
+
sweep.close();
|
|
138
|
+
await hub.drain();
|
|
139
|
+
return new Promise((resolve) => {
|
|
140
|
+
if ('closeAllConnections' in server)
|
|
141
|
+
server.closeAllConnections();
|
|
142
|
+
server.close(() => {
|
|
143
|
+
opened.close();
|
|
144
|
+
clearConfig(dataDir);
|
|
145
|
+
resolve();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { serveStatic } from '@hono/node-server/serve-static';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { TOKEN_HEADER } from '../shared/index.js';
|
|
7
|
+
export const BOOTSTRAP_MARKER = '<!--agent-lens-bootstrap-->';
|
|
8
|
+
const IMMUTABLE_CACHE_CONTROL = 'public, max-age=31536000, immutable';
|
|
9
|
+
export function resolveUiDir(fromDir) {
|
|
10
|
+
const start = fromDir ?? dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
let dir = start;
|
|
12
|
+
for (;;) {
|
|
13
|
+
if (existsSync(join(dir, 'package.json')))
|
|
14
|
+
return join(dir, 'ui', 'dist');
|
|
15
|
+
const parent = dirname(dir);
|
|
16
|
+
if (parent === dir) {
|
|
17
|
+
throw new Error(`agent-lens: no package.json above ${start}; cannot locate ui/dist`);
|
|
18
|
+
}
|
|
19
|
+
dir = parent;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function jsLiteral(value) {
|
|
23
|
+
return JSON.stringify(value).replace(/</g, '\\u003C');
|
|
24
|
+
}
|
|
25
|
+
function hasBootstrapMarker(html) {
|
|
26
|
+
return html.includes(BOOTSTRAP_MARKER);
|
|
27
|
+
}
|
|
28
|
+
export function injectToken(html, token) {
|
|
29
|
+
if (!hasBootstrapMarker(html)) {
|
|
30
|
+
throw new Error(`agent-lens: ${BOOTSTRAP_MARKER} not found in index.html — the UI build is missing or stale`);
|
|
31
|
+
}
|
|
32
|
+
const script = `<script>window.__AGENT_LENS__=Object.freeze(` +
|
|
33
|
+
`{token:${jsLiteral(token)},tokenHeader:${jsLiteral(TOKEN_HEADER)}})</script>`;
|
|
34
|
+
return html.replace(BOOTSTRAP_MARKER, () => script);
|
|
35
|
+
}
|
|
36
|
+
export function renderNotBuilt() {
|
|
37
|
+
return `<!doctype html>
|
|
38
|
+
<html lang="en">
|
|
39
|
+
<head>
|
|
40
|
+
<meta charset="utf-8" />
|
|
41
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
42
|
+
<meta name="color-scheme" content="dark" />
|
|
43
|
+
<title>agent-lens — UI not built</title>
|
|
44
|
+
<style>
|
|
45
|
+
body { background: #0b0b0e; color: #e6e6ea; font: 15px/1.6 ui-sans-serif, system-ui, sans-serif;
|
|
46
|
+
margin: 0; display: grid; place-items: center; min-height: 100vh; }
|
|
47
|
+
main { max-width: 34rem; padding: 2rem; }
|
|
48
|
+
h1 { font-size: 1.25rem; margin: 0 0 .75rem; }
|
|
49
|
+
code { background: #1a1a20; border-radius: 4px; padding: .15rem .4rem;
|
|
50
|
+
font-family: ui-monospace, SFMono-Regular, monospace; }
|
|
51
|
+
p { margin: 0 0 .75rem; color: #a1a1ad; }
|
|
52
|
+
</style>
|
|
53
|
+
</head>
|
|
54
|
+
<body>
|
|
55
|
+
<main>
|
|
56
|
+
<h1>The agent-lens UI is not built</h1>
|
|
57
|
+
<p>The UI bundle is missing or stale. Build it, then reload this page:</p>
|
|
58
|
+
<p><code>npm run build</code></p>
|
|
59
|
+
<p>The collector itself is running — the API under <code>/api/</code> is
|
|
60
|
+
serving normally, so nothing that was captured has been lost.</p>
|
|
61
|
+
</main>
|
|
62
|
+
</body>
|
|
63
|
+
</html>`;
|
|
64
|
+
}
|
|
65
|
+
async function readIndexHtml(uiDir) {
|
|
66
|
+
let html;
|
|
67
|
+
try {
|
|
68
|
+
html = await readFile(join(uiDir, 'index.html'), 'utf8');
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
if (err.code === 'ENOENT')
|
|
72
|
+
return undefined;
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
return hasBootstrapMarker(html) ? html : undefined;
|
|
76
|
+
}
|
|
77
|
+
export function makeServeIndex(options) {
|
|
78
|
+
return async (c) => {
|
|
79
|
+
const html = await readIndexHtml(options.uiDir ?? resolveUiDir());
|
|
80
|
+
c.header('Cache-Control', 'no-store');
|
|
81
|
+
return c.html(html === undefined ? renderNotBuilt() : injectToken(html, options.token));
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export function registerUi(app, options) {
|
|
85
|
+
app.use('/assets/*', async (c, next) => {
|
|
86
|
+
await next();
|
|
87
|
+
if (c.res.status === 200)
|
|
88
|
+
c.header('Cache-Control', IMMUTABLE_CACHE_CONTROL);
|
|
89
|
+
});
|
|
90
|
+
let assets;
|
|
91
|
+
app.use('/assets/*', (c, next) => {
|
|
92
|
+
assets ??= serveStatic({ root: options.uiDir ?? resolveUiDir() });
|
|
93
|
+
return assets(c, next);
|
|
94
|
+
});
|
|
95
|
+
app.all('/assets/*', (c) => c.text('not found', 404));
|
|
96
|
+
app.get('*', makeServeIndex(options));
|
|
97
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export const STREAM_EVENTS = [
|
|
2
|
+
'session_changed',
|
|
3
|
+
'session_indexed',
|
|
4
|
+
'warm_progress',
|
|
5
|
+
'heartbeat',
|
|
6
|
+
];
|
|
7
|
+
export const HEARTBEAT_MS = 15_000;
|
|
8
|
+
export function createStreamHub(options = {}) {
|
|
9
|
+
const heartbeatMs = options.heartbeatMs ?? HEARTBEAT_MS;
|
|
10
|
+
const clients = new Map();
|
|
11
|
+
const release = (stream) => {
|
|
12
|
+
const client = clients.get(stream);
|
|
13
|
+
if (client === undefined)
|
|
14
|
+
return;
|
|
15
|
+
clients.delete(stream);
|
|
16
|
+
client.unpark();
|
|
17
|
+
};
|
|
18
|
+
const attach = (stream) => new Promise((unpark) => {
|
|
19
|
+
clients.set(stream, { stream, unpark });
|
|
20
|
+
stream.onAbort(() => release(stream));
|
|
21
|
+
});
|
|
22
|
+
const fanOut = async (event, data, id) => {
|
|
23
|
+
const message = { event, data, ...(id !== undefined && { id }) };
|
|
24
|
+
await Promise.all([...clients.values()].map(async ({ stream }) => {
|
|
25
|
+
if (stream.aborted || stream.closed) {
|
|
26
|
+
release(stream);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
await stream.writeSSE(message);
|
|
30
|
+
}));
|
|
31
|
+
};
|
|
32
|
+
const beat = () => fanOut('heartbeat', '');
|
|
33
|
+
const timer = setInterval(() => void beat(), heartbeatMs);
|
|
34
|
+
timer.unref?.();
|
|
35
|
+
return {
|
|
36
|
+
attach,
|
|
37
|
+
publish: (event, data, id) => fanOut(event, JSON.stringify(data), id),
|
|
38
|
+
beat,
|
|
39
|
+
size: () => clients.size,
|
|
40
|
+
drain: async () => {
|
|
41
|
+
clearInterval(timer);
|
|
42
|
+
const attached = [...clients.values()];
|
|
43
|
+
clients.clear();
|
|
44
|
+
await Promise.all(attached.map(async ({ stream, unpark }) => {
|
|
45
|
+
unpark();
|
|
46
|
+
await stream.close();
|
|
47
|
+
}));
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ensureProjectedFold } from '../db/freshness.js';
|
|
2
|
+
import { readWarmableIds } from '../db/read.js';
|
|
3
|
+
const yieldToLoop = () => new Promise((resolve) => setImmediate(resolve));
|
|
4
|
+
export function createWarmQueue(options) {
|
|
5
|
+
const { db, env, hub } = options;
|
|
6
|
+
const yieldTo = options.yieldTo ?? yieldToLoop;
|
|
7
|
+
let inFlight = false;
|
|
8
|
+
let closed = false;
|
|
9
|
+
const drain = async (ids) => {
|
|
10
|
+
const total = ids.length;
|
|
11
|
+
let done = 0;
|
|
12
|
+
for (const id of ids) {
|
|
13
|
+
await yieldTo();
|
|
14
|
+
if (closed)
|
|
15
|
+
return;
|
|
16
|
+
try {
|
|
17
|
+
ensureProjectedFold(db, id, env);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
console.error(error);
|
|
21
|
+
}
|
|
22
|
+
done += 1;
|
|
23
|
+
const frame = { done, total };
|
|
24
|
+
await hub.publish('warm_progress', frame);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
start: () => {
|
|
29
|
+
if (closed)
|
|
30
|
+
return 0;
|
|
31
|
+
const ids = readWarmableIds(db);
|
|
32
|
+
if (inFlight)
|
|
33
|
+
return ids.length;
|
|
34
|
+
if (ids.length === 0)
|
|
35
|
+
return 0;
|
|
36
|
+
inFlight = true;
|
|
37
|
+
void drain(ids)
|
|
38
|
+
.catch((error) => console.error(error))
|
|
39
|
+
.finally(() => {
|
|
40
|
+
inFlight = false;
|
|
41
|
+
});
|
|
42
|
+
return ids.length;
|
|
43
|
+
},
|
|
44
|
+
close: () => {
|
|
45
|
+
closed = true;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
function canonicalJson(value) {
|
|
3
|
+
return JSON.stringify(sortKeys(value));
|
|
4
|
+
}
|
|
5
|
+
function sortKeys(value) {
|
|
6
|
+
if (Array.isArray(value))
|
|
7
|
+
return value.map(sortKeys);
|
|
8
|
+
if (value !== null && typeof value === 'object') {
|
|
9
|
+
const source = value;
|
|
10
|
+
const sorted = {};
|
|
11
|
+
for (const key of Object.keys(source).sort())
|
|
12
|
+
sorted[key] = sortKeys(source[key]);
|
|
13
|
+
return sorted;
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
export const PRICING_TABLE = Object.freeze({
|
|
18
|
+
'claude-fable-5-1': { input: 10, output: 50, cache_read: 0.25, cache_write: 12.5 },
|
|
19
|
+
'claude-mythos-5-1': { input: 10, output: 50, cache_read: 0.25, cache_write: 12.5 },
|
|
20
|
+
'claude-fable-5': { input: 10, output: 50, cache_read: 1, cache_write: 12.5 },
|
|
21
|
+
'claude-mythos-5': { input: 10, output: 50, cache_read: 1, cache_write: 12.5 },
|
|
22
|
+
'claude-opus-5-5': { input: 4, output: 20, cache_read: 0.2, cache_write: 5 },
|
|
23
|
+
'claude-opus-5': { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 },
|
|
24
|
+
'claude-opus-4-8': { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 },
|
|
25
|
+
'claude-opus-4-7': { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 },
|
|
26
|
+
'claude-opus-4-6': { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 },
|
|
27
|
+
'claude-opus-4-5': { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 },
|
|
28
|
+
'claude-opus-4-1': { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 },
|
|
29
|
+
'claude-opus-4': { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 },
|
|
30
|
+
'claude-sonnet-5': { input: 2, output: 10, cache_read: 0.2, cache_write: 2.5 },
|
|
31
|
+
'claude-sonnet-4-6': { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 },
|
|
32
|
+
'claude-sonnet-4-5': { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 },
|
|
33
|
+
'claude-sonnet-4': { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 },
|
|
34
|
+
'claude-haiku-4-5': { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 },
|
|
35
|
+
'claude-3-5-haiku': { input: 0.8, output: 4, cache_read: 0.08, cache_write: 1 },
|
|
36
|
+
});
|
|
37
|
+
export const PRICING_TABLE_DATE = '2026-09-22';
|
|
38
|
+
export const PRICING_VERSION = `${PRICING_TABLE_DATE}+${createHash('sha256')
|
|
39
|
+
.update(canonicalJson(PRICING_TABLE), 'utf8')
|
|
40
|
+
.digest('hex')
|
|
41
|
+
.slice(0, 8)}`;
|
|
42
|
+
const BUILD_SUFFIX = /-\d{8}$/;
|
|
43
|
+
const CONTEXT_TAG = /\[[^\]]*\]$/;
|
|
44
|
+
const VENDOR_PREFIX = /^(?:[a-z]{2,4}\.)?anthropic\./;
|
|
45
|
+
export function normalizeModelKey(raw) {
|
|
46
|
+
const model = raw.replace(CONTEXT_TAG, '');
|
|
47
|
+
const withoutVendor = model.replace(VENDOR_PREFIX, '');
|
|
48
|
+
const candidates = [
|
|
49
|
+
model,
|
|
50
|
+
model.replace(BUILD_SUFFIX, ''),
|
|
51
|
+
withoutVendor,
|
|
52
|
+
withoutVendor.replace(BUILD_SUFFIX, ''),
|
|
53
|
+
];
|
|
54
|
+
return candidates.find((key) => key !== '' && key in PRICING_TABLE);
|
|
55
|
+
}
|
|
56
|
+
export function estimateCost(model, usage) {
|
|
57
|
+
if (model === undefined || model === '')
|
|
58
|
+
return null;
|
|
59
|
+
const key = normalizeModelKey(model);
|
|
60
|
+
if (key === undefined)
|
|
61
|
+
return null;
|
|
62
|
+
const price = PRICING_TABLE[key];
|
|
63
|
+
const millionths = (usage.tokens_in ?? 0) * price.input +
|
|
64
|
+
(usage.tokens_out ?? 0) * price.output +
|
|
65
|
+
(usage.cache_read ?? 0) * price.cache_read +
|
|
66
|
+
(usage.cache_write ?? 0) * price.cache_write;
|
|
67
|
+
return millionths / 1_000_000;
|
|
68
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
export const TOKEN_HEADER = 'x-agentlens-token';
|
|
6
|
+
const TOKEN_FILE = 'token';
|
|
7
|
+
function resolveDir(dir) {
|
|
8
|
+
return dir ?? process.env.AGENT_LENS_DIR ?? join(homedir(), '.agent-lens');
|
|
9
|
+
}
|
|
10
|
+
function generateToken() {
|
|
11
|
+
return randomBytes(32).toString('base64url');
|
|
12
|
+
}
|
|
13
|
+
export function readToken(dir) {
|
|
14
|
+
const tokenPath = join(resolveDir(dir), TOKEN_FILE);
|
|
15
|
+
try {
|
|
16
|
+
return readFileSync(tokenPath, 'utf8');
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
if (err.code === 'ENOENT') {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
throw err;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function readOrCreateToken(dir) {
|
|
26
|
+
const existing = readToken(dir);
|
|
27
|
+
if (existing !== null) {
|
|
28
|
+
return existing;
|
|
29
|
+
}
|
|
30
|
+
const targetDir = resolveDir(dir);
|
|
31
|
+
mkdirSync(targetDir, { recursive: true });
|
|
32
|
+
const token = generateToken();
|
|
33
|
+
const tokenPath = join(targetDir, TOKEN_FILE);
|
|
34
|
+
const tempPath = join(targetDir, `${TOKEN_FILE}.tmp.${process.pid}`);
|
|
35
|
+
writeFileSync(tempPath, token, { mode: 0o600 });
|
|
36
|
+
chmodSync(tempPath, 0o600);
|
|
37
|
+
renameSync(tempPath, tokenPath);
|
|
38
|
+
return token;
|
|
39
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function str(value, fallback) {
|
|
2
|
+
return typeof value === 'string' ? value : fallback;
|
|
3
|
+
}
|
|
4
|
+
export function num(value, fallback) {
|
|
5
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
6
|
+
}
|
|
7
|
+
export function arr(value, fallback) {
|
|
8
|
+
try {
|
|
9
|
+
return Array.isArray(value) ? value : fallback;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return fallback;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function obj(value, fallback) {
|
|
16
|
+
try {
|
|
17
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
18
|
+
return fallback;
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return fallback;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const ISO_8601 = /^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d{1,9})?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/;
|
|
26
|
+
export function isoTs(value, fallback) {
|
|
27
|
+
return typeof value === 'string' && ISO_8601.test(value) ? value : fallback;
|
|
28
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { num, obj, str } from './accessors.js';
|
|
2
|
+
const PERSISTED_MARKER = '<persisted-output>';
|
|
3
|
+
const ASYNC_LAUNCH_MARKER = 'Async agent launched successfully.';
|
|
4
|
+
const NOTIFICATION_TAG = '<task-notification>';
|
|
5
|
+
const MARKER_AGENT_ID = /^agentId: (\S+)/m;
|
|
6
|
+
function tag(name) {
|
|
7
|
+
return new RegExp(`<${name}>([\\s\\S]*?)</${name}>`);
|
|
8
|
+
}
|
|
9
|
+
const TOOL_CALL_ID = tag('tool-use-id');
|
|
10
|
+
const STATUS = tag('status');
|
|
11
|
+
const RESULT = tag('result');
|
|
12
|
+
export function asyncAgentLaunch(line, text) {
|
|
13
|
+
const structured = obj(line.raw.toolUseResult, undefined);
|
|
14
|
+
if (structured?.isAsync === true) {
|
|
15
|
+
return { agentId: str(structured.agentId, undefined) };
|
|
16
|
+
}
|
|
17
|
+
if (!text.startsWith(ASYNC_LAUNCH_MARKER))
|
|
18
|
+
return undefined;
|
|
19
|
+
return { agentId: MARKER_AGENT_ID.exec(text)?.[1] };
|
|
20
|
+
}
|
|
21
|
+
export function taskNotification(text) {
|
|
22
|
+
if (!text.startsWith(NOTIFICATION_TAG))
|
|
23
|
+
return undefined;
|
|
24
|
+
return {
|
|
25
|
+
toolCallId: TOOL_CALL_ID.exec(text)?.[1],
|
|
26
|
+
status: STATUS.exec(text)?.[1],
|
|
27
|
+
result: RESULT.exec(text)?.[1],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function parseAgentMeta(json) {
|
|
31
|
+
const meta = obj(json, undefined);
|
|
32
|
+
if (meta === undefined)
|
|
33
|
+
return undefined;
|
|
34
|
+
return {
|
|
35
|
+
agentType: str(meta.agentType, undefined),
|
|
36
|
+
description: str(meta.description, undefined),
|
|
37
|
+
toolUseId: str(meta.toolUseId, undefined),
|
|
38
|
+
spawnDepth: num(meta.spawnDepth, undefined),
|
|
39
|
+
parentAgentId: str(meta.parentAgentId, undefined),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function claimsPersistedOutput(text) {
|
|
43
|
+
return text.startsWith(PERSISTED_MARKER);
|
|
44
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { arr, obj, str } from './accessors.js';
|
|
2
|
+
export const THINKING_ELIDED_MARKER = 'reasoning not recorded (signature only)';
|
|
3
|
+
const EMPTY_RECORD = Object.freeze({});
|
|
4
|
+
const NO_BLOCKS = Object.freeze([]);
|
|
5
|
+
const BYTES_PER_KB = 1000;
|
|
6
|
+
const BYTES_PER_MB = 1_000_000;
|
|
7
|
+
export function normalizeContent(content) {
|
|
8
|
+
if (typeof content === 'string')
|
|
9
|
+
return [{ type: 'text', text: content }];
|
|
10
|
+
return arr(content, NO_BLOCKS);
|
|
11
|
+
}
|
|
12
|
+
function toolReferenceBlock(raw) {
|
|
13
|
+
return { kind: 'tool_reference', name: str(raw.tool_name, '') };
|
|
14
|
+
}
|
|
15
|
+
function imagePlaceholder(mediaType, byteLength) {
|
|
16
|
+
const size = byteLength < BYTES_PER_MB
|
|
17
|
+
? `${(byteLength / BYTES_PER_KB).toFixed(1)} KB`
|
|
18
|
+
: `${(byteLength / BYTES_PER_MB).toFixed(1)} MB`;
|
|
19
|
+
return `[image ${mediaType}, ${size}]`;
|
|
20
|
+
}
|
|
21
|
+
function imageBlock(raw) {
|
|
22
|
+
const source = obj(raw.source, EMPTY_RECORD);
|
|
23
|
+
const media_type = str(source.media_type, '');
|
|
24
|
+
const byte_length = (str(source.data, '').length * 3) / 4;
|
|
25
|
+
return {
|
|
26
|
+
kind: 'image',
|
|
27
|
+
media_type,
|
|
28
|
+
byte_length,
|
|
29
|
+
placeholder: imagePlaceholder(media_type, byte_length),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function classifyBlock(raw, position) {
|
|
33
|
+
const block = obj(raw, EMPTY_RECORD);
|
|
34
|
+
const type = str(block.type, '');
|
|
35
|
+
switch (type) {
|
|
36
|
+
case 'text':
|
|
37
|
+
return { kind: 'text', text: str(block.text, '') };
|
|
38
|
+
case 'thinking': {
|
|
39
|
+
const text = str(block.thinking, '');
|
|
40
|
+
return text === ''
|
|
41
|
+
? { kind: 'thinking_elided', text: THINKING_ELIDED_MARKER }
|
|
42
|
+
: { kind: 'thinking', text };
|
|
43
|
+
}
|
|
44
|
+
case 'tool_use':
|
|
45
|
+
return {
|
|
46
|
+
kind: 'tool_use',
|
|
47
|
+
id: str(block.id, ''),
|
|
48
|
+
name: str(block.name, ''),
|
|
49
|
+
input: obj(block.input, undefined),
|
|
50
|
+
};
|
|
51
|
+
case 'tool_result':
|
|
52
|
+
if (position === 'top') {
|
|
53
|
+
return {
|
|
54
|
+
kind: 'tool_result',
|
|
55
|
+
tool_call_id: str(block.tool_use_id, ''),
|
|
56
|
+
is_error: block.is_error === true,
|
|
57
|
+
children: normalizeContent(block.content).map((child) => classifyBlock(child, 'in_tool_result')),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
case 'image':
|
|
62
|
+
return imageBlock(block);
|
|
63
|
+
case 'tool_reference':
|
|
64
|
+
if (position === 'in_tool_result')
|
|
65
|
+
return toolReferenceBlock(block);
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
return { kind: 'unknown_block', raw_type: type };
|
|
69
|
+
}
|
|
70
|
+
export function classifyContent(content) {
|
|
71
|
+
return normalizeContent(content).map((block) => classifyBlock(block, 'top'));
|
|
72
|
+
}
|
|
73
|
+
export function contentBlocks(line) {
|
|
74
|
+
return classifyContent(obj(line.raw.message, undefined)?.content);
|
|
75
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
function bucket(counts) {
|
|
2
|
+
if (counts.size === 0)
|
|
3
|
+
return undefined;
|
|
4
|
+
return Object.fromEntries([...counts.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
|
|
5
|
+
}
|
|
6
|
+
function bump(counts, key) {
|
|
7
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
8
|
+
}
|
|
9
|
+
export class DriftCounter {
|
|
10
|
+
lineTypes = new Map();
|
|
11
|
+
fields = new Map();
|
|
12
|
+
blockTypes = new Map();
|
|
13
|
+
unjoinedToolUses = 0;
|
|
14
|
+
sidecarAgentIdMismatches = 0;
|
|
15
|
+
noteLine(line, knownFields) {
|
|
16
|
+
for (const name of Object.keys(line)) {
|
|
17
|
+
if (!knownFields.has(name))
|
|
18
|
+
bump(this.fields, name);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
noteUnknownType(rawType) {
|
|
22
|
+
bump(this.lineTypes, rawType);
|
|
23
|
+
}
|
|
24
|
+
noteUnknownBlock(rawType) {
|
|
25
|
+
bump(this.blockTypes, rawType);
|
|
26
|
+
}
|
|
27
|
+
noteUnjoinedToolUse() {
|
|
28
|
+
this.unjoinedToolUses += 1;
|
|
29
|
+
}
|
|
30
|
+
noteSidecarMismatch() {
|
|
31
|
+
this.sidecarAgentIdMismatches += 1;
|
|
32
|
+
}
|
|
33
|
+
serialize() {
|
|
34
|
+
return JSON.stringify({
|
|
35
|
+
sidecar_agent_id_mismatch: this.sidecarAgentIdMismatches === 0 ? undefined : this.sidecarAgentIdMismatches,
|
|
36
|
+
unjoined_tool_uses: this.unjoinedToolUses === 0 ? undefined : this.unjoinedToolUses,
|
|
37
|
+
unknown_block_types: bucket(this.blockTypes),
|
|
38
|
+
unknown_line_types: bucket(this.lineTypes),
|
|
39
|
+
unknown_top_level_fields: bucket(this.fields),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|