@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,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
+ }
@@ -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
+ }