blun-king-cli 9.1.286 → 9.1.288

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.
@@ -26,6 +26,7 @@ const {
26
26
  const { CORE_LOADED_MESSAGE, RUNTIME_READY_MESSAGE } = require('./core-bootstrap');
27
27
  const { prepareManagedNodeRuntime } = require('./node-runtime');
28
28
  const { repairConfiguredNativeModules } = require('./native-module-repair');
29
+ const { startMnemoConnectHeartbeat } = require('./mnemo-connect-heartbeat.cjs');
29
30
  const { acquireSharedRuntimeLease, tryAcquireUpdateLease } = require('./update-lease');
30
31
  const { compareSemver, runExplicitUpdate, runUpdateNotice } = require('./update-notice');
31
32
  const {
@@ -923,13 +924,22 @@ async function runLauncher(options = {}) {
923
924
  env.BLUN_LOG_HOME = privatePaths.sharedHome;
924
925
  env.BLUN_PROFILE = PROFILE.profileName;
925
926
  env.BLUN_MODEL_MAX_COMPLETION_TOKENS = env.BLUN_MODEL_MAX_COMPLETION_TOKENS || '32768';
926
- process.exitCode = await superviseProtectedCore(
927
- ARGS,
927
+ const mnemoConnect = startMnemoConnectHeartbeat({
928
928
  env,
929
- callerCwd,
930
- releaseNotice,
931
- { startupPendingRuntime, staleActiveRuntime },
932
- );
929
+ profileName: PROFILE.profileName,
930
+ version: readPackageVersion(),
931
+ });
932
+ try {
933
+ process.exitCode = await superviseProtectedCore(
934
+ ARGS,
935
+ env,
936
+ callerCwd,
937
+ releaseNotice,
938
+ { startupPendingRuntime, staleActiveRuntime },
939
+ );
940
+ } finally {
941
+ await mnemoConnect.stop();
942
+ }
933
943
  } finally {
934
944
  await releaseNotice();
935
945
  }
@@ -0,0 +1,173 @@
1
+ 'use strict';
2
+
3
+ const os = require('node:os');
4
+
5
+ const DEFAULT_HEARTBEAT_MS = 60_000;
6
+ const DEFAULT_TIMEOUT_MS = 5_000;
7
+
8
+ function resolveMnemoHubUrl(env = process.env) {
9
+ const raw = [
10
+ env.BLUN_MNEMO_HUB_URL,
11
+ env.MNEMO_HUB_URL,
12
+ env.MNEMO_URL,
13
+ ].find((value) => typeof value === 'string' && value.trim().length > 0);
14
+ if (!raw) return null;
15
+
16
+ try {
17
+ const url = new URL(raw.trim());
18
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) return null;
19
+ url.pathname = url.pathname.replace(/\/+$/u, '');
20
+ url.search = '';
21
+ url.hash = '';
22
+ return url.toString().replace(/\/$/u, '');
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ function resolveMnemoAgentName(profileName, env = process.env) {
29
+ const configured = typeof env.BLUN_MNEMO_AGENT === 'string'
30
+ ? env.BLUN_MNEMO_AGENT.trim()
31
+ : '';
32
+ return configured || String(profileName || '').trim();
33
+ }
34
+
35
+ async function postMnemoTool(baseUrl, tool, payload, options = {}) {
36
+ const controller = new AbortController();
37
+ const timeout = (options.setTimeoutImpl || setTimeout)(
38
+ () => controller.abort(),
39
+ options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
40
+ );
41
+ timeout.unref?.();
42
+ try {
43
+ const response = await (options.fetchImpl || fetch)(
44
+ `${baseUrl}/tool/${encodeURIComponent(tool)}`,
45
+ {
46
+ method: 'POST',
47
+ headers: { 'content-type': 'application/json' },
48
+ body: JSON.stringify(payload),
49
+ signal: controller.signal,
50
+ },
51
+ );
52
+ if (!response.ok) throw new Error(`Mnemo ${tool} HTTP ${response.status}`);
53
+ const body = await response.json();
54
+ if (body && body.error) {
55
+ throw new Error(typeof body.error === 'string' ? body.error : JSON.stringify(body.error));
56
+ }
57
+ return body;
58
+ } finally {
59
+ (options.clearTimeoutImpl || clearTimeout)(timeout);
60
+ }
61
+ }
62
+
63
+ function startMnemoConnectHeartbeat(options = {}) {
64
+ const env = options.env || process.env;
65
+ const baseUrl = options.baseUrl || resolveMnemoHubUrl(env);
66
+ const agentName = options.agentName || resolveMnemoAgentName(options.profileName, env);
67
+ if (!baseUrl || !agentName) {
68
+ return Object.freeze({ enabled: false, ready: Promise.resolve(false), stop: async () => {} });
69
+ }
70
+
71
+ const intervalMs = options.intervalMs ?? DEFAULT_HEARTBEAT_MS;
72
+ const hostname = options.hostname || os.hostname();
73
+ const pid = options.pid ?? process.pid;
74
+ const callTool = options.callTool || ((tool, payload) => postMnemoTool(baseUrl, tool, payload, options));
75
+ const setIntervalImpl = options.setIntervalImpl || setInterval;
76
+ const clearIntervalImpl = options.clearIntervalImpl || clearInterval;
77
+ const onState = typeof options.onState === 'function' ? options.onState : () => {};
78
+ let stopped = false;
79
+ let registered = false;
80
+ let inFlight = null;
81
+
82
+ const register = async () => {
83
+ await callTool('mem_connect_register', {
84
+ agent_name: agentName,
85
+ display_name: agentName,
86
+ host: hostname,
87
+ pid,
88
+ skills: ['blun-king'],
89
+ meta: {
90
+ source: 'blun-king-cli',
91
+ runtime: 'king',
92
+ profile: String(options.profileName || agentName),
93
+ version: options.version || null,
94
+ },
95
+ });
96
+ registered = true;
97
+ onState({ kind: 'registered', agentName, baseUrl });
98
+ };
99
+
100
+ const runPulse = async () => {
101
+ if (stopped) return false;
102
+ if (!registered) await register();
103
+ const result = await callTool('mem_connect_heartbeat', {
104
+ agent_name: agentName,
105
+ status: 'online',
106
+ meta: {
107
+ source: 'blun-king-cli',
108
+ runtime: 'king',
109
+ profile: String(options.profileName || agentName),
110
+ version: options.version || null,
111
+ },
112
+ });
113
+ const payload = result && typeof result === 'object' && result.result
114
+ ? result.result
115
+ : result;
116
+ if (payload && payload.updated === false) {
117
+ registered = false;
118
+ await register();
119
+ }
120
+ onState({ kind: 'heartbeat', agentName, baseUrl });
121
+ return true;
122
+ };
123
+
124
+ const pulse = () => {
125
+ if (stopped) return Promise.resolve(false);
126
+ if (inFlight) return inFlight;
127
+ inFlight = runPulse()
128
+ .catch((error) => {
129
+ registered = false;
130
+ onState({ kind: 'error', agentName, baseUrl, error });
131
+ return false;
132
+ })
133
+ .finally(() => { inFlight = null; });
134
+ return inFlight;
135
+ };
136
+
137
+ const ready = pulse();
138
+ const timer = setIntervalImpl(() => { void pulse(); }, intervalMs);
139
+ timer.unref?.();
140
+
141
+ return Object.freeze({
142
+ enabled: true,
143
+ agentName,
144
+ baseUrl,
145
+ ready,
146
+ pulse,
147
+ async stop(stopOptions = {}) {
148
+ if (stopped) return;
149
+ stopped = true;
150
+ clearIntervalImpl(timer);
151
+ if (inFlight) await inFlight;
152
+ if (stopOptions.offline === false || !registered) return;
153
+ try {
154
+ await callTool('mem_connect_heartbeat', {
155
+ agent_name: agentName,
156
+ status: 'offline',
157
+ meta: { source: 'blun-king-cli', runtime: 'king' },
158
+ });
159
+ } catch (error) {
160
+ onState({ kind: 'error', agentName, baseUrl, error });
161
+ }
162
+ },
163
+ });
164
+ }
165
+
166
+ module.exports = {
167
+ DEFAULT_HEARTBEAT_MS,
168
+ DEFAULT_TIMEOUT_MS,
169
+ postMnemoTool,
170
+ resolveMnemoAgentName,
171
+ resolveMnemoHubUrl,
172
+ startMnemoConnectHeartbeat,
173
+ };
@@ -4,6 +4,7 @@ const TOOL_SCHEMA_BUDGET_RATIO = 0.25;
4
4
  const TOOL_SCHEMA_MAX_TOKENS = 32_000;
5
5
  const DEFERRED_TOOL_LOADER_NAME = 'ToolSearch';
6
6
  const MAX_PERSISTENT_DEFERRED_TOOLS = 4;
7
+ const MAX_TOOL_SEARCH_QUERY_CHARS = 1_000;
7
8
  const TOOL_SEARCH_INTENT_WORDS = new Set([
8
9
  'find', 'fetch', 'get', 'holen', 'list', 'read', 'search', 'show', 'anzeigen', 'lesen', 'suchen',
9
10
  ]);
@@ -83,8 +84,18 @@ function deferredToolCatalog(tools) {
83
84
  .sort((left, right) => left.localeCompare(right));
84
85
  }
85
86
 
87
+ function normalizeDeferredToolQuery(value) {
88
+ const query = String(value ?? '').trim();
89
+ if (query.length <= MAX_TOOL_SEARCH_QUERY_CHARS) return query;
90
+ const bounded = query.slice(0, MAX_TOOL_SEARCH_QUERY_CHARS);
91
+ const wordBoundary = bounded.lastIndexOf(' ');
92
+ return (wordBoundary >= MAX_TOOL_SEARCH_QUERY_CHARS * 0.8
93
+ ? bounded.slice(0, wordBoundary)
94
+ : bounded).trimEnd();
95
+ }
96
+
86
97
  function searchDeferredTools(tools, query) {
87
- const normalized = String(query || '').trim().toLowerCase();
98
+ const normalized = normalizeDeferredToolQuery(query).toLowerCase();
88
99
  if (!normalized) return [];
89
100
  if (normalized.startsWith('select:')) {
90
101
  const selector = normalized.slice('select:'.length).trim();
@@ -132,7 +143,7 @@ function relatedSearchTerms(token) {
132
143
  }
133
144
 
134
145
  function searchRelatedDeferredTools(tools, query, options = {}) {
135
- const normalized = String(query || '').trim().toLowerCase();
146
+ const normalized = normalizeDeferredToolQuery(query).toLowerCase();
136
147
  if (!normalized || normalized.startsWith('select:')) return [];
137
148
  const tokens = normalized.split(/\s+/).filter(Boolean);
138
149
  const requestedNameTokens = tokens
@@ -230,6 +241,7 @@ function createDeferredToolLoader(selectedTools, deferredTools, loadedToolNames
230
241
  properties: {
231
242
  query: {
232
243
  type: 'string',
244
+ maxLength: MAX_TOOL_SEARCH_QUERY_CHARS,
233
245
  description: 'Exact selection or keyword query, for example select:download_attachment or +slack send.',
234
246
  },
235
247
  },
@@ -237,7 +249,7 @@ function createDeferredToolLoader(selectedTools, deferredTools, loadedToolNames
237
249
  additionalProperties: false,
238
250
  },
239
251
  resolveExecution(args) {
240
- const query = String(args?.query || '').trim();
252
+ const query = normalizeDeferredToolQuery(args?.query);
241
253
  return {
242
254
  description: query ? `Searching tool schemas for ${query}` : 'Searching tool schemas',
243
255
  approvalRule: DEFERRED_TOOL_LOADER_NAME,
@@ -302,12 +314,14 @@ module.exports = {
302
314
  CORE_TOOL_NAMES,
303
315
  DEFERRED_TOOL_LOADER_NAME,
304
316
  MAX_PERSISTENT_DEFERRED_TOOLS,
317
+ MAX_TOOL_SEARCH_QUERY_CHARS,
305
318
  TOOL_SCHEMA_BUDGET_RATIO,
306
319
  TOOL_SCHEMA_MAX_TOKENS,
307
320
  createDeferredToolLoader,
308
321
  deferredToolCatalog,
309
322
  deferredToolNames,
310
323
  mediaGenerationToolNamesForText,
324
+ normalizeDeferredToolQuery,
311
325
  rememberDeferredToolAfterNotFound,
312
326
  toolSchemaBudgetTokens,
313
327
  };
package/blun.mjs CHANGED
@@ -295851,7 +295851,7 @@ async function withTimeout(promise, timeoutMs, onTimeout) {
295851
295851
  if (timer !== void 0) clearTimeout(timer);
295852
295852
  }
295853
295853
  }
295854
- var DEFAULT_STARTUP_TIMEOUT_MS$1, McpConnectionManager;
295854
+ var DEFAULT_STARTUP_TIMEOUT_MS$1, MCP_AUTO_RECONNECT_DELAYS_MS, McpConnectionManager;
295855
295855
  var init_connection_manager = __esmMin((() => {
295856
295856
  init_errors$8();
295857
295857
  init_logger$1();
@@ -295863,10 +295863,12 @@ var init_connection_manager = __esmMin((() => {
295863
295863
  init_public_display();
295864
295864
  init_types$1();
295865
295865
  DEFAULT_STARTUP_TIMEOUT_MS$1 = 3e4;
295866
+ MCP_AUTO_RECONNECT_DELAYS_MS = [1e3, 3e3];
295866
295867
  McpConnectionManager = class {
295867
295868
  options;
295868
295869
  entries = /* @__PURE__ */ new Map();
295869
295870
  listeners = /* @__PURE__ */ new Set();
295871
+ reconnectTimers = /* @__PURE__ */ new Map();
295870
295872
  initialLoad = Promise.resolve();
295871
295873
  initialLoadAttemptId = 0;
295872
295874
  initialLoadStartedAt;
@@ -295941,6 +295943,7 @@ var init_connection_manager = __esmMin((() => {
295941
295943
  return initialLoad;
295942
295944
  }
295943
295945
  async connect(name, config) {
295946
+ this.clearReconnectTimer(name);
295944
295947
  const previous = this.entries.get(name);
295945
295948
  if (previous !== void 0) await this.closeClient(previous);
295946
295949
  const disabled = config.enabled === false;
@@ -295948,6 +295951,7 @@ var init_connection_manager = __esmMin((() => {
295948
295951
  name,
295949
295952
  config,
295950
295953
  attemptId: 0,
295954
+ autoReconnectAttempt: 0,
295951
295955
  status: disabled ? "disabled" : "pending"
295952
295956
  };
295953
295957
  this.entries.set(name, entry);
@@ -295955,6 +295959,7 @@ var init_connection_manager = __esmMin((() => {
295955
295959
  if (!disabled) await this.connectOne(entry, this.beginConnectAttempt(entry));
295956
295960
  }
295957
295961
  async remove(name) {
295962
+ this.clearReconnectTimer(name);
295958
295963
  const entry = this.entries.get(name);
295959
295964
  if (entry === void 0) return false;
295960
295965
  await this.closeClient(entry);
@@ -295984,6 +295989,7 @@ var init_connection_manager = __esmMin((() => {
295984
295989
  name,
295985
295990
  config,
295986
295991
  attemptId: 0,
295992
+ autoReconnectAttempt: 0,
295987
295993
  status: disabled ? "disabled" : "pending"
295988
295994
  };
295989
295995
  this.entries.set(name, entry);
@@ -295996,6 +296002,8 @@ var init_connection_manager = __esmMin((() => {
295996
296002
  const entry = this.entries.get(name);
295997
296003
  if (entry === void 0) throw new BlunError(ErrorCodes.MCP_SERVER_NOT_FOUND, `Unknown MCP server: ${name}`);
295998
296004
  if (entry.config.enabled === false) throw new BlunError(ErrorCodes.MCP_SERVER_DISABLED, `MCP server is disabled: ${name}`);
296005
+ this.clearReconnectTimer(name);
296006
+ entry.autoReconnectAttempt = 0;
295999
296007
  const attemptId = this.beginConnectAttempt(entry);
296000
296008
  await this.closeClient(entry);
296001
296009
  if (!this.isCurrent(entry, attemptId)) return;
@@ -296013,6 +296021,7 @@ var init_connection_manager = __esmMin((() => {
296013
296021
  }
296014
296022
  }
296015
296023
  async shutdown() {
296024
+ for (const name of this.reconnectTimers.keys()) this.clearReconnectTimer(name);
296016
296025
  const entries = Array.from(this.entries.values());
296017
296026
  this.entries.clear();
296018
296027
  const tasks = entries.map((entry) => this.closeClient(entry));
@@ -296035,6 +296044,8 @@ var init_connection_manager = __esmMin((() => {
296035
296044
  entry.tools = tools;
296036
296045
  entry.enabledNames = computeEnabledNames(entry.config, tools);
296037
296046
  entry.status = "connected";
296047
+ entry.autoReconnectAttempt = 0;
296048
+ this.clearReconnectTimer(entry.name);
296038
296049
  this.watchForUnexpectedClose(entry, startupClient, attemptId);
296039
296050
  } catch (error) {
296040
296051
  if (!this.isCurrent(entry, attemptId)) {
@@ -296054,6 +296065,7 @@ var init_connection_manager = __esmMin((() => {
296054
296065
  }
296055
296066
  if (!this.isCurrent(entry, attemptId)) return;
296056
296067
  this.emit(entry);
296068
+ this.scheduleReconnectIfNeeded(entry);
296057
296069
  }
296058
296070
  watchForUnexpectedClose(entry, client, attemptId) {
296059
296071
  client.onUnexpectedClose((reason) => {
@@ -296066,8 +296078,35 @@ var init_connection_manager = __esmMin((() => {
296066
296078
  entry.client = void 0;
296067
296079
  this.closeRuntimeClient(client);
296068
296080
  this.emit(entry);
296081
+ this.scheduleReconnectIfNeeded(entry);
296069
296082
  });
296070
296083
  }
296084
+ clearReconnectTimer(name) {
296085
+ const timer = this.reconnectTimers.get(name);
296086
+ if (timer === void 0) return;
296087
+ clearTimeout(timer);
296088
+ this.reconnectTimers.delete(name);
296089
+ }
296090
+ scheduleReconnectIfNeeded(entry) {
296091
+ if (entry.status !== "failed" || entry.config.enabled === false) return;
296092
+ if (this.reconnectTimers.has(entry.name)) return;
296093
+ const retryIndex = entry.autoReconnectAttempt ?? 0;
296094
+ if (retryIndex >= MCP_AUTO_RECONNECT_DELAYS_MS.length) return;
296095
+ entry.autoReconnectAttempt = retryIndex + 1;
296096
+ const timer = setTimeout(() => {
296097
+ this.reconnectTimers.delete(entry.name);
296098
+ if (this.entries.get(entry.name) !== entry || entry.config.enabled === false) return;
296099
+ const attemptId = this.beginConnectAttempt(entry);
296100
+ entry.status = "pending";
296101
+ entry.tools = void 0;
296102
+ entry.enabledNames = void 0;
296103
+ entry.error = void 0;
296104
+ this.emit(entry);
296105
+ void this.connectOne(entry, attemptId);
296106
+ }, MCP_AUTO_RECONNECT_DELAYS_MS[retryIndex]);
296107
+ timer.unref?.();
296108
+ this.reconnectTimers.set(entry.name, timer);
296109
+ }
296071
296110
  beginConnectAttempt(entry) {
296072
296111
  entry.attemptId += 1;
296073
296112
  return entry.attemptId;
@@ -504339,7 +504378,7 @@ var EditorKeyboardController = class {
504339
504378
  this.clearPendingUndoEsc();
504340
504379
  return;
504341
504380
  }
504342
- if (host.streamingUI.hasActiveTurn() || host.state.appState.streamingPhase !== "idle") {
504381
+ if (host.queueSteerInFlight !== void 0 || host.streamingUI.hasActiveTurn() || host.state.appState.streamingPhase !== "idle") {
504343
504382
  this.cancelCurrentStream();
504344
504383
  this.clearPendingUndoEsc();
504345
504384
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.286",
3
+ "version": "9.1.288",
4
4
  "description": "BLUN CLI - your own AI agent with a Telegram channel. Get it done. With BLUN.",
5
5
  "license": "MIT",
6
6
  "bin": {