@bike4mind/cli 0.2.20-feat-web-fetch-tool.18049 → 0.2.20-feat-cli-smart-stream-logging.18065

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.
@@ -36,6 +36,11 @@ const argv = await yargs(hideBin(process.argv))
36
36
  description: 'Show debug logs in console',
37
37
  default: false,
38
38
  })
39
+ .option('debug-stream', {
40
+ type: 'boolean',
41
+ description: 'Ultra-verbose: log every SSE event (for debugging stream parser)',
42
+ default: false,
43
+ })
39
44
  .option('no-project-config', {
40
45
  type: 'boolean',
41
46
  description: 'Disable loading project-specific configuration (.bike4mind/)',
@@ -94,6 +99,11 @@ if (argv.verbose) {
94
99
  process.env.B4M_VERBOSE = '1';
95
100
  process.env.LOG_LEVEL = 'debug'; // Enable debug logs in core packages
96
101
  }
102
+ if (argv['debug-stream']) {
103
+ process.env.B4M_DEBUG_STREAM = '1';
104
+ process.env.B4M_VERBOSE = '1'; // Ultra-verbose implies verbose
105
+ process.env.LOG_LEVEL = 'debug';
106
+ }
97
107
  if (argv['no-project-config']) {
98
108
  process.env.B4M_NO_PROJECT_CONFIG = '1';
99
109
  }
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  CurationArtifactType
4
- } from "./chunk-T5NXFTZC.js";
4
+ } from "./chunk-DTPBHKQL.js";
5
5
 
6
6
  // ../../b4m-core/packages/services/dist/src/notebookCurationService/artifactExtractor.js
7
7
  var ARTIFACT_TAG_REGEX = /<artifact\s+(.*?)>([\s\S]*?)<\/artifact>/gi;
@@ -7,11 +7,11 @@ import {
7
7
  getSettingsMap,
8
8
  getSettingsValue,
9
9
  secureParameters
10
- } from "./chunk-VAFY6X3M.js";
10
+ } from "./chunk-EP4OGIQI.js";
11
11
  import {
12
12
  KnowledgeType,
13
13
  SupportedFabFileMimeTypes
14
- } from "./chunk-T5NXFTZC.js";
14
+ } from "./chunk-DTPBHKQL.js";
15
15
 
16
16
  // ../../b4m-core/packages/services/dist/src/fabFileService/create.js
17
17
  import { z } from "zod";
