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
package/src/core/hologram.ts
CHANGED
|
@@ -1,243 +1,71 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// Export declarations (re-exports)
|
|
36
|
-
if (ts.isExportDeclaration(node)) {
|
|
37
|
-
return node.getText().replace(/\s+/g, " ").trim();
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Export assignment (export default ...)
|
|
41
|
-
if (ts.isExportAssignment(node)) {
|
|
42
|
-
return `export default ${getTypeName(node.expression)};`;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Type alias
|
|
46
|
-
if (ts.isTypeAliasDeclaration(node)) {
|
|
47
|
-
return collapseWhitespace(node.getText());
|
|
1
|
+
/**
|
|
2
|
+
* Hologram Engine — Language Dispatcher
|
|
3
|
+
*
|
|
4
|
+
* Routes file parsing to the appropriate tree-sitter extractor based on extension.
|
|
5
|
+
* Falls back to L0 (full source) for unsupported languages or parse errors.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { TreeSitterEngine } from "./hologram/engine";
|
|
9
|
+
import { tsExtractor } from "./hologram/ts-extractor";
|
|
10
|
+
import { pyExtractor } from "./hologram/py-extractor";
|
|
11
|
+
import { fallbackL0 } from "./hologram/fallback";
|
|
12
|
+
import { generateIncrementalHologram, setCachedHologram } from "./hologram/incremental";
|
|
13
|
+
import type { HologramResult, HologramOptions, LanguageExtractor } from "./hologram/types";
|
|
14
|
+
|
|
15
|
+
// Re-export types for backward compatibility
|
|
16
|
+
export type { HologramResult, HologramOptions } from "./hologram/types";
|
|
17
|
+
export { clearHologramCache } from "./hologram/incremental";
|
|
18
|
+
|
|
19
|
+
const extractors: LanguageExtractor[] = [tsExtractor, pyExtractor];
|
|
20
|
+
|
|
21
|
+
function detectExtractor(filePath: string): LanguageExtractor | null {
|
|
22
|
+
const ext = filePath.split(".").pop()?.toLowerCase() ?? "";
|
|
23
|
+
return extractors.find(e => e.extensions.includes(ext)) ?? null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function generateHologram(
|
|
27
|
+
filePath: string,
|
|
28
|
+
source: string,
|
|
29
|
+
options?: HologramOptions,
|
|
30
|
+
): Promise<HologramResult> {
|
|
31
|
+
const extractor = detectExtractor(filePath);
|
|
32
|
+
|
|
33
|
+
if (!extractor) {
|
|
34
|
+
return fallbackL0(filePath, source);
|
|
48
35
|
}
|
|
49
36
|
|
|
50
|
-
//
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (ts.isEnumDeclaration(node)) {
|
|
57
|
-
return extractEnum(node);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Class
|
|
61
|
-
if (ts.isClassDeclaration(node)) {
|
|
62
|
-
return extractClass(node);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Function declaration
|
|
66
|
-
if (ts.isFunctionDeclaration(node)) {
|
|
67
|
-
return extractFunction(node);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Variable statement (const/let/var — may contain arrow functions, objects, etc.)
|
|
71
|
-
if (ts.isVariableStatement(node)) {
|
|
72
|
-
return extractVariableStatement(node);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Fallback: skip unknown top-level statements
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function extractInterface(node: ts.InterfaceDeclaration): string {
|
|
80
|
-
const mods = getModifiers(node);
|
|
81
|
-
const name = node.name.text;
|
|
82
|
-
const ext = node.heritageClauses
|
|
83
|
-
? " " + node.heritageClauses.map(h => h.getText()).join(", ")
|
|
84
|
-
: "";
|
|
85
|
-
const members = node.members.map(m => {
|
|
86
|
-
const text = collapseWhitespace(m.getText()).replace(/;$/, "");
|
|
87
|
-
return " " + text + ";";
|
|
88
|
-
}).join("\n");
|
|
89
|
-
return `${mods}interface ${name}${ext} {\n${members}\n}`;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function extractEnum(node: ts.EnumDeclaration): string {
|
|
93
|
-
const mods = getModifiers(node);
|
|
94
|
-
const name = node.name.text;
|
|
95
|
-
const members = node.members.map(m => collapseWhitespace(m.getText())).join(", ");
|
|
96
|
-
return `${mods}enum ${name} { ${members} }`;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function extractClass(node: ts.ClassDeclaration): string {
|
|
100
|
-
const mods = getModifiers(node);
|
|
101
|
-
const name = node.name?.text ?? "Anonymous";
|
|
102
|
-
const ext = node.heritageClauses
|
|
103
|
-
? " " + node.heritageClauses.map(h => h.getText()).join(", ")
|
|
104
|
-
: "";
|
|
105
|
-
|
|
106
|
-
const members: string[] = [];
|
|
107
|
-
for (const member of node.members) {
|
|
108
|
-
if (ts.isPropertyDeclaration(member)) {
|
|
109
|
-
members.push(" " + extractProperty(member) + ";");
|
|
110
|
-
} else if (ts.isMethodDeclaration(member) || ts.isGetAccessor(member) || ts.isSetAccessor(member)) {
|
|
111
|
-
members.push(" " + extractMethodSignature(member) + ";");
|
|
112
|
-
} else if (ts.isConstructorDeclaration(member)) {
|
|
113
|
-
const params = extractParams(member.parameters);
|
|
114
|
-
members.push(` constructor(${params});`);
|
|
37
|
+
// Incremental diff mode
|
|
38
|
+
if (options?.diffOnly) {
|
|
39
|
+
try {
|
|
40
|
+
return await generateIncrementalHologram(filePath, source, extractor, options);
|
|
41
|
+
} catch {
|
|
42
|
+
return fallbackL0(filePath, source);
|
|
115
43
|
}
|
|
116
44
|
}
|
|
117
45
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (d.initializer) {
|
|
143
|
-
// Arrow function or function expression
|
|
144
|
-
if (ts.isArrowFunction(d.initializer) || ts.isFunctionExpression(d.initializer)) {
|
|
145
|
-
const fn = d.initializer;
|
|
146
|
-
const async = hasModifier(fn, ts.SyntaxKind.AsyncKeyword) ? "async " : "";
|
|
147
|
-
const typeParams = fn.typeParameters
|
|
148
|
-
? `<${fn.typeParameters.map(t => t.getText()).join(", ")}>`
|
|
149
|
-
: "";
|
|
150
|
-
const params = extractParams(fn.parameters);
|
|
151
|
-
const ret = fn.type ? ": " + collapseWhitespace(fn.type.getText()) : "";
|
|
152
|
-
return `${name} = ${async}${typeParams}(${params})${ret} => {…}`;
|
|
153
|
-
}
|
|
154
|
-
// Object/array/other — just show type or truncated value
|
|
155
|
-
if (typeAnn) return `${name}${typeAnn}`;
|
|
156
|
-
return `${name} = …`;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return `${name}${typeAnn}`;
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
return `${mods}${keyword} ${decls.join(", ")};`;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function extractProperty(node: ts.PropertyDeclaration): string {
|
|
166
|
-
const mods = getMemberModifiers(node);
|
|
167
|
-
const name = node.name.getText();
|
|
168
|
-
const type = node.type ? ": " + collapseWhitespace(node.type.getText()) : "";
|
|
169
|
-
const optional = node.questionToken ? "?" : "";
|
|
170
|
-
return `${mods}${name}${optional}${type}`;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function extractMethodSignature(node: ts.MethodDeclaration | ts.GetAccessorDeclaration | ts.SetAccessorDeclaration): string {
|
|
174
|
-
const mods = getMemberModifiers(node);
|
|
175
|
-
const name = node.name.getText();
|
|
176
|
-
const async = hasModifier(node, ts.SyntaxKind.AsyncKeyword) ? "async " : "";
|
|
177
|
-
|
|
178
|
-
if (ts.isGetAccessor(node)) {
|
|
179
|
-
const ret = node.type ? ": " + collapseWhitespace(node.type.getText()) : "";
|
|
180
|
-
return `${mods}get ${name}()${ret}`;
|
|
181
|
-
}
|
|
182
|
-
if (ts.isSetAccessor(node)) {
|
|
183
|
-
const params = extractParams(node.parameters);
|
|
184
|
-
return `${mods}set ${name}(${params})`;
|
|
46
|
+
try {
|
|
47
|
+
const engine = await TreeSitterEngine.getInstance();
|
|
48
|
+
const tree = await engine.parse(source, extractor.grammarName);
|
|
49
|
+
const lines = extractor.extract(tree, source, options);
|
|
50
|
+
tree.delete();
|
|
51
|
+
|
|
52
|
+
// Cache for future incremental diffs
|
|
53
|
+
setCachedHologram(filePath, lines);
|
|
54
|
+
|
|
55
|
+
const hologram = lines.join("\n");
|
|
56
|
+
const hologramLength = hologram.length;
|
|
57
|
+
const savings = source.length > 0
|
|
58
|
+
? Math.round((source.length - hologramLength) / source.length * 1000) / 10
|
|
59
|
+
: 0;
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
hologram,
|
|
63
|
+
originalLength: source.length,
|
|
64
|
+
hologramLength,
|
|
65
|
+
savings,
|
|
66
|
+
language: extractor.grammarName,
|
|
67
|
+
};
|
|
68
|
+
} catch {
|
|
69
|
+
return fallbackL0(filePath, source);
|
|
185
70
|
}
|
|
186
|
-
|
|
187
|
-
const md = node as ts.MethodDeclaration;
|
|
188
|
-
const typeParams = md.typeParameters
|
|
189
|
-
? `<${md.typeParameters.map(t => t.getText()).join(", ")}>`
|
|
190
|
-
: "";
|
|
191
|
-
const params = extractParams(md.parameters);
|
|
192
|
-
const ret = md.type ? ": " + collapseWhitespace(md.type.getText()) : "";
|
|
193
|
-
return `${mods}${async}${name}${typeParams}(${params})${ret}`;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function extractParams(params: ts.NodeArray<ts.ParameterDeclaration>): string {
|
|
197
|
-
return params.map(p => {
|
|
198
|
-
const name = p.name.getText();
|
|
199
|
-
const optional = p.questionToken ? "?" : "";
|
|
200
|
-
const type = p.type ? ": " + collapseWhitespace(p.type.getText()) : "";
|
|
201
|
-
const rest = p.dotDotDotToken ? "..." : "";
|
|
202
|
-
return `${rest}${name}${optional}${type}`;
|
|
203
|
-
}).join(", ");
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function getModifiers(node: ts.Node): string {
|
|
207
|
-
const mods = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
|
|
208
|
-
if (!mods) return "";
|
|
209
|
-
const relevant = mods
|
|
210
|
-
.filter(m => m.kind === ts.SyntaxKind.ExportKeyword || m.kind === ts.SyntaxKind.DefaultKeyword || m.kind === ts.SyntaxKind.DeclareKeyword)
|
|
211
|
-
.map(m => m.getText());
|
|
212
|
-
return relevant.length ? relevant.join(" ") + " " : "";
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function getMemberModifiers(node: ts.Node): string {
|
|
216
|
-
const mods = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
|
|
217
|
-
if (!mods) return "";
|
|
218
|
-
const relevant = mods
|
|
219
|
-
.filter(m =>
|
|
220
|
-
m.kind === ts.SyntaxKind.PublicKeyword ||
|
|
221
|
-
m.kind === ts.SyntaxKind.PrivateKeyword ||
|
|
222
|
-
m.kind === ts.SyntaxKind.ProtectedKeyword ||
|
|
223
|
-
m.kind === ts.SyntaxKind.StaticKeyword ||
|
|
224
|
-
m.kind === ts.SyntaxKind.ReadonlyKeyword ||
|
|
225
|
-
m.kind === ts.SyntaxKind.AbstractKeyword
|
|
226
|
-
)
|
|
227
|
-
.map(m => m.getText());
|
|
228
|
-
return relevant.length ? relevant.join(" ") + " " : "";
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function hasModifier(node: ts.Node, kind: ts.SyntaxKind): boolean {
|
|
232
|
-
const mods = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
|
|
233
|
-
return mods?.some(m => m.kind === kind) ?? false;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function getTypeName(node: ts.Node): string {
|
|
237
|
-
if (ts.isIdentifier(node)) return node.text;
|
|
238
|
-
return "…";
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function collapseWhitespace(s: string): string {
|
|
242
|
-
return s.replace(/\s+/g, " ").trim();
|
|
243
71
|
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
2
|
+
import { dirname } from "path";
|
|
3
|
+
import { resolveHookCommand } from "../platform";
|
|
4
|
+
|
|
5
|
+
export type HookOwner = "afd" | "omc" | "user";
|
|
6
|
+
|
|
7
|
+
export interface HookEntry {
|
|
8
|
+
id?: string;
|
|
9
|
+
matcher?: string;
|
|
10
|
+
command: string;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface HooksConfig {
|
|
15
|
+
hooks?: Record<string, HookEntry[]>;
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ManagedHook {
|
|
20
|
+
id: string;
|
|
21
|
+
matcher: string;
|
|
22
|
+
command: string;
|
|
23
|
+
owner: HookOwner;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface HookConflict {
|
|
27
|
+
type: "matcher-overlap" | "duplicate-id";
|
|
28
|
+
hookA: ManagedHook;
|
|
29
|
+
hookB: ManagedHook;
|
|
30
|
+
resolution: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface MergeResult {
|
|
34
|
+
merged: HookEntry[];
|
|
35
|
+
conflicts: HookConflict[];
|
|
36
|
+
changes: { added: string[]; removed: string[]; reordered: string[] };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface HookSummary {
|
|
40
|
+
zones: Record<HookOwner, ManagedHook[]>;
|
|
41
|
+
conflicts: HookConflict[];
|
|
42
|
+
orderingOk: boolean;
|
|
43
|
+
total: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Canonical set of afd-managed hooks.
|
|
48
|
+
* Only hooks in this set are removed during `stop --clean`.
|
|
49
|
+
* This prevents accidental deletion of user hooks with an `afd-` prefix
|
|
50
|
+
* (e.g., project-local `afd-read-gate` scripts).
|
|
51
|
+
*/
|
|
52
|
+
export const KNOWN_AFD_HOOKS = new Set(["afd-auto-heal"]);
|
|
53
|
+
|
|
54
|
+
/** afd's canonical desired hooks — authoritative source for merge. */
|
|
55
|
+
export function getAfdDesiredHooks(): HookEntry[] {
|
|
56
|
+
return [
|
|
57
|
+
{
|
|
58
|
+
id: "afd-auto-heal",
|
|
59
|
+
matcher: "Write|Edit|MultiEdit",
|
|
60
|
+
command: resolveHookCommand(),
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Classify a hook's owner by id prefix. */
|
|
66
|
+
export function classifyOwner(id: string): HookOwner {
|
|
67
|
+
if (id.startsWith("afd-")) return "afd";
|
|
68
|
+
if (id.startsWith("omc-")) return "omc";
|
|
69
|
+
return "user";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Classify a list of hook entries into owner zones. */
|
|
73
|
+
export function classifyHooks(entries: HookEntry[]): Map<HookOwner, ManagedHook[]> {
|
|
74
|
+
const zones = new Map<HookOwner, ManagedHook[]>([
|
|
75
|
+
["afd", []],
|
|
76
|
+
["omc", []],
|
|
77
|
+
["user", []],
|
|
78
|
+
]);
|
|
79
|
+
let anonIdx = 0;
|
|
80
|
+
for (const entry of entries) {
|
|
81
|
+
const id = entry.id ?? `user-anonymous-${anonIdx++}`;
|
|
82
|
+
const owner = classifyOwner(id);
|
|
83
|
+
zones.get(owner)!.push({
|
|
84
|
+
id,
|
|
85
|
+
matcher: entry.matcher ?? "",
|
|
86
|
+
command: entry.command,
|
|
87
|
+
owner,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return zones;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Detect conflicts between hooks from different owners. */
|
|
94
|
+
export function detectConflicts(hooks: ManagedHook[]): HookConflict[] {
|
|
95
|
+
const conflicts: HookConflict[] = [];
|
|
96
|
+
|
|
97
|
+
// 1. Duplicate ID check
|
|
98
|
+
const idMap = new Map<string, ManagedHook>();
|
|
99
|
+
for (const hook of hooks) {
|
|
100
|
+
if (idMap.has(hook.id)) {
|
|
101
|
+
conflicts.push({
|
|
102
|
+
type: "duplicate-id",
|
|
103
|
+
hookA: idMap.get(hook.id)!,
|
|
104
|
+
hookB: hook,
|
|
105
|
+
resolution: `Remove or rename one of the duplicate '${hook.id}' hooks`,
|
|
106
|
+
});
|
|
107
|
+
} else {
|
|
108
|
+
idMap.set(hook.id, hook);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 2. Matcher overlap check (cross-owner only, O(n^2) — safe for <20 hooks)
|
|
113
|
+
for (let i = 0; i < hooks.length; i++) {
|
|
114
|
+
for (let j = i + 1; j < hooks.length; j++) {
|
|
115
|
+
if (hooks[i].owner === hooks[j].owner) continue;
|
|
116
|
+
const matcherA = hooks[i].matcher || "*";
|
|
117
|
+
const matcherB = hooks[j].matcher || "*";
|
|
118
|
+
const setA = new Set(matcherA.split("|").map(s => s.trim()));
|
|
119
|
+
const setB = new Set(matcherB.split("|").map(s => s.trim()));
|
|
120
|
+
const aIsWild = setA.has("*") || setA.has("");
|
|
121
|
+
const bIsWild = setB.has("*") || setB.has("");
|
|
122
|
+
const overlap =
|
|
123
|
+
aIsWild || bIsWild || [...setA].some(m => setB.has(m));
|
|
124
|
+
if (overlap) {
|
|
125
|
+
const shared =
|
|
126
|
+
aIsWild || bIsWild
|
|
127
|
+
? "*"
|
|
128
|
+
: [...setA].filter(m => setB.has(m)).join("|");
|
|
129
|
+
conflicts.push({
|
|
130
|
+
type: "matcher-overlap",
|
|
131
|
+
hookA: hooks[i],
|
|
132
|
+
hookB: hooks[j],
|
|
133
|
+
resolution: `Both hooks trigger on '${shared}'. Verify they don't conflict in behavior.`,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return conflicts;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Read hooks.json from disk, returning empty config on missing/invalid file. */
|
|
143
|
+
export function readHooksFile(hooksPath: string): HooksConfig {
|
|
144
|
+
if (!existsSync(hooksPath)) return { hooks: {} };
|
|
145
|
+
try {
|
|
146
|
+
return JSON.parse(readFileSync(hooksPath, "utf-8")) as HooksConfig;
|
|
147
|
+
} catch {
|
|
148
|
+
return { hooks: {} };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Write hooks.json to disk, creating parent directory if needed. */
|
|
153
|
+
export function writeHooksFile(hooksPath: string, config: HooksConfig): void {
|
|
154
|
+
mkdirSync(dirname(hooksPath), { recursive: true });
|
|
155
|
+
writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Merge current hook entries with the desired afd hooks.
|
|
160
|
+
* Ordering guarantee: afd → omc → user.
|
|
161
|
+
* afd zone is fully authoritative — desired list overwrites existing afd hooks.
|
|
162
|
+
* omc and user zones are preserved as-is from the current file.
|
|
163
|
+
*/
|
|
164
|
+
export function mergeHooks(
|
|
165
|
+
current: HookEntry[],
|
|
166
|
+
desiredAfd: HookEntry[],
|
|
167
|
+
): MergeResult {
|
|
168
|
+
const changes: MergeResult["changes"] = { added: [], removed: [], reordered: [] };
|
|
169
|
+
|
|
170
|
+
const zones = classifyHooks(current);
|
|
171
|
+
const afdExisting = zones.get("afd")!;
|
|
172
|
+
const omcHooks = zones.get("omc")!;
|
|
173
|
+
const userHooks = zones.get("user")!;
|
|
174
|
+
|
|
175
|
+
// Build merged afd zone from desired list (authoritative)
|
|
176
|
+
const mergedAfd: HookEntry[] = desiredAfd.map(desired => {
|
|
177
|
+
const existing = afdExisting.find(h => h.id === desired.id);
|
|
178
|
+
if (!existing) changes.added.push(desired.id!);
|
|
179
|
+
return desired;
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Track explicitly removed afd hooks (only canonical ones)
|
|
183
|
+
for (const existing of afdExisting) {
|
|
184
|
+
if (KNOWN_AFD_HOOKS.has(existing.id) && !desiredAfd.find(d => d.id === existing.id)) {
|
|
185
|
+
changes.removed.push(existing.id);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Detect reordering (only if no add/remove)
|
|
190
|
+
if (changes.added.length === 0 && changes.removed.length === 0) {
|
|
191
|
+
const originalOrder = current.map(h => h.id ?? "");
|
|
192
|
+
const newOrder = [
|
|
193
|
+
...mergedAfd.map(h => h.id!),
|
|
194
|
+
...omcHooks.map(h => h.id),
|
|
195
|
+
...userHooks.map(h => h.id),
|
|
196
|
+
];
|
|
197
|
+
const hasReordering =
|
|
198
|
+
originalOrder.length !== newOrder.length ||
|
|
199
|
+
originalOrder.some((id, i) => id !== (newOrder[i] ?? ""));
|
|
200
|
+
if (hasReordering) {
|
|
201
|
+
changes.reordered.push("hooks reordered to afd → omc → user zones");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const merged: HookEntry[] = [
|
|
206
|
+
...mergedAfd,
|
|
207
|
+
...omcHooks.map(({ id, matcher, command }) => ({ id, matcher, command })),
|
|
208
|
+
...userHooks.map(({ id, matcher, command }) => ({ id, matcher, command })),
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
const allManaged: ManagedHook[] = [
|
|
212
|
+
...mergedAfd.map(h => ({
|
|
213
|
+
id: h.id!,
|
|
214
|
+
matcher: h.matcher ?? "",
|
|
215
|
+
command: h.command,
|
|
216
|
+
owner: "afd" as HookOwner,
|
|
217
|
+
})),
|
|
218
|
+
...omcHooks,
|
|
219
|
+
...userHooks,
|
|
220
|
+
];
|
|
221
|
+
const conflicts = detectConflicts(allManaged);
|
|
222
|
+
|
|
223
|
+
return { merged, conflicts, changes };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Get a summary of current hook state for display and status commands. */
|
|
227
|
+
export function getHookSummary(hooksPath: string): HookSummary {
|
|
228
|
+
const config = readHooksFile(hooksPath);
|
|
229
|
+
const entries = config.hooks?.PreToolUse ?? [];
|
|
230
|
+
const zones = classifyHooks(entries);
|
|
231
|
+
|
|
232
|
+
const allHooks: ManagedHook[] = [
|
|
233
|
+
...(zones.get("afd") ?? []),
|
|
234
|
+
...(zones.get("omc") ?? []),
|
|
235
|
+
...(zones.get("user") ?? []),
|
|
236
|
+
];
|
|
237
|
+
const conflicts = detectConflicts(allHooks);
|
|
238
|
+
|
|
239
|
+
// Check ordering invariant: afd(0) → omc(1) → user(2)
|
|
240
|
+
const ownerPriority: Record<HookOwner, number> = { afd: 0, omc: 1, user: 2 };
|
|
241
|
+
let orderingOk = true;
|
|
242
|
+
let lastPriority = -1;
|
|
243
|
+
for (const entry of entries) {
|
|
244
|
+
const id = entry.id ?? "";
|
|
245
|
+
const priority = ownerPriority[classifyOwner(id)];
|
|
246
|
+
if (priority < lastPriority) {
|
|
247
|
+
orderingOk = false;
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
lastPriority = priority;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
zones: Object.fromEntries(zones) as Record<HookOwner, ManagedHook[]>,
|
|
255
|
+
conflicts,
|
|
256
|
+
orderingOk,
|
|
257
|
+
total: entries.length,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
@@ -45,6 +45,8 @@ export interface MessageDict {
|
|
|
45
45
|
SCORE_HOLOGRAM_EFFICIENCY: string;
|
|
46
46
|
SCORE_HOLOGRAM_EMPTY: string;
|
|
47
47
|
SCORE_HOLOGRAM_HINT: string;
|
|
48
|
+
SCORE_HOLOGRAM_TODAY: string;
|
|
49
|
+
SCORE_HOLOGRAM_LIFETIME: string;
|
|
48
50
|
SCORE_IMMUNE_TITLE: string;
|
|
49
51
|
SCORE_ANTIBODIES: string;
|
|
50
52
|
SCORE_LEVEL: string;
|
|
@@ -79,6 +81,19 @@ export interface MessageDict {
|
|
|
79
81
|
DAEMON_NOT_RUNNING: string;
|
|
80
82
|
DAEMON_NOT_RESPONDING: string;
|
|
81
83
|
DAEMON_START_FAILED: string; // "{path}"
|
|
84
|
+
DAEMON_RESTARTING: string;
|
|
85
|
+
|
|
86
|
+
// ── Setup checklist ──
|
|
87
|
+
SETUP_HEADER: string; // "{ecosystem}"
|
|
88
|
+
SETUP_HOOKS_NEW: string;
|
|
89
|
+
SETUP_HOOKS_OK: string;
|
|
90
|
+
SETUP_MCP_NEW: string;
|
|
91
|
+
SETUP_MCP_OK: string;
|
|
92
|
+
SETUP_MCP_SKIP: string;
|
|
93
|
+
SETUP_STATUS_NEW: string;
|
|
94
|
+
SETUP_STATUS_OK: string;
|
|
95
|
+
SETUP_STATUS_SKIP: string;
|
|
96
|
+
SETUP_DONE: string;
|
|
82
97
|
}
|
|
83
98
|
|
|
84
99
|
const en: MessageDict = {
|
|
@@ -133,6 +148,8 @@ const en: MessageDict = {
|
|
|
133
148
|
SCORE_HOLOGRAM_EFFICIENCY: "Efficiency",
|
|
134
149
|
SCORE_HOLOGRAM_EMPTY: "No hologram requests yet.",
|
|
135
150
|
SCORE_HOLOGRAM_HINT: "Use: GET /hologram?file=<path>",
|
|
151
|
+
SCORE_HOLOGRAM_TODAY: "Today",
|
|
152
|
+
SCORE_HOLOGRAM_LIFETIME: "All-time",
|
|
136
153
|
SCORE_IMMUNE_TITLE: "Immune System",
|
|
137
154
|
SCORE_ANTIBODIES: "Antibodies",
|
|
138
155
|
SCORE_LEVEL: "Level",
|
|
@@ -163,6 +180,18 @@ const en: MessageDict = {
|
|
|
163
180
|
DAEMON_NOT_RUNNING: "[afd] No daemon running.",
|
|
164
181
|
DAEMON_NOT_RESPONDING: "[afd] Daemon not responding. Cleaning up stale PID files.",
|
|
165
182
|
DAEMON_START_FAILED: "[afd] Failed to start daemon. Check logs: {path}",
|
|
183
|
+
DAEMON_RESTARTING: "[afd] 🔄 Restarting daemon...",
|
|
184
|
+
|
|
185
|
+
SETUP_HEADER: "[afd] Setting up {ecosystem} ecosystem:",
|
|
186
|
+
SETUP_HOOKS_NEW: " [+] Hook injected into PreToolUse",
|
|
187
|
+
SETUP_HOOKS_OK: " [=] Hook already present",
|
|
188
|
+
SETUP_MCP_NEW: " [+] MCP server registered in .mcp.json",
|
|
189
|
+
SETUP_MCP_OK: " [=] MCP server already registered",
|
|
190
|
+
SETUP_MCP_SKIP: " [-] MCP registration not available",
|
|
191
|
+
SETUP_STATUS_NEW: " [+] StatusLine configured",
|
|
192
|
+
SETUP_STATUS_OK: " [=] StatusLine already configured",
|
|
193
|
+
SETUP_STATUS_SKIP: " [-] StatusLine not available",
|
|
194
|
+
SETUP_DONE: "[afd] Zero-touch setup complete. All channels active.",
|
|
166
195
|
};
|
|
167
196
|
|
|
168
197
|
const ko: MessageDict = {
|
|
@@ -217,6 +246,8 @@ const ko: MessageDict = {
|
|
|
217
246
|
SCORE_HOLOGRAM_EFFICIENCY: "압축 효율",
|
|
218
247
|
SCORE_HOLOGRAM_EMPTY: "아직 홀로그램 요청이 없습니다.",
|
|
219
248
|
SCORE_HOLOGRAM_HINT: "사용법: GET /hologram?file=<경로>",
|
|
249
|
+
SCORE_HOLOGRAM_TODAY: "오늘",
|
|
250
|
+
SCORE_HOLOGRAM_LIFETIME: "누적",
|
|
220
251
|
SCORE_IMMUNE_TITLE: "면역 시스템",
|
|
221
252
|
SCORE_ANTIBODIES: "항체 수",
|
|
222
253
|
SCORE_LEVEL: "방어 레벨",
|
|
@@ -247,6 +278,18 @@ const ko: MessageDict = {
|
|
|
247
278
|
DAEMON_NOT_RUNNING: "[afd] 실행 중인 데몬을 찾을 수 없습니다.",
|
|
248
279
|
DAEMON_NOT_RESPONDING: "[afd] 데몬이 응답하지 않네요. 남은 PID 파일을 정리합니다.",
|
|
249
280
|
DAEMON_START_FAILED: "[afd] 데몬 시작 실패. 로그를 확인해 보세요: {path}",
|
|
281
|
+
DAEMON_RESTARTING: "[afd] 🔄 데몬을 재시작합니다...",
|
|
282
|
+
|
|
283
|
+
SETUP_HEADER: "[afd] {ecosystem} 에코시스템 설정 중:",
|
|
284
|
+
SETUP_HOOKS_NEW: " [+] PreToolUse 훅 주입 완료",
|
|
285
|
+
SETUP_HOOKS_OK: " [=] 훅 이미 설정됨",
|
|
286
|
+
SETUP_MCP_NEW: " [+] MCP 서버 등록 완료 (.mcp.json)",
|
|
287
|
+
SETUP_MCP_OK: " [=] MCP 서버 이미 등록됨",
|
|
288
|
+
SETUP_MCP_SKIP: " [-] MCP 등록 미지원",
|
|
289
|
+
SETUP_STATUS_NEW: " [+] StatusLine 설정 완료",
|
|
290
|
+
SETUP_STATUS_OK: " [=] StatusLine 이미 설정됨",
|
|
291
|
+
SETUP_STATUS_SKIP: " [-] StatusLine 미지원",
|
|
292
|
+
SETUP_DONE: "[afd] 제로터치 설정 완료. 모든 채널 활성화.",
|
|
250
293
|
};
|
|
251
294
|
|
|
252
295
|
const dictionaries: Record<SupportedLang, MessageDict> = { en, ko };
|