micode 0.6.0 → 0.7.1

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.
Files changed (84) hide show
  1. package/README.md +64 -331
  2. package/package.json +9 -14
  3. package/src/agents/artifact-searcher.ts +46 -0
  4. package/src/agents/brainstormer.ts +145 -0
  5. package/src/agents/codebase-analyzer.ts +75 -0
  6. package/src/agents/codebase-locator.ts +71 -0
  7. package/src/agents/commander.ts +138 -0
  8. package/src/agents/executor.ts +215 -0
  9. package/src/agents/implementer.ts +99 -0
  10. package/src/agents/index.ts +44 -0
  11. package/src/agents/ledger-creator.ts +113 -0
  12. package/src/agents/pattern-finder.ts +70 -0
  13. package/src/agents/planner.ts +230 -0
  14. package/src/agents/project-initializer.ts +264 -0
  15. package/src/agents/reviewer.ts +102 -0
  16. package/src/config-loader.ts +89 -0
  17. package/src/hooks/artifact-auto-index.ts +111 -0
  18. package/src/hooks/auto-clear-ledger.ts +230 -0
  19. package/src/hooks/auto-compact.ts +241 -0
  20. package/src/hooks/comment-checker.ts +120 -0
  21. package/src/hooks/context-injector.ts +163 -0
  22. package/src/hooks/context-window-monitor.ts +106 -0
  23. package/src/hooks/file-ops-tracker.ts +96 -0
  24. package/src/hooks/ledger-loader.ts +78 -0
  25. package/src/hooks/preemptive-compaction.ts +183 -0
  26. package/src/hooks/session-recovery.ts +258 -0
  27. package/src/hooks/token-aware-truncation.ts +189 -0
  28. package/src/index.ts +258 -0
  29. package/src/tools/artifact-index/index.ts +269 -0
  30. package/src/tools/artifact-index/schema.sql +44 -0
  31. package/src/tools/artifact-search.ts +49 -0
  32. package/src/tools/ast-grep/index.ts +189 -0
  33. package/src/tools/background-task/manager.ts +374 -0
  34. package/src/tools/background-task/tools.ts +145 -0
  35. package/src/tools/background-task/types.ts +68 -0
  36. package/src/tools/btca/index.ts +82 -0
  37. package/src/tools/look-at.ts +210 -0
  38. package/src/tools/pty/buffer.ts +49 -0
  39. package/src/tools/pty/index.ts +34 -0
  40. package/src/tools/pty/manager.ts +159 -0
  41. package/src/tools/pty/tools/kill.ts +68 -0
  42. package/src/tools/pty/tools/list.ts +55 -0
  43. package/src/tools/pty/tools/read.ts +152 -0
  44. package/src/tools/pty/tools/spawn.ts +78 -0
  45. package/src/tools/pty/tools/write.ts +97 -0
  46. package/src/tools/pty/types.ts +62 -0
  47. package/src/utils/model-limits.ts +36 -0
  48. package/dist/agents/artifact-searcher.d.ts +0 -2
  49. package/dist/agents/brainstormer.d.ts +0 -2
  50. package/dist/agents/codebase-analyzer.d.ts +0 -2
  51. package/dist/agents/codebase-locator.d.ts +0 -2
  52. package/dist/agents/commander.d.ts +0 -3
  53. package/dist/agents/executor.d.ts +0 -2
  54. package/dist/agents/implementer.d.ts +0 -2
  55. package/dist/agents/index.d.ts +0 -15
  56. package/dist/agents/ledger-creator.d.ts +0 -2
  57. package/dist/agents/pattern-finder.d.ts +0 -2
  58. package/dist/agents/planner.d.ts +0 -2
  59. package/dist/agents/project-initializer.d.ts +0 -2
  60. package/dist/agents/reviewer.d.ts +0 -2
  61. package/dist/config-loader.d.ts +0 -20
  62. package/dist/hooks/artifact-auto-index.d.ts +0 -19
  63. package/dist/hooks/auto-clear-ledger.d.ts +0 -11
  64. package/dist/hooks/auto-compact.d.ts +0 -9
  65. package/dist/hooks/comment-checker.d.ts +0 -9
  66. package/dist/hooks/context-injector.d.ts +0 -15
  67. package/dist/hooks/context-window-monitor.d.ts +0 -15
  68. package/dist/hooks/file-ops-tracker.d.ts +0 -26
  69. package/dist/hooks/ledger-loader.d.ts +0 -16
  70. package/dist/hooks/preemptive-compaction.d.ts +0 -9
  71. package/dist/hooks/session-recovery.d.ts +0 -9
  72. package/dist/hooks/token-aware-truncation.d.ts +0 -15
  73. package/dist/index.d.ts +0 -3
  74. package/dist/index.js +0 -16267
  75. package/dist/tools/artifact-index/index.d.ts +0 -38
  76. package/dist/tools/artifact-search.d.ts +0 -17
  77. package/dist/tools/ast-grep/index.d.ts +0 -88
  78. package/dist/tools/background-task/manager.d.ts +0 -27
  79. package/dist/tools/background-task/tools.d.ts +0 -41
  80. package/dist/tools/background-task/types.d.ts +0 -53
  81. package/dist/tools/btca/index.d.ts +0 -19
  82. package/dist/tools/look-at.d.ts +0 -11
  83. package/dist/utils/model-limits.d.ts +0 -7
  84. /package/{dist/tools/background-task/index.d.ts → src/tools/background-task/index.ts} +0 -0
