@oh-my-pi/pi-coding-agent 3.15.0 → 3.20.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 +61 -1
- package/docs/extensions.md +1055 -0
- package/docs/rpc.md +69 -13
- package/docs/session-tree-plan.md +1 -1
- package/examples/extensions/README.md +141 -0
- package/examples/extensions/api-demo.ts +87 -0
- package/examples/extensions/chalk-logger.ts +26 -0
- package/examples/extensions/hello.ts +33 -0
- package/examples/extensions/pirate.ts +44 -0
- package/examples/extensions/plan-mode.ts +551 -0
- package/examples/extensions/subagent/agents/reviewer.md +35 -0
- package/examples/extensions/todo.ts +299 -0
- package/examples/extensions/tools.ts +145 -0
- package/examples/extensions/with-deps/index.ts +36 -0
- package/examples/extensions/with-deps/package-lock.json +31 -0
- package/examples/extensions/with-deps/package.json +16 -0
- package/examples/sdk/02-custom-model.ts +3 -3
- package/examples/sdk/05-tools.ts +7 -3
- package/examples/sdk/06-extensions.ts +81 -0
- package/examples/sdk/06-hooks.ts +14 -13
- package/examples/sdk/08-prompt-templates.ts +42 -0
- package/examples/sdk/08-slash-commands.ts +17 -12
- package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
- package/examples/sdk/12-full-control.ts +6 -6
- package/package.json +11 -7
- package/src/capability/extension-module.ts +34 -0
- package/src/cli/args.ts +22 -7
- package/src/cli/file-processor.ts +38 -67
- package/src/cli/list-models.ts +1 -1
- package/src/config.ts +25 -14
- package/src/core/agent-session.ts +505 -242
- package/src/core/auth-storage.ts +33 -21
- package/src/core/compaction/branch-summarization.ts +4 -4
- package/src/core/compaction/compaction.ts +3 -3
- package/src/core/custom-commands/bundled/wt/index.ts +430 -0
- package/src/core/custom-commands/loader.ts +9 -0
- package/src/core/custom-tools/wrapper.ts +5 -0
- package/src/core/event-bus.ts +59 -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/extensions/index.ts +100 -0
- package/src/core/extensions/loader.ts +501 -0
- package/src/core/extensions/runner.ts +477 -0
- package/src/core/extensions/types.ts +712 -0
- package/src/core/extensions/wrapper.ts +147 -0
- package/src/core/hooks/types.ts +2 -2
- package/src/core/index.ts +10 -21
- package/src/core/keybindings.ts +199 -0
- package/src/core/messages.ts +26 -7
- package/src/core/model-registry.ts +123 -46
- package/src/core/model-resolver.ts +7 -5
- package/src/core/prompt-templates.ts +242 -0
- package/src/core/sdk.ts +378 -295
- package/src/core/session-manager.ts +72 -58
- package/src/core/settings-manager.ts +118 -22
- package/src/core/system-prompt.ts +24 -1
- package/src/core/terminal-notify.ts +37 -0
- package/src/core/tools/context.ts +4 -4
- package/src/core/tools/exa/mcp-client.ts +5 -4
- package/src/core/tools/exa/render.ts +176 -131
- package/src/core/tools/gemini-image.ts +361 -0
- package/src/core/tools/git.ts +216 -0
- package/src/core/tools/index.ts +28 -15
- package/src/core/tools/lsp/config.ts +5 -4
- package/src/core/tools/lsp/index.ts +17 -12
- package/src/core/tools/lsp/render.ts +39 -47
- package/src/core/tools/read.ts +66 -29
- package/src/core/tools/render-utils.ts +268 -0
- package/src/core/tools/renderers.ts +243 -225
- package/src/core/tools/task/discovery.ts +2 -2
- package/src/core/tools/task/executor.ts +66 -58
- package/src/core/tools/task/index.ts +29 -10
- package/src/core/tools/task/model-resolver.ts +8 -13
- package/src/core/tools/task/omp-command.ts +24 -0
- package/src/core/tools/task/render.ts +35 -60
- package/src/core/tools/task/types.ts +3 -0
- package/src/core/tools/web-fetch.ts +29 -28
- package/src/core/tools/web-search/index.ts +6 -5
- package/src/core/tools/web-search/providers/exa.ts +6 -5
- package/src/core/tools/web-search/render.ts +66 -111
- package/src/core/voice-controller.ts +135 -0
- package/src/core/voice-supervisor.ts +1003 -0
- package/src/core/voice.ts +308 -0
- package/src/discovery/builtin.ts +75 -1
- package/src/discovery/claude.ts +47 -1
- package/src/discovery/codex.ts +54 -2
- package/src/discovery/gemini.ts +55 -2
- package/src/discovery/helpers.ts +100 -1
- package/src/discovery/index.ts +2 -0
- package/src/index.ts +14 -9
- package/src/lib/worktree/collapse.ts +179 -0
- package/src/lib/worktree/constants.ts +14 -0
- package/src/lib/worktree/errors.ts +23 -0
- package/src/lib/worktree/git.ts +110 -0
- package/src/lib/worktree/index.ts +23 -0
- package/src/lib/worktree/operations.ts +216 -0
- package/src/lib/worktree/session.ts +114 -0
- package/src/lib/worktree/stats.ts +67 -0
- package/src/main.ts +61 -37
- package/src/migrations.ts +37 -7
- package/src/modes/interactive/components/bash-execution.ts +6 -4
- package/src/modes/interactive/components/custom-editor.ts +55 -0
- package/src/modes/interactive/components/custom-message.ts +95 -0
- package/src/modes/interactive/components/extensions/extension-list.ts +5 -0
- package/src/modes/interactive/components/extensions/inspector-panel.ts +18 -12
- package/src/modes/interactive/components/extensions/state-manager.ts +12 -0
- package/src/modes/interactive/components/extensions/types.ts +1 -0
- package/src/modes/interactive/components/footer.ts +324 -0
- package/src/modes/interactive/components/hook-editor.ts +1 -0
- package/src/modes/interactive/components/hook-selector.ts +3 -3
- package/src/modes/interactive/components/model-selector.ts +7 -6
- package/src/modes/interactive/components/oauth-selector.ts +3 -3
- package/src/modes/interactive/components/settings-defs.ts +55 -6
- package/src/modes/interactive/components/status-line/separators.ts +4 -4
- package/src/modes/interactive/components/status-line.ts +45 -35
- package/src/modes/interactive/components/tool-execution.ts +95 -23
- package/src/modes/interactive/interactive-mode.ts +644 -113
- package/src/modes/interactive/theme/defaults/alabaster.json +99 -0
- package/src/modes/interactive/theme/defaults/amethyst.json +103 -0
- package/src/modes/interactive/theme/defaults/anthracite.json +100 -0
- package/src/modes/interactive/theme/defaults/basalt.json +90 -0
- package/src/modes/interactive/theme/defaults/birch.json +101 -0
- package/src/modes/interactive/theme/defaults/dark-abyss.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-aurora.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-cavern.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-copper.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-cosmos.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-eclipse.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-ember.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-equinox.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-lavender.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-lunar.json +95 -0
- package/src/modes/interactive/theme/defaults/dark-midnight.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-nebula.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-rainforest.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-reef.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-sakura.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-slate.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-solstice.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-starfall.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-swamp.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-taiga.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-terminal.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-tundra.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-twilight.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-volcanic.json +97 -0
- package/src/modes/interactive/theme/defaults/graphite.json +99 -0
- package/src/modes/interactive/theme/defaults/index.ts +128 -0
- package/src/modes/interactive/theme/defaults/light-aurora-day.json +97 -0
- package/src/modes/interactive/theme/defaults/light-canyon.json +97 -0
- package/src/modes/interactive/theme/defaults/light-cirrus.json +96 -0
- package/src/modes/interactive/theme/defaults/light-coral.json +94 -0
- package/src/modes/interactive/theme/defaults/light-dawn.json +96 -0
- package/src/modes/interactive/theme/defaults/light-dunes.json +97 -0
- package/src/modes/interactive/theme/defaults/light-eucalyptus.json +94 -0
- package/src/modes/interactive/theme/defaults/light-frost.json +94 -0
- package/src/modes/interactive/theme/defaults/light-glacier.json +97 -0
- package/src/modes/interactive/theme/defaults/light-haze.json +96 -0
- package/src/modes/interactive/theme/defaults/light-honeycomb.json +94 -0
- package/src/modes/interactive/theme/defaults/light-lagoon.json +97 -0
- package/src/modes/interactive/theme/defaults/light-lavender.json +94 -0
- package/src/modes/interactive/theme/defaults/light-meadow.json +97 -0
- package/src/modes/interactive/theme/defaults/light-mint.json +94 -0
- package/src/modes/interactive/theme/defaults/light-opal.json +97 -0
- package/src/modes/interactive/theme/defaults/light-orchard.json +97 -0
- package/src/modes/interactive/theme/defaults/light-paper.json +94 -0
- package/src/modes/interactive/theme/defaults/light-prism.json +96 -0
- package/src/modes/interactive/theme/defaults/light-sand.json +94 -0
- package/src/modes/interactive/theme/defaults/light-savanna.json +97 -0
- package/src/modes/interactive/theme/defaults/light-soleil.json +96 -0
- package/src/modes/interactive/theme/defaults/light-wetland.json +97 -0
- package/src/modes/interactive/theme/defaults/light-zenith.json +95 -0
- package/src/modes/interactive/theme/defaults/limestone.json +100 -0
- package/src/modes/interactive/theme/defaults/mahogany.json +104 -0
- package/src/modes/interactive/theme/defaults/marble.json +99 -0
- package/src/modes/interactive/theme/defaults/obsidian.json +90 -0
- package/src/modes/interactive/theme/defaults/onyx.json +90 -0
- package/src/modes/interactive/theme/defaults/pearl.json +99 -0
- package/src/modes/interactive/theme/defaults/porcelain.json +90 -0
- package/src/modes/interactive/theme/defaults/quartz.json +102 -0
- package/src/modes/interactive/theme/defaults/sandstone.json +101 -0
- package/src/modes/interactive/theme/defaults/titanium.json +89 -0
- package/src/modes/print-mode.ts +14 -72
- package/src/modes/rpc/rpc-client.ts +23 -9
- package/src/modes/rpc/rpc-mode.ts +137 -125
- package/src/modes/rpc/rpc-types.ts +46 -24
- package/src/prompts/task.md +1 -0
- package/src/prompts/tools/gemini-image.md +4 -0
- package/src/prompts/tools/git.md +9 -0
- package/src/prompts/voice-summary.md +12 -0
- package/src/utils/image-convert.ts +26 -0
- package/src/utils/image-resize.ts +215 -0
- package/src/utils/shell-snapshot.ts +22 -20
package/src/core/tools/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ export { createEditTool, type EditToolOptions, editTool } from "./edit";
|
|
|
5
5
|
export { exaTools } from "./exa/index";
|
|
6
6
|
export type { ExaRenderDetails, ExaSearchResponse, ExaSearchResult } from "./exa/types";
|
|
7
7
|
export { createFindTool, type FindToolDetails, findTool } from "./find";
|
|
8
|
+
export { createGitTool, type GitToolDetails, gitTool } from "./git";
|
|
8
9
|
export { createGrepTool, type GrepToolDetails, grepTool } from "./grep";
|
|
9
10
|
export { createLsTool, type LsToolDetails, lsTool } from "./ls";
|
|
10
11
|
export {
|
|
@@ -20,7 +21,7 @@ export {
|
|
|
20
21
|
} from "./lsp/index";
|
|
21
22
|
export { createNotebookTool, type NotebookToolDetails, notebookTool } from "./notebook";
|
|
22
23
|
export { createOutputTool, type OutputToolDetails, outputTool } from "./output";
|
|
23
|
-
export { createReadTool, type ReadToolDetails, readTool } from "./read";
|
|
24
|
+
export { createReadTool, type ReadToolDetails, type ReadToolOptions, readTool } from "./read";
|
|
24
25
|
export { createReportFindingTool, createSubmitReviewTool, reportFindingTool, submitReviewTool } from "./review";
|
|
25
26
|
export {
|
|
26
27
|
createRulebookTool,
|
|
@@ -57,6 +58,7 @@ import { bashTool, createBashTool } from "./bash";
|
|
|
57
58
|
import { checkBashInterception, checkSimpleLsInterception } from "./bash-interceptor";
|
|
58
59
|
import { createEditTool, editTool } from "./edit";
|
|
59
60
|
import { createFindTool, findTool } from "./find";
|
|
61
|
+
import { createGitTool, gitTool } from "./git";
|
|
60
62
|
import { createGrepTool, grepTool } from "./grep";
|
|
61
63
|
import { createLsTool, lsTool } from "./ls";
|
|
62
64
|
import { createLspTool, createLspWritethrough, lspTool } from "./lsp/index";
|
|
@@ -87,17 +89,22 @@ export interface CodingToolsOptions {
|
|
|
87
89
|
lspFormatOnWrite?: boolean;
|
|
88
90
|
/** Whether to accept high-confidence fuzzy matches in edit tool (default: true) */
|
|
89
91
|
editFuzzyMatch?: boolean;
|
|
92
|
+
/** Whether to auto-resize images to 2000x2000 max in read tool (default: true) */
|
|
93
|
+
readAutoResizeImages?: boolean;
|
|
90
94
|
/** Set of tool names available to the agent (for cross-tool awareness) */
|
|
91
95
|
availableTools?: Set<string>;
|
|
92
96
|
}
|
|
93
97
|
|
|
94
98
|
// Factory function type
|
|
95
|
-
type ToolFactory = (cwd: string, sessionContext?: SessionContext, options?: CodingToolsOptions) => Tool
|
|
99
|
+
type ToolFactory = (cwd: string, sessionContext?: SessionContext, options?: CodingToolsOptions) => Tool | Promise<Tool>;
|
|
96
100
|
|
|
97
101
|
// Tool definitions: static tools and their factory functions
|
|
98
102
|
const toolDefs: Record<string, { tool: Tool; create: ToolFactory }> = {
|
|
99
103
|
ask: { tool: askTool, create: createAskTool },
|
|
100
|
-
read: {
|
|
104
|
+
read: {
|
|
105
|
+
tool: readTool,
|
|
106
|
+
create: (cwd, _ctx, options) => createReadTool(cwd, { autoResizeImages: options?.readAutoResizeImages ?? true }),
|
|
107
|
+
},
|
|
101
108
|
bash: { tool: bashTool, create: createBashTool },
|
|
102
109
|
edit: {
|
|
103
110
|
tool: editTool,
|
|
@@ -125,6 +132,7 @@ const toolDefs: Record<string, { tool: Tool; create: ToolFactory }> = {
|
|
|
125
132
|
},
|
|
126
133
|
grep: { tool: grepTool, create: createGrepTool },
|
|
127
134
|
find: { tool: findTool, create: createFindTool },
|
|
135
|
+
git: { tool: gitTool, create: createGitTool },
|
|
128
136
|
ls: { tool: lsTool, create: createLsTool },
|
|
129
137
|
lsp: { tool: lspTool, create: createLspTool },
|
|
130
138
|
notebook: { tool: notebookTool, create: createNotebookTool },
|
|
@@ -142,13 +150,14 @@ export type ToolName = keyof typeof toolDefs;
|
|
|
142
150
|
const uiToolNames: ToolName[] = ["ask"];
|
|
143
151
|
|
|
144
152
|
// Tool sets defined by name (base sets, without UI-only tools)
|
|
145
|
-
const baseCodingToolNames: ToolName[] = [
|
|
153
|
+
export const baseCodingToolNames: ToolName[] = [
|
|
146
154
|
"read",
|
|
147
155
|
"bash",
|
|
148
156
|
"edit",
|
|
149
157
|
"write",
|
|
150
158
|
"grep",
|
|
151
159
|
"find",
|
|
160
|
+
"git",
|
|
152
161
|
"ls",
|
|
153
162
|
"lsp",
|
|
154
163
|
"notebook",
|
|
@@ -178,15 +187,15 @@ export const allTools = Object.fromEntries(Object.entries(toolDefs).map(([name,
|
|
|
178
187
|
* @param sessionContext - Optional session context for tools that need it
|
|
179
188
|
* @param options - Options for tool configuration
|
|
180
189
|
*/
|
|
181
|
-
export function createCodingTools(
|
|
190
|
+
export async function createCodingTools(
|
|
182
191
|
cwd: string,
|
|
183
192
|
hasUI = false,
|
|
184
193
|
sessionContext?: SessionContext,
|
|
185
194
|
options?: CodingToolsOptions,
|
|
186
|
-
): Tool[] {
|
|
195
|
+
): Promise<Tool[]> {
|
|
187
196
|
const names = hasUI ? [...baseCodingToolNames, ...uiToolNames] : baseCodingToolNames;
|
|
188
197
|
const optionsWithTools = { ...options, availableTools: new Set(names) };
|
|
189
|
-
return names.map((name) => toolDefs[name].create(cwd, sessionContext, optionsWithTools));
|
|
198
|
+
return Promise.all(names.map((name) => toolDefs[name].create(cwd, sessionContext, optionsWithTools)));
|
|
190
199
|
}
|
|
191
200
|
|
|
192
201
|
/**
|
|
@@ -196,15 +205,15 @@ export function createCodingTools(
|
|
|
196
205
|
* @param sessionContext - Optional session context for tools that need it
|
|
197
206
|
* @param options - Options for tool configuration
|
|
198
207
|
*/
|
|
199
|
-
export function createReadOnlyTools(
|
|
208
|
+
export async function createReadOnlyTools(
|
|
200
209
|
cwd: string,
|
|
201
210
|
hasUI = false,
|
|
202
211
|
sessionContext?: SessionContext,
|
|
203
212
|
options?: CodingToolsOptions,
|
|
204
|
-
): Tool[] {
|
|
213
|
+
): Promise<Tool[]> {
|
|
205
214
|
const names = hasUI ? [...baseReadOnlyToolNames, ...uiToolNames] : baseReadOnlyToolNames;
|
|
206
215
|
const optionsWithTools = { ...options, availableTools: new Set(names) };
|
|
207
|
-
return names.map((name) => toolDefs[name].create(cwd, sessionContext, optionsWithTools));
|
|
216
|
+
return Promise.all(names.map((name) => toolDefs[name].create(cwd, sessionContext, optionsWithTools)));
|
|
208
217
|
}
|
|
209
218
|
|
|
210
219
|
/**
|
|
@@ -213,16 +222,20 @@ export function createReadOnlyTools(
|
|
|
213
222
|
* @param sessionContext - Optional session context for tools that need it
|
|
214
223
|
* @param options - Options for tool configuration
|
|
215
224
|
*/
|
|
216
|
-
export function createAllTools(
|
|
225
|
+
export async function createAllTools(
|
|
217
226
|
cwd: string,
|
|
218
227
|
sessionContext?: SessionContext,
|
|
219
228
|
options?: CodingToolsOptions,
|
|
220
|
-
): Record<ToolName, Tool
|
|
229
|
+
): Promise<Record<ToolName, Tool>> {
|
|
221
230
|
const names = Object.keys(toolDefs);
|
|
222
231
|
const optionsWithTools = { ...options, availableTools: new Set(names) };
|
|
223
|
-
|
|
224
|
-
Object.entries(toolDefs).map(([name, def]) => [
|
|
225
|
-
|
|
232
|
+
const entries = await Promise.all(
|
|
233
|
+
Object.entries(toolDefs).map(async ([name, def]) => [
|
|
234
|
+
name,
|
|
235
|
+
await def.create(cwd, sessionContext, optionsWithTools),
|
|
236
|
+
]),
|
|
237
|
+
);
|
|
238
|
+
return Object.fromEntries(entries) as Record<ToolName, Tool>;
|
|
226
239
|
}
|
|
227
240
|
|
|
228
241
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { extname, join } from "node:path";
|
|
4
|
-
import { getConfigDirPaths } from "../../../config
|
|
4
|
+
import { getConfigDirPaths } from "../../../config";
|
|
5
5
|
import { createBiomeClient } from "./clients/biome-client";
|
|
6
6
|
import type { ServerConfig } from "./types";
|
|
7
7
|
|
|
@@ -618,7 +618,7 @@ export function hasRootMarkers(cwd: string, markers: string[]): boolean {
|
|
|
618
618
|
// Handle glob-like patterns (e.g., "*.cabal")
|
|
619
619
|
if (marker.includes("*")) {
|
|
620
620
|
try {
|
|
621
|
-
const { globSync } = require("
|
|
621
|
+
const { globSync } = require("glob");
|
|
622
622
|
const matches = globSync(join(cwd, marker));
|
|
623
623
|
return matches.length > 0;
|
|
624
624
|
} catch {
|
|
@@ -626,7 +626,8 @@ export function hasRootMarkers(cwd: string, markers: string[]): boolean {
|
|
|
626
626
|
return false;
|
|
627
627
|
}
|
|
628
628
|
}
|
|
629
|
-
|
|
629
|
+
const filePath = join(cwd, marker);
|
|
630
|
+
return existsSync(filePath);
|
|
630
631
|
});
|
|
631
632
|
}
|
|
632
633
|
|
|
@@ -741,7 +742,7 @@ function getConfigPaths(cwd: string): string[] {
|
|
|
741
742
|
* }
|
|
742
743
|
* ```
|
|
743
744
|
*/
|
|
744
|
-
export function loadConfig(cwd: string): LspConfig {
|
|
745
|
+
export async function loadConfig(cwd: string): Promise<LspConfig> {
|
|
745
746
|
const configPaths = getConfigPaths(cwd);
|
|
746
747
|
|
|
747
748
|
for (const configPath of configPaths) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { Dirent } from "node:fs";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
4
5
|
import type { BunFile } from "bun";
|
|
@@ -78,7 +79,7 @@ export interface LspWarmupResult {
|
|
|
78
79
|
* @returns Status of each server that was started
|
|
79
80
|
*/
|
|
80
81
|
export async function warmupLspServers(cwd: string): Promise<LspWarmupResult> {
|
|
81
|
-
const config = loadConfig(cwd);
|
|
82
|
+
const config = await loadConfig(cwd);
|
|
82
83
|
setIdleTimeout(config.idleTimeoutMs);
|
|
83
84
|
const servers: LspWarmupResult["servers"] = [];
|
|
84
85
|
const lspServers = getLspServers(config);
|
|
@@ -173,10 +174,10 @@ async function notifyFileSaved(
|
|
|
173
174
|
// Cache config per cwd to avoid repeated file I/O
|
|
174
175
|
const configCache = new Map<string, LspConfig>();
|
|
175
176
|
|
|
176
|
-
function getConfig(cwd: string): LspConfig {
|
|
177
|
+
async function getConfig(cwd: string): Promise<LspConfig> {
|
|
177
178
|
let config = configCache.get(cwd);
|
|
178
179
|
if (!config) {
|
|
179
|
-
config = loadConfig(cwd);
|
|
180
|
+
config = await loadConfig(cwd);
|
|
180
181
|
setIdleTimeout(config.idleTimeoutMs);
|
|
181
182
|
configCache.set(cwd, config);
|
|
182
183
|
}
|
|
@@ -225,9 +226,13 @@ function findFileByExtensions(baseDir: string, extensions: string[], maxDepth: n
|
|
|
225
226
|
const normalized = extensions.map((ext) => ext.toLowerCase());
|
|
226
227
|
const search = (dir: string, depth: number): string | null => {
|
|
227
228
|
if (depth > maxDepth) return null;
|
|
228
|
-
let entries:
|
|
229
|
+
let entries: Dirent[];
|
|
229
230
|
try {
|
|
230
|
-
entries =
|
|
231
|
+
entries = Array.from(new Bun.Glob("*").scanSync({ cwd: dir, onlyFiles: false })).map((name) => ({
|
|
232
|
+
name,
|
|
233
|
+
isFile: () => !existsSync(path.join(dir, name)) || Bun.file(path.join(dir, name)).type !== "directory",
|
|
234
|
+
isDirectory: () => existsSync(path.join(dir, name)) && Bun.file(path.join(dir, name)).type === "directory",
|
|
235
|
+
})) as Dirent[];
|
|
231
236
|
} catch {
|
|
232
237
|
return null;
|
|
233
238
|
}
|
|
@@ -312,22 +317,22 @@ interface ProjectType {
|
|
|
312
317
|
/** Detect project type from root markers */
|
|
313
318
|
function detectProjectType(cwd: string): ProjectType {
|
|
314
319
|
// Check for Rust (Cargo.toml)
|
|
315
|
-
if (
|
|
320
|
+
if (existsSync(path.join(cwd, "Cargo.toml"))) {
|
|
316
321
|
return { type: "rust", command: ["cargo", "check", "--message-format=short"], description: "Rust (cargo check)" };
|
|
317
322
|
}
|
|
318
323
|
|
|
319
324
|
// Check for TypeScript (tsconfig.json)
|
|
320
|
-
if (
|
|
325
|
+
if (existsSync(path.join(cwd, "tsconfig.json"))) {
|
|
321
326
|
return { type: "typescript", command: ["npx", "tsc", "--noEmit"], description: "TypeScript (tsc --noEmit)" };
|
|
322
327
|
}
|
|
323
328
|
|
|
324
329
|
// Check for Go (go.mod)
|
|
325
|
-
if (
|
|
330
|
+
if (existsSync(path.join(cwd, "go.mod"))) {
|
|
326
331
|
return { type: "go", command: ["go", "build", "./..."], description: "Go (go build)" };
|
|
327
332
|
}
|
|
328
333
|
|
|
329
334
|
// Check for Python (pyproject.toml or pyrightconfig.json)
|
|
330
|
-
if (
|
|
335
|
+
if (existsSync(path.join(cwd, "pyproject.toml")) || existsSync(path.join(cwd, "pyrightconfig.json"))) {
|
|
331
336
|
return { type: "python", command: ["pyright"], description: "Python (pyright)" };
|
|
332
337
|
}
|
|
333
338
|
|
|
@@ -612,7 +617,7 @@ export function createLspWritethrough(cwd: string, options?: WritethroughOptions
|
|
|
612
617
|
return writethroughNoop;
|
|
613
618
|
}
|
|
614
619
|
return async (dst: string, content: string, signal?: AbortSignal, file?: BunFile) => {
|
|
615
|
-
const config = getConfig(cwd);
|
|
620
|
+
const config = await getConfig(cwd);
|
|
616
621
|
const servers = getServersForFile(config, dst);
|
|
617
622
|
if (servers.length === 0) {
|
|
618
623
|
return writethroughNoop(dst, content, signal, file);
|
|
@@ -708,7 +713,7 @@ export function createLspTool(cwd: string): AgentTool<typeof lspSchema, LspToolD
|
|
|
708
713
|
include_declaration,
|
|
709
714
|
} = params;
|
|
710
715
|
|
|
711
|
-
const config = getConfig(cwd);
|
|
716
|
+
const config = await getConfig(cwd);
|
|
712
717
|
|
|
713
718
|
// Status action doesn't need a file
|
|
714
719
|
if (action === "status") {
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
import type { AgentToolResult, RenderResultOptions } from "@oh-my-pi/pi-agent-core";
|
|
12
12
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
13
13
|
import { highlight, supportsLanguage } from "cli-highlight";
|
|
14
|
-
import type
|
|
14
|
+
import { getLanguageFromPath, type Theme } from "../../../modes/interactive/theme/theme";
|
|
15
|
+
import { formatExpandHint, formatMoreItems, TRUNCATE_LENGTHS, truncate } from "../render-utils";
|
|
15
16
|
import type { LspParams, LspToolDetails } from "./types";
|
|
16
17
|
|
|
17
18
|
// =============================================================================
|
|
@@ -25,8 +26,8 @@ import type { LspParams, LspToolDetails } from "./types";
|
|
|
25
26
|
export function renderCall(args: unknown, theme: Theme): Text {
|
|
26
27
|
const p = args as LspParams & { file?: string; files?: string[] };
|
|
27
28
|
|
|
28
|
-
let text = theme.fg("toolTitle", theme.bold("LSP
|
|
29
|
-
text += theme.fg("accent", p.action || "?")
|
|
29
|
+
let text = theme.fg("toolTitle", theme.bold("LSP"));
|
|
30
|
+
text += ` ${theme.fg("accent", p.action || "?")}`;
|
|
30
31
|
|
|
31
32
|
if (p.file) {
|
|
32
33
|
text += ` ${theme.fg("muted", p.file)}`;
|
|
@@ -112,7 +113,7 @@ function renderHover(
|
|
|
112
113
|
const v = theme.boxSharp.vertical;
|
|
113
114
|
const top = `${theme.boxSharp.topLeft}${h.repeat(3)}`;
|
|
114
115
|
const bottom = `${theme.boxSharp.bottomLeft}${h.repeat(3)}`;
|
|
115
|
-
let output = `${icon}
|
|
116
|
+
let output = `${icon}${langLabel}`;
|
|
116
117
|
output += `\n ${theme.fg("mdCodeBlockBorder", top)}`;
|
|
117
118
|
for (const line of codeLines) {
|
|
118
119
|
output += `\n ${theme.fg("mdCodeBlockBorder", v)} ${line}`;
|
|
@@ -126,9 +127,10 @@ function renderHover(
|
|
|
126
127
|
|
|
127
128
|
// Collapsed view
|
|
128
129
|
const firstCodeLine = codeLines[0] || "";
|
|
129
|
-
const
|
|
130
|
+
const hasMore = codeLines.length > 1 || Boolean(afterCode);
|
|
131
|
+
const expandHint = formatExpandHint(false, hasMore, theme);
|
|
130
132
|
|
|
131
|
-
let output = `${icon}
|
|
133
|
+
let output = `${icon}${langLabel}${expandHint}`;
|
|
132
134
|
const h = theme.boxSharp.horizontal;
|
|
133
135
|
const v = theme.boxSharp.vertical;
|
|
134
136
|
const bottom = `${theme.boxSharp.bottomLeft}${h.repeat(3)}`;
|
|
@@ -142,9 +144,7 @@ function renderHover(
|
|
|
142
144
|
}
|
|
143
145
|
|
|
144
146
|
if (afterCode) {
|
|
145
|
-
const
|
|
146
|
-
const sliceLen = Math.max(0, 60 - ellipsis.length);
|
|
147
|
-
const docPreview = afterCode.length > 60 ? `${afterCode.slice(0, sliceLen)}${ellipsis}` : afterCode;
|
|
147
|
+
const docPreview = truncate(afterCode, TRUNCATE_LENGTHS.TITLE, theme.format.ellipsis);
|
|
148
148
|
output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg("muted", docPreview)}`;
|
|
149
149
|
} else {
|
|
150
150
|
output += `\n ${theme.fg("mdCodeBlockBorder", bottom)}`;
|
|
@@ -186,6 +186,12 @@ function highlightCode(codeText: string, language: string, theme: Theme): string
|
|
|
186
186
|
// Diagnostics Rendering
|
|
187
187
|
// =============================================================================
|
|
188
188
|
|
|
189
|
+
function formatDiagnosticLocation(file: string, line: string | number, col: string | number, theme: Theme): string {
|
|
190
|
+
const lang = getLanguageFromPath(file);
|
|
191
|
+
const icon = theme.fg("muted", theme.getLangIcon(lang));
|
|
192
|
+
return `${icon} ${file}:${line}:${col}`;
|
|
193
|
+
}
|
|
194
|
+
|
|
189
195
|
/**
|
|
190
196
|
* Render diagnostics with color-coded severity.
|
|
191
197
|
*/
|
|
@@ -218,7 +224,7 @@ function renderDiagnostics(
|
|
|
218
224
|
const fallbackDiagnostics: RawDiagnostic[] = diagLines.map((line) => ({ raw: line.trim() }));
|
|
219
225
|
|
|
220
226
|
if (expanded) {
|
|
221
|
-
let output = `${icon} ${theme.fg("
|
|
227
|
+
let output = `${icon} ${theme.fg("dim", meta.join(theme.sep.dot))}`;
|
|
222
228
|
const items: DiagnosticItem[] = parsedDiagnostics.length > 0 ? parsedDiagnostics : fallbackDiagnostics;
|
|
223
229
|
for (let i = 0; i < items.length; i++) {
|
|
224
230
|
const item = items[i];
|
|
@@ -230,26 +236,25 @@ function renderDiagnostics(
|
|
|
230
236
|
continue;
|
|
231
237
|
}
|
|
232
238
|
const severityColor = severityToColor(item.severity);
|
|
233
|
-
const location =
|
|
239
|
+
const location = formatDiagnosticLocation(item.file, item.line, item.col, theme);
|
|
234
240
|
output += `\n ${theme.fg("dim", branch)} ${theme.fg(severityColor, location)} ${theme.fg(
|
|
235
241
|
"dim",
|
|
236
242
|
`[${item.severity}]`,
|
|
237
243
|
)}`;
|
|
238
244
|
if (item.message) {
|
|
239
|
-
output += `\n ${theme.fg("dim", detailPrefix)}${theme.fg("muted",
|
|
245
|
+
output += `\n ${theme.fg("dim", detailPrefix)}${theme.fg("muted", truncate(item.message, TRUNCATE_LENGTHS.LINE, theme.format.ellipsis))}`;
|
|
240
246
|
}
|
|
241
247
|
}
|
|
242
248
|
return new Text(output, 0, 0);
|
|
243
249
|
}
|
|
244
250
|
|
|
245
251
|
// Collapsed view
|
|
246
|
-
const expandHint = theme.fg("dim", " (Ctrl+O to expand)");
|
|
247
|
-
let output = `${icon} ${theme.fg("toolTitle", "Diagnostics")} ${theme.fg("dim", meta.join(", "))}${expandHint}`;
|
|
248
|
-
|
|
249
252
|
const previewItems: DiagnosticItem[] =
|
|
250
253
|
parsedDiagnostics.length > 0 ? parsedDiagnostics.slice(0, 3) : fallbackDiagnostics.slice(0, 3);
|
|
251
254
|
const remaining =
|
|
252
255
|
(parsedDiagnostics.length > 0 ? parsedDiagnostics.length : fallbackDiagnostics.length) - previewItems.length;
|
|
256
|
+
const expandHint = formatExpandHint(false, remaining > 0, theme);
|
|
257
|
+
let output = `${icon} ${theme.fg("dim", meta.join(theme.sep.dot))}${expandHint}`;
|
|
253
258
|
for (let i = 0; i < previewItems.length; i++) {
|
|
254
259
|
const item = previewItems[i];
|
|
255
260
|
const isLast = i === previewItems.length - 1 && remaining <= 0;
|
|
@@ -259,15 +264,14 @@ function renderDiagnostics(
|
|
|
259
264
|
continue;
|
|
260
265
|
}
|
|
261
266
|
const severityColor = severityToColor(item.severity);
|
|
262
|
-
const location =
|
|
263
|
-
const message = item.message
|
|
267
|
+
const location = formatDiagnosticLocation(item.file, item.line, item.col, theme);
|
|
268
|
+
const message = item.message
|
|
269
|
+
? ` ${theme.fg("muted", truncate(item.message, TRUNCATE_LENGTHS.CONTENT, theme.format.ellipsis))}`
|
|
270
|
+
: "";
|
|
264
271
|
output += `\n ${theme.fg("dim", branch)} ${theme.fg(severityColor, location)}${message}`;
|
|
265
272
|
}
|
|
266
273
|
if (remaining > 0) {
|
|
267
|
-
output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg(
|
|
268
|
-
"muted",
|
|
269
|
-
`${theme.format.ellipsis} ${remaining} more`,
|
|
270
|
-
)}`;
|
|
274
|
+
output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg("muted", `${theme.format.ellipsis} ${remaining} more`)}`;
|
|
271
275
|
}
|
|
272
276
|
|
|
273
277
|
return new Text(output, 0, 0);
|
|
@@ -301,8 +305,8 @@ function renderReferences(refMatch: RegExpMatchArray, lines: string[], expanded:
|
|
|
301
305
|
const files = Array.from(byFile.keys());
|
|
302
306
|
|
|
303
307
|
const renderGrouped = (maxFiles: number, maxLocsPerFile: number, showHint: boolean): string => {
|
|
304
|
-
const expandHint =
|
|
305
|
-
let output = `${icon} ${theme.fg("
|
|
308
|
+
const expandHint = formatExpandHint(false, showHint, theme);
|
|
309
|
+
let output = `${icon} ${theme.fg("dim", `${refCount} found`)}${expandHint}`;
|
|
306
310
|
|
|
307
311
|
const filesToShow = files.slice(0, maxFiles);
|
|
308
312
|
for (let fi = 0; fi < filesToShow.length; fi++) {
|
|
@@ -330,7 +334,7 @@ function renderReferences(refMatch: RegExpMatchArray, lines: string[], expanded:
|
|
|
330
334
|
const context = `at ${file}:${line}:${col}`;
|
|
331
335
|
output += `\n ${theme.fg("dim", fileCont)}${theme.fg("dim", locCont)}${theme.fg(
|
|
332
336
|
"muted",
|
|
333
|
-
|
|
337
|
+
truncate(context, TRUNCATE_LENGTHS.LINE, theme.format.ellipsis),
|
|
334
338
|
)}`;
|
|
335
339
|
}
|
|
336
340
|
}
|
|
@@ -346,7 +350,7 @@ function renderReferences(refMatch: RegExpMatchArray, lines: string[], expanded:
|
|
|
346
350
|
if (files.length > maxFiles) {
|
|
347
351
|
output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg(
|
|
348
352
|
"muted",
|
|
349
|
-
|
|
353
|
+
formatMoreItems(files.length - maxFiles, "file", theme),
|
|
350
354
|
)}`;
|
|
351
355
|
}
|
|
352
356
|
|
|
@@ -424,7 +428,7 @@ function renderSymbols(symbolsMatch: RegExpMatchArray, lines: string[], expanded
|
|
|
424
428
|
const topLevelCount = symbols.filter((s) => s.indent === 0).length;
|
|
425
429
|
|
|
426
430
|
if (expanded) {
|
|
427
|
-
let output = `${icon} ${theme.fg("
|
|
431
|
+
let output = `${icon} ${theme.fg("dim", `in ${fileName}`)}`;
|
|
428
432
|
|
|
429
433
|
for (let i = 0; i < symbols.length; i++) {
|
|
430
434
|
const sym = symbols[i];
|
|
@@ -442,10 +446,10 @@ function renderSymbols(symbolsMatch: RegExpMatchArray, lines: string[], expanded
|
|
|
442
446
|
}
|
|
443
447
|
|
|
444
448
|
// Collapsed: show first 3 top-level symbols
|
|
445
|
-
const expandHint = theme.fg("dim", " (Ctrl+O to expand)");
|
|
446
|
-
let output = `${icon} ${theme.fg("toolTitle", "Symbols")} ${theme.fg("dim", `in ${fileName}`)}${expandHint}`;
|
|
447
|
-
|
|
448
449
|
const topLevel = symbols.filter((s) => s.indent === 0).slice(0, 3);
|
|
450
|
+
const hasMoreSymbols = symbols.length > topLevel.length;
|
|
451
|
+
const expandHint = formatExpandHint(false, hasMoreSymbols, theme);
|
|
452
|
+
let output = `${icon} ${theme.fg("dim", `in ${fileName}`)}${expandHint}`;
|
|
449
453
|
for (let i = 0; i < topLevel.length; i++) {
|
|
450
454
|
const sym = topLevel[i];
|
|
451
455
|
const isLast = i === topLevel.length - 1 && topLevelCount <= 3;
|
|
@@ -456,10 +460,7 @@ function renderSymbols(symbolsMatch: RegExpMatchArray, lines: string[], expanded
|
|
|
456
460
|
)} ${theme.fg("muted", `line ${sym.line}`)}`;
|
|
457
461
|
}
|
|
458
462
|
if (topLevelCount > 3) {
|
|
459
|
-
output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg(
|
|
460
|
-
"muted",
|
|
461
|
-
`${theme.format.ellipsis} ${topLevelCount - 3} more`,
|
|
462
|
-
)}`;
|
|
463
|
+
output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg("muted", `${theme.format.ellipsis} ${topLevelCount - 3} more`)}`;
|
|
463
464
|
}
|
|
464
465
|
|
|
465
466
|
return new Text(output, 0, 0);
|
|
@@ -484,7 +485,7 @@ function renderGeneric(text: string, lines: string[], expanded: boolean, theme:
|
|
|
484
485
|
: theme.styledSymbol("status.info", "accent");
|
|
485
486
|
|
|
486
487
|
if (expanded) {
|
|
487
|
-
let output = `${icon} ${theme.fg("
|
|
488
|
+
let output = `${icon} ${theme.fg("dim", "Output")}`;
|
|
488
489
|
for (let i = 0; i < lines.length; i++) {
|
|
489
490
|
const isLast = i === lines.length - 1;
|
|
490
491
|
const branch = isLast ? theme.tree.last : theme.tree.branch;
|
|
@@ -494,21 +495,18 @@ function renderGeneric(text: string, lines: string[], expanded: boolean, theme:
|
|
|
494
495
|
}
|
|
495
496
|
|
|
496
497
|
const firstLine = lines[0] || "No output";
|
|
497
|
-
const expandHint = lines.length > 1
|
|
498
|
-
let output = `${icon} ${theme.fg("
|
|
498
|
+
const expandHint = formatExpandHint(false, lines.length > 1, theme);
|
|
499
|
+
let output = `${icon} ${theme.fg("dim", truncate(firstLine, TRUNCATE_LENGTHS.TITLE, theme.format.ellipsis))}${expandHint}`;
|
|
499
500
|
|
|
500
501
|
if (lines.length > 1) {
|
|
501
502
|
const previewLines = lines.slice(1, 4);
|
|
502
503
|
for (let i = 0; i < previewLines.length; i++) {
|
|
503
504
|
const isLast = i === previewLines.length - 1 && lines.length <= 4;
|
|
504
505
|
const branch = isLast ? theme.tree.last : theme.tree.branch;
|
|
505
|
-
output += `\n ${theme.fg("dim", branch)} ${theme.fg("dim", previewLines[i].trim().
|
|
506
|
+
output += `\n ${theme.fg("dim", branch)} ${theme.fg("dim", truncate(previewLines[i].trim(), TRUNCATE_LENGTHS.CONTENT, theme.format.ellipsis))}`;
|
|
506
507
|
}
|
|
507
508
|
if (lines.length > 4) {
|
|
508
|
-
output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg(
|
|
509
|
-
"muted",
|
|
510
|
-
`${theme.format.ellipsis} ${lines.length - 4} more lines`,
|
|
511
|
-
)}`;
|
|
509
|
+
output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg("muted", formatMoreItems(lines.length - 4, "line", theme))}`;
|
|
512
510
|
}
|
|
513
511
|
}
|
|
514
512
|
|
|
@@ -552,9 +550,3 @@ function severityToColor(severity: string): "error" | "warning" | "accent" | "di
|
|
|
552
550
|
return "dim";
|
|
553
551
|
}
|
|
554
552
|
}
|
|
555
|
-
|
|
556
|
-
function trimTo(value: string, maxLength: number, theme: Theme): string {
|
|
557
|
-
if (value.length <= maxLength) return value;
|
|
558
|
-
const sliceLen = Math.max(0, maxLength - theme.format.ellipsis.length);
|
|
559
|
-
return `${value.slice(0, sliceLen)}${theme.format.ellipsis}`;
|
|
560
|
-
}
|
package/src/core/tools/read.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { constants, existsSync } from "node:fs";
|
|
3
|
-
import { access, readFile, stat } from "node:fs/promises";
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
4
2
|
import path from "node:path";
|
|
5
3
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
6
4
|
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
7
5
|
import { Type } from "@sinclair/typebox";
|
|
8
6
|
import { globSync } from "glob";
|
|
9
7
|
import readDescription from "../../prompts/tools/read.md" with { type: "text" };
|
|
8
|
+
import { formatDimensionNote, resizeImage } from "../../utils/image-resize";
|
|
10
9
|
import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime";
|
|
11
10
|
import { ensureTool } from "../../utils/tools-manager";
|
|
12
11
|
import { untilAborted } from "../utils";
|
|
@@ -49,9 +48,14 @@ async function findExistingDirectory(startDir: string): Promise<string | null> {
|
|
|
49
48
|
|
|
50
49
|
while (true) {
|
|
51
50
|
try {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
if (existsSync(current)) {
|
|
52
|
+
// Check if directory by trying to read it as dir
|
|
53
|
+
try {
|
|
54
|
+
await Bun.$`test -d ${current}`.quiet();
|
|
55
|
+
return current;
|
|
56
|
+
} catch {
|
|
57
|
+
// Not a directory, continue
|
|
58
|
+
}
|
|
55
59
|
}
|
|
56
60
|
} catch {
|
|
57
61
|
// Keep walking up.
|
|
@@ -300,17 +304,17 @@ function convertWithMarkitdown(filePath: string): { content: string; ok: boolean
|
|
|
300
304
|
return { content: "", ok: false, error: "markitdown not found" };
|
|
301
305
|
}
|
|
302
306
|
|
|
303
|
-
const result = spawnSync(cmd,
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
+
const result = Bun.spawnSync([cmd, filePath], {
|
|
308
|
+
stdin: "ignore",
|
|
309
|
+
stdout: "pipe",
|
|
310
|
+
stderr: "pipe",
|
|
307
311
|
});
|
|
308
312
|
|
|
309
|
-
if (result.
|
|
310
|
-
return { content: result.stdout, ok: true };
|
|
313
|
+
if (result.exitCode === 0 && result.stdout && result.stdout.length > 0) {
|
|
314
|
+
return { content: result.stdout.toString(), ok: true };
|
|
311
315
|
}
|
|
312
316
|
|
|
313
|
-
return { content: "", ok: false, error: result.stderr || "Conversion failed" };
|
|
317
|
+
return { content: "", ok: false, error: result.stderr.toString() || "Conversion failed" };
|
|
314
318
|
}
|
|
315
319
|
|
|
316
320
|
const readSchema = Type.Object({
|
|
@@ -324,7 +328,13 @@ export interface ReadToolDetails {
|
|
|
324
328
|
redirectedTo?: "ls";
|
|
325
329
|
}
|
|
326
330
|
|
|
327
|
-
export
|
|
331
|
+
export interface ReadToolOptions {
|
|
332
|
+
/** Whether to auto-resize images to 2000x2000 max. Default: true */
|
|
333
|
+
autoResizeImages?: boolean;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function createReadTool(cwd: string, options?: ReadToolOptions): AgentTool<typeof readSchema> {
|
|
337
|
+
const autoResizeImages = options?.autoResizeImages ?? true;
|
|
328
338
|
const lsTool = createLsTool(cwd);
|
|
329
339
|
return {
|
|
330
340
|
name: "read",
|
|
@@ -339,9 +349,21 @@ export function createReadTool(cwd: string): AgentTool<typeof readSchema> {
|
|
|
339
349
|
const absolutePath = resolveReadPath(readPath, cwd);
|
|
340
350
|
|
|
341
351
|
return untilAborted(signal, async () => {
|
|
342
|
-
let
|
|
352
|
+
let isDirectory = false;
|
|
353
|
+
let fileSize = 0;
|
|
343
354
|
try {
|
|
344
|
-
|
|
355
|
+
if (!existsSync(absolutePath)) {
|
|
356
|
+
throw { code: "ENOENT" };
|
|
357
|
+
}
|
|
358
|
+
const file = Bun.file(absolutePath);
|
|
359
|
+
fileSize = file.size;
|
|
360
|
+
// Check if directory
|
|
361
|
+
try {
|
|
362
|
+
await Bun.$`test -d ${absolutePath}`.quiet();
|
|
363
|
+
isDirectory = true;
|
|
364
|
+
} catch {
|
|
365
|
+
isDirectory = false;
|
|
366
|
+
}
|
|
345
367
|
} catch (error) {
|
|
346
368
|
if (isNotFoundError(error)) {
|
|
347
369
|
const suggestions = await findReadPathSuggestions(readPath, cwd);
|
|
@@ -366,7 +388,7 @@ export function createReadTool(cwd: string): AgentTool<typeof readSchema> {
|
|
|
366
388
|
throw error;
|
|
367
389
|
}
|
|
368
390
|
|
|
369
|
-
if (
|
|
391
|
+
if (isDirectory) {
|
|
370
392
|
const lsResult = await lsTool.execute(toolCallId, { path: readPath, limit }, signal);
|
|
371
393
|
return {
|
|
372
394
|
content: lsResult.content,
|
|
@@ -374,8 +396,6 @@ export function createReadTool(cwd: string): AgentTool<typeof readSchema> {
|
|
|
374
396
|
};
|
|
375
397
|
}
|
|
376
398
|
|
|
377
|
-
await access(absolutePath, constants.R_OK);
|
|
378
|
-
|
|
379
399
|
const mimeType = await detectSupportedImageMimeTypeFromFile(absolutePath);
|
|
380
400
|
const ext = path.extname(absolutePath).toLowerCase();
|
|
381
401
|
|
|
@@ -385,9 +405,8 @@ export function createReadTool(cwd: string): AgentTool<typeof readSchema> {
|
|
|
385
405
|
|
|
386
406
|
if (mimeType) {
|
|
387
407
|
// Check image file size before reading to prevent OOM during serialization
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const sizeStr = formatSize(fileStat.size);
|
|
408
|
+
if (fileSize > MAX_IMAGE_SIZE) {
|
|
409
|
+
const sizeStr = formatSize(fileSize);
|
|
391
410
|
const maxStr = formatSize(MAX_IMAGE_SIZE);
|
|
392
411
|
content = [
|
|
393
412
|
{
|
|
@@ -397,13 +416,30 @@ export function createReadTool(cwd: string): AgentTool<typeof readSchema> {
|
|
|
397
416
|
];
|
|
398
417
|
} else {
|
|
399
418
|
// Read as image (binary)
|
|
400
|
-
const
|
|
401
|
-
const
|
|
419
|
+
const file = Bun.file(absolutePath);
|
|
420
|
+
const buffer = await file.arrayBuffer();
|
|
421
|
+
const base64 = Buffer.from(buffer).toString("base64");
|
|
422
|
+
|
|
423
|
+
if (autoResizeImages) {
|
|
424
|
+
// Resize image if needed
|
|
425
|
+
const resized = await resizeImage({ type: "image", data: base64, mimeType });
|
|
426
|
+
const dimensionNote = formatDimensionNote(resized);
|
|
427
|
+
|
|
428
|
+
let textNote = `Read image file [${resized.mimeType}]`;
|
|
429
|
+
if (dimensionNote) {
|
|
430
|
+
textNote += `\n${dimensionNote}`;
|
|
431
|
+
}
|
|
402
432
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
433
|
+
content = [
|
|
434
|
+
{ type: "text", text: textNote },
|
|
435
|
+
{ type: "image", data: resized.data, mimeType: resized.mimeType },
|
|
436
|
+
];
|
|
437
|
+
} else {
|
|
438
|
+
content = [
|
|
439
|
+
{ type: "text", text: `Read image file [${mimeType}]` },
|
|
440
|
+
{ type: "image", data: base64, mimeType },
|
|
441
|
+
];
|
|
442
|
+
}
|
|
407
443
|
}
|
|
408
444
|
} else if (CONVERTIBLE_EXTENSIONS.has(ext)) {
|
|
409
445
|
// Convert document via markitdown
|
|
@@ -431,7 +467,8 @@ export function createReadTool(cwd: string): AgentTool<typeof readSchema> {
|
|
|
431
467
|
}
|
|
432
468
|
} else {
|
|
433
469
|
// Read as text
|
|
434
|
-
const
|
|
470
|
+
const file = Bun.file(absolutePath);
|
|
471
|
+
const textContent = await file.text();
|
|
435
472
|
const allLines = textContent.split("\n");
|
|
436
473
|
const totalFileLines = allLines.length;
|
|
437
474
|
|