codemeld 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +514 -0
- package/bin/cli.js +2 -0
- package/dist/ai/agent.d.ts +124 -0
- package/dist/ai/agent.d.ts.map +1 -0
- package/dist/ai/agent.js +289 -0
- package/dist/ai/agent.js.map +1 -0
- package/dist/ai/index.d.ts +10 -0
- package/dist/ai/index.d.ts.map +1 -0
- package/dist/ai/index.js +10 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/ai/prompts.d.ts +35 -0
- package/dist/ai/prompts.d.ts.map +1 -0
- package/dist/ai/prompts.js +166 -0
- package/dist/ai/prompts.js.map +1 -0
- package/dist/ai/refinement-loop.d.ts +29 -0
- package/dist/ai/refinement-loop.d.ts.map +1 -0
- package/dist/ai/refinement-loop.js +180 -0
- package/dist/ai/refinement-loop.js.map +1 -0
- package/dist/ai/tools.d.ts +17 -0
- package/dist/ai/tools.d.ts.map +1 -0
- package/dist/ai/tools.js +353 -0
- package/dist/ai/tools.js.map +1 -0
- package/dist/ai/visual-compare.d.ts +43 -0
- package/dist/ai/visual-compare.d.ts.map +1 -0
- package/dist/ai/visual-compare.js +176 -0
- package/dist/ai/visual-compare.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +179 -0
- package/dist/cli.js.map +1 -0
- package/dist/converter.d.ts +10 -0
- package/dist/converter.d.ts.map +1 -0
- package/dist/converter.js +836 -0
- package/dist/converter.js.map +1 -0
- package/dist/deconverter.d.ts +19 -0
- package/dist/deconverter.d.ts.map +1 -0
- package/dist/deconverter.js +188 -0
- package/dist/deconverter.js.map +1 -0
- package/dist/frameworks/angular-adapter.d.ts +27 -0
- package/dist/frameworks/angular-adapter.d.ts.map +1 -0
- package/dist/frameworks/angular-adapter.js +617 -0
- package/dist/frameworks/angular-adapter.js.map +1 -0
- package/dist/frameworks/index.d.ts +10 -0
- package/dist/frameworks/index.d.ts.map +1 -0
- package/dist/frameworks/index.js +21 -0
- package/dist/frameworks/index.js.map +1 -0
- package/dist/frameworks/nextjs-adapter.d.ts +22 -0
- package/dist/frameworks/nextjs-adapter.d.ts.map +1 -0
- package/dist/frameworks/nextjs-adapter.js +392 -0
- package/dist/frameworks/nextjs-adapter.js.map +1 -0
- package/dist/frameworks/react-adapter.d.ts +21 -0
- package/dist/frameworks/react-adapter.d.ts.map +1 -0
- package/dist/frameworks/react-adapter.js +71 -0
- package/dist/frameworks/react-adapter.js.map +1 -0
- package/dist/frameworks/svelte-adapter.d.ts +27 -0
- package/dist/frameworks/svelte-adapter.d.ts.map +1 -0
- package/dist/frameworks/svelte-adapter.js +519 -0
- package/dist/frameworks/svelte-adapter.js.map +1 -0
- package/dist/frameworks/types.d.ts +78 -0
- package/dist/frameworks/types.d.ts.map +1 -0
- package/dist/frameworks/types.js +2 -0
- package/dist/frameworks/types.js.map +1 -0
- package/dist/frameworks/vue-adapter.d.ts +34 -0
- package/dist/frameworks/vue-adapter.d.ts.map +1 -0
- package/dist/frameworks/vue-adapter.js +632 -0
- package/dist/frameworks/vue-adapter.js.map +1 -0
- package/dist/generators/accessibility-generator.d.ts +43 -0
- package/dist/generators/accessibility-generator.d.ts.map +1 -0
- package/dist/generators/accessibility-generator.js +507 -0
- package/dist/generators/accessibility-generator.js.map +1 -0
- package/dist/generators/asset-handler.d.ts +14 -0
- package/dist/generators/asset-handler.d.ts.map +1 -0
- package/dist/generators/asset-handler.js +79 -0
- package/dist/generators/asset-handler.js.map +1 -0
- package/dist/generators/build-verifier.d.ts +8 -0
- package/dist/generators/build-verifier.d.ts.map +1 -0
- package/dist/generators/build-verifier.js +64 -0
- package/dist/generators/build-verifier.js.map +1 -0
- package/dist/generators/component-extractor.d.ts +25 -0
- package/dist/generators/component-extractor.d.ts.map +1 -0
- package/dist/generators/component-extractor.js +146 -0
- package/dist/generators/component-extractor.js.map +1 -0
- package/dist/generators/component-generator.d.ts +12 -0
- package/dist/generators/component-generator.d.ts.map +1 -0
- package/dist/generators/component-generator.js +724 -0
- package/dist/generators/component-generator.js.map +1 -0
- package/dist/generators/deploy-generator.d.ts +9 -0
- package/dist/generators/deploy-generator.d.ts.map +1 -0
- package/dist/generators/deploy-generator.js +409 -0
- package/dist/generators/deploy-generator.js.map +1 -0
- package/dist/generators/error-boundary.d.ts +5 -0
- package/dist/generators/error-boundary.d.ts.map +1 -0
- package/dist/generators/error-boundary.js +59 -0
- package/dist/generators/error-boundary.js.map +1 -0
- package/dist/generators/form-generator.d.ts +42 -0
- package/dist/generators/form-generator.d.ts.map +1 -0
- package/dist/generators/form-generator.js +662 -0
- package/dist/generators/form-generator.js.map +1 -0
- package/dist/generators/hooks-generator.d.ts +40 -0
- package/dist/generators/hooks-generator.d.ts.map +1 -0
- package/dist/generators/hooks-generator.js +297 -0
- package/dist/generators/hooks-generator.js.map +1 -0
- package/dist/generators/html-generator.d.ts +27 -0
- package/dist/generators/html-generator.d.ts.map +1 -0
- package/dist/generators/html-generator.js +772 -0
- package/dist/generators/html-generator.js.map +1 -0
- package/dist/generators/jquery-converter.d.ts +41 -0
- package/dist/generators/jquery-converter.d.ts.map +1 -0
- package/dist/generators/jquery-converter.js +594 -0
- package/dist/generators/jquery-converter.js.map +1 -0
- package/dist/generators/pattern-implementer.d.ts +26 -0
- package/dist/generators/pattern-implementer.d.ts.map +1 -0
- package/dist/generators/pattern-implementer.js +336 -0
- package/dist/generators/pattern-implementer.js.map +1 -0
- package/dist/generators/performance-generator.d.ts +51 -0
- package/dist/generators/performance-generator.d.ts.map +1 -0
- package/dist/generators/performance-generator.js +428 -0
- package/dist/generators/performance-generator.js.map +1 -0
- package/dist/generators/router-generator.d.ts +21 -0
- package/dist/generators/router-generator.d.ts.map +1 -0
- package/dist/generators/router-generator.js +178 -0
- package/dist/generators/router-generator.js.map +1 -0
- package/dist/generators/scaffolder.d.ts +28 -0
- package/dist/generators/scaffolder.d.ts.map +1 -0
- package/dist/generators/scaffolder.js +266 -0
- package/dist/generators/scaffolder.js.map +1 -0
- package/dist/generators/seo-generator.d.ts +29 -0
- package/dist/generators/seo-generator.d.ts.map +1 -0
- package/dist/generators/seo-generator.js +223 -0
- package/dist/generators/seo-generator.js.map +1 -0
- package/dist/generators/test-generator.d.ts +19 -0
- package/dist/generators/test-generator.d.ts.map +1 -0
- package/dist/generators/test-generator.js +398 -0
- package/dist/generators/test-generator.js.map +1 -0
- package/dist/generators/type-generator.d.ts +33 -0
- package/dist/generators/type-generator.d.ts.map +1 -0
- package/dist/generators/type-generator.js +663 -0
- package/dist/generators/type-generator.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/parsers/css-processor.d.ts +23 -0
- package/dist/parsers/css-processor.d.ts.map +1 -0
- package/dist/parsers/css-processor.js +129 -0
- package/dist/parsers/css-processor.js.map +1 -0
- package/dist/parsers/framework-parser.d.ts +48 -0
- package/dist/parsers/framework-parser.d.ts.map +1 -0
- package/dist/parsers/framework-parser.js +770 -0
- package/dist/parsers/framework-parser.js.map +1 -0
- package/dist/parsers/html-parser.d.ts +12 -0
- package/dist/parsers/html-parser.d.ts.map +1 -0
- package/dist/parsers/html-parser.js +444 -0
- package/dist/parsers/html-parser.js.map +1 -0
- package/dist/parsers/js-analyzer.d.ts +199 -0
- package/dist/parsers/js-analyzer.d.ts.map +1 -0
- package/dist/parsers/js-analyzer.js +680 -0
- package/dist/parsers/js-analyzer.js.map +1 -0
- package/dist/parsers/js-resolver.d.ts +8 -0
- package/dist/parsers/js-resolver.d.ts.map +1 -0
- package/dist/parsers/js-resolver.js +45 -0
- package/dist/parsers/js-resolver.js.map +1 -0
- package/dist/parsers/tailwind-detector.d.ts +23 -0
- package/dist/parsers/tailwind-detector.d.ts.map +1 -0
- package/dist/parsers/tailwind-detector.js +104 -0
- package/dist/parsers/tailwind-detector.js.map +1 -0
- package/dist/tests/advanced-features.test.d.ts +2 -0
- package/dist/tests/advanced-features.test.d.ts.map +1 -0
- package/dist/tests/advanced-features.test.js +235 -0
- package/dist/tests/advanced-features.test.js.map +1 -0
- package/dist/tests/css-modules.test.d.ts +2 -0
- package/dist/tests/css-modules.test.d.ts.map +1 -0
- package/dist/tests/css-modules.test.js +61 -0
- package/dist/tests/css-modules.test.js.map +1 -0
- package/dist/tests/css-processor.test.d.ts +2 -0
- package/dist/tests/css-processor.test.d.ts.map +1 -0
- package/dist/tests/css-processor.test.js +48 -0
- package/dist/tests/css-processor.test.js.map +1 -0
- package/dist/tests/html-parser.test.d.ts +2 -0
- package/dist/tests/html-parser.test.d.ts.map +1 -0
- package/dist/tests/html-parser.test.js +78 -0
- package/dist/tests/html-parser.test.js.map +1 -0
- package/dist/tests/integration.test.d.ts +2 -0
- package/dist/tests/integration.test.d.ts.map +1 -0
- package/dist/tests/integration.test.js +65 -0
- package/dist/tests/integration.test.js.map +1 -0
- package/dist/tests/js-analyzer.test.d.ts +2 -0
- package/dist/tests/js-analyzer.test.d.ts.map +1 -0
- package/dist/tests/js-analyzer.test.js +58 -0
- package/dist/tests/js-analyzer.test.js.map +1 -0
- package/dist/tests/naming.test.d.ts +2 -0
- package/dist/tests/naming.test.d.ts.map +1 -0
- package/dist/tests/naming.test.js +43 -0
- package/dist/tests/naming.test.js.map +1 -0
- package/dist/tests/router-generator.test.d.ts +2 -0
- package/dist/tests/router-generator.test.d.ts.map +1 -0
- package/dist/tests/router-generator.test.js +60 -0
- package/dist/tests/router-generator.test.js.map +1 -0
- package/dist/tui/chat.d.ts +13 -0
- package/dist/tui/chat.d.ts.map +1 -0
- package/dist/tui/chat.js +499 -0
- package/dist/tui/chat.js.map +1 -0
- package/dist/tui/design-guide.d.ts +41 -0
- package/dist/tui/design-guide.d.ts.map +1 -0
- package/dist/tui/design-guide.js +184 -0
- package/dist/tui/design-guide.js.map +1 -0
- package/dist/tui/input.d.ts +30 -0
- package/dist/tui/input.d.ts.map +1 -0
- package/dist/tui/input.js +239 -0
- package/dist/tui/input.js.map +1 -0
- package/dist/tui/renderer.d.ts +48 -0
- package/dist/tui/renderer.d.ts.map +1 -0
- package/dist/tui/renderer.js +212 -0
- package/dist/tui/renderer.js.map +1 -0
- package/dist/tui/tools.d.ts +14 -0
- package/dist/tui/tools.d.ts.map +1 -0
- package/dist/tui/tools.js +1370 -0
- package/dist/tui/tools.js.map +1 -0
- package/dist/types.d.ts +93 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/config.d.ts +20 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +33 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/formatter.d.ts +5 -0
- package/dist/utils/formatter.d.ts.map +1 -0
- package/dist/utils/formatter.js +68 -0
- package/dist/utils/formatter.js.map +1 -0
- package/dist/utils/logger.d.ts +8 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +19 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/naming.d.ts +17 -0
- package/dist/utils/naming.d.ts.map +1 -0
- package/dist/utils/naming.js +48 -0
- package/dist/utils/naming.js.map +1 -0
- package/dist/utils/report.d.ts +56 -0
- package/dist/utils/report.d.ts.map +1 -0
- package/dist/utils/report.js +339 -0
- package/dist/utils/report.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,1370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full-powered tool suite for the TUI chat agent.
|
|
3
|
+
* Mirrors Claude Code's capabilities: file I/O, glob, grep, edit,
|
|
4
|
+
* bash, git, npm, tree, and more.
|
|
5
|
+
*/
|
|
6
|
+
import { readFile, writeFile, readdir, stat, mkdir, unlink, rename, copyFile } from 'fs/promises';
|
|
7
|
+
import { join, extname, dirname } from 'path';
|
|
8
|
+
import { execFile } from 'child_process';
|
|
9
|
+
import { promisify } from 'util';
|
|
10
|
+
import { generateDesignGuide, getFallbackGuide } from './design-guide.js';
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
12
|
+
const SHELL_OPTS = { shell: true };
|
|
13
|
+
const IS_WIN = process.platform === 'win32';
|
|
14
|
+
const TODO_FILE = '.todos.json';
|
|
15
|
+
async function loadTodos(projectDir) {
|
|
16
|
+
const filePath = join(projectDir, TODO_FILE);
|
|
17
|
+
try {
|
|
18
|
+
const raw = await readFile(filePath, 'utf-8');
|
|
19
|
+
return JSON.parse(raw);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return { nextId: 1, items: [] };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function saveTodos(projectDir, store) {
|
|
26
|
+
const filePath = join(projectDir, TODO_FILE);
|
|
27
|
+
await writeFile(filePath, JSON.stringify(store, null, 2), 'utf-8');
|
|
28
|
+
}
|
|
29
|
+
const STATUS_ICONS = {
|
|
30
|
+
pending: '○',
|
|
31
|
+
in_progress: '◐',
|
|
32
|
+
done: '●',
|
|
33
|
+
blocked: '✗',
|
|
34
|
+
};
|
|
35
|
+
const PRIORITY_LABELS = {
|
|
36
|
+
high: '🔴 HIGH',
|
|
37
|
+
medium: '🟡 MED',
|
|
38
|
+
low: '🔵 LOW',
|
|
39
|
+
};
|
|
40
|
+
function formatTodoItem(item, indent = '') {
|
|
41
|
+
const icon = STATUS_ICONS[item.status] || '?';
|
|
42
|
+
const pri = PRIORITY_LABELS[item.priority] || item.priority;
|
|
43
|
+
const statusLabel = item.status === 'done' ? '✓ done' : item.status === 'blocked' ? '✗ blocked' : item.status;
|
|
44
|
+
let line = `${indent}${icon} #${item.id} [${pri}] ${item.title} (${statusLabel})`;
|
|
45
|
+
if (item.description) {
|
|
46
|
+
line += `\n${indent} ${item.description}`;
|
|
47
|
+
}
|
|
48
|
+
return line;
|
|
49
|
+
}
|
|
50
|
+
function formatTodoList(store, filter) {
|
|
51
|
+
let items = store.items;
|
|
52
|
+
if (filter && filter !== 'all') {
|
|
53
|
+
items = items.filter(i => i.status === filter);
|
|
54
|
+
}
|
|
55
|
+
if (items.length === 0) {
|
|
56
|
+
return filter && filter !== 'all'
|
|
57
|
+
? `No ${filter} todos.`
|
|
58
|
+
: 'No todos yet. Use todo_add to create tasks.';
|
|
59
|
+
}
|
|
60
|
+
// Group by status
|
|
61
|
+
const groups = {};
|
|
62
|
+
for (const item of items) {
|
|
63
|
+
if (!groups[item.status])
|
|
64
|
+
groups[item.status] = [];
|
|
65
|
+
groups[item.status].push(item);
|
|
66
|
+
}
|
|
67
|
+
const lines = [];
|
|
68
|
+
const order = ['in_progress', 'pending', 'blocked', 'done'];
|
|
69
|
+
// Summary
|
|
70
|
+
const total = store.items.length;
|
|
71
|
+
const done = store.items.filter(i => i.status === 'done').length;
|
|
72
|
+
const progress = total > 0 ? Math.round((done / total) * 100) : 0;
|
|
73
|
+
const bar = '█'.repeat(Math.floor(progress / 5)) + '░'.repeat(20 - Math.floor(progress / 5));
|
|
74
|
+
lines.push(`Progress: [${bar}] ${done}/${total} (${progress}%)\n`);
|
|
75
|
+
for (const status of order) {
|
|
76
|
+
const group = groups[status];
|
|
77
|
+
if (!group || group.length === 0)
|
|
78
|
+
continue;
|
|
79
|
+
lines.push(`── ${status.toUpperCase().replace('_', ' ')} (${group.length}) ──`);
|
|
80
|
+
// Separate top-level and subtasks
|
|
81
|
+
const topLevel = group.filter(i => !i.parentId);
|
|
82
|
+
const subtasks = group.filter(i => i.parentId);
|
|
83
|
+
for (const item of topLevel) {
|
|
84
|
+
lines.push(formatTodoItem(item));
|
|
85
|
+
// Show subtasks under parent
|
|
86
|
+
const children = subtasks.filter(s => s.parentId === item.id);
|
|
87
|
+
for (const child of children) {
|
|
88
|
+
lines.push(formatTodoItem(child, ' '));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Orphaned subtasks (parent not in this group)
|
|
92
|
+
const shownChildIds = new Set(topLevel.flatMap(p => subtasks.filter(s => s.parentId === p.id).map(s => s.id)));
|
|
93
|
+
for (const sub of subtasks) {
|
|
94
|
+
if (!shownChildIds.has(sub.id)) {
|
|
95
|
+
lines.push(formatTodoItem(sub, ' '));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
lines.push('');
|
|
99
|
+
}
|
|
100
|
+
return lines.join('\n');
|
|
101
|
+
}
|
|
102
|
+
// ─── Tool Definitions (sent to AI API) ──────────────────────────────
|
|
103
|
+
export const chatToolDefinitions = [
|
|
104
|
+
// ── File Reading ──────────────────────────────────────────────
|
|
105
|
+
{
|
|
106
|
+
type: 'function',
|
|
107
|
+
function: {
|
|
108
|
+
name: 'read_file',
|
|
109
|
+
description: 'Read a file with line numbers. Returns max 200 lines per call. For larger files, use offset and limit to page through in chunks. Always shows total line count so you know if there is more to read.',
|
|
110
|
+
parameters: {
|
|
111
|
+
type: 'object',
|
|
112
|
+
properties: {
|
|
113
|
+
path: {
|
|
114
|
+
type: 'string',
|
|
115
|
+
description: 'Relative file path (e.g., "src/App.tsx", "package.json")',
|
|
116
|
+
},
|
|
117
|
+
offset: {
|
|
118
|
+
type: 'number',
|
|
119
|
+
description: 'Start reading from this line number (1-based). Default: 1.',
|
|
120
|
+
},
|
|
121
|
+
limit: {
|
|
122
|
+
type: 'number',
|
|
123
|
+
description: 'Number of lines to read, max 200. Default: 200.',
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
required: ['path'],
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
// ── File Writing ──────────────────────────────────────────────
|
|
131
|
+
{
|
|
132
|
+
type: 'function',
|
|
133
|
+
function: {
|
|
134
|
+
name: 'write_file',
|
|
135
|
+
description: 'Create or overwrite a file with the given content. Creates parent directories automatically.',
|
|
136
|
+
parameters: {
|
|
137
|
+
type: 'object',
|
|
138
|
+
properties: {
|
|
139
|
+
path: {
|
|
140
|
+
type: 'string',
|
|
141
|
+
description: 'Relative file path to write',
|
|
142
|
+
},
|
|
143
|
+
content: {
|
|
144
|
+
type: 'string',
|
|
145
|
+
description: 'The complete file content',
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
required: ['path', 'content'],
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
// ── File Editing (targeted replacement) ───────────────────────
|
|
153
|
+
{
|
|
154
|
+
type: 'function',
|
|
155
|
+
function: {
|
|
156
|
+
name: 'edit_file',
|
|
157
|
+
description: 'Make a targeted edit by finding and replacing a specific string in a file. The old_string must appear exactly once in the file (include enough context to make it unique). Use replace_all=true to replace ALL occurrences.',
|
|
158
|
+
parameters: {
|
|
159
|
+
type: 'object',
|
|
160
|
+
properties: {
|
|
161
|
+
path: {
|
|
162
|
+
type: 'string',
|
|
163
|
+
description: 'Relative file path',
|
|
164
|
+
},
|
|
165
|
+
old_string: {
|
|
166
|
+
type: 'string',
|
|
167
|
+
description: 'The exact text to find (must be unique unless replace_all=true)',
|
|
168
|
+
},
|
|
169
|
+
new_string: {
|
|
170
|
+
type: 'string',
|
|
171
|
+
description: 'The replacement text',
|
|
172
|
+
},
|
|
173
|
+
replace_all: {
|
|
174
|
+
type: 'boolean',
|
|
175
|
+
description: 'Replace ALL occurrences instead of requiring uniqueness (default: false)',
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
required: ['path', 'old_string', 'new_string'],
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
// ── Multi-Edit (multiple replacements in one call) ────────────
|
|
183
|
+
{
|
|
184
|
+
type: 'function',
|
|
185
|
+
function: {
|
|
186
|
+
name: 'multi_edit',
|
|
187
|
+
description: 'Apply multiple find-and-replace edits to a file in a single operation. Each edit is applied sequentially. More efficient than multiple edit_file calls.',
|
|
188
|
+
parameters: {
|
|
189
|
+
type: 'object',
|
|
190
|
+
properties: {
|
|
191
|
+
path: {
|
|
192
|
+
type: 'string',
|
|
193
|
+
description: 'Relative file path',
|
|
194
|
+
},
|
|
195
|
+
edits: {
|
|
196
|
+
type: 'array',
|
|
197
|
+
items: {
|
|
198
|
+
type: 'object',
|
|
199
|
+
properties: {
|
|
200
|
+
old_string: { type: 'string', description: 'Text to find' },
|
|
201
|
+
new_string: { type: 'string', description: 'Replacement text' },
|
|
202
|
+
},
|
|
203
|
+
required: ['old_string', 'new_string'],
|
|
204
|
+
},
|
|
205
|
+
description: 'Array of {old_string, new_string} edit operations',
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
required: ['path', 'edits'],
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
// ── Insert at Line ────────────────────────────────────────────
|
|
213
|
+
{
|
|
214
|
+
type: 'function',
|
|
215
|
+
function: {
|
|
216
|
+
name: 'insert_at_line',
|
|
217
|
+
description: 'Insert text at a specific line number in a file. The existing content at that line shifts down.',
|
|
218
|
+
parameters: {
|
|
219
|
+
type: 'object',
|
|
220
|
+
properties: {
|
|
221
|
+
path: {
|
|
222
|
+
type: 'string',
|
|
223
|
+
description: 'Relative file path',
|
|
224
|
+
},
|
|
225
|
+
line: {
|
|
226
|
+
type: 'number',
|
|
227
|
+
description: 'Line number to insert at (1-based). Use 1 for beginning, or a large number for end.',
|
|
228
|
+
},
|
|
229
|
+
content: {
|
|
230
|
+
type: 'string',
|
|
231
|
+
description: 'The text to insert',
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
required: ['path', 'line', 'content'],
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
// ── File Operations (delete, move, copy) ──────────────────────
|
|
239
|
+
{
|
|
240
|
+
type: 'function',
|
|
241
|
+
function: {
|
|
242
|
+
name: 'delete_file',
|
|
243
|
+
description: 'Delete a file from the project.',
|
|
244
|
+
parameters: {
|
|
245
|
+
type: 'object',
|
|
246
|
+
properties: {
|
|
247
|
+
path: {
|
|
248
|
+
type: 'string',
|
|
249
|
+
description: 'Relative file path to delete',
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
required: ['path'],
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
type: 'function',
|
|
258
|
+
function: {
|
|
259
|
+
name: 'move_file',
|
|
260
|
+
description: 'Move or rename a file. Creates destination directories automatically.',
|
|
261
|
+
parameters: {
|
|
262
|
+
type: 'object',
|
|
263
|
+
properties: {
|
|
264
|
+
from: {
|
|
265
|
+
type: 'string',
|
|
266
|
+
description: 'Current relative file path',
|
|
267
|
+
},
|
|
268
|
+
to: {
|
|
269
|
+
type: 'string',
|
|
270
|
+
description: 'New relative file path',
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
required: ['from', 'to'],
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
type: 'function',
|
|
279
|
+
function: {
|
|
280
|
+
name: 'copy_file',
|
|
281
|
+
description: 'Copy a file to a new location.',
|
|
282
|
+
parameters: {
|
|
283
|
+
type: 'object',
|
|
284
|
+
properties: {
|
|
285
|
+
from: {
|
|
286
|
+
type: 'string',
|
|
287
|
+
description: 'Source relative file path',
|
|
288
|
+
},
|
|
289
|
+
to: {
|
|
290
|
+
type: 'string',
|
|
291
|
+
description: 'Destination relative file path',
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
required: ['from', 'to'],
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
// ── Directory Listing ─────────────────────────────────────────
|
|
299
|
+
{
|
|
300
|
+
type: 'function',
|
|
301
|
+
function: {
|
|
302
|
+
name: 'list_files',
|
|
303
|
+
description: 'List files and directories at a path with sizes. Excludes node_modules and hidden files.',
|
|
304
|
+
parameters: {
|
|
305
|
+
type: 'object',
|
|
306
|
+
properties: {
|
|
307
|
+
path: {
|
|
308
|
+
type: 'string',
|
|
309
|
+
description: 'Relative directory path (use "." for project root)',
|
|
310
|
+
},
|
|
311
|
+
recursive: {
|
|
312
|
+
type: 'boolean',
|
|
313
|
+
description: 'List files recursively (default: false). Shows a tree-like structure.',
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
required: ['path'],
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
// ── Glob (find files by pattern) ──────────────────────────────
|
|
321
|
+
{
|
|
322
|
+
type: 'function',
|
|
323
|
+
function: {
|
|
324
|
+
name: 'glob',
|
|
325
|
+
description: 'Find files matching a glob pattern. Returns matching file paths. Examples: "**/*.tsx", "src/**/*.css", "*.json".',
|
|
326
|
+
parameters: {
|
|
327
|
+
type: 'object',
|
|
328
|
+
properties: {
|
|
329
|
+
pattern: {
|
|
330
|
+
type: 'string',
|
|
331
|
+
description: 'Glob pattern (e.g., "**/*.tsx", "src/components/*.tsx", "**/*.test.ts")',
|
|
332
|
+
},
|
|
333
|
+
path: {
|
|
334
|
+
type: 'string',
|
|
335
|
+
description: 'Base directory for the search (default: project root ".")',
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
required: ['pattern'],
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
// ── Grep (search file contents) ───────────────────────────────
|
|
343
|
+
{
|
|
344
|
+
type: 'function',
|
|
345
|
+
function: {
|
|
346
|
+
name: 'grep',
|
|
347
|
+
description: 'Search for a regex pattern across files. Returns matching lines with file paths and line numbers. Powerful for finding definitions, usages, imports, etc.',
|
|
348
|
+
parameters: {
|
|
349
|
+
type: 'object',
|
|
350
|
+
properties: {
|
|
351
|
+
pattern: {
|
|
352
|
+
type: 'string',
|
|
353
|
+
description: 'Regex pattern to search for (e.g., "useState", "function\\s+handle", "import.*from")',
|
|
354
|
+
},
|
|
355
|
+
path: {
|
|
356
|
+
type: 'string',
|
|
357
|
+
description: 'Directory or file to search in (default: ".")',
|
|
358
|
+
},
|
|
359
|
+
file_type: {
|
|
360
|
+
type: 'string',
|
|
361
|
+
description: 'File extension filter without dot (e.g., "tsx", "css", "json", "ts")',
|
|
362
|
+
},
|
|
363
|
+
context: {
|
|
364
|
+
type: 'number',
|
|
365
|
+
description: 'Number of context lines to show before/after each match (default: 0)',
|
|
366
|
+
},
|
|
367
|
+
case_insensitive: {
|
|
368
|
+
type: 'boolean',
|
|
369
|
+
description: 'Case-insensitive search (default: false)',
|
|
370
|
+
},
|
|
371
|
+
max_results: {
|
|
372
|
+
type: 'number',
|
|
373
|
+
description: 'Maximum number of matching lines to return (default: 50)',
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
required: ['pattern'],
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
// ── Bash / Shell Command ──────────────────────────────────────
|
|
381
|
+
{
|
|
382
|
+
type: 'function',
|
|
383
|
+
function: {
|
|
384
|
+
name: 'bash',
|
|
385
|
+
description: 'Execute a shell command in the project directory. Use for: npm/npx commands, git operations, curl, system tools, piped commands, etc. Returns stdout and stderr.',
|
|
386
|
+
parameters: {
|
|
387
|
+
type: 'object',
|
|
388
|
+
properties: {
|
|
389
|
+
command: {
|
|
390
|
+
type: 'string',
|
|
391
|
+
description: 'Shell command to run (e.g., "npm install lodash", "git diff HEAD~1", "ls -la src/")',
|
|
392
|
+
},
|
|
393
|
+
timeout: {
|
|
394
|
+
type: 'number',
|
|
395
|
+
description: 'Timeout in milliseconds (default: 120000 = 2 minutes)',
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
required: ['command'],
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
// ── Build & Typecheck (convenience shortcuts) ─────────────────
|
|
403
|
+
{
|
|
404
|
+
type: 'function',
|
|
405
|
+
function: {
|
|
406
|
+
name: 'run_build',
|
|
407
|
+
description: 'Run "npx vite build" to compile the project. Shortcut for bash with build command.',
|
|
408
|
+
parameters: { type: 'object', properties: {} },
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
type: 'function',
|
|
413
|
+
function: {
|
|
414
|
+
name: 'run_typecheck',
|
|
415
|
+
description: 'Run "npx tsc --noEmit" to check TypeScript types. Shortcut for bash with typecheck command.',
|
|
416
|
+
parameters: { type: 'object', properties: {} },
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
// ── Git Operations ────────────────────────────────────────────
|
|
420
|
+
{
|
|
421
|
+
type: 'function',
|
|
422
|
+
function: {
|
|
423
|
+
name: 'git_status',
|
|
424
|
+
description: 'Show git status: modified, staged, and untracked files.',
|
|
425
|
+
parameters: { type: 'object', properties: {} },
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
type: 'function',
|
|
430
|
+
function: {
|
|
431
|
+
name: 'git_diff',
|
|
432
|
+
description: 'Show git diff of changes. Can diff specific files or all changes.',
|
|
433
|
+
parameters: {
|
|
434
|
+
type: 'object',
|
|
435
|
+
properties: {
|
|
436
|
+
path: {
|
|
437
|
+
type: 'string',
|
|
438
|
+
description: 'Specific file to diff (optional, defaults to all changes)',
|
|
439
|
+
},
|
|
440
|
+
staged: {
|
|
441
|
+
type: 'boolean',
|
|
442
|
+
description: 'Show staged changes only (--cached)',
|
|
443
|
+
},
|
|
444
|
+
ref: {
|
|
445
|
+
type: 'string',
|
|
446
|
+
description: 'Compare against a specific ref (e.g., "HEAD~1", "main", commit hash)',
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
type: 'function',
|
|
454
|
+
function: {
|
|
455
|
+
name: 'git_log',
|
|
456
|
+
description: 'Show recent git commit history.',
|
|
457
|
+
parameters: {
|
|
458
|
+
type: 'object',
|
|
459
|
+
properties: {
|
|
460
|
+
count: {
|
|
461
|
+
type: 'number',
|
|
462
|
+
description: 'Number of commits to show (default: 10)',
|
|
463
|
+
},
|
|
464
|
+
oneline: {
|
|
465
|
+
type: 'boolean',
|
|
466
|
+
description: 'One-line format (default: true)',
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
type: 'function',
|
|
474
|
+
function: {
|
|
475
|
+
name: 'git_commit',
|
|
476
|
+
description: 'Stage files and create a git commit.',
|
|
477
|
+
parameters: {
|
|
478
|
+
type: 'object',
|
|
479
|
+
properties: {
|
|
480
|
+
message: {
|
|
481
|
+
type: 'string',
|
|
482
|
+
description: 'Commit message',
|
|
483
|
+
},
|
|
484
|
+
files: {
|
|
485
|
+
type: 'array',
|
|
486
|
+
items: { type: 'string' },
|
|
487
|
+
description: 'Files to stage (default: all modified files). Use ["."] for all.',
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
required: ['message'],
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
// ── NPM Operations ───────────────────────────────────────────
|
|
495
|
+
{
|
|
496
|
+
type: 'function',
|
|
497
|
+
function: {
|
|
498
|
+
name: 'npm_install',
|
|
499
|
+
description: 'Install npm packages. Can install specific packages or run npm install for all dependencies.',
|
|
500
|
+
parameters: {
|
|
501
|
+
type: 'object',
|
|
502
|
+
properties: {
|
|
503
|
+
packages: {
|
|
504
|
+
type: 'array',
|
|
505
|
+
items: { type: 'string' },
|
|
506
|
+
description: 'Package names to install (e.g., ["lodash", "@types/lodash"]). Omit to run plain "npm install".',
|
|
507
|
+
},
|
|
508
|
+
dev: {
|
|
509
|
+
type: 'boolean',
|
|
510
|
+
description: 'Install as devDependency (--save-dev)',
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
// ── File Info ──────────────────────────────────────────────────
|
|
517
|
+
{
|
|
518
|
+
type: 'function',
|
|
519
|
+
function: {
|
|
520
|
+
name: 'file_info',
|
|
521
|
+
description: 'Get detailed information about a file: size, line count, last modified, type.',
|
|
522
|
+
parameters: {
|
|
523
|
+
type: 'object',
|
|
524
|
+
properties: {
|
|
525
|
+
path: {
|
|
526
|
+
type: 'string',
|
|
527
|
+
description: 'Relative file path',
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
required: ['path'],
|
|
531
|
+
},
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
// ── Project Overview ──────────────────────────────────────────
|
|
535
|
+
{
|
|
536
|
+
type: 'function',
|
|
537
|
+
function: {
|
|
538
|
+
name: 'project_overview',
|
|
539
|
+
description: 'Get a high-level overview of the project: directory tree, key config files (package.json, tsconfig.json), dependencies, and scripts. Great first tool to call when exploring a new project.',
|
|
540
|
+
parameters: { type: 'object', properties: {} },
|
|
541
|
+
},
|
|
542
|
+
},
|
|
543
|
+
// ── Todo Management ──────────────────────────────────────────
|
|
544
|
+
{
|
|
545
|
+
type: 'function',
|
|
546
|
+
function: {
|
|
547
|
+
name: 'todo_list',
|
|
548
|
+
description: 'List all todo items. Shows ID, status, priority, and description for each task. Use this to see current progress and what needs to be done.',
|
|
549
|
+
parameters: {
|
|
550
|
+
type: 'object',
|
|
551
|
+
properties: {
|
|
552
|
+
filter: {
|
|
553
|
+
type: 'string',
|
|
554
|
+
enum: ['all', 'pending', 'in_progress', 'done', 'blocked'],
|
|
555
|
+
description: 'Filter by status (default: "all")',
|
|
556
|
+
},
|
|
557
|
+
},
|
|
558
|
+
},
|
|
559
|
+
},
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
type: 'function',
|
|
563
|
+
function: {
|
|
564
|
+
name: 'todo_add',
|
|
565
|
+
description: 'Add a new todo item. Use this to track tasks, plan work, and break down complex requests into steps.',
|
|
566
|
+
parameters: {
|
|
567
|
+
type: 'object',
|
|
568
|
+
properties: {
|
|
569
|
+
title: {
|
|
570
|
+
type: 'string',
|
|
571
|
+
description: 'Short title for the task',
|
|
572
|
+
},
|
|
573
|
+
description: {
|
|
574
|
+
type: 'string',
|
|
575
|
+
description: 'Detailed description of what needs to be done',
|
|
576
|
+
},
|
|
577
|
+
priority: {
|
|
578
|
+
type: 'string',
|
|
579
|
+
enum: ['high', 'medium', 'low'],
|
|
580
|
+
description: 'Priority level (default: "medium")',
|
|
581
|
+
},
|
|
582
|
+
parent_id: {
|
|
583
|
+
type: 'number',
|
|
584
|
+
description: 'ID of a parent task to create a subtask under (optional)',
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
required: ['title'],
|
|
588
|
+
},
|
|
589
|
+
},
|
|
590
|
+
},
|
|
591
|
+
{
|
|
592
|
+
type: 'function',
|
|
593
|
+
function: {
|
|
594
|
+
name: 'todo_update',
|
|
595
|
+
description: 'Update an existing todo item. Change its status, priority, title, or description. Mark tasks as done when completed.',
|
|
596
|
+
parameters: {
|
|
597
|
+
type: 'object',
|
|
598
|
+
properties: {
|
|
599
|
+
id: {
|
|
600
|
+
type: 'number',
|
|
601
|
+
description: 'ID of the todo item to update',
|
|
602
|
+
},
|
|
603
|
+
status: {
|
|
604
|
+
type: 'string',
|
|
605
|
+
enum: ['pending', 'in_progress', 'done', 'blocked'],
|
|
606
|
+
description: 'New status',
|
|
607
|
+
},
|
|
608
|
+
title: {
|
|
609
|
+
type: 'string',
|
|
610
|
+
description: 'Updated title',
|
|
611
|
+
},
|
|
612
|
+
description: {
|
|
613
|
+
type: 'string',
|
|
614
|
+
description: 'Updated description',
|
|
615
|
+
},
|
|
616
|
+
priority: {
|
|
617
|
+
type: 'string',
|
|
618
|
+
enum: ['high', 'medium', 'low'],
|
|
619
|
+
description: 'Updated priority',
|
|
620
|
+
},
|
|
621
|
+
},
|
|
622
|
+
required: ['id'],
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
},
|
|
626
|
+
{
|
|
627
|
+
type: 'function',
|
|
628
|
+
function: {
|
|
629
|
+
name: 'todo_remove',
|
|
630
|
+
description: 'Remove a todo item by ID.',
|
|
631
|
+
parameters: {
|
|
632
|
+
type: 'object',
|
|
633
|
+
properties: {
|
|
634
|
+
id: {
|
|
635
|
+
type: 'number',
|
|
636
|
+
description: 'ID of the todo item to remove',
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
required: ['id'],
|
|
640
|
+
},
|
|
641
|
+
},
|
|
642
|
+
},
|
|
643
|
+
{
|
|
644
|
+
type: 'function',
|
|
645
|
+
function: {
|
|
646
|
+
name: 'todo_batch',
|
|
647
|
+
description: 'Perform multiple todo operations at once. Efficient for setting up a task plan or marking multiple items done.',
|
|
648
|
+
parameters: {
|
|
649
|
+
type: 'object',
|
|
650
|
+
properties: {
|
|
651
|
+
operations: {
|
|
652
|
+
type: 'array',
|
|
653
|
+
items: {
|
|
654
|
+
type: 'object',
|
|
655
|
+
properties: {
|
|
656
|
+
action: {
|
|
657
|
+
type: 'string',
|
|
658
|
+
enum: ['add', 'update', 'remove'],
|
|
659
|
+
description: 'The operation to perform',
|
|
660
|
+
},
|
|
661
|
+
title: { type: 'string', description: 'Title (for add)' },
|
|
662
|
+
description: { type: 'string', description: 'Description (for add)' },
|
|
663
|
+
priority: { type: 'string', enum: ['high', 'medium', 'low'] },
|
|
664
|
+
id: { type: 'number', description: 'ID (for update/remove)' },
|
|
665
|
+
status: { type: 'string', enum: ['pending', 'in_progress', 'done', 'blocked'] },
|
|
666
|
+
},
|
|
667
|
+
required: ['action'],
|
|
668
|
+
},
|
|
669
|
+
description: 'Array of todo operations',
|
|
670
|
+
},
|
|
671
|
+
},
|
|
672
|
+
required: ['operations'],
|
|
673
|
+
},
|
|
674
|
+
},
|
|
675
|
+
},
|
|
676
|
+
// ── Design Guide Generation ──────────────────────────────────
|
|
677
|
+
{
|
|
678
|
+
type: 'function',
|
|
679
|
+
function: {
|
|
680
|
+
name: 'generate_design_guide',
|
|
681
|
+
description: 'Generate a unique, project-specific design system with typography (Google Fonts), color palette, layout patterns, animations, hero section style, and sample copy. The guide is generated by an AI design director and ensures every project has a distinctive aesthetic. Call this BEFORE creating or modifying UI components.',
|
|
682
|
+
parameters: {
|
|
683
|
+
type: 'object',
|
|
684
|
+
properties: {
|
|
685
|
+
project_type: {
|
|
686
|
+
type: 'string',
|
|
687
|
+
description: 'Type of project (e.g., "Travel booking app", "SaaS dashboard", "Portfolio site", "E-commerce store")',
|
|
688
|
+
},
|
|
689
|
+
user_request: {
|
|
690
|
+
type: 'string',
|
|
691
|
+
description: 'Additional design preferences from the user (e.g., "dark mode, minimalist", "colorful and playful")',
|
|
692
|
+
},
|
|
693
|
+
},
|
|
694
|
+
required: ['project_type'],
|
|
695
|
+
},
|
|
696
|
+
},
|
|
697
|
+
},
|
|
698
|
+
];
|
|
699
|
+
/**
|
|
700
|
+
* Run a shell command and return stdout+stderr.
|
|
701
|
+
*/
|
|
702
|
+
async function runShell(command, cwd, timeout = 120_000) {
|
|
703
|
+
try {
|
|
704
|
+
const { stdout, stderr } = await execFileAsync(IS_WIN ? 'cmd' : 'sh', IS_WIN ? ['/c', command] : ['-c', command], { cwd, timeout, maxBuffer: 10 * 1024 * 1024 });
|
|
705
|
+
const output = (stdout + (stderr ? `\nstderr:\n${stderr}` : '')).trim() || '(no output)';
|
|
706
|
+
return { output, success: true };
|
|
707
|
+
}
|
|
708
|
+
catch (err) {
|
|
709
|
+
const e = err;
|
|
710
|
+
const output = `Exit code ${e.code || '?'}:\n${e.stdout || ''}\n${e.stderr || ''}`.trim();
|
|
711
|
+
return { output, success: false };
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Recursively list a directory tree.
|
|
716
|
+
*/
|
|
717
|
+
async function listTree(dir, base, prefix = '', depth = 0, maxDepth = 4) {
|
|
718
|
+
if (depth > maxDepth)
|
|
719
|
+
return [`${prefix}... (max depth reached)`];
|
|
720
|
+
const lines = [];
|
|
721
|
+
try {
|
|
722
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
723
|
+
const filtered = entries.filter(e => e.name !== '.git' && e.name !== 'node_modules' && e.name !== 'dist');
|
|
724
|
+
for (let i = 0; i < filtered.length; i++) {
|
|
725
|
+
const entry = filtered[i];
|
|
726
|
+
const isLast = i === filtered.length - 1;
|
|
727
|
+
const connector = isLast ? '└── ' : '├── ';
|
|
728
|
+
const childPrefix = isLast ? ' ' : '│ ';
|
|
729
|
+
if (entry.isDirectory()) {
|
|
730
|
+
lines.push(`${prefix}${connector}${entry.name}/`);
|
|
731
|
+
const children = await listTree(join(dir, entry.name), base, prefix + childPrefix, depth + 1, maxDepth);
|
|
732
|
+
lines.push(...children);
|
|
733
|
+
}
|
|
734
|
+
else {
|
|
735
|
+
try {
|
|
736
|
+
const s = await stat(join(dir, entry.name));
|
|
737
|
+
const size = s.size < 1024 ? `${s.size}B` : s.size < 1048576 ? `${(s.size / 1024).toFixed(1)}K` : `${(s.size / 1048576).toFixed(1)}M`;
|
|
738
|
+
lines.push(`${prefix}${connector}${entry.name} (${size})`);
|
|
739
|
+
}
|
|
740
|
+
catch {
|
|
741
|
+
lines.push(`${prefix}${connector}${entry.name}`);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
catch {
|
|
747
|
+
lines.push(`${prefix}(error reading directory)`);
|
|
748
|
+
}
|
|
749
|
+
return lines;
|
|
750
|
+
}
|
|
751
|
+
export function createChatToolExecutor(ctx) {
|
|
752
|
+
const { projectDir, onToolStart, onToolEnd } = ctx;
|
|
753
|
+
return async function executeTool(name, args) {
|
|
754
|
+
onToolStart?.(name, args);
|
|
755
|
+
let result;
|
|
756
|
+
let success = true;
|
|
757
|
+
try {
|
|
758
|
+
switch (name) {
|
|
759
|
+
// ── read_file ─────────────────────────────────────────
|
|
760
|
+
case 'read_file': {
|
|
761
|
+
const MAX_READ_LINES = 200;
|
|
762
|
+
const filePath = join(projectDir, args.path);
|
|
763
|
+
try {
|
|
764
|
+
const content = await readFile(filePath, 'utf-8');
|
|
765
|
+
const allLines = content.split('\n');
|
|
766
|
+
const offset = (args.offset || 1) - 1;
|
|
767
|
+
const limit = Math.min(args.limit || MAX_READ_LINES, MAX_READ_LINES);
|
|
768
|
+
const slice = allLines.slice(offset, offset + limit);
|
|
769
|
+
const numbered = slice.map((l, i) => `${String(offset + i + 1).padStart(4)} ${l}`).join('\n');
|
|
770
|
+
const endLine = offset + slice.length;
|
|
771
|
+
const remaining = allLines.length - endLine;
|
|
772
|
+
let info = `\n\n(${allLines.length} total lines`;
|
|
773
|
+
if (remaining > 0) {
|
|
774
|
+
info += `, showing ${offset + 1}-${endLine}. ${remaining} more lines — use offset: ${endLine + 1} to continue`;
|
|
775
|
+
}
|
|
776
|
+
info += ')';
|
|
777
|
+
result = numbered + info;
|
|
778
|
+
}
|
|
779
|
+
catch {
|
|
780
|
+
result = `Error: File not found: ${args.path}`;
|
|
781
|
+
success = false;
|
|
782
|
+
}
|
|
783
|
+
break;
|
|
784
|
+
}
|
|
785
|
+
// ── write_file ────────────────────────────────────────
|
|
786
|
+
case 'write_file': {
|
|
787
|
+
const filePath = join(projectDir, args.path);
|
|
788
|
+
try {
|
|
789
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
790
|
+
await writeFile(filePath, args.content, 'utf-8');
|
|
791
|
+
const lineCount = args.content.split('\n').length;
|
|
792
|
+
result = `Wrote ${args.path} (${lineCount} lines)`;
|
|
793
|
+
}
|
|
794
|
+
catch (err) {
|
|
795
|
+
result = `Error writing: ${err instanceof Error ? err.message : String(err)}`;
|
|
796
|
+
success = false;
|
|
797
|
+
}
|
|
798
|
+
break;
|
|
799
|
+
}
|
|
800
|
+
// ── edit_file ─────────────────────────────────────────
|
|
801
|
+
case 'edit_file': {
|
|
802
|
+
const filePath = join(projectDir, args.path);
|
|
803
|
+
try {
|
|
804
|
+
const content = await readFile(filePath, 'utf-8');
|
|
805
|
+
const oldStr = args.old_string;
|
|
806
|
+
const newStr = args.new_string;
|
|
807
|
+
const replaceAll = args.replace_all;
|
|
808
|
+
const count = content.split(oldStr).length - 1;
|
|
809
|
+
if (count === 0) {
|
|
810
|
+
result = `Error: String not found in ${args.path}. Check for exact whitespace/indentation match.`;
|
|
811
|
+
success = false;
|
|
812
|
+
}
|
|
813
|
+
else if (count > 1 && !replaceAll) {
|
|
814
|
+
result = `Error: Found ${count} occurrences in ${args.path} — must be unique. Add more context or use replace_all=true.`;
|
|
815
|
+
success = false;
|
|
816
|
+
}
|
|
817
|
+
else {
|
|
818
|
+
const updated = replaceAll ? content.split(oldStr).join(newStr) : content.replace(oldStr, newStr);
|
|
819
|
+
await writeFile(filePath, updated, 'utf-8');
|
|
820
|
+
result = replaceAll
|
|
821
|
+
? `Replaced ${count} occurrence(s) in ${args.path}`
|
|
822
|
+
: `Edited ${args.path} (${oldStr.split('\n').length} → ${newStr.split('\n').length} lines)`;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
catch {
|
|
826
|
+
result = `Error: File not found: ${args.path}`;
|
|
827
|
+
success = false;
|
|
828
|
+
}
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
831
|
+
// ── multi_edit ────────────────────────────────────────
|
|
832
|
+
case 'multi_edit': {
|
|
833
|
+
const filePath = join(projectDir, args.path);
|
|
834
|
+
try {
|
|
835
|
+
let content = await readFile(filePath, 'utf-8');
|
|
836
|
+
const edits = args.edits;
|
|
837
|
+
let applied = 0;
|
|
838
|
+
const errors = [];
|
|
839
|
+
for (let i = 0; i < edits.length; i++) {
|
|
840
|
+
const { old_string, new_string } = edits[i];
|
|
841
|
+
if (!content.includes(old_string)) {
|
|
842
|
+
errors.push(`Edit ${i + 1}: old_string not found`);
|
|
843
|
+
}
|
|
844
|
+
else {
|
|
845
|
+
content = content.replace(old_string, new_string);
|
|
846
|
+
applied++;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
await writeFile(filePath, content, 'utf-8');
|
|
850
|
+
result = `Applied ${applied}/${edits.length} edits to ${args.path}`;
|
|
851
|
+
if (errors.length > 0) {
|
|
852
|
+
result += `\nWarnings:\n${errors.join('\n')}`;
|
|
853
|
+
success = applied > 0;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
catch {
|
|
857
|
+
result = `Error: File not found: ${args.path}`;
|
|
858
|
+
success = false;
|
|
859
|
+
}
|
|
860
|
+
break;
|
|
861
|
+
}
|
|
862
|
+
// ── insert_at_line ────────────────────────────────────
|
|
863
|
+
case 'insert_at_line': {
|
|
864
|
+
const filePath = join(projectDir, args.path);
|
|
865
|
+
try {
|
|
866
|
+
const content = await readFile(filePath, 'utf-8');
|
|
867
|
+
const lines = content.split('\n');
|
|
868
|
+
const lineNum = Math.max(1, Math.min(args.line || 1, lines.length + 1));
|
|
869
|
+
const insertContent = args.content;
|
|
870
|
+
lines.splice(lineNum - 1, 0, insertContent);
|
|
871
|
+
await writeFile(filePath, lines.join('\n'), 'utf-8');
|
|
872
|
+
result = `Inserted ${insertContent.split('\n').length} line(s) at line ${lineNum} in ${args.path}`;
|
|
873
|
+
}
|
|
874
|
+
catch {
|
|
875
|
+
result = `Error: File not found: ${args.path}`;
|
|
876
|
+
success = false;
|
|
877
|
+
}
|
|
878
|
+
break;
|
|
879
|
+
}
|
|
880
|
+
// ── delete_file ───────────────────────────────────────
|
|
881
|
+
case 'delete_file': {
|
|
882
|
+
const filePath = join(projectDir, args.path);
|
|
883
|
+
try {
|
|
884
|
+
await unlink(filePath);
|
|
885
|
+
result = `Deleted ${args.path}`;
|
|
886
|
+
}
|
|
887
|
+
catch {
|
|
888
|
+
result = `Error: Could not delete ${args.path}`;
|
|
889
|
+
success = false;
|
|
890
|
+
}
|
|
891
|
+
break;
|
|
892
|
+
}
|
|
893
|
+
// ── move_file ─────────────────────────────────────────
|
|
894
|
+
case 'move_file': {
|
|
895
|
+
const fromPath = join(projectDir, args.from);
|
|
896
|
+
const toPath = join(projectDir, args.to);
|
|
897
|
+
try {
|
|
898
|
+
await mkdir(dirname(toPath), { recursive: true });
|
|
899
|
+
await rename(fromPath, toPath);
|
|
900
|
+
result = `Moved ${args.from} → ${args.to}`;
|
|
901
|
+
}
|
|
902
|
+
catch (err) {
|
|
903
|
+
result = `Error moving file: ${err instanceof Error ? err.message : String(err)}`;
|
|
904
|
+
success = false;
|
|
905
|
+
}
|
|
906
|
+
break;
|
|
907
|
+
}
|
|
908
|
+
// ── copy_file ─────────────────────────────────────────
|
|
909
|
+
case 'copy_file': {
|
|
910
|
+
const fromPath = join(projectDir, args.from);
|
|
911
|
+
const toPath = join(projectDir, args.to);
|
|
912
|
+
try {
|
|
913
|
+
await mkdir(dirname(toPath), { recursive: true });
|
|
914
|
+
await copyFile(fromPath, toPath);
|
|
915
|
+
result = `Copied ${args.from} → ${args.to}`;
|
|
916
|
+
}
|
|
917
|
+
catch (err) {
|
|
918
|
+
result = `Error copying: ${err instanceof Error ? err.message : String(err)}`;
|
|
919
|
+
success = false;
|
|
920
|
+
}
|
|
921
|
+
break;
|
|
922
|
+
}
|
|
923
|
+
// ── list_files ────────────────────────────────────────
|
|
924
|
+
case 'list_files': {
|
|
925
|
+
const dirPath = join(projectDir, args.path || '.');
|
|
926
|
+
const recursive = args.recursive;
|
|
927
|
+
if (recursive) {
|
|
928
|
+
const tree = await listTree(dirPath, projectDir);
|
|
929
|
+
result = tree.length > 0 ? tree.join('\n') : '(empty)';
|
|
930
|
+
}
|
|
931
|
+
else {
|
|
932
|
+
try {
|
|
933
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
934
|
+
const lines = [];
|
|
935
|
+
for (const entry of entries) {
|
|
936
|
+
if (entry.name === '.git' || entry.name === 'node_modules')
|
|
937
|
+
continue;
|
|
938
|
+
if (entry.isDirectory()) {
|
|
939
|
+
lines.push(` [dir] ${entry.name}/`);
|
|
940
|
+
}
|
|
941
|
+
else {
|
|
942
|
+
try {
|
|
943
|
+
const s = await stat(join(dirPath, entry.name));
|
|
944
|
+
const size = s.size < 1024 ? `${s.size}B` : `${(s.size / 1024).toFixed(1)}K`;
|
|
945
|
+
lines.push(` ${size.padStart(7)} ${entry.name}`);
|
|
946
|
+
}
|
|
947
|
+
catch {
|
|
948
|
+
lines.push(` ${entry.name}`);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
result = lines.length > 0 ? lines.join('\n') : '(empty directory)';
|
|
953
|
+
}
|
|
954
|
+
catch {
|
|
955
|
+
result = `Error: Directory not found: ${args.path}`;
|
|
956
|
+
success = false;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
break;
|
|
960
|
+
}
|
|
961
|
+
// ── glob ──────────────────────────────────────────────
|
|
962
|
+
case 'glob': {
|
|
963
|
+
const pattern = args.pattern;
|
|
964
|
+
const basePath = join(projectDir, args.path || '.');
|
|
965
|
+
try {
|
|
966
|
+
const { glob: globFn } = await import('glob');
|
|
967
|
+
const matches = await globFn(pattern, {
|
|
968
|
+
cwd: basePath,
|
|
969
|
+
nodir: true,
|
|
970
|
+
ignore: ['node_modules/**', '.git/**', 'dist/**'],
|
|
971
|
+
});
|
|
972
|
+
result = matches.length > 0
|
|
973
|
+
? `${matches.length} file(s) found:\n${matches.slice(0, 100).join('\n')}${matches.length > 100 ? '\n... (truncated)' : ''}`
|
|
974
|
+
: 'No files matched.';
|
|
975
|
+
}
|
|
976
|
+
catch (err) {
|
|
977
|
+
result = `Glob error: ${err instanceof Error ? err.message : String(err)}`;
|
|
978
|
+
success = false;
|
|
979
|
+
}
|
|
980
|
+
break;
|
|
981
|
+
}
|
|
982
|
+
// ── grep ──────────────────────────────────────────────
|
|
983
|
+
case 'grep': {
|
|
984
|
+
const pattern = args.pattern;
|
|
985
|
+
const searchPath = join(projectDir, args.path || '.');
|
|
986
|
+
const fileType = args.file_type;
|
|
987
|
+
const contextLines = args.context || 0;
|
|
988
|
+
const caseInsensitive = args.case_insensitive;
|
|
989
|
+
const maxResults = args.max_results || 50;
|
|
990
|
+
const grepCmd = IS_WIN ? 'findstr' : 'grep';
|
|
991
|
+
if (IS_WIN) {
|
|
992
|
+
// Use findstr on Windows
|
|
993
|
+
const findstrArgs = [
|
|
994
|
+
caseInsensitive ? '/I' : '',
|
|
995
|
+
'/S', '/N',
|
|
996
|
+
`/R`, `"${pattern}"`,
|
|
997
|
+
fileType ? `*.${fileType}` : '*.*',
|
|
998
|
+
].filter(Boolean).join(' ');
|
|
999
|
+
const { output, success: ok } = await runShell(`findstr ${findstrArgs}`, searchPath, 15_000);
|
|
1000
|
+
if (!ok && output.includes('Exit code')) {
|
|
1001
|
+
result = 'No matches found.';
|
|
1002
|
+
}
|
|
1003
|
+
else {
|
|
1004
|
+
const lines = output.split('\n').slice(0, maxResults);
|
|
1005
|
+
result = lines.join('\n') + (lines.length >= maxResults ? `\n... (showing first ${maxResults})` : '');
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
else {
|
|
1009
|
+
// Use grep on Unix
|
|
1010
|
+
const grepArgs = ['-rn'];
|
|
1011
|
+
if (caseInsensitive)
|
|
1012
|
+
grepArgs.push('-i');
|
|
1013
|
+
if (contextLines > 0)
|
|
1014
|
+
grepArgs.push(`-C${contextLines}`);
|
|
1015
|
+
if (fileType)
|
|
1016
|
+
grepArgs.push(`--include=*.${fileType}`);
|
|
1017
|
+
grepArgs.push('--exclude-dir=node_modules', '--exclude-dir=.git', '--exclude-dir=dist');
|
|
1018
|
+
grepArgs.push('-e', pattern, searchPath);
|
|
1019
|
+
try {
|
|
1020
|
+
const { stdout } = await execFileAsync('grep', grepArgs, {
|
|
1021
|
+
timeout: 15_000,
|
|
1022
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
1023
|
+
});
|
|
1024
|
+
const lines = stdout.split('\n')
|
|
1025
|
+
.filter(Boolean)
|
|
1026
|
+
.map(l => l.replace(projectDir + '/', ''))
|
|
1027
|
+
.slice(0, maxResults);
|
|
1028
|
+
result = lines.length > 0
|
|
1029
|
+
? lines.join('\n') + (lines.length >= maxResults ? `\n... (showing first ${maxResults})` : '')
|
|
1030
|
+
: 'No matches found.';
|
|
1031
|
+
}
|
|
1032
|
+
catch (err) {
|
|
1033
|
+
const e = err;
|
|
1034
|
+
result = e.code === 1 ? 'No matches found.' : `Grep error: ${err instanceof Error ? err.message : String(err)}`;
|
|
1035
|
+
success = e.code === 1;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
break;
|
|
1039
|
+
}
|
|
1040
|
+
// ── bash ──────────────────────────────────────────────
|
|
1041
|
+
case 'bash': {
|
|
1042
|
+
const command = args.command;
|
|
1043
|
+
const timeout = args.timeout || 120_000;
|
|
1044
|
+
// Block truly dangerous operations
|
|
1045
|
+
const blocked = ['rm -rf /', 'format c:', 'del /f /s /q c:', 'mkfs', ':(){'];
|
|
1046
|
+
if (blocked.some(b => command.includes(b))) {
|
|
1047
|
+
result = 'Error: Dangerous command blocked.';
|
|
1048
|
+
success = false;
|
|
1049
|
+
break;
|
|
1050
|
+
}
|
|
1051
|
+
const r = await runShell(command, projectDir, timeout);
|
|
1052
|
+
result = r.output;
|
|
1053
|
+
success = r.success;
|
|
1054
|
+
break;
|
|
1055
|
+
}
|
|
1056
|
+
// ── run_build ─────────────────────────────────────────
|
|
1057
|
+
case 'run_build': {
|
|
1058
|
+
const r = await runShell('npx vite build', projectDir, 60_000);
|
|
1059
|
+
result = (r.success ? 'BUILD SUCCESS\n' : 'BUILD FAILED\n') + r.output;
|
|
1060
|
+
success = r.success;
|
|
1061
|
+
break;
|
|
1062
|
+
}
|
|
1063
|
+
// ── run_typecheck ─────────────────────────────────────
|
|
1064
|
+
case 'run_typecheck': {
|
|
1065
|
+
const r = await runShell('npx tsc --noEmit', projectDir, 60_000);
|
|
1066
|
+
result = (r.success ? 'TYPECHECK PASSED\n' : 'TYPECHECK ERRORS\n') + r.output;
|
|
1067
|
+
success = r.success;
|
|
1068
|
+
break;
|
|
1069
|
+
}
|
|
1070
|
+
// ── git_status ────────────────────────────────────────
|
|
1071
|
+
case 'git_status': {
|
|
1072
|
+
const r = await runShell('git status', projectDir);
|
|
1073
|
+
result = r.output;
|
|
1074
|
+
success = r.success;
|
|
1075
|
+
break;
|
|
1076
|
+
}
|
|
1077
|
+
// ── git_diff ──────────────────────────────────────────
|
|
1078
|
+
case 'git_diff': {
|
|
1079
|
+
let cmd = 'git diff';
|
|
1080
|
+
if (args.staged)
|
|
1081
|
+
cmd += ' --cached';
|
|
1082
|
+
if (args.ref)
|
|
1083
|
+
cmd += ` ${args.ref}`;
|
|
1084
|
+
if (args.path)
|
|
1085
|
+
cmd += ` -- ${args.path}`;
|
|
1086
|
+
const r = await runShell(cmd, projectDir);
|
|
1087
|
+
result = r.output || '(no changes)';
|
|
1088
|
+
success = r.success;
|
|
1089
|
+
break;
|
|
1090
|
+
}
|
|
1091
|
+
// ── git_log ───────────────────────────────────────────
|
|
1092
|
+
case 'git_log': {
|
|
1093
|
+
const count = args.count || 10;
|
|
1094
|
+
const oneline = args.oneline !== false;
|
|
1095
|
+
const fmt = oneline ? '--oneline' : '--format=medium';
|
|
1096
|
+
const r = await runShell(`git log ${fmt} -${count}`, projectDir);
|
|
1097
|
+
result = r.output;
|
|
1098
|
+
success = r.success;
|
|
1099
|
+
break;
|
|
1100
|
+
}
|
|
1101
|
+
// ── git_commit ────────────────────────────────────────
|
|
1102
|
+
case 'git_commit': {
|
|
1103
|
+
const message = args.message;
|
|
1104
|
+
const files = args.files || ['.'];
|
|
1105
|
+
// Stage files
|
|
1106
|
+
const stageCmd = files.map(f => `git add "${f}"`).join(' && ');
|
|
1107
|
+
const stageR = await runShell(stageCmd, projectDir);
|
|
1108
|
+
if (!stageR.success) {
|
|
1109
|
+
result = `Staging failed: ${stageR.output}`;
|
|
1110
|
+
success = false;
|
|
1111
|
+
break;
|
|
1112
|
+
}
|
|
1113
|
+
// Commit
|
|
1114
|
+
const commitR = await runShell(`git commit -m "${message.replace(/"/g, '\\"')}"`, projectDir);
|
|
1115
|
+
result = commitR.output;
|
|
1116
|
+
success = commitR.success;
|
|
1117
|
+
break;
|
|
1118
|
+
}
|
|
1119
|
+
// ── npm_install ───────────────────────────────────────
|
|
1120
|
+
case 'npm_install': {
|
|
1121
|
+
const packages = args.packages;
|
|
1122
|
+
const isDev = args.dev;
|
|
1123
|
+
let cmd = 'npm install';
|
|
1124
|
+
if (packages && packages.length > 0) {
|
|
1125
|
+
cmd += ` ${packages.join(' ')}`;
|
|
1126
|
+
if (isDev)
|
|
1127
|
+
cmd += ' --save-dev';
|
|
1128
|
+
}
|
|
1129
|
+
const r = await runShell(cmd, projectDir, 120_000);
|
|
1130
|
+
result = r.output;
|
|
1131
|
+
success = r.success;
|
|
1132
|
+
break;
|
|
1133
|
+
}
|
|
1134
|
+
// ── file_info ─────────────────────────────────────────
|
|
1135
|
+
case 'file_info': {
|
|
1136
|
+
const filePath = join(projectDir, args.path);
|
|
1137
|
+
try {
|
|
1138
|
+
const s = await stat(filePath);
|
|
1139
|
+
const content = await readFile(filePath, 'utf-8');
|
|
1140
|
+
const lines = content.split('\n').length;
|
|
1141
|
+
const ext = extname(filePath);
|
|
1142
|
+
const size = s.size < 1024 ? `${s.size} bytes` : s.size < 1048576 ? `${(s.size / 1024).toFixed(1)} KB` : `${(s.size / 1048576).toFixed(1)} MB`;
|
|
1143
|
+
result = [
|
|
1144
|
+
`File: ${args.path}`,
|
|
1145
|
+
`Size: ${size}`,
|
|
1146
|
+
`Lines: ${lines}`,
|
|
1147
|
+
`Type: ${ext || 'unknown'}`,
|
|
1148
|
+
`Modified: ${s.mtime.toISOString()}`,
|
|
1149
|
+
`Created: ${s.birthtime.toISOString()}`,
|
|
1150
|
+
].join('\n');
|
|
1151
|
+
}
|
|
1152
|
+
catch {
|
|
1153
|
+
result = `Error: File not found: ${args.path}`;
|
|
1154
|
+
success = false;
|
|
1155
|
+
}
|
|
1156
|
+
break;
|
|
1157
|
+
}
|
|
1158
|
+
// ── project_overview ──────────────────────────────────
|
|
1159
|
+
case 'project_overview': {
|
|
1160
|
+
const sections = [];
|
|
1161
|
+
// Directory tree
|
|
1162
|
+
sections.push('=== Project Structure ===');
|
|
1163
|
+
const tree = await listTree(projectDir, projectDir);
|
|
1164
|
+
sections.push(tree.slice(0, 50).join('\n'));
|
|
1165
|
+
if (tree.length > 50)
|
|
1166
|
+
sections.push(`... (${tree.length - 50} more entries)`);
|
|
1167
|
+
// package.json summary
|
|
1168
|
+
try {
|
|
1169
|
+
const pkgRaw = await readFile(join(projectDir, 'package.json'), 'utf-8');
|
|
1170
|
+
const pkg = JSON.parse(pkgRaw);
|
|
1171
|
+
sections.push('\n=== package.json ===');
|
|
1172
|
+
if (pkg.name)
|
|
1173
|
+
sections.push(`Name: ${pkg.name}`);
|
|
1174
|
+
if (pkg.version)
|
|
1175
|
+
sections.push(`Version: ${pkg.version}`);
|
|
1176
|
+
if (pkg.scripts) {
|
|
1177
|
+
sections.push(`Scripts: ${Object.keys(pkg.scripts).join(', ')}`);
|
|
1178
|
+
}
|
|
1179
|
+
if (pkg.dependencies) {
|
|
1180
|
+
sections.push(`Dependencies: ${Object.keys(pkg.dependencies).join(', ')}`);
|
|
1181
|
+
}
|
|
1182
|
+
if (pkg.devDependencies) {
|
|
1183
|
+
sections.push(`DevDependencies: ${Object.keys(pkg.devDependencies).join(', ')}`);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
catch { /* no package.json */ }
|
|
1187
|
+
// tsconfig summary
|
|
1188
|
+
try {
|
|
1189
|
+
const tscRaw = await readFile(join(projectDir, 'tsconfig.json'), 'utf-8');
|
|
1190
|
+
const tsc = JSON.parse(tscRaw);
|
|
1191
|
+
sections.push('\n=== tsconfig.json ===');
|
|
1192
|
+
if (tsc.compilerOptions?.target)
|
|
1193
|
+
sections.push(`Target: ${tsc.compilerOptions.target}`);
|
|
1194
|
+
if (tsc.compilerOptions?.module)
|
|
1195
|
+
sections.push(`Module: ${tsc.compilerOptions.module}`);
|
|
1196
|
+
if (tsc.compilerOptions?.jsx)
|
|
1197
|
+
sections.push(`JSX: ${tsc.compilerOptions.jsx}`);
|
|
1198
|
+
if (tsc.compilerOptions?.strict !== undefined)
|
|
1199
|
+
sections.push(`Strict: ${tsc.compilerOptions.strict}`);
|
|
1200
|
+
}
|
|
1201
|
+
catch { /* no tsconfig */ }
|
|
1202
|
+
result = sections.join('\n');
|
|
1203
|
+
break;
|
|
1204
|
+
}
|
|
1205
|
+
// ── generate_design_guide ─────────────────────────────
|
|
1206
|
+
case 'generate_design_guide': {
|
|
1207
|
+
const projectType = args.project_type;
|
|
1208
|
+
const userRequest = args.user_request;
|
|
1209
|
+
try {
|
|
1210
|
+
const { guide, design } = await generateDesignGuide(projectType, userRequest);
|
|
1211
|
+
// Save to project for reference
|
|
1212
|
+
await writeFile(join(projectDir, '.design-guide.md'), guide, 'utf-8');
|
|
1213
|
+
result = `Design guide generated and saved to .design-guide.md\n\n${guide}`;
|
|
1214
|
+
}
|
|
1215
|
+
catch (err) {
|
|
1216
|
+
const fallback = getFallbackGuide(projectType);
|
|
1217
|
+
await writeFile(join(projectDir, '.design-guide.md'), fallback, 'utf-8');
|
|
1218
|
+
result = `Design API failed (using fallback). Saved to .design-guide.md\n\n${fallback}`;
|
|
1219
|
+
success = false;
|
|
1220
|
+
}
|
|
1221
|
+
break;
|
|
1222
|
+
}
|
|
1223
|
+
// ── todo_list ──────────────────────────────────────────
|
|
1224
|
+
case 'todo_list': {
|
|
1225
|
+
const store = await loadTodos(projectDir);
|
|
1226
|
+
const filter = args.filter || 'all';
|
|
1227
|
+
result = formatTodoList(store, filter);
|
|
1228
|
+
break;
|
|
1229
|
+
}
|
|
1230
|
+
// ── todo_add ───────────────────────────────────────────
|
|
1231
|
+
case 'todo_add': {
|
|
1232
|
+
const store = await loadTodos(projectDir);
|
|
1233
|
+
const newItem = {
|
|
1234
|
+
id: store.nextId++,
|
|
1235
|
+
title: args.title,
|
|
1236
|
+
description: args.description,
|
|
1237
|
+
status: 'pending',
|
|
1238
|
+
priority: args.priority || 'medium',
|
|
1239
|
+
parentId: args.parent_id,
|
|
1240
|
+
createdAt: new Date().toISOString(),
|
|
1241
|
+
updatedAt: new Date().toISOString(),
|
|
1242
|
+
};
|
|
1243
|
+
store.items.push(newItem);
|
|
1244
|
+
await saveTodos(projectDir, store);
|
|
1245
|
+
const parentInfo = newItem.parentId ? ` (subtask of #${newItem.parentId})` : '';
|
|
1246
|
+
result = `Added todo #${newItem.id}: "${newItem.title}" [${newItem.priority}]${parentInfo}`;
|
|
1247
|
+
break;
|
|
1248
|
+
}
|
|
1249
|
+
// ── todo_update ────────────────────────────────────────
|
|
1250
|
+
case 'todo_update': {
|
|
1251
|
+
const store = await loadTodos(projectDir);
|
|
1252
|
+
const id = args.id;
|
|
1253
|
+
const item = store.items.find(i => i.id === id);
|
|
1254
|
+
if (!item) {
|
|
1255
|
+
result = `Error: Todo #${id} not found.`;
|
|
1256
|
+
success = false;
|
|
1257
|
+
break;
|
|
1258
|
+
}
|
|
1259
|
+
const changes = [];
|
|
1260
|
+
if (args.status) {
|
|
1261
|
+
item.status = args.status;
|
|
1262
|
+
changes.push(`status → ${item.status}`);
|
|
1263
|
+
}
|
|
1264
|
+
if (args.title) {
|
|
1265
|
+
item.title = args.title;
|
|
1266
|
+
changes.push(`title updated`);
|
|
1267
|
+
}
|
|
1268
|
+
if (args.description !== undefined) {
|
|
1269
|
+
item.description = args.description;
|
|
1270
|
+
changes.push(`description updated`);
|
|
1271
|
+
}
|
|
1272
|
+
if (args.priority) {
|
|
1273
|
+
item.priority = args.priority;
|
|
1274
|
+
changes.push(`priority → ${item.priority}`);
|
|
1275
|
+
}
|
|
1276
|
+
item.updatedAt = new Date().toISOString();
|
|
1277
|
+
await saveTodos(projectDir, store);
|
|
1278
|
+
result = `Updated todo #${id}: ${changes.join(', ')}`;
|
|
1279
|
+
break;
|
|
1280
|
+
}
|
|
1281
|
+
// ── todo_remove ────────────────────────────────────────
|
|
1282
|
+
case 'todo_remove': {
|
|
1283
|
+
const store = await loadTodos(projectDir);
|
|
1284
|
+
const id = args.id;
|
|
1285
|
+
const idx = store.items.findIndex(i => i.id === id);
|
|
1286
|
+
if (idx === -1) {
|
|
1287
|
+
result = `Error: Todo #${id} not found.`;
|
|
1288
|
+
success = false;
|
|
1289
|
+
break;
|
|
1290
|
+
}
|
|
1291
|
+
const removed = store.items.splice(idx, 1)[0];
|
|
1292
|
+
// Also remove subtasks
|
|
1293
|
+
const subtaskIds = store.items.filter(i => i.parentId === id).map(i => i.id);
|
|
1294
|
+
store.items = store.items.filter(i => i.parentId !== id);
|
|
1295
|
+
await saveTodos(projectDir, store);
|
|
1296
|
+
result = `Removed todo #${id}: "${removed.title}"`;
|
|
1297
|
+
if (subtaskIds.length > 0) {
|
|
1298
|
+
result += ` (and ${subtaskIds.length} subtask(s))`;
|
|
1299
|
+
}
|
|
1300
|
+
break;
|
|
1301
|
+
}
|
|
1302
|
+
// ── todo_batch ─────────────────────────────────────────
|
|
1303
|
+
case 'todo_batch': {
|
|
1304
|
+
const store = await loadTodos(projectDir);
|
|
1305
|
+
const operations = args.operations;
|
|
1306
|
+
const results = [];
|
|
1307
|
+
for (const op of operations) {
|
|
1308
|
+
switch (op.action) {
|
|
1309
|
+
case 'add': {
|
|
1310
|
+
const newItem = {
|
|
1311
|
+
id: store.nextId++,
|
|
1312
|
+
title: op.title || 'Untitled',
|
|
1313
|
+
description: op.description,
|
|
1314
|
+
status: 'pending',
|
|
1315
|
+
priority: op.priority || 'medium',
|
|
1316
|
+
createdAt: new Date().toISOString(),
|
|
1317
|
+
updatedAt: new Date().toISOString(),
|
|
1318
|
+
};
|
|
1319
|
+
store.items.push(newItem);
|
|
1320
|
+
results.push(`+ Added #${newItem.id}: "${newItem.title}"`);
|
|
1321
|
+
break;
|
|
1322
|
+
}
|
|
1323
|
+
case 'update': {
|
|
1324
|
+
const item = store.items.find(i => i.id === op.id);
|
|
1325
|
+
if (item) {
|
|
1326
|
+
if (op.status)
|
|
1327
|
+
item.status = op.status;
|
|
1328
|
+
if (op.title)
|
|
1329
|
+
item.title = op.title;
|
|
1330
|
+
if (op.priority)
|
|
1331
|
+
item.priority = op.priority;
|
|
1332
|
+
item.updatedAt = new Date().toISOString();
|
|
1333
|
+
results.push(`~ Updated #${item.id}: ${op.status || op.title || 'modified'}`);
|
|
1334
|
+
}
|
|
1335
|
+
else {
|
|
1336
|
+
results.push(`! #${op.id} not found`);
|
|
1337
|
+
}
|
|
1338
|
+
break;
|
|
1339
|
+
}
|
|
1340
|
+
case 'remove': {
|
|
1341
|
+
const idx = store.items.findIndex(i => i.id === op.id);
|
|
1342
|
+
if (idx >= 0) {
|
|
1343
|
+
const removed = store.items.splice(idx, 1)[0];
|
|
1344
|
+
results.push(`- Removed #${removed.id}: "${removed.title}"`);
|
|
1345
|
+
}
|
|
1346
|
+
else {
|
|
1347
|
+
results.push(`! #${op.id} not found`);
|
|
1348
|
+
}
|
|
1349
|
+
break;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
await saveTodos(projectDir, store);
|
|
1354
|
+
result = `Batch: ${operations.length} operation(s)\n${results.join('\n')}`;
|
|
1355
|
+
break;
|
|
1356
|
+
}
|
|
1357
|
+
default:
|
|
1358
|
+
result = `Unknown tool: ${name}`;
|
|
1359
|
+
success = false;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
catch (err) {
|
|
1363
|
+
result = `Tool error: ${err instanceof Error ? err.message : String(err)}`;
|
|
1364
|
+
success = false;
|
|
1365
|
+
}
|
|
1366
|
+
onToolEnd?.(name, result, success);
|
|
1367
|
+
return result;
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
//# sourceMappingURL=tools.js.map
|