@oh-my-pi/pi-coding-agent 15.0.2 → 15.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/CHANGELOG.md +56 -1
  2. package/examples/custom-tools/README.md +11 -7
  3. package/examples/custom-tools/hello/index.ts +2 -2
  4. package/examples/extensions/README.md +19 -8
  5. package/examples/extensions/api-demo.ts +15 -19
  6. package/examples/extensions/hello.ts +5 -6
  7. package/examples/extensions/plan-mode.ts +1 -1
  8. package/examples/extensions/reload-runtime.ts +4 -3
  9. package/examples/extensions/with-deps/index.ts +4 -3
  10. package/examples/sdk/06-extensions.ts +4 -2
  11. package/package.json +7 -17
  12. package/src/autoresearch/tools/init-experiment.ts +38 -41
  13. package/src/autoresearch/tools/log-experiment.ts +32 -41
  14. package/src/autoresearch/tools/run-experiment.ts +3 -3
  15. package/src/autoresearch/tools/update-notes.ts +11 -11
  16. package/src/commit/agentic/tools/analyze-file.ts +4 -4
  17. package/src/commit/agentic/tools/git-file-diff.ts +4 -4
  18. package/src/commit/agentic/tools/git-hunk.ts +5 -5
  19. package/src/commit/agentic/tools/git-overview.ts +4 -4
  20. package/src/commit/agentic/tools/propose-changelog.ts +13 -13
  21. package/src/commit/agentic/tools/propose-commit.ts +6 -6
  22. package/src/commit/agentic/tools/recent-commits.ts +3 -3
  23. package/src/commit/agentic/tools/schemas.ts +28 -28
  24. package/src/commit/agentic/tools/split-commit.ts +22 -21
  25. package/src/commit/analysis/summary.ts +4 -4
  26. package/src/commit/changelog/generate.ts +7 -11
  27. package/src/commit/shared-llm.ts +22 -34
  28. package/src/config/config-file.ts +35 -13
  29. package/src/config/model-registry.ts +9 -190
  30. package/src/config/models-config-schema.ts +166 -0
  31. package/src/config/settings-schema.ts +18 -0
  32. package/src/edit/index.ts +2 -2
  33. package/src/edit/modes/apply-patch.ts +7 -6
  34. package/src/edit/modes/patch.ts +18 -25
  35. package/src/edit/modes/replace.ts +18 -20
  36. package/src/eval/js/shared/rewrite-imports.ts +131 -10
  37. package/src/eval/py/executor.ts +233 -623
  38. package/src/eval/py/kernel.ts +27 -2
  39. package/src/exa/factory.ts +5 -4
  40. package/src/exa/mcp-client.ts +1 -1
  41. package/src/exa/researcher.ts +9 -20
  42. package/src/exa/search.ts +26 -52
  43. package/src/exa/types.ts +1 -1
  44. package/src/exa/websets.ts +54 -53
  45. package/src/exec/bash-executor.ts +2 -1
  46. package/src/extensibility/custom-commands/loader.ts +5 -3
  47. package/src/extensibility/custom-commands/types.ts +4 -2
  48. package/src/extensibility/custom-tools/loader.ts +5 -3
  49. package/src/extensibility/custom-tools/types.ts +7 -6
  50. package/src/extensibility/custom-tools/wrapper.ts +1 -1
  51. package/src/extensibility/extensions/loader.ts +7 -3
  52. package/src/extensibility/extensions/types.ts +9 -5
  53. package/src/extensibility/extensions/wrapper.ts +1 -2
  54. package/src/extensibility/hooks/loader.ts +3 -1
  55. package/src/extensibility/hooks/tool-wrapper.ts +1 -1
  56. package/src/extensibility/hooks/types.ts +4 -2
  57. package/src/extensibility/plugins/legacy-pi-compat.ts +30 -0
  58. package/src/extensibility/shared-events.ts +1 -1
  59. package/src/extensibility/typebox.ts +391 -0
  60. package/src/goals/tools/goal-tool.ts +6 -12
  61. package/src/hashline/types.ts +4 -4
  62. package/src/hindsight/state.ts +2 -2
  63. package/src/index.ts +0 -2
  64. package/src/internal-urls/docs-index.generated.ts +7 -7
  65. package/src/lsp/types.ts +30 -38
  66. package/src/mcp/manager.ts +1 -1
  67. package/src/mcp/tool-bridge.ts +1 -1
  68. package/src/modes/components/session-observer-overlay.ts +12 -1
  69. package/src/modes/components/status-line/segments.ts +2 -1
  70. package/src/modes/controllers/command-controller.ts +27 -2
  71. package/src/modes/controllers/event-controller.ts +3 -4
  72. package/src/modes/interactive-mode.ts +1 -1
  73. package/src/modes/rpc/host-tools.ts +1 -1
  74. package/src/modes/rpc/rpc-client.ts +1 -1
  75. package/src/modes/rpc/rpc-types.ts +1 -1
  76. package/src/modes/theme/theme.ts +111 -117
  77. package/src/modes/types.ts +1 -1
  78. package/src/modes/utils/context-usage.ts +2 -2
  79. package/src/sdk.ts +31 -8
  80. package/src/session/agent-session.ts +74 -104
  81. package/src/session/messages.ts +16 -51
  82. package/src/session/session-manager.ts +22 -2
  83. package/src/session/streaming-output.ts +16 -6
  84. package/src/task/executor.ts +208 -86
  85. package/src/task/index.ts +15 -11
  86. package/src/task/render.ts +32 -5
  87. package/src/task/types.ts +54 -39
  88. package/src/tools/ask.ts +12 -12
  89. package/src/tools/ast-edit.ts +11 -15
  90. package/src/tools/ast-grep.ts +9 -10
  91. package/src/tools/bash.ts +9 -23
  92. package/src/tools/browser.ts +39 -53
  93. package/src/tools/calculator.ts +12 -11
  94. package/src/tools/checkpoint.ts +7 -7
  95. package/src/tools/debug.ts +40 -43
  96. package/src/tools/eval.ts +6 -8
  97. package/src/tools/find.ts +10 -13
  98. package/src/tools/gh.ts +71 -128
  99. package/src/tools/hindsight-recall.ts +4 -6
  100. package/src/tools/hindsight-reflect.ts +5 -5
  101. package/src/tools/hindsight-retain.ts +15 -17
  102. package/src/tools/image-gen.ts +32 -82
  103. package/src/tools/index.ts +4 -1
  104. package/src/tools/inspect-image.ts +8 -9
  105. package/src/tools/irc.ts +15 -27
  106. package/src/tools/job.ts +14 -21
  107. package/src/tools/read.ts +7 -8
  108. package/src/tools/recipe/index.ts +7 -9
  109. package/src/tools/render-mermaid.ts +12 -12
  110. package/src/tools/report-tool-issue.ts +4 -4
  111. package/src/tools/resolve.ts +11 -11
  112. package/src/tools/review.ts +14 -26
  113. package/src/tools/search-tool-bm25.ts +7 -9
  114. package/src/tools/search.ts +19 -22
  115. package/src/tools/ssh.ts +7 -7
  116. package/src/tools/todo-write.ts +26 -34
  117. package/src/tools/vim.ts +10 -26
  118. package/src/tools/write.ts +5 -5
  119. package/src/tools/yield.ts +100 -54
  120. package/src/web/search/index.ts +9 -24
  121. package/src/prompts/compaction/branch-summary-context.md +0 -5
  122. package/src/prompts/compaction/branch-summary-preamble.md +0 -2
  123. package/src/prompts/compaction/branch-summary.md +0 -30
  124. package/src/prompts/compaction/compaction-short-summary.md +0 -9
  125. package/src/prompts/compaction/compaction-summary-context.md +0 -5
  126. package/src/prompts/compaction/compaction-summary.md +0 -38
  127. package/src/prompts/compaction/compaction-turn-prefix.md +0 -17
  128. package/src/prompts/compaction/compaction-update-summary.md +0 -45
  129. package/src/prompts/system/auto-handoff-threshold-focus.md +0 -1
  130. package/src/prompts/system/file-operations.md +0 -10
  131. package/src/prompts/system/handoff-document.md +0 -49
  132. package/src/prompts/system/summarization-system.md +0 -3
  133. package/src/session/compaction/branch-summarization.ts +0 -324
  134. package/src/session/compaction/compaction.ts +0 -1420
  135. package/src/session/compaction/errors.ts +0 -31
  136. package/src/session/compaction/index.ts +0 -8
  137. package/src/session/compaction/pruning.ts +0 -91
  138. package/src/session/compaction/utils.ts +0 -184
