@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,526 @@
1
+ import path from "node:path";
2
+ import { type Theme, theme } from "../../../modes/interactive/theme/theme";
3
+ import type {
4
+ Diagnostic,
5
+ DiagnosticSeverity,
6
+ DocumentSymbol,
7
+ Location,
8
+ SymbolInformation,
9
+ SymbolKind,
10
+ TextEdit,
11
+ WorkspaceEdit,
12
+ } from "./types";
13
+
14
+ // =============================================================================
15
+ // Language Detection
16
+ // =============================================================================
17
+
18
+ const LANGUAGE_MAP: Record<string, string> = {
19
+ // TypeScript/JavaScript
20
+ ".ts": "typescript",
21
+ ".tsx": "typescriptreact",
22
+ ".js": "javascript",
23
+ ".jsx": "javascriptreact",
24
+ ".mjs": "javascript",
25
+ ".cjs": "javascript",
26
+ ".mts": "typescript",
27
+ ".cts": "typescript",
28
+
29
+ // Systems languages
30
+ ".rs": "rust",
31
+ ".go": "go",
32
+ ".c": "c",
33
+ ".h": "c",
34
+ ".cpp": "cpp",
35
+ ".cc": "cpp",
36
+ ".cxx": "cpp",
37
+ ".hpp": "cpp",
38
+ ".hxx": "cpp",
39
+ ".zig": "zig",
40
+
41
+ // Scripting languages
42
+ ".py": "python",
43
+ ".rb": "ruby",
44
+ ".lua": "lua",
45
+ ".sh": "shellscript",
46
+ ".bash": "shellscript",
47
+ ".zsh": "shellscript",
48
+ ".fish": "fish",
49
+ ".pl": "perl",
50
+ ".php": "php",
51
+
52
+ // JVM languages
53
+ ".java": "java",
54
+ ".kt": "kotlin",
55
+ ".kts": "kotlin",
56
+ ".scala": "scala",
57
+ ".groovy": "groovy",
58
+ ".clj": "clojure",
59
+
60
+ // .NET languages
61
+ ".cs": "csharp",
62
+ ".fs": "fsharp",
63
+ ".vb": "vb",
64
+
65
+ // Web
66
+ ".html": "html",
67
+ ".htm": "html",
68
+ ".css": "css",
69
+ ".scss": "scss",
70
+ ".sass": "sass",
71
+ ".less": "less",
72
+ ".vue": "vue",
73
+ ".svelte": "svelte",
74
+
75
+ // Data formats
76
+ ".json": "json",
77
+ ".jsonc": "jsonc",
78
+ ".yaml": "yaml",
79
+ ".yml": "yaml",
80
+ ".toml": "toml",
81
+ ".xml": "xml",
82
+ ".ini": "ini",
83
+
84
+ // Documentation
85
+ ".md": "markdown",
86
+ ".markdown": "markdown",
87
+ ".rst": "restructuredtext",
88
+ ".adoc": "asciidoc",
89
+ ".tex": "latex",
90
+
91
+ // Other
92
+ ".sql": "sql",
93
+ ".graphql": "graphql",
94
+ ".gql": "graphql",
95
+ ".proto": "protobuf",
96
+ ".dockerfile": "dockerfile",
97
+ ".tf": "terraform",
98
+ ".hcl": "hcl",
99
+ ".nix": "nix",
100
+ ".ex": "elixir",
101
+ ".exs": "elixir",
102
+ ".erl": "erlang",
103
+ ".hrl": "erlang",
104
+ ".hs": "haskell",
105
+ ".ml": "ocaml",
106
+ ".mli": "ocaml",
107
+ ".swift": "swift",
108
+ ".r": "r",
109
+ ".R": "r",
110
+ ".jl": "julia",
111
+ ".dart": "dart",
112
+ ".elm": "elm",
113
+ ".v": "v",
114
+ ".nim": "nim",
115
+ ".cr": "crystal",
116
+ ".d": "d",
117
+ ".pas": "pascal",
118
+ ".pp": "pascal",
119
+ ".lisp": "lisp",
120
+ ".lsp": "lisp",
121
+ ".rkt": "racket",
122
+ ".scm": "scheme",
123
+ ".ps1": "powershell",
124
+ ".psm1": "powershell",
125
+ ".bat": "bat",
126
+ ".cmd": "bat",
127
+ };
128
+
129
+ /**
130
+ * Detect language ID from file path.
131
+ * Returns the LSP language identifier for the file type.
132
+ */
133
+ export function detectLanguageId(filePath: string): string {
134
+ const ext = path.extname(filePath).toLowerCase();
135
+ const basename = path.basename(filePath).toLowerCase();
136
+
137
+ // Handle special filenames
138
+ if (basename === "dockerfile" || basename.startsWith("dockerfile.")) {
139
+ return "dockerfile";
140
+ }
141
+ if (basename === "makefile" || basename === "gnumakefile") {
142
+ return "makefile";
143
+ }
144
+ if (basename === "cmakelists.txt" || ext === ".cmake") {
145
+ return "cmake";
146
+ }
147
+
148
+ return LANGUAGE_MAP[ext] ?? "plaintext";
149
+ }
150
+
151
+ // =============================================================================
152
+ // URI Handling (Cross-Platform)
153
+ // =============================================================================
154
+
155
+ /**
156
+ * Convert a file path to a file:// URI.
157
+ * Handles Windows drive letters correctly.
158
+ */
159
+ export function fileToUri(filePath: string): string {
160
+ const resolved = path.resolve(filePath);
161
+
162
+ if (process.platform === "win32") {
163
+ // Windows: file:///C:/path/to/file
164
+ return `file:///${resolved.replace(/\\/g, "/")}`;
165
+ }
166
+
167
+ // Unix: file:///path/to/file
168
+ return `file://${resolved}`;
169
+ }
170
+
171
+ /**
172
+ * Convert a file:// URI to a file path.
173
+ * Handles Windows drive letters correctly.
174
+ */
175
+ export function uriToFile(uri: string): string {
176
+ if (!uri.startsWith("file://")) {
177
+ return uri;
178
+ }
179
+
180
+ let filePath = decodeURIComponent(uri.slice(7));
181
+
182
+ // Windows: file:///C:/path → C:/path (strip leading slash before drive letter)
183
+ if (process.platform === "win32" && filePath.startsWith("/") && /^[A-Za-z]:/.test(filePath.slice(1))) {
184
+ filePath = filePath.slice(1);
185
+ }
186
+
187
+ return filePath;
188
+ }
189
+
190
+ // =============================================================================
191
+ // Diagnostic Formatting
192
+ // =============================================================================
193
+
194
+ const SEVERITY_NAMES: Record<DiagnosticSeverity, string> = {
195
+ 1: "error",
196
+ 2: "warning",
197
+ 3: "info",
198
+ 4: "hint",
199
+ };
200
+
201
+ /**
202
+ * Convert diagnostic severity number to string name.
203
+ */
204
+ export function severityToString(severity?: DiagnosticSeverity): string {
205
+ return SEVERITY_NAMES[severity ?? 1] ?? "unknown";
206
+ }
207
+
208
+ /**
209
+ * Get icon for diagnostic severity.
210
+ */
211
+ export function severityToIcon(severity?: DiagnosticSeverity): string {
212
+ const currentTheme = theme as Theme | undefined;
213
+ const fallback = currentTheme?.format?.bullet ?? "*";
214
+ const status = currentTheme?.status;
215
+ switch (severity ?? 1) {
216
+ case 1:
217
+ return status?.error ?? fallback;
218
+ case 2:
219
+ return status?.warning ?? fallback;
220
+ case 3:
221
+ return status?.info ?? fallback;
222
+ case 4:
223
+ return currentTheme?.format?.bullet ?? fallback;
224
+ default:
225
+ return status?.error ?? fallback;
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Format a diagnostic as a human-readable string.
231
+ */
232
+ export function formatDiagnostic(diagnostic: Diagnostic, filePath: string): string {
233
+ const severity = severityToString(diagnostic.severity);
234
+ const line = diagnostic.range.start.line + 1;
235
+ const col = diagnostic.range.start.character + 1;
236
+ const source = diagnostic.source ? `[${diagnostic.source}] ` : "";
237
+ const code = diagnostic.code ? ` (${diagnostic.code})` : "";
238
+
239
+ return `${filePath}:${line}:${col} [${severity}] ${source}${diagnostic.message}${code}`;
240
+ }
241
+
242
+ /**
243
+ * Format diagnostics grouped by severity.
244
+ */
245
+ export function formatDiagnosticsSummary(diagnostics: Diagnostic[]): string {
246
+ const counts = { error: 0, warning: 0, info: 0, hint: 0 };
247
+
248
+ for (const d of diagnostics) {
249
+ const sev = severityToString(d.severity);
250
+ if (sev in counts) {
251
+ counts[sev as keyof typeof counts]++;
252
+ }
253
+ }
254
+
255
+ const parts: string[] = [];
256
+ if (counts.error > 0) parts.push(`${counts.error} error(s)`);
257
+ if (counts.warning > 0) parts.push(`${counts.warning} warning(s)`);
258
+ if (counts.info > 0) parts.push(`${counts.info} info(s)`);
259
+ if (counts.hint > 0) parts.push(`${counts.hint} hint(s)`);
260
+
261
+ return parts.length > 0 ? parts.join(", ") : "no issues";
262
+ }
263
+
264
+ // =============================================================================
265
+ // Location Formatting
266
+ // =============================================================================
267
+
268
+ /**
269
+ * Format a location as file:line:col relative to cwd.
270
+ */
271
+ export function formatLocation(location: Location, cwd: string): string {
272
+ const file = path.relative(cwd, uriToFile(location.uri));
273
+ const line = location.range.start.line + 1;
274
+ const col = location.range.start.character + 1;
275
+ return `${file}:${line}:${col}`;
276
+ }
277
+
278
+ /**
279
+ * Format a position as line:col.
280
+ */
281
+ export function formatPosition(line: number, col: number): string {
282
+ return `${line}:${col}`;
283
+ }
284
+
285
+ // =============================================================================
286
+ // WorkspaceEdit Formatting
287
+ // =============================================================================
288
+
289
+ /**
290
+ * Format a workspace edit as a summary of changes.
291
+ */
292
+ export function formatWorkspaceEdit(edit: WorkspaceEdit, cwd: string): string[] {
293
+ const results: string[] = [];
294
+
295
+ // Handle changes map (legacy format)
296
+ if (edit.changes) {
297
+ for (const [uri, textEdits] of Object.entries(edit.changes)) {
298
+ const file = path.relative(cwd, uriToFile(uri));
299
+ results.push(`${file}: ${textEdits.length} edit${textEdits.length > 1 ? "s" : ""}`);
300
+ }
301
+ }
302
+
303
+ // Handle documentChanges array (modern format)
304
+ if (edit.documentChanges) {
305
+ for (const change of edit.documentChanges) {
306
+ if ("edits" in change && change.textDocument) {
307
+ const file = path.relative(cwd, uriToFile(change.textDocument.uri));
308
+ results.push(`${file}: ${change.edits.length} edit${change.edits.length > 1 ? "s" : ""}`);
309
+ } else if ("kind" in change) {
310
+ switch (change.kind) {
311
+ case "create":
312
+ results.push(`CREATE: ${path.relative(cwd, uriToFile(change.uri))}`);
313
+ break;
314
+ case "rename":
315
+ results.push(
316
+ `RENAME: ${path.relative(cwd, uriToFile(change.oldUri))} ${theme.nav.cursor} ${path.relative(cwd, uriToFile(change.newUri))}`,
317
+ );
318
+ break;
319
+ case "delete":
320
+ results.push(`DELETE: ${path.relative(cwd, uriToFile(change.uri))}`);
321
+ break;
322
+ }
323
+ }
324
+ }
325
+ }
326
+
327
+ return results;
328
+ }
329
+
330
+ /**
331
+ * Format a text edit as a preview.
332
+ */
333
+ export function formatTextEdit(edit: TextEdit, maxLength = 50): string {
334
+ const range = `${edit.range.start.line + 1}:${edit.range.start.character + 1}`;
335
+ const preview =
336
+ edit.newText.length > maxLength
337
+ ? `${edit.newText.slice(0, maxLength).replace(/\n/g, "\\n")}...`
338
+ : edit.newText.replace(/\n/g, "\\n");
339
+ return `line ${range} ${theme.nav.cursor} "${preview}"`;
340
+ }
341
+
342
+ // =============================================================================
343
+ // Symbol Formatting
344
+ // =============================================================================
345
+
346
+ function getSymbolKindIcons(): Record<SymbolKind, string> {
347
+ const currentTheme = theme as Theme | undefined;
348
+ const fallback = currentTheme?.format?.bullet ?? "*";
349
+ const dash = currentTheme?.format?.dash ?? fallback;
350
+ const icon = currentTheme?.icon;
351
+
352
+ const file = icon?.file ?? fallback;
353
+ const folder = icon?.folder ?? fallback;
354
+ const pkg = icon?.package ?? folder;
355
+ const model = icon?.model ?? fallback;
356
+ const func = icon?.auto ?? dash;
357
+
358
+ return {
359
+ 1: file, // File
360
+ 2: folder, // Module
361
+ 3: folder, // Namespace
362
+ 4: pkg, // Package
363
+ 5: model, // Class
364
+ 6: func, // Method
365
+ 7: fallback, // Property
366
+ 8: fallback, // Field
367
+ 9: func, // Constructor
368
+ 10: fallback, // Enum
369
+ 11: model, // Interface
370
+ 12: func, // Function
371
+ 13: fallback, // Variable
372
+ 14: fallback, // Constant
373
+ 15: fallback, // String
374
+ 16: fallback, // Number
375
+ 17: fallback, // Boolean
376
+ 18: fallback, // Array
377
+ 19: fallback, // Object
378
+ 20: fallback, // Key
379
+ 21: fallback, // Null
380
+ 22: fallback, // EnumMember
381
+ 23: folder, // Struct
382
+ 24: fallback, // Event
383
+ 25: fallback, // Operator
384
+ 26: fallback, // TypeParameter
385
+ };
386
+ }
387
+
388
+ /**
389
+ * Get icon for symbol kind.
390
+ */
391
+ export function symbolKindToIcon(kind: SymbolKind): string {
392
+ const currentTheme = theme as Theme | undefined;
393
+ const bullet = currentTheme?.format?.bullet ?? "*";
394
+ return getSymbolKindIcons()[kind] ?? bullet;
395
+ }
396
+
397
+ /**
398
+ * Get name for symbol kind.
399
+ */
400
+ export function symbolKindToName(kind: SymbolKind): string {
401
+ const names: Record<number, string> = {
402
+ 1: "File",
403
+ 2: "Module",
404
+ 3: "Namespace",
405
+ 4: "Package",
406
+ 5: "Class",
407
+ 6: "Method",
408
+ 7: "Property",
409
+ 8: "Field",
410
+ 9: "Constructor",
411
+ 10: "Enum",
412
+ 11: "Interface",
413
+ 12: "Function",
414
+ 13: "Variable",
415
+ 14: "Constant",
416
+ 15: "String",
417
+ 16: "Number",
418
+ 17: "Boolean",
419
+ 18: "Array",
420
+ 19: "Object",
421
+ 20: "Key",
422
+ 21: "Null",
423
+ 22: "EnumMember",
424
+ 23: "Struct",
425
+ 24: "Event",
426
+ 25: "Operator",
427
+ 26: "TypeParameter",
428
+ };
429
+ return names[kind] ?? "Unknown";
430
+ }
431
+
432
+ /**
433
+ * Format a document symbol with optional hierarchy.
434
+ */
435
+ export function formatDocumentSymbol(symbol: DocumentSymbol, indent = 0): string[] {
436
+ const prefix = " ".repeat(indent);
437
+ const icon = symbolKindToIcon(symbol.kind);
438
+ const line = symbol.range.start.line + 1;
439
+ const results = [`${prefix}${icon} ${symbol.name} @ line ${line}`];
440
+
441
+ if (symbol.children) {
442
+ for (const child of symbol.children) {
443
+ results.push(...formatDocumentSymbol(child, indent + 1));
444
+ }
445
+ }
446
+
447
+ return results;
448
+ }
449
+
450
+ /**
451
+ * Format a symbol information (flat format).
452
+ */
453
+ export function formatSymbolInformation(symbol: SymbolInformation, cwd: string): string {
454
+ const icon = symbolKindToIcon(symbol.kind);
455
+ const location = formatLocation(symbol.location, cwd);
456
+ const container = symbol.containerName ? ` (${symbol.containerName})` : "";
457
+ return `${icon} ${symbol.name}${container} @ ${location}`;
458
+ }
459
+
460
+ // =============================================================================
461
+ // Hover Content Extraction
462
+ // =============================================================================
463
+
464
+ /**
465
+ * Extract plain text from hover contents.
466
+ */
467
+ export function extractHoverText(
468
+ contents: string | { kind: string; value: string } | { language: string; value: string } | unknown[],
469
+ ): string {
470
+ if (typeof contents === "string") {
471
+ return contents;
472
+ }
473
+
474
+ if (Array.isArray(contents)) {
475
+ return contents.map((c) => extractHoverText(c as string | { kind: string; value: string })).join("\n\n");
476
+ }
477
+
478
+ if (typeof contents === "object" && contents !== null) {
479
+ if ("value" in contents && typeof contents.value === "string") {
480
+ return contents.value;
481
+ }
482
+ }
483
+
484
+ return String(contents);
485
+ }
486
+
487
+ // =============================================================================
488
+ // General Utilities
489
+ // =============================================================================
490
+
491
+ /**
492
+ * Sleep for the specified number of milliseconds.
493
+ */
494
+ export function sleep(ms: number): Promise<void> {
495
+ return new Promise((resolve) => setTimeout(resolve, ms));
496
+ }
497
+
498
+ /**
499
+ * Check if a command exists in PATH.
500
+ */
501
+ export async function commandExists(command: string): Promise<boolean> {
502
+ return Bun.which(command) !== null;
503
+ }
504
+
505
+ /**
506
+ * Truncate a string to a maximum length with ellipsis.
507
+ */
508
+ export function truncate(str: string, maxLength: number): string {
509
+ if (str.length <= maxLength) return str;
510
+ return `${str.slice(0, maxLength - 3)}...`;
511
+ }
512
+
513
+ /**
514
+ * Group items by a key function.
515
+ */
516
+ export function groupBy<T, K extends string | number>(items: T[], keyFn: (item: T) => K): Record<K, T[]> {
517
+ const result = {} as Record<K, T[]>;
518
+ for (const item of items) {
519
+ const key = keyFn(item);
520
+ if (!result[key]) {
521
+ result[key] = [];
522
+ }
523
+ result[key].push(item);
524
+ }
525
+ return result;
526
+ }
@@ -0,0 +1,182 @@
1
+ import type { AgentTool } from "@oh-my-pi/pi-agent-core";
2
+ import { Type } from "@sinclair/typebox";
3
+ import { untilAborted } from "../utils";
4
+ import { resolveToCwd } from "./path-utils";
5
+
6
+ const notebookSchema = Type.Object({
7
+ action: Type.Union([Type.Literal("edit"), Type.Literal("insert"), Type.Literal("delete")], {
8
+ description: "Action to perform on the notebook cell",
9
+ }),
10
+ notebook_path: Type.String({ description: "Path to the .ipynb file (relative or absolute)" }),
11
+ cell_index: Type.Number({ description: "0-based index of the cell to operate on" }),
12
+ content: Type.Optional(Type.String({ description: "New cell content (required for edit/insert)" })),
13
+ cell_type: Type.Optional(
14
+ Type.Union([Type.Literal("code"), Type.Literal("markdown")], {
15
+ description: "Cell type for insert (default: code)",
16
+ }),
17
+ ),
18
+ });
19
+
20
+ export interface NotebookToolDetails {
21
+ /** Action performed */
22
+ action: "edit" | "insert" | "delete";
23
+ /** Cell index operated on */
24
+ cellIndex: number;
25
+ /** Cell type */
26
+ cellType?: string;
27
+ /** Total cell count after operation */
28
+ totalCells: number;
29
+ /** Cell content lines after operation (or removed content for delete) */
30
+ cellSource?: string[];
31
+ }
32
+
33
+ interface NotebookCell {
34
+ cell_type: "code" | "markdown" | "raw";
35
+ source: string[];
36
+ metadata: Record<string, unknown>;
37
+ execution_count?: number | null;
38
+ outputs?: unknown[];
39
+ }
40
+
41
+ interface Notebook {
42
+ cells: NotebookCell[];
43
+ metadata: Record<string, unknown>;
44
+ nbformat: number;
45
+ nbformat_minor: number;
46
+ }
47
+
48
+ function splitIntoLines(content: string): string[] {
49
+ return content.split("\n").map((line, i, arr) => (i < arr.length - 1 ? `${line}\n` : line));
50
+ }
51
+
52
+ export function createNotebookTool(cwd: string): AgentTool<typeof notebookSchema> {
53
+ return {
54
+ name: "notebook",
55
+ label: "Notebook",
56
+ description:
57
+ "Completely replaces the contents of a specific cell in a Jupyter notebook (.ipynb file) with new source. Jupyter notebooks are interactive documents that combine code, text, and visualizations, commonly used for data analysis and scientific computing. The notebook_path parameter must be an absolute path, not a relative path. The cell_number is 0-indexed. Use edit_mode=insert to add a new cell at the index specified by cell_number. Use edit_mode=delete to delete the cell at the index specified by cell_number.",
58
+ parameters: notebookSchema,
59
+ execute: async (
60
+ _toolCallId: string,
61
+ {
62
+ action,
63
+ notebook_path,
64
+ cell_index,
65
+ content,
66
+ cell_type,
67
+ }: { action: string; notebook_path: string; cell_index: number; content?: string; cell_type?: string },
68
+ signal?: AbortSignal,
69
+ ) => {
70
+ const absolutePath = resolveToCwd(notebook_path, cwd);
71
+
72
+ return untilAborted(signal, async () => {
73
+ // Check if file exists
74
+ const file = Bun.file(absolutePath);
75
+ if (!(await file.exists())) {
76
+ throw new Error(`Notebook not found: ${notebook_path}`);
77
+ }
78
+
79
+ // Read and parse notebook
80
+ let notebook: Notebook;
81
+ try {
82
+ notebook = await file.json();
83
+ } catch {
84
+ throw new Error(`Invalid JSON in notebook: ${notebook_path}`);
85
+ }
86
+
87
+ // Validate notebook structure
88
+ if (!notebook.cells || !Array.isArray(notebook.cells)) {
89
+ throw new Error(`Invalid notebook structure (missing cells array): ${notebook_path}`);
90
+ }
91
+
92
+ const cellCount = notebook.cells.length;
93
+
94
+ // Validate cell_index based on action
95
+ if (action === "insert") {
96
+ if (cell_index < 0 || cell_index > cellCount) {
97
+ throw new Error(
98
+ `Cell index ${cell_index} out of range for insert (0-${cellCount}) in ${notebook_path}`,
99
+ );
100
+ }
101
+ } else {
102
+ if (cell_index < 0 || cell_index >= cellCount) {
103
+ throw new Error(`Cell index ${cell_index} out of range (0-${cellCount - 1}) in ${notebook_path}`);
104
+ }
105
+ }
106
+
107
+ // Validate content for edit/insert
108
+ if ((action === "edit" || action === "insert") && content === undefined) {
109
+ throw new Error(`Content is required for ${action} action`);
110
+ }
111
+
112
+ // Perform the action
113
+ let resultMessage: string;
114
+ let finalCellType: string | undefined;
115
+ let cellSource: string[] | undefined;
116
+
117
+ switch (action) {
118
+ case "edit": {
119
+ const sourceLines = splitIntoLines(content!);
120
+ notebook.cells[cell_index].source = sourceLines;
121
+ finalCellType = notebook.cells[cell_index].cell_type;
122
+ cellSource = sourceLines;
123
+ resultMessage = `Replaced cell ${cell_index} (${finalCellType})`;
124
+ break;
125
+ }
126
+ case "insert": {
127
+ const sourceLines = splitIntoLines(content!);
128
+ const newCellType = (cell_type as "code" | "markdown") || "code";
129
+ const newCell: NotebookCell = {
130
+ cell_type: newCellType,
131
+ source: sourceLines,
132
+ metadata: {},
133
+ };
134
+ if (newCellType === "code") {
135
+ newCell.execution_count = null;
136
+ newCell.outputs = [];
137
+ }
138
+ notebook.cells.splice(cell_index, 0, newCell);
139
+ finalCellType = newCellType;
140
+ cellSource = sourceLines;
141
+ resultMessage = `Inserted ${newCellType} cell at position ${cell_index}`;
142
+ break;
143
+ }
144
+ case "delete": {
145
+ const removedCell = notebook.cells[cell_index];
146
+ finalCellType = removedCell.cell_type;
147
+ cellSource = removedCell.source;
148
+ notebook.cells.splice(cell_index, 1);
149
+ resultMessage = `Deleted cell ${cell_index} (${finalCellType})`;
150
+ break;
151
+ }
152
+ default: {
153
+ throw new Error(`Invalid action: ${action}`);
154
+ }
155
+ }
156
+
157
+ // Write back with single-space indentation
158
+ await Bun.write(absolutePath, JSON.stringify(notebook, null, 1));
159
+
160
+ const newCellCount = notebook.cells.length;
161
+ return {
162
+ content: [
163
+ {
164
+ type: "text",
165
+ text: `${resultMessage}. Notebook now has ${newCellCount} cells.`,
166
+ },
167
+ ],
168
+ details: {
169
+ action: action as "edit" | "insert" | "delete",
170
+ cellIndex: cell_index,
171
+ cellType: finalCellType,
172
+ totalCells: newCellCount,
173
+ cellSource,
174
+ },
175
+ };
176
+ });
177
+ },
178
+ };
179
+ }
180
+
181
+ /** Default notebook tool using process.cwd() */
182
+ export const notebookTool = createNotebookTool(process.cwd());