@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.
- package/CHANGELOG.md +1228 -0
- package/README.md +1041 -0
- package/docs/compaction.md +403 -0
- package/docs/custom-tools.md +541 -0
- package/docs/extension-loading.md +1004 -0
- package/docs/hooks.md +867 -0
- package/docs/rpc.md +1040 -0
- package/docs/sdk.md +994 -0
- package/docs/session-tree-plan.md +441 -0
- package/docs/session.md +240 -0
- package/docs/skills.md +290 -0
- package/docs/theme.md +637 -0
- package/docs/tree.md +197 -0
- package/docs/tui.md +341 -0
- package/examples/README.md +21 -0
- package/examples/custom-tools/README.md +124 -0
- package/examples/custom-tools/hello/index.ts +20 -0
- package/examples/custom-tools/question/index.ts +84 -0
- package/examples/custom-tools/subagent/README.md +172 -0
- package/examples/custom-tools/subagent/agents/planner.md +37 -0
- package/examples/custom-tools/subagent/agents/reviewer.md +35 -0
- package/examples/custom-tools/subagent/agents/scout.md +50 -0
- package/examples/custom-tools/subagent/agents/worker.md +24 -0
- package/examples/custom-tools/subagent/agents.ts +156 -0
- package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
- package/examples/custom-tools/subagent/commands/implement.md +10 -0
- package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
- package/examples/custom-tools/subagent/index.ts +1002 -0
- package/examples/custom-tools/todo/index.ts +212 -0
- package/examples/hooks/README.md +56 -0
- package/examples/hooks/auto-commit-on-exit.ts +49 -0
- package/examples/hooks/confirm-destructive.ts +59 -0
- package/examples/hooks/custom-compaction.ts +116 -0
- package/examples/hooks/dirty-repo-guard.ts +52 -0
- package/examples/hooks/file-trigger.ts +41 -0
- package/examples/hooks/git-checkpoint.ts +53 -0
- package/examples/hooks/handoff.ts +150 -0
- package/examples/hooks/permission-gate.ts +34 -0
- package/examples/hooks/protected-paths.ts +30 -0
- package/examples/hooks/qna.ts +119 -0
- package/examples/hooks/snake.ts +343 -0
- package/examples/hooks/status-line.ts +40 -0
- package/examples/sdk/01-minimal.ts +22 -0
- package/examples/sdk/02-custom-model.ts +49 -0
- package/examples/sdk/03-custom-prompt.ts +44 -0
- package/examples/sdk/04-skills.ts +44 -0
- package/examples/sdk/05-tools.ts +90 -0
- package/examples/sdk/06-hooks.ts +61 -0
- package/examples/sdk/07-context-files.ts +36 -0
- package/examples/sdk/08-slash-commands.ts +42 -0
- package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
- package/examples/sdk/10-settings.ts +38 -0
- package/examples/sdk/11-sessions.ts +48 -0
- package/examples/sdk/12-full-control.ts +95 -0
- package/examples/sdk/README.md +154 -0
- package/package.json +81 -0
- package/src/cli/args.ts +246 -0
- package/src/cli/file-processor.ts +72 -0
- package/src/cli/list-models.ts +104 -0
- package/src/cli/plugin-cli.ts +650 -0
- package/src/cli/session-picker.ts +41 -0
- package/src/cli.ts +10 -0
- package/src/commands/init.md +20 -0
- package/src/config.ts +159 -0
- package/src/core/agent-session.ts +1900 -0
- package/src/core/auth-storage.ts +236 -0
- package/src/core/bash-executor.ts +196 -0
- package/src/core/compaction/branch-summarization.ts +343 -0
- package/src/core/compaction/compaction.ts +742 -0
- package/src/core/compaction/index.ts +7 -0
- package/src/core/compaction/utils.ts +154 -0
- package/src/core/custom-tools/index.ts +21 -0
- package/src/core/custom-tools/loader.ts +248 -0
- package/src/core/custom-tools/types.ts +169 -0
- package/src/core/custom-tools/wrapper.ts +28 -0
- package/src/core/exec.ts +129 -0
- package/src/core/export-html/index.ts +211 -0
- package/src/core/export-html/template.css +781 -0
- package/src/core/export-html/template.html +54 -0
- package/src/core/export-html/template.js +1185 -0
- package/src/core/export-html/vendor/highlight.min.js +1213 -0
- package/src/core/export-html/vendor/marked.min.js +6 -0
- package/src/core/hooks/index.ts +16 -0
- package/src/core/hooks/loader.ts +312 -0
- package/src/core/hooks/runner.ts +434 -0
- package/src/core/hooks/tool-wrapper.ts +99 -0
- package/src/core/hooks/types.ts +773 -0
- package/src/core/index.ts +52 -0
- package/src/core/mcp/client.ts +158 -0
- package/src/core/mcp/config.ts +154 -0
- package/src/core/mcp/index.ts +45 -0
- package/src/core/mcp/loader.ts +68 -0
- package/src/core/mcp/manager.ts +181 -0
- package/src/core/mcp/tool-bridge.ts +148 -0
- package/src/core/mcp/transports/http.ts +316 -0
- package/src/core/mcp/transports/index.ts +6 -0
- package/src/core/mcp/transports/stdio.ts +252 -0
- package/src/core/mcp/types.ts +220 -0
- package/src/core/messages.ts +189 -0
- package/src/core/model-registry.ts +317 -0
- package/src/core/model-resolver.ts +393 -0
- package/src/core/plugins/doctor.ts +59 -0
- package/src/core/plugins/index.ts +38 -0
- package/src/core/plugins/installer.ts +189 -0
- package/src/core/plugins/loader.ts +338 -0
- package/src/core/plugins/manager.ts +672 -0
- package/src/core/plugins/parser.ts +105 -0
- package/src/core/plugins/paths.ts +32 -0
- package/src/core/plugins/types.ts +190 -0
- package/src/core/sdk.ts +760 -0
- package/src/core/session-manager.ts +1128 -0
- package/src/core/settings-manager.ts +443 -0
- package/src/core/skills.ts +437 -0
- package/src/core/slash-commands.ts +248 -0
- package/src/core/system-prompt.ts +439 -0
- package/src/core/timings.ts +25 -0
- package/src/core/tools/ask.ts +211 -0
- package/src/core/tools/bash-interceptor.ts +120 -0
- package/src/core/tools/bash.ts +250 -0
- package/src/core/tools/context.ts +32 -0
- package/src/core/tools/edit-diff.ts +475 -0
- package/src/core/tools/edit.ts +208 -0
- package/src/core/tools/exa/company.ts +59 -0
- package/src/core/tools/exa/index.ts +64 -0
- package/src/core/tools/exa/linkedin.ts +59 -0
- package/src/core/tools/exa/logger.ts +56 -0
- package/src/core/tools/exa/mcp-client.ts +368 -0
- package/src/core/tools/exa/render.ts +196 -0
- package/src/core/tools/exa/researcher.ts +90 -0
- package/src/core/tools/exa/search.ts +337 -0
- package/src/core/tools/exa/types.ts +168 -0
- package/src/core/tools/exa/websets.ts +248 -0
- package/src/core/tools/find.ts +261 -0
- package/src/core/tools/grep.ts +555 -0
- package/src/core/tools/index.ts +202 -0
- package/src/core/tools/ls.ts +140 -0
- package/src/core/tools/lsp/client.ts +605 -0
- package/src/core/tools/lsp/config.ts +147 -0
- package/src/core/tools/lsp/edits.ts +101 -0
- package/src/core/tools/lsp/index.ts +804 -0
- package/src/core/tools/lsp/render.ts +447 -0
- package/src/core/tools/lsp/rust-analyzer.ts +145 -0
- package/src/core/tools/lsp/types.ts +463 -0
- package/src/core/tools/lsp/utils.ts +486 -0
- package/src/core/tools/notebook.ts +229 -0
- package/src/core/tools/path-utils.ts +61 -0
- package/src/core/tools/read.ts +240 -0
- package/src/core/tools/renderers.ts +540 -0
- package/src/core/tools/task/agents.ts +153 -0
- package/src/core/tools/task/artifacts.ts +114 -0
- package/src/core/tools/task/bundled-agents/browser.md +71 -0
- package/src/core/tools/task/bundled-agents/explore.md +82 -0
- package/src/core/tools/task/bundled-agents/plan.md +54 -0
- package/src/core/tools/task/bundled-agents/reviewer.md +59 -0
- package/src/core/tools/task/bundled-agents/task.md +53 -0
- package/src/core/tools/task/bundled-commands/architect-plan.md +10 -0
- package/src/core/tools/task/bundled-commands/implement-with-critic.md +11 -0
- package/src/core/tools/task/bundled-commands/implement.md +11 -0
- package/src/core/tools/task/commands.ts +213 -0
- package/src/core/tools/task/discovery.ts +208 -0
- package/src/core/tools/task/executor.ts +367 -0
- package/src/core/tools/task/index.ts +388 -0
- package/src/core/tools/task/model-resolver.ts +115 -0
- package/src/core/tools/task/parallel.ts +38 -0
- package/src/core/tools/task/render.ts +232 -0
- package/src/core/tools/task/types.ts +99 -0
- package/src/core/tools/truncate.ts +265 -0
- package/src/core/tools/web-fetch.ts +2370 -0
- package/src/core/tools/web-search/auth.ts +193 -0
- package/src/core/tools/web-search/index.ts +537 -0
- package/src/core/tools/web-search/providers/anthropic.ts +198 -0
- package/src/core/tools/web-search/providers/exa.ts +302 -0
- package/src/core/tools/web-search/providers/perplexity.ts +195 -0
- package/src/core/tools/web-search/render.ts +182 -0
- package/src/core/tools/web-search/types.ts +180 -0
- package/src/core/tools/write.ts +99 -0
- package/src/index.ts +176 -0
- package/src/main.ts +464 -0
- package/src/migrations.ts +135 -0
- package/src/modes/index.ts +43 -0
- package/src/modes/interactive/components/armin.ts +382 -0
- package/src/modes/interactive/components/assistant-message.ts +86 -0
- package/src/modes/interactive/components/bash-execution.ts +196 -0
- package/src/modes/interactive/components/bordered-loader.ts +41 -0
- package/src/modes/interactive/components/branch-summary-message.ts +42 -0
- package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
- package/src/modes/interactive/components/custom-editor.ts +122 -0
- package/src/modes/interactive/components/diff.ts +147 -0
- package/src/modes/interactive/components/dynamic-border.ts +25 -0
- package/src/modes/interactive/components/footer.ts +381 -0
- package/src/modes/interactive/components/hook-editor.ts +117 -0
- package/src/modes/interactive/components/hook-input.ts +64 -0
- package/src/modes/interactive/components/hook-message.ts +96 -0
- package/src/modes/interactive/components/hook-selector.ts +91 -0
- package/src/modes/interactive/components/model-selector.ts +247 -0
- package/src/modes/interactive/components/oauth-selector.ts +120 -0
- package/src/modes/interactive/components/plugin-settings.ts +479 -0
- package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
- package/src/modes/interactive/components/session-selector.ts +204 -0
- package/src/modes/interactive/components/settings-selector.ts +453 -0
- package/src/modes/interactive/components/show-images-selector.ts +45 -0
- package/src/modes/interactive/components/theme-selector.ts +62 -0
- package/src/modes/interactive/components/thinking-selector.ts +64 -0
- package/src/modes/interactive/components/tool-execution.ts +675 -0
- package/src/modes/interactive/components/tree-selector.ts +866 -0
- package/src/modes/interactive/components/user-message-selector.ts +159 -0
- package/src/modes/interactive/components/user-message.ts +18 -0
- package/src/modes/interactive/components/visual-truncate.ts +50 -0
- package/src/modes/interactive/components/welcome.ts +183 -0
- package/src/modes/interactive/interactive-mode.ts +2516 -0
- package/src/modes/interactive/theme/dark.json +101 -0
- package/src/modes/interactive/theme/light.json +98 -0
- package/src/modes/interactive/theme/theme-schema.json +308 -0
- package/src/modes/interactive/theme/theme.ts +998 -0
- package/src/modes/print-mode.ts +128 -0
- package/src/modes/rpc/rpc-client.ts +527 -0
- package/src/modes/rpc/rpc-mode.ts +483 -0
- package/src/modes/rpc/rpc-types.ts +203 -0
- package/src/utils/changelog.ts +99 -0
- package/src/utils/clipboard.ts +265 -0
- package/src/utils/fuzzy.ts +108 -0
- package/src/utils/mime.ts +30 -0
- package/src/utils/shell.ts +276 -0
- 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());
|