blun-king-cli 9.1.525 → 9.1.527
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 +12 -0
- package/LIESMICH.txt +1 -1
- package/README.md +1 -1
- package/bin/reload-plugin-bootstrap.cjs +18 -0
- package/blun.mjs +54 -0
- package/package.json +4 -2
- package/scripts/check-copy-command-regression.js +74 -0
- package/scripts/check-reload-agent-spine-regression.js +76 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 9.1.527 - 2026-09-01
|
|
4
|
+
|
|
5
|
+
- Adds DeepAgents' `/copy` workflow to copy the latest completed, non-empty assistant response through BLUN's existing cross-platform clipboard path.
|
|
6
|
+
- Keeps `/copy` available during active turns, where the existing command queue runs it after the response completes, and reports empty-history or clipboard failures in all six interface languages.
|
|
7
|
+
- Adds a red-to-green regression for command registration, dispatch, response selection, byte-preserving clipboard content, and released UI text.
|
|
8
|
+
|
|
9
|
+
## 9.1.526 - 2026-09-01
|
|
10
|
+
|
|
11
|
+
- Materializes the AgentSpine version bundled with the current package before `/reload` rebuilds the plugin catalogue and MCP configuration.
|
|
12
|
+
- Preserves unrelated plugin records and reuses an unchanged managed AgentSpine tree without rewriting `installed.json`.
|
|
13
|
+
- Adds a red-to-green regression against 9.1.525 for bootstrap ordering, managed-plugin adoption, and idempotent reload behavior.
|
|
14
|
+
|
|
3
15
|
## 9.1.525 - 2026-09-01
|
|
4
16
|
|
|
5
17
|
- Starts every local shell command in its own process group on Windows as well as POSIX, so programs cannot attach to the BLUN TUI's controlling console and consume `Escape`, `Ctrl+S`, or other editor input.
|
package/LIESMICH.txt
CHANGED
package/README.md
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
const { installManagedAgentSpinePlugin } = require('./plugin-bootstrap');
|
|
6
|
+
|
|
7
|
+
function installBundledAgentSpineForReload(options = {}) {
|
|
8
|
+
const packageRoot = path.resolve(options.packageRoot || path.join(__dirname, '..'));
|
|
9
|
+
const blunDir = String(options.blunDir || '').trim();
|
|
10
|
+
if (blunDir.length === 0) throw new Error('BLUN profile directory is required for plugin reload');
|
|
11
|
+
return installManagedAgentSpinePlugin({
|
|
12
|
+
packageRoot,
|
|
13
|
+
blunDir: path.resolve(blunDir),
|
|
14
|
+
now: options.now,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = { installBundledAgentSpineForReload };
|
package/blun.mjs
CHANGED
|
@@ -317353,6 +317353,7 @@ var init_core_impl = __esmMin((() => {
|
|
|
317353
317353
|
if (active?.hasActiveTurn === true) throw new BlunError(ErrorCodes.TURN_AGENT_BUSY, `Session "${summary.id}" cannot be reloaded while a turn is running`, { details: { sessionId: summary.id } });
|
|
317354
317354
|
this.reloadProviderManager();
|
|
317355
317355
|
this.clearRuntimeCache();
|
|
317356
|
+
createRequire(import.meta.url)("./bin/reload-plugin-bootstrap.cjs").installBundledAgentSpineForReload({ blunDir: this.homeDir });
|
|
317356
317357
|
await this.reloadPlugins({});
|
|
317357
317358
|
if (active !== void 0) {
|
|
317358
317359
|
await active.closeForReload();
|
|
@@ -403289,6 +403290,10 @@ registerUiCatalogFragment({
|
|
|
403289
403290
|
"command.telegramAccess.description": "Manage the Telegram bot token and allowed chats",
|
|
403290
403291
|
"command.add-dir.description": "Add or list an additional workspace directory",
|
|
403291
403292
|
"command.experiments.description": "Manage experimental features",
|
|
403293
|
+
"command.copy.description": "Copy the latest assistant response to the clipboard.",
|
|
403294
|
+
"copy.status.copied": "Copied the latest response to the clipboard.",
|
|
403295
|
+
"copy.status.none": "There is no response to copy yet.",
|
|
403296
|
+
"copy.error.failed": "Could not copy the latest response: {error}",
|
|
403292
403297
|
"command.reload.description": "Reload the session and apply runtime and interface settings",
|
|
403293
403298
|
"command.reload-tui.description": "Reload interface settings only",
|
|
403294
403299
|
"command.compact.description": "Compact the conversation context",
|
|
@@ -403348,6 +403353,10 @@ registerUiCatalogFragment({
|
|
|
403348
403353
|
"command.telegramAccess.description": "Telegram-Bot-Token und freigegebene Chats verwalten",
|
|
403349
403354
|
"command.add-dir.description": "Zusätzlichen Arbeitsbereichsordner hinzufügen oder vorhandene auflisten",
|
|
403350
403355
|
"command.experiments.description": "Experimentelle Funktionen verwalten",
|
|
403356
|
+
"command.copy.description": "Neueste Assistentenantwort in die Zwischenablage übernehmen.",
|
|
403357
|
+
"copy.status.copied": "Die neueste Antwort wurde in die Zwischenablage übernommen.",
|
|
403358
|
+
"copy.status.none": "Es ist noch keine Antwort zum Kopieren verfügbar.",
|
|
403359
|
+
"copy.error.failed": "Die neueste Antwort konnte nicht kopiert werden: {error}",
|
|
403351
403360
|
"command.reload.description": "Sitzung neu laden und Runtime- sowie Oberflächeneinstellungen anwenden",
|
|
403352
403361
|
"command.reload-tui.description": "Nur Oberflächeneinstellungen neu laden",
|
|
403353
403362
|
"command.compact.description": "Konversationskontext komprimieren",
|
|
@@ -403407,6 +403416,10 @@ registerUiCatalogFragment({
|
|
|
403407
403416
|
"command.telegramAccess.description": "Gestionar el token del bot de Telegram y los chats permitidos",
|
|
403408
403417
|
"command.add-dir.description": "Añadir o listar directorios adicionales del espacio de trabajo",
|
|
403409
403418
|
"command.experiments.description": "Gestionar funciones experimentales",
|
|
403419
|
+
"command.copy.description": "Copiar la respuesta más reciente del asistente al portapapeles.",
|
|
403420
|
+
"copy.status.copied": "La respuesta más reciente se copió al portapapeles.",
|
|
403421
|
+
"copy.status.none": "Todavía no hay ninguna respuesta que copiar.",
|
|
403422
|
+
"copy.error.failed": "No se pudo copiar la respuesta más reciente: {error}",
|
|
403410
403423
|
"command.reload.description": "Recargar la sesión y aplicar los ajustes del entorno de ejecución y de la interfaz",
|
|
403411
403424
|
"command.reload-tui.description": "Recargar solo los ajustes de la interfaz",
|
|
403412
403425
|
"command.compact.description": "Compactar el contexto de la conversación",
|
|
@@ -403466,6 +403479,10 @@ registerUiCatalogFragment({
|
|
|
403466
403479
|
"command.telegramAccess.description": "Gérer le jeton du bot Telegram et les discussions autorisées",
|
|
403467
403480
|
"command.add-dir.description": "Ajouter ou répertorier un répertoire d’espace de travail supplémentaire",
|
|
403468
403481
|
"command.experiments.description": "Gérer les fonctions expérimentales",
|
|
403482
|
+
"command.copy.description": "Copier la réponse la plus récente de l’assistant dans le presse-papiers.",
|
|
403483
|
+
"copy.status.copied": "La réponse la plus récente a été copiée dans le presse-papiers.",
|
|
403484
|
+
"copy.status.none": "Aucune réponse à copier pour le moment.",
|
|
403485
|
+
"copy.error.failed": "Impossible de copier la réponse la plus récente : {error}",
|
|
403469
403486
|
"command.reload.description": "Recharger la session et appliquer les paramètres d’exécution et d’interface",
|
|
403470
403487
|
"command.reload-tui.description": "Recharger uniquement les paramètres de l’interface",
|
|
403471
403488
|
"command.compact.description": "Compacter le contexte de la conversation",
|
|
@@ -403525,6 +403542,10 @@ registerUiCatalogFragment({
|
|
|
403525
403542
|
"command.telegramAccess.description": "Hantera Telegram-botens API-token och tillåtna chattar",
|
|
403526
403543
|
"command.add-dir.description": "Lägg till eller lista en extra katalog för arbetsytan",
|
|
403527
403544
|
"command.experiments.description": "Hantera experimentella funktioner",
|
|
403545
|
+
"command.copy.description": "Kopiera assistentens senaste svar till urklipp.",
|
|
403546
|
+
"copy.status.copied": "Det senaste svaret kopierades till urklipp.",
|
|
403547
|
+
"copy.status.none": "Det finns inget svar att kopiera ännu.",
|
|
403548
|
+
"copy.error.failed": "Det gick inte att kopiera det senaste svaret: {error}",
|
|
403528
403549
|
"command.reload.description": "Läs in sessionen på nytt och tillämpa körningsmiljö- och gränssnittsinställningar",
|
|
403529
403550
|
"command.reload-tui.description": "Läs bara in gränssnittsinställningarna på nytt",
|
|
403530
403551
|
"command.compact.description": "Komprimera konversationskontexten",
|
|
@@ -403584,6 +403605,10 @@ registerUiCatalogFragment({
|
|
|
403584
403605
|
"command.telegramAccess.description": "Spravovat token telegramového bota a povolené chaty",
|
|
403585
403606
|
"command.add-dir.description": "Přidat nebo vypsat další adresář pracovního prostoru",
|
|
403586
403607
|
"command.experiments.description": "Spravovat experimentální funkce",
|
|
403608
|
+
"command.copy.description": "Zkopírovat nejnovější odpověď asistenta do schránky.",
|
|
403609
|
+
"copy.status.copied": "Nejnovější odpověď byla zkopírována do schránky.",
|
|
403610
|
+
"copy.status.none": "Zatím není k dispozici žádná odpověď ke zkopírování.",
|
|
403611
|
+
"copy.error.failed": "Nejnovější odpověď se nepodařilo zkopírovat: {error}",
|
|
403587
403612
|
"command.reload.description": "Znovu načíst relaci a použít nastavení běhového prostředí a rozhraní",
|
|
403588
403613
|
"command.reload-tui.description": "Znovu načíst pouze nastavení rozhraní",
|
|
403589
403614
|
"command.compact.description": "Komprimovat kontext konverzace",
|
|
@@ -403975,6 +404000,13 @@ const BUILTIN_SLASH_COMMAND_DEFINITIONS = [
|
|
|
403975
404000
|
priority: 60,
|
|
403976
404001
|
availability: "idle-only"
|
|
403977
404002
|
},
|
|
404003
|
+
{
|
|
404004
|
+
name: "copy",
|
|
404005
|
+
aliases: [],
|
|
404006
|
+
descriptionKey: "command.copy.description",
|
|
404007
|
+
priority: 40,
|
|
404008
|
+
availability: "always"
|
|
404009
|
+
},
|
|
403978
404010
|
{
|
|
403979
404011
|
name: "reload",
|
|
403980
404012
|
aliases: [],
|
|
@@ -496815,6 +496847,25 @@ async function handlePremortemCommand(host, args) {
|
|
|
496815
496847
|
}
|
|
496816
496848
|
host.sendMessage(host.requireSession(), plan, { parts: buildPremortemPrompt(plan) });
|
|
496817
496849
|
}
|
|
496850
|
+
function findLatestAssistantResponse(entries) {
|
|
496851
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
496852
|
+
const entry = entries[index];
|
|
496853
|
+
if (entry?.kind === "assistant" && typeof entry.content === "string" && entry.content.trim().length > 0) return entry.content;
|
|
496854
|
+
}
|
|
496855
|
+
}
|
|
496856
|
+
async function handleCopyCommand(host) {
|
|
496857
|
+
const text = findLatestAssistantResponse(host.state.transcriptEntries);
|
|
496858
|
+
if (text === void 0) {
|
|
496859
|
+
host.showStatus(uiText("copy.status.none"));
|
|
496860
|
+
return;
|
|
496861
|
+
}
|
|
496862
|
+
try {
|
|
496863
|
+
await copyTextToClipboard(text);
|
|
496864
|
+
host.showStatus(uiText("copy.status.copied"));
|
|
496865
|
+
} catch (error) {
|
|
496866
|
+
host.showError(uiText("copy.error.failed", { error: formatErrorMessage$2(error) }));
|
|
496867
|
+
}
|
|
496868
|
+
}
|
|
496818
496869
|
async function handleBuiltInSlashCommand(host, name, args) {
|
|
496819
496870
|
switch (name) {
|
|
496820
496871
|
case "exit":
|
|
@@ -496857,6 +496908,9 @@ async function handleBuiltInSlashCommand(host, name, args) {
|
|
|
496857
496908
|
case "experiments":
|
|
496858
496909
|
await showExperimentsPanel(host);
|
|
496859
496910
|
return;
|
|
496911
|
+
case "copy":
|
|
496912
|
+
await handleCopyCommand(host);
|
|
496913
|
+
return;
|
|
496860
496914
|
case "reload":
|
|
496861
496915
|
await handleReloadCommand(host);
|
|
496862
496916
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blun-king-cli",
|
|
3
|
-
"version": "9.1.
|
|
3
|
+
"version": "9.1.527",
|
|
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": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"test": "node --test test/*.test.js",
|
|
12
|
-
"prepack": "node scripts/check-release-metadata.js && node scripts/check-shell-terminal-isolation-regression.js && node scripts/check-todo-loop-regression.js && node scripts/check-telegram-loop-exactly-once-regression.js && node scripts/check-session-picker-resume-metrics-regression.js && node scripts/check-bundled-agent-spine-regression.js && node scripts/check-todo-recovery-catalog-regression.js && node scripts/check-historical-tool-result-preview-regression.js && node scripts/check-session-start-hook-context-regression.js && node scripts/check-queue-controls-regression.js && node scripts/check-approval-queue-shortcuts-regression.js && node scripts/check-approval-observability-regression.js && node scripts/check-slash-escape-regression.js && node scripts/check-telegram-bridge-watchdog.js && node scripts/check-resume-replay-regression.js && node scripts/check-session-cancel-regression.js && node scripts/check-plugin-startup-regression.js && node scripts/check-active-profile-plugin-startup.js && node scripts/check-mcp-startup-wait-budget.js",
|
|
12
|
+
"prepack": "node scripts/check-release-metadata.js && node scripts/check-copy-command-regression.js && node scripts/check-shell-terminal-isolation-regression.js && node scripts/check-todo-loop-regression.js && node scripts/check-telegram-loop-exactly-once-regression.js && node scripts/check-session-picker-resume-metrics-regression.js && node scripts/check-bundled-agent-spine-regression.js && node scripts/check-reload-agent-spine-regression.js && node scripts/check-todo-recovery-catalog-regression.js && node scripts/check-historical-tool-result-preview-regression.js && node scripts/check-session-start-hook-context-regression.js && node scripts/check-queue-controls-regression.js && node scripts/check-approval-queue-shortcuts-regression.js && node scripts/check-approval-observability-regression.js && node scripts/check-slash-escape-regression.js && node scripts/check-telegram-bridge-watchdog.js && node scripts/check-resume-replay-regression.js && node scripts/check-session-cancel-regression.js && node scripts/check-plugin-startup-regression.js && node scripts/check-active-profile-plugin-startup.js && node scripts/check-mcp-startup-wait-budget.js",
|
|
13
13
|
"release:verify": "node scripts/check-release-metadata.js --external",
|
|
14
14
|
"postinstall": "node scripts/fix-node-pty-perms.js"
|
|
15
15
|
},
|
|
@@ -36,9 +36,11 @@
|
|
|
36
36
|
"native/",
|
|
37
37
|
"release-planned-removals.json",
|
|
38
38
|
"scripts/check-package-regression.js",
|
|
39
|
+
"scripts/check-copy-command-regression.js",
|
|
39
40
|
"scripts/check-active-profile-plugin-startup.js",
|
|
40
41
|
"scripts/check-approval-queue-shortcuts-regression.js",
|
|
41
42
|
"scripts/check-bundled-agent-spine-regression.js",
|
|
43
|
+
"scripts/check-reload-agent-spine-regression.js",
|
|
42
44
|
"scripts/check-approval-observability-regression.js",
|
|
43
45
|
"scripts/check-slash-escape-regression.js",
|
|
44
46
|
"scripts/check-mcp-startup-wait-budget.js",
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const vm = require('node:vm');
|
|
7
|
+
|
|
8
|
+
const rootArg = process.argv.find((value) => value.startsWith('--root='));
|
|
9
|
+
const packageRoot = rootArg ? path.resolve(rootArg.slice('--root='.length)) : path.resolve(__dirname, '..');
|
|
10
|
+
const bundle = fs.readFileSync(path.join(packageRoot, 'blun.mjs'), 'utf8');
|
|
11
|
+
|
|
12
|
+
function extractFunction(name) {
|
|
13
|
+
const start = bundle.indexOf(`function ${name}(`);
|
|
14
|
+
assert.notEqual(start, -1, `${name} must exist`);
|
|
15
|
+
const open = bundle.indexOf('{', start);
|
|
16
|
+
let depth = 0;
|
|
17
|
+
for (let index = open; index < bundle.length; index += 1) {
|
|
18
|
+
if (bundle[index] === '{') depth += 1;
|
|
19
|
+
if (bundle[index] === '}') depth -= 1;
|
|
20
|
+
if (depth === 0) return bundle.slice(start, index + 1);
|
|
21
|
+
}
|
|
22
|
+
assert.fail(`${name} must have a complete function body`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
assert.match(
|
|
26
|
+
bundle,
|
|
27
|
+
/name:\s*"copy"[\s\S]{0,180}descriptionKey:\s*"command\.copy\.description"[\s\S]{0,180}availability:\s*"always"/,
|
|
28
|
+
'/copy must be registered and available while a turn is running',
|
|
29
|
+
);
|
|
30
|
+
assert.match(bundle, /case "copy":\s*await handleCopyCommand\(host\);\s*return;/, '/copy must dispatch to its handler');
|
|
31
|
+
assert.match(bundle, /await copyTextToClipboard\(text\);/, '/copy must use the existing cross-platform clipboard helper');
|
|
32
|
+
|
|
33
|
+
const selectorSource = extractFunction('findLatestAssistantResponse');
|
|
34
|
+
const findLatestAssistantResponse = vm.runInNewContext(`(${selectorSource})`);
|
|
35
|
+
assert.equal(
|
|
36
|
+
findLatestAssistantResponse([
|
|
37
|
+
{ kind: 'assistant', content: ' previous answer ' },
|
|
38
|
+
{ kind: 'user', content: 'question' },
|
|
39
|
+
{ kind: 'assistant', content: ' ' },
|
|
40
|
+
]),
|
|
41
|
+
' previous answer ',
|
|
42
|
+
'the selector must ignore user and empty assistant entries while preserving response bytes',
|
|
43
|
+
);
|
|
44
|
+
assert.equal(findLatestAssistantResponse([{ kind: 'user', content: 'question' }]), undefined);
|
|
45
|
+
|
|
46
|
+
const requiredUiStrings = [
|
|
47
|
+
'Copy the latest assistant response to the clipboard.',
|
|
48
|
+
'Copied the latest response to the clipboard.',
|
|
49
|
+
'There is no response to copy yet.',
|
|
50
|
+
'Could not copy the latest response: {error}',
|
|
51
|
+
'Neueste Assistentenantwort in die Zwischenablage übernehmen.',
|
|
52
|
+
'Die neueste Antwort wurde in die Zwischenablage übernommen.',
|
|
53
|
+
'Es ist noch keine Antwort zum Kopieren verfügbar.',
|
|
54
|
+
'Die neueste Antwort konnte nicht kopiert werden: {error}',
|
|
55
|
+
'Copiar la respuesta más reciente del asistente al portapapeles.',
|
|
56
|
+
'La respuesta más reciente se copió al portapapeles.',
|
|
57
|
+
'Todavía no hay ninguna respuesta que copiar.',
|
|
58
|
+
'No se pudo copiar la respuesta más reciente: {error}',
|
|
59
|
+
'Copier la réponse la plus récente de l’assistant dans le presse-papiers.',
|
|
60
|
+
'La réponse la plus récente a été copiée dans le presse-papiers.',
|
|
61
|
+
'Aucune réponse à copier pour le moment.',
|
|
62
|
+
'Impossible de copier la réponse la plus récente : {error}',
|
|
63
|
+
'Kopiera assistentens senaste svar till urklipp.',
|
|
64
|
+
'Det senaste svaret kopierades till urklipp.',
|
|
65
|
+
'Det finns inget svar att kopiera ännu.',
|
|
66
|
+
'Det gick inte att kopiera det senaste svaret: {error}',
|
|
67
|
+
'Zkopírovat nejnovější odpověď asistenta do schránky.',
|
|
68
|
+
'Nejnovější odpověď byla zkopírována do schránky.',
|
|
69
|
+
'Zatím není k dispozici žádná odpověď ke zkopírování.',
|
|
70
|
+
'Nejnovější odpověď se nepodařilo zkopírovat: {error}',
|
|
71
|
+
];
|
|
72
|
+
for (const text of requiredUiStrings) assert.ok(bundle.includes(text), `missing released UI text: ${text}`);
|
|
73
|
+
|
|
74
|
+
process.stdout.write('Copy command regression gate: PASS\n');
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
|
|
8
|
+
const rootArg = process.argv.find((value) => value.startsWith('--root='));
|
|
9
|
+
const packageRoot = rootArg ? path.resolve(rootArg.slice('--root='.length)) : path.resolve(__dirname, '..');
|
|
10
|
+
const bundle = fs.readFileSync(path.join(packageRoot, 'blun.mjs'), 'utf8');
|
|
11
|
+
const reloadStart = bundle.indexOf('async reloadSession(input) {');
|
|
12
|
+
const bootstrapCall = bundle.indexOf('installBundledAgentSpineForReload({ blunDir: this.homeDir });', reloadStart);
|
|
13
|
+
const pluginReload = bundle.indexOf('await this.reloadPlugins({});', reloadStart);
|
|
14
|
+
|
|
15
|
+
assert.notEqual(reloadStart, -1, 'reloadSession must exist');
|
|
16
|
+
assert.ok(
|
|
17
|
+
bootstrapCall > reloadStart && bootstrapCall < pluginReload,
|
|
18
|
+
'/reload must materialize bundled AgentSpine before reloading the plugin catalogue',
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const helperPath = path.join(packageRoot, 'bin', 'reload-plugin-bootstrap.cjs');
|
|
22
|
+
assert.ok(fs.existsSync(helperPath), 'reload plugin bootstrap helper must be packaged');
|
|
23
|
+
const { installBundledAgentSpineForReload } = require(helperPath);
|
|
24
|
+
|
|
25
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'blun-reload-agent-spine-'));
|
|
26
|
+
const profile = path.join(root, 'profile');
|
|
27
|
+
const pluginsDir = path.join(profile, 'plugins');
|
|
28
|
+
const installedFile = path.join(pluginsDir, 'installed.json');
|
|
29
|
+
const unrelatedRoot = path.join(root, 'unrelated-plugin');
|
|
30
|
+
fs.mkdirSync(pluginsDir, { recursive: true });
|
|
31
|
+
fs.mkdirSync(unrelatedRoot, { recursive: true });
|
|
32
|
+
fs.writeFileSync(installedFile, `${JSON.stringify({
|
|
33
|
+
version: 1,
|
|
34
|
+
plugins: [{
|
|
35
|
+
id: 'unrelated',
|
|
36
|
+
root: unrelatedRoot,
|
|
37
|
+
source: 'local-path',
|
|
38
|
+
enabled: false,
|
|
39
|
+
capabilities: {},
|
|
40
|
+
}],
|
|
41
|
+
}, null, 2)}\n`);
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const first = installBundledAgentSpineForReload({
|
|
45
|
+
packageRoot,
|
|
46
|
+
blunDir: profile,
|
|
47
|
+
now: '2026-09-01T00:00:00.000Z',
|
|
48
|
+
});
|
|
49
|
+
const installed = JSON.parse(fs.readFileSync(installedFile, 'utf8'));
|
|
50
|
+
const agentSpine = installed.plugins.find((plugin) => plugin.id === 'agent-spine');
|
|
51
|
+
const unrelated = installed.plugins.find((plugin) => plugin.id === 'unrelated');
|
|
52
|
+
|
|
53
|
+
assert.ok(agentSpine, 'reload bootstrap must add bundled AgentSpine');
|
|
54
|
+
assert.equal(agentSpine.root, first.pluginRoot);
|
|
55
|
+
assert.equal(agentSpine.enabled, true);
|
|
56
|
+
assert.equal(agentSpine.capabilities.mcpServers['agent-spine'].enabled, true);
|
|
57
|
+
assert.equal(unrelated.root, unrelatedRoot, 'reload bootstrap must preserve unrelated plugins');
|
|
58
|
+
assert.equal(unrelated.enabled, false, 'reload bootstrap must preserve unrelated plugin state');
|
|
59
|
+
assert.equal(
|
|
60
|
+
fs.readFileSync(path.join(agentSpine.root, 'package.json'), 'utf8'),
|
|
61
|
+
fs.readFileSync(path.join(packageRoot, 'agent-spine-plugin', 'package.json'), 'utf8'),
|
|
62
|
+
'managed AgentSpine must come from the current package bundle',
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const beforeSecondRun = fs.readFileSync(installedFile, 'utf8');
|
|
66
|
+
const second = installBundledAgentSpineForReload({
|
|
67
|
+
packageRoot,
|
|
68
|
+
blunDir: profile,
|
|
69
|
+
now: '2026-09-01T00:01:00.000Z',
|
|
70
|
+
});
|
|
71
|
+
assert.equal(second.reused, true, 'an unchanged bundle must be reused');
|
|
72
|
+
assert.equal(fs.readFileSync(installedFile, 'utf8'), beforeSecondRun, 'idempotent reload must not rewrite installed.json');
|
|
73
|
+
process.stdout.write('Reload AgentSpine regression gate: PASS\n');
|
|
74
|
+
} finally {
|
|
75
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
76
|
+
}
|