@@ -0,0 +1,269 @@
1
+ // src/tools/artifact-index/index.ts
2
+ import { Database } from "bun:sqlite";
3
+ import { readFileSync } from "node:fs";
4
+ import { join, dirname } from "node:path";
5
+ import { mkdirSync, existsSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+
8
+ const DEFAULT_DB_DIR = join(homedir(), ".config", "opencode", "artifact-index");
9
+ const DB_NAME = "context.db";
10
+
11
+ export interface PlanRecord {
12
+ id: string;
13
+ title?: string;
14
+ filePath: string;
15
+ overview?: string;
16
+ approach?: string;
17
+ }
18
+
19
+ export interface LedgerRecord {
20
+ id: string;
21
+ sessionName?: string;
22
+ filePath: string;
23
+ goal?: string;
24
+ stateNow?: string;
25
+ keyDecisions?: string;
26
+ filesRead?: string;
27
+ filesModified?: string;
28
+ }
29
+
30
+ export interface SearchResult {
31
+ type: "plan" | "ledger";
32
+ id: string;
33
+ filePath: string;
34
+ title?: string;
35
+ summary?: string;
36
+ score: number;
37
+ }
38
+
39
+ export class ArtifactIndex {
40
+ private db: Database | null = null;
41
+ private dbPath: string;
42
+
43
+ constructor(dbDir: string = DEFAULT_DB_DIR) {
44
+ this.dbPath = join(dbDir, DB_NAME);
45
+ }
46
+
47
+ async initialize(): Promise<void> {
48
+ // Ensure directory exists
49
+ const dir = dirname(this.dbPath);
50
+ if (!existsSync(dir)) {
51
+ mkdirSync(dir, { recursive: true });
52
+ }
53
+
54
+ this.db = new Database(this.dbPath);
55
+
56
+ // Load and execute schema
57
+ const schemaPath = join(dirname(import.meta.path), "schema.sql");
58
+ let schema: string;
59
+
60
+ try {
61
+ schema = readFileSync(schemaPath, "utf-8");
62
+ } catch {
63
+ // Fallback: inline schema for when bundled
64
+ schema = this.getInlineSchema();
65
+ }
66
+
67
+ // Execute schema - use exec for multi-statement support
68
+ this.db.exec(schema);
69
+ }
70
+
71
+ private getInlineSchema(): string {
72
+ return `
73
+ CREATE TABLE IF NOT EXISTS plans (
74
+ id TEXT PRIMARY KEY,
75
+ title TEXT,
76
+ file_path TEXT UNIQUE NOT NULL,
77
+ overview TEXT,
78
+ approach TEXT,
79
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
80
+ indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
81
+ );
82
+ CREATE TABLE IF NOT EXISTS ledgers (
83
+ id TEXT PRIMARY KEY,
84
+ session_name TEXT,
85
+ file_path TEXT UNIQUE NOT NULL,
86
+ goal TEXT,
87
+ state_now TEXT,
88
+ key_decisions TEXT,
89
+ files_read TEXT,
90
+ files_modified TEXT,
91
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
92
+ indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
93
+ );
94
+ CREATE VIRTUAL TABLE IF NOT EXISTS plans_fts USING fts5(id, title, overview, approach);
95
+ CREATE VIRTUAL TABLE IF NOT EXISTS ledgers_fts USING fts5(id, session_name, goal, state_now, key_decisions);
96
+ `;
97
+ }
98
+
99
+ async indexPlan(record: PlanRecord): Promise<void> {
100
+ if (!this.db) throw new Error("Database not initialized");
101
+
102
+ // Check for existing record by file_path to clean up old FTS entry
103
+ const existing = this.db
104
+ .query<{ id: string }, [string]>(`SELECT id FROM plans WHERE file_path = ?`)
105
+ .get(record.filePath);
106
+ if (existing) {
107
+ this.db.run(`DELETE FROM plans_fts WHERE id = ?`, [existing.id]);
108
+ }
109
+
110
+ this.db.run(
111
+ `
112
+ INSERT INTO plans (id, title, file_path, overview, approach, indexed_at)
113
+ VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
114
+ ON CONFLICT(file_path) DO UPDATE SET
115
+ id = excluded.id,
116
+ title = excluded.title,
117
+ overview = excluded.overview,
118
+ approach = excluded.approach,
119
+ indexed_at = CURRENT_TIMESTAMP
120
+ `,
121
+ [record.id, record.title ?? null, record.filePath, record.overview ?? null, record.approach ?? null],
122
+ );
123
+
124
+ this.db.run(
125
+ `
126
+ INSERT INTO plans_fts (id, title, overview, approach)
127
+ VALUES (?, ?, ?, ?)
128
+ `,
129
+ [record.id, record.title ?? null, record.overview ?? null, record.approach ?? null],
130
+ );
131
+ }
132
+
133
+ async indexLedger(record: LedgerRecord): Promise<void> {
134
+ if (!this.db) throw new Error("Database not initialized");
135
+
136
+ // Check for existing record by file_path to clean up old FTS entry
137
+ const existing = this.db
138
+ .query<{ id: string }, [string]>(`SELECT id FROM ledgers WHERE file_path = ?`)
139
+ .get(record.filePath);
140
+ if (existing) {
141
+ this.db.run(`DELETE FROM ledgers_fts WHERE id = ?`, [existing.id]);
142
+ }
143
+
144
+ this.db.run(
145
+ `
146
+ INSERT INTO ledgers (id, session_name, file_path, goal, state_now, key_decisions, files_read, files_modified, indexed_at)
147
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
148
+ ON CONFLICT(file_path) DO UPDATE SET
149
+ id = excluded.id,
150
+ session_name = excluded.session_name,
151
+ goal = excluded.goal,
152
+ state_now = excluded.state_now,
153
+ key_decisions = excluded.key_decisions,
154
+ files_read = excluded.files_read,
155
+ files_modified = excluded.files_modified,
156
+ indexed_at = CURRENT_TIMESTAMP
157
+ `,
158
+ [
159
+ record.id,
160
+ record.sessionName ?? null,
161
+ record.filePath,
162
+ record.goal ?? null,
163
+ record.stateNow ?? null,
164
+ record.keyDecisions ?? null,
165
+ record.filesRead ?? null,
166
+ record.filesModified ?? null,
167
+ ],
168
+ );
169
+
170
+ this.db.run(
171
+ `
172
+ INSERT INTO ledgers_fts (id, session_name, goal, state_now, key_decisions)
173
+ VALUES (?, ?, ?, ?, ?)
174
+ `,
175
+ [
176
+ record.id,
177
+ record.sessionName ?? null,
178
+ record.goal ?? null,
179
+ record.stateNow ?? null,
180
+ record.keyDecisions ?? null,
181
+ ],
182
+ );
183
+ }
184
+
185
+ async search(query: string, limit: number = 10): Promise<SearchResult[]> {
186
+ if (!this.db) throw new Error("Database not initialized");
187
+
188
+ const results: SearchResult[] = [];
189
+ const escapedQuery = this.escapeFtsQuery(query);
190
+
191
+ // Search plans
192
+ const plans = this.db
193
+ .query<{ id: string; file_path: string; title: string; rank: number }, [string, number]>(`
194
+ SELECT p.id, p.file_path, p.title, rank
195
+ FROM plans_fts
196
+ JOIN plans p ON plans_fts.id = p.id
197
+ WHERE plans_fts MATCH ?
198
+ ORDER BY rank
199
+ LIMIT ?
200
+ `)
201
+ .all(escapedQuery, limit);
202
+
203
+ for (const row of plans) {
204
+ results.push({
205
+ type: "plan",
206
+ id: row.id,
207
+ filePath: row.file_path,
208
+ title: row.title,
209
+ score: -row.rank,
210
+ });
211
+ }
212
+
213
+ // Search ledgers
214
+ const ledgers = this.db
215
+ .query<{ id: string; file_path: string; session_name: string; goal: string; rank: number }, [string, number]>(`
216
+ SELECT l.id, l.file_path, l.session_name, l.goal, rank
217
+ FROM ledgers_fts
218
+ JOIN ledgers l ON ledgers_fts.id = l.id
219
+ WHERE ledgers_fts MATCH ?
220
+ ORDER BY rank
221
+ LIMIT ?
222
+ `)
223
+ .all(escapedQuery, limit);
224
+
225
+ for (const row of ledgers) {
226
+ results.push({
227
+ type: "ledger",
228
+ id: row.id,
229
+ filePath: row.file_path,
230
+ title: row.session_name,
231
+ summary: row.goal,
232
+ score: -row.rank,
233
+ });
234
+ }
235
+
236
+ // Sort all results by score descending
237
+ results.sort((a, b) => b.score - a.score);
238
+
239
+ return results.slice(0, limit);
240
+ }
241
+
242
+ private escapeFtsQuery(query: string): string {
243
+ // Escape special FTS5 characters and wrap terms in quotes
244
+ return query
245
+ .replace(/['"]/g, "")
246
+ .split(/\s+/)
247
+ .filter((term) => term.length > 0)
248
+ .map((term) => `"${term}"`)
249
+ .join(" OR ");
250
+ }
251
+
252
+ async close(): Promise<void> {
253
+ if (this.db) {
254
+ this.db.close();
255
+ this.db = null;
256
+ }
257
+ }
258
+ }
259
+
260
+ // Singleton instance for global use
261
+ let globalIndex: ArtifactIndex | null = null;
262
+
263
+ export async function getArtifactIndex(): Promise<ArtifactIndex> {
264
+ if (!globalIndex) {
265
+ globalIndex = new ArtifactIndex();
266
+ await globalIndex.initialize();
267
+ }
268
+ return globalIndex;
269
+ }
@@ -0,0 +1,44 @@
1
+ -- src/tools/artifact-index/schema.sql
2
+ -- Artifact Index Schema for SQLite + FTS5
3
+ -- NOTE: FTS tables are standalone (not content-linked) and manually synced by code
4
+
5
+ -- Plans table
6
+ CREATE TABLE IF NOT EXISTS plans (
7
+ id TEXT PRIMARY KEY,
8
+ title TEXT,
9
+ file_path TEXT UNIQUE NOT NULL,
10
+ overview TEXT,
11
+ approach TEXT,
12
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
13
+ indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
14
+ );
15
+
16
+ -- Ledgers table - with file operation tracking
17
+ CREATE TABLE IF NOT EXISTS ledgers (
18
+ id TEXT PRIMARY KEY,
19
+ session_name TEXT,
20
+ file_path TEXT UNIQUE NOT NULL,
21
+ goal TEXT,
22
+ state_now TEXT,
23
+ key_decisions TEXT,
24
+ files_read TEXT,
25
+ files_modified TEXT,
26
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
27
+ indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
28
+ );
29
+
30
+ -- FTS5 virtual tables for full-text search (standalone, manually synced)
31
+ CREATE VIRTUAL TABLE IF NOT EXISTS plans_fts USING fts5(
32
+ id,
33
+ title,
34
+ overview,
35
+ approach
36
+ );
37
+
38
+ CREATE VIRTUAL TABLE IF NOT EXISTS ledgers_fts USING fts5(
39
+ id,
40
+ session_name,
41
+ goal,
42
+ state_now,
43
+ key_decisions
44
+ );
@@ -0,0 +1,49 @@
1
+ // src/tools/artifact-search.ts
2
+ import { tool } from "@opencode-ai/plugin/tool";
3
+ import { getArtifactIndex } from "./artifact-index";
4
+
5
+ export const artifact_search = tool({
6
+ description: `Search past plans and ledgers for relevant precedent.
7
+ Use this to find:
8
+ - Similar problems you've solved before
9
+ - Patterns and approaches that worked
10
+ - Lessons learned from past sessions
11
+ Returns ranked results with file paths for further reading.`,
12
+ args: {
13
+ query: tool.schema.string().describe("Search query - describe what you're looking for"),
14
+ limit: tool.schema.number().optional().describe("Max results to return (default: 10)"),
15
+ type: tool.schema.enum(["all", "plan", "ledger"]).optional().describe("Filter by artifact type (default: all)"),
16
+ },
17
+ execute: async (args) => {
18
+ try {
19
+ const index = await getArtifactIndex();
20
+ const results = await index.search(args.query, args.limit || 10);
21
+
22
+ // Filter by type if specified
23
+ const filtered = args.type && args.type !== "all" ? results.filter((r) => r.type === args.type) : results;
24
+
25
+ if (filtered.length === 0) {
26
+ return `No results found for "${args.query}". Try broader search terms.`;
27
+ }
28
+
29
+ let output = `## Search Results for "${args.query}"\n\n`;
30
+ output += `Found ${filtered.length} result(s):\n\n`;
31
+
32
+ for (const result of filtered) {
33
+ const typeLabel = result.type.charAt(0).toUpperCase() + result.type.slice(1);
34
+ output += `### ${typeLabel}: ${result.title || result.id}\n`;
35
+ output += `**File:** \`${result.filePath}\`\n`;
36
+ if (result.summary) {
37
+ output += `**Summary:** ${result.summary}\n`;
38
+ }
39
+ output += `**Relevance Score:** ${result.score.toFixed(2)}\n\n`;
40
+ }
41
+
42
+ output += `---\n*Use the Read tool to view full content of relevant files.*`;
43
+
44
+ return output;
45
+ } catch (e) {
46
+ return `Error searching artifacts: ${e instanceof Error ? e.message : String(e)}`;
47
+ }
48
+ },
49
+ });
@@ -0,0 +1,189 @@
1
+ import { spawn, which } from "bun";
2
+ import { tool } from "@opencode-ai/plugin/tool";
3
+
4
+ /**
5
+ * Check if ast-grep CLI (sg) is available on the system.
6
+ * Returns installation instructions if not found.
7
+ */
8
+ export async function checkAstGrepAvailable(): Promise<{ available: boolean; message?: string }> {
9
+ const sgPath = which("sg");
10
+ if (sgPath) {
11
+ return { available: true };
12
+ }
13
+ return {
14
+ available: false,
15
+ message:
16
+ "ast-grep CLI (sg) not found. AST-aware search/replace will not work.\n" +
17
+ "Install with one of:\n" +
18
+ " npm install -g @ast-grep/cli\n" +
19
+ " cargo install ast-grep --locked\n" +
20
+ " brew install ast-grep",
21
+ };
22
+ }
23
+
24
+ const LANGUAGES = [
25
+ "c",
26
+ "cpp",
27
+ "csharp",
28
+ "css",
29
+ "dart",
30
+ "elixir",
31
+ "go",
32
+ "haskell",
33
+ "html",
34
+ "java",
35
+ "javascript",
36
+ "json",
37
+ "kotlin",
38
+ "lua",
39
+ "php",
40
+ "python",
41
+ "ruby",
42
+ "rust",
43
+ "scala",
44
+ "sql",
45
+ "swift",
46
+ "tsx",
47
+ "typescript",
48
+ "yaml",
49
+ ] as const;
50
+
51
+ interface Match {
52
+ file: string;
53
+ range: { start: { line: number; column: number }; end: { line: number; column: number } };
54
+ text: string;
55
+ replacement?: string;
56
+ }
57
+
58
+ async function runSg(args: string[]): Promise<{ matches: Match[]; error?: string }> {
59
+ try {
60
+ const proc = spawn(["sg", ...args], {
61
+ stdout: "pipe",
62
+ stderr: "pipe",
63
+ });
64
+
65
+ const [stdout, stderr, exitCode] = await Promise.all([
66
+ new Response(proc.stdout).text(),
67
+ new Response(proc.stderr).text(),
68
+ proc.exited,
69
+ ]);
70
+
71
+ if (exitCode !== 0 && !stdout.trim()) {
72
+ if (stderr.includes("No files found")) {
73
+ return { matches: [] };
74
+ }
75
+ return { matches: [], error: stderr.trim() || `Exit code ${exitCode}` };
76
+ }
77
+
78
+ if (!stdout.trim()) return { matches: [] };
79
+
80
+ try {
81
+ const matches = JSON.parse(stdout) as Match[];
82
+ return { matches };
83
+ } catch {
84
+ return { matches: [], error: "Failed to parse output" };
85
+ }
86
+ } catch (e) {
87
+ const err = e as Error;
88
+ if (err.message?.includes("ENOENT")) {
89
+ return {
90
+ matches: [],
91
+ error:
92
+ "ast-grep CLI not found. Install with:\n" +
93
+ " npm install -g @ast-grep/cli\n" +
94
+ " cargo install ast-grep --locked\n" +
95
+ " brew install ast-grep",
96
+ };
97
+ }
98
+ return { matches: [], error: err.message };
99
+ }
100
+ }
101
+
102
+ function formatMatches(matches: Match[], isDryRun = false): string {
103
+ if (matches.length === 0) return "No matches found";
104
+
105
+ const MAX = 100;
106
+ const truncated = matches.length > MAX;
107
+ const shown = matches.slice(0, MAX);
108
+
109
+ const lines = shown.map((m) => {
110
+ const loc = `${m.file}:${m.range.start.line}:${m.range.start.column}`;
111
+ const text = m.text.length > 100 ? `${m.text.slice(0, 100)}...` : m.text;
112
+ if (isDryRun && m.replacement) {
113
+ return `${loc}\n - ${text}\n + ${m.replacement}`;
114
+ }
115
+ return `${loc}: ${text}`;
116
+ });
117
+
118
+ if (truncated) {
119
+ lines.unshift(`Found ${matches.length} matches (showing first ${MAX}):`);
120
+ }
121
+
122
+ return lines.join("\n");
123
+ }
124
+
125
+ export const ast_grep_search = tool({
126
+ description:
127
+ "Search code patterns using AST-aware matching. " +
128
+ "Use meta-variables: $VAR (single node), $$$ (multiple nodes). " +
129
+ "Patterns must be complete AST nodes. " +
130
+ "Examples: 'console.log($MSG)', 'def $FUNC($$$):', 'async function $NAME($$$)'",
131
+ args: {
132
+ pattern: tool.schema.string().describe("AST pattern with meta-variables"),
133
+ lang: tool.schema.enum(LANGUAGES).describe("Target language"),
134
+ paths: tool.schema.array(tool.schema.string()).optional().describe("Paths to search"),
135
+ },
136
+ execute: async (args) => {
137
+ const sgArgs = ["run", "-p", args.pattern, "--lang", args.lang, "--json=compact"];
138
+ if (args.paths?.length) {
139
+ sgArgs.push(...args.paths);
140
+ } else {
141
+ sgArgs.push(".");
142
+ }
143
+
144
+ const result = await runSg(sgArgs);
145
+ if (result.error) return `Error: ${result.error}`;
146
+ return formatMatches(result.matches);
147
+ },
148
+ });
149
+
150
+ export const ast_grep_replace = tool({
151
+ description:
152
+ "Replace code patterns with AST-aware rewriting. " +
153
+ "Dry-run by default. Use meta-variables in rewrite to preserve matched content. " +
154
+ "Example: pattern='console.log($MSG)' rewrite='logger.info($MSG)'",
155
+ args: {
156
+ pattern: tool.schema.string().describe("AST pattern to match"),
157
+ rewrite: tool.schema.string().describe("Replacement pattern"),
158
+ lang: tool.schema.enum(LANGUAGES).describe("Target language"),
159
+ paths: tool.schema.array(tool.schema.string()).optional().describe("Paths to search"),
160
+ apply: tool.schema.boolean().optional().describe("Apply changes (default: false, dry-run)"),
161
+ },
162
+ execute: async (args) => {
163
+ const sgArgs = ["run", "-p", args.pattern, "-r", args.rewrite, "--lang", args.lang, "--json=compact"];
164
+
165
+ if (args.apply) {
166
+ sgArgs.push("--update-all");
167
+ }
168
+
169
+ if (args.paths?.length) {
170
+ sgArgs.push(...args.paths);
171
+ } else {
172
+ sgArgs.push(".");
173
+ }
174
+
175
+ const result = await runSg(sgArgs);
176
+ if (result.error) return `Error: ${result.error}`;
177
+
178
+ const isDryRun = !args.apply;
179
+ const output = formatMatches(result.matches, isDryRun);
180
+
181
+ if (isDryRun && result.matches.length > 0) {
182
+ return `${output}\n\n(Dry run - use apply=true to apply changes)`;
183
+ }
184
+ if (args.apply && result.matches.length > 0) {
185
+ return `Applied ${result.matches.length} replacements:\n${output}`;
186
+ }
187
+ return output;
188
+ },
189
+ });