scalix 0.0.1 → 1.0.0

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 (48) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/LICENSE +100 -0
  3. package/README.md +300 -2
  4. package/app.py +55 -0
  5. package/bin/scalix.js +19 -0
  6. package/dist/agent-replay.js +166 -0
  7. package/dist/agent.js +852 -0
  8. package/dist/auto-verify.js +136 -0
  9. package/dist/change-tracker.js +53 -0
  10. package/dist/cli.js +2329 -0
  11. package/dist/commands.js +71 -0
  12. package/dist/config.js +16 -0
  13. package/dist/constants.js +324 -0
  14. package/dist/context-builder.js +151 -0
  15. package/dist/context-manager.js +148 -0
  16. package/dist/cost-tracker.js +85 -0
  17. package/dist/diff.js +140 -0
  18. package/dist/git-context.js +180 -0
  19. package/dist/git-undo.js +67 -0
  20. package/dist/headless.js +654 -0
  21. package/dist/hooks.js +94 -0
  22. package/dist/ipc-server.js +228 -0
  23. package/dist/logger.js +45 -0
  24. package/dist/markdown.js +41 -0
  25. package/dist/mcp.js +49 -0
  26. package/dist/model-router.js +68 -0
  27. package/dist/plugins.js +132 -0
  28. package/dist/project-index.js +374 -0
  29. package/dist/project-memory.js +165 -0
  30. package/dist/safe-math.js +194 -0
  31. package/dist/sandbox.js +279 -0
  32. package/dist/scoped-rules.js +82 -0
  33. package/dist/sessions.js +71 -0
  34. package/dist/settings.js +155 -0
  35. package/dist/sub-agent.js +79 -0
  36. package/dist/token-counter.js +47 -0
  37. package/dist/tool-helper.js +97 -0
  38. package/dist/tool-selector.js +77 -0
  39. package/dist/tools-advanced.js +232 -0
  40. package/dist/tools-browser.js +273 -0
  41. package/dist/tools-extra.js +383 -0
  42. package/dist/tools-vision.js +115 -0
  43. package/dist/tools-voice.js +147 -0
  44. package/dist/tools-web.js +346 -0
  45. package/dist/tools.js +370 -0
  46. package/dist/types.js +1 -0
  47. package/docs/DISTRIBUTION.md +301 -0
  48. package/package.json +67 -6
