openlore 2.0.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 (634) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +268 -0
  3. package/dist/api/analyze.d.ts +17 -0
  4. package/dist/api/analyze.d.ts.map +1 -0
  5. package/dist/api/analyze.js +143 -0
  6. package/dist/api/analyze.js.map +1 -0
  7. package/dist/api/audit.d.ts +10 -0
  8. package/dist/api/audit.d.ts.map +1 -0
  9. package/dist/api/audit.js +117 -0
  10. package/dist/api/audit.js.map +1 -0
  11. package/dist/api/decisions.d.ts +55 -0
  12. package/dist/api/decisions.d.ts.map +1 -0
  13. package/dist/api/decisions.js +157 -0
  14. package/dist/api/decisions.js.map +1 -0
  15. package/dist/api/drift.d.ts +21 -0
  16. package/dist/api/drift.d.ts.map +1 -0
  17. package/dist/api/drift.js +152 -0
  18. package/dist/api/drift.js.map +1 -0
  19. package/dist/api/generate.d.ts +18 -0
  20. package/dist/api/generate.d.ts.map +1 -0
  21. package/dist/api/generate.js +259 -0
  22. package/dist/api/generate.js.map +1 -0
  23. package/dist/api/index.d.ts +41 -0
  24. package/dist/api/index.d.ts.map +1 -0
  25. package/dist/api/index.js +34 -0
  26. package/dist/api/index.js.map +1 -0
  27. package/dist/api/init.d.ts +18 -0
  28. package/dist/api/init.d.ts.map +1 -0
  29. package/dist/api/init.js +83 -0
  30. package/dist/api/init.js.map +1 -0
  31. package/dist/api/run.d.ts +19 -0
  32. package/dist/api/run.d.ts.map +1 -0
  33. package/dist/api/run.js +312 -0
  34. package/dist/api/run.js.map +1 -0
  35. package/dist/api/specs.d.ts +49 -0
  36. package/dist/api/specs.d.ts.map +1 -0
  37. package/dist/api/specs.js +137 -0
  38. package/dist/api/specs.js.map +1 -0
  39. package/dist/api/types.d.ts +201 -0
  40. package/dist/api/types.d.ts.map +1 -0
  41. package/dist/api/types.js +9 -0
  42. package/dist/api/types.js.map +1 -0
  43. package/dist/api/verify.d.ts +20 -0
  44. package/dist/api/verify.d.ts.map +1 -0
  45. package/dist/api/verify.js +117 -0
  46. package/dist/api/verify.js.map +1 -0
  47. package/dist/cli/commands/analyze.d.ts +30 -0
  48. package/dist/cli/commands/analyze.d.ts.map +1 -0
  49. package/dist/cli/commands/analyze.js +683 -0
  50. package/dist/cli/commands/analyze.js.map +1 -0
  51. package/dist/cli/commands/audit.d.ts +9 -0
  52. package/dist/cli/commands/audit.d.ts.map +1 -0
  53. package/dist/cli/commands/audit.js +98 -0
  54. package/dist/cli/commands/audit.js.map +1 -0
  55. package/dist/cli/commands/decisions.d.ts +16 -0
  56. package/dist/cli/commands/decisions.d.ts.map +1 -0
  57. package/dist/cli/commands/decisions.js +864 -0
  58. package/dist/cli/commands/decisions.js.map +1 -0
  59. package/dist/cli/commands/digest.d.ts +9 -0
  60. package/dist/cli/commands/digest.d.ts.map +1 -0
  61. package/dist/cli/commands/digest.js +61 -0
  62. package/dist/cli/commands/digest.js.map +1 -0
  63. package/dist/cli/commands/doctor.d.ts +9 -0
  64. package/dist/cli/commands/doctor.d.ts.map +1 -0
  65. package/dist/cli/commands/doctor.js +398 -0
  66. package/dist/cli/commands/doctor.js.map +1 -0
  67. package/dist/cli/commands/drift.d.ts +9 -0
  68. package/dist/cli/commands/drift.d.ts.map +1 -0
  69. package/dist/cli/commands/drift.js +550 -0
  70. package/dist/cli/commands/drift.js.map +1 -0
  71. package/dist/cli/commands/generate.d.ts +9 -0
  72. package/dist/cli/commands/generate.d.ts.map +1 -0
  73. package/dist/cli/commands/generate.js +565 -0
  74. package/dist/cli/commands/generate.js.map +1 -0
  75. package/dist/cli/commands/init.d.ts +9 -0
  76. package/dist/cli/commands/init.d.ts.map +1 -0
  77. package/dist/cli/commands/init.js +173 -0
  78. package/dist/cli/commands/init.js.map +1 -0
  79. package/dist/cli/commands/mcp.d.ts +2235 -0
  80. package/dist/cli/commands/mcp.d.ts.map +1 -0
  81. package/dist/cli/commands/mcp.js +1384 -0
  82. package/dist/cli/commands/mcp.js.map +1 -0
  83. package/dist/cli/commands/refresh-stories.d.ts +10 -0
  84. package/dist/cli/commands/refresh-stories.d.ts.map +1 -0
  85. package/dist/cli/commands/refresh-stories.js +314 -0
  86. package/dist/cli/commands/refresh-stories.js.map +1 -0
  87. package/dist/cli/commands/run.d.ts +9 -0
  88. package/dist/cli/commands/run.d.ts.map +1 -0
  89. package/dist/cli/commands/run.js +459 -0
  90. package/dist/cli/commands/run.js.map +1 -0
  91. package/dist/cli/commands/setup.d.ts +19 -0
  92. package/dist/cli/commands/setup.d.ts.map +1 -0
  93. package/dist/cli/commands/setup.js +355 -0
  94. package/dist/cli/commands/setup.js.map +1 -0
  95. package/dist/cli/commands/test.d.ts +22 -0
  96. package/dist/cli/commands/test.d.ts.map +1 -0
  97. package/dist/cli/commands/test.js +180 -0
  98. package/dist/cli/commands/test.js.map +1 -0
  99. package/dist/cli/commands/verify.d.ts +9 -0
  100. package/dist/cli/commands/verify.d.ts.map +1 -0
  101. package/dist/cli/commands/verify.js +383 -0
  102. package/dist/cli/commands/verify.js.map +1 -0
  103. package/dist/cli/commands/view.d.ts +13 -0
  104. package/dist/cli/commands/view.d.ts.map +1 -0
  105. package/dist/cli/commands/view.js +547 -0
  106. package/dist/cli/commands/view.js.map +1 -0
  107. package/dist/cli/index.d.ts +9 -0
  108. package/dist/cli/index.d.ts.map +1 -0
  109. package/dist/cli/index.js +118 -0
  110. package/dist/cli/index.js.map +1 -0
  111. package/dist/cli/tui-approval.d.ts +11 -0
  112. package/dist/cli/tui-approval.d.ts.map +1 -0
  113. package/dist/cli/tui-approval.js +129 -0
  114. package/dist/cli/tui-approval.js.map +1 -0
  115. package/dist/constants.d.ts +314 -0
  116. package/dist/constants.d.ts.map +1 -0
  117. package/dist/constants.js +382 -0
  118. package/dist/constants.js.map +1 -0
  119. package/dist/core/analyzer/ai-config-generator.d.ts +54 -0
  120. package/dist/core/analyzer/ai-config-generator.d.ts.map +1 -0
  121. package/dist/core/analyzer/ai-config-generator.js +98 -0
  122. package/dist/core/analyzer/ai-config-generator.js.map +1 -0
  123. package/dist/core/analyzer/architecture-writer.d.ts +67 -0
  124. package/dist/core/analyzer/architecture-writer.d.ts.map +1 -0
  125. package/dist/core/analyzer/architecture-writer.js +209 -0
  126. package/dist/core/analyzer/architecture-writer.js.map +1 -0
  127. package/dist/core/analyzer/artifact-generator.d.ts +261 -0
  128. package/dist/core/analyzer/artifact-generator.d.ts.map +1 -0
  129. package/dist/core/analyzer/artifact-generator.js +909 -0
  130. package/dist/core/analyzer/artifact-generator.js.map +1 -0
  131. package/dist/core/analyzer/ast-chunker.d.ts +24 -0
  132. package/dist/core/analyzer/ast-chunker.d.ts.map +1 -0
  133. package/dist/core/analyzer/ast-chunker.js +198 -0
  134. package/dist/core/analyzer/ast-chunker.js.map +1 -0
  135. package/dist/core/analyzer/call-graph.d.ts +162 -0
  136. package/dist/core/analyzer/call-graph.d.ts.map +1 -0
  137. package/dist/core/analyzer/call-graph.js +2040 -0
  138. package/dist/core/analyzer/call-graph.js.map +1 -0
  139. package/dist/core/analyzer/code-shaper.d.ts +33 -0
  140. package/dist/core/analyzer/code-shaper.d.ts.map +1 -0
  141. package/dist/core/analyzer/code-shaper.js +154 -0
  142. package/dist/core/analyzer/code-shaper.js.map +1 -0
  143. package/dist/core/analyzer/codebase-digest.d.ts +40 -0
  144. package/dist/core/analyzer/codebase-digest.d.ts.map +1 -0
  145. package/dist/core/analyzer/codebase-digest.js +195 -0
  146. package/dist/core/analyzer/codebase-digest.js.map +1 -0
  147. package/dist/core/analyzer/cpp-header-resolver.d.ts +30 -0
  148. package/dist/core/analyzer/cpp-header-resolver.d.ts.map +1 -0
  149. package/dist/core/analyzer/cpp-header-resolver.js +71 -0
  150. package/dist/core/analyzer/cpp-header-resolver.js.map +1 -0
  151. package/dist/core/analyzer/dependency-graph.d.ts +230 -0
  152. package/dist/core/analyzer/dependency-graph.d.ts.map +1 -0
  153. package/dist/core/analyzer/dependency-graph.js +752 -0
  154. package/dist/core/analyzer/dependency-graph.js.map +1 -0
  155. package/dist/core/analyzer/duplicate-detector.d.ts +52 -0
  156. package/dist/core/analyzer/duplicate-detector.d.ts.map +1 -0
  157. package/dist/core/analyzer/duplicate-detector.js +289 -0
  158. package/dist/core/analyzer/duplicate-detector.js.map +1 -0
  159. package/dist/core/analyzer/embedding-service.d.ts +56 -0
  160. package/dist/core/analyzer/embedding-service.d.ts.map +1 -0
  161. package/dist/core/analyzer/embedding-service.js +118 -0
  162. package/dist/core/analyzer/embedding-service.js.map +1 -0
  163. package/dist/core/analyzer/env-extractor.d.ts +33 -0
  164. package/dist/core/analyzer/env-extractor.d.ts.map +1 -0
  165. package/dist/core/analyzer/env-extractor.js +196 -0
  166. package/dist/core/analyzer/env-extractor.js.map +1 -0
  167. package/dist/core/analyzer/external-packages.d.ts +20 -0
  168. package/dist/core/analyzer/external-packages.d.ts.map +1 -0
  169. package/dist/core/analyzer/external-packages.js +175 -0
  170. package/dist/core/analyzer/external-packages.js.map +1 -0
  171. package/dist/core/analyzer/file-walker.d.ts +78 -0
  172. package/dist/core/analyzer/file-walker.d.ts.map +1 -0
  173. package/dist/core/analyzer/file-walker.js +532 -0
  174. package/dist/core/analyzer/file-walker.js.map +1 -0
  175. package/dist/core/analyzer/function-registry-trie.d.ts +21 -0
  176. package/dist/core/analyzer/function-registry-trie.d.ts.map +1 -0
  177. package/dist/core/analyzer/function-registry-trie.js +39 -0
  178. package/dist/core/analyzer/function-registry-trie.js.map +1 -0
  179. package/dist/core/analyzer/http-route-parser.d.ts +152 -0
  180. package/dist/core/analyzer/http-route-parser.d.ts.map +1 -0
  181. package/dist/core/analyzer/http-route-parser.js +971 -0
  182. package/dist/core/analyzer/http-route-parser.js.map +1 -0
  183. package/dist/core/analyzer/import-parser.d.ts +100 -0
  184. package/dist/core/analyzer/import-parser.d.ts.map +1 -0
  185. package/dist/core/analyzer/import-parser.js +952 -0
  186. package/dist/core/analyzer/import-parser.js.map +1 -0
  187. package/dist/core/analyzer/import-resolver-bridge.d.ts +25 -0
  188. package/dist/core/analyzer/import-resolver-bridge.d.ts.map +1 -0
  189. package/dist/core/analyzer/import-resolver-bridge.js +99 -0
  190. package/dist/core/analyzer/import-resolver-bridge.js.map +1 -0
  191. package/dist/core/analyzer/index.d.ts +10 -0
  192. package/dist/core/analyzer/index.d.ts.map +1 -0
  193. package/dist/core/analyzer/index.js +10 -0
  194. package/dist/core/analyzer/index.js.map +1 -0
  195. package/dist/core/analyzer/middleware-extractor.d.ts +29 -0
  196. package/dist/core/analyzer/middleware-extractor.d.ts.map +1 -0
  197. package/dist/core/analyzer/middleware-extractor.js +195 -0
  198. package/dist/core/analyzer/middleware-extractor.js.map +1 -0
  199. package/dist/core/analyzer/refactor-analyzer.d.ts +83 -0
  200. package/dist/core/analyzer/refactor-analyzer.d.ts.map +1 -0
  201. package/dist/core/analyzer/refactor-analyzer.js +351 -0
  202. package/dist/core/analyzer/refactor-analyzer.js.map +1 -0
  203. package/dist/core/analyzer/repository-mapper.d.ts +150 -0
  204. package/dist/core/analyzer/repository-mapper.d.ts.map +1 -0
  205. package/dist/core/analyzer/repository-mapper.js +740 -0
  206. package/dist/core/analyzer/repository-mapper.js.map +1 -0
  207. package/dist/core/analyzer/schema-extractor.d.ts +41 -0
  208. package/dist/core/analyzer/schema-extractor.d.ts.map +1 -0
  209. package/dist/core/analyzer/schema-extractor.js +229 -0
  210. package/dist/core/analyzer/schema-extractor.js.map +1 -0
  211. package/dist/core/analyzer/signature-extractor.d.ts +31 -0
  212. package/dist/core/analyzer/signature-extractor.d.ts.map +1 -0
  213. package/dist/core/analyzer/signature-extractor.js +675 -0
  214. package/dist/core/analyzer/signature-extractor.js.map +1 -0
  215. package/dist/core/analyzer/significance-scorer.d.ts +79 -0
  216. package/dist/core/analyzer/significance-scorer.d.ts.map +1 -0
  217. package/dist/core/analyzer/significance-scorer.js +407 -0
  218. package/dist/core/analyzer/significance-scorer.js.map +1 -0
  219. package/dist/core/analyzer/spec-snapshot-generator.d.ts +17 -0
  220. package/dist/core/analyzer/spec-snapshot-generator.d.ts.map +1 -0
  221. package/dist/core/analyzer/spec-snapshot-generator.js +201 -0
  222. package/dist/core/analyzer/spec-snapshot-generator.js.map +1 -0
  223. package/dist/core/analyzer/spec-vector-index.d.ts +68 -0
  224. package/dist/core/analyzer/spec-vector-index.d.ts.map +1 -0
  225. package/dist/core/analyzer/spec-vector-index.js +340 -0
  226. package/dist/core/analyzer/spec-vector-index.js.map +1 -0
  227. package/dist/core/analyzer/subgraph-extractor.d.ts +51 -0
  228. package/dist/core/analyzer/subgraph-extractor.d.ts.map +1 -0
  229. package/dist/core/analyzer/subgraph-extractor.js +147 -0
  230. package/dist/core/analyzer/subgraph-extractor.js.map +1 -0
  231. package/dist/core/analyzer/type-inference-engine.d.ts +23 -0
  232. package/dist/core/analyzer/type-inference-engine.d.ts.map +1 -0
  233. package/dist/core/analyzer/type-inference-engine.js +130 -0
  234. package/dist/core/analyzer/type-inference-engine.js.map +1 -0
  235. package/dist/core/analyzer/ui-component-extractor.d.ts +43 -0
  236. package/dist/core/analyzer/ui-component-extractor.d.ts.map +1 -0
  237. package/dist/core/analyzer/ui-component-extractor.js +245 -0
  238. package/dist/core/analyzer/ui-component-extractor.js.map +1 -0
  239. package/dist/core/analyzer/unified-search.d.ts +116 -0
  240. package/dist/core/analyzer/unified-search.d.ts.map +1 -0
  241. package/dist/core/analyzer/unified-search.js +231 -0
  242. package/dist/core/analyzer/unified-search.js.map +1 -0
  243. package/dist/core/analyzer/vector-index.d.ts +92 -0
  244. package/dist/core/analyzer/vector-index.d.ts.map +1 -0
  245. package/dist/core/analyzer/vector-index.js +451 -0
  246. package/dist/core/analyzer/vector-index.js.map +1 -0
  247. package/dist/core/decisions/consolidator.d.ts +14 -0
  248. package/dist/core/decisions/consolidator.d.ts.map +1 -0
  249. package/dist/core/decisions/consolidator.js +169 -0
  250. package/dist/core/decisions/consolidator.js.map +1 -0
  251. package/dist/core/decisions/extractor.d.ts +26 -0
  252. package/dist/core/decisions/extractor.d.ts.map +1 -0
  253. package/dist/core/decisions/extractor.js +156 -0
  254. package/dist/core/decisions/extractor.js.map +1 -0
  255. package/dist/core/decisions/index.d.ts +19 -0
  256. package/dist/core/decisions/index.d.ts.map +1 -0
  257. package/dist/core/decisions/index.js +16 -0
  258. package/dist/core/decisions/index.js.map +1 -0
  259. package/dist/core/decisions/store.d.ts +36 -0
  260. package/dist/core/decisions/store.d.ts.map +1 -0
  261. package/dist/core/decisions/store.js +109 -0
  262. package/dist/core/decisions/store.js.map +1 -0
  263. package/dist/core/decisions/syncer.d.ts +27 -0
  264. package/dist/core/decisions/syncer.d.ts.map +1 -0
  265. package/dist/core/decisions/syncer.js +214 -0
  266. package/dist/core/decisions/syncer.js.map +1 -0
  267. package/dist/core/decisions/verifier.d.ts +20 -0
  268. package/dist/core/decisions/verifier.d.ts.map +1 -0
  269. package/dist/core/decisions/verifier.js +115 -0
  270. package/dist/core/decisions/verifier.js.map +1 -0
  271. package/dist/core/digest/digest-generator.d.ts +29 -0
  272. package/dist/core/digest/digest-generator.d.ts.map +1 -0
  273. package/dist/core/digest/digest-generator.js +181 -0
  274. package/dist/core/digest/digest-generator.js.map +1 -0
  275. package/dist/core/drift/drift-detector.d.ts +102 -0
  276. package/dist/core/drift/drift-detector.d.ts.map +1 -0
  277. package/dist/core/drift/drift-detector.js +598 -0
  278. package/dist/core/drift/drift-detector.js.map +1 -0
  279. package/dist/core/drift/git-diff.d.ts +60 -0
  280. package/dist/core/drift/git-diff.d.ts.map +1 -0
  281. package/dist/core/drift/git-diff.js +383 -0
  282. package/dist/core/drift/git-diff.js.map +1 -0
  283. package/dist/core/drift/index.d.ts +12 -0
  284. package/dist/core/drift/index.d.ts.map +1 -0
  285. package/dist/core/drift/index.js +9 -0
  286. package/dist/core/drift/index.js.map +1 -0
  287. package/dist/core/drift/spec-mapper.d.ts +73 -0
  288. package/dist/core/drift/spec-mapper.d.ts.map +1 -0
  289. package/dist/core/drift/spec-mapper.js +353 -0
  290. package/dist/core/drift/spec-mapper.js.map +1 -0
  291. package/dist/core/drift/test-suggester.d.ts +18 -0
  292. package/dist/core/drift/test-suggester.d.ts.map +1 -0
  293. package/dist/core/drift/test-suggester.js +107 -0
  294. package/dist/core/drift/test-suggester.js.map +1 -0
  295. package/dist/core/generator/adr-generator.d.ts +32 -0
  296. package/dist/core/generator/adr-generator.d.ts.map +1 -0
  297. package/dist/core/generator/adr-generator.js +192 -0
  298. package/dist/core/generator/adr-generator.js.map +1 -0
  299. package/dist/core/generator/index.d.ts +9 -0
  300. package/dist/core/generator/index.d.ts.map +1 -0
  301. package/dist/core/generator/index.js +12 -0
  302. package/dist/core/generator/index.js.map +1 -0
  303. package/dist/core/generator/mapping-generator.d.ts +54 -0
  304. package/dist/core/generator/mapping-generator.d.ts.map +1 -0
  305. package/dist/core/generator/mapping-generator.js +240 -0
  306. package/dist/core/generator/mapping-generator.js.map +1 -0
  307. package/dist/core/generator/openspec-compat.d.ts +160 -0
  308. package/dist/core/generator/openspec-compat.d.ts.map +1 -0
  309. package/dist/core/generator/openspec-compat.js +524 -0
  310. package/dist/core/generator/openspec-compat.js.map +1 -0
  311. package/dist/core/generator/openspec-format-generator.d.ts +131 -0
  312. package/dist/core/generator/openspec-format-generator.d.ts.map +1 -0
  313. package/dist/core/generator/openspec-format-generator.js +963 -0
  314. package/dist/core/generator/openspec-format-generator.js.map +1 -0
  315. package/dist/core/generator/openspec-writer.d.ts +130 -0
  316. package/dist/core/generator/openspec-writer.d.ts.map +1 -0
  317. package/dist/core/generator/openspec-writer.js +404 -0
  318. package/dist/core/generator/openspec-writer.js.map +1 -0
  319. package/dist/core/generator/prompts.d.ts +35 -0
  320. package/dist/core/generator/prompts.d.ts.map +1 -0
  321. package/dist/core/generator/prompts.js +212 -0
  322. package/dist/core/generator/prompts.js.map +1 -0
  323. package/dist/core/generator/rag-manifest-generator.d.ts +37 -0
  324. package/dist/core/generator/rag-manifest-generator.d.ts.map +1 -0
  325. package/dist/core/generator/rag-manifest-generator.js +134 -0
  326. package/dist/core/generator/rag-manifest-generator.js.map +1 -0
  327. package/dist/core/generator/schemas.d.ts +365 -0
  328. package/dist/core/generator/schemas.d.ts.map +1 -0
  329. package/dist/core/generator/schemas.js +190 -0
  330. package/dist/core/generator/schemas.js.map +1 -0
  331. package/dist/core/generator/spec-pipeline.d.ts +123 -0
  332. package/dist/core/generator/spec-pipeline.d.ts.map +1 -0
  333. package/dist/core/generator/spec-pipeline.js +699 -0
  334. package/dist/core/generator/spec-pipeline.js.map +1 -0
  335. package/dist/core/generator/stages/stage1-survey.d.ts +19 -0
  336. package/dist/core/generator/stages/stage1-survey.d.ts.map +1 -0
  337. package/dist/core/generator/stages/stage1-survey.js +171 -0
  338. package/dist/core/generator/stages/stage1-survey.js.map +1 -0
  339. package/dist/core/generator/stages/stage2-entities.d.ts +11 -0
  340. package/dist/core/generator/stages/stage2-entities.d.ts.map +1 -0
  341. package/dist/core/generator/stages/stage2-entities.js +74 -0
  342. package/dist/core/generator/stages/stage2-entities.js.map +1 -0
  343. package/dist/core/generator/stages/stage3-services.d.ts +11 -0
  344. package/dist/core/generator/stages/stage3-services.d.ts.map +1 -0
  345. package/dist/core/generator/stages/stage3-services.js +85 -0
  346. package/dist/core/generator/stages/stage3-services.js.map +1 -0
  347. package/dist/core/generator/stages/stage4-api.d.ts +11 -0
  348. package/dist/core/generator/stages/stage4-api.d.ts.map +1 -0
  349. package/dist/core/generator/stages/stage4-api.js +72 -0
  350. package/dist/core/generator/stages/stage4-api.js.map +1 -0
  351. package/dist/core/generator/stages/stage5-architecture.d.ts +11 -0
  352. package/dist/core/generator/stages/stage5-architecture.d.ts.map +1 -0
  353. package/dist/core/generator/stages/stage5-architecture.js +75 -0
  354. package/dist/core/generator/stages/stage5-architecture.js.map +1 -0
  355. package/dist/core/generator/stages/stage6-adr.d.ts +8 -0
  356. package/dist/core/generator/stages/stage6-adr.d.ts.map +1 -0
  357. package/dist/core/generator/stages/stage6-adr.js +47 -0
  358. package/dist/core/generator/stages/stage6-adr.js.map +1 -0
  359. package/dist/core/services/chat-agent.d.ts +50 -0
  360. package/dist/core/services/chat-agent.d.ts.map +1 -0
  361. package/dist/core/services/chat-agent.js +369 -0
  362. package/dist/core/services/chat-agent.js.map +1 -0
  363. package/dist/core/services/chat-tools.d.ts +32 -0
  364. package/dist/core/services/chat-tools.d.ts.map +1 -0
  365. package/dist/core/services/chat-tools.js +494 -0
  366. package/dist/core/services/chat-tools.js.map +1 -0
  367. package/dist/core/services/config-manager.d.ts +61 -0
  368. package/dist/core/services/config-manager.d.ts.map +1 -0
  369. package/dist/core/services/config-manager.js +149 -0
  370. package/dist/core/services/config-manager.js.map +1 -0
  371. package/dist/core/services/edge-store.d.ts +57 -0
  372. package/dist/core/services/edge-store.d.ts.map +1 -0
  373. package/dist/core/services/edge-store.js +419 -0
  374. package/dist/core/services/edge-store.js.map +1 -0
  375. package/dist/core/services/gitignore-manager.d.ts +29 -0
  376. package/dist/core/services/gitignore-manager.d.ts.map +1 -0
  377. package/dist/core/services/gitignore-manager.js +95 -0
  378. package/dist/core/services/gitignore-manager.js.map +1 -0
  379. package/dist/core/services/index.d.ts +8 -0
  380. package/dist/core/services/index.d.ts.map +1 -0
  381. package/dist/core/services/index.js +8 -0
  382. package/dist/core/services/index.js.map +1 -0
  383. package/dist/core/services/llm-service.d.ts +379 -0
  384. package/dist/core/services/llm-service.d.ts.map +1 -0
  385. package/dist/core/services/llm-service.js +1553 -0
  386. package/dist/core/services/llm-service.js.map +1 -0
  387. package/dist/core/services/mcp-handlers/analysis.d.ts +127 -0
  388. package/dist/core/services/mcp-handlers/analysis.d.ts.map +1 -0
  389. package/dist/core/services/mcp-handlers/analysis.js +1185 -0
  390. package/dist/core/services/mcp-handlers/analysis.js.map +1 -0
  391. package/dist/core/services/mcp-handlers/change.d.ts +14 -0
  392. package/dist/core/services/mcp-handlers/change.d.ts.map +1 -0
  393. package/dist/core/services/mcp-handlers/change.js +416 -0
  394. package/dist/core/services/mcp-handlers/change.js.map +1 -0
  395. package/dist/core/services/mcp-handlers/decisions.d.ts +16 -0
  396. package/dist/core/services/mcp-handlers/decisions.d.ts.map +1 -0
  397. package/dist/core/services/mcp-handlers/decisions.js +239 -0
  398. package/dist/core/services/mcp-handlers/decisions.js.map +1 -0
  399. package/dist/core/services/mcp-handlers/graph.d.ts +94 -0
  400. package/dist/core/services/mcp-handlers/graph.d.ts.map +1 -0
  401. package/dist/core/services/mcp-handlers/graph.js +693 -0
  402. package/dist/core/services/mcp-handlers/graph.js.map +1 -0
  403. package/dist/core/services/mcp-handlers/orient.d.ts +17 -0
  404. package/dist/core/services/mcp-handlers/orient.d.ts.map +1 -0
  405. package/dist/core/services/mcp-handlers/orient.js +357 -0
  406. package/dist/core/services/mcp-handlers/orient.js.map +1 -0
  407. package/dist/core/services/mcp-handlers/semantic.d.ts +66 -0
  408. package/dist/core/services/mcp-handlers/semantic.d.ts.map +1 -0
  409. package/dist/core/services/mcp-handlers/semantic.js +432 -0
  410. package/dist/core/services/mcp-handlers/semantic.js.map +1 -0
  411. package/dist/core/services/mcp-handlers/utils.d.ts +85 -0
  412. package/dist/core/services/mcp-handlers/utils.d.ts.map +1 -0
  413. package/dist/core/services/mcp-handlers/utils.js +262 -0
  414. package/dist/core/services/mcp-handlers/utils.js.map +1 -0
  415. package/dist/core/services/mcp-watcher.d.ts +41 -0
  416. package/dist/core/services/mcp-watcher.d.ts.map +1 -0
  417. package/dist/core/services/mcp-watcher.js +254 -0
  418. package/dist/core/services/mcp-watcher.js.map +1 -0
  419. package/dist/core/services/project-detector.d.ts +32 -0
  420. package/dist/core/services/project-detector.d.ts.map +1 -0
  421. package/dist/core/services/project-detector.js +100 -0
  422. package/dist/core/services/project-detector.js.map +1 -0
  423. package/dist/core/test-generator/coverage-analyzer.d.ts +27 -0
  424. package/dist/core/test-generator/coverage-analyzer.d.ts.map +1 -0
  425. package/dist/core/test-generator/coverage-analyzer.js +285 -0
  426. package/dist/core/test-generator/coverage-analyzer.js.map +1 -0
  427. package/dist/core/test-generator/framework-detector.d.ts +17 -0
  428. package/dist/core/test-generator/framework-detector.d.ts.map +1 -0
  429. package/dist/core/test-generator/framework-detector.js +65 -0
  430. package/dist/core/test-generator/framework-detector.js.map +1 -0
  431. package/dist/core/test-generator/index.d.ts +14 -0
  432. package/dist/core/test-generator/index.d.ts.map +1 -0
  433. package/dist/core/test-generator/index.js +11 -0
  434. package/dist/core/test-generator/index.js.map +1 -0
  435. package/dist/core/test-generator/renderers/catch2.d.ts +8 -0
  436. package/dist/core/test-generator/renderers/catch2.d.ts.map +1 -0
  437. package/dist/core/test-generator/renderers/catch2.js +47 -0
  438. package/dist/core/test-generator/renderers/catch2.js.map +1 -0
  439. package/dist/core/test-generator/renderers/gtest.d.ts +8 -0
  440. package/dist/core/test-generator/renderers/gtest.d.ts.map +1 -0
  441. package/dist/core/test-generator/renderers/gtest.js +45 -0
  442. package/dist/core/test-generator/renderers/gtest.js.map +1 -0
  443. package/dist/core/test-generator/renderers/index.d.ts +20 -0
  444. package/dist/core/test-generator/renderers/index.d.ts.map +1 -0
  445. package/dist/core/test-generator/renderers/index.js +35 -0
  446. package/dist/core/test-generator/renderers/index.js.map +1 -0
  447. package/dist/core/test-generator/renderers/playwright.d.ts +8 -0
  448. package/dist/core/test-generator/renderers/playwright.d.ts.map +1 -0
  449. package/dist/core/test-generator/renderers/playwright.js +44 -0
  450. package/dist/core/test-generator/renderers/playwright.js.map +1 -0
  451. package/dist/core/test-generator/renderers/pytest.d.ts +8 -0
  452. package/dist/core/test-generator/renderers/pytest.d.ts.map +1 -0
  453. package/dist/core/test-generator/renderers/pytest.js +44 -0
  454. package/dist/core/test-generator/renderers/pytest.js.map +1 -0
  455. package/dist/core/test-generator/renderers/shared.d.ts +21 -0
  456. package/dist/core/test-generator/renderers/shared.d.ts.map +1 -0
  457. package/dist/core/test-generator/renderers/shared.js +56 -0
  458. package/dist/core/test-generator/renderers/shared.js.map +1 -0
  459. package/dist/core/test-generator/renderers/vitest.d.ts +8 -0
  460. package/dist/core/test-generator/renderers/vitest.d.ts.map +1 -0
  461. package/dist/core/test-generator/renderers/vitest.js +52 -0
  462. package/dist/core/test-generator/renderers/vitest.js.map +1 -0
  463. package/dist/core/test-generator/scenario-parser.d.ts +33 -0
  464. package/dist/core/test-generator/scenario-parser.d.ts.map +1 -0
  465. package/dist/core/test-generator/scenario-parser.js +244 -0
  466. package/dist/core/test-generator/scenario-parser.js.map +1 -0
  467. package/dist/core/test-generator/test-generator.d.ts +30 -0
  468. package/dist/core/test-generator/test-generator.d.ts.map +1 -0
  469. package/dist/core/test-generator/test-generator.js +174 -0
  470. package/dist/core/test-generator/test-generator.js.map +1 -0
  471. package/dist/core/test-generator/test-writer.d.ts +25 -0
  472. package/dist/core/test-generator/test-writer.d.ts.map +1 -0
  473. package/dist/core/test-generator/test-writer.js +128 -0
  474. package/dist/core/test-generator/test-writer.js.map +1 -0
  475. package/dist/core/test-generator/then-matchers.d.ts +35 -0
  476. package/dist/core/test-generator/then-matchers.d.ts.map +1 -0
  477. package/dist/core/test-generator/then-matchers.js +211 -0
  478. package/dist/core/test-generator/then-matchers.js.map +1 -0
  479. package/dist/core/verifier/index.d.ts +5 -0
  480. package/dist/core/verifier/index.d.ts.map +1 -0
  481. package/dist/core/verifier/index.js +5 -0
  482. package/dist/core/verifier/index.js.map +1 -0
  483. package/dist/core/verifier/verification-engine.d.ts +293 -0
  484. package/dist/core/verifier/verification-engine.d.ts.map +1 -0
  485. package/dist/core/verifier/verification-engine.js +919 -0
  486. package/dist/core/verifier/verification-engine.js.map +1 -0
  487. package/dist/types/index.d.ts +368 -0
  488. package/dist/types/index.d.ts.map +1 -0
  489. package/dist/types/index.js +5 -0
  490. package/dist/types/index.js.map +1 -0
  491. package/dist/types/pipeline.d.ts +167 -0
  492. package/dist/types/pipeline.d.ts.map +1 -0
  493. package/dist/types/pipeline.js +5 -0
  494. package/dist/types/pipeline.js.map +1 -0
  495. package/dist/types/test-generator.d.ts +103 -0
  496. package/dist/types/test-generator.d.ts.map +1 -0
  497. package/dist/types/test-generator.js +17 -0
  498. package/dist/types/test-generator.js.map +1 -0
  499. package/dist/utils/command-helpers.d.ts +68 -0
  500. package/dist/utils/command-helpers.d.ts.map +1 -0
  501. package/dist/utils/command-helpers.js +150 -0
  502. package/dist/utils/command-helpers.js.map +1 -0
  503. package/dist/utils/errors.d.ts +51 -0
  504. package/dist/utils/errors.d.ts.map +1 -0
  505. package/dist/utils/errors.js +129 -0
  506. package/dist/utils/errors.js.map +1 -0
  507. package/dist/utils/logger.d.ts +149 -0
  508. package/dist/utils/logger.d.ts.map +1 -0
  509. package/dist/utils/logger.js +342 -0
  510. package/dist/utils/logger.js.map +1 -0
  511. package/dist/utils/misc.d.ts +10 -0
  512. package/dist/utils/misc.d.ts.map +1 -0
  513. package/dist/utils/misc.js +21 -0
  514. package/dist/utils/misc.js.map +1 -0
  515. package/dist/utils/progress.d.ts +142 -0
  516. package/dist/utils/progress.d.ts.map +1 -0
  517. package/dist/utils/progress.js +283 -0
  518. package/dist/utils/progress.js.map +1 -0
  519. package/dist/utils/prompts.d.ts +53 -0
  520. package/dist/utils/prompts.d.ts.map +1 -0
  521. package/dist/utils/prompts.js +199 -0
  522. package/dist/utils/prompts.js.map +1 -0
  523. package/dist/utils/shutdown.d.ts +89 -0
  524. package/dist/utils/shutdown.d.ts.map +1 -0
  525. package/dist/utils/shutdown.js +238 -0
  526. package/dist/utils/shutdown.js.map +1 -0
  527. package/examples/bmad/README.md +113 -0
  528. package/examples/bmad/agents/architect.md +226 -0
  529. package/examples/bmad/agents/dev-brownfield.md +69 -0
  530. package/examples/bmad/setup/architect.customize.yaml +14 -0
  531. package/examples/bmad/tasks/implement-story.md +254 -0
  532. package/examples/bmad/tasks/onboarding.md +169 -0
  533. package/examples/bmad/tasks/refactor.md +178 -0
  534. package/examples/bmad/tasks/sprint-planning.md +168 -0
  535. package/examples/bmad/templates/story.md +108 -0
  536. package/examples/cline-workflows/openlore-analyze-codebase.md +101 -0
  537. package/examples/cline-workflows/openlore-check-spec-drift.md +102 -0
  538. package/examples/cline-workflows/openlore-execute-refactor.md +212 -0
  539. package/examples/cline-workflows/openlore-implement-feature.md +266 -0
  540. package/examples/cline-workflows/openlore-plan-refactor.md +279 -0
  541. package/examples/cline-workflows/openlore-refactor-codebase.md +16 -0
  542. package/examples/cline-workflows/openlore-write-tests.md +177 -0
  543. package/examples/drift-demo/openspec/config.yaml +14 -0
  544. package/examples/drift-demo/openspec/specs/architecture/spec.md +30 -0
  545. package/examples/drift-demo/openspec/specs/auth/spec.md +71 -0
  546. package/examples/drift-demo/openspec/specs/database/spec.md +33 -0
  547. package/examples/drift-demo/openspec/specs/overview/spec.md +20 -0
  548. package/examples/drift-demo/openspec/specs/projects/spec.md +55 -0
  549. package/examples/drift-demo/openspec/specs/tasks/spec.md +78 -0
  550. package/examples/drift-demo/package.json +21 -0
  551. package/examples/drift-demo/src/auth/auth-middleware.ts +30 -0
  552. package/examples/drift-demo/src/auth/auth-routes.ts +29 -0
  553. package/examples/drift-demo/src/auth/auth-service.ts +45 -0
  554. package/examples/drift-demo/src/database/connection.ts +27 -0
  555. package/examples/drift-demo/src/index.ts +16 -0
  556. package/examples/drift-demo/src/projects/project-model.ts +15 -0
  557. package/examples/drift-demo/src/projects/project-service.ts +34 -0
  558. package/examples/drift-demo/src/tasks/task-model.ts +37 -0
  559. package/examples/drift-demo/src/tasks/task-routes.ts +53 -0
  560. package/examples/drift-demo/src/tasks/task-service.ts +60 -0
  561. package/examples/drift-demo/src/utils/validation.ts +11 -0
  562. package/examples/drift-demo/tests/auth.test.ts +4 -0
  563. package/examples/drift-demo/tests/tasks.test.ts +4 -0
  564. package/examples/drift-demo/tsconfig.json +10 -0
  565. package/examples/drift-test/run-drift-test.sh +1087 -0
  566. package/examples/gsd/README.md +119 -0
  567. package/examples/gsd/commands/gsd/openlore-drift.md +111 -0
  568. package/examples/gsd/commands/gsd/openlore-orient.md +191 -0
  569. package/examples/mistral-vibe/README.md +101 -0
  570. package/examples/mistral-vibe/antipatterns-template.md +18 -0
  571. package/examples/mistral-vibe/skills/openlore-analyze-codebase/SKILL.md +124 -0
  572. package/examples/mistral-vibe/skills/openlore-brainstorm/SKILL.md +379 -0
  573. package/examples/mistral-vibe/skills/openlore-debug/SKILL.md +330 -0
  574. package/examples/mistral-vibe/skills/openlore-execute-refactor/SKILL.md +291 -0
  575. package/examples/mistral-vibe/skills/openlore-generate/SKILL.md +245 -0
  576. package/examples/mistral-vibe/skills/openlore-implement-story/SKILL.md +326 -0
  577. package/examples/mistral-vibe/skills/openlore-plan-refactor/SKILL.md +365 -0
  578. package/examples/mistral-vibe/skills/openlore-review-changes/SKILL.md +128 -0
  579. package/examples/mistral-vibe/skills/openlore-write-tests/SKILL.md +261 -0
  580. package/examples/opencode/agent-guard.ts +170 -0
  581. package/examples/opencode/plugins/anti-laziness.ts +202 -0
  582. package/examples/opencode/plugins/lib/openlore-context-injector-helpers.ts +116 -0
  583. package/examples/opencode/plugins/lib/openlore-decision-extractor-helpers.ts +65 -0
  584. package/examples/opencode/plugins/openlore-context-injector.test.ts +211 -0
  585. package/examples/opencode/plugins/openlore-context-injector.ts +165 -0
  586. package/examples/opencode/plugins/openlore-decision-extractor.test.ts +131 -0
  587. package/examples/opencode/plugins/openlore-decision-extractor.ts +322 -0
  588. package/examples/opencode/plugins/openlore-enforcer.ts +227 -0
  589. package/examples/opencode/prompts/sisyphus-sdd.md +150 -0
  590. package/examples/opencode-skills/openlore-analyze-codebase/SKILL.md +101 -0
  591. package/examples/opencode-skills/openlore-brainstorm/SKILL.md +354 -0
  592. package/examples/opencode-skills/openlore-debug/SKILL.md +291 -0
  593. package/examples/opencode-skills/openlore-execute-refactor/SKILL.md +241 -0
  594. package/examples/opencode-skills/openlore-generate/SKILL.md +236 -0
  595. package/examples/opencode-skills/openlore-implement-story/SKILL.md +251 -0
  596. package/examples/opencode-skills/openlore-plan-refactor/SKILL.md +298 -0
  597. package/examples/opencode-skills/openlore-review-changes/SKILL.md +134 -0
  598. package/examples/opencode-skills/openlore-write-tests/SKILL.md +230 -0
  599. package/examples/openspec-analysis/README.md +59 -0
  600. package/examples/openspec-analysis/SUMMARY.md +72 -0
  601. package/examples/openspec-analysis/config.json +16 -0
  602. package/examples/openspec-analysis/dependencies.mermaid +35 -0
  603. package/examples/openspec-analysis/dependency-graph.json +12116 -0
  604. package/examples/openspec-analysis/llm-context.json +119 -0
  605. package/examples/openspec-analysis/repo-structure.json +871 -0
  606. package/examples/openspec-cli/README.md +67 -0
  607. package/examples/openspec-cli/openspec/config.yaml +26 -0
  608. package/examples/openspec-cli/openspec/specs/architecture/spec.md +178 -0
  609. package/examples/openspec-cli/openspec/specs/artifact-graph/spec.md +143 -0
  610. package/examples/openspec-cli/openspec/specs/cli/spec.md +138 -0
  611. package/examples/openspec-cli/openspec/specs/overview/spec.md +60 -0
  612. package/examples/openspec-cli/openspec/specs/parsing/spec.md +123 -0
  613. package/examples/openspec-cli/openspec/specs/validation/spec.md +108 -0
  614. package/examples/spec-kit/README.md +104 -0
  615. package/examples/spec-kit/commands/drift.md +87 -0
  616. package/examples/spec-kit/commands/orient.md +138 -0
  617. package/examples/spec-kit/extension.yml +54 -0
  618. package/package.json +125 -0
  619. package/src/viewer/InteractiveGraphViewer.jsx +1600 -0
  620. package/src/viewer/app/index.html +17 -0
  621. package/src/viewer/app/main.jsx +13 -0
  622. package/src/viewer/components/ArchitectureView.jsx +177 -0
  623. package/src/viewer/components/ChatPanel.jsx +450 -0
  624. package/src/viewer/components/ClassGraph.jsx +782 -0
  625. package/src/viewer/components/ClusterGraph.jsx +469 -0
  626. package/src/viewer/components/FilterBar.jsx +179 -0
  627. package/src/viewer/components/FlatGraph.jsx +282 -0
  628. package/src/viewer/components/MicroComponents.jsx +85 -0
  629. package/src/viewer/hooks/usePanZoom.js +79 -0
  630. package/src/viewer/utils/constants.js +64 -0
  631. package/src/viewer/utils/graph-helpers.js +303 -0
  632. package/src/viewer/utils/graph-helpers.test.ts +39 -0
  633. package/src/viewer/utils/themes.js +206 -0
  634. package/stubs/tree-sitter-cli-stub/package.json +6 -0
