@kr78/pi-coding-agent 17.2.10

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 (1593) hide show
  1. package/CHANGELOG.md +14861 -0
  2. package/README.md +35 -0
  3. package/dist/CHANGELOG-chtdxh1z.md +14861 -0
  4. package/dist/cli.js +20483 -0
  5. package/dist/template-c2hyaytt.js +1653 -0
  6. package/dist/template-f8wx9vfn.css +1355 -0
  7. package/dist/template-qat058wr.html +55 -0
  8. package/dist/tool-views.generated-jdfmzwmn.js +35 -0
  9. package/examples/README.md +21 -0
  10. package/examples/custom-tools/README.md +104 -0
  11. package/examples/custom-tools/hello/index.ts +20 -0
  12. package/examples/extensions/README.md +142 -0
  13. package/examples/extensions/api-demo.ts +78 -0
  14. package/examples/extensions/chalk-logger.ts +25 -0
  15. package/examples/extensions/hello.ts +31 -0
  16. package/examples/extensions/pirate.ts +43 -0
  17. package/examples/extensions/plan-mode.ts +549 -0
  18. package/examples/extensions/reload-runtime.ts +38 -0
  19. package/examples/extensions/thinking-note.ts +13 -0
  20. package/examples/extensions/tools.ts +145 -0
  21. package/examples/extensions/with-deps/index.ts +36 -0
  22. package/examples/extensions/with-deps/package-lock.json +31 -0
  23. package/examples/extensions/with-deps/package.json +17 -0
  24. package/examples/hooks/README.md +56 -0
  25. package/examples/hooks/auto-commit-on-exit.ts +48 -0
  26. package/examples/hooks/confirm-destructive.ts +58 -0
  27. package/examples/hooks/custom-compaction.ts +115 -0
  28. package/examples/hooks/dirty-repo-guard.ts +51 -0
  29. package/examples/hooks/file-trigger.ts +40 -0
  30. package/examples/hooks/git-checkpoint.ts +52 -0
  31. package/examples/hooks/handoff.ts +149 -0
  32. package/examples/hooks/permission-gate.ts +33 -0
  33. package/examples/hooks/protected-paths.ts +29 -0
  34. package/examples/hooks/qna.ts +118 -0
  35. package/examples/hooks/status-line.ts +39 -0
  36. package/examples/sdk/01-minimal.ts +21 -0
  37. package/examples/sdk/02-custom-model.ts +49 -0
  38. package/examples/sdk/03-custom-prompt.ts +46 -0
  39. package/examples/sdk/04-skills.ts +43 -0
  40. package/examples/sdk/06-extensions.ts +82 -0
  41. package/examples/sdk/06-hooks.ts +61 -0
  42. package/examples/sdk/07-context-files.ts +35 -0
  43. package/examples/sdk/08-prompt-templates.ts +41 -0
  44. package/examples/sdk/08-slash-commands.ts +46 -0
  45. package/examples/sdk/09-api-keys-and-oauth.ts +54 -0
  46. package/examples/sdk/11-sessions.ts +47 -0
  47. package/examples/sdk/12-redis-sessions.ts +54 -0
  48. package/examples/sdk/13-sql-sessions.ts +61 -0
  49. package/examples/sdk/README.md +169 -0
  50. package/package.json +587 -0
  51. package/scripts/bench-guard.ts +71 -0
  52. package/scripts/bench-title-models.ts +332 -0
  53. package/scripts/build-binary.ts +117 -0
  54. package/scripts/bundle-dist.ts +123 -0
  55. package/scripts/compile-binary.ts +69 -0
  56. package/scripts/embed-mupdf-wasm.ts +67 -0
  57. package/scripts/format-prompts.ts +68 -0
  58. package/scripts/generate-aria-snapshot.ts +134 -0
  59. package/scripts/generate-docs-index.ts +58 -0
  60. package/scripts/generate-share-viewer.ts +34 -0
  61. package/scripts/legacy-pi-virtual-module.ts +205 -0
  62. package/scripts/measure-prompt-tokens.ts +63 -0
  63. package/scripts/omp +51 -0
  64. package/scripts/omp.ts +19 -0
  65. package/scripts/security-compare.ts +40 -0
  66. package/src/advisor/advise-tool.ts +234 -0
  67. package/src/advisor/config.ts +341 -0
  68. package/src/advisor/emission-guard.ts +172 -0
  69. package/src/advisor/index.ts +6 -0
  70. package/src/advisor/runtime.ts +1412 -0
  71. package/src/advisor/transcript-recorder.ts +215 -0
  72. package/src/advisor/watchdog.ts +135 -0
  73. package/src/async/index.ts +1 -0
  74. package/src/async/job-manager.ts +847 -0
  75. package/src/auto-thinking/classifier.ts +220 -0
  76. package/src/autolearn/controller.ts +152 -0
  77. package/src/autolearn/managed-skills.ts +255 -0
  78. package/src/autoresearch/command-resume.md +14 -0
  79. package/src/autoresearch/dashboard.ts +436 -0
  80. package/src/autoresearch/git.ts +331 -0
  81. package/src/autoresearch/helpers.ts +218 -0
  82. package/src/autoresearch/index.ts +541 -0
  83. package/src/autoresearch/prompt-setup.md +43 -0
  84. package/src/autoresearch/prompt.md +103 -0
  85. package/src/autoresearch/resume-message.md +10 -0
  86. package/src/autoresearch/state.ts +273 -0
  87. package/src/autoresearch/storage.ts +700 -0
  88. package/src/autoresearch/tools/init-experiment.ts +268 -0
  89. package/src/autoresearch/tools/log-experiment.ts +520 -0
  90. package/src/autoresearch/tools/run-experiment.ts +407 -0
  91. package/src/autoresearch/tools/update-notes.ts +109 -0
  92. package/src/autoresearch/types.ts +168 -0
  93. package/src/capability/context-file.ts +44 -0
  94. package/src/capability/extension-module.ts +34 -0
  95. package/src/capability/extension.ts +47 -0
  96. package/src/capability/fs.ts +117 -0
  97. package/src/capability/hook.ts +40 -0
  98. package/src/capability/index.ts +467 -0
  99. package/src/capability/instruction.ts +37 -0
  100. package/src/capability/mcp.ts +115 -0
  101. package/src/capability/prompt.ts +35 -0
  102. package/src/capability/rule-buckets.ts +66 -0
  103. package/src/capability/rule.ts +298 -0
  104. package/src/capability/settings.ts +34 -0
  105. package/src/capability/skill.ts +69 -0
  106. package/src/capability/slash-command.ts +40 -0
  107. package/src/capability/ssh.ts +41 -0
  108. package/src/capability/system-prompt.ts +34 -0
  109. package/src/capability/tool.ts +38 -0
  110. package/src/capability/types.ts +187 -0
  111. package/src/cleanse/agent.ts +226 -0
  112. package/src/cleanse/balance.ts +79 -0
  113. package/src/cleanse/checkers.ts +996 -0
  114. package/src/cleanse/index.ts +190 -0
  115. package/src/cleanse/loop.ts +51 -0
  116. package/src/cleanse/parsers.ts +726 -0
  117. package/src/cleanse/progress.ts +50 -0
  118. package/src/cleanse/prompts/assignment.md +47 -0
  119. package/src/cleanse/types.ts +72 -0
  120. package/src/cli/agents-cli.ts +138 -0
  121. package/src/cli/args.ts +368 -0
  122. package/src/cli/auth-broker-cli.ts +940 -0
  123. package/src/cli/auth-gateway-cli.ts +674 -0
  124. package/src/cli/bench-cli.ts +990 -0
  125. package/src/cli/browser-relay-cli.ts +119 -0
  126. package/src/cli/classify-install-target.ts +76 -0
  127. package/src/cli/claude-trace-cli.ts +795 -0
  128. package/src/cli/command-help.ts +107 -0
  129. package/src/cli/commands/init-xdg.ts +27 -0
  130. package/src/cli/completion-gen.ts +550 -0
  131. package/src/cli/config-cli.ts +459 -0
  132. package/src/cli/dry-balance-cli.ts +864 -0
  133. package/src/cli/extension-flags.ts +48 -0
  134. package/src/cli/file-processor.ts +132 -0
  135. package/src/cli/flag-tables.ts +367 -0
  136. package/src/cli/gallery-cli.ts +272 -0
  137. package/src/cli/gallery-fixtures/agentic.ts +420 -0
  138. package/src/cli/gallery-fixtures/codeintel.ts +187 -0
  139. package/src/cli/gallery-fixtures/edit.ts +254 -0
  140. package/src/cli/gallery-fixtures/fs.ts +245 -0
  141. package/src/cli/gallery-fixtures/index.ts +40 -0
  142. package/src/cli/gallery-fixtures/interaction.ts +46 -0
  143. package/src/cli/gallery-fixtures/memory.ts +81 -0
  144. package/src/cli/gallery-fixtures/misc.ts +177 -0
  145. package/src/cli/gallery-fixtures/search.ts +135 -0
  146. package/src/cli/gallery-fixtures/shell.ts +241 -0
  147. package/src/cli/gallery-fixtures/types.ts +57 -0
  148. package/src/cli/gallery-fixtures/web.ts +158 -0
  149. package/src/cli/gallery-screenshot.ts +279 -0
  150. package/src/cli/gc-cli.ts +1566 -0
  151. package/src/cli/grep-cli.ts +161 -0
  152. package/src/cli/grievances-cli.ts +256 -0
  153. package/src/cli/help-extra.ts +89 -0
  154. package/src/cli/initial-message.ts +58 -0
  155. package/src/cli/models-cli.ts +385 -0
  156. package/src/cli/plugin-cli.ts +996 -0
  157. package/src/cli/profile-alias.ts +369 -0
  158. package/src/cli/profile-bootstrap.ts +233 -0
  159. package/src/cli/read-cli.ts +99 -0
  160. package/src/cli/session-picker.ts +110 -0
  161. package/src/cli/setup-cli.ts +312 -0
  162. package/src/cli/setup-model-picker.ts +43 -0
  163. package/src/cli/shell-cli.ts +176 -0
  164. package/src/cli/ssh-cli.ts +179 -0
  165. package/src/cli/startup-cwd.ts +58 -0
  166. package/src/cli/stats-cli.ts +229 -0
  167. package/src/cli/thinking-levels.ts +7 -0
  168. package/src/cli/tiny-models-cli.ts +153 -0
  169. package/src/cli/ttsr-cli.ts +1013 -0
  170. package/src/cli/update-cli.ts +1184 -0
  171. package/src/cli/usage-cli.ts +1084 -0
  172. package/src/cli/usage-error.ts +7 -0
  173. package/src/cli/web-search-cli.ts +144 -0
  174. package/src/cli/worktree-cli.ts +311 -0
  175. package/src/cli-commands.ts +308 -0
  176. package/src/cli.ts +428 -0
  177. package/src/collab/crypto.ts +63 -0
  178. package/src/collab/display-name.ts +13 -0
  179. package/src/collab/guest.ts +764 -0
  180. package/src/collab/host.ts +692 -0
  181. package/src/collab/protocol.ts +296 -0
  182. package/src/collab/relay-client.ts +282 -0
  183. package/src/collab/replication-shrink.ts +111 -0
  184. package/src/commands/acp.ts +35 -0
  185. package/src/commands/agents.ts +58 -0
  186. package/src/commands/auth-broker.ts +100 -0
  187. package/src/commands/auth-gateway.ts +70 -0
  188. package/src/commands/bench.ts +64 -0
  189. package/src/commands/browser-relay.ts +53 -0
  190. package/src/commands/cleanse.ts +45 -0
  191. package/src/commands/commit.ts +47 -0
  192. package/src/commands/complete.ts +67 -0
  193. package/src/commands/completions.ts +61 -0
  194. package/src/commands/config.ts +52 -0
  195. package/src/commands/dry-balance.ts +43 -0
  196. package/src/commands/gallery.ts +61 -0
  197. package/src/commands/gc.ts +47 -0
  198. package/src/commands/grep.ts +49 -0
  199. package/src/commands/grievances.ts +52 -0
  200. package/src/commands/install.ts +107 -0
  201. package/src/commands/join.ts +40 -0
  202. package/src/commands/launch-help.ts +116 -0
  203. package/src/commands/launch.ts +34 -0
  204. package/src/commands/models.ts +61 -0
  205. package/src/commands/plugin.ts +79 -0
  206. package/src/commands/read.ts +39 -0
  207. package/src/commands/say.ts +146 -0
  208. package/src/commands/setup.ts +68 -0
  209. package/src/commands/share.ts +71 -0
  210. package/src/commands/shell.ts +30 -0
  211. package/src/commands/ssh.ts +61 -0
  212. package/src/commands/stats.ts +30 -0
  213. package/src/commands/tiny-models.ts +36 -0
  214. package/src/commands/token.ts +164 -0
  215. package/src/commands/ttsr.ts +125 -0
  216. package/src/commands/update.ts +34 -0
  217. package/src/commands/usage.ts +55 -0
  218. package/src/commands/web-search.ts +43 -0
  219. package/src/commands/worktree.ts +63 -0
  220. package/src/commit/agentic/agent.ts +322 -0
  221. package/src/commit/agentic/fallback.ts +96 -0
  222. package/src/commit/agentic/index.ts +383 -0
  223. package/src/commit/agentic/lock-files.ts +107 -0
  224. package/src/commit/agentic/prompts/analyze-file.md +22 -0
  225. package/src/commit/agentic/prompts/session-user.md +25 -0
  226. package/src/commit/agentic/prompts/split-confirm.md +1 -0
  227. package/src/commit/agentic/prompts/system.md +38 -0
  228. package/src/commit/agentic/state.ts +60 -0
  229. package/src/commit/agentic/tools/analyze-file.ts +148 -0
  230. package/src/commit/agentic/tools/git-file-diff.ts +191 -0
  231. package/src/commit/agentic/tools/git-hunk.ts +52 -0
  232. package/src/commit/agentic/tools/git-overview.ts +62 -0
  233. package/src/commit/agentic/tools/index.ts +54 -0
  234. package/src/commit/agentic/tools/propose-changelog.ts +147 -0
  235. package/src/commit/agentic/tools/propose-commit.ts +109 -0
  236. package/src/commit/agentic/tools/recent-commits.ts +81 -0
  237. package/src/commit/agentic/tools/schemas.ts +11 -0
  238. package/src/commit/agentic/tools/split-commit.ts +241 -0
  239. package/src/commit/agentic/topo-sort.ts +44 -0
  240. package/src/commit/agentic/trivial.ts +51 -0
  241. package/src/commit/agentic/validation.ts +183 -0
  242. package/src/commit/analysis/conventional.ts +64 -0
  243. package/src/commit/analysis/index.ts +4 -0
  244. package/src/commit/analysis/scope.ts +242 -0
  245. package/src/commit/analysis/summary.ts +107 -0
  246. package/src/commit/analysis/validation.ts +66 -0
  247. package/src/commit/changelog/detect.ts +40 -0
  248. package/src/commit/changelog/generate.ts +101 -0
  249. package/src/commit/changelog/index.ts +234 -0
  250. package/src/commit/changelog/parse.ts +44 -0
  251. package/src/commit/cli.ts +85 -0
  252. package/src/commit/git/diff.ts +148 -0
  253. package/src/commit/index.ts +5 -0
  254. package/src/commit/map-reduce/index.ts +69 -0
  255. package/src/commit/map-reduce/map-phase.ts +193 -0
  256. package/src/commit/map-reduce/reduce-phase.ts +49 -0
  257. package/src/commit/map-reduce/utils.ts +9 -0
  258. package/src/commit/message.ts +11 -0
  259. package/src/commit/model-selection.ts +95 -0
  260. package/src/commit/pipeline.ts +244 -0
  261. package/src/commit/prompts/analysis-system.md +148 -0
  262. package/src/commit/prompts/analysis-user.md +38 -0
  263. package/src/commit/prompts/changelog-system.md +50 -0
  264. package/src/commit/prompts/changelog-user.md +18 -0
  265. package/src/commit/prompts/file-observer-system.md +24 -0
  266. package/src/commit/prompts/file-observer-user.md +8 -0
  267. package/src/commit/prompts/reduce-system.md +50 -0
  268. package/src/commit/prompts/reduce-user.md +17 -0
  269. package/src/commit/prompts/summary-retry.md +3 -0
  270. package/src/commit/prompts/summary-system.md +38 -0
  271. package/src/commit/prompts/summary-user.md +13 -0
  272. package/src/commit/prompts/types-description.md +2 -0
  273. package/src/commit/shared-llm.ts +70 -0
  274. package/src/commit/types.ts +118 -0
  275. package/src/commit/utils/exclusions.ts +42 -0
  276. package/src/commit/utils.ts +58 -0
  277. package/src/config/api-key-resolver.ts +81 -0
  278. package/src/config/append-only-context-mode.ts +76 -0
  279. package/src/config/config-file.ts +347 -0
  280. package/src/config/inline-tool-descriptors-mode.ts +26 -0
  281. package/src/config/keybindings.ts +703 -0
  282. package/src/config/mcp-schema.json +252 -0
  283. package/src/config/model-discovery.ts +1077 -0
  284. package/src/config/model-registry.ts +2822 -0
  285. package/src/config/model-resolver.ts +2115 -0
  286. package/src/config/model-roles.ts +113 -0
  287. package/src/config/models-config-schema-bundle.ts +315 -0
  288. package/src/config/models-config-schema.ts +14 -0
  289. package/src/config/models-config.ts +130 -0
  290. package/src/config/prompt-templates.ts +205 -0
  291. package/src/config/provider-globals.ts +25 -0
  292. package/src/config/resolve-config-value.ts +95 -0
  293. package/src/config/service-tier.ts +146 -0
  294. package/src/config/settings-schema.ts +5887 -0
  295. package/src/config/settings.ts +2394 -0
  296. package/src/config.ts +242 -0
  297. package/src/cursor-bridge-tools.ts +81 -0
  298. package/src/cursor.ts +960 -0
  299. package/src/dap/client.ts +1043 -0
  300. package/src/dap/config.ts +480 -0
  301. package/src/dap/defaults.json +212 -0
  302. package/src/dap/index.ts +4 -0
  303. package/src/dap/session.ts +1841 -0
  304. package/src/dap/types.ts +611 -0
  305. package/src/debug/index.ts +584 -0
  306. package/src/debug/log-formatting.ts +58 -0
  307. package/src/debug/log-viewer.ts +966 -0
  308. package/src/debug/profiler.ts +168 -0
  309. package/src/debug/protocol-probe.ts +267 -0
  310. package/src/debug/raw-sse-buffer.ts +421 -0
  311. package/src/debug/raw-sse.ts +312 -0
  312. package/src/debug/remote-debugger.ts +151 -0
  313. package/src/debug/report-bundle.ts +411 -0
  314. package/src/debug/system-info.ts +111 -0
  315. package/src/debug/terminal-info.ts +124 -0
  316. package/src/discovery/agent-plugin-format.ts +551 -0
  317. package/src/discovery/agent-plugins.ts +341 -0
  318. package/src/discovery/agents-md.ts +67 -0
  319. package/src/discovery/agents.ts +300 -0
  320. package/src/discovery/at-imports.ts +273 -0
  321. package/src/discovery/builtin-defaults.ts +39 -0
  322. package/src/discovery/builtin-rules/go-add-cleanup.md +33 -0
  323. package/src/discovery/builtin-rules/go-bench-loop.md +36 -0
  324. package/src/discovery/builtin-rules/go-exp-promoted.md +40 -0
  325. package/src/discovery/builtin-rules/go-ioutil.md +37 -0
  326. package/src/discovery/builtin-rules/go-join-hostport.md +30 -0
  327. package/src/discovery/builtin-rules/go-new-expr.md +44 -0
  328. package/src/discovery/builtin-rules/go-rand-v2.md +41 -0
  329. package/src/discovery/builtin-rules/go-range-int.md +45 -0
  330. package/src/discovery/builtin-rules/index.ts +74 -0
  331. package/src/discovery/builtin-rules/rs-box-leak.md +49 -0
  332. package/src/discovery/builtin-rules/rs-future-prelude.md +24 -0
  333. package/src/discovery/builtin-rules/rs-lazylock.md +52 -0
  334. package/src/discovery/builtin-rules/rs-match-ergonomics.md +68 -0
  335. package/src/discovery/builtin-rules/rs-parking-lot.md +45 -0
  336. package/src/discovery/builtin-rules/rs-result-type.md +20 -0
  337. package/src/discovery/builtin-rules/ts-bare-catch.md +39 -0
  338. package/src/discovery/builtin-rules/ts-import-type.md +43 -0
  339. package/src/discovery/builtin-rules/ts-no-any.md +66 -0
  340. package/src/discovery/builtin-rules/ts-no-deprecated-leftovers.md +45 -0
  341. package/src/discovery/builtin-rules/ts-no-dynamic-import.md +40 -0
  342. package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +56 -0
  343. package/src/discovery/builtin-rules/ts-no-local-is-record.md +48 -0
  344. package/src/discovery/builtin-rules/ts-no-return-type.md +45 -0
  345. package/src/discovery/builtin-rules/ts-no-test-timers.md +55 -0
  346. package/src/discovery/builtin-rules/ts-no-tiny-functions.md +51 -0
  347. package/src/discovery/builtin-rules/ts-promise-with-resolvers.md +66 -0
  348. package/src/discovery/builtin-rules/ts-redundant-clear-guard.md +75 -0
  349. package/src/discovery/builtin-rules/ts-set-map.md +28 -0
  350. package/src/discovery/builtin.ts +945 -0
  351. package/src/discovery/claude-plugins.ts +640 -0
  352. package/src/discovery/claude.ts +591 -0
  353. package/src/discovery/cline.ts +83 -0
  354. package/src/discovery/codex.ts +553 -0
  355. package/src/discovery/contained-path.ts +78 -0
  356. package/src/discovery/cursor.ts +223 -0
  357. package/src/discovery/gemini.ts +386 -0
  358. package/src/discovery/github.ts +337 -0
  359. package/src/discovery/helpers.ts +1183 -0
  360. package/src/discovery/index.ts +82 -0
  361. package/src/discovery/mcp-json.ts +182 -0
  362. package/src/discovery/omp-extension-roots.ts +272 -0
  363. package/src/discovery/omp-plugins.ts +409 -0
  364. package/src/discovery/opencode.ts +441 -0
  365. package/src/discovery/plugin-dir-roots.ts +28 -0
  366. package/src/discovery/ssh.ts +153 -0
  367. package/src/discovery/substitute-plugin-root.ts +77 -0
  368. package/src/discovery/vscode.ts +106 -0
  369. package/src/discovery/windsurf.ts +149 -0
  370. package/src/edit/apply-patch/index.ts +87 -0
  371. package/src/edit/apply-patch/parser.ts +174 -0
  372. package/src/edit/diff.ts +1008 -0
  373. package/src/edit/edit-clipboard.ts +23 -0
  374. package/src/edit/file-snapshot-store.ts +148 -0
  375. package/src/edit/hashline/block-resolver.ts +33 -0
  376. package/src/edit/hashline/diff.ts +402 -0
  377. package/src/edit/hashline/execute.ts +313 -0
  378. package/src/edit/hashline/filesystem.ts +247 -0
  379. package/src/edit/hashline/index.ts +5 -0
  380. package/src/edit/hashline/noop-loop-guard.ts +99 -0
  381. package/src/edit/hashline/params.ts +12 -0
  382. package/src/edit/index.ts +699 -0
  383. package/src/edit/modes/apply-patch.lark +19 -0
  384. package/src/edit/modes/apply-patch.ts +53 -0
  385. package/src/edit/modes/patch.ts +1958 -0
  386. package/src/edit/modes/replace.ts +1191 -0
  387. package/src/edit/normalize.ts +345 -0
  388. package/src/edit/notebook.ts +242 -0
  389. package/src/edit/read-file.ts +25 -0
  390. package/src/edit/renderer.ts +998 -0
  391. package/src/edit/snapshot-details.ts +77 -0
  392. package/src/edit/streaming.ts +710 -0
  393. package/src/eval/agent-bridge.ts +224 -0
  394. package/src/eval/backend-helpers.ts +48 -0
  395. package/src/eval/backend.ts +71 -0
  396. package/src/eval/bridge-timeout.ts +65 -0
  397. package/src/eval/budget-bridge.ts +48 -0
  398. package/src/eval/completion-bridge.ts +212 -0
  399. package/src/eval/concurrency-bridge.ts +34 -0
  400. package/src/eval/executor-base.ts +562 -0
  401. package/src/eval/idle-timeout.ts +91 -0
  402. package/src/eval/index.ts +6 -0
  403. package/src/eval/jl/executor.ts +551 -0
  404. package/src/eval/jl/index.ts +54 -0
  405. package/src/eval/jl/kernel.ts +236 -0
  406. package/src/eval/jl/prelude.jl +736 -0
  407. package/src/eval/jl/prelude.ts +3 -0
  408. package/src/eval/jl/runner.jl +666 -0
  409. package/src/eval/jl/runtime.ts +118 -0
  410. package/src/eval/js/context-manager.ts +872 -0
  411. package/src/eval/js/executor.ts +178 -0
  412. package/src/eval/js/index.ts +41 -0
  413. package/src/eval/js/process-entry.ts +31 -0
  414. package/src/eval/js/shared/helpers.ts +170 -0
  415. package/src/eval/js/shared/indirect-eval.ts +30 -0
  416. package/src/eval/js/shared/local-module-loader.ts +364 -0
  417. package/src/eval/js/shared/prelude.ts +2 -0
  418. package/src/eval/js/shared/prelude.txt +294 -0
  419. package/src/eval/js/shared/rewrite-imports.ts +550 -0
  420. package/src/eval/js/shared/runtime.ts +590 -0
  421. package/src/eval/js/shared/types.ts +18 -0
  422. package/src/eval/js/tool-bridge.ts +163 -0
  423. package/src/eval/js/worker-core.ts +380 -0
  424. package/src/eval/js/worker-entry.ts +37 -0
  425. package/src/eval/js/worker-protocol.ts +47 -0
  426. package/src/eval/kernel-base.ts +569 -0
  427. package/src/eval/py/display.ts +71 -0
  428. package/src/eval/py/executor.ts +643 -0
  429. package/src/eval/py/index.ts +57 -0
  430. package/src/eval/py/kernel.ts +235 -0
  431. package/src/eval/py/prelude.py +673 -0
  432. package/src/eval/py/prelude.ts +3 -0
  433. package/src/eval/py/runner.py +1425 -0
  434. package/src/eval/py/runtime.ts +276 -0
  435. package/src/eval/py/spawn-options.ts +134 -0
  436. package/src/eval/py/tool-bridge.ts +201 -0
  437. package/src/eval/rb/executor.ts +511 -0
  438. package/src/eval/rb/index.ts +54 -0
  439. package/src/eval/rb/kernel.ts +231 -0
  440. package/src/eval/rb/prelude.rb +551 -0
  441. package/src/eval/rb/prelude.ts +3 -0
  442. package/src/eval/rb/runner.rb +581 -0
  443. package/src/eval/rb/runtime.ts +132 -0
  444. package/src/eval/runtime-env.ts +104 -0
  445. package/src/eval/session-id.ts +8 -0
  446. package/src/eval/types.ts +48 -0
  447. package/src/exa/index.ts +2 -0
  448. package/src/exa/mcp-client.ts +370 -0
  449. package/src/exa/types.ts +69 -0
  450. package/src/exec/bash-executor.ts +627 -0
  451. package/src/exec/direnv.ts +145 -0
  452. package/src/exec/exec.ts +53 -0
  453. package/src/exec/non-interactive-env.ts +118 -0
  454. package/src/export/custom-share.ts +65 -0
  455. package/src/export/html/args.ts +20 -0
  456. package/src/export/html/index.ts +320 -0
  457. package/src/export/html/share-loader.js +102 -0
  458. package/src/export/html/template.css +1355 -0
  459. package/src/export/html/template.html +55 -0
  460. package/src/export/html/template.js +1653 -0
  461. package/src/export/html/tool-views.generated.js +35 -0
  462. package/src/export/html/vendor/highlight.min.js +1213 -0
  463. package/src/export/html/vendor/marked.min.js +6 -0
  464. package/src/export/html/web-palette.ts +126 -0
  465. package/src/export/share.ts +686 -0
  466. package/src/export/ttsr.ts +590 -0
  467. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +54 -0
  468. package/src/extensibility/custom-commands/bundled/review/index.ts +698 -0
  469. package/src/extensibility/custom-commands/index.ts +2 -0
  470. package/src/extensibility/custom-commands/loader.ts +242 -0
  471. package/src/extensibility/custom-commands/types.ts +119 -0
  472. package/src/extensibility/custom-tools/index.ts +7 -0
  473. package/src/extensibility/custom-tools/loader.ts +301 -0
  474. package/src/extensibility/custom-tools/types.ts +287 -0
  475. package/src/extensibility/custom-tools/wrapper.ts +50 -0
  476. package/src/extensibility/extensions/compact-handler.ts +40 -0
  477. package/src/extensibility/extensions/get-commands-handler.ts +78 -0
  478. package/src/extensibility/extensions/index.ts +16 -0
  479. package/src/extensibility/extensions/load-errors.ts +13 -0
  480. package/src/extensibility/extensions/loader.ts +663 -0
  481. package/src/extensibility/extensions/managed-timers.ts +83 -0
  482. package/src/extensibility/extensions/model-api.ts +39 -0
  483. package/src/extensibility/extensions/runner.ts +1396 -0
  484. package/src/extensibility/extensions/types.ts +1603 -0
  485. package/src/extensibility/extensions/wrapper.ts +395 -0
  486. package/src/extensibility/hooks/index.ts +6 -0
  487. package/src/extensibility/hooks/loader.ts +243 -0
  488. package/src/extensibility/hooks/runner.ts +425 -0
  489. package/src/extensibility/hooks/tool-wrapper.ts +124 -0
  490. package/src/extensibility/hooks/types.ts +612 -0
  491. package/src/extensibility/legacy-pi-ai-shim.ts +161 -0
  492. package/src/extensibility/legacy-pi-coding-agent-shim.ts +1460 -0
  493. package/src/extensibility/legacy-pi-tui-shim.ts +43 -0
  494. package/src/extensibility/legacy-typebox.ts +101 -0
  495. package/src/extensibility/plugins/bun-git-cache.ts +91 -0
  496. package/src/extensibility/plugins/doctor.ts +65 -0
  497. package/src/extensibility/plugins/git-url.ts +367 -0
  498. package/src/extensibility/plugins/index.ts +9 -0
  499. package/src/extensibility/plugins/installer.ts +201 -0
  500. package/src/extensibility/plugins/legacy-pi-compat.ts +2643 -0
  501. package/src/extensibility/plugins/legacy-pi-virtual-modules.d.ts +4 -0
  502. package/src/extensibility/plugins/loader.ts +535 -0
  503. package/src/extensibility/plugins/manager.ts +1142 -0
  504. package/src/extensibility/plugins/marketplace/cache.ts +136 -0
  505. package/src/extensibility/plugins/marketplace/fetcher.ts +316 -0
  506. package/src/extensibility/plugins/marketplace/index.ts +6 -0
  507. package/src/extensibility/plugins/marketplace/manager.ts +927 -0
  508. package/src/extensibility/plugins/marketplace/registry.ts +192 -0
  509. package/src/extensibility/plugins/marketplace/source-resolver.ts +147 -0
  510. package/src/extensibility/plugins/marketplace/types.ts +192 -0
  511. package/src/extensibility/plugins/marketplace-auto-update.ts +49 -0
  512. package/src/extensibility/plugins/parser.ts +107 -0
  513. package/src/extensibility/plugins/runtime-config.ts +9 -0
  514. package/src/extensibility/plugins/types.ts +194 -0
  515. package/src/extensibility/session-handler-types.ts +21 -0
  516. package/src/extensibility/shared-events.ts +401 -0
  517. package/src/extensibility/skills.ts +531 -0
  518. package/src/extensibility/slash-commands.ts +131 -0
  519. package/src/extensibility/tool-event-input.ts +80 -0
  520. package/src/extensibility/tool-proxy.ts +28 -0
  521. package/src/extensibility/utils.ts +192 -0
  522. package/src/goals/index.ts +3 -0
  523. package/src/goals/runtime.ts +521 -0
  524. package/src/goals/state.ts +37 -0
  525. package/src/goals/tools/goal-tool.ts +251 -0
  526. package/src/hindsight/backend.ts +354 -0
  527. package/src/hindsight/bank.ts +156 -0
  528. package/src/hindsight/client.ts +680 -0
  529. package/src/hindsight/config.ts +193 -0
  530. package/src/hindsight/content.ts +266 -0
  531. package/src/hindsight/index.ts +8 -0
  532. package/src/hindsight/mental-models.ts +429 -0
  533. package/src/hindsight/seeds.json +32 -0
  534. package/src/hindsight/state.ts +530 -0
  535. package/src/hindsight/transcript.ts +71 -0
  536. package/src/index.ts +66 -0
  537. package/src/internal-urls/agent-protocol.ts +180 -0
  538. package/src/internal-urls/artifact-protocol.ts +151 -0
  539. package/src/internal-urls/docs-index.ts +102 -0
  540. package/src/internal-urls/filesystem-resource.ts +34 -0
  541. package/src/internal-urls/history-protocol.ts +198 -0
  542. package/src/internal-urls/index.ts +28 -0
  543. package/src/internal-urls/issue-pr-protocol.ts +594 -0
  544. package/src/internal-urls/json-query.ts +126 -0
  545. package/src/internal-urls/local-protocol.ts +471 -0
  546. package/src/internal-urls/mcp-protocol.ts +168 -0
  547. package/src/internal-urls/memory-protocol.ts +380 -0
  548. package/src/internal-urls/omp-protocol.ts +94 -0
  549. package/src/internal-urls/parse.ts +103 -0
  550. package/src/internal-urls/registry-helpers.ts +132 -0
  551. package/src/internal-urls/router.ts +153 -0
  552. package/src/internal-urls/rule-protocol.ts +45 -0
  553. package/src/internal-urls/security-protocol.ts +261 -0
  554. package/src/internal-urls/skill-protocol.ts +131 -0
  555. package/src/internal-urls/ssh-protocol.ts +368 -0
  556. package/src/internal-urls/types.ts +196 -0
  557. package/src/internal-urls/vault-protocol.ts +940 -0
  558. package/src/internal-urls/xd-protocol.ts +46 -0
  559. package/src/irc/bus.ts +380 -0
  560. package/src/jsonrpc/message-framing.ts +142 -0
  561. package/src/launch/broker.ts +1368 -0
  562. package/src/launch/client.ts +521 -0
  563. package/src/launch/ensure.ts +73 -0
  564. package/src/launch/paths.ts +14 -0
  565. package/src/launch/presence.ts +82 -0
  566. package/src/launch/protocol.ts +448 -0
  567. package/src/launch/spawn-options.ts +17 -0
  568. package/src/launch/terminal-output-worker-client.ts +53 -0
  569. package/src/launch/terminal-output-worker-protocol.ts +11 -0
  570. package/src/launch/terminal-output-worker.ts +23 -0
  571. package/src/launch/terminal-output.ts +46 -0
  572. package/src/lib/xai-http.ts +150 -0
  573. package/src/live/attestation.ts +91 -0
  574. package/src/live/controller.ts +517 -0
  575. package/src/live/prompts/agent-final-message.md +3 -0
  576. package/src/live/prompts/live-instructions.md +23 -0
  577. package/src/live/protocol.ts +233 -0
  578. package/src/live/transport.ts +422 -0
  579. package/src/live/visualizer.ts +221 -0
  580. package/src/live/voices.ts +18 -0
  581. package/src/lsp/client.ts +1465 -0
  582. package/src/lsp/clients/biome-client.ts +263 -0
  583. package/src/lsp/clients/index.ts +50 -0
  584. package/src/lsp/clients/lsp-linter-client.ts +85 -0
  585. package/src/lsp/clients/swiftlint-client.ts +120 -0
  586. package/src/lsp/config.ts +549 -0
  587. package/src/lsp/defaults.json +499 -0
  588. package/src/lsp/deferred-diagnostics.ts +66 -0
  589. package/src/lsp/diagnostics-ledger.ts +51 -0
  590. package/src/lsp/edits.ts +288 -0
  591. package/src/lsp/format-options.ts +119 -0
  592. package/src/lsp/index.ts +2821 -0
  593. package/src/lsp/lspmux.ts +233 -0
  594. package/src/lsp/mux/daemon.ts +348 -0
  595. package/src/lsp/mux/protocol.ts +96 -0
  596. package/src/lsp/mux/server.ts +797 -0
  597. package/src/lsp/render.ts +668 -0
  598. package/src/lsp/startup-events.ts +13 -0
  599. package/src/lsp/types.ts +479 -0
  600. package/src/lsp/utils.ts +747 -0
  601. package/src/main.ts +1799 -0
  602. package/src/markit/NOTICE +32 -0
  603. package/src/markit/converters/docx.ts +56 -0
  604. package/src/markit/converters/epub.ts +136 -0
  605. package/src/markit/converters/pdf/columns.ts +103 -0
  606. package/src/markit/converters/pdf/extract.ts +598 -0
  607. package/src/markit/converters/pdf/grid.ts +780 -0
  608. package/src/markit/converters/pdf/headers.ts +106 -0
  609. package/src/markit/converters/pdf/index.ts +146 -0
  610. package/src/markit/converters/pdf/render.ts +501 -0
  611. package/src/markit/converters/pdf/types.ts +84 -0
  612. package/src/markit/converters/pptx.ts +325 -0
  613. package/src/markit/converters/xlsx.ts +173 -0
  614. package/src/markit/index.ts +2 -0
  615. package/src/markit/registry.ts +59 -0
  616. package/src/markit/types.ts +35 -0
  617. package/src/mcp/client.ts +511 -0
  618. package/src/mcp/config-writer.ts +377 -0
  619. package/src/mcp/config.ts +385 -0
  620. package/src/mcp/index.ts +29 -0
  621. package/src/mcp/json-rpc.ts +122 -0
  622. package/src/mcp/loader.ts +125 -0
  623. package/src/mcp/manager.ts +1524 -0
  624. package/src/mcp/oauth-credentials.ts +104 -0
  625. package/src/mcp/oauth-discovery.ts +587 -0
  626. package/src/mcp/oauth-flow.ts +830 -0
  627. package/src/mcp/render.ts +214 -0
  628. package/src/mcp/request-id.ts +24 -0
  629. package/src/mcp/smithery-auth.ts +108 -0
  630. package/src/mcp/smithery-connect.ts +154 -0
  631. package/src/mcp/smithery-registry.ts +500 -0
  632. package/src/mcp/startup-events.ts +116 -0
  633. package/src/mcp/timeout.ts +59 -0
  634. package/src/mcp/tool-bridge.ts +691 -0
  635. package/src/mcp/tool-cache.ts +117 -0
  636. package/src/mcp/transports/header-policy.ts +95 -0
  637. package/src/mcp/transports/http.ts +508 -0
  638. package/src/mcp/transports/index.ts +7 -0
  639. package/src/mcp/transports/sse.ts +372 -0
  640. package/src/mcp/transports/stdio.ts +905 -0
  641. package/src/mcp/types.ts +467 -0
  642. package/src/memories/index.ts +1434 -0
  643. package/src/memories/storage.ts +578 -0
  644. package/src/memory-backend/index.ts +19 -0
  645. package/src/memory-backend/local-backend.ts +47 -0
  646. package/src/memory-backend/messages.ts +19 -0
  647. package/src/memory-backend/off-backend.ts +25 -0
  648. package/src/memory-backend/resolve.ts +25 -0
  649. package/src/memory-backend/runtime.ts +66 -0
  650. package/src/memory-backend/tool-names.ts +2 -0
  651. package/src/memory-backend/types.ts +166 -0
  652. package/src/mnemopi/backend.ts +629 -0
  653. package/src/mnemopi/config.ts +267 -0
  654. package/src/mnemopi/embed-client.ts +293 -0
  655. package/src/mnemopi/embed-protocol.ts +35 -0
  656. package/src/mnemopi/embed-worker.ts +114 -0
  657. package/src/mnemopi/index.ts +3 -0
  658. package/src/mnemopi/state.ts +940 -0
  659. package/src/modes/acp/acp-agent.ts +2595 -0
  660. package/src/modes/acp/acp-client-bridge.ts +154 -0
  661. package/src/modes/acp/acp-event-mapper.ts +1084 -0
  662. package/src/modes/acp/acp-mode.ts +48 -0
  663. package/src/modes/acp/index.ts +2 -0
  664. package/src/modes/acp/terminal-auth.ts +37 -0
  665. package/src/modes/components/advisor-config.ts +635 -0
  666. package/src/modes/components/advisor-message.ts +109 -0
  667. package/src/modes/components/agent-dashboard.ts +1252 -0
  668. package/src/modes/components/agent-hub-projection.ts +248 -0
  669. package/src/modes/components/agent-hub-renderer.ts +194 -0
  670. package/src/modes/components/agent-hub.ts +1126 -0
  671. package/src/modes/components/agent-transcript-viewer.ts +647 -0
  672. package/src/modes/components/ask-dialog.ts +1017 -0
  673. package/src/modes/components/assistant-message.ts +978 -0
  674. package/src/modes/components/background-tan-message.ts +36 -0
  675. package/src/modes/components/bash-execution.ts +233 -0
  676. package/src/modes/components/bordered-loader.ts +41 -0
  677. package/src/modes/components/btw-panel.ts +161 -0
  678. package/src/modes/components/cache-invalidation-marker.ts +110 -0
  679. package/src/modes/components/chat-block.ts +111 -0
  680. package/src/modes/components/chat-transcript-builder.ts +518 -0
  681. package/src/modes/components/codex-reset-fireworks.ts +369 -0
  682. package/src/modes/components/collab-prompt-message.ts +32 -0
  683. package/src/modes/components/compaction-summary-message.ts +221 -0
  684. package/src/modes/components/copy-selector.ts +218 -0
  685. package/src/modes/components/countdown-timer.ts +75 -0
  686. package/src/modes/components/custom-editor.ts +1036 -0
  687. package/src/modes/components/custom-message.ts +70 -0
  688. package/src/modes/components/diff.ts +254 -0
  689. package/src/modes/components/dynamic-border.ts +37 -0
  690. package/src/modes/components/error-banner.ts +33 -0
  691. package/src/modes/components/eval-execution.ts +169 -0
  692. package/src/modes/components/execution-shared.ts +101 -0
  693. package/src/modes/components/extensions/extension-dashboard.ts +492 -0
  694. package/src/modes/components/extensions/extension-list.ts +507 -0
  695. package/src/modes/components/extensions/index.ts +9 -0
  696. package/src/modes/components/extensions/inspector-panel.ts +326 -0
  697. package/src/modes/components/extensions/state-manager.ts +648 -0
  698. package/src/modes/components/extensions/types.ts +186 -0
  699. package/src/modes/components/footer.ts +274 -0
  700. package/src/modes/components/history-search.ts +268 -0
  701. package/src/modes/components/hook-editor.ts +213 -0
  702. package/src/modes/components/hook-input.ts +87 -0
  703. package/src/modes/components/hook-message.ts +67 -0
  704. package/src/modes/components/hook-selector.ts +691 -0
  705. package/src/modes/components/index.ts +42 -0
  706. package/src/modes/components/keybinding-hints.ts +56 -0
  707. package/src/modes/components/late-diagnostics-message.ts +60 -0
  708. package/src/modes/components/login-dialog.ts +197 -0
  709. package/src/modes/components/logout-account-selector.ts +130 -0
  710. package/src/modes/components/mcp-add-wizard.ts +1413 -0
  711. package/src/modes/components/message-frame.ts +98 -0
  712. package/src/modes/components/model-browser.ts +888 -0
  713. package/src/modes/components/model-hub.ts +2014 -0
  714. package/src/modes/components/model-picker.ts +237 -0
  715. package/src/modes/components/move-overlay.ts +293 -0
  716. package/src/modes/components/oauth-selector.ts +474 -0
  717. package/src/modes/components/omfg-panel.ts +141 -0
  718. package/src/modes/components/overlay-box.ts +109 -0
  719. package/src/modes/components/pause-screen.ts +208 -0
  720. package/src/modes/components/plan-review-overlay.ts +1226 -0
  721. package/src/modes/components/plan-toc.ts +138 -0
  722. package/src/modes/components/plugin-selector.ts +100 -0
  723. package/src/modes/components/plugin-settings.ts +745 -0
  724. package/src/modes/components/queue-mode-selector.ts +61 -0
  725. package/src/modes/components/read-tool-group.ts +856 -0
  726. package/src/modes/components/reset-usage-selector.ts +161 -0
  727. package/src/modes/components/segment-track.ts +89 -0
  728. package/src/modes/components/select-list-mouse-routing.ts +35 -0
  729. package/src/modes/components/selector-helpers.ts +129 -0
  730. package/src/modes/components/session-account-selector.ts +62 -0
  731. package/src/modes/components/session-selector.ts +1035 -0
  732. package/src/modes/components/settings-defs.ts +267 -0
  733. package/src/modes/components/settings-selector.ts +1445 -0
  734. package/src/modes/components/show-images-selector.ts +50 -0
  735. package/src/modes/components/skill-message.ts +110 -0
  736. package/src/modes/components/snapcompact-shape-preview-doc.md +14 -0
  737. package/src/modes/components/snapcompact-shape-preview.ts +192 -0
  738. package/src/modes/components/status-line/component.ts +1843 -0
  739. package/src/modes/components/status-line/context-thresholds.ts +86 -0
  740. package/src/modes/components/status-line/git-utils.ts +42 -0
  741. package/src/modes/components/status-line/index.ts +5 -0
  742. package/src/modes/components/status-line/presets.ts +106 -0
  743. package/src/modes/components/status-line/segments.ts +710 -0
  744. package/src/modes/components/status-line/separators.ts +55 -0
  745. package/src/modes/components/status-line/types.ts +159 -0
  746. package/src/modes/components/stripped-tool-calls-placeholder.ts +35 -0
  747. package/src/modes/components/theme-selector.ts +68 -0
  748. package/src/modes/components/thinking-selector.ts +57 -0
  749. package/src/modes/components/tiny-title-download-progress.ts +90 -0
  750. package/src/modes/components/tips.txt +26 -0
  751. package/src/modes/components/todo-reminder.ts +43 -0
  752. package/src/modes/components/tool-execution.ts +1467 -0
  753. package/src/modes/components/transcript-container.ts +527 -0
  754. package/src/modes/components/tree-selector.ts +1006 -0
  755. package/src/modes/components/ttsr-notification.ts +123 -0
  756. package/src/modes/components/usage-row.ts +52 -0
  757. package/src/modes/components/user-message-selector.ts +227 -0
  758. package/src/modes/components/user-message.ts +167 -0
  759. package/src/modes/components/visual-truncate.ts +63 -0
  760. package/src/modes/components/welcome.ts +578 -0
  761. package/src/modes/controllers/btw-controller.ts +246 -0
  762. package/src/modes/controllers/command-controller-shared.ts +109 -0
  763. package/src/modes/controllers/command-controller.ts +2024 -0
  764. package/src/modes/controllers/event-controller.ts +2173 -0
  765. package/src/modes/controllers/extension-ui-controller.ts +1243 -0
  766. package/src/modes/controllers/input-controller.ts +2037 -0
  767. package/src/modes/controllers/live-command-controller.ts +261 -0
  768. package/src/modes/controllers/mcp-command-controller.ts +2552 -0
  769. package/src/modes/controllers/omfg-controller.ts +287 -0
  770. package/src/modes/controllers/omfg-rule.ts +647 -0
  771. package/src/modes/controllers/selector-controller.ts +2058 -0
  772. package/src/modes/controllers/session-focus-controller.ts +117 -0
  773. package/src/modes/controllers/ssh-command-controller.ts +385 -0
  774. package/src/modes/controllers/streaming-reveal.ts +399 -0
  775. package/src/modes/controllers/tan-command-controller.ts +243 -0
  776. package/src/modes/controllers/todo-command-controller.ts +487 -0
  777. package/src/modes/controllers/tool-args-reveal.ts +591 -0
  778. package/src/modes/data/emojis.json +1 -0
  779. package/src/modes/emoji-autocomplete.ts +285 -0
  780. package/src/modes/github-ref-autocomplete.ts +75 -0
  781. package/src/modes/gradient-highlight.ts +99 -0
  782. package/src/modes/image-references.ts +137 -0
  783. package/src/modes/index.ts +10 -0
  784. package/src/modes/interactive-mode.ts +5055 -0
  785. package/src/modes/internal-url-autocomplete.ts +158 -0
  786. package/src/modes/loop-limit.ts +192 -0
  787. package/src/modes/magic-keyword-boundary.ts +23 -0
  788. package/src/modes/magic-keywords.ts +42 -0
  789. package/src/modes/markdown-prose.ts +247 -0
  790. package/src/modes/oauth-manual-input.ts +69 -0
  791. package/src/modes/orchestrate.ts +43 -0
  792. package/src/modes/print-mode.ts +282 -0
  793. package/src/modes/prompt-action-autocomplete.ts +322 -0
  794. package/src/modes/queue-input.ts +132 -0
  795. package/src/modes/rpc/host-tools.ts +204 -0
  796. package/src/modes/rpc/host-uris.ts +241 -0
  797. package/src/modes/rpc/rpc-client.ts +1213 -0
  798. package/src/modes/rpc/rpc-frame.ts +316 -0
  799. package/src/modes/rpc/rpc-input.ts +38 -0
  800. package/src/modes/rpc/rpc-messages.ts +127 -0
  801. package/src/modes/rpc/rpc-mode.ts +1519 -0
  802. package/src/modes/rpc/rpc-subagents.ts +265 -0
  803. package/src/modes/rpc/rpc-types.ts +544 -0
  804. package/src/modes/running-subagent-badge.ts +13 -0
  805. package/src/modes/runtime-init.ts +144 -0
  806. package/src/modes/session-observer-registry.ts +223 -0
  807. package/src/modes/session-teardown.ts +82 -0
  808. package/src/modes/setup-version.ts +11 -0
  809. package/src/modes/setup-wizard/index.ts +103 -0
  810. package/src/modes/setup-wizard/lazy.ts +16 -0
  811. package/src/modes/setup-wizard/scenes/glyph.ts +103 -0
  812. package/src/modes/setup-wizard/scenes/model.ts +132 -0
  813. package/src/modes/setup-wizard/scenes/outro.ts +35 -0
  814. package/src/modes/setup-wizard/scenes/providers.ts +105 -0
  815. package/src/modes/setup-wizard/scenes/sign-in.ts +312 -0
  816. package/src/modes/setup-wizard/scenes/splash.ts +201 -0
  817. package/src/modes/setup-wizard/scenes/theme.ts +330 -0
  818. package/src/modes/setup-wizard/scenes/types.ts +65 -0
  819. package/src/modes/setup-wizard/scenes/web-search.ts +153 -0
  820. package/src/modes/setup-wizard/startup-splash.ts +107 -0
  821. package/src/modes/setup-wizard/wizard-overlay.ts +335 -0
  822. package/src/modes/shared.ts +49 -0
  823. package/src/modes/skill-command.ts +91 -0
  824. package/src/modes/theme/dark.json +95 -0
  825. package/src/modes/theme/defaults/alabaster.json +93 -0
  826. package/src/modes/theme/defaults/amethyst.json +96 -0
  827. package/src/modes/theme/defaults/anthracite.json +93 -0
  828. package/src/modes/theme/defaults/basalt.json +91 -0
  829. package/src/modes/theme/defaults/birch.json +95 -0
  830. package/src/modes/theme/defaults/dark-abyss.json +91 -0
  831. package/src/modes/theme/defaults/dark-arctic.json +104 -0
  832. package/src/modes/theme/defaults/dark-aurora.json +95 -0
  833. package/src/modes/theme/defaults/dark-catppuccin.json +107 -0
  834. package/src/modes/theme/defaults/dark-cavern.json +91 -0
  835. package/src/modes/theme/defaults/dark-copper.json +95 -0
  836. package/src/modes/theme/defaults/dark-cosmos.json +90 -0
  837. package/src/modes/theme/defaults/dark-cyberpunk.json +102 -0
  838. package/src/modes/theme/defaults/dark-dracula.json +98 -0
  839. package/src/modes/theme/defaults/dark-eclipse.json +91 -0
  840. package/src/modes/theme/defaults/dark-ember.json +95 -0
  841. package/src/modes/theme/defaults/dark-equinox.json +90 -0
  842. package/src/modes/theme/defaults/dark-forest.json +96 -0
  843. package/src/modes/theme/defaults/dark-github.json +105 -0
  844. package/src/modes/theme/defaults/dark-gruvbox.json +112 -0
  845. package/src/modes/theme/defaults/dark-lavender.json +95 -0
  846. package/src/modes/theme/defaults/dark-lunar.json +89 -0
  847. package/src/modes/theme/defaults/dark-midnight.json +95 -0
  848. package/src/modes/theme/defaults/dark-monochrome.json +94 -0
  849. package/src/modes/theme/defaults/dark-monokai.json +98 -0
  850. package/src/modes/theme/defaults/dark-nebula.json +90 -0
  851. package/src/modes/theme/defaults/dark-nord.json +97 -0
  852. package/src/modes/theme/defaults/dark-ocean.json +101 -0
  853. package/src/modes/theme/defaults/dark-one.json +100 -0
  854. package/src/modes/theme/defaults/dark-poimandres.json +143 -0
  855. package/src/modes/theme/defaults/dark-rainforest.json +91 -0
  856. package/src/modes/theme/defaults/dark-reef.json +91 -0
  857. package/src/modes/theme/defaults/dark-retro.json +92 -0
  858. package/src/modes/theme/defaults/dark-rose-pine.json +96 -0
  859. package/src/modes/theme/defaults/dark-sakura.json +95 -0
  860. package/src/modes/theme/defaults/dark-slate.json +95 -0
  861. package/src/modes/theme/defaults/dark-solarized.json +97 -0
  862. package/src/modes/theme/defaults/dark-solstice.json +90 -0
  863. package/src/modes/theme/defaults/dark-starfall.json +91 -0
  864. package/src/modes/theme/defaults/dark-sunset.json +99 -0
  865. package/src/modes/theme/defaults/dark-swamp.json +90 -0
  866. package/src/modes/theme/defaults/dark-synthwave.json +103 -0
  867. package/src/modes/theme/defaults/dark-taiga.json +91 -0
  868. package/src/modes/theme/defaults/dark-terminal.json +95 -0
  869. package/src/modes/theme/defaults/dark-tokyo-night.json +101 -0
  870. package/src/modes/theme/defaults/dark-tundra.json +91 -0
  871. package/src/modes/theme/defaults/dark-twilight.json +91 -0
  872. package/src/modes/theme/defaults/dark-volcanic.json +91 -0
  873. package/src/modes/theme/defaults/graphite.json +92 -0
  874. package/src/modes/theme/defaults/index.ts +199 -0
  875. package/src/modes/theme/defaults/light-arctic.json +107 -0
  876. package/src/modes/theme/defaults/light-aurora-day.json +91 -0
  877. package/src/modes/theme/defaults/light-canyon.json +91 -0
  878. package/src/modes/theme/defaults/light-catppuccin.json +106 -0
  879. package/src/modes/theme/defaults/light-cirrus.json +90 -0
  880. package/src/modes/theme/defaults/light-coral.json +95 -0
  881. package/src/modes/theme/defaults/light-cyberpunk.json +96 -0
  882. package/src/modes/theme/defaults/light-dawn.json +90 -0
  883. package/src/modes/theme/defaults/light-dunes.json +91 -0
  884. package/src/modes/theme/defaults/light-eucalyptus.json +95 -0
  885. package/src/modes/theme/defaults/light-forest.json +100 -0
  886. package/src/modes/theme/defaults/light-frost.json +95 -0
  887. package/src/modes/theme/defaults/light-github.json +115 -0
  888. package/src/modes/theme/defaults/light-glacier.json +91 -0
  889. package/src/modes/theme/defaults/light-gruvbox.json +108 -0
  890. package/src/modes/theme/defaults/light-haze.json +90 -0
  891. package/src/modes/theme/defaults/light-honeycomb.json +95 -0
  892. package/src/modes/theme/defaults/light-lagoon.json +91 -0
  893. package/src/modes/theme/defaults/light-lavender.json +95 -0
  894. package/src/modes/theme/defaults/light-meadow.json +91 -0
  895. package/src/modes/theme/defaults/light-mint.json +95 -0
  896. package/src/modes/theme/defaults/light-monochrome.json +101 -0
  897. package/src/modes/theme/defaults/light-ocean.json +99 -0
  898. package/src/modes/theme/defaults/light-one.json +99 -0
  899. package/src/modes/theme/defaults/light-opal.json +91 -0
  900. package/src/modes/theme/defaults/light-orchard.json +91 -0
  901. package/src/modes/theme/defaults/light-paper.json +95 -0
  902. package/src/modes/theme/defaults/light-poimandres.json +143 -0
  903. package/src/modes/theme/defaults/light-prism.json +90 -0
  904. package/src/modes/theme/defaults/light-retro.json +98 -0
  905. package/src/modes/theme/defaults/light-sand.json +95 -0
  906. package/src/modes/theme/defaults/light-savanna.json +91 -0
  907. package/src/modes/theme/defaults/light-solarized.json +102 -0
  908. package/src/modes/theme/defaults/light-soleil.json +90 -0
  909. package/src/modes/theme/defaults/light-sunset.json +99 -0
  910. package/src/modes/theme/defaults/light-synthwave.json +98 -0
  911. package/src/modes/theme/defaults/light-tokyo-night.json +111 -0
  912. package/src/modes/theme/defaults/light-wetland.json +91 -0
  913. package/src/modes/theme/defaults/light-zenith.json +89 -0
  914. package/src/modes/theme/defaults/limestone.json +94 -0
  915. package/src/modes/theme/defaults/mahogany.json +97 -0
  916. package/src/modes/theme/defaults/marble.json +93 -0
  917. package/src/modes/theme/defaults/obsidian.json +91 -0
  918. package/src/modes/theme/defaults/onyx.json +91 -0
  919. package/src/modes/theme/defaults/pearl.json +93 -0
  920. package/src/modes/theme/defaults/porcelain.json +91 -0
  921. package/src/modes/theme/defaults/quartz.json +96 -0
  922. package/src/modes/theme/defaults/sandstone.json +95 -0
  923. package/src/modes/theme/defaults/titanium.json +90 -0
  924. package/src/modes/theme/light.json +93 -0
  925. package/src/modes/theme/mermaid-cache.ts +92 -0
  926. package/src/modes/theme/shimmer.ts +305 -0
  927. package/src/modes/theme/theme-schema.json +463 -0
  928. package/src/modes/theme/theme.ts +3171 -0
  929. package/src/modes/turn-budget.ts +31 -0
  930. package/src/modes/types.ts +494 -0
  931. package/src/modes/ultrathink.ts +42 -0
  932. package/src/modes/utils/context-usage.ts +518 -0
  933. package/src/modes/utils/copy-targets.ts +378 -0
  934. package/src/modes/utils/hotkeys-markdown.ts +65 -0
  935. package/src/modes/utils/interactive-context-helpers.ts +31 -0
  936. package/src/modes/utils/keybinding-matchers.ts +86 -0
  937. package/src/modes/utils/tools-markdown.ts +31 -0
  938. package/src/modes/utils/transcript-render-helpers.ts +253 -0
  939. package/src/modes/utils/ui-helpers.ts +976 -0
  940. package/src/modes/warp-events.ts +232 -0
  941. package/src/modes/workflow.ts +55 -0
  942. package/src/plan-mode/approved-plan.ts +194 -0
  943. package/src/plan-mode/model-transition.ts +51 -0
  944. package/src/plan-mode/plan-files.ts +40 -0
  945. package/src/plan-mode/plan-handoff.ts +37 -0
  946. package/src/plan-mode/plan-protection.ts +31 -0
  947. package/src/plan-mode/state.ts +6 -0
  948. package/src/priority.json +60 -0
  949. package/src/prompts/advisor/active-repo-watchdog.md +6 -0
  950. package/src/prompts/advisor/advise-tool.md +3 -0
  951. package/src/prompts/advisor/context-files.md +8 -0
  952. package/src/prompts/advisor/system.md +98 -0
  953. package/src/prompts/agents/designer.md +74 -0
  954. package/src/prompts/agents/frontmatter.md +12 -0
  955. package/src/prompts/agents/init.md +33 -0
  956. package/src/prompts/agents/librarian.md +119 -0
  957. package/src/prompts/agents/reviewer.md +139 -0
  958. package/src/prompts/agents/scout.md +58 -0
  959. package/src/prompts/agents/security-reviewer.md +75 -0
  960. package/src/prompts/agents/task.md +17 -0
  961. package/src/prompts/bench/cache-prefix-chunk.md +1 -0
  962. package/src/prompts/bench/cache-prefix.md +3 -0
  963. package/src/prompts/bench/cache-suffix.md +1 -0
  964. package/src/prompts/bench.md +6 -0
  965. package/src/prompts/ci-green-request.md +36 -0
  966. package/src/prompts/dry-balance-bench.md +8 -0
  967. package/src/prompts/goals/goal-budget-limit.md +16 -0
  968. package/src/prompts/goals/goal-continuation.md +28 -0
  969. package/src/prompts/goals/goal-mode-active.md +23 -0
  970. package/src/prompts/goals/goal-mode-context.md +4 -0
  971. package/src/prompts/goals/goal-todo-context.md +12 -0
  972. package/src/prompts/goals/guided-goal-interview.md +43 -0
  973. package/src/prompts/memories/consolidation.md +30 -0
  974. package/src/prompts/memories/consolidation_system.md +4 -0
  975. package/src/prompts/memories/read-path.md +17 -0
  976. package/src/prompts/memories/stage_one_input.md +6 -0
  977. package/src/prompts/memories/stage_one_system.md +21 -0
  978. package/src/prompts/review-custom-request.md +21 -0
  979. package/src/prompts/review-headless-request.md +16 -0
  980. package/src/prompts/review-request.md +68 -0
  981. package/src/prompts/security/scan-coordinator.md +7 -0
  982. package/src/prompts/security/scan-request.md +21 -0
  983. package/src/prompts/security/validate-request.md +8 -0
  984. package/src/prompts/session/launch-completion.md +1 -0
  985. package/src/prompts/skills/autoload.md +8 -0
  986. package/src/prompts/skills/user-invocation.md +11 -0
  987. package/src/prompts/steering/parent-irc.md +5 -0
  988. package/src/prompts/steering/user-interjection.md +6 -0
  989. package/src/prompts/system/active-repo-context.md +4 -0
  990. package/src/prompts/system/agent-creation-architect.md +50 -0
  991. package/src/prompts/system/agent-creation-user.md +6 -0
  992. package/src/prompts/system/auto-continue.md +1 -0
  993. package/src/prompts/system/auto-thinking-difficulty-local.md +14 -0
  994. package/src/prompts/system/auto-thinking-difficulty.md +14 -0
  995. package/src/prompts/system/autolearn-guidance-learn.md +1 -0
  996. package/src/prompts/system/autolearn-guidance.md +7 -0
  997. package/src/prompts/system/autolearn-nudge-autocontinue.md +5 -0
  998. package/src/prompts/system/background-tan-dispatch.md +8 -0
  999. package/src/prompts/system/btw-user.md +8 -0
  1000. package/src/prompts/system/commit-message-system.md +14 -0
  1001. package/src/prompts/system/computer-safety.md +14 -0
  1002. package/src/prompts/system/custom-system-prompt.md +64 -0
  1003. package/src/prompts/system/eager-task.md +7 -0
  1004. package/src/prompts/system/eager-todo.md +18 -0
  1005. package/src/prompts/system/empty-stop-retry.md +4 -0
  1006. package/src/prompts/system/gemini-tool-call-reminder.md +9 -0
  1007. package/src/prompts/system/interrupted-thinking.md +7 -0
  1008. package/src/prompts/system/irc-autoreply.md +6 -0
  1009. package/src/prompts/system/irc-incoming.md +9 -0
  1010. package/src/prompts/system/manual-continue.md +7 -0
  1011. package/src/prompts/system/mcp-xdev-guidance.md +11 -0
  1012. package/src/prompts/system/memory-consolidation-system.md +8 -0
  1013. package/src/prompts/system/memory-extraction-system.md +26 -0
  1014. package/src/prompts/system/mid-run-todo-nudge.md +3 -0
  1015. package/src/prompts/system/omfg-user.md +50 -0
  1016. package/src/prompts/system/orchestrate-notice.md +40 -0
  1017. package/src/prompts/system/personalities/default.md +18 -0
  1018. package/src/prompts/system/personalities/friendly.md +17 -0
  1019. package/src/prompts/system/personalities/pragmatic.md +15 -0
  1020. package/src/prompts/system/plan-mode-active.md +125 -0
  1021. package/src/prompts/system/plan-mode-approved.md +22 -0
  1022. package/src/prompts/system/plan-mode-compact-instructions.md +17 -0
  1023. package/src/prompts/system/plan-mode-reference.md +10 -0
  1024. package/src/prompts/system/plan-mode-subagent.md +33 -0
  1025. package/src/prompts/system/plan-mode-tool-decision-reminder.md +9 -0
  1026. package/src/prompts/system/plan-yolo-handoff.md +5 -0
  1027. package/src/prompts/system/prewalk-checklist.md +7 -0
  1028. package/src/prompts/system/prewalk-continue.md +1 -0
  1029. package/src/prompts/system/prewalk-plan.md +13 -0
  1030. package/src/prompts/system/project-prompt.md +61 -0
  1031. package/src/prompts/system/recap-user.md +9 -0
  1032. package/src/prompts/system/resolve-device-reminder.md +3 -0
  1033. package/src/prompts/system/rewind-report.md +6 -0
  1034. package/src/prompts/system/side-channel-no-tools.md +3 -0
  1035. package/src/prompts/system/snapcompact-context-frames-note.md +1 -0
  1036. package/src/prompts/system/snapcompact-context-stub.md +1 -0
  1037. package/src/prompts/system/snapcompact-system-frames-note.md +1 -0
  1038. package/src/prompts/system/snapcompact-system-stub.md +1 -0
  1039. package/src/prompts/system/snapcompact-toolresult-note.md +1 -0
  1040. package/src/prompts/system/speech-rewrite.md +15 -0
  1041. package/src/prompts/system/subagent-async-pending.md +6 -0
  1042. package/src/prompts/system/subagent-system-prompt.md +73 -0
  1043. package/src/prompts/system/subagent-user-prompt.md +3 -0
  1044. package/src/prompts/system/subagent-yield-reminder.md +23 -0
  1045. package/src/prompts/system/system-prompt.md +263 -0
  1046. package/src/prompts/system/tan-context-switch.md +17 -0
  1047. package/src/prompts/system/task-label.md +23 -0
  1048. package/src/prompts/system/thinking-loop-redirect.md +10 -0
  1049. package/src/prompts/system/title-marker-instruction.md +1 -0
  1050. package/src/prompts/system/title-system.md +16 -0
  1051. package/src/prompts/system/tool-call-loop-redirect.md +8 -0
  1052. package/src/prompts/system/ttsr-interrupt.md +7 -0
  1053. package/src/prompts/system/ttsr-tool-reminder.md +5 -0
  1054. package/src/prompts/system/ultrathink-notice.md +3 -0
  1055. package/src/prompts/system/unexpected-stop-classifier.md +17 -0
  1056. package/src/prompts/system/unexpected-stop-retry.md +4 -0
  1057. package/src/prompts/system/vibe-mode-active.md +26 -0
  1058. package/src/prompts/system/web-search.md +25 -0
  1059. package/src/prompts/system/workflow-notice.md +112 -0
  1060. package/src/prompts/system/xdev-mount-notice.md +20 -0
  1061. package/src/prompts/tools/apply-patch.md +65 -0
  1062. package/src/prompts/tools/ask.md +22 -0
  1063. package/src/prompts/tools/ast-edit.md +11 -0
  1064. package/src/prompts/tools/ast-grep.md +19 -0
  1065. package/src/prompts/tools/async-result.md +8 -0
  1066. package/src/prompts/tools/bash.md +23 -0
  1067. package/src/prompts/tools/browser.md +29 -0
  1068. package/src/prompts/tools/checkpoint.md +15 -0
  1069. package/src/prompts/tools/computer.md +26 -0
  1070. package/src/prompts/tools/debug.md +3 -0
  1071. package/src/prompts/tools/eval.md +45 -0
  1072. package/src/prompts/tools/github.md +22 -0
  1073. package/src/prompts/tools/glob.md +16 -0
  1074. package/src/prompts/tools/goal.md +11 -0
  1075. package/src/prompts/tools/grep.md +13 -0
  1076. package/src/prompts/tools/hub.md +34 -0
  1077. package/src/prompts/tools/image-attachment-describe-system.md +8 -0
  1078. package/src/prompts/tools/image-attachment-describe.md +10 -0
  1079. package/src/prompts/tools/image-gen.md +7 -0
  1080. package/src/prompts/tools/inspect-image-system.md +20 -0
  1081. package/src/prompts/tools/inspect-image.md +22 -0
  1082. package/src/prompts/tools/learn.md +7 -0
  1083. package/src/prompts/tools/lsp-late-diagnostic.md +8 -0
  1084. package/src/prompts/tools/lsp.md +19 -0
  1085. package/src/prompts/tools/manage-skill.md +9 -0
  1086. package/src/prompts/tools/memory-edit.md +12 -0
  1087. package/src/prompts/tools/patch.md +57 -0
  1088. package/src/prompts/tools/read.md +27 -0
  1089. package/src/prompts/tools/recall.md +7 -0
  1090. package/src/prompts/tools/reflect.md +5 -0
  1091. package/src/prompts/tools/replace.md +30 -0
  1092. package/src/prompts/tools/retain.md +6 -0
  1093. package/src/prompts/tools/rewind.md +14 -0
  1094. package/src/prompts/tools/security-publish.md +1 -0
  1095. package/src/prompts/tools/security-scan.md +1 -0
  1096. package/src/prompts/tools/task-async-contract.md +1 -0
  1097. package/src/prompts/tools/task-summary.md +20 -0
  1098. package/src/prompts/tools/task.md +83 -0
  1099. package/src/prompts/tools/todo.md +42 -0
  1100. package/src/prompts/tools/vibe-kill.md +3 -0
  1101. package/src/prompts/tools/vibe-list.md +3 -0
  1102. package/src/prompts/tools/vibe-send.md +9 -0
  1103. package/src/prompts/tools/vibe-spawn.md +10 -0
  1104. package/src/prompts/tools/vibe-turn-result.md +19 -0
  1105. package/src/prompts/tools/vibe-wait.md +8 -0
  1106. package/src/prompts/tools/web-search.md +8 -0
  1107. package/src/prompts/tools/write.md +14 -0
  1108. package/src/registry/agent-lifecycle.ts +505 -0
  1109. package/src/registry/agent-registry.ts +287 -0
  1110. package/src/registry/persisted-agents.ts +423 -0
  1111. package/src/sdk.ts +3820 -0
  1112. package/src/secrets/index.ts +375 -0
  1113. package/src/secrets/obfuscator.ts +2629 -0
  1114. package/src/secrets/regex.ts +21 -0
  1115. package/src/security/auth.ts +98 -0
  1116. package/src/security/cloud.ts +686 -0
  1117. package/src/security/comparison.ts +255 -0
  1118. package/src/security/contracts/ids.ts +111 -0
  1119. package/src/security/contracts/index.ts +4 -0
  1120. package/src/security/contracts/schemas.ts +219 -0
  1121. package/src/security/contracts/types.ts +254 -0
  1122. package/src/security/contracts/validation.ts +69 -0
  1123. package/src/security/coordinator.ts +708 -0
  1124. package/src/security/importers/codex-security.ts +387 -0
  1125. package/src/security/importers/index.ts +2 -0
  1126. package/src/security/importers/sarif.ts +357 -0
  1127. package/src/security/index.ts +13 -0
  1128. package/src/security/preflight.ts +405 -0
  1129. package/src/security/provenance.ts +106 -0
  1130. package/src/security/publication.ts +326 -0
  1131. package/src/security/remediation.ts +93 -0
  1132. package/src/security/resource-output.ts +50 -0
  1133. package/src/security/sarif.ts +78 -0
  1134. package/src/security/store.ts +430 -0
  1135. package/src/session/acp-permission-gate.ts +165 -0
  1136. package/src/session/agent-session-events.ts +67 -0
  1137. package/src/session/agent-session-types.ts +407 -0
  1138. package/src/session/agent-session.ts +9000 -0
  1139. package/src/session/agent-storage.ts +807 -0
  1140. package/src/session/artifacts.ts +154 -0
  1141. package/src/session/async-job-delivery.ts +82 -0
  1142. package/src/session/auth-broker-config.ts +92 -0
  1143. package/src/session/auth-storage.ts +25 -0
  1144. package/src/session/bash-runner.ts +326 -0
  1145. package/src/session/blob-store.ts +295 -0
  1146. package/src/session/checkpoint-entries.ts +81 -0
  1147. package/src/session/claude-session-store.ts +426 -0
  1148. package/src/session/client-bridge.ts +85 -0
  1149. package/src/session/codex-auto-reset.ts +673 -0
  1150. package/src/session/codex-session-store.ts +673 -0
  1151. package/src/session/compact-modes.ts +105 -0
  1152. package/src/session/credential-pin.ts +93 -0
  1153. package/src/session/eval-runner.ts +219 -0
  1154. package/src/session/exit-diagnostics.ts +310 -0
  1155. package/src/session/foreign-session-import.ts +52 -0
  1156. package/src/session/foreign-session-jsonl.ts +29 -0
  1157. package/src/session/foreign-session-store.ts +26 -0
  1158. package/src/session/history-storage.ts +329 -0
  1159. package/src/session/indexed-session-storage.ts +553 -0
  1160. package/src/session/irc-bridge.ts +203 -0
  1161. package/src/session/launch-completion.ts +37 -0
  1162. package/src/session/messages.ts +1304 -0
  1163. package/src/session/model-controls.ts +757 -0
  1164. package/src/session/prewalk.ts +279 -0
  1165. package/src/session/provider-image-budget.ts +86 -0
  1166. package/src/session/queued-messages.ts +99 -0
  1167. package/src/session/redis-session-storage.ts +257 -0
  1168. package/src/session/retry-fallback-chains.ts +455 -0
  1169. package/src/session/role-models.ts +85 -0
  1170. package/src/session/session-advisors.ts +1896 -0
  1171. package/src/session/session-context.ts +584 -0
  1172. package/src/session/session-dump-format.ts +216 -0
  1173. package/src/session/session-entries.ts +306 -0
  1174. package/src/session/session-handoff.ts +325 -0
  1175. package/src/session/session-history-format.ts +459 -0
  1176. package/src/session/session-listing.ts +715 -0
  1177. package/src/session/session-loader.ts +362 -0
  1178. package/src/session/session-maintenance.ts +3115 -0
  1179. package/src/session/session-manager.ts +2747 -0
  1180. package/src/session/session-memory.ts +222 -0
  1181. package/src/session/session-metadata.ts +53 -0
  1182. package/src/session/session-migrations.ts +78 -0
  1183. package/src/session/session-paths.ts +280 -0
  1184. package/src/session/session-persistence.ts +293 -0
  1185. package/src/session/session-provider-boundary.ts +306 -0
  1186. package/src/session/session-stats.ts +349 -0
  1187. package/src/session/session-storage.ts +774 -0
  1188. package/src/session/session-title-slot.ts +141 -0
  1189. package/src/session/session-tools.ts +1385 -0
  1190. package/src/session/session-workspace.ts +53 -0
  1191. package/src/session/settings-stream-fn.ts +79 -0
  1192. package/src/session/shake-types.ts +43 -0
  1193. package/src/session/snapcompact-inline.ts +545 -0
  1194. package/src/session/snapcompact-savings-journal.ts +113 -0
  1195. package/src/session/sql-session-storage.ts +374 -0
  1196. package/src/session/stream-guards.ts +417 -0
  1197. package/src/session/streaming-output.ts +1459 -0
  1198. package/src/session/todo-tracker.ts +380 -0
  1199. package/src/session/tool-choice-queue.ts +305 -0
  1200. package/src/session/ttsr-coordinator.ts +496 -0
  1201. package/src/session/turn-persistence.ts +142 -0
  1202. package/src/session/turn-recovery.ts +2005 -0
  1203. package/src/session/unexpected-stop-classifier.ts +141 -0
  1204. package/src/session/yield-queue.ts +288 -0
  1205. package/src/slash-commands/acp-builtins.ts +70 -0
  1206. package/src/slash-commands/available-commands.ts +105 -0
  1207. package/src/slash-commands/builtin-registry.ts +3149 -0
  1208. package/src/slash-commands/helpers/active-oauth-account.ts +80 -0
  1209. package/src/slash-commands/helpers/collab-qrcode.ts +28 -0
  1210. package/src/slash-commands/helpers/context-report.ts +66 -0
  1211. package/src/slash-commands/helpers/format.ts +46 -0
  1212. package/src/slash-commands/helpers/logout.ts +108 -0
  1213. package/src/slash-commands/helpers/marketplace-manager.ts +25 -0
  1214. package/src/slash-commands/helpers/mcp.ts +533 -0
  1215. package/src/slash-commands/helpers/parse.ts +85 -0
  1216. package/src/slash-commands/helpers/reset-usage.ts +68 -0
  1217. package/src/slash-commands/helpers/security.ts +451 -0
  1218. package/src/slash-commands/helpers/session-pin.ts +44 -0
  1219. package/src/slash-commands/helpers/ssh.ts +196 -0
  1220. package/src/slash-commands/helpers/stats-dashboard.ts +86 -0
  1221. package/src/slash-commands/helpers/todo.ts +285 -0
  1222. package/src/slash-commands/helpers/usage-report.ts +198 -0
  1223. package/src/slash-commands/marketplace-install-parser.ts +99 -0
  1224. package/src/slash-commands/types.ts +139 -0
  1225. package/src/ssh/config-writer.ts +183 -0
  1226. package/src/ssh/connection-manager.ts +667 -0
  1227. package/src/ssh/file-transfer.ts +214 -0
  1228. package/src/ssh/sshfs-mount.ts +163 -0
  1229. package/src/ssh/utils.ts +51 -0
  1230. package/src/startup-splash.ts +19 -0
  1231. package/src/stt/asr-client.ts +401 -0
  1232. package/src/stt/asr-protocol.ts +65 -0
  1233. package/src/stt/asr-worker.ts +603 -0
  1234. package/src/stt/downloader.ts +142 -0
  1235. package/src/stt/endpointer.ts +259 -0
  1236. package/src/stt/index.ts +6 -0
  1237. package/src/stt/models.ts +150 -0
  1238. package/src/stt/sherpa-runtime.ts +71 -0
  1239. package/src/stt/stt-controller.ts +320 -0
  1240. package/src/stt/submit-trigger.ts +74 -0
  1241. package/src/subprocess/worker-client.ts +463 -0
  1242. package/src/subprocess/worker-runtime.ts +494 -0
  1243. package/src/system-prompt.ts +921 -0
  1244. package/src/task/agents.ts +170 -0
  1245. package/src/task/commands.ts +132 -0
  1246. package/src/task/discovery.ts +145 -0
  1247. package/src/task/executor.ts +3431 -0
  1248. package/src/task/index.ts +1515 -0
  1249. package/src/task/isolation-ownership.ts +106 -0
  1250. package/src/task/isolation-runner.ts +444 -0
  1251. package/src/task/label.ts +40 -0
  1252. package/src/task/name-generator.ts +1577 -0
  1253. package/src/task/omp-command.ts +26 -0
  1254. package/src/task/output-manager.ts +115 -0
  1255. package/src/task/parallel.ts +221 -0
  1256. package/src/task/persisted-revive.ts +173 -0
  1257. package/src/task/prewalk.ts +6 -0
  1258. package/src/task/prompt-policy.ts +8 -0
  1259. package/src/task/provider-concurrency.ts +100 -0
  1260. package/src/task/read-only-policy.ts +27 -0
  1261. package/src/task/render.ts +1830 -0
  1262. package/src/task/renderer.ts +14 -0
  1263. package/src/task/repair-args.ts +118 -0
  1264. package/src/task/spawn-policy.ts +72 -0
  1265. package/src/task/structured-subagent.ts +676 -0
  1266. package/src/task/subprocess-tool-registry.ts +88 -0
  1267. package/src/task/types.ts +555 -0
  1268. package/src/task/worktree.ts +967 -0
  1269. package/src/task/yield-assembly.ts +198 -0
  1270. package/src/telemetry-export.ts +504 -0
  1271. package/src/thinking.ts +377 -0
  1272. package/src/tiny/device.ts +111 -0
  1273. package/src/tiny/dtype.ts +101 -0
  1274. package/src/tiny/message-preproc.ts +155 -0
  1275. package/src/tiny/models.ts +268 -0
  1276. package/src/tiny/text.ts +290 -0
  1277. package/src/tiny/title-client.ts +460 -0
  1278. package/src/tiny/title-protocol.ts +56 -0
  1279. package/src/tiny/worker.ts +354 -0
  1280. package/src/tools/acp-bridge.ts +125 -0
  1281. package/src/tools/approval.ts +245 -0
  1282. package/src/tools/ask.ts +1459 -0
  1283. package/src/tools/ast-edit.ts +718 -0
  1284. package/src/tools/ast-grep.ts +526 -0
  1285. package/src/tools/auto-generated-guard.ts +335 -0
  1286. package/src/tools/bash-interactive.ts +435 -0
  1287. package/src/tools/bash-interceptor.ts +148 -0
  1288. package/src/tools/bash-pty-selection.ts +14 -0
  1289. package/src/tools/bash-skill-urls.ts +350 -0
  1290. package/src/tools/bash.ts +1826 -0
  1291. package/src/tools/browser/aria/aria-snapshot.bundle.txt +7 -0
  1292. package/src/tools/browser/aria/aria-snapshot.ts +131 -0
  1293. package/src/tools/browser/attach.ts +219 -0
  1294. package/src/tools/browser/cmux/cmux-tab.ts +1531 -0
  1295. package/src/tools/browser/cmux/rpc.ts +200 -0
  1296. package/src/tools/browser/cmux/socket-client.ts +445 -0
  1297. package/src/tools/browser/launch.ts +939 -0
  1298. package/src/tools/browser/readable.ts +112 -0
  1299. package/src/tools/browser/registry.ts +447 -0
  1300. package/src/tools/browser/relay/bridge.ts +945 -0
  1301. package/src/tools/browser/relay/daemon.ts +117 -0
  1302. package/src/tools/browser/relay/extension-assets/background.js.txt +242 -0
  1303. package/src/tools/browser/relay/extension-assets/manifest.json.txt +14 -0
  1304. package/src/tools/browser/relay/extension-assets/options.html.txt +53 -0
  1305. package/src/tools/browser/relay/extension-assets/options.js.txt +23 -0
  1306. package/src/tools/browser/relay/kind.ts +41 -0
  1307. package/src/tools/browser/relay/protocol.ts +54 -0
  1308. package/src/tools/browser/relay/server.ts +141 -0
  1309. package/src/tools/browser/render.ts +229 -0
  1310. package/src/tools/browser/run-output.ts +76 -0
  1311. package/src/tools/browser/shared-daemon.ts +139 -0
  1312. package/src/tools/browser/tab-protocol.ts +123 -0
  1313. package/src/tools/browser/tab-supervisor.ts +1077 -0
  1314. package/src/tools/browser/tab-worker-entry.ts +29 -0
  1315. package/src/tools/browser/tab-worker.ts +1982 -0
  1316. package/src/tools/browser.ts +487 -0
  1317. package/src/tools/builtin-names.ts +67 -0
  1318. package/src/tools/checkpoint.ts +137 -0
  1319. package/src/tools/computer/exposure.ts +14 -0
  1320. package/src/tools/computer/protocol.ts +69 -0
  1321. package/src/tools/computer/supervisor.ts +409 -0
  1322. package/src/tools/computer/worker-entry.ts +39 -0
  1323. package/src/tools/computer/worker.ts +745 -0
  1324. package/src/tools/computer-renderer.ts +147 -0
  1325. package/src/tools/computer.ts +222 -0
  1326. package/src/tools/conflict-detect.ts +815 -0
  1327. package/src/tools/context.ts +49 -0
  1328. package/src/tools/debug.ts +1121 -0
  1329. package/src/tools/default-renderer.ts +154 -0
  1330. package/src/tools/essential-tools.ts +46 -0
  1331. package/src/tools/eval-backends.ts +34 -0
  1332. package/src/tools/eval-format/index.ts +24 -0
  1333. package/src/tools/eval-format/javascript.ts +952 -0
  1334. package/src/tools/eval-format/julia.ts +446 -0
  1335. package/src/tools/eval-format/python.ts +544 -0
  1336. package/src/tools/eval-format/ruby.ts +380 -0
  1337. package/src/tools/eval-render.ts +784 -0
  1338. package/src/tools/eval.ts +774 -0
  1339. package/src/tools/fetch.ts +1889 -0
  1340. package/src/tools/file-recorder.ts +35 -0
  1341. package/src/tools/fs-cache-invalidation.ts +28 -0
  1342. package/src/tools/gh-cache-invalidation.ts +175 -0
  1343. package/src/tools/gh-format.ts +12 -0
  1344. package/src/tools/gh-renderer.ts +484 -0
  1345. package/src/tools/gh.ts +3958 -0
  1346. package/src/tools/github-cache.ts +663 -0
  1347. package/src/tools/glob.ts +691 -0
  1348. package/src/tools/grep.ts +1920 -0
  1349. package/src/tools/grouped-file-output.ts +210 -0
  1350. package/src/tools/hub/index.ts +579 -0
  1351. package/src/tools/hub/jobs.ts +714 -0
  1352. package/src/tools/hub/launch.ts +690 -0
  1353. package/src/tools/hub/messaging.ts +735 -0
  1354. package/src/tools/hub/types.ts +117 -0
  1355. package/src/tools/image-gen.ts +1689 -0
  1356. package/src/tools/image-providers.ts +50 -0
  1357. package/src/tools/index.ts +736 -0
  1358. package/src/tools/inspect-image-renderer.ts +133 -0
  1359. package/src/tools/inspect-image.ts +316 -0
  1360. package/src/tools/json-tree.ts +260 -0
  1361. package/src/tools/jtd-to-json-schema.ts +219 -0
  1362. package/src/tools/jtd-to-typescript.ts +136 -0
  1363. package/src/tools/jtd-utils.ts +102 -0
  1364. package/src/tools/learn.ts +141 -0
  1365. package/src/tools/list-limit.ts +40 -0
  1366. package/src/tools/manage-skill.ts +102 -0
  1367. package/src/tools/match-line-format.ts +20 -0
  1368. package/src/tools/memory-edit.ts +61 -0
  1369. package/src/tools/memory-recall.ts +102 -0
  1370. package/src/tools/memory-reflect.ts +88 -0
  1371. package/src/tools/memory-render.ts +211 -0
  1372. package/src/tools/memory-retain.ts +89 -0
  1373. package/src/tools/output-meta.ts +860 -0
  1374. package/src/tools/output-schema-validator.ts +307 -0
  1375. package/src/tools/path-utils.ts +1489 -0
  1376. package/src/tools/plan-mode-guard.ts +155 -0
  1377. package/src/tools/puppeteer/00_stealth_tampering.txt +44 -0
  1378. package/src/tools/puppeteer/01_stealth_activity.txt +80 -0
  1379. package/src/tools/puppeteer/02_stealth_hairline.txt +57 -0
  1380. package/src/tools/puppeteer/03_stealth_botd.txt +380 -0
  1381. package/src/tools/puppeteer/04_stealth_iframe.txt +174 -0
  1382. package/src/tools/puppeteer/05_stealth_webgl.txt +233 -0
  1383. package/src/tools/puppeteer/06_stealth_screen.txt +260 -0
  1384. package/src/tools/puppeteer/07_stealth_fonts.txt +99 -0
  1385. package/src/tools/puppeteer/08_stealth_audio.txt +63 -0
  1386. package/src/tools/puppeteer/09_stealth_locale.txt +51 -0
  1387. package/src/tools/puppeteer/10_stealth_plugins.txt +212 -0
  1388. package/src/tools/puppeteer/11_stealth_hardware.txt +59 -0
  1389. package/src/tools/puppeteer/12_stealth_codecs.txt +42 -0
  1390. package/src/tools/puppeteer/13_stealth_worker.txt +235 -0
  1391. package/src/tools/read.ts +3762 -0
  1392. package/src/tools/render-utils.ts +926 -0
  1393. package/src/tools/renderers.ts +133 -0
  1394. package/src/tools/report-tool-issue.ts +568 -0
  1395. package/src/tools/resolve.ts +423 -0
  1396. package/src/tools/review.ts +103 -0
  1397. package/src/tools/run-scope.ts +417 -0
  1398. package/src/tools/security-scan.ts +287 -0
  1399. package/src/tools/shell-tokenize.ts +213 -0
  1400. package/src/tools/sqlite-reader.ts +884 -0
  1401. package/src/tools/terminal-output.ts +141 -0
  1402. package/src/tools/todo.ts +1226 -0
  1403. package/src/tools/tool-errors.ts +62 -0
  1404. package/src/tools/tool-result.ts +102 -0
  1405. package/src/tools/tool-timeouts.ts +38 -0
  1406. package/src/tools/tts.ts +266 -0
  1407. package/src/tools/vibe.ts +608 -0
  1408. package/src/tools/write.ts +1646 -0
  1409. package/src/tools/xdev.ts +560 -0
  1410. package/src/tools/yield.ts +486 -0
  1411. package/src/tts/downloader.ts +64 -0
  1412. package/src/tts/index.ts +10 -0
  1413. package/src/tts/models.ts +137 -0
  1414. package/src/tts/runtime.ts +21 -0
  1415. package/src/tts/speakable.ts +392 -0
  1416. package/src/tts/speech-enhancer.ts +206 -0
  1417. package/src/tts/streaming-player.ts +120 -0
  1418. package/src/tts/tts-client.ts +475 -0
  1419. package/src/tts/tts-protocol.ts +69 -0
  1420. package/src/tts/tts-worker.ts +434 -0
  1421. package/src/tts/vocalizer.ts +419 -0
  1422. package/src/tts/wav.ts +58 -0
  1423. package/src/tui/code-cell.ts +268 -0
  1424. package/src/tui/file-list.ts +55 -0
  1425. package/src/tui/hyperlink.ts +178 -0
  1426. package/src/tui/index.ts +13 -0
  1427. package/src/tui/output-block.ts +268 -0
  1428. package/src/tui/status-line.ts +54 -0
  1429. package/src/tui/tree-list.ts +172 -0
  1430. package/src/tui/types.ts +15 -0
  1431. package/src/tui/utils.ts +103 -0
  1432. package/src/tui/width-aware-text.ts +58 -0
  1433. package/src/utils/active-repo-context.ts +143 -0
  1434. package/src/utils/block-context.ts +312 -0
  1435. package/src/utils/changelog.ts +373 -0
  1436. package/src/utils/clipboard.ts +361 -0
  1437. package/src/utils/command-args.ts +74 -0
  1438. package/src/utils/commit-message-generator.ts +148 -0
  1439. package/src/utils/cpuprofile.ts +235 -0
  1440. package/src/utils/edit-mode.ts +61 -0
  1441. package/src/utils/enhanced-paste.ts +230 -0
  1442. package/src/utils/event-bus.ts +33 -0
  1443. package/src/utils/external-editor.ts +80 -0
  1444. package/src/utils/fetch-timeout.ts +10 -0
  1445. package/src/utils/file-display-mode.ts +44 -0
  1446. package/src/utils/file-mentions.ts +293 -0
  1447. package/src/utils/git.ts +2472 -0
  1448. package/src/utils/image-loading.ts +236 -0
  1449. package/src/utils/image-resize.ts +420 -0
  1450. package/src/utils/image-vision-fallback.ts +196 -0
  1451. package/src/utils/inspect-image-mode.ts +39 -0
  1452. package/src/utils/ipc.ts +38 -0
  1453. package/src/utils/jj.ts +422 -0
  1454. package/src/utils/lang-from-path.ts +251 -0
  1455. package/src/utils/late-cleanup.ts +17 -0
  1456. package/src/utils/local-date.ts +7 -0
  1457. package/src/utils/mac-file-urls.applescript +37 -0
  1458. package/src/utils/markit-cache.ts +166 -0
  1459. package/src/utils/markit.ts +223 -0
  1460. package/src/utils/mupdf-wasm-embed.ts +12 -0
  1461. package/src/utils/open.ts +126 -0
  1462. package/src/utils/profile-tree.ts +111 -0
  1463. package/src/utils/prompt-path.ts +3 -0
  1464. package/src/utils/qrcode.ts +535 -0
  1465. package/src/utils/sample-profile.ts +437 -0
  1466. package/src/utils/session-color.ts +142 -0
  1467. package/src/utils/shell-snapshot-fn-env.sh +63 -0
  1468. package/src/utils/shell-snapshot.ts +326 -0
  1469. package/src/utils/sixel.ts +69 -0
  1470. package/src/utils/thinking-display.ts +163 -0
  1471. package/src/utils/title-generator.ts +597 -0
  1472. package/src/utils/token-rate.ts +72 -0
  1473. package/src/utils/tool-choice.ts +50 -0
  1474. package/src/utils/tools-manager.ts +411 -0
  1475. package/src/utils/turndown.ts +82 -0
  1476. package/src/utils/zip.ts +1106 -0
  1477. package/src/vibe/runtime.ts +1684 -0
  1478. package/src/vibe/state.ts +4 -0
  1479. package/src/web/kagi.ts +305 -0
  1480. package/src/web/parallel.ts +354 -0
  1481. package/src/web/scrapers/artifacthub.ts +207 -0
  1482. package/src/web/scrapers/arxiv.ts +83 -0
  1483. package/src/web/scrapers/aur.ts +162 -0
  1484. package/src/web/scrapers/biorxiv.ts +133 -0
  1485. package/src/web/scrapers/bluesky.ts +262 -0
  1486. package/src/web/scrapers/brew.ts +172 -0
  1487. package/src/web/scrapers/cheatsh.ts +68 -0
  1488. package/src/web/scrapers/chocolatey.ts +196 -0
  1489. package/src/web/scrapers/choosealicense.ts +95 -0
  1490. package/src/web/scrapers/cisa-kev.ts +87 -0
  1491. package/src/web/scrapers/clojars.ts +154 -0
  1492. package/src/web/scrapers/coingecko.ts +177 -0
  1493. package/src/web/scrapers/crates-io.ts +97 -0
  1494. package/src/web/scrapers/crossref.ts +136 -0
  1495. package/src/web/scrapers/devto.ts +147 -0
  1496. package/src/web/scrapers/discogs.ts +306 -0
  1497. package/src/web/scrapers/discourse.ts +197 -0
  1498. package/src/web/scrapers/dockerhub.ts +138 -0
  1499. package/src/web/scrapers/docs-rs.ts +663 -0
  1500. package/src/web/scrapers/fdroid.ts +134 -0
  1501. package/src/web/scrapers/firefox-addons.ts +191 -0
  1502. package/src/web/scrapers/flathub.ts +223 -0
  1503. package/src/web/scrapers/github-gist.ts +58 -0
  1504. package/src/web/scrapers/github.ts +800 -0
  1505. package/src/web/scrapers/gitlab.ts +401 -0
  1506. package/src/web/scrapers/go-pkg.ts +266 -0
  1507. package/src/web/scrapers/hackage.ts +129 -0
  1508. package/src/web/scrapers/hackernews.ts +189 -0
  1509. package/src/web/scrapers/hex.ts +105 -0
  1510. package/src/web/scrapers/huggingface.ts +321 -0
  1511. package/src/web/scrapers/iacr.ts +89 -0
  1512. package/src/web/scrapers/index.ts +252 -0
  1513. package/src/web/scrapers/jetbrains-marketplace.ts +159 -0
  1514. package/src/web/scrapers/lemmy.ts +203 -0
  1515. package/src/web/scrapers/lobsters.ts +175 -0
  1516. package/src/web/scrapers/mastodon.ts +292 -0
  1517. package/src/web/scrapers/maven.ts +138 -0
  1518. package/src/web/scrapers/mdn.ts +173 -0
  1519. package/src/web/scrapers/metacpan.ts +222 -0
  1520. package/src/web/scrapers/musicbrainz.ts +250 -0
  1521. package/src/web/scrapers/npm.ts +98 -0
  1522. package/src/web/scrapers/nuget.ts +183 -0
  1523. package/src/web/scrapers/nvd.ts +222 -0
  1524. package/src/web/scrapers/ollama.ts +239 -0
  1525. package/src/web/scrapers/open-vsx.ts +106 -0
  1526. package/src/web/scrapers/opencorporates.ts +292 -0
  1527. package/src/web/scrapers/openlibrary.ts +336 -0
  1528. package/src/web/scrapers/orcid.ts +286 -0
  1529. package/src/web/scrapers/osv.ts +176 -0
  1530. package/src/web/scrapers/packagist.ts +160 -0
  1531. package/src/web/scrapers/pub-dev.ts +143 -0
  1532. package/src/web/scrapers/pubmed.ts +211 -0
  1533. package/src/web/scrapers/pypi.ts +112 -0
  1534. package/src/web/scrapers/rawg.ts +110 -0
  1535. package/src/web/scrapers/readthedocs.ts +121 -0
  1536. package/src/web/scrapers/reddit.ts +95 -0
  1537. package/src/web/scrapers/repology.ts +251 -0
  1538. package/src/web/scrapers/rfc.ts +201 -0
  1539. package/src/web/scrapers/rubygems.ts +103 -0
  1540. package/src/web/scrapers/searchcode.ts +189 -0
  1541. package/src/web/scrapers/sec-edgar.ts +261 -0
  1542. package/src/web/scrapers/semantic-scholar.ts +171 -0
  1543. package/src/web/scrapers/snapcraft.ts +187 -0
  1544. package/src/web/scrapers/sourcegraph.ts +336 -0
  1545. package/src/web/scrapers/spdx.ts +108 -0
  1546. package/src/web/scrapers/spotify.ts +198 -0
  1547. package/src/web/scrapers/stackoverflow.ts +120 -0
  1548. package/src/web/scrapers/terraform.ts +277 -0
  1549. package/src/web/scrapers/tldr.ts +47 -0
  1550. package/src/web/scrapers/twitter.ts +94 -0
  1551. package/src/web/scrapers/types.ts +354 -0
  1552. package/src/web/scrapers/utils.ts +109 -0
  1553. package/src/web/scrapers/vimeo.ts +133 -0
  1554. package/src/web/scrapers/vscode-marketplace.ts +187 -0
  1555. package/src/web/scrapers/w3c.ts +156 -0
  1556. package/src/web/scrapers/wikidata.ts +344 -0
  1557. package/src/web/scrapers/wikipedia.ts +84 -0
  1558. package/src/web/scrapers/youtube.ts +325 -0
  1559. package/src/web/search/index.ts +393 -0
  1560. package/src/web/search/provider.ts +272 -0
  1561. package/src/web/search/providers/anthropic.ts +406 -0
  1562. package/src/web/search/providers/base.ts +112 -0
  1563. package/src/web/search/providers/brave.ts +181 -0
  1564. package/src/web/search/providers/browser-headers.ts +82 -0
  1565. package/src/web/search/providers/browser-page.ts +125 -0
  1566. package/src/web/search/providers/codex.ts +788 -0
  1567. package/src/web/search/providers/duckduckgo.ts +382 -0
  1568. package/src/web/search/providers/ecosia.ts +183 -0
  1569. package/src/web/search/providers/exa.ts +513 -0
  1570. package/src/web/search/providers/firecrawl.ts +213 -0
  1571. package/src/web/search/providers/gemini.ts +634 -0
  1572. package/src/web/search/providers/google.ts +195 -0
  1573. package/src/web/search/providers/jina.ts +144 -0
  1574. package/src/web/search/providers/kagi.ts +98 -0
  1575. package/src/web/search/providers/kimi.ts +221 -0
  1576. package/src/web/search/providers/mojeek.ts +220 -0
  1577. package/src/web/search/providers/parallel.ts +186 -0
  1578. package/src/web/search/providers/perplexity-auth.ts +142 -0
  1579. package/src/web/search/providers/perplexity.ts +997 -0
  1580. package/src/web/search/providers/public.ts +199 -0
  1581. package/src/web/search/providers/searxng.ts +467 -0
  1582. package/src/web/search/providers/startpage.ts +225 -0
  1583. package/src/web/search/providers/synthetic.ts +126 -0
  1584. package/src/web/search/providers/tavily.ts +244 -0
  1585. package/src/web/search/providers/tinyfish.ts +166 -0
  1586. package/src/web/search/providers/utils.ts +130 -0
  1587. package/src/web/search/providers/xai.ts +372 -0
  1588. package/src/web/search/providers/zai.ts +452 -0
  1589. package/src/web/search/query.ts +850 -0
  1590. package/src/web/search/render.ts +262 -0
  1591. package/src/web/search/types.ts +518 -0
  1592. package/src/web/search/utils.ts +17 -0
  1593. package/src/workspace-tree.ts +326 -0
