@mariozechner/pi-coding-agent 0.37.8 → 0.39.0

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 (165) hide show
  1. package/CHANGELOG.md +115 -4
  2. package/README.md +11 -0
  3. package/dist/cli/args.d.ts +2 -0
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +8 -0
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/core/agent-session.d.ts +23 -0
  8. package/dist/core/agent-session.d.ts.map +1 -1
  9. package/dist/core/agent-session.js +75 -35
  10. package/dist/core/agent-session.js.map +1 -1
  11. package/dist/core/bash-executor.d.ts +6 -0
  12. package/dist/core/bash-executor.d.ts.map +1 -1
  13. package/dist/core/bash-executor.js +77 -0
  14. package/dist/core/bash-executor.js.map +1 -1
  15. package/dist/core/extensions/index.d.ts +3 -3
  16. package/dist/core/extensions/index.d.ts.map +1 -1
  17. package/dist/core/extensions/index.js +1 -1
  18. package/dist/core/extensions/index.js.map +1 -1
  19. package/dist/core/extensions/loader.d.ts +8 -6
  20. package/dist/core/extensions/loader.d.ts.map +1 -1
  21. package/dist/core/extensions/loader.js +94 -211
  22. package/dist/core/extensions/loader.js.map +1 -1
  23. package/dist/core/extensions/runner.d.ts +27 -30
  24. package/dist/core/extensions/runner.d.ts.map +1 -1
  25. package/dist/core/extensions/runner.js +102 -45
  26. package/dist/core/extensions/runner.js.map +1 -1
  27. package/dist/core/extensions/types.d.ts +155 -30
  28. package/dist/core/extensions/types.d.ts.map +1 -1
  29. package/dist/core/extensions/types.js.map +1 -1
  30. package/dist/core/extensions/wrapper.d.ts +5 -3
  31. package/dist/core/extensions/wrapper.d.ts.map +1 -1
  32. package/dist/core/extensions/wrapper.js +6 -4
  33. package/dist/core/extensions/wrapper.js.map +1 -1
  34. package/dist/core/index.d.ts +2 -2
  35. package/dist/core/index.d.ts.map +1 -1
  36. package/dist/core/index.js +1 -1
  37. package/dist/core/index.js.map +1 -1
  38. package/dist/core/model-resolver.d.ts +4 -2
  39. package/dist/core/model-resolver.d.ts.map +1 -1
  40. package/dist/core/model-resolver.js +8 -9
  41. package/dist/core/model-resolver.js.map +1 -1
  42. package/dist/core/sdk.d.ts +8 -5
  43. package/dist/core/sdk.d.ts.map +1 -1
  44. package/dist/core/sdk.js +39 -87
  45. package/dist/core/sdk.js.map +1 -1
  46. package/dist/core/settings-manager.d.ts +8 -0
  47. package/dist/core/settings-manager.d.ts.map +1 -1
  48. package/dist/core/settings-manager.js +9 -1
  49. package/dist/core/settings-manager.js.map +1 -1
  50. package/dist/core/system-prompt.d.ts.map +1 -1
  51. package/dist/core/system-prompt.js +1 -5
  52. package/dist/core/system-prompt.js.map +1 -1
  53. package/dist/core/tools/bash.d.ts +25 -1
  54. package/dist/core/tools/bash.d.ts.map +1 -1
  55. package/dist/core/tools/bash.js +103 -73
  56. package/dist/core/tools/bash.js.map +1 -1
  57. package/dist/core/tools/edit.d.ts +17 -1
  58. package/dist/core/tools/edit.d.ts.map +1 -1
  59. package/dist/core/tools/edit.js +12 -5
  60. package/dist/core/tools/edit.js.map +1 -1
  61. package/dist/core/tools/find.d.ts +18 -1
  62. package/dist/core/tools/find.d.ts.map +1 -1
  63. package/dist/core/tools/find.js +68 -18
  64. package/dist/core/tools/find.js.map +1 -1
  65. package/dist/core/tools/grep.d.ts +15 -1
  66. package/dist/core/tools/grep.d.ts.map +1 -1
  67. package/dist/core/tools/grep.js +22 -10
  68. package/dist/core/tools/grep.js.map +1 -1
  69. package/dist/core/tools/index.d.ts +7 -7
  70. package/dist/core/tools/index.d.ts.map +1 -1
  71. package/dist/core/tools/index.js +1 -1
  72. package/dist/core/tools/index.js.map +1 -1
  73. package/dist/core/tools/ls.d.ts +21 -1
  74. package/dist/core/tools/ls.d.ts.map +1 -1
  75. package/dist/core/tools/ls.js +80 -72
  76. package/dist/core/tools/ls.js.map +1 -1
  77. package/dist/core/tools/read.d.ts +14 -0
  78. package/dist/core/tools/read.d.ts.map +1 -1
  79. package/dist/core/tools/read.js +12 -5
  80. package/dist/core/tools/read.js.map +1 -1
  81. package/dist/core/tools/write.d.ts +15 -1
  82. package/dist/core/tools/write.d.ts.map +1 -1
  83. package/dist/core/tools/write.js +9 -4
  84. package/dist/core/tools/write.js.map +1 -1
  85. package/dist/index.d.ts +5 -4
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +4 -2
  88. package/dist/index.js.map +1 -1
  89. package/dist/main.d.ts.map +1 -1
  90. package/dist/main.js +58 -116
  91. package/dist/main.js.map +1 -1
  92. package/dist/modes/index.d.ts +2 -2
  93. package/dist/modes/index.d.ts.map +1 -1
  94. package/dist/modes/index.js.map +1 -1
  95. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  96. package/dist/modes/interactive/components/assistant-message.js +7 -3
  97. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  98. package/dist/modes/interactive/components/countdown-timer.d.ts +14 -0
  99. package/dist/modes/interactive/components/countdown-timer.d.ts.map +1 -0
  100. package/dist/modes/interactive/components/countdown-timer.js +33 -0
  101. package/dist/modes/interactive/components/countdown-timer.js.map +1 -0
  102. package/dist/modes/interactive/components/custom-editor.d.ts +1 -1
  103. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  104. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  105. package/dist/modes/interactive/components/extension-input.d.ts +10 -2
  106. package/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
  107. package/dist/modes/interactive/components/extension-input.js +18 -14
  108. package/dist/modes/interactive/components/extension-input.js.map +1 -1
  109. package/dist/modes/interactive/components/extension-selector.d.ts +10 -2
  110. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  111. package/dist/modes/interactive/components/extension-selector.js +18 -22
  112. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  113. package/dist/modes/interactive/components/tool-execution.d.ts +6 -0
  114. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  115. package/dist/modes/interactive/components/tool-execution.js +50 -23
  116. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  117. package/dist/modes/interactive/interactive-mode.d.ts +44 -3
  118. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  119. package/dist/modes/interactive/interactive-mode.js +440 -139
  120. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  121. package/dist/modes/interactive/theme/theme.d.ts +7 -0
  122. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  123. package/dist/modes/interactive/theme/theme.js +34 -0
  124. package/dist/modes/interactive/theme/theme.js.map +1 -1
  125. package/dist/modes/print-mode.d.ts +14 -7
  126. package/dist/modes/print-mode.d.ts.map +1 -1
  127. package/dist/modes/print-mode.js +45 -21
  128. package/dist/modes/print-mode.js.map +1 -1
  129. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  130. package/dist/modes/rpc/rpc-mode.js +111 -101
  131. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  132. package/dist/modes/rpc/rpc-types.d.ts +3 -0
  133. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  134. package/dist/modes/rpc/rpc-types.js.map +1 -1
  135. package/dist/utils/clipboard-image.d.ts.map +1 -1
  136. package/dist/utils/clipboard-image.js +1 -1
  137. package/dist/utils/clipboard-image.js.map +1 -1
  138. package/dist/utils/clipboard.d.ts.map +1 -1
  139. package/dist/utils/clipboard.js +35 -7
  140. package/dist/utils/clipboard.js.map +1 -1
  141. package/docs/extensions.md +211 -15
  142. package/docs/sdk.md +68 -9
  143. package/docs/tui.md +81 -4
  144. package/examples/extensions/README.md +3 -0
  145. package/examples/extensions/claude-rules.ts +5 -2
  146. package/examples/extensions/handoff.ts +1 -1
  147. package/examples/extensions/interactive-shell.ts +196 -0
  148. package/examples/extensions/mac-system-theme.ts +25 -0
  149. package/examples/extensions/modal-editor.ts +85 -0
  150. package/examples/extensions/overlay-test.ts +145 -0
  151. package/examples/extensions/pirate.ts +7 -4
  152. package/examples/extensions/preset.ts +3 -3
  153. package/examples/extensions/qna.ts +1 -1
  154. package/examples/extensions/rainbow-editor.ts +95 -0
  155. package/examples/extensions/shutdown-command.ts +63 -0
  156. package/examples/extensions/snake.ts +1 -1
  157. package/examples/extensions/ssh.ts +220 -0
  158. package/examples/extensions/timed-confirm.ts +32 -25
  159. package/examples/extensions/todo.ts +1 -1
  160. package/examples/extensions/tool-override.ts +143 -0
  161. package/examples/extensions/tools.ts +1 -1
  162. package/examples/extensions/with-deps/package-lock.json +2 -2
  163. package/examples/extensions/with-deps/package.json +1 -1
  164. package/examples/sdk/04-skills.ts +4 -1
  165. package/package.json +6 -6
