@hyperspaceng/neural-coding-agent 0.63.0 → 0.64.1

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 (124) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +3 -3
  3. package/dist/core/agent-session.d.ts +14 -6
  4. package/dist/core/agent-session.d.ts.map +1 -1
  5. package/dist/core/agent-session.js +111 -54
  6. package/dist/core/agent-session.js.map +1 -1
  7. package/dist/core/auth-storage.d.ts +3 -1
  8. package/dist/core/auth-storage.d.ts.map +1 -1
  9. package/dist/core/auth-storage.js +5 -2
  10. package/dist/core/auth-storage.js.map +1 -1
  11. package/dist/core/compaction/branch-summarization.d.ts +2 -0
  12. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  13. package/dist/core/compaction/branch-summarization.js +2 -2
  14. package/dist/core/compaction/branch-summarization.js.map +1 -1
  15. package/dist/core/compaction/compaction.d.ts +3 -3
  16. package/dist/core/compaction/compaction.d.ts.map +1 -1
  17. package/dist/core/compaction/compaction.js +27 -26
  18. package/dist/core/compaction/compaction.js.map +1 -1
  19. package/dist/core/export-html/index.d.ts.map +1 -1
  20. package/dist/core/export-html/index.js +5 -4
  21. package/dist/core/export-html/index.js.map +1 -1
  22. package/dist/core/extensions/runner.d.ts +1 -0
  23. package/dist/core/extensions/runner.d.ts.map +1 -1
  24. package/dist/core/extensions/runner.js +4 -0
  25. package/dist/core/extensions/runner.js.map +1 -1
  26. package/dist/core/extensions/types.d.ts +14 -1
  27. package/dist/core/extensions/types.d.ts.map +1 -1
  28. package/dist/core/extensions/types.js.map +1 -1
  29. package/dist/core/model-registry.d.ts +21 -3
  30. package/dist/core/model-registry.d.ts.map +1 -1
  31. package/dist/core/model-registry.js +90 -70
  32. package/dist/core/model-registry.js.map +1 -1
  33. package/dist/core/model-resolver.d.ts.map +1 -1
  34. package/dist/core/model-resolver.js +4 -4
  35. package/dist/core/model-resolver.js.map +1 -1
  36. package/dist/core/package-manager.d.ts.map +1 -1
  37. package/dist/core/package-manager.js +88 -24
  38. package/dist/core/package-manager.js.map +1 -1
  39. package/dist/core/resolve-config-value.d.ts +6 -0
  40. package/dist/core/resolve-config-value.d.ts.map +1 -1
  41. package/dist/core/resolve-config-value.js +37 -5
  42. package/dist/core/resolve-config-value.js.map +1 -1
  43. package/dist/core/sdk.d.ts +2 -2
  44. package/dist/core/sdk.d.ts.map +1 -1
  45. package/dist/core/sdk.js +14 -23
  46. package/dist/core/sdk.js.map +1 -1
  47. package/dist/core/settings-manager.d.ts +2 -0
  48. package/dist/core/settings-manager.d.ts.map +1 -1
  49. package/dist/core/settings-manager.js +3 -0
  50. package/dist/core/settings-manager.js.map +1 -1
  51. package/dist/core/timings.d.ts +1 -0
  52. package/dist/core/timings.d.ts.map +1 -1
  53. package/dist/core/timings.js +6 -0
  54. package/dist/core/timings.js.map +1 -1
  55. package/dist/core/tools/edit-diff.d.ts +23 -1
  56. package/dist/core/tools/edit-diff.d.ts.map +1 -1
  57. package/dist/core/tools/edit-diff.js +150 -57
  58. package/dist/core/tools/edit-diff.js.map +1 -1
  59. package/dist/core/tools/edit.d.ts +13 -11
  60. package/dist/core/tools/edit.d.ts.map +1 -1
  61. package/dist/core/tools/edit.js +55 -77
  62. package/dist/core/tools/edit.js.map +1 -1
  63. package/dist/core/tools/file-mutation-queue.d.ts.map +1 -1
  64. package/dist/core/tools/file-mutation-queue.js +4 -4
  65. package/dist/core/tools/file-mutation-queue.js.map +1 -1
  66. package/dist/core/tools/index.d.ts +9 -6
  67. package/dist/core/tools/index.d.ts.map +1 -1
  68. package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
  69. package/dist/core/tools/tool-definition-wrapper.js +2 -0
  70. package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
  71. package/dist/main.d.ts.map +1 -1
  72. package/dist/main.js +29 -11
  73. package/dist/main.js.map +1 -1
  74. package/dist/modes/interactive/components/assistant-message.d.ts +3 -1
  75. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  76. package/dist/modes/interactive/components/assistant-message.js +14 -3
  77. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  78. package/dist/modes/interactive/components/bash-execution.d.ts +0 -1
  79. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  80. package/dist/modes/interactive/components/bash-execution.js +18 -5
  81. package/dist/modes/interactive/components/bash-execution.js.map +1 -1
  82. package/dist/modes/interactive/components/tool-execution.d.ts +0 -1
  83. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  84. package/dist/modes/interactive/components/tool-execution.js +2 -7
  85. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  86. package/dist/modes/interactive/interactive-mode.d.ts +3 -1
  87. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  88. package/dist/modes/interactive/interactive-mode.js +51 -67
  89. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  90. package/dist/modes/print-mode.d.ts +1 -1
  91. package/dist/modes/print-mode.d.ts.map +1 -1
  92. package/dist/modes/print-mode.js +83 -71
  93. package/dist/modes/print-mode.js.map +1 -1
  94. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  95. package/dist/modes/rpc/rpc-mode.js +3 -0
  96. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  97. package/docs/compaction.md +4 -2
  98. package/docs/development.md +3 -1
  99. package/docs/extensions.md +107 -2
  100. package/docs/json.md +5 -2
  101. package/docs/models.md +6 -0
  102. package/docs/rpc.md +32 -9
  103. package/docs/sdk.md +12 -9
  104. package/docs/settings.md +12 -0
  105. package/docs/skills.md +3 -2
  106. package/examples/extensions/README.md +1 -0
  107. package/examples/extensions/custom-compaction.ts +17 -4
  108. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  109. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  110. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  111. package/examples/extensions/custom-provider-qwen-cli/package.json +1 -1
  112. package/examples/extensions/handoff.ts +5 -2
  113. package/examples/extensions/hidden-thinking-label.ts +57 -0
  114. package/examples/extensions/qna.ts +5 -2
  115. package/examples/extensions/sandbox/index.ts +4 -0
  116. package/examples/extensions/summarize.ts +15 -4
  117. package/examples/extensions/trigger-compact.ts +11 -1
  118. package/examples/extensions/with-deps/package-lock.json +2 -2
  119. package/examples/extensions/with-deps/package.json +1 -1
  120. package/examples/sdk/02-custom-model.ts +1 -1
  121. package/examples/sdk/09-api-keys-and-oauth.ts +3 -3
  122. package/examples/sdk/12-full-control.ts +1 -1
  123. package/examples/sdk/README.md +3 -3
  124. package/package.json +5 -4
