devassist-agent 1.0.2 → 1.0.4

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/bin/devassist.js CHANGED
@@ -166,7 +166,7 @@ function printHelp() {
166
166
  printHeader('开发守护 CLI');
167
167
  console.log(`${c.bold('用法:')} devassist <命令> [选项]\n`);
168
168
  console.log(`${c.bold('命令:')}`);
169
- console.log(` ${c.green('start')} 一键启动:自动初始化 + AI多引擎实时监控`);
169
+ console.log(` ${c.green('start')} 一键启动:自动初始化 + AI双引擎实时监控 + 自动修复`);
170
170
  console.log(` ${c.green('init')} 初始化项目——创建 .devassist/ 配置与基础代码`);
171
171
  console.log(` ${c.green('check')} 运行全部开发时检查(代码质量 + 约定 + 模式 + 架构)`);
172
172
  console.log(` ${c.green('watch')} 实时监控文件变更并运行检查`);
@@ -189,18 +189,19 @@ function printHelp() {
189
189
  console.log(` ${c.gray('--ai')} 启用 AI 深度分析(需配置)`);
190
190
  console.log(` ${c.gray('--compare')} 多引擎对比分析(所有引擎并行)`);
191
191
  console.log(` ${c.gray('--json')} 输出 JSON 格式(用于 CI 集成)`);
192
+ console.log(` ${c.gray('--fix')} 自动修复安全项(watch 模式下生效)`);
192
193
  console.log(` ${c.gray('--fail-on <级别>')} 在以下级别非零退出:block(默认)或 warn`);
193
194
  console.log(` ${c.gray('--base-code')} 安装运行时基础设施(Layer 2)`);
194
195
  console.log(` ${c.gray('--force')} 覆盖已有配置`);
195
196
  console.log(` ${c.gray('-v, --version')} 显示版本`);
196
197
  console.log(` ${c.gray('-h, --help')} 显示帮助\n`);
197
198
  console.log(`${c.bold('示例:')}`);
198
- console.log(` ${c.gray('$')} devassist start # 一键启动(自动init+AI多引擎监控)`);
199
+ console.log(` ${c.gray('$')} devassist start # 一键启动(自动init+AI双引擎+自动修复)`);
199
200
  console.log(` ${c.gray('$')} devassist start --check # 一键检查(不启动监控)`);
200
201
  console.log(` ${c.gray('$')} devassist init`);
201
202
  console.log(` ${c.gray('$')} devassist check --ai`);
202
203
  console.log(` ${c.gray('$')} devassist watch # 开发时实时监控`);
203
- console.log(` ${c.gray('$')} devassist watch --ai --compare # 实时监控+AI多引擎对比`);
204
+ console.log(` ${c.gray('$')} devassist watch --ai --compare --fix # 全自动:检测+AI+修复`);
204
205
  console.log(` ${c.gray('$')} devassist diff --staged # 提交前检查暂存文件`);
205
206
  console.log(` ${c.gray('$')} devassist report --open # 生成并查看 HTML 报告`);
206
207
  console.log(` ${c.gray('$')} devassist convention add --id naming-list --rule "使用 data.list 而非 data.items" --severity block`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devassist-agent",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "AI Agent 开发质量守护工具 — 30+规则检测代码缺陷/安全漏洞/性能反模式,多引擎AI根因分析,零外部依赖",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -3,12 +3,13 @@
3
3
  *
4
4
  * Automatically:
5
5
  * 1. Initialize project (if not already initialized)
6
- * 2. Verify AI config (global or project-level)
6
+ * 2. Verify AI config (global or project-level, with real API keys)
7
7
  * 3. Start watch mode with --ai --compare
8
8
  *
9
9
  * Usage:
10
- * devassist start # Full auto: init + watch --ai --compare
10
+ * devassist start # Full auto: init + watch --ai --compare --fix
11
11
  * devassist start --no-ai # Init + watch (rules only, no AI)
12
+ * devassist start --no-fix # Init + watch --ai --compare (no auto-fix)
12
13
  * devassist start --check # Init + one-time check --ai --compare (no watch)
13
14
  */
14
15
 
@@ -20,6 +21,7 @@ module.exports = async function start(projectRoot, args, ui) {
20
21
  const { printHeader, printFinding, printStats, c } = ui;
21
22
 
22
23
  const noAI = args.includes('--no-ai');
24
+ const noFix = args.includes('--no-fix');
23
25
  const checkOnly = args.includes('--check');
24
26
 
25
27
  printHeader('一键启动');
@@ -36,26 +38,31 @@ module.exports = async function start(projectRoot, args, ui) {
36
38
  }
37
39
 
38
40
  // ─── Step 2: Verify AI config ─────────────────────
39
- const projectAIConfig = path.join(projectRoot, '.devassist', 'ai-config.json');
40
- const globalAIConfig = path.join(os.homedir(), '.devassist', 'ai-config.json');
41
+ // Use loadAIConfig to check if real API keys are configured
42
+ const aiCmd = require('./ai');
43
+ const config = aiCmd.loadAIConfig(projectRoot);
44
+ const readyEngines = config ? config.engines.filter(e => e.enabled && e.apiKey) : [];
45
+ const aiReady = readyEngines.length >= 2;
41
46
 
42
- let aiReady = false;
43
- let configSource = '';
44
-
45
- if (fs.existsSync(projectAIConfig)) {
46
- aiReady = true;
47
- configSource = '项目配置';
48
- } else if (fs.existsSync(globalAIConfig)) {
49
- aiReady = true;
50
- configSource = '全局配置';
51
- }
47
+ // Determine config source
48
+ const projectConfigPath = path.join(projectRoot, '.devassist', 'ai-config.json');
49
+ const globalConfigPath = path.join(os.homedir(), '.devassist', 'ai-config.json');
50
+ const configSource = fs.existsSync(projectConfigPath) ? '项目配置' :
51
+ (fs.existsSync(globalConfigPath) ? '全局配置' : '');
52
52
 
53
53
  if (aiReady && !noAI) {
54
- console.log(` ${c.green('2/3')} AI 引擎已就绪 (${configSource}:通义千问 + 腾讯混元)`);
55
- } else if (!aiReady && !noAI) {
54
+ const engineNames = readyEngines.map(e => e.name || e.provider).join(' + ');
55
+ console.log(` ${c.green('2/3')} AI 引擎已就绪 (${configSource}:${engineNames})`);
56
+ if (!noFix) {
57
+ console.log(` ${c.gray('自动修复已启用(--fix)')}`);
58
+ }
59
+ } else if (readyEngines.length === 1 && !noAI) {
60
+ console.log(` ${c.green('2/3')} AI 引擎已就绪 ✓ (${configSource}:${readyEngines[0].name},仅单引擎)`);
61
+ console.log(` ${c.gray('提示:配置第二个引擎可启用对比模式')}`);
62
+ } else if (!noAI) {
56
63
  console.log(` ${c.yellow('2/3')} 未检测到 AI 配置`);
57
- console.log(` ${c.gray('将仅使用规则引擎检查(28条规则)')}`);
58
- console.log(` ${c.gray('配置 AI 后可启用多引擎对比:devassist ai --setup --provider qwen --key <key>')}`);
64
+ console.log(` ${c.gray('将仅使用规则引擎检查(30条规则)')}`);
65
+ console.log(` ${c.gray('配置 AIdevassist ai --setup --provider qwen --key <key>')}`);
59
66
  noAI = true; // fallback to rules-only
60
67
  } else {
61
68
  console.log(` ${c.green('2/3')} 规则引擎模式 ✓`);
@@ -77,6 +84,9 @@ module.exports = async function start(projectRoot, args, ui) {
77
84
  if (!noAI) {
78
85
  watchArgs.push('--ai', '--compare');
79
86
  }
87
+ if (!noFix) {
88
+ watchArgs.push('--fix');
89
+ }
80
90
  await watchCmd(projectRoot, watchArgs, ui);
81
91
  }
82
92
  };
@@ -1,18 +1,20 @@
1
1
  /**
2
- * devassist watch - Real-time file monitoring
2
+ * devassist watch - Real-time file monitoring with full automation
3
3
  *
4
4
  * Watches project files for changes and runs checks on modified files.
5
- * Provides instant feedback during AI-assisted development.
5
+ * With --ai flag, automatically runs AI analysis on findings.
6
+ * With --compare flag, runs multi-engine comparison.
7
+ * With --fix flag, automatically applies safe fixes.
6
8
  *
7
9
  * Usage:
8
- * devassist watch # Watch all source files
9
- * devassist watch --ai # Watch + AI analysis on findings
10
- * devassist watch --dir src # Watch specific directory
10
+ * devassist watch # Watch all source files (rules only)
11
+ * devassist watch --ai # Watch + AI analysis on findings
12
+ * devassist watch --ai --compare # Watch + multi-engine comparison
13
+ * devassist watch --ai --compare --fix # Full auto: detect + AI + fix
11
14
  */
12
15
 
13
16
  const fs = require('fs');
14
17
  const path = require('path');
15
- const { execSync } = require('child_process');
16
18
 
17
19
  // Core
18
20
  const { engine, RuleEngine } = require('../../core/rule-engine');
@@ -25,14 +27,29 @@ const { ArchRiskAssessor } = require('../../modules/dev-time/arch-risk-assessor'
25
27
  // Shared utilities
26
28
  const { loadConfig } = require('../shared/config-loader');
27
29
  const { filterIgnored } = require('../shared/inline-ignore');
30
+ const { enrichWithContext } = require('../shared/code-context');
31
+
32
+ // AI
33
+ const ai = require('../../ai/adapter');
34
+ const qwen = require('../../ai/providers/qwen');
35
+ const openai = require('../../ai/providers/openai');
36
+ const claude = require('../../ai/providers/claude');
37
+ const hunyuan = require('../../ai/providers/hunyuan');
38
+
39
+ // Register AI providers
40
+ ai.registerProvider('qwen', qwen);
41
+ ai.registerProvider('openai', openai);
42
+ ai.registerProvider('claude', claude);
43
+ ai.registerProvider('hunyuan', hunyuan);
28
44
 
29
45
  const CODE_EXTENSIONS = /\.(js|ts|jsx|tsx|py|java)$/;
30
46
  const SKIP_DIRS = ['node_modules', '.git', '.devassist', 'dist', 'build', 'vendor', '.gradle', '__pycache__'];
31
47
 
32
48
  module.exports = async function watch(projectRoot, args, ui) {
33
- const { printHeader, printFinding, c } = ui;
49
+ const { printHeader, printFinding, printStats, c } = ui;
34
50
  const useAI = args.includes('--ai');
35
51
  const useCompare = args.includes('--compare');
52
+ const autoFix = args.includes('--fix');
36
53
  const watchDir = args.includes('--dir') ? args[args.indexOf('--dir') + 1] : null;
37
54
 
38
55
  printHeader('监控模式');
@@ -40,6 +57,7 @@ module.exports = async function watch(projectRoot, args, ui) {
40
57
  console.log(` ${c.gray('监控目录:')} ${watchDir || '整个项目'}`);
41
58
  console.log(` ${c.gray('AI 分析:')} ${useAI ? '已启用' : '已禁用'}`);
42
59
  console.log(` ${c.gray('多引擎对比:')} ${useCompare ? '已启用' : '已禁用'}`);
60
+ console.log(` ${c.gray('自动修复:')} ${autoFix ? '已启用' : '已禁用'}`);
43
61
  console.log(` ${c.gray('按 Ctrl+C 停止')}\n`);
44
62
 
45
63
  // Initialize convention store
@@ -62,9 +80,28 @@ module.exports = async function watch(projectRoot, args, ui) {
62
80
  }
63
81
  });
64
82
 
83
+ // Load AI config (global or project-level)
84
+ let aiConfig = null;
85
+ let readyEngines = [];
86
+ if (useAI) {
87
+ const { loadAIConfig } = require('./ai');
88
+ aiConfig = loadAIConfig(projectRoot);
89
+ if (aiConfig && aiConfig.engines) {
90
+ readyEngines = aiConfig.engines.filter(e => e.enabled && e.apiKey);
91
+ }
92
+ if (readyEngines.length === 0) {
93
+ console.log(` ${c.yellow('警告:')} 未检测到可用的 AI 引擎,将仅使用规则引擎\n`);
94
+ } else if (useCompare && readyEngines.length < 2) {
95
+ console.log(` ${c.yellow('提示:')} 对比模式需要 2+ 引擎,当前仅 ${readyEngines.length} 个,将使用单引擎\n`);
96
+ } else {
97
+ console.log(` ${c.green('AI 引擎就绪:')} ${readyEngines.map(e => e.name || e.provider).join(' + ')}\n`);
98
+ }
99
+ }
100
+
65
101
  // Track watchers for cleanup
66
102
  const watchers = [];
67
103
  const debounceTimers = new Map();
104
+ const analyzingFiles = new Set(); // Lock: prevent overlapping AI analysis on same file
68
105
 
69
106
  // Watch a single file
70
107
  function watchFile(filePath) {
@@ -72,14 +109,17 @@ module.exports = async function watch(projectRoot, args, ui) {
72
109
  const watcher = fs.watch(filePath, { persistent: true }, (eventType) => {
73
110
  if (eventType !== 'change') return;
74
111
 
75
- // Debounce: wait 300ms after last change before checking
112
+ // Debounce: wait 500ms after last change before checking (longer with AI)
113
+ const debounceMs = useAI ? 500 : 300;
76
114
  if (debounceTimers.has(filePath)) {
77
115
  clearTimeout(debounceTimers.get(filePath));
78
116
  }
79
117
  debounceTimers.set(filePath, setTimeout(() => {
80
118
  debounceTimers.delete(filePath);
81
- checkFile(filePath);
82
- }, 300));
119
+ checkFile(filePath).catch(err => {
120
+ console.log(` ${c.red('检查出错:')} ${err.message}`);
121
+ });
122
+ }, debounceMs));
83
123
  });
84
124
  watcher.on('error', () => {});
85
125
  watchers.push(watcher);
@@ -102,61 +142,144 @@ module.exports = async function watch(projectRoot, args, ui) {
102
142
  }
103
143
  }
104
144
 
105
- // Check a single file
106
- function checkFile(filePath) {
107
- const relPath = path.relative(projectRoot, filePath);
108
- const timestamp = new Date().toLocaleTimeString();
145
+ // Check a single file — async for AI analysis
146
+ async function checkFile(filePath) {
147
+ // Skip if already analyzing this file (AI takes time)
148
+ if (analyzingFiles.has(filePath)) return;
149
+ analyzingFiles.add(filePath);
109
150
 
110
- let content = '';
111
- try { content = fs.readFileSync(filePath, 'utf-8'); } catch (_) { return; }
151
+ try {
152
+ const relPath = path.relative(projectRoot, filePath);
153
+ const timestamp = new Date().toLocaleTimeString();
112
154
 
113
- // Quick skip for HTML without script tags
114
- if (/\.html?$/.test(filePath) && !/<script[\s>]/i.test(content)) return;
155
+ let content = '';
156
+ try { content = fs.readFileSync(filePath, 'utf-8'); } catch (_) { return; }
115
157
 
116
- const ctx = {
117
- files: [{ path: relPath, content, fullPath: filePath }],
118
- projectRoot,
119
- config: loadConfig(projectRoot),
120
- };
158
+ // Quick skip for HTML without script tags
159
+ if (/\.html?$/.test(filePath) && !/<script[\s>]/i.test(content)) return;
121
160
 
122
- const { findings: ruleFindings } = engine.run(ctx);
123
- const archFindings = archAssessor.analyzeFile(relPath, content);
124
- let allFindings = [...ruleFindings, ...archFindings];
161
+ const ctx = {
162
+ files: [{ path: relPath, content, fullPath: filePath }],
163
+ projectRoot,
164
+ config: loadConfig(projectRoot),
165
+ };
125
166
 
126
- // Apply inline ignores
127
- allFindings = filterIgnored(allFindings, [{ path: relPath, content }]);
167
+ const { findings: ruleFindings } = engine.run(ctx);
168
+ const archFindings = archAssessor.analyzeFile(relPath, content);
169
+ let allFindings = [...ruleFindings, ...archFindings];
128
170
 
129
- // Save findings for ai --analyze
130
- saveWatchFindings(projectRoot, allFindings);
171
+ // Apply inline ignores
172
+ allFindings = filterIgnored(allFindings, [{ path: relPath, content }]);
131
173
 
132
- // Print header
133
- console.log(`\n ${c.gray(`[${timestamp}]`)} ${c.bold(relPath)}`);
174
+ // Save findings for ai --analyze
175
+ saveWatchFindings(projectRoot, allFindings);
134
176
 
135
- if (allFindings.length === 0) {
136
- console.log(` ${c.green('OK')} — 未发现问题`);
137
- return;
138
- }
177
+ // Print header
178
+ console.log(`\n ${c.gray(`[${timestamp}]`)} ${c.bold(relPath)}`);
139
179
 
140
- // Print findings
141
- const blocks = allFindings.filter(f => f.severity === 'block');
142
- const warns = allFindings.filter(f => f.severity === 'warn');
143
- const infos = allFindings.filter(f => f.severity === 'info');
144
-
145
- for (const f of allFindings) {
146
- const icon = f.severity === 'block' ? c.red('[阻断]') :
147
- f.severity === 'warn' ? c.yellow('[警告]') :
148
- c.gray('[提示]');
149
- console.log(` ${icon} ${f.message}`);
150
- if (f.line) console.log(` ${c.gray(`第 ${f.line} 行`)}`);
151
- if (f.suggestion) console.log(` ${c.gray(f.suggestion)}`);
152
- }
180
+ if (allFindings.length === 0) {
181
+ console.log(` ${c.green('OK')} — 未发现问题`);
182
+ return;
183
+ }
153
184
 
154
- // Summary
155
- const parts = [];
156
- if (blocks.length > 0) parts.push(c.red(`${blocks.length} 阻断`));
157
- if (warns.length > 0) parts.push(c.yellow(`${warns.length} 警告`));
158
- if (infos.length > 0) parts.push(c.gray(`${infos.length} 提示`));
159
- console.log(` ${c.gray('')} ${parts.join(c.gray(' · '))}`);
185
+ // --- AI Analysis (if enabled and there are warn/block findings) ---
186
+ let finalFindings = allFindings;
187
+ const suspectFindings = allFindings.filter(f => f.severity === 'block' || f.severity === 'warn');
188
+
189
+ if (useAI && readyEngines.length > 0 && suspectFindings.length > 0) {
190
+ console.log(` ${c.cyan('AI 分析中...')} (${suspectFindings.length} 个疑似问题)`);
191
+
192
+ // Enrich with code context
193
+ const fileContents = [{ path: relPath, content }];
194
+ finalFindings = enrichWithContext(allFindings, fileContents);
195
+
196
+ const aiContext = {
197
+ conventions: conventionStore.getRelevant({}),
198
+ projectInfo: { root: projectRoot },
199
+ };
200
+
201
+ try {
202
+ if (useCompare && readyEngines.length >= 2) {
203
+ finalFindings = await ai.multiAnalyze(readyEngines, finalFindings, aiContext);
204
+ } else {
205
+ finalFindings = await ai.analyze(readyEngines[0], finalFindings, aiContext);
206
+ }
207
+ } catch (err) {
208
+ console.log(` ${c.yellow('AI 分析失败:')} ${err.message}`);
209
+ console.log(` ${c.gray('已回退到规则引擎结果')}`);
210
+ }
211
+ }
212
+
213
+ // --- Print findings ---
214
+ const blocks = finalFindings.filter(f => f.severity === 'block');
215
+ const warns = finalFindings.filter(f => f.severity === 'warn');
216
+ const infos = finalFindings.filter(f => f.severity === 'info');
217
+
218
+ for (const f of finalFindings) {
219
+ const icon = f.severity === 'block' ? c.red('[阻断]') :
220
+ f.severity === 'warn' ? c.yellow('[警告]') :
221
+ c.gray('[提示]');
222
+ console.log(` ${icon} ${f.message}`);
223
+ if (f.line) console.log(` ${c.gray(`第 ${f.line} 行`)}`);
224
+ if (f.suggestion) console.log(` ${c.gray(f.suggestion)}`);
225
+
226
+ // Print AI analysis if available
227
+ if (f.aiAnalysis) {
228
+ const a = f.aiAnalysis;
229
+ if (a.falsePositive) {
230
+ console.log(` ${c.gray('AI 判定:误报 — ' + (a.falsePositiveReason || '不适用'))}`);
231
+ } else {
232
+ const rootCause = a.rootCause || a.root_cause || '';
233
+ const fixSuggestion = a.fixSuggestion || a.fix_suggestion || '';
234
+ const confidence = a.confidence || '';
235
+ if (rootCause) console.log(` ${c.cyan('AI 根因:')} ${rootCause}`);
236
+ if (fixSuggestion) console.log(` ${c.cyan('AI 建议:')} ${fixSuggestion}`);
237
+ if (confidence) console.log(` ${c.gray(`置信度:${confidence}`)}`);
238
+ }
239
+ }
240
+
241
+ // Print multi-engine comparison
242
+ if (f.aiAnalyses && f.aiAnalyses.length >= 2) {
243
+ for (const ea of f.aiAnalyses) {
244
+ if (ea.analysis && !ea.failed) {
245
+ const engineName = ea.engineName || ea.provider;
246
+ const a = ea.analysis;
247
+ if (a.falsePositive) {
248
+ console.log(` ${c.gray(`[${engineName}] 误报 — ${a.falsePositiveReason || ''}`)}`);
249
+ } else {
250
+ const rootCause = a.rootCause || a.root_cause || '';
251
+ if (rootCause) console.log(` ${c.cyan(`[${engineName}]`)} ${rootCause}`);
252
+ }
253
+ } else if (ea.failed) {
254
+ console.log(` ${c.gray(`[${ea.engineName || ea.provider}] 分析失败`)}`);
255
+ }
256
+ }
257
+ // Agreement indicator
258
+ if (f.aiAnalyses.filter(ea => ea.analysis && !ea.failed && !ea.analysis.falsePositive).length === 0
259
+ && f.aiAnalyses.filter(ea => ea.analysis && !ea.failed && ea.analysis.falsePositive).length >= 2) {
260
+ console.log(` ${c.green('引擎一致:均判定为误报')}`);
261
+ }
262
+ }
263
+ }
264
+
265
+ // --- Summary ---
266
+ const parts = [];
267
+ if (blocks.length > 0) parts.push(c.red(`${blocks.length} 阻断`));
268
+ if (warns.length > 0) parts.push(c.yellow(`${warns.length} 警告`));
269
+ if (infos.length > 0) parts.push(c.gray(`${infos.length} 提示`));
270
+ console.log(` ${c.gray('—')} ${parts.join(c.gray(' · '))}`);
271
+
272
+ // --- Auto Fix (if enabled) ---
273
+ if (autoFix && suspectFindings.length > 0) {
274
+ const fixedCount = await autoFixFile(filePath, content, projectRoot, c);
275
+ if (fixedCount > 0) {
276
+ console.log(` ${c.green('自动修复')} ${fixedCount} 处问题`);
277
+ }
278
+ }
279
+
280
+ } finally {
281
+ analyzingFiles.delete(filePath);
282
+ }
160
283
  }
161
284
 
162
285
  // Start watching
@@ -199,6 +322,78 @@ module.exports = async function watch(projectRoot, args, ui) {
199
322
  });
200
323
  };
201
324
 
325
+ /**
326
+ * Auto-fix a single file in-place (safe fixes only).
327
+ * Returns the number of fixes applied.
328
+ */
329
+ async function autoFixFile(filePath, content, projectRoot, c) {
330
+ const lines = content.split('\n');
331
+ let fixCount = 0;
332
+ const changes = [];
333
+
334
+ for (let i = 0; i < lines.length; i++) {
335
+ const line = lines[i];
336
+ if (/^\s*(\/\/|\/\*|\*)/.test(line)) continue;
337
+
338
+ // Fix 1: fetch() without .catch()
339
+ if (/\bfetch\s*\(/.test(line)) {
340
+ const context = lines.slice(i, Math.min(i + 5, lines.length)).join('\n');
341
+ if (/\.catch\s*\(/.test(context)) continue;
342
+ const hasAwait = /\bawait\b/.test(line);
343
+ const hasTryCatch = /try\s*\{/.test(lines.slice(Math.max(0, i - 5), i + 1).join('\n'));
344
+ if (hasAwait && hasTryCatch) continue;
345
+
346
+ const trimmed = line.trimEnd();
347
+ if (trimmed.endsWith(')') && !trimmed.endsWith('.catch()')) {
348
+ const fixedLine = trimmed.replace(/\)\s*$/, ").catch(err => console.error('fetch error:', err))");
349
+ changes.push({ line: i, newLine: fixedLine, type: 'fetch-catch' });
350
+ fixCount++;
351
+ continue;
352
+ }
353
+ }
354
+
355
+ // Fix 2: Add await to async calls (fetch, axios, etc.)
356
+ if (!/\bawait\b/.test(line)) {
357
+ const asyncMatch = line.match(/(?<![\w.])(fetch|axios\.(get|post|put|delete|patch)|\.save\(|\.findOne\(|\.findById\(|\.create\()\s*/);
358
+ if (asyncMatch) {
359
+ const trimmed = line.trim();
360
+ if (/^\s*(const|let|var|if|while|return|throw)/.test(trimmed)) continue;
361
+ if (/^\s*(async\s+)?function\s+/.test(line)) continue;
362
+ if (/\.then\s*\(|\.catch\s*\(|\.finally\s*\(/.test(line)) continue;
363
+
364
+ const fixedLine = line.replace(
365
+ new RegExp('(?<![\\w.])(' + asyncMatch[1] + '\\w+)\\s*\\('),
366
+ 'await $1('
367
+ );
368
+ if (fixedLine !== line) {
369
+ changes.push({ line: i, newLine: fixedLine, type: 'add-await' });
370
+ fixCount++;
371
+ continue;
372
+ }
373
+ }
374
+ }
375
+ }
376
+
377
+ if (fixCount === 0) return 0;
378
+
379
+ // Apply changes
380
+ for (const ch of changes) {
381
+ lines[ch.line] = ch.newLine;
382
+ }
383
+ const newContent = lines.join('\n');
384
+
385
+ // Backup original
386
+ const backupDir = path.join(projectRoot, '.devassist', 'backups', `watch-fix-${Date.now()}`);
387
+ if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir, { recursive: true });
388
+ const backupPath = path.join(backupDir, path.basename(filePath));
389
+ fs.writeFileSync(backupPath, content, 'utf-8');
390
+
391
+ // Write fixed content
392
+ fs.writeFileSync(filePath, newContent, 'utf-8');
393
+
394
+ return fixCount;
395
+ }
396
+
202
397
  function saveWatchFindings(projectRoot, findings) {
203
398
  const reportDir = path.join(projectRoot, '.devassist', 'reports');
204
399
  if (!fs.existsSync(reportDir)) fs.mkdirSync(reportDir, { recursive: true });