claude-code-workflow 6.0.4 → 6.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.
Files changed (120) hide show
  1. package/.claude/agents/action-planning-agent.md +1 -1
  2. package/.claude/agents/cli-execution-agent.md +269 -269
  3. package/.claude/agents/cli-explore-agent.md +182 -182
  4. package/.claude/agents/context-search-agent.md +582 -582
  5. package/.claude/agents/memory-bridge.md +93 -93
  6. package/.claude/commands/cli/cli-init.md +1 -1
  7. package/.claude/commands/memory/docs-full-cli.md +471 -471
  8. package/.claude/commands/memory/docs-related-cli.md +386 -386
  9. package/.claude/commands/memory/docs.md +615 -615
  10. package/.claude/commands/memory/load.md +1 -1
  11. package/.claude/commands/memory/update-full.md +332 -332
  12. package/.claude/commands/memory/update-related.md +5 -5
  13. package/.claude/commands/workflow/init.md +1 -1
  14. package/.claude/commands/workflow/lite-fix.md +621 -621
  15. package/.claude/commands/workflow/lite-plan.md +592 -592
  16. package/.claude/commands/workflow/tools/context-gather.md +434 -434
  17. package/.claude/commands/workflow/ui-design/generate.md +504 -504
  18. package/.claude/commands/workflow/ui-design/import-from-code.md +537 -537
  19. package/.claude/scripts/classify-folders.sh +4 -0
  20. package/.claude/scripts/convert_tokens_to_css.sh +4 -0
  21. package/.claude/scripts/detect_changed_modules.sh +5 -1
  22. package/.claude/scripts/discover-design-files.sh +87 -83
  23. package/.claude/scripts/generate_module_docs.sh +717 -713
  24. package/.claude/scripts/get_modules_by_depth.sh +5 -1
  25. package/.claude/scripts/ui-generate-preview.sh +4 -0
  26. package/.claude/scripts/ui-instantiate-prototypes.sh +4 -0
  27. package/.claude/scripts/update_module_claude.sh +4 -0
  28. package/.claude/skills/command-guide/index/all-commands.json +1 -12
  29. package/.claude/skills/command-guide/index/by-category.json +1 -12
  30. package/.claude/skills/command-guide/index/by-use-case.json +1 -12
  31. package/.claude/skills/command-guide/index/essential-commands.json +1 -12
  32. package/.claude/skills/command-guide/reference/agents/action-planning-agent.md +127 -71
  33. package/.claude/skills/command-guide/reference/agents/cli-execution-agent.md +269 -269
  34. package/.claude/skills/command-guide/reference/agents/cli-explore-agent.md +182 -182
  35. package/.claude/skills/command-guide/reference/agents/conceptual-planning-agent.md +18 -38
  36. package/.claude/skills/command-guide/reference/agents/context-search-agent.md +582 -577
  37. package/.claude/skills/command-guide/reference/agents/memory-bridge.md +93 -93
  38. package/.claude/skills/command-guide/reference/commands/cli/cli-init.md +1 -1
  39. package/.claude/skills/command-guide/reference/commands/memory/docs-full-cli.md +471 -471
  40. package/.claude/skills/command-guide/reference/commands/memory/docs-related-cli.md +386 -386
  41. package/.claude/skills/command-guide/reference/commands/memory/docs.md +615 -610
  42. package/.claude/skills/command-guide/reference/commands/memory/load.md +1 -1
  43. package/.claude/skills/command-guide/reference/commands/memory/update-full.md +332 -332
  44. package/.claude/skills/command-guide/reference/commands/memory/update-related.md +5 -5
  45. package/.claude/skills/command-guide/reference/commands/workflow/brainstorm/artifacts.md +299 -451
  46. package/.claude/skills/command-guide/reference/commands/workflow/brainstorm/auto-parallel.md +14 -37
  47. package/.claude/skills/command-guide/reference/commands/workflow/brainstorm/synthesis.md +252 -350
  48. package/.claude/skills/command-guide/reference/commands/workflow/init.md +2 -2
  49. package/.claude/skills/command-guide/reference/commands/workflow/lite-execute.md +52 -0
  50. package/.claude/skills/command-guide/reference/commands/workflow/lite-fix.md +621 -602
  51. package/.claude/skills/command-guide/reference/commands/workflow/lite-plan.md +46 -36
  52. package/.claude/skills/command-guide/reference/commands/workflow/review-fix.md +18 -58
  53. package/.claude/skills/command-guide/reference/commands/workflow/review-module-cycle.md +22 -52
  54. package/.claude/skills/command-guide/reference/commands/workflow/review-session-cycle.md +19 -48
  55. package/.claude/skills/command-guide/reference/commands/workflow/session/start.md +25 -5
  56. package/.claude/skills/command-guide/reference/commands/workflow/tdd-plan.md +1 -1
  57. package/.claude/skills/command-guide/reference/commands/workflow/test-fix-gen.md +7 -7
  58. package/.claude/skills/command-guide/reference/commands/workflow/tools/context-gather.md +434 -434
  59. package/.claude/skills/command-guide/reference/commands/workflow/tools/task-generate-agent.md +151 -11
  60. package/.claude/skills/command-guide/reference/commands/workflow/tools/task-generate-tdd.md +4 -4
  61. package/.claude/skills/command-guide/reference/commands/workflow/tools/test-task-generate.md +1 -1
  62. package/.claude/skills/command-guide/reference/commands/workflow/ui-design/generate.md +504 -504
  63. package/.claude/skills/command-guide/reference/commands/workflow/ui-design/import-from-code.md +537 -537
  64. package/.claude/workflows/context-search-strategy.md +77 -77
  65. package/.claude/workflows/tool-strategy.md +90 -71
  66. package/.claude/workflows/workflow-architecture.md +1 -1
  67. package/ccw/package.json +6 -6
  68. package/ccw/src/cli.js +16 -0
  69. package/ccw/src/commands/stop.js +101 -0
  70. package/ccw/src/commands/tool.js +181 -0
  71. package/ccw/src/core/dashboard-generator.js +18 -3
  72. package/ccw/src/core/lite-scanner.js +35 -11
  73. package/ccw/src/core/server.js +583 -17
  74. package/ccw/src/templates/dashboard-css/01-base.css +161 -0
  75. package/ccw/src/templates/dashboard-css/02-session.css +726 -0
  76. package/ccw/src/templates/dashboard-css/03-tasks.css +512 -0
  77. package/ccw/src/templates/dashboard-css/04-lite-tasks.css +843 -0
  78. package/ccw/src/templates/dashboard-css/05-context.css +2206 -0
  79. package/ccw/src/templates/dashboard-css/06-cards.css +1570 -0
  80. package/ccw/src/templates/dashboard-css/07-managers.css +936 -0
  81. package/ccw/src/templates/dashboard-css/08-review.css +1266 -0
  82. package/ccw/src/templates/dashboard-css/09-explorer.css +1397 -0
  83. package/ccw/src/templates/dashboard-js/components/global-notifications.js +219 -0
  84. package/ccw/src/templates/dashboard-js/components/hook-manager.js +10 -0
  85. package/ccw/src/templates/dashboard-js/components/mcp-manager.js +24 -2
  86. package/ccw/src/templates/dashboard-js/components/navigation.js +11 -5
  87. package/ccw/src/templates/dashboard-js/components/tabs-context.js +20 -20
  88. package/ccw/src/templates/dashboard-js/components/tabs-other.js +11 -11
  89. package/ccw/src/templates/dashboard-js/components/theme.js +29 -1
  90. package/ccw/src/templates/dashboard-js/main.js +4 -0
  91. package/ccw/src/templates/dashboard-js/state.js +5 -0
  92. package/ccw/src/templates/dashboard-js/views/explorer.js +852 -0
  93. package/ccw/src/templates/dashboard-js/views/home.js +13 -9
  94. package/ccw/src/templates/dashboard-js/views/hook-manager.js +8 -5
  95. package/ccw/src/templates/dashboard-js/views/lite-tasks.js +21 -16
  96. package/ccw/src/templates/dashboard-js/views/mcp-manager.js +148 -8
  97. package/ccw/src/templates/dashboard-js/views/project-overview.js +15 -11
  98. package/ccw/src/templates/dashboard-js/views/review-session.js +3 -3
  99. package/ccw/src/templates/dashboard-js/views/session-detail.js +38 -28
  100. package/ccw/src/templates/dashboard.html +129 -28
  101. package/ccw/src/tools/classify-folders.js +204 -0
  102. package/ccw/src/tools/convert-tokens-to-css.js +250 -0
  103. package/ccw/src/tools/detect-changed-modules.js +288 -0
  104. package/ccw/src/tools/discover-design-files.js +134 -0
  105. package/ccw/src/tools/edit-file.js +266 -0
  106. package/ccw/src/tools/generate-module-docs.js +416 -0
  107. package/ccw/src/tools/get-modules-by-depth.js +308 -0
  108. package/ccw/src/tools/index.js +176 -0
  109. package/ccw/src/tools/ui-generate-preview.js +327 -0
  110. package/ccw/src/tools/ui-instantiate-prototypes.js +301 -0
  111. package/ccw/src/tools/update-module-claude.js +380 -0
  112. package/ccw/src/utils/browser-launcher.js +15 -4
  113. package/package.json +1 -1
  114. package/.claude/skills/command-guide/reference/commands/workflow/status.md +0 -352
  115. package/ccw/src/core/server.js.bak +0 -385
  116. package/ccw/src/core/server_original.bak +0 -385
  117. package/ccw/src/templates/dashboard.css +0 -8114
  118. package/ccw/src/templates/dashboard_tailwind.html +0 -42
  119. package/ccw/src/templates/dashboard_test.html +0 -37
  120. package/ccw/src/templates/tailwind-base.css +0 -212
