@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alia-codea/cli",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "description": "Codea CLI - AI coding assistant for your terminal by Alia",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "type": "module",
10
10
  "scripts": {
11
- "build": "tsup src/index.ts --format esm --dts --clean",
11
+ "build": "tsup src/index.ts --format esm --clean",
12
12
  "dev": "tsx src/index.ts",
13
13
  "start": "node dist/index.js",
14
14
  "lint": "eslint src",
@@ -29,19 +29,19 @@
29
29
  "chalk": "^5.3.0",
30
30
  "commander": "^12.1.0",
31
31
  "conf": "^13.0.1",
32
- "ink": "^5.0.1",
32
+ "ink": "^6.7.0",
33
33
  "ink-spinner": "^5.0.0",
34
34
  "ink-text-input": "^6.0.0",
35
35
  "marked": "^15.0.4",
36
36
  "marked-terminal": "^7.2.1",
37
37
  "openai": "^6.16.0",
38
38
  "ora": "^8.1.1",
39
- "react": "^18.3.1",
39
+ "react": "19.2.0",
40
40
  "simple-git": "^3.27.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "^22.10.5",
44
- "@types/react": "^18.3.12",
44
+ "@types/react": "^19.2.0",
45
45
  "tsup": "^8.3.5",
46
46
  "tsx": "^4.19.2",
47
47
  "typescript": "^5.7.2"
package/src/app.tsx ADDED
@@ -0,0 +1,281 @@
1
+ import React, { useState, useCallback, useRef, useEffect } from 'react';
2
+ import { Box, Text, useApp, useInput } from 'ink';
3
+ import { Header } from './components/Header.js';
4
+ import { MessageList, DisplayMessage } from './components/MessageList.js';
5
+ import { InputBar } from './components/InputBar.js';
6
+ import { ApprovalPrompt } from './components/ApprovalPrompt.js';
7
+ import { processConversation, Message, ToolExecution } from './utils/conversation.js';
8
+ import { buildSystemMessage, getCodebaseContext, loadProjectInstructions } from './utils/context.js';
9
+ import { createSession, saveSession } from './utils/config.js';
10
+ import { ApprovalMode } from './utils/approval.js';
11
+
12
+ export interface AppOptions {
13
+ model: string;
14
+ approvalMode: ApprovalMode;
15
+ context: boolean;
16
+ }
17
+
18
+ let msgCounter = 0;
19
+ function nextId(): string {
20
+ return `msg-${++msgCounter}`;
21
+ }
22
+
23
+ export function App({ options }: { options: AppOptions }) {
24
+ const { exit } = useApp();
25
+ const [displayMessages, setDisplayMessages] = useState<DisplayMessage[]>([]);
26
+ const [isProcessing, setIsProcessing] = useState(false);
27
+ const [thinkingLabel, setThinkingLabel] = useState('Thinking');
28
+ const [approvalMode, setApprovalMode] = useState<ApprovalMode>(options.approvalMode);
29
+ const [contextPercent, setContextPercent] = useState(100);
30
+ const [pendingApproval, setPendingApproval] = useState<{
31
+ execution: ToolExecution;
32
+ resolve: (approved: boolean) => void;
33
+ } | null>(null);
34
+ const [ready, setReady] = useState(false);
35
+ const [codebaseContext, setCodebaseContext] = useState('');
36
+ const [instructions, setInstructions] = useState('');
37
+
38
+ const messagesRef = useRef<Message[]>([]);
39
+ const sessionRef = useRef(createSession());
40
+ const activeRef = useRef(true);
41
+ const streamingIdRef = useRef<string | null>(null);
42
+
43
+ // Initialize on mount
44
+ useEffect(() => {
45
+ let cancelled = false;
46
+ (async () => {
47
+ let ctx = '';
48
+ let instr = '';
49
+ if (options.context !== false) {
50
+ ctx = await getCodebaseContext();
51
+ if (ctx && !cancelled) {
52
+ setDisplayMessages((prev) => [
53
+ ...prev,
54
+ { id: nextId(), type: 'info', content: `Loaded context from ${ctx.split('\n').length} lines` },
55
+ ]);
56
+ }
57
+ }
58
+ instr = await loadProjectInstructions();
59
+ if (instr && !cancelled) {
60
+ const count = instr.split('\n---\n').length;
61
+ setDisplayMessages((prev) => [
62
+ ...prev,
63
+ { id: nextId(), type: 'info', content: `Loaded ${count} CODEA.md instruction file(s)` },
64
+ ]);
65
+ }
66
+ if (!cancelled) {
67
+ setCodebaseContext(ctx);
68
+ setInstructions(instr);
69
+ setReady(true);
70
+ }
71
+ })();
72
+ return () => { cancelled = true; };
73
+ }, []);
74
+
75
+ // Handle Ctrl+C
76
+ useInput((_input, key) => {
77
+ if (key.ctrl && (_input === 'c' || _input === 'C')) {
78
+ if (isProcessing) {
79
+ activeRef.current = false;
80
+ setIsProcessing(false);
81
+ setPendingApproval(null);
82
+ setDisplayMessages((prev) => [
83
+ ...prev,
84
+ { id: nextId(), type: 'info', content: 'Cancelled.' },
85
+ ]);
86
+ } else {
87
+ exit();
88
+ }
89
+ }
90
+ });
91
+
92
+ const addMessage = useCallback((msg: DisplayMessage) => {
93
+ setDisplayMessages((prev) => [...prev, msg]);
94
+ }, []);
95
+
96
+ const updateLastAssistant = useCallback((text: string) => {
97
+ setDisplayMessages((prev) => {
98
+ const last = prev[prev.length - 1];
99
+ if (last && last.type === 'assistant' && last.streaming) {
100
+ return [...prev.slice(0, -1), { ...last, content: last.content + text }];
101
+ }
102
+ return prev;
103
+ });
104
+ }, []);
105
+
106
+ const finalizeAssistant = useCallback(() => {
107
+ setDisplayMessages((prev) => {
108
+ const last = prev[prev.length - 1];
109
+ if (last && last.type === 'assistant' && last.streaming) {
110
+ return [...prev.slice(0, -1), { ...last, streaming: false }];
111
+ }
112
+ return prev;
113
+ });
114
+ }, []);
115
+
116
+ const handleSubmit = useCallback(async (input: string) => {
117
+ // Handle slash commands
118
+ if (input.startsWith('/')) {
119
+ const [cmd, ...args] = input.slice(1).split(' ');
120
+ switch (cmd.toLowerCase()) {
121
+ case 'help':
122
+ addMessage({
123
+ id: nextId(),
124
+ type: 'info',
125
+ content: 'Commands: /help, /clear, /mode <suggest|auto-edit|full-auto>, /model <name>, /exit',
126
+ });
127
+ return;
128
+ case 'clear':
129
+ messagesRef.current = [];
130
+ setDisplayMessages([]);
131
+ setContextPercent(100);
132
+ return;
133
+ case 'mode':
134
+ if (args[0] && ['suggest', 'auto-edit', 'full-auto'].includes(args[0])) {
135
+ setApprovalMode(args[0] as ApprovalMode);
136
+ addMessage({ id: nextId(), type: 'info', content: `Approval mode: ${args[0]}` });
137
+ } else {
138
+ addMessage({ id: nextId(), type: 'info', content: `Current mode: ${approvalMode}. Options: suggest, auto-edit, full-auto` });
139
+ }
140
+ return;
141
+ case 'model':
142
+ if (args[0]) {
143
+ options.model = args[0].startsWith('alia-') ? args[0] : `alia-v1-${args[0]}`;
144
+ addMessage({ id: nextId(), type: 'info', content: `Model: ${options.model}` });
145
+ } else {
146
+ addMessage({ id: nextId(), type: 'info', content: `Current model: ${options.model}` });
147
+ }
148
+ return;
149
+ case 'exit':
150
+ case 'quit':
151
+ exit();
152
+ return;
153
+ default:
154
+ addMessage({ id: nextId(), type: 'info', content: `Unknown command: /${cmd}` });
155
+ return;
156
+ }
157
+ }
158
+
159
+ // Add user message
160
+ addMessage({ id: nextId(), type: 'user', content: input });
161
+ messagesRef.current.push({ role: 'user', content: input });
162
+
163
+ setIsProcessing(true);
164
+ activeRef.current = true;
165
+ streamingIdRef.current = null;
166
+
167
+ const systemMessage = buildSystemMessage(options.model, codebaseContext, instructions);
168
+
169
+ await processConversation({
170
+ messages: messagesRef.current,
171
+ systemMessage,
172
+ model: options.model,
173
+ approvalMode,
174
+ isActive: () => activeRef.current,
175
+ requestApproval: (execution) => {
176
+ return new Promise<boolean>((resolve) => {
177
+ setPendingApproval({ execution, resolve });
178
+ });
179
+ },
180
+ onEvent: (event) => {
181
+ switch (event.type) {
182
+ case 'thinking':
183
+ setThinkingLabel('Thinking');
184
+ // Start a new streaming assistant message
185
+ streamingIdRef.current = nextId();
186
+ setDisplayMessages((prev) => [
187
+ ...prev,
188
+ { id: streamingIdRef.current!, type: 'assistant', content: '', streaming: true },
189
+ ]);
190
+ break;
191
+ case 'content':
192
+ updateLastAssistant(event.text);
193
+ break;
194
+ case 'tool_start':
195
+ // Finalize any streaming text before showing tool
196
+ finalizeAssistant();
197
+ setThinkingLabel(`Running ${event.execution.tool}`);
198
+ addMessage({
199
+ id: nextId(),
200
+ type: 'tool',
201
+ content: '',
202
+ toolExecution: { ...event.execution },
203
+ });
204
+ break;
205
+ case 'tool_done':
206
+ // Update the tool message with result
207
+ setDisplayMessages((prev) => {
208
+ const idx = prev.findLastIndex(
209
+ (m) => m.type === 'tool' && m.toolExecution?.id === event.execution.id
210
+ );
211
+ if (idx >= 0) {
212
+ const updated = [...prev];
213
+ updated[idx] = {
214
+ ...updated[idx],
215
+ toolExecution: { ...event.execution },
216
+ };
217
+ return updated;
218
+ }
219
+ return prev;
220
+ });
221
+ break;
222
+ case 'done':
223
+ finalizeAssistant();
224
+ break;
225
+ case 'error':
226
+ finalizeAssistant();
227
+ addMessage({ id: nextId(), type: 'info', content: `Error: ${event.message}` });
228
+ break;
229
+ }
230
+ },
231
+ });
232
+
233
+ setIsProcessing(false);
234
+ setPendingApproval(null);
235
+
236
+ // Save session
237
+ const session = sessionRef.current;
238
+ session.messages = messagesRef.current.map((m) => ({ role: m.role, content: m.content }));
239
+ session.title = messagesRef.current[0]?.content.slice(0, 50) || 'New conversation';
240
+ session.updatedAt = Date.now();
241
+ saveSession(session);
242
+
243
+ // Update context estimate
244
+ const totalChars = messagesRef.current.reduce((acc, m) => acc + m.content.length, 0);
245
+ const maxContext = 128000;
246
+ setContextPercent(Math.max(5, 100 - Math.floor((totalChars / maxContext) * 100)));
247
+ }, [approvalMode, codebaseContext, instructions, options]);
248
+
249
+ const handleApprovalResolve = useCallback((approved: boolean) => {
250
+ if (pendingApproval) {
251
+ pendingApproval.resolve(approved);
252
+ setPendingApproval(null);
253
+ }
254
+ }, [pendingApproval]);
255
+
256
+ const modelDisplay = options.model.replace('alia-v1-', '');
257
+
258
+ return (
259
+ <Box flexDirection="column">
260
+ <Header
261
+ cwd={process.cwd()}
262
+ model={modelDisplay}
263
+ approvalMode={approvalMode}
264
+ contextPercent={contextPercent}
265
+ />
266
+ <MessageList messages={displayMessages} />
267
+ {pendingApproval ? (
268
+ <ApprovalPrompt
269
+ execution={pendingApproval.execution}
270
+ onResolve={handleApprovalResolve}
271
+ />
272
+ ) : (
273
+ <InputBar
274
+ onSubmit={handleSubmit}
275
+ isProcessing={isProcessing}
276
+ thinkingLabel={thinkingLabel}
277
+ />
278
+ )}
279
+ </Box>
280
+ );
281
+ }
@@ -4,7 +4,17 @@ import * as http from 'http';
4
4
  import { exec } from 'child_process';
5
5
  import chalk from 'chalk';
6
6
  import { config } from '../utils/config.js';
7
- import { printSuccess, printError, printInfo } from '../utils/ui.js';
7
+ function printSuccess(message: string): void {
8
+ console.log(chalk.green('✓ ') + message);
9
+ }
10
+
11
+ function printError(message: string): void {
12
+ console.log(chalk.red('✗ Error: ') + message);
13
+ }
14
+
15
+ function printInfo(message: string): void {
16
+ console.log(chalk.blue('ℹ ') + message);
17
+ }
8
18
 
9
19
  function openBrowser(url: string): void {
10
20
  const cmd =
@@ -1,309 +1,21 @@
1
- import * as readline from 'readline';
2
- import chalk from 'chalk';
3
- import { config, createSession, saveSession } from '../utils/config.js';
4
- import { streamChat } from '../utils/api.js';
5
- import { executeTool, formatToolCall } from '../tools/executor.js';
6
- import {
7
- printBanner,
8
- printTips,
9
- printPrompt,
10
- printToolExecution,
11
- printToolResult,
12
- showThinkingStatus,
13
- hideThinkingStatus,
14
- printStatusBar,
15
- printAssistantPrefix,
16
- printError,
17
- printInfo
18
- } from '../utils/ui.js';
19
- import { buildSystemMessage, getCodebaseContext } from '../utils/context.js';
20
-
21
- interface Message {
22
- role: 'user' | 'assistant' | 'system' | 'tool';
23
- content: string;
24
- tool_calls?: any[];
25
- tool_call_id?: string;
26
- }
1
+ import React from 'react';
2
+ import { render } from 'ink';
3
+ import { App, AppOptions } from '../app.js';
4
+ import { ApprovalMode } from '../utils/approval.js';
27
5
 
28
6
  interface ReplOptions {
29
7
  model: string;
30
8
  context: boolean;
9
+ approvalMode?: string;
31
10
  }
32
11
 
33
12
  export async function startRepl(options: ReplOptions): Promise<void> {
34
- const session = createSession();
35
- const messages: Message[] = [];
36
- let isProcessing = false;
37
- let contextUsed = 0;
38
- const maxContext = 128000;
39
-
40
- // Print welcome UI
41
- printTips();
42
-
43
- // Get initial codebase context
44
- let codebaseContext = '';
45
- if (options.context !== false) {
46
- printInfo('Analyzing codebase...');
47
- codebaseContext = await getCodebaseContext();
48
- if (codebaseContext) {
49
- printInfo(`Loaded context from ${codebaseContext.split('\n').length} files`);
50
- }
51
- }
52
-
53
- // Setup readline
54
- const rl = readline.createInterface({
55
- input: process.stdin,
56
- output: process.stdout,
57
- terminal: true
58
- });
59
-
60
- // Handle Ctrl+C
61
- rl.on('SIGINT', () => {
62
- if (isProcessing) {
63
- isProcessing = false;
64
- hideThinkingStatus();
65
- console.log(chalk.yellow('\nCancelled.'));
66
- printPrompt();
67
- } else {
68
- console.log(chalk.gray('\nGoodbye!'));
69
- process.exit(0);
70
- }
71
- });
72
-
73
- const askQuestion = (): void => {
74
- printPrompt();
75
- rl.question('', async (input) => {
76
- const trimmed = input.trim();
77
-
78
- if (!trimmed) {
79
- askQuestion();
80
- return;
81
- }
82
-
83
- // Handle slash commands
84
- if (trimmed.startsWith('/')) {
85
- await handleSlashCommand(trimmed, messages, session, options);
86
- askQuestion();
87
- return;
88
- }
89
-
90
- // Add user message
91
- messages.push({ role: 'user', content: trimmed });
92
- isProcessing = true;
93
-
94
- // Build system message
95
- const systemMessage = buildSystemMessage(options.model, codebaseContext);
96
-
97
- // Process conversation with tool loop
98
- await processConversation(messages, systemMessage, options.model, () => isProcessing);
99
-
100
- isProcessing = false;
101
-
102
- // Update session
103
- session.messages = messages.map(m => ({ role: m.role, content: m.content }));
104
- session.title = messages[0]?.content.slice(0, 50) || 'New conversation';
105
- session.updatedAt = Date.now();
106
- saveSession(session);
107
-
108
- // Update context usage estimate
109
- contextUsed = Math.min(95, Math.floor(messages.reduce((acc, m) => acc + m.content.length, 0) / maxContext * 100));
110
-
111
- // Print status bar
112
- printStatusBar(process.cwd(), getModelDisplayName(options.model), 100 - contextUsed);
113
-
114
- askQuestion();
115
- });
13
+ const appOptions: AppOptions = {
14
+ model: options.model,
15
+ approvalMode: (options.approvalMode as ApprovalMode) || 'suggest',
16
+ context: options.context,
116
17
  };
117
18
 
118
- askQuestion();
119
- }
120
-
121
- async function processConversation(
122
- messages: Message[],
123
- systemMessage: string,
124
- model: string,
125
- isActive: () => boolean
126
- ): Promise<void> {
127
- while (isActive()) {
128
- console.log();
129
- printAssistantPrefix();
130
-
131
- let fullContent = '';
132
- let toolCalls: any[] | undefined;
133
-
134
- showThinkingStatus('Thinking');
135
-
136
- try {
137
- await streamChat(messages, systemMessage, model, {
138
- onContent: (content) => {
139
- if (!isActive()) return;
140
- hideThinkingStatus();
141
- process.stdout.write(content);
142
- fullContent += content;
143
- },
144
- onToolCall: (tc) => {
145
- // Tool calls are accumulated
146
- },
147
- onDone: (content, tcs) => {
148
- hideThinkingStatus();
149
- toolCalls = tcs;
150
- },
151
- onError: (error) => {
152
- hideThinkingStatus();
153
- printError(error.message);
154
- }
155
- });
156
- } catch (error: any) {
157
- hideThinkingStatus();
158
- printError(error.message);
159
- break;
160
- }
161
-
162
- if (!isActive()) break;
163
-
164
- // Handle tool calls
165
- if (toolCalls && toolCalls.length > 0) {
166
- // Add assistant message with tool calls
167
- messages.push({
168
- role: 'assistant',
169
- content: fullContent,
170
- tool_calls: toolCalls
171
- });
172
-
173
- if (fullContent) {
174
- console.log(); // New line after content
175
- }
176
-
177
- // Execute each tool
178
- for (const tc of toolCalls) {
179
- if (!isActive()) break;
180
-
181
- const args = JSON.parse(tc.function.arguments);
182
- printToolExecution(tc.function.name, formatToolArgs(tc.function.name, args));
183
-
184
- showThinkingStatus(`Executing ${tc.function.name}`);
185
- const result = await executeTool(tc.function.name, args);
186
- hideThinkingStatus();
187
-
188
- printToolResult(result.success, result.result);
189
-
190
- // Add tool result
191
- messages.push({
192
- role: 'tool',
193
- tool_call_id: tc.id,
194
- content: result.result
195
- });
196
- }
197
-
198
- // Continue loop for next response
199
- continue;
200
- } else {
201
- // No tool calls, conversation turn complete
202
- if (fullContent) {
203
- messages.push({ role: 'assistant', content: fullContent });
204
- console.log(); // New line after response
205
- }
206
- break;
207
- }
208
- }
209
- }
210
-
211
- async function handleSlashCommand(
212
- command: string,
213
- messages: Message[],
214
- session: any,
215
- options: ReplOptions
216
- ): Promise<void> {
217
- const [cmd, ...args] = command.slice(1).split(' ');
218
-
219
- switch (cmd.toLowerCase()) {
220
- case 'help':
221
- console.log();
222
- console.log(chalk.bold('Available commands:'));
223
- console.log(chalk.cyan(' /help') + chalk.gray(' - Show this help'));
224
- console.log(chalk.cyan(' /clear') + chalk.gray(' - Clear conversation'));
225
- console.log(chalk.cyan(' /model') + chalk.gray(' - Switch model'));
226
- console.log(chalk.cyan(' /context') + chalk.gray(' - Show current context'));
227
- console.log(chalk.cyan(' /save') + chalk.gray(' - Save conversation'));
228
- console.log(chalk.cyan(' /exit') + chalk.gray(' - Exit Codea'));
229
- console.log();
230
- break;
231
-
232
- case 'clear':
233
- messages.length = 0;
234
- console.log(chalk.green('Conversation cleared.'));
235
- break;
236
-
237
- case 'model':
238
- const modelArg = args[0];
239
- if (modelArg) {
240
- options.model = modelArg.startsWith('alia-') ? modelArg : `alia-v1-${modelArg}`;
241
- console.log(chalk.green(`Model switched to ${options.model}`));
242
- } else {
243
- console.log(chalk.gray('Current model: ') + chalk.cyan(options.model));
244
- try {
245
- const { fetchModels } = await import('../utils/api.js');
246
- const apiModels = await fetchModels();
247
- if (apiModels.length > 0) {
248
- console.log(chalk.gray('Available models:'));
249
- for (const m of apiModels) {
250
- console.log(chalk.gray(' ') + chalk.cyan(m.id) + chalk.gray(` - ${m.name}`));
251
- }
252
- } else {
253
- console.log(chalk.gray('Available: codea, codea-pro, codea-thinking'));
254
- }
255
- } catch {
256
- console.log(chalk.gray('Available: codea, codea-pro, codea-thinking'));
257
- }
258
- }
259
- break;
260
-
261
- case 'context':
262
- console.log(chalk.gray(`Messages in context: ${messages.length}`));
263
- console.log(chalk.gray(`Working directory: ${process.cwd()}`));
264
- break;
265
-
266
- case 'save':
267
- session.messages = messages.map(m => ({ role: m.role, content: m.content }));
268
- session.updatedAt = Date.now();
269
- saveSession(session);
270
- console.log(chalk.green('Conversation saved.'));
271
- break;
272
-
273
- case 'exit':
274
- case 'quit':
275
- console.log(chalk.gray('Goodbye!'));
276
- process.exit(0);
277
- break;
278
-
279
- default:
280
- console.log(chalk.yellow(`Unknown command: /${cmd}`));
281
- console.log(chalk.gray('Type /help for available commands.'));
282
- }
283
- }
284
-
285
- function formatToolArgs(name: string, args: Record<string, any>): string {
286
- switch (name) {
287
- case 'read_file':
288
- case 'write_file':
289
- case 'edit_file':
290
- return args.path || '';
291
- case 'list_files':
292
- return args.path || '.';
293
- case 'search_files':
294
- return `"${args.pattern}" in ${args.path || '.'}`;
295
- case 'run_command':
296
- return args.command || '';
297
- default:
298
- return JSON.stringify(args).slice(0, 50);
299
- }
300
- }
301
-
302
- function getModelDisplayName(model: string): string {
303
- const names: Record<string, string> = {
304
- 'alia-v1-codea': 'codea',
305
- 'alia-v1-pro': 'codea-pro',
306
- 'alia-v1-thinking': 'codea-thinking'
307
- };
308
- return names[model] || model;
19
+ const { waitUntilExit } = render(React.createElement(App, { options: appOptions }));
20
+ await waitUntilExit();
309
21
  }