autonomous-flow-daemon 1.1.0 → 1.9.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 +85 -46
- package/LICENSE +21 -21
- package/README-ko.md +282 -0
- package/README.md +282 -337
- package/mcp-config.json +10 -10
- package/package.json +14 -6
- package/src/adapters/index.ts +370 -159
- package/src/cli.ts +162 -57
- package/src/commands/benchmark.ts +187 -0
- package/src/commands/correlate.ts +180 -0
- package/src/commands/dashboard.ts +404 -0
- package/src/commands/diagnose.ts +56 -14
- package/src/commands/doctor.ts +243 -0
- package/src/commands/evolution.ts +190 -0
- package/src/commands/fix.ts +158 -138
- package/src/commands/hooks.ts +136 -0
- package/src/commands/lang.ts +41 -41
- package/src/commands/mcp.ts +129 -0
- package/src/commands/plugin.ts +110 -0
- package/src/commands/restart.ts +14 -0
- package/src/commands/score.ts +276 -208
- package/src/commands/start.ts +155 -96
- package/src/commands/stats.ts +103 -0
- package/src/commands/status.ts +157 -0
- package/src/commands/stop.ts +68 -49
- package/src/commands/suggest.ts +211 -0
- package/src/commands/sync.ts +567 -21
- package/src/commands/vaccine.ts +177 -0
- package/src/constants.ts +32 -8
- package/src/core/boast.ts +280 -265
- package/src/core/config.ts +49 -49
- package/src/core/correlation-engine.ts +265 -0
- package/src/core/db.ts +145 -46
- package/src/core/discovery.ts +65 -65
- package/src/core/evolution.ts +215 -0
- package/src/core/federation.ts +129 -0
- package/src/core/hologram/engine.ts +71 -0
- package/src/core/hologram/fallback.ts +11 -0
- package/src/core/hologram/go-extractor.ts +203 -0
- package/src/core/hologram/incremental.ts +227 -0
- package/src/core/hologram/py-extractor.ts +132 -0
- package/src/core/hologram/rust-extractor.ts +244 -0
- package/src/core/hologram/ts-extractor.ts +406 -0
- package/src/core/hologram/types.ts +27 -0
- package/src/core/hologram.ts +73 -243
- package/src/core/hook-manager.ts +259 -0
- package/src/core/i18n/messages.ts +309 -266
- package/src/core/immune.ts +8 -123
- package/src/core/locale.ts +88 -88
- 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 +74 -66
- package/src/core/plugin-manager.ts +225 -0
- package/src/core/rule-engine.ts +287 -0
- package/src/core/rule-suggestion.ts +127 -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/validator-generator.ts +224 -0
- package/src/core/workspace.ts +28 -0
- package/src/core/yaml-minimal.ts +176 -0
- package/src/daemon/client.ts +78 -37
- package/src/daemon/event-batcher.ts +108 -0
- package/src/daemon/guards.ts +13 -0
- package/src/daemon/http-routes.ts +376 -0
- package/src/daemon/mcp-handler.ts +575 -0
- package/src/daemon/mcp-subscriptions.ts +81 -0
- package/src/daemon/mesh.ts +51 -0
- package/src/daemon/server.ts +655 -504
- package/src/daemon/types.ts +121 -0
- package/src/daemon/workspace-map.ts +104 -0
- package/src/platform.ts +60 -39
- package/src/version.ts +15 -0
- package/README.ko.md +0 -306
package/src/core/immune.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { diagnoseWithRules, loadAllRules, evaluateRules } from "./rule-engine";
|
|
2
|
+
export type { DiagnosticRule } from "./rule-engine";
|
|
3
|
+
export { loadAllRules, evaluateRules };
|
|
2
4
|
|
|
3
5
|
// RFC 6902 JSON-Patch operation
|
|
4
6
|
export interface PatchOp {
|
|
@@ -24,127 +26,10 @@ export interface DiagnosisResult {
|
|
|
24
26
|
healthy: string[];
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
*.log
|
|
32
|
-
.env
|
|
33
|
-
`;
|
|
34
|
-
|
|
35
|
-
const HOOKS_DEFAULT = `{
|
|
36
|
-
"hooks": []
|
|
37
|
-
}
|
|
38
|
-
`;
|
|
39
|
-
|
|
40
|
-
type Check = () => Symptom | null;
|
|
41
|
-
|
|
42
|
-
const checks: Check[] = [
|
|
43
|
-
// Check 1: Missing .claudeignore
|
|
44
|
-
() => {
|
|
45
|
-
if (existsSync(".claudeignore")) return null;
|
|
46
|
-
return {
|
|
47
|
-
id: "IMM-001",
|
|
48
|
-
patternType: "missing-file",
|
|
49
|
-
fileTarget: ".claudeignore",
|
|
50
|
-
title: "Missing .claudeignore",
|
|
51
|
-
description:
|
|
52
|
-
"No .claudeignore found. Without it, AI agents ingest node_modules, build artifacts, and other noise — wasting tokens and degrading context quality.",
|
|
53
|
-
severity: "critical",
|
|
54
|
-
patches: [
|
|
55
|
-
{ op: "add", path: "/.claudeignore", value: CLAUDEIGNORE_DEFAULT },
|
|
56
|
-
],
|
|
57
|
-
};
|
|
58
|
-
},
|
|
59
|
-
|
|
60
|
-
// Check 2: Missing .claude/hooks.json fallback
|
|
61
|
-
() => {
|
|
62
|
-
const hooksPath = ".claude/hooks.json";
|
|
63
|
-
if (existsSync(hooksPath)) {
|
|
64
|
-
try {
|
|
65
|
-
const content = readFileSync(hooksPath, "utf-8");
|
|
66
|
-
JSON.parse(content);
|
|
67
|
-
return null;
|
|
68
|
-
} catch {
|
|
69
|
-
return {
|
|
70
|
-
id: "IMM-002",
|
|
71
|
-
patternType: "invalid-json",
|
|
72
|
-
fileTarget: hooksPath,
|
|
73
|
-
title: "Invalid hooks.json",
|
|
74
|
-
description:
|
|
75
|
-
"hooks.json exists but contains invalid JSON. Agents may fail silently when hooks cannot be parsed.",
|
|
76
|
-
severity: "critical",
|
|
77
|
-
patches: [
|
|
78
|
-
{ op: "replace", path: "/.claude/hooks.json", value: HOOKS_DEFAULT },
|
|
79
|
-
],
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return {
|
|
84
|
-
id: "IMM-002",
|
|
85
|
-
patternType: "missing-file",
|
|
86
|
-
fileTarget: hooksPath,
|
|
87
|
-
title: "No fallback in hooks.json",
|
|
88
|
-
description:
|
|
89
|
-
"No .claude/hooks.json found. Without a hooks file, pre/post-command automation cannot be configured.",
|
|
90
|
-
severity: "warning",
|
|
91
|
-
patches: [
|
|
92
|
-
{ op: "add", path: "/.claude/hooks.json", value: HOOKS_DEFAULT },
|
|
93
|
-
],
|
|
94
|
-
};
|
|
95
|
-
},
|
|
96
|
-
|
|
97
|
-
// Check 3: CLAUDE.md missing or empty
|
|
98
|
-
() => {
|
|
99
|
-
if (!existsSync("CLAUDE.md")) {
|
|
100
|
-
return {
|
|
101
|
-
id: "IMM-003",
|
|
102
|
-
patternType: "missing-file",
|
|
103
|
-
fileTarget: "CLAUDE.md",
|
|
104
|
-
title: "Missing CLAUDE.md",
|
|
105
|
-
description:
|
|
106
|
-
"No CLAUDE.md found. AI agents have no project constitution to follow.",
|
|
107
|
-
severity: "critical",
|
|
108
|
-
patches: [
|
|
109
|
-
{ op: "add", path: "/CLAUDE.md", value: "# Project Constitution\n\n<!-- Add project rules here -->\n" },
|
|
110
|
-
],
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
const content = readFileSync("CLAUDE.md", "utf-8").trim();
|
|
114
|
-
if (content.length < 20) {
|
|
115
|
-
return {
|
|
116
|
-
id: "IMM-003",
|
|
117
|
-
patternType: "insufficient-content",
|
|
118
|
-
fileTarget: "CLAUDE.md",
|
|
119
|
-
title: "CLAUDE.md is nearly empty",
|
|
120
|
-
description:
|
|
121
|
-
`CLAUDE.md has only ${content.length} chars. Agents work better with clear project rules.`,
|
|
122
|
-
severity: "info",
|
|
123
|
-
patches: [],
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
return null;
|
|
127
|
-
},
|
|
128
|
-
];
|
|
129
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Run diagnosis using the rule engine.
|
|
31
|
+
* Loads built-in rules + custom .afd/rules/*.yml rules.
|
|
32
|
+
*/
|
|
130
33
|
export function diagnose(knownAntibodies: string[], opts?: { raw?: boolean }): DiagnosisResult {
|
|
131
|
-
|
|
132
|
-
const healthy: string[] = [];
|
|
133
|
-
|
|
134
|
-
for (const check of checks) {
|
|
135
|
-
const symptom = check();
|
|
136
|
-
if (!symptom) {
|
|
137
|
-
healthy.push("OK");
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
// In raw mode, report all symptoms regardless of antibodies
|
|
141
|
-
// (used by auto-heal to detect regressions)
|
|
142
|
-
if (!opts?.raw && knownAntibodies.includes(symptom.id)) {
|
|
143
|
-
healthy.push(`${symptom.id} (immunized)`);
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
symptoms.push(symptom);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return { symptoms, healthy };
|
|
34
|
+
return diagnoseWithRules(knownAntibodies, opts);
|
|
150
35
|
}
|
package/src/core/locale.ts
CHANGED
|
@@ -1,88 +1,88 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OS Locale Detection — cached per-process.
|
|
3
|
-
*
|
|
4
|
-
* Priority:
|
|
5
|
-
* 0. ~/.afdrc config file (persistent user preference via `afd lang`)
|
|
6
|
-
* 1. AFD_LANG env (explicit override per-session)
|
|
7
|
-
* 2. LC_ALL / LANG / LANGUAGE env (user shell config, skip "C"/"POSIX")
|
|
8
|
-
* 3. macOS AppleLocale (system preferences — solves LANG=C.UTF-8 on Korean macOS)
|
|
9
|
-
* 4. Intl API (runtime default)
|
|
10
|
-
* 5. Default: "ko"
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { execSync } from "child_process";
|
|
14
|
-
import { readConfig } from "./config";
|
|
15
|
-
|
|
16
|
-
export type SupportedLang = "en" | "ko";
|
|
17
|
-
|
|
18
|
-
const SUPPORTED: SupportedLang[] = ["en", "ko"];
|
|
19
|
-
|
|
20
|
-
let cached: SupportedLang | null = null;
|
|
21
|
-
|
|
22
|
-
function isSupported(value: string): value is SupportedLang {
|
|
23
|
-
return SUPPORTED.includes(value as SupportedLang);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function matchKo(value: string): boolean {
|
|
27
|
-
return value.startsWith("ko");
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Detect system language. Returns 'ko' or 'en'. */
|
|
31
|
-
export function getSystemLanguage(): SupportedLang {
|
|
32
|
-
if (cached) return cached;
|
|
33
|
-
|
|
34
|
-
// 0. Persistent config (~/.afdrc)
|
|
35
|
-
const rc = readConfig();
|
|
36
|
-
if (rc.lang && isSupported(rc.lang)) {
|
|
37
|
-
cached = rc.lang as SupportedLang;
|
|
38
|
-
return cached;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// 1. Explicit env override
|
|
42
|
-
const afdLang = process.env.AFD_LANG ?? "";
|
|
43
|
-
if (isSupported(afdLang)) { cached = afdLang; return cached; }
|
|
44
|
-
if (matchKo(afdLang)) { cached = "ko"; return cached; }
|
|
45
|
-
|
|
46
|
-
// 2. Standard env — skip generic "C" / "POSIX"
|
|
47
|
-
const envLang = process.env.LC_ALL || process.env.LANG || process.env.LANGUAGE || "";
|
|
48
|
-
if (envLang !== "" && !envLang.startsWith("C") && !envLang.startsWith("POSIX")) {
|
|
49
|
-
if (matchKo(envLang)) { cached = "ko"; return cached; }
|
|
50
|
-
cached = "en";
|
|
51
|
-
return cached;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// 3. macOS: AppleLocale
|
|
55
|
-
if (process.platform === "darwin") {
|
|
56
|
-
try {
|
|
57
|
-
const appleLocale = execSync("defaults read -g AppleLocale", {
|
|
58
|
-
encoding: "utf-8",
|
|
59
|
-
timeout: 500,
|
|
60
|
-
}).trim();
|
|
61
|
-
if (matchKo(appleLocale)) { cached = "ko"; return cached; }
|
|
62
|
-
} catch {
|
|
63
|
-
// Not macOS or defaults unavailable
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// 4. Intl API fallback
|
|
68
|
-
try {
|
|
69
|
-
const intlLocale = Intl.DateTimeFormat().resolvedOptions().locale;
|
|
70
|
-
if (matchKo(intlLocale)) { cached = "ko"; return cached; }
|
|
71
|
-
} catch {
|
|
72
|
-
// Fallback to default
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// 5. Default
|
|
76
|
-
cached = "ko";
|
|
77
|
-
return cached;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** Get list of supported languages. */
|
|
81
|
-
export function getSupportedLanguages(): SupportedLang[] {
|
|
82
|
-
return [...SUPPORTED];
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** Override locale (for testing or after `afd lang` write). */
|
|
86
|
-
export function setLanguageOverride(lang: SupportedLang | null): void {
|
|
87
|
-
cached = lang;
|
|
88
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* OS Locale Detection — cached per-process.
|
|
3
|
+
*
|
|
4
|
+
* Priority:
|
|
5
|
+
* 0. ~/.afdrc config file (persistent user preference via `afd lang`)
|
|
6
|
+
* 1. AFD_LANG env (explicit override per-session)
|
|
7
|
+
* 2. LC_ALL / LANG / LANGUAGE env (user shell config, skip "C"/"POSIX")
|
|
8
|
+
* 3. macOS AppleLocale (system preferences — solves LANG=C.UTF-8 on Korean macOS)
|
|
9
|
+
* 4. Intl API (runtime default)
|
|
10
|
+
* 5. Default: "ko"
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { execSync } from "child_process";
|
|
14
|
+
import { readConfig } from "./config";
|
|
15
|
+
|
|
16
|
+
export type SupportedLang = "en" | "ko";
|
|
17
|
+
|
|
18
|
+
const SUPPORTED: SupportedLang[] = ["en", "ko"];
|
|
19
|
+
|
|
20
|
+
let cached: SupportedLang | null = null;
|
|
21
|
+
|
|
22
|
+
function isSupported(value: string): value is SupportedLang {
|
|
23
|
+
return SUPPORTED.includes(value as SupportedLang);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function matchKo(value: string): boolean {
|
|
27
|
+
return value.startsWith("ko");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Detect system language. Returns 'ko' or 'en'. */
|
|
31
|
+
export function getSystemLanguage(): SupportedLang {
|
|
32
|
+
if (cached) return cached;
|
|
33
|
+
|
|
34
|
+
// 0. Persistent config (~/.afdrc)
|
|
35
|
+
const rc = readConfig();
|
|
36
|
+
if (rc.lang && isSupported(rc.lang)) {
|
|
37
|
+
cached = rc.lang as SupportedLang;
|
|
38
|
+
return cached;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 1. Explicit env override
|
|
42
|
+
const afdLang = process.env.AFD_LANG ?? "";
|
|
43
|
+
if (isSupported(afdLang)) { cached = afdLang; return cached; }
|
|
44
|
+
if (matchKo(afdLang)) { cached = "ko"; return cached; }
|
|
45
|
+
|
|
46
|
+
// 2. Standard env — skip generic "C" / "POSIX"
|
|
47
|
+
const envLang = process.env.LC_ALL || process.env.LANG || process.env.LANGUAGE || "";
|
|
48
|
+
if (envLang !== "" && !envLang.startsWith("C") && !envLang.startsWith("POSIX")) {
|
|
49
|
+
if (matchKo(envLang)) { cached = "ko"; return cached; }
|
|
50
|
+
cached = "en";
|
|
51
|
+
return cached;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 3. macOS: AppleLocale
|
|
55
|
+
if (process.platform === "darwin") {
|
|
56
|
+
try {
|
|
57
|
+
const appleLocale = execSync("defaults read -g AppleLocale", {
|
|
58
|
+
encoding: "utf-8",
|
|
59
|
+
timeout: 500,
|
|
60
|
+
}).trim();
|
|
61
|
+
if (matchKo(appleLocale)) { cached = "ko"; return cached; }
|
|
62
|
+
} catch {
|
|
63
|
+
// Not macOS or defaults unavailable
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 4. Intl API fallback
|
|
68
|
+
try {
|
|
69
|
+
const intlLocale = Intl.DateTimeFormat().resolvedOptions().locale;
|
|
70
|
+
if (matchKo(intlLocale)) { cached = "ko"; return cached; }
|
|
71
|
+
} catch {
|
|
72
|
+
// Fallback to default
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 5. Default
|
|
76
|
+
cached = "ko";
|
|
77
|
+
return cached;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Get list of supported languages. */
|
|
81
|
+
export function getSupportedLanguages(): SupportedLang[] {
|
|
82
|
+
return [...SUPPORTED];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Override locale (for testing or after `afd lang` write). */
|
|
86
|
+
export function setLanguageOverride(lang: SupportedLang | null): void {
|
|
87
|
+
cached = lang;
|
|
88
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { existsSync, statSync, renameSync, unlinkSync } from "fs";
|
|
2
|
+
|
|
3
|
+
const MAX_LOG_SIZE = 5 * 1024 * 1024; // 5 MB
|
|
4
|
+
const MAX_ROTATED_FILES = 3; // daemon.log.1, .2, .3
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Rotate log file if it exceeds MAX_LOG_SIZE.
|
|
8
|
+
* Keeps up to MAX_ROTATED_FILES old logs.
|
|
9
|
+
*
|
|
10
|
+
* daemon.log → daemon.log.1 → daemon.log.2 → daemon.log.3 (deleted)
|
|
11
|
+
*/
|
|
12
|
+
export function rotateLogIfNeeded(logPath: string): void {
|
|
13
|
+
if (!existsSync(logPath)) return;
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const { size } = statSync(logPath);
|
|
17
|
+
if (size < MAX_LOG_SIZE) return;
|
|
18
|
+
|
|
19
|
+
// Shift existing rotated files: .3→delete, .2→.3, .1→.2
|
|
20
|
+
for (let i = MAX_ROTATED_FILES; i >= 1; i--) {
|
|
21
|
+
const src = i === 1 ? logPath : `${logPath}.${i - 1}`;
|
|
22
|
+
const dst = `${logPath}.${i}`;
|
|
23
|
+
if (!existsSync(src)) continue;
|
|
24
|
+
if (i === MAX_ROTATED_FILES && existsSync(dst)) {
|
|
25
|
+
unlinkSync(dst);
|
|
26
|
+
}
|
|
27
|
+
renameSync(src, dst);
|
|
28
|
+
}
|
|
29
|
+
// logPath has been renamed to logPath.1, fresh log will be created on open
|
|
30
|
+
} catch {
|
|
31
|
+
// Non-critical: if rotation fails, just keep appending
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logging utilities for the afd daemon.
|
|
3
|
+
* Extracted for testability and reuse.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Format current time as HH:MM:SS.mmm */
|
|
7
|
+
export function formatTimestamp(date: Date = new Date()): string {
|
|
8
|
+
const hh = String(date.getHours()).padStart(2, "0");
|
|
9
|
+
const mm = String(date.getMinutes()).padStart(2, "0");
|
|
10
|
+
const ss = String(date.getSeconds()).padStart(2, "0");
|
|
11
|
+
const ms = String(date.getMilliseconds()).padStart(3, "0");
|
|
12
|
+
return `${hh}:${mm}:${ss}.${ms}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Build a concise line-diff between old and new content.
|
|
17
|
+
* Returns at most `maxLines` diff entries (default 10).
|
|
18
|
+
*/
|
|
19
|
+
export function lineDiff(oldText: string, newText: string, maxLines = 10): string[] {
|
|
20
|
+
const oldLines = oldText.split("\n");
|
|
21
|
+
const newLines = newText.split("\n");
|
|
22
|
+
const diffs: string[] = [];
|
|
23
|
+
const maxLen = Math.max(oldLines.length, newLines.length);
|
|
24
|
+
for (let i = 0; i < maxLen; i++) {
|
|
25
|
+
if (oldLines[i] !== newLines[i]) {
|
|
26
|
+
const ln = i + 1;
|
|
27
|
+
if (i < oldLines.length && i < newLines.length) {
|
|
28
|
+
diffs.push(` L${ln}: "${oldLines[i].trimEnd()}" → "${newLines[i].trimEnd()}"`);
|
|
29
|
+
} else if (i < oldLines.length) {
|
|
30
|
+
diffs.push(` L${ln}: - "${oldLines[i].trimEnd()}"`);
|
|
31
|
+
} else {
|
|
32
|
+
diffs.push(` L${ln}: + "${newLines[i]!.trimEnd()}"`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (diffs.length >= maxLines) { diffs.push(" ... (truncated)"); break; }
|
|
36
|
+
}
|
|
37
|
+
return diffs;
|
|
38
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Size-bounded LRU Map that evicts by total byte size of values.
|
|
3
|
+
* Uses Map insertion order for LRU eviction (oldest first).
|
|
4
|
+
*/
|
|
5
|
+
export class LruStringMap {
|
|
6
|
+
private map = new Map<string, string>();
|
|
7
|
+
private currentBytes = 0;
|
|
8
|
+
|
|
9
|
+
constructor(private readonly maxBytes: number) {}
|
|
10
|
+
|
|
11
|
+
get(key: string): string | undefined {
|
|
12
|
+
const val = this.map.get(key);
|
|
13
|
+
if (val !== undefined) {
|
|
14
|
+
// Move to end (most recently used)
|
|
15
|
+
this.map.delete(key);
|
|
16
|
+
this.map.set(key, val);
|
|
17
|
+
}
|
|
18
|
+
return val;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Returns false if the value was too large to store (exceeds maxBytes). */
|
|
22
|
+
set(key: string, value: string): boolean {
|
|
23
|
+
const valueBytes = value.length * 2;
|
|
24
|
+
|
|
25
|
+
// Skip if single value exceeds budget (check BEFORE removing old entry)
|
|
26
|
+
if (valueBytes > this.maxBytes) return false;
|
|
27
|
+
|
|
28
|
+
// Remove old entry if exists
|
|
29
|
+
const old = this.map.get(key);
|
|
30
|
+
if (old !== undefined) {
|
|
31
|
+
this.currentBytes -= old.length * 2; // JS string ≈ 2 bytes per char
|
|
32
|
+
this.map.delete(key);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Evict oldest entries until we have room
|
|
36
|
+
while (this.currentBytes + valueBytes > this.maxBytes && this.map.size > 0) {
|
|
37
|
+
const first = this.map.keys().next();
|
|
38
|
+
if (first.done) break;
|
|
39
|
+
const evicted = this.map.get(first.value)!;
|
|
40
|
+
this.currentBytes -= evicted.length * 2;
|
|
41
|
+
this.map.delete(first.value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.map.set(key, value);
|
|
45
|
+
this.currentBytes += valueBytes;
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
delete(key: string): boolean {
|
|
50
|
+
const val = this.map.get(key);
|
|
51
|
+
if (val !== undefined) {
|
|
52
|
+
this.currentBytes -= val.length * 2;
|
|
53
|
+
this.map.delete(key);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get size(): number { return this.map.size; }
|
|
60
|
+
get bytes(): number { return this.currentBytes; }
|
|
61
|
+
}
|
package/src/core/notify.ts
CHANGED
|
@@ -1,66 +1,74 @@
|
|
|
1
|
-
import { spawn } from "child_process";
|
|
2
|
-
import { IS_WINDOWS, IS_MACOS } from "../platform";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Fire an OS-native toast notification.
|
|
6
|
-
* Runs asynchronously, never blocks, silently ignores all errors.
|
|
7
|
-
*
|
|
8
|
-
* - Windows 10+: PowerShell BalloonTip
|
|
9
|
-
* - macOS: osascript display notification
|
|
10
|
-
* - Linux: notify-send (libnotify)
|
|
11
|
-
*/
|
|
12
|
-
export function notifyAutoHeal(patternId: string): void {
|
|
13
|
-
const title = "\u{1F6E1}\uFE0F afd Auto-Healed";
|
|
14
|
-
const body = `Silently fixed: ${patternId}`;
|
|
15
|
-
|
|
16
|
-
try {
|
|
17
|
-
if (IS_WINDOWS) {
|
|
18
|
-
notifyWindows(title, body);
|
|
19
|
-
} else if (IS_MACOS) {
|
|
20
|
-
notifyMacOS(title, body);
|
|
21
|
-
} else {
|
|
22
|
-
notifyLinux(title, body);
|
|
23
|
-
}
|
|
24
|
-
} catch {
|
|
25
|
-
// Crash-only: silently ignore notification failures
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { IS_WINDOWS, IS_MACOS } from "../platform";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Fire an OS-native toast notification.
|
|
6
|
+
* Runs asynchronously, never blocks, silently ignores all errors.
|
|
7
|
+
*
|
|
8
|
+
* - Windows 10+: PowerShell BalloonTip
|
|
9
|
+
* - macOS: osascript display notification
|
|
10
|
+
* - Linux: notify-send (libnotify)
|
|
11
|
+
*/
|
|
12
|
+
export function notifyAutoHeal(patternId: string): void {
|
|
13
|
+
const title = "\u{1F6E1}\uFE0F afd Auto-Healed";
|
|
14
|
+
const body = `Silently fixed: ${patternId}`;
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
if (IS_WINDOWS) {
|
|
18
|
+
notifyWindows(title, body);
|
|
19
|
+
} else if (IS_MACOS) {
|
|
20
|
+
notifyMacOS(title, body);
|
|
21
|
+
} else {
|
|
22
|
+
notifyLinux(title, body);
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
// Crash-only: silently ignore notification failures
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Escape single quotes for PowerShell string literals */
|
|
30
|
+
function escapePS(s: string): string {
|
|
31
|
+
return s.replace(/'/g, "''");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Escape double quotes and backslashes for AppleScript string literals */
|
|
35
|
+
function escapeAS(s: string): string {
|
|
36
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function safeSpawn(cmd: string, args: string[], opts: Record<string, unknown> = {}): void {
|
|
40
|
+
try {
|
|
41
|
+
const child = spawn(cmd, args, { detached: true, stdio: "ignore", ...opts });
|
|
42
|
+
child.on("error", () => {}); // Swallow async ENOENT when binary is missing
|
|
43
|
+
child.unref();
|
|
44
|
+
} catch {
|
|
45
|
+
// Binary not found or spawn failed — silently ignore
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function notifyWindows(title: string, body: string): void {
|
|
50
|
+
const ps = `
|
|
51
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
52
|
+
$n = New-Object System.Windows.Forms.NotifyIcon
|
|
53
|
+
$n.Icon = [System.Drawing.SystemIcons]::Shield
|
|
54
|
+
$n.BalloonTipTitle = '${escapePS(title)}'
|
|
55
|
+
$n.BalloonTipText = '${escapePS(body)}'
|
|
56
|
+
$n.BalloonTipIcon = 'Info'
|
|
57
|
+
$n.Visible = $true
|
|
58
|
+
$n.ShowBalloonTip(3000)
|
|
59
|
+
Start-Sleep -Milliseconds 3500
|
|
60
|
+
$n.Dispose()
|
|
61
|
+
`.replace(/\n\s*/g, " ");
|
|
62
|
+
|
|
63
|
+
safeSpawn("powershell", ["-NoProfile", "-NonInteractive", "-Command", ps], { windowsHide: true });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function notifyMacOS(title: string, body: string): void {
|
|
67
|
+
const script = `display notification "${escapeAS(body)}" with title "${escapeAS(title)}"`;
|
|
68
|
+
|
|
69
|
+
safeSpawn("osascript", ["-e", script]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function notifyLinux(title: string, body: string): void {
|
|
73
|
+
safeSpawn("notify-send", [title, body, "--icon=dialog-information"]);
|
|
74
|
+
}
|