bazilion 0.3.0 → 0.5.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.
package/dist/worker.js CHANGED
@@ -1,23 +1,30 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // ../daemon/src/runtime/worker/entry.ts
4
- import { randomUUID as randomUUID5 } from "crypto";
5
- import { join as join14 } from "path";
4
+ import { randomUUID as randomUUID6 } from "crypto";
5
+ import { join as join16 } from "path";
6
6
 
7
7
  // ../daemon/src/core/agent/delete.ts
8
8
  import { existsSync, rmSync } from "fs";
9
9
 
10
+ // ../daemon/src/core/agent/resolve.ts
11
+ import { join } from "path";
12
+
13
+ // ../daemon/src/core/profile/identity.ts
14
+ import { readFileSync } from "fs";
15
+
10
16
  // ../daemon/src/core/agent/spawn.ts
11
17
  import { randomUUID } from "crypto";
12
- import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "fs";
13
- import { join as join4 } from "path";
18
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
19
+ import { join as join6 } from "path";
14
20
 
15
21
  // ../daemon/src/core/profile/load.ts
16
22
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
17
- import { join } from "path";
23
+ import { join as join2 } from "path";
18
24
 
19
- // ../daemon/src/core/profile/identity.ts
20
- import { readFileSync } from "fs";
25
+ // ../daemon/src/core/profile/seed.ts
26
+ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
27
+ import { join as join4 } from "path";
21
28
 
22
29
  // ../daemon/src/core/group/register.ts
23
30
  import { existsSync as existsSync3, mkdirSync, statSync, symlinkSync } from "fs";
@@ -25,56 +32,56 @@ import { resolve } from "path";
25
32
 
26
33
  // ../daemon/src/core/profile/create.ts
27
34
  import { mkdirSync as mkdirSync2, writeFileSync } from "fs";
28
- import { join as join2 } from "path";
35
+ import { join as join3 } from "path";
29
36
 
30
37
  // ../daemon/src/core/skills/discover.ts
31
- import { existsSync as existsSync4, readdirSync } from "fs";
32
- import { join as join3 } from "path";
38
+ import { existsSync as existsSync5, readdirSync } from "fs";
39
+ import { join as join5 } from "path";
33
40
 
34
41
  // ../daemon/src/core/db/client.ts
35
42
  import { DatabaseSync } from "node:sqlite";
36
43
 
37
44
  // ../daemon/src/core/db/migrate.ts
38
- import { readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
39
- import { dirname, join as join5 } from "path";
45
+ import { readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
46
+ import { dirname, join as join7 } from "path";
40
47
  import { fileURLToPath } from "url";
41
- var migrationsDir = join5(dirname(fileURLToPath(import.meta.url)), "migrations");
48
+ var migrationsDir = join7(dirname(fileURLToPath(import.meta.url)), "migrations");
42
49
 
43
50
  // ../daemon/src/core/paths.ts
44
51
  import { homedir } from "os";
45
- import { join as join6 } from "path";
52
+ import { join as join8 } from "path";
46
53
  function resolvePaths(home) {
47
- const root = home ?? process.env.BAZILION_HOME ?? join6(homedir(), ".bazilion");
54
+ const root = home ?? process.env.BAZILION_HOME ?? join8(homedir(), ".bazilion");
48
55
  return {
49
56
  home: root,
50
- db: join6(root, "bazilion.db"),
51
- authFile: join6(root, "auth.json"),
52
- profilesDir: join6(root, "profiles"),
53
- agentsDir: join6(root, "agents"),
54
- skillsDir: join6(root, "skills"),
55
- groupsDir: join6(root, "groups"),
56
- logsDir: join6(root, "logs"),
57
+ db: join8(root, "bazilion.db"),
58
+ authFile: join8(root, "auth.json"),
59
+ profilesDir: join8(root, "profiles"),
60
+ agentsDir: join8(root, "agents"),
61
+ skillsDir: join8(root, "skills"),
62
+ groupsDir: join8(root, "groups"),
63
+ logsDir: join8(root, "logs"),
57
64
  profileDir(id) {
58
- return join6(root, "profiles", id);
65
+ return join8(root, "profiles", id);
59
66
  },
60
67
  agentDir(id) {
61
- return join6(root, "agents", id);
68
+ return join8(root, "agents", id);
62
69
  },
63
70
  skillDir(name) {
64
- return join6(root, "skills", name);
71
+ return join8(root, "skills", name);
65
72
  },
66
73
  groupDir(slug) {
67
- return join6(root, "groups", slug);
74
+ return join8(root, "groups", slug);
68
75
  }
69
76
  };
70
77
  }
71
78
 
72
79
  // ../daemon/src/core/profile/delete.ts
73
- import { existsSync as existsSync5, rmSync as rmSync2 } from "fs";
80
+ import { existsSync as existsSync6, rmSync as rmSync2 } from "fs";
74
81
 
75
82
  // ../daemon/src/core/profile/update.ts
76
- import { writeFileSync as writeFileSync3 } from "fs";
77
- import { join as join7 } from "path";
83
+ import { writeFileSync as writeFileSync4 } from "fs";
84
+ import { join as join9 } from "path";
78
85
 
79
86
  // ../daemon/src/core/profile-group/spawn.ts
80
87
  import { readdirSync as readdirSync3, rmSync as rmSync4 } from "fs";
@@ -419,6 +426,50 @@ var SERVICES = [
419
426
  }
420
427
  ]
421
428
  },
