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/daemon.js CHANGED
@@ -541,12 +541,32 @@ Fill this in during your first conversation. Make it yours.
541
541
  `;
542
542
  var DEFAULT_BOOTSTRAP = `# BOOTSTRAP.md \u2014 First Run
543
543
 
544
- You just woke up. There is no memory yet \u2014 that's normal.
544
+ You just woke up. There is no memory yet \u2014 that's normal. This is a multi-turn
545
+ ritual: ask ONE question per turn and wait for the human's reply before moving
546
+ on. Do not race through it. Do not call any tool until the ritual is finished.
545
547
 
546
- ## What to do
547
- 1. Ask your human who they are and what they want to call you.
548
- 2. Call the \`home_write\` tool with \`file: "IDENTITY.md"\` and new content that captures your name, vibe, and emoji. Do NOT use the generic \`edit\` / \`write\` tools for this \u2014 those land in your workspace, not your private home, and would collide with other agents.
549
- 3. Call \`bootstrap_done\` when finished \u2014 it removes this file so it does not appear in future sessions.
548
+ ## The ritual
549
+
550
+ **Turn 1 (right now):** Greet the human warmly and ask a single opening
551
+ question \u2014 what should they call you, or what should you focus on for them.
552
+ Do NOT call any tool yet. Just reply with greeting + one question.
553
+
554
+ **Turn 2+:** Continue with one more question per turn to fill in the rest of
555
+ your identity \u2014 vibe (warm / sharp / playful / calm / \u2026), an emoji that
556
+ feels right. Each turn is acknowledging the previous answer + at most one
557
+ new question. Skip a turn when you already have enough.
558
+
559
+ **Final turn:** Once you have everything (Name, Vibe, Emoji), call \`home_write\`
560
+ with \`file: "IDENTITY.md"\` and the populated content. Do NOT use the generic
561
+ \`edit\` / \`write\` tools \u2014 those land in the shared workspace.
562
+
563
+ Then call \`bootstrap_done\` to retire this ritual file. After that, future
564
+ sessions skip the bootstrap and start from IDENTITY.md directly.
565
+
566
+ ## Hard rules
567
+ - Do not invent a name on your own. Ask the human and use what they say.
568
+ - Do not call \`home_write\` or \`bootstrap_done\` on your very first reply.
569
+ - One question per turn. Wait for the human to answer.
550
570
  `;
551
571
 
552
572
  // ../daemon/src/core/profile/create.ts
