@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,682 @@
1
+ /**
2
+ * Observe Command
3
+ *
4
+ * Observe running instances OR codebases to extract/validate activities.
5
+ *
6
+ * Two modes:
7
+ * 1. Instance mode (default): View execution state of running minibob instances
8
+ * 2. Codebase mode (--codebase): Observe a codebase to extract activities
9
+ *
10
+ * The codebase mode extracts the instructional→functional mappings:
11
+ * - Developer Intent → Written Code (activities inferred from functions)
12
+ * - Written Code → Runtime (traced via execution)
13
+ *
14
+ * Discovery mechanisms for instance mode:
15
+ * 1. Explicit targets (--target flag)
16
+ * 2. Local registry (~/.minibob/instances.json)
17
+ * 3. Port scanning (--scan flag)
18
+ * 4. Backend query (--backend flag) - centralized view from metabob-activity-api
19
+ */
20
+
21
+ import { parseArgs, formatHelp, printInfo, printWarning, printSuccess } from './index'
22
+ import {
23
+ listInstances,
24
+ scanForInstances,
25
+ fetchInstanceStatus,
26
+ cleanupStaleInstances,
27
+ type MinibobInstance,
28
+ type InstanceStatus,
29
+ } from './instance-registry'
30
+ import {
31
+ analyzeCodebase,
32
+ mapTraceToSource,
33
+ inferIntent,
34
+ type CodebaseAnalysis,
35
+ } from '../runtime-mapping'
36
+
37
+ export interface ObserveOptions {
38
+ target?: string
39
+ codebase?: string
40
+ list?: boolean
41
+ follow?: boolean
42
+ scan?: boolean
43
+ backend?: boolean
44
+ json?: boolean
45
+ interval?: number
46
+ validate?: boolean
47
+ }
48
+
49
+ /**
50
+ * Run observe command
51
+ */
52
+ export async function observe(args: string[]): Promise<void> {
53
+ const { flags, values, positional } = parseArgs(args)
54
+
55
+ // Show help
56
+ if (flags.help || flags.h) {
57
+ console.log(formatHelp('observe', 'Observe instances or codebases', [
58
+ { flag: '--codebase <path>', description: 'Observe a codebase to extract activities' },
59
+ { flag: '--validate', description: 'Validate observed activities against execution traces' },
60
+ { flag: '--target <url>', description: 'Connect to specific instance (e.g., http://localhost:8080)' },
61
+ { flag: '--list', description: 'List instances from registry (default)' },
62
+ { flag: '--follow', description: 'Continuously monitor (refresh every 2s)' },
63
+ { flag: '--scan', description: 'Scan localhost ports 8080-8090 for instances' },
64
+ { flag: '--backend', description: 'Query backend for all registered vessels' },
65
+ { flag: '--json', description: 'Output as JSON' },
66
+ { flag: '--help, -h', description: 'Show this help message' },
67
+ ]))
68
+ return
69
+ }
70
+
71
+ const options: ObserveOptions = {
72
+ target: values.target,
73
+ codebase: values.codebase || positional[0], // Allow positional arg for codebase
74
+ list: flags.list,
75
+ follow: flags.follow || flags.f,
76
+ scan: flags.scan,
77
+ backend: flags.backend || flags.b,
78
+ json: flags.json,
79
+ interval: values.interval ? parseInt(values.interval) : 2000,
80
+ validate: flags.validate,
81
+ }
82
+
83
+ // If codebase specified, observe the codebase
84
+ if (options.codebase) {
85
+ await observeCodebase(options.codebase, options)
86
+ return
87
+ }
88
+
89
+ // If target specified, observe that instance
90
+ if (options.target) {
91
+ await observeTarget(options.target, options)
92
+ return
93
+ }
94
+
95
+ // If backend query requested
96
+ if (options.backend) {
97
+ await queryBackend(options)
98
+ return
99
+ }
100
+
101
+ // If scan requested, scan for instances
102
+ if (options.scan) {
103
+ await scanAndObserve(options)
104
+ return
105
+ }
106
+
107
+ // Default: list from registry
108
+ await listAndObserve(options)
109
+ }
110
+
111
+ /**
112
+ * Observe a codebase to extract activities (instructional→functional mappings)
113
+ *
114
+ * Activities ARE the mappings:
115
+ * - Developer Intent → Written Code (what functions do)
116
+ * - Written Code → Runtime (execution behavior)
117
+ */
118
+ async function observeCodebase(path: string, options: ObserveOptions): Promise<void> {
119
+ const { existsSync } = await import('fs')
120
+ const { resolve } = await import('path')
121
+
122
+ const fullPath = resolve(path)
123
+
124
+ if (!existsSync(fullPath)) {
125
+ printWarning(`Path not found: ${fullPath}`)
126
+ return
127
+ }
128
+
129
+ printInfo(`Observing codebase: ${fullPath}`)
130
+ console.log('')
131
+
132
+ // Use existing analyzeCodebase from runtime-mapping.ts
133
+ const analysis = await analyzeCodebase(fullPath)
134
+
135
+ if (options.json) {
136
+ console.log(JSON.stringify(analysis, null, 2))
137
+ return
138
+ }
139
+
140
+ // Display as activities (the instructional→functional mappings)
141
+ console.log('┌─ Observed Activities (Intent → Code Mappings) ──────────────┐\n')
142
+
143
+ console.log(` Entry Points: ${analysis.entryPoints.length}`)
144
+ for (const entry of analysis.entryPoints) {
145
+ console.log(` • ${entry}`)
146
+ }
147
+ console.log('')
148
+
149
+ console.log(` Functions: ${analysis.functions.length}`)
150
+ console.log(` Classes: ${analysis.classes.length}`)
151
+ console.log(` Modules: ${analysis.modules.length}`)
152
+ console.log('')
153
+
154
+ // Show functions as activities (each function is an intent→code mapping)
155
+ const activitiesFound: Array<{ name: string; intent: string; confidence: number; file: string }> = []
156
+
157
+ for (const func of analysis.functions) {
158
+ const intent = inferIntent(func, analysis.classes, analysis.documentation)
159
+ activitiesFound.push({
160
+ name: func.name,
161
+ intent: intent.purpose,
162
+ confidence: intent.confidence,
163
+ file: func.file,
164
+ })
165
+ }
166
+
167
+ // Sort by confidence
168
+ activitiesFound.sort((a, b) => b.confidence - a.confidence)
169
+
170
+ // Show top activities
171
+ const topActivities = activitiesFound.slice(0, 20)
172
+ console.log(` Top Activities by Confidence:\n`)
173
+
174
+ for (const activity of topActivities) {
175
+ const confidenceBar = '█'.repeat(Math.round(activity.confidence * 10))
176
+ const spaces = ' '.repeat(10 - Math.round(activity.confidence * 10))
177
+ console.log(` ${activity.name}`)
178
+ console.log(` Intent: "${activity.intent}"`)
179
+ console.log(` Confidence: [${confidenceBar}${spaces}] ${(activity.confidence * 100).toFixed(0)}%`)
180
+ console.log(` File: ${activity.file}`)
181
+ console.log('')
182
+ }
183
+
184
+ if (activitiesFound.length > 20) {
185
+ console.log(` ... and ${activitiesFound.length - 20} more activities`)
186
+ console.log('')
187
+ }
188
+
189
+ console.log('└─────────────────────────────────────────────────────────────┘\n')
190
+
191
+ // If validate flag, compare with execution traces
192
+ if (options.validate) {
193
+ await validateObservation(analysis, options)
194
+ }
195
+
196
+ printSuccess(`Observed ${activitiesFound.length} activities in codebase`)
197
+ printInfo('Use --validate to compare against execution traces')
198
+ printInfo('Use --json for machine-readable output')
199
+ }
200
+
201
+ /**
202
+ * Validate observed activities against actual execution traces
203
+ */
204
+ async function validateObservation(analysis: CodebaseAnalysis, options: ObserveOptions): Promise<void> {
205
+ const backendUrl = process.env.MINIBOB_MCP_ENDPOINT || 'http://api.minibob.local'
206
+
207
+ printInfo(`\nValidating against execution traces from: ${backendUrl}`)
208
+
209
+ try {
210
+ // Fetch recent execution traces
211
+ const response = await fetch(`${backendUrl}/v2/activities/execution-traces?limit=100`, {
212
+ signal: AbortSignal.timeout(10000),
213
+ })
214
+
215
+ if (!response.ok) {
216
+ printWarning(`Could not fetch execution traces: ${response.status}`)
217
+ return
218
+ }
219
+
220
+ const data = await response.json() as { executions?: Array<{ tool_calls?: string[] }> }
221
+ const traces = data.executions || []
222
+
223
+ console.log(`\n┌─ Validation: Observed vs Executed ─────────────────────────┐\n`)
224
+ console.log(` Execution traces found: ${traces.length}`)
225
+
226
+ // Extract function names from traces
227
+ const executedFunctions = new Set<string>()
228
+ for (const trace of traces) {
229
+ // Tool calls contain function names
230
+ if (trace.tool_calls) {
231
+ for (const call of trace.tool_calls) {
232
+ executedFunctions.add(call)
233
+ }
234
+ }
235
+ }
236
+
237
+ // Compare observed functions to executed
238
+ const observedNames = new Set(analysis.functions.map(f => f.name))
239
+
240
+ const matched = [...observedNames].filter(n => executedFunctions.has(n))
241
+ const observedOnly = [...observedNames].filter(n => !executedFunctions.has(n))
242
+ const executedOnly = [...executedFunctions].filter(n => !observedNames.has(n))
243
+
244
+ console.log(`\n Matched (observed AND executed): ${matched.length}`)
245
+ for (const name of matched.slice(0, 10)) {
246
+ console.log(` ✓ ${name}`)
247
+ }
248
+ if (matched.length > 10) {
249
+ console.log(` ... and ${matched.length - 10} more`)
250
+ }
251
+
252
+ console.log(`\n Observed only (not yet executed): ${observedOnly.length}`)
253
+ for (const name of observedOnly.slice(0, 5)) {
254
+ console.log(` ○ ${name}`)
255
+ }
256
+ if (observedOnly.length > 5) {
257
+ console.log(` ... and ${observedOnly.length - 5} more`)
258
+ }
259
+
260
+ console.log(`\n Executed only (not in observation): ${executedOnly.length}`)
261
+ for (const name of executedOnly.slice(0, 5)) {
262
+ console.log(` ? ${name}`)
263
+ }
264
+ if (executedOnly.length > 5) {
265
+ console.log(` ... and ${executedOnly.length - 5} more`)
266
+ }
267
+
268
+ // Calculate coverage
269
+ const coverage = matched.length / (observedNames.size || 1)
270
+ console.log(`\n Coverage: ${(coverage * 100).toFixed(1)}% of observed activities have been executed`)
271
+
272
+ console.log('\n└─────────────────────────────────────────────────────────────┘\n')
273
+
274
+ } catch (error) {
275
+ printWarning(`Validation failed: ${error instanceof Error ? error.message : String(error)}`)
276
+ printInfo('Is the backend running? Start minibob and execute some activities first.')
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Observe a specific target instance
282
+ */
283
+ async function observeTarget(target: string, options: ObserveOptions): Promise<void> {
284
+ // Normalize target URL
285
+ let url = target
286
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
287
+ url = `http://${url}`
288
+ }
289
+
290
+ // Extract host and port
291
+ const urlObj = new URL(url)
292
+ const instance: MinibobInstance = {
293
+ id: `target-${urlObj.port}`,
294
+ host: urlObj.hostname,
295
+ port: parseInt(urlObj.port) || 8080,
296
+ startTime: Date.now(),
297
+ workingDirectory: '(unknown)',
298
+ status: 'unknown',
299
+ }
300
+
301
+ if (options.follow) {
302
+ await followInstance(instance, options)
303
+ } else {
304
+ await displayInstanceStatus(instance, options)
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Scan for instances and observe
310
+ */
311
+ async function scanAndObserve(options: ObserveOptions): Promise<void> {
312
+ printInfo('Scanning localhost ports 8080-8090 for minibob instances...')
313
+
314
+ const instances = await scanForInstances()
315
+
316
+ if (instances.length === 0) {
317
+ printWarning('No minibob instances found')
318
+ return
319
+ }
320
+
321
+ printInfo(`Found ${instances.length} instance(s)`)
322
+
323
+ if (options.follow) {
324
+ await followInstances(instances, options)
325
+ } else {
326
+ await displayInstances(instances, options)
327
+ }
328
+ }
329
+
330
+ /**
331
+ * List instances from registry and observe
332
+ */
333
+ async function listAndObserve(options: ObserveOptions): Promise<void> {
334
+ // Clean up stale instances first
335
+ const cleaned = await cleanupStaleInstances()
336
+ if (cleaned > 0) {
337
+ printInfo(`Cleaned up ${cleaned} stale instance(s)`)
338
+ }
339
+
340
+ const instances = await listInstances()
341
+
342
+ if (instances.length === 0) {
343
+ printWarning('No instances registered')
344
+ printInfo('Use --scan to search for running instances')
345
+ printInfo('Use --backend to query the centralized backend')
346
+ printInfo('Or start a minibob server: minibob')
347
+ return
348
+ }
349
+
350
+ if (options.follow) {
351
+ await followInstances(instances, options)
352
+ } else {
353
+ await displayInstances(instances, options)
354
+ }
355
+ }
356
+
357
+ /**
358
+ * Backend vessel status from metabob-activity-api
359
+ */
360
+ interface BackendVesselStatus {
361
+ pod_name: string
362
+ namespace: string
363
+ status: 'idle' | 'executing' | 'bored' | 'error' | 'unknown'
364
+ current_activity?: {
365
+ variant_id: string
366
+ activity_id: string
367
+ variant_name: string
368
+ started_at: string
369
+ current_task?: string
370
+ progress?: number
371
+ }
372
+ metrics?: {
373
+ executions_completed: number
374
+ total_cost_usd: number
375
+ uptime_seconds: number
376
+ }
377
+ last_heartbeat: string
378
+ ready: boolean
379
+ phase: string
380
+ }
381
+
382
+ /**
383
+ * Query backend for all registered vessels
384
+ */
385
+ async function queryBackend(options: ObserveOptions): Promise<void> {
386
+ const backendUrl = process.env.MINIBOB_MCP_ENDPOINT || 'http://api.minibob.local'
387
+
388
+ printInfo(`Querying backend: ${backendUrl}/v2/vessels/status`)
389
+
390
+ try {
391
+ const response = await fetch(`${backendUrl}/v2/vessels/status`, {
392
+ signal: AbortSignal.timeout(10000),
393
+ })
394
+
395
+ if (!response.ok) {
396
+ printWarning(`Backend returned ${response.status}`)
397
+ return
398
+ }
399
+
400
+ const data = (await response.json()) as { vessels: BackendVesselStatus[]; total: number }
401
+
402
+ if (options.json) {
403
+ console.log(JSON.stringify(data, null, 2))
404
+ return
405
+ }
406
+
407
+ if (!data.vessels || data.vessels.length === 0) {
408
+ printWarning('No vessels reporting to backend')
409
+ printInfo('Vessels send heartbeats every 30 seconds')
410
+ printInfo('Start a minibob server and wait for heartbeat')
411
+ return
412
+ }
413
+
414
+ console.log(`\n┌─ Backend Vessel Status (${data.total} vessel(s)) ─────────────────┐\n`)
415
+
416
+ for (const vessel of data.vessels) {
417
+ printBackendVesselInfo(vessel)
418
+ console.log('')
419
+ }
420
+
421
+ console.log('└─────────────────────────────────────────────────────────────┘\n')
422
+
423
+ // Follow mode for backend
424
+ if (options.follow) {
425
+ await followBackend(backendUrl, options)
426
+ }
427
+ } catch (error) {
428
+ printWarning(`Could not reach backend: ${error instanceof Error ? error.message : String(error)}`)
429
+ printInfo('Is the backend running? Check: curl http://api.minibob.local/health')
430
+ }
431
+ }
432
+
433
+ /**
434
+ * Follow backend status
435
+ */
436
+ async function followBackend(backendUrl: string, options: ObserveOptions): Promise<void> {
437
+ console.log('Watching backend (Ctrl+C to stop)...\n')
438
+
439
+ const updateDisplay = async () => {
440
+ console.clear()
441
+ console.log('┌─ Backend Vessel Status (live) ─────────────────────────────┐\n')
442
+
443
+ try {
444
+ const response = await fetch(`${backendUrl}/v2/vessels/status`, {
445
+ signal: AbortSignal.timeout(5000),
446
+ })
447
+
448
+ if (response.ok) {
449
+ const data = (await response.json()) as { vessels: BackendVesselStatus[] }
450
+
451
+ if (data.vessels && data.vessels.length > 0) {
452
+ for (const vessel of data.vessels) {
453
+ printBackendVesselInfo(vessel)
454
+ console.log('')
455
+ }
456
+ } else {
457
+ console.log(' No vessels reporting')
458
+ }
459
+ } else {
460
+ console.log(` Backend error: ${response.status}`)
461
+ }
462
+ } catch {
463
+ console.log(' Backend unreachable')
464
+ }
465
+
466
+ console.log(`Last updated: ${new Date().toLocaleTimeString()}`)
467
+ console.log('└─────────────────────────────────────────────────────────────┘')
468
+ }
469
+
470
+ await updateDisplay()
471
+ const interval = setInterval(updateDisplay, options.interval)
472
+
473
+ process.on('SIGINT', () => {
474
+ clearInterval(interval)
475
+ console.log('\n\nStopped watching.')
476
+ process.exit(0)
477
+ })
478
+
479
+ await new Promise(() => {})
480
+ }
481
+
482
+ /**
483
+ * Print backend vessel info
484
+ */
485
+ function printBackendVesselInfo(vessel: BackendVesselStatus): void {
486
+ const statusIcon = getStatusIcon(vessel.status)
487
+ const readyIcon = vessel.ready ? '●' : '○'
488
+
489
+ console.log(`${statusIcon} ${vessel.pod_name} ${readyIcon}`)
490
+ console.log(` Namespace: ${vessel.namespace}`)
491
+ console.log(` Status: ${vessel.status}`)
492
+ console.log(` Last Heartbeat: ${new Date(vessel.last_heartbeat).toLocaleTimeString()}`)
493
+
494
+ if (vessel.current_activity) {
495
+ console.log(` Current Activity: ${vessel.current_activity.variant_name}`)
496
+ if (vessel.current_activity.current_task) {
497
+ console.log(` Task: ${vessel.current_activity.current_task}`)
498
+ }
499
+ if (vessel.current_activity.progress !== undefined) {
500
+ console.log(` Progress: ${vessel.current_activity.progress}%`)
501
+ }
502
+ }
503
+
504
+ if (vessel.metrics) {
505
+ console.log(` Executions: ${vessel.metrics.executions_completed}`)
506
+ console.log(` Cost: $${vessel.metrics.total_cost_usd.toFixed(4)}`)
507
+ console.log(` Uptime: ${formatUptime(vessel.metrics.uptime_seconds * 1000)}`)
508
+ }
509
+ }
510
+
511
+ /**
512
+ * Display status for multiple instances
513
+ */
514
+ async function displayInstances(instances: MinibobInstance[], options: ObserveOptions): Promise<void> {
515
+ const statuses: Array<{ instance: MinibobInstance; status: InstanceStatus | null }> = []
516
+
517
+ for (const instance of instances) {
518
+ const status = await fetchInstanceStatus(instance)
519
+ statuses.push({ instance, status })
520
+ }
521
+
522
+ if (options.json) {
523
+ console.log(JSON.stringify(statuses, null, 2))
524
+ return
525
+ }
526
+
527
+ console.log('\n┌─ minibob instances ─────────────────────────────────────────┐\n')
528
+
529
+ for (const { instance, status } of statuses) {
530
+ printInstanceInfo(instance, status)
531
+ console.log('')
532
+ }
533
+
534
+ console.log('└─────────────────────────────────────────────────────────────┘\n')
535
+ }
536
+
537
+ /**
538
+ * Display status for a single instance
539
+ */
540
+ async function displayInstanceStatus(instance: MinibobInstance, options: ObserveOptions): Promise<void> {
541
+ const status = await fetchInstanceStatus(instance)
542
+
543
+ if (options.json) {
544
+ console.log(JSON.stringify({ instance, status }, null, 2))
545
+ return
546
+ }
547
+
548
+ console.log('')
549
+ printInstanceInfo(instance, status)
550
+ console.log('')
551
+ }
552
+
553
+ /**
554
+ * Follow instances continuously
555
+ */
556
+ async function followInstances(instances: MinibobInstance[], options: ObserveOptions): Promise<void> {
557
+ console.log('Watching instances (Ctrl+C to stop)...\n')
558
+
559
+ // Clear screen and show header
560
+ const clearScreen = () => {
561
+ console.clear()
562
+ console.log('┌─ minibob instances (live) ─────────────────────────────────┐\n')
563
+ }
564
+
565
+ const updateDisplay = async () => {
566
+ clearScreen()
567
+
568
+ for (const instance of instances) {
569
+ const status = await fetchInstanceStatus(instance)
570
+ printInstanceInfo(instance, status)
571
+ console.log('')
572
+ }
573
+
574
+ console.log(`Last updated: ${new Date().toLocaleTimeString()}`)
575
+ console.log('└─────────────────────────────────────────────────────────────┘')
576
+ }
577
+
578
+ // Initial display
579
+ await updateDisplay()
580
+
581
+ // Update periodically
582
+ const interval = setInterval(updateDisplay, options.interval)
583
+
584
+ // Handle Ctrl+C
585
+ process.on('SIGINT', () => {
586
+ clearInterval(interval)
587
+ console.log('\n\nStopped watching.')
588
+ process.exit(0)
589
+ })
590
+
591
+ // Keep process alive
592
+ await new Promise(() => {})
593
+ }
594
+
595
+ /**
596
+ * Follow a single instance
597
+ */
598
+ async function followInstance(instance: MinibobInstance, options: ObserveOptions): Promise<void> {
599
+ await followInstances([instance], options)
600
+ }
601
+
602
+ /**
603
+ * Print instance info
604
+ */
605
+ function printInstanceInfo(instance: MinibobInstance, status: InstanceStatus | null): void {
606
+ const statusIcon = status ? getStatusIcon(status.status) : '?'
607
+ const statusText = status?.status || 'unreachable'
608
+
609
+ console.log(`${statusIcon} ${instance.id}`)
610
+ console.log(` URL: http://${instance.host}:${instance.port}`)
611
+ console.log(` Status: ${statusText}`)
612
+
613
+ if (status) {
614
+ console.log(` Working Dir: ${status.config.workingDirectory}`)
615
+ console.log(` Model: ${status.config.model}`)
616
+ console.log(` Uptime: ${formatUptime(status.metrics.uptime)}`)
617
+ console.log(` Activities: ${status.metrics.activitiesExecuted}`)
618
+
619
+ if (status.currentActivity) {
620
+ console.log(` Current: ${status.currentActivity.name || status.currentActivity.id}`)
621
+ if (status.currentActivity.task) {
622
+ console.log(` Task: ${status.currentActivity.task}`)
623
+ }
624
+ if (status.currentActivity.progress !== undefined) {
625
+ console.log(` Progress: ${Math.round(status.currentActivity.progress * 100)}%`)
626
+ }
627
+ }
628
+
629
+ if (status.metrics.totalCost !== undefined) {
630
+ console.log(` Total Cost: $${status.metrics.totalCost.toFixed(4)}`)
631
+ }
632
+ } else {
633
+ console.log(` PID: ${instance.pid || 'unknown'}`)
634
+ console.log(` Started: ${new Date(instance.startTime).toLocaleTimeString()}`)
635
+ }
636
+ }
637
+
638
+ /**
639
+ * Get status icon
640
+ */
641
+ function getStatusIcon(status: string): string {
642
+ switch (status) {
643
+ case 'idle':
644
+ return '💤'
645
+ case 'executing':
646
+ return '🔄'
647
+ case 'boredom':
648
+ return '🤖'
649
+ default:
650
+ return '❓'
651
+ }
652
+ }
653
+
654
+ /**
655
+ * Format uptime
656
+ */
657
+ function formatUptime(ms: number): string {
658
+ const seconds = Math.floor(ms / 1000)
659
+ const minutes = Math.floor(seconds / 60)
660
+ const hours = Math.floor(minutes / 60)
661
+ const days = Math.floor(hours / 24)
662
+
663
+ if (days > 0) {
664
+ return `${days}d ${hours % 24}h`
665
+ }
666
+ if (hours > 0) {
667
+ return `${hours}h ${minutes % 60}m`
668
+ }
669
+ if (minutes > 0) {
670
+ return `${minutes}m ${seconds % 60}s`
671
+ }
672
+ return `${seconds}s`
673
+ }
674
+
675
+ // CLI entry point
676
+ if (import.meta.main) {
677
+ const args = process.argv.slice(2)
678
+ observe(args).catch((error) => {
679
+ console.error('Observe failed:', error)
680
+ process.exit(1)
681
+ })
682
+ }