@oh-my-pi/pi-coding-agent 3.25.0 → 3.31.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 (157) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/package.json +5 -5
  3. package/src/cli/args.ts +4 -0
  4. package/src/core/agent-session.ts +29 -2
  5. package/src/core/bash-executor.ts +2 -1
  6. package/src/core/custom-commands/bundled/review/index.ts +369 -14
  7. package/src/core/custom-commands/bundled/wt/index.ts +1 -1
  8. package/src/core/session-manager.ts +158 -246
  9. package/src/core/session-storage.ts +379 -0
  10. package/src/core/settings-manager.ts +155 -4
  11. package/src/core/system-prompt.ts +62 -64
  12. package/src/core/tools/ask.ts +5 -4
  13. package/src/core/tools/bash-interceptor.ts +26 -61
  14. package/src/core/tools/bash.ts +13 -8
  15. package/src/core/tools/complete.ts +2 -4
  16. package/src/core/tools/edit-diff.ts +11 -4
  17. package/src/core/tools/edit.ts +7 -13
  18. package/src/core/tools/find.ts +111 -50
  19. package/src/core/tools/gemini-image.ts +128 -147
  20. package/src/core/tools/grep.ts +397 -415
  21. package/src/core/tools/index.test.ts +5 -1
  22. package/src/core/tools/index.ts +6 -8
  23. package/src/core/tools/jtd-to-json-schema.ts +174 -196
  24. package/src/core/tools/ls.ts +12 -10
  25. package/src/core/tools/lsp/client.ts +58 -9
  26. package/src/core/tools/lsp/config.ts +205 -656
  27. package/src/core/tools/lsp/defaults.json +465 -0
  28. package/src/core/tools/lsp/index.ts +55 -32
  29. package/src/core/tools/lsp/rust-analyzer.ts +49 -10
  30. package/src/core/tools/lsp/types.ts +1 -0
  31. package/src/core/tools/lsp/utils.ts +1 -1
  32. package/src/core/tools/read.ts +152 -76
  33. package/src/core/tools/render-utils.ts +70 -10
  34. package/src/core/tools/review.ts +38 -126
  35. package/src/core/tools/task/artifacts.ts +5 -4
  36. package/src/core/tools/task/executor.ts +204 -67
  37. package/src/core/tools/task/index.ts +129 -92
  38. package/src/core/tools/task/name-generator.ts +1544 -214
  39. package/src/core/tools/task/parallel.ts +30 -3
  40. package/src/core/tools/task/render.ts +85 -39
  41. package/src/core/tools/task/types.ts +34 -11
  42. package/src/core/tools/task/worker.ts +152 -27
  43. package/src/core/tools/web-fetch.ts +220 -1657
  44. package/src/core/tools/web-scrapers/academic.test.ts +239 -0
  45. package/src/core/tools/web-scrapers/artifacthub.ts +215 -0
  46. package/src/core/tools/web-scrapers/arxiv.ts +88 -0
  47. package/src/core/tools/web-scrapers/aur.ts +175 -0
  48. package/src/core/tools/web-scrapers/biorxiv.ts +141 -0
  49. package/src/core/tools/web-scrapers/bluesky.ts +284 -0
  50. package/src/core/tools/web-scrapers/brew.ts +177 -0
  51. package/src/core/tools/web-scrapers/business.test.ts +82 -0
  52. package/src/core/tools/web-scrapers/cheatsh.ts +78 -0
  53. package/src/core/tools/web-scrapers/chocolatey.ts +158 -0
  54. package/src/core/tools/web-scrapers/choosealicense.ts +110 -0
  55. package/src/core/tools/web-scrapers/cisa-kev.ts +100 -0
  56. package/src/core/tools/web-scrapers/clojars.ts +180 -0
  57. package/src/core/tools/web-scrapers/coingecko.ts +184 -0
  58. package/src/core/tools/web-scrapers/crates-io.ts +128 -0
  59. package/src/core/tools/web-scrapers/crossref.ts +149 -0
  60. package/src/core/tools/web-scrapers/dev-platforms.test.ts +254 -0
  61. package/src/core/tools/web-scrapers/devto.ts +177 -0
  62. package/src/core/tools/web-scrapers/discogs.ts +308 -0
  63. package/src/core/tools/web-scrapers/discourse.ts +221 -0
  64. package/src/core/tools/web-scrapers/dockerhub.ts +160 -0
  65. package/src/core/tools/web-scrapers/documentation.test.ts +85 -0
  66. package/src/core/tools/web-scrapers/fdroid.ts +158 -0
  67. package/src/core/tools/web-scrapers/finance-media.test.ts +144 -0
  68. package/src/core/tools/web-scrapers/firefox-addons.ts +214 -0
  69. package/src/core/tools/web-scrapers/flathub.ts +239 -0
  70. package/src/core/tools/web-scrapers/git-hosting.test.ts +272 -0
  71. package/src/core/tools/web-scrapers/github-gist.ts +68 -0
  72. package/src/core/tools/web-scrapers/github.ts +455 -0
  73. package/src/core/tools/web-scrapers/gitlab.ts +456 -0
  74. package/src/core/tools/web-scrapers/go-pkg.ts +275 -0
  75. package/src/core/tools/web-scrapers/hackage.ts +94 -0
  76. package/src/core/tools/web-scrapers/hackernews.ts +208 -0
  77. package/src/core/tools/web-scrapers/hex.ts +121 -0
  78. package/src/core/tools/web-scrapers/huggingface.ts +385 -0
  79. package/src/core/tools/web-scrapers/iacr.ts +86 -0
  80. package/src/core/tools/web-scrapers/index.ts +250 -0
  81. package/src/core/tools/web-scrapers/jetbrains-marketplace.ts +169 -0
  82. package/src/core/tools/web-scrapers/lemmy.ts +220 -0
  83. package/src/core/tools/web-scrapers/lobsters.ts +186 -0
  84. package/src/core/tools/web-scrapers/mastodon.ts +310 -0
  85. package/src/core/tools/web-scrapers/maven.ts +152 -0
  86. package/src/core/tools/web-scrapers/mdn.ts +174 -0
  87. package/src/core/tools/web-scrapers/media.test.ts +138 -0
  88. package/src/core/tools/web-scrapers/metacpan.ts +253 -0
  89. package/src/core/tools/web-scrapers/musicbrainz.ts +273 -0
  90. package/src/core/tools/web-scrapers/npm.ts +114 -0
  91. package/src/core/tools/web-scrapers/nuget.ts +205 -0
  92. package/src/core/tools/web-scrapers/nvd.ts +243 -0
  93. package/src/core/tools/web-scrapers/ollama.ts +267 -0
  94. package/src/core/tools/web-scrapers/open-vsx.ts +119 -0
  95. package/src/core/tools/web-scrapers/opencorporates.ts +275 -0
  96. package/src/core/tools/web-scrapers/openlibrary.ts +319 -0
  97. package/src/core/tools/web-scrapers/orcid.ts +299 -0
  98. package/src/core/tools/web-scrapers/osv.ts +189 -0
  99. package/src/core/tools/web-scrapers/package-managers-2.test.ts +199 -0
  100. package/src/core/tools/web-scrapers/package-managers.test.ts +171 -0
  101. package/src/core/tools/web-scrapers/package-registries.test.ts +259 -0
  102. package/src/core/tools/web-scrapers/packagist.ts +174 -0
  103. package/src/core/tools/web-scrapers/pub-dev.ts +185 -0
  104. package/src/core/tools/web-scrapers/pubmed.ts +178 -0
  105. package/src/core/tools/web-scrapers/pypi.ts +129 -0
  106. package/src/core/tools/web-scrapers/rawg.ts +124 -0
  107. package/src/core/tools/web-scrapers/readthedocs.ts +126 -0
  108. package/src/core/tools/web-scrapers/reddit.ts +104 -0
  109. package/src/core/tools/web-scrapers/repology.ts +262 -0
  110. package/src/core/tools/web-scrapers/research.test.ts +107 -0
  111. package/src/core/tools/web-scrapers/rfc.ts +209 -0
  112. package/src/core/tools/web-scrapers/rubygems.ts +117 -0
  113. package/src/core/tools/web-scrapers/searchcode.ts +217 -0
  114. package/src/core/tools/web-scrapers/sec-edgar.ts +274 -0
  115. package/src/core/tools/web-scrapers/security.test.ts +103 -0
  116. package/src/core/tools/web-scrapers/semantic-scholar.ts +190 -0
  117. package/src/core/tools/web-scrapers/snapcraft.ts +200 -0
  118. package/src/core/tools/web-scrapers/social-extended.test.ts +192 -0
  119. package/src/core/tools/web-scrapers/social.test.ts +259 -0
  120. package/src/core/tools/web-scrapers/sourcegraph.ts +373 -0
  121. package/src/core/tools/web-scrapers/spdx.ts +121 -0
  122. package/src/core/tools/web-scrapers/spotify.ts +218 -0
  123. package/src/core/tools/web-scrapers/stackexchange.test.ts +120 -0
  124. package/src/core/tools/web-scrapers/stackoverflow.ts +124 -0
  125. package/src/core/tools/web-scrapers/standards.test.ts +122 -0
  126. package/src/core/tools/web-scrapers/terraform.ts +304 -0
  127. package/src/core/tools/web-scrapers/tldr.ts +51 -0
  128. package/src/core/tools/web-scrapers/twitter.ts +96 -0
  129. package/src/core/tools/web-scrapers/types.ts +234 -0
  130. package/src/core/tools/web-scrapers/utils.ts +162 -0
  131. package/src/core/tools/web-scrapers/vimeo.ts +152 -0
  132. package/src/core/tools/web-scrapers/vscode-marketplace.ts +195 -0
  133. package/src/core/tools/web-scrapers/w3c.ts +163 -0
  134. package/src/core/tools/web-scrapers/wikidata.ts +357 -0
  135. package/src/core/tools/web-scrapers/wikipedia.test.ts +73 -0
  136. package/src/core/tools/web-scrapers/wikipedia.ts +95 -0
  137. package/src/core/tools/web-scrapers/youtube.test.ts +198 -0
  138. package/src/core/tools/web-scrapers/youtube.ts +371 -0
  139. package/src/core/tools/write.ts +21 -18
  140. package/src/core/voice.ts +3 -2
  141. package/src/lib/worktree/collapse.ts +2 -1
  142. package/src/lib/worktree/git.ts +2 -18
  143. package/src/main.ts +59 -3
  144. package/src/modes/interactive/components/extensions/extension-dashboard.ts +33 -19
  145. package/src/modes/interactive/components/extensions/extension-list.ts +15 -8
  146. package/src/modes/interactive/components/hook-editor.ts +2 -1
  147. package/src/modes/interactive/components/model-selector.ts +19 -4
  148. package/src/modes/interactive/interactive-mode.ts +41 -38
  149. package/src/modes/interactive/theme/theme.ts +58 -58
  150. package/src/modes/rpc/rpc-mode.ts +10 -9
  151. package/src/prompts/review-request.md +27 -0
  152. package/src/prompts/reviewer.md +64 -68
  153. package/src/prompts/tools/output.md +22 -3
  154. package/src/prompts/tools/task.md +32 -33
  155. package/src/utils/clipboard.ts +2 -1
  156. package/src/utils/tools-manager.ts +110 -8
  157. package/examples/extensions/subagent/agents/reviewer.md +0 -35
