@moxxy/plugin-usage-stats 0.28.0 → 0.29.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/dist/index.js CHANGED
@@ -111,7 +111,7 @@ export function buildUsageStatsPlugin(opts = {}) {
111
111
  // An unsubscribe that throws must not block the merge below.
112
112
  }
113
113
  clearUnsubs.delete(ctx.sessionId);
114
- const initMaxSeq = cursors.has(ctx.sessionId) ? cursors.get(ctx.sessionId) : null;
114
+ const initMaxSeq = cursors.get(ctx.sessionId) ?? null;
115
115
  cursors.delete(ctx.sessionId);
116
116
  // Best-effort: a throwing reader or a failed fold degrades to "record
117
117
  // nothing for this run" rather than rejecting the hook (which the host
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,sBAAsB,GAMvB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,eAAe,EAAE,eAAe,EAAwB,MAAM,aAAa,CAAC;AAuBrF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAAqC,EAAE;IAC3E,8EAA8E;IAC9E,wEAAwE;IACxE,2EAA2E;IAC3E,0EAA0E;IAC1E,2EAA2E;IAC3E,0EAA0E;IAC1E,+EAA+E;IAC/E,uDAAuD;IACvD,EAAE;IACF,wEAAwE;IACxE,8EAA8E;IAC9E,2EAA2E;IAC3E,kEAAkE;IAClE,MAAM,OAAO,GAAG,IAAI,GAAG,EAA4B,CAAC;IACpD,uEAAuE;IACvE,6CAA6C;IAC7C,MAAM,WAAW,GAAG,IAAI,GAAG,EAAyB,CAAC;IACrD,OAAO,YAAY,CAAC;QAClB,IAAI,EAAE,2BAA2B;QACjC,KAAK,EAAE;YACL,MAAM,CAAC,GAAG;gBACR,uEAAuE;gBACvE,qEAAqE;gBACrE,6BAA6B;gBAC7B,EAAE;gBACF,yEAAyE;gBACzE,mEAAmE;gBACnE,wEAAwE;gBACxE,wEAAwE;gBACxE,iEAAiE;gBACjE,iEAAiE;gBACjE,iEAAiE;gBACjE,0EAA0E;gBAC1E,IAAI,CAAC;oBACH,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;gBACnD,CAAC;gBAAC,MAAM,CAAC;oBACP,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;gBACnC,CAAC;gBACD,IAAI,CAAC;oBACH,MAAM,MAAM,GAAG,GAAG,CAAC,GAAsC,CAAC;oBAC1D,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,UAAU,EAAE,CAAC;wBACzC,uEAAuE;wBACvE,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC;wBACnC,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE;4BAChC,+DAA+D;4BAC/D,oEAAoE;4BACpE,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;wBACnC,CAAC,CAAC,CAAC;wBACH,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;oBACxC,CAAC;gBACH,CAAC;gBAAC,MAAM,CAAC;oBACP,mEAAmE;oBACnE,qEAAqE;oBACrE,qEAAqE;oBACrE,iDAAiD;gBACnD,CAAC;YACH,CAAC;YACD,KAAK,CAAC,UAAU,CAAC,GAAG;gBAClB,wEAAwE;gBACxE,yEAAyE;gBACzE,0EAA0E;gBAC1E,wDAAwD;gBACxD,IAAI,CAAC;oBACH,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC;gBACrC,CAAC;gBAAC,MAAM,CAAC;oBACP,6DAA6D;gBAC/D,CAAC;gBACD,WAAW,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;gBAClC,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,SAAS,CAAE,CAAC,CAAC,CAAC,IAAI,CAAC;gBACnF,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;gBAC9B,sEAAsE;gBACtE,uEAAuE;gBACvE,wEAAwE;gBACxE,uEAAuE;gBACvE,mEAAmE;gBACnE,wEAAwE;gBACxE,qDAAqD;gBAErD,uEAAuE;gBACvE,wEAAwE;gBACxE,6BAA6B;gBAC7B,IAAI,CAAC;oBACH,MAAM,SAAS,GAAG,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC;oBACtD,MAAM,IAAI,GACR,UAAU,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,UAAU,CAAC,CAAC;oBAChF,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;wBACpB,MAAM,KAAK,GAAG,sBAAsB,CAAC,IAAI,CAAC,CAAC;wBAC3C,MAAM,eAAe,CAAC,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;oBAC/C,CAAC;gBACH,CAAC;gBAAC,MAAM,CAAC;oBACP,sEAAsE;oBACtE,qEAAqE;gBACvE,CAAC;gBAED,yEAAyE;gBACzE,gEAAgE;gBAChE,IAAI,CAAC;oBACH,MAAM,OAAO,GAAG,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;oBAChD,MAAM,OAAO,GAAG,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;oBAChD,MAAM,UAAU,GAAG,oBAAoB,CACrC,UAAU,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,UAAU,CAAC,EACzE,UAAU,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,UAAU,CAAC,CAC1E,CAAC;oBACF,sEAAsE;oBACtE,IAAI,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;wBACvC,MAAM,eAAe,CAAC,UAAU,EAAE,IAAI,CAAC,cAAc,CAAC,CAAC;oBACzD,CAAC;gBACH,CAAC;gBAAC,MAAM,CAAC;oBACP,gEAAgE;oBAChE,iDAAiD;gBACnD,CAAC;YACH,CAAC;SACF;KACF,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAClC,OAAyC,EACzC,OAAyC;IAEzC,MAAM,GAAG,GAAG,IAAI,GAAG,EAA+E,CAAC;IACnG,MAAM,KAAK,GAAG,CAAC,IAAY,EAAE,EAAE;QAC7B,IAAI,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACtB,IAAI,CAAC,CAAC,EAAE,CAAC;YACP,CAAC,GAAG,EAAE,WAAW,EAAE,CAAC,EAAE,CAAC;YACvB,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QACnB,CAAC;QACD,OAAO,CAAC,CAAC;IACX,CAAC,CAAC;IACF,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAC1B,GAAG,CAAC,WAAW,IAAI,CAAC,CAAC;QACrB,MAAM,GAAG,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;QACzC,IAAI,CAAC,GAAG,CAAC,aAAa,IAAI,GAAG,GAAG,GAAG,CAAC,aAAa;YAAE,GAAG,CAAC,aAAa,GAAG,GAAG,CAAC;IAC7E,CAAC;IACD,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAC1B,MAAM,GAAG,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;QACzC,6EAA6E;QAC7E,IAAI,CAAC,GAAG,CAAC,SAAS,IAAI,GAAG,GAAG,GAAG,CAAC,SAAS;YAAE,GAAG,CAAC,SAAS,GAAG,GAAG,CAAC;IACjE,CAAC;IACD,MAAM,GAAG,GAAoC,EAAE,CAAC;IAChD,KAAK,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,GAAG,EAAE,CAAC;QAC5B,GAAG,CAAC,IAAI,CAAC,GAAG;YACV,WAAW,EAAE,CAAC,CAAC,WAAW;YAC1B,GAAG,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,CAAC,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC9D,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACnD,CAAC;IACJ,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,wEAAwE;AACxE,6EAA6E;AAC7E,wEAAwE;AACxE,iFAAiF;AACjF,2EAA2E;AAC3E,MAAM,kBAAkB,GAAG,CAAC,mBAAmB,EAAE,eAAe,EAAE,eAAe,CAAU,CAAC;AAE5F;;;;;;;;;GASG;AACH,SAAS,WAAW,CAAC,GAAoC;IACvD,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAClC,IAAI,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ;QAAE,OAAO,GAAG,CAAC,OAAO,GAAG,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC;IACzE,IAAI,GAAG,GAAkB,IAAI,CAAC;IAC9B,KAAK,MAAM,IAAI,IAAI,kBAAkB,EAAE,CAAC;QACtC,KAAK,MAAM,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;YAAE,IAAI,GAAG,KAAK,IAAI,IAAI,CAAC,CAAC,GAAG,GAAG,GAAG;gBAAE,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC;IACjF,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,eAAe,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moxxy/plugin-usage-stats",
3
- "version": "0.28.0",
3
+ "version": "0.29.0",
4
4
  "description": "Records cross-session token usage. On session close it folds this run's provider_response events by provider/model and merges the delta into ~/.moxxy/usage.json (a forward-going aggregate surfaced in the /usage panel and reset by /usage clear).",
