@metabob/minibob 0.1.2

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 (174) hide show
  1. package/ARCHITECTURE.md +255 -0
  2. package/CHANGELOG.md +112 -0
  3. package/README.md +380 -0
  4. package/bin/minibob.js +36 -0
  5. package/dist/acp-gossip.d.ts +72 -0
  6. package/dist/acp-gossip.d.ts.map +1 -0
  7. package/dist/acp-gossip.js +156 -0
  8. package/dist/acp-gossip.js.map +1 -0
  9. package/dist/acp.d.ts +62 -0
  10. package/dist/acp.d.ts.map +1 -0
  11. package/dist/acp.js +292 -0
  12. package/dist/acp.js.map +1 -0
  13. package/dist/activity.d.ts +157 -0
  14. package/dist/activity.d.ts.map +1 -0
  15. package/dist/activity.js +518 -0
  16. package/dist/activity.js.map +1 -0
  17. package/dist/agent-runtime.d.ts +104 -0
  18. package/dist/agent-runtime.d.ts.map +1 -0
  19. package/dist/boredom.d.ts +125 -0
  20. package/dist/boredom.d.ts.map +1 -0
  21. package/dist/boredom.js +244 -0
  22. package/dist/boredom.js.map +1 -0
  23. package/dist/cli/acp-server.d.ts +23 -0
  24. package/dist/cli/acp-server.d.ts.map +1 -0
  25. package/dist/cli/burrow.d.ts +26 -0
  26. package/dist/cli/burrow.d.ts.map +1 -0
  27. package/dist/cli/doctor.d.ts +22 -0
  28. package/dist/cli/doctor.d.ts.map +1 -0
  29. package/dist/cli/goal.d.ts +22 -0
  30. package/dist/cli/goal.d.ts.map +1 -0
  31. package/dist/cli/index.d.ts +47 -0
  32. package/dist/cli/index.d.ts.map +1 -0
  33. package/dist/cli/instance-registry.d.ts +78 -0
  34. package/dist/cli/instance-registry.d.ts.map +1 -0
  35. package/dist/cli/observe.d.ts +35 -0
  36. package/dist/cli/observe.d.ts.map +1 -0
  37. package/dist/cli/vessel.d.ts +14 -0
  38. package/dist/cli/vessel.d.ts.map +1 -0
  39. package/dist/composition-observer.d.ts +96 -0
  40. package/dist/composition-observer.d.ts.map +1 -0
  41. package/dist/config.d.ts +36 -0
  42. package/dist/config.d.ts.map +1 -0
  43. package/dist/config.js +128 -0
  44. package/dist/config.js.map +1 -0
  45. package/dist/docker/Dockerfile +35 -0
  46. package/dist/environment.d.ts +72 -0
  47. package/dist/environment.d.ts.map +1 -0
  48. package/dist/environment.js +142 -0
  49. package/dist/environment.js.map +1 -0
  50. package/dist/goal-processor.d.ts +165 -0
  51. package/dist/goal-processor.d.ts.map +1 -0
  52. package/dist/helm/minibob-cluster/Chart.yaml +13 -0
  53. package/dist/helm/minibob-cluster/templates/_helpers.tpl +60 -0
  54. package/dist/helm/minibob-cluster/templates/configmap.yaml +11 -0
  55. package/dist/helm/minibob-cluster/templates/deployment.yaml +108 -0
  56. package/dist/helm/minibob-cluster/templates/secret.yaml +10 -0
  57. package/dist/helm/minibob-cluster/templates/service.yaml +37 -0
  58. package/dist/helm/minibob-cluster/values-local.yaml +41 -0
  59. package/dist/helm/minibob-cluster/values-production.yaml +57 -0
  60. package/dist/helm/minibob-cluster/values-testing-cluster.yaml +43 -0
  61. package/dist/helm/minibob-cluster/values.yaml +127 -0
  62. package/dist/improviser.d.ts +74 -0
  63. package/dist/improviser.d.ts.map +1 -0
  64. package/dist/impulse-filter.d.ts +74 -0
  65. package/dist/impulse-filter.d.ts.map +1 -0
  66. package/dist/impulse.d.ts +92 -0
  67. package/dist/impulse.d.ts.map +1 -0
  68. package/dist/impulse.js +234 -0
  69. package/dist/impulse.js.map +1 -0
  70. package/dist/lib.d.ts +29 -0
  71. package/dist/lib.d.ts.map +1 -0
  72. package/dist/lib.js +18561 -0
  73. package/dist/lib.js.map +98 -0
  74. package/dist/lifecycle-hooks.d.ts +99 -0
  75. package/dist/lifecycle-hooks.d.ts.map +1 -0
  76. package/dist/lifecycle-hooks.js +135 -0
  77. package/dist/lifecycle-hooks.js.map +1 -0
  78. package/dist/llm.d.ts +31 -0
  79. package/dist/llm.d.ts.map +1 -0
  80. package/dist/llm.js +349 -0
  81. package/dist/llm.js.map +1 -0
  82. package/dist/mcp-activity-bridge.d.ts +66 -0
  83. package/dist/mcp-activity-bridge.d.ts.map +1 -0
  84. package/dist/mcp-activity-bridge.js +126 -0
  85. package/dist/mcp-activity-bridge.js.map +1 -0
  86. package/dist/mcp.d.ts +216 -0
  87. package/dist/mcp.d.ts.map +1 -0
  88. package/dist/mcp.js +292 -0
  89. package/dist/mcp.js.map +1 -0
  90. package/dist/memory-agent.d.ts +92 -0
  91. package/dist/memory-agent.d.ts.map +1 -0
  92. package/dist/memory-agent.js +277 -0
  93. package/dist/memory-agent.js.map +1 -0
  94. package/dist/runtime-mapping.d.ts +97 -0
  95. package/dist/runtime-mapping.d.ts.map +1 -0
  96. package/dist/search-first-executor.d.ts +113 -0
  97. package/dist/search-first-executor.d.ts.map +1 -0
  98. package/dist/session.d.ts +48 -0
  99. package/dist/session.d.ts.map +1 -0
  100. package/dist/template-extractor.d.ts +9 -0
  101. package/dist/template-extractor.d.ts.map +1 -0
  102. package/dist/template-generator.d.ts +12 -0
  103. package/dist/template-generator.d.ts.map +1 -0
  104. package/dist/tools.d.ts +58 -0
  105. package/dist/tools.d.ts.map +1 -0
  106. package/dist/tools.js +771 -0
  107. package/dist/tools.js.map +1 -0
  108. package/dist/types.d.ts +503 -0
  109. package/dist/types.d.ts.map +1 -0
  110. package/dist/types.js +8 -0
  111. package/dist/types.js.map +1 -0
  112. package/dist/understanding/analyzer.d.ts +55 -0
  113. package/dist/understanding/analyzer.d.ts.map +1 -0
  114. package/dist/understanding/explorer.d.ts +73 -0
  115. package/dist/understanding/explorer.d.ts.map +1 -0
  116. package/dist/understanding/index.d.ts +7 -0
  117. package/dist/understanding/index.d.ts.map +1 -0
  118. package/dist/understanding/types.d.ts +136 -0
  119. package/dist/understanding/types.d.ts.map +1 -0
  120. package/dist/validation.d.ts +29 -0
  121. package/dist/validation.d.ts.map +1 -0
  122. package/dist/validation.js +106 -0
  123. package/dist/validation.js.map +1 -0
  124. package/dist/vessel-bootstrap.d.ts +190 -0
  125. package/dist/vessel-bootstrap.d.ts.map +1 -0
  126. package/dist/vessel-registry.d.ts +229 -0
  127. package/dist/vessel-registry.d.ts.map +1 -0
  128. package/index.ts +1329 -0
  129. package/package.json +54 -0
  130. package/src/acp-gossip.ts +193 -0
  131. package/src/acp.ts +362 -0
  132. package/src/activity.ts +1464 -0
  133. package/src/agent-runtime.ts +365 -0
  134. package/src/boredom.ts +423 -0
  135. package/src/cli/acp-server.ts +377 -0
  136. package/src/cli/burrow.ts +896 -0
  137. package/src/cli/doctor.ts +526 -0
  138. package/src/cli/goal.ts +224 -0
  139. package/src/cli/index.ts +147 -0
  140. package/src/cli/instance-registry.ts +271 -0
  141. package/src/cli/observe.ts +682 -0
  142. package/src/cli/vessel.ts +287 -0
  143. package/src/components/SystemOverview.tsx +331 -0
  144. package/src/composition-observer.ts +449 -0
  145. package/src/config.ts +172 -0
  146. package/src/environment.ts +167 -0
  147. package/src/goal-processor.ts +654 -0
  148. package/src/improviser.ts +591 -0
  149. package/src/impulse-filter.ts +273 -0
  150. package/src/impulse.ts +311 -0
  151. package/src/lib.ts +147 -0
  152. package/src/lifecycle-hooks.ts +181 -0
  153. package/src/llm.ts +434 -0
  154. package/src/mcp-activity-bridge.ts +158 -0
  155. package/src/mcp.ts +747 -0
  156. package/src/memory-agent.ts +316 -0
  157. package/src/runtime-mapping.ts +527 -0
  158. package/src/search-first-executor.ts +666 -0
  159. package/src/session.ts +141 -0
  160. package/src/template-extractor.ts +256 -0
  161. package/src/template-generator.ts +130 -0
  162. package/src/tools.ts +924 -0
  163. package/src/types.ts +497 -0
  164. package/src/understanding/analyzer.ts +354 -0
  165. package/src/understanding/explorer.ts +488 -0
  166. package/src/understanding/index.ts +27 -0
  167. package/src/understanding/types.ts +153 -0
  168. package/src/validation.ts +125 -0
  169. package/src/vessel-bootstrap.ts +440 -0
  170. package/src/vessel-registry.ts +621 -0
  171. package/templates/core/edit-file.json +85 -0
  172. package/templates/understanding/diagnose-problem.json +32 -0
  173. package/templates/understanding/explore-codebase-v2.json +57 -0
  174. package/templates/understanding/explore-codebase.json +37 -0
