@bike4mind/cli 0.2.37 → 0.2.38-docs-clawdbot-comparison-analysis.20242

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.
@@ -4,7 +4,7 @@ import {
4
4
  getOpenWeatherKey,
5
5
  getSerperKey,
6
6
  getWolframAlphaKey
7
- } from "./chunk-NJQYWIDJ.js";
7
+ } from "./chunk-G3PPBZQR.js";
8
8
  import {
9
9
  BFLImageService,
10
10
  BaseStorage,
@@ -16,14 +16,14 @@ import {
16
16
  OpenAIBackend,
17
17
  OpenAIImageService,
18
18
  XAIImageService
19
- } from "./chunk-5YLGNW2B.js";
19
+ } from "./chunk-Q3TLK7O5.js";
20
20
  import {
21
21
  Logger
22
22
  } from "./chunk-PFBYGCOW.js";
23
23
  import {
24
24
  ConfigStore,
25
25
  logger
26
- } from "./chunk-H7RVLAQD.js";
26
+ } from "./chunk-VNZXDZRP.js";
27
27
  import {
28
28
  AiEvents,
29
29
  ApiKeyEvents,
@@ -77,10 +77,11 @@ import {
77
77
  VideoModels,
78
78
  XAI_IMAGE_MODELS,
79
79
  b4mLLMTools,
80
+ getDataLakeTags,
80
81
  getMcpProviderMetadata,
81
82
  getViewById,
82
83
  resolveNavigationIntents
83
- } from "./chunk-EY65E4W4.js";
84
+ } from "./chunk-DF2SGZ6Z.js";
84
85
 
85
86
  // src/utils/fileSearch.ts
86
87
  import * as fs from "fs";
