@oh-my-pi/pi-coding-agent 1.337.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 (224) hide show
  1. package/CHANGELOG.md +1228 -0
  2. package/README.md +1041 -0
  3. package/docs/compaction.md +403 -0
  4. package/docs/custom-tools.md +541 -0
  5. package/docs/extension-loading.md +1004 -0
  6. package/docs/hooks.md +867 -0
  7. package/docs/rpc.md +1040 -0
  8. package/docs/sdk.md +994 -0
  9. package/docs/session-tree-plan.md +441 -0
  10. package/docs/session.md +240 -0
  11. package/docs/skills.md +290 -0
  12. package/docs/theme.md +637 -0
  13. package/docs/tree.md +197 -0
  14. package/docs/tui.md +341 -0
  15. package/examples/README.md +21 -0
  16. package/examples/custom-tools/README.md +124 -0
  17. package/examples/custom-tools/hello/index.ts +20 -0
  18. package/examples/custom-tools/question/index.ts +84 -0
  19. package/examples/custom-tools/subagent/README.md +172 -0
  20. package/examples/custom-tools/subagent/agents/planner.md +37 -0
  21. package/examples/custom-tools/subagent/agents/reviewer.md +35 -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 +81 -0
  57. package/src/cli/args.ts +246 -0
  58. package/src/cli/file-processor.ts +72 -0
  59. package/src/cli/list-models.ts +104 -0
  60. package/src/cli/plugin-cli.ts +650 -0
  61. package/src/cli/session-picker.ts +41 -0
  62. package/src/cli.ts +10 -0
  63. package/src/commands/init.md +20 -0
  64. package/src/config.ts +159 -0
  65. package/src/core/agent-session.ts +1900 -0
  66. package/src/core/auth-storage.ts +236 -0
  67. package/src/core/bash-executor.ts +196 -0
  68. package/src/core/compaction/branch-summarization.ts +343 -0
  69. package/src/core/compaction/compaction.ts +742 -0
  70. package/src/core/compaction/index.ts +7 -0
  71. package/src/core/compaction/utils.ts +154 -0
  72. package/src/core/custom-tools/index.ts +21 -0
  73. package/src/core/custom-tools/loader.ts +248 -0
  74. package/src/core/custom-tools/types.ts +169 -0
  75. package/src/core/custom-tools/wrapper.ts +28 -0
  76. package/src/core/exec.ts +129 -0
  77. package/src/core/export-html/index.ts +211 -0
  78. package/src/core/export-html/template.css +781 -0
  79. package/src/core/export-html/template.html +54 -0
  80. package/src/core/export-html/template.js +1185 -0
  81. package/src/core/export-html/vendor/highlight.min.js +1213 -0
  82. package/src/core/export-html/vendor/marked.min.js +6 -0
  83. package/src/core/hooks/index.ts +16 -0
  84. package/src/core/hooks/loader.ts +312 -0
  85. package/src/core/hooks/runner.ts +434 -0
  86. package/src/core/hooks/tool-wrapper.ts +99 -0
  87. package/src/core/hooks/types.ts +773 -0
  88. package/src/core/index.ts +52 -0
  89. package/src/core/mcp/client.ts +158 -0
  90. package/src/core/mcp/config.ts +154 -0
  91. package/src/core/mcp/index.ts +45 -0
  92. package/src/core/mcp/loader.ts +68 -0
  93. package/src/core/mcp/manager.ts +181 -0
  94. package/src/core/mcp/tool-bridge.ts +148 -0
  95. package/src/core/mcp/transports/http.ts +316 -0
  96. package/src/core/mcp/transports/index.ts +6 -0
  97. package/src/core/mcp/transports/stdio.ts +252 -0
  98. package/src/core/mcp/types.ts +220 -0
  99. package/src/core/messages.ts +189 -0
  100. package/src/core/model-registry.ts +317 -0
  101. package/src/core/model-resolver.ts +393 -0
  102. package/src/core/plugins/doctor.ts +59 -0
  103. package/src/core/plugins/index.ts +38 -0
  104. package/src/core/plugins/installer.ts +189 -0
  105. package/src/core/plugins/loader.ts +338 -0
  106. package/src/core/plugins/manager.ts +672 -0
  107. package/src/core/plugins/parser.ts +105 -0
  108. package/src/core/plugins/paths.ts +32 -0
  109. package/src/core/plugins/types.ts +190 -0
  110. package/src/core/sdk.ts +760 -0
  111. package/src/core/session-manager.ts +1128 -0
  112. package/src/core/settings-manager.ts +443 -0
  113. package/src/core/skills.ts +437 -0
  114. package/src/core/slash-commands.ts +248 -0
  115. package/src/core/system-prompt.ts +439 -0
  116. package/src/core/timings.ts +25 -0
  117. package/src/core/tools/ask.ts +211 -0
  118. package/src/core/tools/bash-interceptor.ts +120 -0
  119. package/src/core/tools/bash.ts +250 -0
  120. package/src/core/tools/context.ts +32 -0
  121. package/src/core/tools/edit-diff.ts +475 -0
  122. package/src/core/tools/edit.ts +208 -0
  123. package/src/core/tools/exa/company.ts +59 -0
  124. package/src/core/tools/exa/index.ts +64 -0
  125. package/src/core/tools/exa/linkedin.ts +59 -0
  126. package/src/core/tools/exa/logger.ts +56 -0
  127. package/src/core/tools/exa/mcp-client.ts +368 -0
  128. package/src/core/tools/exa/render.ts +196 -0
  129. package/src/core/tools/exa/researcher.ts +90 -0
  130. package/src/core/tools/exa/search.ts +337 -0
  131. package/src/core/tools/exa/types.ts +168 -0
  132. package/src/core/tools/exa/websets.ts +248 -0
  133. package/src/core/tools/find.ts +261 -0
  134. package/src/core/tools/grep.ts +555 -0
  135. package/src/core/tools/index.ts +202 -0
  136. package/src/core/tools/ls.ts +140 -0
  137. package/src/core/tools/lsp/client.ts +605 -0
  138. package/src/core/tools/lsp/config.ts +147 -0
  139. package/src/core/tools/lsp/edits.ts +101 -0
  140. package/src/core/tools/lsp/index.ts +804 -0
  141. package/src/core/tools/lsp/render.ts +447 -0
  142. package/src/core/tools/lsp/rust-analyzer.ts +145 -0
  143. package/src/core/tools/lsp/types.ts +463 -0
  144. package/src/core/tools/lsp/utils.ts +486 -0
  145. package/src/core/tools/notebook.ts +229 -0
  146. package/src/core/tools/path-utils.ts +61 -0
  147. package/src/core/tools/read.ts +240 -0
  148. package/src/core/tools/renderers.ts +540 -0
  149. package/src/core/tools/task/agents.ts +153 -0
  150. package/src/core/tools/task/artifacts.ts +114 -0
  151. package/src/core/tools/task/bundled-agents/browser.md +71 -0
  152. package/src/core/tools/task/bundled-agents/explore.md +82 -0
  153. package/src/core/tools/task/bundled-agents/plan.md +54 -0
  154. package/src/core/tools/task/bundled-agents/reviewer.md +59 -0
  155. package/src/core/tools/task/bundled-agents/task.md +53 -0
  156. package/src/core/tools/task/bundled-commands/architect-plan.md +10 -0
  157. package/src/core/tools/task/bundled-commands/implement-with-critic.md +11 -0
  158. package/src/core/tools/task/bundled-commands/implement.md +11 -0
  159. package/src/core/tools/task/commands.ts +213 -0
  160. package/src/core/tools/task/discovery.ts +208 -0
  161. package/src/core/tools/task/executor.ts +367 -0
  162. package/src/core/tools/task/index.ts +388 -0
  163. package/src/core/tools/task/model-resolver.ts +115 -0
  164. package/src/core/tools/task/parallel.ts +38 -0
  165. package/src/core/tools/task/render.ts +232 -0
  166. package/src/core/tools/task/types.ts +99 -0
  167. package/src/core/tools/truncate.ts +265 -0
  168. package/src/core/tools/web-fetch.ts +2370 -0
  169. package/src/core/tools/web-search/auth.ts +193 -0
  170. package/src/core/tools/web-search/index.ts +537 -0
  171. package/src/core/tools/web-search/providers/anthropic.ts +198 -0
  172. package/src/core/tools/web-search/providers/exa.ts +302 -0
  173. package/src/core/tools/web-search/providers/perplexity.ts +195 -0
  174. package/src/core/tools/web-search/render.ts +182 -0
  175. package/src/core/tools/web-search/types.ts +180 -0
  176. package/src/core/tools/write.ts +99 -0
  177. package/src/index.ts +176 -0
  178. package/src/main.ts +464 -0
  179. package/src/migrations.ts +135 -0
  180. package/src/modes/index.ts +43 -0
  181. package/src/modes/interactive/components/armin.ts +382 -0
  182. package/src/modes/interactive/components/assistant-message.ts +86 -0
  183. package/src/modes/interactive/components/bash-execution.ts +196 -0
  184. package/src/modes/interactive/components/bordered-loader.ts +41 -0
  185. package/src/modes/interactive/components/branch-summary-message.ts +42 -0
  186. package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
  187. package/src/modes/interactive/components/custom-editor.ts +122 -0
  188. package/src/modes/interactive/components/diff.ts +147 -0
  189. package/src/modes/interactive/components/dynamic-border.ts +25 -0
  190. package/src/modes/interactive/components/footer.ts +381 -0
  191. package/src/modes/interactive/components/hook-editor.ts +117 -0
  192. package/src/modes/interactive/components/hook-input.ts +64 -0
  193. package/src/modes/interactive/components/hook-message.ts +96 -0
  194. package/src/modes/interactive/components/hook-selector.ts +91 -0
  195. package/src/modes/interactive/components/model-selector.ts +247 -0
  196. package/src/modes/interactive/components/oauth-selector.ts +120 -0
  197. package/src/modes/interactive/components/plugin-settings.ts +479 -0
  198. package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
  199. package/src/modes/interactive/components/session-selector.ts +204 -0
  200. package/src/modes/interactive/components/settings-selector.ts +453 -0
  201. package/src/modes/interactive/components/show-images-selector.ts +45 -0
  202. package/src/modes/interactive/components/theme-selector.ts +62 -0
  203. package/src/modes/interactive/components/thinking-selector.ts +64 -0
  204. package/src/modes/interactive/components/tool-execution.ts +675 -0
  205. package/src/modes/interactive/components/tree-selector.ts +866 -0
  206. package/src/modes/interactive/components/user-message-selector.ts +159 -0
  207. package/src/modes/interactive/components/user-message.ts +18 -0
  208. package/src/modes/interactive/components/visual-truncate.ts +50 -0
  209. package/src/modes/interactive/components/welcome.ts +183 -0
  210. package/src/modes/interactive/interactive-mode.ts +2516 -0
  211. package/src/modes/interactive/theme/dark.json +101 -0
  212. package/src/modes/interactive/theme/light.json +98 -0
  213. package/src/modes/interactive/theme/theme-schema.json +308 -0
  214. package/src/modes/interactive/theme/theme.ts +998 -0
  215. package/src/modes/print-mode.ts +128 -0
  216. package/src/modes/rpc/rpc-client.ts +527 -0
  217. package/src/modes/rpc/rpc-mode.ts +483 -0
  218. package/src/modes/rpc/rpc-types.ts +203 -0
  219. package/src/utils/changelog.ts +99 -0
  220. package/src/utils/clipboard.ts +265 -0
  221. package/src/utils/fuzzy.ts +108 -0
  222. package/src/utils/mime.ts +30 -0
  223. package/src/utils/shell.ts +276 -0
  224. package/src/utils/tools-manager.ts +274 -0