5
5
  "keywords": [
6
6
  "moxxy",
@@ -43,15 +43,15 @@
43
43
  }
44
44
  },
45
45
  "dependencies": {
46
- "@moxxy/core": "0.28.0",
47
- "@moxxy/sdk": "0.28.0"
46
+ "@moxxy/sdk": "0.29.0",
47
+ "@moxxy/core": "0.29.0"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@types/node": "^22.10.0",
51
51
  "typescript": "^5.7.3",
52
52
  "vitest": "^2.1.8",
53
- "@moxxy/vitest-preset": "0.0.0",
54
- "@moxxy/tsconfig": "0.0.0"
53
+ "@moxxy/tsconfig": "0.0.0",
54
+ "@moxxy/vitest-preset": "0.0.0"
55
55
  },
56
56
  "scripts": {
57
57
  "build": "tsc -p tsconfig.json",
package/src/index.test.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  asSessionId,
8
8
  asSkillId,
9
9
  asTurnId,
10
+ assertDefined,
10
11
  type AppContext,
11
12
  type EventLogReader,
12
13
  type MoxxyEvent,
@@ -69,7 +70,8 @@ function reader(events: ReadonlyArray<MoxxyEvent>): EventLogReader {
69
70
  // Faithfully model the real EventLog: `length` is a COUNT of held events,
70
71
  // while `slice(from)`/`at(seq)` are SEQ-addressed (translated through the
71
72
  // log's base). On a rebased mirror the held events' seqs start above 0.
72
- const base = events.length > 0 ? events[0]!.seq : 0;
73
+ const first = events[0];
74
+ const base = first !== undefined ? first.seq : 0;
73
75
  return {
74
76
  length: events.length,
75
77
  at: (seq: number) => events[seq - base],
@@ -97,7 +99,8 @@ class FakeLog {
97
99
  private readonly clearListeners = new Set<() => void>();
98
100
  constructor(seed: ReadonlyArray<MoxxyEvent> = []) {
99
101
  this.events = [...seed];
100
- this.base = seed.length > 0 ? seed[0]!.seq : 0;
102
+ const first = seed[0];
103
+ this.base = first !== undefined ? first.seq : 0;
101
104
  }
102
105
  get length() {
103
106
  return this.events.length;
@@ -165,12 +168,20 @@ describe('usage-stats plugin', () => {
165
168
  const plugin = buildUsageStatsPlugin({ statsPath });
166
169
  const events = [resp(0, 'opus', 100), resp(1, 'opus', 50)];
167
170
 
168
- await plugin.hooks!.onInit!(ctxFor([])); // boots with empty log
169
- await plugin.hooks!.onShutdown!(ctxFor(events));
171
+ const hooks = plugin.hooks;
172
+ assertDefined(hooks, 'usage-stats plugin hooks');
173
+ const onInit = hooks.onInit;
174
+ assertDefined(onInit, 'onInit hook');
175
+ const onShutdown = hooks.onShutdown;
176
+ assertDefined(onShutdown, 'onShutdown hook');
177
+ await onInit(ctxFor([])); // boots with empty log
178
+ await onShutdown(ctxFor(events));
170
179
 
171
180
  const file = await loadUsageStats(statsPath);
172
- expect(file.models['anthropic/opus']!.calls).toBe(2);
173
- expect(file.models['anthropic/opus']!.inputTokens).toBe(150);
181
+ const opus = file.models['anthropic/opus'];
182
+ assertDefined(opus, 'anthropic/opus model stats');
183
+ expect(opus.calls).toBe(2);
184
+ expect(opus.inputTokens).toBe(150);
174
185
  });
175
186
 
176
187
  it('skips restored events on resume and folds only the live suffix', async () => {
@@ -178,21 +189,35 @@ describe('usage-stats plugin', () => {
178
189
  const liveSuffix = [resp(2, 'opus', 30)];
179
190
  const plugin = buildUsageStatsPlugin({ statsPath });
180
191
 
192
+ const hooks = plugin.hooks;
193
+ assertDefined(hooks, 'usage-stats plugin hooks');
194
+ const onInit = hooks.onInit;
195
+ assertDefined(onInit, 'onInit hook');
196
+ const onShutdown = hooks.onShutdown;
197
+ assertDefined(onShutdown, 'onShutdown hook');
181
198
  // onInit fires after restored events are already seeded into the log.
182
- await plugin.hooks!.onInit!(ctxFor(restored));
199
+ await onInit(ctxFor(restored));
183
200
  // onShutdown sees restored + live.
184
- await plugin.hooks!.onShutdown!(ctxFor([...restored, ...liveSuffix]));
201
+ await onShutdown(ctxFor([...restored, ...liveSuffix]));
185
202
 
186
203
  const file = await loadUsageStats(statsPath);
187
204
  // Only the single live call (30 tokens) is counted — not the 3000 restored.
188
- expect(file.models['anthropic/opus']!.calls).toBe(1);
189
- expect(file.models['anthropic/opus']!.inputTokens).toBe(30);
205
+ const opus = file.models['anthropic/opus'];
206
+ assertDefined(opus, 'anthropic/opus model stats');
207
+ expect(opus.calls).toBe(1);
208
+ expect(opus.inputTokens).toBe(30);
190
209
  });
191
210
 
192
211
  it('writes nothing when no live events were produced', async () => {
193
212
  const plugin = buildUsageStatsPlugin({ statsPath });
194
- await plugin.hooks!.onInit!(ctxFor([resp(0, 'opus', 100)]));
195
- await plugin.hooks!.onShutdown!(ctxFor([resp(0, 'opus', 100)]));
213
+ const hooks = plugin.hooks;
214
+ assertDefined(hooks, 'usage-stats plugin hooks');
215
+ const onInit = hooks.onInit;
216
+ assertDefined(onInit, 'onInit hook');
217
+ const onShutdown = hooks.onShutdown;
218
+ assertDefined(onShutdown, 'onShutdown hook');
219
+ await onInit(ctxFor([resp(0, 'opus', 100)]));
220
+ await onShutdown(ctxFor([resp(0, 'opus', 100)]));
196
221
  // No new events past the cursor → file never created.
197
222
  expect((await loadUsageStats(statsPath)).models).toEqual({});
198
223
  });
@@ -206,13 +231,21 @@ describe('usage-stats plugin', () => {
206
231
  const liveSuffix = [resp(103, 'opus', 30)];
207
232
  const plugin = buildUsageStatsPlugin({ statsPath });
208
233
 
209
- await plugin.hooks!.onInit!(ctxFor(restored));
210
- await plugin.hooks!.onShutdown!(ctxFor([...restored, ...liveSuffix]));
234
+ const hooks = plugin.hooks;
235
+ assertDefined(hooks, 'usage-stats plugin hooks');
236
+ const onInit = hooks.onInit;
237
+ assertDefined(onInit, 'onInit hook');
238
+ const onShutdown = hooks.onShutdown;
239
+ assertDefined(onShutdown, 'onShutdown hook');
240
+ await onInit(ctxFor(restored));
241
+ await onShutdown(ctxFor([...restored, ...liveSuffix]));
211
242
 
212
243
  const file = await loadUsageStats(statsPath);
213
244
  // Only the single live call (30 tokens) — never the 7000 restored tokens.
214
- expect(file.models['anthropic/opus']!.calls).toBe(1);
215
- expect(file.models['anthropic/opus']!.inputTokens).toBe(30);
245
+ const opus = file.models['anthropic/opus'];
246
+ assertDefined(opus, 'anthropic/opus model stats');
247
+ expect(opus.calls).toBe(1);
248
+ expect(opus.inputTokens).toBe(30);
216
249
  });
217
250
 
218
251
  it('counts the post-/new (log clear/reset) suffix that restarts at seq 0', async () => {
@@ -225,16 +258,24 @@ describe('usage-stats plugin', () => {
225
258
  const plugin = buildUsageStatsPlugin({ statsPath });
226
259
  const log = new FakeLog([resp(100, 'opus', 1000), resp(101, 'opus', 2000)]);
227
260
 
228
- await plugin.hooks!.onInit!(ctxForLog(log)); // boundary captured at seq 101
261
+ const hooks = plugin.hooks;
262
+ assertDefined(hooks, 'usage-stats plugin hooks');
263
+ const onInit = hooks.onInit;
264
+ assertDefined(onInit, 'onInit hook');
265
+ const onShutdown = hooks.onShutdown;
266
+ assertDefined(onShutdown, 'onShutdown hook');
267
+ await onInit(ctxForLog(log)); // boundary captured at seq 101
229
268
  log.clear(); // /new — events emptied, base reset to 0, onClear fires
230
269
  log.push(resp(0, 'opus', 40), resp(1, 'opus', 60)); // post-/new live events
231
270
 
232
- await plugin.hooks!.onShutdown!(ctxForLog(log));
271
+ await onShutdown(ctxForLog(log));
233
272
 
234
273
  const file = await loadUsageStats(statsPath);
235
274
  // Both post-/new calls counted (100 tokens) — not dropped as <= old boundary.
236
- expect(file.models['anthropic/opus']!.calls).toBe(2);
237
- expect(file.models['anthropic/opus']!.inputTokens).toBe(100);
275
+ const opus = file.models['anthropic/opus'];
276
+ assertDefined(opus, 'anthropic/opus model stats');
277
+ expect(opus.calls).toBe(2);
278
+ expect(opus.inputTokens).toBe(100);
238
279
  });
239
280
 
240
281
  it('isolates the cursor per session so concurrent sessions on one instance never clobber each other', async () => {
@@ -247,19 +288,27 @@ describe('usage-stats plugin', () => {
247
288
  const logA = new FakeLog([resp(0, 'opus', 1000)]); // A restored 1 event (seq 0)
248
289
  const logB = new FakeLog([]); // B is a fresh run
249
290
 
250
- await plugin.hooks!.onInit!(ctxForLog(logA, sidA)); // A boundary = 0
251
- await plugin.hooks!.onInit!(ctxForLog(logB, sidB)); // B boundary = null (would clobber A's scalar)
291
+ const hooks = plugin.hooks;
292
+ assertDefined(hooks, 'usage-stats plugin hooks');
293
+ const onInit = hooks.onInit;
294
+ assertDefined(onInit, 'onInit hook');
295
+ const onShutdown = hooks.onShutdown;
296
+ assertDefined(onShutdown, 'onShutdown hook');
297
+ await onInit(ctxForLog(logA, sidA)); // A boundary = 0
298
+ await onInit(ctxForLog(logB, sidB)); // B boundary = null (would clobber A's scalar)
252
299
 
253
300
  logA.push(resp(1, 'opus', 5)); // A's single live call
254
301
  logB.push(resp(0, 'opus', 7)); // B's single live call
255
302
 
256
- await plugin.hooks!.onShutdown!(ctxForLog(logA, sidA));
257
- await plugin.hooks!.onShutdown!(ctxForLog(logB, sidB));
303
+ await onShutdown(ctxForLog(logA, sidA));
304
+ await onShutdown(ctxForLog(logB, sidB));
258
305
 
259
306
  const file = await loadUsageStats(statsPath);
260
307
  // A counts only its live call (5), B counts its only call (7) → 2 calls, 12.
261
- expect(file.models['anthropic/opus']!.calls).toBe(2);
262
- expect(file.models['anthropic/opus']!.inputTokens).toBe(12);
308
+ const opus = file.models['anthropic/opus'];
309
+ assertDefined(opus, 'anthropic/opus model stats');
310
+ expect(opus.calls).toBe(2);
311
+ expect(opus.inputTokens).toBe(12);
263
312
  });
264
313
 
265
314
  it('folds the whole log when onShutdown fires without a preceding onInit (cursor defaults to null)', async () => {
@@ -271,11 +320,17 @@ describe('usage-stats plugin', () => {
271
320
  const plugin = buildUsageStatsPlugin({ statsPath });
272
321
  const events = [resp(0, 'opus', 100), resp(1, 'opus', 50)];
273
322
 
274
- await plugin.hooks!.onShutdown!(ctxFor(events)); // no onInit called
323
+ const hooks = plugin.hooks;
324
+ assertDefined(hooks, 'usage-stats plugin hooks');
325
+ const onShutdown = hooks.onShutdown;
326
+ assertDefined(onShutdown, 'onShutdown hook');
327
+ await onShutdown(ctxFor(events)); // no onInit called
275
328
 
276
329
  const file = await loadUsageStats(statsPath);
277
- expect(file.models['anthropic/opus']!.calls).toBe(2);
278
- expect(file.models['anthropic/opus']!.inputTokens).toBe(150);
330
+ const opus = file.models['anthropic/opus'];
331
+ assertDefined(opus, 'anthropic/opus model stats');
332
+ expect(opus.calls).toBe(2);
333
+ expect(opus.inputTokens).toBe(150);
279
334
  });
280
335
 
281
336
  it('onShutdown degrades to a no-op (resolves, writes nothing) when the reader throws', async () => {
@@ -296,7 +351,11 @@ describe('usage-stats plugin', () => {
296
351
  } as unknown as EventLogReader;
297
352
  const ctx = { sessionId: sid, cwd: '/tmp', log: hostile, env: {} } as AppContext;
298
353
 
299
- await expect(plugin.hooks!.onShutdown!(ctx)).resolves.toBeUndefined();
354
+ const hooks = plugin.hooks;
355
+ assertDefined(hooks, 'usage-stats plugin hooks');
356
+ const onShutdown = hooks.onShutdown;
357
+ assertDefined(onShutdown, 'onShutdown hook');
358
+ await expect(onShutdown(ctx)).resolves.toBeUndefined();
300
359
  // Nothing folded → file never created.
301
360
  expect((await loadUsageStats(statsPath)).models).toEqual({});
302
361
  });
@@ -322,16 +381,24 @@ describe('usage-stats plugin', () => {
322
381
  } as unknown as EventLogReader;
323
382
  const initCtx = { sessionId: sid, cwd: '/tmp', log: throwingInit, env: {} } as AppContext;
324
383
 
384
+ const hooks = plugin.hooks;
385
+ assertDefined(hooks, 'usage-stats plugin hooks');
386
+ const onInit = hooks.onInit;
387
+ assertDefined(onInit, 'onInit hook');
388
+ const onShutdown = hooks.onShutdown;
389
+ assertDefined(onShutdown, 'onShutdown hook');
325
390
  // onInit is synchronous; it must not throw despite ofType + onClear both
326
391
  // throwing. (`Promise.resolve` tolerates the sync void return.)
327
- expect(() => plugin.hooks!.onInit!(initCtx)).not.toThrow();
392
+ expect(() => onInit(initCtx)).not.toThrow();
328
393
 
329
394
  // Now shut down with a healthy reader: null cursor folds the whole log.
330
- await plugin.hooks!.onShutdown!(ctxFor([resp(0, 'opus', 10), resp(1, 'opus', 20)]));
395
+ await onShutdown(ctxFor([resp(0, 'opus', 10), resp(1, 'opus', 20)]));
331
396
 
332
397
  const file = await loadUsageStats(statsPath);
333
- expect(file.models['anthropic/opus']!.calls).toBe(2);
334
- expect(file.models['anthropic/opus']!.inputTokens).toBe(30);
398
+ const opus = file.models['anthropic/opus'];
399
+ assertDefined(opus, 'anthropic/opus model stats');
400
+ expect(opus.calls).toBe(2);
401
+ expect(opus.inputTokens).toBe(30);
335
402
  });
336
403
 
337
404
  it('onShutdown tears down the onClear listener even when the fold/merge path throws (no leak)', async () => {
@@ -357,9 +424,15 @@ describe('usage-stats plugin', () => {
357
424
  } as unknown as EventLogReader;
358
425
  const ctx = { sessionId: sid, cwd: '/tmp', log, env: {} } as AppContext;
359
426
 
360
- await plugin.hooks!.onInit!(ctx); // registers the clear listener (ofType ok: empty log path)
427
+ const hooks = plugin.hooks;
428
+ assertDefined(hooks, 'usage-stats plugin hooks');
429
+ const onInit = hooks.onInit;
430
+ assertDefined(onInit, 'onInit hook');
431
+ const onShutdown = hooks.onShutdown;
432
+ assertDefined(onShutdown, 'onShutdown hook');
433
+ await onInit(ctx); // registers the clear listener (ofType ok: empty log path)
361
434
  throwFromOfType = true; // now the fold path will throw on shutdown
362
- await expect(plugin.hooks!.onShutdown!(ctx)).resolves.toBeUndefined();
435
+ await expect(onShutdown(ctx)).resolves.toBeUndefined();
363
436
  // Listener was unsubscribed despite the fold throwing → no leak.
364
437
  expect(unsubscribed).toBe(true);
365
438
  });
@@ -371,6 +444,10 @@ describe('usage-stats plugin', () => {
371
444
  // non-array — it must be swallowed by the same best-effort guard, resolving
372
445
  // the hook and writing nothing rather than crashing the timeboxed shutdown.
373
446
  const plugin = buildUsageStatsPlugin({ statsPath });
447
+ const hooks = plugin.hooks;
448
+ assertDefined(hooks, 'usage-stats plugin hooks');
449
+ const onShutdown = hooks.onShutdown;
450
+ assertDefined(onShutdown, 'onShutdown hook');
374
451
  for (const bogus of [42, null, 'nope', { length: 1 }] as unknown[]) {
375
452
  const garbageReader = {
376
453
  length: 1,
@@ -381,7 +458,7 @@ describe('usage-stats plugin', () => {
381
458
  toJSON: () => [],
382
459
  } as unknown as EventLogReader;
383
460
  const ctx = { sessionId: sid, cwd: '/tmp', log: garbageReader, env: {} } as AppContext;
384
- await expect(plugin.hooks!.onShutdown!(ctx)).resolves.toBeUndefined();
461
+ await expect(onShutdown(ctx)).resolves.toBeUndefined();
385
462
  }
386
463
  // No call ever produced a fold-able array → file never created.
387
464
  expect((await loadUsageStats(statsPath)).models).toEqual({});
@@ -397,15 +474,21 @@ describe('usage-stats plugin', () => {
397
474
  const plugin = buildUsageStatsPlugin({ statsPath });
398
475
  const events = [resp(0, 'opus', 100)];
399
476
 
400
- await expect(plugin.hooks!.onShutdown!(ctxFor(events))).resolves.toBeUndefined();
477
+ const hooks = plugin.hooks;
478
+ assertDefined(hooks, 'usage-stats plugin hooks');
479
+ const onShutdown = hooks.onShutdown;
480
+ assertDefined(onShutdown, 'onShutdown hook');
481
+ await expect(onShutdown(ctxFor(events))).resolves.toBeUndefined();
401
482
  // A repeat shutdown re-folds the same whole log (cursor still absent → null),
402
483
  // so it must not throw; the aggregate doubles, which is the documented
403
484
  // whole-log-fold default, asserted so a future guard change is deliberate.
404
- await expect(plugin.hooks!.onShutdown!(ctxFor(events))).resolves.toBeUndefined();
485
+ await expect(onShutdown(ctxFor(events))).resolves.toBeUndefined();
405
486
 
406
487
  const file = await loadUsageStats(statsPath);
407
- expect(file.models['anthropic/opus']!.calls).toBe(2);
408
- expect(file.models['anthropic/opus']!.inputTokens).toBe(200);
488
+ const opus = file.models['anthropic/opus'];
489
+ assertDefined(opus, 'anthropic/opus model stats');
490
+ expect(opus.calls).toBe(2);
491
+ expect(opus.inputTokens).toBe(200);
409
492
  });
410
493
 
411
494
  it('a re-init of the same session id replaces (does not stack) the onClear listener', async () => {
@@ -419,12 +502,18 @@ describe('usage-stats plugin', () => {
419
502
  const plugin = buildUsageStatsPlugin({ statsPath });
420
503
  const log = new FakeLog([resp(0, 'opus', 10)]);
421
504
 
422
- await plugin.hooks!.onInit!(ctxForLog(log));
423
- await plugin.hooks!.onInit!(ctxForLog(log));
424
- await plugin.hooks!.onInit!(ctxForLog(log));
505
+ const hooks = plugin.hooks;
506
+ assertDefined(hooks, 'usage-stats plugin hooks');
507
+ const onInit = hooks.onInit;
508
+ assertDefined(onInit, 'onInit hook');
509
+ const onShutdown = hooks.onShutdown;
510
+ assertDefined(onShutdown, 'onShutdown hook');
511
+ await onInit(ctxForLog(log));
512
+ await onInit(ctxForLog(log));
513
+ await onInit(ctxForLog(log));
425
514
  expect(log.clearListenerCount).toBe(1); // replaced each time, never stacked
426
515
 
427
- await plugin.hooks!.onShutdown!(ctxForLog(log));
516
+ await onShutdown(ctxForLog(log));
428
517
  expect(log.clearListenerCount).toBe(0); // fully torn down
429
518
  });
430
519
 
@@ -452,14 +541,22 @@ describe('usage-stats plugin', () => {
452
541
  } as unknown as EventLogReader;
453
542
  const initCtx = { sessionId: sid, cwd: '/tmp', log: onClearThrows, env: {} } as AppContext;
454
543
 
455
- expect(() => plugin.hooks!.onInit!(initCtx)).not.toThrow();
544
+ const hooks = plugin.hooks;
545
+ assertDefined(hooks, 'usage-stats plugin hooks');
546
+ const onInit = hooks.onInit;
547
+ assertDefined(onInit, 'onInit hook');
548
+ const onShutdown = hooks.onShutdown;
549
+ assertDefined(onShutdown, 'onShutdown hook');
550
+ expect(() => onInit(initCtx)).not.toThrow();
456
551
  // Shut down with restored prefix + one live call. The preserved boundary (102)
457
552
  // must exclude every restored response and include only the live one.
458
- await plugin.hooks!.onShutdown!(ctxFor([...restored, resp(103, 'opus', 30)]));
553
+ await onShutdown(ctxFor([...restored, resp(103, 'opus', 30)]));
459
554
 
460
555
  const file = await loadUsageStats(statsPath);
461
- expect(file.models['anthropic/opus']!.calls).toBe(1);
462
- expect(file.models['anthropic/opus']!.inputTokens).toBe(30); // not 7030
556
+ const opus = file.models['anthropic/opus'];
557
+ assertDefined(opus, 'anthropic/opus model stats');
558
+ expect(opus.calls).toBe(1);
559
+ expect(opus.inputTokens).toBe(30); // not 7030
463
560
  });
464
561
 
465
562
  it('actually tears down the real onClear listener on shutdown (no leaked subscription)', async () => {
@@ -471,23 +568,31 @@ describe('usage-stats plugin', () => {
471
568
  const plugin = buildUsageStatsPlugin({ statsPath });
472
569
  const log = new FakeLog([resp(0, 'opus', 100)]);
473
570
 
474
- await plugin.hooks!.onInit!(ctxForLog(log));
571
+ const hooks = plugin.hooks;
572
+ assertDefined(hooks, 'usage-stats plugin hooks');
573
+ const onInit = hooks.onInit;
574
+ assertDefined(onInit, 'onInit hook');
575
+ const onShutdown = hooks.onShutdown;
576
+ assertDefined(onShutdown, 'onShutdown hook');
577
+ await onInit(ctxForLog(log));
475
578
  expect(log.clearListenerCount).toBe(1); // listener registered
476
579
 
477
580
  log.push(resp(1, 'opus', 50));
478
- await plugin.hooks!.onShutdown!(ctxForLog(log));
581
+ await onShutdown(ctxForLog(log));
479
582
  expect(log.clearListenerCount).toBe(0); // torn down — no leak
480
583
 
481
584
  // A clear() now fires no plugin listener; a second shutdown sees an absent
482
585
  // cursor (null) and folds the (post-clear, empty) log → still no crash.
483
586
  log.clear();
484
- await expect(plugin.hooks!.onShutdown!(ctxForLog(log))).resolves.toBeUndefined();
587
+ await expect(onShutdown(ctxForLog(log))).resolves.toBeUndefined();
485
588
 
486
589
  const file = await loadUsageStats(statsPath);
487
590
  // Only the first run's live call (50) — the restored 100 was excluded, and
488
591
  // the post-shutdown clear added nothing.
489
- expect(file.models['anthropic/opus']!.calls).toBe(1);
490
- expect(file.models['anthropic/opus']!.inputTokens).toBe(50);
592
+ const opus = file.models['anthropic/opus'];
593
+ assertDefined(opus, 'anthropic/opus model stats');
594
+ expect(opus.calls).toBe(1);
595
+ expect(opus.inputTokens).toBe(50);
491
596
  });
492
597
  });
493
598
 
@@ -497,15 +602,21 @@ describe('summarizeSkillEvents', () => {
497
602
  [skillInvoked(0, 'deploy', 1000), skillInvoked(1, 'deploy', 3000), skillInvoked(2, 'lint', 2000)],
498
603
  [],
499
604
  );
500
- expect(delta['deploy']!.invocations).toBe(2);
501
- expect(delta['deploy']!.lastInvokedAt).toBe(new Date(3000).toISOString());
502
- expect(delta['lint']!.invocations).toBe(1);
605
+ const deploy = delta['deploy'];
606
+ assertDefined(deploy, 'deploy skill delta');
607
+ expect(deploy.invocations).toBe(2);
608
+ expect(deploy.lastInvokedAt).toBe(new Date(3000).toISOString());
609
+ const lint = delta['lint'];
610
+ assertDefined(lint, 'lint skill delta');
611
+ expect(lint.invocations).toBe(1);
503
612
  });
504
613
 
505
614
  it('records createdAt for a created-but-never-invoked skill (invocations 0)', () => {
506
615
  const delta = summarizeSkillEvents([], [skillCreated(0, 'fresh', 5000)]);
507
- expect(delta['fresh']!.invocations).toBe(0);
508
- expect(delta['fresh']!.createdAt).toBe(new Date(5000).toISOString());
616
+ const fresh = delta['fresh'];
617
+ assertDefined(fresh, 'fresh skill delta');
618
+ expect(fresh.invocations).toBe(0);
619
+ expect(fresh.createdAt).toBe(new Date(5000).toISOString());
509
620
  });
510
621
 
511
622
  it('is empty for no events', () => {
@@ -523,14 +634,24 @@ describe('usage-stats plugin — skill folding', () => {
523
634
  skillInvoked(3, 'lint', 3000),
524
635
  ];
525
636
 
526
- await plugin.hooks!.onInit!(ctxFor([]));
527
- await plugin.hooks!.onShutdown!(ctxFor(events));
637
+ const hooks = plugin.hooks;
638
+ assertDefined(hooks, 'usage-stats plugin hooks');
639
+ const onInit = hooks.onInit;
640
+ assertDefined(onInit, 'onInit hook');
641
+ const onShutdown = hooks.onShutdown;
642
+ assertDefined(onShutdown, 'onShutdown hook');
643
+ await onInit(ctxFor([]));
644
+ await onShutdown(ctxFor(events));
528
645
 
529
646
  const file = await loadSkillUsage(skillUsagePath);
530
- expect(file.skills['deploy']!.invocations).toBe(2);
531
- expect(file.skills['deploy']!.createdAt).toBe(new Date(1000).toISOString());
532
- expect(file.skills['deploy']!.lastInvokedAt).toBe(new Date(4000).toISOString());
533
- expect(file.skills['lint']!.invocations).toBe(1);
647
+ const deploy = file.skills['deploy'];
648
+ assertDefined(deploy, 'deploy skill usage');
649
+ expect(deploy.invocations).toBe(2);
650
+ expect(deploy.createdAt).toBe(new Date(1000).toISOString());
651
+ expect(deploy.lastInvokedAt).toBe(new Date(4000).toISOString());
652
+ const lint = file.skills['lint'];
653
+ assertDefined(lint, 'lint skill usage');
654
+ expect(lint.invocations).toBe(1);
534
655
  });
535
656
 
536
657
  it('counts only the live skill suffix on resume (skips restored skill events)', async () => {
@@ -538,11 +659,19 @@ describe('usage-stats plugin — skill folding', () => {
538
659
  const live = [skillInvoked(2, 'deploy', 3000)];
539
660
  const plugin = buildUsageStatsPlugin({ statsPath, skillUsagePath });
540
661
 
541
- await plugin.hooks!.onInit!(ctxFor(restored));
542
- await plugin.hooks!.onShutdown!(ctxFor([...restored, ...live]));
662
+ const hooks = plugin.hooks;
663
+ assertDefined(hooks, 'usage-stats plugin hooks');
664
+ const onInit = hooks.onInit;
665
+ assertDefined(onInit, 'onInit hook');
666
+ const onShutdown = hooks.onShutdown;
667
+ assertDefined(onShutdown, 'onShutdown hook');
668
+ await onInit(ctxFor(restored));
669
+ await onShutdown(ctxFor([...restored, ...live]));
543
670
 
544
671
  // Only the single live invocation is counted — not the 2 restored.
545
- expect((await loadSkillUsage(skillUsagePath)).skills['deploy']!.invocations).toBe(1);
672
+ const deploy = (await loadSkillUsage(skillUsagePath)).skills['deploy'];
673
+ assertDefined(deploy, 'deploy skill usage');
674
+ expect(deploy.invocations).toBe(1);
546
675
  });
547
676
 
548
677
  it('folds skill usage even when the run produced no provider_response (no token early-return)', async () => {
@@ -551,18 +680,32 @@ describe('usage-stats plugin — skill folding', () => {
551
680
  const plugin = buildUsageStatsPlugin({ statsPath, skillUsagePath });
552
681
  const events = [skillInvoked(0, 'deploy', 1000)];
553
682
 
554
- await plugin.hooks!.onInit!(ctxFor([]));
555
- await plugin.hooks!.onShutdown!(ctxFor(events));
556
-
557
- expect((await loadSkillUsage(skillUsagePath)).skills['deploy']!.invocations).toBe(1);
683
+ const hooks = plugin.hooks;
684
+ assertDefined(hooks, 'usage-stats plugin hooks');
685
+ const onInit = hooks.onInit;
686
+ assertDefined(onInit, 'onInit hook');
687
+ const onShutdown = hooks.onShutdown;
688
+ assertDefined(onShutdown, 'onShutdown hook');
689
+ await onInit(ctxFor([]));
690
+ await onShutdown(ctxFor(events));
691
+
692
+ const deploy = (await loadSkillUsage(skillUsagePath)).skills['deploy'];
693
+ assertDefined(deploy, 'deploy skill usage');
694
+ expect(deploy.invocations).toBe(1);
558
695
  // No token usage was recorded (no provider_response events).
559
696
  expect((await loadUsageStats(statsPath)).models).toEqual({});
560
697
  });
561
698
 
562
699
  it('writes no skill usage file when the run exercised no skills', async () => {
563
700
  const plugin = buildUsageStatsPlugin({ statsPath, skillUsagePath });
564
- await plugin.hooks!.onInit!(ctxFor([]));
565
- await plugin.hooks!.onShutdown!(ctxFor([resp(0, 'opus', 100)]));
701
+ const hooks = plugin.hooks;
702
+ assertDefined(hooks, 'usage-stats plugin hooks');
703
+ const onInit = hooks.onInit;
704
+ assertDefined(onInit, 'onInit hook');
705
+ const onShutdown = hooks.onShutdown;
706
+ assertDefined(onShutdown, 'onShutdown hook');
707
+ await onInit(ctxFor([]));
708
+ await onShutdown(ctxFor([resp(0, 'opus', 100)]));
566
709
  // Empty skill delta → merge never touches disk.
567
710
  await expect(fs.access(skillUsagePath)).rejects.toThrow();
568
711
  });
package/src/index.ts CHANGED
@@ -138,7 +138,7 @@ export function buildUsageStatsPlugin(opts: BuildUsageStatsPluginOptions = {}):
138
138
  // An unsubscribe that throws must not block the merge below.
139
139
  }
140
140
  clearUnsubs.delete(ctx.sessionId);
141
- const initMaxSeq = cursors.has(ctx.sessionId) ? cursors.get(ctx.sessionId)! : null;
141
+ const initMaxSeq = cursors.get(ctx.sessionId) ?? null;
142
142
  cursors.delete(ctx.sessionId);
143
143
  // Best-effort: a throwing reader or a failed fold degrades to "record
144
144
  // nothing for this run" rather than rejecting the hook (which the host