@oh-my-pi/pi-coding-agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) hide show
  1. package/CHANGELOG.md +1629 -0
  2. package/README.md +1041 -0
  3. package/docs/compaction.md +403 -0
  4. package/docs/config-usage.md +113 -0
  5. package/docs/custom-tools.md +541 -0
  6. package/docs/extension-loading.md +1004 -0
  7. package/docs/hooks.md +867 -0
  8. package/docs/rpc.md +1040 -0
  9. package/docs/sdk.md +994 -0
  10. package/docs/session-tree-plan.md +441 -0
  11. package/docs/session.md +240 -0
  12. package/docs/skills.md +290 -0
  13. package/docs/theme.md +670 -0
  14. package/docs/tree.md +197 -0
  15. package/docs/tui.md +341 -0
  16. package/examples/README.md +21 -0
  17. package/examples/custom-tools/README.md +124 -0
  18. package/examples/custom-tools/hello/index.ts +20 -0
  19. package/examples/custom-tools/question/index.ts +84 -0
  20. package/examples/custom-tools/subagent/README.md +172 -0
  21. package/examples/custom-tools/subagent/agents/planner.md +37 -0
  22. package/examples/custom-tools/subagent/agents/scout.md +50 -0
  23. package/examples/custom-tools/subagent/agents/worker.md +24 -0
  24. package/examples/custom-tools/subagent/agents.ts +156 -0
  25. package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
  26. package/examples/custom-tools/subagent/commands/implement.md +10 -0
  27. package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
  28. package/examples/custom-tools/subagent/index.ts +1002 -0
  29. package/examples/custom-tools/todo/index.ts +212 -0
  30. package/examples/hooks/README.md +56 -0
  31. package/examples/hooks/auto-commit-on-exit.ts +49 -0
  32. package/examples/hooks/confirm-destructive.ts +59 -0
  33. package/examples/hooks/custom-compaction.ts +116 -0
  34. package/examples/hooks/dirty-repo-guard.ts +52 -0
  35. package/examples/hooks/file-trigger.ts +41 -0
  36. package/examples/hooks/git-checkpoint.ts +53 -0
  37. package/examples/hooks/handoff.ts +150 -0
  38. package/examples/hooks/permission-gate.ts +34 -0
  39. package/examples/hooks/protected-paths.ts +30 -0
  40. package/examples/hooks/qna.ts +119 -0
  41. package/examples/hooks/snake.ts +343 -0
  42. package/examples/hooks/status-line.ts +40 -0
  43. package/examples/sdk/01-minimal.ts +22 -0
  44. package/examples/sdk/02-custom-model.ts +49 -0
  45. package/examples/sdk/03-custom-prompt.ts +44 -0
  46. package/examples/sdk/04-skills.ts +44 -0
  47. package/examples/sdk/05-tools.ts +90 -0
  48. package/examples/sdk/06-hooks.ts +61 -0
  49. package/examples/sdk/07-context-files.ts +36 -0
  50. package/examples/sdk/08-slash-commands.ts +42 -0
  51. package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
  52. package/examples/sdk/10-settings.ts +38 -0
  53. package/examples/sdk/11-sessions.ts +48 -0
  54. package/examples/sdk/12-full-control.ts +95 -0
  55. package/examples/sdk/README.md +154 -0
  56. package/package.json +89 -0
  57. package/src/bun-imports.d.ts +16 -0
  58. package/src/capability/context-file.ts +40 -0
  59. package/src/capability/extension.ts +48 -0
  60. package/src/capability/hook.ts +40 -0
  61. package/src/capability/index.ts +616 -0
  62. package/src/capability/instruction.ts +37 -0
  63. package/src/capability/mcp.ts +52 -0
  64. package/src/capability/prompt.ts +35 -0
  65. package/src/capability/rule.ts +56 -0
  66. package/src/capability/settings.ts +35 -0
  67. package/src/capability/skill.ts +49 -0
  68. package/src/capability/slash-command.ts +40 -0
  69. package/src/capability/system-prompt.ts +35 -0
  70. package/src/capability/tool.ts +38 -0
  71. package/src/capability/types.ts +166 -0
  72. package/src/cli/args.ts +259 -0
  73. package/src/cli/file-processor.ts +121 -0
  74. package/src/cli/list-models.ts +104 -0
  75. package/src/cli/plugin-cli.ts +661 -0
  76. package/src/cli/session-picker.ts +41 -0
  77. package/src/cli/update-cli.ts +274 -0
  78. package/src/cli.ts +10 -0
  79. package/src/config.ts +391 -0
  80. package/src/core/agent-session.ts +2178 -0
  81. package/src/core/auth-storage.ts +258 -0
  82. package/src/core/bash-executor.ts +197 -0
  83. package/src/core/compaction/branch-summarization.ts +315 -0
  84. package/src/core/compaction/compaction.ts +664 -0
  85. package/src/core/compaction/index.ts +7 -0
  86. package/src/core/compaction/utils.ts +153 -0
  87. package/src/core/custom-commands/bundled/review/index.ts +156 -0
  88. package/src/core/custom-commands/index.ts +15 -0
  89. package/src/core/custom-commands/loader.ts +226 -0
  90. package/src/core/custom-commands/types.ts +112 -0
  91. package/src/core/custom-tools/index.ts +22 -0
  92. package/src/core/custom-tools/loader.ts +248 -0
  93. package/src/core/custom-tools/types.ts +185 -0
  94. package/src/core/custom-tools/wrapper.ts +29 -0
  95. package/src/core/exec.ts +139 -0
  96. package/src/core/export-html/index.ts +159 -0
  97. package/src/core/export-html/template.css +774 -0
  98. package/src/core/export-html/template.generated.ts +2 -0
  99. package/src/core/export-html/template.html +45 -0
  100. package/src/core/export-html/template.js +1185 -0
  101. package/src/core/export-html/template.macro.ts +24 -0
  102. package/src/core/file-mentions.ts +54 -0
  103. package/src/core/hooks/index.ts +16 -0
  104. package/src/core/hooks/loader.ts +288 -0
  105. package/src/core/hooks/runner.ts +434 -0
  106. package/src/core/hooks/tool-wrapper.ts +98 -0
  107. package/src/core/hooks/types.ts +770 -0
  108. package/src/core/index.ts +53 -0
  109. package/src/core/logger.ts +112 -0
  110. package/src/core/mcp/client.ts +185 -0
  111. package/src/core/mcp/config.ts +248 -0
  112. package/src/core/mcp/index.ts +45 -0
  113. package/src/core/mcp/loader.ts +99 -0
  114. package/src/core/mcp/manager.ts +235 -0
  115. package/src/core/mcp/tool-bridge.ts +156 -0
  116. package/src/core/mcp/transports/http.ts +316 -0
  117. package/src/core/mcp/transports/index.ts +6 -0
  118. package/src/core/mcp/transports/stdio.ts +252 -0
  119. package/src/core/mcp/types.ts +228 -0
  120. package/src/core/messages.ts +211 -0
  121. package/src/core/model-registry.ts +334 -0
  122. package/src/core/model-resolver.ts +494 -0
  123. package/src/core/plugins/doctor.ts +67 -0
  124. package/src/core/plugins/index.ts +38 -0
  125. package/src/core/plugins/installer.ts +189 -0
  126. package/src/core/plugins/loader.ts +339 -0
  127. package/src/core/plugins/manager.ts +672 -0
  128. package/src/core/plugins/parser.ts +105 -0
  129. package/src/core/plugins/paths.ts +37 -0
  130. package/src/core/plugins/types.ts +190 -0
  131. package/src/core/sdk.ts +900 -0
  132. package/src/core/session-manager.ts +1837 -0
  133. package/src/core/settings-manager.ts +860 -0
  134. package/src/core/skills.ts +352 -0
  135. package/src/core/slash-commands.ts +132 -0
  136. package/src/core/system-prompt.ts +442 -0
  137. package/src/core/timings.ts +25 -0
  138. package/src/core/title-generator.ts +110 -0
  139. package/src/core/tools/ask.ts +193 -0
  140. package/src/core/tools/bash-interceptor.ts +120 -0
  141. package/src/core/tools/bash.ts +91 -0
  142. package/src/core/tools/context.ts +32 -0
  143. package/src/core/tools/edit-diff.ts +487 -0
  144. package/src/core/tools/edit.ts +140 -0
  145. package/src/core/tools/exa/company.ts +59 -0
  146. package/src/core/tools/exa/index.ts +63 -0
  147. package/src/core/tools/exa/linkedin.ts +59 -0
  148. package/src/core/tools/exa/mcp-client.ts +368 -0
  149. package/src/core/tools/exa/render.ts +200 -0
  150. package/src/core/tools/exa/researcher.ts +90 -0
  151. package/src/core/tools/exa/search.ts +338 -0
  152. package/src/core/tools/exa/types.ts +167 -0
  153. package/src/core/tools/exa/websets.ts +248 -0
  154. package/src/core/tools/find.ts +244 -0
  155. package/src/core/tools/grep.ts +584 -0
  156. package/src/core/tools/index.ts +283 -0
  157. package/src/core/tools/ls.ts +142 -0
  158. package/src/core/tools/lsp/client.ts +767 -0
  159. package/src/core/tools/lsp/clients/biome-client.ts +207 -0
  160. package/src/core/tools/lsp/clients/index.ts +49 -0
  161. package/src/core/tools/lsp/clients/lsp-linter-client.ts +98 -0
  162. package/src/core/tools/lsp/config.ts +845 -0
  163. package/src/core/tools/lsp/edits.ts +110 -0
  164. package/src/core/tools/lsp/index.ts +1364 -0
  165. package/src/core/tools/lsp/render.ts +560 -0
  166. package/src/core/tools/lsp/rust-analyzer.ts +145 -0
  167. package/src/core/tools/lsp/types.ts +495 -0
  168. package/src/core/tools/lsp/utils.ts +526 -0
  169. package/src/core/tools/notebook.ts +182 -0
  170. package/src/core/tools/output.ts +198 -0
  171. package/src/core/tools/path-utils.ts +61 -0
  172. package/src/core/tools/read.ts +507 -0
  173. package/src/core/tools/renderers.ts +820 -0
  174. package/src/core/tools/review.ts +275 -0
  175. package/src/core/tools/rulebook.ts +124 -0
  176. package/src/core/tools/task/agents.ts +158 -0
  177. package/src/core/tools/task/artifacts.ts +114 -0
  178. package/src/core/tools/task/commands.ts +157 -0
  179. package/src/core/tools/task/discovery.ts +217 -0
  180. package/src/core/tools/task/executor.ts +531 -0
  181. package/src/core/tools/task/index.ts +548 -0
  182. package/src/core/tools/task/model-resolver.ts +176 -0
  183. package/src/core/tools/task/parallel.ts +38 -0
  184. package/src/core/tools/task/render.ts +502 -0
  185. package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
  186. package/src/core/tools/task/types.ts +142 -0
  187. package/src/core/tools/truncate.ts +265 -0
  188. package/src/core/tools/web-fetch.ts +2511 -0
  189. package/src/core/tools/web-search/auth.ts +199 -0
  190. package/src/core/tools/web-search/index.ts +583 -0
  191. package/src/core/tools/web-search/providers/anthropic.ts +198 -0
  192. package/src/core/tools/web-search/providers/exa.ts +196 -0
  193. package/src/core/tools/web-search/providers/perplexity.ts +195 -0
  194. package/src/core/tools/web-search/render.ts +372 -0
  195. package/src/core/tools/web-search/types.ts +180 -0
  196. package/src/core/tools/write.ts +63 -0
  197. package/src/core/ttsr.ts +211 -0
  198. package/src/core/utils.ts +187 -0
  199. package/src/discovery/agents-md.ts +75 -0
  200. package/src/discovery/builtin.ts +647 -0
  201. package/src/discovery/claude.ts +623 -0
  202. package/src/discovery/cline.ts +104 -0
  203. package/src/discovery/codex.ts +571 -0
  204. package/src/discovery/cursor.ts +266 -0
  205. package/src/discovery/gemini.ts +368 -0
  206. package/src/discovery/github.ts +120 -0
  207. package/src/discovery/helpers.test.ts +127 -0
  208. package/src/discovery/helpers.ts +249 -0
  209. package/src/discovery/index.ts +84 -0
  210. package/src/discovery/mcp-json.ts +127 -0
  211. package/src/discovery/vscode.ts +99 -0
  212. package/src/discovery/windsurf.ts +219 -0
  213. package/src/index.ts +192 -0
  214. package/src/main.ts +507 -0
  215. package/src/migrations.ts +156 -0
  216. package/src/modes/cleanup.ts +23 -0
  217. package/src/modes/index.ts +48 -0
  218. package/src/modes/interactive/components/armin.ts +382 -0
  219. package/src/modes/interactive/components/assistant-message.ts +86 -0
  220. package/src/modes/interactive/components/bash-execution.ts +199 -0
  221. package/src/modes/interactive/components/bordered-loader.ts +41 -0
  222. package/src/modes/interactive/components/branch-summary-message.ts +42 -0
  223. package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
  224. package/src/modes/interactive/components/custom-editor.ts +122 -0
  225. package/src/modes/interactive/components/diff.ts +147 -0
  226. package/src/modes/interactive/components/dynamic-border.ts +25 -0
  227. package/src/modes/interactive/components/extensions/extension-dashboard.ts +296 -0
  228. package/src/modes/interactive/components/extensions/extension-list.ts +479 -0
  229. package/src/modes/interactive/components/extensions/index.ts +9 -0
  230. package/src/modes/interactive/components/extensions/inspector-panel.ts +313 -0
  231. package/src/modes/interactive/components/extensions/state-manager.ts +558 -0
  232. package/src/modes/interactive/components/extensions/types.ts +191 -0
  233. package/src/modes/interactive/components/hook-editor.ts +117 -0
  234. package/src/modes/interactive/components/hook-input.ts +64 -0
  235. package/src/modes/interactive/components/hook-message.ts +96 -0
  236. package/src/modes/interactive/components/hook-selector.ts +91 -0
  237. package/src/modes/interactive/components/model-selector.ts +560 -0
  238. package/src/modes/interactive/components/oauth-selector.ts +136 -0
  239. package/src/modes/interactive/components/plugin-settings.ts +481 -0
  240. package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
  241. package/src/modes/interactive/components/session-selector.ts +220 -0
  242. package/src/modes/interactive/components/settings-defs.ts +597 -0
  243. package/src/modes/interactive/components/settings-selector.ts +545 -0
  244. package/src/modes/interactive/components/show-images-selector.ts +45 -0
  245. package/src/modes/interactive/components/status-line/index.ts +4 -0
  246. package/src/modes/interactive/components/status-line/presets.ts +94 -0
  247. package/src/modes/interactive/components/status-line/segments.ts +350 -0
  248. package/src/modes/interactive/components/status-line/separators.ts +55 -0
  249. package/src/modes/interactive/components/status-line/types.ts +81 -0
  250. package/src/modes/interactive/components/status-line-segment-editor.ts +357 -0
  251. package/src/modes/interactive/components/status-line.ts +384 -0
  252. package/src/modes/interactive/components/theme-selector.ts +62 -0
  253. package/src/modes/interactive/components/thinking-selector.ts +64 -0
  254. package/src/modes/interactive/components/tool-execution.ts +946 -0
  255. package/src/modes/interactive/components/tree-selector.ts +877 -0
  256. package/src/modes/interactive/components/ttsr-notification.ts +82 -0
  257. package/src/modes/interactive/components/user-message-selector.ts +159 -0
  258. package/src/modes/interactive/components/user-message.ts +18 -0
  259. package/src/modes/interactive/components/visual-truncate.ts +50 -0
  260. package/src/modes/interactive/components/welcome.ts +228 -0
  261. package/src/modes/interactive/interactive-mode.ts +2669 -0
  262. package/src/modes/interactive/theme/dark.json +102 -0
  263. package/src/modes/interactive/theme/defaults/dark-arctic.json +111 -0
  264. package/src/modes/interactive/theme/defaults/dark-catppuccin.json +106 -0
  265. package/src/modes/interactive/theme/defaults/dark-cyberpunk.json +109 -0
  266. package/src/modes/interactive/theme/defaults/dark-dracula.json +105 -0
  267. package/src/modes/interactive/theme/defaults/dark-forest.json +103 -0
  268. package/src/modes/interactive/theme/defaults/dark-github.json +112 -0
  269. package/src/modes/interactive/theme/defaults/dark-gruvbox.json +119 -0
  270. package/src/modes/interactive/theme/defaults/dark-monochrome.json +101 -0
  271. package/src/modes/interactive/theme/defaults/dark-monokai.json +105 -0
  272. package/src/modes/interactive/theme/defaults/dark-nord.json +104 -0
  273. package/src/modes/interactive/theme/defaults/dark-ocean.json +108 -0
  274. package/src/modes/interactive/theme/defaults/dark-one.json +107 -0
  275. package/src/modes/interactive/theme/defaults/dark-retro.json +99 -0
  276. package/src/modes/interactive/theme/defaults/dark-rose-pine.json +95 -0
  277. package/src/modes/interactive/theme/defaults/dark-solarized.json +96 -0
  278. package/src/modes/interactive/theme/defaults/dark-sunset.json +106 -0
  279. package/src/modes/interactive/theme/defaults/dark-synthwave.json +102 -0
  280. package/src/modes/interactive/theme/defaults/dark-tokyo-night.json +108 -0
  281. package/src/modes/interactive/theme/defaults/index.ts +67 -0
  282. package/src/modes/interactive/theme/defaults/light-arctic.json +106 -0
  283. package/src/modes/interactive/theme/defaults/light-catppuccin.json +105 -0
  284. package/src/modes/interactive/theme/defaults/light-cyberpunk.json +103 -0
  285. package/src/modes/interactive/theme/defaults/light-forest.json +107 -0
  286. package/src/modes/interactive/theme/defaults/light-github.json +114 -0
  287. package/src/modes/interactive/theme/defaults/light-gruvbox.json +115 -0
  288. package/src/modes/interactive/theme/defaults/light-monochrome.json +100 -0
  289. package/src/modes/interactive/theme/defaults/light-ocean.json +106 -0
  290. package/src/modes/interactive/theme/defaults/light-one.json +105 -0
  291. package/src/modes/interactive/theme/defaults/light-retro.json +105 -0
  292. package/src/modes/interactive/theme/defaults/light-solarized.json +101 -0
  293. package/src/modes/interactive/theme/defaults/light-sunset.json +106 -0
  294. package/src/modes/interactive/theme/defaults/light-synthwave.json +105 -0
  295. package/src/modes/interactive/theme/defaults/light-tokyo-night.json +118 -0
  296. package/src/modes/interactive/theme/light.json +99 -0
  297. package/src/modes/interactive/theme/theme-schema.json +424 -0
  298. package/src/modes/interactive/theme/theme.ts +2211 -0
  299. package/src/modes/print-mode.ts +163 -0
  300. package/src/modes/rpc/rpc-client.ts +527 -0
  301. package/src/modes/rpc/rpc-mode.ts +494 -0
  302. package/src/modes/rpc/rpc-types.ts +203 -0
  303. package/src/prompts/architect-plan.md +10 -0
  304. package/src/prompts/branch-summary-preamble.md +3 -0
  305. package/src/prompts/branch-summary.md +28 -0
  306. package/src/prompts/browser.md +71 -0
  307. package/src/prompts/compaction-summary.md +34 -0
  308. package/src/prompts/compaction-turn-prefix.md +16 -0
  309. package/src/prompts/compaction-update-summary.md +41 -0
  310. package/src/prompts/explore.md +82 -0
  311. package/src/prompts/implement-with-critic.md +11 -0
  312. package/src/prompts/implement.md +11 -0
  313. package/src/prompts/init.md +30 -0
  314. package/src/prompts/plan.md +54 -0
  315. package/src/prompts/reviewer.md +81 -0
  316. package/src/prompts/summarization-system.md +3 -0
  317. package/src/prompts/system-prompt.md +27 -0
  318. package/src/prompts/task.md +56 -0
  319. package/src/prompts/title-system.md +8 -0
  320. package/src/prompts/tools/ask.md +24 -0
  321. package/src/prompts/tools/bash.md +23 -0
  322. package/src/prompts/tools/edit.md +9 -0
  323. package/src/prompts/tools/find.md +6 -0
  324. package/src/prompts/tools/grep.md +12 -0
  325. package/src/prompts/tools/lsp.md +14 -0
  326. package/src/prompts/tools/output.md +23 -0
  327. package/src/prompts/tools/read.md +25 -0
  328. package/src/prompts/tools/web-fetch.md +8 -0
  329. package/src/prompts/tools/web-search.md +10 -0
  330. package/src/prompts/tools/write.md +10 -0
  331. package/src/utils/changelog.ts +99 -0
  332. package/src/utils/clipboard.ts +265 -0
  333. package/src/utils/fuzzy.ts +108 -0
  334. package/src/utils/mime.ts +30 -0
  335. package/src/utils/shell-snapshot.ts +218 -0
  336. package/src/utils/shell.ts +364 -0
  337. package/src/utils/tools-manager.ts +265 -0
