@monoes/monomindcli 1.13.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 +11 -11
- package/.claude/helpers/hook-handler.cjs +17 -1
- package/.claude/helpers/session.cjs +20 -2
- 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/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/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/ui/dashboard.html +1639 -249
- package/dist/src/ui/orgs.html +1 -0
- package/dist/src/ui/server.mjs +389 -132
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -61,6 +61,29 @@ function findCliPath() {
|
|
|
61
61
|
const local = path.join(CWD, 'packages', '@monomind', 'cli', 'bin', 'cli.js');
|
|
62
62
|
if (fs.existsSync(local)) return { cmd: process.execPath, args: [local], usePort: false };
|
|
63
63
|
|
|
64
|
+
// Try global npm install paths for both package names
|
|
65
|
+
// npm root -g is slow; probe known conventional paths instead
|
|
66
|
+
const globalCandidates = [];
|
|
67
|
+
try {
|
|
68
|
+
const { execSync } = require('child_process');
|
|
69
|
+
const npmRoot = execSync('npm root -g', { timeout: 3000, encoding: 'utf-8' }).trim();
|
|
70
|
+
if (npmRoot) {
|
|
71
|
+
globalCandidates.push(
|
|
72
|
+
path.join(npmRoot, 'monomind', 'packages', '@monomind', 'cli', 'dist', 'src', 'ui', 'server.mjs'),
|
|
73
|
+
path.join(npmRoot, '@monoes', 'monomindcli', 'dist', 'src', 'ui', 'server.mjs'),
|
|
74
|
+
path.join(npmRoot, 'monomind', 'packages', '@monomind', 'cli', 'bin', 'cli.js'),
|
|
75
|
+
path.join(npmRoot, '@monoes', 'monomindcli', 'bin', 'cli.js'),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
} catch { /* npm root -g failed — skip */ }
|
|
79
|
+
|
|
80
|
+
for (const candidate of globalCandidates) {
|
|
81
|
+
if (fs.existsSync(candidate)) {
|
|
82
|
+
const usePort = candidate.endsWith('server.mjs');
|
|
83
|
+
return { cmd: process.execPath, args: [candidate], usePort };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
64
87
|
// Try npx monomind as last resort
|
|
65
88
|
return { cmd: 'npx', args: ['monomind@latest'], usePort: false };
|
|
66
89
|
}
|
|
@@ -88,9 +111,45 @@ function main() {
|
|
|
88
111
|
|
|
89
112
|
child.unref();
|
|
90
113
|
|
|
114
|
+
// Write optimistic status with DEFAULT_PORT immediately so dependent scripts
|
|
115
|
+
// (hooks, boss agents) have something to read while the server starts up.
|
|
91
116
|
writeStatus(child.pid, DEFAULT_PORT);
|
|
92
117
|
process.stdout.write(`[control] started Neural Control Room on port ${DEFAULT_PORT} (pid ${child.pid})\n`);
|
|
93
|
-
|
|
118
|
+
|
|
119
|
+
// If port 4242 was in use, server.mjs auto-increments (up to +10).
|
|
120
|
+
// Poll a few ports to find where it actually bound and update control.json.
|
|
121
|
+
const http = require('http');
|
|
122
|
+
function probePort(p) {
|
|
123
|
+
return new Promise((resolve) => {
|
|
124
|
+
const req = http.get({ hostname: 'localhost', port: p, path: '/api/status', timeout: 1000 }, (res) => {
|
|
125
|
+
res.resume();
|
|
126
|
+
resolve(res.statusCode < 500);
|
|
127
|
+
});
|
|
128
|
+
req.on('error', () => resolve(false));
|
|
129
|
+
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Give the server up to ~3 s to start, then confirm the actual port.
|
|
134
|
+
async function confirmPort() {
|
|
135
|
+
for (let attempt = 0; attempt < 6; attempt++) {
|
|
136
|
+
await new Promise(r => setTimeout(r, 500));
|
|
137
|
+
for (let delta = 0; delta <= 10; delta++) {
|
|
138
|
+
const p = DEFAULT_PORT + delta;
|
|
139
|
+
if (await probePort(p)) {
|
|
140
|
+
if (p !== DEFAULT_PORT) {
|
|
141
|
+
writeStatus(child.pid, p);
|
|
142
|
+
process.stdout.write(`[control] server bound to port ${p} (updated control.json)\n`);
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Server didn't respond in time — leave control.json as-is (best-effort)
|
|
149
|
+
process.stdout.write('[control] server did not respond within 3 s — control.json may have wrong port\n');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
confirmPort().catch(() => {}).finally(() => process.exit(0));
|
|
94
153
|
}
|
|
95
154
|
|
|
96
155
|
main();
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* Universal Event Logger
|
|
4
|
-
* Captures 100% of Claude Code hook events
|
|
4
|
+
* Captures 100% of Claude Code hook events.
|
|
5
|
+
* Writes to .git/monomind/events/ (branch-agnostic, shared across worktrees).
|
|
6
|
+
* Falls back to .monomind/events/ when git is unavailable.
|
|
5
7
|
* Runs as a fast, non-blocking hook — append-only, no processing.
|
|
6
8
|
*/
|
|
7
9
|
|
|
@@ -83,8 +85,26 @@ async function main() {
|
|
|
83
85
|
// Remove undefined keys
|
|
84
86
|
Object.keys(entry).forEach(k => entry[k] === undefined && delete entry[k]);
|
|
85
87
|
|
|
88
|
+
// Resolve the git-safe monomind root (mirrors _getGitMonomindDir in server.mjs)
|
|
89
|
+
function getMonoDir(workDir) {
|
|
90
|
+
try {
|
|
91
|
+
const gitEntry = path.join(workDir, '.git');
|
|
92
|
+
const st = fs.statSync(gitEntry);
|
|
93
|
+
if (st.isDirectory()) return path.join(gitEntry, 'monomind');
|
|
94
|
+
if (st.isFile()) {
|
|
95
|
+
const m = fs.readFileSync(gitEntry, 'utf8').match(/^gitdir:\s*(.+)/m);
|
|
96
|
+
if (m) {
|
|
97
|
+
const worktreeDir = path.resolve(workDir, m[1].trim());
|
|
98
|
+
return path.join(path.dirname(path.dirname(worktreeDir)), 'monomind');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} catch {}
|
|
102
|
+
return path.join(workDir, '.monomind');
|
|
103
|
+
}
|
|
104
|
+
const monoDir = getMonoDir(CWD);
|
|
105
|
+
|
|
86
106
|
// Write to daily all-events log
|
|
87
|
-
const eventsDir = path.join(
|
|
107
|
+
const eventsDir = path.join(monoDir, 'events');
|
|
88
108
|
const today = new Date().toISOString().slice(0, 10);
|
|
89
109
|
const allLog = path.join(eventsDir, `${today}-all-events.jsonl`);
|
|
90
110
|
|
|
@@ -101,6 +121,27 @@ async function main() {
|
|
|
101
121
|
} catch {}
|
|
102
122
|
}
|
|
103
123
|
|
|
124
|
+
// Solution 5: cross-reference Claude session ID into the active mastermind session.
|
|
125
|
+
// When a new Claude session starts, record its UUID in current.json so future
|
|
126
|
+
// lookups can join mastermind session state with the Claude conversation transcript.
|
|
127
|
+
if (eventType === 'session-start' && sessionId !== 'unknown') {
|
|
128
|
+
try {
|
|
129
|
+
const sessionsDir = path.join(monoDir, 'sessions');
|
|
130
|
+
const currentFile = path.join(sessionsDir, 'current.json');
|
|
131
|
+
if (fs.existsSync(currentFile)) {
|
|
132
|
+
const cur = JSON.parse(fs.readFileSync(currentFile, 'utf8'));
|
|
133
|
+
const claudeSessions = Array.isArray(cur.claude_sessions) ? cur.claude_sessions : [];
|
|
134
|
+
if (!claudeSessions.includes(sessionId)) {
|
|
135
|
+
claudeSessions.push(sessionId);
|
|
136
|
+
const updated = { ...cur, claude_sessions: claudeSessions };
|
|
137
|
+
const tmp = `${currentFile}.${process.pid}.tmp`;
|
|
138
|
+
fs.writeFileSync(tmp, JSON.stringify(updated, null, 2), 'utf8');
|
|
139
|
+
fs.renameSync(tmp, currentFile);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch {}
|
|
143
|
+
}
|
|
144
|
+
|
|
104
145
|
// Forward notifications and subagent events to dashboard for live visibility
|
|
105
146
|
const forwardTypes = ['notification', 'subagent-start', 'subagent-stop', 'user-prompt'];
|
|
106
147
|
if (forwardTypes.includes(eventType)) {
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Comprehensive session + subagent capture handler.
|
|
3
|
+
// Wired to SubagentStart (snapshot + spawn event) and SubagentStop (diff + emit).
|
|
4
|
+
//
|
|
5
|
+
// On SubagentStart:
|
|
6
|
+
// - Snapshots ~/.claude/projects/{proj}/*.jsonl file sizes
|
|
7
|
+
// - Emits agent:spawn to link the agent to the current mastermind session immediately
|
|
8
|
+
//
|
|
9
|
+
// On SubagentStop:
|
|
10
|
+
// - Diffs the snapshot, parses new JSONL files for token usage and last messages
|
|
11
|
+
// - Emits agent:complete (replaces org:comms) with full result and token data
|
|
12
|
+
// - Emits agent:usage for cost tracking
|
|
13
|
+
// - Persists to per-run capture log
|
|
14
|
+
//
|
|
15
|
+
// Active session awareness:
|
|
16
|
+
// - Reads .monomind/capture/active-session.json (written by server on session:start)
|
|
17
|
+
// - Reads .monomind/capture/active-run.json (written by server on run:start)
|
|
18
|
+
// - Includes sessionId in all emitted events so they link to the dashboard session
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const http = require('http');
|
|
23
|
+
const os = require('os');
|
|
24
|
+
|
|
25
|
+
const CWD = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
26
|
+
const CAPTURE_DIR = path.join(CWD, '.monomind', 'capture');
|
|
27
|
+
|
|
28
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
function readStdin() {
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
let data = '';
|
|
33
|
+
process.stdin.on('data', c => data += c);
|
|
34
|
+
process.stdin.on('end', () => { try { resolve(JSON.parse(data)); } catch { resolve({}); } });
|
|
35
|
+
setTimeout(() => resolve({}), 3000);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getClaudeProjectDir() {
|
|
40
|
+
const encoded = CWD.replace(/\//g, '-');
|
|
41
|
+
return path.join(os.homedir(), '.claude', 'projects', encoded);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function snapshotJSONLFiles() {
|
|
45
|
+
const claudeDir = getClaudeProjectDir();
|
|
46
|
+
if (!fs.existsSync(claudeDir)) return [];
|
|
47
|
+
try {
|
|
48
|
+
return fs.readdirSync(claudeDir)
|
|
49
|
+
.filter(f => f.endsWith('.jsonl'))
|
|
50
|
+
.map(f => {
|
|
51
|
+
try { return { name: f, size: fs.statSync(path.join(claudeDir, f)).size }; }
|
|
52
|
+
catch { return { name: f, size: 0 }; }
|
|
53
|
+
});
|
|
54
|
+
} catch { return []; }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseJSONLForData(filePath) {
|
|
58
|
+
let tin = 0, tout = 0;
|
|
59
|
+
let allMsgs = [];
|
|
60
|
+
let toolCalls = [];
|
|
61
|
+
try {
|
|
62
|
+
const lines = fs.readFileSync(filePath, 'utf8').split('\n').filter(Boolean);
|
|
63
|
+
for (const line of lines) {
|
|
64
|
+
try {
|
|
65
|
+
const d = JSON.parse(line);
|
|
66
|
+
const u = d?.message?.usage || {};
|
|
67
|
+
tin += (u.input_tokens || 0) + (u.cache_creation_input_tokens || 0);
|
|
68
|
+
tout += (u.output_tokens || 0);
|
|
69
|
+
if (d?.message?.role === 'assistant') {
|
|
70
|
+
const content = d?.message?.content;
|
|
71
|
+
if (Array.isArray(content)) {
|
|
72
|
+
const tb = content.find(b => b.type === 'text');
|
|
73
|
+
if (tb?.text) allMsgs.push(tb.text);
|
|
74
|
+
// Capture tool uses for context
|
|
75
|
+
const tools = content.filter(b => b.type === 'tool_use').map(b => b.name).filter(Boolean);
|
|
76
|
+
toolCalls.push(...tools);
|
|
77
|
+
} else if (typeof content === 'string' && content) {
|
|
78
|
+
allMsgs.push(content);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
} catch { /* skip malformed lines */ }
|
|
82
|
+
}
|
|
83
|
+
} catch { /* unreadable */ }
|
|
84
|
+
// Return last 3 messages for context + first+last message
|
|
85
|
+
const lastMsg = allMsgs[allMsgs.length - 1] || '';
|
|
86
|
+
const firstMsg = allMsgs[0] || '';
|
|
87
|
+
const summary = allMsgs.length > 1
|
|
88
|
+
? (firstMsg.slice(0, 300) + (allMsgs.length > 2 ? `\n…[${allMsgs.length - 2} more msgs]…\n` : '\n') + lastMsg.slice(0, 700))
|
|
89
|
+
: lastMsg.slice(0, 1000);
|
|
90
|
+
return { tokens_in: tin, tokens_out: tout, summary, last_msg: lastMsg, toolCalls: [...new Set(toolCalls)] };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function getActiveRun() {
|
|
94
|
+
const runFile = path.join(CAPTURE_DIR, 'active-run.json');
|
|
95
|
+
if (!fs.existsSync(runFile)) return null;
|
|
96
|
+
try {
|
|
97
|
+
const d = JSON.parse(fs.readFileSync(runFile, 'utf8'));
|
|
98
|
+
// Treat as stale after 8 hours (was 60 min — too short for long loops)
|
|
99
|
+
if (Date.now() - (d.ts || 0) > 8 * 60 * 60 * 1000) return null;
|
|
100
|
+
return d;
|
|
101
|
+
} catch { return null; }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getActiveSession() {
|
|
105
|
+
const sessFile = path.join(CAPTURE_DIR, 'active-session.json');
|
|
106
|
+
if (!fs.existsSync(sessFile)) return null;
|
|
107
|
+
try {
|
|
108
|
+
const d = JSON.parse(fs.readFileSync(sessFile, 'utf8'));
|
|
109
|
+
// Treat as stale after 8 hours
|
|
110
|
+
if (Date.now() - (d.ts || 0) > 8 * 60 * 60 * 1000) return null;
|
|
111
|
+
return d; // { org, sessionId, ts }
|
|
112
|
+
} catch { return null; }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function postEvent(event) {
|
|
116
|
+
return new Promise((resolve) => {
|
|
117
|
+
try {
|
|
118
|
+
const ctrlPath = path.join(CWD, '.monomind', 'control.json');
|
|
119
|
+
let baseUrl = 'http://localhost:4242';
|
|
120
|
+
if (fs.existsSync(ctrlPath)) {
|
|
121
|
+
try { baseUrl = JSON.parse(fs.readFileSync(ctrlPath, 'utf8')).url || baseUrl; } catch {}
|
|
122
|
+
}
|
|
123
|
+
const u = new URL(baseUrl);
|
|
124
|
+
const body = JSON.stringify(event);
|
|
125
|
+
const req = http.request({
|
|
126
|
+
hostname: u.hostname,
|
|
127
|
+
port: parseInt(u.port || '4242', 10),
|
|
128
|
+
path: '/api/mastermind/event',
|
|
129
|
+
method: 'POST',
|
|
130
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
|
|
131
|
+
}, () => resolve());
|
|
132
|
+
req.on('error', () => resolve());
|
|
133
|
+
req.setTimeout(3000, () => { req.destroy(); resolve(); });
|
|
134
|
+
req.write(body);
|
|
135
|
+
req.end();
|
|
136
|
+
} catch { resolve(); }
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ─── SubagentStart: snapshot + emit agent:spawn ──────────────────────────────
|
|
141
|
+
|
|
142
|
+
async function handleSubagentStart(hookInput) {
|
|
143
|
+
fs.mkdirSync(CAPTURE_DIR, { recursive: true });
|
|
144
|
+
|
|
145
|
+
const snapshot = snapshotJSONLFiles();
|
|
146
|
+
const agentType = String(
|
|
147
|
+
hookInput.subagent_type || hookInput.agentType || hookInput.agent_type || 'unknown'
|
|
148
|
+
).slice(0, 64).replace(/[^a-z0-9_-]/gi, '-');
|
|
149
|
+
const agentDesc = String(hookInput.description || hookInput.prompt_description || '').slice(0, 1000);
|
|
150
|
+
const activeRun = getActiveRun();
|
|
151
|
+
const activeSess = getActiveSession();
|
|
152
|
+
|
|
153
|
+
// Write snapshot to FIFO queue — subagent-stop pops the oldest
|
|
154
|
+
const snapFile = path.join(
|
|
155
|
+
CAPTURE_DIR,
|
|
156
|
+
'snap-' + Date.now() + '-' + Math.random().toString(36).slice(2, 6) + '.json'
|
|
157
|
+
);
|
|
158
|
+
fs.writeFileSync(snapFile, JSON.stringify({
|
|
159
|
+
ts: Date.now(),
|
|
160
|
+
files: snapshot,
|
|
161
|
+
agentType,
|
|
162
|
+
agentDesc,
|
|
163
|
+
org: activeSess?.org || activeRun?.org || null,
|
|
164
|
+
runId: activeRun?.runId || null,
|
|
165
|
+
session: activeSess?.sessionId || null,
|
|
166
|
+
}));
|
|
167
|
+
|
|
168
|
+
console.log('[CAPTURE:start] ' + agentType + ' · snapped ' + snapshot.length + ' files'
|
|
169
|
+
+ (activeSess ? ' · sess=' + activeSess.sessionId : '')
|
|
170
|
+
+ (activeRun ? ' · run=' + activeRun.runId : ' · no active run'));
|
|
171
|
+
|
|
172
|
+
// Emit agent:spawn immediately so the dashboard shows the agent starting
|
|
173
|
+
const org = activeSess?.org || activeRun?.org;
|
|
174
|
+
if (org || activeSess?.sessionId) {
|
|
175
|
+
await postEvent({
|
|
176
|
+
type: 'agent:spawn',
|
|
177
|
+
org: org || '',
|
|
178
|
+
runId: activeRun?.runId || '',
|
|
179
|
+
session: activeSess?.sessionId || '',
|
|
180
|
+
agentType,
|
|
181
|
+
task: agentDesc.slice(0, 500),
|
|
182
|
+
from: 'orchestrator',
|
|
183
|
+
to: agentType,
|
|
184
|
+
ts: Date.now(),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ─── SubagentStop: diff + emit ────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
async function handleSubagentStop(hookInput) {
|
|
192
|
+
fs.mkdirSync(CAPTURE_DIR, { recursive: true });
|
|
193
|
+
|
|
194
|
+
// Pop oldest snapshot (FIFO — matches the agent that just finished)
|
|
195
|
+
const snapFiles = fs.readdirSync(CAPTURE_DIR)
|
|
196
|
+
.filter(f => f.startsWith('snap-') && f.endsWith('.json'))
|
|
197
|
+
.sort(); // lexicographic = timestamp order
|
|
198
|
+
|
|
199
|
+
if (snapFiles.length === 0) {
|
|
200
|
+
console.log('[CAPTURE:stop] No snapshot to diff — skipping');
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const snapPath = path.join(CAPTURE_DIR, snapFiles[0]);
|
|
205
|
+
let snap;
|
|
206
|
+
try { snap = JSON.parse(fs.readFileSync(snapPath, 'utf8')); } catch {
|
|
207
|
+
try { fs.unlinkSync(snapPath); } catch {}
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
try { fs.unlinkSync(snapPath); } catch {}
|
|
211
|
+
|
|
212
|
+
// Also check current active session (may have changed since SubagentStart)
|
|
213
|
+
const activeSess = getActiveSession();
|
|
214
|
+
const session = snap.session || activeSess?.sessionId || null;
|
|
215
|
+
const org = snap.org || activeSess?.org || null;
|
|
216
|
+
const runId = snap.runId || null;
|
|
217
|
+
|
|
218
|
+
// Diff JSONL files — only count entirely NEW files (subagent creates its own session)
|
|
219
|
+
const claudeDir = getClaudeProjectDir();
|
|
220
|
+
const currentFiles = snapshotJSONLFiles();
|
|
221
|
+
const prevNames = new Set((snap.files || []).map(f => f.name));
|
|
222
|
+
|
|
223
|
+
let totalTin = 0, totalTout = 0;
|
|
224
|
+
let summary = '';
|
|
225
|
+
let toolCalls = [];
|
|
226
|
+
const capturedFiles = [];
|
|
227
|
+
|
|
228
|
+
for (const f of currentFiles) {
|
|
229
|
+
if (!prevNames.has(f.name)) {
|
|
230
|
+
const parsed = parseJSONLForData(path.join(claudeDir, f.name));
|
|
231
|
+
totalTin += parsed.tokens_in;
|
|
232
|
+
totalTout += parsed.tokens_out;
|
|
233
|
+
if (parsed.summary) summary = parsed.summary;
|
|
234
|
+
toolCalls.push(...parsed.toolCalls);
|
|
235
|
+
capturedFiles.push(f.name);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const costUsd = parseFloat((totalTin * 3e-6 + totalTout * 15e-6).toFixed(6));
|
|
240
|
+
const { agentType, agentDesc } = snap;
|
|
241
|
+
toolCalls = [...new Set(toolCalls)].slice(0, 20);
|
|
242
|
+
|
|
243
|
+
console.log('[CAPTURE:stop] ' + agentType + ' · ' + totalTin + '+' + totalTout
|
|
244
|
+
+ ' tok · $' + costUsd.toFixed(4) + ' · ' + capturedFiles.length + ' new files'
|
|
245
|
+
+ (session ? ' · sess=' + session : '')
|
|
246
|
+
+ (org ? ' · ' + org + '/' + runId : ''));
|
|
247
|
+
|
|
248
|
+
if (!org && !session) {
|
|
249
|
+
// No active org or session — log to general capture file only
|
|
250
|
+
const genLog = path.join(CAPTURE_DIR, 'unattributed.jsonl');
|
|
251
|
+
fs.appendFileSync(genLog, JSON.stringify({
|
|
252
|
+
ts: Date.now(), agentType, tokens_in: totalTin, tokens_out: totalTout,
|
|
253
|
+
cost_usd: costUsd, capturedFiles, summary: summary.slice(0, 200),
|
|
254
|
+
}) + '\n');
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ── Emit agent:usage (token/cost accounting) ──────────────────────────────
|
|
259
|
+
if (totalTin > 0 || totalTout > 0) {
|
|
260
|
+
await postEvent({
|
|
261
|
+
type: 'agent:usage',
|
|
262
|
+
org: org || '',
|
|
263
|
+
runId: runId || '',
|
|
264
|
+
session: session || '',
|
|
265
|
+
role: agentType,
|
|
266
|
+
agentType,
|
|
267
|
+
tokens_in: totalTin,
|
|
268
|
+
tokens_out: totalTout,
|
|
269
|
+
cost_usd: costUsd,
|
|
270
|
+
ts: Date.now(),
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ── Emit agent:complete (replaces org:comms — richer, includes session) ───
|
|
275
|
+
// Always emit even if summary is empty (so the spawn/complete pair is always visible)
|
|
276
|
+
await postEvent({
|
|
277
|
+
type: 'agent:complete',
|
|
278
|
+
org: org || '',
|
|
279
|
+
runId: runId || '',
|
|
280
|
+
session: session || '',
|
|
281
|
+
agentType,
|
|
282
|
+
role: agentType,
|
|
283
|
+
from: agentType,
|
|
284
|
+
to: 'boss',
|
|
285
|
+
result: summary.slice(0, 1000),
|
|
286
|
+
toolCalls,
|
|
287
|
+
tokens_in: totalTin,
|
|
288
|
+
tokens_out: totalTout,
|
|
289
|
+
cost_usd: costUsd,
|
|
290
|
+
capturedFiles: capturedFiles.slice(0, 10),
|
|
291
|
+
ts: Date.now(),
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// ── Also emit legacy org:comms for backwards compat ───────────────────────
|
|
295
|
+
if (summary && (org || session)) {
|
|
296
|
+
await postEvent({
|
|
297
|
+
type: 'org:comms',
|
|
298
|
+
org: org || '',
|
|
299
|
+
runId: runId || '',
|
|
300
|
+
session: session || '',
|
|
301
|
+
from: agentType,
|
|
302
|
+
to: 'boss',
|
|
303
|
+
msg: summary.slice(0, 500),
|
|
304
|
+
ts: Date.now(),
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ── Persist transcript reference to run directory ─────────────────────────
|
|
309
|
+
if (capturedFiles.length > 0 && org && runId) {
|
|
310
|
+
const runDir = path.join(CWD, '.monomind', 'orgs', org, 'runs');
|
|
311
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
312
|
+
const capLog = path.join(runDir, runId + '-captures.jsonl');
|
|
313
|
+
fs.appendFileSync(capLog, JSON.stringify({
|
|
314
|
+
type: 'agent:capture',
|
|
315
|
+
org, runId, session,
|
|
316
|
+
agentType,
|
|
317
|
+
agentDesc: agentDesc.slice(0, 200),
|
|
318
|
+
tokens_in: totalTin,
|
|
319
|
+
tokens_out: totalTout,
|
|
320
|
+
cost_usd: costUsd,
|
|
321
|
+
capturedFiles,
|
|
322
|
+
claudeProjectDir: claudeDir,
|
|
323
|
+
ts: Date.now(),
|
|
324
|
+
}) + '\n');
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ─── Dispatch ─────────────────────────────────────────────────────────────────
|
|
329
|
+
|
|
330
|
+
const eventType = process.argv[2]; // 'subagent-start' | 'subagent-stop'
|
|
331
|
+
|
|
332
|
+
readStdin().then(hookInput => {
|
|
333
|
+
if (eventType === 'subagent-start') return handleSubagentStart(hookInput);
|
|
334
|
+
if (eventType === 'subagent-stop') return handleSubagentStop(hookInput);
|
|
335
|
+
console.log('[CAPTURE] unknown event type: ' + eventType);
|
|
336
|
+
}).catch(() => process.exit(0));
|
|
@@ -382,9 +382,10 @@ module.exports = {
|
|
|
382
382
|
} catch (e) { /* ignore */ }
|
|
383
383
|
}
|
|
384
384
|
if (nodeCount > 100) {
|
|
385
|
-
// Pre-resolve top-
|
|
385
|
+
// Pre-resolve top-3 relevant files for the user's prompt — the LLM
|
|
386
386
|
// sees the answer inline instead of being told to call a tool.
|
|
387
|
-
|
|
387
|
+
// 3 is enough signal; more files inflate token cost on every prompt.
|
|
388
|
+
var suggestions = hCtx.getMonographSuggestions(prompt, 3);
|
|
388
389
|
|
|
389
390
|
// Boost recently-edited files to the top of pre-resolve suggestions.
|
|
390
391
|
// Even when the FTS index hasn't caught up to the latest edits, the
|
|
@@ -414,25 +415,24 @@ module.exports = {
|
|
|
414
415
|
}
|
|
415
416
|
}
|
|
416
417
|
if (editBoosts.length > 0) {
|
|
417
|
-
suggestions = editBoosts.concat(suggestions).slice(0,
|
|
418
|
+
suggestions = editBoosts.concat(suggestions).slice(0, 3);
|
|
418
419
|
}
|
|
419
420
|
}
|
|
420
421
|
} catch (e) { /* non-fatal */ }
|
|
421
422
|
|
|
422
423
|
if (suggestions.length > 0) {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
424
|
+
// Compact single-line format: "[MONOGRAPH] N nodes. Top files: name [Label] — path:line, ..."
|
|
425
|
+
// Matches the agent-start-handler pattern — keeps per-prompt token cost minimal.
|
|
426
|
+
var hintParts = suggestions.map(function(s) {
|
|
426
427
|
var editTag = s._editBoost ? ' ✎' : '';
|
|
427
|
-
// Include :line suffix when available so LLM can navigate directly
|
|
428
428
|
var fileLoc = (s.file || '');
|
|
429
429
|
if (fileLoc && s.startLine != null) fileLoc = fileLoc + ':' + s.startLine;
|
|
430
|
-
|
|
431
|
-
}
|
|
432
|
-
console.log('
|
|
430
|
+
return s.name + ' [' + s.label + '] — ' + fileLoc + editTag;
|
|
431
|
+
});
|
|
432
|
+
console.log('[MONOGRAPH] ' + nodeCount + ' nodes. Top files: ' + hintParts.join(' · '));
|
|
433
433
|
hCtx._recordGraphTelemetry('preresolve_hit');
|
|
434
434
|
} else {
|
|
435
|
-
console.log('
|
|
435
|
+
console.log('[MONOGRAPH] ' + nodeCount + ' nodes. Call mcp__monomind__monograph_suggest to find relevant files.');
|
|
436
436
|
hCtx._recordGraphTelemetry('preresolve_miss');
|
|
437
437
|
}
|
|
438
438
|
}
|
|
@@ -99,17 +99,33 @@ const [,, command, ...args] = process.argv;
|
|
|
99
99
|
// Read stdin — Claude Code sends hook data as JSON via stdin
|
|
100
100
|
// Uses a timeout to prevent hanging when stdin is in an ambiguous state
|
|
101
101
|
// (not TTY, not a proper pipe) which happens with Claude Code hook invocations.
|
|
102
|
+
// Hard cap at 1 MiB: a legitimate hook payload (tool name + input) is at most
|
|
103
|
+
// a few KB; anything larger is either a bug or an adversarial OOM attempt.
|
|
104
|
+
const MAX_STDIN_BYTES = 1 * 1024 * 1024; // 1 MiB
|
|
102
105
|
async function readStdin() {
|
|
103
106
|
if (process.stdin.isTTY) return '';
|
|
104
107
|
return new Promise((resolve) => {
|
|
105
108
|
let data = '';
|
|
109
|
+
let byteCount = 0;
|
|
110
|
+
let truncated = false;
|
|
106
111
|
const timer = setTimeout(() => {
|
|
107
112
|
process.stdin.removeAllListeners();
|
|
108
113
|
process.stdin.pause();
|
|
109
114
|
resolve(data);
|
|
110
115
|
}, 500);
|
|
111
116
|
process.stdin.setEncoding('utf8');
|
|
112
|
-
process.stdin.on('data', (chunk) => {
|
|
117
|
+
process.stdin.on('data', (chunk) => {
|
|
118
|
+
if (truncated) return;
|
|
119
|
+
byteCount += Buffer.byteLength(chunk, 'utf8');
|
|
120
|
+
if (byteCount > MAX_STDIN_BYTES) {
|
|
121
|
+
truncated = true;
|
|
122
|
+
process.stdin.pause();
|
|
123
|
+
clearTimeout(timer);
|
|
124
|
+
resolve(''); // discard oversized input to prevent OOM
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
data += chunk;
|
|
128
|
+
});
|
|
113
129
|
process.stdin.on('end', () => { clearTimeout(timer); resolve(data); });
|
|
114
130
|
process.stdin.on('error', () => { clearTimeout(timer); resolve(data); });
|
|
115
131
|
process.stdin.resume();
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
/**
|
|
3
3
|
* Session state management for hook-handler.cjs
|
|
4
|
-
* Persists session data to .monomind/sessions/current.json
|
|
4
|
+
* Persists session data to .git/monomind/sessions/current.json (branch-agnostic,
|
|
5
|
+
* shared across git worktrees). Falls back to .monomind/sessions/ if not in a git repo.
|
|
5
6
|
*
|
|
6
7
|
* API: start(), restore(), end(), status(), metric(key), update(patch)
|
|
7
8
|
*/
|
|
@@ -10,7 +11,24 @@ const path = require('path');
|
|
|
10
11
|
const fs = require('fs');
|
|
11
12
|
|
|
12
13
|
const CWD = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
13
|
-
|
|
14
|
+
|
|
15
|
+
function getMonoDir(workDir) {
|
|
16
|
+
try {
|
|
17
|
+
const gitEntry = path.join(workDir, '.git');
|
|
18
|
+
const st = fs.statSync(gitEntry);
|
|
19
|
+
if (st.isDirectory()) return path.join(gitEntry, 'monomind');
|
|
20
|
+
if (st.isFile()) {
|
|
21
|
+
const m = fs.readFileSync(gitEntry, 'utf8').match(/^gitdir:\s*(.+)/m);
|
|
22
|
+
if (m) {
|
|
23
|
+
const worktreeDir = path.resolve(workDir, m[1].trim());
|
|
24
|
+
return path.join(path.dirname(path.dirname(worktreeDir)), 'monomind');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
} catch {}
|
|
28
|
+
return path.join(workDir, '.monomind');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const SESSIONS_DIR = path.join(getMonoDir(CWD), 'sessions');
|
|
14
32
|
const CURRENT_FILE = path.join(SESSIONS_DIR, 'current.json');
|
|
15
33
|
|
|
16
34
|
var KNOWN_METRICS = new Set(['edits', 'commands', 'tasks', 'errors']);
|