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