@monotykamary/pi-tps 1.3.2 → 1.3.4

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.
@@ -13,17 +13,18 @@ jobs:
13
13
  steps:
14
14
  - uses: actions/checkout@v4
15
15
 
16
+ - uses: pnpm/action-setup@v4
16
17
  - name: Setup Node.js
17
18
  uses: actions/setup-node@v4
18
19
  with:
19
20
  node-version: '22'
20
- cache: 'npm'
21
+ cache: 'pnpm'
21
22
 
22
23
  - name: Install dependencies
23
- run: npm ci
24
+ run: pnpm install --frozen-lockfile
24
25
 
25
26
  - name: Type check
26
- run: npm run typecheck
27
+ run: pnpm typecheck
27
28
 
28
29
  test:
29
30
  runs-on: ubuntu-latest
@@ -32,20 +33,21 @@ jobs:
32
33
  steps:
33
34
  - uses: actions/checkout@v4
34
35
 
36
+ - uses: pnpm/action-setup@v4
35
37
  - name: Setup Node.js
36
38
  uses: actions/setup-node@v4
37
39
  with:
38
40
  node-version: '22'
39
- cache: 'npm'
41
+ cache: 'pnpm'
40
42
 
41
43
  - name: Install dependencies
42
- run: npm ci
44
+ run: pnpm install --frozen-lockfile
43
45
 
44
46
  - name: Run tests
45
- run: npm test
47
+ run: pnpm test
46
48
 
47
49
  - name: Run tests with coverage
48
- run: npm run test:coverage
50
+ run: pnpm test:coverage
49
51
 
50
52
  - name: Upload coverage
51
53
  uses: actions/upload-artifact@v4
