@oh-my-pi/pi-coding-agent 3.21.0 → 3.25.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 (71) hide show
  1. package/CHANGELOG.md +55 -1
  2. package/docs/sdk.md +47 -50
  3. package/examples/custom-tools/README.md +0 -15
  4. package/examples/hooks/custom-compaction.ts +1 -3
  5. package/examples/sdk/README.md +6 -10
  6. package/package.json +5 -5
  7. package/src/cli/args.ts +9 -6
  8. package/src/core/agent-session.ts +3 -3
  9. package/src/core/custom-commands/bundled/wt/index.ts +3 -0
  10. package/src/core/custom-tools/wrapper.ts +0 -1
  11. package/src/core/extensions/index.ts +1 -6
  12. package/src/core/extensions/wrapper.ts +0 -7
  13. package/src/core/file-mentions.ts +5 -8
  14. package/src/core/sdk.ts +48 -111
  15. package/src/core/session-manager.ts +7 -0
  16. package/src/core/system-prompt.ts +22 -33
  17. package/src/core/tools/ask.ts +14 -7
  18. package/src/core/tools/bash-interceptor.ts +4 -4
  19. package/src/core/tools/bash.ts +19 -9
  20. package/src/core/tools/complete.ts +131 -0
  21. package/src/core/tools/context.ts +7 -0
  22. package/src/core/tools/edit.ts +8 -15
  23. package/src/core/tools/exa/render.ts +4 -16
  24. package/src/core/tools/find.ts +7 -18
  25. package/src/core/tools/git.ts +13 -3
  26. package/src/core/tools/grep.ts +7 -18
  27. package/src/core/tools/index.test.ts +188 -0
  28. package/src/core/tools/index.ts +106 -236
  29. package/src/core/tools/jtd-to-json-schema.ts +274 -0
  30. package/src/core/tools/ls.ts +4 -9
  31. package/src/core/tools/lsp/index.ts +32 -29
  32. package/src/core/tools/lsp/render.ts +7 -28
  33. package/src/core/tools/notebook.ts +3 -5
  34. package/src/core/tools/output.ts +130 -31
  35. package/src/core/tools/read.ts +8 -19
  36. package/src/core/tools/review.ts +0 -18
  37. package/src/core/tools/rulebook.ts +8 -2
  38. package/src/core/tools/task/agents.ts +28 -7
  39. package/src/core/tools/task/artifacts.ts +6 -9
  40. package/src/core/tools/task/discovery.ts +0 -6
  41. package/src/core/tools/task/executor.ts +306 -257
  42. package/src/core/tools/task/index.ts +65 -235
  43. package/src/core/tools/task/name-generator.ts +247 -0
  44. package/src/core/tools/task/render.ts +158 -19
  45. package/src/core/tools/task/types.ts +13 -11
  46. package/src/core/tools/task/worker-protocol.ts +18 -0
  47. package/src/core/tools/task/worker.ts +270 -0
  48. package/src/core/tools/web-fetch.ts +4 -36
  49. package/src/core/tools/web-search/index.ts +2 -1
  50. package/src/core/tools/web-search/render.ts +1 -4
  51. package/src/core/tools/write.ts +7 -15
  52. package/src/discovery/helpers.test.ts +1 -1
  53. package/src/index.ts +5 -16
  54. package/src/main.ts +4 -4
  55. package/src/modes/interactive/theme/theme.ts +4 -4
  56. package/src/prompts/task.md +14 -57
  57. package/src/prompts/tools/output.md +4 -3
  58. package/src/prompts/tools/task.md +70 -0
  59. package/examples/custom-tools/question/index.ts +0 -84
  60. package/examples/custom-tools/subagent/README.md +0 -172
  61. package/examples/custom-tools/subagent/agents/planner.md +0 -37
  62. package/examples/custom-tools/subagent/agents/scout.md +0 -50
  63. package/examples/custom-tools/subagent/agents/worker.md +0 -24
  64. package/examples/custom-tools/subagent/agents.ts +0 -156
  65. package/examples/custom-tools/subagent/commands/implement-and-review.md +0 -10
  66. package/examples/custom-tools/subagent/commands/implement.md +0 -10
  67. package/examples/custom-tools/subagent/commands/scout-and-plan.md +0 -9
  68. package/examples/custom-tools/subagent/index.ts +0 -1002
  69. package/examples/sdk/05-tools.ts +0 -94
  70. package/examples/sdk/12-full-control.ts +0 -95
  71. package/src/prompts/browser.md +0 -71
