@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,106 @@
|
|
|
1
|
+
// Fixed cards, compact staggered sibling rows, and private branches from each parent's trunk.
|
|
2
|
+
// Shared by the page and node tests; coordinates are first computed as [cross, main].
|
|
3
|
+
(function (root) {
|
|
4
|
+
const SIZE = { w: 340, h: 244 }, MARGIN = 24;
|
|
5
|
+
const FOLD = { w: 140, h: 26 };
|
|
6
|
+
const TRUNK = 36, LANE = 14, LABEL_W = 110, LABEL_V = 80, LABEL_H = 18, LABEL_GAP = 6;
|
|
7
|
+
const rowsOf = count => count > 12 ? 3 : count > 6 ? 2 : 1;
|
|
8
|
+
|
|
9
|
+
function layoutGraph(roots, { vertical = false, collapsed = new Set() } = {}) {
|
|
10
|
+
const { w, h } = SIZE, along = vertical ? h : w, across = vertical ? w : h;
|
|
11
|
+
const LEVEL = TRUNK + (vertical ? LABEL_V : LABEL_W) + 2 * LABEL_GAP, GAP = 72;
|
|
12
|
+
const kids = n => collapsed.has(n.id) ? [] : n.children ?? [];
|
|
13
|
+
const extent = new Map(), slots = new Map(), nodes = [], edges = [];
|
|
14
|
+
const measure = n => {
|
|
15
|
+
const list = kids(n), rows = rowsOf(list.length);
|
|
16
|
+
list.forEach(measure);
|
|
17
|
+
const centers = [];
|
|
18
|
+
list.forEach((k, i) => {
|
|
19
|
+
let at = extent.get(k) / 2;
|
|
20
|
+
for (let j = 0; j < i; j++) {
|
|
21
|
+
const separate = rows === 1 || kids(k).length || kids(list[j]).length;
|
|
22
|
+
at = Math.max(at, centers[j] + (separate ? (extent.get(list[j]) + extent.get(k)) / 2 + (rows === 1 ? 22 : GAP) : (i - j) * (across + GAP) / rows));
|
|
23
|
+
}
|
|
24
|
+
centers.push(at);
|
|
25
|
+
});
|
|
26
|
+
const width = Math.max(across, ...list.map((k, i) => centers[i] + extent.get(k) / 2));
|
|
27
|
+
slots.set(n, centers);
|
|
28
|
+
extent.set(n, width);
|
|
29
|
+
};
|
|
30
|
+
const place = (n, main, start, center = start + extent.get(n) / 2, next = main + along + LEVEL, parent = null) => {
|
|
31
|
+
const box = { id: n.id, x: vertical ? center - w / 2 : main, y: vertical ? main : center - h / 2, w, h };
|
|
32
|
+
nodes.push(box);
|
|
33
|
+
if (parent) edges.push([parent, box]);
|
|
34
|
+
const list = kids(n), rows = rowsOf(list.length);
|
|
35
|
+
list.forEach((k, i) => {
|
|
36
|
+
const cross = start + slots.get(n)[i];
|
|
37
|
+
place(k, next + (i % rows) * (along + LEVEL), cross - extent.get(k) / 2, cross, next + rows * (along + LEVEL), box);
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
let offset = MARGIN;
|
|
41
|
+
for (const r of roots) { measure(r); place(r, MARGIN, offset); offset += extent.get(r) + 44; }
|
|
42
|
+
const mainOf = n => vertical ? n.y : n.x, crossOf = n => vertical ? n.x : n.y;
|
|
43
|
+
const xy = p => vertical ? p : [p[1], p[0]];
|
|
44
|
+
// Assign routes through each band together; keep wide-gap lanes near their targets.
|
|
45
|
+
const routes = edges.map(([a, b]) => {
|
|
46
|
+
const s = [crossOf(a) + across / 2, mainOf(a) + along], t = [crossOf(b) + across / 2, mainOf(b)];
|
|
47
|
+
const bands = edges.filter(([p, c]) => p === a && mainOf(c) < t[1]).map(([, c]) => mainOf(c));
|
|
48
|
+
return { a, b, s, t, bands, lanes: new Map() };
|
|
49
|
+
});
|
|
50
|
+
for (const m of new Set(nodes.map(mainOf))) {
|
|
51
|
+
const blocks = nodes.filter(n => mainOf(n) === m).map(n => [crossOf(n), crossOf(n) + across]).sort((a, b) => a[0] - b[0]);
|
|
52
|
+
const gaps = [[blocks[0][0] - GAP, blocks[0][0]], ...blocks.slice(1).map((b, i) => [blocks[i][1], b[0]]), [blocks.at(-1)[1], blocks.at(-1)[1] + GAP]];
|
|
53
|
+
const groups = new Map();
|
|
54
|
+
for (const r of routes.filter(r => r.bands.includes(m))) {
|
|
55
|
+
const distance = ([lo, hi]) => Math.abs(Math.max(lo + 12, Math.min(hi - 12, r.t[0])) - r.t[0]);
|
|
56
|
+
const gap = gaps.filter(([lo, hi]) => hi - lo >= 36).sort((a, b) => distance(a) - distance(b))[0];
|
|
57
|
+
if (!groups.has(gap)) groups.set(gap, []);
|
|
58
|
+
groups.get(gap).push(r);
|
|
59
|
+
}
|
|
60
|
+
for (const [[lo, hi], group] of groups) {
|
|
61
|
+
group.sort((a, b) => a.t[0] - b.t[0]);
|
|
62
|
+
group.forEach((r, i) => {
|
|
63
|
+
const fraction = (i + 1) / (group.length + 1);
|
|
64
|
+
const lane = hi - lo > 2 * GAP ? Math.max(lo + GAP * fraction, Math.min(hi - GAP * (1 - fraction), r.t[0])) : lo + (hi - lo) * fraction;
|
|
65
|
+
r.lanes.set(m, lane);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const wires = routes.map(({ a, b, s, t, lanes }) => {
|
|
70
|
+
const p = [s, [s[0], s[1] + TRUNK]];
|
|
71
|
+
// Thread later-row edges through the gaps of each preceding row. Turns stay in the level gaps.
|
|
72
|
+
const bands = [...lanes.keys()].sort((x, y) => x - y);
|
|
73
|
+
for (const m of bands) {
|
|
74
|
+
const lane = lanes.get(m), bend = m - LEVEL + LANE;
|
|
75
|
+
p.push([p.at(-1)[0], bend], [lane, bend], [lane, m + along + LANE]);
|
|
76
|
+
}
|
|
77
|
+
const final = t[1] - LEVEL + TRUNK;
|
|
78
|
+
p.push([p.at(-1)[0], final], [t[0], final], t);
|
|
79
|
+
// Remove zero-length steps and redundant collinear points, retaining an axis-aligned route for inspection.
|
|
80
|
+
const points = [];
|
|
81
|
+
for (const q of p) {
|
|
82
|
+
if (points.length && q.every((v, i) => v === points.at(-1)[i])) continue;
|
|
83
|
+
while (points.length > 1 && [0, 1].some(i => points.at(-2)[i] === points.at(-1)[i] && points.at(-1)[i] === q[i])) points.pop();
|
|
84
|
+
points.push(q);
|
|
85
|
+
}
|
|
86
|
+
const q = points.map(xy), end = q.at(-1);
|
|
87
|
+
const label = vertical ? [end[0] + LABEL_GAP, end[1] - LABEL_GAP - LABEL_V / 2] : [end[0] - LABEL_GAP - LABEL_W / 2, end[1] - LABEL_GAP];
|
|
88
|
+
// Round bends with short cubic curves; the 12px card clearance also covers the moving dot.
|
|
89
|
+
let path = 'M' + q[0].join(' ');
|
|
90
|
+
for (let i = 1; i < q.length - 1; i++) {
|
|
91
|
+
const prev = q[i - 1], cur = q[i], next = q[i + 1];
|
|
92
|
+
const dist = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1]);
|
|
93
|
+
const r = Math.min(8, dist(prev, cur) / 2, dist(cur, next) / 2);
|
|
94
|
+
const near = other => cur.map((v, j) => v + (other[j] - v) * r / dist(cur, other));
|
|
95
|
+
path += ' L' + near(prev).join(' ') + ' C' + cur.join(' ') + ' ' + cur.join(' ') + ' ' + near(next).join(' ');
|
|
96
|
+
}
|
|
97
|
+
path += ' L' + end.join(' ');
|
|
98
|
+
return { from: a.id, to: b.id, segments: q.slice(1).map((v, i) => [...q[i], ...v]), path, label, labelAlign: vertical ? 'start' : 'center' };
|
|
99
|
+
});
|
|
100
|
+
const extra = n => collapsed.has(n.id) ? (vertical ? FOLD.h / 2 : FOLD.w - FOLD.h / 2) : 0;
|
|
101
|
+
return { nodes, wires, width: Math.max(300, ...nodes.map(n => n.x + n.w + MARGIN + (vertical ? 0 : extra(n)))), height: Math.max(120, ...nodes.map(n => n.y + n.h + MARGIN + (vertical ? extra(n) : 0))) };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (typeof module === 'object' && module.exports) module.exports = { layoutGraph, FOLD, LABEL_W, LABEL_V, LABEL_H };
|
|
105
|
+
else root.AgentGraph = { layoutGraph, FOLD, LABEL_W, LABEL_V, LABEL_H };
|
|
106
|
+
})(typeof globalThis === 'object' ? globalThis : this);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Shared history calculations for the browser and tests.
|
|
2
|
+
(function (root) {
|
|
3
|
+
function subtree(node, depth = 0, seen = new Set()) {
|
|
4
|
+
if (!node || seen.has(node.key)) return [];
|
|
5
|
+
seen.add(node.key);
|
|
6
|
+
return [{ ...node, depth }, ...(node.children ?? []).flatMap(c => subtree(c, depth + 1, seen))];
|
|
7
|
+
}
|
|
8
|
+
// Copies of the live roots with their finished descendants (from `history`, by parent key) attached under the real parent. Finished sessions whose tree has no live root are left out.
|
|
9
|
+
function withFinished(roots, history) {
|
|
10
|
+
const live = n => ({ ...n, children: n.children.map(live) });
|
|
11
|
+
const out = roots.map(live), byKey = new Map();
|
|
12
|
+
const index = n => { byKey.set(n.key, n); n.children.forEach(index); };
|
|
13
|
+
out.forEach(index);
|
|
14
|
+
const ghosts = history.filter(s => !byKey.has(s.key)).map(s => ({ ...s, state: 'ended', lastActivityAt: s.endedAt, context: { tokens: null, limit: null }, done: 0, children: [] }));
|
|
15
|
+
ghosts.forEach(g => byKey.set(g.key, g));
|
|
16
|
+
const touched = new Set();
|
|
17
|
+
for (const g of ghosts) { const p = byKey.get(g.parent); if (p) { p.children.push(g); touched.add(p); } }
|
|
18
|
+
touched.forEach(p => p.children.sort((a, b) => a.startedAt - b.startedAt));
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
function union(intervals) {
|
|
22
|
+
const valid = intervals.filter(([a, b]) => Number.isFinite(a) && Number.isFinite(b) && b >= a).sort((a, b) => a[0] - b[0]);
|
|
23
|
+
let active = 0, end = -Infinity;
|
|
24
|
+
for (const [a, b] of valid) { active += Math.max(0, b - Math.max(a, end)); end = Math.max(end, b); }
|
|
25
|
+
return { active, span: valid.length ? end - valid[0][0] : 0 };
|
|
26
|
+
}
|
|
27
|
+
function totals(rows, now = Date.now()) {
|
|
28
|
+
const kinds = {}, providers = {};
|
|
29
|
+
for (const s of rows) {
|
|
30
|
+
kinds[s.kind] = (kinds[s.kind] ?? 0) + 1;
|
|
31
|
+
const p = providers[s.provider] ??= { tokens: {}, share: {}, kinds: {} };
|
|
32
|
+
p.kinds[s.kind] = (p.kinds[s.kind] ?? 0) + 1;
|
|
33
|
+
for (const k of ['input', 'cacheRead', 'cacheWrite', 'output']) {
|
|
34
|
+
const v = s.tokens?.[k];
|
|
35
|
+
p.tokens[k] ??= null;
|
|
36
|
+
if (Number.isFinite(v)) p.tokens[k] = (p.tokens[k] ?? 0) + v;
|
|
37
|
+
}
|
|
38
|
+
for (const w of Object.keys(s.share ?? {})) {
|
|
39
|
+
const v = s.share?.[w]?.total;
|
|
40
|
+
if (!(w in p.share)) p.share[w] = 0;
|
|
41
|
+
p.share[w] = p.share[w] == null || !Number.isFinite(v) ? null : p.share[w] + v;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
for (const [provider, p] of Object.entries(providers)) Object.assign(p, union(rows.filter(s => s.provider === provider).map(s => [s.startedAt, s.endedAt ?? (s.state === 'ended' ? s.lastActivityAt : now)])));
|
|
45
|
+
return { kinds, providers, ...union(rows.map(s => [s.startedAt, s.endedAt ?? (s.state === 'ended' ? s.lastActivityAt : now)])) };
|
|
46
|
+
}
|
|
47
|
+
const api = { subtree, withFinished, union, totals };
|
|
48
|
+
if (typeof module !== 'undefined') module.exports = api;
|
|
49
|
+
else root.AgentHistory = api;
|
|
50
|
+
})(globalThis);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Read only selected non-secret string fields. Other values are skipped without decoding them.
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
function readFields(file, fields, presence = []) {
|
|
4
|
+
try {
|
|
5
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
6
|
+
let i = 0;
|
|
7
|
+
const out = {};
|
|
8
|
+
const ws = () => { while (/\s/.test(text[i] ?? '') && i < text.length) i++; };
|
|
9
|
+
const expect = c => { ws(); if (text[i++] !== c) throw new Error('bad JSON'); };
|
|
10
|
+
function string(decode) {
|
|
11
|
+
const start = i;
|
|
12
|
+
expect('"');
|
|
13
|
+
while (i < text.length) {
|
|
14
|
+
const c = text[i++];
|
|
15
|
+
if (c === '"') return decode ? JSON.parse(text.slice(start, i)) : undefined;
|
|
16
|
+
if (c < ' ') throw new Error('bad string');
|
|
17
|
+
if (c === '\\') {
|
|
18
|
+
const e = text[i++];
|
|
19
|
+
if (e === 'u') { if (!/^[\da-f]{4}$/i.test(text.slice(i, i + 4))) throw new Error('bad escape'); i += 4; }
|
|
20
|
+
else if (!'"\\/bfnrt'.includes(e ?? ' ') || e === undefined) throw new Error('bad escape');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
throw new Error('unfinished string');
|
|
24
|
+
}
|
|
25
|
+
function value(keys, depth = 0) {
|
|
26
|
+
if (depth > 100) throw new Error('too deep');
|
|
27
|
+
ws();
|
|
28
|
+
const prefix = keys.join('.');
|
|
29
|
+
for (const field of fields) if (field === prefix || field.startsWith(prefix + '.')) delete out[field];
|
|
30
|
+
const selected = fields.includes(keys.join('.'));
|
|
31
|
+
if (text[i] === '"') {
|
|
32
|
+
const exists = presence.includes(keys.join('.'));
|
|
33
|
+
const v = string(selected && !exists);
|
|
34
|
+
if (selected) out[keys.join('.')] = exists ? true : v;
|
|
35
|
+
}
|
|
36
|
+
else if (text[i] === '{' || text[i] === '[') {
|
|
37
|
+
const object = text[i++] === '{', end = object ? '}' : ']';
|
|
38
|
+
ws(); if (text[i] === end) { i++; return; }
|
|
39
|
+
do {
|
|
40
|
+
ws();
|
|
41
|
+
const key = object ? string(true) : '';
|
|
42
|
+
if (object) expect(':');
|
|
43
|
+
value([...keys, key], depth + 1);
|
|
44
|
+
ws(); if (text[i] === end) { i++; return; }
|
|
45
|
+
expect(',');
|
|
46
|
+
} while (i < text.length);
|
|
47
|
+
throw new Error('unfinished object');
|
|
48
|
+
} else {
|
|
49
|
+
const m = /^(?:true|false|null|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?)/.exec(text.slice(i));
|
|
50
|
+
if (!m) throw new Error('bad value');
|
|
51
|
+
i += m[0].length;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
value([]); ws();
|
|
55
|
+
return i === text.length ? out : {};
|
|
56
|
+
} catch { return {}; }
|
|
57
|
+
}
|
|
58
|
+
module.exports = { readFields };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Reads agent logs (external data): unreadable files and malformed lines are skipped and counted, never fatal.
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
// All .jsonl files under dir (not following symlinks) modified at or after sinceMs.
|
|
6
|
+
function files(dir, sinceMs = 0) {
|
|
7
|
+
let entries;
|
|
8
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return []; }
|
|
9
|
+
return entries.flatMap(e => {
|
|
10
|
+
const full = path.join(dir, e.name);
|
|
11
|
+
if (e.isDirectory()) return files(full, sinceMs);
|
|
12
|
+
if (!e.isFile() || !e.name.endsWith('.jsonl')) return [];
|
|
13
|
+
try { return fs.statSync(full).mtimeMs >= sinceMs ? [full] : []; } catch { return []; }
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Calls visit(record) for each line where want(line) is true (a cheap string test that skips huge lines
|
|
18
|
+
// before JSON.parse). Returns the number of malformed lines; an unreadable file counts as one.
|
|
19
|
+
function scan(file, want, visit) {
|
|
20
|
+
let text, bad = 0;
|
|
21
|
+
try { text = fs.readFileSync(file, 'utf8'); } catch { return 1; }
|
|
22
|
+
for (const line of text.split('\n')) {
|
|
23
|
+
if (!line.trim() || !want(line)) continue;
|
|
24
|
+
let record;
|
|
25
|
+
try { record = JSON.parse(line); } catch { bad++; continue; }
|
|
26
|
+
if (record && typeof record === 'object') visit(record, line);
|
|
27
|
+
}
|
|
28
|
+
return bad;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Parsed result per file, reused while the file's mtime and size are unchanged; callers must not mutate it.
|
|
32
|
+
const cache = new Map();
|
|
33
|
+
function cached(file, parse, store = cache) {
|
|
34
|
+
let st;
|
|
35
|
+
try { st = fs.statSync(file); } catch { return parse(file); }
|
|
36
|
+
const hit = store.get(file);
|
|
37
|
+
if (hit && hit.mtimeMs === st.mtimeMs && hit.size === st.size) return hit.value;
|
|
38
|
+
const value = parse(file);
|
|
39
|
+
store.set(file, { mtimeMs: st.mtimeMs, size: st.size, value });
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { files, scan, cached };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Finds registered agent launch commands in shell command position.
|
|
2
|
+
function segments(line) {
|
|
3
|
+
const out = [];
|
|
4
|
+
let cur = '', quote = null;
|
|
5
|
+
for (let i = 0; i < line.length; i++) {
|
|
6
|
+
const ch = line[i];
|
|
7
|
+
if (ch === '\\' && quote !== "'") { cur += ch + (line[i + 1] ?? ''); i++; continue; } // an escaped character never opens, closes or splits (not inside '...')
|
|
8
|
+
if (quote) { if (ch === quote) quote = null; }
|
|
9
|
+
else if (ch === '"' || ch === "'") quote = ch;
|
|
10
|
+
else if (ch === '\n') { out.push(cur); return out; }
|
|
11
|
+
else if (ch === ';' || ch === '|' || ch === '&') {
|
|
12
|
+
if (ch !== ';' && line[i + 1] === ch) i++;
|
|
13
|
+
out.push(cur); cur = ''; continue;
|
|
14
|
+
}
|
|
15
|
+
cur += ch;
|
|
16
|
+
}
|
|
17
|
+
return [...out, cur];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// The launch segment of a command line: { kind, seg } or null.
|
|
21
|
+
function launchSegment(command, max = 600, registry = require('./agents/index.js')) {
|
|
22
|
+
if (typeof command !== 'string') return null;
|
|
23
|
+
for (const seg of segments(command.slice(0, max))) {
|
|
24
|
+
const c = seg.trim().replace(/^(?:(?:\w+=\S+|time)\s+)+/, ''); // env assignments and `time` before the command
|
|
25
|
+
if (/\s(?:--help|-h)(?:\s|$)/.test(c)) continue;
|
|
26
|
+
for (const [kind, re] of registry.flatMap(a => a.launchPatterns)) if (re.test(c)) return { kind, seg: c };
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const launchKind = (command, registry) => launchSegment(command, 600, registry)?.kind ?? null;
|
|
31
|
+
|
|
32
|
+
// Letters and digits only: the shell drops quotes and backslashes, so a prompt in a launch command and in the thread differ in punctuation.
|
|
33
|
+
const squash = t => String(t ?? '').toLowerCase().replace(/[^\p{L}\p{N}]/gu, '');
|
|
34
|
+
// text: only the quoted arguments of the launch segment itself (the prompt is one of them). A prompt passed by file or unquoted
|
|
35
|
+
// stays out, so a thread cannot match words from another command on the same line; then the choice stays ambiguous.
|
|
36
|
+
const launchOf = (at, kind, cmd, registry, full = false) => {
|
|
37
|
+
const line = cmd.split('\n')[0], seg = launchSegment(cmd, 65536, registry)?.seg ?? '';
|
|
38
|
+
const quoted = [...seg.matchAll(/"((?:[^"\\]|\\.)*)"|'([^']*)'/g)].map(m => m[1] ?? m[2]).join(' ');
|
|
39
|
+
return { at, kind, cmd: line.slice(0, 200), text: full ? squash(quoted) : [...seg.matchAll(/"((?:[^"\\]|\\.)*)"|'([^']*)'/g)].map(m => squash(m[1] ?? m[2]).slice(0, 40)).join(' ') };
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Full prompt text is indexed once per transcript version, separately from parsed sessions.
|
|
43
|
+
const promptCache = new Map();
|
|
44
|
+
function fullLaunchTexts(file, registry) {
|
|
45
|
+
const texts = new Map();
|
|
46
|
+
require('./jsonl.js').scan(file, line => registry.some(a => a.launchLine?.(line)), r => {
|
|
47
|
+
const calls = registry.flatMap(a => a.launchCalls?.(r) ?? []);
|
|
48
|
+
for (const [id, commands] of calls) {
|
|
49
|
+
if (typeof id !== 'string') continue;
|
|
50
|
+
const text = commands.filter(cmd => typeof cmd === 'string').map(cmd => launchOf(0, null, cmd, registry, true).text).filter(Boolean);
|
|
51
|
+
if (text.length) texts.set(id, [...(texts.get(id) ?? []), ...text]);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
return texts;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Revisit long commands only when their retained head cannot decide a prompt match.
|
|
58
|
+
function matchesPrompt(launch, prompt, file, registry = require('./agents/index.js')) {
|
|
59
|
+
if (launch.text?.includes(prompt)) return true;
|
|
60
|
+
if (!launch.toolUseId || !file) return false;
|
|
61
|
+
if (!promptCache.has(registry)) promptCache.set(registry, new Map());
|
|
62
|
+
const texts = require('./jsonl.js').cached(file, f => fullLaunchTexts(f, registry), promptCache.get(registry));
|
|
63
|
+
return texts.get(launch.toolUseId)?.some(text => text.includes(prompt)) ?? false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Only literal paths are usable shell evidence.
|
|
67
|
+
const literal = v => typeof v === 'string' && v && !/[$\x60*?{}()<>|&;\n]/.test(v) ? v : null;
|
|
68
|
+
function launchCwd(cmd, cwd, flags = []) {
|
|
69
|
+
if (typeof cmd !== 'string') return cwd;
|
|
70
|
+
const parts = segments(cmd.slice(0, 65536));
|
|
71
|
+
const cds = parts.filter(s => /^\s*cd(?:\s|$)/.test(s));
|
|
72
|
+
if (cds.length && (cds.length !== 1 || cds[0] !== parts[0])) return null;
|
|
73
|
+
const cd = /^\s*cd\s+(?:"([^"\n]+)"|'([^'\n]+)'|([^\s;&]+))\s*(?:&&|;)/.exec(cmd);
|
|
74
|
+
if (cds.length && !cd) return null;
|
|
75
|
+
const resolve = v => {
|
|
76
|
+
v = literal(v);
|
|
77
|
+
if (!v || /^(?:~|-)|^\/(?:mnt\/)?[a-z]\/?$|^[a-z]:[^\\/]/i.test(v) || /^[a-z]:$/i.test(v)) return null;
|
|
78
|
+
// shortcut: resolve all paths as Windows paths, including Git Bash and WSL drive paths.
|
|
79
|
+
v = v.replace(/^\/(?:mnt\/)?([a-z])\//i, (_, drive) => drive.toUpperCase() + ':\\');
|
|
80
|
+
const path = require('node:path');
|
|
81
|
+
return path.win32.isAbsolute(v) ? path.win32.normalize(v) : cwd ? path.win32.resolve(cwd, v) : null;
|
|
82
|
+
};
|
|
83
|
+
if (cd) {
|
|
84
|
+
cwd = resolve(cd[1] ?? cd[2] ?? cd[3]);
|
|
85
|
+
if (!cwd) return null;
|
|
86
|
+
}
|
|
87
|
+
const seg = launchSegment(cmd, 65536)?.seg ?? '';
|
|
88
|
+
const args = [];
|
|
89
|
+
const tokens = /\s*(?:"((?:[^"\\]|\\.)*)"|'([^']*)'|([^\s"']+))/gy;
|
|
90
|
+
let m;
|
|
91
|
+
while ((m = tokens.exec(seg))) args.push({ value: m[1] ?? m[2] ?? m[3], bare: m[3] != null });
|
|
92
|
+
for (let i = 0; i < args.length; i++) {
|
|
93
|
+
if (!args[i].bare || !flags.includes(args[i].value) || args[i + 1] == null) continue;
|
|
94
|
+
cwd = resolve(args[++i].value); // a non-literal value gives no cwd, like a non-literal cd
|
|
95
|
+
if (!cwd) return null;
|
|
96
|
+
}
|
|
97
|
+
return cwd;
|
|
98
|
+
}
|
|
99
|
+
function resultJob(content) {
|
|
100
|
+
const text = typeof content === 'string' ? content : Array.isArray(content) ? content.filter(c => typeof c?.text === 'string').map(c => c.text).join('\n') : '';
|
|
101
|
+
const ids = [...new Set(text.match(/\btask-[a-z0-9]+-[a-z0-9]+\b/g) ?? [])];
|
|
102
|
+
return ids.length === 1 ? ids[0] : null;
|
|
103
|
+
}
|
|
104
|
+
// Only structured CLI records carry session evidence; prose is not proof.
|
|
105
|
+
function resultSessions(content) {
|
|
106
|
+
const texts = typeof content === 'string' ? [content] : Array.isArray(content) ? content.filter(c => typeof c?.text === 'string').map(c => c.text) : [];
|
|
107
|
+
const ids = new Set();
|
|
108
|
+
for (const text of texts) {
|
|
109
|
+
for (const part of [text, ...text.split('\n')]) {
|
|
110
|
+
try {
|
|
111
|
+
const r = JSON.parse(part);
|
|
112
|
+
if (typeof r?.session_id === 'string' && /^[a-zA-Z0-9_-]+$/.test(r.session_id)) ids.add(r.session_id);
|
|
113
|
+
} catch { /* not a JSON record */ }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return [...ids];
|
|
117
|
+
}
|
|
118
|
+
module.exports = { launchKind, squash, launchOf, launchCwd, resultJob, resultSessions, matchesPrompt };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Context limits from agent-local sources, refreshed once per minute.
|
|
2
|
+
const agents = require('./agents/index.js');
|
|
3
|
+
function createLimits(options = {}) {
|
|
4
|
+
const registry = options.registry ?? agents;
|
|
5
|
+
let table, at = 0;
|
|
6
|
+
return (provider, model, now = Date.now()) => {
|
|
7
|
+
if (!table || now - at > 60000) { table = Object.fromEntries(registry.map(a => [a.id, a.loadLimits?.(options)])); at = now; }
|
|
8
|
+
return table[provider]?.get(String(model ?? '').replace(/\[1m\]$/, '')) ?? null;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
module.exports = { createLimits };
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// Parent-child links: proven evidence first, then conservative command inference.
|
|
2
|
+
const agents = require('./agents/index.js');
|
|
3
|
+
const { squash, matchesPrompt } = require('./launch.js');
|
|
4
|
+
const key = s => `${s.provider}:${s.id}`;
|
|
5
|
+
const norm = cwd => typeof cwd === 'string' ? cwd.replace(/^\/([a-z])\//i, '$1:/').replace(/\//g, '\\').replace(/\\+$/, '').toLowerCase() : null;
|
|
6
|
+
|
|
7
|
+
const WINDOW_MS = 90 * 1000; // B starts within this after the launch command
|
|
8
|
+
const PROMPT_KEY = 40, PROMPT_MIN = 12; // letters of B's first prompt compared with the launch command; shorter proves nothing
|
|
9
|
+
const SKEW_MS = 2000; // clocks of two logs may disagree slightly
|
|
10
|
+
|
|
11
|
+
// Which launch command could have created B (null: B is not something a command starts).
|
|
12
|
+
// The session owning the nearest ancestor process of B's process (the process table must hold the whole chain).
|
|
13
|
+
// A parent must have started no later than its child, or the pid was reused by an unrelated process.
|
|
14
|
+
function parentByProcess(b, procs, owners) {
|
|
15
|
+
const byPid = new Map(procs.map(p => [p.pid, p]));
|
|
16
|
+
let p = byPid.get(b.pid);
|
|
17
|
+
for (let i = 0; p && i < 32; i++) {
|
|
18
|
+
const child = p;
|
|
19
|
+
p = byPid.get(p.ppid);
|
|
20
|
+
if (p && !(p.startMs <= child.startMs)) return null;
|
|
21
|
+
const owner = p && owners.get(p.pid);
|
|
22
|
+
if (owner && owner !== key(b)) return { owner, pid: p.pid };
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// sessions come from resolveStates (they carry state and pid). -> { sessions, warnings }; input objects are not changed.
|
|
28
|
+
function link(sessions, { procs = [], pluginJobs = [], registry = agents } = {}) {
|
|
29
|
+
const adapter = s => registry.find(a => a.id === s.provider);
|
|
30
|
+
const out = sessions.map(s => ({ ...s, parent: null, link: null }));
|
|
31
|
+
const byKey = new Map(out.map(s => [key(s), s]));
|
|
32
|
+
const owners = new Map(out.filter(s => s.pid != null && s.state !== 'ended').map(s => [s.pid, key(s)]));
|
|
33
|
+
const ambiguous = []; // { sessions, parents, text }: links the data cannot decide
|
|
34
|
+
const attach = (s, parent, evidence) => {
|
|
35
|
+
const seen = new Set([key(s)]);
|
|
36
|
+
for (let p = parent; p; p = byKey.get(p)?.parent) {
|
|
37
|
+
if (seen.has(p)) {
|
|
38
|
+
ambiguous.push({ sessions: [key(s)], parents: [parent], text: `${key(s)} -> ${parent} would create a cycle: not linked` });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
seen.add(p);
|
|
42
|
+
}
|
|
43
|
+
s.parent = parent;
|
|
44
|
+
s.link = evidence;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const claimed = new Map();
|
|
48
|
+
for (const a of out) for (const l of a.launches ?? []) {
|
|
49
|
+
const keys = registry.flatMap(adapter => adapter.launchClaims?.(l) ?? []);
|
|
50
|
+
for (const k of keys) {
|
|
51
|
+
claimed.set(k, [...(claimed.get(k) ?? []), { a, l, count: keys.length }]);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const blocked = new Set();
|
|
55
|
+
for (const s of out) {
|
|
56
|
+
const claims = claimed.get(key(s));
|
|
57
|
+
if (!claims) continue;
|
|
58
|
+
const { a, l, count } = claims.reduce((first, c) => c.l.at < first.l.at ? c : first), parent = key(a);
|
|
59
|
+
const processParent = s.pid != null ? parentByProcess(s, procs, owners)?.owner : null;
|
|
60
|
+
if ((processParent && processParent !== parent) || new Set(claims.map(c => key(c.a))).size !== 1 || count !== 1 || parent === key(s) || (s.parentId && parent !== s.provider + ':' + s.parentId)) {
|
|
61
|
+
blocked.add(key(s));
|
|
62
|
+
ambiguous.push({ sessions: [key(s)], parents: [...new Set(claims.map(c => key(c.a)))], text: key(s) + ' has conflicting launch proofs: not linked' });
|
|
63
|
+
} else attach(s, parent, { kind: 'proven', label: l.kind, evidence: { sessionId: s.id, toolUseId: l.toolUseId } });
|
|
64
|
+
}
|
|
65
|
+
for (const s of out) {
|
|
66
|
+
if (blocked.has(key(s)) || s.link || !s.parentId) continue;
|
|
67
|
+
attach(s, `${s.provider}:${s.parentId}`, { kind: 'proven', label: adapter(s).parentLabel, evidence: { file: s.source.file, at: s.startedAt, ...(s.agentType ? { agentType: s.agentType } : {}) } });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const picks = [];
|
|
71
|
+
for (const b of out) {
|
|
72
|
+
if (blocked.has(key(b))) continue;
|
|
73
|
+
const exact = adapter(b).launchProofs?.(b, pluginJobs, out) ?? [];
|
|
74
|
+
const want = exact[0]?.label ?? adapter(b).launchKindOf(b);
|
|
75
|
+
if (!want || b.link) continue;
|
|
76
|
+
const policy = adapter(b).launchPolicy(want, b);
|
|
77
|
+
const parentProviders = policy.parentOrigin == null ? null : registry.filter(a => a.originators?.includes(policy.parentOrigin)).map(a => a.id);
|
|
78
|
+
const proofs = [...exact, ...(adapter(b).provenLinks?.(b, pluginJobs, out) ?? [])].flatMap(p => {
|
|
79
|
+
if (p.parent) return [p];
|
|
80
|
+
// Link records may name a session without its agent. Prefer sessions we actually read;
|
|
81
|
+
// for a missing parent, resolve the origin label through the declarations.
|
|
82
|
+
const known = out.filter(s => s.id === p.parentId && key(s) !== key(b));
|
|
83
|
+
const providers = known.length ? known.map(s => s.provider) : registry.filter(a => a.originators?.includes(p.parentOrigin)).map(a => a.id);
|
|
84
|
+
return providers.map(provider => ({ ...p, parent: `${provider}:${p.parentId}` }));
|
|
85
|
+
});
|
|
86
|
+
const parents = [...new Set(proofs.map(p => p.parent))];
|
|
87
|
+
if (parents.length > 1) {
|
|
88
|
+
ambiguous.push({ sessions: [key(b)], parents, text: `${key(b)} has conflicting parents: not linked` });
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (parents.length === 1) {
|
|
92
|
+
attach(b, parents[0], { kind: 'proven', label: want, evidence: proofs[0].evidence });
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
// A command-started process can be traced up to the session that ran it. Shared hosts cannot supply a process link.
|
|
96
|
+
const hit = policy.process && b.pid != null ? parentByProcess(b, procs, owners) : null;
|
|
97
|
+
if (hit) {
|
|
98
|
+
attach(b, hit.owner, { kind: 'proven', label: want, evidence: { pid: b.pid, parentPid: hit.pid, file: b.source.file, at: b.startedAt } });
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const cands = [];
|
|
102
|
+
for (const a of out) {
|
|
103
|
+
if (a === b || (parentProviders && !parentProviders.includes(a.provider))) continue;
|
|
104
|
+
for (const l of a.launches ?? []) {
|
|
105
|
+
if (norm(l.cwd === undefined ? a.cwd : l.cwd) == null || norm(l.cwd === undefined ? a.cwd : l.cwd) !== norm(b.cwd)) continue;
|
|
106
|
+
const d = b.startedAt - l.at;
|
|
107
|
+
if (!l.sessionIds?.length && l.kind === want && d >= -SKEW_MS && d <= WINDOW_MS) cands.push({ a, l, d });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// The prompt B started with is inside the command that started it: when some launches carry it, only those count.
|
|
111
|
+
const p = squash(b.prompt).slice(0, PROMPT_KEY);
|
|
112
|
+
const named = p.length >= PROMPT_MIN ? cands.filter(c => matchesPrompt(c.l, p, c.a.source?.file, registry)) : [];
|
|
113
|
+
const fits = new Map(); // parent key -> its launch closest before B
|
|
114
|
+
for (const { a, l, d } of named.length ? named : cands) {
|
|
115
|
+
if (!fits.has(key(a)) || d < fits.get(key(a)).d) fits.set(key(a), { d, l, file: a.source.file, byPrompt: named.length > 0 });
|
|
116
|
+
}
|
|
117
|
+
if (fits.size > 1) ambiguous.push({ sessions: [key(b)], parents: [...fits.keys()], text: `${key(b)} could belong to ${[...fits.keys()].join(' or ')}: not linked` });
|
|
118
|
+
if (fits.size === 1) picks.push({ b, want, parent: [...fits.keys()][0], ...[...fits.values()][0] });
|
|
119
|
+
}
|
|
120
|
+
// One launch that fits several sessions cannot say which it started: link none of them.
|
|
121
|
+
const uses = new Map();
|
|
122
|
+
for (const p of picks) uses.set(`${p.parent}@${p.want}@${p.l.at}`, [...(uses.get(`${p.parent}@${p.want}@${p.l.at}`) ?? []), p.b]);
|
|
123
|
+
// All proven edges are attached before these inferences, so weaker evidence cannot displace them.
|
|
124
|
+
for (const p of picks) {
|
|
125
|
+
const users = uses.get(`${p.parent}@${p.want}@${p.l.at}`);
|
|
126
|
+
if (users.length > 1) { if (users[0] === p.b) ambiguous.push({ sessions: users.map(key), parents: [p.parent], text: `one launch by ${p.parent} fits ${users.map(key).join(' and ')}: not linked` }); continue; }
|
|
127
|
+
attach(p.b, p.parent, { kind: 'inferred', label: p.want, evidence: { command: p.l.cmd, at: p.l.at, file: p.file, ...(p.byPrompt ? { match: 'prompt' } : {}) } });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (const s of out) {
|
|
131
|
+
if (s.link && s.kind === 'standalone') s.kind = 'sub';
|
|
132
|
+
else if (!s.link && s.kind === 'sub' && !s.parentId) s.kind = 'standalone'; // a plugin thread whose parent is unknown
|
|
133
|
+
// A child still alive under an ended parent keeps working with nobody waiting for it.
|
|
134
|
+
s.orphan = s.state !== 'ended' && byKey.get(s.parent)?.state === 'ended';
|
|
135
|
+
}
|
|
136
|
+
return { sessions: out, warnings: ambiguous.map(a => a.text), ambiguous };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = { link };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Debug listing of the sessions found on this machine, with links and quota shares:
|
|
2
|
+
// agentmoni --list [--project <path>] [--days N] [--live] [--json]
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { createMonitor } = require('./monitor.js');
|
|
5
|
+
const { providersOf } = require('./present.js');
|
|
6
|
+
const agents = require('./agents/index.js');
|
|
7
|
+
const windowsOf = p => (agents.find(a => a.id === p)?.windows ?? []).map(w => w.id);
|
|
8
|
+
const { totals } = require('./history.js');
|
|
9
|
+
const { tok: k, pct, dur } = require('./format.js');
|
|
10
|
+
|
|
11
|
+
const norm = p => path.resolve(p).toLowerCase().replace(/[\\/]+$/, '');
|
|
12
|
+
const inside = (cwd, root) => typeof cwd === 'string' && (norm(cwd) === root || norm(cwd).startsWith(root + path.sep));
|
|
13
|
+
const split = sh => sh ? `${pct(sh.total)} [I ${pct(sh.input)} CR ${pct(sh.cacheRead)} CW ${pct(sh.cacheWrite)} O ${pct(sh.output)}]` : '—';
|
|
14
|
+
|
|
15
|
+
function accountLine(name, a, outside, now) {
|
|
16
|
+
const w = x => {
|
|
17
|
+
const r = a.windows[x];
|
|
18
|
+
return r ? `${x} ${pct(r.percent)}%${r.resetsAt ? ` reset ${dur(r.resetsAt - now)}` : ''} ${r.at ? ` (read ${dur(now - r.at)} ago)` : ''}${r.stale ? ' stale' : ''}` : `${x} —`;
|
|
19
|
+
};
|
|
20
|
+
return `${name.padEnd(6)} ${windowsOf(name).map(w).join(' ')} outside monitor: ${windowsOf(name).map(x => `${x} +${pct(outside[x])}%`).join(' ')}${a.error ? ` warning: ${a.error}` : ''}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function main(argv) {
|
|
24
|
+
const arg = name => { const i = argv.indexOf(name); return i < 0 ? null : argv[i + 1]; };
|
|
25
|
+
const days = Number(arg('--days') ?? 7), now = Date.now();
|
|
26
|
+
const project = arg('--project'), root = project ? norm(project) : null;
|
|
27
|
+
const monitor = createMonitor({ days: Math.max(days, 8) });
|
|
28
|
+
monitor.start(now);
|
|
29
|
+
const r = await monitor.refresh({ now, waitQuota: true });
|
|
30
|
+
monitor.stop();
|
|
31
|
+
const named = new Map([...(r.history ?? []), ...r.sessions].map(s => [`${s.provider}:${s.id}`, s]));
|
|
32
|
+
const inScope = s => {
|
|
33
|
+
const seen = new Set();
|
|
34
|
+
for (let a = s; a && !seen.has(a); a = named.get(a.parent)) {
|
|
35
|
+
if (inside(a.cwd, root)) return true;
|
|
36
|
+
seen.add(a);
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
};
|
|
40
|
+
const sessions = r.sessions
|
|
41
|
+
.filter(s => s.lastActivityAt >= now - days * 86400e3 && (root == null || inScope(s)) && (!argv.includes('--live') || s.state !== 'ended'))
|
|
42
|
+
.sort((a, b) => b.lastActivityAt - a.lastActivityAt);
|
|
43
|
+
if (argv.includes('--json')) return console.log(JSON.stringify({ accounts: r.accounts, outside: r.outside, sessions, warnings: r.warnings, ambiguous: r.ambiguous }, null, 2));
|
|
44
|
+
for (const p of providersOf(r)) console.log(accountLine(p, r.accounts[p], r.outside[p], now));
|
|
45
|
+
for (const s of sessions) {
|
|
46
|
+
const t = s.tokens;
|
|
47
|
+
console.log([s.provider, s.kind, (s.turn === 'aborted' ? '⊘ interrupted' : s.state) + (s.orphan ? ' orphan' : '') + (s.resume ? ` resume ${s.resume}` : ''), s.model ?? '—', s.effort ?? '',
|
|
48
|
+
new Date(s.lastActivityAt).toISOString().slice(0, 16), `I ${k(t.input)} CR ${k(t.cacheRead)} CW ${k(t.cacheWrite)} O ${k(t.output)} ctx ${k(s.context)}`,
|
|
49
|
+
s.name ?? '', s.link ? `<- ${s.parent} (${s.link.kind}, ${s.link.label})` : ''].join(' | '));
|
|
50
|
+
console.log(' ' + windowsOf(s.provider).map(w => `${w}\u2248${split(s.share[w])}`).join(' '));
|
|
51
|
+
if (r.sessions.some(c => c.parent === `${s.provider}:${s.id}`)) {
|
|
52
|
+
const own = s.shareTree[s.provider];
|
|
53
|
+
console.log(' with children: ' + windowsOf(s.provider).map(w => `${w}\u2248${pct(own[w]?.total)}`).join(' '));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
for (const line of totalsLines(sessions, now)) console.log(line);
|
|
57
|
+
for (const w of [...r.warnings, ...r.ambiguous.map(a => a.text)]) console.error('warning:', w);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (require.main === module) main(process.argv.slice(2));
|
|
61
|
+
function totalsLines(rows, now) {
|
|
62
|
+
if (!rows.length) return [];
|
|
63
|
+
const t = totals(rows, now);
|
|
64
|
+
const lines = Object.entries(t.providers).map(([name, p]) => {
|
|
65
|
+
const v = p.tokens;
|
|
66
|
+
const kinds = ['main', 'sub', 'standalone'].filter(kind => p.kinds[kind] > 0).map(kind => `${kind} ${p.kinds[kind]}`).join(', ');
|
|
67
|
+
return `total ${name} | ${kinds} | active ${dur(p.active)} | I ${k(v.input)} CR ${k(v.cacheRead)} CW ${k(v.cacheWrite)} O ${k(v.output)} | ${Object.keys(p.share).map(w => `${w}\u2248${pct(p.share[w])}`).join(' ')}`;
|
|
68
|
+
});
|
|
69
|
+
return [...lines, `total all | sessions ${rows.length} | active ${dur(t.active)} | span ${dur(t.span)}`];
|
|
70
|
+
}
|
|
71
|
+
module.exports = { main, totalsLines };
|