@@ -0,0 +1,494 @@
1
+ /**
2
+ * Model resolution, scoping, and initial selection
3
+ */
4
+
5
+ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
6
+ import { type Api, type KnownProvider, type Model, modelsAreEqual } from "@oh-my-pi/pi-ai";
7
+ import chalk from "chalk";
8
+ import { minimatch } from "minimatch";
9
+ import { isValidThinkingLevel } from "../cli/args";
10
+ import type { ModelRegistry } from "./model-registry";
11
+
12
+ /** Default model IDs for each known provider */
13
+ export const defaultModelPerProvider: Record<KnownProvider, string> = {
14
+ anthropic: "claude-sonnet-4-5",
15
+ openai: "gpt-5.1-codex",
16
+ google: "gemini-2.5-pro",
17
+ "google-gemini-cli": "gemini-2.5-pro",
18
+ "google-antigravity": "gemini-3-pro-high",
19
+ "github-copilot": "gpt-4o",
20
+ openrouter: "openai/gpt-5.1-codex",
21
+ xai: "grok-4-fast-non-reasoning",
22
+ groq: "openai/gpt-oss-120b",
23
+ cerebras: "zai-glm-4.6",
24
+ zai: "glm-4.6",
25
+ mistral: "devstral-medium-latest",
26
+ };
27
+
28
+ export interface ScopedModel {
29
+ model: Model<Api>;
30
+ thinkingLevel: ThinkingLevel;
31
+ }
32
+
33
+ /** Priority chain for auto-discovering smol/fast models */
34
+ export const SMOL_MODEL_PRIORITY = ["claude-haiku-4-5", "haiku", "flash", "mini"];
35
+
36
+ /** Priority chain for auto-discovering slow/comprehensive models (reasoning, codex) */
37
+ export const SLOW_MODEL_PRIORITY = ["gpt-5.2-codex", "gpt-5.2", "codex", "gpt", "opus", "pro"];
38
+
39
+ /**
40
+ * Parse a model string in "provider/modelId" format.
41
+ * Returns undefined if the format is invalid.
42
+ */
43
+ export function parseModelString(modelStr: string): { provider: string; id: string } | undefined {
44
+ const slashIdx = modelStr.indexOf("/");
45
+ if (slashIdx <= 0) return undefined;
46
+ return { provider: modelStr.slice(0, slashIdx), id: modelStr.slice(slashIdx + 1) };
47
+ }
48
+
49
+ /**
50
+ * Format a model as "provider/modelId" string.
51
+ */
52
+ export function formatModelString(model: Model<Api>): string {
53
+ return `${model.provider}/${model.id}`;
54
+ }
55
+
56
+ /**
57
+ * Helper to check if a model ID looks like an alias (no date suffix)
58
+ * Dates are typically in format: -20241022 or -20250929
59
+ */
60
+ function isAlias(id: string): boolean {
61
+ // Check if ID ends with -latest
62
+ if (id.endsWith("-latest")) return true;
63
+
64
+ // Check if ID ends with a date pattern (-YYYYMMDD)
65
+ const datePattern = /-\d{8}$/;
66
+ return !datePattern.test(id);
67
+ }
68
+
69
+ /**
70
+ * Try to match a pattern to a model from the available models list.
71
+ * Returns the matched model or undefined if no match found.
72
+ */
73
+ function tryMatchModel(modelPattern: string, availableModels: Model<Api>[]): Model<Api> | undefined {
74
+ // Check for provider/modelId format (provider is everything before the first /)
75
+ const slashIndex = modelPattern.indexOf("/");
76
+ if (slashIndex !== -1) {
77
+ const provider = modelPattern.substring(0, slashIndex);
78
+ const modelId = modelPattern.substring(slashIndex + 1);
79
+ const providerMatch = availableModels.find(
80
+ (m) => m.provider.toLowerCase() === provider.toLowerCase() && m.id.toLowerCase() === modelId.toLowerCase(),
81
+ );
82
+ if (providerMatch) {
83
+ return providerMatch;
84
+ }
85
+ // No exact provider/model match - fall through to other matching
86
+ }
87
+
88
+ // Check for exact ID match (case-insensitive)
89
+ const exactMatch = availableModels.find((m) => m.id.toLowerCase() === modelPattern.toLowerCase());
90
+ if (exactMatch) {
91
+ return exactMatch;
92
+ }
93
+
94
+ // No exact match - fall back to partial matching
95
+ const matches = availableModels.filter(
96
+ (m) =>
97
+ m.id.toLowerCase().includes(modelPattern.toLowerCase()) ||
98
+ m.name?.toLowerCase().includes(modelPattern.toLowerCase()),
99
+ );
100
+
101
+ if (matches.length === 0) {
102
+ return undefined;
103
+ }
104
+
105
+ // Separate into aliases and dated versions
106
+ const aliases = matches.filter((m) => isAlias(m.id));
107
+ const datedVersions = matches.filter((m) => !isAlias(m.id));
108
+
109
+ if (aliases.length > 0) {
110
+ // Prefer alias - if multiple aliases, pick the one that sorts highest
111
+ aliases.sort((a, b) => b.id.localeCompare(a.id));
112
+ return aliases[0];
113
+ } else {
114
+ // No alias found, pick latest dated version
115
+ datedVersions.sort((a, b) => b.id.localeCompare(a.id));
116
+ return datedVersions[0];
117
+ }
118
+ }
119
+
120
+ export interface ParsedModelResult {
121
+ model: Model<Api> | undefined;
122
+ thinkingLevel: ThinkingLevel;
123
+ warning: string | undefined;
124
+ }
125
+
126
+ /**
127
+ * Parse a pattern to extract model and thinking level.
128
+ * Handles models with colons in their IDs (e.g., OpenRouter's :exacto suffix).
129
+ *
130
+ * Algorithm:
131
+ * 1. Try to match full pattern as a model
132
+ * 2. If found, return it with "off" thinking level
133
+ * 3. If not found and has colons, split on last colon:
134
+ * - If suffix is valid thinking level, use it and recurse on prefix
135
+ * - If suffix is invalid, warn and recurse on prefix with "off"
136
+ *
137
+ * @internal Exported for testing
138
+ */
139
+ export function parseModelPattern(pattern: string, availableModels: Model<Api>[]): ParsedModelResult {
140
+ // Try exact match first
141
+ const exactMatch = tryMatchModel(pattern, availableModels);
142
+ if (exactMatch) {
143
+ return { model: exactMatch, thinkingLevel: "off", warning: undefined };
144
+ }
145
+
146
+ // No match - try splitting on last colon if present
147
+ const lastColonIndex = pattern.lastIndexOf(":");
148
+ if (lastColonIndex === -1) {
149
+ // No colons, pattern simply doesn't match any model
150
+ return { model: undefined, thinkingLevel: "off", warning: undefined };
151
+ }
152
+
153
+ const prefix = pattern.substring(0, lastColonIndex);
154
+ const suffix = pattern.substring(lastColonIndex + 1);
155
+
156
+ if (isValidThinkingLevel(suffix)) {
157
+ // Valid thinking level - recurse on prefix and use this level
158
+ const result = parseModelPattern(prefix, availableModels);
159
+ if (result.model) {
160
+ // Only use this thinking level if no warning from inner recursion
161
+ // (if there was an invalid suffix deeper, we already have "off")
162
+ return {
163
+ model: result.model,
164
+ thinkingLevel: result.warning ? "off" : suffix,
165
+ warning: result.warning,
166
+ };
167
+ }
168
+ return result;
169
+ } else {
170
+ // Invalid suffix - recurse on prefix with "off" and warn
171
+ const result = parseModelPattern(prefix, availableModels);
172
+ if (result.model) {
173
+ return {
174
+ model: result.model,
175
+ thinkingLevel: "off",
176
+ warning: `Invalid thinking level "${suffix}" in pattern "${pattern}". Using "off" instead.`,
177
+ };
178
+ }
179
+ return result;
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Resolve model patterns to actual Model objects with optional thinking levels
185
+ * Format: "pattern:level" where :level is optional
186
+ * For each pattern, finds all matching models and picks the best version:
187
+ * 1. Prefer alias (e.g., claude-sonnet-4-5) over dated versions (claude-sonnet-4-5-20250929)
188
+ * 2. If no alias, pick the latest dated version
189
+ *
190
+ * Supports models with colons in their IDs (e.g., OpenRouter's model:exacto).
191
+ * The algorithm tries to match the full pattern first, then progressively
192
+ * strips colon-suffixes to find a match.
193
+ */
194
+ export async function resolveModelScope(patterns: string[], modelRegistry: ModelRegistry): Promise<ScopedModel[]> {
195
+ const availableModels = await modelRegistry.getAvailable();
196
+ const scopedModels: ScopedModel[] = [];
197
+
198
+ for (const pattern of patterns) {
199
+ // Check if pattern contains glob characters
200
+ if (pattern.includes("*") || pattern.includes("?") || pattern.includes("[")) {
201
+ // Extract optional thinking level suffix (e.g., "provider/*:high")
202
+ const colonIdx = pattern.lastIndexOf(":");
203
+ let globPattern = pattern;
204
+ let thinkingLevel: ThinkingLevel = "off";
205
+
206
+ if (colonIdx !== -1) {
207
+ const suffix = pattern.substring(colonIdx + 1);
208
+ if (isValidThinkingLevel(suffix)) {
209
+ thinkingLevel = suffix;
210
+ globPattern = pattern.substring(0, colonIdx);
211
+ }
212
+ }
213
+
214
+ // Match against "provider/modelId" format OR just model ID
215
+ // This allows "*sonnet*" to match without requiring "anthropic/*sonnet*"
216
+ const matchingModels = availableModels.filter((m) => {
217
+ const fullId = `${m.provider}/${m.id}`;
218
+ return minimatch(fullId, globPattern, { nocase: true }) || minimatch(m.id, globPattern, { nocase: true });
219
+ });
220
+
221
+ if (matchingModels.length === 0) {
222
+ console.warn(chalk.yellow(`Warning: No models match pattern "${pattern}"`));
223
+ continue;
224
+ }
225
+
226
+ for (const model of matchingModels) {
227
+ if (!scopedModels.find((sm) => modelsAreEqual(sm.model, model))) {
228
+ scopedModels.push({ model, thinkingLevel });
229
+ }
230
+ }
231
+ continue;
232
+ }
233
+
234
+ const { model, thinkingLevel, warning } = parseModelPattern(pattern, availableModels);
235
+
236
+ if (warning) {
237
+ console.warn(chalk.yellow(`Warning: ${warning}`));
238
+ }
239
+
240
+ if (!model) {
241
+ console.warn(chalk.yellow(`Warning: No models match pattern "${pattern}"`));
242
+ continue;
243
+ }
244
+
245
+ // Avoid duplicates
246
+ if (!scopedModels.find((sm) => modelsAreEqual(sm.model, model))) {
247
+ scopedModels.push({ model, thinkingLevel });
248
+ }
249
+ }
250
+
251
+ return scopedModels;
252
+ }
253
+
254
+ export interface InitialModelResult {
255
+ model: Model<Api> | undefined;
256
+ thinkingLevel: ThinkingLevel;
257
+ fallbackMessage: string | undefined;
258
+ }
259
+
260
+ /**
261
+ * Find the initial model to use based on priority:
262
+ * 1. CLI args (provider + model)
263
+ * 2. First model from scoped models (if not continuing/resuming)
264
+ * 3. Restored from session (if continuing/resuming)
265
+ * 4. Saved default from settings
266
+ * 5. First available model with valid API key
267
+ */
268
+ export async function findInitialModel(options: {
269
+ cliProvider?: string;
270
+ cliModel?: string;
271
+ scopedModels: ScopedModel[];
272
+ isContinuing: boolean;
273
+ defaultProvider?: string;
274
+ defaultModelId?: string;
275
+ defaultThinkingLevel?: ThinkingLevel;
276
+ modelRegistry: ModelRegistry;
277
+ }): Promise<InitialModelResult> {
278
+ const {
279
+ cliProvider,
280
+ cliModel,
281
+ scopedModels,
282
+ isContinuing,
283
+ defaultProvider,
284
+ defaultModelId,
285
+ defaultThinkingLevel,
286
+ modelRegistry,
287
+ } = options;
288
+
289
+ let model: Model<Api> | undefined;
290
+ let thinkingLevel: ThinkingLevel = "off";
291
+
292
+ // 1. CLI args take priority
293
+ if (cliProvider && cliModel) {
294
+ const found = modelRegistry.find(cliProvider, cliModel);
295
+ if (!found) {
296
+ console.error(chalk.red(`Model ${cliProvider}/${cliModel} not found`));
297
+ process.exit(1);
298
+ }
299
+ return { model: found, thinkingLevel: "off", fallbackMessage: undefined };
300
+ }
301
+
302
+ // 2. Use first model from scoped models (skip if continuing/resuming)
303
+ if (scopedModels.length > 0 && !isContinuing) {
304
+ return {
305
+ model: scopedModels[0].model,
306
+ thinkingLevel: scopedModels[0].thinkingLevel,
307
+ fallbackMessage: undefined,
308
+ };
309
+ }
310
+
311
+ // 3. Try saved default from settings
312
+ if (defaultProvider && defaultModelId) {
313
+ const found = modelRegistry.find(defaultProvider, defaultModelId);
314
+ if (found) {
315
+ model = found;
316
+ if (defaultThinkingLevel) {
317
+ thinkingLevel = defaultThinkingLevel;
318
+ }
319
+ return { model, thinkingLevel, fallbackMessage: undefined };
320
+ }
321
+ }
322
+
323
+ // 4. Try first available model with valid API key
324
+ const availableModels = await modelRegistry.getAvailable();
325
+
326
+ if (availableModels.length > 0) {
327
+ // Try to find a default model from known providers
328
+ for (const provider of Object.keys(defaultModelPerProvider) as KnownProvider[]) {
329
+ const defaultId = defaultModelPerProvider[provider];
330
+ const match = availableModels.find((m) => m.provider === provider && m.id === defaultId);
331
+ if (match) {
332
+ return { model: match, thinkingLevel: "off", fallbackMessage: undefined };
333
+ }
334
+ }
335
+
336
+ // If no default found, use first available
337
+ return { model: availableModels[0], thinkingLevel: "off", fallbackMessage: undefined };
338
+ }
339
+
340
+ // 5. No model found
341
+ return { model: undefined, thinkingLevel: "off", fallbackMessage: undefined };
342
+ }
343
+
344
+ /**
345
+ * Restore model from session, with fallback to available models
346
+ */
347
+ export async function restoreModelFromSession(
348
+ savedProvider: string,
349
+ savedModelId: string,
350
+ currentModel: Model<Api> | undefined,
351
+ shouldPrintMessages: boolean,
352
+ modelRegistry: ModelRegistry,
353
+ ): Promise<{ model: Model<Api> | undefined; fallbackMessage: string | undefined }> {
354
+ const restoredModel = modelRegistry.find(savedProvider, savedModelId);
355
+
356
+ // Check if restored model exists and has a valid API key
357
+ const hasApiKey = restoredModel ? !!(await modelRegistry.getApiKey(restoredModel)) : false;
358
+
359
+ if (restoredModel && hasApiKey) {
360
+ if (shouldPrintMessages) {
361
+ console.log(chalk.dim(`Restored model: ${savedProvider}/${savedModelId}`));
362
+ }
363
+ return { model: restoredModel, fallbackMessage: undefined };
364
+ }
365
+
366
+ // Model not found or no API key - fall back
367
+ const reason = !restoredModel ? "model no longer exists" : "no API key available";
368
+
369
+ if (shouldPrintMessages) {
370
+ console.error(chalk.yellow(`Warning: Could not restore model ${savedProvider}/${savedModelId} (${reason}).`));
371
+ }
372
+
373
+ // If we already have a model, use it as fallback
374
+ if (currentModel) {
375
+ if (shouldPrintMessages) {
376
+ console.log(chalk.dim(`Falling back to: ${currentModel.provider}/${currentModel.id}`));
377
+ }
378
+ return {
379
+ model: currentModel,
380
+ fallbackMessage: `Could not restore model ${savedProvider}/${savedModelId} (${reason}). Using ${currentModel.provider}/${currentModel.id}.`,
381
+ };
382
+ }
383
+
384
+ // Try to find any available model
385
+ const availableModels = await modelRegistry.getAvailable();
386
+
387
+ if (availableModels.length > 0) {
388
+ // Try to find a default model from known providers
389
+ let fallbackModel: Model<Api> | undefined;
390
+ for (const provider of Object.keys(defaultModelPerProvider) as KnownProvider[]) {
391
+ const defaultId = defaultModelPerProvider[provider];
392
+ const match = availableModels.find((m) => m.provider === provider && m.id === defaultId);
393
+ if (match) {
394
+ fallbackModel = match;
395
+ break;
396
+ }
397
+ }
398
+
399
+ // If no default found, use first available
400
+ if (!fallbackModel) {
401
+ fallbackModel = availableModels[0];
402
+ }
403
+
404
+ if (shouldPrintMessages) {
405
+ console.log(chalk.dim(`Falling back to: ${fallbackModel.provider}/${fallbackModel.id}`));
406
+ }
407
+
408
+ return {
409
+ model: fallbackModel,
410
+ fallbackMessage: `Could not restore model ${savedProvider}/${savedModelId} (${reason}). Using ${fallbackModel.provider}/${fallbackModel.id}.`,
411
+ };
412
+ }
413
+
414
+ // No models available
415
+ return { model: undefined, fallbackMessage: undefined };
416
+ }
417
+
418
+ /**
419
+ * Find a smol/fast model using the priority chain.
420
+ * Tries exact matches first, then fuzzy matches.
421
+ *
422
+ * @param modelRegistry The model registry to search
423
+ * @param savedModel Optional saved model string from settings (provider/modelId)
424
+ * @returns The best available smol model, or undefined if none found
425
+ */
426
+ export async function findSmolModel(
427
+ modelRegistry: ModelRegistry,
428
+ savedModel?: string,
429
+ ): Promise<Model<Api> | undefined> {
430
+ const availableModels = await modelRegistry.getAvailable();
431
+ if (availableModels.length === 0) return undefined;
432
+
433
+ // 1. Try saved model from settings
434
+ if (savedModel) {
435
+ const parsed = parseModelString(savedModel);
436
+ if (parsed) {
437
+ const match = availableModels.find((m) => m.provider === parsed.provider && m.id === parsed.id);
438
+ if (match) return match;
439
+ }
440
+ }
441
+
442
+ // 2. Try priority chain
443
+ for (const pattern of SMOL_MODEL_PRIORITY) {
444
+ // Try exact match first
445
+ const exactMatch = availableModels.find((m) => m.id.toLowerCase() === pattern.toLowerCase());
446
+ if (exactMatch) return exactMatch;
447
+
448
+ // Try fuzzy match (substring)
449
+ const fuzzyMatch = availableModels.find((m) => m.id.toLowerCase().includes(pattern.toLowerCase()));
450
+ if (fuzzyMatch) return fuzzyMatch;
451
+ }
452
+
453
+ // 3. Fallback to first available (same as default)
454
+ return availableModels[0];
455
+ }
456
+
457
+ /**
458
+ * Find a slow/comprehensive model using the priority chain.
459
+ * Prioritizes reasoning and codex models for thorough analysis.
460
+ *
461
+ * @param modelRegistry The model registry to search
462
+ * @param savedModel Optional saved model string from settings (provider/modelId)
463
+ * @returns The best available slow model, or undefined if none found
464
+ */
465
+ export async function findSlowModel(
466
+ modelRegistry: ModelRegistry,
467
+ savedModel?: string,
468
+ ): Promise<Model<Api> | undefined> {
469
+ const availableModels = await modelRegistry.getAvailable();
470
+ if (availableModels.length === 0) return undefined;
471
+
472
+ // 1. Try saved model from settings
473
+ if (savedModel) {
474
+ const parsed = parseModelString(savedModel);
475
+ if (parsed) {
476
+ const match = availableModels.find((m) => m.provider === parsed.provider && m.id === parsed.id);
477
+ if (match) return match;
478
+ }
479
+ }
480
+
481
+ // 2. Try priority chain
482
+ for (const pattern of SLOW_MODEL_PRIORITY) {
483
+ // Try exact match first
484
+ const exactMatch = availableModels.find((m) => m.id.toLowerCase() === pattern.toLowerCase());
485
+ if (exactMatch) return exactMatch;
486
+
487
+ // Try fuzzy match (substring)
488
+ const fuzzyMatch = availableModels.find((m) => m.id.toLowerCase().includes(pattern.toLowerCase()));
489
+ if (fuzzyMatch) return fuzzyMatch;
490
+ }
491
+
492
+ // 3. Fallback to first available (same as default)
493
+ return availableModels[0];
494
+ }
@@ -0,0 +1,67 @@
1
+ import { theme } from "../../modes/interactive/theme/theme";
2
+ import type { DoctorCheck } from "./types";
3
+
4
+ export async function runDoctorChecks(): Promise<DoctorCheck[]> {
5
+ const checks: DoctorCheck[] = [];
6
+
7
+ // Check external tools
8
+ const tools = [
9
+ { name: "fd", description: "File finder" },
10
+ { name: "rg", description: "Ripgrep" },
11
+ { name: "sd", description: "Find-replace" },
12
+ { name: "sg", description: "AST-grep" },
13
+ { name: "git", description: "Version control" },
14
+ ];
15
+
16
+ for (const tool of tools) {
17
+ const path = Bun.which(tool.name);
18
+ checks.push({
19
+ name: tool.name,
20
+ status: path ? "ok" : "warning",
21
+ message: path ? `Found at ${path}` : `${tool.description} not found - some features may be limited`,
22
+ });
23
+ }
24
+
25
+ // Check API keys
26
+ const apiKeys = [
27
+ { name: "ANTHROPIC_API_KEY", description: "Anthropic API" },
28
+ { name: "OPENAI_API_KEY", description: "OpenAI API" },
29
+ { name: "PERPLEXITY_API_KEY", description: "Perplexity search" },
30
+ { name: "EXA_API_KEY", description: "Exa search" },
31
+ ];
32
+
33
+ for (const key of apiKeys) {
34
+ const hasKey = !!process.env[key.name];
35
+ checks.push({
36
+ name: key.name,
37
+ status: hasKey ? "ok" : "warning",
38
+ message: hasKey ? "Configured" : `Not set - ${key.description} unavailable`,
39
+ });
40
+ }
41
+
42
+ return checks;
43
+ }
44
+
45
+ export function formatDoctorResults(checks: DoctorCheck[]): string {
46
+ // Note: This function returns plain text without theming as it may be called outside TUI context.
47
+ // For TUI usage, the plugin CLI handler applies theme colors.
48
+ const lines: string[] = ["System Health Check", "=".repeat(40), ""];
49
+
50
+ for (const check of checks) {
51
+ const icon =
52
+ check.status === "ok"
53
+ ? theme.status.success
54
+ : check.status === "warning"
55
+ ? theme.status.warning
56
+ : theme.status.error;
57
+ lines.push(`${icon} ${check.name}: ${check.message}`);
58
+ }
59
+
60
+ const errors = checks.filter((c) => c.status === "error").length;
61
+ const warnings = checks.filter((c) => c.status === "warning").length;
62
+
63
+ lines.push("");
64
+ lines.push(`Summary: ${checks.length - errors - warnings} ok, ${warnings} warnings, ${errors} errors`);
65
+
66
+ return lines.join("\n");
67
+ }
@@ -0,0 +1,38 @@
1
+ // Plugin system exports
2
+ export { formatDoctorResults, runDoctorChecks } from "./doctor";
3
+ export {
4
+ getAllPluginCommandPaths,
5
+ getAllPluginHookPaths,
6
+ getAllPluginToolPaths,
7
+ getEnabledPlugins,
8
+ getPluginSettings,
9
+ resolvePluginCommandPaths,
10
+ resolvePluginHookPaths,
11
+ resolvePluginToolPaths,
12
+ } from "./loader";
13
+ export { PluginManager, parseSettingValue, validateSetting } from "./manager";
14
+ export { extractPackageName, formatPluginSpec, parsePluginSpec } from "./parser";
15
+ export {
16
+ getPluginsDir,
17
+ getPluginsLockfile,
18
+ getPluginsNodeModules,
19
+ getPluginsPackageJson,
20
+ getProjectPluginOverrides,
21
+ } from "./paths";
22
+ export type {
23
+ BooleanSetting,
24
+ DoctorCheck,
25
+ DoctorOptions,
26
+ EnumSetting,
27
+ InstalledPlugin,
28
+ InstallOptions,
29
+ NumberSetting,
30
+ PluginFeature,
31
+ PluginManifest,
32
+ PluginRuntimeConfig,
33
+ PluginRuntimeState,
34
+ PluginSettingSchema,
35
+ PluginSettingType,
36
+ ProjectPluginOverrides,
37
+ StringSetting,
38
+ } from "./types";