cgraphx 1.1.0 → 1.2.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 +0 -1
- package/dist/.claude-template/skills/cgraphx/SKILL.md +3 -3
- package/dist/.claude-template/skills/cgraphx/agent-prompt.md +1 -1
- package/dist/.claude-template/skills/cgraphx-guide/SKILL.md +94 -0
- package/dist/.claude-template/skills/cgraphx-guide/how-to-use.html +403 -0
- package/dist/.claude-template/skills/clarify-requirements/SKILL.md +19 -8
- package/dist/.claude-template/skills/code-impact-docgen/SKILL.md +186 -176
- package/dist/.claude-template/skills/code-impact-docgen/template-design-html.md +357 -0
- package/dist/.claude-template/skills/code-impact-docgen/template-design-md.md +164 -0
- package/dist/.claude-template/skills/code-impact-init/SKILL.md +47 -47
- package/dist/.claude-template/skills/developer-timeline/SKILL.md +9 -0
- package/dist/.claude-template/skills/write-api-doc/SKILL.md +317 -0
- package/dist/.claude-template/skills/write-api-doc/template-api-html.md +422 -0
- package/dist/.claude-template/skills/write-plan/SKILL.md +38 -16
- package/dist/.claude-template/skills/write-prd/SKILL.md +32 -8
- package/dist/.claude-template/skills/write-spec/SKILL.md +34 -9
- package/dist/bin/codegraph.js +0 -100
- package/dist/bin/codegraph.js.map +1 -1
- package/dist/resolution/index.d.ts.map +1 -1
- package/dist/resolution/index.js +13 -0
- package/dist/resolution/index.js.map +1 -1
- package/dist/resolution/scope-index.d.ts +86 -0
- package/dist/resolution/scope-index.d.ts.map +1 -0
- package/dist/resolution/scope-index.js +143 -0
- package/dist/resolution/scope-index.js.map +1 -0
- package/dist/resolution/stdlib-blocklist.d.ts +53 -0
- package/dist/resolution/stdlib-blocklist.d.ts.map +1 -0
- package/dist/resolution/stdlib-blocklist.js +143 -0
- package/dist/resolution/stdlib-blocklist.js.map +1 -0
- package/dist/search/ast-helpers.d.ts +42 -0
- package/dist/search/ast-helpers.d.ts.map +1 -0
- package/dist/search/ast-helpers.js +106 -0
- package/dist/search/ast-helpers.js.map +1 -0
- package/dist/search/call-sites.d.ts +398 -0
- package/dist/search/call-sites.d.ts.map +1 -0
- package/dist/search/call-sites.js +1433 -0
- package/dist/search/call-sites.js.map +1 -0
- package/dist/search/context.d.ts +134 -0
- package/dist/search/context.d.ts.map +1 -0
- package/dist/search/context.js +575 -0
- package/dist/search/context.js.map +1 -0
- package/dist/search/impact.d.ts +139 -0
- package/dist/search/impact.d.ts.map +1 -0
- package/dist/search/impact.js +646 -0
- package/dist/search/impact.js.map +1 -0
- package/dist/search/related.d.ts +178 -0
- package/dist/search/related.d.ts.map +1 -0
- package/dist/search/related.js +667 -0
- package/dist/search/related.js.map +1 -0
- package/dist/search/slice.d.ts +148 -0
- package/dist/search/slice.d.ts.map +1 -0
- package/dist/search/slice.js +460 -0
- package/dist/search/slice.js.map +1 -0
- package/dist/search/snr-constants.d.ts +41 -0
- package/dist/search/snr-constants.d.ts.map +1 -0
- package/dist/search/snr-constants.js +44 -0
- package/dist/search/snr-constants.js.map +1 -0
- package/dist/search/types.d.ts +28 -0
- package/dist/search/types.d.ts.map +1 -0
- package/dist/search/types.js +12 -0
- package/dist/search/types.js.map +1 -0
- package/dist/timeline/cli.d.ts.map +1 -1
- package/dist/timeline/cli.js +22 -3
- package/dist/timeline/cli.js.map +1 -1
- package/dist/timeline/store.d.ts +5 -0
- package/dist/timeline/store.d.ts.map +1 -1
- package/dist/timeline/store.js +23 -3
- package/dist/timeline/store.js.map +1 -1
- package/package.json +1 -1
- package/scripts/agent-eval/block-cgraphx-and-gitnexus-cli-hook.sh +43 -0
- package/scripts/agent-eval/block-cgraphx-cli-hook.sh +32 -0
- package/scripts/agent-eval/block-cgraphx-cli-settings.json +16 -0
- package/scripts/agent-eval/cli-vs-mcp-3arm.sh +121 -0
- package/scripts/agent-eval/multi-tool-eval.sh +171 -0
- package/scripts/agent-eval/parse-cli-vs-mcp.mjs +232 -0
- package/scripts/agent-eval/parse-multi-tool.mjs +242 -0
- package/scripts/agent-eval/subagent-token-cost.py +188 -0
- package/dist/.claude-template/skills/code-impact-docgen/template-business-html.md +0 -242
- package/dist/.claude-template/skills/code-impact-docgen/template-business-md.md +0 -107
- package/dist/.claude-template/skills/code-impact-docgen/template-technical-html.md +0 -205
- package/dist/.claude-template/skills/code-impact-docgen/template-technical-md.md +0 -155
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Parse multi-tool-eval transcripts (3 arms × 3 tasks × n reps).
|
|
3
|
+
//
|
|
4
|
+
// File-name contract: <repo>-<task>-<arm>-<rep>.jsonl
|
|
5
|
+
// task ∈ {flow, onboarding, impact}
|
|
6
|
+
// arm ∈ {cgraphx-cli, cgraphx-mcp, gitnexus-mcp}
|
|
7
|
+
//
|
|
8
|
+
// Output:
|
|
9
|
+
// 1. <out-dir>/results.jsonl — one JSON line per transcript
|
|
10
|
+
// 2. stdout:
|
|
11
|
+
// - PER TASK × ARM × REP (detail)
|
|
12
|
+
// - AGGREGATE PER TASK × ARM (mean across reps)
|
|
13
|
+
// - AGGREGATE PER ARM (mean across task × reps — overall tool ranking)
|
|
14
|
+
import { readFileSync, readdirSync, existsSync, writeFileSync } from 'fs';
|
|
15
|
+
import { join } from 'path';
|
|
16
|
+
|
|
17
|
+
const OUT = process.argv[2] || '/tmp/cg-multi-tool';
|
|
18
|
+
const RUNS_DIR = join(OUT, 'runs');
|
|
19
|
+
const RESULTS = join(OUT, 'results.jsonl');
|
|
20
|
+
|
|
21
|
+
if (!existsSync(RUNS_DIR)) {
|
|
22
|
+
console.error(`no runs dir at ${RUNS_DIR}`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parse(file) {
|
|
27
|
+
const lines = readFileSync(file, 'utf8').split('\n').filter(Boolean);
|
|
28
|
+
const toolCounts = {};
|
|
29
|
+
const cliCommands = { cgraphx: [], gitnexus: [] };
|
|
30
|
+
const hookDenies = { cgraphx: 0, gitnexus: 0 };
|
|
31
|
+
let result = null;
|
|
32
|
+
|
|
33
|
+
for (const line of lines) {
|
|
34
|
+
let ev;
|
|
35
|
+
try { ev = JSON.parse(line); } catch { continue; }
|
|
36
|
+
|
|
37
|
+
if (ev.type === 'assistant' && Array.isArray(ev.message?.content)) {
|
|
38
|
+
for (const b of ev.message.content) {
|
|
39
|
+
if (b.type !== 'tool_use') continue;
|
|
40
|
+
toolCounts[b.name] = (toolCounts[b.name] || 0) + 1;
|
|
41
|
+
if (b.name === 'Bash' && typeof b.input?.command === 'string') {
|
|
42
|
+
const cmd = b.input.command;
|
|
43
|
+
if (/^cgraphx /.test(cmd) || / cgraphx /.test(cmd) || /codegraph\.js/.test(cmd)) {
|
|
44
|
+
cliCommands.cgraphx.push(cmd.slice(0, 80));
|
|
45
|
+
}
|
|
46
|
+
if (/gitnexus\/dist\/cli\/index\.js/.test(cmd) || /gitnexus\/run\.cjs/.test(cmd) || /^gitnexus /.test(cmd) || / gitnexus /.test(cmd)) {
|
|
47
|
+
cliCommands.gitnexus.push(cmd.slice(0, 80));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (ev.type === 'user' && Array.isArray(ev.message?.content)) {
|
|
54
|
+
for (const b of ev.message.content) {
|
|
55
|
+
if (b.type !== 'tool_result') continue;
|
|
56
|
+
const text = typeof b.content === 'string'
|
|
57
|
+
? b.content
|
|
58
|
+
: Array.isArray(b.content)
|
|
59
|
+
? b.content.map(c => (typeof c === 'string' ? c : c?.text || '')).join('')
|
|
60
|
+
: '';
|
|
61
|
+
if (/禁用 cgraphx CLI/.test(text)) hookDenies.cgraphx++;
|
|
62
|
+
if (/禁用 gitnexus CLI/.test(text)) hookDenies.gitnexus++;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (ev.type === 'result') result = ev;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let skillCgraphx = 0, skillGitnexus = 0;
|
|
70
|
+
for (const line of lines) {
|
|
71
|
+
let ev;
|
|
72
|
+
try { ev = JSON.parse(line); } catch { continue; }
|
|
73
|
+
if (ev.type === 'assistant' && Array.isArray(ev.message?.content)) {
|
|
74
|
+
for (const b of ev.message.content) {
|
|
75
|
+
if (b.type !== 'tool_use' || b.name !== 'Skill') continue;
|
|
76
|
+
const s = b.input?.skill || '';
|
|
77
|
+
if (s === 'cgraphx-search' || s === 'cgraphx') skillCgraphx++;
|
|
78
|
+
if (s.startsWith('gitnexus')) skillGitnexus++;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const u = result?.usage || {};
|
|
84
|
+
const tokGen = u.output_tokens || 0;
|
|
85
|
+
const tokFresh = (u.input_tokens || 0) + (u.cache_creation_input_tokens || 0);
|
|
86
|
+
const tokCached = u.cache_read_input_tokens || 0;
|
|
87
|
+
|
|
88
|
+
const get = (n) => toolCounts[n] || 0;
|
|
89
|
+
const mcpCalls = Object.keys(toolCounts)
|
|
90
|
+
.filter(k => /^mcp__(cgraphx|gitnexus)__/.test(k))
|
|
91
|
+
.reduce((s, k) => s + toolCounts[k], 0);
|
|
92
|
+
// Per-tool MCP breakdown
|
|
93
|
+
const mcpByServer = {};
|
|
94
|
+
for (const k of Object.keys(toolCounts)) {
|
|
95
|
+
const m = k.match(/^mcp__(cgraphx|gitnexus)__/);
|
|
96
|
+
if (m) mcpByServer[m[1]] = (mcpByServer[m[1]] || 0) + toolCounts[k];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
read: get('Read'),
|
|
101
|
+
grep: get('Grep') + get('Glob'),
|
|
102
|
+
bash: get('Bash'),
|
|
103
|
+
cliCallsCgraphx: cliCommands.cgraphx.length,
|
|
104
|
+
cliCallsGitnexus: cliCommands.gitnexus.length,
|
|
105
|
+
mcpCalls,
|
|
106
|
+
mcpCgraphx: mcpByServer.cgraphx || 0,
|
|
107
|
+
mcpGitnexus: mcpByServer.gitnexus || 0,
|
|
108
|
+
skillCgraphx,
|
|
109
|
+
skillGitnexus,
|
|
110
|
+
hookDeniesCgraphx: hookDenies.cgraphx,
|
|
111
|
+
hookDeniesGitnexus: hookDenies.gitnexus,
|
|
112
|
+
durationSec: result ? Math.floor(result.duration_ms / 1000) : null,
|
|
113
|
+
numTurns: result?.num_turns ?? null,
|
|
114
|
+
costUsdMain: result ? +(result.total_cost_usd || 0).toFixed(4) : null,
|
|
115
|
+
tokGen,
|
|
116
|
+
tokFresh,
|
|
117
|
+
tokCached,
|
|
118
|
+
tokBillable: tokGen + tokFresh,
|
|
119
|
+
ok: result?.subtype === 'success',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// task -> arm -> [runs]
|
|
124
|
+
const data = {};
|
|
125
|
+
const allMetrics = [];
|
|
126
|
+
for (const f of readdirSync(RUNS_DIR)) {
|
|
127
|
+
const m = f.match(/^(.+)-(\w+)-(\w+-\w+)-(\d+)\.jsonl$/);
|
|
128
|
+
if (!m) continue;
|
|
129
|
+
const [, repo, task, arm, repStr] = m;
|
|
130
|
+
const parsed = parse(join(RUNS_DIR, f));
|
|
131
|
+
const rep = Number(repStr);
|
|
132
|
+
const record = { repo, task, arm, rep, file: f, ...parsed };
|
|
133
|
+
allMetrics.push(record);
|
|
134
|
+
(((data[task] ??= {})[arm]) ??= []).push(record);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
writeFileSync(RESULTS, allMetrics
|
|
138
|
+
.sort((a, b) => a.task.localeCompare(b.task) || a.arm.localeCompare(b.arm) || a.rep - b.rep)
|
|
139
|
+
.map(r => JSON.stringify(r))
|
|
140
|
+
.join('\n') + '\n'
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const pad = (s, n) => String(s).padEnd(n);
|
|
144
|
+
const TASKS = ['flow', 'onboarding', 'impact'];
|
|
145
|
+
const ARMS = ['cgraphx-cli', 'cgraphx-mcp', 'gitnexus-mcp'];
|
|
146
|
+
const LABEL = {
|
|
147
|
+
'cgraphx-cli': 'A1 cgraphx-cli',
|
|
148
|
+
'cgraphx-mcp': 'A2 cgraphx-mcp',
|
|
149
|
+
'gitnexus-mcp': 'A3 gitnexus-mcp',
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// ---- PER TASK × ARM × REP ----
|
|
153
|
+
console.log('\n=== PER TASK × ARM × REP (detail) ===');
|
|
154
|
+
console.log(
|
|
155
|
+
pad('task', 11), pad('arm', 18), pad('rep', 4),
|
|
156
|
+
pad('dur', 5), pad('read', 5), pad('grep', 5), pad('bash', 5),
|
|
157
|
+
pad('cgCli', 5), pad('gnCli', 5),
|
|
158
|
+
pad('cgMcp', 5), pad('gnMcp', 5),
|
|
159
|
+
pad('deny', 5),
|
|
160
|
+
pad('tokBill', 8), pad('cost', 8), 'ok'
|
|
161
|
+
);
|
|
162
|
+
for (const task of TASKS) {
|
|
163
|
+
for (const arm of ARMS) {
|
|
164
|
+
const runs = data[task]?.[arm] || [];
|
|
165
|
+
for (const r of runs) {
|
|
166
|
+
console.log(
|
|
167
|
+
pad(task, 11), pad(LABEL[arm], 18), pad(r.rep, 4),
|
|
168
|
+
pad(r.durationSec ?? '?', 5),
|
|
169
|
+
pad(r.read, 5), pad(r.grep, 5), pad(r.bash, 5),
|
|
170
|
+
pad(r.cliCallsCgraphx, 5), pad(r.cliCallsGitnexus, 5),
|
|
171
|
+
pad(r.mcpCgraphx, 5), pad(r.mcpGitnexus, 5),
|
|
172
|
+
pad(r.hookDeniesCgraphx + r.hookDeniesGitnexus, 5),
|
|
173
|
+
pad(r.tokBillable, 8),
|
|
174
|
+
pad('$' + (r.costUsdMain ?? 0).toFixed(4), 8),
|
|
175
|
+
r.ok ? '✓' : '✗'
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const avg = (a, f) => a.length ? a.reduce((s, x) => s + (f(x) || 0), 0) / a.length : 0;
|
|
182
|
+
|
|
183
|
+
// ---- AGGREGATE PER TASK × ARM ----
|
|
184
|
+
console.log('\n=== AGGREGATE PER TASK × ARM (mean across reps) ===');
|
|
185
|
+
console.log(
|
|
186
|
+
pad('task', 11), pad('arm', 18),
|
|
187
|
+
pad('read', 6), pad('grep', 6), pad('bash', 6),
|
|
188
|
+
pad('cgCli', 5), pad('gnCli', 5),
|
|
189
|
+
pad('cgMcp', 5), pad('gnMcp', 5),
|
|
190
|
+
pad('dur', 6), pad('tokBill', 8), pad('cost', 8), 'n'
|
|
191
|
+
);
|
|
192
|
+
for (const task of TASKS) {
|
|
193
|
+
for (const arm of ARMS) {
|
|
194
|
+
const runs = data[task]?.[arm] || [];
|
|
195
|
+
if (!runs.length) continue;
|
|
196
|
+
console.log(
|
|
197
|
+
pad(task, 11), pad(LABEL[arm], 18),
|
|
198
|
+
pad(avg(runs, r => r.read).toFixed(1), 6),
|
|
199
|
+
pad(avg(runs, r => r.grep).toFixed(1), 6),
|
|
200
|
+
pad(avg(runs, r => r.bash).toFixed(1), 6),
|
|
201
|
+
pad(avg(runs, r => r.cliCallsCgraphx).toFixed(1), 5),
|
|
202
|
+
pad(avg(runs, r => r.cliCallsGitnexus).toFixed(1), 5),
|
|
203
|
+
pad(avg(runs, r => r.mcpCgraphx).toFixed(1), 5),
|
|
204
|
+
pad(avg(runs, r => r.mcpGitnexus).toFixed(1), 5),
|
|
205
|
+
pad(avg(runs, r => r.durationSec).toFixed(0) + 's', 6),
|
|
206
|
+
pad(avg(runs, r => r.tokBillable).toFixed(0), 8),
|
|
207
|
+
pad('$' + avg(runs, r => r.costUsdMain).toFixed(4), 8),
|
|
208
|
+
runs.length
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ---- AGGREGATE PER ARM (across all tasks × reps) ----
|
|
214
|
+
console.log('\n=== AGGREGATE PER ARM (mean across all tasks × reps) ===');
|
|
215
|
+
console.log(
|
|
216
|
+
pad('arm', 18),
|
|
217
|
+
pad('read', 6), pad('grep', 6), pad('bash', 6),
|
|
218
|
+
pad('cgCli', 5), pad('gnCli', 5),
|
|
219
|
+
pad('cgMcp', 5), pad('gnMcp', 5),
|
|
220
|
+
pad('dur', 6), pad('tokBill', 8), pad('cost', 8), 'n'
|
|
221
|
+
);
|
|
222
|
+
for (const arm of ARMS) {
|
|
223
|
+
const all = [];
|
|
224
|
+
for (const task of TASKS) for (const r of (data[task]?.[arm] || [])) all.push(r);
|
|
225
|
+
if (!all.length) continue;
|
|
226
|
+
console.log(
|
|
227
|
+
pad(LABEL[arm], 18),
|
|
228
|
+
pad(avg(all, r => r.read).toFixed(1), 6),
|
|
229
|
+
pad(avg(all, r => r.grep).toFixed(1), 6),
|
|
230
|
+
pad(avg(all, r => r.bash).toFixed(1), 6),
|
|
231
|
+
pad(avg(all, r => r.cliCallsCgraphx).toFixed(1), 5),
|
|
232
|
+
pad(avg(all, r => r.cliCallsGitnexus).toFixed(1), 5),
|
|
233
|
+
pad(avg(all, r => r.mcpCgraphx).toFixed(1), 5),
|
|
234
|
+
pad(avg(all, r => r.mcpGitnexus).toFixed(1), 5),
|
|
235
|
+
pad(avg(all, r => r.durationSec).toFixed(0) + 's', 6),
|
|
236
|
+
pad(avg(all, r => r.tokBillable).toFixed(0), 8),
|
|
237
|
+
pad('$' + avg(all, r => r.costUsdMain).toFixed(4), 8),
|
|
238
|
+
all.length
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
console.log(`\nresults.jsonl: ${RESULTS}`);
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Measure real API token usage and $ cost from Claude Code subagent transcripts.
|
|
3
|
+
|
|
4
|
+
Reads the `.output` JSONL file Claude Code writes per background subagent
|
|
5
|
+
(the `output_file` returned by the Agent tool), sums the `usage` field on
|
|
6
|
+
each assistant message, and applies Sonnet list pricing to get a cache-aware
|
|
7
|
+
$ cost.
|
|
8
|
+
|
|
9
|
+
Why this exists: tool-output chars (the easy thing to count by eye) is a
|
|
10
|
+
poor proxy for real cost. The dominant cost driver in multi-turn sessions is
|
|
11
|
+
`cache_read_input_tokens` — every turn re-sends the growing conversation as
|
|
12
|
+
cached input, billed at ~10% of full input cost. A tool that returns tiny
|
|
13
|
+
output but forces many turns (grep) is more expensive than a tool that
|
|
14
|
+
returns rich output in few turns (codegraph_explore). chars hides this;
|
|
15
|
+
`usage` + $ pricing exposes it.
|
|
16
|
+
|
|
17
|
+
See docs/features/2026-07-01-calls-command/abac-comparison.md for the case
|
|
18
|
+
study that motivated this script.
|
|
19
|
+
|
|
20
|
+
Pricing (Sonnet, per million tokens):
|
|
21
|
+
input $3.00
|
|
22
|
+
output $15.00
|
|
23
|
+
cache_read $0.30 (10% of input)
|
|
24
|
+
cache_creation $3.75 (125% of input — first-time cache write)
|
|
25
|
+
|
|
26
|
+
Usage:
|
|
27
|
+
python3 subagent-token-cost.py <agent-id> [<agent-id> ...]
|
|
28
|
+
python3 subagent-token-cost.py --dir <tasks-dir> # all .output files
|
|
29
|
+
|
|
30
|
+
The agent-id form looks up `<tasks-dir>/<agent-id>.output` under the standard
|
|
31
|
+
session tasks directory. Pass `--dir` to point at a different directory
|
|
32
|
+
(e.g. when comparing across sessions).
|
|
33
|
+
|
|
34
|
+
Output: one line per arm with turns, tool_uses, input/output/cache_read/
|
|
35
|
+
cache_write token counts, raw total, and $ cost. Sorted in input order.
|
|
36
|
+
|
|
37
|
+
Notes:
|
|
38
|
+
- This reads ONLY the `usage` field and tool_use block counts — never message
|
|
39
|
+
content. Safe to run on large transcripts; won't overflow your context.
|
|
40
|
+
- For main-session cost (not subagent), use offload-eval-cost.mjs instead,
|
|
41
|
+
which reads the structured eval-runs format and uses Claude Code's own
|
|
42
|
+
authoritative costUSD accounting from the `result` event.
|
|
43
|
+
"""
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import argparse
|
|
47
|
+
import json
|
|
48
|
+
import os
|
|
49
|
+
import sys
|
|
50
|
+
from pathlib import Path
|
|
51
|
+
|
|
52
|
+
# Sonnet list pricing, per million tokens.
|
|
53
|
+
PRICE_INPUT_PER_M = 3.00
|
|
54
|
+
PRICE_OUTPUT_PER_M = 15.00
|
|
55
|
+
PRICE_CACHE_READ_PER_M = 0.30
|
|
56
|
+
PRICE_CACHE_WRITE_PER_M = 3.75
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def default_tasks_dir() -> Path:
|
|
60
|
+
"""Best-effort default for this session's tasks dir."""
|
|
61
|
+
# Claude Code writes subagent .output files under a session-scoped tasks dir.
|
|
62
|
+
# The exact path varies; common locations:
|
|
63
|
+
candidates = [
|
|
64
|
+
Path('/private/tmp/claude-501'),
|
|
65
|
+
Path.home() / '.claude' / 'tasks',
|
|
66
|
+
]
|
|
67
|
+
for c in candidates:
|
|
68
|
+
if c.exists():
|
|
69
|
+
# Find the most recently modified session dir
|
|
70
|
+
session_dirs = sorted(
|
|
71
|
+
c.glob('*-Users-*'), key=lambda p: p.stat().st_mtime, reverse=True
|
|
72
|
+
)
|
|
73
|
+
for sd in session_dirs:
|
|
74
|
+
tasks = sd / 'tasks'
|
|
75
|
+
if tasks.exists():
|
|
76
|
+
return tasks
|
|
77
|
+
return Path('/private/tmp/claude-501/unknown-session/tasks')
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def measure_file(path: Path) -> dict | None:
|
|
81
|
+
if not path.exists():
|
|
82
|
+
return None
|
|
83
|
+
totals = {'input': 0, 'output': 0, 'cache_read': 0, 'cache_creation': 0}
|
|
84
|
+
assistant_turns = 0
|
|
85
|
+
tool_uses = 0
|
|
86
|
+
with path.open() as f:
|
|
87
|
+
for line in f:
|
|
88
|
+
line = line.strip()
|
|
89
|
+
if not line:
|
|
90
|
+
continue
|
|
91
|
+
try:
|
|
92
|
+
obj = json.loads(line)
|
|
93
|
+
except json.JSONDecodeError:
|
|
94
|
+
continue
|
|
95
|
+
msg = obj.get('message') or {}
|
|
96
|
+
if msg.get('role') != 'assistant':
|
|
97
|
+
continue
|
|
98
|
+
usage = msg.get('usage') or {}
|
|
99
|
+
if not usage:
|
|
100
|
+
continue
|
|
101
|
+
assistant_turns += 1
|
|
102
|
+
totals['input'] += usage.get('input_tokens', 0) or 0
|
|
103
|
+
totals['output'] += usage.get('output_tokens', 0) or 0
|
|
104
|
+
totals['cache_read'] += usage.get('cache_read_input_tokens', 0) or 0
|
|
105
|
+
totals['cache_creation'] += usage.get('cache_creation_input_tokens', 0) or 0
|
|
106
|
+
content = msg.get('content') or []
|
|
107
|
+
if isinstance(content, list):
|
|
108
|
+
for block in content:
|
|
109
|
+
if isinstance(block, dict) and block.get('type') == 'tool_use':
|
|
110
|
+
tool_uses += 1
|
|
111
|
+
cost = (
|
|
112
|
+
totals['input'] * PRICE_INPUT_PER_M
|
|
113
|
+
+ totals['output'] * PRICE_OUTPUT_PER_M
|
|
114
|
+
+ totals['cache_read'] * PRICE_CACHE_READ_PER_M
|
|
115
|
+
+ totals['cache_creation'] * PRICE_CACHE_WRITE_PER_M
|
|
116
|
+
) / 1_000_000
|
|
117
|
+
return {
|
|
118
|
+
'turns': assistant_turns,
|
|
119
|
+
'tool_uses': tool_uses,
|
|
120
|
+
'totals': totals,
|
|
121
|
+
'all_tokens': sum(totals.values()),
|
|
122
|
+
'cost_usd': cost,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def main() -> int:
|
|
127
|
+
ap = argparse.ArgumentParser(description=__doc__.split('\n')[0])
|
|
128
|
+
ap.add_argument('agent_ids', nargs='*', help='subagent IDs to measure')
|
|
129
|
+
ap.add_argument(
|
|
130
|
+
'--dir',
|
|
131
|
+
help='tasks directory containing <id>.output files (default: auto-detect latest session)',
|
|
132
|
+
)
|
|
133
|
+
ap.add_argument(
|
|
134
|
+
'--label',
|
|
135
|
+
action='append',
|
|
136
|
+
default=[],
|
|
137
|
+
help='label for an agent, in the same order as agent_ids (repeatable)',
|
|
138
|
+
)
|
|
139
|
+
args = ap.parse_args()
|
|
140
|
+
|
|
141
|
+
tasks_dir = Path(args.dir) if args.dir else default_tasks_dir()
|
|
142
|
+
|
|
143
|
+
if not args.agent_ids:
|
|
144
|
+
# Measure every .output file in the tasks dir
|
|
145
|
+
files = sorted(tasks_dir.glob('*.output'))
|
|
146
|
+
if not files:
|
|
147
|
+
print(f'no .output files in {tasks_dir}', file=sys.stderr)
|
|
148
|
+
return 1
|
|
149
|
+
targets = [(f.stem, f) for f in files]
|
|
150
|
+
else:
|
|
151
|
+
targets = [(aid, tasks_dir / f'{aid}.output') for aid in args.agent_ids]
|
|
152
|
+
|
|
153
|
+
rows = []
|
|
154
|
+
for i, (label, path) in enumerate(targets):
|
|
155
|
+
if args.label and i < len(args.label):
|
|
156
|
+
label = args.label[i]
|
|
157
|
+
r = measure_file(path)
|
|
158
|
+
if r is None:
|
|
159
|
+
print(f'{label}: transcript not found at {path}', file=sys.stderr)
|
|
160
|
+
continue
|
|
161
|
+
r['label'] = label
|
|
162
|
+
rows.append(r)
|
|
163
|
+
|
|
164
|
+
if not rows:
|
|
165
|
+
return 1
|
|
166
|
+
|
|
167
|
+
header = (
|
|
168
|
+
f'{"Arm":<32} {"turns":>6} {"tools":>6} {"input":>10} {"output":>10} '
|
|
169
|
+
f'{"cache_r":>10} {"cache_w":>10} {"total_tok":>11} {"$ cost":>9}'
|
|
170
|
+
)
|
|
171
|
+
print(header)
|
|
172
|
+
print('-' * len(header))
|
|
173
|
+
for r in rows:
|
|
174
|
+
t = r['totals']
|
|
175
|
+
print(
|
|
176
|
+
f'{r["label"]:<32} {r["turns"]:>6} {r["tool_uses"]:>6} {t["input"]:>10,} '
|
|
177
|
+
f'{t["output"]:>10,} {t["cache_read"]:>10,} {t["cache_creation"]:>10,} '
|
|
178
|
+
f'{r["all_tokens"]:>11,} ${r["cost_usd"]:>8.4f}'
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
print()
|
|
182
|
+
print('Pricing: Sonnet list. input $3/M, output $15/M, cache_read $0.30/M, cache_write $3.75/M.')
|
|
183
|
+
print('total_tok = raw token sum (ignores cache discount). $ cost = cache-aware (what actually bills).')
|
|
184
|
+
return 0
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
if __name__ == '__main__':
|
|
188
|
+
sys.exit(main())
|
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
# Business 文档 HTML 模板
|
|
2
|
-
|
|
3
|
-
适用于面向业务读者的 HTML 格式文档。
|
|
4
|
-
|
|
5
|
-
生成前阅读此文件获取完整的 CSS 样式和章节结构参考。
|
|
6
|
-
|
|
7
|
-
## 完整 HTML 结构
|
|
8
|
-
|
|
9
|
-
```html
|
|
10
|
-
<!DOCTYPE html>
|
|
11
|
-
<html lang="zh-CN">
|
|
12
|
-
<head>
|
|
13
|
-
<meta charset="UTF-8">
|
|
14
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
15
|
-
<title>{文档标题} — {项目名称}</title>
|
|
16
|
-
<style>
|
|
17
|
-
:root {
|
|
18
|
-
--bg: #ffffff;
|
|
19
|
-
--surface: #f8f9fa;
|
|
20
|
-
--text: #1d1d1f;
|
|
21
|
-
--text-secondary: #6e6e73;
|
|
22
|
-
--accent: #0071e3;
|
|
23
|
-
--accent-light: #e8f0fe;
|
|
24
|
-
--border: #d2d2d7;
|
|
25
|
-
--code-bg: #f5f5f7;
|
|
26
|
-
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
27
|
-
--font-mono: "SF Mono", "Fira Code", "Cascadia Code", "Source Code Pro", monospace;
|
|
28
|
-
--max-width: 800px;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
|
32
|
-
|
|
33
|
-
body {
|
|
34
|
-
font-family: var(--font-sans);
|
|
35
|
-
font-size: 16px;
|
|
36
|
-
line-height: 1.7;
|
|
37
|
-
color: var(--text);
|
|
38
|
-
background: var(--bg);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
.container {
|
|
42
|
-
max-width: var(--max-width);
|
|
43
|
-
margin: 0 auto;
|
|
44
|
-
padding: 2rem 1.5rem;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
h1 {
|
|
48
|
-
font-size: 2rem;
|
|
49
|
-
font-weight: 700;
|
|
50
|
-
letter-spacing: -0.02em;
|
|
51
|
-
margin-bottom: 0.5em;
|
|
52
|
-
padding-bottom: 0.5em;
|
|
53
|
-
border-bottom: 2px solid var(--border);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
.meta {
|
|
57
|
-
color: var(--text-secondary);
|
|
58
|
-
font-size: 0.875rem;
|
|
59
|
-
margin-bottom: 2em;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
h2 {
|
|
63
|
-
font-size: 1.5rem;
|
|
64
|
-
font-weight: 600;
|
|
65
|
-
margin-top: 2.5em;
|
|
66
|
-
margin-bottom: 0.75em;
|
|
67
|
-
padding-bottom: 0.3em;
|
|
68
|
-
border-bottom: 1px solid var(--border);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
h3 {
|
|
72
|
-
font-size: 1.2rem;
|
|
73
|
-
font-weight: 600;
|
|
74
|
-
margin-top: 1.5em;
|
|
75
|
-
margin-bottom: 0.5em;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
p { margin-bottom: 1em; }
|
|
79
|
-
|
|
80
|
-
ul, ol {
|
|
81
|
-
margin-bottom: 1em;
|
|
82
|
-
padding-left: 1.5em;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
li { margin-bottom: 0.3em; }
|
|
86
|
-
|
|
87
|
-
strong { font-weight: 600; }
|
|
88
|
-
|
|
89
|
-
code {
|
|
90
|
-
font-family: var(--font-mono);
|
|
91
|
-
background: var(--code-bg);
|
|
92
|
-
padding: 0.15em 0.4em;
|
|
93
|
-
border-radius: 4px;
|
|
94
|
-
font-size: 0.9em;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
pre {
|
|
98
|
-
background: var(--code-bg);
|
|
99
|
-
border: 1px solid var(--border);
|
|
100
|
-
border-radius: 8px;
|
|
101
|
-
padding: 1em;
|
|
102
|
-
margin-bottom: 1em;
|
|
103
|
-
overflow-x: auto;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
pre code {
|
|
107
|
-
background: none;
|
|
108
|
-
padding: 0;
|
|
109
|
-
font-size: 0.875em;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
table {
|
|
113
|
-
width: 100%;
|
|
114
|
-
border-collapse: collapse;
|
|
115
|
-
margin-bottom: 1em;
|
|
116
|
-
font-size: 0.95em;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
th, td {
|
|
120
|
-
padding: 0.6em 1em;
|
|
121
|
-
text-align: left;
|
|
122
|
-
border-bottom: 1px solid var(--border);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
thead th {
|
|
126
|
-
font-weight: 600;
|
|
127
|
-
color: var(--accent);
|
|
128
|
-
background: var(--surface);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
tbody tr:nth-child(even) { background: var(--surface); }
|
|
132
|
-
|
|
133
|
-
.toc {
|
|
134
|
-
background: var(--surface);
|
|
135
|
-
border: 1px solid var(--border);
|
|
136
|
-
border-radius: 8px;
|
|
137
|
-
padding: 1em 1.5em;
|
|
138
|
-
margin-bottom: 2em;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
.toc h3 {
|
|
142
|
-
margin-top: 0;
|
|
143
|
-
margin-bottom: 0.5em;
|
|
144
|
-
font-size: 1rem;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
.toc ul { list-style: none; padding-left: 0; }
|
|
148
|
-
.toc li { margin-bottom: 0.25em; }
|
|
149
|
-
.toc a { color: var(--accent); text-decoration: none; }
|
|
150
|
-
.toc a:hover { text-decoration: underline; }
|
|
151
|
-
.toc ul ul { padding-left: 1.5em; margin-top: 0.25em; }
|
|
152
|
-
|
|
153
|
-
.callout {
|
|
154
|
-
background: var(--accent-light);
|
|
155
|
-
border-left: 4px solid var(--accent);
|
|
156
|
-
padding: 0.75em 1em;
|
|
157
|
-
border-radius: 0 8px 8px 0;
|
|
158
|
-
margin-bottom: 1em;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
@media print {
|
|
162
|
-
body { font-size: 12pt; }
|
|
163
|
-
.container { max-width: 100%; padding: 0; }
|
|
164
|
-
.toc { break-after: page; }
|
|
165
|
-
h2 { break-after: avoid; }
|
|
166
|
-
}
|
|
167
|
-
</style>
|
|
168
|
-
</head>
|
|
169
|
-
<body>
|
|
170
|
-
<div class="container">
|
|
171
|
-
<h1>{文档标题}</h1>
|
|
172
|
-
<p class="meta">生成日期:YYYY-MM-DD | 主题范围:{范围描述}</p>
|
|
173
|
-
|
|
174
|
-
<nav class="toc">
|
|
175
|
-
<h3>目录</h3>
|
|
176
|
-
<ul>
|
|
177
|
-
<li><a href="#背景与目标">背景与目标</a></li>
|
|
178
|
-
<li><a href="#核心能力">核心能力</a></li>
|
|
179
|
-
<li><a href="#业务流程">业务流程</a></li>
|
|
180
|
-
<li><a href="#关键决策">关键决策</a></li>
|
|
181
|
-
<li><a href="#现状与展望">现状与展望</a></li>
|
|
182
|
-
</ul>
|
|
183
|
-
</nav>
|
|
184
|
-
|
|
185
|
-
<article>
|
|
186
|
-
<section id="背景与目标">
|
|
187
|
-
<h2>背景与目标</h2>
|
|
188
|
-
<p>说明项目或系统的定位、目标和解决的问题。</p>
|
|
189
|
-
</section>
|
|
190
|
-
|
|
191
|
-
<section id="核心能力">
|
|
192
|
-
<h2>核心能力</h2>
|
|
193
|
-
<p>描述系统提供的主要能力和价值。</p>
|
|
194
|
-
</section>
|
|
195
|
-
|
|
196
|
-
<section id="业务流程">
|
|
197
|
-
<h2>业务流程</h2>
|
|
198
|
-
<p>说明核心业务流程和操作步骤。</p>
|
|
199
|
-
</section>
|
|
200
|
-
|
|
201
|
-
<section id="关键决策">
|
|
202
|
-
<h2>关键决策</h2>
|
|
203
|
-
<p>列出重要的技术和业务决策及其理由。</p>
|
|
204
|
-
</section>
|
|
205
|
-
|
|
206
|
-
<section id="现状与展望">
|
|
207
|
-
<h2>现状与展望</h2>
|
|
208
|
-
<p>说明当前状态和未来方向。</p>
|
|
209
|
-
</section>
|
|
210
|
-
</article>
|
|
211
|
-
</div>
|
|
212
|
-
</body>
|
|
213
|
-
</html>
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
## Business HTML 章节内容指南
|
|
217
|
-
|
|
218
|
-
每个章节应按照以下指引从 AI 知识文档中提取、转换和改写内容。
|
|
219
|
-
|
|
220
|
-
| 章节 | 内容来源 | 改写方向 |
|
|
221
|
-
|------|------------------------|----------|
|
|
222
|
-
| 背景与目标 | Summary 文本 | 从"为什么分析"转为"项目/系统的目标和定位" |
|
|
223
|
-
| 核心能力 | Summary 文本 | 从"发现"转为"能力描述",面向非技术读者 |
|
|
224
|
-
| 业务流程 | Read 文件原文(该章节 cgraphx 不提取) | 从"操作步骤"转为"业务流程说明",去除技术细节 |
|
|
225
|
-
| 关键决策 | Read 文件原文(该章节 cgraphx 不提取) | 从"决策记录"转为"决策解释",说明决策的业务原因 |
|
|
226
|
-
| 价值与影响 | Read 文件原文(该章节 cgraphx 不提取) | 从"对未来变更的影响"转为"对业务的价值和影响" |
|
|
227
|
-
| 现状与展望 | Read 文件原文(该章节 cgraphx 不提取) | 从"发现和后续"转为"当前状态和未来规划" |
|
|
228
|
-
| 风险与注意事项 | Read 文件原文(该章节 cgraphx 不提取) | 从"故障排除"转为"风险提示和应对措施" |
|
|
229
|
-
| 常见问题 | Read 文件原文(该章节 cgraphx 不提取) | 从"分析记录"转为"Q&A 格式" |
|
|
230
|
-
| 术语表 | 各文本字段中的术语 | 提取关键术语,给出简洁的业务定义 |
|
|
231
|
-
| 相关资源 | — | 列出相关的代码文件(相对路径)或外部文档 |
|
|
232
|
-
|
|
233
|
-
## HTML 格式要点
|
|
234
|
-
|
|
235
|
-
- 所有 CSS 内联在 `<style>` 标签中,不引用外部样式表
|
|
236
|
-
- `<title>` 包含文档标题和项目名称
|
|
237
|
-
- 使用 `<nav class="toc">` 作为目录容器
|
|
238
|
-
- 每个章节使用 `<section id="章节锚点">` 包裹
|
|
239
|
-
- 章节锚点使用中文 ID(如 `id="背景与目标"`)
|
|
240
|
-
- 重要提示或注意事项使用 `<div class="callout">` 样式
|
|
241
|
-
- 代码示例使用 `<pre><code>` 标签
|
|
242
|
-
- 表格使用 `<thead>` + `<tbody>` 结构
|