bazilion 0.1.0 → 0.2.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
@@ -76,9 +76,34 @@ import { existsSync as existsSync5, rmSync as rmSync2 } from "fs";
76
76
  import { writeFileSync as writeFileSync3 } from "fs";
77
77
  import { join as join7 } from "path";
78
78
 
79
+ // ../daemon/src/core/profile-group/spawn.ts
80
+ import { readdirSync as readdirSync3, rmSync as rmSync4 } from "fs";
81
+
82
+ // ../daemon/src/core/profile-group/rm-with-retry.ts
83
+ import { rmSync as rmSync3 } from "fs";
84
+
79
85
  // ../daemon/src/core/services.ts
80
86
  var SERVICES = [
81
87
  // --- LLM providers (configured via API keys / URLs) ---
88
+ // Top 3: openai-codex (ChatGPT OAuth), openai (API key), anthropic.
89
+ // Everything else in rough popularity order; locals last.
90
+ {
91
+ id: "openai-codex",
92
+ displayName: "OpenAI ChatGPT (OAuth)",
93
+ category: "provider",
94
+ hint: "Use your ChatGPT Plus/Pro/Team account (same login as Codex CLI)",
95
+ // No form fields — credentials come from an OAuth flow. The /config page
96
+ // renders a Connect/Disconnect card using /api/auth/openai instead of the
97
+ // standard field inputs.
98
+ fields: []
99
+ },
100
+ {
101
+ id: "openai",
102
+ displayName: "OpenAI",
103
+ category: "provider",
104
+ hint: "GPT models \xB7 platform.openai.com",
105
+ fields: [{ envVar: "OPENAI_API_KEY", kind: "secret", label: "API key", placeholder: "sk-..." }]
106
+ },
82
107
  {
83
108
  id: "anthropic",
84
109
  displayName: "Anthropic",
@@ -94,23 +119,6 @@ var SERVICES = [
94
119
  }
95
120
  ]
96
121
  },
