@bike4mind/cli 2.0.0 → 2.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/README.md CHANGED
@@ -395,7 +395,7 @@ You can also run any MCP server via npx or custom executables:
395
395
 
396
396
  #### Serve Bike4Mind itself as an MCP server (`b4m mcp serve`)
397
397
 
398
- The reverse of adding a server: `b4m mcp serve` exposes your Bike4Mind backend to any MCP client (Claude Desktop, editors, other agents). It advertises ten tools (`list_notebooks`, `get_notebook`, `create_notebook`, `send_message`, `search_knowledge_base`, `list_lakes`, `list_files`, `get_file`, `generate_sound_effect`, `text_to_speech`) plus four resource templates (`b4m://notebook/{id}`, `b4m://file/{id}`, `b4m://project/{id}`, `b4m://artifact/{id}`), each listable and readable as JSON, and `b4m://agent-quest` - the credential-free manifest for The Open Door, the onboarding quest for agents.
398
+ The reverse of adding a server: `b4m mcp serve` exposes your Bike4Mind backend to any MCP client (Claude Desktop, editors, other agents). It advertises seventeen tools (`list_notebooks`, `get_notebook`, `create_notebook`, `rename_notebook`, `clone_notebook`, `delete_notebook`, `list_projects`, `get_project`, `create_project`, `send_message`, `search_knowledge_base`, `list_lakes`, `list_files`, `get_file`, `generate_sound_effect`, `text_to_speech`, `generate_image`) plus four resource templates (`b4m://notebook/{id}`, `b4m://file/{id}`, `b4m://project/{id}`, `b4m://artifact/{id}`), each listable and readable as JSON, and `b4m://agent-quest` - the credential-free manifest for The Open Door, the onboarding quest for agents.
399
399
 
400
400
  To get answers grounded in a data lake, call `list_lakes`, then `create_notebook { dataLakeId }` with a lake's id, then `send_message { notebookId }` on that notebook: the result carries the `reply` plus the `citables` it was grounded in. `list_lakes` needs a key with `datalake:read` (and data lakes enabled on the instance).
401
401
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { F as NotFoundError, G as WORK_ITEM_STATUSES, I as OllamaEmbeddingModel, L as OpenAIEmbeddingModel, N as McpServerName, O as ChatModels, Q as googleMapsSearchUrl, T as BedrockEmbeddingModel, U as SupportedFabFileMimeTypes, V as SEARCH_RESULT_CARDS_LANGUAGE, W as VoyageAIEmbeddingModel, X as getMcpProviderMetadata, a as htmlErrorTitle, at as secureParameters, bt as APP_NAME, c as loadContextFiles, d as isReadOnlyTool, i as isValidSessionId, it as obfuscateApiKey, j as LOCATION_MAP_LANGUAGE, l as canTrustTool, mt as withRetry, n as logger, nt as isRetryableError, o as htmlFirstH1, ot as signImageUrl, r as SESSION_ID_PATTERN, st as splitEquals, tt as isPlaceholderImageSigningSecret, u as getToolCategory, w as ApiKeyType, xt as WEBSITE_URL, z as PermissionDeniedError } from "./ConfigStore-DIs-K9oD.mjs";
2
+ import { A as ChatModels, B as NotFoundError, E as BedrockEmbeddingModel, H as OpenAIEmbeddingModel, I as LOCATION_MAP_LANGUAGE, J as SupportedFabFileMimeTypes, K as SEARCH_RESULT_CARDS_LANGUAGE, Mt as APP_NAME, Nt as WEBSITE_URL, R as McpServerName, V as OllamaEmbeddingModel, W as PermissionDeniedError, X as WORK_ITEM_STATUSES, Y as VoyageAIEmbeddingModel, a as htmlErrorTitle, c as loadContextFiles, ct as isRetryableError, d as isReadOnlyTool, dt as obfuscateApiKey, gt as splitEquals, ht as signImageUrl, i as isValidSessionId, it as googleMapsSearchUrl, l as canTrustTool, mt as secureParameters, n as logger, nt as getMcpProviderMetadata, o as htmlFirstH1, r as SESSION_ID_PATTERN, st as isPlaceholderImageSigningSecret, u as getToolCategory, w as ApiKeyType, wt as withRetry } from "./ConfigStore-BygzEI6H.mjs";
3
3
  import { n as isPathAllowed, t as assertPathAllowed } from "./pathValidation-BRqf4HFX-CHwtwp3O.mjs";
