@diegopetrucci/pi-extensions 0.1.57 → 0.1.59

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 (102) hide show
  1. package/.pi-fleet-tested-version +1 -1
  2. package/extensions/agent-workflow-audit/.pi-fleet-tested-version +1 -1
  3. package/extensions/agent-workflow-audit/index.ts +8 -1
  4. package/extensions/agent-workflow-audit/package.json +5 -1
  5. package/extensions/annotate-git-diff/.pi-fleet-tested-version +1 -1
  6. package/extensions/annotate-git-diff/package.json +5 -2
  7. package/extensions/annotate-last-message/.pi-fleet-tested-version +1 -1
  8. package/extensions/annotate-last-message/package.json +5 -2
  9. package/extensions/brrr/.pi-fleet-tested-version +1 -1
  10. package/extensions/brrr/README.md +2 -2
  11. package/extensions/brrr/package.json +5 -1
  12. package/extensions/claude-fast/.pi-fleet-tested-version +1 -1
  13. package/extensions/claude-fast/package.json +5 -1
  14. package/extensions/code-reviewer/.pi-fleet-tested-version +1 -1
  15. package/extensions/code-reviewer/index.ts +129 -39
  16. package/extensions/code-reviewer/package.json +5 -1
  17. package/extensions/confirm-destructive/.pi-fleet-tested-version +1 -1
  18. package/extensions/confirm-destructive/README.md +1 -1
  19. package/extensions/confirm-destructive/index.ts +20 -5
  20. package/extensions/confirm-destructive/package.json +5 -1
  21. package/extensions/context-cap/.pi-fleet-tested-version +1 -1
  22. package/extensions/context-cap/package.json +5 -1
  23. package/extensions/context-inspector/.pi-fleet-tested-version +1 -1
  24. package/extensions/context-inspector/package.json +5 -1
  25. package/extensions/contrarian/.pi-fleet-tested-version +1 -1
  26. package/extensions/contrarian/README.md +1 -1
  27. package/extensions/contrarian/index.ts +199 -40
  28. package/extensions/contrarian/package.json +5 -1
  29. package/extensions/dirty-repo-guard/.pi-fleet-tested-version +1 -1
  30. package/extensions/dirty-repo-guard/README.md +1 -1
  31. package/extensions/dirty-repo-guard/package.json +5 -1
  32. package/extensions/dynamic-context-pruning/.pi-fleet-tested-version +1 -1
  33. package/extensions/dynamic-context-pruning/package.json +4 -1
  34. package/extensions/git-footer/.pi-fleet-tested-version +1 -1
  35. package/extensions/git-footer/package.json +5 -1
  36. package/extensions/gnosis/.pi-fleet-tested-version +1 -1
  37. package/extensions/gnosis/package.json +5 -1
  38. package/extensions/illustrations-to-explain-things/.pi-fleet-tested-version +1 -1
  39. package/extensions/illustrations-to-explain-things/package.json +4 -1
  40. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/01-two-breakpoints.webp +0 -0
  41. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/02-minimum-loop.webp +0 -0
  42. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/03-sort-by-purpose.webp +0 -0
  43. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/04-one-fish-many-uses.webp +0 -0
  44. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/05-handoff-path.webp +0 -0
  45. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/06-three-sources.webp +0 -0
  46. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/07-three-content-jobs.webp +0 -0
  47. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/08-handoff-copy-toolbox.webp +0 -0
  48. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/09-common-pits-no-title.webp +0 -0
  49. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/10-information-well.webp +0 -0
  50. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/11-idea-press.webp +0 -0
  51. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/12-content-fermentation.webp +0 -0
  52. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/13-system-bearing.webp +0 -0
  53. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/14-trust-bridge.webp +0 -0
  54. package/extensions/inline-bash/.pi-fleet-tested-version +1 -1
  55. package/extensions/inline-bash/README.md +1 -1
  56. package/extensions/inline-bash/package.json +5 -1
  57. package/extensions/librarian/.pi-fleet-tested-version +1 -1
  58. package/extensions/librarian/index.ts +393 -107
  59. package/extensions/librarian/package.json +5 -1
  60. package/extensions/minimal-footer/.pi-fleet-tested-version +1 -1
  61. package/extensions/minimal-footer/index.ts +2 -5
  62. package/extensions/minimal-footer/openai-usage.ts +58 -11
  63. package/extensions/minimal-footer/package.json +5 -1
  64. package/extensions/notify/.pi-fleet-tested-version +1 -1
  65. package/extensions/notify/README.md +3 -3
  66. package/extensions/notify/package.json +5 -1
  67. package/extensions/openai-fast/.pi-fleet-tested-version +1 -1
  68. package/extensions/openai-fast/package.json +5 -1
  69. package/extensions/oracle/.pi-fleet-tested-version +1 -1
  70. package/extensions/oracle/README.md +1 -1
  71. package/extensions/oracle/index.ts +200 -41
  72. package/extensions/oracle/package.json +5 -1
  73. package/extensions/permission-gate/.pi-fleet-tested-version +1 -1
  74. package/extensions/permission-gate/README.md +1 -1
  75. package/extensions/permission-gate/index.ts +453 -2
  76. package/extensions/permission-gate/package.json +5 -1
  77. package/extensions/quiet-tools/.pi-fleet-tested-version +1 -1
  78. package/extensions/quiet-tools/package.json +5 -1
  79. package/extensions/review/.pi-fleet-tested-version +1 -1
  80. package/extensions/review/index.ts +53 -19
  81. package/extensions/review/package.json +5 -1
  82. package/extensions/todo/.pi-fleet-tested-version +1 -1
  83. package/extensions/todo/README.md +1 -1
  84. package/extensions/todo/package.json +5 -1
  85. package/extensions/triage-comments/.pi-fleet-tested-version +1 -1
  86. package/extensions/triage-comments/index.ts +115 -34
  87. package/extensions/triage-comments/package.json +5 -1
  88. package/package.json +10 -6
  89. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/01-two-breakpoints.png +0 -0
  90. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/02-minimum-loop.png +0 -0
  91. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/03-sort-by-purpose.png +0 -0
  92. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/04-one-fish-many-uses.png +0 -0
  93. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/05-handoff-path.png +0 -0
  94. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/06-three-sources.png +0 -0
  95. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/07-three-content-jobs.png +0 -0
  96. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/08-handoff-copy-toolbox.png +0 -0
  97. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/09-common-pits-no-title.png +0 -0
  98. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/10-information-well.png +0 -0
  99. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/11-idea-press.png +0 -0
  100. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/12-content-fermentation.png +0 -0
  101. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/13-system-bearing.png +0 -0
  102. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/14-trust-bridge.png +0 -0