@@ -1846,7 +1847,8 @@ ${options.context}` : this.getSystemPrompt()
1846
1847
  temperature,
1847
1848
  abortSignal: options.signal,
1848
1849
  tool_choice: this.context.toolChoice,
1849
- executeTools: false
1850
+ executeTools: false,
1851
+ thinking: this.context.thinking
1850
1852
  },
1851
1853
  async (texts, completionInfo) => {
1852
1854
  for (const text of texts) {
@@ -2146,34 +2148,20 @@ Remember: You are an autonomous AGENT. Act independently and solve problems proa
2146
2148
  }
2147
2149
  }
2148
2150
  /**
2149
- * Build and append tool call/result messages for the conversation history
2151
+ * Build and append tool call/result messages for the conversation history.
2152
+ * Delegates to the backend's pushToolMessages so each provider formats
2153
+ * messages according to its own API requirements.
2150
2154
  */
2151
2155
  appendToolMessages(messages, toolUse, observation, thinkingBlocks) {
2152
2156
  const params = this.parseToolArguments(toolUse.arguments);
2153
- const msgToolCallId = `${toolUse.name}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
2154
- const assistantContent = [
2155
- ...thinkingBlocks,
2156
- {
2157
- type: "tool_use",
2158
- id: msgToolCallId,
2159
- name: toolUse.name,
2160
- input: params
2161
- }
2162
- ];
2163
- messages.push({
2164
- role: "assistant",
2165
- content: assistantContent
2166
- });
2167
- messages.push({
2168
- role: "user",
2169
- content: [
2170
- {
2171
- type: "tool_result",
2172
- tool_use_id: msgToolCallId,
2173
- content: observation
2174
- }
2175
- ]
2176
- });
2157
+ const toolId = toolUse.id || `${toolUse.name}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
2158
+ const parameters = typeof toolUse.arguments === "string" ? toolUse.arguments : JSON.stringify(params);
2159
+ this.context.llm.pushToolMessages(
2160
+ messages,
2161
+ { id: toolId, name: toolUse.name, parameters },
2162
+ observation,
2163
+ thinkingBlocks
2164
+ );
2177
2165
  }
2178
2166
  };
2179
2167
 
@@ -8745,7 +8733,7 @@ var knowledgeBaseSearchTool = {
8745
8733
  toolFn: async (value) => {
8746
8734
  const params = value;
8747
8735
  const { query, tags, file_type, max_results = 5 } = params;
8748
- context.logger.log("\u{1F4DA} Knowledge Base Search: Starting search for query:", query);
8736
+ context.logger.log("\u{1F4DA} Knowledge Base Search: userId:", context.userId, "query:", query, "tags:", tags);
8749
8737
  if (!context.db.fabfiles) {
8750
8738
  context.logger.error("\u274C Knowledge Base Search: fabfiles repository not available");
8751
8739
  return "Knowledge base search is not available at this time.";
@@ -8768,10 +8756,12 @@ var knowledgeBaseSearchTool = {
8768
8756
  // Search across fileName + tags + notes for better recall
8769
8757
  includeShared: true,
8770
8758
  // Include owned + explicitly shared + org-shared files
8771
- userGroups: context.user.groups || []
8759
+ userGroups: context.user.groups || [],
8772
8760
  // Pass user's groups for org-level sharing
8761
+ dataLakeTags: getDataLakeTags(context.user.tags || [])
8762
+ // Include data lake files user has access to
8773
8763
  });
8774
- context.logger.log("\u{1F4DA} Knowledge Base Search: Found", searchResults.data.length, "results");
8764
+ context.logger.log("\u{1F4DA} Knowledge Base Search: Found", searchResults.data.length, "of", searchResults.total, "results. Files:", searchResults.data.map((f) => f.fileName));
8775
8765
  return formatSearchResults(searchResults.data);
8776
8766
  } catch (error) {
8777
8767
  context.logger.error("\u274C Knowledge Base Search: Error during search:", error);
@@ -8791,7 +8781,7 @@ var knowledgeBaseSearchTool = {
8791
8781
  tags: {
8792
8782
  type: "array",
8793
8783
  items: { type: "string" },
8794
- description: 'Optional: filter results by tag names. Supports partial matching. For optimization docs, use tags like "opti:family:scheduling", "opti:QUBO", "opti:solver:highs", etc. Any matching tag qualifies the file.'
8784
+ description: 'Optional: filter results by tag names. Supports partial matching. For optimization docs, use tags like "opti:family:scheduling", "opti:QUBO", "opti:solver:highs". For IonQ sales intelligence, use tags like "ionq:vertical:pharma", "ionq:competitor:ibm", "ionq:type:product-specs", "ionq:stage:discovery", "ionq:offering:forte". Any matching tag qualifies the file.'
8795
8785
  },
8796
8786
  file_type: {
8797
8787
  type: "string",
@@ -8840,7 +8830,7 @@ var knowledgeBaseRetrieveTool = {
8840
8830
  if (file) {
8841
8831
  files = [file];
8842
8832
  } else {
8843
- const searchResults = await context.db.fabfiles.search(context.userId, file_id, { tags: [], shared: false }, { page: 1, limit: 1 }, { by: "fileName", direction: "asc" }, { textSearch: true, includeShared: true, userGroups: context.user.groups || [] });
8833
+ const searchResults = await context.db.fabfiles.search(context.userId, file_id, { tags: [], shared: false }, { page: 1, limit: 1 }, { by: "fileName", direction: "asc" }, { textSearch: true, includeShared: true, userGroups: context.user.groups || [], dataLakeTags: getDataLakeTags(context.user.tags || []) });
8844
8834
  files = searchResults.data;
8845
8835
  }
8846
8836
  if (files.length === 0) {
@@ -8848,7 +8838,7 @@ var knowledgeBaseRetrieveTool = {
8848
8838
  }
8849
8839
  }
8850
8840
  if (files.length === 0 && (tags?.length || query)) {
8851
- const searchResults = await context.db.fabfiles.search(context.userId, query || "", { tags: tags || [], shared: false }, { page: 1, limit: 5 }, { by: "fileName", direction: "asc" }, { textSearch: true, includeShared: true, userGroups: context.user.groups || [] });
8841
+ const searchResults = await context.db.fabfiles.search(context.userId, query || "", { tags: tags || [], shared: false }, { page: 1, limit: 5 }, { by: "fileName", direction: "asc" }, { textSearch: true, includeShared: true, userGroups: context.user.groups || [], dataLakeTags: getDataLakeTags(context.user.tags || []) });
8852
8842
  files = searchResults.data;
8853
8843
  if (files.length === 0) {
8854
8844
  const searchDesc = [query && `query "${query}"`, tags?.length && `tags [${tags.join(", ")}]`].filter(Boolean).join(" and ");
@@ -8904,7 +8894,7 @@ Chunks: ${chunks.length} | Characters: ${charLabel}
8904
8894
  tags: {
8905
8895
  type: "array",
8906
8896
  items: { type: "string" },
8907
- description: 'Filter documents by tags. For optimization docs, use tags like "opti:family:scheduling", "opti:solver:highs", etc.'
8897
+ description: 'Filter documents by tags. For optimization docs, use tags like "opti:family:scheduling", "opti:solver:highs". For IonQ sales intelligence, use tags like "ionq:vertical:pharma", "ionq:competitor:ibm", "ionq:type:product-specs", "ionq:offering:forte".'
8908
8898
  },
8909
8899
  query: {
8910
8900
  type: "string",
@@ -13280,232 +13270,64 @@ var generateMcpTools = async (mcpData) => {
13280
13270
  Logger.debug(`\u{1F527} generateMcpTools: Generated ${result.length} tool implementations for ${mcpData.serverName}`);
13281
13271
  return result;
13282
13272
  };
13283
-
13284
- // ../../b4m-core/packages/services/dist/src/llm/agents/ServerSubagentOrchestrator.js
13285
- var SUBAGENT_TIMEOUT_MS = 5 * 60 * 1e3;
13286
-
13287
- // ../../b4m-core/packages/services/dist/src/llm/agents/CodeReviewAgent.js
13288
- var CodeReviewAgent = (config) => ({
13289
- name: "code_review",
13290
- description: "Code review specialist for analyzing code quality, bugs, and improvements",
13291
- model: config?.model ?? ChatModels.CLAUDE_4_6_SONNET_BEDROCK,
13292
- fallbackModels: [ChatModels.GPT4_1, ChatModels.GPT4_1_MINI],
13293
- defaultThoroughness: config?.defaultThoroughness ?? "medium",
13294
- maxIterations: { quick: 3, medium: 8, very_thorough: 15 },
13295
- deniedTools: ["image_generation", "edit_image", "delegate_to_agent", ...config?.extraDeniedTools ?? []],
13296
- allowedTools: config?.extraAllowedTools,
13297
- systemPrompt: `You are a code review specialist. Your job is to analyze code for quality, correctness, security, and maintainability.
13298
-
13299
- ## Focus Areas
13300
- - Bugs, logic errors, and edge cases
13301
- - Security vulnerabilities (injection, auth issues, data exposure)
13302
- - Code quality and readability
13303
- - Performance concerns
13304
- - Adherence to project patterns and conventions
13305
-
13306
- ## Review Process
13307
- 1. Understand the context and intent of the code changes
13308
- 2. Check for bugs, logic errors, and unhandled edge cases
13309
- 3. Identify security vulnerabilities and data handling issues
13310
- 4. Evaluate code clarity, naming, and structure
13311
- 5. Look for performance problems or unnecessary complexity
13312
-
13313
- ## Output Format
13314
- Provide actionable feedback:
13315
-
13316
- ### Critical Issues
13317
- - Bugs, security vulnerabilities, or correctness problems that must be fixed
13318
-
13319
- ### Suggestions
13320
- - Code quality improvements with rationale
13321
-
13322
- ### Positive Observations
13323
- - Well-implemented patterns worth noting (optional)
13324
-
13325
- Focus on actionable, specific feedback referencing exact code locations. Your review will be used by the main agent.`
13326
- });
13327
-
13328
- // ../../b4m-core/packages/services/dist/src/llm/agents/ProjectManagerAgent.js
13329
- var ProjectManagerAgent = (config) => ({
13330
- name: "project_manager",
13331
- description: "Project management via Jira and Confluence (create issues, search, update status, manage attachments, write docs). ALWAYS delegate Jira/Confluence requests to this agent \u2014 you do not have direct access to these tools",
13332
- model: config?.model ?? ChatModels.CLAUDE_4_6_SONNET_BEDROCK,
13333
- fallbackModels: [ChatModels.GPT4_1, ChatModels.GPT4_1_MINI],
13334
- defaultThoroughness: config?.defaultThoroughness ?? "medium",
13335
- maxIterations: { quick: 3, medium: 8, very_thorough: 15 },
13336
- allowedTools: [...config?.extraAllowedTools ?? []],
13337
- deniedTools: [...config?.extraDeniedTools ?? []],
13338
- exclusiveMcpServers: ["atlassian"],
13339
- systemPrompt: `You are a project management specialist with access to Jira and Confluence. Your job is to help manage projects, issues, documentation, and team workflows.
13340
-
13341
- ## Capabilities
13342
-
13343
- ### Jira
13344
- - Search for issues using JQL
13345
- - Create, update, and transition issues
13346
- - Add comments and manage watchers
13347
- - List projects and issue types
13348
- - List, upload, download, and delete attachments on issues
13349
-
13350
- ### Confluence
13351
- - Search for documentation
13352
- - Create and update pages
13353
- - Browse spaces and page hierarchies
13354
- - List, upload, download, and delete attachments on pages
13355
-
13356
- ### Account & Identity
13357
- - Check connected Jira account using \`atlassian__jira_get_current_user\`
13358
- - Check connected Confluence account using \`atlassian__confluence_get_current_user\`
13359
- - Look up users by name or email using \`atlassian__jira_search_users\`
13360
-
13361
- ## Best Practices
13362
- 1. When searching Jira, use precise JQL queries (e.g., \`project = PROJ AND status = "In Progress"\`)
13363
- 2. When creating issues, always check available issue types first with \`atlassian__jira_list_issue_types\`
13364
- 3. When updating issue status, use \`atlassian__jira_update_issue_transition\` with the target status name
13365
- 4. When creating Confluence pages, use the user's personal space if no space is specified
13366
- 5. Always confirm destructive operations (delete) with the user before proceeding
13367
- 6. When asked about the connected Atlassian account call BOTH \`atlassian__jira_get_current_user\` and \`atlassian__confluence_get_current_user\` to show the full picture. Present results clearly labeled by product, and note if either service is not connected. NEVER say you don't have access to account information \u2014 you DO have these tools.
13368
- 7. When asked specifically about "Jira account" or "Confluence account" only, use the respective tool alone.
13369
- 8. When asked about attachments, ALWAYS use the attachment tools \u2014 never guess or fabricate attachment details. For uploading files shared in Slack, pass the \`fabFileId\` from the message context to \`atlassian__jira_upload_attachment\` or \`atlassian__confluence_upload_attachment\`.
13370
-
13371
- ## Output Format
13372
- Provide a clear summary of actions taken:
13373
- 1. What was done (created, updated, searched, etc.)
13374
- 2. Links or keys to relevant items (e.g., PROJ-123)
13375
- 3. Any issues or warnings encountered
13376
-
13377
- Be precise with issue keys and project names. Your results will be used by the main agent.`
13378
- });
13379
-
13380
- // ../../b4m-core/packages/services/dist/src/llm/agents/GithubManagerAgent.js
13381
- var GithubManagerAgent = (config) => ({
13382
- name: "github_manager",
13383
- description: "GitHub operations (issues, pull requests, code search, branches, workflows, reviews). ALWAYS delegate GitHub requests to this agent \u2014 you do not have direct access to these tools",
13384
- model: config?.model ?? ChatModels.CLAUDE_4_6_SONNET_BEDROCK,
13385
- fallbackModels: [ChatModels.GPT4_1, ChatModels.GPT4_1_MINI],
13386
- defaultThoroughness: config?.defaultThoroughness ?? "medium",
13387
- maxIterations: { quick: 3, medium: 8, very_thorough: 15 },
13388
- allowedTools: [...config?.extraAllowedTools ?? []],
13389
- deniedTools: [...config?.extraDeniedTools ?? []],
13390
- exclusiveMcpServers: ["github"],
13391
- systemPrompt: `You are a GitHub specialist with access to GitHub's API. Your job is to help manage repositories, issues, pull requests, code search, branches, and CI/CD workflows.
13392
-
13393
- ## First Step: ALWAYS Resolve Repository Context Automatically
13394
- Before doing ANYTHING else, call the \`github__current_user\` tool. The response includes:
13395
- - **selected_repositories**: The list of repositories the user has enabled for AI access (in \`owner/repo\` format)
13396
- - **user**: The authenticated user's profile (login, name, etc.)
13397
-
13398
- ### Repository Resolution Rules (CRITICAL)
13399
- You MUST use the selected_repositories list to automatically resolve the owner and repo. NEVER ask the user for the org, owner, or full repository path. Instead:
13400
-
13401
- 1. **Exact match**: If the user says a repo name that exactly matches a repo name in selected_repositories, use it immediately.
13402
- 2. **Partial/fuzzy match**: If the user provides a partial name, match it against any repo in selected_repositories whose name contains that term (e.g., if the user says "lumina" and selected_repositories contains \`SomeOrg/lumina5\`, use that).
13403
- 3. **Single repo shortcut**: If only one repository is selected, ALWAYS default to it \u2014 no questions asked.
13404
- 4. **Multiple matches**: Only if multiple selected repos match the user's term AND you truly cannot disambiguate, list the matching repos and ask which one. Do NOT list repos that don't match.
13405
- 5. **No match**: If nothing in selected_repositories matches, tell the user which repos are available and ask them to clarify.
13406
-
13407
- **NEVER ask the user to "paste the GitHub URL" or provide the org/owner name.** You already have this information from \`github__current_user\`. The owner and repo are embedded in the \`owner/repo\` format of each selected repository entry.
13408
-
13409
- ### Defaults for Ambiguous Requests
13410
- - **Timezone**: Default to UTC unless the user specifies otherwise.
13411
- - **PR/Issue state**: Default to \`open\` unless the user specifies otherwise.
13412
- - **Scope**: All matching repos unless the user narrows it down.
13413
- - **When in doubt, act**: Prefer making reasonable assumptions and proceeding over asking clarifying questions. You can always note your assumptions in the response.
13414
-
13415
- ## Capabilities
13416
-
13417
- ### Issues
13418
- - Create, update, and search issues
13419
- - Add comments and manage labels
13420
- - List and filter issues by state, assignee, labels
13421
-
13422
- ### Pull Requests
13423
- - Create, update, and list pull requests
13424
- - Get PR diffs, files changed, and review status
13425
- - Merge pull requests and manage reviews
13426
- - Request reviews from team members
13427
-
13428
- ### Code & Repository
13429
- - Search code across repositories
13430
- - Get file contents and commit history
13431
- - Create and list branches and tags
13432
- - Fork repositories
13433
-
13434
- ### CI/CD & Workflows
13435
- - List and monitor workflow runs
13436
- - Get job logs for debugging failures
13437
- - Re-run failed jobs or entire workflows
13438
- - Download workflow artifacts
13439
-
13440
- ### Notifications
13441
- - List and manage GitHub notifications
13442
- - Mark notifications as read/done
13443
-
13444
- ## Best Practices
13445
- 1. When searching issues or PRs, use GitHub search syntax (e.g., \`is:open label:bug assignee:username\`)
13446
- 2. When creating PRs, always include a clear title and description
13447
- 3. When reviewing PR changes, use \`get_pull_request_diff\` or \`get_pull_request_files\` for context
13448
- 4. For CI/CD debugging, use \`get_job_logs\` with \`failed_only=true\` to focus on failures
13449
- 5. Always include links to issues/PRs in your responses (e.g., owner/repo#123)
13450
-
13451
- ## Output Format
13452
- Provide a clear summary of actions taken:
13453
- 1. What was done (created, searched, merged, etc.)
13454
- 2. Always provide links to relevant items (e.g., owner/repo#123, PR URLs)
13455
- 3. Any issues or warnings encountered
13456
-
13457
- Be precise with repository names, issue numbers, and PR numbers. Your results will be used by the main agent.
13458
-
13459
- ## Write Operations & Confirmation
13460
- Write tools (create issue, update issue, comment, merge PR, etc.) have a built-in confirmation system. When you call them, they return a preview with Confirm/Cancel buttons \u2014 the user clicks to execute. **NEVER ask the user for text confirmation before calling a write tool.** Just call the tool immediately. The tool handles confirmation automatically.
13461
-
13462
- **CRITICAL: When a write tool returns \`"confirmation_required": true\`, the action has NOT been executed yet.** It is a preview awaiting user confirmation via buttons. Your summary MUST say the action is **awaiting confirmation**, NOT that it was completed. Example:
13463
- - \u2705 "A comment preview is ready on PR #123. Click Confirm below to post it."
13464
- - \u274C "Done! The comment has been posted on PR #123." (WRONG \u2014 it hasn't been posted yet)`
13465
- });
13466
-
13467
- // ../../b4m-core/packages/services/dist/src/llm/agents/ServerAgentStore.js
13468
- var ServerAgentStore = class {
13469
- constructor() {
13470
- const builtInAgents = [
13471
- // For now disable explore and plan agents as it is much faster if the parent llm handles this.
13472
- // ExploreAgent(),
13473
- // PlanAgent(),
13474
- CodeReviewAgent(),
13475
- ProjectManagerAgent(),
13476
- GithubManagerAgent()
13477
- ];
13478
- this.agents = new Map(builtInAgents.map((a) => [a.name, a]));
13479
- }
13480
- getAgent(name) {
13481
- return this.agents.get(name);
13482
- }
13483
- getAllAgents() {
13484
- return Array.from(this.agents.values());
13485
- }
13486
- getAgentNames() {
13487
- return Array.from(this.agents.keys());
13488
- }
13489
- hasAgent(name) {
13490
- return this.agents.has(name);
13491
- }
13492
- /**
13493
- * Get the deduplicated list of MCP server names that are exclusive to agents.
13494
- * Derived from each agent's `exclusiveMcpServers` field.
13495
- */
13496
- getExclusiveMcpServers() {
13497
- const servers = /* @__PURE__ */ new Set();
13498
- for (const agent of this.agents.values()) {
13499
- if (agent.exclusiveMcpServers) {
13500
- for (const s of agent.exclusiveMcpServers) {
13501
- servers.add(s);
13273
+ var generateMcpToolsFromCache = (serverName, cachedTools, callTool) => {
13274
+ const normalizedServerName = serverName.toLowerCase();
13275
+ const result = cachedTools.map((item) => {
13276
+ const { name: originalToolName, ...rest } = item;
13277
+ const namespacedToolName = `${normalizedServerName}__${originalToolName}`;
13278
+ const providerMetadata = getMcpProviderMetadata(normalizedServerName);
13279
+ const fallbackDescription = providerMetadata?.defaultToolDescriptions?.[originalToolName] ?? "";
13280
+ const parameters = normalizeToolParameters(rest);
13281
+ const optionTools = {
13282
+ toolFn: async (args) => {
13283
+ Logger.debug(`Calling ${originalToolName} tool via ${serverName}`, args);
13284
+ try {
13285
+ const toolResult = await callTool(originalToolName, args);
13286
+ const contentBlocks = toolResult?.content;
13287
+ if (Array.isArray(contentBlocks) && contentBlocks.length > 0) {
13288
+ const normalized = contentBlocks.map((entry) => {
13289
+ if (entry && typeof entry === "object" && "text" in entry) {
13290
+ return entry.text;
13291
+ }
13292
+ return JSON.stringify(entry);
13293
+ }).join("\n");
13294
+ Logger.debug(`[Tool Result] ${originalToolName}:`, normalized);
13295
+ return normalized;
13296
+ }
13297
+ const serialized = typeof toolResult === "string" ? toolResult : JSON.stringify(toolResult);
13298
+ Logger.debug(`[Tool Result] Unexpected format for ${originalToolName}, returning serialized output`);
13299
+ return serialized;
13300
+ } catch (error) {
13301
+ if (normalizedServerName === "atlassian") {
13302
+ const errorName = error instanceof Error ? error.name : "";
13303
+ const errorMessage = error instanceof Error ? error.message : String(error);
13304
+ const isTokenError = errorName === "AtlassianReconnectRequiredError" || errorMessage.includes("401") || errorMessage.includes("403") || errorMessage.includes("unauthorized") || errorMessage.includes("expired");
13305
+ if (isTokenError) {
13306
+ console.warn(`Atlassian token may be expired for tool ${originalToolName}, error:`, errorMessage);
13307
+ return ATLASSIAN_RECONNECT_MESSAGE;
13308
+ }
13309
+ }
13310
+ throw error;
13502
13311
  }
13312
+ },
13313
+ toolSchema: {
13314
+ name: namespacedToolName,
13315
+ description: rest.description || fallbackDescription,
13316
+ parameters
13503
13317
  }
13504
- }
13505
- return Array.from(servers);
13506
- }
13318
+ };
13319
+ return {
13320
+ name: namespacedToolName,
13321
+ ...optionTools,
13322
+ _isMcpTool: true
13323
+ };
13324
+ });
13325
+ Logger.debug(`\u{1F527} generateMcpToolsFromCache: Generated ${result.length} tool implementations for ${serverName} (from cache)`);
13326
+ return result;
13507
13327
  };
13508
- var serverAgentStore = new ServerAgentStore();
13328
+
13329
+ // ../../b4m-core/packages/services/dist/src/llm/agents/ServerSubagentOrchestrator.js
13330
+ var SUBAGENT_TIMEOUT_MS = 5 * 60 * 1e3;
13509
13331
 
13510
13332
  // ../../b4m-core/packages/services/dist/src/llm/ChatCompletionProcess.js
13511
13333
  import throttle2 from "lodash/throttle.js";
@@ -15238,6 +15060,12 @@ function substituteArguments(template, args) {
15238
15060
  return result;
15239
15061
  }
15240
15062
 
15063
+ // src/utils/mcpAdapter.ts
15064
+ import { createHash } from "crypto";
15065
+ import { readFile as readFile2, writeFile, mkdir } from "fs/promises";
15066
+ import { homedir as homedir4 } from "os";
15067
+ import { join as join3, dirname as dirname2 } from "path";
15068
+
15241
15069
  // ../../b4m-core/packages/mcp/dist/src/client.js
15242
15070
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
15243
15071
  import { Client as Client2 } from "@modelcontextprotocol/sdk/client/index.js";
@@ -15251,22 +15079,17 @@ var MCPClient = class {
15251
15079
  customCommand;
15252
15080
  customArgs;
15253
15081
  suppressStderr;
15082
+ onStderrLine;
15254
15083
  tools = [];
15255
15084
  serverName;
15256
- constructor({ envVariables, name, selectedRepositories, command, args, suppressStderr = false }) {
15085
+ constructor({ envVariables, name, command, args, suppressStderr = false, onStderrLine }) {
15257
15086
  this.mcp = new Client2({ name: "mcp-client-cli", version: "1.0.0" });
15258
15087
  this.envVariables = [...envVariables];
15259
15088
  this.serverName = name;
15260
15089
  this.customCommand = command;
15261
15090
  this.customArgs = args;
15262
15091
  this.suppressStderr = suppressStderr;
15263
- if (name === "github" && selectedRepositories) {
15264
- const reposJson = JSON.stringify(selectedRepositories);
15265
- this.envVariables.push({
15266
- key: "SELECTED_REPOSITORIES",
15267
- value: reposJson
15268
- });
15269
- }
15092
+ this.onStderrLine = onStderrLine;
15270
15093
  }
15271
15094
  async connectToServer() {
15272
15095
  try {
@@ -15305,6 +15128,7 @@ var MCPClient = class {
15305
15128
  args = [serverScriptPath];
15306
15129
  console.log(`[MCP] Using server: ${this.serverName} at ${serverScriptPath}`);
15307
15130
  }
15131
+ const stderrMode = this.suppressStderr ? "ignore" : this.onStderrLine ? "pipe" : void 0;
15308
15132
  const transportConfig = {
15309
15133
  command,
15310
15134
  args,
@@ -15312,14 +15136,16 @@ var MCPClient = class {
15312
15136
  ...Object.fromEntries(Object.entries(process.env).filter((entry) => entry[1] !== void 0)),
15313
15137
  ...envVarsObject
15314
15138
  },
15315
- // Suppress stderr in CLI context to prevent MCP server startup logs from breaking layout
15316
- ...this.suppressStderr && { stderr: "ignore" }
15139
+ ...stderrMode && { stderr: stderrMode }
15317
15140
  };
15318
15141
  this.transport = new StdioClientTransport(transportConfig);
15319
15142
  this.transport.onerror = (error) => {
15320
15143
  console.error(`[MCP] Transport error for ${this.serverName}:`, error);
15321
15144
  };
15322
15145
  await this.mcp.connect(this.transport);
15146
+ if (this.onStderrLine && this.transport.stderr) {
15147
+ this.readStderr(this.transport.stderr);
15148
+ }
15323
15149
  await new Promise((resolve3) => setTimeout(resolve3, 100));
15324
15150
  const toolsResult = await this.mcp.listTools();
15325
15151
  this.tools = toolsResult.tools.map((tool) => {
@@ -15341,6 +15167,30 @@ var MCPClient = class {
15341
15167
  throw e;
15342
15168
  }
15343
15169
  }
15170
+ /**
15171
+ * Read lines from the child process stderr stream and dispatch to the callback.
15172
+ * Uses Node.js stream events; errors are silently caught (stream closes on disconnect).
15173
+ */
15174
+ readStderr(stream) {
15175
+ let buffer = "";
15176
+ stream.setEncoding("utf8");
15177
+ stream.on("data", (chunk) => {
15178
+ buffer += chunk;
15179
+ const lines = buffer.split("\n");
15180
+ buffer = lines.pop() || "";
15181
+ for (const line of lines) {
15182
+ const trimmed = line.trim();
15183
+ if (trimmed)
15184
+ this.onStderrLine?.(trimmed);
15185
+ }
15186
+ });
15187
+ stream.on("end", () => {
15188
+ if (buffer.trim())
15189
+ this.onStderrLine?.(buffer.trim());
15190
+ });
15191
+ stream.on("error", () => {
15192
+ });
15193
+ }
15344
15194
  async callTool(toolName, toolArgs) {
15345
15195
  try {
15346
15196
  const args = toolArgs && typeof toolArgs === "object" ? toolArgs : void 0;
@@ -15376,14 +15226,30 @@ var MCPClient = class {
15376
15226
  };
15377
15227
 
15378
15228
  // src/utils/mcpAdapter.ts
15229
+ var CACHE_VERSION = 1;
15230
+ var CACHE_FILE = join3(homedir4(), ".bike4mind", "mcp-schema-cache.json");
15379
15231
  var McpManager = class {
15380
15232
  constructor(config) {
15381
15233
  this.servers = /* @__PURE__ */ new Map();
15382
15234
  this.connectionStates = /* @__PURE__ */ new Map();
15235
+ /** Per-server deferred promise resolved once the background connection is ready */
15236
+ this.connectionReady = /* @__PURE__ */ new Map();
15237
+ /**
15238
+ * Serializes background cache saves so concurrent writes don't race.
15239
+ * JSON.stringify is evaluated at run-time, always capturing the latest cache state.
15240
+ */
15241
+ this.backgroundSaveQueue = Promise.resolve();
15383
15242
  this.config = config;
15384
15243
  }
15244
+ /** Subscribe to background connection state changes for live UI updates. */
15245
+ setOnStateChange(callback) {
15246
+ this.onStateChange = callback;
15247
+ }
15385
15248
  /**
15386
- * Initialize and connect to all enabled MCP servers
15249
+ * Initialize MCP servers with schema caching.
15250
+ *
15251
+ * - Cache hit → tools registered immediately from cache; server connected in background
15252
+ * - Cache miss → server connected eagerly (blocks); result cached for next run
15387
15253
  */
15388
15254
  async initialize() {
15389
15255
  const enabledServers = this.config.mcpServers.filter((s) => s.enabled);
@@ -15392,47 +15258,96 @@ var McpManager = class {
15392
15258
  return;
15393
15259
  }
15394
15260
  logger.debug(`\u{1F4E1} Initializing ${enabledServers.length} MCP server(s)...`);
15395
- const results = await Promise.allSettled(enabledServers.map((serverConfig) => this.connectServer(serverConfig)));
15396
- const successful = results.filter((r) => r.status === "fulfilled").length;
15397
- const failed = results.filter((r) => r.status === "rejected").length;
15398
- if (successful > 0) {
15399
- logger.debug(`\u2705 Connected to ${successful} MCP server(s)`);
15261
+ const cache = await this.loadCache();
15262
+ const configuredNames = new Set(this.config.mcpServers.map((s) => s.name));
15263
+ let pruned = false;
15264
+ for (const name of Object.keys(cache.servers)) {
15265
+ if (!configuredNames.has(name)) {
15266
+ delete cache.servers[name];
15267
+ pruned = true;
15268
+ }
15269
+ }
15270
+ const eagerConnections = [];
15271
+ for (const serverConfig of enabledServers) {
15272
+ const configHash = this.hashServerConfig(serverConfig);
15273
+ const cachedEntry = cache.servers[serverConfig.name];
15274
+ if (cachedEntry && cachedEntry.configHash === configHash) {
15275
+ this.registerFromCache(serverConfig, cachedEntry);
15276
+ this.connectBackground(serverConfig, cache);
15277
+ } else {
15278
+ eagerConnections.push(this.connectEager(serverConfig, cache));
15279
+ }
15280
+ }
15281
+ if (eagerConnections.length > 0) {
15282
+ await Promise.allSettled(eagerConnections);
15283
+ await this.saveCache(cache);
15284
+ } else if (pruned) {
15285
+ await this.saveCache(cache);
15400
15286
  }
15401
- if (failed > 0) {
15402
- logger.debug(`\u26A0\uFE0F Failed to connect to ${failed} MCP server(s)`);
15287
+ const connected = [...this.connectionStates.values()].filter((s) => s === "connected").length;
15288
+ const pending = [...this.connectionStates.values()].filter((s) => s === "connecting").length;
15289
+ if (connected > 0 || pending > 0) {
15290
+ logger.debug(`\u2705 ${connected} MCP server(s) ready${pending > 0 ? `, ${pending} connecting in background` : ""}`);
15403
15291
  }
15404
15292
  }
15293
+ // ─── Private: connection strategies ────────────────────────────────────────
15405
15294
  /**
15406
- * Connect to a single MCP server
15295
+ * Register tool stubs from cache immediately. callTool lazily awaits the
15296
+ * background connection before forwarding to the real client.
15407
15297
  */
15408
- async connectServer(serverConfig) {
15298
+ registerFromCache(serverConfig, entry) {
15409
15299
  this.connectionStates.set(serverConfig.name, "connecting");
15300
+ let resolveReady;
15301
+ let rejectReady;
15302
+ const promise = new Promise((res, rej) => {
15303
+ resolveReady = res;
15304
+ rejectReady = rej;
15305
+ });
15306
+ promise.catch(() => {
15307
+ });
15308
+ this.connectionReady.set(serverConfig.name, { resolve: resolveReady, reject: rejectReady, promise });
15309
+ const callTool = async (name, args) => {
15310
+ await promise;
15311
+ return this.servers.get(serverConfig.name).client.callTool(name, args);
15312
+ };
15313
+ const tools = generateMcpToolsFromCache(serverConfig.name, entry.tools, callTool);
15314
+ this.servers.set(serverConfig.name, { name: serverConfig.name, client: null, tools });
15315
+ logger.debug(`\u{1F4CB} ${entry.tools.length} tools for ${serverConfig.name} loaded from cache`);
15316
+ }
15317
+ /**
15318
+ * Spawn the server process in the background. Resolves the per-server
15319
+ * deferred promise when ready so pending callTool invocations can proceed.
15320
+ */
15321
+ connectBackground(serverConfig, cache) {
15322
+ this.doConnect(serverConfig).then(({ client }) => {
15323
+ const instance = this.servers.get(serverConfig.name);
15324
+ if (instance) {
15325
+ instance.client = client;
15326
+ }
15327
+ this.connectionStates.set(serverConfig.name, "connected");
15328
+ this.connectionReady.get(serverConfig.name)?.resolve();
15329
+ this.onStateChange?.();
15330
+ logger.debug(`\u2705 Background connection to ${serverConfig.name} established`);
15331
+ this.writeCacheEntry(cache, serverConfig, client.tools);
15332
+ this.scheduleBackgroundSave(cache);
15333
+ }).catch((err) => {
15334
+ this.connectionStates.set(serverConfig.name, "failed");
15335
+ this.connectionReady.get(serverConfig.name)?.reject(err);
15336
+ this.onStateChange?.();
15337
+ logger.debug(`\u274C Background connection to ${serverConfig.name} failed: ${err}`);
15338
+ });
15339
+ }
15340
+ /**
15341
+ * Connect eagerly (blocks initialize). Populates servers map and updates cache.
15342
+ */
15343
+ async connectEager(serverConfig, cache) {
15344
+ this.connectionStates.set(serverConfig.name, "connecting");
15345
+ logger.debug(`\u{1F504} Connecting to ${serverConfig.name}...`);
15410
15346
  try {
15411
- logger.debug(`\u{1F504} Connecting to ${serverConfig.name}...`);
15412
- const envVariables = Object.entries(serverConfig.env).map(([key, value]) => ({
15413
- key,
15414
- value
15415
- }));
15416
- const client = new MCPClient({
15417
- envVariables,
15418
- name: serverConfig.name,
15419
- command: serverConfig.command,
15420
- args: serverConfig.args,
15421
- suppressStderr: true
15422
- });
15423
- await client.connectToServer();
15424
- const mcpData = {
15425
- serverName: serverConfig.name,
15426
- getTools: async () => client.tools,
15427
- callTool: async (name, args) => client.callTool(name, args)
15428
- };
15429
- const tools = await generateMcpTools(mcpData);
15430
- this.servers.set(serverConfig.name, {
15431
- name: serverConfig.name,
15432
- client,
15433
- tools
15434
- });
15347
+ const { client, tools } = await this.doConnect(serverConfig);
15348
+ this.servers.set(serverConfig.name, { name: serverConfig.name, client, tools });
15435
15349
  this.connectionStates.set(serverConfig.name, "connected");
15350
+ this.writeCacheEntry(cache, serverConfig, client.tools);
15436
15351
  logger.debug(`\u2705 Connected to ${serverConfig.name} (${tools.length} tools)`);
15437
15352
  } catch (error) {
15438
15353
  this.connectionStates.set(serverConfig.name, "failed");
@@ -15440,8 +15355,82 @@ var McpManager = class {
15440
15355
  }
15441
15356
  }
15442
15357
  /**
15443
- * Get all tools from all connected MCP servers
15358
+ * Shared: spawn and handshake with an MCP server process.
15359
+ */
15360
+ async doConnect(serverConfig) {
15361
+ const envVariables = Object.entries(serverConfig.env).map(([key, value]) => ({ key, value }));
15362
+ const client = new MCPClient({
15363
+ envVariables,
15364
+ name: serverConfig.name,
15365
+ command: serverConfig.command,
15366
+ args: serverConfig.args,
15367
+ suppressStderr: true
15368
+ });
15369
+ await client.connectToServer();
15370
+ const mcpData = {
15371
+ serverName: serverConfig.name,
15372
+ getTools: async () => client.tools,
15373
+ // any: generateMcpTools accepts a loose duck-type; MCPClient.callTool return type
15374
+ // doesn't match the internal interface exactly, but runtime behaviour is correct.
15375
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
15376
+ callTool: async (name, args) => client.callTool(name, args)
15377
+ };
15378
+ const tools = await generateMcpTools(mcpData);
15379
+ return { client, tools };
15380
+ }
15381
+ // ─── Private: cache helpers ──────────────────────────────────────────────────
15382
+ async loadCache() {
15383
+ try {
15384
+ const raw = await readFile2(CACHE_FILE, "utf-8");
15385
+ const parsed = JSON.parse(raw);
15386
+ if (parsed.version === CACHE_VERSION) {
15387
+ return parsed;
15388
+ }
15389
+ } catch {
15390
+ }
15391
+ return { version: CACHE_VERSION, servers: {} };
15392
+ }
15393
+ async saveCache(cache) {
15394
+ try {
15395
+ await mkdir(dirname2(CACHE_FILE), { recursive: true });
15396
+ await writeFile(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
15397
+ } catch (error) {
15398
+ logger.debug(`\u26A0\uFE0F Failed to save MCP schema cache: ${error}`);
15399
+ }
15400
+ }
15401
+ /**
15402
+ * Enqueue a background cache save. Saves are serialized so concurrent background
15403
+ * connections can't interleave writes and lose each other's entries. JSON.stringify
15404
+ * is evaluated when the queued write runs, so it always captures the latest state.
15444
15405
  */
15406
+ scheduleBackgroundSave(cache) {
15407
+ this.backgroundSaveQueue = this.backgroundSaveQueue.then(() => this.saveCache(cache)).catch(() => {
15408
+ });
15409
+ }
15410
+ writeCacheEntry(cache, serverConfig, rawTools) {
15411
+ cache.servers[serverConfig.name] = {
15412
+ configHash: this.hashServerConfig(serverConfig),
15413
+ tools: rawTools.map((t) => {
15414
+ const tool = t;
15415
+ return {
15416
+ name: t.name,
15417
+ description: tool.description,
15418
+ // Normalize both MCP SDK casing variants to input_schema for storage
15419
+ input_schema: tool.inputSchema ?? tool.input_schema
15420
+ };
15421
+ }),
15422
+ cachedAt: (/* @__PURE__ */ new Date()).toISOString()
15423
+ };
15424
+ }
15425
+ hashServerConfig(serverConfig) {
15426
+ const key = JSON.stringify({
15427
+ command: serverConfig.command,
15428
+ args: serverConfig.args,
15429
+ env: serverConfig.env
15430
+ });
15431
+ return createHash("sha256").update(key).digest("hex").slice(0, 16);
15432
+ }
15433
+ // ─── Public API ───────────────────────────────────────────────────────────────
15445
15434
  getTools() {
15446
15435
  const allTools = [];
15447
15436
  for (const server of this.servers.values()) {
@@ -15449,24 +15438,17 @@ var McpManager = class {
15449
15438
  }
15450
15439
  return allTools;
15451
15440
  }
15452
- /**
15453
- * Get tool count by server
15454
- */
15455
15441
  getToolCount() {
15456
15442
  return Array.from(this.servers.values()).map((server) => ({
15457
15443
  serverName: server.name,
15458
15444
  count: server.tools.length
15459
15445
  }));
15460
15446
  }
15461
- /**
15462
- * Disconnect from all MCP servers
15463
- */
15464
15447
  async disconnect() {
15465
- if (this.servers.size === 0) {
15466
- return;
15467
- }
15448
+ if (this.servers.size === 0) return;
15468
15449
  logger.debug(`\u{1F50C} Disconnecting from ${this.servers.size} MCP server(s)...`);
15469
15450
  const disconnectPromises = Array.from(this.servers.values()).map(async (server) => {
15451
+ if (!server.client) return;
15470
15452
  try {
15471
15453
  await server.client.disconnect();
15472
15454
  logger.debug(`\u2705 Disconnected from ${server.name}`);
@@ -15476,27 +15458,17 @@ var McpManager = class {
15476
15458
  });
15477
15459
  await Promise.allSettled(disconnectPromises);
15478
15460
  this.servers.clear();
15461
+ this.connectionReady.clear();
15479
15462
  }
15480
- /**
15481
- * Check if any MCP servers are connected
15482
- */
15483
15463
  hasServers() {
15484
15464
  return this.servers.size > 0;
15485
15465
  }
15486
- /**
15487
- * Get list of connected server names
15488
- */
15489
15466
  getServerNames() {
15490
15467
  return Array.from(this.servers.keys());
15491
15468
  }
15492
- /**
15493
- * Get connection state for a server
15494
- */
15495
15469
  getConnectionState(serverName) {
15496
15470
  const serverConfig = this.config.mcpServers.find((s) => s.name === serverName);
15497
- if (!serverConfig?.enabled) {
15498
- return "disabled";
15499
- }
15471
+ if (!serverConfig?.enabled) return "disabled";
15500
15472
  return this.connectionStates.get(serverName) || "connecting";
15501
15473
  }
15502
15474
  };
@@ -15904,6 +15876,9 @@ var ServerLlmBackend = class {
15904
15876
  });
15905
15877
  });
15906
15878
  }
15879
+ pushToolMessages(_messages, _tool, _result) {
15880
+ throw new Error("ServerLlmBackend does not support pushToolMessages \u2014 tools are executed server-side");
15881
+ }
15907
15882
  /**
15908
15883
  * Get available models from server
15909
15884
  * Fetches from /api/models and filters for CLI-compatible models
@@ -16123,6 +16098,9 @@ var WebSocketLlmBackend = class {
16123
16098
  });
16124
16099
  });
16125
16100
  }
16101
+ pushToolMessages(_messages, _tool, _result) {
16102
+ throw new Error("WebSocketLlmBackend does not support pushToolMessages \u2014 tools are executed server-side");
16103
+ }
16126
16104
  /**
16127
16105
  * Get available models from server (REST call, not streaming).
16128
16106
  * Delegates to ApiClient -- same as ServerLlmBackend.
@@ -16163,6 +16141,9 @@ var WebSocketLlmBackend = class {
16163
16141
  };
16164
16142
 
16165
16143
  // src/ws/WebSocketConnectionManager.ts
16144
+ import WsWebSocket from "ws";
16145
+ var useWsPolyfill = typeof globalThis.WebSocket === "undefined";
16146
+ var WS = useWsPolyfill ? WsWebSocket : globalThis.WebSocket;
16166
16147
  var WebSocketConnectionManager = class {
16167
16148
  constructor(wsUrl, getToken) {
16168
16149
  this.ws = null;
@@ -16170,6 +16151,7 @@ var WebSocketConnectionManager = class {
16170
16151
  this.reconnectAttempts = 0;
16171
16152
  this.maxReconnectDelay = 3e4;
16172
16153
  this.handlers = /* @__PURE__ */ new Map();
16154
+ this.actionHandlers = /* @__PURE__ */ new Map();
16173
16155
  this.disconnectHandlers = /* @__PURE__ */ new Set();
16174
16156
  this.reconnectTimer = null;
16175
16157
  this.connected = false;
@@ -16192,7 +16174,13 @@ var WebSocketConnectionManager = class {
16192
16174
  }
16193
16175
  return new Promise((resolve3, reject) => {
16194
16176
  logger.debug(`[WS] Connecting to ${this.wsUrl}...`);
16195
- this.ws = new WebSocket(this.wsUrl, [`access_token.${token}`]);
16177
+ if (useWsPolyfill) {
16178
+ this.ws = new WsWebSocket(this.wsUrl, {
16179
+ headers: { "Sec-WebSocket-Protocol": `access_token.${token}` }
16180
+ });
16181
+ } else {
16182
+ this.ws = new WS(this.wsUrl, [`access_token.${token}`]);
16183
+ }
16196
16184
  this.ws.onopen = () => {
16197
16185
  logger.debug("[WS] Connected");
16198
16186
  this.connected = true;
@@ -16209,7 +16197,12 @@ var WebSocketConnectionManager = class {
16209
16197
  if (requestId && this.handlers.has(requestId)) {
16210
16198
  this.handlers.get(requestId)(message);
16211
16199
  } else {
16212
- logger.debug(`[WS] Unhandled message: ${message.action || "unknown"}`);
16200
+ const action = message.action;
16201
+ if (action && this.actionHandlers.has(action)) {
16202
+ this.actionHandlers.get(action)(message);
16203
+ } else {
16204
+ logger.debug(`[WS] Unhandled message: ${action || "unknown"}`);
16205
+ }
16213
16206
  }
16214
16207
  } catch (err) {
16215
16208
  logger.debug(`[WS] Failed to parse message: ${err}`);
@@ -16224,11 +16217,13 @@ var WebSocketConnectionManager = class {
16224
16217
  }
16225
16218
  };
16226
16219
  this.ws.onerror = (err) => {
16227
- logger.debug(`[WS] Error: ${err}`);
16220
+ const underlying = err.error;
16221
+ const detail = underlying?.message || String(err);
16222
+ logger.debug(`[WS] Error: ${detail}`);
16228
16223
  if (this.connecting) {
16229
16224
  this.connecting = false;
16230
16225
  this.connected = false;
16231
- reject(new Error("WebSocket connection failed"));
16226
+ reject(new Error(`WebSocket connection failed: ${detail}`));
16232
16227
  }
16233
16228
  };
16234
16229
  });
@@ -16241,7 +16236,7 @@ var WebSocketConnectionManager = class {
16241
16236
  * Send a JSON message over the WebSocket connection.
16242
16237
  */
16243
16238
  send(data) {
16244
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
16239
+ if (!this.ws || this.ws.readyState !== WS.OPEN) {
16245
16240
  throw new Error("WebSocket is not connected");
16246
16241
  }
16247
16242
  const payload = JSON.stringify(data);
@@ -16264,6 +16259,19 @@ var WebSocketConnectionManager = class {
16264
16259
  offRequest(requestId) {
16265
16260
  this.handlers.delete(requestId);
16266
16261
  }
16262
+ /**
16263
+ * Register a handler for messages matching a specific action type.
16264
+ * Used for server-pushed commands like keep_command.
16265
+ */
16266
+ onAction(action, handler) {
16267
+ this.actionHandlers.set(action, handler);
16268
+ }
16269
+ /**
16270
+ * Remove a handler for a specific action type.
16271
+ */
16272
+ offAction(action) {
16273
+ this.actionHandlers.delete(action);
16274
+ }
16267
16275
  /**
16268
16276
  * Register a handler that fires when the connection drops.
16269
16277
  */
@@ -16287,13 +16295,14 @@ var WebSocketConnectionManager = class {
16287
16295
  this.ws = null;
16288
16296
  }
16289
16297
  this.handlers.clear();
16298
+ this.actionHandlers.clear();
16290
16299
  this.disconnectHandlers.clear();
16291
16300
  }
16292
16301
  startHeartbeat() {
16293
16302
  this.stopHeartbeat();
16294
16303
  this.heartbeatInterval = setInterval(
16295
16304
  () => {
16296
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
16305
+ if (this.ws && this.ws.readyState === WS.OPEN) {
16297
16306
  this.ws.send(JSON.stringify({ action: "heartbeat" }));
16298
16307
  logger.debug("[WS] Heartbeat sent");
16299
16308
  }