@@ -209,7 +209,6 @@ var b4mLLMTools = z3.enum([
209
209
  "edit_image",
210
210
  "weather_info",
211
211
  "web_search",
212
- "web_fetch",
213
212
  "math_evaluate",
214
213
  "mermaid_chart",
215
214
  "current_datetime",
@@ -4667,8 +4666,66 @@ function formatPageList(pagesResponse, siteUrl) {
4667
4666
  results
4668
4667
  };
4669
4668
  }
4669
+ function formatPageRestrictions(restrictionsResponse, pageId) {
4670
+ if (!restrictionsResponse || typeof restrictionsResponse !== "object" || restrictionsResponse.error) {
4671
+ return {
4672
+ pageId,
4673
+ hasRestrictions: false,
4674
+ restrictions: []
4675
+ };
4676
+ }
4677
+ const restrictions = [];
4678
+ const operationsMap = {};
4679
+ if (Array.isArray(restrictionsResponse.results)) {
4680
+ for (const item of restrictionsResponse.results) {
4681
+ if (item.operation && item.restrictions) {
4682
+ operationsMap[item.operation] = item;
4683
+ }
4684
+ }
4685
+ } else {
4686
+ for (const operation of RESTRICTION_OPERATIONS) {
4687
+ if (restrictionsResponse[operation]) {
4688
+ operationsMap[operation] = restrictionsResponse[operation];
4689
+ }
4690
+ }
4691
+ }
4692
+ for (const [operation, opData] of Object.entries(operationsMap)) {
4693
+ if (!opData?.restrictions)
4694
+ continue;
4695
+ const subjects = [];
4696
+ const users = opData.restrictions.user?.results || opData.restrictions.user || [];
4697
+ for (const user of users) {
4698
+ subjects.push({
4699
+ type: "user",
4700
+ identifier: user.accountId || user.username || user.key,
4701
+ displayName: user.displayName || user.publicName
4702
+ });
4703
+ }
4704
+ const groups = opData.restrictions.group?.results || opData.restrictions.group || [];
4705
+ for (const group of groups) {
4706
+ subjects.push({
4707
+ type: "group",
4708
+ identifier: group.name || group.id,
4709
+ displayName: group.name
4710
+ });
4711
+ }
4712
+ if (subjects.length > 0) {
4713
+ restrictions.push({
4714
+ operation,
4715
+ subjects
4716
+ });
4717
+ }
4718
+ }
4719
+ return {
4720
+ pageId,
4721
+ hasRestrictions: restrictions.length > 0,
4722
+ restrictions
4723
+ };
4724
+ }
4670
4725
 
4671
4726
  // ../../b4m-core/packages/common/dist/src/confluence/api.js
4727
+ var RESTRICTION_OPERATIONS = ["read", "update"];
4728
+ var RESTRICTION_SUBJECT_TYPES = ["user", "group"];
4672
4729
  var ConfluenceApi = class {
4673
4730
  config;
4674
4731
  constructor(config) {
@@ -4735,11 +4792,15 @@ var ConfluenceApi = class {
4735
4792
  if (response.status === 204) {
4736
4793
  return void 0;
4737
4794
  }
4795
+ const responseText = await response.text();
4796
+ if (!responseText || responseText.trim() === "") {
4797
+ return void 0;
4798
+ }
4738
4799
  const contentType = response.headers.get("content-type") || "";
4739
4800
  if (contentType.includes("application/json")) {
4740
- return await response.json();
4801
+ return JSON.parse(responseText);
4741
4802
  }
4742
- return await response.text();
4803
+ return responseText;
4743
4804
  }
4744
4805
  /**
4745
4806
  * Make v1 API request
@@ -4942,6 +5003,17 @@ var ConfluenceApi = class {
4942
5003
  }
4943
5004
  return formatSpaceResponse(results[0], this.config.siteUrl);
4944
5005
  }
5006
+ /**
5007
+ * Get details about a specific Confluence space by ID (v2 API)
5008
+ */
5009
+ async getSpaceById(params) {
5010
+ const { spaceId } = params;
5011
+ if (!spaceId) {
5012
+ throw new Error("spaceId is required to fetch space details.");
5013
+ }
5014
+ const space = await this.get(`/spaces/${spaceId}`);
5015
+ return formatSpaceResponse(space, this.config.siteUrl);
5016
+ }
4945
5017
  /**
4946
5018
  * Get child pages for a given page
4947
5019
  */
@@ -5177,6 +5249,161 @@ var ConfluenceApi = class {
5177
5249
  throw error;
5178
5250
  }
5179
5251
  }
5252
+ // ============================================================================
5253
+ // Page Restrictions API (v1)
5254
+ // ============================================================================
5255
+ /**
5256
+ * Validate that a user exists by account ID
5257
+ * @throws Error if user does not exist
5258
+ */
5259
+ async validateUserExists(accountId) {
5260
+ try {
5261
+ const user = await this.getV1("/user", { accountId });
5262
+ if (!user || user.error) {
5263
+ throw new Error(`User with account ID "${accountId}" not found.`);
5264
+ }
5265
+ return {
5266
+ accountId: user.accountId,
5267
+ displayName: user.displayName || user.publicName || accountId
5268
+ };
5269
+ } catch (error) {
5270
+ if (error instanceof Error && error.message.includes("not found")) {
5271
+ throw error;
5272
+ }
5273
+ throw new Error(`User with account ID "${accountId}" not found or access denied.`);
5274
+ }
5275
+ }
5276
+ /**
5277
+ * Validate that a group exists by name
5278
+ * @throws Error if group does not exist
5279
+ */
5280
+ async validateGroupExists(groupName) {
5281
+ try {
5282
+ const group = await this.getV1(`/group/${encodeURIComponent(groupName)}`);
5283
+ if (!group || group.error) {
5284
+ throw new Error(`Group "${groupName}" not found.`);
5285
+ }
5286
+ return { name: group.name || groupName };
5287
+ } catch (error) {
5288
+ if (error instanceof Error && error.message.includes("not found")) {
5289
+ throw error;
5290
+ }
5291
+ throw new Error(`Group "${groupName}" not found or access denied.`);
5292
+ }
5293
+ }
5294
+ /**
5295
+ * Get current restrictions for a Confluence page
5296
+ */
5297
+ async getPageRestrictions(params) {
5298
+ const { pageId } = params;
5299
+ if (!pageId) {
5300
+ throw new Error("pageId is required to get page restrictions.");
5301
+ }
5302
+ const result = await this.getV1(`/content/${pageId}/restriction`, {
5303
+ expand: "restrictions.user,restrictions.group"
5304
+ });
5305
+ return formatPageRestrictions(result, pageId);
5306
+ }
5307
+ /**
5308
+ * Add a restriction to a Confluence page
5309
+ * Validates that the user/group exists before applying the restriction.
5310
+ * Automatically includes the current user in restrictions to prevent self-lockout.
5311
+ */
5312
+ async addPageRestriction(params) {
5313
+ const { pageId, operation, restrictionType, subject, skipValidation = false } = params;
5314
+ if (!pageId || !operation || !restrictionType || !subject) {
5315
+ throw new Error("pageId, operation, restrictionType, and subject are required to add a page restriction.");
5316
+ }
5317
+ let subjectDisplayName;
5318
+ if (!skipValidation) {
5319
+ if (restrictionType === "user") {
5320
+ const userInfo = await this.validateUserExists(subject);
5321
+ subjectDisplayName = userInfo.displayName;
5322
+ } else {
5323
+ const groupInfo = await this.validateGroupExists(subject);
5324
+ subjectDisplayName = groupInfo.name;
5325
+ }
5326
+ }
5327
+ const currentUser = await this.getCurrentUser();
5328
+ const currentUserAccountId = currentUser.accountId;
5329
+ if (!currentUserAccountId) {
5330
+ throw new Error("Unable to determine current user account ID. Cannot add restrictions safely.");
5331
+ }
5332
+ const userList = [];
5333
+ userList.push({ accountId: currentUserAccountId });
5334
+ if (restrictionType === "user" && subject !== currentUserAccountId) {
5335
+ userList.push({ accountId: subject });
5336
+ }
5337
+ const restrictionPayload = [
5338
+ {
5339
+ operation,
5340
+ restrictions: {
5341
+ user: userList,
5342
+ ...restrictionType === "group" ? { group: [{ name: subject }] } : {}
5343
+ }
5344
+ }
5345
+ ];
5346
+ await this.postV1(`/content/${pageId}/restriction`, restrictionPayload);
5347
+ return {
5348
+ success: true,
5349
+ pageId,
5350
+ operation,
5351
+ restrictionType,
5352
+ subject,
5353
+ subjectDisplayName
5354
+ };
5355
+ }
5356
+ /**
5357
+ * Remove a restriction from a Confluence page
5358
+ * First checks if the restriction exists to avoid 404 errors
5359
+ */
5360
+ async removePageRestriction(params) {
5361
+ const { pageId, operation, restrictionType, subject } = params;
5362
+ if (!pageId || !operation || !restrictionType || !subject) {
5363
+ throw new Error("pageId, operation, restrictionType, and subject are required to remove a page restriction.");
5364
+ }
5365
+ const currentRestrictions = await this.getPageRestrictions({ pageId });
5366
+ const operationRestriction = currentRestrictions.restrictions.find((r) => r.operation === operation);
5367
+ if (!operationRestriction) {
5368
+ return {
5369
+ success: true,
5370
+ pageId,
5371
+ operation,
5372
+ restrictionType,
5373
+ subject,
5374
+ skipped: true,
5375
+ message: `No ${operation} restrictions exist on this page.`
5376
+ };
5377
+ }
5378
+ const subjectExists = operationRestriction.subjects.some((s) => s.type === restrictionType && s.identifier === subject);
5379
+ if (!subjectExists) {
5380
+ return {
5381
+ success: true,
5382
+ pageId,
5383
+ operation,
5384
+ restrictionType,
5385
+ subject,
5386
+ skipped: true,
5387
+ message: `Restriction for ${restrictionType} "${subject}" on ${operation} operation does not exist.`
5388
+ };
5389
+ }
5390
+ let deletePath;
5391
+ let deleteQuery;
5392
+ if (restrictionType === "user") {
5393
+ deletePath = `/content/${pageId}/restriction/byOperation/${operation}/user`;
5394
+ deleteQuery = { accountId: subject };
5395
+ } else {
5396
+ deletePath = `/content/${pageId}/restriction/byOperation/${operation}/group/${encodeURIComponent(subject)}`;
5397
+ }
5398
+ await this.request("DELETE", deletePath, { query: deleteQuery, useV1: true });
5399
+ return {
5400
+ success: true,
5401
+ pageId,
5402
+ operation,
5403
+ restrictionType,
5404
+ subject
5405
+ };
5406
+ }
5180
5407
  };
5181
5408
 
5182
5409
  // ../../b4m-core/packages/common/dist/src/jira/format.js
@@ -6836,6 +7063,8 @@ export {
6836
7063
  extractSnippetMeta,
6837
7064
  searchSchema,
6838
7065
  LinkedInApi,
7066
+ RESTRICTION_OPERATIONS,
7067
+ RESTRICTION_SUBJECT_TYPES,
6839
7068
  ConfluenceApi,
6840
7069
  formatIssueResponse,
6841
7070
  formatProjectResponse,
@@ -13,7 +13,7 @@ import {
13
13
  dayjsConfig_default,
14
14
  extractSnippetMeta,
15
15
  settingsMap
16
- } from "./chunk-T5NXFTZC.js";
16
+ } from "./chunk-DTPBHKQL.js";
17
17
  import {
18
18
  Logger
19
19
  } from "./chunk-OCYRD7D6.js";
@@ -205,7 +205,8 @@ function obfuscateApiKey(apiKey) {
205
205
  }
206
206
 
207
207
  // ../../b4m-core/packages/utils/dist/src/llm/anthropicBackend.js
208
- import { Anthropic } from "@anthropic-ai/sdk";
208
+ import { Anthropic, RateLimitError } from "@anthropic-ai/sdk";
209
+ import { CloudWatchClient, PutMetricDataCommand, StandardUnit } from "@aws-sdk/client-cloudwatch";
209
210
 
210
211
  // ../../b4m-core/packages/utils/dist/src/llm/backend.js
211
212
  var DEFAULT_MAX_TOOL_CALLS = 10;
@@ -1952,9 +1953,44 @@ var AnthropicBackend = class {
1952
1953
  isThinkingEnabled = false;
1953
1954
  // Track if thinking is enabled for current request
1954
1955
  constructor(apiKey, logger) {
1955
- this._api = new Anthropic({ apiKey });
1956
+ this._api = new Anthropic({ apiKey, maxRetries: 5 });
1956
1957
  this.logger = logger ?? new Logger();
1957
1958
  }
1959
+ /**
1960
+ * Emit a CloudWatch metric when a rate limit error occurs.
1961
+ * This helps monitor and alert on Anthropic API rate limiting issues.
1962
+ */
1963
+ async emitRateLimitMetric(model) {
1964
+ try {
1965
+ const client = new CloudWatchClient({
1966
+ region: process.env.AWS_REGION || "us-east-2"
1967
+ });
1968
+ const command = new PutMetricDataCommand({
1969
+ Namespace: "Lumina5/AnthropicAPI",
1970
+ MetricData: [
1971
+ {
1972
+ MetricName: "RateLimitError",
1973
+ Value: 1,
1974
+ Unit: StandardUnit.Count,
1975
+ Timestamp: /* @__PURE__ */ new Date(),
1976
+ Dimensions: [
1977
+ { Name: "Model", Value: model },
1978
+ // SEED_STAGE_NAME is set via DEFAULT_LAMBDA_ENVIRONMENT in infra/constants.ts
1979
+ // and maps to $app.stage. The alarm in infra/alarms.ts filters by Stage dimension.
1980
+ // Fallback to 'local' for non-Lambda contexts (local dev, tests).
1981
+ { Name: "Stage", Value: process.env.SEED_STAGE_NAME || "local" }
1982
+ ]
1983
+ }
1984
+ ]
1985
+ });
1986
+ await client.send(command);
1987
+ this.logger.info("[AnthropicBackend] Emitted RateLimitError metric to CloudWatch", { model });
1988
+ } catch (metricsError) {
1989
+ this.logger.warn("[AnthropicBackend] Failed to emit CloudWatch metric", {
1990
+ error: metricsError instanceof Error ? metricsError.message : String(metricsError)
1991
+ });
1992
+ }
1993
+ }
1958
1994
  /**
1959
1995
  * Get thinking blocks from the last assistant content.
1960
1996
  * Filters to only include thinking/redacted_thinking blocks.
@@ -2368,6 +2404,13 @@ var AnthropicBackend = class {
2368
2404
  }
2369
2405
  const hasToolsToExecute = func.some((f) => f && f.name);
2370
2406
  if (!hasToolsToExecute) {
2407
+ if (options.tools && options.tools.length > 0) {
2408
+ const mcpToolCount = options.tools.filter((t) => t._isMcpTool).length;
2409
+ const hasContent = collectedContent.some((c) => c?.type === "text" && c?.text?.trim());
2410
+ if (!hasContent && mcpToolCount > 0) {
2411
+ this.logger.warn(`\u26A0\uFE0F [AnthropicBackend] Model returned empty content with ${mcpToolCount} MCP tools available. Total tools: ${options.tools.length}. This may indicate tool overload or model confusion.`);
2412
+ }
2413
+ }
2371
2414
  await cb([], { toolsUsed });
2372
2415
  }
2373
2416
  resolve();
@@ -2523,7 +2566,17 @@ var AnthropicBackend = class {
2523
2566
  }
2524
2567
  }
2525
2568
  } catch (error) {
2526
- this.logger.debug("Error in complete:", error);
2569
+ if (error instanceof RateLimitError) {
2570
+ this.logger.error("[AnthropicBackend] Rate limit error after all retries exhausted", {
2571
+ model,
2572
+ status: error.status,
2573
+ message: error.message
2574
+ });
2575
+ this.emitRateLimitMetric(model).catch(() => {
2576
+ });
2577
+ } else {
2578
+ this.logger.debug("Error in complete:", error);
2579
+ }
2527
2580
  throw error;
2528
2581
  }
2529
2582
  }
@@ -6,12 +6,12 @@ import {
6
6
  getSettingsByNames,
7
7
  obfuscateApiKey,
8
8
  secureParameters
9
- } from "./chunk-VAFY6X3M.js";
9
+ } from "./chunk-EP4OGIQI.js";
10
10
  import {
11
11
  ApiKeyType,
12
12
  MementoTier,
13
13
  isSupportedEmbeddingModel
14
- } from "./chunk-T5NXFTZC.js";
14
+ } from "./chunk-DTPBHKQL.js";
15
15
 
16
16
  // ../../b4m-core/packages/services/dist/src/apiKeyService/get.js
17
17
  import { z } from "zod";
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  BadRequestError,
4
4
  secureParameters
5
- } from "./chunk-VAFY6X3M.js";
5
+ } from "./chunk-EP4OGIQI.js";
6
6
  import {
7
7
  CompletionApiUsageTransaction,
8
8
  GenericCreditDeductTransaction,
@@ -11,7 +11,7 @@ import {
11
11
  RealtimeVoiceUsageTransaction,
12
12
  TextGenerationUsageTransaction,
13
13
  TransferCreditTransaction
14
- } from "./chunk-T5NXFTZC.js";
14
+ } from "./chunk-DTPBHKQL.js";
15
15
 
16
16
  // ../../b4m-core/packages/services/dist/src/creditService/subtractCredits.js
17
17
  import { z } from "zod";
@@ -2,9 +2,9 @@
2
2
  import {
3
3
  createFabFile,
4
4
  createFabFileSchema
5
- } from "./chunk-V2UQMO2S.js";
6
- import "./chunk-VAFY6X3M.js";
7
- import "./chunk-T5NXFTZC.js";
5
+ } from "./chunk-AKCBPXLX.js";
6
+ import "./chunk-EP4OGIQI.js";
7
+ import "./chunk-DTPBHKQL.js";
8
8
  import "./chunk-OCYRD7D6.js";
9
9
  export {
10
10
  createFabFile,
package/dist/index.js CHANGED
@@ -4,12 +4,12 @@ import {
4
4
  getEffectiveApiKey,
5
5
  getOpenWeatherKey,
6
6
  getSerperKey
7
- } from "./chunk-NAZHGW73.js";
7
+ } from "./chunk-RRYX2K7W.js";
8
8
  import {
9
9
  ConfigStore
10
10
  } from "./chunk-VFQF2JIT.js";
11
- import "./chunk-W5U65OA2.js";
12
- import "./chunk-V2UQMO2S.js";
11
+ import "./chunk-XWI42ILN.js";
12
+ import "./chunk-AKCBPXLX.js";
13
13
  import {
14
14
  BFLImageService,
15
15
  BaseStorage,
@@ -21,7 +21,7 @@ import {
21
21
  OpenAIBackend,
22
22
  OpenAIImageService,
23
23
  XAIImageService
24
- } from "./chunk-VAFY6X3M.js";
24
+ } from "./chunk-EP4OGIQI.js";
25
25
  import {
26
26
  AiEvents,
27
27
  ApiKeyEvents,
@@ -75,7 +75,7 @@ import {
75
75
  XAI_IMAGE_MODELS,
76
76
  b4mLLMTools,
77
77
  getMcpProviderMetadata
78
- } from "./chunk-T5NXFTZC.js";
78
+ } from "./chunk-DTPBHKQL.js";
79
79
  import {
80
80
  Logger
81
81
  } from "./chunk-OCYRD7D6.js";
@@ -5608,7 +5608,7 @@ var webSearchTool = {
5608
5608
  },
5609
5609
  toolSchema: {
5610
5610
  name: "web_search",
5611
- description: "Search the web using Google Search API to FIND pages about a topic. Use this when you need to find URLs or search for information. DO NOT use this if the user provides a specific URL - use web_fetch instead to read the full content.",
5611
+ description: "Search the web using Google Search API",
5612
5612
  parameters: {
5613
5613
  type: "object",
5614
5614
  properties: {
@@ -5629,109 +5629,6 @@ var webSearchTool = {
5629
5629
  })
5630
5630
  };
5631
5631
 
5632
- // ../../b4m-core/packages/services/dist/src/llm/tools/implementation/webfetch/index.js
5633
- import FirecrawlApp2 from "@mendable/firecrawl-js";
5634
- async function firecrawlFetch(adapters, url) {
5635
- const urlPattern = /^https?:\/\/.+/i;
5636
- if (!urlPattern.test(url)) {
5637
- throw new Error(`Invalid URL format: ${url}. URL must start with http:// or https://`);
5638
- }
5639
- const apiKeySetting = await adapters.db.adminSettings.findBySettingName("FirecrawlApiKey");
5640
- if (!apiKeySetting?.settingValue) {
5641
- console.error("\u274C WebFetch Tool: Firecrawl API key not configured");
5642
- throw new Error("Firecrawl API key not configured");
5643
- }
5644
- const app = new FirecrawlApp2({ apiKey: apiKeySetting.settingValue });
5645
- const controller = new AbortController();
5646
- const timeoutId = setTimeout(() => controller.abort(), 6e4);
5647
- try {
5648
- console.log("\u{1F4E5} WebFetch Tool: Scraping URL with Firecrawl...");
5649
- const result = await app.scrapeUrl(url, {
5650
- formats: ["markdown"],
5651
- actions: [
5652
- {
5653
- type: "wait",
5654
- milliseconds: 1e3
5655
- // Wait for JavaScript to execute
5656
- }
5657
- ]
5658
- });
5659
- clearTimeout(timeoutId);
5660
- if (!result || result.error) {
5661
- const errorMessage = result?.error || "Unknown error";
5662
- console.error("\u274C WebFetch Tool: Firecrawl error:", errorMessage);
5663
- throw new Error(`Failed to fetch content from URL: ${errorMessage}`);
5664
- }
5665
- if (!("markdown" in result) || !result.markdown) {
5666
- console.error("\u274C WebFetch Tool: No markdown content returned");
5667
- throw new Error("No content could be extracted from the URL");
5668
- }
5669
- const markdown = result.markdown.slice(0, 5e4);
5670
- console.log(`\u{1F4C4} WebFetch Tool: Extracted ${markdown.length} characters of content`);
5671
- return {
5672
- markdown,
5673
- title: result.metadata?.title
5674
- };
5675
- } catch (error) {
5676
- clearTimeout(timeoutId);
5677
- throw error;
5678
- }
5679
- }
5680
- var webFetchTool = {
5681
- name: "web_fetch",
5682
- implementation: (context) => ({
5683
- toolFn: async (value) => {
5684
- const params = value;
5685
- console.log("\u{1F310} WebFetch Tool: Starting fetch for URL:", params.url);
5686
- try {
5687
- await context.statusUpdate({}, `Fetching content from ${params.url}...`);
5688
- const result = await firecrawlFetch({ db: context.db }, params.url);
5689
- const citable = {
5690
- id: params.url,
5691
- type: "web_url",
5692
- title: result.title || params.url,
5693
- url: params.url,
5694
- description: result.markdown.slice(0, 200) + (result.markdown.length > 200 ? "..." : ""),
5695
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5696
- status: "complete",
5697
- metadata: {
5698
- sourceSystem: "web_fetch",
5699
- contentLength: result.markdown.length
5700
- }
5701
- };
5702
- await context.statusUpdate({
5703
- promptMeta: {
5704
- citables: [citable]
5705
- }
5706
- }, "WebFetch completed successfully");
5707
- console.log(`\u{1F4DA} WebFetch Tool: Stored citable source for ${params.url}`);
5708
- return result.markdown;
5709
- } catch (error) {
5710
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
5711
- console.error("\u274C WebFetch Tool: Error during fetch:", error);
5712
- await context.statusUpdate({}, `WebFetch failed: ${errorMessage}`);
5713
- context.logger.error("WebFetch error:", error);
5714
- throw error;
5715
- }
5716
- },
5717
- toolSchema: {
5718
- name: "web_fetch",
5719
- description: 'Fetches and reads the FULL CONTENT of a specific URL that the user provides. Use this when the user gives you a direct URL link (e.g., "fetch https://example.com", "read this article https://...", "summarize the content at https://..."). This tool downloads the entire page content and converts it to markdown for you to read. You can then answer questions, summarize, or extract information from the content yourself. DO NOT use web_search when the user provides a specific URL - always use web_fetch instead.',
5720
- parameters: {
5721
- type: "object",
5722
- properties: {
5723
- url: {
5724
- type: "string",
5725
- format: "uri",
5726
- description: "The URL to fetch content from (must be http or https)"
5727
- }
5728
- },
5729
- required: ["url"]
5730
- }
5731
- }
5732
- })
5733
- };
5734
-
5735
5632
  // ../../b4m-core/packages/services/dist/src/llm/tools/implementation/math/index.js
5736
5633
  import { create as create6, all } from "mathjs";
5737
5634
  var math = create6(all, {
@@ -9631,7 +9528,6 @@ var tools = {
9631
9528
  image_generation: imageGenerationTool,
9632
9529
  edit_image: imageEditTool,
9633
9530
  web_search: webSearchTool,
9634
- web_fetch: webFetchTool,
9635
9531
  math_evaluate: mathTool,
9636
9532
  mermaid_chart: mermaidChartTool,
9637
9533
  current_datetime: currentDateTimeTool,
@@ -10247,14 +10143,14 @@ var ServerToolExecutor = class {
10247
10143
  const errorMessage = err?.response?.data?.error || err?.message || "Unknown error";
10248
10144
  const status = err?.response?.status || "unknown";
10249
10145
  logger.error(`[ServerToolExecutor] Request failed for ${toolName}: ${errorMessage} (status: ${status})`);
10250
- logger.debug(`[ServerToolExecutor] Full error: ${JSON.stringify(err, null, 2)}`);
10146
+ logger.error(`[ServerToolExecutor] Full error: ${JSON.stringify(err, null, 2)}`);
10251
10147
  return `Error executing ${toolName}: ${errorMessage}`;
10252
10148
  }
10253
10149
  }
10254
10150
  };
10255
10151
 
10256
10152
  // src/llm/ToolRouter.ts
10257
- var SERVER_TOOLS = ["weather_info", "web_search", "web_fetch"];
10153
+ var SERVER_TOOLS = ["weather_info", "web_search"];
10258
10154
  var LOCAL_TOOLS = [
10259
10155
  "file_read",
10260
10156
  "create_file",
@@ -10723,8 +10619,7 @@ function generateCliTools(userId, llm, model, permissionManager, showPermissionP
10723
10619
  // Server-side tools (executed via /api/ai/v1/tools Lambda)
10724
10620
  // Routed by ToolRouter to use B4M company API keys
10725
10621
  weather_info: {},
10726
- web_search: {},
10727
- web_fetch: {}
10622
+ web_search: {}
10728
10623
  // Disable web-only tools
10729
10624
  // blog_publish: undefined,
10730
10625
  // blog_edit: undefined,
@@ -11615,6 +11510,169 @@ var MessageBuilder = class {
11615
11510
  // src/llm/ServerLlmBackend.ts
11616
11511
  import { createParser } from "eventsource-parser";
11617
11512
  import { isAxiosError } from "axios";
11513
+
11514
+ // src/utils/StreamLogger.ts
11515
+ var StreamLogger = class _StreamLogger {
11516
+ constructor(logger2, context, verbose = false, ultraVerbose = false) {
11517
+ // State tracking
11518
+ this.startEventNum = 0;
11519
+ this.currentEventNum = 0;
11520
+ this.lastLoggedLength = 0;
11521
+ this.totalLength = 0;
11522
+ this.lastLogTime = 0;
11523
+ this.streamStartTime = 0;
11524
+ this.lastPreview = "";
11525
+ this.logger = logger2;
11526
+ this.context = context;
11527
+ this.verbose = verbose;
11528
+ this.ultraVerbose = ultraVerbose;
11529
+ }
11530
+ static {
11531
+ // Configuration thresholds
11532
+ this.CHAR_THRESHOLD = 200;
11533
+ }
11534
+ static {
11535
+ this.TIME_THRESHOLD_MS = 500;
11536
+ }
11537
+ static {
11538
+ this.PREVIEW_LENGTH = 30;
11539
+ }
11540
+ static {
11541
+ this.MAX_FULL_TEXT_LENGTH = 1e4;
11542
+ }
11543
+ /**
11544
+ * Called when stream starts
11545
+ */
11546
+ streamStart() {
11547
+ this.streamStartTime = Date.now();
11548
+ this.lastLogTime = this.streamStartTime;
11549
+ this.startEventNum = 0;
11550
+ this.currentEventNum = 0;
11551
+ this.lastLoggedLength = 0;
11552
+ this.totalLength = 0;
11553
+ this.lastPreview = "";
11554
+ this.logger.debug(`[${this.context}] Stream started`);
11555
+ }
11556
+ /**
11557
+ * Called on each SSE event
11558
+ */
11559
+ onEvent(eventNum, eventData) {
11560
+ this.currentEventNum = eventNum;
11561
+ if (this.ultraVerbose) {
11562
+ this.logger.debug(`[${this.context}] SSE event #${eventNum}, data: ${eventData.substring(0, 100)}`);
11563
+ }
11564
+ }
11565
+ /**
11566
+ * Called when content is accumulated
11567
+ */
11568
+ onContent(eventNum, textChunk, totalAccumulated) {
11569
+ this.currentEventNum = eventNum;
11570
+ this.totalLength = totalAccumulated.length;
11571
+ const rawPreview = totalAccumulated.slice(-_StreamLogger.PREVIEW_LENGTH);
11572
+ this.lastPreview = this.sanitizePreview(rawPreview);
11573
+ if (this.ultraVerbose) {
11574
+ this.logger.debug(`[${this.context}] Content event #${eventNum}, chunk: "${textChunk}"`);
11575
+ this.logger.debug(`[${this.context}] Accumulated text length: ${this.totalLength}`);
11576
+ return;
11577
+ }
11578
+ if (!this.verbose) {
11579
+ return;
11580
+ }
11581
+ const charsDelta = this.totalLength - this.lastLoggedLength;
11582
+ const timeDelta = Date.now() - this.lastLogTime;
11583
+ if (charsDelta >= _StreamLogger.CHAR_THRESHOLD || timeDelta >= _StreamLogger.TIME_THRESHOLD_MS) {
11584
+ this.flushBatch();
11585
+ }
11586
+ }
11587
+ /**
11588
+ * Called for non-content events (tool_use, errors, etc.)
11589
+ * These are always logged immediately (critical events)
11590
+ */
11591
+ onCriticalEvent(eventNum, eventType, description) {
11592
+ this.currentEventNum = eventNum;
11593
+ if (this.verbose && !this.ultraVerbose) {
11594
+ const charsDelta = this.totalLength - this.lastLoggedLength;
11595
+ if (charsDelta > 0) {
11596
+ this.flushBatch();
11597
+ }
11598
+ }
11599
+ this.logger.debug(`[${this.context}] ${eventType} event #${eventNum}: ${description}`);
11600
+ }
11601
+ /**
11602
+ * Called when stream completes
11603
+ */
11604
+ streamComplete(totalText) {
11605
+ if (this.verbose && !this.ultraVerbose) {
11606
+ const charsDelta = this.totalLength - this.lastLoggedLength;
11607
+ if (charsDelta > 0) {
11608
+ this.flushBatch();
11609
+ }
11610
+ }
11611
+ const totalTime = Date.now() - this.streamStartTime;
11612
+ this.logger.debug(
11613
+ `[${this.context}] Stream complete: ${this.currentEventNum} events, ${this.totalLength} chars, ${(totalTime / 1e3).toFixed(2)}s`
11614
+ );
11615
+ if (totalText.length > _StreamLogger.MAX_FULL_TEXT_LENGTH) {
11616
+ const truncated = totalText.substring(0, _StreamLogger.MAX_FULL_TEXT_LENGTH);
11617
+ this.logger.debug(
11618
+ `[${this.context}] Full text (truncated to ${_StreamLogger.MAX_FULL_TEXT_LENGTH} chars):
11619
+ ${truncated}
11620
+ ... [+${totalText.length - _StreamLogger.MAX_FULL_TEXT_LENGTH} more chars]`
11621
+ );
11622
+ } else if (totalText.length > 0) {
11623
+ this.logger.debug(`[${this.context}] Full text:
11624
+ ${totalText}`);
11625
+ }
11626
+ }
11627
+ /**
11628
+ * Called when stream fails
11629
+ */
11630
+ streamError(error) {
11631
+ if (this.verbose && !this.ultraVerbose) {
11632
+ const charsDelta = this.totalLength - this.lastLoggedLength;
11633
+ if (charsDelta > 0) {
11634
+ this.flushBatch();
11635
+ }
11636
+ }
11637
+ this.logger.error(`[${this.context}] Stream error:`, error);
11638
+ }
11639
+ /**
11640
+ * Flush the current batch to console
11641
+ */
11642
+ flushBatch() {
11643
+ const charsDelta = this.totalLength - this.lastLoggedLength;
11644
+ const timeDelta = Date.now() - this.lastLogTime;
11645
+ const eventRange = this.startEventNum === this.currentEventNum ? `#${this.currentEventNum}` : `#${this.startEventNum}-${this.currentEventNum}`;
11646
+ const preview = this.lastPreview.length > 0 ? ` "...${this.lastPreview}"` : "";
11647
+ this.logger.debug(
11648
+ `[${this.context}] Streaming... events ${eventRange}, ${this.totalLength} chars (+${charsDelta}, ${(timeDelta / 1e3).toFixed(1)}s)${preview}`
11649
+ );
11650
+ this.lastLoggedLength = this.totalLength;
11651
+ this.lastLogTime = Date.now();
11652
+ this.startEventNum = this.currentEventNum + 1;
11653
+ }
11654
+ /**
11655
+ * Sanitize preview text for single-line display
11656
+ * Replaces newlines and control characters with spaces
11657
+ */
11658
+ sanitizePreview(text) {
11659
+ return text.replace(/[\r\n\t]/g, " ").replace(/\s+/g, " ");
11660
+ }
11661
+ /**
11662
+ * Reset state for new stream
11663
+ */
11664
+ reset() {
11665
+ this.startEventNum = 0;
11666
+ this.currentEventNum = 0;
11667
+ this.lastLoggedLength = 0;
11668
+ this.totalLength = 0;
11669
+ this.lastLogTime = 0;
11670
+ this.streamStartTime = 0;
11671
+ this.lastPreview = "";
11672
+ }
11673
+ };
11674
+
11675
+ // src/llm/ServerLlmBackend.ts
11618
11676
  function stripThinkingBlocks(text) {
11619
11677
  return text.replace(/<think>[\s\S]*?<\/think>/g, "").trim();
11620
11678
  }
@@ -11639,6 +11697,10 @@ var ServerLlmBackend = class {
11639
11697
  logger.debug("[ServerLlmBackend] Making streaming request...");
11640
11698
  const response = await this.makeStreamingRequest(model, messages, options);
11641
11699
  logger.debug("[ServerLlmBackend] Got response, setting up SSE parser");
11700
+ const isVerbose = process.env.B4M_VERBOSE === "1";
11701
+ const isUltraVerbose = process.env.B4M_DEBUG_STREAM === "1";
11702
+ const streamLogger = new StreamLogger(logger, "ServerLlmBackend", isVerbose, isUltraVerbose);
11703
+ streamLogger.streamStart();
11642
11704
  let eventCount = 0;
11643
11705
  let accumulatedText = "";
11644
11706
  let lastUsageInfo = {};
@@ -11648,12 +11710,13 @@ var ServerLlmBackend = class {
11648
11710
  const parser = createParser({
11649
11711
  onEvent: (event) => {
11650
11712
  eventCount++;
11651
- logger.debug(`[ServerLlmBackend] SSE event #${eventCount}, data: ${event.data?.substring(0, 100)}`);
11713
+ streamLogger.onEvent(eventCount, event.data || "");
11652
11714
  const data = event.data;
11653
11715
  if (data === "[DONE]") {
11654
11716
  receivedDone = true;
11655
- logger.debug(`[ServerLlmBackend] Received [DONE], accumulated text length: ${accumulatedText.length}`);
11717
+ streamLogger.onCriticalEvent(eventCount, "[DONE]", `accumulated text length: ${accumulatedText.length}`);
11656
11718
  const cleanedText = stripThinkingBlocks(accumulatedText);
11719
+ streamLogger.streamComplete(accumulatedText);
11657
11720
  if (toolsUsed.length > 0) {
11658
11721
  const info = {
11659
11722
  toolsUsed,
@@ -11682,9 +11745,8 @@ var ServerLlmBackend = class {
11682
11745
  }
11683
11746
  try {
11684
11747
  const parsed = JSON.parse(data);
11685
- logger.debug(`[ServerLlmBackend] Parsed event type: ${parsed.type}`);
11686
11748
  if (parsed.type === "error") {
11687
- logger.error("[ServerLlmBackend] Error event:", parsed);
11749
+ streamLogger.onCriticalEvent(eventCount, "ERROR", parsed.message || "Server error");
11688
11750
  reject(new Error(parsed.message || "Server error"));
11689
11751
  return;
11690
11752
  }
@@ -11697,9 +11759,9 @@ var ServerLlmBackend = class {
11697
11759
  outputTokens: parsed.usage.outputTokens
11698
11760
  };
11699
11761
  }
11700
- logger.debug(`[ServerLlmBackend] Accumulated text length: ${accumulatedText.length}`);
11762
+ streamLogger.onContent(eventCount, textChunk, accumulatedText);
11701
11763
  } else if (parsed.type === "tool_use") {
11702
- logger.debug(`[ServerLlmBackend] Tool use event, tools: ${parsed.tools?.length}`);
11764
+ streamLogger.onCriticalEvent(eventCount, "TOOL_USE", `tools: ${parsed.tools?.length}`);
11703
11765
  if (parsed.tools && parsed.tools.length > 0) {
11704
11766
  for (const tool of parsed.tools) {
11705
11767
  logger.debug(`TOOL REQUEST: ${tool.name}`);
@@ -11720,7 +11782,7 @@ var ServerLlmBackend = class {
11720
11782
  }
11721
11783
  if (parsed.thinking && parsed.thinking.length > 0) {
11722
11784
  thinkingBlocks = parsed.thinking;
11723
- logger.debug(`[ServerLlmBackend] Received ${thinkingBlocks.length} thinking blocks`);
11785
+ streamLogger.onCriticalEvent(eventCount, "THINKING", `${thinkingBlocks.length} thinking blocks`);
11724
11786
  }
11725
11787
  if (parsed.usage) {
11726
11788
  lastUsageInfo = {
@@ -11730,7 +11792,7 @@ var ServerLlmBackend = class {
11730
11792
  }
11731
11793
  }
11732
11794
  } catch (parseError) {
11733
- logger.error("[ServerLlmBackend] Failed to parse SSE event:", parseError);
11795
+ streamLogger.streamError(parseError);
11734
11796
  }
11735
11797
  }
11736
11798
  });
@@ -12078,7 +12140,7 @@ import { isAxiosError as isAxiosError2 } from "axios";
12078
12140
  // package.json
12079
12141
  var package_default = {
12080
12142
  name: "@bike4mind/cli",
12081
- version: "0.2.20-feat-web-fetch-tool.18049+39c7dfb4d",
12143
+ version: "0.2.20-feat-cli-smart-stream-logging.18065+d52838015",
12082
12144
  type: "module",
12083
12145
  description: "Interactive CLI tool for Bike4Mind with ReAct agents",
12084
12146
  license: "UNLICENSED",
@@ -12185,10 +12247,10 @@ var package_default = {
12185
12247
  },
12186
12248
  devDependencies: {
12187
12249
  "@bike4mind/agents": "0.1.0",
12188
- "@bike4mind/common": "2.44.1-feat-web-fetch-tool.18049+39c7dfb4d",
12189
- "@bike4mind/mcp": "1.24.1-feat-web-fetch-tool.18049+39c7dfb4d",
12190
- "@bike4mind/services": "2.42.1-feat-web-fetch-tool.18049+39c7dfb4d",
12191
- "@bike4mind/utils": "2.2.2-feat-web-fetch-tool.18049+39c7dfb4d",
12250
+ "@bike4mind/common": "2.44.1-feat-cli-smart-stream-logging.18065+d52838015",
12251
+ "@bike4mind/mcp": "1.24.1-feat-cli-smart-stream-logging.18065+d52838015",
12252
+ "@bike4mind/services": "2.42.1-feat-cli-smart-stream-logging.18065+d52838015",
12253
+ "@bike4mind/utils": "2.2.2-feat-cli-smart-stream-logging.18065+d52838015",
12192
12254
  "@types/better-sqlite3": "^7.6.13",
12193
12255
  "@types/diff": "^5.0.9",
12194
12256
  "@types/jsonwebtoken": "^9.0.4",
@@ -12205,7 +12267,7 @@ var package_default = {
12205
12267
  optionalDependencies: {
12206
12268
  "@vscode/ripgrep": "^1.17.0"
12207
12269
  },
12208
- gitHead: "39c7dfb4d069a6ad381f141879508e07c8f94368"
12270
+ gitHead: "d5283801593920282f1b31c29b56dea473919327"
12209
12271
  };
12210
12272
 
12211
12273
  // src/config/constants.ts
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  CurationArtifactType
4
- } from "./chunk-T5NXFTZC.js";
4
+ } from "./chunk-DTPBHKQL.js";
5
5
 
6
6
  // ../../b4m-core/packages/services/dist/src/notebookCurationService/llmMarkdownGenerator.js
7
7
  var DEFAULT_OPTIONS = {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  CurationArtifactType
4
- } from "./chunk-T5NXFTZC.js";
4
+ } from "./chunk-DTPBHKQL.js";
5
5
 
6
6
  // ../../b4m-core/packages/services/dist/src/notebookCurationService/markdownGenerator.js
7
7
  var DEFAULT_OPTIONS = {
@@ -2,9 +2,9 @@
2
2
  import {
3
3
  findMostSimilarMemento,
4
4
  getRelevantMementos
5
- } from "./chunk-NAZHGW73.js";
6
- import "./chunk-VAFY6X3M.js";
7
- import "./chunk-T5NXFTZC.js";
5
+ } from "./chunk-RRYX2K7W.js";
6
+ import "./chunk-EP4OGIQI.js";
7
+ import "./chunk-DTPBHKQL.js";
8
8
  import "./chunk-OCYRD7D6.js";
9
9
  export {
10
10
  findMostSimilarMemento,
@@ -129,8 +129,8 @@ import {
129
129
  validateMermaidSyntax,
130
130
  warmUpSettingsCache,
131
131
  withRetry
132
- } from "./chunk-VAFY6X3M.js";
133
- import "./chunk-T5NXFTZC.js";
132
+ } from "./chunk-EP4OGIQI.js";
133
+ import "./chunk-DTPBHKQL.js";
134
134
  import {
135
135
  Logger,
136
136
  NotificationDeduplicator,
@@ -160,6 +160,8 @@ import {
160
160
  QuestResourceSchema,
161
161
  QuestSchema,
162
162
  QuestStatusSchema,
163
+ RESTRICTION_OPERATIONS,
164
+ RESTRICTION_SUBJECT_TYPES,
163
165
  RapidReplyFallbackBehaviors,
164
166
  RapidReplyResponseStylesCommon,
165
167
  RapidReplySettingsSchema,
@@ -306,7 +308,7 @@ import {
306
308
  validateReactArtifactV2,
307
309
  validateSvgArtifactV2,
308
310
  wikiMarkupToAdf
309
- } from "./chunk-T5NXFTZC.js";
311
+ } from "./chunk-DTPBHKQL.js";
310
312
  export {
311
313
  ALL_IMAGE_MODELS,
312
314
  ALL_IMAGE_SIZES,
@@ -468,6 +470,8 @@ export {
468
470
  QuestResourceSchema,
469
471
  QuestSchema,
470
472
  QuestStatusSchema,
473
+ RESTRICTION_OPERATIONS,
474
+ RESTRICTION_SUBJECT_TYPES,
471
475
  RapidReplyFallbackBehaviors,
472
476
  RapidReplyResponseStylesCommon,
473
477
  RapidReplySettingsSchema,
@@ -2,9 +2,9 @@
2
2
  import {
3
3
  SubtractCreditsSchema,
4
4
  subtractCredits
5
- } from "./chunk-W5U65OA2.js";
6
- import "./chunk-VAFY6X3M.js";
7
- import "./chunk-T5NXFTZC.js";
5
+ } from "./chunk-XWI42ILN.js";
6
+ import "./chunk-EP4OGIQI.js";
7
+ import "./chunk-DTPBHKQL.js";
8
8
  import "./chunk-OCYRD7D6.js";
9
9
  export {
10
10
  SubtractCreditsSchema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bike4mind/cli",
3
- "version": "0.2.20-feat-web-fetch-tool.18049+39c7dfb4d",
3
+ "version": "0.2.20-feat-cli-smart-stream-logging.18065+d52838015",
4
4
  "type": "module",
5
5
  "description": "Interactive CLI tool for Bike4Mind with ReAct agents",
6
6
  "license": "UNLICENSED",
@@ -107,10 +107,10 @@
107
107
  },
108
108
  "devDependencies": {
109
109
  "@bike4mind/agents": "0.1.0",
110
- "@bike4mind/common": "2.44.1-feat-web-fetch-tool.18049+39c7dfb4d",
111
- "@bike4mind/mcp": "1.24.1-feat-web-fetch-tool.18049+39c7dfb4d",
112
- "@bike4mind/services": "2.42.1-feat-web-fetch-tool.18049+39c7dfb4d",
113
- "@bike4mind/utils": "2.2.2-feat-web-fetch-tool.18049+39c7dfb4d",
110
+ "@bike4mind/common": "2.44.1-feat-cli-smart-stream-logging.18065+d52838015",
111
+ "@bike4mind/mcp": "1.24.1-feat-cli-smart-stream-logging.18065+d52838015",
112
+ "@bike4mind/services": "2.42.1-feat-cli-smart-stream-logging.18065+d52838015",
113
+ "@bike4mind/utils": "2.2.2-feat-cli-smart-stream-logging.18065+d52838015",
114
114
  "@types/better-sqlite3": "^7.6.13",
115
115
  "@types/diff": "^5.0.9",
116
116
  "@types/jsonwebtoken": "^9.0.4",
@@ -127,5 +127,5 @@
127
127
  "optionalDependencies": {
128
128
  "@vscode/ripgrep": "^1.17.0"
129
129
  },
130
- "gitHead": "39c7dfb4d069a6ad381f141879508e07c8f94368"
130
+ "gitHead": "d5283801593920282f1b31c29b56dea473919327"
131
131
  }