@byfriends/agent-core 0.3.0 → 0.3.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/index.mjs CHANGED
@@ -3032,7 +3032,7 @@ function parseSkillText(options) {
3032
3032
  throw error;
3033
3033
  }
3034
3034
  const frontmatter = parsed.data ?? {};
3035
- if (!isRecord$1(frontmatter)) throw new SkillParseError(`Frontmatter in ${options.skillMdPath} must be a mapping at the top level`);
3035
+ if (!isRecord$2(frontmatter)) throw new SkillParseError(`Frontmatter in ${options.skillMdPath} must be a mapping at the top level`);
3036
3036
  const metadata = normalizeMetadata(frontmatter);
3037
3037
  if (!isSupportedSkillType(metadata.type)) throw new UnsupportedSkillTypeError(metadata.type ?? String(frontmatter["type"]));
3038
3038
  const name = nonEmptyString$2(metadata.name);
@@ -3145,7 +3145,7 @@ function tokenizeArgs(raw) {
3145
3145
  function nonEmptyString$2(value) {
3146
3146
  return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
3147
3147
  }
3148
- function isRecord$1(value) {
3148
+ function isRecord$2(value) {
3149
3149
  return typeof value === "object" && value !== null && !Array.isArray(value);
3150
3150
  }
3151
3151
  //#endregion
@@ -3966,12 +3966,12 @@ function joinPath(parent, child, pathClass) {
3966
3966
  }
3967
3967
  //#endregion
3968
3968
  //#region src/tools/builtin/file/glob.md
3969
- var glob_default = "Find files (and optionally directories) by glob pattern, sorted by modification time (most recent first).\n\nGood patterns:\n- `*.ts` files in the current directory matching an extension\n- `src/**/*.ts` recursive with a subdirectory anchor and extension\n- `test_*.py` files whose name starts with a literal prefix\n\nRejected patterns (no literal anchor):\n- `**`, `**/*`, `*/*` — pure wildcards. Add an extension or subdirectory to give the walk a concrete target.\n- Anything starting with `**/` (e.g. `**/*.md`, `**/main/*.py`). Anchor it with a top-level subdirectory like `src/**/*.md`.\n- `*.{ts,tsx}` — brace expansion is not supported. Issue two calls: `*.ts` and `*.tsx`.\n\nLarge-directory warning — avoid recursing into dependency/build output even with an anchor:\n- `node_modules/**/*.js`, `.venv/**/*.py`, `__pycache__/**`, `target/**` match technically but typically produce thousands of results that truncate at the match cap. Prefer specific subpaths like `node_modules/react/src/**/*.js`.\n";
3969
+ var glob_default = "Find files (and optionally directories) by glob pattern, sorted by modification time (most recent first).\n\nREJECTED patterns (no literal anchor will be rejected):\n- **Pure wildcards**: `**`, `**/*`, `*/*` — no literal anchor bounds the result. Add an extension or subdirectory to give the walk a concrete target.\n- **`**/` prefix**: Anything starting with `**/` (e.g. `**/*.py`, `**/main/*.ts`). The leading `**/` has no literal anchor in front of it. Anchor it with a top-level subdirectory like `src/**/*.ts`.\n- **Brace expansion**: `*.{ts,tsx}` is not supported. Split it into separate calls: `*.ts` and `*.tsx`.\n\nGood patterns:\n- `*.ts` — files in the current directory matching an extension\n- `src/**/*.ts` — recursive with a subdirectory anchor and extension\n- `test_*.py` — files whose name starts with a literal prefix\n\nLarge-directory warning — avoid recursing into dependency/build output even with an anchor:\n- `node_modules/**/*.js`, `.venv/**/*.py`, `__pycache__/**`, `target/**` match technically but typically produce thousands of results that truncate at the match cap. Prefer specific subpaths like `node_modules/react/src/**/*.js`.\n\nWhen you need to search the entire project, first use Glob to explore the top-level directory structure, then use an anchored pattern like `src/**/*.ts` or `packages/**/*.ts` to narrow the search.\n";
3970
3970
  //#endregion
3971
3971
  //#region src/tools/builtin/file/glob.ts
3972
3972
  const GlobInputSchema = z.object({
3973
- pattern: z.string().describe("Glob pattern to match files/directories."),
3974
- path: z.string().optional().describe("Absolute path to the directory to search in. Defaults to the current working directory."),
3973
+ pattern: z.string().describe("Glob pattern to match files/directories. IMPORTANT: pattern MUST contain a literal anchor like a file extension (.ts) or a subdirectory name (src/). Patterns starting with **/ (e.g. **/*.py, **/main/*.ts), pure wildcards (**, **/*, */*, *), and brace expansion (*.{ts,tsx}) are REJECTED. Example: src/**/*.ts searches all .ts files under src/."),
3974
+ path: z.string().optional().describe("Absolute path to the directory to search in. Defaults to the current working directory. Explicit absolute paths outside the workspace are allowed (e.g. to search a dependency installed elsewhere); relative paths are resolved against the working directory and rejected if they escape it."),
3975
3975
  include_dirs: z.boolean().default(true).optional().describe("Whether to include directories in results. Defaults to true. Set false to return only files.")
3976
3976
  });
3977
3977
  const MAX_MATCHES = 1e3;
@@ -4013,7 +4013,7 @@ var GlobTool = class {
4013
4013
  workspace: this.workspace,
4014
4014
  operation: "search",
4015
4015
  policy: {
4016
- guardMode: "strict",
4016
+ guardMode: "absolute-outside-allowed",
4017
4017
  checkSensitive: false
4018
4018
  }
4019
4019
  });
@@ -4047,7 +4047,7 @@ var GlobTool = class {
4047
4047
  }
4048
4048
  return {
4049
4049
  isError: true,
4050
- output: `Pattern "${args.pattern}" is a pure wildcard (only \`*\`, \`?\`, \`**\`, \`/\`) and would enumerate every file under the search root — with no literal anchor to bound the result set, this typically exhausts your context on large trees. Add an extension ("${args.pattern === "**" || args.pattern === "**/*" ? "**/*.ts" : "**/*.md"}") or a subdirectory ("src/**/*.ts") to constrain the walk.\n\nAllowed roots for explicit path searches:\n${rootList}\n\nTop of ${this.workspace.workspaceDir}:\n${tree}`
4050
+ output: `Pattern "${args.pattern}" is a pure wildcard (only \`*\`, \`?\`, \`**\`, \`/\`) and would enumerate every file under the search root — with no literal anchor to bound the result set, this typically exhausts your context on large trees. Add an extension ("${args.pattern === "**" || args.pattern === "**/*" ? "**/*.ts" : "**/*.md"}") or a subdirectory ("src/**/*.ts") to constrain the walk.\n\nWorkspace roots:\n${rootList}\n\nTop of ${this.workspace.workspaceDir}:\n${tree}`
4051
4051
  };
4052
4052
  }