@@ -0,0 +1,2629 @@
1
+ import * as crypto from "node:crypto";
2
+ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
3
+ import type { AssistantMessage, Context, ImageContent, Message, TextContent } from "@oh-my-pi/pi-ai";
4
+ import type { SessionContext } from "../session/session-context";
5
+ import { compileSecretRegex } from "./regex";
6
+
7
+ // ═══════════════════════════════════════════════════════════════════════════
8
+ // Types
9
+ // ═══════════════════════════════════════════════════════════════════════════
10
+
11
+ export interface SecretEntry {
12
+ type: "plain" | "regex";
13
+ content: string;
14
+ mode?: "obfuscate" | "replace";
15
+ replacement?: string;
16
+ flags?: string;
17
+ friendlyName?: string;
18
+ }
19
+
20
+ export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue | undefined };
21
+ export type JsonRecord = { [key: string]: JsonValue | undefined };
22
+
23
+ // ═══════════════════════════════════════════════════════════════════════════
24
+ // Deterministic replacement generation
25
+ // ═══════════════════════════════════════════════════════════════════════════
26
+
27
+ const REPLACEMENT_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
28
+ const NONMATCHING_REPLACEMENT_CHARS = `${REPLACEMENT_CHARS}!#$%&()*+,-./:;<=>?@[]^_{|}~`;
29
+ // Whitespace bytes used to build last-resort redactions for a default replace
30
+ // regex that matches every non-whitespace candidate (e.g. `\S{n}`). Only
31
+ // `space`/`tab` are used — never a line terminator — so a `.`-style
32
+ // match-everything regex (which matches space and tab but not `\n`) still
33
+ // exhausts to the sentinel instead of redacting to a newline run.
34
+ const WHITESPACE_REPLACEMENT_CHARS = " \t";
35
+
36
+ /** Generate a deterministic same-length replacement string from a secret value. */
37
+ function generateDeterministicReplacement(secret: string): string {
38
+ if (secret.length === 0) return "";
39
+ // Prefix generated chunks with a fixed `ZZ` so re-redacting an already-emitted
40
+ // 1–2 char chunk is a fixed point (the deterministic replacement of a <=2-char
41
+ // value is itself `Z`/`ZZ`), keeping short default-replacement remainders next
42
+ // to a reversible placeholder stable across an obfuscator restart.
43
+ const hash = BigInt(Bun.hash(secret));
44
+ const chars = secret.length === 1 ? ["Z"] : ["Z", "Z"];
45
+ let h = hash;
46
+ for (let i = chars.length; i < secret.length; i++) {
47
+ h = h ^ (BigInt(i + 1) * 0x9e3779b97f4a7c15n);
48
+ const idx = Number((h < 0n ? -h : h) % BigInt(REPLACEMENT_CHARS.length));
49
+ chars.push(REPLACEMENT_CHARS[idx]);
50
+ }
51
+ return chars.join("");
52
+ }
53
+
54
+ /**
55
+ * Force a length-preserving deterministic replacement to differ from the secret
56
+ * it stands in for. `generateDeterministicReplacement` seeds its first 1–2 chars
57
+ * with the `Z`/`ZZ` sentinel, so a whole configured value that is exactly `Z` or
58
+ * `ZZ` (or an astronomically unlikely longer hash collision) would otherwise be
59
+ * emitted unchanged and ship the raw secret to the provider. Flip the first char
60
+ * to a fixed different glyph: same length, still deterministic, guaranteed != the
61
+ * secret. Only safe for a whole CONFIGURED value (a plain secret matches its own
62
+ * literal, so the perturbed output is no longer matched and stays a fixed point);
63
+ * per-chunk remainders must keep the sentinel to remain idempotent across restart.
64
+ */
65
+ function ensureDistinctReplacement(replacement: string, secret: string): string {
66
+ if (replacement.length === 0 || replacement !== secret) return replacement;
67
+ const alt = replacement[0] === REPLACEMENT_CHARS[0] ? REPLACEMENT_CHARS[1] : REPLACEMENT_CHARS[0];
68
+ return alt + replacement.slice(1);
69
+ }
70
+
71
+ // How far left of the matched span the re-match scan begins looking for a match
72
+ // that overlaps the candidate. This bounds ONLY the match-start search position,
73
+ // never the lookbehind/lookahead context: the probe below substitutes the
74
+ // candidate into the FULL text, so a regex's lookbehind/lookahead assertions
75
+ // always evaluate against complete context regardless of width. The single
76
+ // re-match this misses is one that begins more than this many bytes before the
77
+ // span and extends into it (a single match longer than the window) — that only
78
+ // churns the chosen redaction marker between candidates, never back to the raw
79
+ // matched value, so it cannot leak a secret.
80
+ const REGEX_REMATCH_BACKSCAN = 512;
81
+
82
+ interface RegexMatchContext {
83
+ /** Full text the match was found in (positions are offsets into it). */
84
+ text: string;
85
+ /** Start/end of the matched span being replaced. */
86
+ start: number;
87
+ end: number;
88
+ }
89
+
90
+ /**
91
+ * Whether `candidate`, substituted for the matched span in its surrounding text,
92
+ * is re-matched by `regex` at its own position. A replace-mode regex that depends
93
+ * on context (lookbehind/lookahead/`\b`) can match a candidate that does NOT match
94
+ * in isolation: e.g. `(?<=api=)[AZ]` never matches a bare `A`, but `api=A` does, so
95
+ * a candidate `A` chosen by an isolation test is re-redacted on the next obfuscate()
96
+ * pass and can oscillate back to the raw matched value. The probe substitutes the
97
+ * candidate into the FULL text — not a truncated window — so a wide lookbehind or
98
+ * lookahead (e.g. `(?<=A{600})`) still evaluates against the context that makes it
99
+ * match. Truncating that context dropped the assertion's reach and falsely
100
+ * accepted an oscillating, leaky candidate. The scan starts a bounded distance
101
+ * left of the span and stops once a match begins at/after the span's end (matches
102
+ * arrive in order), keeping per-candidate cost independent of total text length.
103
+ */
104
+ function regexRematchesInContext(candidate: string, regex: RegExp, ctx: RegexMatchContext): boolean {
105
+ const probe = ctx.text.slice(0, ctx.start) + candidate + ctx.text.slice(ctx.end);
106
+ const spanStart = ctx.start;
107
+ const spanEnd = spanStart + candidate.length;
108
+ regex.lastIndex = Math.max(0, spanStart - REGEX_REMATCH_BACKSCAN);
109
+ for (let m = regex.exec(probe); m !== null; m = regex.exec(probe)) {
110
+ const matchStart = m.index;
111
+ const matchEnd = m.index + m[0].length;
112
+ // Matches arrive in increasing position; once one starts at or past the
113
+ // span's end it cannot cover the candidate, and neither can any later one.
114
+ if (matchStart >= spanEnd) break;
115
+ // A match overlapping the candidate's own bytes means those bytes get
116
+ // re-redacted on a later pass — not a fixed point.
117
+ if (matchEnd > spanStart) return true;
118
+ // Zero-width matches do not advance lastIndex; step past to avoid a loop.
119
+ if (m[0].length === 0) regex.lastIndex++;
120
+ }
121
+ return false;
122
+ }
123
+
124
+ /**
125
+ * Search same-length replacements for one the regex does NOT match, so a default
126
+ * regex secret whose deterministic replacement collides with its own value (the
127
+ * `Z`/`ZZ` sentinel, or an astronomical hash collision) is still redacted to a
128
+ * STABLE nonmatching value instead of shipping the raw secret. A nonmatching
129
+ * candidate is a fixed point under re-obfuscation — the regex never re-matches it,
130
+ * so it cannot re-leak on a later pass. The search stays bounded to O(length *
131
+ * alphabet) regardless of value length: first exhaust every single-position
132
+ * substitution against a deterministic baseline (`AAAA…`, then `!AAA…`, `A!AA…`,
133
+ * …) so any regex that only needs one out-of-class byte — regardless of position —
134
+ * is found in a handful of probes rather than enumerating every combination (which
135
+ * for a 3-byte match-everything config, e.g. `[\s\S]{3}`, would otherwise run
136
+ * 90**3 = 729000 candidates through the regex on every single match, stalling
137
+ * provider requests). Candidates are enumerated deterministically over a stable
138
+ * ASCII alphabet: alphanumerics first (usually enough), then punctuation fallback
139
+ * bytes when the regex covers every alphanumeric candidate. When the regex still
140
+ * matches around a lone perturbed byte (for example `[A-Za-z0-9].*` matching the
141
+ * unperturbed tail), full-width same-byte candidates (`!!!!!`, `_____`, …) are
142
+ * tried next. When the regex covers every non-whitespace candidate (e.g. `\S{n}`),
143
+ * whitespace markers (a full space/tab run, then a single whitespace byte among
144
+ * non-whitespace filler) are tried as a last resort. A genuine match-everything
145
+ * regex (`.`/`[\s\S]`, which also matches space and tab) still exhausts this bounded
146
+ * sweep and returns undefined, letting the caller keep its own fixed-point fallback
147
+ * — bounded search can in principle miss an escape that depends jointly on
148
+ * multiple positions in a way no single-position swap reaches, but no realistic
149
+ * secret-redaction regex (character classes, literal matches, anchored/bounded
150
+ * repeats) has that shape.
151
+ */
152
+ function findNonMatchingReplacement(value: string, regex: RegExp, context: RegexMatchContext): string | undefined {
153
+ const len = value.length;
154
+ if (len === 0) return undefined;
155
+ // Exhaust every single-position substitution against the deterministic baseline
156
+ // first (covers the common case cheaply), then fall back to full-width same-byte
157
+ // candidates for a regex that only rejects a lone perturbed byte in context.
158
+ const baseline = NONMATCHING_REPLACEMENT_CHARS[0].repeat(len);
159
+ for (let position = 0; position < len; position++) {
160
+ for (const ch of NONMATCHING_REPLACEMENT_CHARS) {
161
+ const candidate = `${baseline.slice(0, position)}${ch}${baseline.slice(position + 1)}`;
162
+ if (candidate === value) continue;
163
+ if (!regexRematchesInContext(candidate, regex, context)) return candidate;
164
+ }
165
+ }
166
+ // If the regex can still match around a lone punctuation byte (for example
167
+ // `[A-Za-z0-9].*` matching the `AAAA` tail of `!AAAA`), try full-width
168
+ // same-byte fallbacks like `!!!!!`, `_____`, etc. before giving up.
169
+ for (const ch of NONMATCHING_REPLACEMENT_CHARS) {
170
+ const candidate = ch.repeat(len);
171
+ if (candidate === value) continue;
172
+ if (!regexRematchesInContext(candidate, regex, context)) return candidate;
173
+ }
174
+ return findWhitespaceFallbackReplacement(value, regex, context);
175
+ }
176
+
177
+ /**
178
+ * Last-resort fallback for a default replace regex that matches every
179
+ * non-whitespace candidate. Builds same-length whitespace markers the regex
180
+ * cannot match: first a full space/tab run (handles `\S`-class patterns), then a
181
+ * single whitespace byte among non-whitespace filler (` AAAA`, `A AAA`, …). The
182
+ * mixed marker defeats regexes that ALSO match all-space/all-tab runs, e.g.
183
+ * `(?:\S{n}| {n}|\t{n})`, because the lone whitespace byte breaks every
184
+ * fixed-length run. A genuine match-everything regex (`.`/`[\s\S]`) matches the
185
+ * filler and the whitespace alike, so this still returns undefined there, keeping
186
+ * the caller's sentinel as the sole fixed point.
187
+ */
188
+ function findWhitespaceFallbackReplacement(
189
+ value: string,
190
+ regex: RegExp,
191
+ context: RegexMatchContext,
192
+ ): string | undefined {
193
+ const len = value.length;
194
+ const filler = NONMATCHING_REPLACEMENT_CHARS[0];
195
+ for (const ws of WHITESPACE_REPLACEMENT_CHARS) {
196
+ const full = ws.repeat(len);
197
+ if (full !== value) {
198
+ if (!regexRematchesInContext(full, regex, context)) return full;
199
+ }
200
+ for (let pos = 0; pos < len; pos++) {
201
+ const candidate = `${filler.repeat(pos)}${ws}${filler.repeat(len - pos - 1)}`;
202
+ if (candidate === value) continue;
203
+ if (!regexRematchesInContext(candidate, regex, context)) return candidate;
204
+ }
205
+ }
206
+ return undefined;
207
+ }
208
+
209
+ /**
210
+ * Whether a default (no custom `replacement`) replace-mode regex can never
211
+ * safely redact a 1-2 char match: `findNonMatchingReplacement`'s bounded
212
+ * search — the same search `#generateRegexReplacement` runs at match time —
213
+ * finds no candidate the regex fails to re-match. This holds independent of
214
+ * any actual per-install key: the search already exhausts every character in
215
+ * `REPLACEMENT_CHARS` (the alphabet `buildKeyedReplacementRun` draws its
216
+ * fallback marker from) plus punctuation and whitespace, so if none of those
217
+ * escape the regex, no key-derived marker drawn from the same alphabet can
218
+ * either — the marker is guaranteed to re-match too, making every such match
219
+ * unresolvable: the fallback could only ever emit the raw matched text
220
+ * unchanged. Probed with a value (`"\0".repeat(length)`) the bounded search
221
+ * never treats as a real candidate, so the result depends only on the
222
+ * regex's own matching behavior, not on this specific probe.
223
+ */
224
+ export function regexHasUnresolvableShortMatchFallback(regex: RegExp): boolean {
225
+ return ([1, 2] as const).some(length => {
226
+ const probe = "\u0000".repeat(length);
227
+ const savedLastIndex = regex.lastIndex;
228
+ try {
229
+ return findNonMatchingReplacement(probe, regex, { text: probe, start: 0, end: length }) === undefined;
230
+ } finally {
231
+ regex.lastIndex = savedLastIndex;
232
+ }
233
+ });
234
+ }
235
+
236
+ // ═══════════════════════════════════════════════════════════════════════════
237
+ // Placeholder format
238
+ // ═══════════════════════════════════════════════════════════════════════════
239
+
240
+ const HASH_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
241
+ // Base length is sized for ~62 bits of entropy (64 bits of a keyed digest
242
+ // rendered as 12 base36 chars) so unrelated secrets do not collide on a shared
243
+ // base. A collision would let a persisted placeholder deobfuscate to the wrong
244
+ // secret when the configured secret set or its ordering changes across sessions.
245
+ const HASH_LEN = 12;
246
+ const MAX_FRIENDLY_NAME_LEN = 32;
247
+ // Plain/regex obfuscate matches shorter than this are toned down (never placed
248
+ // behind a reversible placeholder) to avoid redacting small words/fragments.
249
+ export const MIN_OBFUSCATE_SECRET_LEN = 8;
250
+
251
+ // Per-process fallback key used when a caller does not supply a persisted
252
+ // per-install key. It is random (never shipped in source), so model-visible
253
+ // placeholders cannot be reversed by dictionary-hashing candidate secrets; it
254
+ // only forgoes cross-session token stability, which the persisted key provides.
255
+ let ephemeralPlaceholderKey: string | undefined;
256
+ function defaultPlaceholderKey(): string {
257
+ ephemeralPlaceholderKey ??= crypto.randomBytes(32).toString("base64url");
258
+ return ephemeralPlaceholderKey;
259
+ }
260
+
261
+ type PlaceholderCaseHint = "U" | "L" | "C" | "M";
262
+
263
+ /** Normalize a friendly name into the model-visible placeholder prefix. */
264
+ export function sanitizeSecretFriendlyName(name: string): string | undefined {
265
+ const sanitized = name
266
+ .replace(/[^A-Za-z0-9]/g, "")
267
+ .toUpperCase()
268
+ .slice(0, MAX_FRIENDLY_NAME_LEN);
269
+ return sanitized.length > 0 ? sanitized : undefined;
270
+ }
271
+
272
+ /**
273
+ * Normalize a secret value into the same alnum-only, uppercased shape a
274
+ * friendly-name label or placeholder prefix is sanitized into, so comparing a
275
+ * raw (possibly lowercase/punctuated) secret value against already-sanitized,
276
+ * model-visible text does not miss a case- or separator-only variant. Unlike
277
+ * `sanitizeSecretFriendlyName` this never truncates and never signals "empty"
278
+ * via `undefined` — callers already guard on `.length > 0` before comparing.
279
+ */
280
+ function sanitizeForCollisionCheck(value: string): string {
281
+ return value.replace(/[^A-Za-z0-9]/g, "").toUpperCase();
282
+ }
283
+
284
+ // A label leaks a secret either by containing the whole normalized secret or,
285
+ // once it reaches the public display cap, by being the secret's visible prefix.
286
+ // Shorter names like "TOKEN" can still be intentional generic labels.
287
+ function sanitizedLabelCollidesWithSecret(sanitizedLabel: string, sanitizedSecret: string): boolean {
288
+ if (sanitizedSecret.length === 0) return false;
289
+ if (sanitizedLabel.includes(sanitizedSecret)) return true;
290
+ return sanitizedLabel.length >= MAX_FRIENDLY_NAME_LEN && sanitizedSecret.startsWith(sanitizedLabel);
291
+ }
292
+
293
+ /**
294
+ * Whether an entry needs the persisted placeholder key: either because it can
295
+ * produce a reversible (keyed) obfuscate-mode placeholder, or because a default
296
+ * (no custom `replacement`) replace-mode regex can reach
297
+ * `#generateRegexReplacement`'s key-derived idempotent fallback marker (see
298
+ * `#generateReplacement`) when every same-length candidate re-matches a
299
+ * pathological match-everything config (e.g. `[\s\S]{8}`). That fallback depends
300
+ * on the persisted per-install key — not just length — to stay a fixed point
301
+ * across a process restart; without a persisted key, a fresh install falls back
302
+ * to a process-random key (`defaultPlaceholderKey()`), so the fallback marker
303
+ * would churn across restarts even though the algorithm itself is stable. A
304
+ * regex WITH a custom `replacement` never reaches that fallback (it always emits
305
+ * the literal configured string), and a plain replace secret's replacement is
306
+ * pure content-hash (`#generateSecretReplacement`), so neither needs the key.
307
+ * Short plain obfuscate entries are toned down (never placeheld), so they must
308
+ * NOT force key creation: otherwise a `secret-placeholder.key` file is written
309
+ * and persisted for a config that ends up with no active secrets, leaving the
310
+ * key readable via a tool and reusable for later placeholders.
311
+ */
312
+ export function secretEntryNeedsPlaceholderKey(entry: SecretEntry): boolean {
313
+ if ((entry.mode ?? "obfuscate") === "obfuscate") {
314
+ if (entry.type === "regex") return true;
315
+ return entry.content.length >= MIN_OBFUSCATE_SECRET_LEN;
316
+ }
317
+ return entry.type === "regex" && entry.replacement === undefined;
318
+ }
319
+
320
+ /**
321
+ * Whether a plain replace-mode replacement string can contribute a fragment that
322
+ * helps the replace phase reconstruct an obfuscate `content`. During obfuscate()'s
323
+ * replace phase the output is a tiling of passthrough bytes (adversary-controlled
324
+ * provider text) and whole replacement outputs; any contiguous occurrence of
325
+ * `content` in that output is covered by interior replacement tiles (each a
326
+ * substring of `content`) bordered by passthrough at the ends, where the border
327
+ * tile may be a suffix of a replacement (forming `content`'s prefix) or a prefix
328
+ * of a replacement (forming `content`'s suffix). An EMPTY replacement deletes its
329
+ * trigger entirely, joining the passthrough on both sides; with adversary-chosen
330
+ * surrounding bytes that can form any non-empty `content` across the deleted gap.
331
+ * So a replacement can help iff it is empty, is a substring of `content`,
332
+ * contains `content`, or shares such a border overlap.
333
+ */
334
+ function replacementCanFormContent(replacement: string, content: string): boolean {
335
+ if (replacement.length === 0) return content.length > 0;
336
+ if (content.includes(replacement) || replacement.includes(content)) return true;
337
+ const maxOverlap = Math.min(replacement.length, content.length);
338
+ for (let k = 1; k <= maxOverlap; k++) {
339
+ // A suffix of the replacement forms the prefix of the content (left border),
340
+ // or a prefix of the replacement forms the suffix of the content (right border).
341
+ if (content.startsWith(replacement.slice(replacement.length - k)) || content.endsWith(replacement.slice(0, k))) {
342
+ return true;
343
+ }
344
+ }
345
+ return false;
346
+ }
347
+
348
+ /**
349
+ * Whether a SET of entries needs the persisted placeholder key. `obfuscate()`
350
+ * applies plain replace-mode mappings before the plain-obfuscate pass, so a plain
351
+ * obfuscate entry only emits a reversible (keyed) placeholder when its content can
352
+ * still appear AFTER the replace phase. When no obfuscate entry can ever produce a
353
+ * placeholder, the persisted key must NOT be required/created — otherwise an
354
+ * effectively replace-only secret set still writes `secret-placeholder.key` and
355
+ * fails startup when the agent config dir is unwritable.
356
+ *
357
+ * The decision models the replace phase as the obfuscator actually runs it:
358
+ * replace mappings are content-keyed (later duplicate wins) and applied in
359
+ * descending content-length order; for a fresh probe (no prior placeholders) that
360
+ * phase is plain sequential substring replacement. A plain obfuscate entry needs
361
+ * the key when its content survives that simulated phase (direct typing) OR when
362
+ * any effective replacement can form the content via tiling — a substring,
363
+ * wholesale superstring, or prefix/suffix border that joins with surrounding
364
+ * passthrough bytes (see `replacementCanFormContent`). This covers direct
365
+ * shadowing (`SECRET -> safe`), reintroduction, duplicate ordering, transitive
366
+ * chains, and context-joined fragments uniformly. Default (omitted) replacements
367
+ * are deterministic, length-preserving, and distinct, so a same-content shadow
368
+ * with no other interacting replacement stays key-free.
369
+ * Replacement outputs are themselves rewritten by every later (shorter-content)
370
+ * replacement before the plain-obfuscate pass sees them, so a fragment that a
371
+ * subsequent replacement erases (`AA -> SEC` then `S -> X` turns every `SEC` into
372
+ * `XEC`) no longer forces the key. Surrounding bytes stay modeled as arbitrary
373
+ * passthrough, so testing the surviving fragment only drops false positives and
374
+ * never under-approximates a real key need.
375
+ */
376
+ export function secretEntriesNeedPlaceholderKey(entries: SecretEntry[]): boolean {
377
+ const replaceMap = new Map<string, string>();
378
+ for (const entry of entries) {
379
+ if (entry.type !== "plain" || (entry.mode ?? "obfuscate") !== "replace") continue;
380
+ replaceMap.set(
381
+ entry.content,
382
+ entry.replacement ?? ensureDistinctReplacement(generateDeterministicReplacement(entry.content), entry.content),
383
+ );
384
+ }
385
+ const replacePhase = [...replaceMap].sort((a, b) => b[0].length - a[0].length);
386
+ // Apply the replace phase from `start` onward. The phase runs in descending
387
+ // content-length order, so a replacement output emitted at index i is rewritten
388
+ // only by the later (shorter-content) replacements at i+1…; `start` 0 models a
389
+ // value typed directly into the input.
390
+ const applyReplacePhaseFrom = (text: string, start: number): string => {
391
+ let result = text;
392
+ for (let i = start; i < replacePhase.length; i++) {
393
+ result = result.split(replacePhase[i][0]).join(replacePhase[i][1]);
394
+ }
395
+ return result;
396
+ };
397
+ return entries.some(entry => {
398
+ if (!secretEntryNeedsPlaceholderKey(entry)) return false;
399
+ // Regex obfuscate entries match dynamically; conservatively require the key.
400
+ if (entry.type !== "plain") return true;
401
+ const content = entry.content;
402
+ if (applyReplacePhaseFrom(content, 0).includes(content)) return true;
403
+ // Test each replacement output in the form it SURVIVES the rest of the phase,
404
+ // so a fragment a later replacement erases no longer forces the key. The
405
+ // content it tiles into must also survive those later replacements: if a
406
+ // shorter-content replacement rewrites the surrounding passthrough bytes
407
+ // (e.g. `AA -> SEC` forms `SEC`+`RET12`, then `R -> X` turns the freshly
408
+ // formed `SECRET12` into `SECXET12`), the content can never reach the
409
+ // obfuscate pass, so the key is not needed. Requiring content stability only
410
+ // drops such false positives — a formation that genuinely survives is still
411
+ // caught at the replacement index that produces it.
412
+ return replacePhase.some(
413
+ ([, replacement], i) =>
414
+ applyReplacePhaseFrom(content, i + 1) === content &&
415
+ replacementCanFormContent(applyReplacePhaseFrom(replacement, i + 1), content),
416
+ );
417
+ });
418
+ }
419
+
420
+ // Derive the model-visible base from a KEYED digest of the secret. xxHash is
421
+ // fast and unkeyed, so a fixed-seed content hash of a low-entropy secret could
422
+ // be dictionaried from the transcript; HMAC-SHA256 under a private per-install
423
+ // key cannot, since the attacker lacks the key.
424
+ function buildHashBase(key: string, value: string): string {
425
+ const digest = new Bun.CryptoHasher("sha256", key).update(value).digest();
426
+ let v = 0n;
427
+ for (let i = 0; i < 8; i++) v = (v << 8n) | BigInt(digest[i]);
428
+ const radix = BigInt(HASH_CHARS.length);
429
+ let tag = "";
430
+ for (let i = 0; i < HASH_LEN; i++) {
431
+ tag += HASH_CHARS[Number(v % radix)];
432
+ v /= radix;
433
+ }
434
+ return tag;
435
+ }
436
+
437
+ // Build a deterministic, key-derived run of REPLACEMENT_CHARS of the given
438
+ // length. Used to redact a per-chunk replace remainder to a marker that depends
439
+ // only on the per-install key and the remainder length, so a fresh obfuscator
440
+ // reproduces the identical marker (idempotent redaction across restarts) while
441
+ // the run stays unpredictable without the key (raw sentinel-shaped bytes cannot
442
+ // equal the marker, so they are still redacted rather than passed through).
443
+ function buildKeyedReplacementRun(key: string, length: number): string {
444
+ if (length <= 0) return "";
445
+ const radix = REPLACEMENT_CHARS.length;
446
+ let out = "";
447
+ for (let block = 0; out.length < length; block++) {
448
+ const digest = new Bun.CryptoHasher("sha256", key).update(`replace-chunk\0${length}\0${block}`).digest();
449
+ for (let i = 0; i < digest.length && out.length < length; i++) {
450
+ out += REPLACEMENT_CHARS[digest[i] % radix];
451
+ }
452
+ }
453
+ return out;
454
+ }
455
+
456
+ function inferCaseHint(secret: string): PlaceholderCaseHint | undefined {
457
+ let hasCased = false;
458
+ let hasUpper = false;
459
+ let hasLower = false;
460
+ let capitalized = true;
461
+ let seenFirstCased = false;
462
+
463
+ for (let i = 0; i < secret.length; i++) {
464
+ const code = secret.charCodeAt(i);
465
+ const isUpper = code >= 65 && code <= 90;
466
+ const isLower = code >= 97 && code <= 122;
467
+ if (!isUpper && !isLower) continue;
468
+
469
+ hasCased = true;
470
+ if (isUpper) {
471
+ hasUpper = true;
472
+ if (seenFirstCased) capitalized = false;
473
+ } else {
474
+ hasLower = true;
475
+ if (!seenFirstCased) capitalized = false;
476
+ }
477
+ seenFirstCased = true;
478
+ }
479
+
480
+ if (!hasCased) return undefined;
481
+ if (hasUpper && !hasLower) return "U";
482
+ if (hasLower && !hasUpper) return "L";
483
+ if (capitalized) return "C";
484
+ return "M";
485
+ }
486
+
487
+ function buildPlaceholder(hint: PlaceholderCaseHint | undefined, base: string, friendlyName?: string): string {
488
+ const prefix = friendlyName ? `${friendlyName}_` : "";
489
+ return hint ? `$$${prefix}${base}:${hint}$$` : `$$${prefix}${base}$$`;
490
+ }
491
+
492
+ /** Regex matching `$$HASH$$`, `$$HASH:U$$`, and `$$FRIENDLY_HASH(:hint)$$` placeholders. */
493
+ const PLACEHOLDER_RE = /\$\$(?:[A-Z0-9]+_)?[A-Z0-9]{4,}(?::[ULCM])?\$\$/g;
494
+
495
+ function resumePlaceholderScanAfterRejectedCandidate(match: RegExpExecArray): void {
496
+ // RegExp#exec does not find overlapping matches. Restart at the rejected
497
+ // candidate's closing delimiter, which can open an immediately adjacent placeholder.
498
+ PLACEHOLDER_RE.lastIndex = match.index + match[0].length - 2;
499
+ }
500
+
501
+ function placeholderWithoutFriendlyName(placeholder: string): string | undefined {
502
+ const match = /^\$\$[A-Z0-9]+_([A-Z0-9]{4,}(?::[ULCM])?)\$\$$/.exec(placeholder);
503
+ return match ? `$$${match[1]}$$` : undefined;
504
+ }
505
+
506
+ function lookupFriendlyPlaceholderAlias(
507
+ deobfuscateMap: ReadonlyMap<string, { secret: string; recursive: boolean }>,
508
+ placeholder: string,
509
+ ): { secret: string; recursive: boolean } | undefined {
510
+ const direct = deobfuscateMap.get(placeholder);
511
+ if (direct !== undefined) return direct;
512
+ const unprefixed = placeholderWithoutFriendlyName(placeholder);
513
+ return unprefixed !== undefined ? deobfuscateMap.get(unprefixed) : undefined;
514
+ }
515
+
516
+ const PENDING_PLACEHOLDER_SUFFIX_RE = /(?:\$\$(?:[A-Z0-9]+_)?[A-Z0-9]*(?::[ULCM]?)?|\$)$/;
517
+
518
+ // Withhold a trailing run that could be the start of a placeholder from streamed
519
+ // deltas, so a partial token is never emitted before deobfuscation can replace
520
+ // it. A lone trailing delimiter character is always buffered because it can open
521
+ // a placeholder; the final non-streamed flush re-emits it when no token follows.
522
+ export function stripPendingSecretPlaceholderSuffix(text: string): string {
523
+ const pendingPlaceholderStart = text.match(PENDING_PLACEHOLDER_SUFFIX_RE);
524
+ if (pendingPlaceholderStart?.index === undefined) return text;
525
+ return text.slice(0, pendingPlaceholderStart.index);
526
+ }
527
+
528
+ interface RegexScanSegment {
529
+ scanStart: number;
530
+ scanEnd: number;
531
+ textStart: number;
532
+ textEnd: number;
533
+ generatedPlaceholder: boolean;
534
+ recursive: boolean;
535
+ }
536
+
537
+ interface ReplaceRegexScan {
538
+ text: string;
539
+ segments: RegexScanSegment[];
540
+ }
541
+
542
+ // ═══════════════════════════════════════════════════════════════════════════
543
+ // SecretObfuscator
544
+ // ═══════════════════════════════════════════════════════════════════════════
545
+
546
+ export class SecretObfuscator {
547
+ /** Plain secrets: secret → index (known at construction) */
548
+ #plainMappings = new Map<string, number>();
549
+
550
+ /** Regex entries (patterns compiled at construction) */
551
+ #regexEntries: Array<{ regex: RegExp; mode: "obfuscate" | "replace"; replacement?: string; friendlyName?: string }> =
552
+ [];
553
+
554
+ /** All obfuscate-mode mappings: index → { secret, placeholder } */
555
+ #obfuscateMappings = new Map<number, { secret: string; placeholder: string }>();
556
+
557
+ /** Replace-mode plain mappings: secret → replacement */
558
+ #replaceMappings = new Map<string, string>();
559
+
560
+ /** Reverse lookup for LIVE deobfuscation (provider output, tool-call args):
561
+ * keyed placeholder → secret plus recursion policy. Only placeholders this
562
+ * obfuscator generated under the per-install key (and their friendly-name-
563
+ * independent aliases) live here, so a prompt-injected model cannot synthesize
564
+ * one without the key. */
565
+ #deobfuscateMap = new Map<string, { secret: string; recursive: boolean }>();
566
+
567
+ /** Exact placeholder tokens generated by this obfuscator revision (no aliases). */
568
+ #generatedPlaceholders = new Set<string>();
569
+
570
+ /** Deterministic replace chunks emitted by this obfuscator, used to keep re-obfuscation idempotent. */
571
+ #generatedReplaceChunks = new Set<string>();
572
+
573
+ /** Every configured plain-secret literal value (both obfuscate and replace
574
+ * mode), collected before any placeholder is minted. A generated placeholder
575
+ * must never equal one of these — otherwise a later-processed secret whose
576
+ * raw value happens to equal an earlier secret's placeholder would be
577
+ * indistinguishable from that placeholder on the NEXT obfuscate() pass (its
578
+ * own plain-secret redaction already ran, sorted by length, before the
579
+ * placeholder existed) and would survive verbatim in provider-visible text. */
580
+ #configuredSecretValues = new Set<string>();
581
+
582
+ /** Regex values seen in the current obfuscate input, used to keep friendly labels from exposing normalized matches that are discovered later in the same pass. */
583
+ #currentRegexSecretValues = new Set<string>();
584
+
585
+ /** Placeholder base-key (exact value for :M, case-folded otherwise) → base hash. */
586
+ #placeholderBaseByKey = new Map<string, string>();
587
+
588
+ /** Placeholder base hash → owner key, used to avoid ambiguous placeholders. */
589
+ #placeholderBaseOwners = new Map<string, string>();
590
+
591
+ /** Next available index for regex match discoveries */
592
+ #nextIndex: number;
593
+
594
+ /** Whether any secrets were configured */
595
+ #hasAny: boolean;
596
+
597
+ /**
598
+ * Private per-install (or per-process) key for the keyed placeholder digest.
599
+ * Resolved lazily when the constructor received a key PROVIDER: the first
600
+ * placeholder mint or keyed fallback marker triggers resolution, so callers
601
+ * can defer persisting `secret-placeholder.key` until a dynamic regex entry
602
+ * actually matches session content.
603
+ */
604
+ #key: string | undefined;
605
+ #keyProvider: (() => string) | undefined;
606
+
607
+ constructor(entries: SecretEntry[], key: string | (() => string) = defaultPlaceholderKey()) {
608
+ if (typeof key === "function") {
609
+ this.#keyProvider = key;
610
+ } else {
611
+ this.#setPlaceholderKey(key);
612
+ }
613
+ // Collect every configured plain-secret literal AND compile every regex
614
+ // entry BEFORE minting any placeholder below, so a placeholder's friendly
615
+ // name (checked against both in `#createPlaceholder`) can never embed a
616
+ // LATER entry's raw value or regex coverage, regardless of entries[]
617
+ // order — same reasoning as the base-collision guard below, extended to
618
+ // the friendly-name collision guard.
619
+ for (const entry of entries) {
620
+ if (entry.type === "plain") {
621
+ this.#configuredSecretValues.add(entry.content);
622
+ continue;
623
+ }
624
+ try {
625
+ const regex = compileSecretRegex(entry.content, entry.flags);
626
+ const mode = entry.mode ?? "obfuscate";
627
+ // A default (no custom `replacement`) replace-mode regex that can
628
+ // never redact a 1-2 char match distinctly from itself (see
629
+ // `regexHasUnresolvableShortMatchFallback`) is dropped rather than
630
+ // risk a real secret round-tripping unredacted; `secrets/index.ts`
631
+ // warns loudly for the `secrets.yml`-loaded path — this is the
632
+ // silent backstop for direct construction.
633
+ if (
634
+ mode === "replace" &&
635
+ entry.replacement === undefined &&
636
+ regexHasUnresolvableShortMatchFallback(regex)
637
+ ) {
638
+ continue;
639
+ }
640
+ this.#regexEntries.push({
641
+ regex,
642
+ mode,
643
+ replacement: entry.replacement,
644
+ friendlyName: entry.friendlyName,
645
+ });
646
+ } catch {
647
+ // Invalid regex — skip silently (validation happens at load time)
648
+ }
649
+ }
650
+ let index = 0;
651
+ let hasRealSec = this.#regexEntries.length > 0;
652
+ for (const entry of entries) {
653
+ if (entry.type !== "plain") continue;
654
+ const mode = entry.mode ?? "obfuscate";
655
+ if (mode === "obfuscate") {
656
+ if (entry.content.length < MIN_OBFUSCATE_SECRET_LEN) {
657
+ // Tone down short plain secret obfuscation to avoid false matches on small words like "esp".
658
+ continue;
659
+ }
660
+ const placeholder = this.#createPlaceholder(entry.content, entry.friendlyName);
661
+ this.#plainMappings.set(entry.content, index);
662
+ this.#obfuscateMappings.set(index, { secret: entry.content, placeholder });
663
+ this.#generatedPlaceholders.add(placeholder);
664
+ index++;
665
+ hasRealSec = true;
666
+ } else {
667
+ // replace mode
668
+ const replacement = entry.replacement ?? this.#generateSecretReplacement(entry.content);
669
+ this.#replaceMappings.set(entry.content, replacement);
670
+ hasRealSec = true;
671
+ }
672
+ }
673
+
674
+ this.#nextIndex = index;
675
+ this.#hasAny = hasRealSec;
676
+ }
677
+
678
+ /**
679
+ * The keyed-hash key makes obfuscate-mode placeholder bases un-dictionaryable,
680
+ * but it can be persisted in a user-readable file (`secret-placeholder.key`).
681
+ * A prompt-injected tool read (read/bash) could otherwise surface it to the
682
+ * provider verbatim and undo that protection, so redact the key itself from
683
+ * obfuscated (provider-visible) output as a one-way secret.
684
+ */
685
+ #setPlaceholderKey(key: string): void {
686
+ this.#key = key;
687
+ this.#replaceMappings.set(key, this.#generateSecretReplacement(key));
688
+ this.#configuredSecretValues.add(key);
689
+ }
690
+
691
+ /** Resolve the placeholder key once, minting its self-redaction on first use. */
692
+ #getKey(): string {
693
+ let key = this.#key;
694
+ if (key === undefined) {
695
+ key = this.#keyProvider?.() ?? defaultPlaceholderKey();
696
+ this.#keyProvider = undefined;
697
+ this.#setPlaceholderKey(key);
698
+ }
699
+ return key;
700
+ }
701
+
702
+ /** Whether this pass will mint a keyed placeholder from a regex match. */
703
+ #willMintRegexPlaceholder(secretValues: ReadonlySet<string>): boolean {
704
+ for (const entry of this.#regexEntries) {
705
+ if (entry.mode !== "obfuscate") continue;
706
+ for (const value of secretValues) {
707
+ entry.regex.lastIndex = 0;
708
+ const matches = entry.regex.test(value);
709
+ entry.regex.lastIndex = 0;
710
+ if (matches) return true;
711
+ }
712
+ }
713
+ return false;
714
+ }
715
+
716
+ hasSecrets(): boolean {
717
+ return this.#hasAny;
718
+ }
719
+
720
+ /** Obfuscate all secrets in text. Bidirectional placeholders for obfuscate mode, one-way for replace. */
721
+ obfuscate(text: string, sharedRegexSecretValues?: ReadonlySet<string>): string {
722
+ if (!this.#hasAny) return text;
723
+ this.#currentRegexSecretValues = this.collectRegexSecretValuesForObfuscation(text);
724
+ for (const secretValue of sharedRegexSecretValues ?? []) {
725
+ this.#currentRegexSecretValues.add(secretValue);
726
+ }
727
+ // Resolve a lazy key before the replace phase whenever this pass will mint
728
+ // a regex placeholder. The key registers itself as a replace-mode secret;
729
+ // resolving it later, while processing the regex match, would expose key
730
+ // bytes already present in this same provider-visible input.
731
+ if (this.#keyProvider !== undefined && this.#willMintRegexPlaceholder(this.#currentRegexSecretValues)) {
732
+ this.#getKey();
733
+ }
734
+ let result = text;
735
+ // `origin` runs parallel to `result` (one tag char per result char): "I" for
736
+ // bytes carried from the INPUT (placeholders from a PRIOR obfuscate() call)
737
+ // and "F" for bytes this call freshly inserted. The SDK obfuscates messages
738
+ // in both convertToLlm and transformProviderContext, and prior-turn messages
739
+ // re-enter every turn, so obfuscate() must be a fixed point: a regex must not
740
+ // re-redact around a placeholder that arrived in the input (which would
741
+ // corrupt a prior replacement marker, e.g. REDACTED -> REDACTEDDACTED).
742
+ // Tracking by RANGE (not token value) keeps a fresh placeholder that happens
743
+ // to equal a prior one (same secret seen raw again) eligible for cross-match.
744
+ let origin = "I".repeat(text.length);
745
+ // 1. Process replace-mode plain secrets
746
+ for (const [secret, replacement] of [...this.#replaceMappings].sort((a, b) => b[0].length - a[0].length)) {
747
+ ({ text: result, origin } = this.#replaceOutsidePlaceholdersTracked(result, origin, secret, replacement, "I"));
748
+ }
749
+ for (const secretValue of this.#collectRegexSecretValues(result)) {
750
+ this.#currentRegexSecretValues.add(secretValue);
751
+ }
752
+ for (const secretValue of this.#collectRegexSecretValuesAfterRegexReplacements(result, origin)) {
753
+ this.#currentRegexSecretValues.add(secretValue);
754
+ }
755
+ for (const secretValue of sharedRegexSecretValues ?? []) {
756
+ this.#currentRegexSecretValues.add(secretValue);
757
+ }
758
+ ({ text: result, origin } = this.#stripUnsafeFriendlyPrefixes(result, origin));
759
+
760
+ // 2. Process obfuscate-mode plain secrets
761
+ for (const [secret, index] of [...this.#plainMappings].sort((a, b) => b[0].length - a[0].length)) {
762
+ const mapping = this.#obfuscateMappings.get(index)!;
763
+ ({ text: result, origin } = this.#replaceOutsidePlaceholdersTracked(
764
+ result,
765
+ origin,
766
+ secret,
767
+ this.#placeholderForCurrentInput(mapping.placeholder),
768
+ "F",
769
+ ));
770
+ }
771
+
772
+ // 3. Process regex entries — discover new matches
773
+ for (const entry of this.#regexEntries) {
774
+ entry.regex.lastIndex = 0;
775
+ const matches = this.#collectRegexMatches(result, entry.regex, entry.mode, origin, entry.replacement);
776
+
777
+ for (const match of matches) {
778
+ if (entry.mode === "replace") {
779
+ if (match.preserveGeneratedPlaceholders) {
780
+ if (
781
+ match.preserveInputPlaceholders &&
782
+ entry.replacement === undefined &&
783
+ match.inputPlaceholderOutsideChunkCount === 1 &&
784
+ match.inputPlaceholderOutsideStart >= 0 &&
785
+ origin
786
+ .slice(
787
+ match.inputPlaceholderOutsideStart,
788
+ match.inputPlaceholderOutsideStart + match.inputPlaceholderOutside.length,
789
+ )
790
+ .includes("F") &&
791
+ this.#generatedReplaceChunks.has(match.inputPlaceholderOutside)
792
+ ) {
793
+ continue;
794
+ }
795
+ // Same greedy-spillover fixed point as the obfuscate branch below: when
796
+ // the match only reached across a prior-call placeholder because the
797
+ // placeholder's own value satisfies the regex, and the surrounding raw
798
+ // bytes do not independently match, re-redacting those bytes drifts the
799
+ // deterministic scramble across passes (e.g. `…#…#ZZJ5sotJ` →
800
+ // `…#…#ZZpvsotJ`), invalidating prompt-cache prefixes despite no new
801
+ // input. Leave the placeholder and its spillover verbatim — a fixed
802
+ // point. Structurally-required bytes (placeholder value alone cannot
803
+ // match) and independently-matching outside chunks still fall through to
804
+ // the redaction below. Only prior-call (`origin "I"`) placeholders set
805
+ // these flags, so first-pass redaction of genuinely new bytes is intact.
806
+ if (
807
+ match.inputPlaceholderInnerIndependentlyMatches &&
808
+ !match.inputPlaceholderOutsideIndependentlyMatches
809
+ ) {
810
+ continue;
811
+ }
812
+ let replaceEnd = match.end;
813
+ let span = result.slice(match.start, replaceEnd);
814
+ if (entry.replacement !== undefined) {
815
+ const trailingChunk = trailingOutsidePreservedPlaceholderChunk(span, placeholder =>
816
+ this.#isGeneratedPlaceholder(placeholder),
817
+ );
818
+ if (trailingChunk.length > 0 && entry.replacement.startsWith(trailingChunk)) {
819
+ const trailingSuffix = entry.replacement.slice(trailingChunk.length);
820
+ if (trailingSuffix.length > 0 && result.slice(replaceEnd).startsWith(trailingSuffix)) {
821
+ replaceEnd += trailingSuffix.length;
822
+ span = result.slice(match.start, replaceEnd);
823
+ }
824
+ }
825
+ }
826
+ // A custom replacement is a single redaction marker for the whole
827
+ // match, so emit it once around the preserved placeholder rather
828
+ // than per surrounding chunk (which duplicates it, e.g.
829
+ // `api_key=***#…#api_key=***`). Without one, each surrounding chunk
830
+ // gets its own length-matched fixed-point marker, checked in the
831
+ // expanded scan context so marker bytes cannot re-match beside the
832
+ // preserved placeholder on the next pass. The origin of any placeholder
833
+ // PRESERVED inside `span` must survive verbatim — blanket-tagging the
834
+ // whole redacted span "I" would relabel a same-call-fresh ("F")
835
+ // placeholder as prior-call, wrongly triggering the spillover-skip
836
+ // above for a LATER regex entry over content this call just redacted.
837
+ const spanOrigin = origin.slice(match.start, replaceEnd);
838
+ const redacted =
839
+ entry.replacement !== undefined
840
+ ? redactWithFixedReplacementOutsidePlaceholders(
841
+ span,
842
+ spanOrigin,
843
+ entry.replacement,
844
+ placeholder => this.#isGeneratedPlaceholder(placeholder),
845
+ )
846
+ : this.#redactRegexMatchOutsidePlaceholders(span, spanOrigin, entry.regex, match.scanContext);
847
+ result = replaceRange(result, match.start, replaceEnd, redacted.text);
848
+ origin = replaceRange(origin, match.start, replaceEnd, redacted.origin);
849
+ } else {
850
+ const replacement = entry.replacement ?? match.defaultReplacement;
851
+ if (replacement === undefined) {
852
+ throw new Error("regex replace match missing a generated replacement");
853
+ }
854
+ result = replaceRange(result, match.start, match.end, replacement);
855
+ origin = replaceRange(origin, match.start, match.end, "I".repeat(replacement.length));
856
+ }
857
+ } else {
858
+ if (match.scanMatchLength < MIN_OBFUSCATE_SECRET_LEN) {
859
+ // Tone down short regex matches to avoid obfuscating small
860
+ // words/fragments. Measure the regex's own match length in the
861
+ // canonical (placeholder-expanded) scan view, not the rewritten
862
+ // source span, so the threshold reflects how much content the regex
863
+ // actually matched.
864
+ continue;
865
+ }
866
+ if (match.preserveInputPlaceholders) {
867
+ // The match straddled a prior-call placeholder. When the placeholder's
868
+ // own value already satisfies the regex on its own AND the surrounding
869
+ // raw bytes do not independently match, those bytes are greedy spillover
870
+ // (e.g. the trailing `A` in `SECRETUV→#…#A`): obfuscating them mints
871
+ // fresh placeholders on re-obfuscation and drifts the provider-visible
872
+ // history and prompt-cache prefix. Leave the placeholder atomic and the
873
+ // spillover verbatim — a fixed point. Outside bytes are still obfuscated
874
+ // when the placeholder value alone cannot match (they are structurally
875
+ // part of the secret, e.g. an `api_key=` prefix the regex requires) or
876
+ // when they independently match the regex on their own.
877
+ if (
878
+ match.inputPlaceholderInnerIndependentlyMatches &&
879
+ !match.inputPlaceholderOutsideIndependentlyMatches
880
+ ) {
881
+ continue;
882
+ }
883
+ const span = result.slice(match.start, match.end);
884
+ const spanOrigin = origin.slice(match.start, match.end);
885
+ const obfuscated = this.#obfuscateOutsidePlaceholdersTracked(span, spanOrigin, entry.friendlyName);
886
+ result = replaceRange(result, match.start, match.end, obfuscated.text);
887
+ origin = replaceRange(origin, match.start, match.end, obfuscated.origin);
888
+ continue;
889
+ }
890
+ // obfuscate mode — get or create stable index
891
+ let index = this.#findObfuscateIndex(match.canonicalValue);
892
+ if (index === undefined) {
893
+ index = this.#nextIndex++;
894
+ const placeholder = this.#createPlaceholder(
895
+ match.canonicalValue,
896
+ entry.friendlyName,
897
+ match.recursive,
898
+ );
899
+ this.#obfuscateMappings.set(index, { secret: match.canonicalValue, placeholder });
900
+ this.#generatedPlaceholders.add(placeholder);
901
+ }
902
+ const mapping = this.#obfuscateMappings.get(index)!;
903
+ const placeholder = this.#placeholderForCurrentInput(mapping.placeholder);
904
+ result = replaceRange(result, match.start, match.end, placeholder);
905
+ origin = replaceRange(origin, match.start, match.end, "F".repeat(placeholder.length));
906
+ }
907
+ }
908
+ if (entry.mode === "replace") {
909
+ for (const secretValue of this.#collectRegexSecretValues(result)) {
910
+ this.#currentRegexSecretValues.add(secretValue);
911
+ }
912
+ }
913
+ }
914
+ ({ text: result, origin } = this.#stabilizeReplaceRegexPlaceholderSpillover(result, origin));
915
+
916
+ this.#currentRegexSecretValues = new Set();
917
+ return result;
918
+ }
919
+
920
+ /** Deobfuscate keyed placeholders for provider output, tool-call arguments, replay, and display. */
921
+ deobfuscate(text: string): string {
922
+ return this.#deobfuscate(text);
923
+ }
924
+
925
+ // Reverse-direction counterpart to `#isGeneratedPlaceholder`'s guard: the
926
+ // bare-alias fallback below intentionally accepts ANY prefix so a
927
+ // placeholder minted under a renamed friendly name still deobfuscates
928
+ // (see `#prefixIsSecretShaped`'s docstring for why), but unconditionally
929
+ // stripping and ignoring an attacker-authored prefix would let a forged
930
+ // token like `$$GITHUBPATABC123_<suffix-copied-from-any-real-placeholder>$$`
931
+ // restore to that OTHER secret's raw value with no check at all — worse
932
+ // than the obfuscate-direction leak, since deobfuscation is what feeds
933
+ // tool-call arguments and provider-output restoration. Refuse the
934
+ // fallback (leave the token as opaque, unresolved text) when the prefix
935
+ // is itself secret-shaped; an exact full-token match is unaffected, since
936
+ // that was minted by this instance and carries no forgery risk.
937
+ #lookupLiveAlias(placeholder: string): { secret: string; recursive: boolean } | undefined {
938
+ const direct = this.#deobfuscateMap.get(placeholder);
939
+ if (direct !== undefined) return direct;
940
+ const body = placeholder.slice(2, -2);
941
+ const match = /^([A-Z0-9]+)_([A-Z0-9]{4,}(?::[ULCM])?)$/.exec(body);
942
+ if (match === null || this.#prefixIsSecretShaped(match[1]!)) return undefined;
943
+ return this.#deobfuscateMap.get(`$$${match[2]}$$`);
944
+ }
945
+
946
+ #deobfuscate(text: string): string {
947
+ if (!this.#hasAny || !text.includes("$$")) return text;
948
+ let result = text;
949
+ for (;;) {
950
+ let shouldContinue = false;
951
+ const next = result.replace(PLACEHOLDER_RE, match => {
952
+ const mapped = this.#lookupLiveAlias(match);
953
+ if (mapped !== undefined) {
954
+ shouldContinue ||= mapped.recursive;
955
+ return mapped.secret;
956
+ }
957
+ return match;
958
+ });
959
+ if (next === result || !shouldContinue || !next.includes("$$")) return next;
960
+ result = next;
961
+ }
962
+ }
963
+
964
+ /** Deep-walk an object, deobfuscating string values for LIVE paths (keyed placeholders only). */
965
+ deobfuscateObject<T>(obj: T): T {
966
+ if (!this.#hasAny) return obj;
967
+ return deepWalkStrings(obj, s => this.deobfuscate(s));
968
+ }
969
+
970
+ /** Deep-walk an object, obfuscating all string values. */
971
+ obfuscateObject<T>(obj: T): T {
972
+ if (!this.#hasAny) return obj;
973
+ return deepWalkStrings(obj, s => this.obfuscate(s));
974
+ }
975
+
976
+ #generateReplacement(chunk: string): string {
977
+ // Redact a per-chunk remainder to a marker that is a fixed point under
978
+ // re-redaction on ANY obfuscator sharing the key, so persisted obfuscated text
979
+ // — and the provider prompt-cache prefixes it anchors — never drifts across a
980
+ // restart. The marker keeps the `Z`/`ZZ` sentinel prefix (already a fixed point
981
+ // for <=2-char remainders) and, for longer remainders, a key-derived run that
982
+ // depends ONLY on the key and length. A fresh obfuscator reproduces the
983
+ // identical marker (so re-redacting it stays idempotent across restarts, where
984
+ // the content-derived chunk plus the session-local `#generatedReplaceChunks`
985
+ // were not), yet the run is unpredictable without the per-install key, so raw
986
+ // remainder bytes that merely look sentinel-shaped (e.g. `ZZZZ`) cannot equal
987
+ // the marker and are still redacted instead of passed through.
988
+ const replacement =
989
+ chunk.length <= 2
990
+ ? "Z".repeat(chunk.length)
991
+ : `ZZ${buildKeyedReplacementRun(this.#getKey(), chunk.length - 2)}`;
992
+ this.#generatedReplaceChunks.add(replacement);
993
+ return replacement;
994
+ }
995
+
996
+ /**
997
+ * Replacement for a whole CONFIGURED secret value (a plain replace-mode entry
998
+ * or the redacted key). Unlike a per-chunk remainder redaction, the output
999
+ * must differ from the input so a value equal to the `Z`/`ZZ` sentinel is not
1000
+ * emitted verbatim. A plain secret only matches its own literal, so the
1001
+ * perturbed output stays a fixed point under re-obfuscation.
1002
+ */
1003
+ #generateSecretReplacement(secret: string): string {
1004
+ const replacement = ensureDistinctReplacement(generateDeterministicReplacement(secret), secret);
1005
+ this.#generatedReplaceChunks.add(replacement);
1006
+ return replacement;
1007
+ }
1008
+
1009
+ /**
1010
+ * Replacement for a default (no custom replacement) regex match. The output must
1011
+ * be a fixed point under re-obfuscation: a regex can re-match its own replacement,
1012
+ * and a later obfuscate() pass would then re-redact it — at best churning the
1013
+ * provider-visible text, at worst oscillating back to the raw matched value. The
1014
+ * deterministic replacement already differs from most values, but it is
1015
+ * all-alphanumeric and a regex may still match it, either directly (a `Z`/`ZZ`
1016
+ * sentinel collision, or a class like `[A-Za-z0-9]+`) or only in context
1017
+ * (lookbehind/lookahead/`\b`, e.g. `(?<=api=)[AZ]`). When it would re-match,
1018
+ * search same-length candidates IN CONTEXT for one the regex does not re-match:
1019
+ * that value is a stable fixed point and cannot re-leak. A single perturbation is
1020
+ * not enough — it may also match (e.g. `B`, not `A`, for `Z|A`) — so the search
1021
+ * tries further candidates before giving up. When no candidate avoids the regex (a
1022
+ * pathological match-everything config such as `.`/`[\s\S]`), the content-hash
1023
+ * deterministic value is NOT usable as a fallback: it will itself be re-matched on
1024
+ * the next pass, and because it is derived from the bytes being replaced, rehashing
1025
+ * those bytes (the marker itself, not the original secret) produces a DIFFERENT
1026
+ * value — churning the redaction, and the provider prompt-cache prefix it anchors,
1027
+ * across every re-obfuscation. Fall back instead to a marker that depends only
1028
+ * on `this.#key` and the value's length, not its content, so re-matching and
1029
+ * re-redacting it reproduces the IDENTICAL marker every time, which the
1030
+ * pathological case requires since no value can escape the regex at all. This
1031
+ * cannot reuse `#generateReplacement`'s own <=2-char branch directly: that
1032
+ * branch is the fixed `Z`/`ZZ` sentinel, which is itself a value this fallback
1033
+ * could be asked to replace (an input of exactly `Z` or `ZZ`), and returning it
1034
+ * unchanged would ship the raw secret to the provider — the exact failure plain
1035
+ * replace-mode secrets avoid via `ensureDistinctReplacement`. Neither
1036
+ * `ensureDistinctReplacement`'s single-char flip NOR a length-changing marker is
1037
+ * usable here: a pathological regex re-matches ANY same-length value, including
1038
+ * a flipped one, so a value-dependent flip oscillates between the two forever;
1039
+ * and a regex with no quantifier (matching one input character per match, e.g.
1040
+ * `.`) re-scans a LONGER marker as several independent same-regex matches on the
1041
+ * next pass, re-expanding each one — unbounded growth, not a fixed point. Use a
1042
+ * SAME-LENGTH keyed run instead of the sentinel for <=2 chars: content-independent
1043
+ * (so it is trivially its own fixed point once emitted) and no longer a public,
1044
+ * install-independent constant, closing the specific guessable collision
1045
+ * (`Z`/`ZZ`) the sentinel had. A same-length, content-independent marker cannot
1046
+ * mathematically rule out equaling some pathological input by construction (the
1047
+ * marker is itself a same-length string a match-everything regex also matches),
1048
+ * but that residual case now requires guessing this install's private key rather
1049
+ * than a universal constant — the same class of accepted risk
1050
+ * `generateDeterministicReplacement`'s hash collision already carries for longer
1051
+ * values.
1052
+ * A regex that is UNCONDITIONALLY pathological for a length <= 2 — matching
1053
+ * literally every candidate in isolation, independent of context, like `.`
1054
+ * or `[\s\S]` — is now rejected at construction/config-load time instead
1055
+ * (see `regexHasUnresolvableShortMatchFallback`), since for that narrow
1056
+ * case the residual risk above is fully avoidable rather than merely
1057
+ * unlikely. This branch, and the residual risk above, still applies to a
1058
+ * length > 2 pathological config and to a length <= 2 pattern that is only
1059
+ * pathological in a SPECIFIC match's surrounding context (the
1060
+ * construction-time check tests the regex in isolation, not every context
1061
+ * it could appear in).
1062
+ */
1063
+ #generateRegexReplacement(value: string, regex: RegExp, context: RegexMatchContext): string {
1064
+ let replacement = generateDeterministicReplacement(value);
1065
+ // Verify in context, not just against the sentinel collision: the
1066
+ // deterministic replacement is all-alphanumeric and a context-sensitive regex
1067
+ // (lookbehind/lookahead/`\b`) can re-match it even when it differs from the
1068
+ // value, so a later pass would re-redact and could oscillate back to the raw
1069
+ // secret. Search for a candidate the regex does not re-match in place.
1070
+ if (replacement === value || regexRematchesInContext(replacement, regex, context)) {
1071
+ const stable = findNonMatchingReplacement(value, regex, context);
1072
+ // See docstring above: same-length keyed run for <=2 chars (never the
1073
+ // `Z`/`ZZ` sentinel, which a <=2 char value could itself be), otherwise
1074
+ // the ordinary keyed-run fallback #generateReplacement already uses.
1075
+ replacement =
1076
+ stable ??
1077
+ (value.length <= 2
1078
+ ? buildKeyedReplacementRun(this.#getKey(), value.length)
1079
+ : this.#generateReplacement(value));
1080
+ regex.lastIndex = 0;
1081
+ }
1082
+ this.#generatedReplaceChunks.add(replacement);
1083
+ return replacement;
1084
+ }
1085
+
1086
+ #generateRegexChunkReplacement(chunk: string, regex: RegExp, context: RegexMatchContext): string {
1087
+ let replacement = this.#generateReplacement(chunk);
1088
+ if (regexRematchesInContext(replacement, regex, context)) {
1089
+ const stable = findNonMatchingReplacement(chunk, regex, context);
1090
+ if (stable !== undefined) {
1091
+ replacement = stable;
1092
+ this.#generatedReplaceChunks.add(replacement);
1093
+ }
1094
+ regex.lastIndex = 0;
1095
+ }
1096
+ return replacement;
1097
+ }
1098
+
1099
+ #redactRegexMatchOutsidePlaceholders(
1100
+ text: string,
1101
+ origin: string,
1102
+ regex: RegExp,
1103
+ context: RegexMatchContext,
1104
+ ): { text: string; origin: string } {
1105
+ let scanCursor = context.start;
1106
+ return transformOutsidePlaceholdersTracked(
1107
+ text,
1108
+ origin,
1109
+ placeholder => this.#isGeneratedPlaceholder(placeholder),
1110
+ chunk => {
1111
+ const start = scanCursor;
1112
+ scanCursor += chunk.length;
1113
+ if (chunk.length === 0) return "";
1114
+ return this.#generateRegexChunkReplacement(chunk, regex, {
1115
+ text: context.text,
1116
+ start,
1117
+ end: scanCursor,
1118
+ });
1119
+ },
1120
+ placeholder => {
1121
+ scanCursor +=
1122
+ lookupFriendlyPlaceholderAlias(this.#deobfuscateMap, placeholder)?.secret.length ?? placeholder.length;
1123
+ return placeholder;
1124
+ },
1125
+ );
1126
+ }
1127
+
1128
+ #stabilizeReplaceRegexPlaceholderSpillover(text: string, origin: string): { text: string; origin: string } {
1129
+ let result = text;
1130
+ let currentOrigin = origin;
1131
+ for (const entry of this.#regexEntries) {
1132
+ if (entry.mode !== "replace" || entry.replacement !== undefined) continue;
1133
+ entry.regex.lastIndex = 0;
1134
+ const matches = this.#collectRegexMatches(result, entry.regex, entry.mode, currentOrigin, entry.replacement);
1135
+ entry.regex.lastIndex = 0;
1136
+ for (const match of matches) {
1137
+ if (!match.preserveGeneratedPlaceholders) continue;
1138
+ if (
1139
+ match.preserveInputPlaceholders &&
1140
+ entry.replacement === undefined &&
1141
+ match.inputPlaceholderOutsideChunkCount === 1 &&
1142
+ match.inputPlaceholderOutsideStart >= 0 &&
1143
+ currentOrigin
1144
+ .slice(
1145
+ match.inputPlaceholderOutsideStart,
1146
+ match.inputPlaceholderOutsideStart + match.inputPlaceholderOutside.length,
1147
+ )
1148
+ .includes("F") &&
1149
+ this.#generatedReplaceChunks.has(match.inputPlaceholderOutside)
1150
+ ) {
1151
+ continue;
1152
+ }
1153
+ if (match.inputPlaceholderInnerIndependentlyMatches && !match.inputPlaceholderOutsideIndependentlyMatches) {
1154
+ continue;
1155
+ }
1156
+ const span = result.slice(match.start, match.end);
1157
+ const spanOrigin = currentOrigin.slice(match.start, match.end);
1158
+ const redacted =
1159
+ entry.replacement !== undefined
1160
+ ? redactWithFixedReplacementOutsidePlaceholders(span, spanOrigin, entry.replacement, placeholder =>
1161
+ this.#isGeneratedPlaceholder(placeholder),
1162
+ )
1163
+ : this.#redactRegexMatchOutsidePlaceholders(span, spanOrigin, entry.regex, match.scanContext);
1164
+ if (redacted.text === span) continue;
1165
+ result = replaceRange(result, match.start, match.end, redacted.text);
1166
+ currentOrigin = replaceRange(currentOrigin, match.start, match.end, redacted.origin);
1167
+ }
1168
+ }
1169
+ return { text: result, origin: currentOrigin };
1170
+ }
1171
+
1172
+ /** Find the obfuscate index for a known secret value. */
1173
+ #findObfuscateIndex(secret: string): number | undefined {
1174
+ // Check plain mappings first
1175
+ const plainIndex = this.#plainMappings.get(secret);
1176
+ if (plainIndex !== undefined) return plainIndex;
1177
+
1178
+ // Check regex-discovered mappings
1179
+ for (const [index, mapping] of this.#obfuscateMappings) {
1180
+ if (mapping.secret === secret) return index;
1181
+ }
1182
+ return undefined;
1183
+ }
1184
+
1185
+ #createPlaceholder(secret: string, friendlyName?: string, recursive: boolean = false): string {
1186
+ const hint = inferCaseHint(secret);
1187
+ // Key the base on the EXACT secret value, never a case-folded form. The
1188
+ // case hint is only a model-visible label. If two distinct secrets that
1189
+ // differ solely by ASCII case shared one case-folded base, a provider that
1190
+ // saw one placeholder could swap the hint to synthesize the sibling
1191
+ // secret's keyed token, and live deobfuscation (provider output / tool-call
1192
+ // args) would restore a value that was never provider-visible. Exact-value
1193
+ // keying gives every secret an independent base, so a sibling token cannot
1194
+ // be derived without the per-install key.
1195
+ const baseKey = secret;
1196
+ // A friendly name that embeds a configured secret's literal (or matches a
1197
+ // configured regex pattern) would bake that secret straight into a
1198
+ // LEGITIMATE, exact-registered placeholder — later scans recognize the
1199
+ // whole token as already-generated on an EXACT match, before the
1200
+ // alias-fallback prefix check above ever runs, so the embedded secret
1201
+ // would never be scanned. Drop the label for this mint rather than risk
1202
+ // it; the secret still gets a bare (unprefixed) placeholder. The collision
1203
+ // check runs against the FULL normalized label — `sanitizeForCollisionCheck`,
1204
+ // not yet capped at `MAX_FRIENDLY_NAME_LEN` — so a secret longer than the
1205
+ // 32-char display cap (or one whose sanitized form exceeds it) still gets
1206
+ // caught: a truncated `requestedFriendlyName` can never contain a longer
1207
+ // secret's full sanitized form, so checking the truncated label would let
1208
+ // the secret's first 32 (post-cap) characters leak as an accepted prefix.
1209
+ const requestedFriendlyName = friendlyName ? sanitizeSecretFriendlyName(friendlyName) : undefined;
1210
+ const sanitizedFriendlyName =
1211
+ requestedFriendlyName !== undefined &&
1212
+ friendlyName !== undefined &&
1213
+ !this.#friendlyNameCollidesWithSecret(sanitizeForCollisionCheck(friendlyName), friendlyName, secret)
1214
+ ? requestedFriendlyName
1215
+ : undefined;
1216
+ const preferredBase = this.#resolvePreferredPlaceholderBase(baseKey);
1217
+ const preferredPlaceholder = buildPlaceholder(hint, preferredBase, sanitizedFriendlyName);
1218
+ if (!this.#placeholderConflicts(preferredPlaceholder, secret)) {
1219
+ this.#registerDeobfuscationAlias(preferredPlaceholder, secret, recursive);
1220
+ return preferredPlaceholder;
1221
+ }
1222
+
1223
+ for (let attempt = 1; ; attempt++) {
1224
+ const fallbackBase = this.#reserveFallbackPlaceholderBase(baseKey, attempt);
1225
+ const placeholder = buildPlaceholder(hint, fallbackBase, sanitizedFriendlyName);
1226
+ if (!this.#placeholderConflicts(placeholder, secret)) {
1227
+ this.#registerDeobfuscationAlias(placeholder, secret, recursive);
1228
+ return placeholder;
1229
+ }
1230
+ }
1231
+ }
1232
+
1233
+ #resolvePreferredPlaceholderBase(baseKey: string): string {
1234
+ const existing = this.#placeholderBaseByKey.get(baseKey);
1235
+ if (existing !== undefined) return existing;
1236
+
1237
+ for (let attempt = 0; ; attempt++) {
1238
+ const base =
1239
+ attempt === 0
1240
+ ? buildHashBase(this.#getKey(), baseKey)
1241
+ : buildHashBase(this.#getKey(), `${baseKey}\0${attempt}`);
1242
+ const owner = this.#placeholderBaseOwners.get(base);
1243
+ if (owner !== undefined && owner !== baseKey) continue;
1244
+ this.#placeholderBaseOwners.set(base, baseKey);
1245
+ this.#placeholderBaseByKey.set(baseKey, base);
1246
+ return base;
1247
+ }
1248
+ }
1249
+
1250
+ #reserveFallbackPlaceholderBase(baseKey: string, startAttempt: number): string {
1251
+ for (let attempt = startAttempt; ; attempt++) {
1252
+ const owner = `${baseKey}\0collision\0${attempt}`;
1253
+ const base = buildHashBase(this.#getKey(), `${baseKey}\0collision\0${attempt}`);
1254
+ if (this.#placeholderBaseOwners.has(base)) continue;
1255
+ this.#placeholderBaseOwners.set(base, owner);
1256
+ return base;
1257
+ }
1258
+ }
1259
+
1260
+ #placeholderCollides(placeholder: string, secret: string): boolean {
1261
+ const existing = this.#deobfuscateMap.get(placeholder);
1262
+ return existing !== undefined && existing.secret !== secret;
1263
+ }
1264
+
1265
+ // A friendly placeholder is only safe if BOTH its full token and its
1266
+ // friendly-name-independent alias are free (or already ours), AND the token
1267
+ // itself is not another configured secret's literal value. Without the
1268
+ // latter check, a placeholder minted for secret A that happens to equal
1269
+ // secret B's raw content passes silently: B's own plain-secret redaction
1270
+ // pass (sorted by length, run once per obfuscate() call) already completed
1271
+ // before A's placeholder existed in the text, so B is never redacted out of
1272
+ // it — A's placeholder becomes a verbatim, provider-visible copy of B.
1273
+ #placeholderConflicts(placeholder: string, secret: string): boolean {
1274
+ if (this.#placeholderCollides(placeholder, secret)) return true;
1275
+ if (this.#configuredSecretValues.has(placeholder) && placeholder !== secret) return true;
1276
+ const unprefixed = placeholderWithoutFriendlyName(placeholder);
1277
+ if (unprefixed === undefined) return false;
1278
+ if (this.#placeholderCollides(unprefixed, secret)) return true;
1279
+ return this.#configuredSecretValues.has(unprefixed) && unprefixed !== secret;
1280
+ }
1281
+
1282
+ // A sanitized friendly name must not double as a live secret: it becomes a
1283
+ // verbatim, model-visible prefix on every placeholder minted for THIS
1284
+ // secret, baked in via an exact `#deobfuscateMap` entry rather than the
1285
+ // alias fallback — so it needs its own check independent of the scan-skip
1286
+ // alias guard in `#isGeneratedPlaceholder`. Reuses `#prefixIsSecretShaped`
1287
+ // for the sanitized-vs-sanitized comparisons (configured plain secrets,
1288
+ // every regex-discovered secret this instance has ever minted a
1289
+ // placeholder for) and the sanitized-label-vs-regex-pattern check, then
1290
+ // adds two label-specific checks `#prefixIsSecretShaped` cannot make: the
1291
+ // CURRENT secret being minted right now — which, on its first-ever mint,
1292
+ // is not yet in `#prefixIsSecretShaped`'s previously-discovered set, since
1293
+ // that set is only populated AFTER this call returns — normalized the
1294
+ // same way (catches, e.g., `friendlyName: "TOKABC123"` for a regex secret
1295
+ // whose literal match is `tok_abc123`, which the sanitized-name-vs-pattern
1296
+ // check misses when the pattern is case-sensitive/punctuated); and the RAW
1297
+ // (pre-sanitization) label against every configured regex pattern (catches
1298
+ // `friendlyName: "tok_abc123"` literally, which the SANITIZED label
1299
+ // `"TOKABC123"` could never match against that same case-sensitive
1300
+ // pattern). Any of these means the text is meant to be redacted, not
1301
+ // stamped unredacted onto every use of this secret.
1302
+ #collectRegexSecretValues(text: string): Set<string> {
1303
+ const values = new Set<string>();
1304
+ for (const entry of this.#regexEntries) {
1305
+ entry.regex.lastIndex = 0;
1306
+ for (;;) {
1307
+ const match = entry.regex.exec(text);
1308
+ if (match === null) break;
1309
+ if (match[0].length === 0) {
1310
+ entry.regex.lastIndex++;
1311
+ continue;
1312
+ }
1313
+ values.add(match[0]);
1314
+ }
1315
+ entry.regex.lastIndex = 0;
1316
+ }
1317
+ return values;
1318
+ }
1319
+
1320
+ collectRegexSecretValuesForObfuscation(text: string): Set<string> {
1321
+ const values = this.#collectRegexSecretValues(text);
1322
+ let result = text;
1323
+ let origin = "I".repeat(text.length);
1324
+ for (const [secret, replacement] of [...this.#replaceMappings].sort((a, b) => b[0].length - a[0].length)) {
1325
+ ({ text: result, origin } = this.#replaceOutsidePlaceholdersTracked(result, origin, secret, replacement, "I"));
1326
+ }
1327
+ for (const secretValue of this.#collectRegexSecretValues(result)) {
1328
+ values.add(secretValue);
1329
+ }
1330
+ for (const secretValue of this.#collectRegexSecretValuesAfterRegexReplacements(result, origin)) {
1331
+ values.add(secretValue);
1332
+ }
1333
+ return values;
1334
+ }
1335
+
1336
+ #collectRegexSecretValuesAfterRegexReplacements(text: string, origin: string): Set<string> {
1337
+ const values = new Set<string>();
1338
+ let simulated = text;
1339
+ let simulatedOrigin = origin;
1340
+ for (const entry of this.#regexEntries) {
1341
+ if (entry.mode !== "replace") continue;
1342
+ entry.regex.lastIndex = 0;
1343
+ const matches = this.#collectRegexMatches(
1344
+ simulated,
1345
+ entry.regex,
1346
+ entry.mode,
1347
+ simulatedOrigin,
1348
+ entry.replacement,
1349
+ );
1350
+ entry.regex.lastIndex = 0;
1351
+ if (matches.length === 0) continue;
1352
+ for (const match of [...matches].sort((a, b) => b.start - a.start)) {
1353
+ const replacement = entry.replacement ?? match.defaultReplacement;
1354
+ if (replacement === undefined) continue;
1355
+ for (const secretValue of this.#collectRegexSecretValues(replacement)) {
1356
+ values.add(secretValue);
1357
+ }
1358
+ simulated = replaceRange(simulated, match.start, match.end, replacement);
1359
+ simulatedOrigin = replaceRange(simulatedOrigin, match.start, match.end, "I".repeat(replacement.length));
1360
+ }
1361
+ for (const secretValue of this.#collectRegexSecretValues(simulated)) {
1362
+ values.add(secretValue);
1363
+ }
1364
+ }
1365
+ return values;
1366
+ }
1367
+
1368
+ #friendlyNameCollidesWithSecret(sanitizedName: string, rawName: string, secret: string): boolean {
1369
+ if (this.#prefixIsSecretShaped(sanitizedName)) return true;
1370
+ const sanitizedSecretValue = sanitizeForCollisionCheck(secret);
1371
+ if (sanitizedLabelCollidesWithSecret(sanitizedName, sanitizedSecretValue)) return true;
1372
+ for (const entry of this.#regexEntries) {
1373
+ entry.regex.lastIndex = 0;
1374
+ const matches = entry.regex.test(rawName);
1375
+ entry.regex.lastIndex = 0;
1376
+ if (matches) return true;
1377
+ }
1378
+ return false;
1379
+ }
1380
+
1381
+ #placeholderForCurrentInput(placeholder: string): string {
1382
+ const unprefixed = placeholderWithoutFriendlyName(placeholder);
1383
+ if (unprefixed === undefined) return placeholder;
1384
+ const match = /^([A-Z0-9]+)_/.exec(placeholder.slice(2, -2));
1385
+ if (match === null || !this.#prefixIsSecretShaped(match[1]!)) return placeholder;
1386
+ return unprefixed;
1387
+ }
1388
+
1389
+ #stripUnsafeFriendlyPrefixes(text: string, origin: string): { text: string; origin: string } {
1390
+ PLACEHOLDER_RE.lastIndex = 0;
1391
+ let result = "";
1392
+ let resultOrigin = "";
1393
+ let cursor = 0;
1394
+ for (;;) {
1395
+ const match = PLACEHOLDER_RE.exec(text);
1396
+ if (match === null) break;
1397
+ const placeholder = match[0];
1398
+ const unprefixed = placeholderWithoutFriendlyName(placeholder);
1399
+ const replacement = unprefixed !== undefined ? this.#placeholderForCurrentInput(placeholder) : placeholder;
1400
+ if (replacement === placeholder) {
1401
+ resumePlaceholderScanAfterRejectedCandidate(match);
1402
+ continue;
1403
+ }
1404
+ result += text.slice(cursor, match.index);
1405
+ resultOrigin += origin.slice(cursor, match.index);
1406
+ result += replacement;
1407
+ resultOrigin += origin[match.index]?.repeat(replacement.length) ?? "";
1408
+ cursor = match.index + placeholder.length;
1409
+ }
1410
+ result += text.slice(cursor);
1411
+ resultOrigin += origin.slice(cursor);
1412
+ return { text: result, origin: resultOrigin };
1413
+ }
1414
+
1415
+ stripUnsafeFriendlyPlaceholderPrefixes(text: string, sharedRegexSecretValues: ReadonlySet<string>): string {
1416
+ const previousRegexSecretValues = this.#currentRegexSecretValues;
1417
+ this.#currentRegexSecretValues = new Set(sharedRegexSecretValues);
1418
+ try {
1419
+ return this.#stripUnsafeFriendlyPrefixes(text, "I".repeat(text.length)).text;
1420
+ } finally {
1421
+ this.#currentRegexSecretValues = previousRegexSecretValues;
1422
+ }
1423
+ }
1424
+
1425
+ #registerDeobfuscationAlias(placeholder: string, secret: string, recursive: boolean): void {
1426
+ const existing = this.#deobfuscateMap.get(placeholder);
1427
+ if (existing === undefined || existing.secret === secret) {
1428
+ this.#deobfuscateMap.set(placeholder, { secret, recursive });
1429
+ }
1430
+ const unprefixed = placeholderWithoutFriendlyName(placeholder);
1431
+ if (unprefixed !== undefined) {
1432
+ const existingUnprefixed = this.#deobfuscateMap.get(unprefixed);
1433
+ if (existingUnprefixed === undefined || existingUnprefixed.secret === secret) {
1434
+ this.#deobfuscateMap.set(unprefixed, { secret, recursive });
1435
+ }
1436
+ }
1437
+ }
1438
+
1439
+ // Whether an alnum-only, uppercase friendly-name-shaped prefix dropped from
1440
+ // a candidate placeholder token is itself something that should have been
1441
+ // redacted, rather than an arbitrary label: a sanitized form of a
1442
+ // configured plain secret's value, a sanitized form of any regex-
1443
+ // discovered secret's value this instance has ever minted a placeholder
1444
+ // for, or text a configured regex pattern matches directly. Shared by the
1445
+ // obfuscate-direction guard below and the deobfuscate-direction bare-alias
1446
+ // guard in `#deobfuscate` — both fall back to a friendly-name-independent
1447
+ // alias keyed only by the hash suffix, so both need the same defense
1448
+ // against a forged/attacker-chosen prefix.
1449
+ #prefixIsSecretShaped(prefix: string): boolean {
1450
+ for (const secretValue of this.#configuredSecretValues) {
1451
+ const sanitizedSecret = sanitizeForCollisionCheck(secretValue);
1452
+ if (sanitizedLabelCollidesWithSecret(prefix, sanitizedSecret)) return true;
1453
+ }
1454
+ for (const secretValue of this.#currentRegexSecretValues) {
1455
+ const sanitizedSecret = sanitizeForCollisionCheck(secretValue);
1456
+ if (sanitizedLabelCollidesWithSecret(prefix, sanitizedSecret)) return true;
1457
+ }
1458
+ for (const { secret } of this.#obfuscateMappings.values()) {
1459
+ const sanitizedSecret = sanitizeForCollisionCheck(secret);
1460
+ if (sanitizedLabelCollidesWithSecret(prefix, sanitizedSecret)) return true;
1461
+ }
1462
+ for (const entry of this.#regexEntries) {
1463
+ entry.regex.lastIndex = 0;
1464
+ const matches = entry.regex.test(prefix);
1465
+ entry.regex.lastIndex = 0;
1466
+ if (matches) return true;
1467
+ }
1468
+ return false;
1469
+ }
1470
+
1471
+ // A placeholder is an exact match, or the friendly-name-independent bare
1472
+ // alias: needed so a placeholder minted under a NOW-renamed friendly name
1473
+ // (same secret, same key, different `secrets.yml` label) still round-trips
1474
+ // when older provider-visible text is re-scanned by a renamed-config
1475
+ // instance — the hash suffix is a keyed digest of the secret VALUE alone,
1476
+ // so a same-key instance recomputes it identically regardless of the
1477
+ // label. The dropped prefix is otherwise unconstrained text, though: an
1478
+ // attacker who has observed ANY live placeholder's hash suffix elsewhere
1479
+ // in the transcript could wrap it around a DIFFERENT real secret's
1480
+ // plaintext (or a normalized rendering of one) to make the whole token
1481
+ // look pre-redacted and smuggle that secret through untouched.
1482
+ // `#prefixIsSecretShaped` above guards against that, shared with the
1483
+ // deobfuscate-direction check in `#deobfuscate`.
1484
+ #isGeneratedPlaceholder(placeholder: string): boolean {
1485
+ if (this.#deobfuscateMap.has(placeholder)) return true;
1486
+ const match = /^([A-Z0-9]+)_([A-Z0-9]{4,}(?::[ULCM])?)$/.exec(placeholder.slice(2, -2));
1487
+ if (match === null) return false;
1488
+ if (this.#prefixIsSecretShaped(match[1]!)) return false;
1489
+ return this.#deobfuscateMap.has(`$$${match[2]}$$`);
1490
+ }
1491
+
1492
+ // Replace `search` with `replacement` outside known generated placeholders while
1493
+ // maintaining a parallel `origin` tag string (one char per result char): kept
1494
+ // bytes keep their tag, inserted `replacement` bytes get `tag`, and preserved
1495
+ // placeholder spans keep their original tags. Lets later passes tell prior-call
1496
+ // placeholders ("I") from ones freshly inserted this call ("F") by RANGE.
1497
+ #replaceOutsidePlaceholdersTracked(
1498
+ text: string,
1499
+ origin: string,
1500
+ search: string,
1501
+ replacement: string,
1502
+ tag: string,
1503
+ ): { text: string; origin: string } {
1504
+ if (search.length === 0) return { text, origin };
1505
+ PLACEHOLDER_RE.lastIndex = 0;
1506
+ let outText = "";
1507
+ let outOrigin = "";
1508
+ let pending = 0;
1509
+ const emitChunk = (from: number, to: number): void => {
1510
+ let last = from;
1511
+ let idx = text.indexOf(search, from);
1512
+ while (idx !== -1 && idx + search.length <= to) {
1513
+ outText += text.slice(last, idx) + replacement;
1514
+ outOrigin += origin.slice(last, idx) + tag.repeat(replacement.length);
1515
+ last = idx + search.length;
1516
+ idx = text.indexOf(search, last);
1517
+ }
1518
+ outText += text.slice(last, to);
1519
+ outOrigin += origin.slice(last, to);
1520
+ };
1521
+ for (;;) {
1522
+ const match = PLACEHOLDER_RE.exec(text);
1523
+ if (match === null) break;
1524
+ if (!(this.#isGeneratedPlaceholder(match[0]) && match[0] !== search)) {
1525
+ resumePlaceholderScanAfterRejectedCandidate(match);
1526
+ continue;
1527
+ }
1528
+ emitChunk(pending, match.index);
1529
+ outText += match[0];
1530
+ outOrigin += origin.slice(match.index, match.index + match[0].length);
1531
+ pending = match.index + match[0].length;
1532
+ }
1533
+ emitChunk(pending, text.length);
1534
+ return { text: outText, origin: outOrigin };
1535
+ }
1536
+
1537
+ #placeholderForRegexChunk(secret: string, friendlyName: string | undefined): string {
1538
+ let index = this.#findObfuscateIndex(secret);
1539
+ if (index === undefined) {
1540
+ index = this.#nextIndex++;
1541
+ const placeholder = this.#createPlaceholder(secret, friendlyName);
1542
+ this.#obfuscateMappings.set(index, { secret, placeholder });
1543
+ this.#generatedPlaceholders.add(placeholder);
1544
+ }
1545
+ return this.#placeholderForCurrentInput(this.#obfuscateMappings.get(index)!.placeholder);
1546
+ }
1547
+
1548
+ #obfuscateOutsidePlaceholdersTracked(
1549
+ text: string,
1550
+ origin: string,
1551
+ friendlyName: string | undefined,
1552
+ ): { text: string; origin: string } {
1553
+ PLACEHOLDER_RE.lastIndex = 0;
1554
+ let outText = "";
1555
+ let outOrigin = "";
1556
+ let pending = 0;
1557
+ const emitChunk = (from: number, to: number): void => {
1558
+ if (from >= to) return;
1559
+ const placeholder = this.#placeholderForRegexChunk(text.slice(from, to), friendlyName);
1560
+ outText += placeholder;
1561
+ outOrigin += "F".repeat(placeholder.length);
1562
+ };
1563
+ for (;;) {
1564
+ const match = PLACEHOLDER_RE.exec(text);
1565
+ if (match === null) break;
1566
+ if (!this.#isGeneratedPlaceholder(match[0])) {
1567
+ resumePlaceholderScanAfterRejectedCandidate(match);
1568
+ continue;
1569
+ }
1570
+ emitChunk(pending, match.index);
1571
+ outText += match[0];
1572
+ outOrigin += origin.slice(match.index, match.index + match[0].length);
1573
+ pending = match.index + match[0].length;
1574
+ }
1575
+ emitChunk(pending, text.length);
1576
+ return { text: outText, origin: outOrigin };
1577
+ }
1578
+
1579
+ #knownPlaceholderRanges(text: string): Array<{ start: number; end: number }> {
1580
+ PLACEHOLDER_RE.lastIndex = 0;
1581
+ const ranges: Array<{ start: number; end: number }> = [];
1582
+ for (;;) {
1583
+ const match = PLACEHOLDER_RE.exec(text);
1584
+ if (match === null) break;
1585
+ if (this.#isGeneratedPlaceholder(match[0])) {
1586
+ ranges.push({ start: match.index, end: match.index + match[0].length });
1587
+ } else {
1588
+ resumePlaceholderScanAfterRejectedCandidate(match);
1589
+ }
1590
+ }
1591
+ return ranges;
1592
+ }
1593
+
1594
+ #collectRegexMatches(
1595
+ text: string,
1596
+ regex: RegExp,
1597
+ mode: "obfuscate" | "replace",
1598
+ origin: string,
1599
+ replacement: string | undefined,
1600
+ ): Array<{
1601
+ start: number;
1602
+ end: number;
1603
+ value: string;
1604
+ canonicalValue: string;
1605
+ scanMatchLength: number;
1606
+ recursive: boolean;
1607
+ preserveGeneratedPlaceholders: boolean;
1608
+ preserveInputPlaceholders: boolean;
1609
+ inputPlaceholderOutside: string;
1610
+ inputPlaceholderOutsideIndependentlyMatches: boolean;
1611
+ inputPlaceholderOutsideStart: number;
1612
+ inputPlaceholderOutsideChunkCount: number;
1613
+ inputPlaceholderInnerIndependentlyMatches: boolean;
1614
+ defaultReplacement: string | undefined;
1615
+ scanContext: RegexMatchContext;
1616
+ }> {
1617
+ const knownPlaceholderRanges = this.#knownPlaceholderRanges(text);
1618
+ const regexScan = buildReplaceRegexScan(text, knownPlaceholderRanges, this.#deobfuscateMap);
1619
+ const scanText = regexScan.text;
1620
+ regex.lastIndex = 0;
1621
+ const matches: Array<{
1622
+ start: number;
1623
+ end: number;
1624
+ value: string;
1625
+ canonicalValue: string;
1626
+ scanMatchLength: number;
1627
+ recursive: boolean;
1628
+ preserveGeneratedPlaceholders: boolean;
1629
+ preserveInputPlaceholders: boolean;
1630
+ inputPlaceholderOutside: string;
1631
+ inputPlaceholderOutsideIndependentlyMatches: boolean;
1632
+ inputPlaceholderOutsideStart: number;
1633
+ inputPlaceholderOutsideChunkCount: number;
1634
+ inputPlaceholderInnerIndependentlyMatches: boolean;
1635
+ defaultReplacement: string | undefined;
1636
+ scanContext: RegexMatchContext;
1637
+ }> = [];
1638
+ for (;;) {
1639
+ const match = regex.exec(scanText);
1640
+ if (match === null) break;
1641
+ if (match[0].length === 0) {
1642
+ regex.lastIndex++;
1643
+ continue;
1644
+ }
1645
+ let start = match.index;
1646
+ let end = match.index + match[0].length;
1647
+ let scanMatchLength = match[0].length;
1648
+ let scanMatchValue = match[0];
1649
+ let canonicalValue = "";
1650
+ let recursive = false;
1651
+ let preserveGeneratedPlaceholders = false;
1652
+ let preserveInputPlaceholders = false;
1653
+ let inputPlaceholderOutside = "";
1654
+ let inputPlaceholderOutsideIndependentlyMatches = false;
1655
+ let inputPlaceholderOutsideStart = -1;
1656
+ let inputPlaceholderOutsideChunkCount = 0;
1657
+ let inputPlaceholderInnerIndependentlyMatches = false;
1658
+
1659
+ let mapped = mapReplaceRegexMatch(regexScan.segments, start, end);
1660
+ if (mapped.partialPlaceholderCut) {
1661
+ // The match straddles a generated placeholder (its boundary falls inside
1662
+ // the secret's expanded value). Rewriting across the token drops bytes
1663
+ // (obfuscate) or drifts the redaction across re-obfuscation passes
1664
+ // (replace), so the cut secret must stay as its existing placeholder.
1665
+ // But wholly-outside bytes on either side of the placeholder are still
1666
+ // provider-visible content covered by the regex. Probe the prefix against
1667
+ // the full expanded scan text so right-hand context supplied by the
1668
+ // placeholder still satisfies lookahead/alternatives, then clamp the
1669
+ // accepted match to the prefix boundary. If no prefix is available, redact
1670
+ // the outside suffix that was covered by the full-context match. Every
1671
+ // resume point computed below is chained through
1672
+ // `extendPastAdjacentPlaceholders`: a resume position that lands exactly on
1673
+ // the START of ANOTHER placeholder must skip that one too before a fresh
1674
+ // `regex.exec` attempt runs there, so a run of adjacent placeholders resolves
1675
+ // identically whether its LEADING member is raw text this call is about to
1676
+ // placeholder or is already a placeholder from a prior call/pass (see that
1677
+ // helper's doc for the cross-call drift this prevents).
1678
+ const cutResumeIndex = mapped.cutResumeIndex;
1679
+ const prefixScanEnd = mapped.firstPlaceholderScanStart;
1680
+ let handledOutside = false;
1681
+ if (prefixScanEnd > match.index) {
1682
+ regex.lastIndex = match.index;
1683
+ const prefixMatch = regex.exec(scanText);
1684
+ if (prefixMatch !== null && prefixMatch[0].length > 0 && prefixMatch.index < prefixScanEnd) {
1685
+ const prefixStart = prefixMatch.index;
1686
+ const prefixEnd = Math.min(prefixMatch.index + prefixMatch[0].length, prefixScanEnd);
1687
+ const prefixMapped = mapReplaceRegexMatch(regexScan.segments, prefixStart, prefixEnd);
1688
+ if (!prefixMapped.partialPlaceholderCut && prefixEnd > prefixStart) {
1689
+ start = prefixStart;
1690
+ end = prefixEnd;
1691
+ scanMatchValue = scanText.slice(prefixStart, prefixEnd);
1692
+ // Keep the full match length in the expanded scan view (not the
1693
+ // clamped prefix length) — the short-match guard below measures the
1694
+ // regex's own match length, so a full-size match with a short
1695
+ // outside-prefix remainder must not be undercounted as too short,
1696
+ // matching the suffix-clamp branch below.
1697
+ scanMatchLength = match[0].length;
1698
+ mapped = prefixMapped;
1699
+ regex.lastIndex = extendPastAdjacentPlaceholders(regexScan.segments, prefixEnd);
1700
+ handledOutside = true;
1701
+ }
1702
+ }
1703
+ }
1704
+ if (!handledOutside && cutResumeIndex < end) {
1705
+ const suffixStart = cutResumeIndex;
1706
+ const suffixEnd = end;
1707
+ const suffixMapped = mapReplaceRegexMatch(regexScan.segments, suffixStart, suffixEnd);
1708
+ if (!suffixMapped.partialPlaceholderCut) {
1709
+ start = suffixStart;
1710
+ end = suffixEnd;
1711
+ scanMatchValue = scanText.slice(suffixStart, suffixEnd);
1712
+ scanMatchLength = match[0].length;
1713
+ mapped = suffixMapped;
1714
+ regex.lastIndex = extendPastAdjacentPlaceholders(regexScan.segments, suffixEnd);
1715
+ handledOutside = true;
1716
+ }
1717
+ }
1718
+ if (!handledOutside) {
1719
+ regex.lastIndex = extendPastAdjacentPlaceholders(regexScan.segments, cutResumeIndex);
1720
+ continue;
1721
+ }
1722
+ }
1723
+ // Scan-space coordinates of the match (placeholders expanded). The default
1724
+ // redaction's fixed-point check must run against this expanded view — the
1725
+ // view re-obfuscation actually scans — not the literal `#…#` text, or a
1726
+ // redaction adjacent to a placeholder (e.g. an outside prefix before a cut
1727
+ // secret) could drift when the placeholder expands and connects to it.
1728
+ const scanMatchStart = start;
1729
+ const scanMatchEnd = end;
1730
+ let defaultReplacement: string | undefined;
1731
+ start = mapped.start;
1732
+ end = mapped.end;
1733
+ preserveGeneratedPlaceholders = mapped.preserveGeneratedPlaceholders;
1734
+ // A match overlapping a placeholder that arrived in the INPUT (origin tag
1735
+ // "I" — generated by a PRIOR obfuscate() call) must preserve that
1736
+ // placeholder atomically so repeated obfuscation stays a fixed point. If
1737
+ // raw bytes surround it, they still need redaction; branch-specific
1738
+ // replacement below keeps prior placeholders while covering the
1739
+ // non-placeholder chunks.
1740
+ const overlapsInputPlaceholder = knownPlaceholderRanges.some(
1741
+ range => start < range.end && end > range.start && origin[range.start] === "I",
1742
+ );
1743
+ preserveInputPlaceholders = overlapsInputPlaceholder;
1744
+ if (overlapsInputPlaceholder) {
1745
+ const firstOutside = firstOutsidePlaceholderRange(start, end, knownPlaceholderRanges);
1746
+ if (
1747
+ mode === "replace" &&
1748
+ replacement !== undefined &&
1749
+ firstOutside !== undefined &&
1750
+ text.slice(firstOutside.start, firstOutside.start + replacement.length) === replacement
1751
+ ) {
1752
+ const expandedEnd = firstOutside.start + replacement.length;
1753
+ if (expandedEnd > end) {
1754
+ regex.lastIndex = Math.max(regex.lastIndex, match.index + match[0].length + expandedEnd - end);
1755
+ end = expandedEnd;
1756
+ }
1757
+ }
1758
+ inputPlaceholderOutside = textOutsidePlaceholderRanges(text, start, end, knownPlaceholderRanges);
1759
+ inputPlaceholderOutsideStart =
1760
+ firstOutsidePlaceholderRange(start, end, knownPlaceholderRanges)?.start ?? -1;
1761
+ inputPlaceholderOutsideChunkCount = countOutsidePlaceholderRanges(start, end, knownPlaceholderRanges);
1762
+ if (inputPlaceholderOutside.length === 0) continue;
1763
+ const resumeIndex = regex.lastIndex;
1764
+ // Test each outside chunk in isolation rather than the concatenation of
1765
+ // all of them: concatenating chunks that sit on either side of the
1766
+ // placeholder erases the `#…#` token boundary between them (e.g. a
1767
+ // prefix "ABCDEFGH" that independently matches `\b[A-Z]{8}\b` next to a
1768
+ // placeholder, followed by a raw "I" suffix, concatenates to "ABCDEFGHI"
1769
+ // — which does NOT match, since the boundary the placeholder provided is
1770
+ // gone). That false negative would leave a genuinely independent,
1771
+ // secret-shaped chunk unredacted. Each chunk is tested in BOTH its real
1772
+ // literal-token context (where the token's own non-word boundary can
1773
+ // complete a boundary-sensitive pattern) and the EXPANDED scan context —
1774
+ // the same view re-obfuscation's own regex scan runs against — so a
1775
+ // lookbehind/lookahead that only resolves once the neighboring
1776
+ // placeholder is expanded (e.g. `(?<=ABCDEFGH)SECRET` beside a
1777
+ // placeholder for `ABCDEFGH`) also counts as an independent match.
1778
+ inputPlaceholderOutsideIndependentlyMatches = outsidePlaceholderRangesAnyIndependentlyMatch(
1779
+ text,
1780
+ scanText,
1781
+ regexScan.segments,
1782
+ start,
1783
+ end,
1784
+ knownPlaceholderRanges,
1785
+ regex,
1786
+ );
1787
+ // Whether the placeholder's own (deobfuscated) value satisfies the regex
1788
+ // with the surrounding raw bytes dropped. When it does, those raw bytes
1789
+ // are greedy spillover the match never needed (e.g. the trailing `A` in
1790
+ // `SECRETUV→#…#A`); obfuscating them on re-obfuscation drifts the
1791
+ // provider-visible history and prompt-cache prefix. When it does NOT
1792
+ // (e.g. `api_key=` literal that the placeholder value alone cannot match),
1793
+ // the outside bytes are structurally required and must be obfuscated.
1794
+ const innerText = placeholderInnerText(text, start, end, knownPlaceholderRanges, this.#deobfuscateMap);
1795
+ regex.lastIndex = 0;
1796
+ inputPlaceholderInnerIndependentlyMatches = innerText.length > 0 && regex.test(innerText);
1797
+ regex.lastIndex = resumeIndex;
1798
+ }
1799
+ if (mode === "replace") {
1800
+ canonicalValue = scanMatchValue;
1801
+ recursive = mapped.recursive;
1802
+ } else {
1803
+ const overlappingRanges = knownPlaceholderRanges.filter(range => start < range.end && end > range.start);
1804
+ const containedByPlaceholder = overlappingRanges.some(range => start >= range.start && end <= range.end);
1805
+ if (containedByPlaceholder) {
1806
+ continue;
1807
+ }
1808
+ const canonical = deobfuscateGeneratedPlaceholderRanges(
1809
+ text,
1810
+ start,
1811
+ end,
1812
+ knownPlaceholderRanges,
1813
+ this.#deobfuscateMap,
1814
+ );
1815
+ canonicalValue = canonical.text;
1816
+ recursive = canonical.recursive;
1817
+ }
1818
+
1819
+ const scanContext = {
1820
+ text: scanText,
1821
+ start: scanMatchStart,
1822
+ end: scanMatchEnd,
1823
+ };
1824
+ if (mode === "replace" && replacement === undefined && !preserveGeneratedPlaceholders) {
1825
+ const savedLastIndex = regex.lastIndex;
1826
+ defaultReplacement = this.#generateRegexReplacement(scanMatchValue, regex, scanContext);
1827
+ regex.lastIndex = savedLastIndex;
1828
+ }
1829
+ matches.push({
1830
+ start,
1831
+ end,
1832
+ value: text.slice(start, end),
1833
+ defaultReplacement,
1834
+ canonicalValue,
1835
+ scanMatchLength,
1836
+ recursive,
1837
+ preserveGeneratedPlaceholders,
1838
+ preserveInputPlaceholders,
1839
+ inputPlaceholderOutside,
1840
+ inputPlaceholderOutsideIndependentlyMatches,
1841
+ inputPlaceholderOutsideStart,
1842
+ inputPlaceholderOutsideChunkCount,
1843
+ inputPlaceholderInnerIndependentlyMatches,
1844
+ scanContext,
1845
+ });
1846
+ }
1847
+ return matches.reverse();
1848
+ }
1849
+ }
1850
+
1851
+ // ═══════════════════════════════════════════════════════════════════════════
1852
+ // Display restore (inbound, persisted/provider → local display)
1853
+ // ═══════════════════════════════════════════════════════════════════════════
1854
+
1855
+ /**
1856
+ * Restore secret placeholders for local display. Only message kinds the model
1857
+ * itself authored from obfuscated context carry placeholders — assistant
1858
+ * content and the LLM-written branch/compaction summaries. User, developer, and
1859
+ * tool-result messages are persisted with their literal text, so operator-authored
1860
+ * placeholder-shaped text must survive untouched; those roles are never walked.
1861
+ */
1862
+ export function deobfuscateSessionContext(
1863
+ sessionContext: SessionContext,
1864
+ obfuscator: SecretObfuscator | undefined,
1865
+ ): SessionContext {
1866
+ if (!obfuscator?.hasSecrets()) return sessionContext;
1867
+ const messages = deobfuscateAgentMessages(obfuscator, sessionContext.messages);
1868
+ return messages === sessionContext.messages ? sessionContext : { ...sessionContext, messages };
1869
+ }
1870
+
1871
+ export function deobfuscateAgentMessages(obfuscator: SecretObfuscator, messages: AgentMessage[]): AgentMessage[] {
1872
+ const deob = (text: string): string => obfuscator.deobfuscate(text);
1873
+ let changed = false;
1874
+ const result = messages.map((message): AgentMessage => {
1875
+ switch (message.role) {
1876
+ case "assistant": {
1877
+ const content = deobfuscateAssistantContent(obfuscator, message.content);
1878
+ if (content === message.content) return message;
1879
+ changed = true;
1880
+ return { ...message, content };
1881
+ }
1882
+ case "branchSummary": {
1883
+ const summary = deob(message.summary);
1884
+ if (summary === message.summary) return message;
1885
+ changed = true;
1886
+ return { ...message, summary };
1887
+ }
1888
+ case "compactionSummary": {
1889
+ const summary = deob(message.summary);
1890
+ const shortSummary = message.shortSummary === undefined ? undefined : deob(message.shortSummary);
1891
+ const blocks = message.blocks === undefined ? undefined : deobfuscateTextBlocks(obfuscator, message.blocks);
1892
+ if (summary === message.summary && shortSummary === message.shortSummary && blocks === message.blocks) {
1893
+ return message;
1894
+ }
1895
+ changed = true;
1896
+ return { ...message, summary, shortSummary, blocks };
1897
+ }
1898
+ default:
1899
+ return message;
1900
+ }
1901
+ });
1902
+ return changed ? result : messages;
1903
+ }
1904
+
1905
+ /**
1906
+ * Restore placeholders in assistant content: visible text and tool-call
1907
+ * arguments/intent/rawBlock. Thinking and signatures are opaque
1908
+ * provider-replay/hidden-reasoning data and pass through byte-identical.
1909
+ */
1910
+ export function deobfuscateAssistantContent(
1911
+ obfuscator: SecretObfuscator,
1912
+ content: AssistantMessage["content"],
1913
+ ): AssistantMessage["content"] {
1914
+ if (!obfuscator.hasSecrets()) return content;
1915
+ const deob = (text: string): string => obfuscator.deobfuscate(text);
1916
+ let changed = false;
1917
+ const result = content.map((block): AssistantMessage["content"][number] => {
1918
+ if (block.type === "text") {
1919
+ const text = deob(block.text);
1920
+ if (text === block.text) return block;
1921
+ changed = true;
1922
+ return { ...block, text };
1923
+ }
1924
+
1925
+ if (block.type === "toolCall") {
1926
+ const args = deobfuscateToolArguments(obfuscator, block.arguments);
1927
+ const intent = block.intent === undefined ? undefined : deob(block.intent);
1928
+ const rawBlock = block.rawBlock === undefined ? undefined : deob(block.rawBlock);
1929
+ if (args === block.arguments && intent === block.intent && rawBlock === block.rawBlock) return block;
1930
+ changed = true;
1931
+ return { ...block, arguments: args, intent, rawBlock };
1932
+ }
1933
+ return block;
1934
+ });
1935
+ return changed ? result : content;
1936
+ }
1937
+
1938
+ /**
1939
+ * Restore placeholders inside a tool call's arguments. Arguments are arbitrary
1940
+ * model-authored JSON, so tool-call arguments are the ONLY place a recursive
1941
+ * JSON walk runs.
1942
+ */
1943
+ export function deobfuscateToolArguments(
1944
+ obfuscator: SecretObfuscator,
1945
+ args: Record<string, unknown>,
1946
+ ): Record<string, unknown> {
1947
+ if (!obfuscator.hasSecrets()) return args;
1948
+ return mapJsonStrings(args as JsonValue, s => obfuscator.deobfuscate(s)) as Record<string, unknown>;
1949
+ }
1950
+
1951
+ /** Redact secrets inside a tool call's arguments (same JSON-walk exception as {@link deobfuscateToolArguments}). */
1952
+ export function obfuscateToolArguments(
1953
+ obfuscator: SecretObfuscator,
1954
+ args: Record<string, unknown>,
1955
+ sharedRegexSecretValues?: ReadonlySet<string>,
1956
+ ): Record<string, unknown> {
1957
+ if (!obfuscator.hasSecrets()) return args;
1958
+ const regexSecretValues = sharedRegexSecretValues ?? collectJsonRegexSecretValues(obfuscator, args as JsonValue);
1959
+ return mapJsonStrings(args as JsonValue, s => obfuscator.obfuscate(s, regexSecretValues)) as Record<string, unknown>;
1960
+ }
1961
+
1962
+ // ═══════════════════════════════════════════════════════════════════════════
1963
+ // Outbound obfuscation (local → provider)
1964
+ // ═══════════════════════════════════════════════════════════════════════════
1965
+
1966
+ type UserFacingMessage = Extract<Message, { role: "user" | "developer" | "toolResult" }>;
1967
+
1968
+ /** Obfuscate `text` blocks of a content array; image and other blocks pass through. */
1969
+ function obfuscateTextBlocks(
1970
+ obfuscator: SecretObfuscator,
1971
+ content: (TextContent | ImageContent)[],
1972
+ sharedRegexSecretValues?: ReadonlySet<string>,
1973
+ ): (TextContent | ImageContent)[] {
1974
+ let changed = false;
1975
+ const result = content.map((block): TextContent | ImageContent => {
1976
+ if (block.type !== "text") return block;
1977
+ const text = obfuscator.obfuscate(block.text, sharedRegexSecretValues);
1978
+ if (text === block.text) return block;
1979
+ changed = true;
1980
+ return { ...block, text };
1981
+ });
1982
+ return changed ? result : content;
1983
+ }
1984
+
1985
+ /** Restore placeholders in `text` blocks of a content array; image and other blocks pass through. */
1986
+ function deobfuscateTextBlocks(
1987
+ obfuscator: SecretObfuscator,
1988
+ content: (TextContent | ImageContent)[],
1989
+ ): (TextContent | ImageContent)[] {
1990
+ let changed = false;
1991
+ const result = content.map((block): TextContent | ImageContent => {
1992
+ if (block.type !== "text") return block;
1993
+ const text = obfuscator.deobfuscate(block.text);
1994
+ if (text === block.text) return block;
1995
+ changed = true;
1996
+ return { ...block, text };
1997
+ });
1998
+ return changed ? result : content;
1999
+ }
2000
+
2001
+ /**
2002
+ * Re-obfuscate assistant content before it returns to a provider after session
2003
+ * restoration, removing friendly prefixes made unsafe by this batch. A changed
2004
+ * thinking block loses its byte-bound replay signature.
2005
+ */
2006
+ function obfuscateAssistantContentForReplay(
2007
+ obfuscator: SecretObfuscator,
2008
+ content: AssistantMessage["content"],
2009
+ sharedRegexSecretValues: ReadonlySet<string>,
2010
+ ): AssistantMessage["content"] {
2011
+ const obfuscate = (text: string): string =>
2012
+ obfuscator.stripUnsafeFriendlyPlaceholderPrefixes(
2013
+ obfuscator.obfuscate(text, sharedRegexSecretValues),
2014
+ sharedRegexSecretValues,
2015
+ );
2016
+ let changed = false;
2017
+ const result = content.map((block): AssistantMessage["content"][number] => {
2018
+ if (block.type === "text") {
2019
+ const text = obfuscate(block.text);
2020
+ if (text === block.text) return block;
2021
+ changed = true;
2022
+ return { ...block, text };
2023
+ }
2024
+ if (block.type === "thinking") {
2025
+ const thinking = obfuscate(block.thinking);
2026
+ if (thinking === block.thinking) return block;
2027
+ changed = true;
2028
+ return { ...block, thinking, thinkingSignature: undefined };
2029
+ }
2030
+ if (block.type === "toolCall") {
2031
+ const args = mapJsonStrings(block.arguments as JsonValue, obfuscate) as Record<string, unknown>;
2032
+ const intent = block.intent === undefined ? undefined : obfuscate(block.intent);
2033
+ const rawBlock = block.rawBlock === undefined ? undefined : obfuscate(block.rawBlock);
2034
+ if (args === block.arguments && intent === block.intent && rawBlock === block.rawBlock) return block;
2035
+ changed = true;
2036
+ return { ...block, arguments: args, intent, rawBlock };
2037
+ }
2038
+ return block;
2039
+ });
2040
+ return changed ? result : content;
2041
+ }
2042
+
2043
+ function collectMessageRegexSecretValues(obfuscator: SecretObfuscator, messages: Message[]): Set<string> {
2044
+ const values = new Set<string>();
2045
+ const addText = (text: string | undefined): void => {
2046
+ if (text === undefined) return;
2047
+ for (const value of obfuscator.collectRegexSecretValuesForObfuscation(text)) {
2048
+ values.add(value);
2049
+ }
2050
+ };
2051
+ for (const message of messages) {
2052
+ if (message.role === "assistant") {
2053
+ for (const block of message.content) {
2054
+ if (block.type === "text") addText(block.text);
2055
+ else if (block.type === "thinking") addText(block.thinking);
2056
+ else if (block.type === "toolCall") {
2057
+ for (const value of collectJsonRegexSecretValues(obfuscator, block.arguments as JsonValue)) {
2058
+ values.add(value);
2059
+ }
2060
+ addText(block.intent);
2061
+ addText(block.rawBlock);
2062
+ }
2063
+ }
2064
+ continue;
2065
+ }
2066
+ if (
2067
+ message.role !== "user" &&
2068
+ message.role !== "toolResult" &&
2069
+ !(message.role === "developer" && message.attribution === "user")
2070
+ ) {
2071
+ continue;
2072
+ }
2073
+ const target = message as UserFacingMessage;
2074
+ if (typeof target.content === "string") {
2075
+ addText(target.content);
2076
+ continue;
2077
+ }
2078
+ for (const block of target.content) {
2079
+ if (block.type === "text") addText(block.text);
2080
+ }
2081
+ }
2082
+ return values;
2083
+ }
2084
+
2085
+ /**
2086
+ * Redact secrets from outbound messages. User messages, tool results, and
2087
+ * user-authored developer messages (e.g. `@file` mentions) are obfuscated.
2088
+ * Assistant replay content is re-obfuscated too, because session restoration
2089
+ * expands keyed placeholders locally before the next provider request. Inline
2090
+ * image bytes are never walked.
2091
+ */
2092
+ export function obfuscateMessages(obfuscator: SecretObfuscator, messages: Message[]): Message[] {
2093
+ if (!obfuscator.hasSecrets()) return messages;
2094
+ const sharedRegexSecretValues = collectMessageRegexSecretValues(obfuscator, messages);
2095
+ let changed = false;
2096
+ const result = messages.map((message): Message => {
2097
+ if (
2098
+ message.role !== "user" &&
2099
+ message.role !== "toolResult" &&
2100
+ !(message.role === "developer" && message.attribution === "user")
2101
+ ) {
2102
+ if (message.role !== "assistant") return message;
2103
+ const content = obfuscateAssistantContentForReplay(obfuscator, message.content, sharedRegexSecretValues);
2104
+ if (content === message.content) return message;
2105
+ changed = true;
2106
+ return { ...message, content };
2107
+ }
2108
+ const target = message as UserFacingMessage;
2109
+ if (typeof target.content === "string") {
2110
+ const content = obfuscator.obfuscate(target.content, sharedRegexSecretValues);
2111
+ if (content === target.content) return message;
2112
+ changed = true;
2113
+ return { ...target, content } as Message;
2114
+ }
2115
+ const content = obfuscateTextBlocks(obfuscator, target.content, sharedRegexSecretValues);
2116
+ if (content === target.content) return message;
2117
+ changed = true;
2118
+ return { ...target, content } as Message;
2119
+ });
2120
+ return changed ? result : messages;
2121
+ }
2122
+
2123
+ /**
2124
+ * Redact outbound provider context. Only conversation messages are rewritten;
2125
+ * the static system prompt and tool schemas pass through unchanged.
2126
+ */
2127
+ export function obfuscateProviderContext(obfuscator: SecretObfuscator | undefined, context: Context): Context {
2128
+ if (!obfuscator?.hasSecrets()) return context;
2129
+ const messages = obfuscateMessages(obfuscator, context.messages);
2130
+ return messages === context.messages ? context : { ...context, messages };
2131
+ }
2132
+
2133
+ // ═══════════════════════════════════════════════════════════════════════════
2134
+ // Helpers
2135
+ // ═══════════════════════════════════════════════════════════════════════════
2136
+
2137
+ // Like the untracked walk, but threads a parallel `origin` tag string through:
2138
+ // preserved placeholder spans keep their existing origin tag (so a
2139
+ // same-call-fresh "F" placeholder is never relabeled prior-call "I", and vice
2140
+ // versa), while `transform`'s output — always freshly generated or redacted
2141
+ // content in both callers below — is tagged "I" (it must not be re-matched as
2142
+ // though it arrived in the input, mirroring plain-secret replacement tagging).
2143
+ function transformOutsidePlaceholdersTracked(
2144
+ text: string,
2145
+ origin: string,
2146
+ shouldSkipPlaceholder: (placeholder: string) => boolean,
2147
+ transform: (chunk: string) => string,
2148
+ preservePlaceholder?: (placeholder: string) => string,
2149
+ ): { text: string; origin: string } {
2150
+ PLACEHOLDER_RE.lastIndex = 0;
2151
+ let result = "";
2152
+ let resultOrigin = "";
2153
+ let pendingIndex = 0;
2154
+ for (;;) {
2155
+ const match = PLACEHOLDER_RE.exec(text);
2156
+ if (match === null) break;
2157
+ if (!shouldSkipPlaceholder(match[0])) {
2158
+ resumePlaceholderScanAfterRejectedCandidate(match);
2159
+ continue;
2160
+ }
2161
+ const transformed = transform(text.slice(pendingIndex, match.index));
2162
+ result += transformed;
2163
+ resultOrigin += "I".repeat(transformed.length);
2164
+ const preserved = preservePlaceholder ? preservePlaceholder(match[0]) : match[0];
2165
+ result += preserved;
2166
+ resultOrigin += origin.slice(match.index, match.index + match[0].length);
2167
+ pendingIndex = match.index + match[0].length;
2168
+ }
2169
+ const trailing = transform(text.slice(pendingIndex));
2170
+ result += trailing;
2171
+ resultOrigin += "I".repeat(trailing.length);
2172
+ return { text: result, origin: resultOrigin };
2173
+ }
2174
+
2175
+ function trailingOutsidePreservedPlaceholderChunk(
2176
+ text: string,
2177
+ shouldPreservePlaceholder: (placeholder: string) => boolean,
2178
+ ): string {
2179
+ PLACEHOLDER_RE.lastIndex = 0;
2180
+ let pendingIndex = 0;
2181
+ let sawPlaceholder = false;
2182
+ for (;;) {
2183
+ const match = PLACEHOLDER_RE.exec(text);
2184
+ if (match === null) break;
2185
+ if (!shouldPreservePlaceholder(match[0])) {
2186
+ resumePlaceholderScanAfterRejectedCandidate(match);
2187
+ continue;
2188
+ }
2189
+ sawPlaceholder = true;
2190
+ pendingIndex = match.index + match[0].length;
2191
+ }
2192
+ return sawPlaceholder ? text.slice(pendingIndex) : "";
2193
+ }
2194
+
2195
+ function buildReplaceRegexScan(
2196
+ text: string,
2197
+ ranges: ReadonlyArray<{ start: number; end: number }>,
2198
+ deobfuscateMap: ReadonlyMap<string, { secret: string; recursive: boolean }>,
2199
+ ): ReplaceRegexScan {
2200
+ let scanText = "";
2201
+ let cursor = 0;
2202
+ const segments: RegexScanSegment[] = [];
2203
+ const appendSegment = (
2204
+ value: string,
2205
+ textStart: number,
2206
+ textEnd: number,
2207
+ generatedPlaceholder: boolean,
2208
+ recursive: boolean,
2209
+ ) => {
2210
+ if (value.length === 0) return;
2211
+ const scanStart = scanText.length;
2212
+ scanText += value;
2213
+ segments.push({
2214
+ scanStart,
2215
+ scanEnd: scanStart + value.length,
2216
+ textStart,
2217
+ textEnd,
2218
+ generatedPlaceholder,
2219
+ recursive,
2220
+ });
2221
+ };
2222
+
2223
+ for (const range of ranges) {
2224
+ appendSegment(text.slice(cursor, range.start), cursor, range.start, false, false);
2225
+ const placeholder = text.slice(range.start, range.end);
2226
+ const mapping = lookupFriendlyPlaceholderAlias(deobfuscateMap, placeholder);
2227
+ appendSegment(mapping?.secret ?? placeholder, range.start, range.end, true, mapping?.recursive ?? false);
2228
+ cursor = range.end;
2229
+ }
2230
+ appendSegment(text.slice(cursor), cursor, text.length, false, false);
2231
+
2232
+ return { text: scanText, segments };
2233
+ }
2234
+
2235
+ function mapReplaceRegexMatch(
2236
+ segments: ReadonlyArray<RegexScanSegment>,
2237
+ scanStart: number,
2238
+ scanEnd: number,
2239
+ ): {
2240
+ start: number;
2241
+ end: number;
2242
+ recursive: boolean;
2243
+ preserveGeneratedPlaceholders: boolean;
2244
+ partialPlaceholderCut: boolean;
2245
+ cutResumeIndex: number;
2246
+ firstPlaceholderScanStart: number;
2247
+ } {
2248
+ const startSegment = findScanSegment(segments, scanStart);
2249
+ const endSegment = findScanSegment(segments, scanEnd - 1);
2250
+ const start = startSegment.generatedPlaceholder
2251
+ ? startSegment.textStart
2252
+ : startSegment.textStart + (scanStart - startSegment.scanStart);
2253
+ const end = endSegment.generatedPlaceholder
2254
+ ? endSegment.textEnd
2255
+ : endSegment.textStart + (scanEnd - endSegment.scanStart);
2256
+ // A match boundary that falls strictly inside a generated placeholder's
2257
+ // expanded value cuts the underlying secret: the snap above pulls the span out
2258
+ // to the whole `#…#` token, so the obfuscate path can leave it alone instead of
2259
+ // consuming a partial placeholder expansion.
2260
+ const partialPlaceholderCut =
2261
+ (startSegment.generatedPlaceholder && scanStart > startSegment.scanStart) ||
2262
+ (endSegment.generatedPlaceholder && scanEnd < endSegment.scanEnd);
2263
+ let recursive = false;
2264
+ let preserveGeneratedPlaceholders = false;
2265
+ // When the match straddles a placeholder, resume scanning just past the last
2266
+ // overlapping placeholder so trailing wholly-outside content (e.g. an 8-char
2267
+ // run after the secret) still gets matched instead of being consumed by the
2268
+ // straddling span. `firstPlaceholderScanStart` marks where the leading
2269
+ // wholly-outside prefix ends, so a prefix that independently matches can be
2270
+ // redacted on its own rather than skipped along with the cut span.
2271
+ let cutResumeIndex = scanStart;
2272
+ let firstPlaceholderScanStart = -1;
2273
+ for (const segment of segments) {
2274
+ if (segment.scanStart >= scanEnd || segment.scanEnd <= scanStart) continue;
2275
+ recursive ||= segment.recursive;
2276
+ preserveGeneratedPlaceholders ||= segment.generatedPlaceholder;
2277
+ if (segment.generatedPlaceholder) {
2278
+ if (firstPlaceholderScanStart === -1) firstPlaceholderScanStart = segment.scanStart;
2279
+ if (segment.scanEnd > cutResumeIndex) cutResumeIndex = segment.scanEnd;
2280
+ }
2281
+ }
2282
+ return {
2283
+ start,
2284
+ end,
2285
+ recursive,
2286
+ preserveGeneratedPlaceholders,
2287
+ partialPlaceholderCut,
2288
+ cutResumeIndex,
2289
+ firstPlaceholderScanStart,
2290
+ };
2291
+ }
2292
+
2293
+ function findScanSegment(segments: ReadonlyArray<RegexScanSegment>, scanIndex: number): RegexScanSegment {
2294
+ for (const segment of segments) {
2295
+ if (scanIndex >= segment.scanStart && scanIndex < segment.scanEnd) return segment;
2296
+ }
2297
+ throw new Error("regex match did not map to source text");
2298
+ }
2299
+
2300
+ /**
2301
+ * Extend a scan-space resume position past a consecutive run of generated
2302
+ * placeholder segments starting exactly at it, with no raw gap in between. A
2303
+ * cut-resolution resume point that happens to land precisely on the START of
2304
+ * ANOTHER placeholder must not stop there and hand it to a fresh `regex.exec`
2305
+ * attempt — the same content, scanned as an opaque adjacent placeholder run,
2306
+ * must resolve identically whether the run's LEADING member is still raw text
2307
+ * (this call is about to placeholder it) or is ALREADY a placeholder from a
2308
+ * prior call or an earlier pass of this same call. Without this, a bounded
2309
+ * regex whose reach spans two adjacent secrets plus trailing spillover bytes
2310
+ * (e.g. `[A-Z]{9}` over `ABCDEFGH` + `SECRETUV` + `A`) resolves the leading
2311
+ * secret as its own independent redaction on the FIRST obfuscate() call (a
2312
+ * genuinely raw prefix gets its own match, then the discard for the rest
2313
+ * resumes right after it), but on a LATER call — once that prefix is itself a
2314
+ * placeholder — the very first match attempt starts already inside the
2315
+ * placeholder run, cannot be prefix-narrowed at all, and its discard resume
2316
+ * point lands mid-run instead of past it, exposing a shorter tail (`SECRETUV`
2317
+ * + `A`) to a clean, un-cut match the first call never attempted. Chaining the
2318
+ * resume point through every immediately-adjacent placeholder makes both
2319
+ * calls land on the exact same next scan position.
2320
+ */
2321
+ function extendPastAdjacentPlaceholders(segments: ReadonlyArray<RegexScanSegment>, index: number): number {
2322
+ let cursor = index;
2323
+ for (;;) {
2324
+ const segment = segments.find(candidate => candidate.scanStart === cursor && candidate.generatedPlaceholder);
2325
+ if (!segment) return cursor;
2326
+ cursor = segment.scanEnd;
2327
+ }
2328
+ }
2329
+
2330
+ // Apply a fixed custom replacement across a matched span while preserving any
2331
+ // inner generated placeholders. Usually the replacement is the user's single
2332
+ // redaction marker for the whole match, so emit it for the first non-empty
2333
+ // surrounding chunk and drop later chunks. But bounded regexes can cut through
2334
+ // an already-emitted marker on the trailing side (`X#…#RED` from
2335
+ // `XSECRETUVREDACTED`), where dropping the later prefix would leave raw bytes
2336
+ // (`ACTED`) to be consumed on the next pass. Promote later chunks that are a
2337
+ // prefix of the replacement to the FULL marker so the first pass is already a
2338
+ // fixed point. The reversible placeholder stays intact in its relative
2339
+ // position.
2340
+ function redactWithFixedReplacementOutsidePlaceholders(
2341
+ text: string,
2342
+ origin: string,
2343
+ replacement: string,
2344
+ shouldPreservePlaceholder: (placeholder: string) => boolean,
2345
+ ): { text: string; origin: string } {
2346
+ let emitted = false;
2347
+ return transformOutsidePlaceholdersTracked(
2348
+ text,
2349
+ origin,
2350
+ shouldPreservePlaceholder,
2351
+ chunk => {
2352
+ if (chunk.length === 0) return "";
2353
+ if (!emitted) {
2354
+ emitted = true;
2355
+ return replacement;
2356
+ }
2357
+ return replacement.startsWith(chunk) ? replacement : "";
2358
+ },
2359
+ placeholder => placeholder,
2360
+ );
2361
+ }
2362
+
2363
+ function deobfuscateGeneratedPlaceholderRanges(
2364
+ text: string,
2365
+ start: number,
2366
+ end: number,
2367
+ ranges: ReadonlyArray<{ start: number; end: number }>,
2368
+ deobfuscateMap: ReadonlyMap<string, { secret: string; recursive: boolean }>,
2369
+ ): { text: string; recursive: boolean } {
2370
+ let result = "";
2371
+ let cursor = start;
2372
+ let recursive = false;
2373
+ for (const range of ranges) {
2374
+ if (range.end <= start || range.start >= end) continue;
2375
+ const overlapStart = Math.max(range.start, start);
2376
+ const overlapEnd = Math.min(range.end, end);
2377
+ result += text.slice(cursor, overlapStart);
2378
+ const placeholder = text.slice(overlapStart, overlapEnd);
2379
+ const mapping = lookupFriendlyPlaceholderAlias(deobfuscateMap, placeholder);
2380
+ result += mapping?.secret ?? placeholder;
2381
+ recursive ||= mapping?.recursive ?? false;
2382
+ cursor = overlapEnd;
2383
+ }
2384
+ result += text.slice(cursor, end);
2385
+ return { text: result, recursive };
2386
+ }
2387
+
2388
+ // Concatenate ONLY the deobfuscated placeholder ranges within [start, end),
2389
+ // dropping the bytes that lie outside them. Used to test whether a regex match
2390
+ // that straddles a prior-call placeholder would still match on the placeholder's
2391
+ // own (expanded) secret value alone — i.e. the surrounding raw bytes are greedy
2392
+ // spillover the match does not need, rather than content the match depends on.
2393
+ function placeholderInnerText(
2394
+ text: string,
2395
+ start: number,
2396
+ end: number,
2397
+ ranges: ReadonlyArray<{ start: number; end: number }>,
2398
+ deobfuscateMap: ReadonlyMap<string, { secret: string; recursive: boolean }>,
2399
+ ): string {
2400
+ let result = "";
2401
+ for (const range of ranges) {
2402
+ if (range.end <= start || range.start >= end) continue;
2403
+ const overlapStart = Math.max(range.start, start);
2404
+ const overlapEnd = Math.min(range.end, end);
2405
+ const placeholder = text.slice(overlapStart, overlapEnd);
2406
+ const mapping = lookupFriendlyPlaceholderAlias(deobfuscateMap, placeholder);
2407
+ result += mapping?.secret ?? placeholder;
2408
+ }
2409
+ return result;
2410
+ }
2411
+
2412
+ // Concatenate the bytes of [start, end) that lie OUTSIDE the given (ascending,
2413
+ // non-overlapping) placeholder ranges. Used to test whether a regex match that
2414
+ // straddles a prior-call placeholder would still match on its surrounding bytes
2415
+ // alone — i.e. those bytes are genuinely new content to redact rather than a
2416
+ // match that only exists because the deobfuscated placeholder bridges them.
2417
+ function textOutsidePlaceholderRanges(
2418
+ text: string,
2419
+ start: number,
2420
+ end: number,
2421
+ ranges: ReadonlyArray<{ start: number; end: number }>,
2422
+ ): string {
2423
+ let result = "";
2424
+ let cursor = start;
2425
+ for (const range of ranges) {
2426
+ if (range.end <= start || range.start >= end) continue;
2427
+ const overlapStart = Math.max(range.start, start);
2428
+ const overlapEnd = Math.min(range.end, end);
2429
+ result += text.slice(cursor, overlapStart);
2430
+ cursor = overlapEnd;
2431
+ }
2432
+ result += text.slice(cursor, end);
2433
+ return result;
2434
+ }
2435
+
2436
+ // Like `textOutsidePlaceholderRanges`, but tests each outside chunk against
2437
+ // `regex` in its REAL context instead of on an isolated slice — tried in BOTH
2438
+ // the literal `#…#` placeholder-token text AND the EXPANDED scan context
2439
+ // (placeholder resolved to its secret value), since either can be the reason a
2440
+ // chunk independently requires redaction:
2441
+ // - Literal-token context matters when the placeholder TOKEN's own non-word
2442
+ // boundary is what completes a boundary-sensitive pattern, e.g. a prefix
2443
+ // "ABCDEFGH" next to a placeholder token matches `\b[A-Z]{8}\b` because the
2444
+ // token's leading `#` is a non-word byte — but that boundary disappears
2445
+ // once the placeholder expands into more `[A-Z]` bytes with no separator.
2446
+ // - Expanded scan context matters when a lookbehind/lookahead only resolves
2447
+ // once the neighboring placeholder is expanded, e.g. a prior plain
2448
+ // placeholder for `ABCDEFGH` next to raw `SECRET`, matched by
2449
+ // `(?<=ABCDEFGH)SECRET`: the literal placeholder token before `SECRET`
2450
+ // never satisfies the lookbehind, so literal-context alone wrongly reports
2451
+ // no independent match.
2452
+ // A match only counts when it lies ENTIRELY within one outside chunk (in
2453
+ // whichever context it was tested); a match that reaches into the
2454
+ // placeholder itself is not evidence the outside chunk independently
2455
+ // requires redaction.
2456
+ function outsidePlaceholderRangesAnyIndependentlyMatch(
2457
+ text: string,
2458
+ scanText: string,
2459
+ segments: ReadonlyArray<RegexScanSegment>,
2460
+ start: number,
2461
+ end: number,
2462
+ ranges: ReadonlyArray<{ start: number; end: number }>,
2463
+ regex: RegExp,
2464
+ ): boolean {
2465
+ // A text-space outside chunk lies entirely within one non-placeholder scan
2466
+ // segment (placeholder ranges are exactly the gaps between such segments),
2467
+ // so its scan-space span is a fixed offset from its text-space span.
2468
+ const toScanSpace = (chunkStart: number, chunkEnd: number): [number, number] | undefined => {
2469
+ for (const segment of segments) {
2470
+ if (segment.generatedPlaceholder || segment.textStart > chunkStart || segment.textEnd < chunkEnd) continue;
2471
+ const offset = segment.scanStart - segment.textStart;
2472
+ return [chunkStart + offset, chunkEnd + offset];
2473
+ }
2474
+ return undefined;
2475
+ };
2476
+ const chunkIndependentlyMatches = (chunkStart: number, chunkEnd: number): boolean => {
2477
+ if (chunkMatchesInSourceContext(text, chunkStart, chunkEnd, regex)) return true;
2478
+ const scanSpan = toScanSpace(chunkStart, chunkEnd);
2479
+ return scanSpan !== undefined && chunkMatchesInSourceContext(scanText, scanSpan[0], scanSpan[1], regex);
2480
+ };
2481
+ let cursor = start;
2482
+ for (const range of ranges) {
2483
+ if (range.end <= start || range.start >= end) continue;
2484
+ const overlapStart = Math.max(range.start, start);
2485
+ const overlapEnd = Math.min(range.end, end);
2486
+ if (cursor < overlapStart && chunkIndependentlyMatches(cursor, overlapStart)) return true;
2487
+ cursor = overlapEnd;
2488
+ }
2489
+ return cursor < end && chunkIndependentlyMatches(cursor, end);
2490
+ }
2491
+
2492
+ // Whether `regex` (global) has a match fully contained in [chunkStart, chunkEnd)
2493
+ // when run against the full `text` — so lookbehind/lookahead see the actual
2494
+ // surrounding bytes rather than an isolated slice's edges.
2495
+ function chunkMatchesInSourceContext(text: string, chunkStart: number, chunkEnd: number, regex: RegExp): boolean {
2496
+ regex.lastIndex = chunkStart;
2497
+ for (;;) {
2498
+ const found = regex.exec(text);
2499
+ if (found === null || found.index >= chunkEnd) return false;
2500
+ const matchEnd = found.index + found[0].length;
2501
+ if (matchEnd <= chunkEnd) return true;
2502
+ regex.lastIndex = found[0].length === 0 ? found.index + 1 : matchEnd;
2503
+ }
2504
+ }
2505
+
2506
+ function firstOutsidePlaceholderRange(
2507
+ start: number,
2508
+ end: number,
2509
+ ranges: ReadonlyArray<{ start: number; end: number }>,
2510
+ ): { start: number; end: number } | undefined {
2511
+ let cursor = start;
2512
+ for (const range of ranges) {
2513
+ if (range.end <= start || range.start >= end) continue;
2514
+ const overlapStart = Math.max(range.start, start);
2515
+ const overlapEnd = Math.min(range.end, end);
2516
+ if (cursor < overlapStart) return { start: cursor, end: overlapStart };
2517
+ cursor = overlapEnd;
2518
+ }
2519
+ return cursor < end ? { start: cursor, end } : undefined;
2520
+ }
2521
+
2522
+ function countOutsidePlaceholderRanges(
2523
+ start: number,
2524
+ end: number,
2525
+ ranges: ReadonlyArray<{ start: number; end: number }>,
2526
+ ): number {
2527
+ let count = 0;
2528
+ let cursor = start;
2529
+ for (const range of ranges) {
2530
+ if (range.end <= start || range.start >= end) continue;
2531
+ const overlapStart = Math.max(range.start, start);
2532
+ const overlapEnd = Math.min(range.end, end);
2533
+ if (cursor < overlapStart) count++;
2534
+ cursor = overlapEnd;
2535
+ }
2536
+ if (cursor < end) count++;
2537
+ return count;
2538
+ }
2539
+
2540
+ function replaceRange(text: string, start: number, end: number, replacement: string): string {
2541
+ return text.slice(0, start) + replacement + text.slice(end);
2542
+ }
2543
+
2544
+ /** Deep-walk an object, transforming all string values. */
2545
+ function deepWalkStrings<T>(obj: T, transform: (s: string) => string): T {
2546
+ if (typeof obj === "string") {
2547
+ return transform(obj) as unknown as T;
2548
+ }
2549
+ if (Array.isArray(obj)) {
2550
+ let changed = false;
2551
+ const result = obj.map(item => {
2552
+ const transformed = deepWalkStrings(item, transform);
2553
+ if (transformed !== item) changed = true;
2554
+ return transformed;
2555
+ });
2556
+ return (changed ? result : obj) as unknown as T;
2557
+ }
2558
+ if (obj !== null && typeof obj === "object" && isPlainRecord(obj)) {
2559
+ let changed = false;
2560
+ const result: Record<string, unknown> = {};
2561
+ for (const key of Object.keys(obj)) {
2562
+ const value = (obj as Record<string, unknown>)[key];
2563
+ const transformed = deepWalkStrings(value, transform);
2564
+ if (transformed !== value) changed = true;
2565
+ result[key] = transformed;
2566
+ }
2567
+ return (changed ? result : obj) as T;
2568
+ }
2569
+ return obj;
2570
+ }
2571
+
2572
+ function isPlainRecord(obj: object): obj is Record<string, unknown> {
2573
+ const prototype = Object.getPrototypeOf(obj);
2574
+ return prototype === Object.prototype || prototype === null;
2575
+ }
2576
+
2577
+ function collectJsonRegexSecretValues(obfuscator: SecretObfuscator, value: JsonValue): Set<string> {
2578
+ const values = new Set<string>();
2579
+ const collect = (item: JsonValue): void => {
2580
+ if (typeof item === "string") {
2581
+ for (const secretValue of obfuscator.collectRegexSecretValuesForObfuscation(item)) {
2582
+ values.add(secretValue);
2583
+ }
2584
+ return;
2585
+ }
2586
+ if (Array.isArray(item)) {
2587
+ for (const child of item) collect(child);
2588
+ return;
2589
+ }
2590
+ if (item !== null && typeof item === "object") {
2591
+ for (const child of Object.values(item)) {
2592
+ if (child !== undefined) collect(child);
2593
+ }
2594
+ }
2595
+ };
2596
+ collect(value);
2597
+ return values;
2598
+ }
2599
+
2600
+ /**
2601
+ * Map every string in arbitrary JSON. Used ONLY for tool-call arguments, whose
2602
+ * shape is model-authored and not known ahead of time. No other caller may walk
2603
+ * untyped data: every message/content path is handled by a typed transformer.
2604
+ */
2605
+ function mapJsonStrings(value: JsonValue, fn: (s: string) => string): JsonValue {
2606
+ if (typeof value === "string") return fn(value);
2607
+ if (Array.isArray(value)) {
2608
+ let changed = false;
2609
+ const out = value.map(item => {
2610
+ const next = mapJsonStrings(item, fn);
2611
+ if (next !== item) changed = true;
2612
+ return next;
2613
+ });
2614
+ return changed ? out : value;
2615
+ }
2616
+ if (value !== null && typeof value === "object") {
2617
+ let changed = false;
2618
+ const out: JsonRecord = {};
2619
+ for (const key of Object.keys(value)) {
2620
+ const item = value[key];
2621
+ if (item === undefined) continue;
2622
+ const next = mapJsonStrings(item, fn);
2623
+ if (next !== item) changed = true;
2624
+ out[key] = next;
2625
+ }
2626
+ return changed ? out : value;
2627
+ }
2628
+ return value;
2629
+ }