@oh-my-pi/pi-coding-agent 3.25.0 → 3.30.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 (85) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/package.json +4 -4
  3. package/src/core/tools/complete.ts +2 -4
  4. package/src/core/tools/jtd-to-json-schema.ts +174 -196
  5. package/src/core/tools/read.ts +4 -4
  6. package/src/core/tools/task/executor.ts +146 -20
  7. package/src/core/tools/task/name-generator.ts +1544 -214
  8. package/src/core/tools/task/types.ts +19 -5
  9. package/src/core/tools/task/worker.ts +103 -13
  10. package/src/core/tools/web-fetch-handlers/academic.test.ts +239 -0
  11. package/src/core/tools/web-fetch-handlers/artifacthub.ts +210 -0
  12. package/src/core/tools/web-fetch-handlers/arxiv.ts +84 -0
  13. package/src/core/tools/web-fetch-handlers/aur.ts +171 -0
  14. package/src/core/tools/web-fetch-handlers/biorxiv.ts +136 -0
  15. package/src/core/tools/web-fetch-handlers/bluesky.ts +277 -0
  16. package/src/core/tools/web-fetch-handlers/brew.ts +173 -0
  17. package/src/core/tools/web-fetch-handlers/business.test.ts +82 -0
  18. package/src/core/tools/web-fetch-handlers/cheatsh.ts +73 -0
  19. package/src/core/tools/web-fetch-handlers/chocolatey.ts +153 -0
  20. package/src/core/tools/web-fetch-handlers/coingecko.ts +179 -0
  21. package/src/core/tools/web-fetch-handlers/crates-io.ts +123 -0
  22. package/src/core/tools/web-fetch-handlers/dev-platforms.test.ts +254 -0
  23. package/src/core/tools/web-fetch-handlers/devto.ts +173 -0
  24. package/src/core/tools/web-fetch-handlers/discogs.ts +303 -0
  25. package/src/core/tools/web-fetch-handlers/dockerhub.ts +156 -0
  26. package/src/core/tools/web-fetch-handlers/documentation.test.ts +85 -0
  27. package/src/core/tools/web-fetch-handlers/finance-media.test.ts +144 -0
  28. package/src/core/tools/web-fetch-handlers/git-hosting.test.ts +272 -0
  29. package/src/core/tools/web-fetch-handlers/github-gist.ts +64 -0
  30. package/src/core/tools/web-fetch-handlers/github.ts +424 -0
  31. package/src/core/tools/web-fetch-handlers/gitlab.ts +444 -0
  32. package/src/core/tools/web-fetch-handlers/go-pkg.ts +271 -0
  33. package/src/core/tools/web-fetch-handlers/hackage.ts +89 -0
  34. package/src/core/tools/web-fetch-handlers/hackernews.ts +208 -0
  35. package/src/core/tools/web-fetch-handlers/hex.ts +121 -0
  36. package/src/core/tools/web-fetch-handlers/huggingface.ts +385 -0
  37. package/src/core/tools/web-fetch-handlers/iacr.ts +82 -0
  38. package/src/core/tools/web-fetch-handlers/index.ts +69 -0
  39. package/src/core/tools/web-fetch-handlers/lobsters.ts +186 -0
  40. package/src/core/tools/web-fetch-handlers/mastodon.ts +302 -0
  41. package/src/core/tools/web-fetch-handlers/maven.ts +147 -0
  42. package/src/core/tools/web-fetch-handlers/mdn.ts +174 -0
  43. package/src/core/tools/web-fetch-handlers/media.test.ts +138 -0
  44. package/src/core/tools/web-fetch-handlers/metacpan.ts +247 -0
  45. package/src/core/tools/web-fetch-handlers/npm.ts +107 -0
  46. package/src/core/tools/web-fetch-handlers/nuget.ts +201 -0
  47. package/src/core/tools/web-fetch-handlers/nvd.ts +238 -0
  48. package/src/core/tools/web-fetch-handlers/opencorporates.ts +273 -0
  49. package/src/core/tools/web-fetch-handlers/openlibrary.ts +313 -0
  50. package/src/core/tools/web-fetch-handlers/osv.ts +184 -0
  51. package/src/core/tools/web-fetch-handlers/package-managers-2.test.ts +199 -0
  52. package/src/core/tools/web-fetch-handlers/package-managers.test.ts +171 -0
  53. package/src/core/tools/web-fetch-handlers/package-registries.test.ts +259 -0
  54. package/src/core/tools/web-fetch-handlers/packagist.ts +170 -0
  55. package/src/core/tools/web-fetch-handlers/pub-dev.ts +185 -0
  56. package/src/core/tools/web-fetch-handlers/pubmed.ts +174 -0
  57. package/src/core/tools/web-fetch-handlers/pypi.ts +125 -0
  58. package/src/core/tools/web-fetch-handlers/readthedocs.ts +122 -0
  59. package/src/core/tools/web-fetch-handlers/reddit.ts +100 -0
  60. package/src/core/tools/web-fetch-handlers/repology.ts +257 -0
  61. package/src/core/tools/web-fetch-handlers/research.test.ts +107 -0
  62. package/src/core/tools/web-fetch-handlers/rfc.ts +205 -0
  63. package/src/core/tools/web-fetch-handlers/rubygems.ts +112 -0
  64. package/src/core/tools/web-fetch-handlers/sec-edgar.ts +269 -0
  65. package/src/core/tools/web-fetch-handlers/security.test.ts +103 -0
  66. package/src/core/tools/web-fetch-handlers/semantic-scholar.ts +190 -0
  67. package/src/core/tools/web-fetch-handlers/social-extended.test.ts +192 -0
  68. package/src/core/tools/web-fetch-handlers/social.test.ts +259 -0
  69. package/src/core/tools/web-fetch-handlers/spotify.ts +218 -0
  70. package/src/core/tools/web-fetch-handlers/stackexchange.test.ts +120 -0
  71. package/src/core/tools/web-fetch-handlers/stackoverflow.ts +123 -0
  72. package/src/core/tools/web-fetch-handlers/standards.test.ts +122 -0
  73. package/src/core/tools/web-fetch-handlers/terraform.ts +296 -0
  74. package/src/core/tools/web-fetch-handlers/tldr.ts +47 -0
  75. package/src/core/tools/web-fetch-handlers/twitter.ts +84 -0
  76. package/src/core/tools/web-fetch-handlers/types.ts +163 -0
  77. package/src/core/tools/web-fetch-handlers/utils.ts +91 -0
  78. package/src/core/tools/web-fetch-handlers/vimeo.ts +152 -0
  79. package/src/core/tools/web-fetch-handlers/wikidata.ts +349 -0
  80. package/src/core/tools/web-fetch-handlers/wikipedia.test.ts +73 -0
  81. package/src/core/tools/web-fetch-handlers/wikipedia.ts +91 -0
  82. package/src/core/tools/web-fetch-handlers/youtube.test.ts +198 -0
  83. package/src/core/tools/web-fetch-handlers/youtube.ts +319 -0
  84. package/src/core/tools/web-fetch.ts +152 -1324
  85. package/src/utils/tools-manager.ts +110 -8
