monomind 2.7.6 → 2.7.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 +5 -4
- package/packages/@monomind/cli/.claude/agents/core/coder.md +9 -0
- package/packages/@monomind/cli/.claude/agents/core/coordinator.md +62 -0
- package/packages/@monomind/cli/.claude/agents/core/planner.md +9 -0
- package/packages/@monomind/cli/.claude/agents/core/reviewer.md +9 -0
- package/packages/@monomind/cli/.claude/agents/core/tester.md +8 -0
- package/packages/@monomind/cli/.claude/helpers/handlers/gates-handler.cjs +180 -47
- package/packages/@monomind/cli/.claude/helpers/handlers/route-handler.cjs +32 -3
- package/packages/@monomind/cli/.claude/helpers/hook-handler.cjs +55 -3
- package/packages/@monomind/cli/.claude/helpers/intelligence.cjs +3 -1
- package/packages/@monomind/cli/.claude/helpers/statusline.cjs +27 -3
- package/packages/@monomind/cli/.claude/helpers/utils/monograph.cjs +104 -18
- package/packages/@monomind/cli/.claude/settings.json +1 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-server.mjs +38 -1
- package/packages/@monomind/cli/dist/src/browser/dashboard/server.js +6 -1
- package/packages/@monomind/cli/dist/src/capabilities/index.d.ts +0 -1
- package/packages/@monomind/cli/dist/src/capabilities/index.js +8 -1
- package/packages/@monomind/cli/dist/src/commands/agent-lifecycle.js +5 -1
- package/packages/@monomind/cli/dist/src/commands/agent-ops.js +32 -2
- package/packages/@monomind/cli/dist/src/commands/browse-workflow.js +30 -0
- package/packages/@monomind/cli/dist/src/commands/doctor-project-checks.js +8 -1
- package/packages/@monomind/cli/dist/src/commands/guidance.js +8 -2
- package/packages/@monomind/cli/dist/src/commands/hooks-extended-commands.js +10 -2
- package/packages/@monomind/cli/dist/src/commands/init.js +18 -4
- package/packages/@monomind/cli/dist/src/commands/memory-crud.js +40 -3
- package/packages/@monomind/cli/dist/src/commands/org-observe.js +67 -13
- package/packages/@monomind/cli/dist/src/commands/org.d.ts +11 -0
- package/packages/@monomind/cli/dist/src/commands/org.js +162 -15
- package/packages/@monomind/cli/dist/src/commands/performance.js +31 -5
- package/packages/@monomind/cli/dist/src/commands/security-misc.js +18 -3
- package/packages/@monomind/cli/dist/src/commands/security-scan.d.ts +30 -1
- package/packages/@monomind/cli/dist/src/commands/security-scan.js +182 -69
- package/packages/@monomind/cli/dist/src/commands/swarm.js +41 -14
- package/packages/@monomind/cli/dist/src/consensus/audit-writer.js +11 -10
- package/packages/@monomind/cli/dist/src/init/executor.js +138 -22
- package/packages/@monomind/cli/dist/src/init/settings-generator.js +4 -1
- package/packages/@monomind/cli/dist/src/knowledge/document-pipeline.d.ts +17 -0
- package/packages/@monomind/cli/dist/src/knowledge/document-pipeline.js +62 -3
- package/packages/@monomind/cli/dist/src/mcp-client.js +78 -0
- package/packages/@monomind/cli/dist/src/mcp-tools/embeddings-tools.js +16 -8
- package/packages/@monomind/cli/dist/src/mcp-tools/knowledge-tools.js +27 -13
- package/packages/@monomind/cli/dist/src/memory/memory-bridge.d.ts +5 -1
- package/packages/@monomind/cli/dist/src/memory/memory-bridge.js +26 -3
- package/packages/@monomind/cli/dist/src/memory/memory-read.d.ts +9 -0
- package/packages/@monomind/cli/dist/src/memory/memory-read.js +13 -2
- package/packages/@monomind/cli/dist/src/monovector/diff-classifier.js +25 -6
- package/packages/@monomind/cli/dist/src/orgrt/daemon.d.ts +21 -1
- package/packages/@monomind/cli/dist/src/orgrt/daemon.js +82 -11
- package/packages/@monomind/cli/dist/src/orgrt/inbox.js +53 -20
- package/packages/@monomind/cli/dist/src/parser.d.ts +4 -2
- package/packages/@monomind/cli/dist/src/parser.js +61 -25
- package/packages/@monomind/cli/dist/src/routing/route-layer-factory.d.ts +15 -0
- package/packages/@monomind/cli/dist/src/routing/route-layer-factory.js +44 -2
- package/packages/@monomind/cli/dist/src/services/config-file-manager.d.ts +10 -2
- package/packages/@monomind/cli/dist/src/services/config-file-manager.js +10 -2
- package/packages/@monomind/cli/dist/src/ui/collector.mjs +43 -3
- package/packages/@monomind/cli/dist/src/ui/dashboard.html +27 -8
- package/packages/@monomind/cli/dist/src/ui/server.mjs +144 -14
- package/packages/@monomind/cli/package.json +7 -6
- package/packages/@monomind/cli/dist/src/capabilities/watcher.d.ts +0 -18
- package/packages/@monomind/cli/dist/src/capabilities/watcher.js +0 -107
- package/packages/@monomind/cli/dist/src/config-adapter.d.ts +0 -16
- package/packages/@monomind/cli/dist/src/config-adapter.js +0 -220
|
@@ -12,45 +12,97 @@ export const SECRET_PATTERNS = [
|
|
|
12
12
|
{ pattern: /['"]xox[baprs]-[a-zA-Z0-9-]+['"]/g, type: 'Slack Token' },
|
|
13
13
|
{ pattern: /password\s*[:=]\s*['"][^'"]{8,}['"]/gi, type: 'Hardcoded Password' },
|
|
14
14
|
];
|
|
15
|
-
export function
|
|
16
|
-
|
|
15
|
+
export function createScanCoverage() {
|
|
16
|
+
return {
|
|
17
|
+
unreadableDirs: [],
|
|
18
|
+
unreadableFiles: [],
|
|
19
|
+
depthTruncatedDirs: [],
|
|
20
|
+
oversizedFiles: [],
|
|
21
|
+
filesScanned: 0,
|
|
22
|
+
dirsScanned: 0,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** True when some part of the tree was not examined, for any reason. */
|
|
26
|
+
export function scanWasIncomplete(c) {
|
|
27
|
+
return (c.unreadableDirs.length > 0 ||
|
|
28
|
+
c.unreadableFiles.length > 0 ||
|
|
29
|
+
c.depthTruncatedDirs.length > 0 ||
|
|
30
|
+
c.oversizedFiles.length > 0);
|
|
31
|
+
}
|
|
32
|
+
/** True when the scanner hit a hard failure (not merely a configured limit). */
|
|
33
|
+
export function scanHadErrors(c) {
|
|
34
|
+
return c.unreadableDirs.length > 0 || c.unreadableFiles.length > 0;
|
|
35
|
+
}
|
|
36
|
+
/** Human-readable lines describing every gap in coverage. Empty when complete. */
|
|
37
|
+
export function describeScanGaps(c) {
|
|
38
|
+
const lines = [];
|
|
39
|
+
if (c.unreadableDirs.length > 0) {
|
|
40
|
+
lines.push(`${c.unreadableDirs.length} directory(ies) could not be read (e.g. ${c.unreadableDirs[0]})`);
|
|
41
|
+
}
|
|
42
|
+
if (c.unreadableFiles.length > 0) {
|
|
43
|
+
lines.push(`${c.unreadableFiles.length} file(s) could not be read (e.g. ${c.unreadableFiles[0]})`);
|
|
44
|
+
}
|
|
45
|
+
if (c.depthTruncatedDirs.length > 0) {
|
|
46
|
+
lines.push(`${c.depthTruncatedDirs.length} directory(ies) not scanned — depth limit reached (use --depth deep)`);
|
|
47
|
+
}
|
|
48
|
+
if (c.oversizedFiles.length > 0) {
|
|
49
|
+
lines.push(`${c.oversizedFiles.length} file(s) skipped — larger than 1MB`);
|
|
50
|
+
}
|
|
51
|
+
return lines;
|
|
52
|
+
}
|
|
53
|
+
export function findSecretsInDir(dir, depthLimit, baseDir, findings, coverage = createScanCoverage()) {
|
|
54
|
+
if (depthLimit <= 0) {
|
|
55
|
+
coverage.depthTruncatedDirs.push(relative(baseDir, dir) || dir);
|
|
17
56
|
return;
|
|
57
|
+
}
|
|
58
|
+
let entries;
|
|
18
59
|
try {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
60
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
coverage.unreadableDirs.push(relative(baseDir, dir) || dir);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
coverage.dirsScanned++;
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
const isDotEnv = /^\.env(\..+)?$/.test(entry.name);
|
|
69
|
+
if ((entry.name.startsWith('.') && !isDotEnv) || entry.name === 'node_modules' || entry.name === 'dist')
|
|
70
|
+
continue;
|
|
71
|
+
const fullPath = join(dir, entry.name);
|
|
72
|
+
if (entry.isDirectory()) {
|
|
73
|
+
findSecretsInDir(fullPath, depthLimit - 1, baseDir, findings, coverage);
|
|
74
|
+
}
|
|
75
|
+
else if (entry.isFile() && (/\.(ts|js|json|yml|yaml)$/.test(entry.name) || isDotEnv) && !entry.name.endsWith('.d.ts')) {
|
|
76
|
+
let content;
|
|
77
|
+
try {
|
|
78
|
+
if (statSync(fullPath).size > 1024 * 1024) {
|
|
79
|
+
coverage.oversizedFiles.push(relative(baseDir, fullPath));
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
content = readFileSync(fullPath, 'utf-8');
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
coverage.unreadableFiles.push(relative(baseDir, fullPath));
|
|
23
86
|
continue;
|
|
24
|
-
const fullPath = join(dir, entry.name);
|
|
25
|
-
if (entry.isDirectory()) {
|
|
26
|
-
findSecretsInDir(fullPath, depthLimit - 1, baseDir, findings);
|
|
27
87
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
type: 'Hardcoded Secret',
|
|
42
|
-
location: `${relative(baseDir, fullPath)}:${i + 1}`,
|
|
43
|
-
description: type,
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
}
|
|
88
|
+
coverage.filesScanned++;
|
|
89
|
+
const lines = content.split('\n');
|
|
90
|
+
for (let i = 0; i < lines.length; i++) {
|
|
91
|
+
for (const { pattern, type } of SECRET_PATTERNS) {
|
|
92
|
+
pattern.lastIndex = 0;
|
|
93
|
+
let m;
|
|
94
|
+
while ((m = pattern.exec(lines[i])) !== null) {
|
|
95
|
+
findings.push({
|
|
96
|
+
severity: output.warning('HIGH'),
|
|
97
|
+
type: 'Hardcoded Secret',
|
|
98
|
+
location: `${relative(baseDir, fullPath)}:${i + 1}`,
|
|
99
|
+
description: type,
|
|
100
|
+
});
|
|
47
101
|
}
|
|
48
102
|
}
|
|
49
|
-
catch { /* file read error */ }
|
|
50
103
|
}
|
|
51
104
|
}
|
|
52
105
|
}
|
|
53
|
-
catch { /* dir read error */ }
|
|
54
106
|
}
|
|
55
107
|
// ─── scan subcommand ─────────────────────────────────────────────────────────
|
|
56
108
|
export const scanCommand = {
|
|
@@ -92,6 +144,7 @@ export const scanCommand = {
|
|
|
92
144
|
const spinner = output.createSpinner({ text: `Scanning ${target}...`, spinner: 'dots' });
|
|
93
145
|
spinner.start();
|
|
94
146
|
const findings = [];
|
|
147
|
+
const coverage = createScanCoverage();
|
|
95
148
|
let criticalCount = 0, highCount = 0, mediumCount = 0, lowCount = 0;
|
|
96
149
|
try {
|
|
97
150
|
const fs = await import('fs');
|
|
@@ -156,7 +209,7 @@ export const scanCommand = {
|
|
|
156
209
|
spinner.setText('Scanning for hardcoded secrets...');
|
|
157
210
|
const scanDepth = depth === 'deep' ? 10 : depth === 'standard' ? 5 : 3;
|
|
158
211
|
const prevCount = findings.length;
|
|
159
|
-
findSecretsInDir(path.resolve(target), scanDepth, path.resolve(target), findings);
|
|
212
|
+
findSecretsInDir(path.resolve(target), scanDepth, path.resolve(target), findings, coverage);
|
|
160
213
|
highCount += findings.length - prevCount;
|
|
161
214
|
}
|
|
162
215
|
if ((scanType === 'all' || scanType === 'code') && depth !== 'quick') {
|
|
@@ -168,53 +221,74 @@ export const scanCommand = {
|
|
|
168
221
|
{ pattern: /child_process.*exec[^S]/g, type: 'Command Injection', severity: 'high', desc: 'Possible command injection' },
|
|
169
222
|
{ pattern: /\$\{.*\}.*sql|sql.*\$\{/gi, type: 'SQL Injection', severity: 'high', desc: 'Possible SQL injection' },
|
|
170
223
|
];
|
|
224
|
+
// Same coverage accounting as findSecretsInDir: gaps are recorded, never
|
|
225
|
+
// swallowed, so an unreadable tree cannot masquerade as a clean one.
|
|
226
|
+
const codeBase = path.resolve(target);
|
|
171
227
|
const scanCodeDir = (dir, depthLimit) => {
|
|
172
|
-
if (depthLimit <= 0)
|
|
228
|
+
if (depthLimit <= 0) {
|
|
229
|
+
coverage.depthTruncatedDirs.push(path.relative(codeBase, dir) || dir);
|
|
173
230
|
return;
|
|
231
|
+
}
|
|
232
|
+
let entries;
|
|
174
233
|
try {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
234
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
coverage.unreadableDirs.push(path.relative(codeBase, dir) || dir);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
for (const entry of entries) {
|
|
241
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === 'dist')
|
|
242
|
+
continue;
|
|
243
|
+
const fullPath = path.join(dir, entry.name);
|
|
244
|
+
if (entry.isDirectory()) {
|
|
245
|
+
scanCodeDir(fullPath, depthLimit - 1);
|
|
246
|
+
}
|
|
247
|
+
else if (entry.isFile() && /\.(ts|js|tsx|jsx)$/.test(entry.name) && !entry.name.endsWith('.d.ts')) {
|
|
248
|
+
let content;
|
|
249
|
+
try {
|
|
250
|
+
if (fs.statSync(fullPath).size > 1024 * 1024) {
|
|
251
|
+
coverage.oversizedFiles.push(path.relative(codeBase, fullPath));
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
content = fs.readFileSync(fullPath, 'utf-8');
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
coverage.unreadableFiles.push(path.relative(codeBase, fullPath));
|
|
178
258
|
continue;
|
|
179
|
-
const fullPath = path.join(dir, entry.name);
|
|
180
|
-
if (entry.isDirectory()) {
|
|
181
|
-
scanCodeDir(fullPath, depthLimit - 1);
|
|
182
259
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
severity: severity === 'high' ? output.warning('HIGH') : output.warning('MEDIUM'),
|
|
200
|
-
type,
|
|
201
|
-
location: `${path.relative(target, fullPath)}:${i + 1}`,
|
|
202
|
-
description: desc,
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
}
|
|
260
|
+
const lines = content.split('\n');
|
|
261
|
+
for (let i = 0; i < lines.length; i++) {
|
|
262
|
+
for (const { pattern, type, severity, desc } of codePatterns) {
|
|
263
|
+
pattern.lastIndex = 0;
|
|
264
|
+
let m;
|
|
265
|
+
while ((m = pattern.exec(lines[i])) !== null) {
|
|
266
|
+
if (severity === 'high')
|
|
267
|
+
highCount++;
|
|
268
|
+
else
|
|
269
|
+
mediumCount++;
|
|
270
|
+
findings.push({
|
|
271
|
+
severity: severity === 'high' ? output.warning('HIGH') : output.warning('MEDIUM'),
|
|
272
|
+
type,
|
|
273
|
+
location: `${path.relative(target, fullPath)}:${i + 1}`,
|
|
274
|
+
description: desc,
|
|
275
|
+
});
|
|
206
276
|
}
|
|
207
277
|
}
|
|
208
|
-
catch { /* file read error */ }
|
|
209
278
|
}
|
|
210
279
|
}
|
|
211
280
|
}
|
|
212
|
-
catch { /* dir read error */ }
|
|
213
281
|
};
|
|
214
282
|
const scanDepth = depth === 'deep' ? 10 : 5;
|
|
215
283
|
scanCodeDir(path.resolve(target), scanDepth);
|
|
216
284
|
}
|
|
217
|
-
|
|
285
|
+
const gaps = describeScanGaps(coverage);
|
|
286
|
+
if (gaps.length > 0) {
|
|
287
|
+
spinner.stop(output.warning('Scan finished with INCOMPLETE coverage'));
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
spinner.succeed('Scan complete');
|
|
291
|
+
}
|
|
218
292
|
output.writeln();
|
|
219
293
|
if (findings.length > 0) {
|
|
220
294
|
output.printTable({
|
|
@@ -229,9 +303,19 @@ export const scanCommand = {
|
|
|
229
303
|
if (findings.length > 20)
|
|
230
304
|
output.writeln(output.dim(`... and ${findings.length - 20} more issues`));
|
|
231
305
|
}
|
|
306
|
+
else if (gaps.length > 0) {
|
|
307
|
+
// Never present an incomplete scan as a clean bill of health.
|
|
308
|
+
output.writeln(output.warning('No security issues found in the parts that could be scanned — coverage was INCOMPLETE (see below).'));
|
|
309
|
+
}
|
|
232
310
|
else {
|
|
233
311
|
output.writeln(output.success('No security issues found!'));
|
|
234
312
|
}
|
|
313
|
+
if (gaps.length > 0) {
|
|
314
|
+
output.writeln();
|
|
315
|
+
output.writeln(output.warning('Incomplete coverage:'));
|
|
316
|
+
for (const g of gaps)
|
|
317
|
+
output.writeln(output.warning(` - ${g}`));
|
|
318
|
+
}
|
|
235
319
|
output.writeln();
|
|
236
320
|
output.printBox([
|
|
237
321
|
`Target: ${target}`,
|
|
@@ -240,6 +324,9 @@ export const scanCommand = {
|
|
|
240
324
|
``,
|
|
241
325
|
`Critical: ${criticalCount} High: ${highCount} Medium: ${mediumCount} Low: ${lowCount}`,
|
|
242
326
|
`Total Issues: ${findings.length}`,
|
|
327
|
+
``,
|
|
328
|
+
`Coverage: ${coverage.filesScanned} file(s) in ${coverage.dirsScanned} dir(s) scanned`,
|
|
329
|
+
`Coverage status: ${gaps.length === 0 ? 'complete' : `INCOMPLETE (${gaps.length} gap type(s))`}`,
|
|
243
330
|
].join('\n'), 'Scan Summary');
|
|
244
331
|
if (fix && criticalCount + highCount > 0) {
|
|
245
332
|
const resolvedTarget = realpathSync(path.resolve(target));
|
|
@@ -263,6 +350,11 @@ export const scanCommand = {
|
|
|
263
350
|
fixSpinner.fail('Some fixes could not be applied automatically');
|
|
264
351
|
}
|
|
265
352
|
}
|
|
353
|
+
// A scan that hit real read errors cannot certify anything — fail loudly.
|
|
354
|
+
// Depth truncation is a configured limit, not an error, so it is reported
|
|
355
|
+
// above but does not by itself flip the exit status.
|
|
356
|
+
if (scanHadErrors(coverage))
|
|
357
|
+
return { success: false };
|
|
266
358
|
return { success: findings.length === 0 || (criticalCount === 0 && highCount === 0) };
|
|
267
359
|
}
|
|
268
360
|
catch (error) {
|
|
@@ -307,11 +399,21 @@ export const secretsCommand = {
|
|
|
307
399
|
const spinner = output.createSpinner({ text: `Scanning ${targetPath}...`, spinner: 'dots' });
|
|
308
400
|
spinner.start();
|
|
309
401
|
const findings = [];
|
|
402
|
+
const coverage = createScanCoverage();
|
|
310
403
|
const scanDepth = depth === 'deep' ? 10 : depth === 'standard' ? 5 : 3;
|
|
311
|
-
findSecretsInDir(resolve(targetPath), scanDepth, resolve(targetPath), findings);
|
|
312
|
-
|
|
404
|
+
findSecretsInDir(resolve(targetPath), scanDepth, resolve(targetPath), findings, coverage);
|
|
405
|
+
const gaps = describeScanGaps(coverage);
|
|
406
|
+
if (gaps.length > 0) {
|
|
407
|
+
spinner.stop(output.warning('Scan finished with INCOMPLETE coverage'));
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
spinner.succeed('Scan complete');
|
|
411
|
+
}
|
|
313
412
|
output.writeln();
|
|
314
|
-
if (findings.length === 0) {
|
|
413
|
+
if (findings.length === 0 && gaps.length > 0) {
|
|
414
|
+
output.writeln(output.warning('No secrets found in the parts that could be scanned — coverage was INCOMPLETE.'));
|
|
415
|
+
}
|
|
416
|
+
else if (findings.length === 0) {
|
|
315
417
|
output.writeln(output.success('No secrets found.'));
|
|
316
418
|
}
|
|
317
419
|
else {
|
|
@@ -326,9 +428,20 @@ export const secretsCommand = {
|
|
|
326
428
|
if (findings.length > 20)
|
|
327
429
|
output.writeln(output.dim(`... and ${findings.length - 20} more`));
|
|
328
430
|
}
|
|
431
|
+
if (gaps.length > 0) {
|
|
432
|
+
output.writeln();
|
|
433
|
+
output.writeln(output.warning('Incomplete coverage:'));
|
|
434
|
+
for (const g of gaps)
|
|
435
|
+
output.writeln(output.warning(` - ${g}`));
|
|
436
|
+
}
|
|
329
437
|
output.writeln();
|
|
330
|
-
output.writeln(output.bold('Summary: ') +
|
|
331
|
-
|
|
438
|
+
output.writeln(output.bold('Summary: ') +
|
|
439
|
+
`${findings.length} secret(s) found in ${targetPath} ` +
|
|
440
|
+
`(${coverage.filesScanned} file(s) in ${coverage.dirsScanned} dir(s) scanned, ` +
|
|
441
|
+
`coverage ${gaps.length === 0 ? 'complete' : 'INCOMPLETE'})`);
|
|
442
|
+
// Read errors mean the tree was not fully examined — "no secrets" is not
|
|
443
|
+
// a result we can stand behind, so do not exit 0 on it.
|
|
444
|
+
return { success: findings.length === 0 && !scanHadErrors(coverage) };
|
|
332
445
|
},
|
|
333
446
|
};
|
|
334
447
|
//# sourceMappingURL=security-scan.js.map
|
|
@@ -8,19 +8,45 @@ import { callMCPTool, MCPClientError } from '../mcp-client.js';
|
|
|
8
8
|
import * as fs from 'fs';
|
|
9
9
|
import * as path from 'path';
|
|
10
10
|
import { writeJsonFileAtomic } from '../utils/json-file.js';
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
import { getMonomindDataRoot, getProjectCwd, migrateLegacyStoreFile } from '../mcp-tools/types.js';
|
|
12
|
+
// Canonical paths — resolved through getMonomindDataRoot() so the CLI and the MCP
|
|
13
|
+
// tools (agent-tools.ts / swarm-tools.ts / task-tools.ts) read and write the SAME
|
|
14
|
+
// physical files. These used to be `path.join(process.cwd(), '.monomind/...')`,
|
|
15
|
+
// which only coincides with the canonical root when there is no `.git` directory;
|
|
16
|
+
// inside a real repo the canonical root is `<repo>/.git/monomind`, so the CLI read
|
|
17
|
+
// an empty parallel store ("0 agents" with agents on disk) and `swarm init` wrote a
|
|
18
|
+
// second, divergent swarm-state.json no MCP tool ever read.
|
|
19
|
+
const SWARM_STATE_SUBDIR = 'swarm';
|
|
13
20
|
const SWARM_STATE_FILE = 'swarm-state.json';
|
|
14
|
-
const
|
|
21
|
+
const AGENT_STORE_SUBDIR = 'agents';
|
|
22
|
+
const AGENT_STORE_FILE = 'store.json';
|
|
23
|
+
function getSwarmDir() {
|
|
24
|
+
return path.join(getMonomindDataRoot(), SWARM_STATE_SUBDIR);
|
|
25
|
+
}
|
|
26
|
+
// Canonical swarm-state.json path. Also pulls a pre-existing legacy
|
|
27
|
+
// `<cwd>/.monomind/swarm/swarm-state.json` into place (copy, never move; no-op once
|
|
28
|
+
// the canonical file exists or when the two paths coincide) so state written by an
|
|
29
|
+
// older CLI isn't silently orphaned — same helper the MCP tools use.
|
|
30
|
+
function getSwarmStateFile() {
|
|
31
|
+
const file = path.join(getSwarmDir(), SWARM_STATE_FILE);
|
|
32
|
+
migrateLegacyStoreFile(file, path.join(SWARM_STATE_SUBDIR, SWARM_STATE_FILE));
|
|
33
|
+
return file;
|
|
34
|
+
}
|
|
35
|
+
function getAgentStoreFile() {
|
|
36
|
+
const file = path.join(getMonomindDataRoot(), AGENT_STORE_SUBDIR, AGENT_STORE_FILE);
|
|
37
|
+
migrateLegacyStoreFile(file, path.join(AGENT_STORE_SUBDIR, AGENT_STORE_FILE));
|
|
38
|
+
return file;
|
|
39
|
+
}
|
|
15
40
|
// Get dynamic swarm status from MCP-canonical state files
|
|
16
41
|
function getSwarmStatus(swarmId) {
|
|
17
|
-
const
|
|
42
|
+
const projectCwd = getProjectCwd();
|
|
43
|
+
const sessionDir = path.join(projectCwd, '.claude', 'sessions');
|
|
18
44
|
const memoryPaths = [
|
|
19
|
-
path.join(
|
|
20
|
-
path.join(
|
|
45
|
+
path.join(projectCwd, '.monomind', 'memory.db'),
|
|
46
|
+
path.join(projectCwd, '.claude', 'memory.db'),
|
|
21
47
|
];
|
|
22
|
-
// Read swarm state from MCP-canonical path
|
|
23
|
-
const swarmStateFile =
|
|
48
|
+
// Read swarm state from the MCP-canonical path
|
|
49
|
+
const swarmStateFile = getSwarmStateFile();
|
|
24
50
|
let swarmState = null;
|
|
25
51
|
if (fs.existsSync(swarmStateFile)) {
|
|
26
52
|
try {
|
|
@@ -52,10 +78,11 @@ function getSwarmStatus(swarmId) {
|
|
|
52
78
|
// Ignore parse errors
|
|
53
79
|
}
|
|
54
80
|
}
|
|
55
|
-
// Count agents from MCP-canonical agent store (
|
|
81
|
+
// Count agents from the MCP-canonical agent store (the same physical file
|
|
82
|
+
// agent_spawn writes to — see getAgentStoreFile()).
|
|
56
83
|
let activeAgents = 0;
|
|
57
84
|
let totalAgents = 0;
|
|
58
|
-
const agentStoreFile =
|
|
85
|
+
const agentStoreFile = getAgentStoreFile();
|
|
59
86
|
if (fs.existsSync(agentStoreFile)) {
|
|
60
87
|
try {
|
|
61
88
|
const agentSz = fs.statSync(agentStoreFile).size;
|
|
@@ -102,7 +129,7 @@ function getSwarmStatus(swarmId) {
|
|
|
102
129
|
let completedTasks = 0;
|
|
103
130
|
let inProgressTasks = 0;
|
|
104
131
|
let pendingTasks = 0;
|
|
105
|
-
const tasksDir = path.join(
|
|
132
|
+
const tasksDir = path.join(getSwarmDir(), 'tasks');
|
|
106
133
|
if (fs.existsSync(tasksDir)) {
|
|
107
134
|
try {
|
|
108
135
|
const taskFiles = fs.readdirSync(tasksDir).filter(f => f.endsWith('.json'));
|
|
@@ -311,7 +338,7 @@ const initCommand = {
|
|
|
311
338
|
output.printSuccess('Swarm initialized successfully');
|
|
312
339
|
// Save swarm state to MCP-canonical path so CLI and MCP share state
|
|
313
340
|
try {
|
|
314
|
-
const swarmDir2 =
|
|
341
|
+
const swarmDir2 = getSwarmDir();
|
|
315
342
|
if (!fs.existsSync(swarmDir2)) {
|
|
316
343
|
fs.mkdirSync(swarmDir2, { recursive: true });
|
|
317
344
|
}
|
|
@@ -466,7 +493,7 @@ const startCommand = {
|
|
|
466
493
|
output.writeln(output.dim(' Run with -v/--verbose for more detail, or `monomind doctor` to check config/permission issues.'));
|
|
467
494
|
}
|
|
468
495
|
// Persist swarm state to MCP-canonical path so CLI and MCP share state
|
|
469
|
-
const swarmDir =
|
|
496
|
+
const swarmDir = getSwarmDir();
|
|
470
497
|
if (!fs.existsSync(swarmDir))
|
|
471
498
|
fs.mkdirSync(swarmDir, { recursive: true });
|
|
472
499
|
// Read existing store to preserve other swarms
|
|
@@ -627,7 +654,7 @@ const stopCommand = {
|
|
|
627
654
|
}
|
|
628
655
|
// Only update persisted swarm state if the MCP call succeeded (#1423)
|
|
629
656
|
if (mcpStopped) {
|
|
630
|
-
const stopStateFile =
|
|
657
|
+
const stopStateFile = getSwarmStateFile();
|
|
631
658
|
if (fs.existsSync(stopStateFile)) {
|
|
632
659
|
try {
|
|
633
660
|
const stopStatSz = fs.statSync(stopStateFile).size;
|
|
@@ -177,19 +177,20 @@ export class AuditWriter {
|
|
|
177
177
|
appendFileSync(filePath, JSON.stringify(data) + '\n', 'utf-8');
|
|
178
178
|
}
|
|
179
179
|
readLines(filePath) {
|
|
180
|
+
// A missing log is a genuine empty trail. Everything else — an oversized
|
|
181
|
+
// log, a permissions error, an I/O failure — is an *unreadable* trail, and
|
|
182
|
+
// must not be reported as an empty one: this file is the tamper-evidence
|
|
183
|
+
// record, so "0 records" and "cannot read" have opposite meanings.
|
|
184
|
+
// Callers (hive-mind_audit_list / _verify) already surface a throw as
|
|
185
|
+
// `success: false` with the message.
|
|
180
186
|
if (!existsSync(filePath))
|
|
181
187
|
return [];
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
throw new Error(`Audit log ${filePath} exceeds 50MB — run rotation/cleanup`);
|
|
186
|
-
}
|
|
187
|
-
const content = readFileSync(filePath, 'utf-8');
|
|
188
|
-
return parseJsonl(content);
|
|
189
|
-
}
|
|
190
|
-
catch {
|
|
191
|
-
return [];
|
|
188
|
+
const MAX_BYTES = 50 * 1024 * 1024;
|
|
189
|
+
if (statSync(filePath).size > MAX_BYTES) {
|
|
190
|
+
throw new Error(`Audit log ${filePath} exceeds 50MB — run rotation/cleanup`);
|
|
192
191
|
}
|
|
192
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
193
|
+
return parseJsonl(content);
|
|
193
194
|
}
|
|
194
195
|
}
|
|
195
196
|
//# sourceMappingURL=audit-writer.js.map
|