@@ -2,8 +2,8 @@ import { spawn } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
3
  import * as fs from "node:fs/promises";
4
4
  import * as path from "node:path";
5
- import { StringEnum } from "@earendil-works/pi-ai";
6
- import { getAgentDir, getMarkdownTheme, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
5
+ import { StringEnum, type Usage } from "@earendil-works/pi-ai";
6
+ import { getAgentDir, getMarkdownTheme, type AgentToolResult, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
7
7
  import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
8
8
  import { Type } from "typebox";
9
9
 
@@ -25,7 +25,14 @@ interface UsageStats {
25
25
  output: number;
26
26
  cacheRead: number;
27
27
  cacheWrite: number;
28
+ cacheWrite1h?: number;
29
+ reasoning?: number;
30
+ totalTokens: number;
28
31
  cost: number;
32
+ costInput: number;
33
+ costOutput: number;
34
+ costCacheRead: number;
35
+ costCacheWrite: number;
29
36
  turns: number;
30
37
  contextTokens: number;
31
38
  }
@@ -231,7 +238,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
231
238
  "MiniMaxAI/MiniMax-M2.5",
232
239
  "Qwen/Qwen3-Coder-Next",
233
240
  ],
234
- "kimi-coding": ["k2p7", "kimi-k2-thinking", "kimi-for-coding"],
241
+ "kimi-coding": ["k3", "kimi-for-coding", "kimi-for-coding-highspeed"],
235
242
  minimax: ["MiniMax-M3", "MiniMax-M2.7-highspeed", "MiniMax-M2.7"],
236
243
  "minimax-cn": ["MiniMax-M3", "MiniMax-M2.7-highspeed", "MiniMax-M2.7"],
237
244
  mistral: [
@@ -359,17 +366,35 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
359
366
  "z-ai/glm-5.2",
360
367
  "z-ai/glm-5.1",
361
368
  ],
369
+ "qwen-token-plan": [
370
+ "qwen3.8-max-preview",
371
+ "qwen3.7-max",
372
+ "qwen3.7-plus",
373
+ "qwen3.6-plus",
374
+ "deepseek-v4-pro",
375
+ "glm-5.2",
376
+ "kimi-k2.7-code",
377
+ "minimax-m2.5",
378
+ ],
379
+ "qwen-token-plan-cn": [
380
+ "qwen3.8-max-preview",
381
+ "qwen3.7-max",
382
+ "qwen3.7-plus",
383
+ "qwen3.6-plus",
384
+ "deepseek-v4-pro",
385
+ "glm-5.2",
386
+ "kimi-k2.7-code",
387
+ "minimax-m2.5",
388
+ ],
362
389
  together: [
363
390
  "deepseek-ai/DeepSeek-V4-Pro",
364
391
  "zai-org/GLM-5.2",
365
- "zai-org/GLM-5.1",
366
392
  "moonshotai/Kimi-K2.7-Code",
367
393
  "moonshotai/Kimi-K2.6",
368
394
  "Qwen/Qwen3.7-Max",
369
395
  "Qwen/Qwen3.6-Plus",
370
396
  "MiniMaxAI/MiniMax-M3",
371
397
  "MiniMaxAI/MiniMax-M2.7",
372
- "Qwen/Qwen3.5-397B-A17B",
373
398
  "openai/gpt-oss-120b",
374
399
  "openai/gpt-oss-20b",
375
400
  "nvidia/nemotron-3-ultra-550b-a55b",
@@ -417,13 +442,8 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
417
442
  ],
418
443
  xai: [
419
444
  "grok-4.5",
420
- "grok-4.20-0309-reasoning",
421
- "grok-4.20-0309-non-reasoning",
422
445
  "grok-4.3",
423
446
  "grok-build-0.1",
424
- "grok-code-fast-1",
425
- "grok-3-fast",
426
- "grok-3",
427
447
  ],
428
448
  xiaomi: ["mimo-v2.5-pro", "mimo-v2.5", "mimo-v2-pro", "mimo-v2-omni", "mimo-v2-flash"],
429
449
  "xiaomi-token-plan-ams": ["mimo-v2.5-pro", "mimo-v2.5", "mimo-v2-pro"],
@@ -434,7 +454,8 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
434
454
  "nvidia/nemotron-3-super-120b-a12b",
435
455
  "moonshotai/kimi-k2.6",
436
456
  "z-ai/glm-5.2",
437
- "qwen/qwen3.5-122b-a10b",
457
+ "minimaxai/minimax-m3",
458
+ "mistralai/mistral-large-3-675b-instruct-2512",
438
459
  "openai/gpt-oss-120b",
439
460
  "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning",
440
461
  "nvidia/nemotron-3-nano-30b-a3b",
@@ -501,12 +522,91 @@ function createEmptyUsage(): UsageStats {
501
522
  output: 0,
502
523
  cacheRead: 0,
503
524
  cacheWrite: 0,
525
+ totalTokens: 0,
504
526
  cost: 0,
527
+ costInput: 0,
528
+ costOutput: 0,
529
+ costCacheRead: 0,
530
+ costCacheWrite: 0,
505
531
  turns: 0,
506
532
  contextTokens: 0,
507
533
  };
508
534
  }