@@ -110,6 +110,8 @@ describe("createTools", () => {
110
110
  getEditFuzzyMatch: () => true,
111
111
  getGitToolEnabled: () => false,
112
112
  getBashInterceptorEnabled: () => true,
113
+ getBashInterceptorSimpleLsEnabled: () => true,
114
+ getBashInterceptorRules: () => [],
113
115
  },
114
116
  });
115
117
  const tools = await createTools(session);
@@ -128,6 +130,8 @@ describe("createTools", () => {
128
130
  getEditFuzzyMatch: () => true,
129
131
  getGitToolEnabled: () => true,
130
132
  getBashInterceptorEnabled: () => true,
133
+ getBashInterceptorSimpleLsEnabled: () => true,
134
+ getBashInterceptorRules: () => [],
131
135
  },
132
136
  });
133
137
  const tools = await createTools(session);
@@ -183,6 +187,6 @@ describe("createTools", () => {
183
187
  });
184
188
 
185
189
  it("HIDDEN_TOOLS contains review tools", () => {
186
- expect(Object.keys(HIDDEN_TOOLS).sort()).toEqual(["complete", "report_finding", "submit_review"]);
190
+ expect(Object.keys(HIDDEN_TOOLS).sort()).toEqual(["complete", "report_finding"]);
187
191
  });