@@ -0,0 +1,155 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { DEFAULT_BASE_URL, DEFAULT_MODELS, AGENT_DEFAULTS, CONTEXT_DEFAULTS, WEB_DEFAULTS, TOOL_DEFAULTS, SUB_AGENT_DEFAULTS, USER_CONFIG_DIR, registerUserModels, } from './constants.js';
4
+ const CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', USER_CONFIG_DIR);
5
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
6
+ export const THINKING_MODES = ['off', 'low', 'medium', 'high'];
7
+ export const DEFAULT_LIMITS = {
8
+ maxSteps: AGENT_DEFAULTS.maxSteps,
9
+ maxMessages: AGENT_DEFAULTS.maxMessages,
10
+ maxContextTokens: CONTEXT_DEFAULTS.maxContextTokens,
11
+ compressionThreshold: CONTEXT_DEFAULTS.compressionThreshold,
12
+ maxToolResultTokens: CONTEXT_DEFAULTS.maxToolResultTokens,
13
+ toolTimeoutMs: AGENT_DEFAULTS.toolTimeoutMs,
14
+ maxRetries: AGENT_DEFAULTS.maxRetries,
15
+ messageKeepRatio: AGENT_DEFAULTS.messageKeepRatio,
16
+ circuitBreakerThreshold: AGENT_DEFAULTS.circuitBreakerThreshold,
17
+ circuitBreakerResetMs: AGENT_DEFAULTS.circuitBreakerResetMs,
18
+ commandTimeoutMs: TOOL_DEFAULTS.commandTimeoutMs,
19
+ webFetchTimeoutMs: WEB_DEFAULTS.fetchTimeoutMs,
20
+ htmlMaxSize: WEB_DEFAULTS.htmlMaxSize,
21
+ defaultSearchResults: WEB_DEFAULTS.defaultSearchResults,
22
+ defaultMaxFetchLength: WEB_DEFAULTS.defaultMaxLength,
23
+ bgBufferMax: TOOL_DEFAULTS.bgBufferMax,
24
+ bgCleanupMs: TOOL_DEFAULTS.bgCleanupMs,
25
+ searchTimeoutMs: TOOL_DEFAULTS.searchTimeoutMs,
26
+ fileRefTruncation: TOOL_DEFAULTS.fileRefTruncation,
27
+ charsPerToken: CONTEXT_DEFAULTS.charsPerToken,
28
+ minRecentMessages: CONTEXT_DEFAULTS.minRecentMessages,
29
+ subAgentTimeoutMs: SUB_AGENT_DEFAULTS.timeoutMs,
30
+ subAgentMaxSteps: SUB_AGENT_DEFAULTS.maxSteps,
31
+ subAgentMaxContextTokens: SUB_AGENT_DEFAULTS.maxContextTokens,
32
+ subAgentCompressionThreshold: SUB_AGENT_DEFAULTS.compressionThreshold,
33
+ agentToolTimeoutMs: SUB_AGENT_DEFAULTS.agentToolTimeoutMs,
34
+ exploreMaxSteps: SUB_AGENT_DEFAULTS.exploreMaxSteps,
35
+ shellDetectionTimeoutMs: TOOL_DEFAULTS.shellDetectionTimeoutMs,
36
+ treeMaxEntries: TOOL_DEFAULTS.treeMaxEntries,
37
+ gitTimeoutMs: TOOL_DEFAULTS.gitTimeoutMs,
38
+ gitDiffMaxBuffer: TOOL_DEFAULTS.gitDiffMaxBuffer,
39
+ readFileMaxSize: TOOL_DEFAULTS.readFileMaxSize,
40
+ maxEditStringLength: TOOL_DEFAULTS.maxEditStringLength,
41
+ maxNotebookSource: TOOL_DEFAULTS.maxNotebookSource,
42
+ maxWebFetchLength: TOOL_DEFAULTS.maxWebFetchLength,
43
+ slidingWindowTarget: CONTEXT_DEFAULTS.slidingWindowTarget,
44
+ };
45
+ const DEFAULTS = {
46
+ apiKey: '',
47
+ baseUrl: DEFAULT_BASE_URL,
48
+ models: [...DEFAULT_MODELS],
49
+ customModels: {},
50
+ limits: { ...DEFAULT_LIMITS },
51
+ permissionMode: 'default',
52
+ thinkingMode: 'off',
53
+ };
54
+ function safeNumber(val, min, max, fallback) {
55
+ if (typeof val === 'number' && Number.isFinite(val) && val >= min && val <= max)
56
+ return val;
57
+ return fallback;
58
+ }
59
+ function validateConfig(parsed) {
60
+ const safe = {};
61
+ if (typeof parsed.apiKey === 'string')
62
+ safe.apiKey = parsed.apiKey;
63
+ if (typeof parsed.baseUrl === 'string' && parsed.baseUrl.length > 0)
64
+ safe.baseUrl = parsed.baseUrl;
65
+ if (Array.isArray(parsed.models) && parsed.models.every((m) => typeof m === 'string')) {
66
+ safe.models = parsed.models;
67
+ }
68
+ if (typeof parsed.permissionMode === 'string' && ['plan', 'default', 'auto', 'yolo'].includes(parsed.permissionMode)) {
69
+ safe.permissionMode = parsed.permissionMode;
70
+ }
71
+ if (parsed.customModels && typeof parsed.customModels === 'object' && !Array.isArray(parsed.customModels)) {
72
+ const validated = {};
73
+ for (const [id, spec] of Object.entries(parsed.customModels)) {
74
+ const s = spec;
75
+ if (typeof s === 'object' && s &&
76
+ typeof s.contextWindow === 'number' && s.contextWindow > 0 &&
77
+ typeof s.maxOutputTokens === 'number' && s.maxOutputTokens > 0) {
78
+ validated[id] = { contextWindow: s.contextWindow, maxOutputTokens: s.maxOutputTokens };
79
+ }
80
+ }
81
+ if (Object.keys(validated).length > 0) {
82
+ safe.customModels = validated;
83
+ }
84
+ }
85
+ if (parsed.limits && typeof parsed.limits === 'object' && !Array.isArray(parsed.limits)) {
86
+ const l = parsed.limits;
87
+ safe.limits = {
88
+ maxSteps: safeNumber(l.maxSteps, 1, Infinity, DEFAULT_LIMITS.maxSteps),
89
+ maxMessages: safeNumber(l.maxMessages, 1, Infinity, DEFAULT_LIMITS.maxMessages),
90
+ maxContextTokens: safeNumber(l.maxContextTokens, 1000, Infinity, DEFAULT_LIMITS.maxContextTokens),
91
+ compressionThreshold: safeNumber(l.compressionThreshold, 0.1, 0.99, DEFAULT_LIMITS.compressionThreshold),
92
+ maxToolResultTokens: safeNumber(l.maxToolResultTokens, 100, Infinity, DEFAULT_LIMITS.maxToolResultTokens),
93
+ toolTimeoutMs: safeNumber(l.toolTimeoutMs, 1000, 600_000, DEFAULT_LIMITS.toolTimeoutMs),
94
+ maxRetries: safeNumber(l.maxRetries, 0, 20, DEFAULT_LIMITS.maxRetries),
95
+ messageKeepRatio: safeNumber(l.messageKeepRatio, 0.1, 0.99, DEFAULT_LIMITS.messageKeepRatio),
96
+ circuitBreakerThreshold: safeNumber(l.circuitBreakerThreshold, 1, 50, DEFAULT_LIMITS.circuitBreakerThreshold),
97
+ circuitBreakerResetMs: safeNumber(l.circuitBreakerResetMs, 1000, 600_000, DEFAULT_LIMITS.circuitBreakerResetMs),
98
+ commandTimeoutMs: safeNumber(l.commandTimeoutMs, 1000, 600_000, DEFAULT_LIMITS.commandTimeoutMs),
99
+ webFetchTimeoutMs: safeNumber(l.webFetchTimeoutMs, 1000, 120_000, DEFAULT_LIMITS.webFetchTimeoutMs),
100
+ htmlMaxSize: safeNumber(l.htmlMaxSize, 1024, Infinity, DEFAULT_LIMITS.htmlMaxSize),
101
+ defaultSearchResults: safeNumber(l.defaultSearchResults, 1, 100, DEFAULT_LIMITS.defaultSearchResults),
102
+ defaultMaxFetchLength: safeNumber(l.defaultMaxFetchLength, 1000, Infinity, DEFAULT_LIMITS.defaultMaxFetchLength),
103
+ bgBufferMax: safeNumber(l.bgBufferMax, 1024, Infinity, DEFAULT_LIMITS.bgBufferMax),
104
+ bgCleanupMs: safeNumber(l.bgCleanupMs, 60_000, 86_400_000, DEFAULT_LIMITS.bgCleanupMs),
105
+ searchTimeoutMs: safeNumber(l.searchTimeoutMs, 1000, 600_000, DEFAULT_LIMITS.searchTimeoutMs),
106
+ fileRefTruncation: safeNumber(l.fileRefTruncation, 1000, Infinity, DEFAULT_LIMITS.fileRefTruncation),
107
+ charsPerToken: safeNumber(l.charsPerToken, 1, 10, DEFAULT_LIMITS.charsPerToken),
108
+ minRecentMessages: safeNumber(l.minRecentMessages, 1, 100, DEFAULT_LIMITS.minRecentMessages),
109
+ subAgentTimeoutMs: safeNumber(l.subAgentTimeoutMs, 5000, Infinity, DEFAULT_LIMITS.subAgentTimeoutMs),
110
+ subAgentMaxSteps: safeNumber(l.subAgentMaxSteps, 1, Infinity, DEFAULT_LIMITS.subAgentMaxSteps),
111
+ subAgentMaxContextTokens: safeNumber(l.subAgentMaxContextTokens, 1000, Infinity, DEFAULT_LIMITS.subAgentMaxContextTokens),
112
+ subAgentCompressionThreshold: safeNumber(l.subAgentCompressionThreshold, 0.1, 0.99, DEFAULT_LIMITS.subAgentCompressionThreshold),
113
+ agentToolTimeoutMs: safeNumber(l.agentToolTimeoutMs, 5000, Infinity, DEFAULT_LIMITS.agentToolTimeoutMs),
114
+ exploreMaxSteps: safeNumber(l.exploreMaxSteps, 1, Infinity, DEFAULT_LIMITS.exploreMaxSteps),
115
+ shellDetectionTimeoutMs: safeNumber(l.shellDetectionTimeoutMs, 500, 30_000, DEFAULT_LIMITS.shellDetectionTimeoutMs),
116
+ treeMaxEntries: safeNumber(l.treeMaxEntries, 10, Infinity, DEFAULT_LIMITS.treeMaxEntries),
117
+ gitTimeoutMs: safeNumber(l.gitTimeoutMs, 1000, 120_000, DEFAULT_LIMITS.gitTimeoutMs),
118
+ gitDiffMaxBuffer: safeNumber(l.gitDiffMaxBuffer, 1024, Infinity, DEFAULT_LIMITS.gitDiffMaxBuffer),
119
+ readFileMaxSize: safeNumber(l.readFileMaxSize, 1024, Infinity, DEFAULT_LIMITS.readFileMaxSize),
120
+ maxEditStringLength: safeNumber(l.maxEditStringLength, 1000, Infinity, DEFAULT_LIMITS.maxEditStringLength),
121
+ maxNotebookSource: safeNumber(l.maxNotebookSource, 1000, Infinity, DEFAULT_LIMITS.maxNotebookSource),
122
+ maxWebFetchLength: safeNumber(l.maxWebFetchLength, 1000, Infinity, DEFAULT_LIMITS.maxWebFetchLength),
123
+ slidingWindowTarget: safeNumber(l.slidingWindowTarget, 0.1, 0.99, DEFAULT_LIMITS.slidingWindowTarget),
124
+ };
125
+ }
126
+ if (typeof parsed.thinkingMode === 'string' && THINKING_MODES.includes(parsed.thinkingMode)) {
127
+ safe.thinkingMode = parsed.thinkingMode;
128
+ }
129
+ return safe;
130
+ }
131
+ async function ensureDir() {
132
+ await fs.mkdir(CONFIG_DIR, { recursive: true });
133
+ }
134
+ export async function loadConfig() {
135
+ try {
136
+ const raw = await fs.readFile(CONFIG_FILE, 'utf-8');
137
+ const parsed = JSON.parse(raw);
138
+ const validated = validateConfig(parsed);
139
+ const config = { ...DEFAULTS, ...validated };
140
+ if (config.customModels && Object.keys(config.customModels).length > 0) {
141
+ registerUserModels(config.customModels);
142
+ }
143
+ return config;
144
+ }
145
+ catch {
146
+ return { ...DEFAULTS };
147
+ }
148
+ }
149
+ export async function saveConfig(config) {
150
+ await ensureDir();
151
+ await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), { encoding: 'utf-8', mode: 0o600 });
152
+ }
153
+ export function isConfigured(config) {
154
+ return config.apiKey.length > 0 && config.models.length > 0;
155
+ }
@@ -0,0 +1,79 @@
1
+ import { createAgent } from './agent.js';
2
+ import { defaultTools } from './tools.js';
3
+ import { extraTools } from './tools-extra.js';
4
+ import { webTools } from './tools-web.js';
5
+ import { advancedTools } from './tools-advanced.js';
6
+ import { logger } from './logger.js';
7
+ import { SUB_AGENT_DEFAULTS, CHEAP_MODEL_INDICATORS } from './constants.js';
8
+ const READ_ONLY_TOOL_NAMES = new Set([
9
+ 'get_current_time', 'calculate', 'read_file', 'read_file_lines',
10
+ 'list_files', 'search_files', 'find_files', 'tree',
11
+ 'git_status', 'git_diff', 'web_search', 'monitor',
12
+ ]);
13
+ function selectTools(level) {
14
+ // Include advanced tools but exclude 'agent' to prevent recursive spawning
15
+ const all = [...defaultTools, ...extraTools, ...webTools, ...advancedTools]
16
+ .filter((t) => (t.function?.name || t.name) !== 'agent');
17
+ switch (level) {
18
+ case 'read-only':
19
+ return all.filter((t) => READ_ONLY_TOOL_NAMES.has(t.function?.name || t.name));
20
+ case 'all':
21
+ return all;
22
+ case 'standard':
23
+ default:
24
+ return all;
25
+ }
26
+ }
27
+ export async function spawnSubAgent(parentConfig, prompt, options = {}) {
28
+ const model = options.model || parentConfig.models[parentConfig.models.length - 1];
29
+ const tools = selectTools(options.tools);
30
+ const timeout = options.timeout ?? SUB_AGENT_DEFAULTS.timeoutMs;
31
+ const permissionMode = options.permissionMode
32
+ ?? (options.tools === 'read-only' ? 'plan' : options.tools === 'all' ? 'auto' : 'default');
33
+ const subAgent = createAgent({
34
+ apiKey: parentConfig.apiKey,
35
+ baseUrl: parentConfig.baseUrl,
36
+ model,
37
+ instructions: options.instructions || 'You are a focused research assistant. Be concise and factual. Respond in under 500 words.',
38
+ tools,
39
+ permissionMode,
40
+ confirmToolCall: options.confirmToolCall,
41
+ maxSteps: options.maxSteps ?? SUB_AGENT_DEFAULTS.maxSteps,
42
+ contextManager: {
43
+ maxContextTokens: SUB_AGENT_DEFAULTS.maxContextTokens,
44
+ compressionThreshold: SUB_AGENT_DEFAULTS.compressionThreshold,
45
+ },
46
+ });
47
+ let steps = 0;
48
+ let tokenEstimate = 0;
49
+ subAgent.on('tool:call', () => { steps++; });
50
+ subAgent.on('usage', (data) => {
51
+ tokenEstimate += data.total_tokens ?? 0;
52
+ });
53
+ let timer;
54
+ const timeoutPromise = new Promise((_, reject) => {
55
+ timer = setTimeout(() => reject(new Error(`Sub-agent timed out after ${timeout}ms`)), timeout);
56
+ });
57
+ try {
58
+ const response = await Promise.race([subAgent.send(prompt), timeoutPromise]);
59
+ logger.info('Sub-agent completed', { model, steps, tokens: tokenEstimate });
60
+ return { response, model, tokenEstimate, steps };
61
+ }
62
+ catch (err) {
63
+ subAgent.abort();
64
+ logger.error('Sub-agent failed', { model, error: err.message });
65
+ throw err;
66
+ }
67
+ finally {
68
+ clearTimeout(timer);
69
+ }
70
+ }
71
+ export function pickCheapestModel(models) {
72
+ const cheapIndicators = CHEAP_MODEL_INDICATORS;
73
+ for (const model of [...models].reverse()) {
74
+ const lower = model.toLowerCase();
75
+ if (cheapIndicators.some((ind) => lower.includes(ind)))
76
+ return model;
77
+ }
78
+ return models[models.length - 1];
79
+ }
@@ -0,0 +1,47 @@
1
+ import { TOKEN_ESTIMATION } from './constants.js';
2
+ const CODE_CHARS = /[{}()\[\]<>;:,."'`=+\-*/%!&|^~?@#$\\]/g;
3
+ const UNICODE_MULTI = /[\u0080-\uffff]/g;
4
+ const WORD_SPLIT = /\s+/;
5
+ export function estimateTokens(text) {
6
+ if (!text)
7
+ return 0;
8
+ const len = text.length;
9
+ if (len < TOKEN_ESTIMATION.simpleCharsPerToken)
10
+ return 1;
11
+ const codeChars = (text.match(CODE_CHARS) || []).length;
12
+ const unicodeChars = (text.match(UNICODE_MULTI) || []).length;
13
+ const words = text.split(WORD_SPLIT).filter(Boolean).length;
14
+ const codeRatio = codeChars / len;
15
+ const charsPerToken = codeRatio > TOKEN_ESTIMATION.codeRatioThreshold
16
+ ? TOKEN_ESTIMATION.codeCharsPerToken
17
+ : TOKEN_ESTIMATION.textCharsPerToken;
18
+ const charEstimate = (len - unicodeChars) / charsPerToken + unicodeChars * TOKEN_ESTIMATION.unicodeTokenMultiplier;
19
+ const wordEstimate = words * TOKEN_ESTIMATION.wordTokenMultiplier + codeChars * TOKEN_ESTIMATION.codeCharTokenMultiplier;
20
+ return Math.max(1, Math.ceil(charEstimate * TOKEN_ESTIMATION.charEstimateWeight + wordEstimate * TOKEN_ESTIMATION.wordEstimateWeight));
21
+ }
22
+ function contentToString(content) {
23
+ if (!content)
24
+ return '';
25
+ if (typeof content === 'string')
26
+ return content;
27
+ return content
28
+ .filter((p) => p.type === 'text')
29
+ .map((p) => p.text)
30
+ .join('\n');
31
+ }
32
+ export function estimateMessageTokens(messages) {
33
+ let total = 0;
34
+ for (const msg of messages) {
35
+ total += TOKEN_ESTIMATION.messageOverhead;
36
+ if (msg.content)
37
+ total += estimateTokens(contentToString(msg.content));
38
+ if (msg.tool_calls) {
39
+ for (const tc of msg.tool_calls) {
40
+ total += estimateTokens(tc.function.name) + estimateTokens(tc.function.arguments) + TOKEN_ESTIMATION.messageOverhead;
41
+ }
42
+ }
43
+ if (msg.name)
44
+ total += estimateTokens(msg.name);
45
+ }
46
+ return total;
47
+ }
@@ -0,0 +1,97 @@
1
+ export function tool(def) {
2
+ return def;
3
+ }
4
+ export function zodToJsonSchema(schema) {
5
+ return convertZod(schema);
6
+ }
7
+ function convertZod(schema) {
8
+ if (!schema || !schema._def)
9
+ return { type: 'object' };
10
+ const def = schema._def;
11
+ const typeName = def.typeName;
12
+ switch (typeName) {
13
+ case 'ZodObject': {
14
+ const shape = def.shape?.() ?? def.shape ?? {};
15
+ const properties = {};
16
+ const required = [];
17
+ for (const [key, value] of Object.entries(shape)) {
18
+ const prop = convertZod(value);
19
+ const isOptional = value?._def?.typeName === 'ZodOptional';
20
+ if (!isOptional)
21
+ required.push(key);
22
+ properties[key] = prop;
23
+ }
24
+ const result = { type: 'object', properties };
25
+ if (required.length > 0)
26
+ result.required = required;
27
+ return result;
28
+ }
29
+ case 'ZodString': {
30
+ const result = { type: 'string' };
31
+ if (def.description)
32
+ result.description = def.description;
33
+ for (const check of def.checks ?? []) {
34
+ if (check.kind === 'min')
35
+ result.minLength = check.value;
36
+ if (check.kind === 'max')
37
+ result.maxLength = check.value;
38
+ if (check.kind === 'url')
39
+ result.format = 'uri';
40
+ }
41
+ return result;
42
+ }
43
+ case 'ZodNumber': {
44
+ const result = { type: 'number' };
45
+ if (def.description)
46
+ result.description = def.description;
47
+ for (const check of def.checks ?? []) {
48
+ if (check.kind === 'min')
49
+ result.minimum = check.value;
50
+ if (check.kind === 'max')
51
+ result.maximum = check.value;
52
+ }
53
+ return result;
54
+ }
55
+ case 'ZodBoolean':
56
+ return { type: 'boolean', ...(def.description ? { description: def.description } : {}) };
57
+ case 'ZodArray': {
58
+ const result = { type: 'array', items: convertZod(def.type) };
59
+ if (def.description)
60
+ result.description = def.description;
61
+ return result;
62
+ }
63
+ case 'ZodOptional': {
64
+ const inner = convertZod(def.innerType);
65
+ if (def.description)
66
+ inner.description = def.description;
67
+ return inner;
68
+ }
69
+ case 'ZodDefault':
70
+ return convertZod(def.innerType);
71
+ case 'ZodEnum':
72
+ return { type: 'string', enum: def.values };
73
+ case 'ZodRecord': {
74
+ const result = { type: 'object', additionalProperties: convertZod(def.valueType) };
75
+ return result;
76
+ }
77
+ case 'ZodUnion':
78
+ case 'ZodDiscriminatedUnion': {
79
+ const options = def.options ?? [];
80
+ const schemas = options.map((o) => convertZod(o));
81
+ return schemas.length === 1 ? schemas[0] : { anyOf: schemas };
82
+ }
83
+ case 'ZodLiteral': {
84
+ const v = def.value;
85
+ return { type: typeof v === 'number' ? 'number' : typeof v === 'boolean' ? 'boolean' : 'string', const: v };
86
+ }
87
+ case 'ZodNullable': {
88
+ const inner = convertZod(def.innerType);
89
+ return { ...inner, nullable: true };
90
+ }
91
+ case 'ZodEffects':
92
+ case 'ZodPipeline':
93
+ return convertZod(def.schema ?? def.innerType);
94
+ default:
95
+ return { type: 'string' };
96
+ }
97
+ }
@@ -0,0 +1,77 @@
1
+ const CORE_TOOL_NAMES = new Set([
2
+ 'read_file',
3
+ 'write_file',
4
+ 'run_command',
5
+ 'list_files',
6
+ 'edit_file',
7
+ 'search_files',
8
+ 'find_files',
9
+ 'todo_write',
10
+ 'monitor',
11
+ ]);
12
+ const CONTEXT_PATTERNS = [
13
+ [/\b(search|find|grep|look\s+for|where)\b/i, /search|find|grep/],
14
+ [/\b(git|commit|diff|branch|push|pull|merge|rebase)\b/i, /git/],
15
+ [/\b(web|url|http|fetch|browse|search\s+online|internet)\b/i, /web|fetch/],
16
+ [/\b(file|read|write|edit|create|open|save)\b/i, /file|read|write|edit/],
17
+ [/\b(tree|structure|directory|folder|layout|project)\b/i, /tree|list|find/],
18
+ [/\b(math|calc|compute|formula|equation)\b/i, /calc/],
19
+ [/\b(time|date|now|today|clock)\b/i, /time/],
20
+ [/\b(line|lines|range|portion|section)\b/i, /lines|read/],
21
+ [/\b(notebook|ipynb)\b|\.ipynb/i, /notebook/],
22
+ [/\b(sub[\s_-]?agent|delegate|parallel|spawn)\b/i, /agent/],
23
+ [/\b(task|todo|plan|track|progress)\b/i, /todo/],
24
+ [/\b(background|process|pid|monitor)\b/i, /monitor/],
25
+ [/\b(run|exec|shell|command|terminal|bash|script)\b/i, /run_command/],
26
+ [/\b(mcp|server|connect|sse|integration)\b/i, /mcp/],
27
+ ];
28
+ function extractKeywords(text) {
29
+ const words = text.toLowerCase()
30
+ .replace(/[^a-z0-9_\s]/g, ' ')
31
+ .split(/\s+/)
32
+ .filter((w) => w.length > 2);
33
+ return new Set(words);
34
+ }
35
+ export class ToolSelector {
36
+ toolIndex = [];
37
+ index(tools) {
38
+ this.toolIndex = tools.map((t) => {
39
+ const name = t.function?.name ?? t.name ?? '';
40
+ const description = t.function?.description ?? t.description ?? '';
41
+ const keywords = extractKeywords(`${name} ${description}`);
42
+ return { tool: t, name, keywords, isCore: CORE_TOOL_NAMES.has(name) };
43
+ });
44
+ }
45
+ select(query, recentToolNames = [], maxTools = 64) {
46
+ if (this.toolIndex.length <= maxTools) {
47
+ return this.toolIndex.map((t) => t.tool);
48
+ }
49
+ const queryKeywords = extractKeywords(query);
50
+ const recentSet = new Set(recentToolNames);
51
+ const scored = this.toolIndex.map((info) => {
52
+ let score = 0;
53
+ if (info.isCore)
54
+ score += 10;
55
+ if (recentSet.has(info.name))
56
+ score += 5;
57
+ for (const kw of queryKeywords) {
58
+ if (info.keywords.has(kw))
59
+ score += 2;
60
+ for (const tk of info.keywords) {
61
+ if (tk.includes(kw) || kw.includes(tk))
62
+ score += 1;
63
+ }
64
+ }
65
+ for (const [queryPattern, namePattern] of CONTEXT_PATTERNS) {
66
+ if (queryPattern.test(query) && namePattern.test(info.name))
67
+ score += 3;
68
+ }
69
+ return { info, score };
70
+ });
71
+ scored.sort((a, b) => b.score - a.score);
72
+ return scored.slice(0, maxTools).map((s) => s.info.tool);
73
+ }
74
+ getToolNames() {
75
+ return this.toolIndex.map((t) => t.name);
76
+ }
77
+ }