bazilion 0.3.0 → 0.4.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.
@@ -0,0 +1,30 @@
1
+ -- MCP (Model Context Protocol) servers.
2
+ --
3
+ -- Each row is one configured server the daemon connects to as an MCP client.
4
+ -- Tools discovered on it (`tools/list`) are namespaced `mcp__<name>__<tool>`
5
+ -- and injected into agent turns. v0.4.0 scopes servers globally — every agent
6
+ -- sees the tools of every enabled server.
7
+ --
8
+ -- Transports:
9
+ -- stdio — local subprocess (`command` + JSON `args`); inherits the daemon's
10
+ -- merged secrets env, so a server needing e.g. GITHUB_TOKEN picks it
11
+ -- up from the normal secrets table.
12
+ -- http — Streamable-HTTP endpoint at `url`.
13
+ -- sse — SSE endpoint at `url`.
14
+ --
15
+ -- Bearer auth for http/sse is NOT stored here: the token lives in the encrypted
16
+ -- `secrets` table under key `MCP_TOKEN_<id>`; `has_auth` records whether one is
17
+ -- set so the UI/API can show it without decrypting.
18
+
19
+ CREATE TABLE IF NOT EXISTS mcp_servers (
20
+ id TEXT PRIMARY KEY,
21
+ name TEXT NOT NULL UNIQUE,
22
+ transport TEXT NOT NULL CHECK (transport IN ('stdio','http','sse')),
23
+ command TEXT,
24
+ args TEXT NOT NULL DEFAULT '[]',
25
+ url TEXT,
26
+ has_auth INTEGER NOT NULL DEFAULT 0,
27
+ enabled INTEGER NOT NULL DEFAULT 1,
28
+ created_at INTEGER NOT NULL,
29
+ updated_at INTEGER NOT NULL
30
+ );
package/dist/worker.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // ../daemon/src/runtime/worker/entry.ts
4
- import { randomUUID as randomUUID5 } from "crypto";
4
+ import { randomUUID as randomUUID6 } from "crypto";
5
5
  import { join as join14 } from "path";
6
6
 
7
7
  // ../daemon/src/core/agent/delete.ts
@@ -419,6 +419,50 @@ var SERVICES = [
419
419
  }
420
420
  ]
421
421
  },
422
+ {
423
+ id: "browser",
424
+ displayName: "Browser Automation",
425
+ category: "service",
426
+ group: "Browser",
427
+ hint: "Playwright-driven browser tools (navigate, snapshot, click, screenshot). Run `pnpm exec playwright install chromium` once.",
428
+ fields: [
429
+ {
430
+ envVar: "BROWSER_ENABLED",
431
+ kind: "config",
432
+ label: "Enable browser tools",
433
+ placeholder: "true",
434
+ description: "Expose the browser_* tools to agents (true/false). Default true."
435
+ },
436
+ {
437
+ envVar: "BROWSER_HEADLESS",
438
+ kind: "config",
439
+ label: "Headless",
440
+ placeholder: "true",
441
+ description: "Run Chromium headless (true/false). Default true."
442
+ },
443
+ {
444
+ envVar: "BROWSER_ALLOW_PRIVATE_NETWORK",
445
+ kind: "config",
446
+ label: "Allow private network",
447
+ placeholder: "false",
448
+ description: "Permit the browser to reach loopback/private IPs (SSRF guard off). Default false \u2014 only enable for local dev."
449
+ },
450
+ {
451
+ envVar: "BROWSER_IDLE_MS",
452
+ kind: "config",
453
+ label: "Idle timeout (ms)",
454
+ placeholder: "900000",
455
+ description: "Close an idle browser session after this many ms. Default 900000 (15 min)."
456
+ },
457
+ {
458
+ envVar: "BROWSER_MAX_SESSIONS",
459
+ kind: "config",
460
+ label: "Max concurrent sessions",
461
+ placeholder: "4",
462
+ description: "Cap on simultaneously-open browser sessions (LRU-evicted). Default 4."
463
+ }
464
+ ]
465
+ },
422
466
  // --- External integrations (chat bridges, etc) ---
