@bike4mind/cli 0.22.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/dist/{AgentHistoryStore-7zQUOxkA.mjs → AgentHistoryStore-kT9eNMRO.mjs} +8889 -7858
  2. package/dist/{ApiClient-XdY9O3nz.mjs → ApiClient-BOWpVvTq.mjs} +2 -2
  3. package/dist/{ConfigStore-8_0WsN5r.mjs → ConfigStore-DdHHCH2t.mjs} +1451 -249
  4. package/dist/{SandboxOrchestrator-BFPVpmB5.mjs → SandboxOrchestrator-BbMDgjzr.mjs} +1 -1
  5. package/dist/{SandboxOrchestrator-C8uleDn2.mjs → SandboxOrchestrator-CIegJrCj.mjs} +1 -1
  6. package/dist/{buildAgent-CvPRH2n-.mjs → buildAgent-B_kArQ_Y.mjs} +18 -9
  7. package/dist/commands/acpCommand.mjs +34 -13
  8. package/dist/commands/apiCommand.mjs +1 -1
  9. package/dist/commands/doctorCommand.mjs +1 -1
  10. package/dist/commands/envCommand.mjs +1 -1
  11. package/dist/commands/headlessCommand.mjs +73 -33
  12. package/dist/commands/mcpCommand.mjs +8 -13
  13. package/dist/commands/pluginCommand.mjs +9 -15
  14. package/dist/commands/updateCommand.mjs +1 -1
  15. package/dist/{createFile-DPv180yF-BnWFIxey.mjs → createFile-B8bur5Rb-CVzCarEA.mjs} +2 -2
  16. package/dist/{deleteFile-BdjUwUQF-B3XOJmg3.mjs → deleteFile-9B3gW_Nb-DG2sovIl.mjs} +2 -2
  17. package/dist/{globFiles-DjfDGaUK-CNR8pMRC.mjs → globFiles-CwJ8qmYo-BR5b2KvO.mjs} +3 -2
  18. package/dist/{grepSearch-BaYUfIYs-C-fxWc9G.mjs → grepSearch-BgoOOwGe-DtlV8Gn-.mjs} +4 -4
  19. package/dist/index.mjs +212 -47
  20. package/dist/{package-OZdXqn0e.mjs → package-Bqg2LSnH.mjs} +1 -1
  21. package/dist/{pathValidation-D8tjkQXE-1HwvsuYT.mjs → pathValidation-BRqf4HFX-CHwtwp3O.mjs} +7 -3
  22. package/dist/{serve-CEqSZwl6.mjs → serve-DO9Edl5S.mjs} +2 -2
  23. package/dist/types-CdIKgWWe.mjs +3 -0
  24. package/dist/{types-LyRNHOiS.mjs → types-F61_hxmG.mjs} +2 -0
  25. package/package.json +28 -28
  26. package/dist/types-CqscS34o.mjs +0 -3
@@ -1,10 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { t as DEFAULT_SANDBOX_CONFIG } from "./types-LyRNHOiS.mjs";
2
+ import { t as DEFAULT_SANDBOX_CONFIG } from "./types-F61_hxmG.mjs";
3
3
  import "crypto";
4
4
  import { existsSync, promises } from "fs";
5
5
  import os, { homedir } from "os";
6
6
  import path from "path";
7
7
  import { v4 } from "uuid";
8
+ import * as path$1 from "node:path";
9
+ import * as fs$2 from "node:fs";
8
10
  import * as z$2 from "zod";
9
11
  import z, { ZodError, z as z$1 } from "zod";
10
12
  import dayjs from "dayjs";
@@ -13,25 +15,65 @@ import utc from "dayjs/plugin/utc.js";
13
15
  import relativeTime from "dayjs/plugin/relativeTime.js";
14
16
  import localizedFormat from "dayjs/plugin/localizedFormat.js";
15
17
  import { isAxiosError } from "axios";
18
+ import { homedir as homedir$1 } from "node:os";
16
19
  import fs$1 from "fs/promises";
17
20
  process.env.APP_NAME;
18
21
  process.env.WEBSITE_URL;
