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/README.md +1 -1
- package/dist/cli.js +12 -12
- package/dist/cli.js.map +1 -1
- package/dist/daemon.js +648 -76
- package/dist/daemon.js.map +1 -1
- package/dist/worker.js +409 -63
- package/dist/worker.js.map +1 -1
- package/package.json +13 -13
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
|
-
##
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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: "
|
|
1002
|
-
displayName: "
|
|
1051
|
+
id: "fireworks",
|
|
1052
|
+
displayName: "Fireworks AI",
|
|
1003
1053
|
category: "provider",
|
|
1004
|
-
hint: "
|
|
1005
|
-
fields: [{ envVar: "
|
|
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(
|
|
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 "@
|
|
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 "@
|
|
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 "@
|
|
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
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
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
|
|
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
|
-
|
|
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)
|
|
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:
|
|
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 ??
|
|
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 =
|
|
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(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/ /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 ??
|
|
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(
|
|
3888
|
+
throw new Error(`${result.response.status} ${result.response.statusText}`);
|
|
3419
3889
|
}
|
|
3420
3890
|
const ct = result.response.headers.get("content-type") ?? "";
|
|
3421
|
-
const
|
|
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 (
|
|
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: {
|
|
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 "@
|
|
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
|
|
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
|
|
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,
|
|
4414
|
+
function attachIpcHandler(child, messagingHost, userMdHost) {
|
|
3903
4415
|
child.on("message", (msg) => {
|
|
3904
4416
|
if (!isIpcRequest(msg)) return;
|
|
3905
|
-
void dispatch(msg,
|
|
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
|
-
|
|
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
|
|
4443
|
+
result = await requireMessagingHost(messagingHost, req.method).agentExists(req.args.agentId);
|
|
3924
4444
|
break;
|
|
3925
4445
|
case "sendMessage":
|
|
3926
|
-
result = await
|
|
4446
|
+
result = await requireMessagingHost(messagingHost, req.method).sendMessage(req.args);
|
|
3927
4447
|
break;
|
|
3928
4448
|
case "listInbox":
|
|
3929
|
-
result = await
|
|
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
|
|
4455
|
+
await requireMessagingHost(messagingHost, req.method).markRead(req.args.messageId);
|
|
3933
4456
|
result = null;
|
|
3934
4457
|
break;
|
|
3935
4458
|
case "findReplies":
|
|
3936
|
-
result = await
|
|
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
|
|
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") >
|
|
5313
|
-
return c.json({ error: `userMd exceeds ${
|
|
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);
|