@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.
@@ -1,33 +1,28 @@
1
1
  import chalk from 'chalk';
2
- import { config } from '../utils/config.js';
3
- import { streamChat } from '../utils/api.js';
4
- import { executeTool, formatToolCall } from '../tools/executor.js';
5
- import { buildSystemMessage, getCodebaseContext } from '../utils/context.js';
6
- import {
7
- printToolExecution,
8
- printToolResult,
9
- showThinkingStatus,
10
- hideThinkingStatus,
11
- printAssistantPrefix,
12
- printError,
13
- printInfo
14
- } from '../utils/ui.js';
15
-
16
- interface Message {
17
- role: 'user' | 'assistant' | 'system' | 'tool';
18
- content: string;
19
- tool_calls?: any[];
20
- tool_call_id?: string;
21
- }
2
+ import { buildSystemMessage, getCodebaseContext, loadProjectInstructions } from '../utils/context.js';
3
+ import { processConversation, Message, ToolExecution } from '../utils/conversation.js';
4
+ import { ApprovalMode } from '../utils/approval.js';
5
+ import * as readline from 'readline';
22
6
 
23
7
  interface RunOptions {
24
8
  model: string;
25
9
  yes: boolean;
26
10
  context: boolean;
11
+ approvalMode?: string;
12
+ quiet?: boolean;
13
+ json?: boolean;
14
+ }
15
+
16
+ interface JsonOutput {
17
+ model: string;
18
+ prompt: string;
19
+ response: string;
20
+ tool_calls: Array<{ tool: string; args: Record<string, any>; result: string; success: boolean }>;
27
21
  }
28
22
 
29
23
  export async function runPrompt(prompt: string, options: RunOptions): Promise<void> {
30
24
  const messages: Message[] = [];
25
+ const toolResults: JsonOutput['tool_calls'] = [];
31
26
 
32
27
  // Get codebase context
33
28
  let codebaseContext = '';
@@ -35,136 +30,118 @@ export async function runPrompt(prompt: string, options: RunOptions): Promise<vo
35
30
  codebaseContext = await getCodebaseContext();
36
31
  }
37
32
 