4
4
  import { n as isTerminalShellStatus, t as getShellSessionManager } from "./ShellSessionManager-6o8KZzl1-vrbPAUTq.mjs";
5
5
  import { execFile, execFileSync, spawn } from "child_process";
@@ -2210,7 +2210,64 @@ const webFetchTool = {
2210
2210
  })
2211
2211
  };
2212
2212
  //#endregion
2213
- //#region ../../b4m-core/services/dist/websearch-ClW051tj.mjs
2213
+ //#region ../../b4m-core/services/dist/prompts-Df-9fQuz.mjs
2214
+ /**
2215
+ * Teaches the `b4m_cards` fence, appended to a web_search result ONLY when that search actually
2216
+ * returned images (see shouldIncludeImages). Delivering it with the results rather than in the system
2217
+ * prompt costs nothing on the turns that will never use it, and arrives exactly when it is actionable.
2218
+ *
2219
+ * The field names must stay in sync with the parser in
2220
+ * apps/client/app/components/Session/parseSearchResultCards.ts; the fence language is shared.
2221
+ */
2222
+ const WEB_SEARCH_CARDS_PROMPT = [
2223
+ "The results above include images, so illustrate your answer - do not wait to be asked. When you name specific",
2224
+ `things the user would want to SEE, emit a \`\`\`${SEARCH_RESULT_CARDS_LANGUAGE} fenced block inline in your reply, placed`,
2225
+ "exactly where the pictures belong - right after the sentence that introduces them, not at the end.",
2226
+ "The block is a single JSON object:",
2227
+ "",
2228
+ `\`\`\`${SEARCH_RESULT_CARDS_LANGUAGE}`,
2229
+ "{\"cards\":[{\"name\":\"Orient Bambino\",\"note\":\"Your own description of this thing, in your voice.\",",
2230
+ "\"meta\":\"~$200\",\"url\":\"https://orientwatch.co/bambino\",\"images\":[",
2231
+ "{\"url\":\"https://example.com/a.jpg\",\"source\":\"orientwatch.co\"},",
2232
+ "{\"url\":\"https://example.com/b.jpg\",\"source\":\"jomashop\"}]}]}",
2233
+ "```",
2234
+ "",
2235
+ "Rules: `name` and at least one `images` entry are required. Every image `url` must be copied verbatim",
2236
+ "from an `image:` or Images line above, never invented or guessed, and `source` is that entry's own",
2237
+ "`source`/hostname, so each picture is attributed. Prefer the \"Images found for this search\" pool:",
2238
+ "those carry their own page and publisher. `note` is YOUR prose about the thing, not the",
2239
+ "search snippet. `meta` is a short footer such as a price or key spec. `url` is where the card links.",
2240
+ "Two to six cards is the useful range. Keep writing normally around the block - it replaces neither",
2241
+ "your explanation nor your citations. Omit the block only if the results genuinely have no images",
2242
+ "worth showing; never tell the user you could show pictures if they asked - just show them."
2243
+ ].join("\n");
2244
+ /**
2245
+ * Teaches the `b4m_map` fence, appended to a web_search result ONLY when that search returned places
2246
+ * with provider coordinates (see shouldIncludePlaces). The fence names places by id alone, so the
2247
+ * model never writes a coordinate - the client resolves each id against the stored place citables.
2248
+ *
2249
+ * The field names must stay in sync with parseLocationMapFence in
2250
+ * b4m-core/common/src/constants/locationMap.ts.
2251
+ */
2252
+ const WEB_SEARCH_MAP_PROMPT = [
2253
+ "The results above include places with map locations, so show them on a map - do not wait to be asked.",
2254
+ `Emit one \`\`\`${LOCATION_MAP_LANGUAGE} fenced block inline in your reply, placed where the map belongs - right`,
2255
+ "after the sentence that introduces the places. The block is a single JSON object:",
2256
+ "",
2257
+ `\`\`\`${LOCATION_MAP_LANGUAGE}`,
2258
+ "{\"anchor\":{\"id\":\"<anchor id>\",\"name\":\"citizenM Copenhagen\",\"label\":\"Your hotel\"},",
2259
+ "\"places\":[{\"id\":\"<place id>\",\"name\":\"Barr\",\"note\":\"Your own one-line take on this place.\"}]}",
2260
+ "```",
2261
+ "",
2262
+ "Rules: every `id` must be copied verbatim from an `id:` line above - never invent one, and never write",
2263
+ "coordinates; the map locates each place from its id. `name` is the place name as listed. `note` is",
2264
+ "YOUR short line about why it fits the request. Include `anchor` only when an \"Anchor location\" is listed",
2265
+ "above, with a short `label` for what it is to the user (\"Your hotel\", \"Your office\"). List the places",
2266
+ "worth recommending, three to ten. Keep writing normally around the block - it does not replace your",
2267
+ "explanation or citations. Omit it only if none of the places actually answer the question."
2268
+ ].join("\n");
2269
+ //#endregion
2270
+ //#region ../../b4m-core/services/dist/websearch-BpzP6-Zz.mjs
2214
2271
  /**
2215
2272
  * Per-process cache for web-search results. Keyed on normalized query + result count, with a
2216
2273
  * short TTL so repeated/similar searches within a turn or across back-to-back turns reuse the
@@ -2788,61 +2845,6 @@ async function resolveWebSearchProviders(adapters) {
2788
2845
  if (lead.name === "serpapi") return [lead, settings.searxngUrl ? createSearxngProvider(settings.searxngUrl) : null];
2789
2846
  return [lead, settings.serperKey ? createSerpApiProvider(adapters) : null];
2790
2847
  }
2791
- /**
2792
- * Teaches the `b4m_cards` fence, appended to a web_search result ONLY when that search actually
2793
- * returned images (see shouldIncludeImages). Delivering it with the results rather than in the system
2794
- * prompt costs nothing on the turns that will never use it, and arrives exactly when it is actionable.
2795
- *
2796
- * The field names must stay in sync with the parser in
2797
- * apps/client/app/components/Session/parseSearchResultCards.ts; the fence language is shared.
2798
- */
2799
- const WEB_SEARCH_CARDS_PROMPT = [
2800
- "The results above include images, so illustrate your answer - do not wait to be asked. When you name specific",
2801
- `things the user would want to SEE, emit a \`\`\`${SEARCH_RESULT_CARDS_LANGUAGE} fenced block inline in your reply, placed`,
2802
- "exactly where the pictures belong - right after the sentence that introduces them, not at the end.",
2803
- "The block is a single JSON object:",
2804
- "",
2805
- `\`\`\`${SEARCH_RESULT_CARDS_LANGUAGE}`,
2806
- "{\"cards\":[{\"name\":\"Orient Bambino\",\"note\":\"Your own description of this thing, in your voice.\",",
2807
- "\"meta\":\"~$200\",\"url\":\"https://orientwatch.co/bambino\",\"images\":[",
2808
- "{\"url\":\"https://example.com/a.jpg\",\"source\":\"orientwatch.co\"},",
2809
- "{\"url\":\"https://example.com/b.jpg\",\"source\":\"jomashop\"}]}]}",
2810
- "```",
2811
- "",
2812
- "Rules: `name` and at least one `images` entry are required. Every image `url` must be copied verbatim",
2813
- "from an `image:` or Images line above, never invented or guessed, and `source` is that entry's own",
2814
- "`source`/hostname, so each picture is attributed. Prefer the \"Images found for this search\" pool:",
2815
- "those carry their own page and publisher. `note` is YOUR prose about the thing, not the",
2816
- "search snippet. `meta` is a short footer such as a price or key spec. `url` is where the card links.",
2817
- "Two to six cards is the useful range. Keep writing normally around the block - it replaces neither",
2818
- "your explanation nor your citations. Omit the block only if the results genuinely have no images",
2819
- "worth showing; never tell the user you could show pictures if they asked - just show them."
2820
- ].join("\n");
2821
- /**
2822
- * Teaches the `b4m_map` fence, appended to a web_search result ONLY when that search returned places
2823
- * with provider coordinates (see shouldIncludePlaces). The fence names places by id alone, so the
2824
- * model never writes a coordinate - the client resolves each id against the stored place citables.
2825
- *
2826
- * The field names must stay in sync with parseLocationMapFence in
2827
- * b4m-core/common/src/constants/locationMap.ts.
2828
- */
2829
- const WEB_SEARCH_MAP_PROMPT = [
2830
- "The results above include places with map locations, so show them on a map - do not wait to be asked.",
2831
- `Emit one \`\`\`${LOCATION_MAP_LANGUAGE} fenced block inline in your reply, placed where the map belongs - right`,
2832
- "after the sentence that introduces the places. The block is a single JSON object:",
2833
- "",
2834
- `\`\`\`${LOCATION_MAP_LANGUAGE}`,
2835
- "{\"anchor\":{\"id\":\"<anchor id>\",\"name\":\"citizenM Copenhagen\",\"label\":\"Your hotel\"},",
2836
- "\"places\":[{\"id\":\"<place id>\",\"name\":\"Barr\",\"note\":\"Your own one-line take on this place.\"}]}",
2837
- "```",
2838
- "",
2839
- "Rules: every `id` must be copied verbatim from an `id:` line above - never invent one, and never write",
2840
- "coordinates; the map locates each place from its id. `name` is the place name as listed. `note` is",
2841
- "YOUR short line about why it fits the request. Include `anchor` only when an \"Anchor location\" is listed",
2842
- "above, with a short `label` for what it is to the user (\"Your hotel\", \"Your office\"). List the places",
2843
- "worth recommending, three to ten. Keep writing normally around the block - it does not replace your",
2844
- "explanation or citations. Omit it only if none of the places actually answer the question."
2845
- ].join("\n");
2846
2848
  function safeHostname(url) {
2847
2849
  try {
2848
2850
  return new URL(url).hostname;
@@ -3340,7 +3342,7 @@ async function getModelForWrite(user, modelId, deps) {
3340
3342
  return isModelOwner(model, user) ? model : null;
3341
3343
  }
3342
3344
  //#endregion
3343
- //#region ../../b4m-core/services/dist/toolGenerators-KiYiCRXT.mjs
3345
+ //#region ../../b4m-core/services/dist/toolGenerators-DT6OlvUe.mjs
3344
3346
  const diceRoll = async (parameters) => {
3345
3347
  if (!parameters?.sides || !parameters?.times) throw new Error("Tool dice roll: Missing required parameters");
3346
3348
  return sum(times(parameters.times, () => random(1, parameters.sides))).toString();
@@ -3930,27 +3932,6 @@ const promptEnhancementTool = {
3930
3932
  }
3931
3933
  })
3932
3934
  };
