openbot 0.3.2 → 0.3.4

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/README.md CHANGED
@@ -120,7 +120,7 @@ Shared plugins can be placed in `~/.openbot/plugins` and referenced by agents.
120
120
  ## Project Layout
121
121
 
122
122
  - `src/app`: CLI, server, event types, and app config.
123
- - `src/harness`: agent harness, orchestration, process, and MCP runtime helpers.
123
+ - `src/harness`: orchestration, process, and MCP runtime helpers.
124
124
  - `src/plugins`: built-in plugin implementations.
125
125
  - `src/services`: local storage service.
126
126
  - `src/registry`: plugin registry.
package/dist/app/cli.js CHANGED
@@ -16,7 +16,7 @@ function checkNodeVersion() {
16
16
  }
17
17
  }
18
18
  checkNodeVersion();
19
- program.name('openbot').description('OpenBot CLI').version('0.3.1');
19
+ program.name('openbot').description('OpenBot CLI').version('0.3.4');
20
20
  program
21
21
  .command('start')
22
22
  .description('Start the OpenBot harness')
@@ -11,7 +11,7 @@ import { generateId } from 'melony';
11
11
  import { DEFAULT_BASE_DIR, loadConfig, resolvePath } from '../app/config.js';
12
12
  import { processService } from '../harness/process.js';
13
13
  import { storageService } from '../services/storage.js';
14
- import { AgentHarness } from '../harness/agent-harness.js';
14
+ import { dispatch } from '../harness/dispatcher.js';
15
15
  import { initPlugins } from '../registry/plugins.js';
16
16
  import { ensureEventId, openBotEventFromQuery } from './utils.js';
17
17
  export async function startServer(options = {}) {
@@ -69,25 +69,39 @@ export async function startServer(options = {}) {
69
69
  });
70
70
  };
71
71
  const buildActiveRunsSnapshot = () => {
72
- const byChannel = new Map();
72
+ const byBucket = new Map();
73
73
  for (const run of activeRuns.values()) {
74
- const existing = byChannel.get(run.channelId) ?? {
75
- activeCount: 0,
76
- agentIds: new Set(),
77
- };
78
- existing.activeCount += 1;
79
- existing.agentIds.add(run.agentId);
80
- byChannel.set(run.channelId, existing);
74
+ const threadId = run.threadId || undefined;
75
+ const key = JSON.stringify([run.channelId, threadId ?? null]);
76
+ let bucket = byBucket.get(key);
77
+ if (!bucket) {
78
+ bucket = { channelId: run.channelId, threadId, activeCount: 0, agentIds: new Set() };
79
+ byBucket.set(key, bucket);
80
+ }
81
+ bucket.activeCount += 1;
82
+ bucket.agentIds.add(run.agentId);
81
83
  }
84
+ const channels = Array.from(byBucket.values())
85
+ .sort((a, b) => {
86
+ const c = a.channelId.localeCompare(b.channelId);
87
+ if (c !== 0)
88
+ return c;
89
+ return (a.threadId ?? '').localeCompare(b.threadId ?? '');
90
+ })
91
+ .map(({ channelId, threadId, activeCount, agentIds }) => {
92
+ const row = {
93
+ channelId,
94
+ activeCount,
95
+ agentIds: Array.from(agentIds),
96
+ };
97
+ if (threadId !== undefined) {
98
+ row.threadId = threadId;
99
+ }
100
+ return row;
101
+ });
82
102
  return {
83
103
  type: 'agent:active-runs:snapshot',
84
- data: {
85
- channels: Array.from(byChannel.entries()).map(([channelId, value]) => ({
86
- channelId,
87
- activeCount: value.activeCount,
88
- agentIds: Array.from(value.agentIds),
89
- })),
90
- },
104
+ data: { channels },
91
105
  };
92
106
  };
93
107
  app.use(cors());
@@ -178,19 +192,22 @@ export async function startServer(options = {}) {
178
192
  event: chunk,
179
193
  });
180
194
  sendToClientKey(targetClientKey, chunk);
181
- if (chunk.type === 'agent:run:start' || chunk.type === 'agent:run:end') {
195
+ if (chunk.type === 'agent:run:start' ||
196
+ chunk.type === 'agent:run:end' ||
197
+ chunk.type === 'agent:run:stopped') {
182
198
  sendToClientKey(GLOBAL_CHANNEL_ID, chunk);
183
199
  }
184
200
  };
