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/commands/fix.ts
CHANGED
|
@@ -1,138 +1,158 @@
|
|
|
1
|
-
import { writeFileSync, mkdirSync, existsSync } from "fs";
|
|
2
|
-
import { dirname } from "path";
|
|
3
|
-
import { daemonRequest } from "../daemon/client";
|
|
4
|
-
import type { Symptom, PatchOp, DiagnosisResult } from "../core/immune";
|
|
5
|
-
|
|
6
|
-
const SEVERITY_ICON: Record<string, string> = {
|
|
7
|
-
critical: "[!]",
|
|
8
|
-
warning: "[~]",
|
|
9
|
-
info: "[i]",
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
function applyPatch(patch: PatchOp): boolean {
|
|
13
|
-
// Map JSON-Patch path to filesystem path (strip leading /)
|
|
14
|
-
const filePath = patch.path.replace(/^\//, "");
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
console.log();
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
1
|
+
import { writeFileSync, mkdirSync, existsSync } from "fs";
|
|
2
|
+
import { dirname } from "path";
|
|
3
|
+
import { daemonRequest } from "../daemon/client";
|
|
4
|
+
import type { Symptom, PatchOp, DiagnosisResult } from "../core/immune";
|
|
5
|
+
|
|
6
|
+
const SEVERITY_ICON: Record<string, string> = {
|
|
7
|
+
critical: "[!]",
|
|
8
|
+
warning: "[~]",
|
|
9
|
+
info: "[i]",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function applyPatch(patch: PatchOp): boolean {
|
|
13
|
+
// Map JSON-Patch path to filesystem path (strip leading /)
|
|
14
|
+
const filePath = patch.path.replace(/^\//, "");
|
|
15
|
+
|
|
16
|
+
// Guard: reject path traversal attempts
|
|
17
|
+
if (filePath.includes("..") || filePath.startsWith("/") || /^[A-Za-z]:/.test(filePath)) return false;
|
|
18
|
+
|
|
19
|
+
if (patch.op === "add") {
|
|
20
|
+
if (existsSync(filePath)) return false; // don't overwrite
|
|
21
|
+
const dir = dirname(filePath);
|
|
22
|
+
if (dir !== ".") mkdirSync(dir, { recursive: true });
|
|
23
|
+
writeFileSync(filePath, patch.value ?? "", "utf-8");
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (patch.op === "replace") {
|
|
28
|
+
const dir = dirname(filePath);
|
|
29
|
+
if (dir !== ".") mkdirSync(dir, { recursive: true });
|
|
30
|
+
writeFileSync(filePath, patch.value ?? "", "utf-8");
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// remove, move, copy, test — not needed yet
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function learnAntibody(symptom: Symptom): Promise<void> {
|
|
39
|
+
await fetch(
|
|
40
|
+
`http://127.0.0.1:${(await getDaemonPort())}/antibodies/learn`,
|
|
41
|
+
{
|
|
42
|
+
method: "POST",
|
|
43
|
+
headers: { "Content-Type": "application/json" },
|
|
44
|
+
body: JSON.stringify({
|
|
45
|
+
id: symptom.id,
|
|
46
|
+
patternType: symptom.patternType,
|
|
47
|
+
fileTarget: symptom.fileTarget,
|
|
48
|
+
patches: symptom.patches,
|
|
49
|
+
}),
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function getDaemonPort(): Promise<number> {
|
|
55
|
+
const { readFileSync } = await import("fs");
|
|
56
|
+
const { resolveWorkspacePaths } = await import("../constants");
|
|
57
|
+
const paths = resolveWorkspacePaths();
|
|
58
|
+
return parseInt(readFileSync(paths.portFile, "utf-8").trim(), 10);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function fixCommand() {
|
|
62
|
+
let diagnosis: DiagnosisResult;
|
|
63
|
+
try {
|
|
64
|
+
diagnosis = await daemonRequest<DiagnosisResult>("/diagnose");
|
|
65
|
+
} catch (err: unknown) {
|
|
66
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
67
|
+
console.error(`[afd fix] ${msg}`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (diagnosis.symptoms.length === 0) {
|
|
72
|
+
console.log("[afd fix] No symptoms detected. System is healthy.");
|
|
73
|
+
if (diagnosis.healthy.length > 0) {
|
|
74
|
+
console.log(`[afd fix] Passed checks: ${diagnosis.healthy.join(", ")}`);
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Display symptoms
|
|
80
|
+
console.log(`\n[afd fix] Found ${diagnosis.symptoms.length} symptom(s):\n`);
|
|
81
|
+
|
|
82
|
+
for (const s of diagnosis.symptoms) {
|
|
83
|
+
const icon = SEVERITY_ICON[s.severity] ?? "[?]";
|
|
84
|
+
console.log(` ${icon} ${s.id}: ${s.title} (${s.severity})`);
|
|
85
|
+
console.log(` ${s.description}`);
|
|
86
|
+
if (s.patches.length > 0) {
|
|
87
|
+
console.log(` Patch: ${s.patches.map(p => `${p.op} ${p.path}`).join(", ")}`);
|
|
88
|
+
}
|
|
89
|
+
console.log();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Extract phase: inject hologram context for AI consumers
|
|
93
|
+
const symptomsWithHologram = diagnosis.symptoms as (Symptom & { hologram?: string })[];
|
|
94
|
+
const holograms = symptomsWithHologram.filter(s => s.hologram);
|
|
95
|
+
if (holograms.length > 0) {
|
|
96
|
+
console.log("[afd fix] Hologram Context (Extract phase — token-optimized file structures):\n");
|
|
97
|
+
for (const s of holograms) {
|
|
98
|
+
console.log(` --- ${s.fileTarget} ---`);
|
|
99
|
+
console.log(` Here is the structural hologram of the file to help you understand`);
|
|
100
|
+
console.log(` its interfaces without consuming too many tokens:\n`);
|
|
101
|
+
for (const line of s.hologram!.split("\n")) {
|
|
102
|
+
console.log(` ${line}`);
|
|
103
|
+
}
|
|
104
|
+
console.log(`\n Now, generate the JSON-Patch based on the structure above.\n`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Back-stage: dump full JSON-Patch for AI consumers
|
|
109
|
+
const allPatches = diagnosis.symptoms.flatMap(s =>
|
|
110
|
+
s.patches.map(p => ({ symptomId: s.id, ...p }))
|
|
111
|
+
);
|
|
112
|
+
console.log("[afd fix] JSON-Patch (back-stage):");
|
|
113
|
+
console.log(JSON.stringify(allPatches, null, 2));
|
|
114
|
+
console.log();
|
|
115
|
+
|
|
116
|
+
// Prompt user
|
|
117
|
+
process.stdout.write("Apply these fixes? [Y/n] ");
|
|
118
|
+
const answer = await readLine();
|
|
119
|
+
|
|
120
|
+
if (answer.toLowerCase() === "n") {
|
|
121
|
+
console.log("[afd fix] Aborted.");
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Apply patches and learn antibodies
|
|
126
|
+
let applied = 0;
|
|
127
|
+
for (const symptom of diagnosis.symptoms) {
|
|
128
|
+
if (symptom.patches.length === 0) continue;
|
|
129
|
+
let success = true;
|
|
130
|
+
for (const patch of symptom.patches) {
|
|
131
|
+
if (!applyPatch(patch)) {
|
|
132
|
+
console.log(` [skip] ${patch.op} ${patch.path} (already exists or unsupported)`);
|
|
133
|
+
success = false;
|
|
134
|
+
} else {
|
|
135
|
+
console.log(` [done] ${patch.op} ${patch.path}`);
|
|
136
|
+
applied++;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (success) {
|
|
140
|
+
await learnAntibody(symptom);
|
|
141
|
+
console.log(` [immune] Learned antibody: ${symptom.id}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log(`\n[afd fix] Applied ${applied} patch(es). Immune system updated.`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function readLine(): Promise<string> {
|
|
149
|
+
return new Promise((resolve) => {
|
|
150
|
+
const buf: Buffer[] = [];
|
|
151
|
+
process.stdin.setEncoding("utf-8");
|
|
152
|
+
process.stdin.resume();
|
|
153
|
+
process.stdin.once("data", (chunk: string) => {
|
|
154
|
+
process.stdin.pause();
|
|
155
|
+
resolve(chunk.toString().trim());
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { join } from "path";
|
|
2
|
+
import { resolveWorkspacePaths } from "../constants";
|
|
3
|
+
import {
|
|
4
|
+
readHooksFile,
|
|
5
|
+
writeHooksFile,
|
|
6
|
+
mergeHooks,
|
|
7
|
+
getHookSummary,
|
|
8
|
+
getAfdDesiredHooks,
|
|
9
|
+
type HookOwner,
|
|
10
|
+
type ManagedHook,
|
|
11
|
+
} from "../core/hook-manager";
|
|
12
|
+
|
|
13
|
+
const C = {
|
|
14
|
+
reset: "\x1b[0m",
|
|
15
|
+
bold: "\x1b[1m",
|
|
16
|
+
dim: "\x1b[2m",
|
|
17
|
+
red: "\x1b[31m",
|
|
18
|
+
green: "\x1b[32m",
|
|
19
|
+
yellow: "\x1b[33m",
|
|
20
|
+
cyan: "\x1b[36m",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const OWNER_COLOR: Record<HookOwner, string> = {
|
|
24
|
+
afd: C.cyan,
|
|
25
|
+
omc: C.yellow,
|
|
26
|
+
user: C.dim,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function formatHookLine(hook: ManagedHook): string {
|
|
30
|
+
const color = OWNER_COLOR[hook.owner];
|
|
31
|
+
const ownerTag = `[${hook.owner}]`.padEnd(6);
|
|
32
|
+
const id = hook.id.padEnd(24);
|
|
33
|
+
const matcher = hook.matcher || "*";
|
|
34
|
+
return ` ${color}${ownerTag}${C.reset} ${C.bold}${id}${C.reset} ${C.dim}${matcher}${C.reset}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function hooksCommand(subcommand?: string): void {
|
|
38
|
+
const hooksPath = join(resolveWorkspacePaths().root, ".claude", "hooks.json");
|
|
39
|
+
|
|
40
|
+
if (!subcommand || subcommand === "status") {
|
|
41
|
+
const summary = getHookSummary(hooksPath);
|
|
42
|
+
const total = summary.total;
|
|
43
|
+
console.log("");
|
|
44
|
+
console.log(`${C.bold}afd hooks — Hook Manager${C.reset}`);
|
|
45
|
+
console.log("");
|
|
46
|
+
console.log(` PreToolUse (${total} hook${total !== 1 ? "s" : ""})`);
|
|
47
|
+
console.log(" " + "─".repeat(48));
|
|
48
|
+
|
|
49
|
+
const allInOrder: ManagedHook[] = [
|
|
50
|
+
...summary.zones.afd,
|
|
51
|
+
...summary.zones.omc,
|
|
52
|
+
...summary.zones.user,
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
if (allInOrder.length === 0) {
|
|
56
|
+
console.log(` ${C.dim}No hooks registered${C.reset}`);
|
|
57
|
+
} else {
|
|
58
|
+
for (const hook of allInOrder) {
|
|
59
|
+
console.log(formatHookLine(hook));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log("");
|
|
64
|
+
|
|
65
|
+
const orderStatus = summary.orderingOk
|
|
66
|
+
? `${C.green}OK${C.reset} ${C.dim}(afd → omc → user)${C.reset}`
|
|
67
|
+
: `${C.yellow}DISORDERED${C.reset} — run ${C.bold}afd hooks sync${C.reset} to fix`;
|
|
68
|
+
console.log(` Ordering: ${orderStatus}`);
|
|
69
|
+
|
|
70
|
+
if (summary.conflicts.length === 0) {
|
|
71
|
+
console.log(` Conflicts: ${C.green}none${C.reset}`);
|
|
72
|
+
} else {
|
|
73
|
+
console.log(` Conflicts: ${C.yellow}${summary.conflicts.length} warning(s)${C.reset}`);
|
|
74
|
+
for (const c of summary.conflicts) {
|
|
75
|
+
const prefix = c.type === "duplicate-id" ? "✗" : "⚠";
|
|
76
|
+
console.log(` ${C.yellow}${prefix}${C.reset} ${c.type}: ${C.bold}${c.hookA.id}${C.reset} ↔ ${C.bold}${c.hookB.id}${C.reset}`);
|
|
77
|
+
console.log(` ${C.dim}${c.resolution}${C.reset}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log("");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (subcommand === "sync") {
|
|
86
|
+
console.log("");
|
|
87
|
+
console.log(`${C.bold}afd hooks sync${C.reset}`);
|
|
88
|
+
console.log("");
|
|
89
|
+
|
|
90
|
+
const config = readHooksFile(hooksPath);
|
|
91
|
+
if (!config.hooks || typeof config.hooks !== "object" || Array.isArray(config.hooks)) {
|
|
92
|
+
config.hooks = {};
|
|
93
|
+
}
|
|
94
|
+
if (!config.hooks.PreToolUse) config.hooks.PreToolUse = [];
|
|
95
|
+
|
|
96
|
+
const result = mergeHooks(config.hooks.PreToolUse, getAfdDesiredHooks());
|
|
97
|
+
config.hooks.PreToolUse = result.merged;
|
|
98
|
+
writeHooksFile(hooksPath, config);
|
|
99
|
+
|
|
100
|
+
let anyChange = false;
|
|
101
|
+
if (result.changes.added.length > 0) {
|
|
102
|
+
console.log(` Added: ${result.changes.added.join(", ")}`);
|
|
103
|
+
anyChange = true;
|
|
104
|
+
}
|
|
105
|
+
if (result.changes.removed.length > 0) {
|
|
106
|
+
console.log(` Removed: ${result.changes.removed.join(", ")}`);
|
|
107
|
+
anyChange = true;
|
|
108
|
+
}
|
|
109
|
+
if (result.changes.reordered.length > 0) {
|
|
110
|
+
console.log(` Reordered: ${result.changes.reordered.join(", ")}`);
|
|
111
|
+
anyChange = true;
|
|
112
|
+
}
|
|
113
|
+
if (!anyChange) {
|
|
114
|
+
console.log(` ${C.green}Already in sync — no changes needed${C.reset}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (result.conflicts.length > 0) {
|
|
118
|
+
console.log(` Conflicts: ${C.yellow}${result.conflicts.length} warning(s)${C.reset}`);
|
|
119
|
+
for (const c of result.conflicts) {
|
|
120
|
+
const prefix = c.type === "duplicate-id" ? "✗" : "⚠";
|
|
121
|
+
console.log(` ${C.yellow}${prefix}${C.reset} ${c.type}: ${C.bold}${c.hookA.id}${C.reset} ↔ ${C.bold}${c.hookB.id}${C.reset}`);
|
|
122
|
+
console.log(` ${C.dim}${c.resolution}${C.reset}`);
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
console.log(` Conflicts: ${C.green}none${C.reset}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
console.log(` ${C.dim}hooks.json updated.${C.reset}`);
|
|
129
|
+
console.log("");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.error(`Unknown subcommand: ${subcommand}`);
|
|
134
|
+
console.error("Usage: afd hooks [status|sync]");
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
package/src/commands/lang.ts
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
import { getSystemLanguage, getSupportedLanguages, setLanguageOverride } from "../core/locale";
|
|
2
|
-
import type { SupportedLang } from "../core/locale";
|
|
3
|
-
import { writeConfig, getConfigPath } from "../core/config";
|
|
4
|
-
import { getMessages, t } from "../core/i18n/messages";
|
|
5
|
-
|
|
6
|
-
export function langCommand(targetLang?: string, options?: { list?: boolean }) {
|
|
7
|
-
const currentLang = getSystemLanguage();
|
|
8
|
-
const msg = getMessages(currentLang);
|
|
9
|
-
const supported = getSupportedLanguages();
|
|
10
|
-
|
|
11
|
-
// afd lang --list
|
|
12
|
-
if (options?.list) {
|
|
13
|
-
console.log(msg.LANG_LIST_TITLE);
|
|
14
|
-
for (const lang of supported) {
|
|
15
|
-
const marker = lang === currentLang ? " ← current" : "";
|
|
16
|
-
console.log(` ${lang}${marker}`);
|
|
17
|
-
}
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// afd lang (no argument) — show current
|
|
22
|
-
if (!targetLang) {
|
|
23
|
-
console.log(t(msg.LANG_CURRENT, { lang: currentLang }));
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// afd lang <en|ko> — change language
|
|
28
|
-
if (!supported.includes(targetLang as SupportedLang)) {
|
|
29
|
-
console.error(t(msg.LANG_INVALID, { lang: targetLang, supported: supported.join(", ") }));
|
|
30
|
-
process.exit(1);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const newLang = targetLang as SupportedLang;
|
|
34
|
-
writeConfig({ lang: newLang });
|
|
35
|
-
setLanguageOverride(newLang);
|
|
36
|
-
|
|
37
|
-
// Print feedback in the NEW language
|
|
38
|
-
const newMsg = getMessages(newLang);
|
|
39
|
-
console.log(t(newMsg.LANG_CHANGED, { lang: newLang }));
|
|
40
|
-
console.log(t(newMsg.LANG_SAVED, { path: getConfigPath() }));
|
|
41
|
-
}
|
|
1
|
+
import { getSystemLanguage, getSupportedLanguages, setLanguageOverride } from "../core/locale";
|
|
2
|
+
import type { SupportedLang } from "../core/locale";
|
|
3
|
+
import { writeConfig, getConfigPath } from "../core/config";
|
|
4
|
+
import { getMessages, t } from "../core/i18n/messages";
|
|
5
|
+
|
|
6
|
+
export function langCommand(targetLang?: string, options?: { list?: boolean }) {
|
|
7
|
+
const currentLang = getSystemLanguage();
|
|
8
|
+
const msg = getMessages(currentLang);
|
|
9
|
+
const supported = getSupportedLanguages();
|
|
10
|
+
|
|
11
|
+
// afd lang --list
|
|
12
|
+
if (options?.list) {
|
|
13
|
+
console.log(msg.LANG_LIST_TITLE);
|
|
14
|
+
for (const lang of supported) {
|
|
15
|
+
const marker = lang === currentLang ? " ← current" : "";
|
|
16
|
+
console.log(` ${lang}${marker}`);
|
|
17
|
+
}
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// afd lang (no argument) — show current
|
|
22
|
+
if (!targetLang) {
|
|
23
|
+
console.log(t(msg.LANG_CURRENT, { lang: currentLang }));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// afd lang <en|ko> — change language
|
|
28
|
+
if (!supported.includes(targetLang as SupportedLang)) {
|
|
29
|
+
console.error(t(msg.LANG_INVALID, { lang: targetLang, supported: supported.join(", ") }));
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const newLang = targetLang as SupportedLang;
|
|
34
|
+
writeConfig({ lang: newLang });
|
|
35
|
+
setLanguageOverride(newLang);
|
|
36
|
+
|
|
37
|
+
// Print feedback in the NEW language
|
|
38
|
+
const newMsg = getMessages(newLang);
|
|
39
|
+
console.log(t(newMsg.LANG_CHANGED, { lang: newLang }));
|
|
40
|
+
console.log(t(newMsg.LANG_SAVED, { path: getConfigPath() }));
|
|
41
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* afd mcp — MCP server management
|
|
3
|
+
*
|
|
4
|
+
* Subcommands:
|
|
5
|
+
* install — Register afd as an MCP server in project and global Claude config
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
9
|
+
import { join, resolve, dirname } from "path";
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
import { getSystemLanguage } from "../core/locale";
|
|
12
|
+
|
|
13
|
+
const msgs = {
|
|
14
|
+
en: {
|
|
15
|
+
usage: "Usage: afd mcp install",
|
|
16
|
+
unknownSub: (s: string) => `Unknown subcommand: ${s}. Use: afd mcp install`,
|
|
17
|
+
installing: "Registering afd MCP server...",
|
|
18
|
+
projectDone: (p: string) => ` [project] Registered in ${p}`,
|
|
19
|
+
projectSkip: (p: string) => ` [project] Already registered in ${p}`,
|
|
20
|
+
globalDone: (p: string) => ` [global] Registered in ${p}`,
|
|
21
|
+
globalSkip: (p: string) => ` [global] Already registered in ${p}`,
|
|
22
|
+
success: "afd MCP server registration complete. Restart Claude Code to activate.",
|
|
23
|
+
hintRestart: " Hint: Run 'claude --reload-plugins' or restart the IDE to pick up the new MCP server.",
|
|
24
|
+
},
|
|
25
|
+
ko: {
|
|
26
|
+
usage: "사용법: afd mcp install",
|
|
27
|
+
unknownSub: (s: string) => `알 수 없는 하위 명령: ${s}. 사용: afd mcp install`,
|
|
28
|
+
installing: "afd MCP 서버 등록 중...",
|
|
29
|
+
projectDone: (p: string) => ` [project] ${p}에 등록 완료`,
|
|
30
|
+
projectSkip: (p: string) => ` [project] ${p}에 이미 등록됨`,
|
|
31
|
+
globalDone: (p: string) => ` [global] ${p}에 등록 완료`,
|
|
32
|
+
globalSkip: (p: string) => ` [global] ${p}에 이미 등록됨`,
|
|
33
|
+
success: "afd MCP 서버 등록 완료. Claude Code를 재시작하면 활성화됩니다.",
|
|
34
|
+
hintRestart: " 힌트: 'claude --reload-plugins' 또는 IDE를 재시작하여 MCP 서버를 인식시키세요.",
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
interface McpServerEntry {
|
|
39
|
+
command: string;
|
|
40
|
+
args: string[];
|
|
41
|
+
[key: string]: unknown;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface McpConfig {
|
|
45
|
+
mcpServers?: Record<string, McpServerEntry>;
|
|
46
|
+
[key: string]: unknown;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The canonical MCP server definition for afd */
|
|
50
|
+
function afdMcpEntry(): McpServerEntry {
|
|
51
|
+
return {
|
|
52
|
+
command: "bun",
|
|
53
|
+
args: ["run", "src/daemon/server.ts", "--mcp"],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Register afd in a JSON config file's mcpServers section */
|
|
58
|
+
function registerInFile(filePath: string, entry: McpServerEntry): "done" | "skip" | "error" {
|
|
59
|
+
let config: McpConfig = {};
|
|
60
|
+
if (existsSync(filePath)) {
|
|
61
|
+
try {
|
|
62
|
+
config = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
63
|
+
} catch {
|
|
64
|
+
// Preserve existing file by reading raw content for backup
|
|
65
|
+
config = {};
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
try {
|
|
69
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
70
|
+
} catch { return "error"; }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const servers = (config.mcpServers ?? {}) as Record<string, McpServerEntry>;
|
|
74
|
+
const existing = servers.afd;
|
|
75
|
+
|
|
76
|
+
if (existing?.command === entry.command &&
|
|
77
|
+
JSON.stringify(existing.args) === JSON.stringify(entry.args)) {
|
|
78
|
+
return "skip";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
servers.afd = entry;
|
|
82
|
+
config.mcpServers = servers;
|
|
83
|
+
try {
|
|
84
|
+
writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
85
|
+
} catch {
|
|
86
|
+
return "error";
|
|
87
|
+
}
|
|
88
|
+
return "done";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Resolve the global Claude Code settings path (cross-platform) */
|
|
92
|
+
function globalClaudeConfigPath(): string {
|
|
93
|
+
return join(homedir(), ".claude.json");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function mcpCommand(subcommand?: string) {
|
|
97
|
+
const lang = getSystemLanguage();
|
|
98
|
+
const m = msgs[lang];
|
|
99
|
+
|
|
100
|
+
if (!subcommand) {
|
|
101
|
+
console.log(m.usage);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (subcommand !== "install") {
|
|
106
|
+
console.error(m.unknownSub(subcommand));
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.log(m.installing);
|
|
111
|
+
|
|
112
|
+
const entry = afdMcpEntry();
|
|
113
|
+
|
|
114
|
+
// 1. Project-level: .mcp.json
|
|
115
|
+
const projectPath = resolve(".mcp.json");
|
|
116
|
+
const projectResult = registerInFile(projectPath, entry);
|
|
117
|
+
if (projectResult === "error") console.error(` [project] Failed to write ${projectPath}`);
|
|
118
|
+
else console.log(projectResult === "done" ? m.projectDone(projectPath) : m.projectSkip(projectPath));
|
|
119
|
+
|
|
120
|
+
// 2. Global-level: ~/.claude.json — mcpServers section
|
|
121
|
+
const globalPath = globalClaudeConfigPath();
|
|
122
|
+
const globalResult = registerInFile(globalPath, entry);
|
|
123
|
+
if (globalResult === "error") console.error(` [global] Failed to write ${globalPath}`);
|
|
124
|
+
else console.log(globalResult === "done" ? m.globalDone(globalPath) : m.globalSkip(globalPath));
|
|
125
|
+
|
|
126
|
+
console.log("");
|
|
127
|
+
console.log(m.success);
|
|
128
|
+
console.log(m.hintRestart);
|
|
129
|
+
}
|