@@ -0,0 +1,526 @@
1
+ /**
2
+ * Doctor Command
3
+ *
4
+ * Health check and configuration management for minibob.
5
+ *
6
+ * Checks:
7
+ * 1. Configuration - API key, config file, working directory
8
+ * 2. Backend - MCP endpoint reachability and health
9
+ * 3. Templates - Local templates directory and validity
10
+ * 4. Vessels - Configured vessels and their status
11
+ */
12
+
13
+ import { parseArgs, formatHelp, printSuccess, printWarning, printInfo, exitWithError, ensureMinibobHome } from './index'
14
+ import { loadConfig } from '../config'
15
+
16
+ export interface DoctorOptions {
17
+ fix?: boolean
18
+ verbose?: boolean
19
+ json?: boolean
20
+ check?: string
21
+ }
22
+
23
+ interface CheckResult {
24
+ name: string
25
+ status: 'ok' | 'warning' | 'error'
26
+ message: string
27
+ details?: string
28
+ fixable?: boolean
29
+ fixed?: boolean
30
+ }
31
+
32
+ /**
33
+ * Run doctor command
34
+ */
35
+ export async function doctor(args: string[]): Promise<void> {
36
+ const { flags, values } = parseArgs(args)
37
+
38
+ // Show help
39
+ if (flags.help || flags.h) {
40
+ console.log(formatHelp('doctor', 'Health check and configuration management', [
41
+ { flag: '--fix', description: 'Attempt to fix issues' },
42
+ { flag: '--verbose', description: 'Show detailed diagnostics' },
43
+ { flag: '--json', description: 'Output as JSON' },
44
+ { flag: '--check <item>', description: 'Check specific item (config, backend, templates, vessels)' },
45
+ { flag: '--help, -h', description: 'Show this help message' },
46
+ ]))
47
+ return
48
+ }
49
+
50
+ const options: DoctorOptions = {
51
+ fix: flags.fix,
52
+ verbose: flags.verbose || flags.v,
53
+ json: flags.json,
54
+ check: values.check,
55
+ }
56
+
57
+ const results: CheckResult[] = []
58
+
59
+ // Determine which checks to run
60
+ const checksToRun = options.check
61
+ ? [options.check]
62
+ : ['config', 'backend', 'templates', 'vessels']
63
+
64
+ for (const check of checksToRun) {
65
+ switch (check) {
66
+ case 'config':
67
+ results.push(...(await checkConfiguration(options)))
68
+ break
69
+ case 'backend':
70
+ results.push(...(await checkBackend(options)))
71
+ break
72
+ case 'templates':
73
+ results.push(...(await checkTemplates(options)))
74
+ break
75
+ case 'vessels':
76
+ results.push(...(await checkVessels(options)))
77
+ break
78
+ default:
79
+ exitWithError(`Unknown check: ${check}`)
80
+ }
81
+ }
82
+
83
+ // Output results
84
+ if (options.json) {
85
+ console.log(JSON.stringify({ checks: results }, null, 2))
86
+ } else {
87
+ printResults(results, options.verbose)
88
+ }
89
+
90
+ // Exit with error if any checks failed
91
+ const hasErrors = results.some((r) => r.status === 'error')
92
+ if (hasErrors) {
93
+ process.exit(1)
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Check configuration
99
+ */
100
+ async function checkConfiguration(options: DoctorOptions): Promise<CheckResult[]> {
101
+ const results: CheckResult[] = []
102
+
103
+ // Check API key
104
+ const apiKey = process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY
105
+
106
+ if (!apiKey) {
107
+ results.push({
108
+ name: 'API Key',
109
+ status: 'error',
110
+ message: 'No API key found',
111
+ details: 'Set ANTHROPIC_API_KEY or OPENAI_API_KEY environment variable',
112
+ fixable: false,
113
+ })
114
+ } else {
115
+ // Validate API key format
116
+ const isValidFormat =
117
+ apiKey.startsWith('sk-ant-') || apiKey.startsWith('sk-') || apiKey.length > 20
118
+
119
+ if (!isValidFormat) {
120
+ results.push({
121
+ name: 'API Key',
122
+ status: 'warning',
123
+ message: 'API key format looks unusual',
124
+ details: `Key starts with: ${apiKey.slice(0, 7)}...`,
125
+ })
126
+ } else {
127
+ results.push({
128
+ name: 'API Key',
129
+ status: 'ok',
130
+ message: `Configured (${apiKey.slice(0, 7)}...${apiKey.slice(-4)})`,
131
+ })
132
+ }
133
+ }
134
+
135
+ // Check config file
136
+ try {
137
+ const config = await loadConfig()
138
+
139
+ results.push({
140
+ name: 'Config File',
141
+ status: 'ok',
142
+ message: 'Configuration loaded',
143
+ details: options.verbose
144
+ ? `Provider: ${config.provider}, Model: ${config.model}`
145
+ : undefined,
146
+ })
147
+
148
+ // Check working directory
149
+ const fs = await import('fs/promises')
150
+ try {
151
+ await fs.access(config.workingDirectory)
152
+ results.push({
153
+ name: 'Working Directory',
154
+ status: 'ok',
155
+ message: config.workingDirectory,
156
+ })
157
+ } catch {
158
+ results.push({
159
+ name: 'Working Directory',
160
+ status: 'error',
161
+ message: 'Directory not accessible',
162
+ details: config.workingDirectory,
163
+ fixable: options.fix,
164
+ })
165
+
166
+ if (options.fix) {
167
+ try {
168
+ await fs.mkdir(config.workingDirectory, { recursive: true })
169
+ results[results.length - 1]!.fixed = true
170
+ results[results.length - 1]!.status = 'ok'
171
+ results[results.length - 1]!.message = 'Directory created'
172
+ } catch {
173
+ // Couldn't fix
174
+ }
175
+ }
176
+ }
177
+ } catch (error) {
178
+ results.push({
179
+ name: 'Config File',
180
+ status: 'warning',
181
+ message: 'No config file found (using defaults)',
182
+ fixable: options.fix,
183
+ })
184
+
185
+ if (options.fix) {
186
+ try {
187
+ const defaultConfig = {
188
+ provider: 'anthropic',
189
+ model: 'claude-sonnet-4-20250514',
190
+ templatesDir: './templates',
191
+ autoCommit: false,
192
+ }
193
+ await Bun.write('minibob.json', JSON.stringify(defaultConfig, null, 2))
194
+ results[results.length - 1]!.fixed = true
195
+ results[results.length - 1]!.status = 'ok'
196
+ results[results.length - 1]!.message = 'Default config created'
197
+ } catch {
198
+ // Couldn't fix
199
+ }
200
+ }
201
+ }
202
+
203
+ // Check minibob home directory
204
+ try {
205
+ const home = await ensureMinibobHome()
206
+ results.push({
207
+ name: 'Data Directory',
208
+ status: 'ok',
209
+ message: home,
210
+ })
211
+ } catch {
212
+ results.push({
213
+ name: 'Data Directory',
214
+ status: 'warning',
215
+ message: 'Could not create ~/.minibob directory',
216
+ })
217
+ }
218
+
219
+ return results
220
+ }
221
+
222
+ /**
223
+ * Check backend connectivity
224
+ */
225
+ async function checkBackend(options: DoctorOptions): Promise<CheckResult[]> {
226
+ const results: CheckResult[] = []
227
+
228
+ const mcpEndpoint = process.env.MINIBOB_MCP_ENDPOINT || 'http://api.minibob.local'
229
+
230
+ // Check MCP endpoint
231
+ try {
232
+ const response = await fetch(`${mcpEndpoint}/health`, {
233
+ signal: AbortSignal.timeout(5000),
234
+ })
235
+
236
+ if (response.ok) {
237
+ const health = (await response.json()) as { status?: string }
238
+ results.push({
239
+ name: 'MCP Backend',
240
+ status: 'ok',
241
+ message: `Connected to ${mcpEndpoint}`,
242
+ details: options.verbose ? `Health: ${health.status || 'ok'}` : undefined,
243
+ })
244
+
245
+ // Check templates endpoint
246
+ try {
247
+ const templatesResponse = await fetch(`${mcpEndpoint}/v2/activities/templates?limit=1`, {
248
+ signal: AbortSignal.timeout(5000),
249
+ })
250
+
251
+ if (templatesResponse.ok) {
252
+ const data = (await templatesResponse.json()) as { templates?: any[] }
253
+ results.push({
254
+ name: 'Activity Templates',
255
+ status: 'ok',
256
+ message: `Backend has ${data.templates?.length || 0}+ templates`,
257
+ })
258
+ } else {
259
+ results.push({
260
+ name: 'Activity Templates',
261
+ status: 'warning',
262
+ message: 'Could not fetch templates',
263
+ })
264
+ }
265
+ } catch {
266
+ results.push({
267
+ name: 'Activity Templates',
268
+ status: 'warning',
269
+ message: 'Templates endpoint unreachable',
270
+ })
271
+ }
272
+ } else {
273
+ results.push({
274
+ name: 'MCP Backend',
275
+ status: 'error',
276
+ message: `Backend returned ${response.status}`,
277
+ details: mcpEndpoint,
278
+ })
279
+ }
280
+ } catch (error) {
281
+ results.push({
282
+ name: 'MCP Backend',
283
+ status: 'warning',
284
+ message: 'Backend unreachable (local mode enabled)',
285
+ details: mcpEndpoint,
286
+ })
287
+ }
288
+
289
+ return results
290
+ }
291
+
292
+ /**
293
+ * Check templates directory
294
+ */
295
+ async function checkTemplates(options: DoctorOptions): Promise<CheckResult[]> {
296
+ const results: CheckResult[] = []
297
+ const fs = await import('fs/promises')
298
+
299
+ try {
300
+ const config = await loadConfig()
301
+ const templatesDir = config.templatesDir
302
+
303
+ // Check directory exists
304
+ try {
305
+ await fs.access(templatesDir)
306
+ } catch {
307
+ results.push({
308
+ name: 'Templates Directory',
309
+ status: 'warning',
310
+ message: 'Directory not found',
311
+ details: templatesDir,
312
+ fixable: options.fix,
313
+ })
314
+
315
+ if (options.fix) {
316
+ try {
317
+ await fs.mkdir(templatesDir, { recursive: true })
318
+ results[results.length - 1]!.fixed = true
319
+ results[results.length - 1]!.status = 'ok'
320
+ results[results.length - 1]!.message = 'Directory created'
321
+ } catch {
322
+ // Couldn't fix
323
+ }
324
+ }
325
+
326
+ return results
327
+ }
328
+
329
+ // Count and validate templates
330
+ const glob = new Bun.Glob('**/*.json')
331
+ let validCount = 0
332
+ let invalidCount = 0
333
+ const invalidFiles: string[] = []
334
+
335
+ for await (const file of glob.scan({ cwd: templatesDir })) {
336
+ try {
337
+ const content = await Bun.file(`${templatesDir}/${file}`).json()
338
+
339
+ // Basic template validation
340
+ if (content.id && content.name && Array.isArray(content.tasks)) {
341
+ validCount++
342
+ } else {
343
+ invalidCount++
344
+ invalidFiles.push(file)
345
+ }
346
+ } catch {
347
+ invalidCount++
348
+ invalidFiles.push(file)
349
+ }
350
+ }
351
+
352
+ results.push({
353
+ name: 'Templates Directory',
354
+ status: 'ok',
355
+ message: templatesDir,
356
+ })
357
+
358
+ if (validCount > 0) {
359
+ results.push({
360
+ name: 'Valid Templates',
361
+ status: 'ok',
362
+ message: `${validCount} template(s) found`,
363
+ })
364
+ }
365
+
366
+ if (invalidCount > 0) {
367
+ results.push({
368
+ name: 'Invalid Templates',
369
+ status: 'warning',
370
+ message: `${invalidCount} invalid template(s)`,
371
+ details: options.verbose ? invalidFiles.join(', ') : undefined,
372
+ })
373
+ }
374
+
375
+ if (validCount === 0 && invalidCount === 0) {
376
+ results.push({
377
+ name: 'Templates',
378
+ status: 'warning',
379
+ message: 'No templates found',
380
+ })
381
+ }
382
+ } catch (error) {
383
+ results.push({
384
+ name: 'Templates',
385
+ status: 'error',
386
+ message: 'Could not check templates',
387
+ details: error instanceof Error ? error.message : String(error),
388
+ })
389
+ }
390
+
391
+ return results
392
+ }
393
+
394
+ /**
395
+ * Check configured vessels
396
+ */
397
+ async function checkVessels(options: DoctorOptions): Promise<CheckResult[]> {
398
+ const results: CheckResult[] = []
399
+
400
+ try {
401
+ const config = await loadConfig()
402
+
403
+ if (!config.vessels || Object.keys(config.vessels).length === 0) {
404
+ results.push({
405
+ name: 'Vessels',
406
+ status: 'warning',
407
+ message: 'No vessels configured',
408
+ })
409
+ return results
410
+ }
411
+
412
+ for (const [name, vesselConfig] of Object.entries(config.vessels)) {
413
+ if (vesselConfig.type === 'http' && vesselConfig.endpoint) {
414
+ try {
415
+ const response = await fetch(`${vesselConfig.endpoint}/health`, {
416
+ signal: AbortSignal.timeout(3000),
417
+ })
418
+
419
+ if (response.ok) {
420
+ results.push({
421
+ name: `Vessel: ${name}`,
422
+ status: 'ok',
423
+ message: 'Connected',
424
+ details: options.verbose ? vesselConfig.endpoint : undefined,
425
+ })
426
+ } else {
427
+ results.push({
428
+ name: `Vessel: ${name}`,
429
+ status: 'warning',
430
+ message: `Returned ${response.status}`,
431
+ details: vesselConfig.endpoint,
432
+ })
433
+ }
434
+ } catch {
435
+ results.push({
436
+ name: `Vessel: ${name}`,
437
+ status: 'warning',
438
+ message: 'Unreachable',
439
+ details: vesselConfig.endpoint,
440
+ })
441
+ }
442
+ } else {
443
+ results.push({
444
+ name: `Vessel: ${name}`,
445
+ status: 'ok',
446
+ message: `Type: ${vesselConfig.type}`,
447
+ details: options.verbose
448
+ ? `Capabilities: ${vesselConfig.capabilities?.join(', ') || 'none'}`
449
+ : undefined,
450
+ })
451
+ }
452
+ }
453
+ } catch (error) {
454
+ results.push({
455
+ name: 'Vessels',
456
+ status: 'error',
457
+ message: 'Could not check vessels',
458
+ details: error instanceof Error ? error.message : String(error),
459
+ })
460
+ }
461
+
462
+ return results
463
+ }
464
+
465
+ /**
466
+ * Print check results
467
+ */
468
+ function printResults(results: CheckResult[], verbose?: boolean): void {
469
+ console.log('\nminibob doctor\n')
470
+
471
+ const statusIcon = {
472
+ ok: '✓',
473
+ warning: '⚠',
474
+ error: '✗',
475
+ }
476
+
477
+ const statusColor = {
478
+ ok: '\x1b[32m',
479
+ warning: '\x1b[33m',
480
+ error: '\x1b[31m',
481
+ }
482
+
483
+ const reset = '\x1b[0m'
484
+
485
+ for (const result of results) {
486
+ const icon = statusIcon[result.status]
487
+ const color = statusColor[result.status]
488
+
489
+ console.log(`${color}${icon}${reset} ${result.name}: ${result.message}`)
490
+
491
+ if (verbose && result.details) {
492
+ console.log(` ${result.details}`)
493
+ }
494
+
495
+ if (result.fixed) {
496
+ console.log(` (fixed)`)
497
+ }
498
+ }
499
+
500
+ // Summary
501
+ const okCount = results.filter((r) => r.status === 'ok').length
502
+ const warningCount = results.filter((r) => r.status === 'warning').length
503
+ const errorCount = results.filter((r) => r.status === 'error').length
504
+
505
+ console.log('')
506
+ console.log(
507
+ `Summary: ${okCount} ok, ${warningCount} warnings, ${errorCount} errors`
508
+ )
509
+
510
+ if (errorCount === 0 && warningCount === 0) {
511
+ console.log('\n✓ All checks passed!\n')
512
+ } else if (errorCount > 0) {
513
+ console.log('\n✗ Some checks failed. Run with --fix to attempt repairs.\n')
514
+ } else {
515
+ console.log('\n⚠ Some warnings. System should still work.\n')
516
+ }
517
+ }
518
+
519
+ // CLI entry point
520
+ if (import.meta.main) {
521
+ const args = process.argv.slice(2)
522
+ doctor(args).catch((error) => {
523
+ console.error('Doctor failed:', error)
524
+ process.exit(1)
525
+ })
526
+ }