188
192
  });
@@ -18,13 +18,12 @@ export {
18
18
  type LspServerStatus,
19
19
  type LspToolDetails,
20
20
  type LspWarmupResult,
21
- lspTool,
22
21
  warmupLspServers,
23
22
  } from "./lsp/index";
24
23
  export { createNotebookTool, type NotebookToolDetails } from "./notebook";
25
24
  export { createOutputTool, type OutputToolDetails } from "./output";
26
25
  export { createReadTool, type ReadToolDetails } from "./read";
27
- export { reportFindingTool, submitReviewTool } from "./review";
26
+ export { reportFindingTool, type SubmitReviewDetails } from "./review";
28
27
  export { filterRulebookRules, formatRulesForPrompt, type RulebookToolDetails } from "./rulebook";
29
28
  export { BUNDLED_AGENTS, createTaskTool, taskTool } from "./task/index";
30
29
  export type { TruncationResult } from "./truncate";
@@ -53,6 +52,7 @@ export { createWriteTool, type WriteToolDetails } from "./write";
53
52
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
54
53
  import type { Rule } from "../../capability/rule";
55
54
  import type { EventBus } from "../event-bus";
55
+ import type { BashInterceptorRule } from "../settings-manager";
56
56
  import { createAskTool } from "./ask";
