@mariozechner/pi-coding-agent 0.34.2 → 0.35.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 (251) hide show
  1. package/CHANGELOG.md +204 -0
  2. package/README.md +233 -105
  3. package/dist/cli/args.d.ts +3 -4
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +13 -18
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/config.d.ts +2 -2
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +3 -3
  10. package/dist/config.js.map +1 -1
  11. package/dist/core/agent-session.d.ts +39 -50
  12. package/dist/core/agent-session.d.ts.map +1 -1
  13. package/dist/core/agent-session.js +166 -197
  14. package/dist/core/agent-session.js.map +1 -1
  15. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  16. package/dist/core/compaction/branch-summarization.js +3 -3
  17. package/dist/core/compaction/branch-summarization.js.map +1 -1
  18. package/dist/core/compaction/compaction.d.ts +1 -1
  19. package/dist/core/compaction/compaction.d.ts.map +1 -1
  20. package/dist/core/compaction/compaction.js +6 -5
  21. package/dist/core/compaction/compaction.js.map +1 -1
  22. package/dist/core/event-bus.d.ts +9 -0
  23. package/dist/core/event-bus.d.ts.map +1 -0
  24. package/dist/core/event-bus.js +25 -0
  25. package/dist/core/event-bus.js.map +1 -0
  26. package/dist/core/exec.d.ts +1 -1
  27. package/dist/core/exec.d.ts.map +1 -1
  28. package/dist/core/exec.js +1 -1
  29. package/dist/core/exec.js.map +1 -1
  30. package/dist/core/extensions/index.d.ts +10 -0
  31. package/dist/core/extensions/index.d.ts.map +1 -0
  32. package/dist/core/extensions/index.js +9 -0
  33. package/dist/core/extensions/index.js.map +1 -0
  34. package/dist/core/extensions/loader.d.ts +21 -0
  35. package/dist/core/extensions/loader.d.ts.map +1 -0
  36. package/dist/core/extensions/loader.js +400 -0
  37. package/dist/core/extensions/loader.js.map +1 -0
  38. package/dist/core/extensions/runner.d.ts +88 -0
  39. package/dist/core/extensions/runner.d.ts.map +1 -0
  40. package/dist/core/{hooks → extensions}/runner.js +52 -141
  41. package/dist/core/extensions/runner.js.map +1 -0
  42. package/dist/core/extensions/types.d.ts +461 -0
  43. package/dist/core/extensions/types.d.ts.map +1 -0
  44. package/dist/core/{hooks → extensions}/types.js +7 -4
  45. package/dist/core/extensions/types.js.map +1 -0
  46. package/dist/core/extensions/wrapper.d.ts +25 -0
  47. package/dist/core/extensions/wrapper.d.ts.map +1 -0
  48. package/dist/core/{hooks/tool-wrapper.js → extensions/wrapper.js} +39 -24
  49. package/dist/core/extensions/wrapper.js.map +1 -0
  50. package/dist/core/index.d.ts +2 -2
  51. package/dist/core/index.d.ts.map +1 -1
  52. package/dist/core/index.js +3 -2
  53. package/dist/core/index.js.map +1 -1
  54. package/dist/core/messages.d.ts +7 -7
  55. package/dist/core/messages.d.ts.map +1 -1
  56. package/dist/core/messages.js +4 -4
  57. package/dist/core/messages.js.map +1 -1
  58. package/dist/core/prompt-templates.d.ts +40 -0
  59. package/dist/core/prompt-templates.d.ts.map +1 -0
  60. package/dist/core/{slash-commands.js → prompt-templates.js} +31 -31
  61. package/dist/core/prompt-templates.js.map +1 -0
  62. package/dist/core/sdk.d.ts +29 -52
  63. package/dist/core/sdk.d.ts.map +1 -1
  64. package/dist/core/sdk.js +111 -211
  65. package/dist/core/sdk.js.map +1 -1
  66. package/dist/core/session-manager.d.ts +17 -17
  67. package/dist/core/session-manager.d.ts.map +1 -1
  68. package/dist/core/session-manager.js +25 -10
  69. package/dist/core/session-manager.js.map +1 -1
  70. package/dist/core/settings-manager.d.ts +3 -6
  71. package/dist/core/settings-manager.d.ts.map +1 -1
  72. package/dist/core/settings-manager.js +4 -11
  73. package/dist/core/settings-manager.js.map +1 -1
  74. package/dist/core/system-prompt.d.ts.map +1 -1
  75. package/dist/core/system-prompt.js +4 -2
  76. package/dist/core/system-prompt.js.map +1 -1
  77. package/dist/index.d.ts +4 -5
  78. package/dist/index.d.ts.map +1 -1
  79. package/dist/index.js +5 -6
  80. package/dist/index.js.map +1 -1
  81. package/dist/main.d.ts.map +1 -1
  82. package/dist/main.js +36 -33
  83. package/dist/main.js.map +1 -1
  84. package/dist/migrations.d.ts +7 -2
  85. package/dist/migrations.d.ts.map +1 -1
  86. package/dist/migrations.js +93 -4
  87. package/dist/migrations.js.map +1 -1
  88. package/dist/modes/interactive/components/bordered-loader.d.ts +1 -1
  89. package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -1
  90. package/dist/modes/interactive/components/bordered-loader.js +1 -1
  91. package/dist/modes/interactive/components/bordered-loader.js.map +1 -1
  92. package/dist/modes/interactive/components/branch-summary-message.d.ts +1 -1
  93. package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
  94. package/dist/modes/interactive/components/branch-summary-message.js +1 -1
  95. package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
  96. package/dist/modes/interactive/components/compaction-summary-message.d.ts +1 -1
  97. package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
  98. package/dist/modes/interactive/components/compaction-summary-message.js +1 -1
  99. package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
  100. package/dist/modes/interactive/components/custom-editor.d.ts +2 -2
  101. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  102. package/dist/modes/interactive/components/custom-editor.js +4 -4
  103. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  104. package/dist/modes/interactive/components/custom-message.d.ts +18 -0
  105. package/dist/modes/interactive/components/custom-message.d.ts.map +1 -0
  106. package/dist/modes/interactive/components/{hook-message.js → custom-message.js} +3 -3
  107. package/dist/modes/interactive/components/custom-message.js.map +1 -0
  108. package/dist/modes/interactive/components/dynamic-border.d.ts +2 -2
  109. package/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
  110. package/dist/modes/interactive/components/dynamic-border.js +2 -2
  111. package/dist/modes/interactive/components/dynamic-border.js.map +1 -1
  112. package/dist/modes/interactive/components/{hook-editor.d.ts → extension-editor.d.ts} +3 -3
  113. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -0
  114. package/dist/modes/interactive/components/{hook-editor.js → extension-editor.js} +4 -4
  115. package/dist/modes/interactive/components/extension-editor.js.map +1 -0
  116. package/dist/modes/interactive/components/{hook-input.d.ts → extension-input.d.ts} +3 -3
  117. package/dist/modes/interactive/components/extension-input.d.ts.map +1 -0
  118. package/dist/modes/interactive/components/{hook-input.js → extension-input.js} +3 -3
  119. package/dist/modes/interactive/components/extension-input.js.map +1 -0
  120. package/dist/modes/interactive/components/{hook-selector.d.ts → extension-selector.d.ts} +3 -3
  121. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -0
  122. package/dist/modes/interactive/components/{hook-selector.js → extension-selector.js} +3 -3
  123. package/dist/modes/interactive/components/extension-selector.js.map +1 -0
  124. package/dist/modes/interactive/components/footer.d.ts +3 -3
  125. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  126. package/dist/modes/interactive/components/footer.js +8 -8
  127. package/dist/modes/interactive/components/footer.js.map +1 -1
  128. package/dist/modes/interactive/components/tool-execution.d.ts +3 -3
  129. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  130. package/dist/modes/interactive/components/tool-execution.js +9 -9
  131. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  132. package/dist/modes/interactive/interactive-mode.d.ts +37 -44
  133. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  134. package/dist/modes/interactive/interactive-mode.js +143 -189
  135. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  136. package/dist/modes/print-mode.d.ts.map +1 -1
  137. package/dist/modes/print-mode.js +10 -33
  138. package/dist/modes/print-mode.js.map +1 -1
  139. package/dist/modes/rpc/rpc-client.d.ts +3 -3
  140. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  141. package/dist/modes/rpc/rpc-client.js +3 -3
  142. package/dist/modes/rpc/rpc-client.js.map +1 -1
  143. package/dist/modes/rpc/rpc-mode.d.ts +2 -2
  144. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  145. package/dist/modes/rpc/rpc-mode.js +33 -57
  146. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  147. package/dist/modes/rpc/rpc-types.d.ts +16 -16
  148. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  149. package/dist/modes/rpc/rpc-types.js.map +1 -1
  150. package/docs/extensions.md +1053 -0
  151. package/docs/rpc.md +4 -4
  152. package/docs/sdk.md +62 -93
  153. package/docs/session.md +22 -19
  154. package/docs/skills.md +1 -1
  155. package/docs/tui.md +1 -1
  156. package/examples/README.md +9 -15
  157. package/examples/extensions/README.md +141 -0
  158. package/examples/{hooks → extensions}/auto-commit-on-exit.ts +3 -3
  159. package/examples/extensions/chalk-logger.ts +26 -0
  160. package/examples/{hooks → extensions}/confirm-destructive.ts +3 -3
  161. package/examples/{hooks → extensions}/custom-compaction.ts +6 -6
  162. package/examples/{hooks → extensions}/dirty-repo-guard.ts +8 -4
  163. package/examples/{hooks → extensions}/file-trigger.ts +3 -3
  164. package/examples/{hooks → extensions}/git-checkpoint.ts +3 -3
  165. package/examples/{hooks → extensions}/handoff.ts +3 -3
  166. package/examples/extensions/hello.ts +25 -0
  167. package/examples/{hooks → extensions}/permission-gate.ts +3 -3
  168. package/examples/{hooks → extensions}/pirate.ts +5 -5
  169. package/examples/{hooks → extensions}/plan-mode.ts +6 -6
  170. package/examples/{hooks → extensions}/protected-paths.ts +3 -3
  171. package/examples/{hooks → extensions}/qna.ts +3 -3
  172. package/examples/{custom-tools/question/index.ts → extensions/question.ts} +13 -17
  173. package/examples/{hooks → extensions}/snake.ts +3 -3
  174. package/examples/{hooks → extensions}/status-line.ts +3 -3
  175. package/examples/{custom-tools → extensions}/subagent/README.md +15 -15
  176. package/examples/{custom-tools → extensions}/subagent/index.ts +22 -43
  177. package/examples/{custom-tools/todo/index.ts → extensions/todo.ts} +122 -39
  178. package/examples/{hooks → extensions}/tools.ts +5 -5
  179. package/examples/extensions/with-deps/index.ts +40 -0
  180. package/examples/extensions/with-deps/package-lock.json +31 -0
  181. package/examples/extensions/with-deps/package.json +16 -0
  182. package/examples/sdk/01-minimal.ts +1 -1
  183. package/examples/sdk/05-tools.ts +7 -41
  184. package/examples/sdk/06-extensions.ts +81 -0
  185. package/examples/sdk/08-prompt-templates.ts +42 -0
  186. package/examples/sdk/12-full-control.ts +10 -29
  187. package/examples/sdk/README.md +5 -5
  188. package/package.json +4 -4
  189. package/dist/core/custom-tools/index.d.ts +0 -7
  190. package/dist/core/custom-tools/index.d.ts.map +0 -1
  191. package/dist/core/custom-tools/index.js +0 -6
  192. package/dist/core/custom-tools/index.js.map +0 -1
  193. package/dist/core/custom-tools/loader.d.ts +0 -30
  194. package/dist/core/custom-tools/loader.d.ts.map +0 -1
  195. package/dist/core/custom-tools/loader.js +0 -276
  196. package/dist/core/custom-tools/loader.js.map +0 -1
  197. package/dist/core/custom-tools/types.d.ts +0 -144
  198. package/dist/core/custom-tools/types.d.ts.map +0 -1
  199. package/dist/core/custom-tools/types.js +0 -8
  200. package/dist/core/custom-tools/types.js.map +0 -1
  201. package/dist/core/custom-tools/wrapper.d.ts +0 -15
  202. package/dist/core/custom-tools/wrapper.d.ts.map +0 -1
  203. package/dist/core/custom-tools/wrapper.js +0 -23
  204. package/dist/core/custom-tools/wrapper.js.map +0 -1
  205. package/dist/core/hooks/index.d.ts +0 -6
  206. package/dist/core/hooks/index.d.ts.map +0 -1
  207. package/dist/core/hooks/index.js +0 -6
  208. package/dist/core/hooks/index.js.map +0 -1
  209. package/dist/core/hooks/loader.d.ts +0 -146
  210. package/dist/core/hooks/loader.d.ts.map +0 -1
  211. package/dist/core/hooks/loader.js +0 -275
  212. package/dist/core/hooks/loader.js.map +0 -1
  213. package/dist/core/hooks/runner.d.ts +0 -173
  214. package/dist/core/hooks/runner.d.ts.map +0 -1
  215. package/dist/core/hooks/runner.js.map +0 -1
  216. package/dist/core/hooks/tool-wrapper.d.ts +0 -17
  217. package/dist/core/hooks/tool-wrapper.d.ts.map +0 -1
  218. package/dist/core/hooks/tool-wrapper.js.map +0 -1
  219. package/dist/core/hooks/types.d.ts +0 -767
  220. package/dist/core/hooks/types.d.ts.map +0 -1
  221. package/dist/core/hooks/types.js.map +0 -1
  222. package/dist/core/slash-commands.d.ts +0 -40
  223. package/dist/core/slash-commands.d.ts.map +0 -1
  224. package/dist/core/slash-commands.js.map +0 -1
  225. package/dist/modes/interactive/components/hook-editor.d.ts.map +0 -1
  226. package/dist/modes/interactive/components/hook-editor.js.map +0 -1
  227. package/dist/modes/interactive/components/hook-input.d.ts.map +0 -1
  228. package/dist/modes/interactive/components/hook-input.js.map +0 -1
  229. package/dist/modes/interactive/components/hook-message.d.ts +0 -18
  230. package/dist/modes/interactive/components/hook-message.d.ts.map +0 -1
  231. package/dist/modes/interactive/components/hook-message.js.map +0 -1
  232. package/dist/modes/interactive/components/hook-selector.d.ts.map +0 -1
  233. package/dist/modes/interactive/components/hook-selector.js.map +0 -1
  234. package/docs/custom-tools.md +0 -514
  235. package/docs/extension-loading.md +0 -1004
  236. package/docs/hooks.md +0 -979
  237. package/docs/session-tree-plan.md +0 -441
  238. package/examples/custom-tools/README.md +0 -114
  239. package/examples/custom-tools/hello/index.ts +0 -21
  240. package/examples/hooks/README.md +0 -60
  241. package/examples/hooks/todo/index.ts +0 -134
  242. package/examples/sdk/06-hooks.ts +0 -61
  243. package/examples/sdk/08-slash-commands.ts +0 -42
  244. /package/examples/{custom-tools → extensions}/subagent/agents/planner.md +0 -0
  245. /package/examples/{custom-tools → extensions}/subagent/agents/reviewer.md +0 -0
  246. /package/examples/{custom-tools → extensions}/subagent/agents/scout.md +0 -0
  247. /package/examples/{custom-tools → extensions}/subagent/agents/worker.md +0 -0
  248. /package/examples/{custom-tools → extensions}/subagent/agents.ts +0 -0
  249. /package/examples/{custom-tools/subagent/commands → extensions/subagent/prompts}/implement-and-review.md +0 -0
  250. /package/examples/{custom-tools/subagent/commands → extensions/subagent/prompts}/implement.md +0 -0
  251. /package/examples/{custom-tools/subagent/commands → extensions/subagent/prompts}/scout-and-plan.md +0 -0