@@ -1,14 +1,15 @@
1
1
  export { type AskToolDetails, askTool, createAskTool } from "./ask";
2
- export { type BashToolDetails, bashTool, createBashTool } from "./bash";
3
- export { createEditTool, type EditToolOptions, editTool } from "./edit";
2
+ export { type BashToolDetails, createBashTool } from "./bash";
3
+ export { createCompleteTool } from "./complete";
4
+ export { createEditTool } from "./edit";
4
5
  // Exa MCP tools (22 tools)
5
6
  export { exaTools } from "./exa/index";
6
7
  export type { ExaRenderDetails, ExaSearchResponse, ExaSearchResult } from "./exa/types";
7
- export { createFindTool, type FindToolDetails, findTool } from "./find";
8
+ export { createFindTool, type FindToolDetails } from "./find";
8
9
  export { setPreferredImageProvider } from "./gemini-image";
9
10
  export { createGitTool, type GitToolDetails, gitTool } from "./git";
10
- export { createGrepTool, type GrepToolDetails, grepTool } from "./grep";
11
- export { createLsTool, type LsToolDetails, lsTool } from "./ls";
11
+ export { createGrepTool, type GrepToolDetails } from "./grep";
12
+ export { createLsTool, type LsToolDetails } from "./ls";
12
13
  export {
13
14
  createLspTool,
14
15
  type FileDiagnosticsResult,
@@ -20,14 +21,14 @@ export {
20
21
  lspTool,
21
22
  warmupLspServers,
22
23
  } from "./lsp/index";
23
- export { createNotebookTool, type NotebookToolDetails, notebookTool } from "./notebook";
24
- export { createOutputTool, type OutputToolDetails, outputTool } from "./output";
25
- export { createReadTool, type ReadToolDetails, type ReadToolOptions, readTool } from "./read";
26
- export { createReportFindingTool, createSubmitReviewTool, reportFindingTool, submitReviewTool } from "./review";
27
- export { createRulebookTool, filterRulebookRules, formatRulesForPrompt, type RulebookToolDetails } from "./rulebook";
24
+ export { createNotebookTool, type NotebookToolDetails } from "./notebook";
25
+ export { createOutputTool, type OutputToolDetails } from "./output";
26
+ export { createReadTool, type ReadToolDetails } from "./read";
27
+ export { reportFindingTool, submitReviewTool } from "./review";
28
+ export { filterRulebookRules, formatRulesForPrompt, type RulebookToolDetails } from "./rulebook";
28
29
  export { BUNDLED_AGENTS, createTaskTool, taskTool } from "./task/index";
29
30
  export type { TruncationResult } from "./truncate";
30
- export { createWebFetchTool, type WebFetchToolDetails, webFetchCustomTool, webFetchTool } from "./web-fetch";
31
+ export { createWebFetchTool, type WebFetchToolDetails } from "./web-fetch";
31
32
  export {
32
33
  companyWebSearchTools,
33
34
  createWebSearchTool,
@@ -47,247 +48,116 @@ export {
47
48
  webSearchLinkedinTool,
48
49
  webSearchTool,
49
50
  } from "./web-search/index";
50
- export { createWriteTool, type WriteToolDetails, type WriteToolOptions, writeTool } from "./write";
51
+ export { createWriteTool, type WriteToolDetails } from "./write";
51
52
 
52
53
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
53
- import { askTool, createAskTool } from "./ask";
54
- import { bashTool, createBashTool } from "./bash";
55
- import { checkBashInterception, checkSimpleLsInterception } from "./bash-interceptor";
56
- import { createEditTool, editTool } from "./edit";
57
- import { createFindTool, findTool } from "./find";
58
- import { createGitTool, gitTool } from "./git";
59
- import { createGrepTool, grepTool } from "./grep";
60
- import { createLsTool, lsTool } from "./ls";
61
- import { createLspTool, createLspWritethrough, lspTool } from "./lsp/index";
62
- import { createNotebookTool, notebookTool } from "./notebook";
63
- import { createOutputTool, outputTool } from "./output";
64
- import { createReadTool, readTool } from "./read";
65
- import { createReportFindingTool, createSubmitReviewTool, reportFindingTool, submitReviewTool } from "./review";
66
- import { createTaskTool, taskTool } from "./task/index";
67
- import { createWebFetchTool, webFetchTool } from "./web-fetch";
68
- import { createWebSearchTool, webSearchTool } from "./web-search/index";
69
- import { createWriteTool, writeTool } from "./write";
54
+ import type { Rule } from "../../capability/rule";
55
+ import type { EventBus } from "../event-bus";
56
+ import { createAskTool } from "./ask";
57
+ import { createBashTool } from "./bash";
58
+ import { createCompleteTool } from "./complete";
59
+ import { createEditTool } from "./edit";
60
+ import { createFindTool } from "./find";
61
+ import { createGitTool } from "./git";
62
+ import { createGrepTool } from "./grep";
63
+ import { createLsTool } from "./ls";
64
+ import { createLspTool } from "./lsp/index";
65
+ import { createNotebookTool } from "./notebook";
66
+ import { createOutputTool } from "./output";
67
+ import { createReadTool } from "./read";
68
+ import { reportFindingTool, submitReviewTool } from "./review";
69
+ import { createRulebookTool } from "./rulebook";
70
+ import { createTaskTool } from "./task/index";
71
+ import { createWebFetchTool } from "./web-fetch";
72
+ import { createWebSearchTool } from "./web-search/index";
73
+ import { createWriteTool } from "./write";
70
74
 
71
75
  /** Tool type (AgentTool from pi-ai) */
72
76
  export type Tool = AgentTool<any, any, any>;
73
77
 
74
- /** Context for tools that need session information */
75
- export interface SessionContext {
78
+ /** Session context for tool factories */
79
+ export interface ToolSession {
80
+ /** Current working directory */
81
+ cwd: string;
82
+ /** Whether UI is available */
83
+ hasUI: boolean;
84
+ /** Rulebook rules */
85
+ rulebookRules: Rule[];
86
+ /** Event bus for tool/extension communication */
87
+ eventBus?: EventBus;
88
+ /** Output schema for structured completion (subagents) */
89
+ outputSchema?: unknown;
90
+ /** Whether to include the complete tool by default */
91
+ requireCompleteTool?: boolean;
92
+ /** Get session file */
76
93
  getSessionFile: () => string | null;
94
+ /** Get session spawns */
95
+ getSessionSpawns: () => string | null;
96
+ /** Settings manager (optional) */
97
+ settings?: {
98
+ getImageAutoResize(): boolean;
99
+ getLspFormatOnWrite(): boolean;
100
+ getLspDiagnosticsOnWrite(): boolean;
101
+ getLspDiagnosticsOnEdit(): boolean;
102
+ getEditFuzzyMatch(): boolean;
103
+ getGitToolEnabled(): boolean;
104
+ getBashInterceptorEnabled(): boolean;
105
+ };
77
106
  }
78
107
 
79
- /** Options for creating coding tools */
80
- export interface CodingToolsOptions {
81
- /** Whether to fetch LSP diagnostics after write tool writes files (default: true) */
82
- lspDiagnosticsOnWrite?: boolean;
83
- /** Whether to fetch LSP diagnostics after edit tool edits files (default: false) */
84
- lspDiagnosticsOnEdit?: boolean;
85
- /** Whether to format files using LSP after write tool writes (default: true) */
86
- lspFormatOnWrite?: boolean;
87
- /** Whether to accept high-confidence fuzzy matches in edit tool (default: true) */
88
- editFuzzyMatch?: boolean;
89
- /** Whether to auto-resize images to 2000x2000 max in read tool (default: true) */
90
- readAutoResizeImages?: boolean;
91
- /** Set of tool names available to the agent (for cross-tool awareness) */
92
- availableTools?: Set<string>;
93
- }
94
-
95
- // Factory function type
96
- type ToolFactory = (cwd: string, sessionContext?: SessionContext, options?: CodingToolsOptions) => Tool | Promise<Tool>;
97
-
98
- // Tool definitions: static tools and their factory functions
99
- const toolDefs: Record<string, { tool: Tool; create: ToolFactory }> = {
100
- ask: { tool: askTool, create: createAskTool },
101
- read: {
102
- tool: readTool,
103
- create: (cwd, _ctx, options) => createReadTool(cwd, { autoResizeImages: options?.readAutoResizeImages ?? true }),
104
- },
105
- bash: { tool: bashTool, create: createBashTool },
106
- edit: {
107
- tool: editTool,
108
- create: (cwd, _ctx, options) => {
109
- const enableDiagnostics = options?.lspDiagnosticsOnEdit ?? false;
110
- const enableFormat = options?.lspFormatOnWrite ?? true;
111
- const writethrough = createLspWritethrough(cwd, {
112
- enableFormat,
113
- enableDiagnostics,
114
- });
115
- return createEditTool(cwd, { fuzzyMatch: options?.editFuzzyMatch ?? true, writethrough });
116
- },
117
- },
118
- write: {
119
- tool: writeTool,
120
- create: (cwd, _ctx, options) => {
121
- const enableFormat = options?.lspFormatOnWrite ?? true;
122
- const enableDiagnostics = options?.lspDiagnosticsOnWrite ?? true;
123
- const writethrough = createLspWritethrough(cwd, {
124
- enableFormat,
125
- enableDiagnostics,
126
- });
127
- return createWriteTool(cwd, { writethrough });
128
- },
129
- },
130
- grep: { tool: grepTool, create: createGrepTool },
131
- find: { tool: findTool, create: createFindTool },
132
- git: { tool: gitTool, create: createGitTool },
133
- ls: { tool: lsTool, create: createLsTool },
134
- lsp: { tool: lspTool, create: createLspTool },
135
- notebook: { tool: notebookTool, create: createNotebookTool },
136
- output: { tool: outputTool, create: (cwd, ctx) => createOutputTool(cwd, ctx) },
137
- task: { tool: taskTool, create: (cwd, ctx, opts) => createTaskTool(cwd, ctx, opts) },
138
- web_fetch: { tool: webFetchTool, create: createWebFetchTool },
139
- web_search: { tool: webSearchTool, create: createWebSearchTool },
140
- report_finding: { tool: reportFindingTool, create: createReportFindingTool },
141
- submit_review: { tool: submitReviewTool, create: createSubmitReviewTool },
108
+ type ToolFactory = (session: ToolSession) => Tool | null | Promise<Tool | null>;
109
+
110
+ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
111
+ ask: createAskTool,
112
+ bash: createBashTool,
113
+ edit: createEditTool,
114
+ find: createFindTool,
115
+ git: createGitTool,
116
+ grep: createGrepTool,
117
+ ls: createLsTool,
118
+ lsp: createLspTool,
119
+ notebook: createNotebookTool,
120
+ output: createOutputTool,
121
+ read: createReadTool,
122
+ rulebook: createRulebookTool,
123
+ task: createTaskTool,
124
+ web_fetch: createWebFetchTool,
125
+ web_search: createWebSearchTool,
126
+ write: createWriteTool,
142
127
  };
143
128
 
144
- export type ToolName = keyof typeof toolDefs;
145
-
146
- // Tools that require UI (excluded when hasUI is false)
147
- const uiToolNames: ToolName[] = ["ask"];
148
-
149
- // Tool sets defined by name (base sets, without UI-only tools)
150
- export const baseCodingToolNames: ToolName[] = [
151
- "read",
152
- "bash",
153
- "edit",
154
- "write",
155
- "grep",
156
- "find",
157
- "git",
158
- "ls",
159
- "lsp",
160
- "notebook",
161
- "output",
162
- "task",
163
- "web_fetch",
164
- "web_search",
165
- ];
166
- const baseReadOnlyToolNames: ToolName[] = ["read", "grep", "find", "ls"];
167
-
168
- // Default tools for full access mode (using process.cwd(), no UI)
169
- export const codingTools: Tool[] = baseCodingToolNames.map((name) => toolDefs[name].tool);
170
-
171
- // Read-only tools for exploration without modification (using process.cwd(), no UI)
172
- export const readOnlyTools: Tool[] = baseReadOnlyToolNames.map((name) => toolDefs[name].tool);
173
-
174
- // All available tools (using process.cwd(), no UI)
175
- export const allTools = Object.fromEntries(Object.entries(toolDefs).map(([name, def]) => [name, def.tool])) as Record<
176
- ToolName,
177
- Tool
178
- >;
179
-
180
- /**
181
- * Create coding tools configured for a specific working directory.
182
- * @param cwd - Working directory for tools
183
- * @param hasUI - Whether UI is available (includes ask tool if true)
184
- * @param sessionContext - Optional session context for tools that need it
185
- * @param options - Options for tool configuration
186
- */
187
- export async function createCodingTools(
188
- cwd: string,
189
- hasUI = false,
190
- sessionContext?: SessionContext,
191
- options?: CodingToolsOptions,
192
- ): Promise<Tool[]> {
193
- const names = hasUI ? [...baseCodingToolNames, ...uiToolNames] : baseCodingToolNames;
194
- const optionsWithTools = { ...options, availableTools: new Set(names) };
195
- return Promise.all(names.map((name) => toolDefs[name].create(cwd, sessionContext, optionsWithTools)));
196
- }
197
-
198
- /**
199
- * Create read-only tools configured for a specific working directory.
200
- * @param cwd - Working directory for tools
201
- * @param hasUI - Whether UI is available (includes ask tool if true)
202
- * @param sessionContext - Optional session context for tools that need it
203
- * @param options - Options for tool configuration
204
- */
205
- export async function createReadOnlyTools(
206
- cwd: string,
207
- hasUI = false,
208
- sessionContext?: SessionContext,
209
- options?: CodingToolsOptions,
210
- ): Promise<Tool[]> {
211
- const names = hasUI ? [...baseReadOnlyToolNames, ...uiToolNames] : baseReadOnlyToolNames;
212
- const optionsWithTools = { ...options, availableTools: new Set(names) };
213
- return Promise.all(names.map((name) => toolDefs[name].create(cwd, sessionContext, optionsWithTools)));
214
- }
215
-
216
- /**
217
- * Create all tools configured for a specific working directory.
218
- * @param cwd - Working directory for tools
219
- * @param sessionContext - Optional session context for tools that need it
220
- * @param options - Options for tool configuration
221
- */
222
- export async function createAllTools(
223
- cwd: string,
224
- sessionContext?: SessionContext,
225
- options?: CodingToolsOptions,
226
- ): Promise<Record<ToolName, Tool>> {
227
- const names = Object.keys(toolDefs);
228
- const optionsWithTools = { ...options, availableTools: new Set(names) };
229
- const entries = await Promise.all(
230
- Object.entries(toolDefs).map(async ([name, def]) => [
231
- name,
232
- await def.create(cwd, sessionContext, optionsWithTools),
233
- ]),
234
- );
235
- return Object.fromEntries(entries) as Record<ToolName, Tool>;
236
- }
237
-
238
- /**
239
- * Wrap a bash tool with interception that redirects common patterns to specialized tools.
240
- * This helps prevent LLMs from falling back to shell commands when better tools exist.
241
- *
242
- * @param bashTool - The bash tool to wrap
243
- * @param availableTools - Set of tool names that are available (for context-aware blocking)
244
- * @returns Wrapped bash tool with interception
245
- */
246
- export function wrapBashWithInterception(bashTool: Tool, availableTools: Set<string>): Tool {
247
- const originalExecute = bashTool.execute;
248
-
249
- return {
250
- ...bashTool,
251
- execute: async (toolCallId, params, signal, onUpdate, context) => {
252
- const command = (params as { command: string }).command;
253
-
254
- // Check for forbidden patterns
255
- const interception = checkBashInterception(command, availableTools);
256
- if (interception.block) {
257
- throw new Error(interception.message);
258
- }
129
+ export const HIDDEN_TOOLS: Record<string, ToolFactory> = {
130
+ complete: createCompleteTool,
131
+ report_finding: () => reportFindingTool,
132
+ submit_review: () => submitReviewTool,
133
+ };
259
134
 
260
- // Check for simple ls that should use ls tool
261
- const lsInterception = checkSimpleLsInterception(command, availableTools);
262
- if (lsInterception.block) {
263
- throw new Error(lsInterception.message);
264
- }
265
-
266
- // Pass through to original bash tool
267
- return originalExecute(toolCallId, params, signal, onUpdate, context);
268
- },
269
- };
270
- }
135
+ export type ToolName = keyof typeof BUILTIN_TOOLS;
271
136
 
272
137
  /**
273
- * Apply bash interception to a set of tools.
274
- * Finds the bash tool and wraps it with interception based on other available tools.
275
- *
276
- * @param tools - Array of tools to process
277
- * @returns Tools with bash interception applied
138
+ * Create tools from BUILTIN_TOOLS registry.
278
139
  */
279
- export function applyBashInterception(tools: Tool[]): Tool[] {
280
- const toolNames = new Set(tools.map((t) => t.name));
140
+ export async function createTools(session: ToolSession, toolNames?: string[]): Promise<Tool[]> {
141
+ const includeComplete = session.requireCompleteTool === true;
142
+ const requestedTools = toolNames && toolNames.length > 0 ? [...new Set(toolNames)] : undefined;
143
+ const allTools: Record<string, ToolFactory> = { ...BUILTIN_TOOLS, ...HIDDEN_TOOLS };
144
+ if (includeComplete && requestedTools && !requestedTools.includes("complete")) {
145
+ requestedTools.push("complete");
146
+ }
281
147
 
282
- // If bash isn't in the tools, nothing to do
283
- if (!toolNames.has("bash")) {
284
- return tools;
148
+ const entries = requestedTools
149
+ ? requestedTools.filter((name) => name in allTools).map((name) => [name, allTools[name]] as const)
150
+ : [
151
+ ...Object.entries(BUILTIN_TOOLS),
152
+ ...(includeComplete ? ([["complete", HIDDEN_TOOLS.complete]] as const) : []),
153
+ ];
154
+ const results = await Promise.all(entries.map(([, factory]) => factory(session)));
155
+ const tools = results.filter((t): t is Tool => t !== null);
156
+
157
+ if (requestedTools) {
158
+ const allowed = new Set(requestedTools);
159
+ return tools.filter((tool) => allowed.has(tool.name));
285
160
  }
286
161
 
287
- return tools.map((tool) => {
288
- if (tool.name === "bash") {
289
- return wrapBashWithInterception(tool, toolNames);
290
- }
291
- return tool;
292
- });
162
+ return tools;
293
163
  }
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Convert JSON Type Definition (JTD) to JSON Schema.
3
+ *
4
+ * JTD (RFC 8927) is a simpler schema format. This converter allows users to
5
+ * write schemas in JTD and have them converted to JSON Schema for model APIs.
6
+ *
7
+ * @see https://jsontypedef.com/
8
+ * @see https://datatracker.ietf.org/doc/html/rfc8927
9
+ */
10
+
11
+ type JTDPrimitive =
12
+ | "boolean"
13
+ | "string"
14
+ | "timestamp"
15
+ | "float32"
16
+ | "float64"
17
+ | "int8"
18
+ | "uint8"
19
+ | "int16"
20
+ | "uint16"
21
+ | "int32"
22
+ | "uint32";
23
+
24
+ interface JTDType {
25
+ type: JTDPrimitive;
26
+ }
27
+
28
+ interface JTDEnum {
29
+ enum: string[];
30
+ }
31
+
32
+ interface JTDElements {
33
+ elements: JTDSchema;
34
+ }
35
+
36
+ interface JTDValues {
37
+ values: JTDSchema;
38
+ }
39
+
40
+ interface JTDProperties {
41
+ properties?: Record<string, JTDSchema>;
42
+ optionalProperties?: Record<string, JTDSchema>;
43
+ }
44
+
45
+ interface JTDDiscriminator {
46
+ discriminator: string;
47
+ mapping: Record<string, JTDProperties>;
48
+ }
49
+
50
+ interface JTDRef {
51
+ ref: string;
52
+ }
53
+
54
+ interface JTDEmpty {}
55
+
56
+ type JTDSchema =
57
+ | JTDType
58
+ | JTDEnum
59
+ | JTDElements
60
+ | JTDValues
61
+ | JTDProperties
62
+ | JTDDiscriminator
63
+ | JTDRef
64
+ | JTDEmpty;
65
+
66
+ const primitiveMap: Record<JTDPrimitive, string> = {
67
+ boolean: "boolean",
68
+ string: "string",
69
+ timestamp: "string", // ISO 8601
70
+ float32: "number",
71
+ float64: "number",
72
+ int8: "integer",
73
+ uint8: "integer",
74
+ int16: "integer",
75
+ uint16: "integer",
76
+ int32: "integer",
77
+ uint32: "integer",
78
+ };
79
+
80
+ function isJTDType(schema: unknown): schema is JTDType {
81
+ return typeof schema === "object" && schema !== null && "type" in schema;
82
+ }
83
+
84
+ function isJTDEnum(schema: unknown): schema is JTDEnum {
85
+ return typeof schema === "object" && schema !== null && "enum" in schema;
86
+ }
87
+
88
+ function isJTDElements(schema: unknown): schema is JTDElements {
89
+ return typeof schema === "object" && schema !== null && "elements" in schema;
90
+ }
91
+
92
+ function isJTDValues(schema: unknown): schema is JTDValues {
93
+ return typeof schema === "object" && schema !== null && "values" in schema;
94
+ }
95
+
96
+ function isJTDProperties(schema: unknown): schema is JTDProperties {
97
+ return (
98
+ typeof schema === "object" &&
99
+ schema !== null &&
100
+ ("properties" in schema || "optionalProperties" in schema)
101
+ );
102
+ }
103
+
104
+ function isJTDDiscriminator(schema: unknown): schema is JTDDiscriminator {
105
+ return typeof schema === "object" && schema !== null && "discriminator" in schema;
106
+ }
107
+
108
+ function isJTDRef(schema: unknown): schema is JTDRef {
109
+ return typeof schema === "object" && schema !== null && "ref" in schema;
110
+ }
111
+
112
+ function convertSchema(schema: unknown): unknown {
113
+ if (schema === null || typeof schema !== "object") {
114
+ return {};
115
+ }
116
+
117
+ // Type form: { type: "string" } → { type: "string" }
118
+ if (isJTDType(schema)) {
119
+ const jsonType = primitiveMap[schema.type as JTDPrimitive];
120
+ if (!jsonType) {
121
+ return { type: schema.type };
122
+ }
123
+ const result: Record<string, unknown> = { type: jsonType };
124
+ // Add format for timestamp
125
+ if (schema.type === "timestamp") {
126
+ result.format = "date-time";
127
+ }
128
+ return result;
129
+ }
130
+
131
+ // Enum form: { enum: ["a", "b"] } → { enum: ["a", "b"] }
132
+ if (isJTDEnum(schema)) {
133
+ return { enum: schema.enum };
134
+ }
135
+
136
+ // Elements form: { elements: { type: "string" } } → { type: "array", items: ... }
137
+ if (isJTDElements(schema)) {
138
+ return {
139
+ type: "array",
140
+ items: convertSchema(schema.elements),
141
+ };
142
+ }
143
+
144
+ // Values form: { values: { type: "string" } } → { type: "object", additionalProperties: ... }
145
+ if (isJTDValues(schema)) {
146
+ return {
147
+ type: "object",
148
+ additionalProperties: convertSchema(schema.values),
149
+ };
150
+ }
151
+
152
+ // Properties form: { properties: {...}, optionalProperties: {...} }
153
+ if (isJTDProperties(schema)) {
154
+ const properties: Record<string, unknown> = {};
155
+ const required: string[] = [];
156
+
157
+ // Required properties
158
+ if (schema.properties) {
159
+ for (const [key, value] of Object.entries(schema.properties)) {
160
+ properties[key] = convertSchema(value);
161
+ required.push(key);
162
+ }
163
+ }
164
+
165
+ // Optional properties
166
+ if (schema.optionalProperties) {
167
+ for (const [key, value] of Object.entries(schema.optionalProperties)) {
168
+ properties[key] = convertSchema(value);
169
+ }
170
+ }
171
+
172
+ const result: Record<string, unknown> = {
173
+ type: "object",
174
+ properties,
175
+ additionalProperties: false,
176
+ };
177
+
178
+ if (required.length > 0) {
179
+ result.required = required;
180
+ }
181
+
182
+ return result;
183
+ }
184
+
185
+ // Discriminator form: { discriminator: "type", mapping: { ... } }
186
+ if (isJTDDiscriminator(schema)) {
187
+ const oneOf: unknown[] = [];
188
+
189
+ for (const [tag, props] of Object.entries(schema.mapping)) {
190
+ const converted = convertSchema(props) as Record<string, unknown>;
191
+ // Add the discriminator property
192
+ const properties = (converted.properties || {}) as Record<string, unknown>;
193
+ properties[schema.discriminator] = { const: tag };
194
+
195
+ const required = ((converted.required as string[]) || []).slice();
196
+ if (!required.includes(schema.discriminator)) {
197
+ required.push(schema.discriminator);
198
+ }
199
+
200
+ oneOf.push({
201
+ ...converted,
202
+ properties,
203
+ required,
204
+ });
205
+ }
206
+
207
+ return { oneOf };
208
+ }
209
+
210
+ // Ref form: { ref: "MyType" } → { $ref: "#/$defs/MyType" }
211
+ if (isJTDRef(schema)) {
212
+ return { $ref: `#/$defs/${schema.ref}` };
213
+ }
214
+
215
+ // Empty form: {} → {} (accepts anything)
216
+ return {};
217
+ }
218
+
219
+ /**
220
+ * Detect if a schema is JTD format (vs JSON Schema).
221
+ *
222
+ * JTD schemas use: type (primitives), properties, optionalProperties, elements, values, enum, discriminator, ref
223
+ * JSON Schema uses: type: "object", type: "array", items, additionalProperties, etc.
224
+ */
225
+ export function isJTDSchema(schema: unknown): boolean {
226
+ if (schema === null || typeof schema !== "object") {
227
+ return false;
228
+ }
229
+
230
+ const obj = schema as Record<string, unknown>;
231
+
232
+ // JTD-specific keywords
233
+ if ("elements" in obj) return true;
234
+ if ("values" in obj) return true;
235
+ if ("optionalProperties" in obj) return true;
236
+ if ("discriminator" in obj) return true;
237
+ if ("ref" in obj) return true;
238
+
239
+ // JTD type primitives (JSON Schema doesn't have int32, float64, etc.)
240
+ if ("type" in obj) {
241
+ const jtdPrimitives = [
242
+ "timestamp",
243
+ "float32",
244
+ "float64",
245
+ "int8",
246
+ "uint8",
247
+ "int16",
248
+ "uint16",
249
+ "int32",
250
+ "uint32",
251
+ ];
252
+ if (jtdPrimitives.includes(obj.type as string)) {
253
+ return true;
254
+ }
255
+ }
256
+
257
+ // JTD properties form without type: "object" (JSON Schema requires it)
258
+ if ("properties" in obj && !("type" in obj)) {
259
+ return true;
260
+ }
261
+
262
+ return false;
263
+ }
264
+
265
+ /**
266
+ * Convert JTD schema to JSON Schema.
267
+ * If already JSON Schema, returns as-is.
268
+ */
269
+ export function jtdToJsonSchema(schema: unknown): unknown {
270
+ if (!isJTDSchema(schema)) {
271
+ return schema;
272
+ }
273
+ return convertSchema(schema);
274
+ }