thatgfsj-code 0.3.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.
- package/LICENSE +21 -0
- package/README.md +136 -0
- package/ROADMAP.md +61 -0
- package/dist/app/agent.d.ts +31 -0
- package/dist/app/agent.d.ts.map +1 -0
- package/dist/app/agent.js +106 -0
- package/dist/app/agent.js.map +1 -0
- package/dist/app/index.d.ts +39 -0
- package/dist/app/index.d.ts.map +1 -0
- package/dist/app/index.js +80 -0
- package/dist/app/index.js.map +1 -0
- package/dist/cmd/index.d.ts +12 -0
- package/dist/cmd/index.d.ts.map +1 -0
- package/dist/cmd/index.js +85 -0
- package/dist/cmd/index.js.map +1 -0
- package/dist/config/index.d.ts +39 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +110 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/providers.d.ts +23 -0
- package/dist/config/providers.d.ts.map +1 -0
- package/dist/config/providers.js +152 -0
- package/dist/config/providers.js.map +1 -0
- package/dist/config/types.d.ts +33 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +5 -0
- package/dist/config/types.js.map +1 -0
- package/dist/hooks/index.d.ts +31 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +71 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/llm/anthropic.d.ts +17 -0
- package/dist/llm/anthropic.d.ts.map +1 -0
- package/dist/llm/anthropic.js +126 -0
- package/dist/llm/anthropic.js.map +1 -0
- package/dist/llm/gemini.d.ts +16 -0
- package/dist/llm/gemini.d.ts.map +1 -0
- package/dist/llm/gemini.js +107 -0
- package/dist/llm/gemini.js.map +1 -0
- package/dist/llm/index.d.ts +48 -0
- package/dist/llm/index.d.ts.map +1 -0
- package/dist/llm/index.js +174 -0
- package/dist/llm/index.js.map +1 -0
- package/dist/llm/openai.d.ts +17 -0
- package/dist/llm/openai.d.ts.map +1 -0
- package/dist/llm/openai.js +113 -0
- package/dist/llm/openai.js.map +1 -0
- package/dist/llm/provider.d.ts +33 -0
- package/dist/llm/provider.d.ts.map +1 -0
- package/dist/llm/provider.js +6 -0
- package/dist/llm/provider.js.map +1 -0
- package/dist/mcp/client.d.ts +83 -0
- package/dist/mcp/client.d.ts.map +1 -0
- package/dist/mcp/client.js +267 -0
- package/dist/mcp/client.js.map +1 -0
- package/dist/prompts/index.d.ts +27 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +102 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/session/compactor.d.ts +48 -0
- package/dist/session/compactor.d.ts.map +1 -0
- package/dist/session/compactor.js +77 -0
- package/dist/session/compactor.js.map +1 -0
- package/dist/session/index.d.ts +48 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +69 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/message.d.ts +26 -0
- package/dist/session/message.d.ts.map +1 -0
- package/dist/session/message.js +34 -0
- package/dist/session/message.js.map +1 -0
- package/dist/tools/file.d.ts +46 -0
- package/dist/tools/file.d.ts.map +1 -0
- package/dist/tools/file.js +100 -0
- package/dist/tools/file.js.map +1 -0
- package/dist/tools/git.d.ts +52 -0
- package/dist/tools/git.d.ts.map +1 -0
- package/dist/tools/git.js +112 -0
- package/dist/tools/git.js.map +1 -0
- package/dist/tools/index.d.ts +42 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +92 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/search.d.ts +36 -0
- package/dist/tools/search.d.ts.map +1 -0
- package/dist/tools/search.js +223 -0
- package/dist/tools/search.js.map +1 -0
- package/dist/tools/shell.d.ts +50 -0
- package/dist/tools/shell.d.ts.map +1 -0
- package/dist/tools/shell.js +117 -0
- package/dist/tools/shell.js.map +1 -0
- package/dist/tools/types.d.ts +68 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +57 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/tui/index.d.ts +8 -0
- package/dist/tui/index.d.ts.map +1 -0
- package/dist/tui/index.js +8 -0
- package/dist/tui/index.js.map +1 -0
- package/dist/tui/input.d.ts +14 -0
- package/dist/tui/input.d.ts.map +1 -0
- package/dist/tui/input.js +50 -0
- package/dist/tui/input.js.map +1 -0
- package/dist/tui/output.d.ts +20 -0
- package/dist/tui/output.d.ts.map +1 -0
- package/dist/tui/output.js +70 -0
- package/dist/tui/output.js.map +1 -0
- package/dist/tui/repl.d.ts +25 -0
- package/dist/tui/repl.d.ts.map +1 -0
- package/dist/tui/repl.js +114 -0
- package/dist/tui/repl.js.map +1 -0
- package/dist/tui/welcome.d.ts +15 -0
- package/dist/tui/welcome.d.ts.map +1 -0
- package/dist/tui/welcome.js +86 -0
- package/dist/tui/welcome.js.map +1 -0
- package/dist/types.d.ts +38 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/diff.d.ts +22 -0
- package/dist/utils/diff.d.ts.map +1 -0
- package/dist/utils/diff.js +60 -0
- package/dist/utils/diff.js.map +1 -0
- package/dist/utils/project.d.ts +32 -0
- package/dist/utils/project.d.ts.map +1 -0
- package/dist/utils/project.js +89 -0
- package/dist/utils/project.js.map +1 -0
- package/docs/API_KEY_GUIDE.md +236 -0
- package/docs/FAQ.md +182 -0
- package/install.bat +63 -0
- package/install.ps1 +238 -0
- package/install.sh +113 -0
- package/package.json +36 -0
- package/src/app/agent.ts +140 -0
- package/src/app/index.ts +101 -0
- package/src/cmd/index.ts +96 -0
- package/src/config/index.ts +130 -0
- package/src/config/providers.ts +160 -0
- package/src/config/types.ts +46 -0
- package/src/hooks/index.ts +111 -0
- package/src/llm/anthropic.ts +146 -0
- package/src/llm/gemini.ts +127 -0
- package/src/llm/index.ts +209 -0
- package/src/llm/openai.ts +132 -0
- package/src/llm/provider.ts +38 -0
- package/src/mcp/client.ts +330 -0
- package/src/prompts/index.ts +123 -0
- package/src/session/compactor.ts +103 -0
- package/src/session/index.ts +81 -0
- package/src/session/message.ts +42 -0
- package/src/tools/file.ts +117 -0
- package/src/tools/git.ts +132 -0
- package/src/tools/index.ts +108 -0
- package/src/tools/search.ts +263 -0
- package/src/tools/shell.ts +136 -0
- package/src/tools/types.ts +122 -0
- package/src/tui/index.ts +8 -0
- package/src/tui/input.ts +56 -0
- package/src/tui/output.ts +83 -0
- package/src/tui/repl.ts +131 -0
- package/src/tui/welcome.ts +101 -0
- package/src/types.ts +42 -0
- package/src/utils/diff.ts +71 -0
- package/src/utils/project.ts +99 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Registry - Manages available tools
|
|
3
|
+
* Migrated from old src/core/tool-registry.ts + src/tools/index.ts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Tool, ToolResult, ToolContext, ToolInputSchema } from './types.js';
|
|
7
|
+
import { FileTool } from './file.js';
|
|
8
|
+
import { ShellTool } from './shell.js';
|
|
9
|
+
import { GitTool } from './git.js';
|
|
10
|
+
import { SearchTool } from './search.js';
|
|
11
|
+
|
|
12
|
+
export class ToolRegistry {
|
|
13
|
+
private tools: Map<string, Tool> = new Map();
|
|
14
|
+
private context: ToolContext = {};
|
|
15
|
+
|
|
16
|
+
constructor() {
|
|
17
|
+
this.registerDefaults();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Register default built-in tools
|
|
22
|
+
*/
|
|
23
|
+
private registerDefaults(): void {
|
|
24
|
+
this.register(new FileTool());
|
|
25
|
+
this.register(new ShellTool());
|
|
26
|
+
this.register(new GitTool());
|
|
27
|
+
this.register(new SearchTool());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
setContext(ctx: Partial<ToolContext>): void {
|
|
31
|
+
this.context = { ...this.context, ...ctx };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
register(tool: Tool): void {
|
|
35
|
+
this.tools.set(tool.name, tool);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
unregister(name: string): boolean {
|
|
39
|
+
return this.tools.delete(name);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get(name: string): Tool | undefined {
|
|
43
|
+
return this.tools.get(name);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
list(): Tool[] {
|
|
47
|
+
return [...this.tools.values()];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
listNames(): string[] {
|
|
51
|
+
return [...this.tools.keys()];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get tools formatted for AI API
|
|
56
|
+
*/
|
|
57
|
+
getToolsForAPI(): Array<{ type: 'function'; function: any }> {
|
|
58
|
+
return [...this.tools.values()].map(tool => ({
|
|
59
|
+
type: 'function' as const,
|
|
60
|
+
function: {
|
|
61
|
+
name: tool.name,
|
|
62
|
+
description: tool.description,
|
|
63
|
+
parameters: tool.inputSchema || this.buildInputSchema(tool),
|
|
64
|
+
},
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Build JSON Schema from legacy parameters array
|
|
70
|
+
*/
|
|
71
|
+
private buildInputSchema(tool: Tool): ToolInputSchema {
|
|
72
|
+
return {
|
|
73
|
+
type: 'object',
|
|
74
|
+
properties: tool.parameters.reduce((acc, p) => {
|
|
75
|
+
acc[p.name] = {
|
|
76
|
+
type: p.type === 'number' ? 'number' : p.type === 'boolean' ? 'boolean' : 'string',
|
|
77
|
+
description: p.description,
|
|
78
|
+
};
|
|
79
|
+
return acc;
|
|
80
|
+
}, {} as Record<string, { type: string; description: string }>),
|
|
81
|
+
required: tool.parameters.filter(p => p.required).map(p => p.name),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Execute a tool by name
|
|
87
|
+
*/
|
|
88
|
+
async execute(name: string, params: Record<string, any>): Promise<ToolResult> {
|
|
89
|
+
const tool = this.tools.get(name);
|
|
90
|
+
if (!tool) {
|
|
91
|
+
return { success: false, error: `Tool "${name}" not found` };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
return await tool.execute(params, this.context);
|
|
96
|
+
} catch (error: any) {
|
|
97
|
+
return { success: false, error: error.message };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Re-export types and tools
|
|
103
|
+
export type { Tool, ToolResult, ToolContext, ToolParameter, ToolInputSchema, ToolMetadata } from './types.js';
|
|
104
|
+
export { ToolBuilder } from './types.js';
|
|
105
|
+
export { FileTool } from './file.js';
|
|
106
|
+
export { ShellTool } from './shell.js';
|
|
107
|
+
export { GitTool } from './git.js';
|
|
108
|
+
export { SearchTool } from './search.js';
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search Tool - Code search and file operations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Tool, ToolResult } from './types.js';
|
|
6
|
+
import { exec } from 'child_process';
|
|
7
|
+
import { promisify } from 'util';
|
|
8
|
+
import { readdirSync, statSync, readFileSync } from 'fs';
|
|
9
|
+
import { join, relative } from 'path';
|
|
10
|
+
|
|
11
|
+
const execAsync = promisify(exec);
|
|
12
|
+
|
|
13
|
+
export class SearchTool implements Tool {
|
|
14
|
+
name = 'search';
|
|
15
|
+
description = 'Search and find: grep, find files, list directory tree';
|
|
16
|
+
|
|
17
|
+
parameters = [
|
|
18
|
+
{ name: 'action', type: 'string', description: 'Action: grep, find, tree, files', required: true },
|
|
19
|
+
{ name: 'pattern', type: 'string', description: 'Search pattern or file pattern', required: false },
|
|
20
|
+
{ name: 'path', type: 'string', description: 'Directory to search in', required: false },
|
|
21
|
+
{ name: 'options', type: 'string', description: 'Additional options', required: false }
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
async execute(params: Record<string, any>): Promise<ToolResult> {
|
|
25
|
+
const { action, pattern, path, options } = params;
|
|
26
|
+
const workDir = path || process.cwd();
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
switch (action) {
|
|
30
|
+
case 'grep':
|
|
31
|
+
case 'search':
|
|
32
|
+
return await this.grep(pattern || '', workDir, options || '');
|
|
33
|
+
|
|
34
|
+
case 'find':
|
|
35
|
+
return await this.find(pattern || '*', workDir);
|
|
36
|
+
|
|
37
|
+
case 'tree':
|
|
38
|
+
return await this.tree(workDir, parseInt(options) || 3);
|
|
39
|
+
|
|
40
|
+
case 'files':
|
|
41
|
+
return await this.listFiles(workDir, pattern || '*');
|
|
42
|
+
|
|
43
|
+
default:
|
|
44
|
+
return { success: false, error: `Unknown action: ${action}` };
|
|
45
|
+
}
|
|
46
|
+
} catch (error: any) {
|
|
47
|
+
return { success: false, error: error.message };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Grep - search for pattern in files
|
|
53
|
+
*/
|
|
54
|
+
private async grep(pattern: string, path: string, options: string): Promise<ToolResult> {
|
|
55
|
+
if (!pattern) {
|
|
56
|
+
return { success: false, error: 'Pattern required' };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Build grep command
|
|
60
|
+
let cmd = `grep -rn "${pattern}" "${path}"`;
|
|
61
|
+
|
|
62
|
+
if (options?.includes('i')) cmd += ' -i'; // Case insensitive
|
|
63
|
+
if (options?.includes('w')) cmd += ' -w'; // Whole word
|
|
64
|
+
if (options?.includes('l')) cmd += ' -l'; // Files only
|
|
65
|
+
if (options?.includes('n')) cmd += ' -n'; // Line numbers
|
|
66
|
+
|
|
67
|
+
cmd += ' --color=never';
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const { stdout, stderr } = await execAsync(cmd, { timeout: 30000 });
|
|
71
|
+
|
|
72
|
+
if (!stdout && stderr) {
|
|
73
|
+
return { success: false, error: stderr };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const lines = (stdout || '').split('\n').filter(l => l.trim());
|
|
77
|
+
|
|
78
|
+
if (lines.length === 0) {
|
|
79
|
+
return { success: true, output: 'No matches found' };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Limit output
|
|
83
|
+
const limited = lines.slice(0, 50);
|
|
84
|
+
const output = limited.join('\n');
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
success: true,
|
|
88
|
+
output: lines.length > 50
|
|
89
|
+
? output + `\n... and ${lines.length - 50} more matches`
|
|
90
|
+
: output
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
} catch (error: any) {
|
|
94
|
+
if (error.killed) {
|
|
95
|
+
return { success: false, error: 'Search timed out (>30s)' };
|
|
96
|
+
}
|
|
97
|
+
return { success: false, error: error.message };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Find - find files by pattern
|
|
103
|
+
*/
|
|
104
|
+
private async find(pattern: string, path: string): Promise<ToolResult> {
|
|
105
|
+
if (!pattern) {
|
|
106
|
+
pattern = '*';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const results: string[] = [];
|
|
110
|
+
|
|
111
|
+
const search = (dir: string, depth: number) => {
|
|
112
|
+
if (depth > 5) return; // Max depth
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const items = readdirSync(dir);
|
|
116
|
+
|
|
117
|
+
for (const item of items) {
|
|
118
|
+
// Skip common ignored directories
|
|
119
|
+
if (item === 'node_modules' || item === '.git' || item === 'dist' || item === 'build') {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const fullPath = join(dir, item);
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const stat = statSync(fullPath);
|
|
127
|
+
|
|
128
|
+
if (stat.isDirectory()) {
|
|
129
|
+
// Check if matches pattern
|
|
130
|
+
if (this.matchPattern(item, pattern)) {
|
|
131
|
+
results.push(fullPath);
|
|
132
|
+
}
|
|
133
|
+
search(fullPath, depth + 1);
|
|
134
|
+
} else if (stat.isFile()) {
|
|
135
|
+
if (this.matchPattern(item, pattern)) {
|
|
136
|
+
results.push(fullPath);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
// Skip inaccessible files
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
// Skip inaccessible directories
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
search(path, 0);
|
|
149
|
+
|
|
150
|
+
if (results.length === 0) {
|
|
151
|
+
return { success: true, output: 'No files found' };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const limited = results.slice(0, 100);
|
|
155
|
+
const output = limited.join('\n');
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
success: true,
|
|
159
|
+
output: results.length > 100
|
|
160
|
+
? output + `\n... and ${results.length - 100} more files`
|
|
161
|
+
: output
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Tree - show directory structure
|
|
167
|
+
*/
|
|
168
|
+
private async tree(path: string, maxDepth: number): Promise<ToolResult> {
|
|
169
|
+
const lines: string[] = [];
|
|
170
|
+
|
|
171
|
+
const walk = (dir: string, prefix: string, depth: number) => {
|
|
172
|
+
if (depth > maxDepth) return;
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const items = readdirSync(dir).filter(i =>
|
|
176
|
+
!i.startsWith('.') && i !== 'node_modules' && i !== 'dist'
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
for (let i = 0; i < items.length; i++) {
|
|
180
|
+
const item = items[i];
|
|
181
|
+
const fullPath = join(dir, item);
|
|
182
|
+
const isLast = i === items.length - 1;
|
|
183
|
+
const connector = isLast ? '└── ' : '├── ';
|
|
184
|
+
|
|
185
|
+
lines.push(prefix + connector + item);
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const stat = statSync(fullPath);
|
|
189
|
+
if (stat.isDirectory()) {
|
|
190
|
+
const newPrefix = prefix + (isLast ? ' ' : '│ ');
|
|
191
|
+
walk(fullPath, newPrefix, depth + 1);
|
|
192
|
+
}
|
|
193
|
+
} catch {
|
|
194
|
+
// Skip
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} catch {
|
|
198
|
+
// Skip inaccessible
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
lines.push(relative(process.cwd(), path) || '.');
|
|
203
|
+
walk(path, '', 0);
|
|
204
|
+
|
|
205
|
+
return { success: true, output: lines.join('\n') };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* List files in directory
|
|
210
|
+
*/
|
|
211
|
+
private async listFiles(path: string, pattern: string): Promise<ToolResult> {
|
|
212
|
+
const files: string[] = [];
|
|
213
|
+
|
|
214
|
+
const scan = (dir: string) => {
|
|
215
|
+
try {
|
|
216
|
+
const items = readdirSync(dir);
|
|
217
|
+
|
|
218
|
+
for (const item of items) {
|
|
219
|
+
if (item === 'node_modules' || item === '.git') continue;
|
|
220
|
+
|
|
221
|
+
const fullPath = join(dir, item);
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
const stat = statSync(fullPath);
|
|
225
|
+
if (stat.isFile() && this.matchPattern(item, pattern)) {
|
|
226
|
+
files.push(relative(process.cwd(), fullPath));
|
|
227
|
+
}
|
|
228
|
+
} catch {
|
|
229
|
+
// Skip
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
} catch {
|
|
233
|
+
// Skip
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
scan(path);
|
|
238
|
+
|
|
239
|
+
if (files.length === 0) {
|
|
240
|
+
return { success: true, output: 'No files found' };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return { success: true, output: files.join('\n') };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Simple glob-like pattern matching
|
|
248
|
+
*/
|
|
249
|
+
private matchPattern(filename: string, pattern: string): boolean {
|
|
250
|
+
if (pattern === '*') return true;
|
|
251
|
+
|
|
252
|
+
// Convert glob to regex
|
|
253
|
+
const regex = new RegExp(
|
|
254
|
+
'^' + pattern
|
|
255
|
+
.replace(/\./g, '\\.')
|
|
256
|
+
.replace(/\*/g, '.*')
|
|
257
|
+
.replace(/\?/g, '.') + '$',
|
|
258
|
+
'i'
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
return regex.test(filename);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell Tool - Execute shell commands with security checks
|
|
3
|
+
* Migrated from old src/tools/shell.ts (permission logic inlined)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Tool, ToolResult, ToolContext } from './types.js';
|
|
7
|
+
import { exec } from 'child_process';
|
|
8
|
+
import { promisify } from 'util';
|
|
9
|
+
|
|
10
|
+
const execAsync = promisify(exec);
|
|
11
|
+
|
|
12
|
+
// Dangerous command patterns - blocked immediately
|
|
13
|
+
const DANGEROUS_PATTERNS = [
|
|
14
|
+
/^rm\s+-rf\s+\//i,
|
|
15
|
+
/^del\s+\/f\s+\/s\s+\/q/i,
|
|
16
|
+
/^format\s+[a-z]:/i,
|
|
17
|
+
/^mkfs/i,
|
|
18
|
+
/^dd\s+if=/i,
|
|
19
|
+
/^shred/i,
|
|
20
|
+
/^cat\s+\/dev\/null\s*>/i,
|
|
21
|
+
/>\s*\/dev\/sda/i,
|
|
22
|
+
/^rm\s+-rf\s+\$HOME/i,
|
|
23
|
+
/^rm\s+-rf\s+%USERPROFILE%/i,
|
|
24
|
+
/^curl\s+.*\|.*sh/i,
|
|
25
|
+
/^wget\s+.*\|.*sh/i,
|
|
26
|
+
/^eval\s+/i,
|
|
27
|
+
/base64\s+-d\s+.*\|/i,
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// Commands that need user confirmation
|
|
31
|
+
const CONFIRM_REQUIRED = [
|
|
32
|
+
'rm -rf',
|
|
33
|
+
'rmdir',
|
|
34
|
+
'del /s /q',
|
|
35
|
+
'format',
|
|
36
|
+
'mkfs',
|
|
37
|
+
'dd',
|
|
38
|
+
'shutdown',
|
|
39
|
+
'reboot',
|
|
40
|
+
'pkill',
|
|
41
|
+
'killall',
|
|
42
|
+
'git push --force',
|
|
43
|
+
'git push -f',
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
export class ShellTool implements Tool {
|
|
47
|
+
name = 'shell';
|
|
48
|
+
description = 'Execute shell commands and scripts. Some commands require user confirmation.';
|
|
49
|
+
|
|
50
|
+
inputSchema = {
|
|
51
|
+
type: 'object' as const,
|
|
52
|
+
properties: {
|
|
53
|
+
command: { type: 'string', description: 'Shell command to execute' },
|
|
54
|
+
cwd: { type: 'string', description: 'Working directory' },
|
|
55
|
+
timeout: { type: 'number', description: 'Timeout in seconds', default: 30 },
|
|
56
|
+
},
|
|
57
|
+
required: ['command'],
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
metadata = {
|
|
61
|
+
permissions: ['execute', 'write', 'network'] as ('read' | 'write' | 'execute' | 'network')[],
|
|
62
|
+
tags: ['shell', 'system', 'dangerous'],
|
|
63
|
+
maxDuration: 120000,
|
|
64
|
+
version: '1.0.0',
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
parameters = [
|
|
68
|
+
{ name: 'command', type: 'string', description: 'Shell command to execute', required: true },
|
|
69
|
+
{ name: 'cwd', type: 'string', description: 'Working directory', required: false },
|
|
70
|
+
{ name: 'timeout', type: 'number', description: 'Timeout in seconds', required: false },
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check if command matches dangerous patterns
|
|
75
|
+
*/
|
|
76
|
+
private isDangerous(command: string): boolean {
|
|
77
|
+
return DANGEROUS_PATTERNS.some(pattern => pattern.test(command.trim()));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if command needs user confirmation
|
|
82
|
+
*/
|
|
83
|
+
private needsConfirmation(command: string): boolean {
|
|
84
|
+
const lower = command.toLowerCase();
|
|
85
|
+
return CONFIRM_REQUIRED.some(cmd => lower.includes(cmd.toLowerCase()));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async execute(params: Record<string, any>, ctx?: ToolContext): Promise<ToolResult> {
|
|
89
|
+
const { command, cwd, timeout = 30 } = params;
|
|
90
|
+
|
|
91
|
+
if (!command || typeof command !== 'string') {
|
|
92
|
+
return { success: false, error: 'Command is required and must be a string' };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const trimmed = command.trim();
|
|
96
|
+
|
|
97
|
+
// Block dangerous commands
|
|
98
|
+
if (this.isDangerous(trimmed)) {
|
|
99
|
+
return { success: false, error: `Blocked: dangerous command "${trimmed.substring(0, 50)}..."` };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (trimmed.length > 10000) {
|
|
103
|
+
return { success: false, error: 'Command too long (max 10000 characters)' };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Ask for confirmation on risky commands
|
|
107
|
+
if (ctx?.confirmAction && this.needsConfirmation(trimmed)) {
|
|
108
|
+
const confirmed = await ctx.confirmAction(
|
|
109
|
+
`⚠️ Confirm command:\n ${command}\n\n[y] Yes [n] No`
|
|
110
|
+
);
|
|
111
|
+
if (!confirmed) {
|
|
112
|
+
return { success: false, error: 'Command cancelled by user' };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const options: any = {
|
|
118
|
+
timeout: timeout * 1000,
|
|
119
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB
|
|
120
|
+
};
|
|
121
|
+
if (cwd) options.cwd = cwd;
|
|
122
|
+
|
|
123
|
+
const { stdout, stderr } = await execAsync(command, options);
|
|
124
|
+
const stdoutStr = stdout?.toString() || '';
|
|
125
|
+
const stderrStr = stderr?.toString() || '';
|
|
126
|
+
const output = stdoutStr + (stderrStr ? `\n[stderr]: ${stderrStr}` : '');
|
|
127
|
+
|
|
128
|
+
return { success: true, output: output.trim() || '(command executed successfully with no output)' };
|
|
129
|
+
} catch (error: any) {
|
|
130
|
+
if (error.killed) {
|
|
131
|
+
return { success: false, error: 'Command timed out' };
|
|
132
|
+
}
|
|
133
|
+
return { success: false, error: error.message || 'Command execution failed' };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool interface definitions
|
|
3
|
+
* Extracted from old src/core/types.ts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ==================== Tool Parameter & Schema ====================
|
|
7
|
+
|
|
8
|
+
export interface ToolParameter {
|
|
9
|
+
name: string;
|
|
10
|
+
type: string;
|
|
11
|
+
description: string;
|
|
12
|
+
required: boolean;
|
|
13
|
+
default?: any;
|
|
14
|
+
enum?: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ToolInputSchema {
|
|
18
|
+
type: 'object';
|
|
19
|
+
properties: Record<string, {
|
|
20
|
+
type: string;
|
|
21
|
+
description: string;
|
|
22
|
+
default?: any;
|
|
23
|
+
enum?: string[];
|
|
24
|
+
}>;
|
|
25
|
+
required?: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ToolMetadata {
|
|
29
|
+
permissions?: ('read' | 'write' | 'execute' | 'network')[];
|
|
30
|
+
maxDuration?: number;
|
|
31
|
+
tags?: string[];
|
|
32
|
+
version?: string;
|
|
33
|
+
deprecated?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ==================== Tool Interface ====================
|
|
37
|
+
|
|
38
|
+
export interface ToolContext {
|
|
39
|
+
sessionId?: string;
|
|
40
|
+
workingDirectory?: string;
|
|
41
|
+
confirmAction?: (msg: string) => Promise<boolean>;
|
|
42
|
+
signal?: AbortSignal;
|
|
43
|
+
toolCallId?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ToolResult {
|
|
47
|
+
success: boolean;
|
|
48
|
+
output?: string;
|
|
49
|
+
error?: string;
|
|
50
|
+
data?: any;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface Tool {
|
|
54
|
+
name: string;
|
|
55
|
+
description: string;
|
|
56
|
+
parameters: ToolParameter[];
|
|
57
|
+
inputSchema?: ToolInputSchema;
|
|
58
|
+
metadata?: ToolMetadata;
|
|
59
|
+
execute(params: Record<string, any>, ctx?: ToolContext): Promise<ToolResult>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ==================== Tool Builder ====================
|
|
63
|
+
|
|
64
|
+
export class ToolBuilder {
|
|
65
|
+
private _name = '';
|
|
66
|
+
private _description = '';
|
|
67
|
+
private _parameters: ToolParameter[] = [];
|
|
68
|
+
private _inputSchema: ToolInputSchema = { type: 'object', properties: {} };
|
|
69
|
+
private _metadata: ToolMetadata = {};
|
|
70
|
+
private _fn?: (params: Record<string, any>, ctx: ToolContext) => Promise<ToolResult>;
|
|
71
|
+
|
|
72
|
+
name(n: string): this { this._name = n; return this; }
|
|
73
|
+
description(d: string): this { this._description = d; return this; }
|
|
74
|
+
|
|
75
|
+
param(p: ToolParameter): this {
|
|
76
|
+
this._parameters.push(p);
|
|
77
|
+
this._inputSchema.properties[p.name] = {
|
|
78
|
+
type: p.type,
|
|
79
|
+
description: p.description,
|
|
80
|
+
default: p.default,
|
|
81
|
+
enum: p.enum,
|
|
82
|
+
};
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
stringParam(name: string, description: string, required = true, defaultVal?: string): this {
|
|
87
|
+
return this.param({ name, type: 'string', description, required, default: defaultVal });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
booleanParam(name: string, description: string, required = false, defaultVal?: boolean): this {
|
|
91
|
+
return this.param({ name, type: 'boolean', description, required, default: defaultVal });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
permissions(...perms: ('read' | 'write' | 'execute' | 'network')[]): this {
|
|
95
|
+
this._metadata.permissions = perms;
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
tags(...t: string[]): this {
|
|
100
|
+
this._metadata.tags = t;
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
handle(fn: (params: Record<string, any>, ctx: ToolContext) => Promise<ToolResult>): this {
|
|
105
|
+
this._fn = fn;
|
|
106
|
+
return this;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
build(): Tool {
|
|
110
|
+
if (!this._name || !this._fn) {
|
|
111
|
+
throw new Error('Tool name and handler are required');
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
name: this._name,
|
|
115
|
+
description: this._description,
|
|
116
|
+
parameters: this._parameters,
|
|
117
|
+
inputSchema: this._inputSchema,
|
|
118
|
+
metadata: this._metadata,
|
|
119
|
+
execute: this._fn,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
package/src/tui/index.ts
ADDED
package/src/tui/input.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REPL Input Handler
|
|
3
|
+
* Migrated from old src/repl/input.ts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import readline from 'readline';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
|
|
9
|
+
export class REPLInput {
|
|
10
|
+
private rl: readline.Interface;
|
|
11
|
+
private history: string[] = [];
|
|
12
|
+
private historyIndex: number = -1;
|
|
13
|
+
|
|
14
|
+
constructor() {
|
|
15
|
+
this.rl = readline.createInterface({
|
|
16
|
+
input: process.stdin,
|
|
17
|
+
output: process.stdout,
|
|
18
|
+
completer: this.completer.bind(this),
|
|
19
|
+
historySize: 100,
|
|
20
|
+
tabSize: 4,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Handle Ctrl+C
|
|
24
|
+
this.rl.on('SIGINT', () => {
|
|
25
|
+
process.stdout.write(chalk.gray('\n\n👋 Exiting...\n'));
|
|
26
|
+
process.exit(0);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Track history
|
|
30
|
+
this.rl.on('line', (line) => {
|
|
31
|
+
if (line.trim()) {
|
|
32
|
+
this.history.push(line);
|
|
33
|
+
if (this.history.length > 100) this.history.shift();
|
|
34
|
+
}
|
|
35
|
+
this.historyIndex = this.history.length;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async prompt(prefix: string = chalk.green('\n> ')): Promise<string> {
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
this.rl.question(prefix, (answer) => {
|
|
42
|
+
resolve(answer);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
close(): void {
|
|
48
|
+
this.rl.close();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private completer(line: string): [string[], string] {
|
|
52
|
+
const commands = ['exit', 'quit', 'clear', 'help', 'tools', 'model'];
|
|
53
|
+
const hits = commands.filter(c => c.startsWith(line.toLowerCase()));
|
|
54
|
+
return [hits.length ? hits : [], line];
|
|
55
|
+
}
|
|
56
|
+
}
|