@alia-codea/cli 1.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/README.md +131 -0
- package/dist/api-X2G5QROW.js +10 -0
- package/dist/chunk-SVPL4GNV.js +230 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +878 -0
- package/package.json +52 -0
- package/src/commands/auth.ts +66 -0
- package/src/commands/repl.ts +309 -0
- package/src/commands/run.ts +177 -0
- package/src/commands/sessions.ts +122 -0
- package/src/index.ts +87 -0
- package/src/tools/executor.ts +191 -0
- package/src/utils/api.ts +213 -0
- package/src/utils/config.ts +70 -0
- package/src/utils/context.ts +127 -0
- package/src/utils/ui.ts +153 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import * as readline from 'readline';
|
|
3
|
+
import { getSessions, getSession, config } from '../utils/config.js';
|
|
4
|
+
import { startRepl } from './repl.js';
|
|
5
|
+
import { printBanner, printError, printInfo } from '../utils/ui.js';
|
|
6
|
+
|
|
7
|
+
export async function listSessions(): Promise<void> {
|
|
8
|
+
const sessions = getSessions();
|
|
9
|
+
|
|
10
|
+
if (sessions.length === 0) {
|
|
11
|
+
printInfo('No saved sessions found.');
|
|
12
|
+
console.log(chalk.gray('Start a new session with: ') + chalk.cyan('codea'));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
console.log();
|
|
17
|
+
console.log(chalk.bold('Recent Sessions'));
|
|
18
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
19
|
+
|
|
20
|
+
sessions.slice(0, 10).forEach((session, index) => {
|
|
21
|
+
const date = new Date(session.updatedAt).toLocaleDateString();
|
|
22
|
+
const time = new Date(session.updatedAt).toLocaleTimeString();
|
|
23
|
+
const messageCount = session.messages?.length || 0;
|
|
24
|
+
const title = session.title.slice(0, 40) + (session.title.length > 40 ? '...' : '');
|
|
25
|
+
|
|
26
|
+
console.log(
|
|
27
|
+
chalk.cyan(`${index + 1}.`) + ' ' +
|
|
28
|
+
chalk.white(title) + ' ' +
|
|
29
|
+
chalk.gray(`(${messageCount} msgs, ${date} ${time})`)
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
console.log();
|
|
34
|
+
console.log(chalk.gray('Resume a session with: ') + chalk.cyan('codea resume <number>'));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function resumeSession(sessionId?: string): Promise<void> {
|
|
38
|
+
const sessions = getSessions();
|
|
39
|
+
|
|
40
|
+
if (sessions.length === 0) {
|
|
41
|
+
printInfo('No saved sessions found.');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let selectedSession;
|
|
46
|
+
|
|
47
|
+
if (sessionId) {
|
|
48
|
+
// Try to find by index (1-based) or ID
|
|
49
|
+
const index = parseInt(sessionId) - 1;
|
|
50
|
+
if (!isNaN(index) && index >= 0 && index < sessions.length) {
|
|
51
|
+
selectedSession = sessions[index];
|
|
52
|
+
} else {
|
|
53
|
+
selectedSession = getSession(sessionId);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!selectedSession) {
|
|
57
|
+
printError(`Session not found: ${sessionId}`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
// Show picker
|
|
62
|
+
console.log();
|
|
63
|
+
console.log(chalk.bold('Select a session to resume:'));
|
|
64
|
+
console.log();
|
|
65
|
+
|
|
66
|
+
sessions.slice(0, 10).forEach((session, index) => {
|
|
67
|
+
const date = new Date(session.updatedAt).toLocaleDateString();
|
|
68
|
+
const title = session.title.slice(0, 50) + (session.title.length > 50 ? '...' : '');
|
|
69
|
+
console.log(chalk.cyan(` ${index + 1}.`) + ' ' + title + ' ' + chalk.gray(`(${date})`));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
console.log();
|
|
73
|
+
|
|
74
|
+
const rl = readline.createInterface({
|
|
75
|
+
input: process.stdin,
|
|
76
|
+
output: process.stdout
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return new Promise((resolve) => {
|
|
80
|
+
rl.question(chalk.cyan('Enter number: '), (answer) => {
|
|
81
|
+
rl.close();
|
|
82
|
+
|
|
83
|
+
const index = parseInt(answer) - 1;
|
|
84
|
+
if (isNaN(index) || index < 0 || index >= sessions.length) {
|
|
85
|
+
printError('Invalid selection.');
|
|
86
|
+
resolve();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
selectedSession = sessions[index];
|
|
91
|
+
startRestoredSession(selectedSession).then(resolve);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (selectedSession) {
|
|
97
|
+
await startRestoredSession(selectedSession);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function startRestoredSession(session: any): Promise<void> {
|
|
102
|
+
printInfo(`Resuming: ${session.title}`);
|
|
103
|
+
console.log();
|
|
104
|
+
|
|
105
|
+
// Display previous messages
|
|
106
|
+
for (const msg of session.messages || []) {
|
|
107
|
+
if (msg.role === 'user') {
|
|
108
|
+
console.log(chalk.cyan('❯ ') + msg.content);
|
|
109
|
+
} else if (msg.role === 'assistant') {
|
|
110
|
+
console.log(chalk.magenta('✦ ') + msg.content.slice(0, 200) + (msg.content.length > 200 ? '...' : ''));
|
|
111
|
+
}
|
|
112
|
+
console.log();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
116
|
+
console.log(chalk.gray('Session restored. Continue the conversation below.'));
|
|
117
|
+
console.log();
|
|
118
|
+
|
|
119
|
+
// Start REPL with restored session
|
|
120
|
+
const model = config.get('defaultModel') || 'alia-v1-codea';
|
|
121
|
+
await startRepl({ model, context: true });
|
|
122
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { config } from './utils/config.js';
|
|
5
|
+
import { startRepl } from './commands/repl.js';
|
|
6
|
+
import { runPrompt } from './commands/run.js';
|
|
7
|
+
import { login } from './commands/auth.js';
|
|
8
|
+
import { listSessions, resumeSession } from './commands/sessions.js';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
|
|
11
|
+
const VERSION = '1.0.0';
|
|
12
|
+
|
|
13
|
+
const program = new Command();
|
|
14
|
+
|
|
15
|
+
// ASCII art banner
|
|
16
|
+
const banner = `
|
|
17
|
+
${chalk.cyan(' ____ _ ')}
|
|
18
|
+
${chalk.cyan(' / ___|___ __| | ___ __ _ ')}
|
|
19
|
+
${chalk.cyan(' | | / _ \\ / _\` |/ _ \\/ _\` |')}
|
|
20
|
+
${chalk.cyan(' | |__| (_) | (_| | __/ (_| |')}
|
|
21
|
+
${chalk.cyan(' \\____\\___/ \\__,_|\\___|\\__,_|')}
|
|
22
|
+
${chalk.gray(' AI Coding Assistant by Alia')}
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
program
|
|
26
|
+
.name('codea')
|
|
27
|
+
.description('Codea CLI - AI coding assistant for your terminal')
|
|
28
|
+
.version(VERSION)
|
|
29
|
+
.hook('preAction', () => {
|
|
30
|
+
// Check for API key before running commands (except login)
|
|
31
|
+
const command = program.args[0];
|
|
32
|
+
if (command !== 'login' && command !== 'help' && !config.get('apiKey')) {
|
|
33
|
+
console.log(chalk.yellow('\nNo API key found. Please run `codea login` first.\n'));
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Default command - start REPL
|
|
39
|
+
program
|
|
40
|
+
.command('chat', { isDefault: true })
|
|
41
|
+
.description('Start an interactive chat session')
|
|
42
|
+
.option('-m, --model <model>', 'Model to use (codea, codea-pro, codea-thinking)', 'alia-v1-codea')
|
|
43
|
+
.option('--no-context', 'Disable automatic codebase context')
|
|
44
|
+
.action(async (options) => {
|
|
45
|
+
console.log(banner);
|
|
46
|
+
await startRepl(options);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Run a single prompt
|
|
50
|
+
program
|
|
51
|
+
.command('run <prompt>')
|
|
52
|
+
.alias('r')
|
|
53
|
+
.description('Run a single prompt and exit')
|
|
54
|
+
.option('-m, --model <model>', 'Model to use', 'alia-v1-codea')
|
|
55
|
+
.option('-y, --yes', 'Auto-approve all file changes')
|
|
56
|
+
.option('--no-context', 'Disable automatic codebase context')
|
|
57
|
+
.action(async (prompt, options) => {
|
|
58
|
+
await runPrompt(prompt, options);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Login/configure
|
|
62
|
+
program
|
|
63
|
+
.command('login')
|
|
64
|
+
.description('Configure your Alia API key')
|
|
65
|
+
.action(async () => {
|
|
66
|
+
await login();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Session management
|
|
70
|
+
program
|
|
71
|
+
.command('sessions')
|
|
72
|
+
.alias('s')
|
|
73
|
+
.description('List recent chat sessions')
|
|
74
|
+
.action(async () => {
|
|
75
|
+
await listSessions();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
program
|
|
79
|
+
.command('resume [sessionId]')
|
|
80
|
+
.description('Resume a previous chat session')
|
|
81
|
+
.action(async (sessionId) => {
|
|
82
|
+
console.log(banner);
|
|
83
|
+
await resumeSession(sessionId);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Parse and run
|
|
87
|
+
program.parse();
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { exec } from 'child_process';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
import { glob } from 'fs/promises';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
|
|
10
|
+
interface ToolResult {
|
|
11
|
+
success: boolean;
|
|
12
|
+
result: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function executeTool(name: string, args: Record<string, any>): Promise<ToolResult> {
|
|
16
|
+
try {
|
|
17
|
+
switch (name) {
|
|
18
|
+
case 'read_file':
|
|
19
|
+
return await readFile(args.path);
|
|
20
|
+
case 'write_file':
|
|
21
|
+
return await writeFile(args.path, args.content);
|
|
22
|
+
case 'edit_file':
|
|
23
|
+
return await editFile(args.path, args.old_text, args.new_text);
|
|
24
|
+
case 'list_files':
|
|
25
|
+
return await listFiles(args.path, args.recursive);
|
|
26
|
+
case 'search_files':
|
|
27
|
+
return await searchFiles(args.pattern, args.path, args.file_pattern);
|
|
28
|
+
case 'run_command':
|
|
29
|
+
return await runCommand(args.command, args.cwd);
|
|
30
|
+
default:
|
|
31
|
+
return { success: false, result: `Unknown tool: ${name}` };
|
|
32
|
+
}
|
|
33
|
+
} catch (error: any) {
|
|
34
|
+
return { success: false, result: error.message };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function readFile(filePath: string): Promise<ToolResult> {
|
|
39
|
+
const absolutePath = path.resolve(process.cwd(), filePath);
|
|
40
|
+
const content = await fs.readFile(absolutePath, 'utf-8');
|
|
41
|
+
return { success: true, result: content };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function writeFile(filePath: string, content: string): Promise<ToolResult> {
|
|
45
|
+
const absolutePath = path.resolve(process.cwd(), filePath);
|
|
46
|
+
|
|
47
|
+
// Ensure directory exists
|
|
48
|
+
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
|
|
49
|
+
await fs.writeFile(absolutePath, content, 'utf-8');
|
|
50
|
+
|
|
51
|
+
return { success: true, result: `File written: ${filePath}` };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function editFile(filePath: string, oldText: string, newText: string): Promise<ToolResult> {
|
|
55
|
+
const absolutePath = path.resolve(process.cwd(), filePath);
|
|
56
|
+
const content = await fs.readFile(absolutePath, 'utf-8');
|
|
57
|
+
|
|
58
|
+
if (!content.includes(oldText)) {
|
|
59
|
+
return { success: false, result: `Text not found in file: "${oldText.slice(0, 50)}..."` };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const newContent = content.replace(oldText, newText);
|
|
63
|
+
await fs.writeFile(absolutePath, newContent, 'utf-8');
|
|
64
|
+
|
|
65
|
+
return { success: true, result: `File edited: ${filePath}` };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function listFiles(dirPath: string = '.', recursive: boolean = false): Promise<ToolResult> {
|
|
69
|
+
const absolutePath = path.resolve(process.cwd(), dirPath);
|
|
70
|
+
|
|
71
|
+
if (recursive) {
|
|
72
|
+
const files: string[] = [];
|
|
73
|
+
async function walk(dir: string) {
|
|
74
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
const fullPath = path.join(dir, entry.name);
|
|
77
|
+
const relativePath = path.relative(absolutePath, fullPath);
|
|
78
|
+
|
|
79
|
+
// Skip common ignored directories
|
|
80
|
+
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === 'dist') {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (entry.isDirectory()) {
|
|
85
|
+
files.push(relativePath + '/');
|
|
86
|
+
await walk(fullPath);
|
|
87
|
+
} else {
|
|
88
|
+
files.push(relativePath);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
await walk(absolutePath);
|
|
93
|
+
return { success: true, result: files.join('\n') };
|
|
94
|
+
} else {
|
|
95
|
+
const entries = await fs.readdir(absolutePath, { withFileTypes: true });
|
|
96
|
+
const files = entries.map(e => e.name + (e.isDirectory() ? '/' : ''));
|
|
97
|
+
return { success: true, result: files.join('\n') };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function searchFiles(pattern: string, dirPath: string = '.', filePattern?: string): Promise<ToolResult> {
|
|
102
|
+
const absolutePath = path.resolve(process.cwd(), dirPath);
|
|
103
|
+
const regex = new RegExp(pattern, 'gi');
|
|
104
|
+
const results: string[] = [];
|
|
105
|
+
|
|
106
|
+
async function searchDir(dir: string) {
|
|
107
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
108
|
+
|
|
109
|
+
for (const entry of entries) {
|
|
110
|
+
const fullPath = path.join(dir, entry.name);
|
|
111
|
+
|
|
112
|
+
// Skip common ignored directories
|
|
113
|
+
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === 'dist') {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (entry.isDirectory()) {
|
|
118
|
+
await searchDir(fullPath);
|
|
119
|
+
} else {
|
|
120
|
+
// Check file pattern
|
|
121
|
+
if (filePattern) {
|
|
122
|
+
const ext = path.extname(entry.name);
|
|
123
|
+
const patternExt = filePattern.replace('*', '');
|
|
124
|
+
if (ext !== patternExt && !entry.name.match(filePattern.replace('*', '.*'))) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
131
|
+
const lines = content.split('\n');
|
|
132
|
+
|
|
133
|
+
lines.forEach((line, index) => {
|
|
134
|
+
if (regex.test(line)) {
|
|
135
|
+
const relativePath = path.relative(absolutePath, fullPath);
|
|
136
|
+
results.push(`${relativePath}:${index + 1}: ${line.trim()}`);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
} catch {
|
|
140
|
+
// Skip unreadable files
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
await searchDir(absolutePath);
|
|
147
|
+
|
|
148
|
+
if (results.length === 0) {
|
|
149
|
+
return { success: true, result: 'No matches found.' };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { success: true, result: results.slice(0, 100).join('\n') + (results.length > 100 ? `\n... and ${results.length - 100} more` : '') };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function runCommand(command: string, cwd?: string): Promise<ToolResult> {
|
|
156
|
+
const workingDir = cwd ? path.resolve(process.cwd(), cwd) : process.cwd();
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const { stdout, stderr } = await execAsync(command, {
|
|
160
|
+
cwd: workingDir,
|
|
161
|
+
maxBuffer: 1024 * 1024,
|
|
162
|
+
timeout: 60000
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const output = stdout + (stderr ? `\nStderr:\n${stderr}` : '');
|
|
166
|
+
return { success: true, result: output || 'Command completed successfully.' };
|
|
167
|
+
} catch (error: any) {
|
|
168
|
+
return {
|
|
169
|
+
success: false,
|
|
170
|
+
result: error.stdout + (error.stderr ? `\nStderr:\n${error.stderr}` : '') || error.message
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function formatToolCall(name: string, args: Record<string, any>): string {
|
|
176
|
+
const labels: Record<string, string> = {
|
|
177
|
+
read_file: 'Reading file',
|
|
178
|
+
write_file: 'Writing file',
|
|
179
|
+
edit_file: 'Editing file',
|
|
180
|
+
list_files: 'Listing files',
|
|
181
|
+
search_files: 'Searching files',
|
|
182
|
+
run_command: 'Running command'
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const label = labels[name] || name;
|
|
186
|
+
const argStr = Object.entries(args)
|
|
187
|
+
.map(([k, v]) => `${k}: ${typeof v === 'string' ? v.slice(0, 50) : v}`)
|
|
188
|
+
.join(', ');
|
|
189
|
+
|
|
190
|
+
return `${chalk.cyan('→')} ${chalk.bold(label)}: ${chalk.gray(argStr)}`;
|
|
191
|
+
}
|
package/src/utils/api.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import { config } from './config.js';
|
|
3
|
+
|
|
4
|
+
interface Message {
|
|
5
|
+
role: 'user' | 'assistant' | 'system' | 'tool';
|
|
6
|
+
content: string;
|
|
7
|
+
tool_calls?: ToolCall[];
|
|
8
|
+
tool_call_id?: string;
|
|
9
|
+
name?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ToolCall {
|
|
13
|
+
id: string;
|
|
14
|
+
type: 'function';
|
|
15
|
+
function: {
|
|
16
|
+
name: string;
|
|
17
|
+
arguments: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface StreamCallbacks {
|
|
22
|
+
onContent: (content: string) => void;
|
|
23
|
+
onToolCall: (toolCall: ToolCall) => void;
|
|
24
|
+
onDone: (content: string, toolCalls?: ToolCall[]) => void;
|
|
25
|
+
onError: (error: Error) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const fileTools = [
|
|
29
|
+
{
|
|
30
|
+
type: 'function',
|
|
31
|
+
function: {
|
|
32
|
+
name: 'read_file',
|
|
33
|
+
description: 'Read the contents of a file',
|
|
34
|
+
parameters: {
|
|
35
|
+
type: 'object',
|
|
36
|
+
properties: {
|
|
37
|
+
path: { type: 'string', description: 'The file path to read' }
|
|
38
|
+
},
|
|
39
|
+
required: ['path']
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
type: 'function',
|
|
45
|
+
function: {
|
|
46
|
+
name: 'write_file',
|
|
47
|
+
description: 'Write content to a file (creates or overwrites)',
|
|
48
|
+
parameters: {
|
|
49
|
+
type: 'object',
|
|
50
|
+
properties: {
|
|
51
|
+
path: { type: 'string', description: 'The file path to write to' },
|
|
52
|
+
content: { type: 'string', description: 'The content to write' }
|
|
53
|
+
},
|
|
54
|
+
required: ['path', 'content']
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
type: 'function',
|
|
60
|
+
function: {
|
|
61
|
+
name: 'edit_file',
|
|
62
|
+
description: 'Make targeted edits to a file by replacing specific text',
|
|
63
|
+
parameters: {
|
|
64
|
+
type: 'object',
|
|
65
|
+
properties: {
|
|
66
|
+
path: { type: 'string', description: 'The file path to edit' },
|
|
67
|
+
old_text: { type: 'string', description: 'The text to find and replace' },
|
|
68
|
+
new_text: { type: 'string', description: 'The replacement text' }
|
|
69
|
+
},
|
|
70
|
+
required: ['path', 'old_text', 'new_text']
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
type: 'function',
|
|
76
|
+
function: {
|
|
77
|
+
name: 'list_files',
|
|
78
|
+
description: 'List files in a directory',
|
|
79
|
+
parameters: {
|
|
80
|
+
type: 'object',
|
|
81
|
+
properties: {
|
|
82
|
+
path: { type: 'string', description: 'The directory path (default: current directory)' },
|
|
83
|
+
recursive: { type: 'boolean', description: 'Whether to list recursively' }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
type: 'function',
|
|
90
|
+
function: {
|
|
91
|
+
name: 'search_files',
|
|
92
|
+
description: 'Search for text patterns across files',
|
|
93
|
+
parameters: {
|
|
94
|
+
type: 'object',
|
|
95
|
+
properties: {
|
|
96
|
+
pattern: { type: 'string', description: 'The search pattern (regex supported)' },
|
|
97
|
+
path: { type: 'string', description: 'Directory to search in (default: current)' },
|
|
98
|
+
file_pattern: { type: 'string', description: 'File glob pattern (e.g., "*.ts")' }
|
|
99
|
+
},
|
|
100
|
+
required: ['pattern']
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
type: 'function',
|
|
106
|
+
function: {
|
|
107
|
+
name: 'run_command',
|
|
108
|
+
description: 'Execute a shell command',
|
|
109
|
+
parameters: {
|
|
110
|
+
type: 'object',
|
|
111
|
+
properties: {
|
|
112
|
+
command: { type: 'string', description: 'The command to execute' },
|
|
113
|
+
cwd: { type: 'string', description: 'Working directory (default: current)' }
|
|
114
|
+
},
|
|
115
|
+
required: ['command']
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
export async function streamChat(
|
|
122
|
+
messages: Message[],
|
|
123
|
+
systemMessage: string,
|
|
124
|
+
model: string,
|
|
125
|
+
callbacks: StreamCallbacks
|
|
126
|
+
): Promise<void> {
|
|
127
|
+
const apiKey = config.get('apiKey') as string;
|
|
128
|
+
const baseUrl = config.get('apiBaseUrl') as string || 'https://api.alia.onl';
|
|
129
|
+
|
|
130
|
+
const openai = new OpenAI({
|
|
131
|
+
apiKey,
|
|
132
|
+
baseURL: `${baseUrl}/v1`
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const allMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
|
136
|
+
{ role: 'system', content: systemMessage },
|
|
137
|
+
...messages.map(m => {
|
|
138
|
+
if (m.role === 'tool') {
|
|
139
|
+
return { role: 'tool', tool_call_id: m.tool_call_id!, content: m.content };
|
|
140
|
+
} else if (m.tool_calls) {
|
|
141
|
+
return { role: 'assistant', content: m.content || '', tool_calls: m.tool_calls as any };
|
|
142
|
+
}
|
|
143
|
+
return { role: m.role, content: m.content };
|
|
144
|
+
})
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const stream = await openai.chat.completions.create({
|
|
149
|
+
model,
|
|
150
|
+
messages: allMessages,
|
|
151
|
+
tools: fileTools as OpenAI.Chat.ChatCompletionTool[],
|
|
152
|
+
stream: true
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
let fullContent = '';
|
|
156
|
+
const toolCalls: ToolCall[] = [];
|
|
157
|
+
const toolCallsMap = new Map<number, ToolCall>();
|
|
158
|
+
|
|
159
|
+
for await (const chunk of stream) {
|
|
160
|
+
const delta = chunk.choices?.[0]?.delta;
|
|
161
|
+
if (!delta) continue;
|
|
162
|
+
|
|
163
|
+
if (delta.content) {
|
|
164
|
+
fullContent += delta.content;
|
|
165
|
+
callbacks.onContent(delta.content);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (delta.tool_calls) {
|
|
169
|
+
for (const tc of delta.tool_calls) {
|
|
170
|
+
const index = tc.index ?? 0;
|
|
171
|
+
|
|
172
|
+
if (!toolCallsMap.has(index)) {
|
|
173
|
+
const newToolCall: ToolCall = {
|
|
174
|
+
id: tc.id || '',
|
|
175
|
+
type: 'function',
|
|
176
|
+
function: {
|
|
177
|
+
name: tc.function?.name || '',
|
|
178
|
+
arguments: tc.function?.arguments || ''
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
toolCallsMap.set(index, newToolCall);
|
|
182
|
+
toolCalls.push(newToolCall);
|
|
183
|
+
} else {
|
|
184
|
+
const existingToolCall = toolCallsMap.get(index)!;
|
|
185
|
+
if (tc.function?.name) {
|
|
186
|
+
existingToolCall.function.name = tc.function.name;
|
|
187
|
+
}
|
|
188
|
+
if (tc.function?.arguments) {
|
|
189
|
+
existingToolCall.function.arguments += tc.function.arguments;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
callbacks.onDone(fullContent, toolCalls.length > 0 ? toolCalls : undefined);
|
|
197
|
+
} catch (error: any) {
|
|
198
|
+
callbacks.onError(error);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export async function fetchModels(): Promise<any[]> {
|
|
203
|
+
const baseUrl = config.get('apiBaseUrl') || 'https://api.alia.onl';
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const response = await fetch(`${baseUrl}/v1/models?category=coding`);
|
|
207
|
+
if (!response.ok) return [];
|
|
208
|
+
const data = await response.json();
|
|
209
|
+
return data.data || [];
|
|
210
|
+
} catch {
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import Conf from 'conf';
|
|
2
|
+
|
|
3
|
+
interface Session {
|
|
4
|
+
id: string;
|
|
5
|
+
title: string;
|
|
6
|
+
messages: Array<{ role: string; content: string }>;
|
|
7
|
+
createdAt: number;
|
|
8
|
+
updatedAt: number;
|
|
9
|
+
cwd: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ConfigSchema {
|
|
13
|
+
apiKey: string;
|
|
14
|
+
apiBaseUrl: string;
|
|
15
|
+
defaultModel: string;
|
|
16
|
+
sessions: Session[];
|
|
17
|
+
currentSessionId: string | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const config = new Conf<ConfigSchema>({
|
|
21
|
+
projectName: 'alia-codea-cli',
|
|
22
|
+
defaults: {
|
|
23
|
+
apiKey: '',
|
|
24
|
+
apiBaseUrl: 'https://api.alia.onl',
|
|
25
|
+
defaultModel: 'alia-v1-codea',
|
|
26
|
+
sessions: [],
|
|
27
|
+
currentSessionId: null,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export function saveSession(session: Session): void {
|
|
32
|
+
const sessions = config.get('sessions') || [];
|
|
33
|
+
const existingIndex = sessions.findIndex(s => s.id === session.id);
|
|
34
|
+
|
|
35
|
+
if (existingIndex >= 0) {
|
|
36
|
+
sessions[existingIndex] = session;
|
|
37
|
+
} else {
|
|
38
|
+
sessions.unshift(session);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Keep only last 50 sessions
|
|
42
|
+
if (sessions.length > 50) {
|
|
43
|
+
sessions.splice(50);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
config.set('sessions', sessions);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getSession(id: string): Session | undefined {
|
|
50
|
+
const sessions = config.get('sessions') || [];
|
|
51
|
+
return sessions.find(s => s.id === id);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getSessions(): Session[] {
|
|
55
|
+
return config.get('sessions') || [];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function createSession(): Session {
|
|
59
|
+
const session: Session = {
|
|
60
|
+
id: Date.now().toString(),
|
|
61
|
+
title: 'New conversation',
|
|
62
|
+
messages: [],
|
|
63
|
+
createdAt: Date.now(),
|
|
64
|
+
updatedAt: Date.now(),
|
|
65
|
+
cwd: process.cwd(),
|
|
66
|
+
};
|
|
67
|
+
saveSession(session);
|
|
68
|
+
config.set('currentSessionId', session.id);
|
|
69
|
+
return session;
|
|
70
|
+
}
|