@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.
@@ -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
+ }
@@ -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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import React from 'react';
3
+ import { render } from 'ink';
4
+ import { App } from './App.js';
5
+
6
+ const initialCwd = process.cwd();
7
+
8
+ render(<App initialCwd={initialCwd} />);
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "jsx": "react-jsx",
8
+ "outDir": "./dist",
9
+ "rootDir": "./src",
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true
12
+ }
13
+ }