429
+ {
430
+ id: "browser",
431
+ displayName: "Browser Automation",
432
+ category: "service",
433
+ group: "Browser",
434
+ hint: "Playwright-driven browser tools (navigate, snapshot, click, screenshot). Run `pnpm exec playwright install chromium` once.",
435
+ fields: [
436
+ {
437
+ envVar: "BROWSER_ENABLED",
438
+ kind: "config",
439
+ label: "Enable browser tools",
440
+ placeholder: "true",
441
+ description: "Expose the browser_* tools to agents (true/false). Default true."
442
+ },
443
+ {
444
+ envVar: "BROWSER_HEADLESS",
445
+ kind: "config",
446
+ label: "Headless",
447
+ placeholder: "true",
448
+ description: "Run Chromium headless (true/false). Default true."
449
+ },
450
+ {
451
+ envVar: "BROWSER_ALLOW_PRIVATE_NETWORK",
452
+ kind: "config",
453
+ label: "Allow private network",
454
+ placeholder: "false",
455
+ description: "Permit the browser to reach loopback/private IPs (SSRF guard off). Default false \u2014 only enable for local dev."
456
+ },
457
+ {
458
+ envVar: "BROWSER_IDLE_MS",
459
+ kind: "config",
460
+ label: "Idle timeout (ms)",
461
+ placeholder: "900000",
462
+ description: "Close an idle browser session after this many ms. Default 900000 (15 min)."
463
+ },
464
+ {
465
+ envVar: "BROWSER_MAX_SESSIONS",
466
+ kind: "config",
467
+ label: "Max concurrent sessions",
468
+ placeholder: "4",
469
+ description: "Cap on simultaneously-open browser sessions (LRU-evicted). Default 4."
470
+ }
471
+ ]
472
+ },
422
473
  // --- External integrations (chat bridges, etc) ---
423
474
  // Each integration has its own dedicated /config/integrations/* page with
424
475
  // workflow-specific UI (preflight health, setup wizard, …). The fields
@@ -476,42 +527,45 @@ var CONFIG_KEYS = [
476
527
  ];
477
528
  var CONFIG_KEY_SET = new Set(CONFIG_KEYS);
478
529
 
479
- // ../daemon/src/core/repos/messages.ts
530
+ // ../daemon/src/core/repos/mcpServers.ts
480
531
  import { randomUUID as randomUUID2 } from "crypto";
481
532
 
533
+ // ../daemon/src/core/repos/messages.ts
534
+ import { randomUUID as randomUUID3 } from "crypto";
535
+
482
536
  // ../daemon/src/core/repos/secrets.ts
483
537
  import { createCipheriv, createDecipheriv, pbkdf2Sync, randomBytes } from "crypto";
484
538
 
485
539
  // ../daemon/src/core/repos/triggers.ts
486
- import { randomUUID as randomUUID3 } from "crypto";
540
+ import { randomUUID as randomUUID4 } from "crypto";
487
541
 
488
542
  // ../daemon/src/core/repos/webTokens.ts
489
- import { createHash, randomBytes as randomBytes2, randomUUID as randomUUID4 } from "crypto";
543
+ import { createHash, randomBytes as randomBytes2, randomUUID as randomUUID5 } from "crypto";
490
544
 
491
545
  // ../daemon/src/core/secrets.ts
492
- import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
546
+ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
493
547
 
494
548
  // ../daemon/src/core/skills/import.ts
495
- import { cpSync, existsSync as existsSync7, mkdtempSync, readdirSync as readdirSync4, rmSync as rmSync5, statSync as statSync2 } from "fs";
549
+ import { cpSync, existsSync as existsSync8, mkdtempSync, readdirSync as readdirSync4, rmSync as rmSync5, statSync as statSync2 } from "fs";
496
550
  import { tmpdir } from "os";
497
- import { basename, join as join8, resolve as resolve2, sep } from "path";
551
+ import { basename, join as join10, resolve as resolve2, sep } from "path";
498
552
  import AdmZip from "adm-zip";
499
553
 
500
554
  // ../daemon/src/core/skills/parse.ts
501
- import { readFileSync as readFileSync5 } from "fs";
555
+ import { readFileSync as readFileSync6 } from "fs";
502
556
  import { parse as parseYaml } from "yaml";
503
557
 
504
558
  // ../daemon/src/runtime/memory/qmd.ts
505
559
  import {
506
- existsSync as existsSync8,
560
+ existsSync as existsSync9,
507
561
  mkdirSync as mkdirSync4,
508
562
  readdirSync as readdirSync5,
509
- readFileSync as readFileSync6,
563
+ readFileSync as readFileSync7,
510
564
  rmSync as rmSync6,
511
565
  statSync as statSync3,
512
- writeFileSync as writeFileSync4
566
+ writeFileSync as writeFileSync5
513
567
  } from "fs";
