opencode-v2 1.1.53

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 (439) hide show
  1. package/AGENTS.md +27 -0
  2. package/Dockerfile +18 -0
  3. package/README.md +15 -0
  4. package/bin/opencode +84 -0
  5. package/bunfig.toml +5 -0
  6. package/package.json +126 -0
  7. package/parsers-config.ts +253 -0
  8. package/script/build.ts +193 -0
  9. package/script/postinstall.mjs +125 -0
  10. package/script/publish.ts +181 -0
  11. package/script/schema.ts +47 -0
  12. package/script/seed-e2e.ts +50 -0
  13. package/src/acp/README.md +164 -0
  14. package/src/acp/agent.ts +1676 -0
  15. package/src/acp/session.ts +117 -0
  16. package/src/acp/types.ts +23 -0
  17. package/src/agent/agent.ts +414 -0
  18. package/src/agent/generate.txt +75 -0
  19. package/src/agent/prompt/compaction.txt +12 -0
  20. package/src/agent/prompt/explore.txt +18 -0
  21. package/src/agent/prompt/summary.txt +11 -0
  22. package/src/agent/prompt/title.txt +44 -0
  23. package/src/auth/index.ts +70 -0
  24. package/src/bun/index.ts +137 -0
  25. package/src/bun/registry.ts +48 -0
  26. package/src/bus/bus-event.ts +43 -0
  27. package/src/bus/global.ts +10 -0
  28. package/src/bus/index.ts +105 -0
  29. package/src/cli/bootstrap.ts +17 -0
  30. package/src/cli/cmd/acp.ts +70 -0
  31. package/src/cli/cmd/agent.ts +257 -0
  32. package/src/cli/cmd/auth.ts +400 -0
  33. package/src/cli/cmd/cmd.ts +7 -0
  34. package/src/cli/cmd/debug/agent.ts +167 -0
  35. package/src/cli/cmd/debug/config.ts +16 -0
  36. package/src/cli/cmd/debug/file.ts +97 -0
  37. package/src/cli/cmd/debug/index.ts +48 -0
  38. package/src/cli/cmd/debug/lsp.ts +52 -0
  39. package/src/cli/cmd/debug/ripgrep.ts +87 -0
  40. package/src/cli/cmd/debug/scrap.ts +16 -0
  41. package/src/cli/cmd/debug/skill.ts +16 -0
  42. package/src/cli/cmd/debug/snapshot.ts +52 -0
  43. package/src/cli/cmd/export.ts +88 -0
  44. package/src/cli/cmd/generate.ts +38 -0
  45. package/src/cli/cmd/github.ts +1540 -0
  46. package/src/cli/cmd/import.ts +147 -0
  47. package/src/cli/cmd/mcp.ts +755 -0
  48. package/src/cli/cmd/models.ts +77 -0
  49. package/src/cli/cmd/pr.ts +112 -0
  50. package/src/cli/cmd/run.ts +617 -0
  51. package/src/cli/cmd/serve.ts +20 -0
  52. package/src/cli/cmd/session.ts +135 -0
  53. package/src/cli/cmd/stats.ts +426 -0
  54. package/src/cli/cmd/tui/app.tsx +801 -0
  55. package/src/cli/cmd/tui/attach.ts +52 -0
  56. package/src/cli/cmd/tui/component/border.tsx +21 -0
  57. package/src/cli/cmd/tui/component/dialog-agent.tsx +31 -0
  58. package/src/cli/cmd/tui/component/dialog-command.tsx +148 -0
  59. package/src/cli/cmd/tui/component/dialog-mcp.tsx +86 -0
  60. package/src/cli/cmd/tui/component/dialog-model.tsx +234 -0
  61. package/src/cli/cmd/tui/component/dialog-provider.tsx +266 -0
  62. package/src/cli/cmd/tui/component/dialog-session-list.tsx +108 -0
  63. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
  64. package/src/cli/cmd/tui/component/dialog-skill.tsx +36 -0
  65. package/src/cli/cmd/tui/component/dialog-stash.tsx +87 -0
  66. package/src/cli/cmd/tui/component/dialog-status.tsx +177 -0
  67. package/src/cli/cmd/tui/component/dialog-tag.tsx +44 -0
  68. package/src/cli/cmd/tui/component/dialog-theme-list.tsx +50 -0
  69. package/src/cli/cmd/tui/component/logo.tsx +85 -0
  70. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +666 -0
  71. package/src/cli/cmd/tui/component/prompt/frecency.tsx +89 -0
  72. package/src/cli/cmd/tui/component/prompt/history.tsx +108 -0
  73. package/src/cli/cmd/tui/component/prompt/index.tsx +1132 -0
  74. package/src/cli/cmd/tui/component/prompt/stash.tsx +101 -0
  75. package/src/cli/cmd/tui/component/spinner.tsx +24 -0
  76. package/src/cli/cmd/tui/component/textarea-keybindings.ts +73 -0
  77. package/src/cli/cmd/tui/component/tips.tsx +153 -0
  78. package/src/cli/cmd/tui/component/todo-item.tsx +32 -0
  79. package/src/cli/cmd/tui/context/args.tsx +15 -0
  80. package/src/cli/cmd/tui/context/directory.ts +13 -0
  81. package/src/cli/cmd/tui/context/exit.tsx +52 -0
  82. package/src/cli/cmd/tui/context/helper.tsx +25 -0
  83. package/src/cli/cmd/tui/context/keybind.tsx +100 -0
  84. package/src/cli/cmd/tui/context/kv.tsx +52 -0
  85. package/src/cli/cmd/tui/context/local.tsx +409 -0
  86. package/src/cli/cmd/tui/context/prompt.tsx +18 -0
  87. package/src/cli/cmd/tui/context/route.tsx +46 -0
  88. package/src/cli/cmd/tui/context/sdk.tsx +101 -0
  89. package/src/cli/cmd/tui/context/sync.tsx +470 -0
  90. package/src/cli/cmd/tui/context/theme/aura.json +69 -0
  91. package/src/cli/cmd/tui/context/theme/ayu.json +80 -0
  92. package/src/cli/cmd/tui/context/theme/carbonfox.json +248 -0
  93. package/src/cli/cmd/tui/context/theme/catppuccin-frappe.json +233 -0
  94. package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +233 -0
  95. package/src/cli/cmd/tui/context/theme/catppuccin.json +112 -0
  96. package/src/cli/cmd/tui/context/theme/cobalt2.json +228 -0
  97. package/src/cli/cmd/tui/context/theme/cursor.json +249 -0
  98. package/src/cli/cmd/tui/context/theme/dracula.json +219 -0
  99. package/src/cli/cmd/tui/context/theme/everforest.json +241 -0
  100. package/src/cli/cmd/tui/context/theme/flexoki.json +237 -0
  101. package/src/cli/cmd/tui/context/theme/github.json +233 -0
  102. package/src/cli/cmd/tui/context/theme/gruvbox.json +242 -0
  103. package/src/cli/cmd/tui/context/theme/kanagawa.json +77 -0
  104. package/src/cli/cmd/tui/context/theme/lucent-orng.json +237 -0
  105. package/src/cli/cmd/tui/context/theme/material.json +235 -0
  106. package/src/cli/cmd/tui/context/theme/matrix.json +77 -0
  107. package/src/cli/cmd/tui/context/theme/mercury.json +252 -0
  108. package/src/cli/cmd/tui/context/theme/monokai.json +221 -0
  109. package/src/cli/cmd/tui/context/theme/nightowl.json +221 -0
  110. package/src/cli/cmd/tui/context/theme/nord.json +223 -0
  111. package/src/cli/cmd/tui/context/theme/one-dark.json +84 -0
  112. package/src/cli/cmd/tui/context/theme/orng.json +249 -0
  113. package/src/cli/cmd/tui/context/theme/osaka-jade.json +93 -0
  114. package/src/cli/cmd/tui/context/theme/palenight.json +222 -0
  115. package/src/cli/cmd/tui/context/theme/rosepine.json +234 -0
  116. package/src/cli/cmd/tui/context/theme/solarized.json +223 -0
  117. package/src/cli/cmd/tui/context/theme/synthwave84.json +226 -0
  118. package/src/cli/cmd/tui/context/theme/tokyonight.json +243 -0
  119. package/src/cli/cmd/tui/context/theme/vercel.json +245 -0
  120. package/src/cli/cmd/tui/context/theme/vesper.json +218 -0
  121. package/src/cli/cmd/tui/context/theme/zenburn.json +223 -0
  122. package/src/cli/cmd/tui/context/theme.tsx +1152 -0
  123. package/src/cli/cmd/tui/event.ts +48 -0
  124. package/src/cli/cmd/tui/routes/home.tsx +140 -0
  125. package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +64 -0
  126. package/src/cli/cmd/tui/routes/session/dialog-message.tsx +109 -0
  127. package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +26 -0
  128. package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +47 -0
  129. package/src/cli/cmd/tui/routes/session/footer.tsx +91 -0
  130. package/src/cli/cmd/tui/routes/session/header.tsx +142 -0
  131. package/src/cli/cmd/tui/routes/session/index.tsx +2126 -0
  132. package/src/cli/cmd/tui/routes/session/permission.tsx +508 -0
  133. package/src/cli/cmd/tui/routes/session/question.tsx +466 -0
  134. package/src/cli/cmd/tui/routes/session/sidebar.tsx +313 -0
  135. package/src/cli/cmd/tui/thread.ts +175 -0
  136. package/src/cli/cmd/tui/ui/dialog-alert.tsx +68 -0
  137. package/src/cli/cmd/tui/ui/dialog-confirm.tsx +93 -0
  138. package/src/cli/cmd/tui/ui/dialog-export-options.tsx +215 -0
  139. package/src/cli/cmd/tui/ui/dialog-help.tsx +49 -0
  140. package/src/cli/cmd/tui/ui/dialog-prompt.tsx +88 -0
  141. package/src/cli/cmd/tui/ui/dialog-select.tsx +399 -0
  142. package/src/cli/cmd/tui/ui/dialog.tsx +167 -0
  143. package/src/cli/cmd/tui/ui/link.tsx +28 -0
  144. package/src/cli/cmd/tui/ui/spinner.ts +368 -0
  145. package/src/cli/cmd/tui/ui/toast.tsx +100 -0
  146. package/src/cli/cmd/tui/util/clipboard.ts +159 -0
  147. package/src/cli/cmd/tui/util/editor.ts +32 -0
  148. package/src/cli/cmd/tui/util/signal.ts +7 -0
  149. package/src/cli/cmd/tui/util/terminal.ts +114 -0
  150. package/src/cli/cmd/tui/util/transcript.ts +98 -0
  151. package/src/cli/cmd/tui/worker.ts +152 -0
  152. package/src/cli/cmd/uninstall.ts +357 -0
  153. package/src/cli/cmd/upgrade.ts +73 -0
  154. package/src/cli/cmd/web.ts +81 -0
  155. package/src/cli/error.ts +57 -0
  156. package/src/cli/logo.ts +6 -0
  157. package/src/cli/network.ts +60 -0
  158. package/src/cli/ui.ts +113 -0
  159. package/src/cli/upgrade.ts +25 -0
  160. package/src/command/index.ts +150 -0
  161. package/src/command/template/initialize.txt +10 -0
  162. package/src/command/template/review.txt +99 -0
  163. package/src/config/config.ts +1477 -0
  164. package/src/config/markdown.ts +98 -0
  165. package/src/env/index.ts +28 -0
  166. package/src/file/ignore.ts +83 -0
  167. package/src/file/index.ts +583 -0
  168. package/src/file/ripgrep.ts +375 -0
  169. package/src/file/time.ts +69 -0
  170. package/src/file/watcher.ts +127 -0
  171. package/src/flag/flag.ts +97 -0
  172. package/src/format/formatter.ts +366 -0
  173. package/src/format/index.ts +137 -0
  174. package/src/global/index.ts +55 -0
  175. package/src/id/id.ts +83 -0
  176. package/src/ide/index.ts +76 -0
  177. package/src/index.ts +159 -0
  178. package/src/installation/index.ts +246 -0
  179. package/src/lsp/client.ts +252 -0
  180. package/src/lsp/index.ts +485 -0
  181. package/src/lsp/language.ts +119 -0
  182. package/src/lsp/server.ts +2046 -0
  183. package/src/mcp/auth.ts +132 -0
  184. package/src/mcp/index.ts +934 -0
  185. package/src/mcp/oauth-callback.ts +200 -0
  186. package/src/mcp/oauth-provider.ts +154 -0
  187. package/src/patch/index.ts +680 -0
  188. package/src/permission/arity.ts +163 -0
  189. package/src/permission/index.ts +210 -0
  190. package/src/permission/next.ts +280 -0
  191. package/src/plugin/codex.ts +624 -0
  192. package/src/plugin/copilot.ts +327 -0
  193. package/src/plugin/index.ts +138 -0
  194. package/src/project/bootstrap.ts +35 -0
  195. package/src/project/instance.ts +114 -0
  196. package/src/project/project.ts +371 -0
  197. package/src/project/state.ts +70 -0
  198. package/src/project/vcs.ts +76 -0
  199. package/src/provider/auth.ts +147 -0
  200. package/src/provider/models.ts +133 -0
  201. package/src/provider/provider.ts +1262 -0
  202. package/src/provider/sdk/copilot/README.md +5 -0
  203. package/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts +164 -0
  204. package/src/provider/sdk/copilot/chat/get-response-metadata.ts +15 -0
  205. package/src/provider/sdk/copilot/chat/map-openai-compatible-finish-reason.ts +17 -0
  206. package/src/provider/sdk/copilot/chat/openai-compatible-api-types.ts +64 -0
  207. package/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts +780 -0
  208. package/src/provider/sdk/copilot/chat/openai-compatible-chat-options.ts +28 -0
  209. package/src/provider/sdk/copilot/chat/openai-compatible-metadata-extractor.ts +44 -0
  210. package/src/provider/sdk/copilot/chat/openai-compatible-prepare-tools.ts +87 -0
  211. package/src/provider/sdk/copilot/copilot-provider.ts +100 -0
  212. package/src/provider/sdk/copilot/index.ts +2 -0
  213. package/src/provider/sdk/copilot/openai-compatible-error.ts +27 -0
  214. package/src/provider/sdk/copilot/responses/convert-to-openai-responses-input.ts +303 -0
  215. package/src/provider/sdk/copilot/responses/map-openai-responses-finish-reason.ts +22 -0
  216. package/src/provider/sdk/copilot/responses/openai-config.ts +18 -0
  217. package/src/provider/sdk/copilot/responses/openai-error.ts +22 -0
  218. package/src/provider/sdk/copilot/responses/openai-responses-api-types.ts +207 -0
  219. package/src/provider/sdk/copilot/responses/openai-responses-language-model.ts +1732 -0
  220. package/src/provider/sdk/copilot/responses/openai-responses-prepare-tools.ts +177 -0
  221. package/src/provider/sdk/copilot/responses/openai-responses-settings.ts +1 -0
  222. package/src/provider/sdk/copilot/responses/tool/code-interpreter.ts +88 -0
  223. package/src/provider/sdk/copilot/responses/tool/file-search.ts +128 -0
  224. package/src/provider/sdk/copilot/responses/tool/image-generation.ts +115 -0
  225. package/src/provider/sdk/copilot/responses/tool/local-shell.ts +65 -0
  226. package/src/provider/sdk/copilot/responses/tool/web-search-preview.ts +104 -0
  227. package/src/provider/sdk/copilot/responses/tool/web-search.ts +103 -0
  228. package/src/provider/transform.ts +828 -0
  229. package/src/pty/index.ts +250 -0
  230. package/src/question/index.ts +171 -0
  231. package/src/scheduler/index.ts +61 -0
  232. package/src/server/error.ts +36 -0
  233. package/src/server/event.ts +7 -0
  234. package/src/server/mdns.ts +60 -0
  235. package/src/server/routes/config.ts +92 -0
  236. package/src/server/routes/experimental.ts +208 -0
  237. package/src/server/routes/file.ts +197 -0
  238. package/src/server/routes/global.ts +183 -0
  239. package/src/server/routes/mcp.ts +225 -0
  240. package/src/server/routes/permission.ts +68 -0
  241. package/src/server/routes/project.ts +82 -0
  242. package/src/server/routes/provider.ts +165 -0
  243. package/src/server/routes/pty.ts +169 -0
  244. package/src/server/routes/question.ts +98 -0
  245. package/src/server/routes/session.ts +939 -0
  246. package/src/server/routes/tui.ts +379 -0
  247. package/src/server/server.ts +613 -0
  248. package/src/session/compaction.ts +226 -0
  249. package/src/session/index.ts +524 -0
  250. package/src/session/instruction.ts +197 -0
  251. package/src/session/llm.ts +289 -0
  252. package/src/session/message-v2.ts +802 -0
  253. package/src/session/message.ts +189 -0
  254. package/src/session/processor.ts +407 -0
  255. package/src/session/prompt/agent.txt +43 -0
  256. package/src/session/prompt/anthropic-20250930.txt +166 -0
  257. package/src/session/prompt/anthropic.txt +105 -0
  258. package/src/session/prompt/beast.txt +147 -0
  259. package/src/session/prompt/build-switch.txt +5 -0
  260. package/src/session/prompt/codex_header.txt +79 -0
  261. package/src/session/prompt/copilot-gpt-5.txt +143 -0
  262. package/src/session/prompt/gemini.txt +155 -0
  263. package/src/session/prompt/max-steps.txt +16 -0
  264. package/src/session/prompt/plan-reminder-anthropic.txt +67 -0
  265. package/src/session/prompt/plan.txt +26 -0
  266. package/src/session/prompt/qwen.txt +109 -0
  267. package/src/session/prompt/research.txt +81 -0
  268. package/src/session/prompt/trinity.txt +97 -0
  269. package/src/session/prompt.ts +1952 -0
  270. package/src/session/retry.ts +97 -0
  271. package/src/session/revert.ts +121 -0
  272. package/src/session/status.ts +76 -0
  273. package/src/session/summary.ts +217 -0
  274. package/src/session/system.ts +54 -0
  275. package/src/session/todo.ts +37 -0
  276. package/src/share/share-next.ts +200 -0
  277. package/src/share/share.ts +92 -0
  278. package/src/shell/shell.ts +67 -0
  279. package/src/skill/discovery.ts +97 -0
  280. package/src/skill/index.ts +1 -0
  281. package/src/skill/skill.ts +188 -0
  282. package/src/snapshot/index.ts +255 -0
  283. package/src/storage/storage.ts +227 -0
  284. package/src/tool/agent-enter.txt +1 -0
  285. package/src/tool/agent-exit.txt +1 -0
  286. package/src/tool/agent.ts +237 -0
  287. package/src/tool/apply_patch.ts +281 -0
  288. package/src/tool/apply_patch.txt +33 -0
  289. package/src/tool/bash.ts +269 -0
  290. package/src/tool/bash.txt +115 -0
  291. package/src/tool/batch.ts +175 -0
  292. package/src/tool/batch.txt +24 -0
  293. package/src/tool/chat-enter.txt +15 -0
  294. package/src/tool/chat-exit.txt +7 -0
  295. package/src/tool/chat.ts +217 -0
  296. package/src/tool/codesearch.ts +132 -0
  297. package/src/tool/codesearch.txt +12 -0
  298. package/src/tool/edit.ts +655 -0
  299. package/src/tool/edit.txt +10 -0
  300. package/src/tool/external-directory.ts +32 -0
  301. package/src/tool/glob.ts +78 -0
  302. package/src/tool/glob.txt +6 -0
  303. package/src/tool/grep.ts +147 -0
  304. package/src/tool/grep.txt +8 -0
  305. package/src/tool/invalid.ts +17 -0
  306. package/src/tool/ls.ts +121 -0
  307. package/src/tool/ls.txt +1 -0
  308. package/src/tool/lsp.ts +96 -0
  309. package/src/tool/lsp.txt +19 -0
  310. package/src/tool/multiedit.ts +46 -0
  311. package/src/tool/multiedit.txt +41 -0
  312. package/src/tool/plan-enter.txt +14 -0
  313. package/src/tool/plan-exit.txt +13 -0
  314. package/src/tool/plan.ts +130 -0
  315. package/src/tool/question.ts +33 -0
  316. package/src/tool/question.txt +10 -0
  317. package/src/tool/read.ts +211 -0
  318. package/src/tool/read.txt +12 -0
  319. package/src/tool/registry.ts +167 -0
  320. package/src/tool/research-enter.txt +1 -0
  321. package/src/tool/research-exit.txt +1 -0
  322. package/src/tool/research.ts +134 -0
  323. package/src/tool/skill.ts +123 -0
  324. package/src/tool/task.ts +165 -0
  325. package/src/tool/task.txt +60 -0
  326. package/src/tool/todo.ts +53 -0
  327. package/src/tool/todoread.txt +14 -0
  328. package/src/tool/todowrite.txt +167 -0
  329. package/src/tool/tool.ts +89 -0
  330. package/src/tool/truncation.ts +106 -0
  331. package/src/tool/webfetch.ts +186 -0
  332. package/src/tool/webfetch.txt +13 -0
  333. package/src/tool/websearch.ts +150 -0
  334. package/src/tool/websearch.txt +14 -0
  335. package/src/tool/write.ts +85 -0
  336. package/src/tool/write.txt +8 -0
  337. package/src/util/abort.ts +35 -0
  338. package/src/util/archive.ts +16 -0
  339. package/src/util/color.ts +19 -0
  340. package/src/util/context.ts +25 -0
  341. package/src/util/defer.ts +12 -0
  342. package/src/util/eventloop.ts +20 -0
  343. package/src/util/filesystem.ts +93 -0
  344. package/src/util/fn.ts +11 -0
  345. package/src/util/format.ts +20 -0
  346. package/src/util/iife.ts +3 -0
  347. package/src/util/keybind.ts +103 -0
  348. package/src/util/lazy.ts +18 -0
  349. package/src/util/locale.ts +81 -0
  350. package/src/util/lock.ts +98 -0
  351. package/src/util/log.ts +180 -0
  352. package/src/util/proxied.ts +3 -0
  353. package/src/util/queue.ts +32 -0
  354. package/src/util/rpc.ts +66 -0
  355. package/src/util/scrap.ts +10 -0
  356. package/src/util/signal.ts +12 -0
  357. package/src/util/timeout.ts +14 -0
  358. package/src/util/token.ts +7 -0
  359. package/src/util/wildcard.ts +56 -0
  360. package/src/worktree/index.ts +574 -0
  361. package/sst-env.d.ts +9 -0
  362. package/test/acp/agent-interface.test.ts +51 -0
  363. package/test/acp/event-subscription.test.ts +436 -0
  364. package/test/agent/agent.test.ts +675 -0
  365. package/test/bun.test.ts +53 -0
  366. package/test/cli/github-action.test.ts +161 -0
  367. package/test/cli/github-remote.test.ts +80 -0
  368. package/test/cli/import.test.ts +38 -0
  369. package/test/cli/tui/transcript.test.ts +322 -0
  370. package/test/config/agent-color.test.ts +71 -0
  371. package/test/config/config.test.ts +1802 -0
  372. package/test/config/fixtures/empty-frontmatter.md +4 -0
  373. package/test/config/fixtures/frontmatter.md +28 -0
  374. package/test/config/fixtures/markdown-header.md +11 -0
  375. package/test/config/fixtures/no-frontmatter.md +1 -0
  376. package/test/config/fixtures/weird-model-id.md +13 -0
  377. package/test/config/markdown.test.ts +228 -0
  378. package/test/file/ignore.test.ts +10 -0
  379. package/test/file/path-traversal.test.ts +198 -0
  380. package/test/file/ripgrep.test.ts +39 -0
  381. package/test/fixture/fixture.ts +45 -0
  382. package/test/fixture/lsp/fake-lsp-server.js +77 -0
  383. package/test/ide/ide.test.ts +82 -0
  384. package/test/keybind.test.ts +421 -0
  385. package/test/lsp/client.test.ts +95 -0
  386. package/test/mcp/headers.test.ts +153 -0
  387. package/test/mcp/oauth-browser.test.ts +249 -0
  388. package/test/memory/abort-leak.test.ts +136 -0
  389. package/test/patch/patch.test.ts +348 -0
  390. package/test/permission/arity.test.ts +33 -0
  391. package/test/permission/next.test.ts +690 -0
  392. package/test/permission-task.test.ts +319 -0
  393. package/test/plugin/auth-override.test.ts +44 -0
  394. package/test/plugin/codex.test.ts +123 -0
  395. package/test/preload.ts +63 -0
  396. package/test/project/project.test.ts +120 -0
  397. package/test/provider/amazon-bedrock.test.ts +445 -0
  398. package/test/provider/copilot/convert-to-copilot-messages.test.ts +523 -0
  399. package/test/provider/copilot/copilot-chat-model.test.ts +592 -0
  400. package/test/provider/gitlab-duo.test.ts +262 -0
  401. package/test/provider/provider.test.ts +2129 -0
  402. package/test/provider/transform.test.ts +2022 -0
  403. package/test/question/question.test.ts +300 -0
  404. package/test/scheduler.test.ts +73 -0
  405. package/test/server/session-list.test.ts +39 -0
  406. package/test/server/session-select.test.ts +78 -0
  407. package/test/session/compaction.test.ts +293 -0
  408. package/test/session/instruction.test.ts +170 -0
  409. package/test/session/llm.test.ts +691 -0
  410. package/test/session/message-v2.test.ts +786 -0
  411. package/test/session/prompt-missing-file.test.ts +53 -0
  412. package/test/session/prompt-special-chars.test.ts +56 -0
  413. package/test/session/prompt-variant.test.ts +60 -0
  414. package/test/session/retry.test.ts +179 -0
  415. package/test/session/revert-compact.test.ts +285 -0
  416. package/test/session/session.test.ts +71 -0
  417. package/test/skill/discovery.test.ts +60 -0
  418. package/test/skill/skill.test.ts +388 -0
  419. package/test/snapshot/snapshot.test.ts +1040 -0
  420. package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
  421. package/test/tool/apply_patch.test.ts +559 -0
  422. package/test/tool/bash.test.ts +399 -0
  423. package/test/tool/external-directory.test.ts +127 -0
  424. package/test/tool/fixtures/large-image.png +0 -0
  425. package/test/tool/fixtures/models-api.json +38413 -0
  426. package/test/tool/grep.test.ts +110 -0
  427. package/test/tool/question.test.ts +107 -0
  428. package/test/tool/read.test.ts +358 -0
  429. package/test/tool/registry.test.ts +122 -0
  430. package/test/tool/skill.test.ts +112 -0
  431. package/test/tool/truncation.test.ts +159 -0
  432. package/test/util/filesystem.test.ts +39 -0
  433. package/test/util/format.test.ts +59 -0
  434. package/test/util/iife.test.ts +36 -0
  435. package/test/util/lazy.test.ts +50 -0
  436. package/test/util/lock.test.ts +72 -0
  437. package/test/util/timeout.test.ts +21 -0
  438. package/test/util/wildcard.test.ts +75 -0
  439. package/tsconfig.json +16 -0
