@growthub/cli 0.14.1 → 0.14.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 (30) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/login/route.js +3 -2
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/logout/route.js +3 -2
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/status/route.js +3 -2
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +14 -1
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceHelperSetupModal.jsx +1 -1
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/AgentSwarmPanel.jsx +49 -2
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +54 -11
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +113 -36
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxAgentAuthPanel.jsx +34 -14
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +7 -0
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +13 -4
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/docs/sandbox-environment-primitive.md +26 -0
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/adapters/local-intelligence-browser-access.js +516 -0
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-agent-host.js +85 -7
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-process.js +3 -1
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/index.js +1 -0
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/sandbox-adapter-registry.js +5 -1
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/data-model/field-contracts.js +1 -0
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-agent-swarm.js +8 -0
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +3 -0
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +4 -2
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +1 -0
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth.js +82 -27
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-serverless-flow.js +4 -2
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +1 -0
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +6 -0
  27. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-swarm-proposal.js +3 -0
  28. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package-lock.json +364 -0
  29. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -0
  30. package/package.json +1 -1
@@ -0,0 +1,516 @@
1
+ /**
2
+ * Drop-zone override: local-intelligence browser access.
3
+ *
4
+ * The default `local-intelligence` adapter remains JSON-only and propose-only.
5
+ * This fork-level adapter is loaded after the default adapter and replaces the
6
+ * same registry id. It delegates byte-for-byte behavior when `browserAccess`
7
+ * is off, and only runs the local browser bridge when sandbox-run explicitly
8
+ * passes `browserAccess: true`.
9
+ */
10
+
11
+ import { getSandboxAdapter, registerSandboxAdapter } from "../sandbox-adapter-registry.js";
12
+
13
+ const baseLocalIntelligence = getSandboxAdapter("local-intelligence");
14
+ const MAX_OUT = 256 * 1024;
15
+
16
+ function clampText(text) {
17
+ const value = String(text || "");
18
+ if (Buffer.byteLength(value, "utf8") <= MAX_OUT) return value;
19
+ return `${Buffer.from(value).slice(0, MAX_OUT).toString("utf8")}\n…\n[truncated]`;
20
+ }
21
+
22
+ function parseJsonMaybe(text) {
23
+ try {
24
+ return JSON.parse(String(text || "").trim());
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ function parseAdapterEnvelope(result) {
31
+ const parsed = parseJsonMaybe(result?.stdout);
32
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
33
+ }
34
+
35
+ function extractFirstUrl(text) {
36
+ const match = String(text || "").match(/https?:\/\/[^\s"'<>}]+/i);
37
+ return match ? match[0] : "";
38
+ }
39
+
40
+ function inferSearchQuery(request) {
41
+ const raw = String(request?.intelligenceSandbox?.userIntent || request?.command || "");
42
+ const promptMatch = raw.match(/(?:^|\n)\s*Prompt:\s*([\s\S]+)/i);
43
+ const text = String(promptMatch?.[1] || raw).replace(/\s+/g, " ").trim();
44
+ const quoted = text.match(/"([^"]{3,120})"/);
45
+ if (quoted) return quoted[1].trim();
46
+ const objective = text
47
+ .replace(/^instructions:\s*/i, "")
48
+ .replace(/\b(return|include|end with|each fact|outcome_score)\b.*$/i, "")
49
+ .replace(/\b(use sandbox browser access to|use browser access to|use the browser to|browser access to|research|search|look up|browse|web access proof and pull|pull)\b/ig, " ")
50
+ .replace(/\s+/g, " ")
51
+ .trim();
52
+ return (objective || text).slice(0, 160);
53
+ }
54
+
55
+ function browserSearchUrl(query) {
56
+ const base = String(process.env.GROWTHUB_BROWSER_SEARCH_URL || "https://r.jina.ai/http://r.jina.ai/http://https://duckduckgo.com/html/?q={query}").trim();
57
+ return base.includes("{query}")
58
+ ? base.replace("{query}", encodeURIComponent(query))
59
+ : `${base}${base.includes("?") ? "&" : "?"}q=${encodeURIComponent(query)}`;
60
+ }
61
+
62
+ function significantTokens(text) {
63
+ const stop = new Set(["the", "and", "for", "with", "from", "this", "that", "into", "return", "include", "source", "title", "url", "fact", "facts", "research", "search", "browser", "access", "sandbox", "use"]);
64
+ return String(text || "")
65
+ .toLowerCase()
66
+ .match(/[a-z0-9][a-z0-9-]{2,}/g)
67
+ ?.filter((token) => !stop.has(token)) || [];
68
+ }
69
+
70
+ function alignSearchQuery(query, request) {
71
+ const inferred = inferSearchQuery(request);
72
+ if (!query) return inferred;
73
+ const wanted = significantTokens(inferred);
74
+ if (wanted.length < 2) return query;
75
+ const actual = new Set(significantTokens(query));
76
+ const matched = wanted.filter((token) => actual.has(token)).length;
77
+ return matched >= Math.ceil(wanted.length / 2) ? query : inferred;
78
+ }
79
+
80
+ function normalizeToolIntent(intent, request) {
81
+ if (typeof intent === "string") {
82
+ const url = extractFirstUrl(intent);
83
+ const lower = intent.trim().toLowerCase();
84
+ if (url && /(navigate|goto|open|browser\.navigate|browser\.goto)/.test(lower)) {
85
+ return { tool: "browser.navigate", url };
86
+ }
87
+ if (/(extract|read|snapshot|browser\.extract|browser\.read)/.test(lower)) {
88
+ return { tool: "browser.extract", selector: "body" };
89
+ }
90
+ return null;
91
+ }
92
+ if (!intent || typeof intent !== "object" || Array.isArray(intent)) return null;
93
+ const tool = String(intent.tool || intent.name || intent.action || "").trim().toLowerCase();
94
+ const query = String(intent.query || intent.q || intent.search || "").trim();
95
+ const url = String(intent.url || intent.href || extractFirstUrl(JSON.stringify(intent))).trim();
96
+ const selector = String(intent.selector || "").trim();
97
+ const text = String(intent.text || intent.value || "").trim();
98
+ if (["browser.search", "search", "web.search"].includes(tool) && query) {
99
+ return { tool: "browser.search", query: alignSearchQuery(query, request) };
100
+ }
101
+ if (["browser.navigate", "browser.goto", "navigate", "goto", "open"].includes(tool) && url) {
102
+ return { tool: "browser.navigate", url };
103
+ }
104
+ if (["browser.extract", "browser.read", "browser.snapshot", "extract", "read", "snapshot"].includes(tool)) {
105
+ return { tool: "browser.extract", selector: selector || "body" };
106
+ }
107
+ if (["browser.click", "click"].includes(tool) && selector) {
108
+ return { tool: "browser.click", selector };
109
+ }
110
+ if (["browser.type", "browser.fill", "type", "fill"].includes(tool) && selector) {
111
+ return { tool: "browser.type", selector, text };
112
+ }
113
+ return null;
114
+ }
115
+
116
+ function normalizeToolIntents(toolIntents, request) {
117
+ const raw = Array.isArray(toolIntents) ? toolIntents : [];
118
+ const joined = raw.map((item) => typeof item === "string" ? item : JSON.stringify(item)).join(" ");
119
+ const direct = raw.map((intent) => normalizeToolIntent(intent, request)).filter(Boolean);
120
+ const url = extractFirstUrl(joined);
121
+ if (url && !direct.some((intent) => intent.tool === "browser.navigate")) {
122
+ direct.unshift({ tool: "browser.navigate", url });
123
+ }
124
+ const useful = direct.filter((intent) => {
125
+ if (intent.tool !== "browser.navigate") return true;
126
+ return !/^https?:\/\/(www\.)?google\.com\/?$/i.test(intent.url);
127
+ });
128
+ const intents = useful;
129
+ if (intents.some((intent) => intent.tool === "browser.navigate" || intent.tool === "browser.search") && !intents.some((intent) => intent.tool === "browser.extract")) {
130
+ intents.push({ tool: "browser.extract", selector: "body" });
131
+ }
132
+ return intents.slice(0, 8);
133
+ }
134
+
135
+ async function collectPageSnapshot(page, { maxText = 12000 } = {}) {
136
+ return page.evaluate((limit) => {
137
+ const text = (document.body?.innerText || "").replace(/\s+\n/g, "\n").trim().slice(0, limit);
138
+ const links = Array.from(document.querySelectorAll("a"))
139
+ .map((a) => ({
140
+ title: (a.innerText || a.getAttribute("aria-label") || a.textContent || "").replace(/\s+/g, " ").trim(),
141
+ url: a.href || "",
142
+ }))
143
+ .filter((item) => item.title && /^https?:\/\//i.test(item.url))
144
+ .filter((item, index, all) => all.findIndex((other) => other.url === item.url) === index)
145
+ .slice(0, 20);
146
+ return { text, links };
147
+ }, maxText).catch(() => ({ text: "", links: [] }));
148
+ }
149
+
150
+ function buildBrowserToolSystemPrompt() {
151
+ return [
152
+ "You are Growthub workspace sandbox local intelligence.",
153
+ "Reply with a single JSON object only, matching:",
154
+ "{\"text\":string optional,\"json\":object optional,\"toolIntents\":[],\"warnings\":[],\"confidence\":number}",
155
+ "This sandbox run has browserAccess=true.",
156
+ "When the task asks for current facts, research, citations, browsing, or web verification, first return browser tool intents instead of a final answer.",
157
+ "Supported browser tool intents:",
158
+ "{\"tool\":\"browser.search\",\"query\":\"search text\"}",
159
+ "{\"tool\":\"browser.navigate\",\"url\":\"https://example.com\"}",
160
+ "{\"tool\":\"browser.extract\",\"selector\":\"body\"}",
161
+ "{\"tool\":\"browser.click\",\"selector\":\"button\"}",
162
+ "{\"tool\":\"browser.type\",\"selector\":\"input[name=q]\",\"text\":\"search text\"}",
163
+ "Do not claim browser results until observations are supplied back to you.",
164
+ ].join("\n");
165
+ }
166
+
167
+ function withBrowserToolPrompt(request) {
168
+ const box = request?.intelligenceSandbox || {};
169
+ const explicitMessages = Array.isArray(box.messages)
170
+ ? box.messages.filter((m) => m && typeof m.role === "string" && typeof m.content === "string")
171
+ : null;
172
+ const messages = explicitMessages && explicitMessages.length > 0
173
+ ? [{ role: "system", content: buildBrowserToolSystemPrompt() }, ...explicitMessages.filter((m) => m.role !== "system")]
174
+ : [
175
+ { role: "system", content: buildBrowserToolSystemPrompt() },
176
+ { role: "user", content: String(box.userIntent || request?.command || "") },
177
+ ];
178
+ return {
179
+ ...request,
180
+ intelligenceSandbox: {
181
+ ...box,
182
+ messages,
183
+ userIntent: String(box.userIntent || request?.command || "")
184
+ }
185
+ };
186
+ }
187
+
188
+ async function loadBrowserDriver() {
189
+ if (globalThis.__growthubLocalIntelligenceBrowserDriver) {
190
+ return globalThis.__growthubLocalIntelligenceBrowserDriver;
191
+ }
192
+ let mod = null;
193
+ try {
194
+ mod = await import("playwright-core");
195
+ } catch {
196
+ mod = null;
197
+ }
198
+ const chromium = mod?.chromium;
199
+ if (!chromium?.launch) {
200
+ throw new Error("browserAccess requested for local-intelligence, but no local browser bridge is available. Install playwright-core in the workspace, or provide globalThis.__growthubLocalIntelligenceBrowserDriver from the host.");
201
+ }
202
+ return {
203
+ async run(intents, { timeoutMs }) {
204
+ const launchOptions = { headless: true };
205
+ const executablePath = String(process.env.GROWTHUB_BROWSER_EXECUTABLE || "").trim();
206
+ if (executablePath) {
207
+ launchOptions.executablePath = executablePath;
208
+ } else {
209
+ launchOptions.channel = "chrome";
210
+ }
211
+ const browser = await chromium.launch(launchOptions);
212
+ const page = await browser.newPage();
213
+ const observations = [];
214
+ try {
215
+ page.setDefaultTimeout(Math.min(Math.max(Number(timeoutMs) || 30000, 5000), 60000));
216
+ for (const intent of intents) {
217
+ if (intent.tool === "browser.search") {
218
+ const url = browserSearchUrl(intent.query);
219
+ await page.goto(url, { waitUntil: "domcontentloaded" });
220
+ const snapshot = await collectPageSnapshot(page);
221
+ observations.push({
222
+ tool: intent.tool,
223
+ query: intent.query,
224
+ url: page.url(),
225
+ title: await page.title(),
226
+ text: snapshot.text,
227
+ links: snapshot.links,
228
+ });
229
+ } else if (intent.tool === "browser.navigate") {
230
+ await page.goto(intent.url, { waitUntil: "domcontentloaded" });
231
+ const snapshot = await collectPageSnapshot(page);
232
+ observations.push({ tool: intent.tool, url: page.url(), title: await page.title(), text: snapshot.text, links: snapshot.links });
233
+ } else if (intent.tool === "browser.extract") {
234
+ const selector = intent.selector || "body";
235
+ const content = await page.locator(selector).first().innerText({ timeout: 10000 }).catch(async () => page.textContent("body"));
236
+ const snapshot = await collectPageSnapshot(page);
237
+ observations.push({ tool: intent.tool, selector, url: page.url(), text: String(content || snapshot.text || "").slice(0, 12000), links: snapshot.links });
238
+ } else if (intent.tool === "browser.click") {
239
+ await page.locator(intent.selector).first().click();
240
+ observations.push({ tool: intent.tool, selector: intent.selector, url: page.url(), title: await page.title() });
241
+ } else if (intent.tool === "browser.type") {
242
+ await page.locator(intent.selector).first().fill(intent.text || "");
243
+ observations.push({ tool: intent.tool, selector: intent.selector, url: page.url() });
244
+ }
245
+ }
246
+ return observations;
247
+ } finally {
248
+ await browser.close().catch(() => {});
249
+ }
250
+ }
251
+ };
252
+ }
253
+
254
+ function buildToolRepairMessages(request, firstEnvelope) {
255
+ const userIntent = String(request?.intelligenceSandbox?.userIntent || request?.command || "");
256
+ return [
257
+ {
258
+ role: "system",
259
+ content: [
260
+ "You repair browser tool calls for Growthub local intelligence.",
261
+ "Return one JSON object only with this exact shape:",
262
+ "{\"toolIntents\":[{\"tool\":\"browser.search\",\"query\":\"...\"}],\"warnings\":[]}",
263
+ "Supported tools: browser.search, browser.navigate, browser.extract, browser.click, browser.type.",
264
+ "For open-ended web research, use browser.search with a concise query derived from the user's task.",
265
+ "Preserve named entities, years, product names, companies, and domain terms from the user's task. Do not generalize them away.",
266
+ "Do not answer the task. Only return toolIntents."
267
+ ].join("\n")
268
+ },
269
+ {
270
+ role: "user",
271
+ content: [
272
+ "User task:",
273
+ userIntent,
274
+ "",
275
+ "Malformed or unusable first model response:",
276
+ JSON.stringify(firstEnvelope?.result || {}),
277
+ "",
278
+ "Suggested query if search is needed:",
279
+ inferSearchQuery(request)
280
+ ].join("\n")
281
+ }
282
+ ];
283
+ }
284
+
285
+ function buildFollowupMessages(request, firstEnvelope, observations) {
286
+ const box = request.intelligenceSandbox || {};
287
+ const explicitMessages = Array.isArray(box.messages)
288
+ ? box.messages.filter((m) => m && typeof m.role === "string" && typeof m.content === "string")
289
+ : null;
290
+ const messages = explicitMessages && explicitMessages.length > 0
291
+ ? explicitMessages
292
+ : [
293
+ {
294
+ role: "system",
295
+ content: [
296
+ "You are Growthub workspace sandbox local intelligence.",
297
+ "Reply with a single JSON object only, matching:",
298
+ "{\"text\":string optional,\"json\":object optional,\"toolIntents\":[],\"warnings\":[],\"confidence\":number}",
299
+ ].join("\n")
300
+ },
301
+ { role: "user", content: String(box.userIntent || request.command || "") },
302
+ ];
303
+ return [
304
+ ...messages,
305
+ { role: "assistant", content: JSON.stringify(firstEnvelope.result || {}) },
306
+ {
307
+ role: "user",
308
+ content: [
309
+ "Browser observations from the sandbox runtime:",
310
+ JSON.stringify(observations),
311
+ "Return the final JSON object now. Do not claim unobserved facts. Set toolIntents to []."
312
+ ].join("\n")
313
+ }
314
+ ];
315
+ }
316
+
317
+ async function callChatCompletion({ endpoint, model, messages, signal }) {
318
+ const res = await fetch(endpoint, {
319
+ method: "POST",
320
+ headers: { "content-type": "application/json", accept: "application/json" },
321
+ body: JSON.stringify({
322
+ model,
323
+ messages,
324
+ temperature: 0.3,
325
+ response_format: { type: "json_object" },
326
+ }),
327
+ signal,
328
+ });
329
+ const text = clampText(Buffer.from(await res.arrayBuffer()).toString("utf8"));
330
+ return { res, text };
331
+ }
332
+
333
+ function parseModelContent(text) {
334
+ const outer = parseJsonMaybe(text);
335
+ if (outer && Array.isArray(outer.choices) && outer.choices[0]?.message?.content) {
336
+ const inner = String(outer.choices[0].message.content || "").trim();
337
+ return { outer, parsed: parseJsonMaybe(inner) || { text: inner, warnings: ["model completion was not valid JSON"], toolIntents: [], confidence: 0 } };
338
+ }
339
+ return { outer, parsed: outer && typeof outer === "object" ? outer : { text, warnings: ["invalid JSON from model"], toolIntents: [], confidence: 0 } };
340
+ }
341
+
342
+ function normalizeParsedResult(parsed) {
343
+ const json = parsed?.json && typeof parsed.json === "object" ? parsed.json : undefined;
344
+ const text =
345
+ typeof parsed?.text === "string" ? parsed.text
346
+ : typeof json?.text === "string" ? json.text
347
+ : typeof json?.answer === "string" ? json.answer
348
+ : undefined;
349
+ return {
350
+ text,
351
+ json,
352
+ toolIntents: Array.isArray(parsed?.toolIntents) ? parsed.toolIntents : [],
353
+ warnings: Array.isArray(parsed?.warnings) ? parsed.warnings : [],
354
+ confidence: typeof parsed?.confidence === "number"
355
+ ? parsed.confidence
356
+ : (typeof json?.confidence === "number" ? json.confidence : 0),
357
+ };
358
+ }
359
+
360
+ async function run(request) {
361
+ const started = Date.now();
362
+ if (!baseLocalIntelligence?.run) {
363
+ return {
364
+ ok: false,
365
+ exitCode: 1,
366
+ durationMs: 0,
367
+ stdout: "",
368
+ stderr: "",
369
+ error: "default local-intelligence adapter was not registered before browser-access override",
370
+ adapterMeta: { adapter: "local-intelligence", browserAccess: Boolean(request?.browserAccess) }
371
+ };
372
+ }
373
+ if (!request?.browserAccess) {
374
+ return baseLocalIntelligence.run(request);
375
+ }
376
+
377
+ const browserRequest = withBrowserToolPrompt(request);
378
+ const first = await baseLocalIntelligence.run(browserRequest);
379
+ if (!first?.ok) {
380
+ return {
381
+ ...first,
382
+ adapterMeta: {
383
+ ...(first?.adapterMeta || {}),
384
+ browserAccess: true,
385
+ browserLane: "local-intelligence-browser-bridge"
386
+ }
387
+ };
388
+ }
389
+
390
+ const firstEnvelope = parseAdapterEnvelope(first);
391
+ const toolIntents = Array.isArray(firstEnvelope?.result?.toolIntents) ? firstEnvelope.result.toolIntents : [];
392
+ let intents = normalizeToolIntents(toolIntents, browserRequest);
393
+ const endpoint = firstEnvelope?.adapter?.endpoint || first.adapterMeta?.endpoint;
394
+ const model = firstEnvelope?.adapter?.modelId || first.adapterMeta?.model;
395
+ if (!intents.length && endpoint && model) {
396
+ const controller = new AbortController();
397
+ const repairTimer = setTimeout(() => controller.abort(), Math.min(Number(request.timeoutMs) || 60000, 60000));
398
+ try {
399
+ const repair = await callChatCompletion({
400
+ endpoint,
401
+ model,
402
+ messages: buildToolRepairMessages(browserRequest, firstEnvelope),
403
+ signal: controller.signal,
404
+ });
405
+ if (repair.res.ok) {
406
+ const { parsed } = parseModelContent(repair.text);
407
+ intents = normalizeToolIntents(Array.isArray(parsed?.toolIntents) ? parsed.toolIntents : [], browserRequest);
408
+ }
409
+ } finally {
410
+ clearTimeout(repairTimer);
411
+ }
412
+ }
413
+ if (!intents.length) {
414
+ return {
415
+ ...first,
416
+ adapterMeta: {
417
+ ...(first.adapterMeta || {}),
418
+ browserAccess: true,
419
+ browserLane: "local-intelligence-browser-bridge",
420
+ tools: 0
421
+ }
422
+ };
423
+ }
424
+
425
+ const driver = await loadBrowserDriver();
426
+ const observations = await driver.run(intents, { timeoutMs: request.timeoutMs });
427
+ if (!endpoint || !model) {
428
+ return {
429
+ ok: false,
430
+ exitCode: 1,
431
+ durationMs: Date.now() - started,
432
+ stdout: first.stdout || "",
433
+ stderr: "",
434
+ error: "local-intelligence browser bridge could not resolve endpoint/model from first pass",
435
+ adapterMeta: { ...(first.adapterMeta || {}), browserAccess: true, browserLane: "local-intelligence-browser-bridge" }
436
+ };
437
+ }
438
+
439
+ const controller = new AbortController();
440
+ const ms = Number(request.timeoutMs) > 0 ? Math.min(Number(request.timeoutMs), 600000) : 60000;
441
+ const timer = setTimeout(() => controller.abort(), ms);
442
+ try {
443
+ const followup = await callChatCompletion({
444
+ endpoint,
445
+ model,
446
+ messages: buildFollowupMessages(browserRequest, firstEnvelope, observations),
447
+ signal: controller.signal,
448
+ });
449
+ if (!followup.res.ok) {
450
+ return {
451
+ ok: false,
452
+ exitCode: 1,
453
+ durationMs: Date.now() - started,
454
+ stdout: followup.text,
455
+ stderr: "",
456
+ error: `local model HTTP ${followup.res.status}`,
457
+ adapterMeta: { ...(first.adapterMeta || {}), browserAccess: true, browserLane: "local-intelligence-browser-bridge", tools: intents.length }
458
+ };
459
+ }
460
+ const { outer, parsed } = parseModelContent(followup.text);
461
+ const normalized = normalizeParsedResult(parsed);
462
+ const envelope = {
463
+ ...(firstEnvelope || {}),
464
+ result: {
465
+ text: normalized.text,
466
+ json: normalized.json,
467
+ toolIntents: normalized.toolIntents,
468
+ warnings: normalized.warnings,
469
+ confidence: normalized.confidence,
470
+ browserObservations: observations,
471
+ },
472
+ rawText: followup.text,
473
+ latencyMs: Date.now() - started,
474
+ createdAt: new Date().toISOString(),
475
+ };
476
+ return {
477
+ ok: true,
478
+ exitCode: 0,
479
+ durationMs: Date.now() - started,
480
+ stdout: JSON.stringify(envelope, null, 2),
481
+ stderr: "",
482
+ adapterMeta: {
483
+ ...(first.adapterMeta || {}),
484
+ browserAccess: true,
485
+ browserLane: "local-intelligence-browser-bridge",
486
+ tokens: Number.isFinite(outer?.usage?.total_tokens) ? outer.usage.total_tokens : first.adapterMeta?.tokens ?? null,
487
+ tools: intents.length,
488
+ }
489
+ };
490
+ } catch (error) {
491
+ return {
492
+ ok: false,
493
+ exitCode: 1,
494
+ durationMs: Date.now() - started,
495
+ stdout: "",
496
+ stderr: clampText(error?.message || error),
497
+ error: error?.name === "AbortError" ? `timed out after ${ms}ms` : error?.message || "browser bridge failed",
498
+ adapterMeta: { ...(first.adapterMeta || {}), browserAccess: true, browserLane: "local-intelligence-browser-bridge", tools: intents.length }
499
+ };
500
+ } finally {
501
+ clearTimeout(timer);
502
+ }
503
+ }
504
+
505
+ registerSandboxAdapter({
506
+ ...(baseLocalIntelligence || {}),
507
+ id: "local-intelligence",
508
+ label: "Local intelligence (OpenAI-compatible)",
509
+ description:
510
+ "Calls the local OpenAI-compatible model. When sandbox browserAccess is true, browser tool intents execute through the local browser bridge.",
511
+ locality: "local",
512
+ supportedRuntimes: [],
513
+ run,
514
+ });
515
+
516
+ export { normalizeToolIntent };