423
467
  // Each integration has its own dedicated /config/integrations/* page with
424
468
  // workflow-specific UI (preflight health, setup wizard, …). The fields
@@ -476,17 +520,20 @@ var CONFIG_KEYS = [
476
520
  ];
477
521
  var CONFIG_KEY_SET = new Set(CONFIG_KEYS);
478
522
 
479
- // ../daemon/src/core/repos/messages.ts
523
+ // ../daemon/src/core/repos/mcpServers.ts
480
524
  import { randomUUID as randomUUID2 } from "crypto";
481
525
 
526
+ // ../daemon/src/core/repos/messages.ts
527
+ import { randomUUID as randomUUID3 } from "crypto";
528
+
482
529
  // ../daemon/src/core/repos/secrets.ts
483
530
  import { createCipheriv, createDecipheriv, pbkdf2Sync, randomBytes } from "crypto";
484
531
 
485
532
  // ../daemon/src/core/repos/triggers.ts
486
- import { randomUUID as randomUUID3 } from "crypto";
533
+ import { randomUUID as randomUUID4 } from "crypto";
487
534
 
488
535
  // ../daemon/src/core/repos/webTokens.ts
489
- import { createHash, randomBytes as randomBytes2, randomUUID as randomUUID4 } from "crypto";
536
+ import { createHash, randomBytes as randomBytes2, randomUUID as randomUUID5 } from "crypto";
490
537
 
491
538
  // ../daemon/src/core/secrets.ts
492
539
  import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
@@ -666,7 +713,16 @@ function translatePiEvent(e) {
666
713
  if (e.isError) {
667
714
  return [{ type: "tool_error", id: e.toolCallId, name: e.toolName, error: text }];
668
715
  }
669
- return [{ type: "tool_result", id: e.toolCallId, name: e.toolName, result: text }];
716
+ const images = extractToolResultImages(e.result);
717
+ return [
718
+ {
719
+ type: "tool_result",
720
+ id: e.toolCallId,
721
+ name: e.toolName,
722
+ result: text,
723
+ ...images.length > 0 ? { images } : {}
724
+ }
725
+ ];
670
726
  }
671
727
  default:
672
728
  return [];
@@ -697,6 +753,20 @@ function extractToolResultText(result) {
697
753
  }
698
754
  return out;
699
755
  }
