@bike4mind/cli 1.0.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-CrRb8cMt.mjs → AgentHistoryStore-kT9eNMRO.mjs} +2007 -1122
  2. package/dist/{ApiClient-EUTyn5yu.mjs → ApiClient-BOWpVvTq.mjs} +2 -2
  3. package/dist/{ConfigStore-DHNgFdOu.mjs → ConfigStore-DdHHCH2t.mjs} +1435 -248
  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-CJrkEG0M.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-n0XKoGnL.mjs → grepSearch-BgoOOwGe-DtlV8Gn-.mjs} +3 -3
  19. package/dist/index.mjs +212 -47
  20. package/dist/{package-7a45-Svr.mjs → package-Bqg2LSnH.mjs} +1 -1
  21. package/dist/{pathValidation-D8tjkQXE-1HwvsuYT.mjs → pathValidation-BRqf4HFX-CHwtwp3O.mjs} +7 -3
  22. package/dist/{serve-CavAHPdQ.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
  /**
@@ -1650,7 +1733,8 @@ z$1.object({
1650
1733
  "ok",
1651
1734
  "error",
1652
1735
  "timeout",
1653
- "refusal"
1736
+ "refusal",
1737
+ "degenerate"
1654
1738
  ]).default("ok"),
1655
1739
  latencyMs: z$1.number().optional(),
1656
1740
  /** Originating surface (web/cli/api/agent/system); unset => unclassified. */
@@ -1975,6 +2059,7 @@ const MODEL_INFO_FIELD_GROUP_OF = {
1975
2059
  backend: "identity",
1976
2060
  contextWindow: "limits",
1977
2061
  max_tokens: "limits",
2062
+ maxOutputTokensDerived: "limits",
1978
2063
  can_think: "reasoning",
1979
2064
  thinkingStyle: "reasoning",
1980
2065
  can_stream: "modalities",
@@ -2247,6 +2332,16 @@ const DiscoveryPriceSkip = z$1.object({
2247
2332
  modelId: z$1.string().min(1),
2248
2333
  reason: z$1.string()
2249
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
+ });
2250
2345
  const DiscoveryLifecycleTransition = z$1.object({
2251
2346
  modelId: z$1.string().min(1),
2252
2347
  /** Absent when no row in force carried a lifecycle for this model. */
@@ -2303,6 +2398,8 @@ z$1.object({
2303
2398
  */
2304
2399
  mode: z$1.enum(DISCOVERY_RUN_MODES).optional(),
2305
2400
  sources: z$1.array(DiscoverySourceReport).optional(),
2401
+ /** Configured sources this run never attempted; disjoint from `sources`. */
2402
+ skippedSources: z$1.array(DiscoverySkippedSource).optional(),
2306
2403
  joinCoverage: z$1.array(DiscoveryJoinCoverage).optional(),
2307
2404
  /** Ids no aggregator matched: a work item, not a log line. */
2308
2405
  unmatchedIds: z$1.array(z$1.string()).optional(),
@@ -2419,6 +2516,59 @@ let McpServerName = /* @__PURE__ */ function(McpServerName) {
2419
2516
  McpServerName["Notion"] = "notion";
2420
2517
  return McpServerName;
2421
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
+ });
2422
2572
  /**
2423
2573
  * Check if a value is a placeholder (not configured or SST default).
2424
2574
  * Uses case-insensitive comparison and trims whitespace to prevent bypass attempts.
@@ -2467,10 +2617,16 @@ function isPlaceholderApiKey(value) {
2467
2617
  /**
2468
2618
  * Lake lifecycle. Stable states (draft/active/archived/deleted) plus transitional
2469
2619
  * states (archiving/unarchiving/restoring/deleting/purging) that exist to drive UI and make a crashed
2470
- * mid-operation observable. draft -> active is one-way. It happens implicitly once the lake
2471
- * holds its first member file (see `activateIfDraft` below), and unconditionally when an
2472
- * archived or deleted lake is restored, which is how an empty lake can end up active.
2620
+ * mid-operation observable.
2473
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.
2628
+ *
2629
+
2474
2630
  * `purging` is the one transitional state that is NOT recoverable by retrying the same action:
2475
2631
  * it is claimed the moment a phase-2 hard delete is ACCEPTED (#1744), before the background
2476
2632
  * sweep runs, so that `listDeletedDataLakes` stops offering Restore on a lake whose
@@ -2582,6 +2738,7 @@ const LAKE_CONFIG_FIELD_AUDIT = {
2582
2738
  totalChunkedChars: "excluded",
2583
2739
  embeddingSpendMicroUsd: "excluded",
2584
2740
  lastSyncAt: "excluded",
2741
+ lastHealthCheckedAt: "excluded",
2585
2742
  filesDeletedAt: "excluded",
2586
2743
  filesArchivedAt: "excluded",
2587
2744
  lakeMemoryExtractionAt: "excluded",
@@ -2960,6 +3117,26 @@ function isSupportedFabFileMimeType(mimeType) {
2960
3117
  return !!mimeType && SUPPORTED_FAB_FILE_MIME_TYPES.has(mimeType);
2961
3118
  }
2962
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
+ /**
2963
3140
  * Is this an audio MIME type? Matches any `audio/*` (not just the known set)
2964
3141
  * so the LLM-exclusion and vectorization-skip guards fail safe for any audio
2965
3142
  * subtype a provider might emit.
@@ -3117,6 +3294,18 @@ const isRenderableModelType = (type) => MODEL_INFO_TYPES.includes(type);
3117
3294
  */
3118
3295
  const isMediaModelType = (type) => type === "image" || type === "video";
3119
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
+ /**
3120
3309
  * The record -> ModelInfo adapter: one place where every ModelInfo field a
3121
3310
  * catalog record does not carry gets its default. Each default degrades to the
3122
3311
  * visible, recoverable behavior rather than the silent or expensive one.
@@ -3135,6 +3324,7 @@ function toModelInfo(record) {
3135
3324
  backend: record.backend,
3136
3325
  contextWindow: record.contextWindow,
3137
3326
  max_tokens: record.maxOutputTokens ?? Math.min(record.contextWindow, 4096),
3327
+ ...record.maxOutputTokens === void 0 ? { maxOutputTokensDerived: true } : {},
3138
3328
  pricing: {},
3139
3329
  can_stream: record.canStream,
3140
3330
  can_think: record.reasoning?.supported ?? false,
@@ -3240,7 +3430,7 @@ function toModelRecord(info) {
3240
3430
  type: info.type,
3241
3431
  name: info.name,
3242
3432
  contextWindow: info.contextWindow,
3243
- maxOutputTokens: info.max_tokens,
3433
+ maxOutputTokens: info.maxOutputTokensDerived === true ? void 0 : info.max_tokens,
3244
3434
  canStream: info.can_stream,
3245
3435
  reasoning: info.can_think === void 0 && info.thinkingStyle === void 0 ? void 0 : {
3246
3436
  supported: info.can_think === true,
@@ -3535,7 +3725,7 @@ z$1.object({
3535
3725
  "grounded",
3536
3726
  "surface"
3537
3727
  ]).optional(),
3538
- 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."),
3539
3729
  includePromptDetails: z$1.boolean().optional(),
3540
3730
  includeSystemPrompt: z$1.boolean().optional(),
3541
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.")
@@ -3547,6 +3737,8 @@ z$1.object({
3547
3737
  timestamp: z$1.string(),
3548
3738
  model: z$1.string(),
3549
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(),
3550
3742
  tools: z$1.object({
3551
3743
  toolMode: z$1.enum(["fast", "smart"]).optional(),
3552
3744
  autoSelectedTools: z$1.array(z$1.string()).optional(),
@@ -3559,6 +3751,18 @@ z$1.object({
3559
3751
  poll_url: z$1.string().optional()
3560
3752
  })
3561
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
+ });
3562
3766
  /**
3563
3767
  * Reusable JSON error envelope (plain; the OpenAPI layer annotates it).
3564
3768
  *
@@ -3776,6 +3980,33 @@ const AGENT_EXECUTION_STATUSES = [
3776
3980
  "aborted"
3777
3981
  ];
3778
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
+ /**
3779
4010
  * Operator allow-list for the client-authored Mongo filter carried on a `subscribe_query` frame.
3780
4011
  *
3781
4012
  * The WS data-subscribe handler forwards that filter to `Model.find` and persists it on the
@@ -4044,6 +4275,19 @@ const QuestExportProgressAction = z$1.object({
4044
4275
  droppedQuestCount: z$1.number().optional(),
4045
4276
  clientId: z$1.string().optional()
4046
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
+ });
4047
4291
  const SpiderProgressUpdateAction = z$1.object({
4048
4292
  action: z$1.literal("spider_progress"),
4049
4293
  spiderJobId: z$1.string(),
@@ -4128,13 +4372,7 @@ const StreamedChatCompletionAction = z$1.object({
4128
4372
  replies: z$1.array(z$1.string()).optional(),
4129
4373
  images: z$1.array(z$1.string()).optional(),
4130
4374
  videos: z$1.array(z$1.string()).optional(),
4131
- type: z$1.enum([
4132
- "message",
4133
- "oob",
4134
- "error",
4135
- "system",
4136
- "voice_transcript"
4137
- ]),
4375
+ type: z$1.enum(CHAT_HISTORY_ITEM_TYPES),
4138
4376
  status: z$1.enum([
4139
4377
  "stopped",
4140
4378
  "running",
@@ -4935,18 +5173,13 @@ const SessionCreatedAction = shareableDocumentSchema.extend({
4935
5173
  claudeConversationId: z$1.string().optional(),
4936
5174
  summary: z$1.string().optional(),
4937
5175
  summaryAt: z$1.date().optional(),
4938
- summaryTrigger: z$1.enum([
4939
- "manual",
4940
- "project",
4941
- "earlyMilestone",
4942
- "contentGrowth",
4943
- "throttling"
4944
- ]).optional(),
5176
+ summaryTrigger: z$1.enum(SESSION_SUMMARY_TRIGGERS).optional(),
4945
5177
  deletedAt: z$1.date().optional(),
4946
5178
  tags: z$1.array(z$1.object({
4947
5179
  name: z$1.string(),
4948
5180
  strength: z$1.number()
4949
5181
  })).optional(),
5182
+ taggedAt: z$1.date().optional(),
4950
5183
  clonedSourceId: z$1.string().nullable().optional(),
4951
5184
  forkedSourceId: z$1.string().nullable().optional(),
4952
5185
  isAutoNamed: z$1.boolean().optional(),
@@ -5208,6 +5441,7 @@ z$1.discriminatedUnion("action", [
5208
5441
  ResearchTaskStatusUpdateAction,
5209
5442
  NotebookCurationProgressUpdateAction,
5210
5443
  QuestExportProgressAction,
5444
+ OrgFeedbackSummaryProgressAction,
5211
5445
  SpiderProgressUpdateAction,
5212
5446
  SpiderCompleteAction,
5213
5447
  SpiderErrorAction,
@@ -5466,11 +5700,27 @@ const CompletionContentEventSchema = z$1.object({
5466
5700
  stopReason: z$1.string().optional(),
5467
5701
  thinking: z$1.array(z$1.any()).optional()
5468
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
+ */
5469
5719
  const CompletionSseErrorEventSchema = z$1.object({
5470
5720
  type: z$1.literal("error"),
5471
5721
  message: z$1.string(),
5472
5722
  requestId: z$1.string().optional(),
5473
- code: z$1.string().optional()
5723
+ code: z$1.enum(QUEST_ERROR_CODES).optional()
5474
5724
  });
5475
5725
  z$1.union([
5476
5726
  CompletionMetaEventSchema,
@@ -6156,21 +6406,9 @@ const ALL_IMAGE_MODELS = [
6156
6406
  ...XAI_IMAGE_MODELS,
6157
6407
  ...GEMINI_IMAGE_MODELS
6158
6408
  ];
6159
- const OPENAI_GPT_IMAGE_1_IMAGE_SIZES = [
6160
- "1024x1024",
6161
- "1024x1536",
6162
- "1536x1024"
6163
- ];
6164
- const OPENAI_GPT_IMAGE_2_IMAGE_SIZES = [
6165
- "1024x1024",
6166
- "1536x1024",
6167
- "1024x1536",
6168
- "2048x2048",
6169
- "2048x1152",
6170
- "3840x2160",
6171
- "2160x3840",
6172
- "auto"
6173
- ];
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];
6174
6412
  const BFL_IMAGE_SIZES = ["1024x768"];
6175
6413
  const OPENAI_IMAGE_SIZES = [...OPENAI_GPT_IMAGE_1_IMAGE_SIZES];
6176
6414
  const ALL_IMAGE_SIZES = [
@@ -6180,16 +6418,41 @@ const ALL_IMAGE_SIZES = [
6180
6418
  ];
6181
6419
  z$1.enum(OPENAI_IMAGE_SIZES);
6182
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'" })]);
6183
- const OpenAIImageQualitySchema = z$1.enum([
6421
+ const OPENAI_IMAGE_QUALITIES = [
6184
6422
  "standard",
6185
6423
  "hd",
6186
6424
  "low",
6187
6425
  "medium",
6188
6426
  "high",
6189
6427
  "auto"
6190
- ]);
6428
+ ];
6429
+ const OpenAIImageQualitySchema = z$1.enum(OPENAI_IMAGE_QUALITIES);
6430
+ OPENAI_IMAGE_QUALITIES.filter((quality) => quality !== "auto");
6191
6431
  const OpenAIImageStyleSchema = z$1.enum(["vivid", "natural"]);
6192
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
+ /**
6193
6456
  * Maps legacy/removed image model IDs to their current replacements.
6194
6457
  * Prevents Zod validation failures when clients send stale persisted model names.
6195
6458
  *
@@ -6232,6 +6495,7 @@ const OpenAIImageGenerationInput = z$1.object({
6232
6495
  response_format: z$1.enum(["b64_json", "url"]).optional(),
6233
6496
  size: ImageSizeSchema.nullable().optional(),
6234
6497
  style: OpenAIImageStyleSchema.optional(),
6498
+ background: OpenAIImageBackgroundSchema.nullable().optional(),
6235
6499
  width: z$1.number().optional(),
6236
6500
  height: z$1.number().optional(),
6237
6501
  aspect_ratio: z$1.string().optional()
@@ -6354,6 +6618,7 @@ z$1.object({
6354
6618
  tags: z$1.array(SessionTagSchema).optional(),
6355
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."),
6356
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."),
6357
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.")
6358
6623
  });
6359
6624
  z$1.object({ id: z$1.string().min(1) });
@@ -6365,6 +6630,8 @@ z$1.object({
6365
6630
  artifactIds: z$1.array(z$1.string()).optional(),
6366
6631
  tags: z$1.array(SessionTagSchema).optional(),
6367
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\"."),
6368
6635
  lastUsedModel: z$1.string().nullish(),
6369
6636
  firstCreated: z$1.date(),
6370
6637
  lastUpdated: z$1.date()
@@ -6683,7 +6950,7 @@ function defaultEmbeddingModelForEnv() {
6683
6950
  const hasOllama = !!process.env.OLLAMA_BASE_URL?.trim();
6684
6951
  const hasCloudEmbeddingKey = !isPlaceholderApiKey(process.env.OPENAI_API_KEY) || !isPlaceholderApiKey(process.env.VOYAGE_API_KEY);
6685
6952
  if (selfHost && hasOllama && !hasCloudEmbeddingKey) return "qwen3-embedding:0.6b";
6686
- return "text-embedding-ada-002";
6953
+ return "text-embedding-3-small";
6687
6954
  }
6688
6955
  /**
6689
6956
  * True when this deployment can embed with no provider API key at all: a cloud stage reaches
@@ -6732,15 +6999,15 @@ z$1.union([
6732
6999
  ]);
6733
7000
  /**
6734
7001
  * The measured per-space floors, rendered for an admin-facing description (e.g. "75 for
6735
- * text-embedding-ada-002, 35 for text-embedding-3-small").
7002
+ * text-embedding-ada-002, 58 for text-embedding-3-small").
6736
7003
  *
6737
- * Rendered rather than written out in prose because these numbers are expected to move - 35 is
6738
- * provisional until it is re-derived against a production lake - and a description that restates
6739
- * 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.
6740
7007
  */
6741
7008
  const forcedRetrievalFloorsBySpaceSummary = Object.entries({
6742
7009
  ["text-embedding-ada-002"]: 75,
6743
- ["text-embedding-3-small"]: 35
7010
+ ["text-embedding-3-small"]: 58
6744
7011
  }).map(([space, pct]) => `${pct} for ${space}`).join(", ");
6745
7012
  /**
6746
7013
  * Default text for the artifact-emission system prompt. Single source of truth used BOTH as the
@@ -7078,6 +7345,7 @@ z$1.enum([
7078
7345
  "kbSearchMinRelevancePct",
7079
7346
  "forcedRetrievalRelativeFloorPct",
7080
7347
  "forcedRetrievalMinSimilarityPct",
7348
+ "forcedRetrievalSpreadFloorPct",
7081
7349
  "dataLakeEmbeddingSpendEnabled",
7082
7350
  "dataLakeEmbeddingBudgetPerRunUsd",
7083
7351
  "dataLakeEmbeddingBudgetPerLakeUsd",
@@ -7315,7 +7583,7 @@ function makeNumberSetting(config) {
7315
7583
  return {
7316
7584
  ...config,
7317
7585
  type: "number",
7318
- 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))
7319
7587
  };
7320
7588
  }
7321
7589
  function makeBooleanSetting(config) {
@@ -7392,6 +7660,17 @@ const RapidReplySettingsSchema = z$1.object({
7392
7660
  lastUpdated: /* @__PURE__ */ new Date()
7393
7661
  })
7394
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";
7395
7674
  const L = {
7396
7675
  temperature: {
7397
7676
  min: 0,
@@ -7495,8 +7774,8 @@ const WhatsNewConfigSchema = z$1.object({
7495
7774
  maxCommitMessageLength: z$1.number().min(L.maxCommitMessageLength.min).max(L.maxCommitMessageLength.max).default(L.maxCommitMessageLength.default),
7496
7775
  maxPRBodyLength: z$1.number().min(L.maxPRBodyLength.min).max(L.maxPRBodyLength.max).default(L.maxPRBodyLength.default),
7497
7776
  maxChangelogLength: z$1.number().min(L.maxChangelogLength.min).max(L.maxChangelogLength.max).default(L.maxChangelogLength.default),
7498
- repository: z$1.string().regex(/^[\w.-]+\/[\w.-]+$/, "Must be in owner/repo format (e.g., MyOrg/my-repo)").default("MillionOnMars/lumina5"),
7499
- 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),
7500
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()
7501
7780
  });
7502
7781
  const WhatsNewSyncConfigSchema = z$1.object({
@@ -7934,8 +8213,12 @@ const API_SERVICE_GROUPS = {
7934
8213
  order: 10
7935
8214
  },
7936
8215
  {
7937
- key: "dataLakeSearchMaxChunksPerFile",
8216
+ key: "forcedRetrievalSpreadFloorPct",
7938
8217
  order: 11
8218
+ },
8219
+ {
8220
+ key: "dataLakeSearchMaxChunksPerFile",
8221
+ order: 12
7939
8222
  }
7940
8223
  ]
7941
8224
  },
@@ -10192,7 +10475,7 @@ const settingsMap = {
10192
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.",
10193
10476
  category: "AI",
10194
10477
  group: API_SERVICE_GROUPS.EMBEDDING.id,
10195
- order: 11,
10478
+ order: 12,
10196
10479
  scope: { settableAt: ["organization", "owner"] }
10197
10480
  }),
10198
10481
  forcedRetrievalCharBudget: makeNumberSetting({
@@ -10275,12 +10558,25 @@ const settingsMap = {
10275
10558
  min: 1,
10276
10559
  max: 100,
10277
10560
  int: true,
10278
- 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.`,
10279
10562
  category: "AI",
10280
10563
  group: API_SERVICE_GROUPS.EMBEDDING.id,
10281
10564
  order: 10,
10282
10565
  scope: { settableAt: ["organization", "owner"] }
10283
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
+ }),
10284
10580
  LakeAccessAuditRetentionDays: makeNumberSetting({
10285
10581
  key: "LakeAccessAuditRetentionDays",
10286
10582
  name: "Lake Access Audit Retention (days)",
@@ -10708,8 +11004,8 @@ const settingsMap = {
10708
11004
  maxCommitMessageLength: 200,
10709
11005
  maxPRBodyLength: 500,
10710
11006
  maxChangelogLength: 1e3,
10711
- repository: "MillionOnMars/lumina5",
10712
- targetBranch: "main"
11007
+ repository: WHATS_NEW_DEFAULT_REPOSITORY,
11008
+ targetBranch: WHATS_NEW_DEFAULT_TARGET_BRANCH
10713
11009
  },
10714
11010
  description: "Configuration for automated What's New modal generation, including LLM model selection, prompt parameters, validation rules, and content sanitization limits.",
10715
11011
  category: "Admin",
@@ -11281,7 +11577,14 @@ const TokensBySourceSchema = z$1.object({
11281
11577
  fabFiles: z$1.number(),
11282
11578
  urlContent: z$1.number(),
11283
11579
  toolSchemas: z$1.number(),
11284
- 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()
11285
11588
  });
11286
11589
  z$1.object({
11287
11590
  /** gen_ai.usage.input_tokens */
@@ -11528,6 +11831,7 @@ const PromptMetaModelParametersSchema = z$1.object({
11528
11831
  prompt_upsampling: z$1.boolean().optional(),
11529
11832
  seed: z$1.number().optional(),
11530
11833
  output_format: z$1.string().optional(),
11834
+ background: z$1.string().optional(),
11531
11835
  response_format: z$1.string().optional(),
11532
11836
  seconds: z$1.number().optional(),
11533
11837
  model: z$1.string().optional()
@@ -11535,11 +11839,7 @@ const PromptMetaModelParametersSchema = z$1.object({
11535
11839
  const PromptMetaModelSchema = z$1.object({
11536
11840
  name: z$1.string(),
11537
11841
  parameters: PromptMetaModelParametersSchema.optional(),
11538
- type: z$1.enum([
11539
- "text",
11540
- "image",
11541
- "video"
11542
- ]).optional(),
11842
+ type: z$1.enum(PROMPT_META_MODEL_TYPES).optional(),
11543
11843
  backend: z$1.string().optional(),
11544
11844
  contextWindow: z$1.number().optional(),
11545
11845
  maxTokens: z$1.number().optional(),
@@ -11593,7 +11893,16 @@ const PromptMetaTokensBySourceSchema = z$1.object({
11593
11893
  fabFiles: z$1.number(),
11594
11894
  urlContent: z$1.number(),
11595
11895
  toolSchemas: z$1.number(),
11596
- 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()
11597
11906
  });
11598
11907
  const PromptMetaContextSchema = z$1.object({
11599
11908
  attachedFiles: z$1.array(PromptMetaAttachedFileSchema).optional(),
@@ -11903,12 +12212,28 @@ const RetrievalSummarySchema = z$1.object({
11903
12212
  * the per-turn routing question is about, and before this it was indistinguishable from a turn
11904
12213
  * where forced retrieval was never configured at all.
11905
12214
  */
11906
- 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(),
11907
12220
  /** Which retrieval-capable surface(s) ran this turn, e.g. 'lake-memory', 'knowledgeBaseSearch'. */
11908
12221
  surfaces: z$1.array(z$1.string()),
11909
12222
  /** Lakes resolved at the moment retrieval ran, stamped point-in-time (not read live from the session). */
11910
12223
  dataLakeTags: z$1.array(z$1.string()),
11911
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
+ /**
11912
12237
  * The lake scope the turn's retrieval surfaces WOULD have searched, resolved at the seed site
11913
12238
  * whether or not any of them ran: the caller's accessible lakes narrowed to the session
11914
12239
  * (narrowLakeAccessToSession), or empty where the corpus is personal and the lake arms are
@@ -11995,16 +12320,25 @@ const RetrievalSummarySchema = z$1.object({
11995
12320
  * not "what reached the model": `ranked.length` and `scored.length` in KnowledgeRetrievalFeature
11996
12321
  * - the candidates left after the absolute similarity floor, and after the relative floor
11997
12322
  * trims them. `chunks` is what survived the char budget on top of that, so the three
11998
- * numbers bracket two independent trimmers:
12323
+ * numbers bracket three independent trimmers:
11999
12324
  *
12000
- * pre -> [relative floor] -> post -> [char budget] -> chunks
12325
+ * pre -> [relative floor] -> post -> [spread floor] -> postSpread -> [char budget] -> chunks
12001
12326
  *
12002
12327
  * They exist so a low `chunks` is diagnosable - a small corpus and a floor that trimmed a large
12003
- * 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;
12004
12330
  * `pre - chunks` is NOT, because the budget trims the same walk. Both optional: only forced
12005
12331
  * retrieval computes a ranked pool, a surface without one (lake memory, the knowledge tools)
12006
- * never writes either, and absence must not read as zero candidates. SUMMED like `chunks`, with
12007
- * 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.
12008
12342
  *
12009
12343
  * COMPARE THE PAIR ONLY TO ITSELF, never to `chunks`, unless `surfaces` is forced retrieval
12010
12344
  * alone. `chunks` and `chars` sum across ALL surfaces while this pair is forced-only, so a mixed
@@ -12026,7 +12360,9 @@ const RetrievalSummarySchema = z$1.object({
12026
12360
  chars: z$1.number(),
12027
12361
  topScore: z$1.number().optional(),
12028
12362
  preRelativeFloorCandidates: z$1.number().optional(),
12029
- postRelativeFloorCandidates: z$1.number().optional()
12363
+ postRelativeFloorCandidates: z$1.number().optional(),
12364
+ postSpreadFloorCandidates: z$1.number().optional(),
12365
+ backgroundScore: z$1.number().optional()
12030
12366
  }).optional(),
12031
12367
  /**
12032
12368
  * Could the corpus in scope have answered this turn, whether or not the model went looking?
@@ -12264,9 +12600,18 @@ z$1.object({
12264
12600
  schemaVersion: z$1.number().int().positive(),
12265
12601
  /** Product identifier: 'vibeswire', 'bike4mind', 'stocksandvibes', 'k2kanji', etc. */
12266
12602
  productId: z$1.string().min(1).max(64),
12267
- /** 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
+ */
12268
12607
  userId: z$1.string().min(1).max(256),
12269
- /** 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
+ */
12270
12615
  sessionId: z$1.string().min(1).max(256),
12271
12616
  /** Event type: 'session_start', 'signup', 'feature_used', etc. */
12272
12617
  event: z$1.string().min(1).max(128),
@@ -12283,6 +12628,153 @@ z$1.object({
12283
12628
  z$1.boolean()
12284
12629
  ])).refine((v) => JSON.stringify(v).length <= 1024, "metadata must be ≤ 1KB serialized").optional()
12285
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
+ });
12286
12778
  const InternalTeamMemberSchema = z$1.object({
12287
12779
  name: z$1.string().min(1, "Name is required"),
12288
12780
  phone: z$1.string().min(1, "Phone is required"),
@@ -12835,12 +13327,13 @@ new Set(DATA_LAKES.map((l) => l.id));
12835
13327
  */
12836
13328
  const normalizeEntitlementKey = (key) => key.trim().toLowerCase();
12837
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\")");
12838
13331
  z.object({
12839
13332
  name: z.string().min(1).max(200),
12840
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\")"),
12841
13334
  description: z.string().max(2e3).optional(),
12842
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`),
12843
- requiredUserTag: z.string().min(1).max(100).optional(),
13336
+ requiredUserTag: requiredUserTagValue.optional(),
12844
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(),
12845
13338
  organizationId: z.string().optional()
12846
13339
  });
@@ -12850,7 +13343,7 @@ z.object({
12850
13343
  systemPrompt: z.string().optional(),
12851
13344
  preferredSystemPromptId: z.union([z.literal(""), z.string().min(1).max(200)]).optional(),
12852
13345
  groundingMode: z.enum(DATA_LAKE_GROUNDING_MODES).optional(),
12853
- requiredUserTag: z.union([z.literal(""), z.string().min(1).max(100)]).optional(),
13346
+ requiredUserTag: z.union([z.literal(""), requiredUserTagValue]).optional(),
12854
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(),
12855
13348
  auditQueryTextEnabled: z.boolean().optional(),
12856
13349
  lakeMemoryEnabled: z.boolean().optional(),
@@ -13096,6 +13589,51 @@ const OPTIONAL_DEP_BLESSED_SCRIPT_PATHS = Object.values({
13096
13589
  }
13097
13590
  }).map((d) => d.path);
13098
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];
13099
13637
  const DashboardParamsSchema = z$1.object({
13100
13638
  dashboardDataSources: z$1.array(z$1.object({
13101
13639
  sourceName: z$1.string(),
@@ -13139,11 +13677,21 @@ OpenAIImageGenerationInput.extend({
13139
13677
  height: z$1.number().optional(),
13140
13678
  aspect_ratio: z$1.string().optional(),
13141
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(),
13142
13690
  tools: z$1.array(z$1.union([b4mLLMTools, z$1.string()])).optional(),
13143
13691
  safety_tolerance: BFLSafetyToleranceSchema,
13144
13692
  prompt_upsampling: z$1.boolean().optional(),
13145
13693
  seed: z$1.number().nullable().optional(),
13146
- output_format: z$1.enum(["jpeg", "png"]).nullable().optional(),
13694
+ output_format: ImageOutputFormatSchema.nullable().optional(),
13147
13695
  /** Resolved by the API route's prompt resolver. Defaults to 'fresh' for first-turn or sessions with no prior image. */
13148
13696
  intent: PromptIntentSchema.optional(),
13149
13697
  promptEnhancement: z$1.object({
@@ -13161,7 +13709,7 @@ OpenAIImageGenerationInput.extend({
13161
13709
  const GenerateImageToolCallSchema = OpenAIImageGenerationInput.extend({
13162
13710
  safety_tolerance: z$1.number().optional(),
13163
13711
  prompt_upsampling: z$1.boolean().optional(),
13164
- output_format: z$1.enum(["jpeg", "png"]).nullable().optional(),
13712
+ output_format: ImageOutputFormatSchema.nullable().optional(),
13165
13713
  seed: z$1.number().nullable().optional(),
13166
13714
  editModel: z$1.string().optional()
13167
13715
  }).omit({ prompt: true });
@@ -13198,7 +13746,18 @@ OpenAIImageGenerationInput.extend({
13198
13746
  organizationId: z$1.string().nullable().optional(),
13199
13747
  aspect_ratio: z$1.string().optional(),
13200
13748
  fabFileIds: z$1.array(z$1.string()).prefault([]),
13201
- 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()
13202
13761
  });
13203
13762
  const isUnlimitedHistory = (historyCount) => historyCount === -1;
13204
13763
  /**
@@ -13232,6 +13791,13 @@ z$1.object({
13232
13791
  message: z$1.string(),
13233
13792
  messageFileIds: z$1.array(z$1.string()).prefault([]),
13234
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(),
13235
13801
  /** Extra context messages to include in the conversation from external sources */
13236
13802
  extraContextMessages: z$1.array(z$1.object({
13237
13803
  role: z$1.enum([
@@ -13267,11 +13833,19 @@ z$1.object({
13267
13833
  * was the only switch for the offer and it also strips every authored prompt, so no caller could
13268
13834
  * have an arm that went unoffered AND kept the abstention licence.
13269
13835
  *
13270
- * 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
13271
13837
  * auto-add, the blog/skill gate), unioned with `Boolean(promptMode)` by
13272
13838
  * resolveSkipAutoOffers. A force-on, not an override: `false` under a promptMode still suppresses.
13273
13839
  * Withholding navigate_view also drops the viewRegistry system block, which only describes it.
13274
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
+ *
13275
13849
  * Withholds the OFFER, not knowledge: `session.forceKnowledgeRetrieval` is untouched, and an
13276
13850
  * already-attached corpus is inlined rather than deferred to the tool. An arm that must see no
13277
13851
  * knowledge at all also needs a session with no attachments and forced retrieval off.
@@ -14132,43 +14706,158 @@ const countCodePoints = (text) => {
14132
14706
  for (const _ch of text) count++;
14133
14707
  return count;
14134
14708
  };
14135
- function isGPTImageModel(model) {
14136
- if (!model) return false;
14137
- return OPENAI_IMAGE_MODELS.includes(model) || model.startsWith("gpt-image-");
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);
14138
14762
  }
14139
- /** Returns true specifically for gpt-image-2 (including versioned snapshots like gpt-image-2-2026-04-21). */
14140
- function isGPTImage2Model(model) {
14141
- if (!model) return false;
14142
- 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
+ };
14143
14785
  }
14144
14786
  /**
14145
- * Whether a user can access a model. Access is any-of (mirrors the Q3b data-lake
14146
- * rule, `getAccessibleDataLakes`): a non-admin reaches the model via
14147
- * `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.
14148
14810
  *
14149
- * `entitlementKeys` is optional - when omitted/empty the entitlement branch is
14150
- * inert, so a model with no `allowedEntitlements` behaves exactly as before
14151
- * (tag-only). This lets a tag-less subscriber reach an entitlement-gated model
14152
- * 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.
14153
14814
  *
14154
- * Pure + zero-dependency (only types + the shared key normalizer), so it lives
14155
- * in `@bike4mind/common` as the SINGLE source of truth - imported by the core
14156
- * `@bike4mind/utils` re-export (server/services) AND the client
14157
- * `useAccessibleModels` hook, which previously kept a hand-rolled twin "to avoid
14158
- * AWS SDK imports". Common is browser-safe, so there is no longer any reason to
14159
- * 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.
14160
14817
  */
14161
- function isModelAccessible(model, userTags, isAdmin = false, entitlementKeys = []) {
14162
- if (!model.enabled) return false;
14163
- if (isAdmin) return true;
14164
- const normalizedUserTags = userTags.map((tag) => tag.toLowerCase());
14165
- const normalizedAllowedTags = (model.allowedUserTags ?? []).map((tag) => tag.toLowerCase());
14166
- if (normalizedUserTags.some((tag) => normalizedAllowedTags.includes(tag))) return true;
14167
- const normalizedKeys = entitlementKeys.map(normalizeEntitlementKey);
14168
- const normalizedAllowedEntitlements = (model.allowedEntitlements ?? []).map(normalizeEntitlementKey);
14169
- 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);
14170
14860
  }
14171
- [...OPENAI_IMAGE_MODELS, ...GEMINI_IMAGE_MODELS];
14172
14861
  /** Generation was cut off against the output-token ceiling. */
14173
14862
  const TRUNCATED_FINISH_REASON = "max_tokens";
14174
14863
  /**
@@ -14195,6 +14884,43 @@ function isEarlyStop(stopReason) {
14195
14884
  return !!stopReason && EARLY_STOP_FINISH_REASONS.has(stopReason);
14196
14885
  }
14197
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
+ /**
14198
14924
  * Trigger-word validation, shared between client form and server agent
14199
14925
  * endpoints so the validation rules can't drift.
14200
14926
  *
@@ -14268,6 +14994,7 @@ function isImageServeable(f) {
14268
14994
  function isImageAttachment(mimeType) {
14269
14995
  return typeof mimeType === "string" && mimeType.toLowerCase().startsWith("image/");
14270
14996
  }
14997
+ IMAGE_SIZE_CONSTRAINTS.BFL.minWidth, IMAGE_SIZE_CONSTRAINTS.BFL.maxWidth;
14271
14998
  Array.from(new Set([
14272
14999
  {
14273
15000
  id: "opti.root",
@@ -15406,6 +16133,42 @@ async function withRetry(fn, options = {}) {
15406
16133
  }
15407
16134
  }
15408
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
+ }
15409
16172
  //#endregion
15410
16173
  //#region src/utils/apiUrl.ts
15411
16174
  /**
@@ -15526,6 +16289,212 @@ function getEnvironmentName(configApiConfig) {
15526
16289
  if (/^https?:\/\/(localhost|127\.0\.0\.1)(:|\/|$)/i.test(endpoint.url)) return "Local Dev";
15527
16290
  return "Self-Hosted";
15528
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
+ }
15529
16498
  const logger = class Logger {
15530
16499
  static {
15531
16500
  this.instance = null;
@@ -15943,6 +16912,7 @@ const CliConfigSchema = z$1.object({
15943
16912
  hearth: z$1.boolean().optional()
15944
16913
  }).catchall(z$1.boolean())).optional().prefault({}),
15945
16914
  trustedTools: z$1.array(z$1.string()).optional().prefault([]),
16915
+ trustedProjects: z$1.array(z$1.string()).optional().prefault([]),
15946
16916
  sandbox: SandboxConfigSchema.optional(),
15947
16917
  additionalDirectories: z$1.array(z$1.string()).optional().prefault([]),
15948
16918
  fallbackModels: z$1.array(z$1.string()).optional()
@@ -16051,6 +17021,7 @@ const DEFAULT_CONFIG = {
16051
17021
  config: {}
16052
17022
  },
16053
17023
  trustedTools: [],
17024
+ trustedProjects: [],
16054
17025
  additionalDirectories: []
16055
17026
  };
16056
17027
  /**
@@ -16071,6 +17042,19 @@ function findProjectConfigDir(startDir = process.cwd()) {
16071
17042
  return process.cwd();
16072
17043
  }
16073
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
+ /**
16074
17058
  * Load project config from .bike4mind/config.json
16075
17059
  * Returns null if file doesn't exist (this is normal - config.json is optional)
16076
17060
  */
@@ -16178,32 +17162,89 @@ function mergeMcpServers(...serverArrays) {
16178
17162
  for (const servers of serverArrays) if (servers) for (const server of servers) serverMap.set(server.name, server);
16179
17163
  return Array.from(serverMap.values());
16180
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
+ }
16181
17183
  /**
16182
- * Deep-merge sandbox configs. Later values override earlier ones.
16183
- * 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.
16184
17190
  */
16185
- function mergeSandboxConfig(base, override) {
16186
- const resolved = base ?? DEFAULT_SANDBOX_CONFIG;
16187
- if (!override) return resolved;
16188
- return {
16189
- enabled: override.enabled ?? resolved.enabled,
16190
- mode: override.mode ?? resolved.mode,
16191
- filesystem: {
16192
- ...resolved.filesystem,
16193
- ...override.filesystem ?? {}
16194
- },
16195
- network: {
16196
- ...resolved.network,
16197
- ...override.network ?? {}
16198
- },
16199
- excludedCommands: override.excludedCommands ?? resolved.excludedCommands,
16200
- allowUnsandboxedCommands: override.allowUnsandboxedCommands ?? resolved.allowUnsandboxedCommands,
16201
- 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 }
16202
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;
16203
17224
  }
16204
17225
  /**
16205
- * Merge configs with priority: global -> project -> local
16206
- * Each layer overrides the previous one
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());
17239
+ }
17240
+ /**
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.
16207
17248
  */
16208
17249
  function mergeConfigs(global, project, local) {
16209
17250
  const merged = { ...global };
@@ -16222,8 +17263,7 @@ function mergeConfigs(global, project, local) {
16222
17263
  ...project.tools.config
16223
17264
  }
16224
17265
  };
16225
- if (project.mcpServers) merged.mcpServers = mergeMcpServers(merged.mcpServers, project.mcpServers);
16226
- if (project.sandbox) merged.sandbox = mergeSandboxConfig(merged.sandbox, project.sandbox);
17266
+ if (project.sandbox) merged.sandbox = tightenSandbox(merged.sandbox, project.sandbox);
16227
17267
  }
16228
17268
  if (local) {
16229
17269
  if (local.trustedTools) {
@@ -16238,9 +17278,15 @@ function mergeConfigs(global, project, local) {
16238
17278
  ...merged.preferences,
16239
17279
  ...local.preferences
16240
17280
  };
16241
- if (local.mcpServers) merged.mcpServers = mergeMcpServers(merged.mcpServers, local.mcpServers);
16242
- if (local.sandbox) merged.sandbox = mergeSandboxConfig(merged.sandbox, local.sandbox);
17281
+ if (local.sandbox) merged.sandbox = tightenSandbox(merged.sandbox, local.sandbox);
16243
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));
16244
17290
  return merged;
16245
17291
  }
16246
17292
  /**
@@ -16270,6 +17316,12 @@ var ConfigStore = class {
16270
17316
  constructor(configPath) {
16271
17317
  this.config = null;
16272
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;
16273
17325
  this.configPath = configPath || path.join(homedir(), ".bike4mind", "config.json");
16274
17326
  }
16275
17327
  /**
@@ -16336,35 +17388,33 @@ var ConfigStore = class {
16336
17388
  trustedTools: validated.trustedTools || []
16337
17389
  };
16338
17390
  } catch (error) {
16339
- if (error.code === "ENOENT") globalConfig = { ...DEFAULT_CONFIG };
17391
+ if (error.code === "ENOENT") globalConfig = structuredClone(DEFAULT_CONFIG);
16340
17392
  else if (error instanceof z$1.ZodError) {
16341
17393
  console.error("Global config validation error:", error.issues);
16342
17394
  console.error("Using default configuration");
16343
- globalConfig = { ...DEFAULT_CONFIG };
17395
+ globalConfig = structuredClone(DEFAULT_CONFIG);
16344
17396
  } else throw error;
16345
17397
  }
16346
- let projectConfig = null;
16347
- let projectLocalConfig = null;
16348
- 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;
16349
17405
  if (process.env.B4M_NO_PROJECT_CONFIG !== "1") {
16350
17406
  this.projectConfigDir = findProjectConfigDir();
16351
17407
  if (this.projectConfigDir) {
16352
- projectConfig = await loadProjectConfig(this.projectConfigDir);
16353
- projectLocalConfig = await loadProjectLocalConfig(this.projectConfigDir);
16354
- mcpJsonServers = await loadMcpJsonConfig(this.projectConfigDir);
16355
- if (projectConfig) logger.debug(`📁 Project config loaded from: ${this.projectConfigDir}/.bike4mind/`);
16356
- 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
+ }
16357
17415
  }
16358
- } else this.projectConfigDir = null;
16359
- const mergedConfig = mergeConfigs(globalConfig, projectConfig, projectLocalConfig);
16360
- if (mcpJsonServers && mcpJsonServers.length > 0) mergedConfig.mcpServers = mergeMcpServers(mcpJsonServers, mergedConfig.mcpServers);
16361
- const mcpConfigFile = process.env.B4M_MCP_CONFIG_FILE;
16362
- if (mcpConfigFile) {
16363
- const injected = await loadMcpConfigFile(mcpConfigFile);
16364
- if (process.env.B4M_STRICT_MCP_CONFIG === "1") mergedConfig.mcpServers = injected ?? [];
16365
- else if (injected) mergedConfig.mcpServers = mergeMcpServers(mergedConfig.mcpServers, injected);
16366
17416
  }
16367
- this.config = mergedConfig;
17417
+ this.config = await this.computeMerged();
16368
17418
  return this.config;
16369
17419
  } catch (error) {
16370
17420
  if (error.code === "ENOENT") return this.reset();
@@ -16378,6 +17428,116 @@ var ConfigStore = class {
16378
17428
  }
16379
17429
  }
16380
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
+ /**
16381
17541
  * Read the features map straight from the global config file, bypassing the
16382
17542
  * in-memory cache. save() merges over this so concurrent writers (the
16383
17543
  * interactive session vs `b4m plugin add`) don't clobber each other's keys.
@@ -16412,47 +17572,52 @@ var ConfigStore = class {
16412
17572
  /**
16413
17573
  * Save configuration to disk
16414
17574
  */
16415
- async save(config) {
17575
+ async save(config, opts) {
16416
17576
  await this.init();
17577
+ if (!this.globalConfig) await this.load();
17578
+ const global = this.globalConfig;
16417
17579
  if (config) {
16418
- const existingConfig = await this.load();
16419
- this.config = {
16420
- ...existingConfig,
16421
- ...config,
16422
- 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,
16423
17585
  preferences: {
16424
- ...existingConfig.preferences,
17586
+ ...global.preferences,
16425
17587
  ...config.preferences || {}
16426
17588
  },
16427
- tools: {
16428
- ...existingConfig.tools,
16429
- ...config.tools || {}
16430
- },
16431
17589
  toolApiKeys: {
16432
- ...existingConfig.toolApiKeys,
17590
+ ...global.toolApiKeys,
16433
17591
  ...config.toolApiKeys || {}
16434
17592
  },
16435
- 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)
16436
17594
  };
16437
- }
16438
- if (!this.config) throw new Error("No configuration to save");
17595
+ } else this.globalConfig = {
17596
+ ...global,
17597
+ features: opts?.clearFeatures ? {} : await this.readDiskFeatures()
17598
+ };
16439
17599
  try {
16440
- 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");
16441
17601
  await promises.chmod(this.configPath, 384);
16442
17602
  } catch (error) {
16443
17603
  console.error("Failed to save config:", error);
16444
17604
  throw error;
16445
17605
  }
17606
+ this.config = await this.computeMerged();
16446
17607
  }
16447
17608
  /**
16448
17609
  * Reset configuration to defaults
16449
17610
  */
16450
17611
  async reset() {
16451
- this.config = {
16452
- ...DEFAULT_CONFIG,
17612
+ this.globalConfig = {
17613
+ ...structuredClone(DEFAULT_CONFIG),
16453
17614
  userId: v4()
16454
17615
  };
16455
- 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 });
16456
17621
  return this.config;
16457
17622
  }
16458
17623
  /**
@@ -16468,56 +17633,71 @@ var ConfigStore = class {
16468
17633
  await this.save(updates);
16469
17634
  }
16470
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
+ /**
16471
17647
  * Add MCP server configuration
16472
17648
  */
16473
17649
  async addMcpServer(server) {
16474
- const config = await this.load();
16475
- config.mcpServers = config.mcpServers.filter((s) => s.name !== server.name);
16476
- config.mcpServers.push(server);
16477
- 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();
16478
17655
  }
16479
17656
  /**
16480
17657
  * Remove MCP server configuration
16481
17658
  */
16482
17659
  async removeMcpServer(name) {
16483
- const config = await this.load();
16484
- config.mcpServers = config.mcpServers.filter((s) => s.name !== name);
16485
- 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();
16486
17664
  }
16487
17665
  /**
16488
17666
  * Enable/disable MCP server
16489
17667
  */
16490
17668
  async toggleMcpServer(name, enabled) {
16491
- const config = await this.load();
16492
- const server = config.mcpServers.find((s) => s.name === name);
17669
+ await this.load();
17670
+ const server = this.globalConfig.mcpServers.find((s) => s.name === name);
16493
17671
  if (server) {
16494
17672
  server.enabled = enabled;
16495
- await this.save(config);
17673
+ await this.save();
16496
17674
  }
16497
17675
  }
16498
17676
  /**
16499
17677
  * Add a tool to trusted tools list
16500
17678
  */
16501
17679
  async trustTool(toolName) {
16502
- const config = await this.load();
16503
- if (!config.trustedTools) config.trustedTools = [];
16504
- if (!config.trustedTools.includes(toolName)) {
16505
- config.trustedTools.push(toolName);
16506
- 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();
16507
17686
  }
16508
17687
  }
16509
17688
  /**
16510
17689
  * Remove a tool from trusted tools list
16511
17690
  */
16512
17691
  async untrustTool(toolName) {
16513
- const config = await this.load();
16514
- if (config.trustedTools) {
16515
- config.trustedTools = config.trustedTools.filter((t) => t !== toolName);
16516
- 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();
16517
17697
  }
16518
17698
  }
16519
17699
  /**
16520
- * Get list of trusted tools
17700
+ * Get list of trusted tools (merged effective view)
16521
17701
  */
16522
17702
  async getTrustedTools() {
16523
17703
  return (await this.load()).trustedTools || [];
@@ -16526,9 +17706,9 @@ var ConfigStore = class {
16526
17706
  * Clear all trusted tools
16527
17707
  */
16528
17708
  async clearTrustedTools() {
16529
- const config = await this.load();
16530
- config.trustedTools = [];
16531
- await this.save(config);
17709
+ await this.load();
17710
+ this.globalConfig.trustedTools = [];
17711
+ await this.save();
16532
17712
  }
16533
17713
  /**
16534
17714
  * Get authentication tokens
@@ -16540,17 +17720,17 @@ var ConfigStore = class {
16540
17720
  * Set authentication tokens
16541
17721
  */
16542
17722
  async setAuthTokens(tokens) {
16543
- const config = await this.load();
16544
- config.auth = tokens;
16545
- await this.save(config);
17723
+ await this.load();
17724
+ this.globalConfig.auth = tokens;
17725
+ await this.save();
16546
17726
  }
16547
17727
  /**
16548
17728
  * Clear authentication tokens (logout)
16549
17729
  */
16550
17730
  async clearAuthTokens() {
16551
- const config = await this.load();
16552
- config.auth = void 0;
16553
- await this.save(config);
17731
+ await this.load();
17732
+ this.globalConfig.auth = void 0;
17733
+ await this.save();
16554
17734
  }
16555
17735
  /**
16556
17736
  * Check if user is authenticated
@@ -16571,10 +17751,9 @@ var ConfigStore = class {
16571
17751
  * Pass null to reset to the build-time default service.
16572
17752
  */
16573
17753
  async setCustomApiUrl(url) {
16574
- const config = await this.load();
16575
- if (url === null) config.apiConfig = void 0;
16576
- else config.apiConfig = { customUrl: url };
16577
- await this.save(config);
17754
+ await this.load();
17755
+ this.globalConfig.apiConfig = url === null ? void 0 : { customUrl: url };
17756
+ await this.save();
16578
17757
  }
16579
17758
  /**
16580
17759
  * Switch the active API environment, caching auth tokens per-environment so
@@ -16591,8 +17770,9 @@ var ConfigStore = class {
16591
17770
  * preserve the previous `auth` and defeat the per-env swap.
16592
17771
  */
16593
17772
  async switchApiEnvironment(target) {
16594
- const config = await this.load();
16595
- const prevKey = normalizeEnvKey(config.apiConfig?.customUrl || getDefaultApiUrl());
17773
+ await this.load();
17774
+ const g = this.globalConfig;
17775
+ const prevKey = normalizeEnvKey(g.apiConfig?.customUrl || getDefaultApiUrl());
16596
17776
  let newUrl;
16597
17777
  let newApiConfig;
16598
17778
  if (target === "prod") {
@@ -16611,16 +17791,15 @@ var ConfigStore = class {
16611
17791
  url: newUrl,
16612
17792
  envName,
16613
17793
  changed: false,
16614
- authenticated: hasValidAuth(config.auth)
17794
+ authenticated: hasValidAuth(g.auth)
16615
17795
  };
16616
- const authByEnv = { ...config.authByEnv || {} };
16617
- if (config.auth) authByEnv[prevKey] = config.auth;
17796
+ const authByEnv = { ...g.authByEnv || {} };
17797
+ if (g.auth) authByEnv[prevKey] = g.auth;
16618
17798
  else delete authByEnv[prevKey];
16619
17799
  const restored = authByEnv[newKey];
16620
- config.apiConfig = newApiConfig;
16621
- config.authByEnv = authByEnv;
16622
- config.auth = restored;
16623
- config.features = await this.readDiskFeatures();
17800
+ g.apiConfig = newApiConfig;
17801
+ g.authByEnv = authByEnv;
17802
+ g.auth = restored;
16624
17803
  await this.save();
16625
17804
  return {
16626
17805
  url: newUrl,
@@ -16708,40 +17887,48 @@ var ConfigStore = class {
16708
17887
  * Persists to global config
16709
17888
  */
16710
17889
  async addDirectory(dirPath) {
16711
- const config = await this.load();
16712
- if (!config.additionalDirectories) config.additionalDirectories = [];
17890
+ await this.load();
17891
+ const g = this.globalConfig;
17892
+ if (!g.additionalDirectories) g.additionalDirectories = [];
16713
17893
  const resolvedPath = path.resolve(dirPath);
16714
- if (!config.additionalDirectories.includes(resolvedPath)) {
16715
- config.additionalDirectories.push(resolvedPath);
16716
- await this.save(config);
17894
+ if (!g.additionalDirectories.includes(resolvedPath)) {
17895
+ g.additionalDirectories.push(resolvedPath);
17896
+ await this.save();
16717
17897
  }
16718
17898
  }
16719
17899
  /**
16720
17900
  * Remove a directory from the allowed directories list
16721
17901
  */
16722
17902
  async removeDirectory(dirPath) {
16723
- const config = await this.load();
16724
- if (config.additionalDirectories) {
17903
+ await this.load();
17904
+ const g = this.globalConfig;
17905
+ if (g.additionalDirectories) {
16725
17906
  const resolvedPath = path.resolve(dirPath);
16726
- config.additionalDirectories = config.additionalDirectories.filter((d) => path.resolve(d) !== resolvedPath);
16727
- await this.save(config);
17907
+ g.additionalDirectories = g.additionalDirectories.filter((d) => path.resolve(d) !== resolvedPath);
17908
+ await this.save();
16728
17909
  }
16729
17910
  }
16730
17911
  /**
16731
- * Get all additional directories (merged from global + project configs)
16732
- * 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).
16733
17916
  */
16734
17917
  async getAdditionalDirectories() {
16735
- const config = await this.load();
17918
+ await this.load();
17919
+ const g = this.globalConfig;
16736
17920
  const dirs = /* @__PURE__ */ new Set();
16737
- if (config.additionalDirectories) for (const dir of config.additionalDirectories) dirs.add(path.resolve(dir));
16738
- const projectConfig = await this.loadRawProjectConfig();
16739
- if (projectConfig?.additionalDirectories) {
16740
- const projectRoot = this.projectConfigDir || process.cwd();
16741
- 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
+ }
16742
17929
  }
16743
17930
  return Array.from(dirs);
16744
17931
  }
16745
17932
  };
16746
17933
  //#endregion
16747
- export { SupportedFabFileMimeTypes as $, actorKindSchema as $t, HTTPError as A, isModelDeprecated as At, OPENAI_GPT_IMAGE_1_IMAGE_SIZES as B, parseEmbeddingRateLimitHeaders as Bt, DEFAULT_MUSIC_MODEL_ID as C, isGPTImage2Model as Ct, FIXED_TEMPERATURE_MODELS as D, isImageServeable as Dt, FIELD_GROUP_OF as E, isImageAttachment as Et, MODEL_INFO_FIELD_GROUP_OF as F, isUnlimitedHistory as Ft, PermissionDeniedError as G, toModelInfo as Gt, OllamaEmbeddingModel as H, resolveHistoryFetchLimit as Ht, McpServerName as I, isUserInitiatedAbort as It, REFUSAL_FALLBACK_MODELS as J, usdToCreditsStochastic as Jt, REASONING_EFFORT_INCOMPATIBLE_WITH_TOOLS_MODELS as K, toModelRecord as Kt, ModelBackend as L, isZodError as Lt, IMAGE_SIZE_CONSTRAINTS as M, isRenderableModelType as Mt, ImageModels as N, isRetryableError as Nt, FORMAT_PROMPT_TEMPLATE as O, isMediaModelType as Ot, InternalServerError as P, isSupportedFabFileMimeType as Pt, SpeechToTextModels as Q, actorKindMarker as Qt, NO_TEMPERATURE_MODELS as R, mapMimeTypeToArtifactType as Rt, CorruptedFileError as S, isFieldGroup as St, DEGENERATE_FINISH_REASON as T, isGeminiModelId as Tt, OpenAIEmbeddingModel as U, secureParameters as Ut, OPENAI_GPT_IMAGE_2_IMAGE_SIZES as V, reservationOutputTokens as Vt, PROMPT_TEXT_MAX as W, settingsMap as Wt, REVIEW_GATE_STATUS_VALUES as X, ACTOR_COLOR_SLOTS as Xt, RESPONSES_API_TOOL_MODELS as Y, withRetry as Yt, SUBQUEST_STATUS_VALUES as Z, actorColorIndex as Zt, BadRequestError as _, hasUsableLimits as _t, getCreditsUrl as a, VideoModels as at, CREDIT_DEDUCT_TRANSACTION_TYPES as b, isChunkStalledFile as bt, requireApiUrl as c, applyModelPriceCatalog as ct, AGENT_QUEST_MANIFEST as d, dayjsConfig_default as dt, selfClaimedActorKindSchema as en, TTS_MAX_INPUT_CHARS as et, AGENT_QUEST_MCP_URI as f, defaultEmbeddingModelForEnv as ft, BFL_SAFETY_TOLERANCE as g, hasKeylessCloudEmbedder as gt, BEDROCK_NO_PROMPT_CACHING_MODELS as h, getRetryAfterMs as ht, LOCAL_DEV_URL as i, parseRateLimitHeaders as in, VIDEO_SIZE_CONSTRAINTS as it, HttpStatus as j, isPlaceholderApiKey as jt, ForbiddenError as k, isModelAccessible as kt, resolveApiEndpoint as l, calculateRetryDelay as lt, ApiKeyType as m, getQuestErrorCode as mt, logger as n, extractSnippetMeta as nn, UnauthorizedError as nt, getEnvironmentName as o, VoyageAIEmbeddingModel as ot, ARTIFACT_ATTRS_PATTERN as p, getMcpProviderMetadata as pt, REASONING_SUPPORTED_MODELS as q, usdToCredits as qt, ApiEndpointUnconfiguredError as r, isNearLimit as rn, UnprocessableEntityError as rt, parseApiUrl as s, WORK_ITEM_STATUSES as st, ConfigStore as t, buildRateLimitLogEntry as tn, TooManyRequestsError as tt, AGENT_QUEST_ID as u, countCodePoints as ut, BedrockEmbeddingModel as v, isAudioMimeType as vt, DEFAULT_UNKNOWN_CONTEXT_WINDOW as w, isGPTImageModel as wt, ChatModels as x, isEarlyStop as xt, CONTEXT_WINDOW_SAFETY_BUFFER_TOKENS as y, isChunkRebuildPending as yt, NotFoundError as z, obfuscateApiKey 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 };