@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
  }
@@ -247,7 +254,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
247
254
  "MiniMaxAI/MiniMax-M2.5",
248
255
  "Qwen/Qwen3-Coder-Next",
249
256
  ],
250
- "kimi-coding": ["k2p7", "kimi-k2-thinking", "kimi-for-coding"],
257
+ "kimi-coding": ["k3", "kimi-for-coding", "kimi-for-coding-highspeed"],
251
258
  minimax: ["MiniMax-M3", "MiniMax-M2.7-highspeed", "MiniMax-M2.7"],
252
259
  "minimax-cn": ["MiniMax-M3", "MiniMax-M2.7-highspeed", "MiniMax-M2.7"],
253
260
  mistral: [
@@ -375,17 +382,35 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
375
382
  "z-ai/glm-5.2",
376
383
  "z-ai/glm-5.1",
377
384
  ],
385
+ "qwen-token-plan": [
386
+ "qwen3.8-max-preview",
387
+ "qwen3.7-max",
388
+ "qwen3.7-plus",
389
+ "qwen3.6-plus",
390
+ "deepseek-v4-pro",
391
+ "glm-5.2",
392
+ "kimi-k2.7-code",
393
+ "minimax-m2.5",
394
+ ],
395
+ "qwen-token-plan-cn": [
396
+ "qwen3.8-max-preview",
397
+ "qwen3.7-max",
398
+ "qwen3.7-plus",
399
+ "qwen3.6-plus",
400
+ "deepseek-v4-pro",
401
+ "glm-5.2",
402
+ "kimi-k2.7-code",
403
+ "minimax-m2.5",
404
+ ],
378
405
  together: [
379
406
  "deepseek-ai/DeepSeek-V4-Pro",
380
407
  "zai-org/GLM-5.2",
381
- "zai-org/GLM-5.1",
382
408
  "moonshotai/Kimi-K2.7-Code",
383
409
  "moonshotai/Kimi-K2.6",
384
410
  "Qwen/Qwen3.7-Max",
385
411
  "Qwen/Qwen3.6-Plus",
386
412
  "MiniMaxAI/MiniMax-M3",
387
413
  "MiniMaxAI/MiniMax-M2.7",
388
- "Qwen/Qwen3.5-397B-A17B",
389
414
  "openai/gpt-oss-120b",
390
415
  "openai/gpt-oss-20b",
391
416
  "nvidia/nemotron-3-ultra-550b-a55b",
@@ -433,13 +458,8 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
433
458
  ],
434
459
  xai: [
435
460
  "grok-4.5",
436
- "grok-4.20-0309-reasoning",
437
- "grok-4.20-0309-non-reasoning",
438
461
  "grok-4.3",
439
462
  "grok-build-0.1",
440
- "grok-code-fast-1",
441
- "grok-3-fast",
442
- "grok-3",
443
463
  ],
444
464
  xiaomi: ["mimo-v2.5-pro", "mimo-v2.5", "mimo-v2-pro", "mimo-v2-omni", "mimo-v2-flash"],
445
465
  "xiaomi-token-plan-ams": ["mimo-v2.5-pro", "mimo-v2.5", "mimo-v2-pro"],
@@ -450,7 +470,8 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
450
470
  "nvidia/nemotron-3-super-120b-a12b",
451
471
  "moonshotai/kimi-k2.6",
452
472
  "z-ai/glm-5.2",
453
- "qwen/qwen3.5-122b-a10b",
473
+ "minimaxai/minimax-m3",
474
+ "mistralai/mistral-large-3-675b-instruct-2512",
454
475
  "openai/gpt-oss-120b",
455
476
  "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning",
456
477
  "nvidia/nemotron-3-nano-30b-a3b",
@@ -523,12 +544,91 @@ function createEmptyUsage(): UsageStats {
523
544
  output: 0,
524
545
  cacheRead: 0,
525
546
  cacheWrite: 0,
547
+ totalTokens: 0,
526
548
  cost: 0,
549
+ costInput: 0,
550
+ costOutput: 0,
551
+ costCacheRead: 0,
552
+ costCacheWrite: 0,
527
553
  turns: 0,
528
554
  contextTokens: 0,
529
555
  };
530
556
  }
531
557
 
