attesso 0.4.1 → 0.6.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 +201 -0
- package/NOTICE +4 -0
- package/README.md +158 -20
- package/bin/attesso-control-mcp.mjs +13 -0
- package/bin/attesso-mcp.mjs +2 -0
- package/bin/attesso.mjs +554 -75
- package/package.json +8 -4
- package/src/bundle.mjs +128 -0
- package/src/classify.mjs +83 -0
- package/src/cli-io.mjs +130 -0
- package/src/connect.mjs +160 -0
- package/src/control.mjs +101 -0
- package/src/decide.mjs +10 -0
- package/src/fs-paths.mjs +121 -0
- package/src/harness.mjs +130 -0
- package/src/hook.mjs +64 -40
- package/src/install.mjs +122 -0
- package/src/jsonl.mjs +16 -0
- package/src/ledger.mjs +79 -20
- package/src/mcp/control.mjs +171 -0
- package/src/mcp-proxy.mjs +605 -0
- package/src/merkle.mjs +83 -0
- package/src/policy.mjs +8 -8
- package/src/receipt.mjs +104 -0
- package/src/replay.mjs +133 -0
- package/src/report.mjs +69 -27
- package/src/rules.mjs +58 -11
- package/src/serve.mjs +4 -2
- package/src/setup.mjs +34 -45
- package/src/store.mjs +425 -64
- package/src/stream.mjs +72 -0
- package/src/toolpin.mjs +131 -0
- package/src/ui/catalog.mjs +266 -0
- package/src/ui/live.mjs +58 -0
- package/src/ui/query.mjs +163 -0
- package/src/ui/server.mjs +379 -0
- package/src/ui/static/index.html +1695 -0
- package/src/usage.mjs +96 -0
- package/src/watch.mjs +78 -0
package/src/usage.mjs
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Usage is reported by the agent or the recorder, never inferred from a tool
|
|
2
|
+
// proposal. The page may show exact call counts without usage; it must not
|
|
3
|
+
// convert task totals into a per-call saving.
|
|
4
|
+
const numeric = (value, field) => {
|
|
5
|
+
const number = Number(value);
|
|
6
|
+
if (!Number.isFinite(number) || number < 0) throw new Error(`usage.${field} must be a non-negative number`);
|
|
7
|
+
return number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function normalizeUsage(entry) {
|
|
11
|
+
const meta = entry.meta ?? {};
|
|
12
|
+
const source = meta.usage && typeof meta.usage === 'object' ? meta.usage : meta;
|
|
13
|
+
const hasUsage = entry.action === 'llm.session'
|
|
14
|
+
|| source.tokens !== undefined
|
|
15
|
+
|| source.input_tokens !== undefined
|
|
16
|
+
|| source.output_tokens !== undefined
|
|
17
|
+
|| source.cached_tokens !== undefined
|
|
18
|
+
|| source.cost_usd !== undefined;
|
|
19
|
+
if (!hasUsage) return null;
|
|
20
|
+
const input = source.input_tokens === undefined ? 0 : numeric(source.input_tokens, 'input_tokens');
|
|
21
|
+
const output = source.output_tokens === undefined ? 0 : numeric(source.output_tokens, 'output_tokens');
|
|
22
|
+
const total = source.tokens === undefined ? input + output : numeric(source.tokens, 'tokens');
|
|
23
|
+
return {
|
|
24
|
+
task: entry.task ?? '(unscoped)',
|
|
25
|
+
seq: entry.action === 'llm.session' ? null : entry.seq,
|
|
26
|
+
tokens: total,
|
|
27
|
+
cached_tokens: source.cached_tokens === undefined ? 0 : numeric(source.cached_tokens, 'cached_tokens'),
|
|
28
|
+
cost_usd: source.cost_usd === undefined ? 0 : numeric(source.cost_usd, 'cost_usd'),
|
|
29
|
+
api_calls: source.api_calls === undefined ? 0 : numeric(source.api_calls, 'api_calls'),
|
|
30
|
+
source: entry.action === 'llm.session' ? 'session' : 'call',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function usageForDay(catalog, { day, timeZone }) {
|
|
35
|
+
const tasks = new Map();
|
|
36
|
+
let invalid = 0;
|
|
37
|
+
catalog.scanDay(day, timeZone, (entry) => {
|
|
38
|
+
const task = entry.task ?? '(unscoped)';
|
|
39
|
+
const item = tasks.get(task) ?? {
|
|
40
|
+
task, calls: 0, blocked: 0, tokens: 0, cached_tokens: 0,
|
|
41
|
+
cost_usd: 0, api_calls: 0, measured: false, invalid: 0,
|
|
42
|
+
};
|
|
43
|
+
if (entry.action === 'llm.session') {
|
|
44
|
+
try {
|
|
45
|
+
const usage = normalizeUsage(entry);
|
|
46
|
+
if (usage) {
|
|
47
|
+
item.tokens += usage.tokens;
|
|
48
|
+
item.cached_tokens += usage.cached_tokens;
|
|
49
|
+
item.cost_usd += usage.cost_usd;
|
|
50
|
+
item.api_calls += usage.api_calls;
|
|
51
|
+
item.measured = true;
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
item.invalid += 1;
|
|
55
|
+
invalid += 1;
|
|
56
|
+
}
|
|
57
|
+
tasks.set(task, item);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
item.calls += 1;
|
|
61
|
+
if (entry.decision === 'DENY') item.blocked += 1;
|
|
62
|
+
try {
|
|
63
|
+
const usage = normalizeUsage(entry);
|
|
64
|
+
if (usage) {
|
|
65
|
+
item.tokens += usage.tokens;
|
|
66
|
+
item.cached_tokens += usage.cached_tokens;
|
|
67
|
+
item.cost_usd += usage.cost_usd;
|
|
68
|
+
item.api_calls += usage.api_calls;
|
|
69
|
+
item.measured = true;
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
item.invalid += 1;
|
|
73
|
+
invalid += 1;
|
|
74
|
+
}
|
|
75
|
+
tasks.set(task, item);
|
|
76
|
+
});
|
|
77
|
+
const list = [...tasks.values()].map((item) => ({ ...item, cost_usd: Number(item.cost_usd.toFixed(6)) }));
|
|
78
|
+
const totals = list.reduce((sum, item) => ({
|
|
79
|
+
calls: sum.calls + item.calls,
|
|
80
|
+
blocked: sum.blocked + item.blocked,
|
|
81
|
+
measured_calls: sum.measured_calls + (item.measured ? item.calls : 0),
|
|
82
|
+
tokens: sum.tokens + item.tokens,
|
|
83
|
+
cached_tokens: sum.cached_tokens + item.cached_tokens,
|
|
84
|
+
cost_usd: Number((sum.cost_usd + item.cost_usd).toFixed(6)),
|
|
85
|
+
api_calls: sum.api_calls + item.api_calls,
|
|
86
|
+
}), { calls: 0, blocked: 0, measured_calls: 0, tokens: 0, cached_tokens: 0, cost_usd: 0, api_calls: 0 });
|
|
87
|
+
return {
|
|
88
|
+
day,
|
|
89
|
+
tasks: list,
|
|
90
|
+
totals,
|
|
91
|
+
coverage: totals.calls ? totals.measured_calls / totals.calls : 0,
|
|
92
|
+
invalid,
|
|
93
|
+
generated_at: new Date().toISOString(),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
package/src/watch.mjs
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// A live reader over the ledger. It keeps a byte offset and a partial-line
|
|
2
|
+
// buffer, so watching a large ledger does not reread it on every tick.
|
|
3
|
+
import { closeSync, openSync, readSync, statSync } from 'node:fs';
|
|
4
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
5
|
+
import { parseLine } from './jsonl.mjs';
|
|
6
|
+
import { paths } from './store.mjs';
|
|
7
|
+
|
|
8
|
+
const CHUNK = 64 * 1024;
|
|
9
|
+
|
|
10
|
+
function readRange(file, start, end) {
|
|
11
|
+
const handle = openSync(file, 'r');
|
|
12
|
+
const buffer = Buffer.alloc(end - start);
|
|
13
|
+
let offset = 0;
|
|
14
|
+
try {
|
|
15
|
+
while (offset < buffer.length) {
|
|
16
|
+
const bytes = readSync(handle, buffer, offset, buffer.length - offset, start + offset);
|
|
17
|
+
if (bytes === 0) break;
|
|
18
|
+
offset += bytes;
|
|
19
|
+
}
|
|
20
|
+
} finally {
|
|
21
|
+
closeSync(handle);
|
|
22
|
+
}
|
|
23
|
+
return buffer.subarray(0, offset);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const wait = (ms, signal) =>
|
|
27
|
+
new Promise((resolve) => {
|
|
28
|
+
const done = () => {
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
signal?.removeEventListener('abort', done);
|
|
31
|
+
resolve();
|
|
32
|
+
};
|
|
33
|
+
const timer = setTimeout(done, ms);
|
|
34
|
+
signal?.addEventListener('abort', done, { once: true });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export async function* watchEntries(
|
|
38
|
+
dir,
|
|
39
|
+
{ fromSeq = Number.MAX_SAFE_INTEGER, task, interval = 250, signal } = {},
|
|
40
|
+
) {
|
|
41
|
+
const file = paths(dir).ledger;
|
|
42
|
+
const decoder = new StringDecoder('utf8');
|
|
43
|
+
let buffer = '';
|
|
44
|
+
let offset = 0;
|
|
45
|
+
let index = 0;
|
|
46
|
+
|
|
47
|
+
const parse = () => {
|
|
48
|
+
const entries = [];
|
|
49
|
+
let newline;
|
|
50
|
+
while ((newline = buffer.indexOf('\n')) !== -1) {
|
|
51
|
+
const line = buffer.slice(0, newline);
|
|
52
|
+
buffer = buffer.slice(newline + 1);
|
|
53
|
+
index += 1;
|
|
54
|
+
const entry = parseLine(line, index);
|
|
55
|
+
if ((task === undefined || entry.task === task) && entry.seq > fromSeq) entries.push(entry);
|
|
56
|
+
}
|
|
57
|
+
return entries;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
let size = statSync(file).size;
|
|
61
|
+
while (offset < size) {
|
|
62
|
+
const end = Math.min(offset + CHUNK, size);
|
|
63
|
+
buffer += decoder.write(readRange(file, offset, end));
|
|
64
|
+
offset = end;
|
|
65
|
+
for (const entry of parse()) yield entry;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
while (!signal?.aborted) {
|
|
69
|
+
await wait(interval, signal);
|
|
70
|
+
if (signal?.aborted) return;
|
|
71
|
+
size = statSync(file).size;
|
|
72
|
+
if (size < offset) throw new Error('the ledger shrank while watch was reading it');
|
|
73
|
+
if (size === offset) continue;
|
|
74
|
+
buffer += decoder.write(readRange(file, offset, size));
|
|
75
|
+
offset = size;
|
|
76
|
+
for (const entry of parse()) yield entry;
|
|
77
|
+
}
|
|
78
|
+
}
|