@eminent337/aery 0.1.44 → 0.1.53

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.
@@ -1,17 +1,19 @@
1
- > Aery can create extensions. Ask it to build one for your use case.
1
+ > The agent can create extensions. Ask it to build one for your use case.
2
2
 
3
3
  # Extensions
4
4
 
5
- Extensions are TypeScript modules that extend Aery's behavior. They can subscribe to lifecycle events, register custom tools callable by the LLM, add commands, and more.
5
+ Extensions are TypeScript modules that extend the agent's behavior. They can subscribe to lifecycle events, register custom tools callable by the LLM, add commands, and more.
6
+
7
+ > This extension system is shared across the open-source AI coding agent ecosystem. Extensions written for [pi](https://github.com/badlogic/pi-mono) are compatible with [Aery](https://github.com/eminent337/aery). For more extension examples, see [openclaude](https://github.com/Gitlawb/openclaude) and [opencode](https://github.com/sst/opencode) which use similar plugin architectures.
6
8
 
7
9
  > **Placement for /reload:** Put extensions in `~/.aery/agent/extensions/` (global) or `.aery/extensions/` (project-local) for auto-discovery. Use `aery -e ./path.ts` only for quick tests. Extensions in auto-discovered locations can be hot-reloaded with `/reload`.
8
10
 
9
11
  **Key capabilities:**
10
- - **Custom tools** - Register tools the LLM can call via `aery.registerTool()`
12
+ - **Custom tools** - Register tools the LLM can call via `pi.registerTool()`
11
13
  - **Event interception** - Block or modify tool calls, inject context, customize compaction
12
14
  - **User interaction** - Prompt users via `ctx.ui` (select, confirm, input, notify)
13
15
  - **Custom UI components** - Full TUI components with keyboard input via `ctx.ui.custom()` for complex interactions
14
- - **Custom commands** - Register commands like `/mycommand` via `aery.registerCommand()`
16
+ - **Custom commands** - Register commands like `/mycommand` via `pi.registerCommand()`
15
17
  - **Session persistence** - Store state that survives restarts via `aery.appendEntry()`
16
18
  - **Custom rendering** - Control how tool calls/results and messages appear in TUI
17
19
 
@@ -61,11 +63,11 @@ import { Type } from "@sinclair/typebox";
61
63
 
62
64
  export default function (aery: ExtensionAPI) {
63
65
  // React to events
64
- aery.on("session_start", async (_event, ctx) => {
66
+ pi.on("session_start", async (_event, ctx) => {
65
67
  ctx.ui.notify("Extension loaded!", "info");
66
68
  });
67
69
 
68
- aery.on("tool_call", async (event, ctx) => {
70
+ pi.on("tool_call", async (event, ctx) => {
69
71
  if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) {
70
72
  const ok = await ctx.ui.confirm("Dangerous!", "Allow rm -rf?");
71
73
  if (!ok) return { block: true, reason: "Blocked by user" };
@@ -73,7 +75,7 @@ export default function (aery: ExtensionAPI) {
73
75
  });
74
76
 
75
77
  // Register a custom tool
76
- aery.registerTool({
78
+ pi.registerTool({
77
79
  name: "greet",
78
80
  label: "Greet",
79
81
  description: "Greet someone by name",
@@ -89,7 +91,7 @@ export default function (aery: ExtensionAPI) {
89
91
  });
90
92
 
91
93
  // Register a command
92
- aery.registerCommand("hello", {
94
+ pi.registerCommand("hello", {
93
95
  description: "Say hello",
94
96
  handler: async (args, ctx) => {
95
97
  ctx.ui.notify(`Hello ${args || "world"}!`, "info");
@@ -132,7 +134,7 @@ Additional paths via `settings.json`:
132
134
  }
133
135
  ```
134
136
 
135
- To share extensions via npm or git as Aery packages, see [packages.md](packages.md).
137
+ To share extensions via npm or git as pi packages, see [packages.md](packages.md).
136
138
 
137
139
  ## Available Imports
138
140
 
@@ -145,7 +147,7 @@ To share extensions via npm or git as Aery packages, see [packages.md](packages.
145
147
 
146
148
  npm dependencies work too. Add a `package.json` next to your extension (or in a parent directory), run `npm install`, and imports from `node_modules/` are resolved automatically.
147
149
 
148
- For distributed Aery packages installed with `aery install` (npm or git), runtime deps must be in `dependencies`. Package installation uses production installs (`npm install --omit=dev`), so `devDependencies` are not available at runtime.
150
+ For distributed pi packages installed with `aery install` (npm or git), runtime deps must be in `dependencies`. Package installation uses production installs (`npm install --omit=dev`), so `devDependencies` are not available at runtime.
149
151
 
150
152
  Node.js built-ins (`node:fs`, `node:path`, etc.) are also available.
151
153
 
@@ -158,7 +160,7 @@ import type { ExtensionAPI } from "@eminent337/aery";
158
160
 
159
161
  export default function (aery: ExtensionAPI) {
160
162
  // Subscribe to events
161
- aery.on("event_name", async (event, ctx) => {
163
+ pi.on("event_name", async (event, ctx) => {
162
164
  // ctx.ui for user interaction
163
165
  const ok = await ctx.ui.confirm("Title", "Are you sure?");
164
166
  ctx.ui.notify("Done!", "success");
@@ -167,8 +169,8 @@ export default function (aery: ExtensionAPI) {
167
169
  });
168
170
 
169
171
  // Register tools, commands, shortcuts, flags
170
- aery.registerTool({ ... });
171
- aery.registerCommand("name", { ... });
172
+ pi.registerTool({ ... });
173
+ pi.registerCommand("name", { ... });
172
174
  aery.registerShortcut("ctrl+x", { ... });
173
175
  aery.registerFlag("my-flag", { ... });
174
176
  }
@@ -176,7 +178,7 @@ export default function (aery: ExtensionAPI) {
176
178
 
177
179
  Extensions are loaded via [jiti](https://github.com/unjs/jiti), so TypeScript works without compilation.
178
180
 
179
- If the factory returns a `Promise`, Aery awaits it before continuing startup. That means async initialization completes before `session_start`, before `resources_discover`, and before provider registrations queued via `aery.registerProvider()` are flushed.
181
+ If the factory returns a `Promise`, pi awaits it before continuing startup. That means async initialization completes before `session_start`, before `resources_discover`, and before provider registrations queued via `pi.registerProvider()` are flushed.
180
182
 
181
183
  ### Async factory functions
182
184
 
@@ -196,7 +198,7 @@ export default async function (aery: ExtensionAPI) {
196
198
  }>;
197
199
  };
198
200
 
199
- aery.registerProvider("local-openai", {
201
+ pi.registerProvider("local-openai", {
200
202
  baseUrl: "http://localhost:1234/v1",
201
203
  apiKey: "LOCAL_OPENAI_API_KEY",
202
204
  api: "openai-completions",
@@ -267,7 +269,7 @@ Run `npm install` in the extension directory, then imports from `node_modules/`
267
269
  ### Lifecycle Overview
268
270
 
269
271
  ```
270
- Aery starts
272
+ pi starts
271
273
 
272
274
  ├─► session_start { reason: "startup" }
273
275
  └─► resources_discover { reason: "startup" }
@@ -337,7 +339,7 @@ Fired after `session_start` so extensions can contribute additional skill, promp
337
339
  The startup path uses `reason: "startup"`. Reload uses `reason: "reload"`.
338
340
 
339
341
  ```typescript
340
- aery.on("resources_discover", async (event, _ctx) => {
342
+ pi.on("resources_discover", async (event, _ctx) => {
341
343
  // event.cwd - current working directory
342
344
  // event.reason - "startup" | "reload"
343
345
  return {
@@ -357,7 +359,7 @@ See [session.md](session.md) for session storage internals and the SessionManage
357
359
  Fired when a session is started, loaded, or reloaded.
358
360
 
359
361
  ```typescript
360
- aery.on("session_start", async (event, ctx) => {
362
+ pi.on("session_start", async (event, ctx) => {
361
363
  // event.reason - "startup" | "reload" | "new" | "resume" | "fork"
362
364
  // event.previousSessionFile - present for "new", "resume", and "fork"
363
365
  ctx.ui.notify(`Session: ${ctx.sessionManager.getSessionFile() ?? "ephemeral"}`, "info");
@@ -369,7 +371,7 @@ aery.on("session_start", async (event, ctx) => {
369
371
  Fired before starting a new session (`/new`) or switching sessions (`/resume`).
370
372
 
371
373
  ```typescript
372
- aery.on("session_before_switch", async (event, ctx) => {
374
+ pi.on("session_before_switch", async (event, ctx) => {
373
375
  // event.reason - "new" or "resume"
374
376
  // event.targetSessionFile - session we're switching to (only for "resume")
375
377
 
@@ -380,7 +382,7 @@ aery.on("session_before_switch", async (event, ctx) => {
380
382
  });
381
383
  ```
382
384
 
383
- After a successful switch or new-session action, Aery emits `session_shutdown` for the old extension instance, reloads and rebinds extensions for the new session, then emits `session_start` with `reason: "new" | "resume"` and `previousSessionFile`.
385
+ After a successful switch or new-session action, pi emits `session_shutdown` for the old extension instance, reloads and rebinds extensions for the new session, then emits `session_start` with `reason: "new" | "resume"` and `previousSessionFile`.
384
386
  Do cleanup work in `session_shutdown`, then reestablish any in-memory state in `session_start`.
385
387
 
386
388
  #### session_before_fork
@@ -388,7 +390,7 @@ Do cleanup work in `session_shutdown`, then reestablish any in-memory state in `
388
390
  Fired when forking via `/fork` or cloning via `/clone`.
389
391
 
390
392
  ```typescript
391
- aery.on("session_before_fork", async (event, ctx) => {
393
+ pi.on("session_before_fork", async (event, ctx) => {
392
394
  // event.entryId - ID of the selected entry
393
395
  // event.position - "before" for /fork, "at" for /clone
394
396
  return { cancel: true }; // Cancel fork/clone
@@ -397,7 +399,7 @@ aery.on("session_before_fork", async (event, ctx) => {
397
399
  });
398
400
  ```
399
401
 
400
- After a successful fork or clone, Aery emits `session_shutdown` for the old extension instance, reloads and rebinds extensions for the new session, then emits `session_start` with `reason: "fork"` and `previousSessionFile`.
402
+ After a successful fork or clone, pi emits `session_shutdown` for the old extension instance, reloads and rebinds extensions for the new session, then emits `session_start` with `reason: "fork"` and `previousSessionFile`.
401
403
  Do cleanup work in `session_shutdown`, then reestablish any in-memory state in `session_start`.
402
404
 
403
405
  #### session_before_compact / session_compact
@@ -405,7 +407,7 @@ Do cleanup work in `session_shutdown`, then reestablish any in-memory state in `
405
407
  Fired on compaction. See [compaction.md](compaction.md) for details.
406
408
 
407
409
  ```typescript
408
- aery.on("session_before_compact", async (event, ctx) => {
410
+ pi.on("session_before_compact", async (event, ctx) => {
409
411
  const { preparation, branchEntries, customInstructions, signal } = event;
410
412
 
411
413
  // Cancel:
@@ -421,7 +423,7 @@ aery.on("session_before_compact", async (event, ctx) => {
421
423
  };
422
424
  });
423
425
 
424
- aery.on("session_compact", async (event, ctx) => {
426
+ pi.on("session_compact", async (event, ctx) => {
425
427
  // event.compactionEntry - the saved compaction
426
428
  // event.fromExtension - whether extension provided it
427
429
  });
@@ -432,14 +434,14 @@ aery.on("session_compact", async (event, ctx) => {
432
434
  Fired on `/tree` navigation. See [tree.md](tree.md) for tree navigation concepts.
433
435
 
434
436
  ```typescript
435
- aery.on("session_before_tree", async (event, ctx) => {
437
+ pi.on("session_before_tree", async (event, ctx) => {
436
438
  const { preparation, signal } = event;
437
439
  return { cancel: true };
438
440
  // OR provide custom summary:
439
441
  return { summary: { summary: "...", details: {} } };
440
442
  });
441
443
 
442
- aery.on("session_tree", async (event, ctx) => {
444
+ pi.on("session_tree", async (event, ctx) => {
443
445
  // event.newLeafId, oldLeafId, summaryEntry, fromExtension
444
446
  });
445
447
  ```
@@ -449,7 +451,7 @@ aery.on("session_tree", async (event, ctx) => {
449
451
  Fired on exit (Ctrl+C, Ctrl+D, SIGHUP, SIGTERM).
450
452
 
451
453
  ```typescript
452
- aery.on("session_shutdown", async (_event, ctx) => {
454
+ pi.on("session_shutdown", async (_event, ctx) => {
453
455
  // Cleanup, save state, etc.
454
456
  });
455
457
  ```
@@ -461,7 +463,7 @@ aery.on("session_shutdown", async (_event, ctx) => {
461
463
  Fired after user submits prompt, before agent loop. Can inject a message and/or modify the system prompt.
462
464
 
463
465
  ```typescript
464
- aery.on("before_agent_start", async (event, ctx) => {
466
+ pi.on("before_agent_start", async (event, ctx) => {
465
467
  // event.prompt - user's prompt text
466
468
  // event.images - attached images (if any)
467
469
  // event.systemPrompt - current system prompt
@@ -484,9 +486,9 @@ aery.on("before_agent_start", async (event, ctx) => {
484
486
  Fired once per user prompt.
485
487
 
486
488
  ```typescript
487
- aery.on("agent_start", async (_event, ctx) => {});
489
+ pi.on("agent_start", async (_event, ctx) => {});
488
490
 
489
- aery.on("agent_end", async (event, ctx) => {
491
+ pi.on("agent_end", async (event, ctx) => {
490
492
  // event.messages - messages from this prompt
491
493
  });
492
494
  ```
@@ -496,11 +498,11 @@ aery.on("agent_end", async (event, ctx) => {
496
498
  Fired for each turn (one LLM response + tool calls).
497
499
 
498
500
  ```typescript
499
- aery.on("turn_start", async (event, ctx) => {
501
+ pi.on("turn_start", async (event, ctx) => {
500
502
  // event.turnIndex, event.timestamp
501
503
  });
502
504
 
503
- aery.on("turn_end", async (event, ctx) => {
505
+ pi.on("turn_end", async (event, ctx) => {
504
506
  // event.turnIndex, event.message, event.toolResults
505
507
  });
506
508
  ```
@@ -513,16 +515,16 @@ Fired for message lifecycle updates.
513
515
  - `message_update` fires for assistant streaming updates.
514
516
 
515
517
  ```typescript
516
- aery.on("message_start", async (event, ctx) => {
518
+ pi.on("message_start", async (event, ctx) => {
517
519
  // event.message
518
520
  });
519
521
 
520
- aery.on("message_update", async (event, ctx) => {
522
+ pi.on("message_update", async (event, ctx) => {
521
523
  // event.message
522
524
  // event.assistantMessageEvent (token-by-token stream event)
523
525
  });
524
526
 
525
- aery.on("message_end", async (event, ctx) => {
527
+ pi.on("message_end", async (event, ctx) => {
526
528
  // event.message
527
529
  });
528
530
  ```
@@ -537,15 +539,15 @@ In parallel tool mode:
537
539
  - `tool_execution_end` is emitted in assistant source order, matching final tool result message order
538
540
 
539
541
  ```typescript
540
- aery.on("tool_execution_start", async (event, ctx) => {
542
+ pi.on("tool_execution_start", async (event, ctx) => {
541
543
  // event.toolCallId, event.toolName, event.args
542
544
  });
543
545
 
544
- aery.on("tool_execution_update", async (event, ctx) => {
546
+ pi.on("tool_execution_update", async (event, ctx) => {
545
547
  // event.toolCallId, event.toolName, event.args, event.partialResult
546
548
  });
547
549
 
548
- aery.on("tool_execution_end", async (event, ctx) => {
550
+ pi.on("tool_execution_end", async (event, ctx) => {
549
551
  // event.toolCallId, event.toolName, event.result, event.isError
550
552
  });
551
553
  ```
@@ -555,7 +557,7 @@ aery.on("tool_execution_end", async (event, ctx) => {
555
557
  Fired before each LLM call. Modify messages non-destructively. See [session.md](session.md) for message types.
556
558
 
557
559
  ```typescript
558
- aery.on("context", async (event, ctx) => {
560
+ pi.on("context", async (event, ctx) => {
559
561
  // event.messages - deep copy, safe to modify
560
562
  const filtered = event.messages.filter(m => !shouldPrune(m));
561
563
  return { messages: filtered };
@@ -567,7 +569,7 @@ aery.on("context", async (event, ctx) => {
567
569
  Fired after the provider-specific payload is built, right before the request is sent. Handlers run in extension load order. Returning `undefined` keeps the payload unchanged. Returning any other value replaces the payload for later handlers and for the actual request.
568
570
 
569
571
  ```typescript
570
- aery.on("before_provider_request", (event, ctx) => {
572
+ pi.on("before_provider_request", (event, ctx) => {
571
573
  console.log(JSON.stringify(event.payload, null, 2));
572
574
 
573
575
  // Optional: replace payload
@@ -582,7 +584,7 @@ This is mainly useful for debugging provider serialization and cache behavior.
582
584
  Fired after an HTTP response is received and before its stream body is consumed. Handlers run in extension load order.
583
585
 
584
586
  ```typescript
585
- aery.on("after_provider_response", (event, ctx) => {
587
+ pi.on("after_provider_response", (event, ctx) => {
586
588
  // event.status - HTTP status code
587
589
  // event.headers - normalized response headers
588
590
  if (event.status === 429) {
@@ -600,7 +602,7 @@ Header availability depends on provider and transport. Providers that abstract H
600
602
  Fired when the model changes via `/model` command, model cycling (`Ctrl+P`), or session restore.
601
603
 
602
604
  ```typescript
603
- aery.on("model_select", async (event, ctx) => {
605
+ pi.on("model_select", async (event, ctx) => {
604
606
  // event.model - newly selected model
605
607
  // event.previousModel - previous model (undefined if first selection)
606
608
  // event.source - "set" | "cycle" | "restore"
@@ -622,7 +624,7 @@ Use this to update UI elements (status bars, footers) or perform model-specific
622
624
 
623
625
  Fired after `tool_execution_start`, before the tool executes. **Can block.** Use `isToolCallEventType` to narrow and get typed inputs.
624
626
 
625
- Before `tool_call` runs, Aery waits for previously emitted Agent events to finish draining through `AgentSession`. This means `ctx.sessionManager` is up to date through the current assistant tool-calling message.
627
+ Before `tool_call` runs, pi waits for previously emitted Agent events to finish draining through `AgentSession`. This means `ctx.sessionManager` is up to date through the current assistant tool-calling message.
626
628
 
627
629
  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`.
628
630
 
@@ -637,7 +639,7 @@ Behavior guarantees:
637
639
  ```typescript
638
640
  import { isToolCallEventType } from "@eminent337/aery";
639
641
 
640
- aery.on("tool_call", async (event, ctx) => {
642
+ pi.on("tool_call", async (event, ctx) => {
641
643
  // event.toolName - "bash", "read", "write", "edit", etc.
642
644
  // event.toolCallId
643
645
  // event.input - tool parameters (mutable)
@@ -674,7 +676,7 @@ Use `isToolCallEventType` with explicit type parameters:
674
676
  import { isToolCallEventType } from "@eminent337/aery";
675
677
  import type { MyToolInput } from "my-extension";
676
678
 
677
- aery.on("tool_call", (event) => {
679
+ pi.on("tool_call", (event) => {
678
680
  if (isToolCallEventType<"my_tool", MyToolInput>("my_tool", event)) {
679
681
  event.input.action; // typed
680
682
  }
@@ -695,7 +697,7 @@ Use `ctx.signal` for nested async work inside the handler. This lets Esc cancel
695
697
  ```typescript
696
698
  import { isBashToolResult } from "@eminent337/aery";
697
699
 
698
- aery.on("tool_result", async (event, ctx) => {
700
+ pi.on("tool_result", async (event, ctx) => {
699
701
  // event.toolName, event.toolCallId, event.input
700
702
  // event.content, event.details, event.isError
701
703
 
@@ -723,7 +725,7 @@ Fired when user executes `!` or `!!` commands. **Can intercept.**
723
725
  ```typescript
724
726
  import { createLocalBashOperations } from "@eminent337/aery";
725
727
 
726
- aery.on("user_bash", (event, ctx) => {
728
+ pi.on("user_bash", (event, ctx) => {
727
729
  // event.command - the bash command
728
730
  // event.excludeFromContext - true if !! prefix
729
731
  // event.cwd - working directory
@@ -731,7 +733,7 @@ aery.on("user_bash", (event, ctx) => {
731
733
  // Option 1: Provide custom operations (e.g., SSH)
732
734
  return { operations: remoteBashOps };
733
735
 
734
- // Option 2: Wrap Aery's built-in local bash backend
736
+ // Option 2: Wrap pi's built-in local bash backend
735
737
  const local = createLocalBashOperations();
736
738
  return {
737
739
  operations: {
@@ -760,7 +762,7 @@ Fired when user input is received, after extension commands are checked but befo
760
762
  5. Agent processing begins (`before_agent_start`, etc.)
761
763
 
762
764
  ```typescript
763
- aery.on("input", async (event, ctx) => {
765
+ pi.on("input", async (event, ctx) => {
764
766
  // event.text - raw input (before skill/template expansion)
765
767
  // event.images - attached images, if any
766
768
  // event.source - "interactive" (typed), "rpc" (API), or "extension" (via sendUserMessage)
@@ -836,10 +838,10 @@ Use this for abort-aware nested work started by extension handlers, for example:
836
838
  - file or process helpers that accept `AbortSignal`
837
839
 
838
840
  `ctx.signal` is typically defined during active turn events such as `tool_call`, `tool_result`, `message_update`, and `turn_end`.
839
- It is usually `undefined` in idle or non-turn contexts such as session events, extension commands, and shortcuts fired while Aery is idle.
841
+ It is usually `undefined` in idle or non-turn contexts such as session events, extension commands, and shortcuts fired while pi is idle.
840
842
 
841
843
  ```typescript
842
- aery.on("tool_result", async (event, ctx) => {
844
+ pi.on("tool_result", async (event, ctx) => {
843
845
  const response = await fetch("https://example.com/api", {
844
846
  method: "POST",
845
847
  body: JSON.stringify(event),
@@ -857,7 +859,7 @@ Control flow helpers.
857
859
 
858
860
  ### ctx.shutdown()
859
861
 
860
- Request a graceful shutdown of Aery.
862
+ Request a graceful shutdown of pi.
861
863
 
862
864
  - **Interactive mode:** Deferred until the agent becomes idle (after processing all queued steering and follow-up messages).
863
865
  - **RPC mode:** Deferred until the next idle state (after completing the current command response, when waiting for the next command).
@@ -866,7 +868,7 @@ Request a graceful shutdown of Aery.
866
868
  Emits `session_shutdown` event to all extensions before exiting. Available in all contexts (event handlers, tools, commands, shortcuts).
867
869
 
868
870
  ```typescript
869
- aery.on("tool_call", (event, ctx) => {
871
+ pi.on("tool_call", (event, ctx) => {
870
872
  if (isFatal(event.input)) {
871
873
  ctx.shutdown();
872
874
  }
@@ -905,7 +907,7 @@ ctx.compact({
905
907
  Returns the current effective system prompt. This includes any modifications made by `before_agent_start` handlers for the current turn.
906
908
 
907
909
  ```typescript
908
- aery.on("before_agent_start", (event, ctx) => {
910
+ pi.on("before_agent_start", (event, ctx) => {
909
911
  const prompt = ctx.getSystemPrompt();
910
912
  console.log(`System prompt length: ${prompt.length}`);
911
913
  });
@@ -920,7 +922,7 @@ Command handlers receive `ExtensionCommandContext`, which extends `ExtensionCont
920
922
  Wait for the agent to finish streaming:
921
923
 
922
924
  ```typescript
923
- aery.registerCommand("my-cmd", {
925
+ pi.registerCommand("my-cmd", {
924
926
  handler: async (args, ctx) => {
925
927
  await ctx.waitForIdle();
926
928
  // Agent is now idle, safe to modify session
@@ -1004,7 +1006,7 @@ To discover available sessions, use the static `SessionManager.list()` or `Sessi
1004
1006
  ```typescript
1005
1007
  import { SessionManager } from "@eminent337/aery";
1006
1008
 
1007
- aery.registerCommand("switch", {
1009
+ pi.registerCommand("switch", {
1008
1010
  description: "Switch to another session",
1009
1011
  handler: async (args, ctx) => {
1010
1012
  const sessions = await SessionManager.list(ctx.cwd);
@@ -1025,7 +1027,7 @@ aery.registerCommand("switch", {
1025
1027
  Run the same reload flow as `/reload`.
1026
1028
 
1027
1029
  ```typescript
1028
- aery.registerCommand("reload-runtime", {
1030
+ pi.registerCommand("reload-runtime", {
1029
1031
  description: "Reload extensions, skills, prompts, and themes",
1030
1032
  handler: async (_args, ctx) => {
1031
1033
  await ctx.reload();
@@ -1053,7 +1055,7 @@ import type { ExtensionAPI } from "@eminent337/aery";
1053
1055
  import { Type } from "@sinclair/typebox";
1054
1056
 
1055
1057
  export default function (aery: ExtensionAPI) {
1056
- aery.registerCommand("reload-runtime", {
1058
+ pi.registerCommand("reload-runtime", {
1057
1059
  description: "Reload extensions, skills, prompts, and themes",
1058
1060
  handler: async (_args, ctx) => {
1059
1061
  await ctx.reload();
@@ -1061,13 +1063,13 @@ export default function (aery: ExtensionAPI) {
1061
1063
  },
1062
1064
  });
1063
1065
 
1064
- aery.registerTool({
1066
+ pi.registerTool({
1065
1067
  name: "reload_runtime",
1066
1068
  label: "Reload Runtime",
1067
1069
  description: "Reload extensions, skills, prompts, and themes",
1068
1070
  parameters: Type.Object({}),
1069
1071
  async execute() {
1070
- aery.sendUserMessage("/reload-runtime", { deliverAs: "followUp" });
1072
+ pi.sendUserMessage("/reload-runtime", { deliverAs: "followUp" });
1071
1073
  return {
1072
1074
  content: [{ type: "text", text: "Queued /reload-runtime as a follow-up command." }],
1073
1075
  };
@@ -1078,15 +1080,15 @@ export default function (aery: ExtensionAPI) {
1078
1080
 
1079
1081
  ## ExtensionAPI Methods
1080
1082
 
1081
- ### aery.on(event, handler)
1083
+ ### pi.on(event, handler)
1082
1084
 
1083
1085
  Subscribe to events. See [Events](#events) for event types and return values.
1084
1086
 
1085
- ### aery.registerTool(definition)
1087
+ ### pi.registerTool(definition)
1086
1088
 
1087
1089
  Register a custom tool callable by the LLM. See [Custom Tools](#custom-tools) for full details.
1088
1090
 
1089
- `aery.registerTool()` works both during extension load and after startup. You can call it inside `session_start`, command handlers, or other event handlers. New tools are refreshed immediately in the same session, so they appear in `aery.getAllTools()` and are callable by the LLM without `/reload`.
1091
+ `pi.registerTool()` works both during extension load and after startup. You can call it inside `session_start`, command handlers, or other event handlers. New tools are refreshed immediately in the same session, so they appear in `aery.getAllTools()` and are callable by the LLM without `/reload`.
1090
1092
 
1091
1093
  Use `aery.setActiveTools()` to enable or disable tools (including dynamically added tools) at runtime.
1092
1094
 
@@ -1098,7 +1100,7 @@ See [dynamic-tools.ts](../examples/extensions/dynamic-tools.ts) for a full examp
1098
1100
  import { Type } from "@sinclair/typebox";
1099
1101
  import { StringEnum } from "@eminent337/aery-ai";
1100
1102
 
1101
- aery.registerTool({
1103
+ pi.registerTool({
1102
1104
  name: "my_tool",
1103
1105
  label: "My Tool",
1104
1106
  description: "What this tool does",
@@ -1154,23 +1156,23 @@ aery.sendMessage({
1154
1156
  - `"nextTurn"` - Queued for next user prompt. Does not interrupt or trigger anything.
1155
1157
  - `triggerTurn: true` - If agent is idle, trigger an LLM response immediately. Only applies to `"steer"` and `"followUp"` modes (ignored for `"nextTurn"`).
1156
1158
 
1157
- ### aery.sendUserMessage(content, options?)
1159
+ ### pi.sendUserMessage(content, options?)
1158
1160
 
1159
1161
  Send a user message to the agent. Unlike `sendMessage()` which sends custom messages, this sends an actual user message that appears as if typed by the user. Always triggers a turn.
1160
1162
 
1161
1163
  ```typescript
1162
1164
  // Simple text message
1163
- aery.sendUserMessage("What is 2+2?");
1165
+ pi.sendUserMessage("What is 2+2?");
1164
1166
 
1165
1167
  // With content array (text + images)
1166
- aery.sendUserMessage([
1168
+ pi.sendUserMessage([
1167
1169
  { type: "text", text: "Describe this image:" },
1168
1170
  { type: "image", source: { type: "base64", mediaType: "image/png", data: "..." } },
1169
1171
  ]);
1170
1172
 
1171
1173
  // During streaming - must specify delivery mode
1172
- aery.sendUserMessage("Focus on error handling", { deliverAs: "steer" });
1173
- aery.sendUserMessage("And then summarize", { deliverAs: "followUp" });
1174
+ pi.sendUserMessage("Focus on error handling", { deliverAs: "steer" });
1175
+ pi.sendUserMessage("And then summarize", { deliverAs: "followUp" });
1174
1176
  ```
1175
1177
 
1176
1178
  **Options:**
@@ -1190,7 +1192,7 @@ Persist extension state (does NOT participate in LLM context).
1190
1192
  aery.appendEntry("my-state", { count: 42 });
1191
1193
 
1192
1194
  // Restore on reload
1193
- aery.on("session_start", async (_event, ctx) => {
1195
+ pi.on("session_start", async (_event, ctx) => {
1194
1196
  for (const entry of ctx.sessionManager.getEntries()) {
1195
1197
  if (entry.type === "custom" && entry.customType === "my-state") {
1196
1198
  // Reconstruct from entry.data
@@ -1235,14 +1237,14 @@ const label = ctx.sessionManager.getLabel(entryId);
1235
1237
 
1236
1238
  Labels persist in the session and survive restarts. Use them to mark important points (turns, checkpoints) in the conversation tree.
1237
1239
 
1238
- ### aery.registerCommand(name, options)
1240
+ ### pi.registerCommand(name, options)
1239
1241
 
1240
1242
  Register a command.
1241
1243
 
1242
- If multiple extensions register the same command name, Aery keeps them all and assigns numeric invocation suffixes in load order, for example `/review:1` and `/review:2`.
1244
+ If multiple extensions register the same command name, pi keeps them all and assigns numeric invocation suffixes in load order, for example `/review:1` and `/review:2`.
1243
1245
 
1244
1246
  ```typescript
1245
- aery.registerCommand("stats", {
1247
+ pi.registerCommand("stats", {
1246
1248
  description: "Show session statistics",
1247
1249
  handler: async (args, ctx) => {
1248
1250
  const count = ctx.sessionManager.getEntries().length;
@@ -1256,7 +1258,7 @@ Optional: add argument auto-completion for `/command ...`:
1256
1258
  ```typescript
1257
1259
  import type { AutocompleteItem } from "@eminent337/aery-tui";
1258
1260
 
1259
- aery.registerCommand("deploy", {
1261
+ pi.registerCommand("deploy", {
1260
1262
  description: "Deploy to an environment",
1261
1263
  getArgumentCompletions: (prefix: string): AutocompleteItem[] | null => {
1262
1264
  const envs = ["dev", "staging", "prod"];
@@ -1404,17 +1406,17 @@ aery.events.on("my:event", (data) => { ... });
1404
1406
  aery.events.emit("my:event", { ... });
1405
1407
  ```
1406
1408
 
1407
- ### aery.registerProvider(name, config)
1409
+ ### pi.registerProvider(name, config)
1408
1410
 
1409
1411
  Register or override a model provider dynamically. Useful for proxies, custom endpoints, or team-wide model configurations.
1410
1412
 
1411
1413
  Calls made during the extension factory function are queued and applied once the runner initialises. Calls made after that — for example from a command handler following a user setup flow — take effect immediately without requiring a `/reload`.
1412
1414
 
1413
- If you need to discover models from a remote endpoint, prefer an async extension factory over deferring the fetch to `session_start`. Aery waits for the factory before startup continues, so the registered models are available immediately, including to `aery --list-models`.
1415
+ If you need to discover models from a remote endpoint, prefer an async extension factory over deferring the fetch to `session_start`. pi waits for the factory before startup continues, so the registered models are available immediately, including to `aery --list-models`.
1414
1416
 
1415
1417
  ```typescript
1416
1418
  // Register a new provider with custom models
1417
- aery.registerProvider("my-proxy", {
1419
+ pi.registerProvider("my-proxy", {
1418
1420
  baseUrl: "https://proxy.example.com",
1419
1421
  apiKey: "PROXY_API_KEY", // env var name or literal
1420
1422
  api: "anthropic-messages",
@@ -1432,12 +1434,12 @@ aery.registerProvider("my-proxy", {
1432
1434
  });
1433
1435
 
1434
1436
  // Override baseUrl for an existing provider (keeps all models)
1435
- aery.registerProvider("anthropic", {
1437
+ pi.registerProvider("anthropic", {
1436
1438
  baseUrl: "https://proxy.example.com"
1437
1439
  });
1438
1440
 
1439
1441
  // Register provider with OAuth support for /login
1440
- aery.registerProvider("corporate-ai", {
1442
+ pi.registerProvider("corporate-ai", {
1441
1443
  baseUrl: "https://ai.corp.com",
1442
1444
  api: "openai-responses",
1443
1445
  models: [...],
@@ -1479,7 +1481,7 @@ Remove a previously registered provider and its models. Built-in models that wer
1479
1481
  Like `registerProvider`, this takes effect immediately when called after the initial load phase, so a `/reload` is not required.
1480
1482
 
1481
1483
  ```typescript
1482
- aery.registerCommand("my-setup-teardown", {
1484
+ pi.registerCommand("my-setup-teardown", {
1483
1485
  description: "Remove the custom proxy provider",
1484
1486
  handler: async (_args, _ctx) => {
1485
1487
  aery.unregisterProvider("my-proxy");
@@ -1496,7 +1498,7 @@ export default function (aery: ExtensionAPI) {
1496
1498
  let items: string[] = [];
1497
1499
 
1498
1500
  // Reconstruct state from session
1499
- aery.on("session_start", async (_event, ctx) => {
1501
+ pi.on("session_start", async (_event, ctx) => {
1500
1502
  items = [];
1501
1503
  for (const entry of ctx.sessionManager.getBranch()) {
1502
1504
  if (entry.type === "message" && entry.message.role === "toolResult") {
@@ -1507,7 +1509,7 @@ export default function (aery: ExtensionAPI) {
1507
1509
  }
1508
1510
  });
1509
1511
 
1510
- aery.registerTool({
1512
+ pi.registerTool({
1511
1513
  name: "my_tool",
1512
1514
  // ...
1513
1515
  async execute(toolCallId, params, signal, onUpdate, ctx) {
@@ -1523,7 +1525,7 @@ export default function (aery: ExtensionAPI) {
1523
1525
 
1524
1526
  ## Custom Tools
1525
1527
 
1526
- Register tools the LLM can call via `aery.registerTool()`. Tools appear in the system prompt and can have custom rendering.
1528
+ Register tools the LLM can call via `pi.registerTool()`. Tools appear in the system prompt and can have custom rendering.
1527
1529
 
1528
1530
  Use `promptSnippet` for a short one-line entry in the `Available tools` section in the default system prompt. If omitted, custom tools are left out of that section.
1529
1531
 
@@ -1568,7 +1570,7 @@ import { Type } from "@sinclair/typebox";
1568
1570
  import { StringEnum } from "@eminent337/aery-ai";
1569
1571
  import { Text } from "@eminent337/aery-tui";
1570
1572
 
1571
- aery.registerTool({
1573
+ pi.registerTool({
1572
1574
  name: "my_tool",
1573
1575
  label: "My Tool",
1574
1576
  description: "What this tool does (shown to LLM)",
@@ -1631,12 +1633,12 @@ async execute(toolCallId, params) {
1631
1633
 
1632
1634
  **Important:** Use `StringEnum` from `@eminent337/aery-ai` for string enums. `Type.Union`/`Type.Literal` doesn't work with Google's API.
1633
1635
 
1634
- **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 Aery 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.
1636
+ **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.
1635
1637
 
1636
1638
  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 }]`.
1637
1639
 
1638
1640
  ```typescript
1639
- aery.registerTool({
1641
+ pi.registerTool({
1640
1642
  name: "edit",
1641
1643
  label: "Edit",
1642
1644
  description: "Edit a single file using exact text replacement",
@@ -1702,13 +1704,13 @@ See [examples/extensions/tool-override.ts](../examples/extensions/tool-override.
1702
1704
  **Your implementation must match the exact result shape**, including the `details` type. The UI and session logic depend on these shapes for rendering and state tracking.
1703
1705
 
1704
1706
  Built-in tool implementations:
1705
- - [read.ts](https://github.com/eminent337/aery/blob/main/packages/coding-agent/src/core/tools/read.ts) - `ReadToolDetails`
1706
- - [bash.ts](https://github.com/eminent337/aery/blob/main/packages/coding-agent/src/core/tools/bash.ts) - `BashToolDetails`
1707
- - [edit.ts](https://github.com/eminent337/aery/blob/main/packages/coding-agent/src/core/tools/edit.ts)
1708
- - [write.ts](https://github.com/eminent337/aery/blob/main/packages/coding-agent/src/core/tools/write.ts)
1709
- - [grep.ts](https://github.com/eminent337/aery/blob/main/packages/coding-agent/src/core/tools/grep.ts) - `GrepToolDetails`
1710
- - [find.ts](https://github.com/eminent337/aery/blob/main/packages/coding-agent/src/core/tools/find.ts) - `FindToolDetails`
1711
- - [ls.ts](https://github.com/eminent337/aery/blob/main/packages/coding-agent/src/core/tools/ls.ts) - `LsToolDetails`
1707
+ - [read.ts](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/tools/read.ts) - `ReadToolDetails`
1708
+ - [bash.ts](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/tools/bash.ts) - `BashToolDetails`
1709
+ - [edit.ts](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/tools/edit.ts)
1710
+ - [write.ts](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/tools/write.ts)
1711
+ - [grep.ts](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/tools/grep.ts) - `GrepToolDetails`
1712
+ - [find.ts](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/tools/find.ts) - `FindToolDetails`
1713
+ - [ls.ts](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/tools/ls.ts) - `LsToolDetails`
1712
1714
 
1713
1715
  ### Remote Execution
1714
1716
 
@@ -1726,7 +1728,7 @@ const remoteRead = createReadTool(cwd, {
1726
1728
  });
1727
1729
 
1728
1730
  // Register, checking flag at execution time
1729
- aery.registerTool({
1731
+ pi.registerTool({
1730
1732
  ...remoteRead,
1731
1733
  async execute(id, params, signal, onUpdate, _ctx) {
1732
1734
  const ssh = getSshConfig();
@@ -1741,7 +1743,7 @@ aery.registerTool({
1741
1743
 
1742
1744
  **Operations interfaces:** `ReadOperations`, `WriteOperations`, `EditOperations`, `BashOperations`, `LsOperations`, `GrepOperations`, `FindOperations`
1743
1745
 
1744
- For `user_bash`, extensions can reuse Aery's local shell backend via `createLocalBashOperations()` instead of reimplementing local process spawning, shell resolution, and process-tree termination.
1746
+ For `user_bash`, extensions can reuse pi's local shell backend via `createLocalBashOperations()` instead of reimplementing local process spawning, shell resolution, and process-tree termination.
1745
1747
 
1746
1748
  The bash tool also supports a spawn hook to adjust the command, cwd, or env before execution:
1747
1749
 
@@ -1819,11 +1821,11 @@ One extension can register multiple tools with shared state:
1819
1821
  export default function (aery: ExtensionAPI) {
1820
1822
  let connection = null;
1821
1823
 
1822
- aery.registerTool({ name: "db_connect", ... });
1823
- aery.registerTool({ name: "db_query", ... });
1824
- aery.registerTool({ name: "db_close", ... });
1824
+ pi.registerTool({ name: "db_connect", ... });
1825
+ pi.registerTool({ name: "db_query", ... });
1826
+ pi.registerTool({ name: "db_close", ... });
1825
1827
 
1826
- aery.on("session_shutdown", async () => {
1828
+ pi.on("session_shutdown", async () => {
1827
1829
  connection?.close();
1828
1830
  });
1829
1831
  }
@@ -1831,14 +1833,14 @@ export default function (aery: ExtensionAPI) {
1831
1833
 
1832
1834
  ### Custom Rendering
1833
1835
 
1834
- Tools can provide `renderCall` and `renderResult` for custom TUI display. See [tui.md](tui.md) for the full component API and [tool-execution.ts](https://github.com/eminent337/aery/blob/main/packages/coding-agent/src/modes/interactive/components/tool-execution.ts) for how tool rows are composed.
1836
+ Tools can provide `renderCall` and `renderResult` for custom TUI display. See [tui.md](tui.md) for the full component API and [tool-execution.ts](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/modes/interactive/components/tool-execution.ts) for how tool rows are composed.
1835
1837
 
1836
1838
  By default, tool output is wrapped in a `Box` that handles padding and background. A defined `renderCall` or `renderResult` must return a `Component`. If a slot renderer is not defined, `tool-execution.ts` uses fallback rendering for that slot.
1837
1839
 
1838
1840
  Set `renderShell: "self"` when the tool should render its own shell instead of using the default `Box`. This is useful for tools that need complete control over framing or background behavior, for example large previews that must stay visually stable after the tool settles.
1839
1841
 
1840
1842
  ```typescript
1841
- aery.registerTool({
1843
+ pi.registerTool({
1842
1844
  name: "my_tool",
1843
1845
  label: "My Tool",
1844
1846
  description: "Custom shell example",
@@ -2191,7 +2193,7 @@ class VimEditor extends CustomEditor {
2191
2193
  }
2192
2194
 
2193
2195
  export default function (aery: ExtensionAPI) {
2194
- aery.on("session_start", (_event, ctx) => {
2196
+ pi.on("session_start", (_event, ctx) => {
2195
2197
  ctx.ui.setEditorComponent((_tui, theme, keybindings) =>
2196
2198
  new VimEditor(theme, keybindings)
2197
2199
  );