watchfix 0.2.0 → 0.2.2
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 +17 -1
- package/dist/cli/commands/fix.js +8 -0
- package/dist/config/schema.d.ts +13 -0
- package/dist/config/schema.js +5 -0
- package/dist/fixer/index.d.ts +1 -0
- package/dist/fixer/index.js +2 -0
- package/dist/watcher/index.js +17 -0
- package/dist/watcher/parser.js +23 -0
- package/dist/watcher/sources/file.js +4 -6
- package/package.json +15 -1
package/README.md
CHANGED
|
@@ -64,12 +64,28 @@ watchfix watch
|
|
|
64
64
|
watchfix fix <error-id>
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
## Autonomous Mode
|
|
68
|
+
|
|
69
|
+
For fully automated error fixing without manual approval:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
watchfix watch --autonomous
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
In autonomous mode, watchfix automatically dispatches AI agents to fix detected errors. Combine with daemon mode for background operation (Linux/macOS):
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
watchfix watch --daemon --autonomous
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Note:** Manual `watchfix fix` commands are blocked while running in autonomous mode.
|
|
82
|
+
|
|
67
83
|
## CLI Commands
|
|
68
84
|
|
|
69
85
|
| Command | Description |
|
|
70
86
|
|---------|-------------|
|
|
71
87
|
| `watchfix init` | Create `watchfix.yaml` in current directory |
|
|
72
|
-
| `watchfix watch` | Watch logs in foreground (use `--daemon` for background) |
|
|
88
|
+
| `watchfix watch` | Watch logs in foreground (use `--daemon` for background, `--autonomous` for auto-fix) |
|
|
73
89
|
| `watchfix fix [id]` | Analyze and fix a specific error (or `--all` for all pending) |
|
|
74
90
|
| `watchfix show <id>` | Show full error details and analysis |
|
|
75
91
|
| `watchfix status` | Show watcher state and pending errors |
|
package/dist/cli/commands/fix.js
CHANGED
|
@@ -175,6 +175,14 @@ const formatFixOutcome = (result, verbosity, maxAttempts) => {
|
|
|
175
175
|
}
|
|
176
176
|
else if (!result.fix?.success) {
|
|
177
177
|
lines.push(`Error #${result.errorId}: Agent could not apply fix`);
|
|
178
|
+
if (result.diagnostic) {
|
|
179
|
+
// Extract key info from diagnostic
|
|
180
|
+
const diagLines = result.diagnostic.split('\n');
|
|
181
|
+
const reason = diagLines.find(l => l.includes('timed out') || l.includes('Exit code'));
|
|
182
|
+
if (reason) {
|
|
183
|
+
lines.push(` Reason: ${reason.trim()}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
178
186
|
}
|
|
179
187
|
else {
|
|
180
188
|
lines.push(`Error #${result.errorId}: Fix attempt completed`);
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -259,6 +259,13 @@ declare const configSchema: z.ZodObject<{
|
|
|
259
259
|
context_max_age_days?: number | undefined;
|
|
260
260
|
context_max_size_kb?: number | undefined;
|
|
261
261
|
}>>;
|
|
262
|
+
deduplication: z.ZodDefault<z.ZodObject<{
|
|
263
|
+
fixed_grace_period: z.ZodDefault<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>>;
|
|
264
|
+
}, "strip", z.ZodTypeAny, {
|
|
265
|
+
fixed_grace_period: string;
|
|
266
|
+
}, {
|
|
267
|
+
fixed_grace_period?: string | undefined;
|
|
268
|
+
}>>;
|
|
262
269
|
patterns: z.ZodDefault<z.ZodObject<{
|
|
263
270
|
match: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
264
271
|
ignore: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
@@ -315,6 +322,9 @@ declare const configSchema: z.ZodObject<{
|
|
|
315
322
|
context_max_age_days: number;
|
|
316
323
|
context_max_size_kb: number;
|
|
317
324
|
};
|
|
325
|
+
deduplication: {
|
|
326
|
+
fixed_grace_period: string;
|
|
327
|
+
};
|
|
318
328
|
patterns: {
|
|
319
329
|
ignore: string[];
|
|
320
330
|
match: string[];
|
|
@@ -365,6 +375,9 @@ declare const configSchema: z.ZodObject<{
|
|
|
365
375
|
context_max_age_days?: number | undefined;
|
|
366
376
|
context_max_size_kb?: number | undefined;
|
|
367
377
|
} | undefined;
|
|
378
|
+
deduplication?: {
|
|
379
|
+
fixed_grace_period?: string | undefined;
|
|
380
|
+
} | undefined;
|
|
368
381
|
patterns?: {
|
|
369
382
|
ignore?: string[] | undefined;
|
|
370
383
|
match?: string[] | undefined;
|
package/dist/config/schema.js
CHANGED
|
@@ -89,6 +89,11 @@ const configSchema = z.object({
|
|
|
89
89
|
context_max_size_kb: z.number().int().min(64).default(256),
|
|
90
90
|
})
|
|
91
91
|
.default({}),
|
|
92
|
+
deduplication: z
|
|
93
|
+
.object({
|
|
94
|
+
fixed_grace_period: durationSchema.default('10m'),
|
|
95
|
+
})
|
|
96
|
+
.default({}),
|
|
92
97
|
patterns: z
|
|
93
98
|
.object({
|
|
94
99
|
match: z.array(patternSchema).default([]),
|
package/dist/fixer/index.d.ts
CHANGED
package/dist/fixer/index.js
CHANGED
|
@@ -246,6 +246,7 @@ export class FixOrchestrator {
|
|
|
246
246
|
lockAcquired: true,
|
|
247
247
|
attempts,
|
|
248
248
|
message: 'Analysis failed',
|
|
249
|
+
diagnostic: analysisResult.diagnostic,
|
|
249
250
|
};
|
|
250
251
|
}
|
|
251
252
|
}
|
|
@@ -348,6 +349,7 @@ export class FixOrchestrator {
|
|
|
348
349
|
attempts,
|
|
349
350
|
analysis: analysisOutput,
|
|
350
351
|
message: 'Fix failed',
|
|
352
|
+
diagnostic: fixResult.diagnostic,
|
|
351
353
|
};
|
|
352
354
|
}
|
|
353
355
|
logActivity(this.db, 'verification_start', errorId, JSON.stringify({ attempt: attempts }));
|
package/dist/watcher/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
import { getErrorByHash, insertError, logActivity } from '../db/queries.js';
|
|
3
|
+
import { parseDuration } from '../utils/duration.js';
|
|
3
4
|
import { Logger } from '../utils/logger.js';
|
|
4
5
|
import { ErrorParser } from './parser.js';
|
|
5
6
|
import { CommandSource } from './sources/command.js';
|
|
@@ -186,6 +187,22 @@ export class WatcherOrchestrator {
|
|
|
186
187
|
});
|
|
187
188
|
return;
|
|
188
189
|
}
|
|
190
|
+
// Deduplicate fixed errors within grace period to prevent re-detection
|
|
191
|
+
// after a fix when the log file is re-read
|
|
192
|
+
if (existing && existing.status === 'fixed') {
|
|
193
|
+
const gracePeriodMs = parseDuration(this.config.deduplication.fixed_grace_period);
|
|
194
|
+
const fixedAt = new Date(existing.updatedAt).getTime();
|
|
195
|
+
if (Date.now() - fixedAt < gracePeriodMs) {
|
|
196
|
+
logActivity(this.db, 'error_deduplicated', existing.id, `status=${existing.status} grace_period=true`);
|
|
197
|
+
this.logger.info(`Deduplicated error ${error.hash} (within grace period after fix)`);
|
|
198
|
+
this.emitter.emit('error_deduplicated', {
|
|
199
|
+
errorId: existing.id,
|
|
200
|
+
error,
|
|
201
|
+
status: existing.status,
|
|
202
|
+
});
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
189
206
|
const newId = insertError(this.db, {
|
|
190
207
|
hash: error.hash,
|
|
191
208
|
source: error.source,
|
package/dist/watcher/parser.js
CHANGED
|
@@ -11,6 +11,14 @@ const CONTINUATION_PATTERNS = [
|
|
|
11
11
|
/^\s+\.\.\./,
|
|
12
12
|
];
|
|
13
13
|
const isContinuationLine = (line) => CONTINUATION_PATTERNS.some((pattern) => pattern.test(line));
|
|
14
|
+
const extractCoreMessage = (line) => {
|
|
15
|
+
// "TypeError: Cannot read..." → "Cannot read..."
|
|
16
|
+
const colonIndex = line.indexOf(': ');
|
|
17
|
+
if (colonIndex > 0 && colonIndex < 30) {
|
|
18
|
+
return line.slice(colonIndex + 2).trim();
|
|
19
|
+
}
|
|
20
|
+
return line.trim();
|
|
21
|
+
};
|
|
14
22
|
const truncateLine = (line) => {
|
|
15
23
|
if (line.length <= MAX_LINE_LENGTH) {
|
|
16
24
|
return line;
|
|
@@ -58,6 +66,21 @@ export class ErrorParser {
|
|
|
58
66
|
return;
|
|
59
67
|
}
|
|
60
68
|
if (isError) {
|
|
69
|
+
// Check if this is the same error with a more specific type
|
|
70
|
+
if (this.current && this.current.stackLines.length === 0) {
|
|
71
|
+
const currentCore = extractCoreMessage(this.current.message);
|
|
72
|
+
const newCore = extractCoreMessage(line);
|
|
73
|
+
if (currentCore === newCore) {
|
|
74
|
+
// Same error - discard shallow entry, use the more detailed one
|
|
75
|
+
if (this.flushTimer) {
|
|
76
|
+
clearTimeout(this.flushTimer);
|
|
77
|
+
this.flushTimer = undefined;
|
|
78
|
+
}
|
|
79
|
+
this.current = undefined;
|
|
80
|
+
this.startError(event, line);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
61
84
|
await this.flushCurrent('new_error');
|
|
62
85
|
this.startError(event, line);
|
|
63
86
|
return;
|
|
@@ -125,14 +125,12 @@ export class FileSource {
|
|
|
125
125
|
}
|
|
126
126
|
const inode = typeof stats.ino === 'number' ? stats.ino : null;
|
|
127
127
|
const mtimeMs = stats.mtimeMs;
|
|
128
|
-
const shouldForceRead = this.forceRead;
|
|
129
128
|
this.forceRead = false;
|
|
130
129
|
const inodeChanged = this.lastInode !== null && inode !== null && inode !== this.lastInode;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
(mtimeChanged && stats.size === this.position && this.position > 0)) {
|
|
130
|
+
// Only reset position on actual file replacement (inode change) or truncation (size shrunk)
|
|
131
|
+
// Do NOT reset on mtime changes without content changes - this prevents re-reading
|
|
132
|
+
// old errors when the file is touched but no new content is added
|
|
133
|
+
if (inodeChanged || stats.size < this.position) {
|
|
136
134
|
this.position = 0;
|
|
137
135
|
this.partialLine = '';
|
|
138
136
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "watchfix",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "CLI tool that watches logs, detects errors, and dispatches AI agents to fix them",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cli",
|
|
7
|
+
"log-watcher",
|
|
8
|
+
"error-detection",
|
|
9
|
+
"ai",
|
|
10
|
+
"ai-agent",
|
|
11
|
+
"automation",
|
|
12
|
+
"auto-fix",
|
|
13
|
+
"llm",
|
|
14
|
+
"devtools",
|
|
15
|
+
"monitoring",
|
|
16
|
+
"logs",
|
|
17
|
+
"debugging"
|
|
18
|
+
],
|
|
5
19
|
"type": "module",
|
|
6
20
|
"bin": {
|
|
7
21
|
"watchfix": "./dist/cli/index.js"
|