57
57
  import { createBashTool } from "./bash";
58
58
  import { createCompleteTool } from "./complete";
@@ -65,7 +65,7 @@ import { createLspTool } from "./lsp/index";
65
65
  import { createNotebookTool } from "./notebook";
66
66
  import { createOutputTool } from "./output";
67
67
  import { createReadTool } from "./read";
68
- import { reportFindingTool, submitReviewTool } from "./review";
68
+ import { reportFindingTool } from "./review";
69
69
  import { createRulebookTool } from "./rulebook";
70
70
  import { createTaskTool } from "./task/index";
71
71
  import { createWebFetchTool } from "./web-fetch";
@@ -102,6 +102,8 @@ export interface ToolSession {
102
102
  getEditFuzzyMatch(): boolean;
103
103
  getGitToolEnabled(): boolean;
104
104
  getBashInterceptorEnabled(): boolean;
105
+ getBashInterceptorSimpleLsEnabled(): boolean;
106
+ getBashInterceptorRules(): BashInterceptorRule[];
105
107
  };
106
108
  }
107
109
 
@@ -129,7 +131,6 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
129
131
  export const HIDDEN_TOOLS: Record<string, ToolFactory> = {
130
132
  complete: createCompleteTool,
131
133
  report_finding: () => reportFindingTool,
132
- submit_review: () => submitReviewTool,
133
134
  };
134
135
 
135
136
  export type ToolName = keyof typeof BUILTIN_TOOLS;
@@ -147,10 +148,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
147
148
 
148
149
  const entries = requestedTools
149
150
  ? 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
- ];
151
+ : [...Object.entries(BUILTIN_TOOLS), ...(includeComplete ? ([["complete", HIDDEN_TOOLS.complete]] as const) : [])];
154
152
  const results = await Promise.all(entries.map(([, factory]) => factory(session)));
155
153
  const tools = results.filter((t): t is Tool => t !== null);
156
154
 
@@ -9,211 +9,199 @@
9
9
  */
10
10
 
11
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";
12
+ | "boolean"
13
+ | "string"
14
+ | "timestamp"
15
+ | "float32"
16
+ | "float64"
17
+ | "int8"
18
+ | "uint8"
19
+ | "int16"
20
+ | "uint16"
21
+ | "int32"
22
+ | "uint32";
23
23
 