3933
- /**
3934
- * Is this value shaped like something Mongoose can cast to an `_id`?
3935
- *
3936
- * Tool arguments are composed by the model out of conversation text and reach us as unvalidated
3937
- * JSON, so an id parameter routinely holds something that is not an id at all - a filename token,
3938
- * an arXiv number, a bare integer. Mongoose casts `_id` and throws a CastError on those, which a
3939
- * generic catch upstream then reports as an outage rather than the bad argument it is (#2530).
3940
- * Call this before handing a model-supplied id to `findById` and answer a false the same way the
3941
- * surface answers a genuinely missing row.
3942
- *
3943
- * `isObjectIdOrHexString`, not `isValidObjectId`: the latter also accepts a number and casts it to
3944
- * a fabricated id, and a model emitting `{"file_id": 12}` gives us exactly that despite the
3945
- * `string` type. Same choice, same reason, as `usableObjectIds` in @bike4mind/db-core, which is
3946
- * the array-shaped version of this check.
3947
- *
3948
- * NOT usable for artifact ids (`artifact_<...>`), which are matched on a string `id` field rather
3949
- * than `_id` - see `createArtifactId` in @bike4mind/common.
3950
- */
3951
- function isObjectIdShaped$1(id) {
3952
- return isObjectIdOrHexString(id);
3953
- }
3954
3935
  let _showUserQuestion = null;
