@leanandmean/coding-agent 0.74.1-scramjet.7 → 0.74.1-scramjet.71

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 (226) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/LICENSE +202 -0
  3. package/README.md +37 -9
  4. package/dist/cli/args.d.ts +2 -0
  5. package/dist/cli/args.d.ts.map +1 -1
  6. package/dist/cli/args.js +45 -1
  7. package/dist/cli/args.js.map +1 -1
  8. package/dist/cli/initial-message.d.ts +1 -0
  9. package/dist/cli/initial-message.d.ts.map +1 -1
  10. package/dist/cli/initial-message.js +3 -0
  11. package/dist/cli/initial-message.js.map +1 -1
  12. package/dist/cli/list-models.d.ts +2 -0
  13. package/dist/cli/list-models.d.ts.map +1 -1
  14. package/dist/cli/list-models.js +4 -1
  15. package/dist/cli/list-models.js.map +1 -1
  16. package/dist/config.d.ts +9 -2
  17. package/dist/config.d.ts.map +1 -1
  18. package/dist/config.js +33 -22
  19. package/dist/config.js.map +1 -1
  20. package/dist/core/agent-session-runtime.d.ts +12 -3
  21. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  22. package/dist/core/agent-session-runtime.js +64 -38
  23. package/dist/core/agent-session-runtime.js.map +1 -1
  24. package/dist/core/agent-session-services.d.ts +2 -0
  25. package/dist/core/agent-session-services.d.ts.map +1 -1
  26. package/dist/core/agent-session-services.js +3 -0
  27. package/dist/core/agent-session-services.js.map +1 -1
  28. package/dist/core/agent-session.d.ts +84 -17
  29. package/dist/core/agent-session.d.ts.map +1 -1
  30. package/dist/core/agent-session.js +1082 -209
  31. package/dist/core/agent-session.js.map +1 -1
  32. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  33. package/dist/core/compaction/branch-summarization.js +21 -17
  34. package/dist/core/compaction/branch-summarization.js.map +1 -1
  35. package/dist/core/compaction/compaction.d.ts +3 -5
  36. package/dist/core/compaction/compaction.d.ts.map +1 -1
  37. package/dist/core/compaction/compaction.js +43 -10
  38. package/dist/core/compaction/compaction.js.map +1 -1
  39. package/dist/core/exec.d.ts +12 -0
  40. package/dist/core/exec.d.ts.map +1 -1
  41. package/dist/core/exec.js +74 -28
  42. package/dist/core/exec.js.map +1 -1
  43. package/dist/core/export-html/template.js +4 -0
  44. package/dist/core/extensions/index.d.ts +1 -1
  45. package/dist/core/extensions/index.d.ts.map +1 -1
  46. package/dist/core/extensions/index.js.map +1 -1
  47. package/dist/core/extensions/runner.d.ts +9 -5
  48. package/dist/core/extensions/runner.d.ts.map +1 -1
  49. package/dist/core/extensions/runner.js +160 -3
  50. package/dist/core/extensions/runner.js.map +1 -1
  51. package/dist/core/extensions/types.d.ts +85 -8
  52. package/dist/core/extensions/types.d.ts.map +1 -1
  53. package/dist/core/extensions/types.js.map +1 -1
  54. package/dist/core/keybindings.d.ts +8 -0
  55. package/dist/core/keybindings.d.ts.map +1 -1
  56. package/dist/core/model-registry.d.ts +103 -0
  57. package/dist/core/model-registry.d.ts.map +1 -1
  58. package/dist/core/model-registry.js +272 -28
  59. package/dist/core/model-registry.js.map +1 -1
  60. package/dist/core/model-resolver.d.ts.map +1 -1
  61. package/dist/core/model-resolver.js +24 -6
  62. package/dist/core/model-resolver.js.map +1 -1
  63. package/dist/core/output-throughput-history.d.ts +25 -0
  64. package/dist/core/output-throughput-history.d.ts.map +1 -0
  65. package/dist/core/output-throughput-history.js +240 -0
  66. package/dist/core/output-throughput-history.js.map +1 -0
  67. package/dist/core/output-throughput.d.ts +43 -0
  68. package/dist/core/output-throughput.d.ts.map +1 -0
  69. package/dist/core/output-throughput.js +161 -0
  70. package/dist/core/output-throughput.js.map +1 -0
  71. package/dist/core/resource-loader.d.ts +6 -0
  72. package/dist/core/resource-loader.d.ts.map +1 -1
  73. package/dist/core/resource-loader.js +76 -21
  74. package/dist/core/resource-loader.js.map +1 -1
  75. package/dist/core/scramjet-command-parser.d.ts +4 -0
  76. package/dist/core/scramjet-command-parser.d.ts.map +1 -1
  77. package/dist/core/scramjet-command-parser.js +72 -1
  78. package/dist/core/scramjet-command-parser.js.map +1 -1
  79. package/dist/core/sdk.d.ts +4 -0
  80. package/dist/core/sdk.d.ts.map +1 -1
  81. package/dist/core/sdk.js +70 -11
  82. package/dist/core/sdk.js.map +1 -1
  83. package/dist/core/session-manager.d.ts +11 -3
  84. package/dist/core/session-manager.d.ts.map +1 -1
  85. package/dist/core/session-manager.js +46 -11
  86. package/dist/core/session-manager.js.map +1 -1
  87. package/dist/core/settings-manager.d.ts +19 -6
  88. package/dist/core/settings-manager.d.ts.map +1 -1
  89. package/dist/core/settings-manager.js +131 -17
  90. package/dist/core/settings-manager.js.map +1 -1
  91. package/dist/core/skills.d.ts.map +1 -1
  92. package/dist/core/skills.js +6 -2
  93. package/dist/core/skills.js.map +1 -1
  94. package/dist/core/system-prompt.d.ts +2 -0
  95. package/dist/core/system-prompt.d.ts.map +1 -1
  96. package/dist/core/system-prompt.js +5 -2
  97. package/dist/core/system-prompt.js.map +1 -1
  98. package/dist/core/tools/bash.d.ts.map +1 -1
  99. package/dist/core/tools/bash.js +4 -3
  100. package/dist/core/tools/bash.js.map +1 -1
  101. package/dist/index.d.ts +4 -3
  102. package/dist/index.d.ts.map +1 -1
  103. package/dist/index.js +3 -2
  104. package/dist/index.js.map +1 -1
  105. package/dist/main.d.ts +119 -0
  106. package/dist/main.d.ts.map +1 -1
  107. package/dist/main.js +113 -11
  108. package/dist/main.js.map +1 -1
  109. package/dist/modes/interactive/components/armin.d.ts +4 -2
  110. package/dist/modes/interactive/components/armin.d.ts.map +1 -1
  111. package/dist/modes/interactive/components/armin.js +14 -8
  112. package/dist/modes/interactive/components/armin.js.map +1 -1
  113. package/dist/modes/interactive/components/assistant-message.d.ts +3 -1
  114. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  115. package/dist/modes/interactive/components/assistant-message.js +19 -8
  116. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  117. package/dist/modes/interactive/components/daxnuts.d.ts +4 -1
  118. package/dist/modes/interactive/components/daxnuts.d.ts.map +1 -1
  119. package/dist/modes/interactive/components/daxnuts.js +14 -6
  120. package/dist/modes/interactive/components/daxnuts.js.map +1 -1
  121. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  122. package/dist/modes/interactive/components/extension-editor.js +5 -3
  123. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  124. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  125. package/dist/modes/interactive/components/extension-selector.js +4 -3
  126. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  127. package/dist/modes/interactive/components/footer.d.ts +4 -0
  128. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  129. package/dist/modes/interactive/components/footer.js +24 -20
  130. package/dist/modes/interactive/components/footer.js.map +1 -1
  131. package/dist/modes/interactive/components/model-selector.d.ts +3 -3
  132. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  133. package/dist/modes/interactive/components/model-selector.js +10 -7
  134. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  135. package/dist/modes/interactive/components/scoped-models-selector.d.ts +3 -1
  136. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  137. package/dist/modes/interactive/components/scoped-models-selector.js +8 -2
  138. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  139. package/dist/modes/interactive/components/settings-selector.d.ts +17 -3
  140. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  141. package/dist/modes/interactive/components/settings-selector.js +87 -16
  142. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  143. package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  144. package/dist/modes/interactive/components/thinking-selector.js +2 -1
  145. package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  146. package/dist/modes/interactive/components/tool-execution.d.ts +19 -1
  147. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  148. package/dist/modes/interactive/components/tool-execution.js +129 -36
  149. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  150. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  151. package/dist/modes/interactive/components/user-message.js +3 -2
  152. package/dist/modes/interactive/components/user-message.js.map +1 -1
  153. package/dist/modes/interactive/components/visual-truncate.d.ts.map +1 -1
  154. package/dist/modes/interactive/components/visual-truncate.js +2 -2
  155. package/dist/modes/interactive/components/visual-truncate.js.map +1 -1
  156. package/dist/modes/interactive/interactive-mode.d.ts +37 -1
  157. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  158. package/dist/modes/interactive/interactive-mode.js +902 -177
  159. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  160. package/dist/modes/interactive/spellcheck.d.ts +26 -0
  161. package/dist/modes/interactive/spellcheck.d.ts.map +1 -0
  162. package/dist/modes/interactive/spellcheck.js +144 -0
  163. package/dist/modes/interactive/spellcheck.js.map +1 -0
  164. package/dist/modes/interactive/theme/{dark.json → pi-dark.json} +3 -2
  165. package/dist/modes/interactive/theme/{light.json → pi-light.json} +27 -26
  166. package/dist/modes/interactive/theme/theme-schema.json +6 -1
  167. package/dist/modes/interactive/theme/theme.d.ts +24 -4
  168. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  169. package/dist/modes/interactive/theme/theme.js +123 -22
  170. package/dist/modes/interactive/theme/theme.js.map +1 -1
  171. package/dist/modes/print-mode.d.ts.map +1 -1
  172. package/dist/modes/print-mode.js +5 -0
  173. package/dist/modes/print-mode.js.map +1 -1
  174. package/dist/modes/rpc/rpc-client.d.ts +3 -1
  175. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  176. package/dist/modes/rpc/rpc-client.js.map +1 -1
  177. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  178. package/dist/modes/rpc/rpc-mode.js +6 -1
  179. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  180. package/dist/package-manager-cli.d.ts.map +1 -1
  181. package/dist/package-manager-cli.js +33 -14
  182. package/dist/package-manager-cli.js.map +1 -1
  183. package/dist/utils/clipboard-native.d.ts +1 -0
  184. package/dist/utils/clipboard-native.d.ts.map +1 -1
  185. package/dist/utils/clipboard-native.js.map +1 -1
  186. package/dist/utils/clipboard.d.ts +6 -0
  187. package/dist/utils/clipboard.d.ts.map +1 -1
  188. package/dist/utils/clipboard.js +278 -10
  189. package/dist/utils/clipboard.js.map +1 -1
  190. package/dist/utils/tools-manager.d.ts +1 -1
  191. package/dist/utils/tools-manager.d.ts.map +1 -1
  192. package/dist/utils/tools-manager.js +7 -6
  193. package/dist/utils/tools-manager.js.map +1 -1
  194. package/dist/utils/version-check.d.ts +17 -5
  195. package/dist/utils/version-check.d.ts.map +1 -1
  196. package/dist/utils/version-check.js +88 -21
  197. package/dist/utils/version-check.js.map +1 -1
  198. package/docs/compaction.md +21 -3
  199. package/docs/custom-provider.md +33 -4
  200. package/docs/extensions.md +150 -24
  201. package/docs/json.md +8 -0
  202. package/docs/keybindings.md +25 -1
  203. package/docs/models.md +103 -4
  204. package/docs/packages.md +2 -0
  205. package/docs/prompt-templates.md +2 -0
  206. package/docs/quickstart.md +1 -4
  207. package/docs/rpc.md +16 -6
  208. package/docs/sdk.md +48 -3
  209. package/docs/session-format.md +5 -1
  210. package/docs/settings.md +19 -5
  211. package/docs/skills.md +7 -5
  212. package/docs/terminal-setup.md +79 -2
  213. package/docs/terminal-verification.md +71 -0
  214. package/docs/themes.md +63 -10
  215. package/docs/tui.md +120 -25
  216. package/docs/usage.md +6 -4
  217. package/examples/extensions/border-status-editor.ts +2 -3
  218. package/examples/extensions/doom-overlay/doom-component.ts +3 -3
  219. package/examples/extensions/mac-system-theme.ts +3 -2
  220. package/examples/extensions/modal-editor.ts +13 -8
  221. package/examples/extensions/overlay-qa-tests.ts +22 -9
  222. package/examples/extensions/overlay-test.ts +5 -3
  223. package/examples/extensions/snake.ts +3 -1
  224. package/examples/extensions/space-invaders.ts +6 -3
  225. package/examples/extensions/subagent/index.ts +20 -17
  226. package/package.json +69 -58