@@ -0,0 +1,150 @@
1
+ import z from "zod"
2
+ import { Tool } from "./tool"
3
+ import DESCRIPTION from "./websearch.txt"
4
+ import { abortAfterAny } from "../util/abort"
5
+
6
+ const API_CONFIG = {
7
+ BASE_URL: "https://mcp.exa.ai",
8
+ ENDPOINTS: {
9
+ SEARCH: "/mcp",
10
+ },
11
+ DEFAULT_NUM_RESULTS: 8,
12
+ } as const
13
+
14
+ interface McpSearchRequest {
15
+ jsonrpc: string
16
+ id: number
17
+ method: string
18
+ params: {
19
+ name: string
20
+ arguments: {
21
+ query: string
22
+ numResults?: number
23
+ livecrawl?: "fallback" | "preferred"
24
+ type?: "auto" | "fast" | "deep"
25
+ contextMaxCharacters?: number
26
+ }
27
+ }
28
+ }
29
+
30
+ interface McpSearchResponse {
31
+ jsonrpc: string
32
+ result: {
33
+ content: Array<{
34
+ type: string
35
+ text: string
36
+ }>
37
+ }
38
+ }
39
+
40
+ export const WebSearchTool = Tool.define("websearch", async () => {
41
+ return {
42
+ get description() {
43
+ return DESCRIPTION.replace("{{date}}", new Date().toISOString().slice(0, 10))
44
+ },
45
+ parameters: z.object({
46
+ query: z.string().describe("Websearch query"),
47
+ numResults: z.number().optional().describe("Number of search results to return (default: 8)"),
48
+ livecrawl: z
49
+ .enum(["fallback", "preferred"])
50
+ .optional()
51
+ .describe(
52
+ "Live crawl mode - 'fallback': use live crawling as backup if cached content unavailable, 'preferred': prioritize live crawling (default: 'fallback')",
53
+ ),
54
+ type: z
55
+ .enum(["auto", "fast", "deep"])
56
+ .optional()
57
+ .describe(
58
+ "Search type - 'auto': balanced search (default), 'fast': quick results, 'deep': comprehensive search",
59
+ ),
60
+ contextMaxCharacters: z
61
+ .number()
62
+ .optional()
63
+ .describe("Maximum characters for context string optimized for LLMs (default: 10000)"),
64
+ }),
65
+ async execute(params, ctx) {
66
+ await ctx.ask({
67
+ permission: "websearch",
68
+ patterns: [params.query],
69
+ always: ["*"],
70
+ metadata: {
71
+ query: params.query,
72
+ numResults: params.numResults,
73
+ livecrawl: params.livecrawl,
74
+ type: params.type,
75
+ contextMaxCharacters: params.contextMaxCharacters,
76
+ },
77
+ })
78
+
79
+ const searchRequest: McpSearchRequest = {
80
+ jsonrpc: "2.0",
81
+ id: 1,
82
+ method: "tools/call",
83
+ params: {
84
+ name: "web_search_exa",
85
+ arguments: {
86
+ query: params.query,
87
+ type: params.type || "auto",
88
+ numResults: params.numResults || API_CONFIG.DEFAULT_NUM_RESULTS,
89
+ livecrawl: params.livecrawl || "fallback",
90
+ contextMaxCharacters: params.contextMaxCharacters,
91
+ },
92
+ },
93
+ }
94
+
95
+ const { signal, clearTimeout } = abortAfterAny(25000, ctx.abort)
96
+
97
+ try {
98
+ const headers: Record<string, string> = {
99
+ accept: "application/json, text/event-stream",
100
+ "content-type": "application/json",
101
+ }
102
+
103
+ const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.SEARCH}`, {
104
+ method: "POST",
105
+ headers,
106
+ body: JSON.stringify(searchRequest),
107
+ signal,
108
+ })
109
+
110
+ clearTimeout()
111
+
112
+ if (!response.ok) {
113
+ const errorText = await response.text()
114
+ throw new Error(`Search error (${response.status}): ${errorText}`)
115
+ }
116
+
117
+ const responseText = await response.text()
118
+
119
+ // Parse SSE response
120
+ const lines = responseText.split("\n")
121
+ for (const line of lines) {
122
+ if (line.startsWith("data: ")) {
123
+ const data: McpSearchResponse = JSON.parse(line.substring(6))
124
+ if (data.result && data.result.content && data.result.content.length > 0) {
125
+ return {
126
+ output: data.result.content[0].text,
127
+ title: `Web search: ${params.query}`,
128
+ metadata: {},
129
+ }
130
+ }
131
+ }
132
+ }
133
+
134
+ return {
135
+ output: "No search results found. Please try a different query.",
136
+ title: `Web search: ${params.query}`,
137
+ metadata: {},
138
+ }
139
+ } catch (error) {
140
+ clearTimeout()
141
+
142
+ if (error instanceof Error && error.name === "AbortError") {
143
+ throw new Error("Search request timed out")
144
+ }
145
+
146
+ throw error
147
+ }
148
+ },
149
+ }
150
+ })
@@ -0,0 +1,14 @@
1
+ - Search the web using Exa AI - performs real-time web searches and can scrape content from specific URLs
2
+ - Provides up-to-date information for current events and recent data
3
+ - Supports configurable result counts and returns the content from the most relevant websites
4
+ - Use this tool for accessing information beyond knowledge cutoff
5
+ - Searches are performed automatically within a single API call
6
+
7
+ Usage notes:
8
+ - Supports live crawling modes: 'fallback' (backup if cached unavailable) or 'preferred' (prioritize live crawling)
9
+ - Search types: 'auto' (balanced), 'fast' (quick results), 'deep' (comprehensive search)
10
+ - Configurable context length for optimal LLM integration
11
+ - Domain filtering and advanced search options available
12
+
13
+ Today's date is {{date}}. You MUST use this year when searching for recent information or current events
14
+ - Example: If today is 2025-07-15 and the user asks for "latest AI news", search for "AI news 2025", NOT "AI news 2024"
@@ -0,0 +1,85 @@
1
+ import z from "zod"
2
+ import * as path from "path"
3
+ import { Tool } from "./tool"
4
+ import { LSP } from "../lsp"
5
+ import { createTwoFilesPatch } from "diff"
6
+ import DESCRIPTION from "./write.txt"
7
+ import { Bus } from "../bus"
8
+ import { File } from "../file"
9
+ import { FileWatcher } from "../file/watcher"
10
+ import { FileTime } from "../file/time"
11
+ import { Filesystem } from "../util/filesystem"
12
+ import { Instance } from "../project/instance"
13
+ import { trimDiff } from "./edit"
14
+ import { assertExternalDirectory } from "./external-directory"
15
+
16
+ const MAX_DIAGNOSTICS_PER_FILE = 20
17
+ const MAX_PROJECT_DIAGNOSTICS_FILES = 5
18
+
19
+ export const WriteTool = Tool.define("write", {
20
+ description: DESCRIPTION,
21
+ parameters: z.object({
22
+ content: z.string().describe("The content to write to the file"),
23
+ filePath: z.string().describe("The absolute path to the file to write (must be absolute, not relative)"),
24
+ }),
25
+ async execute(params, ctx) {
26
+ const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath)
27
+ await assertExternalDirectory(ctx, filepath)
28
+
29
+ const file = Bun.file(filepath)
30
+ const exists = await file.exists()
31
+ const contentOld = exists ? await file.text() : ""
32
+ if (exists) await FileTime.assert(ctx.sessionID, filepath)
33
+
34
+ const diff = trimDiff(createTwoFilesPatch(filepath, filepath, contentOld, params.content))
35
+ await ctx.ask({
36
+ permission: "edit",
37
+ patterns: [path.relative(Instance.worktree, filepath)],
38
+ always: ["*"],
39
+ metadata: {
40
+ filepath,
41
+ diff,
42
+ },
43
+ })
44
+
45
+ await Bun.write(filepath, params.content)
46
+ await Bus.publish(File.Event.Edited, {
47
+ file: filepath,
48
+ })
49
+ await Bus.publish(FileWatcher.Event.Updated, {
50
+ file: filepath,
51
+ event: exists ? "change" : "add",
52
+ })
53
+ FileTime.read(ctx.sessionID, filepath)
54
+
55
+ let output = "Wrote file successfully."
56
+ await LSP.touchFile(filepath, true)
57
+ const diagnostics = await LSP.diagnostics()
58
+ const normalizedFilepath = Filesystem.normalizePath(filepath)
59
+ let projectDiagnosticsCount = 0
60
+ for (const [file, issues] of Object.entries(diagnostics)) {
61
+ const errors = issues.filter((item) => item.severity === 1)
62
+ if (errors.length === 0) continue
63
+ const limited = errors.slice(0, MAX_DIAGNOSTICS_PER_FILE)
64
+ const suffix =
65
+ errors.length > MAX_DIAGNOSTICS_PER_FILE ? `\n... and ${errors.length - MAX_DIAGNOSTICS_PER_FILE} more` : ""
66
+ if (file === normalizedFilepath) {
67
+ output += `\n\nLSP errors detected in this file, please fix:\n<diagnostics file="${filepath}">\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n</diagnostics>`
68
+ continue
69
+ }
70
+ if (projectDiagnosticsCount >= MAX_PROJECT_DIAGNOSTICS_FILES) continue
71
+ projectDiagnosticsCount++
72
+ output += `\n\nLSP errors detected in other files:\n<diagnostics file="${file}">\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n</diagnostics>`
73
+ }
74
+
75
+ return {
76
+ title: path.relative(Instance.worktree, filepath),
77
+ metadata: {
78
+ diagnostics,
79
+ filepath,
80
+ exists: exists,
81
+ },
82
+ output,
83
+ }
84
+ },
85
+ })
@@ -0,0 +1,8 @@
1
+ Writes a file to the local filesystem.
2
+
3
+ Usage:
4
+ - This tool will overwrite the existing file if there is one at the provided path.
5
+ - If this is an existing file, you MUST use the Read tool first to read the file's contents. This tool will fail if you did not read the file first.
6
+ - ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
7
+ - NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.
8
+ - Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Creates an AbortController that automatically aborts after a timeout.
3
+ *
4
+ * Uses bind() instead of arrow functions to avoid capturing the surrounding
5
+ * scope in closures. Arrow functions like `() => controller.abort()` capture
6
+ * request bodies and other large objects, preventing GC for the timer lifetime.
7
+ *
8
+ * @param ms Timeout in milliseconds
9
+ * @returns Object with controller, signal, and clearTimeout function
10
+ */
11
+ export function abortAfter(ms: number) {
12
+ const controller = new AbortController()
13
+ const id = setTimeout(controller.abort.bind(controller), ms)
14
+ return {
15
+ controller,
16
+ signal: controller.signal,
17
+ clearTimeout: () => globalThis.clearTimeout(id),
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Combines multiple AbortSignals with a timeout.
23
+ *
24
+ * @param ms Timeout in milliseconds
25
+ * @param signals Additional signals to combine
26
+ * @returns Combined signal that aborts on timeout or when any input signal aborts
27
+ */
28
+ export function abortAfterAny(ms: number, ...signals: AbortSignal[]) {
29
+ const timeout = abortAfter(ms)
30
+ const signal = AbortSignal.any([timeout.signal, ...signals])
31
+ return {
32
+ signal,
33
+ clearTimeout: timeout.clearTimeout,
34
+ }
35
+ }
@@ -0,0 +1,16 @@
1
+ import { $ } from "bun"
2
+ import path from "path"
3
+
4
+ export namespace Archive {
5
+ export async function extractZip(zipPath: string, destDir: string) {
6
+ if (process.platform === "win32") {
7
+ const winZipPath = path.resolve(zipPath)
8
+ const winDestDir = path.resolve(destDir)
9
+ // $global:ProgressPreference suppresses PowerShell's blue progress bar popup
10
+ const cmd = `$global:ProgressPreference = 'SilentlyContinue'; Expand-Archive -Path '${winZipPath}' -DestinationPath '${winDestDir}' -Force`
11
+ await $`powershell -NoProfile -NonInteractive -Command ${cmd}`.quiet()
12
+ } else {
13
+ await $`unzip -o -q ${zipPath} -d ${destDir}`.quiet()
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,19 @@
1
+ export namespace Color {
2
+ export function isValidHex(hex?: string): hex is string {
3
+ if (!hex) return false
4
+ return /^#[0-9a-fA-F]{6}$/.test(hex)
5
+ }
6
+
7
+ export function hexToRgb(hex: string): { r: number; g: number; b: number } {
8
+ const r = parseInt(hex.slice(1, 3), 16)
9
+ const g = parseInt(hex.slice(3, 5), 16)
10
+ const b = parseInt(hex.slice(5, 7), 16)
11
+ return { r, g, b }
12
+ }
13
+
14
+ export function hexToAnsiBold(hex?: string): string | undefined {
15
+ if (!isValidHex(hex)) return undefined
16
+ const { r, g, b } = hexToRgb(hex)
17
+ return `\x1b[38;2;${r};${g};${b}m\x1b[1m`
18
+ }
19
+ }
@@ -0,0 +1,25 @@
1
+ import { AsyncLocalStorage } from "async_hooks"
2
+
3
+ export namespace Context {
4
+ export class NotFound extends Error {
5
+ constructor(public override readonly name: string) {
6
+ super(`No context found for ${name}`)
7
+ }
8
+ }
9
+
10
+ export function create<T>(name: string) {
11
+ const storage = new AsyncLocalStorage<T>()
12
+ return {
13
+ use() {
14
+ const result = storage.getStore()
15
+ if (!result) {
16
+ throw new NotFound(name)
17
+ }
18
+ return result
19
+ },
20
+ provide<R>(value: T, fn: () => R) {
21
+ return storage.run(value, fn)
22
+ },
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,12 @@
1
+ export function defer<T extends () => void | Promise<void>>(
2
+ fn: T,
3
+ ): T extends () => Promise<void> ? { [Symbol.asyncDispose]: () => Promise<void> } : { [Symbol.dispose]: () => void } {
4
+ return {
5
+ [Symbol.dispose]() {
6
+ fn()
7
+ },
8
+ [Symbol.asyncDispose]() {
9
+ return Promise.resolve(fn())
10
+ },
11
+ } as any
12
+ }
@@ -0,0 +1,20 @@
1
+ import { Log } from "./log"
2
+
3
+ export namespace EventLoop {
4
+ export async function wait() {
5
+ return new Promise<void>((resolve) => {
6
+ const check = () => {
7
+ const active = [...(process as any)._getActiveHandles(), ...(process as any)._getActiveRequests()]
8
+ Log.Default.info("eventloop", {
9
+ active,
10
+ })
11
+ if ((process as any)._getActiveHandles().length === 0 && (process as any)._getActiveRequests().length === 0) {
12
+ resolve()
13
+ } else {
14
+ setImmediate(check)
15
+ }
16
+ }
17
+ check()
18
+ })
19
+ }
20
+ }
@@ -0,0 +1,93 @@
1
+ import { realpathSync } from "fs"
2
+ import { dirname, join, relative } from "path"
3
+
4
+ export namespace Filesystem {
5
+ export const exists = (p: string) =>
6
+ Bun.file(p)
7
+ .stat()
8
+ .then(() => true)
9
+ .catch(() => false)
10
+
11
+ export const isDir = (p: string) =>
12
+ Bun.file(p)
13
+ .stat()
14
+ .then((s) => s.isDirectory())
15
+ .catch(() => false)
16
+ /**
17
+ * On Windows, normalize a path to its canonical casing using the filesystem.
18
+ * This is needed because Windows paths are case-insensitive but LSP servers
19
+ * may return paths with different casing than what we send them.
20
+ */
21
+ export function normalizePath(p: string): string {
22
+ if (process.platform !== "win32") return p
23
+ try {
24
+ return realpathSync.native(p)
25
+ } catch {
26
+ return p
27
+ }
28
+ }
29
+ export function overlaps(a: string, b: string) {
30
+ const relA = relative(a, b)
31
+ const relB = relative(b, a)
32
+ return !relA || !relA.startsWith("..") || !relB || !relB.startsWith("..")
33
+ }
34
+
35
+ export function contains(parent: string, child: string) {
36
+ return !relative(parent, child).startsWith("..")
37
+ }
38
+
39
+ export async function findUp(target: string, start: string, stop?: string) {
40
+ let current = start
41
+ const result = []
42
+ while (true) {
43
+ const search = join(current, target)
44
+ if (await exists(search)) result.push(search)
45
+ if (stop === current) break
46
+ const parent = dirname(current)
47
+ if (parent === current) break
48
+ current = parent
49
+ }
50
+ return result
51
+ }
52
+
53
+ export async function* up(options: { targets: string[]; start: string; stop?: string }) {
54
+ const { targets, start, stop } = options
55
+ let current = start
56
+ while (true) {
57
+ for (const target of targets) {
58
+ const search = join(current, target)
59
+ if (await exists(search)) yield search
60
+ }
61
+ if (stop === current) break
62
+ const parent = dirname(current)
63
+ if (parent === current) break
64
+ current = parent
65
+ }
66
+ }
67
+
68
+ export async function globUp(pattern: string, start: string, stop?: string) {
69
+ let current = start
70
+ const result = []
71
+ while (true) {
72
+ try {
73
+ const glob = new Bun.Glob(pattern)
74
+ for await (const match of glob.scan({
75
+ cwd: current,
76
+ absolute: true,
77
+ onlyFiles: true,
78
+ followSymlinks: true,
79
+ dot: true,
80
+ })) {
81
+ result.push(match)
82
+ }
83
+ } catch {
84
+ // Skip invalid glob patterns
85
+ }
86
+ if (stop === current) break
87
+ const parent = dirname(current)
88
+ if (parent === current) break
89
+ current = parent
90
+ }
91
+ return result
92
+ }
93
+ }
package/src/util/fn.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { z } from "zod"
2
+
3
+ export function fn<T extends z.ZodType, Result>(schema: T, cb: (input: z.infer<T>) => Result) {
4
+ const result = (input: z.infer<T>) => {
5
+ const parsed = schema.parse(input)
6
+ return cb(parsed)
7
+ }
8
+ result.force = (input: z.infer<T>) => cb(input)
9
+ result.schema = schema
10
+ return result
11
+ }
@@ -0,0 +1,20 @@
1
+ export function formatDuration(secs: number) {
2
+ if (secs <= 0) return ""
3
+ if (secs < 60) return `${secs}s`
4
+ if (secs < 3600) {
5
+ const mins = Math.floor(secs / 60)
6
+ const remaining = secs % 60
7
+ return remaining > 0 ? `${mins}m ${remaining}s` : `${mins}m`
8
+ }
9
+ if (secs < 86400) {
10
+ const hours = Math.floor(secs / 3600)
11
+ const remaining = Math.floor((secs % 3600) / 60)
12
+ return remaining > 0 ? `${hours}h ${remaining}m` : `${hours}h`
13
+ }
14
+ if (secs < 604800) {
15
+ const days = Math.floor(secs / 86400)
16
+ return days === 1 ? "~1 day" : `~${days} days`
17
+ }
18
+ const weeks = Math.floor(secs / 604800)
19
+ return weeks === 1 ? "~1 week" : `~${weeks} weeks`
20
+ }
@@ -0,0 +1,3 @@
1
+ export function iife<T>(fn: () => T) {
2
+ return fn()
3
+ }
@@ -0,0 +1,103 @@
1
+ import { isDeepEqual } from "remeda"
2
+ import type { ParsedKey } from "@opentui/core"
3
+
4
+ export namespace Keybind {
5
+ /**
6
+ * Keybind info derived from OpenTUI's ParsedKey with our custom `leader` field.
7
+ * This ensures type compatibility and catches missing fields at compile time.
8
+ */
9
+ export type Info = Pick<ParsedKey, "name" | "ctrl" | "meta" | "shift" | "super"> & {
10
+ leader: boolean // our custom field
11
+ }
12
+
13
+ export function match(a: Info | undefined, b: Info): boolean {
14
+ if (!a) return false
15
+ const normalizedA = { ...a, super: a.super ?? false }
16
+ const normalizedB = { ...b, super: b.super ?? false }
17
+ return isDeepEqual(normalizedA, normalizedB)
18
+ }
19
+
20
+ /**
21
+ * Convert OpenTUI's ParsedKey to our Keybind.Info format.
22
+ * This helper ensures all required fields are present and avoids manual object creation.
23
+ */
24
+ export function fromParsedKey(key: ParsedKey, leader = false): Info {
25
+ return {
26
+ name: key.name,
27
+ ctrl: key.ctrl,
28
+ meta: key.meta,
29
+ shift: key.shift,
30
+ super: key.super ?? false,
31
+ leader,
32
+ }
33
+ }
34
+
35
+ export function toString(info: Info | undefined): string {
36
+ if (!info) return ""
37
+ const parts: string[] = []
38
+
39
+ if (info.ctrl) parts.push("ctrl")
40
+ if (info.meta) parts.push("alt")
41
+ if (info.super) parts.push("super")
42
+ if (info.shift) parts.push("shift")
43
+ if (info.name) {
44
+ if (info.name === "delete") parts.push("del")
45
+ else parts.push(info.name)
46
+ }
47
+
48
+ let result = parts.join("+")
49
+
50
+ if (info.leader) {
51
+ result = result ? `<leader> ${result}` : `<leader>`
52
+ }
53
+
54
+ return result
55
+ }
56
+
57
+ export function parse(key: string): Info[] {
58
+ if (key === "none") return []
59
+
60
+ return key.split(",").map((combo) => {
61
+ // Handle <leader> syntax by replacing with leader+
62
+ const normalized = combo.replace(/<leader>/g, "leader+")
63
+ const parts = normalized.toLowerCase().split("+")
64
+ const info: Info = {
65
+ ctrl: false,
66
+ meta: false,
67
+ shift: false,
68
+ leader: false,
69
+ name: "",
70
+ }
71
+
72
+ for (const part of parts) {
73
+ switch (part) {
74
+ case "ctrl":
75
+ info.ctrl = true
76
+ break
77
+ case "alt":
78
+ case "meta":
79
+ case "option":
80
+ info.meta = true
81
+ break
82
+ case "super":
83
+ info.super = true
84
+ break
85
+ case "shift":
86
+ info.shift = true
87
+ break
88
+ case "leader":
89
+ info.leader = true
90
+ break
91
+ case "esc":
92
+ info.name = "escape"
93
+ break
94
+ default:
95
+ info.name = part
96
+ break
97
+ }
98
+ }
99
+
100
+ return info
101
+ })
102
+ }
103
+ }
@@ -0,0 +1,18 @@
1
+ export function lazy<T>(fn: () => T) {
2
+ let value: T | undefined
3
+ let loaded = false
4
+
5
+ const result = (): T => {
6
+ if (loaded) return value as T
7
+ loaded = true
8
+ value = fn()
9
+ return value as T
10
+ }
11
+
12
+ result.reset = () => {
13
+ loaded = false
14
+ value = undefined
15
+ }
16
+
17
+ return result
18
+ }