@agilsee/mcp-orchestrator 0.5.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.
Files changed (43) hide show
  1. package/bin/cli.js +490 -0
  2. package/dist/index.js +454 -0
  3. package/dist/memory/memory-manager.js +234 -0
  4. package/dist/server/web-server.js +574 -0
  5. package/dist/tools/aggregate-patterns.js +101 -0
  6. package/dist/tools/analyze-history.js +213 -0
  7. package/dist/tools/auto-dispatch.js +199 -0
  8. package/dist/tools/check-energy.js +49 -0
  9. package/dist/tools/cross-search.js +171 -0
  10. package/dist/tools/get-focus.js +7 -0
  11. package/dist/tools/get-identity.js +7 -0
  12. package/dist/tools/get-project-status.js +35 -0
  13. package/dist/tools/list-projects.js +21 -0
  14. package/dist/tools/list-recent-tasks.js +59 -0
  15. package/dist/tools/log-insight.js +43 -0
  16. package/dist/tools/qcc-create.js +82 -0
  17. package/dist/tools/qcc-status.js +164 -0
  18. package/dist/tools/qcc-update.js +188 -0
  19. package/dist/tools/smart-bootstrap.js +255 -0
  20. package/dist/tools/summarize-session.js +161 -0
  21. package/dist/tools/switch-focus.js +40 -0
  22. package/dist/tools/workflow-router.js +438 -0
  23. package/package.json +44 -0
  24. package/templates/index.ts.template +42 -0
  25. package/templates/shared/get-claude-md.ts +12 -0
  26. package/templates/shared/get-current-state.ts +21 -0
  27. package/templates/shared/get-mistakes.ts +18 -0
  28. package/templates/shared/log-task.ts +27 -0
  29. package/templates/shared/predict-impact.ts +67 -0
  30. package/templates/shared/record-mistake.ts +40 -0
  31. package/templates/shared/update-state.ts +83 -0
  32. package/templates/stacks/express/config.json +9 -0
  33. package/templates/stacks/express/list-routes.ts +56 -0
  34. package/templates/stacks/express/symbol-index.ts +70 -0
  35. package/templates/stacks/laravel/config.json +9 -0
  36. package/templates/stacks/laravel/list-routes.ts +19 -0
  37. package/templates/stacks/laravel/symbol-index.ts +64 -0
  38. package/templates/stacks/nextjs/config.json +9 -0
  39. package/templates/stacks/nextjs/list-routes.ts +67 -0
  40. package/templates/stacks/nextjs/symbol-index.ts +78 -0
  41. package/templates/stacks/react/config.json +10 -0
  42. package/templates/stacks/react/list-routes.ts +44 -0
  43. package/templates/stacks/react/symbol-index.ts +81 -0