509
535
 
536
+ function cloneUsageStats(stats: UsageStats): UsageStats {
537
+ return { ...stats };
538
+ }
539
+
540
+ function mergeUsageStats(target: UsageStats, source: UsageStats | undefined): UsageStats {
541
+ if (!source) return target;
542
+ target.input += source.input;
543
+ target.output += source.output;
544
+ target.cacheRead += source.cacheRead;
545
+ target.cacheWrite += source.cacheWrite;
546
+ if (source.cacheWrite1h !== undefined) {
547
+ target.cacheWrite1h = (target.cacheWrite1h ?? 0) + source.cacheWrite1h;
548
+ }
549
+ if (source.reasoning !== undefined) {
550
+ target.reasoning = (target.reasoning ?? 0) + source.reasoning;
551
+ }
552
+ target.totalTokens += source.totalTokens;
553
+ target.cost += source.cost;
554
+ target.costInput += source.costInput;
555
+ target.costOutput += source.costOutput;
556
+ target.costCacheRead += source.costCacheRead;
557
+ target.costCacheWrite += source.costCacheWrite;
558
+ target.turns += source.turns;
559
+ target.contextTokens = source.contextTokens || target.contextTokens;
560
+ return target;
561
+ }
562
+
563
+ function usageStatsToToolUsage(stats: UsageStats): Usage {
564
+ return {
565
+ input: stats.input,
566
+ output: stats.output,
567
+ cacheRead: stats.cacheRead,
568
+ cacheWrite: stats.cacheWrite,
569
+ ...(stats.cacheWrite1h !== undefined ? { cacheWrite1h: stats.cacheWrite1h } : {}),
570
+ ...(stats.reasoning !== undefined ? { reasoning: stats.reasoning } : {}),
571
+ totalTokens: stats.totalTokens,
572
+ cost: {
573
+ input: stats.costInput,
574
+ output: stats.costOutput,
575
+ cacheRead: stats.costCacheRead,
576
+ cacheWrite: stats.costCacheWrite,
577
+ total: stats.cost,
578
+ },
579
+ };
580
+ }
581
+
582
+ function addUsageStats(
583
+ target: UsageStats,
584
+ source: Usage | undefined,
585
+ options: { countTurn?: boolean; updateContextTokens?: boolean } = {},
586
+ ): void {
587
+ if (!source) return;
588
+ if (options.countTurn) target.turns += 1;
589
+ target.input += source.input || 0;
590
+ target.output += source.output || 0;
591
+ target.cacheRead += source.cacheRead || 0;
592
+ target.cacheWrite += source.cacheWrite || 0;
593
+ if (source.cacheWrite1h !== undefined) {
594
+ target.cacheWrite1h = (target.cacheWrite1h ?? 0) + source.cacheWrite1h;
595
+ }
596
+ if (source.reasoning !== undefined) {
597
+ target.reasoning = (target.reasoning ?? 0) + source.reasoning;
598
+ }
599
+ target.totalTokens += source.totalTokens || 0;
600
+ target.cost += source.cost?.total || 0;
601
+ target.costInput += source.cost?.input || 0;
602
+ target.costOutput += source.cost?.output || 0;
603
+ target.costCacheRead += source.cost?.cacheRead || 0;
604
+ target.costCacheWrite += source.cost?.cacheWrite || 0;
605
+ if (options.updateContextTokens) {
606
+ target.contextTokens = source.totalTokens || target.contextTokens;
607
+ }
608
+ }
609
+
510
610
  function getOracleConfigPath(): string {
511
611
  return path.join(getAgentDir(), "extensions", ORACLE_CONFIG_FILE);
512
612
  }
