oh-my-opencode 4.2.2 → 4.2.3
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/README.ja.md +7 -0
- package/README.ko.md +7 -0
- package/README.md +13 -0
- package/README.ru.md +7 -0
- package/README.zh-cn.md +7 -0
- package/dist/cli/doctor/checks/tools-lsp.d.ts +6 -1
- package/dist/cli/index.js +2367 -1240
- package/dist/features/background-agent/parent-wake-notifier.d.ts +1 -1
- package/dist/hooks/comment-checker/apply-patch-edits.d.ts +2 -0
- package/dist/hooks/comment-checker/hook.d.ts +1 -0
- package/dist/hooks/comment-checker/initialization-gate.d.ts +1 -0
- package/dist/hooks/directory-agents-injector/finder.d.ts +4 -2
- package/dist/hooks/directory-agents-injector/injector.d.ts +2 -0
- package/dist/hooks/rules-injector/injector.d.ts +7 -3
- package/dist/hooks/rules-injector/matcher.d.ts +3 -24
- package/dist/hooks/rules-injector/parser.d.ts +2 -18
- package/dist/hooks/rules-injector/project-root-finder.d.ts +1 -13
- package/dist/hooks/rules-injector/rule-distance.d.ts +1 -10
- package/dist/hooks/rules-injector/rule-file-finder.d.ts +2 -6
- package/dist/hooks/rules-injector/rule-file-scanner.d.ts +2 -13
- package/dist/hooks/rules-injector/rule-scan-cache.d.ts +2 -13
- package/dist/hooks/rules-injector/transcript-hydration.d.ts +18 -0
- package/dist/hooks/rules-injector/types.d.ts +2 -38
- package/dist/hooks/runtime-fallback/types.d.ts +1 -0
- package/dist/hooks/team-session-events/team-member-error-handler.d.ts +14 -1
- package/dist/hooks/unstable-agent-babysitter/unstable-agent-babysitter-hook.d.ts +0 -21
- package/dist/index.js +4094 -3170
- package/dist/mcp/ast-grep.d.ts +9 -0
- package/dist/mcp/index.d.ts +4 -1
- package/dist/mcp/types.d.ts +1 -0
- package/dist/plugin/tool-execute-after.d.ts +15 -9
- package/dist/plugin/tool-registry.d.ts +1 -2
- package/dist/plugin-handlers/mcp-config-handler.d.ts +3 -0
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/prompt-async-gate.d.ts +13 -1
- package/dist/shared/prompt-failure-classifier.d.ts +9 -0
- package/dist/shared/prompt-timeout-context.d.ts +1 -0
- package/dist/tools/delegate-task/sync-prompt-sender.d.ts +1 -2
- package/dist/tools/index.d.ts +0 -1
- package/package.json +38 -28
- package/packages/ast-grep-mcp/dist/cli.js +861 -0
- package/packages/lsp-tools-mcp/dist/cli.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/cli.js +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/cleanup-errors.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/cleanup-errors.js +1 -2
- package/packages/lsp-tools-mcp/dist/lsp/client-wrapper.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/client-wrapper.js +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/client.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/client.js +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/config-loader.d.ts +1 -10
- package/packages/lsp-tools-mcp/dist/lsp/config-loader.js +55 -10
- package/packages/lsp-tools-mcp/dist/lsp/connection.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/connection.js +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/constants.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/constants.js +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/directory-diagnostics.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/directory-diagnostics.js +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/errors.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/errors.js +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/formatters.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/formatters.js +7 -10
- package/packages/lsp-tools-mcp/dist/lsp/infer-extension.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/infer-extension.js +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/json-rpc-connection.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/json-rpc-connection.js +10 -11
- package/packages/lsp-tools-mcp/dist/lsp/language-mappings.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/language-mappings.js +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/manager.d.ts +1 -3
- package/packages/lsp-tools-mcp/dist/lsp/manager.js +6 -23
- package/packages/lsp-tools-mcp/dist/lsp/process-signal-cleanup.d.ts +1 -0
- package/packages/lsp-tools-mcp/dist/lsp/process-signal-cleanup.js +17 -0
- package/packages/lsp-tools-mcp/dist/lsp/process.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/process.js +41 -12
- package/packages/lsp-tools-mcp/dist/lsp/server-definitions.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/server-definitions.js +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/server-installation.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/server-installation.js +3 -4
- package/packages/lsp-tools-mcp/dist/lsp/server-resolution.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/server-resolution.js +18 -7
- package/packages/lsp-tools-mcp/dist/lsp/transport.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/transport.js +20 -11
- package/packages/lsp-tools-mcp/dist/lsp/types.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/types.js +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/utils.d.ts +0 -2
- package/packages/lsp-tools-mcp/dist/lsp/utils.js +0 -8
- package/packages/lsp-tools-mcp/dist/lsp/workspace-edit.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/workspace-edit.js +0 -1
- package/packages/lsp-tools-mcp/dist/mcp.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/mcp.js +14 -14
- package/packages/lsp-tools-mcp/dist/tools.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/tools.js +21 -14
- package/dist/tools/ast-grep/cli-binary-path-resolution.d.ts +0 -5
- package/dist/tools/ast-grep/cli.d.ts +0 -12
- package/dist/tools/ast-grep/constants.d.ts +0 -5
- package/dist/tools/ast-grep/downloader.d.ts +0 -5
- package/dist/tools/ast-grep/environment-check.d.ts +0 -20
- package/dist/tools/ast-grep/index.d.ts +0 -5
- package/dist/tools/ast-grep/language-support.d.ts +0 -6
- package/dist/tools/ast-grep/pattern-hints.d.ts +0 -4
- package/dist/tools/ast-grep/process-output-timeout.d.ts +0 -12
- package/dist/tools/ast-grep/result-formatter.d.ts +0 -5
- package/dist/tools/ast-grep/sg-cli-path.d.ts +0 -3
- package/dist/tools/ast-grep/sg-compact-json-output.d.ts +0 -2
- package/dist/tools/ast-grep/tool-descriptions.d.ts +0 -3
- package/dist/tools/ast-grep/tools.d.ts +0 -3
- package/dist/tools/ast-grep/types.d.ts +0 -58
- package/packages/lsp-tools-mcp/dist/cli.d.ts.map +0 -1
- package/packages/lsp-tools-mcp/dist/cli.js.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/cleanup-errors.d.ts.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/cleanup-errors.js.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/client-wrapper.d.ts.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/client-wrapper.js.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/client.d.ts.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/client.js.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/config-loader.d.ts.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/config-loader.js.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/connection.d.ts.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/connection.js.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/constants.d.ts.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/constants.js.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/directory-diagnostics.d.ts.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/directory-diagnostics.js.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/errors.d.ts.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/errors.js.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/formatters.d.ts.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/formatters.js.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/infer-extension.d.ts.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/infer-extension.js.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/json-rpc-connection.d.ts.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/json-rpc-connection.js.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/language-mappings.d.ts.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/language-mappings.js.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/manager.d.ts.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/manager.js.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/process.d.ts.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/process.js.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/server-definitions.d.ts.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/server-definitions.js.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/server-installation.d.ts.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/server-installation.js.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/server-resolution.d.ts.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/server-resolution.js.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/transport.d.ts.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/transport.js.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/types.d.ts.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/types.js.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/utils.d.ts.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/utils.js.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/workspace-edit.d.ts.map +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/workspace-edit.js.map +0 -1
- package/packages/lsp-tools-mcp/dist/mcp.d.ts.map +0 -1
- package/packages/lsp-tools-mcp/dist/mcp.js.map +0 -1
- package/packages/lsp-tools-mcp/dist/tools.d.ts.map +0 -1
- package/packages/lsp-tools-mcp/dist/tools.js.map +0 -1
|
@@ -0,0 +1,861 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { argv, stderr } from "node:process";
|
|
5
|
+
|
|
6
|
+
// src/mcp.ts
|
|
7
|
+
import { createInterface } from "node:readline";
|
|
8
|
+
|
|
9
|
+
// src/language-support.ts
|
|
10
|
+
var CLI_LANGUAGES = [
|
|
11
|
+
"bash",
|
|
12
|
+
"c",
|
|
13
|
+
"cpp",
|
|
14
|
+
"csharp",
|
|
15
|
+
"css",
|
|
16
|
+
"elixir",
|
|
17
|
+
"go",
|
|
18
|
+
"haskell",
|
|
19
|
+
"html",
|
|
20
|
+
"java",
|
|
21
|
+
"javascript",
|
|
22
|
+
"json",
|
|
23
|
+
"kotlin",
|
|
24
|
+
"lua",
|
|
25
|
+
"nix",
|
|
26
|
+
"php",
|
|
27
|
+
"python",
|
|
28
|
+
"ruby",
|
|
29
|
+
"rust",
|
|
30
|
+
"scala",
|
|
31
|
+
"solidity",
|
|
32
|
+
"swift",
|
|
33
|
+
"typescript",
|
|
34
|
+
"tsx",
|
|
35
|
+
"yaml"
|
|
36
|
+
];
|
|
37
|
+
var DEFAULT_TIMEOUT_MS = 300000;
|
|
38
|
+
var DEFAULT_MAX_OUTPUT_BYTES = 1 * 1024 * 1024;
|
|
39
|
+
var DEFAULT_MAX_MATCHES = 500;
|
|
40
|
+
// src/sg-cli-path.ts
|
|
41
|
+
import { createRequire } from "module";
|
|
42
|
+
import { dirname, join } from "path";
|
|
43
|
+
import { existsSync, statSync } from "fs";
|
|
44
|
+
function isValidBinary(filePath) {
|
|
45
|
+
try {
|
|
46
|
+
return statSync(filePath).size > 1e4;
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function getPlatformPackageName() {
|
|
52
|
+
const platform = process.platform;
|
|
53
|
+
const arch = process.arch;
|
|
54
|
+
const platformMap = {
|
|
55
|
+
"darwin-arm64": "@ast-grep/cli-darwin-arm64",
|
|
56
|
+
"darwin-x64": "@ast-grep/cli-darwin-x64",
|
|
57
|
+
"linux-arm64": "@ast-grep/cli-linux-arm64-gnu",
|
|
58
|
+
"linux-x64": "@ast-grep/cli-linux-x64-gnu",
|
|
59
|
+
"win32-x64": "@ast-grep/cli-win32-x64-msvc",
|
|
60
|
+
"win32-arm64": "@ast-grep/cli-win32-arm64-msvc",
|
|
61
|
+
"win32-ia32": "@ast-grep/cli-win32-ia32-msvc"
|
|
62
|
+
};
|
|
63
|
+
return platformMap[`${platform}-${arch}`] ?? null;
|
|
64
|
+
}
|
|
65
|
+
function findSgCliPathSync() {
|
|
66
|
+
const binaryName = process.platform === "win32" ? "sg.exe" : "sg";
|
|
67
|
+
try {
|
|
68
|
+
const require2 = createRequire(import.meta.url);
|
|
69
|
+
const cliPackageJsonPath = require2.resolve("@ast-grep/cli/package.json");
|
|
70
|
+
const cliDirectory = dirname(cliPackageJsonPath);
|
|
71
|
+
const sgPath = join(cliDirectory, binaryName);
|
|
72
|
+
if (existsSync(sgPath) && isValidBinary(sgPath)) {
|
|
73
|
+
return sgPath;
|
|
74
|
+
}
|
|
75
|
+
} catch {}
|
|
76
|
+
const platformPackage = getPlatformPackageName();
|
|
77
|
+
if (platformPackage) {
|
|
78
|
+
try {
|
|
79
|
+
const require2 = createRequire(import.meta.url);
|
|
80
|
+
const packageJsonPath = require2.resolve(`${platformPackage}/package.json`);
|
|
81
|
+
const packageDirectory = dirname(packageJsonPath);
|
|
82
|
+
const astGrepBinaryName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep";
|
|
83
|
+
const binaryPath = join(packageDirectory, astGrepBinaryName);
|
|
84
|
+
if (existsSync(binaryPath) && isValidBinary(binaryPath)) {
|
|
85
|
+
return binaryPath;
|
|
86
|
+
}
|
|
87
|
+
} catch {}
|
|
88
|
+
}
|
|
89
|
+
if (process.platform === "darwin") {
|
|
90
|
+
const homebrewPaths = ["/opt/homebrew/bin/sg", "/usr/local/bin/sg"];
|
|
91
|
+
for (const path of homebrewPaths) {
|
|
92
|
+
if (existsSync(path) && isValidBinary(path)) {
|
|
93
|
+
return path;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
var resolvedCliPath = null;
|
|
100
|
+
function getSgCliPath() {
|
|
101
|
+
if (resolvedCliPath !== null) {
|
|
102
|
+
return resolvedCliPath;
|
|
103
|
+
}
|
|
104
|
+
const syncPath = findSgCliPathSync();
|
|
105
|
+
if (syncPath) {
|
|
106
|
+
resolvedCliPath = syncPath;
|
|
107
|
+
return syncPath;
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
function setSgCliPath(path) {
|
|
112
|
+
resolvedCliPath = path;
|
|
113
|
+
}
|
|
114
|
+
// src/pattern-hints.ts
|
|
115
|
+
function detectRegexMisuse(pattern) {
|
|
116
|
+
const src = pattern.trim();
|
|
117
|
+
if (/\\[wWdDsSbB]/.test(src)) {
|
|
118
|
+
return 'Hint: "\\w", "\\d", "\\s", "\\b" are regex escapes. ast-grep matches AST nodes, not text - use $VAR for identifiers, $$$ for node lists, or switch to grep for text search.';
|
|
119
|
+
}
|
|
120
|
+
if (/\[[a-zA-Z0-9]-[a-zA-Z0-9]\]/.test(src)) {
|
|
121
|
+
return 'Hint: "[a-z]" and similar character classes are regex, not AST. Use $VAR to match any identifier, or switch to grep for text search.';
|
|
122
|
+
}
|
|
123
|
+
if (!src.includes("$") && /\w\.[*+]/.test(src)) {
|
|
124
|
+
return 'Hint: ".*" and ".+" are regex wildcards. In ast-grep use $$$ for multiple AST nodes and $VAR for a single node. For text patterns, switch to grep.';
|
|
125
|
+
}
|
|
126
|
+
if (/^[-\w.*]+\|[-\w.*|]+$/.test(src)) {
|
|
127
|
+
return 'Hint: "|" is regex alternation and does NOT work in ast-grep patterns. Options: (a) fire one ast_grep_search per alternative, or (b) switch to grep with a regex pattern like "foo|bar".';
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
function detectLanguageSpecificMistake(pattern, lang) {
|
|
132
|
+
const src = pattern.trim();
|
|
133
|
+
if (lang === "python") {
|
|
134
|
+
if (src.startsWith("class ") && src.endsWith(":")) {
|
|
135
|
+
return `Hint: Remove trailing colon. Try: "${src.slice(0, -1)}"`;
|
|
136
|
+
}
|
|
137
|
+
if ((src.startsWith("def ") || src.startsWith("async def ")) && src.endsWith(":")) {
|
|
138
|
+
return `Hint: Remove trailing colon. Try: "${src.slice(0, -1)}"`;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (["javascript", "typescript", "tsx"].includes(lang)) {
|
|
142
|
+
if (/^(export\s+)?(async\s+)?function\s+\$[A-Z_]+\s*$/i.test(src)) {
|
|
143
|
+
return 'Hint: Function patterns need params and body. Try "function $NAME($$$) { $$$ }"';
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (lang === "go") {
|
|
147
|
+
if (/^func\s+\$[A-Z_]+\s*$/i.test(src)) {
|
|
148
|
+
return 'Hint: Go function patterns need params and body. Try "func $NAME($$$) { $$$ }"';
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (lang === "rust") {
|
|
152
|
+
if (/^fn\s+\$[A-Z_]+\s*$/i.test(src)) {
|
|
153
|
+
return 'Hint: Rust fn patterns need params and body. Try "fn $NAME($$$) { $$$ }"';
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
function getPatternHint(pattern, lang) {
|
|
159
|
+
return detectRegexMisuse(pattern) ?? detectLanguageSpecificMistake(pattern, lang);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/result-formatter.ts
|
|
163
|
+
function formatSearchResult(result) {
|
|
164
|
+
if (result.error) {
|
|
165
|
+
return `Error: ${result.error}`;
|
|
166
|
+
}
|
|
167
|
+
if (result.matches.length === 0) {
|
|
168
|
+
return "No matches found";
|
|
169
|
+
}
|
|
170
|
+
const lines = [];
|
|
171
|
+
if (result.truncated) {
|
|
172
|
+
const reason = result.truncatedReason === "max_matches" ? `showing first ${result.matches.length} of ${result.totalMatches}` : result.truncatedReason === "max_output_bytes" ? "output exceeded 1MB limit" : "search timed out";
|
|
173
|
+
lines.push(`[TRUNCATED] Results truncated (${reason})
|
|
174
|
+
`);
|
|
175
|
+
}
|
|
176
|
+
lines.push(`Found ${result.matches.length} match(es)${result.truncated ? ` (truncated from ${result.totalMatches})` : ""}:
|
|
177
|
+
`);
|
|
178
|
+
for (const match of result.matches) {
|
|
179
|
+
const loc = `${match.file}:${match.range.start.line + 1}:${match.range.start.column + 1}`;
|
|
180
|
+
lines.push(`${loc}`);
|
|
181
|
+
lines.push(` ${match.lines.trim()}`);
|
|
182
|
+
lines.push("");
|
|
183
|
+
}
|
|
184
|
+
return lines.join(`
|
|
185
|
+
`);
|
|
186
|
+
}
|
|
187
|
+
function formatReplaceResult(result, isDryRun) {
|
|
188
|
+
if (result.error) {
|
|
189
|
+
return `Error: ${result.error}`;
|
|
190
|
+
}
|
|
191
|
+
if (result.matches.length === 0) {
|
|
192
|
+
return "No matches found to replace";
|
|
193
|
+
}
|
|
194
|
+
const prefix = isDryRun ? "[DRY RUN] " : "";
|
|
195
|
+
const lines = [];
|
|
196
|
+
if (result.truncated) {
|
|
197
|
+
const reason = result.truncatedReason === "max_matches" ? `showing first ${result.matches.length} of ${result.totalMatches}` : result.truncatedReason === "max_output_bytes" ? "output exceeded 1MB limit" : "search timed out";
|
|
198
|
+
lines.push(`[TRUNCATED] Results truncated (${reason})
|
|
199
|
+
`);
|
|
200
|
+
}
|
|
201
|
+
lines.push(`${prefix}${result.matches.length} replacement(s):
|
|
202
|
+
`);
|
|
203
|
+
for (const match of result.matches) {
|
|
204
|
+
const loc = `${match.file}:${match.range.start.line + 1}:${match.range.start.column + 1}`;
|
|
205
|
+
lines.push(`${loc}`);
|
|
206
|
+
lines.push(` ${match.text}`);
|
|
207
|
+
lines.push("");
|
|
208
|
+
}
|
|
209
|
+
if (isDryRun) {
|
|
210
|
+
lines.push("Use dryRun=false to apply changes");
|
|
211
|
+
}
|
|
212
|
+
return lines.join(`
|
|
213
|
+
`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/bun-spawn-shim.ts
|
|
217
|
+
import { spawn as nodeSpawn, spawnSync as nodeSpawnSync } from "node:child_process";
|
|
218
|
+
import { Writable } from "node:stream";
|
|
219
|
+
var runtime = globalThis;
|
|
220
|
+
var IS_BUN = typeof runtime.Bun !== "undefined";
|
|
221
|
+
function emptyReadableStream() {
|
|
222
|
+
return new ReadableStream({
|
|
223
|
+
start(controller) {
|
|
224
|
+
controller.close();
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
function toReadableStream(stream) {
|
|
229
|
+
if (!stream)
|
|
230
|
+
return emptyReadableStream();
|
|
231
|
+
return new ReadableStream({
|
|
232
|
+
async start(controller) {
|
|
233
|
+
try {
|
|
234
|
+
for await (const chunk of stream) {
|
|
235
|
+
controller.enqueue(toUint8Array(chunk));
|
|
236
|
+
}
|
|
237
|
+
controller.close();
|
|
238
|
+
} catch (error) {
|
|
239
|
+
controller.error(error);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
function toUint8Array(chunk) {
|
|
245
|
+
if (chunk instanceof Uint8Array)
|
|
246
|
+
return new Uint8Array(chunk);
|
|
247
|
+
return new TextEncoder().encode(String(chunk));
|
|
248
|
+
}
|
|
249
|
+
function emptyWritableStream() {
|
|
250
|
+
return new Writable({
|
|
251
|
+
write(_chunk, _encoding, callback) {
|
|
252
|
+
callback();
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
function isOptionsWithCommand(value) {
|
|
257
|
+
return typeof value === "object" && value !== null && "cmd" in value && Array.isArray(value.cmd);
|
|
258
|
+
}
|
|
259
|
+
function resolveCommand(cmdOrOpts, optsArg) {
|
|
260
|
+
if (isOptionsWithCommand(cmdOrOpts))
|
|
261
|
+
return { cmd: cmdOrOpts.cmd, opts: cmdOrOpts };
|
|
262
|
+
return { cmd: cmdOrOpts, opts: optsArg ?? {} };
|
|
263
|
+
}
|
|
264
|
+
function resolveStdio(options) {
|
|
265
|
+
if (options.stdio)
|
|
266
|
+
return options.stdio;
|
|
267
|
+
return [options.stdin ?? "ignore", options.stdout ?? "pipe", options.stderr ?? "inherit"];
|
|
268
|
+
}
|
|
269
|
+
function wrapNodeProcess(proc) {
|
|
270
|
+
let exitCode = null;
|
|
271
|
+
const exited = new Promise((resolve, reject) => {
|
|
272
|
+
proc.on("exit", (code) => {
|
|
273
|
+
exitCode = code ?? 1;
|
|
274
|
+
resolve(exitCode);
|
|
275
|
+
});
|
|
276
|
+
proc.on("error", (error) => {
|
|
277
|
+
if (exitCode === null) {
|
|
278
|
+
exitCode = 1;
|
|
279
|
+
reject(error);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
return {
|
|
284
|
+
get exitCode() {
|
|
285
|
+
return exitCode;
|
|
286
|
+
},
|
|
287
|
+
exited,
|
|
288
|
+
stdout: toReadableStream(proc.stdout),
|
|
289
|
+
stderr: toReadableStream(proc.stderr),
|
|
290
|
+
stdin: proc.stdin ?? emptyWritableStream(),
|
|
291
|
+
pid: proc.pid,
|
|
292
|
+
kill(signal) {
|
|
293
|
+
if (proc.killed || exitCode !== null)
|
|
294
|
+
return;
|
|
295
|
+
proc.kill(signal);
|
|
296
|
+
},
|
|
297
|
+
ref() {
|
|
298
|
+
proc.ref();
|
|
299
|
+
},
|
|
300
|
+
unref() {
|
|
301
|
+
proc.unref();
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function wrapBunProcess(proc) {
|
|
306
|
+
return {
|
|
307
|
+
...proc,
|
|
308
|
+
stdout: proc.stdout ?? emptyReadableStream(),
|
|
309
|
+
stderr: proc.stderr ?? emptyReadableStream()
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function spawn(cmdOrOpts, opts) {
|
|
313
|
+
const { cmd, opts: options } = resolveCommand(cmdOrOpts, opts);
|
|
314
|
+
if (IS_BUN)
|
|
315
|
+
return wrapBunProcess(runtime.Bun.spawn(cmd, options));
|
|
316
|
+
const [bin, ...args] = cmd;
|
|
317
|
+
if (!bin)
|
|
318
|
+
throw new Error("spawn requires a command");
|
|
319
|
+
return wrapNodeProcess(nodeSpawn(bin, args, {
|
|
320
|
+
cwd: options.cwd,
|
|
321
|
+
env: options.env,
|
|
322
|
+
stdio: resolveStdio(options),
|
|
323
|
+
detached: options.detached,
|
|
324
|
+
signal: options.signal
|
|
325
|
+
}));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// src/runner.ts
|
|
329
|
+
import { existsSync as existsSync3 } from "fs";
|
|
330
|
+
|
|
331
|
+
// src/cli-binary-path-resolution.ts
|
|
332
|
+
import { existsSync as existsSync2 } from "fs";
|
|
333
|
+
var resolvedCliPath2 = null;
|
|
334
|
+
var initPromise = null;
|
|
335
|
+
async function getAstGrepPath() {
|
|
336
|
+
if (resolvedCliPath2 !== null && existsSync2(resolvedCliPath2)) {
|
|
337
|
+
return resolvedCliPath2;
|
|
338
|
+
}
|
|
339
|
+
if (initPromise) {
|
|
340
|
+
return initPromise;
|
|
341
|
+
}
|
|
342
|
+
initPromise = (async () => {
|
|
343
|
+
const syncPath = findSgCliPathSync();
|
|
344
|
+
if (syncPath && existsSync2(syncPath)) {
|
|
345
|
+
resolvedCliPath2 = syncPath;
|
|
346
|
+
setSgCliPath(syncPath);
|
|
347
|
+
return syncPath;
|
|
348
|
+
}
|
|
349
|
+
return null;
|
|
350
|
+
})();
|
|
351
|
+
return initPromise;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// src/process-output-timeout.ts
|
|
355
|
+
async function collectProcessOutputWithTimeout(process2, timeoutMs) {
|
|
356
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
357
|
+
const timeoutId = setTimeout(() => {
|
|
358
|
+
process2.kill();
|
|
359
|
+
reject(new Error(`Search timeout after ${timeoutMs}ms`));
|
|
360
|
+
}, timeoutMs);
|
|
361
|
+
process2.exited.then(() => clearTimeout(timeoutId));
|
|
362
|
+
});
|
|
363
|
+
const stdoutPromise = process2.stdout ? new Response(process2.stdout).text() : Promise.resolve("");
|
|
364
|
+
const stderrPromise = process2.stderr ? new Response(process2.stderr).text() : Promise.resolve("");
|
|
365
|
+
const stdout = await Promise.race([stdoutPromise, timeoutPromise]);
|
|
366
|
+
const stderr = await stderrPromise;
|
|
367
|
+
const exitCode = await process2.exited;
|
|
368
|
+
return { stdout, stderr, exitCode };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// src/sg-compact-json-output.ts
|
|
372
|
+
function createSgResultFromStdout(stdout) {
|
|
373
|
+
if (!stdout.trim()) {
|
|
374
|
+
return { matches: [], totalMatches: 0, truncated: false };
|
|
375
|
+
}
|
|
376
|
+
const outputTruncated = stdout.length >= DEFAULT_MAX_OUTPUT_BYTES;
|
|
377
|
+
const outputToProcess = outputTruncated ? stdout.substring(0, DEFAULT_MAX_OUTPUT_BYTES) : stdout;
|
|
378
|
+
let matches = [];
|
|
379
|
+
try {
|
|
380
|
+
matches = JSON.parse(outputToProcess);
|
|
381
|
+
} catch {
|
|
382
|
+
if (outputTruncated) {
|
|
383
|
+
try {
|
|
384
|
+
const lastValidIndex = outputToProcess.lastIndexOf("}");
|
|
385
|
+
if (lastValidIndex > 0) {
|
|
386
|
+
const bracketIndex = outputToProcess.lastIndexOf("},", lastValidIndex);
|
|
387
|
+
if (bracketIndex > 0) {
|
|
388
|
+
const truncatedJson = outputToProcess.substring(0, bracketIndex + 1) + "]";
|
|
389
|
+
matches = JSON.parse(truncatedJson);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
} catch {
|
|
393
|
+
return {
|
|
394
|
+
matches: [],
|
|
395
|
+
totalMatches: 0,
|
|
396
|
+
truncated: true,
|
|
397
|
+
truncatedReason: "max_output_bytes",
|
|
398
|
+
error: "Output too large and could not be parsed"
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
} else {
|
|
402
|
+
return { matches: [], totalMatches: 0, truncated: false };
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const totalMatches = matches.length;
|
|
406
|
+
const matchesTruncated = totalMatches > DEFAULT_MAX_MATCHES;
|
|
407
|
+
const finalMatches = matchesTruncated ? matches.slice(0, DEFAULT_MAX_MATCHES) : matches;
|
|
408
|
+
return {
|
|
409
|
+
matches: finalMatches,
|
|
410
|
+
totalMatches,
|
|
411
|
+
truncated: outputTruncated || matchesTruncated,
|
|
412
|
+
truncatedReason: outputTruncated ? "max_output_bytes" : matchesTruncated ? "max_matches" : undefined
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// src/runner.ts
|
|
417
|
+
async function runSg(options) {
|
|
418
|
+
const shouldSeparateWritePass = !!(options.rewrite && options.updateAll);
|
|
419
|
+
const args = createSgArgs(options, { includeJson: true, includeUpdateAll: false });
|
|
420
|
+
let cliPath = getSgCliPath();
|
|
421
|
+
if (!cliPath || !existsSync3(cliPath)) {
|
|
422
|
+
const resolvedPath = await getAstGrepPath();
|
|
423
|
+
if (resolvedPath) {
|
|
424
|
+
cliPath = resolvedPath;
|
|
425
|
+
} else {
|
|
426
|
+
return {
|
|
427
|
+
matches: [],
|
|
428
|
+
totalMatches: 0,
|
|
429
|
+
truncated: false,
|
|
430
|
+
error: `ast-grep (sg) binary not found.
|
|
431
|
+
|
|
432
|
+
` + `Install options:
|
|
433
|
+
` + ` bun add -D @ast-grep/cli
|
|
434
|
+
` + ` cargo install ast-grep --locked
|
|
435
|
+
` + ` brew install ast-grep`
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
const timeout = DEFAULT_TIMEOUT_MS;
|
|
440
|
+
const proc = spawn([cliPath, ...args], {
|
|
441
|
+
cwd: options.cwd,
|
|
442
|
+
stdout: "pipe",
|
|
443
|
+
stderr: "pipe"
|
|
444
|
+
});
|
|
445
|
+
let stdout;
|
|
446
|
+
let stderr;
|
|
447
|
+
let exitCode;
|
|
448
|
+
try {
|
|
449
|
+
const output = await collectProcessOutputWithTimeout(proc, timeout);
|
|
450
|
+
stdout = output.stdout;
|
|
451
|
+
stderr = output.stderr;
|
|
452
|
+
exitCode = output.exitCode;
|
|
453
|
+
} catch (error) {
|
|
454
|
+
if (error instanceof Error && error.message.includes("timeout")) {
|
|
455
|
+
return {
|
|
456
|
+
matches: [],
|
|
457
|
+
totalMatches: 0,
|
|
458
|
+
truncated: true,
|
|
459
|
+
truncatedReason: "timeout",
|
|
460
|
+
error: error.message
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
464
|
+
const errorCode = errorCodeFrom(error);
|
|
465
|
+
const isNoEntry = errorCode === "ENOENT" || errorMessage.includes("ENOENT") || errorMessage.includes("not found");
|
|
466
|
+
if (isNoEntry) {
|
|
467
|
+
return {
|
|
468
|
+
matches: [],
|
|
469
|
+
totalMatches: 0,
|
|
470
|
+
truncated: false,
|
|
471
|
+
error: `ast-grep CLI binary not found.
|
|
472
|
+
|
|
473
|
+
` + `Install options:
|
|
474
|
+
` + ` bun add -D @ast-grep/cli
|
|
475
|
+
` + ` cargo install ast-grep --locked
|
|
476
|
+
` + ` brew install ast-grep`
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
return {
|
|
480
|
+
matches: [],
|
|
481
|
+
totalMatches: 0,
|
|
482
|
+
truncated: false,
|
|
483
|
+
error: `Failed to spawn ast-grep: ${errorMessage}`
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
if (exitCode !== 0 && stdout.trim() === "") {
|
|
487
|
+
if (stderr.includes("No files found")) {
|
|
488
|
+
return { matches: [], totalMatches: 0, truncated: false };
|
|
489
|
+
}
|
|
490
|
+
if (stderr.trim()) {
|
|
491
|
+
return { matches: [], totalMatches: 0, truncated: false, error: stderr.trim() };
|
|
492
|
+
}
|
|
493
|
+
return { matches: [], totalMatches: 0, truncated: false };
|
|
494
|
+
}
|
|
495
|
+
const jsonResult = createSgResultFromStdout(stdout);
|
|
496
|
+
if (shouldSeparateWritePass && jsonResult.matches.length > 0) {
|
|
497
|
+
const writeArgs = createSgArgs(options, { includeJson: false, includeUpdateAll: true });
|
|
498
|
+
const writeProc = spawn([cliPath, ...writeArgs], {
|
|
499
|
+
cwd: options.cwd,
|
|
500
|
+
stdout: "pipe",
|
|
501
|
+
stderr: "pipe"
|
|
502
|
+
});
|
|
503
|
+
try {
|
|
504
|
+
const writeOutput = await collectProcessOutputWithTimeout(writeProc, timeout);
|
|
505
|
+
if (writeOutput.exitCode !== 0) {
|
|
506
|
+
const errorDetail = writeOutput.stderr.trim() || `ast-grep exited with code ${writeOutput.exitCode}`;
|
|
507
|
+
return { ...jsonResult, error: `Replace failed: ${errorDetail}` };
|
|
508
|
+
}
|
|
509
|
+
} catch (error) {
|
|
510
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
511
|
+
return { ...jsonResult, error: `Replace failed: ${errorMessage}` };
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return jsonResult;
|
|
515
|
+
}
|
|
516
|
+
function createSgArgs(options, flags) {
|
|
517
|
+
const args = ["run", "-p", options.pattern, "--lang", options.lang];
|
|
518
|
+
if (flags.includeJson) {
|
|
519
|
+
args.push("--json=compact");
|
|
520
|
+
}
|
|
521
|
+
if (options.rewrite) {
|
|
522
|
+
args.push("-r", options.rewrite);
|
|
523
|
+
if (flags.includeUpdateAll) {
|
|
524
|
+
args.push("--update-all");
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (options.context && options.context > 0) {
|
|
528
|
+
args.push("-C", String(options.context));
|
|
529
|
+
}
|
|
530
|
+
if (options.globs) {
|
|
531
|
+
for (const glob of options.globs) {
|
|
532
|
+
args.push("--globs", glob);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
const paths = options.paths && options.paths.length > 0 ? options.paths : ["."];
|
|
536
|
+
args.push("--", ...paths);
|
|
537
|
+
return args;
|
|
538
|
+
}
|
|
539
|
+
function errorCodeFrom(error) {
|
|
540
|
+
if (typeof error !== "object" || error === null || !("code" in error))
|
|
541
|
+
return;
|
|
542
|
+
return Reflect.get(error, "code");
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// src/tool-descriptions.ts
|
|
546
|
+
var AST_GREP_SEARCH_DESCRIPTION = [
|
|
547
|
+
"Search code by AST structure (25 languages). This is NOT regex.",
|
|
548
|
+
"",
|
|
549
|
+
"Meta-variables (the only wildcards ast-grep understands):",
|
|
550
|
+
" $VAR - one AST node (an identifier, expression, statement, ...)",
|
|
551
|
+
" $$$ - zero or more nodes (argument lists, function bodies, ...)",
|
|
552
|
+
" $$$VAR - same, captured by name",
|
|
553
|
+
"Patterns must be complete, parseable source code. Each meta-variable replaces a whole node, not a substring.",
|
|
554
|
+
"",
|
|
555
|
+
"Regex syntax does NOT work - never pass these to pattern:",
|
|
556
|
+
' "foo|bar" alternation → run separate calls, or switch to grep',
|
|
557
|
+
' ".*", ".+" wildcards → use $$$ between AST fragments',
|
|
558
|
+
' "\\w", "\\d" escapes → use $VAR to capture any identifier',
|
|
559
|
+
' "[a-z]" class ranges → no AST equivalent',
|
|
560
|
+
"For text search, cross-language search, or regex features, use the grep tool instead.",
|
|
561
|
+
"",
|
|
562
|
+
"Examples by language:",
|
|
563
|
+
` typescript/tsx "function $NAME($$$) { $$$ }", "console.log($$$)", "import { $$$ } from '$MOD'"`,
|
|
564
|
+
' python "def $FUNC($$$)", "class $C($$$)" - no trailing colon',
|
|
565
|
+
' go "func $NAME($$$) { $$$ }", "if err != nil { $$$ }"',
|
|
566
|
+
' rust "fn $NAME($$$) -> $RET { $$$ }", "impl $TRAIT for $T { $$$ }"',
|
|
567
|
+
"",
|
|
568
|
+
"On empty results the tool returns a hint naming the exact mistake. If the pattern is fundamentally text-shaped, stop retrying and switch to grep."
|
|
569
|
+
].join(`
|
|
570
|
+
`);
|
|
571
|
+
var AST_GREP_SEARCH_PATTERN_PARAM = "AST pattern - valid, parseable code using $VAR (one node) and $$$ (many nodes). NOT regex: no `|`, no `.*`, no `\\w`, no `[a-z]`. For text or alternation, use grep instead.";
|
|
572
|
+
var AST_GREP_REPLACE_DESCRIPTION = [
|
|
573
|
+
"Rewrite code by AST pattern (25 languages). Dry-run by default.",
|
|
574
|
+
"Both pattern and rewrite use AST syntax ($VAR for one node, $$$ for many) - regex does NOT work.",
|
|
575
|
+
"Meta-variables captured in pattern can be reused in rewrite to preserve matched content.",
|
|
576
|
+
'Example: pattern="console.log($MSG)" rewrite="logger.info($MSG)"',
|
|
577
|
+
"For text-only replacement or regex features, use a text editor instead."
|
|
578
|
+
].join(`
|
|
579
|
+
`);
|
|
580
|
+
|
|
581
|
+
// src/workspace-paths.ts
|
|
582
|
+
import { existsSync as existsSync4, realpathSync } from "node:fs";
|
|
583
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
584
|
+
function normalizeWorkspaceDirectory(workspaceDirectory) {
|
|
585
|
+
return realpathSync(resolve(workspaceDirectory));
|
|
586
|
+
}
|
|
587
|
+
function resolveWorkspacePaths(rawPaths, workspaceDirectory) {
|
|
588
|
+
const workspace = normalizeWorkspaceDirectory(workspaceDirectory);
|
|
589
|
+
const requestedPaths = rawPaths && rawPaths.length > 0 ? rawPaths : ["."];
|
|
590
|
+
return requestedPaths.map((rawPath) => resolveWorkspacePath(rawPath, workspace));
|
|
591
|
+
}
|
|
592
|
+
function resolveWorkspacePath(rawPath, workspaceDirectory) {
|
|
593
|
+
if (rawPath.length === 0)
|
|
594
|
+
throw new Error("paths entries must be non-empty strings");
|
|
595
|
+
if (rawPath.startsWith("-"))
|
|
596
|
+
throw new Error(`paths entries must not start with '-': ${rawPath}`);
|
|
597
|
+
if (rawPath.includes("\x00"))
|
|
598
|
+
throw new Error("paths entries must not contain null bytes");
|
|
599
|
+
if (isAbsolute(rawPath))
|
|
600
|
+
return resolveAbsoluteWorkspacePath(rawPath, workspaceDirectory);
|
|
601
|
+
const absolutePath = resolve(workspaceDirectory, rawPath);
|
|
602
|
+
assertInsideWorkspace(absolutePath, workspaceDirectory, rawPath);
|
|
603
|
+
if (existsSync4(absolutePath)) {
|
|
604
|
+
const realPath = realpathSync(absolutePath);
|
|
605
|
+
assertInsideWorkspace(realPath, workspaceDirectory, rawPath);
|
|
606
|
+
}
|
|
607
|
+
const normalizedPath = relative(workspaceDirectory, absolutePath);
|
|
608
|
+
return normalizedPath === "" ? "." : normalizedPath;
|
|
609
|
+
}
|
|
610
|
+
function resolveAbsoluteWorkspacePath(rawPath, workspaceDirectory) {
|
|
611
|
+
let realPath;
|
|
612
|
+
try {
|
|
613
|
+
realPath = realpathSync(rawPath);
|
|
614
|
+
} catch {
|
|
615
|
+
throw new Error(`absolute path entry does not exist: ${rawPath}`);
|
|
616
|
+
}
|
|
617
|
+
assertInsideWorkspace(realPath, workspaceDirectory, rawPath);
|
|
618
|
+
const normalizedPath = relative(workspaceDirectory, realPath);
|
|
619
|
+
return normalizedPath === "" ? "." : normalizedPath;
|
|
620
|
+
}
|
|
621
|
+
function assertInsideWorkspace(candidatePath, workspaceDirectory, rawPath) {
|
|
622
|
+
const workspaceRelativePath = relative(workspaceDirectory, candidatePath);
|
|
623
|
+
if (workspaceRelativePath === "" || !workspaceRelativePath.startsWith("..") && !isAbsolute(workspaceRelativePath))
|
|
624
|
+
return;
|
|
625
|
+
throw new Error(`paths entries must stay inside the workspace: ${rawPath}`);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// src/mcp.ts
|
|
629
|
+
var SERVER_NAME = "ast_grep";
|
|
630
|
+
var SERVER_VERSION = "0.1.0";
|
|
631
|
+
var LANGUAGE_VALUES = CLI_LANGUAGES;
|
|
632
|
+
var DISABLED_TOOLS_ENV = "OMO_AST_GREP_DISABLED_TOOLS";
|
|
633
|
+
var AST_GREP_MCP_TOOLS = [
|
|
634
|
+
{
|
|
635
|
+
name: "search",
|
|
636
|
+
title: "AST grep search",
|
|
637
|
+
description: AST_GREP_SEARCH_DESCRIPTION,
|
|
638
|
+
inputSchema: {
|
|
639
|
+
type: "object",
|
|
640
|
+
properties: {
|
|
641
|
+
pattern: { type: "string", description: AST_GREP_SEARCH_PATTERN_PARAM },
|
|
642
|
+
lang: { type: "string", enum: CLI_LANGUAGES, description: "Target language" },
|
|
643
|
+
paths: { type: "array", items: { type: "string" }, description: "Paths to search" },
|
|
644
|
+
globs: { type: "array", items: { type: "string" }, description: "Include/exclude globs" },
|
|
645
|
+
context: { type: "number", description: "Context lines around each match" }
|
|
646
|
+
},
|
|
647
|
+
required: ["pattern", "lang"],
|
|
648
|
+
additionalProperties: false
|
|
649
|
+
}
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
name: "replace",
|
|
653
|
+
title: "AST grep replace",
|
|
654
|
+
description: AST_GREP_REPLACE_DESCRIPTION,
|
|
655
|
+
inputSchema: {
|
|
656
|
+
type: "object",
|
|
657
|
+
properties: {
|
|
658
|
+
pattern: { type: "string", description: "AST pattern to match" },
|
|
659
|
+
rewrite: { type: "string", description: "Replacement pattern" },
|
|
660
|
+
lang: { type: "string", enum: CLI_LANGUAGES, description: "Target language" },
|
|
661
|
+
paths: { type: "array", items: { type: "string" }, description: "Paths to search" },
|
|
662
|
+
globs: { type: "array", items: { type: "string" }, description: "Include/exclude globs" },
|
|
663
|
+
dryRun: { type: "boolean", description: "Preview changes without applying. Defaults to true." }
|
|
664
|
+
},
|
|
665
|
+
required: ["pattern", "rewrite", "lang"],
|
|
666
|
+
additionalProperties: false
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
];
|
|
670
|
+
async function handleAstGrepMcpRequest(input, options = {}) {
|
|
671
|
+
if (!isRecord(input))
|
|
672
|
+
return errorResponse(null, -32600, "Invalid Request");
|
|
673
|
+
const id = jsonRpcId(input.id);
|
|
674
|
+
if (input.method === "notifications/initialized")
|
|
675
|
+
return;
|
|
676
|
+
if (input.method === "ping")
|
|
677
|
+
return successResponse(id, {});
|
|
678
|
+
if (input.method === "initialize") {
|
|
679
|
+
return successResponse(id, {
|
|
680
|
+
capabilities: { tools: { listChanged: false } },
|
|
681
|
+
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
|
|
682
|
+
protocolVersion: requestedProtocolVersion(input.params)
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
if (input.method === "tools/list")
|
|
686
|
+
return successResponse(id, { tools: enabledTools(options) });
|
|
687
|
+
if (input.method === "tools/call")
|
|
688
|
+
return handleToolCall(id, input.params, options);
|
|
689
|
+
return errorResponse(id, -32601, `Method not found: ${String(input.method)}`);
|
|
690
|
+
}
|
|
691
|
+
async function runMcpStdioServer(input = process.stdin, output = process.stdout, options = {}) {
|
|
692
|
+
const lines = createInterface({ input, crlfDelay: Number.POSITIVE_INFINITY });
|
|
693
|
+
for await (const line of lines) {
|
|
694
|
+
if (!line.trim())
|
|
695
|
+
continue;
|
|
696
|
+
let parsed;
|
|
697
|
+
try {
|
|
698
|
+
parsed = JSON.parse(line);
|
|
699
|
+
} catch (error) {
|
|
700
|
+
output.write(`${JSON.stringify(errorResponse(null, -32700, "Parse error", messageFromError(error)))}
|
|
701
|
+
`);
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
const response = await handleAstGrepMcpRequest(parsed, options);
|
|
705
|
+
if (response)
|
|
706
|
+
output.write(`${JSON.stringify(response)}
|
|
707
|
+
`);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
async function handleToolCall(id, params, options) {
|
|
711
|
+
if (!isRecord(params) || typeof params.name !== "string")
|
|
712
|
+
return errorResponse(id, -32602, "tools/call requires params.name");
|
|
713
|
+
try {
|
|
714
|
+
const result = await executeAstGrepTool(params.name, params.arguments, options);
|
|
715
|
+
return successResponse(id, { content: result.content, isError: result.isError ?? false });
|
|
716
|
+
} catch (error) {
|
|
717
|
+
return successResponse(id, { content: [{ type: "text", text: messageFromError(error) }], isError: true });
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
async function executeAstGrepTool(name, args, options) {
|
|
721
|
+
if (disabledToolNames(options).has(name))
|
|
722
|
+
throw new Error(`ast-grep tool is disabled: ${name}`);
|
|
723
|
+
const runner = options.runSg ?? runSg;
|
|
724
|
+
const workspaceDirectory = normalizeWorkspaceDirectory(options.workspaceDirectory ?? process.env.OMO_AST_GREP_WORKSPACE ?? process.cwd());
|
|
725
|
+
if (name === "search") {
|
|
726
|
+
const input = parseSearchArgs(args, workspaceDirectory);
|
|
727
|
+
const result = await runner(input);
|
|
728
|
+
let output = formatSearchResult(result);
|
|
729
|
+
if (result.matches.length === 0 && !result.error) {
|
|
730
|
+
const hint = getPatternHint(input.pattern, input.lang);
|
|
731
|
+
if (hint)
|
|
732
|
+
output += `
|
|
733
|
+
|
|
734
|
+
${hint}`;
|
|
735
|
+
}
|
|
736
|
+
return { content: [{ type: "text", text: output }], isError: Boolean(result.error) };
|
|
737
|
+
}
|
|
738
|
+
if (name === "replace") {
|
|
739
|
+
const input = parseReplaceArgs(args, workspaceDirectory);
|
|
740
|
+
const result = await runner(input.options);
|
|
741
|
+
return { content: [{ type: "text", text: formatReplaceResult(result, input.dryRun) }], isError: Boolean(result.error) };
|
|
742
|
+
}
|
|
743
|
+
throw new Error(`Unknown ast-grep tool: ${name}`);
|
|
744
|
+
}
|
|
745
|
+
function parseSearchArgs(args, workspaceDirectory) {
|
|
746
|
+
const input = requireRecord(args);
|
|
747
|
+
return {
|
|
748
|
+
pattern: requireString(input, "pattern"),
|
|
749
|
+
lang: requireLanguage(input, "lang"),
|
|
750
|
+
cwd: workspaceDirectory,
|
|
751
|
+
paths: resolveWorkspacePaths(optionalStringArray(input, "paths"), workspaceDirectory),
|
|
752
|
+
globs: optionalStringArray(input, "globs"),
|
|
753
|
+
context: optionalNumber(input, "context")
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
function parseReplaceArgs(args, workspaceDirectory) {
|
|
757
|
+
const input = requireRecord(args);
|
|
758
|
+
const dryRun = optionalBoolean(input, "dryRun") ?? true;
|
|
759
|
+
return {
|
|
760
|
+
dryRun,
|
|
761
|
+
options: {
|
|
762
|
+
pattern: requireString(input, "pattern"),
|
|
763
|
+
rewrite: requireString(input, "rewrite"),
|
|
764
|
+
lang: requireLanguage(input, "lang"),
|
|
765
|
+
cwd: workspaceDirectory,
|
|
766
|
+
paths: resolveWorkspacePaths(optionalStringArray(input, "paths"), workspaceDirectory),
|
|
767
|
+
globs: optionalStringArray(input, "globs"),
|
|
768
|
+
updateAll: !dryRun
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
function requireRecord(value) {
|
|
773
|
+
if (!isRecord(value))
|
|
774
|
+
throw new Error("Tool arguments must be an object");
|
|
775
|
+
return value;
|
|
776
|
+
}
|
|
777
|
+
function requireString(input, key) {
|
|
778
|
+
const value = input[key];
|
|
779
|
+
if (typeof value !== "string" || value.length === 0)
|
|
780
|
+
throw new Error(`${key} must be a non-empty string`);
|
|
781
|
+
return value;
|
|
782
|
+
}
|
|
783
|
+
function requireLanguage(input, key) {
|
|
784
|
+
const value = requireString(input, key);
|
|
785
|
+
if (!isCliLanguage(value))
|
|
786
|
+
throw new Error(`${key} must be one of: ${LANGUAGE_VALUES.join(", ")}`);
|
|
787
|
+
return value;
|
|
788
|
+
}
|
|
789
|
+
function isCliLanguage(value) {
|
|
790
|
+
return LANGUAGE_VALUES.includes(value);
|
|
791
|
+
}
|
|
792
|
+
function optionalStringArray(input, key) {
|
|
793
|
+
const value = input[key];
|
|
794
|
+
if (value === undefined)
|
|
795
|
+
return;
|
|
796
|
+
if (!Array.isArray(value) || !value.every((item) => typeof item === "string"))
|
|
797
|
+
throw new Error(`${key} must be an array of strings`);
|
|
798
|
+
return value;
|
|
799
|
+
}
|
|
800
|
+
function enabledTools(options) {
|
|
801
|
+
const disabled = disabledToolNames(options);
|
|
802
|
+
return AST_GREP_MCP_TOOLS.filter((tool) => !disabled.has(tool.name));
|
|
803
|
+
}
|
|
804
|
+
function disabledToolNames(options) {
|
|
805
|
+
const fromOptions = options.disabledTools ?? [];
|
|
806
|
+
const fromEnv = process.env[DISABLED_TOOLS_ENV]?.split(",") ?? [];
|
|
807
|
+
return new Set([...fromOptions, ...fromEnv].map((tool) => tool.trim()).filter(Boolean));
|
|
808
|
+
}
|
|
809
|
+
function optionalNumber(input, key) {
|
|
810
|
+
const value = input[key];
|
|
811
|
+
if (value === undefined)
|
|
812
|
+
return;
|
|
813
|
+
if (typeof value !== "number")
|
|
814
|
+
throw new Error(`${key} must be a number`);
|
|
815
|
+
return value;
|
|
816
|
+
}
|
|
817
|
+
function optionalBoolean(input, key) {
|
|
818
|
+
const value = input[key];
|
|
819
|
+
if (value === undefined)
|
|
820
|
+
return;
|
|
821
|
+
if (typeof value !== "boolean")
|
|
822
|
+
throw new Error(`${key} must be a boolean`);
|
|
823
|
+
return value;
|
|
824
|
+
}
|
|
825
|
+
function successResponse(id, result) {
|
|
826
|
+
return { jsonrpc: "2.0", id, result };
|
|
827
|
+
}
|
|
828
|
+
function errorResponse(id, code, message, data) {
|
|
829
|
+
return { jsonrpc: "2.0", id, error: data === undefined ? { code, message } : { code, message, data } };
|
|
830
|
+
}
|
|
831
|
+
function requestedProtocolVersion(params) {
|
|
832
|
+
if (!isRecord(params) || typeof params.protocolVersion !== "string")
|
|
833
|
+
return "2024-11-05";
|
|
834
|
+
return params.protocolVersion;
|
|
835
|
+
}
|
|
836
|
+
function jsonRpcId(value) {
|
|
837
|
+
return typeof value === "string" || typeof value === "number" || value === null ? value : null;
|
|
838
|
+
}
|
|
839
|
+
function isRecord(value) {
|
|
840
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
841
|
+
}
|
|
842
|
+
function messageFromError(error) {
|
|
843
|
+
return error instanceof Error ? error.message : String(error);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// src/cli.ts
|
|
847
|
+
async function main() {
|
|
848
|
+
const [command = "mcp"] = argv.slice(2);
|
|
849
|
+
if (command === "mcp") {
|
|
850
|
+
await runMcpStdioServer();
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
stderr.write(`Usage: ast-grep-mcp [mcp]
|
|
854
|
+
`);
|
|
855
|
+
process.exitCode = 2;
|
|
856
|
+
}
|
|
857
|
+
main().catch((error) => {
|
|
858
|
+
stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}
|
|
859
|
+
`);
|
|
860
|
+
process.exitCode = 1;
|
|
861
|
+
});
|