@@ -0,0 +1,220 @@
1
+ /**
2
+ * SSH Remote Execution Example
3
+ *
4
+ * Demonstrates delegating tool operations to a remote machine via SSH.
5
+ * When --ssh is provided, read/write/edit/bash run on the remote.
6
+ *
7
+ * Usage:
8
+ * pi -e ./ssh.ts --ssh user@host
9
+ * pi -e ./ssh.ts --ssh user@host:/remote/path
10
+ *
11
+ * Requirements:
12
+ * - SSH key-based auth (no password prompts)
13
+ * - bash on remote
14
+ */
15
+
16
+ import { spawn } from "node:child_process";
17
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
18
+ import {
19
+ type BashOperations,
20
+ createBashTool,
21
+ createEditTool,
22
+ createReadTool,
23
+ createWriteTool,
24
+ type EditOperations,
25
+ type ReadOperations,
26
+ type WriteOperations,
27
+ } from "@mariozechner/pi-coding-agent";
28
+
29
+ function sshExec(remote: string, command: string): Promise<Buffer> {
30
+ return new Promise((resolve, reject) => {
31
+ const child = spawn("ssh", [remote, command], { stdio: ["ignore", "pipe", "pipe"] });
32
+ const chunks: Buffer[] = [];
33
+ const errChunks: Buffer[] = [];
34
+ child.stdout.on("data", (data) => chunks.push(data));
35
+ child.stderr.on("data", (data) => errChunks.push(data));
36
+ child.on("error", reject);
37
+ child.on("close", (code) => {
38
+ if (code !== 0) {
39
+ reject(new Error(`SSH failed (${code}): ${Buffer.concat(errChunks).toString()}`));
40
+ } else {
41
+ resolve(Buffer.concat(chunks));
42
+ }
43
+ });
44
+ });
45
+ }
46
+
47
+ function createRemoteReadOps(remote: string, remoteCwd: string, localCwd: string): ReadOperations {
48
+ const toRemote = (p: string) => p.replace(localCwd, remoteCwd);
49
+ return {
50
+ readFile: (p) => sshExec(remote, `cat ${JSON.stringify(toRemote(p))}`),
51
+ access: (p) => sshExec(remote, `test -r ${JSON.stringify(toRemote(p))}`).then(() => {}),
52
+ detectImageMimeType: async (p) => {
53
+ try {
54
+ const r = await sshExec(remote, `file --mime-type -b ${JSON.stringify(toRemote(p))}`);
55
+ const m = r.toString().trim();
56
+ return ["image/jpeg", "image/png", "image/gif", "image/webp"].includes(m) ? m : null;
57
+ } catch {
58
+ return null;
59
+ }
60
+ },
61
+ };
62
+ }
63
+
64
+ function createRemoteWriteOps(remote: string, remoteCwd: string, localCwd: string): WriteOperations {
65
+ const toRemote = (p: string) => p.replace(localCwd, remoteCwd);
66
+ return {
67
+ writeFile: async (p, content) => {
68
+ const b64 = Buffer.from(content).toString("base64");
69
+ await sshExec(remote, `echo ${JSON.stringify(b64)} | base64 -d > ${JSON.stringify(toRemote(p))}`);
70
+ },
71
+ mkdir: (dir) => sshExec(remote, `mkdir -p ${JSON.stringify(toRemote(dir))}`).then(() => {}),
72
+ };
73
+ }
74
+
75
+ function createRemoteEditOps(remote: string, remoteCwd: string, localCwd: string): EditOperations {
76
+ const r = createRemoteReadOps(remote, remoteCwd, localCwd);
77
+ const w = createRemoteWriteOps(remote, remoteCwd, localCwd);
78
+ return { readFile: r.readFile, access: r.access, writeFile: w.writeFile };
79
+ }
80
+
81
+ function createRemoteBashOps(remote: string, remoteCwd: string, localCwd: string): BashOperations {
82
+ const toRemote = (p: string) => p.replace(localCwd, remoteCwd);
83
+ return {
84
+ exec: (command, cwd, { onData, signal, timeout }) =>
85
+ new Promise((resolve, reject) => {
86
+ const cmd = `cd ${JSON.stringify(toRemote(cwd))} && ${command}`;
87
+ const child = spawn("ssh", [remote, cmd], { stdio: ["ignore", "pipe", "pipe"] });
88
+ let timedOut = false;
89
+ const timer = timeout
90
+ ? setTimeout(() => {
91
+ timedOut = true;
92
+ child.kill();
93
+ }, timeout * 1000)
94
+ : undefined;
95
+ child.stdout.on("data", onData);
96
+ child.stderr.on("data", onData);
97
+ child.on("error", (e) => {
98
+ if (timer) clearTimeout(timer);
99
+ reject(e);
100
+ });
101
+ const onAbort = () => child.kill();
102
+ signal?.addEventListener("abort", onAbort, { once: true });
103
+ child.on("close", (code) => {
104
+ if (timer) clearTimeout(timer);
105
+ signal?.removeEventListener("abort", onAbort);
106
+ if (signal?.aborted) reject(new Error("aborted"));
107
+ else if (timedOut) reject(new Error(`timeout:${timeout}`));
108
+ else resolve({ exitCode: code });
109
+ });
110
+ }),
111
+ };
112
+ }
113
+
114
+ export default function (pi: ExtensionAPI) {
115
+ pi.registerFlag("ssh", { description: "SSH remote: user@host or user@host:/path", type: "string" });
116
+
117
+ const localCwd = process.cwd();
118
+ const localRead = createReadTool(localCwd);
119
+ const localWrite = createWriteTool(localCwd);
120
+ const localEdit = createEditTool(localCwd);
121
+ const localBash = createBashTool(localCwd);
122
+
123
+ // Resolved lazily on session_start (CLI flags not available during factory)
124
+ let resolvedSsh: { remote: string; remoteCwd: string } | null = null;
125
+
126
+ const getSsh = () => resolvedSsh;
127
+
128
+ pi.registerTool({
129
+ ...localRead,
130
+ async execute(id, params, onUpdate, _ctx, signal) {
131
+ const ssh = getSsh();
132
+ if (ssh) {
133
+ const tool = createReadTool(localCwd, {
134
+ operations: createRemoteReadOps(ssh.remote, ssh.remoteCwd, localCwd),
135
+ });
136
+ return tool.execute(id, params, signal, onUpdate);
137
+ }
138
+ return localRead.execute(id, params, signal, onUpdate);
139
+ },
140
+ });
141
+
142
+ pi.registerTool({
143
+ ...localWrite,
144
+ async execute(id, params, onUpdate, _ctx, signal) {
145
+ const ssh = getSsh();
146
+ if (ssh) {
147
+ const tool = createWriteTool(localCwd, {
148
+ operations: createRemoteWriteOps(ssh.remote, ssh.remoteCwd, localCwd),
149
+ });
150
+ return tool.execute(id, params, signal, onUpdate);
151
+ }
152
+ return localWrite.execute(id, params, signal, onUpdate);
153
+ },
154
+ });
155
+
156
+ pi.registerTool({
157
+ ...localEdit,
158
+ async execute(id, params, onUpdate, _ctx, signal) {
159
+ const ssh = getSsh();
160
+ if (ssh) {
161
+ const tool = createEditTool(localCwd, {
162
+ operations: createRemoteEditOps(ssh.remote, ssh.remoteCwd, localCwd),
163
+ });
164
+ return tool.execute(id, params, signal, onUpdate);
165
+ }
166
+ return localEdit.execute(id, params, signal, onUpdate);
167
+ },
168
+ });
169
+
170
+ pi.registerTool({
171
+ ...localBash,
172
+ async execute(id, params, onUpdate, _ctx, signal) {
173
+ const ssh = getSsh();
174
+ if (ssh) {
175
+ const tool = createBashTool(localCwd, {
176
+ operations: createRemoteBashOps(ssh.remote, ssh.remoteCwd, localCwd),
177
+ });
178
+ return tool.execute(id, params, signal, onUpdate);
179
+ }
180
+ return localBash.execute(id, params, signal, onUpdate);
181
+ },
182
+ });
183
+
184
+ pi.on("session_start", async (_event, ctx) => {
185
+ // Resolve SSH config now that CLI flags are available
186
+ const arg = pi.getFlag("ssh") as string | undefined;
187
+ if (arg) {
188
+ if (arg.includes(":")) {
189
+ const [remote, path] = arg.split(":");
190
+ resolvedSsh = { remote, remoteCwd: path };
191
+ } else {
192
+ // No path given, evaluate pwd on remote
193
+ const remote = arg;
194
+ const pwd = (await sshExec(remote, "pwd")).toString().trim();
195
+ resolvedSsh = { remote, remoteCwd: pwd };
196
+ }
197
+ ctx.ui.setStatus("ssh", ctx.ui.theme.fg("accent", `SSH: ${resolvedSsh.remote}:${resolvedSsh.remoteCwd}`));
198
+ ctx.ui.notify(`SSH mode: ${resolvedSsh.remote}:${resolvedSsh.remoteCwd}`, "info");
199
+ }
200
+ });
201
+
202
+ // Handle user ! commands via SSH
203
+ pi.on("user_bash", (_event) => {
204
+ const ssh = getSsh();
205
+ if (!ssh) return; // No SSH, use local execution
206
+ return { operations: createRemoteBashOps(ssh.remote, ssh.remoteCwd, localCwd) };
207
+ });
208
+
209
+ // Replace local cwd with remote cwd in system prompt
210
+ pi.on("before_agent_start", async (event) => {
211
+ const ssh = getSsh();
212
+ if (ssh) {
213
+ const modified = event.systemPrompt.replace(
214
+ `Current working directory: ${localCwd}`,
215
+ `Current working directory: ${ssh.remoteCwd} (via SSH: ${ssh.remote})`,
216
+ );
217
+ return { systemPrompt: modified };
218
+ }
219
+ });
220
+ }
@@ -1,62 +1,69 @@
1
1
  /**
2
- * Example extension demonstrating AbortSignal for auto-dismissing dialogs.
2
+ * Example extension demonstrating timed dialogs with live countdown.
3
3
  *
4
4
  * Commands:
5
- * - /timed - Shows confirm dialog that auto-cancels after 5 seconds
6
- * - /timed-select - Shows select dialog that auto-cancels after 10 seconds
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)
7
8
  */
