pi-goosedump 0.9.3 → 0.9.5

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 (2) hide show
  1. package/index.ts +202 -135
  2. package/package.json +10 -7
package/index.ts CHANGED
@@ -10,7 +10,12 @@ import type {
10
10
  SessionEntry,
11
11
  } from '@earendil-works/pi-coding-agent';
12
12
 
13
- import { defineTool, getSettingsListTheme } from '@earendil-works/pi-coding-agent';
13
+ import {
14
+ DEFAULT_COMPACTION_SETTINGS,
15
+ defineTool,
16
+ findCutPoint,
17
+ getSettingsListTheme,
18
+ } from '@earendil-works/pi-coding-agent';
14
19
 
15
20
  import { execFile, execFileSync } from 'node:child_process';
16
21
  import {
@@ -368,6 +373,43 @@ function resolveTarget(
368
373
  return null;
369
374
  }
370
375
 
376
+ const GOOSE_PROVIDERS = ['pi', 'claude', 'codex', 'crush', 'gemini', 'goose', 'opencode'] as const;
377
+
378
+ type HistoryOperation = 'search' | 'read' | 'grep';
379
+
380
+ const HISTORY_TARGET_GUIDELINE =
381
+ 'Use the current Pi session by default; non-Pi providers require an explicit session ID.';
382
+
383
+ function historyProviderParam(operation: HistoryOperation) {
384
+ return Type.Optional(
385
+ Type.Union(
386
+ GOOSE_PROVIDERS.map((provider) => Type.Literal(provider)),
387
+ {
388
+ default: 'pi',
389
+ description: `Source provider whose sessions to ${operation}. Defaults to pi. Non-pi providers have no current session, so they require an explicit session id.`,
390
+ },
391
+ ),
392
+ );
393
+ }
394
+
395
+ function historySessionParam() {
396
+ return Type.Optional(
397
+ Type.String({
398
+ description:
399
+ 'Context/session ID. Defaults to the current Pi session for pi; required when provider is not pi.',
400
+ }),
401
+ );
402
+ }
403
+
404
+ function historyScopeParam() {
405
+ return Type.Optional(
406
+ Type.Union([Type.Literal('lineage'), Type.Literal('all')], {
407
+ default: 'lineage',
408
+ description: 'Scope: lineage (current branch) or all entries in the session',
409
+ }),
410
+ );
411
+ }
412
+
371
413
  async function goosedumpCompact(
372
414
  target: string,
373
415
  options: { scope?: string; from?: string; until: string },
@@ -449,6 +491,56 @@ function isMessageEntry(entry: SessionEntry): entry is SessionEntry & {
449
491
  return entry.type === 'message';
450
492
  }
451
493
 
494
+ // Entry types that sessionEntryToContextMessages turns into context messages;
495
+ // only these count toward "is there anything to summarize".
496
+ const SUMMARIZABLE_ENTRY_TYPES = new Set(['message', 'custom_message', 'branch_summary']);
497
+
498
+ // Pi's compaction keepRecentTokens setting, project scope over global, matching
499
+ // SettingsManager's merge order.
500
+ function piKeepRecentTokens(cwd: string): number {
501
+ for (const path of [projectSettingsPath(cwd), globalSettingsPath()]) {
502
+ const compaction = readJsonObject(path).compaction;
503
+ if (isRecord(compaction) && typeof compaction.keepRecentTokens === 'number') {
504
+ return compaction.keepRecentTokens;
505
+ }
506
+ }
507
+ return DEFAULT_COMPACTION_SETTINGS.keepRecentTokens;
508
+ }
509
+
510
+ // Predict whether ctx.compact() would throw "Nothing to compact" or "Already
511
+ // compacted": mirrors prepareCompaction's guards over the active lineage so
512
+ // plugin-triggered compaction can skip the call instead of surfacing pi's
513
+ // error toast (and needlessly aborting the agent).
514
+ function hasCompactableHistory(ctx: ExtensionContext): boolean {
515
+ const branch = ctx.sessionManager.getBranch();
516
+ if (branch.length === 0) return false;
517
+ if (branch[branch.length - 1].type === 'compaction') return false;
518
+
519
+ let boundaryStart = 0;
520
+ for (let i = branch.length - 1; i >= 0; i -= 1) {
521
+ const entry = branch[i];
522
+ if (isCompactionEntry(entry)) {
523
+ const keptIndex = branch.findIndex((e) => e.id === entry.firstKeptEntryId);
524
+ boundaryStart = keptIndex >= 0 ? keptIndex : i + 1;
525
+ break;
526
+ }
527
+ }
528
+
529
+ const cut = findCutPoint(branch, boundaryStart, branch.length, piKeepRecentTokens(ctx.cwd));
530
+ if (!branch[cut.firstKeptEntryIndex]?.id) return false;
531
+
532
+ const historyEnd = cut.isSplitTurn ? cut.turnStartIndex : cut.firstKeptEntryIndex;
533
+ for (let i = boundaryStart; i < historyEnd; i += 1) {
534
+ if (SUMMARIZABLE_ENTRY_TYPES.has(branch[i].type)) return true;
535
+ }
536
+ if (cut.isSplitTurn) {
537
+ for (let i = cut.turnStartIndex; i < cut.firstKeptEntryIndex; i += 1) {
538
+ if (SUMMARIZABLE_ENTRY_TYPES.has(branch[i].type)) return true;
539
+ }
540
+ }
541
+ return false;
542
+ }
543
+
452
544
  // Resolution of the `keep:N` boundary: scan the active lineage from the newest
453
545
  // entry backward, count user turns (those whose message role is `user`), and
454
546
  // return the id of the Nth-from-last user entry — the earliest entry to keep.
@@ -595,12 +687,16 @@ class NumberInputSubmenu implements Component {
595
687
  handleInput(data: string): void {
596
688
  const kb = getKeybindings();
597
689
  if (kb.matches(data, 'tui.select.confirm') || data === '\n') {
598
- const digits = this.input.getValue().replace(/\D/g, '');
599
- if (digits === '') {
690
+ const value = this.input.getValue();
691
+ if (value === '') {
600
692
  this.onCancelEdit();
601
693
  return;
602
694
  }
603
- const parsed = Number(digits);
695
+ if (!/^\d+$/.test(value)) {
696
+ this.onInvalidValue(`Enter a whole number between ${this.min} and ${this.max}`);
697
+ return;
698
+ }
699
+ const parsed = Number(value);
604
700
  if (!Number.isSafeInteger(parsed) || parsed < this.min || parsed > this.max) {
605
701
  this.onInvalidValue(`Enter a whole number between ${this.min} and ${this.max}`);
606
702
  return;
@@ -1017,29 +1113,6 @@ export function createGoosedumpIntegration(
1017
1113
  if (turnsRemaining <= 0) counterTriggered = true;
1018
1114
  }
1019
1115
  updateStatus(ctx);
1020
-
1021
- if (!counterTriggered || compactionQueued) return;
1022
- // ctx.compact() uses manual compaction semantics and aborts the current run.
1023
- // Wait until no queued work remains before triggering plugin-driven compaction.
1024
- if (ctx.hasPendingMessages()) return;
1025
-
1026
- resetCompactCounters(ctx.cwd);
1027
- updateStatus(ctx);
1028
- compactionQueued = true;
1029
- ctx.compact({
1030
- onComplete: () => {
1031
- compactionQueued = false;
1032
- resetCompactCounters(ctx.cwd);
1033
- updateStatus(ctx);
1034
- },
1035
- onError: (error) => {
1036
- compactionQueued = false;
1037
- resetCompactCounters(ctx.cwd);
1038
- updateStatus(ctx);
1039
- const message = error instanceof Error ? error.message : String(error);
1040
- if (ctx.hasUI) ctx.ui.notify(`goosedump compact failed: ${message}`, 'error');
1041
- },
1042
- });
1043
1116
  }
1044
1117
 
1045
1118
  function checkGoosedump(ctx?: ExtensionContext): boolean {
@@ -1061,18 +1134,28 @@ export function createGoosedumpIntegration(
1061
1134
  // command and the goose_compact tool). Stashes a turn-based keep override and
1062
1135
  // fires Pi's compaction flow; the session_before_compact hook reads pendingKeep
1063
1136
  // and resets it on use, completion, or error.
1064
- function requestCompaction(ctx: ExtensionContext, keep: number | null): void {
1137
+ function requestCompaction(
1138
+ ctx: ExtensionContext,
1139
+ keep: number | null,
1140
+ ): 'started' | 'busy' | 'nothing-to-compact' {
1141
+ if (compactionQueued) return 'busy';
1142
+ if (!hasCompactableHistory(ctx)) return 'nothing-to-compact';
1143
+
1144
+ compactionQueued = true;
1065
1145
  pendingKeep = keep;
1066
1146
  ctx.compact({
1067
1147
  onComplete: () => {
1148
+ compactionQueued = false;
1068
1149
  pendingKeep = null;
1069
1150
  },
1070
1151
  onError: (error) => {
1152
+ compactionQueued = false;
1071
1153
  pendingKeep = null;
1072
1154
  const message = error instanceof Error ? error.message : String(error);
1073
1155
  if (ctx.hasUI) ctx.ui.notify(`goosedump compact failed: ${message}`, 'error');
1074
1156
  },
1075
1157
  });
1158
+ return 'started';
1076
1159
  }
1077
1160
 
1078
1161
  function register(pi: ExtensionAPI): void {
@@ -1083,46 +1166,18 @@ export function createGoosedumpIntegration(
1083
1166
  label: 'goose_search',
1084
1167
  description:
1085
1168
  'Search coding agent session history by ranked relevance. Returns compact entry overviews with entry IDs; use goose_get to expand specific IDs to full content. Defaults to the current Pi session.',
1086
- promptSnippet:
1087
- 'goose_search({ query, provider?, session?, scope?, page? }) - rank messages by query relevance; defaults to current Pi session',
1169
+ promptSnippet: 'Search session history by ranked relevance',
1088
1170
  promptGuidelines: [
1089
1171
  "Use goose_search to find relevant context in this or another coding agent's session history.",
1090
1172
  'Start with compact ranked results, then use goose_get to expand interesting entry IDs to full content.',
1091
1173
  'Default scope is "lineage" (current branch); use scope: "all" to include all entries in the session.',
1092
- 'Omit session to search the current Pi session; set provider (e.g. "claude", "gemini") with a session id to browse another tool\'s sessions.',
1174
+ HISTORY_TARGET_GUIDELINE,
1093
1175
  ],
1094
1176
  parameters: Type.Object({
1095
1177
  query: Type.String({ description: 'Search query to rank messages by relevance' }),
1096
- provider: Type.Optional(
1097
- Type.Union(
1098
- [
1099
- Type.Literal('pi'),
1100
- Type.Literal('claude'),
1101
- Type.Literal('codex'),
1102
- Type.Literal('crush'),
1103
- Type.Literal('gemini'),
1104
- Type.Literal('goose'),
1105
- Type.Literal('opencode'),
1106
- ],
1107
- {
1108
- default: 'pi',
1109
- description:
1110
- 'Source provider whose sessions to search. Defaults to pi. Non-pi providers have no current session, so they require an explicit session id.',
1111
- },
1112
- ),
1113
- ),
1114
- session: Type.Optional(
1115
- Type.String({
1116
- description:
1117
- 'Context/session ID. Defaults to the current Pi session for pi; required when provider is not pi.',
1118
- }),
1119
- ),
1120
- scope: Type.Optional(
1121
- Type.Union([Type.Literal('lineage'), Type.Literal('all')], {
1122
- default: 'lineage',
1123
- description: 'Scope: lineage (current branch) or all entries in the session',
1124
- }),
1125
- ),
1178
+ provider: historyProviderParam('search'),
1179
+ session: historySessionParam(),
1180
+ scope: historyScopeParam(),
1126
1181
  page: Type.Optional(
1127
1182
  Type.Integer({
1128
1183
  minimum: 1,
@@ -1166,47 +1221,21 @@ export function createGoosedumpIntegration(
1166
1221
  label: 'goose_get',
1167
1222
  description:
1168
1223
  'Expand specific entry IDs (from goose_search or goose_grep) to their full content. Defaults to the current Pi session.',
1169
- promptSnippet:
1170
- 'goose_get({ ids, provider?, session? }) - expand entry IDs to full content; defaults to current Pi session',
1224
+ promptSnippet: 'Expand session entries by ID to full content',
1171
1225
  promptGuidelines: [
1172
1226
  'Use goose_get to expand entry IDs returned by goose_search or goose_grep into their full content.',
1173
1227
  'Pass only the specific IDs you need; full transcripts are expensive.',
1228
+ 'Default scope is "lineage" (current branch); use scope: "all" to retrieve entries from all session branches.',
1229
+ HISTORY_TARGET_GUIDELINE,
1174
1230
  ],
1175
1231
  parameters: Type.Object({
1176
1232
  ids: Type.Array(Type.String(), {
1177
1233
  minItems: 1,
1178
1234
  description: 'Entry IDs to expand (show full content)',
1179
1235
  }),
1180
- provider: Type.Optional(
1181
- Type.Union(
1182
- [
1183
- Type.Literal('pi'),
1184
- Type.Literal('claude'),
1185
- Type.Literal('codex'),
1186
- Type.Literal('crush'),
1187
- Type.Literal('gemini'),
1188
- Type.Literal('goose'),
1189
- Type.Literal('opencode'),
1190
- ],
1191
- {
1192
- default: 'pi',
1193
- description:
1194
- 'Source provider whose sessions to read. Defaults to pi. Non-pi providers have no current session, so they require an explicit session id.',
1195
- },
1196
- ),
1197
- ),
1198
- session: Type.Optional(
1199
- Type.String({
1200
- description:
1201
- 'Context/session ID. Defaults to the current Pi session for pi; required when provider is not pi.',
1202
- }),
1203
- ),
1204
- scope: Type.Optional(
1205
- Type.Union([Type.Literal('lineage'), Type.Literal('all')], {
1206
- default: 'lineage',
1207
- description: 'Scope: lineage (current branch) or all entries in the session',
1208
- }),
1209
- ),
1236
+ provider: historyProviderParam('read'),
1237
+ session: historySessionParam(),
1238
+ scope: historyScopeParam(),
1210
1239
  }),
1211
1240
  async execute(_id, params, _signal, _onUpdate, ctx): Promise<AgentToolResult<void>> {
1212
1241
  if (!goosedumpAvailable) return goosedumpUnavailableResult();
@@ -1239,45 +1268,18 @@ export function createGoosedumpIntegration(
1239
1268
  label: 'goose_grep',
1240
1269
  description:
1241
1270
  'Filter session messages by glob pattern (e.g. *rand*). Returns compact entry overviews with entry IDs; use goose_get to expand. Defaults to the current Pi session.',
1242
- promptSnippet:
1243
- 'goose_grep({ pattern, provider?, session?, scope? }) - filter messages by glob; defaults to current Pi session',
1271
+ promptSnippet: 'Filter session history by glob pattern',
1244
1272
  promptGuidelines: [
1245
1273
  'Use goose_grep to filter session messages by glob pattern when you need exact substring/glob matching instead of ranked relevance.',
1246
1274
  'Expand interesting entry IDs with goose_get.',
1247
1275
  'Default scope is "lineage" (current branch); use scope: "all" to include all entries in the session.',
1276
+ HISTORY_TARGET_GUIDELINE,
1248
1277
  ],
1249
1278
  parameters: Type.Object({
1250
1279
  pattern: Type.String({ description: 'Glob pattern to filter messages (e.g. *rand*)' }),
1251
- provider: Type.Optional(
1252
- Type.Union(
1253
- [
1254
- Type.Literal('pi'),
1255
- Type.Literal('claude'),
1256
- Type.Literal('codex'),
1257
- Type.Literal('crush'),
1258
- Type.Literal('gemini'),
1259
- Type.Literal('goose'),
1260
- Type.Literal('opencode'),
1261
- ],
1262
- {
1263
- default: 'pi',
1264
- description:
1265
- 'Source provider whose sessions to grep. Defaults to pi. Non-pi providers have no current session, so they require an explicit session id.',
1266
- },
1267
- ),
1268
- ),
1269
- session: Type.Optional(
1270
- Type.String({
1271
- description:
1272
- 'Context/session ID. Defaults to the current Pi session for pi; required when provider is not pi.',
1273
- }),
1274
- ),
1275
- scope: Type.Optional(
1276
- Type.Union([Type.Literal('lineage'), Type.Literal('all')], {
1277
- default: 'lineage',
1278
- description: 'Scope: lineage (current branch) or all entries in the session',
1279
- }),
1280
- ),
1280
+ provider: historyProviderParam('grep'),
1281
+ session: historySessionParam(),
1282
+ scope: historyScopeParam(),
1281
1283
  }),
1282
1284
  async execute(_id, params, _signal, _onUpdate, ctx): Promise<AgentToolResult<void>> {
1283
1285
  if (!goosedumpAvailable) return goosedumpUnavailableResult();
@@ -1310,8 +1312,7 @@ export function createGoosedumpIntegration(
1310
1312
  label: 'goose_compact',
1311
1313
  description:
1312
1314
  "Trigger a goosedump compaction of the current session now, optionally keeping the last N user turns verbatim while summarizing everything before. Compaction runs asynchronously via Pi's compaction flow.",
1313
- promptSnippet:
1314
- "goose_compact({ keep? }) - compact now; keep last N user turns (turn-based), omit to compact up to Pi's cut",
1315
+ promptSnippet: 'Compact session history; optionally keep recent user turns',
1315
1316
  promptGuidelines: [
1316
1317
  'Use goose_compact to compact the session proactively with goosedump when context is large or nearing limits.',
1317
1318
  "Pass keep:N (N >= 1) to keep the last N user turns verbatim and summarize everything before them; omit to compact up to Pi's token-based cut.",
@@ -1330,7 +1331,11 @@ export function createGoosedumpIntegration(
1330
1331
  if (!goosedumpAvailable) return goosedumpUnavailableResult();
1331
1332
 
1332
1333
  const keep = params.keep ?? null;
1333
- requestCompaction(ctx, keep);
1334
+ const result = requestCompaction(ctx, keep);
1335
+ if (result === 'busy') return textResult('Compaction already in progress.');
1336
+ if (result === 'nothing-to-compact') {
1337
+ return textResult('Nothing to compact (session too small).');
1338
+ }
1334
1339
  return textResult(
1335
1340
  keep !== null
1336
1341
  ? `Compacting with goosedump; keeping the last ${keep} user turn${keep === 1 ? '' : 's'}.`
@@ -1341,14 +1346,62 @@ export function createGoosedumpIntegration(
1341
1346
  );
1342
1347
  }
1343
1348
 
1349
+ // Fire counter-driven compaction. ctx.compact() uses manual compaction
1350
+ // semantics and aborts the current run, so a mid-run trigger (resumeAfter)
1351
+ // re-triggers the agent with a synthetic continue message once the
1352
+ // compaction lands. If the session is still too small to compact, the
1353
+ // counter stays armed and retries at a later turn_end/agent_end.
1354
+ function maybeAutoCompact(ctx: ExtensionContext, resumeAfter: boolean): void {
1355
+ if (!goosedumpAvailable || !shouldOverrideDefaultCompaction) return;
1356
+ if (!counterTriggered || compactionQueued) return;
1357
+ if (ctx.hasPendingMessages()) return;
1358
+ if (!hasCompactableHistory(ctx)) return;
1359
+
1360
+ resetCompactCounters(ctx.cwd);
1361
+ updateStatus(ctx);
1362
+ compactionQueued = true;
1363
+ ctx.compact({
1364
+ onComplete: () => {
1365
+ compactionQueued = false;
1366
+ resetCompactCounters(ctx.cwd);
1367
+ updateStatus(ctx);
1368
+ if (resumeAfter && !ctx.hasPendingMessages()) {
1369
+ pi.sendMessage(
1370
+ {
1371
+ customType: 'goosedump-autocompact-resume',
1372
+ content:
1373
+ 'The session was compacted to reduce context size. Continue the task you were working on from where you left off.',
1374
+ display: false,
1375
+ },
1376
+ { triggerTurn: true },
1377
+ );
1378
+ }
1379
+ },
1380
+ onError: (error) => {
1381
+ compactionQueued = false;
1382
+ resetCompactCounters(ctx.cwd);
1383
+ updateStatus(ctx);
1384
+ const message = error instanceof Error ? error.message : String(error);
1385
+ if (ctx.hasUI) ctx.ui.notify(`goosedump compact failed: ${message}`, 'error');
1386
+ },
1387
+ });
1388
+ }
1389
+
1344
1390
  pi.on('session_start', async (_event, ctx) => {
1345
1391
  goosedumpAvailable = checkGoosedump(ctx);
1346
1392
  resetCompactCounters(ctx.cwd);
1347
1393
  updateStatus(ctx);
1348
1394
  });
1349
1395
 
1350
- pi.on('turn_end', async (_event, ctx) => {
1396
+ pi.on('turn_end', async (event, ctx) => {
1351
1397
  noteCompletedTurn(ctx);
1398
+ // A turn with tool results means the agent loop would keep going; compact
1399
+ // between turns and resume so the run's progress is not lost.
1400
+ if (event.toolResults.length > 0) maybeAutoCompact(ctx, true);
1401
+ });
1402
+
1403
+ pi.on('agent_end', async (_event, ctx) => {
1404
+ maybeAutoCompact(ctx, false);
1352
1405
  });
1353
1406
 
1354
1407
  pi.on('session_before_compact', async (event, ctx) => {
@@ -1371,11 +1424,9 @@ export function createGoosedumpIntegration(
1371
1424
  keep,
1372
1425
  );
1373
1426
  if (compaction && 'cancel' in compaction) {
1374
- pendingKeep = null;
1375
1427
  if (ctx.hasUI) ctx.ui.notify(compaction.reason, 'info');
1376
1428
  return { cancel: true };
1377
1429
  }
1378
- pendingKeep = null;
1379
1430
  return compaction ? { compaction } : undefined;
1380
1431
  } catch (err) {
1381
1432
  if (ctx.hasUI) {
@@ -1386,15 +1437,26 @@ export function createGoosedumpIntegration(
1386
1437
  );
1387
1438
  }
1388
1439
  return undefined;
1440
+ } finally {
1441
+ pendingKeep = null;
1389
1442
  }
1390
1443
  });
1391
1444
 
1392
1445
  pi.on('session_compact', async (event, ctx) => {
1393
- if (!goosedumpAvailable || !event.fromExtension) return;
1446
+ if (!goosedumpAvailable || !shouldOverrideDefaultCompaction) return;
1394
1447
 
1395
- const compactionEntry = event.compactionEntry;
1396
- if (!compactionEntry) return;
1448
+ resetCompactCounters(ctx.cwd);
1449
+ updateStatus(ctx);
1397
1450
 
1451
+ const compactionEntry = event.compactionEntry;
1452
+ if (
1453
+ !event.fromExtension ||
1454
+ !compactionEntry ||
1455
+ !isRecord(compactionEntry.details) ||
1456
+ compactionEntry.details.source !== 'goosedump'
1457
+ ) {
1458
+ return;
1459
+ }
1398
1460
  const tokensBefore = compactionEntry.tokensBefore;
1399
1461
  const summaryLen = compactionEntry.summary.length;
1400
1462
  const tokenEstimate = Math.ceil(summaryLen / 4);
@@ -1451,7 +1513,12 @@ export function createGoosedumpIntegration(
1451
1513
  }
1452
1514
  }
1453
1515
 
1454
- requestCompaction(ctx, keep);
1516
+ const result = requestCompaction(ctx, keep);
1517
+ if (result === 'busy') {
1518
+ ctx.ui.notify('Compaction already in progress', 'info');
1519
+ } else if (result === 'nothing-to-compact') {
1520
+ ctx.ui.notify('Nothing to compact (session too small)', 'info');
1521
+ }
1455
1522
  },
1456
1523
  });
1457
1524
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goosedump",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
4
4
  "description": "Pi extension for goosedump-backed session-history search, entry expansion, and compaction",
5
5
  "keywords": [
6
6
  "compaction",
@@ -21,13 +21,15 @@
21
21
  ],
22
22
  "type": "module",
23
23
  "scripts": {
24
- "fmt": "oxfmt index.ts package.json tsconfig.json .oxfmtrc.json README.md",
25
- "lint": "oxlint index.ts",
24
+ "fmt": "oxfmt index.ts tests package.json tsconfig.json .oxfmtrc.json README.md",
25
+ "lint": "oxlint index.ts tests",
26
26
  "check": "tsc --noEmit",
27
- "all": "npm run fmt && npm run lint && npm run check",
28
- "ci:fmt": "oxfmt --check index.ts package.json tsconfig.json .oxfmtrc.json README.md",
27
+ "test": "vitest run",
28
+ "all": "npm run fmt && npm run lint && npm run check && npm test",
29
+ "ci:fmt": "oxfmt --check index.ts tests package.json tsconfig.json .oxfmtrc.json README.md",
29
30
  "ci:lint": "npm run lint",
30
- "ci:check": "npm run check"
31
+ "ci:check": "npm run check",
32
+ "ci:test": "npm test"
31
33
  },
32
34
  "dependencies": {
33
35
  "@earendil-works/pi-tui": "^0.78.0",
@@ -39,7 +41,8 @@
39
41
  "@types/node": "^24.0.0",
40
42
  "oxfmt": "^0.53.0",
41
43
  "oxlint": "^1.68.0",
42
- "typescript": "^5.0.0"
44
+ "typescript": "^5.0.0",
45
+ "vitest": "^4.1.10"
43
46
  },
44
47
  "peerDependencies": {
45
48
  "@earendil-works/pi-coding-agent": "^0.78.0"