@mseep/clawdcursor 1.5.5

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 (354) hide show
  1. package/CHANGELOG.md +2264 -0
  2. package/LICENSE +21 -0
  3. package/README.md +385 -0
  4. package/SECURITY.md +44 -0
  5. package/SKILL.md +503 -0
  6. package/dist/core/agent-loop/agent.d.ts +42 -0
  7. package/dist/core/agent-loop/agent.js +1023 -0
  8. package/dist/core/agent-loop/agent.js.map +1 -0
  9. package/dist/core/agent-loop/batch-tool.d.ts +25 -0
  10. package/dist/core/agent-loop/batch-tool.js +218 -0
  11. package/dist/core/agent-loop/batch-tool.js.map +1 -0
  12. package/dist/core/agent-loop/coord-scale.d.ts +72 -0
  13. package/dist/core/agent-loop/coord-scale.js +89 -0
  14. package/dist/core/agent-loop/coord-scale.js.map +1 -0
  15. package/dist/core/agent-loop/focus-guard.d.ts +24 -0
  16. package/dist/core/agent-loop/focus-guard.js +29 -0
  17. package/dist/core/agent-loop/focus-guard.js.map +1 -0
  18. package/dist/core/agent-loop/project-mcp.d.ts +97 -0
  19. package/dist/core/agent-loop/project-mcp.js +253 -0
  20. package/dist/core/agent-loop/project-mcp.js.map +1 -0
  21. package/dist/core/agent-loop/prompt.d.ts +45 -0
  22. package/dist/core/agent-loop/prompt.js +426 -0
  23. package/dist/core/agent-loop/prompt.js.map +1 -0
  24. package/dist/core/agent-loop/tool-meta.d.ts +93 -0
  25. package/dist/core/agent-loop/tool-meta.js +651 -0
  26. package/dist/core/agent-loop/tool-meta.js.map +1 -0
  27. package/dist/core/agent-loop/tools.d.ts +38 -0
  28. package/dist/core/agent-loop/tools.js +2134 -0
  29. package/dist/core/agent-loop/tools.js.map +1 -0
  30. package/dist/core/agent-loop/types.d.ts +170 -0
  31. package/dist/core/agent-loop/types.js +12 -0
  32. package/dist/core/agent-loop/types.js.map +1 -0
  33. package/dist/core/agent.d.ts +51 -0
  34. package/dist/core/agent.js +245 -0
  35. package/dist/core/agent.js.map +1 -0
  36. package/dist/core/app-categories.d.ts +67 -0
  37. package/dist/core/app-categories.js +108 -0
  38. package/dist/core/app-categories.js.map +1 -0
  39. package/dist/core/banner.d.ts +70 -0
  40. package/dist/core/banner.js +245 -0
  41. package/dist/core/banner.js.map +1 -0
  42. package/dist/core/classify/capability.d.ts +45 -0
  43. package/dist/core/classify/capability.js +78 -0
  44. package/dist/core/classify/capability.js.map +1 -0
  45. package/dist/core/decompose/llm-decomposer.d.ts +35 -0
  46. package/dist/core/decompose/llm-decomposer.js +156 -0
  47. package/dist/core/decompose/llm-decomposer.js.map +1 -0
  48. package/dist/core/decompose/parser.d.ts +27 -0
  49. package/dist/core/decompose/parser.js +101 -0
  50. package/dist/core/decompose/parser.js.map +1 -0
  51. package/dist/core/observability/correlation.d.ts +19 -0
  52. package/dist/core/observability/correlation.js +36 -0
  53. package/dist/core/observability/correlation.js.map +1 -0
  54. package/dist/core/observability/cost-meter.d.ts +51 -0
  55. package/dist/core/observability/cost-meter.js +134 -0
  56. package/dist/core/observability/cost-meter.js.map +1 -0
  57. package/dist/core/observability/logger.d.ts +61 -0
  58. package/dist/core/observability/logger.js +550 -0
  59. package/dist/core/observability/logger.js.map +1 -0
  60. package/dist/core/router/aliases.d.ts +50 -0
  61. package/dist/core/router/aliases.js +104 -0
  62. package/dist/core/router/aliases.js.map +1 -0
  63. package/dist/core/router/normalize.d.ts +41 -0
  64. package/dist/core/router/normalize.js +80 -0
  65. package/dist/core/router/normalize.js.map +1 -0
  66. package/dist/core/safety.d.ts +126 -0
  67. package/dist/core/safety.js +568 -0
  68. package/dist/core/safety.js.map +1 -0
  69. package/dist/core/sense/a11y-resolver.d.ts +73 -0
  70. package/dist/core/sense/a11y-resolver.js +76 -0
  71. package/dist/core/sense/a11y-resolver.js.map +1 -0
  72. package/dist/core/sense/fingerprint.d.ts +41 -0
  73. package/dist/core/sense/fingerprint.js +123 -0
  74. package/dist/core/sense/fingerprint.js.map +1 -0
  75. package/dist/core/sense/rank.d.ts +70 -0
  76. package/dist/core/sense/rank.js +192 -0
  77. package/dist/core/sense/rank.js.map +1 -0
  78. package/dist/core/sense/reactive-check.d.ts +40 -0
  79. package/dist/core/sense/reactive-check.js +48 -0
  80. package/dist/core/sense/reactive-check.js.map +1 -0
  81. package/dist/core/sense/snapshot.d.ts +19 -0
  82. package/dist/core/sense/snapshot.js +100 -0
  83. package/dist/core/sense/snapshot.js.map +1 -0
  84. package/dist/core/sense/types.d.ts +66 -0
  85. package/dist/core/sense/types.js +9 -0
  86. package/dist/core/sense/types.js.map +1 -0
  87. package/dist/core/sense/ui-map-anchors.d.ts +7 -0
  88. package/dist/core/sense/ui-map-anchors.js +24 -0
  89. package/dist/core/sense/ui-map-anchors.js.map +1 -0
  90. package/dist/core/sense/ui-map-elements.d.ts +5 -0
  91. package/dist/core/sense/ui-map-elements.js +33 -0
  92. package/dist/core/sense/ui-map-elements.js.map +1 -0
  93. package/dist/core/sense/ui-map-find.d.ts +56 -0
  94. package/dist/core/sense/ui-map-find.js +153 -0
  95. package/dist/core/sense/ui-map-find.js.map +1 -0
  96. package/dist/core/sense/ui-map-fuse.d.ts +4 -0
  97. package/dist/core/sense/ui-map-fuse.js +44 -0
  98. package/dist/core/sense/ui-map-fuse.js.map +1 -0
  99. package/dist/core/sense/ui-map-geom.d.ts +3 -0
  100. package/dist/core/sense/ui-map-geom.js +16 -0
  101. package/dist/core/sense/ui-map-geom.js.map +1 -0
  102. package/dist/core/sense/ui-map-holder.d.ts +58 -0
  103. package/dist/core/sense/ui-map-holder.js +87 -0
  104. package/dist/core/sense/ui-map-holder.js.map +1 -0
  105. package/dist/core/sense/ui-map-normalize.d.ts +19 -0
  106. package/dist/core/sense/ui-map-normalize.js +65 -0
  107. package/dist/core/sense/ui-map-normalize.js.map +1 -0
  108. package/dist/core/sense/ui-map-render.d.ts +4 -0
  109. package/dist/core/sense/ui-map-render.js +34 -0
  110. package/dist/core/sense/ui-map-render.js.map +1 -0
  111. package/dist/core/sense/ui-map-resolve.d.ts +41 -0
  112. package/dist/core/sense/ui-map-resolve.js +59 -0
  113. package/dist/core/sense/ui-map-resolve.js.map +1 -0
  114. package/dist/core/sense/ui-map-types.d.ts +66 -0
  115. package/dist/core/sense/ui-map-types.js +11 -0
  116. package/dist/core/sense/ui-map-types.js.map +1 -0
  117. package/dist/core/sense/ui-map.d.ts +29 -0
  118. package/dist/core/sense/ui-map.js +113 -0
  119. package/dist/core/sense/ui-map.js.map +1 -0
  120. package/dist/core/verify/assertions.d.ts +132 -0
  121. package/dist/core/verify/assertions.js +284 -0
  122. package/dist/core/verify/assertions.js.map +1 -0
  123. package/dist/index.d.ts +21 -0
  124. package/dist/index.js +24 -0
  125. package/dist/index.js.map +1 -0
  126. package/dist/llm/browser-config.d.ts +36 -0
  127. package/dist/llm/browser-config.js +83 -0
  128. package/dist/llm/browser-config.js.map +1 -0
  129. package/dist/llm/client.d.ts +268 -0
  130. package/dist/llm/client.js +1094 -0
  131. package/dist/llm/client.js.map +1 -0
  132. package/dist/llm/config.d.ts +79 -0
  133. package/dist/llm/config.js +375 -0
  134. package/dist/llm/config.js.map +1 -0
  135. package/dist/llm/credentials.d.ts +35 -0
  136. package/dist/llm/credentials.js +491 -0
  137. package/dist/llm/credentials.js.map +1 -0
  138. package/dist/llm/external-creds.d.ts +42 -0
  139. package/dist/llm/external-creds.js +169 -0
  140. package/dist/llm/external-creds.js.map +1 -0
  141. package/dist/llm/providers.d.ts +123 -0
  142. package/dist/llm/providers.js +717 -0
  143. package/dist/llm/providers.js.map +1 -0
  144. package/dist/paths.d.ts +31 -0
  145. package/dist/paths.js +147 -0
  146. package/dist/paths.js.map +1 -0
  147. package/dist/platform/accessibility.d.ts +139 -0
  148. package/dist/platform/accessibility.js +670 -0
  149. package/dist/platform/accessibility.js.map +1 -0
  150. package/dist/platform/cdp-driver.d.ts +318 -0
  151. package/dist/platform/cdp-driver.js +1179 -0
  152. package/dist/platform/cdp-driver.js.map +1 -0
  153. package/dist/platform/index.d.ts +11 -0
  154. package/dist/platform/index.js +69 -0
  155. package/dist/platform/index.js.map +1 -0
  156. package/dist/platform/keys.d.ts +17 -0
  157. package/dist/platform/keys.js +129 -0
  158. package/dist/platform/keys.js.map +1 -0
  159. package/dist/platform/launch-poll.d.ts +101 -0
  160. package/dist/platform/launch-poll.js +177 -0
  161. package/dist/platform/launch-poll.js.map +1 -0
  162. package/dist/platform/linux.d.ts +173 -0
  163. package/dist/platform/linux.js +1253 -0
  164. package/dist/platform/linux.js.map +1 -0
  165. package/dist/platform/macos.d.ts +136 -0
  166. package/dist/platform/macos.js +976 -0
  167. package/dist/platform/macos.js.map +1 -0
  168. package/dist/platform/native-desktop.d.ts +145 -0
  169. package/dist/platform/native-desktop.js +936 -0
  170. package/dist/platform/native-desktop.js.map +1 -0
  171. package/dist/platform/native-helper.d.ts +130 -0
  172. package/dist/platform/native-helper.js +592 -0
  173. package/dist/platform/native-helper.js.map +1 -0
  174. package/dist/platform/ocr-engine.d.ts +78 -0
  175. package/dist/platform/ocr-engine.js +363 -0
  176. package/dist/platform/ocr-engine.js.map +1 -0
  177. package/dist/platform/ps-runner.d.ts +28 -0
  178. package/dist/platform/ps-runner.js +228 -0
  179. package/dist/platform/ps-runner.js.map +1 -0
  180. package/dist/platform/types.d.ts +397 -0
  181. package/dist/platform/types.js +15 -0
  182. package/dist/platform/types.js.map +1 -0
  183. package/dist/platform/uri-handler.d.ts +75 -0
  184. package/dist/platform/uri-handler.js +273 -0
  185. package/dist/platform/uri-handler.js.map +1 -0
  186. package/dist/platform/wayland-backend.d.ts +53 -0
  187. package/dist/platform/wayland-backend.js +348 -0
  188. package/dist/platform/wayland-backend.js.map +1 -0
  189. package/dist/platform/windows.d.ts +232 -0
  190. package/dist/platform/windows.js +1210 -0
  191. package/dist/platform/windows.js.map +1 -0
  192. package/dist/postbuild.d.ts +10 -0
  193. package/dist/postbuild.js +98 -0
  194. package/dist/postbuild.js.map +1 -0
  195. package/dist/schema/snapshot.d.ts +33 -0
  196. package/dist/schema/snapshot.js +90 -0
  197. package/dist/schema/snapshot.js.map +1 -0
  198. package/dist/shortcuts.d.ts +30 -0
  199. package/dist/shortcuts.js +261 -0
  200. package/dist/shortcuts.js.map +1 -0
  201. package/dist/surface/cli.d.ts +7 -0
  202. package/dist/surface/cli.js +1556 -0
  203. package/dist/surface/cli.js.map +1 -0
  204. package/dist/surface/dashboard.d.ts +8 -0
  205. package/dist/surface/dashboard.js +1193 -0
  206. package/dist/surface/dashboard.js.map +1 -0
  207. package/dist/surface/doctor.d.ts +29 -0
  208. package/dist/surface/doctor.js +1514 -0
  209. package/dist/surface/doctor.js.map +1 -0
  210. package/dist/surface/format.d.ts +10 -0
  211. package/dist/surface/format.js +37 -0
  212. package/dist/surface/format.js.map +1 -0
  213. package/dist/surface/http-utility.d.ts +65 -0
  214. package/dist/surface/http-utility.js +336 -0
  215. package/dist/surface/http-utility.js.map +1 -0
  216. package/dist/surface/mcp-server.d.ts +91 -0
  217. package/dist/surface/mcp-server.js +280 -0
  218. package/dist/surface/mcp-server.js.map +1 -0
  219. package/dist/surface/onboarding.d.ts +15 -0
  220. package/dist/surface/onboarding.js +184 -0
  221. package/dist/surface/onboarding.js.map +1 -0
  222. package/dist/surface/pidfile.d.ts +79 -0
  223. package/dist/surface/pidfile.js +263 -0
  224. package/dist/surface/pidfile.js.map +1 -0
  225. package/dist/surface/readiness.d.ts +45 -0
  226. package/dist/surface/readiness.js +230 -0
  227. package/dist/surface/readiness.js.map +1 -0
  228. package/dist/surface/report.d.ts +68 -0
  229. package/dist/surface/report.js +341 -0
  230. package/dist/surface/report.js.map +1 -0
  231. package/dist/surface/skill-register.d.ts +14 -0
  232. package/dist/surface/skill-register.js +150 -0
  233. package/dist/surface/skill-register.js.map +1 -0
  234. package/dist/surface/version.d.ts +6 -0
  235. package/dist/surface/version.js +27 -0
  236. package/dist/surface/version.js.map +1 -0
  237. package/dist/tools/a11y.d.ts +8 -0
  238. package/dist/tools/a11y.js +545 -0
  239. package/dist/tools/a11y.js.map +1 -0
  240. package/dist/tools/a11y_depth.d.ts +19 -0
  241. package/dist/tools/a11y_depth.js +455 -0
  242. package/dist/tools/a11y_depth.js.map +1 -0
  243. package/dist/tools/agent.d.ts +15 -0
  244. package/dist/tools/agent.js +248 -0
  245. package/dist/tools/agent.js.map +1 -0
  246. package/dist/tools/batch.d.ts +46 -0
  247. package/dist/tools/batch.js +230 -0
  248. package/dist/tools/batch.js.map +1 -0
  249. package/dist/tools/cdp.d.ts +8 -0
  250. package/dist/tools/cdp.js +233 -0
  251. package/dist/tools/cdp.js.map +1 -0
  252. package/dist/tools/compact.d.ts +63 -0
  253. package/dist/tools/compact.js +418 -0
  254. package/dist/tools/compact.js.map +1 -0
  255. package/dist/tools/cost-class.d.ts +38 -0
  256. package/dist/tools/cost-class.js +117 -0
  257. package/dist/tools/cost-class.js.map +1 -0
  258. package/dist/tools/desktop.d.ts +9 -0
  259. package/dist/tools/desktop.js +346 -0
  260. package/dist/tools/desktop.js.map +1 -0
  261. package/dist/tools/electron_bridge.d.ts +41 -0
  262. package/dist/tools/electron_bridge.js +261 -0
  263. package/dist/tools/electron_bridge.js.map +1 -0
  264. package/dist/tools/extras.d.ts +22 -0
  265. package/dist/tools/extras.js +942 -0
  266. package/dist/tools/extras.js.map +1 -0
  267. package/dist/tools/favorites.d.ts +13 -0
  268. package/dist/tools/favorites.js +137 -0
  269. package/dist/tools/favorites.js.map +1 -0
  270. package/dist/tools/introspection.d.ts +13 -0
  271. package/dist/tools/introspection.js +55 -0
  272. package/dist/tools/introspection.js.map +1 -0
  273. package/dist/tools/ocr.d.ts +8 -0
  274. package/dist/tools/ocr.js +66 -0
  275. package/dist/tools/ocr.js.map +1 -0
  276. package/dist/tools/orchestration.d.ts +7 -0
  277. package/dist/tools/orchestration.js +377 -0
  278. package/dist/tools/orchestration.js.map +1 -0
  279. package/dist/tools/playbooks/extract-compose.d.ts +22 -0
  280. package/dist/tools/playbooks/extract-compose.js +85 -0
  281. package/dist/tools/playbooks/extract-compose.js.map +1 -0
  282. package/dist/tools/playbooks/find-replace.d.ts +11 -0
  283. package/dist/tools/playbooks/find-replace.js +56 -0
  284. package/dist/tools/playbooks/find-replace.js.map +1 -0
  285. package/dist/tools/playbooks/index.d.ts +63 -0
  286. package/dist/tools/playbooks/index.js +70 -0
  287. package/dist/tools/playbooks/index.js.map +1 -0
  288. package/dist/tools/playbooks/keys-blocklist.d.ts +24 -0
  289. package/dist/tools/playbooks/keys-blocklist.js +89 -0
  290. package/dist/tools/playbooks/keys-blocklist.js.map +1 -0
  291. package/dist/tools/registry.d.ts +40 -0
  292. package/dist/tools/registry.js +560 -0
  293. package/dist/tools/registry.js.map +1 -0
  294. package/dist/tools/safety-gate.d.ts +16 -0
  295. package/dist/tools/safety-gate.js +70 -0
  296. package/dist/tools/safety-gate.js.map +1 -0
  297. package/dist/tools/scheduler.d.ts +76 -0
  298. package/dist/tools/scheduler.js +413 -0
  299. package/dist/tools/scheduler.js.map +1 -0
  300. package/dist/tools/shortcuts.d.ts +13 -0
  301. package/dist/tools/shortcuts.js +205 -0
  302. package/dist/tools/shortcuts.js.map +1 -0
  303. package/dist/tools/smart.d.ts +15 -0
  304. package/dist/tools/smart.js +785 -0
  305. package/dist/tools/smart.js.map +1 -0
  306. package/dist/tools/types.d.ts +174 -0
  307. package/dist/tools/types.js +67 -0
  308. package/dist/tools/types.js.map +1 -0
  309. package/dist/tools/window-text.d.ts +15 -0
  310. package/dist/tools/window-text.js +39 -0
  311. package/dist/tools/window-text.js.map +1 -0
  312. package/dist/types.d.ts +122 -0
  313. package/dist/types.js +41 -0
  314. package/dist/types.js.map +1 -0
  315. package/native/Package.swift +38 -0
  316. package/native/README.md +113 -0
  317. package/native/Sources/ClawdCursorHelper/main.swift +602 -0
  318. package/native/Sources/ClawdCursorHost/main.swift +182 -0
  319. package/native/Sources/PermissionCheck/main.swift +53 -0
  320. package/native/Sources/ScreenshotHelper/main.swift +219 -0
  321. package/native/build.sh +139 -0
  322. package/native/entitlements.plist +12 -0
  323. package/package.json +115 -0
  324. package/scripts/banner.ps1 +112 -0
  325. package/scripts/coord-accuracy.ps1 +140 -0
  326. package/scripts/coord-uwp.ps1 +80 -0
  327. package/scripts/edge-glow.ps1 +180 -0
  328. package/scripts/find-element.ps1 +198 -0
  329. package/scripts/get-foreground-window.ps1 +71 -0
  330. package/scripts/get-screen-context.ps1 +183 -0
  331. package/scripts/get-windows.ps1 +66 -0
  332. package/scripts/install-panic-hotkey.ps1 +46 -0
  333. package/scripts/interact-element.ps1 +431 -0
  334. package/scripts/invoke-element.ps1 +314 -0
  335. package/scripts/linux/atspi-bridge.py +356 -0
  336. package/scripts/linux/ocr-recognize.py +154 -0
  337. package/scripts/mac/_window-picker.jxa +163 -0
  338. package/scripts/mac/find-element.jxa +0 -0
  339. package/scripts/mac/find-element.sh +161 -0
  340. package/scripts/mac/focus-window.jxa +284 -0
  341. package/scripts/mac/get-focused-element.jxa +102 -0
  342. package/scripts/mac/get-foreground-window.jxa +173 -0
  343. package/scripts/mac/get-screen-context.jxa +197 -0
  344. package/scripts/mac/get-ui-tree.sh +141 -0
  345. package/scripts/mac/get-windows.jxa +117 -0
  346. package/scripts/mac/interact-element.sh +235 -0
  347. package/scripts/mac/invoke-element.jxa +408 -0
  348. package/scripts/mac/ocr-recognize.swift +124 -0
  349. package/scripts/ocr-recognize.ps1 +102 -0
  350. package/scripts/postinstall-native.js +48 -0
  351. package/scripts/ps-bridge.ps1 +830 -0
  352. package/scripts/smoke-mcp.ps1 +119 -0
  353. package/scripts/sync-version.ts +178 -0
  354. package/scripts/verify-install.js +81 -0