@@ -903,6 +923,25 @@ function updateProfile(db, paths, id, input) {
903
923
  // ../daemon/src/core/services.ts
904
924
  var SERVICES = [
905
925
  // --- LLM providers (configured via API keys / URLs) ---
926
+ // Top 3: openai-codex (ChatGPT OAuth), openai (API key), anthropic.
927
+ // Everything else in rough popularity order; locals last.
928
+ {
929
+ id: "openai-codex",
930
+ displayName: "OpenAI ChatGPT (OAuth)",
931
+ category: "provider",
932
+ hint: "Use your ChatGPT Plus/Pro/Team account (same login as Codex CLI)",
933
+ // No form fields — credentials come from an OAuth flow. The /config page
934
+ // renders a Connect/Disconnect card using /api/auth/openai instead of the
935
+ // standard field inputs.
936
+ fields: []
937
+ },
938
+ {
939
+ id: "openai",
940
+ displayName: "OpenAI",
941
+ category: "provider",
942
+ hint: "GPT models \xB7 platform.openai.com",
943
+ fields: [{ envVar: "OPENAI_API_KEY", kind: "secret", label: "API key", placeholder: "sk-..." }]
944
+ },
906
945
  {
907
946
  id: "anthropic",
908
947
  displayName: "Anthropic",
@@ -918,23 +957,6 @@ var SERVICES = [
918
957
  }
919
958
  ]
920
959
  },
921
- {
922
- id: "openai",
923
- displayName: "OpenAI",
924
- category: "provider",
925
- hint: "GPT models \xB7 platform.openai.com",
926
- fields: [{ envVar: "OPENAI_API_KEY", kind: "secret", label: "API key", placeholder: "sk-..." }]
927
- },
928
- {
929
- id: "openai-codex",
930
- displayName: "OpenAI ChatGPT (OAuth)",
931
- category: "provider",
932
- hint: "Use your ChatGPT Plus/Pro/Team account (same login as Codex CLI)",
933
- // No form fields — credentials come from an OAuth flow. The /config page
934
- // renders a Connect/Disconnect card using /api/auth/openai instead of the
935
- // standard field inputs.
936
- fields: []
937
- },
938
960
  {
939
961
  id: "google",
940
962
  displayName: "Google (Gemini)",
@@ -977,6 +999,27 @@ var SERVICES = [
977
999
  hint: "Authenticates via AWS SDK env (AWS_PROFILE or AWS_ACCESS_KEY_ID/SECRET)",
978
1000
  fields: []
979
1001
  },
1002
+ {
1003
+ id: "github-copilot",
1004
+ displayName: "GitHub Copilot",
1005
+ category: "provider",
1006
+ hint: "Use a GitHub Copilot subscription to call Claude/GPT/Gemini via Copilot",
1007
+ fields: [
1008
+ {
1009
+ envVar: "COPILOT_GITHUB_TOKEN",
1010
+ kind: "secret",
1011
+ label: "GitHub token",
1012
+ description: "Generic GH_TOKEN/GITHUB_TOKEN are ignored \u2014 set this scoped variable explicitly (or run `bazilion auth copilot login` once available)."
1013
+ }
1014
+ ]
1015
+ },
1016
+ {
1017
+ id: "deepseek",
1018
+ displayName: "DeepSeek",
1019
+ category: "provider",
1020
+ hint: "DeepSeek V4 Flash / Pro \xB7 platform.deepseek.com",
1021
+ fields: [{ envVar: "DEEPSEEK_API_KEY", kind: "secret", label: "API key" }]
1022
+ },
980
1023
  {
981
1024
  id: "mistral",
982
1025
  displayName: "Mistral",
@@ -984,6 +1027,13 @@ var SERVICES = [
984
1027
  hint: "mistral.ai",
985
1028
  fields: [{ envVar: "MISTRAL_API_KEY", kind: "secret", label: "API key" }]
986
1029
  },
1030
+ {
1031
+ id: "xai",
1032
+ displayName: "xAI",
1033
+ category: "provider",
1034
+ hint: "Grok \xB7 x.ai",
1035
+ fields: [{ envVar: "XAI_API_KEY", kind: "secret", label: "API key" }]
1036
+ },
987
1037
  {
988
1038
  id: "groq",
989
1039
  displayName: "Groq",
@@ -998,11 +1048,46 @@ var SERVICES = [
998
1048
  fields: [{ envVar: "CEREBRAS_API_KEY", kind: "secret", label: "API key" }]
999
1049
  },
1000
1050
  {
1001
- id: "xai",
1002
- displayName: "xAI",
1051
+ id: "fireworks",
1052
+ displayName: "Fireworks AI",
1003
1053
  category: "provider",
1004
- hint: "Grok \xB7 x.ai",
1005
- fields: [{ envVar: "XAI_API_KEY", kind: "secret", label: "API key" }]
1054
+ hint: "DeepSeek/GLM/Kimi via fireworks.ai",
1055
+ fields: [{ envVar: "FIREWORKS_API_KEY", kind: "secret", label: "API key" }]
1056
+ },
1057
+ {
1058
+ id: "together",
1059
+ displayName: "Together AI",
1060
+ category: "provider",
1061
+ hint: "Open-weight models \xB7 together.ai",
1062
+ fields: [{ envVar: "TOGETHER_API_KEY", kind: "secret", label: "API key" }]
1063
+ },
1064
+ {
1065
+ id: "moonshotai",
1066
+ displayName: "Moonshot AI",
1067
+ category: "provider",
1068
+ hint: "Kimi K2/K2.5/K2.6 \xB7 platform.moonshot.ai",
1069
+ fields: [{ envVar: "MOONSHOT_API_KEY", kind: "secret", label: "API key" }]
1070
+ },
1071
+ {
1072
+ id: "kimi-coding",
1073
+ displayName: "Kimi Coding",
1074
+ category: "provider",
1075
+ hint: "Coding-tuned Kimi endpoint \xB7 platform.moonshot.cn",
1076
+ fields: [{ envVar: "KIMI_API_KEY", kind: "secret", label: "API key" }]
1077
+ },
1078
+ {
1079
+ id: "minimax",
1080
+ displayName: "MiniMax",
1081
+ category: "provider",
1082
+ hint: "MiniMax M2 family \xB7 platform.minimaxi.com",
1083
+ fields: [{ envVar: "MINIMAX_API_KEY", kind: "secret", label: "API key" }]
1084
+ },
1085
+ {
1086
+ id: "xiaomi",
1087
+ displayName: "Xiaomi MiMo",
1088
+ category: "provider",
1089
+ hint: "API billing endpoint \xB7 platform.xiaomimimo.com",
1090
+ fields: [{ envVar: "XIAOMI_API_KEY", kind: "secret", label: "API key" }]
1006
1091
  },
1007
1092
  {
1008
1093
  id: "zai",
@@ -1017,6 +1102,27 @@ var SERVICES = [
1017
1102
  hint: "Inference endpoints \xB7 huggingface.co",
1018
1103
  fields: [{ envVar: "HF_TOKEN", kind: "secret", label: "Access token", placeholder: "hf_..." }]
1019
1104
  },
1105
+ {
1106
+ id: "cloudflare-ai-gateway",
1107
+ displayName: "Cloudflare AI Gateway",
1108
+ category: "provider",
1109
+ hint: "Per-gateway routing to OpenAI/Anthropic/Workers AI",
1110
+ fields: [
1111
+ { envVar: "CLOUDFLARE_API_KEY", kind: "secret", label: "API key" },
1112
+ { envVar: "CLOUDFLARE_ACCOUNT_ID", kind: "config", label: "Account ID" },
1113
+ { envVar: "CLOUDFLARE_GATEWAY_ID", kind: "config", label: "Gateway ID" }
1114
+ ]
1115
+ },
1116
+ {
1117
+ id: "cloudflare-workers-ai",
1118
+ displayName: "Cloudflare Workers AI",
1119
+ category: "provider",
1120
+ hint: "Inference on Cloudflare Workers \xB7 ai.cloudflare.com",
1121
+ fields: [
1122
+ { envVar: "CLOUDFLARE_API_KEY", kind: "secret", label: "API key" },
1123
+ { envVar: "CLOUDFLARE_ACCOUNT_ID", kind: "config", label: "Account ID" }
1124
+ ]
1125
+ },
1020
1126
  {
1021
1127
  id: "openrouter",
1022
1128
  displayName: "OpenRouter",
@@ -1040,6 +1146,13 @@ var SERVICES = [
1040
1146
  }
1041
1147
  ]
1042
1148
  },
1149
+ {
1150
+ id: "opencode",
1151
+ displayName: "OpenCode",
1152
+ category: "provider",
1153
+ hint: "OpenAI-compatible proxy from the OpenCode CLI",
1154
+ fields: [{ envVar: "OPENCODE_API_KEY", kind: "secret", label: "API key" }]
1155
+ },
1043
1156
  {
1044
1157
  id: "lmstudio",
1045
1158
  displayName: "LM Studio",
@@ -1078,11 +1191,54 @@ var SERVICES = [
1078
1191
  }
1079
1192
  ]
1080
1193
  },
1194
+ {
1195
+ id: "llamacpp",
1196
+ displayName: "llama.cpp",
1197
+ category: "provider",
1198
+ hint: "Local inference \xB7 llama.cpp llama-server (OpenAI-compat /v1 endpoint)",
1199
+ fields: [
1200
+ {
1201
+ envVar: "LLAMACPP_URL",
1202
+ kind: "config",
1203
+ label: "Endpoint URL",
1204
+ placeholder: "http://127.0.0.1:8080/v1"
1205
+ },
1206
+ {
1207
+ envVar: "LLAMACPP_API_KEY",
1208
+ kind: "secret",
1209
+ label: "API key (only if started with --api-key)",
1210
+ description: "llama-server runs without auth by default. Set this only if you launched the server with the `--api-key KEY` flag."
1211
+ }
1212
+ ]
1213
+ },
1081
1214
  // --- Ancillary services (web search, etc) ---
1215
+ {
1216
+ id: "firecrawl",
1217
+ displayName: "Firecrawl",
1218
+ category: "service",
1219
+ group: "Web Search",
1220
+ hint: "web_fetch fallback for JS-heavy/blocked pages \xB7 firecrawl.dev (free tier available)",
1221
+ fields: [
1222
+ {
1223
+ envVar: "FIRECRAWL_API_KEY",
1224
+ kind: "secret",
1225
+ label: "API key",
1226
+ placeholder: "fc-...",
1227
+ description: "When set, web_fetch automatically falls back to Firecrawl if the primary Readability extraction yields too little content."
1228
+ },
1229
+ {
1230
+ envVar: "FIRECRAWL_URL",
1231
+ kind: "config",
1232
+ label: "Base URL (optional, for self-hosted)",
1233
+ placeholder: "https://api.firecrawl.dev"
1234
+ }
1235
+ ]
1236
+ },
1082
1237
  {
1083
1238
  id: "brave-search",
1084
1239
  displayName: "Brave Search",
1085
1240
  category: "service",
1241
+ group: "Web Search",
1086
1242
  hint: "Web search tool \xB7 free tier at brave.com/search/api/",
1087
1243
  fields: [{ envVar: "BRAVE_API_KEY", kind: "secret", label: "API key", placeholder: "BSA..." }]
1088
1244
  },
@@ -1090,6 +1246,7 @@ var SERVICES = [
1090
1246
  id: "searxng",
1091
1247
  displayName: "SearXNG",
1092
1248
  category: "service",
1249
+ group: "Web Search",
1093
1250
  hint: "Self-hosted meta-search engine \xB7 searxng.org",
1094
1251
  fields: [
1095
1252
  {
@@ -1516,7 +1673,9 @@ function revoke(db, id, when = Date.now()) {
1516
1673
  import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
1517
1674
  function readAuthFile(authFile) {
1518
1675
  if (!existsSync6(authFile)) {
1519
- throw new Error(`${authFile} not found. Start the daemon (\`bazilion serve\`) \u2014 it auto-bootstraps on first run.`);
1676
+ throw new Error(
1677
+ `${authFile} not found. Start the daemon (\`bazilion serve\`) \u2014 it auto-bootstraps on first run.`
1678
+ );
1520
1679
  }
1521
1680
  const raw = readFileSync4(authFile, "utf8");
1522
1681
  const parsed = JSON.parse(raw);
@@ -1724,7 +1883,7 @@ function isActiveAgent(agentId) {
1724
1883
  }
1725
1884
 
1726
1885
  // ../daemon/src/runtime/auth/openai-codex.ts
1727
- import { loginOpenAICodex, refreshOpenAICodexToken } from "@mariozechner/pi-ai/oauth";
1886
+ import { loginOpenAICodex, refreshOpenAICodexToken } from "@earendil-works/pi-ai/oauth";
1728
1887
  var OPENAI_CODEX_SECRET_KEY = "OPENAI_CODEX_OAUTH";
1729
1888
  var REFRESH_MARGIN_MS = 6e4;
1730
1889
  function readCredentials(db, authToken) {
@@ -1999,14 +2158,14 @@ import {
1999
2158
  ModelRegistry,
2000
2159
  SessionManager,
2001
2160
  SettingsManager
2002
- } from "@mariozechner/pi-coding-agent";
2161
+ } from "@earendil-works/pi-coding-agent";
2003
2162
 
2004
2163
  // ../daemon/src/runtime/providers/pi-adapter.ts
2005
2164
  import {
2006
2165
  getModel,
2007
2166
  streamSimple,
2008
2167
  Type
2009
- } from "@mariozechner/pi-ai";
2168
+ } from "@earendil-works/pi-ai";
2010
2169
  function defaultBaseUrlFor(providerName) {
2011
2170
  switch (providerName) {
2012
2171
  case "lmstudio":
@@ -2343,6 +2502,10 @@ function loadProviderConfigFromEnv(env = process.env, oauth) {
2343
2502
  ollama: {
2344
2503
  ...env.OLLAMA_URL !== void 0 ? { baseURL: env.OLLAMA_URL } : {},
2345
2504
  ...env.OLLAMA_API_KEY !== void 0 ? { apiKey: env.OLLAMA_API_KEY } : {}
2505
+ },
2506
+ llamacpp: {
2507
+ ...env.LLAMACPP_URL !== void 0 ? { baseURL: env.LLAMACPP_URL } : {},
2508
+ ...env.LLAMACPP_API_KEY !== void 0 ? { apiKey: env.LLAMACPP_API_KEY } : {}
2346
2509
  }
2347
2510
  };
2348
2511
  if (env.ANTHROPIC_API_KEY || env.ANTHROPIC_OAUTH_TOKEN) {
@@ -2365,6 +2528,28 @@ function loadProviderConfigFromEnv(env = process.env, oauth) {
2365
2528
  if (env.HF_TOKEN) config.huggingface = { apiKey: env.HF_TOKEN };
2366
2529
  if (env.OPENROUTER_API_KEY) config.openrouter = { apiKey: env.OPENROUTER_API_KEY };
2367
2530
  if (env.AI_GATEWAY_API_KEY) config.vercelAiGateway = { apiKey: env.AI_GATEWAY_API_KEY };
2531
+ if (env.DEEPSEEK_API_KEY) config.deepseek = { apiKey: env.DEEPSEEK_API_KEY };
2532
+ if (env.FIREWORKS_API_KEY) config.fireworks = { apiKey: env.FIREWORKS_API_KEY };
2533
+ if (env.TOGETHER_API_KEY) config.together = { apiKey: env.TOGETHER_API_KEY };
2534
+ if (env.MOONSHOT_API_KEY) config.moonshotai = { apiKey: env.MOONSHOT_API_KEY };
2535
+ if (env.KIMI_API_KEY) config.kimiCoding = { apiKey: env.KIMI_API_KEY };
2536
+ if (env.MINIMAX_API_KEY) config.minimax = { apiKey: env.MINIMAX_API_KEY };
2537
+ if (env.XIAOMI_API_KEY) config.xiaomi = { apiKey: env.XIAOMI_API_KEY };
2538
+ if (env.OPENCODE_API_KEY) config.opencode = { apiKey: env.OPENCODE_API_KEY };
2539
+ if (env.COPILOT_GITHUB_TOKEN) config.githubCopilot = { apiKey: env.COPILOT_GITHUB_TOKEN };
2540
+ if (env.CLOUDFLARE_API_KEY && env.CLOUDFLARE_ACCOUNT_ID) {
2541
+ config.cloudflareWorkersAi = {
2542
+ apiKey: env.CLOUDFLARE_API_KEY,
2543
+ accountId: env.CLOUDFLARE_ACCOUNT_ID
2544
+ };
2545
+ if (env.CLOUDFLARE_GATEWAY_ID) {
2546
+ config.cloudflareAiGateway = {
2547
+ apiKey: env.CLOUDFLARE_API_KEY,
2548
+ accountId: env.CLOUDFLARE_ACCOUNT_ID,
2549
+ gatewayId: env.CLOUDFLARE_GATEWAY_ID
2550
+ };
2551
+ }
2552
+ }
2368
2553
  if (oauth && hasCredentials(oauth.db, oauth.authToken)) {
2369
2554
  config.openaiCodex = oauth;
2370
2555
  }
@@ -2523,6 +2708,113 @@ var PROVIDERS = {
2523
2708
  }),
2524
2709
  hint: "AI_GATEWAY_API_KEY"
2525
2710
  },
2711
+ deepseek: {
2712
+ configured: (c) => !!c.deepseek,
2713
+ build: (c) => piProvider({
2714
+ providerName: "deepseek",
2715
+ fallbackApi: "openai-completions",
2716
+ apiKey: c.deepseek?.apiKey,
2717
+ baseUrl: c.deepseek?.baseURL
2718
+ }),
2719
+ hint: "DEEPSEEK_API_KEY"
2720
+ },
2721
+ fireworks: {
2722
+ configured: (c) => !!c.fireworks,
2723
+ build: (c) => piProvider({
2724
+ providerName: "fireworks",
2725
+ fallbackApi: "anthropic-messages",
2726
+ apiKey: c.fireworks?.apiKey,
2727
+ baseUrl: c.fireworks?.baseURL
2728
+ }),
2729
+ hint: "FIREWORKS_API_KEY"
2730
+ },
2731
+ together: {
2732
+ configured: (c) => !!c.together,
2733
+ build: (c) => piProvider({
2734
+ providerName: "together",
2735
+ fallbackApi: "openai-completions",
2736
+ apiKey: c.together?.apiKey,
2737
+ baseUrl: c.together?.baseURL
2738
+ }),
2739
+ hint: "TOGETHER_API_KEY"
2740
+ },
2741
+ moonshotai: {
2742
+ configured: (c) => !!c.moonshotai,
2743
+ build: (c) => piProvider({
2744
+ providerName: "moonshotai",
2745
+ fallbackApi: "openai-completions",
2746
+ apiKey: c.moonshotai?.apiKey,
2747
+ baseUrl: c.moonshotai?.baseURL
2748
+ }),
2749
+ hint: "MOONSHOT_API_KEY"
2750
+ },
2751
+ "kimi-coding": {
2752
+ configured: (c) => !!c.kimiCoding,
2753
+ build: (c) => piProvider({
2754
+ providerName: "kimi-coding",
2755
+ fallbackApi: "anthropic-messages",
2756
+ apiKey: c.kimiCoding?.apiKey,
2757
+ baseUrl: c.kimiCoding?.baseURL
2758
+ }),
2759
+ hint: "KIMI_API_KEY"
2760
+ },
2761
+ minimax: {
2762
+ configured: (c) => !!c.minimax,
2763
+ build: (c) => piProvider({
2764
+ providerName: "minimax",
2765
+ fallbackApi: "anthropic-messages",
2766
+ apiKey: c.minimax?.apiKey,
2767
+ baseUrl: c.minimax?.baseURL
2768
+ }),
2769
+ hint: "MINIMAX_API_KEY"
2770
+ },
2771
+ xiaomi: {
2772
+ configured: (c) => !!c.xiaomi,
2773
+ build: (c) => piProvider({
2774
+ providerName: "xiaomi",
2775
+ fallbackApi: "openai-completions",
2776
+ apiKey: c.xiaomi?.apiKey,
2777
+ baseUrl: c.xiaomi?.baseURL
2778
+ }),
2779
+ hint: "XIAOMI_API_KEY"
2780
+ },
2781
+ opencode: {
2782
+ configured: (c) => !!c.opencode,
2783
+ build: (c) => piProvider({
2784
+ providerName: "opencode",
2785
+ fallbackApi: "openai-completions",
2786
+ apiKey: c.opencode?.apiKey,
2787
+ baseUrl: c.opencode?.baseURL
2788
+ }),
2789
+ hint: "OPENCODE_API_KEY"
2790
+ },
2791
+ "github-copilot": {
2792
+ configured: (c) => !!c.githubCopilot,
2793
+ build: (c) => piProvider({
2794
+ providerName: "github-copilot",
2795
+ fallbackApi: "anthropic-messages",
2796
+ apiKey: c.githubCopilot?.apiKey
2797
+ }),
2798
+ hint: "COPILOT_GITHUB_TOKEN (generic GH_TOKEN/GITHUB_TOKEN are ignored)"
2799
+ },
2800
+ "cloudflare-ai-gateway": {
2801
+ configured: (c) => !!c.cloudflareAiGateway,
2802
+ build: (c) => piProvider({
2803
+ providerName: "cloudflare-ai-gateway",
2804
+ fallbackApi: "anthropic-messages",
2805
+ apiKey: c.cloudflareAiGateway?.apiKey
2806
+ }),
2807
+ hint: "CLOUDFLARE_API_KEY + CLOUDFLARE_ACCOUNT_ID + CLOUDFLARE_GATEWAY_ID"
2808
+ },
2809
+ "cloudflare-workers-ai": {
2810
+ configured: (c) => !!c.cloudflareWorkersAi,
2811
+ build: (c) => piProvider({
2812
+ providerName: "cloudflare-workers-ai",
2813
+ fallbackApi: "openai-completions",
2814
+ apiKey: c.cloudflareWorkersAi?.apiKey
2815
+ }),
2816
+ hint: "CLOUDFLARE_API_KEY + CLOUDFLARE_ACCOUNT_ID"
2817
+ },
2526
2818
  lmstudio: {
2527
2819
  configured: () => true,
2528
2820
  build: (c) => piProvider({
@@ -2542,6 +2834,20 @@ var PROVIDERS = {
2542
2834
  baseUrl: c.ollama?.baseURL ?? "http://127.0.0.1:11434/v1"
2543
2835
  }),
2544
2836
  hint: "OLLAMA_URL (default http://127.0.0.1:11434/v1)"
2837
+ },
2838
+ llamacpp: {
2839
+ // Like lmstudio/ollama, always considered "configured" — the daemon
2840
+ // can't tell if llama-server is actually running until a request hits
2841
+ // it. Falls back to the documented default port + a placeholder
2842
+ // apiKey (llama-server ignores it unless --api-key was passed).
2843
+ configured: () => true,
2844
+ build: (c) => piProvider({
2845
+ providerName: "llamacpp",
2846
+ fallbackApi: "openai-completions",
2847
+ apiKey: c.llamacpp?.apiKey ?? "no-key",
2848
+ baseUrl: c.llamacpp?.baseURL ?? "http://127.0.0.1:8080/v1"
2849
+ }),
2850
+ hint: "LLAMACPP_URL (default http://127.0.0.1:8080/v1)"
2545
2851
  }
2546
2852
  };
2547
2853
  function createProviderRegistry(config, opts = {}) {
@@ -2606,13 +2912,11 @@ var CONTEXT_FILE_ORDER = [
2606
2912
  "SOUL.md",
2607
2913
  "TOOLS.md",
2608
2914
  "IDENTITY.md",
2609
- "HEARTBEAT.md",
2610
- "BOOTSTRAP.md"
2915
+ "HEARTBEAT.md"
2611
2916
  ];
2612
2917
  function buildSystemPrompt(agent) {
2613
2918
  const parts = [];
2614
2919
  const contextBlocks = [];
2615
- let bootstrapPresent = false;
2616
2920
  for (const file of CONTEXT_FILE_ORDER) {
2617
2921
  const path = join11(agent.agent.dir, file);
2618
2922
  if (!existsSync10(path)) continue;
@@ -2621,23 +2925,39 @@ function buildSystemPrompt(agent) {
2621
2925
  contextBlocks.push(`## ${file}
2622
2926
 
2623
2927
  ${content}`);
2624
- if (file === "BOOTSTRAP.md") bootstrapPresent = true;
2625
2928
  }
2626
2929
  if (contextBlocks.length > 0) {
2627
2930
  parts.push(`# Project Context
2628
2931
 
2629
2932
  ${contextBlocks.join("\n\n")}`);
2630
2933
  }
2631
- if (bootstrapPresent) {
2632
- parts.push(
2633
- "NOTE: This is your first session. After you have completed the bootstrap conversation, call the `bootstrap_done` tool to delete BOOTSTRAP.md."
2634
- );
2934
+ const bootstrapPath = join11(agent.agent.dir, "BOOTSTRAP.md");
2935
+ if (existsSync10(bootstrapPath)) {
2936
+ const bootstrap2 = readFileSync8(bootstrapPath, "utf8").trimEnd();
2937
+ if (bootstrap2) {
2938
+ parts.push(
2939
+ [
2940
+ "# First-Run Ritual",
2941
+ "",
2942
+ "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.",
2943
+ "",
2944
+ "## Hard rules",
2945
+ "- Your first reply is ONLY a greeting + ONE question. No tool calls. Wait for the human to answer.",
2946
+ "- Each subsequent turn: at most one new question. Wait between turns.",
2947
+ "- Only after the ritual is complete (you have enough to write IDENTITY.md): call `home_write` once, then `bootstrap_done`.",
2948
+ "",
2949
+ "## BOOTSTRAP.md",
2950
+ "",
2951
+ bootstrap2
2952
+ ].join("\n")
2953
+ );
2954
+ }
2635
2955
  }
2636
2956
  parts.push(
2637
2957
  [
2638
2958
  "# Agent Home",
2639
2959
  "",
2640
- "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.",
2960
+ "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.",
2641
2961
  "",
2642
2962
  "- To change who you are (name, vibe, personality, how you behave): use `home_write`.",
2643
2963
  "- To inspect exact wording of your own files: use `home_read` or `home_list`.",
@@ -2664,13 +2984,19 @@ You have access to the following skills: ${agent.skills.join(", ")}.`
2664
2984
  parts.push(
2665
2985
  `# About the User
2666
2986
 
2667
- 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.
2987
+ 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.**
2668
2988
 
2669
2989
  ${agent.group.userMd.trim()}`
2670
2990
  );
2991
+ } else {
2992
+ parts.push(
2993
+ `# About the User
2994
+
2995
+ 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.**`
2996
+ );
2671
2997
  }
2672
2998
  parts.push(
2673
- "# 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."
2999
+ "# 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.**"
2674
3000
  );
2675
3001
  return parts.join("\n\n---\n\n");
2676
3002
  }
@@ -2811,7 +3137,7 @@ function memoryTools(memory) {
2811
3137
  {
2812
3138
  def: {
2813
3139
  name: "memory_write",
2814
- 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".',
3140
+ 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".',
2815
3141
  parameters: {
2816
3142
  type: "object",
2817
3143
  properties: {
@@ -2897,7 +3223,7 @@ function messagingTools(host2, fromAgentId) {
2897
3223
  {
2898
3224
  def: {
2899
3225
  name: "send_message",
2900
- description: "Send a message to another agent. Use the recipient's agent id (UUID).",
3226
+ 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.`,
2901
3227
  parameters: {
2902
3228
  type: "object",
2903
3229
  properties: {
@@ -3001,6 +3327,64 @@ function messagingTools(host2, fromAgentId) {
3001
3327
  ];
3002
3328
  }
3003
3329
 
3330
+ // ../daemon/src/runtime/tools/user-md.ts
3331
+ function userMdTools(host2, groupId) {
3332
+ return [
3333
+ {
3334
+ def: {
3335
+ name: "user_md_get",
3336
+ 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`.",
3337
+ parameters: {
3338
+ type: "object",
3339
+ properties: {}
3340
+ }
3341
+ },
3342
+ async invoke() {
3343
+ const { content, etag } = await host2.get(groupId);
3344
+ const body = content.length > 0 ? content : "(USER.md is empty)";
3345
+ return `${body}
3346
+
3347
+ ---
3348
+ etag: ${etag}`;
3349
+ }
3350
+ },
3351
+ {
3352
+ def: {
3353
+ name: "user_md_write",
3354
+ 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.",
3355
+ parameters: {
3356
+ type: "object",
3357
+ properties: {
3358
+ content: {
3359
+ type: "string",
3360
+ description: "Full new contents of USER.md (this is a complete replacement, NOT an append). Include everything you want to keep."
3361
+ },
3362
+ if_match: {
3363
+ type: "string",
3364
+ description: "The etag returned by your most recent `user_md_get`. The write fails if USER.md changed in the meantime."
3365
+ }
3366
+ },
3367
+ required: ["content", "if_match"]
3368
+ }
3369
+ },
3370
+ async invoke(args) {
3371
+ const content = String(args.content ?? "");
3372
+ const ifMatch = String(args.if_match ?? "");
3373
+ if (!ifMatch) {
3374
+ throw new Error(
3375
+ 'user_md_write: "if_match" is required \u2014 call user_md_get first to obtain the current etag.'
3376
+ );
3377
+ }
3378
+ const { etag, totalBytes } = await host2.write(groupId, content, ifMatch);
3379
+ return `wrote USER.md (${totalBytes} bytes, new etag: ${etag})`;
3380
+ }
3381
+ }
3382
+ ];
3383
+ }
3384
+
3385
+ // ../daemon/src/runtime/tools/web.ts
3386
+ import { fetch as undiciFetch2 } from "undici";
3387
+
3004
3388
  // ../daemon/src/runtime/tools/web-extract.ts
3005
3389
  import { Readability } from "@mozilla/readability";
3006
3390
  import { parseHTML } from "linkedom";
@@ -3081,7 +3465,7 @@ function extractReadable(html, url, mode) {
3081
3465
  // ../daemon/src/runtime/tools/web-ssrf.ts
3082
3466
  import { lookup as dnsLookupCb } from "dns";
3083
3467
  import { lookup as dnsLookup } from "dns/promises";
3084
- import { Agent } from "undici";
3468
+ import { Agent, fetch as undiciFetch } from "undici";
3085
3469
  var SsrFBlockedError = class extends Error {
3086
3470
  constructor(message) {
3087
3471
  super(message);
@@ -3190,7 +3574,7 @@ async function closeDispatcher(d) {
3190
3574
  }
3191
3575
  }
3192
3576
  async function guardedFetch(opts) {
3193
- const fetcher = opts.fetchImpl ?? globalThis.fetch;
3577
+ const fetcher = opts.fetchImpl ?? undiciFetch;
3194
3578
  const maxRedirects = opts.maxRedirects ?? 3;
3195
3579
  const abortController = new AbortController();
3196
3580
  const timeoutId = opts.timeoutMs ? setTimeout(() => abortController.abort(new Error("timeout")), opts.timeoutMs) : null;
@@ -3271,7 +3655,10 @@ var DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7_2) AppleWe
3271
3655
  var DEFAULT_CACHE_TTL_MS = 15 * 6e4;
3272
3656
  var DEFAULT_CACHE_MAX = 100;
3273
3657
  var DEFAULT_MAX_LENGTH = 2e4;
3274
- var DEFAULT_TIMEOUT_MS = 2e4;
3658
+ var DEFAULT_TIMEOUT_MS = 3e4;
3659
+ var DEFAULT_MAX_BODY_BYTES = 3 * 1024 * 1024;
3660
+ var FIRECRAWL_FALLBACK_THRESHOLD = 200;
3661
+ var FIRECRAWL_DEFAULT_URL = "https://api.firecrawl.dev";
3275
3662
  function stripHtml(s) {
3276
3663
  return s.replace(/<[^>]*>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&nbsp;/g, " ").trim();
3277
3664
  }
@@ -3305,6 +3692,92 @@ async function searxngSearch(query, limit, baseURL, fetchFn) {
3305
3692
  snippet: r.content ?? ""
3306
3693
  }));
3307
3694
  }
3695
+ function describeError(err) {
3696
+ if (!(err instanceof Error)) return String(err);
3697
+ const parts = [];
3698
+ const seen = /* @__PURE__ */ new Set();
3699
+ let cur = err;
3700
+ while (cur instanceof Error && !seen.has(cur)) {
3701
+ seen.add(cur);
3702
+ const code = cur.code;
3703
+ parts.push(code ? `[${code}] ${cur.message}` : cur.message);
3704
+ cur = cur.cause;
3705
+ }
3706
+ return parts.join(" \u2014 cause: ");
3707
+ }
3708
+ async function readBodyCapped(res, maxBytes) {
3709
+ const ct = res.headers.get("content-type") ?? "";
3710
+ const charset = /charset=([^;]+)/i.exec(ct)?.[1]?.trim().toLowerCase() || "utf-8";
3711
+ const decoder = new TextDecoder(charset, { fatal: false });
3712
+ const reader = res.body?.getReader();
3713
+ if (!reader) {
3714
+ const text = await res.text();
3715
+ if (text.length > maxBytes) return { text: text.slice(0, maxBytes), truncated: true };
3716
+ return { text, truncated: false };
3717
+ }
3718
+ const chunks = [];
3719
+ let total = 0;
3720
+ let truncated = false;
3721
+ while (true) {
3722
+ const { value, done } = await reader.read();
3723
+ if (done) break;
3724
+ if (!value) continue;
3725
+ if (total + value.byteLength > maxBytes) {
3726
+ const keep = maxBytes - total;
3727
+ if (keep > 0) chunks.push(value.subarray(0, keep));
3728
+ truncated = true;
3729
+ try {
3730
+ await reader.cancel();
3731
+ } catch {
3732
+ }
3733
+ break;
3734
+ }
3735
+ chunks.push(value);
3736
+ total += value.byteLength;
3737
+ }
3738
+ const sum = chunks.reduce((s, c) => s + c.byteLength, 0);
3739
+ const merged = new Uint8Array(sum);
3740
+ let off = 0;
3741
+ for (const c of chunks) {
3742
+ merged.set(c, off);
3743
+ off += c.byteLength;
3744
+ }
3745
+ return { text: decoder.decode(merged), truncated };
3746
+ }
3747
+ async function firecrawlScrape(url, mode, env, fetchFn, timeoutMs) {
3748
+ const apiKey = env.FIRECRAWL_API_KEY;
3749
+ if (!apiKey) return null;
3750
+ const base = (env.FIRECRAWL_URL ?? FIRECRAWL_DEFAULT_URL).replace(/\/$/, "");
3751
+ const ac = new AbortController();
3752
+ const t = setTimeout(() => ac.abort(new Error("firecrawl timeout")), timeoutMs);
3753
+ try {
3754
+ const res = await fetchFn(`${base}/v1/scrape`, {
3755
+ method: "POST",
3756
+ headers: {
3757
+ "content-type": "application/json",
3758
+ authorization: `Bearer ${apiKey}`,
3759
+ accept: "application/json"
3760
+ },
3761
+ body: JSON.stringify({
3762
+ url,
3763
+ formats: ["markdown"],
3764
+ onlyMainContent: true
3765
+ }),
3766
+ signal: ac.signal
3767
+ });
3768
+ if (!res.ok) return null;
3769
+ const body = await res.json();
3770
+ if (!body.success || !body.data?.markdown) return null;
3771
+ const md = body.data.markdown;
3772
+ const title = body.data.metadata?.title;
3773
+ const text = mode === "text" ? markdownToPlain(md) : md;
3774
+ return title ? { text, title } : { text };
3775
+ } catch {
3776
+ return null;
3777
+ } finally {
3778
+ clearTimeout(t);
3779
+ }
3780
+ }
3308
3781
  function cacheGet(cache, key) {
3309
3782
  const entry = cache.get(key);
3310
3783
  if (!entry) return null;
@@ -3325,12 +3798,14 @@ function cacheSet(cache, key, value, ttlMs, maxEntries) {
3325
3798
  }
3326
3799
  }
3327
3800
  function webTools(opts) {
3328
- const fetchFn = opts?.fetchImpl ?? fetch;
3801
+ const fetchFn = opts?.fetchImpl ?? undiciFetch2;
3329
3802
  const env = opts?.env ?? process.env;
3330
3803
  const allowPrivate = opts?.allowPrivate ?? false;
3331
3804
  const cacheTtlMs = opts?.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
3332
3805
  const cacheMax = opts?.cacheMax ?? DEFAULT_CACHE_MAX;
3333
3806
  const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
3807
+ const maxBodyBytes = opts?.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
3808
+ const firecrawlDisabled = opts?.firecrawlDisabled ?? false;
3334
3809
  const cache = /* @__PURE__ */ new Map();
3335
3810
  return [
3336
3811
  {
@@ -3368,7 +3843,7 @@ function webTools(opts) {
3368
3843
  {
3369
3844
  def: {
3370
3845
  name: "web_fetch",
3371
- description: "Fetch a URL and return its readable content. HTML is extracted via Readability and converted to markdown. Results are cached for 15 minutes.",
3846
+ 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.",
3372
3847
  parameters: {
3373
3848
  type: "object",
3374
3849
  properties: {
@@ -3394,7 +3869,7 @@ function webTools(opts) {
3394
3869
  const cacheKey = `${mode}|${url}`;
3395
3870
  const cached = cacheGet(cache, cacheKey);
3396
3871
  if (cached) return formatOutput(cached, maxLen);
3397
- let result;
3872
+ let result = null;
3398
3873
  try {
3399
3874
  result = await guardedFetch({
3400
3875
  url,
@@ -3409,18 +3884,14 @@ function webTools(opts) {
3409
3884
  }
3410
3885
  }
3411
3886
  });
3412
- } catch (err) {
3413
- if (err instanceof SsrFBlockedError) throw new Error(`web_fetch: ${err.message}`);
3414
- throw err;
3415
- }
3416
- try {
3417
3887
  if (!result.response.ok) {
3418
- throw new Error(`web_fetch: ${result.response.status} ${result.response.statusText}`);
3888
+ throw new Error(`${result.response.status} ${result.response.statusText}`);
3419
3889
  }
3420
3890
  const ct = result.response.headers.get("content-type") ?? "";
3421
- const body = await result.response.text();
3891
+ const isHtml = ct.includes("text/html") || ct.includes("xhtml");
3892
+ const { text: body, truncated } = await readBodyCapped(result.response, maxBodyBytes);
3422
3893
  let extracted;
3423
- if (ct.includes("text/html") || ct.includes("xhtml")) {
3894
+ if (isHtml) {
3424
3895
  extracted = extractReadable(body, result.finalUrl, mode);
3425
3896
  } else if (ct.includes("application/json")) {
3426
3897
  try {
@@ -3431,10 +3902,38 @@ function webTools(opts) {
3431
3902
  } else {
3432
3903
  extracted = { text: body };
3433
3904
  }
3905
+ if (isHtml && !firecrawlDisabled && extracted.text.length < FIRECRAWL_FALLBACK_THRESHOLD) {
3906
+ const rescued = await firecrawlScrape(
3907
+ result.finalUrl,
3908
+ mode,
3909
+ env,
3910
+ fetchFn,
3911
+ timeoutMs
3912
+ );
3913
+ if (rescued) {
3914
+ extracted = {
3915
+ ...rescued,
3916
+ text: `${rescued.text}
3917
+
3918
+ [content rendered via Firecrawl fallback \u2014 primary extraction returned ${extracted.text.length} chars]`
3919
+ };
3920
+ }
3921
+ }
3922
+ if (truncated) {
3923
+ extracted = {
3924
+ ...extracted,
3925
+ text: `${extracted.text}
3926
+
3927
+ [raw body truncated at ${maxBodyBytes} bytes before extraction \u2014 page exceeded the size cap]`
3928
+ };
3929
+ }
3434
3930
  cacheSet(cache, cacheKey, extracted, cacheTtlMs, cacheMax);
3435
3931
  return formatOutput(extracted, maxLen);
3932
+ } catch (err) {
3933
+ if (err instanceof SsrFBlockedError) throw new Error(`web_fetch: ${err.message}`);
3934
+ throw new Error(`web_fetch: ${describeError(err)}`);
3436
3935
  } finally {
3437
- await result.release();
3936
+ if (result) await result.release();
3438
3937
  }
3439
3938
  }
3440
3939
  }
@@ -3481,13 +3980,16 @@ function createBazilionCustomTools(opts) {
3481
3980
  if (opts.messagingHost) {
3482
3981
  handlers.push(...messagingTools(opts.messagingHost, opts.agent.agent.id));
3483
3982
  }
3983
+ if (opts.userMdHost) {
3984
+ handlers.push(...userMdTools(opts.userMdHost, opts.agent.group.id));
3985
+ }
3484
3986
  return handlers.map(ourToolToPiTool);
3485
3987
  }
3486
3988
 
3487
3989
  // ../daemon/src/runtime/pi/session.ts
3488
3990
  var BUILTIN_TOOL_NAMES = ["read", "bash", "edit", "write", "grep", "find", "ls"];
3489
3991
  async function createBazilionSession(opts) {
3490
- const { agent, paths, env, memory, enabledProviders, messagingHost, refreshApiKey } = opts;
3992
+ const { agent, paths, env, memory, enabledProviders, messagingHost, userMdHost, refreshApiKey } = opts;
3491
3993
  const { providerName, modelId } = splitModelString(agent.model);
3492
3994
  if (enabledProviders.size > 0 && !enabledProviders.has(providerName)) {
3493
3995
  throw new Error(`${providerName} provider is disabled \u2014 enable it on the /config page`);
@@ -3524,12 +4026,17 @@ async function createBazilionSession(opts) {
3524
4026
  const sessionManager = existing ? SessionManager.open(existing, sessionDir, cwd) : SessionManager.create(cwd, sessionDir);
3525
4027
  const settingsManager = SettingsManager.inMemory({
3526
4028
  compaction: { enabled: false },
3527
- retry: { enabled: true, maxRetries: 2, baseDelayMs: 500, maxDelayMs: 8e3 }
4029
+ retry: {
4030
+ enabled: true,
4031
+ maxRetries: 2,
4032
+ baseDelayMs: 500,
4033
+ provider: { maxRetryDelayMs: 8e3 }
4034
+ }
3528
4035
  });
3529
4036
  const bazilionPrompt = buildSystemPrompt(agent);
3530
4037
  const resourceLoader = createBazilionResourceLoader(bazilionPrompt);
3531
4038
  await resourceLoader.reload();
3532
- const customTools = createBazilionCustomTools({ agent, memory, messagingHost, env });
4039
+ const customTools = createBazilionCustomTools({ agent, memory, messagingHost, userMdHost, env });
3533
4040
  const allowedTools = [...BUILTIN_TOOL_NAMES, ...customTools.map((t) => t.name)];
3534
4041
  const { session } = await createAgentSession({
3535
4042
  cwd,
@@ -3705,7 +4212,8 @@ function findMostRecent(sessionDir) {
3705
4212
  }
3706
4213
 
3707
4214
  // ../daemon/src/runtime/providers/catalog.ts
3708
- import { getModels as piGetModels } from "@mariozechner/pi-ai";
4215
+ import { getModels as piGetModels } from "@earendil-works/pi-ai";
4216
+ import { fetch as undiciFetch3 } from "undici";
3709
4217
  var REGISTRY_TO_PI = {
3710
4218
  bedrock: "amazon-bedrock",
3711
4219
  "azure-openai": "azure-openai-responses"
@@ -3716,6 +4224,8 @@ function liveEndpointFor(providerName, env) {
3716
4224
  return env.LMSTUDIO_URL ?? "http://127.0.0.1:1234/v1";
3717
4225
  case "ollama":
3718
4226
  return env.OLLAMA_URL ?? "http://127.0.0.1:11434/v1";
4227
+ case "llamacpp":
4228
+ return env.LLAMACPP_URL ?? "http://127.0.0.1:8080/v1";
3719
4229
  case "openrouter":
3720
4230
  return "https://openrouter.ai/api/v1";
3721
4231
  case "vercel-ai-gateway":
@@ -3746,7 +4256,7 @@ async function fetchModelsFrom(baseURL, apiKey, signal) {
3746
4256
  try {
3747
4257
  const headers = { accept: "application/json" };
3748
4258
  if (apiKey) headers.authorization = `Bearer ${apiKey}`;
3749
- const res = await fetch(`${baseURL.replace(/\/$/, "")}/models`, { headers, signal });
4259
+ const res = await undiciFetch3(`${baseURL.replace(/\/$/, "")}/models`, { headers, signal });
3750
4260
  if (!res.ok) return { models: [], error: `${res.status} ${res.statusText}` };
3751
4261
  const body = await res.json();
3752
4262
  const ids = (body?.data ?? []).map((m) => m.id).filter((id) => !!id);
@@ -3819,7 +4329,9 @@ async function* spawnWorkerTurn(spec, opts = {}) {
3819
4329
  env: opts.env ?? process.env,
3820
4330
  stdio: ["pipe", "pipe", "inherit", "ipc"]
3821
4331
  });
3822
- if (opts.messagingHost) attachIpcHandler(child, opts.messagingHost);
4332
+ if (opts.messagingHost || opts.userMdHost) {
4333
+ attachIpcHandler(child, opts.messagingHost, opts.userMdHost);
4334
+ }
3823
4335
  child.stdin?.write(JSON.stringify(spec));
3824
4336
  child.stdin?.end();
3825
4337
  const grace = opts.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
@@ -3899,10 +4411,10 @@ function parseFrame(line) {
3899
4411
  return { kind: "fatal", error: `worker emitted malformed frame: ${line.slice(0, 200)}` };
3900
4412
  }
3901
4413
  }
3902
- function attachIpcHandler(child, host2) {
4414
+ function attachIpcHandler(child, messagingHost, userMdHost) {
3903
4415
  child.on("message", (msg) => {
3904
4416
  if (!isIpcRequest(msg)) return;
3905
- void dispatch(msg, host2).then((reply) => {
4417
+ void dispatch(msg, messagingHost, userMdHost).then((reply) => {
3906
4418
  try {
3907
4419
  child.send?.(reply);
3908
4420
  } catch {
@@ -3915,25 +4427,49 @@ function isIpcRequest(msg) {
3915
4427
  const m = msg;
3916
4428
  return m.type === "rpc" && typeof m.id === "string" && typeof m.method === "string";
3917
4429
  }
3918
- async function dispatch(req, host2) {
4430
+ function requireMessagingHost(host2, method) {
4431
+ if (!host2) throw new Error(`worker called messaging method "${method}" without a messagingHost`);
4432
+ return host2;
4433
+ }
4434
+ function requireUserMdHost(host2, method) {
4435
+ if (!host2) throw new Error(`worker called user_md method "${method}" without a userMdHost`);
4436
+ return host2;
4437
+ }
4438
+ async function dispatch(req, messagingHost, userMdHost) {
3919
4439
  try {
3920
4440
  let result;
3921
4441
  switch (req.method) {
3922
4442
  case "agentExists":
3923
- result = await host2.agentExists(req.args.agentId);
4443
+ result = await requireMessagingHost(messagingHost, req.method).agentExists(req.args.agentId);
3924
4444
  break;
3925
4445
  case "sendMessage":
3926
- result = await host2.sendMessage(req.args);
4446
+ result = await requireMessagingHost(messagingHost, req.method).sendMessage(req.args);
3927
4447
  break;
3928
4448
  case "listInbox":
3929
- result = await host2.listInbox(req.args.agentId, { unreadOnly: req.args.unreadOnly });
4449
+ result = await requireMessagingHost(messagingHost, req.method).listInbox(
4450
+ req.args.agentId,
4451
+ { unreadOnly: req.args.unreadOnly }
4452
+ );
3930
4453
  break;
3931
4454
  case "markRead":
3932
- await host2.markRead(req.args.messageId);
4455
+ await requireMessagingHost(messagingHost, req.method).markRead(req.args.messageId);
3933
4456
  result = null;
3934
4457
  break;
3935
4458
  case "findReplies":
3936
- result = await host2.findReplies(req.args.agentId, req.args.replyTo);
4459
+ result = await requireMessagingHost(messagingHost, req.method).findReplies(
4460
+ req.args.agentId,
4461
+ req.args.replyTo
4462
+ );
4463
+ break;
4464
+ case "userMdGet":
4465
+ result = await requireUserMdHost(userMdHost, req.method).get(req.args.groupId);
4466
+ break;
4467
+ case "userMdWrite":
4468
+ result = await requireUserMdHost(userMdHost, req.method).write(
4469
+ req.args.groupId,
4470
+ req.args.content,
4471
+ req.args.ifMatch
4472
+ );
3937
4473
  break;
3938
4474
  }
3939
4475
  return { type: "rpc-reply", id: req.id, ok: true, result };
@@ -3986,6 +4522,40 @@ function createDbMessagingHost(db) {
3986
4522
  };
3987
4523
  }
3988
4524
 
4525
+ // ../daemon/src/lib/user-md-host.ts
4526
+ import { createHash as createHash2 } from "crypto";
4527
+ var USER_MD_MAX_BYTES = 12e3;
4528
+ function computeEtag(content) {
4529
+ return createHash2("sha256").update(content, "utf8").digest("hex").slice(0, 16);
4530
+ }
4531
+ function createDbUserMdHost(db, paths) {
4532
+ return {
4533
+ get(groupId) {
4534
+ const group = groups_exports.get(db, groupId, paths);
4535
+ if (!group) throw new Error(`group not found: ${groupId}`);
4536
+ return { content: group.userMd, etag: computeEtag(group.userMd) };
4537
+ },
4538
+ write(groupId, content, ifMatch) {
4539
+ const group = groups_exports.get(db, groupId, paths);
4540
+ if (!group) throw new Error(`group not found: ${groupId}`);
4541
+ const currentEtag = computeEtag(group.userMd);
4542
+ if (currentEtag !== ifMatch) {
4543
+ throw new Error(
4544
+ `etag mismatch \u2014 USER.md was updated by another agent. Current etag is ${currentEtag} (you passed ${ifMatch}). Call user_md_get again to re-read, merge your change, and retry.`
4545
+ );
4546
+ }
4547
+ const bytes = Buffer.byteLength(content, "utf8");
4548
+ if (bytes > USER_MD_MAX_BYTES) {
4549
+ throw new Error(
4550
+ `USER.md would exceed the ${USER_MD_MAX_BYTES}-byte cap (you tried to write ${bytes}). Trim your content or ask the human to compact via the web UI.`
4551
+ );
4552
+ }
4553
+ groups_exports.setUserMd(db, groupId, content);
4554
+ return { etag: computeEtag(content), totalBytes: bytes };
4555
+ }
4556
+ };
4557
+ }
4558
+
3989
4559
  // ../daemon/src/lib/agent-turn.ts
3990
4560
  async function* runAgentTurn(agentId, message, opts = {}) {
3991
4561
  const { db, paths, authToken } = getCtx();
@@ -3993,13 +4563,14 @@ async function* runAgentTurn(agentId, message, opts = {}) {
3993
4563
  const enabledProviders = Array.from(providerState_exports.listEnabled(db));
3994
4564
  const env = mergeSecretsIntoEnv(db, authToken);
3995
4565
  const messagingHost = createDbMessagingHost(db);
4566
+ const userMdHost = createDbUserMdHost(db, paths);
3996
4567
  const { apiKey } = await resolveAgentApiKey(db, authToken, agent);
3997
4568
  const controller = opts.controller ?? new AbortController();
3998
4569
  registerAgent(agentId, controller);
3999
4570
  try {
4000
4571
  for await (const frame of spawnWorkerTurn(
4001
4572
  { agent, message, enabledProviders, apiKey },
4002
- { signal: controller.signal, env, messagingHost }
4573
+ { signal: controller.signal, env, messagingHost, userMdHost }
4003
4574
  )) {
4004
4575
  yield frame;
4005
4576
  }
@@ -5136,6 +5707,7 @@ configRouter.get("/services", (c) => {
5136
5707
  id: svc.id,
5137
5708
  displayName: svc.displayName,
5138
5709
  ...svc.hint ? { hint: svc.hint } : {},
5710
+ ...svc.group ? { group: svc.group } : {},
5139
5711
  fields: resolveFieldStates(svc, configValues, secretValues)
5140
5712
  }));
5141
5713
  const body = { services };
@@ -5270,7 +5842,7 @@ function knownProviderRegistryNames() {
5270
5842
  // ../daemon/src/routes/groups.ts
5271
5843
  import { join as join16 } from "path";
5272
5844
  import { Hono as Hono4 } from "hono";
5273
- var USER_MD_MAX_BYTES = 12e3;
5845
+ var USER_MD_MAX_BYTES2 = 12e3;
5274
5846
  var groupsRouter = new Hono4();
5275
5847
  groupsRouter.get("/", (c) => {
5276
5848
  const { db, paths } = getCtx();
@@ -5309,8 +5881,8 @@ groupsRouter.put("/:id/user-md", async (c) => {
5309
5881
  if (!body || typeof body.userMd !== "string") {
5310
5882
  return c.json({ error: "userMd (string) is required" }, 400);
5311
5883
  }
5312
- if (Buffer.byteLength(body.userMd, "utf8") > USER_MD_MAX_BYTES) {
5313
- return c.json({ error: `userMd exceeds ${USER_MD_MAX_BYTES}-byte cap` }, 413);
5884
+ if (Buffer.byteLength(body.userMd, "utf8") > USER_MD_MAX_BYTES2) {
5885
+ return c.json({ error: `userMd exceeds ${USER_MD_MAX_BYTES2}-byte cap` }, 413);
5314
5886
  }
5315
5887
  const { db, paths } = getCtx();
5316
5888
  const g = groups_exports.get(db, c.req.param("id"), paths);