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
@@ -5,7 +5,7 @@
5
5
  * github.com/monoes/monomind
6
6
  */
7
7
  import { output } from '../output.js';
8
- import { existsSync, statSync, rmSync, readdirSync } from 'fs';
8
+ import { existsSync, lstatSync, rmSync, readdirSync } from 'fs';
9
9
  import { join } from 'path';
10
10
  /**
11
11
  * Artifact directories and files that monomind/monomind may create
@@ -30,11 +30,27 @@ const KEEP_CONFIG_PATHS = [
30
30
  join('.claude', 'settings.json'),
31
31
  ];
32
32
  /**
33
- * Calculate the total size of a path (file or directory) in bytes
33
+ * Maximum directory recursion depth for size calculation.
34
+ * Prevents stack overflow on deeply-nested or circular-symlink trees.
34
35
  */
35
- function getSize(fullPath) {
36
+ const MAX_SIZE_DEPTH = 20;
37
+ /**
38
+ * Calculate the total size of a path (file or directory) in bytes.
39
+ *
40
+ * Uses lstatSync (not statSync) so that symlinks are never followed:
41
+ * a symlink counts only the size of the link itself, not its target.
42
+ * This prevents a crafted symlink (e.g. .claude -> /) from causing
43
+ * the cleanup command to recursively traverse the entire filesystem.
44
+ */
45
+ function getSize(fullPath, depth = 0) {
46
+ if (depth > MAX_SIZE_DEPTH)
47
+ return 0;
36
48
  try {
37
- const stat = statSync(fullPath);
49
+ const stat = lstatSync(fullPath);
50
+ if (stat.isSymbolicLink()) {
51
+ // Count only the symlink entry itself; never traverse the target.
52
+ return stat.size;
53
+ }
38
54
  if (stat.isFile()) {
39
55
  return stat.size;
40
56
  }
@@ -42,7 +58,11 @@ function getSize(fullPath) {
42
58
  let total = 0;
43
59
  const entries = readdirSync(fullPath, { withFileTypes: true });
44
60
  for (const entry of entries) {
45
- total += getSize(join(fullPath, entry.name));
61
+ // Skip symlinks at the entry level too — lstatSync below will still
62
+ // catch them, but checking here avoids unnecessary path joins.
63
+ if (!entry.isSymbolicLink()) {
64
+ total += getSize(join(fullPath, entry.name), depth + 1);
65
+ }
46
66
  }
47
67
  return total;
48
68
  }
@@ -69,7 +69,7 @@ const getCommand = {
69
69
  { command: 'monomind config get -k memory.backend', description: 'Get memory backend' }
70
70
  ],
71
71
  action: async (ctx) => {
72
- const key = ctx.flags.key || ctx.args[0];
72
+ const key = (ctx.flags.key || ctx.args[0] || '').slice(0, 256);
73
73
  if (!key) {
74
74
  // Show all config from actual config file (fall back to defaults)
75
75
  const config = configManager.getConfig(ctx.cwd);
@@ -102,6 +102,14 @@ const getCommand = {
102
102
  });
103
103
  return { success: true, data: flatEntries };
104
104
  }
105
+ // Prototype pollution guard — mirrors the same check in setCommand.
106
+ const FORBIDDEN_KEY_SEGMENTS = new Set(['__proto__', 'constructor', 'prototype']);
107
+ for (const seg of key.split('.')) {
108
+ if (FORBIDDEN_KEY_SEGMENTS.has(seg)) {
109
+ output.printError(`Forbidden config key segment: "${seg}"`);
110
+ return { success: false, exitCode: 1 };
111
+ }
112
+ }
105
113
  const value = configManager.get(ctx.cwd, key);
106
114
  if (value === undefined) {
107
115
  output.printError(`Configuration key not found: ${key}`);
@@ -141,8 +149,8 @@ const setCommand = {
141
149
  { command: 'monomind config set -k memory.backend -v agentdb', description: 'Set memory backend' }
142
150
  ],
143
151
  action: async (ctx) => {
144
- const key = ctx.flags.key || ctx.args[0];
145
- const value = ctx.flags.value || ctx.args[1];
152
+ const key = (ctx.flags.key || ctx.args[0] || '').slice(0, 256);
153
+ const value = (ctx.flags.value ?? ctx.args[1] ?? '');
146
154
  if (!key || value === undefined) {
147
155
  output.printError('Both key and value are required');
148
156
  return { success: false, exitCode: 1 };
@@ -203,10 +211,10 @@ const providersCommand = {
203
211
  { name: 'gemini', model: 'gemini-2.0-flash', priority: 4, enabled: false, status: 'Disabled' }
204
212
  ];
205
213
  // Handle mutation flags
206
- const addProvider = ctx.flags.add;
207
- const removeProvider = ctx.flags.remove;
208
- const enableProvider = ctx.flags.enable;
209
- const disableProvider = ctx.flags.disable;
214
+ const addProvider = ctx.flags.add?.slice(0, 64);
215
+ const removeProvider = ctx.flags.remove?.slice(0, 64);
216
+ const enableProvider = ctx.flags.enable?.slice(0, 64);
217
+ const disableProvider = ctx.flags.disable?.slice(0, 64);
210
218
  if (addProvider || removeProvider || enableProvider || disableProvider) {
211
219
  // Read current providers from config
212
220
  let currentProviders = configManager.get(ctx.cwd, 'providers') || [];
@@ -327,6 +327,10 @@ async function killBackgroundDaemon(projectRoot) {
327
327
  return false;
328
328
  }
329
329
  try {
330
+ if (fs.statSync(pidFile).size > 32) {
331
+ fs.unlinkSync(pidFile);
332
+ return false;
333
+ }
330
334
  const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
331
335
  if (isNaN(pid)) {
332
336
  fs.unlinkSync(pidFile);
@@ -376,6 +380,8 @@ function getBackgroundDaemonPid(projectRoot) {
376
380
  return null;
377
381
  }
378
382
  try {
383
+ if (fs.statSync(pidFile).size > 32)
384
+ return null;
379
385
  const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
380
386
  return isNaN(pid) ? null : pid;
381
387
  }
@@ -19,11 +19,23 @@ function getStatePath(cwd) {
19
19
  function emptyState() {
20
20
  return { environments: {}, history: [], activeDeployment: undefined };
21
21
  }
22
+ const MAX_DEPLOYMENT_STATE_BYTES = 10 * 1024 * 1024; // 10 MB
23
+ // Input length caps to prevent DoS via unbounded strings stored to disk
24
+ const MAX_ENV_NAME_LEN = 128;
25
+ const MAX_VERSION_LEN = 64;
26
+ const MAX_DESCRIPTION_LEN = 1024;
27
+ const MAX_URL_LEN = 2048;
28
+ const MAX_ENV_TYPE_LEN = 64;
29
+ const MAX_HISTORY_LIMIT = 1000;
30
+ const MAX_LOGS_LIMIT = 1000;
22
31
  function loadDeploymentState(cwd) {
23
32
  const filePath = getStatePath(cwd);
24
33
  if (!fs.existsSync(filePath)) {
25
34
  return emptyState();
26
35
  }
36
+ if (fs.statSync(filePath).size > MAX_DEPLOYMENT_STATE_BYTES) {
37
+ return emptyState();
38
+ }
27
39
  try {
28
40
  const raw = fs.readFileSync(filePath, 'utf-8');
29
41
  const parsed = JSON.parse(raw);
@@ -66,6 +78,9 @@ function readProjectVersion(cwd) {
66
78
  if (!fs.existsSync(pkgPath)) {
67
79
  return null;
68
80
  }
81
+ if (fs.statSync(pkgPath).size > 1024 * 1024) {
82
+ return null;
83
+ }
69
84
  try {
70
85
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
71
86
  return pkg.version ?? null;
@@ -92,10 +107,10 @@ const deployCommand = {
92
107
  ],
93
108
  action: async (ctx) => {
94
109
  try {
95
- const envName = String(ctx.flags['env'] || 'staging');
110
+ const envName = String(ctx.flags['env'] || 'staging').slice(0, MAX_ENV_NAME_LEN);
96
111
  const dryRun = Boolean(ctx.flags['dry-run']);
97
- const description = ctx.flags['description'] ? String(ctx.flags['description']) : undefined;
98
- let version = ctx.flags['version'] ? String(ctx.flags['version']) : null;
112
+ const description = ctx.flags['description'] ? String(ctx.flags['description']).slice(0, MAX_DESCRIPTION_LEN) : undefined;
113
+ let version = ctx.flags['version'] ? String(ctx.flags['version']).slice(0, MAX_VERSION_LEN) : null;
99
114
  if (!version) {
100
115
  version = readProjectVersion(ctx.cwd) || '0.0.0';
101
116
  }
@@ -181,7 +196,7 @@ const statusCommand = {
181
196
  action: async (ctx) => {
182
197
  try {
183
198
  const state = loadDeploymentState(ctx.cwd);
184
- const filterEnv = ctx.flags['env'] ? String(ctx.flags['env']) : null;
199
+ const filterEnv = ctx.flags['env'] ? String(ctx.flags['env']).slice(0, MAX_ENV_NAME_LEN) : null;
185
200
  output.writeln();
186
201
  output.writeln(output.bold('Deployment Status'));
187
202
  output.writeln();
@@ -277,13 +292,13 @@ const rollbackCommand = {
277
292
  ],
278
293
  action: async (ctx) => {
279
294
  try {
280
- const envName = String(ctx.flags['env'] || '');
295
+ const envName = String(ctx.flags['env'] || '').slice(0, MAX_ENV_NAME_LEN);
281
296
  if (!envName) {
282
297
  output.printError('Environment is required', 'Use --env or -e to specify');
283
298
  return { success: false, exitCode: 1 };
284
299
  }
285
- const targetVersion = ctx.flags['version'] ? String(ctx.flags['version']) : null;
286
- const steps = parseInt(ctx.flags.steps || '1', 10);
300
+ const targetVersion = ctx.flags['version'] ? String(ctx.flags['version']).slice(0, MAX_VERSION_LEN) : null;
301
+ const steps = Math.min(Math.max(parseInt(ctx.flags.steps || '1', 10), 1), 100);
287
302
  if (steps > 1) {
288
303
  output.printWarning(`Multi-step rollback (--steps ${steps}) is not yet implemented. Rolling back 1 step only.`);
289
304
  }
@@ -370,8 +385,8 @@ const historyCommand = {
370
385
  action: async (ctx) => {
371
386
  try {
372
387
  const state = loadDeploymentState(ctx.cwd);
373
- const filterEnv = ctx.flags['env'] ? String(ctx.flags['env']) : null;
374
- const limit = Number(ctx.flags['limit']) || 10;
388
+ const filterEnv = ctx.flags['env'] ? String(ctx.flags['env']).slice(0, MAX_ENV_NAME_LEN) : null;
389
+ const limit = Math.min(Math.max(Number(ctx.flags['limit']) || 10, 1), MAX_HISTORY_LIMIT);
375
390
  let records = [...state.history].reverse();
376
391
  if (filterEnv) {
377
392
  records = records.filter(r => r.environment === filterEnv);
@@ -455,7 +470,7 @@ const environmentsCommand = {
455
470
  return { success: true };
456
471
  }
457
472
  if (action === 'add') {
458
- const name = ctx.flags['name'] ? String(ctx.flags['name']) : null;
473
+ const name = ctx.flags['name'] ? String(ctx.flags['name']).slice(0, MAX_ENV_NAME_LEN) : null;
459
474
  if (!name) {
460
475
  output.printError('Environment name is required', 'Use --name or -n to specify');
461
476
  return { success: false, exitCode: 1 };
@@ -464,8 +479,8 @@ const environmentsCommand = {
464
479
  output.printWarning(`Environment '${name}' already exists`);
465
480
  return { success: false, exitCode: 1 };
466
481
  }
467
- const envType = String(ctx.flags['type'] || 'local');
468
- const url = ctx.flags['url'] ? String(ctx.flags['url']) : undefined;
482
+ const envType = String(ctx.flags['type'] || 'local').slice(0, MAX_ENV_TYPE_LEN);
483
+ const url = ctx.flags['url'] ? String(ctx.flags['url']).slice(0, MAX_URL_LEN) : undefined;
469
484
  state.environments[name] = {
470
485
  name,
471
486
  type: envType,
@@ -481,7 +496,7 @@ const environmentsCommand = {
481
496
  return { success: true };
482
497
  }
483
498
  if (action === 'remove') {
484
- const name = ctx.flags['name'] ? String(ctx.flags['name']) : null;
499
+ const name = ctx.flags['name'] ? String(ctx.flags['name']).slice(0, MAX_ENV_NAME_LEN) : null;
485
500
  if (!name) {
486
501
  output.printError('Environment name is required', 'Use --name or -n to specify');
487
502
  return { success: false, exitCode: 1 };
@@ -524,9 +539,9 @@ const logsCommand = {
524
539
  action: async (ctx) => {
525
540
  try {
526
541
  const state = loadDeploymentState(ctx.cwd);
527
- const filterEnv = ctx.flags['env'] ? String(ctx.flags['env']) : null;
528
- const deploymentId = ctx.flags['deployment'] ? String(ctx.flags['deployment']) : null;
529
- const limit = Number(ctx.flags['lines']) || 50;
542
+ const filterEnv = ctx.flags['env'] ? String(ctx.flags['env']).slice(0, MAX_ENV_NAME_LEN) : null;
543
+ const deploymentId = ctx.flags['deployment'] ? String(ctx.flags['deployment']).slice(0, 64) : null;
544
+ const limit = Math.min(Math.max(Number(ctx.flags['lines']) || 50, 1), MAX_LOGS_LIMIT);
530
545
  output.writeln();
531
546
  output.writeln(output.bold('Deployment Logs'));
532
547
  output.writeln();
@@ -588,9 +603,9 @@ const releaseCommand = {
588
603
  ],
589
604
  action: async (ctx) => {
590
605
  try {
591
- const envName = String(ctx.flags['env'] || 'production');
592
- const description = ctx.flags['description'] ? String(ctx.flags['description']) : undefined;
593
- let version = ctx.flags['version'] ? String(ctx.flags['version']) : null;
606
+ const envName = String(ctx.flags['env'] || 'production').slice(0, MAX_ENV_NAME_LEN);
607
+ const description = ctx.flags['description'] ? String(ctx.flags['description']).slice(0, MAX_DESCRIPTION_LEN) : undefined;
608
+ let version = ctx.flags['version'] ? String(ctx.flags['version']).slice(0, MAX_VERSION_LEN) : null;
594
609
  if (!version) {
595
610
  const pkgVersion = readProjectVersion(ctx.cwd);
596
611
  if (!pkgVersion) {
@@ -7,6 +7,11 @@
7
7
  import { output } from '../output.js';
8
8
  import { existsSync, readFileSync, statSync } from 'fs';
9
9
  import { join, dirname } from 'path';
10
+ const MAX_DOCTOR_PKG_BYTES = 1024 * 1024; // 1 MB — package.json / settings.json
11
+ const MAX_DOCTOR_CONFIG_BYTES = 10 * 1024 * 1024; // 10 MB — monomind.config.json / MCP configs
12
+ const MAX_DOCTOR_GITIGNORE_BYTES = 512 * 1024; // 512 KB — .gitignore
13
+ const MAX_DOCTOR_PID_BYTES = 64; // 64 bytes — daemon PID file
14
+ const MAX_DOCTOR_HELPER_BYTES = 2 * 1024 * 1024; // 2 MB — hook helper .cjs bundles
10
15
  import { fileURLToPath } from 'url';
11
16
  import { execSync, exec } from 'child_process';
12
17
  import { homedir } from 'os';
@@ -67,7 +72,7 @@ async function checkConfigFile() {
67
72
  '.monomind.json'
68
73
  ];
69
74
  for (const configPath of jsonPaths) {
70
- if (existsSync(configPath)) {
75
+ if (existsSync(configPath) && statSync(configPath).size <= MAX_DOCTOR_CONFIG_BYTES) {
71
76
  try {
72
77
  const content = readFileSync(configPath, 'utf8');
73
78
  JSON.parse(content);
@@ -95,7 +100,7 @@ async function checkConfigFile() {
95
100
  async function checkDaemonStatus() {
96
101
  try {
97
102
  const pidFile = '.monomind/daemon.pid';
98
- if (existsSync(pidFile)) {
103
+ if (existsSync(pidFile) && statSync(pidFile).size <= MAX_DOCTOR_PID_BYTES) {
99
104
  const pid = readFileSync(pidFile, 'utf8').trim();
100
105
  try {
101
106
  process.kill(parseInt(pid, 10), 0); // Check if process exists
@@ -184,7 +189,7 @@ async function checkMcpServers() {
184
189
  '.mcp.json'
185
190
  ];
186
191
  for (const configPath of mcpConfigPaths) {
187
- if (existsSync(configPath)) {
192
+ if (existsSync(configPath) && statSync(configPath).size <= MAX_DOCTOR_CONFIG_BYTES) {
188
193
  try {
189
194
  const content = JSON.parse(readFileSync(configPath, 'utf8'));
190
195
  const servers = content.mcpServers || content.servers || {};
@@ -202,7 +207,7 @@ async function checkMcpServers() {
202
207
  }
203
208
  }
204
209
  }
205
- return { name: 'MCP Servers', status: 'warn', message: 'No MCP config found', fix: 'claude mcp add monomind npx @monomind/cli@v1alpha mcp start' };
210
+ return { name: 'MCP Servers', status: 'warn', message: 'No MCP config found', fix: 'claude mcp add monomind -- npx -y monomind@latest mcp start' };
206
211
  }
207
212
  // Check disk space (async with proper env inheritance)
208
213
  async function checkDiskSpace() {
@@ -260,11 +265,11 @@ async function checkVersionFreshness() {
260
265
  for (;;) {
261
266
  const candidate = join(dir, 'package.json');
262
267
  try {
263
- if (existsSync(candidate)) {
268
+ if (existsSync(candidate) && statSync(candidate).size <= MAX_DOCTOR_PKG_BYTES) {
264
269
  const pkg = JSON.parse(readFileSync(candidate, 'utf8'));
265
270
  if (pkg.version &&
266
271
  typeof pkg.name === 'string' &&
267
- (pkg.name === '@monomind/cli' || pkg.name === 'monomind')) {
272
+ (pkg.name === '@monomind/cli' || pkg.name === 'monomind' || pkg.name === '@monoes/monomindcli')) {
268
273
  currentVersion = pkg.version;
269
274
  break;
270
275
  }
@@ -287,10 +292,10 @@ async function checkVersionFreshness() {
287
292
  const isNpx = process.argv[1]?.includes('_npx') ||
288
293
  process.env.npm_execpath?.includes('npx') ||
289
294
  process.cwd().includes('_npx');
290
- // Query npm for latest version (using alpha tag since that's what we publish to)
295
+ // Query npm for latest version of the published umbrella package
291
296
  let latestVersion = currentVersion;
292
297
  try {
293
- const npmInfo = await runCommand('npm view @monomind/cli@alpha version', 5000);
298
+ const npmInfo = await runCommand('npm view monomind version', 5000);
294
299
  latestVersion = npmInfo.trim();
295
300
  }
296
301
  catch {
@@ -322,8 +327,8 @@ async function checkVersionFreshness() {
322
327
  (latest.major === current.major && latest.minor === current.minor && latest.patch === current.patch && latest.prerelease > current.prerelease));
323
328
  if (isOutdated) {
324
329
  const fix = isNpx
325
- ? 'rm -rf ~/.npm/_npx/* && npx -y @monomind/cli@latest'
326
- : 'npm update @monomind/cli';
330
+ ? 'rm -rf ~/.npm/_npx/* && npx -y monomind@latest doctor'
331
+ : 'npm update -g monomind';
327
332
  return {
328
333
  name: 'Version Freshness',
329
334
  status: 'warn',
@@ -387,18 +392,39 @@ async function installClaudeCode() {
387
392
  async function checkMonograph() {
388
393
  try {
389
394
  const __filename = fileURLToPath(import.meta.url);
395
+ const _base = dirname(__filename);
396
+ let _globalRoot = '';
397
+ try {
398
+ _globalRoot = execSync('npm root -g', { encoding: 'utf8', timeout: 3000 }).trim();
399
+ }
400
+ catch { /* no npm */ }
390
401
  const candidates = [
391
- join(dirname(__filename), '..', '..', 'node_modules', '@monomind', 'monograph', 'package.json'),
392
- join(dirname(__filename), '..', '..', '..', '..', 'node_modules', '@monomind', 'monograph', 'package.json'),
402
+ // local dev monorepo paths (both old @monomind and published @monoes scope)
403
+ join(_base, '..', '..', 'node_modules', '@monomind', 'monograph', 'package.json'),
404
+ join(_base, '..', '..', '..', '..', 'node_modules', '@monomind', 'monograph', 'package.json'),
405
+ join(_base, '..', '..', 'node_modules', '@monoes', 'monograph', 'package.json'),
406
+ join(_base, '..', '..', '..', '..', 'node_modules', '@monoes', 'monograph', 'package.json'),
407
+ // global install paths
408
+ ...(_globalRoot ? [
409
+ join(_globalRoot, '@monomind', 'monograph', 'package.json'),
410
+ join(_globalRoot, '@monoes', 'monograph', 'package.json'),
411
+ ] : []),
393
412
  ];
394
- if (candidates.some(p => existsSync(p))) {
395
- return { name: 'Monograph', status: 'pass', message: 'available (knowledge graph engine)' };
413
+ const found = candidates.find(p => existsSync(p) && statSync(p).size <= MAX_DOCTOR_PKG_BYTES);
414
+ if (found) {
415
+ try {
416
+ const pkg = JSON.parse(readFileSync(found, 'utf-8'));
417
+ return { name: 'Monograph', status: 'pass', message: `v${pkg.version || '?'} available (knowledge graph engine)` };
418
+ }
419
+ catch {
420
+ return { name: 'Monograph', status: 'pass', message: 'available (knowledge graph engine)' };
421
+ }
396
422
  }
397
423
  return {
398
424
  name: 'Monograph',
399
425
  status: 'warn',
400
426
  message: 'Package not found (knowledge graph disabled)',
401
- fix: 'npm install in project root'
427
+ fix: 'npm install -g monomind@latest # reinstall to get @monoes/monograph'
402
428
  };
403
429
  }
404
430
  catch {
@@ -406,10 +432,124 @@ async function checkMonograph() {
406
432
  name: 'Monograph',
407
433
  status: 'warn',
408
434
  message: 'Package check failed (knowledge graph may be unavailable)',
409
- fix: 'npm install in project root'
435
+ fix: 'npm install -g monomind@latest'
410
436
  };
411
437
  }
412
438
  }
439
+ // Check monograph graph freshness (is the graph built? how stale?)
440
+ async function checkMonographFreshness() {
441
+ try {
442
+ const cwd = process.cwd();
443
+ const dbPath = join(cwd, '.monomind', 'monograph.db');
444
+ const lockPath = join(cwd, '.monomind', 'graph', '.rebuild-lock');
445
+ const statsPath = join(cwd, '.monomind', 'graph', 'stats.json');
446
+ // Check if graph exists at all
447
+ const hasDb = existsSync(dbPath);
448
+ const hasLock = existsSync(lockPath);
449
+ const hasStats = existsSync(statsPath);
450
+ if (!hasDb && !hasStats) {
451
+ return {
452
+ name: 'Graph freshness',
453
+ status: 'warn',
454
+ message: 'No monograph graph built yet',
455
+ fix: 'mcp__monomind__monograph_build codeOnly:true — or run npx monomind@latest hooks graph-status',
456
+ };
457
+ }
458
+ // Determine last build time
459
+ let buildMs = 0;
460
+ if (hasDb) {
461
+ try {
462
+ buildMs = Math.max(buildMs, statSync(dbPath).mtimeMs);
463
+ }
464
+ catch { /* ignore */ }
465
+ }
466
+ if (hasLock) {
467
+ try {
468
+ buildMs = Math.max(buildMs, statSync(lockPath).mtimeMs);
469
+ }
470
+ catch { /* ignore */ }
471
+ }
472
+ if (hasStats) {
473
+ try {
474
+ buildMs = Math.max(buildMs, statSync(statsPath).mtimeMs);
475
+ }
476
+ catch { /* ignore */ }
477
+ }
478
+ if (buildMs === 0) {
479
+ return { name: 'Graph freshness', status: 'warn', message: 'Graph exists but build time unknown' };
480
+ }
481
+ // Count commits since last build
482
+ const buildIso = new Date(buildMs).toISOString();
483
+ let commitsBehind = 0;
484
+ try {
485
+ const out = execSync(`git rev-list --count --since='${buildIso}' HEAD 2>/dev/null`, {
486
+ encoding: 'utf8', timeout: 2000, cwd,
487
+ }).trim();
488
+ commitsBehind = parseInt(out, 10) || 0;
489
+ }
490
+ catch { /* git not available or not a git repo */ }
491
+ const ageMinutes = Math.floor((Date.now() - buildMs) / 60000);
492
+ const ageStr = ageMinutes < 60 ? `${ageMinutes}m ago` : `${Math.floor(ageMinutes / 60)}h ago`;
493
+ if (commitsBehind === 0) {
494
+ return { name: 'Graph freshness', status: 'pass', message: `FRESH — built ${ageStr}, 0 commits behind` };
495
+ }
496
+ else if (commitsBehind <= 5) {
497
+ return {
498
+ name: 'Graph freshness',
499
+ status: 'warn',
500
+ message: `${commitsBehind} commit(s) behind — built ${ageStr}`,
501
+ fix: 'mcp__monomind__monograph_build codeOnly:true',
502
+ };
503
+ }
504
+ else {
505
+ return {
506
+ name: 'Graph freshness',
507
+ status: 'fail',
508
+ message: `STALE — ${commitsBehind} commits behind (built ${ageStr})`,
509
+ fix: 'mcp__monomind__monograph_build codeOnly:true',
510
+ };
511
+ }
512
+ }
513
+ catch {
514
+ return { name: 'Graph freshness', status: 'warn', message: 'Could not check graph freshness' };
515
+ }
516
+ }
517
+ // Check @monoes/memory (optional HNSW vector search package)
518
+ async function checkMonoesMemory() {
519
+ try {
520
+ const __filename = fileURLToPath(import.meta.url);
521
+ const _base = dirname(__filename);
522
+ let _globalRoot = '';
523
+ try {
524
+ _globalRoot = execSync('npm root -g', { encoding: 'utf8', timeout: 3000 }).trim();
525
+ }
526
+ catch { /* no npm */ }
527
+ const candidates = [
528
+ join(_base, '..', '..', 'node_modules', '@monoes', 'memory', 'package.json'),
529
+ join(_base, '..', '..', '..', '..', 'node_modules', '@monoes', 'memory', 'package.json'),
530
+ ...(_globalRoot ? [join(_globalRoot, '@monoes', 'memory', 'package.json')] : []),
531
+ ];
532
+ const found = candidates.find(p => existsSync(p) && statSync(p).size <= MAX_DOCTOR_PKG_BYTES);
533
+ if (found) {
534
+ try {
535
+ const pkg = JSON.parse(readFileSync(found, 'utf-8'));
536
+ return { name: 'Vector Memory', status: 'pass', message: `@monoes/memory v${pkg.version || '?'} (HNSW search enabled)` };
537
+ }
538
+ catch {
539
+ return { name: 'Vector Memory', status: 'pass', message: '@monoes/memory available (HNSW search enabled)' };
540
+ }
541
+ }
542
+ return {
543
+ name: 'Vector Memory',
544
+ status: 'warn',
545
+ message: '@monoes/memory not installed (vector search disabled — using fallback)',
546
+ fix: 'npm install @monoes/memory'
547
+ };
548
+ }
549
+ catch {
550
+ return { name: 'Vector Memory', status: 'warn', message: 'Vector memory check failed' };
551
+ }
552
+ }
413
553
  // Check agentic-flow v1 integration (filesystem-based to avoid slow WASM/DB init)
414
554
  // Resolve the path to the bundled (npm-installed) copy of a helper file.
415
555
  // Walks up from this module's location to find the package root, then joins
@@ -420,10 +560,10 @@ function _resolveBundledHelper(relativePath) {
420
560
  let dir = dirname(thisFile);
421
561
  for (;;) {
422
562
  const candidate = join(dir, 'package.json');
423
- if (existsSync(candidate)) {
563
+ if (existsSync(candidate) && statSync(candidate).size <= MAX_DOCTOR_PKG_BYTES) {
424
564
  try {
425
565
  const pkg = JSON.parse(readFileSync(candidate, 'utf8'));
426
- if (pkg.name === '@monomind/cli' || pkg.name === 'monomind') {
566
+ if (pkg.name === '@monomind/cli' || pkg.name === 'monomind' || pkg.name === '@monoes/monomindcli') {
427
567
  const helperPath = join(dir, relativePath);
428
568
  return existsSync(helperPath) ? helperPath : null;
429
569
  }
@@ -451,11 +591,15 @@ async function _detectStaleHelpers() {
451
591
  const local = join(process.cwd(), '.claude', 'helpers', name);
452
592
  if (!existsSync(local))
453
593
  continue;
594
+ if (statSync(local).size > MAX_DOCTOR_HELPER_BYTES)
595
+ continue; // skip oversized helper
454
596
  const bundled = _resolveBundledHelper(join('.claude', 'helpers', name));
455
597
  if (!bundled) {
456
598
  missing.push(name);
457
599
  continue;
458
600
  }
601
+ if (statSync(bundled).size > MAX_DOCTOR_HELPER_BYTES)
602
+ continue; // skip oversized bundled
459
603
  try {
460
604
  const hashLocal = crypto.createHash('sha256').update(readFileSync(local)).digest('hex');
461
605
  const hashBundled = crypto.createHash('sha256').update(readFileSync(bundled)).digest('hex');
@@ -513,6 +657,9 @@ async function checkAgenticFlow() {
513
657
  fix: 'npm install agentic-flow@latest'
514
658
  };
515
659
  }
660
+ if (statSync(pkgJsonPath).size > MAX_DOCTOR_PKG_BYTES) {
661
+ return { name: 'agentic-flow', status: 'warn', message: 'package.json too large to parse' };
662
+ }
516
663
  const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
517
664
  const version = pkg.version || 'unknown';
518
665
  const exports = pkg.exports || {};
@@ -524,7 +671,7 @@ async function checkAgenticFlow() {
524
671
  return {
525
672
  name: 'agentic-flow',
526
673
  status: 'pass',
527
- message: `v${version} (${features.join(', ')})`
674
+ message: `v${version}${features.length ? ' (' + features.join(', ') + ')' : ' (installed)'}`
528
675
  };
529
676
  }
530
677
  catch {
@@ -609,6 +756,9 @@ async function checkGitignoreCoverage() {
609
756
  fix: 'echo ".monomind/\\n**/.monomind/" >> .gitignore',
610
757
  };
611
758
  }
759
+ if (statSync(gitignorePath).size > MAX_DOCTOR_GITIGNORE_BYTES) {
760
+ return { name: 'Gitignore Coverage', status: 'warn', message: '.gitignore too large to parse' };
761
+ }
612
762
  const content = readFileSync(gitignorePath, 'utf-8');
613
763
  const lines = content.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#'));
614
764
  const missing = REQUIRED_GITIGNORE_PATTERNS.filter(({ pattern }) => {
@@ -651,6 +801,9 @@ async function checkGuidanceGates() {
651
801
  };
652
802
  }
653
803
  try {
804
+ if (statSync(settingsPath).size > MAX_DOCTOR_CONFIG_BYTES) {
805
+ return { name: 'Guidance Gates', status: 'warn', message: 'settings.json too large to parse' };
806
+ }
654
807
  const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
655
808
  const preToolUse = settings?.hooks?.PreToolUse ?? [];
656
809
  const hasPreWrite = preToolUse.some(e => e.matcher === 'Write|Edit|MultiEdit' && e.hooks.some(h => h.command?.includes('pre-write')));
@@ -723,7 +876,7 @@ export const doctorCommand = {
723
876
  {
724
877
  name: 'component',
725
878
  short: 'c',
726
- description: 'Check specific component (version, node, npm, config, daemon, memory, api, git, mcp, claude, disk, typescript, monograph, helpers, monoes, gates, gitignore)',
879
+ description: 'Check specific component (version, node, npm, config, daemon, memory, api, git, mcp, claude, disk, typescript, monograph, graph-freshness, memory-pkg, helpers, agentic-flow, monoes, gates, gitignore)',
727
880
  type: 'string'
728
881
  },
729
882
  {
@@ -766,6 +919,8 @@ export const doctorCommand = {
766
919
  checkDiskSpace,
767
920
  checkBuildTools,
768
921
  checkMonograph,
922
+ checkMonographFreshness,
923
+ checkMonoesMemory,
769
924
  checkHelpersFresh,
770
925
  checkAgenticFlow,
771
926
  checkMonoesIntegration,
@@ -787,6 +942,8 @@ export const doctorCommand = {
787
942
  'disk': checkDiskSpace,
788
943
  'typescript': checkBuildTools,
789
944
  'monograph': checkMonograph,
945
+ 'graph-freshness': checkMonographFreshness,
946
+ 'memory-pkg': checkMonoesMemory,
790
947
  'helpers': checkHelpersFresh,
791
948
  'agentic-flow': checkAgenticFlow,
792
949
  'monoes': checkMonoesIntegration,
@@ -812,6 +969,14 @@ export const doctorCommand = {
812
969
  const result = settledResult.value;
813
970
  results.push(result);
814
971
  output.writeln(formatCheck(result));
972
+ if (result.fix && result.status === 'fail') {
973
+ // Always show fix inline for failures — no flag needed
974
+ output.writeln(output.dim(` Fix: ${result.fix}`));
975
+ }
976
+ else if (result.fix && result.status === 'warn') {
977
+ // Show fix inline for warnings too, so users don't need --fix for common issues
978
+ output.writeln(output.dim(` Hint: ${result.fix}`));
979
+ }
815
980
  if (result.fix && (result.status === 'fail' || result.status === 'warn')) {
816
981
  fixes.push(`${result.name}: ${result.fix}`);
817
982
  }
@@ -872,9 +1037,13 @@ export const doctorCommand = {
872
1037
  output.writeln(output.dim(` ${fix}`));
873
1038
  }
874
1039
  }
875
- else if (fixes.length > 0 && !showFix) {
876
- output.writeln();
877
- output.writeln(output.dim(`Run with --fix to see ${fixes.length} suggested fix${fixes.length > 1 ? 'es' : ''}`));
1040
+ else if (!showFix) {
1041
+ // Only nudge about --fix for warnings (failures already showed their fix inline)
1042
+ const warnFixes = results.filter(r => r.status === 'warn' && r.fix).length;
1043
+ if (warnFixes > 0) {
1044
+ output.writeln();
1045
+ output.writeln(output.dim(`Run with --fix to see ${warnFixes} suggested fix${warnFixes > 1 ? 'es' : ''} for warnings`));
1046
+ }
878
1047
  }
879
1048
  // Overall result
880
1049
  if (failed > 0) {