@@ -293,10 +293,11 @@ Fired by the `pi` CLI during startup session resolution, before the initial sess
293
293
  This event is:
294
294
  - CLI-only. It is not emitted in SDK mode.
295
295
  - Startup-only. It is not emitted for later interactive `/new` or `/resume` actions.
296
- - Bypassed when `--session-dir` is provided.
296
+ - Lower priority than `--session-dir` and `sessionDir` in `settings.json`.
297
297
  - Special-cased to receive no `ctx` argument.
298
298
 
299
299
  If multiple extensions return `sessionDir`, the last one wins.
300
+ Combined precedence is: `--session-dir` CLI flag, then `sessionDir` in settings, then extension `session_directory` hooks.
300
301
 
301
302
  ```typescript
302
303
  pi.on("session_directory", async (event) => {
@@ -564,17 +565,27 @@ Before `tool_call` runs, pi waits for previously emitted Agent events to finish
564
565
 
565
566
  In the default parallel tool execution mode, sibling tool calls from the same assistant message are preflighted sequentially, then executed concurrently. `tool_call` is not guaranteed to see sibling tool results from that same assistant message in `ctx.sessionManager`.
566
567
 
568
+ `event.input` is mutable. Mutate it in place to patch tool arguments before execution.
569
+
570
+ Behavior guarantees:
571
+ - Mutations to `event.input` affect the actual tool execution
572
+ - Later `tool_call` handlers see mutations made by earlier handlers
573
+ - No re-validation is performed after your mutation
574
+ - Return values from `tool_call` only control blocking via `{ block: true, reason?: string }`
575
+
567
576
  ```typescript
568
577
  import { isToolCallEventType } from "@mariozechner/pi-coding-agent";
569
578
 
