@demirarch/recode 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.
- package/COMMERCIAL.md +53 -0
- package/LICENSE +184 -0
- package/README.md +63 -0
- package/dist/App.js +31 -0
- package/dist/components/ChatScreen.js +130 -0
- package/dist/components/SetupScreen.js +56 -0
- package/dist/components/StatusBar.js +6 -0
- package/dist/components/TopBar.js +15 -0
- package/dist/hooks/useAgent.js +106 -0
- package/dist/lib/commands.js +79 -0
- package/dist/lib/config.js +22 -0
- package/dist/lib/models.js +56 -0
- package/dist/lib/openrouter.js +94 -0
- package/dist/lib/tools.js +211 -0
- package/dist/main.js +6 -0
- package/package.json +33 -0
- package/src/App.tsx +54 -0
- package/src/components/ChatScreen.tsx +231 -0
- package/src/components/SetupScreen.tsx +113 -0
- package/src/components/StatusBar.tsx +23 -0
- package/src/components/TopBar.tsx +33 -0
- package/src/hooks/useAgent.ts +132 -0
- package/src/lib/commands.ts +101 -0
- package/src/lib/config.ts +29 -0
- package/src/lib/models.ts +64 -0
- package/src/lib/openrouter.ts +131 -0
- package/src/lib/tools.ts +235 -0
- package/src/main.tsx +8 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export interface Model {
|
|
2
|
+
id: string;
|
|
3
|
+
label: string;
|
|
4
|
+
provider: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const MODELS: Model[] = [
|
|
8
|
+
// Anthropic
|
|
9
|
+
{ id: 'anthropic/claude-opus-4-5', label: 'Claude Opus 4.5', provider: 'Anthropic' },
|
|
10
|
+
{ id: 'anthropic/claude-sonnet-4-5', label: 'Claude Sonnet 4.5 ★', provider: 'Anthropic' },
|
|
11
|
+
{ id: 'anthropic/claude-haiku-4-5-20251001', label: 'Claude Haiku 4.5', provider: 'Anthropic' },
|
|
12
|
+
{ id: 'anthropic/claude-3-5-sonnet', label: 'Claude 3.5 Sonnet', provider: 'Anthropic' },
|
|
13
|
+
{ id: 'anthropic/claude-3-5-haiku', label: 'Claude 3.5 Haiku', provider: 'Anthropic' },
|
|
14
|
+
{ id: 'anthropic/claude-3-opus', label: 'Claude 3 Opus', provider: 'Anthropic' },
|
|
15
|
+
// OpenAI
|
|
16
|
+
{ id: 'openai/gpt-4o', label: 'GPT-4o', provider: 'OpenAI' },
|
|
17
|
+
{ id: 'openai/gpt-4o-mini', label: 'GPT-4o Mini', provider: 'OpenAI' },
|
|
18
|
+
{ id: 'openai/o1', label: 'o1', provider: 'OpenAI' },
|
|
19
|
+
{ id: 'openai/o1-mini', label: 'o1 Mini', provider: 'OpenAI' },
|
|
20
|
+
{ id: 'openai/o3-mini', label: 'o3 Mini', provider: 'OpenAI' },
|
|
21
|
+
{ id: 'openai/o4-mini', label: 'o4 Mini', provider: 'OpenAI' },
|
|
22
|
+
{ id: 'openai/gpt-4-turbo', label: 'GPT-4 Turbo', provider: 'OpenAI' },
|
|
23
|
+
// Google
|
|
24
|
+
{ id: 'google/gemini-2.5-pro', label: 'Gemini 2.5 Pro', provider: 'Google' },
|
|
25
|
+
{ id: 'google/gemini-2.5-flash', label: 'Gemini 2.5 Flash', provider: 'Google' },
|
|
26
|
+
{ id: 'google/gemini-flash-1.5', label: 'Gemini Flash 1.5', provider: 'Google' },
|
|
27
|
+
{ id: 'google/gemini-pro-1.5', label: 'Gemini Pro 1.5', provider: 'Google' },
|
|
28
|
+
// Meta
|
|
29
|
+
{ id: 'meta-llama/llama-4-maverick', label: 'Llama 4 Maverick', provider: 'Meta' },
|
|
30
|
+
{ id: 'meta-llama/llama-4-scout', label: 'Llama 4 Scout', provider: 'Meta' },
|
|
31
|
+
{ id: 'meta-llama/llama-3.3-70b-instruct', label: 'Llama 3.3 70B', provider: 'Meta' },
|
|
32
|
+
{ id: 'meta-llama/llama-3.1-405b-instruct', label: 'Llama 3.1 405B', provider: 'Meta' },
|
|
33
|
+
// DeepSeek
|
|
34
|
+
{ id: 'deepseek/deepseek-chat', label: 'DeepSeek Chat V3', provider: 'DeepSeek' },
|
|
35
|
+
{ id: 'deepseek/deepseek-r1', label: 'DeepSeek R1', provider: 'DeepSeek' },
|
|
36
|
+
{ id: 'deepseek/deepseek-r1-distill-llama-70b', label: 'DeepSeek R1 Distill 70B', provider: 'DeepSeek' },
|
|
37
|
+
// Mistral
|
|
38
|
+
{ id: 'mistralai/mistral-large', label: 'Mistral Large', provider: 'Mistral' },
|
|
39
|
+
{ id: 'mistralai/mistral-small', label: 'Mistral Small', provider: 'Mistral' },
|
|
40
|
+
{ id: 'mistralai/codestral-latest', label: 'Codestral', provider: 'Mistral' },
|
|
41
|
+
{ id: 'mistralai/mixtral-8x22b-instruct', label: 'Mixtral 8x22B', provider: 'Mistral' },
|
|
42
|
+
// Qwen
|
|
43
|
+
{ id: 'qwen/qwen3-235b-a22b', label: 'Qwen3 235B', provider: 'Qwen' },
|
|
44
|
+
{ id: 'qwen/qwen-2.5-72b-instruct', label: 'Qwen 2.5 72B', provider: 'Qwen' },
|
|
45
|
+
{ id: 'qwen/qwen-2.5-coder-32b-instruct', label: 'Qwen 2.5 Coder 32B', provider: 'Qwen' },
|
|
46
|
+
// Cohere
|
|
47
|
+
{ id: 'cohere/command-r-plus', label: 'Command R+', provider: 'Cohere' },
|
|
48
|
+
{ id: 'cohere/command-r', label: 'Command R', provider: 'Cohere' },
|
|
49
|
+
// xAI
|
|
50
|
+
{ id: 'x-ai/grok-3', label: 'Grok 3', provider: 'xAI' },
|
|
51
|
+
{ id: 'x-ai/grok-3-mini', label: 'Grok 3 Mini', provider: 'xAI' },
|
|
52
|
+
// Microsoft
|
|
53
|
+
{ id: 'microsoft/phi-4', label: 'Phi-4', provider: 'Microsoft' },
|
|
54
|
+
{ id: 'microsoft/wizardlm-2-8x22b', label: 'WizardLM 2 8x22B', provider: 'Microsoft' },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
export const MODEL_SELECT_ITEMS = MODELS.map((m) => ({
|
|
58
|
+
label: `${m.provider.padEnd(11)} ${m.label}`,
|
|
59
|
+
value: m.id,
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
export function getModelLabel(id: string): string {
|
|
63
|
+
return MODELS.find((m) => m.id === id)?.label ?? id.split('/')[1] ?? id;
|
|
64
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { TOOL_DEFINITIONS } from './tools.js';
|
|
2
|
+
|
|
3
|
+
export interface Message {
|
|
4
|
+
role: 'user' | 'assistant' | 'system' | 'tool';
|
|
5
|
+
content: string | null;
|
|
6
|
+
tool_calls?: ToolCall[];
|
|
7
|
+
tool_call_id?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ToolCall {
|
|
11
|
+
id: string;
|
|
12
|
+
type: 'function';
|
|
13
|
+
function: { name: string; arguments: string };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface OpenRouterResponse {
|
|
17
|
+
content: string | null;
|
|
18
|
+
tool_calls: ToolCall[] | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ── System Prompt ─────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
export const SYSTEM_PROMPT = `You are Recode — an autonomous AI coding agent built by DemirArch.
|
|
24
|
+
You run inside a terminal and have direct access to the filesystem and shell through your tools.
|
|
25
|
+
|
|
26
|
+
━━━ IDENTITY ━━━
|
|
27
|
+
Name: Recode
|
|
28
|
+
Author: DemirArch (github.com/demirgitbuh/recode)
|
|
29
|
+
Interface: Terminal TUI (Ink + React + TypeScript)
|
|
30
|
+
Mission: Autonomous software development — from idea to working code
|
|
31
|
+
|
|
32
|
+
━━━ CAPABILITIES ━━━
|
|
33
|
+
You can read, write, and create any file; execute shell commands; explore
|
|
34
|
+
directories; search codebases. You are capable of building entire projects
|
|
35
|
+
from scratch, debugging production bugs, refactoring legacy code, setting up
|
|
36
|
+
CI/CD, writing tests, managing git, installing packages, and more.
|
|
37
|
+
|
|
38
|
+
━━━ CORE RULES ━━━
|
|
39
|
+
1. Always complete tasks fully. Never stub, never "TODO" without implementing.
|
|
40
|
+
2. Read before edit — use read_file before modifying any existing file.
|
|
41
|
+
3. Explore before assuming — use list_directory on unfamiliar codebases.
|
|
42
|
+
4. Verify your work — run the code, test it, check for errors with execute_command.
|
|
43
|
+
5. Chain as many tools as needed to finish the job completely.
|
|
44
|
+
6. Never truncate code in write_file — write the complete file every time.
|
|
45
|
+
7. If ambiguous, make a reasonable assumption, state it once, and proceed.
|
|
46
|
+
8. Ask at most ONE clarifying question per turn, only when truly needed.
|
|
47
|
+
9. When creating multi-file projects, create ALL files before reporting done.
|
|
48
|
+
10. Always create parent directories before writing files.
|
|
49
|
+
|
|
50
|
+
━━━ OUTPUT STYLE ━━━
|
|
51
|
+
- Terminal display — keep lines under 80 characters when possible.
|
|
52
|
+
- Use fenced code blocks with language tags in explanations.
|
|
53
|
+
- No filler: skip "Certainly!", "Great question!", "Of course!", "Sure!".
|
|
54
|
+
- File operations: one line per file, brief and clear.
|
|
55
|
+
- Errors: show the error → root cause → fix. In that order, no padding.
|
|
56
|
+
- When done with a multi-step task, give a concise summary of what was built.
|
|
57
|
+
|
|
58
|
+
━━━ CODE STANDARDS ━━━
|
|
59
|
+
- Write production-quality, runnable code. No placeholder logic.
|
|
60
|
+
- Match existing style when editing projects (indentation, naming, imports).
|
|
61
|
+
- Comments only for non-obvious logic. No docstring spam.
|
|
62
|
+
- Handle errors at system boundaries (user input, network, filesystem).
|
|
63
|
+
- Use the language's modern idioms and standard library features.
|
|
64
|
+
- Prefer explicit over clever. Prefer simple over abstract.
|
|
65
|
+
- Never add features the user didn't ask for.
|
|
66
|
+
|
|
67
|
+
━━━ TOOL DISCIPLINE ━━━
|
|
68
|
+
read_file — call before any edit to understand current content
|
|
69
|
+
write_file — complete file content only; never partial or truncated
|
|
70
|
+
execute_command — builds, tests, git, npm, pip, cargo, go, linters, etc.
|
|
71
|
+
list_directory — first action when exploring an unfamiliar codebase
|
|
72
|
+
create_directory — call before write_file when parent dir may not exist
|
|
73
|
+
delete_file — only when explicitly requested or clearly needed
|
|
74
|
+
search_files — find definitions, usages, or patterns before assuming
|
|
75
|
+
|
|
76
|
+
━━━ LANGUAGE EXPERTISE ━━━
|
|
77
|
+
You are expert-level in: TypeScript, JavaScript, Python, Rust, Go, C, C++,
|
|
78
|
+
C#, Java, Ruby, Swift, Kotlin, PHP, Bash, SQL, HTML, CSS, SCSS, React,
|
|
79
|
+
Vue, Svelte, Next.js, Express, FastAPI, Django, Axum, and more.
|
|
80
|
+
You know package managers: npm, pnpm, yarn, pip, cargo, go mod, apt, brew.
|
|
81
|
+
You know tools: git, docker, docker-compose, make, cmake, webpack, vite, esbuild.`;
|
|
82
|
+
|
|
83
|
+
// ── API Client ────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
export async function callOpenRouter(
|
|
86
|
+
messages: Message[],
|
|
87
|
+
model: string,
|
|
88
|
+
apiKey: string
|
|
89
|
+
): Promise<OpenRouterResponse> {
|
|
90
|
+
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: {
|
|
93
|
+
Authorization: `Bearer ${apiKey}`,
|
|
94
|
+
'Content-Type': 'application/json',
|
|
95
|
+
'HTTP-Referer': 'https://github.com/demirgitbuh/recode',
|
|
96
|
+
'X-Title': 'Recode',
|
|
97
|
+
},
|
|
98
|
+
body: JSON.stringify({
|
|
99
|
+
model,
|
|
100
|
+
messages: [{ role: 'system', content: SYSTEM_PROMPT }, ...messages],
|
|
101
|
+
tools: TOOL_DEFINITIONS,
|
|
102
|
+
tool_choice: 'auto',
|
|
103
|
+
stream: false,
|
|
104
|
+
}),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
const err = await response.text();
|
|
109
|
+
throw new Error(`OpenRouter ${response.status}: ${err}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const data = (await response.json()) as {
|
|
113
|
+
choices?: {
|
|
114
|
+
message?: {
|
|
115
|
+
content?: string | null;
|
|
116
|
+
tool_calls?: ToolCall[];
|
|
117
|
+
};
|
|
118
|
+
}[];
|
|
119
|
+
error?: { message: string };
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
if (data.error) throw new Error(data.error.message);
|
|
123
|
+
|
|
124
|
+
const msg = data.choices?.[0]?.message;
|
|
125
|
+
if (!msg) throw new Error('Empty response from OpenRouter');
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
content: msg.content ?? null,
|
|
129
|
+
tool_calls: msg.tool_calls?.length ? msg.tool_calls : null,
|
|
130
|
+
};
|
|
131
|
+
}
|
package/src/lib/tools.ts
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import { exec } from 'child_process';
|
|
3
|
+
import { promisify } from 'util';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
const execAsync = promisify(exec);
|
|
7
|
+
|
|
8
|
+
// ── OpenRouter tool definitions ──────────────────────────────
|
|
9
|
+
|
|
10
|
+
export const TOOL_DEFINITIONS = [
|
|
11
|
+
{
|
|
12
|
+
type: 'function' as const,
|
|
13
|
+
function: {
|
|
14
|
+
name: 'read_file',
|
|
15
|
+
description: 'Read the contents of a file at the given path.',
|
|
16
|
+
parameters: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
path: { type: 'string', description: 'Path to the file (absolute or relative to cwd)' },
|
|
20
|
+
},
|
|
21
|
+
required: ['path'],
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
type: 'function' as const,
|
|
27
|
+
function: {
|
|
28
|
+
name: 'write_file',
|
|
29
|
+
description: 'Write or overwrite a file with the given content. Always write complete file content.',
|
|
30
|
+
parameters: {
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: {
|
|
33
|
+
path: { type: 'string', description: 'Path to the file' },
|
|
34
|
+
content: { type: 'string', description: 'Full file content to write' },
|
|
35
|
+
},
|
|
36
|
+
required: ['path', 'content'],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
type: 'function' as const,
|
|
42
|
+
function: {
|
|
43
|
+
name: 'list_directory',
|
|
44
|
+
description: 'List files and directories at the given path.',
|
|
45
|
+
parameters: {
|
|
46
|
+
type: 'object',
|
|
47
|
+
properties: {
|
|
48
|
+
path: { type: 'string', description: 'Directory path to list (default: cwd)' },
|
|
49
|
+
},
|
|
50
|
+
required: [],
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
type: 'function' as const,
|
|
56
|
+
function: {
|
|
57
|
+
name: 'create_directory',
|
|
58
|
+
description: 'Create a directory (and all parent directories) at the given path.',
|
|
59
|
+
parameters: {
|
|
60
|
+
type: 'object',
|
|
61
|
+
properties: {
|
|
62
|
+
path: { type: 'string', description: 'Directory path to create' },
|
|
63
|
+
},
|
|
64
|
+
required: ['path'],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
type: 'function' as const,
|
|
70
|
+
function: {
|
|
71
|
+
name: 'delete_file',
|
|
72
|
+
description: 'Delete a file at the given path.',
|
|
73
|
+
parameters: {
|
|
74
|
+
type: 'object',
|
|
75
|
+
properties: {
|
|
76
|
+
path: { type: 'string', description: 'Path to the file to delete' },
|
|
77
|
+
},
|
|
78
|
+
required: ['path'],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
type: 'function' as const,
|
|
84
|
+
function: {
|
|
85
|
+
name: 'execute_command',
|
|
86
|
+
description: 'Execute a shell command in the current working directory. Use for builds, tests, git, npm, etc.',
|
|
87
|
+
parameters: {
|
|
88
|
+
type: 'object',
|
|
89
|
+
properties: {
|
|
90
|
+
command: { type: 'string', description: 'Shell command to execute' },
|
|
91
|
+
timeout: { type: 'number', description: 'Timeout in milliseconds (default 30000)' },
|
|
92
|
+
},
|
|
93
|
+
required: ['command'],
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
type: 'function' as const,
|
|
99
|
+
function: {
|
|
100
|
+
name: 'search_files',
|
|
101
|
+
description: 'Search for a text pattern in files under a directory.',
|
|
102
|
+
parameters: {
|
|
103
|
+
type: 'object',
|
|
104
|
+
properties: {
|
|
105
|
+
pattern: { type: 'string', description: 'Text or regex pattern to search for' },
|
|
106
|
+
directory: { type: 'string', description: 'Directory to search in (default: cwd)' },
|
|
107
|
+
extension: { type: 'string', description: 'File extension filter e.g. ".ts"' },
|
|
108
|
+
},
|
|
109
|
+
required: ['pattern'],
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
// ── Tool execution ────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
export interface ToolArgs {
|
|
118
|
+
read_file: { path: string };
|
|
119
|
+
write_file: { path: string; content: string };
|
|
120
|
+
list_directory: { path?: string };
|
|
121
|
+
create_directory: { path: string };
|
|
122
|
+
delete_file: { path: string };
|
|
123
|
+
execute_command: { command: string; timeout?: number };
|
|
124
|
+
search_files: { pattern: string; directory?: string; extension?: string };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function executeTool(
|
|
128
|
+
name: string,
|
|
129
|
+
args: Record<string, unknown>,
|
|
130
|
+
cwd: string
|
|
131
|
+
): Promise<string> {
|
|
132
|
+
const resolve = (p: string) =>
|
|
133
|
+
path.isAbsolute(p) ? p : path.resolve(cwd, p);
|
|
134
|
+
|
|
135
|
+
switch (name) {
|
|
136
|
+
case 'read_file': {
|
|
137
|
+
const { path: p } = args as ToolArgs['read_file'];
|
|
138
|
+
const content = await fs.readFile(resolve(p), 'utf-8');
|
|
139
|
+
const lines = content.split('\n');
|
|
140
|
+
const numbered = lines
|
|
141
|
+
.map((l, i) => `${String(i + 1).padStart(4, ' ')} ${l}`)
|
|
142
|
+
.join('\n');
|
|
143
|
+
return `File: ${p} (${lines.length} lines)\n\n${numbered}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
case 'write_file': {
|
|
147
|
+
const { path: p, content } = args as ToolArgs['write_file'];
|
|
148
|
+
const full = resolve(p);
|
|
149
|
+
await fs.mkdir(path.dirname(full), { recursive: true });
|
|
150
|
+
await fs.writeFile(full, content, 'utf-8');
|
|
151
|
+
const lines = content.split('\n').length;
|
|
152
|
+
return `Written: ${p} (${lines} lines)`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
case 'list_directory': {
|
|
156
|
+
const { path: p = '.' } = args as ToolArgs['list_directory'];
|
|
157
|
+
const entries = await fs.readdir(resolve(p), { withFileTypes: true });
|
|
158
|
+
const lines = entries.map((e) => {
|
|
159
|
+
const type = e.isDirectory() ? '/' : e.isSymbolicLink() ? '@' : ' ';
|
|
160
|
+
return `${type} ${e.name}`;
|
|
161
|
+
});
|
|
162
|
+
return `${resolve(p)}\n${lines.join('\n')}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
case 'create_directory': {
|
|
166
|
+
const { path: p } = args as ToolArgs['create_directory'];
|
|
167
|
+
await fs.mkdir(resolve(p), { recursive: true });
|
|
168
|
+
return `Created: ${p}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
case 'delete_file': {
|
|
172
|
+
const { path: p } = args as ToolArgs['delete_file'];
|
|
173
|
+
await fs.unlink(resolve(p));
|
|
174
|
+
return `Deleted: ${p}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
case 'execute_command': {
|
|
178
|
+
const { command, timeout = 30000 } = args as ToolArgs['execute_command'];
|
|
179
|
+
try {
|
|
180
|
+
const { stdout, stderr } = await execAsync(command, { cwd, timeout });
|
|
181
|
+
const out = [stdout.trim(), stderr.trim()].filter(Boolean).join('\n');
|
|
182
|
+
return out || '(no output)';
|
|
183
|
+
} catch (err: unknown) {
|
|
184
|
+
const e = err as { stdout?: string; stderr?: string; message?: string };
|
|
185
|
+
const out = [e.stdout?.trim(), e.stderr?.trim(), e.message]
|
|
186
|
+
.filter(Boolean)
|
|
187
|
+
.join('\n');
|
|
188
|
+
return `Exit error:\n${out}`;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
case 'search_files': {
|
|
193
|
+
const { pattern, directory = '.', extension } = args as ToolArgs['search_files'];
|
|
194
|
+
const results: string[] = [];
|
|
195
|
+
|
|
196
|
+
async function walk(dir: string) {
|
|
197
|
+
let entries: import('fs').Dirent[];
|
|
198
|
+
try {
|
|
199
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
200
|
+
} catch {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
for (const entry of entries) {
|
|
204
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
205
|
+
const full = path.join(dir, entry.name);
|
|
206
|
+
if (entry.isDirectory()) {
|
|
207
|
+
await walk(full);
|
|
208
|
+
} else {
|
|
209
|
+
if (extension && !entry.name.endsWith(extension)) continue;
|
|
210
|
+
try {
|
|
211
|
+
const content = await fs.readFile(full, 'utf-8');
|
|
212
|
+
const lines = content.split('\n');
|
|
213
|
+
const re = new RegExp(pattern, 'i');
|
|
214
|
+
lines.forEach((line, idx) => {
|
|
215
|
+
if (re.test(line)) {
|
|
216
|
+
const rel = path.relative(cwd, full);
|
|
217
|
+
results.push(`${rel}:${idx + 1}: ${line.trim()}`);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
} catch {
|
|
221
|
+
// skip binary files
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
await walk(resolve(directory));
|
|
228
|
+
if (results.length === 0) return `No matches for "${pattern}"`;
|
|
229
|
+
return results.slice(0, 100).join('\n');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
default:
|
|
233
|
+
return `Unknown tool: ${name}`;
|
|
234
|
+
}
|
|
235
|
+
}
|
package/src/main.tsx
ADDED
package/tsconfig.json
ADDED