@bike4mind/cli 0.2.29-feat-session-permission.18877 → 0.2.29-feat-cli-websocket-streaming.18878

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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  CurationArtifactType
4
- } from "./chunk-5GSDPBTM.js";
4
+ } from "./chunk-KPOK6V6E.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;
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  BadRequestError,
4
4
  secureParameters
5
- } from "./chunk-S7BPPS33.js";
5
+ } from "./chunk-TY6Z2UV2.js";
6
6
  import {
7
7
  CompletionApiUsageTransaction,
8
8
  GenericCreditDeductTransaction,
@@ -12,7 +12,7 @@ import {
12
12
  TextGenerationUsageTransaction,
13
13
  TransferCreditTransaction,
14
14
  VideoGenerationUsageTransaction
15
- } from "./chunk-5GSDPBTM.js";
15
+ } from "./chunk-KPOK6V6E.js";
16
16
 
17
17
  // ../../b4m-core/packages/services/dist/src/creditService/subtractCredits.js
18
18
  import { z } from "zod";
@@ -7,11 +7,11 @@ import {
7
7
  getSettingsMap,
8
8
  getSettingsValue,
9
9
  secureParameters
10
- } from "./chunk-S7BPPS33.js";
10
+ } from "./chunk-TY6Z2UV2.js";
11
11
  import {
12
12
  KnowledgeType,
13
13
  SupportedFabFileMimeTypes
14
- } from "./chunk-5GSDPBTM.js";
14
+ } from "./chunk-KPOK6V6E.js";
15
15
 
16
16
  // ../../b4m-core/packages/services/dist/src/fabFileService/create.js
17
17
  import { z } from "zod";
@@ -1012,6 +1012,29 @@ var VoiceSessionSendTranscriptAction = z10.object({
1012
1012
  conversationItemId: z10.string(),
1013
1013
  timestamp: z10.coerce.date().optional()
1014
1014
  });
1015
+ var CliCompletionRequestAction = z10.object({
1016
+ action: z10.literal("cli_completion_request"),
1017
+ accessToken: z10.string(),
1018
+ requestId: z10.string().uuid(),
1019
+ model: z10.string(),
1020
+ messages: z10.array(z10.object({
1021
+ role: z10.enum(["user", "assistant", "system"]),
1022
+ content: z10.union([z10.string(), z10.array(z10.unknown())])
1023
+ })),
1024
+ options: z10.object({
1025
+ temperature: z10.number().optional(),
1026
+ maxTokens: z10.number().optional(),
1027
+ stream: z10.boolean().optional(),
1028
+ tools: z10.array(z10.unknown()).optional()
1029
+ }).optional()
1030
+ });
1031
+ var CliToolRequestAction = z10.object({
1032
+ action: z10.literal("cli_tool_request"),
1033
+ accessToken: z10.string(),
1034
+ requestId: z10.string().uuid(),
1035
+ toolName: z10.string(),
1036
+ input: z10.record(z10.unknown())
1037
+ });
1015
1038
  var VoiceSessionEndedAction = z10.object({
1016
1039
  action: z10.literal("voice_session_ended"),
1017
1040
  userId: z10.string(),
@@ -1258,6 +1281,36 @@ var ResearchModeStreamAction = z10.object({
1258
1281
  completionInfo: z10.any().optional()
1259
1282
  }).optional()
1260
1283
  });
