@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,213 @@
1
+ import { readFile, readdir } from "fs/promises";
2
+ import { join } from "path";
3
+ export async function analyzeHistory(claudeHome) {
4
+ const docsRoot = join(claudeHome, "project-docs");
5
+ let slugs;
6
+ try {
7
+ const entries = await readdir(docsRoot, { withFileTypes: true });
8
+ slugs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
9
+ }
10
+ catch {
11
+ return emptyResult("Cannot read project-docs");
12
+ }
13
+ // Collect all audit entries and mistakes
14
+ const allFiles = new Map();
15
+ const allMistakes = [];
16
+ const allDates = [];
17
+ let totalTasks = 0;
18
+ for (const slug of slugs) {
19
+ // Parse AUDIT_LOG
20
+ try {
21
+ const auditPath = join(docsRoot, slug, "AUDIT_LOG.md");
22
+ const auditContent = await readFile(auditPath, "utf-8");
23
+ const auditEntries = parseAuditFiles(auditContent, slug);
24
+ totalTasks += auditEntries.length;
25
+ for (const entry of auditEntries) {
26
+ if (entry.date)
27
+ allDates.push(entry.date);
28
+ for (const file of entry.files) {
29
+ const normalized = normalizeFile(file);
30
+ if (!normalized)
31
+ continue;
32
+ if (!allFiles.has(normalized)) {
33
+ allFiles.set(normalized, { count: 0, projects: new Set(), tasks: [] });
34
+ }
35
+ const record = allFiles.get(normalized);
36
+ record.count++;
37
+ record.projects.add(slug);
38
+ record.tasks.push(entry.action);
39
+ }
40
+ }
41
+ }
42
+ catch { /* skip */ }
43
+ // Parse MISTAKES
44
+ try {
45
+ const mistakePath = join(docsRoot, slug, "MISTAKES.md");
46
+ const mistakeContent = await readFile(mistakePath, "utf-8");
47
+ const mistakes = parseMistakes(mistakeContent, slug);
48
+ allMistakes.push(...mistakes);
49
+ }
50
+ catch { /* skip */ }
51
+ }
52
+ // === ANALYSIS ===
53
+ // 1. Hotspots: files touched 3+ times
54
+ const hotspots = Array.from(allFiles.entries())
55
+ .filter(([, v]) => v.count >= 3)
56
+ .sort((a, b) => b[1].count - a[1].count)
57
+ .slice(0, 10)
58
+ .map(([file, v]) => ({
59
+ file,
60
+ touch_count: v.count,
61
+ projects: Array.from(v.projects),
62
+ }));
63
+ // 2. Repeat bugs: similar root causes
64
+ const causeGroups = groupBySimilarity(allMistakes.map((m) => ({
65
+ key: m.root_cause.toLowerCase(),
66
+ label: m.title,
67
+ project: m.project,
68
+ date: m.date,
69
+ })));
70
+ const repeatBugs = causeGroups
71
+ .filter((g) => g.count >= 2)
72
+ .map((g) => ({
73
+ pattern: g.representative,
74
+ count: g.count,
75
+ entries: g.items.map((i) => `[${i.project}] ${i.label} (${i.date})`),
76
+ }));
77
+ // 3. Warnings based on analysis
78
+ const warnings = [];
79
+ const suggestions = [];
80
+ // Hotspot warnings
81
+ for (const hs of hotspots) {
82
+ if (hs.touch_count >= 5) {
83
+ warnings.push(`🔥 ${hs.file} disentuh ${hs.touch_count}x — kemungkinan file terlalu besar atau tanggung jawab terlalu banyak. Pertimbangkan refactor/split.`);
84
+ }
85
+ }
86
+ // Repeat bug warnings
87
+ for (const rb of repeatBugs) {
88
+ warnings.push(`🔁 Bug pattern "${rb.pattern}" muncul ${rb.count}x — ada masalah sistemik yang perlu di-address, bukan cuma fix per-case.`);
89
+ }
90
+ // Common mistake patterns
91
+ const readFirstBugs = allMistakes.filter((m) => m.root_cause.toLowerCase().includes("read-first") ||
92
+ m.root_cause.toLowerCase().includes("myisam") ||
93
+ m.root_cause.toLowerCase().includes("transaction"));
94
+ if (readFirstBugs.length >= 2) {
95
+ warnings.push(`⚠️ ${readFirstBugs.length} bugs terkait read-first/MyISAM/transaction — pertimbangkan auto-check sebelum save.`);
96
+ }
97
+ // Suggestions based on data
98
+ if (totalTasks > 0 && allMistakes.length === 0) {
99
+ suggestions.push("Belum ada MISTAKES.md tercatat — mulai catat bug yang ditemukan supaya pattern bisa terdeteksi.");
100
+ }
101
+ if (hotspots.length > 0) {
102
+ suggestions.push(`Top hotspot: ${hotspots[0].file} (${hotspots[0].touch_count}x). Setiap kali mau sentuh file ini, cek MISTAKES.md dulu — kemungkinan ada known gotcha.`);
103
+ }
104
+ if (totalTasks >= 10 && repeatBugs.length === 0) {
105
+ suggestions.push("Tidak ada repeat bug terdeteksi dari history — good sign, pattern sudah cukup bersih.");
106
+ }
107
+ // Cross-project file overlap
108
+ const crossProjectFiles = Array.from(allFiles.entries())
109
+ .filter(([, v]) => v.projects.size > 1);
110
+ if (crossProjectFiles.length > 0) {
111
+ suggestions.push(`${crossProjectFiles.length} file disentuh di multiple projects — perubahan di sini bisa impact project lain.`);
112
+ }
113
+ // Date range
114
+ allDates.sort();
115
+ const dateRange = allDates.length > 0
116
+ ? { earliest: allDates[0], latest: allDates[allDates.length - 1] }
117
+ : null;
118
+ return {
119
+ hotspots,
120
+ repeat_bugs: repeatBugs,
121
+ warnings,
122
+ suggestions,
123
+ stats: {
124
+ total_tasks: totalTasks,
125
+ total_mistakes: allMistakes.length,
126
+ projects_analyzed: slugs.length,
127
+ date_range: dateRange,
128
+ },
129
+ };
130
+ }
131
+ function parseAuditFiles(content, _slug) {
132
+ const entries = [];
133
+ for (const line of content.split("\n")) {
134
+ if (!line.startsWith("|"))
135
+ continue;
136
+ if (line.includes("---") || /\|\s*Date\s*\|/i.test(line))
137
+ continue;
138
+ const cols = line.split("|").map((c) => c.trim()).filter(Boolean);
139
+ if (cols.length < 3 || !/^\d{4}-\d{2}-\d{2}/.test(cols[0]))
140
+ continue;
141
+ entries.push({
142
+ date: cols[0],
143
+ action: cols[1],
144
+ files: cols[2].split(",").map((f) => f.trim()).filter(Boolean),
145
+ });
146
+ }
147
+ return entries;
148
+ }
149
+ function parseMistakes(content, project) {
150
+ const sections = content.split(/(?=^##\s)/m).filter((s) => s.trim());
151
+ const results = [];
152
+ for (const section of sections) {
153
+ const titleMatch = section.match(/^##\s+\[(\d{4}-\d{2}-\d{2})\]\s*—\s*(.+)$/m);
154
+ if (!titleMatch)
155
+ continue;
156
+ const fileMatch = section.match(/\*\*File\*\*:\s*(.+)/);
157
+ const causeMatch = section.match(/\*\*Root cause\*\*:\s*(.+)/);
158
+ results.push({
159
+ project,
160
+ date: titleMatch[1],
161
+ title: titleMatch[2].trim(),
162
+ file: fileMatch ? fileMatch[1].trim() : "",
163
+ root_cause: causeMatch ? causeMatch[1].trim() : "",
164
+ });
165
+ }
166
+ return results;
167
+ }
168
+ function normalizeFile(raw) {
169
+ const trimmed = raw.trim();
170
+ if (!trimmed || trimmed === "—" || trimmed.length < 3)
171
+ return null;
172
+ // Remove line numbers, clean up
173
+ return trimmed.replace(/:\d+$/, "").replace(/\s*\(.*\)$/, "").trim();
174
+ }
175
+ function groupBySimilarity(items) {
176
+ const groups = [];
177
+ for (const item of items) {
178
+ let matched = false;
179
+ for (const group of groups) {
180
+ if (fuzzyMatch(item.key, group.items[0].key)) {
181
+ group.items.push(item);
182
+ group.count++;
183
+ matched = true;
184
+ break;
185
+ }
186
+ }
187
+ if (!matched) {
188
+ groups.push({ representative: item.label, count: 1, items: [item] });
189
+ }
190
+ }
191
+ return groups.sort((a, b) => b.count - a.count);
192
+ }
193
+ function fuzzyMatch(a, b) {
194
+ const wordsA = new Set(a.split(/\s+/).filter((w) => w.length > 3));
195
+ const wordsB = new Set(b.split(/\s+/).filter((w) => w.length > 3));
196
+ if (wordsA.size === 0 || wordsB.size === 0)
197
+ return false;
198
+ let overlap = 0;
199
+ for (const w of wordsA) {
200
+ if (wordsB.has(w))
201
+ overlap++;
202
+ }
203
+ return overlap / Math.min(wordsA.size, wordsB.size) >= 0.5;
204
+ }
205
+ function emptyResult(note) {
206
+ return {
207
+ hotspots: [],
208
+ repeat_bugs: [],
209
+ warnings: [note],
210
+ suggestions: [],
211
+ stats: { total_tasks: 0, total_mistakes: 0, projects_analyzed: 0, date_range: null },
212
+ };
213
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Auto-Dispatch — Orchestrator menganalisis query user,
3
+ * menentukan project mana yang terlibat, dan return dispatch plan
4
+ * berisi exact MCP tool calls yang harus Claude eksekusi.
5
+ *
6
+ * Flow: user query → detect module → search memory → build plan → Claude execute
7
+ */
8
+ import { readFile } from "fs/promises";
9
+ import { join } from "path";
10
+ /**
11
+ * Module → project mapping.
12
+ *
13
+ * Default kosong. User bisa override dengan menulis file:
14
+ * $CLAUDE_HOME/orchestrator-dispatch-map.json
15
+ *
16
+ * Format:
17
+ * {
18
+ * "order": ["app-fe", "app-be"],
19
+ * "invoice": ["app-fe", "app-be"],
20
+ * "vendor": ["procurement-fe", "procurement-be"]
21
+ * }
22
+ *
23
+ * Tanpa file ini, auto-dispatch fallback ke active_projects di current-focus.json.
24
+ */
25
+ const DEFAULT_MODULE_PROJECT_MAP = {};
26
+ // Intent detection patterns
27
+ const INTENT_PATTERNS = {
28
+ debug: [/bug/i, /error/i, /gagal/i, /tidak (bisa|jalan|tampil|muncul)/i, /rusak/i, /broken/i, /fix/i, /issue/i, /masalah/i],
29
+ trace: [/trace/i, /lacak/i, /cari (di|ke) mana/i, /flow/i, /alur/i],
30
+ feature: [/tambah/i, /bikin/i, /buat/i, /new feature/i, /fitur baru/i, /implementasi/i],
31
+ review: [/review/i, /cek/i, /periksa/i, /audit/i],
32
+ refactor: [/refactor/i, /pindah/i, /restructure/i, /cleanup/i],
33
+ };
34
+ function detectModule(query, map) {
35
+ const q = query.toLowerCase();
36
+ for (const mod of Object.keys(map)) {
37
+ if (q.includes(mod.toLowerCase()))
38
+ return mod;
39
+ }
40
+ return "unknown";
41
+ }
42
+ function detectIntent(query) {
43
+ for (const [intent, patterns] of Object.entries(INTENT_PATTERNS)) {
44
+ for (const pattern of patterns) {
45
+ if (pattern.test(query))
46
+ return intent;
47
+ }
48
+ }
49
+ return "explore";
50
+ }
51
+ function buildDispatchPlan(query, module, intent, projects, memoryHits) {
52
+ const targets = [];
53
+ for (const proj of projects) {
54
+ const agentName = proj; // MCP server name = project slug
55
+ const tools = [];
56
+ let order = 1;
57
+ // Step 1: Cek known bugs dulu (semua intent)
58
+ tools.push({
59
+ order: order++,
60
+ tool: `mcp__${agentName}__get_mistakes`,
61
+ args: { keyword: module !== "unknown" ? module : "" },
62
+ purpose: `Cek apakah bug terkait '${module}' sudah pernah ditemukan di ${proj}`,
63
+ });
64
+ // Step 2: Intent-specific tools
65
+ if (intent === "debug" || intent === "trace") {
66
+ // Find related symbols
67
+ const symbolQuery = module !== "unknown" ? module.replace(/-/g, "") : query.split(" ").slice(0, 2).join("");
68
+ tools.push({
69
+ order: order++,
70
+ tool: `mcp__${agentName}__find_symbol`,
71
+ args: { query: symbolQuery },
72
+ purpose: `Cari function/class terkait '${module}' di ${proj}`,
73
+ });
74
+ tools.push({
75
+ order: order++,
76
+ tool: `mcp__${agentName}__list_routes`,
77
+ args: {},
78
+ purpose: `List routes untuk trace endpoint terkait di ${proj}`,
79
+ });
80
+ }
81
+ if (intent === "feature" || intent === "refactor") {
82
+ tools.push({
83
+ order: order++,
84
+ tool: `mcp__${agentName}__find_symbol`,
85
+ args: { query: module !== "unknown" ? module : "" },
86
+ purpose: `Cari existing code terkait untuk extend/refactor di ${proj}`,
87
+ });
88
+ tools.push({
89
+ order: order++,
90
+ tool: `mcp__${agentName}__predict_impact`,
91
+ args: { symbol: module },
92
+ purpose: `Analisis impact perubahan di ${proj}`,
93
+ });
94
+ }
95
+ if (intent === "review") {
96
+ tools.push({
97
+ order: order++,
98
+ tool: `mcp__${agentName}__get_current_state`,
99
+ args: {},
100
+ purpose: `Cek state terkini ${proj}`,
101
+ });
102
+ }
103
+ // Determine role
104
+ const isFE = proj.includes("-fe") || proj.endsWith("-frontend") || proj.endsWith("-web");
105
+ const role = isFE ? "Trace di view/controller/JS" : "Trace di API/service/model/DB";
106
+ targets.push({
107
+ agent: agentName,
108
+ project_slug: proj,
109
+ tools,
110
+ reason: `${role} — ${proj} ${intent === "debug" ? "mungkin ada bug" : "perlu dicek"}`,
111
+ });
112
+ }
113
+ return targets;
114
+ }
115
+ async function loadDispatchMap(claudeHome) {
116
+ try {
117
+ const raw = await readFile(join(claudeHome, "orchestrator-dispatch-map.json"), "utf8");
118
+ const parsed = JSON.parse(raw);
119
+ if (parsed && typeof parsed === "object")
120
+ return parsed;
121
+ }
122
+ catch { }
123
+ return DEFAULT_MODULE_PROJECT_MAP;
124
+ }
125
+ export async function autoDispatch(claudeHome, args) {
126
+ const { query, force_projects } = args;
127
+ // 1. Load module map (user-overridable) + detect module & intent
128
+ const moduleMap = await loadDispatchMap(claudeHome);
129
+ const module = detectModule(query, moduleMap);
130
+ const intent = detectIntent(query);
131
+ // 2. Determine target projects
132
+ let projects;
133
+ if (force_projects && force_projects.length > 0) {
134
+ projects = force_projects;
135
+ }
136
+ else if (module !== "unknown" && moduleMap[module]) {
137
+ projects = moduleMap[module];
138
+ }
139
+ else {
140
+ // Default: cek focus aktif
141
+ try {
142
+ const focusRaw = await readFile(join(claudeHome, "current-focus.json"), "utf8");
143
+ const focus = JSON.parse(focusRaw);
144
+ projects = focus.active_projects?.map((p) => p.slug || p) || [];
145
+ }
146
+ catch {
147
+ projects = [];
148
+ }
149
+ }
150
+ // 3. Search memory for related issues
151
+ let memoryHits = 0;
152
+ try {
153
+ const { readFileSync, existsSync } = await import("fs");
154
+ const memDb = join(claudeHome, "data", "memory.json");
155
+ if (existsSync(memDb)) {
156
+ const db = JSON.parse(readFileSync(memDb, "utf8"));
157
+ const col = db.collections?.find((c) => c.name === "memories");
158
+ if (col?.data) {
159
+ const q = query.toLowerCase();
160
+ memoryHits = col.data.filter((e) => {
161
+ const text = `${e.title} ${e.content} ${(e.tags || []).join(" ")}`.toLowerCase();
162
+ return q.split(/\s+/).some((word) => word.length > 2 && text.includes(word));
163
+ }).length;
164
+ }
165
+ }
166
+ }
167
+ catch { }
168
+ // 4. Build dispatch plan
169
+ const targets = buildDispatchPlan(query, module, intent, projects, memoryHits);
170
+ const totalCalls = targets.reduce((sum, t) => sum + t.tools.length, 0);
171
+ // 5. Aggregation hint
172
+ let aggregationHint = "";
173
+ if (targets.length > 1) {
174
+ if (intent === "debug") {
175
+ aggregationHint = "Setelah eksekusi semua, bandingkan: apakah bug di FE (view/JS) atau BE (API/logic)? Cross-reference error message.";
176
+ }
177
+ else if (intent === "feature") {
178
+ aggregationHint = "Setelah eksekusi, pastikan FE dan BE konsisten. Cek apakah perlu endpoint baru di BE.";
179
+ }
180
+ else if (intent === "trace") {
181
+ aggregationHint = "Trace full flow: Blade → JS → Controller FE → Utility::HttpRequest → BE Controller → Service → Model.";
182
+ }
183
+ else {
184
+ aggregationHint = "Gabungkan hasil dari semua agent, highlight perbedaan atau konflik.";
185
+ }
186
+ }
187
+ else {
188
+ aggregationHint = "Single agent — langsung eksekusi.";
189
+ }
190
+ return {
191
+ query,
192
+ detected_module: module,
193
+ detected_intent: intent,
194
+ memory_hits: memoryHits,
195
+ targets,
196
+ aggregation_hint: aggregationHint,
197
+ total_calls: totalCalls,
198
+ };
199
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Check apakah saat ini di luar jam kerja developer.
3
+ * Jika ya, return warning supaya agent tanya kondisi dulu.
4
+ *
5
+ * Jam kerja: Senin-Jumat 08:00-17:00 WIB (UTC+7)
6
+ * Sabtu-Minggu: opsional (tidak trigger warning)
7
+ */
8
+ export function checkEnergy() {
9
+ // Use UTC+7 for WIB
10
+ const now = new Date();
11
+ const wibOffset = 7 * 60; // minutes
12
+ const utcMinutes = now.getUTCHours() * 60 + now.getUTCMinutes();
13
+ const wibMinutes = utcMinutes + wibOffset;
14
+ const wibHour = Math.floor((wibMinutes % 1440) / 60);
15
+ const wibDay = now.getUTCDay(); // 0=Sun, 6=Sat
16
+ // Adjust day if WIB offset pushes past midnight
17
+ const adjustedDay = wibMinutes >= 1440 ? (wibDay + 1) % 7 : wibDay;
18
+ const isWeekday = adjustedDay >= 1 && adjustedDay <= 5;
19
+ const isWorkHours = wibHour >= 8 && wibHour < 17;
20
+ const dayNames = [
21
+ "Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu",
22
+ ];
23
+ const result = {
24
+ current_time_wib: `${String(wibHour).padStart(2, "0")}:${String(wibMinutes % 60).padStart(2, "0")} WIB`,
25
+ day: dayNames[adjustedDay],
26
+ is_work_hours: isWorkHours,
27
+ is_weekday: isWeekday,
28
+ warning: null,
29
+ suggestion: null,
30
+ };
31
+ if (isWeekday && !isWorkHours) {
32
+ if (wibHour >= 17) {
33
+ result.warning = `Sekarang ${result.current_time_wib} — sudah lewat jam kerja (17:00).`;
34
+ result.suggestion =
35
+ "WAJIB tanyakan kondisi developer sebelum mulai task: 'Sudah lewat jam kerja, mau lanjut atau istirahat dulu?'";
36
+ }
37
+ else if (wibHour < 8) {
38
+ result.warning = `Sekarang ${result.current_time_wib} — belum jam kerja (08:00).`;
39
+ result.suggestion =
40
+ "Tanyakan apakah developer sengaja kerja pagi atau lupa waktu.";
41
+ }
42
+ }
43
+ else if (!isWeekday) {
44
+ result.warning = `Hari ${result.day} — weekend/opsional.`;
45
+ result.suggestion =
46
+ "Weekend opsional. Jangan push task berat kecuali developer yang minta.";
47
+ }
48
+ return result;
49
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Cross-Search — Cari informasi di SEMUA project sekaligus.
3
+ *
4
+ * Orchestrator langsung baca file dari semua project (tahu path dari registry).
5
+ * Tidak perlu dispatch ke agent lain — akses langsung.
6
+ *
7
+ * Search targets: MISTAKES.md, CURRENT_STATE.md, AUDIT_LOG.md, routes, symbols
8
+ */
9
+ import { readFile, readdir } from "fs/promises";
10
+ import { join, extname } from "path";
11
+ import { existsSync } from "fs";
12
+ async function loadRegistry(claudeHome) {
13
+ try {
14
+ const raw = await readFile(join(claudeHome, "project-registry.json"), "utf8");
15
+ const reg = JSON.parse(raw);
16
+ return reg.projects || {};
17
+ }
18
+ catch {
19
+ return {};
20
+ }
21
+ }
22
+ function scoreMatch(line, keywords) {
23
+ const lower = line.toLowerCase();
24
+ let score = 0;
25
+ for (const kw of keywords) {
26
+ if (kw.length < 3)
27
+ continue;
28
+ if (lower.includes(kw))
29
+ score += 1;
30
+ }
31
+ return score;
32
+ }
33
+ async function searchInFile(filePath, keywords, source, maxResults = 5) {
34
+ if (!existsSync(filePath))
35
+ return [];
36
+ try {
37
+ const content = await readFile(filePath, "utf8");
38
+ const lines = content.split("\n");
39
+ const matches = [];
40
+ for (let i = 0; i < lines.length; i++) {
41
+ const line = lines[i].trim();
42
+ if (!line || line.startsWith("---") || line === "#")
43
+ continue;
44
+ const score = scoreMatch(line, keywords);
45
+ if (score > 0) {
46
+ matches.push({
47
+ match: {
48
+ source,
49
+ file: filePath,
50
+ line_preview: line.substring(0, 200),
51
+ relevance: score >= 2 ? "high" : score === 1 ? "medium" : "low",
52
+ },
53
+ score,
54
+ });
55
+ }
56
+ }
57
+ // Sort by score desc, take top N
58
+ matches.sort((a, b) => b.score - a.score);
59
+ return matches.slice(0, maxResults).map((m) => m.match);
60
+ }
61
+ catch {
62
+ return [];
63
+ }
64
+ }
65
+ async function searchCodeFiles(projectPath, keywords, extensions = [".php", ".blade.php", ".js"], maxResults = 5) {
66
+ const matches = [];
67
+ const searchDirs = ["app", "routes", "resources"];
68
+ for (const dir of searchDirs) {
69
+ const dirPath = join(projectPath, dir);
70
+ if (!existsSync(dirPath))
71
+ continue;
72
+ try {
73
+ await scanDir(dirPath, keywords, extensions, matches, 3); // max depth 3
74
+ }
75
+ catch { }
76
+ }
77
+ matches.sort((a, b) => b.score - a.score);
78
+ return matches.slice(0, maxResults).map((m) => m.match);
79
+ }
80
+ async function scanDir(dirPath, keywords, extensions, results, maxDepth, currentDepth = 0) {
81
+ if (currentDepth >= maxDepth)
82
+ return;
83
+ if (results.length >= 20)
84
+ return; // cap total scanned
85
+ const ignore = ["vendor", "node_modules", ".git", "storage", "bootstrap", "public", "dist"];
86
+ try {
87
+ const entries = await readdir(dirPath, { withFileTypes: true });
88
+ for (const entry of entries) {
89
+ if (ignore.includes(entry.name))
90
+ continue;
91
+ const fullPath = join(dirPath, entry.name);
92
+ if (entry.isDirectory()) {
93
+ await scanDir(fullPath, keywords, extensions, results, maxDepth, currentDepth + 1);
94
+ }
95
+ else if (entry.isFile()) {
96
+ const ext = extname(entry.name);
97
+ const isBladePhp = entry.name.endsWith(".blade.php");
98
+ if (!extensions.includes(ext) && !isBladePhp)
99
+ continue;
100
+ // Only check filename match for speed
101
+ const nameLower = entry.name.toLowerCase();
102
+ const nameScore = scoreMatch(nameLower, keywords);
103
+ if (nameScore > 0) {
104
+ results.push({
105
+ match: {
106
+ source: "code",
107
+ file: fullPath,
108
+ line_preview: `File: ${entry.name} (name matches keyword)`,
109
+ relevance: nameScore >= 2 ? "high" : "medium",
110
+ },
111
+ score: nameScore + 1, // bonus for filename match
112
+ });
113
+ }
114
+ }
115
+ }
116
+ }
117
+ catch { }
118
+ }
119
+ export async function crossSearch(claudeHome, args) {
120
+ const { query, scope, include_code } = args;
121
+ const keywords = query
122
+ .toLowerCase()
123
+ .split(/[\s,;.]+/)
124
+ .filter((w) => w.length > 2);
125
+ // Load projects from registry
126
+ const projects = await loadRegistry(claudeHome);
127
+ const projectEntries = Object.entries(projects).filter(([slug, proj]) => {
128
+ if (scope)
129
+ return slug === scope || proj.group?.toLowerCase() === scope.toLowerCase();
130
+ return proj.status === "active";
131
+ });
132
+ const results = [];
133
+ let totalMatches = 0;
134
+ for (const [slug, proj] of projectEntries) {
135
+ const projectPath = proj.path?.replace(/\\/g, "/") || "";
136
+ const docsPath = join(claudeHome, "project-docs", slug);
137
+ const projectMatches = [];
138
+ // Search MISTAKES.md
139
+ const mistakesPath = join(docsPath, "MISTAKES.md");
140
+ const mistakeMatches = await searchInFile(mistakesPath, keywords, "mistakes");
141
+ projectMatches.push(...mistakeMatches);
142
+ // Search CURRENT_STATE.md
143
+ const statePath = join(docsPath, "CURRENT_STATE.md");
144
+ const stateMatches = await searchInFile(statePath, keywords, "current_state");
145
+ projectMatches.push(...stateMatches);
146
+ // Search AUDIT_LOG.md
147
+ const auditPath = join(docsPath, "AUDIT_LOG.md");
148
+ const auditMatches = await searchInFile(auditPath, keywords, "audit_log");
149
+ projectMatches.push(...auditMatches);
150
+ // Search code files (optional, slower)
151
+ if (include_code && projectPath && existsSync(projectPath)) {
152
+ const codeMatches = await searchCodeFiles(projectPath, keywords);
153
+ projectMatches.push(...codeMatches);
154
+ }
155
+ if (projectMatches.length > 0) {
156
+ results.push({
157
+ project: slug,
158
+ matches: projectMatches,
159
+ });
160
+ totalMatches += projectMatches.length;
161
+ }
162
+ }
163
+ // Sort projects by match count desc
164
+ results.sort((a, b) => b.matches.length - a.matches.length);
165
+ return {
166
+ query,
167
+ results,
168
+ total_matches: totalMatches,
169
+ projects_searched: projectEntries.length,
170
+ };
171
+ }
@@ -0,0 +1,7 @@
1
+ import { readFile } from "fs/promises";
2
+ import { join } from "path";
3
+ export async function getCurrentFocus(claudeHome) {
4
+ const path = join(claudeHome, "current-focus.json");
5
+ const raw = await readFile(path, "utf-8");
6
+ return JSON.parse(raw);
7
+ }
@@ -0,0 +1,7 @@
1
+ import { readFile } from "fs/promises";
2
+ import { join } from "path";
3
+ export async function getIdentity(claudeHome) {
4
+ const path = join(claudeHome, "agent-identity.json");
5
+ const raw = await readFile(path, "utf-8");
6
+ return JSON.parse(raw);
7
+ }
@@ -0,0 +1,35 @@
1
+ import { readFile } from "fs/promises";
2
+ import { join } from "path";
3
+ export async function getProjectStatus(claudeHome, slug) {
4
+ if (!slug)
5
+ throw new Error("slug is required");
6
+ const path = join(claudeHome, "project-docs", slug, "CURRENT_STATE.md");
7
+ const content = await readFile(path, "utf-8");
8
+ const activeSection = extractSection(content, "Task Aktif");
9
+ const completedSection = extractSection(content, "Terakhir Selesai");
10
+ const blockersSection = extractSection(content, "Blockers");
11
+ return {
12
+ slug,
13
+ file: path,
14
+ active_task: activeSection ? parseFirstTask(activeSection) : null,
15
+ recent_completed_preview: completedSection ? completedSection.slice(0, 1500) : null,
16
+ blockers: blockersSection ? blockersSection.slice(0, 500) : null,
17
+ };
18
+ }
19
+ function extractSection(content, heading) {
20
+ const regex = new RegExp(`##\\s+${heading}\\s*\\n([\\s\\S]*?)(?=\\n##\\s|$)`);
21
+ const match = content.match(regex);
22
+ return match ? match[1].trim() : null;
23
+ }
24
+ function parseFirstTask(section) {
25
+ const taskMatch = section.match(/###\s+(\S+)\s+(.*?)$/m);
26
+ if (!taskMatch) {
27
+ const trimmed = section.slice(0, 600);
28
+ return { raw_preview: trimmed, empty: /\(kosong\)/i.test(trimmed) };
29
+ }
30
+ return {
31
+ id: taskMatch[1],
32
+ title: taskMatch[2],
33
+ preview: section.slice(0, 800),
34
+ };
35
+ }