@@ -1 +1 @@
1
- {"version":3,"file":"clipboard.d.ts","sourceRoot":"","sources":["../../src/utils/clipboard.ts"],"names":[],"mappings":"AAkCA,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA4FjE","sourcesContent":["import { execSync, spawn } from \"child_process\";\nimport { platform } from \"os\";\nimport { isWaylandSession } from \"./clipboard-image.js\";\nimport { clipboard } from \"./clipboard-native.js\";\n\ntype NativeClipboardExecOptions = {\n\tinput: string;\n\ttimeout: number;\n\tstdio: [\"pipe\", \"ignore\", \"ignore\"];\n};\n\nfunction copyToX11Clipboard(options: NativeClipboardExecOptions): void {\n\ttry {\n\t\texecSync(\"xclip -selection clipboard\", options);\n\t} catch {\n\t\texecSync(\"xsel --clipboard --input\", options);\n\t}\n}\n\nconst MAX_OSC52_ENCODED_LENGTH = 100_000;\n\nfunction isRemoteSession(env: NodeJS.ProcessEnv = process.env): boolean {\n\treturn Boolean(env.SSH_CONNECTION || env.SSH_CLIENT || env.MOSH_CONNECTION);\n}\n\nfunction emitOsc52(text: string): boolean {\n\tconst encoded = Buffer.from(text).toString(\"base64\");\n\tif (encoded.length > MAX_OSC52_ENCODED_LENGTH) {\n\t\treturn false;\n\t}\n\tprocess.stdout.write(`\\x1b]52;c;${encoded}\\x07`);\n\treturn true;\n}\n\nexport async function copyToClipboard(text: string): Promise<void> {\n\tlet copied = false;\n\n\tconst p = platform();\n\n\t// Prefer direct clipboard writes. Emitting OSC 52 first can make terminals\n\t// write the same native clipboard concurrently with the addon, and very large\n\t// OSC 52 payloads can desynchronize terminal rendering.\n\t//\n\t// On Linux, skip the native addon. The underlying `clipboard-rs` crate is\n\t// X11-only and does not retain selection ownership after `set_text`\n\t// resolves, so on Wayland-only compositors (Hyprland, Niri, ...) and even\n\t// some X11 sessions the call resolves successfully without populating the\n\t// clipboard. The platform tools below (wl-copy, xclip, xsel) properly\n\t// daemonize and keep ownership.\n\ttry {\n\t\tif (clipboard && p !== \"linux\") {\n\t\t\tawait clipboard.setText(text);\n\t\t\tcopied = true;\n\t\t}\n\t} catch {\n\t\t// Fall through to platform-specific clipboard tools.\n\t}\n\n\tconst remote = isRemoteSession();\n\tif (copied && !remote) {\n\t\treturn;\n\t}\n\n\tconst options: NativeClipboardExecOptions = { input: text, timeout: 5000, stdio: [\"pipe\", \"ignore\", \"ignore\"] };\n\n\tif (!copied) {\n\t\ttry {\n\t\t\tif (p === \"darwin\") {\n\t\t\t\texecSync(\"pbcopy\", options);\n\t\t\t\tcopied = true;\n\t\t\t} else if (p === \"win32\") {\n\t\t\t\texecSync(\"clip\", options);\n\t\t\t\tcopied = true;\n\t\t\t} else {\n\t\t\t\t// Linux. Try Termux, Wayland, or X11 clipboard tools.\n\t\t\t\tif (process.env.TERMUX_VERSION) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\texecSync(\"termux-clipboard-set\", options);\n\t\t\t\t\t\tcopied = true;\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Fall back to Wayland or X11 tools.\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (!copied) {\n\t\t\t\t\tconst hasWaylandDisplay = Boolean(process.env.WAYLAND_DISPLAY);\n\t\t\t\t\tconst hasX11Display = Boolean(process.env.DISPLAY);\n\t\t\t\t\tconst isWayland = isWaylandSession();\n\t\t\t\t\tif (isWayland && hasWaylandDisplay) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t// Verify wl-copy exists (spawn errors are async and won't be caught)\n\t\t\t\t\t\t\texecSync(\"which wl-copy\", { stdio: \"ignore\" });\n\t\t\t\t\t\t\t// wl-copy with execSync hangs due to fork behavior; use spawn instead\n\t\t\t\t\t\t\tconst proc = spawn(\"wl-copy\", [], { stdio: [\"pipe\", \"ignore\", \"ignore\"] });\n\t\t\t\t\t\t\tproc.stdin.on(\"error\", () => {\n\t\t\t\t\t\t\t\t// Ignore EPIPE errors if wl-copy exits early\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tproc.stdin.write(text);\n\t\t\t\t\t\t\tproc.stdin.end();\n\t\t\t\t\t\t\tproc.unref();\n\t\t\t\t\t\t\tcopied = true;\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\tif (hasX11Display) {\n\t\t\t\t\t\t\t\tcopyToX11Clipboard(options);\n\t\t\t\t\t\t\t\tcopied = true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (hasX11Display) {\n\t\t\t\t\t\tcopyToX11Clipboard(options);\n\t\t\t\t\t\tcopied = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// Fall through to OSC 52 fallback.\n\t\t}\n\t}\n\n\tif (remote || !copied) {\n\t\tconst osc52Copied = emitOsc52(text);\n\t\tcopied = copied || osc52Copied;\n\t}\n\n\tif (!copied) {\n\t\tthrow new Error(\"Failed to copy to clipboard\");\n\t}\n}\n"]}
1
+ {"version":3,"file":"clipboard.d.ts","sourceRoot":"","sources":["../../src/utils/clipboard.ts"],"names":[],"mappings":"AAoCA,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC,CA0DzD;AAGD,wBAAgB,wBAAwB;;;;cA2LvC;AAED,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAiHjE","sourcesContent":["import { execFile, execSync, spawn } from \"child_process\";\nimport { platform } from \"os\";\nimport { waitForChildProcess } from \"./child-process.js\";\nimport { isWaylandSession } from \"./clipboard-image.js\";\nimport { clipboard } from \"./clipboard-native.js\";\n\ntype NativeClipboardExecOptions = {\n\tinput: string;\n\ttimeout: number;\n\tstdio: [\"pipe\", \"ignore\", \"ignore\"];\n};\n\nfunction copyToX11Clipboard(options: NativeClipboardExecOptions): void {\n\ttry {\n\t\texecSync(\"xclip -selection clipboard\", options);\n\t} catch {\n\t\texecSync(\"xsel --clipboard --input\", options);\n\t}\n}\n\nconst MAX_OSC52_ENCODED_LENGTH = 100_000;\n\nfunction isRemoteSession(env: NodeJS.ProcessEnv = process.env): boolean {\n\treturn Boolean(env.SSH_CONNECTION || env.SSH_CLIENT || env.MOSH_CONNECTION);\n}\n\nfunction emitOsc52(text: string): boolean {\n\tconst encoded = Buffer.from(text).toString(\"base64\");\n\tif (encoded.length > MAX_OSC52_ENCODED_LENGTH) {\n\t\treturn false;\n\t}\n\tprocess.stdout.write(`\\x1b]52;c;${encoded}\\x07`);\n\treturn true;\n}\n\n// SCRAMJET-DIVERGENCE: mouse reporting replaces the terminal's editor right-click paste.\nexport async function readClipboardText(): Promise<string> {\n\tif (isRemoteSession()) throw new Error(\"Use your terminal's Paste command in remote sessions\");\n\tconst p = platform();\n\tconst wsl = p === \"linux\" && Boolean(process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP);\n\tif (clipboard && !wsl && p !== \"linux\") {\n\t\ttry {\n\t\t\treturn await clipboard.getText();\n\t\t} catch {\n\t\t\t// The platform command remains available if the optional addon fails.\n\t\t}\n\t}\n\tconst commands: [string, string[]][] =\n\t\twsl || p === \"win32\"\n\t\t\t? [\n\t\t\t\t\t[\n\t\t\t\t\t\t\"powershell.exe\",\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\"-NoProfile\",\n\t\t\t\t\t\t\t\"-NonInteractive\",\n\t\t\t\t\t\t\t\"-STA\",\n\t\t\t\t\t\t\t\"-Command\",\n\t\t\t\t\t\t\t\"Add-Type -AssemblyName System.Windows.Forms; [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false); [Console]::Write([System.Windows.Forms.Clipboard]::GetText())\",\n\t\t\t\t\t\t],\n\t\t\t\t\t],\n\t\t\t\t]\n\t\t\t: p === \"darwin\"\n\t\t\t\t? [[\"pbpaste\", []]]\n\t\t\t\t: process.env.TERMUX_VERSION\n\t\t\t\t\t? [[\"termux-clipboard-get\", []]]\n\t\t\t\t\t: [\n\t\t\t\t\t\t\t...(process.env.WAYLAND_DISPLAY\n\t\t\t\t\t\t\t\t? [[\"wl-paste\", [\"--no-newline\", \"--type\", \"text\"]] as [string, string[]]]\n\t\t\t\t\t\t\t\t: []),\n\t\t\t\t\t\t\t...(process.env.DISPLAY\n\t\t\t\t\t\t\t\t? ([\n\t\t\t\t\t\t\t\t\t\t[\"xclip\", [\"-selection\", \"clipboard\", \"-o\"]],\n\t\t\t\t\t\t\t\t\t\t[\"xsel\", [\"--clipboard\", \"--output\"]],\n\t\t\t\t\t\t\t\t\t] as [string, string[]][])\n\t\t\t\t\t\t\t\t: []),\n\t\t\t\t\t\t];\n\tfor (const [command, args] of commands) {\n\t\ttry {\n\t\t\treturn await new Promise<string>((resolve, reject) => {\n\t\t\t\texecFile(\n\t\t\t\t\tcommand,\n\t\t\t\t\targs,\n\t\t\t\t\t{ encoding: \"utf8\", timeout: 5000, maxBuffer: 10 * 1024 * 1024, killSignal: \"SIGKILL\" },\n\t\t\t\t\t(error, stdout) => {\n\t\t\t\t\t\tif (error) reject(error);\n\t\t\t\t\t\telse resolve(stdout);\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t});\n\t\t} catch {\n\t\t\t// Try the next local backend without requesting remote clipboard access.\n\t\t}\n\t}\n\tthrow new Error(\"Cannot read clipboard text; use your terminal's Paste command\");\n}\n\n// SCRAMJET-DIVERGENCE: prewarm WSL interop without polling or caching clipboard contents.\nexport function createWslClipboardReader() {\n\tif (platform() !== \"linux\" || !(process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) || isRemoteSession())\n\t\treturn undefined;\n\tconst maxBytes = 10 * 1024 * 1024;\n\tconst maxWireBytes = 4 * Math.ceil(maxBytes / 3) + 6;\n\tconst script = `\n$ErrorActionPreference = 'Stop'\nAdd-Type @'\nusing System;\nusing System.Runtime.InteropServices;\npublic static class ScramjetClipboard {\n [DllImport(\"user32.dll\")] static extern bool OpenClipboard(IntPtr owner);\n [DllImport(\"user32.dll\")] static extern bool CloseClipboard();\n [DllImport(\"user32.dll\")] static extern bool IsClipboardFormatAvailable(uint format);\n [DllImport(\"user32.dll\")] static extern IntPtr GetClipboardData(uint format);\n [DllImport(\"kernel32.dll\")] static extern IntPtr GlobalLock(IntPtr handle);\n [DllImport(\"kernel32.dll\")] static extern bool GlobalUnlock(IntPtr handle);\n [DllImport(\"kernel32.dll\")] static extern UIntPtr GlobalSize(IntPtr handle);\n public static string ReadText() {\n for (int attempt = 0; !OpenClipboard(IntPtr.Zero); attempt++) {\n if (attempt == 9) throw new InvalidOperationException(\"Clipboard unavailable\");\n System.Threading.Thread.Sleep(5);\n }\n try {\n if (!IsClipboardFormatAvailable(13)) return \"\";\n IntPtr handle = GetClipboardData(13);\n if (handle == IntPtr.Zero) throw new InvalidOperationException(\"Clipboard data unavailable\");\n ulong size = GlobalSize(handle).ToUInt64();\n if (size < 2 || size > ${maxBytes * 2 + 2} || size % 2 != 0) throw new InvalidOperationException(\"Invalid clipboard size\");\n IntPtr data = GlobalLock(handle);\n if (data == IntPtr.Zero) throw new InvalidOperationException(\"Clipboard data unavailable\");\n try {\n string value = Marshal.PtrToStringUni(data, (int)(size / 2));\n int end = value.IndexOf('\\\\0');\n if (end < 0) throw new InvalidOperationException(\"Unterminated clipboard text\");\n return value.Substring(0, end);\n } finally { GlobalUnlock(handle); }\n } finally { CloseClipboard(); }\n }\n}\n'@\n[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)\n[Console]::Out.WriteLine('READY'); [Console]::Out.Flush()\nwhile ($null -ne ($request = [Console]::In.ReadLine())) {\n if ($request -ne 'read') { break }\n try {\n $text = [ScramjetClipboard]::ReadText()\n if ([System.Text.Encoding]::UTF8.GetByteCount($text) -gt ${maxBytes}) {\n [Console]::Out.WriteLine('ERR:size')\n } else {\n $bytes = [System.Text.Encoding]::UTF8.GetBytes($text)\n [Console]::Out.WriteLine('OK:' + [Convert]::ToBase64String($bytes))\n }\n } catch { [Console]::Out.WriteLine('ERR:read') }\n finally { $text = $null; $bytes = $null }\n [Console]::Out.Flush()\n}`;\n\tlet worker: ReturnType<typeof launch> | undefined;\n\tfunction launch() {\n\t\tconst child = spawn(\n\t\t\t\"powershell.exe\",\n\t\t\t[\n\t\t\t\t\"-NoLogo\",\n\t\t\t\t\"-NoProfile\",\n\t\t\t\t\"-NonInteractive\",\n\t\t\t\t\"-STA\",\n\t\t\t\t\"-EncodedCommand\",\n\t\t\t\tBuffer.from(script, \"utf16le\").toString(\"base64\"),\n\t\t\t],\n\t\t\t{ stdio: [\"pipe\", \"pipe\", \"ignore\"] },\n\t\t);\n\t\tlet resolveReady!: () => void;\n\t\tlet rejectReady!: (error: Error) => void;\n\t\tconst ready = new Promise<void>((resolve, reject) => {\n\t\t\tresolveReady = resolve;\n\t\t\trejectReady = reject;\n\t\t});\n\t\tlet initialized = false;\n\t\tlet closed = false;\n\t\tlet buffer = \"\";\n\t\tlet pending: { resolve(text: string): void; reject(error: Error): void; promise: Promise<string> } | undefined;\n\t\tconst kill = () => {\n\t\t\tchild.kill(\"SIGKILL\");\n\t\t};\n\t\tconst fail = (error = new Error(\"Clipboard reader stopped; use your terminal's Paste command\")) => {\n\t\t\tif (closed) return;\n\t\t\tclosed = true;\n\t\t\tclearTimeout(timer);\n\t\t\tbuffer = \"\";\n\t\t\trejectReady(error);\n\t\t\tpending?.reject(error);\n\t\t\tpending = undefined;\n\t\t\tif (worker === instance) worker = undefined;\n\t\t\tkill();\n\t\t};\n\t\tlet timer = setTimeout(\n\t\t\t() => fail(new Error(\"Clipboard reader startup timed out; use your terminal's Paste command\")),\n\t\t\t5000,\n\t\t);\n\t\tconst instance = {\n\t\t\tready,\n\t\t\tclose: fail,\n\t\t\tread(): Promise<string> {\n\t\t\t\tif (isRemoteSession())\n\t\t\t\t\treturn Promise.reject(new Error(\"Use your terminal's Paste command in remote sessions\"));\n\t\t\t\tif (closed || !initialized) return Promise.reject(new Error(\"Clipboard reader is not ready\"));\n\t\t\t\tif (pending) return pending.promise;\n\t\t\t\tlet resolve!: (text: string) => void;\n\t\t\t\tlet reject!: (error: Error) => void;\n\t\t\t\tconst promise = new Promise<string>((done, error) => {\n\t\t\t\t\tresolve = done;\n\t\t\t\t\treject = error;\n\t\t\t\t});\n\t\t\t\tpending = { resolve, reject, promise };\n\t\t\t\ttimer = setTimeout(\n\t\t\t\t\t() => fail(new Error(\"Clipboard read timed out; use your terminal's Paste command\")),\n\t\t\t\t\t5000,\n\t\t\t\t);\n\t\t\t\tchild.stdin.write(\"read\\n\", (error) => {\n\t\t\t\t\tif (error) fail(new Error(\"Clipboard reader input failed\"));\n\t\t\t\t});\n\t\t\t\treturn promise;\n\t\t\t},\n\t\t};\n\t\tprocess.once(\"exit\", kill);\n\t\tchild.once(\"spawn\", () => {\n\t\t\tif (closed) kill();\n\t\t});\n\t\tvoid waitForChildProcess(child)\n\t\t\t.then(\n\t\t\t\t() => fail(),\n\t\t\t\t() => fail(new Error(\"Cannot start clipboard reader; use your terminal's Paste command\")),\n\t\t\t)\n\t\t\t.finally(() => process.off(\"exit\", kill));\n\t\tchild.stdin.on(\"error\", () => fail(new Error(\"Clipboard reader input failed\")));\n\t\tchild.stdout.on(\"error\", () => fail(new Error(\"Clipboard reader output failed\")));\n\t\tchild.stdout.on(\"data\", (chunk: Buffer) => {\n\t\t\tif (closed) return;\n\t\t\tif (buffer.length + chunk.length > (initialized ? maxWireBytes : 7)) {\n\t\t\t\tfail(new Error(\"Clipboard response exceeded its size limit\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tbuffer += chunk.toString(\"utf8\");\n\t\t\tif (!buffer.includes(\"\\n\")) return;\n\t\t\tconst response = buffer;\n\t\t\tbuffer = \"\";\n\t\t\tif (!initialized && /^READY\\r?\\n$/.test(response)) {\n\t\t\t\tinitialized = true;\n\t\t\t\tclearTimeout(timer);\n\t\t\t\tresolveReady();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst match = /^OK:([A-Za-z0-9+/]*={0,2})\\r?\\n$/.exec(response);\n\t\t\tif (!pending || !match || match[1].length % 4 !== 0) {\n\t\t\t\tfail(new Error(\"Cannot read clipboard text; use your terminal's Paste command\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst bytes = Buffer.from(match[1], \"base64\");\n\t\t\tif (bytes.length > maxBytes || bytes.toString(\"base64\") !== match[1]) {\n\t\t\t\tfail(new Error(\"Invalid or oversized clipboard response\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tclearTimeout(timer);\n\t\t\tconst request = pending;\n\t\t\tpending = undefined;\n\t\t\trequest.resolve(bytes.toString(\"utf8\"));\n\t\t});\n\t\treturn instance;\n\t}\n\treturn {\n\t\tstart(): Promise<void> {\n\t\t\tif (isRemoteSession())\n\t\t\t\treturn Promise.reject(new Error(\"Use your terminal's Paste command in remote sessions\"));\n\t\t\ttry {\n\t\t\t\tworker ??= launch();\n\t\t\t\treturn worker.ready;\n\t\t\t} catch {\n\t\t\t\treturn Promise.reject(new Error(\"Cannot start clipboard reader; use your terminal's Paste command\"));\n\t\t\t}\n\t\t},\n\t\tread(): Promise<string> {\n\t\t\treturn worker ? worker.read() : Promise.reject(new Error(\"Clipboard reader stopped; try pasting again\"));\n\t\t},\n\t\tclose(): void {\n\t\t\tworker?.close();\n\t\t},\n\t};\n}\n\nexport async function copyToClipboard(text: string): Promise<void> {\n\tlet copied = false;\n\n\tconst p = platform();\n\n\t// Prefer direct clipboard writes. Emitting OSC 52 first can make terminals\n\t// write the same native clipboard concurrently with the addon, and very large\n\t// OSC 52 payloads can desynchronize terminal rendering.\n\t//\n\t// On Linux, skip the native addon. The underlying `clipboard-rs` crate is\n\t// X11-only and does not retain selection ownership after `set_text`\n\t// resolves, so on Wayland-only compositors (Hyprland, Niri, ...) and even\n\t// some X11 sessions the call resolves successfully without populating the\n\t// clipboard. The platform tools below (wl-copy, xclip, xsel) properly\n\t// daemonize and keep ownership.\n\ttry {\n\t\tif (clipboard && p !== \"linux\") {\n\t\t\tawait clipboard.setText(text);\n\t\t\tcopied = true;\n\t\t}\n\t} catch {\n\t\t// Fall through to platform-specific clipboard tools.\n\t}\n\n\tconst remote = isRemoteSession();\n\tif (copied && !remote) {\n\t\treturn;\n\t}\n\n\tconst options: NativeClipboardExecOptions = { input: text, timeout: 5000, stdio: [\"pipe\", \"ignore\", \"ignore\"] };\n\n\tif (!copied) {\n\t\ttry {\n\t\t\tif (p === \"darwin\") {\n\t\t\t\texecSync(\"pbcopy\", options);\n\t\t\t\tcopied = true;\n\t\t\t} else if (p === \"win32\") {\n\t\t\t\texecSync(\"clip\", options);\n\t\t\t\tcopied = true;\n\t\t\t} else {\n\t\t\t\t// Linux. Try Termux, Wayland, or X11 clipboard tools.\n\t\t\t\tif (process.env.TERMUX_VERSION) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\texecSync(\"termux-clipboard-set\", options);\n\t\t\t\t\t\tcopied = true;\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Fall back to Wayland or X11 tools.\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (!copied) {\n\t\t\t\t\tconst hasWaylandDisplay = Boolean(process.env.WAYLAND_DISPLAY);\n\t\t\t\t\tconst hasX11Display = Boolean(process.env.DISPLAY);\n\t\t\t\t\tconst isWayland = isWaylandSession();\n\t\t\t\t\tif (isWayland && hasWaylandDisplay) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t// SCRAMJET-DIVERGENCE: await backend acceptance without execSync's daemon/fork hang.\n\t\t\t\t\t\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\t\t\t\t\t\tconst proc = spawn(\"wl-copy\", [], {\n\t\t\t\t\t\t\t\t\tstdio: [\"pipe\", \"ignore\", \"ignore\"],\n\t\t\t\t\t\t\t\t\ttimeout: options.timeout,\n\t\t\t\t\t\t\t\t\tkillSignal: \"SIGKILL\",\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\tlet inputFinished = false;\n\t\t\t\t\t\t\t\tlet exited = false;\n\t\t\t\t\t\t\t\tproc.on(\"error\", reject);\n\t\t\t\t\t\t\t\tproc.stdin.on(\"error\", (error) => {\n\t\t\t\t\t\t\t\t\tproc.kill(\"SIGKILL\");\n\t\t\t\t\t\t\t\t\treject(error);\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\tproc.once(\"exit\", (code) => {\n\t\t\t\t\t\t\t\t\tif (code !== 0) {\n\t\t\t\t\t\t\t\t\t\treject(new Error(\"wl-copy did not accept clipboard content\"));\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\texited = true;\n\t\t\t\t\t\t\t\t\tif (inputFinished) resolve();\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\tproc.stdin.end(text, (error?: Error | null) => {\n\t\t\t\t\t\t\t\t\tif (error) {\n\t\t\t\t\t\t\t\t\t\treject(error);\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tinputFinished = true;\n\t\t\t\t\t\t\t\t\tif (exited) resolve();\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tcopied = true;\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\tif (hasX11Display) {\n\t\t\t\t\t\t\t\tcopyToX11Clipboard(options);\n\t\t\t\t\t\t\t\tcopied = true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (hasX11Display) {\n\t\t\t\t\t\tcopyToX11Clipboard(options);\n\t\t\t\t\t\tcopied = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// Fall through to OSC 52 fallback.\n\t\t}\n\t}\n\n\tif (remote || !copied) {\n\t\tconst osc52Copied = emitOsc52(text);\n\t\tcopied = copied || osc52Copied;\n\t}\n\n\tif (!copied) {\n\t\tthrow new Error(\"Failed to copy to clipboard\");\n\t}\n}\n"]}
@@ -1,5 +1,6 @@
1
- import { execSync, spawn } from "child_process";
1
+ import { execFile, execSync, spawn } from "child_process";
2
2
  import { platform } from "os";