3955
3936
  /**
3956
3937
  * Inject the CLI callback that displays the question UI.
@@ -4070,7 +4051,7 @@ const askUserQuestionTool = {
4070
4051
  * re-export them without pulling the full tool graph. `index.ts` re-exports them
4071
4052
  * so the server barrel's public API is unchanged.
4072
4053
  */
4073
- const generateTools = (userId, user, logger, { db, retrievalFilter, kbScope, inlinedAttachmentIds, fullyInlinedAttachmentIds, suppressLakeArms, sessionRetrievalTags, sessionReaderConsentDatalakeTags, sessionLakeScopeExplicit, sessionPreauthorizedLakeIds, organizationId, apiKeyId, questId, getAbortSignal }, storage, imageGenerateStorage, statusUpdate, onStart, onFinish, llm, config, model, imageProcessorLambdaName, tools, allowedDirectories, entitlementKeys = [], sessionId, codeMinifier, availableModels, onToolLlmUsage) => {
4054
+ const generateTools = (userId, user, logger, { db, retrievalFilter, kbScope, inlinedAttachmentIds, attachedFileIds, fullyInlinedAttachmentIds, suppressLakeArms, sessionRetrievalTags, sessionReaderConsentDatalakeTags, sessionLakeScopeExplicit, sessionIncludeLibraryFiles, sessionPreauthorizedLakeIds, organizationId, apiKeyId, questId, getAbortSignal }, storage, imageGenerateStorage, statusUpdate, onStart, onFinish, llm, config, model, imageProcessorLambdaName, tools, allowedDirectories, entitlementKeys = [], sessionId, codeMinifier, availableModels, onToolLlmUsage) => {
4074
4055
  const context = {
4075
4056
  userId,
4076
4057
  user,
@@ -4091,11 +4072,13 @@ const generateTools = (userId, user, logger, { db, retrievalFilter, kbScope, inl
4091
4072
  retrievalFilter,
4092
4073
  kbScope,
4093
4074
  inlinedAttachmentIds,
4075
+ attachedFileIds,
4094
4076
  fullyInlinedAttachmentIds,
4095
4077
  suppressLakeArms,
4096
4078
  sessionRetrievalTags,
4097
4079
  sessionReaderConsentDatalakeTags,
4098
4080
  sessionLakeScopeExplicit,
4081
+ sessionIncludeLibraryFiles,
4099
4082
  sessionPreauthorizedLakeIds,
4100
4083
  organizationId,
4101
4084
  apiKeyId,
@@ -4116,16 +4099,34 @@ const generateTools = (userId, user, logger, { db, retrievalFilter, kbScope, inl
4116
4099
  }, {});
4117
4100
  };
4118
4101
  /**
4102
+ * MCP tool args only the confirm-button handlers may set. Those handlers call the MCP host
4103
+ * directly, never through these generators, so a model-originated call carrying one is forged.
4104
+ * `_executeFromButton` mirrors the button-only keys of `confirmationParams` in
4105
+ * b4m-core/mcp/src/shared/schemas.ts (pinned by github/__tests__/helpers/schemas.test.ts there);
4106
+ * `_confirmToken` is the preview token tools return.
4107
+ */
4108
+ const SERVER_ONLY_MCP_ARG_KEYS = ["_executeFromButton", "_confirmToken"];
4109
+ const isPlainObject$1 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
4110
+ function stripServerOnlyArgs(args) {
4111
+ if (!isPlainObject$1(args)) return args;
4112
+ return Object.fromEntries(Object.entries(args).filter(([key]) => !SERVER_ONLY_MCP_ARG_KEYS.includes(key)));
4113
+ }
4114
+ /**
4119
4115
  * Normalize MCP tool parameters for OpenAI compatibility.
4120
4116
  * OpenAI requires 'properties' on object schemas - MCP tools like current_user
4121
4117
  * return { type: 'object' } without it, causing 400 errors.
4122
4118
  */
4123
4119
  function normalizeToolParameters(rest) {
4124
4120
  const rawParameters = rest?.input_schema ?? rest?.inputSchema ?? rest?.parameters;
4125
- if (rawParameters && typeof rawParameters === "object") return {
4126
- ...rawParameters,
4127
- properties: rawParameters.properties ?? {}
4128
- };
4121
+ if (isPlainObject$1(rawParameters)) {
4122
+ const properties = isPlainObject$1(rawParameters.properties) ? stripServerOnlyArgs(rawParameters.properties) : rawParameters.properties ?? {};
4123
+ const required = Array.isArray(rawParameters.required) ? rawParameters.required.filter((key) => typeof key !== "string" || !SERVER_ONLY_MCP_ARG_KEYS.includes(key)) : rawParameters.required;
4124
+ return {
4125
+ ...rawParameters,
4126
+ properties,
4127
+ ...required !== void 0 ? { required } : {}
4128
+ };
4129
+ }
4129
4130
  return {
4130
4131
  type: "object",
4131
4132
  properties: {},
@@ -4158,7 +4159,7 @@ const generateMcpTools = async (mcpData) => {
4158
4159
  toolFn: async (args) => {
4159
4160
  Logger.debug(`Calling ${originalToolName} tool via ${mcpData.serverName}`, args);
4160
4161
  try {
4161
- const toolResult = await mcpData.callTool(originalToolName, args);
4162
+ const toolResult = await mcpData.callTool(originalToolName, stripServerOnlyArgs(args));
4162
4163
  const contentBlocks = toolResult?.content;
4163
4164
  if (Array.isArray(contentBlocks) && contentBlocks.length > 0) {
4164
4165
  const normalized = contentBlocks.map((entry) => {
@@ -4210,7 +4211,7 @@ const generateMcpToolsFromCache = (serverName, cachedTools, callTool) => {
4210
4211
  toolFn: async (args) => {
4211
4212
  Logger.debug(`Calling ${originalToolName} tool via ${serverName}`, args);
4212
4213
  try {
4213
- const toolResult = await callTool(originalToolName, args);
4214
+ const toolResult = await callTool(originalToolName, stripServerOnlyArgs(args));
4214
4215
  const contentBlocks = toolResult?.content;
4215
4216
  if (Array.isArray(contentBlocks) && contentBlocks.length > 0) {
4216
4217
  const normalized = contentBlocks.map((entry) => {
@@ -4247,6 +4248,29 @@ const generateMcpToolsFromCache = (serverName, cachedTools, callTool) => {
4247
4248
  return result;
4248
4249
  };
4249
4250
  //#endregion
4251
+ //#region ../../b4m-core/services/dist/objectId-DTad97PS.mjs
4252
+ /**
4253
+ * Is this value shaped like something Mongoose can cast to an `_id`?
4254
+ *
4255
+ * Tool arguments are composed by the model out of conversation text and reach us as unvalidated
4256
+ * JSON, so an id parameter routinely holds something that is not an id at all - a filename token,
4257
+ * an arXiv number, a bare integer. Mongoose casts `_id` and throws a CastError on those, which a
4258
+ * generic catch upstream then reports as an outage rather than the bad argument it is (#2530).
4259
+ * Call this before handing a model-supplied id to `findById` and answer a false the same way the
4260
+ * surface answers a genuinely missing row.
4261
+ *
4262
+ * `isObjectIdOrHexString`, not `isValidObjectId`: the latter also accepts a number and casts it to
4263
+ * a fabricated id, and a model emitting `{"file_id": 12}` gives us exactly that despite the
4264
+ * `string` type. Same choice, same reason, as `usableObjectIds` in @bike4mind/db-core, which is
4265
+ * the array-shaped version of this check.
4266
+ *
4267
+ * NOT usable for artifact ids (`artifact_<...>`), which are matched on a string `id` field rather
4268
+ * than `_id` - see `createArtifactId` in @bike4mind/common.
4269
+ */
4270
+ function isObjectIdShaped(id) {
4271
+ return isObjectIdOrHexString(id);
4272
+ }
4273
+ //#endregion
4250
4274
  //#region ../../b4m-core/services/dist/llm/tools/cliTools.mjs
4251
4275
  /**
4252
4276
  * Validated fuzzy fallback for `edit_local_file` string matching.
@@ -5636,7 +5660,7 @@ const latticeAddEntityTool = {
5636
5660
  createdAt: /* @__PURE__ */ new Date(),
5637
5661
  updatedAt: /* @__PURE__ */ new Date()
5638
5662
  };
5639
- if (context.db.latticeModels && modelId && isObjectIdShaped$1(modelId)) try {
5663
+ if (context.db.latticeModels && modelId && isObjectIdShaped(modelId)) try {
5640
5664
  const model = await getModelForWrite({
5641
5665
  id: context.userId,
5642
5666
  organizationId: context.user?.organizationId
@@ -5780,7 +5804,7 @@ const latticeSetValueTool = {
5780
5804
  else if (rawValue.toLowerCase() === "true") value = true;
5781
5805
  else if (rawValue.toLowerCase() === "false") value = false;
5782
5806
  const entityId = toEntityId(entityName);
5783
- if (context.db.latticeModels && modelId && isObjectIdShaped$1(modelId)) try {
5807
+ if (context.db.latticeModels && modelId && isObjectIdShaped(modelId)) try {
5784
5808
  const model = await getModelForWrite({
5785
5809
  id: context.userId,
5786
5810
  organizationId: context.user?.organizationId
@@ -5925,7 +5949,7 @@ const latticeCreateRuleTool = {
5925
5949
  };
5926
5950
  const outputEntityId = parsedRule.outputEntity.toLowerCase().replace(/\s+/g, "_");
5927
5951
  let entityCreatedMessage = "";
5928
- if (context.db.latticeModels && modelId && isObjectIdShaped$1(modelId)) try {
5952
+ if (context.db.latticeModels && modelId && isObjectIdShaped(modelId)) try {
5929
5953
  const model = await getModelForWrite({
5930
5954
  id: context.userId,
5931
5955
  organizationId: context.user?.organizationId
@@ -9254,16 +9278,24 @@ function leadingProgram(args) {
9254
9278
  return index < args.length ? programName(args[index]) : "";
9255
9279
  }
9256
9280
  /**
9257
- * True if any `>`/`>>` redirection targets a raw block device. Covers the common
9258
- * host shapes: SCSI/SATA (`sd[a-z]`), NVMe, Xen (`xvd[a-z]`, default on many EC2
9259
- * AMIs), virtio (`vd[a-z]`), legacy IDE (`hd[a-z]`), device-mapper/LVM (`dm-N`,
9260
- * `mapper/`), software RAID (`md N`), eMMC/SD (`mmcblkN`), loop, and macOS `diskN`.
9281
+ * True if any write redirection (`>`, `>>`, `>|`, `&>`, `&>>`, `<>`) targets a raw block
9282
+ * device. Covers the common host shapes: SCSI/SATA (`sd[a-z]`), NVMe, Xen (`xvd[a-z]`,
9283
+ * default on many EC2 AMIs), virtio (`vd[a-z]`), legacy IDE (`hd[a-z]`), device-mapper/LVM
9284
+ * (`dm-N`, `mapper/`), software RAID (`md N`), eMMC/SD (`mmcblkN`), loop, and macOS `diskN`.
9261
9285
  */
9262
9286
  const BLOCK_DEVICE_RE = /^\/dev\/(sd[a-z]|nvme\d+n\d+|xvd[a-z]|vd[a-z]|hd[a-z]|dm-\d+|md\d+|mmcblk\d+|loop\d+|disk\d+|mapper\/)/;
9287
+ const WRITE_REDIRECT_OPS = /* @__PURE__ */ new Set([
9288
+ ">",
9289
+ ">>",
9290
+ ">|",
9291
+ "&>",
9292
+ "&>>",
9293
+ "<>"
9294
+ ]);
9263
9295
  function writesToBlockDevice(tokens) {
9264
9296
  for (let i = 0; i < tokens.length; i += 1) {
9265
9297
  const token = tokens[i];
9266
- if (isOperator(token) && (token.op === ">" || token.op === ">>")) {
9298
+ if (isOperator(token) && WRITE_REDIRECT_OPS.has(token.op)) {
9267
9299
  let targetIndex = i + 1;
9268
9300
  const following = tokens[targetIndex];
9269
9301
  if (isOperator(following) && following.op === "|") targetIndex += 1;
@@ -15890,6 +15922,10 @@ var StreamAccumulator = class {
15890
15922
  isEmpty() {
15891
15923
  return this.accumulatedText.trim().length === 0 && this.toolsUsed.length === 0;
15892
15924
  }
15925
+ /** The stop reason the stream reported, if any: its presence means the stream ended normally. */
15926
+ get finalStopReason() {
15927
+ return this.stopReason;
15928
+ }
15893
15929
  get accumulatedLength() {
15894
15930
  return this.accumulatedText.length;
15895
15931
  }
@@ -15935,6 +15971,19 @@ var EmptyCompletionError = class extends Error {
15935
15971
  this.name = "EmptyCompletionError";
15936
15972
  }
15937
15973
  };
15974
+ /**
15975
+ * Thrown when a stream finished normally (it carried a stop reason) but produced no reply,
15976
+ * e.g. a reasoning model that spent its whole output budget thinking: the server drops
15977
+ * reasoning text, so such a turn arrives blank. Not retried - the same request would
15978
+ * finish the same way and be billed again.
15979
+ */
15980
+ var NoReplyCompletionError = class extends Error {
15981
+ constructor(stopReason) {
15982
+ super(stopReason === "max_tokens" ? "The model used its whole output limit on reasoning and produced no reply. Raise the max tokens setting or ask a narrower question." : `The model finished (stop reason: ${stopReason}) without producing a reply.`);
15983
+ this.stopReason = stopReason;
15984
+ this.name = "NoReplyCompletionError";
15985
+ }
15986
+ };
15938
15987
  /** User-facing message when a transient network drop survives all retries. */
15939
15988
  const TRANSIENT_EXHAUSTED_MESSAGE = "The connection dropped mid-response (likely a network timeout during a long thinking step). It was retried automatically but kept failing - type \"continue\" to resume.";
15940
15989
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -15979,12 +16028,13 @@ async function runCompletion(transport, req, callback, policy, signal) {
15979
16028
  await accumulator.finalize(callback);
15980
16029
  return;
15981
16030
  }
16031
+ if (accumulator.finalStopReason) throw new NoReplyCompletionError(accumulator.finalStopReason);
15982
16032
  lastError = new EmptyCompletionError();
15983
16033
  logger.warn("[runCompletion] Stream produced no content; treating as retryable.");
15984
16034
  } catch (error) {
15985
16035
  lastError = error instanceof Error ? error : new Error(String(error));
15986
16036
  if (signal?.aborted) return;
15987
- if (!policy.isRetryable(lastError)) throw lastError;
16037
+ if (lastError instanceof NoReplyCompletionError || !policy.isRetryable(lastError)) throw lastError;
15988
16038
  logger.warn(`[runCompletion] Transient stream failure (attempt ${attempt + 1}): ${lastError.message}`);
15989
16039
  }
15990
16040
  if (attempt < policy.maxRetries && backoffMs > 0) await sleep(backoffMs * (attempt + 1));
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { n as logger, t as ConfigStore } from "./ConfigStore-DIs-K9oD.mjs";
3
- import { t as version } from "./package-Dl_F2Fah.mjs";
2
+ import { n as logger, t as ConfigStore } from "./ConfigStore-BygzEI6H.mjs";
3
+ import { t as version } from "./package-CRNnNXxq.mjs";
4
4
  import axios, { isAxiosError } from "axios";
5
5
  //#region src/auth/OAuthClient.ts
6
6
  /**