@guenbankhai/tools 0.0.0-stage → 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/README.md +53 -2
- package/README.vi.md +54 -0
- package/agentmoni/lib/agents/claude/billing.js +13 -0
- package/agentmoni/lib/agents/claude/home.js +4 -0
- package/agentmoni/lib/agents/claude/index.js +181 -0
- package/agentmoni/lib/agents/claude/sources/oauth-usage.js +74 -0
- package/agentmoni/lib/agents/claude/timeline.js +26 -0
- package/agentmoni/lib/agents/codex/billing.js +9 -0
- package/agentmoni/lib/agents/codex/index.js +280 -0
- package/agentmoni/lib/agents/codex/sources/rollout.js +26 -0
- package/agentmoni/lib/agents/codex/timeline.js +22 -0
- package/agentmoni/lib/agents/index.js +13 -0
- package/agentmoni/lib/catalog.js +96 -0
- package/agentmoni/lib/catalog.json +493 -0
- package/agentmoni/lib/collect.js +11 -0
- package/agentmoni/lib/colors.js +35 -0
- package/agentmoni/lib/cost.js +87 -0
- package/agentmoni/lib/format.js +18 -0
- package/agentmoni/lib/graph.js +106 -0
- package/agentmoni/lib/history.js +50 -0
- package/agentmoni/lib/json-fields.js +58 -0
- package/agentmoni/lib/jsonl.js +43 -0
- package/agentmoni/lib/launch.js +118 -0
- package/agentmoni/lib/limits.js +11 -0
- package/agentmoni/lib/links.js +139 -0
- package/agentmoni/lib/list.js +71 -0
- package/agentmoni/lib/log.js +226 -0
- package/agentmoni/lib/monitor.js +58 -0
- package/agentmoni/lib/page.html +591 -0
- package/agentmoni/lib/present.js +184 -0
- package/agentmoni/lib/procs.js +22 -0
- package/agentmoni/lib/quota.js +34 -0
- package/agentmoni/lib/repokey.js +44 -0
- package/agentmoni/lib/share.js +106 -0
- package/agentmoni/lib/state.js +62 -0
- package/agentmoni/lib/terminal.js +143 -0
- package/agentmoni/lib/timeline.js +40 -0
- package/agentmoni/lib/web.js +75 -0
- package/agentmoni/monitor.js +98 -0
- package/package.json +22 -4
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Web server: GET / (page), GET /events (SSE of the presenter view model), GET /timeline?key= (transcript timeline).
|
|
2
|
+
// Listens on 127.0.0.1 only and refuses any other Host header (DNS rebinding).
|
|
3
|
+
const http = require('node:http');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { readTimeline } = require('./timeline.js');
|
|
7
|
+
|
|
8
|
+
const PORT = 4777, HOST = '127.0.0.1';
|
|
9
|
+
const MAX_BUFFER = 1024 * 1024; // a tab that stops reading is dropped instead of buffering forever
|
|
10
|
+
|
|
11
|
+
function renderPage() {
|
|
12
|
+
const read = f => fs.readFileSync(path.join(__dirname, f), 'utf8');
|
|
13
|
+
// The page is one self-contained file: the layout module and the number formats are inlined (no CDN, no extra requests).
|
|
14
|
+
return read('page.html').replace('/*GRAPH*/', () => read('graph.js')).replace('/*HISTORY*/', () => read('history.js')).replace('/*FORMAT*/', () => read('format.js')).replace('/*COLORS*/', () => 'globalThis.AgentRegistry = ' + JSON.stringify(require('./agents/index.js').map(({ id, name, hue, models, windows, modelPrefix, efforts }) => ({ id, name, hue, models, windows, modelPrefix, efforts }))) + ';' + read('colors.js'));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Port busy -> any free port.
|
|
18
|
+
function listen(server, port, host) {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const failed = e => {
|
|
21
|
+
if (e.code === 'EADDRINUSE' && port !== 0) { port = 0; server.once('error', failed); server.listen(0, host); } else reject(e);
|
|
22
|
+
};
|
|
23
|
+
server.once('error', failed);
|
|
24
|
+
server.once('listening', () => { server.removeListener('error', failed); resolve(server.address()); });
|
|
25
|
+
server.listen(port, host);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// -> { url, close, publish(view, files, treeFor) }. files: Map "provider:id" -> transcript path; only these paths can ever be read.
|
|
30
|
+
async function startWeb({ port = PORT, host = HOST, readTimeline: read = readTimeline } = {}) {
|
|
31
|
+
const page = renderPage(), clients = new Set();
|
|
32
|
+
let treeFor = null;
|
|
33
|
+
let packet = null, files = new Map();
|
|
34
|
+
const server = http.createServer((req, res) => {
|
|
35
|
+
const { port: p } = server.address();
|
|
36
|
+
if (![`${host}:${p}`, `localhost:${p}`].includes(req.headers.host)) { res.writeHead(403); res.end(); return; }
|
|
37
|
+
const url = new URL(req.url, 'http://x'); // query params (e.g. VS Code Simple Browser's) are ignored on /
|
|
38
|
+
if (req.method !== 'GET') { res.writeHead(405); res.end(); return; }
|
|
39
|
+
if (url.pathname === '/') {
|
|
40
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff',
|
|
41
|
+
'Content-Security-Policy': "default-src 'none'; base-uri 'none'; form-action 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'" });
|
|
42
|
+
res.end(page);
|
|
43
|
+
} else if (url.pathname === '/events') {
|
|
44
|
+
res.writeHead(200, { 'Content-Type': 'text/event-stream; charset=utf-8', 'Cache-Control': 'no-cache', Connection: 'keep-alive' });
|
|
45
|
+
res.write('retry: 2000\n\n');
|
|
46
|
+
if (packet) res.write(packet);
|
|
47
|
+
clients.add(res);
|
|
48
|
+
res.on('close', () => clients.delete(res));
|
|
49
|
+
} else if (url.pathname === '/tree') {
|
|
50
|
+
const keys = url.searchParams.getAll('key'), k = keys[0];
|
|
51
|
+
const valid = keys.length === 1 && [...url.searchParams.keys()].every(x => x === 'key') && /^[a-zA-Z0-9_-]+:[a-zA-Z0-9_-]+$/.test(k ?? '');
|
|
52
|
+
const tree = valid && treeFor?.(k);
|
|
53
|
+
res.writeHead(valid ? (tree ? 200 : 404) : 400, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
54
|
+
res.end(JSON.stringify(tree || { error: valid ? 'unknown session' : 'invalid key' }));
|
|
55
|
+
} else if (url.pathname === '/timeline') {
|
|
56
|
+
const file = files.get(url.searchParams.get('key'));
|
|
57
|
+
if (!file) { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end('{"error":"no transcript for this session"}'); return; }
|
|
58
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
59
|
+
res.end(JSON.stringify(read(file, url.searchParams.get('key').split(':')[0])));
|
|
60
|
+
} else { res.writeHead(404); res.end(); }
|
|
61
|
+
});
|
|
62
|
+
const publish = (view, fileMap, tree) => {
|
|
63
|
+
treeFor = tree;
|
|
64
|
+
if (fileMap) files = fileMap;
|
|
65
|
+
packet = `data: ${JSON.stringify(view)}\n\n`;
|
|
66
|
+
for (const c of clients) { if (c.writableLength > MAX_BUFFER) c.destroy(); else c.write(packet); }
|
|
67
|
+
};
|
|
68
|
+
const close = () => { for (const c of clients) c.destroy(); server.close(); };
|
|
69
|
+
try {
|
|
70
|
+
const a = await listen(server, port, host);
|
|
71
|
+
return { server, close, publish, url: `http://${a.address}:${a.port}` };
|
|
72
|
+
} catch (e) { close(); throw e; }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = { startWeb, renderPage };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Monitor entry: agentmoni [--project <path>] [--quota | --token] [--once] | agentmoni --list [...]
|
|
3
|
+
// Default run = terminal + web (HTTP/SSE on 127.0.0.1:4777). --once prints one frame and exits, no web.
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { createMonitor } = require('./lib/monitor.js');
|
|
6
|
+
const { present, treeOf } = require('./lib/present.js');
|
|
7
|
+
const { renderFrame } = require('./lib/terminal.js');
|
|
8
|
+
const { repoKey } = require('./lib/repokey.js');
|
|
9
|
+
const { createLimits } = require('./lib/limits.js');
|
|
10
|
+
const { startWeb } = require('./lib/web.js');
|
|
11
|
+
|
|
12
|
+
const REFRESH_MS = 1500;
|
|
13
|
+
// COLUMNS is only a fallback when stdout is not a terminal: a finite integer of at least 20, else the default 120.
|
|
14
|
+
const columnsOf = v => { const n = Number(v); return Number.isInteger(n) && n >= 20 ? n : 120; };
|
|
15
|
+
const colorOn = () => !process.env.NO_COLOR && (!!process.stdout.isTTY || !!process.env.FORCE_COLOR); // https://no-color.org
|
|
16
|
+
|
|
17
|
+
async function main(argv) {
|
|
18
|
+
if (argv.includes('--list')) return require('./lib/list.js').main(argv);
|
|
19
|
+
const arg = name => { const i = argv.indexOf(name); return i < 0 ? null : argv[i + 1]; };
|
|
20
|
+
const once = argv.includes('--once'), modeFlag = argv.includes('--token') ? 'token' : argv.includes('--quota') ? 'quota' : null, mode = modeFlag ?? 'quota';
|
|
21
|
+
const project = arg('--project'), repo = project ? repoKey(path.resolve(project)) : null;
|
|
22
|
+
const monitor = createMonitor(), limits = createLimits();
|
|
23
|
+
monitor.start();
|
|
24
|
+
if (!once && !monitor.log.owner) {
|
|
25
|
+
// Allow a concurrent starter to finish binding its web port.
|
|
26
|
+
let url = monitor.log.readWeb();
|
|
27
|
+
for (let i = 0; !url && i < 20; i++) {
|
|
28
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
29
|
+
url = monitor.log.readWeb();
|
|
30
|
+
}
|
|
31
|
+
if (url) console.log('web: ' + url);
|
|
32
|
+
else {
|
|
33
|
+
const owner = monitor.log.warnings.find(w => /^another monitor \(pid /.test(w)) ?? 'another monitor is running (pid unknown)';
|
|
34
|
+
console.error(owner + '; no web URL yet');
|
|
35
|
+
process.exitCode = 1;
|
|
36
|
+
}
|
|
37
|
+
monitor.stop();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const frame = async (waitQuota) => {
|
|
41
|
+
const now = Date.now();
|
|
42
|
+
const state = await monitor.refresh({ now, waitQuota });
|
|
43
|
+
const view = present(state, { mode, repo, now, catalog: monitor.catalog, limits });
|
|
44
|
+
// The web gets every repo (its filter starts at `repo`) and the transcript paths its timeline endpoint may read.
|
|
45
|
+
const web = () => ({ view: { ...present(state, { mode, repo: null, now, catalog: monitor.catalog, limits }), defaultRepo: repo, modeFlag },
|
|
46
|
+
files: new Map(state.sessions.map(s => [`${s.provider}:${s.id}`, s.source?.file]).filter(([, f]) => f)),
|
|
47
|
+
treeFor: k => treeOf(state, k, { mode, repo: null, now, catalog: monitor.catalog, limits }) });
|
|
48
|
+
return { view, web, text: renderFrame(view, { cols: process.stdout.columns || columnsOf(process.env.COLUMNS), rows: once ? Infinity : process.stdout.rows || Infinity, color: colorOn() }) };
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const tty = !!process.stdout.isTTY;
|
|
52
|
+
if (once) {
|
|
53
|
+
const { view, text } = await frame(true);
|
|
54
|
+
console.log(text);
|
|
55
|
+
for (const w of view.warnings) console.error('warning:', w);
|
|
56
|
+
monitor.stop();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let web = null, webError = null;
|
|
61
|
+
try { web = await startWeb(); monitor.log.publishWeb(web.url); } catch (e) { webError = e.message; }
|
|
62
|
+
let view = null, timer = null, stopped = false;
|
|
63
|
+
// Without a TTY there is no terminal view: only the web runs (URL printed once), until SIGINT/SIGTERM.
|
|
64
|
+
if (!tty) console.log(web ? `web: ${web.url}` : `web: off (${webError})`);
|
|
65
|
+
const draw = () => {
|
|
66
|
+
if (!view || !tty) return;
|
|
67
|
+
const text = renderFrame(view, { cols: process.stdout.columns, rows: process.stdout.rows - 2, color: colorOn() });
|
|
68
|
+
const foot = web ? `web: ${web.url}` : `web: off (${webError})`;
|
|
69
|
+
process.stdout.write('\x1b[H' + [...text.split('\n'), foot].map(l => l + '\x1b[K').join('\n') + '\n\x1b[J');
|
|
70
|
+
};
|
|
71
|
+
const quit = () => {
|
|
72
|
+
if (stopped) return;
|
|
73
|
+
stopped = true; clearTimeout(timer);
|
|
74
|
+
web?.close(); monitor.stop();
|
|
75
|
+
if (tty) { process.stdin.setRawMode(false); process.stdout.write('\x1b[?25h\x1b[?1049l'); }
|
|
76
|
+
process.exit(0);
|
|
77
|
+
};
|
|
78
|
+
if (tty) {
|
|
79
|
+
process.stdout.write('\x1b[?1049h\x1b[?25l');
|
|
80
|
+
process.stdout.on('resize', draw);
|
|
81
|
+
process.stdin.setRawMode(true);
|
|
82
|
+
process.stdin.resume();
|
|
83
|
+
process.stdin.on('data', d => { const s = String(d); if (s === 'q' || s === 'Q' || s === '\x03') quit(); });
|
|
84
|
+
}
|
|
85
|
+
process.on('SIGTERM', quit);
|
|
86
|
+
process.on('SIGINT', quit);
|
|
87
|
+
const tick = async () => {
|
|
88
|
+
try {
|
|
89
|
+
const f = await frame(false), w = f.web();
|
|
90
|
+
view = f.view; web?.publish(w.view, w.files, w.treeFor); draw();
|
|
91
|
+
} catch (e) { if (tty) process.stdout.write(`\x1b[H\x1b[Kmonitor error: ${e.message}`); else console.error('monitor error:', e.message); }
|
|
92
|
+
if (!stopped) timer = setTimeout(tick, REFRESH_MS);
|
|
93
|
+
};
|
|
94
|
+
tick();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (require.main === module) main(process.argv.slice(2)).catch(e => { console.error(e.stack ?? e); process.exitCode = 1; });
|
|
98
|
+
module.exports = { main, columnsOf };
|
package/package.json
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@guenbankhai/tools",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
|
|
6
|
-
}
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"engines": {
|
|
5
|
+
"node": ">=20"
|
|
6
|
+
},
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/guenbankhai/tools.git"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"agentmoni": "agentmoni/monitor.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"agentmoni/monitor.js",
|
|
19
|
+
"agentmoni/lib/"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "node --test"
|
|
23
|
+
}
|
|
24
|
+
}
|