@meetopenbot/openbot 0.2.6 → 1.0.0

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.
@@ -1,183 +1,88 @@
1
- import z from "zod";
2
- import { asActionBuilder } from "../types.js";
3
- import { buildWorkspaceFileUrl } from "../utils/workspace-url.js";
4
- import { isPlaceholderSpaceId, placeholderSpaceError, } from "../space-id.js";
1
+ import z from 'zod';
2
+ import { isPlaceholderSpaceId, placeholderSpaceError } from '../space-id.js';
5
3
  function normalizeChannelId(raw) {
6
4
  return raw
7
5
  .trim()
8
6
  .toLowerCase()
9
- .replace(/[^a-z0-9]+/g, "-")
10
- .replace(/(^-|-$)/g, "");
7
+ .replace(/[^a-z0-9]+/g, '-')
8
+ .replace(/(^-|-$)/g, '');
11
9
  }
12
10
  function displayNameFromChannelId(channelId) {
13
11
  return channelId
14
- .split("-")
12
+ .split('-')
15
13
  .filter(Boolean)
16
14
  .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
17
- .join(" ");
15
+ .join(' ');
18
16
  }
19
- function createChannelWidget(args) {
20
- return {
21
- type: "client:ui:widget",
17
+ async function emitCreateChannelWidget(ctx, args) {
18
+ await ctx.emit({
19
+ type: 'client:ui:widget',
22
20
  data: {
23
21
  widgetId: args.widgetId,
24
- kind: "message",
25
- title: "create_channel",
22
+ kind: 'message',
23
+ title: 'create_channel',
26
24
  description: args.channelId || undefined,
27
25
  body: args.body,
28
- display: "collapsed",
26
+ display: 'collapsed',
29
27
  state: args.state,
30
28
  },
31
- meta: args.meta,
32
- };
29
+ meta: { agentId: ctx.agentId, threadId: ctx.threadId ?? ctx.state.threadId },
30
+ });
33
31
  }
34
- const storageToolDefinitions = {
32
+ export const storageTools = {
35
33
  create_channel: {
36
- description: "Create a new Space after the human asked for it or agreed to your suggestion. Prefer an existing Space and start_work. Skip questions, listings, and Q&A in #general. Never use placeholder ids (noop, temp, misc).",
34
+ description: 'Create a new Space after the human asked for it or agreed to your suggestion. Prefer an existing Space and start_work. Skip questions, listings, and Q&A in #general. Never use placeholder ids (noop, temp, misc).',
37
35
  inputSchema: z.object({
38
36
  channelId: z
39
37
  .string()
40
- .describe("Unique Space id (e.g. product-launch, backend-platform)."),
38
+ .describe('Unique Space id (e.g. product-launch, backend-platform).'),
41
39
  name: z
42
40
  .string()
43
41
  .optional()
44
- .describe("Optional display name. Defaults to a title-cased Space id."),
42
+ .describe('Optional display name. Defaults to a title-cased Space id.'),
45
43
  spec: z
46
44
  .string()
47
45
  .optional()
48
- .describe("Optional initial markdown content for the channel spec."),
46
+ .describe('Optional initial markdown content for the channel spec.'),
49
47
  initialState: z
50
48
  .record(z.string(), z.unknown())
51
49
  .optional()
52
- .describe("Optional initial state object for the channel. Do not set cwd; the runtime assigns the workspace path."),
50
+ .describe('Optional initial state object for the channel. Do not set cwd; the runtime assigns the workspace path.'),
53
51
  }),
54
- },
55
- patch_channel_details: {
56
- description: "Patch the current Space (state, spec, cwd).",
57
- inputSchema: z
58
- .object({
59
- state: z
60
- .record(z.string(), z.unknown())
61
- .optional()
62
- .describe("JSON state object for the channel."),
63
- spec: z
64
- .string()
65
- .optional()
66
- .describe("Markdown content for the channel specification (SPEC.md). Use for goals and rules."),
67
- cwd: z
68
- .string()
69
- .optional()
70
- .describe("Current working directory for the channel."),
71
- })
72
- .refine((value) => value.state !== undefined ||
73
- value.spec !== undefined ||
74
- value.cwd !== undefined, { message: "Provide at least one of state, spec, or cwd." }),
75
- },
76
- patch_thread_details: {
77
- description: "Patch current thread details (state). Use for thread metadata such as `name` or `isSmartNamed`. For job status use `set_thread_status`. For multi-step task tracking, use `todo_write`.",
78
- inputSchema: z.object({
79
- state: z
80
- .record(z.string(), z.unknown())
81
- .describe("JSON state object for the thread. Merges with existing state."),
82
- }),
83
- },
84
- create_variable: {
85
- description: "Create or update a variable in the workspace storage.",
86
- inputSchema: z.object({
87
- key: z.string().describe("The key of the variable."),
88
- value: z.string().describe("The value of the variable."),
89
- secret: z
90
- .boolean()
91
- .optional()
92
- .describe("Whether the variable is a secret."),
93
- }),
94
- },
95
- delete_variable: {
96
- description: "Delete a variable from the workspace storage.",
97
- inputSchema: z.object({
98
- key: z.string().describe("The key of the variable to delete."),
99
- }),
100
- },
101
- delete_channel: {
102
- description: "Permanently delete a Space and all its threads. Always confirm with the user before deleting.",
103
- inputSchema: z.object({
104
- channelId: z.string().describe("The channel ID to delete."),
105
- }),
106
- },
107
- get_workspace_file_url: {
108
- description: "Get a fetchable HTTP URL for a file in the current channel workspace (images, video, audio, documents).",
109
- inputSchema: z.object({
110
- path: z
111
- .string()
112
- .describe('Path relative to the channel working directory, e.g. "uploads/clip.mp4".'),
113
- }),
114
- },
115
- };
116
- export function registerStorageTools(context) {
117
- const storage = context.storage;
118
- const { publicBaseUrl } = context;
119
- const resolvePublicBaseUrl = () => publicBaseUrl;
120
- return (builder) => {
121
- const actions = asActionBuilder(builder);
122
- actions.on("create_channel", async function* (event, context) {
123
- const { channelId, name, spec, initialState } = event.data;
124
- const rawChannelId = normalizeChannelId(channelId || "");
125
- const resultMeta = {
126
- ...(event.meta || {}),
127
- agentId: context.state.agentId,
128
- threadId: event.meta?.threadId || context.state.threadId,
129
- };
130
- const widgetId = typeof event.meta?.toolCallId === "string"
131
- ? event.meta.toolCallId
132
- : `create_channel:${rawChannelId || Date.now()}`;
52
+ execute: async (raw, ctx) => {
53
+ const { channelId, name, spec, initialState } = (raw ?? {});
54
+ const rawChannelId = normalizeChannelId(channelId || '');
55
+ const widgetId = ctx.toolCallId ?? `create_channel:${rawChannelId || Date.now()}`;
133
56
  if (!rawChannelId) {
134
- const error = "channelId is required";
135
- yield createChannelWidget({
57
+ const error = 'channelId is required';
58
+ await emitCreateChannelWidget(ctx, {
136
59
  widgetId,
137
- channelId: "",
60
+ channelId: '',
138
61
  body: error,
139
- state: "error",
140
- meta: resultMeta,
62
+ state: 'error',
141
63
  });
142
- yield {
143
- type: "action:create_channel:result",
144
- data: {
145
- success: false,
146
- channelId: "",
147
- channelUrl: "",
148
- error,
149
- output: error,
150
- },
151
- meta: resultMeta,
152
- };
153
- return;
64
+ return { success: false, channelId: '', channelUrl: '', error, output: error };
154
65
  }
155
66
  if (isPlaceholderSpaceId(rawChannelId)) {
156
67
  const error = placeholderSpaceError(rawChannelId);
157
- yield createChannelWidget({
68
+ await emitCreateChannelWidget(ctx, {
158
69
  widgetId,
159
70
  channelId: rawChannelId,
160
71
  body: error,
161
- state: "error",
162
- meta: resultMeta,
72
+ state: 'error',
163
73
  });
164
- yield {
165
- type: "action:create_channel:result",
166
- data: {
167
- success: false,
168
- channelId: rawChannelId,
169
- channelUrl: "",
170
- error,
171
- output: error,
172
- },
173
- meta: resultMeta,
74
+ return {
75
+ success: false,
76
+ channelId: rawChannelId,
77
+ channelUrl: '',
78
+ error,
79
+ output: error,
174
80
  };
175
- return;
176
81
  }
177
82
  const channelUrl = `/channels/${rawChannelId}`;
178
- const displayName = typeof name === "string" && name.trim()
83
+ const displayName = typeof name === 'string' && name.trim()
179
84
  ? name.trim()
180
- : typeof initialState?.name === "string" && initialState.name.trim()
85
+ : typeof initialState?.name === 'string' && initialState.name.trim()
181
86
  ? initialState.name.trim()
182
87
  : displayNameFromChannelId(rawChannelId);
183
88
  const mergedInitial = {
@@ -185,317 +90,50 @@ export function registerStorageTools(context) {
185
90
  name: displayName,
186
91
  };
187
92
  delete mergedInitial.cwd;
188
- const channelSpec = typeof spec === "string" && spec.trim() ? spec : `# ${displayName}\n\n`;
189
- yield createChannelWidget({
93
+ const channelSpec = typeof spec === 'string' && spec.trim() ? spec : `# ${displayName}\n\n`;
94
+ await emitCreateChannelWidget(ctx, {
190
95
  widgetId,
191
96
  channelId: rawChannelId,
192
- body: "Creating…",
193
- state: "open",
194
- meta: resultMeta,
97
+ body: 'Creating…',
98
+ state: 'open',
195
99
  });
196
100
  try {
197
- await storage.createChannel({
101
+ await ctx.storage.createChannel({
198
102
  channelId: rawChannelId,
199
103
  spec: channelSpec,
200
104
  initialState: mergedInitial,
201
105
  });
202
106
  const output = `Created channel \`${rawChannelId}\`.`;
203
- yield createChannelWidget({
107
+ await emitCreateChannelWidget(ctx, {
204
108
  widgetId,
205
109
  channelId: rawChannelId,
206
110
  body: output,
207
- state: "submitted",
208
- meta: resultMeta,
111
+ state: 'submitted',
209
112
  });
210
- yield {
211
- type: "action:create_channel:result",
212
- data: { success: true, channelId: rawChannelId, channelUrl, output },
213
- meta: resultMeta,
214
- };
113
+ return { success: true, channelId: rawChannelId, channelUrl, output };
215
114
  }
216
115
  catch (error) {
217
- const message = error instanceof Error ? error.message : "Unknown error";
116
+ const message = error instanceof Error ? error.message : 'Unknown error';
218
117
  const output = `Failed to create channel: ${message}`;
219
- yield createChannelWidget({
118
+ await emitCreateChannelWidget(ctx, {
220
119
  widgetId,
221
120
  channelId: rawChannelId,
222
121
  body: output,
223
- state: "error",
224
- meta: resultMeta,
225
- });
226
- yield {
227
- type: "action:create_channel:result",
228
- data: {
229
- success: false,
230
- channelId: rawChannelId,
231
- channelUrl,
232
- error: message,
233
- output,
234
- },
235
- meta: resultMeta,
236
- };
237
- }
238
- });
239
- actions.on("delete_channel", async function* (event, context) {
240
- const rawChannelId = (event.data?.channelId || "").trim();
241
- const resultMeta = {
242
- ...(event.meta || {}),
243
- agentId: context.state.agentId,
244
- };
245
- if (!rawChannelId) {
246
- yield {
247
- type: "action:delete_channel:result",
248
- data: {
249
- success: false,
250
- channelId: "",
251
- error: "channelId is required",
252
- },
253
- meta: resultMeta,
254
- };
255
- return;
256
- }
257
- try {
258
- await storage.deleteChannel({ channelId: rawChannelId });
259
- yield {
260
- type: "action:delete_channel:result",
261
- data: { success: true, channelId: rawChannelId },
262
- meta: resultMeta,
263
- };
264
- yield {
265
- type: "agent:output",
266
- data: { content: `Deleted channel \`${rawChannelId}\`.` },
267
- meta: resultMeta,
268
- };
269
- }
270
- catch (error) {
271
- yield {
272
- type: "action:delete_channel:result",
273
- data: {
274
- success: false,
275
- channelId: rawChannelId,
276
- error: error instanceof Error ? error.message : "Unknown error",
277
- },
278
- meta: resultMeta,
279
- };
280
- }
281
- });
282
- actions.on("update_channel", async function* (event, context) {
283
- const data = event.data;
284
- const targetChannelId = (data.channelId ||
285
- context.state.channelId ||
286
- "").trim();
287
- const resultMeta = {
288
- ...(event.meta || {}),
289
- agentId: context.state.agentId,
290
- };
291
- if (!targetChannelId) {
292
- yield {
293
- type: "action:update_channel:result",
294
- data: {
295
- success: false,
296
- channelId: "",
297
- updatedFields: [],
298
- },
299
- meta: resultMeta,
300
- };
301
- return;
302
- }
303
- const patch = {};
304
- const updatedFields = [];
305
- if (typeof data.name === "string" && data.name.trim()) {
306
- patch.name = data.name.trim();
307
- updatedFields.push("name");
308
- }
309
- if (typeof data.cwd === "string" && data.cwd.trim()) {
310
- patch.cwd = data.cwd.trim();
311
- updatedFields.push("cwd");
312
- }
313
- try {
314
- if (updatedFields.length > 0) {
315
- await storage.patchChannelState({
316
- channelId: targetChannelId,
317
- state: patch,
318
- });
319
- }
320
- if (targetChannelId === context.state.channelId) {
321
- context.state.channelDetails = await storage.getChannelDetails({
322
- channelId: context.state.channelId,
323
- });
324
- }
325
- yield {
326
- type: "action:update_channel:result",
327
- data: { success: true, channelId: targetChannelId, updatedFields },
328
- meta: resultMeta,
329
- };
330
- }
331
- catch {
332
- yield {
333
- type: "action:update_channel:result",
334
- data: { success: false, channelId: targetChannelId, updatedFields },
335
- meta: resultMeta,
336
- };
337
- }
338
- });
339
- actions.on("patch_channel_details", async function* (event, context) {
340
- const updatedFields = [];
341
- const resultMeta = {
342
- ...(event.meta || {}),
343
- agentId: context.state.agentId,
344
- };
345
- const data = event.data;
346
- try {
347
- if (data.state !== undefined) {
348
- await storage.patchChannelState({
349
- channelId: context.state.channelId,
350
- state: data.state,
351
- });
352
- updatedFields.push("state");
353
- }
354
- if (typeof data.spec === "string") {
355
- await storage.patchChannelSpec({
356
- channelId: context.state.channelId,
357
- spec: data.spec,
358
- });
359
- updatedFields.push("spec");
360
- }
361
- if (typeof data.cwd === "string") {
362
- await storage.patchChannelState({
363
- channelId: context.state.channelId,
364
- state: { cwd: data.cwd },
365
- });
366
- updatedFields.push("cwd");
367
- }
368
- context.state.channelDetails = await storage.getChannelDetails({
369
- channelId: context.state.channelId,
370
- });
371
- yield {
372
- type: "client:ui:widget",
373
- data: {
374
- widgetId: "patch-channel-details-result" + Date.now(),
375
- kind: "message",
376
- title: "Channel details updated.",
377
- body: `The channel details have been updated. ${updatedFields.join(", ")}`,
378
- display: "collapsed",
379
- },
380
- meta: resultMeta,
381
- };
382
- yield {
383
- type: "action:patch_channel_details:result",
384
- data: { success: true, updatedFields },
385
- meta: resultMeta,
386
- };
387
- }
388
- catch {
389
- yield {
390
- type: "action:patch_channel_details:result",
391
- data: { success: false, updatedFields },
392
- meta: resultMeta,
393
- };
394
- }
395
- });
396
- actions.on("patch_thread_details", async function* (event, context) {
397
- const updatedFields = [];
398
- const resultMeta = {
399
- ...(event.meta || {}),
400
- agentId: context.state.agentId,
401
- };
402
- try {
403
- if (!context.state.threadId) {
404
- throw new Error("Missing threadId in state for patch_thread_details");
405
- }
406
- if (event.data?.state !== undefined) {
407
- const state = { ...event.data.state };
408
- delete state.status;
409
- delete state.statusUpdatedAt;
410
- delete state.statusReason;
411
- await storage.patchThreadState({
412
- channelId: context.state.channelId,
413
- threadId: context.state.threadId,
414
- state,
415
- });
416
- updatedFields.push("state");
417
- }
418
- context.state.threadDetails = await storage.getThreadDetails({
419
- channelId: context.state.channelId,
420
- threadId: context.state.threadId,
421
- });
422
- yield {
423
- type: "action:patch_thread_details:result",
424
- data: { success: true, updatedFields },
425
- meta: resultMeta,
426
- };
427
- }
428
- catch {
429
- yield {
430
- type: "action:patch_thread_details:result",
431
- data: { success: false, updatedFields },
432
- meta: resultMeta,
433
- };
434
- }
435
- });
436
- actions.on("get_workspace_file_url", async function* (event, context) {
437
- const channelId = context.state.channelId;
438
- const filePath = event.data?.path;
439
- const toolCallId = event.meta?.toolCallId;
440
- if (!filePath) {
441
- yield {
442
- type: "action:get_workspace_file_url:result",
443
- data: {
444
- success: false,
445
- path: "",
446
- error: "Path is required",
447
- output: "Path is required",
448
- },
449
- meta: { ...(event.meta || {}), toolCallId },
450
- };
451
- return;
452
- }
453
- try {
454
- const { size, mimeType } = await storage.getChannelFileStat({
455
- channelId,
456
- path: filePath,
122
+ state: 'error',
457
123
  });
458
- const url = buildWorkspaceFileUrl({
459
- baseUrl: resolvePublicBaseUrl(),
460
- channelId,
461
- filePath,
462
- });
463
- const output = JSON.stringify({
464
- path: filePath,
465
- url,
466
- mimeType,
467
- size,
468
- });
469
- yield {
470
- type: "action:get_workspace_file_url:result",
471
- data: {
472
- success: true,
473
- path: filePath,
474
- url,
475
- mimeType,
476
- size,
477
- output,
478
- },
479
- meta: { ...(event.meta || {}), toolCallId },
480
- };
481
- }
482
- catch (error) {
483
- const message = error instanceof Error ? error.message : "Unknown error";
484
- yield {
485
- type: "action:get_workspace_file_url:result",
486
- data: {
487
- success: false,
488
- path: filePath,
489
- error: message,
490
- output: message,
491
- },
492
- meta: { ...(event.meta || {}), toolCallId },
124
+ return {
125
+ success: false,
126
+ channelId: rawChannelId,
127
+ channelUrl,
128
+ error: message,
129
+ output,
493
130
  };
494
131
  }
495
- });
496
- };
497
- }
132
+ },
133
+ },
134
+ };
135
+ export const tools = storageTools;
498
136
  export const storageToolPlugin = {
499
- toolDefinitions: storageToolDefinitions,
500
- register: registerStorageTools,
137
+ toolDefinitions: storageTools,
138
+ tools: storageTools,
501
139
  };
@@ -10,7 +10,7 @@ function formatStatusOutput(args) {
10
10
  ? `Job status set to ${args.status}: ${reason}`
11
11
  : `Job status set to ${args.status}.`;
12
12
  }
13
- const threadStatusToolDefinitions = {
13
+ export const toolDefinitions = {
14
14
  set_thread_status: {
15
15
  description: "Set this job's workflow status. Use needs_input when blocked on the human, ready_for_review when you believe the work is done, working when the job is still in progress. Do not use working to mean you are currently executing — that is tracked automatically. Never mark completed or archived — only the human can.",
16
16
  inputSchema: z.object({
@@ -24,31 +24,27 @@ const threadStatusToolDefinitions = {
24
24
  }),
25
25
  },
26
26
  };
27
- export const threadStatusPlugin = {
28
- id: "thread-status",
29
- name: "Thread status",
30
- description: "Sets the durable workflow status for the current job/thread.",
31
- toolDefinitions: threadStatusToolDefinitions,
32
- factory: (pluginContext) => (builder) => {
33
- builder.on("action:set_thread_status", async function* (event, context) {
34
- const resultMeta = { ...(event.meta || {}), agentId: context.state.agentId };
27
+ export const tools = {
28
+ set_thread_status: {
29
+ ...toolDefinitions.set_thread_status,
30
+ execute: async (rawArgs, ctx) => {
35
31
  try {
36
- const channelId = context.state.channelId;
37
- const threadId = context.state.threadId;
32
+ const channelId = ctx.channelId ?? ctx.state.channelId ?? '';
33
+ const threadId = ctx.threadId ?? ctx.state.threadId;
38
34
  if (!channelId || !threadId) {
39
35
  throw new Error("Missing channelId or threadId for set_thread_status");
40
36
  }
41
- const status = event.data.status;
37
+ const data = (rawArgs ?? {});
38
+ const status = data.status;
42
39
  if (status !== "working" &&
43
40
  status !== "needs_input" &&
44
41
  status !== "ready_for_review") {
45
- throw new Error('Agents may only set working, needs_input, or ready_for_review. The human marks completed and archived.');
42
+ throw new Error("Agents may only set working, needs_input, or ready_for_review. The human marks completed and archived.");
46
43
  }
47
- const reasonRaw = event.data.reason;
48
- const reason = typeof reasonRaw === "string" && reasonRaw.trim()
49
- ? reasonRaw.trim()
44
+ const reason = typeof data.reason === "string" && data.reason.trim()
45
+ ? data.reason.trim()
50
46
  : "";
51
- await pluginContext.storage.patchThreadState({
47
+ await ctx.storage.patchThreadState({
52
48
  channelId,
53
49
  threadId,
54
50
  state: {
@@ -57,35 +53,23 @@ export const threadStatusPlugin = {
57
53
  statusReason: reason,
58
54
  },
59
55
  });
60
- context.state.threadDetails = await pluginContext.storage.getThreadDetails({
56
+ ctx.state.threadDetails = await ctx.storage.getThreadDetails({
61
57
  channelId,
62
58
  threadId,
63
59
  });
64
60
  const output = formatStatusOutput({ status, reason });
65
- yield {
66
- type: "action:set_thread_status:result",
67
- data: {
68
- success: true,
69
- status,
70
- ...(reason ? { reason } : {}),
71
- output,
72
- },
73
- meta: resultMeta,
61
+ return {
62
+ success: true,
63
+ status,
64
+ ...(reason ? { reason } : {}),
65
+ output,
74
66
  };
75
67
  }
76
68
  catch (error) {
77
69
  const message = error instanceof Error ? error.message : "Unknown error";
78
- yield {
79
- type: "action:set_thread_status:result",
80
- data: {
81
- success: false,
82
- error: message,
83
- output: message,
84
- },
85
- meta: resultMeta,
86
- };
70
+ return { success: false, error: message, output: message };
87
71
  }
88
- });
72
+ },
89
73
  },
90
74
  };
91
- export default threadStatusPlugin;
75
+ export const threadStatusTools = tools;