@alia-codea/cli 1.1.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1263 -487
- package/package.json +5 -5
- package/src/app.tsx +281 -0
- package/src/commands/auth.ts +11 -1
- package/src/commands/repl.ts +11 -299
- package/src/commands/run.ts +103 -126
- package/src/commands/sessions.ts +5 -6
- package/src/components/ApprovalPrompt.tsx +60 -0
- package/src/components/Header.tsx +39 -0
- package/src/components/InputBar.tsx +36 -0
- package/src/components/MessageList.tsx +81 -0
- package/src/components/ThinkingIndicator.tsx +28 -0
- package/src/components/ToolCallCard.tsx +68 -0
- package/src/index.ts +20 -6
- package/src/tools/executor.ts +140 -14
- package/src/tools/patch.ts +167 -0
- package/src/utils/api.ts +22 -3
- package/src/utils/approval.ts +31 -0
- package/src/utils/context.ts +65 -4
- package/src/utils/conversation.ts +141 -0
- package/dist/api-X2G5QROW.js +0 -10
- package/dist/chunk-SVPL4GNV.js +0 -230
- package/dist/index.d.ts +0 -1
- package/src/utils/ui.ts +0 -153
package/src/index.ts
CHANGED
|
@@ -8,15 +8,14 @@ import { login } from './commands/auth.js';
|
|
|
8
8
|
import { listSessions, resumeSession } from './commands/sessions.js';
|
|
9
9
|
import chalk from 'chalk';
|
|
10
10
|
|
|
11
|
-
const VERSION = '
|
|
11
|
+
const VERSION = '2.0.0';
|
|
12
12
|
|
|
13
13
|
const program = new Command();
|
|
14
14
|
|
|
15
|
-
// ASCII art banner
|
|
16
15
|
const banner = `
|
|
17
16
|
${chalk.cyan(' ____ _ ')}
|
|
18
17
|
${chalk.cyan(' / ___|___ __| | ___ __ _ ')}
|
|
19
|
-
${chalk.cyan(' | | / _ \\ / _
|
|
18
|
+
${chalk.cyan(' | | / _ \\ / _` |/ _ \\/ _` |')}
|
|
20
19
|
${chalk.cyan(' | |__| (_) | (_| | __/ (_| |')}
|
|
21
20
|
${chalk.cyan(' \\____\\___/ \\__,_|\\___|\\__,_|')}
|
|
22
21
|
${chalk.gray(' AI Coding Assistant by Alia')}
|
|
@@ -41,14 +40,15 @@ program
|
|
|
41
40
|
}
|
|
42
41
|
});
|
|
43
42
|
|
|
44
|
-
// Default command - start REPL
|
|
43
|
+
// Default command - start REPL with Ink TUI
|
|
45
44
|
program
|
|
46
45
|
.command('chat', { isDefault: true })
|
|
47
46
|
.description('Start an interactive chat session')
|
|
48
47
|
.option('-m, --model <model>', 'Model to use (codea, codea-pro, codea-thinking)', 'alia-v1-codea')
|
|
48
|
+
.option('-a, --approval-mode <mode>', 'Approval mode: suggest, auto-edit, full-auto', 'suggest')
|
|
49
49
|
.option('--no-context', 'Disable automatic codebase context')
|
|
50
|
+
.option('--no-instructions', 'Disable CODEA.md project instructions')
|
|
50
51
|
.action(async (options) => {
|
|
51
|
-
console.log(banner);
|
|
52
52
|
await startRepl(options);
|
|
53
53
|
});
|
|
54
54
|
|
|
@@ -58,12 +58,26 @@ program
|
|
|
58
58
|
.alias('r')
|
|
59
59
|
.description('Run a single prompt and exit')
|
|
60
60
|
.option('-m, --model <model>', 'Model to use', 'alia-v1-codea')
|
|
61
|
-
.option('-y, --yes', 'Auto-approve all
|
|
61
|
+
.option('-y, --yes', 'Auto-approve all actions (full-auto mode)')
|
|
62
|
+
.option('-a, --approval-mode <mode>', 'Approval mode: suggest, auto-edit, full-auto', 'suggest')
|
|
63
|
+
.option('-q, --quiet', 'Suppress UI, output only response text')
|
|
64
|
+
.option('--json', 'Output structured JSON')
|
|
62
65
|
.option('--no-context', 'Disable automatic codebase context')
|
|
63
66
|
.action(async (prompt, options) => {
|
|
64
67
|
await runPrompt(prompt, options);
|
|
65
68
|
});
|
|
66
69
|
|
|
70
|
+
// Exec command - shorthand for run --json --quiet --yes
|
|
71
|
+
program
|
|
72
|
+
.command('exec <prompt>')
|
|
73
|
+
.alias('x')
|
|
74
|
+
.description('Execute a prompt in full-auto mode with JSON output')
|
|
75
|
+
.option('-m, --model <model>', 'Model to use', 'alia-v1-codea')
|
|
76
|
+
.option('--no-context', 'Disable automatic codebase context')
|
|
77
|
+
.action(async (prompt, options) => {
|
|
78
|
+
await runPrompt(prompt, { ...options, yes: true, quiet: false, json: true });
|
|
79
|
+
});
|
|
80
|
+
|
|
67
81
|
// Login/configure
|
|
68
82
|
program
|
|
69
83
|
.command('login')
|
package/src/tools/executor.ts
CHANGED
|
@@ -2,8 +2,8 @@ import * as fs from 'fs/promises';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { exec } from 'child_process';
|
|
4
4
|
import { promisify } from 'util';
|
|
5
|
-
import { glob } from 'fs/promises';
|
|
6
5
|
import chalk from 'chalk';
|
|
6
|
+
import { applyPatch } from './patch.js';
|
|
7
7
|
|
|
8
8
|
const execAsync = promisify(exec);
|
|
9
9
|
|
|
@@ -21,10 +21,12 @@ export async function executeTool(name: string, args: Record<string, any>): Prom
|
|
|
21
21
|
return await writeFile(args.path, args.content);
|
|
22
22
|
case 'edit_file':
|
|
23
23
|
return await editFile(args.path, args.old_text, args.new_text);
|
|
24
|
+
case 'apply_patch':
|
|
25
|
+
return await applyPatchTool(args.patch);
|
|
24
26
|
case 'list_files':
|
|
25
27
|
return await listFiles(args.path, args.recursive);
|
|
26
28
|
case 'search_files':
|
|
27
|
-
return await searchFiles(args.pattern, args.path, args.file_pattern);
|
|
29
|
+
return await searchFiles(args.pattern, args.path, args.file_pattern, args.context_lines, args.max_results);
|
|
28
30
|
case 'run_command':
|
|
29
31
|
return await runCommand(args.command, args.cwd);
|
|
30
32
|
default:
|
|
@@ -55,14 +57,46 @@ async function editFile(filePath: string, oldText: string, newText: string): Pro
|
|
|
55
57
|
const absolutePath = path.resolve(process.cwd(), filePath);
|
|
56
58
|
const content = await fs.readFile(absolutePath, 'utf-8');
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
// Try exact match first
|
|
61
|
+
if (content.includes(oldText)) {
|
|
62
|
+
const newContent = content.replace(oldText, newText);
|
|
63
|
+
await fs.writeFile(absolutePath, newContent, 'utf-8');
|
|
64
|
+
return { success: true, result: `File edited: ${filePath}` };
|
|
60
65
|
}
|
|
61
66
|
|
|
62
|
-
|
|
63
|
-
|
|
67
|
+
// Try whitespace-normalized match
|
|
68
|
+
const normalizedOld = oldText.replace(/\s+/g, ' ').trim();
|
|
69
|
+
const lines = content.split('\n');
|
|
70
|
+
let matchStart = -1;
|
|
71
|
+
let matchEnd = -1;
|
|
64
72
|
|
|
65
|
-
|
|
73
|
+
for (let i = 0; i < lines.length; i++) {
|
|
74
|
+
for (let j = i; j < lines.length; j++) {
|
|
75
|
+
const block = lines.slice(i, j + 1).join('\n');
|
|
76
|
+
if (block.replace(/\s+/g, ' ').trim() === normalizedOld) {
|
|
77
|
+
matchStart = i;
|
|
78
|
+
matchEnd = j;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (matchStart >= 0) break;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (matchStart >= 0) {
|
|
86
|
+
const newLines = [...lines.slice(0, matchStart), ...newText.split('\n'), ...lines.slice(matchEnd + 1)];
|
|
87
|
+
await fs.writeFile(absolutePath, newLines.join('\n'), 'utf-8');
|
|
88
|
+
return { success: true, result: `File edited (fuzzy match): ${filePath}` };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { success: false, result: `Text not found in file: "${oldText.slice(0, 50)}..."` };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function applyPatchTool(patchText: string): Promise<ToolResult> {
|
|
95
|
+
const result = await applyPatch(patchText, process.cwd());
|
|
96
|
+
const summary = result.results
|
|
97
|
+
.map((r) => `${r.success ? '✓' : '✗'} ${r.file}: ${r.message}`)
|
|
98
|
+
.join('\n');
|
|
99
|
+
return { success: result.success, result: summary };
|
|
66
100
|
}
|
|
67
101
|
|
|
68
102
|
async function listFiles(dirPath: string = '.', recursive: boolean = false): Promise<ToolResult> {
|
|
@@ -98,10 +132,71 @@ async function listFiles(dirPath: string = '.', recursive: boolean = false): Pro
|
|
|
98
132
|
}
|
|
99
133
|
}
|
|
100
134
|
|
|
101
|
-
async function searchFiles(
|
|
135
|
+
async function searchFiles(
|
|
136
|
+
pattern: string,
|
|
137
|
+
dirPath: string = '.',
|
|
138
|
+
filePattern?: string,
|
|
139
|
+
contextLines: number = 2,
|
|
140
|
+
maxResults: number = 50
|
|
141
|
+
): Promise<ToolResult> {
|
|
102
142
|
const absolutePath = path.resolve(process.cwd(), dirPath);
|
|
143
|
+
|
|
144
|
+
// Try ripgrep first
|
|
145
|
+
try {
|
|
146
|
+
const rgArgs = [
|
|
147
|
+
'--json',
|
|
148
|
+
'-C', String(contextLines),
|
|
149
|
+
'-m', String(maxResults),
|
|
150
|
+
'--no-heading',
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
if (filePattern) {
|
|
154
|
+
rgArgs.push('-g', filePattern);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
rgArgs.push('--', pattern, absolutePath);
|
|
158
|
+
|
|
159
|
+
const { stdout } = await execAsync(`rg ${rgArgs.map(a => `'${a}'`).join(' ')}`, {
|
|
160
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
161
|
+
timeout: 30000,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Parse rg --json output
|
|
165
|
+
const results: string[] = [];
|
|
166
|
+
const lines = stdout.trim().split('\n');
|
|
167
|
+
|
|
168
|
+
for (const line of lines) {
|
|
169
|
+
try {
|
|
170
|
+
const data = JSON.parse(line);
|
|
171
|
+
if (data.type === 'match') {
|
|
172
|
+
const relPath = path.relative(absolutePath, data.data.path.text);
|
|
173
|
+
const lineNum = data.data.line_number;
|
|
174
|
+
const text = data.data.lines.text.trimEnd();
|
|
175
|
+
results.push(`${relPath}:${lineNum}: ${text}`);
|
|
176
|
+
} else if (data.type === 'context') {
|
|
177
|
+
const relPath = path.relative(absolutePath, data.data.path.text);
|
|
178
|
+
const lineNum = data.data.line_number;
|
|
179
|
+
const text = data.data.lines.text.trimEnd();
|
|
180
|
+
results.push(`${relPath}:${lineNum} ${text}`);
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
// Skip malformed JSON lines
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (results.length === 0) {
|
|
188
|
+
return { success: true, result: 'No matches found.' };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return { success: true, result: results.join('\n') };
|
|
192
|
+
} catch {
|
|
193
|
+
// ripgrep not available or failed, use built-in
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Built-in fallback
|
|
103
197
|
const regex = new RegExp(pattern, 'gi');
|
|
104
|
-
const results: string
|
|
198
|
+
const results: Array<{ file: string; line: number; text: string; isMatch: boolean }> = [];
|
|
199
|
+
const fileMatchCounts = new Map<string, number>();
|
|
105
200
|
|
|
106
201
|
async function searchDir(dir: string) {
|
|
107
202
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
@@ -109,7 +204,6 @@ async function searchFiles(pattern: string, dirPath: string = '.', filePattern?:
|
|
|
109
204
|
for (const entry of entries) {
|
|
110
205
|
const fullPath = path.join(dir, entry.name);
|
|
111
206
|
|
|
112
|
-
// Skip common ignored directories
|
|
113
207
|
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === 'dist') {
|
|
114
208
|
continue;
|
|
115
209
|
}
|
|
@@ -117,7 +211,6 @@ async function searchFiles(pattern: string, dirPath: string = '.', filePattern?:
|
|
|
117
211
|
if (entry.isDirectory()) {
|
|
118
212
|
await searchDir(fullPath);
|
|
119
213
|
} else {
|
|
120
|
-
// Check file pattern
|
|
121
214
|
if (filePattern) {
|
|
122
215
|
const ext = path.extname(entry.name);
|
|
123
216
|
const patternExt = filePattern.replace('*', '');
|
|
@@ -128,14 +221,38 @@ async function searchFiles(pattern: string, dirPath: string = '.', filePattern?:
|
|
|
128
221
|
|
|
129
222
|
try {
|
|
130
223
|
const content = await fs.readFile(fullPath, 'utf-8');
|
|
224
|
+
|
|
225
|
+
// Skip binary files
|
|
226
|
+
if (content.includes('\0')) continue;
|
|
227
|
+
|
|
131
228
|
const lines = content.split('\n');
|
|
229
|
+
const matchIndices: number[] = [];
|
|
132
230
|
|
|
133
231
|
lines.forEach((line, index) => {
|
|
232
|
+
regex.lastIndex = 0;
|
|
134
233
|
if (regex.test(line)) {
|
|
135
|
-
|
|
136
|
-
results.push(`${relativePath}:${index + 1}: ${line.trim()}`);
|
|
234
|
+
matchIndices.push(index);
|
|
137
235
|
}
|
|
138
236
|
});
|
|
237
|
+
|
|
238
|
+
if (matchIndices.length > 0) {
|
|
239
|
+
const relativePath = path.relative(absolutePath, fullPath);
|
|
240
|
+
fileMatchCounts.set(relativePath, matchIndices.length);
|
|
241
|
+
|
|
242
|
+
for (const idx of matchIndices) {
|
|
243
|
+
const start = Math.max(0, idx - contextLines);
|
|
244
|
+
const end = Math.min(lines.length - 1, idx + contextLines);
|
|
245
|
+
|
|
246
|
+
for (let i = start; i <= end; i++) {
|
|
247
|
+
results.push({
|
|
248
|
+
file: relativePath,
|
|
249
|
+
line: i + 1,
|
|
250
|
+
text: lines[i].trimEnd(),
|
|
251
|
+
isMatch: i === idx,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
139
256
|
} catch {
|
|
140
257
|
// Skip unreadable files
|
|
141
258
|
}
|
|
@@ -149,7 +266,15 @@ async function searchFiles(pattern: string, dirPath: string = '.', filePattern?:
|
|
|
149
266
|
return { success: true, result: 'No matches found.' };
|
|
150
267
|
}
|
|
151
268
|
|
|
152
|
-
|
|
269
|
+
const formatted = results
|
|
270
|
+
.slice(0, maxResults * (1 + contextLines * 2))
|
|
271
|
+
.map((r) => `${r.file}:${r.line}${r.isMatch ? ':' : ' '} ${r.text}`)
|
|
272
|
+
.join('\n');
|
|
273
|
+
|
|
274
|
+
const totalMatches = Array.from(fileMatchCounts.values()).reduce((a, b) => a + b, 0);
|
|
275
|
+
const footer = `\n(${totalMatches} matches in ${fileMatchCounts.size} files)`;
|
|
276
|
+
|
|
277
|
+
return { success: true, result: formatted + footer };
|
|
153
278
|
}
|
|
154
279
|
|
|
155
280
|
async function runCommand(command: string, cwd?: string): Promise<ToolResult> {
|
|
@@ -177,6 +302,7 @@ export function formatToolCall(name: string, args: Record<string, any>): string
|
|
|
177
302
|
read_file: 'Reading file',
|
|
178
303
|
write_file: 'Writing file',
|
|
179
304
|
edit_file: 'Editing file',
|
|
305
|
+
apply_patch: 'Applying patch',
|
|
180
306
|
list_files: 'Listing files',
|
|
181
307
|
search_files: 'Searching files',
|
|
182
308
|
run_command: 'Running command'
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
interface Hunk {
|
|
5
|
+
oldStart: number;
|
|
6
|
+
oldLines: string[];
|
|
7
|
+
newLines: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface FilePatch {
|
|
11
|
+
filePath: string;
|
|
12
|
+
hunks: Hunk[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface PatchResult {
|
|
16
|
+
success: boolean;
|
|
17
|
+
results: Array<{ file: string; success: boolean; message: string }>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function parsePatch(patchText: string): FilePatch[] {
|
|
21
|
+
const files: FilePatch[] = [];
|
|
22
|
+
const lines = patchText.split('\n');
|
|
23
|
+
let currentFile: FilePatch | null = null;
|
|
24
|
+
let currentHunk: Hunk | null = null;
|
|
25
|
+
|
|
26
|
+
for (let i = 0; i < lines.length; i++) {
|
|
27
|
+
const line = lines[i];
|
|
28
|
+
|
|
29
|
+
// File header: --- a/path or --- path
|
|
30
|
+
if (line.startsWith('--- ')) {
|
|
31
|
+
// Next line should be +++ b/path
|
|
32
|
+
const nextLine = lines[i + 1];
|
|
33
|
+
if (nextLine && nextLine.startsWith('+++ ')) {
|
|
34
|
+
let filePath = nextLine.slice(4).trim();
|
|
35
|
+
// Remove b/ prefix
|
|
36
|
+
if (filePath.startsWith('b/')) {
|
|
37
|
+
filePath = filePath.slice(2);
|
|
38
|
+
}
|
|
39
|
+
currentFile = { filePath, hunks: [] };
|
|
40
|
+
files.push(currentFile);
|
|
41
|
+
i++; // skip the +++ line
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Hunk header: @@ -start,count +start,count @@
|
|
47
|
+
const hunkMatch = line.match(/^@@ -(\d+)(?:,\d+)? \+\d+(?:,\d+)? @@/);
|
|
48
|
+
if (hunkMatch && currentFile) {
|
|
49
|
+
currentHunk = {
|
|
50
|
+
oldStart: parseInt(hunkMatch[1], 10),
|
|
51
|
+
oldLines: [],
|
|
52
|
+
newLines: [],
|
|
53
|
+
};
|
|
54
|
+
currentFile.hunks.push(currentHunk);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Hunk content
|
|
59
|
+
if (currentHunk) {
|
|
60
|
+
if (line.startsWith('-')) {
|
|
61
|
+
currentHunk.oldLines.push(line.slice(1));
|
|
62
|
+
} else if (line.startsWith('+')) {
|
|
63
|
+
currentHunk.newLines.push(line.slice(1));
|
|
64
|
+
} else if (line.startsWith(' ')) {
|
|
65
|
+
// Context line - appears in both old and new
|
|
66
|
+
currentHunk.oldLines.push(line.slice(1));
|
|
67
|
+
currentHunk.newLines.push(line.slice(1));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return files;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function findHunkPosition(fileLines: string[], hunkOldLines: string[], expectedStart: number, drift: number = 20): number {
|
|
76
|
+
// Try exact position first (0-indexed)
|
|
77
|
+
const start = expectedStart - 1;
|
|
78
|
+
if (matchesAt(fileLines, hunkOldLines, start)) {
|
|
79
|
+
return start;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Fuzzy search within ±drift lines
|
|
83
|
+
for (let offset = 1; offset <= drift; offset++) {
|
|
84
|
+
if (matchesAt(fileLines, hunkOldLines, start + offset)) {
|
|
85
|
+
return start + offset;
|
|
86
|
+
}
|
|
87
|
+
if (matchesAt(fileLines, hunkOldLines, start - offset)) {
|
|
88
|
+
return start - offset;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return -1;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function matchesAt(fileLines: string[], hunkOldLines: string[], position: number): boolean {
|
|
96
|
+
if (position < 0 || position + hunkOldLines.length > fileLines.length) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (let i = 0; i < hunkOldLines.length; i++) {
|
|
101
|
+
// Normalize whitespace for comparison
|
|
102
|
+
const fileLine = fileLines[position + i].trimEnd();
|
|
103
|
+
const hunkLine = hunkOldLines[i].trimEnd();
|
|
104
|
+
if (fileLine !== hunkLine) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function applyPatch(patchText: string, basePath: string): Promise<PatchResult> {
|
|
113
|
+
const filePatches = parsePatch(patchText);
|
|
114
|
+
const results: PatchResult['results'] = [];
|
|
115
|
+
let allSuccess = true;
|
|
116
|
+
|
|
117
|
+
for (const filePatch of filePatches) {
|
|
118
|
+
const absolutePath = path.resolve(basePath, filePatch.filePath);
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
let content: string;
|
|
122
|
+
try {
|
|
123
|
+
content = await fs.readFile(absolutePath, 'utf-8');
|
|
124
|
+
} catch {
|
|
125
|
+
// File doesn't exist - if all hunks are additions, create it
|
|
126
|
+
const allAdditions = filePatch.hunks.every((h) => h.oldLines.length === 0);
|
|
127
|
+
if (allAdditions) {
|
|
128
|
+
const newContent = filePatch.hunks.map((h) => h.newLines.join('\n')).join('\n');
|
|
129
|
+
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
|
|
130
|
+
await fs.writeFile(absolutePath, newContent, 'utf-8');
|
|
131
|
+
results.push({ file: filePatch.filePath, success: true, message: 'Created new file' });
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
throw new Error(`File not found: ${filePatch.filePath}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let fileLines = content.split('\n');
|
|
138
|
+
|
|
139
|
+
// Apply hunks in reverse order to preserve line numbers
|
|
140
|
+
const sortedHunks = [...filePatch.hunks].sort((a, b) => b.oldStart - a.oldStart);
|
|
141
|
+
|
|
142
|
+
for (const hunk of sortedHunks) {
|
|
143
|
+
const position = findHunkPosition(fileLines, hunk.oldLines, hunk.oldStart);
|
|
144
|
+
if (position === -1) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`Could not find match for hunk at line ${hunk.oldStart} in ${filePatch.filePath}`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Replace old lines with new lines
|
|
151
|
+
fileLines.splice(position, hunk.oldLines.length, ...hunk.newLines);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
await fs.writeFile(absolutePath, fileLines.join('\n'), 'utf-8');
|
|
155
|
+
results.push({
|
|
156
|
+
file: filePatch.filePath,
|
|
157
|
+
success: true,
|
|
158
|
+
message: `Applied ${filePatch.hunks.length} hunk(s)`,
|
|
159
|
+
});
|
|
160
|
+
} catch (error: any) {
|
|
161
|
+
allSuccess = false;
|
|
162
|
+
results.push({ file: filePatch.filePath, success: false, message: error.message });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return { success: allSuccess, results };
|
|
167
|
+
}
|
package/src/utils/api.ts
CHANGED
|
@@ -59,7 +59,7 @@ export const fileTools = [
|
|
|
59
59
|
type: 'function',
|
|
60
60
|
function: {
|
|
61
61
|
name: 'edit_file',
|
|
62
|
-
description: 'Make targeted edits to a file by replacing specific text',
|
|
62
|
+
description: 'Make targeted edits to a file by replacing specific text. For small single-location changes.',
|
|
63
63
|
parameters: {
|
|
64
64
|
type: 'object',
|
|
65
65
|
properties: {
|
|
@@ -71,6 +71,23 @@ export const fileTools = [
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
},
|
|
74
|
+
{
|
|
75
|
+
type: 'function',
|
|
76
|
+
function: {
|
|
77
|
+
name: 'apply_patch',
|
|
78
|
+
description: 'Apply a unified diff patch to one or more files. Preferred for multi-line or multi-file changes. Uses standard unified diff format with fuzzy line matching (±20 line drift).',
|
|
79
|
+
parameters: {
|
|
80
|
+
type: 'object',
|
|
81
|
+
properties: {
|
|
82
|
+
patch: {
|
|
83
|
+
type: 'string',
|
|
84
|
+
description: 'The unified diff patch text. Must include --- a/file and +++ b/file headers and @@ hunk headers.'
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
required: ['patch']
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
},
|
|
74
91
|
{
|
|
75
92
|
type: 'function',
|
|
76
93
|
function: {
|
|
@@ -89,13 +106,15 @@ export const fileTools = [
|
|
|
89
106
|
type: 'function',
|
|
90
107
|
function: {
|
|
91
108
|
name: 'search_files',
|
|
92
|
-
description: 'Search for text patterns across files',
|
|
109
|
+
description: 'Search for text patterns across files. Uses ripgrep when available for fast results with context lines.',
|
|
93
110
|
parameters: {
|
|
94
111
|
type: 'object',
|
|
95
112
|
properties: {
|
|
96
113
|
pattern: { type: 'string', description: 'The search pattern (regex supported)' },
|
|
97
114
|
path: { type: 'string', description: 'Directory to search in (default: current)' },
|
|
98
|
-
file_pattern: { type: 'string', description: 'File glob pattern (e.g., "*.ts")' }
|
|
115
|
+
file_pattern: { type: 'string', description: 'File glob pattern (e.g., "*.ts")' },
|
|
116
|
+
context_lines: { type: 'number', description: 'Number of context lines around matches (default: 2)' },
|
|
117
|
+
max_results: { type: 'number', description: 'Maximum number of matches to return (default: 50)' }
|
|
99
118
|
},
|
|
100
119
|
required: ['pattern']
|
|
101
120
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type ApprovalMode = 'suggest' | 'auto-edit' | 'full-auto';
|
|
2
|
+
export type ToolCategory = 'read_only' | 'file_write' | 'shell';
|
|
3
|
+
|
|
4
|
+
const TOOL_CATEGORIES: Record<string, ToolCategory> = {
|
|
5
|
+
read_file: 'read_only',
|
|
6
|
+
list_files: 'read_only',
|
|
7
|
+
search_files: 'read_only',
|
|
8
|
+
write_file: 'file_write',
|
|
9
|
+
edit_file: 'file_write',
|
|
10
|
+
apply_patch: 'file_write',
|
|
11
|
+
run_command: 'shell',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function categorize(toolName: string): ToolCategory {
|
|
15
|
+
return TOOL_CATEGORIES[toolName] || 'shell';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function needsApproval(toolName: string, mode: ApprovalMode): boolean {
|
|
19
|
+
const category = categorize(toolName);
|
|
20
|
+
|
|
21
|
+
switch (mode) {
|
|
22
|
+
case 'full-auto':
|
|
23
|
+
return false;
|
|
24
|
+
case 'auto-edit':
|
|
25
|
+
return category === 'shell';
|
|
26
|
+
case 'suggest':
|
|
27
|
+
return category === 'file_write' || category === 'shell';
|
|
28
|
+
default:
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/utils/context.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { promisify } from 'util';
|
|
|
5
5
|
|
|
6
6
|
const execAsync = promisify(exec);
|
|
7
7
|
|
|
8
|
-
export function buildSystemMessage(model: string, codebaseContext: string): string {
|
|
8
|
+
export function buildSystemMessage(model: string, codebaseContext: string, projectInstructions?: string): string {
|
|
9
9
|
let systemMessage = `You are Codea, an expert AI coding assistant created by Alia. You help developers write, debug, refactor, and understand code directly in their terminal.
|
|
10
10
|
|
|
11
11
|
## Core Principles
|
|
@@ -19,16 +19,18 @@ You have powerful tools to interact with the user's workspace:
|
|
|
19
19
|
|
|
20
20
|
- **read_file**: Read file contents. Use to understand existing code before making changes.
|
|
21
21
|
- **write_file**: Create new files or completely rewrite existing ones.
|
|
22
|
-
- **edit_file**: Make precise, targeted changes
|
|
22
|
+
- **edit_file**: Make precise, targeted changes using exact text match and replace.
|
|
23
|
+
- **apply_patch**: Apply unified diff patches to files. Preferred for multi-line or multi-file changes. Supports fuzzy line matching.
|
|
23
24
|
- **list_files**: Explore directory structure. Use to understand project layout.
|
|
24
|
-
- **search_files**: Find text/patterns across the codebase
|
|
25
|
+
- **search_files**: Find text/patterns across the codebase with context lines. Uses ripgrep when available.
|
|
25
26
|
- **run_command**: Execute shell commands (build, test, git, npm, etc.)
|
|
26
27
|
|
|
27
28
|
## Best Practices
|
|
28
29
|
1. **Read before writing**: Always read relevant files before modifying them.
|
|
29
30
|
2. **Minimal changes**: Make the smallest change necessary to accomplish the task.
|
|
30
31
|
3. **Preserve style**: Match existing formatting, naming conventions, and patterns.
|
|
31
|
-
4. **
|
|
32
|
+
4. **Prefer apply_patch**: For multi-line edits, use apply_patch with unified diff format.
|
|
33
|
+
5. **Explain when helpful**: For complex changes, briefly explain the approach.
|
|
32
34
|
|
|
33
35
|
## Response Style
|
|
34
36
|
- Use markdown for formatting code blocks, lists, and emphasis.
|
|
@@ -36,6 +38,10 @@ You have powerful tools to interact with the user's workspace:
|
|
|
36
38
|
- For code changes, be precise and action-oriented.
|
|
37
39
|
- If unsure about requirements, ask clarifying questions.`;
|
|
38
40
|
|
|
41
|
+
if (projectInstructions) {
|
|
42
|
+
systemMessage += `\n\n## Project Instructions (from CODEA.md)\n${projectInstructions}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
39
45
|
if (codebaseContext) {
|
|
40
46
|
systemMessage += `\n\n## Current Codebase Context\n${codebaseContext}`;
|
|
41
47
|
}
|
|
@@ -43,6 +49,61 @@ You have powerful tools to interact with the user's workspace:
|
|
|
43
49
|
return systemMessage;
|
|
44
50
|
}
|
|
45
51
|
|
|
52
|
+
export async function loadProjectInstructions(): Promise<string> {
|
|
53
|
+
const parts: string[] = [];
|
|
54
|
+
|
|
55
|
+
// 1. Global: ~/.codea/CODEA.md
|
|
56
|
+
const home = process.env.HOME || '';
|
|
57
|
+
if (home) {
|
|
58
|
+
const globalPath = path.join(home, '.codea', 'CODEA.md');
|
|
59
|
+
try {
|
|
60
|
+
const content = await fs.readFile(globalPath, 'utf-8');
|
|
61
|
+
if (content.trim()) {
|
|
62
|
+
parts.push(`# Global Instructions (~/.codea/CODEA.md)\n${content.trim()}`);
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// No global instructions
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 2. Project root: {git_root}/CODEA.md
|
|
70
|
+
let gitRoot = '';
|
|
71
|
+
try {
|
|
72
|
+
const { stdout } = await execAsync('git rev-parse --show-toplevel', { cwd: process.cwd() });
|
|
73
|
+
gitRoot = stdout.trim();
|
|
74
|
+
} catch {
|
|
75
|
+
// Not a git repo
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (gitRoot) {
|
|
79
|
+
const projectPath = path.join(gitRoot, 'CODEA.md');
|
|
80
|
+
try {
|
|
81
|
+
const content = await fs.readFile(projectPath, 'utf-8');
|
|
82
|
+
if (content.trim()) {
|
|
83
|
+
parts.push(`# Project Instructions (CODEA.md)\n${content.trim()}`);
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
// No project instructions
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 3. Directory-level: {cwd}/CODEA.md (if different from git root)
|
|
91
|
+
const cwd = process.cwd();
|
|
92
|
+
if (cwd !== gitRoot) {
|
|
93
|
+
const dirPath = path.join(cwd, 'CODEA.md');
|
|
94
|
+
try {
|
|
95
|
+
const content = await fs.readFile(dirPath, 'utf-8');
|
|
96
|
+
if (content.trim()) {
|
|
97
|
+
parts.push(`# Directory Instructions (./CODEA.md)\n${content.trim()}`);
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
// No directory instructions
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return parts.join('\n\n---\n\n');
|
|
105
|
+
}
|
|
106
|
+
|
|
46
107
|
export async function getCodebaseContext(): Promise<string> {
|
|
47
108
|
const cwd = process.cwd();
|
|
48
109
|
const contextParts: string[] = [];
|