3
+ import { waitForChildProcess } from "./child-process.js";
3
4
  import { isWaylandSession } from "./clipboard-image.js";
4
5
  import { clipboard } from "./clipboard-native.js";
5
6
  function copyToX11Clipboard(options) {
@@ -22,6 +23,250 @@ function emitOsc52(text) {
22
23
  process.stdout.write(`\x1b]52;c;${encoded}\x07`);
23
24
  return true;
24
25
  }
26
+ // SCRAMJET-DIVERGENCE: mouse reporting replaces the terminal's editor right-click paste.
27
+ export async function readClipboardText() {
28
+ if (isRemoteSession())
29
+ throw new Error("Use your terminal's Paste command in remote sessions");
30
+ const p = platform();
31
+ const wsl = p === "linux" && Boolean(process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP);
32
+ if (clipboard && !wsl && p !== "linux") {
33
+ try {
34
+ return await clipboard.getText();
35
+ }
36
+ catch {
37
+ // The platform command remains available if the optional addon fails.
38
+ }
39
+ }
40
+ const commands = wsl || p === "win32"
41
+ ? [
42
+ [
43
+ "powershell.exe",
44
+ [
45
+ "-NoProfile",
46
+ "-NonInteractive",
47
+ "-STA",
48
+ "-Command",
49
+ "Add-Type -AssemblyName System.Windows.Forms; [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false); [Console]::Write([System.Windows.Forms.Clipboard]::GetText())",
50
+ ],
51
+ ],
52
+ ]
53
+ : p === "darwin"
54
+ ? [["pbpaste", []]]
55
+ : process.env.TERMUX_VERSION
56
+ ? [["termux-clipboard-get", []]]
57
+ : [
58
+ ...(process.env.WAYLAND_DISPLAY
59
+ ? [["wl-paste", ["--no-newline", "--type", "text"]]]
60
+ : []),
61
+ ...(process.env.DISPLAY
62
+ ? [
63
+ ["xclip", ["-selection", "clipboard", "-o"]],
64
+ ["xsel", ["--clipboard", "--output"]],
65
+ ]
66
+ : []),
67
+ ];
68
+ for (const [command, args] of commands) {
69
+ try {
70
+ return await new Promise((resolve, reject) => {
71
+ execFile(command, args, { encoding: "utf8", timeout: 5000, maxBuffer: 10 * 1024 * 1024, killSignal: "SIGKILL" }, (error, stdout) => {
72
+ if (error)
73
+ reject(error);
74
+ else
75
+ resolve(stdout);
76
+ });
77
+ });
78
+ }
79
+ catch {
80
+ // Try the next local backend without requesting remote clipboard access.
81
+ }
82
+ }
83
+ throw new Error("Cannot read clipboard text; use your terminal's Paste command");
84
+ }
85
+ // SCRAMJET-DIVERGENCE: prewarm WSL interop without polling or caching clipboard contents.
86
+ export function createWslClipboardReader() {
87
+ if (platform() !== "linux" || !(process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) || isRemoteSession())
88
+ return undefined;
89
+ const maxBytes = 10 * 1024 * 1024;
90
+ const maxWireBytes = 4 * Math.ceil(maxBytes / 3) + 6;
91
+ const script = `
92
+ $ErrorActionPreference = 'Stop'
93
+ Add-Type @'
94
+ using System;
95
+ using System.Runtime.InteropServices;
96
+ public static class ScramjetClipboard {
97
+ [DllImport("user32.dll")] static extern bool OpenClipboard(IntPtr owner);
98
+ [DllImport("user32.dll")] static extern bool CloseClipboard();
99
+ [DllImport("user32.dll")] static extern bool IsClipboardFormatAvailable(uint format);
100
+ [DllImport("user32.dll")] static extern IntPtr GetClipboardData(uint format);
101
+ [DllImport("kernel32.dll")] static extern IntPtr GlobalLock(IntPtr handle);
102
+ [DllImport("kernel32.dll")] static extern bool GlobalUnlock(IntPtr handle);
103
+ [DllImport("kernel32.dll")] static extern UIntPtr GlobalSize(IntPtr handle);
104
+ public static string ReadText() {
105
+ for (int attempt = 0; !OpenClipboard(IntPtr.Zero); attempt++) {
106
+ if (attempt == 9) throw new InvalidOperationException("Clipboard unavailable");
107
+ System.Threading.Thread.Sleep(5);
108
+ }
109
+ try {
110
+ if (!IsClipboardFormatAvailable(13)) return "";
111
+ IntPtr handle = GetClipboardData(13);
112
+ if (handle == IntPtr.Zero) throw new InvalidOperationException("Clipboard data unavailable");
113
+ ulong size = GlobalSize(handle).ToUInt64();
114
+ if (size < 2 || size > ${maxBytes * 2 + 2} || size % 2 != 0) throw new InvalidOperationException("Invalid clipboard size");
115
+ IntPtr data = GlobalLock(handle);
116
+ if (data == IntPtr.Zero) throw new InvalidOperationException("Clipboard data unavailable");
117
+ try {
118
+ string value = Marshal.PtrToStringUni(data, (int)(size / 2));
119
+ int end = value.IndexOf('\\0');
120
+ if (end < 0) throw new InvalidOperationException("Unterminated clipboard text");
121
+ return value.Substring(0, end);
122
+ } finally { GlobalUnlock(handle); }
123
+ } finally { CloseClipboard(); }
124
+ }
125
+ }
126
+ '@
127
+ [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
128
+ [Console]::Out.WriteLine('READY'); [Console]::Out.Flush()
129
+ while ($null -ne ($request = [Console]::In.ReadLine())) {
130
+ if ($request -ne 'read') { break }
131
+ try {
132
+ $text = [ScramjetClipboard]::ReadText()
133
+ if ([System.Text.Encoding]::UTF8.GetByteCount($text) -gt ${maxBytes}) {
134
+ [Console]::Out.WriteLine('ERR:size')
135
+ } else {
136
+ $bytes = [System.Text.Encoding]::UTF8.GetBytes($text)
137
+ [Console]::Out.WriteLine('OK:' + [Convert]::ToBase64String($bytes))
138
+ }
139
+ } catch { [Console]::Out.WriteLine('ERR:read') }
140
+ finally { $text = $null; $bytes = $null }
141
+ [Console]::Out.Flush()
142
+ }`;
143
+ let worker;
144
+ function launch() {
145
+ const child = spawn("powershell.exe", [
146
+ "-NoLogo",
147
+ "-NoProfile",
148
+ "-NonInteractive",
149
+ "-STA",
150
+ "-EncodedCommand",
151
+ Buffer.from(script, "utf16le").toString("base64"),
152
+ ], { stdio: ["pipe", "pipe", "ignore"] });
153
+ let resolveReady;
154
+ let rejectReady;
155
+ const ready = new Promise((resolve, reject) => {
156
+ resolveReady = resolve;
157
+ rejectReady = reject;
158
+ });
159
+ let initialized = false;
160
+ let closed = false;
161
+ let buffer = "";
162
+ let pending;
163
+ const kill = () => {
164
+ child.kill("SIGKILL");
165
+ };
166
+ const fail = (error = new Error("Clipboard reader stopped; use your terminal's Paste command")) => {
167
+ if (closed)
168
+ return;
169
+ closed = true;
170
+ clearTimeout(timer);
171
+ buffer = "";
172
+ rejectReady(error);
173
+ pending?.reject(error);
174
+ pending = undefined;
175
+ if (worker === instance)
176
+ worker = undefined;
177
+ kill();
178
+ };
179
+ let timer = setTimeout(() => fail(new Error("Clipboard reader startup timed out; use your terminal's Paste command")), 5000);
180
+ const instance = {
181
+ ready,
182
+ close: fail,
183
+ read() {
184
+ if (isRemoteSession())
185
+ return Promise.reject(new Error("Use your terminal's Paste command in remote sessions"));
186
+ if (closed || !initialized)
187
+ return Promise.reject(new Error("Clipboard reader is not ready"));
188
+ if (pending)
189
+ return pending.promise;
190
+ let resolve;
191
+ let reject;
192
+ const promise = new Promise((done, error) => {
193
+ resolve = done;
194
+ reject = error;
195
+ });
196
+ pending = { resolve, reject, promise };
197
+ timer = setTimeout(() => fail(new Error("Clipboard read timed out; use your terminal's Paste command")), 5000);
198
+ child.stdin.write("read\n", (error) => {
199
+ if (error)
200
+ fail(new Error("Clipboard reader input failed"));
201
+ });
202
+ return promise;
203
+ },
204
+ };
205
+ process.once("exit", kill);
206
+ child.once("spawn", () => {
207
+ if (closed)
208
+ kill();
209
+ });
210
+ void waitForChildProcess(child)
211
+ .then(() => fail(), () => fail(new Error("Cannot start clipboard reader; use your terminal's Paste command")))
212
+ .finally(() => process.off("exit", kill));
213
+ child.stdin.on("error", () => fail(new Error("Clipboard reader input failed")));
214
+ child.stdout.on("error", () => fail(new Error("Clipboard reader output failed")));
215
+ child.stdout.on("data", (chunk) => {
216
+ if (closed)
217
+ return;
218
+ if (buffer.length + chunk.length > (initialized ? maxWireBytes : 7)) {
219
+ fail(new Error("Clipboard response exceeded its size limit"));
220
+ return;
221
+ }
222
+ buffer += chunk.toString("utf8");
223
+ if (!buffer.includes("\n"))
224
+ return;
225
+ const response = buffer;
226
+ buffer = "";
227
+ if (!initialized && /^READY\r?\n$/.test(response)) {
228
+ initialized = true;
229
+ clearTimeout(timer);
230
+ resolveReady();
231
+ return;
232
+ }
233
+ const match = /^OK:([A-Za-z0-9+/]*={0,2})\r?\n$/.exec(response);
234
+ if (!pending || !match || match[1].length % 4 !== 0) {
235
+ fail(new Error("Cannot read clipboard text; use your terminal's Paste command"));
236
+ return;
237
+ }
238
+ const bytes = Buffer.from(match[1], "base64");
239
+ if (bytes.length > maxBytes || bytes.toString("base64") !== match[1]) {
240
+ fail(new Error("Invalid or oversized clipboard response"));
241
+ return;
242
+ }
243
+ clearTimeout(timer);
244
+ const request = pending;
245
+ pending = undefined;
246
+ request.resolve(bytes.toString("utf8"));
247
+ });
248
+ return instance;
249
+ }
250
+ return {
251
+ start() {
252
+ if (isRemoteSession())
253
+ return Promise.reject(new Error("Use your terminal's Paste command in remote sessions"));
254
+ try {
255
+ worker ??= launch();
256
+ return worker.ready;
257
+ }
258
+ catch {
259
+ return Promise.reject(new Error("Cannot start clipboard reader; use your terminal's Paste command"));
260
+ }
261
+ },
262
+ read() {
263
+ return worker ? worker.read() : Promise.reject(new Error("Clipboard reader stopped; try pasting again"));
264
+ },
265
+ close() {
266
+ worker?.close();
267
+ },
268
+ };
269
+ }
25
270
  export async function copyToClipboard(text) {
26
271
  let copied = false;
27
272
  const p = platform();
@@ -76,16 +321,39 @@ export async function copyToClipboard(text) {
76
321
  const isWayland = isWaylandSession();
77
322
  if (isWayland && hasWaylandDisplay) {
78
323
  try {
79
- // Verify wl-copy exists (spawn errors are async and won't be caught)
80
- execSync("which wl-copy", { stdio: "ignore" });
81
- // wl-copy with execSync hangs due to fork behavior; use spawn instead
82
- const proc = spawn("wl-copy", [], { stdio: ["pipe", "ignore", "ignore"] });
83
- proc.stdin.on("error", () => {
84
- // Ignore EPIPE errors if wl-copy exits early
324
+ // SCRAMJET-DIVERGENCE: await backend acceptance without execSync's daemon/fork hang.
325
+ await new Promise((resolve, reject) => {
326
+ const proc = spawn("wl-copy", [], {
327
+ stdio: ["pipe", "ignore", "ignore"],
328
+ timeout: options.timeout,
329
+ killSignal: "SIGKILL",
330
+ });
331
+ let inputFinished = false;
332
+ let exited = false;
333
+ proc.on("error", reject);
334
+ proc.stdin.on("error", (error) => {
335
+ proc.kill("SIGKILL");
336
+ reject(error);
337
+ });
338
+ proc.once("exit", (code) => {
339
+ if (code !== 0) {
340
+ reject(new Error("wl-copy did not accept clipboard content"));
341
+ return;
342
+ }
343
+ exited = true;
344
+ if (inputFinished)
345
+ resolve();
346
+ });
347
+ proc.stdin.end(text, (error) => {
348
+ if (error) {
349
+ reject(error);
350
+ return;
351
+ }
352
+ inputFinished = true;
353
+ if (exited)
354
+ resolve();
355
+ });
85
356
  });
86
- proc.stdin.write(text);
87
- proc.stdin.end();
88
- proc.unref();
89
357
  copied = true;
90
358
  }
91
359
  catch {
@@ -1 +1 @@
1
- {"version":3,"file":"clipboard.js","sourceRoot":"","sources":["../../src/utils/clipboard.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync, spawn } from \"child_process\";\nimport { platform } from \"os\";\nimport { isWaylandSession } from \"./clipboard-image.js\";\nimport { clipboard } from \"./clipboard-native.js\";\n\ntype NativeClipboardExecOptions = {\n\tinput: string;\n\ttimeout: number;\n\tstdio: [\"pipe\", \"ignore\", \"ignore\"];\n};\n\nfunction copyToX11Clipboard(options: NativeClipboardExecOptions): void {\n\ttry {\n\t\texecSync(\"xclip -selection clipboard\", options);\n\t} catch {\n\t\texecSync(\"xsel --clipboard --input\", options);\n\t}\n}\n\nconst MAX_OSC52_ENCODED_LENGTH = 100_000;\n\nfunction isRemoteSession(env: NodeJS.ProcessEnv = process.env): boolean {\n\treturn Boolean(env.SSH_CONNECTION || env.SSH_CLIENT || env.MOSH_CONNECTION);\n}\n\nfunction emitOsc52(text: string): boolean {\n\tconst encoded = Buffer.from(text).toString(\"base64\");\n\tif (encoded.length > MAX_OSC52_ENCODED_LENGTH) {\n\t\treturn false;\n\t}\n\tprocess.stdout.write(`\\x1b]52;c;${encoded}\\x07`);\n\treturn true;\n}\n\nexport async function copyToClipboard(text: string): Promise<void> {\n\tlet copied = false;\n\n\tconst p = platform();\n\n\t// Prefer direct clipboard writes. Emitting OSC 52 first can make terminals\n\t// write the same native clipboard concurrently with the addon, and very large\n\t// OSC 52 payloads can desynchronize terminal rendering.\n\t//\n\t// On Linux, skip the native addon. The underlying `clipboard-rs` crate is\n\t// X11-only and does not retain selection ownership after `set_text`\n\t// resolves, so on Wayland-only compositors (Hyprland, Niri, ...) and even\n\t// some X11 sessions the call resolves successfully without populating the\n\t// clipboard. The platform tools below (wl-copy, xclip, xsel) properly\n\t// daemonize and keep ownership.\n\ttry {\n\t\tif (clipboard && p !== \"linux\") {\n\t\t\tawait clipboard.setText(text);\n\t\t\tcopied = true;\n\t\t}\n\t} catch {\n\t\t// Fall through to platform-specific clipboard tools.\n\t}\n\n\tconst remote = isRemoteSession();\n\tif (copied && !remote) {\n\t\treturn;\n\t}\n\n\tconst options: NativeClipboardExecOptions = { input: text, timeout: 5000, stdio: [\"pipe\", \"ignore\", \"ignore\"] };\n\n\tif (!copied) {\n\t\ttry {\n\t\t\tif (p === \"darwin\") {\n\t\t\t\texecSync(\"pbcopy\", options);\n\t\t\t\tcopied = true;\n\t\t\t} else if (p === \"win32\") {\n\t\t\t\texecSync(\"clip\", options);\n\t\t\t\tcopied = true;\n\t\t\t} else {\n\t\t\t\t// Linux. Try Termux, Wayland, or X11 clipboard tools.\n\t\t\t\tif (process.env.TERMUX_VERSION) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\texecSync(\"termux-clipboard-set\", options);\n\t\t\t\t\t\tcopied = true;\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Fall back to Wayland or X11 tools.\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (!copied) {\n\t\t\t\t\tconst hasWaylandDisplay = Boolean(process.env.WAYLAND_DISPLAY);\n\t\t\t\t\tconst hasX11Display = Boolean(process.env.DISPLAY);\n\t\t\t\t\tconst isWayland = isWaylandSession();\n\t\t\t\t\tif (isWayland && hasWaylandDisplay) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t// Verify wl-copy exists (spawn errors are async and won't be caught)\n\t\t\t\t\t\t\texecSync(\"which wl-copy\", { stdio: \"ignore\" });\n\t\t\t\t\t\t\t// wl-copy with execSync hangs due to fork behavior; use spawn instead\n\t\t\t\t\t\t\tconst proc = spawn(\"wl-copy\", [], { stdio: [\"pipe\", \"ignore\", \"ignore\"] });\n\t\t\t\t\t\t\tproc.stdin.on(\"error\", () => {\n\t\t\t\t\t\t\t\t// Ignore EPIPE errors if wl-copy exits early\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tproc.stdin.write(text);\n\t\t\t\t\t\t\tproc.stdin.end();\n\t\t\t\t\t\t\tproc.unref();\n\t\t\t\t\t\t\tcopied = true;\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\tif (hasX11Display) {\n\t\t\t\t\t\t\t\tcopyToX11Clipboard(options);\n\t\t\t\t\t\t\t\tcopied = true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (hasX11Display) {\n\t\t\t\t\t\tcopyToX11Clipboard(options);\n\t\t\t\t\t\tcopied = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// Fall through to OSC 52 fallback.\n\t\t}\n\t}\n\n\tif (remote || !copied) {\n\t\tconst osc52Copied = emitOsc52(text);\n\t\tcopied = copied || osc52Copied;\n\t}\n\n\tif (!copied) {\n\t\tthrow new Error(\"Failed to copy to clipboard\");\n\t}\n}\n"]}
1
+ {"version":3,"file":"clipboard.js","sourceRoot":"","sources":["../../src/utils/clipboard.ts"],"names":[],"mappings":"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","sourcesContent":["import { execFile, execSync, spawn } from \"child_process\";\nimport { platform } from \"os\";\nimport { waitForChildProcess } from \"./child-process.js\";\nimport { isWaylandSession } from \"./clipboard-image.js\";\nimport { clipboard } from \"./clipboard-native.js\";\n\ntype NativeClipboardExecOptions = {\n\tinput: string;\n\ttimeout: number;\n\tstdio: [\"pipe\", \"ignore\", \"ignore\"];\n};\n\nfunction copyToX11Clipboard(options: NativeClipboardExecOptions): void {\n\ttry {\n\t\texecSync(\"xclip -selection clipboard\", options);\n\t} catch {\n\t\texecSync(\"xsel --clipboard --input\", options);\n\t}\n}\n\nconst MAX_OSC52_ENCODED_LENGTH = 100_000;\n\nfunction isRemoteSession(env: NodeJS.ProcessEnv = process.env): boolean {\n\treturn Boolean(env.SSH_CONNECTION || env.SSH_CLIENT || env.MOSH_CONNECTION);\n}\n\nfunction emitOsc52(text: string): boolean {\n\tconst encoded = Buffer.from(text).toString(\"base64\");\n\tif (encoded.length > MAX_OSC52_ENCODED_LENGTH) {\n\t\treturn false;\n\t}\n\tprocess.stdout.write(`\\x1b]52;c;${encoded}\\x07`);\n\treturn true;\n}\n\n// SCRAMJET-DIVERGENCE: mouse reporting replaces the terminal's editor right-click paste.\nexport async function readClipboardText(): Promise<string> {\n\tif (isRemoteSession()) throw new Error(\"Use your terminal's Paste command in remote sessions\");\n\tconst p = platform();\n\tconst wsl = p === \"linux\" && Boolean(process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP);\n\tif (clipboard && !wsl && p !== \"linux\") {\n\t\ttry {\n\t\t\treturn await clipboard.getText();\n\t\t} catch {\n\t\t\t// The platform command remains available if the optional addon fails.\n\t\t}\n\t}\n\tconst commands: [string, string[]][] =\n\t\twsl || p === \"win32\"\n\t\t\t? [\n\t\t\t\t\t[\n\t\t\t\t\t\t\"powershell.exe\",\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\"-NoProfile\",\n\t\t\t\t\t\t\t\"-NonInteractive\",\n\t\t\t\t\t\t\t\"-STA\",\n\t\t\t\t\t\t\t\"-Command\",\n\t\t\t\t\t\t\t\"Add-Type -AssemblyName System.Windows.Forms; [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false); [Console]::Write([System.Windows.Forms.Clipboard]::GetText())\",\n\t\t\t\t\t\t],\n\t\t\t\t\t],\n\t\t\t\t]\n\t\t\t: p === \"darwin\"\n\t\t\t\t? [[\"pbpaste\", []]]\n\t\t\t\t: process.env.TERMUX_VERSION\n\t\t\t\t\t? [[\"termux-clipboard-get\", []]]\n\t\t\t\t\t: [\n\t\t\t\t\t\t\t...(process.env.WAYLAND_DISPLAY\n\t\t\t\t\t\t\t\t? [[\"wl-paste\", [\"--no-newline\", \"--type\", \"text\"]] as [string, string[]]]\n\t\t\t\t\t\t\t\t: []),\n\t\t\t\t\t\t\t...(process.env.DISPLAY\n\t\t\t\t\t\t\t\t? ([\n\t\t\t\t\t\t\t\t\t\t[\"xclip\", [\"-selection\", \"clipboard\", \"-o\"]],\n\t\t\t\t\t\t\t\t\t\t[\"xsel\", [\"--clipboard\", \"--output\"]],\n\t\t\t\t\t\t\t\t\t] as [string, string[]][])\n\t\t\t\t\t\t\t\t: []),\n\t\t\t\t\t\t];\n\tfor (const [command, args] of commands) {\n\t\ttry {\n\t\t\treturn await new Promise<string>((resolve, reject) => {\n\t\t\t\texecFile(\n\t\t\t\t\tcommand,\n\t\t\t\t\targs,\n\t\t\t\t\t{ encoding: \"utf8\", timeout: 5000, maxBuffer: 10 * 1024 * 1024, killSignal: \"SIGKILL\" },\n\t\t\t\t\t(error, stdout) => {\n\t\t\t\t\t\tif (error) reject(error);\n\t\t\t\t\t\telse resolve(stdout);\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t});\n\t\t} catch {\n\t\t\t// Try the next local backend without requesting remote clipboard access.\n\t\t}\n\t}\n\tthrow new Error(\"Cannot read clipboard text; use your terminal's Paste command\");\n}\n\n// SCRAMJET-DIVERGENCE: prewarm WSL interop without polling or caching clipboard contents.\nexport function createWslClipboardReader() {\n\tif (platform() !== \"linux\" || !(process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) || isRemoteSession())\n\t\treturn undefined;\n\tconst maxBytes = 10 * 1024 * 1024;\n\tconst maxWireBytes = 4 * Math.ceil(maxBytes / 3) + 6;\n\tconst script = `\n$ErrorActionPreference = 'Stop'\nAdd-Type @'\nusing System;\nusing System.Runtime.InteropServices;\npublic static class ScramjetClipboard {\n [DllImport(\"user32.dll\")] static extern bool OpenClipboard(IntPtr owner);\n [DllImport(\"user32.dll\")] static extern bool CloseClipboard();\n [DllImport(\"user32.dll\")] static extern bool IsClipboardFormatAvailable(uint format);\n [DllImport(\"user32.dll\")] static extern IntPtr GetClipboardData(uint format);\n [DllImport(\"kernel32.dll\")] static extern IntPtr GlobalLock(IntPtr handle);\n [DllImport(\"kernel32.dll\")] static extern bool GlobalUnlock(IntPtr handle);\n [DllImport(\"kernel32.dll\")] static extern UIntPtr GlobalSize(IntPtr handle);\n public static string ReadText() {\n for (int attempt = 0; !OpenClipboard(IntPtr.Zero); attempt++) {\n if (attempt == 9) throw new InvalidOperationException(\"Clipboard unavailable\");\n System.Threading.Thread.Sleep(5);\n }\n try {\n if (!IsClipboardFormatAvailable(13)) return \"\";\n IntPtr handle = GetClipboardData(13);\n if (handle == IntPtr.Zero) throw new InvalidOperationException(\"Clipboard data unavailable\");\n ulong size = GlobalSize(handle).ToUInt64();\n if (size < 2 || size > ${maxBytes * 2 + 2} || size % 2 != 0) throw new InvalidOperationException(\"Invalid clipboard size\");\n IntPtr data = GlobalLock(handle);\n if (data == IntPtr.Zero) throw new InvalidOperationException(\"Clipboard data unavailable\");\n try {\n string value = Marshal.PtrToStringUni(data, (int)(size / 2));\n int end = value.IndexOf('\\\\0');\n if (end < 0) throw new InvalidOperationException(\"Unterminated clipboard text\");\n return value.Substring(0, end);\n } finally { GlobalUnlock(handle); }\n } finally { CloseClipboard(); }\n }\n}\n'@\n[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)\n[Console]::Out.WriteLine('READY'); [Console]::Out.Flush()\nwhile ($null -ne ($request = [Console]::In.ReadLine())) {\n if ($request -ne 'read') { break }\n try {\n $text = [ScramjetClipboard]::ReadText()\n if ([System.Text.Encoding]::UTF8.GetByteCount($text) -gt ${maxBytes}) {\n [Console]::Out.WriteLine('ERR:size')\n } else {\n $bytes = [System.Text.Encoding]::UTF8.GetBytes($text)\n [Console]::Out.WriteLine('OK:' + [Convert]::ToBase64String($bytes))\n }\n } catch { [Console]::Out.WriteLine('ERR:read') }\n finally { $text = $null; $bytes = $null }\n [Console]::Out.Flush()\n}`;\n\tlet worker: ReturnType<typeof launch> | undefined;\n\tfunction launch() {\n\t\tconst child = spawn(\n\t\t\t\"powershell.exe\",\n\t\t\t[\n\t\t\t\t\"-NoLogo\",\n\t\t\t\t\"-NoProfile\",\n\t\t\t\t\"-NonInteractive\",\n\t\t\t\t\"-STA\",\n\t\t\t\t\"-EncodedCommand\",\n\t\t\t\tBuffer.from(script, \"utf16le\").toString(\"base64\"),\n\t\t\t],\n\t\t\t{ stdio: [\"pipe\", \"pipe\", \"ignore\"] },\n\t\t);\n\t\tlet resolveReady!: () => void;\n\t\tlet rejectReady!: (error: Error) => void;\n\t\tconst ready = new Promise<void>((resolve, reject) => {\n\t\t\tresolveReady = resolve;\n\t\t\trejectReady = reject;\n\t\t});\n\t\tlet initialized = false;\n\t\tlet closed = false;\n\t\tlet buffer = \"\";\n\t\tlet pending: { resolve(text: string): void; reject(error: Error): void; promise: Promise<string> } | undefined;\n\t\tconst kill = () => {\n\t\t\tchild.kill(\"SIGKILL\");\n\t\t};\n\t\tconst fail = (error = new Error(\"Clipboard reader stopped; use your terminal's Paste command\")) => {\n\t\t\tif (closed) return;\n\t\t\tclosed = true;\n\t\t\tclearTimeout(timer);\n\t\t\tbuffer = \"\";\n\t\t\trejectReady(error);\n\t\t\tpending?.reject(error);\n\t\t\tpending = undefined;\n\t\t\tif (worker === instance) worker = undefined;\n\t\t\tkill();\n\t\t};\n\t\tlet timer = setTimeout(\n\t\t\t() => fail(new Error(\"Clipboard reader startup timed out; use your terminal's Paste command\")),\n\t\t\t5000,\n\t\t);\n\t\tconst instance = {\n\t\t\tready,\n\t\t\tclose: fail,\n\t\t\tread(): Promise<string> {\n\t\t\t\tif (isRemoteSession())\n\t\t\t\t\treturn Promise.reject(new Error(\"Use your terminal's Paste command in remote sessions\"));\n\t\t\t\tif (closed || !initialized) return Promise.reject(new Error(\"Clipboard reader is not ready\"));\n\t\t\t\tif (pending) return pending.promise;\n\t\t\t\tlet resolve!: (text: string) => void;\n\t\t\t\tlet reject!: (error: Error) => void;\n\t\t\t\tconst promise = new Promise<string>((done, error) => {\n\t\t\t\t\tresolve = done;\n\t\t\t\t\treject = error;\n\t\t\t\t});\n\t\t\t\tpending = { resolve, reject, promise };\n\t\t\t\ttimer = setTimeout(\n\t\t\t\t\t() => fail(new Error(\"Clipboard read timed out; use your terminal's Paste command\")),\n\t\t\t\t\t5000,\n\t\t\t\t);\n\t\t\t\tchild.stdin.write(\"read\\n\", (error) => {\n\t\t\t\t\tif (error) fail(new Error(\"Clipboard reader input failed\"));\n\t\t\t\t});\n\t\t\t\treturn promise;\n\t\t\t},\n\t\t};\n\t\tprocess.once(\"exit\", kill);\n\t\tchild.once(\"spawn\", () => {\n\t\t\tif (closed) kill();\n\t\t});\n\t\tvoid waitForChildProcess(child)\n\t\t\t.then(\n\t\t\t\t() => fail(),\n\t\t\t\t() => fail(new Error(\"Cannot start clipboard reader; use your terminal's Paste command\")),\n\t\t\t)\n\t\t\t.finally(() => process.off(\"exit\", kill));\n\t\tchild.stdin.on(\"error\", () => fail(new Error(\"Clipboard reader input failed\")));\n\t\tchild.stdout.on(\"error\", () => fail(new Error(\"Clipboard reader output failed\")));\n\t\tchild.stdout.on(\"data\", (chunk: Buffer) => {\n\t\t\tif (closed) return;\n\t\t\tif (buffer.length + chunk.length > (initialized ? maxWireBytes : 7)) {\n\t\t\t\tfail(new Error(\"Clipboard response exceeded its size limit\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tbuffer += chunk.toString(\"utf8\");\n\t\t\tif (!buffer.includes(\"\\n\")) return;\n\t\t\tconst response = buffer;\n\t\t\tbuffer = \"\";\n\t\t\tif (!initialized && /^READY\\r?\\n$/.test(response)) {\n\t\t\t\tinitialized = true;\n\t\t\t\tclearTimeout(timer);\n\t\t\t\tresolveReady();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst match = /^OK:([A-Za-z0-9+/]*={0,2})\\r?\\n$/.exec(response);\n\t\t\tif (!pending || !match || match[1].length % 4 !== 0) {\n\t\t\t\tfail(new Error(\"Cannot read clipboard text; use your terminal's Paste command\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst bytes = Buffer.from(match[1], \"base64\");\n\t\t\tif (bytes.length > maxBytes || bytes.toString(\"base64\") !== match[1]) {\n\t\t\t\tfail(new Error(\"Invalid or oversized clipboard response\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tclearTimeout(timer);\n\t\t\tconst request = pending;\n\t\t\tpending = undefined;\n\t\t\trequest.resolve(bytes.toString(\"utf8\"));\n\t\t});\n\t\treturn instance;\n\t}\n\treturn {\n\t\tstart(): Promise<void> {\n\t\t\tif (isRemoteSession())\n\t\t\t\treturn Promise.reject(new Error(\"Use your terminal's Paste command in remote sessions\"));\n\t\t\ttry {\n\t\t\t\tworker ??= launch();\n\t\t\t\treturn worker.ready;\n\t\t\t} catch {\n\t\t\t\treturn Promise.reject(new Error(\"Cannot start clipboard reader; use your terminal's Paste command\"));\n\t\t\t}\n\t\t},\n\t\tread(): Promise<string> {\n\t\t\treturn worker ? worker.read() : Promise.reject(new Error(\"Clipboard reader stopped; try pasting again\"));\n\t\t},\n\t\tclose(): void {\n\t\t\tworker?.close();\n\t\t},\n\t};\n}\n\nexport async function copyToClipboard(text: string): Promise<void> {\n\tlet copied = false;\n\n\tconst p = platform();\n\n\t// Prefer direct clipboard writes. Emitting OSC 52 first can make terminals\n\t// write the same native clipboard concurrently with the addon, and very large\n\t// OSC 52 payloads can desynchronize terminal rendering.\n\t//\n\t// On Linux, skip the native addon. The underlying `clipboard-rs` crate is\n\t// X11-only and does not retain selection ownership after `set_text`\n\t// resolves, so on Wayland-only compositors (Hyprland, Niri, ...) and even\n\t// some X11 sessions the call resolves successfully without populating the\n\t// clipboard. The platform tools below (wl-copy, xclip, xsel) properly\n\t// daemonize and keep ownership.\n\ttry {\n\t\tif (clipboard && p !== \"linux\") {\n\t\t\tawait clipboard.setText(text);\n\t\t\tcopied = true;\n\t\t}\n\t} catch {\n\t\t// Fall through to platform-specific clipboard tools.\n\t}\n\n\tconst remote = isRemoteSession();\n\tif (copied && !remote) {\n\t\treturn;\n\t}\n\n\tconst options: NativeClipboardExecOptions = { input: text, timeout: 5000, stdio: [\"pipe\", \"ignore\", \"ignore\"] };\n\n\tif (!copied) {\n\t\ttry {\n\t\t\tif (p === \"darwin\") {\n\t\t\t\texecSync(\"pbcopy\", options);\n\t\t\t\tcopied = true;\n\t\t\t} else if (p === \"win32\") {\n\t\t\t\texecSync(\"clip\", options);\n\t\t\t\tcopied = true;\n\t\t\t} else {\n\t\t\t\t// Linux. Try Termux, Wayland, or X11 clipboard tools.\n\t\t\t\tif (process.env.TERMUX_VERSION) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\texecSync(\"termux-clipboard-set\", options);\n\t\t\t\t\t\tcopied = true;\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Fall back to Wayland or X11 tools.\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (!copied) {\n\t\t\t\t\tconst hasWaylandDisplay = Boolean(process.env.WAYLAND_DISPLAY);\n\t\t\t\t\tconst hasX11Display = Boolean(process.env.DISPLAY);\n\t\t\t\t\tconst isWayland = isWaylandSession();\n\t\t\t\t\tif (isWayland && hasWaylandDisplay) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t// SCRAMJET-DIVERGENCE: await backend acceptance without execSync's daemon/fork hang.\n\t\t\t\t\t\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\t\t\t\t\t\tconst proc = spawn(\"wl-copy\", [], {\n\t\t\t\t\t\t\t\t\tstdio: [\"pipe\", \"ignore\", \"ignore\"],\n\t\t\t\t\t\t\t\t\ttimeout: options.timeout,\n\t\t\t\t\t\t\t\t\tkillSignal: \"SIGKILL\",\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\tlet inputFinished = false;\n\t\t\t\t\t\t\t\tlet exited = false;\n\t\t\t\t\t\t\t\tproc.on(\"error\", reject);\n\t\t\t\t\t\t\t\tproc.stdin.on(\"error\", (error) => {\n\t\t\t\t\t\t\t\t\tproc.kill(\"SIGKILL\");\n\t\t\t\t\t\t\t\t\treject(error);\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\tproc.once(\"exit\", (code) => {\n\t\t\t\t\t\t\t\t\tif (code !== 0) {\n\t\t\t\t\t\t\t\t\t\treject(new Error(\"wl-copy did not accept clipboard content\"));\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\texited = true;\n\t\t\t\t\t\t\t\t\tif (inputFinished) resolve();\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\tproc.stdin.end(text, (error?: Error | null) => {\n\t\t\t\t\t\t\t\t\tif (error) {\n\t\t\t\t\t\t\t\t\t\treject(error);\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tinputFinished = true;\n\t\t\t\t\t\t\t\t\tif (exited) resolve();\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tcopied = true;\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\tif (hasX11Display) {\n\t\t\t\t\t\t\t\tcopyToX11Clipboard(options);\n\t\t\t\t\t\t\t\tcopied = true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (hasX11Display) {\n\t\t\t\t\t\tcopyToX11Clipboard(options);\n\t\t\t\t\t\tcopied = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// Fall through to OSC 52 fallback.\n\t\t}\n\t}\n\n\tif (remote || !copied) {\n\t\tconst osc52Copied = emitOsc52(text);\n\t\tcopied = copied || osc52Copied;\n\t}\n\n\tif (!copied) {\n\t\tthrow new Error(\"Failed to copy to clipboard\");\n\t}\n}\n"]}
@@ -1,3 +1,3 @@
1
1
  export declare function getToolPath(tool: "fd" | "rg"): string | null;
2
- export declare function ensureTool(tool: "fd" | "rg", silent?: boolean): Promise<string | undefined>;
2
+ export declare function ensureTool(tool: "fd" | "rg", silent?: boolean, report?: (message: string) => void): Promise<string | undefined>;
3
3
  //# sourceMappingURL=tools-manager.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tools-manager.d.ts","sourceRoot":"","sources":["../../src/utils/tools-manager.ts"],"names":[],"mappings":"AAqFA,wBAAgB,WAAW,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAmB5D;AA8ND,wBAAsB,UAAU,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,EAAE,MAAM,GAAE,OAAe,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CA2CxG","sourcesContent":["import chalk from \"chalk\";\nimport { type SpawnSyncReturns, spawnSync } from \"child_process\";\nimport { chmodSync, createWriteStream, existsSync, mkdirSync, readdirSync, renameSync, rmSync } from \"fs\";\nimport { arch, platform } from \"os\";\nimport { join } from \"path\";\nimport { Readable } from \"stream\";\nimport { pipeline } from \"stream/promises\";\nimport { APP_NAME, getBinDir } from \"../config.js\";\n\nconst TOOLS_DIR = getBinDir();\nconst NETWORK_TIMEOUT_MS = 10_000;\nconst DOWNLOAD_TIMEOUT_MS = 120_000;\n\n// SCRAMJET-DIVERGENCE: prefer SCRAMJET_OFFLINE, fall back to PI_OFFLINE.\nfunction isOfflineModeEnabled(): boolean {\n\tconst value = process.env.SCRAMJET_OFFLINE || process.env.PI_OFFLINE;\n\tif (!value) return false;\n\treturn value === \"1\" || value.toLowerCase() === \"true\" || value.toLowerCase() === \"yes\";\n}\n\ninterface ToolConfig {\n\tname: string;\n\trepo: string; // GitHub repo (e.g., \"sharkdp/fd\")\n\tbinaryName: string; // Name of the binary inside the archive\n\tsystemBinaryNames?: string[]; // Alternative system command names to try before downloading\n\ttagPrefix: string; // Prefix for tags (e.g., \"v\" for v1.0.0, \"\" for 1.0.0)\n\tgetAssetName: (version: string, plat: string, architecture: string) => string | null;\n}\n\nconst TOOLS: Record<string, ToolConfig> = {\n\tfd: {\n\t\tname: \"fd\",\n\t\trepo: \"sharkdp/fd\",\n\t\tbinaryName: \"fd\",\n\t\tsystemBinaryNames: [\"fd\", \"fdfind\"],\n\t\ttagPrefix: \"v\",\n\t\tgetAssetName: (version, plat, architecture) => {\n\t\t\tif (plat === \"darwin\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `fd-v${version}-${archStr}-apple-darwin.tar.gz`;\n\t\t\t} else if (plat === \"linux\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `fd-v${version}-${archStr}-unknown-linux-gnu.tar.gz`;\n\t\t\t} else if (plat === \"win32\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `fd-v${version}-${archStr}-pc-windows-msvc.zip`;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t},\n\trg: {\n\t\tname: \"ripgrep\",\n\t\trepo: \"BurntSushi/ripgrep\",\n\t\tbinaryName: \"rg\",\n\t\ttagPrefix: \"\",\n\t\tgetAssetName: (version, plat, architecture) => {\n\t\t\tif (plat === \"darwin\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `ripgrep-${version}-${archStr}-apple-darwin.tar.gz`;\n\t\t\t} else if (plat === \"linux\") {\n\t\t\t\tif (architecture === \"arm64\") {\n\t\t\t\t\treturn `ripgrep-${version}-aarch64-unknown-linux-gnu.tar.gz`;\n\t\t\t\t}\n\t\t\t\treturn `ripgrep-${version}-x86_64-unknown-linux-musl.tar.gz`;\n\t\t\t} else if (plat === \"win32\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `ripgrep-${version}-${archStr}-pc-windows-msvc.zip`;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t},\n};\n\n// Check if a command exists in PATH by trying to run it\nfunction commandExists(cmd: string): boolean {\n\ttry {\n\t\tconst result = spawnSync(cmd, [\"--version\"], { stdio: \"pipe\" });\n\t\t// Check for ENOENT error (command not found)\n\t\treturn result.error === undefined || result.error === null;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n// Get the path to a tool (system-wide or in our tools dir)\nexport function getToolPath(tool: \"fd\" | \"rg\"): string | null {\n\tconst config = TOOLS[tool];\n\tif (!config) return null;\n\n\t// Check our tools directory first\n\tconst localPath = join(TOOLS_DIR, config.binaryName + (platform() === \"win32\" ? \".exe\" : \"\"));\n\tif (existsSync(localPath)) {\n\t\treturn localPath;\n\t}\n\n\t// Check system PATH - if found, just return the command name (it's in PATH)\n\tconst systemBinaryNames = config.systemBinaryNames ?? [config.binaryName];\n\tfor (const systemBinaryName of systemBinaryNames) {\n\t\tif (commandExists(systemBinaryName)) {\n\t\t\treturn systemBinaryName;\n\t\t}\n\t}\n\n\treturn null;\n}\n\n// Fetch latest release version from GitHub\nasync function getLatestVersion(repo: string): Promise<string> {\n\tconst response = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, {\n\t\theaders: { \"User-Agent\": `${APP_NAME}-coding-agent` },\n\t\tsignal: AbortSignal.timeout(NETWORK_TIMEOUT_MS),\n\t});\n\n\tif (!response.ok) {\n\t\tthrow new Error(`GitHub API error: ${response.status}`);\n\t}\n\n\tconst data = (await response.json()) as { tag_name: string };\n\treturn data.tag_name.replace(/^v/, \"\");\n}\n\n// Download a file from URL\nasync function downloadFile(url: string, dest: string): Promise<void> {\n\tconst response = await fetch(url, {\n\t\tsignal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS),\n\t});\n\n\tif (!response.ok) {\n\t\tthrow new Error(`Failed to download: ${response.status}`);\n\t}\n\n\tif (!response.body) {\n\t\tthrow new Error(\"No response body\");\n\t}\n\n\tconst fileStream = createWriteStream(dest);\n\tawait pipeline(Readable.fromWeb(response.body as any), fileStream);\n}\n\nfunction findBinaryRecursively(rootDir: string, binaryFileName: string): string | null {\n\tconst stack: string[] = [rootDir];\n\n\twhile (stack.length > 0) {\n\t\tconst currentDir = stack.pop();\n\t\tif (!currentDir) continue;\n\n\t\tconst entries = readdirSync(currentDir, { withFileTypes: true });\n\t\tfor (const entry of entries) {\n\t\t\tconst fullPath = join(currentDir, entry.name);\n\t\t\tif (entry.isFile() && entry.name === binaryFileName) {\n\t\t\t\treturn fullPath;\n\t\t\t}\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\tstack.push(fullPath);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn null;\n}\n\nfunction formatSpawnFailure(result: SpawnSyncReturns<Buffer>): string {\n\tif (result.error?.message) {\n\t\treturn result.error.message;\n\t}\n\tconst stderr = result.stderr?.toString().trim();\n\tif (stderr) {\n\t\treturn stderr;\n\t}\n\tconst stdout = result.stdout?.toString().trim();\n\tif (stdout) {\n\t\treturn stdout;\n\t}\n\treturn `exit status ${result.status ?? \"unknown\"}`;\n}\n\nfunction runExtractionCommand(command: string, args: string[]): string | null {\n\tconst result = spawnSync(command, args, { stdio: \"pipe\" });\n\tif (!result.error && result.status === 0) {\n\t\treturn null;\n\t}\n\treturn `${command}: ${formatSpawnFailure(result)}`;\n}\n\nfunction extractTarGzArchive(archivePath: string, extractDir: string, assetName: string): void {\n\tconst failure = runExtractionCommand(\"tar\", [\"xzf\", archivePath, \"-C\", extractDir]);\n\tif (failure) {\n\t\tthrow new Error(`Failed to extract ${assetName}: ${failure}`);\n\t}\n}\n\nfunction getWindowsTarCommand(): string {\n\tconst systemRoot = process.env.SystemRoot ?? process.env.WINDIR;\n\tif (systemRoot) {\n\t\tconst systemTar = join(systemRoot, \"System32\", \"tar.exe\");\n\t\tif (existsSync(systemTar)) {\n\t\t\treturn systemTar;\n\t\t}\n\t}\n\treturn \"tar.exe\";\n}\n\nfunction extractZipArchive(archivePath: string, extractDir: string, assetName: string): void {\n\tconst failures: string[] = [];\n\n\tif (platform() === \"win32\") {\n\t\t// Windows ships bsdtar as tar.exe, which supports zip files. Prefer the\n\t\t// System32 binary over Git Bash's GNU tar, which does not handle zip archives.\n\t\tconst tarFailure = runExtractionCommand(getWindowsTarCommand(), [\"xf\", archivePath, \"-C\", extractDir]);\n\t\tif (!tarFailure) return;\n\t\tfailures.push(tarFailure);\n\n\t\tconst script =\n\t\t\t\"& { param($archive, $destination) $ErrorActionPreference = 'Stop'; Expand-Archive -LiteralPath $archive -DestinationPath $destination -Force }\";\n\t\tconst powershellFailure = runExtractionCommand(\"powershell.exe\", [\n\t\t\t\"-NoLogo\",\n\t\t\t\"-NoProfile\",\n\t\t\t\"-NonInteractive\",\n\t\t\t\"-ExecutionPolicy\",\n\t\t\t\"Bypass\",\n\t\t\t\"-Command\",\n\t\t\tscript,\n\t\t\tarchivePath,\n\t\t\textractDir,\n\t\t]);\n\t\tif (!powershellFailure) return;\n\t\tfailures.push(powershellFailure);\n\t} else {\n\t\tconst unzipFailure = runExtractionCommand(\"unzip\", [\"-q\", archivePath, \"-d\", extractDir]);\n\t\tif (!unzipFailure) return;\n\t\tfailures.push(unzipFailure);\n\n\t\tconst tarFailure = runExtractionCommand(\"tar\", [\"xf\", archivePath, \"-C\", extractDir]);\n\t\tif (!tarFailure) return;\n\t\tfailures.push(tarFailure);\n\t}\n\n\tthrow new Error(`Failed to extract ${assetName}: ${failures.join(\"; \")}`);\n}\n\n// Download and install a tool\nasync function downloadTool(tool: \"fd\" | \"rg\"): Promise<string> {\n\tconst config = TOOLS[tool];\n\tif (!config) throw new Error(`Unknown tool: ${tool}`);\n\n\tconst plat = platform();\n\tconst architecture = arch();\n\n\t// Get latest version\n\tlet version = await getLatestVersion(config.repo);\n\tif (tool === \"fd\" && plat === \"darwin\" && architecture === \"x64\") {\n\t\tversion = \"10.3.0\";\n\t}\n\n\t// Get asset name for this platform\n\tconst assetName = config.getAssetName(version, plat, architecture);\n\tif (!assetName) {\n\t\tthrow new Error(`Unsupported platform: ${plat}/${architecture}`);\n\t}\n\n\t// Create tools directory\n\tmkdirSync(TOOLS_DIR, { recursive: true });\n\n\tconst downloadUrl = `https://github.com/${config.repo}/releases/download/${config.tagPrefix}${version}/${assetName}`;\n\tconst archivePath = join(TOOLS_DIR, assetName);\n\tconst binaryExt = plat === \"win32\" ? \".exe\" : \"\";\n\tconst binaryPath = join(TOOLS_DIR, config.binaryName + binaryExt);\n\n\t// Download\n\tawait downloadFile(downloadUrl, archivePath);\n\n\t// Extract into a unique temp directory. fd and rg downloads can run concurrently\n\t// during startup, so sharing a fixed directory causes races.\n\tconst extractDir = join(\n\t\tTOOLS_DIR,\n\t\t`extract_tmp_${config.binaryName}_${process.pid}_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`,\n\t);\n\tmkdirSync(extractDir, { recursive: true });\n\n\ttry {\n\t\tif (assetName.endsWith(\".tar.gz\")) {\n\t\t\textractTarGzArchive(archivePath, extractDir, assetName);\n\t\t} else if (assetName.endsWith(\".zip\")) {\n\t\t\textractZipArchive(archivePath, extractDir, assetName);\n\t\t} else {\n\t\t\tthrow new Error(`Unsupported archive format: ${assetName}`);\n\t\t}\n\n\t\t// Find the binary in extracted files. Some archives contain files directly\n\t\t// at root, others nest under a versioned subdirectory.\n\t\tconst binaryFileName = config.binaryName + binaryExt;\n\t\tconst extractedDir = join(extractDir, assetName.replace(/\\.(tar\\.gz|zip)$/, \"\"));\n\t\tconst extractedBinaryCandidates = [join(extractedDir, binaryFileName), join(extractDir, binaryFileName)];\n\t\tlet extractedBinary = extractedBinaryCandidates.find((candidate) => existsSync(candidate));\n\n\t\tif (!extractedBinary) {\n\t\t\textractedBinary = findBinaryRecursively(extractDir, binaryFileName) ?? undefined;\n\t\t}\n\n\t\tif (extractedBinary) {\n\t\t\trenameSync(extractedBinary, binaryPath);\n\t\t} else {\n\t\t\tthrow new Error(`Binary not found in archive: expected ${binaryFileName} under ${extractDir}`);\n\t\t}\n\n\t\t// Make executable (Unix only)\n\t\tif (plat !== \"win32\") {\n\t\t\tchmodSync(binaryPath, 0o755);\n\t\t}\n\t} finally {\n\t\t// Cleanup\n\t\trmSync(archivePath, { force: true });\n\t\trmSync(extractDir, { recursive: true, force: true });\n\t}\n\n\treturn binaryPath;\n}\n\n// Termux package names for tools\nconst TERMUX_PACKAGES: Record<string, string> = {\n\tfd: \"fd\",\n\trg: \"ripgrep\",\n};\n\n// Ensure a tool is available, downloading if necessary\n// Returns the path to the tool, or null if unavailable\nexport async function ensureTool(tool: \"fd\" | \"rg\", silent: boolean = false): Promise<string | undefined> {\n\tconst existingPath = getToolPath(tool);\n\tif (existingPath) {\n\t\treturn existingPath;\n\t}\n\n\tconst config = TOOLS[tool];\n\tif (!config) return undefined;\n\n\tif (isOfflineModeEnabled()) {\n\t\tif (!silent) {\n\t\t\tconsole.log(chalk.yellow(`${config.name} not found. Offline mode enabled, skipping download.`));\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t// On Android/Termux, Linux binaries don't work due to Bionic libc incompatibility.\n\t// Users must install via pkg.\n\tif (platform() === \"android\") {\n\t\tconst pkgName = TERMUX_PACKAGES[tool] ?? tool;\n\t\tif (!silent) {\n\t\t\tconsole.log(chalk.yellow(`${config.name} not found. Install with: pkg install ${pkgName}`));\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t// Tool not found - download it\n\tif (!silent) {\n\t\tconsole.log(chalk.dim(`${config.name} not found. Downloading...`));\n\t}\n\n\ttry {\n\t\tconst path = await downloadTool(tool);\n\t\tif (!silent) {\n\t\t\tconsole.log(chalk.dim(`${config.name} installed to ${path}`));\n\t\t}\n\t\treturn path;\n\t} catch (e) {\n\t\tif (!silent) {\n\t\t\tconsole.log(chalk.yellow(`Failed to download ${config.name}: ${e instanceof Error ? e.message : e}`));\n\t\t}\n\t\treturn undefined;\n\t}\n}\n"]}
1
+ {"version":3,"file":"tools-manager.d.ts","sourceRoot":"","sources":["../../src/utils/tools-manager.ts"],"names":[],"mappings":"AAqFA,wBAAgB,WAAW,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAmB5D;AA+ND,wBAAsB,UAAU,CAC/B,IAAI,EAAE,IAAI,GAAG,IAAI,EACjB,MAAM,GAAE,OAAe,EACvB,MAAM,GAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAkB,GAC7C,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CA2C7B","sourcesContent":["import chalk from \"chalk\";\nimport { type SpawnSyncReturns, spawnSync } from \"child_process\";\nimport { chmodSync, createWriteStream, existsSync, mkdirSync, readdirSync, renameSync, rmSync } from \"fs\";\nimport { arch, platform } from \"os\";\nimport { join } from \"path\";\nimport { Readable } from \"stream\";\nimport { pipeline } from \"stream/promises\";\nimport { APP_NAME, getBinDir } from \"../config.js\";\n\nconst TOOLS_DIR = getBinDir();\nconst NETWORK_TIMEOUT_MS = 10_000;\nconst DOWNLOAD_TIMEOUT_MS = 120_000;\n\n// SCRAMJET-DIVERGENCE: prefer SCRAMJET_OFFLINE, fall back to PI_OFFLINE.\nfunction isOfflineModeEnabled(): boolean {\n\tconst value = process.env.SCRAMJET_OFFLINE || process.env.PI_OFFLINE;\n\tif (!value) return false;\n\treturn value === \"1\" || value.toLowerCase() === \"true\" || value.toLowerCase() === \"yes\";\n}\n\ninterface ToolConfig {\n\tname: string;\n\trepo: string; // GitHub repo (e.g., \"sharkdp/fd\")\n\tbinaryName: string; // Name of the binary inside the archive\n\tsystemBinaryNames?: string[]; // Alternative system command names to try before downloading\n\ttagPrefix: string; // Prefix for tags (e.g., \"v\" for v1.0.0, \"\" for 1.0.0)\n\tgetAssetName: (version: string, plat: string, architecture: string) => string | null;\n}\n\nconst TOOLS: Record<string, ToolConfig> = {\n\tfd: {\n\t\tname: \"fd\",\n\t\trepo: \"sharkdp/fd\",\n\t\tbinaryName: \"fd\",\n\t\tsystemBinaryNames: [\"fd\", \"fdfind\"],\n\t\ttagPrefix: \"v\",\n\t\tgetAssetName: (version, plat, architecture) => {\n\t\t\tif (plat === \"darwin\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `fd-v${version}-${archStr}-apple-darwin.tar.gz`;\n\t\t\t} else if (plat === \"linux\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `fd-v${version}-${archStr}-unknown-linux-gnu.tar.gz`;\n\t\t\t} else if (plat === \"win32\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `fd-v${version}-${archStr}-pc-windows-msvc.zip`;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t},\n\trg: {\n\t\tname: \"ripgrep\",\n\t\trepo: \"BurntSushi/ripgrep\",\n\t\tbinaryName: \"rg\",\n\t\ttagPrefix: \"\",\n\t\tgetAssetName: (version, plat, architecture) => {\n\t\t\tif (plat === \"darwin\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `ripgrep-${version}-${archStr}-apple-darwin.tar.gz`;\n\t\t\t} else if (plat === \"linux\") {\n\t\t\t\tif (architecture === \"arm64\") {\n\t\t\t\t\treturn `ripgrep-${version}-aarch64-unknown-linux-gnu.tar.gz`;\n\t\t\t\t}\n\t\t\t\treturn `ripgrep-${version}-x86_64-unknown-linux-musl.tar.gz`;\n\t\t\t} else if (plat === \"win32\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `ripgrep-${version}-${archStr}-pc-windows-msvc.zip`;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t},\n};\n\n// Check if a command exists in PATH by trying to run it\nfunction commandExists(cmd: string): boolean {\n\ttry {\n\t\tconst result = spawnSync(cmd, [\"--version\"], { stdio: \"pipe\" });\n\t\t// Check for ENOENT error (command not found)\n\t\treturn result.error === undefined || result.error === null;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n// Get the path to a tool (system-wide or in our tools dir)\nexport function getToolPath(tool: \"fd\" | \"rg\"): string | null {\n\tconst config = TOOLS[tool];\n\tif (!config) return null;\n\n\t// Check our tools directory first\n\tconst localPath = join(TOOLS_DIR, config.binaryName + (platform() === \"win32\" ? \".exe\" : \"\"));\n\tif (existsSync(localPath)) {\n\t\treturn localPath;\n\t}\n\n\t// Check system PATH - if found, just return the command name (it's in PATH)\n\tconst systemBinaryNames = config.systemBinaryNames ?? [config.binaryName];\n\tfor (const systemBinaryName of systemBinaryNames) {\n\t\tif (commandExists(systemBinaryName)) {\n\t\t\treturn systemBinaryName;\n\t\t}\n\t}\n\n\treturn null;\n}\n\n// Fetch latest release version from GitHub\nasync function getLatestVersion(repo: string): Promise<string> {\n\tconst response = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, {\n\t\theaders: { \"User-Agent\": `${APP_NAME}-coding-agent` },\n\t\tsignal: AbortSignal.timeout(NETWORK_TIMEOUT_MS),\n\t});\n\n\tif (!response.ok) {\n\t\tthrow new Error(`GitHub API error: ${response.status}`);\n\t}\n\n\tconst data = (await response.json()) as { tag_name: string };\n\treturn data.tag_name.replace(/^v/, \"\");\n}\n\n// Download a file from URL\nasync function downloadFile(url: string, dest: string): Promise<void> {\n\tconst response = await fetch(url, {\n\t\tsignal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS),\n\t});\n\n\tif (!response.ok) {\n\t\tthrow new Error(`Failed to download: ${response.status}`);\n\t}\n\n\tif (!response.body) {\n\t\tthrow new Error(\"No response body\");\n\t}\n\n\tconst fileStream = createWriteStream(dest);\n\tawait pipeline(Readable.fromWeb(response.body as any), fileStream);\n}\n\nfunction findBinaryRecursively(rootDir: string, binaryFileName: string): string | null {\n\tconst stack: string[] = [rootDir];\n\n\twhile (stack.length > 0) {\n\t\tconst currentDir = stack.pop();\n\t\tif (!currentDir) continue;\n\n\t\tconst entries = readdirSync(currentDir, { withFileTypes: true });\n\t\tfor (const entry of entries) {\n\t\t\tconst fullPath = join(currentDir, entry.name);\n\t\t\tif (entry.isFile() && entry.name === binaryFileName) {\n\t\t\t\treturn fullPath;\n\t\t\t}\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\tstack.push(fullPath);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn null;\n}\n\nfunction formatSpawnFailure(result: SpawnSyncReturns<Buffer>): string {\n\tif (result.error?.message) {\n\t\treturn result.error.message;\n\t}\n\tconst stderr = result.stderr?.toString().trim();\n\tif (stderr) {\n\t\treturn stderr;\n\t}\n\tconst stdout = result.stdout?.toString().trim();\n\tif (stdout) {\n\t\treturn stdout;\n\t}\n\treturn `exit status ${result.status ?? \"unknown\"}`;\n}\n\nfunction runExtractionCommand(command: string, args: string[]): string | null {\n\tconst result = spawnSync(command, args, { stdio: \"pipe\" });\n\tif (!result.error && result.status === 0) {\n\t\treturn null;\n\t}\n\treturn `${command}: ${formatSpawnFailure(result)}`;\n}\n\nfunction extractTarGzArchive(archivePath: string, extractDir: string, assetName: string): void {\n\tconst failure = runExtractionCommand(\"tar\", [\"xzf\", archivePath, \"-C\", extractDir]);\n\tif (failure) {\n\t\tthrow new Error(`Failed to extract ${assetName}: ${failure}`);\n\t}\n}\n\nfunction getWindowsTarCommand(): string {\n\tconst systemRoot = process.env.SystemRoot ?? process.env.WINDIR;\n\tif (systemRoot) {\n\t\tconst systemTar = join(systemRoot, \"System32\", \"tar.exe\");\n\t\tif (existsSync(systemTar)) {\n\t\t\treturn systemTar;\n\t\t}\n\t}\n\treturn \"tar.exe\";\n}\n\nfunction extractZipArchive(archivePath: string, extractDir: string, assetName: string): void {\n\tconst failures: string[] = [];\n\n\tif (platform() === \"win32\") {\n\t\t// Windows ships bsdtar as tar.exe, which supports zip files. Prefer the\n\t\t// System32 binary over Git Bash's GNU tar, which does not handle zip archives.\n\t\tconst tarFailure = runExtractionCommand(getWindowsTarCommand(), [\"xf\", archivePath, \"-C\", extractDir]);\n\t\tif (!tarFailure) return;\n\t\tfailures.push(tarFailure);\n\n\t\tconst script =\n\t\t\t\"& { param($archive, $destination) $ErrorActionPreference = 'Stop'; Expand-Archive -LiteralPath $archive -DestinationPath $destination -Force }\";\n\t\tconst powershellFailure = runExtractionCommand(\"powershell.exe\", [\n\t\t\t\"-NoLogo\",\n\t\t\t\"-NoProfile\",\n\t\t\t\"-NonInteractive\",\n\t\t\t\"-ExecutionPolicy\",\n\t\t\t\"Bypass\",\n\t\t\t\"-Command\",\n\t\t\tscript,\n\t\t\tarchivePath,\n\t\t\textractDir,\n\t\t]);\n\t\tif (!powershellFailure) return;\n\t\tfailures.push(powershellFailure);\n\t} else {\n\t\tconst unzipFailure = runExtractionCommand(\"unzip\", [\"-q\", archivePath, \"-d\", extractDir]);\n\t\tif (!unzipFailure) return;\n\t\tfailures.push(unzipFailure);\n\n\t\tconst tarFailure = runExtractionCommand(\"tar\", [\"xf\", archivePath, \"-C\", extractDir]);\n\t\tif (!tarFailure) return;\n\t\tfailures.push(tarFailure);\n\t}\n\n\tthrow new Error(`Failed to extract ${assetName}: ${failures.join(\"; \")}`);\n}\n\n// Download and install a tool\nasync function downloadTool(tool: \"fd\" | \"rg\"): Promise<string> {\n\tconst config = TOOLS[tool];\n\tif (!config) throw new Error(`Unknown tool: ${tool}`);\n\n\tconst plat = platform();\n\tconst architecture = arch();\n\n\t// Get latest version\n\tlet version = await getLatestVersion(config.repo);\n\tif (tool === \"fd\" && plat === \"darwin\" && architecture === \"x64\") {\n\t\tversion = \"10.3.0\";\n\t}\n\n\t// Get asset name for this platform\n\tconst assetName = config.getAssetName(version, plat, architecture);\n\tif (!assetName) {\n\t\tthrow new Error(`Unsupported platform: ${plat}/${architecture}`);\n\t}\n\n\t// Create tools directory\n\tmkdirSync(TOOLS_DIR, { recursive: true });\n\n\tconst downloadUrl = `https://github.com/${config.repo}/releases/download/${config.tagPrefix}${version}/${assetName}`;\n\tconst archivePath = join(TOOLS_DIR, assetName);\n\tconst binaryExt = plat === \"win32\" ? \".exe\" : \"\";\n\tconst binaryPath = join(TOOLS_DIR, config.binaryName + binaryExt);\n\n\t// Download\n\tawait downloadFile(downloadUrl, archivePath);\n\n\t// Extract into a unique temp directory. fd and rg downloads can run concurrently\n\t// during startup, so sharing a fixed directory causes races.\n\tconst extractDir = join(\n\t\tTOOLS_DIR,\n\t\t`extract_tmp_${config.binaryName}_${process.pid}_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`,\n\t);\n\tmkdirSync(extractDir, { recursive: true });\n\n\ttry {\n\t\tif (assetName.endsWith(\".tar.gz\")) {\n\t\t\textractTarGzArchive(archivePath, extractDir, assetName);\n\t\t} else if (assetName.endsWith(\".zip\")) {\n\t\t\textractZipArchive(archivePath, extractDir, assetName);\n\t\t} else {\n\t\t\tthrow new Error(`Unsupported archive format: ${assetName}`);\n\t\t}\n\n\t\t// Find the binary in extracted files. Some archives contain files directly\n\t\t// at root, others nest under a versioned subdirectory.\n\t\tconst binaryFileName = config.binaryName + binaryExt;\n\t\tconst extractedDir = join(extractDir, assetName.replace(/\\.(tar\\.gz|zip)$/, \"\"));\n\t\tconst extractedBinaryCandidates = [join(extractedDir, binaryFileName), join(extractDir, binaryFileName)];\n\t\tlet extractedBinary = extractedBinaryCandidates.find((candidate) => existsSync(candidate));\n\n\t\tif (!extractedBinary) {\n\t\t\textractedBinary = findBinaryRecursively(extractDir, binaryFileName) ?? undefined;\n\t\t}\n\n\t\tif (extractedBinary) {\n\t\t\trenameSync(extractedBinary, binaryPath);\n\t\t} else {\n\t\t\tthrow new Error(`Binary not found in archive: expected ${binaryFileName} under ${extractDir}`);\n\t\t}\n\n\t\t// Make executable (Unix only)\n\t\tif (plat !== \"win32\") {\n\t\t\tchmodSync(binaryPath, 0o755);\n\t\t}\n\t} finally {\n\t\t// Cleanup\n\t\trmSync(archivePath, { force: true });\n\t\trmSync(extractDir, { recursive: true, force: true });\n\t}\n\n\treturn binaryPath;\n}\n\n// Termux package names for tools\nconst TERMUX_PACKAGES: Record<string, string> = {\n\tfd: \"fd\",\n\trg: \"ripgrep\",\n};\n\n// Ensure a tool is available, downloading if necessary\n// Returns the path to the tool, or null if unavailable\n// SCRAMJET-DIVERGENCE: interactive startup owns presentation of provisioning diagnostics.\nexport async function ensureTool(\n\ttool: \"fd\" | \"rg\",\n\tsilent: boolean = false,\n\treport: (message: string) => void = console.log,\n): Promise<string | undefined> {\n\tconst existingPath = getToolPath(tool);\n\tif (existingPath) {\n\t\treturn existingPath;\n\t}\n\n\tconst config = TOOLS[tool];\n\tif (!config) return undefined;\n\n\tif (isOfflineModeEnabled()) {\n\t\tif (!silent) {\n\t\t\treport(chalk.yellow(`${config.name} not found. Offline mode enabled, skipping download.`));\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t// On Android/Termux, Linux binaries don't work due to Bionic libc incompatibility.\n\t// Users must install via pkg.\n\tif (platform() === \"android\") {\n\t\tconst pkgName = TERMUX_PACKAGES[tool] ?? tool;\n\t\tif (!silent) {\n\t\t\treport(chalk.yellow(`${config.name} not found. Install with: pkg install ${pkgName}`));\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t// Tool not found - download it\n\tif (!silent) {\n\t\treport(chalk.dim(`${config.name} not found. Downloading...`));\n\t}\n\n\ttry {\n\t\tconst path = await downloadTool(tool);\n\t\tif (!silent) {\n\t\t\treport(chalk.dim(`${config.name} installed to ${path}`));\n\t\t}\n\t\treturn path;\n\t} catch (e) {\n\t\tif (!silent) {\n\t\t\treport(chalk.yellow(`Failed to download ${config.name}: ${e instanceof Error ? e.message : e}`));\n\t\t}\n\t\treturn undefined;\n\t}\n}\n"]}
@@ -285,7 +285,8 @@ const TERMUX_PACKAGES = {
285
285
  };
286
286
  // Ensure a tool is available, downloading if necessary
287
287
  // Returns the path to the tool, or null if unavailable
288
- export async function ensureTool(tool, silent = false) {
288
+ // SCRAMJET-DIVERGENCE: interactive startup owns presentation of provisioning diagnostics.
289
+ export async function ensureTool(tool, silent = false, report = console.log) {
289
290
  const existingPath = getToolPath(tool);
290
291
  if (existingPath) {
291
292
  return existingPath;
@@ -295,7 +296,7 @@ export async function ensureTool(tool, silent = false) {
295
296
  return undefined;
296
297
  if (isOfflineModeEnabled()) {
297
298
  if (!silent) {
298
- console.log(chalk.yellow(`${config.name} not found. Offline mode enabled, skipping download.`));
299
+ report(chalk.yellow(`${config.name} not found. Offline mode enabled, skipping download.`));
299
300
  }
300
301
  return undefined;
301
302
  }
@@ -304,24 +305,24 @@ export async function ensureTool(tool, silent = false) {
304
305
  if (platform() === "android") {
305
306
  const pkgName = TERMUX_PACKAGES[tool] ?? tool;
306
307
  if (!silent) {
307
- console.log(chalk.yellow(`${config.name} not found. Install with: pkg install ${pkgName}`));
308
+ report(chalk.yellow(`${config.name} not found. Install with: pkg install ${pkgName}`));
308
309
  }
309
310
  return undefined;
310
311
  }
311
312
  // Tool not found - download it
312
313
  if (!silent) {
313
- console.log(chalk.dim(`${config.name} not found. Downloading...`));
314
+ report(chalk.dim(`${config.name} not found. Downloading...`));
314
315
  }
315
316
  try {
316
317
  const path = await downloadTool(tool);
317
318
  if (!silent) {
318
- console.log(chalk.dim(`${config.name} installed to ${path}`));
319
+ report(chalk.dim(`${config.name} installed to ${path}`));
319
320
  }
320
321
  return path;
321
322
  }
322
323
  catch (e) {
323
324
  if (!silent) {
324
- console.log(chalk.yellow(`Failed to download ${config.name}: ${e instanceof Error ? e.message : e}`));
325
+ report(chalk.yellow(`Failed to download ${config.name}: ${e instanceof Error ? e.message : e}`));
325
326
  }
326
327
  return undefined;
327
328
  }