24
24
  interface JTDType {
25
- type: JTDPrimitive;
25
+ type: JTDPrimitive;
26
26
  }
27
27
 
28
28
  interface JTDEnum {
29
- enum: string[];
29
+ enum: string[];
30
30
  }
31
31
 
32
32
  interface JTDElements {
33
- elements: JTDSchema;
33
+ elements: JTDSchema;
34
34
  }
35
35
 
36
36
  interface JTDValues {
37
- values: JTDSchema;
37
+ values: JTDSchema;
38
38
  }
39
39
 
40
40
  interface JTDProperties {
41
- properties?: Record<string, JTDSchema>;
42
- optionalProperties?: Record<string, JTDSchema>;
41
+ properties?: Record<string, JTDSchema>;
42
+ optionalProperties?: Record<string, JTDSchema>;
43
43
  }
44
44
 
45
45
  interface JTDDiscriminator {
46
- discriminator: string;
47
- mapping: Record<string, JTDProperties>;
46
+ discriminator: string;
47
+ mapping: Record<string, JTDProperties>;
48
48
  }
49
49
 
50
50
  interface JTDRef {
51
- ref: string;
51
+ ref: string;
52
52
  }
53
53
 
54
54
  interface JTDEmpty {}
55
55
 
56
- type JTDSchema =
57
- | JTDType
58
- | JTDEnum
59
- | JTDElements
60
- | JTDValues
61
- | JTDProperties
62
- | JTDDiscriminator
63
- | JTDRef
64
- | JTDEmpty;
56
+ type JTDSchema = JTDType | JTDEnum | JTDElements | JTDValues | JTDProperties | JTDDiscriminator | JTDRef | JTDEmpty;
65
57
 
66
58
  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",
59
+ boolean: "boolean",
60
+ string: "string",
61
+ timestamp: "string", // ISO 8601
62
+ float32: "number",
63
+ float64: "number",
64
+ int8: "integer",
65
+ uint8: "integer",
66
+ int16: "integer",
67
+ uint16: "integer",
68
+ int32: "integer",
69
+ uint32: "integer",
78
70
  };
79
71
 
80
72
  function isJTDType(schema: unknown): schema is JTDType {
81
- return typeof schema === "object" && schema !== null && "type" in schema;
73
+ return typeof schema === "object" && schema !== null && "type" in schema;
82
74
  }
83
75
 
84
76
  function isJTDEnum(schema: unknown): schema is JTDEnum {
85
- return typeof schema === "object" && schema !== null && "enum" in schema;
77
+ return typeof schema === "object" && schema !== null && "enum" in schema;
86
78
  }
87
79
 
88
80
  function isJTDElements(schema: unknown): schema is JTDElements {
89
- return typeof schema === "object" && schema !== null && "elements" in schema;
81
+ return typeof schema === "object" && schema !== null && "elements" in schema;
90
82
  }
91
83
 
92
84
  function isJTDValues(schema: unknown): schema is JTDValues {
93
- return typeof schema === "object" && schema !== null && "values" in schema;
85
+ return typeof schema === "object" && schema !== null && "values" in schema;
94
86
  }
95
87
 
96
88
  function isJTDProperties(schema: unknown): schema is JTDProperties {
97
- return (
98
- typeof schema === "object" &&
99
- schema !== null &&
100
- ("properties" in schema || "optionalProperties" in schema)
101
- );
89
+ return typeof schema === "object" && schema !== null && ("properties" in schema || "optionalProperties" in schema);
102
90
  }
103
91
 
104
92
  function isJTDDiscriminator(schema: unknown): schema is JTDDiscriminator {
105
- return typeof schema === "object" && schema !== null && "discriminator" in schema;
93
+ return typeof schema === "object" && schema !== null && "discriminator" in schema;
106
94
  }
107
95
 
108
96
  function isJTDRef(schema: unknown): schema is JTDRef {
109
- return typeof schema === "object" && schema !== null && "ref" in schema;
97
+ return typeof schema === "object" && schema !== null && "ref" in schema;
110
98
  }