package/CHANGELOG.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [3.30.0] - 2026-01-07
6
+ ### Added
7
+
8
+ - Added environment variable configuration for task limits: `OMP_TASK_MAX_PARALLEL`, `OMP_TASK_MAX_CONCURRENCY`, `OMP_TASK_MAX_OUTPUT_BYTES`, `OMP_TASK_MAX_OUTPUT_LINES`, and `OMP_TASK_MAX_AGENTS_IN_DESCRIPTION`
9
+ - Added specialized web-fetch handlers for 50+ platforms including GitHub, GitLab, npm, PyPI, crates.io, Stack Overflow, Wikipedia, arXiv, PubMed, Hacker News, Reddit, Mastodon, Bluesky, and many more
10
+ - Added automatic yt-dlp installation for YouTube transcript extraction
11
+ - Added YouTube video support with automatic transcript extraction via yt-dlp
12
+
13
+ ### Changed
14
+
15
+ - Changed task executor to gracefully handle worker termination with proper cleanup and timeout handling
16
+
17
+ ### Fixed
18
+
19
+ - Fixed Lobsters front page handler to use correct API endpoint (`/hottest.json` instead of invalid `.json`)
20
+ - Fixed task worker error handling to prevent hanging on worker crashes, uncaught errors, and unhandled rejections
21
+ - Fixed double-stringified JSON output from subagents being returned as escaped strings instead of parsed objects
22
+ - Fixed markitdown tool installation to use automatic tool installer instead of requiring manual installation
23
+
5
24
  ## [3.25.0] - 2026-01-07
