@freesyntax/notch-cli 0.5.16 → 0.5.19

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.
@@ -0,0 +1,174 @@
1
+ // src/tools/github.ts
2
+ import { z } from "zod";
3
+ var GITHUB_API = "https://api.github.com";
4
+ function getToken() {
5
+ return process.env.GITHUB_TOKEN || process.env.GH_TOKEN || null;
6
+ }
7
+ async function ghFetch(path, opts = {}) {
8
+ const token = getToken();
9
+ const headers = {
10
+ "Accept": "application/vnd.github+json",
11
+ "X-GitHub-Api-Version": "2022-11-28",
12
+ ...opts.headers ?? {}
13
+ };
14
+ if (token) headers["Authorization"] = `Bearer ${token}`;
15
+ return fetch(`${GITHUB_API}${path}`, { ...opts, headers });
16
+ }
17
+ var parameters = z.object({
18
+ action: z.enum([
19
+ "create_pr",
20
+ "list_prs",
21
+ "list_issues",
22
+ "repo_info",
23
+ "create_issue",
24
+ "pr_status"
25
+ ]).describe("The GitHub operation to perform"),
26
+ owner: z.string().describe("Repository owner (user or org)"),
27
+ repo: z.string().describe("Repository name"),
28
+ // PR fields
29
+ title: z.string().optional().describe("PR or issue title"),
30
+ body: z.string().optional().describe("PR or issue body/description"),
31
+ head: z.string().optional().describe("PR source branch"),
32
+ base: z.string().optional().describe("PR target branch (default: main)"),
33
+ // Issue fields
34
+ labels: z.array(z.string()).optional().describe("Labels for issue"),
35
+ // Filter
36
+ state: z.enum(["open", "closed", "all"]).optional().describe("Filter by state"),
37
+ pr_number: z.number().optional().describe("PR number for status check")
38
+ });
39
+ async function execute(args, _ctx) {
40
+ const token = getToken();
41
+ if (!token) {
42
+ return {
43
+ content: "No GitHub token found. Set GITHUB_TOKEN or GH_TOKEN environment variable.\nYou can create a token at https://github.com/settings/tokens",
44
+ isError: true
45
+ };
46
+ }
47
+ try {
48
+ switch (args.action) {
49
+ case "repo_info": {
50
+ const res = await ghFetch(`/repos/${args.owner}/${args.repo}`);
51
+ if (!res.ok) return { content: `GitHub API error: ${res.status} ${await res.text()}`, isError: true };
52
+ const repo = await res.json();
53
+ return {
54
+ content: [
55
+ `Repository: ${repo.full_name}`,
56
+ `Description: ${repo.description || "None"}`,
57
+ `Stars: ${repo.stargazers_count} | Forks: ${repo.forks_count} | Open issues: ${repo.open_issues_count}`,
58
+ `Default branch: ${repo.default_branch}`,
59
+ `Language: ${repo.language || "Unknown"}`,
60
+ `Private: ${repo.private}`,
61
+ `URL: ${repo.html_url}`
62
+ ].join("\n")
63
+ };
64
+ }
65
+ case "list_prs": {
66
+ const state = args.state ?? "open";
67
+ const res = await ghFetch(`/repos/${args.owner}/${args.repo}/pulls?state=${state}&per_page=15`);
68
+ if (!res.ok) return { content: `GitHub API error: ${res.status}`, isError: true };
69
+ const prs = await res.json();
70
+ if (prs.length === 0) return { content: `No ${state} pull requests.` };
71
+ const lines = prs.map(
72
+ (pr) => `#${pr.number} [${pr.state}] ${pr.title} (${pr.head.ref} \u2192 ${pr.base.ref}) by @${pr.user.login}`
73
+ );
74
+ return { content: lines.join("\n") };
75
+ }
76
+ case "list_issues": {
77
+ const state = args.state ?? "open";
78
+ const res = await ghFetch(`/repos/${args.owner}/${args.repo}/issues?state=${state}&per_page=15`);
79
+ if (!res.ok) return { content: `GitHub API error: ${res.status}`, isError: true };
80
+ const issues = await res.json();
81
+ const realIssues = issues.filter((i) => !i.pull_request);
82
+ if (realIssues.length === 0) return { content: `No ${state} issues.` };
83
+ const lines = realIssues.map((i) => {
84
+ const labels = i.labels.map((l) => l.name).join(", ");
85
+ return `#${i.number} [${i.state}] ${i.title}${labels ? ` [${labels}]` : ""} by @${i.user.login}`;
86
+ });
87
+ return { content: lines.join("\n") };
88
+ }
89
+ case "create_pr": {
90
+ if (!args.title || !args.head) {
91
+ return { content: "title and head branch are required for create_pr", isError: true };
92
+ }
93
+ const res = await ghFetch(`/repos/${args.owner}/${args.repo}/pulls`, {
94
+ method: "POST",
95
+ body: JSON.stringify({
96
+ title: args.title,
97
+ body: args.body ?? "",
98
+ head: args.head,
99
+ base: args.base ?? "main"
100
+ })
101
+ });
102
+ if (!res.ok) {
103
+ const err = await res.json();
104
+ return { content: `Failed to create PR: ${err.message || res.status}`, isError: true };
105
+ }
106
+ const pr = await res.json();
107
+ return { content: `PR #${pr.number} created: ${pr.html_url}` };
108
+ }
109
+ case "create_issue": {
110
+ if (!args.title) {
111
+ return { content: "title is required for create_issue", isError: true };
112
+ }
113
+ const res = await ghFetch(`/repos/${args.owner}/${args.repo}/issues`, {
114
+ method: "POST",
115
+ body: JSON.stringify({
116
+ title: args.title,
117
+ body: args.body ?? "",
118
+ labels: args.labels ?? []
119
+ })
120
+ });
121
+ if (!res.ok) {
122
+ const err = await res.json();
123
+ return { content: `Failed to create issue: ${err.message || res.status}`, isError: true };
124
+ }
125
+ const issue = await res.json();
126
+ return { content: `Issue #${issue.number} created: ${issue.html_url}` };
127
+ }
128
+ case "pr_status": {
129
+ if (!args.pr_number) {
130
+ return { content: "pr_number is required for pr_status", isError: true };
131
+ }
132
+ const res = await ghFetch(`/repos/${args.owner}/${args.repo}/pulls/${args.pr_number}`);
133
+ if (!res.ok) return { content: `GitHub API error: ${res.status}`, isError: true };
134
+ const pr = await res.json();
135
+ const checksRes = await ghFetch(`/repos/${args.owner}/${args.repo}/commits/${pr.head.sha}/check-runs`);
136
+ let checksInfo = "";
137
+ if (checksRes.ok) {
138
+ const checks = await checksRes.json();
139
+ if (checks.check_runs?.length) {
140
+ checksInfo = "\nChecks:\n" + checks.check_runs.map(
141
+ (c) => ` ${c.status === "completed" ? c.conclusion === "success" ? "\u2713" : "\u2717" : "\u25CC"} ${c.name}: ${c.conclusion ?? c.status}`
142
+ ).join("\n");
143
+ }
144
+ }
145
+ return {
146
+ content: [
147
+ `PR #${pr.number}: ${pr.title}`,
148
+ `State: ${pr.state} | Mergeable: ${pr.mergeable ?? "checking..."}`,
149
+ `${pr.head.ref} \u2192 ${pr.base.ref}`,
150
+ `Author: @${pr.user.login}`,
151
+ `+${pr.additions} -${pr.deletions} (${pr.changed_files} files)`,
152
+ `Reviews: ${pr.requested_reviewers?.length ?? 0} requested`,
153
+ checksInfo,
154
+ `URL: ${pr.html_url}`
155
+ ].join("\n")
156
+ };
157
+ }
158
+ default:
159
+ return { content: `Unknown action: ${args.action}`, isError: true };
160
+ }
161
+ } catch (err) {
162
+ return { content: `GitHub error: ${err.message}`, isError: true };
163
+ }
164
+ }
165
+ var githubTool = {
166
+ name: "github",
167
+ description: "GitHub operations \u2014 create PRs, list issues, check PR status, view repo info. Requires GITHUB_TOKEN env var.",
168
+ parameters,
169
+ execute
170
+ };
171
+
172
+ export {
173
+ githubTool
174
+ };
@@ -0,0 +1,118 @@
1
+ // src/tools/notebook.ts
2
+ import fs from "fs/promises";
3
+ import path from "path";
4
+ import { z } from "zod";
5
+ var parameters = z.object({
6
+ action: z.enum(["read", "edit", "add", "remove"]).describe("Action: read (view cells), edit (modify cell source), add (insert new cell), remove (delete cell)"),
7
+ path: z.string().describe("Path to the .ipynb notebook file"),
8
+ cell_index: z.number().optional().describe("Cell index (0-based) \u2014 for edit/remove actions"),
9
+ cell_type: z.enum(["code", "markdown", "raw"]).optional().describe("Cell type \u2014 for add action"),
10
+ content: z.string().optional().describe("New cell content \u2014 for edit/add actions"),
11
+ insert_after: z.number().optional().describe("Insert after this cell index \u2014 for add action (-1 for beginning)")
12
+ });
13
+ function formatNotebook(nb, filePath) {
14
+ const parts = [
15
+ `# ${path.basename(filePath)}`,
16
+ `# ${nb.cells.length} cells | nbformat ${nb.nbformat}.${nb.nbformat_minor}`,
17
+ ""
18
+ ];
19
+ for (let i = 0; i < nb.cells.length; i++) {
20
+ const cell = nb.cells[i];
21
+ const source = cell.source.join("");
22
+ const header = cell.cell_type === "code" ? `[${i}] Code (exec: ${cell.execution_count ?? "-"})` : `[${i}] ${cell.cell_type.charAt(0).toUpperCase() + cell.cell_type.slice(1)}`;
23
+ parts.push(`--- ${header} ---`);
24
+ parts.push(source);
25
+ if (cell.cell_type === "code" && cell.outputs && cell.outputs.length > 0) {
26
+ parts.push(" Output:");
27
+ for (const output of cell.outputs) {
28
+ if (output.text) {
29
+ parts.push(" " + output.text.join("").slice(0, 500));
30
+ } else if (output.data?.["text/plain"]) {
31
+ parts.push(" " + output.data["text/plain"].join("").slice(0, 500));
32
+ } else if (output.ename) {
33
+ parts.push(` Error: ${output.ename}: ${output.evalue}`);
34
+ }
35
+ }
36
+ }
37
+ parts.push("");
38
+ }
39
+ return parts.join("\n");
40
+ }
41
+ var notebookTool = {
42
+ name: "notebook",
43
+ description: "Read and edit Jupyter .ipynb notebook files. Actions: read (view all cells with outputs), edit (modify a cell), add (insert a new cell), remove (delete a cell).",
44
+ parameters,
45
+ async execute(params, ctx) {
46
+ const filePath = path.isAbsolute(params.path) ? params.path : path.resolve(ctx.cwd, params.path);
47
+ try {
48
+ switch (params.action) {
49
+ case "read": {
50
+ const raw = await fs.readFile(filePath, "utf-8");
51
+ const nb = JSON.parse(raw);
52
+ return { content: formatNotebook(nb, filePath) };
53
+ }
54
+ case "edit": {
55
+ if (params.cell_index === void 0) {
56
+ return { content: "cell_index is required for edit action.", isError: true };
57
+ }
58
+ if (params.content === void 0) {
59
+ return { content: "content is required for edit action.", isError: true };
60
+ }
61
+ const raw = await fs.readFile(filePath, "utf-8");
62
+ const nb = JSON.parse(raw);
63
+ if (params.cell_index < 0 || params.cell_index >= nb.cells.length) {
64
+ return { content: `Cell index ${params.cell_index} out of range (0-${nb.cells.length - 1}).`, isError: true };
65
+ }
66
+ const lines = params.content.split("\n").map(
67
+ (l, i, arr) => i < arr.length - 1 ? l + "\n" : l
68
+ );
69
+ nb.cells[params.cell_index].source = lines;
70
+ await fs.writeFile(filePath, JSON.stringify(nb, null, 1) + "\n");
71
+ return { content: `Cell [${params.cell_index}] updated in ${path.basename(filePath)}.` };
72
+ }
73
+ case "add": {
74
+ if (params.content === void 0) {
75
+ return { content: "content is required for add action.", isError: true };
76
+ }
77
+ const raw = await fs.readFile(filePath, "utf-8");
78
+ const nb = JSON.parse(raw);
79
+ const cellType = params.cell_type ?? "code";
80
+ const lines = params.content.split("\n").map(
81
+ (l, i, arr) => i < arr.length - 1 ? l + "\n" : l
82
+ );
83
+ const newCell = {
84
+ cell_type: cellType,
85
+ source: lines,
86
+ metadata: {},
87
+ ...cellType === "code" ? { outputs: [], execution_count: null } : {}
88
+ };
89
+ const insertIdx = params.insert_after !== void 0 ? params.insert_after + 1 : nb.cells.length;
90
+ nb.cells.splice(insertIdx, 0, newCell);
91
+ await fs.writeFile(filePath, JSON.stringify(nb, null, 1) + "\n");
92
+ return { content: `Added ${cellType} cell at index [${insertIdx}] in ${path.basename(filePath)}.` };
93
+ }
94
+ case "remove": {
95
+ if (params.cell_index === void 0) {
96
+ return { content: "cell_index is required for remove action.", isError: true };
97
+ }
98
+ const raw = await fs.readFile(filePath, "utf-8");
99
+ const nb = JSON.parse(raw);
100
+ if (params.cell_index < 0 || params.cell_index >= nb.cells.length) {
101
+ return { content: `Cell index ${params.cell_index} out of range (0-${nb.cells.length - 1}).`, isError: true };
102
+ }
103
+ const removed = nb.cells.splice(params.cell_index, 1)[0];
104
+ await fs.writeFile(filePath, JSON.stringify(nb, null, 1) + "\n");
105
+ return { content: `Removed ${removed.cell_type} cell [${params.cell_index}] from ${path.basename(filePath)}. ${nb.cells.length} cells remaining.` };
106
+ }
107
+ default:
108
+ return { content: `Unknown action: ${params.action}`, isError: true };
109
+ }
110
+ } catch (err) {
111
+ return { content: `Notebook error: ${err.message}`, isError: true };
112
+ }
113
+ }
114
+ };
115
+
116
+ export {
117
+ notebookTool
118
+ };
@@ -0,0 +1,35 @@
1
+ // src/tools/write.ts
2
+ import fs from "fs/promises";
3
+ import path from "path";
4
+ import { z } from "zod";
5
+ var parameters = z.object({
6
+ path: z.string().describe("Relative or absolute path to write"),
7
+ content: z.string().describe("Complete file content to write")
8
+ });
9
+ var writeTool = {
10
+ name: "write",
11
+ description: "Write a file (create or overwrite). Creates parent directories if needed. Always provide the COMPLETE file content.",
12
+ parameters,
13
+ async execute(params, ctx) {
14
+ const filePath = path.isAbsolute(params.path) ? params.path : path.resolve(ctx.cwd, params.path);
15
+ try {
16
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
17
+ let existed = false;
18
+ try {
19
+ await fs.access(filePath);
20
+ existed = true;
21
+ } catch {
22
+ }
23
+ await fs.writeFile(filePath, params.content, "utf-8");
24
+ const lines = params.content.split("\n").length;
25
+ const action = existed ? "Updated" : "Created";
26
+ return { content: `${action} ${filePath} (${lines} lines)` };
27
+ } catch (err) {
28
+ return { content: `Error writing ${filePath}: ${err.message}`, isError: true };
29
+ }
30
+ }
31
+ };
32
+
33
+ export {
34
+ writeTool
35
+ };
@@ -0,0 +1,315 @@
1
+ // src/tools/lsp.ts
2
+ import { execFileSync } from "child_process";
3
+ import fs from "fs/promises";
4
+ import path from "path";
5
+ import { z } from "zod";
6
+ var parameters = z.object({
7
+ action: z.enum(["definition", "references", "diagnostics", "hover"]).describe("LSP action: definition (go-to-def), references (find usages), diagnostics (type errors), hover (type info)"),
8
+ file: z.string().describe("File path (relative or absolute)"),
9
+ line: z.number().optional().describe("Line number (1-based) \u2014 required for definition, references, hover"),
10
+ character: z.number().optional().describe("Column number (0-based) \u2014 required for definition, references, hover"),
11
+ symbol: z.string().optional().describe("Symbol name to search for (alternative to line/character)")
12
+ });
13
+ function detectLanguage(filePath) {
14
+ const ext = path.extname(filePath).toLowerCase();
15
+ switch (ext) {
16
+ case ".ts":
17
+ case ".tsx":
18
+ case ".js":
19
+ case ".jsx":
20
+ case ".mjs":
21
+ case ".cjs":
22
+ return { lang: "typescript", server: "typescript-language-server", tsconfig: true };
23
+ case ".py":
24
+ return { lang: "python", server: "pyright", tsconfig: false };
25
+ case ".go":
26
+ return { lang: "go", server: "gopls", tsconfig: false };
27
+ case ".rs":
28
+ return { lang: "rust", server: "rust-analyzer", tsconfig: false };
29
+ default:
30
+ return { lang: "unknown", server: null, tsconfig: false };
31
+ }
32
+ }
33
+ function isAvailable(cmd) {
34
+ try {
35
+ const which = process.platform === "win32" ? "where" : "which";
36
+ execFileSync(which, [cmd], { stdio: "pipe", timeout: 3e3 });
37
+ return true;
38
+ } catch {
39
+ return false;
40
+ }
41
+ }
42
+ async function tsDiagnostics(filePath, cwd) {
43
+ try {
44
+ const result = execFileSync("npx", ["tsc", "--noEmit", "--pretty", "false", filePath], {
45
+ cwd,
46
+ encoding: "utf-8",
47
+ timeout: 3e4
48
+ });
49
+ return result.trim() || "No type errors found.";
50
+ } catch (err) {
51
+ const output = err.stdout || err.stderr || "";
52
+ if (output.includes("error TS")) {
53
+ const lines = output.split("\n").filter((l) => l.includes("error TS"));
54
+ return lines.slice(0, 20).join("\n") || "No type errors found.";
55
+ }
56
+ try {
57
+ const result = execFileSync("npx", ["tsc", "--noEmit", "--pretty", "false"], {
58
+ cwd,
59
+ encoding: "utf-8",
60
+ timeout: 3e4
61
+ });
62
+ const allLines = result.trim().split("\n").slice(0, 30).join("\n");
63
+ return allLines || "No type errors found.";
64
+ } catch (err2) {
65
+ const out = err2.stdout || err2.stderr || "";
66
+ const errLines = out.split("\n").filter((l) => l.includes("error TS"));
67
+ return errLines.slice(0, 20).join("\n") || "Could not run type checker.";
68
+ }
69
+ }
70
+ }
71
+ async function findDefinition(symbol, cwd) {
72
+ if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(symbol)) {
73
+ return `Invalid symbol name: "${symbol}"`;
74
+ }
75
+ const patterns = [
76
+ `(function|const|let|var|class|interface|type|enum)\\s+${symbol}\\b`,
77
+ `export\\s+(default\\s+)?(function|const|let|var|class|interface|type|enum)\\s+${symbol}\\b`,
78
+ `def\\s+${symbol}\\s*\\(`,
79
+ // Python
80
+ `func\\s+${symbol}\\s*\\(`,
81
+ // Go
82
+ `fn\\s+${symbol}\\s*[<(]`
83
+ // Rust
84
+ ];
85
+ const combinedPattern = patterns.join("|");
86
+ try {
87
+ const result = execFileSync("rg", [
88
+ "--no-heading",
89
+ "--line-number",
90
+ "-e",
91
+ combinedPattern,
92
+ "--type-add",
93
+ "code:*.{ts,tsx,js,jsx,py,go,rs}",
94
+ "--type",
95
+ "code",
96
+ "-m",
97
+ "10"
98
+ ], { cwd, encoding: "utf-8", timeout: 1e4, maxBuffer: 1024 * 1024 });
99
+ return result.trim() || `No definition found for "${symbol}"`;
100
+ } catch {
101
+ try {
102
+ const result = execFileSync("grep", [
103
+ "-rn",
104
+ "-E",
105
+ combinedPattern,
106
+ "--include=*.ts",
107
+ "--include=*.tsx",
108
+ "--include=*.js",
109
+ "--include=*.py",
110
+ "--include=*.go",
111
+ "--include=*.rs",
112
+ "."
113
+ ], { cwd, encoding: "utf-8", timeout: 1e4, maxBuffer: 1024 * 1024 });
114
+ const lines = result.trim().split("\n").slice(0, 10).join("\n");
115
+ return lines || `No definition found for "${symbol}"`;
116
+ } catch {
117
+ return `No definition found for "${symbol}"`;
118
+ }
119
+ }
120
+ }
121
+ async function findReferences(symbol, cwd) {
122
+ if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(symbol)) {
123
+ return `Invalid symbol name: "${symbol}"`;
124
+ }
125
+ try {
126
+ const result = execFileSync("rg", [
127
+ "--no-heading",
128
+ "--line-number",
129
+ "-w",
130
+ symbol,
131
+ "--type-add",
132
+ "code:*.{ts,tsx,js,jsx,py,go,rs}",
133
+ "--type",
134
+ "code",
135
+ "-m",
136
+ "30"
137
+ ], { cwd, encoding: "utf-8", timeout: 1e4, maxBuffer: 1024 * 1024 });
138
+ const lines = result.trim().split("\n");
139
+ return `Found ${lines.length} reference(s) for "${symbol}":
140
+
141
+ ${lines.join("\n")}`;
142
+ } catch {
143
+ try {
144
+ const result = execFileSync("grep", [
145
+ "-rn",
146
+ "-w",
147
+ symbol,
148
+ "--include=*.ts",
149
+ "--include=*.tsx",
150
+ "--include=*.js",
151
+ "--include=*.py",
152
+ "--include=*.go",
153
+ "--include=*.rs",
154
+ "."
155
+ ], { cwd, encoding: "utf-8", timeout: 1e4, maxBuffer: 1024 * 1024 });
156
+ const lines = result.trim().split("\n").slice(0, 30);
157
+ return `Found ${lines.length} reference(s) for "${symbol}":
158
+
159
+ ${lines.join("\n")}`;
160
+ } catch {
161
+ return `No references found for "${symbol}"`;
162
+ }
163
+ }
164
+ }
165
+ async function getHoverInfo(symbol, filePath, line, cwd) {
166
+ try {
167
+ const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
168
+ const content = await fs.readFile(absPath, "utf-8");
169
+ const lines = content.split("\n");
170
+ const startLine = Math.max(0, line - 3);
171
+ const endLine = Math.min(lines.length, line + 2);
172
+ const context = lines.slice(startLine, endLine).map((l, i) => `${startLine + i + 1}: ${l}`).join("\n");
173
+ const def = await findDefinition(symbol, cwd);
174
+ return `## Context at ${filePath}:${line}
175
+ \`\`\`
176
+ ${context}
177
+ \`\`\`
178
+
179
+ ## Definition
180
+ ${def}`;
181
+ } catch (err) {
182
+ return `Could not get hover info: ${err.message}`;
183
+ }
184
+ }
185
+ async function getDiagnostics(filePath, cwd) {
186
+ const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
187
+ const { lang } = detectLanguage(absPath);
188
+ switch (lang) {
189
+ case "typescript":
190
+ return tsDiagnostics(absPath, cwd);
191
+ case "python": {
192
+ if (isAvailable("pyright")) {
193
+ try {
194
+ const result = execFileSync("pyright", [absPath], {
195
+ cwd,
196
+ encoding: "utf-8",
197
+ timeout: 3e4
198
+ });
199
+ return result.trim() || "No errors found.";
200
+ } catch (err) {
201
+ return err.stdout || err.stderr || "Pyright check failed.";
202
+ }
203
+ }
204
+ try {
205
+ execFileSync("python", ["-m", "py_compile", absPath], {
206
+ cwd,
207
+ encoding: "utf-8",
208
+ timeout: 1e4
209
+ });
210
+ return "No syntax errors found.";
211
+ } catch (err) {
212
+ return err.stderr || err.stdout || "Syntax check failed.";
213
+ }
214
+ }
215
+ case "go": {
216
+ try {
217
+ const result = execFileSync("go", ["vet", absPath], {
218
+ cwd,
219
+ encoding: "utf-8",
220
+ timeout: 15e3
221
+ });
222
+ return result.trim() || "No issues found.";
223
+ } catch (err) {
224
+ return err.stderr || err.stdout || "Go vet failed.";
225
+ }
226
+ }
227
+ case "rust": {
228
+ try {
229
+ const result = execFileSync("cargo", ["check", "--message-format", "short"], {
230
+ cwd,
231
+ encoding: "utf-8",
232
+ timeout: 6e4
233
+ });
234
+ const lines = result.trim().split("\n").slice(0, 30).join("\n");
235
+ return lines || "No issues found.";
236
+ } catch (err) {
237
+ return err.stderr || err.stdout || "Cargo check failed.";
238
+ }
239
+ }
240
+ default:
241
+ return `No language server available for ${lang} files. Use grep to search for symbols.`;
242
+ }
243
+ }
244
+ var lspTool = {
245
+ name: "lsp",
246
+ description: "Code intelligence: find definitions, references, type errors, and hover info. Uses TypeScript compiler, pyright, gopls, or grep-based heuristics. Actions: definition (go-to-def), references (find usages), diagnostics (type errors), hover (type info). Provide either line/character position or a symbol name.",
247
+ parameters,
248
+ async execute(params, ctx) {
249
+ const filePath = params.file;
250
+ const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(ctx.cwd, filePath);
251
+ try {
252
+ switch (params.action) {
253
+ case "definition": {
254
+ const symbol = params.symbol || await extractSymbol(absPath, params.line, params.character);
255
+ if (!symbol) {
256
+ return { content: "Provide a symbol name or line/character position.", isError: true };
257
+ }
258
+ const result = await findDefinition(symbol, ctx.cwd);
259
+ return { content: result };
260
+ }
261
+ case "references": {
262
+ const symbol = params.symbol || await extractSymbol(absPath, params.line, params.character);
263
+ if (!symbol) {
264
+ return { content: "Provide a symbol name or line/character position.", isError: true };
265
+ }
266
+ const result = await findReferences(symbol, ctx.cwd);
267
+ return { content: result };
268
+ }
269
+ case "diagnostics": {
270
+ const result = await getDiagnostics(filePath, ctx.cwd);
271
+ return { content: result };
272
+ }
273
+ case "hover": {
274
+ const symbol = params.symbol || await extractSymbol(absPath, params.line, params.character);
275
+ if (!symbol) {
276
+ return { content: "Provide a symbol name or line/character position.", isError: true };
277
+ }
278
+ const result = await getHoverInfo(symbol, filePath, params.line ?? 1, ctx.cwd);
279
+ return { content: result };
280
+ }
281
+ default:
282
+ return { content: `Unknown action: ${params.action}`, isError: true };
283
+ }
284
+ } catch (err) {
285
+ return { content: `LSP error: ${err.message}`, isError: true };
286
+ }
287
+ }
288
+ };
289
+ async function extractSymbol(filePath, line, character) {
290
+ if (!line) return null;
291
+ try {
292
+ const content = await fs.readFile(filePath, "utf-8");
293
+ const lines = content.split("\n");
294
+ const lineText = lines[line - 1];
295
+ if (!lineText) return null;
296
+ const col = character ?? 0;
297
+ const wordRegex = /[a-zA-Z_$][a-zA-Z0-9_$]*/g;
298
+ let match;
299
+ while ((match = wordRegex.exec(lineText)) !== null) {
300
+ const start = match.index;
301
+ const end = start + match[0].length;
302
+ if (col >= start && col <= end) {
303
+ return match[0];
304
+ }
305
+ }
306
+ const firstMatch = lineText.match(/[a-zA-Z_$][a-zA-Z0-9_$]*/);
307
+ return firstMatch?.[0] ?? null;
308
+ } catch {
309
+ return null;
310
+ }
311
+ }
312
+
313
+ export {
314
+ lspTool
315
+ };