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.
@@ -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
- if (hasEquivalentServer(servers, name, tool)) continue;
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
+ };
@@ -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
  });