570
579
  pi.on("tool_call", async (event, ctx) => {
571
580
  // event.toolName - "bash", "read", "write", "edit", etc.
572
581
  // event.toolCallId
573
- // event.input - tool parameters
582
+ // event.input - tool parameters (mutable)
574
583
 
575
584
  // Built-in tools: no type params needed
576
585
  if (isToolCallEventType("bash", event)) {
577
586
  // event.input is { command: string; timeout?: number }
587
+ event.input.command = `source ~/.profile\n${event.input.command}`;
588
+
578
589
  if (event.input.command.includes("rm -rf")) {
579
590
  return { block: true, reason: "Dangerous command" };
580
591
  }
@@ -618,6 +629,8 @@ Fired after tool execution finishes and before `tool_execution_end` plus the fin
618
629
  - Each handler sees the latest result after previous handler changes
619
630
  - Handlers can return partial patches (`content`, `details`, or `isError`); omitted fields keep their current values
620
631
 
632
+ Use `ctx.signal` for nested async work inside the handler. This lets Esc cancel model calls, `fetch()`, and other abort-aware operations started by the extension.
633
+
621
634
  ```typescript
622
635
  import { isBashToolResult } from "@mariozechner/pi-coding-agent";
623
636
 
@@ -629,6 +642,12 @@ pi.on("tool_result", async (event, ctx) => {
629
642
  // event.details is typed as BashToolDetails
630
643
  }
631
644
 
645
+ const response = await fetch("https://example.com/summarize", {
646
+ method: "POST",
647
+ body: JSON.stringify({ content: event.content }),
648
+ signal: ctx.signal,
649
+ });
650
+
632
651
  // Modify result:
633
652
  return { content: [...], details: {...}, isError: false };
634
653
  });
@@ -748,6 +767,31 @@ ctx.sessionManager.getLeafId() // Current leaf entry ID
748
767
 
749
768
  Access to models and API keys.
750
769
 
770
+ ### ctx.signal
771
+
772
+ The current agent abort signal, or `undefined` when no agent turn is active.
773
+
774
+ Use this for abort-aware nested work started by extension handlers, for example:
775
+ - `fetch(..., { signal: ctx.signal })`
776
+ - model calls that accept `signal`
777
+ - file or process helpers that accept `AbortSignal`
778
+
779
+ `ctx.signal` is typically defined during active turn events such as `tool_call`, `tool_result`, `message_update`, and `turn_end`.
780
+ It is usually `undefined` in idle or non-turn contexts such as session events, extension commands, and shortcuts fired while pi is idle.
781
+
782
+ ```typescript
783
+ pi.on("tool_result", async (event, ctx) => {
784
+ const response = await fetch("https://example.com/api", {
785
+ method: "POST",
786
+ body: JSON.stringify(event),
787
+ signal: ctx.signal,
788
+ });
789
+
790
+ const data = await response.json();
791
+ return { details: data };
792
+ });
793
+ ```
794
+
751
795
  ### ctx.isIdle() / ctx.abort() / ctx.hasPendingMessages()
752
796
 
753
797
  Control flow helpers.
@@ -964,6 +1008,12 @@ pi.registerTool({
964
1008
  action: StringEnum(["list", "add"] as const),
965
1009
  text: Type.Optional(Type.String()),
966
1010
  }),
1011
+ prepareArguments(args) {
1012
+ // Optional compatibility shim. Runs before schema validation.
1013
+ // Return the current schema shape, for example to fold legacy fields
1014
+ // into the modern parameter object.
1015
+ return args;
1016
+ },
967
1017
 
968
1018
  async execute(toolCallId, params, signal, onUpdate, ctx) {
969
1019
  // Stream progress
@@ -1428,6 +1478,14 @@ pi.registerTool({
1428
1478
  action: StringEnum(["list", "add"] as const), // Use StringEnum for Google compatibility
1429
1479
  text: Type.Optional(Type.String()),
1430
1480
  }),
1481
+ prepareArguments(args) {
1482
+ if (!args || typeof args !== "object") return args;
1483
+ const input = args as { action?: string; oldAction?: string };
1484
+ if (typeof input.oldAction === "string" && input.action === undefined) {
1485
+ return { ...input, action: input.oldAction };
1486
+ }
1487
+ return args;
1488
+ },
1431
1489
 
1432
1490
  async execute(toolCallId, params, signal, onUpdate, ctx) {
1433
1491
  // Check for cancellation
@@ -1471,6 +1529,53 @@ async execute(toolCallId, params) {
1471
1529
 
1472
1530
  **Important:** Use `StringEnum` from `@mariozechner/pi-ai` for string enums. `Type.Union`/`Type.Literal` doesn't work with Google's API.
1473
1531
 
1532
+ **Argument preparation:** `prepareArguments(args)` is optional. If defined, it runs before schema validation and before `execute()`. Use it to mimic an older accepted input shape when pi resumes an older session whose stored tool call arguments no longer match the current schema. Return the object you want validated against `parameters`. Keep the public schema strict. Do not add deprecated compatibility fields to `parameters` just to keep old resumed sessions working.
1533
+
1534
+ Example: an older session may contain an `edit` tool call with top-level `oldText` and `newText`, while the current schema only accepts `edits: [{ oldText, newText }]`.
1535
+
1536
+ ```typescript
1537
+ pi.registerTool({
1538
+ name: "edit",
1539
+ label: "Edit",
1540
+ description: "Edit a single file using exact text replacement",
1541
+ parameters: Type.Object({
1542
+ path: Type.String(),
1543
+ edits: Type.Array(
1544
+ Type.Object({
1545
+ oldText: Type.String(),
1546
+ newText: Type.String(),
1547
+ }),
1548
+ ),
1549
+ }),
1550
+ prepareArguments(args) {
1551
+ if (!args || typeof args !== "object") return args;
1552
+
1553
+ const input = args as {
1554
+ path?: string;
1555
+ edits?: Array<{ oldText: string; newText: string }>;
1556
+ oldText?: unknown;
1557
+ newText?: unknown;
1558
+ };
1559
+
1560
+ if (typeof input.oldText !== "string" || typeof input.newText !== "string") {
1561
+ return args;
1562
+ }
1563
+
1564
+ return {
1565
+ ...input,
1566
+ edits: [...(input.edits ?? []), { oldText: input.oldText, newText: input.newText }],
1567
+ };
1568
+ },
1569
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
1570
+ // params now matches the current schema
1571
+ return {
1572
+ content: [{ type: "text", text: `Applying ${params.edits.length} edit block(s)` }],
1573
+ details: {},
1574
+ };
1575
+ },
1576
+ });
1577
+ ```
1578
+
1474
1579
  ### Overriding Built-in Tools
1475
1580
 
1476
1581
  Extensions can override built-in tools (`read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`) by registering a tool with the same name. Interactive mode displays a warning when this happens.
package/docs/json.md CHANGED
@@ -13,12 +13,15 @@ Events are defined in [`AgentSessionEvent`](https://github.com/badlogic/pi-mono/
13
13
  ```typescript
14
14
  type AgentSessionEvent =
15
15
  | AgentEvent
16
- | { type: "auto_compaction_start"; reason: "threshold" | "overflow" }
17
- | { type: "auto_compaction_end"; result: CompactionResult | undefined; aborted: boolean; willRetry: boolean; errorMessage?: string }
16
+ | { type: "queue_update"; steering: readonly string[]; followUp: readonly string[] }
17
+ | { type: "compaction_start"; reason: "manual" | "threshold" | "overflow" }
18
+ | { type: "compaction_end"; reason: "manual" | "threshold" | "overflow"; result: CompactionResult | undefined; aborted: boolean; willRetry: boolean; errorMessage?: string }
18
19
  | { type: "auto_retry_start"; attempt: number; maxAttempts: number; delayMs: number; errorMessage: string }
19
20
  | { type: "auto_retry_end"; success: boolean; attempt: number; finalError?: string };
20
21
  ```
21
22
 
23
+ `queue_update` emits the full pending steering and follow-up queues whenever they change. `compaction_start` and `compaction_end` cover both manual and automatic compaction.
24
+
22
25
  Base events from [`AgentEvent`](https://github.com/badlogic/pi-mono/blob/main/packages/agent/src/types.ts#L179):
23
26
 
24
27
  ```typescript
package/docs/models.md CHANGED
@@ -131,6 +131,12 @@ The `apiKey` and `headers` fields support three formats:
131
131
  "apiKey": "sk-..."
132
132
  ```
133
133
 
134
+ For `models.json`, shell commands are resolved at request time. pi intentionally does not apply built-in TTL, stale reuse, or recovery logic for arbitrary commands. Different commands need different caching and failure strategies, and pi cannot infer the right one.
135
+
136
+ If your command is slow, expensive, rate-limited, or should keep using a previous value on transient failures, wrap it in your own script or command that implements the caching or TTL behavior you want.
137
+
138
+ `/model` availability checks use configured auth presence and do not execute shell commands.
139
+
134
140
  ### Custom Headers
135
141
 
136
142
  ```json
package/docs/rpc.md CHANGED
@@ -494,7 +494,7 @@ Response:
494
494
 
495
495
  #### get_session_stats
496
496
 
497
- Get token usage and cost statistics.
497
+ Get token usage, cost statistics, and current context window usage.
498
498
 
499
499
  ```json
500
500
  {"type": "get_session_stats"}
@@ -521,11 +521,20 @@ Response:
521
521
  "cacheWrite": 5000,
522
522
  "total": 105000
523
523
  },
524
- "cost": 0.45
524
+ "cost": 0.45,
525
+ "contextUsage": {
526
+ "tokens": 60000,
527
+ "contextWindow": 200000,
528
+ "percent": 30
529
+ }
525
530
  }
526
531
  }
527
532
  ```
528
533
 
534
+ `tokens` contains assistant usage totals for the current session state. `contextUsage` contains the actual current context-window estimate used for compaction and footer display.
535
+
536
+ `contextUsage` is omitted when no model or context window is available. `contextUsage.tokens` and `contextUsage.percent` are `null` immediately after compaction until a fresh post-compaction assistant response provides valid usage data.
537
+
529
538
  #### export_html
530
539
 
531
540
  Export session to an HTML file.
@@ -716,8 +725,9 @@ Events are streamed to stdout as JSON lines during agent operation. Events do NO
716
725
  | `tool_execution_start` | Tool begins execution |
717
726
  | `tool_execution_update` | Tool execution progress (streaming output) |
718
727
  | `tool_execution_end` | Tool completes |
719
- | `auto_compaction_start` | Auto-compaction begins |
720
- | `auto_compaction_end` | Auto-compaction completes |
728
+ | `queue_update` | Pending steering/follow-up queue changed |
729
+ | `compaction_start` | Compaction begins |
730
+ | `compaction_end` | Compaction completes |
721
731
  | `auto_retry_start` | Auto-retry begins (after transient error) |
722
732
  | `auto_retry_end` | Auto-retry completes (success or final failure) |
723
733
  | `extension_error` | Extension threw an error |
@@ -853,19 +863,32 @@ When complete:
853
863
 
854
864
  Use `toolCallId` to correlate events. The `partialResult` in `tool_execution_update` contains the accumulated output so far (not just the delta), allowing clients to simply replace their display on each update.
855
865
 
856
- ### auto_compaction_start / auto_compaction_end
866
+ ### queue_update
867
+
868
+ Emitted whenever the pending steering or follow-up queue changes.
869
+
870
+ ```json
871
+ {
872
+ "type": "queue_update",
873
+ "steering": ["Focus on error handling"],
874
+ "followUp": ["After that, summarize the result"]
875
+ }
876
+ ```
877
+
878
+ ### compaction_start / compaction_end
857
879
 
858
- Emitted when automatic compaction runs (when context is nearly full).
880
+ Emitted when compaction runs, whether manual or automatic.
859
881
 
860
882
  ```json
861
- {"type": "auto_compaction_start", "reason": "threshold"}
883
+ {"type": "compaction_start", "reason": "threshold"}
862
884
  ```
863
885
 
864
- The `reason` field is `"threshold"` (context getting large) or `"overflow"` (context exceeded limit).
886
+ The `reason` field is `"manual"`, `"threshold"`, or `"overflow"`.
865
887
 
866
888
  ```json
867
889
  {
868
- "type": "auto_compaction_end",
890
+ "type": "compaction_end",
891
+ "reason": "threshold",
869
892
  "result": {
870
893
  "summary": "Summary of conversation...",
871
894
  "firstKeptEntryId": "abc123",
package/docs/sdk.md CHANGED
@@ -20,7 +20,7 @@ import { AuthStorage, createAgentSession, ModelRegistry, SessionManager } from "
20
20
 
21
21
  // Set up credential storage and model registry
22
22
  const authStorage = AuthStorage.create();
23
- const modelRegistry = new ModelRegistry(authStorage);
23
+ const modelRegistry = ModelRegistry.create(authStorage);
24
24
 
25
25
  const { session } = await createAgentSession({
26
26
  sessionManager: SessionManager.inMemory(),
@@ -232,9 +232,12 @@ session.subscribe((event) => {
232
232
  // event.toolResults: tool results from this turn
233
233
  break;
234
234
 
235
- // Session events (auto-compaction, retry)
236
- case "auto_compaction_start":
237
- case "auto_compaction_end":
235
+ // Session events (queue, compaction, retry)
236
+ case "queue_update":
237
+ console.log(event.steering, event.followUp);
238
+ break;
239
+ case "compaction_start":
240
+ case "compaction_end":
238
241
  case "auto_retry_start":
239
242
  case "auto_retry_end":
240
243
  break;
@@ -286,7 +289,7 @@ import { getModel } from "@mariozechner/pi-ai";
286
289
  import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
287
290
 
288
291
  const authStorage = AuthStorage.create();
289
- const modelRegistry = new ModelRegistry(authStorage);
292
+ const modelRegistry = ModelRegistry.create(authStorage);
290
293
 
291
294
  // Find specific built-in model (doesn't check if API key exists)
292
295
  const opus = getModel("anthropic", "claude-opus-4-5");
@@ -334,7 +337,7 @@ import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
334
337
 
335
338
  // Default: uses ~/.pi/agent/auth.json and ~/.pi/agent/models.json
336
339
  const authStorage = AuthStorage.create();
337
- const modelRegistry = new ModelRegistry(authStorage);
340
+ const modelRegistry = ModelRegistry.create(authStorage);
338
341
 
339
342
  const { session } = await createAgentSession({
340
343
  sessionManager: SessionManager.inMemory(),
@@ -347,7 +350,7 @@ authStorage.setRuntimeApiKey("anthropic", "sk-my-temp-key");
347
350
 
348
351
  // Custom auth storage location
349
352
  const customAuth = AuthStorage.create("/my/app/auth.json");
350
- const customRegistry = new ModelRegistry(customAuth, "/my/app/models.json");
353
+ const customRegistry = ModelRegistry.create(customAuth, "/my/app/models.json");
351
354
 
352
355
  const { session } = await createAgentSession({
353
356
  sessionManager: SessionManager.inMemory(),
@@ -356,7 +359,7 @@ const { session } = await createAgentSession({
356
359
  });
357
360
 
358
361
  // No custom models.json (built-in models only)
359
- const simpleRegistry = new ModelRegistry(authStorage);
362
+ const simpleRegistry = ModelRegistry.inMemory(authStorage);
360
363
  ```
361
364
 
362
365
  > See [examples/sdk/09-api-keys-and-oauth.ts](../examples/sdk/09-api-keys-and-oauth.ts)
@@ -785,7 +788,7 @@ if (process.env.MY_KEY) {
785
788
  }
786
789
 
787
790
  // Model registry (no custom models.json)
788
- const modelRegistry = new ModelRegistry(authStorage);
791
+ const modelRegistry = ModelRegistry.create(authStorage);
789
792
 
790
793
  // Inline tool
791
794
  const statusTool: ToolDefinition = {
package/docs/settings.md CHANGED
@@ -127,6 +127,18 @@ When a provider requests a retry delay longer than `maxDelayMs` (e.g., Google's
127
127
 
128
128
  `npmCommand` is used for all npm package-manager operations, including `npm root -g`, installs, uninstalls, and `npm install` inside git packages. Use argv-style entries exactly as the process should be launched.
129
129
 
130
+ ### Sessions
131
+
132
+ | Setting | Type | Default | Description |
133
+ |---------|------|---------|-------------|
134
+ | `sessionDir` | string | - | Directory where session files are stored. Accepts absolute or relative paths. |
135
+
136
+ ```json
137
+ { "sessionDir": ".pi/sessions" }
138
+ ```
139
+
140
+ When multiple sources specify a session directory, `--session-dir` CLI flag takes precedence, then `sessionDir` in settings.json, then extension hooks.
141
+
130
142
  ### Model Cycling
131
143
 
132
144
  | Setting | Type | Default | Description |
package/docs/skills.md CHANGED
@@ -34,8 +34,9 @@ Pi loads skills from:
34
34
  - CLI: `--skill <path>` (repeatable, additive even with `--no-skills`)
35
35
 
36
36
  Discovery rules:
37
- - Direct `.md` files in the skills directory root
38
- - Recursive `SKILL.md` files under subdirectories
37
+ - In `~/.pi/agent/skills/` and `.pi/skills/`, direct root `.md` files are discovered as individual skills
38
+ - In all skill locations, directories containing `SKILL.md` are discovered recursively
39
+ - In `~/.agents/skills/` and project `.agents/skills/`, root `.md` files are ignored
39
40
 
40
41
  Disable discovery with `--no-skills` (explicit `--skill` paths still load).
41
42
 
@@ -52,6 +52,7 @@ cp permission-gate.ts ~/.pi/agent/extensions/
52
52
  | `qna.ts` | Extracts questions from last response into editor via `ctx.ui.setEditorText()` |
53
53
  | `status-line.ts` | Shows turn progress in footer via `ctx.ui.setStatus()` with themed colors |
54
54
  | `widget-placement.ts` | Shows widgets above and below the editor via `ctx.ui.setWidget()` placement |
55
+ | `hidden-thinking-label.ts` | Customizes the collapsed thinking label via `ctx.ui.setHiddenThinkingLabel()` |
55
56
  | `model-status.ts` | Shows model changes in status bar via `model_select` hook |
56
57
  | `snake.ts` | Snake game with custom UI, keyboard handling, and session persistence |
57
58
  | `send-user-message.ts` | Demonstrates `pi.sendUserMessage()` for sending user messages from extensions |
@@ -31,9 +31,13 @@ export default function (pi: ExtensionAPI) {
31
31
  return;
32
32
  }
33
33
 
34
- // Resolve API key for the summarization model
35
- const apiKey = await ctx.modelRegistry.getApiKey(model);
36
- if (!apiKey) {
34
+ // Resolve request auth for the summarization model
35
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
36
+ if (!auth.ok) {
37
+ ctx.ui.notify(`Compaction auth failed: ${auth.error}`, "warning");
38
+ return;
39
+ }
40
+ if (!auth.apiKey) {
37
41
  ctx.ui.notify(`No API key for ${model.provider}, using default compaction`, "warning");
38
42
  return;
39
43
  }
@@ -83,7 +87,16 @@ ${conversationText}
83
87
 
84
88
  try {
85
89
  // Pass signal to honor abort requests (e.g., user cancels compaction)
86
- const response = await complete(model, { messages: summaryMessages }, { apiKey, maxTokens: 8192, signal });
90
+ const response = await complete(
91
+ model,
92
+ { messages: summaryMessages },
93
+ {
94
+ apiKey: auth.apiKey,
95
+ headers: auth.headers,
96
+ maxTokens: 8192,
97
+ signal,
98
+ },
99
+ );
87
100
 
88
101
  const summary = response.content
89
102
  .filter((c): c is { type: "text"; text: string } => c.type === "text")
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pi-extension-custom-provider",
3
- "version": "1.14.0",
3
+ "version": "1.15.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pi-extension-custom-provider",
9
- "version": "1.14.0",
9
+ "version": "1.15.1",
10
10
  "dependencies": {
11
11
  "@anthropic-ai/sdk": "^0.52.0"
12
12
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-extension-custom-provider-anthropic",
3
3
  "private": true,
4
- "version": "1.14.0",
4
+ "version": "1.15.1",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-extension-custom-provider-gitlab-duo",
3
3
  "private": true,
4
- "version": "1.14.0",
4
+ "version": "1.15.1",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-extension-custom-provider-qwen-cli",
3
3
  "private": true,
4
- "version": "1.13.0",
4
+ "version": "1.14.1",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",
@@ -80,7 +80,10 @@ export default function (pi: ExtensionAPI) {
80
80
  loader.onAbort = () => done(null);
81
81
 
82
82
  const doGenerate = async () => {
83
- const apiKey = await ctx.modelRegistry.getApiKey(ctx.model!);
83
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model!);
84
+ if (!auth.ok || !auth.apiKey) {
85
+ throw new Error(auth.ok ? `No API key for ${ctx.model!.provider}` : auth.error);
86
+ }
84
87
 
85
88
  const userMessage: Message = {
86
89
  role: "user",
@@ -96,7 +99,7 @@ export default function (pi: ExtensionAPI) {
96
99
  const response = await complete(
97
100
  ctx.model!,
98
101
  { systemPrompt: SYSTEM_PROMPT, messages: [userMessage] },
99
- { apiKey, signal: loader.signal },
102
+ { apiKey: auth.apiKey, headers: auth.headers, signal: loader.signal },
100
103
  );
101
104
 
102
105
  if (response.stopReason === "aborted") {
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Hidden Thinking Label Extension
3
+ *
4
+ * Demonstrates `ctx.ui.setHiddenThinkingLabel()` for customizing the label shown
5
+ * when thinking blocks are hidden.
6
+ *
7
+ * Usage:
8
+ * pi --extension examples/extensions/hidden-thinking-label.ts
9
+ *
10
+ * Test:
11
+ * 1. Load this extension
12
+ * 2. Hide thinking blocks with Ctrl+T
13
+ * 3. Ask for something that produces reasoning output
14
+ * 4. The collapsed thinking block label will show the custom text
15
+ *
16
+ * Commands:
17
+ * /thinking-label <text> Set a custom hidden thinking label
18
+ * /thinking-label Reset to the default label
19
+ */
20
+
21
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
22
+
23
+ const DEFAULT_LABEL = "Pondering...";
24
+
25
+ export default function (pi: ExtensionAPI) {
26
+ let label = DEFAULT_LABEL;
27
+
28
+ const applyLabel = (ctx: ExtensionContext) => {
29
+ ctx.ui.setHiddenThinkingLabel(label);
30
+ };
31
+
32
+ pi.on("session_start", async (_event, ctx) => {
33
+ applyLabel(ctx);
34
+ });
35
+
36
+ pi.on("session_switch", async (_event, ctx) => {
37
+ applyLabel(ctx);
38
+ });
39
+
40
+ pi.registerCommand("thinking-label", {
41
+ description: "Set the hidden thinking label. Use without args to reset.",
42
+ handler: async (args, ctx) => {
43
+ const nextLabel = args.trim();
44
+
45
+ if (!nextLabel) {
46
+ label = DEFAULT_LABEL;
47
+ ctx.ui.setHiddenThinkingLabel();
48
+ ctx.ui.notify(`Hidden thinking label reset to: ${DEFAULT_LABEL}`);
49
+ return;
50
+ }
51
+
52
+ label = nextLabel;
53
+ ctx.ui.setHiddenThinkingLabel(label);
54
+ ctx.ui.notify(`Hidden thinking label set to: ${label}`);
55
+ },
56
+ });
57
+ }
@@ -77,7 +77,10 @@ export default function (pi: ExtensionAPI) {
77
77
 
78
78
  // Do the work
79
79
  const doExtract = async () => {
80
- const apiKey = await ctx.modelRegistry.getApiKey(ctx.model!);
80
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model!);
81
+ if (!auth.ok || !auth.apiKey) {
82
+ throw new Error(auth.ok ? `No API key for ${ctx.model!.provider}` : auth.error);
83
+ }
81
84
  const userMessage: UserMessage = {
82
85
  role: "user",
83
86
  content: [{ type: "text", text: lastAssistantText! }],
@@ -87,7 +90,7 @@ export default function (pi: ExtensionAPI) {
87
90
  const response = await complete(
88
91
  ctx.model!,
89
92
  { systemPrompt: SYSTEM_PROMPT, messages: [userMessage] },
90
- { apiKey, signal: loader.signal },
93
+ { apiKey: auth.apiKey, headers: auth.headers, signal: loader.signal },
91
94
  );
92
95
 
93
96
  if (response.stopReason === "aborted") {
@@ -5,6 +5,10 @@
5
5
  * restrictions on bash commands at the OS level (sandbox-exec on macOS,
6
6
  * bubblewrap on Linux).
7
7
  *
8
+ * Note: this example intentionally overrides the built-in `bash` tool to show
9
+ * how built-in tools can be replaced. Alternatively, you could sandbox `bash`
10
+ * via `tool_call` input mutation without replacing the tool.
11
+ *
8
12
  * Config files (merged, project takes precedence):
9
13
  * - ~/.pi/agent/sandbox.json (global)
10
14
  * - <cwd>/.pi/sandbox.json (project-local)
@@ -165,12 +165,15 @@ export default function (pi: ExtensionAPI) {
165
165
  ctx.ui.notify("Model openai/gpt-5.2 not found", "warning");
166
166
  }
167
167
 
168
- const apiKey = model ? await ctx.modelRegistry.getApiKey(model) : undefined;
169
- if (!apiKey && ctx.hasUI) {
168
+ const auth = model ? await ctx.modelRegistry.getApiKeyAndHeaders(model) : undefined;
169
+ if (auth && !auth.ok && ctx.hasUI) {
170
+ ctx.ui.notify(auth.error, "warning");
171
+ }
172
+ if (auth?.ok && !auth.apiKey && ctx.hasUI) {
170
173
  ctx.ui.notify("No API key for openai/gpt-5.2", "warning");
171
174
  }
172
175
 
173
- if (!model || !apiKey) {
176
+ if (!model || !auth?.ok || !auth.apiKey) {
174
177
  return;
175
178
  }
176
179
 
@@ -182,7 +185,15 @@ export default function (pi: ExtensionAPI) {
182
185
  },
183
186
  ];
184
187
 
185
- const response = await complete(model, { messages: summaryMessages }, { apiKey, reasoningEffort: "high" });
188
+ const response = await complete(
189
+ model,
190
+ { messages: summaryMessages },
191
+ {
192
+ apiKey: auth.apiKey,
193
+ headers: auth.headers,
194
+ reasoningEffort: "high",
195
+ },
196
+ );
186
197
 
187
198
  const summary = response.content
188
199
  .filter((c): c is { type: "text"; text: string } => c.type === "text")
@@ -3,6 +3,8 @@ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-age
3
3
  const COMPACT_THRESHOLD_TOKENS = 100_000;
4
4
 
5
5
  export default function (pi: ExtensionAPI) {
6
+ let previousTokens: number | null | undefined;
7
+
6
8
  const triggerCompaction = (ctx: ExtensionContext, customInstructions?: string) => {
7
9
  if (ctx.hasUI) {
8
10
  ctx.ui.notify("Compaction started", "info");
@@ -24,7 +26,15 @@ export default function (pi: ExtensionAPI) {
24
26
 
25
27
  pi.on("turn_end", (_event, ctx) => {
26
28
  const usage = ctx.getContextUsage();
27
- if (!usage || usage.tokens === null || usage.tokens <= COMPACT_THRESHOLD_TOKENS) {
29
+ const currentTokens = usage?.tokens ?? null;
30
+ if (currentTokens === null) {
31
+ return;
32
+ }
33
+
34
+ const crossedThreshold =
35
+ previousTokens !== undefined && previousTokens !== null && previousTokens <= COMPACT_THRESHOLD_TOKENS;
36
+ previousTokens = currentTokens;
37
+ if (!crossedThreshold || currentTokens <= COMPACT_THRESHOLD_TOKENS) {
28
38
  return;
29
39
  }
30
40
  triggerCompaction(ctx);