monomind 1.11.14 → 1.13.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.
Files changed (179) hide show
  1. package/.claude/agents/generated/channel-intelligence-director.md +87 -0
  2. package/.claude/agents/generated/chief-growth-officer.md +88 -0
  3. package/.claude/agents/generated/content-seo-strategist.md +90 -0
  4. package/.claude/agents/generated/developer-community-strategist.md +91 -0
  5. package/.claude/agents/generated/outreach-partnership-strategist.md +90 -0
  6. package/.claude/agents/generated/social-media-strategist.md +91 -0
  7. package/.claude/agents/generated/video-visual-strategist.md +90 -0
  8. package/.claude/commands/mastermind/master.md +1 -1
  9. package/.claude/helpers/auto-memory-hook.mjs +13 -4
  10. package/.claude/helpers/control-start.cjs +5 -0
  11. package/.claude/helpers/event-logger.cjs +114 -0
  12. package/.claude/helpers/handlers/adr-draft-handler.cjs +19 -5
  13. package/.claude/helpers/handlers/agent-start-handler.cjs +13 -4
  14. package/.claude/helpers/handlers/compact-handler.cjs +2 -0
  15. package/.claude/helpers/handlers/edit-handler.cjs +1 -1
  16. package/.claude/helpers/handlers/gates-handler.cjs +3 -0
  17. package/.claude/helpers/handlers/graph-status-handler.cjs +14 -8
  18. package/.claude/helpers/handlers/loops-status-handler.cjs +5 -2
  19. package/.claude/helpers/handlers/route-handler.cjs +24 -10
  20. package/.claude/helpers/handlers/session-handler.cjs +11 -4
  21. package/.claude/helpers/handlers/session-restore-handler.cjs +35 -19
  22. package/.claude/helpers/handlers/task-handler.cjs +13 -5
  23. package/.claude/helpers/hook-handler.cjs +40 -0
  24. package/.claude/helpers/intelligence.cjs +130 -53
  25. package/.claude/helpers/loop-tracker.cjs +15 -3
  26. package/.claude/helpers/memory-palace.cjs +461 -0
  27. package/.claude/helpers/memory.cjs +138 -14
  28. package/.claude/helpers/metrics-db.mjs +87 -0
  29. package/.claude/helpers/router.cjs +300 -42
  30. package/.claude/helpers/session.cjs +89 -30
  31. package/.claude/helpers/statusline.cjs +148 -4
  32. package/.claude/helpers/toggle-statusline.cjs +73 -0
  33. package/.claude/helpers/token-tracker.cjs +934 -0
  34. package/.claude/helpers/utils/micro-agents.cjs +20 -4
  35. package/.claude/helpers/utils/monograph.cjs +39 -4
  36. package/.claude/helpers/utils/telemetry.cjs +3 -3
  37. package/.claude/scheduled_tasks.lock +1 -1
  38. package/.claude/settings.json +92 -1
  39. package/.claude/skills/mastermind/_protocol.md +25 -15
  40. package/.claude/skills/mastermind/architect.md +3 -3
  41. package/.claude/skills/mastermind/autodev.md +4 -2
  42. package/.claude/skills/mastermind/idea.md +10 -0
  43. package/.claude/skills/mastermind/ops.md +3 -3
  44. package/.claude/skills/mastermind/runorg.md +153 -86
  45. package/package.json +20 -3
  46. package/packages/@monomind/cli/dist/src/agents/registry-builder.js +2 -0
  47. package/packages/@monomind/cli/dist/src/autopilot-state.js +10 -5
  48. package/packages/@monomind/cli/dist/src/benchmarks/benchmark-runner.js +13 -0
  49. package/packages/@monomind/cli/dist/src/benchmarks/metric-evaluators.js +20 -9
  50. package/packages/@monomind/cli/dist/src/browser/actions.js +10 -3
  51. package/packages/@monomind/cli/dist/src/browser/browser.js +12 -2
  52. package/packages/@monomind/cli/dist/src/browser/cdp.js +21 -3
  53. package/packages/@monomind/cli/dist/src/browser/har.js +27 -5
  54. package/packages/@monomind/cli/dist/src/commands/agent.js +11 -8
  55. package/packages/@monomind/cli/dist/src/commands/analyze.js +36 -21
  56. package/packages/@monomind/cli/dist/src/commands/autopilot.js +12 -4
  57. package/packages/@monomind/cli/dist/src/commands/benchmark.js +51 -8
  58. package/packages/@monomind/cli/dist/src/commands/browse.js +5 -2
  59. package/packages/@monomind/cli/dist/src/commands/claims.js +29 -11
  60. package/packages/@monomind/cli/dist/src/commands/cleanup.js +25 -5
  61. package/packages/@monomind/cli/dist/src/commands/config.js +15 -7
  62. package/packages/@monomind/cli/dist/src/commands/daemon.js +6 -0
  63. package/packages/@monomind/cli/dist/src/commands/deployment.js +34 -19
  64. package/packages/@monomind/cli/dist/src/commands/doctor.js +192 -23
  65. package/packages/@monomind/cli/dist/src/commands/guidance.js +15 -2
  66. package/packages/@monomind/cli/dist/src/commands/hive-mind.js +37 -14
  67. package/packages/@monomind/cli/dist/src/commands/hooks.js +42 -25
  68. package/packages/@monomind/cli/dist/src/commands/init.js +9 -4
  69. package/packages/@monomind/cli/dist/src/commands/issues.js +29 -26
  70. package/packages/@monomind/cli/dist/src/commands/mcp.js +11 -5
  71. package/packages/@monomind/cli/dist/src/commands/memory.js +10 -0
  72. package/packages/@monomind/cli/dist/src/commands/migrate.js +5 -5
  73. package/packages/@monomind/cli/dist/src/commands/monograph.js +18 -5
  74. package/packages/@monomind/cli/dist/src/commands/monovector/backup.js +8 -2
  75. package/packages/@monomind/cli/dist/src/commands/monovector/benchmark.js +20 -7
  76. package/packages/@monomind/cli/dist/src/commands/monovector/import.js +15 -0
  77. package/packages/@monomind/cli/dist/src/commands/monovector/migrate.js +4 -1
  78. package/packages/@monomind/cli/dist/src/commands/monovector/optimize.js +11 -0
  79. package/packages/@monomind/cli/dist/src/commands/monovector/setup.js +11 -1
  80. package/packages/@monomind/cli/dist/src/commands/neural.js +1 -1
  81. package/packages/@monomind/cli/dist/src/commands/performance.js +20 -7
  82. package/packages/@monomind/cli/dist/src/commands/platforms.js +90 -8
  83. package/packages/@monomind/cli/dist/src/commands/plugins.js +12 -5
  84. package/packages/@monomind/cli/dist/src/commands/process.js +33 -10
  85. package/packages/@monomind/cli/dist/src/commands/progress.js +5 -3
  86. package/packages/@monomind/cli/dist/src/commands/providers.js +5 -5
  87. package/packages/@monomind/cli/dist/src/commands/replay.js +8 -2
  88. package/packages/@monomind/cli/dist/src/commands/route.js +27 -7
  89. package/packages/@monomind/cli/dist/src/commands/security.js +4 -0
  90. package/packages/@monomind/cli/dist/src/commands/session.js +12 -1
  91. package/packages/@monomind/cli/dist/src/commands/start.js +11 -4
  92. package/packages/@monomind/cli/dist/src/commands/status.js +7 -4
  93. package/packages/@monomind/cli/dist/src/commands/swarm.js +27 -13
  94. package/packages/@monomind/cli/dist/src/commands/task.js +26 -11
  95. package/packages/@monomind/cli/dist/src/commands/tokens.js +7 -2
  96. package/packages/@monomind/cli/dist/src/commands/transfer-store.js +36 -22
  97. package/packages/@monomind/cli/dist/src/commands/update.js +15 -3
  98. package/packages/@monomind/cli/dist/src/commands/workflow.js +39 -6
  99. package/packages/@monomind/cli/dist/src/consensus/audit-writer.js +18 -7
  100. package/packages/@monomind/cli/dist/src/consensus/vote-signer.js +25 -8
  101. package/packages/@monomind/cli/dist/src/index.js +7 -3
  102. package/packages/@monomind/cli/dist/src/init/executor.js +14 -11
  103. package/packages/@monomind/cli/dist/src/init/shared-instructions-generator.js +20 -4
  104. package/packages/@monomind/cli/dist/src/init/statusline-generator.js +36 -15
  105. package/packages/@monomind/cli/dist/src/mcp-tools/a2a-tools.js +98 -13
  106. package/packages/@monomind/cli/dist/src/mcp-tools/agent-tools.js +16 -3
  107. package/packages/@monomind/cli/dist/src/mcp-tools/analyze-tools.js +80 -17
  108. package/packages/@monomind/cli/dist/src/mcp-tools/browser-tools.js +84 -22
  109. package/packages/@monomind/cli/dist/src/mcp-tools/claims-tools.js +35 -7
  110. package/packages/@monomind/cli/dist/src/mcp-tools/config-tools.js +82 -17
  111. package/packages/@monomind/cli/dist/src/mcp-tools/coordination-tools.js +37 -4
  112. package/packages/@monomind/cli/dist/src/mcp-tools/daa-tools.js +49 -7
  113. package/packages/@monomind/cli/dist/src/mcp-tools/embeddings-tools.js +45 -18
  114. package/packages/@monomind/cli/dist/src/mcp-tools/github-tools.js +75 -25
  115. package/packages/@monomind/cli/dist/src/mcp-tools/guidance-tools.js +32 -10
  116. package/packages/@monomind/cli/dist/src/mcp-tools/hive-mind-tools.js +91 -20
  117. package/packages/@monomind/cli/dist/src/mcp-tools/hooks-tools.js +188 -29
  118. package/packages/@monomind/cli/dist/src/mcp-tools/memory-tools.js +25 -7
  119. package/packages/@monomind/cli/dist/src/mcp-tools/monograph-compat.js +11 -2
  120. package/packages/@monomind/cli/dist/src/mcp-tools/monograph-tools.js +476 -62
  121. package/packages/@monomind/cli/dist/src/mcp-tools/neural-tools.js +44 -9
  122. package/packages/@monomind/cli/dist/src/mcp-tools/performance-tools.js +45 -10
  123. package/packages/@monomind/cli/dist/src/mcp-tools/progress-tools.js +7 -4
  124. package/packages/@monomind/cli/dist/src/mcp-tools/request-tracker.js +15 -1
  125. package/packages/@monomind/cli/dist/src/mcp-tools/security-tools.js +61 -9
  126. package/packages/@monomind/cli/dist/src/mcp-tools/session-tools.js +45 -14
  127. package/packages/@monomind/cli/dist/src/mcp-tools/swarm-tools.js +15 -3
  128. package/packages/@monomind/cli/dist/src/mcp-tools/system-tools.js +14 -7
  129. package/packages/@monomind/cli/dist/src/mcp-tools/task-tools.js +52 -10
  130. package/packages/@monomind/cli/dist/src/mcp-tools/terminal-tools.js +40 -6
  131. package/packages/@monomind/cli/dist/src/mcp-tools/transfer-tools.js +37 -4
  132. package/packages/@monomind/cli/dist/src/mcp-tools/workflow-tools.js +29 -6
  133. package/packages/@monomind/cli/dist/src/memory/ewc-consolidation.js +26 -10
  134. package/packages/@monomind/cli/dist/src/memory/intelligence.js +80 -19
  135. package/packages/@monomind/cli/dist/src/memory/memory-bridge.js +21 -2
  136. package/packages/@monomind/cli/dist/src/memory/memory-initializer.js +67 -3
  137. package/packages/@monomind/cli/dist/src/memory/sona-optimizer.js +14 -4
  138. package/packages/@monomind/cli/dist/src/monovector/command-outcomes.js +43 -7
  139. package/packages/@monomind/cli/dist/src/monovector/coverage-router.js +8 -4
  140. package/packages/@monomind/cli/dist/src/monovector/coverage-tools.js +6 -3
  141. package/packages/@monomind/cli/dist/src/monovector/diff-classifier.js +13 -0
  142. package/packages/@monomind/cli/dist/src/monovector/route-outcomes.d.ts +2 -1
  143. package/packages/@monomind/cli/dist/src/monovector/route-outcomes.js +46 -4
  144. package/packages/@monomind/cli/dist/src/plugins/manager.js +8 -3
  145. package/packages/@monomind/cli/dist/src/plugins/store/discovery.js +46 -2
  146. package/packages/@monomind/cli/dist/src/plugins/store/search.js +5 -4
  147. package/packages/@monomind/cli/dist/src/production/circuit-breaker.js +17 -3
  148. package/packages/@monomind/cli/dist/src/production/error-handler.js +3 -0
  149. package/packages/@monomind/cli/dist/src/production/monitoring.js +20 -3
  150. package/packages/@monomind/cli/dist/src/production/rate-limiter.js +13 -4
  151. package/packages/@monomind/cli/dist/src/production/retry.js +17 -9
  152. package/packages/@monomind/cli/dist/src/routing/embed-worker.js +6 -2
  153. package/packages/@monomind/cli/dist/src/routing/embedder.js +0 -0
  154. package/packages/@monomind/cli/dist/src/routing/llm-caller.js +13 -2
  155. package/packages/@monomind/cli/dist/src/routing/route-layer-factory.js +18 -3
  156. package/packages/@monomind/cli/dist/src/services/claim-service.d.ts +1 -0
  157. package/packages/@monomind/cli/dist/src/services/claim-service.js +8 -0
  158. package/packages/@monomind/cli/dist/src/services/config-file-manager.js +14 -2
  159. package/packages/@monomind/cli/dist/src/services/headless-worker-executor.js +18 -2
  160. package/packages/@monomind/cli/dist/src/services/worker-daemon.js +348 -17
  161. package/packages/@monomind/cli/dist/src/transfer/anonymization/index.d.ts +0 -3
  162. package/packages/@monomind/cli/dist/src/transfer/anonymization/index.js +16 -1
  163. package/packages/@monomind/cli/dist/src/transfer/export.js +8 -0
  164. package/packages/@monomind/cli/dist/src/transfer/ipfs/upload.js +33 -3
  165. package/packages/@monomind/cli/dist/src/transfer/serialization/cfp.js +8 -2
  166. package/packages/@monomind/cli/dist/src/transfer/storage/gcs.js +37 -3
  167. package/packages/@monomind/cli/dist/src/transfer/store/discovery.js +45 -3
  168. package/packages/@monomind/cli/dist/src/transfer/store/download.js +5 -0
  169. package/packages/@monomind/cli/dist/src/transfer/store/publish.js +13 -1
  170. package/packages/@monomind/cli/dist/src/transfer/store/registry.d.ts +8 -0
  171. package/packages/@monomind/cli/dist/src/transfer/store/registry.js +30 -5
  172. package/packages/@monomind/cli/dist/src/transfer/store/search.js +20 -5
  173. package/packages/@monomind/cli/dist/src/update/checker.js +59 -7
  174. package/packages/@monomind/cli/dist/src/update/executor.js +50 -3
  175. package/packages/@monomind/cli/dist/src/update/index.js +18 -1
  176. package/packages/@monomind/cli/dist/src/update/rate-limiter.d.ts +6 -0
  177. package/packages/@monomind/cli/dist/src/update/rate-limiter.js +79 -7
  178. package/packages/@monomind/cli/dist/src/update/validator.js +52 -1
  179. package/packages/@monomind/cli/package.json +2 -3
