blun-king-cli 9.1.62 → 9.1.63

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.
Files changed (49) hide show
  1. package/bin/context-performance-policy.cjs +18 -0
  2. package/bin/launcher-runtime.js +2 -2
  3. package/bin/package-regression-policy.cjs +77 -0
  4. package/bin/provider-idle-timeout-policy.cjs +14 -0
  5. package/bin/skill-listing-performance-policy.cjs +33 -0
  6. package/bin/standard-tools-bootstrap.js +80 -26
  7. package/bin/turn-thinking-policy.cjs +66 -0
  8. package/bin/turn-tool-performance-policy.cjs +141 -0
  9. package/blun.mjs +519 -220
  10. package/package.json +4 -1
  11. package/release-planned-removals.json +4 -0
  12. package/scripts/check-package-regression.js +38 -0
  13. package/standard-skills/agent-browser/SKILL.md +19 -0
  14. package/standard-skills/agent-browser/references/runtime.md +8 -0
  15. package/standard-skills/design-taste-frontend/SKILL.md +1206 -0
  16. package/standard-skills/full-output-enforcement/SKILL.md +49 -0
  17. package/standard-skills/high-end-visual-design/SKILL.md +98 -0
  18. package/standard-skills/image-to-code/SKILL.md +1228 -0
  19. package/standard-skills/industrial-brutalist-ui/SKILL.md +92 -0
  20. package/standard-skills/minimalist-ui/SKILL.md +85 -0
  21. package/standard-skills/motion-design-taste/SKILL.md +74 -0
  22. package/standard-skills/playwright-testing/SKILL.md +19 -0
  23. package/standard-skills/playwright-testing/references/runtime.md +7 -0
  24. package/standard-skills/premortem/SKILL.md +148 -0
  25. package/standard-skills/redesign-existing-projects/SKILL.md +178 -0
  26. package/standard-skills/screenshot-lesen/SKILL.md +52 -0
  27. package/standard-skills/stitch-design-taste/DESIGN.md +121 -0
  28. package/standard-skills/stitch-design-taste/SKILL.md +184 -0
  29. package/standard-skills/telegram-channel/SKILL.md +18 -0
  30. package/standard-skills/telegram-channel/references/runtime.md +7 -0
  31. package/standard-skills/translate-native/LICENSE +21 -0
  32. package/standard-skills/translate-native/SKILL.md +39 -0
  33. package/standard-skills/translate-native/VERSION +1 -0
  34. package/standard-skills/translate-native/provenance.json +7 -0
  35. package/standard-skills/translate-native/references/evaluation-protocol.md +95 -0
  36. package/standard-skills/translate-native/references/native-orthography.md +79 -0
  37. package/standard-skills/translate-native/references/native-translation-standard.md +94 -0
  38. package/standard-skills/translate-native/references/structured-content.md +72 -0
  39. package/standard-skills/translate-native/references/translationese-review.md +77 -0
  40. package/standard-skills/translate-native/scripts/blun_language_guard.py +686 -0
  41. package/standard-skills/translate-native/scripts/check_diacritics.py +353 -0
  42. package/standard-skills/translate-native/scripts/guard_service_client.py +82 -0
  43. package/standard-skills/translate-native/scripts/language_gateway.py +62 -0
  44. package/standard-skills/translate-native/scripts/language_quality.py +172 -0
  45. package/standard-skills/translate-native/scripts/pre_output_guard.py +67 -0
  46. package/standard-skills/translate-native/scripts/translation_guard.py +916 -0
  47. package/standard-skills/web-lesen/SKILL.md +58 -0
  48. package/standard-skills/windows-mcp/SKILL.md +19 -0
  49. package/standard-skills/windows-mcp/references/runtime.md +9 -0
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+
3
+ const DEFAULT_COMPACTION_TRIGGER_MAX_TOKENS = 256_000;
4
+ const DEFAULT_COMPACTION_BLOCK_MAX_TOKENS = 288_000;
5
+
6
+ function capCompactionThreshold(threshold, phase) {
7
+ if (threshold === undefined) return undefined;
8
+ const maximum = phase === "block"
9
+ ? DEFAULT_COMPACTION_BLOCK_MAX_TOKENS
10
+ : DEFAULT_COMPACTION_TRIGGER_MAX_TOKENS;
11
+ return Math.min(threshold, maximum);
12
+ }
13
+
14
+ module.exports = {
15
+ DEFAULT_COMPACTION_TRIGGER_MAX_TOKENS,
16
+ DEFAULT_COMPACTION_BLOCK_MAX_TOKENS,
17
+ capCompactionThreshold,
18
+ };
@@ -18,7 +18,7 @@ const {
18
18
  const { installManagedTelegramPlugin } = require('./plugin-bootstrap');
19
19
  const {
20
20
  availableStandardToolNames,
21
- seedStandardDesignSkill,
21
+ seedStandardSkills,
22
22
  seedStandardTools,
23
23
  } = require('./standard-tools-bootstrap');
24
24
  const { CORE_LOADED_MESSAGE, RUNTIME_READY_MESSAGE } = require('./core-bootstrap');
@@ -552,7 +552,7 @@ async function runLauncher(options = {}) {
552
552
  });
