bazilion 0.1.0 → 0.1.1

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
@@ -79,6 +79,25 @@ import { join as join7 } from "path";
79
79
  // ../daemon/src/core/services.ts
80
80
  var SERVICES = [
81
81
  // --- LLM providers (configured via API keys / URLs) ---
82
+ // Top 3: openai-codex (ChatGPT OAuth), openai (API key), anthropic.
83
+ // Everything else in rough popularity order; locals last.
84
+ {
85
+ id: "openai-codex",
86
+ displayName: "OpenAI ChatGPT (OAuth)",
87
+ category: "provider",
88
+ hint: "Use your ChatGPT Plus/Pro/Team account (same login as Codex CLI)",
89
+ // No form fields — credentials come from an OAuth flow. The /config page
90
+ // renders a Connect/Disconnect card using /api/auth/openai instead of the
91
+ // standard field inputs.
92
+ fields: []
93
+ },
94
+ {
95
+ id: "openai",
96
+ displayName: "OpenAI",
97
+ category: "provider",
98
+ hint: "GPT models \xB7 platform.openai.com",
99
+ fields: [{ envVar: "OPENAI_API_KEY", kind: "secret", label: "API key", placeholder: "sk-..." }]
100
+ },
82
101
  {
83
102
  id: "anthropic",
84
103
  displayName: "Anthropic",
@@ -94,23 +113,6 @@ var SERVICES = [
94
113
  }
95
114
  ]
96
115
  },
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
116
  {
115
117
  id: "google",
116
118
  displayName: "Google (Gemini)",
@@ -153,6 +155,27 @@ var SERVICES = [
153
155
  hint: "Authenticates via AWS SDK env (AWS_PROFILE or AWS_ACCESS_KEY_ID/SECRET)",
154
156
  fields: []
155
157
  },
158
+ {
159
+ id: "github-copilot",
160
+ displayName: "GitHub Copilot",
161
+ category: "provider",
162
+ hint: "Use a GitHub Copilot subscription to call Claude/GPT/Gemini via Copilot",
163
+ fields: [
164
+ {
165
+ envVar: "COPILOT_GITHUB_TOKEN",
166
+ kind: "secret",
167
+ label: "GitHub token",
168
+ description: "Generic GH_TOKEN/GITHUB_TOKEN are ignored \u2014 set this scoped variable explicitly (or run `bazilion auth copilot login` once available)."
169
+ }
170
+ ]
171
+ },
172
+ {
173
+ id: "deepseek",
174
+ displayName: "DeepSeek",
175
+ category: "provider",
176
+ hint: "DeepSeek V4 Flash / Pro \xB7 platform.deepseek.com",
177
+ fields: [{ envVar: "DEEPSEEK_API_KEY", kind: "secret", label: "API key" }]
178
+ },
156
179
  {
157
180
  id: "mistral",
158
181
  displayName: "Mistral",
@@ -160,6 +183,13 @@ var SERVICES = [
160
183
  hint: "mistral.ai",
161
184
  fields: [{ envVar: "MISTRAL_API_KEY", kind: "secret", label: "API key" }]
162
185
  },
186
+ {
187
+ id: "xai",
188
+ displayName: "xAI",
189
+ category: "provider",
190
+ hint: "Grok \xB7 x.ai",
191
+ fields: [{ envVar: "XAI_API_KEY", kind: "secret", label: "API key" }]
192
+ },
163
193
  {
164
194
  id: "groq",
165
195
  displayName: "Groq",
@@ -174,11 +204,46 @@ var SERVICES = [
174
204
  fields: [{ envVar: "CEREBRAS_API_KEY", kind: "secret", label: "API key" }]
175
205
  },
176
206
  {
177
- id: "xai",
178
- displayName: "xAI",
207
+ id: "fireworks",
208
+ displayName: "Fireworks AI",
179
209
  category: "provider",
180
- hint: "Grok \xB7 x.ai",
181
- fields: [{ envVar: "XAI_API_KEY", kind: "secret", label: "API key" }]
210
+ hint: "DeepSeek/GLM/Kimi via fireworks.ai",
211
+ fields: [{ envVar: "FIREWORKS_API_KEY", kind: "secret", label: "API key" }]
212
+ },
213
+ {
214
+ id: "together",
215
+ displayName: "Together AI",
216
+ category: "provider",
217
+ hint: "Open-weight models \xB7 together.ai",
218
+ fields: [{ envVar: "TOGETHER_API_KEY", kind: "secret", label: "API key" }]
219
+ },
220
+ {
221
+ id: "moonshotai",
222
+ displayName: "Moonshot AI",
223
+ category: "provider",
224
+ hint: "Kimi K2/K2.5/K2.6 \xB7 platform.moonshot.ai",
225
+ fields: [{ envVar: "MOONSHOT_API_KEY", kind: "secret", label: "API key" }]
226
+ },
227
+ {
228
+ id: "kimi-coding",
229
+ displayName: "Kimi Coding",
230
+ category: "provider",
231
+ hint: "Coding-tuned Kimi endpoint \xB7 platform.moonshot.cn",
232
+ fields: [{ envVar: "KIMI_API_KEY", kind: "secret", label: "API key" }]
233
+ },
234
+ {
235
+ id: "minimax",
236
+ displayName: "MiniMax",
237
+ category: "provider",
238
+ hint: "MiniMax M2 family \xB7 platform.minimaxi.com",
239
+ fields: [{ envVar: "MINIMAX_API_KEY", kind: "secret", label: "API key" }]
240
+ },
241
+ {
242
+ id: "xiaomi",
243
+ displayName: "Xiaomi MiMo",
244
+ category: "provider",
245
+ hint: "API billing endpoint \xB7 platform.xiaomimimo.com",
246
+ fields: [{ envVar: "XIAOMI_API_KEY", kind: "secret", label: "API key" }]
182
247
  },
183
248
  {
184
249
  id: "zai",
@@ -193,6 +258,27 @@ var SERVICES = [
193
258
  hint: "Inference endpoints \xB7 huggingface.co",
194
259
  fields: [{ envVar: "HF_TOKEN", kind: "secret", label: "Access token", placeholder: "hf_..." }]
195
260
  },
261
+ {
262
+ id: "cloudflare-ai-gateway",
263
+ displayName: "Cloudflare AI Gateway",
264
+ category: "provider",
265
+ hint: "Per-gateway routing to OpenAI/Anthropic/Workers AI",
266
+ fields: [
267
+ { envVar: "CLOUDFLARE_API_KEY", kind: "secret", label: "API key" },
268
+ { envVar: "CLOUDFLARE_ACCOUNT_ID", kind: "config", label: "Account ID" },
269
+ { envVar: "CLOUDFLARE_GATEWAY_ID", kind: "config", label: "Gateway ID" }
270
+ ]
271
+ },
272
+ {
273
+ id: "cloudflare-workers-ai",
274
+ displayName: "Cloudflare Workers AI",
275
+ category: "provider",
276
+ hint: "Inference on Cloudflare Workers \xB7 ai.cloudflare.com",
277
+ fields: [
278
+ { envVar: "CLOUDFLARE_API_KEY", kind: "secret", label: "API key" },
279
+ { envVar: "CLOUDFLARE_ACCOUNT_ID", kind: "config", label: "Account ID" }
280
+ ]
281
+ },
196
282
  {
197
283
  id: "openrouter",
198
284
  displayName: "OpenRouter",
@@ -216,6 +302,13 @@ var SERVICES = [
216
302
  }
217
303
  ]
218
304
  },
305
+ {
306
+ id: "opencode",
307
+ displayName: "OpenCode",
308
+ category: "provider",
309
+ hint: "OpenAI-compatible proxy from the OpenCode CLI",
310
+ fields: [{ envVar: "OPENCODE_API_KEY", kind: "secret", label: "API key" }]
311
+ },
219
312
  {
220
313
  id: "lmstudio",
221
314
  displayName: "LM Studio",
@@ -254,11 +347,54 @@ var SERVICES = [
254
347
  }
255
348
  ]
256
349
  },
350
+ {
351
+ id: "llamacpp",
352
+ displayName: "llama.cpp",
353
+ category: "provider",
354
+ hint: "Local inference \xB7 llama.cpp llama-server (OpenAI-compat /v1 endpoint)",
355
+ fields: [
356
+ {
357
+ envVar: "LLAMACPP_URL",
358
+ kind: "config",
359
+ label: "Endpoint URL",
360
+ placeholder: "http://127.0.0.1:8080/v1"
361
+ },
362
+ {
363
+ envVar: "LLAMACPP_API_KEY",
364
+ kind: "secret",
365
+ label: "API key (only if started with --api-key)",
366
+ description: "llama-server runs without auth by default. Set this only if you launched the server with the `--api-key KEY` flag."
367
+ }
368
+ ]
369
+ },
257
370
  // --- Ancillary services (web search, etc) ---
371
+ {
372
+ id: "firecrawl",
373
+ displayName: "Firecrawl",
374
+ category: "service",
375
+ group: "Web Search",
376
+ hint: "web_fetch fallback for JS-heavy/blocked pages \xB7 firecrawl.dev (free tier available)",
377
+ fields: [
378
+ {
379
+ envVar: "FIRECRAWL_API_KEY",
380
+ kind: "secret",
381
+ label: "API key",
382
+ placeholder: "fc-...",
383
+ description: "When set, web_fetch automatically falls back to Firecrawl if the primary Readability extraction yields too little content."
384
+ },
385
+ {
386
+ envVar: "FIRECRAWL_URL",
387
+ kind: "config",
388
+ label: "Base URL (optional, for self-hosted)",
389
+ placeholder: "https://api.firecrawl.dev"
390
+ }
391
+ ]
392
+ },
258
393
  {
259
394
  id: "brave-search",
260
395
  displayName: "Brave Search",
261
396
  category: "service",
397
+ group: "Web Search",
262
398
  hint: "Web search tool \xB7 free tier at brave.com/search/api/",
263
399
  fields: [{ envVar: "BRAVE_API_KEY", kind: "secret", label: "API key", placeholder: "BSA..." }]
264
400
  },
@@ -266,6 +402,7 @@ var SERVICES = [
266
402
  id: "searxng",
267
403
  displayName: "SearXNG",
268
404
  category: "service",
405
+ group: "Web Search",
269
406
  hint: "Self-hosted meta-search engine \xB7 searxng.org",
270
407
  fields: [
271
408
  {
@@ -567,14 +704,14 @@ import {
567
704
  ModelRegistry,
568
705
  SessionManager,
569
706
  SettingsManager
570
- } from "@mariozechner/pi-coding-agent";
707
+ } from "@earendil-works/pi-coding-agent";
571
708
 
572
709
  // ../daemon/src/runtime/providers/pi-adapter.ts
573
710
  import {
574
711
  getModel,
575
712
  streamSimple,
576
713
  Type
577
- } from "@mariozechner/pi-ai";
714
+ } from "@earendil-works/pi-ai";
578
715
  function defaultBaseUrlFor(providerName) {
579
716
  switch (providerName) {
580
717
  case "lmstudio":
@@ -616,7 +753,7 @@ function resolveModel(cfg, modelId) {
616
753
  }
617
754
 
618
755
  // ../daemon/src/runtime/auth/openai-codex.ts
619
- import { loginOpenAICodex, refreshOpenAICodexToken } from "@mariozechner/pi-ai/oauth";
756
+ import { loginOpenAICodex, refreshOpenAICodexToken } from "@earendil-works/pi-ai/oauth";
620
757
 
621
758
  // ../daemon/src/runtime/session/prompt.ts
622
759
  import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
@@ -626,13 +763,11 @@ var CONTEXT_FILE_ORDER = [
626
763
  "SOUL.md",
627
764
  "TOOLS.md",
628
765
  "IDENTITY.md",
629
- "HEARTBEAT.md",
630
- "BOOTSTRAP.md"
766
+ "HEARTBEAT.md"
631
767
  ];
632
768
  function buildSystemPrompt(agent) {
633
769
  const parts = [];
634
770
  const contextBlocks = [];
635
- let bootstrapPresent = false;
636
771
  for (const file of CONTEXT_FILE_ORDER) {
637
772
  const path = join10(agent.agent.dir, file);
638
773
  if (!existsSync9(path)) continue;
@@ -641,23 +776,39 @@ function buildSystemPrompt(agent) {
641
776
  contextBlocks.push(`## ${file}
642
777
 
643
778
  ${content}`);
644
- if (file === "BOOTSTRAP.md") bootstrapPresent = true;
645
779
  }
646
780
  if (contextBlocks.length > 0) {
647
781
  parts.push(`# Project Context
648
782
 
649
783
  ${contextBlocks.join("\n\n")}`);
650
784
  }
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
- );
785
+ const bootstrapPath = join10(agent.agent.dir, "BOOTSTRAP.md");
786
+ if (existsSync9(bootstrapPath)) {
787
+ const bootstrap = readFileSync7(bootstrapPath, "utf8").trimEnd();
788
+ if (bootstrap) {
789
+ parts.push(
790
+ [
791
+ "# First-Run Ritual",
792
+ "",
793
+ "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.",
794
+ "",
795
+ "## Hard rules",
796
+ "- Your first reply is ONLY a greeting + ONE question. No tool calls. Wait for the human to answer.",
797
+ "- Each subsequent turn: at most one new question. Wait between turns.",
798
+ "- Only after the ritual is complete (you have enough to write IDENTITY.md): call `home_write` once, then `bootstrap_done`.",
799
+ "",
800
+ "## BOOTSTRAP.md",
801
+ "",
802
+ bootstrap
803
+ ].join("\n")
804
+ );
805
+ }
655
806
  }
656
807
  parts.push(
657
808
  [
658
809
  "# Agent Home",
659
810
  "",
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.",
811
+ "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
812
  "",
662
813
  "- To change who you are (name, vibe, personality, how you behave): use `home_write`.",
663
814
  "- To inspect exact wording of your own files: use `home_read` or `home_list`.",
@@ -684,13 +835,19 @@ You have access to the following skills: ${agent.skills.join(", ")}.`
684
835
  parts.push(
685
836
  `# About the User
686
837
 
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.
838
+ 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
839
 
689
840
  ${agent.group.userMd.trim()}`
690
841
  );
842
+ } else {
843
+ parts.push(
844
+ `# About the User
845
+
846
+ 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.**`
847
+ );
691
848
  }
692
849
  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."
850
+ "# 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
851
  );
695
852
  return parts.join("\n\n---\n\n");
696
853
  }
@@ -831,7 +988,7 @@ function memoryTools(memory) {
831
988
  {
832
989
  def: {
833
990
  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".',
991
+ 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
992
  parameters: {
836
993
  type: "object",
837
994
  properties: {
@@ -917,7 +1074,7 @@ function messagingTools(host, fromAgentId) {
917
1074
  {
918
1075
  def: {
919
1076
  name: "send_message",
920
- description: "Send a message to another agent. Use the recipient's agent id (UUID).",
1077
+ 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
1078
  parameters: {
922
1079
  type: "object",
923
1080
  properties: {
@@ -1021,6 +1178,64 @@ function messagingTools(host, fromAgentId) {
1021
1178
  ];
1022
1179
  }
1023
1180
 
1181
+ // ../daemon/src/runtime/tools/user-md.ts
1182
+ function userMdTools(host, groupId) {
1183
+ return [
1184
+ {
1185
+ def: {
1186
+ name: "user_md_get",
1187
+ 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`.",
1188
+ parameters: {
1189
+ type: "object",
1190
+ properties: {}
1191
+ }
1192
+ },
1193
+ async invoke() {
1194
+ const { content, etag } = await host.get(groupId);
1195
+ const body = content.length > 0 ? content : "(USER.md is empty)";
1196
+ return `${body}
1197
+
1198
+ ---
1199
+ etag: ${etag}`;
1200
+ }
1201
+ },
1202
+ {
1203
+ def: {
1204
+ name: "user_md_write",
1205
+ 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.",
1206
+ parameters: {
1207
+ type: "object",
1208
+ properties: {
1209
+ content: {
1210
+ type: "string",
1211
+ description: "Full new contents of USER.md (this is a complete replacement, NOT an append). Include everything you want to keep."
1212
+ },
1213
+ if_match: {
1214
+ type: "string",
1215
+ description: "The etag returned by your most recent `user_md_get`. The write fails if USER.md changed in the meantime."
1216
+ }
1217
+ },
1218
+ required: ["content", "if_match"]
1219
+ }
1220
+ },
1221
+ async invoke(args) {
1222
+ const content = String(args.content ?? "");
1223
+ const ifMatch = String(args.if_match ?? "");
1224
+ if (!ifMatch) {
1225
+ throw new Error(
1226
+ 'user_md_write: "if_match" is required \u2014 call user_md_get first to obtain the current etag.'
1227
+ );
1228
+ }
1229
+ const { etag, totalBytes } = await host.write(groupId, content, ifMatch);
1230
+ return `wrote USER.md (${totalBytes} bytes, new etag: ${etag})`;
1231
+ }
1232
+ }
1233
+ ];
1234
+ }
1235
+
1236
+ // ../daemon/src/runtime/tools/web.ts
1237
+ import { fetch as undiciFetch2 } from "undici";
1238
+
1024
1239
  // ../daemon/src/runtime/tools/web-extract.ts
1025
1240
  import { Readability } from "@mozilla/readability";
1026
1241
  import { parseHTML } from "linkedom";
@@ -1101,7 +1316,7 @@ function extractReadable(html, url, mode) {
1101
1316
  // ../daemon/src/runtime/tools/web-ssrf.ts
1102
1317
  import { lookup as dnsLookupCb } from "dns";
1103
1318
  import { lookup as dnsLookup } from "dns/promises";
1104
- import { Agent } from "undici";
1319
+ import { Agent, fetch as undiciFetch } from "undici";
1105
1320
  var SsrFBlockedError = class extends Error {
1106
1321
  constructor(message) {
1107
1322
  super(message);
@@ -1210,7 +1425,7 @@ async function closeDispatcher(d) {
1210
1425
  }
1211
1426
  }
1212
1427
  async function guardedFetch(opts) {
1213
- const fetcher = opts.fetchImpl ?? globalThis.fetch;
1428
+ const fetcher = opts.fetchImpl ?? undiciFetch;
1214
1429
  const maxRedirects = opts.maxRedirects ?? 3;
1215
1430
  const abortController = new AbortController();
1216
1431
  const timeoutId = opts.timeoutMs ? setTimeout(() => abortController.abort(new Error("timeout")), opts.timeoutMs) : null;
@@ -1291,7 +1506,10 @@ var DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7_2) AppleWe
1291
1506
  var DEFAULT_CACHE_TTL_MS = 15 * 6e4;
1292
1507
  var DEFAULT_CACHE_MAX = 100;
1293
1508
  var DEFAULT_MAX_LENGTH = 2e4;
1294
- var DEFAULT_TIMEOUT_MS = 2e4;
1509
+ var DEFAULT_TIMEOUT_MS = 3e4;
1510
+ var DEFAULT_MAX_BODY_BYTES = 3 * 1024 * 1024;
1511
+ var FIRECRAWL_FALLBACK_THRESHOLD = 200;
1512
+ var FIRECRAWL_DEFAULT_URL = "https://api.firecrawl.dev";
1295
1513
  function stripHtml(s) {
1296
1514
  return s.replace(/<[^>]*>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&nbsp;/g, " ").trim();
1297
1515
  }
@@ -1325,6 +1543,92 @@ async function searxngSearch(query, limit, baseURL, fetchFn) {
1325
1543
  snippet: r.content ?? ""
1326
1544
  }));
1327
1545
  }
1546
+ function describeError(err) {
1547
+ if (!(err instanceof Error)) return String(err);
1548
+ const parts = [];
1549
+ const seen = /* @__PURE__ */ new Set();
1550
+ let cur = err;
1551
+ while (cur instanceof Error && !seen.has(cur)) {
1552
+ seen.add(cur);
1553
+ const code = cur.code;
1554
+ parts.push(code ? `[${code}] ${cur.message}` : cur.message);
1555
+ cur = cur.cause;
1556
+ }
1557
+ return parts.join(" \u2014 cause: ");
1558
+ }
1559
+ async function readBodyCapped(res, maxBytes) {
1560
+ const ct = res.headers.get("content-type") ?? "";
1561
+ const charset = /charset=([^;]+)/i.exec(ct)?.[1]?.trim().toLowerCase() || "utf-8";
1562
+ const decoder = new TextDecoder(charset, { fatal: false });
1563
+ const reader = res.body?.getReader();
1564
+ if (!reader) {
1565
+ const text = await res.text();
1566
+ if (text.length > maxBytes) return { text: text.slice(0, maxBytes), truncated: true };
1567
+ return { text, truncated: false };
1568
+ }
1569
+ const chunks = [];
1570
+ let total = 0;
1571
+ let truncated = false;
1572
+ while (true) {
1573
+ const { value, done } = await reader.read();
1574
+ if (done) break;
1575
+ if (!value) continue;
1576
+ if (total + value.byteLength > maxBytes) {
1577
+ const keep = maxBytes - total;
1578
+ if (keep > 0) chunks.push(value.subarray(0, keep));
1579
+ truncated = true;
1580
+ try {
1581
+ await reader.cancel();
1582
+ } catch {
1583
+ }
1584
+ break;
1585
+ }
1586
+ chunks.push(value);
1587
+ total += value.byteLength;
1588
+ }
1589
+ const sum = chunks.reduce((s, c) => s + c.byteLength, 0);
1590
+ const merged = new Uint8Array(sum);
1591
+ let off = 0;
1592
+ for (const c of chunks) {
1593
+ merged.set(c, off);
1594
+ off += c.byteLength;
1595
+ }
1596
+ return { text: decoder.decode(merged), truncated };
1597
+ }
1598
+ async function firecrawlScrape(url, mode, env, fetchFn, timeoutMs) {
1599
+ const apiKey = env.FIRECRAWL_API_KEY;
1600
+ if (!apiKey) return null;
1601
+ const base = (env.FIRECRAWL_URL ?? FIRECRAWL_DEFAULT_URL).replace(/\/$/, "");
1602
+ const ac = new AbortController();
1603
+ const t = setTimeout(() => ac.abort(new Error("firecrawl timeout")), timeoutMs);
1604
+ try {
1605
+ const res = await fetchFn(`${base}/v1/scrape`, {
1606
+ method: "POST",
1607
+ headers: {
1608
+ "content-type": "application/json",
1609
+ authorization: `Bearer ${apiKey}`,
1610
+ accept: "application/json"
1611
+ },
1612
+ body: JSON.stringify({
1613
+ url,
1614
+ formats: ["markdown"],
1615
+ onlyMainContent: true
1616
+ }),
1617
+ signal: ac.signal
1618
+ });
1619
+ if (!res.ok) return null;
1620
+ const body = await res.json();
1621
+ if (!body.success || !body.data?.markdown) return null;
1622
+ const md = body.data.markdown;
1623
+ const title = body.data.metadata?.title;
1624
+ const text = mode === "text" ? markdownToPlain(md) : md;
1625
+ return title ? { text, title } : { text };
1626
+ } catch {
1627
+ return null;
1628
+ } finally {
1629
+ clearTimeout(t);
1630
+ }
1631
+ }
1328
1632
  function cacheGet(cache, key) {
1329
1633
  const entry = cache.get(key);
1330
1634
  if (!entry) return null;
@@ -1345,12 +1649,14 @@ function cacheSet(cache, key, value, ttlMs, maxEntries) {
1345
1649
  }
1346
1650
  }
1347
1651
  function webTools(opts) {
1348
- const fetchFn = opts?.fetchImpl ?? fetch;
1652
+ const fetchFn = opts?.fetchImpl ?? undiciFetch2;
1349
1653
  const env = opts?.env ?? process.env;
1350
1654
  const allowPrivate = opts?.allowPrivate ?? false;
1351
1655
  const cacheTtlMs = opts?.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
1352
1656
  const cacheMax = opts?.cacheMax ?? DEFAULT_CACHE_MAX;
1353
1657
  const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1658
+ const maxBodyBytes = opts?.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
1659
+ const firecrawlDisabled = opts?.firecrawlDisabled ?? false;
1354
1660
  const cache = /* @__PURE__ */ new Map();
1355
1661
  return [
1356
1662
  {
@@ -1388,7 +1694,7 @@ function webTools(opts) {
1388
1694
  {
1389
1695
  def: {
1390
1696
  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.",
1697
+ 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
1698
  parameters: {
1393
1699
  type: "object",
1394
1700
  properties: {
@@ -1414,7 +1720,7 @@ function webTools(opts) {
1414
1720
  const cacheKey = `${mode}|${url}`;
1415
1721
  const cached = cacheGet(cache, cacheKey);
1416
1722
  if (cached) return formatOutput(cached, maxLen);
1417
- let result;
1723
+ let result = null;
1418
1724
  try {
1419
1725
  result = await guardedFetch({
1420
1726
  url,
@@ -1429,18 +1735,14 @@ function webTools(opts) {
1429
1735
  }
1430
1736
  }
1431
1737
  });
1432
- } catch (err) {
1433
- if (err instanceof SsrFBlockedError) throw new Error(`web_fetch: ${err.message}`);
1434
- throw err;
1435
- }
1436
- try {
1437
1738
  if (!result.response.ok) {
1438
- throw new Error(`web_fetch: ${result.response.status} ${result.response.statusText}`);
1739
+ throw new Error(`${result.response.status} ${result.response.statusText}`);
1439
1740
  }
1440
1741
  const ct = result.response.headers.get("content-type") ?? "";
1441
- const body = await result.response.text();
1742
+ const isHtml = ct.includes("text/html") || ct.includes("xhtml");
1743
+ const { text: body, truncated } = await readBodyCapped(result.response, maxBodyBytes);
1442
1744
  let extracted;
1443
- if (ct.includes("text/html") || ct.includes("xhtml")) {
1745
+ if (isHtml) {
1444
1746
  extracted = extractReadable(body, result.finalUrl, mode);
1445
1747
  } else if (ct.includes("application/json")) {
1446
1748
  try {
@@ -1451,10 +1753,38 @@ function webTools(opts) {
1451
1753
  } else {
1452
1754
  extracted = { text: body };
1453
1755
  }
1756
+ if (isHtml && !firecrawlDisabled && extracted.text.length < FIRECRAWL_FALLBACK_THRESHOLD) {
1757
+ const rescued = await firecrawlScrape(
1758
+ result.finalUrl,
1759
+ mode,
1760
+ env,
1761
+ fetchFn,
1762
+ timeoutMs
1763
+ );
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,
@@ -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 = () => {