blun-king-cli 9.1.58 → 9.1.61
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/LIESMICH.txt +1 -1
- package/README.md +1 -1
- package/bin/core-bootstrap.js +3 -6
- package/bin/launcher-mode.js +2 -0
- package/bin/launcher-runtime.js +203 -30
- package/bin/profile-runtime.cjs +264 -0
- package/bin/rate-limit-recovery-policy.cjs +34 -0
- package/bin/running-update.cjs +389 -0
- package/bin/standard-tools-bootstrap.js +45 -4
- package/bin/subagent-timeout-policy.cjs +79 -0
- package/bin/update-notice.js +24 -0
- package/blun.mjs +296 -109
- package/package.json +1 -1
- package/standard-tools/manifest.json +3 -3
|
@@ -74,6 +74,41 @@ function hasEquivalentServer(servers, name, tool) {
|
|
|
74
74
|
});
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
function sameStringArray(left, right) {
|
|
78
|
+
return Array.isArray(left)
|
|
79
|
+
&& Array.isArray(right)
|
|
80
|
+
&& left.length === right.length
|
|
81
|
+
&& left.every((value, index) => value === right[index]);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function recognizedBundledServer(current, expected) {
|
|
85
|
+
return isObject(current)
|
|
86
|
+
&& current.transport === expected.transport
|
|
87
|
+
&& current.command === expected.command
|
|
88
|
+
&& sameStringArray(current.args, expected.args);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function migrateBundledTimeouts(name, current, expected) {
|
|
92
|
+
if (!recognizedBundledServer(current, expected)) return false;
|
|
93
|
+
let changed = false;
|
|
94
|
+
if (name === 'agent-browser') {
|
|
95
|
+
if (current.startupTimeoutMs === 30_000) {
|
|
96
|
+
current.startupTimeoutMs = expected.startupTimeoutMs;
|
|
97
|
+
changed = true;
|
|
98
|
+
}
|
|
99
|
+
if (current.toolTimeoutMs === 60_000) {
|
|
100
|
+
current.toolTimeoutMs = expected.toolTimeoutMs;
|
|
101
|
+
changed = true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (name === 'blun-language-guard'
|
|
105
|
+
&& (current.toolTimeoutMs === 30_000 || current.toolTimeoutMs === 60_000)) {
|
|
106
|
+
current.toolTimeoutMs = expected.toolTimeoutMs;
|
|
107
|
+
changed = true;
|
|
108
|
+
}
|
|
109
|
+
return changed;
|
|
110
|
+
}
|
|
111
|
+
|
|
77
112
|
function writeMcpDocument(configPath, document) {
|
|
78
113
|
const temporaryPath = path.join(
|
|
79
114
|
path.dirname(configPath),
|
|
@@ -160,21 +195,27 @@ function seedStandardTools({
|
|
|
160
195
|
const document = readMcpDocument(configPath);
|
|
161
196
|
const servers = { ...document.mcpServers };
|
|
162
197
|
const added = [];
|
|
198
|
+
const updated = [];
|
|
163
199
|
|
|
164
200
|
for (const name of selectedNames) {
|
|
165
201
|
const tool = catalogue.tools[name];
|
|
166
|
-
|
|
167
|
-
servers[name] = bundledToolConfig(name, tool, {
|
|
202
|
+
const expected = bundledToolConfig(name, tool, {
|
|
168
203
|
packageRoot,
|
|
169
204
|
blunDir,
|
|
170
205
|
platform,
|
|
171
206
|
env,
|
|
172
207
|
});
|
|
208
|
+
if (Object.hasOwn(servers, name)) {
|
|
209
|
+
if (migrateBundledTimeouts(name, servers[name], expected)) updated.push(name);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (hasEquivalentServer(servers, name, tool)) continue;
|
|
213
|
+
servers[name] = expected;
|
|
173
214
|
added.push(name);
|
|
174
215
|
}
|
|
175
216
|
|
|
176
|
-
if (added.length > 0) writeMcpDocument(configPath, { ...document, mcpServers: servers });
|
|
177
|
-
return { added, configPath };
|
|
217
|
+
if (added.length > 0 || updated.length > 0) writeMcpDocument(configPath, { ...document, mcpServers: servers });
|
|
218
|
+
return { added, updated, configPath };
|
|
178
219
|
}
|
|
179
220
|
|
|
180
221
|
function seedStandardDesignSkill({ packageRoot, blunDir }) {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_SUBAGENT_TIMEOUT_MS = 4 * 60 * 60 * 1000;
|
|
4
|
+
const MAX_SUBAGENT_TIMEOUT_MINUTES = 7 * 24 * 60;
|
|
5
|
+
const SUBAGENT_TIMEOUT_ENV = "BLUN_SUBAGENT_TIMEOUT_MINUTES";
|
|
6
|
+
|
|
7
|
+
function resolveSubagentTimeoutMs({ perRunMinutes, env = process.env } = {}) {
|
|
8
|
+
const raw = perRunMinutes ?? env[SUBAGENT_TIMEOUT_ENV];
|
|
9
|
+
if (raw === undefined || raw === null || raw === "") {
|
|
10
|
+
return DEFAULT_SUBAGENT_TIMEOUT_MS;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const minutes = typeof raw === "number" ? raw : Number(raw);
|
|
14
|
+
if (!Number.isInteger(minutes) || minutes < 0 || minutes > MAX_SUBAGENT_TIMEOUT_MINUTES) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
`${SUBAGENT_TIMEOUT_ENV} must be an integer from 0 to ${MAX_SUBAGENT_TIMEOUT_MINUTES} minutes.`,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
return minutes === 0 ? undefined : minutes * 60 * 1000;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function abortError(reason) {
|
|
23
|
+
if (reason instanceof Error) return reason;
|
|
24
|
+
return new Error(reason === undefined ? "Subagent interrupted." : String(reason));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function runAgentWithTimeoutContinuation({
|
|
28
|
+
handle,
|
|
29
|
+
timeoutMs,
|
|
30
|
+
signal,
|
|
31
|
+
abortCurrent,
|
|
32
|
+
retrySameAgent,
|
|
33
|
+
}) {
|
|
34
|
+
let currentHandle = handle;
|
|
35
|
+
|
|
36
|
+
while (true) {
|
|
37
|
+
let timeout;
|
|
38
|
+
let removeAbortListener = () => {};
|
|
39
|
+
const completion = Promise.resolve(currentHandle.completion).then(
|
|
40
|
+
(outcome) => ({ kind: "completed", outcome }),
|
|
41
|
+
(error) => ({ kind: "failed", error }),
|
|
42
|
+
);
|
|
43
|
+
const timedOut = timeoutMs === undefined
|
|
44
|
+
? new Promise(() => {})
|
|
45
|
+
: new Promise((resolve) => {
|
|
46
|
+
timeout = setTimeout(() => resolve({ kind: "timed_out" }), timeoutMs);
|
|
47
|
+
});
|
|
48
|
+
const interrupted = signal.aborted
|
|
49
|
+
? Promise.resolve({ kind: "interrupted", reason: signal.reason })
|
|
50
|
+
: new Promise((resolve) => {
|
|
51
|
+
const onAbort = () => resolve({ kind: "interrupted", reason: signal.reason });
|
|
52
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
53
|
+
removeAbortListener = () => signal.removeEventListener("abort", onAbort);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const result = await Promise.race([completion, timedOut, interrupted]);
|
|
57
|
+
if (timeout !== undefined) clearTimeout(timeout);
|
|
58
|
+
removeAbortListener();
|
|
59
|
+
|
|
60
|
+
if (result.kind === "completed") return { handle: currentHandle, outcome: result.outcome };
|
|
61
|
+
if (result.kind === "failed") throw result.error;
|
|
62
|
+
if (result.kind === "interrupted") {
|
|
63
|
+
abortCurrent(result.reason);
|
|
64
|
+
await Promise.resolve(currentHandle.completion).catch(() => {});
|
|
65
|
+
throw abortError(result.reason);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const reason = new Error("Subagent continuation interval elapsed.");
|
|
69
|
+
abortCurrent(reason);
|
|
70
|
+
await Promise.resolve(currentHandle.completion).catch(() => {});
|
|
71
|
+
currentHandle = await retrySameAgent(currentHandle.agentId);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = {
|
|
76
|
+
DEFAULT_SUBAGENT_TIMEOUT_MS,
|
|
77
|
+
resolveSubagentTimeoutMs,
|
|
78
|
+
runAgentWithTimeoutContinuation,
|
|
79
|
+
};
|
package/bin/update-notice.js
CHANGED
|
@@ -54,8 +54,18 @@ const LEGACY_MANIFEST_KEYS = Object.freeze([
|
|
|
54
54
|
'notesUrl',
|
|
55
55
|
'releasedAt',
|
|
56
56
|
]);
|
|
57
|
+
const RELEASE_NOTES_MANIFEST_KEYS = Object.freeze([
|
|
58
|
+
'install',
|
|
59
|
+
'latest',
|
|
60
|
+
'minSupported',
|
|
61
|
+
'name',
|
|
62
|
+
'notesUrl',
|
|
63
|
+
'releaseNotes',
|
|
64
|
+
'releasedAt',
|
|
65
|
+
]);
|
|
57
66
|
const MANIFEST_KEYS = Object.freeze([
|
|
58
67
|
'install',
|
|
68
|
+
'integrity',
|
|
59
69
|
'latest',
|
|
60
70
|
'minSupported',
|
|
61
71
|
'name',
|
|
@@ -214,8 +224,20 @@ function parseReleaseNotes(value) {
|
|
|
214
224
|
return Object.freeze(notes);
|
|
215
225
|
}
|
|
216
226
|
|
|
227
|
+
function validIntegrity(value) {
|
|
228
|
+
if (typeof value !== 'string') return false;
|
|
229
|
+
const match = /^sha512-([A-Za-z0-9+/]+={0,2})$/u.exec(value);
|
|
230
|
+
if (!match) return false;
|
|
231
|
+
try {
|
|
232
|
+
return Buffer.from(match[1], 'base64').length === 64;
|
|
233
|
+
} catch {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
217
238
|
function parseFallbackManifest(value) {
|
|
218
239
|
const record = exactDataRecord(value, MANIFEST_KEYS)
|
|
240
|
+
|| exactDataRecord(value, RELEASE_NOTES_MANIFEST_KEYS)
|
|
219
241
|
|| exactDataRecord(value, LEGACY_MANIFEST_KEYS);
|
|
220
242
|
const notesUrl = record ? canonicalOfficialNotesUrl(record.notesUrl) : undefined;
|
|
221
243
|
const releaseNotes = record && Object.hasOwn(record, 'releaseNotes')
|
|
@@ -228,6 +250,7 @@ function parseFallbackManifest(value) {
|
|
|
228
250
|
|| compareSemver(record.minSupported, record.latest) === 1
|
|
229
251
|
|| !isIsoTimestamp(record.releasedAt)
|
|
230
252
|
|| !notesUrl
|
|
253
|
+
|| (Object.hasOwn(record, 'integrity') && !validIntegrity(record.integrity))
|
|
231
254
|
|| (Object.hasOwn(record, 'releaseNotes') && releaseNotes === undefined)
|
|
232
255
|
|| !isBoundedText(record.install, 256)) {
|
|
233
256
|
return undefined;
|
|
@@ -238,6 +261,7 @@ function parseFallbackManifest(value) {
|
|
|
238
261
|
minSupported: record.minSupported,
|
|
239
262
|
releasedAt: record.releasedAt,
|
|
240
263
|
notesUrl,
|
|
264
|
+
...(Object.hasOwn(record, 'integrity') ? { integrity: record.integrity } : {}),
|
|
241
265
|
...(releaseNotes === undefined ? {} : { releaseNotes }),
|
|
242
266
|
install: record.install,
|
|
243
267
|
});
|