@alia-codea/cli 1.0.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.0.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
+ }
@@ -1,18 +1,134 @@
1
1
  import * as readline from 'readline';
2
+ import * as crypto from 'crypto';
3
+ import * as http from 'http';
4
+ import { exec } from 'child_process';
2
5
  import chalk from 'chalk';
3
6
  import { config } from '../utils/config.js';
4
- import { printSuccess, printError, printInfo } from '../utils/ui.js';
7
+ function printSuccess(message: string): void {
8
+ console.log(chalk.green('✓ ') + message);
9
+ }
5
10
 
6
- export async function login(): Promise<void> {
7
- console.log();
8
- console.log(chalk.bold('Codea CLI Login'));
9
- console.log(chalk.gray('Enter your Alia API key to get started.'));
10
- console.log(chalk.gray('Get your API key at: ') + chalk.cyan('https://alia.onl/settings/api'));
11
- console.log();
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
+ }
18
+
19
+ function openBrowser(url: string): void {
20
+ const cmd =
21
+ process.platform === 'darwin'
22
+ ? 'open'
23
+ : process.platform === 'win32'
24
+ ? 'start ""'
25
+ : 'xdg-open';
26
+ exec(`${cmd} "${url}"`);
27
+ }
28
+
29
+ async function loginWithBrowser(): Promise<boolean> {
30
+ const codeVerifier = crypto.randomBytes(32).toString('base64url');
31
+ const codeChallenge = crypto
32
+ .createHash('sha256')
33
+ .update(codeVerifier)
34
+ .digest('base64url');
35
+
36
+ return new Promise((resolve) => {
37
+ const server = http.createServer(async (req, res) => {
38
+ const url = new URL(req.url!, `http://localhost`);
39
+ if (url.pathname !== '/callback') {
40
+ res.writeHead(404);
41
+ res.end();
42
+ return;
43
+ }
44
+
45
+ const code = url.searchParams.get('code');
46
+ const error = url.searchParams.get('error');
47
+
48
+ if (error || !code) {
49
+ res.writeHead(200, { 'Content-Type': 'text/html' });
50
+ res.end(
51
+ '<html><body style="font-family:system-ui;text-align:center;padding:60px">' +
52
+ '<h2>Authorization cancelled</h2><p>You can close this window.</p></body></html>',
53
+ );
54
+ server.close();
55
+ resolve(false);
56
+ return;
57
+ }
58
+
59
+ try {
60
+ const baseUrl = config.get('apiBaseUrl') || 'https://api.alia.onl';
61
+ const response = await fetch(`${baseUrl}/auth/token`, {
62
+ method: 'POST',
63
+ headers: { 'Content-Type': 'application/json' },
64
+ body: JSON.stringify({
65
+ grant_type: 'authorization_code',
66
+ code,
67
+ code_verifier: codeVerifier,
68
+ client_id: 'codea',
69
+ }),
70
+ });
71
+
72
+ const data = (await response.json()) as { token?: string };
73
+
74
+ if (data.token) {
75
+ config.set('apiKey', data.token);
76
+ res.writeHead(200, { 'Content-Type': 'text/html' });
77
+ res.end(
78
+ '<html><body style="font-family:system-ui;text-align:center;padding:60px">' +
79
+ '<h2>Logged in!</h2><p>You can close this window and return to the terminal.</p></body></html>',
80
+ );
81
+ console.log();
82
+ printSuccess('Logged in successfully!');
83
+ server.close();
84
+ resolve(true);
85
+ } else {
86
+ throw new Error('No token received');
87
+ }
88
+ } catch {
89
+ res.writeHead(200, { 'Content-Type': 'text/html' });
90
+ res.end(
91
+ '<html><body style="font-family:system-ui;text-align:center;padding:60px">' +
92
+ '<h2>Login failed</h2><p>Please try again.</p></body></html>',
93
+ );
94
+ printError('Failed to exchange authorization code.');
95
+ server.close();
96
+ resolve(false);
97
+ }
98
+ });
99
+
100
+ server.listen(0, () => {
101
+ const port = (server.address() as { port: number }).port;
102
+ const callback = encodeURIComponent(`http://localhost:${port}/callback`);
103
+ const authorizeUrl =
104
+ `https://alia.onl/authorize?app=codea` +
105
+ `&callback=${callback}` +
106
+ `&code_challenge=${codeChallenge}` +
107
+ `&code_challenge_method=S256`;
12
108
 
109
+ printInfo('Opening browser for authorization...');
110
+ openBrowser(authorizeUrl);
111
+ console.log(
112
+ chalk.gray('\nIf the browser doesn\'t open, visit:\n') +
113
+ chalk.cyan(authorizeUrl) +
114
+ '\n',
115
+ );
116
+ console.log(chalk.gray('Waiting for authorization...'));
117
+ });
118
+
119
+ // Timeout after 5 minutes
120
+ setTimeout(() => {
121
+ server.close();
122
+ printError('Authorization timed out.');
123
+ resolve(false);
124
+ }, 5 * 60 * 1000);
125
+ });
126
+ }
127
+
128
+ async function loginWithApiKey(): Promise<boolean> {
13
129
  const rl = readline.createInterface({
14
130
  input: process.stdin,
15
- output: process.stdout
131
+ output: process.stdout,
16
132
  });
17
133
 
18
134
  return new Promise((resolve) => {
@@ -20,46 +136,59 @@ export async function login(): Promise<void> {
20
136
  rl.close();
21
137
 
22
138
  const trimmedKey = apiKey.trim();
23
-
24
139
  if (!trimmedKey) {
25
140
  printError('No API key provided.');
26
- resolve();
141
+ resolve(false);
27
142
  return;
28
143
  }
29
144
 
30
- // Validate the API key by making a test request
31
145
  printInfo('Validating API key...');
32
146
 
33
147
  try {
34
148
  const baseUrl = config.get('apiBaseUrl') || 'https://api.alia.onl';
35
149
  const response = await fetch(`${baseUrl}/codea/me`, {
36
- headers: {
37
- 'Authorization': `Bearer ${trimmedKey}`
38
- }
150
+ headers: { Authorization: `Bearer ${trimmedKey}` },
39
151
  });
40
152
 
41
153
  if (response.ok) {
42
- const data = await response.json();
154
+ const data = (await response.json()) as { name?: string };
43
155
  config.set('apiKey', trimmedKey);
44
156
  console.log();
45
- printSuccess(`Logged in successfully!`);
157
+ printSuccess('Logged in successfully!');
46
158
  if (data.name) {
47
159
  console.log(chalk.gray(`Welcome, ${data.name}!`));
48
160
  }
49
- console.log();
50
- console.log(chalk.gray('Run ') + chalk.cyan('codea') + chalk.gray(' to start coding.'));
161
+ resolve(true);
51
162
  } else {
52
163
  printError('Invalid API key. Please check and try again.');
164
+ resolve(false);
53
165
  }
54
166
  } catch (error: any) {
55
167
  printError(`Could not validate API key: ${error.message}`);
168
+ resolve(false);
56
169
  }
57
-
58
- resolve();
59
170
  });
60
171
  });
61
172
  }
62
173
 
174
+ export async function login(): Promise<boolean> {
175
+ console.log();
176
+ console.log(chalk.bold('Codea CLI Login'));
177
+ console.log();
178
+
179
+ const success = await loginWithBrowser();
180
+ if (success) return true;
181
+
182
+ // Fallback to manual API key entry
183
+ console.log();
184
+ console.log(
185
+ chalk.gray('Alternatively, paste your API key from: ') +
186
+ chalk.cyan('https://alia.onl/settings/api'),
187
+ );
188
+ console.log();
189
+ return loginWithApiKey();
190
+ }
191
+
63
192
  export function logout(): void {
64
193
  config.delete('apiKey');
65
194
  printSuccess('Logged out successfully.');