@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.
- package/ARCHITECTURE.md +255 -0
- package/CHANGELOG.md +112 -0
- package/README.md +380 -0
- package/bin/minibob.js +36 -0
- package/dist/acp-gossip.d.ts +72 -0
- package/dist/acp-gossip.d.ts.map +1 -0
- package/dist/acp-gossip.js +156 -0
- package/dist/acp-gossip.js.map +1 -0
- package/dist/acp.d.ts +62 -0
- package/dist/acp.d.ts.map +1 -0
- package/dist/acp.js +292 -0
- package/dist/acp.js.map +1 -0
- package/dist/activity.d.ts +157 -0
- package/dist/activity.d.ts.map +1 -0
- package/dist/activity.js +518 -0
- package/dist/activity.js.map +1 -0
- package/dist/agent-runtime.d.ts +104 -0
- package/dist/agent-runtime.d.ts.map +1 -0
- package/dist/boredom.d.ts +125 -0
- package/dist/boredom.d.ts.map +1 -0
- package/dist/boredom.js +244 -0
- package/dist/boredom.js.map +1 -0
- package/dist/cli/acp-server.d.ts +23 -0
- package/dist/cli/acp-server.d.ts.map +1 -0
- package/dist/cli/burrow.d.ts +26 -0
- package/dist/cli/burrow.d.ts.map +1 -0
- package/dist/cli/doctor.d.ts +22 -0
- package/dist/cli/doctor.d.ts.map +1 -0
- package/dist/cli/goal.d.ts +22 -0
- package/dist/cli/goal.d.ts.map +1 -0
- package/dist/cli/index.d.ts +47 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/instance-registry.d.ts +78 -0
- package/dist/cli/instance-registry.d.ts.map +1 -0
- package/dist/cli/observe.d.ts +35 -0
- package/dist/cli/observe.d.ts.map +1 -0
- package/dist/cli/vessel.d.ts +14 -0
- package/dist/cli/vessel.d.ts.map +1 -0
- package/dist/composition-observer.d.ts +96 -0
- package/dist/composition-observer.d.ts.map +1 -0
- package/dist/config.d.ts +36 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +128 -0
- package/dist/config.js.map +1 -0
- package/dist/docker/Dockerfile +35 -0
- package/dist/environment.d.ts +72 -0
- package/dist/environment.d.ts.map +1 -0
- package/dist/environment.js +142 -0
- package/dist/environment.js.map +1 -0
- package/dist/goal-processor.d.ts +165 -0
- package/dist/goal-processor.d.ts.map +1 -0
- package/dist/helm/minibob-cluster/Chart.yaml +13 -0
- package/dist/helm/minibob-cluster/templates/_helpers.tpl +60 -0
- package/dist/helm/minibob-cluster/templates/configmap.yaml +11 -0
- package/dist/helm/minibob-cluster/templates/deployment.yaml +108 -0
- package/dist/helm/minibob-cluster/templates/secret.yaml +10 -0
- package/dist/helm/minibob-cluster/templates/service.yaml +37 -0
- package/dist/helm/minibob-cluster/values-local.yaml +41 -0
- package/dist/helm/minibob-cluster/values-production.yaml +57 -0
- package/dist/helm/minibob-cluster/values-testing-cluster.yaml +43 -0
- package/dist/helm/minibob-cluster/values.yaml +127 -0
- package/dist/improviser.d.ts +74 -0
- package/dist/improviser.d.ts.map +1 -0
- package/dist/impulse-filter.d.ts +74 -0
- package/dist/impulse-filter.d.ts.map +1 -0
- package/dist/impulse.d.ts +92 -0
- package/dist/impulse.d.ts.map +1 -0
- package/dist/impulse.js +234 -0
- package/dist/impulse.js.map +1 -0
- package/dist/lib.d.ts +29 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +18561 -0
- package/dist/lib.js.map +98 -0
- package/dist/lifecycle-hooks.d.ts +99 -0
- package/dist/lifecycle-hooks.d.ts.map +1 -0
- package/dist/lifecycle-hooks.js +135 -0
- package/dist/lifecycle-hooks.js.map +1 -0
- package/dist/llm.d.ts +31 -0
- package/dist/llm.d.ts.map +1 -0
- package/dist/llm.js +349 -0
- package/dist/llm.js.map +1 -0
- package/dist/mcp-activity-bridge.d.ts +66 -0
- package/dist/mcp-activity-bridge.d.ts.map +1 -0
- package/dist/mcp-activity-bridge.js +126 -0
- package/dist/mcp-activity-bridge.js.map +1 -0
- package/dist/mcp.d.ts +216 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +292 -0
- package/dist/mcp.js.map +1 -0
- package/dist/memory-agent.d.ts +92 -0
- package/dist/memory-agent.d.ts.map +1 -0
- package/dist/memory-agent.js +277 -0
- package/dist/memory-agent.js.map +1 -0
- package/dist/runtime-mapping.d.ts +97 -0
- package/dist/runtime-mapping.d.ts.map +1 -0
- package/dist/search-first-executor.d.ts +113 -0
- package/dist/search-first-executor.d.ts.map +1 -0
- package/dist/session.d.ts +48 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/template-extractor.d.ts +9 -0
- package/dist/template-extractor.d.ts.map +1 -0
- package/dist/template-generator.d.ts +12 -0
- package/dist/template-generator.d.ts.map +1 -0
- package/dist/tools.d.ts +58 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +771 -0
- package/dist/tools.js.map +1 -0
- package/dist/types.d.ts +503 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/understanding/analyzer.d.ts +55 -0
- package/dist/understanding/analyzer.d.ts.map +1 -0
- package/dist/understanding/explorer.d.ts +73 -0
- package/dist/understanding/explorer.d.ts.map +1 -0
- package/dist/understanding/index.d.ts +7 -0
- package/dist/understanding/index.d.ts.map +1 -0
- package/dist/understanding/types.d.ts +136 -0
- package/dist/understanding/types.d.ts.map +1 -0
- package/dist/validation.d.ts +29 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +106 -0
- package/dist/validation.js.map +1 -0
- package/dist/vessel-bootstrap.d.ts +190 -0
- package/dist/vessel-bootstrap.d.ts.map +1 -0
- package/dist/vessel-registry.d.ts +229 -0
- package/dist/vessel-registry.d.ts.map +1 -0
- package/index.ts +1329 -0
- package/package.json +54 -0
- package/src/acp-gossip.ts +193 -0
- package/src/acp.ts +362 -0
- package/src/activity.ts +1464 -0
- package/src/agent-runtime.ts +365 -0
- package/src/boredom.ts +423 -0
- package/src/cli/acp-server.ts +377 -0
- package/src/cli/burrow.ts +896 -0
- package/src/cli/doctor.ts +526 -0
- package/src/cli/goal.ts +224 -0
- package/src/cli/index.ts +147 -0
- package/src/cli/instance-registry.ts +271 -0
- package/src/cli/observe.ts +682 -0
- package/src/cli/vessel.ts +287 -0
- package/src/components/SystemOverview.tsx +331 -0
- package/src/composition-observer.ts +449 -0
- package/src/config.ts +172 -0
- package/src/environment.ts +167 -0
- package/src/goal-processor.ts +654 -0
- package/src/improviser.ts +591 -0
- package/src/impulse-filter.ts +273 -0
- package/src/impulse.ts +311 -0
- package/src/lib.ts +147 -0
- package/src/lifecycle-hooks.ts +181 -0
- package/src/llm.ts +434 -0
- package/src/mcp-activity-bridge.ts +158 -0
- package/src/mcp.ts +747 -0
- package/src/memory-agent.ts +316 -0
- package/src/runtime-mapping.ts +527 -0
- package/src/search-first-executor.ts +666 -0
- package/src/session.ts +141 -0
- package/src/template-extractor.ts +256 -0
- package/src/template-generator.ts +130 -0
- package/src/tools.ts +924 -0
- package/src/types.ts +497 -0
- package/src/understanding/analyzer.ts +354 -0
- package/src/understanding/explorer.ts +488 -0
- package/src/understanding/index.ts +27 -0
- package/src/understanding/types.ts +153 -0
- package/src/validation.ts +125 -0
- package/src/vessel-bootstrap.ts +440 -0
- package/src/vessel-registry.ts +621 -0
- package/templates/core/edit-file.json +85 -0
- package/templates/understanding/diagnose-problem.json +32 -0
- package/templates/understanding/explore-codebase-v2.json +57 -0
- 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
|
+
}
|