@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
package/src/cli/goal.ts
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Goal Command
|
|
3
|
+
*
|
|
4
|
+
* Set a goal and achieve it using available vessels.
|
|
5
|
+
*
|
|
6
|
+
* Uses existing infrastructure:
|
|
7
|
+
* - GoalProcessor for goal-driven execution
|
|
8
|
+
* - SearchFirstExecutor for dynamic decomposition
|
|
9
|
+
* - MCP backend for Thompson Sampling recommendations
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { parseArgs, formatHelp, exitWithError, printInfo } from './index'
|
|
13
|
+
import { loadConfig } from '../config'
|
|
14
|
+
import { ActivityExecutor, type ExecutorConfig } from '../activity'
|
|
15
|
+
import { GoalProcessor, type GoalResult } from '../goal-processor'
|
|
16
|
+
import { SearchFirstExecutor } from '../search-first-executor'
|
|
17
|
+
import { initializeMCP, getMCPClient } from '../mcp'
|
|
18
|
+
import { MCPActivityBridge } from '../mcp-activity-bridge'
|
|
19
|
+
|
|
20
|
+
export interface GoalOptions {
|
|
21
|
+
searchFirst?: boolean
|
|
22
|
+
maxActivities?: number
|
|
23
|
+
maxCost?: number
|
|
24
|
+
context?: Record<string, unknown>
|
|
25
|
+
dryRun?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Run goal command
|
|
30
|
+
*/
|
|
31
|
+
export async function goal(args: string[]): Promise<void> {
|
|
32
|
+
const { flags, values, positional } = parseArgs(args)
|
|
33
|
+
|
|
34
|
+
// Show help
|
|
35
|
+
if (flags.help || flags.h) {
|
|
36
|
+
console.log(formatHelp('goal', 'Set a goal and achieve it', [
|
|
37
|
+
{ flag: '--search-first', description: 'Use SearchFirstExecutor (dynamic decomposition)' },
|
|
38
|
+
{ flag: '--max-activities <n>', description: 'Maximum activities to run', default: '5' },
|
|
39
|
+
{ flag: '--max-cost <n>', description: 'Maximum cost in USD', default: '10.0' },
|
|
40
|
+
{ flag: '--context <json>', description: 'Additional context as JSON' },
|
|
41
|
+
{ flag: '--dry-run', description: 'Plan but do not execute' },
|
|
42
|
+
{ flag: '--help, -h', description: 'Show this help message' },
|
|
43
|
+
]))
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Get goal text from positional arguments
|
|
48
|
+
const goalText = positional.join(' ')
|
|
49
|
+
if (!goalText) {
|
|
50
|
+
exitWithError('Goal description required\nUsage: minibob goal "your goal here"')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Parse options
|
|
54
|
+
const options: GoalOptions = {
|
|
55
|
+
searchFirst: flags['search-first'] || flags.s,
|
|
56
|
+
maxActivities: values['max-activities'] ? parseInt(values['max-activities']) : 5,
|
|
57
|
+
maxCost: values['max-cost'] ? parseFloat(values['max-cost']) : 10.0,
|
|
58
|
+
context: values.context ? JSON.parse(values.context) : undefined,
|
|
59
|
+
dryRun: flags['dry-run'],
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Load configuration
|
|
63
|
+
const config = await loadConfig()
|
|
64
|
+
printInfo(`Goal: ${goalText}`)
|
|
65
|
+
printInfo(`Working directory: ${config.workingDirectory}`)
|
|
66
|
+
|
|
67
|
+
// Initialize MCP if configured
|
|
68
|
+
if (config.vessels.metabob) {
|
|
69
|
+
const mcpEndpoint = config.vessels.metabob.endpoint
|
|
70
|
+
printInfo(`Connecting to backend: ${mcpEndpoint}`)
|
|
71
|
+
await initializeMCP({ endpoint: mcpEndpoint, apiKey: config.apiKey }, true)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Execute goal
|
|
75
|
+
if (options.searchFirst) {
|
|
76
|
+
await executeSearchFirst(goalText, config, options)
|
|
77
|
+
} else {
|
|
78
|
+
await executeGoalProcessor(goalText, config, options)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Execute goal using GoalProcessor
|
|
84
|
+
*/
|
|
85
|
+
async function executeGoalProcessor(
|
|
86
|
+
goalText: string,
|
|
87
|
+
config: Awaited<ReturnType<typeof loadConfig>>,
|
|
88
|
+
options: GoalOptions
|
|
89
|
+
): Promise<void> {
|
|
90
|
+
const executorConfig: ExecutorConfig = {
|
|
91
|
+
provider: config.provider,
|
|
92
|
+
apiKey: config.apiKey,
|
|
93
|
+
model: config.model,
|
|
94
|
+
workingDirectory: config.workingDirectory,
|
|
95
|
+
onSearchActivities: MCPActivityBridge.isAvailable() ? MCPActivityBridge.searchActivities : undefined,
|
|
96
|
+
onCreateActivity: MCPActivityBridge.isAvailable() ? MCPActivityBridge.createActivity : undefined,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const executor = new ActivityExecutor(executorConfig)
|
|
100
|
+
const goalProcessor = new GoalProcessor({
|
|
101
|
+
workingDirectory: config.workingDirectory,
|
|
102
|
+
executor,
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
// Check if MCP is available for recommendations
|
|
106
|
+
const mcp = getMCPClient()
|
|
107
|
+
if (!mcp) {
|
|
108
|
+
console.log('\n⚠ Backend unavailable - recommendations limited')
|
|
109
|
+
console.log(' Set MINIBOB_MCP_ENDPOINT for full goal-seeking capabilities\n')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (options.dryRun) {
|
|
113
|
+
console.log('\n[Dry Run] Would execute goal with these settings:')
|
|
114
|
+
console.log(` Goal: ${goalText}`)
|
|
115
|
+
console.log(` Max activities: ${options.maxActivities}`)
|
|
116
|
+
console.log(` Max cost: $${options.maxCost}`)
|
|
117
|
+
console.log(` Context: ${JSON.stringify(options.context || {})}`)
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
console.log('\n🎯 Executing goal...\n')
|
|
122
|
+
|
|
123
|
+
const result = await goalProcessor.executeGoal(goalText, options.context, {
|
|
124
|
+
maxActivities: options.maxActivities,
|
|
125
|
+
maxCost: options.maxCost,
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
printGoalResult(result)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Execute goal using SearchFirstExecutor
|
|
133
|
+
*/
|
|
134
|
+
async function executeSearchFirst(
|
|
135
|
+
goalText: string,
|
|
136
|
+
config: Awaited<ReturnType<typeof loadConfig>>,
|
|
137
|
+
options: GoalOptions
|
|
138
|
+
): Promise<void> {
|
|
139
|
+
const executor = new SearchFirstExecutor({
|
|
140
|
+
provider: config.provider,
|
|
141
|
+
apiKey: config.apiKey,
|
|
142
|
+
model: config.model,
|
|
143
|
+
workingDirectory: config.workingDirectory,
|
|
144
|
+
maxSteps: options.maxActivities || 5,
|
|
145
|
+
maxTokensPerStep: 4096,
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
if (options.dryRun) {
|
|
149
|
+
console.log('\n[Dry Run] Would execute with SearchFirstExecutor:')
|
|
150
|
+
console.log(` Goal: ${goalText}`)
|
|
151
|
+
console.log(` Max steps: ${options.maxActivities}`)
|
|
152
|
+
console.log(` Context: ${JSON.stringify(options.context || {})}`)
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
console.log('\n🔍 Executing with SearchFirstExecutor...\n')
|
|
157
|
+
console.log('This mode decomposes goals step-by-step and reuses existing activities.\n')
|
|
158
|
+
|
|
159
|
+
const result = await executor.execute(goalText, options.context || {})
|
|
160
|
+
|
|
161
|
+
// Print result
|
|
162
|
+
console.log('\n' + '='.repeat(60))
|
|
163
|
+
console.log(`Goal: ${goalText}`)
|
|
164
|
+
console.log(`Status: ${result.completed ? '✅ Completed' : '❌ Incomplete'}`)
|
|
165
|
+
console.log(`Steps: ${result.steps.length}`)
|
|
166
|
+
console.log(`Tokens: ${result.metrics.totalTokens.input} in / ${result.metrics.totalTokens.output} out`)
|
|
167
|
+
console.log(`Cost: $${result.metrics.totalCost.toFixed(4)}`)
|
|
168
|
+
console.log(`Duration: ${result.metrics.totalDuration}ms`)
|
|
169
|
+
console.log('='.repeat(60))
|
|
170
|
+
|
|
171
|
+
if (result.steps.length > 0) {
|
|
172
|
+
console.log('\nExecution Steps:')
|
|
173
|
+
for (const step of result.steps) {
|
|
174
|
+
const icon = step.success ? '✓' : '✗'
|
|
175
|
+
console.log(` ${icon} Step ${step.stepNumber}: ${step.action}`)
|
|
176
|
+
if (step.reasoning) {
|
|
177
|
+
console.log(` Reasoning: ${step.reasoning.slice(0, 100)}...`)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.log('')
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Print goal result
|
|
187
|
+
*/
|
|
188
|
+
function printGoalResult(result: GoalResult): void {
|
|
189
|
+
console.log('\n' + '='.repeat(60))
|
|
190
|
+
console.log(`Goal: ${result.goal.message}`)
|
|
191
|
+
console.log(`Type: ${result.goal.type}`)
|
|
192
|
+
console.log(`Status: ${result.completed ? '✅ Completed' : '❌ Incomplete'}`)
|
|
193
|
+
console.log(`Reason: ${result.completionReason}`)
|
|
194
|
+
console.log('='.repeat(60))
|
|
195
|
+
console.log(`Activities executed: ${result.executions.length}`)
|
|
196
|
+
console.log(`Total duration: ${result.totalDuration}ms`)
|
|
197
|
+
console.log(`Total cost: $${result.totalCost.toFixed(4)}`)
|
|
198
|
+
console.log(`Total tokens: ${result.totalTokens.input} in / ${result.totalTokens.output} out`)
|
|
199
|
+
console.log('='.repeat(60))
|
|
200
|
+
|
|
201
|
+
if (result.executions.length > 0) {
|
|
202
|
+
console.log('\nExecution Details:')
|
|
203
|
+
for (const exec of result.executions) {
|
|
204
|
+
const icon = exec.status === 'completed' ? '✓' : '✗'
|
|
205
|
+
console.log(` ${icon} ${exec.templateId}`)
|
|
206
|
+
console.log(` Status: ${exec.status}`)
|
|
207
|
+
if (exec.metrics) {
|
|
208
|
+
console.log(` Duration: ${exec.metrics.duration}ms`)
|
|
209
|
+
console.log(` Cost: $${exec.metrics.cost.toFixed(4)}`)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
console.log('')
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// CLI entry point
|
|
218
|
+
if (import.meta.main) {
|
|
219
|
+
const args = process.argv.slice(2)
|
|
220
|
+
goal(args).catch((error) => {
|
|
221
|
+
console.error('Goal failed:', error)
|
|
222
|
+
process.exit(1)
|
|
223
|
+
})
|
|
224
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Utilities and Router
|
|
3
|
+
*
|
|
4
|
+
* Common utilities for all CLI commands
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface ParsedOptions {
|
|
8
|
+
flags: Record<string, boolean>
|
|
9
|
+
values: Record<string, string>
|
|
10
|
+
positional: string[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Flags that take values (not boolean flags)
|
|
14
|
+
const VALUE_FLAGS = new Set([
|
|
15
|
+
'target',
|
|
16
|
+
'codebase',
|
|
17
|
+
'entry',
|
|
18
|
+
'check',
|
|
19
|
+
'mode',
|
|
20
|
+
'port',
|
|
21
|
+
'tools',
|
|
22
|
+
'deny-tools',
|
|
23
|
+
'system',
|
|
24
|
+
'max-activities',
|
|
25
|
+
'max-cost',
|
|
26
|
+
'context',
|
|
27
|
+
'interval',
|
|
28
|
+
])
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parse command line arguments into structured options
|
|
32
|
+
*/
|
|
33
|
+
export function parseArgs(args: string[]): ParsedOptions {
|
|
34
|
+
const result: ParsedOptions = {
|
|
35
|
+
flags: {},
|
|
36
|
+
values: {},
|
|
37
|
+
positional: [],
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (let i = 0; i < args.length; i++) {
|
|
41
|
+
const arg = args[i]
|
|
42
|
+
|
|
43
|
+
if (arg?.startsWith('--')) {
|
|
44
|
+
const key = arg.slice(2)
|
|
45
|
+
|
|
46
|
+
// Check if this flag is known to take a value
|
|
47
|
+
if (VALUE_FLAGS.has(key)) {
|
|
48
|
+
const nextArg = args[i + 1]
|
|
49
|
+
if (nextArg && !nextArg.startsWith('--') && !nextArg.startsWith('-')) {
|
|
50
|
+
result.values[key] = nextArg
|
|
51
|
+
i++ // Skip the next arg
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
// It's a boolean flag
|
|
55
|
+
result.flags[key] = true
|
|
56
|
+
}
|
|
57
|
+
} else if (arg?.startsWith('-') && arg.length === 2) {
|
|
58
|
+
// Short flag like -v, -h
|
|
59
|
+
const key = arg.slice(1)
|
|
60
|
+
result.flags[key] = true
|
|
61
|
+
} else if (arg) {
|
|
62
|
+
result.positional.push(arg)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return result
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Format help text with consistent styling
|
|
71
|
+
*/
|
|
72
|
+
export function formatHelp(command: string, description: string, options: Array<{
|
|
73
|
+
flag: string
|
|
74
|
+
description: string
|
|
75
|
+
default?: string
|
|
76
|
+
}>): string {
|
|
77
|
+
const lines: string[] = [
|
|
78
|
+
`minibob ${command} - ${description}`,
|
|
79
|
+
'',
|
|
80
|
+
'USAGE:',
|
|
81
|
+
` minibob ${command} [options]`,
|
|
82
|
+
'',
|
|
83
|
+
'OPTIONS:',
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
for (const opt of options) {
|
|
87
|
+
const defaultStr = opt.default ? ` (default: ${opt.default})` : ''
|
|
88
|
+
lines.push(` ${opt.flag.padEnd(20)} ${opt.description}${defaultStr}`)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return lines.join('\n')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Print error and exit
|
|
96
|
+
*/
|
|
97
|
+
export function exitWithError(message: string, exitCode = 1): never {
|
|
98
|
+
console.error(`Error: ${message}`)
|
|
99
|
+
process.exit(exitCode)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Print success message
|
|
104
|
+
*/
|
|
105
|
+
export function printSuccess(message: string): void {
|
|
106
|
+
console.log(`✓ ${message}`)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Print warning message
|
|
111
|
+
*/
|
|
112
|
+
export function printWarning(message: string): void {
|
|
113
|
+
console.warn(`⚠ ${message}`)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Print info message
|
|
118
|
+
*/
|
|
119
|
+
export function printInfo(message: string): void {
|
|
120
|
+
console.log(`ℹ ${message}`)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get home directory path for minibob data
|
|
125
|
+
*/
|
|
126
|
+
export function getMinibobHome(): string {
|
|
127
|
+
const home = process.env.HOME || process.env.USERPROFILE || '~'
|
|
128
|
+
return `${home}/.minibob`
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Ensure minibob home directory exists
|
|
133
|
+
*/
|
|
134
|
+
export async function ensureMinibobHome(): Promise<string> {
|
|
135
|
+
const home = getMinibobHome()
|
|
136
|
+
const fs = await import('fs/promises')
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
await fs.mkdir(home, { recursive: true })
|
|
140
|
+
await fs.mkdir(`${home}/instances`, { recursive: true })
|
|
141
|
+
await fs.mkdir(`${home}/cache`, { recursive: true })
|
|
142
|
+
} catch (error) {
|
|
143
|
+
// Directory already exists or other error
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return home
|
|
147
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Instance Registry
|
|
3
|
+
*
|
|
4
|
+
* Tracks running minibob instances for the observe command.
|
|
5
|
+
* Instances register on startup and deregister on shutdown.
|
|
6
|
+
*
|
|
7
|
+
* Storage: ~/.minibob/instances.json
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { ensureMinibobHome, getMinibobHome } from './index'
|
|
11
|
+
|
|
12
|
+
export interface MinibobInstance {
|
|
13
|
+
id: string
|
|
14
|
+
pid?: number
|
|
15
|
+
port: number
|
|
16
|
+
host: string
|
|
17
|
+
startTime: number
|
|
18
|
+
workingDirectory: string
|
|
19
|
+
status: 'running' | 'idle' | 'busy' | 'unknown'
|
|
20
|
+
lastActivity?: string
|
|
21
|
+
lastActivityTime?: number
|
|
22
|
+
vesselType?: string
|
|
23
|
+
capabilities?: string[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface InstanceStatus {
|
|
27
|
+
id: string
|
|
28
|
+
status: 'idle' | 'executing' | 'boredom'
|
|
29
|
+
currentActivity?: {
|
|
30
|
+
id: string
|
|
31
|
+
name?: string
|
|
32
|
+
task?: string
|
|
33
|
+
progress?: number
|
|
34
|
+
}
|
|
35
|
+
metrics: {
|
|
36
|
+
activitiesExecuted: number
|
|
37
|
+
uptime: number
|
|
38
|
+
totalCost?: number
|
|
39
|
+
}
|
|
40
|
+
config: {
|
|
41
|
+
workingDirectory: string
|
|
42
|
+
model: string
|
|
43
|
+
port: number
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const REGISTRY_FILE = 'instances.json'
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Load instance registry from disk
|
|
51
|
+
*/
|
|
52
|
+
async function loadRegistry(): Promise<Record<string, MinibobInstance>> {
|
|
53
|
+
const home = await ensureMinibobHome()
|
|
54
|
+
const registryPath = `${home}/${REGISTRY_FILE}`
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const file = Bun.file(registryPath)
|
|
58
|
+
if (await file.exists()) {
|
|
59
|
+
return await file.json()
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
// Registry doesn't exist or is corrupted
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Save instance registry to disk
|
|
70
|
+
*/
|
|
71
|
+
async function saveRegistry(registry: Record<string, MinibobInstance>): Promise<void> {
|
|
72
|
+
const home = await ensureMinibobHome()
|
|
73
|
+
const registryPath = `${home}/${REGISTRY_FILE}`
|
|
74
|
+
|
|
75
|
+
await Bun.write(registryPath, JSON.stringify(registry, null, 2))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Register a minibob instance
|
|
80
|
+
*/
|
|
81
|
+
export async function registerInstance(instance: MinibobInstance): Promise<void> {
|
|
82
|
+
const registry = await loadRegistry()
|
|
83
|
+
registry[instance.id] = instance
|
|
84
|
+
await saveRegistry(registry)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Deregister a minibob instance
|
|
89
|
+
*/
|
|
90
|
+
export async function deregisterInstance(instanceId: string): Promise<void> {
|
|
91
|
+
const registry = await loadRegistry()
|
|
92
|
+
delete registry[instanceId]
|
|
93
|
+
await saveRegistry(registry)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Update instance status
|
|
98
|
+
*/
|
|
99
|
+
export async function updateInstanceStatus(
|
|
100
|
+
instanceId: string,
|
|
101
|
+
update: Partial<MinibobInstance>
|
|
102
|
+
): Promise<void> {
|
|
103
|
+
const registry = await loadRegistry()
|
|
104
|
+
const instance = registry[instanceId]
|
|
105
|
+
|
|
106
|
+
if (instance) {
|
|
107
|
+
registry[instanceId] = { ...instance, ...update }
|
|
108
|
+
await saveRegistry(registry)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get all registered instances
|
|
114
|
+
*/
|
|
115
|
+
export async function listInstances(): Promise<MinibobInstance[]> {
|
|
116
|
+
const registry = await loadRegistry()
|
|
117
|
+
return Object.values(registry)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get instance by ID
|
|
122
|
+
*/
|
|
123
|
+
export async function getInstance(instanceId: string): Promise<MinibobInstance | null> {
|
|
124
|
+
const registry = await loadRegistry()
|
|
125
|
+
return registry[instanceId] || null
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Clean up stale instances (processes that no longer exist)
|
|
130
|
+
*/
|
|
131
|
+
export async function cleanupStaleInstances(): Promise<number> {
|
|
132
|
+
const registry = await loadRegistry()
|
|
133
|
+
let removed = 0
|
|
134
|
+
|
|
135
|
+
for (const [id, instance] of Object.entries(registry)) {
|
|
136
|
+
if (instance.pid) {
|
|
137
|
+
try {
|
|
138
|
+
// Check if process exists by sending signal 0
|
|
139
|
+
process.kill(instance.pid, 0)
|
|
140
|
+
} catch {
|
|
141
|
+
// Process doesn't exist, remove from registry
|
|
142
|
+
delete registry[id]
|
|
143
|
+
removed++
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
// No PID, try to reach the instance via HTTP
|
|
147
|
+
try {
|
|
148
|
+
const url = `http://${instance.host}:${instance.port}/health`
|
|
149
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(2000) })
|
|
150
|
+
if (!response.ok) {
|
|
151
|
+
delete registry[id]
|
|
152
|
+
removed++
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
// Instance unreachable, remove
|
|
156
|
+
delete registry[id]
|
|
157
|
+
removed++
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (removed > 0) {
|
|
163
|
+
await saveRegistry(registry)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return removed
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Fetch status from a running instance
|
|
171
|
+
*/
|
|
172
|
+
export async function fetchInstanceStatus(instance: MinibobInstance): Promise<InstanceStatus | null> {
|
|
173
|
+
try {
|
|
174
|
+
const url = `http://${instance.host}:${instance.port}/status`
|
|
175
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(5000) })
|
|
176
|
+
|
|
177
|
+
if (response.ok) {
|
|
178
|
+
return await response.json()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Fall back to /config endpoint
|
|
182
|
+
const configUrl = `http://${instance.host}:${instance.port}/config`
|
|
183
|
+
const configResponse = await fetch(configUrl, { signal: AbortSignal.timeout(5000) })
|
|
184
|
+
|
|
185
|
+
if (configResponse.ok) {
|
|
186
|
+
const config = await configResponse.json()
|
|
187
|
+
// Construct minimal status from config
|
|
188
|
+
return {
|
|
189
|
+
id: config.id || instance.id,
|
|
190
|
+
status: 'idle',
|
|
191
|
+
metrics: {
|
|
192
|
+
activitiesExecuted: 0,
|
|
193
|
+
uptime: Date.now() - instance.startTime,
|
|
194
|
+
},
|
|
195
|
+
config: {
|
|
196
|
+
workingDirectory: instance.workingDirectory,
|
|
197
|
+
model: config.model || 'unknown',
|
|
198
|
+
port: instance.port,
|
|
199
|
+
},
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
} catch {
|
|
203
|
+
// Instance unreachable
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return null
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Scan localhost ports for minibob instances
|
|
211
|
+
*/
|
|
212
|
+
export async function scanForInstances(
|
|
213
|
+
startPort: number = 8080,
|
|
214
|
+
endPort: number = 8090
|
|
215
|
+
): Promise<MinibobInstance[]> {
|
|
216
|
+
const found: MinibobInstance[] = []
|
|
217
|
+
|
|
218
|
+
const scanPromises = []
|
|
219
|
+
for (let port = startPort; port <= endPort; port++) {
|
|
220
|
+
scanPromises.push(
|
|
221
|
+
(async () => {
|
|
222
|
+
try {
|
|
223
|
+
const url = `http://localhost:${port}/health`
|
|
224
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(1000) })
|
|
225
|
+
|
|
226
|
+
if (response.ok) {
|
|
227
|
+
const health = (await response.json()) as { vessel?: string }
|
|
228
|
+
if (health.vessel === 'minibob') {
|
|
229
|
+
// Get full config
|
|
230
|
+
const configResponse = await fetch(`http://localhost:${port}/config`, {
|
|
231
|
+
signal: AbortSignal.timeout(1000),
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
if (configResponse.ok) {
|
|
235
|
+
const config = (await configResponse.json()) as {
|
|
236
|
+
id?: string
|
|
237
|
+
capabilities?: string[]
|
|
238
|
+
metadata?: {
|
|
239
|
+
workingDirectory?: string
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
found.push({
|
|
244
|
+
id: config.id || `minibob-${port}`,
|
|
245
|
+
port,
|
|
246
|
+
host: 'localhost',
|
|
247
|
+
startTime: Date.now(),
|
|
248
|
+
workingDirectory: config.metadata?.workingDirectory || process.cwd(),
|
|
249
|
+
status: 'running',
|
|
250
|
+
capabilities: config.capabilities,
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
} catch {
|
|
256
|
+
// Port not responding or not minibob
|
|
257
|
+
}
|
|
258
|
+
})()
|
|
259
|
+
)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
await Promise.all(scanPromises)
|
|
263
|
+
return found
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Generate unique instance ID
|
|
268
|
+
*/
|
|
269
|
+
export function generateInstanceId(): string {
|
|
270
|
+
return `minibob-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`
|
|
271
|
+
}
|