185
201
  try {
186
- const harness = new AgentHarness({
202
+ ensureEventId(event);
203
+ await dispatch({
187
204
  runId,
188
205
  agentId: agentId || 'system',
206
+ event,
189
207
  channelId,
190
208
  threadId,
191
209
  onEvent,
192
210
  });
193
- await harness.dispatch(event);
194
211
  res.sendStatus(200);
195
212
  }
196
213
  catch (error) {
@@ -220,14 +237,15 @@ export async function startServer(options = {}) {
220
237
  events.push(chunk);
221
238
  };
222
239
  try {
223
- const harness = new AgentHarness({
240
+ ensureEventId(event);
241
+ await dispatch({
224
242
  runId,
225
243
  agentId: agentId || 'system',
244
+ event,
226
245
  channelId,
227
246
  threadId,
228
247
  onEvent,
229
248
  });
230
- await harness.dispatch(event);
231
249
  res.json({ events });
232
250
  }
233
251
  catch (error) {
@@ -0,0 +1,267 @@
1
+ import { ensureEventId } from '../app/utils.js';
2
+ import { storageService } from '../services/storage.js';
3
+ import { createAgentRuntime } from './runtime-factory.js';
4
+ import { advanceAfterRun } from './todo-advance.js';
5
+ const MAX_CHAIN_DEPTH = 20;
6
+ const stopRequests = [];
7
+ const STOP_REQUEST_TTL_MS = 30 * 60 * 1000;
8
+ const pruneStopRequests = () => {
9
+ const now = Date.now();
10
+ for (let i = stopRequests.length - 1; i >= 0; i -= 1) {
11
+ if (now - stopRequests[i].requestedAt > STOP_REQUEST_TTL_MS) {
12
+ stopRequests.splice(i, 1);
13
+ }
14
+ }
15
+ };
16
+ const findStopRequest = (target) => {
17
+ pruneStopRequests();
18
+ return stopRequests.find((r) => {
19
+ if (r.runId !== target.runId)
20
+ return false;
21
+ if (r.agentId && r.agentId !== target.agentId)
22
+ return false;
23
+ if (r.channelId && r.channelId !== target.channelId)
24
+ return false;
25
+ if (r.threadId && r.threadId !== target.threadId)
26
+ return false;
27
+ return true;
28
+ });
29
+ };
30
+ // ---------------------------------------------------------------------------
31
+ // Public API
32
+ // ---------------------------------------------------------------------------
33
+ export async function dispatch(options) {
34
+ const { event } = options;
35
+ ensureEventId(event);
36
+ if (event.type === 'action:agent_run_stop') {
37
+ await handleStop(event, options);
38
+ return;
39
+ }
40
+ const ctx = {
41
+ runId: options.runId,
42
+ channelId: options.channelId,
43
+ threadId: options.threadId,
44
+ onEvent: options.onEvent,
45
+ };
46
+ if (event.type === 'user:input' || event.type === 'agent:invoke') {
47
+ const invoke = await normalizeUserInput(event, ctx);
48
+ await runStep({ agentId: options.agentId || 'system', event: invoke }, ctx, 0);
49
+ return;
50
+ }
51
+ // Bus pass-through: route to the targeted agent's runtime once. No agent step,
52
+ // no advance, no follow-ups. Keeps queries (`/api/state`) cheap and idempotent.
53
+ await runBusEvent(event, options.agentId || 'system', ctx);
54
+ }
55
+ // ---------------------------------------------------------------------------
56
+ // Agent step: run:start -> runtime -> run:end -> advance -> chain
57
+ // ---------------------------------------------------------------------------
58
+ async function runStep(step, ctx, depth) {
59
+ if (depth >= MAX_CHAIN_DEPTH) {
60
+ console.warn(`[dispatcher] Reached MAX_CHAIN_DEPTH (${MAX_CHAIN_DEPTH}); stopping chain.`);
61
+ return;
62
+ }
63
+ const target = {
64
+ runId: ctx.runId,
65
+ agentId: step.agentId,
66
+ channelId: ctx.channelId,
67
+ threadId: ctx.threadId,
68
+ };
69
+ const preStop = findStopRequest(target);
70
+ if (preStop) {
71
+ const state = await storageService.getOpenBotState({ ...target, event: step.event });
72
+ await ctx.onEvent({ type: 'agent:run:stopped', data: { ...target, reason: preStop.reason } }, state);
73
+ return;
74
+ }
75
+ let state;
76
+ try {
77
+ state = await storageService.getOpenBotState({ ...target, event: step.event });
78
+ }
79
+ catch (error) {
80
+ if (error.code === 'AGENT_NOT_FOUND') {
81
+ const fallback = await storageService.getOpenBotState({
82
+ ...target,
83
+ agentId: 'system',
84
+ event: step.event,
85
+ });
86
+ await ctx.onEvent({
87
+ type: 'agent:output',
88
+ data: { content: `⚠️ Agent **${step.agentId}** does not exist. Please check the agent ID and try again.` },
89
+ meta: { agentId: 'system', threadId: ctx.threadId },
90
+ }, fallback);
91
+ return;
92
+ }
93
+ throw error;
94
+ }
95
+ await ctx.onEvent({ type: 'agent:run:start', data: { ...target } }, state);
96
+ const followUps = [];
97
+ const queuedAgentIds = new Set();
98
+ let lastAgentOutput;
99
+ try {
100
+ const runtime = await createAgentRuntime(state);
101
+ for await (const chunk of runtime.run(step.event, { state, runId: ctx.runId })) {
102
+ const stop = findStopRequest(target);
103
+ if (stop) {
104
+ await ctx.onEvent({ type: 'agent:run:stopped', data: { ...target, reason: stop.reason } }, state);
105
+ break;
106
+ }
107
+ if (chunk.id === step.event.id && chunk.type === step.event.type)
108
+ continue;
109
+ if (chunk.type === 'action:create_thread:result' && chunk.data.success) {
110
+ ctx.threadId = chunk.data.threadId || ctx.threadId;
111
+ }
112
+ if (chunk.type === 'agent:output' &&
113
+ chunk.meta?.agentId === step.agentId) {
114
+ const content = chunk.data?.content;
115
+ if (typeof content === 'string' && content.trim())
116
+ lastAgentOutput = content.trim();
117
+ }
118
+ // Handoff requests are internal: queue a follow-up step instead of forwarding.
119
+ if (chunk.type === 'handoff:request') {
120
+ const req = chunk;
121
+ const targetAgent = req.data?.agentId;
122
+ if (targetAgent && targetAgent !== step.agentId && !queuedAgentIds.has(targetAgent)) {
123
+ queuedAgentIds.add(targetAgent);
124
+ followUps.push({
125
+ agentId: targetAgent,
126
+ event: makeInvoke(req.data.content, ctx.threadId, req.meta),
127
+ });
128
+ }
129
+ continue;
130
+ }
131
+ chunk.meta = { ...chunk.meta, agentId: step.agentId };
132
+ await ctx.onEvent(chunk, state);
133
+ }
134
+ }
135
+ catch (error) {
136
+ console.error(`[dispatcher] Agent run failed: ${step.agentId}`, error);
137
+ }
138
+ finally {
139
+ const endState = await storageService.getOpenBotState({ ...target, event: step.event });
140
+ await ctx.onEvent({ type: 'agent:run:end', data: { ...target } }, endState);
141
+ }
142
+ // Autonomous todo advance: single trigger point, runs once per `agent:run:end`.
143
+ try {
144
+ const handoff = await advanceAfterRun({
145
+ storage: storageService,
146
+ channelId: ctx.channelId,
147
+ threadId: ctx.threadId,
148
+ endedAgentId: step.agentId,
149
+ lastAgentOutput,
150
+ });
151
+ if (handoff && !queuedAgentIds.has(handoff.agentId)) {
152
+ queuedAgentIds.add(handoff.agentId);
153
+ followUps.push({
154
+ agentId: handoff.agentId,
155
+ event: makeInvoke(handoff.content, ctx.threadId),
156
+ });
157
+ }
158
+ }
159
+ catch (error) {
160
+ console.warn('[dispatcher] todo advance failed', error);
161
+ }
162
+ for (const next of followUps) {
163
+ await runStep(next, ctx, depth + 1);
164
+ }
165
+ }
166
+ // ---------------------------------------------------------------------------
167
+ // Bus pass-through: run an event through the targeted agent's runtime, forward
168
+ // chunks. No run:start/end, no advance, no follow-ups.
169
+ // ---------------------------------------------------------------------------
170
+ async function runBusEvent(event, agentId, ctx) {
171
+ let state;
172
+ try {
173
+ state = await storageService.getOpenBotState({
174
+ runId: ctx.runId,
175
+ agentId,
176
+ channelId: ctx.channelId,
177
+ threadId: ctx.threadId,
178
+ event,
179
+ });
180
+ }
181
+ catch (error) {
182
+ if (error.code === 'AGENT_NOT_FOUND') {
183
+ // Silently drop: bus pass-through has no UI surface to warn into.
184
+ return;
185
+ }
186
+ throw error;
187
+ }
188
+ try {
189
+ const runtime = await createAgentRuntime(state);
190
+ for await (const chunk of runtime.run(event, { state, runId: ctx.runId })) {
191
+ if (chunk.id === event.id && chunk.type === event.type)
192
+ continue;
193
+ chunk.meta = { ...chunk.meta, agentId };
194
+ await ctx.onEvent(chunk, state);
195
+ }
196
+ }
197
+ catch (error) {
198
+ console.error(`[dispatcher] Bus event failed: ${event.type} (${agentId})`, error);
199
+ }
200
+ }
201
+ // ---------------------------------------------------------------------------
202
+ // Helpers
203
+ // ---------------------------------------------------------------------------
204
+ async function normalizeUserInput(event, ctx) {
205
+ const rawContent = event.data?.content || '';
206
+ // The user-facing copy stored/streamed for the UI.
207
+ const userFacing = {
208
+ type: 'agent:invoke',
209
+ id: event.id,
210
+ data: { content: rawContent, role: 'user' },
211
+ meta: {
212
+ agentId: 'system',
213
+ userId: event.meta?.userId,
214
+ userName: event.meta?.userName,
215
+ userAvatarUrl: event.meta?.userAvatarUrl,
216
+ },
217
+ };
218
+ const initialState = await storageService.getOpenBotState({
219
+ runId: ctx.runId,
220
+ agentId: 'system',
221
+ channelId: ctx.channelId,
222
+ threadId: ctx.threadId,
223
+ event: userFacing,
224
+ });
225
+ await ctx.onEvent(userFacing, initialState);
226
+ // The event actually fed to the target agent. Carries the input threadId (or the
227
+ // message id, used as the anchor for Slack-style new threads).
228
+ return {
229
+ ...event,
230
+ type: 'agent:invoke',
231
+ data: { ...(event.data || {}), content: rawContent, role: 'user' },
232
+ meta: {
233
+ ...(event.meta || {}),
234
+ threadId: ctx.threadId || event.id,
235
+ },
236
+ };
237
+ }
238
+ function makeInvoke(content, threadId, baseMeta) {
239
+ return ensureEventId({
240
+ type: 'agent:invoke',
241
+ data: { role: 'user', content },
242
+ meta: { ...(baseMeta || {}), threadId },
243
+ });
244
+ }
245
+ async function handleStop(stopEvent, options) {
246
+ const { runId, channelId, threadId, onEvent } = options;
247
+ stopRequests.push({
248
+ runId: stopEvent.data.runId,
249
+ agentId: stopEvent.data.agentId,
250
+ channelId: stopEvent.data.channelId || channelId,
251
+ threadId: stopEvent.data.threadId || threadId,
252
+ reason: stopEvent.data.reason,
253
+ requestedAt: Date.now(),
254
+ });
255
+ const state = await storageService.getOpenBotState({
256
+ runId,
257
+ agentId: options.agentId || 'system',
258
+ channelId,
259
+ threadId,
260
+ event: stopEvent,
261
+ });
262
+ await onEvent({
263
+ type: 'action:agent_run_stop:result',
264
+ data: { success: true, message: `Stop requested for run ${stopEvent.data.runId}.` },
265
+ meta: stopEvent.meta,
266
+ }, state);
267
+ }
@@ -2,6 +2,30 @@ import { storageService } from '../services/storage.js';
2
2
  import { createAgentRuntime } from './runtime-factory.js';
3
3
  import { EventNormalizer } from './event-normalizer.js';
4
4
  import { QueueProcessor } from './queue-processor.js';
5
+ const stopRequests = [];
6
+ const STOP_REQUEST_TTL_MS = 30 * 60 * 1000;
7
+ const pruneStopRequests = () => {
8
+ const now = Date.now();
9
+ for (let i = stopRequests.length - 1; i >= 0; i -= 1) {
10
+ if (now - stopRequests[i].requestedAt > STOP_REQUEST_TTL_MS) {
11
+ stopRequests.splice(i, 1);
12
+ }
13
+ }
14
+ };
15
+ const findStopRequest = (options) => {
16
+ pruneStopRequests();
17
+ return stopRequests.find((request) => {
18
+ if (request.runId !== options.runId)
19
+ return false;
20
+ if (request.agentId && request.agentId !== options.agentId)
21
+ return false;
22
+ if (request.channelId && request.channelId !== options.channelId)
23
+ return false;
24
+ if (request.threadId && request.threadId !== options.threadId)
25
+ return false;
26
+ return true;
27
+ });
28
+ };
5
29
  export const orchestratorService = {
6
30
  /**
7
31
  * The primary entry point for all events coming into the system (e.g. from the API).
@@ -9,6 +33,33 @@ export const orchestratorService = {
9
33
  */
10
34
  dispatch: async (options) => {
11
35
  const { runId, channelId, threadId, onEvent } = options;
36
+ if (options.event.type === 'action:agent_run_stop') {
37
+ const stopEvent = options.event;
38
+ stopRequests.push({
39
+ runId: stopEvent.data.runId,
40
+ agentId: stopEvent.data.agentId,
41
+ channelId: stopEvent.data.channelId || channelId,
42
+ threadId: stopEvent.data.threadId || threadId,
43
+ reason: stopEvent.data.reason,
44
+ requestedAt: Date.now(),
45
+ });
46
+ const state = await storageService.getOpenBotState({
47
+ runId,
48
+ agentId: options.agentId || 'system',
49
+ channelId,
50
+ threadId,
51
+ event: options.event,
52
+ });
53
+ await onEvent({
54
+ type: 'action:agent_run_stop:result',
55
+ data: {
56
+ success: true,
57
+ message: `Stop requested for run ${stopEvent.data.runId}.`,
58
+ },
59
+ meta: options.event.meta,
60
+ }, state);
61
+ return;
62
+ }
12
63
  // 1. Normalize incoming event
13
64
  const { finalEvent, finalAgentId } = await EventNormalizer.normalize(options.event, {
14
65
  runId,
@@ -24,6 +75,7 @@ export const orchestratorService = {
24
75
  threadId,
25
76
  onEvent,
26
77
  executeAgent: orchestratorService.executeAgent,
78
+ shouldStopRun: orchestratorService.shouldStopRun,
27
79
  });
28
80
  // 3. Enqueue initial event
29
81
  processor.enqueue({ agentId: finalAgentId, event: finalEvent });
@@ -59,9 +111,29 @@ export const orchestratorService = {
59
111
  throw error;
60
112
  }
61
113
  const agentRuntime = await createAgentRuntime(agentState);
114
+ const stopInfo = {
115
+ runId,
116
+ agentId,
117
+ channelId,
118
+ threadId,
119
+ };
62
120
  try {
63
121
  // RUN the agent runtime
64
122
  for await (const chunk of agentRuntime.run(event, { state: agentState, runId })) {
123
+ const stopRequest = findStopRequest(stopInfo);
124
+ if (stopRequest) {
125
+ await onEvent({
126
+ type: 'agent:run:stopped',
127
+ data: {
128
+ runId,
129
+ agentId,
130
+ channelId,
131
+ threadId,
132
+ reason: stopRequest.reason,
133
+ },
134
+ }, agentState);
135
+ break;
136
+ }
65
137
  chunk.meta = { ...chunk.meta, agentId };
66
138
  await onEvent(chunk, agentState);
67
139
  }
@@ -70,4 +142,8 @@ export const orchestratorService = {
70
142
  console.error(`[orchestrator] Agent run failed: ${agentId}`, error);
71
143
  }
72
144
  },
145
+ shouldStopRun: (options) => {
146
+ const request = findStopRequest(options);
147
+ return request ? { shouldStop: true, reason: request.reason } : { shouldStop: false };
148
+ },
73
149
  };
@@ -28,6 +28,32 @@ export class QueueProcessor {
28
28
  // Run items for the SAME agent sequentially to preserve event order and state consistency.
29
29
  for (const item of items) {
30
30
  const { event: currentEvent } = item;
31
+ const stopCheck = this.options.shouldStopRun?.({
32
+ runId: this.options.runId,
33
+ agentId,
34
+ channelId: this.options.channelId,
35
+ threadId: this.currentThreadId,
36
+ });
37
+ if (stopCheck?.shouldStop) {
38
+ const stoppedState = await storageService.getOpenBotState({
39
+ runId: this.options.runId,
40
+ agentId,
41
+ channelId: this.options.channelId,
42
+ threadId: this.currentThreadId,
43
+ event: currentEvent,
44
+ });
45
+ await this.options.onEvent({
46
+ type: 'agent:run:stopped',
47
+ data: {
48
+ runId: this.options.runId,
49
+ agentId,
50
+ channelId: this.options.channelId,
51
+ threadId: this.currentThreadId,
52
+ reason: stopCheck.reason,
53
+ },
54
+ }, stoppedState);
55
+ continue;
56
+ }
31
57
  // Track handoff requests queued in this step to avoid accidental duplicates.
32
58
  const queuedRequestKeys = new Set();
33
59
  const queuedItems = [];
@@ -1,14 +1,11 @@
1
1
  # Architecture
2
2
 
3
- OpenBot is an orchestration platform built on a modular, event-driven architecture. It leverages the `melony` framework to coordinate interactions between multiple specialized agents via a standardized **Agent Harness**.
3
+ OpenBot is an orchestration platform built on a modular, event-driven architecture. It leverages the `melony` framework to coordinate interactions between multiple specialized agents through a central **orchestrator** (HTTP handlers call it directly).
4
4
 
5
5
  ## Core Components
6
6
 
7
- ### 1. Agent Harness
8
- The Harness is the execution environment for agents. It provides the necessary "plumbing" (storage, communication, tools) so the agent can focus on reasoning. It handles event routing and state management.
9
-
10
- ### 2. Agent Orchestration & Routing
11
- The orchestrator is the central dispatcher within the harness. It receives user input and determines how to delegate tasks across the agent network using the following logic:
7
+ ### 1. Orchestrator & routing
8
+ The orchestrator is the execution entry point for agent work: it normalizes incoming events, runs the queue processor (handoffs and todo-driven assignees), builds per-agent Melony runtimes, and streams emitted events back to callers (for example storage and SSE). Routing across the agent network uses:
12
9
 
13
10
  1. **Command Prefix** — Explicit delegation to a specific agent (e.g., `/os list files`).
14
11
  2. **DM context** — Direct communication with a specific agent.
@@ -20,10 +17,10 @@ A dynamic registry that manages all available agents. Agents can be:
20
17
  - **YAML-based**: Rapidly defined agents in `~/.openbot/agents/*/AGENT.md`.
21
18
  - **TS Packages**: Advanced agents with custom logic in `~/.openbot/agents/*/index.ts`.
22
19
 
23
- ### 3. Plugin Registry
20
+ ### 3. Plugin registry
24
21
  The "capability layer" that provides tools and logic shared across the platform. Plugins (like `shell`, `file-system`, or `mcp`) define the actions agents can perform.
25
22
 
26
- ### 4. Orchestration Layer (Melony)
23
+ ### 4. Orchestration layer (Melony)
27
24
  The underlying event bus that handles all communication. It ensures that agents can collaborate asynchronously, share context, and emit real-time updates to the UI.
28
25
 
29
26
  ## Multi-Agent Workflow
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openbot",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
package/src/app/cli.ts CHANGED
@@ -25,7 +25,7 @@ function checkNodeVersion() {
25
25
 
26
26
  checkNodeVersion();
27
27
 
28
- program.name('openbot').description('OpenBot CLI').version('0.3.2');
28
+ program.name('openbot').description('OpenBot CLI').version('0.3.4');
29
29
 
30
30
  program
31
31
  .command('start')