756
+ function extractToolResultImages(result) {
757
+ const r = result;
758
+ if (!r?.content) return [];
759
+ const out = [];
760
+ for (const block of r.content) {
761
+ if (block.type === "image") {
762
+ const b = block;
763
+ if (typeof b.data === "string" && typeof b.mimeType === "string") {
764
+ out.push({ data: b.data, mimeType: b.mimeType });
765
+ }
766
+ }
767
+ }
768
+ return out;
769
+ }
700
770
  function stringifyContent(content) {
701
771
  if (typeof content === "string") return content;
702
772
  if (!Array.isArray(content)) return "";
@@ -711,7 +781,13 @@ function piMessagesToProviderView(messages) {
711
781
  for (const m of messages) {
712
782
  switch (m.role) {
713
783
  case "user": {
714
- out.push({ role: "user", content: stringifyContent(m.content) });
784
+ const content = m.content;
785
+ const images = extractToolResultImages({ content });
786
+ out.push({
787
+ role: "user",
788
+ content: stringifyContent(content),
789
+ ...images.length > 0 ? { images } : {}
790
+ });
715
791
  break;
716
792
  }
717
793
  case "assistant": {
@@ -725,11 +801,13 @@ function piMessagesToProviderView(messages) {
725
801
  }
726
802
  case "toolResult": {
727
803
  const tr = m;
804
+ const images = extractToolResultImages({ content: tr.content });
728
805
  out.push({
729
806
  role: "tool",
730
807
  content: stringifyContent(tr.content),
731
808
  toolCallId: tr.toolCallId,
732
- toolName: tr.toolName
809
+ toolName: tr.toolName,
810
+ ...images.length > 0 ? { images } : {}
733
811
  });
734
812
  break;
735
813
  }
@@ -741,8 +819,8 @@ function piMessagesToProviderView(messages) {
741
819
  }
742
820
 
743
821
  // ../daemon/src/runtime/pi/session.ts
744
- import { existsSync as existsSync11, mkdirSync as mkdirSync5, readdirSync as readdirSync7, statSync as statSync5 } from "fs";
745
- import { basename as basename2, join as join13 } from "path";
822
+ import { existsSync as existsSync11, mkdirSync as mkdirSync5, readdirSync as readdirSync7, statSync as statSync6 } from "fs";
823
+ import { basename as basename3, join as join13 } from "path";
746
824
  import {
747
825
  AuthStorage,
748
826
  createAgentSession,
@@ -922,8 +1000,195 @@ function bootstrapTool(agentDir) {
922
1000
  };
923
1001
  }
924
1002
 
1003
+ // ../daemon/src/runtime/tools/browser.ts
1004
+ var REF_DESC = 'Element ref from the latest browser_snapshot, e.g. "e5".';
1005
+ function browserTools(host, agentId) {
1006
+ const proxy = (name, action, description, properties, required = []) => ({
1007
+ def: {
1008
+ name,
1009
+ description,
1010
+ parameters: { type: "object", properties, required, additionalProperties: false }
1011
+ },
1012
+ invoke: (args) => host.invoke(agentId, action, args)
1013
+ });
1014
+ return [
1015
+ proxy(
1016
+ "browser_navigate",
1017
+ "navigate",
1018
+ "Open a URL in the browser and return an accessibility snapshot of the loaded page. Starts a persistent browser session that survives across turns (cookies/logins carry over).",
1019
+ { url: { type: "string", description: "Absolute URL to navigate to." } },
1020
+ ["url"]
1021
+ ),
1022
+ proxy(
1023
+ "browser_snapshot",
1024
+ "snapshot",
1025
+ 'Capture the current page as an accessibility tree (YAML) with [ref=eN] element references. This is the primary way to "see" the page \u2014 prefer it over screenshots. Use the refs with browser_click/type/etc.',
1026
+ {}
1027
+ ),
1028
+ proxy(
1029
+ "browser_click",
1030
+ "click",
1031
+ "Click an element identified by its snapshot ref. Returns a fresh snapshot.",
1032
+ { ref: { type: "string", description: REF_DESC } },
1033
+ ["ref"]
1034
+ ),
1035
+ proxy(
1036
+ "browser_type",
1037
+ "type",
1038
+ "Type text into an input/textarea identified by its ref. Set submit=true to press Enter afterwards. Returns a fresh snapshot.",
1039
+ {
1040
+ ref: { type: "string", description: REF_DESC },
1041
+ text: { type: "string", description: "Text to type into the field." },
1042
+ submit: { type: "boolean", description: "Press Enter after typing." }
1043
+ },
1044
+ ["ref", "text"]
1045
+ ),
1046
+ proxy(
1047
+ "browser_hover",
1048
+ "hover",
1049
+ "Hover the pointer over an element identified by its ref. Returns a fresh snapshot.",
1050
+ { ref: { type: "string", description: REF_DESC } },
1051
+ ["ref"]
1052
+ ),
1053
+ proxy(
1054
+ "browser_select",
1055
+ "select",
1056
+ "Select option(s) in a <select> element identified by its ref.",
1057
+ {
1058
+ ref: { type: "string", description: REF_DESC },
1059
+ values: {
1060
+ type: "array",
1061
+ items: { type: "string" },
1062
+ description: "Option values to select."
1063
+ }
1064
+ },
1065
+ ["ref", "values"]
1066
+ ),
1067
+ proxy(
1068
+ "browser_fill_form",
1069
+ "fill_form",
1070
+ "Fill multiple form fields in one call. Each field is { ref, value }.",
1071
+ {
1072
+ fields: {
1073
+ type: "array",
1074
+ description: "Fields to fill.",
1075
+ items: {
1076
+ type: "object",
1077
+ properties: {
1078
+ ref: { type: "string", description: REF_DESC },
1079
+ value: { type: "string" }
1080
+ },
1081
+ required: ["ref", "value"]
1082
+ }
1083
+ }
1084
+ },
1085
+ ["fields"]
1086
+ ),
1087
+ proxy(
1088
+ "browser_press_key",
1089
+ "press_key",
1090
+ 'Press a keyboard key (e.g. "Enter", "Escape", "ArrowDown", "Control+A").',
1091
+ { key: { type: "string", description: "Key or chord to press." } },
1092
+ ["key"]
1093
+ ),
1094
+ proxy(
1095
+ "browser_go_back",
1096
+ "go_back",
1097
+ "Navigate back to the previous page in history. Returns a fresh snapshot.",
1098
+ {}
1099
+ ),
1100
+ proxy(
1101
+ "browser_tabs",
1102
+ "tabs",
1103
+ 'Manage tabs. op="list" lists open tabs; "new" opens a tab (optional url); "select" focuses tab at index; "close" closes tab at index (defaults to active).',
1104
+ {
1105
+ op: { type: "string", enum: ["list", "new", "select", "close"] },
1106
+ index: { type: "number", description: "Tab index for select/close." },
1107
+ url: { type: "string", description: "URL to open for op=new." }
1108
+ },
1109
+ ["op"]
1110
+ ),
1111
+ proxy(
1112
+ "browser_take_screenshot",
1113
+ "take_screenshot",
1114
+ "Take a PNG screenshot of the current page (returned as an image). Use only for visual verification or canvas/pixel content the accessibility snapshot cannot represent \u2014 prefer browser_snapshot for interaction.",
1115
+ { full_page: { type: "boolean", description: "Capture the full scrollable page." } }
1116
+ ),
1117
+ proxy(
1118
+ "browser_console",
1119
+ "console",
1120
+ "Return recent browser console messages (logs, warnings, errors).",
1121
+ {}
1122
+ ),
1123
+ proxy(
1124
+ "browser_network",
1125
+ "network",
1126
+ "Return recent network requests issued by the page (method, status, URL).",
1127
+ {}
1128
+ )
1129
+ ];
1130
+ }
1131
+
1132
+ // ../daemon/src/runtime/tools/deliver-file.ts
1133
+ import { readFileSync as readFileSync8, statSync as statSync4 } from "fs";
1134
+ import { basename as basename2, extname, isAbsolute, resolve as resolve3 } from "path";
1135
+ var MAX_DELIVER_BYTES = 25 * 1024 * 1024;
1136
+ var MIME = {
1137
+ ".pdf": "application/pdf",
1138
+ ".txt": "text/plain",
1139
+ ".md": "text/markdown",
1140
+ ".csv": "text/csv",
1141
+ ".json": "application/json",
1142
+ ".html": "text/html",
1143
+ ".zip": "application/zip",
1144
+ ".png": "image/png",
1145
+ ".jpg": "image/jpeg",
1146
+ ".jpeg": "image/jpeg",
1147
+ ".gif": "image/gif",
1148
+ ".webp": "image/webp"
1149
+ };
1150
+ function deliverFileTool(cwd, sink) {
1151
+ return {
1152
+ def: {
1153
+ name: "deliver_file",
1154
+ description: "Send a file from your workspace to the user so they can download it (web), receive it as a document (Telegram), or save it (CLI). Use this to deliver reports, exports, or any artifact you produced. Max 25 MB.",
1155
+ parameters: {
1156
+ type: "object",
1157
+ properties: {
1158
+ path: {
1159
+ type: "string",
1160
+ description: "Path to the file, relative to your workspace or absolute."
1161
+ }
1162
+ },
1163
+ required: ["path"],
1164
+ additionalProperties: false
1165
+ }
1166
+ },
1167
+ async invoke(args) {
1168
+ const p = String(args.path ?? "");
1169
+ if (!p) throw new Error("deliver_file: path is required");
1170
+ const abs = isAbsolute(p) ? p : resolve3(cwd, p);
1171
+ let size;
1172
+ try {
1173
+ size = statSync4(abs).size;
1174
+ } catch {
1175
+ throw new Error(`deliver_file: no such file: ${p}`);
1176
+ }
1177
+ if (size > MAX_DELIVER_BYTES) {
1178
+ throw new Error(
1179
+ `deliver_file: "${basename2(abs)}" is too large (${(size / 1024 / 1024).toFixed(1)} MB > 25 MB)`
1180
+ );
1181
+ }
1182
+ const name = basename2(abs);
1183
+ const mimeType = MIME[extname(abs).toLowerCase()] ?? "application/octet-stream";
1184
+ sink({ name, mimeType, data: readFileSync8(abs).toString("base64") });
1185
+ return `Delivered "${name}" (${mimeType}) to the user.`;
1186
+ }
1187
+ };
1188
+ }
1189
+
925
1190
  // ../daemon/src/runtime/tools/home.ts
926
- import { readdirSync as readdirSync6, readFileSync as readFileSync8, statSync as statSync4, writeFileSync as writeFileSync5 } from "fs";
1191
+ import { readdirSync as readdirSync6, readFileSync as readFileSync9, statSync as statSync5, writeFileSync as writeFileSync5 } from "fs";
927
1192
  import { join as join12 } from "path";
928
1193
  var HOME_FILES_READABLE = [
929
1194
  "IDENTITY.md",
@@ -963,7 +1228,7 @@ function homeTools(agentDir) {
963
1228
  }
964
1229
  const path = join12(agentDir, file);
965
1230
  try {
966
- return readFileSync8(path, "utf8");
1231
+ return readFileSync9(path, "utf8");
967
1232
  } catch (err) {
968
1233
  const msg = err instanceof Error ? err.message : String(err);
969
1234
  throw new Error(`home_read: could not read ${file}: ${msg}`);
@@ -1007,7 +1272,7 @@ function homeTools(agentDir) {
1007
1272
  for (const file of HOME_FILES_READABLE) {
1008
1273
  const path = join12(agentDir, file);
1009
1274
  try {
1010
- const s = statSync4(path);
1275
+ const s = statSync5(path);
1011
1276
  entries.push(`${file} (${s.size}b)`);
1012
1277
  } catch {
1013
1278
  }
@@ -1028,6 +1293,18 @@ function homeTools(agentDir) {
1028
1293
  ];
1029
1294
  }
1030
1295
 
1296
+ // ../daemon/src/runtime/tools/mcp.ts
1297
+ function mcpProxyTools(host, tools) {
1298
+ return tools.map((t) => ({
1299
+ def: {
1300
+ name: t.toolName,
1301
+ description: t.description,
1302
+ parameters: t.inputSchema
1303
+ },
1304
+ invoke: (args) => host.invoke(t.serverId, t.rawName, args)
1305
+ }));
1306
+ }
1307
+
1031
1308
  // ../daemon/src/runtime/tools/memory.ts
1032
1309
  function memoryTools(memory) {
1033
1310
  return [
@@ -1853,14 +2130,20 @@ function ourToolToPiTool(h) {
1853
2130
  description: h.def.description,
1854
2131
  parameters: Type2.Unsafe(h.def.parameters),
1855
2132
  async execute(_toolCallId, params) {
1856
- const text = await h.invoke(params);
2133
+ const out = await h.invoke(params);
1857
2134
  return {
1858
- content: [{ type: "text", text }],
2135
+ content: toPiContent(out),
1859
2136
  details: {}
1860
2137
  };
1861
2138
  }
1862
2139
  };
1863
2140
  }
2141
+ function toPiContent(out) {
2142
+ if (typeof out === "string") return [{ type: "text", text: out }];
2143
+ return out.map(
2144
+ (p) => p.type === "text" ? { type: "text", text: p.text } : { type: "image", data: p.data, mimeType: p.mimeType }
2145
+ );
2146
+ }
1864
2147
  function createBazilionCustomTools(opts) {
1865
2148
  const handlers = [
1866
2149
  ...memoryTools(opts.memory),
@@ -1874,13 +2157,35 @@ function createBazilionCustomTools(opts) {
1874
2157
  if (opts.userMdHost) {
1875
2158
  handlers.push(...userMdTools(opts.userMdHost, opts.agent.group.id));
1876
2159
  }
2160
+ if (opts.browserHost) {
2161
+ handlers.push(...browserTools(opts.browserHost, opts.agent.agent.id));
2162
+ }
2163
+ if (opts.mcpHost && opts.mcpTools && opts.mcpTools.length > 0) {
2164
+ handlers.push(...mcpProxyTools(opts.mcpHost, opts.mcpTools));
2165
+ }
2166
+ if (opts.fileSink) {
2167
+ handlers.push(deliverFileTool(opts.agent.group.path, opts.fileSink));
2168
+ }
1877
2169
  return handlers.map(ourToolToPiTool);
1878
2170
  }
1879
2171
 
1880
2172
  // ../daemon/src/runtime/pi/session.ts
1881
2173
  var BUILTIN_TOOL_NAMES = ["read", "bash", "edit", "write", "grep", "find", "ls"];
1882
2174
  async function createBazilionSession(opts) {
1883
- const { agent, paths, env, memory, enabledProviders, messagingHost, userMdHost, refreshApiKey } = opts;
2175
+ const {
2176
+ agent,
2177
+ paths,
2178
+ env,
2179
+ memory,
2180
+ enabledProviders,
2181
+ messagingHost,
2182
+ userMdHost,
2183
+ browserHost,
2184
+ mcpHost,
2185
+ mcpTools,
2186
+ fileSink,
2187
+ refreshApiKey
2188
+ } = opts;
1884
2189
  const { providerName, modelId } = splitModelString(agent.model);
1885
2190
  if (enabledProviders.size > 0 && !enabledProviders.has(providerName)) {
1886
2191
  throw new Error(`${providerName} provider is disabled \u2014 enable it on the /config page`);
@@ -1927,7 +2232,17 @@ async function createBazilionSession(opts) {
1927
2232
  const bazilionPrompt = buildSystemPrompt(agent);
1928
2233
  const resourceLoader = createBazilionResourceLoader(bazilionPrompt);
1929
2234
  await resourceLoader.reload();
1930
- const customTools = createBazilionCustomTools({ agent, memory, messagingHost, userMdHost, env });
2235
+ const customTools = createBazilionCustomTools({
2236
+ agent,
2237
+ memory,
2238
+ messagingHost,
2239
+ userMdHost,
2240
+ browserHost,
2241
+ mcpHost,
2242
+ mcpTools,
2243
+ fileSink,
2244
+ env
2245
+ });
1931
2246
  const allowedTools = [...BUILTIN_TOOL_NAMES, ...customTools.map((t) => t.name)];
1932
2247
  const { session } = await createAgentSession({
1933
2248
  cwd,
@@ -2063,7 +2378,7 @@ function findMostRecent(sessionDir) {
2063
2378
  if (!entry.endsWith(".jsonl")) continue;
2064
2379
  const path = join13(sessionDir, entry);
2065
2380
  try {
2066
- const s = statSync5(path);
2381
+ const s = statSync6(path);
2067
2382
  if (!newest || s.mtimeMs > newest.mtimeMs) newest = { path, mtimeMs: s.mtimeMs };
2068
2383
  } catch {
2069
2384
  }
@@ -2105,10 +2420,10 @@ function createIpcClient() {
2105
2420
  if (!process.send) {
2106
2421
  return Promise.reject(new Error("worker: no IPC channel \u2014 daemon must spawn with stdio:ipc"));
2107
2422
  }
2108
- const id = randomUUID5();
2423
+ const id = randomUUID6();
2109
2424
  const message = { type: "rpc", id, method, args };
2110
- return new Promise((resolve3, reject) => {
2111
- pending.set(id, { resolve: (v) => resolve3(v), reject });
2425
+ return new Promise((resolve4, reject) => {
2426
+ pending.set(id, { resolve: (v) => resolve4(v), reject });
2112
2427
  process.send?.(message, void 0, void 0, (err) => {
2113
2428
  if (err) {
2114
2429
  pending.delete(id);
@@ -2135,6 +2450,16 @@ function createIpcUserMdHost(call) {
2135
2450
  write: (groupId, content, ifMatch) => call("userMdWrite", { groupId, content, ifMatch })
2136
2451
  };
2137
2452
  }
2453
+ function createIpcBrowserHost(call) {
2454
+ return {
2455
+ invoke: (agentId, action, args) => call("browserInvoke", { agentId, action, args })
2456
+ };
2457
+ }
2458
+ function createIpcMcpHost(call) {
2459
+ return {
2460
+ invoke: (serverId, toolName, args) => call("mcpInvoke", { serverId, toolName, args })
2461
+ };
2462
+ }
2138
2463
  function isIpcReply(msg) {
2139
2464
  if (!msg || typeof msg !== "object") return false;
2140
2465
  const m = msg;
@@ -2157,13 +2482,15 @@ async function main() {
2157
2482
  };
2158
2483
  process.on("SIGTERM", onSignal);
2159
2484
  process.on("SIGINT", onSignal);
2160
- const { agent, message, enabledProviders, apiKey } = await readInput();
2485
+ const { agent, message, enabledProviders, apiKey, browserEnabled, mcpTools, images } = await readInput();
2161
2486
  const paths = resolvePaths();
2162
2487
  const memory = qmdBackend(join14(agent.group.path, "memory"));
2163
2488
  await memory.init();
2164
2489
  const ipcCall = createIpcClient();
2165
2490
  const messagingHost = createIpcMessagingHost(ipcCall);
2166
2491
  const userMdHost = createIpcUserMdHost(ipcCall);
2492
+ const browserHost = browserEnabled ? createIpcBrowserHost(ipcCall) : void 0;
2493
+ const mcpHost = mcpTools && mcpTools.length > 0 ? createIpcMcpHost(ipcCall) : void 0;
2167
2494
  const { session, dispose } = await createBazilionSession({
2168
2495
  agent,
2169
2496
  paths,
@@ -2172,7 +2499,12 @@ async function main() {
2172
2499
  enabledProviders: new Set(enabledProviders),
2173
2500
  messagingHost,
2174
2501
  userMdHost,
2175
- apiKey
2502
+ apiKey,
2503
+ browserHost,
2504
+ mcpHost,
2505
+ mcpTools,
2506
+ // deliver_file emits a `file` event straight onto our stdout frame stream.
2507
+ fileSink: (f) => emit({ kind: "event", event: { type: "file", ...f } })
2176
2508
  });
2177
2509
  abortSession = () => {
2178
2510
  void session.abort();
@@ -2183,7 +2515,12 @@ async function main() {
2183
2515
  }
2184
2516
  });
2185
2517
  try {
2186
- await session.prompt(message);
2518
+ const promptImages = (images ?? []).map((img) => ({
2519
+ type: "image",
2520
+ data: img.data,
2521
+ mimeType: img.mimeType
2522
+ }));
2523
+ await session.prompt(message, promptImages.length > 0 ? { images: promptImages } : void 0);
2187
2524
  await session.agent.waitForIdle();
2188
2525
  emit({
2189
2526
  kind: "done",