97
- {
98
- id: "openai",
99
- displayName: "OpenAI",
100
- category: "provider",
101
- hint: "GPT models \xB7 platform.openai.com",
102
- fields: [{ envVar: "OPENAI_API_KEY", kind: "secret", label: "API key", placeholder: "sk-..." }]
103
- },
104
- {
105
- id: "openai-codex",
106
- displayName: "OpenAI ChatGPT (OAuth)",
107
- category: "provider",
108
- hint: "Use your ChatGPT Plus/Pro/Team account (same login as Codex CLI)",
109
- // No form fields — credentials come from an OAuth flow. The /config page
110
- // renders a Connect/Disconnect card using /api/auth/openai instead of the
111
- // standard field inputs.
112
- fields: []
113
- },
114
122
  {
115
123
  id: "google",
116
124
  displayName: "Google (Gemini)",
@@ -153,6 +161,27 @@ var SERVICES = [
153
161
  hint: "Authenticates via AWS SDK env (AWS_PROFILE or AWS_ACCESS_KEY_ID/SECRET)",
154
162
  fields: []
155
163
  },
164
+ {
165
+ id: "github-copilot",
166
+ displayName: "GitHub Copilot",
167
+ category: "provider",
168
+ hint: "Use a GitHub Copilot subscription to call Claude/GPT/Gemini via Copilot",
169
+ fields: [
170
+ {
171
+ envVar: "COPILOT_GITHUB_TOKEN",
172
+ kind: "secret",
173
+ label: "GitHub token",
174
+ description: "Generic GH_TOKEN/GITHUB_TOKEN are ignored \u2014 set this scoped variable explicitly (or run `bazilion auth copilot login` once available)."
175
+ }
176
+ ]
177
+ },
178
+ {
179
+ id: "deepseek",
180
+ displayName: "DeepSeek",
181
+ category: "provider",
182
+ hint: "DeepSeek V4 Flash / Pro \xB7 platform.deepseek.com",
183
+ fields: [{ envVar: "DEEPSEEK_API_KEY", kind: "secret", label: "API key" }]
184
+ },
156
185
  {
157
186
  id: "mistral",
158
187
  displayName: "Mistral",
@@ -160,6 +189,13 @@ var SERVICES = [
160
189
  hint: "mistral.ai",
161
190
  fields: [{ envVar: "MISTRAL_API_KEY", kind: "secret", label: "API key" }]
162
191
  },
192
+ {
193
+ id: "xai",
194
+ displayName: "xAI",
195
+ category: "provider",
196
+ hint: "Grok \xB7 x.ai",
197
+ fields: [{ envVar: "XAI_API_KEY", kind: "secret", label: "API key" }]
198
+ },
163
199
  {
164
200
  id: "groq",
165
201
  displayName: "Groq",
@@ -174,11 +210,46 @@ var SERVICES = [
174
210
  fields: [{ envVar: "CEREBRAS_API_KEY", kind: "secret", label: "API key" }]
175
211
  },
176
212
  {
177
- id: "xai",
178
- displayName: "xAI",
213
+ id: "fireworks",
214
+ displayName: "Fireworks AI",
179
215
  category: "provider",
180
- hint: "Grok \xB7 x.ai",
181
- fields: [{ envVar: "XAI_API_KEY", kind: "secret", label: "API key" }]
216
+ hint: "DeepSeek/GLM/Kimi via fireworks.ai",
217
+ fields: [{ envVar: "FIREWORKS_API_KEY", kind: "secret", label: "API key" }]
218
+ },
219
+ {
220
+ id: "together",
221
+ displayName: "Together AI",
222
+ category: "provider",
223
+ hint: "Open-weight models \xB7 together.ai",
224
+ fields: [{ envVar: "TOGETHER_API_KEY", kind: "secret", label: "API key" }]
225
+ },
226
+ {
227
+ id: "moonshotai",
228
+ displayName: "Moonshot AI",
229
+ category: "provider",
230
+ hint: "Kimi K2/K2.5/K2.6 \xB7 platform.moonshot.ai",
231
+ fields: [{ envVar: "MOONSHOT_API_KEY", kind: "secret", label: "API key" }]
232
+ },
233
+ {
234
+ id: "kimi-coding",
235
+ displayName: "Kimi Coding",
236
+ category: "provider",
237
+ hint: "Coding-tuned Kimi endpoint \xB7 platform.moonshot.cn",
238
+ fields: [{ envVar: "KIMI_API_KEY", kind: "secret", label: "API key" }]
239
+ },
240
+ {
241
+ id: "minimax",
242
+ displayName: "MiniMax",
243
+ category: "provider",
244
+ hint: "MiniMax M2 family \xB7 platform.minimaxi.com",
245
+ fields: [{ envVar: "MINIMAX_API_KEY", kind: "secret", label: "API key" }]
246
+ },
247
+ {
248
+ id: "xiaomi",
249
+ displayName: "Xiaomi MiMo",
250
+ category: "provider",
251
+ hint: "API billing endpoint \xB7 platform.xiaomimimo.com",
252
+ fields: [{ envVar: "XIAOMI_API_KEY", kind: "secret", label: "API key" }]
182
253
  },
183
254
  {
184
255
  id: "zai",
@@ -193,6 +264,27 @@ var SERVICES = [
193
264
  hint: "Inference endpoints \xB7 huggingface.co",
194
265
  fields: [{ envVar: "HF_TOKEN", kind: "secret", label: "Access token", placeholder: "hf_..." }]
195
266
  },
267
+ {
268
+ id: "cloudflare-ai-gateway",
269
+ displayName: "Cloudflare AI Gateway",
270
+ category: "provider",
271
+ hint: "Per-gateway routing to OpenAI/Anthropic/Workers AI",
272
+ fields: [
273
+ { envVar: "CLOUDFLARE_API_KEY", kind: "secret", label: "API key" },
274
+ { envVar: "CLOUDFLARE_ACCOUNT_ID", kind: "config", label: "Account ID" },
275
+ { envVar: "CLOUDFLARE_GATEWAY_ID", kind: "config", label: "Gateway ID" }
276
+ ]
277
+ },
278
+ {
279
+ id: "cloudflare-workers-ai",
280
+ displayName: "Cloudflare Workers AI",
281
+ category: "provider",
282
+ hint: "Inference on Cloudflare Workers \xB7 ai.cloudflare.com",
283
+ fields: [
284
+ { envVar: "CLOUDFLARE_API_KEY", kind: "secret", label: "API key" },
285
+ { envVar: "CLOUDFLARE_ACCOUNT_ID", kind: "config", label: "Account ID" }
286
+ ]
287
+ },
196
288
  {
197
289
  id: "openrouter",
198
290
  displayName: "OpenRouter",
@@ -216,6 +308,13 @@ var SERVICES = [
216
308
  }
217
309
  ]
218
310
  },
311
+ {
312
+ id: "opencode",
313
+ displayName: "OpenCode",
314
+ category: "provider",
315
+ hint: "OpenAI-compatible proxy from the OpenCode CLI",
316
+ fields: [{ envVar: "OPENCODE_API_KEY", kind: "secret", label: "API key" }]
317
+ },
219
318
  {
220
319
  id: "lmstudio",
221
320
  displayName: "LM Studio",
@@ -254,11 +353,54 @@ var SERVICES = [
254
353
  }
255
354
  ]
256
355
  },
356
+ {
357
+ id: "llamacpp",
358
+ displayName: "llama.cpp",
359
+ category: "provider",
360
+ hint: "Local inference \xB7 llama.cpp llama-server (OpenAI-compat /v1 endpoint)",
361
+ fields: [
362
+ {
363
+ envVar: "LLAMACPP_URL",
364
+ kind: "config",
365
+ label: "Endpoint URL",
366
+ placeholder: "http://127.0.0.1:8080/v1"
367
+ },
368
+ {
369
+ envVar: "LLAMACPP_API_KEY",
370
+ kind: "secret",
371
+ label: "API key (only if started with --api-key)",
372
+ description: "llama-server runs without auth by default. Set this only if you launched the server with the `--api-key KEY` flag."
373
+ }
374
+ ]
375
+ },
257
376
  // --- Ancillary services (web search, etc) ---
377
+ {
378
+ id: "firecrawl",
379
+ displayName: "Firecrawl",
380
+ category: "service",
381
+ group: "Web Search",
382
+ hint: "web_fetch fallback for JS-heavy/blocked pages \xB7 firecrawl.dev (free tier available)",
383
+ fields: [
384
+ {
385
+ envVar: "FIRECRAWL_API_KEY",
386
+ kind: "secret",
387
+ label: "API key",
388
+ placeholder: "fc-...",
389
+ description: "When set, web_fetch automatically falls back to Firecrawl if the primary Readability extraction yields too little content."
390
+ },
391
+ {
392
+ envVar: "FIRECRAWL_URL",
393
+ kind: "config",
394
+ label: "Base URL (optional, for self-hosted)",
395
+ placeholder: "https://api.firecrawl.dev"
396
+ }
397
+ ]
398
+ },
258
399
  {
259
400
  id: "brave-search",
260
401
  displayName: "Brave Search",
261
402
  category: "service",
403
+ group: "Web Search",
262
404
  hint: "Web search tool \xB7 free tier at brave.com/search/api/",
263
405
  fields: [{ envVar: "BRAVE_API_KEY", kind: "secret", label: "API key", placeholder: "BSA..." }]
264
406
  },
@@ -266,6 +408,7 @@ var SERVICES = [
266
408
  id: "searxng",
267
409
  displayName: "SearXNG",
268
410
  category: "service",
411
+ group: "Web Search",
269
412
  hint: "Self-hosted meta-search engine \xB7 searxng.org",
270
413
  fields: [
271
414
  {
@@ -309,7 +452,7 @@ import { createHash, randomBytes as randomBytes2, randomUUID as randomUUID4 } fr
309
452
  import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
310
453
 
311
454
  // ../daemon/src/core/skills/import.ts
312
- import { cpSync, existsSync as existsSync7, mkdtempSync, readdirSync as readdirSync3, rmSync as rmSync3, statSync as statSync2 } from "fs";
455
+ import { cpSync, existsSync as existsSync7, mkdtempSync, readdirSync as readdirSync4, rmSync as rmSync5, statSync as statSync2 } from "fs";
313
456
  import { tmpdir } from "os";
314
457
  import { basename, join as join8, resolve as resolve2, sep } from "path";
315
458
  import AdmZip from "adm-zip";
@@ -322,9 +465,9 @@ import { parse as parseYaml } from "yaml";
322
465
  import {
323
466
  existsSync as existsSync8,
324
467
  mkdirSync as mkdirSync4,
325
- readdirSync as readdirSync4,
468
+ readdirSync as readdirSync5,
326
469
  readFileSync as readFileSync6,
327
- rmSync as rmSync4,
470
+ rmSync as rmSync6,
328
471
  statSync as statSync3,
329
472
  writeFileSync as writeFileSync4
330
473
  } from "fs";
@@ -357,7 +500,7 @@ function safeKey(root, key) {
357
500
  }
358
501
  function walkMd(dir, prefix, out) {
359
502
  if (!existsSync8(dir)) return;
360
- for (const e of readdirSync4(dir, { withFileTypes: true })) {
503
+ for (const e of readdirSync5(dir, { withFileTypes: true })) {
361
504
  if (e.name.startsWith(".")) continue;
362
505
  const full = join9(dir, e.name);
363
506
  const key = prefix ? `${prefix}/${e.name}` : e.name;
@@ -432,7 +575,7 @@ function qmdBackend(root) {
432
575
  },
433
576
  async remove(key) {
434
577
  const path = safeKey(root, key);
435
- if (existsSync8(path)) rmSync4(path);
578
+ if (existsSync8(path)) rmSync6(path);
436
579
  const store = await getStore(root);
437
580
  await store.update();
438
581
  }
@@ -558,7 +701,7 @@ function piMessagesToProviderView(messages) {
558
701
  }
559
702
 
560
703
  // ../daemon/src/runtime/pi/session.ts
561
- import { existsSync as existsSync11, mkdirSync as mkdirSync5, readdirSync as readdirSync6, statSync as statSync5 } from "fs";
704
+ import { existsSync as existsSync11, mkdirSync as mkdirSync5, readdirSync as readdirSync7, statSync as statSync5 } from "fs";
562
705
  import { basename as basename2, join as join13 } from "path";
563
706
  import {
564
707
  AuthStorage,
@@ -567,14 +710,14 @@ import {
567
710
  ModelRegistry,
568
711
  SessionManager,
569
712
  SettingsManager
570
- } from "@mariozechner/pi-coding-agent";
713
+ } from "@earendil-works/pi-coding-agent";
571
714
 
572
715
  // ../daemon/src/runtime/providers/pi-adapter.ts
573
716
  import {
574
717
  getModel,
575
718
  streamSimple,
576
719
  Type
577
- } from "@mariozechner/pi-ai";
720
+ } from "@earendil-works/pi-ai";
578
721
  function defaultBaseUrlFor(providerName) {
579
722
  switch (providerName) {
580
723
  case "lmstudio":
@@ -616,7 +759,7 @@ function resolveModel(cfg, modelId) {
616
759
  }
617
760
 
618
761
  // ../daemon/src/runtime/auth/openai-codex.ts
619
- import { loginOpenAICodex, refreshOpenAICodexToken } from "@mariozechner/pi-ai/oauth";
762
+ import { loginOpenAICodex, refreshOpenAICodexToken } from "@earendil-works/pi-ai/oauth";
620
763
 
621
764
  // ../daemon/src/runtime/session/prompt.ts
622
765
  import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
@@ -626,13 +769,11 @@ var CONTEXT_FILE_ORDER = [
626
769
  "SOUL.md",
627
770
  "TOOLS.md",
628
771
  "IDENTITY.md",
629
- "HEARTBEAT.md",
630
- "BOOTSTRAP.md"
772
+ "HEARTBEAT.md"
631
773
  ];
632
774
  function buildSystemPrompt(agent) {
633
775
  const parts = [];
634
776
  const contextBlocks = [];
635
- let bootstrapPresent = false;
636
777
  for (const file of CONTEXT_FILE_ORDER) {
637
778
  const path = join10(agent.agent.dir, file);
638
779
  if (!existsSync9(path)) continue;
@@ -641,23 +782,39 @@ function buildSystemPrompt(agent) {
641
782
  contextBlocks.push(`## ${file}
642
783
 
643
784
  ${content}`);
644
- if (file === "BOOTSTRAP.md") bootstrapPresent = true;
645
785
  }
646
786
  if (contextBlocks.length > 0) {
647
787
  parts.push(`# Project Context
648
788
 
649
789
  ${contextBlocks.join("\n\n")}`);
650
790
  }
651
- if (bootstrapPresent) {
652
- parts.push(
653
- "NOTE: This is your first session. After you have completed the bootstrap conversation, call the `bootstrap_done` tool to delete BOOTSTRAP.md."
654
- );
791
+ const bootstrapPath = join10(agent.agent.dir, "BOOTSTRAP.md");
792
+ if (existsSync9(bootstrapPath)) {
793
+ const bootstrap = readFileSync7(bootstrapPath, "utf8").trimEnd();
794
+ if (bootstrap) {
795
+ parts.push(
796
+ [
797
+ "# First-Run Ritual",
798
+ "",
799
+ "This is your first session. The document below is **conversational guidance**, not a checklist to execute in one shot. It describes a multi-turn Q&A you should have with the human, one question per turn.",
800
+ "",
801
+ "## Hard rules",
802
+ "- Your first reply is ONLY a greeting + ONE question. No tool calls. Wait for the human to answer.",
803
+ "- Each subsequent turn: at most one new question. Wait between turns.",
804
+ "- Only after the ritual is complete (you have enough to write IDENTITY.md): call `home_write` once, then `bootstrap_done`.",
805
+ "",
806
+ "## BOOTSTRAP.md",
807
+ "",
808
+ bootstrap
809
+ ].join("\n")
810
+ );
811
+ }
655
812
  }
656
813
  parts.push(
657
814
  [
658
815
  "# Agent Home",
659
816
  "",
660
- "Your private home holds who you are \u2014 identity, soul, behaviour rules, wake-up routine. It is not shared with other agents and cannot be overwritten by them. The files above (IDENTITY.md, SOUL.md, AGENTS.md, TOOLS.md, HEARTBEAT.md, BOOTSTRAP.md) live in this home.",
817
+ "Your private home holds who you are \u2014 identity, soul, behaviour rules, wake-up routine. It is not shared with other agents and cannot be overwritten by them. The files above (IDENTITY.md, SOUL.md, AGENTS.md, TOOLS.md, HEARTBEAT.md) live in this home, plus BOOTSTRAP.md when you are still in your first-run ritual.",
661
818
  "",
662
819
  "- To change who you are (name, vibe, personality, how you behave): use `home_write`.",
663
820
  "- To inspect exact wording of your own files: use `home_read` or `home_list`.",
@@ -684,13 +841,19 @@ You have access to the following skills: ${agent.skills.join(", ")}.`
684
841
  parts.push(
685
842
  `# About the User
686
843
 
687
- Read-only context about the human you're working with in this group. You cannot edit this \u2014 if it's wrong, say so and they will update it.
844
+ Shared context about the human you're working with in this group. Both you and the human curate it. To update: call \`user_md_get\` (returns current content + an etag), merge your change into the full text, then call \`user_md_write\` with the merged content and the etag. Use this for STABLE user-specific facts (preferences, role, working hours, how they like to be addressed) \u2014 and to CORRECT stale entries when the human tells you something different from what's recorded. For project knowledge use \`memory_write\` instead; for personal notes about yourself use \`home_write\` on IDENTITY.md. **Do NOT send peer messages announcing USER.md changes \u2014 every agent in the group sees the new content in their system prompt on their next turn automatically.**
688
845
 
689
846
  ${agent.group.userMd.trim()}`
690
847
  );
848
+ } else {
849
+ parts.push(
850
+ `# About the User
851
+
852
+ This group's USER.md is empty. As you learn STABLE facts about the human (preferences, role, working hours, how they like to be addressed), populate it via \`user_md_get\` then \`user_md_write\` (always get first \u2014 you need the etag). Reserve this for things you're confident are durable \u2014 project knowledge belongs in \`memory_write\`, personal notes about yourself in \`home_write\` on IDENTITY.md. **Do NOT send peer messages announcing USER.md changes \u2014 every agent in the group sees the new content in their system prompt on their next turn automatically.**`
853
+ );
691
854
  }
692
855
  parts.push(
693
- "# Memory\n\nYou share a persistent memory backend with every other agent in this group. Use `memory_write` to remember things across sessions, and `memory_search` / `memory_read` / `memory_list` to recall them. This memory is for project knowledge \u2014 codebase notes, decisions, things the user told you about the work. For personal notes about yourself (preferences, persona quirks), use `home_write` on IDENTITY.md instead. Always check memory at the start of a session: another agent in the group may have already learned something useful."
856
+ "# Memory\n\nYou share a persistent memory backend with every other agent in this group. Use `memory_write` to remember things across sessions, and `memory_search` / `memory_read` / `memory_list` to recall them. This memory is for project knowledge \u2014 codebase notes, decisions, things the user told you about the work. For personal notes about yourself (preferences, persona quirks), use `home_write` on IDENTITY.md instead. Always check memory at the start of a session: another agent in the group may have already learned something useful. **Do NOT send peer messages announcing memory writes \u2014 every agent has access to the same store via `memory_search` and will find your note when they need it.**"
694
857
  );
695
858
  return parts.join("\n\n---\n\n");
696
859
  }
@@ -699,7 +862,7 @@ ${agent.group.userMd.trim()}`
699
862
  import { Type as Type2 } from "typebox";
700
863
 
701
864
  // ../daemon/src/runtime/tools/bootstrap.ts
702
- import { existsSync as existsSync10, rmSync as rmSync5 } from "fs";
865
+ import { existsSync as existsSync10, rmSync as rmSync7 } from "fs";
703
866
  import { join as join11 } from "path";
704
867
  function bootstrapTool(agentDir) {
705
868
  return {
@@ -711,7 +874,7 @@ function bootstrapTool(agentDir) {
711
874
  async invoke() {
712
875
  const path = join11(agentDir, "BOOTSTRAP.md");
713
876
  if (existsSync10(path)) {
714
- rmSync5(path);
877
+ rmSync7(path);
715
878
  return "BOOTSTRAP.md removed. Bootstrap is complete.";
716
879
  }
717
880
  return "BOOTSTRAP.md was already removed.";
@@ -720,7 +883,7 @@ function bootstrapTool(agentDir) {
720
883
  }
721
884
 
722
885
  // ../daemon/src/runtime/tools/home.ts
723
- import { readdirSync as readdirSync5, readFileSync as readFileSync8, statSync as statSync4, writeFileSync as writeFileSync5 } from "fs";
886
+ import { readdirSync as readdirSync6, readFileSync as readFileSync8, statSync as statSync4, writeFileSync as writeFileSync5 } from "fs";
724
887
  import { join as join12 } from "path";
725
888
  var HOME_FILES_READABLE = [
726
889
  "IDENTITY.md",
@@ -812,7 +975,7 @@ function homeTools(agentDir) {
812
975
  if (entries.length === 0) {
813
976
  const dirEntries = (() => {
814
977
  try {
815
- return readdirSync5(agentDir);
978
+ return readdirSync6(agentDir);
816
979
  } catch {
817
980
  return [];
818
981
  }
@@ -831,7 +994,7 @@ function memoryTools(memory) {
831
994
  {
832
995
  def: {
833
996
  name: "memory_write",
834
- description: 'Write or update a memory note in the GROUP-SHARED memory. All agents in this group can read what you write. Use it for project knowledge, codebase notes, decisions, and findings \u2014 anything other agents in the group should benefit from. For personal notes about yourself (preferences, persona), use `home_write` on IDENTITY.md instead. Key is a path-like string with a markdown extension, e.g. "auth-flow.md" or "people/alice.md".',
997
+ description: 'Write or update a memory note in the GROUP-SHARED memory. All agents in this group can read what you write. Use it for project knowledge, codebase notes, decisions, and findings \u2014 anything other agents in the group should benefit from. For personal notes about yourself (preferences, persona) use `home_write` on IDENTITY.md. For STABLE facts about the human you\'re working with (their preferences, role, working hours, how they like to be addressed) use `user_md_get` then `user_md_write` \u2014 those land in every agent\'s system prompt directly. Key is a path-like string with a markdown extension, e.g. "auth-flow.md" or "decisions/2026-05-migration.md".',
835
998
  parameters: {
836
999
  type: "object",
837
1000
  properties: {
@@ -917,7 +1080,7 @@ function messagingTools(host, fromAgentId) {
917
1080
  {
918
1081
  def: {
919
1082
  name: "send_message",
920
- description: "Send a message to another agent. Use the recipient's agent id (UUID).",
1083
+ description: `Send a message to another agent. Use the recipient's agent id (UUID). Use this ONLY for things the recipient needs to ACT on: delegating a task, asking a peer for information you cannot get yourself, escalating a decision. Do NOT use it for status updates or to announce changes to group-shared resources \u2014 USER.md and the group memory backend both propagate to every agent in the group automatically on their next turn, so messages like "I updated USER.md" or "I wrote a new memory note" are pure noise and will trigger an inbox-wake loop on the recipient.`,
921
1084
  parameters: {
922
1085
  type: "object",
923
1086
  properties: {
@@ -1021,6 +1184,64 @@ function messagingTools(host, fromAgentId) {
1021
1184
  ];
1022
1185
  }
1023
1186
 
1187
+ // ../daemon/src/runtime/tools/user-md.ts
1188
+ function userMdTools(host, groupId) {
1189
+ return [
1190
+ {
1191
+ def: {
1192
+ name: "user_md_get",
1193
+ description: "Read the group-shared USER.md (facts every agent in the group knows about the human). Returns the current content followed by an `etag:` line \u2014 you MUST pass that etag back as `if_match` on the next `user_md_write` so the daemon can detect concurrent edits by other agents in the group. Always call this immediately before any `user_md_write`.",
1194
+ parameters: {
1195
+ type: "object",
1196
+ properties: {}
1197
+ }
1198
+ },
1199
+ async invoke() {
1200
+ const { content, etag } = await host.get(groupId);
1201
+ const body = content.length > 0 ? content : "(USER.md is empty)";
1202
+ return `${body}
1203
+
1204
+ ---
1205
+ etag: ${etag}`;
1206
+ }
1207
+ },
1208
+ {
1209
+ def: {
1210
+ name: "user_md_write",
1211
+ description: "Replace the group-shared USER.md with new content. Use this for STABLE user-specific facts (preferences, role, working hours, how the human likes to be addressed). MANDATORY workflow: (1) call `user_md_get` first, (2) integrate your change into the full content preserving everything unrelated, (3) call `user_md_write` with the merged content and the etag you got from `user_md_get`. If another agent in the group wrote to USER.md between your get and write, this returns an etag-mismatch error \u2014 just call `user_md_get` again and retry the merge. The full result must fit under 12 KB. For project knowledge use `memory_write`; for notes about yourself use `home_write` on IDENTITY.md.",
1212
+ parameters: {
1213
+ type: "object",
1214
+ properties: {
1215
+ content: {
1216
+ type: "string",
1217
+ description: "Full new contents of USER.md (this is a complete replacement, NOT an append). Include everything you want to keep."
1218
+ },
1219
+ if_match: {
1220
+ type: "string",
1221
+ description: "The etag returned by your most recent `user_md_get`. The write fails if USER.md changed in the meantime."
1222
+ }
1223
+ },
1224
+ required: ["content", "if_match"]
1225
+ }
1226
+ },
1227
+ async invoke(args) {
1228
+ const content = String(args.content ?? "");
1229
+ const ifMatch = String(args.if_match ?? "");
1230
+ if (!ifMatch) {
1231
+ throw new Error(
1232
+ 'user_md_write: "if_match" is required \u2014 call user_md_get first to obtain the current etag.'
1233
+ );
1234
+ }
1235
+ const { etag, totalBytes } = await host.write(groupId, content, ifMatch);
1236
+ return `wrote USER.md (${totalBytes} bytes, new etag: ${etag})`;
1237
+ }
1238
+ }
1239
+ ];
1240
+ }
1241
+
1242
+ // ../daemon/src/runtime/tools/web.ts
1243
+ import { fetch as undiciFetch2 } from "undici";
1244
+
1024
1245
  // ../daemon/src/runtime/tools/web-extract.ts
1025
1246
  import { Readability } from "@mozilla/readability";
1026
1247
  import { parseHTML } from "linkedom";
@@ -1101,7 +1322,7 @@ function extractReadable(html, url, mode) {
1101
1322
  // ../daemon/src/runtime/tools/web-ssrf.ts
1102
1323
  import { lookup as dnsLookupCb } from "dns";
1103
1324
  import { lookup as dnsLookup } from "dns/promises";
1104
- import { Agent } from "undici";
1325
+ import { Agent, fetch as undiciFetch } from "undici";
1105
1326
  var SsrFBlockedError = class extends Error {
1106
1327
  constructor(message) {
1107
1328
  super(message);
@@ -1210,7 +1431,7 @@ async function closeDispatcher(d) {
1210
1431
  }
1211
1432
  }
1212
1433
  async function guardedFetch(opts) {
1213
- const fetcher = opts.fetchImpl ?? globalThis.fetch;
1434
+ const fetcher = opts.fetchImpl ?? undiciFetch;
1214
1435
  const maxRedirects = opts.maxRedirects ?? 3;
1215
1436
  const abortController = new AbortController();
1216
1437
  const timeoutId = opts.timeoutMs ? setTimeout(() => abortController.abort(new Error("timeout")), opts.timeoutMs) : null;
@@ -1291,7 +1512,10 @@ var DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7_2) AppleWe
1291
1512
  var DEFAULT_CACHE_TTL_MS = 15 * 6e4;
1292
1513
  var DEFAULT_CACHE_MAX = 100;
1293
1514
  var DEFAULT_MAX_LENGTH = 2e4;
1294
- var DEFAULT_TIMEOUT_MS = 2e4;
1515
+ var DEFAULT_TIMEOUT_MS = 3e4;
1516
+ var DEFAULT_MAX_BODY_BYTES = 3 * 1024 * 1024;
1517
+ var FIRECRAWL_FALLBACK_THRESHOLD = 200;
1518
+ var FIRECRAWL_DEFAULT_URL = "https://api.firecrawl.dev";
1295
1519
  function stripHtml(s) {
1296
1520
  return s.replace(/<[^>]*>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&nbsp;/g, " ").trim();
1297
1521
  }
@@ -1325,6 +1549,92 @@ async function searxngSearch(query, limit, baseURL, fetchFn) {
1325
1549
  snippet: r.content ?? ""
1326
1550
  }));
1327
1551
  }
1552
+ function describeError(err) {
1553
+ if (!(err instanceof Error)) return String(err);
1554
+ const parts = [];
1555
+ const seen = /* @__PURE__ */ new Set();
1556
+ let cur = err;
1557
+ while (cur instanceof Error && !seen.has(cur)) {
1558
+ seen.add(cur);
1559
+ const code = cur.code;
1560
+ parts.push(code ? `[${code}] ${cur.message}` : cur.message);
1561
+ cur = cur.cause;
1562
+ }
1563
+ return parts.join(" \u2014 cause: ");
1564
+ }
1565
+ async function readBodyCapped(res, maxBytes) {
1566
+ const ct = res.headers.get("content-type") ?? "";
1567
+ const charset = /charset=([^;]+)/i.exec(ct)?.[1]?.trim().toLowerCase() || "utf-8";
1568
+ const decoder = new TextDecoder(charset, { fatal: false });
1569
+ const reader = res.body?.getReader();
1570
+ if (!reader) {
1571
+ const text = await res.text();
1572
+ if (text.length > maxBytes) return { text: text.slice(0, maxBytes), truncated: true };
1573
+ return { text, truncated: false };
1574
+ }
1575
+ const chunks = [];
1576
+ let total = 0;
1577
+ let truncated = false;
1578
+ while (true) {
1579
+ const { value, done } = await reader.read();
1580
+ if (done) break;
1581
+ if (!value) continue;
1582
+ if (total + value.byteLength > maxBytes) {
1583
+ const keep = maxBytes - total;
1584
+ if (keep > 0) chunks.push(value.subarray(0, keep));
1585
+ truncated = true;
1586
+ try {
1587
+ await reader.cancel();
1588
+ } catch {
1589
+ }
1590
+ break;
1591
+ }
1592
+ chunks.push(value);
1593
+ total += value.byteLength;
1594
+ }
1595
+ const sum = chunks.reduce((s, c) => s + c.byteLength, 0);
1596
+ const merged = new Uint8Array(sum);
1597
+ let off = 0;
1598
+ for (const c of chunks) {
1599
+ merged.set(c, off);
1600
+ off += c.byteLength;
1601
+ }
1602
+ return { text: decoder.decode(merged), truncated };
1603
+ }
1604
+ async function firecrawlScrape(url, mode, env, fetchFn, timeoutMs) {
1605
+ const apiKey = env.FIRECRAWL_API_KEY;
1606
+ if (!apiKey) return null;
1607
+ const base = (env.FIRECRAWL_URL ?? FIRECRAWL_DEFAULT_URL).replace(/\/$/, "");
1608
+ const ac = new AbortController();
1609
+ const t = setTimeout(() => ac.abort(new Error("firecrawl timeout")), timeoutMs);
1610
+ try {
1611
+ const res = await fetchFn(`${base}/v1/scrape`, {
1612
+ method: "POST",
1613
+ headers: {
1614
+ "content-type": "application/json",
1615
+ authorization: `Bearer ${apiKey}`,
1616
+ accept: "application/json"
1617
+ },
1618
+ body: JSON.stringify({
1619
+ url,
1620
+ formats: ["markdown"],
1621
+ onlyMainContent: true
1622
+ }),
1623
+ signal: ac.signal
1624
+ });
1625
+ if (!res.ok) return null;
1626
+ const body = await res.json();
1627
+ if (!body.success || !body.data?.markdown) return null;
1628
+ const md = body.data.markdown;
1629
+ const title = body.data.metadata?.title;
1630
+ const text = mode === "text" ? markdownToPlain(md) : md;
1631
+ return title ? { text, title } : { text };
1632
+ } catch {
1633
+ return null;
1634
+ } finally {
1635
+ clearTimeout(t);
1636
+ }
1637
+ }
1328
1638
  function cacheGet(cache, key) {
1329
1639
  const entry = cache.get(key);
1330
1640
  if (!entry) return null;
@@ -1345,12 +1655,14 @@ function cacheSet(cache, key, value, ttlMs, maxEntries) {
1345
1655
  }
1346
1656
  }
1347
1657
  function webTools(opts) {
1348
- const fetchFn = opts?.fetchImpl ?? fetch;
1658
+ const fetchFn = opts?.fetchImpl ?? undiciFetch2;
1349
1659
  const env = opts?.env ?? process.env;
1350
1660
  const allowPrivate = opts?.allowPrivate ?? false;
1351
1661
  const cacheTtlMs = opts?.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
1352
1662
  const cacheMax = opts?.cacheMax ?? DEFAULT_CACHE_MAX;
1353
1663
  const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1664
+ const maxBodyBytes = opts?.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
1665
+ const firecrawlDisabled = opts?.firecrawlDisabled ?? false;
1354
1666
  const cache = /* @__PURE__ */ new Map();
1355
1667
  return [
1356
1668
  {
@@ -1388,7 +1700,7 @@ function webTools(opts) {
1388
1700
  {
1389
1701
  def: {
1390
1702
  name: "web_fetch",
1391
- description: "Fetch a URL and return its readable content. HTML is extracted via Readability and converted to markdown. Results are cached for 15 minutes.",
1703
+ description: "Fetch a URL and return its readable content. HTML is extracted via Readability and converted to markdown. When the primary extraction returns near-empty content from a 2xx HTML page (typical of JS-only shells), the tool automatically retries via Firecrawl if FIRECRAWL_API_KEY is configured. Results are cached for 15 minutes.",
1392
1704
  parameters: {
1393
1705
  type: "object",
1394
1706
  properties: {
@@ -1414,7 +1726,7 @@ function webTools(opts) {
1414
1726
  const cacheKey = `${mode}|${url}`;
1415
1727
  const cached = cacheGet(cache, cacheKey);
1416
1728
  if (cached) return formatOutput(cached, maxLen);
1417
- let result;
1729
+ let result = null;
1418
1730
  try {
1419
1731
  result = await guardedFetch({
1420
1732
  url,
@@ -1429,18 +1741,14 @@ function webTools(opts) {
1429
1741
  }
1430
1742
  }
1431
1743
  });
1432
- } catch (err) {
1433
- if (err instanceof SsrFBlockedError) throw new Error(`web_fetch: ${err.message}`);
1434
- throw err;
1435
- }
1436
- try {
1437
1744
  if (!result.response.ok) {
1438
- throw new Error(`web_fetch: ${result.response.status} ${result.response.statusText}`);
1745
+ throw new Error(`${result.response.status} ${result.response.statusText}`);
1439
1746
  }
1440
1747
  const ct = result.response.headers.get("content-type") ?? "";
1441
- const body = await result.response.text();
1748
+ const isHtml = ct.includes("text/html") || ct.includes("xhtml");
1749
+ const { text: body, truncated } = await readBodyCapped(result.response, maxBodyBytes);
1442
1750
  let extracted;
1443
- if (ct.includes("text/html") || ct.includes("xhtml")) {
1751
+ if (isHtml) {
1444
1752
  extracted = extractReadable(body, result.finalUrl, mode);
1445
1753
  } else if (ct.includes("application/json")) {
1446
1754
  try {
@@ -1451,10 +1759,32 @@ function webTools(opts) {
1451
1759
  } else {
1452
1760
  extracted = { text: body };
1453
1761
  }
1762
+ if (isHtml && !firecrawlDisabled && extracted.text.length < FIRECRAWL_FALLBACK_THRESHOLD) {
1763
+ const rescued = await firecrawlScrape(result.finalUrl, mode, env, fetchFn, timeoutMs);
1764
+ if (rescued) {
1765
+ extracted = {
1766
+ ...rescued,
1767
+ text: `${rescued.text}
1768
+
1769
+ [content rendered via Firecrawl fallback \u2014 primary extraction returned ${extracted.text.length} chars]`
1770
+ };
1771
+ }
1772
+ }
1773
+ if (truncated) {
1774
+ extracted = {
1775
+ ...extracted,
1776
+ text: `${extracted.text}
1777
+
1778
+ [raw body truncated at ${maxBodyBytes} bytes before extraction \u2014 page exceeded the size cap]`
1779
+ };
1780
+ }
1454
1781
  cacheSet(cache, cacheKey, extracted, cacheTtlMs, cacheMax);
1455
1782
  return formatOutput(extracted, maxLen);
1783
+ } catch (err) {
1784
+ if (err instanceof SsrFBlockedError) throw new Error(`web_fetch: ${err.message}`);
1785
+ throw new Error(`web_fetch: ${describeError(err)}`);
1456
1786
  } finally {
1457
- await result.release();
1787
+ if (result) await result.release();
1458
1788
  }
1459
1789
  }
1460
1790
  }
@@ -1501,13 +1831,16 @@ function createBazilionCustomTools(opts) {
1501
1831
  if (opts.messagingHost) {
1502
1832
  handlers.push(...messagingTools(opts.messagingHost, opts.agent.agent.id));
1503
1833
  }
1834
+ if (opts.userMdHost) {
1835
+ handlers.push(...userMdTools(opts.userMdHost, opts.agent.group.id));
1836
+ }
1504
1837
  return handlers.map(ourToolToPiTool);
1505
1838
  }
1506
1839
 
1507
1840
  // ../daemon/src/runtime/pi/session.ts
1508
1841
  var BUILTIN_TOOL_NAMES = ["read", "bash", "edit", "write", "grep", "find", "ls"];
1509
1842
  async function createBazilionSession(opts) {
1510
- const { agent, paths, env, memory, enabledProviders, messagingHost, refreshApiKey } = opts;
1843
+ const { agent, paths, env, memory, enabledProviders, messagingHost, userMdHost, refreshApiKey } = opts;
1511
1844
  const { providerName, modelId } = splitModelString(agent.model);
1512
1845
  if (enabledProviders.size > 0 && !enabledProviders.has(providerName)) {
1513
1846
  throw new Error(`${providerName} provider is disabled \u2014 enable it on the /config page`);
@@ -1544,12 +1877,17 @@ async function createBazilionSession(opts) {
1544
1877
  const sessionManager = existing ? SessionManager.open(existing, sessionDir, cwd) : SessionManager.create(cwd, sessionDir);
1545
1878
  const settingsManager = SettingsManager.inMemory({
1546
1879
  compaction: { enabled: false },
1547
- retry: { enabled: true, maxRetries: 2, baseDelayMs: 500, maxDelayMs: 8e3 }
1880
+ retry: {
1881
+ enabled: true,
1882
+ maxRetries: 2,
1883
+ baseDelayMs: 500,
1884
+ provider: { maxRetryDelayMs: 8e3 }
1885
+ }
1548
1886
  });
1549
1887
  const bazilionPrompt = buildSystemPrompt(agent);
1550
1888
  const resourceLoader = createBazilionResourceLoader(bazilionPrompt);
1551
1889
  await resourceLoader.reload();
1552
- const customTools = createBazilionCustomTools({ agent, memory, messagingHost, env });
1890
+ const customTools = createBazilionCustomTools({ agent, memory, messagingHost, userMdHost, env });
1553
1891
  const allowedTools = [...BUILTIN_TOOL_NAMES, ...customTools.map((t) => t.name)];
1554
1892
  const { session } = await createAgentSession({
1555
1893
  cwd,
@@ -1681,7 +2019,7 @@ function createBazilionResourceLoader(appendSystemPrompt) {
1681
2019
  function findMostRecent(sessionDir) {
1682
2020
  if (!existsSync11(sessionDir)) return null;
1683
2021
  let newest = null;
1684
- for (const entry of readdirSync6(sessionDir)) {
2022
+ for (const entry of readdirSync7(sessionDir)) {
1685
2023
  if (!entry.endsWith(".jsonl")) continue;
1686
2024
  const path = join13(sessionDir, entry);
1687
2025
  try {
@@ -1709,7 +2047,7 @@ async function readInput() {
1709
2047
  }
1710
2048
  return parsed;
1711
2049
  }
1712
- function createIpcMessagingHost() {
2050
+ function createIpcClient() {
1713
2051
  const pending = /* @__PURE__ */ new Map();
1714
2052
  process.on("message", (msg) => {
1715
2053
  if (!isIpcReply(msg)) return;
@@ -1723,17 +2061,14 @@ function createIpcMessagingHost() {
1723
2061
  for (const [, entry] of pending) entry.reject(new Error("worker IPC channel disconnected"));
1724
2062
  pending.clear();
1725
2063
  });
1726
- function call(method, args) {
2064
+ return (method, args) => {
1727
2065
  if (!process.send) {
1728
2066
  return Promise.reject(new Error("worker: no IPC channel \u2014 daemon must spawn with stdio:ipc"));
1729
2067
  }
1730
2068
  const id = randomUUID5();
1731
2069
  const message = { type: "rpc", id, method, args };
1732
2070
  return new Promise((resolve3, reject) => {
1733
- pending.set(id, {
1734
- resolve: (v) => resolve3(v),
1735
- reject
1736
- });
2071
+ pending.set(id, { resolve: (v) => resolve3(v), reject });
1737
2072
  process.send?.(message, void 0, void 0, (err) => {
1738
2073
  if (err) {
1739
2074
  pending.delete(id);
@@ -1741,7 +2076,9 @@ function createIpcMessagingHost() {
1741
2076
  }
1742
2077
  });
1743
2078
  });
1744
- }
2079
+ };
2080
+ }
2081
+ function createIpcMessagingHost(call) {
1745
2082
  return {
1746
2083
  agentExists: (agentId) => call("agentExists", { agentId }),
1747
2084
  sendMessage: (input) => call("sendMessage", input),
@@ -1752,6 +2089,12 @@ function createIpcMessagingHost() {
1752
2089
  findReplies: (agentId, replyTo) => call("findReplies", { agentId, replyTo })
1753
2090
  };
1754
2091
  }
2092
+ function createIpcUserMdHost(call) {
2093
+ return {
2094
+ get: (groupId) => call("userMdGet", { groupId }),
2095
+ write: (groupId, content, ifMatch) => call("userMdWrite", { groupId, content, ifMatch })
2096
+ };
2097
+ }
1755
2098
  function isIpcReply(msg) {
1756
2099
  if (!msg || typeof msg !== "object") return false;
1757
2100
  const m = msg;
@@ -1778,7 +2121,9 @@ async function main() {
1778
2121
  const paths = resolvePaths();
1779
2122
  const memory = qmdBackend(join14(agent.group.path, "memory"));
1780
2123
  await memory.init();
1781
- const messagingHost = createIpcMessagingHost();
2124
+ const ipcCall = createIpcClient();
2125
+ const messagingHost = createIpcMessagingHost(ipcCall);
2126
+ const userMdHost = createIpcUserMdHost(ipcCall);
1782
2127
  const { session, dispose } = await createBazilionSession({
1783
2128
  agent,
1784
2129
  paths,
@@ -1786,6 +2131,7 @@ async function main() {
1786
2131
  memory,
1787
2132
  enabledProviders: new Set(enabledProviders),
1788
2133
  messagingHost,
2134
+ userMdHost,
1789
2135
  apiKey
1790
2136
  });
1791
2137
  abortSession = () => {