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,42 @@
1
+ // src/core/llm-ollama.js — Échange avec Ollama (LOCAL ou CLOUD, deux services distincts) par l'API NATIVE /api/chat en STREAMING : le flux est lu
2
+ // au fil de l'eau par un parser à deux états (raisonnement / réponse) ; seul le texte de RÉPONSE est rendu, le raisonnement n'est transmis
3
+ // qu'au chat (showThinking). L'usage réel (tokens) est relevé comme pour les autres fournisseurs.
4
+
5
+ import { localFetch } from "../net/local-fetch.js";
6
+ import { buildOllamaRequest } from "./llm-ollama-request.js";
7
+ import { OllamaLineFeeder, parseOllamaStream } from "./ollama-lines.js";
8
+ import { THINKING } from "./dual-stream-parser.js";
9
+ import { thinkingBudgetError } from "./reasoning-cut.js";
10
+ import { recordUsage } from "./usage-meter.js";
11
+
12
+ export { buildOllamaRequest, parseOllamaStream };
13
+
14
+ /** Cloud : corps lu morceau par morceau ; sans flux lisible (simulacre) : texte d'un bloc. @param {Response} res @param {OllamaLineFeeder} feeder */
15
+ async function readInto(res, feeder) {
16
+ if (!res.body) { feeder.push(await res.text()); return; }
17
+ const decoder = new TextDecoder();
18
+ for await (const chunk of /** @type {AsyncIterable<Uint8Array>} */ (/** @type {unknown} */ (res.body))) feeder.push(decoder.decode(chunk, { stream: true }));
19
+ }
20
+
21
+ /** @param {{ provider: string, key?: string, cloud?: boolean, showThinking?: boolean, onDelta?: (text: string) => void, base: string, model: string, budget: number, systemPrompt: string, userText: string, stablePrefix: string, signal: AbortSignal, label: string }} p @returns {Promise<string>} */
22
+ export async function exchangeOllama(p) {
23
+ const { url, init } = buildOllamaRequest(p);
24
+ const feeder = new OllamaLineFeeder(p.onDelta);
25
+ const res = /** @type {Response} */ (/** @type {unknown} */ (p.cloud ? await fetch(url, init) : await localFetch(url, init, (c) => feeder.push(c)))); // cloud : HTTPS ; local : IPv4 keepAlive
26
+ if (!res.ok) {
27
+ const body = await res.text();
28
+ /** @type {EnrichedError} */ const e = new Error(`${parseOllamaError(body) || `HTTP ${res.status}`} — ${p.label} (modèle ${p.model})`);
29
+ e.status = res.status; e.provider = p.provider; throw e;
30
+ }
31
+ if (p.cloud) await readInto(res, feeder);
32
+ const { text, thinking, state, input, output, doneReason } = feeder.result();
33
+ if (!text.trim()) throw thinking.trim() && (state === THINKING || doneReason === "length") ? thinkingBudgetError(`${p.label}, modèle ${p.model}`) : new Error(`Réponse vide de ${p.label} (modèle ${p.model}).`);
34
+ recordUsage(p.provider, p.model, p.base, JSON.stringify({ usage: { prompt_tokens: input, completion_tokens: output } }));
35
+ return p.showThinking && thinking.trim() ? `<think>${thinking.trim()}</think>\n${text}` : text; // raisonnement transmis SEULEMENT au chat
36
+ }
37
+
38
+ /** @param {string} body @returns {string} */
39
+ function parseOllamaError(body) {
40
+ try { return String(JSON.parse(body).error || ""); } catch { return ""; } // corps non-JSON : statut HTTP seul
41
+ }
42
+ // MODULE_COMPLETE
@@ -0,0 +1,14 @@
1
+ // src/core/llm-prompts.js — Accès aux prompts d'orchestration (Phase 1 diagnostic, Phase 2 diff) + constantes
2
+ // de warmup. Les TEXTES des prompts vivent sur le serveur PipCode (lots de règles signés) : ici seulement des
3
+ // accesseurs lisant le registre mémoire. Sans règles chargées → RulesNotLoadedError (jamais de prompt inventé).
4
+
5
+ import { getRules } from "../rules/rules-registry.js";
6
+
7
+ /** @returns {string} prompt d'analyse (byte-stable pendant la durée de vie des règles en mémoire) */
8
+ export const analysisPrompt = () => getRules().prompts.analysis;
9
+ /** @returns {string} prompt de patch (lot Pro ; vide pour un compte gratuit) */
10
+ export const patchPrompt = () => getRules().prompts.patch;
11
+
12
+ export const WARMUP_USER_TEXT = "OK";
13
+ export const WARMUP_MAX_TOKENS = 16;
14
+ // MODULE_COMPLETE
@@ -0,0 +1,26 @@
1
+ // src/core/llm-providers.js — Catalogue providers + familles de modèles (smart/fast).
2
+ // OpenRouter/Ollama/proxy passent par l'adaptateur openai mais ne sont PAS OpenAI natif.
3
+
4
+ // Adresse (base URL) par défaut de chaque fournisseur. AUCUN modèle par défaut : le modèle est celui configuré par l'utilisateur.
5
+ /** @type {Record<string, { base: string }>} */
6
+ export const PROVIDER_DEFAULTS = {
7
+ openai: { base: "https://api.openai.com/v1" },
8
+ anthropic: { base: "https://api.anthropic.com/v1" },
9
+ gemini: { base: "https://generativelanguage.googleapis.com/v1beta" },
10
+ zhipu: { base: "https://open.bigmodel.cn/api/paas/v4" },
11
+ };
12
+
13
+ /** @param {string} p @returns {boolean} */
14
+ export function isValidProvider(p) {
15
+ return typeof p === "string" && Object.prototype.hasOwnProperty.call(PROVIDER_DEFAULTS, p);
16
+ }
17
+
18
+ // Normalise un host Ollama en base URL OpenAI-compatible (suffixe /v1, sans slash final).
19
+ /** @param {string} host @returns {string} */
20
+ export function normalizeOllamaBase(host) {
21
+ let h = String(host || "").trim().replace(/\/+$/, "");
22
+ if (!h) return "http://127.0.0.1:11434/v1";
23
+ if (!/\/v\d+$/.test(h)) h += "/v1";
24
+ return h;
25
+ }
26
+ // MODULE_COMPLETE
@@ -0,0 +1,17 @@
1
+ // src/core/llm-request.js — buildLLMRequest : dispatch vers le builder du provider
2
+ // (scission audit hebdo). openai/zhipu (chat/completions), anthropic (messages +
3
+ // cache_control ephemeral), gemini (generateContent). Builders : llm-builder-*.js.
4
+
5
+ /** @typedef {{ base: string, key: string, model: string, budget: number, unbounded?: boolean, stream?: boolean, systemPrompt: string, userText: string, stablePrefix: string, signal: AbortSignal }} LLMParams */
6
+
7
+ import { buildOpenAIStyle } from "./llm-builder-openai.js";
8
+ import { buildAnthropicStyle } from "./llm-builder-anthropic.js";
9
+ import { buildGeminiStyle } from "./llm-builder-gemini.js";
10
+
11
+ /** @param {string} provider @param {LLMParams} p @returns {{ url: string, init: RequestInit }} */
12
+ export function buildLLMRequest(provider, p) {
13
+ if (provider === "openai" || provider === "zhipu") return buildOpenAIStyle(p);
14
+ if (provider === "anthropic") return buildAnthropicStyle(p);
15
+ return buildGeminiStyle(p); // gemini (défaut)
16
+ }
17
+ // MODULE_COMPLETE
@@ -0,0 +1,23 @@
1
+ // src/core/llm-retry-429.js — Dépassement de quota / fréquence (HTTP 429) chez N'IMPORTE QUEL fournisseur : 3 nouvelles tentatives avec délai
2
+ // exponentiel + aléa (≈1 s, 2 s, 4 s), puis l'erreur d'origine est relancée, marquée « rateLimitExhausted » pour que les appelants ne
3
+ // recommencent pas la même série. Les autres erreurs passent telles quelles (aucune nouvelle tentative ici).
4
+
5
+ export const RETRY_429 = { retries: 3, baseMs: 1000, jitter: 0.25 };
6
+
7
+ /** @param {number} ms @returns {Promise<void>} */
8
+ const sleepMs = (ms) => new Promise((r) => setTimeout(r, ms));
9
+
10
+ /** @template T @param {() => Promise<T>} fn @param {{ sleep?: (ms: number) => Promise<void>, random?: () => number }} [deps] @returns {Promise<T>} */
11
+ export async function retryOn429(fn, { sleep = sleepMs, random = Math.random } = {}) {
12
+ for (let attempt = 0; ; attempt++) {
13
+ try { return await fn(); }
14
+ catch (err) {
15
+ const e = /** @type {EnrichedError} */ (err);
16
+ if (e?.status !== 429) throw err;
17
+ if (attempt >= RETRY_429.retries) { e.rateLimitExhausted = true; throw err; }
18
+ const base = RETRY_429.baseMs * 2 ** attempt;
19
+ await sleep(Math.round(base * (1 - RETRY_429.jitter + random() * 2 * RETRY_429.jitter)));
20
+ }
21
+ }
22
+ }
23
+ // MODULE_COMPLETE
@@ -0,0 +1,29 @@
1
+ // src/core/llm-stream-events.js — Un événement de flux (JSON) → texte de RÉPONSE, usage et erreur, selon la famille de fournisseur. Le raisonnement
2
+ // (reasoning_content, parties « thought ») n'est JAMAIS du texte de réponse. Entrée non fiable : JSON illisible ignoré, champs vérifiés.
3
+
4
+ /** @typedef {{ text: string, usage: Record<string, unknown> | null, error: string }} StreamAcc */
5
+
6
+ /** @param {unknown} v @returns {string} */
7
+ const str = (v) => (typeof v === "string" ? v : "");
8
+
9
+ /** @param {string} provider @param {string} data événement brut (JSON) @param {StreamAcc} acc @returns {string} texte nouveau (peut être "") */
10
+ export function applyStreamEvent(provider, data, acc) {
11
+ if (data.trim() === "[DONE]") return "";
12
+ /** @type {Record<string, any>} */ let j;
13
+ try { j = JSON.parse(data); } catch { return ""; } // morceau illisible : ignoré
14
+ if (j && typeof j === "object" && j.error) { acc.error = str(j.error.message) || "Erreur du fournisseur pendant le flux."; return ""; }
15
+ if (provider === "gemini") {
16
+ if (j.usageMetadata) acc.usage = { usageMetadata: j.usageMetadata };
17
+ const parts = j.candidates?.[0]?.content?.parts;
18
+ return Array.isArray(parts) ? parts.filter((x) => x && !x.thought).map((x) => str(x.text)).join("") : "";
19
+ }
20
+ if (provider === "anthropic") {
21
+ if (j.type === "message_start" && j.message?.usage) acc.usage = { usage: { ...j.message.usage } };
22
+ if (j.type === "message_delta" && j.usage) acc.usage = { usage: { ...(acc.usage?.usage ?? {}), ...j.usage } };
23
+ if (j.type === "error") acc.error = str(j.error?.message) || "Erreur du fournisseur pendant le flux.";
24
+ return j.type === "content_block_delta" && j.delta?.type === "text_delta" ? str(j.delta.text) : "";
25
+ }
26
+ if (j.usage) acc.usage = { usage: j.usage }; // OpenAI et compatibles (stream_options.include_usage)
27
+ return str(j.choices?.[0]?.delta?.content);
28
+ }
29
+ // MODULE_COMPLETE
@@ -0,0 +1,42 @@
1
+ // src/core/llm-stream.js — UN échange en FLUX (SSE) avec OpenAI et compatibles, Anthropic ou Gemini natif : chaque morceau de réponse est remis à `onDelta`
2
+ // pendant la génération (affichage jeton par jeton), le texte complet est rendu à la fin, l'usage réel relevé. Le raisonnement est écarté (parser à deux
3
+ // états : balises <think> comprises). Erreur HTTP → mêmes messages que l'échange classique (parseLLMResponse) ; l'annulation passe par le signal.
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 { SseParser } from "./sse-parser.js";
10
+ import { applyStreamEvent } from "./llm-stream-events.js";
11
+ import { DualStreamTokenParser } from "./dual-stream-parser.js";
12
+
13
+ /** Corps lu morceau par morceau ; sans flux lisible (simulacre) : texte d'un bloc. @param {Response} res @param {SseParser} sse */
14
+ async function readInto(res, sse) {
15
+ if (!res.body) { sse.push(await res.text()); return; }
16
+ const decoder = new TextDecoder();
17
+ for await (const chunk of /** @type {AsyncIterable<Uint8Array>} */ (/** @type {unknown} */ (res.body))) sse.push(decoder.decode(chunk, { stream: true }));
18
+ }
19
+
20
+ /** @param {{ provider: string, key: string, model: string, base: string, budget: number, unbounded?: boolean, systemPrompt: string, userText: string, stablePrefix: string, signal: AbortSignal, label: string, onDelta: (text: string) => void }} p @returns {Promise<string>} */
21
+ export async function exchangeStream(p) {
22
+ const { url, init } = buildLLMRequest(p.provider, { ...p, stream: true });
23
+ /** @type {import("./llm-stream-events.js").StreamAcc} */ const acc = { text: "", usage: null, error: "" };
24
+ const answer = new DualStreamTokenParser();
25
+ let sent = 0;
26
+ const sse = new SseParser((data) => {
27
+ answer.content(applyStreamEvent(p.provider, data, acc)); // seul le texte de RÉPONSE avance (raisonnement écarté)
28
+ if (answer.payload.length > sent) { p.onDelta(answer.payload.slice(sent)); sent = answer.payload.length; }
29
+ });
30
+ const local = isLocalBase(p.base);
31
+ const res = /** @type {Response} */ (/** @type {unknown} */ (local ? await localFetch(url, init, (c) => sse.push(c)) : await fetch(url, init)));
32
+ if (!res.ok) { parseLLMResponse(p.provider, p.model, res, await res.text(), p.label, url); throw new Error(`HTTP ${res.status} de ${p.label} (modèle ${p.model}) — URL : ${url.split("?")[0]}`); }
33
+ if (!local) await readInto(res, sse);
34
+ sse.end();
35
+ const text = answer.end().payload;
36
+ if (acc.error) throw new Error(`${acc.error} — ${p.label} (modèle ${p.model})`);
37
+ if (!text.trim()) throw new Error(`Réponse vide de ${p.label} (modèle ${p.model}).`);
38
+ if (answer.payload.length > sent) p.onDelta(answer.payload.slice(sent)); // reste retenu par le parser (balise coupée en fin de flux)
39
+ if (acc.usage) recordUsage(p.provider, p.model, p.base, JSON.stringify(acc.usage));
40
+ return text;
41
+ }
42
+ // MODULE_COMPLETE
@@ -0,0 +1,15 @@
1
+ // src/core/llm-unbounded.js — Échange SANS limite de sortie pour les modèles à raisonnement. Un modèle que la liste ne connaît pas mais qui
2
+ // se fait couper PENDANT sa réflexion (erreur typée thinking_budget) est relancé UNE fois sans limite : jamais de limite de remplacement.
3
+ import { exchange } from "./llm-exchange.js";
4
+ import { isReasoningModel } from "./reasoning-models.js";
5
+
6
+ /** @param {Parameters<typeof exchange>[0]} params @returns {Promise<string>} */
7
+ export async function exchangeUnbounded(params) {
8
+ const known = isReasoningModel(params.model);
9
+ try { return await exchange({ ...params, unbounded: known }); }
10
+ catch (e) {
11
+ if (known || /** @type {EnrichedError} */ (e)?.code !== "thinking_budget") throw e;
12
+ return exchange({ ...params, unbounded: true });
13
+ }
14
+ }
15
+ // MODULE_COMPLETE
@@ -0,0 +1,44 @@
1
+ // src/core/llm-warmup.js — warmupCache : pré-charge les préfixes byte-stables (Anthropic prompt caching).
2
+ // Amorce le cache ephemeral au boot (ANALYSIS/PATCH prompts + missionContext). Best-effort, silencieux.
3
+ import { callLLM } from "./llm-call.js";
4
+ import { analysisPrompt, patchPrompt, WARMUP_USER_TEXT, WARMUP_MAX_TOKENS } from "./llm-prompts.js";
5
+
6
+ // Pré-charge le cache prompt Anthropic. Inactif si PLUGCTO_CACHE_WARMUP=0 ou pas d'Anthropic.
7
+ /** @param {{ primary: ProviderPrimary } | null} pcfg @param {{ callLLM?: typeof callLLM, missionContext?: string }} [opts] @returns {Promise<WarmupResult>} */
8
+ export async function warmupCache(pcfg, opts = {}) {
9
+ const call = typeof opts.callLLM === "function" ? opts.callLLM : callLLM;
10
+ const missionContext = typeof opts.missionContext === "string" ? opts.missionContext : "";
11
+ if (process.env.PLUGCTO_CACHE_WARMUP === "0") {
12
+ return { warmed: false, reason: "disabled", primed: [] };
13
+ }
14
+ const providers = /** @type {ProviderPrimary[]} */ ([pcfg && pcfg.primary].filter((p) => p && p.provider === "anthropic"));
15
+ if (providers.length === 0) {
16
+ return { warmed: false, reason: "no_anthropic", primed: [] };
17
+ }
18
+ /** @type {{ name: string, systemPrompt: string }[]} */ let targets;
19
+ try { targets = [{ name: "analysis", systemPrompt: analysisPrompt() }, { name: "patch", systemPrompt: patchPrompt() }].filter((t) => t.systemPrompt); }
20
+ catch { return { warmed: false, reason: "rules_not_loaded", primed: [] }; }
21
+ /** @type {WarmupPrimedEntry[]} */
22
+ const primed = [];
23
+ const tasks = [];
24
+ for (const p of providers) {
25
+ for (const t of targets) {
26
+ tasks.push(
27
+ Promise.resolve()
28
+ .then(() => call({
29
+ provider: "anthropic", key: p.key, model: p.model, base: p.base,
30
+ systemPrompt: t.systemPrompt, cachePrefix: missionContext,
31
+ userText: WARMUP_USER_TEXT, maxTokens: WARMUP_MAX_TOKENS,
32
+ }))
33
+ .then(() => primed.push({ provider: "anthropic", model: p.model, target: t.name, ok: true }))
34
+ .catch((err) => primed.push({
35
+ provider: "anthropic", model: p.model, target: t.name, ok: false,
36
+ error: (err && err.message) || String(err),
37
+ }))
38
+ );
39
+ }
40
+ }
41
+ await Promise.all(tasks);
42
+ return { warmed: primed.some((p) => p.ok), reason: "done", primed };
43
+ }
44
+ // MODULE_COMPLETE
@@ -0,0 +1,42 @@
1
+ // src/core/manifest.js — Catalogue statique d'outils MCP (métadonnées hors-ligne).
2
+ // Une entrée = un outil MCP. slug → fetch cloud signé ; tier = étiquette MIROIR
3
+ // de src/worker/skill-tiers.js (anti-désynchronisation : test miroir).
4
+ // Gate client réelle : sans licence, isWithinTrial (horloge locale, UX SEULEMENT)
5
+ // ouvre tout ; hors essai → seules les fiches tier "lite" passent (handle-skill.js).
6
+ // L'autorité monétaire RÉELLE est le serveur (SKILL_TIERS + trialGate worker) —
7
+ // cette étiquette ne fait que refléter le catalogue payant, jamais accorder.
8
+
9
+ /** @typedef {{ name: string, slug: string, tier: string, title: string }} ManifestEntry */
10
+
11
+ /** @type {ManifestEntry[]} */
12
+ export const MANIFEST = [
13
+ { name: "enforce_readme", slug: "README", tier: "pro", title: "Skills — Virtual CTO (Standard V-CTO, extrait cognitif)" },
14
+ { name: "enforce_core_rules", slug: "core_rules", tier: "pro", title: "Règles Absolues du CTO — Standard V-CTO (couches Rules)" },
15
+ { name: "enforce_cto_pillars", slug: "cto_pillars", tier: "pro", title: "Les 7 Piliers du CTO — Grille d'audit interne" },
16
+ { name: "enforce_orchestration_pipeline", slug: "orchestration_pipeline", tier: "pro", title: "Pipeline d'Orchestration & Cycle DevSecOps — 5 Agents" },
17
+ { name: "enforce_security_auditor", slug: "security_auditor", tier: "pro", title: "Persona : Auditeur DevSecOps — Traque des Vulnérabilités avant Production" },
18
+ { name: "enforce_persona_architect", slug: "persona_architect", tier: "pro", title: "Persona : Architecte Backend & Auditeur DevSecOps (CTO Virtuel)" },
19
+ { name: "enforce_persona_intake", slug: "persona_intake", tier: "pro", title: "Persona : Directeur Technique d'Agence Web (Intake)" },
20
+ { name: "enforce_persona_sql_architect", slug: "persona_sql_architect", tier: "pro", title: "Persona : Architecte SQL Automatisé" },
21
+ { name: "enforce_persona_game_dev_router", slug: "persona_game_dev_router", tier: "pro", title: "Persona : CTO Routeur Game Development Engineering" },
22
+ { name: "enforce_game_dev_standards", slug: "game_dev_standards", tier: "pro", title: "Standards Game Development Engineering — 5 Sous-Rôles" },
23
+ { name: "enforce_standards_by_category", slug: "standards_by_category", tier: "pro", title: "Standards par Catégorie de Projet" },
24
+ { name: "enforce_token_optimization", slug: "token_optimization", tier: "lite", title: "Optimisation des Tokens — Prompt Caching Statique & Context Trimming" },
25
+ { name: "enforce_defensive_engineering", slug: "defensive_engineering", tier: "pro", title: "Ingénierie Défensive — Couche d'Anticipation des Bugs & Sécurisation" },
26
+ { name: "enforce_appsec_compliance", slug: "appsec_compliance", tier: "pro", title: "AppSec & Conformité — Intégration Sécurité Logicielle et Cadres Réglementaires" },
27
+ { name: "enforce_data_mlops_pipeline", slug: "data_mlops_pipeline", tier: "pro", title: "Data & MLOps Pipeline — Ingénierie des Données et Cycle de Vie des Modèles" },
28
+ { name: "enforce_devex_documentation", slug: "devex_documentation", tier: "pro", title: "Developer Experience & Documentation — Doc-as-Code et Onboarding Mesurable" },
29
+ { name: "enforce_finops_governance", slug: "finops_governance", tier: "pro", title: "FinOps & Governance — Maîtrise des Coûts Cloud et Gouvernance Technique" },
30
+ { name: "enforce_delivery_scale", slug: "delivery_scale", tier: "pro", title: "Delivery & Scale — Livraison Continue, Observabilité et Scalabilité" },
31
+ { name: "enforce_design_ux", slug: "design_ux", tier: "pro", title: "Design & Interface — Direction Visuelle, Accessibilité et Vérification du Rendu" },
32
+ ];
33
+
34
+ /** @type {Map<string, ManifestEntry>} */
35
+ export const toolsByName = new Map(MANIFEST.map((m) => [m.name, m]));
36
+
37
+ // Valide le nom d'un outil MCP (anti-injection de nom : préfixe enforce_ + [a-z0-9_]).
38
+ /** @param {string} name @returns {boolean} */
39
+ export function isValidToolName(name) {
40
+ return typeof name === "string" && /^enforce_[a-z0-9_]+$/.test(name);
41
+ }
42
+ // MODULE_COMPLETE
@@ -0,0 +1,11 @@
1
+ // src/core/master-prompt.js — Master Prompt blindé (séparation des privilèges, sandboxing <CTO_VAULT>,
2
+ // anti-fuite). Le GABARIT vit sur le serveur PipCode (lot de règles signé) ; ici seulement l'insertion de la
3
+ // fiche à l'emplacement prévu. Aucune interpolation dynamique hors skillContent (préfixe byte-stable).
4
+ import { getRules } from "../rules/rules-registry.js";
5
+ import { SKILL_PLACEHOLDER } from "../rules/rules-validate.js";
6
+
7
+ /** split/join : le contenu de la fiche est inséré tel quel (aucun motif `$&` interprété). @param {string} skillContent @returns {string} */
8
+ export function buildMasterPrompt(skillContent) {
9
+ return getRules().prompts.master.split(SKILL_PLACEHOLDER).join(skillContent);
10
+ }
11
+ // MODULE_COMPLETE
@@ -0,0 +1,19 @@
1
+ // src/core/model-id.js — Identifiant de modèle ASSAINI juste avant toute requête : Google liste ses modèles « models/gemini-… » (liste
2
+ // OpenAI-compatible comprise) mais n'accepte que « gemini-… » dans le corps de la requête ET dans l'URL native (qui contient déjà /models/) ;
3
+ // le préfixe provoquait des 404. Le préfixe « google/ » (OpenRouter) est retiré de la même façon, mais UNIQUEMENT pour l'hôte Google : chez OpenRouter
4
+ // « google/gemini-… » est l'identifiant légitime et doit rester intact.
5
+
6
+ /** @param {string} rawModel @returns {string} identifiant nu, « gemini-… » */
7
+ export function sanitizeGeminiModelId(rawModel) {
8
+ if (!rawModel) return "";
9
+ return String(rawModel).trim().replace(/^(models\/|google\/)+/i, "").trim();
10
+ }
11
+
12
+ /** @param {string} base adresse du fournisseur @param {string} model @returns {string} */
13
+ export function sanitizeModelId(base, model) {
14
+ const id = String(model ?? "").trim();
15
+ let host = "";
16
+ try { host = new URL(String(base)).hostname; } catch { return id; } // adresse illisible : identifiant inchangé
17
+ return host === "generativelanguage.googleapis.com" ? sanitizeGeminiModelId(id) : id;
18
+ }
19
+ // MODULE_COMPLETE
@@ -0,0 +1,48 @@
1
+ // src/core/ollama-lines.js — Flux Ollama (une ligne JSON par morceau) → parser à deux états. Alimenté au fil de l'eau (télémétrie du
2
+ // raisonnement) ou d'un coup. Une ligne illisible est ignorée ; une ligne d'erreur d'Ollama est retenue et levée à result().
3
+ import { DualStreamTokenParser, THINKING } from "./dual-stream-parser.js";
4
+ import { emitProgress } from "./progress-emit.js";
5
+
6
+ export class OllamaLineFeeder {
7
+ /** @param {(text: string) => void} [onText] texte de RÉPONSE au fil de l'eau (jamais le raisonnement) */
8
+ constructor(onText) { this.onText = onText; this.sent = 0; this.parser = new DualStreamTokenParser(); this.buf = ""; this.input = 0; this.output = 0; this.doneReason = ""; this.error = ""; this.last = ""; }
9
+
10
+ /** @param {string} chunk */
11
+ push(chunk) {
12
+ const lines = (this.buf + chunk).split("\n");
13
+ this.buf = lines.pop() ?? "";
14
+ for (const line of lines) this.#line(line);
15
+ }
16
+
17
+ /** @param {string} raw */
18
+ #line(raw) {
19
+ if (!raw.trim()) return;
20
+ /** @type {{ error?: string, done_reason?: string, message?: { content?: string, thinking?: string }, prompt_eval_count?: number, eval_count?: number }} */ let o;
21
+ try { o = JSON.parse(raw); } catch { return; } // ligne illisible : ignorée
22
+ if (o.error) { this.error = String(o.error); return; }
23
+ this.parser.reason(o.message?.thinking ?? "");
24
+ this.parser.content(o.message?.content ?? "");
25
+ if (this.onText && this.parser.payload.length > this.sent) { this.onText(this.parser.payload.slice(this.sent)); this.sent = this.parser.payload.length; }
26
+ this.input = o.prompt_eval_count ?? this.input; this.output = o.eval_count ?? this.output; this.doneReason = o.done_reason ?? this.doneReason;
27
+ if (this.parser.state === THINKING) emitProgress({ k: "thinking", n: Math.round(this.parser.thinking.length / 4) });
28
+ else { emitProgress({ k: "tick" }); if (this.last === THINKING) emitProgress({ k: "payload" }); }
29
+ this.last = this.parser.state;
30
+ }
31
+
32
+ /** @returns {{ text: string, thinking: string, state: string, input: number, output: number, doneReason: string }} */
33
+ result() {
34
+ this.#line(this.buf); this.buf = "";
35
+ if (this.error) throw new Error(this.error);
36
+ const r = this.parser.end();
37
+ return { text: r.payload, thinking: r.thinking, state: r.state, input: this.input, output: this.output, doneReason: this.doneReason };
38
+ }
39
+ }
40
+
41
+ /** Corps complet → { text, thinking, input, output } ; une ligne d'erreur d'Ollama lève. @param {string} body @returns {{ text: string, thinking: string, input: number, output: number }} */
42
+ export function parseOllamaStream(body) {
43
+ const f = new OllamaLineFeeder();
44
+ f.push(body);
45
+ const { text, thinking, input, output } = f.result();
46
+ return { text, thinking, input, output };
47
+ }
48
+ // MODULE_COMPLETE
@@ -0,0 +1,43 @@
1
+ // src/core/ollama.js — Ollama (127.0.0.1:11434) : détection, racine de l'API native, fenêtre de contexte demandée (num_ctx) et budget
2
+ // de prompt réduit. Un modèle local évalue le prompt lentement : on lui envoie le STRICT utile. num_ctx est choisi pour que le prompt
3
+ // et la réponse tiennent (jamais de troncature silencieuse par Ollama), par défaut 8192 (PLUGCTO_OLLAMA_NUM_CTX : 4096 à 131072).
4
+ import { ipv4Loopback } from "../net/loopback.js";
5
+
6
+ const MIN_CTX = 4096; const MAX_CTX = 131_072; const DEFAULT_CTX = 8192;
7
+ const CHARS_PER_TOKEN = 3; // prudent : sous-estimer les caractères par token évite la troncature
8
+
9
+ /** Serveur Ollama = boucle locale IPv4 sur le port 11434. @param {string} base @returns {boolean} */
10
+ export function isOllamaBase(base) {
11
+ try { const u = new URL(ipv4Loopback(String(base))); return u.hostname === "127.0.0.1" && u.port === "11434"; } catch { return false; } // adresse illisible : pas Ollama
12
+ }
13
+
14
+ /** Ollama CLOUD (ollama.com) : service distinct d'Ollama local — clé API, API native en HTTPS. @param {string} base @returns {boolean} */
15
+ export function isOllamaCloudBase(base) {
16
+ try { return /^(www\.)?ollama\.com$/i.test(new URL(String(base)).hostname); } catch { return false; } // adresse illisible : pas Ollama cloud
17
+ }
18
+
19
+ /** « http://127.0.0.1:11434/v1 » ou « https://ollama.com/api » → racine de l'API native. @param {string} base @returns {string} */
20
+ export const ollamaRoot = (base) => ipv4Loopback(String(base)).replace(/\/+$/, "").replace(/\/(v\d+|api)$/, "");
21
+
22
+ /** @param {Record<string, string | undefined>} [env] @returns {number} fenêtre de base demandée */
23
+ export function baseNumCtx(env = process.env) {
24
+ const n = Number.parseInt(env.PLUGCTO_OLLAMA_NUM_CTX || "", 10);
25
+ return Number.isFinite(n) && n >= MIN_CTX && n <= MAX_CTX ? n : DEFAULT_CTX;
26
+ }
27
+
28
+ /** num_ctx suffisant pour ce prompt + la réponse (arrondi au Ko), jamais sous la base. @param {number} promptChars @param {number} predict @returns {number} */
29
+ export function numCtxFor(promptChars, predict) {
30
+ const needed = Math.ceil(promptChars / CHARS_PER_TOKEN) + predict;
31
+ return Math.min(MAX_CTX, Math.max(baseNumCtx(), Math.ceil(needed / 1024) * 1024));
32
+ }
33
+
34
+ /** Budget (caractères) du CONTEXTE PROJET injecté dans le chat pour Ollama : ~1/4 de la fenêtre (le reste : consignes, historique, réponse). @returns {number} */
35
+ export const ollamaPromptChars = () => Math.floor(baseNumCtx() * CHARS_PER_TOKEN * 0.25);
36
+
37
+ /** Garde les lignes les plus RÉCENTES dans la limite. @param {string} text @param {number} max @returns {string} */
38
+ export function keepRecentLines(text, max) {
39
+ const kept = []; let used = 0;
40
+ for (const l of text.split("\n").reverse()) { if (used + l.length + 1 > max) break; used += l.length + 1; kept.unshift(l); }
41
+ return kept.join("\n");
42
+ }
43
+ // MODULE_COMPLETE
@@ -0,0 +1,35 @@
1
+ // src/core/pkg-manager.js — Agnosticisme des gestionnaires de paquets (Point #3).
2
+ // Détection via npm_config_user_agent (renseigné par npm/pnpm/yarn/bun). Jamais
3
+ // de conditionnement exclusif à npm : les routines setup/teardown sont 100 %
4
+ // Node.js pur — la détection ne sert qu'à l'affichage et à l'aide contextuelle.
5
+
6
+ /** @typedef {"npm" | "pnpm" | "yarn" | "bun" | "unknown"} PmName */
7
+ /** @typedef {{ name: PmName, label: string }} PmInfo */
8
+
9
+ // Étiquettes affichables + équivalent de `npx` par gestionnaire (aide dynamique).
10
+ /** @type {{ [k: string]: { label: string, run: string } }} */
11
+ const PM_META = {
12
+ npm: { label: "npm", run: "npx" },
13
+ pnpm: { label: "pnpm", run: "pnpm dlx" },
14
+ yarn: { label: "yarn", run: "yarn dlx" },
15
+ bun: { label: "bun", run: "bunx" },
16
+ };
17
+
18
+ // Détecte le gestionnaire actif : npm_config_user_agent commence par "<pm>/<version>".
19
+ /** @param {NodeJS.ProcessEnv} [env] @returns {PmInfo} */
20
+ export function detectPackageManager(env = process.env) {
21
+ const ua = String(env.npm_config_user_agent || "");
22
+ const name = (ua.match(/^(npm|pnpm|yarn|bun)\//) || [])[1] || "unknown";
23
+ const meta = PM_META[name];
24
+ return meta
25
+ ? { name: /** @type {PmName} */ (name), label: meta.label }
26
+ : { name: "unknown", label: "non détecté" };
27
+ }
28
+
29
+ // Commande d'exécution équivalente à `npx` pour le gestionnaire détecté.
30
+ /** @param {PmInfo} [pm] @returns {string} */
31
+ export function pmRunCommand(pm = detectPackageManager()) {
32
+ const meta = PM_META[pm.name];
33
+ return meta ? meta.run : "npx";
34
+ }
35
+ // MODULE_COMPLETE
@@ -0,0 +1,47 @@
1
+ // src/core/process-guards.js — Handlers process : aucun crash silencieux,
2
+ // aucun survie en état incohérent : exception ou rejet non géré → log stderr
3
+ // puis exit 1 (fail-fast, aligné sur uncaughtException).
4
+
5
+ // Enregistre les handlers globaux une seule fois (drapeau interne : un double
6
+ // appel n'empilerait pas des listeners en doublon).
7
+ let _installed = false;
8
+
9
+ // Nettoyage best-effort armé par le mode Auto pendant une opération git
10
+ // critique (apply/commit) : un Ctrl+C déclenche le gitRestore de secours.
11
+ /** @type {(() => Promise<void>) | null} */
12
+ let _interruptCleanup = null;
13
+
14
+ /** Arme (ou désarme avec null) le nettoyage d'interruption. @param {(() => Promise<void>) | null} fn */
15
+ export function setInterruptCleanup(fn) {
16
+ _interruptCleanup = fn;
17
+ }
18
+
19
+ // Exécute le nettoyage armé puis sort (codes convention shell : 130 SIGINT,
20
+ // 143 SIGTERM). Best-effort : un nettoyage qui échoue ne masque pas l'exit.
21
+ /** @param {NodeJS.Signals} signal @param {(code: number) => void} [exit] @returns {Promise<void>} */
22
+ export async function runInterruptCleanup(signal, exit = process.exit) {
23
+ const code = signal === "SIGTERM" ? 143 : 130;
24
+ if (_interruptCleanup) {
25
+ try { await _interruptCleanup(); } catch (err) {
26
+ console.error(`[virtual-cto] Nettoyage ${signal} échoué (best-effort) :`, err);
27
+ }
28
+ }
29
+ exit(code);
30
+ }
31
+
32
+ export function installProcessGuards() {
33
+ if (_installed) return;
34
+ _installed = true;
35
+ process.on("unhandledRejection", (reason) => {
36
+ console.error("[virtual-cto] Rejet non géré (unhandledRejection) :", reason);
37
+ process.exit(1);
38
+ });
39
+ process.on("uncaughtException", (err) => {
40
+ console.error("[virtual-cto] Exception non capturée (uncaughtException) :", err);
41
+ process.exit(1);
42
+ });
43
+ // SIGINT/SIGTERM : nettoyage armé de secours avant exit (codes 130/143).
44
+ process.on("SIGINT", () => { void runInterruptCleanup("SIGINT"); });
45
+ process.on("SIGTERM", () => { void runInterruptCleanup("SIGTERM"); });
46
+ }
47
+ // MODULE_COMPLETE
@@ -0,0 +1,25 @@
1
+ // src/core/progress-emit.js — Télémétrie ÉPHÉMÈRE moteur → interface : une ligne « @@VCTO_PROGRESS@@{json} » sur stderr, seulement si
2
+ // l'interface l'a demandé (PLUGCTO_PROGRESS=1). Jamais le contenu du raisonnement : uniquement son avancement (tokens estimés). Limitée à 1/s.
3
+
4
+ export const PROGRESS_PREFIX = "@@VCTO_PROGRESS@@";
5
+ const THROTTLE_MS = 1000;
6
+ /** @type {Record<string, number>} */ const last = {};
7
+
8
+ /** @typedef {{ k: "thinking", n: number } | { k: "payload" } | { k: "tick" } | { k: "stage", s: "rules" | "project" | "sheets" | "model" } | { k: "delta", t: string } | { k: "task", state: "start" | "done" | "failed", id: string, title: string, n: number, total: number }} ProgressEvent */
9
+
10
+ /** @param {ProgressEvent} event @param {number} [now] @param {{ write: (s: string) => unknown }} [out] */
11
+ export function emitProgress(event, now = Date.now(), out = process.stderr) {
12
+ if (process.env.PLUGCTO_PROGRESS !== "1") return;
13
+ if (event.k === "thinking" || event.k === "tick") { if (now - (last[event.k] ?? 0) < THROTTLE_MS) return; last[event.k] = now; }
14
+ out.write(`${PROGRESS_PREFIX}${JSON.stringify(event)}\n`);
15
+ }
16
+
17
+ /** Morceaux de réponse regroupés (≈ 40 ms) en un seul événement « delta » : un jeton par ligne saturerait le canal. Sans demande de l'interface : null.
18
+ * @param {(event: ProgressEvent) => void} [emit] @param {() => number} [now] @returns {{ push: (text: string) => void, flush: () => void } | null} */
19
+ export function createDeltaEmitter(emit = emitProgress, now = Date.now) {
20
+ if (process.env.PLUGCTO_PROGRESS !== "1") return null;
21
+ let buf = ""; let last = 0;
22
+ const flush = () => { if (buf) { emit({ k: "delta", t: buf }); buf = ""; } last = now(); };
23
+ return { push: (text) => { buf += text; if (now() - last >= 40) flush(); }, flush };
24
+ }
25
+ // MODULE_COMPLETE
@@ -0,0 +1,20 @@
1
+ // src/core/purge-key.js — Purge Anti-Hostage (SecOps monétisation).
2
+ // Essai expiré ou licence invalidée (402/403/429 du Worker) → effacement
3
+ // INSTANTANÉ de la clé de licence de TOUT stockage : config.json
4
+ // (licenseKey) + cache RAM du token de session. Idempotent. La clé BYOK
5
+ // provider n'est JAMAIS touchée (propriété de l'utilisateur).
6
+
7
+ import { saveConfig } from "./config-base.js";
8
+ import { purgeSessionCache } from "./session-net.js";
9
+ import { purgeTrialCache } from "./trial-session.js";
10
+
11
+ /** @param {PlugConfig} cfg @param {string} reason @returns {Promise<void>} */
12
+ export async function purgeLicenseKey(cfg, reason) {
13
+ purgeSessionCache(); // cache RAM du token : vidé avant toute autre action.
14
+ purgeTrialCache(); // cache RAM du jeton d'essai : vidé lui aussi (anti-hostage).
15
+ if (!cfg || !cfg.licenseKey) return;
16
+ cfg.licenseKey = "";
17
+ await saveConfig(cfg);
18
+ console.error(`[virtual-cto] Licence purgée du stockage (anti-hostage) : ${reason}`);
19
+ }
20
+ // MODULE_COMPLETE
@@ -0,0 +1,19 @@
1
+ // src/core/reasoning-cut.js — Réponse OpenAI-compatible coupée PAR LA LIMITE (finish_reason « length ») alors que le modèle en était encore à
2
+ // réfléchir (raisonnement présent, réponse vide) : message dédié plutôt qu'un « réponse vide » trompeur.
3
+
4
+ export const THINKING_BUDGET_MESSAGE = "Dépassement du budget de réflexion initial. Réessayez sans bridage de tokens.";
5
+
6
+ /** Erreur typée « budget de réflexion » : l'appelant peut relancer SANS limite. @param {string} where @returns {EnrichedError} */
7
+ export function thinkingBudgetError(where) {
8
+ /** @type {EnrichedError} */ const e = new Error(`${THINKING_BUDGET_MESSAGE} (${where})`);
9
+ e.code = "thinking_budget";
10
+ return e;
11
+ }
12
+
13
+ /** @param {unknown} parsed réponse JSON du fournisseur @returns {boolean} */
14
+ export function cutWhileThinking(parsed) {
15
+ const choice = /** @type {{ choices?: { finish_reason?: unknown, message?: { reasoning_content?: unknown, reasoning?: unknown } }[] }} */ (parsed)?.choices?.[0];
16
+ const reasoning = choice?.message?.reasoning_content ?? choice?.message?.reasoning;
17
+ return choice?.finish_reason === "length" && typeof reasoning === "string" && reasoning.trim() !== "";
18
+ }
19
+ // MODULE_COMPLETE
@@ -0,0 +1,8 @@
1
+ // src/core/reasoning-models.js — Modèles à RAISONNEMENT (GLM, DeepSeek-R1, Kimi, gpt-oss, QwQ, Qwen3, Gemini 2.5/3…) : ils dépensent des tokens à
2
+ // réfléchir AVANT de répondre ; toute limite de sortie les laisse sans réponse. Pour eux AUCUNE limite n'est envoyée (voir les builders).
3
+
4
+ const REASONING = /(glm[-_]?[4-9]|deepseek[-_]?(r1|reasoner)|kimi|k2[-_]?thinking|qwq|qwen3|gpt[-_]?oss|thinking|reasoner|gemini[-_]?(2\.5|3)|(^|[^a-z0-9])o[134](-|$))/i;
5
+
6
+ /** @param {unknown} model @returns {boolean} */
7
+ export const isReasoningModel = (model) => typeof model === "string" && REASONING.test(model);
8
+ // MODULE_COMPLETE