@oh-my-pi/pi-coding-agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1629 -0
- package/README.md +1041 -0
- package/docs/compaction.md +403 -0
- package/docs/config-usage.md +113 -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 +670 -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/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 +89 -0
- package/src/bun-imports.d.ts +16 -0
- package/src/capability/context-file.ts +40 -0
- package/src/capability/extension.ts +48 -0
- package/src/capability/hook.ts +40 -0
- package/src/capability/index.ts +616 -0
- package/src/capability/instruction.ts +37 -0
- package/src/capability/mcp.ts +52 -0
- package/src/capability/prompt.ts +35 -0
- package/src/capability/rule.ts +56 -0
- package/src/capability/settings.ts +35 -0
- package/src/capability/skill.ts +49 -0
- package/src/capability/slash-command.ts +40 -0
- package/src/capability/system-prompt.ts +35 -0
- package/src/capability/tool.ts +38 -0
- package/src/capability/types.ts +166 -0
- package/src/cli/args.ts +259 -0
- package/src/cli/file-processor.ts +121 -0
- package/src/cli/list-models.ts +104 -0
- package/src/cli/plugin-cli.ts +661 -0
- package/src/cli/session-picker.ts +41 -0
- package/src/cli/update-cli.ts +274 -0
- package/src/cli.ts +10 -0
- package/src/config.ts +391 -0
- package/src/core/agent-session.ts +2178 -0
- package/src/core/auth-storage.ts +258 -0
- package/src/core/bash-executor.ts +197 -0
- package/src/core/compaction/branch-summarization.ts +315 -0
- package/src/core/compaction/compaction.ts +664 -0
- package/src/core/compaction/index.ts +7 -0
- package/src/core/compaction/utils.ts +153 -0
- package/src/core/custom-commands/bundled/review/index.ts +156 -0
- package/src/core/custom-commands/index.ts +15 -0
- package/src/core/custom-commands/loader.ts +226 -0
- package/src/core/custom-commands/types.ts +112 -0
- package/src/core/custom-tools/index.ts +22 -0
- package/src/core/custom-tools/loader.ts +248 -0
- package/src/core/custom-tools/types.ts +185 -0
- package/src/core/custom-tools/wrapper.ts +29 -0
- package/src/core/exec.ts +139 -0
- package/src/core/export-html/index.ts +159 -0
- package/src/core/export-html/template.css +774 -0
- package/src/core/export-html/template.generated.ts +2 -0
- package/src/core/export-html/template.html +45 -0
- package/src/core/export-html/template.js +1185 -0
- package/src/core/export-html/template.macro.ts +24 -0
- package/src/core/file-mentions.ts +54 -0
- package/src/core/hooks/index.ts +16 -0
- package/src/core/hooks/loader.ts +288 -0
- package/src/core/hooks/runner.ts +434 -0
- package/src/core/hooks/tool-wrapper.ts +98 -0
- package/src/core/hooks/types.ts +770 -0
- package/src/core/index.ts +53 -0
- package/src/core/logger.ts +112 -0
- package/src/core/mcp/client.ts +185 -0
- package/src/core/mcp/config.ts +248 -0
- package/src/core/mcp/index.ts +45 -0
- package/src/core/mcp/loader.ts +99 -0
- package/src/core/mcp/manager.ts +235 -0
- package/src/core/mcp/tool-bridge.ts +156 -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 +228 -0
- package/src/core/messages.ts +211 -0
- package/src/core/model-registry.ts +334 -0
- package/src/core/model-resolver.ts +494 -0
- package/src/core/plugins/doctor.ts +67 -0
- package/src/core/plugins/index.ts +38 -0
- package/src/core/plugins/installer.ts +189 -0
- package/src/core/plugins/loader.ts +339 -0
- package/src/core/plugins/manager.ts +672 -0
- package/src/core/plugins/parser.ts +105 -0
- package/src/core/plugins/paths.ts +37 -0
- package/src/core/plugins/types.ts +190 -0
- package/src/core/sdk.ts +900 -0
- package/src/core/session-manager.ts +1837 -0
- package/src/core/settings-manager.ts +860 -0
- package/src/core/skills.ts +352 -0
- package/src/core/slash-commands.ts +132 -0
- package/src/core/system-prompt.ts +442 -0
- package/src/core/timings.ts +25 -0
- package/src/core/title-generator.ts +110 -0
- package/src/core/tools/ask.ts +193 -0
- package/src/core/tools/bash-interceptor.ts +120 -0
- package/src/core/tools/bash.ts +91 -0
- package/src/core/tools/context.ts +32 -0
- package/src/core/tools/edit-diff.ts +487 -0
- package/src/core/tools/edit.ts +140 -0
- package/src/core/tools/exa/company.ts +59 -0
- package/src/core/tools/exa/index.ts +63 -0
- package/src/core/tools/exa/linkedin.ts +59 -0
- package/src/core/tools/exa/mcp-client.ts +368 -0
- package/src/core/tools/exa/render.ts +200 -0
- package/src/core/tools/exa/researcher.ts +90 -0
- package/src/core/tools/exa/search.ts +338 -0
- package/src/core/tools/exa/types.ts +167 -0
- package/src/core/tools/exa/websets.ts +248 -0
- package/src/core/tools/find.ts +244 -0
- package/src/core/tools/grep.ts +584 -0
- package/src/core/tools/index.ts +283 -0
- package/src/core/tools/ls.ts +142 -0
- package/src/core/tools/lsp/client.ts +767 -0
- package/src/core/tools/lsp/clients/biome-client.ts +207 -0
- package/src/core/tools/lsp/clients/index.ts +49 -0
- package/src/core/tools/lsp/clients/lsp-linter-client.ts +98 -0
- package/src/core/tools/lsp/config.ts +845 -0
- package/src/core/tools/lsp/edits.ts +110 -0
- package/src/core/tools/lsp/index.ts +1364 -0
- package/src/core/tools/lsp/render.ts +560 -0
- package/src/core/tools/lsp/rust-analyzer.ts +145 -0
- package/src/core/tools/lsp/types.ts +495 -0
- package/src/core/tools/lsp/utils.ts +526 -0
- package/src/core/tools/notebook.ts +182 -0
- package/src/core/tools/output.ts +198 -0
- package/src/core/tools/path-utils.ts +61 -0
- package/src/core/tools/read.ts +507 -0
- package/src/core/tools/renderers.ts +820 -0
- package/src/core/tools/review.ts +275 -0
- package/src/core/tools/rulebook.ts +124 -0
- package/src/core/tools/task/agents.ts +158 -0
- package/src/core/tools/task/artifacts.ts +114 -0
- package/src/core/tools/task/commands.ts +157 -0
- package/src/core/tools/task/discovery.ts +217 -0
- package/src/core/tools/task/executor.ts +531 -0
- package/src/core/tools/task/index.ts +548 -0
- package/src/core/tools/task/model-resolver.ts +176 -0
- package/src/core/tools/task/parallel.ts +38 -0
- package/src/core/tools/task/render.ts +502 -0
- package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
- package/src/core/tools/task/types.ts +142 -0
- package/src/core/tools/truncate.ts +265 -0
- package/src/core/tools/web-fetch.ts +2511 -0
- package/src/core/tools/web-search/auth.ts +199 -0
- package/src/core/tools/web-search/index.ts +583 -0
- package/src/core/tools/web-search/providers/anthropic.ts +198 -0
- package/src/core/tools/web-search/providers/exa.ts +196 -0
- package/src/core/tools/web-search/providers/perplexity.ts +195 -0
- package/src/core/tools/web-search/render.ts +372 -0
- package/src/core/tools/web-search/types.ts +180 -0
- package/src/core/tools/write.ts +63 -0
- package/src/core/ttsr.ts +211 -0
- package/src/core/utils.ts +187 -0
- package/src/discovery/agents-md.ts +75 -0
- package/src/discovery/builtin.ts +647 -0
- package/src/discovery/claude.ts +623 -0
- package/src/discovery/cline.ts +104 -0
- package/src/discovery/codex.ts +571 -0
- package/src/discovery/cursor.ts +266 -0
- package/src/discovery/gemini.ts +368 -0
- package/src/discovery/github.ts +120 -0
- package/src/discovery/helpers.test.ts +127 -0
- package/src/discovery/helpers.ts +249 -0
- package/src/discovery/index.ts +84 -0
- package/src/discovery/mcp-json.ts +127 -0
- package/src/discovery/vscode.ts +99 -0
- package/src/discovery/windsurf.ts +219 -0
- package/src/index.ts +192 -0
- package/src/main.ts +507 -0
- package/src/migrations.ts +156 -0
- package/src/modes/cleanup.ts +23 -0
- package/src/modes/index.ts +48 -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 +199 -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/extensions/extension-dashboard.ts +296 -0
- package/src/modes/interactive/components/extensions/extension-list.ts +479 -0
- package/src/modes/interactive/components/extensions/index.ts +9 -0
- package/src/modes/interactive/components/extensions/inspector-panel.ts +313 -0
- package/src/modes/interactive/components/extensions/state-manager.ts +558 -0
- package/src/modes/interactive/components/extensions/types.ts +191 -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 +560 -0
- package/src/modes/interactive/components/oauth-selector.ts +136 -0
- package/src/modes/interactive/components/plugin-settings.ts +481 -0
- package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
- package/src/modes/interactive/components/session-selector.ts +220 -0
- package/src/modes/interactive/components/settings-defs.ts +597 -0
- package/src/modes/interactive/components/settings-selector.ts +545 -0
- package/src/modes/interactive/components/show-images-selector.ts +45 -0
- package/src/modes/interactive/components/status-line/index.ts +4 -0
- package/src/modes/interactive/components/status-line/presets.ts +94 -0
- package/src/modes/interactive/components/status-line/segments.ts +350 -0
- package/src/modes/interactive/components/status-line/separators.ts +55 -0
- package/src/modes/interactive/components/status-line/types.ts +81 -0
- package/src/modes/interactive/components/status-line-segment-editor.ts +357 -0
- package/src/modes/interactive/components/status-line.ts +384 -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 +946 -0
- package/src/modes/interactive/components/tree-selector.ts +877 -0
- package/src/modes/interactive/components/ttsr-notification.ts +82 -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 +228 -0
- package/src/modes/interactive/interactive-mode.ts +2669 -0
- package/src/modes/interactive/theme/dark.json +102 -0
- package/src/modes/interactive/theme/defaults/dark-arctic.json +111 -0
- package/src/modes/interactive/theme/defaults/dark-catppuccin.json +106 -0
- package/src/modes/interactive/theme/defaults/dark-cyberpunk.json +109 -0
- package/src/modes/interactive/theme/defaults/dark-dracula.json +105 -0
- package/src/modes/interactive/theme/defaults/dark-forest.json +103 -0
- package/src/modes/interactive/theme/defaults/dark-github.json +112 -0
- package/src/modes/interactive/theme/defaults/dark-gruvbox.json +119 -0
- package/src/modes/interactive/theme/defaults/dark-monochrome.json +101 -0
- package/src/modes/interactive/theme/defaults/dark-monokai.json +105 -0
- package/src/modes/interactive/theme/defaults/dark-nord.json +104 -0
- package/src/modes/interactive/theme/defaults/dark-ocean.json +108 -0
- package/src/modes/interactive/theme/defaults/dark-one.json +107 -0
- package/src/modes/interactive/theme/defaults/dark-retro.json +99 -0
- package/src/modes/interactive/theme/defaults/dark-rose-pine.json +95 -0
- package/src/modes/interactive/theme/defaults/dark-solarized.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-sunset.json +106 -0
- package/src/modes/interactive/theme/defaults/dark-synthwave.json +102 -0
- package/src/modes/interactive/theme/defaults/dark-tokyo-night.json +108 -0
- package/src/modes/interactive/theme/defaults/index.ts +67 -0
- package/src/modes/interactive/theme/defaults/light-arctic.json +106 -0
- package/src/modes/interactive/theme/defaults/light-catppuccin.json +105 -0
- package/src/modes/interactive/theme/defaults/light-cyberpunk.json +103 -0
- package/src/modes/interactive/theme/defaults/light-forest.json +107 -0
- package/src/modes/interactive/theme/defaults/light-github.json +114 -0
- package/src/modes/interactive/theme/defaults/light-gruvbox.json +115 -0
- package/src/modes/interactive/theme/defaults/light-monochrome.json +100 -0
- package/src/modes/interactive/theme/defaults/light-ocean.json +106 -0
- package/src/modes/interactive/theme/defaults/light-one.json +105 -0
- package/src/modes/interactive/theme/defaults/light-retro.json +105 -0
- package/src/modes/interactive/theme/defaults/light-solarized.json +101 -0
- package/src/modes/interactive/theme/defaults/light-sunset.json +106 -0
- package/src/modes/interactive/theme/defaults/light-synthwave.json +105 -0
- package/src/modes/interactive/theme/defaults/light-tokyo-night.json +118 -0
- package/src/modes/interactive/theme/light.json +99 -0
- package/src/modes/interactive/theme/theme-schema.json +424 -0
- package/src/modes/interactive/theme/theme.ts +2211 -0
- package/src/modes/print-mode.ts +163 -0
- package/src/modes/rpc/rpc-client.ts +527 -0
- package/src/modes/rpc/rpc-mode.ts +494 -0
- package/src/modes/rpc/rpc-types.ts +203 -0
- package/src/prompts/architect-plan.md +10 -0
- package/src/prompts/branch-summary-preamble.md +3 -0
- package/src/prompts/branch-summary.md +28 -0
- package/src/prompts/browser.md +71 -0
- package/src/prompts/compaction-summary.md +34 -0
- package/src/prompts/compaction-turn-prefix.md +16 -0
- package/src/prompts/compaction-update-summary.md +41 -0
- package/src/prompts/explore.md +82 -0
- package/src/prompts/implement-with-critic.md +11 -0
- package/src/prompts/implement.md +11 -0
- package/src/prompts/init.md +30 -0
- package/src/prompts/plan.md +54 -0
- package/src/prompts/reviewer.md +81 -0
- package/src/prompts/summarization-system.md +3 -0
- package/src/prompts/system-prompt.md +27 -0
- package/src/prompts/task.md +56 -0
- package/src/prompts/title-system.md +8 -0
- package/src/prompts/tools/ask.md +24 -0
- package/src/prompts/tools/bash.md +23 -0
- package/src/prompts/tools/edit.md +9 -0
- package/src/prompts/tools/find.md +6 -0
- package/src/prompts/tools/grep.md +12 -0
- package/src/prompts/tools/lsp.md +14 -0
- package/src/prompts/tools/output.md +23 -0
- package/src/prompts/tools/read.md +25 -0
- package/src/prompts/tools/web-fetch.md +8 -0
- package/src/prompts/tools/web-search.md +10 -0
- package/src/prompts/tools/write.md +10 -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-snapshot.ts +218 -0
- package/src/utils/shell.ts +364 -0
- package/src/utils/tools-manager.ts +265 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { constants, existsSync } from "node:fs";
|
|
3
|
+
import { access, readFile, stat } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
6
|
+
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
7
|
+
import { Type } from "@sinclair/typebox";
|
|
8
|
+
import { globSync } from "glob";
|
|
9
|
+
import readDescription from "../../prompts/tools/read.md" with { type: "text" };
|
|
10
|
+
import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime";
|
|
11
|
+
import { ensureTool } from "../../utils/tools-manager";
|
|
12
|
+
import { untilAborted } from "../utils";
|
|
13
|
+
import { createLsTool } from "./ls";
|
|
14
|
+
import { resolveReadPath, resolveToCwd } from "./path-utils";
|
|
15
|
+
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from "./truncate";
|
|
16
|
+
|
|
17
|
+
// Document types convertible via markitdown
|
|
18
|
+
const CONVERTIBLE_EXTENSIONS = new Set([".pdf", ".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx", ".rtf", ".epub"]);
|
|
19
|
+
|
|
20
|
+
// Maximum image file size (20MB) - larger images will be rejected to prevent OOM during serialization
|
|
21
|
+
const MAX_IMAGE_SIZE = 20 * 1024 * 1024;
|
|
22
|
+
const MAX_FUZZY_RESULTS = 5;
|
|
23
|
+
const MAX_FUZZY_CANDIDATES = 20000;
|
|
24
|
+
const MIN_BASE_SIMILARITY = 0.5;
|
|
25
|
+
const MIN_FULL_SIMILARITY = 0.6;
|
|
26
|
+
|
|
27
|
+
function normalizePathForMatch(value: string): string {
|
|
28
|
+
return value
|
|
29
|
+
.replace(/\\/g, "/")
|
|
30
|
+
.replace(/^\.\/+/, "")
|
|
31
|
+
.replace(/\/+$/, "")
|
|
32
|
+
.toLowerCase();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isNotFoundError(error: unknown): boolean {
|
|
36
|
+
if (!error || typeof error !== "object") return false;
|
|
37
|
+
const code = (error as { code?: string }).code;
|
|
38
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isPathWithin(basePath: string, targetPath: string): boolean {
|
|
42
|
+
const relativePath = path.relative(basePath, targetPath);
|
|
43
|
+
return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function findExistingDirectory(startDir: string): Promise<string | null> {
|
|
47
|
+
let current = startDir;
|
|
48
|
+
const root = path.parse(startDir).root;
|
|
49
|
+
|
|
50
|
+
while (true) {
|
|
51
|
+
try {
|
|
52
|
+
const stats = await stat(current);
|
|
53
|
+
if (stats.isDirectory()) {
|
|
54
|
+
return current;
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// Keep walking up.
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (current === root) {
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
current = path.dirname(current);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function formatScopeLabel(searchRoot: string, cwd: string): string {
|
|
70
|
+
const relative = path.relative(cwd, searchRoot).replace(/\\/g, "/");
|
|
71
|
+
if (relative === "" || relative === ".") {
|
|
72
|
+
return ".";
|
|
73
|
+
}
|
|
74
|
+
if (!relative.startsWith("..") && !path.isAbsolute(relative)) {
|
|
75
|
+
return relative;
|
|
76
|
+
}
|
|
77
|
+
return searchRoot;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function buildDisplayPath(searchRoot: string, cwd: string, relativePath: string): string {
|
|
81
|
+
const scopeLabel = formatScopeLabel(searchRoot, cwd);
|
|
82
|
+
const normalized = relativePath.replace(/\\/g, "/");
|
|
83
|
+
if (scopeLabel === ".") {
|
|
84
|
+
return normalized;
|
|
85
|
+
}
|
|
86
|
+
if (scopeLabel.startsWith("..") || path.isAbsolute(scopeLabel)) {
|
|
87
|
+
return path.join(searchRoot, normalized).replace(/\\/g, "/");
|
|
88
|
+
}
|
|
89
|
+
return `${scopeLabel}/${normalized}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function levenshteinDistance(a: string, b: string): number {
|
|
93
|
+
if (a === b) return 0;
|
|
94
|
+
const aLen = a.length;
|
|
95
|
+
const bLen = b.length;
|
|
96
|
+
if (aLen === 0) return bLen;
|
|
97
|
+
if (bLen === 0) return aLen;
|
|
98
|
+
|
|
99
|
+
let prev = new Array<number>(bLen + 1);
|
|
100
|
+
let curr = new Array<number>(bLen + 1);
|
|
101
|
+
for (let j = 0; j <= bLen; j++) {
|
|
102
|
+
prev[j] = j;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (let i = 1; i <= aLen; i++) {
|
|
106
|
+
curr[0] = i;
|
|
107
|
+
const aCode = a.charCodeAt(i - 1);
|
|
108
|
+
for (let j = 1; j <= bLen; j++) {
|
|
109
|
+
const cost = aCode === b.charCodeAt(j - 1) ? 0 : 1;
|
|
110
|
+
const deletion = prev[j] + 1;
|
|
111
|
+
const insertion = curr[j - 1] + 1;
|
|
112
|
+
const substitution = prev[j - 1] + cost;
|
|
113
|
+
curr[j] = Math.min(deletion, insertion, substitution);
|
|
114
|
+
}
|
|
115
|
+
const tmp = prev;
|
|
116
|
+
prev = curr;
|
|
117
|
+
curr = tmp;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return prev[bLen];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function similarityScore(a: string, b: string): number {
|
|
124
|
+
if (a.length === 0 && b.length === 0) {
|
|
125
|
+
return 1;
|
|
126
|
+
}
|
|
127
|
+
const maxLen = Math.max(a.length, b.length);
|
|
128
|
+
if (maxLen === 0) {
|
|
129
|
+
return 1;
|
|
130
|
+
}
|
|
131
|
+
const distance = levenshteinDistance(a, b);
|
|
132
|
+
return 1 - distance / maxLen;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function listCandidateFiles(
|
|
136
|
+
searchRoot: string,
|
|
137
|
+
): Promise<{ files: string[]; truncated: boolean; error?: string }> {
|
|
138
|
+
let fdPath: string | undefined;
|
|
139
|
+
try {
|
|
140
|
+
fdPath = await ensureTool("fd", true);
|
|
141
|
+
} catch {
|
|
142
|
+
return { files: [], truncated: false, error: "fd not available" };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!fdPath) {
|
|
146
|
+
return { files: [], truncated: false, error: "fd not available" };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const args: string[] = ["--type", "f", "--color=never", "--hidden", "--max-results", String(MAX_FUZZY_CANDIDATES)];
|
|
150
|
+
|
|
151
|
+
const gitignoreFiles = new Set<string>();
|
|
152
|
+
const rootGitignore = path.join(searchRoot, ".gitignore");
|
|
153
|
+
if (existsSync(rootGitignore)) {
|
|
154
|
+
gitignoreFiles.add(rootGitignore);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const nestedGitignores = globSync("**/.gitignore", {
|
|
159
|
+
cwd: searchRoot,
|
|
160
|
+
dot: true,
|
|
161
|
+
absolute: true,
|
|
162
|
+
ignore: ["**/node_modules/**", "**/.git/**"],
|
|
163
|
+
});
|
|
164
|
+
for (const file of nestedGitignores) {
|
|
165
|
+
gitignoreFiles.add(file);
|
|
166
|
+
}
|
|
167
|
+
} catch {
|
|
168
|
+
// Ignore glob errors.
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
for (const gitignorePath of gitignoreFiles) {
|
|
172
|
+
args.push("--ignore-file", gitignorePath);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
args.push(".", searchRoot);
|
|
176
|
+
|
|
177
|
+
const result = Bun.spawnSync([fdPath, ...args], {
|
|
178
|
+
stdin: "ignore",
|
|
179
|
+
stdout: "pipe",
|
|
180
|
+
stderr: "pipe",
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const output = result.stdout.toString().trim();
|
|
184
|
+
|
|
185
|
+
if (result.exitCode !== 0 && !output) {
|
|
186
|
+
const errorMsg = result.stderr.toString().trim() || `fd exited with code ${result.exitCode}`;
|
|
187
|
+
return { files: [], truncated: false, error: errorMsg };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!output) {
|
|
191
|
+
return { files: [], truncated: false };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const files = output
|
|
195
|
+
.split("\n")
|
|
196
|
+
.map((line) => line.replace(/\r$/, "").trim())
|
|
197
|
+
.filter((line) => line.length > 0);
|
|
198
|
+
|
|
199
|
+
return { files, truncated: files.length >= MAX_FUZZY_CANDIDATES };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function findReadPathSuggestions(
|
|
203
|
+
rawPath: string,
|
|
204
|
+
cwd: string,
|
|
205
|
+
): Promise<{ suggestions: string[]; scopeLabel?: string; truncated?: boolean; error?: string } | null> {
|
|
206
|
+
const resolvedPath = resolveToCwd(rawPath, cwd);
|
|
207
|
+
const searchRoot = await findExistingDirectory(path.dirname(resolvedPath));
|
|
208
|
+
if (!searchRoot) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!isPathWithin(cwd, resolvedPath)) {
|
|
213
|
+
const root = path.parse(searchRoot).root;
|
|
214
|
+
if (searchRoot === root) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const { files, truncated, error } = await listCandidateFiles(searchRoot);
|
|
220
|
+
const scopeLabel = formatScopeLabel(searchRoot, cwd);
|
|
221
|
+
|
|
222
|
+
if (error && files.length === 0) {
|
|
223
|
+
return { suggestions: [], scopeLabel, truncated, error };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (files.length === 0) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const queryPath = (() => {
|
|
231
|
+
if (path.isAbsolute(rawPath)) {
|
|
232
|
+
const relative = path.relative(cwd, resolvedPath).replace(/\\/g, "/");
|
|
233
|
+
if (relative && !relative.startsWith("..") && !path.isAbsolute(relative)) {
|
|
234
|
+
return normalizePathForMatch(relative);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return normalizePathForMatch(rawPath);
|
|
238
|
+
})();
|
|
239
|
+
const baseQuery = path.posix.basename(queryPath);
|
|
240
|
+
|
|
241
|
+
const matches: Array<{ path: string; score: number; baseScore: number; fullScore: number }> = [];
|
|
242
|
+
const seen = new Set<string>();
|
|
243
|
+
|
|
244
|
+
for (const file of files) {
|
|
245
|
+
const cleaned = file.replace(/\r$/, "").trim();
|
|
246
|
+
if (!cleaned) continue;
|
|
247
|
+
|
|
248
|
+
const relativePath = path.isAbsolute(cleaned)
|
|
249
|
+
? cleaned.startsWith(searchRoot)
|
|
250
|
+
? cleaned.slice(searchRoot.length + 1)
|
|
251
|
+
: path.relative(searchRoot, cleaned)
|
|
252
|
+
: cleaned;
|
|
253
|
+
|
|
254
|
+
if (!relativePath || relativePath.startsWith("..")) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const displayPath = buildDisplayPath(searchRoot, cwd, relativePath);
|
|
259
|
+
if (seen.has(displayPath)) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
seen.add(displayPath);
|
|
263
|
+
|
|
264
|
+
const normalizedDisplay = normalizePathForMatch(displayPath);
|
|
265
|
+
const baseCandidate = path.posix.basename(normalizedDisplay);
|
|
266
|
+
|
|
267
|
+
const fullScore = similarityScore(queryPath, normalizedDisplay);
|
|
268
|
+
const baseScore = baseQuery ? similarityScore(baseQuery, baseCandidate) : 0;
|
|
269
|
+
|
|
270
|
+
if (baseQuery) {
|
|
271
|
+
if (baseScore < MIN_BASE_SIMILARITY && fullScore < MIN_FULL_SIMILARITY) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
} else if (fullScore < MIN_FULL_SIMILARITY) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const score = baseQuery ? baseScore * 0.75 + fullScore * 0.25 : fullScore;
|
|
279
|
+
matches.push({ path: displayPath, score, baseScore, fullScore });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (matches.length === 0) {
|
|
283
|
+
return { suggestions: [], scopeLabel, truncated };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
matches.sort((a, b) => {
|
|
287
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
288
|
+
if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore;
|
|
289
|
+
return a.path.localeCompare(b.path);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const suggestions = matches.slice(0, MAX_FUZZY_RESULTS).map((match) => match.path);
|
|
293
|
+
|
|
294
|
+
return { suggestions, scopeLabel, truncated };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function convertWithMarkitdown(filePath: string): { content: string; ok: boolean; error?: string } {
|
|
298
|
+
const cmd = Bun.which("markitdown");
|
|
299
|
+
if (!cmd) {
|
|
300
|
+
return { content: "", ok: false, error: "markitdown not found" };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const result = spawnSync(cmd, [filePath], {
|
|
304
|
+
encoding: "utf-8",
|
|
305
|
+
timeout: 60000,
|
|
306
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
if (result.status === 0 && result.stdout && result.stdout.length > 0) {
|
|
310
|
+
return { content: result.stdout, ok: true };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return { content: "", ok: false, error: result.stderr || "Conversion failed" };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const readSchema = Type.Object({
|
|
317
|
+
path: Type.String({ description: "Path to the file to read (relative or absolute)" }),
|
|
318
|
+
offset: Type.Optional(Type.Number({ description: "Line number to start reading from (1-indexed)" })),
|
|
319
|
+
limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" })),
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
export interface ReadToolDetails {
|
|
323
|
+
truncation?: TruncationResult;
|
|
324
|
+
redirectedTo?: "ls";
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function createReadTool(cwd: string): AgentTool<typeof readSchema> {
|
|
328
|
+
const lsTool = createLsTool(cwd);
|
|
329
|
+
return {
|
|
330
|
+
name: "read",
|
|
331
|
+
label: "Read",
|
|
332
|
+
description: readDescription.replace("{{DEFAULT_MAX_LINES}}", String(DEFAULT_MAX_LINES)),
|
|
333
|
+
parameters: readSchema,
|
|
334
|
+
execute: async (
|
|
335
|
+
toolCallId: string,
|
|
336
|
+
{ path: readPath, offset, limit }: { path: string; offset?: number; limit?: number },
|
|
337
|
+
signal?: AbortSignal,
|
|
338
|
+
) => {
|
|
339
|
+
const absolutePath = resolveReadPath(readPath, cwd);
|
|
340
|
+
|
|
341
|
+
return untilAborted(signal, async () => {
|
|
342
|
+
let fileStat: Awaited<ReturnType<typeof stat>>;
|
|
343
|
+
try {
|
|
344
|
+
fileStat = await stat(absolutePath);
|
|
345
|
+
} catch (error) {
|
|
346
|
+
if (isNotFoundError(error)) {
|
|
347
|
+
const suggestions = await findReadPathSuggestions(readPath, cwd);
|
|
348
|
+
let message = `File not found: ${readPath}`;
|
|
349
|
+
|
|
350
|
+
if (suggestions?.suggestions.length) {
|
|
351
|
+
const scopeLabel = suggestions.scopeLabel ? ` in ${suggestions.scopeLabel}` : "";
|
|
352
|
+
message += `\n\nClosest matches${scopeLabel}:\n${suggestions.suggestions
|
|
353
|
+
.map((match) => `- ${match}`)
|
|
354
|
+
.join("\n")}`;
|
|
355
|
+
if (suggestions.truncated) {
|
|
356
|
+
message += `\n[Search truncated to first ${MAX_FUZZY_CANDIDATES} paths. Refine the path if the match isn't listed.]`;
|
|
357
|
+
}
|
|
358
|
+
} else if (suggestions?.error) {
|
|
359
|
+
message += `\n\nFuzzy match failed: ${suggestions.error}`;
|
|
360
|
+
} else if (suggestions?.scopeLabel) {
|
|
361
|
+
message += `\n\nNo similar paths found in ${suggestions.scopeLabel}.`;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
throw new Error(message);
|
|
365
|
+
}
|
|
366
|
+
throw error;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (fileStat.isDirectory()) {
|
|
370
|
+
const lsResult = await lsTool.execute(toolCallId, { path: readPath, limit }, signal);
|
|
371
|
+
return {
|
|
372
|
+
content: lsResult.content,
|
|
373
|
+
details: { redirectedTo: "ls", truncation: lsResult.details?.truncation },
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
await access(absolutePath, constants.R_OK);
|
|
378
|
+
|
|
379
|
+
const mimeType = await detectSupportedImageMimeTypeFromFile(absolutePath);
|
|
380
|
+
const ext = path.extname(absolutePath).toLowerCase();
|
|
381
|
+
|
|
382
|
+
// Read the file based on type
|
|
383
|
+
let content: (TextContent | ImageContent)[];
|
|
384
|
+
let details: ReadToolDetails | undefined;
|
|
385
|
+
|
|
386
|
+
if (mimeType) {
|
|
387
|
+
// Check image file size before reading to prevent OOM during serialization
|
|
388
|
+
const fileStat = await stat(absolutePath);
|
|
389
|
+
if (fileStat.size > MAX_IMAGE_SIZE) {
|
|
390
|
+
const sizeStr = formatSize(fileStat.size);
|
|
391
|
+
const maxStr = formatSize(MAX_IMAGE_SIZE);
|
|
392
|
+
content = [
|
|
393
|
+
{
|
|
394
|
+
type: "text",
|
|
395
|
+
text: `[Image file too large: ${sizeStr} exceeds ${maxStr} limit. Use an image viewer or resize the image.]`,
|
|
396
|
+
},
|
|
397
|
+
];
|
|
398
|
+
} else {
|
|
399
|
+
// Read as image (binary)
|
|
400
|
+
const buffer = await readFile(absolutePath);
|
|
401
|
+
const base64 = buffer.toString("base64");
|
|
402
|
+
|
|
403
|
+
content = [
|
|
404
|
+
{ type: "text", text: `Read image file [${mimeType}]` },
|
|
405
|
+
{ type: "image", data: base64, mimeType },
|
|
406
|
+
];
|
|
407
|
+
}
|
|
408
|
+
} else if (CONVERTIBLE_EXTENSIONS.has(ext)) {
|
|
409
|
+
// Convert document via markitdown
|
|
410
|
+
const result = convertWithMarkitdown(absolutePath);
|
|
411
|
+
if (result.ok) {
|
|
412
|
+
// Apply truncation to converted content
|
|
413
|
+
const truncation = truncateHead(result.content);
|
|
414
|
+
let outputText = truncation.content;
|
|
415
|
+
|
|
416
|
+
if (truncation.truncated) {
|
|
417
|
+
outputText += `\n\n[Document converted via markitdown. Output truncated to $formatSize(
|
|
418
|
+
DEFAULT_MAX_BYTES,
|
|
419
|
+
)]`;
|
|
420
|
+
details = { truncation };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
content = [{ type: "text", text: outputText }];
|
|
424
|
+
} else {
|
|
425
|
+
// markitdown not available or failed
|
|
426
|
+
const errorMsg =
|
|
427
|
+
result.error === "markitdown not found"
|
|
428
|
+
? `markitdown not installed. Install with: pip install markitdown`
|
|
429
|
+
: result.error || "conversion failed";
|
|
430
|
+
content = [{ type: "text", text: `[Cannot read ${ext} file: ${errorMsg}]` }];
|
|
431
|
+
}
|
|
432
|
+
} else {
|
|
433
|
+
// Read as text
|
|
434
|
+
const textContent = await readFile(absolutePath, "utf-8");
|
|
435
|
+
const allLines = textContent.split("\n");
|
|
436
|
+
const totalFileLines = allLines.length;
|
|
437
|
+
|
|
438
|
+
// Apply offset if specified (1-indexed to 0-indexed)
|
|
439
|
+
const startLine = offset ? Math.max(0, offset - 1) : 0;
|
|
440
|
+
const startLineDisplay = startLine + 1; // For display (1-indexed)
|
|
441
|
+
|
|
442
|
+
// Check if offset is out of bounds
|
|
443
|
+
if (startLine >= allLines.length) {
|
|
444
|
+
throw new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// If limit is specified by user, use it; otherwise we'll let truncateHead decide
|
|
448
|
+
let selectedContent: string;
|
|
449
|
+
let userLimitedLines: number | undefined;
|
|
450
|
+
if (limit !== undefined) {
|
|
451
|
+
const endLine = Math.min(startLine + limit, allLines.length);
|
|
452
|
+
selectedContent = allLines.slice(startLine, endLine).join("\n");
|
|
453
|
+
userLimitedLines = endLine - startLine;
|
|
454
|
+
} else {
|
|
455
|
+
selectedContent = allLines.slice(startLine).join("\n");
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Apply truncation (respects both line and byte limits)
|
|
459
|
+
const truncation = truncateHead(selectedContent);
|
|
460
|
+
|
|
461
|
+
let outputText: string;
|
|
462
|
+
|
|
463
|
+
if (truncation.firstLineExceedsLimit) {
|
|
464
|
+
// First line at offset exceeds 30KB - tell model to use bash
|
|
465
|
+
const firstLineSize = formatSize(Buffer.byteLength(allLines[startLine], "utf-8"));
|
|
466
|
+
outputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(
|
|
467
|
+
DEFAULT_MAX_BYTES,
|
|
468
|
+
)} limit. Use bash: sed -n '${startLineDisplay}p' ${readPath} | head -c ${DEFAULT_MAX_BYTES}]`;
|
|
469
|
+
details = { truncation };
|
|
470
|
+
} else if (truncation.truncated) {
|
|
471
|
+
// Truncation occurred - build actionable notice
|
|
472
|
+
const endLineDisplay = startLineDisplay + truncation.outputLines - 1;
|
|
473
|
+
const nextOffset = endLineDisplay + 1;
|
|
474
|
+
|
|
475
|
+
outputText = truncation.content;
|
|
476
|
+
|
|
477
|
+
if (truncation.truncatedBy === "lines") {
|
|
478
|
+
outputText += `\n\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue]`;
|
|
479
|
+
} else {
|
|
480
|
+
outputText += `\n\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(
|
|
481
|
+
DEFAULT_MAX_BYTES,
|
|
482
|
+
)} limit). Use offset=${nextOffset} to continue]`;
|
|
483
|
+
}
|
|
484
|
+
details = { truncation };
|
|
485
|
+
} else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {
|
|
486
|
+
// User specified limit, there's more content, but no truncation
|
|
487
|
+
const remaining = allLines.length - (startLine + userLimitedLines);
|
|
488
|
+
const nextOffset = startLine + userLimitedLines + 1;
|
|
489
|
+
|
|
490
|
+
outputText = truncation.content;
|
|
491
|
+
outputText += `\n\n[${remaining} more lines in file. Use offset=${nextOffset} to continue]`;
|
|
492
|
+
} else {
|
|
493
|
+
// No truncation, no user limit exceeded
|
|
494
|
+
outputText = truncation.content;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
content = [{ type: "text", text: outputText }];
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return { content, details };
|
|
501
|
+
});
|
|
502
|
+
},
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** Default read tool using process.cwd() - for backwards compatibility */
|
|
507
|
+
export const readTool = createReadTool(process.cwd());
|