6
25
  ### Added
7
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "3.25.0",
3
+ "version": "3.30.0",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "ompConfig": {
@@ -40,9 +40,9 @@
40
40
  },
41
41
  "dependencies": {
42
42
  "@mariozechner/pi-ai": "^0.37.4",
43
- "@oh-my-pi/pi-agent-core": "3.25.0",
44
- "@oh-my-pi/pi-git-tool": "3.25.0",
45
- "@oh-my-pi/pi-tui": "3.25.0",
43
+ "@oh-my-pi/pi-agent-core": "3.30.0",
44
+ "@oh-my-pi/pi-git-tool": "3.30.0",
45
+ "@oh-my-pi/pi-tui": "3.30.0",
46
46
  "@openai/agents": "^0.3.7",
47
47
  "@sinclair/typebox": "^0.34.46",
48
48
  "ajv": "^8.17.1",
@@ -74,7 +74,7 @@ export function createCompleteTool(session: ToolSession) {
74
74
  const dataSchema = normalizedSchema
75
75
  ? Type.Unsafe({
76
76
  ...(normalizedSchema as object),
77
- description: "Structured output matching the schema:\n" + schemaHint,
77
+ description: `Structured output matching the schema:\n${schemaHint}`,
78
78
  })
79
79
  : Type.Any({ description: "Structured JSON output (no schema specified)" });
80
80
 
@@ -110,9 +110,7 @@ export function createCompleteTool(session: ToolSession) {
110
110
  }
111
111
 
112
112
  const responseText =
113
- status === "aborted"
114
- ? `Task aborted: ${params.error || "No reason provided"}`
115
- : "Completion recorded.";
113
+ status === "aborted" ? `Task aborted: ${params.error || "No reason provided"}` : "Completion recorded.";
116
114
 
117
115
  return {
118
116
  content: [{ type: "text", text: responseText }],
@@ -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
  }
@@ -311,10 +311,10 @@ async function findReadPathSuggestions(
311
311
  return { suggestions, scopeLabel, truncated };
312
312
  }
313
313
 
314
- function convertWithMarkitdown(filePath: string): { content: string; ok: boolean; error?: string } {
315
- const cmd = Bun.which("markitdown");
314
+ async function convertWithMarkitdown(filePath: string): Promise<{ content: string; ok: boolean; error?: string }> {
315
+ const cmd = await ensureTool("markitdown", true);
316
316
  if (!cmd) {
317
- return { content: "", ok: false, error: "markitdown not found" };
317
+ return { content: "", ok: false, error: "markitdown not found (uv/pip unavailable)" };
318
318
  }
319
319
 
320
320
  const result = Bun.spawnSync([cmd, filePath], {
@@ -449,7 +449,7 @@ export function createReadTool(session: ToolSession): AgentTool<typeof readSchem
449
449
  }
450
450
  } else if (CONVERTIBLE_EXTENSIONS.has(ext)) {
451
451
  // Convert document via markitdown
452
- const result = convertWithMarkitdown(absolutePath);
452
+ const result = await convertWithMarkitdown(absolutePath);
453
453
  if (result.ok) {
454
454
  // Apply truncation to converted content
455
455
  const truncation = truncateHead(result.content);