@@ -0,0 +1,486 @@
1
+ import path from "node:path";
2
+ import type {
3
+ Diagnostic,
4
+ DiagnosticSeverity,
5
+ DocumentSymbol,
6
+ Location,
7
+ SymbolInformation,
8
+ SymbolKind,
9
+ TextEdit,
10
+ WorkspaceEdit,
11
+ } from "./types.js";
12
+
13
+ // =============================================================================
14
+ // Language Detection
15
+ // =============================================================================
16
+
17
+ const LANGUAGE_MAP: Record<string, string> = {
18
+ // TypeScript/JavaScript
19
+ ".ts": "typescript",
20
+ ".tsx": "typescriptreact",
21
+ ".js": "javascript",
22
+ ".jsx": "javascriptreact",
23
+ ".mjs": "javascript",
24
+ ".cjs": "javascript",
25
+ ".mts": "typescript",
26
+ ".cts": "typescript",
27
+
28
+ // Systems languages
29
+ ".rs": "rust",
30
+ ".go": "go",
31
+ ".c": "c",
32
+ ".h": "c",
33
+ ".cpp": "cpp",
34
+ ".cc": "cpp",
35
+ ".cxx": "cpp",
36
+ ".hpp": "cpp",
37
+ ".hxx": "cpp",
38
+ ".zig": "zig",
39
+
40
+ // Scripting languages
41
+ ".py": "python",
42
+ ".rb": "ruby",
43
+ ".lua": "lua",
44
+ ".sh": "shellscript",
45
+ ".bash": "shellscript",
46
+ ".zsh": "shellscript",
47
+ ".fish": "fish",
48
+ ".pl": "perl",
49
+ ".php": "php",
50
+
51
+ // JVM languages
52
+ ".java": "java",
53
+ ".kt": "kotlin",
54
+ ".kts": "kotlin",
55
+ ".scala": "scala",
56
+ ".groovy": "groovy",
57
+ ".clj": "clojure",
58
+
59
+ // .NET languages
60
+ ".cs": "csharp",
61
+ ".fs": "fsharp",
62
+ ".vb": "vb",
63
+
64
+ // Web
65
+ ".html": "html",
66
+ ".htm": "html",
67
+ ".css": "css",
68
+ ".scss": "scss",
69
+ ".sass": "sass",
70
+ ".less": "less",
71
+ ".vue": "vue",
72
+ ".svelte": "svelte",
73
+
74
+ // Data formats
75
+ ".json": "json",
76
+ ".jsonc": "jsonc",
77
+ ".yaml": "yaml",
78
+ ".yml": "yaml",
79
+ ".toml": "toml",
80
+ ".xml": "xml",
81
+ ".ini": "ini",
82
+
83
+ // Documentation
84
+ ".md": "markdown",
85
+ ".markdown": "markdown",
86
+ ".rst": "restructuredtext",
87
+ ".adoc": "asciidoc",
88
+ ".tex": "latex",
89
+
90
+ // Other
91
+ ".sql": "sql",
92
+ ".graphql": "graphql",
93
+ ".gql": "graphql",
94
+ ".proto": "protobuf",
95
+ ".dockerfile": "dockerfile",
96
+ ".tf": "terraform",
97
+ ".hcl": "hcl",
98
+ ".nix": "nix",
99
+ ".ex": "elixir",
100
+ ".exs": "elixir",
101
+ ".erl": "erlang",
102
+ ".hrl": "erlang",
103
+ ".hs": "haskell",
104
+ ".ml": "ocaml",
105
+ ".mli": "ocaml",
106
+ ".swift": "swift",
107
+ ".r": "r",
108
+ ".R": "r",
109
+ ".jl": "julia",
110
+ ".dart": "dart",
111
+ ".elm": "elm",
112
+ ".v": "v",
113
+ ".nim": "nim",
114
+ ".cr": "crystal",
115
+ ".d": "d",
116
+ ".pas": "pascal",
117
+ ".pp": "pascal",
118
+ ".lisp": "lisp",
119
+ ".lsp": "lisp",
120
+ ".rkt": "racket",
121
+ ".scm": "scheme",
122
+ ".ps1": "powershell",
123
+ ".psm1": "powershell",
124
+ ".bat": "bat",
125
+ ".cmd": "bat",
126
+ };
127
+
128
+ /**
129
+ * Detect language ID from file path.
130
+ * Returns the LSP language identifier for the file type.
131
+ */
132
+ export function detectLanguageId(filePath: string): string {
133
+ const ext = path.extname(filePath).toLowerCase();
134
+ const basename = path.basename(filePath).toLowerCase();
135
+
136
+ // Handle special filenames
137
+ if (basename === "dockerfile" || basename.startsWith("dockerfile.")) {
138
+ return "dockerfile";
139
+ }
140
+ if (basename === "makefile" || basename === "gnumakefile") {
141
+ return "makefile";
142
+ }
143
+ if (basename === "cmakelists.txt" || ext === ".cmake") {
144
+ return "cmake";
145
+ }
146
+
147
+ return LANGUAGE_MAP[ext] ?? "plaintext";
148
+ }
149
+
150
+ // =============================================================================
151
+ // URI Handling (Cross-Platform)
152
+ // =============================================================================
153
+
154
+ /**
155
+ * Convert a file path to a file:// URI.
156
+ * Handles Windows drive letters correctly.
157
+ */
158
+ export function fileToUri(filePath: string): string {
159
+ const resolved = path.resolve(filePath);
160
+
161
+ if (process.platform === "win32") {
162
+ // Windows: file:///C:/path/to/file
163
+ return `file:///${resolved.replace(/\\/g, "/")}`;
164
+ }
165
+
166
+ // Unix: file:///path/to/file
167
+ return `file://${resolved}`;
168
+ }
169
+
170
+ /**
171
+ * Convert a file:// URI to a file path.
172
+ * Handles Windows drive letters correctly.
173
+ */
174
+ export function uriToFile(uri: string): string {
175
+ if (!uri.startsWith("file://")) {
176
+ return uri;
177
+ }
178
+
179
+ let filePath = decodeURIComponent(uri.slice(7));
180
+
181
+ // Windows: file:///C:/path → C:/path (strip leading slash before drive letter)
182
+ if (process.platform === "win32" && filePath.startsWith("/") && /^[A-Za-z]:/.test(filePath.slice(1))) {
183
+ filePath = filePath.slice(1);
184
+ }
185
+
186
+ return filePath;
187
+ }
188
+
189
+ // =============================================================================
190
+ // Diagnostic Formatting
191
+ // =============================================================================
192
+
193
+ const SEVERITY_NAMES: Record<DiagnosticSeverity, string> = {
194
+ 1: "error",
195
+ 2: "warning",
196
+ 3: "info",
197
+ 4: "hint",
198
+ };
199
+
200
+ const SEVERITY_ICONS: Record<DiagnosticSeverity, string> = {
201
+ 1: "✖",
202
+ 2: "⚠",
203
+ 3: "ℹ",
204
+ 4: "💡",
205
+ };
206
+
207
+ /**
208
+ * Convert diagnostic severity number to string name.
209
+ */
210
+ export function severityToString(severity?: DiagnosticSeverity): string {
211
+ return SEVERITY_NAMES[severity ?? 1] ?? "unknown";
212
+ }
213
+
214
+ /**
215
+ * Get icon for diagnostic severity.
216
+ */
217
+ export function severityToIcon(severity?: DiagnosticSeverity): string {
218
+ return SEVERITY_ICONS[severity ?? 1] ?? "?";
219
+ }
220
+
221
+ /**
222
+ * Format a diagnostic as a human-readable string.
223
+ */
224
+ export function formatDiagnostic(diagnostic: Diagnostic, filePath: string): string {
225
+ const severity = severityToString(diagnostic.severity);
226
+ const line = diagnostic.range.start.line + 1;
227
+ const col = diagnostic.range.start.character + 1;
228
+ const source = diagnostic.source ? `[${diagnostic.source}] ` : "";
229
+ const code = diagnostic.code ? ` (${diagnostic.code})` : "";
230
+
231
+ return `${filePath}:${line}:${col} [${severity}] ${source}${diagnostic.message}${code}`;
232
+ }
233
+
234
+ /**
235
+ * Format diagnostics grouped by severity.
236
+ */
237
+ export function formatDiagnosticsSummary(diagnostics: Diagnostic[]): string {
238
+ const counts = { error: 0, warning: 0, info: 0, hint: 0 };
239
+
240
+ for (const d of diagnostics) {
241
+ const sev = severityToString(d.severity);
242
+ if (sev in counts) {
243
+ counts[sev as keyof typeof counts]++;
244
+ }
245
+ }
246
+
247
+ const parts: string[] = [];
248
+ if (counts.error > 0) parts.push(`${counts.error} error(s)`);
249
+ if (counts.warning > 0) parts.push(`${counts.warning} warning(s)`);
250
+ if (counts.info > 0) parts.push(`${counts.info} info(s)`);
251
+ if (counts.hint > 0) parts.push(`${counts.hint} hint(s)`);
252
+
253
+ return parts.length > 0 ? parts.join(", ") : "no issues";
254
+ }
255
+
256
+ // =============================================================================
257
+ // Location Formatting
258
+ // =============================================================================
259
+
260
+ /**
261
+ * Format a location as file:line:col relative to cwd.
262
+ */
263
+ export function formatLocation(location: Location, cwd: string): string {
264
+ const file = path.relative(cwd, uriToFile(location.uri));
265
+ const line = location.range.start.line + 1;
266
+ const col = location.range.start.character + 1;
267
+ return `${file}:${line}:${col}`;
268
+ }
269
+
270
+ /**
271
+ * Format a position as line:col.
272
+ */
273
+ export function formatPosition(line: number, col: number): string {
274
+ return `${line}:${col}`;
275
+ }
276
+
277
+ // =============================================================================
278
+ // WorkspaceEdit Formatting
279
+ // =============================================================================
280
+
281
+ /**
282
+ * Format a workspace edit as a summary of changes.
283
+ */
284
+ export function formatWorkspaceEdit(edit: WorkspaceEdit, cwd: string): string[] {
285
+ const results: string[] = [];
286
+
287
+ // Handle changes map (legacy format)
288
+ if (edit.changes) {
289
+ for (const [uri, textEdits] of Object.entries(edit.changes)) {
290
+ const file = path.relative(cwd, uriToFile(uri));
291
+ results.push(`${file}: ${textEdits.length} edit${textEdits.length > 1 ? "s" : ""}`);
292
+ }
293
+ }
294
+
295
+ // Handle documentChanges array (modern format)
296
+ if (edit.documentChanges) {
297
+ for (const change of edit.documentChanges) {
298
+ if ("edits" in change && change.textDocument) {
299
+ const file = path.relative(cwd, uriToFile(change.textDocument.uri));
300
+ results.push(`${file}: ${change.edits.length} edit${change.edits.length > 1 ? "s" : ""}`);
301
+ } else if ("kind" in change) {
302
+ switch (change.kind) {
303
+ case "create":
304
+ results.push(`CREATE: ${path.relative(cwd, uriToFile(change.uri))}`);
305
+ break;
306
+ case "rename":
307
+ results.push(
308
+ `RENAME: ${path.relative(cwd, uriToFile(change.oldUri))} → ${path.relative(cwd, uriToFile(change.newUri))}`,
309
+ );
310
+ break;
311
+ case "delete":
312
+ results.push(`DELETE: ${path.relative(cwd, uriToFile(change.uri))}`);
313
+ break;
314
+ }
315
+ }
316
+ }
317
+ }
318
+
319
+ return results;
320
+ }
321
+
322
+ /**
323
+ * Format a text edit as a preview.
324
+ */
325
+ export function formatTextEdit(edit: TextEdit, maxLength = 50): string {
326
+ const range = `${edit.range.start.line + 1}:${edit.range.start.character + 1}`;
327
+ const preview =
328
+ edit.newText.length > maxLength
329
+ ? `${edit.newText.slice(0, maxLength).replace(/\n/g, "\\n")}...`
330
+ : edit.newText.replace(/\n/g, "\\n");
331
+ return `line ${range} → "${preview}"`;
332
+ }
333
+
334
+ // =============================================================================
335
+ // Symbol Formatting
336
+ // =============================================================================
337
+
338
+ const SYMBOL_KIND_ICONS: Partial<Record<SymbolKind, string>> = {
339
+ 5: "○", // Class
340
+ 6: "ƒ", // Method
341
+ 11: "◇", // Interface
342
+ 12: "ƒ", // Function
343
+ 13: "◆", // Variable
344
+ 14: "◆", // Constant
345
+ 10: "◎", // Enum
346
+ 23: "□", // Struct
347
+ 2: "◫", // Module
348
+ };
349
+
350
+ /**
351
+ * Get icon for symbol kind.
352
+ */
353
+ export function symbolKindToIcon(kind: SymbolKind): string {
354
+ return SYMBOL_KIND_ICONS[kind] ?? "•";
355
+ }
356
+
357
+ /**
358
+ * Get name for symbol kind.
359
+ */
360
+ export function symbolKindToName(kind: SymbolKind): string {
361
+ const names: Record<number, string> = {
362
+ 1: "File",
363
+ 2: "Module",
364
+ 3: "Namespace",
365
+ 4: "Package",
366
+ 5: "Class",
367
+ 6: "Method",
368
+ 7: "Property",
369
+ 8: "Field",
370
+ 9: "Constructor",
371
+ 10: "Enum",
372
+ 11: "Interface",
373
+ 12: "Function",
374
+ 13: "Variable",
375
+ 14: "Constant",
376
+ 15: "String",
377
+ 16: "Number",
378
+ 17: "Boolean",
379
+ 18: "Array",
380
+ 19: "Object",
381
+ 20: "Key",
382
+ 21: "Null",
383
+ 22: "EnumMember",
384
+ 23: "Struct",
385
+ 24: "Event",
386
+ 25: "Operator",
387
+ 26: "TypeParameter",
388
+ };
389
+ return names[kind] ?? "Unknown";
390
+ }
391
+
392
+ /**
393
+ * Format a document symbol with optional hierarchy.
394
+ */
395
+ export function formatDocumentSymbol(symbol: DocumentSymbol, indent = 0): string[] {
396
+ const prefix = " ".repeat(indent);
397
+ const icon = symbolKindToIcon(symbol.kind);
398
+ const line = symbol.range.start.line + 1;
399
+ const results = [`${prefix}${icon} ${symbol.name} @ line ${line}`];
400
+
401
+ if (symbol.children) {
402
+ for (const child of symbol.children) {
403
+ results.push(...formatDocumentSymbol(child, indent + 1));
404
+ }
405
+ }
406
+
407
+ return results;
408
+ }
409
+
410
+ /**
411
+ * Format a symbol information (flat format).
412
+ */
413
+ export function formatSymbolInformation(symbol: SymbolInformation, cwd: string): string {
414
+ const icon = symbolKindToIcon(symbol.kind);
415
+ const location = formatLocation(symbol.location, cwd);
416
+ const container = symbol.containerName ? ` (${symbol.containerName})` : "";
417
+ return `${icon} ${symbol.name}${container} @ ${location}`;
418
+ }
419
+
420
+ // =============================================================================
421
+ // Hover Content Extraction
422
+ // =============================================================================
423
+
424
+ /**
425
+ * Extract plain text from hover contents.
426
+ */
427
+ export function extractHoverText(
428
+ contents: string | { kind: string; value: string } | { language: string; value: string } | unknown[],
429
+ ): string {
430
+ if (typeof contents === "string") {
431
+ return contents;
432
+ }
433
+
434
+ if (Array.isArray(contents)) {
435
+ return contents.map((c) => extractHoverText(c as string | { kind: string; value: string })).join("\n\n");
436
+ }
437
+
438
+ if (typeof contents === "object" && contents !== null) {
439
+ if ("value" in contents && typeof contents.value === "string") {
440
+ return contents.value;
441
+ }
442
+ }
443
+
444
+ return String(contents);
445
+ }
446
+
447
+ // =============================================================================
448
+ // General Utilities
449
+ // =============================================================================
450
+
451
+ /**
452
+ * Sleep for the specified number of milliseconds.
453
+ */
454
+ export function sleep(ms: number): Promise<void> {
455
+ return new Promise((resolve) => setTimeout(resolve, ms));
456
+ }
457
+
458
+ /**
459
+ * Check if a command exists in PATH.
460
+ */
461
+ export async function commandExists(command: string): Promise<boolean> {
462
+ return Bun.which(command) !== null;
463
+ }
464
+
465
+ /**
466
+ * Truncate a string to a maximum length with ellipsis.
467
+ */
468
+ export function truncate(str: string, maxLength: number): string {
469
+ if (str.length <= maxLength) return str;
470
+ return `${str.slice(0, maxLength - 3)}...`;
471
+ }
472
+
473
+ /**
474
+ * Group items by a key function.
475
+ */
476
+ export function groupBy<T, K extends string | number>(items: T[], keyFn: (item: T) => K): Record<K, T[]> {
477
+ const result = {} as Record<K, T[]>;
478
+ for (const item of items) {
479
+ const key = keyFn(item);
480
+ if (!result[key]) {
481
+ result[key] = [];
482
+ }
483
+ result[key].push(item);
484
+ }
485
+ return result;
486
+ }
@@ -0,0 +1,229 @@
1
+ import type { AgentTool } from "@oh-my-pi/pi-agent-core";
2
+ import { Type } from "@sinclair/typebox";
3
+ import { resolveToCwd } from "./path-utils.js";
4
+
5
+ const notebookSchema = Type.Object({
6
+ action: Type.Union([Type.Literal("edit"), Type.Literal("insert"), Type.Literal("delete")], {
7
+ description: "Action to perform on the notebook cell",
8
+ }),
9
+ notebook_path: Type.String({ description: "Path to the .ipynb file (relative or absolute)" }),
10
+ cell_index: Type.Number({ description: "0-based index of the cell to operate on" }),
11
+ content: Type.Optional(Type.String({ description: "New cell content (required for edit/insert)" })),
12
+ cell_type: Type.Optional(
13
+ Type.Union([Type.Literal("code"), Type.Literal("markdown")], {
14
+ description: "Cell type for insert (default: code)",
15
+ }),
16
+ ),
17
+ });
18
+
19
+ export interface NotebookToolDetails {
20
+ /** Action performed */
21
+ action: "edit" | "insert" | "delete";
22
+ /** Cell index operated on */
23
+ cellIndex: number;
24
+ /** Cell type */
25
+ cellType?: string;
26
+ /** Total cell count after operation */
27
+ totalCells: number;
28
+ }
29
+
30
+ interface NotebookCell {
31
+ cell_type: "code" | "markdown" | "raw";
32
+ source: string[];
33
+ metadata: Record<string, unknown>;
34
+ execution_count?: number | null;
35
+ outputs?: unknown[];
36
+ }
37
+
38
+ interface Notebook {
39
+ cells: NotebookCell[];
40
+ metadata: Record<string, unknown>;
41
+ nbformat: number;
42
+ nbformat_minor: number;
43
+ }
44
+
45
+ function splitIntoLines(content: string): string[] {
46
+ return content.split("\n").map((line, i, arr) => (i < arr.length - 1 ? `${line}\n` : line));
47
+ }
48
+
49
+ export function createNotebookTool(cwd: string): AgentTool<typeof notebookSchema> {
50
+ return {
51
+ name: "notebook",
52
+ label: "Notebook",
53
+ description:
54
+ "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.",
55
+ parameters: notebookSchema,
56
+ execute: async (
57
+ _toolCallId: string,
58
+ {
59
+ action,
60
+ notebook_path,
61
+ cell_index,
62
+ content,
63
+ cell_type,
64
+ }: { action: string; notebook_path: string; cell_index: number; content?: string; cell_type?: string },
65
+ signal?: AbortSignal,
66
+ ) => {
67
+ const absolutePath = resolveToCwd(notebook_path, cwd);
68
+
69
+ return new Promise<{
70
+ content: Array<{ type: "text"; text: string }>;
71
+ details: NotebookToolDetails | undefined;
72
+ }>((resolve, reject) => {
73
+ if (signal?.aborted) {
74
+ reject(new Error("Operation aborted"));
75
+ return;
76
+ }
77
+
78
+ let aborted = false;
79
+
80
+ const onAbort = () => {
81
+ aborted = true;
82
+ reject(new Error("Operation aborted"));
83
+ };
84
+
85
+ if (signal) {
86
+ signal.addEventListener("abort", onAbort, { once: true });
87
+ }
88
+
89
+ (async () => {
90
+ try {
91
+ // Check if file exists
92
+ const file = Bun.file(absolutePath);
93
+ if (!(await file.exists())) {
94
+ if (signal) signal.removeEventListener("abort", onAbort);
95
+ reject(new Error(`Notebook not found: ${notebook_path}`));
96
+ return;
97
+ }
98
+
99
+ if (aborted) return;
100
+
101
+ // Read and parse notebook
102
+ let notebook: Notebook;
103
+ try {
104
+ notebook = await file.json();
105
+ } catch {
106
+ if (signal) signal.removeEventListener("abort", onAbort);
107
+ reject(new Error(`Invalid JSON in notebook: ${notebook_path}`));
108
+ return;
109
+ }
110
+
111
+ if (aborted) return;
112
+
113
+ // Validate notebook structure
114
+ if (!notebook.cells || !Array.isArray(notebook.cells)) {
115
+ if (signal) signal.removeEventListener("abort", onAbort);
116
+ reject(new Error(`Invalid notebook structure (missing cells array): ${notebook_path}`));
117
+ return;
118
+ }
119
+
120
+ const cellCount = notebook.cells.length;
121
+
122
+ // Validate cell_index based on action
123
+ if (action === "insert") {
124
+ if (cell_index < 0 || cell_index > cellCount) {
125
+ if (signal) signal.removeEventListener("abort", onAbort);
126
+ reject(
127
+ new Error(
128
+ `Cell index ${cell_index} out of range for insert (0-${cellCount}) in ${notebook_path}`,
129
+ ),
130
+ );
131
+ return;
132
+ }
133
+ } else {
134
+ if (cell_index < 0 || cell_index >= cellCount) {
135
+ if (signal) signal.removeEventListener("abort", onAbort);
136
+ reject(
137
+ new Error(`Cell index ${cell_index} out of range (0-${cellCount - 1}) in ${notebook_path}`),
138
+ );
139
+ return;
140
+ }
141
+ }
142
+
143
+ // Validate content for edit/insert
144
+ if ((action === "edit" || action === "insert") && content === undefined) {
145
+ if (signal) signal.removeEventListener("abort", onAbort);
146
+ reject(new Error(`Content is required for ${action} action`));
147
+ return;
148
+ }
149
+
150
+ if (aborted) return;
151
+
152
+ // Perform the action
153
+ let resultMessage: string;
154
+ let finalCellType: string | undefined;
155
+
156
+ switch (action) {
157
+ case "edit": {
158
+ const sourceLines = splitIntoLines(content!);
159
+ notebook.cells[cell_index].source = sourceLines;
160
+ finalCellType = notebook.cells[cell_index].cell_type;
161
+ resultMessage = `Replaced cell ${cell_index} (${finalCellType})`;
162
+ break;
163
+ }
164
+ case "insert": {
165
+ const sourceLines = splitIntoLines(content!);
166
+ const newCellType = (cell_type as "code" | "markdown") || "code";
167
+ const newCell: NotebookCell = {
168
+ cell_type: newCellType,
169
+ source: sourceLines,
170
+ metadata: {},
171
+ };
172
+ if (newCellType === "code") {
173
+ newCell.execution_count = null;
174
+ newCell.outputs = [];
175
+ }
176
+ notebook.cells.splice(cell_index, 0, newCell);
177
+ finalCellType = newCellType;
178
+ resultMessage = `Inserted ${newCellType} cell at position ${cell_index}`;
179
+ break;
180
+ }
181
+ case "delete": {
182
+ finalCellType = notebook.cells[cell_index].cell_type;
183
+ notebook.cells.splice(cell_index, 1);
184
+ resultMessage = `Deleted cell ${cell_index} (${finalCellType})`;
185
+ break;
186
+ }
187
+ default: {
188
+ if (signal) signal.removeEventListener("abort", onAbort);
189
+ reject(new Error(`Invalid action: ${action}`));
190
+ return;
191
+ }
192
+ }
193
+
194
+ if (aborted) return;
195
+
196
+ // Write back with single-space indentation
197
+ await Bun.write(absolutePath, JSON.stringify(notebook, null, 1));
198
+
199
+ if (aborted) return;
200
+
201
+ if (signal) signal.removeEventListener("abort", onAbort);
202
+
203
+ const newCellCount = notebook.cells.length;
204
+ resolve({
205
+ content: [
206
+ {
207
+ type: "text",
208
+ text: `${resultMessage}. Notebook now has ${newCellCount} cells.`,
209
+ },
210
+ ],
211
+ details: {
212
+ action: action as "edit" | "insert" | "delete",
213
+ cellIndex: cell_index,
214
+ cellType: finalCellType,
215
+ totalCells: newCellCount,
216
+ },
217
+ });
218
+ } catch (error: any) {
219
+ if (signal) signal.removeEventListener("abort", onAbort);
220
+ if (!aborted) reject(error);
221
+ }
222
+ })();
223
+ });
224
+ },
225
+ };
226
+ }
227
+
228
+ /** Default notebook tool using process.cwd() */
229
+ export const notebookTool = createNotebookTool(process.cwd());