@@ -10,7 +10,7 @@
10
10
  * - testgaps: Test coverage analysis (20 min interval)
11
11
  */
12
12
  import { EventEmitter } from 'events';
13
- import { existsSync, mkdirSync, writeFileSync, renameSync, readFileSync, appendFileSync, unlinkSync } from 'fs';
13
+ import { existsSync, mkdirSync, writeFileSync, renameSync, readFileSync, appendFileSync, unlinkSync, statSync } from 'fs';
14
14
  import { cpus } from 'os';
15
15
  import { join, resolve } from 'path';
16
16
  import { HeadlessWorkerExecutor, isHeadlessWorker, } from './headless-worker-executor.js';
@@ -106,14 +106,15 @@ export class WorkerDaemon extends EventEmitter {
106
106
  this.headlessAvailable = await this.headlessExecutor.isAvailable();
107
107
  if (this.headlessAvailable) {
108
108
  this.log('info', 'Claude Code headless mode available - AI workers enabled');
109
- // Forward headless executor events
110
- this.headlessExecutor.on('execution:start', (data) => {
109
+ // Forward headless executor events.
110
+ // The executor emits 'start', 'complete', 'error' (not 'execution:*').
111
+ this.headlessExecutor.on('start', (data) => {
111
112
  this.emit('headless:start', data);
112
113
  });
113
- this.headlessExecutor.on('execution:complete', (data) => {
114
+ this.headlessExecutor.on('complete', (data) => {
114
115
  this.emit('headless:complete', data);
115
116
  });
116
- this.headlessExecutor.on('execution:error', (data) => {
117
+ this.headlessExecutor.on('error', (data) => {
117
118
  this.emit('headless:error', data);
118
119
  });
119
120
  this.headlessExecutor.on('output', (data) => {
@@ -189,6 +190,12 @@ export class WorkerDaemon extends EventEmitter {
189
190
  return {};
190
191
  }
191
192
  try {
193
+ // Guard against OOM from an oversized config file (tampered or corrupted).
194
+ const configSize = statSync(configPath).size;
195
+ if (configSize > 1_048_576 /* 1 MB */) {
196
+ this.log('warn', `config.json is unusually large (${configSize} bytes) — ignoring daemon config`);
197
+ return {};
198
+ }
192
199
  const raw = JSON.parse(readFileSync(configPath, 'utf-8'));
193
200
  // Support both flat keys at root and nested under scopes.project
194
201
  const cfg = raw?.scopes?.project ?? raw;
@@ -271,6 +278,12 @@ export class WorkerDaemon extends EventEmitter {
271
278
  // Try to restore state from file
272
279
  if (existsSync(this.config.stateFile)) {
273
280
  try {
281
+ // Guard against OOM from an oversized state file (tampered or corrupted).
282
+ const stateSize = statSync(this.config.stateFile).size;
283
+ if (stateSize > 5_242_880 /* 5 MB */) {
284
+ this.log('warn', `daemon-state.json is unusually large (${stateSize} bytes) — starting with fresh state`);
285
+ return;
286
+ }
274
287
  const saved = JSON.parse(readFileSync(this.config.stateFile, 'utf-8'));
275
288
  // CRITICAL: Restore worker config (including enabled flag) from saved state
276
289
  // This fixes #950: daemon enable command not persisting worker state
@@ -555,6 +568,17 @@ export class WorkerDaemon extends EventEmitter {
555
568
  const state = this.workers.get(workerConfig.type);
556
569
  const workerId = `${workerConfig.type}_${Date.now()}`;
557
570
  const startTime = Date.now();
571
+ // Track the headless executionId assigned by HeadlessWorkerExecutor so we
572
+ // can cancel it precisely on timeout. The executor generates its own ID
573
+ // (format: `${type}_${ts}_${random}`) which never matches workerId, so we
574
+ // capture it from the 'start' event instead.
575
+ let headlessExecutionId = null;
576
+ const onHeadlessStart = (data) => {
577
+ headlessExecutionId = data.executionId ?? null;
578
+ };
579
+ if (this.headlessExecutor) {
580
+ this.headlessExecutor.on('start', onHeadlessStart);
581
+ }
558
582
  // Track running worker
559
583
  this.runningWorkers.add(workerConfig.type);
560
584
  state.isRunning = true;
@@ -566,11 +590,13 @@ export class WorkerDaemon extends EventEmitter {
566
590
  // cancelAll() was too broad — it would kill concurrent healthy workers.
567
591
  const output = await this.runWithTimeout(() => this.runWorkerLogic(workerConfig), this.config.workerTimeoutMs, `Worker ${workerConfig.type} timed out after ${this.config.workerTimeoutMs / 1000}s`, () => {
568
592
  if (this.headlessExecutor) {
569
- // Try exact-ID cancel first; fall back to type-based cancel which
570
- // avoids killing unrelated concurrent workers (cancelByType).
571
- if (!this.headlessExecutor.cancel(workerId)) {
572
- this.headlessExecutor.cancelByType(workerConfig.type);
593
+ // Use the exact executionId captured from the 'start' event.
594
+ // Fall back to cancelByType only if we didn't capture an ID yet
595
+ // (e.g. timeout fired before the executor emitted 'start').
596
+ if (headlessExecutionId && this.headlessExecutor.cancel(headlessExecutionId)) {
597
+ return;
573
598
  }
599
+ this.headlessExecutor.cancelByType(workerConfig.type);
574
600
  }
575
601
  });
576
602
  const durationMs = Date.now() - startTime;
@@ -615,6 +641,10 @@ export class WorkerDaemon extends EventEmitter {
615
641
  finally {
616
642
  // Remove from running set and process queue
617
643
  this.runningWorkers.delete(workerConfig.type);
644
+ // Unsubscribe the executionId capture listener regardless of outcome
645
+ if (this.headlessExecutor) {
646
+ this.headlessExecutor.off('start', onHeadlessStart);
647
+ }
618
648
  this.processPendingWorkers();
619
649
  }
620
650
  }
@@ -731,6 +761,59 @@ export class WorkerDaemon extends EventEmitter {
731
761
  },
732
762
  scannedAt: Date.now(),
733
763
  };
764
+ // Enrich with monograph graph stats for LLM context injection.
765
+ // Lazy-import to avoid hard dependency; silently skip on any error.
766
+ try {
767
+ const { openDb, closeDb, countNodes, countEdges } = await import('@monoes/monograph');
768
+ const dbPath = join(this.projectRoot, '.monomind', 'monograph.db');
769
+ if (existsSync(dbPath)) {
770
+ const db = openDb(dbPath);
771
+ try {
772
+ // Node/edge counts
773
+ map['graph'] = {
774
+ nodes: countNodes(db),
775
+ edges: countEdges(db),
776
+ };
777
+ // Top 3 god nodes (high degree internal files) — same SQL as monograph_god_nodes tool
778
+ const excluded = ['File', 'Folder', 'Community', 'Concept'];
779
+ const rows = db.prepare(`
780
+ SELECT n.name, n.file_path, n.start_line,
781
+ COUNT(DISTINCT e1.id) + COUNT(DISTINCT e2.id) AS degree
782
+ FROM nodes n
783
+ LEFT JOIN edges e1 ON e1.source_id = n.id
784
+ LEFT JOIN edges e2 ON e2.target_id = n.id
785
+ WHERE n.label NOT IN (${excluded.map(() => '?').join(',')})
786
+ GROUP BY n.id HAVING degree > 0
787
+ ORDER BY degree DESC LIMIT 3
788
+ `).all(...excluded);
789
+ if (rows.length > 0) {
790
+ map['topFiles'] = rows.map(r => ({
791
+ ref: r.file_path
792
+ ? (r.start_line != null ? `${r.file_path}:${r.start_line}` : r.file_path)
793
+ : r.name,
794
+ degree: r.degree,
795
+ }));
796
+ }
797
+ // Index staleness via git — same approach as monograph_health tool
798
+ try {
799
+ const { execSync } = await import('child_process');
800
+ const lastHash = db.prepare("SELECT value FROM meta WHERE key = 'last_commit_hash' LIMIT 1").get()?.value;
801
+ if (lastHash) {
802
+ const countOut = execSync(`git -C ${JSON.stringify(this.projectRoot)} rev-list --count ${lastHash}..HEAD`, { timeout: 5000 }).toString().trim();
803
+ const commitsBehind = parseInt(countOut, 10);
804
+ if (!isNaN(commitsBehind)) {
805
+ map['graphStaleness'] = { commitsBehind };
806
+ }
807
+ }
808
+ }
809
+ catch { /* git unavailable — skip staleness */ }
810
+ }
811
+ finally {
812
+ closeDb(db);
813
+ }
814
+ }
815
+ }
816
+ catch { /* monograph unavailable — skip graph enrichment */ }
734
817
  const metricsFileTmp1 = metricsFile + '.tmp';
735
818
  writeFileSync(metricsFileTmp1, JSON.stringify(map, null, 2));
736
819
  renameSync(metricsFileTmp1, metricsFile);
@@ -752,12 +835,74 @@ export class WorkerDaemon extends EventEmitter {
752
835
  checks: {
753
836
  envFilesProtected: !existsSync(join(this.projectRoot, '.env.local')),
754
837
  gitIgnoreExists: existsSync(join(this.projectRoot, '.gitignore')),
755
- noHardcodedSecrets: true, // Would need actual scanning
838
+ noHardcodedSecrets: null, // Not checked in local mode — requires AI-powered scan
756
839
  },
757
840
  riskLevel: 'low',
758
841
  recommendations: [],
759
842
  note: 'Install Claude Code CLI for AI-powered security analysis',
760
843
  };
844
+ // Enrich with monograph high-centrality files and surprising cross-community edges.
845
+ // God-node files are high-value targets for security review: they are imported by
846
+ // many consumers, so a vulnerability there has the largest blast radius.
847
+ // Cross-community edges reveal unexpected coupling that may indicate hidden attack surfaces.
848
+ try {
849
+ const { openDb, closeDb } = await import('@monoes/monograph');
850
+ const dbPath = join(this.projectRoot, '.monomind', 'monograph.db');
851
+ if (existsSync(dbPath)) {
852
+ const db = openDb(dbPath);
853
+ try {
854
+ const godFileRows = db.prepare(`
855
+ SELECT n.file_path,
856
+ COUNT(DISTINCT e1.id) + COUNT(DISTINCT e2.id) AS degree
857
+ FROM nodes n
858
+ LEFT JOIN edges e1 ON e1.source_id = n.id
859
+ LEFT JOIN edges e2 ON e2.target_id = n.id
860
+ WHERE n.label NOT IN ('File','Folder','Community','Concept')
861
+ AND n.file_path IS NOT NULL
862
+ AND n.file_path NOT LIKE '%node_modules%'
863
+ AND n.file_path NOT LIKE '%/dist/%'
864
+ AND n.file_path NOT LIKE '%.test.%'
865
+ AND n.file_path NOT LIKE '%.spec.%'
866
+ GROUP BY n.file_path
867
+ ORDER BY degree DESC
868
+ LIMIT 5
869
+ `).all();
870
+ const surpriseRows = db.prepare(`
871
+ SELECT n1.name as src_name, n2.name as tgt_name, e.relation, e.confidence_score,
872
+ n1.file_path as src_file, n2.file_path as tgt_file
873
+ FROM edges e
874
+ JOIN nodes n1 ON n1.id = e.source_id
875
+ JOIN nodes n2 ON n2.id = e.target_id
876
+ WHERE e.confidence != 'EXTRACTED'
877
+ AND n1.community_id IS NOT NULL
878
+ AND n2.community_id IS NOT NULL
879
+ AND n1.community_id != n2.community_id
880
+ ORDER BY e.confidence_score ASC
881
+ LIMIT 5
882
+ `).all();
883
+ if (godFileRows.length > 0) {
884
+ audit['priorityScanTargets'] = godFileRows.map(r => ({
885
+ file: r.file_path.replace(this.projectRoot + '/', '').replace(this.projectRoot + '\\', ''),
886
+ degree: r.degree,
887
+ reason: 'high-centrality: vulnerability here affects the most consumers',
888
+ }));
889
+ }
890
+ if (surpriseRows.length > 0) {
891
+ audit['unexpectedCoupling'] = surpriseRows.map(r => ({
892
+ edge: `${r.src_name} --${r.relation}--> ${r.tgt_name}`,
893
+ srcFile: r.src_file ?? '(unknown)',
894
+ tgtFile: r.tgt_file ?? '(unknown)',
895
+ confidenceScore: r.confidence_score,
896
+ reason: 'cross-community edge: may indicate hidden dependency or attack surface',
897
+ }));
898
+ }
899
+ }
900
+ finally {
901
+ closeDb(db);
902
+ }
903
+ }
904
+ }
905
+ catch { /* monograph unavailable — skip graph enrichment */ }
761
906
  const auditFileTmp = auditFile + '.tmp';
762
907
  writeFileSync(auditFileTmp, JSON.stringify(audit, null, 2));
763
908
  renameSync(auditFileTmp, auditFile);
@@ -779,8 +924,8 @@ export class WorkerDaemon extends EventEmitter {
779
924
  memoryUsage: process.memoryUsage(),
780
925
  uptime: process.uptime(),
781
926
  optimizations: {
782
- cacheHitRate: 0.78,
783
- avgResponseTime: 45,
927
+ cacheHitRate: null, // Not measured in local mode — requires AI-powered analysis
928
+ avgResponseTime: null, // Not measured in local mode — requires AI-powered analysis
784
929
  },
785
930
  note: 'Install Claude Code CLI for AI-powered optimization suggestions',
786
931
  };
@@ -897,13 +1042,95 @@ export class WorkerDaemon extends EventEmitter {
897
1042
  * Local ultralearn worker (fallback when headless unavailable)
898
1043
  */
899
1044
  async runUltralearnWorkerLocal() {
900
- return {
1045
+ const result = {
901
1046
  timestamp: new Date().toISOString(),
902
1047
  mode: 'local',
903
1048
  patternsLearned: 0,
904
1049
  insightsGained: [],
905
- note: 'Install Claude Code CLI for AI-powered deep learning',
906
1050
  };
1051
+ // Enrich with monograph community clusters and bridge-node patterns for LLM context injection.
1052
+ // Bridge nodes (symbols that cross community boundaries) are architecturally significant —
1053
+ // they represent coupling points an LLM should be aware of when reasoning about change impact.
1054
+ try {
1055
+ const { openDb, closeDb, countNodes, countEdges } = await import('@monoes/monograph');
1056
+ const dbPath = join(this.projectRoot, '.monomind', 'monograph.db');
1057
+ if (existsSync(dbPath)) {
1058
+ const db = openDb(dbPath);
1059
+ try {
1060
+ const nodeCount = countNodes(db);
1061
+ const edgeCount = countEdges(db);
1062
+ const communityRows = db.prepare(`
1063
+ SELECT community_id, COUNT(*) AS member_count
1064
+ FROM nodes
1065
+ WHERE community_id IS NOT NULL
1066
+ AND label NOT IN ('File','Folder','Community','Concept')
1067
+ GROUP BY community_id
1068
+ ORDER BY member_count DESC
1069
+ LIMIT 5
1070
+ `).all();
1071
+ const bridgeRows = db.prepare(`
1072
+ SELECT n.name, n.label, n.file_path, n.start_line,
1073
+ COUNT(DISTINCT e.id) AS cross_edges
1074
+ FROM nodes n
1075
+ JOIN edges e ON (e.source_id = n.id OR e.target_id = n.id)
1076
+ JOIN nodes n2 ON (
1077
+ CASE WHEN e.source_id = n.id THEN e.target_id ELSE e.source_id END = n2.id
1078
+ )
1079
+ WHERE n.community_id IS NOT NULL
1080
+ AND n2.community_id IS NOT NULL
1081
+ AND n.community_id != n2.community_id
1082
+ AND n.label NOT IN ('File','Folder','Community','Concept')
1083
+ AND (n.file_path IS NULL OR (
1084
+ n.file_path NOT LIKE '%node_modules%'
1085
+ AND n.file_path NOT LIKE '%/dist/%'
1086
+ AND n.file_path NOT LIKE '%.test.%'
1087
+ AND n.file_path NOT LIKE '%.spec.%'
1088
+ ))
1089
+ GROUP BY n.id
1090
+ ORDER BY cross_edges DESC
1091
+ LIMIT 5
1092
+ `).all();
1093
+ const insights = result['insightsGained'];
1094
+ if (communityRows.length > 0) {
1095
+ insights.push({
1096
+ category: 'community_clusters',
1097
+ description: `Top ${communityRows.length} community clusters by size`,
1098
+ items: communityRows.map(r => ({
1099
+ communityId: r.community_id,
1100
+ memberCount: r.member_count,
1101
+ })),
1102
+ });
1103
+ }
1104
+ if (bridgeRows.length > 0) {
1105
+ insights.push({
1106
+ category: 'bridge_nodes',
1107
+ description: `Top ${bridgeRows.length} bridge nodes crossing community boundaries (high coupling risk)`,
1108
+ items: bridgeRows.map(r => {
1109
+ const loc = r.file_path
1110
+ ? (r.start_line != null ? `${r.file_path}:${r.start_line}` : r.file_path)
1111
+ : '(unknown)';
1112
+ return {
1113
+ name: r.name,
1114
+ label: r.label,
1115
+ location: loc,
1116
+ crossCommunityEdges: r.cross_edges,
1117
+ };
1118
+ }),
1119
+ });
1120
+ result['patternsLearned'] = bridgeRows.length + communityRows.length;
1121
+ }
1122
+ result['graph'] = { nodes: nodeCount, edges: edgeCount };
1123
+ }
1124
+ finally {
1125
+ closeDb(db);
1126
+ }
1127
+ }
1128
+ else {
1129
+ result['note'] = 'Monograph index not built — run `monomind monograph build` for deep learning';
1130
+ }
1131
+ }
1132
+ catch { /* monograph unavailable — return minimal result */ }
1133
+ return result;
907
1134
  }
908
1135
  /**
909
1136
  * Local refactor worker (fallback when headless unavailable)
@@ -921,13 +1148,106 @@ export class WorkerDaemon extends EventEmitter {
921
1148
  * Local deepdive worker (fallback when headless unavailable)
922
1149
  */
923
1150
  async runDeepdiveWorkerLocal() {
924
- return {
1151
+ const deepdiveFile = join(this.projectRoot, '.monomind', 'metrics', 'deepdive.json');
1152
+ const metricsDir = join(this.projectRoot, '.monomind', 'metrics');
1153
+ if (!existsSync(metricsDir)) {
1154
+ mkdirSync(metricsDir, { recursive: true });
1155
+ }
1156
+ const result = {
925
1157
  timestamp: new Date().toISOString(),
926
1158
  mode: 'local',
927
- analysisDepth: 'shallow',
1159
+ analysisDepth: 'graph',
928
1160
  findings: [],
929
- note: 'Install Claude Code CLI for AI-powered deep code analysis',
930
1161
  };
1162
+ // Enrich with monograph god nodes and high-degree file analysis for LLM context injection.
1163
+ // Lazy-import to avoid hard dependency; silently skip on any error.
1164
+ try {
1165
+ const { openDb, closeDb, countNodes, countEdges } = await import('@monoes/monograph');
1166
+ const dbPath = join(this.projectRoot, '.monomind', 'monograph.db');
1167
+ if (existsSync(dbPath)) {
1168
+ const db = openDb(dbPath);
1169
+ try {
1170
+ const nodeCount = countNodes(db);
1171
+ const edgeCount = countEdges(db);
1172
+ const godNodeRows = db.prepare(`
1173
+ SELECT n.name, n.file_path, n.label,
1174
+ COUNT(DISTINCT e1.id) + COUNT(DISTINCT e2.id) AS degree
1175
+ FROM nodes n
1176
+ LEFT JOIN edges e1 ON e1.source_id = n.id
1177
+ LEFT JOIN edges e2 ON e2.target_id = n.id
1178
+ WHERE n.label NOT IN ('File','Folder','Community','Concept')
1179
+ AND (n.file_path IS NULL OR (
1180
+ n.file_path NOT LIKE '%node_modules%'
1181
+ AND n.file_path NOT LIKE '%/dist/%'
1182
+ AND n.file_path NOT LIKE '%.test.%'
1183
+ AND n.file_path NOT LIKE '%.spec.%'
1184
+ ))
1185
+ GROUP BY n.id
1186
+ ORDER BY degree DESC
1187
+ LIMIT 5
1188
+ `).all();
1189
+ const godNodes = godNodeRows.map(r => ({
1190
+ name: r.name,
1191
+ label: r.label,
1192
+ file: r.file_path ?? '(unknown)',
1193
+ degree: r.degree,
1194
+ }));
1195
+ const fileRows = db.prepare(`
1196
+ SELECT n.file_path,
1197
+ COUNT(DISTINCT e2.id) AS in_deg,
1198
+ COUNT(DISTINCT e1.id) AS out_deg
1199
+ FROM nodes n
1200
+ LEFT JOIN edges e1 ON e1.source_id = n.id
1201
+ LEFT JOIN edges e2 ON e2.target_id = n.id
1202
+ WHERE n.label = 'File'
1203
+ AND n.file_path NOT LIKE '%node_modules%'
1204
+ AND n.file_path NOT LIKE '%/dist/%'
1205
+ AND n.file_path NOT LIKE '%.test.%'
1206
+ AND n.file_path NOT LIKE '%.spec.%'
1207
+ GROUP BY n.id
1208
+ ORDER BY (in_deg + out_deg) DESC
1209
+ LIMIT 5
1210
+ `).all();
1211
+ const highDegFiles = fileRows.map(r => ({
1212
+ file: r.file_path
1213
+ .replace(this.projectRoot + '/', '')
1214
+ .replace(this.projectRoot + '\\', ''),
1215
+ inDegree: r.in_deg,
1216
+ outDegree: r.out_deg,
1217
+ totalDegree: r.in_deg + r.out_deg,
1218
+ }));
1219
+ const findings = [];
1220
+ if (godNodes.length > 0) {
1221
+ findings.push({
1222
+ category: 'god_nodes',
1223
+ description: `Top ${godNodes.length} high-centrality symbols (most imported/referenced)`,
1224
+ items: godNodes,
1225
+ });
1226
+ }
1227
+ if (highDegFiles.length > 0) {
1228
+ findings.push({
1229
+ category: 'high_degree_files',
1230
+ description: `Top ${highDegFiles.length} files by total edge degree (import + export connections)`,
1231
+ items: highDegFiles,
1232
+ });
1233
+ }
1234
+ result['graph'] = { nodes: nodeCount, edges: edgeCount };
1235
+ result['findings'] = findings;
1236
+ result['analysisDepth'] = 'graph';
1237
+ }
1238
+ finally {
1239
+ closeDb(db);
1240
+ }
1241
+ }
1242
+ else {
1243
+ result['note'] = 'Monograph index not built — run `monomind monograph build` for deep analysis';
1244
+ }
1245
+ }
1246
+ catch { /* monograph unavailable — return minimal result */ }
1247
+ const deepdiveFileTmp = deepdiveFile + '.tmp';
1248
+ writeFileSync(deepdiveFileTmp, JSON.stringify(result, null, 2));
1249
+ renameSync(deepdiveFileTmp, deepdiveFile);
1250
+ return result;
931
1251
  }
932
1252
  /**
933
1253
  * Local benchmark worker
@@ -1042,6 +1362,17 @@ export class WorkerDaemon extends EventEmitter {
1042
1362
  try {
1043
1363
  const logFile = join(this.config.logDir, 'daemon.log');
1044
1364
  appendFileSync(logFile, logMessage + '\n');
1365
+ // Opportunistic rotation: keep the log under 10 MB. When exceeded,
1366
+ // discard the oldest half so recent entries are always retained.
1367
+ const MAX_LOG_BYTES = 10 * 1024 * 1024;
1368
+ if (statSync(logFile).size > MAX_LOG_BYTES) {
1369
+ const content = readFileSync(logFile, 'utf-8');
1370
+ const lines = content.split('\n').filter(Boolean);
1371
+ const trimmed = lines.slice(Math.floor(lines.length / 2)).join('\n') + '\n';
1372
+ const tmp = `${logFile}.${process.pid}.${Date.now()}.tmp`;
1373
+ writeFileSync(tmp, trimmed);
1374
+ renameSync(tmp, logFile);
1375
+ }
1045
1376
  }
1046
1377
  catch {
1047
1378
  // Ignore log write errors
@@ -11,9 +11,6 @@ export declare function detectPII(content: string): PIIDetectionResult;
11
11
  * Redact PII from a string
12
12
  */
13
13
  export declare function redactPII(content: string): string;
14
- /**
15
- * Apply anonymization to CFP document
16
- */
17
14
  export declare function anonymizeCFP(cfp: CFPFormat, level: AnonymizationLevel): {
18
15
  cfp: CFPFormat;
19
16
  transforms: string[];
@@ -35,10 +35,15 @@ const REDACTIONS = {
35
35
  function hash(input) {
36
36
  return crypto.createHash('sha256').update(input).digest('hex');
37
37
  }
38
+ /** Maximum content size for PII scanning/redaction (4 MB). */
39
+ const MAX_SCAN_SIZE = 4 * 1024 * 1024;
38
40
  /**
39
41
  * Detect PII in a string
40
42
  */
41
43
  export function detectPII(content) {
44
+ if (content.length > MAX_SCAN_SIZE) {
45
+ throw new Error(`detectPII: content too large (${content.length} bytes; max ${MAX_SCAN_SIZE})`);
46
+ }
42
47
  const result = {
43
48
  found: false,
44
49
  count: 0,
@@ -85,6 +90,9 @@ function getSeverity(type) {
85
90
  * Redact PII from a string
86
91
  */
87
92
  export function redactPII(content) {
93
+ if (content.length > MAX_SCAN_SIZE) {
94
+ throw new Error(`redactPII: content too large (${content.length} bytes; max ${MAX_SCAN_SIZE})`);
95
+ }
88
96
  let result = content;
89
97
  for (const [type, pattern] of Object.entries(PII_PATTERNS)) {
90
98
  const replacement = REDACTIONS[type];
@@ -100,9 +108,16 @@ export function redactPII(content) {
100
108
  /**
101
109
  * Apply anonymization to CFP document
102
110
  */
111
+ /** Maximum CFP payload size accepted for anonymization (10 MB). */
112
+ const MAX_CFP_ANONYMIZE_SIZE = 10 * 1024 * 1024;
103
113
  export function anonymizeCFP(cfp, level) {
114
+ // Guard before deep clone to prevent OOM on a crafted large object
115
+ const serialized = JSON.stringify(cfp);
116
+ if (serialized.length > MAX_CFP_ANONYMIZE_SIZE) {
117
+ throw new Error(`anonymizeCFP: CFP payload too large (${serialized.length} bytes; max ${MAX_CFP_ANONYMIZE_SIZE})`);
118
+ }
104
119
  const transforms = [];
105
- const anonymized = JSON.parse(JSON.stringify(cfp));
120
+ const anonymized = JSON.parse(serialized);
106
121
  // Level: Minimal
107
122
  if (['minimal', 'standard', 'strict', 'paranoid'].includes(level)) {
108
123
  // Redact author display name
@@ -49,6 +49,14 @@ export async function exportPatterns(cfp, options = {}) {
49
49
  // Write to file
50
50
  const ext = getFileExtension(format);
51
51
  outputPath = output.endsWith(ext) ? output : output + ext;
52
+ // Path traversal guard — output must resolve within the current working
53
+ // directory to prevent callers from writing to arbitrary filesystem paths.
54
+ const projectRoot = path.resolve(process.cwd());
55
+ const resolvedOutputPath = path.resolve(process.cwd(), outputPath);
56
+ if (!resolvedOutputPath.startsWith(projectRoot + path.sep) && resolvedOutputPath !== projectRoot) {
57
+ throw new Error(`Output path must resolve within the project directory: ${projectRoot}`);
58
+ }
59
+ outputPath = resolvedOutputPath;
52
60
  // Ensure directory exists
53
61
  const dir = path.dirname(outputPath);
54
62
  if (!fs.existsSync(dir)) {
@@ -6,6 +6,14 @@
6
6
  * @version 3.0.0
7
7
  */
8
8
  import * as crypto from 'crypto';
9
+ /**
10
+ * Sanitize a user-supplied filename before embedding it inside a multipart
11
+ * Content-Disposition header. Strip all CR/LF characters (MIME header injection)
12
+ * and cap length to prevent oversized headers.
13
+ */
14
+ function sanitizeFileName(name) {
15
+ return name.replace(/[\r\n]/g, '').slice(0, 200);
16
+ }
9
17
  /**
10
18
  * Get web3.storage token from environment or config
11
19
  */
@@ -42,7 +50,7 @@ async function uploadToWeb3Storage(content, options) {
42
50
  'Get a free token at: https://web3.storage');
43
51
  }
44
52
  const endpoint = options.endpoint || 'https://api.web3.storage';
45
- const name = options.name || 'pattern.cfp.json';
53
+ const name = sanitizeFileName(options.name || 'pattern.cfp.json');
46
54
  console.log(`[IPFS] Uploading ${content.length} bytes to web3.storage...`);
47
55
  // Create FormData-like body for upload
48
56
  const boundary = '----WebKitFormBoundary' + crypto.randomBytes(16).toString('hex');
@@ -91,7 +99,7 @@ async function uploadToPinata(content, options) {
91
99
  throw new Error('Pinata API credentials not found. Set PINATA_API_KEY and PINATA_API_SECRET.\n' +
92
100
  'Get credentials at: https://pinata.cloud');
93
101
  }
94
- const name = options.name || 'pattern.cfp.json';
102
+ const name = sanitizeFileName(options.name || 'pattern.cfp.json');
95
103
  console.log(`[IPFS] Uploading ${content.length} bytes to Pinata...`);
96
104
  const boundary = '----WebKitFormBoundary' + crypto.randomBytes(16).toString('hex');
97
105
  const metadata = JSON.stringify({ name });
@@ -277,14 +285,36 @@ export async function unpinContent(cid, options = {}) {
277
285
  console.log(`[IPFS] Demo unpinned`);
278
286
  return { success: true };
279
287
  }
288
+ /** Allowlisted IPFS gateway hosts for SSRF prevention */
289
+ const ALLOWED_GATEWAY_HOSTS_UPLOAD = new Set([
290
+ 'w3s.link',
291
+ 'gateway.pinata.cloud',
292
+ 'cloudflare-ipfs.com',
293
+ 'ipfs.io',
294
+ 'dweb.link',
295
+ ]);
296
+ function isAllowedGateway(gateway) {
297
+ try {
298
+ const parsed = new URL(gateway);
299
+ return parsed.protocol === 'https:' && ALLOWED_GATEWAY_HOSTS_UPLOAD.has(parsed.hostname);
300
+ }
301
+ catch {
302
+ return false;
303
+ }
304
+ }
280
305
  /**
281
306
  * Check if content exists on IPFS
282
307
  */
283
308
  export async function checkContent(cid, gateway = 'https://w3s.link') {
309
+ if (!isAllowedGateway(gateway)) {
310
+ console.warn(`[IPFS] Blocked checkContent: gateway not in allowlist: ${gateway}`);
311
+ return { exists: false };
312
+ }
284
313
  console.log(`[IPFS] Checking ${cid}...`);
285
314
  try {
286
315
  const response = await fetch(`${gateway}/ipfs/${cid}`, {
287
316
  method: 'HEAD',
317
+ signal: AbortSignal.timeout(10000),
288
318
  });
289
319
  if (response.ok) {
290
320
  const size = parseInt(response.headers.get('content-length') || '0', 10);
@@ -315,7 +345,7 @@ export function getIPNSURL(name, gateway = 'https://w3s.link') {
315
345
  */
316
346
  async function uploadToLocalIPFS(content, options) {
317
347
  const apiUrl = process.env.IPFS_API_URL || 'http://localhost:5001';
318
- const name = options.name || 'pattern.cfp.json';
348
+ const name = sanitizeFileName(options.name || 'pattern.cfp.json');
319
349
  console.log(`[IPFS] Uploading ${content.length} bytes to ${apiUrl}...`);
320
350
  const boundary = '----IPFSBoundary' + crypto.randomBytes(16).toString('hex');
321
351
  const body = Buffer.concat([
@@ -107,11 +107,17 @@ export function serializeToBuffer(cfp, format) {
107
107
  return Buffer.from(json, 'utf-8');
108
108
  }
109
109
  }
110
+ /** Maximum CFP payload size (10 MB) — prevents OOM on crafted inputs. */
111
+ const MAX_CFP_SIZE = 10 * 1024 * 1024;
110
112
  /**
111
113
  * Deserialize CFP from string/buffer
112
114
  */
113
115
  export function deserializeCFP(data) {
114
116
  const str = typeof data === 'string' ? data : data.toString('utf-8');
117
+ // Guard against OOM before parsing
118
+ if (str.length > MAX_CFP_SIZE) {
119
+ throw new Error(`CFP payload too large (${str.length} bytes; max ${MAX_CFP_SIZE})`);
120
+ }
115
121
  let parsed;
116
122
  try {
117
123
  parsed = JSON.parse(str);
@@ -119,9 +125,9 @@ export function deserializeCFP(data) {
119
125
  catch (e) {
120
126
  throw new Error(`Invalid CFP file: ${e instanceof Error ? e.message : String(e)}`);
121
127
  }
122
- // Validate magic bytes
128
+ // Validate magic bytes — use a fixed message to avoid reflecting arbitrary input
123
129
  if (parsed.magic !== 'CFP1') {
124
- throw new Error(`Invalid CFP format: expected magic 'CFP1', got '${parsed.magic}'`);
130
+ throw new Error('Invalid CFP format: unexpected magic bytes');
125
131
  }
126
132
  return parsed;
127
133
  }