514
- import { dirname as dirname2, join as join9 } from "path";
568
+ import { dirname as dirname2, join as join11 } from "path";
515
569
  import { createStore, extractSnippet } from "@tobilu/qmd";
516
570
  var INDEX_FILENAME = ".qmd-index.sqlite";
517
571
  var COLLECTION_NAME = "memory";
@@ -521,7 +575,7 @@ function getStore(dir) {
521
575
  let p = storeCache.get(dir);
522
576
  if (!p) {
523
577
  p = createStore({
524
- dbPath: join9(dir, INDEX_FILENAME),
578
+ dbPath: join11(dir, INDEX_FILENAME),
525
579
  config: {
526
580
  collections: {
527
581
  [COLLECTION_NAME]: { path: dir, pattern: PATTERN }
@@ -536,13 +590,13 @@ function safeKey(root, key) {
536
590
  if (key.includes("..") || key.startsWith("/") || key.includes("\0")) {
537
591
  throw new Error(`unsafe memory key: ${key}`);
538
592
  }
539
- return join9(root, key);
593
+ return join11(root, key);
540
594
  }
541
595
  function walkMd(dir, prefix, out) {
542
- if (!existsSync8(dir)) return;
596
+ if (!existsSync9(dir)) return;
543
597
  for (const e of readdirSync5(dir, { withFileTypes: true })) {
544
598
  if (e.name.startsWith(".")) continue;
545
- const full = join9(dir, e.name);
599
+ const full = join11(dir, e.name);
546
600
  const key = prefix ? `${prefix}/${e.name}` : e.name;
547
601
  if (e.isDirectory()) {
548
602
  walkMd(full, key, out);
@@ -550,7 +604,7 @@ function walkMd(dir, prefix, out) {
550
604
  const stats = statSync3(full);
551
605
  out.push({
552
606
  key,
553
- content: readFileSync6(full, "utf8"),
607
+ content: readFileSync7(full, "utf8"),
554
608
  updatedAt: stats.mtimeMs
555
609
  });
556
610
  }
@@ -565,20 +619,20 @@ function qmdBackend(root) {
565
619
  },
566
620
  async read(key) {
567
621
  const path = safeKey(root, key);
568
- if (!existsSync8(path)) {
622
+ if (!existsSync9(path)) {
569
623
  throw new Error(`memory entry not found: ${key}`);
570
624
  }
571
625
  const stats = statSync3(path);
572
626
  return {
573
627
  key,
574
- content: readFileSync6(path, "utf8"),
628
+ content: readFileSync7(path, "utf8"),
575
629
  updatedAt: stats.mtimeMs
576
630
  };
577
631
  },
578
632
  async write(key, content) {
579
633
  const path = safeKey(root, key);
580
634
  mkdirSync4(dirname2(path), { recursive: true });
581
- writeFileSync4(path, content);
635
+ writeFileSync5(path, content);
582
636
  const stats = statSync3(path);
583
637
  const store = await getStore(root);
584
638
  await store.update();
@@ -598,7 +652,7 @@ function qmdBackend(root) {
598
652
  let content = r.body ?? "";
599
653
  if (!content) {
600
654
  try {
601
- content = readFileSync6(join9(root, key), "utf8");
655
+ content = readFileSync7(join11(root, key), "utf8");
602
656
  } catch {
603
657
  content = "";
604
658
  }
@@ -615,7 +669,7 @@ function qmdBackend(root) {
615
669
  },
616
670
  async remove(key) {
617
671
  const path = safeKey(root, key);
618
- if (existsSync8(path)) rmSync6(path);
672
+ if (existsSync9(path)) rmSync6(path);
619
673
  const store = await getStore(root);
620
674
  await store.update();
621
675
  }
@@ -666,7 +720,16 @@ function translatePiEvent(e) {
666
720
  if (e.isError) {
667
721
  return [{ type: "tool_error", id: e.toolCallId, name: e.toolName, error: text }];
668
722
  }
669
- return [{ type: "tool_result", id: e.toolCallId, name: e.toolName, result: text }];
723
+ const images = extractToolResultImages(e.result);
724
+ return [
725
+ {
726
+ type: "tool_result",
727
+ id: e.toolCallId,
728
+ name: e.toolName,
729
+ result: text,
730
+ ...images.length > 0 ? { images } : {}
731
+ }
732
+ ];
670
733
  }
671
734
  default:
672
735
  return [];
@@ -697,6 +760,20 @@ function extractToolResultText(result) {
697
760
  }
698
761
  return out;
699
762
  }
763
+ function extractToolResultImages(result) {
764
+ const r = result;
765
+ if (!r?.content) return [];
766
+ const out = [];
767
+ for (const block of r.content) {
768
+ if (block.type === "image") {
769
+ const b = block;
770
+ if (typeof b.data === "string" && typeof b.mimeType === "string") {
771
+ out.push({ data: b.data, mimeType: b.mimeType });
772
+ }
773
+ }
774
+ }
775
+ return out;
776
+ }
700
777
  function stringifyContent(content) {
701
778
  if (typeof content === "string") return content;
702
779
  if (!Array.isArray(content)) return "";
@@ -711,7 +788,13 @@ function piMessagesToProviderView(messages) {
711
788
  for (const m of messages) {
712
789
  switch (m.role) {
713
790
  case "user": {
714
- out.push({ role: "user", content: stringifyContent(m.content) });
791
+ const content = m.content;
792
+ const images = extractToolResultImages({ content });
793
+ out.push({
794
+ role: "user",
795
+ content: stringifyContent(content),
796
+ ...images.length > 0 ? { images } : {}
797
+ });
715
798
  break;
716
799
  }
717
800
  case "assistant": {
@@ -725,11 +808,13 @@ function piMessagesToProviderView(messages) {
725
808
  }
726
809
  case "toolResult": {
727
810
  const tr = m;
811
+ const images = extractToolResultImages({ content: tr.content });
728
812
  out.push({
729
813
  role: "tool",
730
814
  content: stringifyContent(tr.content),
731
815
  toolCallId: tr.toolCallId,
732
- toolName: tr.toolName
816
+ toolName: tr.toolName,
817
+ ...images.length > 0 ? { images } : {}
733
818
  });
734
819
  break;
735
820
  }
@@ -741,8 +826,8 @@ function piMessagesToProviderView(messages) {
741
826
  }
742
827
 
743
828
  // ../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";
829
+ import { existsSync as existsSync12, mkdirSync as mkdirSync5, readdirSync as readdirSync7, statSync as statSync6 } from "fs";
830
+ import { basename as basename3, join as join15 } from "path";
746
831
  import {
747
832
  AuthStorage,
748
833
  createAgentSession,
@@ -802,8 +887,8 @@ function resolveModel(cfg, modelId) {
802
887
  import { loginOpenAICodex, refreshOpenAICodexToken } from "@earendil-works/pi-ai/oauth";
803
888
 
804
889
  // ../daemon/src/runtime/session/prompt.ts
805
- import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
806
- import { join as join10 } from "path";
890
+ import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
891
+ import { join as join12 } from "path";
807
892
  var CONTEXT_FILE_ORDER = [
808
893
  "AGENTS.md",
809
894
  "SOUL.md",
@@ -815,9 +900,9 @@ function buildSystemPrompt(agent) {
815
900
  const parts = [];
816
901
  const contextBlocks = [];
817
902
  for (const file of CONTEXT_FILE_ORDER) {
818
- const path = join10(agent.agent.dir, file);
819
- if (!existsSync9(path)) continue;
820
- const content = readFileSync7(path, "utf8").trimEnd();
903
+ const path = join12(agent.agent.dir, file);
904
+ if (!existsSync10(path)) continue;
905
+ const content = readFileSync8(path, "utf8").trimEnd();
821
906
  if (!content) continue;
822
907
  contextBlocks.push(`## ${file}
823
908
 
@@ -828,9 +913,9 @@ ${content}`);
828
913
 
829
914
  ${contextBlocks.join("\n\n")}`);
830
915
  }
831
- const bootstrapPath = join10(agent.agent.dir, "BOOTSTRAP.md");
832
- if (existsSync9(bootstrapPath)) {
833
- const bootstrap = readFileSync7(bootstrapPath, "utf8").trimEnd();
916
+ const bootstrapPath = join12(agent.agent.dir, "BOOTSTRAP.md");
917
+ if (existsSync10(bootstrapPath)) {
918
+ const bootstrap = readFileSync8(bootstrapPath, "utf8").trimEnd();
834
919
  if (bootstrap) {
835
920
  parts.push(
836
921
  [
@@ -902,8 +987,8 @@ This group's USER.md is empty. As you learn STABLE facts about the human (prefer
902
987
  import { Type as Type2 } from "typebox";
903
988
 
904
989
  // ../daemon/src/runtime/tools/bootstrap.ts
905
- import { existsSync as existsSync10, rmSync as rmSync7 } from "fs";
906
- import { join as join11 } from "path";
990
+ import { existsSync as existsSync11, rmSync as rmSync7 } from "fs";
991
+ import { join as join13 } from "path";
907
992
  function bootstrapTool(agentDir) {
908
993
  return {
909
994
  def: {
@@ -912,8 +997,8 @@ function bootstrapTool(agentDir) {
912
997
  parameters: { type: "object", properties: {} }
913
998
  },
914
999
  async invoke() {
915
- const path = join11(agentDir, "BOOTSTRAP.md");
916
- if (existsSync10(path)) {
1000
+ const path = join13(agentDir, "BOOTSTRAP.md");
1001
+ if (existsSync11(path)) {
917
1002
  rmSync7(path);
918
1003
  return "BOOTSTRAP.md removed. Bootstrap is complete.";
919
1004
  }
@@ -922,9 +1007,196 @@ function bootstrapTool(agentDir) {
922
1007
  };
923
1008
  }
924
1009
 
1010
+ // ../daemon/src/runtime/tools/browser.ts
1011
+ var REF_DESC = 'Element ref from the latest browser_snapshot, e.g. "e5".';
1012
+ function browserTools(host, agentId) {
1013
+ const proxy = (name, action, description, properties, required = []) => ({
1014
+ def: {
1015
+ name,
1016
+ description,
1017
+ parameters: { type: "object", properties, required, additionalProperties: false }
1018
+ },
1019
+ invoke: (args) => host.invoke(agentId, action, args)
1020
+ });
1021
+ return [
1022
+ proxy(
1023
+ "browser_navigate",
1024
+ "navigate",
1025
+ "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).",
1026
+ { url: { type: "string", description: "Absolute URL to navigate to." } },
1027
+ ["url"]
1028
+ ),
1029
+ proxy(
1030
+ "browser_snapshot",
1031
+ "snapshot",
1032
+ '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.',
1033
+ {}
1034
+ ),
1035
+ proxy(
1036
+ "browser_click",
1037
+ "click",
1038
+ "Click an element identified by its snapshot ref. Returns a fresh snapshot.",
1039
+ { ref: { type: "string", description: REF_DESC } },
1040
+ ["ref"]
1041
+ ),
1042
+ proxy(
1043
+ "browser_type",
1044
+ "type",
1045
+ "Type text into an input/textarea identified by its ref. Set submit=true to press Enter afterwards. Returns a fresh snapshot.",
1046
+ {
1047
+ ref: { type: "string", description: REF_DESC },
1048
+ text: { type: "string", description: "Text to type into the field." },
1049
+ submit: { type: "boolean", description: "Press Enter after typing." }
1050
+ },
1051
+ ["ref", "text"]
1052
+ ),
1053
+ proxy(
1054
+ "browser_hover",
1055
+ "hover",
1056
+ "Hover the pointer over an element identified by its ref. Returns a fresh snapshot.",
1057
+ { ref: { type: "string", description: REF_DESC } },
1058
+ ["ref"]
1059
+ ),
1060
+ proxy(
1061
+ "browser_select",
1062
+ "select",
1063
+ "Select option(s) in a <select> element identified by its ref.",
1064
+ {
1065
+ ref: { type: "string", description: REF_DESC },
1066
+ values: {
1067
+ type: "array",
1068
+ items: { type: "string" },
1069
+ description: "Option values to select."
1070
+ }
1071
+ },
1072
+ ["ref", "values"]
1073
+ ),
1074
+ proxy(
1075
+ "browser_fill_form",
1076
+ "fill_form",
1077
+ "Fill multiple form fields in one call. Each field is { ref, value }.",
1078
+ {
1079
+ fields: {
1080
+ type: "array",
1081
+ description: "Fields to fill.",
1082
+ items: {
1083
+ type: "object",
1084
+ properties: {
1085
+ ref: { type: "string", description: REF_DESC },
1086
+ value: { type: "string" }
1087
+ },
1088
+ required: ["ref", "value"]
1089
+ }
1090
+ }
1091
+ },
1092
+ ["fields"]
1093
+ ),
1094
+ proxy(
1095
+ "browser_press_key",
1096
+ "press_key",
1097
+ 'Press a keyboard key (e.g. "Enter", "Escape", "ArrowDown", "Control+A").',
1098
+ { key: { type: "string", description: "Key or chord to press." } },
1099
+ ["key"]
1100
+ ),
1101
+ proxy(
1102
+ "browser_go_back",
1103
+ "go_back",
1104
+ "Navigate back to the previous page in history. Returns a fresh snapshot.",
1105
+ {}
1106
+ ),
1107
+ proxy(
1108
+ "browser_tabs",
1109
+ "tabs",
1110
+ '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).',
1111
+ {
1112
+ op: { type: "string", enum: ["list", "new", "select", "close"] },
1113
+ index: { type: "number", description: "Tab index for select/close." },
1114
+ url: { type: "string", description: "URL to open for op=new." }
1115
+ },
1116
+ ["op"]
1117
+ ),
1118
+ proxy(
1119
+ "browser_take_screenshot",
1120
+ "take_screenshot",
1121
+ "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.",
1122
+ { full_page: { type: "boolean", description: "Capture the full scrollable page." } }
1123
+ ),
1124
+ proxy(
1125
+ "browser_console",
1126
+ "console",
1127
+ "Return recent browser console messages (logs, warnings, errors).",
1128
+ {}
1129
+ ),
1130
+ proxy(
1131
+ "browser_network",
1132
+ "network",
1133
+ "Return recent network requests issued by the page (method, status, URL).",
1134
+ {}
1135
+ )
1136
+ ];
1137
+ }
1138
+
1139
+ // ../daemon/src/runtime/tools/deliver-file.ts
1140
+ import { readFileSync as readFileSync9, statSync as statSync4 } from "fs";
1141
+ import { basename as basename2, extname, isAbsolute, resolve as resolve3 } from "path";
1142
+ var MAX_DELIVER_BYTES = 25 * 1024 * 1024;
1143
+ var MIME = {
1144
+ ".pdf": "application/pdf",
1145
+ ".txt": "text/plain",
1146
+ ".md": "text/markdown",
1147
+ ".csv": "text/csv",
1148
+ ".json": "application/json",
1149
+ ".html": "text/html",
1150
+ ".zip": "application/zip",
1151
+ ".png": "image/png",
1152
+ ".jpg": "image/jpeg",
1153
+ ".jpeg": "image/jpeg",
1154
+ ".gif": "image/gif",
1155
+ ".webp": "image/webp"
1156
+ };
1157
+ function deliverFileTool(cwd, sink) {
1158
+ return {
1159
+ def: {
1160
+ name: "deliver_file",
1161
+ 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.",
1162
+ parameters: {
1163
+ type: "object",
1164
+ properties: {
1165
+ path: {
1166
+ type: "string",
1167
+ description: "Path to the file, relative to your workspace or absolute."
1168
+ }
1169
+ },
1170
+ required: ["path"],
1171
+ additionalProperties: false
1172
+ }
1173
+ },
1174
+ async invoke(args) {
1175
+ const p = String(args.path ?? "");
1176
+ if (!p) throw new Error("deliver_file: path is required");
1177
+ const abs = isAbsolute(p) ? p : resolve3(cwd, p);
1178
+ let size;
1179
+ try {
1180
+ size = statSync4(abs).size;
1181
+ } catch {
1182
+ throw new Error(`deliver_file: no such file: ${p}`);
1183
+ }
1184
+ if (size > MAX_DELIVER_BYTES) {
1185
+ throw new Error(
1186
+ `deliver_file: "${basename2(abs)}" is too large (${(size / 1024 / 1024).toFixed(1)} MB > 25 MB)`
1187
+ );
1188
+ }
1189
+ const name = basename2(abs);
1190
+ const mimeType = MIME[extname(abs).toLowerCase()] ?? "application/octet-stream";
1191
+ sink({ name, mimeType, data: readFileSync9(abs).toString("base64") });
1192
+ return `Delivered "${name}" (${mimeType}) to the user.`;
1193
+ }
1194
+ };
1195
+ }
1196
+
925
1197
  // ../daemon/src/runtime/tools/home.ts
926
- import { readdirSync as readdirSync6, readFileSync as readFileSync8, statSync as statSync4, writeFileSync as writeFileSync5 } from "fs";
927
- import { join as join12 } from "path";
1198
+ import { readdirSync as readdirSync6, readFileSync as readFileSync10, statSync as statSync5, writeFileSync as writeFileSync6 } from "fs";
1199
+ import { join as join14 } from "path";
928
1200
  var HOME_FILES_READABLE = [
929
1201
  "IDENTITY.md",
930
1202
  "SOUL.md",
@@ -961,9 +1233,9 @@ function homeTools(agentDir) {
961
1233
  `home_read: "file" must be one of ${HOME_FILES_READABLE.join(", ")}; got "${file}"`
962
1234
  );
963
1235
  }
964
- const path = join12(agentDir, file);
1236
+ const path = join14(agentDir, file);
965
1237
  try {
966
- return readFileSync8(path, "utf8");
1238
+ return readFileSync10(path, "utf8");
967
1239
  } catch (err) {
968
1240
  const msg = err instanceof Error ? err.message : String(err);
969
1241
  throw new Error(`home_read: could not read ${file}: ${msg}`);
@@ -991,8 +1263,8 @@ function homeTools(agentDir) {
991
1263
  );
992
1264
  }
993
1265
  const content = typeof args.content === "string" ? args.content : "";
994
- const path = join12(agentDir, file);
995
- writeFileSync5(path, content, "utf8");
1266
+ const path = join14(agentDir, file);
1267
+ writeFileSync6(path, content, "utf8");
996
1268
  return `wrote ${file} (${Buffer.byteLength(content, "utf8")} bytes)`;
997
1269
  }
998
1270
  },
@@ -1005,9 +1277,9 @@ function homeTools(agentDir) {
1005
1277
  async invoke() {
1006
1278
  const entries = [];
1007
1279
  for (const file of HOME_FILES_READABLE) {
1008
- const path = join12(agentDir, file);
1280
+ const path = join14(agentDir, file);
1009
1281
  try {
1010
- const s = statSync4(path);
1282
+ const s = statSync5(path);
1011
1283
  entries.push(`${file} (${s.size}b)`);
1012
1284
  } catch {
1013
1285
  }
@@ -1028,6 +1300,18 @@ function homeTools(agentDir) {
1028
1300
  ];
1029
1301
  }
1030
1302
 
1303
+ // ../daemon/src/runtime/tools/mcp.ts
1304
+ function mcpProxyTools(host, tools) {
1305
+ return tools.map((t) => ({
1306
+ def: {
1307
+ name: t.toolName,
1308
+ description: t.description,
1309
+ parameters: t.inputSchema
1310
+ },
1311
+ invoke: (args) => host.invoke(t.serverId, t.rawName, args)
1312
+ }));
1313
+ }
1314
+
1031
1315
  // ../daemon/src/runtime/tools/memory.ts
1032
1316
  function memoryTools(memory) {
1033
1317
  return [
@@ -1853,14 +2137,20 @@ function ourToolToPiTool(h) {
1853
2137
  description: h.def.description,
1854
2138
  parameters: Type2.Unsafe(h.def.parameters),
1855
2139
  async execute(_toolCallId, params) {
1856
- const text = await h.invoke(params);
2140
+ const out = await h.invoke(params);
1857
2141
  return {
1858
- content: [{ type: "text", text }],
2142
+ content: toPiContent(out),
1859
2143
  details: {}
1860
2144
  };
1861
2145
  }
1862
2146
  };
1863
2147
  }
2148
+ function toPiContent(out) {
2149
+ if (typeof out === "string") return [{ type: "text", text: out }];
2150
+ return out.map(
2151
+ (p) => p.type === "text" ? { type: "text", text: p.text } : { type: "image", data: p.data, mimeType: p.mimeType }
2152
+ );
2153
+ }
1864
2154
  function createBazilionCustomTools(opts) {
1865
2155
  const handlers = [
1866
2156
  ...memoryTools(opts.memory),
@@ -1874,13 +2164,35 @@ function createBazilionCustomTools(opts) {
1874
2164
  if (opts.userMdHost) {
1875
2165
  handlers.push(...userMdTools(opts.userMdHost, opts.agent.group.id));
1876
2166
  }
2167
+ if (opts.browserHost) {
2168
+ handlers.push(...browserTools(opts.browserHost, opts.agent.agent.id));
2169
+ }
2170
+ if (opts.mcpHost && opts.mcpTools && opts.mcpTools.length > 0) {
2171
+ handlers.push(...mcpProxyTools(opts.mcpHost, opts.mcpTools));
2172
+ }
2173
+ if (opts.fileSink) {
2174
+ handlers.push(deliverFileTool(opts.agent.group.path, opts.fileSink));
2175
+ }
1877
2176
  return handlers.map(ourToolToPiTool);
1878
2177
  }
1879
2178
 
1880
2179
  // ../daemon/src/runtime/pi/session.ts
1881
2180
  var BUILTIN_TOOL_NAMES = ["read", "bash", "edit", "write", "grep", "find", "ls"];
1882
2181
  async function createBazilionSession(opts) {
1883
- const { agent, paths, env, memory, enabledProviders, messagingHost, userMdHost, refreshApiKey } = opts;
2182
+ const {
2183
+ agent,
2184
+ paths,
2185
+ env,
2186
+ memory,
2187
+ enabledProviders,
2188
+ messagingHost,
2189
+ userMdHost,
2190
+ browserHost,
2191
+ mcpHost,
2192
+ mcpTools,
2193
+ fileSink,
2194
+ refreshApiKey
2195
+ } = opts;
1884
2196
  const { providerName, modelId } = splitModelString(agent.model);
1885
2197
  if (enabledProviders.size > 0 && !enabledProviders.has(providerName)) {
1886
2198
  throw new Error(`${providerName} provider is disabled \u2014 enable it on the /config page`);
@@ -1910,8 +2222,8 @@ async function createBazilionSession(opts) {
1910
2222
  });
1911
2223
  }
1912
2224
  const cwd = agent.group.path;
1913
- if (!existsSync11(cwd)) mkdirSync5(cwd, { recursive: true });
1914
- const sessionDir = join13(paths.agentDir(agent.agent.id), "sessions");
2225
+ if (!existsSync12(cwd)) mkdirSync5(cwd, { recursive: true });
2226
+ const sessionDir = join15(paths.agentDir(agent.agent.id), "sessions");
1915
2227
  mkdirSync5(sessionDir, { recursive: true });
1916
2228
  const existing = findMostRecent(sessionDir);
1917
2229
  const sessionManager = existing ? SessionManager.open(existing, sessionDir, cwd) : SessionManager.create(cwd, sessionDir);
@@ -1927,11 +2239,21 @@ async function createBazilionSession(opts) {
1927
2239
  const bazilionPrompt = buildSystemPrompt(agent);
1928
2240
  const resourceLoader = createBazilionResourceLoader(bazilionPrompt);
1929
2241
  await resourceLoader.reload();
1930
- const customTools = createBazilionCustomTools({ agent, memory, messagingHost, userMdHost, env });
2242
+ const customTools = createBazilionCustomTools({
2243
+ agent,
2244
+ memory,
2245
+ messagingHost,
2246
+ userMdHost,
2247
+ browserHost,
2248
+ mcpHost,
2249
+ mcpTools,
2250
+ fileSink,
2251
+ env
2252
+ });
1931
2253
  const allowedTools = [...BUILTIN_TOOL_NAMES, ...customTools.map((t) => t.name)];
1932
2254
  const { session } = await createAgentSession({
1933
2255
  cwd,
1934
- agentDir: join13(paths.home, "pi"),
2256
+ agentDir: join15(paths.home, "pi"),
1935
2257
  model,
1936
2258
  thinkingLevel: toPiThinkingLevel(agent.reasoningLevel),
1937
2259
  tools: allowedTools,
@@ -2057,13 +2379,13 @@ function createBazilionResourceLoader(appendSystemPrompt) {
2057
2379
  };
2058
2380
  }
2059
2381
  function findMostRecent(sessionDir) {
2060
- if (!existsSync11(sessionDir)) return null;
2382
+ if (!existsSync12(sessionDir)) return null;
2061
2383
  let newest = null;
2062
2384
  for (const entry of readdirSync7(sessionDir)) {
2063
2385
  if (!entry.endsWith(".jsonl")) continue;
2064
- const path = join13(sessionDir, entry);
2386
+ const path = join15(sessionDir, entry);
2065
2387
  try {
2066
- const s = statSync5(path);
2388
+ const s = statSync6(path);
2067
2389
  if (!newest || s.mtimeMs > newest.mtimeMs) newest = { path, mtimeMs: s.mtimeMs };
2068
2390
  } catch {
2069
2391
  }
@@ -2105,10 +2427,10 @@ function createIpcClient() {
2105
2427
  if (!process.send) {
2106
2428
  return Promise.reject(new Error("worker: no IPC channel \u2014 daemon must spawn with stdio:ipc"));
2107
2429
  }
2108
- const id = randomUUID5();
2430
+ const id = randomUUID6();
2109
2431
  const message = { type: "rpc", id, method, args };
2110
- return new Promise((resolve3, reject) => {
2111
- pending.set(id, { resolve: (v) => resolve3(v), reject });
2432
+ return new Promise((resolve4, reject) => {
2433
+ pending.set(id, { resolve: (v) => resolve4(v), reject });
2112
2434
  process.send?.(message, void 0, void 0, (err) => {
2113
2435
  if (err) {
2114
2436
  pending.delete(id);
@@ -2135,6 +2457,16 @@ function createIpcUserMdHost(call) {
2135
2457
  write: (groupId, content, ifMatch) => call("userMdWrite", { groupId, content, ifMatch })
2136
2458
  };
2137
2459
  }
2460
+ function createIpcBrowserHost(call) {
2461
+ return {
2462
+ invoke: (agentId, action, args) => call("browserInvoke", { agentId, action, args })
2463
+ };
2464
+ }
2465
+ function createIpcMcpHost(call) {
2466
+ return {
2467
+ invoke: (serverId, toolName, args) => call("mcpInvoke", { serverId, toolName, args })
2468
+ };
2469
+ }
2138
2470
  function isIpcReply(msg) {
2139
2471
  if (!msg || typeof msg !== "object") return false;
2140
2472
  const m = msg;
@@ -2157,13 +2489,15 @@ async function main() {
2157
2489
  };
2158
2490
  process.on("SIGTERM", onSignal);
2159
2491
  process.on("SIGINT", onSignal);
2160
- const { agent, message, enabledProviders, apiKey } = await readInput();
2492
+ const { agent, message, enabledProviders, apiKey, browserEnabled, mcpTools, images } = await readInput();
2161
2493
  const paths = resolvePaths();
2162
- const memory = qmdBackend(join14(agent.group.path, "memory"));
2494
+ const memory = qmdBackend(join16(agent.group.path, "memory"));
2163
2495
  await memory.init();
2164
2496
  const ipcCall = createIpcClient();
2165
2497
  const messagingHost = createIpcMessagingHost(ipcCall);
2166
2498
  const userMdHost = createIpcUserMdHost(ipcCall);
2499
+ const browserHost = browserEnabled ? createIpcBrowserHost(ipcCall) : void 0;
2500
+ const mcpHost = mcpTools && mcpTools.length > 0 ? createIpcMcpHost(ipcCall) : void 0;
2167
2501
  const { session, dispose } = await createBazilionSession({
2168
2502
  agent,
2169
2503
  paths,
@@ -2172,7 +2506,12 @@ async function main() {
2172
2506
  enabledProviders: new Set(enabledProviders),
2173
2507
  messagingHost,
2174
2508
  userMdHost,
2175
- apiKey
2509
+ apiKey,
2510
+ browserHost,
2511
+ mcpHost,
2512
+ mcpTools,
2513
+ // deliver_file emits a `file` event straight onto our stdout frame stream.
2514
+ fileSink: (f) => emit({ kind: "event", event: { type: "file", ...f } })
2176
2515
  });
2177
2516
  abortSession = () => {
2178
2517
  void session.abort();
@@ -2183,7 +2522,12 @@ async function main() {
2183
2522
  }
2184
2523
  });
2185
2524
  try {
2186
- await session.prompt(message);
2525
+ const promptImages = (images ?? []).map((img) => ({
2526
+ type: "image",
2527
+ data: img.data,
2528
+ mimeType: img.mimeType
2529
+ }));
2530
+ await session.prompt(message, promptImages.length > 0 ? { images: promptImages } : void 0);
2187
2531
  await session.agent.waitForIdle();
2188
2532
  emit({
2189
2533
  kind: "done",