4053
4053
  if (containsBraceExpansion(args.pattern)) return {
@@ -4075,6 +4075,8 @@ var GlobTool = class {
4075
4075
  try {
4076
4076
  const seen = /* @__PURE__ */ new Set();
4077
4077
  const entries = [];
4078
+ const pathClass = this.kaos.pathClass();
4079
+ const filteredSensitive = [];
4078
4080
  const YIELD_SAFETY_CAP = MAX_MATCHES * 2;
4079
4081
  let yielded = 0;
4080
4082
  let truncated = false;
@@ -4090,6 +4092,10 @@ var GlobTool = class {
4090
4092
  break outer;
4091
4093
  }
4092
4094
  seen.add(filePath);
4095
+ if (isSensitiveFile(filePath, pathClass)) {
4096
+ filteredSensitive.push(filePath);
4097
+ continue;
4098
+ }
4093
4099
  let mtime = 0;
4094
4100
  let isDir = false;
4095
4101
  try {
@@ -4105,10 +4111,10 @@ var GlobTool = class {
4105
4111
  }
4106
4112
  entries.sort((a, b) => b.mtime - a.mtime);
4107
4113
  const paths = entries.map((e) => e.path);
4108
- const pathClass = this.kaos.pathClass();
4109
4114
  const relBase = searchRoots[0] ?? this.workspace.workspaceDir;
4110
4115
  const displayLines = paths.map((p) => relativizeIfUnder$1(p, relBase, pathClass));
4111
- if (entries.length === 0 && !truncated) return { output: "No matches found" };
4116
+ const filteredSome = filteredSensitive.length > 0;
4117
+ if (entries.length === 0 && !truncated) return { output: filteredSome ? `No non-sensitive matches found (filtered ${String(filteredSensitive.length)} sensitive file(s))` : "No matches found" };
4112
4118
  const lines = [];
4113
4119
  if (truncated) {
4114
4120
  lines.push(`[Truncated at ${String(MAX_MATCHES)} matches — use a more specific pattern]`);
@@ -4116,6 +4122,10 @@ var GlobTool = class {
4116
4122
  }
4117
4123
  lines.push(...displayLines);
4118
4124
  if (!truncated && entries.length === 1e3) lines.push(`Found ${String(entries.length)} matches`);
4125
+ if (filteredSome) {
4126
+ const displayedFiltered = filteredSensitive.map((p) => relativizeIfUnder$1(p, relBase, pathClass));
4127
+ lines.push(`Filtered ${String(filteredSensitive.length)} sensitive file(s): ${displayedFiltered.join(", ")}`);
4128
+ }
4119
4129
  return { output: lines.join("\n") };
4120
4130
  } catch (error) {
4121
4131
  if (error !== null && typeof error === "object" && "code" in error) {
@@ -6312,7 +6322,7 @@ function rewriteWindowsNullRedirect$1(command) {
6312
6322
  }
6313
6323
  //#endregion
6314
6324
  //#region src/tools/builtin/state/todo-list.md
6315
- var todo_list_default = "Use this tool to maintain a structured TODO list as you work through a multi-step task.\n\nUse for multi-step tasks, tracking investigation progress, or planning a sequence of edits. Do not use for single-shot answers or trivial requests.\n\n**Avoid churn:**\n- Do not re-call this tool when nothing meaningful has changed since the last call — update the list only after real progress.\n- When unsure of the current state, call query mode first (omit `todos`) to check the list before deciding what to update.\n- If no available tool can move any task forward, tell the user where you are stuck instead of repeatedly re-ordering the same todos.\n\n**How to use:**\n- Call with `todos: [...]` to replace the full list. Statuses: pending / in_progress / done.\n- Call with no arguments to query the current list.\n- Call with `todos: []` to clear the list.\n- Keep titles short and actionable.\n- Update statuses as you make progress — mark one item in_progress at a time.\n";
6325
+ var todo_list_default = "Use this tool to maintain a structured TODO list as you work through a multi-step task.\n\nUse for multi-step tasks, tracking investigation progress, or planning a sequence of edits. Do not use for single-shot answers or trivial requests.\n\n**Update discipline:**\n- Update status immediately when you start or complete a subtask: mark it `in_progress` when you begin working on it, and `done` when finished.\n- Do not skip the `in_progress` state — the user should always see what you are currently working on.\n- Avoid redundant calls: do not re-call this tool when nothing meaningful has changed since the last call.\n- When unsure of the current state, call query mode first (omit `todos`) to check the list before deciding what to update.\n- If no available tool can move any task forward, tell the user where you are stuck instead of repeatedly re-ordering the same todos.\n\n**How to use:**\n- Call with `todos: [...]` to replace the full list. Statuses: pending / in_progress / done.\n- Call with no arguments to query the current list.\n- Call with `todos: []` to clear the list.\n- Keep titles short and actionable.\n- Update statuses as you make progress — mark one item in_progress at a time.\n";
6316
6326
  //#endregion
6317
6327
  //#region src/tools/builtin/state/todo-list.ts
6318
6328
  /**
@@ -8211,7 +8221,7 @@ const OptionalStringSchema = z.preprocess((value) => {
8211
8221
  if (typeof value === "string") return value;
8212
8222
  if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
8213
8223
  }, z.string().optional());
8214
- const HookSpecificOutputSchema = z.preprocess((value) => isRecord(value) ? value : void 0, z.looseObject({
8224
+ const HookSpecificOutputSchema = z.preprocess((value) => isRecord$1(value) ? value : void 0, z.looseObject({
8215
8225
  message: OptionalStringSchema,
8216
8226
  permissionDecision: z.unknown().optional(),
8217
8227
  permissionDecisionReason: OptionalStringSchema
@@ -8371,7 +8381,7 @@ function tryKillProcess(child, signal) {
8371
8381
  } catch {}
8372
8382
  }
8373
8383
  }
8374
- function isRecord(value) {
8384
+ function isRecord$1(value) {
8375
8385
  return typeof value === "object" && value !== null && !Array.isArray(value);
8376
8386
  }
8377
8387
  function errorMessage(error) {
@@ -11476,8 +11486,7 @@ var AgentRecords = class {
11476
11486
  tools: "tools",
11477
11487
  usage: "usage",
11478
11488
  background: "background",
11479
- full_compaction: "fullCompaction",
11480
- plan_mode: "planMode"
11489
+ full_compaction: "fullCompaction"
11481
11490
  }[recordType.split(".")[0]] ?? null;
11482
11491
  }
11483
11492
  async replay() {
@@ -13522,8 +13531,8 @@ var ToolManager = class {
13522
13531
  return (input) => withProviderRequestAuth(resolveAuth, (auth) => uploadVideo(input, { auth }));
13523
13532
  }
13524
13533
  get loopTools() {
13525
- const builtinNames = [...this.builtinTools.keys()].filter((name) => this.enabledTools.has(name)).sort();
13526
- const userNames = [...this.userTools.keys()].filter((name) => this.enabledTools.has(name)).sort();
13534
+ const builtinNames = [...this.builtinTools.keys()].filter((name) => this.enabledTools.has(name)).toSorted();
13535
+ const userNames = [...this.userTools.keys()].filter((name) => this.enabledTools.has(name)).toSorted();
13527
13536
  const mcpNames = [...this.mcpTools.keys()].filter((name) => this.isMcpToolEnabled(name));
13528
13537
  return [
13529
13538
  ...builtinNames.map((name) => this.builtinTools.get(name)),
@@ -13699,7 +13708,7 @@ function findImplicitBoundaries(prompt) {
13699
13708
  const index = prompt.indexOf(header);
13700
13709
  if (index !== -1) boundaries.push(index);
13701
13710
  }
13702
- return boundaries.sort((a, b) => a - b);
13711
+ return boundaries.toSorted((a, b) => a - b);
13703
13712
  }
13704
13713
  /**
13705
13714
  * Split prompt by implicit boundaries into blocks.
@@ -15124,15 +15133,6 @@ var Agent = class {
15124
15133
  getModel: () => {
15125
15134
  return this.config.modelAlias ?? "";
15126
15135
  },
15127
- enterPlan: async () => {
15128
- throw new ByfError(ErrorCodes.NOT_IMPLEMENTED, "Plan mode has been removed");
15129
- },
15130
- cancelPlan: async () => {
15131
- throw new ByfError(ErrorCodes.NOT_IMPLEMENTED, "Plan mode has been removed");
15132
- },
15133
- clearPlan: async () => {
15134
- throw new ByfError(ErrorCodes.NOT_IMPLEMENTED, "Plan mode has been removed");
15135
- },
15136
15136
  beginCompaction: (payload) => {
15137
15137
  this.fullCompaction.begin({
15138
15138
  source: "manual",
@@ -15167,7 +15167,6 @@ var Agent = class {
15167
15167
  getContext: () => this.context.data(),
15168
15168
  getConfig: () => this.config.data(),
15169
15169
  getPermission: () => this.permission.data(),
15170
- getPlan: async () => null,
15171
15170
  getUsage: () => this.usage.data(),
15172
15171
  getTools: () => this.tools.data(),
15173
15172
  getBackground: (payload) => this.background.list(payload.activeOnly ?? false, payload.limit)
@@ -15300,6 +15299,21 @@ function proxyWithExtraPayload(methods, extraPayload) {
15300
15299
  }, ...args);
15301
15300
  } });
15302
15301
  }
15302
+ /** The Zod `z.enum` literal inferred from registry keys. */
15303
+ const PROVIDER_TYPE_VALUES = Object.keys({
15304
+ exa: { defaultBaseUrl: "https://api.exa.ai/search" },
15305
+ brave: { defaultBaseUrl: "https://api.search.brave.com/res/v1/web/search" },
15306
+ firecrawl: { defaultBaseUrl: "https://api.firecrawl.dev/v2/search" }
15307
+ });
15308
+ const providerClassMap = /* @__PURE__ */ new Map();
15309
+ function registerProvider(type, cls) {
15310
+ providerClassMap.set(type, cls);
15311
+ }
15312
+ function createProvider$1(type, options) {
15313
+ const cls = providerClassMap.get(type);
15314
+ if (cls === void 0) throw new Error(`WebSearch provider type "${type}" is not registered. Did you import the provider module?`);
15315
+ return new cls(options);
15316
+ }
15303
15317
  //#endregion
15304
15318
  //#region src/config/schema.ts
15305
15319
  const ProviderTypeSchema = z.enum([
@@ -15399,9 +15413,18 @@ const ByfServiceConfigSchema = z.object({
15399
15413
  oauth: OAuthRefSchema.optional(),
15400
15414
  customHeaders: StringRecordSchema.optional()
15401
15415
  });
15416
+ /** Provider type enum derived from webSearchProviderRegistry keys. */
15417
+ const WebSearchProviderTypeSchema = z.enum(PROVIDER_TYPE_VALUES);
15418
+ const WebSearchProviderConfigSchema = z.object({
15419
+ type: WebSearchProviderTypeSchema,
15420
+ apiKeys: z.array(z.string().min(1)).nonempty(),
15421
+ baseUrl: z.string().optional(),
15422
+ priority: z.number().int().positive()
15423
+ });
15424
+ const WebSearchConfigSchema = z.object({ providers: z.array(WebSearchProviderConfigSchema).nonempty() });
15402
15425
  const ServicesConfigSchema = z.object({
15403
- byfSearch: ByfServiceConfigSchema.optional(),
15404
- byfFetch: ByfServiceConfigSchema.optional()
15426
+ webSearch: WebSearchConfigSchema.optional(),
15427
+ fetchUrl: ByfServiceConfigSchema.optional()
15405
15428
  });
15406
15429
  const McpServerCommonFields = {
15407
15430
  enabled: z.boolean().optional(),
@@ -15467,8 +15490,8 @@ const LoopControlPatchSchema = LoopControlSchema.partial();
15467
15490
  const BackgroundConfigPatchSchema = BackgroundConfigSchema.partial();
15468
15491
  const ByfServiceConfigPatchSchema = ByfServiceConfigSchema.partial();
15469
15492
  const ServicesConfigPatchSchema = z.object({
15470
- byfSearch: ByfServiceConfigPatchSchema.optional(),
15471
- byfFetch: ByfServiceConfigPatchSchema.optional()
15493
+ webSearch: WebSearchConfigSchema.optional(),
15494
+ fetchUrl: ByfServiceConfigPatchSchema.optional()
15472
15495
  });
15473
15496
  const ByfConfigPatchSchema = z.object({
15474
15497
  providers: z.record(z.string(), ProviderConfigPatchSchema).optional(),
@@ -15716,10 +15739,15 @@ function transformServiceData(data) {
15716
15739
  const targetKey = snakeToCamel(key);
15717
15740
  if (targetKey === "oauth") out[targetKey] = isPlainObject(value) ? transformPlainObject(value) : value;
15718
15741
  else if (targetKey === "customHeaders") out[targetKey] = cloneObjectValue(value);
15742
+ else if (Array.isArray(value)) out[targetKey] = value.map((item) => isPlainObject(item) ? transformRecord(item, identity, snakeToCamel) : item);
15743
+ else if (isPlainObject(value)) out[targetKey] = transformPlainObject(value);
15719
15744
  else out[targetKey] = value;
15720
15745
  }
15721
15746
  return out;
15722
15747
  }
15748
+ function identity(v) {
15749
+ return v;
15750
+ }
15723
15751
  function transformLoopControlData(data) {
15724
15752
  const out = transformPlainObject(data);
15725
15753
  if (out["maxStepsPerTurn"] === void 0 && out["maxStepsPerRun"] !== void 0) out["maxStepsPerTurn"] = out["maxStepsPerRun"];
@@ -15739,11 +15767,11 @@ function configToTomlData(config) {
15739
15767
  delete out["default_yolo"];
15740
15768
  delete out["defaultYolo"];
15741
15769
  delete out["defaultPermissionMode"];
15770
+ delete out["default_thinking"];
15742
15771
  for (const key of [
15743
15772
  "defaultProvider",
15744
15773
  "defaultModel",
15745
15774
  "yolo",
15746
- "defaultThinking",
15747
15775
  "defaultPermissionMode",
15748
15776
  "mergeAllAvailableSkills",
15749
15777
  "extraSkillDirs"
@@ -15812,10 +15840,17 @@ function permissionRuleToToml(rule) {
15812
15840
  }
15813
15841
  function servicesToToml(services, rawServices) {
15814
15842
  const out = cloneRecord(rawServices);
15815
- if (services.byfSearch !== void 0) out["byf_search"] = serviceToToml(services.byfSearch);
15816
- else delete out["byf_search"];
15817
- if (services.byfFetch !== void 0) out["byf_fetch"] = serviceToToml(services.byfFetch);
15818
- else delete out["byf_fetch"];
15843
+ if (services.webSearch !== void 0 && services.webSearch.providers.length > 0) {
15844
+ const providersToml = services.webSearch.providers.map((p) => {
15845
+ const providerOut = {};
15846
+ for (const [key, value] of Object.entries(p)) setDefined(providerOut, camelToSnake(key), value);
15847
+ return providerOut;
15848
+ });
15849
+ out["web_search"] = out["web_search"] ?? {};
15850
+ out["web_search"]["providers"] = providersToml;
15851
+ } else delete out["web_search"];
15852
+ if (services.fetchUrl !== void 0) out["fetch_url"] = serviceToToml(services.fetchUrl);
15853
+ else delete out["fetch_url"];
15819
15854
  return out;
15820
15855
  }
15821
15856
  function serviceToToml(service) {
@@ -15873,90 +15908,679 @@ function isFileExistsError(error) {
15873
15908
  return typeof error === "object" && error !== null && error.code === "EEXIST";
15874
15909
  }
15875
15910
  //#endregion
15876
- //#region src/version.ts
15877
- function getCoreVersion() {
15878
- try {
15879
- const raw = readFileSync(fileURLToPath(new URL("../package.json", import.meta.url)), "utf-8");
15880
- const pkg = JSON.parse(raw);
15881
- return typeof pkg.version === "string" ? pkg.version : "0.0.0";
15882
- } catch {
15883
- return "0.0.0";
15911
+ //#region src/config/update-rules.ts
15912
+ const REMOVED_RULES = [
15913
+ {
15914
+ path: "default_yolo",
15915
+ pathParts: ["default_yolo"],
15916
+ kind: "removed",
15917
+ detail: "Top-level field default_yolo is removed. Use yolo instead.",
15918
+ deprecatedSince: "pre-0.1.0"
15919
+ },
15920
+ {
15921
+ path: "defaultYolo",
15922
+ pathParts: ["defaultYolo"],
15923
+ kind: "removed",
15924
+ detail: "Top-level field defaultYolo is removed. Use yolo instead.",
15925
+ deprecatedSince: "pre-0.1.0"
15926
+ },
15927
+ {
15928
+ path: "services.byf_search",
15929
+ pathParts: ["services", "byf_search"],
15930
+ kind: "removed",
15931
+ detail: "Deprecated service byf_search is removed.",
15932
+ deprecatedSince: "pre-0.1.0"
15933
+ },
15934
+ {
15935
+ path: "services.byf_fetch",
15936
+ pathParts: ["services", "byf_fetch"],
15937
+ kind: "removed",
15938
+ detail: "Deprecated service byf_fetch is removed. Use services.fetch_url instead.",
15939
+ deprecatedSince: "pre-0.1.0"
15884
15940
  }
15885
- }
15886
- //#endregion
15887
- //#region src/mcp/client-shared.ts
15888
- const BYF_MCP_CLIENT_VERSION = getCoreVersion();
15941
+ ];
15942
+ const RENAMED_RULES = [{
15943
+ path: "loop_control.max_steps_per_run",
15944
+ pathParts: ["loop_control", "max_steps_per_run"],
15945
+ kind: "renamed",
15946
+ detail: "Renamed to max_steps_per_turn.",
15947
+ deprecatedSince: "pre-0.1.0"
15948
+ }];
15949
+ const MIGRATED_RULES = [{
15950
+ path: "default_thinking",
15951
+ pathParts: ["default_thinking"],
15952
+ kind: "migrated",
15953
+ detail: "Migrate default_thinking to [thinking] block.",
15954
+ deprecatedSince: "pre-0.1.0"
15955
+ }];
15889
15956
  /**
15890
- * Build the `RequestOptions` object accepted by the MCP SDK's `callTool`,
15891
- * including either the configured tool-call timeout, an in-flight abort
15892
- * signal, both, or neither. Returns `undefined` when nothing needs to be
15893
- * passed so the SDK falls back to its defaults.
15957
+ * Combined list of all deprecated-field rules.
15958
+ *
15959
+ * Order within the list does not affect correctness (the CLI groups by kind
15960
+ * at display time), but a stable order helps test expectations.
15894
15961
  */
15895
- function buildRequestOptions(toolCallTimeoutMs, signal) {
15896
- if (toolCallTimeoutMs === void 0 && signal === void 0) return void 0;
15962
+ const DEPRECATED_FIELD_RULES = [
15963
+ ...REMOVED_RULES,
15964
+ ...RENAMED_RULES,
15965
+ ...MIGRATED_RULES
15966
+ ];
15967
+ //#endregion
15968
+ //#region src/providers/runtime-provider.ts
15969
+ function resolveRuntimeProvider(input) {
15970
+ const modelName = input.model ?? input.config.defaultModel;
15971
+ if (modelName === void 0) throw new ByfError(ErrorCodes.CONFIG_INVALID, "No model is selected. Set default_model in config.toml or pass a configured model alias.");
15972
+ const alias = input.config.models?.[modelName];
15973
+ if (alias === void 0) throw new ByfError(ErrorCodes.CONFIG_INVALID, `Model "${modelName}" is not configured in config.toml. Add a [models."${modelName}"] entry with max_context_size.`);
15974
+ const resolvedModel = alias.model;
15975
+ const providerName = alias.provider ?? input.config.defaultProvider;
15976
+ const providerConfig = providerName === void 0 ? void 0 : input.config.providers[providerName];
15977
+ if (providerName === void 0) throw new ByfError(ErrorCodes.CONFIG_INVALID, `Model "${modelName}" must define a provider in config.toml.`);
15978
+ if (providerConfig === void 0) throw new ByfError(ErrorCodes.CONFIG_INVALID, `Provider "${providerName}" for model "${modelName}" is not configured.`);
15979
+ if (!Number.isInteger(alias.maxContextSize) || alias.maxContextSize <= 0) throw new ByfError(ErrorCodes.CONFIG_INVALID, `Model "${modelName}" must define a positive max_context_size in config.toml.`);
15980
+ if (input.validateCredentials !== false && providerConfig.type !== "vertexai" && providerConfig.oauth === void 0 && providerApiKey(providerConfig) === void 0) throw new ByfError(ErrorCodes.CONFIG_INVALID, `Provider "${providerName}" has no credentials configured. Set apiKey, oauth, or a provider env API key in config.toml.`);
15981
+ const provider = toKosongProviderConfig(providerConfig, resolvedModel, input.byfRequestHeaders, alias.maxOutputSize, alias.reasoningKey, input.promptCacheKey);
15897
15982
  return {
15898
- timeout: toolCallTimeoutMs,
15899
- signal
15983
+ modelName,
15984
+ providerName,
15985
+ modelCapabilities: resolveModelCapabilities(alias, provider),
15986
+ provider
15900
15987
  };
15901
15988
  }
15902
- function toMcpToolDefinition(tool) {
15989
+ async function resolveRuntimeProviderWithOAuth(input) {
15990
+ const resolved = resolveRuntimeProvider(input);
15991
+ const resolveAuth = createRuntimeProviderAuthResolver(input, resolved);
15992
+ if (resolveAuth === void 0) return resolved;
15993
+ await resolveAuth();
15903
15994
  return {
15904
- name: tool.name,
15905
- description: tool.description ?? "",
15906
- inputSchema: tool.inputSchema
15995
+ ...resolved,
15996
+ resolveAuth
15997
+ };
15998
+ }
15999
+ function createRuntimeProviderAuthResolver(input, resolved = resolveRuntimeProvider(input)) {
16000
+ const providerName = resolved.providerName;
16001
+ if (providerName === void 0) return void 0;
16002
+ const providerConfig = input.config.providers[providerName];
16003
+ if (providerConfig?.oauth === void 0) return void 0;
16004
+ if (providerApiKey(providerConfig) !== void 0) throw new ByfError(ErrorCodes.CONFIG_INVALID, `Provider "${providerName}" has both apiKey and oauth set in config.toml — they are mutually exclusive. Remove one.`);
16005
+ const tokenProvider = input.resolveOAuthTokenProvider?.(providerName, providerConfig.oauth);
16006
+ if (tokenProvider === void 0) return async () => {
16007
+ throw new ByfError(ErrorCodes.AUTH_LOGIN_REQUIRED, `OAuth provider "${providerName}" requires login before it can be used.`);
16008
+ };
16009
+ return async (options) => {
16010
+ let apiKey;
16011
+ try {
16012
+ apiKey = await tokenProvider.getAccessToken(options?.forceRefresh === true ? { force: true } : void 0);
16013
+ } catch (error) {
16014
+ if (!isAuthLoginRequired(error)) (input.log ?? log).warn("oauth token fetch failed", {
16015
+ providerName,
16016
+ error
16017
+ });
16018
+ throw new ByfError(ErrorCodes.AUTH_LOGIN_REQUIRED, `OAuth provider "${providerName}" requires login before it can be used.`, { cause: error });
16019
+ }
16020
+ if (apiKey.trim().length === 0) throw new ByfError(ErrorCodes.AUTH_LOGIN_REQUIRED, `OAuth provider "${providerName}" requires login before it can be used.`);
16021
+ return { apiKey };
15907
16022
  };
15908
16023
  }
16024
+ function isAuthLoginRequired(error) {
16025
+ return isByfError(error) && error.code === ErrorCodes.AUTH_LOGIN_REQUIRED;
16026
+ }
16027
+ const CAPABILITY_DEFINITIONS = [
16028
+ {
16029
+ name: "image_in",
16030
+ returnKey: "image_in"
16031
+ },
16032
+ {
16033
+ name: "video_in",
16034
+ returnKey: "video_in"
16035
+ },
16036
+ {
16037
+ name: "audio_in",
16038
+ returnKey: "audio_in"
16039
+ },
16040
+ {
16041
+ name: "thinking",
16042
+ returnKey: "thinking"
16043
+ },
16044
+ {
16045
+ name: "always_thinking",
16046
+ returnKey: "thinking"
16047
+ },
16048
+ {
16049
+ name: "tool_use",
16050
+ returnKey: "tool_use"
16051
+ },
16052
+ {
16053
+ name: "thinking_effort",
16054
+ returnKey: "thinking_effort"
16055
+ },
16056
+ {
16057
+ name: "thinking_xhigh",
16058
+ returnKey: "thinking_xhigh"
16059
+ },
16060
+ {
16061
+ name: "thinking_max",
16062
+ returnKey: "thinking_max"
16063
+ }
16064
+ ];
15909
16065
  /**
15910
- * Normalise the SDK's `callTool` return into kosong's {@link MCPToolResult}.
15911
- * The SDK can return either the modern `{ content, isError }` shape or a
15912
- * legacy `{ toolResult }` shape; we collapse the legacy shape to a single
15913
- * text content block.
16066
+ * The list of every valid capability name that can appear in a model
16067
+ * alias's `capabilities` array.
16068
+ *
16069
+ * Derives directly from {@link CAPABILITY_DEFINITIONS} so that adding a
16070
+ * new capability in one place automatically keeps both the validation
16071
+ * gate (`update-config`) and the runtime resolver in sync.
15914
16072
  */
15915
- function toMcpToolResult(result) {
15916
- if (typeof result === "object" && result !== null && "content" in result) {
15917
- const typed = result;
15918
- if (Array.isArray(typed.content)) return {
15919
- content: typed.content,
15920
- isError: typed.isError === true
16073
+ const VALID_CAPABILITIES = CAPABILITY_DEFINITIONS.map((d) => d.name);
16074
+ function resolveModelCapabilities(alias, provider) {
16075
+ const capabilities = new Set((alias.capabilities ?? []).map((capability) => capability.trim().toLowerCase()));
16076
+ const has = (capability) => capabilities.has(capability);
16077
+ const providerCapability = createProvider(providerForCapabilityProbe(provider)).getCapability?.(provider.model) ?? UNKNOWN_CAPABILITY;
16078
+ const returnKeyToNames = /* @__PURE__ */ new Map();
16079
+ for (const def of CAPABILITY_DEFINITIONS) {
16080
+ const names = returnKeyToNames.get(def.returnKey) ?? [];
16081
+ names.push(def.name);
16082
+ returnKeyToNames.set(def.returnKey, names);
16083
+ }
16084
+ const resolved = {};
16085
+ for (const [returnKey, names] of returnKeyToNames) resolved[returnKey] = names.some((n) => has(n)) || Boolean(providerCapability[returnKey]);
16086
+ return {
16087
+ ...resolved,
16088
+ max_context_tokens: alias.maxContextSize
16089
+ };
16090
+ }
16091
+ function toKosongProviderConfig(provider, model, byfRequestHeaders, maxOutputSize, reasoningKey, promptCacheKey) {
16092
+ switch (provider.type) {
16093
+ case "anthropic": return {
16094
+ type: "anthropic",
16095
+ model,
16096
+ baseUrl: providerValue(provider.baseUrl, provider.env, "ANTHROPIC_BASE_URL"),
16097
+ apiKey: providerApiKey(provider),
16098
+ ...maxOutputSize !== void 0 ? { defaultMaxTokens: maxOutputSize } : {},
16099
+ ...defaultHeadersField(provider.customHeaders)
15921
16100
  };
15922
- }
15923
- if (typeof result === "object" && result !== null && "toolResult" in result) {
15924
- const legacy = result.toolResult;
15925
- return {
15926
- content: [{
15927
- type: "text",
15928
- text: typeof legacy === "string" ? legacy : JSON.stringify(legacy)
15929
- }],
15930
- isError: false
16101
+ case "openai-completions": {
16102
+ const defaultHeaders = {
16103
+ ...byfRequestHeaders,
16104
+ ...provider.customHeaders
16105
+ };
16106
+ const generationKwargs = {
16107
+ prompt_cache_key: promptCacheKey,
16108
+ extra_body: provider.extraBody
16109
+ };
16110
+ if (Object.keys(defaultHeaders).length === 0) return {
16111
+ type: "openai-completions",
16112
+ model,
16113
+ baseUrl: providerValue(provider.baseUrl, provider.env, "BYF_BASE_URL"),
16114
+ reasoningKey,
16115
+ thinkingEffortKey: provider.thinkingEffortKey,
16116
+ generationKwargs,
16117
+ apiKey: providerApiKey(provider)
16118
+ };
16119
+ return {
16120
+ type: "openai-completions",
16121
+ model,
16122
+ baseUrl: providerValue(provider.baseUrl, provider.env, "BYF_BASE_URL"),
16123
+ reasoningKey,
16124
+ thinkingEffortKey: provider.thinkingEffortKey,
16125
+ generationKwargs,
16126
+ defaultHeaders,
16127
+ apiKey: providerApiKey(provider)
16128
+ };
16129
+ }
16130
+ case "google-genai": return {
16131
+ type: "google-genai",
16132
+ model,
16133
+ apiKey: providerApiKey(provider)
16134
+ };
16135
+ case "openai_responses": return {
16136
+ type: "openai_responses",
16137
+ model,
16138
+ baseUrl: providerValue(provider.baseUrl, provider.env, "OPENAI_BASE_URL"),
16139
+ apiKey: providerApiKey(provider),
16140
+ ...defaultHeadersField(provider.customHeaders)
16141
+ };
16142
+ case "vertexai": return {
16143
+ type: "vertexai",
16144
+ model,
16145
+ vertexai: hasVertexAIServiceEnv(provider),
16146
+ apiKey: hasVertexAIServiceEnv(provider) ? void 0 : providerApiKey(provider),
16147
+ project: vertexAIProject(provider),
16148
+ location: vertexAILocation(provider)
15931
16149
  };
16150
+ default: {
16151
+ const exhaustive = provider.type;
16152
+ throw new ByfError(ErrorCodes.MODEL_CONFIG_INVALID, `Unsupported provider type: ${String(exhaustive)}`);
16153
+ }
15932
16154
  }
16155
+ }
16156
+ function defaultHeadersField(headers) {
16157
+ if (headers === void 0 || Object.keys(headers).length === 0) return {};
16158
+ return { defaultHeaders: { ...headers } };
16159
+ }
16160
+ function providerForCapabilityProbe(provider) {
16161
+ if (provider.type === "vertexai") return {
16162
+ ...provider,
16163
+ vertexai: false,
16164
+ project: void 0,
16165
+ location: void 0,
16166
+ apiKey: provider.apiKey === void 0 || provider.apiKey.length === 0 ? "capability-probe" : provider.apiKey
16167
+ };
16168
+ if (provider.apiKey !== void 0 && provider.apiKey.length > 0) return provider;
15933
16169
  return {
15934
- content: [],
15935
- isError: false
16170
+ ...provider,
16171
+ apiKey: "capability-probe"
15936
16172
  };
15937
16173
  }
15938
- //#endregion
15939
- //#region src/mcp/client-http.ts
15940
- /**
15941
- * Wraps the SDK streamable-HTTP transport as a kosong {@link MCPClient}.
15942
- * Static bearer tokens are looked up from `process.env[bearerTokenEnvVar]`.
15943
- * OAuth providers are attached separately by the connection manager.
15944
- */
15945
- var HttpMcpClient = class {
15946
- client;
15947
- transport;
15948
- toolCallTimeoutMs;
15949
- started = false;
15950
- closed = false;
15951
- ready = false;
15952
- hooksInstalled = false;
15953
- unexpectedCloseListener;
15954
- lastTransportError;
15955
- pendingUnexpectedClose;
15956
- unexpectedCloseFired = false;
15957
- constructor(config, options = {}) {
15958
- const headers = buildMcpHttpHeaders(config, options.envLookup ?? ((name) => process.env[name]));
15959
- this.transport = new StreamableHTTPClientTransport(new URL(config.url), {
16174
+ function providerApiKey(provider) {
16175
+ switch (provider.type) {
16176
+ case "anthropic": return providerValue(provider.apiKey, provider.env, "ANTHROPIC_API_KEY");
16177
+ case "openai_responses": return providerValue(provider.apiKey, provider.env, "OPENAI_API_KEY");
16178
+ case "openai-completions": return providerValue(provider.apiKey, provider.env, "BYF_API_KEY");
16179
+ case "google-genai": return providerValue(provider.apiKey, provider.env, "GOOGLE_API_KEY");
16180
+ case "vertexai": return nonEmptyString$1(provider.apiKey) ?? envValue(provider.env, "VERTEXAI_API_KEY") ?? envValue(provider.env, "GOOGLE_API_KEY");
16181
+ default: {
16182
+ const exhaustive = provider.type;
16183
+ throw new ByfError(ErrorCodes.MODEL_CONFIG_INVALID, `Unsupported provider type: ${String(exhaustive)}`);
16184
+ }
16185
+ }
16186
+ }
16187
+ function hasVertexAIServiceEnv(provider) {
16188
+ return vertexAIProject(provider) !== void 0 && vertexAILocation(provider) !== void 0;
16189
+ }
16190
+ function vertexAIProject(provider) {
16191
+ return envValue(provider.env, "GOOGLE_CLOUD_PROJECT");
16192
+ }
16193
+ function vertexAILocation(provider) {
16194
+ return envValue(provider.env, "GOOGLE_CLOUD_LOCATION") ?? locationFromVertexAIBaseUrl(provider.baseUrl);
16195
+ }
16196
+ function providerValue(configured, env, envKey) {
16197
+ return nonEmptyString$1(configured) ?? envValue(env, envKey);
16198
+ }
16199
+ function envValue(env, key) {
16200
+ return nonEmptyString$1(env?.[key]);
16201
+ }
16202
+ function nonEmptyString$1(value) {
16203
+ const trimmed = value?.trim();
16204
+ return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
16205
+ }
16206
+ function locationFromVertexAIBaseUrl(baseUrl) {
16207
+ const url = nonEmptyString$1(baseUrl);
16208
+ if (url === void 0) return void 0;
16209
+ try {
16210
+ const host = new URL(url).hostname;
16211
+ return host.endsWith("-aiplatform.googleapis.com") ? nonEmptyString$1(host.slice(0, -26)) : void 0;
16212
+ } catch {
16213
+ return;
16214
+ }
16215
+ }
16216
+ //#endregion
16217
+ //#region src/config/update.ts
16218
+ /**
16219
+ * Scan a parsed config (including `config.raw`) and return all Findings
16220
+ * for deprecated / renamed / migrated / dangling / unknown / invalid-value
16221
+ * fields.
16222
+ *
16223
+ * This is a **pure** function — no file I/O, no side effects.
16224
+ *
16225
+ * Detection is based on `config.raw` (the clone of the original TOML data),
16226
+ * not on the parsed camelCase schema. This matches the PRD's observation
16227
+ * that `raw` is both the protection layer (preserving unknown fields) and
16228
+ * the blind spot (retaining stale keys through read→write cycles).
16229
+ */
16230
+ function analyzeConfig(config) {
16231
+ const raw = config.raw;
16232
+ const findings = [];
16233
+ if (isRecord(raw)) {
16234
+ for (const rule of DEPRECATED_FIELD_RULES) if (pathExistsInRaw(raw, rule.pathParts)) findings.push({
16235
+ kind: rule.kind,
16236
+ path: rule.path,
16237
+ detail: rule.detail,
16238
+ deprecatedSince: rule.deprecatedSince
16239
+ });
16240
+ const defaultThinkingFinding = findings.find((f) => f.path === "default_thinking");
16241
+ if (defaultThinkingFinding) {
16242
+ const thinking = config.thinking;
16243
+ if (thinking && (thinking.mode !== void 0 || thinking.effort !== void 0)) {
16244
+ defaultThinkingFinding.kind = "removed";
16245
+ defaultThinkingFinding.detail = "Already superseded by [thinking] block.";
16246
+ }
16247
+ }
16248
+ const UNKNOWN_SKIP_PATHS = new Set(DEPRECATED_FIELD_RULES.map((r) => r.pathParts.join(".")));
16249
+ const byfShapeKeys = /* @__PURE__ */ new Set();
16250
+ for (const key of Object.keys(ByfConfigSchema.shape)) {
16251
+ byfShapeKeys.add(key);
16252
+ byfShapeKeys.add(camelToSnakeStatic(key));
16253
+ }
16254
+ for (const rawKey of Object.keys(raw)) {
16255
+ if (rawKey === "raw") continue;
16256
+ if (UNKNOWN_SKIP_PATHS.has(rawKey)) continue;
16257
+ const camelKey = snakeToCamelStatic(rawKey);
16258
+ if (!byfShapeKeys.has(camelKey) && !byfShapeKeys.has(rawKey)) findings.push({
16259
+ kind: "unknown",
16260
+ path: rawKey,
16261
+ detail: `Field "${rawKey}" is not recognized by the current schema. Its value has been ignored. This may be a typo or a field from a previous version.`
16262
+ });
16263
+ }
16264
+ const nestedFindings = scanNestedUnknowns(raw, UNKNOWN_SKIP_PATHS);
16265
+ findings.push(...nestedFindings);
16266
+ }
16267
+ if (config.models) {
16268
+ const validCapsLower = new Set(VALID_CAPABILITIES.map((c) => c.toLowerCase()));
16269
+ for (const [alias, modelConfig] of Object.entries(config.models)) if (modelConfig.capabilities) for (let i = 0; i < modelConfig.capabilities.length; i++) {
16270
+ const cap = modelConfig.capabilities[i];
16271
+ if (cap === void 0) continue;
16272
+ if (!validCapsLower.has(cap.toLowerCase())) findings.push({
16273
+ kind: "invalid-value",
16274
+ path: `models.${alias}.capabilities[${i}]`,
16275
+ detail: `"${cap}" is not a valid capability. Valid values: ${VALID_CAPABILITIES.join(", ")}.`
16276
+ });
16277
+ }
16278
+ }
16279
+ const providerKeys = Object.keys(config.providers ?? {});
16280
+ if (config.models) {
16281
+ for (const [alias, modelConfig] of Object.entries(config.models)) if (!providerKeys.includes(modelConfig.provider)) findings.push({
16282
+ kind: "dangling",
16283
+ path: `models.${alias}.provider`,
16284
+ detail: `Model alias "${alias}" references provider "${modelConfig.provider}", which does not exist in [providers].`
16285
+ });
16286
+ }
16287
+ if (config.defaultProvider !== void 0 && !providerKeys.includes(config.defaultProvider)) findings.push({
16288
+ kind: "dangling",
16289
+ path: "default_provider",
16290
+ detail: `Default provider "${config.defaultProvider}" does not exist in [providers].`
16291
+ });
16292
+ const modelKeys = Object.keys(config.models ?? {});
16293
+ if (config.defaultModel !== void 0 && !modelKeys.includes(config.defaultModel)) findings.push({
16294
+ kind: "dangling",
16295
+ path: "default_model",
16296
+ detail: `Default model "${config.defaultModel}" does not exist in [models].`
16297
+ });
16298
+ return findings;
16299
+ }
16300
+ /**
16301
+ * Apply automatic fixes to a `ByfConfig` by deleting every path registered in
16302
+ * `DEPRECATED_FIELD_RULES` from `config.raw`.
16303
+ *
16304
+ * The `_findings` parameter is intentionally **not consulted** — deletion is
16305
+ * driven exclusively by the whitelist in `DEPRECATED_FIELD_RULES`. This ensures
16306
+ * that a call to `applyFixes` always produces a clean config regardless of
16307
+ * what analysis step previously ran.
16308
+ *
16309
+ * This is a **pure** function — it returns a new config object without
16310
+ * mutating the input.
16311
+ */
16312
+ function applyFixes(config, findings) {
16313
+ const newRaw = rawShallowClone(config.raw);
16314
+ for (const rule of DEPRECATED_FIELD_RULES) deletePath(newRaw, rule.pathParts);
16315
+ let newConfig = {
16316
+ ...config,
16317
+ raw: newRaw
16318
+ };
16319
+ if (findings.find((f) => f.kind === "migrated" && f.path === "default_thinking")) {
16320
+ const rawValue = config.raw?.["default_thinking"];
16321
+ const isTruthy = rawValue === true || rawValue === "true" || rawValue === 1;
16322
+ newConfig = {
16323
+ ...newConfig,
16324
+ thinking: isTruthy ? {
16325
+ mode: "on",
16326
+ effort: "high"
16327
+ } : { mode: "off" }
16328
+ };
16329
+ }
16330
+ return newConfig;
16331
+ }
16332
+ /** Convert snake_case to camelCase (static helper for unknown detection). */
16333
+ function snakeToCamelStatic(str) {
16334
+ return str.replaceAll(/_([a-z])/g, (_, ch) => ch.toUpperCase());
16335
+ }
16336
+ /** Convert camelCase to snake_case (static helper for unknown detection). */
16337
+ function camelToSnakeStatic(str) {
16338
+ return str.replaceAll(/[A-Z]/g, (ch) => `_${ch.toLowerCase()}`);
16339
+ }
16340
+ /** True when `value` is a non-null, non-array object. */
16341
+ function isRecord(value) {
16342
+ return typeof value === "object" && value !== null && !Array.isArray(value);
16343
+ }
16344
+ /**
16345
+ * Walk `root` along `pathParts` checking that **every** segment exists.
16346
+ *
16347
+ * Returns `true` iff all parts exist as own keys (or inherited keys — TOML
16348
+ * parse results are plain objects so the distinction doesn't matter here).
16349
+ */
16350
+ function pathExistsInRaw(root, pathParts) {
16351
+ let current = root;
16352
+ for (const part of pathParts) {
16353
+ if (!isRecord(current) || !(part in current)) return false;
16354
+ current = current[part];
16355
+ }
16356
+ return true;
16357
+ }
16358
+ /**
16359
+ * Delete a leaf (or entire sub-tree) from `root` following `pathParts`.
16360
+ *
16361
+ * If the parent after deletion becomes empty it is cleaned up as well
16362
+ * (recursive upward), so that a service table cleared of all deprecated
16363
+ * keys does not leave behind an empty `{}`.
16364
+ */
16365
+ function deletePath(root, pathParts) {
16366
+ if (pathParts.length === 0) return;
16367
+ const parentParts = pathParts.slice(0, -1);
16368
+ const leafKey = pathParts.at(-1);
16369
+ let current;
16370
+ if (parentParts.length === 0) current = root;
16371
+ else current = traverseTo(root, parentParts);
16372
+ if (current === void 0) return;
16373
+ const keyExisted = leafKey in current;
16374
+ delete current[leafKey];
16375
+ if (keyExisted && parentParts.length > 0 && Object.keys(current).length === 0) deletePath(root, parentParts);
16376
+ }
16377
+ /**
16378
+ * Walk `root` along `pathParts` returning the penultimate record, or
16379
+ * `undefined` if any segment is missing.
16380
+ */
16381
+ function traverseTo(root, pathParts) {
16382
+ let current = root;
16383
+ for (const part of pathParts) {
16384
+ if (!isRecord(current) || !(part in current)) return void 0;
16385
+ current = current[part];
16386
+ }
16387
+ return isRecord(current) ? current : void 0;
16388
+ }
16389
+ /**
16390
+ * Shallow-clone `raw`: each nested record is also shallow-cloned so that
16391
+ * mutations in `applyFixes` do not affect the original object.
16392
+ */
16393
+ function rawShallowClone(raw) {
16394
+ if (!isRecord(raw)) return {};
16395
+ const clone = {};
16396
+ for (const [key, value] of Object.entries(raw)) clone[key] = isRecord(value) ? { ...value } : value;
16397
+ return clone;
16398
+ }
16399
+ /**
16400
+ * Build a set of all valid keys (camelCase + snake_case) from a zod
16401
+ * object schema's `.shape`.
16402
+ */
16403
+ function getShapeKeySet(schema) {
16404
+ const keys = /* @__PURE__ */ new Set();
16405
+ for (const key of Object.keys(schema.shape)) {
16406
+ keys.add(key);
16407
+ keys.add(camelToSnakeStatic(key));
16408
+ }
16409
+ return keys;
16410
+ }
16411
+ /**
16412
+ * Scan known container keys in `raw` for sub-keys that don't match the
16413
+ * corresponding schema shape. Unknown paths that overlap with
16414
+ * `skipPaths` (e.g. already reported deprecated fields) are skipped.
16415
+ *
16416
+ * Detects e.g. `models.gpt4.max_context_tokns` (typo) or
16417
+ * `providers.anthropic.api_kei` (typo).
16418
+ */
16419
+ function scanNestedUnknowns(raw, skipPaths) {
16420
+ const findings = [];
16421
+ const containers = [
16422
+ {
16423
+ rawKey: "models",
16424
+ isRecord: true,
16425
+ schema: ModelAliasSchema
16426
+ },
16427
+ {
16428
+ rawKey: "providers",
16429
+ isRecord: true,
16430
+ schema: ProviderConfigSchema
16431
+ },
16432
+ {
16433
+ rawKey: "services",
16434
+ isRecord: false,
16435
+ schema: ServicesConfigSchema
16436
+ },
16437
+ {
16438
+ rawKey: "background",
16439
+ isRecord: false,
16440
+ schema: BackgroundConfigSchema
16441
+ },
16442
+ {
16443
+ rawKey: "loop_control",
16444
+ isRecord: false,
16445
+ schema: LoopControlSchema
16446
+ },
16447
+ {
16448
+ rawKey: "thinking",
16449
+ isRecord: false,
16450
+ schema: ThinkingConfigSchema
16451
+ },
16452
+ {
16453
+ rawKey: "permission",
16454
+ isRecord: false,
16455
+ schema: PermissionConfigSchema,
16456
+ legacyKeys: [
16457
+ "deny",
16458
+ "allow",
16459
+ "ask"
16460
+ ]
16461
+ }
16462
+ ];
16463
+ const schemaKeySets = /* @__PURE__ */ new Map();
16464
+ containers.forEach((entry, index) => {
16465
+ const base = getShapeKeySet(entry.schema);
16466
+ if (entry.legacyKeys) for (const k of entry.legacyKeys) base.add(k);
16467
+ schemaKeySets.set(index, base);
16468
+ });
16469
+ for (const [entryIndex, entry] of containers.entries()) {
16470
+ if (!(entry.rawKey in raw)) continue;
16471
+ const rawValue = raw[entry.rawKey];
16472
+ if (!isRecord(rawValue)) continue;
16473
+ if (entry.isRecord) for (const [itemKey, itemValue] of Object.entries(rawValue)) {
16474
+ if (!isRecord(itemValue)) continue;
16475
+ const validKeys = schemaKeySets.get(entryIndex);
16476
+ for (const subKey of Object.keys(itemValue)) if (!validKeys.has(subKey)) {
16477
+ const path = `${entry.rawKey}.${itemKey}.${subKey}`;
16478
+ if (!skipPaths.has(path)) findings.push({
16479
+ kind: "unknown",
16480
+ path,
16481
+ detail: `Field "${subKey}" is not recognized in ${entry.rawKey}.${itemKey}. This may be a typo or a field from a previous version.`
16482
+ });
16483
+ }
16484
+ }
16485
+ else {
16486
+ const validKeys = schemaKeySets.get(entryIndex);
16487
+ for (const subKey of Object.keys(rawValue)) if (!validKeys.has(subKey)) {
16488
+ const path = `${entry.rawKey}.${subKey}`;
16489
+ if (!skipPaths.has(path)) findings.push({
16490
+ kind: "unknown",
16491
+ path,
16492
+ detail: `Field "${subKey}" is not recognized in [${entry.rawKey}]. This may be a typo or a field from a previous version.`
16493
+ });
16494
+ }
16495
+ }
16496
+ }
16497
+ return findings;
16498
+ }
16499
+ //#endregion
16500
+ //#region src/version.ts
16501
+ function getCoreVersion() {
16502
+ try {
16503
+ const raw = readFileSync(fileURLToPath(new URL("../package.json", import.meta.url)), "utf-8");
16504
+ const pkg = JSON.parse(raw);
16505
+ return typeof pkg.version === "string" ? pkg.version : "0.0.0";
16506
+ } catch {
16507
+ return "0.0.0";
16508
+ }
16509
+ }
16510
+ //#endregion
16511
+ //#region src/mcp/client-shared.ts
16512
+ const BYF_MCP_CLIENT_VERSION = getCoreVersion();
16513
+ /**
16514
+ * Build the `RequestOptions` object accepted by the MCP SDK's `callTool`,
16515
+ * including either the configured tool-call timeout, an in-flight abort
16516
+ * signal, both, or neither. Returns `undefined` when nothing needs to be
16517
+ * passed so the SDK falls back to its defaults.
16518
+ */
16519
+ function buildRequestOptions(toolCallTimeoutMs, signal) {
16520
+ if (toolCallTimeoutMs === void 0 && signal === void 0) return void 0;
16521
+ return {
16522
+ timeout: toolCallTimeoutMs,
16523
+ signal
16524
+ };
16525
+ }
16526
+ function toMcpToolDefinition(tool) {
16527
+ return {
16528
+ name: tool.name,
16529
+ description: tool.description ?? "",
16530
+ inputSchema: tool.inputSchema
16531
+ };
16532
+ }
16533
+ /**
16534
+ * Normalise the SDK's `callTool` return into kosong's {@link MCPToolResult}.
16535
+ * The SDK can return either the modern `{ content, isError }` shape or a
16536
+ * legacy `{ toolResult }` shape; we collapse the legacy shape to a single
16537
+ * text content block.
16538
+ */
16539
+ function toMcpToolResult(result) {
16540
+ if (typeof result === "object" && result !== null && "content" in result) {
16541
+ const typed = result;
16542
+ if (Array.isArray(typed.content)) return {
16543
+ content: typed.content,
16544
+ isError: typed.isError === true
16545
+ };
16546
+ }
16547
+ if (typeof result === "object" && result !== null && "toolResult" in result) {
16548
+ const legacy = result.toolResult;
16549
+ return {
16550
+ content: [{
16551
+ type: "text",
16552
+ text: typeof legacy === "string" ? legacy : JSON.stringify(legacy)
16553
+ }],
16554
+ isError: false
16555
+ };
16556
+ }
16557
+ return {
16558
+ content: [],
16559
+ isError: false
16560
+ };
16561
+ }
16562
+ //#endregion
16563
+ //#region src/mcp/client-http.ts
16564
+ /**
16565
+ * Wraps the SDK streamable-HTTP transport as a kosong {@link MCPClient}.
16566
+ * Static bearer tokens are looked up from `process.env[bearerTokenEnvVar]`.
16567
+ * OAuth providers are attached separately by the connection manager.
16568
+ */
16569
+ var HttpMcpClient = class {
16570
+ client;
16571
+ transport;
16572
+ toolCallTimeoutMs;
16573
+ started = false;
16574
+ closed = false;
16575
+ ready = false;
16576
+ hooksInstalled = false;
16577
+ unexpectedCloseListener;
16578
+ lastTransportError;
16579
+ pendingUnexpectedClose;
16580
+ unexpectedCloseFired = false;
16581
+ constructor(config, options = {}) {
16582
+ const headers = buildMcpHttpHeaders(config, options.envLookup ?? ((name) => process.env[name]));
16583
+ this.transport = new StreamableHTTPClientTransport(new URL(config.url), {
15960
16584
  requestInit: headers !== void 0 ? { headers } : void 0,
15961
16585
  fetch: options.fetch,
15962
16586
  authProvider: options.oauthProvider
@@ -17608,11 +18232,15 @@ function createProxiedFetch(deps) {
17608
18232
  const proxyUrl = getProxyForUrl(url, envLookup, sysProxy);
17609
18233
  const noProxyMatch = noProxy !== void 0 && isNoProxyHost(hostname, noProxy);
17610
18234
  const controller = new AbortController();
17611
- const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
18235
+ const timeoutId = setTimeout(() => {
18236
+ controller.abort();
18237
+ }, REQUEST_TIMEOUT_MS);
17612
18238
  if (init?.signal) if (init.signal.aborted) {
17613
18239
  clearTimeout(timeoutId);
17614
18240
  controller.abort();
17615
- } else init.signal.addEventListener("abort", () => controller.abort(), { once: true });
18241
+ } else init.signal.addEventListener("abort", () => {
18242
+ controller.abort();
18243
+ }, { once: true });
17616
18244
  const mergedInit = {
17617
18245
  ...init,
17618
18246
  signal: controller.signal
@@ -17624,7 +18252,7 @@ function createProxiedFetch(deps) {
17624
18252
  return response;
17625
18253
  } catch (error) {
17626
18254
  clearTimeout(timeoutId);
17627
- if (isRetryableError(error) && proxyUrl && !noProxyMatch) return await retryViaProxy(input, init, proxyUrl, innerFetch);
18255
+ if (isRetryableError(error) && proxyUrl && !noProxyMatch) return retryViaProxy(input, init, proxyUrl, innerFetch);
17628
18256
  throw error;
17629
18257
  }
17630
18258
  };
@@ -17632,11 +18260,15 @@ function createProxiedFetch(deps) {
17632
18260
  }
17633
18261
  async function retryViaProxy(input, init, proxyUrl, innerFetch) {
17634
18262
  const controller = new AbortController();
17635
- const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
18263
+ const timeoutId = setTimeout(() => {
18264
+ controller.abort();
18265
+ }, REQUEST_TIMEOUT_MS);
17636
18266
  if (init?.signal) if (init.signal.aborted) {
17637
18267
  clearTimeout(timeoutId);
17638
18268
  controller.abort();
17639
- } else init.signal.addEventListener("abort", () => controller.abort(), { once: true });
18269
+ } else init.signal.addEventListener("abort", () => {
18270
+ controller.abort();
18271
+ }, { once: true });
17640
18272
  const dispatcher = new ProxyAgent(proxyUrl);
17641
18273
  const retryInit = {
17642
18274
  ...init,
@@ -17786,82 +18418,188 @@ var RemoteFetchURLProvider = class {
17786
18418
  }
17787
18419
  };
17788
18420
  //#endregion
17789
- //#region src/tools/providers/remote-web-search.ts
17790
- var RemoteWebSearchProvider = class {
17791
- tokenProvider;
17792
- apiKey;
18421
+ //#region src/tools/providers/router.ts
18422
+ var AllProvidersFailedError = class extends Error {
18423
+ lastError;
18424
+ constructor(lastError) {
18425
+ super(`All search providers failed. Last error: ${lastError ?? "unknown"}`);
18426
+ this.lastError = lastError;
18427
+ this.name = "AllProvidersFailedError";
18428
+ }
18429
+ };
18430
+ var PriorityRouter = class {
18431
+ providers;
18432
+ constructor(providers) {
18433
+ this.providers = providers;
18434
+ }
18435
+ async search(query, options) {
18436
+ let lastError;
18437
+ for (const provider of this.providers) try {
18438
+ return await provider.search(query, options);
18439
+ } catch (error) {
18440
+ lastError = error instanceof Error ? error.message : String(error);
18441
+ }
18442
+ throw new AllProvidersFailedError(lastError);
18443
+ }
18444
+ };
18445
+ //#endregion
18446
+ //#region src/tools/providers/exa.ts
18447
+ var ExaWebSearchProvider = class {
18448
+ apiKeys;
17793
18449
  baseUrl;
17794
- defaultHeaders;
17795
- customHeaders;
17796
18450
  fetchImpl;
17797
18451
  constructor(options) {
17798
- this.tokenProvider = options.tokenProvider;
17799
- this.apiKey = options.apiKey;
17800
- this.baseUrl = options.baseUrl;
17801
- this.defaultHeaders = options.defaultHeaders ?? {};
17802
- this.customHeaders = options.customHeaders ?? {};
18452
+ this.apiKeys = options.apiKeys;
18453
+ this.baseUrl = options.baseUrl ?? "https://api.exa.ai/search";
17803
18454
  this.fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
17804
18455
  }
17805
18456
  async search(query, options) {
17806
- const body = {
17807
- text_query: query,
17808
- limit: options?.limit ?? 5,
17809
- enable_page_crawling: options?.includeContent ?? false,
17810
- timeout_seconds: 30
18457
+ const limit = options?.limit ?? 5;
18458
+ const includeContent = options?.includeContent ?? false;
18459
+ const contents = {};
18460
+ if (includeContent) contents.text = { maxCharacters: 1e4 };
18461
+ else contents.highlights = {
18462
+ query,
18463
+ maxCharacters: 300
17811
18464
  };
17812
- const bodyJson = JSON.stringify(body);
17813
- const response = await this.post(bodyJson);
17814
- if (response.status === 401) {
17815
- const detail = await safeReadText(response);
17816
- throw new Error(`Remote search request failed: HTTP 401 (auth/unauthorized). ${detail}`.trim());
17817
- }
17818
- if (response.status !== 200) {
17819
- const detail = await safeReadText(response);
17820
- throw new Error(`Remote search request failed: HTTP ${String(response.status)}. ${detail}`.trim());
17821
- }
17822
- const json = await response.json();
17823
- return (Array.isArray(json.search_results) ? json.search_results : []).map((r) => {
17824
- const out = {
17825
- title: r.title ?? "",
17826
- url: r.url ?? "",
17827
- snippet: r.snippet ?? ""
17828
- };
17829
- if (typeof r.date === "string" && r.date.length > 0) out.date = r.date;
17830
- if (typeof r.content === "string" && r.content.length > 0) out.content = r.content;
17831
- return out;
18465
+ const body = JSON.stringify({
18466
+ query,
18467
+ numResults: limit,
18468
+ contents
17832
18469
  });
18470
+ let lastError;
18471
+ for (const apiKey of this.apiKeys) try {
18472
+ const response = await this.fetchImpl(this.baseUrl, {
18473
+ method: "POST",
18474
+ headers: {
18475
+ Authorization: `Bearer ${apiKey}`,
18476
+ "Content-Type": "application/json"
18477
+ },
18478
+ body
18479
+ });
18480
+ if (!response.ok) {
18481
+ const detail = (await response.text().catch(() => "")).slice(0, 200);
18482
+ throw new Error(`Exa search failed: HTTP ${response.status}${detail ? `: ${detail}` : ""}`);
18483
+ }
18484
+ const json = await response.json();
18485
+ return (Array.isArray(json.results) ? json.results : []).map((r) => {
18486
+ const out = {
18487
+ title: r.title ?? "",
18488
+ url: r.url ?? "",
18489
+ snippet: ""
18490
+ };
18491
+ if (includeContent && typeof r.text === "string") {
18492
+ out.snippet = r.text.slice(0, 300);
18493
+ if (r.text.length > 0) out.content = r.text;
18494
+ } else if (Array.isArray(r.highlights) && r.highlights.length > 0) out.snippet = r.highlights[0].slice(0, 300);
18495
+ if (typeof r.publishedDate === "string" && r.publishedDate.length > 0) out.date = r.publishedDate;
18496
+ return out;
18497
+ });
18498
+ } catch (error) {
18499
+ lastError = error instanceof Error ? error : new Error(String(error));
18500
+ }
18501
+ throw lastError ?? /* @__PURE__ */ new Error("Exa search failed: no API keys configured");
17833
18502
  }
17834
- async post(bodyJson) {
17835
- const accessToken = await this.resolveApiKey();
17836
- return this.fetchImpl(this.baseUrl, {
17837
- method: "POST",
17838
- headers: {
17839
- ...this.defaultHeaders,
17840
- Authorization: `Bearer ${accessToken}`,
17841
- "Content-Type": "application/json",
17842
- ...this.customHeaders
17843
- },
17844
- body: bodyJson
17845
- });
18503
+ };
18504
+ registerProvider("exa", ExaWebSearchProvider);
18505
+ //#endregion
18506
+ //#region src/tools/providers/brave.ts
18507
+ var BraveWebSearchProvider = class {
18508
+ apiKeys;
18509
+ baseUrl;
18510
+ fetchImpl;
18511
+ constructor(options) {
18512
+ this.apiKeys = options.apiKeys;
18513
+ this.baseUrl = options.baseUrl ?? "https://api.search.brave.com/res/v1/web/search";
18514
+ this.fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
17846
18515
  }
17847
- async resolveApiKey() {
17848
- if (this.tokenProvider !== void 0) try {
17849
- return await this.tokenProvider.getAccessToken();
18516
+ async search(query, options) {
18517
+ const limit = options?.limit ?? 5;
18518
+ const url = new URL(this.baseUrl);
18519
+ url.searchParams.set("q", query);
18520
+ url.searchParams.set("count", String(limit));
18521
+ let lastError;
18522
+ for (const apiKey of this.apiKeys) try {
18523
+ const response = await this.fetchImpl(url.toString(), {
18524
+ method: "GET",
18525
+ headers: {
18526
+ "Accept": "application/json",
18527
+ "Accept-Encoding": "gzip",
18528
+ "X-Subscription-Token": apiKey
18529
+ }
18530
+ });
18531
+ if (!response.ok) {
18532
+ const detail = (await response.text().catch(() => "")).slice(0, 200);
18533
+ throw new Error(`Brave search failed: HTTP ${response.status}${detail ? `: ${detail}` : ""}`);
18534
+ }
18535
+ const json = await response.json();
18536
+ return (Array.isArray(json.web?.results) ? json.web.results : []).map((r) => {
18537
+ const out = {
18538
+ title: r.title ?? "",
18539
+ url: r.url ?? "",
18540
+ snippet: r.description ?? ""
18541
+ };
18542
+ if (typeof r.age === "string" && r.age.length > 0) out.date = r.age;
18543
+ return out;
18544
+ });
17850
18545
  } catch (error) {
17851
- if (this.apiKey !== void 0 && this.apiKey.length > 0) return this.apiKey;
17852
- throw error;
18546
+ lastError = error instanceof Error ? error : new Error(String(error));
17853
18547
  }
17854
- if (this.apiKey !== void 0 && this.apiKey.length > 0) return this.apiKey;
17855
- throw new Error("Remote search service is not configured: missing API key or token provider.");
18548
+ throw lastError ?? /* @__PURE__ */ new Error("Brave search failed: no API keys configured");
17856
18549
  }
17857
18550
  };
17858
- async function safeReadText(response) {
17859
- try {
17860
- return await response.text();
17861
- } catch {
17862
- return "";
18551
+ registerProvider("brave", BraveWebSearchProvider);
18552
+ //#endregion
18553
+ //#region src/tools/providers/firecrawl.ts
18554
+ var FirecrawlWebSearchProvider = class {
18555
+ apiKeys;
18556
+ baseUrl;
18557
+ fetchImpl;
18558
+ constructor(options) {
18559
+ this.apiKeys = options.apiKeys;
18560
+ this.baseUrl = options.baseUrl ?? "https://api.firecrawl.dev/v2/search";
18561
+ this.fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
17863
18562
  }
17864
- }
18563
+ async search(query, options) {
18564
+ const limit = options?.limit ?? 5;
18565
+ const includeContent = options?.includeContent ?? false;
18566
+ const requestBody = {
18567
+ query,
18568
+ limit
18569
+ };
18570
+ if (includeContent) requestBody.scrapeOptions = { formats: ["markdown"] };
18571
+ const body = JSON.stringify(requestBody);
18572
+ let lastError;
18573
+ for (const apiKey of this.apiKeys) try {
18574
+ const response = await this.fetchImpl(this.baseUrl, {
18575
+ method: "POST",
18576
+ headers: {
18577
+ "Authorization": `Bearer ${apiKey}`,
18578
+ "Content-Type": "application/json"
18579
+ },
18580
+ body
18581
+ });
18582
+ if (!response.ok) {
18583
+ const detail = (await response.text().catch(() => "")).slice(0, 200);
18584
+ throw new Error(`Firecrawl search failed: HTTP ${response.status}${detail ? `: ${detail}` : ""}`);
18585
+ }
18586
+ const json = await response.json();
18587
+ return (Array.isArray(json.data?.web) ? json.data.web : []).map((r) => {
18588
+ const out = {
18589
+ title: r.title ?? "",
18590
+ url: r.url ?? "",
18591
+ snippet: r.description ?? ""
18592
+ };
18593
+ if (includeContent && typeof r.markdown === "string" && r.markdown.length > 0) out.content = r.markdown;
18594
+ return out;
18595
+ });
18596
+ } catch (error) {
18597
+ lastError = error instanceof Error ? error : new Error(String(error));
18598
+ }
18599
+ throw lastError ?? /* @__PURE__ */ new Error("Firecrawl search failed: no API keys configured");
18600
+ }
18601
+ };
18602
+ registerProvider("firecrawl", FirecrawlWebSearchProvider);
17865
18603
  //#endregion
17866
18604
  //#region src/utils/environment.ts
17867
18605
  /**
@@ -18123,15 +18861,6 @@ var SessionAPIImpl = class {
18123
18861
  getModel({ agentId, ...payload }) {
18124
18862
  return this.getAgent(agentId).getModel(payload);
18125
18863
  }
18126
- enterPlan({ agentId, ...payload }) {
18127
- return this.getAgent(agentId).enterPlan(payload);
18128
- }
18129
- cancelPlan({ agentId, ...payload }) {
18130
- return this.getAgent(agentId).cancelPlan(payload);
18131
- }
18132
- clearPlan({ agentId, ...payload }) {
18133
- return this.getAgent(agentId).clearPlan(payload);
18134
- }
18135
18864
  beginCompaction({ agentId, ...payload }) {
18136
18865
  return this.getAgent(agentId).beginCompaction(payload);
18137
18866
  }
@@ -18172,9 +18901,6 @@ var SessionAPIImpl = class {
18172
18901
  getPermission({ agentId, ...payload }) {
18173
18902
  return this.getAgent(agentId).getPermission(payload);
18174
18903
  }
18175
- getPlan({ agentId, ...payload }) {
18176
- return this.getAgent(agentId).getPlan(payload);
18177
- }
18178
18904
  getUsage({ agentId, ...payload }) {
18179
18905
  return this.getAgent(agentId).getUsage(payload);
18180
18906
  }
@@ -19322,207 +20048,6 @@ async function readOptionalFile(path) {
19322
20048
  }
19323
20049
  }
19324
20050
  //#endregion
19325
- //#region src/providers/runtime-provider.ts
19326
- function resolveRuntimeProvider(input) {
19327
- const modelName = input.model ?? input.config.defaultModel;
19328
- if (modelName === void 0) throw new ByfError(ErrorCodes.CONFIG_INVALID, "No model is selected. Set default_model in config.toml or pass a configured model alias.");
19329
- const alias = input.config.models?.[modelName];
19330
- if (alias === void 0) throw new ByfError(ErrorCodes.CONFIG_INVALID, `Model "${modelName}" is not configured in config.toml. Add a [models."${modelName}"] entry with max_context_size.`);
19331
- const resolvedModel = alias.model;
19332
- const providerName = alias.provider ?? input.config.defaultProvider;
19333
- const providerConfig = providerName === void 0 ? void 0 : input.config.providers[providerName];
19334
- if (providerName === void 0) throw new ByfError(ErrorCodes.CONFIG_INVALID, `Model "${modelName}" must define a provider in config.toml.`);
19335
- if (providerConfig === void 0) throw new ByfError(ErrorCodes.CONFIG_INVALID, `Provider "${providerName}" for model "${modelName}" is not configured.`);
19336
- if (!Number.isInteger(alias.maxContextSize) || alias.maxContextSize <= 0) throw new ByfError(ErrorCodes.CONFIG_INVALID, `Model "${modelName}" must define a positive max_context_size in config.toml.`);
19337
- if (input.validateCredentials !== false && providerConfig.type !== "vertexai" && providerConfig.oauth === void 0 && providerApiKey(providerConfig) === void 0) throw new ByfError(ErrorCodes.CONFIG_INVALID, `Provider "${providerName}" has no credentials configured. Set apiKey, oauth, or a provider env API key in config.toml.`);
19338
- const provider = toKosongProviderConfig(providerConfig, resolvedModel, input.byfRequestHeaders, alias.maxOutputSize, alias.reasoningKey, input.promptCacheKey);
19339
- return {
19340
- modelName,
19341
- providerName,
19342
- modelCapabilities: resolveModelCapabilities(alias, provider),
19343
- provider
19344
- };
19345
- }
19346
- async function resolveRuntimeProviderWithOAuth(input) {
19347
- const resolved = resolveRuntimeProvider(input);
19348
- const resolveAuth = createRuntimeProviderAuthResolver(input, resolved);
19349
- if (resolveAuth === void 0) return resolved;
19350
- await resolveAuth();
19351
- return {
19352
- ...resolved,
19353
- resolveAuth
19354
- };
19355
- }
19356
- function createRuntimeProviderAuthResolver(input, resolved = resolveRuntimeProvider(input)) {
19357
- const providerName = resolved.providerName;
19358
- if (providerName === void 0) return void 0;
19359
- const providerConfig = input.config.providers[providerName];
19360
- if (providerConfig?.oauth === void 0) return void 0;
19361
- if (providerApiKey(providerConfig) !== void 0) throw new ByfError(ErrorCodes.CONFIG_INVALID, `Provider "${providerName}" has both apiKey and oauth set in config.toml — they are mutually exclusive. Remove one.`);
19362
- const tokenProvider = input.resolveOAuthTokenProvider?.(providerName, providerConfig.oauth);
19363
- if (tokenProvider === void 0) return async () => {
19364
- throw new ByfError(ErrorCodes.AUTH_LOGIN_REQUIRED, `OAuth provider "${providerName}" requires login before it can be used.`);
19365
- };
19366
- return async (options) => {
19367
- let apiKey;
19368
- try {
19369
- apiKey = await tokenProvider.getAccessToken(options?.forceRefresh === true ? { force: true } : void 0);
19370
- } catch (error) {
19371
- if (!isAuthLoginRequired(error)) (input.log ?? log).warn("oauth token fetch failed", {
19372
- providerName,
19373
- error
19374
- });
19375
- throw new ByfError(ErrorCodes.AUTH_LOGIN_REQUIRED, `OAuth provider "${providerName}" requires login before it can be used.`, { cause: error });
19376
- }
19377
- if (apiKey.trim().length === 0) throw new ByfError(ErrorCodes.AUTH_LOGIN_REQUIRED, `OAuth provider "${providerName}" requires login before it can be used.`);
19378
- return { apiKey };
19379
- };
19380
- }
19381
- function isAuthLoginRequired(error) {
19382
- return isByfError(error) && error.code === ErrorCodes.AUTH_LOGIN_REQUIRED;
19383
- }
19384
- function resolveModelCapabilities(alias, provider) {
19385
- const capabilities = new Set((alias.capabilities ?? []).map((capability) => capability.trim().toLowerCase()));
19386
- const has = (capability) => capabilities.has(capability);
19387
- const providerCapability = createProvider(providerForCapabilityProbe(provider)).getCapability?.(provider.model) ?? UNKNOWN_CAPABILITY;
19388
- return {
19389
- image_in: has("image_in") || providerCapability.image_in,
19390
- video_in: has("video_in") || providerCapability.video_in,
19391
- audio_in: has("audio_in") || providerCapability.audio_in,
19392
- thinking: has("thinking") || has("always_thinking") || providerCapability.thinking,
19393
- tool_use: has("tool_use") || providerCapability.tool_use,
19394
- thinking_effort: has("thinking_effort") || providerCapability.thinking_effort,
19395
- thinking_xhigh: has("thinking_xhigh") || providerCapability.thinking_xhigh,
19396
- thinking_max: has("thinking_max") || providerCapability.thinking_max,
19397
- max_context_tokens: alias.maxContextSize
19398
- };
19399
- }
19400
- function toKosongProviderConfig(provider, model, byfRequestHeaders, maxOutputSize, reasoningKey, promptCacheKey) {
19401
- switch (provider.type) {
19402
- case "anthropic": return {
19403
- type: "anthropic",
19404
- model,
19405
- baseUrl: providerValue(provider.baseUrl, provider.env, "ANTHROPIC_BASE_URL"),
19406
- apiKey: providerApiKey(provider),
19407
- ...maxOutputSize !== void 0 ? { defaultMaxTokens: maxOutputSize } : {},
19408
- ...defaultHeadersField(provider.customHeaders)
19409
- };
19410
- case "openai-completions": {
19411
- const defaultHeaders = {
19412
- ...byfRequestHeaders,
19413
- ...provider.customHeaders
19414
- };
19415
- const generationKwargs = {
19416
- prompt_cache_key: promptCacheKey,
19417
- extra_body: provider.extraBody
19418
- };
19419
- if (Object.keys(defaultHeaders).length === 0) return {
19420
- type: "openai-completions",
19421
- model,
19422
- baseUrl: providerValue(provider.baseUrl, provider.env, "BYF_BASE_URL"),
19423
- reasoningKey,
19424
- thinkingEffortKey: provider.thinkingEffortKey,
19425
- generationKwargs,
19426
- apiKey: providerApiKey(provider)
19427
- };
19428
- return {
19429
- type: "openai-completions",
19430
- model,
19431
- baseUrl: providerValue(provider.baseUrl, provider.env, "BYF_BASE_URL"),
19432
- reasoningKey,
19433
- thinkingEffortKey: provider.thinkingEffortKey,
19434
- generationKwargs,
19435
- defaultHeaders,
19436
- apiKey: providerApiKey(provider)
19437
- };
19438
- }
19439
- case "google-genai": return {
19440
- type: "google-genai",
19441
- model,
19442
- apiKey: providerApiKey(provider)
19443
- };
19444
- case "openai_responses": return {
19445
- type: "openai_responses",
19446
- model,
19447
- baseUrl: providerValue(provider.baseUrl, provider.env, "OPENAI_BASE_URL"),
19448
- apiKey: providerApiKey(provider),
19449
- ...defaultHeadersField(provider.customHeaders)
19450
- };
19451
- case "vertexai": return {
19452
- type: "vertexai",
19453
- model,
19454
- vertexai: hasVertexAIServiceEnv(provider),
19455
- apiKey: hasVertexAIServiceEnv(provider) ? void 0 : providerApiKey(provider),
19456
- project: vertexAIProject(provider),
19457
- location: vertexAILocation(provider)
19458
- };
19459
- default: {
19460
- const exhaustive = provider.type;
19461
- throw new ByfError(ErrorCodes.MODEL_CONFIG_INVALID, `Unsupported provider type: ${String(exhaustive)}`);
19462
- }
19463
- }
19464
- }
19465
- function defaultHeadersField(headers) {
19466
- if (headers === void 0 || Object.keys(headers).length === 0) return {};
19467
- return { defaultHeaders: { ...headers } };
19468
- }
19469
- function providerForCapabilityProbe(provider) {
19470
- if (provider.type === "vertexai") return {
19471
- ...provider,
19472
- vertexai: false,
19473
- project: void 0,
19474
- location: void 0,
19475
- apiKey: provider.apiKey === void 0 || provider.apiKey.length === 0 ? "capability-probe" : provider.apiKey
19476
- };
19477
- if (provider.apiKey !== void 0 && provider.apiKey.length > 0) return provider;
19478
- return {
19479
- ...provider,
19480
- apiKey: "capability-probe"
19481
- };
19482
- }
19483
- function providerApiKey(provider) {
19484
- switch (provider.type) {
19485
- case "anthropic": return providerValue(provider.apiKey, provider.env, "ANTHROPIC_API_KEY");
19486
- case "openai_responses": return providerValue(provider.apiKey, provider.env, "OPENAI_API_KEY");
19487
- case "openai-completions": return providerValue(provider.apiKey, provider.env, "BYF_API_KEY");
19488
- case "google-genai": return providerValue(provider.apiKey, provider.env, "GOOGLE_API_KEY");
19489
- case "vertexai": return nonEmptyString$1(provider.apiKey) ?? envValue(provider.env, "VERTEXAI_API_KEY") ?? envValue(provider.env, "GOOGLE_API_KEY");
19490
- default: {
19491
- const exhaustive = provider.type;
19492
- throw new ByfError(ErrorCodes.MODEL_CONFIG_INVALID, `Unsupported provider type: ${String(exhaustive)}`);
19493
- }
19494
- }
19495
- }
19496
- function hasVertexAIServiceEnv(provider) {
19497
- return vertexAIProject(provider) !== void 0 && vertexAILocation(provider) !== void 0;
19498
- }
19499
- function vertexAIProject(provider) {
19500
- return envValue(provider.env, "GOOGLE_CLOUD_PROJECT");
19501
- }
19502
- function vertexAILocation(provider) {
19503
- return envValue(provider.env, "GOOGLE_CLOUD_LOCATION") ?? locationFromVertexAIBaseUrl(provider.baseUrl);
19504
- }
19505
- function providerValue(configured, env, envKey) {
19506
- return nonEmptyString$1(configured) ?? envValue(env, envKey);
19507
- }
19508
- function envValue(env, key) {
19509
- return nonEmptyString$1(env?.[key]);
19510
- }
19511
- function nonEmptyString$1(value) {
19512
- const trimmed = value?.trim();
19513
- return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
19514
- }
19515
- function locationFromVertexAIBaseUrl(baseUrl) {
19516
- const url = nonEmptyString$1(baseUrl);
19517
- if (url === void 0) return void 0;
19518
- try {
19519
- const host = new URL(url).hostname;
19520
- return host.endsWith("-aiplatform.googleapis.com") ? nonEmptyString$1(host.slice(0, -26)) : void 0;
19521
- } catch {
19522
- return;
19523
- }
19524
- }
19525
- //#endregion
19526
20051
  //#region src/providers/provider-manager.ts
19527
20052
  var ProviderManager = class ProviderManager {
19528
20053
  options;
@@ -19854,15 +20379,6 @@ var ByfCore = class {
19854
20379
  getModel({ sessionId, ...payload }) {
19855
20380
  return this.sessionApi(sessionId).getModel(payload);
19856
20381
  }
19857
- enterPlan({ sessionId, ...payload }) {
19858
- return this.sessionApi(sessionId).enterPlan(payload);
19859
- }
19860
- cancelPlan({ sessionId, ...payload }) {
19861
- return this.sessionApi(sessionId).cancelPlan(payload);
19862
- }
19863
- clearPlan({ sessionId, ...payload }) {
19864
- return this.sessionApi(sessionId).clearPlan(payload);
19865
- }
19866
20382
  beginCompaction({ sessionId, ...payload }) {
19867
20383
  return this.sessionApi(sessionId).beginCompaction(payload);
19868
20384
  }
@@ -19902,9 +20418,6 @@ var ByfCore = class {
19902
20418
  getPermission({ sessionId, ...payload }) {
19903
20419
  return this.sessionApi(sessionId).getPermission(payload);
19904
20420
  }
19905
- getPlan({ sessionId, ...payload }) {
19906
- return this.sessionApi(sessionId).getPlan(payload);
19907
- }
19908
20421
  getUsage({ sessionId, ...payload }) {
19909
20422
  return this.sessionApi(sessionId).getUsage(payload);
19910
20423
  }
@@ -19992,8 +20505,8 @@ async function createRuntimeConfig(input) {
19992
20505
  systemProxy: () => detectSystemProxy()
19993
20506
  });
19994
20507
  const localFetcher = new LocalFetchURLProvider({ fetchImpl: proxiedFetch });
19995
- const searchService = input.config.services?.byfSearch;
19996
- const fetchService = input.config.services?.byfFetch;
20508
+ const fetchService = input.config.services?.fetchUrl;
20509
+ const webSearchConfig = input.config.services?.webSearch;
19997
20510
  return {
19998
20511
  kaos: localKaos,
19999
20512
  osEnv: await detectEnvironmentFromNode(),
@@ -20005,12 +20518,11 @@ async function createRuntimeConfig(input) {
20005
20518
  fetchImpl: proxiedFetch,
20006
20519
  ...serviceCredentials(fetchService, input.resolveOAuthTokenProvider)
20007
20520
  }),
20008
- webSearcher: searchService?.baseUrl === void 0 ? void 0 : new RemoteWebSearchProvider({
20009
- baseUrl: searchService.baseUrl,
20010
- defaultHeaders: input.byfRequestHeaders,
20011
- fetchImpl: proxiedFetch,
20012
- ...serviceCredentials(searchService, input.resolveOAuthTokenProvider)
20013
- })
20521
+ webSearcher: webSearchConfig === void 0 ? void 0 : new PriorityRouter([...webSearchConfig.providers].toSorted((a, b) => a.priority - b.priority).map((p) => createProvider$1(p.type, {
20522
+ apiKeys: p.apiKeys,
20523
+ baseUrl: p.baseUrl,
20524
+ fetchImpl: proxiedFetch
20525
+ })))
20014
20526
  };
20015
20527
  }
20016
20528
  function serviceCredentials(service, resolveOAuthTokenProvider) {
@@ -20045,7 +20557,6 @@ async function resumeSessionResult(summary, session, warning) {
20045
20557
  context,
20046
20558
  replay: agent.replayBuilder.buildResult(),
20047
20559
  permission,
20048
- plan: null,
20049
20560
  usage,
20050
20561
  tools: await api.getTools({ agentId }),
20051
20562
  toolStore: agent.tools.storeData(),
@@ -20105,4 +20616,4 @@ function parsePositiveInt(value) {
20105
20616
  return n;
20106
20617
  }
20107
20618
  //#endregion
20108
- export { AGENT_WIRE_PROTOCOL_VERSION, Agent, BYF_ERROR_INFO, BackgroundConfigSchema, ByfConfigPatchSchema, ByfConfigSchema, ByfCore, ByfError, ByfServiceConfigSchema, ErrorCodes, HookDefSchema, LoopControlSchema, MCP_OAUTH_AUTHORIZATION_URL_TOOL_UPDATE, McpServerConfigSchema, McpServerHttpConfigSchema, McpServerStdioConfigSchema, ModelAliasSchema, OAuthRefSchema, PermissionConfigSchema, PermissionModeSchema, PermissionRuleDecisionSchema, PermissionRuleSchema, PermissionRuleScopeSchema, ProviderConfigSchema, ProviderTypeSchema, ServicesConfigSchema, Session, SessionSubagentHost, ThinkingConfigSchema, USER_PROMPT_ORIGIN, WIRE_PROTOCOL_VERSION, buildExportManifest, buildPromptPlan, collectFilesRecursive, configToTomlData, createRPC, ensureByfHome, ensureConfigFile, exportSessionDirectory, flushDiagnosticLogs, formatConfigValidationError, fromByfErrorPayload, getDefaultConfig, getRootLogger, isByfError, log, makeErrorPayload, mergeConfigPatch, normalizeTimestampMs, parseBooleanEnv, parseConfigString, proxyWithExtraPayload, readConfigFile, redact, resolveByfHome, resolveConfigPath, resolveConfigValue, resolveGlobalLogPath, resolveLoggingConfig, scanSessionWire, toByfErrorPayload, transformTomlData, validateConfig, writeConfigFile, writeExportZip };
20619
+ export { AGENT_WIRE_PROTOCOL_VERSION, Agent, BYF_ERROR_INFO, BackgroundConfigSchema, ByfConfigPatchSchema, ByfConfigSchema, ByfCore, ByfError, ByfServiceConfigSchema, DEPRECATED_FIELD_RULES, ErrorCodes, HookDefSchema, LoopControlSchema, MCP_OAUTH_AUTHORIZATION_URL_TOOL_UPDATE, McpServerConfigSchema, McpServerHttpConfigSchema, McpServerStdioConfigSchema, ModelAliasSchema, OAuthRefSchema, PermissionConfigSchema, PermissionModeSchema, PermissionRuleDecisionSchema, PermissionRuleSchema, PermissionRuleScopeSchema, ProviderConfigSchema, ProviderTypeSchema, ServicesConfigSchema, Session, SessionSubagentHost, ThinkingConfigSchema, USER_PROMPT_ORIGIN, WIRE_PROTOCOL_VERSION, WebSearchConfigSchema, WebSearchProviderConfigSchema, analyzeConfig, applyFixes, buildExportManifest, buildPromptPlan, collectFilesRecursive, configToTomlData, createRPC, ensureByfHome, ensureConfigFile, exportSessionDirectory, flushDiagnosticLogs, formatConfigValidationError, fromByfErrorPayload, getDefaultConfig, getRootLogger, isByfError, log, makeErrorPayload, mergeConfigPatch, normalizeTimestampMs, parseBooleanEnv, parseConfigString, proxyWithExtraPayload, readConfigFile, redact, resolveByfHome, resolveConfigPath, resolveConfigValue, resolveGlobalLogPath, resolveLoggingConfig, scanSessionWire, toByfErrorPayload, transformTomlData, validateConfig, writeConfigFile, writeExportZip };