@eminent337/aery 0.67.68

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 (148) hide show
  1. package/CHANGELOG.md +3768 -0
  2. package/README.md +623 -0
  3. package/docs/compaction.md +394 -0
  4. package/docs/custom-provider.md +637 -0
  5. package/docs/development.md +71 -0
  6. package/docs/extensions.md +2368 -0
  7. package/docs/images/doom-extension.png +0 -0
  8. package/docs/images/exy.png +0 -0
  9. package/docs/images/interactive-mode.png +0 -0
  10. package/docs/images/tree-view.png +0 -0
  11. package/docs/json.md +82 -0
  12. package/docs/keybindings.md +197 -0
  13. package/docs/models.md +395 -0
  14. package/docs/packages.md +218 -0
  15. package/docs/prompt-templates.md +88 -0
  16. package/docs/providers.md +195 -0
  17. package/docs/rpc.md +1407 -0
  18. package/docs/sdk.md +1149 -0
  19. package/docs/session.md +412 -0
  20. package/docs/settings.md +247 -0
  21. package/docs/shell-aliases.md +13 -0
  22. package/docs/skills.md +232 -0
  23. package/docs/terminal-setup.md +106 -0
  24. package/docs/termux.md +127 -0
  25. package/docs/themes.md +295 -0
  26. package/docs/tmux.md +61 -0
  27. package/docs/tree.md +233 -0
  28. package/docs/tui.md +918 -0
  29. package/docs/windows.md +17 -0
  30. package/examples/README.md +25 -0
  31. package/examples/extensions/README.md +208 -0
  32. package/examples/extensions/antigravity-image-gen.ts +418 -0
  33. package/examples/extensions/auto-commit-on-exit.ts +49 -0
  34. package/examples/extensions/bash-spawn-hook.ts +30 -0
  35. package/examples/extensions/bookmark.ts +50 -0
  36. package/examples/extensions/built-in-tool-renderer.ts +249 -0
  37. package/examples/extensions/claude-rules.ts +86 -0
  38. package/examples/extensions/commands.ts +72 -0
  39. package/examples/extensions/confirm-destructive.ts +59 -0
  40. package/examples/extensions/custom-compaction.ts +127 -0
  41. package/examples/extensions/custom-footer.ts +64 -0
  42. package/examples/extensions/custom-header.ts +73 -0
  43. package/examples/extensions/custom-provider-anthropic/index.ts +604 -0
  44. package/examples/extensions/custom-provider-anthropic/package-lock.json +24 -0
  45. package/examples/extensions/custom-provider-anthropic/package.json +19 -0
  46. package/examples/extensions/custom-provider-gitlab-duo/index.ts +349 -0
  47. package/examples/extensions/custom-provider-gitlab-duo/package.json +16 -0
  48. package/examples/extensions/custom-provider-gitlab-duo/test.ts +82 -0
  49. package/examples/extensions/custom-provider-qwen-cli/index.ts +345 -0
  50. package/examples/extensions/custom-provider-qwen-cli/package.json +16 -0
  51. package/examples/extensions/dirty-repo-guard.ts +56 -0
  52. package/examples/extensions/doom-overlay/README.md +46 -0
  53. package/examples/extensions/doom-overlay/doom/build/doom.js +21 -0
  54. package/examples/extensions/doom-overlay/doom/build/doom.wasm +0 -0
  55. package/examples/extensions/doom-overlay/doom/build.sh +152 -0
  56. package/examples/extensions/doom-overlay/doom/doomgeneric_pi.c +72 -0
  57. package/examples/extensions/doom-overlay/doom-component.ts +132 -0
  58. package/examples/extensions/doom-overlay/doom-engine.ts +173 -0
  59. package/examples/extensions/doom-overlay/doom-keys.ts +104 -0
  60. package/examples/extensions/doom-overlay/index.ts +74 -0
  61. package/examples/extensions/doom-overlay/wad-finder.ts +51 -0
  62. package/examples/extensions/dynamic-resources/SKILL.md +8 -0
  63. package/examples/extensions/dynamic-resources/dynamic.json +79 -0
  64. package/examples/extensions/dynamic-resources/dynamic.md +5 -0
  65. package/examples/extensions/dynamic-resources/index.ts +15 -0
  66. package/examples/extensions/dynamic-tools.ts +74 -0
  67. package/examples/extensions/event-bus.ts +43 -0
  68. package/examples/extensions/file-trigger.ts +41 -0
  69. package/examples/extensions/git-checkpoint.ts +53 -0
  70. package/examples/extensions/handoff.ts +153 -0
  71. package/examples/extensions/hello.ts +26 -0
  72. package/examples/extensions/hidden-thinking-label.ts +53 -0
  73. package/examples/extensions/inline-bash.ts +94 -0
  74. package/examples/extensions/input-transform.ts +43 -0
  75. package/examples/extensions/interactive-shell.ts +196 -0
  76. package/examples/extensions/mac-system-theme.ts +47 -0
  77. package/examples/extensions/message-renderer.ts +59 -0
  78. package/examples/extensions/minimal-mode.ts +426 -0
  79. package/examples/extensions/modal-editor.ts +85 -0
  80. package/examples/extensions/model-status.ts +31 -0
  81. package/examples/extensions/notify.ts +55 -0
  82. package/examples/extensions/overlay-qa-tests.ts +1348 -0
  83. package/examples/extensions/overlay-test.ts +150 -0
  84. package/examples/extensions/permission-gate.ts +34 -0
  85. package/examples/extensions/pirate.ts +47 -0
  86. package/examples/extensions/plan-mode/README.md +65 -0
  87. package/examples/extensions/plan-mode/index.ts +340 -0
  88. package/examples/extensions/plan-mode/utils.ts +168 -0
  89. package/examples/extensions/preset.ts +430 -0
  90. package/examples/extensions/protected-paths.ts +30 -0
  91. package/examples/extensions/provider-payload.ts +18 -0
  92. package/examples/extensions/qna.ts +122 -0
  93. package/examples/extensions/question.ts +264 -0
  94. package/examples/extensions/questionnaire.ts +427 -0
  95. package/examples/extensions/rainbow-editor.ts +88 -0
  96. package/examples/extensions/reload-runtime.ts +37 -0
  97. package/examples/extensions/rpc-demo.ts +118 -0
  98. package/examples/extensions/sandbox/index.ts +321 -0
  99. package/examples/extensions/sandbox/package-lock.json +92 -0
  100. package/examples/extensions/sandbox/package.json +19 -0
  101. package/examples/extensions/send-user-message.ts +97 -0
  102. package/examples/extensions/session-name.ts +27 -0
  103. package/examples/extensions/shutdown-command.ts +63 -0
  104. package/examples/extensions/snake.ts +343 -0
  105. package/examples/extensions/space-invaders.ts +560 -0
  106. package/examples/extensions/ssh.ts +220 -0
  107. package/examples/extensions/status-line.ts +32 -0
  108. package/examples/extensions/subagent/README.md +172 -0
  109. package/examples/extensions/subagent/agents/planner.md +37 -0
  110. package/examples/extensions/subagent/agents/reviewer.md +35 -0
  111. package/examples/extensions/subagent/agents/scout.md +50 -0
  112. package/examples/extensions/subagent/agents/worker.md +24 -0
  113. package/examples/extensions/subagent/agents.ts +126 -0
  114. package/examples/extensions/subagent/index.ts +987 -0
  115. package/examples/extensions/subagent/prompts/implement-and-review.md +10 -0
  116. package/examples/extensions/subagent/prompts/implement.md +10 -0
  117. package/examples/extensions/subagent/prompts/scout-and-plan.md +9 -0
  118. package/examples/extensions/summarize.ts +206 -0
  119. package/examples/extensions/system-prompt-header.ts +17 -0
  120. package/examples/extensions/tic-tac-toe.ts +1008 -0
  121. package/examples/extensions/timed-confirm.ts +70 -0
  122. package/examples/extensions/titlebar-spinner.ts +58 -0
  123. package/examples/extensions/todo.ts +297 -0
  124. package/examples/extensions/tool-override.ts +144 -0
  125. package/examples/extensions/tools.ts +141 -0
  126. package/examples/extensions/trigger-compact.ts +50 -0
  127. package/examples/extensions/truncated-tool.ts +195 -0
  128. package/examples/extensions/widget-placement.ts +9 -0
  129. package/examples/extensions/with-deps/index.ts +32 -0
  130. package/examples/extensions/with-deps/package-lock.json +31 -0
  131. package/examples/extensions/with-deps/package.json +22 -0
  132. package/examples/extensions/working-indicator.ts +123 -0
  133. package/examples/rpc-extension-ui.ts +632 -0
  134. package/examples/sdk/01-minimal.ts +22 -0
  135. package/examples/sdk/02-custom-model.ts +49 -0
  136. package/examples/sdk/03-custom-prompt.ts +62 -0
  137. package/examples/sdk/04-skills.ts +55 -0
  138. package/examples/sdk/05-tools.ts +44 -0
  139. package/examples/sdk/06-extensions.ts +90 -0
  140. package/examples/sdk/07-context-files.ts +42 -0
  141. package/examples/sdk/08-prompt-templates.ts +51 -0
  142. package/examples/sdk/09-api-keys-and-oauth.ts +48 -0
  143. package/examples/sdk/10-settings.ts +53 -0
  144. package/examples/sdk/11-sessions.ts +48 -0
  145. package/examples/sdk/12-full-control.ts +73 -0
  146. package/examples/sdk/13-session-runtime.ts +67 -0
  147. package/examples/sdk/README.md +147 -0
  148. package/package.json +102 -0
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Example extension demonstrating timed dialogs with live countdown.
3
+ *
4
+ * Commands:
5
+ * - /timed - Shows confirm dialog that auto-cancels after 5 seconds with countdown
6
+ * - /timed-select - Shows select dialog that auto-cancels after 10 seconds with countdown
7
+ * - /timed-signal - Shows confirm using AbortSignal (manual approach)
8
+ */
9
+
10
+ import type { ExtensionAPI } from "@aryee/aery";
11
+
12
+ export default function (pi: ExtensionAPI) {
13
+ // Simple approach: use timeout option (recommended)
14
+ pi.registerCommand("timed", {
15
+ description: "Show a timed confirmation dialog (auto-cancels in 5s with countdown)",
16
+ handler: async (_args, ctx) => {
17
+ const confirmed = await ctx.ui.confirm(
18
+ "Timed Confirmation",
19
+ "This dialog will auto-cancel in 5 seconds. Confirm?",
20
+ { timeout: 5000 },
21
+ );
22
+
23
+ if (confirmed) {
24
+ ctx.ui.notify("Confirmed by user!", "info");
25
+ } else {
26
+ ctx.ui.notify("Cancelled or timed out", "info");
27
+ }
28
+ },
29
+ });
30
+
31
+ pi.registerCommand("timed-select", {
32
+ description: "Show a timed select dialog (auto-cancels in 10s with countdown)",
33
+ handler: async (_args, ctx) => {
34
+ const choice = await ctx.ui.select("Pick an option", ["Option A", "Option B", "Option C"], { timeout: 10000 });
35
+
36
+ if (choice) {
37
+ ctx.ui.notify(`Selected: ${choice}`, "info");
38
+ } else {
39
+ ctx.ui.notify("Selection cancelled or timed out", "info");
40
+ }
41
+ },
42
+ });
43
+
44
+ // Manual approach: use AbortSignal for more control
45
+ pi.registerCommand("timed-signal", {
46
+ description: "Show a timed confirm using AbortSignal (manual approach)",
47
+ handler: async (_args, ctx) => {
48
+ const controller = new AbortController();
49
+ const timeoutId = setTimeout(() => controller.abort(), 5000);
50
+
51
+ ctx.ui.notify("Dialog will auto-cancel in 5 seconds...", "info");
52
+
53
+ const confirmed = await ctx.ui.confirm(
54
+ "Timed Confirmation",
55
+ "This dialog will auto-cancel in 5 seconds. Confirm?",
56
+ { signal: controller.signal },
57
+ );
58
+
59
+ clearTimeout(timeoutId);
60
+
61
+ if (confirmed) {
62
+ ctx.ui.notify("Confirmed by user!", "info");
63
+ } else if (controller.signal.aborted) {
64
+ ctx.ui.notify("Dialog timed out (auto-cancelled)", "warning");
65
+ } else {
66
+ ctx.ui.notify("Cancelled by user", "info");
67
+ }
68
+ },
69
+ });
70
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Titlebar Spinner Extension
3
+ *
4
+ * Shows a braille spinner animation in the terminal title while the agent is working.
5
+ * Uses `ctx.ui.setTitle()` to update the terminal title via the extension API.
6
+ *
7
+ * Usage:
8
+ * pi --extension examples/extensions/titlebar-spinner.ts
9
+ */
10
+
11
+ import path from "node:path";
12
+ import type { ExtensionAPI, ExtensionContext } from "@aryee/aery";
13
+
14
+ const BRAILLE_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
15
+
16
+ function getBaseTitle(pi: ExtensionAPI): string {
17
+ const cwd = path.basename(process.cwd());
18
+ const session = pi.getSessionName();
19
+ return session ? `π - ${session} - ${cwd}` : `π - ${cwd}`;
20
+ }
21
+
22
+ export default function (pi: ExtensionAPI) {
23
+ let timer: ReturnType<typeof setInterval> | null = null;
24
+ let frameIndex = 0;
25
+
26
+ function stopAnimation(ctx: ExtensionContext) {
27
+ if (timer) {
28
+ clearInterval(timer);
29
+ timer = null;
30
+ }
31
+ frameIndex = 0;
32
+ ctx.ui.setTitle(getBaseTitle(pi));
33
+ }
34
+
35
+ function startAnimation(ctx: ExtensionContext) {
36
+ stopAnimation(ctx);
37
+ timer = setInterval(() => {
38
+ const frame = BRAILLE_FRAMES[frameIndex % BRAILLE_FRAMES.length];
39
+ const cwd = path.basename(process.cwd());
40
+ const session = pi.getSessionName();
41
+ const title = session ? `${frame} π - ${session} - ${cwd}` : `${frame} π - ${cwd}`;
42
+ ctx.ui.setTitle(title);
43
+ frameIndex++;
44
+ }, 80);
45
+ }
46
+
47
+ pi.on("agent_start", async (_event, ctx) => {
48
+ startAnimation(ctx);
49
+ });
50
+
51
+ pi.on("agent_end", async (_event, ctx) => {
52
+ stopAnimation(ctx);
53
+ });
54
+
55
+ pi.on("session_shutdown", async (_event, ctx) => {
56
+ stopAnimation(ctx);
57
+ });
58
+ }
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Todo Extension - Demonstrates state management via session entries
3
+ *
4
+ * This extension:
5
+ * - Registers a `todo` tool for the LLM to manage todos
6
+ * - Registers a `/todos` command for users to view the list
7
+ *
8
+ * State is stored in tool result details (not external files), which allows
9
+ * proper branching - when you branch, the todo state is automatically
10
+ * correct for that point in history.
11
+ */
12
+
13
+ import { StringEnum } from "@aryee/aery-ai";
14
+ import type { ExtensionAPI, ExtensionContext, Theme } from "@aryee/aery";
15
+ import { matchesKey, Text, truncateToWidth } from "@aryee/aery-tui";
16
+ import { Type } from "@sinclair/typebox";
17
+
18
+ interface Todo {
19
+ id: number;
20
+ text: string;
21
+ done: boolean;
22
+ }
23
+
24
+ interface TodoDetails {
25
+ action: "list" | "add" | "toggle" | "clear";
26
+ todos: Todo[];
27
+ nextId: number;
28
+ error?: string;
29
+ }
30
+
31
+ const TodoParams = Type.Object({
32
+ action: StringEnum(["list", "add", "toggle", "clear"] as const),
33
+ text: Type.Optional(Type.String({ description: "Todo text (for add)" })),
34
+ id: Type.Optional(Type.Number({ description: "Todo ID (for toggle)" })),
35
+ });
36
+
37
+ /**
38
+ * UI component for the /todos command
39
+ */
40
+ class TodoListComponent {
41
+ private todos: Todo[];
42
+ private theme: Theme;
43
+ private onClose: () => void;
44
+ private cachedWidth?: number;
45
+ private cachedLines?: string[];
46
+
47
+ constructor(todos: Todo[], theme: Theme, onClose: () => void) {
48
+ this.todos = todos;
49
+ this.theme = theme;
50
+ this.onClose = onClose;
51
+ }
52
+
53
+ handleInput(data: string): void {
54
+ if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
55
+ this.onClose();
56
+ }
57
+ }
58
+
59
+ render(width: number): string[] {
60
+ if (this.cachedLines && this.cachedWidth === width) {
61
+ return this.cachedLines;
62
+ }
63
+
64
+ const lines: string[] = [];
65
+ const th = this.theme;
66
+
67
+ lines.push("");
68
+ const title = th.fg("accent", " Todos ");
69
+ const headerLine =
70
+ th.fg("borderMuted", "─".repeat(3)) + title + th.fg("borderMuted", "─".repeat(Math.max(0, width - 10)));
71
+ lines.push(truncateToWidth(headerLine, width));
72
+ lines.push("");
73
+
74
+ if (this.todos.length === 0) {
75
+ lines.push(truncateToWidth(` ${th.fg("dim", "No todos yet. Ask the agent to add some!")}`, width));
76
+ } else {
77
+ const done = this.todos.filter((t) => t.done).length;
78
+ const total = this.todos.length;
79
+ lines.push(truncateToWidth(` ${th.fg("muted", `${done}/${total} completed`)}`, width));
80
+ lines.push("");
81
+
82
+ for (const todo of this.todos) {
83
+ const check = todo.done ? th.fg("success", "✓") : th.fg("dim", "○");
84
+ const id = th.fg("accent", `#${todo.id}`);
85
+ const text = todo.done ? th.fg("dim", todo.text) : th.fg("text", todo.text);
86
+ lines.push(truncateToWidth(` ${check} ${id} ${text}`, width));
87
+ }
88
+ }
89
+
90
+ lines.push("");
91
+ lines.push(truncateToWidth(` ${th.fg("dim", "Press Escape to close")}`, width));
92
+ lines.push("");
93
+
94
+ this.cachedWidth = width;
95
+ this.cachedLines = lines;
96
+ return lines;
97
+ }
98
+
99
+ invalidate(): void {
100
+ this.cachedWidth = undefined;
101
+ this.cachedLines = undefined;
102
+ }
103
+ }
104
+
105
+ export default function (pi: ExtensionAPI) {
106
+ // In-memory state (reconstructed from session on load)
107
+ let todos: Todo[] = [];
108
+ let nextId = 1;
109
+
110
+ /**
111
+ * Reconstruct state from session entries.
112
+ * Scans tool results for this tool and applies them in order.
113
+ */
114
+ const reconstructState = (ctx: ExtensionContext) => {
115
+ todos = [];
116
+ nextId = 1;
117
+
118
+ for (const entry of ctx.sessionManager.getBranch()) {
119
+ if (entry.type !== "message") continue;
120
+ const msg = entry.message;
121
+ if (msg.role !== "toolResult" || msg.toolName !== "todo") continue;
122
+
123
+ const details = msg.details as TodoDetails | undefined;
124
+ if (details) {
125
+ todos = details.todos;
126
+ nextId = details.nextId;
127
+ }
128
+ }
129
+ };
130
+
131
+ // Reconstruct state on session events
132
+ pi.on("session_start", async (_event, ctx) => reconstructState(ctx));
133
+ pi.on("session_tree", async (_event, ctx) => reconstructState(ctx));
134
+
135
+ // Register the todo tool for the LLM
136
+ pi.registerTool({
137
+ name: "todo",
138
+ label: "Todo",
139
+ description: "Manage a todo list. Actions: list, add (text), toggle (id), clear",
140
+ parameters: TodoParams,
141
+
142
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
143
+ switch (params.action) {
144
+ case "list":
145
+ return {
146
+ content: [
147
+ {
148
+ type: "text",
149
+ text: todos.length
150
+ ? todos.map((t) => `[${t.done ? "x" : " "}] #${t.id}: ${t.text}`).join("\n")
151
+ : "No todos",
152
+ },
153
+ ],
154
+ details: { action: "list", todos: [...todos], nextId } as TodoDetails,
155
+ };
156
+
157
+ case "add": {
158
+ if (!params.text) {
159
+ return {
160
+ content: [{ type: "text", text: "Error: text required for add" }],
161
+ details: { action: "add", todos: [...todos], nextId, error: "text required" } as TodoDetails,
162
+ };
163
+ }
164
+ const newTodo: Todo = { id: nextId++, text: params.text, done: false };
165
+ todos.push(newTodo);
166
+ return {
167
+ content: [{ type: "text", text: `Added todo #${newTodo.id}: ${newTodo.text}` }],
168
+ details: { action: "add", todos: [...todos], nextId } as TodoDetails,
169
+ };
170
+ }
171
+
172
+ case "toggle": {
173
+ if (params.id === undefined) {
174
+ return {
175
+ content: [{ type: "text", text: "Error: id required for toggle" }],
176
+ details: { action: "toggle", todos: [...todos], nextId, error: "id required" } as TodoDetails,
177
+ };
178
+ }
179
+ const todo = todos.find((t) => t.id === params.id);
180
+ if (!todo) {
181
+ return {
182
+ content: [{ type: "text", text: `Todo #${params.id} not found` }],
183
+ details: {
184
+ action: "toggle",
185
+ todos: [...todos],
186
+ nextId,
187
+ error: `#${params.id} not found`,
188
+ } as TodoDetails,
189
+ };
190
+ }
191
+ todo.done = !todo.done;
192
+ return {
193
+ content: [{ type: "text", text: `Todo #${todo.id} ${todo.done ? "completed" : "uncompleted"}` }],
194
+ details: { action: "toggle", todos: [...todos], nextId } as TodoDetails,
195
+ };
196
+ }
197
+
198
+ case "clear": {
199
+ const count = todos.length;
200
+ todos = [];
201
+ nextId = 1;
202
+ return {
203
+ content: [{ type: "text", text: `Cleared ${count} todos` }],
204
+ details: { action: "clear", todos: [], nextId: 1 } as TodoDetails,
205
+ };
206
+ }
207
+
208
+ default:
209
+ return {
210
+ content: [{ type: "text", text: `Unknown action: ${params.action}` }],
211
+ details: {
212
+ action: "list",
213
+ todos: [...todos],
214
+ nextId,
215
+ error: `unknown action: ${params.action}`,
216
+ } as TodoDetails,
217
+ };
218
+ }
219
+ },
220
+
221
+ renderCall(args, theme, _context) {
222
+ let text = theme.fg("toolTitle", theme.bold("todo ")) + theme.fg("muted", args.action);
223
+ if (args.text) text += ` ${theme.fg("dim", `"${args.text}"`)}`;
224
+ if (args.id !== undefined) text += ` ${theme.fg("accent", `#${args.id}`)}`;
225
+ return new Text(text, 0, 0);
226
+ },
227
+
228
+ renderResult(result, { expanded }, theme, _context) {
229
+ const details = result.details as TodoDetails | undefined;
230
+ if (!details) {
231
+ const text = result.content[0];
232
+ return new Text(text?.type === "text" ? text.text : "", 0, 0);
233
+ }
234
+
235
+ if (details.error) {
236
+ return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
237
+ }
238
+
239
+ const todoList = details.todos;
240
+
241
+ switch (details.action) {
242
+ case "list": {
243
+ if (todoList.length === 0) {
244
+ return new Text(theme.fg("dim", "No todos"), 0, 0);
245
+ }
246
+ let listText = theme.fg("muted", `${todoList.length} todo(s):`);
247
+ const display = expanded ? todoList : todoList.slice(0, 5);
248
+ for (const t of display) {
249
+ const check = t.done ? theme.fg("success", "✓") : theme.fg("dim", "○");
250
+ const itemText = t.done ? theme.fg("dim", t.text) : theme.fg("muted", t.text);
251
+ listText += `\n${check} ${theme.fg("accent", `#${t.id}`)} ${itemText}`;
252
+ }
253
+ if (!expanded && todoList.length > 5) {
254
+ listText += `\n${theme.fg("dim", `... ${todoList.length - 5} more`)}`;
255
+ }
256
+ return new Text(listText, 0, 0);
257
+ }
258
+
259
+ case "add": {
260
+ const added = todoList[todoList.length - 1];
261
+ return new Text(
262
+ theme.fg("success", "✓ Added ") +
263
+ theme.fg("accent", `#${added.id}`) +
264
+ " " +
265
+ theme.fg("muted", added.text),
266
+ 0,
267
+ 0,
268
+ );
269
+ }
270
+
271
+ case "toggle": {
272
+ const text = result.content[0];
273
+ const msg = text?.type === "text" ? text.text : "";
274
+ return new Text(theme.fg("success", "✓ ") + theme.fg("muted", msg), 0, 0);
275
+ }
276
+
277
+ case "clear":
278
+ return new Text(theme.fg("success", "✓ ") + theme.fg("muted", "Cleared all todos"), 0, 0);
279
+ }
280
+ },
281
+ });
282
+
283
+ // Register the /todos command for users
284
+ pi.registerCommand("todos", {
285
+ description: "Show all todos on the current branch",
286
+ handler: async (_args, ctx) => {
287
+ if (!ctx.hasUI) {
288
+ ctx.ui.notify("/todos requires interactive mode", "error");
289
+ return;
290
+ }
291
+
292
+ await ctx.ui.custom<void>((_tui, theme, _kb, done) => {
293
+ return new TodoListComponent(todos, theme, () => done());
294
+ });
295
+ },
296
+ });
297
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Tool Override Example - Demonstrates overriding built-in tools
3
+ *
4
+ * Extensions can register tools with the same name as built-in tools to replace them.
5
+ * This is useful for:
6
+ * - Adding logging or auditing to tool calls
7
+ * - Implementing access control or sandboxing
8
+ * - Routing tool calls to remote systems (e.g., pi-ssh-remote)
9
+ * - Modifying tool behavior for specific workflows
10
+ *
11
+ * This example overrides the `read` tool to:
12
+ * 1. Log all file access to a log file
13
+ * 2. Block access to sensitive paths (e.g., .env files)
14
+ * 3. Delegate to the original read implementation for allowed files
15
+ *
16
+ * Since no custom renderCall/renderResult are provided, the built-in renderer
17
+ * is used automatically (syntax highlighting, line numbers, truncation warnings).
18
+ *
19
+ * Usage:
20
+ * pi -e ./tool-override.ts
21
+ */
22
+
23
+ import type { TextContent } from "@aryee/aery-ai";
24
+ import { type ExtensionAPI, getAgentDir, withFileMutationQueue } from "@aryee/aery";
25
+ import { Type } from "@sinclair/typebox";
26
+ import { constants, readFileSync } from "fs";
27
+ import { access, appendFile, readFile } from "fs/promises";
28
+ import { join, resolve } from "path";
29
+
30
+ const LOG_FILE = join(getAgentDir(), "read-access.log");
31
+
32
+ // Paths that are blocked from reading
33
+ const BLOCKED_PATTERNS = [
34
+ /\.env$/,
35
+ /\.env\..+$/,
36
+ /secrets?\.(json|yaml|yml|toml)$/i,
37
+ /credentials?\.(json|yaml|yml|toml)$/i,
38
+ /\/\.ssh\//,
39
+ /\/\.aws\//,
40
+ /\/\.gnupg\//,
41
+ ];
42
+
43
+ function isBlockedPath(path: string): boolean {
44
+ return BLOCKED_PATTERNS.some((pattern) => pattern.test(path));
45
+ }
46
+
47
+ async function logAccess(path: string, allowed: boolean, reason?: string) {
48
+ const timestamp = new Date().toISOString();
49
+ const status = allowed ? "ALLOWED" : "BLOCKED";
50
+ const msg = reason ? ` (${reason})` : "";
51
+ const line = `[${timestamp}] ${status}: ${path}${msg}\n`;
52
+
53
+ try {
54
+ await withFileMutationQueue(LOG_FILE, async () => {
55
+ await appendFile(LOG_FILE, line);
56
+ });
57
+ } catch {
58
+ // Ignore logging errors
59
+ }
60
+ }
61
+
62
+ const readSchema = Type.Object({
63
+ path: Type.String({ description: "Path to the file to read (relative or absolute)" }),
64
+ offset: Type.Optional(Type.Number({ description: "Line number to start reading from (1-indexed)" })),
65
+ limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" })),
66
+ });
67
+
68
+ export default function (pi: ExtensionAPI) {
69
+ pi.registerTool({
70
+ name: "read", // Same name as built-in - this will override it
71
+ label: "read (audited)",
72
+ description:
73
+ "Read the contents of a file with access logging. Some sensitive paths (.env, secrets, credentials) are blocked.",
74
+ parameters: readSchema,
75
+
76
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
77
+ const { path, offset, limit } = params;
78
+ const absolutePath = resolve(ctx.cwd, path);
79
+
80
+ // Check if path is blocked
81
+ if (isBlockedPath(absolutePath)) {
82
+ await logAccess(absolutePath, false, "matches blocked pattern");
83
+ return {
84
+ content: [
85
+ {
86
+ type: "text",
87
+ text: `Access denied: "${path}" matches a blocked pattern (sensitive file). This tool blocks access to .env files, secrets, credentials, and SSH/AWS/GPG directories.`,
88
+ },
89
+ ],
90
+ details: { blocked: true },
91
+ };
92
+ }
93
+
94
+ // Log allowed access
95
+ await logAccess(absolutePath, true);
96
+
97
+ // Perform the actual read (simplified implementation)
98
+ try {
99
+ await access(absolutePath, constants.R_OK);
100
+ const content = await readFile(absolutePath, "utf-8");
101
+ const lines = content.split("\n");
102
+
103
+ // Apply offset and limit
104
+ const startLine = offset ? Math.max(0, offset - 1) : 0;
105
+ const endLine = limit ? startLine + limit : lines.length;
106
+ const selectedLines = lines.slice(startLine, endLine);
107
+
108
+ // Basic truncation (50KB limit)
109
+ let text = selectedLines.join("\n");
110
+ const maxBytes = 50 * 1024;
111
+ if (Buffer.byteLength(text, "utf-8") > maxBytes) {
112
+ text = `${text.slice(0, maxBytes)}\n\n[Output truncated at 50KB]`;
113
+ }
114
+
115
+ return {
116
+ content: [{ type: "text", text }] as TextContent[],
117
+ details: { lines: lines.length },
118
+ };
119
+ } catch (error: any) {
120
+ return {
121
+ content: [{ type: "text", text: `Error reading file: ${error.message}` }] as TextContent[],
122
+ details: { error: true },
123
+ };
124
+ }
125
+ },
126
+
127
+ // No renderCall/renderResult - uses built-in renderer automatically
128
+ // (syntax highlighting, line numbers, truncation warnings, etc.)
129
+ });
130
+
131
+ // Also register a command to view the access log
132
+ pi.registerCommand("read-log", {
133
+ description: "View the file access log",
134
+ handler: async (_args, ctx) => {
135
+ try {
136
+ const log = readFileSync(LOG_FILE, "utf-8");
137
+ const lines = log.trim().split("\n").slice(-20); // Last 20 entries
138
+ ctx.ui.notify(`Recent file access:\n${lines.join("\n")}`, "info");
139
+ } catch {
140
+ ctx.ui.notify("No access log found", "info");
141
+ }
142
+ },
143
+ });
144
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Tools Extension
3
+ *
4
+ * Provides a /tools command to enable/disable tools interactively.
5
+ * Tool selection persists across session reloads and respects branch navigation.
6
+ *
7
+ * Usage:
8
+ * 1. Copy this file to ~/.pi/agent/extensions/ or your project's .pi/extensions/
9
+ * 2. Use /tools to open the tool selector
10
+ */
11
+
12
+ import type { ExtensionAPI, ExtensionContext, ToolInfo } from "@aryee/aery";
13
+ import { getSettingsListTheme } from "@aryee/aery";
14
+ import { Container, type SettingItem, SettingsList } from "@aryee/aery-tui";
15
+
16
+ // State persisted to session
17
+ interface ToolsState {
18
+ enabledTools: string[];
19
+ }
20
+
21
+ export default function toolsExtension(pi: ExtensionAPI) {
22
+ // Track enabled tools
23
+ let enabledTools: Set<string> = new Set();
24
+ let allTools: ToolInfo[] = [];
25
+
26
+ // Persist current state
27
+ function persistState() {
28
+ pi.appendEntry<ToolsState>("tools-config", {
29
+ enabledTools: Array.from(enabledTools),
30
+ });
31
+ }
32
+
33
+ // Apply current tool selection
34
+ function applyTools() {
35
+ pi.setActiveTools(Array.from(enabledTools));
36
+ }
37
+
38
+ // Find the last tools-config entry in the current branch
39
+ function restoreFromBranch(ctx: ExtensionContext) {
40
+ allTools = pi.getAllTools();
41
+
42
+ // Get entries in current branch only
43
+ const branchEntries = ctx.sessionManager.getBranch();
44
+ let savedTools: string[] | undefined;
45
+
46
+ for (const entry of branchEntries) {
47
+ if (entry.type === "custom" && entry.customType === "tools-config") {
48
+ const data = entry.data as ToolsState | undefined;
49
+ if (data?.enabledTools) {
50
+ savedTools = data.enabledTools;
51
+ }
52
+ }
53
+ }
54
+
55
+ if (savedTools) {
56
+ // Restore saved tool selection (filter to only tools that still exist)
57
+ const allToolNames = allTools.map((t) => t.name);
58
+ enabledTools = new Set(savedTools.filter((t: string) => allToolNames.includes(t)));
59
+ applyTools();
60
+ } else {
61
+ // No saved state - sync with currently active tools
62
+ enabledTools = new Set(pi.getActiveTools());
63
+ }
64
+ }
65
+
66
+ // Register /tools command
67
+ pi.registerCommand("tools", {
68
+ description: "Enable/disable tools",
69
+ handler: async (_args, ctx) => {
70
+ // Refresh tool list
71
+ allTools = pi.getAllTools();
72
+
73
+ await ctx.ui.custom((tui, theme, _kb, done) => {
74
+ // Build settings items for each tool
75
+ const items: SettingItem[] = allTools.map((tool) => ({
76
+ id: tool.name,
77
+ label: tool.name,
78
+ currentValue: enabledTools.has(tool.name) ? "enabled" : "disabled",
79
+ values: ["enabled", "disabled"],
80
+ }));
81
+
82
+ const container = new Container();
83
+ container.addChild(
84
+ new (class {
85
+ render(_width: number) {
86
+ return [theme.fg("accent", theme.bold("Tool Configuration")), ""];
87
+ }
88
+ invalidate() {}
89
+ })(),
90
+ );
91
+
92
+ const settingsList = new SettingsList(
93
+ items,
94
+ Math.min(items.length + 2, 15),
95
+ getSettingsListTheme(),
96
+ (id, newValue) => {
97
+ // Update enabled state and apply immediately
98
+ if (newValue === "enabled") {
99
+ enabledTools.add(id);
100
+ } else {
101
+ enabledTools.delete(id);
102
+ }
103
+ applyTools();
104
+ persistState();
105
+ },
106
+ () => {
107
+ // Close dialog
108
+ done(undefined);
109
+ },
110
+ );
111
+
112
+ container.addChild(settingsList);
113
+
114
+ const component = {
115
+ render(width: number) {
116
+ return container.render(width);
117
+ },
118
+ invalidate() {
119
+ container.invalidate();
120
+ },
121
+ handleInput(data: string) {
122
+ settingsList.handleInput?.(data);
123
+ tui.requestRender();
124
+ },
125
+ };
126
+
127
+ return component;
128
+ });
129
+ },
130
+ });
131
+
132
+ // Restore state on session start
133
+ pi.on("session_start", async (_event, ctx) => {
134
+ restoreFromBranch(ctx);
135
+ });
136
+
137
+ // Restore state when navigating the session tree
138
+ pi.on("session_tree", async (_event, ctx) => {
139
+ restoreFromBranch(ctx);
140
+ });
141
+ }