@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
@@ -8,8 +8,7 @@ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
8
8
  import type { ImageContent, Model, TextContent } from "@oh-my-pi/pi-ai";
9
9
  import type { KeyId } from "@oh-my-pi/pi-tui";
10
10
  import { hasFsCode, isEacces, isEnoent, logger } from "@oh-my-pi/pi-utils";
11
- import type { TSchema } from "@sinclair/typebox";
12
- import * as TypeBox from "@sinclair/typebox";
11
+ import * as Zod from "zod/v4";
13
12
  import { type ExtensionModule, extensionModuleCapability } from "../../capability/extension-module";
14
13
  import { loadCapability } from "../../discovery";
15
14
  import { getExtensionNameFromPath } from "../../discovery/helpers";
@@ -19,6 +18,7 @@ import type { CustomMessage } from "../../session/messages";
19
18
  import { EventBus } from "../../utils/event-bus";
20
19
  import { installLegacyPiSpecifierShim, loadLegacyPiModule } from "../plugins/legacy-pi-compat";
21
20
  import { getAllPluginExtensionPaths } from "../plugins/loader";
21
+ import * as TypeBox from "../typebox";
22
22
 
23
23
  import { resolvePath } from "../utils";
24
24
  import type {
@@ -119,6 +119,7 @@ export class ExtensionRuntime implements IExtensionRuntime {
119
119
  class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
120
120
  readonly logger = logger;
121
121
  readonly typebox = TypeBox;
122
+ readonly zod = Zod;
122
123
  readonly flagValues = new Map<string, boolean | string>();
123
124
  readonly pendingProviderRegistrations: Array<{
124
125
  name: string;
@@ -140,7 +141,10 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
140
141
  this.extension.handlers.set(event, list);
141
142
  }
142
143
 
143
- registerTool<TParams extends TSchema = TSchema, TDetails = unknown>(tool: ToolDefinition<TParams, TDetails>): void {
144
+ registerTool<
145
+ TParams extends import("@oh-my-pi/pi-ai").TSchema = import("@oh-my-pi/pi-ai").TSchema,
146
+ TDetails = unknown,
147
+ >(tool: ToolDefinition<TParams, TDetails>): void {
144
148
  this.extension.tools.set(tool.name, {
145
149
  definition: tool,
146
150
  extensionPath: this.extension.path,
@@ -8,6 +8,7 @@
8
8
  * - Interact with the user via UI primitives
9
9
  */
10
10
  import type { AgentMessage, AgentToolResult, AgentToolUpdateCallback, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
11
+ import type { CompactionResult } from "@oh-my-pi/pi-agent-core/compaction";
11
12
  import type {
12
13
  Api,
13
14
  AssistantMessageEvent,
@@ -17,12 +18,13 @@ import type {
17
18
  Model,
18
19
  ProviderResponseMetadata,
19
20
  SimpleStreamOptions,
21
+ Static,
20
22
  TextContent,
23
+ TSchema,
21
24
  } from "@oh-my-pi/pi-ai";
22
25
  import type { OAuthCredentials, OAuthLoginCallbacks } from "@oh-my-pi/pi-ai/utils/oauth/types";
23
26
  import type * as piCodingAgent from "@oh-my-pi/pi-coding-agent";
24
27
  import type { AutocompleteItem, Component, EditorTheme, KeyId, TUI } from "@oh-my-pi/pi-tui";
25
- import type { Static, TSchema } from "@sinclair/typebox";
26
28
  import type { KeybindingsManager } from "../../config/keybindings";
27
29
  import type { ModelRegistry } from "../../config/model-registry";
28
30
  import type { EditToolDetails } from "../../edit";
@@ -31,7 +33,6 @@ import type { BashResult } from "../../exec/bash-executor";
31
33
  import type { ExecOptions, ExecResult } from "../../exec/exec";
32
34
  import type { CustomEditor } from "../../modes/components/custom-editor";
33
35
  import type { Theme } from "../../modes/theme/theme";
34
- import type { CompactionResult } from "../../session/compaction";
35
36
  import type { CustomMessage } from "../../session/messages";
36
37
  import type { ReadonlySessionManager, SessionManager } from "../../session/session-manager";
37
38
  import type {
@@ -355,7 +356,7 @@ export interface ToolDefinition<TParams extends TSchema = TSchema, TDetails = un
355
356
  label: string;
356
357
  /** Description for LLM */
357
358
  description: string;
358
- /** Parameter schema (TypeBox) */
359
+ /** Parameter schema (Zod, or TypeBox for legacy/extension compat). */
359
360
  parameters: TParams;
360
361
  /** If true, tool is excluded unless explicitly listed in --tools or agent's tools field */
361
362
  hidden?: boolean;
@@ -833,8 +834,11 @@ export interface ExtensionAPI {
833
834
  /** File logger for error/warning/debug messages */
834
835
  logger: typeof import("@oh-my-pi/pi-utils").logger;
835
836
 
836
- /** Injected @sinclair/typebox module for defining tool parameters */
837
- typebox: typeof import("@sinclair/typebox");
837
+ /** Injected zod-backed typebox shim for legacy `Type.Object(...)` parameter authoring. */
838
+ typebox: typeof import("../typebox");
839
+
840
+ /** Injected zod module for Zod-authored extension tools (canonical going forward). */
841
+ zod: typeof import("zod/v4");
838
842
 
839
843
  /** Injected pi-coding-agent exports for accessing SDK utilities */
840
844
  pi: typeof piCodingAgent;
@@ -2,8 +2,7 @@
2
2
  * Tool wrappers for extensions.
3
3
  */
4
4
  import type { AgentTool, AgentToolContext, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
5
- import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
6
- import type { Static, TSchema } from "@sinclair/typebox";
5
+ import type { ImageContent, Static, TextContent, TSchema } from "@oh-my-pi/pi-ai";
7
6
  import type { Theme } from "../../modes/theme/theme";
8
7
  import { applyToolProxy } from "../tool-proxy";
9
8
  import type { ExtensionRunner } from "./runner";
@@ -3,12 +3,13 @@
3
3
  */
4
4
  import * as path from "node:path";
5
5
  import { logger } from "@oh-my-pi/pi-utils";
6
- import * as typebox from "@sinclair/typebox";
6
+ import * as zod from "zod/v4";
7
7
  import { hookCapability } from "../../capability/hook";
8
8
  import type { Hook } from "../../discovery";
9
9
  import { loadCapability } from "../../discovery";
10
10
  import type { HookMessage } from "../../session/messages";
11
11
  import type { SessionManager } from "../../session/session-manager";
12
+ import * as typebox from "../typebox";
12
13
  import { resolvePath } from "../utils";
13
14
  import { execCommand } from "./runner";
14
15
  import type { ExecOptions, HookAPI, HookFactory, HookMessageRenderer, RegisteredCommand } from "./types";
@@ -136,6 +137,7 @@ async function createHookAPI(
136
137
  },
137
138
  logger,
138
139
  typebox,
140
+ zod,
139
141
  pi: await import("@oh-my-pi/pi-coding-agent"),
140
142
  } as HookAPI;
141
143
 
@@ -2,7 +2,7 @@
2
2
  * Tool wrapper - wraps tools with hook callbacks for interception.
3
3
  */
4
4
  import type { AgentTool, AgentToolContext, 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 { applyToolProxy } from "../tool-proxy";
7
7
  import type { HookRunner } from "./runner";
8
8
  import type { ToolCallEventResult, ToolResultEventResult } from "./types";
@@ -571,8 +571,10 @@ export interface HookAPI {
571
571
 
572
572
  /** File logger for error/warning/debug messages */
573
573
  logger: typeof import("@oh-my-pi/pi-utils").logger;
574
- /** Injected @sinclair/typebox module */
575
- typebox: typeof import("@sinclair/typebox");
574
+ /** Injected zod-backed typebox shim (legacy/compat — prefer `zod`). */
575
+ typebox: typeof import("../typebox");
576
+ /** Injected zod module for Zod-authored hooks. */
577
+ zod: typeof import("zod/v4");
576
578
  /** Injected pi-coding-agent exports */
577
579
  pi: typeof import("../..");
578
580
  }
@@ -47,6 +47,17 @@ const LEGACY_PI_FILE_PREFIX = "omp-legacy-pi-file:";
47
47
  const LEGACY_PI_FILE_NAMESPACE = "omp-legacy-pi-file";
48
48
  const resolvedSpecifierFallbacks = new Map<string, string>();
49
49
 
50
+ // Extensions that imported `@sinclair/typebox` directly used to resolve against a
51
+ // real `@sinclair/typebox` install. The runtime dep was replaced with the Zod-backed
52
+ // shim under `extensibility/typebox.ts`; plugins still importing the public name
53
+ // are redirected to that shim so existing extensions keep working without code
54
+ // changes. Submodules like `@sinclair/typebox/compiler` are intentionally not
55
+ // remapped — those expose TypeBox-only APIs the shim does not provide and plugins
56
+ // relying on them must vendor `@sinclair/typebox` directly.
57
+ const TYPEBOX_SPECIFIER = "@sinclair/typebox";
58
+ const TYPEBOX_SPECIFIER_FILTER = /^@sinclair\/typebox$/;
59
+ const TYPEBOX_SHIM_PATH = path.resolve(import.meta.dir, "../typebox.ts");
60
+
50
61
  let isLegacyPiSpecifierShimInstalled = false;
51
62
 
52
63
  function remapLegacyPiSpecifier(specifier: string): string | null {
@@ -99,6 +110,12 @@ const ANY_IMPORT_SPECIFIER_REGEX = /((?:from\s+|import\s*\(\s*)["'])([^"']+)(["'
99
110
 
100
111
  /** Resolve bare imports against the extension directory before loading mirrored legacy Pi files. */
101
112
  function isUrlLikeSpecifier(specifier: string): boolean {
113
+ // Windows drive-letter paths (e.g. `C:\foo` or `C:/foo`) also match the URL
114
+ // scheme shape `[A-Za-z][A-Za-z\d+.-]*:`. Treat them as filesystem paths so
115
+ // `toRewrittenImportSpecifier` converts them to `file://` URLs instead of
116
+ // emitting raw paths whose `\n`, `\U`, ... get eaten by TS string-literal
117
+ // escapes inside the mirrored extension file.
118
+ if (/^[a-zA-Z]:[\\/]/.test(specifier)) return false;
102
119
  return /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(specifier);
103
120
  }
104
121
 
@@ -117,6 +134,9 @@ function rewriteBareImportsForLegacyExtension(source: string, importerPath: stri
117
134
  if (shouldPreserveImportSpecifier(specifier)) {
118
135
  return match;
119
136
  }
137
+ if (specifier === TYPEBOX_SPECIFIER) {
138
+ return `${prefix}${toRewrittenImportSpecifier(TYPEBOX_SHIM_PATH)}${suffix}`;
139
+ }
120
140
  try {
121
141
  const resolved = Bun.resolveSync(specifier, importerDir);
122
142
  return `${prefix}${toRewrittenImportSpecifier(resolved)}${suffix}`;
@@ -223,6 +243,10 @@ function resolveLegacyPiSpecifier(args: { path: string }): { path: string } | un
223
243
  };
224
244
  }
225
245
 
246
+ function resolveTypeBoxSpecifier(): { path: string } {
247
+ return { path: TYPEBOX_SHIM_PATH };
248
+ }
249
+
226
250
  export function installLegacyPiSpecifierShim(): void {
227
251
  if (isLegacyPiSpecifierShimInstalled) {
228
252
  return;
@@ -238,6 +262,12 @@ export function installLegacyPiSpecifierShim(): void {
238
262
  resolveLegacyPiSpecifier,
239
263
  );
240
264
 
265
+ build.onResolve({ filter: TYPEBOX_SPECIFIER_FILTER, namespace: "file" }, resolveTypeBoxSpecifier);
266
+ build.onResolve(
267
+ { filter: TYPEBOX_SPECIFIER_FILTER, namespace: LEGACY_PI_FILE_NAMESPACE },
268
+ resolveTypeBoxSpecifier,
269
+ );
270
+
241
271
  build.onResolve({ filter: /^omp-legacy-pi-file:/, namespace: "file" }, args => ({
242
272
  path: args.path.slice(LEGACY_PI_FILE_PREFIX.length),
243
273
  namespace: LEGACY_PI_FILE_NAMESPACE,
@@ -13,10 +13,10 @@
13
13
  * `types.ts` files and is documented there.
14
14
  */
15
15
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
16
+ import type { CompactionPreparation, CompactionResult } from "@oh-my-pi/pi-agent-core/compaction";
16
17
  import type { ImageContent, TextContent, ToolResultMessage } from "@oh-my-pi/pi-ai";
17
18
  import type { Rule } from "../capability/rule";
18
19
  import type { Goal, GoalModeState } from "../goals/state";
19
- import type { CompactionPreparation, CompactionResult } from "../session/compaction";
20
20
  import type { BranchSummaryEntry, CompactionEntry, SessionEntry } from "../session/session-manager";
21
21
  import type { TodoItem } from "../tools/todo-write";
22
22
 
@@ -0,0 +1,391 @@
1
+ /**
2
+ * Minimal `@sinclair/typebox` runtime compatibility shim, backed by Zod.
3
+ *
4
+ * Historically the coding agent injected the real `@sinclair/typebox` (~5MB
5
+ * dependency) into extensions, hooks, custom tools, and custom commands so
6
+ * they could author parameter schemas as `Type.Object({ name: Type.String() })`.
7
+ * Internally everything already runs through Zod (`wire.ts`, `validation.ts`);
8
+ * the only reason TypeBox remained was extension-author compat.
9
+ *
10
+ * This module replaces that injection with a tiny façade whose `Type` builders
11
+ * return Zod schemas. Output is indistinguishable from hand-written Zod inside
12
+ * the agent pipeline:
13
+ *
14
+ * - `isZodSchema()` keys off the Zod `_zod` marker that every schema carries.
15
+ * - `zodToWireSchema()` emits the same draft-7 JSON Schema providers expect
16
+ * from TypeBox-authored tools (defaulted fields treated as optional, etc.).
17
+ *
18
+ * The surface intentionally covers only the common TypeBox builders. Plugins
19
+ * that reached for niche TypeBox-only APIs (`TypeCompiler`, the global
20
+ * `TypeRegistry`, custom `Symbol(TypeBox.Kind)` introspection) must vendor
21
+ * `@sinclair/typebox` directly in their own package.
22
+ */
23
+
24
+ import { areJsonValuesEqual } from "@oh-my-pi/pi-ai/utils/schema";
25
+ import {
26
+ type ZodArray,
27
+ type ZodEnum,
28
+ type ZodObject,
29
+ type ZodOptional,
30
+ type ZodRawShape,
31
+ type ZodType,
32
+ z,
33
+ } from "zod/v4";
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Type aliases — exported so `import type { Static, TSchema } from "..."`
37
+ // patterns keep compiling at the call site.
38
+ // ---------------------------------------------------------------------------
39
+
40
+ export type TSchema = ZodType;
41
+ export type Static<T extends ZodType> = z.infer<T>;
42
+ export type TAny = ZodType;
43
+ export type TUnknown = ZodType;
44
+ export type TNever = ZodType;
45
+ export type TNull = ZodType;
46
+ export type TString = z.ZodString;
47
+ export type TNumber = z.ZodNumber;
48
+ export type TInteger = z.ZodNumber;
49
+ export type TBoolean = z.ZodBoolean;
50
+ export type TLiteral<V extends string | number | boolean> = z.ZodLiteral<V>;
51
+ export type TArray<E extends ZodType> = ZodArray<E>;
52
+ export type TObject<P extends ZodRawShape = ZodRawShape> = ZodObject<P>;
53
+ export type TOptional<E extends ZodType> = ZodOptional<E>;
54
+ export type TUnion<_T extends readonly ZodType[] = readonly ZodType[]> = ZodType;
55
+ export type TEnum<T extends readonly (string | number)[] = readonly (string | number)[]> = ZodEnum<{
56
+ [K in T[number] as `${K}`]: K;
57
+ }>;
58
+ export type TRecord<_K extends ZodType, _V extends ZodType> = ZodType;
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Option shapes — loose subset of JSON Schema metadata + per-type constraints.
62
+ // ---------------------------------------------------------------------------
63
+
64
+ interface Meta {
65
+ title?: string;
66
+ description?: string;
67
+ default?: unknown;
68
+ examples?: unknown[];
69
+ // Real TypeBox accepts arbitrary extra JSON Schema keywords; we tolerate
70
+ // them silently so callers don't blow up on niche metadata.
71
+ [key: string]: unknown;
72
+ }
73
+
74
+ interface StringOpts extends Meta {
75
+ minLength?: number;
76
+ maxLength?: number;
77
+ pattern?: string;
78
+ format?: string;
79
+ }
80
+
81
+ interface NumberOpts extends Meta {
82
+ minimum?: number;
83
+ maximum?: number;
84
+ exclusiveMinimum?: number;
85
+ exclusiveMaximum?: number;
86
+ multipleOf?: number;
87
+ }
88
+
89
+ interface ArrayOpts extends Meta {
90
+ minItems?: number;
91
+ maxItems?: number;
92
+ uniqueItems?: boolean;
93
+ }
94
+
95
+ interface ObjectOpts extends Meta {
96
+ /**
97
+ * TypeBox default: extra keys are preserved. Set `false` to reject unknowns,
98
+ * `true` to allow any, or a schema to validate them.
99
+ */
100
+ additionalProperties?: boolean | ZodType;
101
+ }
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // Helpers
105
+ // ---------------------------------------------------------------------------
106
+
107
+ function withMeta<T extends ZodType>(schema: T, opts: Meta | undefined): T {
108
+ if (!opts) return schema;
109
+ let out: ZodType = schema;
110
+ if (typeof opts.description === "string") out = out.describe(opts.description);
111
+ if ("default" in opts) out = out.default(opts.default as never) as unknown as ZodType;
112
+
113
+ const metadata: Record<string, unknown> = {};
114
+ for (const [key, value] of Object.entries(opts)) {
115
+ if (key === "description" || key === "default" || key === "additionalProperties") continue;
116
+ metadata[key] = value;
117
+ }
118
+ if (Object.keys(metadata).length > 0) out = out.meta(metadata);
119
+ return out as T;
120
+ }
121
+
122
+ // ---------------------------------------------------------------------------
123
+ // Builders
124
+ // ---------------------------------------------------------------------------
125
+
126
+ function tString(opts?: StringOpts): ZodType {
127
+ let s: ZodType = z.string();
128
+ if (opts) {
129
+ // Format selection swaps the base schema for a more specific Zod string
130
+ // validator that emits the right `format` keyword in JSON Schema.
131
+ switch (opts.format) {
132
+ case "email":
133
+ s = z.email();
134
+ break;
135
+ case "url":
136
+ case "uri":
137
+ s = z.url();
138
+ break;
139
+ case "uuid":
140
+ s = z.uuid();
141
+ break;
142
+ case "date-time":
143
+ s = z.iso.datetime();
144
+ break;
145
+ case "date":
146
+ s = z.iso.date();
147
+ break;
148
+ case "time":
149
+ s = z.iso.time();
150
+ break;
151
+ case "ipv4":
152
+ s = z.ipv4();
153
+ break;
154
+ case "ipv6":
155
+ s = z.ipv6();
156
+ break;
157
+ default:
158
+ break;
159
+ }
160
+ // Length/pattern constraints live on the `_ZodString` base that every
161
+ // format-specific schema (ZodEmail, ZodURL, ZodISODateTime, ...) extends,
162
+ // so we apply them regardless of which concrete subclass `s` ended up as.
163
+ const sf = s as z.ZodString;
164
+ if (typeof opts.minLength === "number") s = sf.min(opts.minLength);
165
+ if (typeof opts.maxLength === "number") s = (s as z.ZodString).max(opts.maxLength);
166
+ if (typeof opts.pattern === "string") s = (s as z.ZodString).regex(new RegExp(opts.pattern));
167
+ }
168
+ return withMeta(s, opts);
169
+ }
170
+
171
+ function applyNumberConstraints(base: z.ZodNumber, opts: NumberOpts | undefined): z.ZodNumber {
172
+ if (!opts) return base;
173
+ let out = base;
174
+ if (typeof opts.minimum === "number") out = out.min(opts.minimum);
175
+ if (typeof opts.maximum === "number") out = out.max(opts.maximum);
176
+ if (typeof opts.exclusiveMinimum === "number") out = out.gt(opts.exclusiveMinimum);
177
+ if (typeof opts.exclusiveMaximum === "number") out = out.lt(opts.exclusiveMaximum);
178
+ if (typeof opts.multipleOf === "number") out = out.multipleOf(opts.multipleOf);
179
+ return out;
180
+ }
181
+
182
+ function tNumber(opts?: NumberOpts): ZodType {
183
+ return withMeta(applyNumberConstraints(z.number(), opts), opts);
184
+ }
185
+
186
+ function tInteger(opts?: NumberOpts): ZodType {
187
+ return withMeta(applyNumberConstraints(z.number().int(), opts), opts);
188
+ }
189
+
190
+ function tBoolean(opts?: Meta): ZodType {
191
+ return withMeta(z.boolean(), opts);
192
+ }
193
+
194
+ function tNull(opts?: Meta): ZodType {
195
+ return withMeta(z.null(), opts);
196
+ }
197
+
198
+ function tAny(opts?: Meta): ZodType {
199
+ return withMeta(z.any(), opts);
200
+ }
201
+
202
+ function tUnknown(opts?: Meta): ZodType {
203
+ return withMeta(z.unknown(), opts);
204
+ }
205
+
206
+ function tNever(opts?: Meta): ZodType {
207
+ return withMeta(z.never(), opts);
208
+ }
209
+
210
+ function tLiteral<V extends string | number | boolean>(value: V, opts?: Meta): ZodType {
211
+ return withMeta(z.literal(value), opts);
212
+ }
213
+
214
+ function tUnion<T extends readonly ZodType[]>(schemas: T, opts?: Meta): ZodType {
215
+ if (schemas.length === 0) return withMeta(z.never(), opts);
216
+ if (schemas.length === 1) return withMeta(schemas[0] as ZodType, opts);
217
+ return withMeta(z.union(schemas as unknown as [ZodType, ZodType, ...ZodType[]]), opts);
218
+ }
219
+
220
+ function tIntersect(schemas: readonly ZodType[], opts?: Meta): ZodType {
221
+ if (schemas.length === 0) return withMeta(z.unknown(), opts);
222
+ if (schemas.length === 1) return withMeta(schemas[0] as ZodType, opts);
223
+ let out: ZodType = schemas[0] as ZodType;
224
+ for (let i = 1; i < schemas.length; i++) out = z.intersection(out, schemas[i] as ZodType) as ZodType;
225
+ return withMeta(out, opts);
226
+ }
227
+
228
+ function isArrayIndexKey(key: string): boolean {
229
+ if (!/^(?:0|[1-9]\\d*)$/.test(key)) return false;
230
+ const index = Number(key);
231
+ return Number.isSafeInteger(index) && index >= 0;
232
+ }
233
+
234
+ function uniqueLiteralValues(values: readonly (string | number | boolean)[]): Array<string | number | boolean> {
235
+ const unique: Array<string | number | boolean> = [];
236
+ for (const value of values) {
237
+ if (!unique.some(existing => existing === value)) unique.push(value);
238
+ }
239
+ return unique;
240
+ }
241
+
242
+ function literalUnion(values: readonly (string | number | boolean)[], opts?: Meta): ZodType {
243
+ const unique = uniqueLiteralValues(values);
244
+ if (unique.length === 0) return withMeta(z.never(), opts);
245
+ if (unique.length === 1) return withMeta(z.literal(unique[0] as string | number | boolean), opts);
246
+ const schemas = unique.map(value => z.literal(value as string | number | boolean)) as unknown as [
247
+ ZodType,
248
+ ZodType,
249
+ ...ZodType[],
250
+ ];
251
+ return withMeta(z.union(schemas), opts);
252
+ }
253
+ function tEnum<T extends Record<string, string | number> | readonly (string | number)[]>(
254
+ values: T,
255
+ opts?: Meta,
256
+ ): ZodType {
257
+ const list = Array.isArray(values)
258
+ ? values
259
+ : Object.entries(values)
260
+ .filter(([key, value]) => !(isArrayIndexKey(key) && typeof value === "string"))
261
+ .map(([, value]) => value);
262
+ return literalUnion(list, opts);
263
+ }
264
+
265
+ function tArray<E extends ZodType>(item: E, opts?: ArrayOpts): ZodType {
266
+ let arr: ZodType = z.array(item);
267
+ if (opts) {
268
+ if (typeof opts.minItems === "number") arr = (arr as ZodArray<E>).min(opts.minItems);
269
+ if (typeof opts.maxItems === "number") arr = (arr as ZodArray<E>).max(opts.maxItems);
270
+ if (opts.uniqueItems === true) {
271
+ arr = arr.refine(items => {
272
+ if (!Array.isArray(items)) return true;
273
+ for (let i = 0; i < items.length; i += 1) {
274
+ for (let j = i + 1; j < items.length; j += 1) {
275
+ if (areJsonValuesEqual(items[i], items[j])) return false;
276
+ }
277
+ }
278
+ return true;
279
+ }, "Expected array items to be unique");
280
+ }
281
+ }
282
+ return withMeta(arr, opts);
283
+ }
284
+
285
+ function tTuple(items: readonly ZodType[], opts?: Meta): ZodType {
286
+ return withMeta(z.tuple(items as unknown as [ZodType, ...ZodType[]]) as unknown as ZodType, opts);
287
+ }
288
+
289
+ function isOptional(schema: ZodType): boolean {
290
+ const def = (schema as { _zod?: { def?: { type?: string } } })._zod?.def;
291
+ return def?.type === "optional";
292
+ }
293
+
294
+ function tObject<P extends ZodRawShape>(properties: P, opts?: ObjectOpts): ZodObject<P> {
295
+ // `z.object` automatically derives `required` from non-optional entries,
296
+ // so `Type.Optional(...)` flows through unchanged (Zod treats `.optional()`
297
+ // and `Type.Optional`-style wrappers identically).
298
+ let obj = z.object(properties);
299
+ const ap = opts?.additionalProperties;
300
+ if (ap === false) {
301
+ obj = obj.strict() as unknown as ZodObject<P>;
302
+ } else if (ap === undefined || ap === true) {
303
+ // TypeBox preserves unknown keys by default; Zod's default is `.strip()`.
304
+ obj = obj.loose() as unknown as ZodObject<P>;
305
+ } else {
306
+ obj = obj.catchall(ap) as unknown as ZodObject<P>;
307
+ }
308
+ return withMeta(obj, opts);
309
+ }
310
+
311
+ function tRecord<V extends ZodType>(key: ZodType, value: V, opts?: Meta): ZodType {
312
+ return withMeta(z.record(key as never, value as never) as unknown as ZodType, opts);
313
+ }
314
+
315
+ function tOptional<E extends ZodType>(schema: E, _opts?: Meta): ZodOptional<E> {
316
+ return isOptional(schema) ? (schema as unknown as ZodOptional<E>) : (schema.optional() as ZodOptional<E>);
317
+ }
318
+
319
+ function tNullable<E extends ZodType>(schema: E, opts?: Meta): ZodType {
320
+ return withMeta(schema.nullable() as ZodType, opts);
321
+ }
322
+
323
+ function tReadonly<E extends ZodType>(schema: E): E {
324
+ // TypeBox's `Type.Readonly` is purely a marker; runtime parsing is identical.
325
+ return schema;
326
+ }
327
+
328
+ function tPartial<P extends ZodRawShape>(obj: ZodObject<P>): ZodObject<P> {
329
+ return obj.partial() as unknown as ZodObject<P>;
330
+ }
331
+
332
+ function tRequired<P extends ZodRawShape>(obj: ZodObject<P>): ZodObject<P> {
333
+ return obj.required() as unknown as ZodObject<P>;
334
+ }
335
+
336
+ function tPick<P extends ZodRawShape, K extends keyof P>(obj: ZodObject<P>, keys: readonly K[]): ZodObject<Pick<P, K>> {
337
+ const mask = Object.fromEntries(keys.map(k => [k as string, true]));
338
+ return obj.pick(mask as never) as unknown as ZodObject<Pick<P, K>>;
339
+ }
340
+
341
+ function tOmit<P extends ZodRawShape, K extends keyof P>(obj: ZodObject<P>, keys: readonly K[]): ZodObject<Omit<P, K>> {
342
+ const mask = Object.fromEntries(keys.map(k => [k as string, true]));
343
+ return obj.omit(mask as never) as unknown as ZodObject<Omit<P, K>>;
344
+ }
345
+
346
+ function tComposite(objects: readonly ZodObject<ZodRawShape>[], opts?: Meta): ZodObject<ZodRawShape> {
347
+ // `Type.Composite([...])` flattens every object schema into one object schema
348
+ // rather than producing an intersection. Mirror that via repeated `extend`.
349
+ if (objects.length === 0) return withMeta(z.object({}), opts) as ZodObject<ZodRawShape>;
350
+ let out = objects[0] as ZodObject<ZodRawShape>;
351
+ for (let i = 1; i < objects.length; i += 1) {
352
+ out = out.extend(objects[i].shape) as ZodObject<ZodRawShape>;
353
+ }
354
+ return withMeta(out, opts) as ZodObject<ZodRawShape>;
355
+ }
356
+
357
+ // ---------------------------------------------------------------------------
358
+ // Public `Type` namespace
359
+ // ---------------------------------------------------------------------------
360
+
361
+ export const Type = {
362
+ String: tString,
363
+ Number: tNumber,
364
+ Integer: tInteger,
365
+ Boolean: tBoolean,
366
+ Null: tNull,
367
+ Any: tAny,
368
+ Unknown: tUnknown,
369
+ Never: tNever,
370
+ Literal: tLiteral,
371
+ Union: tUnion,
372
+ Intersect: tIntersect,
373
+ Enum: tEnum,
374
+ Array: tArray,
375
+ Tuple: tTuple,
376
+ Object: tObject,
377
+ Record: tRecord,
378
+ Optional: tOptional,
379
+ Nullable: tNullable,
380
+ Readonly: tReadonly,
381
+ Partial: tPartial,
382
+ Required: tRequired,
383
+ Pick: tPick,
384
+ Omit: tOmit,
385
+ Composite: tComposite,
386
+ } as const;
387
+
388
+ export type TypeBuilder = typeof Type;
389
+
390
+ /** Default namespace export so `import * as typebox from "./typebox"` still resolves the `Type` key. */
391
+ export default { Type };
@@ -2,7 +2,7 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
2
2
  import type { Component } from "@oh-my-pi/pi-tui";
3
3
  import { Text } from "@oh-my-pi/pi-tui";
4
4
  import { formatNumber, prompt } from "@oh-my-pi/pi-utils";
5
- import { type Static, Type } from "@sinclair/typebox";
5
+ import * as z from "zod/v4";
6
6
  import type { RenderResultOptions } from "../../extensibility/custom-tools/types";
7
7
  import type { Theme, ThemeColor } from "../../modes/theme/theme";
8
8
  import goalDescription from "../../prompts/tools/goal.md" with { type: "text" };
@@ -14,19 +14,13 @@ import { renderStatusLine, truncateToWidth } from "../../tui";
14
14
  import { completionBudgetReport, remainingTokens } from "../runtime";
15
15
  import type { Goal, GoalStatus, GoalToolDetails } from "../state";
16
16
 
17
- const goalSchema = Type.Object({
18
- op: Type.Union([Type.Literal("create"), Type.Literal("get"), Type.Literal("complete")], {
19
- description: "Goal operation.",
20
- }),
21
- objective: Type.Optional(Type.String({ description: "Goal objective. Required when op=create." })),
22
- token_budget: Type.Optional(
23
- Type.Integer({
24
- description: "Optional positive token budget. Only honored when op=create.",
25
- }),
26
- ),
17
+ const goalSchema = z.object({
18
+ op: z.union([z.literal("create"), z.literal("get"), z.literal("complete")]).describe("Goal operation."),
19
+ objective: z.string().describe("Goal objective. Required when op=create.").optional(),
20
+ token_budget: z.number().int().describe("Optional positive token budget. Only honored when op=create.").optional(),
27
21
  });
28
22
 
29
- export type GoalToolInput = Static<typeof goalSchema>;
23
+ export type GoalToolInput = z.infer<typeof goalSchema>;
30
24
 
31
25
  export interface GoalToolResponse {
32
26
  goal: Goal | null;
@@ -1,5 +1,4 @@
1
- import type { Static } from "@sinclair/typebox";
2
- import { Type } from "@sinclair/typebox";
1
+ import * as z from "zod/v4";
3
2
  import type { LspBatchRequest } from "../edit/renderer";
4
3
  import type { WritethroughCallback, WritethroughDeferredHandle } from "../lsp";
5
4
  import type { ToolSession } from "../tools";
@@ -26,8 +25,9 @@ export type HashlineEdit =
26
25
  | { kind: "insert"; cursor: HashlineCursor; text: string; lineNum: number; index: number }
27
26
  | { kind: "delete"; anchor: Anchor; lineNum: number; index: number; oldAssertion?: string };
28
27
 
29
- export const hashlineEditParamsSchema = Type.Object({ input: Type.String() });
30
- export type HashlineParams = Static<typeof hashlineEditParamsSchema>;
28
+ /** `path` is accepted by the edit tool runtime; other extra keys are preserved. */
29
+ export const hashlineEditParamsSchema = z.object({ input: z.string(), path: z.string().optional() }).passthrough();
30
+ export type HashlineParams = z.infer<typeof hashlineEditParamsSchema>;
31
31
 
32
32
  export interface HashlineStreamOptions {
33
33
  /** First line number to use when formatting (1-indexed). */