@meetopenbot/openbot 1.2.12 → 1.2.14
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/dist/build-tools.js +2 -0
- package/dist/index.js +2 -0
- package/dist/system-prompt.js +5 -3
- package/dist/tools/ask-agent.js +11 -1
- package/dist/tools/preview.js +129 -0
- package/package.json +2 -2
package/dist/build-tools.js
CHANGED
|
@@ -5,6 +5,7 @@ import { askAgentTools } from "./tools/ask-agent.js";
|
|
|
5
5
|
import { installAgentTools } from "./tools/install-agent.js";
|
|
6
6
|
import { threadStatusTools } from "./tools/thread-status.js";
|
|
7
7
|
import { threadTitleTools } from "./tools/thread-title.js";
|
|
8
|
+
import { previewTools } from "./tools/preview.js";
|
|
8
9
|
import { wrapToolsWithApproval } from "./tools/approval.js";
|
|
9
10
|
export const DEFAULT_APPROVAL_ACTIONS = ["update_channel_spec"];
|
|
10
11
|
/**
|
|
@@ -20,6 +21,7 @@ export function buildOpenbotTools(args) {
|
|
|
20
21
|
...installAgentTools,
|
|
21
22
|
...threadStatusTools,
|
|
22
23
|
...threadTitleTools,
|
|
24
|
+
...previewTools,
|
|
23
25
|
...args.context.tools,
|
|
24
26
|
};
|
|
25
27
|
const approvalConfig = args.context.config?.approval;
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { askAgentTools } from './tools/ask-agent.js';
|
|
|
10
10
|
import { installAgentTools } from './tools/install-agent.js';
|
|
11
11
|
import { threadStatusTools } from './tools/thread-status.js';
|
|
12
12
|
import { threadTitleTools } from './tools/thread-title.js';
|
|
13
|
+
import { previewTools } from './tools/preview.js';
|
|
13
14
|
import { buildOpenbotTools } from './build-tools.js';
|
|
14
15
|
export const OPENBOT_PLUGIN_ID = '@meetopenbot/openbot';
|
|
15
16
|
const modelField = await modelConfigField({
|
|
@@ -49,6 +50,7 @@ export const openbotPlugin = defineOpenbotPlugin({
|
|
|
49
50
|
...installAgentTools,
|
|
50
51
|
...threadStatusTools,
|
|
51
52
|
...threadTitleTools,
|
|
53
|
+
...previewTools,
|
|
52
54
|
},
|
|
53
55
|
async *run(args) {
|
|
54
56
|
const { context } = args;
|
package/dist/system-prompt.js
CHANGED
|
@@ -54,11 +54,13 @@ export const OPENBOT_SYSTEM_PROMPT = [
|
|
|
54
54
|
"This runtime is a remote cloud computer. The human is in the browser at openbot.one and cannot open localhost, 127.0.0.1, or any port on this machine.",
|
|
55
55
|
"- Never tell them to visit localhost. They need a public HTTPS URL.",
|
|
56
56
|
"- When you ask a coding agent to run a dev server, preview, or any local process the human should open, tell them to expose it with `cloudflared tunnel --url http://127.0.0.1:<port>` and to add `cloudflared` as a **project devDependency** (not a global install — this image is ephemeral and only workspace files persist).",
|
|
57
|
-
"- Share the `https://*.trycloudflare.com` URL they print. Quick tunnels change on restart — pass the new one through. If they report a localhost URL, ask them to tunnel it.",
|
|
57
|
+
"- Share the `https://*.trycloudflare.com` URL they print, and call `open_preview` with it so it opens in the space browser. Quick tunnels change on restart — pass the new one through. If they report a localhost URL, ask them to tunnel it.",
|
|
58
|
+
"- For any other public page the human should see, call `open_preview` with that http(s) URL too. Never pass localhost, 127.0.0.1, or a port on this machine.",
|
|
58
59
|
"",
|
|
59
60
|
"# SPACE VARIABLES",
|
|
60
|
-
"- Secrets live in OpenBot variables, not in chat. Workspace-wide keys: Settings → Variables. Per-space keys: Space menu → Variables (override workspace keys with the same name).",
|
|
61
|
-
"-
|
|
61
|
+
"- Secrets live in OpenBot variables, not in chat. Workspace-wide keys: Settings → Variables. Per-space keys: Space menu → Variables (override workspace keys with the same name for shell commands in that space).",
|
|
62
|
+
"- Provider keys for harness LLM calls (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, etc.) are workspace-only — space copies of those names are for app shells only and do not switch coding agents off OpenBot Credits.",
|
|
63
|
+
"- Coding agents inherit merged space variables in their shell environment — including `pnpm dev` subprocesses. Do not ask the human to paste secrets. Only tell a coding agent to write a project `.env` when the scaffold requires it or the app errors about a missing `.env`; otherwise a restarted dev server should pick up keys from the shell (use exact names such as `VITE_*` for Vite).",
|
|
62
64
|
"",
|
|
63
65
|
"# COMMUNICATION & WRITING STYLE",
|
|
64
66
|
"- Be concise, professional, proactive, and polite.",
|
package/dist/tools/ask-agent.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { loadAgentModelCatalogs, validateAskModel } from "../agent-models.js";
|
|
4
|
+
import { previewEventsForTunnelUrls } from "./preview.js";
|
|
4
5
|
/**
|
|
5
6
|
* `ask_agent` — OpenBot sends a message to another agent in its own harness.
|
|
6
7
|
* Only the orchestrator may ask. Child events stream into this thread so the
|
|
@@ -103,6 +104,8 @@ async function runAskedAgent(ctx, args) {
|
|
|
103
104
|
resolveNext = null;
|
|
104
105
|
}
|
|
105
106
|
});
|
|
107
|
+
const seenPreviewUrls = new Set();
|
|
108
|
+
const threadId = ctx.threadId ?? ctx.state.threadId;
|
|
106
109
|
while (!isFinished || eventQueue.length > 0) {
|
|
107
110
|
if (eventQueue.length === 0) {
|
|
108
111
|
await new Promise((r) => {
|
|
@@ -110,7 +113,14 @@ async function runAskedAgent(ctx, args) {
|
|
|
110
113
|
});
|
|
111
114
|
}
|
|
112
115
|
while (eventQueue.length > 0) {
|
|
113
|
-
|
|
116
|
+
const nextEvent = eventQueue.shift();
|
|
117
|
+
await ctx.emit(nextEvent);
|
|
118
|
+
for (const preview of previewEventsForTunnelUrls(nextEvent, seenPreviewUrls, {
|
|
119
|
+
agentId: ctx.agentId,
|
|
120
|
+
threadId,
|
|
121
|
+
})) {
|
|
122
|
+
await ctx.emit(preview);
|
|
123
|
+
}
|
|
114
124
|
}
|
|
115
125
|
}
|
|
116
126
|
await runPromise;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
const LOCAL_HOST = /^(?:localhost|.*\.localhost|0\.0\.0\.0|::1|127(?:\.\d{1,3}){3})$/i;
|
|
3
|
+
function isLocalHostname(hostname) {
|
|
4
|
+
const name = hostname.toLowerCase().replace(/\.$/, '');
|
|
5
|
+
return LOCAL_HOST.test(name);
|
|
6
|
+
}
|
|
7
|
+
/** Public http(s) URL the human's browser can open, or null. */
|
|
8
|
+
export function publicPreviewUrl(input) {
|
|
9
|
+
const trimmed = input.trim();
|
|
10
|
+
if (!trimmed)
|
|
11
|
+
return null;
|
|
12
|
+
const withScheme = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
13
|
+
let url;
|
|
14
|
+
try {
|
|
15
|
+
url = new URL(withScheme);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:')
|
|
21
|
+
return null;
|
|
22
|
+
if (url.username || url.password)
|
|
23
|
+
return null;
|
|
24
|
+
if (isLocalHostname(url.hostname))
|
|
25
|
+
return null;
|
|
26
|
+
return url.href;
|
|
27
|
+
}
|
|
28
|
+
const TUNNEL_URL = /https:\/\/[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*\.trycloudflare\.com(?::\d+)?(?:\/[^\s<>"'`)\\\]]*)?/gi;
|
|
29
|
+
const MAX_SCAN_CHARS = 250000;
|
|
30
|
+
function cleanMatch(raw) {
|
|
31
|
+
return raw.replace(/[.,;:!?)]+$/g, '');
|
|
32
|
+
}
|
|
33
|
+
/** Public Cloudflare quick-tunnel URLs mentioned in specialist output. */
|
|
34
|
+
export function tunnelPreviewUrls(value) {
|
|
35
|
+
let text;
|
|
36
|
+
try {
|
|
37
|
+
text = typeof value === 'string' ? value : JSON.stringify(value) ?? '';
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
if (!text)
|
|
43
|
+
return [];
|
|
44
|
+
if (text.length > MAX_SCAN_CHARS)
|
|
45
|
+
text = text.slice(0, MAX_SCAN_CHARS);
|
|
46
|
+
const seen = new Set();
|
|
47
|
+
const urls = [];
|
|
48
|
+
for (const match of text.matchAll(TUNNEL_URL)) {
|
|
49
|
+
const url = publicPreviewUrl(cleanMatch(match[0]));
|
|
50
|
+
if (!url || seen.has(url))
|
|
51
|
+
continue;
|
|
52
|
+
seen.add(url);
|
|
53
|
+
urls.push(url);
|
|
54
|
+
}
|
|
55
|
+
return urls;
|
|
56
|
+
}
|
|
57
|
+
export function previewWidgetEvent(args) {
|
|
58
|
+
return {
|
|
59
|
+
type: 'client:ui:widget',
|
|
60
|
+
data: {
|
|
61
|
+
widgetId: `preview:${args.url}`,
|
|
62
|
+
kind: 'message',
|
|
63
|
+
title: 'Preview',
|
|
64
|
+
body: args.url,
|
|
65
|
+
display: 'expanded',
|
|
66
|
+
state: 'open',
|
|
67
|
+
actions: [
|
|
68
|
+
{
|
|
69
|
+
id: 'open_preview',
|
|
70
|
+
label: 'Open in browser',
|
|
71
|
+
variant: 'primary',
|
|
72
|
+
value: { previewUrl: args.url },
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
metadata: { autoOpen: true },
|
|
76
|
+
},
|
|
77
|
+
meta: {
|
|
78
|
+
agentId: args.agentId,
|
|
79
|
+
...(args.threadId ? { threadId: args.threadId } : {}),
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/** Preview widgets for tunnel URLs in `value` that `seen` has not emitted yet. */
|
|
84
|
+
export function previewEventsForTunnelUrls(value, seen, meta) {
|
|
85
|
+
const events = [];
|
|
86
|
+
for (const url of tunnelPreviewUrls(value)) {
|
|
87
|
+
if (seen.has(url))
|
|
88
|
+
continue;
|
|
89
|
+
seen.add(url);
|
|
90
|
+
events.push(previewWidgetEvent({ url, ...meta }));
|
|
91
|
+
}
|
|
92
|
+
return events;
|
|
93
|
+
}
|
|
94
|
+
async function openPreview(rawArgs, ctx) {
|
|
95
|
+
const channelId = ctx.channelId ?? ctx.state.channelId ?? '';
|
|
96
|
+
const threadId = ctx.threadId ?? ctx.state.threadId;
|
|
97
|
+
if (!channelId || !threadId) {
|
|
98
|
+
const message = 'Missing channelId or threadId for open_preview';
|
|
99
|
+
return { success: false, error: message, output: message };
|
|
100
|
+
}
|
|
101
|
+
const data = (rawArgs ?? {});
|
|
102
|
+
const rawUrl = typeof data.url === 'string' ? data.url : '';
|
|
103
|
+
const url = publicPreviewUrl(rawUrl);
|
|
104
|
+
if (!url) {
|
|
105
|
+
const message = 'Pass a public http(s) URL. Localhost is not reachable from the browser — tunnel it first.';
|
|
106
|
+
return { success: false, error: message, output: message };
|
|
107
|
+
}
|
|
108
|
+
await ctx.emit(previewWidgetEvent({
|
|
109
|
+
url,
|
|
110
|
+
agentId: ctx.agentId,
|
|
111
|
+
threadId,
|
|
112
|
+
}));
|
|
113
|
+
return { success: true, url, output: `Opened ${url} in the space browser.` };
|
|
114
|
+
}
|
|
115
|
+
export const toolDefinitions = {
|
|
116
|
+
open_preview: {
|
|
117
|
+
description: 'Open a public http(s) URL in the space browser beside the chat. Call this with a preview or tunnel URL the human should see, including https://*.trycloudflare.com. Never pass localhost, 127.0.0.1, or any address on this machine.',
|
|
118
|
+
inputSchema: z.object({
|
|
119
|
+
url: z.string().min(1).describe('Public http(s) URL to open in the space browser.'),
|
|
120
|
+
}),
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
export const tools = {
|
|
124
|
+
open_preview: {
|
|
125
|
+
...toolDefinitions.open_preview,
|
|
126
|
+
execute: openPreview,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
export const previewTools = tools;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meetopenbot/openbot",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.14",
|
|
4
4
|
"description": "OpenBot coordinator runtime: ask specialists, route work into Spaces, and track todos.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"@ai-sdk/openai": "^4.0.72",
|
|
26
26
|
"ai": "^7.0.111",
|
|
27
27
|
"zod": "^4.3.5",
|
|
28
|
-
"@meetopenbot/plugin-sdk": "^1.2.
|
|
28
|
+
"@meetopenbot/plugin-sdk": "^1.2.4"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@types/node": "^20.10.1",
|