558
+ function cloneUsageStats(stats: UsageStats): UsageStats {
559
+ return { ...stats };
560
+ }
561
+
562
+ function mergeUsageStats(target: UsageStats, source: UsageStats | undefined): UsageStats {
563
+ if (!source) return target;
564
+ target.input += source.input;
565
+ target.output += source.output;
566
+ target.cacheRead += source.cacheRead;
567
+ target.cacheWrite += source.cacheWrite;
568
+ if (source.cacheWrite1h !== undefined) {
569
+ target.cacheWrite1h = (target.cacheWrite1h ?? 0) + source.cacheWrite1h;
570
+ }
571
+ if (source.reasoning !== undefined) {
572
+ target.reasoning = (target.reasoning ?? 0) + source.reasoning;
573
+ }
574
+ target.totalTokens += source.totalTokens;
575
+ target.cost += source.cost;
576
+ target.costInput += source.costInput;
577
+ target.costOutput += source.costOutput;
578
+ target.costCacheRead += source.costCacheRead;
579
+ target.costCacheWrite += source.costCacheWrite;
580
+ target.turns += source.turns;
581
+ target.contextTokens = source.contextTokens || target.contextTokens;
582
+ return target;
583
+ }
584
+
585
+ function usageStatsToToolUsage(stats: UsageStats): Usage {
586
+ return {
587
+ input: stats.input,
588
+ output: stats.output,
589
+ cacheRead: stats.cacheRead,
590
+ cacheWrite: stats.cacheWrite,
591
+ ...(stats.cacheWrite1h !== undefined ? { cacheWrite1h: stats.cacheWrite1h } : {}),
592
+ ...(stats.reasoning !== undefined ? { reasoning: stats.reasoning } : {}),
593
+ totalTokens: stats.totalTokens,
594
+ cost: {
595
+ input: stats.costInput,
596
+ output: stats.costOutput,
597
+ cacheRead: stats.costCacheRead,
598
+ cacheWrite: stats.costCacheWrite,
599
+ total: stats.cost,
600
+ },
601
+ };
602
+ }
603
+
604
+ function addUsageStats(
605
+ target: UsageStats,
606
+ source: Usage | undefined,
607
+ options: { countTurn?: boolean; updateContextTokens?: boolean } = {},
608
+ ): void {
609
+ if (!source) return;
610
+ if (options.countTurn) target.turns += 1;
611
+ target.input += source.input || 0;
612
+ target.output += source.output || 0;
613
+ target.cacheRead += source.cacheRead || 0;
614
+ target.cacheWrite += source.cacheWrite || 0;
615
+ if (source.cacheWrite1h !== undefined) {
616
+ target.cacheWrite1h = (target.cacheWrite1h ?? 0) + source.cacheWrite1h;
617
+ }
618
+ if (source.reasoning !== undefined) {
619
+ target.reasoning = (target.reasoning ?? 0) + source.reasoning;
620
+ }
621
+ target.totalTokens += source.totalTokens || 0;
622
+ target.cost += source.cost?.total || 0;
623
+ target.costInput += source.cost?.input || 0;
624
+ target.costOutput += source.cost?.output || 0;
625
+ target.costCacheRead += source.cost?.cacheRead || 0;
626
+ target.costCacheWrite += source.cost?.cacheWrite || 0;
627
+ if (options.updateContextTokens) {
628
+ target.contextTokens = source.totalTokens || target.contextTokens;
629
+ }
630
+ }
631
+
532
632
  function getContrarianConfigPath(): string {
533
633
  return path.join(getAgentDir(), "extensions", CONTRARIAN_CONFIG_FILE);
534
634
  }
