@birdie_moblie/open_spec 2.1.7
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/.agents/plugins/marketplace.json +13 -0
- package/.claude-plugin/marketplace.json +12 -0
- package/.claude-plugin/plugin.json +9 -0
- package/.codex-plugin/plugin.json +9 -0
- package/LICENSE +21 -0
- package/README.md +111 -0
- package/bin/openspec.js +3 -0
- package/dist/cli/commands/change.d.ts +3 -0
- package/dist/cli/commands/change.js +37 -0
- package/dist/cli/commands/ext.d.ts +3 -0
- package/dist/cli/commands/ext.js +196 -0
- package/dist/cli/commands/figma.d.ts +3 -0
- package/dist/cli/commands/figma.js +73 -0
- package/dist/cli/commands/init.d.ts +3 -0
- package/dist/cli/commands/init.js +71 -0
- package/dist/cli/commands/opsx.d.ts +3 -0
- package/dist/cli/commands/opsx.js +324 -0
- package/dist/cli/commands/usage.d.ts +3 -0
- package/dist/cli/commands/usage.js +21 -0
- package/dist/cli/output.d.ts +8 -0
- package/dist/cli/output.js +17 -0
- package/dist/cli/program.d.ts +4 -0
- package/dist/cli/program.js +42 -0
- package/dist/cli/register.d.ts +3 -0
- package/dist/cli/register.js +13 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +3 -0
- package/dist/config/load.d.ts +7 -0
- package/dist/config/load.js +55 -0
- package/dist/config/presets.d.ts +129 -0
- package/dist/config/presets.js +125 -0
- package/dist/config/schema.d.ts +281 -0
- package/dist/config/schema.js +178 -0
- package/dist/core/archive.d.ts +5 -0
- package/dist/core/archive.js +33 -0
- package/dist/core/change.d.ts +12 -0
- package/dist/core/change.js +69 -0
- package/dist/core/code-changes.d.ts +9 -0
- package/dist/core/code-changes.js +31 -0
- package/dist/core/flow.d.ts +6 -0
- package/dist/core/flow.js +99 -0
- package/dist/core/gates.d.ts +5 -0
- package/dist/core/gates.js +71 -0
- package/dist/core/instructions.d.ts +21 -0
- package/dist/core/instructions.js +102 -0
- package/dist/core/source-operations.d.ts +25 -0
- package/dist/core/source-operations.js +94 -0
- package/dist/core/sources.d.ts +33 -0
- package/dist/core/sources.js +234 -0
- package/dist/core/state.d.ts +15 -0
- package/dist/core/state.js +75 -0
- package/dist/core/status.d.ts +31 -0
- package/dist/core/status.js +22 -0
- package/dist/core/tasks.d.ts +15 -0
- package/dist/core/tasks.js +143 -0
- package/dist/core/todo.d.ts +31 -0
- package/dist/core/todo.js +114 -0
- package/dist/core/types.d.ts +297 -0
- package/dist/core/types.js +227 -0
- package/dist/core/verify.d.ts +229 -0
- package/dist/core/verify.js +391 -0
- package/dist/core/worker-evidence.d.ts +71 -0
- package/dist/core/worker-evidence.js +253 -0
- package/dist/core/worker.d.ts +22 -0
- package/dist/core/worker.js +478 -0
- package/dist/delivery/claude-install.d.ts +4 -0
- package/dist/delivery/claude-install.js +44 -0
- package/dist/delivery/distribution.d.ts +3 -0
- package/dist/delivery/distribution.js +53 -0
- package/dist/delivery/skills.d.ts +36 -0
- package/dist/delivery/skills.js +178 -0
- package/dist/figma/cache.d.ts +19 -0
- package/dist/figma/cache.js +115 -0
- package/dist/figma/claude.d.ts +14 -0
- package/dist/figma/claude.js +121 -0
- package/dist/figma/collect.d.ts +3 -0
- package/dist/figma/collect.js +57 -0
- package/dist/figma/index.d.ts +35 -0
- package/dist/figma/index.js +178 -0
- package/dist/figma/media.d.ts +5 -0
- package/dist/figma/media.js +77 -0
- package/dist/figma/response.d.ts +9 -0
- package/dist/figma/response.js +27 -0
- package/dist/hooks/command-context.d.ts +19 -0
- package/dist/hooks/command-context.js +33 -0
- package/dist/hooks/engine.d.ts +61 -0
- package/dist/hooks/engine.js +314 -0
- package/dist/hooks/plugin-runtime.d.ts +188 -0
- package/dist/hooks/plugin-runtime.js +198 -0
- package/dist/index.d.ts +99 -0
- package/dist/index.js +185 -0
- package/dist/marketplace/adapters.d.ts +202 -0
- package/dist/marketplace/adapters.js +203 -0
- package/dist/marketplace/builtin.d.ts +9 -0
- package/dist/marketplace/builtin.js +9 -0
- package/dist/marketplace/install.d.ts +38 -0
- package/dist/marketplace/install.js +108 -0
- package/dist/marketplace/registry.d.ts +71 -0
- package/dist/marketplace/registry.js +342 -0
- package/dist/marketplace/upgrade.d.ts +39 -0
- package/dist/marketplace/upgrade.js +104 -0
- package/dist/shared/command-log.d.ts +9 -0
- package/dist/shared/command-log.js +55 -0
- package/dist/shared/errors.d.ts +7 -0
- package/dist/shared/errors.js +14 -0
- package/dist/shared/exec.d.ts +16 -0
- package/dist/shared/exec.js +86 -0
- package/dist/shared/fs.d.ts +18 -0
- package/dist/shared/fs.js +93 -0
- package/dist/shared/git.d.ts +22 -0
- package/dist/shared/git.js +132 -0
- package/dist/shared/hash.d.ts +5 -0
- package/dist/shared/hash.js +29 -0
- package/dist/shared/paths.d.ts +16 -0
- package/dist/shared/paths.js +97 -0
- package/dist/shared/pkg.d.ts +3 -0
- package/dist/shared/pkg.js +23 -0
- package/dist/shared/version.d.ts +9 -0
- package/dist/shared/version.js +34 -0
- package/dist/usage/analyze.d.ts +5 -0
- package/dist/usage/analyze.js +212 -0
- package/dist/usage/claude.d.ts +9 -0
- package/dist/usage/claude.js +59 -0
- package/dist/usage/collect.d.ts +11 -0
- package/dist/usage/collect.js +71 -0
- package/dist/usage/doctor.d.ts +72 -0
- package/dist/usage/doctor.js +121 -0
- package/dist/usage/parse.d.ts +28 -0
- package/dist/usage/parse.js +345 -0
- package/dist/usage/types.d.ts +103 -0
- package/dist/usage/types.js +2 -0
- package/docs/getting-started.md +20 -0
- package/docs/guides/configuration.md +33 -0
- package/docs/guides/cost-benchmark.md +88 -0
- package/docs/guides/plugin-migration.md +65 -0
- package/docs/guides/smart-figma.md +51 -0
- package/docs/guides/source-reading.md +35 -0
- package/docs/guides/ui-conventions-template.md +39 -0
- package/docs/guides/worker-evidence.md +73 -0
- package/docs/guides/workflow.md +74 -0
- package/docs/maintainers/architecture.md +27 -0
- package/docs/maintainers/plugin-runtime-contract.md +55 -0
- package/package.json +78 -0
- package/plugin.json +9 -0
- package/presets/backend-service.yaml +19 -0
- package/presets/flutter-mobile/plugin-hooks.json +68 -0
- package/presets/flutter-mobile/ui-conventions.md +122 -0
- package/presets/flutter-mobile.yaml +205 -0
- package/presets/web-product.yaml +23 -0
- package/skills/opsx-apply/SKILL.md +35 -0
- package/skills/opsx-apply/nodes/blocked.md +7 -0
- package/skills/opsx-apply/nodes/code-task.md +29 -0
- package/skills/opsx-apply/nodes/gate-a.md +9 -0
- package/skills/opsx-apply/nodes/stop.md +5 -0
- package/skills/opsx-apply/nodes/task-build.md +50 -0
- package/skills/opsx-apply/nodes/task-repair.md +20 -0
- package/skills/opsx-apply/nodes/task-scout.md +28 -0
- package/skills/opsx-apply/nodes/task-verify.md +38 -0
- package/skills/opsx-archive/SKILL.md +24 -0
- package/skills/opsx-archive/nodes/archive.md +15 -0
- package/skills/opsx-explore/SKILL.md +20 -0
- package/skills/opsx-explore/nodes/explore.md +3 -0
- package/skills/opsx-fix-verify/SKILL.md +30 -0
- package/skills/opsx-fix-verify/nodes/repair.md +11 -0
- package/skills/opsx-fix-verify/nodes/reverify.md +3 -0
- package/skills/opsx-fix-verify/nodes/stop.md +3 -0
- package/skills/opsx-gate-decision/SKILL.md +20 -0
- package/skills/opsx-gate-decision/nodes/decide.md +15 -0
- package/skills/opsx-propose/SKILL.md +36 -0
- package/skills/opsx-propose/nodes/blocked.md +13 -0
- package/skills/opsx-propose/nodes/collect.md +35 -0
- package/skills/opsx-propose/nodes/parse.md +72 -0
- package/skills/opsx-propose/nodes/plan.md +65 -0
- package/skills/opsx-propose/nodes/stop.md +17 -0
- package/skills/opsx-propose/nodes/tech.md +77 -0
- package/skills/opsx-propose/scripts/read-json-source.mjs +22 -0
- package/skills/opsx-resume/SKILL.md +21 -0
- package/skills/opsx-resume/nodes/route.md +12 -0
- package/skills/opsx-source-update/SKILL.md +23 -0
- package/skills/opsx-source-update/nodes/update.md +14 -0
- package/skills/opsx-verify/SKILL.md +32 -0
- package/skills/opsx-verify/nodes/blocked.md +3 -0
- package/skills/opsx-verify/nodes/failed.md +3 -0
- package/skills/opsx-verify/nodes/gate-b.md +7 -0
- package/skills/opsx-verify/nodes/stop.md +5 -0
- package/skills/opsx-verify/nodes/verify.md +10 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { sha256 } from '../shared/hash.js';
|
|
4
|
+
import { AppError } from '../shared/errors.js';
|
|
5
|
+
import { parseUsageStream, sumTokens, object } from './parse.js';
|
|
6
|
+
import { mergeClaudeStreams } from './claude.js';
|
|
7
|
+
export { sumTokens } from './parse.js';
|
|
8
|
+
export function analyzeUsage(options) {
|
|
9
|
+
if (!options.sessions.length)
|
|
10
|
+
throw new AppError('usage_sessions_required', '至少指定一个会话日志');
|
|
11
|
+
const hashes = new Set();
|
|
12
|
+
const streams = [];
|
|
13
|
+
for (const input of [...new Set(options.sessions.map((input) => path.resolve(input)))].sort()) {
|
|
14
|
+
const file = path.resolve(input);
|
|
15
|
+
const text = readFileSync(file, 'utf8');
|
|
16
|
+
const hash = sha256(text);
|
|
17
|
+
if (hashes.has(hash))
|
|
18
|
+
continue;
|
|
19
|
+
hashes.add(hash);
|
|
20
|
+
streams.push(parseUsageStream(file, text));
|
|
21
|
+
}
|
|
22
|
+
const groups = new Map();
|
|
23
|
+
for (const stream of streams) {
|
|
24
|
+
const key = `${stream.provider}:${stream.threadId}`;
|
|
25
|
+
groups.set(key, [...(groups.get(key) ?? []), stream]);
|
|
26
|
+
}
|
|
27
|
+
const issues = streams.flatMap((stream) => stream.issues);
|
|
28
|
+
const slices = [];
|
|
29
|
+
const sessions = [];
|
|
30
|
+
const claude = mergeClaudeStreams(streams.filter((stream) => stream.provider === 'claude'), issues);
|
|
31
|
+
for (const group of groups.values()) {
|
|
32
|
+
const rank = (stream) => stream.source === 'rollout' ? 2 : 1;
|
|
33
|
+
group.sort((a, b) => rank(b) - rank(a) || sumTokens(b.slices.map((slice) => slice.tokens)).input - sumTokens(a.slices.map((slice) => slice.tokens)).input || a.file.localeCompare(b.file));
|
|
34
|
+
const selected = group[0];
|
|
35
|
+
if (selected.source === 'exec' && group.length > 1)
|
|
36
|
+
issues.push({ code: 'multiple_exec_exports_require_rollout', severity: 'incomplete', file: selected.file, threadId: selected.threadId });
|
|
37
|
+
let selectedSlices = [...selected.slices];
|
|
38
|
+
if (selected.provider === 'claude') {
|
|
39
|
+
selectedSlices = claude.slices.filter((slice) => slice.threadId === selected.threadId);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
for (const other of group.slice(1)) {
|
|
43
|
+
const chosen = sumTokens(selectedSlices.map((slice) => slice.tokens));
|
|
44
|
+
const reported = sumTokens(other.slices.map((slice) => slice.tokens));
|
|
45
|
+
if (reported.input > chosen.input || reported.output > chosen.output) {
|
|
46
|
+
issues.push({ code: 'conflicting_usage_exports', severity: 'incomplete', file: other.file, threadId: selected.threadId });
|
|
47
|
+
// Preserve every observed token, even when the missing interval cannot be attributed.
|
|
48
|
+
const input = Math.max(0, reported.input - chosen.input);
|
|
49
|
+
const output = Math.max(0, reported.output - chosen.output);
|
|
50
|
+
selectedSlices.push({ key: `${selected.threadId}:unattributed-gap:${other.file}`, threadId: selected.threadId,
|
|
51
|
+
model: null, effort: null, workerId: null, stepId: null, timestamp: null, file: other.file, line: 1,
|
|
52
|
+
tokens: { input, output, total: input + output, cachedInput: null, cacheWriteInput: null, reasoningOutput: null } });
|
|
53
|
+
}
|
|
54
|
+
if (other.source === 'rollout' && selected.source === 'rollout' && reported.input !== chosen.input)
|
|
55
|
+
issues.push({ code: 'partial_rollout_exports', severity: 'incomplete', file: other.file, threadId: selected.threadId });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const tools = selected.provider === 'claude' ? claude.tools.get(selected.threadId) ?? new Set() : selected.tools;
|
|
59
|
+
const spawns = selected.provider === 'claude' ? claude.spawns.get(selected.threadId) ?? new Set() : selected.spawns;
|
|
60
|
+
if (selected.source === 'exec' && selected.collabObserved)
|
|
61
|
+
issues.push({ code: 'exec_collaboration_requires_rollout', severity: 'incomplete', file: selected.file, threadId: selected.threadId });
|
|
62
|
+
slices.push(...selectedSlices);
|
|
63
|
+
const elapsed = selected.startedAt && selected.endedAt ? Date.parse(selected.endedAt) - Date.parse(selected.startedAt) : NaN;
|
|
64
|
+
sessions.push({ threadId: selected.threadId, parentThreadId: selected.parentThreadId, provider: selected.provider, source: selected.source,
|
|
65
|
+
files: group.map((stream) => stream.file), models: [...new Set(selectedSlices.map((slice) => slice.model).filter((value) => value !== null))],
|
|
66
|
+
efforts: [...new Set(selectedSlices.map((slice) => slice.effort).filter((value) => value !== null))],
|
|
67
|
+
tokens: sumTokens(selectedSlices.map((slice) => slice.tokens)), usageObservations: selectedSlices.length, toolCalls: tools.size,
|
|
68
|
+
apiCalls: selected.provider === 'claude' ? selectedSlices.length : selected.apiCalls,
|
|
69
|
+
spawnCalls: spawns.size, epochs: selected.epochs, startedAt: selected.startedAt, endedAt: selected.endedAt,
|
|
70
|
+
elapsedMs: Number.isFinite(elapsed) && elapsed >= 0 ? elapsed : null });
|
|
71
|
+
}
|
|
72
|
+
for (const session of sessions) {
|
|
73
|
+
const children = sessions.filter((child) => child.parentThreadId === session.threadId);
|
|
74
|
+
if (session.spawnCalls > children.length)
|
|
75
|
+
issues.push({ code: 'missing_child_usage', severity: 'incomplete', file: session.files[0], threadId: session.threadId });
|
|
76
|
+
if (session.parentThreadId && !sessions.some((parent) => parent.threadId === session.parentThreadId))
|
|
77
|
+
issues.push({ code: 'missing_parent_usage', severity: 'incomplete', file: session.files[0], threadId: session.threadId });
|
|
78
|
+
}
|
|
79
|
+
for (const slice of slices)
|
|
80
|
+
slice.attribution = slice.stepId === 'compaction' ? 'request_id' : slice.stepId ? 'log_hint' : 'unattributed';
|
|
81
|
+
if (options.events)
|
|
82
|
+
applyEventAttribution(slices, options.events, issues);
|
|
83
|
+
for (const slice of slices) {
|
|
84
|
+
if (options.expectedModel && slice.model !== options.expectedModel)
|
|
85
|
+
issues.push({ code: 'model_not_verified', severity: 'incomplete', file: slice.file, line: slice.line, threadId: slice.threadId });
|
|
86
|
+
if (options.expectedEffort && slice.effort !== options.expectedEffort)
|
|
87
|
+
issues.push({ code: 'effort_not_verified', severity: 'incomplete', file: slice.file, line: slice.line, threadId: slice.threadId });
|
|
88
|
+
}
|
|
89
|
+
if (!slices.length)
|
|
90
|
+
issues.push({ code: 'no_usage', severity: 'incomplete', file: options.sessions[0] });
|
|
91
|
+
const byStage = new Map();
|
|
92
|
+
for (const slice of slices) {
|
|
93
|
+
const key = slice.stepId ?? 'unattributed';
|
|
94
|
+
byStage.set(key, [...(byStage.get(key) ?? []), slice]);
|
|
95
|
+
}
|
|
96
|
+
const configurationIssues = new Set(['model_not_verified', 'effort_not_verified']);
|
|
97
|
+
const tokenComplete = !issues.some((issue) => issue.severity === 'incomplete' && !configurationIssues.has(issue.code));
|
|
98
|
+
const modelsKnown = slices.length > 0 && slices.every((slice) => slice.model !== null);
|
|
99
|
+
const effortsKnown = slices.length > 0 && slices.every((slice) => slice.effort !== null);
|
|
100
|
+
const cost = calculateCost(options, sessions, slices);
|
|
101
|
+
cost.complete &&= tokenComplete;
|
|
102
|
+
return { version: 1, changeName: options.changeName, totals: sumTokens(slices.map((slice) => slice.tokens)), sessions, slices,
|
|
103
|
+
byStage: [...byStage].map(([stepId, rows]) => ({ stepId, tokens: sumTokens(rows.map((slice) => slice.tokens)), observations: rows.length })),
|
|
104
|
+
coverage: { tokenComplete, modelsKnown, effortsKnown, comparisonEligible: tokenComplete && modelsKnown && effortsKnown && !issues.some((issue) => configurationIssues.has(issue.code)), issues }, cost,
|
|
105
|
+
counts: { sessions: sessions.length, usageObservations: slices.length, toolCalls: sessions.reduce((total, session) => total + session.toolCalls, 0),
|
|
106
|
+
apiCalls: sessions.length > 0 && sessions.every((session) => session.apiCalls !== null) ? sessions.reduce((sum, session) => sum + session.apiCalls, 0) : null } };
|
|
107
|
+
}
|
|
108
|
+
function applyEventAttribution(slices, file, issues) {
|
|
109
|
+
const events = [];
|
|
110
|
+
for (const [index, line] of readFileSync(file, 'utf8').split('\n').entries()) {
|
|
111
|
+
if (!line.trim())
|
|
112
|
+
continue;
|
|
113
|
+
try {
|
|
114
|
+
const event = object(JSON.parse(line));
|
|
115
|
+
const time = Date.parse(String(event.at ?? event.timestamp));
|
|
116
|
+
if (Number.isFinite(time))
|
|
117
|
+
events.push({ time, type: String(event.type), payload: object(event.payload) });
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
issues.push({ code: 'malformed_events', severity: 'warning', file, line: index + 1 });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
events.sort((a, b) => a.time - b.time);
|
|
124
|
+
const knownWorkers = new Map();
|
|
125
|
+
const windows = [];
|
|
126
|
+
let stepId = null;
|
|
127
|
+
let workerId = null;
|
|
128
|
+
for (const event of events) {
|
|
129
|
+
const p = event.payload;
|
|
130
|
+
const id = typeof p.workerId === 'string' ? p.workerId : null;
|
|
131
|
+
if (event.type === 'sources.updated') {
|
|
132
|
+
stepId = 'collect';
|
|
133
|
+
workerId = null;
|
|
134
|
+
}
|
|
135
|
+
else if (event.type === 'worker.created' && id && typeof p.stepId === 'string') {
|
|
136
|
+
knownWorkers.set(id, p.stepId);
|
|
137
|
+
workerId = id;
|
|
138
|
+
stepId = p.stepId;
|
|
139
|
+
}
|
|
140
|
+
else if (event.type === 'worker.blocked' && id === workerId)
|
|
141
|
+
stepId = 'blocked';
|
|
142
|
+
else if (event.type === 'worker.answered' && id === workerId)
|
|
143
|
+
stepId = `${knownWorkers.get(id) ?? 'worker'}-resume`;
|
|
144
|
+
else if (event.type === 'worker.accepted' || event.type === 'worker.cancelled' || event.type === 'sources.collected' || event.type === 'gate.approved') {
|
|
145
|
+
stepId = p.node === 'gate_a' ? 'gate-a-report' : null;
|
|
146
|
+
workerId = null;
|
|
147
|
+
}
|
|
148
|
+
else
|
|
149
|
+
continue;
|
|
150
|
+
windows.push({ time: event.time, stepId, workerId });
|
|
151
|
+
}
|
|
152
|
+
for (const slice of slices) {
|
|
153
|
+
if (slice.stepId === 'compaction') {
|
|
154
|
+
slice.attribution = 'request_id';
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (!slice.timestamp)
|
|
158
|
+
continue;
|
|
159
|
+
const window = windows.filter((event) => event.time <= Date.parse(slice.timestamp)).at(-1);
|
|
160
|
+
if (!window)
|
|
161
|
+
continue;
|
|
162
|
+
slice.workerId = window.workerId;
|
|
163
|
+
slice.stepId = window.stepId;
|
|
164
|
+
slice.attribution = window.stepId ? 'event_window' : 'unattributed';
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function calculateCost(options, sessions, slices) {
|
|
168
|
+
if (options.billing) {
|
|
169
|
+
const charges = options.billing.charges;
|
|
170
|
+
if (!options.billing.source || !Array.isArray(charges) || charges.some((charge) => !charge.threadId || !Number.isFinite(charge.usd) || charge.usd < 0)
|
|
171
|
+
|| new Set(charges.map((charge) => charge.threadId)).size !== charges.length)
|
|
172
|
+
throw new AppError('invalid_billing', '账单需来源、唯一 threadId 与非负美元金额');
|
|
173
|
+
const complete = sessions.length > 0 && sessions.every((session) => charges.some((charge) => charge.threadId === session.threadId));
|
|
174
|
+
return { kind: 'actual', usd: charges.filter((charge) => sessions.some((session) => session.threadId === charge.threadId)).reduce((sum, charge) => sum + charge.usd, 0), source: options.billing.source, complete };
|
|
175
|
+
}
|
|
176
|
+
if (options.rates) {
|
|
177
|
+
if (!options.rates.source || !options.rates.models)
|
|
178
|
+
throw new AppError('invalid_rates', '费率必须注明来源并提供 models');
|
|
179
|
+
let usd = 0;
|
|
180
|
+
let complete = slices.length > 0;
|
|
181
|
+
for (const slice of slices) {
|
|
182
|
+
const rate = slice.model ? options.rates.models[slice.model] : undefined;
|
|
183
|
+
const tokens = slice.tokens;
|
|
184
|
+
if (!rate || tokens.cachedInput === null || tokens.cacheWriteInput === null || (tokens.cacheWriteInput > 0 && rate.cacheWriteInputPerMillion === undefined)) {
|
|
185
|
+
complete = false;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const values = [rate.inputPerMillion, rate.cachedInputPerMillion, rate.outputPerMillion, ...(rate.cacheWriteInputPerMillion === undefined ? [] : [rate.cacheWriteInputPerMillion])];
|
|
189
|
+
if (values.some((value) => typeof value !== 'number' || !Number.isFinite(value) || value < 0))
|
|
190
|
+
throw new AppError('invalid_rates', '费率必须为非负有限数');
|
|
191
|
+
const uncached = tokens.input - tokens.cachedInput - tokens.cacheWriteInput;
|
|
192
|
+
if (uncached < 0) {
|
|
193
|
+
complete = false;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
usd += (uncached * rate.inputPerMillion + tokens.cachedInput * rate.cachedInputPerMillion + tokens.cacheWriteInput * (rate.cacheWriteInputPerMillion ?? 0) + tokens.output * rate.outputPerMillion) / 1_000_000;
|
|
197
|
+
}
|
|
198
|
+
return { kind: 'estimated', usd, source: options.rates.source, complete };
|
|
199
|
+
}
|
|
200
|
+
return { kind: 'unavailable', usd: null, source: null, complete: false };
|
|
201
|
+
}
|
|
202
|
+
export function renderUsageMarkdown(report) {
|
|
203
|
+
const show = (value) => value === null ? 'unknown' : value.toLocaleString('en-US');
|
|
204
|
+
return [`# Usage: ${report.changeName}`, '', `输入 ${show(report.totals.input)};输出 ${show(report.totals.output)};合计 ${show(report.totals.total)}。`,
|
|
205
|
+
`缓存输入 ${show(report.totals.cachedInput)}、缓存写入 ${show(report.totals.cacheWriteInput)}、推理输出 ${show(report.totals.reasoningOutput)} 是明细,不再叠加。`,
|
|
206
|
+
`计量完整:${report.coverage.tokenComplete};可比较:${report.coverage.comparisonEligible};费用:${report.cost.kind}${report.cost.usd === null ? '' : ` USD ${report.cost.usd}`}(完整:${report.cost.complete})。`, '',
|
|
207
|
+
'| 阶段 | 输入 | 输出 | 用量观测数 |', '|---|---:|---:|---:|',
|
|
208
|
+
...report.byStage.map((stage) => `| ${stage.stepId} | ${show(stage.tokens.input)} | ${show(stage.tokens.output)} | ${stage.observations} |`), '',
|
|
209
|
+
'未归属用量保留在 unattributed;用量观测数不等于已核验的 API 请求次数。实际费用仅来自显式提供的账单记录,费率估算不代表账单。', '',
|
|
210
|
+
...report.coverage.issues.map((issue) => `- ${issue.severity}: ${issue.code} (${issue.file}${issue.line ? `:${issue.line}` : ''})`), ''].join('\n');
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=analyze.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type UsageStream } from './parse.js';
|
|
2
|
+
import type { UsageIssue, UsageSlice } from './types.js';
|
|
3
|
+
/** Forked Claude logs can contain parent messages, including the Agent call itself. */
|
|
4
|
+
export declare function mergeClaudeStreams(streams: UsageStream[], issues: UsageIssue[]): {
|
|
5
|
+
slices: UsageSlice[];
|
|
6
|
+
tools: Map<string, Set<string>>;
|
|
7
|
+
spawns: Map<string, Set<string>>;
|
|
8
|
+
};
|
|
9
|
+
//# sourceMappingURL=claude.d.ts.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { mergeClaudeTokens } from './parse.js';
|
|
2
|
+
/** Forked Claude logs can contain parent messages, including the Agent call itself. */
|
|
3
|
+
export function mergeClaudeStreams(streams, issues) {
|
|
4
|
+
const parents = new Map(streams.map((stream) => [stream.threadId, stream.parentThreadId]));
|
|
5
|
+
const ancestorOf = (ancestor, descendant) => {
|
|
6
|
+
const visited = new Set();
|
|
7
|
+
let current = descendant;
|
|
8
|
+
while (current && !visited.has(current)) {
|
|
9
|
+
if (current === ancestor)
|
|
10
|
+
return true;
|
|
11
|
+
visited.add(current);
|
|
12
|
+
current = parents.get(current) ?? null;
|
|
13
|
+
}
|
|
14
|
+
return false;
|
|
15
|
+
};
|
|
16
|
+
const owner = (ids, file) => {
|
|
17
|
+
const candidates = [...new Set(ids)].sort();
|
|
18
|
+
const root = candidates.find((candidate) => candidates.every((id) => ancestorOf(candidate, id)));
|
|
19
|
+
if (root)
|
|
20
|
+
return root;
|
|
21
|
+
// Preserve the observed amount once, but do not claim proven attribution.
|
|
22
|
+
issues.push({ code: 'claude_ownership_ambiguous', severity: 'incomplete', file });
|
|
23
|
+
return candidates[0];
|
|
24
|
+
};
|
|
25
|
+
const messages = new Map();
|
|
26
|
+
for (const stream of streams)
|
|
27
|
+
for (const slice of stream.slices) {
|
|
28
|
+
messages.set(slice.key, [...(messages.get(slice.key) ?? []), slice]);
|
|
29
|
+
}
|
|
30
|
+
const slices = [];
|
|
31
|
+
const owners = new Map();
|
|
32
|
+
for (const [id, occurrences] of messages) {
|
|
33
|
+
const threadId = owner(occurrences.map((slice) => slice.threadId), occurrences[0].file);
|
|
34
|
+
owners.set(id, threadId);
|
|
35
|
+
const first = occurrences.find((slice) => slice.threadId === threadId);
|
|
36
|
+
const ambiguous = !occurrences.every((slice) => ancestorOf(threadId, slice.threadId));
|
|
37
|
+
slices.push({ ...first, ...(ambiguous ? { workerId: null, stepId: null } : {}),
|
|
38
|
+
tokens: occurrences.slice(1).reduce((tokens, slice) => mergeClaudeTokens(tokens, slice.tokens), occurrences[0].tokens) });
|
|
39
|
+
}
|
|
40
|
+
const calls = new Map();
|
|
41
|
+
for (const stream of streams)
|
|
42
|
+
for (const id of stream.tools)
|
|
43
|
+
calls.set(id, [...(calls.get(id) ?? []), stream]);
|
|
44
|
+
const tools = new Map();
|
|
45
|
+
const spawns = new Map();
|
|
46
|
+
for (const [id, occurrences] of calls) {
|
|
47
|
+
const threadId = owner(occurrences.map((stream) => owners.get(stream.toolMessages.get(id) ?? '') ?? stream.threadId), occurrences[0].file);
|
|
48
|
+
const owned = tools.get(threadId) ?? new Set();
|
|
49
|
+
owned.add(id);
|
|
50
|
+
tools.set(threadId, owned);
|
|
51
|
+
if (occurrences.some((stream) => stream.spawns.has(id))) {
|
|
52
|
+
const spawned = spawns.get(threadId) ?? new Set();
|
|
53
|
+
spawned.add(id);
|
|
54
|
+
spawns.set(threadId, spawned);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { slices, tools, spawns };
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=claude.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Discover only metadata headers; read full logs only after establishing lineage. */
|
|
2
|
+
export declare function discoverCodexSessions(roots: string[], threadIds: string[]): {
|
|
3
|
+
files: string[];
|
|
4
|
+
missingThreadIds: string[];
|
|
5
|
+
errors: {
|
|
6
|
+
file: string;
|
|
7
|
+
code: string;
|
|
8
|
+
}[];
|
|
9
|
+
};
|
|
10
|
+
export declare function execThreadIds(file: string): string[];
|
|
11
|
+
//# sourceMappingURL=collect.d.ts.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { closeSync, existsSync, openSync, readSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { object, string } from './parse.js';
|
|
4
|
+
/** Discover only metadata headers; read full logs only after establishing lineage. */
|
|
5
|
+
export function discoverCodexSessions(roots, threadIds) {
|
|
6
|
+
const candidates = [];
|
|
7
|
+
const errors = [];
|
|
8
|
+
const visit = (dir) => {
|
|
9
|
+
if (!existsSync(dir))
|
|
10
|
+
return;
|
|
11
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
12
|
+
const file = path.join(dir, entry.name);
|
|
13
|
+
if (entry.isDirectory()) {
|
|
14
|
+
visit(file);
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (!entry.isFile() || !entry.name.endsWith('.jsonl'))
|
|
18
|
+
continue;
|
|
19
|
+
const fd = openSync(file, 'r');
|
|
20
|
+
const buffer = Buffer.alloc(65536);
|
|
21
|
+
let header = '';
|
|
22
|
+
try {
|
|
23
|
+
header = buffer.subarray(0, readSync(fd, buffer, 0, buffer.length, 0)).toString('utf8').split('\n')[0];
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
closeSync(fd);
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const row = object(JSON.parse(header));
|
|
30
|
+
if (row.type !== 'session_meta')
|
|
31
|
+
continue;
|
|
32
|
+
const payload = object(row.payload);
|
|
33
|
+
const id = string(payload.id);
|
|
34
|
+
if (!id)
|
|
35
|
+
continue;
|
|
36
|
+
const subagent = object(object(payload.source).subagent);
|
|
37
|
+
candidates.push({ id, parent: string(object(subagent.thread_spawn).parent_thread_id) ?? string(object(subagent.spawn).parent_thread_id), file });
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
errors.push({ file, code: 'unreadable_session_header' });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
for (const root of roots)
|
|
45
|
+
visit(path.resolve(root));
|
|
46
|
+
const included = new Set(threadIds);
|
|
47
|
+
let changed = true;
|
|
48
|
+
while (changed) {
|
|
49
|
+
changed = false;
|
|
50
|
+
for (const item of candidates)
|
|
51
|
+
if (item.parent && included.has(item.parent) && !included.has(item.id)) {
|
|
52
|
+
included.add(item.id);
|
|
53
|
+
changed = true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { files: [...new Set(candidates.filter((item) => included.has(item.id)).map((item) => item.file))],
|
|
57
|
+
missingThreadIds: threadIds.filter((id) => !candidates.some((item) => item.id === id)), errors };
|
|
58
|
+
}
|
|
59
|
+
export function execThreadIds(file) {
|
|
60
|
+
const ids = new Set();
|
|
61
|
+
for (const line of readFileSync(file, 'utf8').split('\n')) {
|
|
62
|
+
try {
|
|
63
|
+
const row = object(JSON.parse(line));
|
|
64
|
+
if (row.type === 'thread.started' && typeof row.thread_id === 'string')
|
|
65
|
+
ids.add(row.thread_id);
|
|
66
|
+
}
|
|
67
|
+
catch { /* Analyzer retains malformed evidence. */ }
|
|
68
|
+
}
|
|
69
|
+
return [...ids];
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=collect.js.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export declare function costCapabilities(cwd: string, explicit?: string[]): {
|
|
2
|
+
names: string[];
|
|
3
|
+
gaps: string[];
|
|
4
|
+
};
|
|
5
|
+
export declare function costDoctor(options: {
|
|
6
|
+
host: 'codex' | 'claude';
|
|
7
|
+
sessions?: string[];
|
|
8
|
+
evidence?: string;
|
|
9
|
+
model?: string;
|
|
10
|
+
effort?: string;
|
|
11
|
+
capabilities?: string[];
|
|
12
|
+
cwd: string;
|
|
13
|
+
}): {
|
|
14
|
+
ok: boolean;
|
|
15
|
+
readOnly: boolean;
|
|
16
|
+
host: "claude" | "codex";
|
|
17
|
+
version: string | null;
|
|
18
|
+
capabilities: {
|
|
19
|
+
name: string;
|
|
20
|
+
installed: boolean;
|
|
21
|
+
version: string | null;
|
|
22
|
+
enabled: boolean | null;
|
|
23
|
+
probe: string;
|
|
24
|
+
evidenceIntact: boolean;
|
|
25
|
+
evidence: string[];
|
|
26
|
+
}[];
|
|
27
|
+
isolation: {
|
|
28
|
+
verified: boolean;
|
|
29
|
+
accounting: string;
|
|
30
|
+
parentThreadId: string | undefined;
|
|
31
|
+
childThreadId: string | undefined;
|
|
32
|
+
evidence: string[];
|
|
33
|
+
reason: string | null;
|
|
34
|
+
} | {
|
|
35
|
+
verified: boolean;
|
|
36
|
+
accounting: string;
|
|
37
|
+
reason: string;
|
|
38
|
+
};
|
|
39
|
+
usage: {
|
|
40
|
+
totals: import("./types.js").Tokens;
|
|
41
|
+
coverage: {
|
|
42
|
+
tokenComplete: boolean;
|
|
43
|
+
modelsKnown: boolean;
|
|
44
|
+
effortsKnown: boolean;
|
|
45
|
+
comparisonEligible: boolean;
|
|
46
|
+
issues: import("./types.js").UsageIssue[];
|
|
47
|
+
};
|
|
48
|
+
sessions: import("./types.js").SessionUsage[];
|
|
49
|
+
cacheHitRate: number | null;
|
|
50
|
+
cost: {
|
|
51
|
+
kind: "actual" | "estimated" | "unavailable";
|
|
52
|
+
usd: number | null;
|
|
53
|
+
source: string | null;
|
|
54
|
+
complete: boolean;
|
|
55
|
+
};
|
|
56
|
+
} | null;
|
|
57
|
+
gaps: string[];
|
|
58
|
+
note: string;
|
|
59
|
+
};
|
|
60
|
+
export declare function inspectIsolation(probe: {
|
|
61
|
+
parent: string;
|
|
62
|
+
child: string;
|
|
63
|
+
parentOnlyMarker: string;
|
|
64
|
+
}): {
|
|
65
|
+
verified: boolean;
|
|
66
|
+
accounting: string;
|
|
67
|
+
parentThreadId: string | undefined;
|
|
68
|
+
childThreadId: string | undefined;
|
|
69
|
+
evidence: string[];
|
|
70
|
+
reason: string | null;
|
|
71
|
+
};
|
|
72
|
+
//# sourceMappingURL=doctor.d.ts.map
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { findProjectRoot, loadConfig } from '../config/load.js';
|
|
2
|
+
import { resolveAdapterEntry, resolveHookEntry } from '../marketplace/adapters.js';
|
|
3
|
+
import { AppError } from '../shared/errors.js';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { execFile } from '../shared/exec.js';
|
|
7
|
+
import { sha256 } from '../shared/hash.js';
|
|
8
|
+
import { analyzeUsage } from './analyze.js';
|
|
9
|
+
import { object, string } from './parse.js';
|
|
10
|
+
const EvidenceSchema = z.object({
|
|
11
|
+
capabilities: z.array(z.object({ name: z.string(), status: z.enum(['passed', 'failed', 'blocked']),
|
|
12
|
+
files: z.array(z.object({ path: z.string(), sha256: z.string().regex(/^[a-f0-9]{64}$/) }).strict()).min(1),
|
|
13
|
+
}).strict()).default([]),
|
|
14
|
+
requiredCapabilities: z.array(z.string().min(1)).optional(),
|
|
15
|
+
isolation: z.object({ parent: z.string(), child: z.string(), parentOnlyMarker: z.string().min(16) }).strict().optional(),
|
|
16
|
+
}).strict();
|
|
17
|
+
export function costCapabilities(cwd, explicit = []) {
|
|
18
|
+
const names = new Set(['openspec', ...explicit]);
|
|
19
|
+
const gaps = [];
|
|
20
|
+
let root;
|
|
21
|
+
try {
|
|
22
|
+
root = findProjectRoot(cwd);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (error instanceof AppError && error.code === 'project_uninitialized')
|
|
26
|
+
return { names: [...names], gaps };
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
const config = loadConfig(root);
|
|
30
|
+
for (const plugin of config.plugins ?? [])
|
|
31
|
+
names.add(plugin.name);
|
|
32
|
+
for (const hook of config.hooks ?? []) {
|
|
33
|
+
if (!hook.use)
|
|
34
|
+
continue;
|
|
35
|
+
try {
|
|
36
|
+
const resolved = hook.stage === 'collect' || hook.operation === 'preview' ? resolveAdapterEntry(root, hook.use) : resolveHookEntry(root, hook.use);
|
|
37
|
+
names.add(resolved.identity.name);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
gaps.push(`hook:${hook.id}:${error instanceof AppError ? error.code : 'invalid'}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { names: [...names], gaps };
|
|
44
|
+
}
|
|
45
|
+
export function costDoctor(options) {
|
|
46
|
+
// No installation, configuration updates, authentication, or model calls.
|
|
47
|
+
const version = execFile(options.host, ['--version'], { cwd: options.cwd, timeoutMs: 10_000 });
|
|
48
|
+
const listing = execFile(options.host, options.host === 'codex' ? ['plugin', 'list', '--json'] : ['plugin', 'list', '--json'], { cwd: options.cwd, timeoutMs: 10_000 });
|
|
49
|
+
let inventory = [];
|
|
50
|
+
try {
|
|
51
|
+
const value = JSON.parse(listing.stdout);
|
|
52
|
+
const rows = Array.isArray(value) ? value : object(value).installed;
|
|
53
|
+
inventory = (Array.isArray(rows) ? rows : []).map((entry) => {
|
|
54
|
+
const row = object(entry);
|
|
55
|
+
return {
|
|
56
|
+
name: string(row.name) ?? String(row.id ?? row.pluginId ?? '').split('@')[0], version: string(row.version),
|
|
57
|
+
enabled: typeof row.enabled === 'boolean' ? row.enabled : null,
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch { /* Missing or incompatible native inventory is a diagnostic gap. */ }
|
|
62
|
+
const evidence = options.evidence ? EvidenceSchema.parse(JSON.parse(readFileSync(options.evidence, 'utf8'))) : { capabilities: [] };
|
|
63
|
+
const requirements = costCapabilities(options.cwd, [...(options.capabilities ?? []), ...(evidence.requiredCapabilities ?? [])]);
|
|
64
|
+
const capabilities = requirements.names.map((name) => {
|
|
65
|
+
const installed = inventory.find((entry) => entry.name === name);
|
|
66
|
+
const probe = evidence.capabilities.find((entry) => entry.name === name);
|
|
67
|
+
const evidenceIntact = Boolean(probe && probe.files.every((file) => {
|
|
68
|
+
try {
|
|
69
|
+
return sha256(readFileSync(file.path)) === file.sha256;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}));
|
|
75
|
+
return { name, installed: Boolean(installed), version: installed?.version ?? null, enabled: installed?.enabled ?? null,
|
|
76
|
+
probe: probe?.status ?? 'missing', evidenceIntact, evidence: probe?.files.map((file) => file.path) ?? [] };
|
|
77
|
+
});
|
|
78
|
+
const usage = options.sessions?.length ? analyzeUsage({ changeName: 'doctor', sessions: options.sessions, expectedModel: options.model, expectedEffort: options.effort }) : null;
|
|
79
|
+
const isolation = evidence.isolation && options.host === 'codex' ? inspectIsolation(evidence.isolation) : { verified: false, accounting: 'unknown', reason: 'probe_missing' };
|
|
80
|
+
const gaps = [
|
|
81
|
+
...requirements.gaps,
|
|
82
|
+
...(!version.ok ? ['host_cli_unavailable'] : []),
|
|
83
|
+
...(!listing.ok ? ['plugin_inventory_unavailable'] : []),
|
|
84
|
+
...capabilities.filter((item) => !item.installed || item.enabled === false || item.probe !== 'passed' || !item.evidenceIntact).map((item) => `capability:${item.name}`),
|
|
85
|
+
...(!usage?.coverage.comparisonEligible ? ['usage_or_model_unverified'] : []),
|
|
86
|
+
...(!isolation.verified ? ['isolation_or_child_accounting_unverified'] : []),
|
|
87
|
+
];
|
|
88
|
+
return { ok: gaps.length === 0, readOnly: true, host: options.host, version: version.ok ? version.stdout.trim() : null,
|
|
89
|
+
capabilities, isolation, usage: usage ? { totals: usage.totals, coverage: usage.coverage, sessions: usage.sessions,
|
|
90
|
+
cacheHitRate: usage.totals.cachedInput === null || usage.totals.input === 0 ? null : usage.totals.cachedInput / usage.totals.input, cost: usage.cost } : null,
|
|
91
|
+
gaps, note: '插件清单只证明注册;probe 状态来自指定实测记录并校验文件摘要。诊断不执行新探针,也不修改配置。' };
|
|
92
|
+
}
|
|
93
|
+
export function inspectIsolation(probe) {
|
|
94
|
+
const parent = readFileSync(probe.parent, 'utf8');
|
|
95
|
+
const child = readFileSync(probe.child, 'utf8');
|
|
96
|
+
const report = analyzeUsage({ changeName: 'isolation-probe', sessions: [probe.parent, probe.child] });
|
|
97
|
+
const rows = parent.split('\n').filter(Boolean).map((line) => object(JSON.parse(line)));
|
|
98
|
+
const calls = rows.filter((row) => row.type === 'response_item').map((row) => object(row.payload));
|
|
99
|
+
const noHistory = calls.some((call) => {
|
|
100
|
+
if (!String(call.name).endsWith('spawn_agent'))
|
|
101
|
+
return false;
|
|
102
|
+
try {
|
|
103
|
+
return object(JSON.parse(String(call.arguments))).fork_turns === 'none';
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
const parentSession = report.sessions.find((session) => session.files.includes(probe.parent));
|
|
110
|
+
const childSession = report.sessions.find((session) => session.files.includes(probe.child));
|
|
111
|
+
// Per-response records must sum to each thread's snapshots, with no shared
|
|
112
|
+
// response IDs. This checks exclusive accounting instead of trusting a label.
|
|
113
|
+
const requestIds = report.slices.map((slice) => slice.key);
|
|
114
|
+
const exclusive = report.sessions.every((session) => session.apiCalls !== null)
|
|
115
|
+
&& new Set(requestIds).size === requestIds.length && report.coverage.tokenComplete;
|
|
116
|
+
const verified = noHistory && parent.includes(probe.parentOnlyMarker) && !child.includes(probe.parentOnlyMarker)
|
|
117
|
+
&& childSession?.parentThreadId === parentSession?.threadId && Boolean(parentSession && childSession) && exclusive;
|
|
118
|
+
return { verified, accounting: exclusive ? 'exclusive-per-thread' : 'unknown', parentThreadId: parentSession?.threadId,
|
|
119
|
+
childThreadId: childSession?.threadId, evidence: [probe.parent, probe.child], reason: verified ? null : 'probe_not_conclusive' };
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=doctor.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Tokens, UsageIssue, UsageSlice, SessionUsage } from './types.js';
|
|
2
|
+
type Obj = Record<string, unknown>;
|
|
3
|
+
export interface UsageStream {
|
|
4
|
+
threadId: string;
|
|
5
|
+
parentThreadId: string | null;
|
|
6
|
+
provider: 'codex' | 'claude';
|
|
7
|
+
source: SessionUsage['source'];
|
|
8
|
+
file: string;
|
|
9
|
+
slices: UsageSlice[];
|
|
10
|
+
issues: UsageIssue[];
|
|
11
|
+
tools: Set<string>;
|
|
12
|
+
spawns: Set<string>;
|
|
13
|
+
epochs: number;
|
|
14
|
+
startedAt: string | null;
|
|
15
|
+
endedAt: string | null;
|
|
16
|
+
collabObserved: boolean;
|
|
17
|
+
apiCalls: number | null;
|
|
18
|
+
toolMessages: Map<string, string>;
|
|
19
|
+
}
|
|
20
|
+
export declare const object: (value: unknown) => Obj;
|
|
21
|
+
export declare const string: (value: unknown) => string | null;
|
|
22
|
+
export declare const number: (value: unknown) => number | null;
|
|
23
|
+
export declare const zeroTokens: () => Tokens;
|
|
24
|
+
export declare function sumTokens(values: Tokens[]): Tokens;
|
|
25
|
+
export declare function mergeClaudeTokens(a: Tokens, b: Tokens): Tokens;
|
|
26
|
+
export declare function parseUsageStream(file: string, text: string): UsageStream;
|
|
27
|
+
export {};
|
|
28
|
+
//# sourceMappingURL=parse.d.ts.map
|