@@ -0,0 +1,1094 @@
1
+ "use strict";
2
+ /**
3
+ * Shared LLM calling module — text AND vision.
4
+ *
5
+ * Text entry points:
6
+ * callTextLLM() — accepts PipelineConfig (used by pipeline reasoners)
7
+ * callTextLLMDirect() — accepts explicit provider params (used by pipeline preprocessor)
8
+ *
9
+ * Vision entry points:
10
+ * callVisionLLM() — accepts PipelineConfig
11
+ * callVisionLLMDirect() — accepts explicit provider params
12
+ *
13
+ * Image normalization: callers pass images in ANY format (OpenAI or Anthropic),
14
+ * and the client auto-converts to the correct format for the target provider.
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.LLMServerError = exports.LLMModelNotFoundError = exports.LLMRateLimitError = exports.LLMBillingError = exports.LLMAuthError = exports.LLMError = void 0;
18
+ exports.applyModelQuirks = applyModelQuirks;
19
+ exports.callTextLLM = callTextLLM;
20
+ exports.callTextLLMDirect = callTextLLMDirect;
21
+ exports.normalizeImageBlock = normalizeImageBlock;
22
+ exports.callVisionLLM = callVisionLLM;
23
+ exports.callVisionLLMDirect = callVisionLLMDirect;
24
+ exports.callLLMWithTools = callLLMWithTools;
25
+ exports.tryParseProseToolCall = tryParseProseToolCall;
26
+ const providers_1 = require("./providers");
27
+ const credentials_1 = require("./credentials");
28
+ // ═══════════════════════════════════════════════════════════════════════════════
29
+ // LLM Error Classes
30
+ // ═══════════════════════════════════════════════════════════════════════════════
31
+ class LLMError extends Error {
32
+ constructor(msg) { super(msg); this.name = 'LLMError'; }
33
+ }
34
+ exports.LLMError = LLMError;
35
+ class LLMAuthError extends LLMError {
36
+ constructor(msg) { super(msg); this.name = 'LLMAuthError'; }
37
+ }
38
+ exports.LLMAuthError = LLMAuthError;
39
+ class LLMBillingError extends LLMError {
40
+ constructor(msg) { super(msg); this.name = 'LLMBillingError'; }
41
+ }
42
+ exports.LLMBillingError = LLMBillingError;
43
+ class LLMRateLimitError extends LLMError {
44
+ constructor(msg) { super(msg); this.name = 'LLMRateLimitError'; }
45
+ }
46
+ exports.LLMRateLimitError = LLMRateLimitError;
47
+ class LLMModelNotFoundError extends LLMError {
48
+ constructor(msg) { super(msg); this.name = 'LLMModelNotFoundError'; }
49
+ }
50
+ exports.LLMModelNotFoundError = LLMModelNotFoundError;
51
+ class LLMServerError extends LLMError {
52
+ constructor(msg) { super(msg); this.name = 'LLMServerError'; }
53
+ }
54
+ exports.LLMServerError = LLMServerError;
55
+ /** Check HTTP status and throw typed LLM errors. */
56
+ function throwOnHttpError(status, model, errBody) {
57
+ if (status === 401)
58
+ throw new LLMAuthError(`Authentication failed (401). Check your API key for ${model}.`);
59
+ if (status === 402)
60
+ throw new LLMBillingError(`Payment required (402). Your API credits may be exhausted for ${model}.`);
61
+ if (status === 429)
62
+ throw new LLMRateLimitError(`Rate limited (429). Try again shortly or switch providers.`);
63
+ if (status === 404)
64
+ throw new LLMModelNotFoundError(`Model not found (404). Check model name: ${model}.`);
65
+ if (status >= 500)
66
+ throw new LLMServerError(`Server error (${status}). Provider may be experiencing issues.`);
67
+ throw new LLMError(`API error (${status}): ${errBody.substring(0, 200)}`);
68
+ }
69
+ const MODEL_QUIRKS = [
70
+ {
71
+ matches: 'kimi-k2',
72
+ reason: 'Kimi k2 vision/text models require temperature: 1 (rejects 0 with HTTP 400).',
73
+ apply: (b) => { if ('temperature' in b)
74
+ b.temperature = 1; },
75
+ },
76
+ {
77
+ matches: 'o1',
78
+ reason: 'OpenAI o1 models reject max_tokens (use max_completion_tokens) and temperature != 1.',
79
+ apply: (b) => {
80
+ if ('max_tokens' in b && !('max_completion_tokens' in b)) {
81
+ b.max_completion_tokens = b.max_tokens;
82
+ delete b.max_tokens;
83
+ }
84
+ if ('temperature' in b && b.temperature !== 1)
85
+ b.temperature = 1;
86
+ },
87
+ },
88
+ {
89
+ matches: 'o3',
90
+ reason: 'OpenAI o3 family follows the same constraints as o1.',
91
+ apply: (b) => {
92
+ if ('max_tokens' in b && !('max_completion_tokens' in b)) {
93
+ b.max_completion_tokens = b.max_tokens;
94
+ delete b.max_tokens;
95
+ }
96
+ if ('temperature' in b && b.temperature !== 1)
97
+ b.temperature = 1;
98
+ },
99
+ },
100
+ {
101
+ matches: 'gpt-5',
102
+ reason: 'GPT-5 family only accepts temperature: 1.',
103
+ apply: (b) => { if ('temperature' in b && b.temperature !== 1)
104
+ b.temperature = 1; },
105
+ },
106
+ ];
107
+ /**
108
+ * Apply any model-specific quirks to a request body in place.
109
+ *
110
+ * Lookup is by lowercase substring match on the model id. Every matching
111
+ * quirk runs (multiple rules can apply to the same model). Safe to call
112
+ * with any model id; unknown models pass through unchanged.
113
+ */
114
+ function applyModelQuirks(model, body) {
115
+ if (!model)
116
+ return;
117
+ const id = model.toLowerCase();
118
+ for (const quirk of MODEL_QUIRKS) {
119
+ if (id.includes(quirk.matches))
120
+ quirk.apply(body);
121
+ }
122
+ }
123
+ // ─── Public entry points ──────────────────────────────────────────────────────
124
+ /**
125
+ * Call a text LLM using PipelineConfig (used by reasoners).
126
+ */
127
+ async function callTextLLM(config, options) {
128
+ const { model, baseUrl } = config.layer2;
129
+ // Use per-layer API key if available (mixed pipelines use different keys per layer)
130
+ const apiKey = config.layer2.apiKey || config.apiKey || '';
131
+ // Determine API format from the layer's base URL, not the main provider.
132
+ // Mixed pipelines (Kimi text + Anthropic vision) need different formats per layer.
133
+ const isAnthropic = baseUrl.includes('anthropic.com')
134
+ && !baseUrl.includes('localhost')
135
+ && !baseUrl.includes('11434');
136
+ // Build auth headers for the layer's provider (may differ from main provider)
137
+ const layerProviderKey = (0, credentials_1.inferProviderFromBaseUrl)(baseUrl) || config.providerKey;
138
+ const layerProvider = providers_1.PROVIDERS[layerProviderKey] || config.provider;
139
+ const authHeaders = { ...layerProvider.authHeader(apiKey), ...(layerProvider.extraHeaders || {}) };
140
+ return _callText({
141
+ baseUrl,
142
+ model,
143
+ apiKey,
144
+ isAnthropic,
145
+ authHeaders,
146
+ providerProfile: config.provider,
147
+ ...options,
148
+ });
149
+ }
150
+ /**
151
+ * Call a text LLM using explicit provider params (used by pipeline preprocessor).
152
+ */
153
+ async function callTextLLMDirect(opts) {
154
+ const authHeaders = opts.isAnthropic
155
+ ? { 'x-api-key': opts.apiKey, 'anthropic-version': '2023-06-01' }
156
+ : opts.apiKey
157
+ ? { 'Authorization': `Bearer ${opts.apiKey}` }
158
+ : {};
159
+ return _callText({
160
+ ...opts,
161
+ authHeaders,
162
+ });
163
+ }
164
+ async function _callText(opts) {
165
+ const { baseUrl, model, apiKey: _apiKey, isAnthropic, authHeaders, system, user, messages: rawMessages, forceJson = false, maxTokens = 500, timeoutMs, retries = 0, } = opts;
166
+ for (let attempt = 0; attempt <= retries; attempt++) {
167
+ try {
168
+ if (retries > 0) {
169
+ console.log(` 🔗 LLM text call (attempt ${attempt + 1}): model=${model}`);
170
+ }
171
+ const canUseJsonMode = (0, providers_1.supportsOpenAiJsonMode)(opts.providerProfile);
172
+ const result = isAnthropic
173
+ ? await _callAnthropic({ baseUrl, model, authHeaders, system, user, rawMessages, forceJson, maxTokens, timeoutMs })
174
+ : await _callOpenAI({ baseUrl, model, authHeaders, system, user, rawMessages, forceJson, maxTokens, timeoutMs, canUseJsonMode, isReasoningModel: false }); // text calls always use temperature=0
175
+ return result;
176
+ }
177
+ catch (err) {
178
+ // Don't retry auth/billing errors — they won't resolve on retry
179
+ if (err instanceof LLMAuthError || err instanceof LLMBillingError)
180
+ throw err;
181
+ if (attempt < retries) {
182
+ console.warn(` ⚠️ LLM text call attempt ${attempt + 1} failed: ${err}`);
183
+ const backoff = Math.min(1000 * Math.pow(2, attempt), 8000) + Math.random() * 1000;
184
+ console.log(` ⏳ Retrying in ${Math.round(backoff)}ms...`);
185
+ await new Promise(r => setTimeout(r, backoff));
186
+ }
187
+ else {
188
+ throw err;
189
+ }
190
+ }
191
+ }
192
+ // Unreachable — loop always returns or throws, but TypeScript needs this
193
+ throw new Error('LLM text call failed after retries');
194
+ }
195
+ // ─── OpenAI-compatible path ───────────────────────────────────────────────────
196
+ async function _callOpenAI(p) {
197
+ // Build messages: either from rawMessages or from system+user
198
+ let messages;
199
+ if (p.rawMessages && p.rawMessages.length > 0) {
200
+ messages = p.rawMessages;
201
+ }
202
+ else {
203
+ messages = [
204
+ { role: 'system', content: p.system || '' },
205
+ { role: 'user', content: p.user || '' },
206
+ ];
207
+ }
208
+ const body = {
209
+ model: p.model,
210
+ messages,
211
+ max_tokens: p.maxTokens,
212
+ };
213
+ // Reasoning models (kimi-k2.5, etc.) reject temperature=0 — omit it.
214
+ // Uses declarative flag instead of hardcoded model name matching.
215
+ if (!p.isReasoningModel) {
216
+ body.temperature = 0;
217
+ }
218
+ if (p.forceJson && p.canUseJsonMode !== false) {
219
+ body.response_format = { type: 'json_object' };
220
+ }
221
+ const fetchOpts = {
222
+ method: 'POST',
223
+ headers: { 'Content-Type': 'application/json', ...p.authHeaders },
224
+ body: JSON.stringify(body),
225
+ };
226
+ if (p.timeoutMs) {
227
+ fetchOpts.signal = AbortSignal.timeout(p.timeoutMs);
228
+ }
229
+ const response = await fetch(`${p.baseUrl}/chat/completions`, fetchOpts);
230
+ if (!response.ok) {
231
+ const errBody = await response.text().catch(() => '');
232
+ throwOnHttpError(response.status, p.model, errBody);
233
+ }
234
+ const data = await response.json();
235
+ if (data.error)
236
+ throw new LLMError(data.error.message || JSON.stringify(data.error));
237
+ const msg = data.choices?.[0]?.message;
238
+ // kimi-k2.5 and other reasoning models may return empty content with reasoning_content.
239
+ // Fall back to reasoning_content when content is empty.
240
+ return msg?.content || msg?.reasoning_content || '';
241
+ }
242
+ // ─── Anthropic Messages API path ──────────────────────────────────────────────
243
+ async function _callAnthropic(p) {
244
+ let systemPrompt;
245
+ let messages;
246
+ if (p.rawMessages && p.rawMessages.length > 0) {
247
+ // Multi-turn: extract system from first message if it's a system role
248
+ if (p.rawMessages[0].role === 'system') {
249
+ systemPrompt = p.rawMessages[0].content;
250
+ messages = p.rawMessages.slice(1).map(m => ({
251
+ role: m.role === 'system' ? 'user' : m.role,
252
+ content: m.content,
253
+ }));
254
+ }
255
+ else {
256
+ systemPrompt = '';
257
+ messages = p.rawMessages.map(m => ({
258
+ role: m.role === 'system' ? 'user' : m.role,
259
+ content: m.content,
260
+ }));
261
+ }
262
+ }
263
+ else {
264
+ systemPrompt = p.system || '';
265
+ messages = [{ role: 'user', content: p.user || '' }];
266
+ }
267
+ // forceJson: prefill '{' so Anthropic continues with valid JSON
268
+ if (p.forceJson) {
269
+ messages.push({ role: 'assistant', content: '{' });
270
+ }
271
+ const body = {
272
+ model: p.model,
273
+ max_tokens: p.maxTokens,
274
+ system: systemPrompt,
275
+ messages,
276
+ temperature: 0,
277
+ };
278
+ applyModelQuirks(p.model, body);
279
+ const fetchOpts = {
280
+ method: 'POST',
281
+ headers: { 'Content-Type': 'application/json', ...p.authHeaders },
282
+ body: JSON.stringify(body),
283
+ };
284
+ if (p.timeoutMs) {
285
+ fetchOpts.signal = AbortSignal.timeout(p.timeoutMs);
286
+ }
287
+ const response = await fetch(`${p.baseUrl}/messages`, fetchOpts);
288
+ if (!response.ok) {
289
+ const errBody = await response.text().catch(() => '');
290
+ throwOnHttpError(response.status, p.model, errBody);
291
+ }
292
+ const data = await response.json();
293
+ if (data.error)
294
+ throw new LLMError(data.error.message || JSON.stringify(data.error));
295
+ const text = data.content?.[0]?.text || '';
296
+ // When forceJson, prepend the '{' back since the API only returns the continuation
297
+ if (p.forceJson) {
298
+ return text.startsWith('{') ? text : '{' + text;
299
+ }
300
+ return text;
301
+ }
302
+ /**
303
+ * Normalize an image content block to the target provider format.
304
+ * Callers can pass images in either OpenAI or Anthropic format.
305
+ */
306
+ function normalizeImageBlock(block, isAnthropic) {
307
+ if (block.type === 'text')
308
+ return block;
309
+ if (isAnthropic) {
310
+ // Target: Anthropic format
311
+ if (block.type === 'image')
312
+ return block; // already Anthropic
313
+ // Convert OpenAI → Anthropic
314
+ const url = block.image_url?.url || '';
315
+ const match = url.match(/^data:(image\/\w+);base64,(.+)$/);
316
+ if (!match)
317
+ return block;
318
+ return { type: 'image', source: { type: 'base64', media_type: match[1], data: match[2] } };
319
+ }
320
+ else {
321
+ // Target: OpenAI format
322
+ if (block.type === 'image_url')
323
+ return block; // already OpenAI
324
+ // Convert Anthropic → OpenAI
325
+ const src = block.source;
326
+ if (!src?.data)
327
+ return block;
328
+ return { type: 'image_url', image_url: { url: `data:${src.media_type || 'image/png'};base64,${src.data}` } };
329
+ }
330
+ }
331
+ /** Normalize all content blocks in a message array for the target provider. */
332
+ function normalizeMessages(messages, isAnthropic) {
333
+ return messages.map(msg => {
334
+ if (typeof msg.content === 'string')
335
+ return msg;
336
+ return {
337
+ role: msg.role,
338
+ content: msg.content.map(b => normalizeImageBlock(b, isAnthropic)),
339
+ };
340
+ });
341
+ }
342
+ // ─── Vision LLM entry points ────────────────────────────────────────────────
343
+ /**
344
+ * Call a vision LLM using PipelineConfig.
345
+ * Uses layer3 config (vision model) with layer2 as fallback.
346
+ */
347
+ async function callVisionLLM(config, options) {
348
+ const layer = config.layer3.enabled ? config.layer3 : config.layer2;
349
+ const baseUrl = layer.baseUrl;
350
+ const model = layer.model;
351
+ // Use layer-specific API key if available (mixed pipelines use different keys per layer)
352
+ const apiKey = (config.layer3.enabled ? config.layer3.apiKey : undefined) || config.apiKey || '';
353
+ const isAnthropic = !config.provider.openaiCompat
354
+ && !baseUrl.includes('localhost')
355
+ && !baseUrl.includes('11434');
356
+ return callVisionLLMDirect({ ...options, baseUrl, model, apiKey, isAnthropic, providerProfile: config.provider });
357
+ }
358
+ /**
359
+ * Call a vision LLM with explicit provider params.
360
+ */
361
+ async function callVisionLLMDirect(opts) {
362
+ const authHeaders = opts.isAnthropic
363
+ ? { 'x-api-key': opts.apiKey, 'anthropic-version': '2023-06-01' }
364
+ : opts.apiKey
365
+ ? { 'Authorization': `Bearer ${opts.apiKey}` }
366
+ : {};
367
+ const { retries = 0 } = opts;
368
+ for (let attempt = 0; attempt <= retries; attempt++) {
369
+ try {
370
+ return opts.isAnthropic
371
+ ? await _callVisionAnthropic({ ...opts, authHeaders })
372
+ : await _callVisionOpenAI({ ...opts, authHeaders });
373
+ }
374
+ catch (err) {
375
+ // Don't retry auth/billing errors
376
+ if (err instanceof LLMAuthError || err instanceof LLMBillingError)
377
+ throw err;
378
+ if (attempt < retries) {
379
+ const backoff = Math.min(1000 * Math.pow(2, attempt), 8000) + Math.random() * 1000;
380
+ await new Promise(r => setTimeout(r, backoff));
381
+ }
382
+ else {
383
+ throw err;
384
+ }
385
+ }
386
+ }
387
+ throw new Error('Vision LLM call failed after retries');
388
+ }
389
+ // ─── Vision: OpenAI-compatible path ──────────────────────────────────────────
390
+ async function _callVisionOpenAI(p) {
391
+ const messages = normalizeMessages(p.messages, false);
392
+ const body = {
393
+ model: p.model,
394
+ messages,
395
+ max_tokens: p.maxTokens || 1024,
396
+ };
397
+ // Reasoning models reject temperature=0 — use provider flag, not model name
398
+ if (!p.providerProfile?.reasoningVisionModel) {
399
+ body.temperature = 0;
400
+ }
401
+ if (p.forceJson && (0, providers_1.supportsOpenAiJsonMode)(p.providerProfile)) {
402
+ body.response_format = { type: 'json_object' };
403
+ }
404
+ const fetchOpts = {
405
+ method: 'POST',
406
+ headers: { 'Content-Type': 'application/json', ...p.authHeaders },
407
+ body: JSON.stringify(body),
408
+ };
409
+ if (p.timeoutMs) {
410
+ fetchOpts.signal = AbortSignal.timeout(p.timeoutMs);
411
+ }
412
+ const response = await fetch(`${p.baseUrl}/chat/completions`, fetchOpts);
413
+ if (!response.ok) {
414
+ const errBody = await response.text().catch(() => '');
415
+ throwOnHttpError(response.status, p.model, errBody);
416
+ }
417
+ const data = await response.json();
418
+ if (data.error)
419
+ throw new LLMError(data.error.message || JSON.stringify(data.error));
420
+ const msg = data.choices?.[0]?.message;
421
+ return msg?.content || msg?.reasoning_content || '';
422
+ }
423
+ // ─── Vision: Anthropic Messages API path ─────────────────────────────────────
424
+ async function _callVisionAnthropic(p) {
425
+ const normalized = normalizeMessages(p.messages, true);
426
+ // Extract system from first message if system role
427
+ let systemPrompt = p.system || '';
428
+ let messages;
429
+ if (normalized[0]?.role === 'system') {
430
+ systemPrompt = typeof normalized[0].content === 'string' ? normalized[0].content : '';
431
+ messages = normalized.slice(1);
432
+ }
433
+ else {
434
+ messages = normalized;
435
+ }
436
+ // Fix role mapping (Anthropic doesn't have 'system' in messages)
437
+ messages = messages.map(m => ({
438
+ ...m,
439
+ role: m.role === 'system' ? 'user' : m.role,
440
+ }));
441
+ // JSON forcing via assistant prefill
442
+ if (p.forceJson || p.jsonPrefill) {
443
+ const prefill = p.jsonPrefill || '{';
444
+ messages.push({ role: 'assistant', content: prefill });
445
+ }
446
+ const body = {
447
+ model: p.model,
448
+ max_tokens: p.maxTokens || 1024,
449
+ system: systemPrompt,
450
+ messages,
451
+ temperature: 0,
452
+ ...(p.stream ? { stream: true } : {}),
453
+ };
454
+ applyModelQuirks(p.model, body);
455
+ const fetchOpts = {
456
+ method: 'POST',
457
+ headers: { 'Content-Type': 'application/json', ...p.authHeaders },
458
+ body: JSON.stringify(body),
459
+ };
460
+ if (p.timeoutMs) {
461
+ fetchOpts.signal = AbortSignal.timeout(p.timeoutMs);
462
+ }
463
+ const response = await fetch(`${p.baseUrl}/messages`, fetchOpts);
464
+ if (!response.ok) {
465
+ const errBody = await response.text().catch(() => '');
466
+ throwOnHttpError(response.status, p.model, errBody);
467
+ }
468
+ // Streaming path: read SSE events using event-type state machine
469
+ if (p.stream && response.body) {
470
+ let accumulated = '';
471
+ let currentEventType = '';
472
+ let messageComplete = false;
473
+ const reader = response.body.getReader();
474
+ const decoder = new TextDecoder();
475
+ try {
476
+ while (true) {
477
+ const { done, value } = await reader.read();
478
+ if (done)
479
+ break;
480
+ const chunk = decoder.decode(value, { stream: true });
481
+ for (const line of chunk.split('\n')) {
482
+ // Track SSE event type from "event:" lines
483
+ if (line.startsWith('event:')) {
484
+ currentEventType = line.slice(6).trim();
485
+ continue;
486
+ }
487
+ if (!line.startsWith('data:'))
488
+ continue;
489
+ const payload = line.slice(5).trim();
490
+ if (payload === '[DONE]') {
491
+ messageComplete = true;
492
+ break;
493
+ }
494
+ try {
495
+ const event = JSON.parse(payload);
496
+ if (currentEventType === 'content_block_delta') {
497
+ const delta = event.delta?.text || '';
498
+ accumulated += delta;
499
+ }
500
+ else if (currentEventType === 'content_block_stop' || currentEventType === 'message_stop') {
501
+ messageComplete = true;
502
+ break;
503
+ }
504
+ }
505
+ catch { /* skip malformed SSE */ }
506
+ }
507
+ if (messageComplete)
508
+ break;
509
+ }
510
+ }
511
+ finally {
512
+ reader.releaseLock();
513
+ }
514
+ return accumulated;
515
+ }
516
+ // Non-streaming path
517
+ const data = await response.json();
518
+ if (data.error)
519
+ throw new LLMError(data.error.message || JSON.stringify(data.error));
520
+ const text = data.content?.[0]?.text || '';
521
+ if (p.forceJson || p.jsonPrefill) {
522
+ const prefill = p.jsonPrefill || '{';
523
+ return text.startsWith(prefill.charAt(0)) ? text : prefill + text;
524
+ }
525
+ return text;
526
+ }
527
+ /**
528
+ * Combine the per-call timeout with an external user-abort signal.
529
+ * AbortSignal.any needs Node 20.3+; on older 20.x fall back to the timeout
530
+ * alone (the agent loop still notices the abort at its next checkpoint).
531
+ */
532
+ function combineSignals(timeoutMs, external) {
533
+ const timeout = timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined;
534
+ if (timeout && external) {
535
+ const any = AbortSignal.any;
536
+ return any ? any.call(AbortSignal, [timeout, external]) : timeout;
537
+ }
538
+ return external ?? timeout;
539
+ }
540
+ /**
541
+ * Invoke an LLM with a tool catalog. Prefers native tool_use (Anthropic)
542
+ * or tool_calls (OpenAI) and falls back to JSON-in-prose parsing for
543
+ * providers that lack native support (Ollama text-only models, etc.).
544
+ *
545
+ * The caller supplies the tool catalog + multi-turn messages; the function
546
+ * returns a structured ToolUseResult with parsed tool calls. The agent runs
547
+ * each tool and feeds results back as `tool_result` blocks on the next turn.
548
+ */
549
+ async function callLLMWithTools(opts) {
550
+ const authHeaders = opts.isAnthropic
551
+ ? { 'x-api-key': opts.apiKey, 'anthropic-version': '2023-06-01' }
552
+ : opts.apiKey
553
+ ? { 'Authorization': `Bearer ${opts.apiKey}` }
554
+ : {};
555
+ return opts.isAnthropic
556
+ ? callAnthropicTools({ ...opts, authHeaders })
557
+ : callOpenAITools({ ...opts, authHeaders });
558
+ }
559
+ // ─── Anthropic tool_use path ─────────────────────────────────────────────────
560
+ async function callAnthropicTools(p) {
561
+ // Map our LLMTool shape directly onto Anthropic's `tools` schema.
562
+ const tools = p.tools.map(t => ({
563
+ name: t.name,
564
+ description: t.description,
565
+ input_schema: t.inputSchema,
566
+ }));
567
+ // Normalize messages into Anthropic content-block format. We accept
568
+ // both `image_url` (OpenAI) and `image` (Anthropic) and normalize image_url
569
+ // to Anthropic's base64 variant.
570
+ const messages = p.messages.map(turn => {
571
+ if (typeof turn.content === 'string') {
572
+ return { role: turn.role, content: turn.content };
573
+ }
574
+ const blocks = turn.content.map(b => {
575
+ if (b.type === 'image_url') {
576
+ const url = b.image_url?.url || '';
577
+ const m = url.match(/^data:(image\/\w+);base64,(.+)$/);
578
+ if (m)
579
+ return { type: 'image', source: { type: 'base64', media_type: m[1], data: m[2] } };
580
+ return b;
581
+ }
582
+ return b;
583
+ });
584
+ return { role: turn.role, content: blocks };
585
+ });
586
+ // Anthropic tool_choice shapes: {type:'auto'} | {type:'any'} | {type:'tool', name}
587
+ let toolChoice = undefined;
588
+ if (p.toolChoice && p.toolChoice !== 'none') {
589
+ toolChoice = typeof p.toolChoice === 'object'
590
+ ? { type: 'tool', name: p.toolChoice.name }
591
+ : { type: p.toolChoice };
592
+ }
593
+ const body = {
594
+ model: p.model,
595
+ max_tokens: p.maxTokens ?? 1024,
596
+ system: p.system,
597
+ messages,
598
+ tools,
599
+ temperature: 0,
600
+ };
601
+ if (toolChoice)
602
+ body.tool_choice = toolChoice;
603
+ applyModelQuirks(p.model, body);
604
+ const fetchOpts = {
605
+ method: 'POST',
606
+ headers: { 'Content-Type': 'application/json', ...p.authHeaders },
607
+ body: JSON.stringify(body),
608
+ };
609
+ const anthropicSignal = combineSignals(p.timeoutMs, p.signal);
610
+ if (anthropicSignal)
611
+ fetchOpts.signal = anthropicSignal;
612
+ const response = await fetch(`${p.baseUrl}/messages`, fetchOpts);
613
+ if (!response.ok) {
614
+ const errBody = await response.text().catch(() => '');
615
+ throwOnHttpError(response.status, p.model, errBody);
616
+ }
617
+ const data = await response.json();
618
+ if (data.error)
619
+ throw new LLMError(data.error.message || JSON.stringify(data.error));
620
+ const contentBlocks = Array.isArray(data.content) ? data.content : [];
621
+ const raw = [];
622
+ const toolCalls = [];
623
+ let prose = '';
624
+ for (const block of contentBlocks) {
625
+ if (block.type === 'text') {
626
+ prose += block.text || '';
627
+ raw.push({ type: 'text', text: block.text || '' });
628
+ }
629
+ else if (block.type === 'tool_use') {
630
+ const input = (block.input ?? {});
631
+ toolCalls.push({ id: String(block.id), name: String(block.name), args: input });
632
+ raw.push({ type: 'tool_use', id: String(block.id), name: String(block.name), input });
633
+ }
634
+ }
635
+ return {
636
+ text: prose,
637
+ toolCalls,
638
+ stopReason: String(data.stop_reason || 'end_turn'),
639
+ raw,
640
+ };
641
+ }
642
+ // ─── OpenAI tool_calls path ──────────────────────────────────────────────────
643
+ async function callOpenAITools(p) {
644
+ // Map tools to OpenAI's tools[].function schema.
645
+ const tools = p.tools.map(t => ({
646
+ type: 'function',
647
+ function: {
648
+ name: t.name,
649
+ description: t.description,
650
+ parameters: t.inputSchema,
651
+ },
652
+ }));
653
+ // Normalize messages into OpenAI chat format. OpenAI expects:
654
+ // { role:'system'|'user'|'assistant'|'tool', content, tool_calls?, tool_call_id? }
655
+ // We keep it simple: one system message prepended, then caller's turns.
656
+ const openaiMessages = [{ role: 'system', content: p.system }];
657
+ for (const turn of p.messages) {
658
+ if (typeof turn.content === 'string') {
659
+ openaiMessages.push({ role: turn.role, content: turn.content });
660
+ continue;
661
+ }
662
+ const blocks = turn.content;
663
+ if (turn.role === 'assistant') {
664
+ // Assistant turn may contain text + tool_use blocks; OpenAI wants
665
+ // { role:'assistant', content?: string, tool_calls?: [...] }
666
+ let text = '';
667
+ const tcalls = [];
668
+ for (const b of blocks) {
669
+ if (b.type === 'text')
670
+ text += b.text || '';
671
+ else if (b.type === 'tool_use') {
672
+ tcalls.push({
673
+ id: b.id,
674
+ type: 'function',
675
+ function: { name: b.name, arguments: JSON.stringify(b.input ?? {}) },
676
+ });
677
+ }
678
+ }
679
+ const msg = { role: 'assistant' };
680
+ if (text)
681
+ msg.content = text;
682
+ if (tcalls.length)
683
+ msg.tool_calls = tcalls;
684
+ if (!text && !tcalls.length)
685
+ msg.content = '';
686
+ openaiMessages.push(msg);
687
+ continue;
688
+ }
689
+ // User turn: tool_result blocks become separate `tool` messages; other
690
+ // blocks are consolidated into one user content array.
691
+ const userContent = [];
692
+ for (const b of blocks) {
693
+ if (b.type === 'tool_result') {
694
+ // Emit a standalone `tool` role message BEFORE the rest of the user turn.
695
+ const resultText = Array.isArray(b.content)
696
+ ? b.content.map((c) => c.type === 'text' ? c.text : '').filter(Boolean).join('\n')
697
+ : '';
698
+ openaiMessages.push({
699
+ role: 'tool',
700
+ tool_call_id: b.tool_use_id,
701
+ content: resultText,
702
+ });
703
+ }
704
+ else if (b.type === 'text') {
705
+ userContent.push({ type: 'text', text: b.text });
706
+ }
707
+ else if (b.type === 'image') {
708
+ const url = `data:${b.source.media_type};base64,${b.source.data}`;
709
+ userContent.push({ type: 'image_url', image_url: { url } });
710
+ }
711
+ else if (b.type === 'image_url') {
712
+ userContent.push({ type: 'image_url', image_url: b.image_url });
713
+ }
714
+ }
715
+ if (userContent.length === 1 && userContent[0].type === 'text') {
716
+ openaiMessages.push({ role: 'user', content: userContent[0].text });
717
+ }
718
+ else if (userContent.length > 0) {
719
+ openaiMessages.push({ role: 'user', content: userContent });
720
+ }
721
+ }
722
+ // tool_choice mapping
723
+ let toolChoice = 'auto';
724
+ if (p.toolChoice === 'none')
725
+ toolChoice = 'none';
726
+ else if (p.toolChoice === 'any')
727
+ toolChoice = 'required';
728
+ else if (typeof p.toolChoice === 'object') {
729
+ toolChoice = { type: 'function', function: { name: p.toolChoice.name } };
730
+ }
731
+ const body = {
732
+ model: p.model,
733
+ messages: openaiMessages,
734
+ tools,
735
+ tool_choice: toolChoice,
736
+ max_tokens: p.maxTokens ?? 1024,
737
+ temperature: 0,
738
+ };
739
+ applyModelQuirks(p.model, body);
740
+ const fetchOpts = {
741
+ method: 'POST',
742
+ headers: { 'Content-Type': 'application/json', ...p.authHeaders },
743
+ body: JSON.stringify(body),
744
+ };
745
+ const openaiSignal = combineSignals(p.timeoutMs, p.signal);
746
+ if (openaiSignal)
747
+ fetchOpts.signal = openaiSignal;
748
+ const response = await fetch(`${p.baseUrl}/chat/completions`, fetchOpts);
749
+ if (!response.ok) {
750
+ const errBody = await response.text().catch(() => '');
751
+ throwOnHttpError(response.status, p.model, errBody);
752
+ }
753
+ const data = await response.json();
754
+ if (data.error)
755
+ throw new LLMError(data.error.message || JSON.stringify(data.error));
756
+ const msg = data.choices?.[0]?.message ?? {};
757
+ const stopReason = String(data.choices?.[0]?.finish_reason || 'stop');
758
+ const text = typeof msg.content === 'string' ? msg.content : '';
759
+ const raw = [];
760
+ const toolCalls = [];
761
+ if (text)
762
+ raw.push({ type: 'text', text });
763
+ const tcalls = Array.isArray(msg.tool_calls) ? msg.tool_calls : [];
764
+ for (const tc of tcalls) {
765
+ let args = {};
766
+ try {
767
+ args = JSON.parse(tc.function?.arguments ?? '{}');
768
+ }
769
+ catch { /* keep empty */ }
770
+ const id = String(tc.id || `call_${toolCalls.length}`);
771
+ const name = String(tc.function?.name || '');
772
+ if (!name)
773
+ continue;
774
+ toolCalls.push({ id, name, args });
775
+ raw.push({ type: 'tool_use', id, name, input: args });
776
+ }
777
+ // Fallback: some providers (Ollama, some local models) don't emit native
778
+ // tool_calls even when `tools` is provided. If we got a prose response
779
+ // that *looks* like a tool call, parse it out so the agent keeps moving.
780
+ if (toolCalls.length === 0 && text) {
781
+ const parsed = tryParseProseToolCall(text);
782
+ if (parsed) {
783
+ const id = `prose_${Date.now()}`;
784
+ toolCalls.push({ id, name: parsed.name, args: parsed.args });
785
+ raw.push({ type: 'tool_use', id, name: parsed.name, input: parsed.args });
786
+ }
787
+ }
788
+ return { text, toolCalls, stopReason, raw };
789
+ }
790
+ /**
791
+ * Fallback prose→tool-call parser for providers that don't emit native tool_calls.
792
+ *
793
+ * Recognizes three families:
794
+ *
795
+ * 1. **Prefix-style** (Kimi `moonshot-v1-*`, some DeepSeek / Qwen text models):
796
+ * `functions.<TOOL>:<id>$\n{ "arg": "value" }`
797
+ * The function NAME lives in the prefix; the JSON body is the args.
798
+ *
799
+ * 2. **Llama-style** (some Llama/Mistral fine-tunes):
800
+ * `<function=NAME>{...args JSON...}</function>`
801
+ * Or: `<|tool_call|>NAME\n{...}`
802
+ *
803
+ * 3. **JSON-only** (older Ollama, generic text fallbacks):
804
+ * A bare JSON object with `tool|name|action` + `args|input|parameters` keys.
805
+ *
806
+ * The parser tries each in order and returns the first match. Returns null
807
+ * when nothing parses — the caller treats that as a legitimate text-only reply.
808
+ *
809
+ * Pattern-matched, NOT model-name-matched, so any provider that emits one
810
+ * of these formats works without an explicit allowlist entry.
811
+ */
812
+ function tryParseProseToolCall(prose) {
813
+ // Strip code fences once up-front; every family below benefits.
814
+ const cleaned = prose.replace(/```(?:json|tool|function)?\s*|```\s*$/g, '').trim();
815
+ // ── Family 1: prefix-style — `functions.<NAME>(:<id>)?(separator)<JSON>` ──
816
+ // The Kimi `moonshot-v1-*` shape. The model has shipped at least three
817
+ // separator variants in the wild:
818
+ // functions.NAME:0$\n{...} (original v0.8.8 era)
819
+ // functions.NAME:0->{...} (current as of 2026-05)
820
+ // functions.NAME:0\n{...} (no separator, just whitespace)
821
+ // Plus an args wrapper variant: Kimi sometimes emits `{_{...real args...}}`
822
+ // where `_` is a literal underscore "no-key" wrapper. We strip that by
823
+ // letting `extractParseableJsonObject` walk past unparseable outer braces.
824
+ const prefixMatch = cleaned.match(/(?:^|\n)\s*functions\.([A-Za-z_][\w]*)(?::\d+)?\s*(?:\$|->|=>)?\s*([\s\S]*)$/);
825
+ if (prefixMatch) {
826
+ const name = prefixMatch[1];
827
+ const body = prefixMatch[2].trim();
828
+ // Empty body — zero-arg call (e.g. `functions.read_screen:18` with nothing after).
829
+ if (!body)
830
+ return { name, args: {} };
831
+ const args = extractParseableJsonObject(body);
832
+ if (name && args !== null) {
833
+ return { name, args };
834
+ }
835
+ }
836
+ // ── Family 2a: Llama `<function=NAME>{...}</function>` ──
837
+ const llamaMatch = cleaned.match(/<function=([A-Za-z_][\w]*)>([\s\S]*?)<\/function>/);
838
+ if (llamaMatch) {
839
+ const args = extractFirstJsonObject(llamaMatch[2]);
840
+ if (args !== null)
841
+ return { name: llamaMatch[1], args };
842
+ }
843
+ // ── Family 2b: `<|tool_call|>NAME\n{...}` ──
844
+ const tagMatch = cleaned.match(/<\|tool_call|>\s*([A-Za-z_][\w]*)\s*([\s\S]*)$/);
845
+ if (tagMatch) {
846
+ const args = extractFirstJsonObject(tagMatch[2]);
847
+ if (args !== null)
848
+ return { name: tagMatch[1], args };
849
+ }
850
+ // ── Family 2c: Python-call syntax — `<NAME>(<key>: <value>, ...)` ──
851
+ // Kimi `kimi-k2.5` (vision) emits this on every terminal action.
852
+ // Examples observed in the wild:
853
+ // done(evidence: "Screenshot shows Outlook draft email")
854
+ // give_up(reason: "missing credentials")
855
+ // mouse_click(x: 100, y: 200)
856
+ // wait(seconds=2.5)
857
+ // Both `:` and `=` are accepted as kwarg separators. Handles balanced
858
+ // parens inside string literals so values like `"text (with parens)"` work.
859
+ const pyMatch = cleaned.match(/^\s*([A-Za-z_]\w*)\s*\(/);
860
+ if (pyMatch) {
861
+ const start = cleaned.indexOf('(', cleaned.indexOf(pyMatch[1])) + 1;
862
+ let depth = 1;
863
+ let inStr = false;
864
+ let strChar = '';
865
+ let esc = false;
866
+ let end = -1;
867
+ for (let i = start; i < cleaned.length; i++) {
868
+ const c = cleaned[i];
869
+ if (esc) {
870
+ esc = false;
871
+ continue;
872
+ }
873
+ if (c === '\\') {
874
+ esc = true;
875
+ continue;
876
+ }
877
+ if (inStr) {
878
+ if (c === strChar)
879
+ inStr = false;
880
+ continue;
881
+ }
882
+ if (c === '"' || c === "'") {
883
+ inStr = true;
884
+ strChar = c;
885
+ continue;
886
+ }
887
+ if (c === '(')
888
+ depth++;
889
+ else if (c === ')') {
890
+ depth--;
891
+ if (depth === 0) {
892
+ end = i;
893
+ break;
894
+ }
895
+ }
896
+ }
897
+ if (end !== -1) {
898
+ const argsBody = cleaned.slice(start, end).trim();
899
+ if (!argsBody)
900
+ return { name: pyMatch[1], args: {} };
901
+ // Coerce kwargs to JSON object literal: quote unquoted keys, normalise
902
+ // `=` to `:`, normalise single-quoted strings to double-quoted.
903
+ // Single-quote conversion is best-effort: it skips quotes inside
904
+ // already-double-quoted strings to avoid corrupting them.
905
+ let asJson = argsBody;
906
+ // Convert single-quoted strings to double-quoted (only outside of
907
+ // existing double-quoted regions).
908
+ asJson = convertSingleQuotedStrings(asJson);
909
+ // Quote unquoted keys before `:` or `=`, then normalise `=` to `:`.
910
+ asJson = asJson.replace(/(^|[\s,{])([A-Za-z_]\w*)\s*[:=]/g, '$1"$2":');
911
+ try {
912
+ const parsed = JSON.parse('{' + asJson + '}');
913
+ if (parsed && typeof parsed === 'object') {
914
+ return { name: pyMatch[1], args: parsed };
915
+ }
916
+ }
917
+ catch {
918
+ // Coercion failed (unusual nesting, unparseable value). Fall back
919
+ // to returning the call with empty args — the tool dispatcher will
920
+ // surface a clean error so the model can retry with a different shape.
921
+ return { name: pyMatch[1], args: {} };
922
+ }
923
+ }
924
+ }
925
+ // ── Family 3: JSON-only with self-describing keys ──
926
+ const obj = extractFirstJsonObject(cleaned);
927
+ if (obj && typeof obj === 'object') {
928
+ // Treat shape {tool|name|action: "...", args|input|parameters: {...}} as a tool call.
929
+ // Crucially: only accept this when there's an explicit args/input/parameters
930
+ // object — otherwise a payload like {"name":"Outlook"} (which is the *value*
931
+ // for an open_app call) gets mistaken for a call to a tool literally named
932
+ // "Outlook". This was the v0.8.8 mis-parse.
933
+ const explicitName = typeof obj.tool === 'string' ? obj.tool
934
+ : typeof obj.action === 'string' ? obj.action
935
+ : '';
936
+ const argsObj = (obj.args && typeof obj.args === 'object') ? obj.args
937
+ : (obj.input && typeof obj.input === 'object') ? obj.input
938
+ : (obj.parameters && typeof obj.parameters === 'object') ? obj.parameters
939
+ : null;
940
+ if (explicitName && argsObj !== null) {
941
+ return { name: explicitName, args: argsObj };
942
+ }
943
+ // Legacy lenient fallback: {name: "...", ...} — only when there's an `args`
944
+ // object peer, so we don't misread a parameter dictionary as a tool call.
945
+ if (typeof obj.name === 'string' && argsObj !== null) {
946
+ return { name: obj.name, args: argsObj };
947
+ }
948
+ }
949
+ return null;
950
+ }
951
+ /**
952
+ * Convert single-quoted string literals to double-quoted. Conservative:
953
+ * only swaps `'` runs that aren't already inside a double-quoted span.
954
+ * Used by the Python-call parser before JSON.parse — JSON only accepts
955
+ * double quotes, but Kimi-style emissions sometimes mix the two.
956
+ */
957
+ function convertSingleQuotedStrings(text) {
958
+ let out = '';
959
+ let inDouble = false;
960
+ let inSingle = false;
961
+ let esc = false;
962
+ for (let i = 0; i < text.length; i++) {
963
+ const c = text[i];
964
+ if (esc) {
965
+ out += c;
966
+ esc = false;
967
+ continue;
968
+ }
969
+ if (c === '\\') {
970
+ out += c;
971
+ esc = true;
972
+ continue;
973
+ }
974
+ if (!inSingle && c === '"') {
975
+ inDouble = !inDouble;
976
+ out += c;
977
+ continue;
978
+ }
979
+ if (!inDouble && c === "'") {
980
+ inSingle = !inSingle;
981
+ out += '"'; // swap to double-quote
982
+ continue;
983
+ }
984
+ // Inside a single-quoted string, escape any literal double-quotes so the
985
+ // JSON parser sees a valid string body.
986
+ if (inSingle && c === '"') {
987
+ out += '\\"';
988
+ continue;
989
+ }
990
+ out += c;
991
+ }
992
+ return out;
993
+ }
994
+ /**
995
+ * Extract the first balanced JSON object from a string. Returns the parsed
996
+ * object on success, or null if no balanced object exists or it doesn't parse.
997
+ * Handles strings with escapes correctly.
998
+ */
999
+ function extractFirstJsonObject(text) {
1000
+ const start = text.indexOf('{');
1001
+ if (start === -1)
1002
+ return null;
1003
+ let depth = 0;
1004
+ let inStr = false;
1005
+ let esc = false;
1006
+ let end = -1;
1007
+ for (let i = start; i < text.length; i++) {
1008
+ const c = text[i];
1009
+ if (esc) {
1010
+ esc = false;
1011
+ continue;
1012
+ }
1013
+ if (c === '\\') {
1014
+ esc = true;
1015
+ continue;
1016
+ }
1017
+ if (c === '"')
1018
+ inStr = !inStr;
1019
+ if (inStr)
1020
+ continue;
1021
+ if (c === '{')
1022
+ depth++;
1023
+ else if (c === '}') {
1024
+ depth--;
1025
+ if (depth === 0) {
1026
+ end = i + 1;
1027
+ break;
1028
+ }
1029
+ }
1030
+ }
1031
+ if (end === -1)
1032
+ return null;
1033
+ try {
1034
+ const parsed = JSON.parse(text.slice(start, end));
1035
+ return parsed && typeof parsed === 'object' ? parsed : null;
1036
+ }
1037
+ catch {
1038
+ return null;
1039
+ }
1040
+ }
1041
+ /**
1042
+ * Find the first PARSEABLE balanced JSON object in `text`.
1043
+ * Walks every `{` position and tries to JSON.parse the matching balanced
1044
+ * region. Returns the first one that parses successfully.
1045
+ *
1046
+ * This handles wrapper-style outputs like `{_{...real json...}}` that some
1047
+ * models (Kimi `moonshot-v1-*`) emit — the outer `{_{...}}` fails JSON.parse
1048
+ * because `_` isn't a valid key prefix, but the inner `{...}` parses cleanly.
1049
+ */
1050
+ function extractParseableJsonObject(text) {
1051
+ for (let i = 0; i < text.length; i++) {
1052
+ if (text[i] !== '{')
1053
+ continue;
1054
+ let depth = 0;
1055
+ let inStr = false;
1056
+ let esc = false;
1057
+ let end = -1;
1058
+ for (let j = i; j < text.length; j++) {
1059
+ const c = text[j];
1060
+ if (esc) {
1061
+ esc = false;
1062
+ continue;
1063
+ }
1064
+ if (c === '\\') {
1065
+ esc = true;
1066
+ continue;
1067
+ }
1068
+ if (c === '"')
1069
+ inStr = !inStr;
1070
+ if (inStr)
1071
+ continue;
1072
+ if (c === '{')
1073
+ depth++;
1074
+ else if (c === '}') {
1075
+ depth--;
1076
+ if (depth === 0) {
1077
+ end = j + 1;
1078
+ break;
1079
+ }
1080
+ }
1081
+ }
1082
+ if (end === -1)
1083
+ continue;
1084
+ try {
1085
+ const parsed = JSON.parse(text.slice(i, end));
1086
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
1087
+ return parsed;
1088
+ }
1089
+ }
1090
+ catch { /* try next position */ }
1091
+ }
1092
+ return null;
1093
+ }
1094
+ //# sourceMappingURL=client.js.map