8
9
 
9
10
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
10
11
 
11
12
  export default function (pi: ExtensionAPI) {
13
+ // Simple approach: use timeout option (recommended)
12
14
  pi.registerCommand("timed", {
13
- description: "Show a timed confirmation dialog (auto-cancels in 5s)",
15
+ description: "Show a timed confirmation dialog (auto-cancels in 5s with countdown)",
14
16
  handler: async (_args, ctx) => {
15
- const controller = new AbortController();
16
- const timeoutId = setTimeout(() => controller.abort(), 5000);
17
-
18
- ctx.ui.notify("Dialog will auto-cancel in 5 seconds...", "info");
19
-
20
17
  const confirmed = await ctx.ui.confirm(
21
18
  "Timed Confirmation",
22
19
  "This dialog will auto-cancel in 5 seconds. Confirm?",
23
- { signal: controller.signal },
20
+ { timeout: 5000 },
24
21
  );
25
22
 
26
- clearTimeout(timeoutId);
27
-
28
23
  if (confirmed) {
29
24
  ctx.ui.notify("Confirmed by user!", "info");
30
- } else if (controller.signal.aborted) {
31
- ctx.ui.notify("Dialog timed out (auto-cancelled)", "warning");
32
25
  } else {
33
- ctx.ui.notify("Cancelled by user", "info");
26
+ ctx.ui.notify("Cancelled or timed out", "info");
34
27
  }
35
28
  },
36
29
  });
37
30
 
38
31
  pi.registerCommand("timed-select", {
39
- description: "Show a timed select dialog (auto-cancels in 10s)",
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)",
40
47
  handler: async (_args, ctx) => {
41
48
  const controller = new AbortController();
42
- const timeoutId = setTimeout(() => controller.abort(), 10000);
49
+ const timeoutId = setTimeout(() => controller.abort(), 5000);
43
50
 
44
- ctx.ui.notify("Select dialog will auto-cancel in 10 seconds...", "info");
51
+ ctx.ui.notify("Dialog will auto-cancel in 5 seconds...", "info");
45
52
 
46
- const choice = await ctx.ui.select(
47
- "Pick an option (auto-cancels in 10s)",
48
- ["Option A", "Option B", "Option C"],
53
+ const confirmed = await ctx.ui.confirm(
54
+ "Timed Confirmation",
55
+ "This dialog will auto-cancel in 5 seconds. Confirm?",
49
56
  { signal: controller.signal },
50
57
  );
51
58
 
52
59
  clearTimeout(timeoutId);
53
60
 
54
- if (choice) {
55
- ctx.ui.notify(`Selected: ${choice}`, "info");
61
+ if (confirmed) {
62
+ ctx.ui.notify("Confirmed by user!", "info");
56
63
  } else if (controller.signal.aborted) {
57
- ctx.ui.notify("Selection timed out", "warning");
64
+ ctx.ui.notify("Dialog timed out (auto-cancelled)", "warning");
58
65
  } else {
59
- ctx.ui.notify("Selection cancelled", "info");
66
+ ctx.ui.notify("Cancelled by user", "info");
60
67
  }
61
68
  },
62
69
  });
@@ -291,7 +291,7 @@ export default function (pi: ExtensionAPI) {
291
291
  return;
292
292
  }
293
293
 
294
- await ctx.ui.custom<void>((_tui, theme, done) => {
294
+ await ctx.ui.custom<void>((_tui, theme, _kb, done) => {
295
295
  return new TodoListComponent(todos, theme, () => done());
296
296
  });
297
297
  },
@@ -0,0 +1,143 @@
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 "@mariozechner/pi-ai";
24
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
25
+ import { Type } from "@sinclair/typebox";
26
+ import { appendFileSync, constants, readFileSync } from "fs";
27
+ import { access, readFile } from "fs/promises";
28
+ import { homedir } from "os";
29
+ import { join, resolve } from "path";
30
+
31
+ const LOG_FILE = join(homedir(), ".pi", "agent", "read-access.log");
32
+
33
+ // Paths that are blocked from reading
34
+ const BLOCKED_PATTERNS = [
35
+ /\.env$/,
36
+ /\.env\..+$/,
37
+ /secrets?\.(json|yaml|yml|toml)$/i,
38
+ /credentials?\.(json|yaml|yml|toml)$/i,
39
+ /\/\.ssh\//,
40
+ /\/\.aws\//,
41
+ /\/\.gnupg\//,
42
+ ];
43
+
44
+ function isBlockedPath(path: string): boolean {
45
+ return BLOCKED_PATTERNS.some((pattern) => pattern.test(path));
46
+ }
47
+
48
+ function logAccess(path: string, allowed: boolean, reason?: string) {
49
+ const timestamp = new Date().toISOString();
50
+ const status = allowed ? "ALLOWED" : "BLOCKED";
51
+ const msg = reason ? ` (${reason})` : "";
52
+ const line = `[${timestamp}] ${status}: ${path}${msg}\n`;
53
+
54
+ try {
55
+ appendFileSync(LOG_FILE, line);
56
+ } catch {
57
+ // Ignore logging errors
58
+ }
59
+ }
60
+
61
+ const readSchema = Type.Object({
62
+ path: Type.String({ description: "Path to the file to read (relative or absolute)" }),
63
+ offset: Type.Optional(Type.Number({ description: "Line number to start reading from (1-indexed)" })),
64
+ limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" })),
65
+ });
66
+
67
+ export default function (pi: ExtensionAPI) {
68
+ pi.registerTool({
69
+ name: "read", // Same name as built-in - this will override it
70
+ label: "read (audited)",
71
+ description:
72
+ "Read the contents of a file with access logging. Some sensitive paths (.env, secrets, credentials) are blocked.",
73
+ parameters: readSchema,
74
+
75
+ async execute(_toolCallId, params, _onUpdate, ctx) {
76
+ const { path, offset, limit } = params;
77
+ const absolutePath = resolve(ctx.cwd, path);
78
+
79
+ // Check if path is blocked
80
+ if (isBlockedPath(absolutePath)) {
81
+ logAccess(absolutePath, false, "matches blocked pattern");
82
+ return {
83
+ content: [
84
+ {
85
+ type: "text",
86
+ text: `Access denied: "${path}" matches a blocked pattern (sensitive file). This tool blocks access to .env files, secrets, credentials, and SSH/AWS/GPG directories.`,
87
+ },
88
+ ],
89
+ details: { blocked: true },
90
+ };
91
+ }
92
+
93
+ // Log allowed access
94
+ logAccess(absolutePath, true);
95
+
96
+ // Perform the actual read (simplified implementation)
97
+ try {
98
+ await access(absolutePath, constants.R_OK);
99
+ const content = await readFile(absolutePath, "utf-8");
100
+ const lines = content.split("\n");
101
+
102
+ // Apply offset and limit
103
+ const startLine = offset ? Math.max(0, offset - 1) : 0;
104
+ const endLine = limit ? startLine + limit : lines.length;
105
+ const selectedLines = lines.slice(startLine, endLine);
106
+
107
+ // Basic truncation (50KB limit)
108
+ let text = selectedLines.join("\n");
109
+ const maxBytes = 50 * 1024;
110
+ if (Buffer.byteLength(text, "utf-8") > maxBytes) {
111
+ text = `${text.slice(0, maxBytes)}\n\n[Output truncated at 50KB]`;
112
+ }
113
+
114
+ return {
115
+ content: [{ type: "text", text }] as TextContent[],
116
+ details: { lines: lines.length },
117
+ };
118
+ } catch (error: any) {
119
+ return {
120
+ content: [{ type: "text", text: `Error reading file: ${error.message}` }] as TextContent[],
121
+ details: { error: true },
122
+ };
123
+ }
124
+ },
125
+
126
+ // No renderCall/renderResult - uses built-in renderer automatically
127
+ // (syntax highlighting, line numbers, truncation warnings, etc.)
128
+ });
129
+
130
+ // Also register a command to view the access log
131
+ pi.registerCommand("read-log", {
132
+ description: "View the file access log",
133
+ handler: async (_args, ctx) => {
134
+ try {
135
+ const log = readFileSync(LOG_FILE, "utf-8");
136
+ const lines = log.trim().split("\n").slice(-20); // Last 20 entries
137
+ ctx.ui.notify(`Recent file access:\n${lines.join("\n")}`, "info");
138
+ } catch {
139
+ ctx.ui.notify("No access log found", "info");
140
+ }
141
+ },
142
+ });
143
+ }
@@ -69,7 +69,7 @@ export default function toolsExtension(pi: ExtensionAPI) {
69
69
  // Refresh tool list
70
70
  allTools = pi.getAllTools();
71
71
 
72
- await ctx.ui.custom((tui, theme, done) => {
72
+ await ctx.ui.custom((tui, theme, _kb, done) => {
73
73
  // Build settings items for each tool
74
74
  const items: SettingItem[] = allTools.map((tool) => ({
75
75
  id: tool,
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pi-extension-with-deps",
3
- "version": "1.1.8",
3
+ "version": "1.3.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pi-extension-with-deps",
9
- "version": "1.1.8",
9
+ "version": "1.3.0",
10
10
  "dependencies": {
11
11
  "ms": "^2.1.3"
12
12
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-extension-with-deps",
3
3
  "private": true,
4
- "version": "1.1.8",
4
+ "version": "1.3.0",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",
@@ -8,11 +8,14 @@
8
8
  import { createAgentSession, discoverSkills, SessionManager, type Skill } from "@mariozechner/pi-coding-agent";
9
9
 
10
10
  // Discover all skills from cwd/.pi/skills, ~/.pi/agent/skills, etc.
11
- const allSkills = discoverSkills();
11
+ const { skills: allSkills, warnings } = discoverSkills();
12
12
  console.log(
13
13
  "Discovered skills:",
14
14
  allSkills.map((s) => s.name),
15
15
  );
16
+ if (warnings.length > 0) {
17
+ console.log("Warnings:", warnings);
18
+ }
16
19
 
17
20
  // Filter to specific skills
18
21
  const filteredSkills = allSkills.filter((s) => s.name.includes("browser") || s.name.includes("search"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mariozechner/pi-coding-agent",
3
- "version": "0.37.8",
3
+ "version": "0.39.0",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "piConfig": {
@@ -38,10 +38,10 @@
38
38
  "prepublishOnly": "npm run clean && npm run build"
39
39
  },
40
40
  "dependencies": {
41
- "@crosscopy/clipboard": "^0.2.8",
42
- "@mariozechner/pi-agent-core": "^0.37.8",
43
- "@mariozechner/pi-ai": "^0.37.8",
44
- "@mariozechner/pi-tui": "^0.37.8",
41
+ "@mariozechner/clipboard": "^0.3.0",
42
+ "@mariozechner/pi-agent-core": "^0.39.0",
43
+ "@mariozechner/pi-ai": "^0.39.0",
44
+ "@mariozechner/pi-tui": "^0.39.0",
45
45
  "chalk": "^5.5.0",
46
46
  "cli-highlight": "^2.1.11",
47
47
  "diff": "^8.0.2",
@@ -58,7 +58,7 @@
58
58
  "@types/ms": "^2.1.0",
59
59
  "@types/node": "^24.3.0",
60
60
  "@types/proper-lockfile": "^4.1.4",
61
- "shx": "^0.3.4",
61
+ "shx": "^0.4.0",
62
62
  "typescript": "^5.7.3",
63
63
  "vitest": "^3.2.4"
64
64
  },