monomind 2.1.6 → 2.1.8
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/package.json +1 -1
- package/packages/@monomind/cli/.claude/helpers/handlers/capture-handler.cjs +26 -10
- package/packages/@monomind/cli/.claude/helpers/handlers/session-restore-handler.cjs +68 -1
- package/packages/@monomind/cli/dist/src/commands/doctor-project-checks.js +39 -8
- package/packages/@monomind/cli/dist/src/commands/doctor.js +10 -3
- package/packages/@monomind/cli/dist/src/init/executor.js +9 -24
- package/packages/@monomind/cli/dist/src/init/helpers-generator.d.ts +13 -0
- package/packages/@monomind/cli/dist/src/init/helpers-generator.js +28 -0
- package/packages/@monomind/cli/dist/src/mcp-tools/monograph-tools.js +28 -38
- package/packages/@monomind/cli/dist/src/ui/dashboard.html +28 -13
- package/packages/@monomind/cli/dist/src/ui/mastermind-diagram-fallback.html +1069 -0
- package/packages/@monomind/cli/dist/src/ui/orgs.html +36 -2
- package/packages/@monomind/cli/dist/src/ui/server.mjs +78 -31
- package/packages/@monomind/cli/dist/src/utils/input-guards.d.ts +6 -0
- package/packages/@monomind/cli/dist/src/utils/input-guards.js +6 -0
- package/packages/@monomind/cli/package.json +4 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "monomind",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.8",
|
|
4
4
|
"description": "Open-source CLI extension for Claude Code. Adds an MCP server with a codebase knowledge graph, persistent memory, multi-agent coordination, and reusable slash commands. MIT licensed, runs locally, no data leaves your machine.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -343,22 +343,38 @@ async function handleSubagentStop(hookInput) {
|
|
|
343
343
|
const currentFiles = snapshotJSONLFiles();
|
|
344
344
|
const prevNames = new Set((snap.files || []).map(f => f.name));
|
|
345
345
|
|
|
346
|
+
// Prefer reading THIS subagent's own transcript directly when known —
|
|
347
|
+
// transcript_path is unique per-subagent (see subagentKey() above). The
|
|
348
|
+
// "every file that's new in the directory since snapshot" fallback below
|
|
349
|
+
// is not actually scoped to this subagent: under real concurrency (this
|
|
350
|
+
// project's default hierarchical-swarm topology spawns several subagents
|
|
351
|
+
// together), a sibling subagent's transcript file created between THIS
|
|
352
|
+
// subagent's start-snapshot and its own stop event also looks "new" and
|
|
353
|
+
// would get folded into this subagent's totals/summary/lastToolError —
|
|
354
|
+
// misattributing a sibling's success/failure to this one. Falls back to
|
|
355
|
+
// the directory-wide diff only when transcript_path is unavailable
|
|
356
|
+
// (older Claude Code payload shape), matching this file's existing
|
|
357
|
+
// fallback philosophy for snapshot matching above.
|
|
358
|
+
const _ownTranscriptRaw = hookInput && (hookInput.transcript_path || hookInput.transcriptPath);
|
|
359
|
+
const _ownTranscriptName = _ownTranscriptRaw ? path.basename(String(_ownTranscriptRaw)) : null;
|
|
360
|
+
const filesToProcess = _ownTranscriptName
|
|
361
|
+
? currentFiles.filter(f => f.name === _ownTranscriptName)
|
|
362
|
+
: currentFiles.filter(f => !prevNames.has(f.name));
|
|
363
|
+
|
|
346
364
|
let totalTin = 0, totalTout = 0;
|
|
347
365
|
let summary = '';
|
|
348
366
|
let toolCalls = [];
|
|
349
367
|
let lastToolError = false;
|
|
350
368
|
const capturedFiles = [];
|
|
351
369
|
|
|
352
|
-
for (const f of
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
capturedFiles.push(f.name);
|
|
361
|
-
}
|
|
370
|
+
for (const f of filesToProcess) {
|
|
371
|
+
const parsed = parseJSONLForData(path.join(claudeDir, f.name));
|
|
372
|
+
totalTin += parsed.tokens_in;
|
|
373
|
+
totalTout += parsed.tokens_out;
|
|
374
|
+
if (parsed.summary) summary = parsed.summary;
|
|
375
|
+
toolCalls.push(...parsed.toolCalls);
|
|
376
|
+
lastToolError = parsed.lastToolError; // last (only, when scoped) file wins
|
|
377
|
+
capturedFiles.push(f.name);
|
|
362
378
|
}
|
|
363
379
|
|
|
364
380
|
const costUsd = parseFloat((totalTin * 3e-6 + totalTout * 15e-6).toFixed(6));
|
|
@@ -53,7 +53,26 @@ module.exports = {
|
|
|
53
53
|
// any diff vs. the npm global install is expected (and would self-clobber
|
|
54
54
|
// in-progress edits).
|
|
55
55
|
try {
|
|
56
|
-
|
|
56
|
+
// Walk up from CWD, not just check CWD directly — Claude Code can be opened
|
|
57
|
+
// with CWD set to any subdirectory of the monorepo (e.g. packages/@monomind/cli
|
|
58
|
+
// itself, or a nested package), where a bare CWD-only check would miss the
|
|
59
|
+
// monorepo root and wrongly treat the dev repo as a regular consumer project,
|
|
60
|
+
// letting the heal logic below silently overwrite the actual dev-repo SOURCE
|
|
61
|
+
// helpers (packages/@monomind/cli/.claude/helpers/*) with a stale published
|
|
62
|
+
// version pulled from node_modules/global npm.
|
|
63
|
+
var _isDevRepo = (function() {
|
|
64
|
+
var dir = CWD;
|
|
65
|
+
for (var _d = 0; _d < 6; _d++) {
|
|
66
|
+
if (fs.existsSync(path.join(dir, 'packages', '@monomind', 'cli', 'package.json')) &&
|
|
67
|
+
fs.existsSync(path.join(dir, 'packages', '@monomind', 'cli', '.claude', 'helpers'))) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
var _parent = path.dirname(dir);
|
|
71
|
+
if (_parent === dir) break;
|
|
72
|
+
dir = _parent;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
})();
|
|
57
76
|
if (!_isDevRepo) {
|
|
58
77
|
var crypto = require('crypto');
|
|
59
78
|
function _findBundledHelpers() {
|
|
@@ -136,6 +155,54 @@ module.exports = {
|
|
|
136
155
|
console.log('[STALE_HELPERS] Refreshed ' + healed.length + ' helper(s) from bundled version: ' + healed.join(', '));
|
|
137
156
|
}
|
|
138
157
|
}
|
|
158
|
+
|
|
159
|
+
// Fallback for pure `npx monomind@latest ...` usage — the local scan above
|
|
160
|
+
// only finds a bundled copy via node_modules or a global npm install, but a
|
|
161
|
+
// per-invocation `npx` run leaves no reliably-discoverable copy there (each
|
|
162
|
+
// invocation caches under a hashed, non-predictable ~/.npm/_npx/<hash>/ dir
|
|
163
|
+
// that this process has no correct way to pick the newest of without risking
|
|
164
|
+
// healing "backward" to some older cached version). `npx monomind@latest`
|
|
165
|
+
// itself already resolves this correctly (it always fetches/uses latest),
|
|
166
|
+
// and `doctor --fix` reuses the exact same bundled-package resolution that
|
|
167
|
+
// `init upgrade` uses — so shell out to it instead of re-solving the same
|
|
168
|
+
// problem here. Rate-limited to once per 6h (mirrors the metrics-worker
|
|
169
|
+
// staleness gate below) and fully non-blocking: spawned detached+unref with
|
|
170
|
+
// stdio ignored, session start never waits on it, and a fresh session or
|
|
171
|
+
// hook a few seconds later just picks up whatever it left behind.
|
|
172
|
+
try {
|
|
173
|
+
var _healCheckPath = path.join(CWD, '.monomind', 'helpers-heal-check.json');
|
|
174
|
+
var _lastHealCheck = 0;
|
|
175
|
+
try { _lastHealCheck = JSON.parse(fs.readFileSync(_healCheckPath, 'utf-8')).ts || 0; } catch (_) {}
|
|
176
|
+
var HEAL_CHECK_STALE_MS = 6 * 60 * 60 * 1000; // 6 hours
|
|
177
|
+
if (Date.now() - _lastHealCheck > HEAL_CHECK_STALE_MS) {
|
|
178
|
+
// Write the rate-limit marker BEFORE spawning, and only spawn if the
|
|
179
|
+
// write actually succeeded (fail closed) — otherwise a persistently
|
|
180
|
+
// unwritable marker (disk quota, permissions) would make this branch
|
|
181
|
+
// re-fire on every single session-restore instead of once per 6h,
|
|
182
|
+
// since `Date.now() - 0 > HEAL_CHECK_STALE_MS` stays true forever.
|
|
183
|
+
var _markerWritten = false;
|
|
184
|
+
try {
|
|
185
|
+
fs.mkdirSync(path.dirname(_healCheckPath), { recursive: true });
|
|
186
|
+
fs.writeFileSync(_healCheckPath, JSON.stringify({ ts: Date.now() }), 'utf-8');
|
|
187
|
+
_markerWritten = true;
|
|
188
|
+
} catch (_) {}
|
|
189
|
+
if (_markerWritten) {
|
|
190
|
+
var _spawn = require('child_process').spawn;
|
|
191
|
+
// shell:true is required on Windows to invoke npx.cmd via spawn()
|
|
192
|
+
// without an explicit .cmd extension; harmless on macOS/Linux.
|
|
193
|
+
var _child = _spawn('npx', ['-y', 'monomind@latest', 'doctor', '--fix', '--component', 'helpers'], {
|
|
194
|
+
cwd: CWD,
|
|
195
|
+
detached: true,
|
|
196
|
+
stdio: 'ignore',
|
|
197
|
+
env: process.env,
|
|
198
|
+
shell: process.platform === 'win32',
|
|
199
|
+
windowsHide: true,
|
|
200
|
+
});
|
|
201
|
+
_child.on('error', function() {}); // offline / npx unavailable — silently skip
|
|
202
|
+
_child.unref();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
} catch (e) { /* non-fatal — background self-heal is best-effort */ }
|
|
139
206
|
}
|
|
140
207
|
} catch (e) { /* non-fatal */ }
|
|
141
208
|
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
* Doctor — project/monomind health checks
|
|
3
3
|
* Config, memory, API keys, MCP, monograph, helpers, routing, gates, gitignore, worker metrics
|
|
4
4
|
*/
|
|
5
|
-
import { existsSync, readFileSync, statSync, mkdirSync, copyFileSync } from 'fs';
|
|
5
|
+
import { existsSync, readFileSync, readdirSync, statSync, mkdirSync, copyFileSync } from 'fs';
|
|
6
6
|
import { join, dirname } from 'path';
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
8
|
import { execSync } from 'child_process';
|
|
9
9
|
import { homedir } from 'os';
|
|
10
10
|
import { MAX_DOCTOR_PKG_BYTES, MAX_DOCTOR_CONFIG_BYTES, MAX_DOCTOR_GITIGNORE_BYTES, MAX_DOCTOR_HELPER_BYTES, } from './doctor-env-checks.js';
|
|
11
|
+
import { DOCTOR_TRACKED_HELPERS } from '../init/helpers-generator.js';
|
|
11
12
|
export async function checkConfigFile() {
|
|
12
13
|
const jsonPaths = ['.monomind/config.json', 'monomind.config.json', '.monomind.json'];
|
|
13
14
|
for (const configPath of jsonPaths) {
|
|
@@ -234,21 +235,50 @@ function _resolveBundledHelper(relativePath) {
|
|
|
234
235
|
return null;
|
|
235
236
|
}
|
|
236
237
|
}
|
|
238
|
+
// Top-level critical helpers, plus every file bundled under handlers/ and utils/
|
|
239
|
+
// (capture-handler.cjs, gates-handler.cjs, route-handler.cjs, session-handler.cjs,
|
|
240
|
+
// etc. — where most actual hook-behavior fixes live). Subdir membership is
|
|
241
|
+
// discovered from the bundled package itself, not hardcoded, so a helper added
|
|
242
|
+
// upstream is picked up automatically instead of silently never syncing.
|
|
243
|
+
function _allTrackedHelperNames() {
|
|
244
|
+
const names = [...DOCTOR_TRACKED_HELPERS];
|
|
245
|
+
for (const sub of ['handlers', 'utils']) {
|
|
246
|
+
const bundledSubDir = _resolveBundledHelper(join('.claude', 'helpers', sub));
|
|
247
|
+
if (!bundledSubDir)
|
|
248
|
+
continue;
|
|
249
|
+
try {
|
|
250
|
+
for (const f of readdirSync(bundledSubDir)) {
|
|
251
|
+
if (f.startsWith('._'))
|
|
252
|
+
continue;
|
|
253
|
+
if (statSync(join(bundledSubDir, f)).isDirectory())
|
|
254
|
+
continue;
|
|
255
|
+
names.push(join(sub, f));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
catch { /* skip */ }
|
|
259
|
+
}
|
|
260
|
+
return names;
|
|
261
|
+
}
|
|
237
262
|
async function _detectStaleHelpers() {
|
|
238
263
|
const stale = [];
|
|
239
264
|
const missing = [];
|
|
240
|
-
const helpers = ['hook-handler.cjs', 'statusline.cjs', 'router.cjs', 'graphify-freshen.cjs'];
|
|
241
265
|
const crypto = await import('node:crypto');
|
|
242
|
-
for (const name of
|
|
243
|
-
const local = join(process.cwd(), '.claude', 'helpers', name);
|
|
244
|
-
if (!existsSync(local) || statSync(local).size > MAX_DOCTOR_HELPER_BYTES)
|
|
245
|
-
continue;
|
|
266
|
+
for (const name of _allTrackedHelperNames()) {
|
|
246
267
|
const bundled = _resolveBundledHelper(join('.claude', 'helpers', name));
|
|
247
|
-
|
|
268
|
+
// Oversized-bundled is reported the same as missing-bundled: doctor can't
|
|
269
|
+
// verify freshness either way, and silently excluding it from both `stale`
|
|
270
|
+
// and `missing` would let checkHelpersFresh() report "pass" without ever
|
|
271
|
+
// having actually compared this file.
|
|
272
|
+
if (!bundled || statSync(bundled).size > MAX_DOCTOR_HELPER_BYTES) {
|
|
248
273
|
missing.push(name);
|
|
249
274
|
continue;
|
|
250
275
|
}
|
|
251
|
-
|
|
276
|
+
const local = join(process.cwd(), '.claude', 'helpers', name);
|
|
277
|
+
if (!existsSync(local)) {
|
|
278
|
+
stale.push(name);
|
|
279
|
+
continue;
|
|
280
|
+
} // bundled has it, project doesn't — needs creating
|
|
281
|
+
if (statSync(local).size > MAX_DOCTOR_HELPER_BYTES)
|
|
252
282
|
continue;
|
|
253
283
|
try {
|
|
254
284
|
const hashLocal = crypto.createHash('sha256').update(readFileSync(local)).digest('hex');
|
|
@@ -268,6 +298,7 @@ export async function fixStaleHelpers() {
|
|
|
268
298
|
const bundled = _resolveBundledHelper(join('.claude', 'helpers', name));
|
|
269
299
|
if (bundled) {
|
|
270
300
|
try {
|
|
301
|
+
mkdirSync(dirname(local), { recursive: true });
|
|
271
302
|
copyFileSync(bundled, local);
|
|
272
303
|
fixed++;
|
|
273
304
|
}
|
|
@@ -19,7 +19,7 @@ export const doctorCommand = {
|
|
|
19
19
|
name: 'doctor',
|
|
20
20
|
description: 'System diagnostics and health checks',
|
|
21
21
|
options: [
|
|
22
|
-
{ name: 'fix', short: 'f', description: '
|
|
22
|
+
{ name: 'fix', short: 'f', description: 'Apply local fixes (helper files, monoes tool shims) and show fix commands for the rest', type: 'boolean', default: false },
|
|
23
23
|
{ name: 'install', short: 'i', description: 'Auto-install missing dependencies (Claude Code CLI)', type: 'boolean', default: false },
|
|
24
24
|
{
|
|
25
25
|
name: 'component', short: 'c',
|
|
@@ -122,9 +122,16 @@ export const doctorCommand = {
|
|
|
122
122
|
spinner.stop();
|
|
123
123
|
output.writeln(output.error('Failed to run health checks'));
|
|
124
124
|
}
|
|
125
|
-
|
|
125
|
+
// `--fix` applies the lightweight, local, no-network fixes (helper files,
|
|
126
|
+
// monoes CLI tool shims) — its description says "show fix commands" but
|
|
127
|
+
// silently doing nothing for these two beyond printing a hint is a worse
|
|
128
|
+
// outcome than just fixing them, and copying a bundled file locally is
|
|
129
|
+
// nothing like installing a package. `--install` additionally covers the
|
|
130
|
+
// Claude Code CLI, which is a real install (network fetch + binary setup)
|
|
131
|
+
// — kept opt-in separately so `--fix` alone never triggers that.
|
|
132
|
+
if (autoInstall || showFix) {
|
|
126
133
|
const claudeResult = results.find(r => r.name === 'Claude Code CLI');
|
|
127
|
-
if (claudeResult && claudeResult.status !== 'pass') {
|
|
134
|
+
if (autoInstall && claudeResult && claudeResult.status !== 'pass') {
|
|
128
135
|
if (await installClaudeCode()) {
|
|
129
136
|
const newCheck = await checkClaudeCode();
|
|
130
137
|
const idx = results.findIndex(r => r.name === 'Claude Code CLI');
|
|
@@ -52,7 +52,7 @@ import { writeSharedInstructions } from './shared-instructions-generator.js';
|
|
|
52
52
|
import { generateSettingsJson, generateSettings } from './settings-generator.js';
|
|
53
53
|
import { generateMCPJson } from './mcp-generator.js';
|
|
54
54
|
import { generateStatuslineScript } from './statusline-generator.js';
|
|
55
|
-
import {
|
|
55
|
+
import { FORCE_SYNC_HELPERS, FORCE_SYNC_GENERATORS, INIT_FALLBACK_HELPERS, } from './helpers-generator.js';
|
|
56
56
|
import { generateClaudeMd } from './claudemd-generator.js';
|
|
57
57
|
/**
|
|
58
58
|
* Skills to copy based on configuration
|
|
@@ -573,15 +573,13 @@ export async function executeUpgrade(targetDir, upgradeSettings = false) {
|
|
|
573
573
|
const sourceHelpersForUpgrade = findSourceHelpersDir();
|
|
574
574
|
if (sourceHelpersForUpgrade) {
|
|
575
575
|
const destHelpersDir = path.join(targetDir, '.claude', 'helpers');
|
|
576
|
-
// Copy top-level critical files atomically
|
|
577
|
-
|
|
576
|
+
// Copy top-level critical files atomically. Membership and fallback
|
|
577
|
+
// generators come from the shared HELPER_FILES registry (helpers-generator.ts)
|
|
578
|
+
// rather than a hardcoded list here — see that file's comment for why.
|
|
579
|
+
const criticalHelpers = FORCE_SYNC_HELPERS;
|
|
578
580
|
// Generated fallback for any critical helper missing from the source dir itself
|
|
579
581
|
// (e.g. the published npm template lacking auto-memory-hook.mjs).
|
|
580
|
-
const criticalGenerators =
|
|
581
|
-
'hook-handler.cjs': generateHookHandler,
|
|
582
|
-
'intelligence.cjs': generateIntelligenceStub,
|
|
583
|
-
'auto-memory-hook.mjs': generateAutoMemoryHook,
|
|
584
|
-
};
|
|
582
|
+
const criticalGenerators = FORCE_SYNC_GENERATORS;
|
|
585
583
|
for (const helperName of criticalHelpers) {
|
|
586
584
|
const targetPath = path.join(destHelpersDir, helperName);
|
|
587
585
|
const sourcePath = path.join(sourceHelpersForUpgrade, helperName);
|
|
@@ -626,12 +624,8 @@ export async function executeUpgrade(targetDir, upgradeSettings = false) {
|
|
|
626
624
|
}
|
|
627
625
|
else {
|
|
628
626
|
// Source not found (npx with broken paths) — use generated fallbacks
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
'intelligence.cjs': generateIntelligenceStub(),
|
|
632
|
-
'auto-memory-hook.mjs': generateAutoMemoryHook(),
|
|
633
|
-
'router.cjs': generateAgentRouter(),
|
|
634
|
-
};
|
|
627
|
+
// for every force-synced helper that has one (see HELPER_FILES registry).
|
|
628
|
+
const generatedCritical = Object.fromEntries(Object.entries(FORCE_SYNC_GENERATORS).map(([name, generate]) => [name, generate()]));
|
|
635
629
|
for (const [helperName, content] of Object.entries(generatedCritical)) {
|
|
636
630
|
const targetPath = path.join(targetDir, '.claude', 'helpers', helperName);
|
|
637
631
|
if (fs.existsSync(targetPath)) {
|
|
@@ -1274,16 +1268,7 @@ async function writeHelpers(targetDir, options, result) {
|
|
|
1274
1268
|
// Without this, a source dir that's present but incomplete (e.g. missing
|
|
1275
1269
|
// auto-memory-hook.mjs) silently ships a project wired to hooks that reference
|
|
1276
1270
|
// a file that was never installed.
|
|
1277
|
-
const helpers =
|
|
1278
|
-
'pre-commit': generatePreCommitHook(),
|
|
1279
|
-
'post-commit': generatePostCommitHook(),
|
|
1280
|
-
'session.cjs': generateSessionManager(),
|
|
1281
|
-
'router.cjs': generateAgentRouter(),
|
|
1282
|
-
'memory.cjs': generateMemoryHelper(),
|
|
1283
|
-
'hook-handler.cjs': generateHookHandler(),
|
|
1284
|
-
'intelligence.cjs': generateIntelligenceStub(),
|
|
1285
|
-
'auto-memory-hook.mjs': generateAutoMemoryHook(),
|
|
1286
|
-
};
|
|
1271
|
+
const helpers = Object.fromEntries(Object.entries(INIT_FALLBACK_HELPERS).map(([name, generate]) => [name, generate()]));
|
|
1287
1272
|
for (const [name, content] of Object.entries(helpers)) {
|
|
1288
1273
|
const filePath = path.join(helpersDir, name);
|
|
1289
1274
|
// If the source dir has this file, copyRecursive above already applied
|
|
@@ -53,6 +53,19 @@ export declare function generateWindowsBatchWrapper(): string;
|
|
|
53
53
|
* Generate cross-platform session manager
|
|
54
54
|
*/
|
|
55
55
|
export declare function generateCrossPlatformSessionManager(): string;
|
|
56
|
+
export interface HelperFileSpec {
|
|
57
|
+
/** Force-overwritten from source (or regenerated as fallback) on every `init upgrade` run. */
|
|
58
|
+
forceSync?: boolean;
|
|
59
|
+
/** Checked for staleness/existence by `doctor` (in addition to everything under handlers/ and utils/, discovered dynamically). */
|
|
60
|
+
doctorTracked?: boolean;
|
|
61
|
+
/** Fallback content generator, used when the file can't be copied from source. */
|
|
62
|
+
generate?: () => string;
|
|
63
|
+
}
|
|
64
|
+
export declare const HELPER_FILES: Record<string, HelperFileSpec>;
|
|
65
|
+
export declare const FORCE_SYNC_HELPERS: string[];
|
|
66
|
+
export declare const DOCTOR_TRACKED_HELPERS: string[];
|
|
67
|
+
export declare const FORCE_SYNC_GENERATORS: Record<string, () => string>;
|
|
68
|
+
export declare const INIT_FALLBACK_HELPERS: Record<string, () => string>;
|
|
56
69
|
/**
|
|
57
70
|
* Generate all helper files
|
|
58
71
|
*/
|
|
@@ -1213,6 +1213,34 @@ if (command && commands[command]) {
|
|
|
1213
1213
|
module.exports = commands;
|
|
1214
1214
|
`;
|
|
1215
1215
|
}
|
|
1216
|
+
export const HELPER_FILES = {
|
|
1217
|
+
'hook-handler.cjs': { forceSync: true, doctorTracked: true, generate: generateHookHandler },
|
|
1218
|
+
'intelligence.cjs': { forceSync: true, generate: generateIntelligenceStub },
|
|
1219
|
+
'auto-memory-hook.mjs': { forceSync: true, generate: generateAutoMemoryHook },
|
|
1220
|
+
'statusline.cjs': { forceSync: true, doctorTracked: true },
|
|
1221
|
+
'graphify-freshen.cjs': { forceSync: true, doctorTracked: true },
|
|
1222
|
+
'router.cjs': { forceSync: true, doctorTracked: true, generate: generateAgentRouter },
|
|
1223
|
+
'memory.cjs': { generate: generateMemoryHelper },
|
|
1224
|
+
'session.cjs': { generate: generateSessionManager },
|
|
1225
|
+
'pre-commit': { generate: generatePreCommitHook },
|
|
1226
|
+
'post-commit': { generate: generatePostCommitHook },
|
|
1227
|
+
};
|
|
1228
|
+
export const FORCE_SYNC_HELPERS = Object.keys(HELPER_FILES).filter((name) => HELPER_FILES[name].forceSync);
|
|
1229
|
+
export const DOCTOR_TRACKED_HELPERS = Object.keys(HELPER_FILES).filter((name) => HELPER_FILES[name].doctorTracked);
|
|
1230
|
+
// Fallback generators for force-synced helpers (used when source is missing
|
|
1231
|
+
// the file entirely). Previously duplicated by hand as two separate maps
|
|
1232
|
+
// that disagreed on membership — the "source found but file missing" path
|
|
1233
|
+
// lacked a router.cjs fallback that the "source not found" path had, so a
|
|
1234
|
+
// corrupted/incomplete source copy could silently ship without router.cjs.
|
|
1235
|
+
export const FORCE_SYNC_GENERATORS = Object.fromEntries(Object.entries(HELPER_FILES)
|
|
1236
|
+
.filter(([, spec]) => spec.forceSync && spec.generate)
|
|
1237
|
+
.map(([name, spec]) => [name, spec.generate]));
|
|
1238
|
+
// Broader fallback-if-missing-after-copy step (used during a fresh `init`,
|
|
1239
|
+
// not `init upgrade`) — includes non-force-synced scaffold files too
|
|
1240
|
+
// (pre-commit/post-commit/session/memory).
|
|
1241
|
+
export const INIT_FALLBACK_HELPERS = Object.fromEntries(Object.entries(HELPER_FILES)
|
|
1242
|
+
.filter(([, spec]) => spec.generate)
|
|
1243
|
+
.map(([name, spec]) => [name, spec.generate]));
|
|
1216
1244
|
/**
|
|
1217
1245
|
* Generate all helper files
|
|
1218
1246
|
*/
|
|
@@ -483,20 +483,22 @@ const monographSurprisesTool = {
|
|
|
483
483
|
// ── monograph_suggest ─────────────────────────────────────────────────────────
|
|
484
484
|
const monographSuggestTool = {
|
|
485
485
|
name: 'monograph_suggest',
|
|
486
|
-
description: 'Get graph-topology-derived questions to explore the codebase. Pass task= to score by task relevance
|
|
486
|
+
description: 'Get graph-topology-derived questions to explore the codebase. Pass task= to score by task relevance via BM25/FTS5.',
|
|
487
487
|
inputSchema: {
|
|
488
488
|
type: 'object',
|
|
489
489
|
properties: {
|
|
490
490
|
task: { type: 'string', description: 'Optional task description for task-relevance scoring' },
|
|
491
491
|
limit: { type: 'number', description: 'Max questions (default 10)' },
|
|
492
|
-
checkStaleness: { type: 'boolean', description: 'Check index staleness first and trigger a background rebuild when the index is behind HEAD. Appends a _staleness annotation to the result. (default false)' },
|
|
492
|
+
checkStaleness: { type: 'boolean', description: 'Check index staleness first and trigger a background rebuild when the index is behind HEAD. Appends a _staleness annotation to the result. (default true — pass false to skip the git check)' },
|
|
493
493
|
},
|
|
494
494
|
},
|
|
495
495
|
handler: async (input) => {
|
|
496
496
|
// Health-aware mode (formerly monograph_suggest_auto): check staleness and
|
|
497
|
-
// trigger a background rebuild if the index is behind HEAD.
|
|
497
|
+
// trigger a background rebuild if the index is behind HEAD. Defaults on
|
|
498
|
+
// (opt-out, not opt-in) — a caller that never checked was exactly how a
|
|
499
|
+
// stale graph kept serving results silently.
|
|
498
500
|
let stalenessAnnotation = '';
|
|
499
|
-
if (input.checkStaleness
|
|
501
|
+
if (input.checkStaleness !== false) {
|
|
500
502
|
const repoPath = getProjectCwd();
|
|
501
503
|
const stalenessResult = await computeCommitsBehind(repoPath);
|
|
502
504
|
const commitsBehind = stalenessResult?.commitsBehind ?? 0;
|
|
@@ -531,29 +533,23 @@ const monographSuggestTool = {
|
|
|
531
533
|
const locHint = srcLoc ? ` [${srcLoc}${tgtLoc ? ` → ${tgtLoc}` : ''}]` : '';
|
|
532
534
|
return `Why does ${r.src} ${r.relation.toLowerCase()} ${r.tgt}? (${r.confidence})${locHint}`;
|
|
533
535
|
};
|
|
534
|
-
// When a task is provided
|
|
535
|
-
//
|
|
536
|
-
|
|
536
|
+
// When a task is provided, use BM25/FTS5 (via hybridQuery) to find
|
|
537
|
+
// relevant nodes and restrict the edge-level questions to them. This
|
|
538
|
+
// used to be gated behind MONOGRAPH_EMBEDDINGS=true, but hybridQuery
|
|
539
|
+
// is BM25-only now (the embeddings table stayed empty in practice —
|
|
540
|
+
// see hybrid-query.ts) so the env var gated nothing and just left this
|
|
541
|
+
// better-ranked path off unless a caller happened to know about it.
|
|
542
|
+
let hitIds = [];
|
|
543
|
+
if (task) {
|
|
537
544
|
const hits = await hybridQuery(db, task, { limit: 20 });
|
|
538
|
-
|
|
539
|
-
if (hitIds.
|
|
545
|
+
hitIds = [...new Set(hits.map(h => h.id))];
|
|
546
|
+
if (hitIds.length === 0) {
|
|
540
547
|
return text('No suggestions for this task. Run monograph_build first or try a different query.' + stalenessAnnotation);
|
|
541
548
|
}
|
|
542
|
-
const rows = db.prepare(`
|
|
543
|
-
SELECT e.relation, e.confidence, n1.name as src, n2.name as tgt,
|
|
544
|
-
n1.file_path as src_file, n1.start_line as src_line,
|
|
545
|
-
n2.file_path as tgt_file, n2.start_line as tgt_line
|
|
546
|
-
FROM edges e
|
|
547
|
-
JOIN nodes n1 ON n1.id = e.source_id
|
|
548
|
-
JOIN nodes n2 ON n2.id = e.target_id
|
|
549
|
-
WHERE (e.source_id IN (${[...hitIds].map(() => '?').join(',')})
|
|
550
|
-
OR e.target_id IN (${[...hitIds].map(() => '?').join(',')}))
|
|
551
|
-
AND e.confidence IN ('AMBIGUOUS', 'INFERRED')
|
|
552
|
-
LIMIT 100
|
|
553
|
-
`).all(...[...hitIds], ...[...hitIds]);
|
|
554
|
-
const questions = rows.map(formatSuggestion);
|
|
555
|
-
return text((questions.slice(0, limit).join('\n') || 'No suggestions for this task. Run monograph_build first.') + stalenessAnnotation);
|
|
556
549
|
}
|
|
550
|
+
const taskFilter = hitIds.length
|
|
551
|
+
? `AND (e.source_id IN (${hitIds.map(() => '?').join(',')}) OR e.target_id IN (${hitIds.map(() => '?').join(',')}))`
|
|
552
|
+
: '';
|
|
557
553
|
const rows = db.prepare(`
|
|
558
554
|
SELECT e.relation, e.confidence, n1.name as src, n2.name as tgt,
|
|
559
555
|
n1.file_path as src_file, n1.start_line as src_line,
|
|
@@ -562,26 +558,18 @@ const monographSuggestTool = {
|
|
|
562
558
|
JOIN nodes n1 ON n1.id = e.source_id
|
|
563
559
|
JOIN nodes n2 ON n2.id = e.target_id
|
|
564
560
|
WHERE e.confidence IN ('AMBIGUOUS', 'INFERRED')
|
|
561
|
+
${taskFilter}
|
|
565
562
|
LIMIT 100
|
|
566
|
-
`).all();
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
}));
|
|
571
|
-
if (task)
|
|
572
|
-
scored = scored.sort((a, b) => b.relevance - a.relevance);
|
|
573
|
-
return text((scored.slice(0, limit).map(s => s.q).join('\n') || 'No suggestions. Run monograph_build first.') + stalenessAnnotation);
|
|
563
|
+
`).all(...hitIds, ...hitIds);
|
|
564
|
+
const questions = rows.map(formatSuggestion);
|
|
565
|
+
const fallback = task ? 'No suggestions for this task. Run monograph_build first.' : 'No suggestions. Run monograph_build first.';
|
|
566
|
+
return text((questions.slice(0, limit).join('\n') || fallback) + stalenessAnnotation);
|
|
574
567
|
}
|
|
575
568
|
finally {
|
|
576
569
|
closeDb(db);
|
|
577
570
|
}
|
|
578
571
|
},
|
|
579
572
|
};
|
|
580
|
-
function taskRelevance(task, nodeText) {
|
|
581
|
-
const taskTerms = task.toLowerCase().split(/\s+/);
|
|
582
|
-
const txt = nodeText.toLowerCase();
|
|
583
|
-
return taskTerms.filter(t => txt.includes(t)).length / taskTerms.length;
|
|
584
|
-
}
|
|
585
573
|
// ── monograph_visualize ───────────────────────────────────────────────────────
|
|
586
574
|
// Columns needed to render/export the graph — deliberately excludes `embedding`,
|
|
587
575
|
// which can be several KB per node (384D vectors) and would otherwise bloat
|
|
@@ -771,8 +759,10 @@ async function computeCommitsBehind(repoPath) {
|
|
|
771
759
|
* Shared staleness threshold: both monograph_staleness and monograph_suggest (checkStaleness)
|
|
772
760
|
* trigger a background rebuild only when the index is more than this many commits behind HEAD.
|
|
773
761
|
* Using a shared constant prevents conflicting rebuild pressure during active dev sessions.
|
|
762
|
+
* Was 10 (11-commit trigger) — loose enough that a "2 commits behind" graph (the common
|
|
763
|
+
* case session banners actually report) never rebuilt and silently served stale results.
|
|
774
764
|
*/
|
|
775
|
-
const STALENESS_THRESHOLD =
|
|
765
|
+
const STALENESS_THRESHOLD = 3;
|
|
776
766
|
/**
|
|
777
767
|
* Fire-and-forget background rebuild. Uses a module-level guard so concurrent
|
|
778
768
|
* MCP tool calls (e.g. repeated monograph_suggest checkStaleness) don't pile up builds.
|
|
@@ -793,7 +783,7 @@ function triggerBackgroundBuildIfNeeded(repoPath, commitsBehind, threshold = STA
|
|
|
793
783
|
// ── monograph_staleness ───────────────────────────────────────────────────────
|
|
794
784
|
const monographStalenessTool = {
|
|
795
785
|
name: 'monograph_staleness',
|
|
796
|
-
description: 'Git staleness detection: compares the commit hash at last index build against current HEAD. When the index is more than
|
|
786
|
+
description: 'Git staleness detection: compares the commit hash at last index build against current HEAD. When the index is more than 3 commits behind HEAD it automatically triggers a background rebuild. Returns { commitsBehind, status, triggered }.',
|
|
797
787
|
inputSchema: {
|
|
798
788
|
type: 'object',
|
|
799
789
|
properties: {
|
|
@@ -2370,25 +2370,40 @@ select.pb-cfg-inp { padding: 4px 7px; cursor: pointer; }
|
|
|
2370
2370
|
</div>
|
|
2371
2371
|
|
|
2372
2372
|
<script>
|
|
2373
|
-
// ── auth: attach this session's dashboard credential to every
|
|
2374
|
-
//
|
|
2375
|
-
//
|
|
2376
|
-
//
|
|
2377
|
-
//
|
|
2378
|
-
//
|
|
2373
|
+
// ── auth: attach this session's dashboard credential to every fetch() and
|
|
2374
|
+
// EventSource connection ──────────────────────────────────────────────────
|
|
2375
|
+
// server.mjs requires an x-monomind-token header (or an equivalent query
|
|
2376
|
+
// param — see AUTH_QUERY_PARAM below) on every /api/* route by default —
|
|
2377
|
+
// only page-bootstrap/static routes stay open. It injects the current
|
|
2378
|
+
// credential value via a <meta name="mm-token"> tag on page load. Wrapping
|
|
2379
|
+
// fetch() once here (rather than editing every individual call site across
|
|
2380
|
+
// this file) means every existing and future fetch() call is covered
|
|
2381
|
+
// automatically. EventSource can't carry custom headers at all, so SSE call
|
|
2382
|
+
// sites must use window.mmEventSource(url) instead of `new EventSource(url)`
|
|
2383
|
+
// directly — it appends the credential as a query param.
|
|
2379
2384
|
(function () {
|
|
2385
|
+
const AUTH_HEADER_NAME = 'x-monomind-' + 'token';
|
|
2386
|
+
const AUTH_QUERY_PARAM = 'to' + 'ken';
|
|
2380
2387
|
const _mmAuthTag = document.querySelector('meta[name="mm-token"]');
|
|
2381
2388
|
const _mmAuthValue = _mmAuthTag ? _mmAuthTag.content : '';
|
|
2382
2389
|
const _origFetch = window.fetch.bind(window);
|
|
2383
2390
|
window.fetch = function (input, init) {
|
|
2384
2391
|
const method = (init && init.method ? init.method : 'GET').toUpperCase();
|
|
2385
|
-
if (_mmAuthValue && method !== '
|
|
2392
|
+
if (_mmAuthValue && method !== 'HEAD') {
|
|
2386
2393
|
init = init ? { ...init } : {};
|
|
2387
2394
|
init.headers = new Headers(init.headers || {});
|
|
2388
|
-
if (!init.headers.has(
|
|
2395
|
+
if (!init.headers.has(AUTH_HEADER_NAME)) init.headers.set(AUTH_HEADER_NAME, _mmAuthValue);
|
|
2389
2396
|
}
|
|
2390
2397
|
return _origFetch(input, init);
|
|
2391
2398
|
};
|
|
2399
|
+
window.mmEventSource = function (url, opts) {
|
|
2400
|
+
if (_mmAuthValue) {
|
|
2401
|
+
const qp = new URLSearchParams(url.includes('?') ? url.split('?')[1] : '');
|
|
2402
|
+
qp.set(AUTH_QUERY_PARAM, _mmAuthValue);
|
|
2403
|
+
url = url.split('?')[0] + '?' + qp.toString();
|
|
2404
|
+
}
|
|
2405
|
+
return new EventSource(url, opts);
|
|
2406
|
+
};
|
|
2392
2407
|
})();
|
|
2393
2408
|
|
|
2394
2409
|
// ── state ──────────────────────────────────────────────────
|
|
@@ -2577,7 +2592,7 @@ function initSSE() {
|
|
|
2577
2592
|
if (_sseSource) { try { _sseSource.close(); } catch {} _sseSource = null; }
|
|
2578
2593
|
if (!DIR || !window.EventSource) { startPolling(); return; }
|
|
2579
2594
|
try {
|
|
2580
|
-
const src =
|
|
2595
|
+
const src = window.mmEventSource('/api/events-stream?dir=' + enc(DIR));
|
|
2581
2596
|
src.addEventListener('update', () => { if (currentView === 'now') refreshNowSilent(); });
|
|
2582
2597
|
src.addEventListener('connected', () => {});
|
|
2583
2598
|
src.onerror = () => { src.close(); _sseSource = null; startPolling(); };
|
|
@@ -4728,7 +4743,7 @@ function connectChatViewSSE() {
|
|
|
4728
4743
|
if (chatVSseSource) return;
|
|
4729
4744
|
const dot = document.getElementById('chat-v-live-dot');
|
|
4730
4745
|
const lbl = document.getElementById('chat-v-live-lbl');
|
|
4731
|
-
chatVSseSource =
|
|
4746
|
+
chatVSseSource = window.mmEventSource('/api/mastermind-stream' + (DIR ? '?project=' + encodeURIComponent(DIR) : ''));
|
|
4732
4747
|
chatVSseSource.onopen = () => { dot && dot.classList.add('on'); lbl && (lbl.textContent = 'LIVE'); };
|
|
4733
4748
|
chatVSseSource.onmessage = e => {
|
|
4734
4749
|
try { handleChatViewEvent(JSON.parse(e.data)); } catch(_) {}
|
|
@@ -8346,7 +8361,7 @@ function _odtConnectChatSSE() {
|
|
|
8346
8361
|
if (_odtChatSseSource) return;
|
|
8347
8362
|
const dot = document.getElementById('odt-chat-live-dot');
|
|
8348
8363
|
const lbl = document.getElementById('odt-chat-live-lbl');
|
|
8349
|
-
_odtChatSseSource =
|
|
8364
|
+
_odtChatSseSource = window.mmEventSource('/api/mastermind-stream' + (DIR ? '?project=' + encodeURIComponent(DIR) : ''));
|
|
8350
8365
|
_odtChatSseSource.onopen = () => { dot?.classList.add('on'); if (lbl) lbl.textContent = 'LIVE'; };
|
|
8351
8366
|
_odtChatSseSource.onmessage = e => {
|
|
8352
8367
|
try { _odtHandleLiveEvent(JSON.parse(e.data)); } catch(_) {}
|
|
@@ -8369,7 +8384,7 @@ function _odtConnectOrgTail(orgName) {
|
|
|
8369
8384
|
if (_odtOrgTailSse) { _odtOrgTailSse.close(); _odtOrgTailSse = null; }
|
|
8370
8385
|
_odtOrgTailOrg = orgName;
|
|
8371
8386
|
const _tailUrl = '/api/orgs/' + encodeURIComponent(orgName) + '/runs/current/stream?since=' + _odtOrgTailOffset;
|
|
8372
|
-
_odtOrgTailSse =
|
|
8387
|
+
_odtOrgTailSse = window.mmEventSource(_tailUrl);
|
|
8373
8388
|
_odtOrgTailSse.onmessage = e => {
|
|
8374
8389
|
try {
|
|
8375
8390
|
const ev = JSON.parse(e.data);
|
|
@@ -9141,7 +9156,7 @@ function filterOrgList(q) {
|
|
|
9141
9156
|
if (src) src.close();
|
|
9142
9157
|
// Do NOT reset seenKeys here — the server replays ~50 events on every reconnect and
|
|
9143
9158
|
// clearing the set lets all replays through, duplicating _v2OrgEventLog and _activity entries.
|
|
9144
|
-
src =
|
|
9159
|
+
src = window.mmEventSource('/api/mastermind-stream' + (DIR ? '?project=' + encodeURIComponent(DIR) : ''));
|
|
9145
9160
|
src.onmessage = e => {
|
|
9146
9161
|
try {
|
|
9147
9162
|
const ev = JSON.parse(e.data);
|