pipcode 0.0.0-stage → 2.1.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 (401) hide show
  1. package/LICENSE +47 -0
  2. package/README.md +44 -2
  3. package/auto.js +40 -0
  4. package/bin/setup.js +31 -0
  5. package/bin/teardown.js +18 -0
  6. package/grammars/tree-sitter-javascript.wasm +0 -0
  7. package/grammars/tree-sitter-python.wasm +0 -0
  8. package/grammars/tree-sitter-tsx.wasm +0 -0
  9. package/grammars/tree-sitter-typescript.wasm +0 -0
  10. package/index.js +39 -0
  11. package/package.json +45 -4
  12. package/src/auth/auth-email.js +32 -0
  13. package/src/auth/trusted-key.js +13 -0
  14. package/src/auto/adjust.js +41 -0
  15. package/src/auto/amnesia.js +37 -0
  16. package/src/auto/anonymize-tasks.js +13 -0
  17. package/src/auto/apply-diff-backed.js +10 -0
  18. package/src/auto/apply-diff.js +60 -0
  19. package/src/auto/ask-guard.js +16 -0
  20. package/src/auto/ask-pause.js +36 -0
  21. package/src/auto/ask-protocol.js +6 -0
  22. package/src/auto/ask-user.js +29 -0
  23. package/src/auto/backup-touched.js +30 -0
  24. package/src/auto/budget.js +52 -0
  25. package/src/auto/cancel.js +20 -0
  26. package/src/auto/constants.js +30 -0
  27. package/src/auto/crypto-util.js +17 -0
  28. package/src/auto/decompose.js +47 -0
  29. package/src/auto/diff.js +19 -0
  30. package/src/auto/fs-list.js +34 -0
  31. package/src/auto/git-base.js +16 -0
  32. package/src/auto/git-init.js +39 -0
  33. package/src/auto/git-restore.js +34 -0
  34. package/src/auto/git-scope.js +10 -0
  35. package/src/auto/git.js +38 -0
  36. package/src/auto/mission-docs.js +29 -0
  37. package/src/auto/mission-line.js +22 -0
  38. package/src/auto/node-check.js +30 -0
  39. package/src/auto/observe.js +23 -0
  40. package/src/auto/orchestrator.js +30 -0
  41. package/src/auto/patch-ask.js +16 -0
  42. package/src/auto/paths.js +18 -0
  43. package/src/auto/plan.js +42 -0
  44. package/src/auto/pricing.js +49 -0
  45. package/src/auto/project-files.js +12 -0
  46. package/src/auto/prompts.js +24 -0
  47. package/src/auto/quote-format.js +33 -0
  48. package/src/auto/retry.js +40 -0
  49. package/src/auto/run-lock.js +40 -0
  50. package/src/auto/run-loop.js +38 -0
  51. package/src/auto/run.js +49 -0
  52. package/src/auto/snapshot-gate.js +51 -0
  53. package/src/auto/snapshot-paths.js +33 -0
  54. package/src/auto/snapshot-restore.js +59 -0
  55. package/src/auto/snapshot-take.js +30 -0
  56. package/src/auto/state.js +52 -0
  57. package/src/auto/step-commit.js +46 -0
  58. package/src/auto/step-green.js +38 -0
  59. package/src/auto/step-loop.js +56 -0
  60. package/src/auto/step-prep.js +60 -0
  61. package/src/auto/step-qa.js +53 -0
  62. package/src/auto/step.js +30 -0
  63. package/src/auto/stop-options.js +34 -0
  64. package/src/auto/stop-report.js +17 -0
  65. package/src/auto/tasks.js +44 -0
  66. package/src/cli/apply-loop.js +29 -0
  67. package/src/cli/apply-toml.js +26 -0
  68. package/src/cli/apply.js +45 -0
  69. package/src/cli/ask-prepare.js +37 -0
  70. package/src/cli/auth-branches.js +53 -0
  71. package/src/cli/auth-interactive.js +46 -0
  72. package/src/cli/auto-ask.js +33 -0
  73. package/src/cli/auto-audit.js +16 -0
  74. package/src/cli/auto-cli.js +46 -0
  75. package/src/cli/auto-consent.js +22 -0
  76. package/src/cli/auto-flags.js +39 -0
  77. package/src/cli/auto-gate-session.js +27 -0
  78. package/src/cli/auto-gate.js +34 -0
  79. package/src/cli/auto-loader.js +17 -0
  80. package/src/cli/auto-plan.js +36 -0
  81. package/src/cli/auto-preflight.js +35 -0
  82. package/src/cli/auto-quote.js +28 -0
  83. package/src/cli/auto-report.js +36 -0
  84. package/src/cli/auto-resume.js +28 -0
  85. package/src/cli/auto-rules.js +20 -0
  86. package/src/cli/auto-run.js +26 -0
  87. package/src/cli/auto-task.js +26 -0
  88. package/src/cli/banner.js +20 -0
  89. package/src/cli/centralize.js +23 -0
  90. package/src/cli/collect-step.js +25 -0
  91. package/src/cli/config-pairs.js +46 -0
  92. package/src/cli/config.js +51 -0
  93. package/src/cli/detect-os.js +11 -0
  94. package/src/cli/dispatch.js +36 -0
  95. package/src/cli/env-file.js +44 -0
  96. package/src/cli/env-load.js +28 -0
  97. package/src/cli/env-parse.js +39 -0
  98. package/src/cli/env-rebuild.js +33 -0
  99. package/src/cli/gitignore-env.js +46 -0
  100. package/src/cli/help.js +28 -0
  101. package/src/cli/ide-entries.js +42 -0
  102. package/src/cli/ide-paths.js +37 -0
  103. package/src/cli/ide-roots.js +25 -0
  104. package/src/cli/interactive.js +26 -0
  105. package/src/cli/jetbrains.js +40 -0
  106. package/src/cli/legacy-entry.js +17 -0
  107. package/src/cli/main.js +42 -0
  108. package/src/cli/memory-cli.js +15 -0
  109. package/src/cli/prompts.js +24 -0
  110. package/src/cli/server-entry.js +42 -0
  111. package/src/cli/setup-args.js +39 -0
  112. package/src/cli/target-manual.js +24 -0
  113. package/src/cli/target-select.js +51 -0
  114. package/src/cli/toml-merge.js +56 -0
  115. package/src/cli/trial-check.js +15 -0
  116. package/src/cli/welcome.js +7 -0
  117. package/src/core/abort-signal.js +38 -0
  118. package/src/core/auto-deps.js +29 -0
  119. package/src/core/cache-ttl.js +21 -0
  120. package/src/core/config-base.js +38 -0
  121. package/src/core/config-cache.js +50 -0
  122. package/src/core/config.js +58 -0
  123. package/src/core/constants.js +31 -0
  124. package/src/core/crypto.js +45 -0
  125. package/src/core/dlp.js +51 -0
  126. package/src/core/doctrine-all.js +31 -0
  127. package/src/core/doctrine-caches.js +10 -0
  128. package/src/core/doctrine-fetch.js +49 -0
  129. package/src/core/doctrine-one.js +18 -0
  130. package/src/core/doctrine.js +39 -0
  131. package/src/core/dual-stream-parser.js +55 -0
  132. package/src/core/exports.js +28 -0
  133. package/src/core/google-route.js +17 -0
  134. package/src/core/identity.js +23 -0
  135. package/src/core/lang.js +19 -0
  136. package/src/core/leak-guard.js +40 -0
  137. package/src/core/llm-builder-anthropic.js +26 -0
  138. package/src/core/llm-builder-gemini.js +22 -0
  139. package/src/core/llm-builder-openai.js +26 -0
  140. package/src/core/llm-call.js +53 -0
  141. package/src/core/llm-config-secondary.js +28 -0
  142. package/src/core/llm-config.js +32 -0
  143. package/src/core/llm-errors.js +25 -0
  144. package/src/core/llm-exchange.js +27 -0
  145. package/src/core/llm-extract.js +52 -0
  146. package/src/core/llm-label.js +33 -0
  147. package/src/core/llm-ollama-request.js +32 -0
  148. package/src/core/llm-ollama.js +42 -0
  149. package/src/core/llm-prompts.js +14 -0
  150. package/src/core/llm-providers.js +26 -0
  151. package/src/core/llm-request.js +17 -0
  152. package/src/core/llm-retry-429.js +23 -0
  153. package/src/core/llm-stream-events.js +29 -0
  154. package/src/core/llm-stream.js +42 -0
  155. package/src/core/llm-unbounded.js +15 -0
  156. package/src/core/llm-warmup.js +44 -0
  157. package/src/core/manifest.js +42 -0
  158. package/src/core/master-prompt.js +11 -0
  159. package/src/core/model-id.js +19 -0
  160. package/src/core/ollama-lines.js +48 -0
  161. package/src/core/ollama.js +43 -0
  162. package/src/core/pkg-manager.js +35 -0
  163. package/src/core/process-guards.js +47 -0
  164. package/src/core/progress-emit.js +25 -0
  165. package/src/core/purge-key.js +20 -0
  166. package/src/core/reasoning-cut.js +19 -0
  167. package/src/core/reasoning-models.js +8 -0
  168. package/src/core/session-net.js +60 -0
  169. package/src/core/session-resilient.js +44 -0
  170. package/src/core/session.js +49 -0
  171. package/src/core/sse-parser.js +25 -0
  172. package/src/core/stdout-guard.js +11 -0
  173. package/src/core/tier.js +48 -0
  174. package/src/core/trial-resolve.js +22 -0
  175. package/src/core/trial-session.js +60 -0
  176. package/src/core/trial-status.js +35 -0
  177. package/src/core/usage-cost.js +25 -0
  178. package/src/core/usage-extract.js +28 -0
  179. package/src/core/usage-meter.js +41 -0
  180. package/src/engine/analyze-need.js +24 -0
  181. package/src/engine/analyze-ux.js +42 -0
  182. package/src/engine/ask-history.js +43 -0
  183. package/src/engine/ask-llm.js +40 -0
  184. package/src/engine/ask-prompt.js +48 -0
  185. package/src/engine/ask-rules.js +13 -0
  186. package/src/engine/ast-call-rules-danger.js +24 -0
  187. package/src/engine/ast-call-rules-inject.js +29 -0
  188. package/src/engine/ast-context.js +28 -0
  189. package/src/engine/ast-import-helpers.js +46 -0
  190. package/src/engine/ast-lang.js +24 -0
  191. package/src/engine/ast-node-helpers.js +51 -0
  192. package/src/engine/ast-rules-calls.js +16 -0
  193. package/src/engine/ast-rules-graph.js +58 -0
  194. package/src/engine/ast-rules-js.js +30 -0
  195. package/src/engine/ast-rules-jsx-py.js +26 -0
  196. package/src/engine/ast-runtime.js +60 -0
  197. package/src/engine/ast-scan.js +48 -0
  198. package/src/engine/audit-citations.js +28 -0
  199. package/src/engine/audit-facts.js +47 -0
  200. package/src/engine/audit-report.js +40 -0
  201. package/src/engine/audit-task.js +37 -0
  202. package/src/engine/cap-cache.js +50 -0
  203. package/src/engine/cap-eval.js +49 -0
  204. package/src/engine/cap-http.js +32 -0
  205. package/src/engine/cap-meta-anthropic.js +15 -0
  206. package/src/engine/cap-meta-ollama.js +15 -0
  207. package/src/engine/cap-meta-openrouter.js +19 -0
  208. package/src/engine/cap-meta.js +19 -0
  209. package/src/engine/cap-probe.js +34 -0
  210. package/src/engine/cap-types.js +29 -0
  211. package/src/engine/chat-doctrine.js +31 -0
  212. package/src/engine/checkpoint-control.js +23 -0
  213. package/src/engine/checkpoint-detect.js +19 -0
  214. package/src/engine/checkpoint-rules.js +19 -0
  215. package/src/engine/code-decls.js +12 -0
  216. package/src/engine/code-graph.js +52 -0
  217. package/src/engine/coerce.js +8 -0
  218. package/src/engine/collect.js +21 -0
  219. package/src/engine/compute-impact.js +17 -0
  220. package/src/engine/context-files.js +42 -0
  221. package/src/engine/context-fit.js +28 -0
  222. package/src/engine/context-margin.js +37 -0
  223. package/src/engine/context-relay.js +60 -0
  224. package/src/engine/control-duo.js +38 -0
  225. package/src/engine/control-phase.js +32 -0
  226. package/src/engine/control-prompt.js +25 -0
  227. package/src/engine/control-verdict.js +36 -0
  228. package/src/engine/default-steps.js +7 -0
  229. package/src/engine/deps-parse-js.js +49 -0
  230. package/src/engine/deps-parse-more.js +30 -0
  231. package/src/engine/deps-parse-other.js +41 -0
  232. package/src/engine/deps-parse.js +31 -0
  233. package/src/engine/det-builders-1.js +38 -0
  234. package/src/engine/det-builders-2.js +50 -0
  235. package/src/engine/detect-domains.js +27 -0
  236. package/src/engine/diff-files.js +35 -0
  237. package/src/engine/doc-suite.js +39 -0
  238. package/src/engine/doctrine-prefix.js +13 -0
  239. package/src/engine/domain-regex.js +11 -0
  240. package/src/engine/exports.js +35 -0
  241. package/src/engine/extract-deterministic.js +33 -0
  242. package/src/engine/extract-llm.js +48 -0
  243. package/src/engine/facts-binary.js +12 -0
  244. package/src/engine/facts-embedded.js +18 -0
  245. package/src/engine/facts-functions.js +21 -0
  246. package/src/engine/facts-html.js +22 -0
  247. package/src/engine/facts-inventory.js +33 -0
  248. package/src/engine/facts-lang.js +34 -0
  249. package/src/engine/facts-measure.js +43 -0
  250. package/src/engine/facts-patterns.js +28 -0
  251. package/src/engine/facts-project.js +26 -0
  252. package/src/engine/facts-size.js +26 -0
  253. package/src/engine/facts-wordpress.js +36 -0
  254. package/src/engine/generate-architecture-guide.js +46 -0
  255. package/src/engine/generate-readme.js +38 -0
  256. package/src/engine/generate-runbook.js +47 -0
  257. package/src/engine/git-facts.js +16 -0
  258. package/src/engine/history-purge.js +16 -0
  259. package/src/engine/json-balance.js +37 -0
  260. package/src/engine/json-safe.js +13 -0
  261. package/src/engine/line-numbers.js +16 -0
  262. package/src/engine/md-build-mid.js +35 -0
  263. package/src/engine/md-build-top.js +58 -0
  264. package/src/engine/md-build.js +36 -0
  265. package/src/engine/md-parse-helpers.js +7 -0
  266. package/src/engine/md-parse-pillars.js +38 -0
  267. package/src/engine/md-parse-ux-design.js +31 -0
  268. package/src/engine/md-parse.js +33 -0
  269. package/src/engine/mission-context-prefix.js +30 -0
  270. package/src/engine/mission-guard.js +36 -0
  271. package/src/engine/mission-helpers.js +23 -0
  272. package/src/engine/mission-log.js +56 -0
  273. package/src/engine/mission-paths.js +7 -0
  274. package/src/engine/mission-store.js +51 -0
  275. package/src/engine/normalize-extraction.js +35 -0
  276. package/src/engine/normalize-impact-design.js +39 -0
  277. package/src/engine/normalize-pillars.js +36 -0
  278. package/src/engine/orchestrate-route.js +39 -0
  279. package/src/engine/orchestrator-build.js +37 -0
  280. package/src/engine/orchestrator-phases.js +55 -0
  281. package/src/engine/orchestrator.js +40 -0
  282. package/src/engine/osv.js +43 -0
  283. package/src/engine/parse-json-loose.js +36 -0
  284. package/src/engine/patch-file-sections.js +28 -0
  285. package/src/engine/patch-scan-cheat.js +52 -0
  286. package/src/engine/patch-scan-prep.js +37 -0
  287. package/src/engine/patch-scan-rules.js +51 -0
  288. package/src/engine/patch-scan.js +38 -0
  289. package/src/engine/patch-secret-guard.js +50 -0
  290. package/src/engine/patch-ui-guard.js +49 -0
  291. package/src/engine/pattern-sanitize.js +23 -0
  292. package/src/engine/plan-draft.js +37 -0
  293. package/src/engine/plan-mission.js +60 -0
  294. package/src/engine/plan-reply-legacy.js +34 -0
  295. package/src/engine/plan-reply-pillars.js +34 -0
  296. package/src/engine/plan-reply-ux.js +17 -0
  297. package/src/engine/plan-reply.js +28 -0
  298. package/src/engine/plan-summary.js +16 -0
  299. package/src/engine/preflight-text.js +27 -0
  300. package/src/engine/preflight.js +27 -0
  301. package/src/engine/project-brief.js +41 -0
  302. package/src/engine/project-context.js +49 -0
  303. package/src/engine/project-instructions.js +19 -0
  304. package/src/engine/py-imports.js +31 -0
  305. package/src/engine/relay-analysis.js +44 -0
  306. package/src/engine/relay-digest.js +42 -0
  307. package/src/engine/route-llm.js +25 -0
  308. package/src/engine/rule-text.js +12 -0
  309. package/src/engine/self-assessment.js +33 -0
  310. package/src/engine/steps-block.js +19 -0
  311. package/src/engine/ux-violations-leak.js +43 -0
  312. package/src/engine/vuln-facts.js +37 -0
  313. package/src/fs/atomic-text.js +45 -0
  314. package/src/fs/atomic-write.js +21 -0
  315. package/src/fs/backup-once.js +27 -0
  316. package/src/fs/config-path.js +39 -0
  317. package/src/fs/config-read.js +23 -0
  318. package/src/fs/config-write.js +60 -0
  319. package/src/fs/home-root.js +55 -0
  320. package/src/fs/load-config.js +36 -0
  321. package/src/fs/os-errors.js +26 -0
  322. package/src/fs/permissions.js +48 -0
  323. package/src/mcp/boot-warmup.js +9 -0
  324. package/src/mcp/call-tool.js +20 -0
  325. package/src/mcp/client-info.js +35 -0
  326. package/src/mcp/engine-rules.js +16 -0
  327. package/src/mcp/handle-dlp.js +37 -0
  328. package/src/mcp/handle-licence.js +46 -0
  329. package/src/mcp/handle-list.js +15 -0
  330. package/src/mcp/handle-memory.js +37 -0
  331. package/src/mcp/handle-skill-fetch.js +50 -0
  332. package/src/mcp/handle-skill.js +38 -0
  333. package/src/mcp/licence-text.js +29 -0
  334. package/src/mcp/prompt-licence.js +11 -0
  335. package/src/mcp/server-create.js +33 -0
  336. package/src/mcp/server-main.js +39 -0
  337. package/src/mcp/tool-dispatch.js +28 -0
  338. package/src/mcp/tool-dlp.js +20 -0
  339. package/src/mcp/tool-licence.js +17 -0
  340. package/src/mcp/tool-memory.js +32 -0
  341. package/src/mcp/tool-skill.js +24 -0
  342. package/src/mcp/tools-list.js +13 -0
  343. package/src/memory/exports.js +5 -0
  344. package/src/memory/memory-filter.js +32 -0
  345. package/src/memory/memory-gate.js +21 -0
  346. package/src/memory/memory-store.js +44 -0
  347. package/src/net/auth-browser.js +49 -0
  348. package/src/net/auth-endpoints.js +33 -0
  349. package/src/net/local-fetch.js +31 -0
  350. package/src/net/loopback.js +8 -0
  351. package/src/net/proxy-env.js +59 -0
  352. package/src/project-memory/batches.js +17 -0
  353. package/src/project-memory/cahiers.js +42 -0
  354. package/src/project-memory/decision-settle.js +15 -0
  355. package/src/project-memory/decisions.js +36 -0
  356. package/src/project-memory/index-build.js +35 -0
  357. package/src/project-memory/memory-build.js +16 -0
  358. package/src/project-memory/memory-tags.js +40 -0
  359. package/src/project-memory/pending.js +24 -0
  360. package/src/project-memory/qa-apply.js +39 -0
  361. package/src/project-memory/qa-tags.js +28 -0
  362. package/src/project-memory/recall.js +38 -0
  363. package/src/project-memory/store.js +37 -0
  364. package/src/project-memory/window.js +13 -0
  365. package/src/rules/rules-advisor.js +17 -0
  366. package/src/rules/rules-frugality.js +17 -0
  367. package/src/rules/rules-load.js +56 -0
  368. package/src/rules/rules-project.js +16 -0
  369. package/src/rules/rules-registry.js +32 -0
  370. package/src/rules/rules-validate-pricing.js +22 -0
  371. package/src/rules/rules-validate-scan.js +45 -0
  372. package/src/rules/rules-validate.js +53 -0
  373. package/src/teardown/consent.js +31 -0
  374. package/src/teardown/flags.js +39 -0
  375. package/src/teardown/main.js +41 -0
  376. package/src/teardown/plan.js +31 -0
  377. package/src/teardown/remove-entry.js +39 -0
  378. package/src/teardown/remove-file.js +37 -0
  379. package/src/teardown/remove-toml.js +33 -0
  380. package/src/teardown/report.js +34 -0
  381. package/src/types/ast.d.ts +40 -0
  382. package/src/types/auto-ask.d.ts +6 -0
  383. package/src/types/auto-orch.d.ts +59 -0
  384. package/src/types/auto.d.ts +45 -0
  385. package/src/types/capability.d.ts +12 -0
  386. package/src/types/cli-auto-gate.d.ts +45 -0
  387. package/src/types/cli-auto.d.ts +42 -0
  388. package/src/types/config.d.ts +53 -0
  389. package/src/types/control.d.ts +56 -0
  390. package/src/types/docs.d.ts +6 -0
  391. package/src/types/doctrine.d.ts +22 -0
  392. package/src/types/llm.d.ts +28 -0
  393. package/src/types/mcp.d.ts +32 -0
  394. package/src/types/memory.d.ts +17 -0
  395. package/src/types/mission.d.ts +46 -0
  396. package/src/types/orchestrator.d.ts +33 -0
  397. package/src/types/routing.d.ts +25 -0
  398. package/src/types/rules.d.ts +30 -0
  399. package/src/types/snapshots.d.ts +25 -0
  400. package/src/types/teardown.d.ts +13 -0
  401. package/src/types/usage.d.ts +30 -0
