@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.
- package/bin/cli.js +490 -0
- package/dist/index.js +454 -0
- package/dist/memory/memory-manager.js +234 -0
- package/dist/server/web-server.js +574 -0
- package/dist/tools/aggregate-patterns.js +101 -0
- package/dist/tools/analyze-history.js +213 -0
- package/dist/tools/auto-dispatch.js +199 -0
- package/dist/tools/check-energy.js +49 -0
- package/dist/tools/cross-search.js +171 -0
- package/dist/tools/get-focus.js +7 -0
- package/dist/tools/get-identity.js +7 -0
- package/dist/tools/get-project-status.js +35 -0
- package/dist/tools/list-projects.js +21 -0
- package/dist/tools/list-recent-tasks.js +59 -0
- package/dist/tools/log-insight.js +43 -0
- package/dist/tools/qcc-create.js +82 -0
- package/dist/tools/qcc-status.js +164 -0
- package/dist/tools/qcc-update.js +188 -0
- package/dist/tools/smart-bootstrap.js +255 -0
- package/dist/tools/summarize-session.js +161 -0
- package/dist/tools/switch-focus.js +40 -0
- package/dist/tools/workflow-router.js +438 -0
- package/package.json +44 -0
- package/templates/index.ts.template +42 -0
- package/templates/shared/get-claude-md.ts +12 -0
- package/templates/shared/get-current-state.ts +21 -0
- package/templates/shared/get-mistakes.ts +18 -0
- package/templates/shared/log-task.ts +27 -0
- package/templates/shared/predict-impact.ts +67 -0
- package/templates/shared/record-mistake.ts +40 -0
- package/templates/shared/update-state.ts +83 -0
- package/templates/stacks/express/config.json +9 -0
- package/templates/stacks/express/list-routes.ts +56 -0
- package/templates/stacks/express/symbol-index.ts +70 -0
- package/templates/stacks/laravel/config.json +9 -0
- package/templates/stacks/laravel/list-routes.ts +19 -0
- package/templates/stacks/laravel/symbol-index.ts +64 -0
- package/templates/stacks/nextjs/config.json +9 -0
- package/templates/stacks/nextjs/list-routes.ts +67 -0
- package/templates/stacks/nextjs/symbol-index.ts +78 -0
- package/templates/stacks/react/config.json +10 -0
- package/templates/stacks/react/list-routes.ts +44 -0
- 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,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
|
+
}
|