@@ -0,0 +1,4 @@
1
+ {
2
+ "format": 1,
3
+ "actors": []
4
+ }
@@ -52,7 +52,8 @@ function makeMessageWithCost(opts: {
52
52
  async function runBurstTurn(
53
53
  fixture: ReturnType<typeof createTestFixture>,
54
54
  message: AssistantMessage,
55
- turnIndex = 0
55
+ turnIndex = 0,
56
+ beforeTurnEnd?: () => void
56
57
  ) {
57
58
  const { handlers, mockCtx } = fixture;
58
59
  handlers['turn_start']?.({ type: 'turn_start', turnIndex, timestamp: Date.now() });
@@ -65,6 +66,7 @@ async function runBurstTurn(
65
66
  assistantMessageEvent: { type: 'text_delta', delta: 't' },
66
67
  });
67
68
  handlers['message_end']?.({ type: 'message_end', message });
69
+ beforeTurnEnd?.();
68
70
  handlers['turn_end']?.({ type: 'turn_end', turnIndex, message, toolResults: [] }, mockCtx);
69
71
  }
70
72
 
@@ -108,15 +110,15 @@ describe('pi-tps extension — blended $/M-tokens rate', () => {
108
110
  });
109
111
 
110
112
  // Simulate the neuralwatt provider's turn_end running BEFORE ours: it emits
111
- // the per-turn energy event, which our listener stashes keyed by turnIndex.
112
- fixture.emitEvent('neuralwatt:turn-energy', {
113
- costUsd: 0.006,
114
- energyJoules: 21.6,
115
- turnIndex: 0,
113
+ // the per-turn energy event after streaming but before pi-tps handles turn_end.
114
+ await runBurstTurn(fixture, message, 0, () => {
115
+ fixture.emitEvent('neuralwatt:turn-energy', {
116
+ costUsd: 0.006,
117
+ energyJoules: 21.6,
118
+ turnIndex: 0,
119
+ });
116
120
  });
117
121
 
118
- await runBurstTurn(fixture, message, 0);
119
-
120
122
  const { notifySpy, appendEntrySpy } = fixture;
121
123
  expect(notifySpy).toHaveBeenCalledOnce();
122
124
  const banner = notifySpy.mock.calls[0][0] as string;
@@ -157,6 +159,102 @@ describe('pi-tps extension — blended $/M-tokens rate', () => {
157
159
  expect(data.cost).toBeNull();
158
160
  });
159
161
 
162
+ it('omits the rate when pi reports an all-zero cost block for an unpriced model', async () => {
163
+ const message = makeMessageWithCost({
164
+ input: 500,
165
+ output: 500,
166
+ costTotal: 0,
167
+ provider: 'makora',
168
+ model: 'zai-org/GLM-5.2-NVFP4',
169
+ });
170
+
171
+ await runBurstTurn(fixture, message);
172
+
173
+ const { appendEntrySpy, notifySpy } = fixture;
174
+ const [, data] = appendEntrySpy.mock.calls[0];
175
+ expect(data.cost).toBeNull();
176
+ expect(data.rateUsdPerMTokens).toBeNull();
177
+ expect(notifySpy.mock.calls[0][0]).not.toMatch(/\$.*\/M/);
178
+ });
179
+
180
+ it('does not reuse a late billed cost after /tree when a turn index repeats', async () => {
181
+ const neuralwattMessage = makeMessageWithCost({
182
+ input: 1178,
183
+ output: 1235,
184
+ costTotal: 0.0102352,
185
+ provider: 'neuralwatt',
186
+ model: 'glm-5.2-short',
187
+ });
188
+ neuralwattMessage.usage.cacheRead = 8192;
189
+ neuralwattMessage.usage.totalTokens = 10605;
190
+
191
+ await runBurstTurn(fixture, neuralwattMessage, 2);
192
+ fixture.emitEvent('neuralwatt:turn-energy', {
193
+ costUsd: 0.00116,
194
+ energyJoules: 835.2,
195
+ turnIndex: 2,
196
+ });
197
+
198
+ fixture.handlers['session_tree']?.(
199
+ { type: 'session_tree', newLeafId: null, oldLeafId: 'old-leaf' },
200
+ fixture.mockCtx
201
+ );
202
+
203
+ const unpricedMessage = makeMessageWithCost({
204
+ input: 8460,
205
+ output: 717,
206
+ costTotal: 0,
207
+ provider: 'makora',
208
+ model: 'zai-org/GLM-5.2-NVFP4',
209
+ });
210
+ unpricedMessage.usage.cacheRead = 1216;
211
+ unpricedMessage.usage.totalTokens = 10393;
212
+
213
+ await runBurstTurn(fixture, unpricedMessage, 2);
214
+
215
+ const latestTelemetry = fixture.appendEntrySpy.mock.calls.at(-1)![1];
216
+ expect(latestTelemetry.model).toEqual({
217
+ provider: 'makora',
218
+ modelId: 'zai-org/GLM-5.2-NVFP4',
219
+ });
220
+ expect(latestTelemetry.cost).toBeNull();
221
+ expect(latestTelemetry.rateUsdPerMTokens).toBeNull();
222
+
223
+ const latestBanner = fixture.notifySpy.mock.calls.at(-1)![0] as string;
224
+ expect(latestBanner).not.toContain('$0.11/M');
225
+ expect(latestBanner).not.toMatch(/\$.*\/M/);
226
+ });
227
+
228
+ it('does not apply an early current-turn event to a previous run with the same index', async () => {
229
+ const previousMessage = makeMessageWithCost({
230
+ input: 1000,
231
+ output: 1000,
232
+ costTotal: 0.008,
233
+ });
234
+ await runBurstTurn(fixture, previousMessage, 0);
235
+
236
+ const currentMessage = makeMessageWithCost({
237
+ input: 1000,
238
+ output: 1000,
239
+ costTotal: 0.01,
240
+ provider: 'neuralwatt',
241
+ model: 'glm-5.2',
242
+ });
243
+
244
+ await runBurstTurn(fixture, currentMessage, 0, () => {
245
+ fixture.emitEvent('neuralwatt:turn-energy', {
246
+ costUsd: 0.006,
247
+ energyJoules: 21.6,
248
+ turnIndex: 0,
249
+ });
250
+ expect(fixture.appendEntrySpy).toHaveBeenCalledTimes(1);
251
+ });
252
+
253
+ expect(fixture.appendEntrySpy).toHaveBeenCalledTimes(2);
254
+ const latestTelemetry = fixture.appendEntrySpy.mock.calls.at(-1)![1];
255
+ expect(latestTelemetry.rateUsdPerMTokens).toBe(3);
256
+ });
257
+
160
258
  it('falls back to list-price rate when billed-cost event misses (out-of-order load)', async () => {
161
259
  // Neuralwatt turn but the energy event never arrives (provider loaded after
162
260
  // us). Must not block or crash — falls back to the list-price compute rate.
@@ -224,12 +322,13 @@ describe('pi-tps extension — blended $/M-tokens rate', () => {
224
322
  model: 'moonshotai/Kimi-K2.5',
225
323
  });
226
324
 
227
- fixture.emitEvent('neuralwatt:turn-energy', {
228
- costUsd: 0.006,
229
- energyJoules: 21.6,
230
- turnIndex: 0,
325
+ await runBurstTurn(fixture, message, 0, () => {
326
+ fixture.emitEvent('neuralwatt:turn-energy', {
327
+ costUsd: 0.006,
328
+ energyJoules: 21.6,
329
+ turnIndex: 0,
330
+ });
231
331
  });
232
- await runBurstTurn(fixture, message, 0);
233
332
 
234
333
  // A stray duplicate event must not append a second corrected entry.
235
334
  fixture.emitEvent('neuralwatt:turn-energy', {
@@ -108,7 +108,7 @@ export function createTestFixture(): TestFixture {
108
108
  ui: { notify: notifySpy } as any,
109
109
  sessionManager: {
110
110
  getEntries: vi.fn().mockReturnValue(mockEntries),
111
- getBranch: vi.fn(),
111
+ getBranch: vi.fn().mockReturnValue(mockEntries),
112
112
  getSessionId: vi.fn(),
113
113
  },
114
114
  modelRegistry: undefined as any,
@@ -175,6 +175,22 @@ describe('pi-tps extension — rehydration', () => {
175
175
  expect(notifySpy).toHaveBeenCalledOnce();
176
176
  });
177
177
 
178
+ it('should restore from the active branch instead of an abandoned branch', async () => {
179
+ const { handlers, notifySpy, mockEntries, mockCtx } = fixture;
180
+ const activeEntry = makeTpsEntry({ tps: 10.0 });
181
+ const abandonedEntry = makeTpsEntry({ tps: 99.0 });
182
+ mockEntries.push(activeEntry, abandonedEntry);
183
+ (mockCtx.sessionManager.getBranch as ReturnType<typeof vi.fn>).mockReturnValue([activeEntry]);
184
+
185
+ handlers['session_tree']?.({ newLeafId: 'active', oldLeafId: 'abandoned' }, mockCtx);
186
+ await tick();
187
+
188
+ expect(notifySpy).toHaveBeenCalledOnce();
189
+ const msg = notifySpy.mock.calls[0][0] as string;
190
+ expect(msg).toContain('TPS 10.0');
191
+ expect(msg).not.toContain('TPS 99.0');
192
+ });
193
+
178
194
  it('should rehydrate most recent entry, preferring structured over legacy', async () => {
179
195
  const { handlers, notifySpy, mockEntries } = fixture;
180
196
 
@@ -74,8 +74,6 @@ const STALL_THRESHOLD_MS = 500;
74
74
 
75
75
  /** Event name emitted by pi-neuralwatt-provider per turn with energy-billed cost data. */
76
76
  const NEURALWATT_ENERGY_EVENT = 'neuralwatt:turn-energy';
77
- /** Cap the per-turn billed-cost cache (turnIndex → costUsd) to avoid unbounded growth in long sessions. */
78
- const NEURALWATT_ENERGY_CACHE_MAX = 32;
79
77
 
80
78
  // ─── Data types ─────────────────────────────────────────────────────────────
81
79
 
@@ -115,6 +113,7 @@ interface TurnTelemetry {
115
113
 
116
114
  /** In-memory state accumulated during one LLM turn */
117
115
  interface TurnTiming {
116
+ turnIndex: number;
118
117
  turnStartMs: number;
119
118
  lastUpdateMs: number;
120
119
  firstTokenMs: number | null;
@@ -489,7 +488,11 @@ function buildTelemetry(
489
488
  // billed cost when present (energy-based, what the user actually pays),
490
489
  // otherwise the list-price compute cost from message.usage.cost. Never both
491
490
  // — billedCost wins outright when present, so there's no double-counting.
492
- const effectiveCost = billedCost ?? (hasCost ? costTotal : null);
491
+ // Pi represents models without pricing with an all-zero cost block. Treat
492
+ // that as unavailable rather than displaying a misleading $0.00/M rate. An
493
+ // explicit billed cost remains valid even when it is zero.
494
+ const listPriceCost = hasCost && Number.isFinite(costTotal) && costTotal > 0 ? costTotal : null;
495
+ const effectiveCost = billedCost ?? listPriceCost;
493
496
  const rateUsdPerMTokens = computeRateUsdPerM(effectiveCost, totalTokens);
494
497
 
495
498
  return {
@@ -506,15 +509,16 @@ function buildTelemetry(
506
509
  },
507
510
  tps,
508
511
  isPrimaryBranch,
509
- cost: hasCost
510
- ? {
511
- input: costInput,
512
- output: costOutput,
513
- cacheRead: costCacheRead,
514
- cacheWrite: costCacheWrite,
515
- total: costTotal,
516
- }
517
- : null,
512
+ cost:
513
+ listPriceCost !== null
514
+ ? {
515
+ input: costInput,
516
+ output: costOutput,
517
+ cacheRead: costCacheRead,
518
+ cacheWrite: costCacheWrite,
519
+ total: costTotal,
520
+ }
521
+ : null,
518
522
  rateUsdPerMTokens,
519
523
  timestamp: Date.now(),
520
524
  };
@@ -533,24 +537,12 @@ export default function tpsExtension(pi: ExtensionAPI) {
533
537
  // Cached session entries for argument completion (captured on session_start / session_tree)
534
538
  let cachedEntries: Array<{ type?: string; customType?: string; data?: unknown }> = [];
535
539
 
536
- // ── Neuralwatt per-turn billed-cost integration ─────────────────────────
537
- // pi-neuralwatt-provider emits NEURALWATT_ENERGY_EVENT in its turn_end handler
538
- // (after its tee reader drains) with { costUsd, energyJoules, turnIndex }. We stash
539
- // only the one number we need billed costUsd keyed by turnIndex, and read it
540
- // synchronously in our own turn_end. pi awaits turn_end handlers sequentially in
541
- // registration order, so if the neuralwatt provider was registered before us the
542
- // event has already fired and the cache hit is immediate; if after us we miss it
543
- // for that one turn and fall back to the list-price compute rate (the neuralwatt
544
- // widget still shows billing separately). Zero latency, no awaiting, no duplicate
545
- // capture — the raw energy/cost records stay solely in the neuralwatt provider's
546
- // own session entries. Non-Neuralwatt turns never emit, so this stays empty.
547
- const neuralwattBilledCostByTurn = new Map<number, number>();
548
-
549
- // Tracks the most recently committed turn so a late-arriving
550
- // `neuralwatt:turn-energy` event (provider loaded AFTER us) can retroactively
551
- // correct a turn that we already persisted with the list-price fallback.
552
- // Load-order-independent: when the provider loads before us the live cache
553
- // hits and `billedApplied` is already true, so this never fires.
540
+ // pi-neuralwatt-provider emits billed cost during its turn_end handler. Keep
541
+ // an early event only for the active turn; a late event corrects only the
542
+ // turn that was just committed. turnIndex resets to zero on every agent run,
543
+ // so billed costs must never survive beyond either of those scopes.
544
+ let pendingNeuralwattBilledCost: { turnIndex: number; costUsd: number } | null = null;
545
+
554
546
  let lastCommittedTurn: {
555
547
  turnIndex: number;
556
548
  telemetry: TurnTelemetry;
@@ -563,43 +555,34 @@ export default function tpsExtension(pi: ExtensionAPI) {
563
555
  const p = payload as Record<string, unknown>;
564
556
  const turnIndex = typeof p.turnIndex === 'number' ? p.turnIndex : null;
565
557
  const costUsd = typeof p.costUsd === 'number' ? p.costUsd : null;
566
- if (turnIndex === null || costUsd === null) return; // require turnIndex correlation
567
- neuralwattBilledCostByTurn.set(turnIndex, costUsd);
568
-
569
- // Late-arrival correction: the event reached us after our turn_end already
570
- // committed this turn on the list-price fallback. Recompute the blended
571
- // rate from the billed cost, persist a corrected `tps` entry (which later
572
- // resume/export reads in place of the stale list-price one), and refresh
573
- // the banner but only if no newer turn is mid-flight (currentTiming set
574
- // means a new turn is streaming; updating the banner then would be
575
- // misleading). Idempotent: once `billedApplied` flips true we never
576
- // re-correct, so repeated or duplicate events are safe.
577
- const committed = lastCommittedTurn;
578
- if (
579
- committed &&
580
- !committed.billedApplied &&
581
- committed.turnIndex === turnIndex &&
582
- !Number.isNaN(costUsd)
583
- ) {
584
- const totalTokens = committed.telemetry.tokens.total;
585
- const correctedRate = computeRateUsdPerM(costUsd, totalTokens);
586
- if (correctedRate !== null && correctedRate !== committed.telemetry.rateUsdPerMTokens) {
587
- const corrected = { ...committed.telemetry, rateUsdPerMTokens: correctedRate };
588
- committed.telemetry = corrected;
589
- committed.billedApplied = true;
590
- pi.appendEntry('tps', corrected);
591
- pi.events?.emit('tps:telemetry', corrected);
592
- cachedEntries.push({ type: 'custom', customType: 'tps' });
593
- if (committed.ctx.hasUI && !currentTiming) {
594
- committed.ctx.ui.notify(composeDisplayString(corrected), 'info');
595
- }
558
+ if (turnIndex === null || costUsd === null || !Number.isFinite(costUsd) || costUsd < 0) {
559
+ return;
560
+ }
561
+
562
+ // Provider loaded before pi-tps: turn_end has not reached us yet.
563
+ if (currentTiming) {
564
+ if (currentTiming.turnIndex === turnIndex) {
565
+ pendingNeuralwattBilledCost = { turnIndex, costUsd };
596
566
  }
567
+ return;
597
568
  }
598
- // Bound the cache (oldest = lowest turnIndex)
599
- if (neuralwattBilledCostByTurn.size > NEURALWATT_ENERGY_CACHE_MAX) {
600
- let oldest = Infinity;
601
- for (const k of neuralwattBilledCostByTurn.keys()) if (k < oldest) oldest = k;
602
- if (oldest !== Infinity) neuralwattBilledCostByTurn.delete(oldest);
569
+
570
+ // Provider loaded after pi-tps: correct the turn we just committed. Do not
571
+ // cache this event; the same turnIndex can belong to the next agent run.
572
+ const committed = lastCommittedTurn;
573
+ if (!committed || committed.billedApplied || committed.turnIndex !== turnIndex) return;
574
+
575
+ committed.billedApplied = true;
576
+ const correctedRate = computeRateUsdPerM(costUsd, committed.telemetry.tokens.total);
577
+ if (correctedRate === null || correctedRate === committed.telemetry.rateUsdPerMTokens) return;
578
+
579
+ const corrected = { ...committed.telemetry, rateUsdPerMTokens: correctedRate };
580
+ committed.telemetry = corrected;
581
+ pi.appendEntry('tps', corrected);
582
+ pi.events?.emit('tps:telemetry', corrected);
583
+ cachedEntries.push({ type: 'custom', customType: 'tps' });
584
+ if (committed.ctx.hasUI) {
585
+ committed.ctx.ui.notify(composeDisplayString(corrected), 'info');
603
586
  }
604
587
  });
605
588
 
@@ -613,7 +596,7 @@ export default function tpsExtension(pi: ExtensionAPI) {
613
596
  */
614
597
  function restoreTPSNotification(ctx: ExtensionContext) {
615
598
  if (!ctx.hasUI) return;
616
- const entries = cachedEntries.length > 0 ? cachedEntries : ctx.sessionManager.getEntries();
599
+ const entries = ctx.sessionManager.getBranch();
617
600
  for (let i = entries.length - 1; i >= 0; i--) {
618
601
  const entry = entries[i];
619
602
  if (entry.type === 'custom' && entry.customType === 'tps') {
@@ -646,6 +629,8 @@ export default function tpsExtension(pi: ExtensionAPI) {
646
629
 
647
630
  // Restore notification after /tree navigation (same session, different branch)
648
631
  pi.on('session_tree', (_event: SessionTreeEvent, ctx: ExtensionContext) => {
632
+ pendingNeuralwattBilledCost = null;
633
+ lastCommittedTurn = null;
649
634
  cachedEntries = ctx.sessionManager.getEntries();
650
635
  restoreTPSNotification(ctx);
651
636
  });
@@ -654,7 +639,10 @@ export default function tpsExtension(pi: ExtensionAPI) {
654
639
 
655
640
  // Track when a turn starts (request sent to LLM)
656
641
  pi.on('turn_start', (_event: TurnStartEvent) => {
642
+ pendingNeuralwattBilledCost = null;
643
+ lastCommittedTurn = null;
657
644
  currentTiming = {
645
+ turnIndex: _event.turnIndex,
658
646
  turnStartMs: performance.now(),
659
647
  turnStartTimestamp: typeof _event.timestamp === 'number' ? _event.timestamp : Date.now(),
660
648
  lastUpdateMs: performance.now(),
@@ -764,9 +752,8 @@ export default function tpsExtension(pi: ExtensionAPI) {
764
752
  // ── Persist telemetry ───────────────────────────────────────────────────
765
753
 
766
754
  // Calculate, display, and persist telemetry at the end of each LLM turn.
767
- // Synchronous: the Neuralwatt billed cost (if any) was stashed by our
768
- // NEURALWATT_ENERGY_EVENT listener, keyed by turnIndex, and is read here
769
- // synchronously. No awaiting, no added latency to turn dispatch.
755
+ // The Neuralwatt listener stores an early billed cost only while this exact
756
+ // turn is active, so the handoff remains synchronous and adds no latency.
770
757
  pi.on('turn_end', (event: TurnEndEvent, ctx: ExtensionContext) => {
771
758
  if (!currentTiming) return;
772
759
 
@@ -782,8 +769,11 @@ export default function tpsExtension(pi: ExtensionAPI) {
782
769
  // energy source is available do we fall back to the compute rate.
783
770
  // Prefer the live energy event, fall back to the persisted energy entry,
784
771
  // and finally to the list-price compute cost.
785
- let billedCost = neuralwattBilledCostByTurn.get(event.turnIndex) ?? null;
786
- neuralwattBilledCostByTurn.delete(event.turnIndex);
772
+ let billedCost =
773
+ pendingNeuralwattBilledCost?.turnIndex === event.turnIndex
774
+ ? pendingNeuralwattBilledCost.costUsd
775
+ : null;
776
+ pendingNeuralwattBilledCost = null;
787
777
 
788
778
  if (billedCost === null && timing.turnStartTimestamp) {
789
779
  billedCost = findEnergyCostFromSession(ctx, timing.turnStartTimestamp);
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@monotykamary/pi-tps",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@monotykamary/pi-tps",
9
- "version": "1.3.2",
9
+ "version": "1.3.4",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "devDependencies": {
package/package.json CHANGED
@@ -1,19 +1,11 @@
1
1
  {
2
2
  "name": "@monotykamary/pi-tps",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
4
4
  "description": "Tokens-per-second tracker for pi — see your LLM generation speed after every agent turn",
5
5
  "keywords": [
6
6
  "pi-package"
7
7
  ],
8
8
  "license": "MIT",
9
- "scripts": {
10
- "test": "vitest run",
11
- "test:watch": "vitest",
12
- "test:coverage": "vitest run --coverage",
13
- "typecheck": "tsc --noEmit",
14
- "lint:dead": "knip --no-gitignore",
15
- "postinstall": "simple-git-hooks 2>/dev/null || true"
16
- },
17
9
  "pi": {
18
10
  "extensions": [
19
11
  "./extensions"
@@ -32,8 +24,8 @@
32
24
  "devDependencies": {
33
25
  "@commitlint/cli": "21.0.1",
34
26
  "@commitlint/config-conventional": "21.0.1",
35
- "@earendil-works/pi-ai": "0.79.8",
36
- "@earendil-works/pi-coding-agent": "0.79.8",
27
+ "@earendil-works/pi-ai": "0.83.0",
28
+ "@earendil-works/pi-coding-agent": "0.83.0",
37
29
  "@types/node": "25.9.1",
38
30
  "@vitest/coverage-v8": "4.1.7",
39
31
  "knip": "6.14.1",
@@ -52,5 +44,13 @@
52
44
  "fast-xml-builder": "1.2.0",
53
45
  "protobufjs": "8.4.0",
54
46
  "ws": "8.20.1"
47
+ },
48
+ "scripts": {
49
+ "test": "vitest run",
50
+ "test:watch": "vitest",
51
+ "test:coverage": "vitest run --coverage",
52
+ "typecheck": "tsc --noEmit",
53
+ "lint:dead": "knip --no-gitignore",
54
+ "postinstall": "simple-git-hooks 2>/dev/null || true"
55
55
  }
56
- }
56
+ }
@@ -0,0 +1,9 @@
1
+ allowBuilds:
2
+ '@google/genai': false
3
+ protobufjs: false
4
+ simple-git-hooks: false
5
+ minimumReleaseAgeExclude:
6
+ - '@earendil-works/pi-agent-core@0.83.0'
7
+ - '@earendil-works/pi-ai@0.83.0'
8
+ - '@earendil-works/pi-coding-agent@0.83.0'
9
+ - '@earendil-works/pi-tui@0.83.0'