@@ -617,6 +717,7 @@ function formatUsage(stats: UsageStats): string {
617
717
  if (stats.output) parts.push(`↓${formatTokens(stats.output)}`);
618
718
  if (stats.cacheRead) parts.push(`R${formatTokens(stats.cacheRead)}`);
619
719
  if (stats.cacheWrite) parts.push(`W${formatTokens(stats.cacheWrite)}`);
720
+ if (stats.reasoning) parts.push(`T${formatTokens(stats.reasoning)}`);
620
721
  if (stats.cost) parts.push(`$${stats.cost.toFixed(4)}`);
621
722
  if (stats.contextTokens) parts.push(`ctx:${formatTokens(stats.contextTokens)}`);
622
723
  return parts.join(" ");
@@ -1020,6 +1121,7 @@ async function runContrarian(
1020
1121
  signal: AbortSignal | undefined,
1021
1122
  onUpdate: ((result: { content: Array<{ type: "text"; text: string }>; details: ContrarianDetails }) => void) | undefined,
1022
1123
  defaultCwd: string,
1124
+ spawnImpl: typeof spawn = spawn,
1023
1125
  ): Promise<{ ok: true; output: string; details: ContrarianDetails } | { ok: false; error: string; details: ContrarianDetails }> {
1024
1126
  const cwd = params.cwd ?? defaultCwd;
1025
1127
  const includeBash = params.includeBash ?? false;
@@ -1071,7 +1173,7 @@ async function runContrarian(
1071
1173
  let wasAborted = false;
1072
1174
 
1073
1175
  const exitCode = await new Promise<number>((resolve) => {
1074
- const proc = spawn(invocation.command, invocation.args, {
1176
+ const proc = spawnImpl(invocation.command, invocation.args, {
1075
1177
  cwd,
1076
1178
  shell: false,
1077
1179
  stdio: ["ignore", "pipe", "pipe"],
@@ -1109,18 +1211,16 @@ async function runContrarian(
1109
1211
  const errorMessageField = event.message.errorMessage;
1110
1212
  lastErrorMessage = typeof errorMessageField === "string" ? errorMessageField : undefined;
1111
1213
 
1112
- const messageUsage = event.message.usage;
1113
- if (messageUsage) {
1114
- usage.turns += 1;
1115
- usage.input += messageUsage.input || 0;
1116
- usage.output += messageUsage.output || 0;
1117
- usage.cacheRead += messageUsage.cacheRead || 0;
1118
- usage.cacheWrite += messageUsage.cacheWrite || 0;
1119
- usage.cost += messageUsage.cost?.total || 0;
1120
- usage.contextTokens = messageUsage.totalTokens || usage.contextTokens;
1121
- }
1122
-
1214
+ addUsageStats(usage, event.message.usage as Usage | undefined, {
1215
+ countTurn: true,
1216
+ updateContextTokens: true,
1217
+ });
1123
1218
  emit();
1219
+ return;
1220
+ }
1221
+
1222
+ if (event.type === "compaction_end") {
1223
+ addUsageStats(usage, event.result?.usage as Usage | undefined);
1124
1224
  }
1125
1225
  };
1126
1226
 
@@ -1136,26 +1236,53 @@ async function runContrarian(
1136
1236
  emit();
1137
1237
  });
1138
1238
 
1239
+ let closed = false;
1240
+ let killTimer: ReturnType<typeof setTimeout> | undefined;
1241
+ let abortListener: (() => void) | undefined;
1242
+
1243
+ const childHasExited = () => proc.exitCode !== null || proc.signalCode !== null;
1244
+ const clearEscalation = () => {
1245
+ if (killTimer) {
1246
+ clearTimeout(killTimer);
1247
+ killTimer = undefined;
1248
+ }
1249
+ };
1250
+ const cleanup = () => {
1251
+ clearEscalation();
1252
+ if (signal && abortListener) {
1253
+ signal.removeEventListener("abort", abortListener);
1254
+ abortListener = undefined;
1255
+ }
1256
+ };
1257
+
1258
+ proc.on("exit", () => {
1259
+ clearEscalation();
1260
+ });
1261
+
1139
1262
  proc.on("close", (code) => {
1263
+ closed = true;
1264
+ cleanup();
1140
1265
  if (buffer.trim()) processLine(buffer);
1141
1266
  resolve(code ?? 0);
1142
1267
  });
1143
1268
 
1144
1269
  proc.on("error", (error) => {
1270
+ cleanup();
1145
1271
  stderr += `${error instanceof Error ? error.message : String(error)}\n`;
1146
1272
  resolve(1);
1147
1273
  });
1148
1274
 
1149
1275
  if (signal) {
1150
- const killProc = () => {
1276
+ abortListener = () => {
1151
1277
  wasAborted = true;
1152
1278
  proc.kill("SIGTERM");
1153
- setTimeout(() => {
1154
- if (!proc.killed) proc.kill("SIGKILL");
1279
+ if (closed || childHasExited()) return;
1280
+ killTimer = setTimeout(() => {
1281
+ if (!closed && !childHasExited()) proc.kill("SIGKILL");
1155
1282
  }, 5000);
1156
1283
  };
1157
- if (signal.aborted) killProc();
1158
- else signal.addEventListener("abort", killProc, { once: true });
1284
+ if (signal.aborted) abortListener();
1285
+ else signal.addEventListener("abort", abortListener, { once: true });
1159
1286
  }
1160
1287
  });
1161
1288
 
@@ -1164,6 +1291,7 @@ async function runContrarian(
1164
1291
  details.durationMs = Date.now() - startedAt;
1165
1292
 
1166
1293
  if (wasAborted) {
1294
+ if (details.exitCode === 0) details.exitCode = 1;
1167
1295
  return { ok: false, error: "Contrarian was aborted.", details };
1168
1296
  }
1169
1297
 
@@ -1173,6 +1301,7 @@ async function runContrarian(
1173
1301
  // so callers can distinguish transient provider errors from a genuinely
1174
1302
  // empty response.
1175
1303
  if (lastStopReason === "error" || lastStopReason === "aborted") {
1304
+ if (details.exitCode === 0) details.exitCode = 1;
1176
1305
  return {
1177
1306
  ok: false,
1178
1307
  error: formatContrarianModelError(lastStopReason, lastErrorMessage),
@@ -1189,6 +1318,7 @@ async function runContrarian(
1189
1318
  }
1190
1319
 
1191
1320
  if (!finalOutput.trim()) {
1321
+ if (details.exitCode === 0) details.exitCode = 1;
1192
1322
  return {
1193
1323
  ok: false,
1194
1324
  error: details.stderr || "Contrarian finished without returning any text.",
@@ -1209,14 +1339,26 @@ function renderCollapsedText(text: string, lineLimit = COLLAPSED_LINE_LIMIT): st
1209
1339
  return [...lines.slice(0, lineLimit), `... (${lines.length - lineLimit} more lines)`].join("\n");
1210
1340
  }
1211
1341
 
1342
+ type ContrarianExtensionDeps = {
1343
+ spawnImpl?: typeof spawn;
1344
+ };
1345
+
1212
1346
  export const __test__ = {
1347
+ createContrarianExtension,
1213
1348
  findAvailableModel,
1349
+ isModelAvailabilityError,
1214
1350
  parseModelPreference,
1351
+ runContrarian,
1215
1352
  resolveThinkingLevel,
1216
1353
  selectContrarianModel,
1217
1354
  };
1218
1355
 
1219
1356
  export default function contrarianExtension(pi: ExtensionAPI) {
1357
+ createContrarianExtension(pi);
1358
+ }
1359
+
1360
+ function createContrarianExtension(pi: ExtensionAPI, deps: ContrarianExtensionDeps = {}) {
1361
+ const spawnImpl = deps.spawnImpl ?? spawn;
1220
1362
  const activeRuns = new Map<string, ContrarianUiRun>();
1221
1363
  let preferences: ContrarianPreferences = {};
1222
1364
 
@@ -1362,6 +1504,13 @@ export default function contrarianExtension(pi: ExtensionAPI) {
1362
1504
  },
1363
1505
  });
1364
1506
 
1507
+ pi.on("tool_result", async (event) => {
1508
+ if (event.toolName !== "contrarian") return undefined;
1509
+ const details = event.details as ContrarianDetails | undefined;
1510
+ if ((details?.exitCode ?? 0) === 0) return undefined;
1511
+ return { isError: true };
1512
+ });
1513
+
1365
1514
  pi.registerTool({
1366
1515
  name: "contrarian",
1367
1516
  label: "Contrarian",
@@ -1372,9 +1521,10 @@ export default function contrarianExtension(pi: ExtensionAPI) {
1372
1521
  promptGuidelines: [
1373
1522
  "Use contrarian when you need to stress-test a proposal, implementation plan, design, assumption, bug hypothesis, review conclusion, or product direction.",
1374
1523
  "Ask contrarian to steelman the strongest opposing case and separate confirmed objections, plausible concerns, and unresolved unknowns.",
1375
- "Do not use it for routine low-value work; it is slower than the main agent.",
1376
- "The contrarian is read-only by default. Set includeBash only when shell-based inspection is genuinely useful.",
1377
- "The contrarian requests high by default for reasoning models; defaults and explicit thinkingLevel overrides are clamped to the effective model-supported level when the model is matched.",
1524
+ "Do not use contrarian for routine low-value work; contrarian is slower than the main agent.",
1525
+ "The contrarian tool is read-only by default and only exposes read, grep, find, and ls unless contrarian includeBash is enabled.",
1526
+ "Set contrarian includeBash only when the extra bash inspection tool is genuinely useful; keep contrarian relying on read, grep, find, and ls otherwise.",
1527
+ "The contrarian tool requests high by default for reasoning models; contrarian defaults and explicit thinkingLevel overrides are clamped to the effective model-supported level when the model is matched.",
1378
1528
  ],
1379
1529
  parameters: ContrarianParams,
1380
1530
 
@@ -1399,6 +1549,14 @@ export default function contrarianExtension(pi: ExtensionAPI) {
1399
1549
  try {
1400
1550
  const attempts: ContrarianSelection[] = [];
1401
1551
  const seen = new Set<string>();
1552
+ const aggregateUsage = createEmptyUsage();
1553
+ const finalizeResult = (
1554
+ result: { content: Array<{ type: "text"; text: string }>; details: ContrarianDetails },
1555
+ ): AgentToolResult<ContrarianDetails> => ({
1556
+ ...result,
1557
+ details: { ...result.details, usage: cloneUsageStats(aggregateUsage) },
1558
+ usage: usageStatsToToolUsage(aggregateUsage),
1559
+ });
1402
1560
  const pushAttempt = (candidate: ContrarianSelection | undefined) => {
1403
1561
  if (!candidate) return;
1404
1562
  const key = `${candidate.provider}/${candidate.modelId}`.toLowerCase();
@@ -1440,7 +1598,7 @@ export default function contrarianExtension(pi: ExtensionAPI) {
1440
1598
  } else {
1441
1599
  const selectionResult = await selectContrarianModel(ctx, configuredThinkingLevel);
1442
1600
  if (!selectionResult.ok) {
1443
- return {
1601
+ return finalizeResult({
1444
1602
  content: [{ type: "text", text: selectionResult.error }],
1445
1603
  details: {
1446
1604
  modelRef: "",
@@ -1457,7 +1615,7 @@ export default function contrarianExtension(pi: ExtensionAPI) {
1457
1615
  durationMs: 0,
1458
1616
  cwd: params.cwd ?? ctx.cwd,
1459
1617
  },
1460
- };
1618
+ });
1461
1619
  }
1462
1620
  for (const candidate of selectionResult.ordered) pushAttempt(candidate);
1463
1621
  }
@@ -1481,25 +1639,26 @@ export default function contrarianExtension(pi: ExtensionAPI) {
1481
1639
  uiRun.selection = attempt;
1482
1640
  updateContrarianUi(ctx, activeRuns);
1483
1641
 
1484
- const result = await runContrarian(attempt, { ...params, task }, signal, handleUpdate, ctx.cwd);
1642
+ const result = await runContrarian(attempt, { ...params, task }, signal, handleUpdate, ctx.cwd, spawnImpl);
1643
+ mergeUsageStats(aggregateUsage, result.details.usage);
1485
1644
  if (result.ok) {
1486
- return {
1645
+ return finalizeResult({
1487
1646
  content: [{ type: "text", text: result.output }],
1488
1647
  details: result.details,
1489
- };
1648
+ });
1490
1649
  }
1491
1650
 
1492
1651
  lastErrorResult = result;
1493
1652
  const canFallBack = index < attempts.length - 1 && isModelAvailabilityError(result.error);
1494
1653
  if (!canFallBack) {
1495
- return {
1654
+ return finalizeResult({
1496
1655
  content: [{ type: "text", text: result.error }],
1497
1656
  details: result.details,
1498
- };
1657
+ });
1499
1658
  }
1500
1659
  }
1501
1660
 
1502
- return {
1661
+ return finalizeResult({
1503
1662
  content: [
1504
1663
  { type: "text", text: lastErrorResult?.error ?? "Contrarian could not select an available model." },
1505
1664
  ],
@@ -1519,7 +1678,7 @@ export default function contrarianExtension(pi: ExtensionAPI) {
1519
1678
  durationMs: 0,
1520
1679
  cwd: params.cwd ?? ctx.cwd,
1521
1680
  },
1522
- };
1681
+ });
1523
1682
  } finally {
1524
1683
  activeRuns.delete(toolCallId);
1525
1684
  updateContrarianUi(ctx, activeRuns);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-contrarian",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "A read-only contrarian subagent extension for pi that stress-tests plans, designs, assumptions, and conclusions.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -33,5 +33,9 @@
33
33
  "@earendil-works/pi-coding-agent": "*",
34
34
  "@earendil-works/pi-tui": "*",
35
35
  "typebox": "*"
36
+ },
37
+ "type": "module",
38
+ "engines": {
39
+ "node": ">=22.19.0"
36
40
  }
37
41
  }
@@ -1 +1 @@
1
- 0.80.6
1
+ 0.81.1
@@ -2,7 +2,7 @@
2
2
 
3
3
  A small pi extension that prompts before session changes when the current git repo has uncommitted changes.
4
4
 
5
- This is copied from the original `dirty-repo-guard.ts` example in [`earendil-works/pi-mono`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/examples/extensions/dirty-repo-guard.ts) and kept basically the same.
5
+ This is copied from the original `dirty-repo-guard.ts` example in [`earendil-works/pi`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/examples/extensions/dirty-repo-guard.ts) and kept basically the same.
6
6
 
7
7
  ## What it checks
8
8
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-dirty-repo-guard",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "A pi extension that prompts before session changes when the current git repo has uncommitted changes.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -29,5 +29,9 @@
29
29
  },
30
30
  "peerDependencies": {
31
31
  "@earendil-works/pi-coding-agent": "*"
32
+ },
33
+ "type": "module",
34
+ "engines": {
35
+ "node": ">=22.19.0"
32
36
  }
33
37
  }
@@ -1 +1 @@
1
- 0.80.6
1
+ 0.81.1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-dynamic-context-pruning",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "description": "A pi extension that dynamically prunes stale context to keep sessions lean.",
6
6
  "keywords": [
@@ -30,5 +30,8 @@
30
30
  },
31
31
  "peerDependencies": {
32
32
  "@earendil-works/pi-coding-agent": "*"
33
+ },
34
+ "engines": {
35
+ "node": ">=22.19.0"
33
36
  }
34
37
  }
@@ -1 +1 @@
1
- 0.80.6
1
+ 0.81.1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-git-footer",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "A TLH-style git status add-on for pi's built-in footer.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -31,5 +31,9 @@
31
31
  "peerDependencies": {
32
32
  "@earendil-works/pi-coding-agent": "*",
33
33
  "@earendil-works/pi-tui": "*"
34
+ },
35
+ "type": "module",
36
+ "engines": {
37
+ "node": ">=22.19.0"
34
38
  }
35
39
  }
@@ -1 +1 @@
1
- 0.80.6
1
+ 0.81.1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-gnosis",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "A pi extension that exposes the gnosis repo-local knowledge base CLI as an agent tool.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -33,5 +33,9 @@
33
33
  "@earendil-works/pi-ai": "*",
34
34
  "@earendil-works/pi-coding-agent": "*",
35
35
  "typebox": "*"
36
+ },
37
+ "type": "module",
38
+ "engines": {
39
+ "node": ">=22.19.0"
36
40
  }
37
41
  }
@@ -1 +1 @@
1
- 0.80.6
1
+ 0.81.1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-illustrations-to-explain-things",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "A pi skill for generating clean, absurd Xiaohei-style article illustrations, shot lists, and visual metaphors.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -27,5 +27,8 @@
27
27
  "skills": [
28
28
  "skills"
29
29
  ]
30
+ },
31
+ "engines": {
32
+ "node": ">=22.19.0"
30
33
  }
31
34
  }
@@ -1 +1 @@
1
- 0.80.6
1
+ 0.81.1
@@ -2,7 +2,7 @@
2
2
 
3
3
  A pi extension that expands inline bash commands in user prompts before they are sent to the agent.
4
4
 
5
- This started from the original `inline-bash.ts` example in [`earendil-works/pi-mono`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/examples/extensions/inline-bash.ts), with small packaging and safety tweaks.
5
+ This started from the original `inline-bash.ts` example in [`earendil-works/pi`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/examples/extensions/inline-bash.ts), with small packaging and safety tweaks.
6
6
 
7
7
  ## Usage
8
8
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-inline-bash",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "A pi extension that expands inline bash commands in user prompts.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -30,5 +30,9 @@
30
30
  },
31
31
  "peerDependencies": {
32
32
  "@earendil-works/pi-coding-agent": "*"
33
+ },
34
+ "type": "module",
35
+ "engines": {
36
+ "node": ">=22.19.0"
33
37
  }
34
38
  }
@@ -1 +1 @@
1
- 0.80.6
1
+ 0.81.1