553
553
 
554
554
  // --- 4. Eigenes BLUN-Grunddesign als einzigen Standard-Skill installieren
555
- seedStandardDesignSkill({ packageRoot: PKG, blunDir: privatePaths.sharedHome });
555
+ seedStandardSkills({ packageRoot: PKG, blunDir: privatePaths.sharedHome });
556
556
 
557
557
  // --- 5. Telegram-Plugin + Token-Vorlage (nur King) -----------------------
558
558
  if (mode === LAUNCHER_MODES.KING) {
@@ -0,0 +1,77 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const zlib = require('node:zlib');
5
+
6
+ function tarText(buffer, start, length) {
7
+ return buffer.subarray(start, start + length).toString('utf8').replace(/\0.*$/s, '').trim();
8
+ }
9
+
10
+ function listTarGzPaths(tarballPath) {
11
+ const archive = zlib.gunzipSync(fs.readFileSync(tarballPath));
12
+ const paths = [];
13
+ let offset = 0;
14
+ while (offset + 512 <= archive.length) {
15
+ const header = archive.subarray(offset, offset + 512);
16
+ if (header.every((byte) => byte === 0)) break;
17
+ const name = tarText(header, 0, 100);
18
+ const prefix = tarText(header, 345, 155);
19
+ const sizeText = tarText(header, 124, 12);
20
+ const size = sizeText ? Number.parseInt(sizeText, 8) : 0;
21
+ if (!Number.isFinite(size)) throw new Error(`Ungueltige TAR-Groesse bei ${name || '[ohne Namen]'}`);
22
+ const fullName = [prefix, name].filter(Boolean).join('/').replaceAll('\\', '/');
23
+ if (fullName) paths.push(fullName);
24
+ offset += 512 + Math.ceil(size / 512) * 512;
25
+ }
26
+ return paths;
27
+ }
28
+
29
+ function isStablePackagePath(filePath) {
30
+ const normalized = String(filePath || '').replaceAll('\\', '/');
31
+ if (!normalized || normalized.endsWith('/')) return false;
32
+ if (normalized.startsWith('package/dist-web/assets/')) return false;
33
+ if (normalized.startsWith('package/dist-web/vis/')) return false;
34
+ return normalized.startsWith('package/');
35
+ }
36
+
37
+ function validatePlannedRemovals(document) {
38
+ const removals = Array.isArray(document?.removals) ? document.removals : [];
39
+ for (const removal of removals) {
40
+ if (!isStablePackagePath(removal?.path)) throw new Error(`Ungueltiger Entfernungspfad: ${removal?.path || '[leer]'}`);
41
+ if (!String(removal?.reason || '').trim()) throw new Error(`Entfernungsgrund fehlt: ${removal.path}`);
42
+ if (!String(removal?.decisionRef || '').trim()) throw new Error(`Entscheidungsreferenz fehlt: ${removal.path}`);
43
+ }
44
+ return new Set(removals.map((removal) => removal.path.replaceAll('\\', '/')));
45
+ }
46
+
47
+ function findUnexpectedRemovals(previousPaths, currentPaths, plannedDocument = { removals: [] }) {
48
+ const current = new Set(currentPaths.map((entry) => String(entry).replaceAll('\\', '/')));
49
+ const planned = validatePlannedRemovals(plannedDocument);
50
+ return [...new Set(previousPaths
51
+ .map((entry) => String(entry).replaceAll('\\', '/'))
52
+ .filter(isStablePackagePath)
53
+ .filter((entry) => !current.has(entry) && !planned.has(entry)))]
54
+ .sort();
55
+ }
56
+
57
+ function comparePackagePaths(previousPaths, currentPaths, plannedDocument = { removals: [] }) {
58
+ const previousStablePaths = [...new Set(previousPaths
59
+ .map((entry) => String(entry).replaceAll('\\', '/'))
60
+ .filter(isStablePackagePath))];
61
+ const currentStablePaths = [...new Set(currentPaths
62
+ .map((entry) => String(entry).replaceAll('\\', '/'))
63
+ .filter(isStablePackagePath))];
64
+ return {
65
+ checkedStablePaths: previousStablePaths.length,
66
+ currentStablePaths: currentStablePaths.length,
67
+ unexpectedRemovals: findUnexpectedRemovals(previousStablePaths, currentStablePaths, plannedDocument),
68
+ };
69
+ }
70
+
71
+ module.exports = {
72
+ comparePackagePaths,
73
+ findUnexpectedRemovals,
74
+ isStablePackagePath,
75
+ listTarGzPaths,
76
+ validatePlannedRemovals,
77
+ };
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+
3
+ const DEFAULT_PROVIDER_IDLE_TIMEOUT_MS = 30 * 1000;
4
+
5
+ function resolveProviderIdleTimeoutMs(configuredSeconds) {
6
+ return configuredSeconds === undefined
7
+ ? DEFAULT_PROVIDER_IDLE_TIMEOUT_MS
8
+ : configuredSeconds * 1000;
9
+ }
10
+
11
+ module.exports = {
12
+ DEFAULT_PROVIDER_IDLE_TIMEOUT_MS,
13
+ resolveProviderIdleTimeoutMs,
14
+ };
@@ -0,0 +1,33 @@
1
+ 'use strict';
2
+
3
+ const SKILL_DESCRIPTION_MAX_CHARS = 80;
4
+ const FIRST_USE_SKILLS = new Set([
5
+ 'agent-browser',
6
+ 'windows-mcp',
7
+ 'translate-native',
8
+ 'native-diacritics',
9
+ 'blun-app-design-system',
10
+ ]);
11
+
12
+ function compactDescription(value) {
13
+ const normalized = String(value || '').replace(/\s+/g, ' ').trim();
14
+ if (normalized.length <= SKILL_DESCRIPTION_MAX_CHARS) return normalized;
15
+ return `${normalized.slice(0, SKILL_DESCRIPTION_MAX_CHARS - 1).trimEnd()}...`;
16
+ }
17
+
18
+ function renderCompactSkillEntry(skill) {
19
+ const name = String(skill?.name || '').trim();
20
+ if (!FIRST_USE_SKILLS.has(name.toLowerCase())) return `- ${name}`;
21
+ const description = compactDescription(skill?.description);
22
+ return description ? `- ${name}: ${description}` : `- ${name}`;
23
+ }
24
+
25
+ function renderCompactSkillListing(skills) {
26
+ return (Array.isArray(skills) ? skills : []).map(renderCompactSkillEntry).join('\n');
27
+ }
28
+
29
+ module.exports = {
30
+ SKILL_DESCRIPTION_MAX_CHARS,
31
+ renderCompactSkillEntry,
32
+ renderCompactSkillListing,
33
+ };
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { randomUUID } = require('node:crypto');
3
+ const { createHash, randomUUID } = require('node:crypto');
4
4
  const fs = require('node:fs');
5
5
  const path = require('node:path');
6
6
 
@@ -218,42 +218,96 @@ function seedStandardTools({
218
218
  return { added, updated, configPath };
219
219
  }
220
220
 
221
- function seedStandardDesignSkill({ packageRoot, blunDir }) {
222
- const sourcePath = path.join(
223
- packageRoot,
224
- 'standard-skills',
225
- 'blun-app-design-system',
226
- );
227
- const sourceStat = fs.lstatSync(sourcePath);
228
- if (!sourceStat.isDirectory() || sourceStat.isSymbolicLink()) {
229
- throw new Error(`Ungueltiger BLUN-Standard-Skill: ${sourcePath}`);
221
+ function seedStandardSkills({ packageRoot, blunDir }) {
222
+ const sourceRoot = path.join(packageRoot, 'standard-skills');
223
+ const sourceRootStat = fs.lstatSync(sourceRoot);
224
+ if (!sourceRootStat.isDirectory() || sourceRootStat.isSymbolicLink()) {
225
+ throw new Error(`Ungueltiges BLUN-Standard-Skill-Verzeichnis: ${sourceRoot}`);
230
226
  }
231
-
232
227
  const skillsDir = path.join(blunDir, 'skills');
233
228
  ensurePrivateDirectory(skillsDir);
234
- const destinationPath = path.join(skillsDir, 'blun-app-design-system');
235
- const installed = !fs.existsSync(destinationPath);
236
- if (!installed) {
237
- const destinationStat = fs.lstatSync(destinationPath);
238
- if (!destinationStat.isDirectory() || destinationStat.isSymbolicLink()) {
239
- throw new Error(`Ungueltiges Ziel fuer BLUN-Standard-Skill: ${destinationPath}`);
229
+ const markerPath = path.join(skillsDir, '.blun-standard-skills.json');
230
+ const installed = [];
231
+ const updated = [];
232
+ const destinationPaths = [];
233
+
234
+ const entries = fs.readdirSync(sourceRoot, { withFileTypes: true })
235
+ .filter((entry) => entry.isDirectory() && !entry.isSymbolicLink())
236
+ .sort((left, right) => left.name.localeCompare(right.name));
237
+ const fingerprint = createHash('sha256');
238
+ const visit = (directory, relative = '') => {
239
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })
240
+ .sort((left, right) => left.name.localeCompare(right.name))) {
241
+ const sourcePath = path.join(directory, entry.name);
242
+ const relativePath = path.join(relative, entry.name).replaceAll('\\', '/');
243
+ if (entry.isSymbolicLink()) throw new Error(`Symlink im BLUN-Standard-Skill: ${sourcePath}`);
244
+ fingerprint.update(relativePath);
245
+ if (entry.isDirectory()) visit(sourcePath, relativePath);
246
+ else if (entry.isFile()) fingerprint.update(fs.readFileSync(sourcePath));
247
+ else throw new Error(`Ungueltiger Eintrag im BLUN-Standard-Skill: ${sourcePath}`);
240
248
  }
241
- } else {
242
- ensurePrivateDirectory(destinationPath);
249
+ };
250
+ visit(sourceRoot);
251
+ const sourceFingerprint = fingerprint.digest('hex');
252
+ let marker = null;
253
+ try {
254
+ marker = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
255
+ } catch {
256
+ marker = null;
243
257
  }
244
-
245
- fs.cpSync(sourcePath, destinationPath, {
246
- recursive: true,
247
- force: true,
248
- errorOnExist: false,
258
+ const allDestinationsExist = entries.every((entry) => {
259
+ const destinationPath = path.join(skillsDir, entry.name);
260
+ destinationPaths.push(destinationPath);
261
+ if (!fs.existsSync(destinationPath)) return false;
262
+ const stat = fs.lstatSync(destinationPath);
263
+ return stat.isDirectory() && !stat.isSymbolicLink();
249
264
  });
250
- securePrivateTree(destinationPath);
251
- return { installed, updated: !installed, destinationPath };
265
+ if (marker?.fingerprint === sourceFingerprint && allDestinationsExist) {
266
+ return { installed, updated, destinationPaths, unchanged: true };
267
+ }
268
+ destinationPaths.length = 0;
269
+
270
+ for (const entry of entries) {
271
+ const sourcePath = path.join(sourceRoot, entry.name);
272
+ const destinationPath = path.join(skillsDir, entry.name);
273
+ const isInstalled = !fs.existsSync(destinationPath);
274
+ if (!isInstalled) {
275
+ const destinationStat = fs.lstatSync(destinationPath);
276
+ if (!destinationStat.isDirectory() || destinationStat.isSymbolicLink()) {
277
+ throw new Error(`Ungueltiges Ziel fuer BLUN-Standard-Skill: ${destinationPath}`);
278
+ }
279
+ } else {
280
+ fs.mkdirSync(destinationPath, { mode: 0o700 });
281
+ }
282
+
283
+ fs.cpSync(sourcePath, destinationPath, {
284
+ recursive: true,
285
+ force: true,
286
+ errorOnExist: false,
287
+ });
288
+ (isInstalled ? installed : updated).push(entry.name);
289
+ destinationPaths.push(destinationPath);
290
+ }
291
+
292
+ fs.writeFileSync(markerPath, `${JSON.stringify({ fingerprint: sourceFingerprint, skills: entries.map((entry) => entry.name) }, null, 2)}\n`, 'utf8');
293
+ if (process.platform !== 'win32') securePrivateTree(skillsDir);
294
+ return { installed, updated, destinationPaths, unchanged: false };
295
+ }
296
+
297
+ function seedStandardDesignSkill(options) {
298
+ const result = seedStandardSkills(options);
299
+ const name = 'blun-app-design-system';
300
+ return {
301
+ installed: result.installed.includes(name),
302
+ updated: result.updated.includes(name),
303
+ destinationPath: path.join(options.blunDir, 'skills', name),
304
+ };
252
305
  }
253
306
 
254
307
  module.exports = {
255
308
  availableStandardToolNames,
256
309
  readCatalogue,
257
310
  seedStandardDesignSkill,
311
+ seedStandardSkills,
258
312
  seedStandardTools,
259
313
  };
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+
3
+ const EXPLICIT_THINKING_INTENT = /\b(?:analy[sz](?:e|ing|is)|root\s+cause|investigat(?:e|ion)|debug(?:ging)?|architect(?:ure|ural)|plan(?:ning)?|review|audit|threat\s+model|refactor|optim(?:ize|ise|ization)|migration|complex|thorough|deep(?:ly)?|untersuch(?:e|ung)|analysier(?:e|en)?|ursachenanalyse|architektur|plan(?:e|ung)|pruef(?:e|ung)|sicherheit|migration|komplex|gruendlich|analiz(?:a|iraj)|istrazi|arhitektur|planiraj|sigurnost|duboko)\b/iu;
4
+ const ACTIONABLE_PROMPT = /\b(?:aender|analys|apply|bau|build|check|commit|cop(?:y|ier)|create|debug|delete|deploy|edit|entfern|erstelle|find|fix|implement|install|lad|loesch|merge|mess|move|napravi|oeffne|open|patch|pruef|publish|push|read|reparier|restart|run|schreib|search|starte?|stop|such|test|update|verschieb|write|zieh)\w*\b/iu;
5
+ const STANDALONE_CHAT = /^(?:hi|hallo|hey|hello|hoi|moin|servus|yo|danke|thanks|thx|bye|tschau|ciao)[.!?]*$/iu;
6
+ const IDENTITY_CHAT = /^(?:wer bist du|who are you|wie geht(?: es dir)?|how are you|na)[.!?]*$/iu;
7
+ const STATUS_ONLY_CHECK_IN = /(?:\bstandabfrage\b|\bstatusabfrage\b|\bkurzes?\s+update\b|\bwo\s+stehst\s+du\b|\bwie\s+weit\s+bist\s+du\b|\bwhere\s+are\s+you\b|\bquick\s+status\b)/iu;
8
+ const STATUS_FOLLOW_UP_ACTION = /\b(?:aendere|arbeite|baue|build|change|delete|deploy|deploye|fahr|fix|fixe|implement|implementiere|install|installiere|loesche|mach|patch|pruefe|repariere|run|schreibe|setze|start|starte|stop|stoppe|test|teste|write)\b/iu;
9
+ const LIGHTWEIGHT_STEER = /\b(?:cool|danke|dankeschoen|gute arbeit|passt|perfekt|stolz|super|freut mich)\b/iu;
10
+
11
+ function selectThinkingEffortForTurn(text, originKind) {
12
+ if (originKind !== "user") return undefined;
13
+
14
+ const normalized = String(text ?? "").replace(/\s+/gu, " ").trim();
15
+ if (!normalized) return "low";
16
+
17
+ const lower = normalized.toLowerCase();
18
+ if (STANDALONE_CHAT.test(lower) || IDENTITY_CHAT.test(lower)) return "low";
19
+ if (
20
+ STATUS_ONLY_CHECK_IN.test(normalized)
21
+ && !STATUS_FOLLOW_UP_ACTION.test(normalized)
22
+ && !EXPLICIT_THINKING_INTENT.test(normalized)
23
+ ) return "low";
24
+ if (normalized.length > 180 || String(text ?? "").split(/\r?\n/u).length > 2) return undefined;
25
+ if (ACTIONABLE_PROMPT.test(normalized) || EXPLICIT_THINKING_INTENT.test(normalized)) return undefined;
26
+
27
+ const shortQuestion = /[?]\s*$/u.test(normalized)
28
+ || /^(?:was|wer|wie|warum|wieso|weshalb|wo|wann|welche?r?s?|what|who|how|why|where|when|which|can you|kannst du)\b/iu.test(normalized);
29
+ return shortQuestion ? "low" : undefined;
30
+ }
31
+
32
+ function shouldEnableThinkingForTurn(text, originKind) {
33
+ return selectThinkingEffortForTurn(text, originKind) !== "off";
34
+ }
35
+
36
+ function selectThinkingEffortForWorkStep(stepNumber, previousToolOutcome) {
37
+ const normalizedStep = Number(stepNumber);
38
+ if (!Number.isInteger(normalizedStep) || normalizedStep <= 1) return undefined;
39
+ return previousToolOutcome === "success" ? "low" : undefined;
40
+ }
41
+
42
+ function selectThinkingEffortForBufferedSteer(text, originKind) {
43
+ const classified = selectThinkingEffortForTurn(text, originKind);
44
+ if (classified !== undefined) return classified;
45
+ if (originKind !== "user") return undefined;
46
+
47
+ const normalized = String(text ?? "").replace(/\s+/gu, " ").trim();
48
+ if (
49
+ !normalized
50
+ || normalized.length > 180
51
+ || String(text ?? "").split(/\r?\n/u).length > 2
52
+ || ACTIONABLE_PROMPT.test(normalized)
53
+ || EXPLICIT_THINKING_INTENT.test(normalized)
54
+ ) return undefined;
55
+
56
+ return LIGHTWEIGHT_STEER.test(normalized) ? "low" : undefined;
57
+ }
58
+
59
+ module.exports = {
60
+ ACTIONABLE_PROMPT,
61
+ EXPLICIT_THINKING_INTENT,
62
+ selectThinkingEffortForBufferedSteer,
63
+ selectThinkingEffortForWorkStep,
64
+ selectThinkingEffortForTurn,
65
+ shouldEnableThinkingForTurn,
66
+ };
@@ -0,0 +1,141 @@
1
+ 'use strict';
2
+
3
+ const TOOL_SCHEMA_BUDGET_RATIO = 0.25;
4
+ const TOOL_SCHEMA_MAX_TOKENS = 32_000;
5
+ const DEFERRED_TOOL_LOADER_NAME = 'ToolSearch';
6
+ const CORE_TOOL_NAMES = Object.freeze([
7
+ 'Bash',
8
+ 'Read',
9
+ 'Edit',
10
+ 'Grep',
11
+ 'Write',
12
+ 'TodoList',
13
+ 'Glob',
14
+ 'TaskList',
15
+ 'Agent',
16
+ 'GetGoal',
17
+ 'CronList',
18
+ 'ReadMediaFile',
19
+ 'TaskOutput',
20
+ 'Skill',
21
+ 'mcp__plugin-telegram_telegram__reply',
22
+ 'mcp__plugin-telegram_telegram__react',
23
+ 'mcp__plugin-telegram_telegram__edit_message',
24
+ 'mcp__plugin-telegram_telegram__download_attachment',
25
+ ]);
26
+
27
+ function toolSchemaBudgetTokens(maxContextTokens) {
28
+ const context = Number(maxContextTokens);
29
+ if (!Number.isFinite(context) || context <= 0) return TOOL_SCHEMA_MAX_TOKENS;
30
+ return Math.min(TOOL_SCHEMA_MAX_TOKENS, Math.floor(context * TOOL_SCHEMA_BUDGET_RATIO));
31
+ }
32
+
33
+ function deferredToolNames(tools) {
34
+ return [...new Set((Array.isArray(tools) ? tools : [])
35
+ .map((tool) => String(tool?.name || '').trim())
36
+ .filter(Boolean))].sort((left, right) => left.localeCompare(right));
37
+ }
38
+
39
+ function searchDeferredTools(tools, query) {
40
+ const normalized = String(query || '').trim().toLowerCase();
41
+ if (!normalized) return [];
42
+ if (normalized.startsWith('select:')) {
43
+ const selector = normalized.slice('select:'.length).trim();
44
+ if (!selector) return [];
45
+ const exact = tools.find((tool) => String(tool?.name || '').toLowerCase() === selector);
46
+ if (exact) return [exact];
47
+ return tools.filter((tool) => {
48
+ const name = String(tool?.name || '').toLowerCase();
49
+ const leaf = name.split(/__|:/).at(-1);
50
+ return leaf === selector;
51
+ });
52
+ }
53
+
54
+ const tokens = normalized.split(/\s+/).filter(Boolean);
55
+ const requiredNameTokens = tokens.filter((token) => token.startsWith('+')).map((token) => token.slice(1)).filter(Boolean);
56
+ const searchTokens = tokens.filter((token) => !token.startsWith('+'));
57
+ return tools.map((tool) => {
58
+ const name = String(tool?.name || '').toLowerCase();
59
+ const description = String(tool?.description || '').toLowerCase();
60
+ if (!requiredNameTokens.every((token) => name.includes(token))) return null;
61
+ if (!searchTokens.every((token) => name.includes(token) || description.includes(token))) return null;
62
+ const score = searchTokens.reduce((total, token) => total + (name.includes(token) ? 5 : 1), 0)
63
+ + requiredNameTokens.length * 5;
64
+ return { tool, score };
65
+ }).filter(Boolean).sort((left, right) => right.score - left.score || String(left.tool.name).localeCompare(String(right.tool.name))).slice(0, 5).map((entry) => entry.tool);
66
+ }
67
+
68
+ function createDeferredToolLoader(selectedTools, deferredTools, loadedToolNames = new Set()) {
69
+ if (!Array.isArray(selectedTools)) throw new TypeError('selectedTools must be an array');
70
+ if (!(loadedToolNames instanceof Set)) throw new TypeError('loadedToolNames must be a Set');
71
+ const available = new Map();
72
+ for (const tool of Array.isArray(deferredTools) ? deferredTools : []) {
73
+ const name = String(tool?.name || '').trim();
74
+ if (name && !available.has(name)) available.set(name, tool);
75
+ }
76
+ const names = deferredToolNames([...available.values()]);
77
+
78
+ return {
79
+ name: DEFERRED_TOOL_LOADER_NAME,
80
+ description: [
81
+ 'Find and load deferred tool schemas into this running session.',
82
+ 'Use select:leaf_name for one known tool, plain keywords to search, or +word to require that word in the tool name.',
83
+ 'Loaded tools are available in the next step and remain loaded for this session.',
84
+ `Available deferred tool names: ${names.join(', ')}`,
85
+ ].join('\n'),
86
+ parameters: {
87
+ type: 'object',
88
+ properties: {
89
+ query: {
90
+ type: 'string',
91
+ description: 'Exact selection or keyword query, for example select:download_attachment or +slack send.',
92
+ },
93
+ },
94
+ required: ['query'],
95
+ additionalProperties: false,
96
+ },
97
+ resolveExecution(args) {
98
+ const query = String(args?.query || '').trim();
99
+ return {
100
+ description: query ? `Searching tool schemas for ${query}` : 'Searching tool schemas',
101
+ approvalRule: DEFERRED_TOOL_LOADER_NAME,
102
+ execute: async () => {
103
+ const matches = searchDeferredTools([...available.values()], query);
104
+ if (matches.length === 0) {
105
+ return {
106
+ isError: true,
107
+ output: `No deferred tools matched: ${query || '[empty query]'}`,
108
+ };
109
+ }
110
+ if (query.toLowerCase().startsWith('select:') && matches.length !== 1) {
111
+ return {
112
+ isError: true,
113
+ output: `Tool selection is ambiguous: ${matches.map((tool) => tool.name).join(', ')}`,
114
+ };
115
+ }
116
+ const loaded = [];
117
+ for (const tool of matches) {
118
+ const name = String(tool.name);
119
+ if (!selectedTools.some((candidate) => candidate?.name === name)) selectedTools.push(tool);
120
+ loadedToolNames.add(name);
121
+ loaded.push(name);
122
+ }
123
+ return {
124
+ isError: false,
125
+ output: `Loaded tool schemas: ${loaded.join(', ')}. Invoke them in the next step.`,
126
+ };
127
+ },
128
+ };
129
+ },
130
+ };
131
+ }
132
+
133
+ module.exports = {
134
+ CORE_TOOL_NAMES,
135
+ DEFERRED_TOOL_LOADER_NAME,
136
+ TOOL_SCHEMA_BUDGET_RATIO,
137
+ TOOL_SCHEMA_MAX_TOKENS,
138
+ createDeferredToolLoader,
139
+ deferredToolNames,
140
+ toolSchemaBudgetTokens,
141
+ };