111
99
 
112
100
  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 {};
101
+ if (schema === null || typeof schema !== "object") {
102
+ return {};
103
+ }
104
+
105
+ // Type form: { type: "string" } → { type: "string" }
106
+ if (isJTDType(schema)) {
107
+ const jsonType = primitiveMap[schema.type as JTDPrimitive];
108
+ if (!jsonType) {
109
+ return { type: schema.type };
110
+ }
111
+ const result: Record<string, unknown> = { type: jsonType };
112
+ // Add format for timestamp
113
+ if (schema.type === "timestamp") {
114
+ result.format = "date-time";
115
+ }
116
+ return result;
117
+ }
118
+
119
+ // Enum form: { enum: ["a", "b"] } → { enum: ["a", "b"] }
120
+ if (isJTDEnum(schema)) {
121
+ return { enum: schema.enum };
122
+ }
123
+
124
+ // Elements form: { elements: { type: "string" } } → { type: "array", items: ... }
125
+ if (isJTDElements(schema)) {
126
+ return {
127
+ type: "array",
128
+ items: convertSchema(schema.elements),
129
+ };
130
+ }
131
+
132
+ // Values form: { values: { type: "string" } } → { type: "object", additionalProperties: ... }
133
+ if (isJTDValues(schema)) {
134
+ return {
135
+ type: "object",
136
+ additionalProperties: convertSchema(schema.values),
137
+ };
138
+ }
139
+
140
+ // Properties form: { properties: {...}, optionalProperties: {...} }
141
+ if (isJTDProperties(schema)) {
142
+ const properties: Record<string, unknown> = {};
143
+ const required: string[] = [];
144
+
145
+ // Required properties
146
+ if (schema.properties) {
147
+ for (const [key, value] of Object.entries(schema.properties)) {
148
+ properties[key] = convertSchema(value);
149
+ required.push(key);
150
+ }
151
+ }
152
+
153
+ // Optional properties
154
+ if (schema.optionalProperties) {
155
+ for (const [key, value] of Object.entries(schema.optionalProperties)) {
156
+ properties[key] = convertSchema(value);
157
+ }
158
+ }
159
+
160
+ const result: Record<string, unknown> = {
161
+ type: "object",
162
+ properties,
163
+ additionalProperties: false,
164
+ };
165
+
166
+ if (required.length > 0) {
167
+ result.required = required;
168
+ }
169
+
170
+ return result;
171
+ }
172
+
173
+ // Discriminator form: { discriminator: "type", mapping: { ... } }
174
+ if (isJTDDiscriminator(schema)) {
175
+ const oneOf: unknown[] = [];
176
+
177
+ for (const [tag, props] of Object.entries(schema.mapping)) {
178
+ const converted = convertSchema(props) as Record<string, unknown>;
179
+ // Add the discriminator property
180
+ const properties = (converted.properties || {}) as Record<string, unknown>;
181
+ properties[schema.discriminator] = { const: tag };
182
+
183
+ const required = ((converted.required as string[]) || []).slice();
184
+ if (!required.includes(schema.discriminator)) {
185
+ required.push(schema.discriminator);
186
+ }
187
+
188
+ oneOf.push({
189
+ ...converted,
190
+ properties,
191
+ required,
192
+ });
193
+ }
194
+
195
+ return { oneOf };
196
+ }
197
+
198
+ // Ref form: { ref: "MyType" } → { $ref: "#/$defs/MyType" }
199
+ if (isJTDRef(schema)) {
200
+ return { $ref: `#/$defs/${schema.ref}` };
201
+ }
202
+
203
+ // Empty form: {} → {} (accepts anything)
204
+ return {};
217
205
  }
218
206
 
219
207
  /**
@@ -223,43 +211,33 @@ function convertSchema(schema: unknown): unknown {
223
211
  * JSON Schema uses: type: "object", type: "array", items, additionalProperties, etc.
224
212
  */
225
213
  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;
