@monoes/monomindcli 1.12.0 → 1.14.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/.claude/agents/generated/churn-analyst.md +53 -0
- package/.claude/agents/generated/code-reviewer.md +55 -0
- package/.claude/agents/generated/code-validator.md +57 -0
- package/.claude/agents/generated/complexity-scanner.md +56 -0
- package/.claude/agents/generated/devbot-orchestrator.md +58 -0
- package/.claude/agents/generated/devbot-planner.md +63 -0
- package/.claude/agents/generated/impact-assessor.md +54 -0
- package/.claude/commands/mastermind/master.md +88 -24
- package/.claude/helpers/control-start.cjs +60 -1
- package/.claude/helpers/event-logger.cjs +43 -2
- package/.claude/helpers/handlers/capture-handler.cjs +336 -0
- package/.claude/helpers/handlers/route-handler.cjs +20 -13
- package/.claude/helpers/handlers/session-restore-handler.cjs +14 -8
- package/.claude/helpers/hook-handler.cjs +57 -1
- package/.claude/helpers/intelligence.cjs +129 -57
- package/.claude/helpers/memory-palace.cjs +461 -0
- package/.claude/helpers/memory.cjs +134 -15
- package/.claude/helpers/metrics-db.mjs +87 -0
- package/.claude/helpers/router.cjs +296 -41
- package/.claude/helpers/session.cjs +107 -32
- package/.claude/helpers/statusline.cjs +138 -2
- package/.claude/helpers/toggle-statusline.cjs +73 -0
- package/.claude/helpers/token-tracker.cjs +934 -0
- package/.claude/helpers/utils/monograph.cjs +39 -4
- package/.claude/helpers/utils/telemetry.cjs +3 -3
- package/.claude/skills/mastermind/createorg.md +227 -16
- package/.claude/skills/mastermind/idea.md +15 -3
- package/.claude/skills/mastermind/runorg.md +2 -1
- package/dist/src/commands/doctor.d.ts.map +1 -1
- package/dist/src/commands/doctor.js +96 -4
- package/dist/src/commands/doctor.js.map +1 -1
- package/dist/src/commands/index.js +2 -0
- package/dist/src/commands/org.d.ts +4 -0
- package/dist/src/commands/org.d.ts.map +1 -0
- package/dist/src/commands/org.js +93 -0
- package/dist/src/commands/org.js.map +1 -0
- package/dist/src/mcp-tools/memory-tools.js +6 -6
- package/dist/src/mcp-tools/memory-tools.js.map +1 -1
- package/dist/src/mcp-tools/monograph-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/monograph-tools.js +329 -37
- package/dist/src/mcp-tools/monograph-tools.js.map +1 -1
- package/dist/src/mcp-tools/session-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/session-tools.js +9 -10
- package/dist/src/mcp-tools/session-tools.js.map +1 -1
- package/dist/src/mcp-tools/task-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/task-tools.js +7 -8
- package/dist/src/mcp-tools/task-tools.js.map +1 -1
- package/dist/src/mcp-tools/types.d.ts +1 -0
- package/dist/src/mcp-tools/types.d.ts.map +1 -1
- package/dist/src/mcp-tools/types.js +49 -0
- package/dist/src/mcp-tools/types.js.map +1 -1
- package/dist/src/services/worker-daemon.d.ts.map +1 -1
- package/dist/src/services/worker-daemon.js +295 -5
- package/dist/src/services/worker-daemon.js.map +1 -1
- package/dist/src/transfer/serialization/cfp.js +1 -1
- package/dist/src/transfer/serialization/cfp.js.map +1 -1
- package/dist/src/ui/dashboard.html +2235 -178
- package/dist/src/ui/orgs.html +1 -0
- package/dist/src/ui/server.mjs +532 -133
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
package/dist/src/ui/server.mjs
CHANGED
|
@@ -185,6 +185,8 @@ const sseClients = new Set();
|
|
|
185
185
|
const mmSseClients = new Set();
|
|
186
186
|
// Active org run tracking: org -> runId (enables event routing for orgs without runId in payload)
|
|
187
187
|
const activeOrgRuns = new Map();
|
|
188
|
+
// Active session tracking: org -> {sessionId, ts} (enables linking agent events to sessions)
|
|
189
|
+
const activeSessionsByOrg = new Map();
|
|
188
190
|
|
|
189
191
|
// Returns the shared git directory parent so run files survive branch switches and
|
|
190
192
|
// are shared across all worktrees. In a worktree, .git is a FILE pointing to the
|
|
@@ -363,6 +365,207 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
363
365
|
return out;
|
|
364
366
|
}
|
|
365
367
|
|
|
368
|
+
// ── handleMastermindEvent ─────────────────────────────────────────────────
|
|
369
|
+
// Extracted from the request dispatcher to reduce cyclomatic complexity.
|
|
370
|
+
// Handles POST /api/mastermind/event: parses body, enriches with runId/session,
|
|
371
|
+
// persists to JSONL files, broadcasts to SSE clients, returns {ok:true}.
|
|
372
|
+
async function handleMastermindEvent(req, res) {
|
|
373
|
+
let body = '';
|
|
374
|
+
for await (const chunk of req) { body += chunk; if (body.length > 2097152) { req.destroy(); break; } }
|
|
375
|
+
let event = {};
|
|
376
|
+
try { event = JSON.parse(body); } catch (_) {}
|
|
377
|
+
event.ts = event.ts || Date.now();
|
|
378
|
+
// Use project path from event if provided (multi-project support).
|
|
379
|
+
// Security: path.isAbsolute() alone is insufficient — an attacker can
|
|
380
|
+
// supply event.project="/etc" and cause writes to system directories.
|
|
381
|
+
// Only accept paths that resolve to an existing directory AND are not
|
|
382
|
+
// the filesystem root (/), AND are not obviously system paths.
|
|
383
|
+
// Cap to 4096 chars to prevent OOM from huge path strings.
|
|
384
|
+
const _rawProject = event.project;
|
|
385
|
+
let eventProject = null;
|
|
386
|
+
if (typeof _rawProject === 'string' && _rawProject.length > 0 && _rawProject.length <= 4096
|
|
387
|
+
&& path.isAbsolute(_rawProject)) {
|
|
388
|
+
// Reject filesystem root and common system directories
|
|
389
|
+
const _norm = path.resolve(_rawProject);
|
|
390
|
+
const _systemPaths = ['/', '/etc', '/usr', '/bin', '/sbin', '/lib', '/lib64', '/boot', '/dev', '/sys', '/proc', '/tmp'];
|
|
391
|
+
if (!_systemPaths.includes(_norm) && !_systemPaths.some(p => _norm.startsWith(p + '/'))) {
|
|
392
|
+
eventProject = _norm;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
const root = eventProject || projectDir || process.cwd();
|
|
396
|
+
const dataDir = path.join(root, 'data');
|
|
397
|
+
try { fs.mkdirSync(dataDir, { recursive: true }); } catch (_) {}
|
|
398
|
+
// Track known project dirs for aggregated session listing
|
|
399
|
+
if (eventProject) {
|
|
400
|
+
const knownFile = path.join(projectDir || process.cwd(), 'data', 'known-projects.json');
|
|
401
|
+
try {
|
|
402
|
+
let known = [];
|
|
403
|
+
try { known = JSON.parse(fs.readFileSync(knownFile, 'utf8')); } catch (_) {}
|
|
404
|
+
if (!known.includes(eventProject)) { known.push(eventProject); fs.writeFileSync(knownFile, JSON.stringify(known)); }
|
|
405
|
+
} catch (_) {}
|
|
406
|
+
}
|
|
407
|
+
// Track active runs and enrich event with runId BEFORE persisting so the JSONL replay
|
|
408
|
+
// on SSE reconnect contains the same enriched event that live clients received.
|
|
409
|
+
// Previously this was done AFTER the appendFileSync, causing org:comms events stored in
|
|
410
|
+
// mastermind-events.jsonl to lack runId — _odtHandleLiveEvent dropped them on reconnect.
|
|
411
|
+
if (event.org) {
|
|
412
|
+
const _orgKey = String(event.org).trim();
|
|
413
|
+
// Any event with both org+runId updates the active run map (run:start written directly to file so org:start is first via curl)
|
|
414
|
+
if (event.runId) activeOrgRuns.set(_orgKey, String(event.runId).trim());
|
|
415
|
+
else if (activeOrgRuns.has(_orgKey)) event.runId = activeOrgRuns.get(_orgKey);
|
|
416
|
+
if (event.type === 'run:complete' || event.type === 'org:complete') activeOrgRuns.delete(_orgKey);
|
|
417
|
+
// Persist active-run.json so capture-handler.cjs can find the current org/runId without HTTP calls
|
|
418
|
+
try {
|
|
419
|
+
const _captureDir = path.join(root, '.monomind', 'capture');
|
|
420
|
+
const _activeRunFile = path.join(_captureDir, 'active-run.json');
|
|
421
|
+
if (event.type === 'run:start' && event.org && event.runId) {
|
|
422
|
+
fs.mkdirSync(_captureDir, { recursive: true });
|
|
423
|
+
fs.writeFileSync(_activeRunFile, JSON.stringify({ org: String(event.org).trim(), runId: String(event.runId).trim(), ts: Date.now() }));
|
|
424
|
+
} else if ((event.type === 'run:complete' || event.type === 'org:complete') && fs.existsSync(_activeRunFile)) {
|
|
425
|
+
fs.unlinkSync(_activeRunFile);
|
|
426
|
+
}
|
|
427
|
+
} catch(_e) {}
|
|
428
|
+
}
|
|
429
|
+
try { fs.appendFileSync(path.join(dataDir, 'mastermind-events.jsonl'), JSON.stringify(event) + '\n'); } catch (_) {}
|
|
430
|
+
// Persist to git-safe run file (survives branch switches + shared across worktrees)
|
|
431
|
+
if (event.org && event.runId) {
|
|
432
|
+
try {
|
|
433
|
+
const _orn = String(event.org).trim();
|
|
434
|
+
const _rid = String(event.runId).trim();
|
|
435
|
+
if (_orn.length > 0 && _orn.length <= 64 && /^[a-z0-9][a-z0-9_-]*$/i.test(_orn)
|
|
436
|
+
&& _rid.length > 0 && _rid.length <= 80 && /^[a-z0-9][a-z0-9_-]*$/i.test(_rid)) {
|
|
437
|
+
const _monoDir = _getGitMonomindDir(root) || path.join(root, '.monomind');
|
|
438
|
+
const _runDir = path.join(_monoDir, 'orgs', _orn, 'runs');
|
|
439
|
+
fs.mkdirSync(_runDir, { recursive: true });
|
|
440
|
+
fs.appendFileSync(path.join(_runDir, `${_rid}.jsonl`), JSON.stringify(event) + '\n');
|
|
441
|
+
// agent:usage — persist per-role token/cost data to state.json (accumulated across runs)
|
|
442
|
+
if (event.type === 'agent:usage' && event.role) {
|
|
443
|
+
try {
|
|
444
|
+
const _arole = String(event.role).trim();
|
|
445
|
+
if (_arole.length > 0 && _arole.length <= 64 && /^[a-z0-9][a-z0-9_-]*$/i.test(_arole)) {
|
|
446
|
+
const _stateFile = path.join(root, '.monomind', 'orgs', `${_orn}-state.json`);
|
|
447
|
+
let _st = {};
|
|
448
|
+
try { _st = JSON.parse(fs.readFileSync(_stateFile, 'utf8')); } catch(_e) {}
|
|
449
|
+
if (!_st.agents) _st.agents = {};
|
|
450
|
+
const _ex = _st.agents[_arole] || {};
|
|
451
|
+
_st.agents[_arole] = {
|
|
452
|
+
..._ex,
|
|
453
|
+
tokens_in: (_ex.tokens_in || 0) + (Number(event.tokens_in) || 0),
|
|
454
|
+
tokens_out: (_ex.tokens_out || 0) + (Number(event.tokens_out) || 0),
|
|
455
|
+
total_cost_usd: (_ex.total_cost_usd || 0) + (Number(event.cost_usd) || 0),
|
|
456
|
+
lastUpdated: event.ts,
|
|
457
|
+
};
|
|
458
|
+
fs.writeFileSync(_stateFile, JSON.stringify(_st, null, 2));
|
|
459
|
+
}
|
|
460
|
+
} catch(_e) {}
|
|
461
|
+
}
|
|
462
|
+
// Solution 3: dedicated conversation log — org:comms only, for easy replay
|
|
463
|
+
if (event.type === 'org:comms') {
|
|
464
|
+
const _conv = { ts: event.ts, run_id: _rid, from: event.from, to: event.to, msg: event.msg };
|
|
465
|
+
fs.appendFileSync(path.join(_runDir, `${_rid}.convs.jsonl`), JSON.stringify(_conv) + '\n');
|
|
466
|
+
// Also write to org-level threads.jsonl so the dashboard Threads tab shows agent conversations
|
|
467
|
+
const _orgThreadsFile = path.join(root, '.monomind', 'orgs', `${_orn}-threads.jsonl`);
|
|
468
|
+
const _thread = { type: 'message', id: `${_rid}-${event.ts}`, run_id: _rid, ts: event.ts, from: event.from, to: event.to, msg: event.msg, subject: `Run ${_rid}` };
|
|
469
|
+
try { fs.appendFileSync(_orgThreadsFile, JSON.stringify(_thread) + '\n'); } catch(_) {}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
} catch (_) {}
|
|
473
|
+
}
|
|
474
|
+
// ── Active session tracking: link org:comms / agent:usage events to current session ──
|
|
475
|
+
// This must run BEFORE session persistence so events without session get enriched.
|
|
476
|
+
try {
|
|
477
|
+
const _evOrg = event.org ? String(event.org).trim() : null;
|
|
478
|
+
if (event.type === 'session:start' && event.session && _evOrg) {
|
|
479
|
+
activeSessionsByOrg.set(_evOrg, { sessionId: String(event.session), ts: event.ts || Date.now() });
|
|
480
|
+
// Write active-session.json so capture-handler.cjs can read it without HTTP
|
|
481
|
+
try {
|
|
482
|
+
const _captureDir = path.join(root, '.monomind', 'capture');
|
|
483
|
+
fs.mkdirSync(_captureDir, { recursive: true });
|
|
484
|
+
fs.writeFileSync(path.join(_captureDir, 'active-session.json'),
|
|
485
|
+
JSON.stringify({ org: _evOrg, sessionId: String(event.session), ts: Date.now() }));
|
|
486
|
+
} catch(_) {}
|
|
487
|
+
} else if (event.type === 'session:complete' && _evOrg) {
|
|
488
|
+
activeSessionsByOrg.delete(_evOrg);
|
|
489
|
+
try { fs.unlinkSync(path.join(root, '.monomind', 'capture', 'active-session.json')); } catch(_) {}
|
|
490
|
+
}
|
|
491
|
+
// Enrich events that have org but no session (agent:usage, org:comms, agent:spawn, intercom)
|
|
492
|
+
if (_evOrg && !event.session && activeSessionsByOrg.has(_evOrg)) {
|
|
493
|
+
event.session = activeSessionsByOrg.get(_evOrg).sessionId;
|
|
494
|
+
}
|
|
495
|
+
} catch(_) {}
|
|
496
|
+
// ── Per-session JSONL persistence (append-only, O(1) per event) ──────────
|
|
497
|
+
// Replaces the old monolithic mastermind-sessions.json (O(N) read+write per event).
|
|
498
|
+
// Format: data/sessions/<sessionId>.jsonl + data/sessions/_index.json
|
|
499
|
+
try {
|
|
500
|
+
const _sid = String(event.session || '').trim();
|
|
501
|
+
if (_sid.length > 0 && _sid.length <= 128 && /^[a-zA-Z0-9_.-]+$/.test(_sid)) {
|
|
502
|
+
const sessDir = path.join(dataDir, 'sessions');
|
|
503
|
+
fs.mkdirSync(sessDir, { recursive: true });
|
|
504
|
+
// Append event to per-session JSONL (O(1), no read)
|
|
505
|
+
fs.appendFileSync(path.join(sessDir, `${_sid}.jsonl`), JSON.stringify(event) + '\n');
|
|
506
|
+
// Update lightweight index (id, ts, prompt, status, org, startedAt, endedAt, domains only)
|
|
507
|
+
const indexFile = path.join(sessDir, '_index.json');
|
|
508
|
+
let _idx = [];
|
|
509
|
+
try { _idx = JSON.parse(fs.readFileSync(indexFile, 'utf8')); } catch(_) {}
|
|
510
|
+
const _entry = _idx.find(e => e.id === _sid);
|
|
511
|
+
if (event.type === 'session:start') {
|
|
512
|
+
if (!_entry) {
|
|
513
|
+
_idx.unshift({ id: _sid, ts: event.ts, prompt: event.prompt || '', status: 'running',
|
|
514
|
+
org: event.org || '', startedAt: event.ts, domains: [] });
|
|
515
|
+
if (_idx.length > 2000) _idx = _idx.slice(0, 2000);
|
|
516
|
+
}
|
|
517
|
+
} else if (_entry) {
|
|
518
|
+
if (event.type === 'session:complete') { _entry.status = event.status || 'complete'; _entry.endedAt = event.ts; }
|
|
519
|
+
if (event.type === 'domain:dispatch' && event.domain) {
|
|
520
|
+
_entry.domains = _entry.domains || [];
|
|
521
|
+
if (!_entry.domains.includes(event.domain)) _entry.domains.push(event.domain);
|
|
522
|
+
}
|
|
523
|
+
if (event.type === 'agent:usage' || event.type === 'agent:spawn' || event.type === 'agent:complete') {
|
|
524
|
+
_entry.hasAgents = true;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
fs.writeFileSync(indexFile, JSON.stringify(_idx));
|
|
528
|
+
}
|
|
529
|
+
} catch (_) {}
|
|
530
|
+
// ── Legacy mastermind-sessions.json (kept for backwards compat, read by old clients) ──
|
|
531
|
+
try {
|
|
532
|
+
const sessFile = path.join(dataDir, 'mastermind-sessions.json');
|
|
533
|
+
let sessions = [];
|
|
534
|
+
try { sessions = JSON.parse(fs.readFileSync(sessFile, 'utf8')); } catch (_) {}
|
|
535
|
+
if (event.type === 'session:start' && event.session) {
|
|
536
|
+
if (!sessions.find(s => s.id === event.session)) {
|
|
537
|
+
sessions.unshift({ id: event.session, ts: event.ts, prompt: event.prompt || '',
|
|
538
|
+
status: 'running', org: event.org || '', domains: [], startedAt: event.ts });
|
|
539
|
+
}
|
|
540
|
+
} else if (event.session) {
|
|
541
|
+
const s = sessions.find(s => s.id === event.session);
|
|
542
|
+
if (s) {
|
|
543
|
+
if (event.type === 'session:complete') { s.status = event.status || 'complete'; s.endedAt = event.ts; }
|
|
544
|
+
if (event.type === 'domain:dispatch' && event.domain && !s.domains?.includes(event.domain))
|
|
545
|
+
(s.domains = s.domains || []).push(event.domain);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
fs.writeFileSync(sessFile, JSON.stringify(sessions.slice(0, 500)));
|
|
549
|
+
} catch (_) {}
|
|
550
|
+
// For org:stop events, write a stop marker the boss agent can detect
|
|
551
|
+
if (event.type === 'org:stop' && event.org) {
|
|
552
|
+
try {
|
|
553
|
+
const orgName = String(event.org).trim();
|
|
554
|
+
// Validate before any filesystem use — reject rather than strip
|
|
555
|
+
if (orgName.length > 0 && orgName.length <= 64 && /^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
|
|
556
|
+
const stopDir = path.join(root, '.monomind', 'orgs', '.stops');
|
|
557
|
+
fs.mkdirSync(stopDir, { recursive: true });
|
|
558
|
+
fs.writeFileSync(path.join(stopDir, `${orgName}.stop`), String(Date.now()));
|
|
559
|
+
}
|
|
560
|
+
} catch (_) {}
|
|
561
|
+
}
|
|
562
|
+
// Broadcast to all mastermind SSE clients
|
|
563
|
+
const msg = `data: ${JSON.stringify(event)}\n\n`;
|
|
564
|
+
for (const c of mmSseClients) { try { c.write(msg); } catch (_) { mmSseClients.delete(c); } }
|
|
565
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
566
|
+
res.end('{"ok":true}');
|
|
567
|
+
}
|
|
568
|
+
|
|
366
569
|
const server = http.createServer(async (req, res) => {
|
|
367
570
|
const url = req.url.split('?')[0];
|
|
368
571
|
|
|
@@ -409,12 +612,22 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
409
612
|
const cwd = projectDir || process.cwd();
|
|
410
613
|
const name = gitExec('git config user.name', { cwd, encoding: 'utf8' }).trim();
|
|
411
614
|
const email = gitExec('git config user.email', { cwd, encoding: 'utf8' }).trim();
|
|
615
|
+
let remoteUrl = '';
|
|
616
|
+
try { remoteUrl = gitExec('git remote get-url origin', { cwd, encoding: 'utf8' }).trim(); } catch {}
|
|
617
|
+
// Normalise SSH remote to HTTPS URL for browser linking
|
|
618
|
+
if (remoteUrl.startsWith('git@')) {
|
|
619
|
+
remoteUrl = remoteUrl.replace(/^git@([^:]+):/, 'https://$1/').replace(/\.git$/, '');
|
|
620
|
+
} else if (remoteUrl.endsWith('.git')) {
|
|
621
|
+
remoteUrl = remoteUrl.slice(0, -4);
|
|
622
|
+
}
|
|
623
|
+
let branch = '';
|
|
624
|
+
try { branch = gitExec('git rev-parse --abbrev-ref HEAD', { cwd, encoding: 'utf8' }).trim(); } catch {}
|
|
412
625
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
413
|
-
res.end(JSON.stringify({ name, email, cwd }));
|
|
626
|
+
res.end(JSON.stringify({ name, email, cwd, remoteUrl, branch }));
|
|
414
627
|
} catch (_) {
|
|
415
628
|
const cwd2 = projectDir || process.cwd();
|
|
416
629
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
417
|
-
res.end(JSON.stringify({ name: '', email: '', cwd: cwd2 }));
|
|
630
|
+
res.end(JSON.stringify({ name: '', email: '', cwd: cwd2, remoteUrl: '', branch: '' }));
|
|
418
631
|
}
|
|
419
632
|
return;
|
|
420
633
|
}
|
|
@@ -498,7 +711,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
498
711
|
.map(f => { try { return { f, mtime: fs.statSync(path.join(projectClaudeDir, f)).mtimeMs }; } catch { return null; } })
|
|
499
712
|
.filter(Boolean)
|
|
500
713
|
.sort((a, b) => b.mtime - a.mtime)
|
|
501
|
-
.slice(0,
|
|
714
|
+
.slice(0, 50);
|
|
502
715
|
} catch {}
|
|
503
716
|
|
|
504
717
|
const sessions = [];
|
|
@@ -631,6 +844,69 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
631
844
|
return;
|
|
632
845
|
}
|
|
633
846
|
|
|
847
|
+
// ------------------------------------------------------- GET /api/recent-events
|
|
848
|
+
if (req.method === 'GET' && url === '/api/recent-events') {
|
|
849
|
+
try {
|
|
850
|
+
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
851
|
+
const dir = qs.get('dir') || projectDir || process.cwd();
|
|
852
|
+
const limit = Math.min(parseInt(qs.get('limit') || '50', 10), 200);
|
|
853
|
+
const d = path.resolve(dir || process.cwd());
|
|
854
|
+
const slug = d.replace(/\//g, '-');
|
|
855
|
+
const projectClaudeDir = path.join(os.homedir(), '.claude', 'projects', slug);
|
|
856
|
+
let sessionFiles = [];
|
|
857
|
+
try {
|
|
858
|
+
sessionFiles = fs.readdirSync(projectClaudeDir)
|
|
859
|
+
.filter(f => f.endsWith('.jsonl'))
|
|
860
|
+
.map(f => { try { return { f, mtime: fs.statSync(path.join(projectClaudeDir, f)).mtimeMs }; } catch { return null; } })
|
|
861
|
+
.filter(Boolean)
|
|
862
|
+
.sort((a, b) => b.mtime - a.mtime)
|
|
863
|
+
.slice(0, 5); // check last 5 sessions
|
|
864
|
+
} catch {}
|
|
865
|
+
|
|
866
|
+
const events = [];
|
|
867
|
+
const HOOK_RE = /^<(local-command-|command-name>|command-message>)/;
|
|
868
|
+
for (const { f } of sessionFiles) {
|
|
869
|
+
const fp = path.join(projectClaudeDir, f);
|
|
870
|
+
const sessId = f.replace('.jsonl', '');
|
|
871
|
+
try {
|
|
872
|
+
const lines = fs.readFileSync(fp, 'utf8').split('\n').filter(Boolean).slice(-200);
|
|
873
|
+
for (const line of lines) {
|
|
874
|
+
let e; try { e = JSON.parse(line); } catch { continue; }
|
|
875
|
+
if (e.type === 'assistant') {
|
|
876
|
+
const content = e.message?.content || [];
|
|
877
|
+
for (const block of content) {
|
|
878
|
+
if (block?.type === 'tool_use') {
|
|
879
|
+
events.push({ kind: 'tool', ts: e.timestamp, tool: block.name, session: sessId });
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
} else if (e.type === 'user') {
|
|
883
|
+
const content = e.message?.content || [];
|
|
884
|
+
for (const block of content) {
|
|
885
|
+
if (block?.type === 'text' && block.text?.trim() && !HOOK_RE.test(block.text.trim())) {
|
|
886
|
+
events.push({ kind: 'user', ts: e.timestamp, text: block.text.slice(0, 120), session: sessId });
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
} catch {}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// sort by ts desc, take limit
|
|
895
|
+
events.sort((a, b) => {
|
|
896
|
+
const ta = a.ts ? new Date(a.ts).getTime() : 0;
|
|
897
|
+
const tb = b.ts ? new Date(b.ts).getTime() : 0;
|
|
898
|
+
return tb - ta;
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
902
|
+
res.end(JSON.stringify({ events: events.slice(0, limit) }));
|
|
903
|
+
} catch (err) {
|
|
904
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
905
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
906
|
+
}
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
|
|
634
910
|
// ------------------------------------------------------- GET /api/tool-errors
|
|
635
911
|
if (req.method === 'GET' && url === '/api/tool-errors') {
|
|
636
912
|
try {
|
|
@@ -3144,6 +3420,9 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
3144
3420
|
const stopTs = lastStop ? (JSON.parse(lastStop).ts || 0) : 0;
|
|
3145
3421
|
running = startTs > stopTs;
|
|
3146
3422
|
}
|
|
3423
|
+
// Also check in-memory activeOrgRuns so the list reflects LIVE immediately after launch
|
|
3424
|
+
const _lOrgName = cfg.name || '';
|
|
3425
|
+
if (!running && _lOrgName && activeOrgRuns.has(_lOrgName)) running = true;
|
|
3147
3426
|
orgs.push({ name: cfg.name, goal: cfg.goal, roles: Array.isArray(cfg.roles) ? cfg.roles : [], topology: cfg.topology, created_at: cfg.created_at, running, status: cfg.status, loop: cfg.loop ? { poll_interval_minutes: cfg.loop.poll_interval_minutes, last_run: cfg.loop.last_run, next_run: cfg.loop.next_run } : undefined });
|
|
3148
3427
|
} catch(_) {}
|
|
3149
3428
|
}
|
|
@@ -3240,9 +3519,9 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
3240
3519
|
const routinesData = readJsonSafe(path.join(orgsDir, `${orgName}-routines.json`)) || { routines: [] };
|
|
3241
3520
|
const approvalsData = readJsonSafe(path.join(orgsDir, `${orgName}-approvals.json`)) || { approvals: [] };
|
|
3242
3521
|
|
|
3243
|
-
// Check running status
|
|
3522
|
+
// Check running status: stop file absence AND (in-memory activeOrgRuns OR state-file agents)
|
|
3244
3523
|
const stopFile = path.join(orgsDir, '.stops', `${orgName}.stop`);
|
|
3245
|
-
const running = !fs.existsSync(stopFile) && Object.values(state.agents || {}).some(a => a.status === 'running');
|
|
3524
|
+
const running = !fs.existsSync(stopFile) && (activeOrgRuns.has(orgName) || Object.values(state.agents || {}).some(a => a.status === 'running'));
|
|
3246
3525
|
|
|
3247
3526
|
// Read real tasks from the task store and group by status column
|
|
3248
3527
|
const taskStoreData = readJsonSafe(path.join(d, '.monomind', 'tasks', 'store.json'));
|
|
@@ -4001,13 +4280,45 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
4001
4280
|
total_cost_usd: s.total_cost_usd || 0,
|
|
4002
4281
|
}));
|
|
4003
4282
|
} catch(_) {}
|
|
4004
|
-
//
|
|
4005
|
-
|
|
4283
|
+
// Scan org run jsonl files for agent:usage events (fallback when state.json has no token data)
|
|
4284
|
+
const _hasTokenData = agents.some(a => a.tokens_in > 0 || a.tokens_out > 0 || a.total_cost_usd > 0);
|
|
4285
|
+
if (!_hasTokenData) {
|
|
4006
4286
|
try {
|
|
4007
|
-
const
|
|
4008
|
-
|
|
4287
|
+
const _runsDir = path.join(base, orgName, 'runs');
|
|
4288
|
+
if (fs.existsSync(_runsDir)) {
|
|
4289
|
+
const _usageByRole = {};
|
|
4290
|
+
for (const f of fs.readdirSync(_runsDir)) {
|
|
4291
|
+
if (!f.endsWith('.jsonl')) continue;
|
|
4292
|
+
const lines = fs.readFileSync(path.join(_runsDir, f), 'utf8').split('\n').filter(Boolean);
|
|
4293
|
+
for (const l of lines) {
|
|
4294
|
+
try {
|
|
4295
|
+
const ev = JSON.parse(l);
|
|
4296
|
+
if (ev.type === 'agent:usage' && ev.role) {
|
|
4297
|
+
const role = String(ev.role).trim();
|
|
4298
|
+
if (!_usageByRole[role]) _usageByRole[role] = { tokens_in: 0, tokens_out: 0, total_cost_usd: 0 };
|
|
4299
|
+
_usageByRole[role].tokens_in += Number(ev.tokens_in) || 0;
|
|
4300
|
+
_usageByRole[role].tokens_out += Number(ev.tokens_out) || 0;
|
|
4301
|
+
_usageByRole[role].total_cost_usd += Number(ev.cost_usd) || 0;
|
|
4302
|
+
}
|
|
4303
|
+
} catch(_) {}
|
|
4304
|
+
}
|
|
4305
|
+
}
|
|
4306
|
+
if (Object.keys(_usageByRole).length > 0) {
|
|
4307
|
+
// Merge usage into agents list; preserve role titles
|
|
4308
|
+
agents = agents.map(a => {
|
|
4309
|
+
const u = _usageByRole[a.id] || {};
|
|
4310
|
+
return { ...a, tokens_in: u.tokens_in || 0, tokens_out: u.tokens_out || 0, total_cost_usd: u.total_cost_usd || 0 };
|
|
4311
|
+
});
|
|
4312
|
+
// Add any roles that appeared in events but aren't in config
|
|
4313
|
+
for (const [role, u] of Object.entries(_usageByRole)) {
|
|
4314
|
+
if (!agents.find(a => a.id === role)) agents.push({ id: role, title: role, ...u });
|
|
4315
|
+
}
|
|
4316
|
+
}
|
|
4317
|
+
}
|
|
4009
4318
|
} catch(_) {}
|
|
4010
4319
|
}
|
|
4320
|
+
// Do NOT fall back to zero-value role stubs — empty agents array is the honest signal
|
|
4321
|
+
// that no usage has been tracked yet; the UI shows "No cost data" rather than $0.0000 rows.
|
|
4011
4322
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
4012
4323
|
res.end(JSON.stringify({ ...budgetData, agents }));
|
|
4013
4324
|
} catch(_) { res.writeHead(500); res.end('{"org_budget":{},"agent_budgets":{},"agents":[]}'); }
|
|
@@ -4026,11 +4337,20 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
4026
4337
|
try {
|
|
4027
4338
|
const lines = fs.readFileSync(threadsFile, 'utf8').split('\n').filter(l => l.trim());
|
|
4028
4339
|
threads = lines.map(l => { try { return JSON.parse(l); } catch(_) { return null; } }).filter(Boolean);
|
|
4340
|
+
// Group 'message' entries (from org:comms) by run_id into synthetic thread objects
|
|
4341
|
+
const msgsByRun = {};
|
|
4342
|
+
threads.filter(t => t.type === 'message').forEach(m => {
|
|
4343
|
+
const rid = m.run_id || 'unknown';
|
|
4344
|
+
if (!msgsByRun[rid]) msgsByRun[rid] = { id: `thread-${rid}`, type: 'thread', subject: `Run ${rid}`, run_id: rid, createdAt: m.ts, messages: [] };
|
|
4345
|
+
msgsByRun[rid].messages.push({ from: m.from, to: m.to, msg: m.msg, ts: m.ts });
|
|
4346
|
+
});
|
|
4347
|
+
const syntheticThreads = Object.values(msgsByRun).map(t => ({ ...t, messageCount: t.messages.length, author: t.messages[0]?.from || null }));
|
|
4029
4348
|
threads = threads.filter(t => t.type === 'thread' || !t.type).map(t => ({
|
|
4030
4349
|
...t,
|
|
4031
4350
|
author: t.author || t.authorName || t.createdBy || t.authorId || null,
|
|
4032
4351
|
messageCount: t.messageCount != null ? t.messageCount : (Array.isArray(t.messages) ? t.messages.length : (typeof t.messages === 'number' ? t.messages : null)),
|
|
4033
4352
|
}));
|
|
4353
|
+
threads = [...threads, ...syntheticThreads];
|
|
4034
4354
|
} catch(_) {}
|
|
4035
4355
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
4036
4356
|
res.end(JSON.stringify({ threads }));
|
|
@@ -4082,15 +4402,48 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
4082
4402
|
return;
|
|
4083
4403
|
}
|
|
4084
4404
|
|
|
4085
|
-
// GET /api/org/:name/routines — read org routines
|
|
4405
|
+
// GET /api/org/:name/routines — read org routines (falls back to synthesizing from org config's loop object)
|
|
4086
4406
|
if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/routines$/i)) {
|
|
4087
4407
|
try {
|
|
4088
4408
|
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
4089
4409
|
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
|
|
4090
4410
|
const _routinesQs = new URL(req.url, 'http://localhost').searchParams;
|
|
4091
|
-
const
|
|
4411
|
+
const _routinesBase = path.resolve(_routinesQs.get('dir') || projectDir || process.cwd());
|
|
4412
|
+
const routinesFile = path.join(_routinesBase, '.monomind', 'orgs', `${orgName}-routines.json`);
|
|
4092
4413
|
let data = { routines: [] };
|
|
4093
4414
|
try { data = JSON.parse(fs.readFileSync(routinesFile, 'utf8')); } catch(_) {}
|
|
4415
|
+
// Synthesize routines from org config's loop/schedule settings when no explicit routines are defined
|
|
4416
|
+
if (!data.routines || !data.routines.length) {
|
|
4417
|
+
try {
|
|
4418
|
+
const orgCfg = JSON.parse(fs.readFileSync(path.join(_routinesBase, '.monomind', 'orgs', `${orgName}.json`), 'utf8'));
|
|
4419
|
+
const loop = orgCfg.loop;
|
|
4420
|
+
if (loop && (loop.poll_interval_minutes || loop.interval_minutes)) {
|
|
4421
|
+
const intervalMin = loop.poll_interval_minutes || loop.interval_minutes;
|
|
4422
|
+
data.routines = [{
|
|
4423
|
+
name: `${orgName}-cycle`,
|
|
4424
|
+
description: orgCfg.goal ? orgCfg.goal.slice(0, 120) : 'Org iteration cycle',
|
|
4425
|
+
schedule: `every ${intervalMin}m`,
|
|
4426
|
+
cron: null,
|
|
4427
|
+
enabled: orgCfg.status === 'active',
|
|
4428
|
+
status: orgCfg.status || 'stopped',
|
|
4429
|
+
prompt_file: loop.run_prompt_file || null,
|
|
4430
|
+
source: 'loop-config',
|
|
4431
|
+
lastRun: null,
|
|
4432
|
+
}];
|
|
4433
|
+
} else if (orgCfg.schedule) {
|
|
4434
|
+
data.routines = [{
|
|
4435
|
+
name: `${orgName}-schedule`,
|
|
4436
|
+
description: orgCfg.goal ? orgCfg.goal.slice(0, 120) : 'Org scheduled run',
|
|
4437
|
+
schedule: String(orgCfg.schedule),
|
|
4438
|
+
cron: null,
|
|
4439
|
+
enabled: orgCfg.status === 'active',
|
|
4440
|
+
status: orgCfg.status || 'stopped',
|
|
4441
|
+
source: 'schedule-config',
|
|
4442
|
+
lastRun: null,
|
|
4443
|
+
}];
|
|
4444
|
+
}
|
|
4445
|
+
} catch(_) {}
|
|
4446
|
+
}
|
|
4094
4447
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
4095
4448
|
res.end(JSON.stringify({ routines: data.routines || [] }));
|
|
4096
4449
|
} catch(_) { res.writeHead(500); res.end('{"routines":[]}'); }
|
|
@@ -4331,7 +4684,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
4331
4684
|
const allLines = raw.split('\n').filter(Boolean);
|
|
4332
4685
|
const eventCount = allLines.length;
|
|
4333
4686
|
const parse = l => { try { return JSON.parse(l); } catch { return null; } };
|
|
4334
|
-
const headEvents = allLines.slice(0,
|
|
4687
|
+
const headEvents = allLines.slice(0, 10).map(parse).filter(Boolean);
|
|
4335
4688
|
const tailEvents = allLines.slice(-5).map(parse).filter(Boolean);
|
|
4336
4689
|
const first = headEvents.find(e => e.type === 'run:start') || headEvents[0];
|
|
4337
4690
|
const last = tailEvents.slice().reverse().find(e => e.type === 'run:complete' || e.type === 'org:complete');
|
|
@@ -4339,9 +4692,12 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
4339
4692
|
const lastEvent = tailEvents[tailEvents.length - 1] || headEvents[headEvents.length - 1];
|
|
4340
4693
|
const ageMs = lastEvent?.ts ? Date.now() - lastEvent.ts : Infinity;
|
|
4341
4694
|
const isStale = !last && ageMs > 30 * 60 * 1000;
|
|
4695
|
+
// Derive a human-readable goal from the first boss directive when run:start lacks one
|
|
4696
|
+
const firstBossComms = headEvents.find(e => e.type === 'org:comms' && (e.from === 'boss' || e.role === 'boss') && e.msg);
|
|
4697
|
+
const derivedGoal = first?.goal || firstBossComms?.msg?.slice(0, 80) || '';
|
|
4342
4698
|
runs.push({ runId: f.replace('.jsonl', ''), startedAt: first?.ts || 0, endedAt: last?.ts || 0,
|
|
4343
4699
|
status: last ? 'complete' : isStale ? 'stale' : 'running',
|
|
4344
|
-
eventCount, cycleCount: cycles, goal:
|
|
4700
|
+
eventCount, cycleCount: cycles, goal: derivedGoal, bossRole: first?.bossRole || '' });
|
|
4345
4701
|
} catch (_) {}
|
|
4346
4702
|
}
|
|
4347
4703
|
}
|
|
@@ -4373,114 +4729,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
4373
4729
|
// ------------------------------------------------- Mastermind event system
|
|
4374
4730
|
// POST /api/mastermind/event — ingest event from mastermind skill
|
|
4375
4731
|
if (req.method === 'POST' && url === '/api/mastermind/event') {
|
|
4376
|
-
|
|
4377
|
-
for await (const chunk of req) { body += chunk; if (body.length > 2097152) { req.destroy(); break; } }
|
|
4378
|
-
let event = {};
|
|
4379
|
-
try { event = JSON.parse(body); } catch (_) {}
|
|
4380
|
-
event.ts = event.ts || Date.now();
|
|
4381
|
-
// Use project path from event if provided (multi-project support).
|
|
4382
|
-
// Security: path.isAbsolute() alone is insufficient — an attacker can
|
|
4383
|
-
// supply event.project="/etc" and cause writes to system directories.
|
|
4384
|
-
// Only accept paths that resolve to an existing directory AND are not
|
|
4385
|
-
// the filesystem root (/), AND are not obviously system paths.
|
|
4386
|
-
// Cap to 4096 chars to prevent OOM from huge path strings.
|
|
4387
|
-
const _rawProject = event.project;
|
|
4388
|
-
let eventProject = null;
|
|
4389
|
-
if (typeof _rawProject === 'string' && _rawProject.length > 0 && _rawProject.length <= 4096
|
|
4390
|
-
&& path.isAbsolute(_rawProject)) {
|
|
4391
|
-
// Reject filesystem root and common system directories
|
|
4392
|
-
const _norm = path.resolve(_rawProject);
|
|
4393
|
-
const _systemPaths = ['/', '/etc', '/usr', '/bin', '/sbin', '/lib', '/lib64', '/boot', '/dev', '/sys', '/proc', '/tmp'];
|
|
4394
|
-
if (!_systemPaths.includes(_norm) && !_systemPaths.some(p => _norm.startsWith(p + '/'))) {
|
|
4395
|
-
eventProject = _norm;
|
|
4396
|
-
}
|
|
4397
|
-
}
|
|
4398
|
-
const root = eventProject || projectDir || process.cwd();
|
|
4399
|
-
const dataDir = path.join(root, 'data');
|
|
4400
|
-
try { fs.mkdirSync(dataDir, { recursive: true }); } catch (_) {}
|
|
4401
|
-
// Track known project dirs for aggregated session listing
|
|
4402
|
-
if (eventProject) {
|
|
4403
|
-
const knownFile = path.join(projectDir || process.cwd(), 'data', 'known-projects.json');
|
|
4404
|
-
try {
|
|
4405
|
-
let known = [];
|
|
4406
|
-
try { known = JSON.parse(fs.readFileSync(knownFile, 'utf8')); } catch (_) {}
|
|
4407
|
-
if (!known.includes(eventProject)) { known.push(eventProject); fs.writeFileSync(knownFile, JSON.stringify(known)); }
|
|
4408
|
-
} catch (_) {}
|
|
4409
|
-
}
|
|
4410
|
-
try { fs.appendFileSync(path.join(dataDir, 'mastermind-events.jsonl'), JSON.stringify(event) + '\n'); } catch (_) {}
|
|
4411
|
-
// Track active runs and route org events to run files
|
|
4412
|
-
if (event.org) {
|
|
4413
|
-
const _orgKey = String(event.org).trim();
|
|
4414
|
-
// Any event with both org+runId updates the active run map (run:start written directly to file so org:start is first via curl)
|
|
4415
|
-
if (event.runId) activeOrgRuns.set(_orgKey, String(event.runId).trim());
|
|
4416
|
-
else if (activeOrgRuns.has(_orgKey)) event.runId = activeOrgRuns.get(_orgKey);
|
|
4417
|
-
if (event.type === 'run:complete' || event.type === 'org:complete') activeOrgRuns.delete(_orgKey);
|
|
4418
|
-
}
|
|
4419
|
-
// Persist to git-safe run file (survives branch switches + shared across worktrees)
|
|
4420
|
-
if (event.org && event.runId) {
|
|
4421
|
-
try {
|
|
4422
|
-
const _orn = String(event.org).trim();
|
|
4423
|
-
const _rid = String(event.runId).trim();
|
|
4424
|
-
if (_orn.length > 0 && _orn.length <= 64 && /^[a-z0-9][a-z0-9_-]*$/i.test(_orn)
|
|
4425
|
-
&& _rid.length > 0 && _rid.length <= 80 && /^[a-z0-9][a-z0-9_-]*$/i.test(_rid)) {
|
|
4426
|
-
const _monoDir = _getGitMonomindDir(root) || path.join(root, '.monomind');
|
|
4427
|
-
const _runDir = path.join(_monoDir, 'orgs', _orn, 'runs');
|
|
4428
|
-
fs.mkdirSync(_runDir, { recursive: true });
|
|
4429
|
-
fs.appendFileSync(path.join(_runDir, `${_rid}.jsonl`), JSON.stringify(event) + '\n');
|
|
4430
|
-
}
|
|
4431
|
-
} catch (_) {}
|
|
4432
|
-
}
|
|
4433
|
-
// Persist session
|
|
4434
|
-
try {
|
|
4435
|
-
const sessFile = path.join(dataDir, 'mastermind-sessions.json');
|
|
4436
|
-
let sessions = [];
|
|
4437
|
-
try { sessions = JSON.parse(fs.readFileSync(sessFile, 'utf8')); } catch (_) {}
|
|
4438
|
-
if (event.type === 'session:start') {
|
|
4439
|
-
sessions.unshift({ id: event.session, ts: event.ts, prompt: event.prompt || '',
|
|
4440
|
-
status: 'running', domains: [], events: [event], project: root });
|
|
4441
|
-
} else {
|
|
4442
|
-
const s = sessions.find(s => s.id === event.session);
|
|
4443
|
-
if (s) {
|
|
4444
|
-
(s.events = s.events || []).push(event);
|
|
4445
|
-
if (event.type === 'domain:dispatch' && event.domain && !s.domains.includes(event.domain))
|
|
4446
|
-
s.domains.push(event.domain);
|
|
4447
|
-
if (event.type === 'session:complete') { s.status = event.status || 'complete'; s.endTs = event.ts; }
|
|
4448
|
-
}
|
|
4449
|
-
}
|
|
4450
|
-
fs.writeFileSync(sessFile, JSON.stringify(sessions.slice(0, 50), null, 2));
|
|
4451
|
-
// Also write individual session file for direct traceability.
|
|
4452
|
-
// Security: validate event.session before using it as a filename to
|
|
4453
|
-
// prevent path traversal (e.g. "../../../etc/cron.d/payload").
|
|
4454
|
-
const sessionObj = sessions.find(s => s.id === event.session);
|
|
4455
|
-
if (sessionObj) {
|
|
4456
|
-
const sessDir = path.join(dataDir, 'sessions');
|
|
4457
|
-
try { fs.mkdirSync(sessDir, { recursive: true }); } catch (_) {}
|
|
4458
|
-
try {
|
|
4459
|
-
const _sid = String(event.session || '').trim();
|
|
4460
|
-
if (_sid.length > 0 && _sid.length <= 128 && /^[a-zA-Z0-9_.-]+$/.test(_sid)) {
|
|
4461
|
-
fs.writeFileSync(path.join(sessDir, `${_sid}.json`), JSON.stringify(sessionObj, null, 2));
|
|
4462
|
-
}
|
|
4463
|
-
} catch (_) {}
|
|
4464
|
-
}
|
|
4465
|
-
} catch (_) {}
|
|
4466
|
-
// For org:stop events, write a stop marker the boss agent can detect
|
|
4467
|
-
if (event.type === 'org:stop' && event.org) {
|
|
4468
|
-
try {
|
|
4469
|
-
const orgName = String(event.org).trim();
|
|
4470
|
-
// Validate before any filesystem use — reject rather than strip
|
|
4471
|
-
if (orgName.length > 0 && orgName.length <= 64 && /^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
|
|
4472
|
-
const stopDir = path.join(root, '.monomind', 'orgs', '.stops');
|
|
4473
|
-
fs.mkdirSync(stopDir, { recursive: true });
|
|
4474
|
-
fs.writeFileSync(path.join(stopDir, `${orgName}.stop`), String(Date.now()));
|
|
4475
|
-
}
|
|
4476
|
-
} catch (_) {}
|
|
4477
|
-
}
|
|
4478
|
-
// Broadcast to all mastermind SSE clients
|
|
4479
|
-
const msg = `data: ${JSON.stringify(event)}\n\n`;
|
|
4480
|
-
for (const c of mmSseClients) { try { c.write(msg); } catch (_) { mmSseClients.delete(c); } }
|
|
4481
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
4482
|
-
res.end('{"ok":true}');
|
|
4483
|
-
return;
|
|
4732
|
+
return handleMastermindEvent(req, res);
|
|
4484
4733
|
}
|
|
4485
4734
|
|
|
4486
4735
|
// GET /api/mastermind-stream — SSE for real-time events
|
|
@@ -4510,6 +4759,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
4510
4759
|
try {
|
|
4511
4760
|
const qp = new URL('http://x' + req.url).searchParams;
|
|
4512
4761
|
const filterProject = qp.get('project');
|
|
4762
|
+
const limitParam = Math.min(parseInt(qp.get('limit') || '200', 10) || 200, 500);
|
|
4513
4763
|
const serverRoot = projectDir || process.cwd();
|
|
4514
4764
|
// Collect all project dirs to aggregate
|
|
4515
4765
|
const projectDirs = new Set([serverRoot]);
|
|
@@ -4517,23 +4767,44 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
4517
4767
|
const known = JSON.parse(fs.readFileSync(path.join(serverRoot, 'data', 'known-projects.json'), 'utf8'));
|
|
4518
4768
|
known.forEach(p => projectDirs.add(p));
|
|
4519
4769
|
} catch (_) {}
|
|
4520
|
-
// Load and merge sessions from all dirs
|
|
4521
4770
|
let allSessions = [];
|
|
4522
4771
|
for (const pd of projectDirs) {
|
|
4523
4772
|
if (filterProject && pd !== filterProject) continue;
|
|
4524
|
-
const
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4773
|
+
const sessDir = path.join(pd, 'data', 'sessions');
|
|
4774
|
+
const indexFile = path.join(sessDir, '_index.json');
|
|
4775
|
+
// ── New format: per-session JSONL + _index.json ──
|
|
4776
|
+
if (fs.existsSync(indexFile)) {
|
|
4777
|
+
try {
|
|
4778
|
+
const idx = JSON.parse(fs.readFileSync(indexFile, 'utf8'));
|
|
4779
|
+
const top = idx.slice(0, limitParam);
|
|
4780
|
+
for (const entry of top) {
|
|
4781
|
+
const _sid = String(entry.id || '').trim();
|
|
4782
|
+
if (!_sid || !/^[a-zA-Z0-9_.-]+$/.test(_sid)) continue;
|
|
4783
|
+
let events = [];
|
|
4784
|
+
try {
|
|
4785
|
+
const jl = fs.readFileSync(path.join(sessDir, `${_sid}.jsonl`), 'utf8');
|
|
4786
|
+
events = jl.trim().split('\n').filter(Boolean)
|
|
4787
|
+
.map(l => { try { return JSON.parse(l); } catch { return null; } })
|
|
4788
|
+
.filter(Boolean);
|
|
4789
|
+
} catch(_) {}
|
|
4790
|
+
allSessions.push({ ...entry, events, project: pd });
|
|
4791
|
+
}
|
|
4792
|
+
} catch(_) {}
|
|
4793
|
+
} else {
|
|
4794
|
+
// ── Legacy fallback: mastermind-sessions.json ──
|
|
4795
|
+
const f = path.join(pd, 'data', 'mastermind-sessions.json');
|
|
4796
|
+
if (fs.existsSync(f)) {
|
|
4797
|
+
try {
|
|
4798
|
+
const s = JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
4799
|
+
s.forEach(sess => { if (!sess.project) sess.project = pd; });
|
|
4800
|
+
allSessions = allSessions.concat(s);
|
|
4801
|
+
} catch (_) {}
|
|
4802
|
+
}
|
|
4803
|
+
}
|
|
4532
4804
|
}
|
|
4533
|
-
|
|
4534
|
-
allSessions.sort((a,b) => (b.ts||0)-(a.ts||0));
|
|
4805
|
+
allSessions.sort((a,b) => (b.ts||b.startedAt||0)-(a.ts||a.startedAt||0));
|
|
4535
4806
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
4536
|
-
res.end(JSON.stringify(allSessions.slice(0,
|
|
4807
|
+
res.end(JSON.stringify(allSessions.slice(0, limitParam)));
|
|
4537
4808
|
} catch (_) { res.writeHead(200); res.end('[]'); }
|
|
4538
4809
|
return;
|
|
4539
4810
|
}
|
|
@@ -4637,6 +4908,77 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
4637
4908
|
return;
|
|
4638
4909
|
}
|
|
4639
4910
|
|
|
4911
|
+
// GET /api/status — live system snapshot for dashboard polling
|
|
4912
|
+
if (req.method === 'GET' && url === '/api/status') {
|
|
4913
|
+
try {
|
|
4914
|
+
const root = projectDir || process.cwd();
|
|
4915
|
+
// Active org runs: { orgName -> runId }
|
|
4916
|
+
const orgRuns = {};
|
|
4917
|
+
activeOrgRuns.forEach((runId, org) => { orgRuns[org] = runId; });
|
|
4918
|
+
// Recent events (last 10)
|
|
4919
|
+
let recentEvents = [];
|
|
4920
|
+
try {
|
|
4921
|
+
const evPath = path.join(root, 'data', 'mastermind-events.jsonl');
|
|
4922
|
+
const lines = fs.readFileSync(evPath, 'utf8').split('\n').filter(l => l.trim()).slice(-10);
|
|
4923
|
+
recentEvents = lines.map(l => { try { return JSON.parse(l); } catch(_) { return null; } }).filter(Boolean);
|
|
4924
|
+
} catch(_) {}
|
|
4925
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
4926
|
+
res.end(JSON.stringify({
|
|
4927
|
+
ts: Date.now(),
|
|
4928
|
+
uptime: process.uptime(),
|
|
4929
|
+
dir: root,
|
|
4930
|
+
sseClients: mmSseClients.size,
|
|
4931
|
+
activeOrgs: Object.keys(orgRuns).length,
|
|
4932
|
+
orgRuns,
|
|
4933
|
+
recentEvents,
|
|
4934
|
+
}));
|
|
4935
|
+
} catch(err) {
|
|
4936
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
4937
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
4938
|
+
}
|
|
4939
|
+
return;
|
|
4940
|
+
}
|
|
4941
|
+
|
|
4942
|
+
// GET /api/orgs/:name/runs/current — events from the active run file for an org
|
|
4943
|
+
if (req.method === 'GET' && /^\/api\/orgs\/[^/]+\/runs\/current$/.test(url)) {
|
|
4944
|
+
try {
|
|
4945
|
+
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
4946
|
+
const _curQs = new URL(req.url, 'http://localhost').searchParams;
|
|
4947
|
+
const root = path.resolve(_curQs.get('dir') || projectDir || process.cwd());
|
|
4948
|
+
// Validate orgName
|
|
4949
|
+
if (!orgName || orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
|
|
4950
|
+
res.writeHead(400); res.end('{"error":"invalid org name"}'); return;
|
|
4951
|
+
}
|
|
4952
|
+
const runId = activeOrgRuns.get(orgName);
|
|
4953
|
+
const monoDir = _getGitMonomindDir(root) || path.join(root, '.monomind');
|
|
4954
|
+
// Try active run first, then fall back to most recent run file
|
|
4955
|
+
let runFile = null;
|
|
4956
|
+
if (runId) {
|
|
4957
|
+
const candidate = path.join(monoDir, 'orgs', orgName, 'runs', `${runId}.jsonl`);
|
|
4958
|
+
if (fs.existsSync(candidate)) runFile = candidate;
|
|
4959
|
+
}
|
|
4960
|
+
if (!runFile) {
|
|
4961
|
+
const runsDir = path.join(monoDir, 'orgs', orgName, 'runs');
|
|
4962
|
+
if (fs.existsSync(runsDir)) {
|
|
4963
|
+
const files = fs.readdirSync(runsDir).filter(f => f.endsWith('.jsonl'));
|
|
4964
|
+
if (files.length) {
|
|
4965
|
+
files.sort();
|
|
4966
|
+
runFile = path.join(runsDir, files[files.length - 1]);
|
|
4967
|
+
}
|
|
4968
|
+
}
|
|
4969
|
+
}
|
|
4970
|
+
if (!runFile) { res.writeHead(404); res.end('{"events":[],"runId":null}'); return; }
|
|
4971
|
+
const detectedRunId = path.basename(runFile, '.jsonl');
|
|
4972
|
+
const lines = fs.readFileSync(runFile, 'utf8').split('\n').filter(l => l.trim()).slice(-100);
|
|
4973
|
+
const events = lines.map(l => { try { return JSON.parse(l); } catch(_) { return null; } }).filter(Boolean);
|
|
4974
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
4975
|
+
res.end(JSON.stringify({ runId: detectedRunId, events, active: activeOrgRuns.has(orgName) }));
|
|
4976
|
+
} catch(err) {
|
|
4977
|
+
res.writeHead(500); res.end(JSON.stringify({ error: err.message }));
|
|
4978
|
+
}
|
|
4979
|
+
return;
|
|
4980
|
+
}
|
|
4981
|
+
|
|
4640
4982
|
// GET /api/mastermind/metrics — aggregate system metrics from token-summary and swarm-activity
|
|
4641
4983
|
if (req.method === 'GET' && url === '/api/mastermind/metrics') {
|
|
4642
4984
|
try {
|
|
@@ -4664,6 +5006,63 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
4664
5006
|
const boundPort = await bindServer(server, port);
|
|
4665
5007
|
const url = `http://localhost:${boundPort}`;
|
|
4666
5008
|
|
|
5009
|
+
// ── One-time migration: mastermind-sessions.json → per-session JSONL ─────
|
|
5010
|
+
// Runs once on startup. Existing sessions in the old monolithic format are
|
|
5011
|
+
// split into individual JSONL files + _index.json for O(1) event writes.
|
|
5012
|
+
try {
|
|
5013
|
+
const _migDataDir = path.join(projectDir || process.cwd(), 'data');
|
|
5014
|
+
const _migOldFile = path.join(_migDataDir, 'mastermind-sessions.json');
|
|
5015
|
+
const _migSessDir = path.join(_migDataDir, 'sessions');
|
|
5016
|
+
const _migIndexFile = path.join(_migSessDir, '_index.json');
|
|
5017
|
+
if (fs.existsSync(_migOldFile) && !fs.existsSync(_migIndexFile)) {
|
|
5018
|
+
try {
|
|
5019
|
+
const _migOld = JSON.parse(fs.readFileSync(_migOldFile, 'utf8'));
|
|
5020
|
+
fs.mkdirSync(_migSessDir, { recursive: true });
|
|
5021
|
+
const _migIndex = [];
|
|
5022
|
+
for (const sess of (_migOld || [])) {
|
|
5023
|
+
const _msid = String(sess.id || '').trim();
|
|
5024
|
+
if (!_msid || !/^[a-zA-Z0-9_.-]+$/.test(_msid)) continue;
|
|
5025
|
+
// Write per-session JSONL
|
|
5026
|
+
const _mEvts = (sess.events || []);
|
|
5027
|
+
const _mLines = _mEvts.map(e => JSON.stringify(e)).join('\n');
|
|
5028
|
+
fs.writeFileSync(path.join(_migSessDir, `${_msid}.jsonl`), _mLines + (_mLines ? '\n' : ''));
|
|
5029
|
+
_migIndex.push({ id: _msid, ts: sess.ts, prompt: sess.prompt || '',
|
|
5030
|
+
status: sess.status || 'complete', org: sess.org || '',
|
|
5031
|
+
startedAt: sess.ts || sess.startedAt, endedAt: sess.endTs || sess.endedAt,
|
|
5032
|
+
domains: sess.domains || [] });
|
|
5033
|
+
}
|
|
5034
|
+
fs.writeFileSync(_migIndexFile, JSON.stringify(_migIndex));
|
|
5035
|
+
console.log('[server] migrated ' + _migIndex.length + ' sessions to per-session JSONL format');
|
|
5036
|
+
} catch(_me) { console.warn('[server] session migration failed:', _me.message); }
|
|
5037
|
+
}
|
|
5038
|
+
} catch (_) {}
|
|
5039
|
+
|
|
5040
|
+
// Rebuild activeOrgRuns from disk so event enrichment (runId injection) still works
|
|
5041
|
+
// after a server restart. Without this, org events emitted mid-run that lack runId
|
|
5042
|
+
// are broadcast without it and _odtHandleLiveEvent drops them.
|
|
5043
|
+
try {
|
|
5044
|
+
const _rbOrgsDir = path.join(projectDir || process.cwd(), '.monomind', 'orgs');
|
|
5045
|
+
if (fs.existsSync(_rbOrgsDir)) {
|
|
5046
|
+
for (const _rbOrg of fs.readdirSync(_rbOrgsDir)) {
|
|
5047
|
+
if (!_rbOrg || _rbOrg.startsWith('.') || !/^[a-z0-9][a-z0-9_-]*$/i.test(_rbOrg)) continue;
|
|
5048
|
+
const _rbRunsDir = path.join(_rbOrgsDir, _rbOrg, 'runs');
|
|
5049
|
+
if (!fs.existsSync(_rbRunsDir)) continue;
|
|
5050
|
+
const _rbFiles = fs.readdirSync(_rbRunsDir)
|
|
5051
|
+
.filter(f => f.endsWith('.jsonl') && !f.endsWith('.convs.jsonl'))
|
|
5052
|
+
.sort().reverse();
|
|
5053
|
+
for (const _rbF of _rbFiles.slice(0, 5)) {
|
|
5054
|
+
try {
|
|
5055
|
+
const _rbId = _rbF.replace('.jsonl', '');
|
|
5056
|
+
const _rbContent = fs.readFileSync(path.join(_rbRunsDir, _rbF), 'utf8');
|
|
5057
|
+
const _rbLast = _rbContent.trim().split('\n').filter(Boolean).slice(-10);
|
|
5058
|
+
const _rbDone = _rbLast.some(l => { try { const e = JSON.parse(l); return e.type === 'run:complete' || e.type === 'org:complete'; } catch { return false; } });
|
|
5059
|
+
if (!_rbDone) { activeOrgRuns.set(_rbOrg, _rbId); break; }
|
|
5060
|
+
} catch (_) {}
|
|
5061
|
+
}
|
|
5062
|
+
}
|
|
5063
|
+
}
|
|
5064
|
+
} catch (_) {}
|
|
5065
|
+
|
|
4667
5066
|
// ---------------------------------------------------------------- Watchers
|
|
4668
5067
|
let debounceTimer = null;
|
|
4669
5068
|
let pendingSections = new Set();
|