@@ -0,0 +1,39 @@
1
+ // src/core/doctrine.js — loadDoctrine : fetch + vérif Ed25519 de core_rules (tier lite, hors quota Pro).
2
+ // Cache RAM TTL 5 h (cache-ttl.js), jamais sur disque. Fail-Fast : signature invalide ou contenu vide → Error.
3
+ import { fetchSignedSkill } from "./doctrine-fetch.js";
4
+ import { verifySignature } from "./crypto.js";
5
+ import { freshCache, isCacheFresh } from "./cache-ttl.js";
6
+
7
+ /** @type {DoctrineCache | null} */
8
+ let doctrineCache = null;
9
+
10
+ // Charge la doctrine core_rules (vérifiée Ed25519). Cache RAM TTL 5 h.
11
+ /** @param {PlugConfig} cfg @returns {Promise<string>} */
12
+ export async function loadDoctrine(cfg) {
13
+ if (isCacheFresh(doctrineCache)) return doctrineCache.content;
14
+ const payload = await fetchSignedSkill("core_rules", "lite", cfg);
15
+ if (!verifySignature(payload.data, payload.sig)) {
16
+ throw new Error(
17
+ "Signature Ed25519 de la doctrine (core_rules) INVALIDE — payload rejeté. " +
18
+ "Cause probable : l'ancre de confiance client (PLUGCTO_TRUSTED_PUBLIC_X) ne " +
19
+ "correspond pas à la clé privée du Worker déployé. Exécuter `npx pipcode setup` " +
20
+ "pour mettre le client à jour."
21
+ );
22
+ }
23
+ let parsed;
24
+ try {
25
+ parsed = /** @type {SkillPayloadContent} */ (JSON.parse(payload.data));
26
+ } catch {
27
+ throw new Error("Doctrine (core_rules) : payload signé valide mais données illisibles.");
28
+ }
29
+ const content = typeof parsed.content === "string" ? parsed.content : "";
30
+ if (!content.trim()) throw new Error("Doctrine (core_rules) vide.");
31
+ doctrineCache = freshCache(content);
32
+ return content;
33
+ }
34
+
35
+ // Test-only : réinitialise le cache core_rules.
36
+ export function resetDoctrineCache() {
37
+ doctrineCache = null;
38
+ }
39
+ // MODULE_COMPLETE
@@ -0,0 +1,55 @@
1
+ // src/core/dual-stream-parser.js — Parser de flux à DEUX ÉTATS : THINKING (raisonnement : champ dédié ou <think>…</think>) et PAYLOAD
2
+ // (réponse). Le raisonnement ne touche JAMAIS le tampon de la réponse : il sert à la télémétrie (ligne de statut), pas au JSON extrait.
3
+ // Balises coupées entre deux morceaux gérées ; « </think> » seul (modèle dont l'ouverture est dans son gabarit) accepté ; non fermé = fin en THINKING.
4
+
5
+ export const THINKING = "THINKING";
6
+ export const PAYLOAD = "PAYLOAD";
7
+ const OPENS = ["<think>", "<thinking>"];
8
+ const CLOSES = ["</think>", "</thinking>"];
9
+
10
+ /** @param {string} s @param {string[]} tags @returns {{ at: number, len: number } | null} première balise rencontrée */
11
+ function firstTag(s, tags) {
12
+ /** @type {{ at: number, len: number } | null} */ let best = null;
13
+ for (const t of tags) { const at = s.indexOf(t); if (at !== -1 && (!best || at < best.at)) best = { at, len: t.length }; }
14
+ return best;
15
+ }
16
+
17
+ /** Longueur de la fin de `s` qui pourrait être le début d'une des balises (à garder pour le prochain morceau). @param {string} s @param {string[]} tags @returns {number} */
18
+ function heldTail(s, tags) {
19
+ for (let n = Math.min(s.length, 11); n > 0; n--) if (tags.some((t) => t.startsWith(s.slice(-n)) && t !== s.slice(-n))) return n;
20
+ return 0;
21
+ }
22
+
23
+ export class DualStreamTokenParser {
24
+ constructor() { this.state = PAYLOAD; this.thinking = ""; this.payload = ""; this.held = ""; this.inTag = false; }
25
+
26
+ /** Jetons de raisonnement d'un champ dédié (reasoning_content / thinking). @param {string} text */
27
+ reason(text) { if (text) { this.thinking += text; this.state = THINKING; } }
28
+
29
+ /** Jetons de contenu : texte de réponse, éventuellement entrecoupé de balises de raisonnement. @param {string} text */
30
+ content(text) {
31
+ let rest = this.held + text; this.held = "";
32
+ while (rest) {
33
+ const tag = firstTag(rest, this.inTag ? CLOSES : [...OPENS, ...CLOSES]);
34
+ if (!tag) { const keep = heldTail(rest, this.inTag ? CLOSES : [...OPENS, ...CLOSES]); this.#put(rest.slice(0, rest.length - keep)); this.held = rest.slice(rest.length - keep); return; }
35
+ const closing = CLOSES.some((c) => rest.startsWith(c, tag.at));
36
+ if (closing && !this.inTag) { this.thinking += this.payload + rest.slice(0, tag.at); this.payload = ""; } else this.#put(rest.slice(0, tag.at));
37
+ this.inTag = !closing; this.state = closing ? PAYLOAD : THINKING;
38
+ rest = rest.slice(tag.at + tag.len);
39
+ }
40
+ }
41
+
42
+ /** @param {string} s */
43
+ #put(s) { if (this.inTag) { this.thinking += s; return; } this.payload += s; if (s.trim()) this.state = PAYLOAD; }
44
+
45
+ /** Fin du flux : le reste retenu est versé là où le parser se trouve. @returns {{ thinking: string, payload: string, state: string }} */
46
+ end() { this.#put(this.held); this.held = ""; return { thinking: this.thinking, payload: this.payload, state: this.inTag ? THINKING : this.state }; }
47
+ }
48
+
49
+ /** Texte complet → réponse SANS raisonnement. @param {string} text @returns {string} */
50
+ export function stripReasoning(text) {
51
+ const p = new DualStreamTokenParser();
52
+ p.content(String(text ?? ""));
53
+ return p.end().payload.trim();
54
+ }
55
+ // MODULE_COMPLETE
@@ -0,0 +1,28 @@
1
+ // src/core/exports.js — Surface publique du domaine core : agrégation des
2
+ // re-exports consommés par les tests via le hub index.js.
3
+ export { readProviderConfig } from "./llm-config.js";
4
+ export { extractLLMText } from "./llm-extract.js";
5
+ export { callLLM } from "./llm-call.js";
6
+ export { RATE_LIMIT_MESSAGE, isRateLimit, friendlyError } from "./llm-errors.js";
7
+ export { warmupCache } from "./llm-warmup.js";
8
+ export { PROVIDER_DEFAULTS } from "./llm-providers.js";
9
+ export { patchMaxTokens, ANALYSIS_MAX_TOKENS } from "./constants.js";
10
+ export { verifySessionToken } from "./session.js";
11
+ export { ensureSession, requestSession, purgeSessionCache } from "./session-net.js";
12
+ export { ensureTrialSession, requestTrialSession, purgeTrialCache } from "./trial-session.js";
13
+ export { resolveTrialOrLite } from "./trial-resolve.js";
14
+ export { resolveSessionResilient } from "./session-resilient.js";
15
+ export { purgeLicenseKey } from "./purge-key.js";
16
+ export { isCacheFresh, DOCTRINE_TTL_MS } from "./cache-ttl.js";
17
+ export { machineFp } from "./identity.js";
18
+ export { verifySignature, resolveTrustAnchor } from "./crypto.js";
19
+ export { normalizeText, scanForLeak, extractFingerprints } from "./dlp.js";
20
+ export { isValidToolName } from "./manifest.js";
21
+ export { loadConfig } from "./config.js";
22
+ export { resolveTier, isWithinTrial, maybeTrialExpiredNotice } from "./tier.js";
23
+ export { loadDoctrine } from "./doctrine.js";
24
+ export { loadAllDoctrine } from "./doctrine-all.js";
25
+ export { __resetDoctrineCachesForTest } from "./doctrine-caches.js";
26
+ export { fetchSignedSkill } from "./doctrine-fetch.js";
27
+ export { buildMasterPrompt } from "./master-prompt.js";
28
+ // MODULE_COMPLETE
@@ -0,0 +1,17 @@
1
+ // src/core/google-route.js — Tout appel vers Google (generativelanguage.googleapis.com) passe par l'API REST NATIVE : adaptateur « gemini », route
2
+ // /v1beta/models/<id>:generateContent, clé dans l'en-tête x-goog-api-key (jamais Authorization: Bearer, que le service natif refuse en 401).
3
+ // Une configuration qui pointe encore vers l'adresse « compatible OpenAI » (/v1beta/openai) ou vers /v1 est ramenée à la route native /v1beta.
4
+
5
+ export const GOOGLE_HOST = "generativelanguage.googleapis.com";
6
+ export const GOOGLE_NATIVE_BASE = `https://${GOOGLE_HOST}/v1beta`;
7
+
8
+ /** @param {string} base @returns {boolean} */
9
+ export function isGoogleBase(base) {
10
+ try { return new URL(String(base)).hostname === GOOGLE_HOST; } catch { return false; } // adresse illisible : pas Google
11
+ }
12
+
13
+ /** @param {string} provider @param {string} base @returns {{ provider: string, base: string }} */
14
+ export function routeGoogle(provider, base) {
15
+ return isGoogleBase(base) ? { provider: "gemini", base: GOOGLE_NATIVE_BASE } : { provider, base };
16
+ }
17
+ // MODULE_COMPLETE
@@ -0,0 +1,23 @@
1
+ // src/core/identity.js — Empreinte machine stable (anti-rotation IP de l'essai, B1).
2
+ // Calculée depuis des identifiants OS stables (hostname, utilisateur, platform, arch) :
3
+ // survit aux changements d'IP (VPN/DHCP) et redémarrages, diffère par machine/compte.
4
+ // Hashée SHA-256 (anonyme : aucune donnée OS lisible n'est envoyée). Côté serveur,
5
+ // l'essai Pro est bindé à cette empreinte (et non à l'IP). Cache vie-du-process.
6
+
7
+ import os from "node:os";
8
+ import crypto from "node:crypto";
9
+
10
+ /** @type {string | null} */
11
+ let _machineFpCache = null;
12
+
13
+ // Empreinte machine SHA-256 (base64url). Stable par machine/compte, anonyme.
14
+ /** @returns {string} */
15
+ export function machineFp() {
16
+ if (_machineFpCache) return _machineFpCache;
17
+ let username = "";
18
+ try { username = (os.userInfo && os.userInfo().username) || ""; } catch { username = ""; }
19
+ const parts = [os.hostname() || "", username, os.platform() || "", os.arch() || ""];
20
+ _machineFpCache = crypto.createHash("sha256").update(parts.join("|")).digest("base64url");
21
+ return _machineFpCache;
22
+ }
23
+ // MODULE_COMPLETE
@@ -0,0 +1,19 @@
1
+ // src/core/lang.js — Consigne de LANGUE injectée dans les prompts qui produisent du texte lu par l'utilisateur (chat,
2
+ // extraction de mission, analyse). La langue vient de l'extension (PLUGCTO_LANG = langue d'affichage de VS Code) ;
3
+ // valeur absente ou hors forme BCP 47 → aucune consigne (jamais de texte construit à partir d'une entrée non validée).
4
+ // Les prompts à format machine (diff, décomposition JSON, verdict) n'en reçoivent pas.
5
+
6
+ const LANG_RE = /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8}){0,3}$/;
7
+
8
+ /** @param {NodeJS.ProcessEnv} [env] @returns {string} */
9
+ export function languageDirective(env = process.env) {
10
+ const code = String(env.PLUGCTO_LANG || "").trim();
11
+ return LANG_RE.test(code) ? `Formule l'intégralité de tes réponses et analyses dans la langue du système (${code}).` : "";
12
+ }
13
+
14
+ /** Prompt système + consigne de langue (préfixe stable par langue : le cache de prompt reste valide). @param {string} systemPrompt @param {NodeJS.ProcessEnv} [env] @returns {string} */
15
+ export function withLanguage(systemPrompt, env = process.env) {
16
+ const d = languageDirective(env);
17
+ return d ? `${systemPrompt}\n\n${d}` : systemPrompt;
18
+ }
19
+ // MODULE_COMPLETE
@@ -0,0 +1,40 @@
1
+ // src/core/leak-guard.js — Garde anti-fuite : une ligne de réponse qui recopie 8 mots consécutifs d'un texte CONFIDENTIEL (règles, doctrine, protocole servis
2
+ // par le serveur) est remplacée par une mention neutre, AVANT affichage (flux compris : sortie ligne par ligne). Complète la consigne de confidentialité
3
+ // du prompt : une paraphrase passe, une récitation non. Pur ; sans secret (liste vide) → aucun effet, aucune mise en tampon.
4
+
5
+ const SPAN = 8;
6
+ export const REDACTED = "[Contenu interne PipCode non détaillé.]";
7
+
8
+ /** @param {string} s @returns {string[]} */
9
+ const words = (s) => s.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ").split(" ").filter(Boolean);
10
+
11
+ export class LeakGuard {
12
+ /** @param {string[]} secrets textes confidentiels transmis au modèle */
13
+ constructor(secrets) {
14
+ /** @type {Set<string>} */ this.known = new Set();
15
+ for (const text of secrets) { const w = words(text); for (let i = 0; i + SPAN <= w.length; i++) this.known.add(w.slice(i, i + SPAN).join(" ")); }
16
+ this.buf = "";
17
+ }
18
+
19
+ /** @param {string} line @returns {string} */
20
+ #line(line) {
21
+ const w = words(line);
22
+ for (let i = 0; i + SPAN <= w.length; i++) if (this.known.has(w.slice(i, i + SPAN).join(" "))) return REDACTED;
23
+ return line;
24
+ }
25
+
26
+ /** Texte complet (réponse finale, raisonnement compris). @param {string} text @returns {string} */
27
+ sanitize(text) { return this.known.size ? text.split("\n").map((l) => this.#line(l)).join("\n") : text; }
28
+
29
+ /** Flux : ne rend que des lignes COMPLÈTES, contrôlées. @param {string} chunk @returns {string} */
30
+ push(chunk) {
31
+ if (!this.known.size) return chunk;
32
+ const parts = (this.buf + chunk).split("\n");
33
+ this.buf = parts.pop() ?? "";
34
+ return parts.map((l) => this.#line(l) + "\n").join("");
35
+ }
36
+
37
+ /** Fin du flux : dernière ligne incomplète contrôlée. @returns {string} */
38
+ end() { const rest = this.buf; this.buf = ""; return rest ? this.#line(rest) : ""; }
39
+ }
40
+ // MODULE_COMPLETE
@@ -0,0 +1,26 @@
1
+ // src/core/llm-builder-anthropic.js — Builder de requête Anthropic (messages +
2
+ // cache_control ephemeral sur le préfixe stable). Scission de llm-request.js.
3
+
4
+ /** @param {import("./llm-request.js").LLMParams} p @returns {{ url: string, init: RequestInit }} */
5
+ export function buildAnthropicStyle(p) {
6
+ const systemBlocks = [{ type: "text", text: p.systemPrompt, cache_control: { type: "ephemeral" } }];
7
+ const userContent = p.stablePrefix
8
+ ? [{ type: "text", text: p.stablePrefix, cache_control: { type: "ephemeral" } }, { type: "text", text: p.userText }]
9
+ : p.userText;
10
+ return {
11
+ url: `${p.base}/messages`,
12
+ init: {
13
+ method: "POST",
14
+ signal: p.signal,
15
+ headers: { "Content-Type": "application/json", "x-api-key": p.key, "anthropic-version": "2023-06-01" },
16
+ body: JSON.stringify({
17
+ model: p.model,
18
+ max_tokens: p.budget,
19
+ ...(p.stream ? { stream: true } : {}),
20
+ system: systemBlocks,
21
+ messages: [{ role: "user", content: userContent }],
22
+ }),
23
+ },
24
+ };
25
+ }
26
+ // MODULE_COMPLETE
@@ -0,0 +1,22 @@
1
+ // src/core/llm-builder-gemini.js — Builder de requête Gemini (generateContent).
2
+ // Scission de llm-request.js (audit hebdo) : un builder par famille de provider.
3
+ import { isReasoningModel } from "./reasoning-models.js";
4
+ import { sanitizeGeminiModelId } from "./model-id.js";
5
+
6
+ /** @param {import("./llm-request.js").LLMParams} p @returns {{ url: string, init: RequestInit }} */
7
+ export function buildGeminiStyle(p) {
8
+ return {
9
+ url: `${p.base.replace(/\/+$/, "")}/models/${encodeURIComponent(sanitizeGeminiModelId(p.model))}${p.stream ? ":streamGenerateContent?alt=sse" : ":generateContent"}`, // « : » LITTÉRAL (encodé en %3A = 404) ; le segment /models/ est celui de la route, le préfixe de l'identifiant est retiré
10
+ init: {
11
+ method: "POST",
12
+ signal: p.signal,
13
+ headers: { "Content-Type": "application/json", "x-goog-api-key": p.key },
14
+ body: JSON.stringify({
15
+ system_instruction: { parts: [{ text: p.systemPrompt }] },
16
+ contents: [{ role: "user", parts: [{ text: p.stablePrefix ? p.stablePrefix + "\n" + p.userText : p.userText }] }],
17
+ generationConfig: p.unbounded || isReasoningModel(p.model) ? {} : { maxOutputTokens: p.budget }, // Gemini 2.5/3 : la réflexion compte dans la limite → aucune limite
18
+ }),
19
+ },
20
+ };
21
+ }
22
+ // MODULE_COMPLETE
@@ -0,0 +1,26 @@
1
+ // src/core/llm-builder-openai.js — Builder de requête OpenAI/Zhipu (chat/completions).
2
+ // Scission de llm-request.js (audit hebdo) : un builder par famille de provider.
3
+
4
+ import { isReasoningModel } from "./reasoning-models.js";
5
+
6
+ /** @param {import("./llm-request.js").LLMParams} p @returns {{ url: string, init: RequestInit }} */
7
+ export function buildOpenAIStyle(p) {
8
+ return {
9
+ url: `${p.base}/chat/completions`,
10
+ init: {
11
+ method: "POST",
12
+ signal: p.signal,
13
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${p.key}` },
14
+ body: JSON.stringify({
15
+ model: p.model,
16
+ ...(p.unbounded || isReasoningModel(p.model) ? {} : { max_tokens: p.budget }), // modèle à raisonnement : AUCUNE limite (il s'arrête seul sur son jeton de fin)
17
+ ...(p.stream ? { stream: true, stream_options: { include_usage: true } } : {}), // flux SSE (+ usage réel dans le dernier morceau)
18
+ messages: [
19
+ { role: "system", content: p.systemPrompt },
20
+ { role: "user", content: p.stablePrefix ? p.stablePrefix + "\n" + p.userText : p.userText },
21
+ ],
22
+ }),
23
+ },
24
+ };
25
+ }
26
+ // MODULE_COMPLETE
@@ -0,0 +1,53 @@
1
+ // src/core/llm-call.js — callLLM : adaptateur d'API unifié (OpenAI/Anthropic/Gemini/Zhipu).
2
+ // Budget max_tokens dynamique (Token Saver), timeout AbortController, Error enrichie (status/provider).
3
+ import { DEFAULT_MAX_TOKENS } from "./constants.js";
4
+ import { providerLabel, timeoutFor } from "./llm-label.js";
5
+ import { ipv4Loopback } from "../net/loopback.js";
6
+ import { exchangeUnbounded } from "./llm-unbounded.js";
7
+ import { retryOn429 } from "./llm-retry-429.js";
8
+ import { sanitizeModelId } from "./model-id.js";
9
+ import { emitProgress } from "./progress-emit.js";
10
+ import { isRunAborted, linkAbort, RunAbortedError } from "./abort-signal.js";
11
+ import { stripReasoning } from "./dual-stream-parser.js";
12
+
13
+ // Appelle un provider compatible OpenAI/Anthropic/Gemini/Zhipu. Retourne le texte généré ou jette.
14
+ /** @param {CallLLMArgs} { provider, key, model, base, systemPrompt, userText, cachePrefix, maxTokens } @returns {Promise<string>} */
15
+ export async function callLLM({ provider, key, model: rawModel, base: rawBase, systemPrompt, userText, cachePrefix, maxTokens, showThinking, onDelta }) {
16
+ const base = ipv4Loopback(String(rawBase || "")); // localhost → 127.0.0.1 (IPv4 strict)
17
+ const model = sanitizeModelId(base, rawModel); // dernier point avant toute requête : « models/x » → « x » chez Google
18
+ if (provider && key && base && !model) {
19
+ throw new Error(`Aucun modèle configuré pour ${provider} : choisissez un modèle dans les réglages de PipCode (aucune requête envoyée).`);
20
+ }
21
+ if (!provider || !key || !model || !base) {
22
+ throw new Error("Configuration provider incomplète (provider/key/model/base).");
23
+ }
24
+ const requested = (Number.isFinite(maxTokens) && (/** @type {number} */ (maxTokens)) > 0)
25
+ ? Math.min(DEFAULT_MAX_TOKENS, Math.round(/** @type {number} */ (maxTokens)))
26
+ : DEFAULT_MAX_TOKENS;
27
+ const budget = requested; // modèle à raisonnement : cette valeur n'est PAS envoyée (unbounded)
28
+ const stablePrefix = cachePrefix ? String(cachePrefix) : "";
29
+ if (isRunAborted()) throw new RunAbortedError(); // arrêt déjà demandé : aucune requête ne part
30
+ const controller = new AbortController();
31
+ linkAbort(controller); // « Stop » de l'interface : la requête en cours est annulée
32
+ const timeoutMs = timeoutFor(base);
33
+ const label = providerLabel(provider, base);
34
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
35
+ try {
36
+ emitProgress({ k: "tick" }); // signe de vie : l'interface sait que le moteur travaille (délai d'inactivité)
37
+ const text = await retryOn429(() => exchangeUnbounded({ provider, key, model, base, budget, systemPrompt, userText, stablePrefix, signal: controller.signal, label, showThinking, onDelta })); // 429 : 3 essais, délai exponentiel
38
+ emitProgress({ k: "tick" });
39
+ return showThinking ? text : stripReasoning(text); // le raisonnement n'entre jamais dans un texte analysé (JSON, diff, verdict)
40
+ } catch (err) {
41
+ const e0 = /** @type {EnrichedError} */ (err);
42
+ if (isRunAborted()) throw new RunAbortedError(); // arrêt voulu : ni « délai dépassé » ni panne
43
+ if (e0 && e0.name === "AbortError") {
44
+ const e = /** @type {EnrichedError} */ (new Error(`Délai dépassé (${Math.round(timeoutMs / 1000)} s) : ${label} n'a pas répondu (modèle ${model}).`));
45
+ e.provider = provider;
46
+ throw e;
47
+ }
48
+ throw err;
49
+ } finally {
50
+ clearTimeout(timer);
51
+ }
52
+ }
53
+ // MODULE_COMPLETE
@@ -0,0 +1,28 @@
1
+ // src/core/llm-config-secondary.js — readSecondaryProvider : provider cloud
2
+ // SECONDAIRE optionnel (bascule BYOK conditionnelle du routage hybride). Même
3
+ // schéma que le primaire, préfixe PLUGCTO_PROVIDER_SECONDARY. Zéro secret en dur.
4
+
5
+ import { PROVIDER_DEFAULTS, isValidProvider } from "./llm-providers.js";
6
+ import { ipv4Loopback } from "../net/loopback.js";
7
+
8
+ // Retourne { secondary } = { provider, key, model, base, smartModel, fastModel }
9
+ // ou { secondary: null } si absent/incomplet. Accepte un env explicite (tests).
10
+ /** @param {Record<string, string | undefined>} [env] @returns {{ secondary: ProviderPrimary | null }} */
11
+ export function readSecondaryProvider(env = process.env) {
12
+ const provider = (env.PLUGCTO_PROVIDER_SECONDARY || "").toLowerCase();
13
+ if (!isValidProvider(provider)) return { secondary: null };
14
+ const key = env.PLUGCTO_PROVIDER_SECONDARY_KEY || "";
15
+ if (!key) return { secondary: null };
16
+ const d = PROVIDER_DEFAULTS[provider];
17
+ const base = ipv4Loopback(env.PLUGCTO_PROVIDER_SECONDARY_BASE || d.base);
18
+ const mainModel = (env.PLUGCTO_PROVIDER_SECONDARY_MODEL || "").trim(); // exact, jamais de modèle par défaut
19
+ const fast = (env.PLUGCTO_PROVIDER_SECONDARY_FAST_MODEL || "").trim() || mainModel;
20
+ return {
21
+ secondary: {
22
+ provider: /** @type {ProviderName} */ (provider),
23
+ key, base,
24
+ model: mainModel, smartModel: mainModel, fastModel: fast,
25
+ },
26
+ };
27
+ }
28
+ // MODULE_COMPLETE
@@ -0,0 +1,32 @@
1
+ // src/core/llm-config.js — readProviderConfig : lit la config provider depuis l'env (single-key strict).
2
+ // API_KEY alias de PLUGCTO_PROVIDER_PRIMARY_KEY. Fallback OLLAMA_HOST normalisé /v1.
3
+ import { PROVIDER_DEFAULTS, isValidProvider, normalizeOllamaBase } from "./llm-providers.js";
4
+ import { ipv4Loopback } from "../net/loopback.js";
5
+
6
+ // Retourne { primary } = { provider, key, model, base, smartModel, fastModel } ou null si incomplet.
7
+ // Accepte un env explicite (tests) ; défaut = process.env. smart_model = modèle principal, fast = variante légère.
8
+ /** @param {Record<string, string | undefined>} [env] @returns {{ primary: ProviderPrimary | null }} */
9
+ export function readProviderConfig(env = process.env) {
10
+ const provider = (env.PLUGCTO_PROVIDER_PRIMARY || "").toLowerCase();
11
+ if (!isValidProvider(provider)) return { primary: null };
12
+ const key = env.PLUGCTO_PROVIDER_PRIMARY_KEY || env.API_KEY || "";
13
+ if (!key) return { primary: null };
14
+ const d = PROVIDER_DEFAULTS[provider];
15
+ const explicitBase = env.PLUGCTO_PROVIDER_PRIMARY_BASE;
16
+ const base = ipv4Loopback(explicitBase
17
+ || (provider === "openai" && env.OLLAMA_HOST ? normalizeOllamaBase(env.OLLAMA_HOST) : d.base)); // OLLAMA_HOST ne concerne QUE l'adaptateur openai ; localhost → 127.0.0.1
18
+ const mainModel = (env.PLUGCTO_PROVIDER_PRIMARY_MODEL || "").trim(); // exact, jamais de modèle par défaut (vide : erreur explicite à l'appel)
19
+ const smartModel = (env.PLUGCTO_SMART_MODEL || "").trim() || mainModel;
20
+ const fastModel = (env.PLUGCTO_FAST_MODEL || "").trim() || smartModel;
21
+ return {
22
+ primary: {
23
+ provider: /** @type {ProviderName} */ (provider),
24
+ key,
25
+ base,
26
+ model: smartModel,
27
+ smartModel,
28
+ fastModel,
29
+ },
30
+ };
31
+ }
32
+ // MODULE_COMPLETE
@@ -0,0 +1,25 @@
1
+ // src/core/llm-errors.js — Erreurs LLM user-friendly (Complexity Hiding) :
2
+ // message unique, clair et rassurant pour le HTTP 429 (quota du fournisseur —
3
+ // clé utilisée simultanément par plusieurs outils). Zéro jargon, zéro stack
4
+ // trace, zéro objet d'erreur JSON. Générique : TOUS les providers passent par
5
+ // parseLLMResponse (llm-extract.js), point d'étranglement unique.
6
+
7
+ /** Message utilisateur affiché en cas de refus de quota HTTP 429. */
8
+ export const RATE_LIMIT_MESSAGE =
9
+ "⏳ Limite de requêtes atteinte par votre fournisseur (trop d'IA sollicitées en même temps). Veuillez patienter quelques secondes.";
10
+
11
+ /** Vrai si l'erreur (enrichie par parseLLMResponse) est un refus de quota 429. */
12
+ /** @param {unknown} e @returns {boolean} */
13
+ export function isRateLimit(e) {
14
+ if (!e || typeof e !== "object") return false;
15
+ return (/** @type {{ status?: unknown }} */ (e)).status === 429;
16
+ }
17
+
18
+ /** Message à montrer à l'utilisateur : friendly si 429, sinon message nettoyé. */
19
+ /** @param {unknown} e @returns {string} */
20
+ export function friendlyError(e) {
21
+ if (isRateLimit(e)) return RATE_LIMIT_MESSAGE;
22
+ if (e && typeof e === "object") return String((/** @type {{ message?: unknown }} */ (e)).message || "Erreur inconnue.");
23
+ return "Erreur inconnue.";
24
+ }
25
+ // MODULE_COMPLETE
@@ -0,0 +1,27 @@
1
+ // src/core/llm-exchange.js — UN échange avec le fournisseur : Ollama → API native en streaming (llm-ollama) ; autre serveur local →
2
+ // transport IPv4 keepAlive ; cloud → fetch. Le texte généré est retourné, l'usage réel relevé. Erreurs nommées au bon fournisseur.
3
+
4
+ import { isLocalBase } from "./llm-label.js";
5
+ import { localFetch } from "../net/local-fetch.js";
6
+ import { buildLLMRequest } from "./llm-request.js";
7
+ import { parseLLMResponse } from "./llm-extract.js";
8
+ import { recordUsage } from "./usage-meter.js";
9
+ import { exchangeOllama } from "./llm-ollama.js";
10
+ import { isOllamaBase, isOllamaCloudBase } from "./ollama.js";
11
+ import { routeGoogle } from "./google-route.js";
12
+ import { exchangeStream } from "./llm-stream.js";
13
+
14
+ /** @param {{ provider: string, key: string, model: string, base: string, budget: number, unbounded?: boolean, systemPrompt: string, userText: string, stablePrefix: string, signal: AbortSignal, label: string, showThinking?: boolean, onDelta?: (text: string) => void }} input @returns {Promise<string>} */
15
+ export async function exchange(input) {
16
+ const p = { ...input, ...routeGoogle(input.provider, input.base) }; // Google : toujours l'API REST native (gemini, /v1beta, x-goog-api-key)
17
+ if (isOllamaBase(p.base)) return exchangeOllama(p);
18
+ if (isOllamaCloudBase(p.base)) return exchangeOllama({ ...p, cloud: true }); // service distinct : clé API, API native HTTPS
19
+ if (p.onDelta) return exchangeStream(/** @type {Parameters<typeof exchangeStream>[0]} */ (p)); // affichage jeton par jeton (chat)
20
+ const { url, init } = buildLLMRequest(p.provider, p);
21
+ const res = /** @type {Response} */ (/** @type {unknown} */ (await (isLocalBase(p.base) ? localFetch : fetch)(url, init)));
22
+ const body = await res.text();
23
+ const text = parseLLMResponse(p.provider, p.model, res, body, p.label, url); // url : route exacte citée dans les erreurs
24
+ recordUsage(p.provider, p.model, p.base, body); // usage RÉEL rapporté par l'API (réponse réussie seulement)
25
+ return text;
26
+ }
27
+ // MODULE_COMPLETE
@@ -0,0 +1,52 @@
1
+ // src/core/llm-extract.js — extraction du texte réponse LLM + parsing HTTP (status/erreur/vide).
2
+ // Schémas hétérogènes : OpenAI/Zhipu (choices), Anthropic (content blocks), Gemini (candidates).
3
+ // HTTP 429 (quota provider) → message user-friendly unique (llm-errors.js), zéro jargon.
4
+ import { RATE_LIMIT_MESSAGE } from "./llm-errors.js";
5
+ import { cutWhileThinking, thinkingBudgetError } from "./reasoning-cut.js";
6
+
7
+ // Extrait le texte selon le schéma du provider.
8
+ /** @param {string} provider @param {LLMResponse | null} j @returns {string} */
9
+ export function extractLLMText(provider, j) {
10
+ if (!j) return "";
11
+ if (provider === "openai" || provider === "zhipu") {
12
+ return j.choices?.[0]?.message?.content ?? "";
13
+ }
14
+ if (provider === "anthropic") {
15
+ const blocks = Array.isArray(j.content) ? j.content : [];
16
+ return blocks.filter((b) => b && b.type === "text").map((b) => b.text).join("\n");
17
+ }
18
+ if (provider === "gemini") {
19
+ const parts = j.candidates?.[0]?.content?.parts; // Gemini 2.5 : plusieurs parties possibles, celles de « réflexion » exclues
20
+ return Array.isArray(parts) ? parts.filter((x) => x && !x.thought && typeof x.text === "string").map((x) => x.text).join("") : "";
21
+ }
22
+ return "";
23
+ }
24
+
25
+ // Parse la réponse HTTP : jette une Error enrichie (status/provider) si !ok ou vide.
26
+ /** @param {string} provider @param {string} model @param {Response} res @param {string} bodyText @param {string} [label] fournisseur réellement appelé (adresse), pour les messages @returns {string} */
27
+ export function parseLLMResponse(provider, model, res, bodyText, label = provider, url = "") {
28
+ let parsed = null;
29
+ try { parsed = /** @type {LLMResponse | LLMErrorResponse | null} */ (JSON.parse(bodyText)); } catch { /* corps non-JSON : parsed reste null */ }
30
+ if (!res.ok) {
31
+ // Quota provider (429) : message clair et rassurant, JAMAIS le JSON brut du
32
+ // fournisseur ni un code HTTP (Complexity Hiding) — générique, tout provider.
33
+ if (res.status === 429) {
34
+ const rl = /** @type {EnrichedError} */ (new Error(RATE_LIMIT_MESSAGE));
35
+ rl.status = res.status;
36
+ rl.provider = provider;
37
+ throw rl;
38
+ }
39
+ const apiMsg = parsed && (/** @type {LLMErrorResponse} */ (parsed).error?.message || /** @type {LLMErrorResponse} */ (parsed).message);
40
+ const route = url ? ` — URL : ${String(url).split("?")[0]}` : ""; // route exacte appelée (sans paramètres : jamais de clé dans un message)
41
+ const err = /** @type {EnrichedError} */ (new Error(`${apiMsg || `HTTP ${res.status} de ${label} (modèle ${model})`}${route}`));
42
+ err.status = res.status;
43
+ err.provider = provider;
44
+ throw err;
45
+ }
46
+ const txt = extractLLMText(provider, /** @type {LLMResponse | null} */ (parsed));
47
+ if (typeof txt !== "string" || !txt.trim()) {
48
+ throw cutWhileThinking(parsed) ? thinkingBudgetError(`${label}, modèle ${model}`) : new Error(`Réponse vide de ${label} (modèle ${model}).`);
49
+ }
50
+ return txt;
51
+ }
52
+ // MODULE_COMPLETE
@@ -0,0 +1,33 @@
1
+ // src/core/llm-label.js — Libellé HONNÊTE du fournisseur dans les messages d'erreur (l'adaptateur « openai » sert aussi Ollama,
2
+ // Gemini, OpenRouter… : afficher « openai » trompait) + délai adapté (modèle local = chargement à froid long). Pur.
3
+
4
+ import { LLM_TIMEOUT_MS, LLM_LOCAL_TOTAL_MS } from "./constants.js";
5
+
6
+ /** @type {Array<[RegExp, string]>} */
7
+ const KNOWN = [[/(^|\.)openrouter\.ai$/, "OpenRouter"], [/(^|\.)generativelanguage\.googleapis\.com$/, "Google Gemini"], [/(^|\.)api\.openai\.com$/, "OpenAI"],
8
+ [/(^|\.)anthropic\.com$/, "Anthropic"], [/(^|\.)groq\.com$/, "Groq"], [/(^|\.)bigmodel\.cn$/, "Zhipu"], [/^(www\.)?ollama\.com$/, "Ollama Cloud"]];
9
+ const LOOPBACK = /^(localhost|127\.\d+\.\d+\.\d+|\[?::1\]?|0\.0\.0\.0)$/;
10
+
11
+ /** @param {string} base @returns {URL | null} */
12
+ function parse(base) {
13
+ try { return new URL(base); } catch { return null; } // adresse illisible : libellé de repli
14
+ }
15
+
16
+ /** Serveur local (boucle locale) : Ollama, LM Studio… @param {string} base @returns {boolean} */
17
+ export function isLocalBase(base) {
18
+ const u = parse(base);
19
+ return !!u && LOOPBACK.test(u.hostname);
20
+ }
21
+
22
+ /** « Ollama (localhost:11434) », « Google Gemini », « OpenRouter »… selon l'ADRESSE réellement appelée. @param {string} provider @param {string} base @returns {string} */
23
+ export function providerLabel(provider, base) {
24
+ const u = parse(base);
25
+ if (!u) return provider;
26
+ if (LOOPBACK.test(u.hostname)) return u.port === "11434" ? `Ollama (${u.host})` : `serveur local (${u.host})`;
27
+ const known = KNOWN.find(([re]) => re.test(u.hostname));
28
+ return known ? known[1] : u.host;
29
+ }
30
+
31
+ /** Délai d'attente : long pour un modèle local (chargement à froid), standard pour le cloud. @param {string} base @returns {number} */
32
+ export const timeoutFor = (base) => (isLocalBase(base) || providerLabel("", base) === "Ollama Cloud" ? LLM_LOCAL_TOTAL_MS : LLM_TIMEOUT_MS); // génération en streaming : durée totale longue
33
+ // MODULE_COMPLETE
@@ -0,0 +1,32 @@
1
+ // src/core/llm-ollama-request.js — Requête native Ollama /api/chat (streaming). Modèle à RAISONNEMENT (ou rejeu sans limite) : AUCUNE limite de
2
+ // sortie envoyée (pas de num_predict) — le modèle s'arrête seul sur son jeton de fin. Les autres gardent le budget demandé ; seul le chat LOCAL
3
+ // conserve un plafond de latence (2048). Pour un modèle local, CTX_RESERVE DIMENSIONNE la fenêtre (num_ctx), il n'est jamais envoyé comme limite.
4
+ import { numCtxFor, ollamaRoot } from "./ollama.js";
5
+ import { isReasoningModel } from "./reasoning-models.js";
6
+
7
+ const LOCAL_CHAT_CAP = 2048;
8
+ const CTX_RESERVE = 16384;
9
+
10
+ /** @param {{ model: string, unbounded?: boolean }} p @returns {boolean} */
11
+ export const unboundedFor = (p) => p.unbounded === true || isReasoningModel(p.model);
12
+
13
+ /** @param {{ model: string, unbounded?: boolean, budget: number, cloud?: boolean, showThinking?: boolean, systemPrompt: string }} p @param {string} user @returns {{ num_ctx?: number, num_predict?: number }} */
14
+ function optionsFor(p, user) {
15
+ const predict = unboundedFor(p) ? undefined : p.cloud || !p.showThinking ? p.budget : Math.min(p.budget, LOCAL_CHAT_CAP);
16
+ /** @type {{ num_predict?: number }} */ const limit = {};
17
+ if (predict !== undefined) limit.num_predict = predict; // absent = aucune limite
18
+ return p.cloud ? limit : { num_ctx: numCtxFor(p.systemPrompt.length + user.length, predict ?? CTX_RESERVE), ...limit };
19
+ }
20
+
21
+ /** @param {{ base: string, key?: string, cloud?: boolean, showThinking?: boolean, unbounded?: boolean, model: string, budget: number, systemPrompt: string, userText: string, stablePrefix: string, signal: AbortSignal }} p */
22
+ export function buildOllamaRequest(p) {
23
+ const user = p.stablePrefix ? `${p.stablePrefix}\n${p.userText}` : p.userText;
24
+ return {
25
+ url: `${ollamaRoot(p.base)}/api/chat`,
26
+ init: {
27
+ method: "POST", signal: p.signal, headers: { "Content-Type": "application/json", ...(p.cloud && p.key ? { Authorization: `Bearer ${p.key}` } : {}) },
28
+ body: JSON.stringify({ model: p.model, stream: true, messages: [{ role: "system", content: p.systemPrompt }, { role: "user", content: user }], options: optionsFor(p, user) }),
29
+ },
30
+ };
31
+ }
32
+ // MODULE_COMPLETE