214
+ if (schema === null || typeof schema !== "object") {
215
+ return false;
216
+ }
217
+
218
+ const obj = schema as Record<string, unknown>;
219
+
220
+ // JTD-specific keywords
221
+ if ("elements" in obj) return true;
222
+ if ("values" in obj) return true;
223
+ if ("optionalProperties" in obj) return true;
224
+ if ("discriminator" in obj) return true;
225
+ if ("ref" in obj) return true;
226
+
227
+ // JTD type primitives (JSON Schema doesn't have int32, float64, etc.)
228
+ if ("type" in obj) {
229
+ const jtdPrimitives = ["timestamp", "float32", "float64", "int8", "uint8", "int16", "uint16", "int32", "uint32"];
230
+ if (jtdPrimitives.includes(obj.type as string)) {
231
+ return true;
232
+ }
233
+ }
234
+
235
+ // JTD properties form without type: "object" (JSON Schema requires it)
236
+ if ("properties" in obj && !("type" in obj)) {
237
+ return true;
238
+ }
239
+
240
+ return false;
263
241
  }
264
242
 
265
243
  /**
@@ -267,8 +245,8 @@ export function isJTDSchema(schema: unknown): boolean {
267
245
  * If already JSON Schema, returns as-is.
268
246
  */
269
247
  export function jtdToJsonSchema(schema: unknown): unknown {
270
- if (!isJTDSchema(schema)) {
271
- return schema;
272
- }
273
- return convertSchema(schema);
248
+ if (!isJTDSchema(schema)) {
249
+ return schema;
250
+ }
251
+ return convertSchema(schema);
274
252
  }
@@ -1,4 +1,3 @@
1
- import { existsSync, readdirSync, statSync } from "node:fs";
2
1
  import nodePath from "node:path";
3
2
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
4
3
  import type { Component } from "@oh-my-pi/pi-tui";
@@ -53,23 +52,25 @@ export function createLsTool(session: ToolSession): AgentTool<typeof lsSchema> {
53
52
  const dirPath = resolveToCwd(path || ".", session.cwd);
54
53
  const effectiveLimit = limit ?? DEFAULT_LIMIT;
55
54
 
56
- // Check if path exists
57
- if (!existsSync(dirPath)) {
55
+ // Check if path exists and is a directory
56
+ let dirStat: Awaited<ReturnType<Bun.BunFile["stat"]>>;
57
+ try {
58
+ dirStat = await Bun.file(dirPath).stat();
59
+ } catch {
58
60
  throw new Error(`Path not found: ${dirPath}`);
59
61
  }
60
62
 
61
- // Check if path is a directory
62
- const stat = statSync(dirPath);
63
- if (!stat.isDirectory()) {
63
+ if (!dirStat.isDirectory()) {
64
64
  throw new Error(`Not a directory: ${dirPath}`);
65
65
  }
66
66
 
67
67
  // Read directory entries
68
68
  let entries: string[];
69
69
  try {
70
- entries = readdirSync(dirPath);
71
- } catch (e: any) {
72
- throw new Error(`Cannot read directory: ${e.message}`);
70
+ entries = await Array.fromAsync(new Bun.Glob("*").scan({ cwd: dirPath, dot: true, onlyFiles: false }));
71
+ } catch (error) {
72
+ const message = error instanceof Error ? error.message : String(error);
73
+ throw new Error(`Cannot read directory: ${message}`);
73
74
  }
74
75
 
75
76
  // Sort alphabetically (case-insensitive)
@@ -82,6 +83,7 @@ export function createLsTool(session: ToolSession): AgentTool<typeof lsSchema> {
82
83
  let fileCount = 0;
83
84
 
84
85
  for (const entry of entries) {
86
+ signal?.throwIfAborted();
85
87
  if (results.length >= effectiveLimit) {
86
88
  entryLimitReached = true;
87
89
  break;
@@ -92,7 +94,7 @@ export function createLsTool(session: ToolSession): AgentTool<typeof lsSchema> {
92
94
  let age = "";
93
95
 
94
96
  try {
95
- const entryStat = statSync(fullPath);
97
+ const entryStat = await Bun.file(fullPath).stat();
96
98
  if (entryStat.isDirectory()) {
97
99
  suffix = "/";
98
100
  dirCount += 1;