@@ -19,19 +19,13 @@ import * as path from "node:path";
19
19
  import type { AgentToolResult } from "@mariozechner/pi-agent-core";
20
20
  import type { Message } from "@mariozechner/pi-ai";
21
21
  import { StringEnum } from "@mariozechner/pi-ai";
22
- import {
23
- type CustomTool,
24
- type CustomToolAPI,
25
- type CustomToolFactory,
26
- getMarkdownTheme,
27
- } from "@mariozechner/pi-coding-agent";
22
+ import { type ExtensionAPI, getMarkdownTheme } from "@mariozechner/pi-coding-agent";
28
23
  import { Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
29
24
  import { Type } from "@sinclair/typebox";
30
- import { type AgentConfig, type AgentScope, discoverAgents, formatAgentList } from "./agents.js";
25
+ import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.js";
31
26
 
32
27
  const MAX_PARALLEL_TASKS = 8;
33
28
  const MAX_CONCURRENCY = 4;
34
- const MAX_AGENTS_IN_DESCRIPTION = 10;
35
29
  const COLLAPSED_ITEM_COUNT = 10;
36
30
 
37
31
  function formatTokens(count: number): string {
@@ -224,7 +218,7 @@ function writePromptToTempFile(agentName: string, prompt: string): { dir: string
224
218
  type OnUpdateCallback = (partial: AgentToolResult<SubagentDetails>) => void;
225
219
 
226
220
  async function runSingleAgent(
227
- pi: CustomToolAPI,
221
+ defaultCwd: string,
228
222
  agents: AgentConfig[],
229
223
  agentName: string,
230
224
  task: string,
@@ -289,7 +283,7 @@ async function runSingleAgent(
289
283
  let wasAborted = false;
290
284
 
291
285
  const exitCode = await new Promise<number>((resolve) => {
292
- const proc = spawn("pi", args, { cwd: cwd ?? pi.cwd, shell: false, stdio: ["ignore", "pipe", "pipe"] });
286
+ const proc = spawn("pi", args, { cwd: cwd ?? defaultCwd, shell: false, stdio: ["ignore", "pipe", "pipe"] });
293
287
  let buffer = "";
294
288
 
295
289
  const processLine = (line: string) => {
@@ -410,32 +404,21 @@ const SubagentParams = Type.Object({
410
404
  cwd: Type.Optional(Type.String({ description: "Working directory for the agent process (single mode)" })),
411
405
  });
412
406
 
413
- const factory: CustomToolFactory = (pi) => {
414
- const tool: CustomTool<typeof SubagentParams, SubagentDetails> = {
407
+ export default function (pi: ExtensionAPI) {
408
+ pi.registerTool({
415
409
  name: "subagent",
416
410
  label: "Subagent",
417
- get description() {
418
- const user = discoverAgents(pi.cwd, "user");
419
- const project = discoverAgents(pi.cwd, "project");
420
- const userList = formatAgentList(user.agents, MAX_AGENTS_IN_DESCRIPTION);
421
- const projectList = formatAgentList(project.agents, MAX_AGENTS_IN_DESCRIPTION);
422
- const userSuffix = userList.remaining > 0 ? `; ... and ${userList.remaining} more` : "";
423
- const projectSuffix = projectList.remaining > 0 ? `; ... and ${projectList.remaining} more` : "";
424
- const projectDirNote = project.projectAgentsDir ? ` (from ${project.projectAgentsDir})` : "";
425
- return [
426
- "Delegate tasks to specialized subagents with isolated context.",
427
- "Modes: single (agent + task), parallel (tasks array), chain (sequential with {previous} placeholder).",
428
- 'Default agent scope is "user" (from ~/.pi/agent/agents).',
429
- 'To enable project-local agents in .pi/agents, set agentScope: "both" (or "project").',
430
- `User agents: ${userList.text}${userSuffix}.`,
431
- `Project agents${projectDirNote}: ${projectList.text}${projectSuffix}.`,
432
- ].join(" ");
433
- },
411
+ description: [
412
+ "Delegate tasks to specialized subagents with isolated context.",
413
+ "Modes: single (agent + task), parallel (tasks array), chain (sequential with {previous} placeholder).",
414
+ 'Default agent scope is "user" (from ~/.pi/agent/agents).',
415
+ 'To enable project-local agents in .pi/agents, set agentScope: "both" (or "project").',
416
+ ].join(" "),
434
417
  parameters: SubagentParams,
435
418
 
436
- async execute(_toolCallId, params, onUpdate, _ctx, signal) {
419
+ async execute(_toolCallId, params, onUpdate, ctx, signal) {
437
420
  const agentScope: AgentScope = params.agentScope ?? "user";
438
- const discovery = discoverAgents(pi.cwd, agentScope);
421
+ const discovery = discoverAgents(ctx.cwd, agentScope);
439
422
  const agents = discovery.agents;
440
423
  const confirmProjectAgents = params.confirmProjectAgents ?? true;
441
424
 
@@ -466,7 +449,7 @@ const factory: CustomToolFactory = (pi) => {
466
449
  };
467
450
  }
468
451
 
469
- if ((agentScope === "project" || agentScope === "both") && confirmProjectAgents && pi.hasUI) {
452
+ if ((agentScope === "project" || agentScope === "both") && confirmProjectAgents && ctx.hasUI) {
470
453
  const requestedAgentNames = new Set<string>();
471
454
  if (params.chain) for (const step of params.chain) requestedAgentNames.add(step.agent);
472
455
  if (params.tasks) for (const t of params.tasks) requestedAgentNames.add(t.agent);
@@ -479,7 +462,7 @@ const factory: CustomToolFactory = (pi) => {
479
462
  if (projectAgentsRequested.length > 0) {
480
463
  const names = projectAgentsRequested.map((a) => a.name).join(", ");
481
464
  const dir = discovery.projectAgentsDir ?? "(unknown)";
482
- const ok = await pi.ui.confirm(
465
+ const ok = await ctx.ui.confirm(
483
466
  "Run project-local agents?",
484
467
  `Agents: ${names}\nSource: ${dir}\n\nProject agents are repo-controlled. Only continue for trusted repositories.`,
485
468
  );
@@ -515,7 +498,7 @@ const factory: CustomToolFactory = (pi) => {
515
498
  : undefined;
516
499
 
517
500
  const result = await runSingleAgent(
518
- pi,
501
+ ctx.cwd,
519
502
  agents,
520
503
  step.agent,
521
504
  taskWithContext,
@@ -589,7 +572,7 @@ const factory: CustomToolFactory = (pi) => {
589
572
 
590
573
  const results = await mapWithConcurrencyLimit(params.tasks, MAX_CONCURRENCY, async (t, index) => {
591
574
  const result = await runSingleAgent(
592
- pi,
575
+ ctx.cwd,
593
576
  agents,
594
577
  t.agent,
595
578
  t.task,
@@ -629,7 +612,7 @@ const factory: CustomToolFactory = (pi) => {
629
612
 
630
613
  if (params.agent && params.task) {
631
614
  const result = await runSingleAgent(
632
- pi,
615
+ ctx.cwd,
633
616
  agents,
634
617
  params.agent,
635
618
  params.task,
@@ -707,7 +690,7 @@ const factory: CustomToolFactory = (pi) => {
707
690
  },
708
691
 
709
692
  renderResult(result, { expanded }, theme) {
710
- const { details } = result;
693
+ const details = result.details as SubagentDetails | undefined;
711
694
  if (!details || details.results.length === 0) {
712
695
  const text = result.content[0];
713
696
  return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
@@ -976,9 +959,5 @@ const factory: CustomToolFactory = (pi) => {
976
959
  const text = result.content[0];
977
960
  return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
978
961
  },
979
- };
980
-
981
- return tool;
982
- };
983
-
984
- export default factory;
962
+ });
963
+ }
@@ -1,21 +1,18 @@
1
1
  /**
2
- * Todo Tool - Demonstrates state management via session entries
2
+ * Todo Extension - Demonstrates state management via session entries
3
3
  *
4
- * This tool stores state in tool result details (not external files),
5
- * which allows proper branching - when you branch, the todo state
6
- * is automatically correct for that point in history.
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
7
  *
8
- * The onSession callback reconstructs state by scanning past tool results.
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.
9
11
  */
10
12
 
11
13
  import { StringEnum } from "@mariozechner/pi-ai";
12
- import type {
13
- CustomTool,
14
- CustomToolContext,
15
- CustomToolFactory,
16
- CustomToolSessionEvent,
17
- } from "@mariozechner/pi-coding-agent";
18
- import { Text } from "@mariozechner/pi-tui";
14
+ import type { ExtensionAPI, ExtensionContext, Theme } from "@mariozechner/pi-coding-agent";
15
+ import { matchesKey, Text, truncateToWidth } from "@mariozechner/pi-tui";
19
16
  import { Type } from "@sinclair/typebox";
20
17
 
21
18
  interface Todo {
@@ -24,7 +21,6 @@ interface Todo {
24
21
  done: boolean;
25
22
  }
26
23
 
27
- // State stored in tool result details
28
24
  interface TodoDetails {
29
25
  action: "list" | "add" | "toggle" | "clear";
30
26
  todos: Todo[];
@@ -32,14 +28,81 @@ interface TodoDetails {
32
28
  error?: string;
33
29
  }
34
30
 
35
- // Define schema separately for proper type inference
36
31
  const TodoParams = Type.Object({
37
32
  action: StringEnum(["list", "add", "toggle", "clear"] as const),
38
33
  text: Type.Optional(Type.String({ description: "Todo text (for add)" })),
39
34
  id: Type.Optional(Type.Number({ description: "Todo ID (for toggle)" })),
40
35
  });
41
36
 
42
- const factory: CustomToolFactory = (_pi) => {
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) {
43
106
  // In-memory state (reconstructed from session on load)
44
107
  let todos: Todo[] = [];
45
108
  let nextId = 1;
@@ -48,18 +111,14 @@ const factory: CustomToolFactory = (_pi) => {
48
111
  * Reconstruct state from session entries.
49
112
  * Scans tool results for this tool and applies them in order.
50
113
  */
51
- const reconstructState = (_event: CustomToolSessionEvent, ctx: CustomToolContext) => {
114
+ const reconstructState = (ctx: ExtensionContext) => {
52
115
  todos = [];
53
116
  nextId = 1;
54
117
 
55
- // Use getBranch() to get entries on the current branch
56
118
  for (const entry of ctx.sessionManager.getBranch()) {
57
119
  if (entry.type !== "message") continue;
58
120
  const msg = entry.message;
59
-
60
- // Tool results have role "toolResult"
61
- if (msg.role !== "toolResult") continue;
62
- if (msg.toolName !== "todo") continue;
121
+ if (msg.role !== "toolResult" || msg.toolName !== "todo") continue;
63
122
 
64
123
  const details = msg.details as TodoDetails | undefined;
65
124
  if (details) {
@@ -69,15 +128,19 @@ const factory: CustomToolFactory = (_pi) => {
69
128
  }
70
129
  };
71
130
 
72
- const tool: CustomTool<typeof TodoParams, TodoDetails> = {
131
+ // Reconstruct state on session events
132
+ pi.on("session_start", async (_event, ctx) => reconstructState(ctx));
133
+ pi.on("session_switch", async (_event, ctx) => reconstructState(ctx));
134
+ pi.on("session_branch", async (_event, ctx) => reconstructState(ctx));
135
+ pi.on("session_tree", async (_event, ctx) => reconstructState(ctx));
136
+
137
+ // Register the todo tool for the LLM
138
+ pi.registerTool({
73
139
  name: "todo",
74
140
  label: "Todo",
75
141
  description: "Manage a todo list. Actions: list, add (text), toggle (id), clear",
76
142
  parameters: TodoParams,
77
143
 
78
- // Called on session start/switch/branch/clear
79
- onSession: reconstructState,
80
-
81
144
  async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
82
145
  switch (params.action) {
83
146
  case "list":
@@ -90,21 +153,21 @@ const factory: CustomToolFactory = (_pi) => {
90
153
  : "No todos",
91
154
  },
92
155
  ],
93
- details: { action: "list", todos: [...todos], nextId },
156
+ details: { action: "list", todos: [...todos], nextId } as TodoDetails,
94
157
  };
95
158
 
96
159
  case "add": {
97
160
  if (!params.text) {
98
161
  return {
99
162
  content: [{ type: "text", text: "Error: text required for add" }],
100
- details: { action: "add", todos: [...todos], nextId, error: "text required" },
163
+ details: { action: "add", todos: [...todos], nextId, error: "text required" } as TodoDetails,
101
164
  };
102
165
  }
103
166
  const newTodo: Todo = { id: nextId++, text: params.text, done: false };
104
167
  todos.push(newTodo);
105
168
  return {
106
169
  content: [{ type: "text", text: `Added todo #${newTodo.id}: ${newTodo.text}` }],
107
- details: { action: "add", todos: [...todos], nextId },
170
+ details: { action: "add", todos: [...todos], nextId } as TodoDetails,
108
171
  };
109
172
  }
110
173
 
@@ -112,20 +175,25 @@ const factory: CustomToolFactory = (_pi) => {
112
175
  if (params.id === undefined) {
113
176
  return {
114
177
  content: [{ type: "text", text: "Error: id required for toggle" }],
115
- details: { action: "toggle", todos: [...todos], nextId, error: "id required" },
178
+ details: { action: "toggle", todos: [...todos], nextId, error: "id required" } as TodoDetails,
116
179
  };
117
180
  }
118
181
  const todo = todos.find((t) => t.id === params.id);
119
182
  if (!todo) {
120
183
  return {
121
184
  content: [{ type: "text", text: `Todo #${params.id} not found` }],
122
- details: { action: "toggle", todos: [...todos], nextId, error: `#${params.id} not found` },
185
+ details: {
186
+ action: "toggle",
187
+ todos: [...todos],
188
+ nextId,
189
+ error: `#${params.id} not found`,
190
+ } as TodoDetails,
123
191
  };
124
192
  }
125
193
  todo.done = !todo.done;
126
194
  return {
127
195
  content: [{ type: "text", text: `Todo #${todo.id} ${todo.done ? "completed" : "uncompleted"}` }],
128
- details: { action: "toggle", todos: [...todos], nextId },
196
+ details: { action: "toggle", todos: [...todos], nextId } as TodoDetails,
129
197
  };
130
198
  }
131
199
 
@@ -135,14 +203,19 @@ const factory: CustomToolFactory = (_pi) => {
135
203
  nextId = 1;
136
204
  return {
137
205
  content: [{ type: "text", text: `Cleared ${count} todos` }],
138
- details: { action: "clear", todos: [], nextId: 1 },
206
+ details: { action: "clear", todos: [], nextId: 1 } as TodoDetails,
139
207
  };
140
208
  }
141
209
 
142
210
  default:
143
211
  return {
144
212
  content: [{ type: "text", text: `Unknown action: ${params.action}` }],
145
- details: { action: "list", todos: [...todos], nextId, error: `unknown action: ${params.action}` },
213
+ details: {
214
+ action: "list",
215
+ todos: [...todos],
216
+ nextId,
217
+ error: `unknown action: ${params.action}`,
218
+ } as TodoDetails,
146
219
  };
147
220
  }
148
221
  },
@@ -155,13 +228,12 @@ const factory: CustomToolFactory = (_pi) => {
155
228
  },
156
229
 
157
230
  renderResult(result, { expanded }, theme) {
158
- const { details } = result;
231
+ const details = result.details as TodoDetails | undefined;
159
232
  if (!details) {
160
233
  const text = result.content[0];
161
234
  return new Text(text?.type === "text" ? text.text : "", 0, 0);
162
235
  }
163
236
 
164
- // Error
165
237
  if (details.error) {
166
238
  return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
167
239
  }
@@ -208,9 +280,20 @@ const factory: CustomToolFactory = (_pi) => {
208
280
  return new Text(theme.fg("success", "✓ ") + theme.fg("muted", "Cleared all todos"), 0, 0);
209
281
  }
210
282
  },
211
- };
283
+ });
212
284
 
213
- return tool;
214
- };
285
+ // Register the /todos command for users
286
+ pi.registerCommand("todos", {
287
+ description: "Show all todos on the current branch",
288
+ handler: async (_args, ctx) => {
289
+ if (!ctx.hasUI) {
290
+ ctx.ui.notify("/todos requires interactive mode", "error");
291
+ return;
292
+ }
215
293
 
216
- export default factory;
294
+ await ctx.ui.custom<void>((_tui, theme, done) => {
295
+ return new TodoListComponent(todos, theme, () => done());
296
+ });
297
+ },
298
+ });
299
+ }
@@ -1,16 +1,16 @@
1
1
  /**
2
- * Tools Hook
2
+ * Tools Extension
3
3
  *
4
4
  * Provides a /tools command to enable/disable tools interactively.
5
5
  * Tool selection persists across session reloads and respects branch navigation.
6
6
  *
7
7
  * Usage:
8
- * 1. Copy this file to ~/.pi/agent/hooks/ or your project's .pi/hooks/
8
+ * 1. Copy this file to ~/.pi/agent/extensions/ or your project's .pi/extensions/
9
9
  * 2. Use /tools to open the tool selector
10
10
  */
11
11
 
12
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
12
13
  import { getSettingsListTheme } from "@mariozechner/pi-coding-agent";
13
- import type { HookAPI, HookContext } from "@mariozechner/pi-coding-agent/hooks";
14
14
  import { Container, type SettingItem, SettingsList } from "@mariozechner/pi-tui";
15
15
 
16
16
  // State persisted to session
@@ -18,7 +18,7 @@ interface ToolsState {
18
18
  enabledTools: string[];
19
19
  }
20
20
 
21
- export default function toolsHook(pi: HookAPI) {
21
+ export default function toolsExtension(pi: ExtensionAPI) {
22
22
  // Track enabled tools
23
23
  let enabledTools: Set<string> = new Set();
24
24
  let allTools: string[] = [];
@@ -36,7 +36,7 @@ export default function toolsHook(pi: HookAPI) {
36
36
  }
37
37
 
38
38
  // Find the last tools-config entry in the current branch
39
- function restoreFromBranch(ctx: HookContext) {
39
+ function restoreFromBranch(ctx: ExtensionContext) {
40
40
  allTools = pi.getAllTools();
41
41
 
42
42
  // Get entries in current branch only
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Example extension with its own npm dependencies.
3
+ * Tests that jiti resolves modules from the extension's own node_modules.
4
+ *
5
+ * Requires: npm install in this directory
6
+ */
7
+
8
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
9
+ import { Type } from "@sinclair/typebox";
10
+ import ms from "ms";
11
+
12
+ export default function (pi: ExtensionAPI) {
13
+ // Use the ms package to prove it loaded
14
+ const uptime = ms(process.uptime() * 1000, { long: true });
15
+ console.log(`[with-deps] Extension loaded. Process uptime: ${uptime}`);
16
+
17
+ // Register a tool that uses ms
18
+ pi.registerTool({
19
+ name: "parse_duration",
20
+ label: "Parse Duration",
21
+ description: "Parse a human-readable duration string (e.g., '2 days', '1h', '5m') to milliseconds",
22
+ parameters: Type.Object({
23
+ duration: Type.String({ description: "Duration string like '2 days', '1h', '5m'" }),
24
+ }),
25
+ execute: async (_toolCallId, params) => {
26
+ const result = ms(params.duration as ms.StringValue);
27
+ if (result === undefined) {
28
+ return {
29
+ content: [{ type: "text", text: `Invalid duration: "${params.duration}"` }],
30
+ isError: true,
31
+ details: {},
32
+ };
33
+ }
34
+ return {
35
+ content: [{ type: "text", text: `${params.duration} = ${result} milliseconds` }],
36
+ details: {},
37
+ };
38
+ },
39
+ });
40
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "pi-extension-with-deps",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "pi-extension-with-deps",
9
+ "version": "1.0.0",
10
+ "dependencies": {
11
+ "ms": "^2.1.3"
12
+ },
13
+ "devDependencies": {
14
+ "@types/ms": "^2.1.0"
15
+ }
16
+ },
17
+ "node_modules/@types/ms": {
18
+ "version": "2.1.0",
19
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
20
+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
21
+ "dev": true,
22
+ "license": "MIT"
23
+ },
24
+ "node_modules/ms": {
25
+ "version": "2.1.3",
26
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
27
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
28
+ "license": "MIT"
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "pi-extension-with-deps",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "pi": {
6
+ "extensions": [
7
+ "./index.ts"
8
+ ]
9
+ },
10
+ "dependencies": {
11
+ "ms": "^2.1.3"
12
+ },
13
+ "devDependencies": {
14
+ "@types/ms": "^2.1.0"
15
+ }
16
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Minimal SDK Usage
3
3
  *
4
- * Uses all defaults: discovers skills, hooks, tools, context files
4
+ * Uses all defaults: discovers skills, extensions, tools, context files
5
5
  * from cwd and ~/.pi/agent. Model chosen from settings or first available.
6
6
  */
7
7
 
@@ -1,27 +1,28 @@
1
1
  /**
2
2
  * Tools Configuration
3
3
  *
4
- * Use built-in tool sets, individual tools, or add custom tools.
4
+ * Use built-in tool sets or individual tools.
5
5
  *
6
6
  * IMPORTANT: When using a custom `cwd`, you must use the tool factory functions
7
7
  * (createCodingTools, createReadOnlyTools, createReadTool, etc.) to ensure
8
8
  * tools resolve paths relative to your cwd, not process.cwd().
9
+ *
10
+ * For custom tools, see 06-extensions.ts - custom tools are now registered
11
+ * via the extensions system using pi.registerTool().
9
12
  */
10
13
 
11
14
  import {
12
- bashTool, // read, bash, edit, write - uses process.cwd()
13
- type CustomTool,
15
+ bashTool,
14
16
  createAgentSession,
15
17
  createBashTool,
16
- createCodingTools, // Factory: creates tools for specific cwd
18
+ createCodingTools,
17
19
  createGrepTool,
18
20
  createReadTool,
19
21
  grepTool,
20
- readOnlyTools, // read, grep, find, ls - uses process.cwd()
22
+ readOnlyTools,
21
23
  readTool,
22
24
  SessionManager,
23
25
  } from "@mariozechner/pi-coding-agent";
24
- import { Type } from "@sinclair/typebox";
25
26
 
26
27
  // Read-only mode (no edit/write) - uses process.cwd()
27
28
  await createAgentSession({
@@ -53,38 +54,3 @@ await createAgentSession({
53
54
  sessionManager: SessionManager.inMemory(),
54
55
  });
55
56
  console.log("Specific tools with custom cwd session created");
56
-
57
- // Inline custom tool (needs TypeBox schema)
58
- const weatherTool: CustomTool = {
59
- name: "get_weather",
60
- label: "Get Weather",
61
- description: "Get current weather for a city",
62
- parameters: Type.Object({
63
- city: Type.String({ description: "City name" }),
64
- }),
65
- execute: async (_toolCallId, params) => ({
66
- content: [{ type: "text", text: `Weather in ${(params as { city: string }).city}: 22°C, sunny` }],
67
- details: {},
68
- }),
69
- };
70
-
71
- const { session } = await createAgentSession({
72
- customTools: [{ tool: weatherTool }],
73
- sessionManager: SessionManager.inMemory(),
74
- });
75
-
76
- session.subscribe((event) => {
77
- if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
78
- process.stdout.write(event.assistantMessageEvent.delta);
79
- }
80
- });
81
-
82
- await session.prompt("What's the weather in Tokyo?");
83
- console.log();
84
-
85
- // Merge with discovered tools from cwd/.pi/tools and ~/.pi/agent/tools:
86
- // const discovered = await discoverCustomTools();
87
- // customTools: [...discovered, { tool: myTool }]
88
-
89
- // Or add paths without replacing discovery:
90
- // additionalCustomToolPaths: ["/extra/tools"]