package/bin/cli.js ADDED
@@ -0,0 +1,490 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { writeFileSync, readFileSync, existsSync, mkdirSync, readdirSync, copyFileSync, cpSync } from "fs";
4
+ import { join, resolve, dirname, basename } from "path";
5
+ import { fileURLToPath } from "url";
6
+ import { createInterface } from "readline";
7
+ import { execSync } from "child_process";
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = dirname(__filename);
11
+
12
+ const CLAUDE_HOME = join(
13
+ process.env.USERPROFILE ?? process.env.HOME ?? "~",
14
+ ".claude"
15
+ );
16
+
17
+ const TEMPLATES_DIR = resolve(__dirname, "..", "templates");
18
+
19
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
20
+ const ask = (q) => new Promise((r) => rl.question(q, r));
21
+
22
+ const command = process.argv[2];
23
+
24
+ // ─── DETECT STACK ──────────────────────────────────────────────
25
+
26
+ function detectStack(projectPath) {
27
+ // Priority order: most specific first
28
+ // 1. Next.js
29
+ for (const f of ["next.config.js", "next.config.mjs", "next.config.ts"]) {
30
+ if (existsSync(join(projectPath, f))) return "nextjs";
31
+ }
32
+
33
+ // 2. Laravel
34
+ if (existsSync(join(projectPath, "artisan")) && existsSync(join(projectPath, "composer.json"))) {
35
+ return "laravel";
36
+ }
37
+
38
+ // 3. Check package.json deps
39
+ const pkgPath = join(projectPath, "package.json");
40
+ if (existsSync(pkgPath)) {
41
+ try {
42
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
43
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
44
+
45
+ // Express
46
+ if (allDeps["express"]) return "express";
47
+
48
+ // React (but not Next.js — already checked above)
49
+ if (allDeps["react"]) return "react";
50
+ } catch {}
51
+ }
52
+
53
+ return null;
54
+ }
55
+
56
+ // ─── GENERATE ──────────────────────────────────────────────────
57
+
58
+ async function generate() {
59
+ const cwd = process.cwd();
60
+ const projectName = basename(cwd).toLowerCase().replace(/[^a-z0-9-]/g, "-");
61
+
62
+ console.log("\n🔧 MCP Generator\n");
63
+ console.log(`📂 Project: ${cwd}`);
64
+
65
+ // 1. Detect stack
66
+ const detected = detectStack(cwd);
67
+ if (!detected) {
68
+ console.log("❌ Stack tidak terdeteksi. Pastikan ada:");
69
+ console.log(" - Laravel: composer.json + artisan");
70
+ console.log(" - Next.js: next.config.js/mjs/ts");
71
+ console.log(" - Express: package.json dengan dependency 'express'");
72
+ console.log(" - React: package.json dengan dependency 'react'");
73
+ rl.close();
74
+ return;
75
+ }
76
+ console.log(`🔍 Stack detected: ${detected}`);
77
+
78
+ // 2. Ask project slug
79
+ const defaultSlug = projectName;
80
+ const slug = (await ask(`Project slug [${defaultSlug}]: `)).trim() || defaultSlug;
81
+
82
+ // 3. Setup output directory
83
+ const outputDir = join(CLAUDE_HOME, "mcp", slug);
84
+ if (existsSync(outputDir)) {
85
+ const overwrite = await ask(`⚠️ ${outputDir} sudah ada. Overwrite? (y/n): `);
86
+ if (overwrite.toLowerCase() !== "y") {
87
+ console.log("❌ Dibatalkan.");
88
+ rl.close();
89
+ return;
90
+ }
91
+ }
92
+
93
+ console.log(`\n📦 Generating MCP server for '${slug}' (${detected})...\n`);
94
+
95
+ // 4. Create directory structure
96
+ mkdirSync(join(outputDir, "src", "tools"), { recursive: true });
97
+ mkdirSync(join(outputDir, "src", "indexer"), { recursive: true });
98
+
99
+ // 5. Copy shared tools
100
+ const sharedDir = join(TEMPLATES_DIR, "shared");
101
+ const sharedFiles = ["get-claude-md.ts", "get-current-state.ts", "get-mistakes.ts", "log-task.ts", "record-mistake.ts", "update-state.ts", "predict-impact.ts"];
102
+ for (const f of sharedFiles) {
103
+ const src = join(sharedDir, f);
104
+ if (existsSync(src)) {
105
+ copyFileSync(src, join(outputDir, "src", "tools", f));
106
+ console.log(` ✅ shared/${f}`);
107
+ }
108
+ }
109
+
110
+ // 6. Copy stack-specific files
111
+ const stackDir = join(TEMPLATES_DIR, "stacks", detected);
112
+ const stackConfig = JSON.parse(readFileSync(join(stackDir, "config.json"), "utf-8"));
113
+
114
+ if (existsSync(join(stackDir, "symbol-index.ts"))) {
115
+ copyFileSync(join(stackDir, "symbol-index.ts"), join(outputDir, "src", "indexer", "symbol-index.ts"));
116
+ console.log(` ✅ ${detected}/symbol-index.ts`);
117
+ }
118
+ if (existsSync(join(stackDir, "list-routes.ts"))) {
119
+ copyFileSync(join(stackDir, "list-routes.ts"), join(outputDir, "src", "tools", "list-routes.ts"));
120
+ console.log(` ✅ ${detected}/list-routes.ts`);
121
+ }
122
+
123
+ // 7. Generate index.ts
124
+ const indexContent = generateIndex(slug, cwd, detected, stackConfig);
125
+ writeFileSync(join(outputDir, "src", "index.ts"), indexContent);
126
+ console.log(` ✅ index.ts (generated)`);
127
+
128
+ // 8. Generate tsconfig.json
129
+ writeFileSync(join(outputDir, "tsconfig.json"), JSON.stringify({
130
+ compilerOptions: {
131
+ target: "ES2022", module: "Node16", moduleResolution: "Node16",
132
+ outDir: "./dist", rootDir: "./src", strict: true,
133
+ esModuleInterop: true, skipLibCheck: true, resolveJsonModule: true,
134
+ forceConsistentCasingInFileNames: true, declaration: false,
135
+ },
136
+ include: ["src/**/*"],
137
+ }, null, 2));
138
+ console.log(` ✅ tsconfig.json`);
139
+
140
+ // 9. Generate package.json
141
+ writeFileSync(join(outputDir, "package.json"), JSON.stringify({
142
+ name: `mcp-${slug}`,
143
+ version: "0.1.0",
144
+ description: `MCP server for ${slug} (${stackConfig.label})`,
145
+ type: "module",
146
+ main: "./dist/index.js",
147
+ scripts: { build: "tsc", start: "node dist/index.js" },
148
+ dependencies: { "@modelcontextprotocol/sdk": "^1.0.4" },
149
+ devDependencies: { "@types/node": "^20.11.0", typescript: "^5.4.0" },
150
+ }, null, 2));
151
+ console.log(` ✅ package.json`);
152
+
153
+ // 10. Create project-docs folder
154
+ const docsDir = join(CLAUDE_HOME, "project-docs", slug);
155
+ if (!existsSync(docsDir)) {
156
+ mkdirSync(docsDir, { recursive: true });
157
+ console.log(` ✅ Created ${docsDir}`);
158
+ }
159
+
160
+ // 11. Install deps & build
161
+ console.log(`\n📥 Installing dependencies...`);
162
+ try {
163
+ execSync("npm install", { cwd: outputDir, stdio: "pipe" });
164
+ console.log(` ✅ npm install`);
165
+ } catch (e) {
166
+ console.log(` ⚠️ npm install failed — run manually: cd ${outputDir} && npm install`);
167
+ }
168
+
169
+ console.log(`🔨 Building...`);
170
+ try {
171
+ execSync("npm run build", { cwd: outputDir, stdio: "pipe" });
172
+ console.log(` ✅ Build successful`);
173
+ } catch (e) {
174
+ console.log(` ⚠️ Build failed — run manually: cd ${outputDir} && npm run build`);
175
+ }
176
+
177
+ console.log(`\n✅ MCP server '${slug}' generated at ${outputDir}`);
178
+ console.log(`\nJalankan 'asap-mcp link' di project untuk register ke .mcp.json\n`);
179
+
180
+ rl.close();
181
+ }
182
+
183
+ function generateIndex(slug, projectPath, stack, config) {
184
+ const scanRoots = JSON.stringify(config.scan_roots);
185
+ const fileExts = JSON.stringify(config.file_extensions);
186
+ const hasSymbolIndex = config.symbol_index !== false;
187
+ const pathEscaped = projectPath.replace(/\\/g, "\\\\");
188
+
189
+ let imports = `
190
+ import { getClaudeMd } from "./tools/get-claude-md.js";
191
+ import { getCurrentState } from "./tools/get-current-state.js";
192
+ import { getMistakes } from "./tools/get-mistakes.js";
193
+ import { logTask } from "./tools/log-task.js";
194
+ import { updateState } from "./tools/update-state.js";
195
+ import { recordMistake } from "./tools/record-mistake.js";
196
+ import { predictImpact } from "./tools/predict-impact.js";
197
+ import { listRoutes } from "./tools/list-routes.js";`;
198
+
199
+ let symbolInit = "";
200
+ let findSymbolImport = "";
201
+ let symbolTools = "";
202
+ let symbolHandlers = "";
203
+
204
+ if (hasSymbolIndex) {
205
+ findSymbolImport = `
206
+ import { SymbolIndex } from "./indexer/symbol-index.js";`;
207
+ symbolInit = `const symbolIndex = new SymbolIndex(PROJECT_PATH);`;
208
+
209
+ const kindEnum = stack === "laravel"
210
+ ? '"function", "method", "class", "any"'
211
+ : '"function", "component", "class", "hook", "type", "context", "any"';
212
+
213
+ symbolTools = `,
214
+ {
215
+ name: "find_symbol",
216
+ description: "Cari function/method/class/component di codebase. Return: name, file, line, kind.",
217
+ inputSchema: {
218
+ type: "object" as const,
219
+ properties: {
220
+ name: { type: "string", description: "Exact name (case-sensitive)" },
221
+ kind: { type: "string", enum: [${kindEnum}], description: "Filter by kind, default any" },
222
+ },
223
+ required: ["name"],
224
+ },
225
+ },
226
+ {
227
+ name: "get_symbol_body",
228
+ description: "Ambil body fungsi/method/component (default 80 baris). Pakai SETELAH find_symbol.",
229
+ inputSchema: {
230
+ type: "object" as const,
231
+ properties: {
232
+ name: { type: "string" },
233
+ max_lines: { type: "number", description: "Max baris, default 80" },
234
+ },
235
+ required: ["name"],
236
+ },
237
+ },
238
+ {
239
+ name: "rebuild_index",
240
+ description: "Re-scan semua file di project untuk update symbol index.",
241
+ inputSchema: { type: "object" as const, properties: {} },
242
+ }`;
243
+
244
+ symbolHandlers = `
245
+ case "find_symbol": {
246
+ const name = String(args?.name ?? "");
247
+ await symbolIndex.ensureBuilt();
248
+ let results = symbolIndex.find(name, args?.kind ? String(args.kind) : undefined);
249
+ if (results.length === 0) {
250
+ const rb = await symbolIndex.rebuild();
251
+ results = symbolIndex.find(name, args?.kind ? String(args.kind) : undefined);
252
+ result = { query: { name, kind: args?.kind ?? "any" }, count: results.length, results, auto_rebuilt: true, rebuild_ms: rb.ms };
253
+ } else {
254
+ result = { query: { name, kind: args?.kind ?? "any" }, count: results.length, results };
255
+ }
256
+ break;
257
+ }
258
+ case "get_symbol_body": {
259
+ const name = String(args?.name ?? "");
260
+ const maxLines = typeof args?.max_lines === "number" ? args.max_lines : 80;
261
+ await symbolIndex.ensureBuilt();
262
+ let results = symbolIndex.find(name);
263
+ if (results.length === 0) { await symbolIndex.rebuild(); results = symbolIndex.find(name); }
264
+ if (results.length === 0) { result = { name, found: false }; break; }
265
+ const first = results[0];
266
+ const content = await readFile(first.file, "utf-8");
267
+ const lines = content.split("\\n");
268
+ const startLine = first.line - 1;
269
+ const endLine = Math.min(lines.length, startLine + maxLines);
270
+ result = { name, file: first.file, line: first.line, kind: first.kind, body: lines.slice(startLine, endLine).join("\\n"), truncated: endLine - startLine === maxLines };
271
+ break;
272
+ }
273
+ case "rebuild_index":
274
+ result = await symbolIndex.rebuild();
275
+ break;`;
276
+ }
277
+
278
+ return `#!/usr/bin/env node
279
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
280
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
281
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
282
+ import { join } from "path";
283
+ import { readFile } from "fs/promises";
284
+ ${imports}
285
+ ${findSymbolImport}
286
+
287
+ const PROJECT_SLUG = process.env.PROJECT_SLUG ?? "${slug}";
288
+ const PROJECT_PATH = process.env.PROJECT_PATH ?? "${pathEscaped}";
289
+ const CLAUDE_HOME = process.env.CLAUDE_HOME ?? \`\${process.env.USERPROFILE ?? process.env.HOME}/.claude\`;
290
+ const DOCS_PATH = join(CLAUDE_HOME, "project-docs", PROJECT_SLUG);
291
+
292
+ ${symbolInit}
293
+
294
+ const server = new Server(
295
+ { name: "${slug}", version: "0.1.0" },
296
+ { capabilities: { tools: {} } }
297
+ );
298
+
299
+ const tools = [
300
+ { name: "get_claude_md", description: "Ambil CLAUDE.md project.", inputSchema: { type: "object" as const, properties: {} } },
301
+ { name: "get_current_state", description: "Ambil CURRENT_STATE.md — task aktif + selesai.", inputSchema: { type: "object" as const, properties: {} } },
302
+ { name: "get_mistakes", description: "Ambil MISTAKES.md, bisa filter by topic.", inputSchema: { type: "object" as const, properties: { topic: { type: "string", description: "Optional keyword filter" } } } },
303
+ { name: "list_routes", description: "List semua routes/pages di project.", inputSchema: { type: "object" as const, properties: {} } },
304
+ {
305
+ name: "predict_impact",
306
+ description: "Impact analysis — cari siapa yang pakai symbol ini. Panggil sebelum refactor.",
307
+ inputSchema: { type: "object" as const, properties: { symbol: { type: "string", description: "Nama symbol (case-sensitive)" } }, required: ["symbol"] },
308
+ },
309
+ {
310
+ name: "log_task",
311
+ description: "Catat task selesai ke AUDIT_LOG.md.",
312
+ inputSchema: { type: "object" as const, properties: { task_id: { type: "string" }, action: { type: "string" }, files: { type: "array", items: { type: "string" } }, result: { type: "string" } }, required: ["task_id", "action", "files", "result"] },
313
+ },
314
+ {
315
+ name: "update_state",
316
+ description: "Update CURRENT_STATE.md — tambah/complete/update task.",
317
+ inputSchema: { type: "object" as const, properties: { action: { type: "string", enum: ["add_task", "complete_task", "update_task"] }, task_id: { type: "string" }, title: { type: "string" }, status: { type: "string" }, scope: { type: "string" }, files_changed: { type: "array", items: { type: "string" } }, notes: { type: "string" } }, required: ["action", "task_id"] },
318
+ },
319
+ {
320
+ name: "record_mistake",
321
+ description: "Catat bug/gotcha ke MISTAKES.md.",
322
+ inputSchema: { type: "object" as const, properties: { title: { type: "string" }, found_during: { type: "string" }, file: { type: "string" }, root_cause: { type: "string" }, fix: { type: "string" }, impact: { type: "string", enum: ["low", "medium", "high"] }, related: { type: "string" } }, required: ["title", "found_during", "file", "root_cause", "fix", "impact"] },
323
+ }${symbolTools}
324
+ ];
325
+
326
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
327
+
328
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
329
+ const { name, arguments: args } = request.params;
330
+ try {
331
+ let result: unknown;
332
+ switch (name) {
333
+ case "get_claude_md": result = await getClaudeMd(DOCS_PATH); break;
334
+ case "get_current_state": result = await getCurrentState(DOCS_PATH); break;
335
+ case "get_mistakes": result = await getMistakes(DOCS_PATH, args?.topic ? String(args.topic) : undefined); break;
336
+ case "list_routes": result = await listRoutes(PROJECT_PATH); break;
337
+ case "predict_impact": result = await predictImpact(PROJECT_PATH, ${scanRoots}, ${fileExts}, String(args?.symbol ?? "")); break;
338
+ case "log_task": result = await logTask(DOCS_PATH, { task_id: String(args?.task_id ?? ""), action: String(args?.action ?? ""), files: Array.isArray(args?.files) ? args.files.map(String) : [], result: String(args?.result ?? "done") }); break;
339
+ case "update_state": result = await updateState(DOCS_PATH, { action: String(args?.action ?? "add_task") as any, task_id: String(args?.task_id ?? ""), title: args?.title ? String(args.title) : undefined, status: args?.status ? String(args.status) : undefined, scope: args?.scope ? String(args.scope) : undefined, files_changed: Array.isArray(args?.files_changed) ? args.files_changed.map(String) : undefined, notes: args?.notes ? String(args.notes) : undefined }); break;
340
+ case "record_mistake": result = await recordMistake(DOCS_PATH, { title: String(args?.title ?? ""), found_during: String(args?.found_during ?? ""), file: String(args?.file ?? ""), root_cause: String(args?.root_cause ?? ""), fix: String(args?.fix ?? ""), impact: (args?.impact as any) ?? "medium", related: args?.related ? String(args.related) : undefined }); break;
341
+ ${symbolHandlers}
342
+ default: throw new Error(\`Unknown tool: \${name}\`);
343
+ }
344
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
345
+ } catch (err) {
346
+ return { content: [{ type: "text", text: \`Error: \${err instanceof Error ? err.message : String(err)}\` }], isError: true };
347
+ }
348
+ });
349
+
350
+ const transport = new StdioServerTransport();
351
+ await server.connect(transport);
352
+ console.error(\`[\${PROJECT_SLUG}-mcp] connected. path=\${PROJECT_PATH}\`);
353
+ `;
354
+ }
355
+
356
+ // ─── INIT ──────────────────────────────────────────────────────
357
+
358
+ async function init() {
359
+ console.log("\n🔧 MCP Orchestrator — Setup\n");
360
+
361
+ if (!existsSync(CLAUDE_HOME)) {
362
+ mkdirSync(CLAUDE_HOME, { recursive: true });
363
+ console.log(`✅ Created ${CLAUDE_HOME}`);
364
+ }
365
+
366
+ const name = await ask("Nama developer: ");
367
+ const email = await ask("Email: ");
368
+
369
+ const identityPath = join(CLAUDE_HOME, "agent-identity.json");
370
+ if (!existsSync(identityPath)) {
371
+ writeFileSync(identityPath, JSON.stringify({
372
+ name: "Agent Fullstack",
373
+ specialization: "Multi-stack: Laravel, React, Next.js, Express",
374
+ developer: { name, email },
375
+ }, null, 2));
376
+ console.log(`✅ Created ${identityPath}`);
377
+ } else {
378
+ console.log(`⏭️ ${identityPath} already exists, skipping`);
379
+ }
380
+
381
+ const focusPath = join(CLAUDE_HOME, "current-focus.json");
382
+ if (!existsSync(focusPath)) {
383
+ writeFileSync(focusPath, JSON.stringify({ mode: "general", active_profile: null, profiles: {} }, null, 2));
384
+ console.log(`✅ Created ${focusPath}`);
385
+ } else {
386
+ console.log(`⏭️ ${focusPath} already exists, skipping`);
387
+ }
388
+
389
+ const docsPath = join(CLAUDE_HOME, "project-docs");
390
+ if (!existsSync(docsPath)) {
391
+ mkdirSync(docsPath, { recursive: true });
392
+ console.log(`✅ Created ${docsPath}`);
393
+ }
394
+
395
+ console.log("\n✅ Setup selesai!\n");
396
+ console.log("Selanjutnya:");
397
+ console.log(" asap-mcp generate — Generate MCP server untuk project");
398
+ console.log(" asap-mcp link — Link semua MCP ke .mcp.json\n");
399
+
400
+ rl.close();
401
+ }
402
+
403
+ // ─── LINK ──────────────────────────────────────────────────────
404
+
405
+ function link() {
406
+ const cwd = process.cwd();
407
+ const mcpConfigPath = join(cwd, ".mcp.json");
408
+
409
+ let config = { mcpServers: {} };
410
+ if (existsSync(mcpConfigPath)) {
411
+ try {
412
+ config = JSON.parse(readFileSync(mcpConfigPath, "utf-8"));
413
+ if (!config.mcpServers) config.mcpServers = {};
414
+ } catch { config = { mcpServers: {} }; }
415
+ }
416
+
417
+ // 1. Orchestrator from npm
418
+ const orchestratorPath = resolve(__dirname, "..", "dist", "index.js");
419
+ if (existsSync(orchestratorPath)) {
420
+ const action = config.mcpServers.orchestrator ? "🔄 Updated" : "✅ Added";
421
+ config.mcpServers.orchestrator = {
422
+ command: "node",
423
+ args: [orchestratorPath.replace(/\\/g, "/")],
424
+ env: { CLAUDE_HOME: CLAUDE_HOME.replace(/\\/g, "/") },
425
+ };
426
+ console.log(`${action} 'orchestrator' (from npm package)`);
427
+ }
428
+
429
+ // 2. Scan ~/.claude/mcp/ for all MCP servers
430
+ const mcpRoot = join(CLAUDE_HOME, "mcp");
431
+ if (existsSync(mcpRoot)) {
432
+ try {
433
+ const entries = readdirSync(mcpRoot, { withFileTypes: true });
434
+ for (const entry of entries) {
435
+ if (!entry.isDirectory()) continue;
436
+ if (entry.name === "orchestrator") continue;
437
+ const serverIndex = join(mcpRoot, entry.name, "dist", "index.js");
438
+ if (!existsSync(serverIndex)) continue;
439
+
440
+ const serverName = entry.name;
441
+ const action = config.mcpServers[serverName] ? "🔄 Updated" : "✅ Added";
442
+ config.mcpServers[serverName] = {
443
+ command: "node",
444
+ args: [serverIndex.replace(/\\/g, "/")],
445
+ env: { CLAUDE_HOME: CLAUDE_HOME.replace(/\\/g, "/") },
446
+ };
447
+ console.log(`${action} '${serverName}' (from ~/.claude/mcp/${entry.name}/)`);
448
+ }
449
+ } catch {}
450
+ }
451
+
452
+ const serverCount = Object.keys(config.mcpServers).length;
453
+ writeFileSync(mcpConfigPath, JSON.stringify(config, null, 2) + "\n");
454
+ console.log(`\n📄 ${mcpConfigPath}`);
455
+ console.log(`🔗 ${serverCount} MCP server(s) linked — mereka satu tim sekarang.`);
456
+ console.log("Restart Claude Code jika sudah running.\n");
457
+ }
458
+
459
+ // ─── INFO ──────────────────────────────────────────────────────
460
+
461
+ function info() {
462
+ console.log("\n📦 @agilsee/mcp-orchestrator");
463
+ console.log(` CLAUDE_HOME: ${CLAUDE_HOME}`);
464
+ console.log("\nCommands:");
465
+ console.log(" asap-mcp init — Setup ~/.claude (identity, focus, project-docs)");
466
+ console.log(" asap-mcp generate — Auto-detect stack & generate MCP server untuk project");
467
+ console.log(" asap-mcp link — Link semua MCP servers ke .mcp.json");
468
+ console.log(" asap-mcp dashboard — Open Memory Dashboard (localhost:37800)");
469
+ console.log(" asap-mcp info — Show this info");
470
+ console.log("\nSupported stacks: Laravel, Next.js, Express, React (Vite/CRA)");
471
+ console.log("Memory Dashboard: http://localhost:37800\n");
472
+ }
473
+
474
+ // ─── MAIN ──────────────────────────────────────────────────────
475
+
476
+ switch (command) {
477
+ case "init": await init(); break;
478
+ case "generate": await generate(); break;
479
+ case "link": link(); break;
480
+ case "info": info(); break;
481
+ case "dashboard": case "dash": case "ui": {
482
+ const { execSync, spawn } = await import("child_process");
483
+ const serverPath = path.join(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1")), "..", "dist", "server", "web-server.js");
484
+ console.log("Starting MCP Memory Dashboard...");
485
+ const child = spawn("node", [serverPath], { stdio: "inherit", env: { ...process.env, CLAUDE_HOME: CLAUDE_HOME } });
486
+ child.on("error", (err) => { console.error("Failed to start dashboard:", err.message); });
487
+ break;
488
+ }
489
+ default: info(); break;
490
+ }