autonomous-flow-daemon 1.1.0 → 1.6.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/CHANGELOG.md +39 -0
- package/README.ko.md +124 -164
- package/README.md +99 -170
- package/package.json +11 -5
- package/src/adapters/index.ts +246 -35
- package/src/cli.ts +71 -1
- package/src/commands/benchmark.ts +187 -0
- package/src/commands/diagnose.ts +56 -14
- package/src/commands/doctor.ts +243 -0
- package/src/commands/evolution.ts +107 -0
- package/src/commands/fix.ts +22 -2
- package/src/commands/hooks.ts +136 -0
- package/src/commands/mcp.ts +129 -0
- package/src/commands/restart.ts +14 -0
- package/src/commands/score.ts +164 -96
- package/src/commands/start.ts +74 -15
- package/src/commands/stats.ts +103 -0
- package/src/commands/status.ts +157 -0
- package/src/commands/stop.ts +23 -4
- package/src/commands/sync.ts +253 -20
- package/src/commands/vaccine.ts +177 -0
- package/src/constants.ts +25 -1
- package/src/core/boast.ts +27 -12
- package/src/core/db.ts +74 -3
- package/src/core/evolution.ts +215 -0
- package/src/core/hologram/engine.ts +71 -0
- package/src/core/hologram/fallback.ts +11 -0
- package/src/core/hologram/incremental.ts +227 -0
- package/src/core/hologram/py-extractor.ts +132 -0
- package/src/core/hologram/ts-extractor.ts +320 -0
- package/src/core/hologram/types.ts +25 -0
- package/src/core/hologram.ts +64 -236
- package/src/core/hook-manager.ts +259 -0
- package/src/core/i18n/messages.ts +43 -0
- package/src/core/immune.ts +8 -123
- package/src/core/log-rotate.ts +33 -0
- package/src/core/log-utils.ts +38 -0
- package/src/core/lru-map.ts +61 -0
- package/src/core/notify.ts +27 -19
- package/src/core/rule-engine.ts +287 -0
- package/src/core/semantic-diff.ts +432 -0
- package/src/core/telemetry.ts +94 -0
- package/src/core/vaccine-registry.ts +212 -0
- package/src/core/workspace.ts +28 -0
- package/src/core/yaml-minimal.ts +176 -0
- package/src/daemon/client.ts +34 -6
- package/src/daemon/event-batcher.ts +108 -0
- package/src/daemon/guards.ts +13 -0
- package/src/daemon/http-routes.ts +293 -0
- package/src/daemon/mcp-handler.ts +270 -0
- package/src/daemon/server.ts +439 -353
- package/src/daemon/types.ts +100 -0
- package/src/daemon/workspace-map.ts +92 -0
- package/src/platform.ts +23 -2
- package/src/version.ts +15 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic Diff Engine — AST-based change classification for TypeScript/JavaScript.
|
|
3
|
+
*
|
|
4
|
+
* Instead of line-by-line text diff, parses both old and new source into AST,
|
|
5
|
+
* extracts top-level declarations, and classifies changes as:
|
|
6
|
+
* - signature-change (breaking)
|
|
7
|
+
* - body-change (non-breaking)
|
|
8
|
+
* - added / removed
|
|
9
|
+
* - renamed
|
|
10
|
+
* - comment-only
|
|
11
|
+
*
|
|
12
|
+
* Falls back to text diff for non-TS/JS files.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import ts from "typescript";
|
|
16
|
+
|
|
17
|
+
export type ChangeKind =
|
|
18
|
+
| "signature-change" // function/class signature modified (breaking)
|
|
19
|
+
| "body-change" // implementation changed, signature intact
|
|
20
|
+
| "added" // new declaration
|
|
21
|
+
| "removed" // declaration deleted
|
|
22
|
+
| "export-change" // export modifier added/removed
|
|
23
|
+
| "comment-only" // only comments changed
|
|
24
|
+
| "whitespace-only" // only formatting changed
|
|
25
|
+
| "type-change" // type annotation changed
|
|
26
|
+
| "unknown";
|
|
27
|
+
|
|
28
|
+
export interface SemanticChange {
|
|
29
|
+
kind: ChangeKind;
|
|
30
|
+
name: string; // declaration name
|
|
31
|
+
nodeType: string; // "function", "class", "interface", "variable", etc.
|
|
32
|
+
breaking: boolean; // true if this could break consumers
|
|
33
|
+
detail?: string; // human-readable description
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SemanticDiffResult {
|
|
37
|
+
changes: SemanticChange[];
|
|
38
|
+
hasBreakingChanges: boolean;
|
|
39
|
+
summary: string; // one-line summary for logging
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface DeclInfo {
|
|
43
|
+
name: string;
|
|
44
|
+
nodeType: string;
|
|
45
|
+
signature: string; // type signature (no body)
|
|
46
|
+
body: string; // full text including body
|
|
47
|
+
exported: boolean;
|
|
48
|
+
hasComments: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Check if a file path is TypeScript or JavaScript */
|
|
52
|
+
export function isAstSupported(filePath: string): boolean {
|
|
53
|
+
return /\.(ts|tsx|js|jsx|mts|mjs|cts|cjs)$/.test(filePath);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Generate semantic diff between old and new source */
|
|
57
|
+
export function semanticDiff(filePath: string, oldSource: string, newSource: string): SemanticDiffResult {
|
|
58
|
+
if (!isAstSupported(filePath)) {
|
|
59
|
+
return textFallback(oldSource, newSource);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const oldDecls = extractDeclarations(filePath, oldSource);
|
|
64
|
+
const newDecls = extractDeclarations(filePath, newSource);
|
|
65
|
+
return compareDeclarations(oldDecls, newDecls);
|
|
66
|
+
} catch {
|
|
67
|
+
// AST parsing failed — fall back to text diff
|
|
68
|
+
return textFallback(oldSource, newSource);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function extractDeclarations(filePath: string, source: string): Map<string, DeclInfo> {
|
|
73
|
+
const sf = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true);
|
|
74
|
+
const decls = new Map<string, DeclInfo>();
|
|
75
|
+
|
|
76
|
+
for (const stmt of sf.statements) {
|
|
77
|
+
const infos = extractDeclInfos(stmt, source);
|
|
78
|
+
for (const info of infos) {
|
|
79
|
+
decls.set(info.name, info);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return decls;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function extractDeclInfos(node: ts.Statement, _source: string): DeclInfo[] {
|
|
87
|
+
const exported = hasExportModifier(node);
|
|
88
|
+
const fullText = node.getFullText().trim();
|
|
89
|
+
const hasComments = /^\s*\/[/*]/.test(node.getFullText());
|
|
90
|
+
|
|
91
|
+
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
92
|
+
return [{
|
|
93
|
+
name: node.name.text,
|
|
94
|
+
nodeType: "function",
|
|
95
|
+
signature: getFunctionSignature(node),
|
|
96
|
+
body: fullText,
|
|
97
|
+
exported,
|
|
98
|
+
hasComments,
|
|
99
|
+
}];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (ts.isClassDeclaration(node) && node.name) {
|
|
103
|
+
return [{
|
|
104
|
+
name: node.name.text,
|
|
105
|
+
nodeType: "class",
|
|
106
|
+
signature: getClassSignature(node),
|
|
107
|
+
body: fullText,
|
|
108
|
+
exported,
|
|
109
|
+
hasComments,
|
|
110
|
+
}];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (ts.isInterfaceDeclaration(node)) {
|
|
114
|
+
return [{
|
|
115
|
+
name: node.name.text,
|
|
116
|
+
nodeType: "interface",
|
|
117
|
+
signature: normalizeWhitespace(node.getText()),
|
|
118
|
+
body: fullText,
|
|
119
|
+
exported,
|
|
120
|
+
hasComments,
|
|
121
|
+
}];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (ts.isTypeAliasDeclaration(node)) {
|
|
125
|
+
return [{
|
|
126
|
+
name: node.name.text,
|
|
127
|
+
nodeType: "type",
|
|
128
|
+
signature: normalizeWhitespace(node.getText()),
|
|
129
|
+
body: fullText,
|
|
130
|
+
exported,
|
|
131
|
+
hasComments,
|
|
132
|
+
}];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (ts.isEnumDeclaration(node)) {
|
|
136
|
+
return [{
|
|
137
|
+
name: node.name.text,
|
|
138
|
+
nodeType: "enum",
|
|
139
|
+
signature: normalizeWhitespace(node.getText()),
|
|
140
|
+
body: fullText,
|
|
141
|
+
exported,
|
|
142
|
+
hasComments,
|
|
143
|
+
}];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (ts.isVariableStatement(node)) {
|
|
147
|
+
const results: DeclInfo[] = [];
|
|
148
|
+
for (const decl of node.declarationList.declarations) {
|
|
149
|
+
if (ts.isIdentifier(decl.name)) {
|
|
150
|
+
const typeStr = decl.type ? `: ${decl.type.getText()}` : "";
|
|
151
|
+
const isArrowFn = decl.initializer && ts.isArrowFunction(decl.initializer);
|
|
152
|
+
results.push({
|
|
153
|
+
name: decl.name.text,
|
|
154
|
+
nodeType: isArrowFn ? "function" : "variable",
|
|
155
|
+
signature: `${decl.name.text}${typeStr}`,
|
|
156
|
+
body: fullText,
|
|
157
|
+
exported,
|
|
158
|
+
hasComments,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return results;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (ts.isImportDeclaration(node)) {
|
|
166
|
+
const importText = normalizeWhitespace(node.getText());
|
|
167
|
+
return [{
|
|
168
|
+
name: `import:${importText}`,
|
|
169
|
+
nodeType: "import",
|
|
170
|
+
signature: importText,
|
|
171
|
+
body: fullText,
|
|
172
|
+
exported: false,
|
|
173
|
+
hasComments,
|
|
174
|
+
}];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (ts.isExportDeclaration(node)) {
|
|
178
|
+
const exportText = normalizeWhitespace(node.getText());
|
|
179
|
+
return [{
|
|
180
|
+
name: `export:${exportText}`,
|
|
181
|
+
nodeType: "export",
|
|
182
|
+
signature: exportText,
|
|
183
|
+
body: fullText,
|
|
184
|
+
exported: true,
|
|
185
|
+
hasComments,
|
|
186
|
+
}];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function compareDeclarations(
|
|
193
|
+
oldDecls: Map<string, DeclInfo>,
|
|
194
|
+
newDecls: Map<string, DeclInfo>,
|
|
195
|
+
): SemanticDiffResult {
|
|
196
|
+
const changes: SemanticChange[] = [];
|
|
197
|
+
|
|
198
|
+
// Check removed declarations
|
|
199
|
+
for (const [name, oldInfo] of oldDecls) {
|
|
200
|
+
if (!newDecls.has(name)) {
|
|
201
|
+
changes.push({
|
|
202
|
+
kind: "removed",
|
|
203
|
+
name,
|
|
204
|
+
nodeType: oldInfo.nodeType,
|
|
205
|
+
breaking: oldInfo.exported,
|
|
206
|
+
detail: oldInfo.exported ? `exported ${oldInfo.nodeType} removed` : `${oldInfo.nodeType} removed`,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Check added and modified declarations
|
|
212
|
+
for (const [name, newInfo] of newDecls) {
|
|
213
|
+
const oldInfo = oldDecls.get(name);
|
|
214
|
+
|
|
215
|
+
if (!oldInfo) {
|
|
216
|
+
changes.push({
|
|
217
|
+
kind: "added",
|
|
218
|
+
name,
|
|
219
|
+
nodeType: newInfo.nodeType,
|
|
220
|
+
breaking: false,
|
|
221
|
+
detail: `new ${newInfo.nodeType}`,
|
|
222
|
+
});
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Same full text → no change
|
|
227
|
+
if (normalizeWhitespace(oldInfo.body) === normalizeWhitespace(newInfo.body)) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Check export modifier change
|
|
232
|
+
if (oldInfo.exported !== newInfo.exported) {
|
|
233
|
+
changes.push({
|
|
234
|
+
kind: "export-change",
|
|
235
|
+
name,
|
|
236
|
+
nodeType: newInfo.nodeType,
|
|
237
|
+
breaking: oldInfo.exported && !newInfo.exported, // removing export is breaking
|
|
238
|
+
detail: newInfo.exported ? "export added" : "export removed",
|
|
239
|
+
});
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Check if only comments changed
|
|
244
|
+
const oldNoComments = stripComments(oldInfo.body);
|
|
245
|
+
const newNoComments = stripComments(newInfo.body);
|
|
246
|
+
if (normalizeWhitespace(oldNoComments) === normalizeWhitespace(newNoComments)) {
|
|
247
|
+
changes.push({
|
|
248
|
+
kind: "comment-only",
|
|
249
|
+
name,
|
|
250
|
+
nodeType: oldInfo.nodeType,
|
|
251
|
+
breaking: false,
|
|
252
|
+
detail: "comments modified",
|
|
253
|
+
});
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Check if only whitespace changed
|
|
258
|
+
if (oldNoComments.replace(/\s+/g, "") === newNoComments.replace(/\s+/g, "")) {
|
|
259
|
+
changes.push({
|
|
260
|
+
kind: "whitespace-only",
|
|
261
|
+
name,
|
|
262
|
+
nodeType: oldInfo.nodeType,
|
|
263
|
+
breaking: false,
|
|
264
|
+
detail: "formatting changed",
|
|
265
|
+
});
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Signature changed?
|
|
270
|
+
if (oldInfo.signature !== newInfo.signature) {
|
|
271
|
+
// Check if it's just a type annotation change
|
|
272
|
+
const oldSigNoType = stripTypeAnnotations(oldInfo.signature);
|
|
273
|
+
const newSigNoType = stripTypeAnnotations(newInfo.signature);
|
|
274
|
+
if (oldSigNoType !== newSigNoType) {
|
|
275
|
+
changes.push({
|
|
276
|
+
kind: "signature-change",
|
|
277
|
+
name,
|
|
278
|
+
nodeType: oldInfo.nodeType,
|
|
279
|
+
breaking: oldInfo.exported,
|
|
280
|
+
detail: `${oldInfo.signature} → ${newInfo.signature}`,
|
|
281
|
+
});
|
|
282
|
+
} else {
|
|
283
|
+
changes.push({
|
|
284
|
+
kind: "type-change",
|
|
285
|
+
name,
|
|
286
|
+
nodeType: oldInfo.nodeType,
|
|
287
|
+
breaking: oldInfo.exported,
|
|
288
|
+
detail: "type annotation changed",
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Signature same, body different → body-only change
|
|
295
|
+
changes.push({
|
|
296
|
+
kind: "body-change",
|
|
297
|
+
name,
|
|
298
|
+
nodeType: oldInfo.nodeType,
|
|
299
|
+
breaking: false,
|
|
300
|
+
detail: "implementation changed",
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const hasBreakingChanges = changes.some(c => c.breaking);
|
|
305
|
+
const summary = buildSummary(changes);
|
|
306
|
+
|
|
307
|
+
return { changes, hasBreakingChanges, summary };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function buildSummary(changes: SemanticChange[]): string {
|
|
311
|
+
if (changes.length === 0) return "no semantic changes";
|
|
312
|
+
|
|
313
|
+
const counts: Record<string, number> = {};
|
|
314
|
+
for (const c of changes) {
|
|
315
|
+
counts[c.kind] = (counts[c.kind] ?? 0) + 1;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const parts: string[] = [];
|
|
319
|
+
for (const [kind, count] of Object.entries(counts)) {
|
|
320
|
+
parts.push(`${count} ${kind}`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const breaking = changes.filter(c => c.breaking).length;
|
|
324
|
+
if (breaking > 0) parts.push(`⚠️ ${breaking} breaking`);
|
|
325
|
+
|
|
326
|
+
return parts.join(", ");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ── Helpers ──
|
|
330
|
+
|
|
331
|
+
function hasExportModifier(node: ts.Statement): boolean {
|
|
332
|
+
if (!ts.canHaveModifiers(node)) return false;
|
|
333
|
+
const mods = ts.getModifiers(node);
|
|
334
|
+
return mods?.some(m => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function getFunctionSignature(node: ts.FunctionDeclaration): string {
|
|
338
|
+
const name = node.name?.text ?? "anonymous";
|
|
339
|
+
const params = node.parameters.map(p => {
|
|
340
|
+
const pName = p.name.getText();
|
|
341
|
+
const pType = p.type ? `: ${p.type.getText()}` : "";
|
|
342
|
+
const optional = p.questionToken ? "?" : "";
|
|
343
|
+
return `${pName}${optional}${pType}`;
|
|
344
|
+
}).join(", ");
|
|
345
|
+
const ret = node.type ? `: ${node.type.getText()}` : "";
|
|
346
|
+
return `${name}(${params})${ret}`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function getClassSignature(node: ts.ClassDeclaration): string {
|
|
350
|
+
const name = node.name?.text ?? "anonymous";
|
|
351
|
+
const heritage = node.heritageClauses?.map(h => h.getText()).join(" ") ?? "";
|
|
352
|
+
const members: string[] = [];
|
|
353
|
+
for (const member of node.members) {
|
|
354
|
+
if (ts.isPropertyDeclaration(member) || ts.isMethodDeclaration(member)) {
|
|
355
|
+
const mName = member.name?.getText() ?? "";
|
|
356
|
+
if (ts.isMethodDeclaration(member)) {
|
|
357
|
+
const params = member.parameters.map(p => p.getText()).join(", ");
|
|
358
|
+
const ret = member.type ? `: ${member.type.getText()}` : "";
|
|
359
|
+
members.push(`${mName}(${params})${ret}`);
|
|
360
|
+
} else {
|
|
361
|
+
const type = (member as ts.PropertyDeclaration).type?.getText() ?? "";
|
|
362
|
+
members.push(`${mName}: ${type}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return `class ${name} ${heritage} { ${members.join("; ")} }`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Strip type annotations from a signature, handling nested braces/parens/generics */
|
|
370
|
+
function stripTypeAnnotations(sig: string): string {
|
|
371
|
+
let result = "";
|
|
372
|
+
let depth = 0;
|
|
373
|
+
let inType = false;
|
|
374
|
+
for (let i = 0; i < sig.length; i++) {
|
|
375
|
+
const ch = sig[i];
|
|
376
|
+
if (ch === ":" && depth === 0 && !inType) {
|
|
377
|
+
inType = true;
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (inType) {
|
|
381
|
+
if (ch === "<" || ch === "{" || ch === "(") depth++;
|
|
382
|
+
else if (ch === ">" || ch === "}" || ch === ")") {
|
|
383
|
+
if (depth > 0) { depth--; continue; }
|
|
384
|
+
// depth === 0 and closing paren → end of type, keep the paren
|
|
385
|
+
inType = false;
|
|
386
|
+
result += ch;
|
|
387
|
+
} else if (ch === "," && depth === 0) {
|
|
388
|
+
inType = false;
|
|
389
|
+
result += ch;
|
|
390
|
+
}
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
result += ch;
|
|
394
|
+
}
|
|
395
|
+
return result;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function normalizeWhitespace(s: string): string {
|
|
399
|
+
return s.replace(/\s+/g, " ").trim();
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function stripComments(source: string): string {
|
|
403
|
+
return source
|
|
404
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
405
|
+
.replace(/\/\/.*$/gm, "")
|
|
406
|
+
.trim();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** Fallback for non-TS/JS files */
|
|
410
|
+
function textFallback(oldSource: string, newSource: string): SemanticDiffResult {
|
|
411
|
+
const oldLines = oldSource.split("\n");
|
|
412
|
+
const newLines = newSource.split("\n");
|
|
413
|
+
let added = 0, removed = 0, changed = 0;
|
|
414
|
+
const maxLen = Math.max(oldLines.length, newLines.length);
|
|
415
|
+
|
|
416
|
+
for (let i = 0; i < maxLen; i++) {
|
|
417
|
+
if (i >= oldLines.length) { added++; continue; }
|
|
418
|
+
if (i >= newLines.length) { removed++; continue; }
|
|
419
|
+
if (oldLines[i] !== newLines[i]) changed++;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const changes: SemanticChange[] = [];
|
|
423
|
+
if (added > 0) changes.push({ kind: "added", name: `${added} lines`, nodeType: "text", breaking: false });
|
|
424
|
+
if (removed > 0) changes.push({ kind: "removed", name: `${removed} lines`, nodeType: "text", breaking: false });
|
|
425
|
+
if (changed > 0) changes.push({ kind: "body-change", name: `${changed} lines`, nodeType: "text", breaking: false });
|
|
426
|
+
|
|
427
|
+
return {
|
|
428
|
+
changes,
|
|
429
|
+
hasBreakingChanges: false,
|
|
430
|
+
summary: `text diff: +${added} -${removed} ~${changed} lines`,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight telemetry helpers for CLI-side tracking.
|
|
3
|
+
* Uses a short-lived DB connection — no daemon required.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Database } from "bun:sqlite";
|
|
7
|
+
import { resolveWorkspacePaths } from "../constants";
|
|
8
|
+
import { existsSync } from "fs";
|
|
9
|
+
|
|
10
|
+
function openDb(): Database | null {
|
|
11
|
+
try {
|
|
12
|
+
const paths = resolveWorkspacePaths();
|
|
13
|
+
if (!existsSync(paths.dbFile)) return null;
|
|
14
|
+
const db = new Database(paths.dbFile);
|
|
15
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
16
|
+
return db;
|
|
17
|
+
} catch { return null; }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Track a CLI command invocation (fire-and-forget). */
|
|
21
|
+
export function trackCliCommand(command: string) {
|
|
22
|
+
const db = openDb();
|
|
23
|
+
if (!db) return;
|
|
24
|
+
try {
|
|
25
|
+
db.prepare("INSERT INTO telemetry (category, action, timestamp) VALUES (?, ?, ?)").run("cli", command, Date.now());
|
|
26
|
+
} catch { /* table may not exist yet — ignore */ }
|
|
27
|
+
finally { db.close(); }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface TelemetryRow {
|
|
31
|
+
category: string;
|
|
32
|
+
action: string;
|
|
33
|
+
detail: string | null;
|
|
34
|
+
duration_ms: number | null;
|
|
35
|
+
timestamp: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface TelemetrySummary {
|
|
39
|
+
cli: Record<string, number>;
|
|
40
|
+
mcp: Record<string, number>;
|
|
41
|
+
seam: { counts: Record<string, number>; avgDurationMs: Record<string, number> };
|
|
42
|
+
immune: Record<string, number>;
|
|
43
|
+
validator: Record<string, number>;
|
|
44
|
+
totalEvents: number;
|
|
45
|
+
periodDays: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Query aggregated telemetry for the last N days. */
|
|
49
|
+
export function queryTelemetry(days: number): TelemetrySummary {
|
|
50
|
+
const db = openDb();
|
|
51
|
+
const empty: TelemetrySummary = { cli: {}, mcp: {}, seam: { counts: {}, avgDurationMs: {} }, immune: {}, validator: {}, totalEvents: 0, periodDays: days };
|
|
52
|
+
if (!db) return empty;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const since = Date.now() - days * 86_400_000;
|
|
56
|
+
const rows = db.prepare(
|
|
57
|
+
"SELECT category, action, duration_ms FROM telemetry WHERE timestamp >= ?"
|
|
58
|
+
).all(since) as { category: string; action: string; duration_ms: number | null }[];
|
|
59
|
+
|
|
60
|
+
const result = { ...empty };
|
|
61
|
+
const seamDurations: Record<string, number[]> = {};
|
|
62
|
+
|
|
63
|
+
for (const row of rows) {
|
|
64
|
+
result.totalEvents++;
|
|
65
|
+
switch (row.category) {
|
|
66
|
+
case "cli":
|
|
67
|
+
result.cli[row.action] = (result.cli[row.action] ?? 0) + 1;
|
|
68
|
+
break;
|
|
69
|
+
case "mcp":
|
|
70
|
+
result.mcp[row.action] = (result.mcp[row.action] ?? 0) + 1;
|
|
71
|
+
break;
|
|
72
|
+
case "seam":
|
|
73
|
+
result.seam.counts[row.action] = (result.seam.counts[row.action] ?? 0) + 1;
|
|
74
|
+
if (row.duration_ms != null) {
|
|
75
|
+
(seamDurations[row.action] ??= []).push(row.duration_ms);
|
|
76
|
+
}
|
|
77
|
+
break;
|
|
78
|
+
case "immune":
|
|
79
|
+
result.immune[row.action] = (result.immune[row.action] ?? 0) + 1;
|
|
80
|
+
break;
|
|
81
|
+
case "validator":
|
|
82
|
+
result.validator[row.action] = (result.validator[row.action] ?? 0) + 1;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const [action, durations] of Object.entries(seamDurations)) {
|
|
88
|
+
result.seam.avgDurationMs[action] = Math.round(durations.reduce((a, b) => a + b, 0) / durations.length);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return result;
|
|
92
|
+
} catch { return empty; }
|
|
93
|
+
finally { db.close(); }
|
|
94
|
+
}
|