@@ -0,0 +1,1553 @@
1
+ /**
2
+ * LLM Service
3
+ *
4
+ * Provides a clean interface for LLM interactions with proper error handling,
5
+ * retry logic, token management, and cost tracking.
6
+ */
7
+ import { writeFile, mkdir } from 'node:fs/promises';
8
+ import { join } from 'node:path';
9
+ import logger from '../../utils/logger.js';
10
+ import { CLAUDE_MAX_CONTEXT_TOKENS, CLAUDE_MAX_OUTPUT_TOKENS, MISTRAL_VIBE_MAX_CONTEXT_TOKENS, MISTRAL_VIBE_MAX_OUTPUT_TOKENS, LLM_CLI_MAX_BUFFER_BYTES, LLM_CLI_TIMEOUT_MS, DEFAULT_ANTHROPIC_MODEL, DEFAULT_OPENAI_MODEL, DEFAULT_OPENAI_COMPAT_MODEL, DEFAULT_COPILOT_MODEL, DEFAULT_GEMINI_MODEL, DEFAULT_LLM_MAX_RETRIES, DEFAULT_LLM_INITIAL_DELAY_MS, DEFAULT_LLM_MAX_DELAY_MS, DEFAULT_LLM_TIMEOUT_MS, DEFAULT_LLM_COST_WARNING_THRESHOLD, CONTEXT_LIMIT_WARNING_RATIO, OPENLORE_DIR, OPENLORE_LOGS_SUBDIR, } from '../../constants.js';
11
+ // ============================================================================
12
+ // CLAUDE CODE PROVIDER (uses local `claude` CLI, no API key required)
13
+ // ============================================================================
14
+ /**
15
+ * Claude Code CLI provider
16
+ *
17
+ * Routes LLM calls through the local `claude` CLI binary in non-interactive
18
+ * mode (`claude -p ...`). Authentication is handled by the Claude Code session
19
+ * (Max/Pro subscription) — no ANTHROPIC_API_KEY is required.
20
+ */
21
+ export class ClaudeCodeProvider {
22
+ name = 'claude-code';
23
+ maxContextTokens = CLAUDE_MAX_CONTEXT_TOKENS;
24
+ maxOutputTokens = CLAUDE_MAX_OUTPUT_TOKENS;
25
+ model;
26
+ constructor(model) {
27
+ // Only pass --model if it looks like a Claude model name.
28
+ // Ignore the sentinel 'claude-code' string and non-Claude model names
29
+ // (e.g. 'mistral-large-latest' from a shared config).
30
+ this.model = model && model !== 'claude-code' && model.startsWith('claude-') ? model : undefined;
31
+ }
32
+ async generateCompletion(request) {
33
+ const { execFileSync } = await import('child_process');
34
+ // Claude Code CLI takes a single prompt; combine system + user prompts
35
+ const fullPrompt = request.systemPrompt
36
+ ? `${request.systemPrompt}\n\n---\n\n${request.userPrompt}`
37
+ : request.userPrompt;
38
+ const args = ['-p', fullPrompt, '--output-format', 'json'];
39
+ if (this.model)
40
+ args.push('--model', this.model);
41
+ // Remove Claude Code session env vars so the CLI can run inside an existing session
42
+ const env = { ...process.env };
43
+ delete env.CLAUDECODE;
44
+ delete env.CLAUDE_CODE_ENTRYPOINT;
45
+ delete env.CLAUDE_CODE_SSE_PORT;
46
+ delete env.CLAUDE_CODE_IDE_PORT;
47
+ let raw;
48
+ try {
49
+ raw = execFileSync('claude', args, {
50
+ encoding: 'utf8',
51
+ maxBuffer: LLM_CLI_MAX_BUFFER_BYTES,
52
+ timeout: LLM_CLI_TIMEOUT_MS,
53
+ env,
54
+ });
55
+ }
56
+ catch (err) {
57
+ const e = err;
58
+ const detail = e.stderr || e.stdout || e.message || String(err);
59
+ throw Object.assign(new Error(`claude CLI failed: ${detail}`), { retryable: false });
60
+ }
61
+ let parsed;
62
+ try {
63
+ parsed = JSON.parse(raw);
64
+ }
65
+ catch {
66
+ throw new Error(`claude CLI returned non-JSON output: ${raw.slice(0, 200)}`);
67
+ }
68
+ if (parsed.is_error) {
69
+ throw Object.assign(new Error(`claude CLI error: ${parsed.result}`), { retryable: false });
70
+ }
71
+ const inputTokens = parsed.usage?.input_tokens ?? estimateTokens(fullPrompt);
72
+ const outputTokens = parsed.usage?.output_tokens ?? estimateTokens(parsed.result ?? '');
73
+ return {
74
+ content: parsed.result ?? '',
75
+ usage: { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens },
76
+ model: this.model ?? 'claude-code',
77
+ finishReason: 'stop',
78
+ };
79
+ }
80
+ countTokens(text) {
81
+ return estimateTokens(text);
82
+ }
83
+ }
84
+ // ============================================================================
85
+ // MISTRAL VIBE PROVIDER (uses local `mistral-vibe` CLI, no API key required)
86
+ // ============================================================================
87
+ /**
88
+ * Mistral Vibe CLI provider
89
+ *
90
+ * Routes LLM calls through the local `mistral-vibe` CLI binary (standalone, no npm).
91
+ * No API key required — uses local LLM execution.
92
+ * If the binary is not on PATH, set MISTRAL_VIBE_CLI to its full path.
93
+ * The CLI is invoked as `vibe` (not `mistral-vibe`).
94
+ */
95
+ export class MistralVibeProvider {
96
+ name = 'mistral-vibe';
97
+ maxContextTokens = MISTRAL_VIBE_MAX_CONTEXT_TOKENS;
98
+ maxOutputTokens = MISTRAL_VIBE_MAX_OUTPUT_TOKENS;
99
+ model;
100
+ constructor(model) {
101
+ // Ignore the sentinel 'mistral-vibe' string — let the CLI pick the default
102
+ this.model = model && model !== 'mistral-vibe' ? model : undefined;
103
+ }
104
+ async generateCompletion(request) {
105
+ const { execFileSync } = await import('child_process');
106
+ // Mistral Vibe CLI takes a single prompt; combine system + user prompts
107
+ const fullPrompt = request.systemPrompt
108
+ ? `${request.systemPrompt}\n\n---\n\n${request.userPrompt}`
109
+ : request.userPrompt;
110
+ // vibe CLI: -p for prompt, --output json for JSON, --agent for model/agent name
111
+ const args = ['-p', fullPrompt, '--output', 'json'];
112
+ if (this.model)
113
+ args.push('--agent', this.model);
114
+ // Use MISTRAL_VIBE_CLI if set (standalone install not on PATH), else 'vibe'
115
+ const mistralVibeBin = process.env.MISTRAL_VIBE_CLI ?? 'vibe';
116
+ let raw;
117
+ try {
118
+ raw = execFileSync(mistralVibeBin, args, {
119
+ encoding: 'utf8',
120
+ maxBuffer: LLM_CLI_MAX_BUFFER_BYTES,
121
+ timeout: LLM_CLI_TIMEOUT_MS,
122
+ });
123
+ }
124
+ catch (err) {
125
+ const e = err;
126
+ const detail = e.stderr ?? e.stdout ?? e.message ?? String(err);
127
+ throw Object.assign(new Error(`mistral-vibe CLI failed: ${detail}`), { retryable: false });
128
+ }
129
+ // Defensive parsing: vibe --output json format is undocumented.
130
+ // Try multiple known shapes before falling back to raw text.
131
+ let content = '';
132
+ let inputTokens;
133
+ let outputTokens;
134
+ try {
135
+ const parsed = JSON.parse(raw);
136
+ if (Array.isArray(parsed)) {
137
+ // Shape: [{role, content}, ...] — "all messages at end"
138
+ const msgs = parsed;
139
+ const lastAssistant = [...msgs].reverse().find(m => m.role === 'assistant');
140
+ content = String(lastAssistant?.content ?? '');
141
+ }
142
+ else if (typeof parsed === 'object' && parsed !== null) {
143
+ const p = parsed;
144
+ // Shape: {result: string, usage?: {...}} — Claude Code-style
145
+ if (typeof p.result === 'string') {
146
+ content = p.result;
147
+ const u = p.usage;
148
+ inputTokens = u?.input_tokens;
149
+ outputTokens = u?.output_tokens;
150
+ // Shape: {message: string} or {text: string} or {content: string}
151
+ }
152
+ else {
153
+ content = String(p.message ?? p.text ?? p.content ?? '');
154
+ }
155
+ }
156
+ }
157
+ catch {
158
+ // non-JSON output: use raw text
159
+ }
160
+ if (!content)
161
+ content = raw.trim();
162
+ inputTokens ??= estimateTokens(fullPrompt);
163
+ outputTokens ??= estimateTokens(content);
164
+ return {
165
+ content,
166
+ usage: { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens },
167
+ model: this.model ?? 'mistral-vibe',
168
+ finishReason: 'stop',
169
+ };
170
+ }
171
+ countTokens(text) {
172
+ return estimateTokens(text);
173
+ }
174
+ }
175
+ // ============================================================================
176
+ // SSL / FETCH HELPERS
177
+ // ============================================================================
178
+ /**
179
+ * Disable TLS certificate verification for all fetch requests in this process.
180
+ *
181
+ * Node.js native fetch does not support per-request TLS configuration.
182
+ * The only reliable cross-version approach is the NODE_TLS_REJECT_UNAUTHORIZED
183
+ * environment variable, which is process-global. This is set once and logged
184
+ * prominently so the user is aware.
185
+ */
186
+ function disableSslVerification() {
187
+ if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0')
188
+ return; // already disabled
189
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
190
+ // Warn prominently: this is process-global and affects all fetch calls.
191
+ console.warn('[openlore] WARNING: TLS certificate verification is DISABLED for this process.' +
192
+ ' All HTTPS connections (including LLM API calls) are vulnerable to MITM attacks.' +
193
+ ' Only use --insecure on trusted private networks with self-signed certificates.');
194
+ }
195
+ /**
196
+ * Validate and normalise an API base URL.
197
+ * Returns the cleaned URL or throws on invalid input.
198
+ */
199
+ function normalizeApiBase(url) {
200
+ // Must be a valid, absolute URL
201
+ let parsed;
202
+ try {
203
+ parsed = new URL(url);
204
+ }
205
+ catch {
206
+ throw new Error(`Invalid API base URL: "${url}". Must be a valid URL (e.g., http://localhost:8000/v1).`);
207
+ }
208
+ // Only allow http and https schemes
209
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
210
+ throw new Error(`Unsupported protocol in API base URL: "${parsed.protocol}". Only http and https are allowed.`);
211
+ }
212
+ // Strip trailing slashes for consistent path joining
213
+ return parsed.toString().replace(/\/+$/, '');
214
+ }
215
+ // ============================================================================
216
+ // RETRY-AFTER PARSING
217
+ // ============================================================================
218
+ /**
219
+ * Parse the number of milliseconds to wait before retrying a 429 response.
220
+ *
221
+ * Checks (in order):
222
+ * 1. Standard `Retry-After` HTTP header (seconds as integer, or HTTP-date)
223
+ * 2. `Limit resets at: YYYY-MM-DD HH:MM:SS UTC` in the response body
224
+ *
225
+ * Returns `undefined` when nothing useful is found so the caller can fall back
226
+ * to its own exponential-backoff delay.
227
+ */
228
+ export function parseRetryAfterMs(body, retryAfterHeader) {
229
+ const BUFFER_MS = 500; // small buffer to avoid hitting the wall again immediately
230
+ // 1. Retry-After header
231
+ if (retryAfterHeader) {
232
+ const seconds = Number(retryAfterHeader);
233
+ if (!isNaN(seconds) && seconds > 0) {
234
+ return Math.ceil(seconds * 1000) + BUFFER_MS;
235
+ }
236
+ // HTTP-date format
237
+ const headerDate = Date.parse(retryAfterHeader);
238
+ if (!isNaN(headerDate)) {
239
+ const ms = headerDate - Date.now();
240
+ if (ms > 0)
241
+ return ms + BUFFER_MS;
242
+ }
243
+ }
244
+ // 2. "Limit resets at: YYYY-MM-DD HH:MM:SS UTC" in body
245
+ const match = body.match(/Limit resets at:\s*(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?\s*UTC)/i);
246
+ if (match) {
247
+ const resetMs = Date.parse(match[1].replace(' UTC', 'Z').replace(' ', 'T'));
248
+ if (!isNaN(resetMs)) {
249
+ const ms = resetMs - Date.now();
250
+ if (ms > 0)
251
+ return ms + BUFFER_MS;
252
+ }
253
+ }
254
+ return undefined;
255
+ }
256
+ // ============================================================================
257
+ // PRICING (per 1M tokens)
258
+ // ============================================================================
259
+ const PRICING = {
260
+ anthropic: {
261
+ // Claude 4 family
262
+ 'claude-opus-4': { input: 15.0, output: 75.0 },
263
+ 'claude-sonnet-4': { input: 3.0, output: 15.0 },
264
+ 'claude-haiku-4': { input: 0.80, output: 4.0 },
265
+ // Claude 3.7 / 3.5
266
+ 'claude-3-7-sonnet': { input: 3.0, output: 15.0 },
267
+ 'claude-3-5-sonnet': { input: 3.0, output: 15.0 },
268
+ 'claude-3-5-haiku': { input: 0.80, output: 4.0 },
269
+ // Claude 3 (legacy)
270
+ 'claude-3-opus': { input: 15.0, output: 75.0 },
271
+ 'claude-3-sonnet': { input: 3.0, output: 15.0 },
272
+ 'claude-3-haiku': { input: 0.25, output: 1.25 },
273
+ // Fallback: assume Sonnet-class pricing
274
+ default: { input: 3.0, output: 15.0 },
275
+ },
276
+ openai: {
277
+ // GPT-4o family
278
+ 'gpt-4o': { input: 2.5, output: 10.0 },
279
+ 'gpt-4o-mini': { input: 0.15, output: 0.6 },
280
+ // o-series reasoning models
281
+ 'o1': { input: 15.0, output: 60.0 },
282
+ 'o1-mini': { input: 3.0, output: 12.0 },
283
+ 'o3': { input: 10.0, output: 40.0 },
284
+ 'o3-mini': { input: 1.1, output: 4.4 },
285
+ 'o4-mini': { input: 1.1, output: 4.4 },
286
+ // Legacy (still in use)
287
+ 'gpt-4-turbo': { input: 10.0, output: 30.0 },
288
+ 'gpt-4': { input: 30.0, output: 60.0 },
289
+ 'gpt-3.5-turbo': { input: 0.5, output: 1.5 },
290
+ default: { input: 2.5, output: 10.0 },
291
+ },
292
+ 'openai-compat': {
293
+ // Mistral
294
+ 'mistral-large-latest': { input: 2.0, output: 6.0 },
295
+ 'mistral-small-latest': { input: 0.1, output: 0.3 },
296
+ 'codestral-latest': { input: 0.2, output: 0.6 },
297
+ // Groq
298
+ 'llama-3.3-70b-versatile': { input: 0.59, output: 0.79 },
299
+ 'llama-3.1-8b-instant': { input: 0.05, output: 0.08 },
300
+ default: { input: 1.0, output: 3.0 },
301
+ },
302
+ gemini: {
303
+ 'gemini-2.0-flash': { input: 0.1, output: 0.4 },
304
+ 'gemini-2.0-flash-lite': { input: 0.075, output: 0.3 },
305
+ 'gemini-2.5-pro': { input: 1.25, output: 10.0 },
306
+ 'gemini-1.5-pro': { input: 1.25, output: 5.0 },
307
+ 'gemini-1.5-flash': { input: 0.075, output: 0.3 },
308
+ default: { input: 0.1, output: 0.4 },
309
+ },
310
+ 'claude-code': {
311
+ // No per-token cost: covered by Claude Max/Pro subscription
312
+ default: { input: 0, output: 0 },
313
+ },
314
+ 'mistral-vibe': {
315
+ // No per-token cost: local CLI tool
316
+ default: { input: 0, output: 0 },
317
+ },
318
+ 'gemini-cli': {
319
+ // No per-token cost: covered by Google account free tier
320
+ default: { input: 0, output: 0 },
321
+ },
322
+ 'cursor-agent': {
323
+ // No per-token cost in openlore: Cursor subscription / CLI auth
324
+ default: { input: 0, output: 0 },
325
+ },
326
+ copilot: {
327
+ // No per-token cost: covered by GitHub Copilot subscription
328
+ default: { input: 0, output: 0 },
329
+ },
330
+ };
331
+ /**
332
+ * Exported for use in pre-flight cost estimation.
333
+ * Look up pricing for a model ID using prefix/family matching.
334
+ * Exact match first, then longest prefix match, then provider default.
335
+ *
336
+ * This is robust to minor version suffixes like "claude-sonnet-4-6-20251120"
337
+ * matching the "claude-sonnet-4" family entry.
338
+ */
339
+ export function lookupPricing(providerName, modelId) {
340
+ const table = PRICING[providerName] ?? PRICING.anthropic;
341
+ // 1. Exact match
342
+ if (table[modelId])
343
+ return table[modelId];
344
+ // 2. Longest prefix match (handles "claude-sonnet-4-6-20251120" → "claude-sonnet-4")
345
+ const modelLower = modelId.toLowerCase();
346
+ let bestKey = '';
347
+ for (const key of Object.keys(table)) {
348
+ if (key === 'default')
349
+ continue;
350
+ if (modelLower.startsWith(key) && key.length > bestKey.length) {
351
+ bestKey = key;
352
+ }
353
+ }
354
+ if (bestKey)
355
+ return table[bestKey];
356
+ // 3. Provider default
357
+ return table.default ?? { input: 3.0, output: 15.0 };
358
+ }
359
+ // ============================================================================
360
+ // TOKEN ESTIMATION
361
+ // ============================================================================
362
+ /**
363
+ * Estimate token count from text (rough approximation)
364
+ * ~4 characters per token for English text
365
+ */
366
+ export function estimateTokens(text) {
367
+ // More accurate estimation considering code
368
+ // Code tends to have more tokens per character due to special chars
369
+ const codePatterns = /[{}()[\];:,.<>/\\|`~!@#$%^&*=+]/g;
370
+ const codeCharCount = (text.match(codePatterns) || []).length;
371
+ const regularCharCount = text.length - codeCharCount;
372
+ // Regular text: ~4 chars per token, code chars: ~2 chars per token
373
+ return Math.ceil(regularCharCount / 4 + codeCharCount / 2);
374
+ }
375
+ // ============================================================================
376
+ // ANTHROPIC PROVIDER
377
+ // ============================================================================
378
+ /**
379
+ * Anthropic Claude provider
380
+ */
381
+ export class AnthropicProvider {
382
+ name = 'anthropic';
383
+ maxContextTokens = 200000;
384
+ maxOutputTokens = 4096;
385
+ apiKey;
386
+ model;
387
+ baseUrl;
388
+ constructor(apiKey, model = DEFAULT_ANTHROPIC_MODEL, baseUrl, sslVerify = true) {
389
+ this.apiKey = apiKey;
390
+ this.model = model;
391
+ this.baseUrl = baseUrl ? normalizeApiBase(baseUrl) : 'https://api.anthropic.com/v1';
392
+ if (!sslVerify)
393
+ disableSslVerification();
394
+ }
395
+ countTokens(text) {
396
+ return estimateTokens(text);
397
+ }
398
+ async generateCompletion(request) {
399
+ const response = await fetch(`${this.baseUrl}/messages`, {
400
+ method: 'POST',
401
+ headers: {
402
+ 'Content-Type': 'application/json',
403
+ 'x-api-key': this.apiKey,
404
+ 'anthropic-version': '2023-06-01',
405
+ },
406
+ body: JSON.stringify({
407
+ model: this.model,
408
+ max_tokens: request.maxTokens ?? this.maxOutputTokens,
409
+ temperature: request.temperature ?? 0.3,
410
+ system: request.systemPrompt,
411
+ messages: [
412
+ { role: 'user', content: request.userPrompt },
413
+ ],
414
+ stop_sequences: request.stopSequences,
415
+ }),
416
+ });
417
+ if (!response.ok) {
418
+ const error = await response.text();
419
+ const errorObj = this.parseError(error, response.status, response.headers.get('retry-after'));
420
+ throw errorObj;
421
+ }
422
+ const data = await response.json();
423
+ const content = data.content
424
+ .filter(c => c.type === 'text')
425
+ .map(c => c.text)
426
+ .join('');
427
+ return {
428
+ content,
429
+ usage: {
430
+ inputTokens: data.usage.input_tokens,
431
+ outputTokens: data.usage.output_tokens,
432
+ totalTokens: data.usage.input_tokens + data.usage.output_tokens,
433
+ },
434
+ model: data.model,
435
+ finishReason: data.stop_reason === 'end_turn' ? 'stop' : data.stop_reason === 'max_tokens' ? 'length' : 'error',
436
+ };
437
+ }
438
+ parseError(error, status, retryAfterHeader) {
439
+ const detail = error.trim() || '(empty response body)';
440
+ const err = new Error(`HTTP ${status}: ${detail}`);
441
+ err.status = status;
442
+ err.retryable = status === 429 || status >= 500;
443
+ if (status === 429) {
444
+ err.retryAfterMs = parseRetryAfterMs(error, retryAfterHeader);
445
+ }
446
+ return err;
447
+ }
448
+ }
449
+ // ============================================================================
450
+ // OPENAI PROVIDER
451
+ // ============================================================================
452
+ /**
453
+ * Wrap a top-level array schema in an object so it satisfies OpenAI's
454
+ * structured-output requirement that the root type is "object".
455
+ * The existing unwrap logic in completeJSON (single-key object → array)
456
+ * reverses this transparently. See: #52
457
+ */
458
+ function wrapArraySchema(schema) {
459
+ if (schema.type === 'array') {
460
+ return {
461
+ type: 'object',
462
+ properties: { items: schema },
463
+ required: ['items'],
464
+ };
465
+ }
466
+ return schema;
467
+ }
468
+ function isSchemaRecord(value) {
469
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
470
+ }
471
+ function schemaAllowsNull(schema) {
472
+ if (!isSchemaRecord(schema))
473
+ return false;
474
+ const type = schema.type;
475
+ if (type === 'null')
476
+ return true;
477
+ if (Array.isArray(type) && type.includes('null'))
478
+ return true;
479
+ for (const key of ['anyOf', 'oneOf']) {
480
+ const schemas = schema[key];
481
+ if (Array.isArray(schemas) && schemas.some(schemaAllowsNull)) {
482
+ return true;
483
+ }
484
+ }
485
+ return false;
486
+ }
487
+ function makeSchemaNullable(schema) {
488
+ if (schemaAllowsNull(schema))
489
+ return schema;
490
+ if (isSchemaRecord(schema)) {
491
+ const type = schema.type;
492
+ if (typeof type === 'string') {
493
+ schema.type = [type, 'null'];
494
+ return schema;
495
+ }
496
+ }
497
+ return { anyOf: [schema, { type: 'null' }] };
498
+ }
499
+ function normalizeOpenAISchemaNode(node) {
500
+ if (Array.isArray(node)) {
501
+ for (const item of node) {
502
+ normalizeOpenAISchemaNode(item);
503
+ }
504
+ return;
505
+ }
506
+ if (!isSchemaRecord(node))
507
+ return;
508
+ const isObjectSchema = node.type === 'object' || (Array.isArray(node.type) && node.type.includes('object'));
509
+ if (isObjectSchema) {
510
+ const properties = isSchemaRecord(node.properties) ? node.properties : {};
511
+ const originalRequired = new Set(Array.isArray(node.required)
512
+ ? node.required.filter((field) => typeof field === 'string')
513
+ : []);
514
+ node.properties = properties;
515
+ node.additionalProperties = false;
516
+ node.required = Object.keys(properties);
517
+ for (const [key, propertySchema] of Object.entries(properties)) {
518
+ normalizeOpenAISchemaNode(propertySchema);
519
+ if (!originalRequired.has(key)) {
520
+ properties[key] = makeSchemaNullable(properties[key]);
521
+ }
522
+ }
523
+ }
524
+ for (const [key, value] of Object.entries(node)) {
525
+ if (isObjectSchema && key === 'properties')
526
+ continue;
527
+ normalizeOpenAISchemaNode(value);
528
+ }
529
+ }
530
+ function normalizeOpenAIResponseSchema(schema) {
531
+ const clonedSchema = JSON.parse(JSON.stringify(schema));
532
+ const wrappedSchema = wrapArraySchema(clonedSchema);
533
+ normalizeOpenAISchemaNode(wrappedSchema);
534
+ return wrappedSchema;
535
+ }
536
+ /**
537
+ * OpenAI provider
538
+ */
539
+ export class OpenAIProvider {
540
+ name = 'openai';
541
+ maxContextTokens = 128000;
542
+ maxOutputTokens = 4096;
543
+ apiKey;
544
+ model;
545
+ baseUrl;
546
+ constructor(apiKey, model = DEFAULT_OPENAI_MODEL, baseUrl, sslVerify = true) {
547
+ this.apiKey = apiKey;
548
+ this.model = model;
549
+ this.baseUrl = baseUrl ? normalizeApiBase(baseUrl) : 'https://api.openai.com/v1';
550
+ if (!sslVerify)
551
+ disableSslVerification();
552
+ }
553
+ countTokens(text) {
554
+ return estimateTokens(text);
555
+ }
556
+ async generateCompletion(request) {
557
+ const messages = [
558
+ { role: 'system', content: request.systemPrompt },
559
+ { role: 'user', content: request.userPrompt },
560
+ ];
561
+ const body = {
562
+ model: this.model,
563
+ messages,
564
+ max_tokens: request.maxTokens ?? this.maxOutputTokens,
565
+ temperature: request.temperature ?? 0.3,
566
+ stop: request.stopSequences,
567
+ };
568
+ if (request.responseFormat === 'json' && request.jsonSchema) {
569
+ // Use OpenAI structured outputs when a JSON schema is provided.
570
+ // This forces the model to conform to the schema (e.g. start an array). (#26)
571
+ // Wrap top-level array schemas in an object to satisfy OpenAI's requirement. (#52)
572
+ body.response_format = {
573
+ type: 'json_schema',
574
+ json_schema: {
575
+ name: 'response',
576
+ schema: normalizeOpenAIResponseSchema(request.jsonSchema),
577
+ },
578
+ };
579
+ }
580
+ else if (request.responseFormat === 'json') {
581
+ body.response_format = { type: 'json_object' };
582
+ }
583
+ const response = await fetch(`${this.baseUrl}/chat/completions`, {
584
+ method: 'POST',
585
+ headers: {
586
+ 'Content-Type': 'application/json',
587
+ 'Authorization': `Bearer ${this.apiKey}`,
588
+ },
589
+ body: JSON.stringify(body),
590
+ });
591
+ if (!response.ok) {
592
+ const error = await response.text();
593
+ const errorObj = this.parseError(error, response.status, response.headers.get('retry-after'));
594
+ throw errorObj;
595
+ }
596
+ const data = await response.json();
597
+ return {
598
+ content: data.choices[0]?.message?.content ?? '',
599
+ usage: {
600
+ inputTokens: data.usage.prompt_tokens,
601
+ outputTokens: data.usage.completion_tokens,
602
+ totalTokens: data.usage.total_tokens,
603
+ },
604
+ model: data.model,
605
+ finishReason: data.choices[0]?.finish_reason === 'stop' ? 'stop' : data.choices[0]?.finish_reason === 'length' ? 'length' : 'error',
606
+ };
607
+ }
608
+ parseError(error, status, retryAfterHeader) {
609
+ const detail = error.trim() || '(empty response body)';
610
+ const err = new Error(`HTTP ${status}: ${detail}`);
611
+ err.status = status;
612
+ err.retryable = status === 429 || status >= 500;
613
+ if (status === 429) {
614
+ err.retryAfterMs = parseRetryAfterMs(error, retryAfterHeader);
615
+ }
616
+ return err;
617
+ }
618
+ }
619
+ export class OpenAICompatibleProvider {
620
+ name = 'openai-compat';
621
+ maxContextTokens = 128000;
622
+ maxOutputTokens = 4096;
623
+ apiKey;
624
+ model;
625
+ baseUrl;
626
+ disableResponseFormat;
627
+ constructor(apiKey, baseUrl, model = DEFAULT_OPENAI_COMPAT_MODEL, disableResponseFormat = false) {
628
+ this.apiKey = apiKey;
629
+ this.baseUrl = normalizeApiBase(baseUrl);
630
+ this.model = model;
631
+ this.disableResponseFormat = disableResponseFormat;
632
+ }
633
+ countTokens(text) {
634
+ return estimateTokens(text);
635
+ }
636
+ /**
637
+ * Fetch available models from the API endpoint
638
+ */
639
+ async fetchAvailableModels() {
640
+ try {
641
+ const response = await fetch(`${this.baseUrl}/models`, {
642
+ method: 'GET',
643
+ headers: {
644
+ 'Authorization': `Bearer ${this.apiKey}`,
645
+ 'Content-Type': 'application/json',
646
+ },
647
+ });
648
+ if (!response.ok) {
649
+ return [];
650
+ }
651
+ const data = await response.json();
652
+ return data.data?.map(model => model.id).sort() ?? [];
653
+ }
654
+ catch {
655
+ return [];
656
+ }
657
+ }
658
+ /**
659
+ * Get known models for common API endpoints when /models is not available
660
+ */
661
+ getKnownModelsForEndpoint() {
662
+ const url = this.baseUrl.toLowerCase();
663
+ if (url.includes('codestral.mistral.ai')) {
664
+ return ['codestral-2508', 'codestral-latest'];
665
+ }
666
+ if (url.includes('api.mistral.ai')) {
667
+ return [
668
+ 'mistral-large-3-25-12',
669
+ 'mistral-medium-3-1-25-08',
670
+ 'mistral-small-4-0-26-03',
671
+ 'mistral-nemo-12b-24-07',
672
+ 'codestral-2508',
673
+ 'devstral-2-25-12'
674
+ ];
675
+ }
676
+ if (url.includes('api.openai.com')) {
677
+ return [
678
+ 'gpt-4o',
679
+ 'gpt-4o-mini',
680
+ 'gpt-4-turbo',
681
+ 'gpt-4',
682
+ 'gpt-3.5-turbo'
683
+ ];
684
+ }
685
+ if (url.includes('api.groq.com')) {
686
+ return [
687
+ 'llama-3.1-70b-versatile',
688
+ 'llama-3.1-8b-instant',
689
+ 'mixtral-8x7b-32768'
690
+ ];
691
+ }
692
+ // For unknown endpoints, return empty array
693
+ return [];
694
+ }
695
+ async generateCompletion(request) {
696
+ const body = {
697
+ model: this.model,
698
+ messages: [
699
+ { role: 'system', content: request.systemPrompt },
700
+ { role: 'user', content: request.userPrompt },
701
+ ],
702
+ max_tokens: request.maxTokens ?? this.maxOutputTokens,
703
+ temperature: request.temperature ?? 0.3,
704
+ stream: true,
705
+ stream_options: { include_usage: true },
706
+ ...(request.stopSequences && { stop: request.stopSequences }),
707
+ };
708
+ if (!this.disableResponseFormat) {
709
+ if (request.responseFormat === 'json' && request.jsonSchema) {
710
+ body.response_format = {
711
+ type: 'json_schema',
712
+ json_schema: {
713
+ name: 'response',
714
+ schema: normalizeOpenAIResponseSchema(request.jsonSchema),
715
+ },
716
+ };
717
+ }
718
+ else if (request.responseFormat === 'json') {
719
+ body.response_format = { type: 'json_object' };
720
+ }
721
+ }
722
+ const response = await fetch(`${this.baseUrl}/chat/completions`, {
723
+ method: 'POST',
724
+ headers: {
725
+ 'Content-Type': 'application/json',
726
+ 'Authorization': `Bearer ${this.apiKey}`,
727
+ },
728
+ body: JSON.stringify(body),
729
+ });
730
+ if (!response.ok) {
731
+ const error = await response.text();
732
+ const detail = error.trim() || '(empty response body)';
733
+ const err = new Error(`HTTP ${response.status}: ${detail}`);
734
+ err.status = response.status;
735
+ err.retryable = response.status === 429 || response.status >= 500;
736
+ if (response.status === 429) {
737
+ err.retryAfterMs = parseRetryAfterMs(error, response.headers.get('retry-after'));
738
+ }
739
+ throw err;
740
+ }
741
+ let content = '';
742
+ let usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
743
+ let finishReason = 'stop';
744
+ let model = this.model;
745
+ const reader = response.body.getReader();
746
+ const decoder = new TextDecoder();
747
+ let buffer = '';
748
+ outer: while (true) {
749
+ const { done, value } = await reader.read();
750
+ if (done)
751
+ break;
752
+ buffer += decoder.decode(value, { stream: true });
753
+ const lines = buffer.split('\n');
754
+ buffer = lines.pop() ?? '';
755
+ for (const line of lines) {
756
+ if (!line.startsWith('data: '))
757
+ continue;
758
+ const data = line.slice(6).trim();
759
+ if (data === '[DONE]')
760
+ break outer;
761
+ try {
762
+ const chunk = JSON.parse(data);
763
+ const delta = chunk.choices?.[0]?.delta?.content;
764
+ if (delta)
765
+ content += delta;
766
+ const fr = chunk.choices?.[0]?.finish_reason;
767
+ if (fr)
768
+ finishReason = fr === 'length' ? 'length' : 'stop';
769
+ if (chunk.model)
770
+ model = chunk.model;
771
+ if (chunk.usage) {
772
+ usage = {
773
+ inputTokens: chunk.usage.prompt_tokens,
774
+ outputTokens: chunk.usage.completion_tokens,
775
+ totalTokens: chunk.usage.total_tokens,
776
+ };
777
+ }
778
+ }
779
+ catch { /* ignore malformed SSE chunks */ }
780
+ }
781
+ }
782
+ return { content, usage, model, finishReason };
783
+ }
784
+ }
785
+ // ============================================================================
786
+ // COPILOT PROVIDER (via copilot-api proxy — OpenAI-compatible)
787
+ // ============================================================================
788
+ /**
789
+ * GitHub Copilot provider via copilot-api proxy.
790
+ * Requires a running copilot-api proxy (https://github.com/ericc-ch/copilot-api)
791
+ * which exposes an OpenAI-compatible /v1/chat/completions endpoint.
792
+ *
793
+ * Required env vars:
794
+ * COPILOT_API_BASE_URL — Base URL of the copilot-api proxy (default: http://localhost:4141/v1)
795
+ *
796
+ * Optional env vars:
797
+ * COPILOT_API_KEY — API key if the proxy requires auth (default: "copilot")
798
+ */
799
+ export class CopilotProvider {
800
+ name = 'copilot';
801
+ maxContextTokens = 128000;
802
+ maxOutputTokens = 4096;
803
+ apiKey;
804
+ model;
805
+ baseUrl;
806
+ constructor(baseUrl, model = DEFAULT_COPILOT_MODEL, apiKey = 'copilot') {
807
+ this.apiKey = apiKey;
808
+ this.baseUrl = normalizeApiBase(baseUrl);
809
+ this.model = model;
810
+ }
811
+ countTokens(text) {
812
+ return estimateTokens(text);
813
+ }
814
+ async generateCompletion(request) {
815
+ const body = {
816
+ model: this.model,
817
+ messages: [
818
+ { role: 'system', content: request.systemPrompt },
819
+ { role: 'user', content: request.userPrompt },
820
+ ],
821
+ max_tokens: request.maxTokens ?? this.maxOutputTokens,
822
+ temperature: request.temperature ?? 0.3,
823
+ ...(request.stopSequences && { stop: request.stopSequences }),
824
+ };
825
+ if (request.responseFormat === 'json' && request.jsonSchema) {
826
+ body.response_format = {
827
+ type: 'json_schema',
828
+ json_schema: {
829
+ name: 'response',
830
+ schema: wrapArraySchema(request.jsonSchema),
831
+ },
832
+ };
833
+ }
834
+ else if (request.responseFormat === 'json') {
835
+ body.response_format = { type: 'json_object' };
836
+ }
837
+ const response = await fetch(`${this.baseUrl}/chat/completions`, {
838
+ method: 'POST',
839
+ headers: {
840
+ 'Content-Type': 'application/json',
841
+ 'Authorization': `Bearer ${this.apiKey}`,
842
+ },
843
+ body: JSON.stringify(body),
844
+ });
845
+ if (!response.ok) {
846
+ const error = await response.text();
847
+ const detail = error.trim() || '(empty response body)';
848
+ const err = new Error(`HTTP ${response.status}: ${detail}`);
849
+ err.status = response.status;
850
+ err.retryable = response.status === 429 || response.status >= 500;
851
+ if (response.status === 429) {
852
+ err.retryAfterMs = parseRetryAfterMs(error, response.headers.get('retry-after'));
853
+ }
854
+ throw err;
855
+ }
856
+ const data = await response.json();
857
+ return {
858
+ content: data.choices[0]?.message?.content ?? '',
859
+ usage: {
860
+ inputTokens: data.usage.prompt_tokens,
861
+ outputTokens: data.usage.completion_tokens,
862
+ totalTokens: data.usage.total_tokens,
863
+ },
864
+ model: data.model ?? this.model,
865
+ finishReason: data.choices[0]?.finish_reason === 'stop' ? 'stop' : data.choices[0]?.finish_reason === 'length' ? 'length' : 'error',
866
+ };
867
+ }
868
+ }
869
+ // ============================================================================
870
+ // GEMINI CLI PROVIDER (uses local `gemini` CLI, no API key required)
871
+ // ============================================================================
872
+ /**
873
+ * Gemini CLI provider
874
+ *
875
+ * Routes LLM calls through the local `gemini` CLI binary in non-interactive
876
+ * mode (`gemini -p ...`). Authentication is handled by the Google account
877
+ * session — no GEMINI_API_KEY is required.
878
+ * If the binary is not on PATH, set GEMINI_CLI to its full path.
879
+ */
880
+ export class GeminiCLIProvider {
881
+ name = 'gemini-cli';
882
+ maxContextTokens = 1_000_000;
883
+ maxOutputTokens = 8_192;
884
+ model;
885
+ constructor(model) {
886
+ this.model = model && model !== 'gemini-cli' ? model : undefined;
887
+ }
888
+ async generateCompletion(request) {
889
+ const { execFileSync } = await import('child_process');
890
+ const fullPrompt = request.systemPrompt
891
+ ? `${request.systemPrompt}\n\n---\n\n${request.userPrompt}`
892
+ : request.userPrompt;
893
+ // gemini CLI: -p for prompt, --output-format json, -m for model
894
+ const args = ['-p', fullPrompt, '--output-format', 'json'];
895
+ if (this.model)
896
+ args.push('-m', this.model);
897
+ const geminiCLIBin = process.env.GEMINI_CLI ?? 'gemini';
898
+ let raw;
899
+ try {
900
+ raw = execFileSync(geminiCLIBin, args, {
901
+ encoding: 'utf8',
902
+ maxBuffer: 50 * 1024 * 1024,
903
+ timeout: 300_000,
904
+ });
905
+ }
906
+ catch (err) {
907
+ const e = err;
908
+ const detail = e.stderr ?? e.stdout ?? e.message ?? String(err);
909
+ throw Object.assign(new Error(`gemini CLI failed: ${detail}`), { retryable: false });
910
+ }
911
+ // Format: {response: string, stats: {models: {[name]: {tokens: {input, candidates, total}}}}}
912
+ let content = '';
913
+ let inputTokens;
914
+ let outputTokens;
915
+ let modelUsed = this.model ?? 'gemini-cli';
916
+ try {
917
+ const parsed = JSON.parse(raw);
918
+ content = parsed.response ?? '';
919
+ if (parsed.stats?.models) {
920
+ const models = Object.entries(parsed.stats.models);
921
+ if (models.length > 0) {
922
+ modelUsed = models[0][0];
923
+ // Sum tokens across all models used (gemini-cli may use multiple internally)
924
+ inputTokens = models.reduce((sum, [, m]) => sum + (m.tokens?.input ?? 0), 0);
925
+ outputTokens = models.reduce((sum, [, m]) => sum + (m.tokens?.candidates ?? 0), 0);
926
+ }
927
+ }
928
+ }
929
+ catch {
930
+ content = raw.trim();
931
+ }
932
+ if (!content)
933
+ content = raw.trim();
934
+ inputTokens ??= estimateTokens(fullPrompt);
935
+ outputTokens ??= estimateTokens(content);
936
+ return {
937
+ content,
938
+ usage: { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens },
939
+ model: modelUsed,
940
+ finishReason: 'stop',
941
+ };
942
+ }
943
+ countTokens(text) {
944
+ return estimateTokens(text);
945
+ }
946
+ }
947
+ // ============================================================================
948
+ // CURSOR AGENT CLI PROVIDER (uses local `cursor-agent` CLI, no cloud API key)
949
+ // ============================================================================
950
+ /**
951
+ * Cursor Agent CLI provider
952
+ *
953
+ * Routes LLM calls through the Cursor Agent CLI in print mode (`-p`, JSON output).
954
+ * Authentication is handled by Cursor (see Cursor CLI headless documentation) —
955
+ * e.g. `cursor auth login` or `CURSOR_API_KEY` — not ANTHROPIC_API_KEY / OPENAI_API_KEY.
956
+ * If the binary is not on PATH, set `CURSOR_AGENT_CLI` to its full path.
957
+ */
958
+ export class CursorAgentProvider {
959
+ name = 'cursor-agent';
960
+ maxContextTokens = 1_000_000;
961
+ maxOutputTokens = 8192;
962
+ model;
963
+ constructor(model) {
964
+ this.model = model && model !== 'cursor-agent' ? model : undefined;
965
+ }
966
+ async generateCompletion(request) {
967
+ const { execFileSync } = await import('child_process');
968
+ const fullPrompt = request.systemPrompt
969
+ ? `${request.systemPrompt}\n\n---\n\n${request.userPrompt}`
970
+ : request.userPrompt;
971
+ const args = ['-p', fullPrompt, '--output-format', 'json'];
972
+ if (this.model)
973
+ args.push('--model', this.model);
974
+ const bin = process.env.CURSOR_AGENT_CLI ?? 'cursor-agent';
975
+ let raw;
976
+ try {
977
+ raw = execFileSync(bin, args, {
978
+ encoding: 'utf8',
979
+ maxBuffer: LLM_CLI_MAX_BUFFER_BYTES,
980
+ timeout: LLM_CLI_TIMEOUT_MS,
981
+ });
982
+ }
983
+ catch (err) {
984
+ const e = err;
985
+ const detail = e.stderr ?? e.stdout ?? e.message ?? String(err);
986
+ throw Object.assign(new Error(`cursor-agent CLI failed: ${detail}`), { retryable: false });
987
+ }
988
+ let content = '';
989
+ let inputTokens;
990
+ let outputTokens;
991
+ try {
992
+ const parsed = JSON.parse(raw);
993
+ if (parsed.is_error === true && typeof parsed.result === 'string') {
994
+ throw Object.assign(new Error(`cursor-agent CLI error: ${parsed.result}`), { retryable: false });
995
+ }
996
+ if (typeof parsed.result === 'string') {
997
+ content = parsed.result;
998
+ }
999
+ else if (typeof parsed.response === 'string') {
1000
+ content = parsed.response;
1001
+ }
1002
+ else {
1003
+ content = String(parsed.message ?? parsed.text ?? parsed.content ?? '');
1004
+ }
1005
+ const u = parsed.usage;
1006
+ if (u) {
1007
+ inputTokens = (u.input_tokens ?? u.inputTokens);
1008
+ outputTokens = (u.output_tokens ?? u.outputTokens);
1009
+ }
1010
+ }
1011
+ catch (err) {
1012
+ if (err instanceof Error && /cursor-agent CLI error:/.test(err.message))
1013
+ throw err;
1014
+ content = raw.trim();
1015
+ }
1016
+ if (!content)
1017
+ content = raw.trim();
1018
+ inputTokens ??= estimateTokens(fullPrompt);
1019
+ outputTokens ??= estimateTokens(content);
1020
+ return {
1021
+ content,
1022
+ usage: { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens },
1023
+ model: this.model ?? 'cursor-agent',
1024
+ finishReason: 'stop',
1025
+ };
1026
+ }
1027
+ countTokens(text) {
1028
+ return estimateTokens(text);
1029
+ }
1030
+ }
1031
+ // ============================================================================
1032
+ // GEMINI PROVIDER
1033
+ // ============================================================================
1034
+ /**
1035
+ * Google Gemini provider
1036
+ */
1037
+ export class GeminiProvider {
1038
+ name = 'gemini';
1039
+ maxContextTokens = 1000000;
1040
+ maxOutputTokens = 8192;
1041
+ apiKey;
1042
+ model;
1043
+ baseUrl = 'https://generativelanguage.googleapis.com/v1beta/models';
1044
+ constructor(apiKey, model = DEFAULT_GEMINI_MODEL) {
1045
+ this.apiKey = apiKey;
1046
+ this.model = model;
1047
+ }
1048
+ countTokens(text) {
1049
+ return estimateTokens(text);
1050
+ }
1051
+ async generateCompletion(request) {
1052
+ const body = {
1053
+ contents: [
1054
+ { role: 'user', parts: [{ text: request.userPrompt }] },
1055
+ ],
1056
+ systemInstruction: {
1057
+ parts: [{ text: request.systemPrompt }],
1058
+ },
1059
+ generationConfig: {
1060
+ temperature: request.temperature ?? 0.3,
1061
+ maxOutputTokens: request.maxTokens ?? this.maxOutputTokens,
1062
+ ...(request.responseFormat === 'json' && { responseMimeType: 'application/json' }),
1063
+ ...(request.responseFormat === 'json' && request.jsonSchema && { responseSchema: request.jsonSchema }),
1064
+ ...(request.stopSequences && { stopSequences: request.stopSequences }),
1065
+ },
1066
+ };
1067
+ const url = `${this.baseUrl}/${this.model}:generateContent?key=${this.apiKey}`;
1068
+ const response = await fetch(url, {
1069
+ method: 'POST',
1070
+ headers: { 'Content-Type': 'application/json' },
1071
+ body: JSON.stringify(body),
1072
+ });
1073
+ if (!response.ok) {
1074
+ const error = await response.text();
1075
+ const detail = error.trim() || '(empty response body)';
1076
+ const err = new Error(`HTTP ${response.status}: ${detail}`);
1077
+ err.status = response.status;
1078
+ err.retryable = response.status === 429 || response.status >= 500;
1079
+ if (response.status === 429) {
1080
+ err.retryAfterMs = parseRetryAfterMs(error, response.headers.get('retry-after'));
1081
+ }
1082
+ throw err;
1083
+ }
1084
+ const data = await response.json();
1085
+ const content = data.candidates[0]?.content?.parts?.map(p => p.text).join('') ?? '';
1086
+ const finishReason = data.candidates[0]?.finishReason;
1087
+ return {
1088
+ content,
1089
+ usage: {
1090
+ inputTokens: data.usageMetadata.promptTokenCount,
1091
+ outputTokens: data.usageMetadata.candidatesTokenCount,
1092
+ totalTokens: data.usageMetadata.totalTokenCount,
1093
+ },
1094
+ model: this.model,
1095
+ finishReason: finishReason === 'STOP' ? 'stop' : finishReason === 'MAX_TOKENS' ? 'length' : 'error',
1096
+ };
1097
+ }
1098
+ }
1099
+ // ============================================================================
1100
+ // MOCK PROVIDER (for testing)
1101
+ // ============================================================================
1102
+ /**
1103
+ * Mock provider for testing
1104
+ */
1105
+ export class MockLLMProvider {
1106
+ name = 'mock';
1107
+ maxContextTokens = 100000;
1108
+ maxOutputTokens = 4096;
1109
+ responses = new Map();
1110
+ defaultResponse = '{"result": "mock response"}';
1111
+ callHistory = [];
1112
+ shouldFail = false;
1113
+ failCount = 0;
1114
+ currentFailCount = 0;
1115
+ setResponse(promptContains, response) {
1116
+ this.responses.set(promptContains, response);
1117
+ }
1118
+ setDefaultResponse(response) {
1119
+ this.defaultResponse = response;
1120
+ }
1121
+ countTokens(text) {
1122
+ return estimateTokens(text);
1123
+ }
1124
+ async generateCompletion(request) {
1125
+ this.callHistory.push(request);
1126
+ if (this.shouldFail && this.currentFailCount < this.failCount) {
1127
+ this.currentFailCount++;
1128
+ const err = new Error('Mock failure');
1129
+ err.status = 500;
1130
+ err.retryable = true;
1131
+ throw err;
1132
+ }
1133
+ // Find matching response
1134
+ let content = this.defaultResponse;
1135
+ for (const [key, value] of this.responses) {
1136
+ if (request.userPrompt.includes(key) || request.systemPrompt.includes(key)) {
1137
+ content = value;
1138
+ break;
1139
+ }
1140
+ }
1141
+ const inputTokens = this.countTokens(request.systemPrompt + request.userPrompt);
1142
+ const outputTokens = this.countTokens(content);
1143
+ return {
1144
+ content,
1145
+ usage: {
1146
+ inputTokens,
1147
+ outputTokens,
1148
+ totalTokens: inputTokens + outputTokens,
1149
+ },
1150
+ model: 'mock-model',
1151
+ finishReason: 'stop',
1152
+ };
1153
+ }
1154
+ reset() {
1155
+ this.callHistory = [];
1156
+ this.shouldFail = false;
1157
+ this.failCount = 0;
1158
+ this.currentFailCount = 0;
1159
+ this.responses.clear();
1160
+ }
1161
+ }
1162
+ // ============================================================================
1163
+ // LLM SERVICE
1164
+ // ============================================================================
1165
+ /**
1166
+ * LLM Service - main interface for LLM interactions
1167
+ */
1168
+ export class LLMService {
1169
+ provider;
1170
+ retryConfig;
1171
+ options;
1172
+ tokenUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0, requests: 0 };
1173
+ costTracking = { estimatedCost: 0, currency: 'USD', byProvider: {} };
1174
+ requestLog = [];
1175
+ constructor(provider, options = {}) {
1176
+ this.provider = provider;
1177
+ this.options = {
1178
+ provider: options.provider ?? 'anthropic',
1179
+ model: options.model ?? '',
1180
+ apiBase: options.apiBase ?? '',
1181
+ sslVerify: options.sslVerify ?? true,
1182
+ openaiCompatBaseUrl: options.openaiCompatBaseUrl ?? '',
1183
+ maxRetries: options.maxRetries ?? DEFAULT_LLM_MAX_RETRIES,
1184
+ initialDelay: options.initialDelay ?? DEFAULT_LLM_INITIAL_DELAY_MS,
1185
+ maxDelay: options.maxDelay ?? DEFAULT_LLM_MAX_DELAY_MS,
1186
+ timeout: options.timeout ?? DEFAULT_LLM_TIMEOUT_MS,
1187
+ costWarningThreshold: options.costWarningThreshold ?? DEFAULT_LLM_COST_WARNING_THRESHOLD,
1188
+ logDir: options.logDir ?? `${OPENLORE_DIR}/${OPENLORE_LOGS_SUBDIR}`,
1189
+ enableLogging: options.enableLogging ?? false,
1190
+ disableResponseFormat: options.disableResponseFormat ?? false,
1191
+ };
1192
+ this.retryConfig = {
1193
+ maxRetries: this.options.maxRetries,
1194
+ initialDelay: this.options.initialDelay,
1195
+ maxDelay: this.options.maxDelay,
1196
+ timeout: this.options.timeout,
1197
+ };
1198
+ }
1199
+ /**
1200
+ * Get the provider name
1201
+ */
1202
+ getProviderName() {
1203
+ return this.provider.name;
1204
+ }
1205
+ /**
1206
+ * Get maximum context tokens for the provider
1207
+ */
1208
+ getMaxContextTokens() {
1209
+ return this.provider.maxContextTokens;
1210
+ }
1211
+ /**
1212
+ * Count tokens in text
1213
+ */
1214
+ countTokens(text) {
1215
+ return this.provider.countTokens(text);
1216
+ }
1217
+ /**
1218
+ * Get current token usage
1219
+ */
1220
+ getTokenUsage() {
1221
+ return { ...this.tokenUsage };
1222
+ }
1223
+ /**
1224
+ * Get current cost tracking
1225
+ */
1226
+ getCostTracking() {
1227
+ return { ...this.costTracking };
1228
+ }
1229
+ /**
1230
+ * Reset usage tracking
1231
+ */
1232
+ resetTracking() {
1233
+ this.tokenUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0, requests: 0 };
1234
+ this.costTracking = { estimatedCost: 0, currency: 'USD', byProvider: {} };
1235
+ this.requestLog = [];
1236
+ }
1237
+ /**
1238
+ * Generate a completion with retry logic
1239
+ */
1240
+ async complete(request) {
1241
+ // Pre-calculate tokens and warn if approaching limit
1242
+ const inputTokens = this.countTokens(request.systemPrompt + request.userPrompt);
1243
+ const maxTokens = request.maxTokens ?? this.provider.maxOutputTokens;
1244
+ const totalExpected = inputTokens + maxTokens;
1245
+ if (totalExpected > this.provider.maxContextTokens * CONTEXT_LIMIT_WARNING_RATIO) {
1246
+ logger.warning(`Approaching context limit: ${totalExpected} tokens (max: ${this.provider.maxContextTokens})`);
1247
+ }
1248
+ if (totalExpected > this.provider.maxContextTokens) {
1249
+ throw new Error(`Request exceeds context limit: ${totalExpected} > ${this.provider.maxContextTokens}`);
1250
+ }
1251
+ // Execute with retry logic
1252
+ let lastError = null;
1253
+ let delay = this.retryConfig.initialDelay;
1254
+ for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
1255
+ try {
1256
+ logger.debug(`LLM request attempt ${attempt + 1}/${this.retryConfig.maxRetries + 1}`);
1257
+ const response = await this.executeWithTimeout(request);
1258
+ // Update tracking
1259
+ this.updateTracking(response);
1260
+ // Log if enabled
1261
+ if (this.options.enableLogging) {
1262
+ this.logRequest(request, response);
1263
+ }
1264
+ // Check cost threshold
1265
+ if (this.costTracking.estimatedCost > this.options.costWarningThreshold) {
1266
+ logger.warning(`Cost threshold exceeded: $${this.costTracking.estimatedCost.toFixed(4)} > $${this.options.costWarningThreshold}`);
1267
+ }
1268
+ return response;
1269
+ }
1270
+ catch (error) {
1271
+ lastError = error;
1272
+ const errWithStatus = error;
1273
+ // Log error
1274
+ if (this.options.enableLogging) {
1275
+ this.logRequest(request, undefined, lastError.message);
1276
+ }
1277
+ // Check if retryable
1278
+ if (!errWithStatus.retryable || attempt === this.retryConfig.maxRetries) {
1279
+ throw lastError;
1280
+ }
1281
+ // Use the provider-supplied reset time if available, otherwise exponential backoff
1282
+ const retryAfterMs = errWithStatus.retryAfterMs;
1283
+ const waitMs = retryAfterMs !== undefined ? retryAfterMs : delay;
1284
+ logger.warning(`LLM request failed (attempt ${attempt + 1}), retrying in ${waitMs}ms: ${lastError.message}`);
1285
+ await this.sleep(waitMs);
1286
+ // Only advance the backoff delay when we didn't use a provider-supplied wait
1287
+ if (retryAfterMs === undefined) {
1288
+ delay = Math.min(delay * 2, this.retryConfig.maxDelay);
1289
+ }
1290
+ }
1291
+ }
1292
+ throw lastError ?? new Error('Unknown error');
1293
+ }
1294
+ /**
1295
+ * Generate a completion expecting JSON response
1296
+ */
1297
+ async completeJSON(request, schema) {
1298
+ const jsonRequest = { ...request, responseFormat: 'json', jsonSchema: schema };
1299
+ // Add JSON instruction to prompt if not already present
1300
+ if (!jsonRequest.systemPrompt.toLowerCase().includes('json')) {
1301
+ jsonRequest.systemPrompt += '\n\nRespond with valid JSON only.';
1302
+ }
1303
+ // When a schema is provided, append it to the system prompt so the model
1304
+ // knows the exact shape expected (especially that it must start an array).
1305
+ // This addresses issue #26: without this, models may return a single object.
1306
+ if (schema) {
1307
+ jsonRequest.systemPrompt += `\n\nYour response MUST conform to this JSON Schema:\n${JSON.stringify(schema)}`;
1308
+ }
1309
+ const response = await this.complete(jsonRequest);
1310
+ let content = response.content;
1311
+ // Extract JSON from markdown code blocks if present
1312
+ const jsonMatch = content.match(/```(?:json)?\s*([\s\S]*?)```/);
1313
+ if (jsonMatch) {
1314
+ content = jsonMatch[1].trim();
1315
+ }
1316
+ // Parse JSON
1317
+ let parsed;
1318
+ try {
1319
+ parsed = JSON.parse(content);
1320
+ }
1321
+ catch (parseError) {
1322
+ // Retry with correction prompt for parse errors
1323
+ logger.warning('JSON parse failed, attempting correction');
1324
+ const correctionRequest = {
1325
+ systemPrompt: 'Fix the following invalid JSON and return only valid JSON. Do not include any explanation.',
1326
+ userPrompt: `Invalid JSON:\n${content}\n\nError: ${parseError.message}\n\nReturn the corrected JSON:`,
1327
+ temperature: 0.1,
1328
+ responseFormat: 'json',
1329
+ };
1330
+ const correctionResponse = await this.complete(correctionRequest);
1331
+ let correctedContent = correctionResponse.content;
1332
+ // Extract from code blocks again
1333
+ const correctedMatch = correctedContent.match(/```(?:json)?\s*([\s\S]*?)```/);
1334
+ if (correctedMatch) {
1335
+ correctedContent = correctedMatch[1].trim();
1336
+ }
1337
+ parsed = JSON.parse(correctedContent);
1338
+ }
1339
+ // Unwrap single-key object whose value is an array (e.g. {entities:[...]} → [...])
1340
+ // LLM correction attempts sometimes wrap arrays in an object
1341
+ if (parsed !== null &&
1342
+ typeof parsed === 'object' &&
1343
+ !Array.isArray(parsed)) {
1344
+ const keys = Object.keys(parsed);
1345
+ if (keys.length === 1) {
1346
+ const val = parsed[keys[0]];
1347
+ if (Array.isArray(val)) {
1348
+ parsed = val;
1349
+ }
1350
+ }
1351
+ }
1352
+ // Validate against schema if provided (after successful parsing)
1353
+ if (schema) {
1354
+ this.validateSchema(parsed, schema);
1355
+ }
1356
+ return parsed;
1357
+ }
1358
+ /**
1359
+ * Execute request with timeout
1360
+ */
1361
+ async executeWithTimeout(request) {
1362
+ const timeoutMs = this.retryConfig.timeout;
1363
+ const result = await Promise.race([
1364
+ this.provider.generateCompletion(request),
1365
+ new Promise((_, reject) => {
1366
+ setTimeout(() => reject(new Error(`LLM request timed out after ${timeoutMs}ms`)), timeoutMs);
1367
+ }),
1368
+ ]);
1369
+ return result;
1370
+ }
1371
+ /**
1372
+ * Update tracking after a successful request
1373
+ */
1374
+ updateTracking(response) {
1375
+ this.tokenUsage.inputTokens += response.usage.inputTokens;
1376
+ this.tokenUsage.outputTokens += response.usage.outputTokens;
1377
+ this.tokenUsage.totalTokens += response.usage.totalTokens;
1378
+ this.tokenUsage.requests++;
1379
+ // Calculate cost
1380
+ const cost = this.calculateCost(response);
1381
+ this.costTracking.estimatedCost += cost;
1382
+ this.costTracking.byProvider[this.provider.name] = (this.costTracking.byProvider[this.provider.name] ?? 0) + cost;
1383
+ }
1384
+ /**
1385
+ * Calculate cost for a response
1386
+ */
1387
+ calculateCost(response) {
1388
+ const modelPricing = lookupPricing(this.provider.name, response.model);
1389
+ const inputCost = (response.usage.inputTokens / 1_000_000) * modelPricing.input;
1390
+ const outputCost = (response.usage.outputTokens / 1_000_000) * modelPricing.output;
1391
+ return inputCost + outputCost;
1392
+ }
1393
+ /**
1394
+ * Log request/response
1395
+ */
1396
+ logRequest(request, response, error) {
1397
+ const logEntry = {
1398
+ timestamp: new Date().toISOString(),
1399
+ request: this.redactSecrets(request),
1400
+ response,
1401
+ error,
1402
+ };
1403
+ this.requestLog.push(logEntry);
1404
+ }
1405
+ /**
1406
+ * Redact potential secrets from request
1407
+ */
1408
+ redactSecrets(request) {
1409
+ const secretPatterns = [
1410
+ /(?:api[_-]?key|password|secret|token|auth)['":\s]*[=:]\s*['"]?[\w-]{20,}['"]?/gi,
1411
+ /['"]?[a-zA-Z0-9]{32,}['"]?/g, // Long alphanumeric strings
1412
+ ];
1413
+ let systemPrompt = request.systemPrompt;
1414
+ let userPrompt = request.userPrompt;
1415
+ for (const pattern of secretPatterns) {
1416
+ systemPrompt = systemPrompt.replace(pattern, '[REDACTED]');
1417
+ userPrompt = userPrompt.replace(pattern, '[REDACTED]');
1418
+ }
1419
+ return { ...request, systemPrompt, userPrompt };
1420
+ }
1421
+ /**
1422
+ * Simple schema validation
1423
+ */
1424
+ validateSchema(data, schema) {
1425
+ const schemaObj = schema;
1426
+ if (schemaObj.type === 'array') {
1427
+ if (!Array.isArray(data)) {
1428
+ throw new Error('Expected JSON array but received object');
1429
+ }
1430
+ // Validate each item against the items schema if provided
1431
+ const itemsSchema = schemaObj.items;
1432
+ if (itemsSchema?.required && Array.isArray(itemsSchema.required)) {
1433
+ for (const item of data) {
1434
+ for (const field of itemsSchema.required) {
1435
+ if (!(field in item)) {
1436
+ throw new Error(`Missing required field in array item: ${field}`);
1437
+ }
1438
+ }
1439
+ }
1440
+ }
1441
+ }
1442
+ else if (schemaObj.type === 'object' && schemaObj.required && Array.isArray(schemaObj.required)) {
1443
+ const dataObj = data;
1444
+ for (const field of schemaObj.required) {
1445
+ if (!(field in dataObj)) {
1446
+ throw new Error(`Missing required field: ${field}`);
1447
+ }
1448
+ }
1449
+ }
1450
+ }
1451
+ /**
1452
+ * Save logs to disk
1453
+ */
1454
+ async saveLogs() {
1455
+ if (this.requestLog.length === 0)
1456
+ return;
1457
+ await mkdir(this.options.logDir, { recursive: true });
1458
+ const filename = `llm-log-${new Date().toISOString().replace(/[:.]/g, '-')}.json`;
1459
+ const filepath = join(this.options.logDir, filename);
1460
+ await writeFile(filepath, JSON.stringify({
1461
+ summary: {
1462
+ tokenUsage: this.tokenUsage,
1463
+ costTracking: this.costTracking,
1464
+ },
1465
+ requests: this.requestLog,
1466
+ }, null, 2));
1467
+ logger.debug(`Saved LLM logs to ${filepath}`);
1468
+ }
1469
+ /**
1470
+ * Sleep helper
1471
+ */
1472
+ sleep(ms) {
1473
+ return new Promise(resolve => setTimeout(resolve, ms));
1474
+ }
1475
+ }
1476
+ // ============================================================================
1477
+ // FACTORY FUNCTIONS
1478
+ // ============================================================================
1479
+ /**
1480
+ * Create an LLM service with the specified provider
1481
+ */
1482
+ export function createLLMService(options = {}) {
1483
+ const providerName = options.provider ?? 'anthropic';
1484
+ const sslVerify = options.sslVerify ?? true;
1485
+ let provider;
1486
+ if (providerName === 'anthropic') {
1487
+ const apiKey = process.env.ANTHROPIC_API_KEY;
1488
+ if (!apiKey) {
1489
+ throw new Error('ANTHROPIC_API_KEY environment variable is not set');
1490
+ }
1491
+ const apiBase = options.apiBase ?? process.env.ANTHROPIC_API_BASE ?? undefined;
1492
+ provider = new AnthropicProvider(apiKey, options.model ?? DEFAULT_ANTHROPIC_MODEL, apiBase, sslVerify);
1493
+ }
1494
+ else if (providerName === 'openai') {
1495
+ const apiKey = process.env.OPENAI_API_KEY;
1496
+ if (!apiKey) {
1497
+ throw new Error('OPENAI_API_KEY environment variable is not set');
1498
+ }
1499
+ const apiBase = options.apiBase ?? process.env.OPENAI_API_BASE ?? undefined;
1500
+ provider = new OpenAIProvider(apiKey, options.model ?? DEFAULT_OPENAI_MODEL, apiBase, sslVerify);
1501
+ }
1502
+ else if (providerName === 'openai-compat') {
1503
+ const apiKey = process.env.OPENAI_COMPAT_API_KEY;
1504
+ const baseUrl = options.openaiCompatBaseUrl ?? options.apiBase ?? process.env.OPENAI_COMPAT_BASE_URL;
1505
+ if (!apiKey) {
1506
+ throw new Error('OPENAI_COMPAT_API_KEY environment variable is not set');
1507
+ }
1508
+ if (!baseUrl) {
1509
+ throw new Error('openaiCompatBaseUrl must be set in config or OPENAI_COMPAT_BASE_URL env var (e.g. https://api.mistral.ai/v1)');
1510
+ }
1511
+ provider = new OpenAICompatibleProvider(apiKey, baseUrl, options.model ?? DEFAULT_OPENAI_COMPAT_MODEL, options.disableResponseFormat ?? false);
1512
+ }
1513
+ else if (providerName === 'copilot') {
1514
+ const baseUrl = options.openaiCompatBaseUrl ?? options.apiBase ?? process.env.COPILOT_API_BASE_URL ?? 'http://localhost:4141/v1';
1515
+ const apiKey = process.env.COPILOT_API_KEY ?? 'copilot';
1516
+ provider = new CopilotProvider(baseUrl, options.model ?? DEFAULT_COPILOT_MODEL, apiKey);
1517
+ }
1518
+ else if (providerName === 'gemini') {
1519
+ const apiKey = process.env.GEMINI_API_KEY;
1520
+ if (!apiKey) {
1521
+ throw new Error('GEMINI_API_KEY environment variable is not set');
1522
+ }
1523
+ provider = new GeminiProvider(apiKey, options.model ?? DEFAULT_GEMINI_MODEL);
1524
+ }
1525
+ else if (providerName === 'claude-code') {
1526
+ provider = new ClaudeCodeProvider(options.model);
1527
+ }
1528
+ else if (providerName === 'mistral-vibe') {
1529
+ provider = new MistralVibeProvider(options.model);
1530
+ }
1531
+ else if (providerName === 'gemini-cli') {
1532
+ provider = new GeminiCLIProvider(options.model);
1533
+ }
1534
+ else if (providerName === 'cursor-agent') {
1535
+ provider = new CursorAgentProvider(options.model);
1536
+ }
1537
+ else {
1538
+ throw new Error(`Unknown provider: ${providerName}. Supported: anthropic, openai, openai-compat, copilot, gemini, gemini-cli, claude-code, mistral-vibe, cursor-agent`);
1539
+ }
1540
+ if (!sslVerify) {
1541
+ logger.warning('SSL verification is disabled. Use only for trusted internal servers.');
1542
+ }
1543
+ return new LLMService(provider, options);
1544
+ }
1545
+ /**
1546
+ * Create an LLM service with a mock provider (for testing)
1547
+ */
1548
+ export function createMockLLMService(options = {}) {
1549
+ const provider = new MockLLMProvider();
1550
+ const service = new LLMService(provider, options);
1551
+ return { service, provider };
1552
+ }
1553
+ //# sourceMappingURL=llm-service.js.map