38
- // Add user message
39
- messages.push({ role: 'user', content: prompt });
40
-
41
- // Build system message
42
- const systemMessage = buildSystemMessage(options.model, codebaseContext);
43
-
44
- // Process with tool loop
45
- await processConversation(messages, systemMessage, options.model, options.yes);
46
- }
47
-
48
- async function processConversation(
49
- messages: Message[],
50
- systemMessage: string,
51
- model: string,
52
- autoApprove: boolean
53
- ): Promise<void> {
54
- let continueProcessing = true;
55
-
56
- while (continueProcessing) {
57
- printAssistantPrefix();
58
-
59
- let fullContent = '';
60
- let toolCalls: any[] | undefined;
61
-
62
- showThinkingStatus('Thinking');
63
-
64
- try {
65
- await streamChat(messages, systemMessage, model, {
66
- onContent: (content) => {
67
- hideThinkingStatus();
68
- process.stdout.write(content);
69
- fullContent += content;
70
- },
71
- onToolCall: () => {},
72
- onDone: (content, tcs) => {
73
- hideThinkingStatus();
74
- toolCalls = tcs;
75
- },
76
- onError: (error) => {
77
- hideThinkingStatus();
78
- printError(error.message);
79
- continueProcessing = false;
80
- }
81
- });
82
- } catch (error: any) {
83
- hideThinkingStatus();
84
- printError(error.message);
85
- break;
86
- }
87
-
88
- // Handle tool calls
89
- if (toolCalls && toolCalls.length > 0) {
90
- messages.push({
91
- role: 'assistant',
92
- content: fullContent,
93
- tool_calls: toolCalls
94
- });
95
-
96
- if (fullContent) console.log();
33
+ const instructions = await loadProjectInstructions();
97
34
 
98
- for (const tc of toolCalls) {
99
- const args = JSON.parse(tc.function.arguments);
100
-
101
- // Check if we need approval for file writes
102
- const isDestructive = ['write_file', 'edit_file', 'run_command'].includes(tc.function.name);
103
-
104
- if (isDestructive && !autoApprove) {
105
- console.log();
106
- console.log(chalk.yellow('⚠ ') + chalk.bold('Approval required:'));
107
- console.log(formatToolCall(tc.function.name, args));
108
- console.log();
35
+ messages.push({ role: 'user', content: prompt });
109
36
 
110
- const approved = await askApproval();
111
- if (!approved) {
112
- messages.push({
113
- role: 'tool',
114
- tool_call_id: tc.id,
115
- content: 'User declined this action.'
37
+ const systemMessage = buildSystemMessage(options.model, codebaseContext, instructions);
38
+
39
+ const approvalMode: ApprovalMode = options.yes
40
+ ? 'full-auto'
41
+ : (options.approvalMode as ApprovalMode) || 'suggest';
42
+
43
+ let fullResponse = '';
44
+
45
+ await processConversation({
46
+ messages,
47
+ systemMessage,
48
+ model: options.model,
49
+ approvalMode,
50
+ isActive: () => true,
51
+ requestApproval: async (execution) => {
52
+ if (options.quiet || options.json) return false;
53
+ return askApproval(execution);
54
+ },
55
+ onEvent: (event) => {
56
+ switch (event.type) {
57
+ case 'thinking':
58
+ if (!options.quiet && !options.json) {
59
+ process.stdout.write(chalk.magenta('✦ '));
60
+ }
61
+ break;
62
+ case 'content':
63
+ fullResponse += event.text;
64
+ if (!options.quiet && !options.json) {
65
+ process.stdout.write(event.text);
66
+ }
67
+ break;
68
+ case 'tool_start':
69
+ if (!options.quiet && !options.json) {
70
+ console.log();
71
+ console.log(chalk.cyan(' → ') + chalk.bold(event.execution.tool) + ' ' + chalk.gray(formatArgs(event.execution)));
72
+ }
73
+ break;
74
+ case 'tool_done':
75
+ if (event.execution.result !== undefined) {
76
+ toolResults.push({
77
+ tool: event.execution.tool,
78
+ args: event.execution.args,
79
+ result: event.execution.result,
80
+ success: event.execution.success ?? false,
116
81
  });
117
- continue;
118
82
  }
119
- }
120
-
121
- printToolExecution(tc.function.name, formatToolArgs(tc.function.name, args));
122
-
123
- showThinkingStatus(`Executing ${tc.function.name}`);
124
- const result = await executeTool(tc.function.name, args);
125
- hideThinkingStatus();
126
-
127
- printToolResult(result.success, result.result);
128
-
129
- messages.push({
130
- role: 'tool',
131
- tool_call_id: tc.id,
132
- content: result.result
133
- });
83
+ if (!options.quiet && !options.json) {
84
+ const icon = event.execution.success ? chalk.green(' ✓') : chalk.red(' ✗');
85
+ const preview = (event.execution.result || '').slice(0, 100).replace(/\n/g, ' ');
86
+ console.log(`${icon} ${chalk.gray(preview)}`);
87
+ }
88
+ break;
89
+ case 'done':
90
+ if (!options.quiet && !options.json) {
91
+ console.log();
92
+ }
93
+ break;
94
+ case 'error':
95
+ if (!options.json) {
96
+ console.error(chalk.red('Error: ') + event.message);
97
+ }
98
+ break;
134
99
  }
100
+ },
101
+ });
135
102
 
136
- continue;
137
- } else {
138
- if (fullContent) {
139
- messages.push({ role: 'assistant', content: fullContent });
140
- console.log();
141
- }
142
- break;
103
+ if (options.json) {
104
+ const output: JsonOutput = {
105
+ model: options.model,
106
+ prompt,
107
+ response: fullResponse,
108
+ tool_calls: toolResults,
109
+ };
110
+ console.log(JSON.stringify(output, null, 2));
111
+ } else if (options.quiet) {
112
+ if (fullResponse) {
113
+ console.log(fullResponse);
143
114
  }
144
115
  }
145
116
  }
146
117
 
147
- async function askApproval(): Promise<boolean> {
148
- const readline = await import('readline');
118
+ async function askApproval(execution: ToolExecution): Promise<boolean> {
149
119
  const rl = readline.createInterface({
150
120
  input: process.stdin,
151
- output: process.stdout
121
+ output: process.stdout,
152
122
  });
153
123
 
124
+ const desc = formatArgs(execution);
125
+ console.log();
126
+ console.log(chalk.yellow('⚠ ') + chalk.bold(execution.tool) + ' ' + desc);
127
+
154
128
  return new Promise((resolve) => {
155
- rl.question(chalk.cyan('Allow? [y/N] '), (answer) => {
129
+ rl.question(chalk.cyan(' Allow? [y/N] '), (answer) => {
156
130
  rl.close();
157
131
  resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
158
132
  });
159
133
  });
160
134
  }
161
135
 
162
- function formatToolArgs(name: string, args: Record<string, any>): string {
163
- switch (name) {
136
+ function formatArgs(execution: ToolExecution): string {
137
+ const { tool, args } = execution;
138
+ switch (tool) {
164
139
  case 'read_file':
165
140
  case 'write_file':
166
141
  case 'edit_file':
167
142
  return args.path || '';
143
+ case 'apply_patch':
144
+ return 'applying patch...';
168
145
  case 'list_files':
169
146
  return args.path || '.';
170
147
  case 'search_files':
@@ -172,6 +149,6 @@ function formatToolArgs(name: string, args: Record<string, any>): string {
172
149
  case 'run_command':
173
150
  return args.command || '';
174
151
  default:
175
- return JSON.stringify(args).slice(0, 50);
152
+ return JSON.stringify(args).slice(0, 60);
176
153
  }
177
154
  }
@@ -2,13 +2,12 @@ import chalk from 'chalk';
2
2
  import * as readline from 'readline';
3
3
  import { getSessions, getSession, config } from '../utils/config.js';
4
4
  import { startRepl } from './repl.js';
5
- import { printBanner, printError, printInfo } from '../utils/ui.js';
6
5
 
7
6
  export async function listSessions(): Promise<void> {
8
7
  const sessions = getSessions();
9
8
 
10
9
  if (sessions.length === 0) {
11
- printInfo('No saved sessions found.');
10
+ console.log(chalk.blue('ℹ ') + 'No saved sessions found.');
12
11
  console.log(chalk.gray('Start a new session with: ') + chalk.cyan('codea'));
13
12
  return;
14
13
  }
@@ -38,7 +37,7 @@ export async function resumeSession(sessionId?: string): Promise<void> {
38
37
  const sessions = getSessions();
39
38
 
40
39
  if (sessions.length === 0) {
41
- printInfo('No saved sessions found.');
40
+ console.log(chalk.blue('ℹ ') + 'No saved sessions found.');
42
41
  return;
43
42
  }
44
43
 
@@ -54,7 +53,7 @@ export async function resumeSession(sessionId?: string): Promise<void> {
54
53
  }
55
54
 
56
55
  if (!selectedSession) {
57
- printError(`Session not found: ${sessionId}`);
56
+ console.log(chalk.red('✗ Error: ') + `Session not found: ${sessionId}`);
58
57
  return;
59
58
  }
60
59
  } else {
@@ -82,7 +81,7 @@ export async function resumeSession(sessionId?: string): Promise<void> {
82
81
 
83
82
  const index = parseInt(answer) - 1;
84
83
  if (isNaN(index) || index < 0 || index >= sessions.length) {
85
- printError('Invalid selection.');
84
+ console.log(chalk.red('✗ Error: ') + 'Invalid selection.');
86
85
  resolve();
87
86
  return;
88
87
  }
@@ -99,7 +98,7 @@ export async function resumeSession(sessionId?: string): Promise<void> {
99
98
  }
100
99
 
101
100
  async function startRestoredSession(session: any): Promise<void> {
102
- printInfo(`Resuming: ${session.title}`);
101
+ console.log(chalk.blue('ℹ ') + `Resuming: ${session.title}`);
103
102
  console.log();
104
103
 
105
104
  // Display previous messages
@@ -0,0 +1,60 @@
1
+ import React from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { ToolExecution } from '../utils/conversation.js';
4
+
5
+ interface ApprovalPromptProps {
6
+ execution: ToolExecution;
7
+ onResolve: (approved: boolean) => void;
8
+ }
9
+
10
+ function formatArgs(tool: string, args: Record<string, any>): string {
11
+ switch (tool) {
12
+ case 'write_file':
13
+ return `Write to ${args.path}`;
14
+ case 'edit_file':
15
+ return `Edit ${args.path}`;
16
+ case 'apply_patch':
17
+ return `Apply patch`;
18
+ case 'run_command':
19
+ return `Run: ${args.command}`;
20
+ default:
21
+ return `${tool}: ${JSON.stringify(args).slice(0, 80)}`;
22
+ }
23
+ }
24
+
25
+ export function ApprovalPrompt({ execution, onResolve }: ApprovalPromptProps) {
26
+ useInput((input, key) => {
27
+ if (input === 'y' || input === 'Y') {
28
+ onResolve(true);
29
+ } else if (input === 'n' || input === 'N' || key.escape) {
30
+ onResolve(false);
31
+ }
32
+ });
33
+
34
+ const description = formatArgs(execution.tool, execution.args);
35
+
36
+ return (
37
+ <Box flexDirection="column" paddingLeft={2} paddingY={0}>
38
+ <Box gap={1}>
39
+ <Text color="yellow">{'⚠'}</Text>
40
+ <Text bold>{description}</Text>
41
+ </Box>
42
+ {execution.tool === 'run_command' && execution.args.command && (
43
+ <Box paddingLeft={2}>
44
+ <Text color="gray">$ {execution.args.command}</Text>
45
+ </Box>
46
+ )}
47
+ {execution.tool === 'write_file' && execution.args.content && (
48
+ <Box paddingLeft={2}>
49
+ <Text color="gray">{execution.args.content.split('\n').length} lines</Text>
50
+ </Box>
51
+ )}
52
+ <Box paddingLeft={2} gap={1}>
53
+ <Text color="green">[y]</Text>
54
+ <Text>approve</Text>
55
+ <Text color="red">[n]</Text>
56
+ <Text>deny</Text>
57
+ </Box>
58
+ </Box>
59
+ );
60
+ }
@@ -0,0 +1,39 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { ApprovalMode } from '../utils/approval.js';
4
+
5
+ interface HeaderProps {
6
+ cwd: string;
7
+ model: string;
8
+ approvalMode: ApprovalMode;
9
+ contextPercent: number;
10
+ }
11
+
12
+ function shortenPath(p: string): string {
13
+ const home = process.env.HOME || '';
14
+ if (home && p.startsWith(home)) {
15
+ return '~' + p.slice(home.length);
16
+ }
17
+ return p;
18
+ }
19
+
20
+ const MODE_COLORS: Record<ApprovalMode, string> = {
21
+ 'suggest': 'yellow',
22
+ 'auto-edit': 'cyan',
23
+ 'full-auto': 'green',
24
+ };
25
+
26
+ export function Header({ cwd, model, approvalMode, contextPercent }: HeaderProps) {
27
+ return (
28
+ <Box borderStyle="single" borderColor="gray" paddingX={1} justifyContent="space-between">
29
+ <Text color="cyan">{shortenPath(cwd)}</Text>
30
+ <Box gap={1}>
31
+ <Text color="magenta">{model}</Text>
32
+ <Text color="gray">|</Text>
33
+ <Text color={MODE_COLORS[approvalMode]}>{approvalMode}</Text>
34
+ <Text color="gray">|</Text>
35
+ <Text color={contextPercent < 20 ? 'red' : 'gray'}>{contextPercent}% left</Text>
36
+ </Box>
37
+ </Box>
38
+ );
39
+ }
@@ -0,0 +1,36 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import TextInput from 'ink-text-input';
4
+ import { ThinkingIndicator } from './ThinkingIndicator.js';
5
+
6
+ interface InputBarProps {
7
+ onSubmit: (value: string) => void;
8
+ isProcessing: boolean;
9
+ thinkingLabel?: string;
10
+ }
11
+
12
+ export function InputBar({ onSubmit, isProcessing, thinkingLabel }: InputBarProps) {
13
+ const [value, setValue] = useState('');
14
+
15
+ const handleSubmit = (text: string) => {
16
+ const trimmed = text.trim();
17
+ if (!trimmed) return;
18
+ setValue('');
19
+ onSubmit(trimmed);
20
+ };
21
+
22
+ if (isProcessing) {
23
+ return (
24
+ <Box paddingX={1}>
25
+ <ThinkingIndicator label={thinkingLabel || 'Thinking'} />
26
+ </Box>
27
+ );
28
+ }
29
+
30
+ return (
31
+ <Box paddingX={1}>
32
+ <Text color="cyan">{'❯ '}</Text>
33
+ <TextInput value={value} onChange={setValue} onSubmit={handleSubmit} />
34
+ </Box>
35
+ );
36
+ }
@@ -0,0 +1,81 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { Marked } from 'marked';
4
+ import { markedTerminal } from 'marked-terminal';
5
+ import { ToolCallCard } from './ToolCallCard.js';
6
+ import { ToolExecution } from '../utils/conversation.js';
7
+
8
+ export interface DisplayMessage {
9
+ id: string;
10
+ type: 'user' | 'assistant' | 'tool' | 'info';
11
+ content: string;
12
+ toolExecution?: ToolExecution;
13
+ streaming?: boolean;
14
+ }
15
+
16
+ interface MessageListProps {
17
+ messages: DisplayMessage[];
18
+ }
19
+
20
+ const marked = new Marked(markedTerminal() as any);
21
+
22
+ function renderMarkdown(text: string): string {
23
+ try {
24
+ const rendered = marked.parse(text);
25
+ if (typeof rendered === 'string') {
26
+ return rendered.trimEnd();
27
+ }
28
+ return text;
29
+ } catch {
30
+ return text;
31
+ }
32
+ }
33
+
34
+ function MessageBlock({ message }: { message: DisplayMessage }) {
35
+ switch (message.type) {
36
+ case 'user':
37
+ return (
38
+ <Box paddingLeft={1} paddingY={0}>
39
+ <Text color="cyan">{'❯ '}</Text>
40
+ <Text>{message.content}</Text>
41
+ </Box>
42
+ );
43
+
44
+ case 'assistant':
45
+ return (
46
+ <Box flexDirection="column" paddingLeft={1}>
47
+ <Box>
48
+ <Text color="magenta">{'✦ '}</Text>
49
+ <Text>{message.streaming ? message.content : renderMarkdown(message.content)}</Text>
50
+ </Box>
51
+ </Box>
52
+ );
53
+
54
+ case 'tool':
55
+ if (message.toolExecution) {
56
+ return <ToolCallCard execution={message.toolExecution} />;
57
+ }
58
+ return null;
59
+
60
+ case 'info':
61
+ return (
62
+ <Box paddingLeft={1}>
63
+ <Text color="blue">{'ℹ '}</Text>
64
+ <Text color="gray">{message.content}</Text>
65
+ </Box>
66
+ );
67
+
68
+ default:
69
+ return null;
70
+ }
71
+ }
72
+
73
+ export function MessageList({ messages }: MessageListProps) {
74
+ return (
75
+ <Box flexDirection="column" flexGrow={1} gap={0}>
76
+ {messages.map((msg) => (
77
+ <MessageBlock key={msg.id} message={msg} />
78
+ ))}
79
+ </Box>
80
+ );
81
+ }
@@ -0,0 +1,28 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import Spinner from 'ink-spinner';
4
+
5
+ interface ThinkingIndicatorProps {
6
+ label?: string;
7
+ }
8
+
9
+ export function ThinkingIndicator({ label = 'Thinking' }: ThinkingIndicatorProps) {
10
+ const [elapsed, setElapsed] = useState(0);
11
+
12
+ useEffect(() => {
13
+ const interval = setInterval(() => {
14
+ setElapsed((e) => e + 1);
15
+ }, 1000);
16
+ return () => clearInterval(interval);
17
+ }, []);
18
+
19
+ return (
20
+ <Box gap={1}>
21
+ <Text color="cyan">
22
+ <Spinner type="dots" />
23
+ </Text>
24
+ <Text bold>{label}</Text>
25
+ <Text color="gray">({elapsed}s)</Text>
26
+ </Box>
27
+ );
28
+ }
@@ -0,0 +1,68 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { ToolExecution } from '../utils/conversation.js';
4
+
5
+ interface ToolCallCardProps {
6
+ execution: ToolExecution;
7
+ }
8
+
9
+ function formatArgs(tool: string, args: Record<string, any>): string {
10
+ switch (tool) {
11
+ case 'read_file':
12
+ case 'write_file':
13
+ case 'edit_file':
14
+ return args.path || '';
15
+ case 'apply_patch':
16
+ return 'applying patch...';
17
+ case 'list_files':
18
+ return args.path || '.';
19
+ case 'search_files':
20
+ return `"${args.pattern}" in ${args.path || '.'}`;
21
+ case 'run_command':
22
+ return args.command || '';
23
+ default:
24
+ return JSON.stringify(args).slice(0, 60);
25
+ }
26
+ }
27
+
28
+ export function ToolCallCard({ execution }: ToolCallCardProps) {
29
+ const { tool, args, result, success, approved } = execution;
30
+ const argStr = formatArgs(tool, args);
31
+ const isDone = result !== undefined;
32
+
33
+ if (approved === false) {
34
+ return (
35
+ <Box flexDirection="column" paddingLeft={2}>
36
+ <Box gap={1}>
37
+ <Text color="red">{'✗'}</Text>
38
+ <Text bold color="gray" strikethrough>{tool}</Text>
39
+ <Text color="gray">{argStr}</Text>
40
+ </Box>
41
+ <Box paddingLeft={2}>
42
+ <Text color="yellow" dimColor>Declined by user</Text>
43
+ </Box>
44
+ </Box>
45
+ );
46
+ }
47
+
48
+ return (
49
+ <Box flexDirection="column" paddingLeft={2}>
50
+ <Box gap={1}>
51
+ {isDone ? (
52
+ <Text color={success ? 'green' : 'red'}>{success ? '✓' : '✗'}</Text>
53
+ ) : (
54
+ <Text color="cyan">{'→'}</Text>
55
+ )}
56
+ <Text bold>{tool}</Text>
57
+ <Text color="gray">{argStr}</Text>
58
+ </Box>
59
+ {isDone && result && (
60
+ <Box paddingLeft={2}>
61
+ <Text color="gray" wrap="truncate-end">
62
+ {result.slice(0, 120).replace(/\n/g, ' ')}{result.length > 120 ? '...' : ''}
63
+ </Text>
64
+ </Box>
65
+ )}
66
+ </Box>
67
+ );
68
+ }