@@ -0,0 +1,288 @@
1
+ /**
2
+ * Detect Changed Modules Tool
3
+ * Find modules affected by git changes or recent modifications
4
+ */
5
+
6
+ import { readdirSync, statSync, existsSync, readFileSync } from 'fs';
7
+ import { join, resolve, dirname, extname, relative } from 'path';
8
+ import { execSync } from 'child_process';
9
+
10
+ // Source file extensions to track
11
+ const SOURCE_EXTENSIONS = [
12
+ '.md', '.js', '.ts', '.jsx', '.tsx',
13
+ '.py', '.go', '.rs', '.java', '.cpp', '.c', '.h',
14
+ '.sh', '.ps1', '.json', '.yaml', '.yml'
15
+ ];
16
+
17
+ // Directories to exclude
18
+ const EXCLUDE_DIRS = [
19
+ '.git', '__pycache__', 'node_modules', '.venv', 'venv', 'env',
20
+ 'dist', 'build', '.cache', '.pytest_cache', '.mypy_cache',
21
+ 'coverage', '.nyc_output', 'logs', 'tmp', 'temp'
22
+ ];
23
+
24
+ /**
25
+ * Check if git is available and we're in a repo
26
+ */
27
+ function isGitRepo(basePath) {
28
+ try {
29
+ execSync('git rev-parse --git-dir', { cwd: basePath, stdio: 'pipe' });
30
+ return true;
31
+ } catch (e) {
32
+ return false;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Get changed files from git
38
+ */
39
+ function getGitChangedFiles(basePath) {
40
+ try {
41
+ // Get staged + unstaged changes
42
+ let output = execSync('git diff --name-only HEAD 2>/dev/null', {
43
+ cwd: basePath,
44
+ encoding: 'utf8',
45
+ stdio: ['pipe', 'pipe', 'pipe']
46
+ }).trim();
47
+
48
+ const cachedOutput = execSync('git diff --name-only --cached 2>/dev/null', {
49
+ cwd: basePath,
50
+ encoding: 'utf8',
51
+ stdio: ['pipe', 'pipe', 'pipe']
52
+ }).trim();
53
+
54
+ if (cachedOutput) {
55
+ output = output ? `${output}\n${cachedOutput}` : cachedOutput;
56
+ }
57
+
58
+ // If no working changes, check last commit
59
+ if (!output) {
60
+ output = execSync('git diff --name-only HEAD~1 HEAD 2>/dev/null', {
61
+ cwd: basePath,
62
+ encoding: 'utf8',
63
+ stdio: ['pipe', 'pipe', 'pipe']
64
+ }).trim();
65
+ }
66
+
67
+ return output ? output.split('\n').filter(f => f.trim()) : [];
68
+ } catch (e) {
69
+ return [];
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Find recently modified files (fallback when no git changes)
75
+ */
76
+ function findRecentlyModified(basePath, hoursAgo = 24) {
77
+ const results = [];
78
+ const cutoffTime = Date.now() - (hoursAgo * 60 * 60 * 1000);
79
+
80
+ function scan(dirPath) {
81
+ try {
82
+ const entries = readdirSync(dirPath, { withFileTypes: true });
83
+
84
+ for (const entry of entries) {
85
+ if (entry.isDirectory()) {
86
+ if (EXCLUDE_DIRS.includes(entry.name)) continue;
87
+ scan(join(dirPath, entry.name));
88
+ } else if (entry.isFile()) {
89
+ const ext = extname(entry.name).toLowerCase();
90
+ if (!SOURCE_EXTENSIONS.includes(ext)) continue;
91
+
92
+ const fullPath = join(dirPath, entry.name);
93
+ try {
94
+ const stat = statSync(fullPath);
95
+ if (stat.mtimeMs > cutoffTime) {
96
+ results.push(relative(basePath, fullPath));
97
+ }
98
+ } catch (e) {
99
+ // Skip files we can't stat
100
+ }
101
+ }
102
+ }
103
+ } catch (e) {
104
+ // Ignore permission errors
105
+ }
106
+ }
107
+
108
+ scan(basePath);
109
+ return results;
110
+ }
111
+
112
+ /**
113
+ * Extract unique parent directories from file list
114
+ */
115
+ function extractDirectories(files, basePath) {
116
+ const dirs = new Set();
117
+
118
+ for (const file of files) {
119
+ const dir = dirname(file);
120
+ if (dir === '.' || dir === '') {
121
+ dirs.add('.');
122
+ } else {
123
+ dirs.add('./' + dir.replace(/\\/g, '/'));
124
+ }
125
+ }
126
+
127
+ return Array.from(dirs).sort();
128
+ }
129
+
130
+ /**
131
+ * Count files in directory
132
+ */
133
+ function countFiles(dirPath) {
134
+ try {
135
+ const entries = readdirSync(dirPath, { withFileTypes: true });
136
+ return entries.filter(e => e.isFile()).length;
137
+ } catch (e) {
138
+ return 0;
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Get file types in directory
144
+ */
145
+ function getFileTypes(dirPath) {
146
+ const types = new Set();
147
+ try {
148
+ const entries = readdirSync(dirPath, { withFileTypes: true });
149
+ entries.forEach(entry => {
150
+ if (entry.isFile()) {
151
+ const ext = extname(entry.name).slice(1);
152
+ if (ext) types.add(ext);
153
+ }
154
+ });
155
+ } catch (e) {
156
+ // Ignore
157
+ }
158
+ return Array.from(types);
159
+ }
160
+
161
+ /**
162
+ * Main execute function
163
+ */
164
+ async function execute(params) {
165
+ const { format = 'paths', path: targetPath = '.' } = params;
166
+
167
+ const basePath = resolve(process.cwd(), targetPath);
168
+
169
+ if (!existsSync(basePath)) {
170
+ throw new Error(`Directory not found: ${basePath}`);
171
+ }
172
+
173
+ // Get changed files
174
+ let changedFiles = [];
175
+ let changeSource = 'none';
176
+
177
+ if (isGitRepo(basePath)) {
178
+ changedFiles = getGitChangedFiles(basePath);
179
+ changeSource = changedFiles.length > 0 ? 'git' : 'none';
180
+ }
181
+
182
+ // Fallback to recently modified files
183
+ if (changedFiles.length === 0) {
184
+ changedFiles = findRecentlyModified(basePath);
185
+ changeSource = changedFiles.length > 0 ? 'mtime' : 'none';
186
+ }
187
+
188
+ // Extract affected directories
189
+ const affectedDirs = extractDirectories(changedFiles, basePath);
190
+
191
+ // Format output
192
+ let output;
193
+ const results = [];
194
+
195
+ for (const dir of affectedDirs) {
196
+ const fullPath = dir === '.' ? basePath : resolve(basePath, dir);
197
+ if (!existsSync(fullPath) || !statSync(fullPath).isDirectory()) continue;
198
+
199
+ const fileCount = countFiles(fullPath);
200
+ const types = getFileTypes(fullPath);
201
+ const depth = dir === '.' ? 0 : (dir.match(/\//g) || []).length;
202
+ const hasClaude = existsSync(join(fullPath, 'CLAUDE.md'));
203
+
204
+ results.push({
205
+ depth,
206
+ path: dir,
207
+ files: fileCount,
208
+ types,
209
+ has_claude: hasClaude
210
+ });
211
+ }
212
+
213
+ switch (format) {
214
+ case 'list':
215
+ output = results.map(r =>
216
+ `depth:${r.depth}|path:${r.path}|files:${r.files}|types:[${r.types.join(',')}]|has_claude:${r.has_claude ? 'yes' : 'no'}|status:changed`
217
+ ).join('\n');
218
+ break;
219
+
220
+ case 'grouped':
221
+ const maxDepth = results.length > 0 ? Math.max(...results.map(r => r.depth)) : 0;
222
+ const lines = ['Affected modules by changes:'];
223
+
224
+ for (let d = 0; d <= maxDepth; d++) {
225
+ const atDepth = results.filter(r => r.depth === d);
226
+ if (atDepth.length > 0) {
227
+ lines.push(` Depth ${d}:`);
228
+ atDepth.forEach(r => {
229
+ const claudeIndicator = r.has_claude ? ' [OK]' : '';
230
+ lines.push(` - ${r.path}${claudeIndicator} (changed)`);
231
+ });
232
+ }
233
+ }
234
+
235
+ if (results.length === 0) {
236
+ lines.push(' No recent changes detected');
237
+ }
238
+
239
+ output = lines.join('\n');
240
+ break;
241
+
242
+ case 'paths':
243
+ default:
244
+ output = affectedDirs.join('\n');
245
+ break;
246
+ }
247
+
248
+ return {
249
+ format,
250
+ change_source: changeSource,
251
+ changed_files_count: changedFiles.length,
252
+ affected_modules_count: results.length,
253
+ results,
254
+ output
255
+ };
256
+ }
257
+
258
+ /**
259
+ * Tool Definition
260
+ */
261
+ export const detectChangedModulesTool = {
262
+ name: 'detect_changed_modules',
263
+ description: `Detect modules affected by git changes or recent file modifications.
264
+ Features:
265
+ - Git-aware: detects staged, unstaged, or last commit changes
266
+ - Fallback: finds files modified in last 24 hours
267
+ - Respects .gitignore patterns
268
+
269
+ Output formats: list, grouped, paths (default)`,
270
+ parameters: {
271
+ type: 'object',
272
+ properties: {
273
+ format: {
274
+ type: 'string',
275
+ enum: ['list', 'grouped', 'paths'],
276
+ description: 'Output format (default: paths)',
277
+ default: 'paths'
278
+ },
279
+ path: {
280
+ type: 'string',
281
+ description: 'Target directory path (default: current directory)',
282
+ default: '.'
283
+ }
284
+ },
285
+ required: []
286
+ },
287
+ execute
288
+ };
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Discover Design Files Tool
3
+ * Find CSS/JS/HTML design-related files and output JSON
4
+ */
5
+
6
+ import { readdirSync, statSync, existsSync, writeFileSync } from 'fs';
7
+ import { join, resolve, relative, extname } from 'path';
8
+
9
+ // Directories to exclude
10
+ const EXCLUDE_DIRS = [
11
+ 'node_modules', 'dist', '.git', 'build', 'coverage',
12
+ '.cache', '.next', '.nuxt', '__pycache__', '.venv'
13
+ ];
14
+
15
+ // File type patterns
16
+ const FILE_PATTERNS = {
17
+ css: ['.css', '.scss', '.sass', '.less', '.styl'],
18
+ js: ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs', '.vue', '.svelte'],
19
+ html: ['.html', '.htm']
20
+ };
21
+
22
+ /**
23
+ * Find files matching extensions recursively
24
+ */
25
+ function findFiles(basePath, extensions) {
26
+ const results = [];
27
+
28
+ function scan(dirPath) {
29
+ try {
30
+ const entries = readdirSync(dirPath, { withFileTypes: true });
31
+
32
+ for (const entry of entries) {
33
+ if (entry.isDirectory()) {
34
+ if (EXCLUDE_DIRS.includes(entry.name)) continue;
35
+ scan(join(dirPath, entry.name));
36
+ } else if (entry.isFile()) {
37
+ const ext = extname(entry.name).toLowerCase();
38
+ if (extensions.includes(ext)) {
39
+ results.push(relative(basePath, join(dirPath, entry.name)).replace(/\\/g, '/'));
40
+ }
41
+ }
42
+ }
43
+ } catch (e) {
44
+ // Ignore permission errors
45
+ }
46
+ }
47
+
48
+ scan(basePath);
49
+ return results.sort();
50
+ }
51
+
52
+ /**
53
+ * Main execute function
54
+ */
55
+ async function execute(params) {
56
+ const { sourceDir = '.', outputPath } = params;
57
+
58
+ const basePath = resolve(process.cwd(), sourceDir);
59
+
60
+ if (!existsSync(basePath)) {
61
+ throw new Error(`Directory not found: ${basePath}`);
62
+ }
63
+
64
+ if (!statSync(basePath).isDirectory()) {
65
+ throw new Error(`Not a directory: ${basePath}`);
66
+ }
67
+
68
+ // Find files by type
69
+ const cssFiles = findFiles(basePath, FILE_PATTERNS.css);
70
+ const jsFiles = findFiles(basePath, FILE_PATTERNS.js);
71
+ const htmlFiles = findFiles(basePath, FILE_PATTERNS.html);
72
+
73
+ // Build result
74
+ const result = {
75
+ discovery_time: new Date().toISOString(),
76
+ source_directory: basePath,
77
+ file_types: {
78
+ css: {
79
+ count: cssFiles.length,
80
+ files: cssFiles
81
+ },
82
+ js: {
83
+ count: jsFiles.length,
84
+ files: jsFiles
85
+ },
86
+ html: {
87
+ count: htmlFiles.length,
88
+ files: htmlFiles
89
+ }
90
+ },
91
+ total_files: cssFiles.length + jsFiles.length + htmlFiles.length
92
+ };
93
+
94
+ // Write to file if outputPath specified
95
+ if (outputPath) {
96
+ const outPath = resolve(process.cwd(), outputPath);
97
+ writeFileSync(outPath, JSON.stringify(result, null, 2), 'utf8');
98
+ }
99
+
100
+ return {
101
+ css_count: cssFiles.length,
102
+ js_count: jsFiles.length,
103
+ html_count: htmlFiles.length,
104
+ total_files: result.total_files,
105
+ output_path: outputPath || null,
106
+ result
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Tool Definition
112
+ */
113
+ export const discoverDesignFilesTool = {
114
+ name: 'discover_design_files',
115
+ description: `Discover CSS/JS/HTML design-related files in a directory.
116
+ Scans recursively and excludes common build/cache directories.
117
+ Returns JSON with file discovery results.`,
118
+ parameters: {
119
+ type: 'object',
120
+ properties: {
121
+ sourceDir: {
122
+ type: 'string',
123
+ description: 'Source directory to scan (default: current directory)',
124
+ default: '.'
125
+ },
126
+ outputPath: {
127
+ type: 'string',
128
+ description: 'Optional path to write JSON output file'
129
+ }
130
+ },
131
+ required: []
132
+ },
133
+ execute
134
+ };
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Edit File Tool - AI-focused file editing
3
+ * Two complementary modes:
4
+ * - update: Content-driven text replacement (AI primary use)
5
+ * - line: Position-driven line operations (precise control)
6
+ */
7
+
8
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
9
+ import { resolve, isAbsolute } from 'path';
10
+
11
+ /**
12
+ * Resolve file path and read content
13
+ * @param {string} filePath - Path to file
14
+ * @returns {{resolvedPath: string, content: string}}
15
+ */
16
+ function readFile(filePath) {
17
+ const resolvedPath = isAbsolute(filePath) ? filePath : resolve(process.cwd(), filePath);
18
+
19
+ if (!existsSync(resolvedPath)) {
20
+ throw new Error(`File not found: ${resolvedPath}`);
21
+ }
22
+
23
+ try {
24
+ const content = readFileSync(resolvedPath, 'utf8');
25
+ return { resolvedPath, content };
26
+ } catch (error) {
27
+ throw new Error(`Failed to read file: ${error.message}`);
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Write content to file
33
+ * @param {string} filePath - Path to file
34
+ * @param {string} content - Content to write
35
+ */
36
+ function writeFile(filePath, content) {
37
+ try {
38
+ writeFileSync(filePath, content, 'utf8');
39
+ } catch (error) {
40
+ throw new Error(`Failed to write file: ${error.message}`);
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Mode: update - Simple text replacement
46
+ * Auto-adapts line endings (CRLF/LF)
47
+ */
48
+ function executeUpdateMode(content, params) {
49
+ const { oldText, newText, replaceAll } = params;
50
+
51
+ if (!oldText) throw new Error('Parameter "oldText" is required for update mode');
52
+ if (newText === undefined) throw new Error('Parameter "newText" is required for update mode');
53
+
54
+ // Detect original line ending
55
+ const hasCRLF = content.includes('\r\n');
56
+
57
+ // Normalize to LF for matching
58
+ const normalize = (str) => str.replace(/\r\n/g, '\n');
59
+ const normalizedContent = normalize(content);
60
+ const normalizedOld = normalize(oldText);
61
+ const normalizedNew = normalize(newText);
62
+
63
+ let newContent = normalizedContent;
64
+ let status = 'not found';
65
+ let replacements = 0;
66
+
67
+ if (newContent.includes(normalizedOld)) {
68
+ if (replaceAll) {
69
+ const parts = newContent.split(normalizedOld);
70
+ replacements = parts.length - 1;
71
+ newContent = parts.join(normalizedNew);
72
+ status = 'replaced_all';
73
+ } else {
74
+ newContent = newContent.replace(normalizedOld, normalizedNew);
75
+ status = 'replaced';
76
+ replacements = 1;
77
+ }
78
+ }
79
+
80
+ // Restore original line ending
81
+ if (hasCRLF) {
82
+ newContent = newContent.replace(/\n/g, '\r\n');
83
+ }
84
+
85
+ return {
86
+ content: newContent,
87
+ modified: content !== newContent,
88
+ status,
89
+ replacements,
90
+ message:
91
+ status === 'replaced_all'
92
+ ? `Text replaced successfully (${replacements} occurrences)`
93
+ : status === 'replaced'
94
+ ? 'Text replaced successfully'
95
+ : 'oldText not found in file'
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Mode: line - Line-based operations
101
+ * Operations: insert_before, insert_after, replace, delete
102
+ */
103
+ function executeLineMode(content, params) {
104
+ const { operation, line, text, end_line } = params;
105
+
106
+ if (!operation) throw new Error('Parameter "operation" is required for line mode');
107
+ if (line === undefined) throw new Error('Parameter "line" is required for line mode');
108
+
109
+ // Detect original line ending and normalize for processing
110
+ const hasCRLF = content.includes('\r\n');
111
+ const normalizedContent = hasCRLF ? content.replace(/\r\n/g, '\n') : content;
112
+
113
+ const lines = normalizedContent.split('\n');
114
+ const lineIndex = line - 1; // Convert to 0-based
115
+
116
+ if (lineIndex < 0 || lineIndex >= lines.length) {
117
+ throw new Error(`Line ${line} out of range (1-${lines.length})`);
118
+ }
119
+
120
+ let newLines = [...lines];
121
+ let message = '';
122
+
123
+ switch (operation) {
124
+ case 'insert_before':
125
+ if (text === undefined) throw new Error('Parameter "text" is required for insert_before');
126
+ newLines.splice(lineIndex, 0, text);
127
+ message = `Inserted before line ${line}`;
128
+ break;
129
+
130
+ case 'insert_after':
131
+ if (text === undefined) throw new Error('Parameter "text" is required for insert_after');
132
+ newLines.splice(lineIndex + 1, 0, text);
133
+ message = `Inserted after line ${line}`;
134
+ break;
135
+
136
+ case 'replace':
137
+ if (text === undefined) throw new Error('Parameter "text" is required for replace');
138
+ const endIdx = end_line ? end_line - 1 : lineIndex;
139
+ if (endIdx < lineIndex || endIdx >= lines.length) {
140
+ throw new Error(`end_line ${end_line} is invalid`);
141
+ }
142
+ const deleteCount = endIdx - lineIndex + 1;
143
+ newLines.splice(lineIndex, deleteCount, text);
144
+ message = end_line ? `Replaced lines ${line}-${end_line}` : `Replaced line ${line}`;
145
+ break;
146
+
147
+ case 'delete':
148
+ const endDelete = end_line ? end_line - 1 : lineIndex;
149
+ if (endDelete < lineIndex || endDelete >= lines.length) {
150
+ throw new Error(`end_line ${end_line} is invalid`);
151
+ }
152
+ const count = endDelete - lineIndex + 1;
153
+ newLines.splice(lineIndex, count);
154
+ message = end_line ? `Deleted lines ${line}-${end_line}` : `Deleted line ${line}`;
155
+ break;
156
+
157
+ default:
158
+ throw new Error(`Unknown operation: ${operation}. Valid: insert_before, insert_after, replace, delete`);
159
+ }
160
+
161
+ let newContent = newLines.join('\n');
162
+
163
+ // Restore original line endings
164
+ if (hasCRLF) {
165
+ newContent = newContent.replace(/\n/g, '\r\n');
166
+ }
167
+
168
+ return {
169
+ content: newContent,
170
+ modified: content !== newContent,
171
+ operation,
172
+ line,
173
+ end_line,
174
+ message
175
+ };
176
+ }
177
+
178
+ /**
179
+ * Main execute function - routes to appropriate mode
180
+ */
181
+ async function execute(params) {
182
+ const { path: filePath, mode = 'update' } = params;
183
+
184
+ if (!filePath) throw new Error('Parameter "path" is required');
185
+
186
+ const { resolvedPath, content } = readFile(filePath);
187
+
188
+ let result;
189
+ switch (mode) {
190
+ case 'update':
191
+ result = executeUpdateMode(content, params);
192
+ break;
193
+ case 'line':
194
+ result = executeLineMode(content, params);
195
+ break;
196
+ default:
197
+ throw new Error(`Unknown mode: ${mode}. Valid modes: update, line`);
198
+ }
199
+
200
+ // Write if modified
201
+ if (result.modified) {
202
+ writeFile(resolvedPath, result.content);
203
+ }
204
+
205
+ // Remove content from result (don't return file content)
206
+ const { content: _, ...output } = result;
207
+ return output;
208
+ }
209
+
210
+ /**
211
+ * Edit File Tool Definition
212
+ */
213
+ export const editFileTool = {
214
+ name: 'edit_file',
215
+ description: `Update file with two modes:
216
+ - update: Replace oldText with newText (default)
217
+ - line: Position-driven line operations`,
218
+ parameters: {
219
+ type: 'object',
220
+ properties: {
221
+ path: {
222
+ type: 'string',
223
+ description: 'Path to the file to modify'
224
+ },
225
+ mode: {
226
+ type: 'string',
227
+ enum: ['update', 'line'],
228
+ description: 'Edit mode (default: update)',
229
+ default: 'update'
230
+ },
231
+ // Update mode params
232
+ oldText: {
233
+ type: 'string',
234
+ description: '[update mode] Text to find and replace'
235
+ },
236
+ newText: {
237
+ type: 'string',
238
+ description: '[update mode] Replacement text'
239
+ },
240
+ replaceAll: {
241
+ type: 'boolean',
242
+ description: '[update mode] Replace all occurrences of oldText (default: false)'
243
+ },
244
+ // Line mode params
245
+ operation: {
246
+ type: 'string',
247
+ enum: ['insert_before', 'insert_after', 'replace', 'delete'],
248
+ description: '[line mode] Line operation type'
249
+ },
250
+ line: {
251
+ type: 'number',
252
+ description: '[line mode] Line number (1-based)'
253
+ },
254
+ end_line: {
255
+ type: 'number',
256
+ description: '[line mode] End line for range operations'
257
+ },
258
+ text: {
259
+ type: 'string',
260
+ description: '[line mode] Text for insert/replace operations'
261
+ }
262
+ },
263
+ required: ['path']
264
+ },
265
+ execute
266
+ };