1284
+ var CliCompletionChunkAction = z10.object({
1285
+ action: z10.literal("cli_completion_chunk"),
1286
+ requestId: z10.string(),
1287
+ chunk: z10.object({
1288
+ type: z10.enum(["content", "tool_use"]),
1289
+ text: z10.string(),
1290
+ tools: z10.array(z10.unknown()).optional(),
1291
+ usage: z10.object({
1292
+ inputTokens: z10.number().optional(),
1293
+ outputTokens: z10.number().optional()
1294
+ }).optional(),
1295
+ thinking: z10.array(z10.unknown()).optional()
1296
+ })
1297
+ });
1298
+ var CliCompletionDoneAction = z10.object({
1299
+ action: z10.literal("cli_completion_done"),
1300
+ requestId: z10.string()
1301
+ });
1302
+ var CliCompletionErrorAction = z10.object({
1303
+ action: z10.literal("cli_completion_error"),
1304
+ requestId: z10.string(),
1305
+ error: z10.string()
1306
+ });
1307
+ var CliToolResponseAction = z10.object({
1308
+ action: z10.literal("cli_tool_response"),
1309
+ requestId: z10.string(),
1310
+ success: z10.boolean(),
1311
+ content: z10.unknown().optional(),
1312
+ error: z10.string().optional()
1313
+ });
1261
1314
  var SessionCreatedAction = shareableDocumentSchema.extend({
1262
1315
  action: z10.literal("session.created"),
1263
1316
  id: z10.string(),
@@ -1292,7 +1345,9 @@ var MessageDataToServer = z10.discriminatedUnion("action", [
1292
1345
  DataUnsubscribeRequestAction,
1293
1346
  HeartbeatAction,
1294
1347
  VoiceSessionSendTranscriptAction,
1295
- VoiceSessionEndedAction
1348
+ VoiceSessionEndedAction,
1349
+ CliCompletionRequestAction,
1350
+ CliToolRequestAction
1296
1351
  ]);
1297
1352
  var MessageDataToClient = z10.discriminatedUnion("action", [
1298
1353
  DataSubscriptionUpdateAction,
@@ -1314,7 +1369,11 @@ var MessageDataToClient = z10.discriminatedUnion("action", [
1314
1369
  SpiderProgressUpdateAction,
1315
1370
  SpiderCompleteAction,
1316
1371
  SpiderErrorAction,
1317
- SessionCreatedAction
1372
+ SessionCreatedAction,
1373
+ CliCompletionChunkAction,
1374
+ CliCompletionDoneAction,
1375
+ CliCompletionErrorAction,
1376
+ CliToolResponseAction
1318
1377
  ]);
1319
1378
 
1320
1379
  // ../../b4m-core/packages/common/dist/src/schemas/cliCompletions.js
@@ -7926,6 +7985,8 @@ export {
7926
7985
  DataUnsubscribeRequestAction,
7927
7986
  HeartbeatAction,
7928
7987
  VoiceSessionSendTranscriptAction,
7988
+ CliCompletionRequestAction,
7989
+ CliToolRequestAction,
7929
7990
  VoiceSessionEndedAction,
7930
7991
  DataSubscriptionUpdateAction,
7931
7992
  LLMStatusUpdateAction,
@@ -7946,6 +8007,10 @@ export {
7946
8007
  UpdateResearchTaskStatusAction,
7947
8008
  ImportHistoryJobProgressUpdateAction,
7948
8009
  ResearchModeStreamAction,
8010
+ CliCompletionChunkAction,
8011
+ CliCompletionDoneAction,
8012
+ CliCompletionErrorAction,
8013
+ CliToolResponseAction,
7949
8014
  SessionCreatedAction,
7950
8015
  MessageDataToServer,
7951
8016
  MessageDataToClient,
@@ -6,12 +6,12 @@ import {
6
6
  getSettingsByNames,
7
7
  obfuscateApiKey,
8
8
  secureParameters
9
- } from "./chunk-S7BPPS33.js";
9
+ } from "./chunk-TY6Z2UV2.js";
10
10
  import {
11
11
  ApiKeyType,
12
12
  MementoTier,
13
13
  isSupportedEmbeddingModel
14
- } from "./chunk-5GSDPBTM.js";
14
+ } from "./chunk-KPOK6V6E.js";
15
15
 
16
16
  // ../../b4m-core/packages/services/dist/src/apiKeyService/get.js
17
17
  import { z } from "zod";
@@ -15,7 +15,7 @@ import {
15
15
  dayjsConfig_default,
16
16
  extractSnippetMeta,
17
17
  settingsMap
18
- } from "./chunk-5GSDPBTM.js";
18
+ } from "./chunk-KPOK6V6E.js";
19
19
  import {
20
20
  Logger
21
21
  } from "./chunk-OCYRD7D6.js";
@@ -2,9 +2,9 @@
2
2
  import {
3
3
  createFabFile,
4
4
  createFabFileSchema
5
- } from "./chunk-ELTTBWAQ.js";
6
- import "./chunk-S7BPPS33.js";
7
- import "./chunk-5GSDPBTM.js";
5
+ } from "./chunk-EC2VEDD2.js";
6
+ import "./chunk-TY6Z2UV2.js";
7
+ import "./chunk-KPOK6V6E.js";
8
8
  import "./chunk-OCYRD7D6.js";
9
9
  export {
10
10
  createFabFile,
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  getEffectiveApiKey,
6
6
  getOpenWeatherKey,
7
7
  getSerperKey
8
- } from "./chunk-BCOGDPUB.js";
8
+ } from "./chunk-QQL3OBKL.js";
9
9
  import {
10
10
  ConfigStore,
11
11
  logger
@@ -14,8 +14,8 @@ import {
14
14
  selectActiveBackgroundAgents,
15
15
  useCliStore
16
16
  } from "./chunk-TVW4ZESU.js";
17
- import "./chunk-PKKJ44C5.js";
18
- import "./chunk-ELTTBWAQ.js";
17
+ import "./chunk-BXCARBUQ.js";
18
+ import "./chunk-EC2VEDD2.js";
19
19
  import {
20
20
  BFLImageService,
21
21
  BaseStorage,
@@ -27,7 +27,7 @@ import {
27
27
  OpenAIBackend,
28
28
  OpenAIImageService,
29
29
  XAIImageService
30
- } from "./chunk-S7BPPS33.js";
30
+ } from "./chunk-TY6Z2UV2.js";
31
31
  import {
32
32
  AiEvents,
33
33
  ApiKeyEvents,
@@ -83,7 +83,7 @@ import {
83
83
  XAI_IMAGE_MODELS,
84
84
  b4mLLMTools,
85
85
  getMcpProviderMetadata
86
- } from "./chunk-5GSDPBTM.js";
86
+ } from "./chunk-KPOK6V6E.js";
87
87
  import {
88
88
  Logger
89
89
  } from "./chunk-OCYRD7D6.js";
@@ -93,7 +93,7 @@ import React21, { useState as useState9, useEffect as useEffect6, useCallback as
93
93
  import { render, Box as Box20, Text as Text20, useApp, useInput as useInput9 } from "ink";
94
94
  import { execSync } from "child_process";
95
95
  import { randomBytes as randomBytes5 } from "crypto";
96
- import { v4 as uuidv411 } from "uuid";
96
+ import { v4 as uuidv413 } from "uuid";
97
97
 
98
98
  // src/components/App.tsx
99
99
  import React15, { useState as useState5, useEffect as useEffect4 } from "react";
@@ -1260,12 +1260,10 @@ function PermissionPrompt({
1260
1260
  }) {
1261
1261
  const items = canBeTrusted ? [
1262
1262
  { label: "\u2713 Allow once", value: "allow-once" },
1263
- { label: "\u2713 Allow for this session", value: "allow-session" },
1264
1263
  { label: "\u2713 Always allow (trust this tool)", value: "allow-always" },
1265
1264
  { label: "\u2717 Deny", value: "deny" }
1266
1265
  ] : [
1267
1266
  { label: "\u2713 Allow once", value: "allow-once" },
1268
- { label: "\u2713 Allow for this session", value: "allow-session" },
1269
1267
  { label: "\u2717 Deny", value: "deny" }
1270
1268
  ];
1271
1269
  const [selectedIndex, setSelectedIndex] = useState3(0);
@@ -11511,6 +11509,10 @@ var ServerToolExecutor = class {
11511
11509
  };
11512
11510
 
11513
11511
  // src/llm/ToolRouter.ts
11512
+ var wsToolExecutor = null;
11513
+ function setWebSocketToolExecutor(executor) {
11514
+ wsToolExecutor = executor;
11515
+ }
11514
11516
  var SERVER_TOOLS = ["weather_info", "web_search", "web_fetch"];
11515
11517
  var LOCAL_TOOLS = [
11516
11518
  "file_read",
@@ -11533,7 +11535,15 @@ function isLocalTool(toolName) {
11533
11535
  }
11534
11536
  async function executeTool(toolName, input, apiClient, localToolFn) {
11535
11537
  if (isServerTool(toolName)) {
11536
- logger.debug(`[ToolRouter] Routing ${toolName} to server`);
11538
+ if (wsToolExecutor) {
11539
+ logger.debug(`[ToolRouter] Routing ${toolName} to server via WebSocket`);
11540
+ const result = await wsToolExecutor.execute(toolName, input);
11541
+ if (!result.success) {
11542
+ return `Error executing ${toolName}: ${result.error || "Tool execution failed"}`;
11543
+ }
11544
+ return typeof result.content === "string" ? result.content : JSON.stringify(result.content ?? "");
11545
+ }
11546
+ logger.debug(`[ToolRouter] Routing ${toolName} to server via HTTP`);
11537
11547
  const executor = new ServerToolExecutor(apiClient);
11538
11548
  return await executor.executeTool(toolName, input);
11539
11549
  } else if (isLocalTool(toolName)) {
@@ -11837,9 +11847,6 @@ function wrapToolWithPermission(tool, permissionManager, showPermissionPrompt, a
11837
11847
  if (response.action === "deny") {
11838
11848
  throw new PermissionDeniedError(toolName, args);
11839
11849
  }
11840
- if (response.action === "allow-session") {
11841
- permissionManager.trustToolForSession(toolName);
11842
- }
11843
11850
  if (response.action === "allow-always") {
11844
11851
  const canTrust = permissionManager.trustTool(toolName);
11845
11852
  if (canTrust) {
@@ -12044,7 +12051,6 @@ function generateCliTools(userId, llm, model, permissionManager, showPermissionP
12044
12051
  var PermissionManager = class {
12045
12052
  constructor(trustedTools = [], customCategories, deniedTools) {
12046
12053
  this.trustedTools = /* @__PURE__ */ new Set();
12047
- this.sessionTrustedTools = /* @__PURE__ */ new Set();
12048
12054
  this.deniedTools = /* @__PURE__ */ new Set();
12049
12055
  this.trustedTools = new Set(trustedTools);
12050
12056
  this.customCategories = new Map(Object.entries(customCategories || {}));
@@ -12070,9 +12076,6 @@ var PermissionManager = class {
12070
12076
  if (category === "auto_approve") {
12071
12077
  return false;
12072
12078
  }
12073
- if (this.sessionTrustedTools.has(toolName)) {
12074
- return false;
12075
- }
12076
12079
  if (category === "prompt_always") {
12077
12080
  return true;
12078
12081
  }
@@ -12140,29 +12143,6 @@ var PermissionManager = class {
12140
12143
  getDeniedTools() {
12141
12144
  return Array.from(this.deniedTools).sort();
12142
12145
  }
12143
- /**
12144
- * Trust a tool for the current session only (in-memory, no persistence)
12145
- * Works for all tool categories including prompt_always, but NOT project-denied tools
12146
- */
12147
- trustToolForSession(toolName) {
12148
- if (this.deniedTools.has(toolName)) {
12149
- return false;
12150
- }
12151
- this.sessionTrustedTools.add(toolName);
12152
- return true;
12153
- }
12154
- /**
12155
- * Check if a tool is trusted for the current session
12156
- */
12157
- isSessionTrusted(toolName) {
12158
- return this.sessionTrustedTools.has(toolName);
12159
- }
12160
- /**
12161
- * Clear all session-scoped trust (called on exit or session reset)
12162
- */
12163
- clearSessionTrust() {
12164
- this.sessionTrustedTools.clear();
12165
- }
12166
12146
  /**
12167
12147
  * Clear all trusted tools
12168
12148
  */
@@ -13493,6 +13473,168 @@ var ServerLlmBackend = class {
13493
13473
  }
13494
13474
  };
13495
13475
 
13476
+ // src/llm/WebSocketLlmBackend.ts
13477
+ import { v4 as uuidv411 } from "uuid";
13478
+ function stripThinkingBlocks2(text) {
13479
+ return text.replace(/<think>[\s\S]*?<\/think>/g, "").trim();
13480
+ }
13481
+ var WebSocketLlmBackend = class {
13482
+ constructor(options) {
13483
+ this.wsManager = options.wsManager;
13484
+ this.apiClient = options.apiClient;
13485
+ this.currentModel = options.model;
13486
+ this.tokenGetter = options.tokenGetter;
13487
+ }
13488
+ /**
13489
+ * Make LLM completion request via WebSocket.
13490
+ * Collects all streamed chunks, then calls callback once at completion
13491
+ * with the full accumulated content.
13492
+ */
13493
+ async complete(model, messages, options, callback) {
13494
+ logger.debug(`[WebSocketLlmBackend] Starting complete() with model: ${model}`);
13495
+ if (options.abortSignal?.aborted) {
13496
+ logger.debug("[WebSocketLlmBackend] Request aborted before start");
13497
+ return;
13498
+ }
13499
+ if (!this.wsManager.isConnected) {
13500
+ throw new Error("WebSocket is not connected");
13501
+ }
13502
+ const requestId = uuidv411();
13503
+ const token = await this.tokenGetter();
13504
+ if (!token) {
13505
+ throw new Error("No access token available");
13506
+ }
13507
+ return new Promise((resolve3, reject) => {
13508
+ const isVerbose = process.env.B4M_VERBOSE === "1";
13509
+ const isUltraVerbose = process.env.B4M_DEBUG_STREAM === "1";
13510
+ const streamLogger = new StreamLogger(logger, "WebSocketLlmBackend", isVerbose, isUltraVerbose);
13511
+ streamLogger.streamStart();
13512
+ let eventCount = 0;
13513
+ let accumulatedText = "";
13514
+ let lastUsageInfo = {};
13515
+ let toolsUsed = [];
13516
+ let thinkingBlocks = [];
13517
+ let settled = false;
13518
+ const settle = (action) => {
13519
+ if (settled) return;
13520
+ settled = true;
13521
+ this.wsManager.offRequest(requestId);
13522
+ action();
13523
+ };
13524
+ const settleResolve = () => settle(() => resolve3());
13525
+ const settleReject = (err) => settle(() => reject(err));
13526
+ if (options.abortSignal) {
13527
+ if (options.abortSignal.aborted) {
13528
+ settleResolve();
13529
+ return;
13530
+ }
13531
+ options.abortSignal.addEventListener(
13532
+ "abort",
13533
+ () => {
13534
+ logger.debug("[WebSocketLlmBackend] Abort signal received");
13535
+ settleResolve();
13536
+ },
13537
+ { once: true }
13538
+ );
13539
+ }
13540
+ const updateUsage = (usage) => {
13541
+ if (usage) {
13542
+ lastUsageInfo = { inputTokens: usage.inputTokens, outputTokens: usage.outputTokens };
13543
+ }
13544
+ };
13545
+ this.wsManager.onRequest(requestId, (message) => {
13546
+ if (options.abortSignal?.aborted) return;
13547
+ const action = message.action;
13548
+ if (action === "cli_completion_chunk") {
13549
+ eventCount++;
13550
+ const chunk = message.chunk;
13551
+ streamLogger.onEvent(eventCount, JSON.stringify(chunk));
13552
+ const textChunk = chunk.text || "";
13553
+ if (textChunk) accumulatedText += textChunk;
13554
+ updateUsage(chunk.usage);
13555
+ if (chunk.type === "content") {
13556
+ streamLogger.onContent(eventCount, textChunk, accumulatedText);
13557
+ } else if (chunk.type === "tool_use") {
13558
+ streamLogger.onCriticalEvent(eventCount, "TOOL_USE", `tools: ${chunk.tools?.length}`);
13559
+ if (chunk.tools && chunk.tools.length > 0) toolsUsed = chunk.tools;
13560
+ if (chunk.thinking && chunk.thinking.length > 0) thinkingBlocks = chunk.thinking;
13561
+ }
13562
+ } else if (action === "cli_completion_done") {
13563
+ streamLogger.streamComplete(accumulatedText);
13564
+ const cleanedText = stripThinkingBlocks2(accumulatedText);
13565
+ if (!cleanedText && toolsUsed.length === 0) {
13566
+ settleResolve();
13567
+ return;
13568
+ }
13569
+ const info = {
13570
+ ...lastUsageInfo,
13571
+ ...toolsUsed.length > 0 && { toolsUsed },
13572
+ ...thinkingBlocks.length > 0 && { thinking: thinkingBlocks }
13573
+ };
13574
+ callback([cleanedText], info).then(() => settleResolve()).catch((err) => settleReject(err));
13575
+ } else if (action === "cli_completion_error") {
13576
+ const errorMsg = message.error || "Server error";
13577
+ streamLogger.onCriticalEvent(eventCount, "ERROR", errorMsg);
13578
+ settleReject(new Error(errorMsg));
13579
+ }
13580
+ });
13581
+ try {
13582
+ this.wsManager.send({
13583
+ action: "cli_completion_request",
13584
+ accessToken: token,
13585
+ requestId,
13586
+ model,
13587
+ messages,
13588
+ options: {
13589
+ temperature: options.temperature,
13590
+ maxTokens: options.maxTokens,
13591
+ stream: true,
13592
+ tools: options.tools || []
13593
+ }
13594
+ });
13595
+ } catch (err) {
13596
+ settleReject(err instanceof Error ? err : new Error(String(err)));
13597
+ }
13598
+ });
13599
+ }
13600
+ /**
13601
+ * Get available models from server (REST call, not streaming).
13602
+ * Delegates to ApiClient -- same as ServerLlmBackend.
13603
+ */
13604
+ async getModelInfo() {
13605
+ try {
13606
+ logger.debug("[WebSocketLlmBackend] Fetching models from /api/models");
13607
+ const response = await this.apiClient.get("/api/models");
13608
+ if (!response || typeof response !== "object" || !Array.isArray(response.models)) {
13609
+ logger.warn("[WebSocketLlmBackend] Invalid API response format, using fallback models");
13610
+ return this.getFallbackModels();
13611
+ }
13612
+ const filteredModels = response.models.filter(
13613
+ (model) => model.type === "text" && model.supportsTools === true
13614
+ );
13615
+ if (filteredModels.length === 0) {
13616
+ logger.warn("[WebSocketLlmBackend] No CLI-compatible models found, using fallback");
13617
+ return this.getFallbackModels();
13618
+ }
13619
+ logger.debug(`[WebSocketLlmBackend] Loaded ${filteredModels.length} models`);
13620
+ return filteredModels;
13621
+ } catch (error) {
13622
+ logger.warn(
13623
+ `[WebSocketLlmBackend] Failed to fetch models: ${error instanceof Error ? error.message : String(error)}`
13624
+ );
13625
+ return this.getFallbackModels();
13626
+ }
13627
+ }
13628
+ getFallbackModels() {
13629
+ return [
13630
+ { id: "claude-sonnet-4-5-20250929", name: "Claude 4.5 Sonnet" },
13631
+ { id: "claude-3-5-haiku-20241022", name: "Claude 3.5 Haiku" },
13632
+ { id: "gpt-4o", name: "GPT-4o" },
13633
+ { id: "gpt-4o-mini", name: "GPT-4o Mini" }
13634
+ ];
13635
+ }
13636
+ };
13637
+
13496
13638
  // src/llm/NotifyingLlmBackend.ts
13497
13639
  var NotifyingLlmBackend = class {
13498
13640
  constructor(inner, backgroundManager) {
@@ -13527,6 +13669,214 @@ Please acknowledge these background agent results and incorporate them into your
13527
13669
  }
13528
13670
  };
13529
13671
 
13672
+ // src/ws/WebSocketConnectionManager.ts
13673
+ var WebSocketConnectionManager = class {
13674
+ constructor(wsUrl, getToken) {
13675
+ this.ws = null;
13676
+ this.heartbeatInterval = null;
13677
+ this.reconnectAttempts = 0;
13678
+ this.maxReconnectDelay = 3e4;
13679
+ this.handlers = /* @__PURE__ */ new Map();
13680
+ this.connected = false;
13681
+ this.connecting = false;
13682
+ this.closed = false;
13683
+ this.wsUrl = wsUrl;
13684
+ this.getToken = getToken;
13685
+ }
13686
+ /**
13687
+ * Connect to the WebSocket server.
13688
+ * Resolves when connection is established, rejects on failure.
13689
+ */
13690
+ async connect() {
13691
+ if (this.connected || this.connecting) return;
13692
+ this.connecting = true;
13693
+ const token = await this.getToken();
13694
+ if (!token) {
13695
+ this.connecting = false;
13696
+ throw new Error("No access token available for WebSocket connection");
13697
+ }
13698
+ return new Promise((resolve3, reject) => {
13699
+ const url = `${this.wsUrl}?token=${token}`;
13700
+ logger.debug(`[WS] Connecting to ${this.wsUrl}...`);
13701
+ this.ws = new WebSocket(url);
13702
+ this.ws.onopen = () => {
13703
+ logger.debug("[WS] Connected");
13704
+ this.connected = true;
13705
+ this.connecting = false;
13706
+ this.reconnectAttempts = 0;
13707
+ this.startHeartbeat();
13708
+ resolve3();
13709
+ };
13710
+ this.ws.onmessage = (event) => {
13711
+ try {
13712
+ const data = typeof event.data === "string" ? event.data : event.data.toString();
13713
+ const message = JSON.parse(data);
13714
+ const requestId = message.requestId;
13715
+ if (requestId && this.handlers.has(requestId)) {
13716
+ this.handlers.get(requestId)(message);
13717
+ } else {
13718
+ logger.debug(`[WS] Unhandled message: ${message.action || "unknown"}`);
13719
+ }
13720
+ } catch (err) {
13721
+ logger.debug(`[WS] Failed to parse message: ${err}`);
13722
+ }
13723
+ };
13724
+ this.ws.onclose = () => {
13725
+ logger.debug("[WS] Connection closed");
13726
+ this.cleanup();
13727
+ if (!this.closed) {
13728
+ this.scheduleReconnect();
13729
+ }
13730
+ };
13731
+ this.ws.onerror = (err) => {
13732
+ logger.debug(`[WS] Error: ${err}`);
13733
+ if (this.connecting) {
13734
+ this.connecting = false;
13735
+ this.connected = false;
13736
+ reject(new Error("WebSocket connection failed"));
13737
+ }
13738
+ };
13739
+ });
13740
+ }
13741
+ /** Whether the connection is currently established */
13742
+ get isConnected() {
13743
+ return this.connected;
13744
+ }
13745
+ /**
13746
+ * Send a JSON message over the WebSocket connection.
13747
+ */
13748
+ send(data) {
13749
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
13750
+ throw new Error("WebSocket is not connected");
13751
+ }
13752
+ this.ws.send(JSON.stringify(data));
13753
+ }
13754
+ /**
13755
+ * Register a handler for messages matching a specific requestId.
13756
+ */
13757
+ onRequest(requestId, handler) {
13758
+ this.handlers.set(requestId, handler);
13759
+ }
13760
+ /**
13761
+ * Remove a handler for a specific requestId.
13762
+ */
13763
+ offRequest(requestId) {
13764
+ this.handlers.delete(requestId);
13765
+ }
13766
+ /**
13767
+ * Close the connection and stop all heartbeat/reconnect logic.
13768
+ */
13769
+ disconnect() {
13770
+ this.closed = true;
13771
+ this.cleanup();
13772
+ if (this.ws) {
13773
+ this.ws.close();
13774
+ this.ws = null;
13775
+ }
13776
+ this.handlers.clear();
13777
+ }
13778
+ startHeartbeat() {
13779
+ this.stopHeartbeat();
13780
+ this.heartbeatInterval = setInterval(
13781
+ () => {
13782
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
13783
+ this.ws.send(JSON.stringify({ action: "heartbeat" }));
13784
+ logger.debug("[WS] Heartbeat sent");
13785
+ }
13786
+ },
13787
+ 5 * 60 * 1e3
13788
+ );
13789
+ }
13790
+ stopHeartbeat() {
13791
+ if (this.heartbeatInterval) {
13792
+ clearInterval(this.heartbeatInterval);
13793
+ this.heartbeatInterval = null;
13794
+ }
13795
+ }
13796
+ cleanup() {
13797
+ this.connected = false;
13798
+ this.connecting = false;
13799
+ this.stopHeartbeat();
13800
+ }
13801
+ scheduleReconnect() {
13802
+ if (this.closed) return;
13803
+ this.reconnectAttempts++;
13804
+ const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts - 1), this.maxReconnectDelay);
13805
+ logger.debug(`[WS] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
13806
+ setTimeout(async () => {
13807
+ if (this.closed) return;
13808
+ try {
13809
+ await this.connect();
13810
+ } catch {
13811
+ logger.debug("[WS] Reconnection failed");
13812
+ }
13813
+ }, delay);
13814
+ }
13815
+ };
13816
+
13817
+ // src/ws/WebSocketToolExecutor.ts
13818
+ import { v4 as uuidv412 } from "uuid";
13819
+ var WebSocketToolExecutor = class {
13820
+ constructor(wsManager, tokenGetter) {
13821
+ this.wsManager = wsManager;
13822
+ this.tokenGetter = tokenGetter;
13823
+ }
13824
+ /**
13825
+ * Execute a server-side tool via WebSocket.
13826
+ * Returns the tool result or throws on error.
13827
+ */
13828
+ async execute(toolName, input, abortSignal) {
13829
+ if (!this.wsManager.isConnected) {
13830
+ throw new Error("WebSocket is not connected");
13831
+ }
13832
+ const token = await this.tokenGetter();
13833
+ if (!token) {
13834
+ throw new Error("No access token available");
13835
+ }
13836
+ const requestId = uuidv412();
13837
+ return new Promise((resolve3, reject) => {
13838
+ let settled = false;
13839
+ const settle = (action) => {
13840
+ if (settled) return;
13841
+ settled = true;
13842
+ this.wsManager.offRequest(requestId);
13843
+ action();
13844
+ };
13845
+ const settleResolve = (result) => settle(() => resolve3(result));
13846
+ const settleReject = (err) => settle(() => reject(err));
13847
+ if (abortSignal) {
13848
+ if (abortSignal.aborted) {
13849
+ settleReject(new Error("Tool execution aborted"));
13850
+ return;
13851
+ }
13852
+ abortSignal.addEventListener("abort", () => settleReject(new Error("Tool execution aborted")), {
13853
+ once: true
13854
+ });
13855
+ }
13856
+ this.wsManager.onRequest(requestId, (message) => {
13857
+ if (message.action === "cli_tool_response") {
13858
+ settleResolve({
13859
+ success: message.success,
13860
+ content: message.content,
13861
+ error: message.error
13862
+ });
13863
+ }
13864
+ });
13865
+ try {
13866
+ this.wsManager.send({
13867
+ action: "cli_tool_request",
13868
+ accessToken: token,
13869
+ requestId,
13870
+ toolName,
13871
+ input
13872
+ });
13873
+ } catch (err) {
13874
+ settleReject(err instanceof Error ? err : new Error(String(err)));
13875
+ }
13876
+ });
13877
+ }
13878
+ };
13879
+
13530
13880
  // src/auth/ApiClient.ts
13531
13881
  import axios11 from "axios";
13532
13882
  var ApiClient = class {
@@ -13664,7 +14014,7 @@ import { isAxiosError as isAxiosError2 } from "axios";
13664
14014
  // package.json
13665
14015
  var package_default = {
13666
14016
  name: "@bike4mind/cli",
13667
- version: "0.2.29-feat-session-permission.18877+9b858652a",
14017
+ version: "0.2.29-feat-cli-websocket-streaming.18878+661239950",
13668
14018
  type: "module",
13669
14019
  description: "Interactive CLI tool for Bike4Mind with ReAct agents",
13670
14020
  license: "UNLICENSED",
@@ -13774,10 +14124,10 @@ var package_default = {
13774
14124
  },
13775
14125
  devDependencies: {
13776
14126
  "@bike4mind/agents": "0.1.0",
13777
- "@bike4mind/common": "2.50.1-feat-session-permission.18877+9b858652a",
13778
- "@bike4mind/mcp": "1.29.1-feat-session-permission.18877+9b858652a",
13779
- "@bike4mind/services": "2.48.1-feat-session-permission.18877+9b858652a",
13780
- "@bike4mind/utils": "2.5.1-feat-session-permission.18877+9b858652a",
14127
+ "@bike4mind/common": "2.50.1-feat-cli-websocket-streaming.18878+661239950",
14128
+ "@bike4mind/mcp": "1.29.1-feat-cli-websocket-streaming.18878+661239950",
14129
+ "@bike4mind/services": "2.48.1-feat-cli-websocket-streaming.18878+661239950",
14130
+ "@bike4mind/utils": "2.5.1-feat-cli-websocket-streaming.18878+661239950",
13781
14131
  "@types/better-sqlite3": "^7.6.13",
13782
14132
  "@types/diff": "^5.0.9",
13783
14133
  "@types/jsonwebtoken": "^9.0.4",
@@ -13794,7 +14144,7 @@ var package_default = {
13794
14144
  optionalDependencies: {
13795
14145
  "@vscode/ripgrep": "^1.17.0"
13796
14146
  },
13797
- gitHead: "9b858652ae9c40dab415fd8fab4515a2f0d312be"
14147
+ gitHead: "661239950fcb26f7027596b319567a19d56fb1a7"
13798
14148
  };
13799
14149
 
13800
14150
  // src/config/constants.ts
@@ -15573,7 +15923,8 @@ function CliApp() {
15573
15923
  agentStore: null,
15574
15924
  abortController: null,
15575
15925
  contextContent: "",
15576
- backgroundManager: null
15926
+ backgroundManager: null,
15927
+ wsManager: null
15577
15928
  });
15578
15929
  const [isInitialized, setIsInitialized] = useState9(false);
15579
15930
  const [initError, setInitError] = useState9(null);
@@ -15601,6 +15952,10 @@ function CliApp() {
15601
15952
  })
15602
15953
  );
15603
15954
  }
15955
+ if (state.wsManager) {
15956
+ state.wsManager.disconnect();
15957
+ setWebSocketToolExecutor(null);
15958
+ }
15604
15959
  if (state.agent) {
15605
15960
  state.agent.removeAllListeners();
15606
15961
  }
@@ -15619,7 +15974,7 @@ function CliApp() {
15619
15974
  setTimeout(() => {
15620
15975
  process.exit(0);
15621
15976
  }, 100);
15622
- }, [state.session, state.sessionStore, state.mcpManager, state.agent, state.imageStore]);
15977
+ }, [state.session, state.sessionStore, state.mcpManager, state.agent, state.imageStore, state.wsManager]);
15623
15978
  useInput9((input, key) => {
15624
15979
  if (key.escape) {
15625
15980
  if (state.abortController) {
@@ -15689,7 +16044,7 @@ function CliApp() {
15689
16044
  if (!isAuthenticated) {
15690
16045
  console.log("\u2139\uFE0F AI features disabled. Available commands: /login, /help, /config\n");
15691
16046
  const minimalSession = {
15692
- id: uuidv411(),
16047
+ id: uuidv413(),
15693
16048
  name: `Session ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
15694
16049
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
15695
16050
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -15717,10 +16072,41 @@ function CliApp() {
15717
16072
  console.log(`\u{1F30D} API Environment: ${envName} (${apiBaseURL})`);
15718
16073
  }
15719
16074
  const apiClient = new ApiClient(apiBaseURL, state.configStore);
15720
- const llm = new ServerLlmBackend({
15721
- apiClient,
15722
- model: config.defaultModel
15723
- });
16075
+ const tokenGetter = async () => {
16076
+ const tokens = await state.configStore.getAuthTokens();
16077
+ return tokens?.accessToken ?? null;
16078
+ };
16079
+ let wsManager = null;
16080
+ let llm;
16081
+ try {
16082
+ const serverConfig = await apiClient.get("/api/settings/serverConfig");
16083
+ const wsUrl = serverConfig?.websocketUrl;
16084
+ if (wsUrl) {
16085
+ wsManager = new WebSocketConnectionManager(wsUrl, tokenGetter);
16086
+ await wsManager.connect();
16087
+ const wsToolExecutor2 = new WebSocketToolExecutor(wsManager, tokenGetter);
16088
+ setWebSocketToolExecutor(wsToolExecutor2);
16089
+ llm = new WebSocketLlmBackend({
16090
+ wsManager,
16091
+ apiClient,
16092
+ model: config.defaultModel,
16093
+ tokenGetter
16094
+ });
16095
+ logger.debug("\u{1F50C} Using WebSocket transport (bypasses CloudFront timeout)");
16096
+ } else {
16097
+ throw new Error("No websocketUrl in server config");
16098
+ }
16099
+ } catch (wsError) {
16100
+ logger.debug(
16101
+ `[WS] WebSocket unavailable, using SSE fallback: ${wsError instanceof Error ? wsError.message : String(wsError)}`
16102
+ );
16103
+ wsManager = null;
16104
+ setWebSocketToolExecutor(null);
16105
+ llm = new ServerLlmBackend({
16106
+ apiClient,
16107
+ model: config.defaultModel
16108
+ });
16109
+ }
15724
16110
  const models = await llm.getModelInfo();
15725
16111
  if (models.length === 0) {
15726
16112
  throw new Error("No models available from server.");
@@ -15733,7 +16119,7 @@ function CliApp() {
15733
16119
  }
15734
16120
  llm.currentModel = modelInfo.id;
15735
16121
  const newSession = {
15736
- id: uuidv411(),
16122
+ id: uuidv413(),
15737
16123
  name: `Session ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
15738
16124
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
15739
16125
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -15947,8 +16333,10 @@ function CliApp() {
15947
16333
  // Store agent store for agent management commands
15948
16334
  contextContent: contextResult.mergedContent,
15949
16335
  // Store raw context for compact instructions
15950
- backgroundManager
16336
+ backgroundManager,
15951
16337
  // Store for grouped notification turn tracking
16338
+ wsManager
16339
+ // WebSocket connection manager (null if using SSE fallback)
15952
16340
  }));
15953
16341
  setStoreSession(newSession);
15954
16342
  const bannerLines = [
@@ -16043,13 +16431,13 @@ function CliApp() {
16043
16431
  messageContent = multimodalMessage.content;
16044
16432
  }
16045
16433
  const userMessage = {
16046
- id: uuidv411(),
16434
+ id: uuidv413(),
16047
16435
  role: "user",
16048
16436
  content: userMessageContent,
16049
16437
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
16050
16438
  };
16051
16439
  const pendingAssistantMessage = {
16052
- id: uuidv411(),
16440
+ id: uuidv413(),
16053
16441
  role: "assistant",
16054
16442
  content: "...",
16055
16443
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -16260,13 +16648,13 @@ function CliApp() {
16260
16648
  userMessageContent = message;
16261
16649
  }
16262
16650
  const userMessage = {
16263
- id: uuidv411(),
16651
+ id: uuidv413(),
16264
16652
  role: "user",
16265
16653
  content: userMessageContent,
16266
16654
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
16267
16655
  };
16268
16656
  const pendingAssistantMessage = {
16269
- id: uuidv411(),
16657
+ id: uuidv413(),
16270
16658
  role: "assistant",
16271
16659
  content: "...",
16272
16660
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -16344,7 +16732,7 @@ function CliApp() {
16344
16732
  const currentSession = useCliStore.getState().session;
16345
16733
  if (currentSession) {
16346
16734
  const cancelMessage = {
16347
- id: uuidv411(),
16735
+ id: uuidv413(),
16348
16736
  role: "assistant",
16349
16737
  content: "\u26A0\uFE0F Operation cancelled by user",
16350
16738
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -16389,7 +16777,7 @@ function CliApp() {
16389
16777
  setState((prev) => ({ ...prev, abortController }));
16390
16778
  try {
16391
16779
  const pendingAssistantMessage = {
16392
- id: uuidv411(),
16780
+ id: uuidv413(),
16393
16781
  role: "assistant",
16394
16782
  content: "...",
16395
16783
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -16417,7 +16805,7 @@ function CliApp() {
16417
16805
  const currentSession = useCliStore.getState().session;
16418
16806
  if (!currentSession) return;
16419
16807
  const continuationMessage = {
16420
- id: uuidv411(),
16808
+ id: uuidv413(),
16421
16809
  role: "assistant",
16422
16810
  content: "---\n\n**Background Agent Results:**\n\n" + result.finalAnswer,
16423
16811
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -16476,13 +16864,13 @@ function CliApp() {
16476
16864
  isError = true;
16477
16865
  }
16478
16866
  const userMessage = {
16479
- id: uuidv411(),
16867
+ id: uuidv413(),
16480
16868
  role: "user",
16481
16869
  content: `$ ${command}`,
16482
16870
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
16483
16871
  };
16484
16872
  const assistantMessage = {
16485
- id: uuidv411(),
16873
+ id: uuidv413(),
16486
16874
  role: "assistant",
16487
16875
  content: isError ? `\u274C Error:
16488
16876
  ${output}` : output.trim() || "(no output)",
@@ -16951,7 +17339,7 @@ Keyboard Shortcuts:
16951
17339
  console.clear();
16952
17340
  const model = state.session?.model || state.config?.defaultModel || "claude-sonnet";
16953
17341
  const newSession = {
16954
- id: uuidv411(),
17342
+ id: uuidv413(),
16955
17343
  name: `Session ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
16956
17344
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
16957
17345
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -17456,9 +17844,9 @@ No usage data available for the last ${USAGE_DAYS} days.`);
17456
17844
  return { ...prev, config: updatedConfig };
17457
17845
  });
17458
17846
  if (modelChanged && state.agent) {
17459
- const llm = state.agent.context.llm;
17460
- if (llm) {
17461
- llm.currentModel = updatedConfig.defaultModel;
17847
+ const backend = state.agent.context.llm;
17848
+ if (backend) {
17849
+ backend.currentModel = updatedConfig.defaultModel;
17462
17850
  }
17463
17851
  }
17464
17852
  };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  CurationArtifactType
4
- } from "./chunk-5GSDPBTM.js";
4
+ } from "./chunk-KPOK6V6E.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-5GSDPBTM.js";
4
+ } from "./chunk-KPOK6V6E.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-BCOGDPUB.js";
6
- import "./chunk-S7BPPS33.js";
7
- import "./chunk-5GSDPBTM.js";
5
+ } from "./chunk-QQL3OBKL.js";
6
+ import "./chunk-TY6Z2UV2.js";
7
+ import "./chunk-KPOK6V6E.js";
8
8
  import "./chunk-OCYRD7D6.js";
9
9
  export {
10
10
  findMostSimilarMemento,
@@ -41,6 +41,12 @@ import {
41
41
  ChatModels,
42
42
  CitableSourceSchema,
43
43
  ClaudeArtifactMimeTypes,
44
+ CliCompletionChunkAction,
45
+ CliCompletionDoneAction,
46
+ CliCompletionErrorAction,
47
+ CliCompletionRequestAction,
48
+ CliToolRequestAction,
49
+ CliToolResponseAction,
44
50
  CollectionType,
45
51
  CompletionApiUsageTransaction,
46
52
  CompletionRequestSchema,
@@ -340,7 +346,7 @@ import {
340
346
  validateReactArtifactV2,
341
347
  validateSvgArtifactV2,
342
348
  wikiMarkupToAdf
343
- } from "./chunk-5GSDPBTM.js";
349
+ } from "./chunk-KPOK6V6E.js";
344
350
  export {
345
351
  ALL_IMAGE_MODELS,
346
352
  ALL_IMAGE_SIZES,
@@ -384,6 +390,12 @@ export {
384
390
  ChatModels,
385
391
  CitableSourceSchema,
386
392
  ClaudeArtifactMimeTypes,
393
+ CliCompletionChunkAction,
394
+ CliCompletionDoneAction,
395
+ CliCompletionErrorAction,
396
+ CliCompletionRequestAction,
397
+ CliToolRequestAction,
398
+ CliToolResponseAction,
387
399
  CollectionType,
388
400
  CompletionApiUsageTransaction,
389
401
  CompletionRequestSchema,
@@ -134,8 +134,8 @@ import {
134
134
  validateMermaidSyntax,
135
135
  warmUpSettingsCache,
136
136
  withRetry
137
- } from "./chunk-S7BPPS33.js";
138
- import "./chunk-5GSDPBTM.js";
137
+ } from "./chunk-TY6Z2UV2.js";
138
+ import "./chunk-KPOK6V6E.js";
139
139
  import {
140
140
  Logger,
141
141
  NotificationDeduplicator,
@@ -2,9 +2,9 @@
2
2
  import {
3
3
  SubtractCreditsSchema,
4
4
  subtractCredits
5
- } from "./chunk-PKKJ44C5.js";
6
- import "./chunk-S7BPPS33.js";
7
- import "./chunk-5GSDPBTM.js";
5
+ } from "./chunk-BXCARBUQ.js";
6
+ import "./chunk-TY6Z2UV2.js";
7
+ import "./chunk-KPOK6V6E.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.29-feat-session-permission.18877+9b858652a",
3
+ "version": "0.2.29-feat-cli-websocket-streaming.18878+661239950",
4
4
  "type": "module",
5
5
  "description": "Interactive CLI tool for Bike4Mind with ReAct agents",
6
6
  "license": "UNLICENSED",
@@ -110,10 +110,10 @@
110
110
  },
111
111
  "devDependencies": {
112
112
  "@bike4mind/agents": "0.1.0",
113
- "@bike4mind/common": "2.50.1-feat-session-permission.18877+9b858652a",
114
- "@bike4mind/mcp": "1.29.1-feat-session-permission.18877+9b858652a",
115
- "@bike4mind/services": "2.48.1-feat-session-permission.18877+9b858652a",
116
- "@bike4mind/utils": "2.5.1-feat-session-permission.18877+9b858652a",
113
+ "@bike4mind/common": "2.50.1-feat-cli-websocket-streaming.18878+661239950",
114
+ "@bike4mind/mcp": "1.29.1-feat-cli-websocket-streaming.18878+661239950",
115
+ "@bike4mind/services": "2.48.1-feat-cli-websocket-streaming.18878+661239950",
116
+ "@bike4mind/utils": "2.5.1-feat-cli-websocket-streaming.18878+661239950",
117
117
  "@types/better-sqlite3": "^7.6.13",
118
118
  "@types/diff": "^5.0.9",
119
119
  "@types/jsonwebtoken": "^9.0.4",
@@ -130,5 +130,5 @@
130
130
  "optionalDependencies": {
131
131
  "@vscode/ripgrep": "^1.17.0"
132
132
  },
133
- "gitHead": "9b858652ae9c40dab415fd8fab4515a2f0d312be"
133
+ "gitHead": "661239950fcb26f7027596b319567a19d56fb1a7"
134
134
  }