@@ -43,6 +43,11 @@ async function ensureRunnerScript(): Promise<string> {
43
43
 
44
44
  const SHUTDOWN_GRACE_MS = 1_000;
45
45
  const STARTUP_TIMEOUT_MS = 10_000;
46
+ // How long to wait after SIGINT for the runner to emit `done`. If the cell is
47
+ // stuck in code that ignores Python signals (e.g. a C extension holding the
48
+ // GIL), we escalate to a full subprocess shutdown so the host queue unblocks
49
+ // instead of hanging the session forever.
50
+ const INTERRUPT_ESCALATION_MS = 2_000;
46
51
 
47
52
  export interface KernelExecuteOptions {
48
53
  signal?: AbortSignal;
@@ -158,6 +163,7 @@ interface PendingExecution {
158
163
  timedOut: boolean;
159
164
  stdinRequested: boolean;
160
165
  settled: boolean;
166
+ escalationTimer?: NodeJS.Timeout;
161
167
  }
162
168
 
163
169
  export class PythonKernel {
@@ -273,22 +279,41 @@ export class PythonKernel {
273
279
  });
274
280
  };
275
281
 
282
+ const requestCancel = () => {
283
+ if (pending.settled || pending.escalationTimer) return;
284
+ void this.interrupt();
285
+ const escalation = setTimeout(() => {
286
+ if (pending.settled) return;
287
+ logger.warn("Python runner did not respond to SIGINT; terminating subprocess", {
288
+ kernelId: this.id,
289
+ });
290
+ // `shutdown()` aborts pending executions immediately and escalates to
291
+ // SIGTERM/SIGKILL, so the host queue unblocks even if the runner is
292
+ // stuck in a non-interruptible state.
293
+ void this.shutdown();
294
+ }, INTERRUPT_ESCALATION_MS);
295
+ escalation.unref?.();
296
+ pending.escalationTimer = escalation;
297
+ };
298
+
276
299
  const onAbort = () => {
277
300
  pending.cancelled = true;
278
301
  pending.timedOut = pending.timedOut || isTimeoutReason(options?.signal?.reason);
279
- void this.interrupt();
302
+ requestCancel();
280
303
  };
281
304
  const timeoutId =
282
305
  typeof options?.timeoutMs === "number" && options.timeoutMs > 0
283
306
  ? setTimeout(() => {
284
307
  pending.timedOut = true;
285
308
  pending.cancelled = true;
286
- void this.interrupt();
309
+ requestCancel();
287
310
  }, options.timeoutMs)
288
311
  : undefined;
289
312
 
290
313
  const cleanup = () => {
291
314
  if (timeoutId) clearTimeout(timeoutId);
315
+ if (pending.escalationTimer) clearTimeout(pending.escalationTimer);
316
+ pending.escalationTimer = undefined;
292
317
  options?.signal?.removeEventListener("abort", onAbort);
293
318
  };
294
319
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Shared factory for creating Exa tools with consistent error handling and response formatting.
3
3
  */
4
- import type { TObject, TProperties } from "@sinclair/typebox";
4
+ import type { TSchema } from "@oh-my-pi/pi-ai";
5
5
  import type { CustomTool } from "../extensibility/custom-tools/types";
6
6
  import { callExaTool, findApiKey, formatGenericResponse, formatSearchResults, isSearchResponse } from "./mcp-client";
7
7
  import type { ExaRenderDetails } from "./types";
@@ -11,7 +11,7 @@ export function createExaTool(
11
11
  name: string,
12
12
  label: string,
13
13
  description: string,
14
- parameters: TObject<TProperties>,
14
+ parameters: TSchema,
15
15
  mcpToolName: string,
16
16
  options?: {
17
17
  /** When true, checks isSearchResponse and formats with formatSearchResults. Default: true */
@@ -19,7 +19,7 @@ export function createExaTool(
19
19
  /** Transform params before passing to callExaTool */
20
20
  transformParams?: (params: Record<string, unknown>) => Record<string, unknown>;
21
21
  },
22
- ): CustomTool<any, ExaRenderDetails> {
22
+ ): CustomTool<TSchema, ExaRenderDetails> {
23
23
  const formatResponse = options?.formatResponse ?? true;
24
24
  const transformParams = options?.transformParams;
25
25
 
@@ -32,7 +32,8 @@ export function createExaTool(
32
32
  try {
33
33
  const apiKey = findApiKey();
34
34
  // Exa MCP endpoint is publicly accessible; API key is optional
35
- const args = transformParams ? transformParams(params as Record<string, unknown>) : params;
35
+ const rawArgs = params as Record<string, unknown>;
36
+ const args = transformParams ? transformParams(rawArgs) : rawArgs;
36
37
  const response = await callExaTool(mcpToolName, args, apiKey);
37
38
 
38
39
  if (formatResponse && isSearchResponse(response)) {
@@ -1,5 +1,5 @@
1
+ import type { TSchema } from "@oh-my-pi/pi-ai";
1
2
  import { $env, logger } from "@oh-my-pi/pi-utils";
2
- import type { TSchema } from "@sinclair/typebox";
3
3
  import type { CustomTool, CustomToolResult } from "../extensibility/custom-tools/types";
4
4
  import { callMCP } from "../mcp/json-rpc";
5
5
  import type {
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * Async research tasks with polling for completion.
5
5
  */
6
- import { Type } from "@sinclair/typebox";
6
+ import type { TSchema } from "@oh-my-pi/pi-ai";
7
+ import * as z from "zod/v4";
7
8
  import type { CustomTool } from "../extensibility/custom-tools/types";
8
9
  import { createExaTool } from "./factory";
9
10
  import type { ExaRenderDetails } from "./types";
@@ -12,22 +13,10 @@ const researcherStartTool = createExaTool(
12
13
  "exa_researcher_start",
13
14
  "Start Deep Research",
14
15
  "Start an asynchronous deep research task using Exa's researcher. Returns a task_id for polling completion.",
15
- Type.Object({
16
- query: Type.String({ description: "Research query to investigate" }),
17
- depth: Type.Optional(
18
- Type.Number({
19
- description: "Research depth (1-5, default: 3)",
20
- minimum: 1,
21
- maximum: 5,
22
- }),
23
- ),
24
- breadth: Type.Optional(
25
- Type.Number({
26
- description: "Research breadth (1-5, default: 3)",
27
- minimum: 1,
28
- maximum: 5,
29
- }),
30
- ),
16
+ z.object({
17
+ query: z.string().describe("Research query to investigate"),
18
+ depth: z.number().int().min(1).max(5).describe("Research depth (1-5, default: 3)").optional(),
19
+ breadth: z.number().int().min(1).max(5).describe("Research breadth (1-5, default: 3)").optional(),
31
20
  }),
32
21
  "deep_researcher_start",
33
22
  { formatResponse: false },
@@ -37,11 +26,11 @@ const researcherPollTool = createExaTool(
37
26
  "exa_researcher_poll",
38
27
  "Poll Research Status",
39
28
  "Poll the status of an asynchronous research task. Returns status (pending|running|completed|failed) and result if completed.",
40
- Type.Object({
41
- task_id: Type.String({ description: "Task ID returned from exa_researcher_start" }),
29
+ z.object({
30
+ task_id: z.string().describe("Task ID returned from exa_researcher_start"),
42
31
  }),
43
32
  "deep_researcher_check",
44
33
  { formatResponse: false },
45
34
  );
46
35
 
47
- export const researcherTools: CustomTool<any, ExaRenderDetails>[] = [researcherStartTool, researcherPollTool];
36
+ export const researcherTools: CustomTool<TSchema, ExaRenderDetails>[] = [researcherStartTool, researcherPollTool];
package/src/exa/search.ts CHANGED
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Basic neural/keyword search, deep research, code search, and URL crawling.
5
5
  */
6
- import { StringEnum } from "@oh-my-pi/pi-ai";
7
- import { Type } from "@sinclair/typebox";
6
+ import type { TSchema } from "@oh-my-pi/pi-ai";
7
+ import * as z from "zod/v4";
8
8
  import type { CustomTool } from "../extensibility/custom-tools/types";
9
9
  import { createExaTool } from "./factory";
10
10
  import type { ExaRenderDetails } from "./types";
@@ -29,57 +29,31 @@ Parameters:
29
29
  - highlights: Include highlighted relevant snippets (default: false)
30
30
  - num_results: Maximum number of results to return (default: 10, max: 100)`,
31
31
 
32
- Type.Object({
33
- query: Type.String({ description: "Search query" }),
34
- type: Type.Optional(
35
- StringEnum(["keyword", "neural", "auto"], {
36
- description: "Search type - neural (semantic), keyword (exact), or auto",
37
- }),
38
- ),
39
- include_domains: Type.Optional(
40
- Type.Array(Type.String(), {
41
- description: "Only include results from these domains",
42
- }),
43
- ),
44
- exclude_domains: Type.Optional(
45
- Type.Array(Type.String(), {
46
- description: "Exclude results from these domains",
47
- }),
48
- ),
49
- start_published_date: Type.Optional(
50
- Type.String({
51
- description: "Filter results published after this date (ISO 8601 format)",
52
- }),
53
- ),
54
- end_published_date: Type.Optional(
55
- Type.String({
56
- description: "Filter results published before this date (ISO 8601 format)",
57
- }),
58
- ),
59
- use_autoprompt: Type.Optional(
60
- Type.Boolean({
61
- description: "Let Exa optimize your query automatically (default: true)",
62
- }),
63
- ),
64
- text: Type.Optional(
65
- Type.Boolean({
66
- description: "Include page text content in results (costs more, default: false)",
67
- }),
68
- ),
69
- highlights: Type.Optional(
70
- Type.Boolean({
71
- description: "Include highlighted relevant snippets (default: false)",
72
- }),
73
- ),
74
- num_results: Type.Optional(
75
- Type.Number({
76
- description: "Maximum number of results to return (default: 10, max: 100)",
77
- minimum: 1,
78
- maximum: 100,
79
- }),
80
- ),
32
+ z.object({
33
+ query: z.string().describe("Search query"),
34
+ type: z
35
+ .enum(["keyword", "neural", "auto"])
36
+ .describe("Search type - neural (semantic), keyword (exact), or auto")
37
+ .optional(),
38
+ include_domains: z.array(z.string()).describe("Only include results from these domains").optional(),
39
+ exclude_domains: z.array(z.string()).describe("Exclude results from these domains").optional(),
40
+ start_published_date: z
41
+ .string()
42
+ .describe("Filter results published after this date (ISO 8601 format)")
43
+ .optional(),
44
+ end_published_date: z.string().describe("Filter results published before this date (ISO 8601 format)").optional(),
45
+ use_autoprompt: z.boolean().describe("Let Exa optimize your query automatically (default: true)").optional(),
46
+ text: z.boolean().describe("Include page text content in results (costs more, default: false)").optional(),
47
+ highlights: z.boolean().describe("Include highlighted relevant snippets (default: false)").optional(),
48
+ num_results: z
49
+ .number()
50
+ .int()
51
+ .min(1)
52
+ .max(100)
53
+ .describe("Maximum number of results to return (default: 10, max: 100)")
54
+ .optional(),
81
55
  }),
82
56
  "web_search_exa",
83
57
  );
84
58
 
85
- export const searchTools: CustomTool<any, ExaRenderDetails>[] = [exaSearchTool];
59
+ export const searchTools: CustomTool<TSchema, ExaRenderDetails>[] = [exaSearchTool];
package/src/exa/types.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Types for the Exa MCP client and tool implementations.
5
5
  */
6
- import type { TSchema } from "@sinclair/typebox";
6
+ import type { TSchema } from "@oh-my-pi/pi-ai";
7
7
 
8
8
  /** MCP endpoint URLs */
9
9
  export const EXA_MCP_URL = "https://mcp.exa.ai/mcp";
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * CRUD operations for websets, items, searches, enrichments, and monitoring.
5
5
  */
6
- import { type TObject, type TProperties, Type } from "@sinclair/typebox";
6
+ import type { TSchema } from "@oh-my-pi/pi-ai";
7
+ import * as z from "zod/v4";
7
8
  import type { CustomTool } from "../extensibility/custom-tools/types";
8
9
  import { callWebsetsTool, findApiKey } from "./mcp-client";
9
10
  import type { ExaRenderDetails } from "./types";
@@ -13,9 +14,9 @@ function createWebsetTool(
13
14
  name: string,
14
15
  label: string,
15
16
  description: string,
16
- parameters: TObject<TProperties>,
17
+ parameters: TSchema,
17
18
  mcpToolName: string,
18
- ): CustomTool<any, ExaRenderDetails> {
19
+ ): CustomTool<TSchema, ExaRenderDetails> {
19
20
  return {
20
21
  name,
21
22
  label,
@@ -51,9 +52,9 @@ const websetCreateTool = createWebsetTool(
51
52
  "webset_create",
52
53
  "Create Webset",
53
54
  "Create a new webset collection for organizing web content.",
54
- Type.Object({
55
- name: Type.String({ description: "Name of the webset" }),
56
- description: Type.Optional(Type.String({ description: "Optional description" })),
55
+ z.object({
56
+ name: z.string().describe("Name of the webset"),
57
+ description: z.string().describe("Optional description").optional(),
57
58
  }),
58
59
  "create_webset",
59
60
  );
@@ -62,7 +63,7 @@ const websetListTool = createWebsetTool(
62
63
  "webset_list",
63
64
  "List Websets",
64
65
  "List all websets in your account.",
65
- Type.Object({}),
66
+ z.object({}),
66
67
  "list_websets",
67
68
  );
68
69
 
@@ -70,8 +71,8 @@ const websetGetTool = createWebsetTool(
70
71
  "webset_get",
71
72
  "Get Webset",
72
73
  "Get details of a specific webset by ID.",
73
- Type.Object({
74
- id: Type.String({ description: "Webset ID" }),
74
+ z.object({
75
+ id: z.string().describe("Webset ID"),
75
76
  }),
76
77
  "get_webset",
77
78
  );
@@ -80,10 +81,10 @@ const websetUpdateTool = createWebsetTool(
80
81
  "webset_update",
81
82
  "Update Webset",
82
83
  "Update a webset's name or description.",
83
- Type.Object({
84
- id: Type.String({ description: "Webset ID" }),
85
- name: Type.Optional(Type.String({ description: "New name" })),
86
- description: Type.Optional(Type.String({ description: "New description" })),
84
+ z.object({
85
+ id: z.string().describe("Webset ID"),
86
+ name: z.string().describe("New name").optional(),
87
+ description: z.string().describe("New description").optional(),
87
88
  }),
88
89
  "update_webset",
89
90
  );
@@ -92,8 +93,8 @@ const websetDeleteTool = createWebsetTool(
92
93
  "webset_delete",
93
94
  "Delete Webset",
94
95
  "Delete a webset and all its contents.",
95
- Type.Object({
96
- id: Type.String({ description: "Webset ID" }),
96
+ z.object({
97
+ id: z.string().describe("Webset ID"),
97
98
  }),
98
99
  "delete_webset",
99
100
  );
@@ -103,10 +104,10 @@ const websetItemsListTool = createWebsetTool(
103
104
  "webset_items_list",
104
105
  "List Webset Items",
105
106
  "List items in a webset with optional pagination.",
106
- Type.Object({
107
- webset_id: Type.String({ description: "Webset ID" }),
108
- limit: Type.Optional(Type.Number({ description: "Number of items to return" })),
109
- offset: Type.Optional(Type.Number({ description: "Pagination offset" })),
107
+ z.object({
108
+ webset_id: z.string().describe("Webset ID"),
109
+ limit: z.number().describe("Number of items to return").optional(),
110
+ offset: z.number().describe("Pagination offset").optional(),
110
111
  }),
111
112
  "list_webset_items",
112
113
  );
@@ -115,9 +116,9 @@ const websetItemGetTool = createWebsetTool(
115
116
  "webset_item_get",
116
117
  "Get Webset Item",
117
118
  "Get a specific item from a webset.",
118
- Type.Object({
119
- webset_id: Type.String({ description: "Webset ID" }),
120
- item_id: Type.String({ description: "Item ID" }),
119
+ z.object({
120
+ webset_id: z.string().describe("Webset ID"),
121
+ item_id: z.string().describe("Item ID"),
121
122
  }),
122
123
  "get_item",
123
124
  );
@@ -127,9 +128,9 @@ const websetSearchCreateTool = createWebsetTool(
127
128
  "webset_search_create",
128
129
  "Create Webset Search",
129
130
  "Create a new search within a webset.",
130
- Type.Object({
131
- webset_id: Type.String({ description: "Webset ID" }),
132
- query: Type.String({ description: "Search query" }),
131
+ z.object({
132
+ webset_id: z.string().describe("Webset ID"),
133
+ query: z.string().describe("Search query"),
133
134
  }),
134
135
  "create_search",
135
136
  );
@@ -138,9 +139,9 @@ const websetSearchGetTool = createWebsetTool(
138
139
  "webset_search_get",
139
140
  "Get Webset Search",
140
141
  "Get the status and results of a webset search.",
141
- Type.Object({
142
- webset_id: Type.String({ description: "Webset ID" }),
143
- search_id: Type.String({ description: "Search ID" }),
142
+ z.object({
143
+ webset_id: z.string().describe("Webset ID"),
144
+ search_id: z.string().describe("Search ID"),
144
145
  }),
145
146
  "get_search",
146
147
  );
@@ -149,9 +150,9 @@ const websetSearchCancelTool = createWebsetTool(
149
150
  "webset_search_cancel",
150
151
  "Cancel Webset Search",
151
152
  "Cancel a running webset search.",
152
- Type.Object({
153
- webset_id: Type.String({ description: "Webset ID" }),
154
- search_id: Type.String({ description: "Search ID" }),
153
+ z.object({
154
+ webset_id: z.string().describe("Webset ID"),
155
+ search_id: z.string().describe("Search ID"),
155
156
  }),
156
157
  "cancel_search",
157
158
  );
@@ -161,10 +162,10 @@ const websetEnrichmentCreateTool = createWebsetTool(
161
162
  "webset_enrichment_create",
162
163
  "Create Enrichment",
163
164
  "Create a new enrichment task for a webset.",
164
- Type.Object({
165
- webset_id: Type.String({ description: "Webset ID" }),
166
- name: Type.String({ description: "Enrichment name" }),
167
- prompt: Type.String({ description: "Enrichment prompt" }),
165
+ z.object({
166
+ webset_id: z.string().describe("Webset ID"),
167
+ name: z.string().describe("Enrichment name"),
168
+ prompt: z.string().describe("Enrichment prompt"),
168
169
  }),
169
170
  "create_enrichment",
170
171
  );
@@ -173,9 +174,9 @@ const websetEnrichmentGetTool = createWebsetTool(
173
174
  "webset_enrichment_get",
174
175
  "Get Enrichment",
175
176
  "Get the status and results of an enrichment task.",
176
- Type.Object({
177
- webset_id: Type.String({ description: "Webset ID" }),
178
- enrichment_id: Type.String({ description: "Enrichment ID" }),
177
+ z.object({
178
+ webset_id: z.string().describe("Webset ID"),
179
+ enrichment_id: z.string().describe("Enrichment ID"),
179
180
  }),
180
181
  "get_enrichment",
181
182
  );
@@ -184,11 +185,11 @@ const websetEnrichmentUpdateTool = createWebsetTool(
184
185
  "webset_enrichment_update",
185
186
  "Update Enrichment",
186
187
  "Update an enrichment's name or prompt.",
187
- Type.Object({
188
- webset_id: Type.String({ description: "Webset ID" }),
189
- enrichment_id: Type.String({ description: "Enrichment ID" }),
190
- name: Type.Optional(Type.String({ description: "New name" })),
191
- prompt: Type.Optional(Type.String({ description: "New prompt" })),
188
+ z.object({
189
+ webset_id: z.string().describe("Webset ID"),
190
+ enrichment_id: z.string().describe("Enrichment ID"),
191
+ name: z.string().describe("New name").optional(),
192
+ prompt: z.string().describe("New prompt").optional(),
192
193
  }),
193
194
  "update_enrichment",
194
195
  );
@@ -197,9 +198,9 @@ const websetEnrichmentDeleteTool = createWebsetTool(
197
198
  "webset_enrichment_delete",
198
199
  "Delete Enrichment",
199
200
  "Delete an enrichment task.",
200
- Type.Object({
201
- webset_id: Type.String({ description: "Webset ID" }),
202
- enrichment_id: Type.String({ description: "Enrichment ID" }),
201
+ z.object({
202
+ webset_id: z.string().describe("Webset ID"),
203
+ enrichment_id: z.string().describe("Enrichment ID"),
203
204
  }),
204
205
  "delete_enrichment",
205
206
  );
@@ -208,9 +209,9 @@ const websetEnrichmentCancelTool = createWebsetTool(
208
209
  "webset_enrichment_cancel",
209
210
  "Cancel Enrichment",
210
211
  "Cancel a running enrichment task.",
211
- Type.Object({
212
- webset_id: Type.String({ description: "Webset ID" }),
213
- enrichment_id: Type.String({ description: "Enrichment ID" }),
212
+ z.object({
213
+ webset_id: z.string().describe("Webset ID"),
214
+ enrichment_id: z.string().describe("Enrichment ID"),
214
215
  }),
215
216
  "cancel_enrichment",
216
217
  );
@@ -220,14 +221,14 @@ const websetMonitorCreateTool = createWebsetTool(
220
221
  "webset_monitor_create",
221
222
  "Create Monitor",
222
223
  "Create a monitoring task for a webset with optional webhook notifications.",
223
- Type.Object({
224
- webset_id: Type.String({ description: "Webset ID" }),
225
- webhook_url: Type.Optional(Type.String({ description: "Webhook URL for notifications" })),
224
+ z.object({
225
+ webset_id: z.string().describe("Webset ID"),
226
+ webhook_url: z.string().describe("Webhook URL for notifications").optional(),
226
227
  }),
227
228
  "create_monitor",
228
229
  );
229
230
 
230
- export const websetsTools: CustomTool<any, ExaRenderDetails>[] = [
231
+ export const websetsTools: CustomTool<TSchema, ExaRenderDetails>[] = [
231
232
  websetCreateTool,
232
233
  websetListTool,
233
234
  websetGetTool,
@@ -252,7 +252,8 @@ export async function executeBash(command: string, options?: BashExecutorOptions
252
252
  outputBytes: minimized.outputBytes,
253
253
  });
254
254
  if (artifactId) {
255
- sink.push(`\n[raw output: artifact://${artifactId}]\n`);
255
+ const sep = minimized.text.endsWith("\n") ? "" : "\n";
256
+ sink.push(`${sep}[raw output: artifact://${artifactId}]\n`);
256
257
  }
257
258
  }
258
259
  }
@@ -1,15 +1,16 @@
1
1
  /**
2
2
  * Custom command loader - loads TypeScript command modules using native Bun import.
3
3
  *
4
- * Dependencies (@sinclair/typebox and pi-coding-agent) are injected via the CustomCommandAPI
5
- * to avoid import resolution issues with custom commands loaded from user directories.
4
+ * Dependencies (the zod-backed typebox shim and pi-coding-agent) are injected via the
5
+ * CustomCommandAPI to avoid import resolution issues with custom commands loaded from user directories.
6
6
  */
7
7
  import * as fs from "node:fs";
8
8
  import * as path from "node:path";
9
9
  import { getAgentDir, getProjectDir, isEnoent, logger } from "@oh-my-pi/pi-utils";
10
- import * as typebox from "@sinclair/typebox";
10
+ import * as zod from "zod/v4";
11
11
  import { getConfigDirs } from "../../config";
12
12
  import { execCommand } from "../../exec/exec";
13
+ import * as typebox from "../typebox";
13
14
  import { GreenCommand } from "./bundled/ci-green";
14
15
  import { ReviewCommand } from "./bundled/review";
15
16
  import type {
@@ -183,6 +184,7 @@ export async function loadCustomCommands(options: LoadCustomCommandsOptions = {}
183
184
  exec: (command: string, args: string[], execOptions) =>
184
185
  execCommand(command, args, execOptions?.cwd ?? cwd, execOptions),
185
186
  typebox,
187
+ zod,
186
188
  pi: await import("@oh-my-pi/pi-coding-agent"),
187
189
  };
188
190
 
@@ -19,8 +19,10 @@ export interface CustomCommandAPI {
19
19
  cwd: string;
20
20
  /** Execute a shell command */
21
21
  exec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;
22
- /** Injected @sinclair/typebox module */
23
- typebox: typeof import("@sinclair/typebox");
22
+ /** Injected zod-backed typebox shim (legacy/compat). */
23
+ typebox: typeof import("../typebox");
24
+ /** Injected zod module for Zod-authored custom commands. */
25
+ zod: typeof import("zod/v4");
24
26
  /** Injected pi-coding-agent exports */
25
27
  pi: typeof import("../..");
26
28
  }
@@ -1,19 +1,20 @@
1
1
  /**
2
2
  * Custom tool loader - loads TypeScript tool modules using native Bun import.
3
3
  *
4
- * Dependencies (@sinclair/typebox and pi-coding-agent) are injected via the CustomToolAPI
5
- * to avoid import resolution issues with custom tools loaded from user directories.
4
+ * Dependencies (the zod-backed typebox shim and pi-coding-agent) are injected via the
5
+ * CustomToolAPI to avoid import resolution issues with custom tools loaded from user directories.
6
6
  */
7
7
  import * as path from "node:path";
8
8
  import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
9
9
  import { logger } from "@oh-my-pi/pi-utils";
10
- import * as typebox from "@sinclair/typebox";
10
+ import * as z from "zod/v4";
11
11
  import { toolCapability } from "../../capability/tool";
12
12
  import { type CustomTool, loadCapability } from "../../discovery";
13
13
  import type { ExecOptions } from "../../exec/exec";
14
14
  import { execCommand } from "../../exec/exec";
15
15
  import type { HookUIContext } from "../../extensibility/hooks/types";
16
16
  import { getAllPluginToolPaths } from "../../extensibility/plugins/loader";
17
+ import * as typebox from "../typebox";
17
18
  import { createNoOpUIContext, resolvePath } from "../utils";
18
19
  import type { CustomToolAPI, CustomToolFactory, LoadedCustomTool, ToolLoadError } from "./types";
19
20
 
@@ -103,6 +104,7 @@ export class CustomToolLoader {
103
104
  hasUI: false,
104
105
  logger,
105
106
  typebox,
107
+ zod: z,
106
108
  pi,
107
109
  pushPendingAction: action => {
108
110
  if (!pushPendingAction) {
@@ -5,16 +5,15 @@
5
5
  * They can provide custom rendering for tool calls and results in the TUI.
6
6
  */
7
7
  import type { AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
8
- import type { Model } from "@oh-my-pi/pi-ai";
8
+ import type { CompactionResult } from "@oh-my-pi/pi-agent-core/compaction";
9
+ import type { Model, Static, TSchema } from "@oh-my-pi/pi-ai";
9
10
  import type { Component } from "@oh-my-pi/pi-tui";
10
- import type { Static, TSchema } from "@sinclair/typebox";
11
11
  import type { Rule } from "../../capability/rule";
12
12
  import type { ModelRegistry } from "../../config/model-registry";
13
13
  import type { Settings } from "../../config/settings";
14
14
  import type { ExecOptions, ExecResult } from "../../exec/exec";
15
15
  import type { HookUIContext } from "../../extensibility/hooks/types";
16
16
  import type { Theme } from "../../modes/theme/theme";
17
- import type { CompactionResult } from "../../session/compaction";
18
17
  import type { ReadonlySessionManager } from "../../session/session-manager";
19
18
  import type { TodoItem } from "../../tools/todo-write";
20
19
 
@@ -52,8 +51,10 @@ export interface CustomToolAPI {
52
51
  hasUI: boolean;
53
52
  /** File logger for error/warning/debug messages */
54
53
  logger: typeof import("@oh-my-pi/pi-utils").logger;
55
- /** Injected @sinclair/typebox module */
56
- typebox: typeof import("@sinclair/typebox");
54
+ /** Injected zod-backed typebox shim (legacy/compat — Zod-authored tools are preferred). */
55
+ typebox: typeof import("../typebox");
56
+ /** Injected zod module for Zod-authored custom tools. */
57
+ zod: typeof import("zod/v4");
57
58
  /** Injected pi-coding-agent exports */
58
59
  pi: typeof import("../..");
59
60
  /** Push a preview action that can later be resolved with the hidden resolve tool */
@@ -180,7 +181,7 @@ export interface CustomTool<TParams extends TSchema = TSchema, TDetails = any> {
180
181
  strict?: boolean;
181
182
  /** Description for LLM */
182
183
  description: string;
183
- /** Parameter schema (TypeBox) */
184
+ /** Parameter schema (Zod or TypeBox; TypeBox is auto-lifted to Zod at registration). */
184
185
  parameters: TParams;
185
186
  /** If true, tool is excluded unless explicitly listed in --tools or agent's tools field */
186
187
  hidden?: boolean;
@@ -2,7 +2,7 @@
2
2
  * CustomToolAdapter wraps CustomTool instances into AgentTool for use with the agent.
3
3
  */
4
4
  import type { AgentTool, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
5
- import type { Static, TSchema } from "@sinclair/typebox";
5
+ import type { Static, TSchema } from "@oh-my-pi/pi-ai";
6
6
  import type { Theme } from "../../modes/theme/theme";
7
7
  import { applyToolProxy } from "../tool-proxy";
8
8
  import type { CustomTool, CustomToolContext } from "./types";