@@ -595,6 +695,7 @@ function formatUsage(stats: UsageStats): string {
595
695
  if (stats.output) parts.push(`↓${formatTokens(stats.output)}`);
596
696
  if (stats.cacheRead) parts.push(`R${formatTokens(stats.cacheRead)}`);
597
697
  if (stats.cacheWrite) parts.push(`W${formatTokens(stats.cacheWrite)}`);
698
+ if (stats.reasoning) parts.push(`T${formatTokens(stats.reasoning)}`);
598
699
  if (stats.cost) parts.push(`$${stats.cost.toFixed(4)}`);
599
700
  if (stats.contextTokens) parts.push(`ctx:${formatTokens(stats.contextTokens)}`);
600
701
  return parts.join(" ");
@@ -934,6 +1035,7 @@ async function runOracle(
934
1035
  signal: AbortSignal | undefined,
935
1036
  onUpdate: ((result: { content: Array<{ type: "text"; text: string }>; details: OracleDetails }) => void) | undefined,
936
1037
  defaultCwd: string,
1038
+ spawnImpl: typeof spawn = spawn,
937
1039
  ): Promise<{ ok: true; output: string; details: OracleDetails } | { ok: false; error: string; details: OracleDetails }> {
938
1040
  const cwd = params.cwd ?? defaultCwd;
939
1041
  const includeBash = params.includeBash ?? false;
@@ -985,7 +1087,7 @@ async function runOracle(
985
1087
  let wasAborted = false;
986
1088
 
987
1089
  const exitCode = await new Promise<number>((resolve) => {
988
- const proc = spawn(invocation.command, invocation.args, {
1090
+ const proc = spawnImpl(invocation.command, invocation.args, {
989
1091
  cwd,
990
1092
  shell: false,
991
1093
  stdio: ["ignore", "pipe", "pipe"],
@@ -1023,18 +1125,16 @@ async function runOracle(
1023
1125
  const errorMessageField = event.message.errorMessage;
1024
1126
  lastErrorMessage = typeof errorMessageField === "string" ? errorMessageField : undefined;
1025
1127
 
1026
- const messageUsage = event.message.usage;
1027
- if (messageUsage) {
1028
- usage.turns += 1;
1029
- usage.input += messageUsage.input || 0;
1030
- usage.output += messageUsage.output || 0;
1031
- usage.cacheRead += messageUsage.cacheRead || 0;
1032
- usage.cacheWrite += messageUsage.cacheWrite || 0;
1033
- usage.cost += messageUsage.cost?.total || 0;
1034
- usage.contextTokens = messageUsage.totalTokens || usage.contextTokens;
1035
- }
1036
-
1128
+ addUsageStats(usage, event.message.usage as Usage | undefined, {
1129
+ countTurn: true,
1130
+ updateContextTokens: true,
1131
+ });
1037
1132
  emit();
1133
+ return;
1134
+ }
1135
+
1136
+ if (event.type === "compaction_end") {
1137
+ addUsageStats(usage, event.result?.usage as Usage | undefined);
1038
1138
  }
1039
1139
  };
1040
1140
 
@@ -1050,26 +1150,53 @@ async function runOracle(
1050
1150
  emit();
1051
1151
  });
1052
1152
 
1153
+ let closed = false;
1154
+ let killTimer: ReturnType<typeof setTimeout> | undefined;
1155
+ let abortListener: (() => void) | undefined;
1156
+
1157
+ const childHasExited = () => proc.exitCode !== null || proc.signalCode !== null;
1158
+ const clearEscalation = () => {
1159
+ if (killTimer) {
1160
+ clearTimeout(killTimer);
1161
+ killTimer = undefined;
1162
+ }
1163
+ };
1164
+ const cleanup = () => {
1165
+ clearEscalation();
1166
+ if (signal && abortListener) {
1167
+ signal.removeEventListener("abort", abortListener);
1168
+ abortListener = undefined;
1169
+ }
1170
+ };
1171
+
1172
+ proc.on("exit", () => {
1173
+ clearEscalation();
1174
+ });
1175
+
1053
1176
  proc.on("close", (code) => {
1177
+ closed = true;
1178
+ cleanup();
1054
1179
  if (buffer.trim()) processLine(buffer);
1055
1180
  resolve(code ?? 0);
1056
1181
  });
1057
1182
 
1058
1183
  proc.on("error", (error) => {
1184
+ cleanup();
1059
1185
  stderr += `${error instanceof Error ? error.message : String(error)}\n`;
1060
1186
  resolve(1);
1061
1187
  });
1062
1188
 
1063
1189
  if (signal) {
1064
- const killProc = () => {
1190
+ abortListener = () => {
1065
1191
  wasAborted = true;
1066
1192
  proc.kill("SIGTERM");
1067
- setTimeout(() => {
1068
- if (!proc.killed) proc.kill("SIGKILL");
1193
+ if (closed || childHasExited()) return;
1194
+ killTimer = setTimeout(() => {
1195
+ if (!closed && !childHasExited()) proc.kill("SIGKILL");
1069
1196
  }, 5000);
1070
1197
  };
1071
- if (signal.aborted) killProc();
1072
- else signal.addEventListener("abort", killProc, { once: true });
1198
+ if (signal.aborted) abortListener();
1199
+ else signal.addEventListener("abort", abortListener, { once: true });
1073
1200
  }
1074
1201
  });
1075
1202
 
@@ -1078,6 +1205,7 @@ async function runOracle(
1078
1205
  details.durationMs = Date.now() - startedAt;
1079
1206
 
1080
1207
  if (wasAborted) {
1208
+ if (details.exitCode === 0) details.exitCode = 1;
1081
1209
  return { ok: false, error: "Oracle was aborted.", details };
1082
1210
  }
1083
1211
 
@@ -1087,6 +1215,7 @@ async function runOracle(
1087
1215
  // so callers can distinguish transient provider errors from a genuinely
1088
1216
  // empty response.
1089
1217
  if (lastStopReason === "error" || lastStopReason === "aborted") {
1218
+ if (details.exitCode === 0) details.exitCode = 1;
1090
1219
  return {
1091
1220
  ok: false,
1092
1221
  error: formatOracleModelError(lastStopReason, lastErrorMessage),
@@ -1103,6 +1232,7 @@ async function runOracle(
1103
1232
  }
1104
1233
 
1105
1234
  if (!finalOutput.trim()) {
1235
+ if (details.exitCode === 0) details.exitCode = 1;
1106
1236
  return {
1107
1237
  ok: false,
1108
1238
  error: details.stderr || "Oracle finished without returning any text.",
@@ -1123,14 +1253,26 @@ function renderCollapsedText(text: string, lineLimit = COLLAPSED_LINE_LIMIT): st
1123
1253
  return [...lines.slice(0, lineLimit), `... (${lines.length - lineLimit} more lines)`].join("\n");
1124
1254
  }
1125
1255
 
1256
+ type OracleExtensionDeps = {
1257
+ spawnImpl?: typeof spawn;
1258
+ };
1259
+
1126
1260
  export const __test__ = {
1261
+ createOracleExtension,
1127
1262
  findAvailableModel,
1263
+ isModelAvailabilityError,
1128
1264
  parseModelPreference,
1265
+ runOracle,
1129
1266
  resolveThinkingLevel,
1130
1267
  selectOracleModel,
1131
1268
  };
1132
1269
 
1133
1270
  export default function oracleExtension(pi: ExtensionAPI) {
1271
+ createOracleExtension(pi);
1272
+ }
1273
+
1274
+ function createOracleExtension(pi: ExtensionAPI, deps: OracleExtensionDeps = {}) {
1275
+ const spawnImpl = deps.spawnImpl ?? spawn;
1134
1276
  const activeRuns = new Map<string, OracleUiRun>();
1135
1277
  let preferences: OraclePreferences = {};
1136
1278
 
@@ -1276,6 +1418,13 @@ export default function oracleExtension(pi: ExtensionAPI) {
1276
1418
  },
1277
1419
  });
1278
1420
 
1421
+ pi.on("tool_result", async (event) => {
1422
+ if (event.toolName !== "oracle") return undefined;
1423
+ const details = event.details as OracleDetails | undefined;
1424
+ if ((details?.exitCode ?? 0) === 0) return undefined;
1425
+ return { isError: true };
1426
+ });
1427
+
1279
1428
  pi.registerTool({
1280
1429
  name: "oracle",
1281
1430
  label: "Oracle",
@@ -1284,10 +1433,11 @@ export default function oracleExtension(pi: ExtensionAPI) {
1284
1433
  promptSnippet:
1285
1434
  "Consult a read-only oracle that auto-selects the strongest reasoning model on the current provider/subscription.",
1286
1435
  promptGuidelines: [
1287
- "Use this tool sparingly when you want a second opinion, deeper analysis, code review, debugging help, or a higher-reasoning pass.",
1288
- "Do not use it for routine low-value work; it is slower than the main agent.",
1289
- "The oracle is read-only by default. Set includeBash only when shell-based inspection is genuinely useful.",
1290
- "The oracle requests xhigh by default for reasoning models; defaults and explicit thinkingLevel overrides are clamped to the effective model-supported level when the model is matched.",
1436
+ "Use oracle sparingly when you want a second opinion, deeper analysis, code review, debugging help, or a higher-reasoning pass.",
1437
+ "Do not use oracle for routine low-value work; oracle is slower than the main agent.",
1438
+ "The oracle tool is read-only by default and only exposes read, grep, find, and ls unless oracle includeBash is enabled.",
1439
+ "Set oracle includeBash only when the extra bash inspection tool is genuinely useful; keep oracle relying on read, grep, find, and ls otherwise.",
1440
+ "The oracle tool requests xhigh by default for reasoning models; oracle defaults and explicit thinkingLevel overrides are clamped to the effective model-supported level when the model is matched.",
1291
1441
  ],
1292
1442
  parameters: OracleParams,
1293
1443
 
@@ -1312,6 +1462,14 @@ export default function oracleExtension(pi: ExtensionAPI) {
1312
1462
  try {
1313
1463
  const attempts: OracleSelection[] = [];
1314
1464
  const seen = new Set<string>();
1465
+ const aggregateUsage = createEmptyUsage();
1466
+ const finalizeResult = (
1467
+ result: { content: Array<{ type: "text"; text: string }>; details: OracleDetails },
1468
+ ): AgentToolResult<OracleDetails> => ({
1469
+ ...result,
1470
+ details: { ...result.details, usage: cloneUsageStats(aggregateUsage) },
1471
+ usage: usageStatsToToolUsage(aggregateUsage),
1472
+ });
1315
1473
  const pushAttempt = (candidate: OracleSelection | undefined) => {
1316
1474
  if (!candidate) return;
1317
1475
  const key = `${candidate.provider}/${candidate.modelId}`.toLowerCase();
@@ -1353,7 +1511,7 @@ export default function oracleExtension(pi: ExtensionAPI) {
1353
1511
  } else {
1354
1512
  const selectionResult = await selectOracleModel(ctx, configuredThinkingLevel);
1355
1513
  if (!selectionResult.ok) {
1356
- return {
1514
+ return finalizeResult({
1357
1515
  content: [{ type: "text", text: selectionResult.error }],
1358
1516
  details: {
1359
1517
  modelRef: "",
@@ -1370,7 +1528,7 @@ export default function oracleExtension(pi: ExtensionAPI) {
1370
1528
  durationMs: 0,
1371
1529
  cwd: params.cwd ?? ctx.cwd,
1372
1530
  },
1373
- };
1531
+ });
1374
1532
  }
1375
1533
  for (const candidate of selectionResult.ordered) pushAttempt(candidate);
1376
1534
  }
@@ -1394,25 +1552,26 @@ export default function oracleExtension(pi: ExtensionAPI) {
1394
1552
  uiRun.selection = attempt;
1395
1553
  updateOracleUi(ctx, activeRuns);
1396
1554
 
1397
- const result = await runOracle(attempt, { ...params, task }, signal, handleUpdate, ctx.cwd);
1555
+ const result = await runOracle(attempt, { ...params, task }, signal, handleUpdate, ctx.cwd, spawnImpl);
1556
+ mergeUsageStats(aggregateUsage, result.details.usage);
1398
1557
  if (result.ok) {
1399
- return {
1558
+ return finalizeResult({
1400
1559
  content: [{ type: "text", text: result.output }],
1401
1560
  details: result.details,
1402
- };
1561
+ });
1403
1562
  }
1404
1563
 
1405
1564
  lastErrorResult = result;
1406
1565
  const canFallBack = index < attempts.length - 1 && isModelAvailabilityError(result.error);
1407
1566
  if (!canFallBack) {
1408
- return {
1567
+ return finalizeResult({
1409
1568
  content: [{ type: "text", text: result.error }],
1410
1569
  details: result.details,
1411
- };
1570
+ });
1412
1571
  }
1413
1572
  }
1414
1573
 
1415
- return {
1574
+ return finalizeResult({
1416
1575
  content: [
1417
1576
  { type: "text", text: lastErrorResult?.error ?? "Oracle could not select an available model." },
1418
1577
  ],
@@ -1432,7 +1591,7 @@ export default function oracleExtension(pi: ExtensionAPI) {
1432
1591
  durationMs: 0,
1433
1592
  cwd: params.cwd ?? ctx.cwd,
1434
1593
  },
1435
- };
1594
+ });
1436
1595
  } finally {
1437
1596
  activeRuns.delete(toolCallId);
1438
1597
  updateOracleUi(ctx, activeRuns);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-oracle",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "description": "An Amp-style oracle extension for pi that consults the strongest reasoning model on your current provider.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -34,5 +34,9 @@
34
34
  "@earendil-works/pi-coding-agent": "*",
35
35
  "@earendil-works/pi-tui": "*",
36
36
  "typebox": "*"
37
+ },
38
+ "type": "module",
39
+ "engines": {
40
+ "node": ">=22.19.0"
37
41
  }
38
42
  }
@@ -1 +1 @@
1
- 0.80.6
1
+ 0.81.1
@@ -2,7 +2,7 @@
2
2
 
3
3
  A small pi extension that prompts for confirmation before running potentially dangerous bash commands or writing to protected paths.
4
4
 
5
- This is adapted from the original `permission-gate.ts` example in [`earendil-works/pi-mono`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/examples/extensions/permission-gate.ts) and kept basically the same.
5
+ This is adapted from the original `permission-gate.ts` example in [`earendil-works/pi`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/examples/extensions/permission-gate.ts) and kept basically the same.
6
6
 
7
7
  ## What it checks
8
8