@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.
@@ -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
+ }
@@ -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
+ }