@ai-dossier/cli 0.51.0 → 0.54.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.
Files changed (57) hide show
  1. package/README.md +73 -6
  2. package/dist/batch-suite-runner.d.ts +30 -6
  3. package/dist/batch-suite-runner.d.ts.map +1 -1
  4. package/dist/batch-suite-runner.js +86 -25
  5. package/dist/batch-suite-runner.js.map +1 -1
  6. package/dist/capability.d.ts +8 -0
  7. package/dist/capability.d.ts.map +1 -1
  8. package/dist/capability.js +16 -1
  9. package/dist/capability.js.map +1 -1
  10. package/dist/cli.js +2 -0
  11. package/dist/cli.js.map +1 -1
  12. package/dist/commands/classify.d.ts.map +1 -1
  13. package/dist/commands/classify.js +8 -2
  14. package/dist/commands/classify.js.map +1 -1
  15. package/dist/commands/run.d.ts.map +1 -1
  16. package/dist/commands/run.js +10 -1
  17. package/dist/commands/run.js.map +1 -1
  18. package/dist/commands/sched.d.ts.map +1 -1
  19. package/dist/commands/sched.js +35 -0
  20. package/dist/commands/sched.js.map +1 -1
  21. package/dist/commands/usage.d.ts +84 -0
  22. package/dist/commands/usage.d.ts.map +1 -0
  23. package/dist/commands/usage.js +395 -0
  24. package/dist/commands/usage.js.map +1 -0
  25. package/dist/prescreen.d.ts +48 -5
  26. package/dist/prescreen.d.ts.map +1 -1
  27. package/dist/prescreen.js +195 -6
  28. package/dist/prescreen.js.map +1 -1
  29. package/dist/usage/collect-claude.d.ts +47 -0
  30. package/dist/usage/collect-claude.d.ts.map +1 -0
  31. package/dist/usage/collect-claude.js +237 -0
  32. package/dist/usage/collect-claude.js.map +1 -0
  33. package/dist/usage/collect-dispatch.d.ts +73 -0
  34. package/dist/usage/collect-dispatch.d.ts.map +1 -0
  35. package/dist/usage/collect-dispatch.js +297 -0
  36. package/dist/usage/collect-dispatch.js.map +1 -0
  37. package/dist/usage/collect-opencode.d.ts +74 -0
  38. package/dist/usage/collect-opencode.d.ts.map +1 -0
  39. package/dist/usage/collect-opencode.js +198 -0
  40. package/dist/usage/collect-opencode.js.map +1 -0
  41. package/dist/usage/host-session.d.ts +63 -0
  42. package/dist/usage/host-session.d.ts.map +1 -0
  43. package/dist/usage/host-session.js +325 -0
  44. package/dist/usage/host-session.js.map +1 -0
  45. package/dist/usage/ledger.d.ts +87 -0
  46. package/dist/usage/ledger.d.ts.map +1 -0
  47. package/dist/usage/ledger.js +268 -0
  48. package/dist/usage/ledger.js.map +1 -0
  49. package/dist/usage/types.d.ts +80 -0
  50. package/dist/usage/types.d.ts.map +1 -0
  51. package/dist/usage/types.js +11 -0
  52. package/dist/usage/types.js.map +1 -0
  53. package/dist/usage/util.d.ts +36 -0
  54. package/dist/usage/util.d.ts.map +1 -0
  55. package/dist/usage/util.js +158 -0
  56. package/dist/usage/util.js.map +1 -0
  57. package/package.json +2 -2
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Scheduler + dossier-run attribution for the usage ledger (#769).
3
+ *
4
+ * Tokens are NOT read from here: every sched-dispatched agent's tokens are
5
+ * already in the Claude Code transcripts / opencode.db on the same host, and
6
+ * `runs.jsonl`'s token columns would double-count them. What these sources
7
+ * add is *attribution*:
8
+ *
9
+ * - sched dispatch logs (`~/.dossier/sched/<project>/runs/*.log`) name the
10
+ * unit/batch/issue in their filename and carry the agent's own session id
11
+ * in the stream (opencode `sessionID`, claude `session_id`) → an
12
+ * authoritative session → issue/batch join.
13
+ * - sched `events.jsonl` records dispatch-failure/-unhealthy walls (#629/#505)
14
+ * → limit events.
15
+ * - `runs.jsonl` entries written with `session_id` (#769) → how many
16
+ * `ai-dossier run` invocations each session made.
17
+ */
18
+ import type { LimitEvent, UsageRow } from './types';
19
+ /** A session that a sched dispatch log proves belongs to a unit. */
20
+ export interface DispatchRef {
21
+ session_id: string;
22
+ unit: string;
23
+ issue: number | null;
24
+ batch: string | null;
25
+ /** `member` | `fix` | `tail` | `report` | `issue` | `other`. */
26
+ role: string;
27
+ /** sched project slug (`owner-repo`). */
28
+ project: string;
29
+ /** Model from the dispatch preamble's argv, when it named one. */
30
+ model: string | null;
31
+ log: string;
32
+ }
33
+ /** Filename → unit attribution. Mirrors `packages/sched`'s `unitLogName`-based builders. */
34
+ export declare function parseDispatchLogName(name: string): Pick<DispatchRef, 'unit' | 'issue' | 'batch' | 'role'> | null;
35
+ /**
36
+ * Every agent session in a dispatch log, each with the model of the
37
+ * `sched-dispatch` preamble that precedes it (logs are append-mode: a
38
+ * redispatched unit holds several dispatches, possibly on different agents).
39
+ * Only a record's TOP-LEVEL `sessionID` (opencode) / `session_id` (claude)
40
+ * counts — a tool call's input or output that merely mentions another
41
+ * session id must not attribute that session to this unit.
42
+ */
43
+ export declare function sessionsIn(content: string): {
44
+ session_id: string;
45
+ model: string | null;
46
+ }[];
47
+ /** Session ids only — see {@link sessionsIn}. */
48
+ export declare function sessionIdsIn(content: string): string[];
49
+ /**
50
+ * session id → dispatch attribution, across every sched project under
51
+ * `schedRoot` (`~/.dossier/sched`). Logs last modified before `sinceMs`
52
+ * cannot hold a session active in the window and are skipped unread.
53
+ */
54
+ export declare function indexDispatchLogs(schedRoot: string, sinceMs: number): Map<string, DispatchRef>;
55
+ /**
56
+ * Stamp dispatch attribution onto rows in place. A subagent/child session
57
+ * inherits its parent's dispatch (a batch member's opencode child session is
58
+ * still that member's spend). Dispatch attribution replaces the branch
59
+ * heuristic; a row whose store recorded no model falls back to the
60
+ * preamble's model.
61
+ */
62
+ export declare function applyDispatchIndex(rows: UsageRow[], index: Map<string, DispatchRef>): void;
63
+ /**
64
+ * Provider named by a limit message — sched events record none, but the
65
+ * provider's own wording does (`claude.ai/settings/usage` → anthropic). Null
66
+ * when it cannot be told, so the view compares against every provider.
67
+ */
68
+ export declare function providerFromText(text: string): string | null;
69
+ /** sched `dispatch-failure` / `dispatch-unhealthy` limit walls in `[sinceMs, untilMs)`. */
70
+ export declare function collectSchedLimitEvents(schedRoot: string, sinceMs: number, untilMs: number, host: string): LimitEvent[];
71
+ /** session id → number of `ai-dossier run` invocations recorded from it in `[sinceMs, untilMs)`. */
72
+ export declare function countDossierRuns(runsLog: string, sinceMs: number, untilMs: number): Map<string, number>;
73
+ //# sourceMappingURL=collect-dispatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collect-dispatch.d.ts","sourceRoot":"","sources":["../../src/usage/collect-dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAMpD,oEAAoE;AACpE,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,4FAA4F;AAC5F,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,GACX,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC,GAAG,IAAI,CAa/D;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EAAE,CAsB1F;AAKD,iDAAiD;AACjD,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAEtD;AAmBD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAmB9F;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,IAAI,CA6B1F;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM5D;AAED,2FAA2F;AAC3F,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,GACX,UAAU,EAAE,CAkCd;AAED,oGAAoG;AACpG,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,GACd,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAiBrB"}
@@ -0,0 +1,297 @@
1
+ "use strict";
2
+ /**
3
+ * Scheduler + dossier-run attribution for the usage ledger (#769).
4
+ *
5
+ * Tokens are NOT read from here: every sched-dispatched agent's tokens are
6
+ * already in the Claude Code transcripts / opencode.db on the same host, and
7
+ * `runs.jsonl`'s token columns would double-count them. What these sources
8
+ * add is *attribution*:
9
+ *
10
+ * - sched dispatch logs (`~/.dossier/sched/<project>/runs/*.log`) name the
11
+ * unit/batch/issue in their filename and carry the agent's own session id
12
+ * in the stream (opencode `sessionID`, claude `session_id`) → an
13
+ * authoritative session → issue/batch join.
14
+ * - sched `events.jsonl` records dispatch-failure/-unhealthy walls (#629/#505)
15
+ * → limit events.
16
+ * - `runs.jsonl` entries written with `session_id` (#769) → how many
17
+ * `ai-dossier run` invocations each session made.
18
+ */
19
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ var desc = Object.getOwnPropertyDescriptor(m, k);
22
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
23
+ desc = { enumerable: true, get: function() { return m[k]; } };
24
+ }
25
+ Object.defineProperty(o, k2, desc);
26
+ }) : (function(o, m, k, k2) {
27
+ if (k2 === undefined) k2 = k;
28
+ o[k2] = m[k];
29
+ }));
30
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
31
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
32
+ }) : function(o, v) {
33
+ o["default"] = v;
34
+ });
35
+ var __importStar = (this && this.__importStar) || (function () {
36
+ var ownKeys = function(o) {
37
+ ownKeys = Object.getOwnPropertyNames || function (o) {
38
+ var ar = [];
39
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
40
+ return ar;
41
+ };
42
+ return ownKeys(o);
43
+ };
44
+ return function (mod) {
45
+ if (mod && mod.__esModule) return mod;
46
+ var result = {};
47
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
48
+ __setModuleDefault(result, mod);
49
+ return result;
50
+ };
51
+ })();
52
+ Object.defineProperty(exports, "__esModule", { value: true });
53
+ exports.parseDispatchLogName = parseDispatchLogName;
54
+ exports.sessionsIn = sessionsIn;
55
+ exports.sessionIdsIn = sessionIdsIn;
56
+ exports.indexDispatchLogs = indexDispatchLogs;
57
+ exports.applyDispatchIndex = applyDispatchIndex;
58
+ exports.providerFromText = providerFromText;
59
+ exports.collectSchedLimitEvents = collectSchedLimitEvents;
60
+ exports.countDossierRuns = countDossierRuns;
61
+ const fs = __importStar(require("node:fs"));
62
+ const path = __importStar(require("node:path"));
63
+ const sched_1 = require("@ai-dossier/sched");
64
+ const util_1 = require("./util");
65
+ /** Most bytes of one dispatch log scanned for session ids (streams can be large). */
66
+ const MAX_LOG_SCAN_BYTES = 32 * 1024 * 1024;
67
+ /** Filename → unit attribution. Mirrors `packages/sched`'s `unitLogName`-based builders. */
68
+ function parseDispatchLogName(name) {
69
+ let m = name.match(/^batch-(.+)-m\d+-(\d+)\.log$/);
70
+ if (m)
71
+ return { unit: `issue:${m[2]}`, issue: Number(m[2]), batch: m[1], role: 'member' };
72
+ m = name.match(/^batch-(.+)-fix-(\d+)\.log$/);
73
+ if (m)
74
+ return { unit: `issue:${m[2]}`, issue: Number(m[2]), batch: m[1], role: 'fix' };
75
+ m = name.match(/^batch-(.+)-(tail|report)\.log$/);
76
+ if (m)
77
+ return { unit: `batch:${m[1]}`, issue: null, batch: m[1], role: m[2] };
78
+ m = name.match(/^issue-(\d+)\.log$/);
79
+ if (m)
80
+ return { unit: `issue:${m[1]}`, issue: Number(m[1]), batch: null, role: 'issue' };
81
+ if (name.endsWith('.log')) {
82
+ return { unit: name.slice(0, -'.log'.length), issue: null, batch: null, role: 'other' };
83
+ }
84
+ return null;
85
+ }
86
+ /**
87
+ * Every agent session in a dispatch log, each with the model of the
88
+ * `sched-dispatch` preamble that precedes it (logs are append-mode: a
89
+ * redispatched unit holds several dispatches, possibly on different agents).
90
+ * Only a record's TOP-LEVEL `sessionID` (opencode) / `session_id` (claude)
91
+ * counts — a tool call's input or output that merely mentions another
92
+ * session id must not attribute that session to this unit.
93
+ */
94
+ function sessionsIn(content) {
95
+ const found = new Map();
96
+ let model = null;
97
+ for (const line of content.split('\n')) {
98
+ const cmd = (0, sched_1.parsePreambleLine)(line);
99
+ if (cmd) {
100
+ model = (0, sched_1.modelFromCmd)(cmd);
101
+ continue;
102
+ }
103
+ if (!line.includes('"sessionID"') && !line.includes('"session_id"'))
104
+ continue;
105
+ let record;
106
+ try {
107
+ record = JSON.parse(line);
108
+ }
109
+ catch {
110
+ continue;
111
+ }
112
+ const id = record.sessionID ?? record.session_id;
113
+ if (typeof id === 'string' && SESSION_ID_SHAPE.test(id) && !found.has(id)) {
114
+ found.set(id, model);
115
+ }
116
+ }
117
+ return [...found].map(([session_id, m]) => ({ session_id, model: m }));
118
+ }
119
+ const SESSION_ID_SHAPE = /^(?:ses_[A-Za-z0-9]+|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/;
120
+ /** Session ids only — see {@link sessionsIn}. */
121
+ function sessionIdsIn(content) {
122
+ return sessionsIn(content).map((s) => s.session_id);
123
+ }
124
+ function readBounded(file) {
125
+ try {
126
+ const size = fs.statSync(file).size;
127
+ if (size <= MAX_LOG_SCAN_BYTES)
128
+ return fs.readFileSync(file, 'utf-8');
129
+ const fd = fs.openSync(file, 'r');
130
+ try {
131
+ const buffer = Buffer.allocUnsafe(MAX_LOG_SCAN_BYTES);
132
+ const read = fs.readSync(fd, buffer, 0, MAX_LOG_SCAN_BYTES, 0);
133
+ return buffer.subarray(0, read).toString('utf-8');
134
+ }
135
+ finally {
136
+ fs.closeSync(fd);
137
+ }
138
+ }
139
+ catch {
140
+ return null;
141
+ }
142
+ }
143
+ /**
144
+ * session id → dispatch attribution, across every sched project under
145
+ * `schedRoot` (`~/.dossier/sched`). Logs last modified before `sinceMs`
146
+ * cannot hold a session active in the window and are skipped unread.
147
+ */
148
+ function indexDispatchLogs(schedRoot, sinceMs) {
149
+ const index = new Map();
150
+ for (const project of (0, util_1.safeReaddir)(schedRoot)) {
151
+ if (!project.isDirectory())
152
+ continue;
153
+ const runsDir = path.join(schedRoot, project.name, 'runs');
154
+ for (const entry of (0, util_1.safeReaddir)(runsDir)) {
155
+ if (!entry.isFile())
156
+ continue;
157
+ const parsed = parseDispatchLogName(entry.name);
158
+ if (!parsed)
159
+ continue;
160
+ const log = path.join(runsDir, entry.name);
161
+ if (((0, util_1.mtimeMs)(log) ?? 0) < sinceMs)
162
+ continue;
163
+ const content = readBounded(log);
164
+ if (!content)
165
+ continue;
166
+ for (const { session_id, model } of sessionsIn(content)) {
167
+ index.set(session_id, { session_id, ...parsed, project: project.name, model, log });
168
+ }
169
+ }
170
+ }
171
+ return index;
172
+ }
173
+ /**
174
+ * Stamp dispatch attribution onto rows in place. A subagent/child session
175
+ * inherits its parent's dispatch (a batch member's opencode child session is
176
+ * still that member's spend). Dispatch attribution replaces the branch
177
+ * heuristic; a row whose store recorded no model falls back to the
178
+ * preamble's model.
179
+ */
180
+ function applyDispatchIndex(rows, index) {
181
+ if (index.size === 0)
182
+ return;
183
+ // session → parent, so a grandchild (a subagent's own subagent) still
184
+ // reaches the dispatched root.
185
+ const parentOf = new Map();
186
+ for (const row of rows) {
187
+ if (row.parent_session_id)
188
+ parentOf.set(row.session_id, row.parent_session_id);
189
+ }
190
+ const refFor = (session) => {
191
+ let current = session;
192
+ for (let depth = 0; current && depth < 16; depth++) {
193
+ const ref = index.get(current);
194
+ if (ref)
195
+ return ref;
196
+ current = parentOf.get(current);
197
+ }
198
+ return undefined;
199
+ };
200
+ for (const row of rows) {
201
+ const ref = refFor(row.session_id) ?? (row.parent_session_id ? refFor(row.parent_session_id) : undefined);
202
+ if (!ref)
203
+ continue;
204
+ row.unit = ref.unit;
205
+ row.batch = ref.batch;
206
+ if (ref.issue !== null) {
207
+ row.issue = ref.issue;
208
+ row.issue_source = 'dispatch';
209
+ }
210
+ if (row.model === null && ref.model)
211
+ row.model = ref.model;
212
+ }
213
+ }
214
+ /**
215
+ * Provider named by a limit message — sched events record none, but the
216
+ * provider's own wording does (`claude.ai/settings/usage` → anthropic). Null
217
+ * when it cannot be told, so the view compares against every provider.
218
+ */
219
+ function providerFromText(text) {
220
+ if (/claude|anthropic/i.test(text))
221
+ return 'anthropic';
222
+ if (/openai|chatgpt|codex/i.test(text))
223
+ return 'openai';
224
+ if (/z\.ai|zhipu|glm/i.test(text))
225
+ return 'zai';
226
+ if (/alibaba|dashscope|qwen/i.test(text))
227
+ return 'alibaba';
228
+ return null;
229
+ }
230
+ /** sched `dispatch-failure` / `dispatch-unhealthy` limit walls in `[sinceMs, untilMs)`. */
231
+ function collectSchedLimitEvents(schedRoot, sinceMs, untilMs, host) {
232
+ const out = [];
233
+ for (const project of (0, util_1.safeReaddir)(schedRoot)) {
234
+ if (!project.isDirectory())
235
+ continue;
236
+ const file = path.join(schedRoot, project.name, 'events.jsonl');
237
+ if (((0, util_1.mtimeMs)(file) ?? 0) < sinceMs)
238
+ continue;
239
+ (0, util_1.forEachLine)(file, (line) => {
240
+ if (!line.includes('"dispatch-'))
241
+ return;
242
+ let event;
243
+ try {
244
+ event = JSON.parse(line);
245
+ }
246
+ catch {
247
+ return;
248
+ }
249
+ if (event.event !== 'dispatch-failure' && event.event !== 'dispatch-unhealthy')
250
+ return;
251
+ const ts = typeof event.ts === 'string' ? event.ts : null;
252
+ const tsMs = ts ? Date.parse(ts) : Number.NaN;
253
+ if (!ts || Number.isNaN(tsMs) || tsMs < sinceMs || tsMs >= untilMs)
254
+ return;
255
+ const status = typeof event.api_error_status === 'number' ? event.api_error_status : null;
256
+ const detail = (0, util_1.oneLine)(event.detail);
257
+ if (status !== 429 && !util_1.LIMIT_TEXT_RE.test(detail))
258
+ return;
259
+ out.push({
260
+ ts,
261
+ host,
262
+ source: 'sched',
263
+ provider: providerFromText(detail),
264
+ session_id: null,
265
+ unit: typeof event.unit === 'string' ? event.unit : null,
266
+ status,
267
+ detail: `[${project.name}] ${event.event}: ${detail}`,
268
+ });
269
+ });
270
+ }
271
+ return out;
272
+ }
273
+ /** session id → number of `ai-dossier run` invocations recorded from it in `[sinceMs, untilMs)`. */
274
+ function countDossierRuns(runsLog, sinceMs, untilMs) {
275
+ const counts = new Map();
276
+ if (((0, util_1.mtimeMs)(runsLog) ?? 0) < sinceMs)
277
+ return counts;
278
+ (0, util_1.forEachLine)(runsLog, (line) => {
279
+ if (!line.includes('"session_id"'))
280
+ return;
281
+ let entry;
282
+ try {
283
+ entry = JSON.parse(line);
284
+ }
285
+ catch {
286
+ return;
287
+ }
288
+ if (!entry.session_id)
289
+ return;
290
+ const tsMs = Date.parse(entry.timestamp);
291
+ if (Number.isNaN(tsMs) || tsMs < sinceMs || tsMs >= untilMs)
292
+ return;
293
+ counts.set(entry.session_id, (counts.get(entry.session_id) ?? 0) + 1);
294
+ });
295
+ return counts;
296
+ }
297
+ //# sourceMappingURL=collect-dispatch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collect-dispatch.js","sourceRoot":"","sources":["../../src/usage/collect-dispatch.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * opencode collector (#769): per-message usage from opencode's SQLite store
3
+ * (`~/.local/share/opencode/opencode.db`, `message.data` JSON with
4
+ * `modelID`/`providerID`/`tokens`/`cost`).
5
+ *
6
+ * Read-only by construction: the database is opened with `readOnly: true`.
7
+ * The store can be very large (14 GB observed on hcc) and `message` has no
8
+ * time index, so the query never scans `message` by time — it selects the
9
+ * (small) `session` table by `time_updated`, then reads each session's
10
+ * messages through the `(session_id, time_created, id)` index.
11
+ *
12
+ * Uses the built-in `node:sqlite` (Node >= 22.13 unflagged). Where it is
13
+ * unavailable (Node 20) the collector reports `unavailable` rather than
14
+ * failing the whole view.
15
+ */
16
+ import type { UsageRow } from './types';
17
+ /** Session metadata the collector needs. */
18
+ export interface OpenCodeSession {
19
+ id: string;
20
+ parent_id: string | null;
21
+ directory: string | null;
22
+ title: string | null;
23
+ }
24
+ /** One `message` row as stored (`data` is the JSON payload). */
25
+ export interface OpenCodeMessage {
26
+ id: string;
27
+ time_created: number;
28
+ data: string;
29
+ }
30
+ /** The narrow read surface the collector uses — injectable for tests. */
31
+ export interface OpenCodeReader {
32
+ sessionsUpdatedSince(sinceMs: number): OpenCodeSession[];
33
+ messagesSince(sessionId: string, sinceMs: number): OpenCodeMessage[];
34
+ close(): void;
35
+ }
36
+ export type OpenCodeOpenResult = {
37
+ status: 'ok';
38
+ reader: OpenCodeReader;
39
+ } | {
40
+ status: 'unavailable' | 'error';
41
+ detail: string;
42
+ };
43
+ /** Default opencode DB path, honouring `OPENCODE_DB` and `XDG_DATA_HOME`. */
44
+ export declare function defaultOpenCodeDbPath(env?: NodeJS.ProcessEnv): string;
45
+ interface SqliteStatement {
46
+ all(...params: unknown[]): unknown[];
47
+ }
48
+ interface SqliteDatabase {
49
+ prepare(sql: string): SqliteStatement;
50
+ close(): void;
51
+ }
52
+ type SqliteModule = {
53
+ DatabaseSync: new (file: string, options?: {
54
+ readOnly?: boolean;
55
+ }) => SqliteDatabase;
56
+ };
57
+ /** `node:sqlite`, or null on a runtime without it. */
58
+ export declare function loadNodeSqlite(): SqliteModule | null;
59
+ /** Open `dbFile` read-only. Never throws. */
60
+ export declare function openOpenCodeDb(dbFile: string, sqlite?: SqliteModule | null): OpenCodeOpenResult;
61
+ export interface OpenCodeCollectOptions {
62
+ reader: OpenCodeReader;
63
+ sinceMs: number;
64
+ untilMs?: number;
65
+ host: string;
66
+ }
67
+ /**
68
+ * Usage rows for every assistant message with non-zero tokens in sessions
69
+ * active since `sinceMs`. A message still streaming (all-zero tokens) is
70
+ * skipped rather than recorded as a zero row.
71
+ */
72
+ export declare function collectOpenCode(opts: OpenCodeCollectOptions): UsageRow[];
73
+ export {};
74
+ //# sourceMappingURL=collect-opencode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collect-opencode.d.ts","sourceRoot":"","sources":["../../src/usage/collect-opencode.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAKH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAGxC,4CAA4C;AAC5C,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,gEAAgE;AAChE,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,yEAAyE;AACzE,MAAM,WAAW,cAAc;IAC7B,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe,EAAE,CAAC;IACzD,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,eAAe,EAAE,CAAC;IACrE,KAAK,IAAI,IAAI,CAAC;CACf;AAED,MAAM,MAAM,kBAAkB,GAC1B;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,cAAc,CAAA;CAAE,GACxC;IAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAExD,6EAA6E;AAC7E,wBAAgB,qBAAqB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAIlF;AAKD,UAAU,eAAe;IACvB,GAAG,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAAC;CACtC;AACD,UAAU,cAAc;IACtB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAAC;IACtC,KAAK,IAAI,IAAI,CAAC;CACf;AACD,KAAK,YAAY,GAAG;IAClB,YAAY,EAAE,KAAK,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,cAAc,CAAC;CACtF,CAAC;AAEF,sDAAsD;AACtD,wBAAgB,cAAc,IAAI,YAAY,GAAG,IAAI,CAoBpD;AAED,6CAA6C;AAC7C,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,YAAY,GAAG,IAAuB,GAC7C,kBAAkB,CAwCpB;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,cAAc,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,sBAAsB,GAAG,QAAQ,EAAE,CAyDxE"}
@@ -0,0 +1,198 @@
1
+ "use strict";
2
+ /**
3
+ * opencode collector (#769): per-message usage from opencode's SQLite store
4
+ * (`~/.local/share/opencode/opencode.db`, `message.data` JSON with
5
+ * `modelID`/`providerID`/`tokens`/`cost`).
6
+ *
7
+ * Read-only by construction: the database is opened with `readOnly: true`.
8
+ * The store can be very large (14 GB observed on hcc) and `message` has no
9
+ * time index, so the query never scans `message` by time — it selects the
10
+ * (small) `session` table by `time_updated`, then reads each session's
11
+ * messages through the `(session_id, time_created, id)` index.
12
+ *
13
+ * Uses the built-in `node:sqlite` (Node >= 22.13 unflagged). Where it is
14
+ * unavailable (Node 20) the collector reports `unavailable` rather than
15
+ * failing the whole view.
16
+ */
17
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ var desc = Object.getOwnPropertyDescriptor(m, k);
20
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
21
+ desc = { enumerable: true, get: function() { return m[k]; } };
22
+ }
23
+ Object.defineProperty(o, k2, desc);
24
+ }) : (function(o, m, k, k2) {
25
+ if (k2 === undefined) k2 = k;
26
+ o[k2] = m[k];
27
+ }));
28
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
29
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
30
+ }) : function(o, v) {
31
+ o["default"] = v;
32
+ });
33
+ var __importStar = (this && this.__importStar) || (function () {
34
+ var ownKeys = function(o) {
35
+ ownKeys = Object.getOwnPropertyNames || function (o) {
36
+ var ar = [];
37
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
38
+ return ar;
39
+ };
40
+ return ownKeys(o);
41
+ };
42
+ return function (mod) {
43
+ if (mod && mod.__esModule) return mod;
44
+ var result = {};
45
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
46
+ __setModuleDefault(result, mod);
47
+ return result;
48
+ };
49
+ })();
50
+ Object.defineProperty(exports, "__esModule", { value: true });
51
+ exports.defaultOpenCodeDbPath = defaultOpenCodeDbPath;
52
+ exports.loadNodeSqlite = loadNodeSqlite;
53
+ exports.openOpenCodeDb = openOpenCodeDb;
54
+ exports.collectOpenCode = collectOpenCode;
55
+ const fs = __importStar(require("node:fs"));
56
+ const os = __importStar(require("node:os"));
57
+ const path = __importStar(require("node:path"));
58
+ const util_1 = require("./util");
59
+ /** Default opencode DB path, honouring `OPENCODE_DB` and `XDG_DATA_HOME`. */
60
+ function defaultOpenCodeDbPath(env = process.env) {
61
+ if (env.OPENCODE_DB)
62
+ return env.OPENCODE_DB;
63
+ const dataHome = env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share');
64
+ return path.join(dataHome, 'opencode', 'opencode.db');
65
+ }
66
+ /** How far before the window start messages are read (a long-running message completes later than it is created). */
67
+ const MESSAGE_SLACK_MS = 60 * 60 * 1000;
68
+ /** `node:sqlite`, or null on a runtime without it. */
69
+ function loadNodeSqlite() {
70
+ try {
71
+ // Lazy: a static import would crash the whole CLI on Node 20.
72
+ // Node 22.13–22.x still emits an ExperimentalWarning on first load; it
73
+ // would land on stderr of every `ai-dossier run` inside opencode. Drop
74
+ // just that one warning while loading.
75
+ const emit = process.emitWarning;
76
+ process.emitWarning = ((warning, ...rest) => {
77
+ const text = typeof warning === 'string' ? warning : warning.message;
78
+ if (/sqlite/i.test(text))
79
+ return;
80
+ return emit.call(process, warning, ...rest);
81
+ });
82
+ try {
83
+ return require('node:sqlite');
84
+ }
85
+ finally {
86
+ process.emitWarning = emit;
87
+ }
88
+ }
89
+ catch {
90
+ return null;
91
+ }
92
+ }
93
+ /** Open `dbFile` read-only. Never throws. */
94
+ function openOpenCodeDb(dbFile, sqlite = loadNodeSqlite()) {
95
+ if (!fs.existsSync(dbFile))
96
+ return { status: 'unavailable', detail: `no database at ${dbFile}` };
97
+ if (!sqlite) {
98
+ return {
99
+ status: 'unavailable',
100
+ detail: `node:sqlite not available in Node ${process.versions.node} (needs >= 22.13)`,
101
+ };
102
+ }
103
+ let db;
104
+ try {
105
+ db = new sqlite.DatabaseSync(dbFile, { readOnly: true });
106
+ }
107
+ catch (err) {
108
+ return { status: 'error', detail: `cannot open ${dbFile}: ${err.message}` };
109
+ }
110
+ let sessions;
111
+ let messages;
112
+ try {
113
+ sessions = db.prepare('SELECT id, parent_id, directory, title FROM session WHERE time_updated >= ?');
114
+ // `time_created` bound is widened by MESSAGE_SLACK_MS: a row's timestamp is
115
+ // its COMPLETION time, so a message created just before the window and
116
+ // completed inside it must still be read; the collector re-filters exactly.
117
+ messages = db.prepare('SELECT id, time_created, data FROM message WHERE session_id = ? AND time_created >= ? ORDER BY time_created');
118
+ }
119
+ catch (err) {
120
+ // Not an opencode store of the expected shape (other version / wrong file).
121
+ db.close();
122
+ return { status: 'error', detail: `unexpected schema in ${dbFile}: ${err.message}` };
123
+ }
124
+ return {
125
+ status: 'ok',
126
+ reader: {
127
+ sessionsUpdatedSince: (sinceMs) => sessions.all(sinceMs),
128
+ messagesSince: (sessionId, sinceMs) => messages.all(sessionId, sinceMs - MESSAGE_SLACK_MS),
129
+ close: () => db.close(),
130
+ },
131
+ };
132
+ }
133
+ /**
134
+ * Usage rows for every assistant message with non-zero tokens in sessions
135
+ * active since `sinceMs`. A message still streaming (all-zero tokens) is
136
+ * skipped rather than recorded as a zero row.
137
+ */
138
+ function collectOpenCode(opts) {
139
+ const rows = [];
140
+ const untilMs = opts.untilMs ?? Number.POSITIVE_INFINITY;
141
+ for (const session of opts.reader.sessionsUpdatedSince(opts.sinceMs)) {
142
+ for (const message of opts.reader.messagesSince(session.id, opts.sinceMs)) {
143
+ let data;
144
+ try {
145
+ data = JSON.parse(message.data);
146
+ }
147
+ catch {
148
+ continue;
149
+ }
150
+ if (data.role !== 'assistant')
151
+ continue;
152
+ const tokens = (data.tokens ?? {});
153
+ const cache = (tokens.cache ?? {});
154
+ const input = (0, util_1.count)(tokens.input);
155
+ const output = (0, util_1.count)(tokens.output);
156
+ const reasoning = (0, util_1.count)(tokens.reasoning);
157
+ const cache_read = (0, util_1.count)(cache.read);
158
+ const cache_write = (0, util_1.count)(cache.write);
159
+ if (input + output + reasoning + cache_read + cache_write === 0)
160
+ continue;
161
+ const time = (data.time ?? {});
162
+ const tsMs = typeof time.completed === 'number' ? time.completed : message.time_created;
163
+ if (tsMs < opts.sinceMs || tsMs >= untilMs)
164
+ continue;
165
+ const pathInfo = (data.path ?? {});
166
+ const cwd = typeof pathInfo.cwd === 'string' ? pathInfo.cwd : session.directory;
167
+ const providerID = typeof data.providerID === 'string' ? data.providerID : null;
168
+ const modelID = typeof data.modelID === 'string' ? data.modelID : null;
169
+ const issue = (0, util_1.issueFromRef)(cwd ? path.basename(cwd) : null);
170
+ rows.push({
171
+ ts: new Date(tsMs).toISOString(),
172
+ host: opts.host,
173
+ provider: providerID ?? 'opencode',
174
+ // `provider/model` — the same id shape opencode's own `-m` flag takes.
175
+ model: modelID ? (providerID ? `${providerID}/${modelID}` : modelID) : null,
176
+ input,
177
+ output,
178
+ reasoning,
179
+ cache_read,
180
+ cache_write,
181
+ cost_usd: typeof data.cost === 'number' ? data.cost : null,
182
+ source: 'opencode',
183
+ session_id: session.id,
184
+ parent_session_id: session.parent_id ?? null,
185
+ title: session.title,
186
+ cwd,
187
+ project: (0, util_1.projectOf)(cwd),
188
+ branch: null,
189
+ issue,
190
+ issue_source: issue === null ? null : 'branch',
191
+ batch: null,
192
+ unit: null,
193
+ });
194
+ }
195
+ }
196
+ return rows;
197
+ }
198
+ //# sourceMappingURL=collect-opencode.js.map