blun-king-cli 9.1.57 → 9.1.60
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/launcher-mode.js +2 -0
- package/bin/launcher-runtime.js +68 -18
- package/bin/private-paths.js +6 -3
- package/bin/profile-runtime.cjs +128 -0
- package/bin/rate-limit-recovery-policy.cjs +34 -0
- package/bin/standard-tools-bootstrap.js +45 -4
- package/bin/subagent-timeout-policy.cjs +79 -0
- package/blun.mjs +209 -92
- package/package.json +1 -1
- package/standard-tools/manifest.json +3 -3
package/LIESMICH.txt
CHANGED
package/README.md
CHANGED
package/bin/launcher-mode.js
CHANGED
|
@@ -10,6 +10,8 @@ const LAUNCHER_HELP_TEXT = [
|
|
|
10
10
|
'Launchers:',
|
|
11
11
|
' blun Starts the CLI without automatically attaching Telegram.',
|
|
12
12
|
' king Starts the same CLI and automatically attaches the configured Telegram channel.',
|
|
13
|
+
' king <profile> starts an isolated named King profile.',
|
|
14
|
+
' king -c <profile> continues the last session in that profile.',
|
|
13
15
|
' Both use the same version, account, model, and commands.',
|
|
14
16
|
' blun tools list shows optional tools; enable writes only explicit choices.',
|
|
15
17
|
'',
|
package/bin/launcher-runtime.js
CHANGED
|
@@ -31,9 +31,16 @@ const {
|
|
|
31
31
|
securePrivateFile,
|
|
32
32
|
writePrivateFile,
|
|
33
33
|
} = require('./private-paths');
|
|
34
|
+
const {
|
|
35
|
+
parseProfileLaunchArgs,
|
|
36
|
+
resolveProfilePaths,
|
|
37
|
+
seedDefaultProfileFromLegacy,
|
|
38
|
+
} = require('./profile-runtime.cjs');
|
|
34
39
|
|
|
35
40
|
const PKG = path.resolve(__dirname, '..');
|
|
36
|
-
const
|
|
41
|
+
const RAW_ARGS = process.argv.slice(2);
|
|
42
|
+
const PROFILE = parseProfileLaunchArgs(RAW_ARGS, launcherModeFromArgv(process.argv));
|
|
43
|
+
const ARGS = PROFILE.args;
|
|
37
44
|
const CORE_LOAD_TIMEOUT_MS = 30_000;
|
|
38
45
|
|
|
39
46
|
function normalizeWindowsPath(value) {
|
|
@@ -44,10 +51,24 @@ function resolveLauncherPrivatePaths(options = {}) {
|
|
|
44
51
|
const env = options.env || process.env;
|
|
45
52
|
const platform = options.platform || process.platform;
|
|
46
53
|
const configuredHome = String(env.BLUN_HOME || '').trim();
|
|
54
|
+
const configuredSharedHome = String(env.BLUN_SHARED_HOME || '').trim();
|
|
55
|
+
const profileName = options.profileName || PROFILE.profileName;
|
|
47
56
|
if (platform !== 'win32') {
|
|
48
57
|
const userHome = (options.homedir || os.homedir)();
|
|
58
|
+
const sharedHome = configuredSharedHome
|
|
59
|
+
? path.resolve(configuredSharedHome)
|
|
60
|
+
: configuredHome
|
|
61
|
+
? path.resolve(configuredHome)
|
|
62
|
+
: path.join(userHome, '.blun');
|
|
63
|
+
const profilePaths = resolveProfilePaths(sharedHome, profileName);
|
|
64
|
+
if (configuredSharedHome && configuredHome
|
|
65
|
+
&& path.resolve(configuredHome) !== path.resolve(profilePaths.home)) {
|
|
66
|
+
throw new Error(`BLUN_HOME passt nicht zum Profil ${profileName}: ${profilePaths.home}`);
|
|
67
|
+
}
|
|
49
68
|
return {
|
|
50
|
-
blunHome:
|
|
69
|
+
blunHome: profilePaths.home,
|
|
70
|
+
profilePaths,
|
|
71
|
+
sharedHome,
|
|
51
72
|
userHome,
|
|
52
73
|
};
|
|
53
74
|
}
|
|
@@ -71,12 +92,26 @@ function resolveLauncherPrivatePaths(options = {}) {
|
|
|
71
92
|
throw new Error('Das lokale Windows-Benutzerprofil ist kein kanonischer lokaler Pfad.');
|
|
72
93
|
}
|
|
73
94
|
|
|
74
|
-
const
|
|
95
|
+
const sharedHome = path.win32.join(userHome, '.blun');
|
|
96
|
+
const profilePaths = resolveProfilePaths(sharedHome, profileName, path.win32);
|
|
97
|
+
if (configuredSharedHome
|
|
98
|
+
&& normalizeWindowsPath(path.win32.resolve(configuredSharedHome)) !== normalizeWindowsPath(sharedHome)) {
|
|
99
|
+
throw new Error(`BLUN_SHARED_HOME muss unter Windows exakt im Windows-Benutzerprofil liegen: ${sharedHome}`);
|
|
100
|
+
}
|
|
101
|
+
const configuredHomeIsLegacyShared = !configuredSharedHome
|
|
102
|
+
&& configuredHome
|
|
103
|
+
&& normalizeWindowsPath(path.win32.resolve(configuredHome)) === normalizeWindowsPath(sharedHome);
|
|
75
104
|
if (configuredHome
|
|
76
|
-
&&
|
|
77
|
-
|
|
105
|
+
&& !configuredHomeIsLegacyShared
|
|
106
|
+
&& normalizeWindowsPath(path.win32.resolve(configuredHome)) !== normalizeWindowsPath(profilePaths.home)) {
|
|
107
|
+
throw new Error(`BLUN_HOME passt nicht zum Profil ${profileName}: ${profilePaths.home}`);
|
|
78
108
|
}
|
|
79
|
-
return {
|
|
109
|
+
return {
|
|
110
|
+
blunHome: profilePaths.home,
|
|
111
|
+
profilePaths,
|
|
112
|
+
sharedHome,
|
|
113
|
+
userHome,
|
|
114
|
+
};
|
|
80
115
|
}
|
|
81
116
|
|
|
82
117
|
function readPackageVersion() {
|
|
@@ -271,12 +306,12 @@ async function runLauncher(options = {}) {
|
|
|
271
306
|
if (mcpNames.length > 0) {
|
|
272
307
|
seedStandardTools({
|
|
273
308
|
packageRoot: PKG,
|
|
274
|
-
blunDir: privatePaths.
|
|
309
|
+
blunDir: privatePaths.profilePaths.home,
|
|
275
310
|
names: mcpNames,
|
|
276
311
|
});
|
|
277
312
|
}
|
|
278
313
|
if (selected.includes('telegram')) {
|
|
279
|
-
installTelegramForProfile(PKG, privatePaths.
|
|
314
|
+
installTelegramForProfile(PKG, privatePaths.profilePaths.home);
|
|
280
315
|
}
|
|
281
316
|
process.stdout.write(`${selected.join('\n')}\n`);
|
|
282
317
|
return;
|
|
@@ -298,7 +333,7 @@ async function runLauncher(options = {}) {
|
|
|
298
333
|
stdin: process.stdin,
|
|
299
334
|
stdout: process.stdout,
|
|
300
335
|
stderr: process.stderr,
|
|
301
|
-
blunDir: privatePaths.
|
|
336
|
+
blunDir: privatePaths.sharedHome,
|
|
302
337
|
packageRoot: PKG,
|
|
303
338
|
noticeLease,
|
|
304
339
|
request: ARGS[0],
|
|
@@ -307,13 +342,13 @@ async function runLauncher(options = {}) {
|
|
|
307
342
|
return;
|
|
308
343
|
}
|
|
309
344
|
|
|
310
|
-
let
|
|
345
|
+
let privatePathsCache;
|
|
311
346
|
const getPrivatePaths = () => {
|
|
312
|
-
|
|
313
|
-
return
|
|
347
|
+
privatePathsCache ||= resolveLauncherPrivatePaths();
|
|
348
|
+
return privatePathsCache;
|
|
314
349
|
};
|
|
315
350
|
const nodeRuntime = await prepareManagedNodeRuntime({
|
|
316
|
-
getBlunDir: () => getPrivatePaths().
|
|
351
|
+
getBlunDir: () => getPrivatePaths().sharedHome,
|
|
317
352
|
});
|
|
318
353
|
if (nodeRuntime.kind === 'failed') {
|
|
319
354
|
process.stderr.write(`${nodeRuntime.message}\n`);
|
|
@@ -332,12 +367,24 @@ async function runLauncher(options = {}) {
|
|
|
332
367
|
const env = createLauncherEnvironment(process.env, mode, readPackageVersion());
|
|
333
368
|
const informationalEnv = { ...process.env };
|
|
334
369
|
delete informationalEnv.BLUN_HOME;
|
|
335
|
-
|
|
370
|
+
delete informationalEnv.BLUN_SHARED_HOME;
|
|
371
|
+
const privatePaths = resolveLauncherPrivatePaths({ env: informationalEnv });
|
|
372
|
+
const profilePaths = privatePaths.profilePaths;
|
|
373
|
+
env.BLUN_HOME = profilePaths.home;
|
|
374
|
+
env.BLUN_SHARED_HOME = privatePaths.sharedHome;
|
|
375
|
+
env.BLUN_LOG_HOME = privatePaths.sharedHome;
|
|
376
|
+
env.BLUN_PROFILE = PROFILE.profileName;
|
|
336
377
|
process.exitCode = await superviseProtectedCore(ARGS, env, callerCwd, releaseNotice);
|
|
337
378
|
return;
|
|
338
379
|
}
|
|
339
380
|
|
|
340
|
-
const
|
|
381
|
+
const privatePaths = getPrivatePaths();
|
|
382
|
+
const profilePaths = privatePaths.profilePaths;
|
|
383
|
+
ensurePrivateDirectory(privatePaths.sharedHome);
|
|
384
|
+
if (PROFILE.profileName === 'default') {
|
|
385
|
+
seedDefaultProfileFromLegacy(privatePaths.sharedHome);
|
|
386
|
+
}
|
|
387
|
+
const blunDir = profilePaths.home;
|
|
341
388
|
ensurePrivateDirectory(blunDir);
|
|
342
389
|
|
|
343
390
|
// --- 1. Oeffentlicher npm-Update-Dialog. -------------------------------
|
|
@@ -350,7 +397,7 @@ async function runLauncher(options = {}) {
|
|
|
350
397
|
stdin: process.stdin,
|
|
351
398
|
stdout: process.stdout,
|
|
352
399
|
stderr: process.stderr,
|
|
353
|
-
blunDir,
|
|
400
|
+
blunDir: privatePaths.sharedHome,
|
|
354
401
|
packageRoot: PKG,
|
|
355
402
|
noticeLease,
|
|
356
403
|
});
|
|
@@ -382,7 +429,7 @@ async function runLauncher(options = {}) {
|
|
|
382
429
|
});
|
|
383
430
|
|
|
384
431
|
// --- 4. Eigenes BLUN-Grunddesign als einzigen Standard-Skill installieren
|
|
385
|
-
seedStandardDesignSkill({ packageRoot: PKG, blunDir });
|
|
432
|
+
seedStandardDesignSkill({ packageRoot: PKG, blunDir: privatePaths.sharedHome });
|
|
386
433
|
|
|
387
434
|
// --- 5. Telegram-Plugin + Token-Vorlage (nur King) -----------------------
|
|
388
435
|
if (mode === LAUNCHER_MODES.KING) {
|
|
@@ -402,7 +449,10 @@ async function runLauncher(options = {}) {
|
|
|
402
449
|
|
|
403
450
|
// --- 6. Start -----------------------------------------------------------
|
|
404
451
|
const env = createLauncherEnvironment(process.env, mode, readPackageVersion());
|
|
405
|
-
env.BLUN_HOME =
|
|
452
|
+
env.BLUN_HOME = profilePaths.home;
|
|
453
|
+
env.BLUN_SHARED_HOME = privatePaths.sharedHome;
|
|
454
|
+
env.BLUN_LOG_HOME = privatePaths.sharedHome;
|
|
455
|
+
env.BLUN_PROFILE = PROFILE.profileName;
|
|
406
456
|
env.BLUN_MODEL_MAX_COMPLETION_TOKENS = env.BLUN_MODEL_MAX_COMPLETION_TOKENS || '32768';
|
|
407
457
|
process.exitCode = await superviseProtectedCore(
|
|
408
458
|
ARGS,
|
package/bin/private-paths.js
CHANGED
|
@@ -34,6 +34,9 @@ public static class BlunPrivateAclNative
|
|
|
34
34
|
private const uint READ_CONTROL = 0x00020000;
|
|
35
35
|
private const uint WRITE_DAC = 0x00040000;
|
|
36
36
|
private const uint SYNCHRONIZE = 0x00100000;
|
|
37
|
+
private const uint FILE_SHARE_READ = 0x00000001;
|
|
38
|
+
private const uint FILE_SHARE_WRITE = 0x00000002;
|
|
39
|
+
private const uint FILE_SHARE_DELETE = 0x00000004;
|
|
37
40
|
private const uint CREATE_NEW = 1;
|
|
38
41
|
private const uint OPEN_EXISTING = 3;
|
|
39
42
|
private const uint FILE_ATTRIBUTE_NORMAL = 0x00000080;
|
|
@@ -608,7 +611,7 @@ public static class BlunPrivateAclNative
|
|
|
608
611
|
out ioStatus,
|
|
609
612
|
IntPtr.Zero,
|
|
610
613
|
0,
|
|
611
|
-
|
|
614
|
+
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
|
612
615
|
FILE_OPEN_IF,
|
|
613
616
|
FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT | FILE_OPEN_REPARSE_POINT,
|
|
614
617
|
IntPtr.Zero,
|
|
@@ -1271,8 +1274,8 @@ function windowsAclController(options) {
|
|
|
1271
1274
|
const detail = rawDetail.replace(/[\u0000-\u001f\u007f]+/gu, ' ').trim().slice(0, 500);
|
|
1272
1275
|
throw new Error(
|
|
1273
1276
|
detail.length > 0
|
|
1274
|
-
? `Windows private ACL operation failed: ${detail}`
|
|
1275
|
-
:
|
|
1277
|
+
? `Windows private ACL operation failed for ${kind} target "${absoluteTarget}": ${detail}`
|
|
1278
|
+
: `Windows private ACL operation failed for ${kind} target "${absoluteTarget}".`,
|
|
1276
1279
|
{ cause: error },
|
|
1277
1280
|
);
|
|
1278
1281
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const DEFAULT_PROFILE = 'default';
|
|
7
|
+
const PROFILE_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]{0,31}$/u;
|
|
8
|
+
const RESERVED_SINGLE_ARGUMENTS = new Set([
|
|
9
|
+
'doctor',
|
|
10
|
+
'export',
|
|
11
|
+
'help',
|
|
12
|
+
'mcp',
|
|
13
|
+
'server',
|
|
14
|
+
'tools',
|
|
15
|
+
'update',
|
|
16
|
+
'upgrade',
|
|
17
|
+
'version',
|
|
18
|
+
]);
|
|
19
|
+
const LEGACY_PROFILE_ENTRIES = Object.freeze([
|
|
20
|
+
'channels',
|
|
21
|
+
'config.toml',
|
|
22
|
+
'mcp.json',
|
|
23
|
+
'plugins',
|
|
24
|
+
'session_index.jsonl',
|
|
25
|
+
'sessions',
|
|
26
|
+
'tui.toml',
|
|
27
|
+
]);
|
|
28
|
+
const MIGRATION_MARKER = '.legacy-profile-migrated-v1';
|
|
29
|
+
|
|
30
|
+
function normalizeProfileName(value) {
|
|
31
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
32
|
+
if (!PROFILE_NAME_PATTERN.test(normalized)) {
|
|
33
|
+
throw new Error('Profilnamen duerfen nur Buchstaben, Zahlen, Unterstriche und Bindestriche enthalten.');
|
|
34
|
+
}
|
|
35
|
+
return normalized;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parseProfileLaunchArgs(inputArgs, mode = 'blun') {
|
|
39
|
+
const args = Array.isArray(inputArgs) ? [...inputArgs] : [];
|
|
40
|
+
if (String(mode).toLowerCase() !== 'king') {
|
|
41
|
+
return { args, profileName: DEFAULT_PROFILE };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const profileFlagIndex = args.indexOf('--profile');
|
|
45
|
+
if (profileFlagIndex >= 0) {
|
|
46
|
+
if (profileFlagIndex + 1 >= args.length) {
|
|
47
|
+
throw new Error('--profile braucht einen Profilnamen.');
|
|
48
|
+
}
|
|
49
|
+
const profileName = normalizeProfileName(args[profileFlagIndex + 1]);
|
|
50
|
+
args.splice(profileFlagIndex, 2);
|
|
51
|
+
return { args, profileName };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (args.length === 1) {
|
|
55
|
+
const candidate = String(args[0] || '');
|
|
56
|
+
if (!candidate.startsWith('-') && !RESERVED_SINGLE_ARGUMENTS.has(candidate.toLowerCase())) {
|
|
57
|
+
return { args: [], profileName: normalizeProfileName(candidate) };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (args.length === 2 && args.includes('-c')) {
|
|
62
|
+
const candidate = args.find((arg) => arg !== '-c');
|
|
63
|
+
if (candidate && !candidate.startsWith('-')) {
|
|
64
|
+
return { args: ['-c'], profileName: normalizeProfileName(candidate) };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { args, profileName: DEFAULT_PROFILE };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function resolveProfilePaths(sharedHome, profileName, pathImpl = path) {
|
|
72
|
+
const normalized = normalizeProfileName(profileName);
|
|
73
|
+
const home = pathImpl.join(sharedHome, 'profile', normalized);
|
|
74
|
+
return Object.freeze({
|
|
75
|
+
profileName: normalized,
|
|
76
|
+
sharedHome,
|
|
77
|
+
home,
|
|
78
|
+
channels: pathImpl.join(home, 'channels'),
|
|
79
|
+
sessions: pathImpl.join(home, 'sessions'),
|
|
80
|
+
plugins: pathImpl.join(home, 'plugins'),
|
|
81
|
+
config: pathImpl.join(home, 'config.toml'),
|
|
82
|
+
mcp: pathImpl.join(home, 'mcp.json'),
|
|
83
|
+
sessionIndex: pathImpl.join(home, 'session_index.jsonl'),
|
|
84
|
+
credentials: pathImpl.join(sharedHome, 'credentials'),
|
|
85
|
+
oauth: pathImpl.join(sharedHome, 'oauth'),
|
|
86
|
+
deviceId: pathImpl.join(sharedHome, 'device_id'),
|
|
87
|
+
skills: pathImpl.join(sharedHome, 'skills'),
|
|
88
|
+
secure: pathImpl.join(sharedHome, 'secure'),
|
|
89
|
+
persona: pathImpl.join(sharedHome, 'persona.json'),
|
|
90
|
+
globalLog: pathImpl.join(sharedHome, 'logs', 'blun.log'),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function copyLegacyEntry(source, target, fsImpl) {
|
|
95
|
+
if (!fsImpl.existsSync(source) || fsImpl.existsSync(target)) return;
|
|
96
|
+
const stat = fsImpl.lstatSync(source);
|
|
97
|
+
if (stat.isSymbolicLink()) return;
|
|
98
|
+
if (stat.isDirectory()) {
|
|
99
|
+
fsImpl.cpSync(source, target, { recursive: true, errorOnExist: false });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (stat.isFile()) fsImpl.copyFileSync(source, target);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function seedDefaultProfileFromLegacy(sharedHome, fsImpl = fs) {
|
|
106
|
+
const paths = resolveProfilePaths(sharedHome, DEFAULT_PROFILE);
|
|
107
|
+
const marker = path.join(paths.home, MIGRATION_MARKER);
|
|
108
|
+
if (fsImpl.existsSync(marker)) return paths;
|
|
109
|
+
|
|
110
|
+
fsImpl.mkdirSync(paths.home, { recursive: true });
|
|
111
|
+
for (const entry of LEGACY_PROFILE_ENTRIES) {
|
|
112
|
+
copyLegacyEntry(path.join(sharedHome, entry), path.join(paths.home, entry), fsImpl);
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
fsImpl.writeFileSync(marker, 'legacy profile state copied once\n', { encoding: 'utf8', flag: 'wx' });
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
118
|
+
}
|
|
119
|
+
return paths;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = {
|
|
123
|
+
DEFAULT_PROFILE,
|
|
124
|
+
normalizeProfileName,
|
|
125
|
+
parseProfileLaunchArgs,
|
|
126
|
+
resolveProfilePaths,
|
|
127
|
+
seedDefaultProfileFromLegacy,
|
|
128
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const MAX_AUTOMATIC_RATE_LIMIT_RESUMES = 3;
|
|
4
|
+
const RATE_LIMIT_RETRY_BASE_MS = 60_000;
|
|
5
|
+
const RATE_LIMIT_COMPACTION_THRESHOLD_TOKENS = 64_000;
|
|
6
|
+
|
|
7
|
+
function isImmediateGenerateRetryAllowed(statusCode) {
|
|
8
|
+
return statusCode !== 429 && [500, 502, 503, 504].includes(statusCode);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function rateLimitRetryDelayMs(retryCount) {
|
|
12
|
+
if (!Number.isInteger(retryCount) || retryCount < 1) {
|
|
13
|
+
throw new Error("retryCount must be a positive integer.");
|
|
14
|
+
}
|
|
15
|
+
return RATE_LIMIT_RETRY_BASE_MS * (2 ** (retryCount - 1));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function shouldStopAutomaticRateLimitResume(retryCount) {
|
|
19
|
+
return retryCount >= MAX_AUTOMATIC_RATE_LIMIT_RESUMES;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function shouldCompactBeforeRateLimitResume(estimatedRequestTokens) {
|
|
23
|
+
return Number.isFinite(estimatedRequestTokens)
|
|
24
|
+
&& estimatedRequestTokens >= RATE_LIMIT_COMPACTION_THRESHOLD_TOKENS;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
MAX_AUTOMATIC_RATE_LIMIT_RESUMES,
|
|
29
|
+
RATE_LIMIT_RETRY_BASE_MS,
|
|
30
|
+
isImmediateGenerateRetryAllowed,
|
|
31
|
+
rateLimitRetryDelayMs,
|
|
32
|
+
shouldCompactBeforeRateLimitResume,
|
|
33
|
+
shouldStopAutomaticRateLimitResume,
|
|
34
|
+
};
|
|
@@ -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
|
+
};
|