blun-king-cli 9.1.569 → 9.1.570

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,33 @@
1
+ # Changelog
2
+
3
+ ## 9.1.570
4
+
5
+ ### Fixed
6
+
7
+ - Give each new prompt delivery a distinct hook correlation ID. Hooks for the same delivery retain the same ID; explicitly supplied IDs are preserved.
8
+ - Pass session-specific runtime environment values to hook subprocesses and MCP connections, including reconnects, without rewriting stored plugin configuration.
9
+ - Preserve native tool-result correlation across turns, including failed tools and duplicate calls.
10
+ - Match canonically equivalent Unicode research queries without conflating different characters. Stored sources, dates, versions and project boundaries are preserved.
11
+ - Keep `king --help` and `king --version` from changing Windows crash-dump registry settings.
12
+
13
+ ### Added
14
+
15
+ - Support negotiated partial web-search results with a visible source-failure notice. Empty or incompatible partial responses remain explicit errors. This client change does not activate or guarantee a hosted search service.
16
+ - Add the default-off `BLUN_EXPERIMENTAL_AGENT_SPINE_TIMELINE` transport. When explicitly enabled, it passes the actual persisted session-record path and protocol to the selected AgentSpine plugin, with a separate capability per session and a new capability after resume. This is transport support, not proof of recall or authorization by the receiving plugin.
17
+
18
+ ### Preserved
19
+
20
+ - Includes the public 9.1.569 Telegram group mention routing, startup, permission persistence, research collection and crawler changes.
21
+ - The bundled AgentSpine implementation is unchanged from public 9.1.569. No Mnemo dependency is introduced.
22
+ - User profiles, permissions, conversations and plugin settings are not migrated or reset by this release.
23
+
24
+ ### Updating
25
+
26
+ - Public-channel installations use `king update`.
27
+ - Old packages explicitly pinned to the internal update channel do not discover public releases. Close the relevant King sessions, then migrate once with `npm install -g blun-king-cli@9.1.570`.
28
+ - A Windows `EBUSY` rename failure means the installation did not complete. Identify the process holding the package before retrying; do not delete the package or terminate unrelated Node processes.
29
+
30
+ ## 9.1.569
31
+
32
+ - Cumulative public release including Telegram group mention routing, startup and permission fixes, research collection, and crawler tooling.
33
+ - Published artifacts are immutable. Later changes ship under a new version.
@@ -12,7 +12,8 @@ const CORE_LOADED_MESSAGE = 'blun-core-bootstrap-loaded';
12
12
  async function runCoreBootstrap() {
13
13
  const packageRoot = path.resolve(__dirname, '..');
14
14
  const mainPath = path.join(packageRoot, 'blun.mjs');
15
- installWindowsNodeCrashDump({ homeDir: process.env.BLUN_HOME });
15
+ const informational = process.argv.slice(2).some(arg => ['-h', '--help', '-V', '--version'].includes(arg));
16
+ if (!informational) installWindowsNodeCrashDump({ homeDir: process.env.BLUN_HOME });
16
17
  const leaseResult = await acquireSharedRuntimeLease({ packageRoot });
17
18
  if (!leaseResult.acquired) throw new Error('RUNTIME_PROTECTION_UNAVAILABLE');
18
19
 
package/blun.mjs CHANGED
@@ -21351,7 +21351,15 @@ var init_types$14 = __esmMin((() => {}));
21351
21351
  //#region ../../packages/agent-core/src/flags/registry.ts
21352
21352
  var FLAG_DEFINITIONS;
21353
21353
  var init_registry$1 = __esmMin((() => {
21354
- FLAG_DEFINITIONS = [{
21354
+ FLAG_DEFINITIONS = [
21355
+ {
21356
+ id: 'agent_spine_timeline',
21357
+ title: 'AgentSpine session transport',
21358
+ description: 'Pass the current native record source to the enabled AgentSpine plugin.',
21359
+ env: 'BLUN_EXPERIMENTAL_AGENT_SPINE_TIMELINE',
21360
+ default: false,
21361
+ surface: 'core',
21362
+ },{
21355
21363
  id: "vision_reader",
21356
21364
  title: "Vision reader",
21357
21365
  description: "Enrich user images with observations from a configured internal service.",
@@ -231318,10 +231326,9 @@ async function runHook(command, input, options) {
231318
231326
  cwd: options.cwd,
231319
231327
  stdio: "pipe",
231320
231328
  detached: process.platform !== "win32",
231321
- env: options.env ? {
231322
- ...process.env,
231323
- ...options.env
231324
- } : void 0
231329
+ env: options.env || options.runtimeEnv
231330
+ ? overlayRuntimeEnv({ ...process.env, ...options.env }, options.runtimeEnv)
231331
+ : undefined
231325
231332
  });
231326
231333
  } catch (error) {
231327
231334
  return allowResult({ stderr: errorMessage$11(error) });
@@ -231659,28 +231666,44 @@ async prepareToolUse(args = {}) {
231659
231666
  await Promise.allSettled([...this.pendingTriggers]);
231660
231667
  }
231661
231668
  async triggerInner(event, args) {
231662
- const matcherValue = matcherValueText(args.matcherValue);
231663
- const inputData = toHookInputData({
231664
- hookEventName: event,
231665
- sessionId: this.options.sessionId ?? "",
231666
- cwd: this.options.cwd ?? "",
231667
- ...args.inputData
231668
- });
231669
- const matched = this.matchingHooks(event, matcherValue);
231670
- if (matched.length === 0) return [];
231671
- this.emitTriggered(event, matcherValue, matched.length);
231672
- const startedAt = Date.now();
231673
- const results = await createRequire(import.meta.url)("./bin/observer-hooks.cjs").runHookPhases(matched, (hook) => runHook(hook.command, inputData, {
231674
- timeout: hook.timeout ?? DEFAULT_HOOK_TIMEOUT_SECONDS,
231675
- cwd: hook.cwd ?? (this.options.cwd === "" ? void 0 : this.options.cwd),
231676
- env: hook.env,
231677
- signal: args.signal
231678
- }), args.signal);
231679
- for (const message of hookWarningMessages(results)) this.emitWarning(event, message);
231680
- const { action, reason } = aggregateResults(event, results);
231681
- this.emitResolved(event, matcherValue, action, reason, Date.now() - startedAt);
231682
- return results;
231683
- }
231669
+ const matcherValue = matcherValueText(args.matcherValue);
231670
+ const inputData = toHookInputData({
231671
+ hookEventName: event,
231672
+ sessionId: this.options.sessionId ?? '',
231673
+ cwd: this.options.cwd ?? '',
231674
+ ...args.inputData,
231675
+ });
231676
+ const matched = this.matchingHooks(event, matcherValue);
231677
+ if (matched.length === 0)
231678
+ return [];
231679
+ if (event === 'UserPromptSubmit'
231680
+ && !Object.hasOwn(inputData, 'event_id') && !Object.hasOwn(inputData, 'hook_event_id')) {
231681
+ // One logical delivery, shared by every hook; explicit IDs preserve redelivery identity.
231682
+ inputData['event_id'] = releasePromptUUID();
231683
+ }
231684
+ this.emitTriggered(event, matcherValue, matched.length);
231685
+ const startedAt = Date.now();
231686
+ const results = await Promise.all(matched.map(async (hook) => {
231687
+ let runtimeEnv;
231688
+ try {
231689
+ runtimeEnv = this.options.runtimeEnv?.(hook);
231690
+ }
231691
+ catch {
231692
+ // A resolver error may contain private values. Other hooks still run.
231693
+ return { action: 'allow', stderr: 'Hook runtime environment is unavailable.' };
231694
+ }
231695
+ return runHook(hook.command, inputData, {
231696
+ timeout: hook.timeout ?? DEFAULT_HOOK_TIMEOUT_SECONDS,
231697
+ cwd: hook.cwd ?? (this.options.cwd === '' ? undefined : this.options.cwd),
231698
+ env: hook.env,
231699
+ runtimeEnv,
231700
+ signal: args.signal,
231701
+ });
231702
+ }));
231703
+ const { action, reason } = aggregateResults(event, results);
231704
+ this.emitResolved(event, matcherValue, action, reason, Date.now() - startedAt);
231705
+ return results;
231706
+ }
231684
231707
  matchingHooks(event, matcherValue) {
231685
231708
  const seen = /* @__PURE__ */ new Set();
231686
231709
  const matched = [];
@@ -236096,6 +236119,13 @@ var init_records = __esmMin((() => {
236096
236119
  persistence;
236097
236120
  _restoring = null;
236098
236121
  metadataInitialized = false;
236122
+ persistedProtocolVersion;
236123
+ get fileSource() {
236124
+ if (!(this.persistence instanceof FileSystemAgentRecordPersistence)
236125
+ || this.persistedProtocolVersion === undefined)
236126
+ return undefined;
236127
+ return { path: this.persistence.filePath, protocolVersion: this.persistedProtocolVersion };
236128
+ }
236099
236129
  resumeCheckpointRestored = false;
236100
236130
  messageTimes = new WeakMap();
236101
236131
  constructor(agent, persistence) {
@@ -236119,8 +236149,15 @@ var init_records = __esmMin((() => {
236119
236149
  created_at: Date.now()
236120
236150
  });
236121
236151
  this.metadataInitialized = true;
236122
- }
236123
- if (stamped.type === "metadata") this.metadataInitialized = true;
236152
+
236153
+ this.persistedProtocolVersion = "1.4";
236154
+ }
236155
+ if (stamped.type === 'metadata') {
236156
+ if (!this.metadataInitialized) {
236157
+ this.persistedProtocolVersion = stamped.protocol_version;
236158
+ }
236159
+ this.metadataInitialized = true;
236160
+ }
236124
236161
  this.persistence?.append(stamped);
236125
236162
  }
236126
236163
  restore(record) {
@@ -236134,7 +236171,8 @@ var init_records = __esmMin((() => {
236134
236171
  }
236135
236172
  }
236136
236173
  async replay(options = {}) {
236137
- if (!this.persistence) throw new Error("No persistence provided for AgentRecords");
236174
+ this.persistedProtocolVersion = undefined;
236175
+ if (!this.persistence) throw new Error("No persistence provided for AgentRecords");
236138
236176
  this.resumeCheckpointRestored = false;
236139
236177
  const rewriteMigratedRecords = options.rewriteMigratedRecords ?? true;
236140
236178
  if (rewriteMigratedRecords && await this.tryReplayResumeCheckpoint()) return {};
@@ -236152,6 +236190,7 @@ var init_records = __esmMin((() => {
236152
236190
  hasMetadata = true;
236153
236191
  this.metadataInitialized = true;
236154
236192
  const readVersion = record.protocol_version;
236193
+ this.persistedProtocolVersion = readVersion;
236155
236194
  if (isNewerWireVersion(readVersion)) {
236156
236195
  warning = `Session wire protocol version ${readVersion} is newer than the current version 1.4. Records will be replayed without migration.`;
236157
236196
  shouldRewrite = false;
@@ -236174,6 +236213,7 @@ var init_records = __esmMin((() => {
236174
236213
  if (completed && shouldRewrite && replayedRecords !== void 0) {
236175
236214
  this.persistence.rewrite(replayedRecords);
236176
236215
  await this.persistence.flush();
236216
+ this.persistedProtocolVersion = "1.4";
236177
236217
  }
236178
236218
  if (completed && this.persistence instanceof FileSystemAgentRecordPersistence) this.persistence.persistedRecordCount = replayedRecordCount;
236179
236219
  if (completed && this.agent.blobStore !== void 0) for (const msg of this.agent.context.history) await this.agent.blobStore.rehydrateParts(msg.content);
@@ -236184,8 +236224,17 @@ var init_records = __esmMin((() => {
236184
236224
  if (this.agent.replayBuilder.options.range !== void 0) return false;
236185
236225
  const checkpoint = await loadResumeCheckpoint({ wirePath: this.persistence.filePath });
236186
236226
  if (!checkpoint.ok) return false;
236227
+ let sourceProtocol;
236228
+ for await (const metadata of this.persistence.read()) {
236229
+ if (metadata.type !== 'metadata' || metadata.protocol_version !== "1.4") return false;
236230
+ sourceProtocol = metadata.protocol_version;
236231
+ break;
236232
+ }
236233
+ if (sourceProtocol === undefined) return false;
236234
+
236187
236235
  if (!restoreAgentResumeSnapshot(this.agent, checkpoint.snapshot)) return false;
236188
236236
  this.metadataInitialized = true;
236237
+ this.persistedProtocolVersion = sourceProtocol;
236189
236238
  let tailRecordCount = 0;
236190
236239
  for await (const record of this.persistence.readFrom(checkpoint.offset, checkpoint.recordCount)) {
236191
236240
  this.restore(record);
@@ -265678,34 +265727,52 @@ var init_web_search = __esmMin((() => {
265678
265727
  execute: (ctx) => this.execution(args, ctx)
265679
265728
  };
265680
265729
  }
265681
- async execution(args, { toolCallId }) {
265682
- try {
265683
- const opts = { toolCallId };
265684
- const results = await this.provider.search(args.query, opts);
265685
- const builder = new ToolResultBuilder({ maxLineLength: null });
265686
- if (results.length === 0) {
265687
- builder.write("No search results found.");
265688
- return builder.ok();
265689
- }
265690
- let first = true;
265691
- for (const result of results) {
265692
- if (!first) builder.write("---\n\n");
265693
- first = false;
265694
- builder.write(`Title: ${result.title}\n`);
265695
- if (result.siteName) builder.write(`Site: ${result.siteName}\n`);
265696
- if (result.date) builder.write(`Date: ${result.date}\n`);
265697
- builder.write(`URL: ${result.url}\n`);
265698
- builder.write(`Snippet: ${result.snippet}\n\n`);
265699
- }
265700
- builder.write("When you rely on a result in your answer, cite it inline as a markdown link, e.g. [title](url).");
265701
- return builder.ok();
265702
- } catch (error) {
265703
- return {
265704
- isError: true,
265705
- output: classifySearchError(error)
265706
- };
265707
- }
265708
- }
265730
+ async execution(args, { toolCallId, }) {
265731
+ try {
265732
+ let failedSources = 0;
265733
+ const opts = { toolCallId, onNotice: notice => {
265734
+ if (notice.code === 'partial-sources' && Number.isInteger(notice.failedSources)
265735
+ && notice.failedSources > 0 && notice.failedSources <= 128) {
265736
+ failedSources = Math.max(failedSources, notice.failedSources);
265737
+ }
265738
+ } };
265739
+ const results = await this.provider.search(args.query, opts);
265740
+ const builder = new ToolResultBuilder({ maxLineLength: null });
265741
+ if (failedSources > 0) {
265742
+ if (results.length === 0)
265743
+ throw new Error('Search sources failed without usable results.');
265744
+ builder.write(`Some search sources failed (${failedSources}). Results below are partial, not a complete search.\n\n`);
265745
+ }
265746
+ if (results.length === 0) {
265747
+ builder.write('No search results found.');
265748
+ return builder.ok();
265749
+ }
265750
+ let first = true;
265751
+ for (const result of results) {
265752
+ if (!first)
265753
+ builder.write('---\n\n');
265754
+ first = false;
265755
+ builder.write(`Title: ${result.title}\n`);
265756
+ if (result.siteName)
265757
+ builder.write(`Site: ${result.siteName}\n`);
265758
+ if (result.date)
265759
+ builder.write(`Date: ${result.date}\n`);
265760
+ builder.write(`URL: ${result.url}\n`);
265761
+ builder.write(`Snippet: ${result.snippet}\n\n`);
265762
+ }
265763
+ // Keep the citation reminder next to the data (not just in the static tool
265764
+ // description), so it is present on every search. Cite the page actually
265765
+ // relied on — after a FetchURL follow-up, that is the fetched page.
265766
+ builder.write('When you rely on a result in your answer, cite it inline as a markdown link, e.g. [title](url).');
265767
+ return builder.ok();
265768
+ }
265769
+ catch (error) {
265770
+ return {
265771
+ isError: true,
265772
+ output: classifySearchError(error),
265773
+ };
265774
+ }
265775
+ }
265709
265776
  };
265710
265777
  }));
265711
265778
  //#endregion
@@ -298151,13 +298218,11 @@ function resolveStdioCwd(configCwd, defaultCwd) {
298151
298218
  if (defaultCwd !== void 0 && !isAbsolute$2(configCwd)) return resolve$2(defaultCwd, configCwd);
298152
298219
  return configCwd;
298153
298220
  }
298154
- function mergeStdioEnv(configEnv, parentEnv = process.env) {
298155
- const merged = {};
298156
- for (const [key, value] of Object.entries(parentEnv)) if (value !== void 0) merged[key] = value;
298157
- if (configEnv !== void 0) Object.assign(merged, configEnv);
298158
- Object.assign(merged, proxyEnvForChild(merged));
298159
- reconcileChildNoProxy(merged, configEnv);
298160
- return merged;
298221
+ function mergeStdioEnv(configEnv, parentEnv = process.env, runtimeEnv) {
298222
+ const merged = overlayRuntimeEnv({ ...parentEnv, ...configEnv }, runtimeEnv);
298223
+ Object.assign(merged, proxyEnvForChild(merged));
298224
+ reconcileChildNoProxy(merged, overlayRuntimeEnv(configEnv ?? {}, runtimeEnv));
298225
+ return merged;
298161
298226
  }
298162
298227
  var STDERR_BUFFER_CAPACITY, StdioMcpClient, BoundedTail;
298163
298228
  var init_client_stdio = __esmMin((() => {
@@ -298187,7 +298252,7 @@ var init_client_stdio = __esmMin((() => {
298187
298252
  this.transport = new StdioClientTransport({
298188
298253
  command: config.command,
298189
298254
  args: config.args,
298190
- env: mergeStdioEnv(config.env, config.inheritEnv === false ? {} : process.env),
298255
+ env: mergeStdioEnv(config.env, config.inheritEnv === false ? {} : process.env, options.runtimeEnv),
298191
298256
  cwd: resolveStdioCwd(config.cwd, options.defaultCwd),
298192
298257
  stderr: "pipe"
298193
298258
  });
@@ -298689,22 +298754,32 @@ var init_connection_manager = __esmMin((() => {
298689
298754
  return entry.attemptId;
298690
298755
  }
298691
298756
  createClient(config, name) {
298692
- const toolCallTimeoutMs = config.toolTimeoutMs;
298693
- if (config.transport === "stdio") return new StdioMcpClient(config, {
298694
- toolCallTimeoutMs,
298695
- defaultCwd: this.options.stdioCwd
298696
- });
298697
- if (config.transport === "sse") return new SseMcpClient(config, {
298698
- toolCallTimeoutMs,
298699
- envLookup: this.options.envLookup,
298700
- oauthProvider: this.resolveOAuthProvider(config, name)
298701
- });
298702
- return new HttpMcpClient(config, {
298703
- toolCallTimeoutMs,
298704
- envLookup: this.options.envLookup,
298705
- oauthProvider: this.resolveOAuthProvider(config, name)
298706
- });
298707
- }
298757
+ const toolCallTimeoutMs = config.toolTimeoutMs;
298758
+ if (config.transport === 'stdio') {
298759
+ let runtimeEnv;
298760
+ try {
298761
+ runtimeEnv = this.options.stdioRuntimeEnv?.(name, config);
298762
+ }
298763
+ catch {
298764
+ throw new Error('MCP runtime environment is unavailable.');
298765
+ }
298766
+ return new StdioMcpClient(config, {
298767
+ toolCallTimeoutMs, defaultCwd: this.options.stdioCwd, runtimeEnv,
298768
+ });
298769
+ }
298770
+ if (config.transport === 'sse') {
298771
+ return new SseMcpClient(config, {
298772
+ toolCallTimeoutMs,
298773
+ envLookup: this.options.envLookup,
298774
+ oauthProvider: this.resolveOAuthProvider(config, name),
298775
+ });
298776
+ }
298777
+ return new HttpMcpClient(config, {
298778
+ toolCallTimeoutMs,
298779
+ envLookup: this.options.envLookup,
298780
+ oauthProvider: this.resolveOAuthProvider(config, name),
298781
+ });
298782
+ }
298708
298783
  resolveOAuthProvider(config, name) {
298709
298784
  const oauthService = this.oauthService;
298710
298785
  if (oauthService === void 0) return void 0;
@@ -299417,6 +299492,43 @@ var init_lsp = __esmMin((() => {
299417
299492
  init_manager$1();
299418
299493
  }));
299419
299494
  //#endregion
299495
+
299496
+ import { randomBytes as releaseRecordRandomBytes } from "node:crypto";
299497
+ const EMPTY_RUNTIME = Object.freeze({
299498
+ AGENTSPINE_KING_TIMELINE_SOURCE: '',
299499
+ AGENTSPINE_KING_WIRE_PROTOCOL_VERSION: '',
299500
+ AGENTSPINE_TIMELINE_TRANSPORT_SESSION_ID: '',
299501
+ AGENTSPINE_TIMELINE_SESSION_CAPABILITY: '',
299502
+ });
299503
+ class AgentSpineSessionRuntime {
299504
+ #env = EMPTY_RUNTIME;
299505
+ #hooks;
299506
+ #servers;
299507
+ constructor(recipients) {
299508
+ this.#hooks = new Set(recipients?.hooks);
299509
+ this.#servers = new Map(Object.entries(recipients?.mcpServers ?? {}));
299510
+ }
299511
+ get hasRecipients() {
299512
+ return this.#hooks.size > 0 || this.#servers.size > 0;
299513
+ }
299514
+ bind(sessionId, source) {
299515
+ this.#env = EMPTY_RUNTIME;
299516
+ if (!sessionId || !/^[A-Za-z0-9][A-Za-z0-9:_.@/-]{0,127}$/.test(sessionId) || !source)
299517
+ return;
299518
+ this.#env = Object.freeze({
299519
+ AGENTSPINE_KING_TIMELINE_SOURCE: source.path,
299520
+ AGENTSPINE_KING_WIRE_PROTOCOL_VERSION: source.protocolVersion,
299521
+ AGENTSPINE_TIMELINE_TRANSPORT_SESSION_ID: sessionId,
299522
+ AGENTSPINE_TIMELINE_SESSION_CAPABILITY: `astc_${releaseRecordRandomBytes(32).toString('base64url')}`,
299523
+ });
299524
+ }
299525
+ forHook(hook) {
299526
+ return this.#hooks.has(hook) ? this.#env : EMPTY_RUNTIME;
299527
+ }
299528
+ forMcp(name, config) {
299529
+ return config.transport === 'stdio' && this.#servers.get(name) === config ? this.#env : EMPTY_RUNTIME;
299530
+ }
299531
+ }
299420
299532
  //#region ../../packages/agent-core/src/session/index.ts
299421
299533
  async function waitForSettlementOrTimeout(promise, timeoutMs) {
299422
299534
  let timeout;
@@ -299497,6 +299609,16 @@ var init_session$1 = __esmMin((() => {
299497
299609
  writeMetadataPromise = Promise.resolve();
299498
299610
  agentsMdWarning;
299499
299611
  accountMemoryContext = "";
299612
+ agentSpineRuntime;
299613
+ mcpLoadStarted = false;
299614
+ startMcpServers() {
299615
+ if (this.mcpLoadStarted)
299616
+ return;
299617
+ this.mcpLoadStarted = true;
299618
+ void this.loadMcpServers().catch((error) => {
299619
+ this.emitInitialMcpLoadError(error);
299620
+ });
299621
+ }
299500
299622
  constructor(options) {
299501
299623
  this.options = options;
299502
299624
  this.logHandle = options.id === void 0 ? void 0 : getRootLogger().attachSession({
@@ -299506,6 +299628,8 @@ var init_session$1 = __esmMin((() => {
299506
299628
  this.log = this.logHandle?.logger ?? (options.id === void 0 ? log : log.createChild({ sessionId: options.id }));
299507
299629
  this.rpc = options.rpc;
299508
299630
  this.experimentalFlags = options.experimentalFlags ?? new FlagResolver();
299631
+ this.agentSpineRuntime = this.experimentalFlags.enabled('agent_spine_timeline')
299632
+ ? new AgentSpineSessionRuntime(options.agentSpineRuntime) : undefined;
299509
299633
  this.visionReader = createManagedMediaVisionReader(options.toolServices?.media) ?? (this.experimentalFlags.enabled("vision_reader") ? createVisionReader(options.config?.services?.visionReader) : void 0);
299510
299634
  this.hookEngine = new HookEngine(options.hooks, {
299511
299635
  cwd: options.kaos.getcwd(),
@@ -299534,8 +299658,13 @@ var init_session$1 = __esmMin((() => {
299534
299658
  code: "HOST_HOOK_WARNING",
299535
299659
  message
299536
299660
  });
299537
- }
299538
- });
299661
+ },
299662
+ runtimeEnv: (hook) => {
299663
+ const hostEnv = options.hookRuntimeEnv?.(hook);
299664
+ const timelineEnv = this.agentSpineRuntime?.forHook(hook);
299665
+ return timelineEnv === undefined ? hostEnv : { ...hostEnv, ...timelineEnv };
299666
+ }
299667
+ });
299539
299668
  this.telemetry = options.telemetry ?? noopTelemetryClient;
299540
299669
  this.toolKaos = options.kaos;
299541
299670
  this.persistenceKaos = options.persistenceKaos ?? options.kaos;
@@ -299546,8 +299675,13 @@ var init_session$1 = __esmMin((() => {
299546
299675
  this.mcp = new McpConnectionManager({
299547
299676
  oauthService: new McpOAuthService({ blunHomeDir: resolveSharedBlunHome(options.blunHomeDir) }),
299548
299677
  log: this.log,
299549
- stdioCwd: options.kaos.getcwd()
299550
- });
299678
+ stdioCwd: options.kaos.getcwd(),
299679
+ stdioRuntimeEnv: (name, config) => {
299680
+ const hostEnv = options.mcpRuntimeEnv?.(name, config);
299681
+ const timelineEnv = this.agentSpineRuntime?.forMcp(name, config);
299682
+ return timelineEnv === undefined ? hostEnv : { ...hostEnv, ...timelineEnv };
299683
+ }
299684
+ });
299551
299685
  this.lsp = options.lspServers === void 0 || Object.keys(options.lspServers).length === 0 ? void 0 : new LspManager({
299552
299686
  cwd: options.kaos.getcwd(),
299553
299687
  servers: options.lspServers
@@ -299560,9 +299694,7 @@ var init_session$1 = __esmMin((() => {
299560
299694
  }).then(() => {
299561
299695
  this.refreshAgentBuiltinTools();
299562
299696
  });
299563
- this.loadMcpServers().catch((error) => {
299564
- this.emitInitialMcpLoadError(error);
299565
- });
299697
+ if (!this.agentSpineRuntime?.hasRecipients) this.startMcpServers();
299566
299698
  }
299567
299699
  setToolKaos(kaos) {
299568
299700
  this.toolKaos = kaos;
@@ -300208,7 +300340,18 @@ var init_session$1 = __esmMin((() => {
300208
300340
  });
300209
300341
  }
300210
300342
  async triggerSessionStart(source) {
300211
- const results = await this.hookEngine.trigger("SessionStart", {
300343
+ if (this.agentSpineRuntime?.hasRecipients && !this.mcpLoadStarted) {
300344
+ try {
300345
+ const main = this.getReadyAgent('main');
300346
+ await main?.records.flush();
300347
+ this.agentSpineRuntime.bind(this.options.id, main?.records.fileSource);
300348
+ }
300349
+ catch {
300350
+ this.log.warn('Native session record source is unavailable.');
300351
+ }
300352
+ this.startMcpServers();
300353
+ }
300354
+ const results = await this.hookEngine.trigger("SessionStart", {
300212
300355
  matcherValue: source,
300213
300356
  inputData: { source }
300214
300357
  });
@@ -315095,43 +315238,80 @@ var init_blun_web_search = __esmMin((() => {
315095
315238
  this.fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
315096
315239
  }
315097
315240
  async search(query, options) {
315098
- const bodyJson = JSON.stringify({ text_query: query });
315099
- const toolCallId = options?.toolCallId;
315100
- const response = await this.post(bodyJson, toolCallId);
315101
- if (response.status === 401) {
315102
- const detail = await safeReadText(response);
315103
- throw new Error(`Blun search request failed: HTTP 401 (auth/unauthorized). ${detail}`.trim());
315104
- }
315105
- if (response.status !== 200) {
315106
- const detail = await safeReadText(response);
315107
- throw new Error(`Blun search request failed: HTTP ${String(response.status)}. ${detail}`.trim());
315108
- }
315109
- const json = await response.json();
315110
- return (Array.isArray(json.search_results) ? json.search_results : []).map((r) => {
315111
- const out = {
315112
- title: r.title ?? "",
315113
- url: r.url ?? "",
315114
- snippet: r.snippet ?? ""
315115
- };
315116
- if (typeof r.date === "string" && r.date.length > 0) out.date = r.date;
315117
- if (typeof r.site_name === "string" && r.site_name.length > 0) out.siteName = r.site_name;
315118
- return out;
315119
- });
315120
- }
315121
- async post(bodyJson, toolCallId) {
315122
- const accessToken = await this.resolveApiKey();
315123
- return this.fetchImpl(this.baseUrl, {
315124
- method: "POST",
315125
- headers: {
315126
- ...this.defaultHeaders,
315127
- Authorization: `Bearer ${accessToken}`,
315128
- "Content-Type": "application/json",
315129
- ...toolCallId !== void 0 && toolCallId.length > 0 ? { "X-Msh-Tool-Call-Id": toolCallId } : {},
315130
- ...this.customHeaders
315131
- },
315132
- body: bodyJson
315133
- });
315134
- }
315241
+ const body = { text_query: query };
315242
+ const bodyJson = JSON.stringify(body);
315243
+ const toolCallId = options?.toolCallId;
315244
+ const supportsNotices = typeof options?.onNotice === 'function';
315245
+ const response = await this.post(bodyJson, toolCallId, supportsNotices);
315246
+ if (response.status === 401) {
315247
+ const detail = await safeReadText(response);
315248
+ throw new Error(`Blun search request failed: HTTP 401 (auth/unauthorized). ${detail}`.trim());
315249
+ }
315250
+ if (response.status !== 200) {
315251
+ const detail = await safeReadText(response);
315252
+ throw new Error(`Blun search request failed: HTTP ${String(response.status)}. ${detail}`.trim());
315253
+ }
315254
+ const json = (await response.json());
315255
+ const raw = Array.isArray(json.search_results) ? json.search_results : [];
315256
+ const status = json.search_status;
315257
+ if (status !== undefined) {
315258
+ if (!status || typeof status !== 'object' || !Array.isArray(json.search_results)
315259
+ || !Number.isInteger(status.failed_sources) || typeof status.failed_sources !== 'number'
315260
+ || status.failed_sources < 0 || status.failed_sources > 128
315261
+ || (status.state !== 'complete' && status.state !== 'partial')
315262
+ || (status.state === 'complete' && status.failed_sources !== 0)
315263
+ || (status.state === 'partial' && (status.failed_sources === 0 || raw.length === 0))) {
315264
+ throw new Error('Invalid search status response.');
315265
+ }
315266
+ if (!raw.every(row => {
315267
+ if (!row || typeof row.title !== 'string' || !row.title.trim()
315268
+ || typeof row.snippet !== 'string' || typeof row.url !== 'string')
315269
+ return false;
315270
+ try {
315271
+ const url = new URL(row.url);
315272
+ return (url.protocol === 'https:' || url.protocol === 'http:') && !url.username && !url.password;
315273
+ }
315274
+ catch {
315275
+ return false;
315276
+ }
315277
+ }))
315278
+ throw new Error('Invalid search results response.');
315279
+ if (status.state === 'partial') {
315280
+ if (!supportsNotices)
315281
+ throw new Error('Partial search results require a notice consumer.');
315282
+ options?.onNotice?.({ code: 'partial-sources', failedSources: status.failed_sources });
315283
+ }
315284
+ }
315285
+ return raw.map((r) => {
315286
+ const out = {
315287
+ title: r.title ?? '',
315288
+ url: r.url ?? '',
315289
+ snippet: r.snippet ?? '',
315290
+ };
315291
+ if (typeof r.date === 'string' && r.date.length > 0)
315292
+ out.date = r.date;
315293
+ if (typeof r.site_name === 'string' && r.site_name.length > 0)
315294
+ out.siteName = r.site_name;
315295
+ return out;
315296
+ });
315297
+ }
315298
+ async post(bodyJson, toolCallId, supportsNotices) {
315299
+ const accessToken = await this.resolveApiKey();
315300
+ return this.fetchImpl(this.baseUrl, {
315301
+ method: 'POST',
315302
+ headers: {
315303
+ ...this.defaultHeaders,
315304
+ Authorization: `Bearer ${accessToken}`,
315305
+ 'Content-Type': 'application/json',
315306
+ ...(toolCallId !== undefined && toolCallId.length > 0
315307
+ ? { 'X-Msh-Tool-Call-Id': toolCallId }
315308
+ : {}),
315309
+ ...this.customHeaders,
315310
+ ...(supportsNotices ? { 'X-BLUN-Search-Contract': 'partial-v1' } : {}),
315311
+ },
315312
+ body: bodyJson,
315313
+ });
315314
+ }
315135
315315
  async resolveApiKey() {
315136
315316
  if (this.tokenProvider !== void 0) try {
315137
315317
  return await this.tokenProvider.getAccessToken();
@@ -318903,6 +319083,7 @@ var init_core_impl = __esmMin((() => {
318903
319083
  const pluginSessionStarts = this.plugins.enabledSessionStarts();
318904
319084
  const pluginCommands = await this.plugins.enabledCommands();
318905
319085
  const mcpConfig = this.mergePluginMcpConfig(withCallerMcp);
319086
+ const pluginHooks = this.plugins.enabledHooks();
318906
319087
  const runtime = await this.resolveRuntime(config);
318907
319088
  const session = new Session$1({
318908
319089
  kaos: parentKaos.withCwd(workDir),
@@ -318915,7 +319096,8 @@ var init_core_impl = __esmMin((() => {
318915
319096
  rpc: proxyWithExtraPayload(await this.sdk, { sessionId: summary.id }),
318916
319097
  providerManager: this.resolveProviderManager(summary.id),
318917
319098
  background: config.background,
318918
- hooks: [...config.hooks ?? [], ...this.plugins.enabledHooks()],
319099
+ hooks: [...(config.hooks ?? []), ...pluginHooks],
319100
+ agentSpineRuntime: this.agentSpineRuntimeRecipients(pluginHooks, mcpConfig),
318919
319101
  permissionRules: config.permission?.rules,
318920
319102
  skills: this.resolveSessionSkillConfig(config),
318921
319103
  mcpConfig,
@@ -318998,6 +319180,7 @@ var init_core_impl = __esmMin((() => {
318998
319180
  const pluginSessionStarts = this.plugins.enabledSessionStarts();
318999
319181
  const pluginCommands = await this.plugins.enabledCommands();
319000
319182
  const mcpConfig = this.mergePluginMcpConfig(withCallerMcp);
319183
+ const pluginHooks = this.plugins.enabledHooks();
319001
319184
  const runtime = await this.resolveRuntime(config);
319002
319185
  const parentKaos = parentKaosForRead;
319003
319186
  const persistenceKaos = overrides.persistenceKaos ?? parentKaos;
@@ -319012,7 +319195,8 @@ var init_core_impl = __esmMin((() => {
319012
319195
  rpc: proxyWithExtraPayload(await this.sdk, { sessionId: summary.id }),
319013
319196
  providerManager: this.resolveProviderManager(summary.id),
319014
319197
  background: config.background,
319015
- hooks: [...config.hooks ?? [], ...this.plugins.enabledHooks()],
319198
+ hooks: [...(config.hooks ?? []), ...pluginHooks],
319199
+ agentSpineRuntime: this.agentSpineRuntimeRecipients(pluginHooks, mcpConfig),
319016
319200
  permissionRules: config.permission?.rules,
319017
319201
  skills: this.resolveSessionSkillConfig(config),
319018
319202
  mcpConfig,
@@ -319468,6 +319652,21 @@ var init_core_impl = __esmMin((() => {
319468
319652
  }
319469
319653
  };
319470
319654
  }
319655
+ agentSpineRuntimeRecipients(pluginHooks, mcpConfig) {
319656
+ if (!this.experimentalFlags.enabled('agent_spine_timeline'))
319657
+ return undefined;
319658
+ const plugin = this.plugins.get('agent-spine');
319659
+ if (plugin?.state !== 'ok' || !plugin.enabled)
319660
+ return undefined;
319661
+ // These objects came from PluginManager, not caller hooks or session metadata.
319662
+ const hooks = pluginHooks.filter(hook => hook.env?.['BLUN_PLUGIN_ROOT'] === plugin.root);
319663
+ const mcpServers = Object.fromEntries(Object.entries(mcpConfig?.servers ?? {}).filter(([name, config]) => {
319664
+ const origin = mcpConfig?.origins?.[name];
319665
+ return origin?.source === 'plugin' && origin.pluginId === plugin.id && origin.pluginEnabled
319666
+ && config.transport === 'stdio' && config.enabled !== false;
319667
+ }));
319668
+ return hooks.length > 0 || Object.keys(mcpServers).length > 0 ? { hooks, mcpServers } : undefined;
319669
+ }
319471
319670
  async resolveCurrentMcpConfig(session) {
319472
319671
  const snapshot = session.mcpConfigSnapshot();
319473
319672
  const callerServers = Object.fromEntries(Object.entries(snapshot?.servers ?? {}).filter(([name]) => snapshot?.origins?.[name]?.source === "session"));
@@ -525292,3 +525491,23 @@ async function logStartupFailure(operation, error) {
525292
525491
  }
525293
525492
  //#endregion
525294
525493
  export { configureTelegramPluginLaunchMode, createBlunHarness, createBlunHostIdentity, handleMainCommand, isDirectCliEntry, main };
525494
+
525495
+ import { randomUUID as releasePromptUUID } from "node:crypto";
525496
+ function overlayRuntimeEnv(base, runtime) {
525497
+ const result = {};
525498
+ for (const [key, value] of Object.entries(base)) {
525499
+ if (value !== undefined)
525500
+ result[key] = value;
525501
+ }
525502
+ for (const [key, value] of Object.entries(runtime ?? {})) {
525503
+ // Node selects one casing on Windows; leaving both can preserve a stale value.
525504
+ if (process.platform === 'win32') {
525505
+ for (const existing of Object.keys(result)) {
525506
+ if (existing.toUpperCase() === key.toUpperCase())
525507
+ delete result[existing];
525508
+ }
525509
+ }
525510
+ result[key] = value;
525511
+ }
525512
+ return result;
525513
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.569",
3
+ "version": "9.1.570",
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": {
@@ -39,7 +39,8 @@
39
39
  "agent-spine-plugin/.codex-plugin/",
40
40
  "agent-spine-plugin/.mcp.json",
41
41
  "telegram-plugin/",
42
- "LIESMICH.txt"
42
+ "LIESMICH.txt",
43
+ "CHANGELOG.md"
43
44
  ],
44
45
  "keywords": [
45
46
  "blun",
@@ -151,7 +151,7 @@ function save({ home, project, report }) {
151
151
  }
152
152
  }
153
153
  }
154
- const terms = text => [...new Set(text.toLocaleLowerCase('en').match(/[\p{L}\p{N}]{2,}/gu) || [])];
154
+ const terms = text => [...new Set(text.normalize('NFC').toLocaleLowerCase('en').match(/[\p{L}\p{N}]{2,}/gu) || [])];
155
155
  function query({ home, project, query: question, limit = 5, maxChars = 8000, offset = 0, expectedCollectionSha256 }) {
156
156
  const output = { schema: 'blun.research-recall/v1', availability: 'unavailable', matches: [], diagnostics: [],
157
157
  sourceVerified: false, retrieval: 'lexical-artifact-search', bytesRead: 0, limited: false,