22
+ const SNIPPET_META_OPEN = "<!--snippet-meta";
23
+ const SNIPPET_META_CLOSE = "-->";
24
+ /** Index of the first non-whitespace character at or after `from`. */
25
+ const skipSpaceForward = (text, from) => {
26
+ let i = from;
27
+ while (i < text.length && /\s/.test(text[i])) i++;
28
+ return i;
29
+ };
30
+ /** Index just past the last non-whitespace character before `before`. */
31
+ const skipSpaceBackward = (text, before) => {
32
+ let i = before;
33
+ while (i > 0 && /\s/.test(text[i - 1])) i--;
34
+ return i;
35
+ };
36
+ /**
37
+ * Split a prompt into its `<!--snippet-meta {...}-->` sections and the text around them.
38
+ *
39
+ * Scanned with indexOf rather than a regex. The regex this replaces backtracked
40
+ * super-linearly on a marker whose JSON never closed, which invited a parse cap - but a
41
+ * cap could not be applied safely here: the section pattern was terminated by
42
+ * `(?=<!--snippet-meta|$)`, so capping moved `$` and changed section SHAPE, not just
43
+ * length. Past the cap a snippet re-emitted as a `text` section, and callers that skip
44
+ * snippets when collecting URLs to fetch (utils/src/llm/utils.ts) would start fetching
45
+ * them. A linear scan needs no cap, so the shape is the same at every input length.
46
+ */
19
47
  const extractSnippetMeta = (content) => {
20
- const snippetRegex = /<!--snippet-meta\s*(\{[\s\S]*?\})\s*-->[\n\s]*([\s\S]*?)(?=<!--snippet-meta|$)/g;
21
48
  const sections = [];
22
- let lastIndex = 0;
23
- let match;
24
- while ((match = snippetRegex.exec(content)) !== null) {
25
- if (match.index > lastIndex) {
26
- const textBefore = content.slice(lastIndex, match.index).trim();
27
- if (textBefore) sections.push({
28
- type: "text",
29
- content: textBefore
30
- });
49
+ let cursor = 0;
50
+ let search = 0;
51
+ while (search < content.length) {
52
+ const open = content.indexOf(SNIPPET_META_OPEN, search);
53
+ if (open === -1) break;
54
+ const metaStart = skipSpaceForward(content, open + 16);
55
+ if (content[metaStart] !== "{") {
56
+ search = open + 16;
57
+ continue;
58
+ }
59
+ let close = content.indexOf(SNIPPET_META_CLOSE, metaStart);
60
+ while (close !== -1) {
61
+ const metaEnd = skipSpaceBackward(content, close);
62
+ if (metaEnd > metaStart && content[metaEnd - 1] === "}") break;
63
+ close = content.indexOf(SNIPPET_META_CLOSE, close + 3);
31
64
  }
65
+ if (close === -1) break;
66
+ const bodyStart = close + 3;
67
+ const nextMarker = content.indexOf(SNIPPET_META_OPEN, bodyStart);
68
+ const bodyEnd = nextMarker === -1 ? content.length : nextMarker;
69
+ const textBefore = content.slice(cursor, open).trim();
70
+ if (textBefore) sections.push({
71
+ type: "text",
72
+ content: textBefore
73
+ });
32
74
  try {
33
- const meta = JSON.parse(match[1]);
34
- const snippetContent = match[2].trim();
75
+ const meta = JSON.parse(content.slice(metaStart, skipSpaceBackward(content, close)));
76
+ const snippetContent = content.slice(bodyStart, bodyEnd).trim();
35
77
  if (meta && snippetContent) sections.push({
36
78
  type: "snippet",
37
79
  meta,
@@ -40,15 +82,14 @@ const extractSnippetMeta = (content) => {
40
82
  } catch (e) {
41
83
  console.error("Error parsing snippet meta:", e);
42
84
  }
43
- lastIndex = match.index + match[0].length;
44
- }
45
- if (lastIndex < content.length) {
46
- const remainingText = content.slice(lastIndex).trim();
47
- if (remainingText) sections.push({
48
- type: "text",
49
- content: remainingText
50
- });
85
+ cursor = bodyEnd;
86
+ search = bodyEnd;
51
87
  }
88
+ const remainingText = content.slice(cursor).trim();
89
+ if (remainingText) sections.push({
90
+ type: "text",
91
+ content: remainingText
92
+ });
52
93
  return { sections };
53
94
  };
54
95
  function getHeader$1(headers, name) {
@@ -858,6 +899,19 @@ let KnowledgeType = /* @__PURE__ */ function(KnowledgeType) {
858
899
  * fails the build.
859
900
  */
860
901
  const QUEST_ERROR_CODES = ["insufficient_credits", "spend_cap_exceeded"];
902
+ /**
903
+ * Single source of truth for `IChatHistoryItem.type` / quest `type`. Restated by hand in
904
+ * `schemas/actions.ts`, `schemas/chat.ts`, and `notebookExportService/types.ts` before this
905
+ * const existed, which is exactly how `voice_transcript` went missing from the export contract
906
+ * once already - derive from this tuple instead of retyping the union.
907
+ */
908
+ const CHAT_HISTORY_ITEM_TYPES = [
909
+ "message",
910
+ "oob",
911
+ "error",
912
+ "system",
913
+ "voice_transcript"
914
+ ];
861
915
  z$1.union([
862
916
  z$1.literal("string"),
863
917
  z$1.literal("number"),
@@ -982,6 +1036,12 @@ const IMAGE_SIZE_CONSTRAINTS = {
982
1036
  "2160x3840"
983
1037
  ],
984
1038
  defaultSize: "1024x1024",
1039
+ /**
1040
+ * Accepted by the API, and what generate sends when no size is asked for, but it is not a
1041
+ * resolution - so it is deliberately out of `sizes`, the preset list the size picker renders.
1042
+ * The one spelling: schemas/openai.ts and utils/imageSizes.ts both read it from here.
1043
+ */
1044
+ autoSize: "auto",
985
1045
  /** Constraints for custom/flexible sizes */
986
1046
  constraints: {
987
1047
  maxEdge: 3840,
@@ -990,6 +1050,29 @@ const IMAGE_SIZE_CONSTRAINTS = {
990
1050
  edgeMultiple: 16,
991
1051
  maxAspectRatio: 3
992
1052
  }
1053
+ },
1054
+ /** Also the only sizes the variation endpoint accepts - variations are dall-e-2 only. */
1055
+ DALL_E_2: {
1056
+ sizes: [
1057
+ "256x256",
1058
+ "512x512",
1059
+ "1024x1024"
1060
+ ],
1061
+ defaultSize: "1024x1024"
1062
+ },
1063
+ /**
1064
+ * dall-e-3 is no longer in ImageModels, but the generate path still accepts its sizes
1065
+ * for callers holding a persisted one. Listed separately to record what each tier really
1066
+ * accepts; isSupportedImageSize currently measures both against the union of the two
1067
+ * (OPENAI_LEGACY_IMAGE_SIZES), so the split is documentation rather than enforcement.
1068
+ */
1069
+ DALL_E_3: {
1070
+ sizes: [
1071
+ "1024x1024",
1072
+ "1792x1024",
1073
+ "1024x1792"
1074
+ ],
1075
+ defaultSize: "1024x1024"
993
1076
  }
994
1077
  };
995
1078
  /**
@@ -1106,6 +1189,7 @@ let ChatModels = /* @__PURE__ */ function(ChatModels) {
1106
1189
  ChatModels["KIMI_K2_5_BEDROCK"] = "moonshotai.kimi-k2.5";
1107
1190
  ChatModels["KIMI_K2_THINKING_BEDROCK"] = "moonshot.kimi-k2-thinking";
1108
1191
  ChatModels["DEEPSEEK_FLASH"] = "deepseek-flash";
1192
+ ChatModels["DEEPSEEK_V4_PRO"] = "deepseek-v4-pro";
1109
1193
  return ChatModels;
1110
1194
  }({});
1111
1195
  const CHAT_MODELS = Object.values(ChatModels);
@@ -1241,7 +1325,8 @@ const NO_TEMPERATURE_MODELS = /* @__PURE__ */ new Set([
1241
1325
  "kimi-k2.7-code-highspeed",
1242
1326
  "kimi-k2.6",
1243
1327
  "kimi-k2.5",
1244
- "deepseek-flash"
1328
+ "deepseek-flash",
1329
+ "deepseek-v4-pro"
1245
1330
  ]);
1246
1331
  /**
1247
1332
  * Models whose safety classifiers can decline a request with `stop_reason: 'refusal'`
@@ -1648,7 +1733,8 @@ z$1.object({
1648
1733
  "ok",
1649
1734
  "error",
1650
1735
  "timeout",
1651
- "refusal"
1736
+ "refusal",
1737
+ "degenerate"
1652
1738
  ]).default("ok"),
1653
1739
  latencyMs: z$1.number().optional(),
1654
1740
  /** Originating surface (web/cli/api/agent/system); unset => unclassified. */
@@ -1973,6 +2059,7 @@ const MODEL_INFO_FIELD_GROUP_OF = {
1973
2059
  backend: "identity",
1974
2060
  contextWindow: "limits",
1975
2061
  max_tokens: "limits",
2062
+ maxOutputTokensDerived: "limits",
1976
2063
  can_think: "reasoning",
1977
2064
  thinkingStyle: "reasoning",
1978
2065
  can_stream: "modalities",
@@ -2245,6 +2332,16 @@ const DiscoveryPriceSkip = z$1.object({
2245
2332
  modelId: z$1.string().min(1),
2246
2333
  reason: z$1.string()
2247
2334
  });
2335
+ /**
2336
+ * A configured source the run never attempted, and why. Produced by
2337
+ * `skipReasonFor` in runModelDiscovery (services/modelDiscoveryService); the
2338
+ * reason stays a plain string rather than that module's SourceSkipReason union
2339
+ * so a new reason needs no schema or UI change.
2340
+ */
2341
+ const DiscoverySkippedSource = z$1.object({
2342
+ name: z$1.string().min(1),
2343
+ reason: z$1.string()
2344
+ });
2248
2345
  const DiscoveryLifecycleTransition = z$1.object({
2249
2346
  modelId: z$1.string().min(1),
2250
2347
  /** Absent when no row in force carried a lifecycle for this model. */
@@ -2301,6 +2398,8 @@ z$1.object({
2301
2398
  */
2302
2399
  mode: z$1.enum(DISCOVERY_RUN_MODES).optional(),
2303
2400
  sources: z$1.array(DiscoverySourceReport).optional(),
2401
+ /** Configured sources this run never attempted; disjoint from `sources`. */
2402
+ skippedSources: z$1.array(DiscoverySkippedSource).optional(),
2304
2403
  joinCoverage: z$1.array(DiscoveryJoinCoverage).optional(),
2305
2404
  /** Ids no aggregator matched: a work item, not a log line. */
2306
2405
  unmatchedIds: z$1.array(z$1.string()).optional(),
@@ -2417,6 +2516,59 @@ let McpServerName = /* @__PURE__ */ function(McpServerName) {
2417
2516
  McpServerName["Notion"] = "notion";
2418
2517
  return McpServerName;
2419
2518
  }({});
2519
+ const ROLLUP_DAY_MS = 864e5;
2520
+ /**
2521
+ * Reads a rollup bound as a UTC instant. The schema below accepts an offset-less value (what a
2522
+ * date picker emits), and a bare `new Date` would read that in the host's local zone - so the
2523
+ * same query would cover a different window depending on where it ran.
2524
+ */
2525
+ function parseFeedbackRollupBound(value) {
2526
+ return new Date(/([Zz]|[+-]\d{2}:?\d{2})$/.test(value) ? value : `${value}Z`);
2527
+ }
2528
+ z$1.object({
2529
+ from: z$1.iso.datetime({
2530
+ offset: true,
2531
+ local: true
2532
+ }),
2533
+ to: z$1.iso.datetime({
2534
+ offset: true,
2535
+ local: true
2536
+ })
2537
+ }).refine((query) => parseFeedbackRollupBound(query.from) < parseFeedbackRollupBound(query.to), {
2538
+ message: "from must be strictly before to",
2539
+ path: ["from"]
2540
+ }).refine((query) => parseFeedbackRollupBound(query.to).getTime() - parseFeedbackRollupBound(query.from).getTime() < 366 * ROLLUP_DAY_MS, {
2541
+ message: `window must not exceed 366 days`,
2542
+ path: ["to"]
2543
+ });
2544
+ const feedbackCountBucketSchema = z$1.object({
2545
+ key: z$1.string(),
2546
+ count: z$1.number()
2547
+ });
2548
+ const orgFeedbackSummaryCountsSchema = z$1.object({
2549
+ totals: z$1.object({ count: z$1.number() }),
2550
+ byDay: z$1.array(z$1.object({
2551
+ day: z$1.string(),
2552
+ count: z$1.number()
2553
+ })),
2554
+ bySubject: z$1.array(feedbackCountBucketSchema),
2555
+ byType: z$1.array(feedbackCountBucketSchema),
2556
+ byStatus: z$1.array(feedbackCountBucketSchema),
2557
+ byTag: z$1.array(feedbackCountBucketSchema),
2558
+ byTagTruncated: z$1.boolean().optional()
2559
+ });
2560
+ z$1.object({
2561
+ summaryJobId: z$1.string(),
2562
+ organizationId: z$1.string(),
2563
+ range: z$1.object({
2564
+ from: z$1.string(),
2565
+ to: z$1.string()
2566
+ }),
2567
+ generatedAt: z$1.string(),
2568
+ model: z$1.string(),
2569
+ summary: z$1.string(),
2570
+ counts: orgFeedbackSummaryCountsSchema
2571
+ });
2420
2572
  /**
2421
2573
  * Check if a value is a placeholder (not configured or SST default).
2422
2574
  * Uses case-insensitive comparison and trims whitespace to prevent bypass attempts.
@@ -2465,10 +2617,16 @@ function isPlaceholderApiKey(value) {
2465
2617
  /**
2466
2618
  * Lake lifecycle. Stable states (draft/active/archived/deleted) plus transitional
2467
2619
  * states (archiving/unarchiving/restoring/deleting/purging) that exist to drive UI and make a crashed
2468
- * mid-operation observable. draft -> active is one-way. It happens implicitly once the lake
2469
- * holds its first member file (see `activateIfDraft` below), and unconditionally when an
2470
- * archived or deleted lake is restored, which is how an empty lake can end up active.
2620
+ * mid-operation observable.
2621
+ *
2622
+ * draft <-> active is a DELIBERATE, two-way move: `promoteDataLake` publishes a draft (the only
2623
+ * door onto `activateIfDraft` below) and `demoteDataLake` reverses it. Neither fires as a side
2624
+ * effect of adding content - a draft lake that fills up with files stays a draft, excluded from
2625
+ * grounding, until an owner or admin explicitly promotes it. Restoring an archived or
2626
+ * deleted lake still lands unconditionally on `active`, which is how an empty lake can end up
2627
+ * active without ever passing through an explicit promote.
2471
2628
  *
2629
+
2472
2630
  * `purging` is the one transitional state that is NOT recoverable by retrying the same action:
2473
2631
  * it is claimed the moment a phase-2 hard delete is ACCEPTED (#1744), before the background
2474
2632
  * sweep runs, so that `listDeletedDataLakes` stops offering Restore on a lake whose
@@ -2580,6 +2738,7 @@ const LAKE_CONFIG_FIELD_AUDIT = {
2580
2738
  totalChunkedChars: "excluded",
2581
2739
  embeddingSpendMicroUsd: "excluded",
2582
2740
  lastSyncAt: "excluded",
2741
+ lastHealthCheckedAt: "excluded",
2583
2742
  filesDeletedAt: "excluded",
2584
2743
  filesArchivedAt: "excluded",
2585
2744
  lakeMemoryExtractionAt: "excluded",
@@ -2958,6 +3117,26 @@ function isSupportedFabFileMimeType(mimeType) {
2958
3117
  return !!mimeType && SUPPORTED_FAB_FILE_MIME_TYPES.has(mimeType);
2959
3118
  }
2960
3119
  /**
3120
+ * Known MIME types emitted by the generated-audio endpoints (TTS + sound
3121
+ * effects). Deliberately SEPARATE from SupportedFabFileMimeTypes: that set is
3122
+ * the ingest pipeline's chunk+vectorize allowlist, and audio is intentionally
3123
+ * NOT vectorizable and NOT attachable to a chat completion (no model accepts
3124
+ * audio input). Kept only for extension mapping / documentation; the runtime
3125
+ * guard below matches any `audio/*` so unusual provider subtypes are still
3126
+ * treated as audio (i.e. still excluded from every LLM path).
3127
+ */
3128
+ let AudioMimeType = /* @__PURE__ */ function(AudioMimeType) {
3129
+ AudioMimeType["MP3"] = "audio/mpeg";
3130
+ AudioMimeType["WAV"] = "audio/wav";
3131
+ AudioMimeType["OPUS"] = "audio/opus";
3132
+ AudioMimeType["AAC"] = "audio/aac";
3133
+ AudioMimeType["FLAC"] = "audio/flac";
3134
+ AudioMimeType["PCM"] = "audio/pcm";
3135
+ AudioMimeType["OGG"] = "audio/ogg";
3136
+ AudioMimeType["WEBM"] = "audio/webm";
3137
+ return AudioMimeType;
3138
+ }({});
3139
+ /**
2961
3140
  * Is this an audio MIME type? Matches any `audio/*` (not just the known set)
2962
3141
  * so the LLM-exclusion and vectorization-skip guards fail safe for any audio
2963
3142
  * subtype a provider might emit.
@@ -3115,6 +3294,18 @@ const isRenderableModelType = (type) => MODEL_INFO_TYPES.includes(type);
3115
3294
  */
3116
3295
  const isMediaModelType = (type) => type === "image" || type === "video";
3117
3296
  /**
3297
+ * The modalities promptMeta.model.type is allowed to record. Deliberately NARROWER than
3298
+ * MODEL_INFO_TYPES: 'speech-to-text' is served by its own transcription route, never by a
3299
+ * completion, so recording it would put a value in the field that no reader narrows on.
3300
+ * PromptMetaModelSchema.type (schemas/promptMeta) builds its z.enum from this const, and
3301
+ * QuestModelType (apps/client admin reporting) is an alias of the type - one source, three uses.
3302
+ */
3303
+ const PROMPT_META_MODEL_TYPES = [
3304
+ "text",
3305
+ "image",
3306
+ "video"
3307
+ ];
3308
+ /**
3118
3309
  * The record -> ModelInfo adapter: one place where every ModelInfo field a
3119
3310
  * catalog record does not carry gets its default. Each default degrades to the
3120
3311
  * visible, recoverable behavior rather than the silent or expensive one.
@@ -3133,6 +3324,7 @@ function toModelInfo(record) {
3133
3324
  backend: record.backend,
3134
3325
  contextWindow: record.contextWindow,
3135
3326
  max_tokens: record.maxOutputTokens ?? Math.min(record.contextWindow, 4096),
3327
+ ...record.maxOutputTokens === void 0 ? { maxOutputTokensDerived: true } : {},
3136
3328
  pricing: {},
3137
3329
  can_stream: record.canStream,
3138
3330
  can_think: record.reasoning?.supported ?? false,
@@ -3238,7 +3430,7 @@ function toModelRecord(info) {
3238
3430
  type: info.type,
3239
3431
  name: info.name,
3240
3432
  contextWindow: info.contextWindow,
3241
- maxOutputTokens: info.max_tokens,
3433
+ maxOutputTokens: info.maxOutputTokensDerived === true ? void 0 : info.max_tokens,
3242
3434
  canStream: info.can_stream,
3243
3435
  reasoning: info.can_think === void 0 && info.thinkingStyle === void 0 ? void 0 : {
3244
3436
  supported: info.can_think === true,
@@ -3533,7 +3725,7 @@ z$1.object({
3533
3725
  "grounded",
3534
3726
  "surface"
3535
3727
  ]).optional(),
3536
- skip_auto_offers: z$1.boolean().optional().describe("Suppress tools the server would otherwise attach on its own for this session (the knowledge-base search offer, in-app view navigation, blog drafting/editing/publishing, and skill invocation). Tools you request explicitly are unaffected. One system-prompt block goes with them: withholding in-app view navigation also drops the view-registry block that exists only to describe it. No other prompt content changes. This does not switch off retrieval: a session with forced knowledge retrieval still retrieves, and documents already attached to the session are still placed in the prompt directly. Any promptMode suppresses these too, so false has no effect alongside one."),
3728
+ skip_auto_offers: z$1.boolean().optional().describe("Suppress tools the server would otherwise attach on its own for this session (the knowledge-base search offer, in-app view navigation, blog drafting/editing/publishing, skill invocation, and every tool from a connected MCP server). Native tools you request explicitly in `tools` are unaffected, but an MCP server tool cannot be named through this field, so with this flag set a caller with an MCP server connected sees none of its tools regardless of `tools`. One system-prompt block goes with them: withholding in-app view navigation also drops the view-registry block that exists only to describe it. No other prompt content changes. This does not switch off retrieval: a session with forced knowledge retrieval still retrieves, and documents already attached to the session are still placed in the prompt directly. Any promptMode suppresses these too, so false has no effect alongside one."),
3537
3729
  includePromptDetails: z$1.boolean().optional(),
3538
3730
  includeSystemPrompt: z$1.boolean().optional(),
3539
3731
  systemPrompt: z$1.string().max(PROMPT_TEXT_MAX).optional().describe("System-prompt text for this request only, never persisted. Rendered as a defended block appended after every other system-prompt source, with prose instructing the model to defer to organization, session and data-lake guidance. Over the cap is a 422, never truncated.")
@@ -3545,6 +3737,8 @@ z$1.object({
3545
3737
  timestamp: z$1.string(),
3546
3738
  model: z$1.string(),
3547
3739
  message: z$1.string().optional(),
3740
+ type: z$1.enum(CHAT_HISTORY_ITEM_TYPES).optional(),
3741
+ errorCode: z$1.enum(QUEST_ERROR_CODES).optional(),
3548
3742
  tools: z$1.object({
3549
3743
  toolMode: z$1.enum(["fast", "smart"]).optional(),
3550
3744
  autoSelectedTools: z$1.array(z$1.string()).optional(),
@@ -3557,6 +3751,18 @@ z$1.object({
3557
3751
  poll_url: z$1.string().optional()
3558
3752
  })
3559
3753
  });
3754
+ z$1.object({
3755
+ id: z$1.string(),
3756
+ status: z$1.enum([
3757
+ "stopped",
3758
+ "running",
3759
+ "done"
3760
+ ]).optional(),
3761
+ type: z$1.enum(CHAT_HISTORY_ITEM_TYPES).optional(),
3762
+ errorCode: z$1.enum(QUEST_ERROR_CODES).optional(),
3763
+ reply: z$1.string().nullable().optional(),
3764
+ replies: z$1.array(z$1.string()).optional()
3765
+ });
3560
3766
  /**
3561
3767
  * Reusable JSON error envelope (plain; the OpenAPI layer annotates it).
3562
3768
  *
@@ -3774,6 +3980,33 @@ const AGENT_EXECUTION_STATUSES = [
3774
3980
  "aborted"
3775
3981
  ];
3776
3982
  /**
3983
+ * Why a session summarization happened, stamped on `ISession.summaryTrigger`. Single source for
3984
+ * the four places that each used to spell this list out: the Session zod schema
3985
+ * (schemas/actions.ts), the entity type (types/entities/SessionTypes.ts), the Mongoose path
3986
+ * (packages/database SessionModel) and the session.summarize event payload (apps/client
3987
+ * server/utils/eventBus.ts). They drifted - the Mongoose enum said 'milestone'/'growth' for two
3988
+ * values nothing produces - and a drift there is invisible at runtime, because BaseModel's
3989
+ * findOneAndUpdate writes without runValidators.
3990
+ *
3991
+ * 'throttling' is the one member no stored document can carry: shouldSummarizeSession
3992
+ * (b4m-core/services ChatCompletionFeatures) returns it as the reason it declined to summarize,
3993
+ * so it never reaches a write. It stays in the union because that return value is typed as
3994
+ * `ISessionDocument['summaryTrigger']`.
3995
+ *
3996
+ * 'manual' means someone asked for one notebook's summary. The admin sweep (apps/client
3997
+ * server/events/spider.ts) summarizes every un-summarized notebook of the admin who ran it in one
3998
+ * billed pass, so it stamps 'spider' instead: without that, one deliberate click and a whole sweep
3999
+ * are indistinguishable when someone investigates unexpected summarization spend.
4000
+ */
4001
+ const SESSION_SUMMARY_TRIGGERS = [
4002
+ "manual",
4003
+ "project",
4004
+ "earlyMilestone",
4005
+ "contentGrowth",
4006
+ "throttling",
4007
+ "spider"
4008
+ ];
4009
+ /**
3777
4010
  * Operator allow-list for the client-authored Mongo filter carried on a `subscribe_query` frame.
3778
4011
  *
3779
4012
  * The WS data-subscribe handler forwards that filter to `Model.find` and persists it on the
@@ -4042,6 +4275,19 @@ const QuestExportProgressAction = z$1.object({
4042
4275
  droppedQuestCount: z$1.number().optional(),
4043
4276
  clientId: z$1.string().optional()
4044
4277
  });
4278
+ const OrgFeedbackSummaryProgressAction = z$1.object({
4279
+ action: z$1.literal("org_feedback_summary_progress"),
4280
+ summaryJobId: z$1.string(),
4281
+ organizationId: z$1.string(),
4282
+ status: z$1.enum([
4283
+ "processing",
4284
+ "completed",
4285
+ "failed"
4286
+ ]),
4287
+ progress: z$1.number(),
4288
+ errorMessage: z$1.string().optional(),
4289
+ clientId: z$1.string().optional()
4290
+ });
4045
4291
  const SpiderProgressUpdateAction = z$1.object({
4046
4292
  action: z$1.literal("spider_progress"),
4047
4293
  spiderJobId: z$1.string(),
@@ -4126,13 +4372,7 @@ const StreamedChatCompletionAction = z$1.object({
4126
4372
  replies: z$1.array(z$1.string()).optional(),
4127
4373
  images: z$1.array(z$1.string()).optional(),
4128
4374
  videos: z$1.array(z$1.string()).optional(),
4129
- type: z$1.enum([
4130
- "message",
4131
- "oob",
4132
- "error",
4133
- "system",
4134
- "voice_transcript"
4135
- ]),
4375
+ type: z$1.enum(CHAT_HISTORY_ITEM_TYPES),
4136
4376
  status: z$1.enum([
4137
4377
  "stopped",
4138
4378
  "running",
@@ -4933,18 +5173,13 @@ const SessionCreatedAction = shareableDocumentSchema.extend({
4933
5173
  claudeConversationId: z$1.string().optional(),
4934
5174
  summary: z$1.string().optional(),
4935
5175
  summaryAt: z$1.date().optional(),
4936
- summaryTrigger: z$1.enum([
4937
- "manual",
4938
- "project",
4939
- "earlyMilestone",
4940
- "contentGrowth",
4941
- "throttling"
4942
- ]).optional(),
5176
+ summaryTrigger: z$1.enum(SESSION_SUMMARY_TRIGGERS).optional(),
4943
5177
  deletedAt: z$1.date().optional(),
4944
5178
  tags: z$1.array(z$1.object({
4945
5179
  name: z$1.string(),
4946
5180
  strength: z$1.number()
4947
5181
  })).optional(),
5182
+ taggedAt: z$1.date().optional(),
4948
5183
  clonedSourceId: z$1.string().nullable().optional(),
4949
5184
  forkedSourceId: z$1.string().nullable().optional(),
4950
5185
  isAutoNamed: z$1.boolean().optional(),
@@ -5206,6 +5441,7 @@ z$1.discriminatedUnion("action", [
5206
5441
  ResearchTaskStatusUpdateAction,
5207
5442
  NotebookCurationProgressUpdateAction,
5208
5443
  QuestExportProgressAction,
5444
+ OrgFeedbackSummaryProgressAction,
5209
5445
  SpiderProgressUpdateAction,
5210
5446
  SpiderCompleteAction,
5211
5447
  SpiderErrorAction,
@@ -5464,11 +5700,27 @@ const CompletionContentEventSchema = z$1.object({
5464
5700
  stopReason: z$1.string().optional(),
5465
5701
  thinking: z$1.array(z$1.any()).optional()
5466
5702
  });
5703
+ /**
5704
+ * The in-band terminal failure frame. Because this endpoint flushes SSE headers
5705
+ * before it authenticates or prices anything, EVERY failure past the malformed-body
5706
+ * check arrives here under a 200 - there is no pre-stream 422 on this surface the
5707
+ * way there is on /api/chat and /api/embed/chat.
5708
+ *
5709
+ * `code` is the machine-readable classifier, drawn from QUEST_ERROR_CODES (itself a
5710
+ * narrowing of the platform-wide API_ERROR_CODES, so it cannot drift from the codes
5711
+ * the JSON surfaces use). It is what makes mid-generation credit exhaustion
5712
+ * detectable: the `message` is prose and callers must never match on it. Absent for
5713
+ * unclassified failures, so treat it as optional.
5714
+ *
5715
+ * The field is `code` rather than the conventions' `errorCode` because this frame
5716
+ * shipped that way and is a published wire shape; the vocabulary is what had to be
5717
+ * shared, not the key.
5718
+ */
5467
5719
  const CompletionSseErrorEventSchema = z$1.object({
5468
5720
  type: z$1.literal("error"),
5469
5721
  message: z$1.string(),
5470
5722
  requestId: z$1.string().optional(),
5471
- code: z$1.string().optional()
5723
+ code: z$1.enum(QUEST_ERROR_CODES).optional()
5472
5724
  });
5473
5725
  z$1.union([
5474
5726
  CompletionMetaEventSchema,
@@ -6154,21 +6406,9 @@ const ALL_IMAGE_MODELS = [
6154
6406
  ...XAI_IMAGE_MODELS,
6155
6407
  ...GEMINI_IMAGE_MODELS
6156
6408
  ];
6157
- const OPENAI_GPT_IMAGE_1_IMAGE_SIZES = [
6158
- "1024x1024",
6159
- "1024x1536",
6160
- "1536x1024"
6161
- ];
6162
- const OPENAI_GPT_IMAGE_2_IMAGE_SIZES = [
6163
- "1024x1024",
6164
- "1536x1024",
6165
- "1024x1536",
6166
- "2048x2048",
6167
- "2048x1152",
6168
- "3840x2160",
6169
- "2160x3840",
6170
- "auto"
6171
- ];
6409
+ const OPENAI_GPT_IMAGE_1_IMAGE_SIZES = IMAGE_SIZE_CONSTRAINTS.GPT_IMAGE_1.sizes;
6410
+ /** The UI presets plus the tier's non-resolution `autoSize`, which the picker cannot render. */
6411
+ const OPENAI_GPT_IMAGE_2_IMAGE_SIZES = [...IMAGE_SIZE_CONSTRAINTS.GPT_IMAGE_2.sizes, IMAGE_SIZE_CONSTRAINTS.GPT_IMAGE_2.autoSize];
6172
6412
  const BFL_IMAGE_SIZES = ["1024x768"];
6173
6413
  const OPENAI_IMAGE_SIZES = [...OPENAI_GPT_IMAGE_1_IMAGE_SIZES];
6174
6414
  const ALL_IMAGE_SIZES = [
@@ -6178,16 +6418,41 @@ const ALL_IMAGE_SIZES = [
6178
6418
  ];
6179
6419
  z$1.enum(OPENAI_IMAGE_SIZES);
6180
6420
  const ImageSizeSchema = z$1.union([z$1.enum(ALL_IMAGE_SIZES), z$1.string().regex(/^\d+x\d+$/, { error: "Size must be in format 'widthxheight'" })]);
6181
- const OpenAIImageQualitySchema = z$1.enum([
6421
+ const OPENAI_IMAGE_QUALITIES = [
6182
6422
  "standard",
6183
6423
  "hd",
6184
6424
  "low",
6185
6425
  "medium",
6186
6426
  "high",
6187
6427
  "auto"
6188
- ]);
6428
+ ];
6429
+ const OpenAIImageQualitySchema = z$1.enum(OPENAI_IMAGE_QUALITIES);
6430
+ OPENAI_IMAGE_QUALITIES.filter((quality) => quality !== "auto");
6189
6431
  const OpenAIImageStyleSchema = z$1.enum(["vivid", "natural"]);
6190
6432
  /**
6433
+ * Alpha handling for gpt-image renders. `transparent` only yields real alpha when the
6434
+ * output format also carries an alpha channel (png or webp) - OpenAI rejects it alongside
6435
+ * jpeg. Ignored by every other provider (see OpenAIImageService.generate).
6436
+ */
6437
+ const OpenAIImageBackgroundSchema = z$1.enum([
6438
+ "transparent",
6439
+ "opaque",
6440
+ "auto"
6441
+ ]);
6442
+ /** Container for a generated image. `webp` is gpt-image only; BFL and Gemini take png/jpeg. */
6443
+ const ImageOutputFormatSchema = z$1.enum([
6444
+ "png",
6445
+ "jpeg",
6446
+ "webp"
6447
+ ]);
6448
+ /**
6449
+ * Degrade a shared output-format setting to what BFL and Gemini accept, so selecting
6450
+ * webp for gpt-image cannot fail an unrelated render after a model switch.
6451
+ */
6452
+ function toNonWebpOutputFormat(format) {
6453
+ return format === "webp" ? "png" : format;
6454
+ }
6455
+ /**
6191
6456
  * Maps legacy/removed image model IDs to their current replacements.
6192
6457
  * Prevents Zod validation failures when clients send stale persisted model names.
6193
6458
  *
@@ -6230,6 +6495,7 @@ const OpenAIImageGenerationInput = z$1.object({
6230
6495
  response_format: z$1.enum(["b64_json", "url"]).optional(),
6231
6496
  size: ImageSizeSchema.nullable().optional(),
6232
6497
  style: OpenAIImageStyleSchema.optional(),
6498
+ background: OpenAIImageBackgroundSchema.nullable().optional(),
6233
6499
  width: z$1.number().optional(),
6234
6500
  height: z$1.number().optional(),
6235
6501
  aspect_ratio: z$1.string().optional()
@@ -6352,6 +6618,7 @@ z$1.object({
6352
6618
  tags: z$1.array(SessionTagSchema).optional(),
6353
6619
  lastUsedModel: z$1.string().min(1).nullish().describe("Pin a specific model id, or omit/send null to leave the current pin unchanged. Sending null does NOT clear it."),
6354
6620
  forceKnowledgeRetrieval: z$1.boolean().optional(),
6621
+ lakeScope: z$1.array(z$1.string()).nullable().optional().describe("The data lakes this session grounds on, as lake tags (the `datalakeTag` of each lake from GET /api/data-lakes). Send a list to ground only on those lakes, `[]` to ground on no lake at all, or `null` to clear the choice so retrieval falls back to every lake you can reach. Omit to leave the current choice unchanged. Tags naming a lake you cannot reach are ignored at retrieval time rather than rejected here. Narrowing the scope does not by itself turn retrieval on: pair it with `forceKnowledgeRetrieval: true` for a session that is not already grounded. Conversely `[]` leaves a grounded session nothing to retrieve from, so its forced retrieval is skipped rather than run against every lake."),
6355
6622
  propagateToProjects: z$1.boolean().optional().describe("Defaults to true when omitted. When knowledgeIds grows, the newly-added file ids are also appended to every project that contains this session, granting every member of that project access to those files. This propagation is append-only and cannot be undone through the UI - pass false if newly-attached files should not be shared with the project.")
6356
6623
  });
6357
6624
  z$1.object({ id: z$1.string().min(1) });
@@ -6363,6 +6630,8 @@ z$1.object({
6363
6630
  artifactIds: z$1.array(z$1.string()).optional(),
6364
6631
  tags: z$1.array(SessionTagSchema).optional(),
6365
6632
  forceKnowledgeRetrieval: z$1.boolean().optional(),
6633
+ retrievalTags: z$1.array(z$1.string()).optional(),
6634
+ lakeScopeExplicit: z$1.boolean().optional().describe("True when `retrievalTags` is a deliberate choice, so an empty list means \"no lake\" rather than \"any\"."),
6366
6635
  lastUsedModel: z$1.string().nullish(),
6367
6636
  firstCreated: z$1.date(),
6368
6637
  lastUpdated: z$1.date()
@@ -6681,7 +6950,7 @@ function defaultEmbeddingModelForEnv() {
6681
6950
  const hasOllama = !!process.env.OLLAMA_BASE_URL?.trim();
6682
6951
  const hasCloudEmbeddingKey = !isPlaceholderApiKey(process.env.OPENAI_API_KEY) || !isPlaceholderApiKey(process.env.VOYAGE_API_KEY);
6683
6952
  if (selfHost && hasOllama && !hasCloudEmbeddingKey) return "qwen3-embedding:0.6b";
6684
- return "text-embedding-ada-002";
6953
+ return "text-embedding-3-small";
6685
6954
  }
6686
6955
  /**
6687
6956
  * True when this deployment can embed with no provider API key at all: a cloud stage reaches
@@ -6730,15 +6999,15 @@ z$1.union([
6730
6999
  ]);
6731
7000
  /**
6732
7001
  * The measured per-space floors, rendered for an admin-facing description (e.g. "75 for
6733
- * text-embedding-ada-002, 35 for text-embedding-3-small").
7002
+ * text-embedding-ada-002, 58 for text-embedding-3-small").
6734
7003
  *
6735
- * Rendered rather than written out in prose because these numbers are expected to move - 35 is
6736
- * provisional until it is re-derived against a production lake - and a description that restates
6737
- * the table is a wrong number shown to operators the moment it drifts, with nothing failing.
7004
+ * Rendered rather than written out in prose because these numbers move as each space is re-measured
7005
+ * against a production lake, and a description that restates the table is a wrong number shown to
7006
+ * operators the moment it drifts, with nothing failing.
6738
7007
  */
6739
7008
  const forcedRetrievalFloorsBySpaceSummary = Object.entries({
6740
7009
  ["text-embedding-ada-002"]: 75,
6741
- ["text-embedding-3-small"]: 35
7010
+ ["text-embedding-3-small"]: 58
6742
7011
  }).map(([space, pct]) => `${pct} for ${space}`).join(", ");
6743
7012
  /**
6744
7013
  * Default text for the artifact-emission system prompt. Single source of truth used BOTH as the
@@ -7076,6 +7345,7 @@ z$1.enum([
7076
7345
  "kbSearchMinRelevancePct",
7077
7346
  "forcedRetrievalRelativeFloorPct",
7078
7347
  "forcedRetrievalMinSimilarityPct",
7348
+ "forcedRetrievalSpreadFloorPct",
7079
7349
  "dataLakeEmbeddingSpendEnabled",
7080
7350
  "dataLakeEmbeddingBudgetPerRunUsd",
7081
7351
  "dataLakeEmbeddingBudgetPerLakeUsd",
@@ -7313,7 +7583,7 @@ function makeNumberSetting(config) {
7313
7583
  return {
7314
7584
  ...config,
7315
7585
  type: "number",
7316
- schema: numberSchema.prefault(config.defaultValue ?? 0)
7586
+ schema: z$1.preprocess((val) => val === null || typeof val === "string" && val.trim() === "" ? void 0 : val, numberSchema.prefault(config.defaultValue ?? 0))
7317
7587
  };
7318
7588
  }
7319
7589
  function makeBooleanSetting(config) {
@@ -7390,6 +7660,17 @@ const RapidReplySettingsSchema = z$1.object({
7390
7660
  lastUpdated: /* @__PURE__ */ new Date()
7391
7661
  })
7392
7662
  });
7663
+ /**
7664
+ * Canonical repository and branch the What's New generator reads from.
7665
+ *
7666
+ * Every default in the What's New path (zod schema, settings registry, server
7667
+ * config service, cron/backfill fallbacks, admin form seed) must resolve here.
7668
+ * A stale slug is invisible in production: GitHubService.listMergedPullRequests
7669
+ * returns [] for a repository outside the connection allowlist, so generation
7670
+ * records "no PRs today" instead of an error and the surface silently goes dark.
7671
+ */
7672
+ const WHATS_NEW_DEFAULT_REPOSITORY = "Bike4Mind/bike4mind";
7673
+ const WHATS_NEW_DEFAULT_TARGET_BRANCH = "prod";
7393
7674
  const L = {
7394
7675
  temperature: {
7395
7676
  min: 0,
@@ -7493,8 +7774,8 @@ const WhatsNewConfigSchema = z$1.object({
7493
7774
  maxCommitMessageLength: z$1.number().min(L.maxCommitMessageLength.min).max(L.maxCommitMessageLength.max).default(L.maxCommitMessageLength.default),
7494
7775
  maxPRBodyLength: z$1.number().min(L.maxPRBodyLength.min).max(L.maxPRBodyLength.max).default(L.maxPRBodyLength.default),
7495
7776
  maxChangelogLength: z$1.number().min(L.maxChangelogLength.min).max(L.maxChangelogLength.max).default(L.maxChangelogLength.default),
7496
- repository: z$1.string().regex(/^[\w.-]+\/[\w.-]+$/, "Must be in owner/repo format (e.g., MyOrg/my-repo)").default("MillionOnMars/lumina5"),
7497
- targetBranch: z$1.string().regex(/^[\w./-]+$/, "Must be a valid branch name").default("prod"),
7777
+ repository: z$1.string().regex(/^[\w.-]+\/[\w.-]+$/, "Must be in owner/repo format (e.g., MyOrg/my-repo)").default(WHATS_NEW_DEFAULT_REPOSITORY),
7778
+ targetBranch: z$1.string().regex(/^[\w./-]+$/, "Must be a valid branch name").default(WHATS_NEW_DEFAULT_TARGET_BRANCH),
7498
7779
  promptTemplate: z$1.string().min(L.promptTemplate.min, `Prompt template must be at least ${L.promptTemplate.min} characters`).max(L.promptTemplate.max, `Prompt template cannot exceed ${L.promptTemplate.max.toLocaleString()} characters`).trim().optional()
7499
7780
  });
7500
7781
  const WhatsNewSyncConfigSchema = z$1.object({
@@ -7932,8 +8213,12 @@ const API_SERVICE_GROUPS = {
7932
8213
  order: 10
7933
8214
  },
7934
8215
  {
7935
- key: "dataLakeSearchMaxChunksPerFile",
8216
+ key: "forcedRetrievalSpreadFloorPct",
7936
8217
  order: 11
8218
+ },
8219
+ {
8220
+ key: "dataLakeSearchMaxChunksPerFile",
8221
+ order: 12
7937
8222
  }
7938
8223
  ]
7939
8224
  },
@@ -10190,7 +10475,7 @@ const settingsMap = {
10190
10475
  description: "Most chunks from any ONE source document a data-lake semantic search may return in its top-K. A diversity guard for CONTESTED slots: where several documents answer the question, it stops the best-scoring one from taking slots the others could have filled. It is NOT a fix for severe crowding - the cap redistributes only among the candidates retrieval already returned, so a document that supplies enough of the top-scoring chunks to fill that pool on its own is one the cap cannot change at all. On a corpus of book-length documents, expect enabling this to change little beyond widening the vector-search request. 0 (default) disables the cap, byte-identical to behavior before this setting existed. The cap never SHRINKS a result set - once the spread-out picks are in, any slots still open are backfilled with the highest-scoring chunks the cap held back, so a lake whose only match is one document still returns a full top-K. A value at or above the result count is also a no-op, since nothing can ever be held back. Below it, each retrieval stream's candidate pool is widened to a fixed multiple of the result count so the cap has a spread to choose from. The scanned corpus itself does not grow (that is bounded separately), but the vector-search backends are asked for that many more matches, and a larger in-memory ranking pool costs some CPU. 2-3 is the useful range; 1 serves one passage per document, which suits a corpus of many short documents and starves a question whose answer spans one long one. The chat knowledge-base path ranks more passages than it serves, so it applies the cap a second time at the count it actually serves - otherwise the spread-out picks, which are by definition the lowest-scoring ones admitted, would land in the passages that path discards.",
10191
10476
  category: "AI",
10192
10477
  group: API_SERVICE_GROUPS.EMBEDDING.id,
10193
- order: 11,
10478
+ order: 12,
10194
10479
  scope: { settableAt: ["organization", "owner"] }
10195
10480
  }),
10196
10481
  forcedRetrievalCharBudget: makeNumberSetting({
@@ -10273,12 +10558,25 @@ const settingsMap = {
10273
10558
  min: 1,
10274
10559
  max: 100,
10275
10560
  int: true,
10276
- description: `Absolute minimum cosine similarity, as a percent, a chunk must clear to be injected on a Data-Lake-mode turn. This is a sanity floor for genuinely unrelated content, NOT the ranking gate - the relative floor above does the ranking. LEAVE IT AT 75 UNLESS YOU HAVE MEASURED YOUR OWN CORPUS: a raw cosine means nothing outside the embedding model it was fitted to, so while this reads 75 the server ignores it and applies the floor measured for whichever model your documents are actually embedded with (${forcedRetrievalFloorsBySpaceSummary}, and no absolute floor at all for a model nobody has measured - the relative floor still applies). Set any other value and the server uses exactly that, in every space, which is yours to get right: 75 against text-embedding-3-small sits above that band entirely and returns nothing on every query. Where this floor lands inside your band decides a lot - on one measured corpus 74 / 75 / 76 swung recall 91% / 65% / 40% - and the same 75 that is a cliff on one lake rejects nothing at all on another. Re-measure after changing the embedding model; the sweep tool is packages/scripts/retrieval/forcedFloorSweep.ts.`,
10561
+ description: `Absolute minimum cosine similarity, as a percent, a chunk must clear to be injected on a Data-Lake-mode turn. This is a sanity floor for genuinely unrelated content, NOT the ranking gate - the relative floor above does the ranking. LEAVE IT AT 75 UNLESS YOU HAVE MEASURED YOUR OWN CORPUS: a raw cosine means nothing outside the embedding model it was fitted to, so while this reads 75 the server ignores it and applies the floor measured for whichever model your documents are actually embedded with (${forcedRetrievalFloorsBySpaceSummary}, and no absolute floor at all for a model nobody has measured - the relative floor still applies). Set any other value and the server uses exactly that, in every space, which is yours to get right: 75 against text-embedding-3-small sits above that band entirely and returns nothing on every query. Where this floor lands inside your band decides a lot - on one measured corpus 74 / 75 / 76 swung recall 91% / 65% / 40% - and the same 75 that is a cliff on one lake rejects nothing at all on another. Re-measure after changing the embedding model.`,
10277
10562
  category: "AI",
10278
10563
  group: API_SERVICE_GROUPS.EMBEDDING.id,
10279
10564
  order: 10,
10280
10565
  scope: { settableAt: ["organization", "owner"] }
10281
10566
  }),
10567
+ forcedRetrievalSpreadFloorPct: makeNumberSetting({
10568
+ key: "forcedRetrievalSpreadFloorPct",
10569
+ name: "Forced Retrieval Spread Floor (%)",
10570
+ defaultValue: 0,
10571
+ min: 0,
10572
+ max: 100,
10573
+ int: true,
10574
+ description: "How far below the best-scoring passage of the SAME turn a chunk may score and still be injected, as a percent of the gap between that best score and a typical one (this turn's median). 0 (the default) disables it. This is the only one of the three floors whose cut depends on the QUESTION rather than on where the score band sits: a question one document answers sharply leaves its answer far above the median and admits few passages, while a broad question leaves many passages bunched near the top and admits many. The other two floors cannot tell those turns apart, which is why retrieved volume otherwise tracks the character budget instead of the question. Because it is measured in units of the band rather than against a fixed cosine, the same value means the same thing in every embedding space and does not need re-tuning when the embedding model changes. Lower is stricter (10 keeps only passages within a tenth of the way down to the median); 100 cuts at the median itself. It can never empty a turn - the best passage always clears its own cutoff. Ships off because no magnitude has been measured yet; measure one offline before turning it on.",
10575
+ category: "AI",
10576
+ group: API_SERVICE_GROUPS.EMBEDDING.id,
10577
+ order: 11,
10578
+ scope: { settableAt: ["organization", "owner"] }
10579
+ }),
10282
10580
  LakeAccessAuditRetentionDays: makeNumberSetting({
10283
10581
  key: "LakeAccessAuditRetentionDays",
10284
10582
  name: "Lake Access Audit Retention (days)",
@@ -10706,8 +11004,8 @@ const settingsMap = {
10706
11004
  maxCommitMessageLength: 200,
10707
11005
  maxPRBodyLength: 500,
10708
11006
  maxChangelogLength: 1e3,
10709
- repository: "MillionOnMars/lumina5",
10710
- targetBranch: "main"
11007
+ repository: WHATS_NEW_DEFAULT_REPOSITORY,
11008
+ targetBranch: WHATS_NEW_DEFAULT_TARGET_BRANCH
10711
11009
  },
10712
11010
  description: "Configuration for automated What's New modal generation, including LLM model selection, prompt parameters, validation rules, and content sanitization limits.",
10713
11011
  category: "Admin",
@@ -11279,7 +11577,14 @@ const TokensBySourceSchema = z$1.object({
11279
11577
  fabFiles: z$1.number(),
11280
11578
  urlContent: z$1.number(),
11281
11579
  toolSchemas: z$1.number(),
11282
- userPrompt: z$1.number()
11580
+ userPrompt: z$1.number(),
11581
+ /**
11582
+ * Lake-sourced content injected this turn (forced retrieval + the lake-memory hot card), split out
11583
+ * of `systemPrompts`. Optional and absent means UNKNOWN, never zero - telemetry captured before
11584
+ * this bucket existed has no value to report and must not be read as "no lake content". Mirrors
11585
+ * PromptMetaTokensBySourceSchema in promptMeta.ts.
11586
+ */
11587
+ lakeRetrieval: z$1.number().optional()
11283
11588
  });
11284
11589
  z$1.object({
11285
11590
  /** gen_ai.usage.input_tokens */
@@ -11526,6 +11831,7 @@ const PromptMetaModelParametersSchema = z$1.object({
11526
11831
  prompt_upsampling: z$1.boolean().optional(),
11527
11832
  seed: z$1.number().optional(),
11528
11833
  output_format: z$1.string().optional(),
11834
+ background: z$1.string().optional(),
11529
11835
  response_format: z$1.string().optional(),
11530
11836
  seconds: z$1.number().optional(),
11531
11837
  model: z$1.string().optional()
@@ -11533,11 +11839,7 @@ const PromptMetaModelParametersSchema = z$1.object({
11533
11839
  const PromptMetaModelSchema = z$1.object({
11534
11840
  name: z$1.string(),
11535
11841
  parameters: PromptMetaModelParametersSchema.optional(),
11536
- type: z$1.enum([
11537
- "text",
11538
- "image",
11539
- "video"
11540
- ]).optional(),
11842
+ type: z$1.enum(PROMPT_META_MODEL_TYPES).optional(),
11541
11843
  backend: z$1.string().optional(),
11542
11844
  contextWindow: z$1.number().optional(),
11543
11845
  maxTokens: z$1.number().optional(),
@@ -11591,7 +11893,16 @@ const PromptMetaTokensBySourceSchema = z$1.object({
11591
11893
  fabFiles: z$1.number(),
11592
11894
  urlContent: z$1.number(),
11593
11895
  toolSchemas: z$1.number(),
11594
- userPrompt: z$1.number()
11896
+ userPrompt: z$1.number(),
11897
+ /**
11898
+ * Lake-sourced content injected this turn - forced retrieval (`knowledge_retrieval`) plus the
11899
+ * lake-memory hot card (`lake_memory`) - moved out of the `systemPrompts` residual so the
11900
+ * breakdown can price the lake separately. Optional, and absent means UNKNOWN, never zero: turns
11901
+ * recorded before this field existed carry no value and none can be backfilled, because the only
11902
+ * evidence was the residual this split had not yet made. Same absent-is-unknown rule as
11903
+ * `retrieval.injected` below.
11904
+ */
11905
+ lakeRetrieval: z$1.number().optional()
11595
11906
  });
11596
11907
  const PromptMetaContextSchema = z$1.object({
11597
11908
  attachedFiles: z$1.array(PromptMetaAttachedFileSchema).optional(),
@@ -11901,12 +12212,28 @@ const RetrievalSummarySchema = z$1.object({
11901
12212
  * the per-turn routing question is about, and before this it was indistinguishable from a turn
11902
12213
  * where forced retrieval was never configured at all.
11903
12214
  */
11904
- forcedSkipReason: z$1.enum(["attached_files", "personal_corpus"]).optional(),
12215
+ forcedSkipReason: z$1.enum([
12216
+ "attached_files",
12217
+ "personal_corpus",
12218
+ "no_lake_scope"
12219
+ ]).optional(),
11905
12220
  /** Which retrieval-capable surface(s) ran this turn, e.g. 'lake-memory', 'knowledgeBaseSearch'. */
11906
12221
  surfaces: z$1.array(z$1.string()),
11907
12222
  /** Lakes resolved at the moment retrieval ran, stamped point-in-time (not read live from the session). */
11908
12223
  dataLakeTags: z$1.array(z$1.string()),
11909
12224
  /**
12225
+ * The subset of `dataLakeTags` that actually put files into the ranked scope. `dataLakeTags`
12226
+ * alone says which lakes were REQUESTED, which read as "searched" while a lake could contribute
12227
+ * nothing - the retrieval budget used to be spent in file order and starved whichever lake
12228
+ * sorted last.
12229
+ *
12230
+ * Optional because attribution is best-effort: a file matched by a lake's prefix/membership arm
12231
+ * can carry no reversible `datalake:` tag (see attributeAccessedLakes), and the producer omits
12232
+ * this rather than reporting an inconclusive scope as "no lake contributed". Absent means
12233
+ * unknown, NOT none.
12234
+ */
12235
+ dataLakeTagsWithCandidates: z$1.array(z$1.string()).optional(),
12236
+ /**
11910
12237
  * The lake scope the turn's retrieval surfaces WOULD have searched, resolved at the seed site
11911
12238
  * whether or not any of them ran: the caller's accessible lakes narrowed to the session
11912
12239
  * (narrowLakeAccessToSession), or empty where the corpus is personal and the lake arms are
@@ -11993,16 +12320,25 @@ const RetrievalSummarySchema = z$1.object({
11993
12320
  * not "what reached the model": `ranked.length` and `scored.length` in KnowledgeRetrievalFeature
11994
12321
  * - the candidates left after the absolute similarity floor, and after the relative floor
11995
12322
  * trims them. `chunks` is what survived the char budget on top of that, so the three
11996
- * numbers bracket two independent trimmers:
12323
+ * numbers bracket three independent trimmers:
11997
12324
  *
11998
- * pre -> [relative floor] -> post -> [char budget] -> chunks
12325
+ * pre -> [relative floor] -> post -> [spread floor] -> postSpread -> [char budget] -> chunks
11999
12326
  *
12000
12327
  * They exist so a low `chunks` is diagnosable - a small corpus and a floor that trimmed a large
12001
- * pool end in the same `chunks`. `pre - post` is the floor's own effect and nothing else;
12328
+ * pool end in the same `chunks`. `pre - post` is the relative floor's own effect and nothing
12329
+ * else, and `post - postSpread` the spread floor's;
12002
12330
  * `pre - chunks` is NOT, because the budget trims the same walk. Both optional: only forced
12003
12331
  * retrieval computes a ranked pool, a surface without one (lake memory, the knowledge tools)
12004
- * never writes either, and absence must not read as zero candidates. SUMMED like `chunks`, with
12005
- * the same absent-is-not-zero handling as `topScore`.
12332
+ * never writes any of them, and absence must not read as zero candidates. SUMMED like `chunks`,
12333
+ * with the same absent-is-not-zero handling as `topScore`.
12334
+ *
12335
+ * `backgroundScore` is the median of every score the turn compared, and `postSpreadFloorCandidates`
12336
+ * what is left once the spread floor cuts against it. Recorded even while that floor is OFF (its
12337
+ * shipped default), in which case `postSpread` equals `post` and the pair degenerates to a
12338
+ * diagnostic: `topScore - backgroundScore` is the turn's signal spread, and the distribution of
12339
+ * that quantity over production traffic is what a value for `forcedRetrievalSpreadFloorPct` has to
12340
+ * be chosen from. NOT comparable across embedding spaces as an absolute number, for the same
12341
+ * reason `topScore` is not; the RATIO of the two floors' cuts is.
12006
12342
  *
12007
12343
  * COMPARE THE PAIR ONLY TO ITSELF, never to `chunks`, unless `surfaces` is forced retrieval
12008
12344
  * alone. `chunks` and `chars` sum across ALL surfaces while this pair is forced-only, so a mixed
@@ -12024,7 +12360,9 @@ const RetrievalSummarySchema = z$1.object({
12024
12360
  chars: z$1.number(),
12025
12361
  topScore: z$1.number().optional(),
12026
12362
  preRelativeFloorCandidates: z$1.number().optional(),
12027
- postRelativeFloorCandidates: z$1.number().optional()
12363
+ postRelativeFloorCandidates: z$1.number().optional(),
12364
+ postSpreadFloorCandidates: z$1.number().optional(),
12365
+ backgroundScore: z$1.number().optional()
12028
12366
  }).optional(),
12029
12367
  /**
12030
12368
  * Could the corpus in scope have answered this turn, whether or not the model went looking?
@@ -12262,9 +12600,18 @@ z$1.object({
12262
12600
  schemaVersion: z$1.number().int().positive(),
12263
12601
  /** Product identifier: 'vibeswire', 'bike4mind', 'stocksandvibes', 'k2kanji', etc. */
12264
12602
  productId: z$1.string().min(1).max(64),
12265
- /** Product's internal user ID */
12603
+ /**
12604
+ * Product's internal user ID, or OVERWATCH_ANONYMOUS_USER_ID when the event has no
12605
+ * identified user. See the session and identity conventions below.
12606
+ */
12266
12607
  userId: z$1.string().min(1).max(256),
12267
- /** Session identifier for retention/funnel analysis */
12608
+ /**
12609
+ * Visit identifier. Overwatch counts distinct values of this per product as the first
12610
+ * stage of its acquisition funnel, so one value per visit is what makes that count a
12611
+ * count of visits. An emitter that cannot tell which visit a request belongs to sends
12612
+ * OVERWATCH_UNKNOWN_SESSION_ID rather than a value of its own invention - see the
12613
+ * conventions below.
12614
+ */
12268
12615
  sessionId: z$1.string().min(1).max(256),
12269
12616
  /** Event type: 'session_start', 'signup', 'feature_used', etc. */
12270
12617
  event: z$1.string().min(1).max(128),
@@ -12281,6 +12628,153 @@ z$1.object({
12281
12628
  z$1.boolean()
12282
12629
  ])).refine((v) => JSON.stringify(v).length <= 1024, "metadata must be ≤ 1KB serialized").optional()
12283
12630
  });
12631
+ /**
12632
+ * Public wire schema for `GET /api/v1/me` - the caller's own identity and
12633
+ * commercial state, and nothing else.
12634
+ *
12635
+ * Deliberately narrower than the user document `/api/identify` returns: no email,
12636
+ * no tags, no internal flags. Everything here answers one of three questions a
12637
+ * downstream app has to ask before it spends - who is this, what have they paid
12638
+ * for, can they afford the next call.
12639
+ *
12640
+ * Public-API rules apply: snake_case wire fields, no `.catch()`, no top-level
12641
+ * `.transform()`.
12642
+ */
12643
+ /**
12644
+ * Rung on the B4M plan ladder the caller currently sits on.
12645
+ *
12646
+ * `free` means no active subscription. `other` is still a paying customer: either
12647
+ * an active plan that is not on the ladder (`SubscriptionPlanDetail.tier` is
12648
+ * optional - a standalone product omits it so it stays out of the cross-plan
12649
+ * change flow), or one whose Stripe price the deployment can no longer name, as
12650
+ * happens to a subscriber grandfathered on a superseded price. `subscription` is
12651
+ * `null` in that second case.
12652
+ *
12653
+ * Gate on `tier !== 'free'` for "is this caller paying" and on `subscription.
12654
+ * price_id` for "which product" - NOT on `basic`/`pro`, whose ordinals come from
12655
+ * the internal change-flow ladder and do not track a plan's marketing name (the
12656
+ * Professional plan occupies the `basic` rung today).
12657
+ */
12658
+ const ME_TIERS = [
12659
+ "free",
12660
+ "basic",
12661
+ "pro",
12662
+ "other"
12663
+ ];
12664
+ /** The caller's active subscription, or `null` when they have none the deployment can name. */
12665
+ const MeSubscriptionSchema = z$1.object({
12666
+ plan_name: z$1.string(),
12667
+ price_id: z$1.string(),
12668
+ interval: z$1.enum(["monthly", "yearly"]),
12669
+ /** ISO 8601. When the current billing period ends - not a cancellation date. */
12670
+ current_period_ends_at: z$1.string()
12671
+ });
12672
+ z$1.object({
12673
+ /** Stable B4M user id. Safe to key an integrator's own records on. */
12674
+ id: z$1.string(),
12675
+ /** Display name. Never the email address. */
12676
+ name: z$1.string(),
12677
+ tier: z$1.enum(ME_TIERS),
12678
+ subscription: MeSubscriptionSchema.nullable(),
12679
+ credits: z$1.object({
12680
+ /**
12681
+ * Spendable credits on the caller's PERSONAL ledger. Organization pools are not
12682
+ * included - a call billed to an organization draws on a balance this number
12683
+ * does not describe.
12684
+ */
12685
+ balance: z$1.number() }),
12686
+ /** Entitlement keys the caller currently holds, e.g. `base`. */
12687
+ entitlements: z$1.array(z$1.string())
12688
+ });
12689
+ /**
12690
+ * Every tool name in `b4mLLMTools`. Exhaustive by annotation - do not widen the type.
12691
+ */
12692
+ const CORE_TOOL_SIDE_EFFECTS = {
12693
+ dice_roll: "none",
12694
+ weather_info: "none",
12695
+ web_search: "none",
12696
+ web_fetch: "none",
12697
+ wolfram_alpha: "none",
12698
+ deep_research: "none",
12699
+ math_evaluate: "none",
12700
+ current_datetime: "none",
12701
+ prompt_enhancement: "none",
12702
+ wikipedia_on_this_day: "none",
12703
+ moon_phase: "none",
12704
+ sunrise_sunset: "none",
12705
+ iss_tracker: "none",
12706
+ planet_visibility: "none",
12707
+ search_knowledge_base: "none",
12708
+ retrieve_knowledge_content: "none",
12709
+ count_knowledge_base: "none",
12710
+ describe_knowledge_base: "none",
12711
+ chess_engine: "none",
12712
+ fmp_financial_data: "none",
12713
+ bob_panel_read: "none",
12714
+ skill: "none",
12715
+ recharts: "local",
12716
+ mermaid_chart: "local",
12717
+ navigate_view: "local",
12718
+ generate_jupyter_notebook: "local",
12719
+ optihashi_schedule: "local",
12720
+ optihashi_formulate: "local",
12721
+ optihashi_edit_problem: "local",
12722
+ image_generation: "external",
12723
+ edit_image: "external",
12724
+ music_generation: "external",
12725
+ audio_generation: "external",
12726
+ excel_generation: "external",
12727
+ edit_file: "external",
12728
+ blog_publish: "external",
12729
+ blog_edit: "external",
12730
+ blog_draft: "external",
12731
+ delegate_to_agent: "external"
12732
+ };
12733
+ /**
12734
+ * Tool names that reach the pipeline from outside `b4mLLMTools` - the CLI tool set, the
12735
+ * Slack tool set, and premium-overlay tools supplied at runtime via the `externalTools`
12736
+ * merge. No enum to key off, so these get no compile-time exhaustiveness; an unlisted name
12737
+ * falls through to the gated default, which is the correct failure direction.
12738
+ */
12739
+ const EXTERNAL_REGISTRY_SIDE_EFFECTS = {
12740
+ file_read: "none",
12741
+ glob_files: "none",
12742
+ grep_search: "none",
12743
+ recent_changes: "none",
12744
+ check_shell_output: "none",
12745
+ list_background_shells: "none",
12746
+ lattice_query: "none",
12747
+ lattice_explain: "none",
12748
+ ask_user_question: "local",
12749
+ create_file: "external",
12750
+ edit_local_file: "external",
12751
+ delete_file: "external",
12752
+ bash_execute: "external",
12753
+ write_shell_stdin: "external",
12754
+ kill_background_shell: "external",
12755
+ lattice_create_model: "external",
12756
+ lattice_add_entity: "external",
12757
+ lattice_set_value: "external",
12758
+ lattice_create_rule: "external",
12759
+ slackbot_help: "none",
12760
+ list_curated_files: "none",
12761
+ notebook_status: "none",
12762
+ share_curated_file: "external",
12763
+ notebook_new: "external",
12764
+ confirm_pending_action: "external",
12765
+ cancel_pending_action: "external",
12766
+ mission_status: "none",
12767
+ optihashi_decompose: "local",
12768
+ optihashi_solve: "local",
12769
+ send_slack_message: "external",
12770
+ coordinate_task: "external",
12771
+ code_execute: "external",
12772
+ video_generation: "external"
12773
+ };
12774
+ ({
12775
+ ...CORE_TOOL_SIDE_EFFECTS,
12776
+ ...EXTERNAL_REGISTRY_SIDE_EFFECTS
12777
+ });
12284
12778
  const InternalTeamMemberSchema = z$1.object({
12285
12779
  name: z$1.string().min(1, "Name is required"),
12286
12780
  phone: z$1.string().min(1, "Phone is required"),
@@ -12833,12 +13327,13 @@ new Set(DATA_LAKES.map((l) => l.id));
12833
13327
  */
12834
13328
  const normalizeEntitlementKey = (key) => key.trim().toLowerCase();
12835
13329
  const sha256Regex = /^[a-f0-9]{64}$/;
13330
+ const requiredUserTagValue = z.string().trim().min(1).max(100).refine((s) => !/[,;]/.test(s), "User tag must be a single tag with no commas or semicolons (e.g. \"vip\" or \"Sales Team\")");
12836
13331
  z.object({
12837
13332
  name: z.string().min(1).max(200),
12838
13333
  slug: z.string().min(2).max(60).regex(DATA_LAKE_SLUG_REGEX, "Slug must be lowercase alphanumeric with hyphens (e.g. \"my-data-lake\")"),
12839
13334
  description: z.string().max(2e3).optional(),
12840
13335
  fileTagPrefix: z.string().trim().min(2).max(30).refine((s) => s.endsWith(":"), "Tag prefix must end with \":\" (e.g. \"acme:\")").refine((s) => !hasBlankTagPrefixSegment(s), "Tag prefix segments must be non-empty (e.g. \"acme:\" or \"acme:legal:\")").refine((s) => !isReservedTagPrefix(s), `Tag prefix cannot use the reserved "${DATALAKE_TAG_PREFIX}" namespace`),
12841
- requiredUserTag: z.string().min(1).max(100).optional(),
13336
+ requiredUserTag: requiredUserTagValue.optional(),
12842
13337
  requiredEntitlement: z.string().min(3).max(100).refine((s) => s.includes(":") && s.split(":").every((part) => part.length > 0), "Entitlement key must be namespaced with non-empty parts (e.g. \"product:pro\")").optional(),
12843
13338
  organizationId: z.string().optional()
12844
13339
  });
@@ -12848,7 +13343,7 @@ z.object({
12848
13343
  systemPrompt: z.string().optional(),
12849
13344
  preferredSystemPromptId: z.union([z.literal(""), z.string().min(1).max(200)]).optional(),
12850
13345
  groundingMode: z.enum(DATA_LAKE_GROUNDING_MODES).optional(),
12851
- requiredUserTag: z.union([z.literal(""), z.string().min(1).max(100)]).optional(),
13346
+ requiredUserTag: z.union([z.literal(""), requiredUserTagValue]).optional(),
12852
13347
  requiredEntitlement: z.union([z.literal(""), z.string().min(3).max(100).refine((s) => s.includes(":") && s.split(":").every((part) => part.length > 0), "Entitlement key must be namespaced with non-empty parts (e.g. \"product:pro\")")]).optional(),
12853
13348
  auditQueryTextEnabled: z.boolean().optional(),
12854
13349
  lakeMemoryEnabled: z.boolean().optional(),
@@ -13094,6 +13589,51 @@ const OPTIONAL_DEP_BLESSED_SCRIPT_PATHS = Object.values({
13094
13589
  }
13095
13590
  }).map((d) => d.path);
13096
13591
  [...REACT_BLESSED_SCRIPT_PATHS, ...OPTIONAL_DEP_BLESSED_SCRIPT_PATHS];
13592
+ function isGPTImageModel(model) {
13593
+ if (!model) return false;
13594
+ return OPENAI_IMAGE_MODELS.includes(model) || model.startsWith("gpt-image-");
13595
+ }
13596
+ /**
13597
+ * Flux Ultra is the only BFL generation model driven by `aspect_ratio`; the Pro family takes
13598
+ * discrete `width`/`height` and ignores aspect_ratio outright. Must stay in sync with the branch
13599
+ * in `BFLImageService.generate`, which is what actually builds the request body.
13600
+ */
13601
+ function isBflUltraImageModel(model) {
13602
+ return model === "flux-pro-1.1-ultra";
13603
+ }
13604
+ /** Returns true specifically for gpt-image-2 (including versioned snapshots like gpt-image-2-2026-04-21). */
13605
+ function isGPTImage2Model(model) {
13606
+ if (!model) return false;
13607
+ return model === "gpt-image-2" || model.startsWith("gpt-image-2");
13608
+ }
13609
+ /**
13610
+ * Whether a user can access a model. Access is any-of (mirrors the Q3b data-lake
13611
+ * rule, `getAccessibleDataLakes`): a non-admin reaches the model via
13612
+ * `allowedUserTags ∩ userTags` OR `allowedEntitlements ∩ entitlementKeys`.
13613
+ *
13614
+ * `entitlementKeys` is optional - when omitted/empty the entitlement branch is
13615
+ * inert, so a model with no `allowedEntitlements` behaves exactly as before
13616
+ * (tag-only). This lets a tag-less subscriber reach an entitlement-gated model
13617
+ * while leaving every existing tag-gated model unchanged (zero regression).
13618
+ *
13619
+ * Pure + zero-dependency (only types + the shared key normalizer), so it lives
13620
+ * in `@bike4mind/common` as the SINGLE source of truth - imported by the core
13621
+ * `@bike4mind/utils` re-export (server/services) AND the client
13622
+ * `useAccessibleModels` hook, which previously kept a hand-rolled twin "to avoid
13623
+ * AWS SDK imports". Common is browser-safe, so there is no longer any reason to
13624
+ * duplicate the logic.
13625
+ */
13626
+ function isModelAccessible(model, userTags, isAdmin = false, entitlementKeys = []) {
13627
+ if (!model.enabled) return false;
13628
+ if (isAdmin) return true;
13629
+ const normalizedUserTags = userTags.map((tag) => tag.toLowerCase());
13630
+ const normalizedAllowedTags = (model.allowedUserTags ?? []).map((tag) => tag.toLowerCase());
13631
+ if (normalizedUserTags.some((tag) => normalizedAllowedTags.includes(tag))) return true;
13632
+ const normalizedKeys = entitlementKeys.map(normalizeEntitlementKey);
13633
+ const normalizedAllowedEntitlements = (model.allowedEntitlements ?? []).map(normalizeEntitlementKey);
13634
+ return normalizedKeys.some((key) => normalizedAllowedEntitlements.includes(key));
13635
+ }
13636
+ [...OPENAI_IMAGE_MODELS, ...GEMINI_IMAGE_MODELS];
13097
13637
  const DashboardParamsSchema = z$1.object({
13098
13638
  dashboardDataSources: z$1.array(z$1.object({
13099
13639
  sourceName: z$1.string(),
@@ -13137,11 +13677,21 @@ OpenAIImageGenerationInput.extend({
13137
13677
  height: z$1.number().optional(),
13138
13678
  aspect_ratio: z$1.string().optional(),
13139
13679
  fabFileIds: z$1.array(z$1.string()).prefault([]),
13680
+ /**
13681
+ * Extra gpt-image "style anchor" images, as fabFile ids. They ride alongside the primary
13682
+ * input image (the first image-type entry in `fabFileIds`) rather than replacing it, and
13683
+ * OpenAI receives them in this order - which matters, because a mask always applies to the
13684
+ * first image in the array. fabFile ids rather than URLs so the existing access +
13685
+ * moderation gates (findAccessibleInIds, isImageServeable) still apply. Ignored by every
13686
+ * non-gpt-image provider. Repeated ids collapse to one anchor. See MAX_REFERENCE_IMAGES for
13687
+ * why the cap is 4 and not OpenAI's 16.
13688
+ */
13689
+ referenceImageFabFileIds: z$1.array(z$1.string()).max(4).optional(),
13140
13690
  tools: z$1.array(z$1.union([b4mLLMTools, z$1.string()])).optional(),
13141
13691
  safety_tolerance: BFLSafetyToleranceSchema,
13142
13692
  prompt_upsampling: z$1.boolean().optional(),
13143
13693
  seed: z$1.number().nullable().optional(),
13144
- output_format: z$1.enum(["jpeg", "png"]).nullable().optional(),
13694
+ output_format: ImageOutputFormatSchema.nullable().optional(),
13145
13695
  /** Resolved by the API route's prompt resolver. Defaults to 'fresh' for first-turn or sessions with no prior image. */
13146
13696
  intent: PromptIntentSchema.optional(),
13147
13697
  promptEnhancement: z$1.object({
@@ -13159,7 +13709,7 @@ OpenAIImageGenerationInput.extend({
13159
13709
  const GenerateImageToolCallSchema = OpenAIImageGenerationInput.extend({
13160
13710
  safety_tolerance: z$1.number().optional(),
13161
13711
  prompt_upsampling: z$1.boolean().optional(),
13162
- output_format: z$1.enum(["jpeg", "png"]).nullable().optional(),
13712
+ output_format: ImageOutputFormatSchema.nullable().optional(),
13163
13713
  seed: z$1.number().nullable().optional(),
13164
13714
  editModel: z$1.string().optional()
13165
13715
  }).omit({ prompt: true });
@@ -13196,7 +13746,18 @@ OpenAIImageGenerationInput.extend({
13196
13746
  organizationId: z$1.string().nullable().optional(),
13197
13747
  aspect_ratio: z$1.string().optional(),
13198
13748
  fabFileIds: z$1.array(z$1.string()).prefault([]),
13199
- image: z$1.string()
13749
+ /**
13750
+ * Extra gpt-image "style anchor" images, as fabFile ids. On this endpoint `fabFileIds` is
13751
+ * the inpainting mask, not an image input, so references need their own field: they are
13752
+ * appended after `image` (the edit source), and OpenAI applies the mask to the first entry
13753
+ * of that array - i.e. always to `image`, never to a reference. fabFile ids rather than URLs
13754
+ * so the existing access + moderation gates (findAccessibleInIds, isImageServeable) still
13755
+ * apply. Ignored by BFL and Gemini. Repeated ids collapse to one anchor. See
13756
+ * MAX_REFERENCE_IMAGES for why the cap is 4, not 16.
13757
+ */
13758
+ referenceImageFabFileIds: z$1.array(z$1.string()).max(4).optional(),
13759
+ image: z$1.string(),
13760
+ output_format: ImageOutputFormatSchema.nullable().optional()
13200
13761
  });
13201
13762
  const isUnlimitedHistory = (historyCount) => historyCount === -1;
13202
13763
  /**
@@ -13230,6 +13791,13 @@ z$1.object({
13230
13791
  message: z$1.string(),
13231
13792
  messageFileIds: z$1.array(z$1.string()).prefault([]),
13232
13793
  questId: z$1.string().optional(),
13794
+ /**
13795
+ * Correct-and-retry: the quest whose answer the user says was wrong. Produces a NEW quest
13796
+ * carrying the user's correction, rather than overwriting the flagged one the way `questId`
13797
+ * (retry) does - the original answer has to survive for the evaluation-pair export to read it.
13798
+ * Validated server-side against the resolved session before it is persisted.
13799
+ */
13800
+ correctsQuestId: z$1.string().optional(),
13233
13801
  /** Extra context messages to include in the conversation from external sources */
13234
13802
  extraContextMessages: z$1.array(z$1.object({
13235
13803
  role: z$1.enum([
@@ -13265,11 +13833,19 @@ z$1.object({
13265
13833
  * was the only switch for the offer and it also strips every authored prompt, so no caller could
13266
13834
  * have an arm that went unoffered AND kept the abstention licence.
13267
13835
  *
13268
- * Gates the three auto-add sites (the knowledge offer in resolveEnabledTools, the navigate_view
13836
+ * Gates the auto-add sites (the knowledge offer in resolveEnabledTools, the navigate_view
13269
13837
  * auto-add, the blog/skill gate), unioned with `Boolean(promptMode)` by
13270
13838
  * resolveSkipAutoOffers. A force-on, not an override: `false` under a promptMode still suppresses.
13271
13839
  * Withholding navigate_view also drops the viewRegistry system block, which only describes it.
13272
13840
  *
13841
+ * `buildSharedTools`' `offerOnlyNamedTools` (see sharedToolBuilder.ts) reads the same union for
13842
+ * the same reason: MCP tools merged past the `enabledTools` filter are withheld too, since the
13843
+ * caller never named them either. Unlike the auto-add sites, this one can still be reached per
13844
+ * tool - a caller with `session.enabledTools` (not the public `tools` field, which
13845
+ * `filterKnownTools` strips non-native ids from before this flag is even consulted) can name one
13846
+ * MCP tool by its namespaced `server__tool` id and keep it while every unnamed sibling is
13847
+ * withheld.
13848
+ *
13273
13849
  * Withholds the OFFER, not knowledge: `session.forceKnowledgeRetrieval` is untouched, and an
13274
13850
  * already-attached corpus is inlined rather than deferred to the tool. An arm that must see no
13275
13851
  * knowledge at all also needs a session with no attachments and forced retrieval off.
@@ -14117,43 +14693,171 @@ z$1.object({
14117
14693
  createdAt: z$1.union([z$1.string(), z$1.date()]).optional(),
14118
14694
  updatedAt: z$1.union([z$1.string(), z$1.date()]).optional()
14119
14695
  });
14120
- function isGPTImageModel(model) {
14121
- if (!model) return false;
14122
- return OPENAI_IMAGE_MODELS.includes(model) || model.startsWith("gpt-image-");
14696
+ /**
14697
+ * Length of `text` in Unicode CODE POINTS - the unit `IFabFileChunk.charLength` is stored in.
14698
+ * Matches MongoDB's `$strLenCP`, which is what lets the char-length backfill
14699
+ * (packages/scripts/datalake/backfill-chunk-char-length.ts) compute the same number server-side
14700
+ * without reading chunk text out of the database. Deliberately NOT `text.length` (UTF-16 code
14701
+ * units): the two differ on astral characters (surrogate pairs), and the write path and the
14702
+ * backfill must agree exactly.
14703
+ */
14704
+ const countCodePoints = (text) => {
14705
+ let count = 0;
14706
+ for (const _ch of text) count++;
14707
+ return count;
14708
+ };
14709
+ /**
14710
+ * Default character budget for a super-linear parse pass. A starting point only -
14711
+ * see the sizing rule below; any call site in front of a measured parser should pass
14712
+ * an explicit `max` derived from that parser's own curve.
14713
+ */
14714
+ const DEFAULT_PARSE_CAP = 1e5;
14715
+ /**
14716
+ * Bound the length of input a super-linear parser will scan.
14717
+ *
14718
+ * A regex that backtracks polynomially (or worse) turns an unbounded input length
14719
+ * into unbounded CPU on a shared process - the algorithmic-DoS shape. Refusing to
14720
+ * scan more than `max` characters converts that into a fixed worst case regardless
14721
+ * of how pathological the input is.
14722
+ *
14723
+ * Truncation (never throwing) is deliberate: these caps sit in front of best-effort
14724
+ * cleaners and content detectors where an over-cap input should degrade quietly,
14725
+ * not crash the pipeline.
14726
+ *
14727
+ * PREFER LINEARIZING THE PARSER. A cap is the fallback, not the first move. The email
14728
+ * cleaners in services/lib/turndown.ts and the snippet-meta extractor in common/utils.ts
14729
+ * both started here and both ended up as single-pass scans instead, because a cap that
14730
+ * was small enough to bound their cost was also small enough to stop them working on
14731
+ * real content. When the parser is linear, the cap can go.
14732
+ *
14733
+ * SIZING RULE - if you do cap, pick `max` from the parser's measured cost AT the cap,
14734
+ * not from the headroom above real input. For an O(n^2)/O(n^3) inner loop, "far above
14735
+ * anything legitimate" is not a bound: the email cleaners were cubic, so a 512k cap
14736
+ * still ran for minutes. Measure the worst-case shape at the cap you intend to ship and
14737
+ * make sure the number of milliseconds is one you can defend.
14738
+ *
14739
+ * Four further traps this helper cannot solve for you:
14740
+ * - A per-item cap is not a total bound when the item count is also attacker-chosen
14741
+ * (a .pptx picks its own slide count). Carry a running budget across the loop.
14742
+ * - Cap what is SCANNED, not what is returned. Where the result is rendered or
14743
+ * stored, re-append `input.slice(max)` unchanged: truncating the value silently
14744
+ * drops content, and a cut inside an element can strand its closing tag. Splicing
14745
+ * the original halves back together also avoids leaving a lone surrogate, since
14746
+ * `slice` cuts by UTF-16 code unit.
14747
+ * - Splicing the tail back is not enough when the cap changes the SHAPE of the parse
14748
+ * rather than only its length - a pattern terminated by `$`, say, whose sections then
14749
+ * end early and get classified differently. Check what an over-cap input parses INTO,
14750
+ * not just that its characters survive.
14751
+ * - Cap in front of a best-effort cleaner or detector, where degrading quietly is
14752
+ * acceptable. Do not cap in front of a parser whose output drives execution (a
14753
+ * tool-call parser): silently parsing a prefix there means running a subset of what
14754
+ * was asked. If such a parser caps internally, its callers must pass it text already
14755
+ * scoped to the construct being parsed.
14756
+ *
14757
+ * @returns the input unchanged when within budget, otherwise its first `max` chars.
14758
+ */
14759
+ function capForParse(input, max = DEFAULT_PARSE_CAP) {
14760
+ if (!Number.isFinite(max) || max < 0) throw new RangeError(`capForParse: max must be a non-negative finite number, got ${max}`);
14761
+ return input.length <= max ? input : input.slice(0, max);
14123
14762
  }
14124
- /** Returns true specifically for gpt-image-2 (including versioned snapshots like gpt-image-2-2026-04-21). */
14125
- function isGPTImage2Model(model) {
14126
- if (!model) return false;
14127
- return model === "gpt-image-2" || model.startsWith("gpt-image-2");
14763
+ /**
14764
+ * Exactly 'WIDTHxHEIGHT' - nothing else, no surrounding whitespace and no third segment.
14765
+ * Anchored on purpose: this module is the single source of the size rule, so a value that
14766
+ * only looks like a resolution ('1024x1024x1024') must not be measured as one.
14767
+ */
14768
+ const SIZE_PATTERN = /^(\d+)x(\d+)$/;
14769
+ /**
14770
+ * Splits a 'WIDTHxHEIGHT' string into numeric edges, or null when it is not a pair
14771
+ * of positive numbers. Lets callers tell a resolution that breaks a rule apart from
14772
+ * a value that expresses no resolution at all ('auto', 'wide', undefined).
14773
+ */
14774
+ function parseSizeEdges(size) {
14775
+ if (typeof size !== "string") return null;
14776
+ const match = SIZE_PATTERN.exec(size);
14777
+ if (!match) return null;
14778
+ const width = Number(match[1]);
14779
+ const height = Number(match[2]);
14780
+ if (!width || !height) return null;
14781
+ return {
14782
+ width,
14783
+ height
14784
+ };
14128
14785
  }
14129
14786
  /**
14130
- * Whether a user can access a model. Access is any-of (mirrors the Q3b data-lake
14131
- * rule, `getAccessibleDataLakes`): a non-admin reaches the model via
14132
- * `allowedUserTags ∩ userTags` OR `allowedEntitlements ∩ entitlementKeys`.
14787
+ * True when a custom gpt-image-2 resolution meets OpenAI's documented limits.
14788
+ * gpt-image-2 accepts any resolution satisfying these, not only the presets in
14789
+ * IMAGE_SIZE_CONSTRAINTS.GPT_IMAGE_2.sizes, so a flat preset check would reject
14790
+ * valid custom sizes.
14791
+ */
14792
+ function satisfiesGptImage2Constraints({ width, height }) {
14793
+ const { maxEdge, minTotalPixels, maxTotalPixels, edgeMultiple, maxAspectRatio } = IMAGE_SIZE_CONSTRAINTS.GPT_IMAGE_2.constraints;
14794
+ const longEdge = Math.max(width, height);
14795
+ const shortEdge = Math.min(width, height);
14796
+ const totalPixels = width * height;
14797
+ return longEdge <= maxEdge && width % edgeMultiple === 0 && height % edgeMultiple === 0 && longEdge / shortEdge <= maxAspectRatio && totalPixels >= minTotalPixels && totalPixels <= maxTotalPixels;
14798
+ }
14799
+ /**
14800
+ * Sizes the legacy (pre-GPT-Image) OpenAI generate endpoint accepts. The two tiers
14801
+ * overlap on 1024x1024; the repeated entry is harmless because this list is only ever
14802
+ * tested for membership.
14803
+ */
14804
+ const OPENAI_LEGACY_IMAGE_SIZES = [...IMAGE_SIZE_CONSTRAINTS.DALL_E_2.sizes, ...IMAGE_SIZE_CONSTRAINTS.DALL_E_3.sizes];
14805
+ /**
14806
+ * True when `size` may be sent to OpenAI for `model`. This is the single source of the
14807
+ * OpenAI image size rule - generate, edit, the variation endpoint, the cost estimate and
14808
+ * the settings UI all resolve through it, so a tier changing its accepted sizes is a
14809
+ * one-line edit to IMAGE_SIZE_CONSTRAINTS rather than a hunt through retyped lists.
14133
14810
  *
14134
- * `entitlementKeys` is optional - when omitted/empty the entitlement branch is
14135
- * inert, so a model with no `allowedEntitlements` behaves exactly as before
14136
- * (tag-only). This lets a tag-less subscriber reach an entitlement-gated model
14137
- * while leaving every existing tag-gated model unchanged (zero regression).
14811
+ * gpt-image-2 takes 'auto', its presets, or any custom WIDTHxHEIGHT meeting
14812
+ * satisfiesGptImage2Constraints. The gpt-image-1 family is limited to its three fixed
14813
+ * sizes. Anything else is treated as legacy dall-e.
14138
14814
  *
14139
- * Pure + zero-dependency (only types + the shared key normalizer), so it lives
14140
- * in `@bike4mind/common` as the SINGLE source of truth - imported by the core
14141
- * `@bike4mind/utils` re-export (server/services) AND the client
14142
- * `useAccessibleModels` hook, which previously kept a hand-rolled twin "to avoid
14143
- * AWS SDK imports". Common is browser-safe, so there is no longer any reason to
14144
- * duplicate the logic.
14815
+ * OpenAI image models only: BFL, Gemini and xAI sizes are validated by their own adapters,
14816
+ * and passing one of those models here would measure it against the wrong list.
14145
14817
  */
14146
- function isModelAccessible(model, userTags, isAdmin = false, entitlementKeys = []) {
14147
- if (!model.enabled) return false;
14148
- if (isAdmin) return true;
14149
- const normalizedUserTags = userTags.map((tag) => tag.toLowerCase());
14150
- const normalizedAllowedTags = (model.allowedUserTags ?? []).map((tag) => tag.toLowerCase());
14151
- if (normalizedUserTags.some((tag) => normalizedAllowedTags.includes(tag))) return true;
14152
- const normalizedKeys = entitlementKeys.map(normalizeEntitlementKey);
14153
- const normalizedAllowedEntitlements = (model.allowedEntitlements ?? []).map(normalizeEntitlementKey);
14154
- return normalizedKeys.some((key) => normalizedAllowedEntitlements.includes(key));
14818
+ function isSupportedImageSize(model, size) {
14819
+ if (typeof size !== "string") return false;
14820
+ if (isGPTImage2Model(model)) {
14821
+ if (size === IMAGE_SIZE_CONSTRAINTS.GPT_IMAGE_2.autoSize || IMAGE_SIZE_CONSTRAINTS.GPT_IMAGE_2.sizes.includes(size)) return true;
14822
+ const edges = parseSizeEdges(size);
14823
+ return edges !== null && satisfiesGptImage2Constraints(edges);
14824
+ }
14825
+ if (isGPTImageModel(model)) return IMAGE_SIZE_CONSTRAINTS.GPT_IMAGE_1.sizes.includes(size);
14826
+ return OPENAI_LEGACY_IMAGE_SIZES.includes(size);
14827
+ }
14828
+ /**
14829
+ * The size to fall back to when a requested size is unsupported for `model`. Every tier
14830
+ * defaults to 1024x1024 today; the indirection keeps that a per-tier decision.
14831
+ */
14832
+ function fallbackImageSize(model) {
14833
+ if (isGPTImage2Model(model)) return IMAGE_SIZE_CONSTRAINTS.GPT_IMAGE_2.defaultSize;
14834
+ if (isGPTImageModel(model)) return IMAGE_SIZE_CONSTRAINTS.GPT_IMAGE_1.defaultSize;
14835
+ return IMAGE_SIZE_CONSTRAINTS.DALL_E_2.defaultSize;
14836
+ }
14837
+ /**
14838
+ * The size the generate endpoint should send for a GPT-Image `model`, given what the
14839
+ * caller asked for. Returns the input unchanged when nothing needs correcting, so a
14840
+ * caller can compare the two to decide whether to warn.
14841
+ *
14842
+ * Absent: gpt-image-2 gets 'auto' - it is the only tier that can pick its own - and the
14843
+ * gpt-image-1 family gets its fixed default.
14844
+ *
14845
+ * Present but unsupported: replaced by the tier fallback, with one deliberate exception.
14846
+ * A gpt-image-2 value that names no resolution at all ('wide') is forwarded untouched:
14847
+ * gpt-image-2 sizing is open-ended, so an unrecognized token is left for OpenAI to
14848
+ * interpret rather than second-guessed here. The gpt-image-1 family has fixed sizes and
14849
+ * no such pass-through.
14850
+ *
14851
+ * GPT-Image tiers only - the legacy dall-e path has its own absent-size handling and
14852
+ * should gate on isSupportedImageSize directly.
14853
+ */
14854
+ function resolveGptImageGenerateSize(model, size) {
14855
+ const isGptImage2 = isGPTImage2Model(model);
14856
+ if (!size) return isGptImage2 ? IMAGE_SIZE_CONSTRAINTS.GPT_IMAGE_2.autoSize : fallbackImageSize(model);
14857
+ if (isSupportedImageSize(model, size)) return size;
14858
+ if (isGptImage2 && parseSizeEdges(size) === null) return size;
14859
+ return fallbackImageSize(model);
14155
14860
  }
14156
- [...OPENAI_IMAGE_MODELS, ...GEMINI_IMAGE_MODELS];
14157
14861
  /** Generation was cut off against the output-token ceiling. */
14158
14862
  const TRUNCATED_FINISH_REASON = "max_tokens";
14159
14863
  /**
@@ -14180,6 +14884,43 @@ function isEarlyStop(stopReason) {
14180
14884
  return !!stopReason && EARLY_STOP_FINISH_REASONS.has(stopReason);
14181
14885
  }
14182
14886
  /**
14887
+ * promptMeta.functionCalls fields that must never reach a viewer who only holds a
14888
+ * "read this conversation" grant - a session share, a live subscription, a bug-report
14889
+ * egress to a third party (Slack/email), or a session clone made by a share holder
14890
+ * (b4m-core/services/src/sessionService/clone.ts, which copies quest.promptMeta wholesale
14891
+ * onto a session the sharee then OWNS - a share/subscribe grant must not become a durable
14892
+ * unredacted copy just because the copy has a new owner).
14893
+ *
14894
+ * `returnValue` is the verbatim output of a tool call the OWNER's turn made - up to 8000 chars
14895
+ * per call (see recordToolResult.ts) - and a tool can read the owner's private corpus, files, or
14896
+ * connected integrations. A share/subscribe grant authorizes reading the conversation the owner
14897
+ * had, not re-reading whatever the owner's tools touched on the owner's behalf. `error` is
14898
+ * currently unwritten but carries the same class of content on the failure path, so it is
14899
+ * redacted alongside it.
14900
+ *
14901
+ * This list is the single source of truth: add a field here and every response boundary that
14902
+ * routes through {@link redactFunctionCallsForViewer} inherits the redaction - PROVIDED the field
14903
+ * is a top-level, directly-owned-content field like these two. The redaction itself is a shallow
14904
+ * `delete` per field (see below), so a future owner-only value nested inside another field (e.g. a
14905
+ * private blob inside `parameters`) would NOT be caught by adding its name here; that shape needs
14906
+ * its own per-field handling, not just a new entry in this array.
14907
+ *
14908
+ * Adjacent unredacted shape, not yet a leak: `promptMeta.executionTracking.steps[].result`/
14909
+ * `.error` (PromptMetaZodSchema) are the same class of owner-only content but nothing writes them
14910
+ * today, so there is nothing to redact yet. A future writer landing there bypasses this list
14911
+ * entirely unless it is added here too.
14912
+ */
14913
+ const OWNER_ONLY_FUNCTION_CALL_FIELDS = ["returnValue", "error"];
14914
+ /**
14915
+ * Verbatim retrieved passage text on a citation chip (#3038). Owner-only for exactly the reason
14916
+ * `returnValue` is: it is a slice of a document the OWNER's retrieval read on the owner's behalf,
14917
+ * and a share/subscribe/clone grant authorizes reading the conversation, not re-reading the
14918
+ * owner's corpus through it. The sibling `chunkId` is deliberately kept - an opaque id is not
14919
+ * content, and `citables[].id` (the file id) is already unredacted beside it.
14920
+ */
14921
+ const OWNER_ONLY_CITABLE_METADATA_FIELDS = ["fullContext"];
14922
+ [...OWNER_ONLY_FUNCTION_CALL_FIELDS.map((field) => `promptMeta.functionCalls.${field}`), ...OWNER_ONLY_CITABLE_METADATA_FIELDS.map((field) => `promptMeta.citables.metadata.${field}`)];
14923
+ /**
14183
14924
  * Trigger-word validation, shared between client form and server agent
14184
14925
  * endpoints so the validation rules can't drift.
14185
14926
  *
@@ -14253,6 +14994,7 @@ function isImageServeable(f) {
14253
14994
  function isImageAttachment(mimeType) {
14254
14995
  return typeof mimeType === "string" && mimeType.toLowerCase().startsWith("image/");
14255
14996
  }
14997
+ IMAGE_SIZE_CONSTRAINTS.BFL.minWidth, IMAGE_SIZE_CONSTRAINTS.BFL.maxWidth;
14256
14998
  Array.from(new Set([
14257
14999
  {
14258
15000
  id: "opti.root",
@@ -15391,6 +16133,42 @@ async function withRetry(fn, options = {}) {
15391
16133
  }
15392
16134
  }
15393
16135
  }
16136
+ function readZipEntryBounded(entry, maxBytes) {
16137
+ return new Promise((resolve, reject) => {
16138
+ const stream = entry.internalStream("nodebuffer");
16139
+ let parts = [];
16140
+ let byteLength = 0;
16141
+ let settled = false;
16142
+ stream.on("data", (chunk) => {
16143
+ if (settled) return;
16144
+ byteLength += chunk.length;
16145
+ if (byteLength > maxBytes) {
16146
+ settled = true;
16147
+ stream.pause();
16148
+ parts = [];
16149
+ resolve({
16150
+ ok: false,
16151
+ reason: "too-large"
16152
+ });
16153
+ return;
16154
+ }
16155
+ parts.push(chunk);
16156
+ }).on("error", (error) => {
16157
+ if (settled) return;
16158
+ settled = true;
16159
+ parts = [];
16160
+ reject(error);
16161
+ }).on("end", () => {
16162
+ if (settled) return;
16163
+ settled = true;
16164
+ resolve({
16165
+ ok: true,
16166
+ text: Buffer.concat(parts).toString("utf8"),
16167
+ byteLength
16168
+ });
16169
+ }).resume();
16170
+ });
16171
+ }
15394
16172
  //#endregion
15395
16173
  //#region src/utils/apiUrl.ts
15396
16174
  /**
@@ -15511,6 +16289,212 @@ function getEnvironmentName(configApiConfig) {
15511
16289
  if (/^https?:\/\/(localhost|127\.0\.0\.1)(:|\/|$)/i.test(endpoint.url)) return "Local Dev";
15512
16290
  return "Self-Hosted";
15513
16291
  }
16292
+ //#endregion
16293
+ //#region src/config/toolSafety.ts
16294
+ /**
16295
+ * Tool safety categories determine when permission is required
16296
+ */
16297
+ const ToolCategorySchema = z$1.enum([
16298
+ "auto_approve",
16299
+ "prompt_always",
16300
+ "prompt_default"
16301
+ ]);
16302
+ z$1.object({
16303
+ categories: z$1.record(z$1.string(), ToolCategorySchema),
16304
+ trustedTools: z$1.array(z$1.string())
16305
+ });
16306
+ /**
16307
+ * Default tool categories
16308
+ *
16309
+ * Categories:
16310
+ * - auto_approve: Safe tools that don't need permission (math, search, datetime)
16311
+ * - prompt_always: Dangerous tools that ALWAYS need permission, cannot be trusted (file edits, shell commands)
16312
+ * - prompt_default: Tools that prompt by default but users can trust them (file reads, searches)
16313
+ */
16314
+ const DEFAULT_TOOL_CATEGORIES = {
16315
+ math_evaluate: "auto_approve",
16316
+ current_datetime: "auto_approve",
16317
+ dice_roll: "auto_approve",
16318
+ prompt_enhancement: "auto_approve",
16319
+ find_definition: "auto_approve",
16320
+ ask_user_question: "auto_approve",
16321
+ weather_info: "prompt_default",
16322
+ edit_file: "prompt_always",
16323
+ edit_local_file: "prompt_always",
16324
+ create_file: "prompt_always",
16325
+ delete_file: "prompt_always",
16326
+ shell_execute: "prompt_always",
16327
+ bash_execute: "prompt_always",
16328
+ write_shell_stdin: "prompt_always",
16329
+ kill_background_shell: "prompt_always",
16330
+ git_commit: "prompt_always",
16331
+ git_push: "prompt_always",
16332
+ skill: "prompt_always",
16333
+ web_search: "prompt_default",
16334
+ check_shell_output: "prompt_default",
16335
+ list_background_shells: "prompt_default",
16336
+ web_fetch: "prompt_default",
16337
+ deep_research: "prompt_default",
16338
+ file_read: "prompt_default",
16339
+ grep_search: "prompt_default",
16340
+ glob_files: "prompt_default",
16341
+ get_file_tree: "prompt_default",
16342
+ get_file_structure: "prompt_default",
16343
+ git_status: "prompt_default",
16344
+ git_diff: "prompt_default",
16345
+ git_log: "prompt_default",
16346
+ git_branch: "prompt_default"
16347
+ };
16348
+ /**
16349
+ * Get the category for a tool
16350
+ * Returns 'prompt_default' if tool is not in the default categories
16351
+ */
16352
+ function getToolCategory(toolName, customCategories) {
16353
+ if (toolName.startsWith("agent_hook:") || toolName.startsWith("skill_hook:")) return "prompt_always";
16354
+ if (customCategories && toolName in customCategories) return customCategories[toolName];
16355
+ if (toolName in DEFAULT_TOOL_CATEGORIES) return DEFAULT_TOOL_CATEGORIES[toolName];
16356
+ if (toolName.startsWith("mcp__")) return "prompt_always";
16357
+ return "prompt_default";
16358
+ }
16359
+ /**
16360
+ * Check if a tool can be trusted (not prompt_always)
16361
+ */
16362
+ function canTrustTool(toolName, customCategories) {
16363
+ return getToolCategory(toolName, customCategories) !== "prompt_always";
16364
+ }
16365
+ /**
16366
+ * Check if a tool is read-only (safe for parallel execution).
16367
+ * Write tools (prompt_always category) must always be sequential.
16368
+ *
16369
+ * @param toolName - Name of the tool to check
16370
+ * @param customCategories - Optional custom category overrides
16371
+ * @returns true if the tool is read-only, false if it's a write tool
16372
+ */
16373
+ function isReadOnlyTool(toolName, customCategories) {
16374
+ return getToolCategory(toolName, customCategories) !== "prompt_always";
16375
+ }
16376
+ const PROJECT_CONTEXT_FILES = [
16377
+ "CLAUDE.local.md",
16378
+ "CLAUDE.md",
16379
+ "AGENTS.md",
16380
+ "AI.local.md",
16381
+ "AI.md",
16382
+ "INSTRUCTIONS.md"
16383
+ ];
16384
+ const GLOBAL_CONTEXT_FILES = ["AI.local.md", "AI.md"];
16385
+ /**
16386
+ * Format file size for display
16387
+ */
16388
+ function formatFileSize(bytes) {
16389
+ if (bytes < 1024) return `${bytes}B`;
16390
+ if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)}KB`;
16391
+ return `${(bytes / 1048576).toFixed(1)}MB`;
16392
+ }
16393
+ /**
16394
+ * Try to read a context file from a directory
16395
+ *
16396
+ * Security: Only reads regular files (not directories or symlinks) within the specified directory.
16397
+ * Files must be under 100KB to prevent abuse. Symlinks are rejected to prevent reading
16398
+ * files outside the intended directory.
16399
+ *
16400
+ * @param dir - The directory to read from (must be a controlled location)
16401
+ * @param filename - The filename to read (must not contain path separators)
16402
+ * @param source - Whether this is a 'global' or 'project' context file
16403
+ * @returns The file result, an error object, or null if file doesn't exist
16404
+ */
16405
+ function tryReadContextFile(dir, filename, source) {
16406
+ const filePath = path$1.join(dir, filename);
16407
+ try {
16408
+ const stats = fs$2.lstatSync(filePath);
16409
+ if (stats.isDirectory()) return null;
16410
+ if (stats.isSymbolicLink()) return { error: `${source === "global" ? "Global" : "Project"} ${filename} is a symlink (not allowed for security)` };
16411
+ if (stats.size > 102400) return { error: `${source === "global" ? "Global" : "Project"} ${filename} exceeds 100KB limit (${formatFileSize(stats.size)})` };
16412
+ return {
16413
+ filename,
16414
+ content: fs$2.readFileSync(filePath, "utf-8"),
16415
+ source,
16416
+ path: filePath
16417
+ };
16418
+ } catch (err) {
16419
+ if (err.code === "ENOENT") return null;
16420
+ if (err.code === "EACCES") return { error: `Cannot read ${source} ${filename}: permission denied` };
16421
+ return { error: `Cannot read ${source} ${filename}: ${err instanceof Error ? err.message : "Unknown error"}` };
16422
+ }
16423
+ }
16424
+ /**
16425
+ * Find the first context file in a directory from a list of candidates
16426
+ */
16427
+ function findContextFile(dir, candidates, source) {
16428
+ for (const filename of candidates) {
16429
+ const result = tryReadContextFile(dir, filename, source);
16430
+ if (result === null) continue;
16431
+ if ("error" in result) return {
16432
+ result: null,
16433
+ error: result.error
16434
+ };
16435
+ return {
16436
+ result,
16437
+ error: null
16438
+ };
16439
+ }
16440
+ return {
16441
+ result: null,
16442
+ error: null
16443
+ };
16444
+ }
16445
+ /**
16446
+ * Merge global and project context into a single string
16447
+ */
16448
+ function mergeContextContent(global, project) {
16449
+ if (global && project) return `${global.content}\n\n---\n\n${project.content}`;
16450
+ if (global) return global.content;
16451
+ if (project) return project.content;
16452
+ return "";
16453
+ }
16454
+ /**
16455
+ * Load context files from global and project directories
16456
+ *
16457
+ * Global files are loaded from ~/.bike4mind/
16458
+ * Project files are loaded from `projectDir`. When it is null the project layer
16459
+ * is SKIPPED entirely (not defaulted to cwd) - the folder-trust gate passes null
16460
+ * for an untrusted project so its repo context file is never loaded.
16461
+ *
16462
+ * Returns the first matching file from each layer based on priority order
16463
+ */
16464
+ async function loadContextFiles(projectDir) {
16465
+ const errors = [];
16466
+ const globalDir = path$1.join(homedir$1(), ".bike4mind");
16467
+ const [globalResult, projectResult] = await Promise.all([Promise.resolve(findContextFile(globalDir, GLOBAL_CONTEXT_FILES, "global")), Promise.resolve(projectDir ? findContextFile(projectDir, PROJECT_CONTEXT_FILES, "project") : {
16468
+ result: null,
16469
+ error: null
16470
+ })]);
16471
+ if (globalResult.error) errors.push(globalResult.error);
16472
+ if (projectResult.error) errors.push(projectResult.error);
16473
+ const mergedContent = mergeContextContent(globalResult.result, projectResult.result);
16474
+ return {
16475
+ globalContext: globalResult.result,
16476
+ projectContext: projectResult.result,
16477
+ mergedContent,
16478
+ errors
16479
+ };
16480
+ }
16481
+ /**
16482
+ * Extract "# Compact Instructions" or "## Compact Instructions" section from CLAUDE.md content
16483
+ *
16484
+ * This section provides project-specific instructions for how conversations should be
16485
+ * summarized when compacting context.
16486
+ *
16487
+ * @param contextContent - The merged context content from CLAUDE.md files
16488
+ * @returns The extracted instructions content, or undefined if not found
16489
+ */
16490
+ function extractCompactInstructions(contextContent) {
16491
+ const match = contextContent.match(/^#{1,2}\s*Compact\s*Instructions\s*$/im);
16492
+ if (!match || match.index === void 0) return;
16493
+ const startIndex = match.index + match[0].length;
16494
+ const remainingContent = contextContent.slice(startIndex);
16495
+ const endIndex = remainingContent.match(/^#{1,2}\s+\S/m)?.index ?? remainingContent.length;
16496
+ return remainingContent.slice(0, endIndex).trim() || void 0;
16497
+ }
15514
16498
  const logger = class Logger {
15515
16499
  static {
15516
16500
  this.instance = null;
@@ -15928,6 +16912,7 @@ const CliConfigSchema = z$1.object({
15928
16912
  hearth: z$1.boolean().optional()
15929
16913
  }).catchall(z$1.boolean())).optional().prefault({}),
15930
16914
  trustedTools: z$1.array(z$1.string()).optional().prefault([]),
16915
+ trustedProjects: z$1.array(z$1.string()).optional().prefault([]),
15931
16916
  sandbox: SandboxConfigSchema.optional(),
15932
16917
  additionalDirectories: z$1.array(z$1.string()).optional().prefault([]),
15933
16918
  fallbackModels: z$1.array(z$1.string()).optional()
@@ -16036,6 +17021,7 @@ const DEFAULT_CONFIG = {
16036
17021
  config: {}
16037
17022
  },
16038
17023
  trustedTools: [],
17024
+ trustedProjects: [],
16039
17025
  additionalDirectories: []
16040
17026
  };
16041
17027
  /**
@@ -16056,6 +17042,19 @@ function findProjectConfigDir(startDir = process.cwd()) {
16056
17042
  return process.cwd();
16057
17043
  }
16058
17044
  /**
17045
+ * Canonicalize a path via realpath, returning null if it doesn't exist or can't
17046
+ * be resolved. Used so a symlinked or relative project root is compared against
17047
+ * the trust set by its real location, and a resolve failure fails safe
17048
+ * (untrusted) rather than crashing the launch.
17049
+ */
17050
+ async function safeRealpath(p) {
17051
+ try {
17052
+ return await promises.realpath(p);
17053
+ } catch {
17054
+ return null;
17055
+ }
17056
+ }
17057
+ /**
16059
17058
  * Load project config from .bike4mind/config.json
16060
17059
  * Returns null if file doesn't exist (this is normal - config.json is optional)
16061
17060
  */
@@ -16163,32 +17162,89 @@ function mergeMcpServers(...serverArrays) {
16163
17162
  for (const servers of serverArrays) if (servers) for (const server of servers) serverMap.set(server.name, server);
16164
17163
  return Array.from(serverMap.values());
16165
17164
  }
17165
+ /** Sandbox modes ordered from weakest to strongest posture. */
17166
+ const SANDBOX_MODE_RANK = {
17167
+ disabled: 0,
17168
+ "auto-allow": 1,
17169
+ permissions: 2
17170
+ };
17171
+ function intersectStrings(a, b) {
17172
+ const set = new Set(a);
17173
+ return b.filter((x) => set.has(x));
17174
+ }
17175
+ function unionStrings(a, b) {
17176
+ return Array.from(/* @__PURE__ */ new Set([...a, ...b]));
17177
+ }
17178
+ /** True when `target` is `root` itself or nested under it (no `..` escape). */
17179
+ function isWithin(root, target) {
17180
+ const rel = path.relative(root, target);
17181
+ return rel === "" || !rel.startsWith("..") && !path.isAbsolute(rel);
17182
+ }
16166
17183
  /**
16167
- * Deep-merge sandbox configs. Later values override earlier ones.
16168
- * Arrays (allowedReadPaths, deniedPaths, excludedCommands) are replaced, not concatenated.
17184
+ * Merge a repo-sourced sandbox override onto the user's base posture so it can
17185
+ * only ever TIGHTEN it, never loosen it. A repo layer may enable the sandbox,
17186
+ * raise the mode (disabled < auto-allow < permissions) but never select
17187
+ * auto-allow itself, add denied paths, narrow read paths / domains / excluded
17188
+ * commands, turn the network filter on, and force allowUnsandboxedCommands off.
17189
+ * Every loosening value is ignored with a warning. `platform` is ignored.
16169
17190
  */
16170
- function mergeSandboxConfig(base, override) {
16171
- const resolved = base ?? DEFAULT_SANDBOX_CONFIG;
16172
- if (!override) return resolved;
16173
- return {
16174
- enabled: override.enabled ?? resolved.enabled,
16175
- mode: override.mode ?? resolved.mode,
16176
- filesystem: {
16177
- ...resolved.filesystem,
16178
- ...override.filesystem ?? {}
16179
- },
16180
- network: {
16181
- ...resolved.network,
16182
- ...override.network ?? {}
16183
- },
16184
- excludedCommands: override.excludedCommands ?? resolved.excludedCommands,
16185
- allowUnsandboxedCommands: override.allowUnsandboxedCommands ?? resolved.allowUnsandboxedCommands,
16186
- platform: override.platform ?? resolved.platform
17191
+ function tightenSandbox(base, repo) {
17192
+ const b = base ?? DEFAULT_SANDBOX_CONFIG;
17193
+ if (!repo) return b;
17194
+ const warn = (what) => logger.warn(`Ignoring repo sandbox override that would loosen posture: ${what}`);
17195
+ const result = {
17196
+ ...b,
17197
+ filesystem: { ...b.filesystem },
17198
+ network: { ...b.network }
16187
17199
  };
17200
+ if (repo.enabled === true) result.enabled = true;
17201
+ else if (repo.enabled === false && b.enabled) warn("sandbox.enabled=false");
17202
+ if (repo.mode !== void 0) if (repo.mode === "auto-allow") warn("sandbox.mode=auto-allow");
17203
+ else if (SANDBOX_MODE_RANK[repo.mode] >= SANDBOX_MODE_RANK[b.mode]) result.mode = repo.mode;
17204
+ else warn(`sandbox.mode=${repo.mode} weaker than ${b.mode}`);
17205
+ if (repo.filesystem) {
17206
+ const fsr = repo.filesystem;
17207
+ if (fsr.deniedPaths) result.filesystem.deniedPaths = unionStrings(result.filesystem.deniedPaths, fsr.deniedPaths);
17208
+ if (fsr.allowedReadPaths) result.filesystem.allowedReadPaths = intersectStrings(result.filesystem.allowedReadPaths, fsr.allowedReadPaths);
17209
+ if (fsr.writeOnlyToWorkingDir === true) result.filesystem.writeOnlyToWorkingDir = true;
17210
+ else if (fsr.writeOnlyToWorkingDir === false && b.filesystem.writeOnlyToWorkingDir) warn("filesystem.writeOnlyToWorkingDir=false");
17211
+ }
17212
+ if (repo.network) {
17213
+ const nr = repo.network;
17214
+ if (nr.enabled === true) result.network.enabled = true;
17215
+ else if (nr.enabled === false && b.network.enabled) warn("network.enabled=false");
17216
+ if (nr.allowedDomains) result.network.allowedDomains = intersectStrings(result.network.allowedDomains, nr.allowedDomains);
17217
+ }
17218
+ if (repo.excludedCommands) result.excludedCommands = intersectStrings(result.excludedCommands, repo.excludedCommands);
17219
+ if (repo.allowUnsandboxedCommands === false) result.allowUnsandboxedCommands = false;
17220
+ else if (repo.allowUnsandboxedCommands === true && !b.allowUnsandboxedCommands) warn("allowUnsandboxedCommands=true");
17221
+ if (result.enabled && result.mode === "disabled") result.mode = "permissions";
17222
+ if (!result.enabled) result.mode = "disabled";
17223
+ return result;
17224
+ }
17225
+ /**
17226
+ * Fold repo-sourced MCP servers under the global set so a repo entry can never
17227
+ * REPLACE a same-named global server. Global definitions always win; repo
17228
+ * entries only fill names global does not already use. Later repo layers win
17229
+ * over earlier ones for names global does not define.
17230
+ */
17231
+ function mergeMcpServersGlobalWins(global, ...repoLayers) {
17232
+ const byName = /* @__PURE__ */ new Map();
17233
+ for (const layer of repoLayers) {
17234
+ if (!layer) continue;
17235
+ for (const server of layer) byName.set(server.name, server);
17236
+ }
17237
+ if (global) for (const server of global) byName.set(server.name, server);
17238
+ return Array.from(byName.values());
16188
17239
  }
16189
17240
  /**
16190
- * Merge configs with priority: global -> project -> local
16191
- * Each layer overrides the previous one
17241
+ * Merge configs with priority: global -> project -> local, with the invariant
17242
+ * that repo layers may only TIGHTEN the user's global security posture, never
17243
+ * loosen it. Sandbox goes through `tightenSandbox`; the repo `trustedTools`
17244
+ * union is filtered to tools that can actually be trusted and aren't globally
17245
+ * disabled; and any tool a repo tries to re-enable while it is disabled is
17246
+ * dropped. `mcpServers` is NOT merged here - callers fold repo servers in via
17247
+ * `mergeMcpServersGlobalWins` where all repo layers are visible together.
16192
17248
  */
16193
17249
  function mergeConfigs(global, project, local) {
16194
17250
  const merged = { ...global };
@@ -16207,8 +17263,7 @@ function mergeConfigs(global, project, local) {
16207
17263
  ...project.tools.config
16208
17264
  }
16209
17265
  };
16210
- if (project.mcpServers) merged.mcpServers = mergeMcpServers(merged.mcpServers, project.mcpServers);
16211
- if (project.sandbox) merged.sandbox = mergeSandboxConfig(merged.sandbox, project.sandbox);
17266
+ if (project.sandbox) merged.sandbox = tightenSandbox(merged.sandbox, project.sandbox);
16212
17267
  }
16213
17268
  if (local) {
16214
17269
  if (local.trustedTools) {
@@ -16223,9 +17278,15 @@ function mergeConfigs(global, project, local) {
16223
17278
  ...merged.preferences,
16224
17279
  ...local.preferences
16225
17280
  };
16226
- if (local.mcpServers) merged.mcpServers = mergeMcpServers(merged.mcpServers, local.mcpServers);
16227
- if (local.sandbox) merged.sandbox = mergeSandboxConfig(merged.sandbox, local.sandbox);
17281
+ if (local.sandbox) merged.sandbox = tightenSandbox(merged.sandbox, local.sandbox);
16228
17282
  }
17283
+ const disabledSet = new Set(merged.tools.disabled);
17284
+ merged.tools = {
17285
+ ...merged.tools,
17286
+ enabled: merged.tools.enabled.filter((t) => !disabledSet.has(t))
17287
+ };
17288
+ const globalTrusted = new Set(global.trustedTools || []);
17289
+ merged.trustedTools = (merged.trustedTools || []).filter((t) => globalTrusted.has(t) || canTrustTool(t) && !disabledSet.has(t));
16229
17290
  return merged;
16230
17291
  }
16231
17292
  /**
@@ -16255,6 +17316,12 @@ var ConfigStore = class {
16255
17316
  constructor(configPath) {
16256
17317
  this.config = null;
16257
17318
  this.projectConfigDir = null;
17319
+ this.globalConfig = null;
17320
+ this.projectRealPath = null;
17321
+ this.projectTrusted = false;
17322
+ this.rawProjectConfig = null;
17323
+ this.rawProjectLocalConfig = null;
17324
+ this.rawMcpJsonServers = null;
16258
17325
  this.configPath = configPath || path.join(homedir(), ".bike4mind", "config.json");
16259
17326
  }
16260
17327
  /**
@@ -16321,35 +17388,33 @@ var ConfigStore = class {
16321
17388
  trustedTools: validated.trustedTools || []
16322
17389
  };
16323
17390
  } catch (error) {
16324
- if (error.code === "ENOENT") globalConfig = { ...DEFAULT_CONFIG };
17391
+ if (error.code === "ENOENT") globalConfig = structuredClone(DEFAULT_CONFIG);
16325
17392
  else if (error instanceof z$1.ZodError) {
16326
17393
  console.error("Global config validation error:", error.issues);
16327
17394
  console.error("Using default configuration");
16328
- globalConfig = { ...DEFAULT_CONFIG };
17395
+ globalConfig = structuredClone(DEFAULT_CONFIG);
16329
17396
  } else throw error;
16330
17397
  }
16331
- let projectConfig = null;
16332
- let projectLocalConfig = null;
16333
- let mcpJsonServers = null;
17398
+ this.globalConfig = globalConfig;
17399
+ this.projectConfigDir = null;
17400
+ this.projectRealPath = null;
17401
+ this.projectTrusted = false;
17402
+ this.rawProjectConfig = null;
17403
+ this.rawProjectLocalConfig = null;
17404
+ this.rawMcpJsonServers = null;
16334
17405
  if (process.env.B4M_NO_PROJECT_CONFIG !== "1") {
16335
17406
  this.projectConfigDir = findProjectConfigDir();
16336
17407
  if (this.projectConfigDir) {
16337
- projectConfig = await loadProjectConfig(this.projectConfigDir);
16338
- projectLocalConfig = await loadProjectLocalConfig(this.projectConfigDir);
16339
- mcpJsonServers = await loadMcpJsonConfig(this.projectConfigDir);
16340
- if (projectConfig) logger.debug(`📁 Project config loaded from: ${this.projectConfigDir}/.bike4mind/`);
16341
- if (mcpJsonServers && mcpJsonServers.length > 0) logger.debug(`📁 Project MCP config loaded from: ${this.projectConfigDir}/.mcp.json`);
17408
+ this.projectRealPath = await safeRealpath(this.projectConfigDir) ?? this.projectConfigDir;
17409
+ this.projectTrusted = (globalConfig.trustedProjects || []).includes(this.projectRealPath);
17410
+ if (this.projectTrusted) {
17411
+ const loaded = await this.loadProjectLayers();
17412
+ if (loaded.hasConfig) logger.debug(`📁 Project config loaded from: ${this.projectConfigDir}/.bike4mind/`);
17413
+ if (loaded.mcpCount > 0) logger.debug(`📁 Project MCP config loaded from: ${this.projectConfigDir}/.mcp.json`);
17414
+ }
16342
17415
  }
16343
- } else this.projectConfigDir = null;
16344
- const mergedConfig = mergeConfigs(globalConfig, projectConfig, projectLocalConfig);
16345
- if (mcpJsonServers && mcpJsonServers.length > 0) mergedConfig.mcpServers = mergeMcpServers(mcpJsonServers, mergedConfig.mcpServers);
16346
- const mcpConfigFile = process.env.B4M_MCP_CONFIG_FILE;
16347
- if (mcpConfigFile) {
16348
- const injected = await loadMcpConfigFile(mcpConfigFile);
16349
- if (process.env.B4M_STRICT_MCP_CONFIG === "1") mergedConfig.mcpServers = injected ?? [];
16350
- else if (injected) mergedConfig.mcpServers = mergeMcpServers(mergedConfig.mcpServers, injected);
16351
17416
  }
16352
- this.config = mergedConfig;
17417
+ this.config = await this.computeMerged();
16353
17418
  return this.config;
16354
17419
  } catch (error) {
16355
17420
  if (error.code === "ENOENT") return this.reset();
@@ -16363,6 +17428,116 @@ var ConfigStore = class {
16363
17428
  }
16364
17429
  }
16365
17430
  /**
17431
+ * Load the raw repo config layers for the current project root. Called only
17432
+ * for a trusted root; stores them on `this` so trust changes can re-merge
17433
+ * without re-reading the global config file.
17434
+ */
17435
+ async loadProjectLayers() {
17436
+ if (!this.projectConfigDir) return {
17437
+ hasConfig: false,
17438
+ mcpCount: 0
17439
+ };
17440
+ this.rawProjectConfig = await loadProjectConfig(this.projectConfigDir);
17441
+ this.rawProjectLocalConfig = await loadProjectLocalConfig(this.projectConfigDir);
17442
+ this.rawMcpJsonServers = await loadMcpJsonConfig(this.projectConfigDir);
17443
+ return {
17444
+ hasConfig: !!this.rawProjectConfig,
17445
+ mcpCount: this.rawMcpJsonServers?.length ?? 0
17446
+ };
17447
+ }
17448
+ /**
17449
+ * Build the merged effective config from the global layer plus the raw repo
17450
+ * layers - but only when the project is trusted, so an untrusted root
17451
+ * contributes nothing. Repo MCP servers are folded in global-wins (a repo
17452
+ * name can never replace a global server); an explicit `--mcp-config` is host-
17453
+ * injected (not repo-sourced) and keeps its override-by-name / strict scope.
17454
+ */
17455
+ async computeMerged() {
17456
+ const global = this.globalConfig;
17457
+ const project = this.projectTrusted ? this.rawProjectConfig : null;
17458
+ const local = this.projectTrusted ? this.rawProjectLocalConfig : null;
17459
+ const mcpJson = this.projectTrusted ? this.rawMcpJsonServers : null;
17460
+ const merged = mergeConfigs(global, project, local);
17461
+ merged.mcpServers = mergeMcpServersGlobalWins(global.mcpServers, mcpJson, project?.mcpServers, local?.mcpServers);
17462
+ const mcpConfigFile = process.env.B4M_MCP_CONFIG_FILE;
17463
+ if (mcpConfigFile) {
17464
+ const injected = await loadMcpConfigFile(mcpConfigFile);
17465
+ if (process.env.B4M_STRICT_MCP_CONFIG === "1") merged.mcpServers = injected ?? [];
17466
+ else if (injected) merged.mcpServers = mergeMcpServers(merged.mcpServers, injected);
17467
+ }
17468
+ return merged;
17469
+ }
17470
+ /** Whether the current project root is trusted (folder-trust gate). */
17471
+ isProjectTrusted() {
17472
+ return this.projectTrusted;
17473
+ }
17474
+ /** Canonicalized project root discovered this session, or null. */
17475
+ getProjectRealPath() {
17476
+ return this.projectRealPath;
17477
+ }
17478
+ /** The realpath'd roots the user has explicitly trusted. */
17479
+ getTrustedProjects() {
17480
+ return this.globalConfig?.trustedProjects ? [...this.globalConfig.trustedProjects] : [];
17481
+ }
17482
+ /**
17483
+ * Whether the current project root ships any repo-committed b4m files that the
17484
+ * trust gate governs. Used to decide whether the startup trust prompt is even
17485
+ * worth showing (nothing to gate = no prompt).
17486
+ */
17487
+ projectHasB4mFiles() {
17488
+ const root = this.projectConfigDir;
17489
+ if (!root) return false;
17490
+ return [
17491
+ [".bike4mind", "config.json"],
17492
+ [".bike4mind", "local.json"],
17493
+ [".bike4mind", "agents"],
17494
+ [".bike4mind", "commands"],
17495
+ [".mcp.json"],
17496
+ [".claude", "agents"],
17497
+ [".claude", "skills"],
17498
+ [".claude", "commands"],
17499
+ ...PROJECT_CONTEXT_FILES.map((name) => [name])
17500
+ ].some((parts) => existsSync(path.join(root, ...parts)));
17501
+ }
17502
+ /**
17503
+ * Trust a project root (default: the current one). Persists the realpath'd
17504
+ * root to the global `trustedProjects` and, when it's the current root, loads
17505
+ * its repo layers and re-merges so they take effect for this session.
17506
+ */
17507
+ async trustProject(root) {
17508
+ await this.load();
17509
+ const target = root ? await safeRealpath(root) : this.projectRealPath;
17510
+ if (!target) return false;
17511
+ const g = this.globalConfig;
17512
+ if (!g.trustedProjects) g.trustedProjects = [];
17513
+ if (!g.trustedProjects.includes(target)) g.trustedProjects.push(target);
17514
+ if (target === this.projectRealPath && this.projectConfigDir && process.env.B4M_NO_PROJECT_CONFIG !== "1") {
17515
+ this.projectTrusted = true;
17516
+ await this.loadProjectLayers();
17517
+ }
17518
+ await this.save();
17519
+ return true;
17520
+ }
17521
+ /**
17522
+ * Revoke trust for a project root (default: the current one). Next launch
17523
+ * re-prompts and repo layers stay inert until re-trusted. Revoking the current
17524
+ * root drops its raw layers so nothing repo-sourced survives in this session.
17525
+ */
17526
+ async untrustProject(root) {
17527
+ await this.load();
17528
+ const target = root ? await safeRealpath(root) : this.projectRealPath;
17529
+ if (!target) return;
17530
+ const g = this.globalConfig;
17531
+ g.trustedProjects = (g.trustedProjects || []).filter((p) => p !== target);
17532
+ if (target === this.projectRealPath) {
17533
+ this.projectTrusted = false;
17534
+ this.rawProjectConfig = null;
17535
+ this.rawProjectLocalConfig = null;
17536
+ this.rawMcpJsonServers = null;
17537
+ }
17538
+ await this.save();
17539
+ }
17540
+ /**
16366
17541
  * Read the features map straight from the global config file, bypassing the
16367
17542
  * in-memory cache. save() merges over this so concurrent writers (the
16368
17543
  * interactive session vs `b4m plugin add`) don't clobber each other's keys.
@@ -16397,47 +17572,52 @@ var ConfigStore = class {
16397
17572
  /**
16398
17573
  * Save configuration to disk
16399
17574
  */
16400
- async save(config) {
17575
+ async save(config, opts) {
16401
17576
  await this.init();
17577
+ if (!this.globalConfig) await this.load();
17578
+ const global = this.globalConfig;
16402
17579
  if (config) {
16403
- const existingConfig = await this.load();
16404
- this.config = {
16405
- ...existingConfig,
16406
- ...config,
16407
- auth: config.auth !== void 0 ? config.auth : existingConfig.auth,
17580
+ const { mcpServers, trustedTools, additionalDirectories, trustedProjects, tools, sandbox, ...rest } = config;
17581
+ this.globalConfig = {
17582
+ ...global,
17583
+ ...rest,
17584
+ auth: "auth" in config ? config.auth : global.auth,
16408
17585
  preferences: {
16409
- ...existingConfig.preferences,
17586
+ ...global.preferences,
16410
17587
  ...config.preferences || {}
16411
17588
  },
16412
- tools: {
16413
- ...existingConfig.tools,
16414
- ...config.tools || {}
16415
- },
16416
17589
  toolApiKeys: {
16417
- ...existingConfig.toolApiKeys,
17590
+ ...global.toolApiKeys,
16418
17591
  ...config.toolApiKeys || {}
16419
17592
  },
16420
- features: this.mergeFeatures(await this.readDiskFeatures(), existingConfig.features, config.features ?? existingConfig.features)
17593
+ features: opts?.clearFeatures ? {} : this.mergeFeatures(await this.readDiskFeatures(), global.features, config.features ?? global.features)
16421
17594
  };
16422
- }
16423
- if (!this.config) throw new Error("No configuration to save");
17595
+ } else this.globalConfig = {
17596
+ ...global,
17597
+ features: opts?.clearFeatures ? {} : await this.readDiskFeatures()
17598
+ };
16424
17599
  try {
16425
- await promises.writeFile(this.configPath, JSON.stringify(this.config, null, 2), "utf-8");
17600
+ await promises.writeFile(this.configPath, JSON.stringify(this.globalConfig, null, 2), "utf-8");
16426
17601
  await promises.chmod(this.configPath, 384);
16427
17602
  } catch (error) {
16428
17603
  console.error("Failed to save config:", error);
16429
17604
  throw error;
16430
17605
  }
17606
+ this.config = await this.computeMerged();
16431
17607
  }
16432
17608
  /**
16433
17609
  * Reset configuration to defaults
16434
17610
  */
16435
17611
  async reset() {
16436
- this.config = {
16437
- ...DEFAULT_CONFIG,
17612
+ this.globalConfig = {
17613
+ ...structuredClone(DEFAULT_CONFIG),
16438
17614
  userId: v4()
16439
17615
  };
16440
- await this.save();
17616
+ this.projectTrusted = false;
17617
+ this.rawProjectConfig = null;
17618
+ this.rawProjectLocalConfig = null;
17619
+ this.rawMcpJsonServers = null;
17620
+ await this.save(void 0, { clearFeatures: true });
16441
17621
  return this.config;
16442
17622
  }
16443
17623
  /**
@@ -16453,56 +17633,71 @@ var ConfigStore = class {
16453
17633
  await this.save(updates);
16454
17634
  }
16455
17635
  /**
17636
+ * Persist a sandbox config to the GLOBAL layer (the `/sandbox` handlers' path).
17637
+ * Sandbox is excluded from the generic `save()` allowlist so it flows only
17638
+ * through here - a merged-config save() can never launder a repo-tightened
17639
+ * sandbox into the user's global default.
17640
+ */
17641
+ async saveSandboxConfig(sandbox) {
17642
+ await this.load();
17643
+ this.globalConfig.sandbox = sandbox;
17644
+ await this.save();
17645
+ }
17646
+ /**
16456
17647
  * Add MCP server configuration
16457
17648
  */
16458
17649
  async addMcpServer(server) {
16459
- const config = await this.load();
16460
- config.mcpServers = config.mcpServers.filter((s) => s.name !== server.name);
16461
- config.mcpServers.push(server);
16462
- await this.save(config);
17650
+ await this.load();
17651
+ const g = this.globalConfig;
17652
+ g.mcpServers = g.mcpServers.filter((s) => s.name !== server.name);
17653
+ g.mcpServers.push(server);
17654
+ await this.save();
16463
17655
  }
16464
17656
  /**
16465
17657
  * Remove MCP server configuration
16466
17658
  */
16467
17659
  async removeMcpServer(name) {
16468
- const config = await this.load();
16469
- config.mcpServers = config.mcpServers.filter((s) => s.name !== name);
16470
- await this.save(config);
17660
+ await this.load();
17661
+ const g = this.globalConfig;
17662
+ g.mcpServers = g.mcpServers.filter((s) => s.name !== name);
17663
+ await this.save();
16471
17664
  }
16472
17665
  /**
16473
17666
  * Enable/disable MCP server
16474
17667
  */
16475
17668
  async toggleMcpServer(name, enabled) {
16476
- const config = await this.load();
16477
- const server = config.mcpServers.find((s) => s.name === name);
17669
+ await this.load();
17670
+ const server = this.globalConfig.mcpServers.find((s) => s.name === name);
16478
17671
  if (server) {
16479
17672
  server.enabled = enabled;
16480
- await this.save(config);
17673
+ await this.save();
16481
17674
  }
16482
17675
  }
16483
17676
  /**
16484
17677
  * Add a tool to trusted tools list
16485
17678
  */
16486
17679
  async trustTool(toolName) {
16487
- const config = await this.load();
16488
- if (!config.trustedTools) config.trustedTools = [];
16489
- if (!config.trustedTools.includes(toolName)) {
16490
- config.trustedTools.push(toolName);
16491
- await this.save(config);
17680
+ await this.load();
17681
+ const g = this.globalConfig;
17682
+ if (!g.trustedTools) g.trustedTools = [];
17683
+ if (!g.trustedTools.includes(toolName)) {
17684
+ g.trustedTools.push(toolName);
17685
+ await this.save();
16492
17686
  }
16493
17687
  }
16494
17688
  /**
16495
17689
  * Remove a tool from trusted tools list
16496
17690
  */
16497
17691
  async untrustTool(toolName) {
16498
- const config = await this.load();
16499
- if (config.trustedTools) {
16500
- config.trustedTools = config.trustedTools.filter((t) => t !== toolName);
16501
- await this.save(config);
17692
+ await this.load();
17693
+ const g = this.globalConfig;
17694
+ if (g.trustedTools) {
17695
+ g.trustedTools = g.trustedTools.filter((t) => t !== toolName);
17696
+ await this.save();
16502
17697
  }
16503
17698
  }
16504
17699
  /**
16505
- * Get list of trusted tools
17700
+ * Get list of trusted tools (merged effective view)
16506
17701
  */
16507
17702
  async getTrustedTools() {
16508
17703
  return (await this.load()).trustedTools || [];
@@ -16511,9 +17706,9 @@ var ConfigStore = class {
16511
17706
  * Clear all trusted tools
16512
17707
  */
16513
17708
  async clearTrustedTools() {
16514
- const config = await this.load();
16515
- config.trustedTools = [];
16516
- await this.save(config);
17709
+ await this.load();
17710
+ this.globalConfig.trustedTools = [];
17711
+ await this.save();
16517
17712
  }
16518
17713
  /**
16519
17714
  * Get authentication tokens
@@ -16525,17 +17720,17 @@ var ConfigStore = class {
16525
17720
  * Set authentication tokens
16526
17721
  */
16527
17722
  async setAuthTokens(tokens) {
16528
- const config = await this.load();
16529
- config.auth = tokens;
16530
- await this.save(config);
17723
+ await this.load();
17724
+ this.globalConfig.auth = tokens;
17725
+ await this.save();
16531
17726
  }
16532
17727
  /**
16533
17728
  * Clear authentication tokens (logout)
16534
17729
  */
16535
17730
  async clearAuthTokens() {
16536
- const config = await this.load();
16537
- config.auth = void 0;
16538
- await this.save(config);
17731
+ await this.load();
17732
+ this.globalConfig.auth = void 0;
17733
+ await this.save();
16539
17734
  }
16540
17735
  /**
16541
17736
  * Check if user is authenticated
@@ -16556,10 +17751,9 @@ var ConfigStore = class {
16556
17751
  * Pass null to reset to the build-time default service.
16557
17752
  */
16558
17753
  async setCustomApiUrl(url) {
16559
- const config = await this.load();
16560
- if (url === null) config.apiConfig = void 0;
16561
- else config.apiConfig = { customUrl: url };
16562
- await this.save(config);
17754
+ await this.load();
17755
+ this.globalConfig.apiConfig = url === null ? void 0 : { customUrl: url };
17756
+ await this.save();
16563
17757
  }
16564
17758
  /**
16565
17759
  * Switch the active API environment, caching auth tokens per-environment so
@@ -16576,8 +17770,9 @@ var ConfigStore = class {
16576
17770
  * preserve the previous `auth` and defeat the per-env swap.
16577
17771
  */
16578
17772
  async switchApiEnvironment(target) {
16579
- const config = await this.load();
16580
- const prevKey = normalizeEnvKey(config.apiConfig?.customUrl || getDefaultApiUrl());
17773
+ await this.load();
17774
+ const g = this.globalConfig;
17775
+ const prevKey = normalizeEnvKey(g.apiConfig?.customUrl || getDefaultApiUrl());
16581
17776
  let newUrl;
16582
17777
  let newApiConfig;
16583
17778
  if (target === "prod") {
@@ -16596,16 +17791,15 @@ var ConfigStore = class {
16596
17791
  url: newUrl,
16597
17792
  envName,
16598
17793
  changed: false,
16599
- authenticated: hasValidAuth(config.auth)
17794
+ authenticated: hasValidAuth(g.auth)
16600
17795
  };
16601
- const authByEnv = { ...config.authByEnv || {} };
16602
- if (config.auth) authByEnv[prevKey] = config.auth;
17796
+ const authByEnv = { ...g.authByEnv || {} };
17797
+ if (g.auth) authByEnv[prevKey] = g.auth;
16603
17798
  else delete authByEnv[prevKey];
16604
17799
  const restored = authByEnv[newKey];
16605
- config.apiConfig = newApiConfig;
16606
- config.authByEnv = authByEnv;
16607
- config.auth = restored;
16608
- config.features = await this.readDiskFeatures();
17800
+ g.apiConfig = newApiConfig;
17801
+ g.authByEnv = authByEnv;
17802
+ g.auth = restored;
16609
17803
  await this.save();
16610
17804
  return {
16611
17805
  url: newUrl,
@@ -16693,40 +17887,48 @@ var ConfigStore = class {
16693
17887
  * Persists to global config
16694
17888
  */
16695
17889
  async addDirectory(dirPath) {
16696
- const config = await this.load();
16697
- if (!config.additionalDirectories) config.additionalDirectories = [];
17890
+ await this.load();
17891
+ const g = this.globalConfig;
17892
+ if (!g.additionalDirectories) g.additionalDirectories = [];
16698
17893
  const resolvedPath = path.resolve(dirPath);
16699
- if (!config.additionalDirectories.includes(resolvedPath)) {
16700
- config.additionalDirectories.push(resolvedPath);
16701
- await this.save(config);
17894
+ if (!g.additionalDirectories.includes(resolvedPath)) {
17895
+ g.additionalDirectories.push(resolvedPath);
17896
+ await this.save();
16702
17897
  }
16703
17898
  }
16704
17899
  /**
16705
17900
  * Remove a directory from the allowed directories list
16706
17901
  */
16707
17902
  async removeDirectory(dirPath) {
16708
- const config = await this.load();
16709
- if (config.additionalDirectories) {
17903
+ await this.load();
17904
+ const g = this.globalConfig;
17905
+ if (g.additionalDirectories) {
16710
17906
  const resolvedPath = path.resolve(dirPath);
16711
- config.additionalDirectories = config.additionalDirectories.filter((d) => path.resolve(d) !== resolvedPath);
16712
- await this.save(config);
17907
+ g.additionalDirectories = g.additionalDirectories.filter((d) => path.resolve(d) !== resolvedPath);
17908
+ await this.save();
16713
17909
  }
16714
17910
  }
16715
17911
  /**
16716
- * Get all additional directories (merged from global + project configs)
16717
- * Returns resolved absolute paths
17912
+ * Get all additional directories (global config + trusted-project config).
17913
+ * Returns resolved absolute paths. Project-declared directories are included
17914
+ * ONLY when the project is trusted, and each must resolve inside the project
17915
+ * root (a repo cannot widen file access beyond its own tree).
16718
17916
  */
16719
17917
  async getAdditionalDirectories() {
16720
- const config = await this.load();
17918
+ await this.load();
17919
+ const g = this.globalConfig;
16721
17920
  const dirs = /* @__PURE__ */ new Set();
16722
- if (config.additionalDirectories) for (const dir of config.additionalDirectories) dirs.add(path.resolve(dir));
16723
- const projectConfig = await this.loadRawProjectConfig();
16724
- if (projectConfig?.additionalDirectories) {
16725
- const projectRoot = this.projectConfigDir || process.cwd();
16726
- for (const dir of projectConfig.additionalDirectories) dirs.add(path.resolve(projectRoot, dir));
17921
+ if (g.additionalDirectories) for (const dir of g.additionalDirectories) dirs.add(path.resolve(dir));
17922
+ if (this.projectTrusted && this.rawProjectConfig?.additionalDirectories) {
17923
+ const projectRoot = this.projectRealPath || this.projectConfigDir;
17924
+ if (projectRoot) for (const dir of this.rawProjectConfig.additionalDirectories) {
17925
+ const real = await safeRealpath(path.resolve(projectRoot, dir));
17926
+ if (real && isWithin(projectRoot, real)) dirs.add(real);
17927
+ else logger.warn(`Ignoring project additionalDirectory outside project root: ${dir}`);
17928
+ }
16727
17929
  }
16728
17930
  return Array.from(dirs);
16729
17931
  }
16730
17932
  };
16731
17933
  //#endregion
16732
- export { SupportedFabFileMimeTypes as $, selfClaimedActorKindSchema as $t, HTTPError as A, isPlaceholderApiKey as At, OPENAI_GPT_IMAGE_1_IMAGE_SIZES as B, reservationOutputTokens as Bt, DEFAULT_MUSIC_MODEL_ID as C, isGPTImageModel as Ct, FIXED_TEMPERATURE_MODELS as D, isMediaModelType as Dt, FIELD_GROUP_OF as E, isImageServeable as Et, MODEL_INFO_FIELD_GROUP_OF as F, isUserInitiatedAbort as Ft, PermissionDeniedError as G, toModelRecord as Gt, OllamaEmbeddingModel as H, secureParameters as Ht, McpServerName as I, isZodError as It, REFUSAL_FALLBACK_MODELS as J, withRetry as Jt, REASONING_EFFORT_INCOMPATIBLE_WITH_TOOLS_MODELS as K, usdToCredits as Kt, ModelBackend as L, mapMimeTypeToArtifactType as Lt, IMAGE_SIZE_CONSTRAINTS as M, isRetryableError as Mt, ImageModels as N, isSupportedFabFileMimeType as Nt, FORMAT_PROMPT_TEMPLATE as O, isModelAccessible as Ot, InternalServerError as P, isUnlimitedHistory as Pt, SpeechToTextModels as Q, actorKindSchema as Qt, NO_TEMPERATURE_MODELS as R, obfuscateApiKey as Rt, CorruptedFileError as S, isGPTImage2Model as St, DEGENERATE_FINISH_REASON as T, isImageAttachment as Tt, OpenAIEmbeddingModel as U, settingsMap as Ut, OPENAI_GPT_IMAGE_2_IMAGE_SIZES as V, resolveHistoryFetchLimit as Vt, PROMPT_TEXT_MAX as W, toModelInfo as Wt, REVIEW_GATE_STATUS_VALUES as X, actorColorIndex as Xt, RESPONSES_API_TOOL_MODELS as Y, ACTOR_COLOR_SLOTS as Yt, SUBQUEST_STATUS_VALUES as Z, actorKindMarker as Zt, BadRequestError as _, isAudioMimeType as _t, getCreditsUrl as a, VideoModels as at, CREDIT_DEDUCT_TRANSACTION_TYPES as b, isEarlyStop as bt, requireApiUrl as c, applyModelPriceCatalog as ct, AGENT_QUEST_MANIFEST as d, defaultEmbeddingModelForEnv as dt, buildRateLimitLogEntry as en, TTS_MAX_INPUT_CHARS as et, AGENT_QUEST_MCP_URI as f, getMcpProviderMetadata as ft, BFL_SAFETY_TOLERANCE as g, hasUsableLimits as gt, BEDROCK_NO_PROMPT_CACHING_MODELS as h, hasKeylessCloudEmbedder as ht, LOCAL_DEV_URL as i, VIDEO_SIZE_CONSTRAINTS as it, HttpStatus as j, isRenderableModelType as jt, ForbiddenError as k, isModelDeprecated as kt, resolveApiEndpoint as l, calculateRetryDelay as lt, ApiKeyType as m, getRetryAfterMs as mt, logger as n, isNearLimit as nn, UnauthorizedError as nt, getEnvironmentName as o, VoyageAIEmbeddingModel as ot, ARTIFACT_ATTRS_PATTERN as p, getQuestErrorCode as pt, REASONING_SUPPORTED_MODELS as q, usdToCreditsStochastic as qt, ApiEndpointUnconfiguredError as r, parseRateLimitHeaders as rn, UnprocessableEntityError as rt, parseApiUrl as s, WORK_ITEM_STATUSES as st, ConfigStore as t, extractSnippetMeta as tn, TooManyRequestsError as tt, AGENT_QUEST_ID as u, dayjsConfig_default as ut, BedrockEmbeddingModel as v, isChunkRebuildPending as vt, DEFAULT_UNKNOWN_CONTEXT_WINDOW as w, isGeminiModelId as wt, ChatModels as x, isFieldGroup as xt, CONTEXT_WINDOW_SAFETY_BUFFER_TOKENS as y, isChunkStalledFile as yt, NotFoundError as z, parseEmbeddingRateLimitHeaders as zt };
17934
+ export { RESPONSES_API_TOOL_MODELS as $, secureParameters as $t, DEFAULT_UNKNOWN_CONTEXT_WINDOW as A, isGPTImage2Model as At, InternalServerError as B, isRetryableError as Bt, BadRequestError as C, hasUsableLimits as Ct, ChatModels as D, isChunkStalledFile as Dt, CREDIT_DEDUCT_TRANSACTION_TYPES as E, isChunkRebuildPending as Et, ForbiddenError as F, isMediaModelType as Ft, NotFoundError as G, isZodError as Gt, McpServerName as H, isSupportedImageSize as Ht, HTTPError as I, isModelAccessible as It, PROMPT_TEXT_MAX as J, parseEmbeddingRateLimitHeaders as Jt, OllamaEmbeddingModel as K, mapMimeTypeToArtifactType as Kt, HttpStatus as L, isModelDeprecated as Lt, FIELD_GROUP_OF as M, isGeminiModelId as Mt, FIXED_TEMPERATURE_MODELS as N, isImageAttachment as Nt, CorruptedFileError as O, isEarlyStop as Ot, FORMAT_PROMPT_TEMPLATE as P, isImageServeable as Pt, REFUSAL_FALLBACK_MODELS as Q, resolveHistoryFetchLimit as Qt, IMAGE_SIZE_CONSTRAINTS as R, isPlaceholderApiKey as Rt, BFL_SAFETY_TOLERANCE as S, hasKeylessCloudEmbedder as St, CONTEXT_WINDOW_SAFETY_BUFFER_TOKENS as T, isBflUltraImageModel as Tt, ModelBackend as U, isUnlimitedHistory as Ut, MODEL_INFO_FIELD_GROUP_OF as V, isSupportedFabFileMimeType as Vt, NO_TEMPERATURE_MODELS as W, isUserInitiatedAbort as Wt, REASONING_EFFORT_INCOMPATIBLE_WITH_TOOLS_MODELS as X, reservationOutputTokens as Xt, PermissionDeniedError as Y, readZipEntryBounded as Yt, REASONING_SUPPORTED_MODELS as Z, resolveGptImageGenerateSize as Zt, AGENT_QUEST_MCP_URI as _, defaultEmbeddingModelForEnv as _t, canTrustTool as a, usdToCreditsStochastic as an, TooManyRequestsError as at, AudioMimeType as b, getQuestErrorCode as bt, ApiEndpointUnconfiguredError as c, actorColorIndex as cn, VIDEO_SIZE_CONSTRAINTS as ct, getEnvironmentName as d, selfClaimedActorKindSchema as dn, WORK_ITEM_STATUSES as dt, settingsMap as en, REVIEW_GATE_STATUS_VALUES as et, parseApiUrl as f, buildRateLimitLogEntry as fn, applyModelPriceCatalog as ft, AGENT_QUEST_MANIFEST as g, dayjsConfig_default as gt, AGENT_QUEST_ID as h, parseRateLimitHeaders as hn, countCodePoints as ht, loadContextFiles as i, usdToCredits as in, TTS_MAX_INPUT_CHARS as it, DEGENERATE_FINISH_REASON as j, isGPTImageModel as jt, DEFAULT_MUSIC_MODEL_ID as k, isFieldGroup as kt, LOCAL_DEV_URL as l, actorKindMarker as ln, VideoModels as lt, resolveApiEndpoint as m, isNearLimit as mn, capForParse as mt, logger as n, toModelRecord as nn, SpeechToTextModels as nt, getToolCategory as o, withRetry as on, UnauthorizedError as ot, requireApiUrl as p, extractSnippetMeta as pn, calculateRetryDelay as pt, OpenAIEmbeddingModel as q, obfuscateApiKey as qt, extractCompactInstructions as r, toNonWebpOutputFormat as rn, SupportedFabFileMimeTypes as rt, isReadOnlyTool as s, ACTOR_COLOR_SLOTS as sn, UnprocessableEntityError as st, ConfigStore as t, toModelInfo as tn, SUBQUEST_STATUS_VALUES as tt, getCreditsUrl as u, actorKindSchema as un, VoyageAIEmbeddingModel as ut, ARTIFACT_ATTRS_PATTERN as v, fallbackImageSize as vt, BedrockEmbeddingModel as w, isAudioMimeType as wt, BEDROCK_NO_PROMPT_CACHING_MODELS as x, getRetryAfterMs as xt, ApiKeyType as y, getMcpProviderMetadata as yt, ImageModels as z, isRenderableModelType as zt };