nova-terminal-assistant 0.1.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.
Potentially problematic release.
This version of nova-terminal-assistant might be problematic. Click here for more details.
- package/README.md +358 -0
- package/bin/nova +38 -0
- package/bin/nova.js +12 -0
- package/package.json +67 -0
- package/src/cli/commands/SmartCompletion.ts +458 -0
- package/src/cli/index.ts +5 -0
- package/src/cli/startup/IFlowRepl.ts +212 -0
- package/src/cli/startup/InkBasedRepl.ts +1056 -0
- package/src/cli/startup/InteractiveRepl.ts +2833 -0
- package/src/cli/startup/NovaApp.ts +1861 -0
- package/src/cli/startup/index.ts +4 -0
- package/src/cli/startup/parseArgs.ts +293 -0
- package/src/cli/test-modules.ts +27 -0
- package/src/cli/ui/IFlowDropdown.ts +425 -0
- package/src/cli/ui/ModernReplUI.ts +276 -0
- package/src/cli/ui/SimpleSelector2.ts +215 -0
- package/src/cli/ui/components/ConfirmDialog.ts +176 -0
- package/src/cli/ui/components/ErrorPanel.ts +364 -0
- package/src/cli/ui/components/InkAppRunner.tsx +67 -0
- package/src/cli/ui/components/InkComponents.tsx +613 -0
- package/src/cli/ui/components/NovaInkApp.tsx +312 -0
- package/src/cli/ui/components/ProgressBar.ts +177 -0
- package/src/cli/ui/components/ProgressIndicator.ts +298 -0
- package/src/cli/ui/components/QuickActions.ts +396 -0
- package/src/cli/ui/components/SimpleErrorPanel.ts +231 -0
- package/src/cli/ui/components/StatusBar.ts +194 -0
- package/src/cli/ui/components/ThinkingBlockRenderer.ts +401 -0
- package/src/cli/ui/components/index.ts +27 -0
- package/src/cli/ui/ink-prototype.tsx +347 -0
- package/src/cli/utils/CliUI.ts +336 -0
- package/src/cli/utils/CompletionHelper.ts +388 -0
- package/src/cli/utils/EnhancedCompleter.test.ts +226 -0
- package/src/cli/utils/EnhancedCompleter.ts +513 -0
- package/src/cli/utils/ErrorEnhancer.ts +429 -0
- package/src/cli/utils/OutputFormatter.ts +193 -0
- package/src/cli/utils/index.ts +9 -0
- package/src/core/agents/AgentOrchestrator.ts +515 -0
- package/src/core/agents/index.ts +17 -0
- package/src/core/audit/AuditLogger.ts +509 -0
- package/src/core/audit/index.ts +11 -0
- package/src/core/auth/AuthManager.d.ts.map +1 -0
- package/src/core/auth/AuthManager.ts +138 -0
- package/src/core/auth/index.d.ts.map +1 -0
- package/src/core/auth/index.ts +2 -0
- package/src/core/config/ConfigManager.d.ts.map +1 -0
- package/src/core/config/ConfigManager.test.ts +183 -0
- package/src/core/config/ConfigManager.ts +1219 -0
- package/src/core/config/index.d.ts.map +1 -0
- package/src/core/config/index.ts +1 -0
- package/src/core/context/ContextBuilder.d.ts.map +1 -0
- package/src/core/context/ContextBuilder.ts +171 -0
- package/src/core/context/ContextCompressor.d.ts.map +1 -0
- package/src/core/context/ContextCompressor.ts +642 -0
- package/src/core/context/LayeredMemoryManager.ts +657 -0
- package/src/core/context/MemoryDiscovery.d.ts.map +1 -0
- package/src/core/context/MemoryDiscovery.ts +175 -0
- package/src/core/context/defaultSystemPrompt.d.ts.map +1 -0
- package/src/core/context/defaultSystemPrompt.ts +35 -0
- package/src/core/context/index.d.ts.map +1 -0
- package/src/core/context/index.ts +22 -0
- package/src/core/extensions/SkillGenerator.ts +421 -0
- package/src/core/extensions/SkillInstaller.d.ts.map +1 -0
- package/src/core/extensions/SkillInstaller.ts +257 -0
- package/src/core/extensions/SkillRegistry.d.ts.map +1 -0
- package/src/core/extensions/SkillRegistry.ts +361 -0
- package/src/core/extensions/SkillValidator.ts +525 -0
- package/src/core/extensions/index.ts +15 -0
- package/src/core/index.d.ts.map +1 -0
- package/src/core/index.ts +42 -0
- package/src/core/mcp/McpManager.d.ts.map +1 -0
- package/src/core/mcp/McpManager.ts +632 -0
- package/src/core/mcp/index.d.ts.map +1 -0
- package/src/core/mcp/index.ts +2 -0
- package/src/core/model/ModelClient.d.ts.map +1 -0
- package/src/core/model/ModelClient.ts +217 -0
- package/src/core/model/ModelConnectionTester.ts +363 -0
- package/src/core/model/ModelValidator.ts +348 -0
- package/src/core/model/index.d.ts.map +1 -0
- package/src/core/model/index.ts +6 -0
- package/src/core/model/providers/AnthropicProvider.d.ts.map +1 -0
- package/src/core/model/providers/AnthropicProvider.ts +279 -0
- package/src/core/model/providers/CodingPlanProvider.d.ts.map +1 -0
- package/src/core/model/providers/CodingPlanProvider.ts +210 -0
- package/src/core/model/providers/OllamaCloudProvider.d.ts.map +1 -0
- package/src/core/model/providers/OllamaCloudProvider.ts +405 -0
- package/src/core/model/providers/OllamaManager.d.ts.map +1 -0
- package/src/core/model/providers/OllamaManager.ts +201 -0
- package/src/core/model/providers/OllamaProvider.d.ts.map +1 -0
- package/src/core/model/providers/OllamaProvider.ts +73 -0
- package/src/core/model/providers/OpenAICompatibleProvider.d.ts.map +1 -0
- package/src/core/model/providers/OpenAICompatibleProvider.ts +327 -0
- package/src/core/model/providers/OpenAIProvider.d.ts.map +1 -0
- package/src/core/model/providers/OpenAIProvider.ts +29 -0
- package/src/core/model/providers/index.d.ts.map +1 -0
- package/src/core/model/providers/index.ts +12 -0
- package/src/core/model/types.d.ts.map +1 -0
- package/src/core/model/types.ts +77 -0
- package/src/core/security/ApprovalManager.d.ts.map +1 -0
- package/src/core/security/ApprovalManager.ts +174 -0
- package/src/core/security/FileFilter.d.ts.map +1 -0
- package/src/core/security/FileFilter.ts +141 -0
- package/src/core/security/HookExecutor.d.ts.map +1 -0
- package/src/core/security/HookExecutor.ts +178 -0
- package/src/core/security/SandboxExecutor.ts +447 -0
- package/src/core/security/index.d.ts.map +1 -0
- package/src/core/security/index.ts +8 -0
- package/src/core/session/AgentLoop.d.ts.map +1 -0
- package/src/core/session/AgentLoop.ts +501 -0
- package/src/core/session/SessionManager.d.ts.map +1 -0
- package/src/core/session/SessionManager.test.ts +183 -0
- package/src/core/session/SessionManager.ts +460 -0
- package/src/core/session/index.d.ts.map +1 -0
- package/src/core/session/index.ts +3 -0
- package/src/core/telemetry/Telemetry.d.ts.map +1 -0
- package/src/core/telemetry/Telemetry.ts +90 -0
- package/src/core/telemetry/TelemetryService.ts +531 -0
- package/src/core/telemetry/index.d.ts.map +1 -0
- package/src/core/telemetry/index.ts +12 -0
- package/src/core/testing/AutoFixer.ts +385 -0
- package/src/core/testing/ErrorAnalyzer.ts +499 -0
- package/src/core/testing/TestRunner.ts +265 -0
- package/src/core/testing/agent-cli-tests.ts +538 -0
- package/src/core/testing/index.ts +11 -0
- package/src/core/tools/ToolRegistry.d.ts.map +1 -0
- package/src/core/tools/ToolRegistry.test.ts +206 -0
- package/src/core/tools/ToolRegistry.ts +260 -0
- package/src/core/tools/impl/EditFileTool.d.ts.map +1 -0
- package/src/core/tools/impl/EditFileTool.ts +97 -0
- package/src/core/tools/impl/ListDirectoryTool.d.ts.map +1 -0
- package/src/core/tools/impl/ListDirectoryTool.ts +142 -0
- package/src/core/tools/impl/MemoryTool.d.ts.map +1 -0
- package/src/core/tools/impl/MemoryTool.ts +102 -0
- package/src/core/tools/impl/ReadFileTool.d.ts.map +1 -0
- package/src/core/tools/impl/ReadFileTool.ts +58 -0
- package/src/core/tools/impl/SearchContentTool.d.ts.map +1 -0
- package/src/core/tools/impl/SearchContentTool.ts +94 -0
- package/src/core/tools/impl/SearchFileTool.d.ts.map +1 -0
- package/src/core/tools/impl/SearchFileTool.ts +61 -0
- package/src/core/tools/impl/ShellTool.d.ts.map +1 -0
- package/src/core/tools/impl/ShellTool.ts +118 -0
- package/src/core/tools/impl/TaskTool.d.ts.map +1 -0
- package/src/core/tools/impl/TaskTool.ts +207 -0
- package/src/core/tools/impl/TodoTool.d.ts.map +1 -0
- package/src/core/tools/impl/TodoTool.ts +122 -0
- package/src/core/tools/impl/WebFetchTool.d.ts.map +1 -0
- package/src/core/tools/impl/WebFetchTool.ts +103 -0
- package/src/core/tools/impl/WebSearchTool.d.ts.map +1 -0
- package/src/core/tools/impl/WebSearchTool.ts +89 -0
- package/src/core/tools/impl/WriteFileTool.d.ts.map +1 -0
- package/src/core/tools/impl/WriteFileTool.ts +49 -0
- package/src/core/tools/impl/index.d.ts.map +1 -0
- package/src/core/tools/impl/index.ts +16 -0
- package/src/core/tools/index.d.ts.map +1 -0
- package/src/core/tools/index.ts +7 -0
- package/src/core/tools/schemas/execution.d.ts.map +1 -0
- package/src/core/tools/schemas/execution.ts +42 -0
- package/src/core/tools/schemas/file.d.ts.map +1 -0
- package/src/core/tools/schemas/file.ts +119 -0
- package/src/core/tools/schemas/index.d.ts.map +1 -0
- package/src/core/tools/schemas/index.ts +11 -0
- package/src/core/tools/schemas/memory.d.ts.map +1 -0
- package/src/core/tools/schemas/memory.ts +52 -0
- package/src/core/tools/schemas/orchestration.d.ts.map +1 -0
- package/src/core/tools/schemas/orchestration.ts +44 -0
- package/src/core/tools/schemas/search.d.ts.map +1 -0
- package/src/core/tools/schemas/search.ts +112 -0
- package/src/core/tools/schemas/todo.d.ts.map +1 -0
- package/src/core/tools/schemas/todo.ts +32 -0
- package/src/core/tools/schemas/web.d.ts.map +1 -0
- package/src/core/tools/schemas/web.ts +86 -0
- package/src/core/types/config.d.ts.map +1 -0
- package/src/core/types/config.ts +200 -0
- package/src/core/types/errors.d.ts.map +1 -0
- package/src/core/types/errors.ts +204 -0
- package/src/core/types/index.d.ts.map +1 -0
- package/src/core/types/index.ts +8 -0
- package/src/core/types/session.d.ts.map +1 -0
- package/src/core/types/session.ts +216 -0
- package/src/core/types/tools.d.ts.map +1 -0
- package/src/core/types/tools.ts +157 -0
- package/src/core/utils/CheckpointManager.d.ts.map +1 -0
- package/src/core/utils/CheckpointManager.ts +327 -0
- package/src/core/utils/Logger.d.ts.map +1 -0
- package/src/core/utils/Logger.ts +98 -0
- package/src/core/utils/RetryManager.ts +471 -0
- package/src/core/utils/TokenCounter.d.ts.map +1 -0
- package/src/core/utils/TokenCounter.ts +414 -0
- package/src/core/utils/VectorMemoryStore.ts +440 -0
- package/src/core/utils/helpers.d.ts.map +1 -0
- package/src/core/utils/helpers.ts +89 -0
- package/src/core/utils/index.d.ts.map +1 -0
- package/src/core/utils/index.ts +19 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ToolRegistry Tests
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
6
|
+
import { ToolRegistry } from './ToolRegistry.js';
|
|
7
|
+
import type { ToolDefinition, ToolHandler, ToolExecutionContext } from '../types/tools.js';
|
|
8
|
+
|
|
9
|
+
describe('ToolRegistry', () => {
|
|
10
|
+
let registry: ToolRegistry;
|
|
11
|
+
|
|
12
|
+
const mockToolDefinition: ToolDefinition = {
|
|
13
|
+
name: 'test_tool',
|
|
14
|
+
description: 'A test tool',
|
|
15
|
+
category: 'utility',
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
message: { type: 'string', description: 'A message' },
|
|
20
|
+
},
|
|
21
|
+
required: ['message'],
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const mockToolHandler: ToolHandler = async (input: { message: string }) => {
|
|
26
|
+
return { result: `Echo: ${input.message}` };
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const dangerousToolDefinition: ToolDefinition = {
|
|
30
|
+
name: 'dangerous_tool',
|
|
31
|
+
description: 'A dangerous tool',
|
|
32
|
+
category: 'system',
|
|
33
|
+
inputSchema: {
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: {
|
|
36
|
+
command: { type: 'string' },
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
requiresApproval: true,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const dangerousToolHandler: ToolHandler = async () => ({ success: true });
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
registry = new ToolRegistry();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('register()', () => {
|
|
49
|
+
it('should register a tool with definition and handler', () => {
|
|
50
|
+
registry.register(mockToolDefinition, mockToolHandler);
|
|
51
|
+
expect(registry.has('test_tool')).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should throw when registering duplicate tool', () => {
|
|
55
|
+
registry.register(mockToolDefinition, mockToolHandler);
|
|
56
|
+
expect(() => {
|
|
57
|
+
registry.register(mockToolDefinition, mockToolHandler);
|
|
58
|
+
}).toThrow('already registered');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('get()', () => {
|
|
63
|
+
it('should return tool entry by name', () => {
|
|
64
|
+
registry.register(mockToolDefinition, mockToolHandler);
|
|
65
|
+
const entry = registry.get('test_tool');
|
|
66
|
+
|
|
67
|
+
expect(entry).toBeDefined();
|
|
68
|
+
expect(entry?.definition.name).toBe('test_tool');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should return undefined for non-existent tool', () => {
|
|
72
|
+
const entry = registry.get('non_existent');
|
|
73
|
+
expect(entry).toBeUndefined();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('getDefinition()', () => {
|
|
78
|
+
it('should return tool definition', () => {
|
|
79
|
+
registry.register(mockToolDefinition, mockToolHandler);
|
|
80
|
+
const def = registry.getDefinition('test_tool');
|
|
81
|
+
|
|
82
|
+
expect(def).toBeDefined();
|
|
83
|
+
expect(def?.name).toBe('test_tool');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('getHandler()', () => {
|
|
88
|
+
it('should return tool handler', () => {
|
|
89
|
+
registry.register(mockToolDefinition, mockToolHandler);
|
|
90
|
+
const handler = registry.getHandler('test_tool');
|
|
91
|
+
|
|
92
|
+
expect(handler).toBeDefined();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('has()', () => {
|
|
97
|
+
it('should return true for registered tool', () => {
|
|
98
|
+
registry.register(mockToolDefinition, mockToolHandler);
|
|
99
|
+
expect(registry.has('test_tool')).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should return false for non-existent tool', () => {
|
|
103
|
+
expect(registry.has('non_existent')).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('unregister()', () => {
|
|
108
|
+
it('should remove tool from registry', () => {
|
|
109
|
+
registry.register(mockToolDefinition, mockToolHandler);
|
|
110
|
+
expect(registry.has('test_tool')).toBe(true);
|
|
111
|
+
|
|
112
|
+
registry.unregister('test_tool');
|
|
113
|
+
expect(registry.has('test_tool')).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should return false for non-existent tool', () => {
|
|
117
|
+
expect(registry.unregister('non_existent')).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('enable() and disable()', () => {
|
|
122
|
+
it('should disable a tool', () => {
|
|
123
|
+
registry.register(mockToolDefinition, mockToolHandler);
|
|
124
|
+
registry.disable('test_tool');
|
|
125
|
+
|
|
126
|
+
expect(registry.isEnabled('test_tool')).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should re-enable a disabled tool', () => {
|
|
130
|
+
registry.register(mockToolDefinition, mockToolHandler);
|
|
131
|
+
registry.disable('test_tool');
|
|
132
|
+
registry.enable('test_tool');
|
|
133
|
+
|
|
134
|
+
expect(registry.isEnabled('test_tool')).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('getToolNames()', () => {
|
|
139
|
+
it('should return empty array when no tools registered', () => {
|
|
140
|
+
expect(registry.getToolNames()).toHaveLength(0);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should return all registered tool names', () => {
|
|
144
|
+
registry.register(mockToolDefinition, mockToolHandler);
|
|
145
|
+
registry.register(dangerousToolDefinition, dangerousToolHandler);
|
|
146
|
+
|
|
147
|
+
const names = registry.getToolNames();
|
|
148
|
+
expect(names).toHaveLength(2);
|
|
149
|
+
expect(names).toContain('test_tool');
|
|
150
|
+
expect(names).toContain('dangerous_tool');
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('getEnabledToolNames()', () => {
|
|
155
|
+
it('should return only enabled tools', () => {
|
|
156
|
+
registry.register(mockToolDefinition, mockToolHandler);
|
|
157
|
+
registry.register(dangerousToolDefinition, dangerousToolHandler);
|
|
158
|
+
registry.disable('dangerous_tool');
|
|
159
|
+
|
|
160
|
+
const names = registry.getEnabledToolNames();
|
|
161
|
+
expect(names).toHaveLength(1);
|
|
162
|
+
expect(names).toContain('test_tool');
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('getAllDefinitions()', () => {
|
|
167
|
+
it('should return definitions for all enabled tools', () => {
|
|
168
|
+
registry.register(mockToolDefinition, mockToolHandler);
|
|
169
|
+
|
|
170
|
+
const defs = registry.getAllDefinitions();
|
|
171
|
+
expect(defs.length).toBeGreaterThan(0);
|
|
172
|
+
expect(defs[0].name).toBe('test_tool');
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('requiresApproval()', () => {
|
|
177
|
+
it('should return false for tool without approval requirement in plan mode', () => {
|
|
178
|
+
registry.register(mockToolDefinition, mockToolHandler);
|
|
179
|
+
// ToolRegistry must be accessed after registration
|
|
180
|
+
const result = registry.requiresApproval('test_tool', {}, 'plan');
|
|
181
|
+
expect(result).toBe(false);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should return true for tool with approval requirement in plan mode', () => {
|
|
185
|
+
registry.register(dangerousToolDefinition, dangerousToolHandler);
|
|
186
|
+
expect(registry.requiresApproval('dangerous_tool', {}, 'plan')).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should return false for all tools in yolo mode', () => {
|
|
190
|
+
registry.register(dangerousToolDefinition, dangerousToolHandler);
|
|
191
|
+
expect(registry.requiresApproval('dangerous_tool', {}, 'yolo')).toBe(false);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('unregister()', () => {
|
|
196
|
+
it('should remove all tools when called for each', () => {
|
|
197
|
+
registry.register(mockToolDefinition, mockToolHandler);
|
|
198
|
+
registry.register(dangerousToolDefinition, dangerousToolHandler);
|
|
199
|
+
|
|
200
|
+
registry.unregister('test_tool');
|
|
201
|
+
registry.unregister('dangerous_tool');
|
|
202
|
+
|
|
203
|
+
expect(registry.getToolNames()).toHaveLength(0);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ToolRegistry - Central registry for tool management
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
ToolDefinition,
|
|
7
|
+
ToolHandler,
|
|
8
|
+
ToolRegistryEntry,
|
|
9
|
+
ToolCategory,
|
|
10
|
+
ToolHandlerInput,
|
|
11
|
+
ToolHandlerOutput,
|
|
12
|
+
BuiltinToolName,
|
|
13
|
+
} from '../types/tools.js';
|
|
14
|
+
import type { ApprovalMode } from '../types/session.js';
|
|
15
|
+
import { ToolError, ToolValidationError } from '../types/errors.js';
|
|
16
|
+
import { BUILTIN_TOOLS } from '../types/tools.js';
|
|
17
|
+
|
|
18
|
+
export class ToolRegistry {
|
|
19
|
+
private tools = new Map<string, ToolRegistryEntry>();
|
|
20
|
+
private categories = new Map<ToolCategory, Set<string>>();
|
|
21
|
+
|
|
22
|
+
/** Register a tool */
|
|
23
|
+
register(definition: ToolDefinition, handler: ToolHandler): void {
|
|
24
|
+
if (this.tools.has(definition.name)) {
|
|
25
|
+
throw new ToolError(`Tool "${definition.name}" is already registered`, definition.name);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
this.tools.set(definition.name, {
|
|
29
|
+
definition,
|
|
30
|
+
handler,
|
|
31
|
+
enabled: true,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Add to category index
|
|
35
|
+
const cat = definition.category;
|
|
36
|
+
if (!this.categories.has(cat)) {
|
|
37
|
+
this.categories.set(cat, new Set());
|
|
38
|
+
}
|
|
39
|
+
this.categories.get(cat)!.add(definition.name);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Unregister a tool by name */
|
|
43
|
+
unregister(name: string): boolean {
|
|
44
|
+
const entry = this.tools.get(name);
|
|
45
|
+
if (!entry) return false;
|
|
46
|
+
|
|
47
|
+
this.tools.delete(name);
|
|
48
|
+
this.categories.get(entry.definition.category)?.delete(name);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Get a tool entry by name */
|
|
53
|
+
get(name: string): ToolRegistryEntry | undefined {
|
|
54
|
+
return this.tools.get(name);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Get a tool's definition */
|
|
58
|
+
getDefinition(name: string): ToolDefinition | undefined {
|
|
59
|
+
return this.tools.get(name)?.definition;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Get a tool's handler */
|
|
63
|
+
getHandler(name: string): ToolHandler | undefined {
|
|
64
|
+
return this.tools.get(name)?.handler;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Check if a tool exists */
|
|
68
|
+
has(name: string): boolean {
|
|
69
|
+
return this.tools.has(name);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Enable a tool */
|
|
73
|
+
enable(name: string): void {
|
|
74
|
+
const entry = this.tools.get(name);
|
|
75
|
+
if (entry) entry.enabled = true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Disable a tool */
|
|
79
|
+
disable(name: string): void {
|
|
80
|
+
const entry = this.tools.get(name);
|
|
81
|
+
if (entry) entry.enabled = false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Check if a tool is enabled */
|
|
85
|
+
isEnabled(name: string): boolean {
|
|
86
|
+
return this.tools.get(name)?.enabled ?? false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Get all tool names */
|
|
90
|
+
getToolNames(): string[] {
|
|
91
|
+
return Array.from(this.tools.keys());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Get all enabled tool names */
|
|
95
|
+
getEnabledToolNames(): string[] {
|
|
96
|
+
return Array.from(this.tools.entries())
|
|
97
|
+
.filter(([, entry]) => entry.enabled)
|
|
98
|
+
.map(([name]) => name);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Get tools by category */
|
|
102
|
+
getToolsByCategory(category: ToolCategory): ToolRegistryEntry[] {
|
|
103
|
+
const names = this.categories.get(category);
|
|
104
|
+
if (!names) return [];
|
|
105
|
+
return Array.from(names)
|
|
106
|
+
.map((name) => this.tools.get(name)!)
|
|
107
|
+
.filter(Boolean);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Get all tool definitions (for LLM schema) */
|
|
111
|
+
getAllDefinitions(): ToolDefinition[] {
|
|
112
|
+
return Array.from(this.tools.values())
|
|
113
|
+
.filter((entry) => entry.enabled)
|
|
114
|
+
.map((entry) => entry.definition);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Get tool definitions in Anthropic tool use format */
|
|
118
|
+
getAnthropicToolDefinitions(): Array<{
|
|
119
|
+
name: string;
|
|
120
|
+
description: string;
|
|
121
|
+
input_schema: Record<string, unknown>;
|
|
122
|
+
}> {
|
|
123
|
+
return this.getAllDefinitions().map((def) => ({
|
|
124
|
+
name: def.name,
|
|
125
|
+
description: def.description,
|
|
126
|
+
input_schema: def.inputSchema,
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Get tool definitions in OpenAI function calling format */
|
|
131
|
+
getOpenAIToolDefinitions(): Array<{
|
|
132
|
+
type: 'function';
|
|
133
|
+
function: {
|
|
134
|
+
name: string;
|
|
135
|
+
description: string;
|
|
136
|
+
parameters: Record<string, unknown>;
|
|
137
|
+
};
|
|
138
|
+
}> {
|
|
139
|
+
return this.getAllDefinitions().map((def) => ({
|
|
140
|
+
type: 'function' as const,
|
|
141
|
+
function: {
|
|
142
|
+
name: def.name,
|
|
143
|
+
description: def.description,
|
|
144
|
+
parameters: def.inputSchema,
|
|
145
|
+
},
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Check if a tool requires approval for given input and mode */
|
|
150
|
+
requiresApproval(name: string, input: Record<string, unknown>, mode: ApprovalMode): boolean {
|
|
151
|
+
const entry = this.tools.get(name);
|
|
152
|
+
if (!entry) return false;
|
|
153
|
+
|
|
154
|
+
// yolo mode: never require approval
|
|
155
|
+
if (mode === 'yolo') return false;
|
|
156
|
+
// accepting_edits: never require approval
|
|
157
|
+
if (mode === 'accepting_edits') return false;
|
|
158
|
+
|
|
159
|
+
const { requiresApproval } = entry.definition;
|
|
160
|
+
if (requiresApproval === undefined) return false;
|
|
161
|
+
if (typeof requiresApproval === 'boolean') return requiresApproval;
|
|
162
|
+
return requiresApproval(input, mode);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Get the risk level of a tool */
|
|
166
|
+
getRiskLevel(name: string): 'low' | 'medium' | 'high' | 'critical' {
|
|
167
|
+
return this.tools.get(name)?.definition.riskLevel ?? 'medium';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Validate tool input against its schema */
|
|
171
|
+
validateInput(name: string, input: Record<string, unknown>): { valid: true } | { valid: false; errors: Array<{ field: string; message: string }> } {
|
|
172
|
+
const entry = this.tools.get(name);
|
|
173
|
+
if (!entry) {
|
|
174
|
+
return { valid: false, errors: [{ field: '_', message: `Tool "${name}" not found` }] };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const schema = entry.definition.inputSchema;
|
|
178
|
+
// Basic validation against JSON Schema properties
|
|
179
|
+
const errors: Array<{ field: string; message: string }> = [];
|
|
180
|
+
const properties = (schema.properties || {}) as Record<string, Record<string, unknown>>;
|
|
181
|
+
const required = (schema.required || []) as string[];
|
|
182
|
+
|
|
183
|
+
// Check required fields
|
|
184
|
+
for (const fieldName of required) {
|
|
185
|
+
if (input[fieldName] === undefined || input[fieldName] === null) {
|
|
186
|
+
errors.push({ field: fieldName, message: `Required field "${fieldName}" is missing` });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Check property types
|
|
191
|
+
for (const [fieldName, value] of Object.entries(input)) {
|
|
192
|
+
const propSchema = properties[fieldName];
|
|
193
|
+
if (!propSchema) continue;
|
|
194
|
+
|
|
195
|
+
const expectedType = propSchema.type as string | string[];
|
|
196
|
+
const actualType = Array.isArray(value) ? 'array' : typeof value;
|
|
197
|
+
|
|
198
|
+
if (Array.isArray(expectedType)) {
|
|
199
|
+
if (!expectedType.includes(actualType)) {
|
|
200
|
+
errors.push({
|
|
201
|
+
field: fieldName,
|
|
202
|
+
message: `Expected type ${expectedType.join('|')}, got ${actualType}`,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
} else if (expectedType && expectedType !== actualType) {
|
|
206
|
+
errors.push({
|
|
207
|
+
field: fieldName,
|
|
208
|
+
message: `Expected type ${expectedType}, got ${actualType}`,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return errors.length === 0 ? { valid: true } : { valid: false, errors };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Execute a tool with the given input */
|
|
217
|
+
async execute(
|
|
218
|
+
name: string,
|
|
219
|
+
input: ToolHandlerInput
|
|
220
|
+
): Promise<ToolHandlerOutput> {
|
|
221
|
+
const entry = this.tools.get(name);
|
|
222
|
+
if (!entry) {
|
|
223
|
+
throw new ToolError(`Tool "${name}" not found`, name);
|
|
224
|
+
}
|
|
225
|
+
if (!entry.enabled) {
|
|
226
|
+
throw new ToolError(`Tool "${name}" is disabled`, name);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Validate input
|
|
230
|
+
const validation = this.validateInput(name, input.params);
|
|
231
|
+
if (!validation.valid) {
|
|
232
|
+
const errors = 'errors' in validation ? validation.errors : [];
|
|
233
|
+
throw new ToolValidationError(
|
|
234
|
+
`Invalid input for tool "${name}"`,
|
|
235
|
+
name,
|
|
236
|
+
errors
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return entry.handler(input);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Get all builtin tool names */
|
|
244
|
+
getBuiltinToolNames(): BuiltinToolName[] {
|
|
245
|
+
return Object.values(BUILTIN_TOOLS);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Get statistics about registered tools */
|
|
249
|
+
getStats(): { total: number; enabled: number; byCategory: Record<string, number> } {
|
|
250
|
+
const byCategory: Record<string, number> = {};
|
|
251
|
+
for (const [cat, names] of this.categories) {
|
|
252
|
+
byCategory[cat] = names.size;
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
total: this.tools.size,
|
|
256
|
+
enabled: this.getEnabledToolNames().length,
|
|
257
|
+
byCategory,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EditFileTool.d.ts","sourceRoot":"","sources":["EditFileTool.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAuC,MAAM,sBAAsB,CAAC;AAG7F,eAAO,MAAM,eAAe,EAAE,WAwE7B,CAAC"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import type { ToolHandler, ToolHandlerInput, ToolHandlerOutput } from '../../types/tools.js';
|
|
4
|
+
import { ToolError } from '../../types/errors.js';
|
|
5
|
+
|
|
6
|
+
export const editFileHandler: ToolHandler = async (input: ToolHandlerInput): Promise<ToolHandlerOutput> => {
|
|
7
|
+
const { filePath, oldText, newText, allOccurrences = false, dryRun = false } = input.params as {
|
|
8
|
+
filePath: string;
|
|
9
|
+
oldText: string;
|
|
10
|
+
newText: string;
|
|
11
|
+
allOccurrences?: boolean;
|
|
12
|
+
dryRun?: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const resolvedPath = path.resolve(filePath);
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const content = await fs.readFile(resolvedPath, 'utf-8');
|
|
19
|
+
|
|
20
|
+
// Check if oldText exists
|
|
21
|
+
if (!content.includes(oldText)) {
|
|
22
|
+
throw new ToolError(
|
|
23
|
+
`The specified text was not found in the file. Make sure oldText matches exactly (including whitespace).`,
|
|
24
|
+
'edit_file'
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Count occurrences
|
|
29
|
+
const occurrences = content.split(oldText).length - 1;
|
|
30
|
+
|
|
31
|
+
if (occurrences > 1 && !allOccurrences) {
|
|
32
|
+
throw new ToolError(
|
|
33
|
+
`The text appears ${occurrences} times in the file. Set allOccurrences=true to replace all, or make oldText more specific to match only once.`,
|
|
34
|
+
'edit_file',
|
|
35
|
+
undefined,
|
|
36
|
+
{ occurrences }
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let newContent: string;
|
|
41
|
+
if (allOccurrences) {
|
|
42
|
+
newContent = content.split(oldText).join(newText);
|
|
43
|
+
} else {
|
|
44
|
+
newContent = content.replace(oldText, newText);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (dryRun) {
|
|
48
|
+
// Show a unified diff-like preview
|
|
49
|
+
const oldLines = content.split('\n');
|
|
50
|
+
const newLines = newContent.split('\n');
|
|
51
|
+
const preview = generateDiffPreview(oldLines, newLines);
|
|
52
|
+
return {
|
|
53
|
+
content: `Dry run preview for ${resolvedPath}:\n${preview}\n\n${occurrences} occurrence(s) would be replaced.`,
|
|
54
|
+
metadata: { dryRun: true, occurrences, path: resolvedPath },
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
await fs.writeFile(resolvedPath, newContent, 'utf-8');
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
content: `Successfully replaced ${occurrences} occurrence(s) in ${resolvedPath}`,
|
|
62
|
+
metadata: {
|
|
63
|
+
path: resolvedPath,
|
|
64
|
+
occurrences,
|
|
65
|
+
oldSize: Buffer.byteLength(content, 'utf-8'),
|
|
66
|
+
newSize: Buffer.byteLength(newContent, 'utf-8'),
|
|
67
|
+
},
|
|
68
|
+
filesAffected: [resolvedPath],
|
|
69
|
+
};
|
|
70
|
+
} catch (err) {
|
|
71
|
+
if (err instanceof ToolError) throw err;
|
|
72
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
73
|
+
if (code === 'ENOENT') {
|
|
74
|
+
throw new ToolError(`File not found: ${filePath}`, 'edit_file', undefined, { code });
|
|
75
|
+
}
|
|
76
|
+
throw new ToolError(`Failed to edit file: ${(err as Error).message}`, 'edit_file');
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
function generateDiffPreview(oldLines: string[], newLines: string[]): string {
|
|
81
|
+
const result: string[] = [];
|
|
82
|
+
const maxLines = Math.max(oldLines.length, newLines.length);
|
|
83
|
+
let shown = 0;
|
|
84
|
+
|
|
85
|
+
for (let i = 0; i < maxLines && shown < 20; i++) {
|
|
86
|
+
const oldLine = oldLines[i];
|
|
87
|
+
const newLine = newLines[i];
|
|
88
|
+
if (oldLine === newLine) continue;
|
|
89
|
+
if (oldLine !== undefined) result.push(`- ${oldLine}`);
|
|
90
|
+
if (newLine !== undefined) result.push(`+ ${newLine}`);
|
|
91
|
+
shown++;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (shown >= 20) result.push('... (truncated)');
|
|
95
|
+
|
|
96
|
+
return result.join('\n');
|
|
97
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ListDirectoryTool.d.ts","sourceRoot":"","sources":["ListDirectoryTool.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAuC,MAAM,sBAAsB,CAAC;AAM7F,eAAO,MAAM,oBAAoB,EAAE,WA4HlC,CAAC"}
|