@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
@@ -0,0 +1,255 @@
1
+ import { readFile, readdir } from "fs/promises";
2
+ import { join } from "path";
3
+ export async function smartBootstrap(claudeHome) {
4
+ const docsRoot = join(claudeHome, "project-docs");
5
+ // 1. Identity
6
+ let identity = null;
7
+ try {
8
+ const raw = await readFile(join(claudeHome, "agent-identity.json"), "utf-8");
9
+ const id = JSON.parse(raw);
10
+ identity = { name: id.name ?? "Agent", author: id.author ?? "—", spec: id.specialization ?? "—" };
11
+ }
12
+ catch { /* skip */ }
13
+ // 2. Focus
14
+ let focus = null;
15
+ let activeProjects = [];
16
+ try {
17
+ const raw = await readFile(join(claudeHome, "current-focus.json"), "utf-8");
18
+ const f = JSON.parse(raw);
19
+ const mode = f.mode ?? "general";
20
+ const profile = f.profiles?.[f.active_profile ?? ""] ?? {};
21
+ const label = profile.label ?? f.active_profile ?? "general";
22
+ activeProjects = profile.active_projects?.map((p) => p.slug ?? p) ?? [];
23
+ focus = { mode, label, projects: activeProjects };
24
+ }
25
+ catch { /* skip */ }
26
+ // 3. Energy check
27
+ const now = new Date();
28
+ const hour = now.getHours();
29
+ const day = now.getDay(); // 0=Sun, 6=Sat
30
+ const isWorkHour = day >= 1 && day <= 5 && hour >= 8 && hour < 17;
31
+ const energy = isWorkHour
32
+ ? { ok: true }
33
+ : { ok: false, warning: `Di luar jam kerja (${now.toLocaleTimeString("id-ID")}). Pertimbangkan istirahat.` };
34
+ // 4. Determine which projects to scan
35
+ let slugsToScan = activeProjects;
36
+ if (slugsToScan.length === 0) {
37
+ try {
38
+ const entries = await readdir(docsRoot, { withFileTypes: true });
39
+ slugsToScan = entries.filter((e) => e.isDirectory()).map((e) => e.name);
40
+ }
41
+ catch {
42
+ slugsToScan = [];
43
+ }
44
+ }
45
+ // 5. Scan each project (parallel-ish)
46
+ const projectSummaries = [];
47
+ const qccActive = [];
48
+ const recentMistakes = [];
49
+ for (const slug of slugsToScan) {
50
+ const slugPath = join(docsRoot, slug);
51
+ // CURRENT_STATE.md
52
+ let activeTasks = 0;
53
+ let recentCompleted = 0;
54
+ const blockers = [];
55
+ try {
56
+ const state = await readFile(join(slugPath, "CURRENT_STATE.md"), "utf-8");
57
+ // Count active tasks
58
+ const activeSection = extractSection(state, "## Task Aktif", "## Terakhir Selesai");
59
+ const taskMatches = activeSection.match(/^### /gm);
60
+ activeTasks = taskMatches?.length ?? 0;
61
+ // Count recent completed
62
+ const completedSection = extractSection(state, "## Terakhir Selesai", "## Blockers");
63
+ const compMatches = completedSection.match(/^### /gm);
64
+ recentCompleted = compMatches?.length ?? 0;
65
+ // Blockers
66
+ const blockerSection = extractSection(state, "## Blockers", null);
67
+ if (blockerSection && !blockerSection.includes("(tidak ada)")) {
68
+ const lines = blockerSection.split("\n").filter((l) => l.startsWith("- "));
69
+ blockers.push(...lines.map((l) => l.replace(/^- /, "").trim()).slice(0, 3));
70
+ }
71
+ }
72
+ catch { /* no state file */ }
73
+ // AUDIT_LOG.md — last entry
74
+ let lastAudit = null;
75
+ try {
76
+ const audit = await readFile(join(slugPath, "AUDIT_LOG.md"), "utf-8");
77
+ const lines = audit.split("\n").filter((l) => l.startsWith("|") && !l.includes("---") && !/\|\s*(Date|Action)\s*\|/.test(l));
78
+ if (lines.length > 0) {
79
+ lastAudit = lines[lines.length - 1].replace(/^\||\|$/g, "").trim();
80
+ }
81
+ }
82
+ catch { /* skip */ }
83
+ // MISTAKES.md — recent (top 3)
84
+ try {
85
+ const mistakes = await readFile(join(slugPath, "MISTAKES.md"), "utf-8");
86
+ const sections = mistakes.split(/(?=^## \[)/m).filter((s) => s.startsWith("## ["));
87
+ const recent = sections.slice(-3).reverse();
88
+ for (const sec of recent) {
89
+ const titleMatch = sec.match(/^## \[[\d-]+\]\s*—\s*(.+)$/m);
90
+ if (titleMatch) {
91
+ recentMistakes.push(`[${slug}] ${titleMatch[1].trim()}`);
92
+ }
93
+ }
94
+ }
95
+ catch { /* skip */ }
96
+ // QCC_CYCLES.md — active cycles
97
+ try {
98
+ const qcc = await readFile(join(slugPath, "QCC_CYCLES.md"), "utf-8");
99
+ const cycles = qcc.split(/(?=\n## \[QCC-)/).filter((b) => b.includes("[QCC-"));
100
+ for (const block of cycles) {
101
+ if (!block.includes("**Status**: active"))
102
+ continue;
103
+ const hMatch = block.match(/\[(QCC-\d+)\] Tema: (.+)/);
104
+ if (!hMatch)
105
+ continue;
106
+ const createdMatch = block.match(/\*\*Created\*\*: ([\d-]+)/);
107
+ const created = createdMatch ? new Date(createdMatch[1]) : now;
108
+ const daysOld = Math.floor((now.getTime() - created.getTime()) / 86400000);
109
+ // Determine current step
110
+ let currentStep = 2;
111
+ for (let s = 8; s >= 2; s--) {
112
+ const header = `### Step ${s}:`;
113
+ const idx = block.indexOf(header);
114
+ if (idx === -1)
115
+ continue;
116
+ const section = block.slice(idx, block.indexOf("### Step", idx + header.length)).concat(block.slice(idx));
117
+ const isEmpty = section.includes("(belum diisi)") ||
118
+ section.includes(": —") ||
119
+ (s >= 5 && s <= 6 && !section.match(/\|\s*\d+\s*\|/));
120
+ if (!isEmpty) {
121
+ currentStep = Math.min(s + 1, 8);
122
+ break;
123
+ }
124
+ }
125
+ qccActive.push({
126
+ slug,
127
+ qcc_id: hMatch[1],
128
+ theme: hMatch[2].trim(),
129
+ current_step: currentStep,
130
+ days_stuck: daysOld,
131
+ });
132
+ }
133
+ }
134
+ catch { /* skip */ }
135
+ projectSummaries.push({
136
+ slug,
137
+ active_tasks: activeTasks,
138
+ recent_completed: recentCompleted,
139
+ blockers,
140
+ last_audit_entry: lastAudit,
141
+ });
142
+ }
143
+ // 6. Determine suggested action
144
+ const suggested = determineSuggestion(projectSummaries, qccActive, recentMistakes);
145
+ // 7. Determine response mode (Caveman compression)
146
+ const responseMode = determineResponseMode(energy, qccActive, suggested);
147
+ // 8. Build greeting
148
+ const name = identity?.name ?? "Agent Fullstack PHP";
149
+ const focusLabel = focus?.label ?? "General";
150
+ const projectList = focus?.projects.join(" + ") ?? "—";
151
+ const greeting = `${name} | Fokus: **${focusLabel}** (${projectList}) | ${projectSummaries.reduce((a, p) => a + p.active_tasks, 0)} task aktif`;
152
+ return {
153
+ greeting,
154
+ identity,
155
+ focus,
156
+ energy,
157
+ response_mode: responseMode,
158
+ project_summaries: projectSummaries,
159
+ qcc_active: qccActive,
160
+ recent_mistakes: recentMistakes.slice(0, 5),
161
+ suggested_action: suggested,
162
+ suggested_skill: suggested.skill ?? null,
163
+ };
164
+ }
165
+ // --- Response Mode (Caveman-style compression) ---
166
+ const RESPONSE_RULES = {
167
+ ultra: [
168
+ "Maks 2 kalimat per poin. No intro, no recap, no filler.",
169
+ "Code snippet > penjelasan panjang. Simbol OK: → ✅ ❌ ⚠️",
170
+ "Jangan ulangi yang sudah user tahu. Fragment > kalimat lengkap.",
171
+ "Bilingual OK: Indo + English technical terms.",
172
+ ],
173
+ full: [
174
+ "Fragment > kalimat lengkap. No filler (Great question, Let me explain...).",
175
+ "Jawab langsung, skip pengantar dan penutup.",
176
+ "Code snippet > penjelasan panjang kalau bisa.",
177
+ "Bilingual OK: Indo + English technical terms.",
178
+ ],
179
+ lite: [
180
+ "Kalimat lengkap tapi ringkas. Skip filler dan basa-basi.",
181
+ "Boleh jelaskan jika konteks baru bagi user.",
182
+ "Bilingual OK: Indo + English technical terms.",
183
+ ],
184
+ off: [],
185
+ };
186
+ function determineResponseMode(energy, qccActive, suggested) {
187
+ // QCC report aktif dan suggested action = qcc → off (butuh kalimat formal)
188
+ if (qccActive.length > 0 && suggested.tool === "qcc_update") {
189
+ return { intensity: "off", rules: RESPONSE_RULES.off };
190
+ }
191
+ // Di luar jam kerja → lite (santai, lebih jelas)
192
+ if (!energy.ok) {
193
+ return { intensity: "lite", rules: RESPONSE_RULES.lite };
194
+ }
195
+ // Default jam kerja → full
196
+ return { intensity: "full", rules: RESPONSE_RULES.full };
197
+ }
198
+ function determineSuggestion(projects, qcc, mistakes) {
199
+ // Priority 1: Blockers
200
+ const allBlockers = projects.flatMap((p) => p.blockers);
201
+ if (allBlockers.length > 0) {
202
+ return {
203
+ priority: "high",
204
+ message: `Ada ${allBlockers.length} blocker: "${allBlockers[0]}". Selesaikan ini dulu.`,
205
+ skill: "/debug",
206
+ };
207
+ }
208
+ // Priority 2: QCC stuck >3 days
209
+ const stuckQcc = qcc.filter((q) => q.days_stuck >= 3);
210
+ if (stuckQcc.length > 0) {
211
+ const q = stuckQcc[0];
212
+ const stepNames = {
213
+ 2: "Kondisi", 3: "Target SMART", 4: "Fishbone",
214
+ 5: "Rencana", 6: "Pelaksanaan", 7: "Evaluasi", 8: "Validasi",
215
+ };
216
+ return {
217
+ priority: "high",
218
+ message: `${q.qcc_id} "${q.theme}" stuck di Step ${q.current_step} (${stepNames[q.current_step] ?? "?"}) sudah ${q.days_stuck} hari. Lanjutkan.`,
219
+ tool: "qcc_update",
220
+ };
221
+ }
222
+ // Priority 3: Active tasks exist
223
+ const totalActive = projects.reduce((a, p) => a + p.active_tasks, 0);
224
+ if (totalActive > 0) {
225
+ const proj = projects.find((p) => p.active_tasks > 0);
226
+ return {
227
+ priority: "medium",
228
+ message: `${totalActive} task aktif di ${proj?.slug ?? "project"}. Lanjutkan task yang in-progress.`,
229
+ skill: "/execute-plan",
230
+ };
231
+ }
232
+ // Priority 4: Recent mistakes suggest debugging opportunity
233
+ if (mistakes.length >= 3) {
234
+ return {
235
+ priority: "medium",
236
+ message: `${mistakes.length} bugs tercatat. Pertimbangkan buat QCC cycle untuk address secara sistematis.`,
237
+ tool: "qcc_create",
238
+ };
239
+ }
240
+ // Priority 5: Nothing urgent
241
+ return {
242
+ priority: "low",
243
+ message: "Tidak ada yang urgent. Siap terima task baru.",
244
+ };
245
+ }
246
+ function extractSection(content, startHeader, endHeader) {
247
+ const startIdx = content.indexOf(startHeader);
248
+ if (startIdx === -1)
249
+ return "";
250
+ const afterHeader = startIdx + startHeader.length;
251
+ if (!endHeader)
252
+ return content.slice(afterHeader);
253
+ const endIdx = content.indexOf(endHeader, afterHeader);
254
+ return endIdx === -1 ? content.slice(afterHeader) : content.slice(afterHeader, endIdx);
255
+ }
@@ -0,0 +1,161 @@
1
+ import { readFile } from "fs/promises";
2
+ import { join } from "path";
3
+ export async function summarizeLastSession(claudeHome, limit = 5) {
4
+ const focus = await readJson(join(claudeHome, "current-focus.json"));
5
+ const mode = focus?.mode ?? "general";
6
+ const label = focus?.label ?? focus?.active_profile ?? null;
7
+ const activeProjects = focus?.active_projects ?? [];
8
+ const perProject = [];
9
+ for (const slug of activeProjects) {
10
+ perProject.push(await summarizeProject(claudeHome, slug, limit));
11
+ }
12
+ const tldr = buildTldr(label, mode, perProject);
13
+ return {
14
+ focus_label: label,
15
+ focus_mode: mode,
16
+ active_projects: activeProjects,
17
+ per_project: perProject,
18
+ tldr,
19
+ generated_at: new Date().toISOString(),
20
+ };
21
+ }
22
+ async function summarizeProject(claudeHome, slug, limit) {
23
+ const docsDir = join(claudeHome, "project-docs", slug);
24
+ const recentTasks = await parseAuditLog(join(docsDir, "AUDIT_LOG.md"), limit);
25
+ const openMistakes = await parseMistakes(join(docsDir, "MISTAKES.md"), limit);
26
+ const activeTasks = await parseCurrentStateActive(join(docsDir, "CURRENT_STATE.md"));
27
+ const lastActivity = recentTasks[0]?.date ?? null;
28
+ return {
29
+ slug,
30
+ recent_tasks: recentTasks,
31
+ open_mistakes: openMistakes,
32
+ active_tasks: activeTasks,
33
+ last_activity: lastActivity,
34
+ };
35
+ }
36
+ async function parseAuditLog(path, limit) {
37
+ const content = await safeRead(path);
38
+ if (!content)
39
+ return [];
40
+ const rows = [];
41
+ for (const line of content.split("\n")) {
42
+ if (!line.startsWith("|") || line.includes("---"))
43
+ continue;
44
+ if (/\|\s*Date\s*\|/i.test(line))
45
+ continue;
46
+ const cols = line
47
+ .split("|")
48
+ .map((c) => c.trim())
49
+ .filter(Boolean);
50
+ if (cols.length < 2 || !/^\d{4}-\d{2}-\d{2}/.test(cols[0]))
51
+ continue;
52
+ rows.push({
53
+ date: cols[0],
54
+ action: cols[1] ?? "",
55
+ result: cols[cols.length - 1] ?? "",
56
+ });
57
+ }
58
+ return rows.sort((a, b) => b.date.localeCompare(a.date)).slice(0, limit);
59
+ }
60
+ async function parseMistakes(path, limit) {
61
+ const content = await safeRead(path);
62
+ if (!content)
63
+ return [];
64
+ const sections = content.split(/(?=^##\s)/m).filter((s) => s.trim());
65
+ const results = [];
66
+ for (const section of sections) {
67
+ const titleMatch = section.match(/^##\s+\[(\d{4}-\d{2}-\d{2})\]\s*—\s*(.+)$/m);
68
+ if (!titleMatch)
69
+ continue;
70
+ const fileMatch = section.match(/\*\*File\*\*:\s*(.+)/);
71
+ const fixMatch = section.match(/\*\*Fix\*\*:\s*(.+)/);
72
+ const fixStatus = fixMatch?.[1]?.trim() ?? "";
73
+ const fixed = /sudah fix|fixed|done|resolved|selesai/i.test(fixStatus) &&
74
+ !/belum|not yet|pending|workaround/i.test(fixStatus);
75
+ if (fixed)
76
+ continue;
77
+ results.push({
78
+ date: titleMatch[1],
79
+ title: titleMatch[2].trim(),
80
+ file: fileMatch?.[1]?.trim() ?? "",
81
+ fix_status: fixStatus || "unknown",
82
+ });
83
+ }
84
+ return results.sort((a, b) => b.date.localeCompare(a.date)).slice(0, limit);
85
+ }
86
+ async function parseCurrentStateActive(path) {
87
+ const content = await safeRead(path);
88
+ if (!content)
89
+ return [];
90
+ const active = [];
91
+ let inActive = false;
92
+ for (const line of content.split("\n")) {
93
+ if (/^##\s+.*(active|sedang|in[- ]progress)/i.test(line)) {
94
+ inActive = true;
95
+ continue;
96
+ }
97
+ if (/^##\s/.test(line)) {
98
+ inActive = false;
99
+ continue;
100
+ }
101
+ if (!inActive)
102
+ continue;
103
+ const item = line.match(/^\s*[-*]\s+(.+)/);
104
+ if (item)
105
+ active.push(item[1].trim());
106
+ }
107
+ return active.slice(0, 10);
108
+ }
109
+ function buildTldr(label, mode, perProject) {
110
+ const tldr = [];
111
+ if (label)
112
+ tldr.push(`Focus: ${label} (mode: ${mode})`);
113
+ else
114
+ tldr.push(`Mode: ${mode} (no active focus label)`);
115
+ if (perProject.length === 0) {
116
+ tldr.push("Tidak ada active project di focus.");
117
+ return tldr;
118
+ }
119
+ for (const p of perProject) {
120
+ const parts = [`[${p.slug}]`];
121
+ if (p.last_activity) {
122
+ parts.push(`last activity ${p.last_activity}`);
123
+ const latest = p.recent_tasks[0];
124
+ if (latest)
125
+ parts.push(`— "${truncate(latest.action, 60)}"`);
126
+ }
127
+ else {
128
+ parts.push("belum ada audit log");
129
+ }
130
+ tldr.push(parts.join(" "));
131
+ if (p.open_mistakes.length > 0) {
132
+ tldr.push(` ⚠ ${p.open_mistakes.length} bug belum fix — terbaru: "${truncate(p.open_mistakes[0].title, 55)}"`);
133
+ }
134
+ if (p.active_tasks.length > 0) {
135
+ tldr.push(` → active task: "${truncate(p.active_tasks[0], 60)}"`);
136
+ }
137
+ }
138
+ return tldr;
139
+ }
140
+ function truncate(s, n) {
141
+ return s.length > n ? s.slice(0, n - 1) + "…" : s;
142
+ }
143
+ async function readJson(path) {
144
+ const content = await safeRead(path);
145
+ if (!content)
146
+ return null;
147
+ try {
148
+ return JSON.parse(content);
149
+ }
150
+ catch {
151
+ return null;
152
+ }
153
+ }
154
+ async function safeRead(path) {
155
+ try {
156
+ return await readFile(path, "utf-8");
157
+ }
158
+ catch {
159
+ return null;
160
+ }
161
+ }
@@ -0,0 +1,40 @@
1
+ import { readFile, writeFile } from "fs/promises";
2
+ import { join } from "path";
3
+ export async function switchFocus(claudeHome, profileKey) {
4
+ const path = join(claudeHome, "current-focus.json");
5
+ const raw = await readFile(path, "utf-8");
6
+ const focus = JSON.parse(raw);
7
+ const profiles = focus.available_focus_profiles ?? {};
8
+ if (profileKey === "general") {
9
+ focus.mode = "general";
10
+ focus.focus_label = "General (semua project terbuka)";
11
+ focus.active_projects = [];
12
+ focus.last_updated = new Date().toISOString().slice(0, 10);
13
+ await writeFile(path, JSON.stringify(focus, null, 2), "utf-8");
14
+ return {
15
+ file: path,
16
+ switched_to: "general",
17
+ message: "Focus switched to General mode",
18
+ };
19
+ }
20
+ if (!profiles[profileKey]) {
21
+ return {
22
+ error: true,
23
+ message: `Profile '${profileKey}' tidak ditemukan.`,
24
+ available: Object.keys(profiles),
25
+ };
26
+ }
27
+ const profile = profiles[profileKey];
28
+ focus.mode = "focused";
29
+ focus.focus_label = profile.label;
30
+ focus.active_projects = profile.active_projects;
31
+ focus.last_updated = new Date().toISOString().slice(0, 10);
32
+ await writeFile(path, JSON.stringify(focus, null, 2), "utf-8");
33
+ return {
34
+ file: path,
35
+ switched_to: profileKey,
36
+ label: profile.label,
37
+ active_projects: profile.active_projects,
38
+ message: `Focus switched to: ${profile.label}`,
39
+ };
40
+ }