@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/dist/index.js CHANGED
@@ -1,40 +1,610 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- config,
4
- createSession,
5
- getSession,
6
- getSessions,
7
- saveSession,
8
- streamChat
9
- } from "./chunk-SVPL4GNV.js";
10
2
 
11
3
  // src/index.ts
12
4
  import { Command } from "commander";
13
5
 
6
+ // src/utils/config.ts
7
+ import Conf from "conf";
8
+ var config = new Conf({
9
+ projectName: "alia-codea-cli",
10
+ defaults: {
11
+ apiKey: "",
12
+ apiBaseUrl: "https://api.alia.onl",
13
+ defaultModel: "alia-v1-codea",
14
+ sessions: [],
15
+ currentSessionId: null
16
+ }
17
+ });
18
+ function saveSession(session) {
19
+ const sessions = config.get("sessions") || [];
20
+ const existingIndex = sessions.findIndex((s) => s.id === session.id);
21
+ if (existingIndex >= 0) {
22
+ sessions[existingIndex] = session;
23
+ } else {
24
+ sessions.unshift(session);
25
+ }
26
+ if (sessions.length > 50) {
27
+ sessions.splice(50);
28
+ }
29
+ config.set("sessions", sessions);
30
+ }
31
+ function getSession(id) {
32
+ const sessions = config.get("sessions") || [];
33
+ return sessions.find((s) => s.id === id);
34
+ }
35
+ function getSessions() {
36
+ return config.get("sessions") || [];
37
+ }
38
+ function createSession() {
39
+ const session = {
40
+ id: Date.now().toString(),
41
+ title: "New conversation",
42
+ messages: [],
43
+ createdAt: Date.now(),
44
+ updatedAt: Date.now(),
45
+ cwd: process.cwd()
46
+ };
47
+ saveSession(session);
48
+ config.set("currentSessionId", session.id);
49
+ return session;
50
+ }
51
+
14
52
  // src/commands/repl.ts
15
- import * as readline2 from "readline";
16
- import chalk3 from "chalk";
53
+ import React4 from "react";
54
+ import { render } from "ink";
55
+
56
+ // src/app.tsx
57
+ import { useState as useState3, useCallback, useRef, useEffect as useEffect2 } from "react";
58
+ import { Box as Box7, useApp, useInput as useInput2 } from "ink";
59
+
60
+ // src/components/Header.tsx
61
+ import { Box, Text } from "ink";
62
+ import { jsx, jsxs } from "react/jsx-runtime";
63
+ function shortenPath(p) {
64
+ const home = process.env.HOME || "";
65
+ if (home && p.startsWith(home)) {
66
+ return "~" + p.slice(home.length);
67
+ }
68
+ return p;
69
+ }
70
+ var MODE_COLORS = {
71
+ "suggest": "yellow",
72
+ "auto-edit": "cyan",
73
+ "full-auto": "green"
74
+ };
75
+ function Header({ cwd, model, approvalMode, contextPercent }) {
76
+ return /* @__PURE__ */ jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, justifyContent: "space-between", children: [
77
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: shortenPath(cwd) }),
78
+ /* @__PURE__ */ jsxs(Box, { gap: 1, children: [
79
+ /* @__PURE__ */ jsx(Text, { color: "magenta", children: model }),
80
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "|" }),
81
+ /* @__PURE__ */ jsx(Text, { color: MODE_COLORS[approvalMode], children: approvalMode }),
82
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "|" }),
83
+ /* @__PURE__ */ jsxs(Text, { color: contextPercent < 20 ? "red" : "gray", children: [
84
+ contextPercent,
85
+ "% left"
86
+ ] })
87
+ ] })
88
+ ] });
89
+ }
90
+
91
+ // src/components/MessageList.tsx
92
+ import { Box as Box3, Text as Text3 } from "ink";
93
+ import { Marked } from "marked";
94
+ import { markedTerminal } from "marked-terminal";
95
+
96
+ // src/components/ToolCallCard.tsx
97
+ import { Box as Box2, Text as Text2 } from "ink";
98
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
99
+ function formatArgs(tool, args) {
100
+ switch (tool) {
101
+ case "read_file":
102
+ case "write_file":
103
+ case "edit_file":
104
+ return args.path || "";
105
+ case "apply_patch":
106
+ return "applying patch...";
107
+ case "list_files":
108
+ return args.path || ".";
109
+ case "search_files":
110
+ return `"${args.pattern}" in ${args.path || "."}`;
111
+ case "run_command":
112
+ return args.command || "";
113
+ default:
114
+ return JSON.stringify(args).slice(0, 60);
115
+ }
116
+ }
117
+ function ToolCallCard({ execution }) {
118
+ const { tool, args, result, success, approved } = execution;
119
+ const argStr = formatArgs(tool, args);
120
+ const isDone = result !== void 0;
121
+ if (approved === false) {
122
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingLeft: 2, children: [
123
+ /* @__PURE__ */ jsxs2(Box2, { gap: 1, children: [
124
+ /* @__PURE__ */ jsx2(Text2, { color: "red", children: "\u2717" }),
125
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: "gray", strikethrough: true, children: tool }),
126
+ /* @__PURE__ */ jsx2(Text2, { color: "gray", children: argStr })
127
+ ] }),
128
+ /* @__PURE__ */ jsx2(Box2, { paddingLeft: 2, children: /* @__PURE__ */ jsx2(Text2, { color: "yellow", dimColor: true, children: "Declined by user" }) })
129
+ ] });
130
+ }
131
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingLeft: 2, children: [
132
+ /* @__PURE__ */ jsxs2(Box2, { gap: 1, children: [
133
+ isDone ? /* @__PURE__ */ jsx2(Text2, { color: success ? "green" : "red", children: success ? "\u2713" : "\u2717" }) : /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: "\u2192" }),
134
+ /* @__PURE__ */ jsx2(Text2, { bold: true, children: tool }),
135
+ /* @__PURE__ */ jsx2(Text2, { color: "gray", children: argStr })
136
+ ] }),
137
+ isDone && result && /* @__PURE__ */ jsx2(Box2, { paddingLeft: 2, children: /* @__PURE__ */ jsxs2(Text2, { color: "gray", wrap: "truncate-end", children: [
138
+ result.slice(0, 120).replace(/\n/g, " "),
139
+ result.length > 120 ? "..." : ""
140
+ ] }) })
141
+ ] });
142
+ }
143
+
144
+ // src/components/MessageList.tsx
145
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
146
+ var marked = new Marked(markedTerminal());
147
+ function renderMarkdown(text) {
148
+ try {
149
+ const rendered = marked.parse(text);
150
+ if (typeof rendered === "string") {
151
+ return rendered.trimEnd();
152
+ }
153
+ return text;
154
+ } catch {
155
+ return text;
156
+ }
157
+ }
158
+ function MessageBlock({ message }) {
159
+ switch (message.type) {
160
+ case "user":
161
+ return /* @__PURE__ */ jsxs3(Box3, { paddingLeft: 1, paddingY: 0, children: [
162
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "\u276F " }),
163
+ /* @__PURE__ */ jsx3(Text3, { children: message.content })
164
+ ] });
165
+ case "assistant":
166
+ return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", paddingLeft: 1, children: /* @__PURE__ */ jsxs3(Box3, { children: [
167
+ /* @__PURE__ */ jsx3(Text3, { color: "magenta", children: "\u2726 " }),
168
+ /* @__PURE__ */ jsx3(Text3, { children: message.streaming ? message.content : renderMarkdown(message.content) })
169
+ ] }) });
170
+ case "tool":
171
+ if (message.toolExecution) {
172
+ return /* @__PURE__ */ jsx3(ToolCallCard, { execution: message.toolExecution });
173
+ }
174
+ return null;
175
+ case "info":
176
+ return /* @__PURE__ */ jsxs3(Box3, { paddingLeft: 1, children: [
177
+ /* @__PURE__ */ jsx3(Text3, { color: "blue", children: "\u2139 " }),
178
+ /* @__PURE__ */ jsx3(Text3, { color: "gray", children: message.content })
179
+ ] });
180
+ default:
181
+ return null;
182
+ }
183
+ }
184
+ function MessageList({ messages }) {
185
+ return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", flexGrow: 1, gap: 0, children: messages.map((msg) => /* @__PURE__ */ jsx3(MessageBlock, { message: msg }, msg.id)) });
186
+ }
187
+
188
+ // src/components/InputBar.tsx
189
+ import { useState as useState2 } from "react";
190
+ import { Box as Box5, Text as Text5 } from "ink";
191
+ import TextInput from "ink-text-input";
192
+
193
+ // src/components/ThinkingIndicator.tsx
194
+ import { useState, useEffect } from "react";
195
+ import { Box as Box4, Text as Text4 } from "ink";
196
+ import Spinner from "ink-spinner";
197
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
198
+ function ThinkingIndicator({ label = "Thinking" }) {
199
+ const [elapsed, setElapsed] = useState(0);
200
+ useEffect(() => {
201
+ const interval = setInterval(() => {
202
+ setElapsed((e) => e + 1);
203
+ }, 1e3);
204
+ return () => clearInterval(interval);
205
+ }, []);
206
+ return /* @__PURE__ */ jsxs4(Box4, { gap: 1, children: [
207
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: /* @__PURE__ */ jsx4(Spinner, { type: "dots" }) }),
208
+ /* @__PURE__ */ jsx4(Text4, { bold: true, children: label }),
209
+ /* @__PURE__ */ jsxs4(Text4, { color: "gray", children: [
210
+ "(",
211
+ elapsed,
212
+ "s)"
213
+ ] })
214
+ ] });
215
+ }
216
+
217
+ // src/components/InputBar.tsx
218
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
219
+ function InputBar({ onSubmit, isProcessing, thinkingLabel }) {
220
+ const [value, setValue] = useState2("");
221
+ const handleSubmit = (text) => {
222
+ const trimmed = text.trim();
223
+ if (!trimmed) return;
224
+ setValue("");
225
+ onSubmit(trimmed);
226
+ };
227
+ if (isProcessing) {
228
+ return /* @__PURE__ */ jsx5(Box5, { paddingX: 1, children: /* @__PURE__ */ jsx5(ThinkingIndicator, { label: thinkingLabel || "Thinking" }) });
229
+ }
230
+ return /* @__PURE__ */ jsxs5(Box5, { paddingX: 1, children: [
231
+ /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: "\u276F " }),
232
+ /* @__PURE__ */ jsx5(TextInput, { value, onChange: setValue, onSubmit: handleSubmit })
233
+ ] });
234
+ }
235
+
236
+ // src/components/ApprovalPrompt.tsx
237
+ import { Box as Box6, Text as Text6, useInput } from "ink";
238
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
239
+ function formatArgs2(tool, args) {
240
+ switch (tool) {
241
+ case "write_file":
242
+ return `Write to ${args.path}`;
243
+ case "edit_file":
244
+ return `Edit ${args.path}`;
245
+ case "apply_patch":
246
+ return `Apply patch`;
247
+ case "run_command":
248
+ return `Run: ${args.command}`;
249
+ default:
250
+ return `${tool}: ${JSON.stringify(args).slice(0, 80)}`;
251
+ }
252
+ }
253
+ function ApprovalPrompt({ execution, onResolve }) {
254
+ useInput((input, key) => {
255
+ if (input === "y" || input === "Y") {
256
+ onResolve(true);
257
+ } else if (input === "n" || input === "N" || key.escape) {
258
+ onResolve(false);
259
+ }
260
+ });
261
+ const description = formatArgs2(execution.tool, execution.args);
262
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", paddingLeft: 2, paddingY: 0, children: [
263
+ /* @__PURE__ */ jsxs6(Box6, { gap: 1, children: [
264
+ /* @__PURE__ */ jsx6(Text6, { color: "yellow", children: "\u26A0" }),
265
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: description })
266
+ ] }),
267
+ execution.tool === "run_command" && execution.args.command && /* @__PURE__ */ jsx6(Box6, { paddingLeft: 2, children: /* @__PURE__ */ jsxs6(Text6, { color: "gray", children: [
268
+ "$ ",
269
+ execution.args.command
270
+ ] }) }),
271
+ execution.tool === "write_file" && execution.args.content && /* @__PURE__ */ jsx6(Box6, { paddingLeft: 2, children: /* @__PURE__ */ jsxs6(Text6, { color: "gray", children: [
272
+ execution.args.content.split("\n").length,
273
+ " lines"
274
+ ] }) }),
275
+ /* @__PURE__ */ jsxs6(Box6, { paddingLeft: 2, gap: 1, children: [
276
+ /* @__PURE__ */ jsx6(Text6, { color: "green", children: "[y]" }),
277
+ /* @__PURE__ */ jsx6(Text6, { children: "approve" }),
278
+ /* @__PURE__ */ jsx6(Text6, { color: "red", children: "[n]" }),
279
+ /* @__PURE__ */ jsx6(Text6, { children: "deny" })
280
+ ] })
281
+ ] });
282
+ }
283
+
284
+ // src/utils/api.ts
285
+ import OpenAI from "openai";
286
+ var fileTools = [
287
+ {
288
+ type: "function",
289
+ function: {
290
+ name: "read_file",
291
+ description: "Read the contents of a file",
292
+ parameters: {
293
+ type: "object",
294
+ properties: {
295
+ path: { type: "string", description: "The file path to read" }
296
+ },
297
+ required: ["path"]
298
+ }
299
+ }
300
+ },
301
+ {
302
+ type: "function",
303
+ function: {
304
+ name: "write_file",
305
+ description: "Write content to a file (creates or overwrites)",
306
+ parameters: {
307
+ type: "object",
308
+ properties: {
309
+ path: { type: "string", description: "The file path to write to" },
310
+ content: { type: "string", description: "The content to write" }
311
+ },
312
+ required: ["path", "content"]
313
+ }
314
+ }
315
+ },
316
+ {
317
+ type: "function",
318
+ function: {
319
+ name: "edit_file",
320
+ description: "Make targeted edits to a file by replacing specific text. For small single-location changes.",
321
+ parameters: {
322
+ type: "object",
323
+ properties: {
324
+ path: { type: "string", description: "The file path to edit" },
325
+ old_text: { type: "string", description: "The text to find and replace" },
326
+ new_text: { type: "string", description: "The replacement text" }
327
+ },
328
+ required: ["path", "old_text", "new_text"]
329
+ }
330
+ }
331
+ },
332
+ {
333
+ type: "function",
334
+ function: {
335
+ name: "apply_patch",
336
+ description: "Apply a unified diff patch to one or more files. Preferred for multi-line or multi-file changes. Uses standard unified diff format with fuzzy line matching (\xB120 line drift).",
337
+ parameters: {
338
+ type: "object",
339
+ properties: {
340
+ patch: {
341
+ type: "string",
342
+ description: "The unified diff patch text. Must include --- a/file and +++ b/file headers and @@ hunk headers."
343
+ }
344
+ },
345
+ required: ["patch"]
346
+ }
347
+ }
348
+ },
349
+ {
350
+ type: "function",
351
+ function: {
352
+ name: "list_files",
353
+ description: "List files in a directory",
354
+ parameters: {
355
+ type: "object",
356
+ properties: {
357
+ path: { type: "string", description: "The directory path (default: current directory)" },
358
+ recursive: { type: "boolean", description: "Whether to list recursively" }
359
+ }
360
+ }
361
+ }
362
+ },
363
+ {
364
+ type: "function",
365
+ function: {
366
+ name: "search_files",
367
+ description: "Search for text patterns across files. Uses ripgrep when available for fast results with context lines.",
368
+ parameters: {
369
+ type: "object",
370
+ properties: {
371
+ pattern: { type: "string", description: "The search pattern (regex supported)" },
372
+ path: { type: "string", description: "Directory to search in (default: current)" },
373
+ file_pattern: { type: "string", description: 'File glob pattern (e.g., "*.ts")' },
374
+ context_lines: { type: "number", description: "Number of context lines around matches (default: 2)" },
375
+ max_results: { type: "number", description: "Maximum number of matches to return (default: 50)" }
376
+ },
377
+ required: ["pattern"]
378
+ }
379
+ }
380
+ },
381
+ {
382
+ type: "function",
383
+ function: {
384
+ name: "run_command",
385
+ description: "Execute a shell command",
386
+ parameters: {
387
+ type: "object",
388
+ properties: {
389
+ command: { type: "string", description: "The command to execute" },
390
+ cwd: { type: "string", description: "Working directory (default: current)" }
391
+ },
392
+ required: ["command"]
393
+ }
394
+ }
395
+ }
396
+ ];
397
+ async function streamChat(messages, systemMessage, model, callbacks) {
398
+ const apiKey = config.get("apiKey");
399
+ const baseUrl = config.get("apiBaseUrl") || "https://api.alia.onl";
400
+ const openai = new OpenAI({
401
+ apiKey,
402
+ baseURL: `${baseUrl}/v1`
403
+ });
404
+ const allMessages = [
405
+ { role: "system", content: systemMessage },
406
+ ...messages.map((m) => {
407
+ if (m.role === "tool") {
408
+ return { role: "tool", tool_call_id: m.tool_call_id, content: m.content };
409
+ } else if (m.tool_calls) {
410
+ return { role: "assistant", content: m.content || "", tool_calls: m.tool_calls };
411
+ }
412
+ return { role: m.role, content: m.content };
413
+ })
414
+ ];
415
+ try {
416
+ const stream = await openai.chat.completions.create({
417
+ model,
418
+ messages: allMessages,
419
+ tools: fileTools,
420
+ stream: true
421
+ });
422
+ let fullContent = "";
423
+ const toolCalls = [];
424
+ const toolCallsMap = /* @__PURE__ */ new Map();
425
+ for await (const chunk of stream) {
426
+ const delta = chunk.choices?.[0]?.delta;
427
+ if (!delta) continue;
428
+ if (delta.content) {
429
+ fullContent += delta.content;
430
+ callbacks.onContent(delta.content);
431
+ }
432
+ if (delta.tool_calls) {
433
+ for (const tc of delta.tool_calls) {
434
+ const index = tc.index ?? 0;
435
+ if (!toolCallsMap.has(index)) {
436
+ const newToolCall = {
437
+ id: tc.id || "",
438
+ type: "function",
439
+ function: {
440
+ name: tc.function?.name || "",
441
+ arguments: tc.function?.arguments || ""
442
+ }
443
+ };
444
+ toolCallsMap.set(index, newToolCall);
445
+ toolCalls.push(newToolCall);
446
+ } else {
447
+ const existingToolCall = toolCallsMap.get(index);
448
+ if (tc.function?.name) {
449
+ existingToolCall.function.name = tc.function.name;
450
+ }
451
+ if (tc.function?.arguments) {
452
+ existingToolCall.function.arguments += tc.function.arguments;
453
+ }
454
+ }
455
+ }
456
+ }
457
+ }
458
+ callbacks.onDone(fullContent, toolCalls.length > 0 ? toolCalls : void 0);
459
+ } catch (error) {
460
+ callbacks.onError(error);
461
+ }
462
+ }
17
463
 
18
464
  // src/tools/executor.ts
19
- import * as fs from "fs/promises";
20
- import * as path from "path";
465
+ import * as fs2 from "fs/promises";
466
+ import * as path2 from "path";
21
467
  import { exec } from "child_process";
22
468
  import { promisify } from "util";
23
469
  import chalk from "chalk";
470
+
471
+ // src/tools/patch.ts
472
+ import * as fs from "fs/promises";
473
+ import * as path from "path";
474
+ function parsePatch(patchText) {
475
+ const files = [];
476
+ const lines = patchText.split("\n");
477
+ let currentFile = null;
478
+ let currentHunk = null;
479
+ for (let i = 0; i < lines.length; i++) {
480
+ const line = lines[i];
481
+ if (line.startsWith("--- ")) {
482
+ const nextLine = lines[i + 1];
483
+ if (nextLine && nextLine.startsWith("+++ ")) {
484
+ let filePath = nextLine.slice(4).trim();
485
+ if (filePath.startsWith("b/")) {
486
+ filePath = filePath.slice(2);
487
+ }
488
+ currentFile = { filePath, hunks: [] };
489
+ files.push(currentFile);
490
+ i++;
491
+ continue;
492
+ }
493
+ }
494
+ const hunkMatch = line.match(/^@@ -(\d+)(?:,\d+)? \+\d+(?:,\d+)? @@/);
495
+ if (hunkMatch && currentFile) {
496
+ currentHunk = {
497
+ oldStart: parseInt(hunkMatch[1], 10),
498
+ oldLines: [],
499
+ newLines: []
500
+ };
501
+ currentFile.hunks.push(currentHunk);
502
+ continue;
503
+ }
504
+ if (currentHunk) {
505
+ if (line.startsWith("-")) {
506
+ currentHunk.oldLines.push(line.slice(1));
507
+ } else if (line.startsWith("+")) {
508
+ currentHunk.newLines.push(line.slice(1));
509
+ } else if (line.startsWith(" ")) {
510
+ currentHunk.oldLines.push(line.slice(1));
511
+ currentHunk.newLines.push(line.slice(1));
512
+ }
513
+ }
514
+ }
515
+ return files;
516
+ }
517
+ function findHunkPosition(fileLines, hunkOldLines, expectedStart, drift = 20) {
518
+ const start = expectedStart - 1;
519
+ if (matchesAt(fileLines, hunkOldLines, start)) {
520
+ return start;
521
+ }
522
+ for (let offset = 1; offset <= drift; offset++) {
523
+ if (matchesAt(fileLines, hunkOldLines, start + offset)) {
524
+ return start + offset;
525
+ }
526
+ if (matchesAt(fileLines, hunkOldLines, start - offset)) {
527
+ return start - offset;
528
+ }
529
+ }
530
+ return -1;
531
+ }
532
+ function matchesAt(fileLines, hunkOldLines, position) {
533
+ if (position < 0 || position + hunkOldLines.length > fileLines.length) {
534
+ return false;
535
+ }
536
+ for (let i = 0; i < hunkOldLines.length; i++) {
537
+ const fileLine = fileLines[position + i].trimEnd();
538
+ const hunkLine = hunkOldLines[i].trimEnd();
539
+ if (fileLine !== hunkLine) {
540
+ return false;
541
+ }
542
+ }
543
+ return true;
544
+ }
545
+ async function applyPatch(patchText, basePath) {
546
+ const filePatches = parsePatch(patchText);
547
+ const results = [];
548
+ let allSuccess = true;
549
+ for (const filePatch of filePatches) {
550
+ const absolutePath = path.resolve(basePath, filePatch.filePath);
551
+ try {
552
+ let content;
553
+ try {
554
+ content = await fs.readFile(absolutePath, "utf-8");
555
+ } catch {
556
+ const allAdditions = filePatch.hunks.every((h) => h.oldLines.length === 0);
557
+ if (allAdditions) {
558
+ const newContent = filePatch.hunks.map((h) => h.newLines.join("\n")).join("\n");
559
+ await fs.mkdir(path.dirname(absolutePath), { recursive: true });
560
+ await fs.writeFile(absolutePath, newContent, "utf-8");
561
+ results.push({ file: filePatch.filePath, success: true, message: "Created new file" });
562
+ continue;
563
+ }
564
+ throw new Error(`File not found: ${filePatch.filePath}`);
565
+ }
566
+ let fileLines = content.split("\n");
567
+ const sortedHunks = [...filePatch.hunks].sort((a, b) => b.oldStart - a.oldStart);
568
+ for (const hunk of sortedHunks) {
569
+ const position = findHunkPosition(fileLines, hunk.oldLines, hunk.oldStart);
570
+ if (position === -1) {
571
+ throw new Error(
572
+ `Could not find match for hunk at line ${hunk.oldStart} in ${filePatch.filePath}`
573
+ );
574
+ }
575
+ fileLines.splice(position, hunk.oldLines.length, ...hunk.newLines);
576
+ }
577
+ await fs.writeFile(absolutePath, fileLines.join("\n"), "utf-8");
578
+ results.push({
579
+ file: filePatch.filePath,
580
+ success: true,
581
+ message: `Applied ${filePatch.hunks.length} hunk(s)`
582
+ });
583
+ } catch (error) {
584
+ allSuccess = false;
585
+ results.push({ file: filePatch.filePath, success: false, message: error.message });
586
+ }
587
+ }
588
+ return { success: allSuccess, results };
589
+ }
590
+
591
+ // src/tools/executor.ts
24
592
  var execAsync = promisify(exec);
25
593
  async function executeTool(name, args) {
26
594
  try {
27
595
  switch (name) {
28
596
  case "read_file":
29
- return await readFile2(args.path);
597
+ return await readFile3(args.path);
30
598
  case "write_file":
31
- return await writeFile2(args.path, args.content);
599
+ return await writeFile3(args.path, args.content);
32
600
  case "edit_file":
33
601
  return await editFile(args.path, args.old_text, args.new_text);
602
+ case "apply_patch":
603
+ return await applyPatchTool(args.patch);
34
604
  case "list_files":
35
605
  return await listFiles(args.path, args.recursive);
36
606
  case "search_files":
37
- return await searchFiles(args.pattern, args.path, args.file_pattern);
607
+ return await searchFiles(args.pattern, args.path, args.file_pattern, args.context_lines, args.max_results);
38
608
  case "run_command":
39
609
  return await runCommand(args.command, args.cwd);
40
610
  default:
@@ -44,36 +614,61 @@ async function executeTool(name, args) {
44
614
  return { success: false, result: error.message };
45
615
  }
46
616
  }
47
- async function readFile2(filePath) {
48
- const absolutePath = path.resolve(process.cwd(), filePath);
49
- const content = await fs.readFile(absolutePath, "utf-8");
617
+ async function readFile3(filePath) {
618
+ const absolutePath = path2.resolve(process.cwd(), filePath);
619
+ const content = await fs2.readFile(absolutePath, "utf-8");
50
620
  return { success: true, result: content };
51
621
  }
52
- async function writeFile2(filePath, content) {
53
- const absolutePath = path.resolve(process.cwd(), filePath);
54
- await fs.mkdir(path.dirname(absolutePath), { recursive: true });
55
- await fs.writeFile(absolutePath, content, "utf-8");
622
+ async function writeFile3(filePath, content) {
623
+ const absolutePath = path2.resolve(process.cwd(), filePath);
624
+ await fs2.mkdir(path2.dirname(absolutePath), { recursive: true });
625
+ await fs2.writeFile(absolutePath, content, "utf-8");
56
626
  return { success: true, result: `File written: ${filePath}` };
57
627
  }
58
628
  async function editFile(filePath, oldText, newText) {
59
- const absolutePath = path.resolve(process.cwd(), filePath);
60
- const content = await fs.readFile(absolutePath, "utf-8");
61
- if (!content.includes(oldText)) {
62
- return { success: false, result: `Text not found in file: "${oldText.slice(0, 50)}..."` };
629
+ const absolutePath = path2.resolve(process.cwd(), filePath);
630
+ const content = await fs2.readFile(absolutePath, "utf-8");
631
+ if (content.includes(oldText)) {
632
+ const newContent = content.replace(oldText, newText);
633
+ await fs2.writeFile(absolutePath, newContent, "utf-8");
634
+ return { success: true, result: `File edited: ${filePath}` };
635
+ }
636
+ const normalizedOld = oldText.replace(/\s+/g, " ").trim();
637
+ const lines = content.split("\n");
638
+ let matchStart = -1;
639
+ let matchEnd = -1;
640
+ for (let i = 0; i < lines.length; i++) {
641
+ for (let j = i; j < lines.length; j++) {
642
+ const block = lines.slice(i, j + 1).join("\n");
643
+ if (block.replace(/\s+/g, " ").trim() === normalizedOld) {
644
+ matchStart = i;
645
+ matchEnd = j;
646
+ break;
647
+ }
648
+ }
649
+ if (matchStart >= 0) break;
63
650
  }
64
- const newContent = content.replace(oldText, newText);
65
- await fs.writeFile(absolutePath, newContent, "utf-8");
66
- return { success: true, result: `File edited: ${filePath}` };
651
+ if (matchStart >= 0) {
652
+ const newLines = [...lines.slice(0, matchStart), ...newText.split("\n"), ...lines.slice(matchEnd + 1)];
653
+ await fs2.writeFile(absolutePath, newLines.join("\n"), "utf-8");
654
+ return { success: true, result: `File edited (fuzzy match): ${filePath}` };
655
+ }
656
+ return { success: false, result: `Text not found in file: "${oldText.slice(0, 50)}..."` };
657
+ }
658
+ async function applyPatchTool(patchText) {
659
+ const result = await applyPatch(patchText, process.cwd());
660
+ const summary = result.results.map((r) => `${r.success ? "\u2713" : "\u2717"} ${r.file}: ${r.message}`).join("\n");
661
+ return { success: result.success, result: summary };
67
662
  }
68
663
  async function listFiles(dirPath = ".", recursive = false) {
69
- const absolutePath = path.resolve(process.cwd(), dirPath);
664
+ const absolutePath = path2.resolve(process.cwd(), dirPath);
70
665
  if (recursive) {
71
666
  const files = [];
72
667
  async function walk(dir) {
73
- const entries = await fs.readdir(dir, { withFileTypes: true });
668
+ const entries = await fs2.readdir(dir, { withFileTypes: true });
74
669
  for (const entry of entries) {
75
- const fullPath = path.join(dir, entry.name);
76
- const relativePath = path.relative(absolutePath, fullPath);
670
+ const fullPath = path2.join(dir, entry.name);
671
+ const relativePath = path2.relative(absolutePath, fullPath);
77
672
  if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist") {
78
673
  continue;
79
674
  }
@@ -88,19 +683,62 @@ async function listFiles(dirPath = ".", recursive = false) {
88
683
  await walk(absolutePath);
89
684
  return { success: true, result: files.join("\n") };
90
685
  } else {
91
- const entries = await fs.readdir(absolutePath, { withFileTypes: true });
686
+ const entries = await fs2.readdir(absolutePath, { withFileTypes: true });
92
687
  const files = entries.map((e) => e.name + (e.isDirectory() ? "/" : ""));
93
688
  return { success: true, result: files.join("\n") };
94
689
  }
95
690
  }
96
- async function searchFiles(pattern, dirPath = ".", filePattern) {
97
- const absolutePath = path.resolve(process.cwd(), dirPath);
691
+ async function searchFiles(pattern, dirPath = ".", filePattern, contextLines = 2, maxResults = 50) {
692
+ const absolutePath = path2.resolve(process.cwd(), dirPath);
693
+ try {
694
+ const rgArgs = [
695
+ "--json",
696
+ "-C",
697
+ String(contextLines),
698
+ "-m",
699
+ String(maxResults),
700
+ "--no-heading"
701
+ ];
702
+ if (filePattern) {
703
+ rgArgs.push("-g", filePattern);
704
+ }
705
+ rgArgs.push("--", pattern, absolutePath);
706
+ const { stdout } = await execAsync(`rg ${rgArgs.map((a) => `'${a}'`).join(" ")}`, {
707
+ maxBuffer: 2 * 1024 * 1024,
708
+ timeout: 3e4
709
+ });
710
+ const results2 = [];
711
+ const lines = stdout.trim().split("\n");
712
+ for (const line of lines) {
713
+ try {
714
+ const data = JSON.parse(line);
715
+ if (data.type === "match") {
716
+ const relPath = path2.relative(absolutePath, data.data.path.text);
717
+ const lineNum = data.data.line_number;
718
+ const text = data.data.lines.text.trimEnd();
719
+ results2.push(`${relPath}:${lineNum}: ${text}`);
720
+ } else if (data.type === "context") {
721
+ const relPath = path2.relative(absolutePath, data.data.path.text);
722
+ const lineNum = data.data.line_number;
723
+ const text = data.data.lines.text.trimEnd();
724
+ results2.push(`${relPath}:${lineNum} ${text}`);
725
+ }
726
+ } catch {
727
+ }
728
+ }
729
+ if (results2.length === 0) {
730
+ return { success: true, result: "No matches found." };
731
+ }
732
+ return { success: true, result: results2.join("\n") };
733
+ } catch {
734
+ }
98
735
  const regex = new RegExp(pattern, "gi");
99
736
  const results = [];
737
+ const fileMatchCounts = /* @__PURE__ */ new Map();
100
738
  async function searchDir(dir) {
101
- const entries = await fs.readdir(dir, { withFileTypes: true });
739
+ const entries = await fs2.readdir(dir, { withFileTypes: true });
102
740
  for (const entry of entries) {
103
- const fullPath = path.join(dir, entry.name);
741
+ const fullPath = path2.join(dir, entry.name);
104
742
  if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist") {
105
743
  continue;
106
744
  }
@@ -108,21 +746,39 @@ async function searchFiles(pattern, dirPath = ".", filePattern) {
108
746
  await searchDir(fullPath);
109
747
  } else {
110
748
  if (filePattern) {
111
- const ext = path.extname(entry.name);
749
+ const ext = path2.extname(entry.name);
112
750
  const patternExt = filePattern.replace("*", "");
113
751
  if (ext !== patternExt && !entry.name.match(filePattern.replace("*", ".*"))) {
114
752
  continue;
115
753
  }
116
754
  }
117
755
  try {
118
- const content = await fs.readFile(fullPath, "utf-8");
756
+ const content = await fs2.readFile(fullPath, "utf-8");
757
+ if (content.includes("\0")) continue;
119
758
  const lines = content.split("\n");
759
+ const matchIndices = [];
120
760
  lines.forEach((line, index) => {
761
+ regex.lastIndex = 0;
121
762
  if (regex.test(line)) {
122
- const relativePath = path.relative(absolutePath, fullPath);
123
- results.push(`${relativePath}:${index + 1}: ${line.trim()}`);
763
+ matchIndices.push(index);
124
764
  }
125
765
  });
766
+ if (matchIndices.length > 0) {
767
+ const relativePath = path2.relative(absolutePath, fullPath);
768
+ fileMatchCounts.set(relativePath, matchIndices.length);
769
+ for (const idx of matchIndices) {
770
+ const start = Math.max(0, idx - contextLines);
771
+ const end = Math.min(lines.length - 1, idx + contextLines);
772
+ for (let i = start; i <= end; i++) {
773
+ results.push({
774
+ file: relativePath,
775
+ line: i + 1,
776
+ text: lines[i].trimEnd(),
777
+ isMatch: i === idx
778
+ });
779
+ }
780
+ }
781
+ }
126
782
  } catch {
127
783
  }
128
784
  }
@@ -132,11 +788,14 @@ async function searchFiles(pattern, dirPath = ".", filePattern) {
132
788
  if (results.length === 0) {
133
789
  return { success: true, result: "No matches found." };
134
790
  }
135
- return { success: true, result: results.slice(0, 100).join("\n") + (results.length > 100 ? `
136
- ... and ${results.length - 100} more` : "") };
791
+ const formatted = results.slice(0, maxResults * (1 + contextLines * 2)).map((r) => `${r.file}:${r.line}${r.isMatch ? ":" : " "} ${r.text}`).join("\n");
792
+ const totalMatches = Array.from(fileMatchCounts.values()).reduce((a, b) => a + b, 0);
793
+ const footer = `
794
+ (${totalMatches} matches in ${fileMatchCounts.size} files)`;
795
+ return { success: true, result: formatted + footer };
137
796
  }
138
797
  async function runCommand(command, cwd) {
139
- const workingDir = cwd ? path.resolve(process.cwd(), cwd) : process.cwd();
798
+ const workingDir = cwd ? path2.resolve(process.cwd(), cwd) : process.cwd();
140
799
  try {
141
800
  const { stdout, stderr } = await execAsync(command, {
142
801
  cwd: workingDir,
@@ -156,113 +815,123 @@ ${error.stderr}` : "") || error.message
156
815
  };
157
816
  }
158
817
  }
159
- function formatToolCall(name, args) {
160
- const labels = {
161
- read_file: "Reading file",
162
- write_file: "Writing file",
163
- edit_file: "Editing file",
164
- list_files: "Listing files",
165
- search_files: "Searching files",
166
- run_command: "Running command"
167
- };
168
- const label = labels[name] || name;
169
- const argStr = Object.entries(args).map(([k, v]) => `${k}: ${typeof v === "string" ? v.slice(0, 50) : v}`).join(", ");
170
- return `${chalk.cyan("\u2192")} ${chalk.bold(label)}: ${chalk.gray(argStr)}`;
171
- }
172
818
 
173
- // src/utils/ui.ts
174
- import chalk2 from "chalk";
175
- import * as readline from "readline";
176
- function printTips() {
177
- console.log(chalk2.white("Tips for getting started:"));
178
- console.log(chalk2.gray("1. Ask questions, edit files, or run commands."));
179
- console.log(chalk2.gray("2. Be specific for the best results."));
180
- console.log(chalk2.gray("3. ") + chalk2.cyan("/help") + chalk2.gray(" for more information."));
181
- console.log();
182
- }
183
- function printPrompt() {
184
- process.stdout.write(chalk2.cyan("\u276F "));
819
+ // src/utils/approval.ts
820
+ var TOOL_CATEGORIES = {
821
+ read_file: "read_only",
822
+ list_files: "read_only",
823
+ search_files: "read_only",
824
+ write_file: "file_write",
825
+ edit_file: "file_write",
826
+ apply_patch: "file_write",
827
+ run_command: "shell"
828
+ };
829
+ function categorize(toolName) {
830
+ return TOOL_CATEGORIES[toolName] || "shell";
185
831
  }
186
- function printToolExecution(tool, description) {
187
- const boxWidth = Math.min(process.stdout.columns || 80, 80);
188
- const content = `${chalk2.bold(tool)} ${description}`;
189
- const paddedContent = ` \u2190 ${content} `.padEnd(boxWidth - 4);
190
- console.log();
191
- console.log(chalk2.gray("\u250C" + "\u2500".repeat(boxWidth - 2) + "\u2510"));
192
- console.log(chalk2.gray("\u2502") + paddedContent + chalk2.gray("\u2502"));
193
- console.log(chalk2.gray("\u2514" + "\u2500".repeat(boxWidth - 2) + "\u2518"));
194
- }
195
- function printToolResult(success, result) {
196
- const status = success ? chalk2.green("\u2713") : chalk2.red("\u2717");
197
- const preview = result.slice(0, 100).replace(/\n/g, " ");
198
- console.log(` ${status} ${chalk2.gray(preview)}${result.length > 100 ? "..." : ""}`);
199
- console.log();
200
- }
201
- var statusInterval = null;
202
- var startTime = 0;
203
- function showThinkingStatus(message) {
204
- startTime = Date.now();
205
- const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
206
- let frameIndex = 0;
207
- statusInterval = setInterval(() => {
208
- const elapsed = Math.floor((Date.now() - startTime) / 1e3);
209
- const frame = frames[frameIndex % frames.length];
210
- frameIndex++;
211
- readline.clearLine(process.stdout, 0);
212
- readline.cursorTo(process.stdout, 0);
213
- process.stdout.write(
214
- chalk2.cyan(frame) + " " + chalk2.bold(message) + chalk2.gray(` (esc to cancel, ${elapsed}s)`)
215
- );
216
- }, 80);
217
- }
218
- function hideThinkingStatus() {
219
- if (statusInterval) {
220
- clearInterval(statusInterval);
221
- statusInterval = null;
222
- readline.clearLine(process.stdout, 0);
223
- readline.cursorTo(process.stdout, 0);
224
- }
225
- }
226
- function printStatusBar(cwd, model, contextPercent) {
227
- const width = process.stdout.columns || 80;
228
- const cwdPart = chalk2.cyan(shortenPath(cwd));
229
- const modelPart = chalk2.magenta(`${model} (${contextPercent}% context left)`);
230
- const padding = width - stripAnsi(cwdPart).length - stripAnsi(modelPart).length - 4;
231
- const spacer = " ".repeat(Math.max(padding, 2));
232
- console.log();
233
- console.log(chalk2.gray("\u2500".repeat(width)));
234
- console.log(`${cwdPart}${spacer}${modelPart}`);
235
- }
236
- function shortenPath(p) {
237
- const home = process.env.HOME || "";
238
- if (p.startsWith(home)) {
239
- return "~" + p.slice(home.length);
832
+ function needsApproval(toolName, mode) {
833
+ const category = categorize(toolName);
834
+ switch (mode) {
835
+ case "full-auto":
836
+ return false;
837
+ case "auto-edit":
838
+ return category === "shell";
839
+ case "suggest":
840
+ return category === "file_write" || category === "shell";
841
+ default:
842
+ return true;
240
843
  }
241
- return p;
242
- }
243
- function stripAnsi(str) {
244
- return str.replace(/\x1b\[[0-9;]*m/g, "");
245
- }
246
- function printAssistantPrefix() {
247
- process.stdout.write(chalk2.magenta("\u2726 "));
248
844
  }
249
- function printError(message) {
250
- console.log(chalk2.red("\u2717 Error: ") + message);
251
- }
252
- function printSuccess(message) {
253
- console.log(chalk2.green("\u2713 ") + message);
254
- }
255
- function printInfo(message) {
256
- console.log(chalk2.blue("\u2139 ") + message);
845
+
846
+ // src/utils/conversation.ts
847
+ async function processConversation(opts) {
848
+ const { messages, systemMessage, model, approvalMode, onEvent, requestApproval, isActive } = opts;
849
+ while (isActive()) {
850
+ let fullContent = "";
851
+ let toolCalls;
852
+ onEvent({ type: "thinking" });
853
+ try {
854
+ await streamChat(messages, systemMessage, model, {
855
+ onContent: (content) => {
856
+ if (!isActive()) return;
857
+ fullContent += content;
858
+ onEvent({ type: "content", text: content });
859
+ },
860
+ onToolCall: () => {
861
+ },
862
+ onDone: (_content, tcs) => {
863
+ toolCalls = tcs;
864
+ },
865
+ onError: (error) => {
866
+ onEvent({ type: "error", message: error.message });
867
+ }
868
+ });
869
+ } catch (error) {
870
+ onEvent({ type: "error", message: error.message });
871
+ break;
872
+ }
873
+ if (!isActive()) break;
874
+ if (toolCalls && toolCalls.length > 0) {
875
+ messages.push({
876
+ role: "assistant",
877
+ content: fullContent,
878
+ tool_calls: toolCalls
879
+ });
880
+ for (const tc of toolCalls) {
881
+ if (!isActive()) break;
882
+ const args = JSON.parse(tc.function.arguments);
883
+ const execution = {
884
+ id: tc.id,
885
+ tool: tc.function.name,
886
+ args
887
+ };
888
+ onEvent({ type: "tool_start", execution });
889
+ if (needsApproval(tc.function.name, approvalMode)) {
890
+ const approved = await requestApproval(execution);
891
+ if (!approved) {
892
+ execution.approved = false;
893
+ execution.success = false;
894
+ execution.result = "User declined this action.";
895
+ messages.push({
896
+ role: "tool",
897
+ tool_call_id: tc.id,
898
+ content: "User declined this action."
899
+ });
900
+ onEvent({ type: "tool_done", execution });
901
+ continue;
902
+ }
903
+ execution.approved = true;
904
+ } else {
905
+ execution.approved = true;
906
+ }
907
+ const result = await executeTool(tc.function.name, args);
908
+ execution.result = result.result;
909
+ execution.success = result.success;
910
+ messages.push({
911
+ role: "tool",
912
+ tool_call_id: tc.id,
913
+ content: result.result
914
+ });
915
+ onEvent({ type: "tool_done", execution });
916
+ }
917
+ continue;
918
+ } else {
919
+ if (fullContent) {
920
+ messages.push({ role: "assistant", content: fullContent });
921
+ }
922
+ onEvent({ type: "done", content: fullContent });
923
+ break;
924
+ }
925
+ }
257
926
  }
258
927
 
259
928
  // src/utils/context.ts
260
- import * as fs2 from "fs/promises";
261
- import * as path2 from "path";
929
+ import * as fs3 from "fs/promises";
930
+ import * as path3 from "path";
262
931
  import { exec as exec2 } from "child_process";
263
932
  import { promisify as promisify2 } from "util";
264
933
  var execAsync2 = promisify2(exec2);
265
- function buildSystemMessage(model, codebaseContext) {
934
+ function buildSystemMessage(model, codebaseContext, projectInstructions) {
266
935
  let systemMessage = `You are Codea, an expert AI coding assistant created by Alia. You help developers write, debug, refactor, and understand code directly in their terminal.
267
936
 
268
937
  ## Core Principles
@@ -276,22 +945,30 @@ You have powerful tools to interact with the user's workspace:
276
945
 
277
946
  - **read_file**: Read file contents. Use to understand existing code before making changes.
278
947
  - **write_file**: Create new files or completely rewrite existing ones.
279
- - **edit_file**: Make precise, targeted changes to existing files. Preferred for small modifications.
948
+ - **edit_file**: Make precise, targeted changes using exact text match and replace.
949
+ - **apply_patch**: Apply unified diff patches to files. Preferred for multi-line or multi-file changes. Supports fuzzy line matching.
280
950
  - **list_files**: Explore directory structure. Use to understand project layout.
281
- - **search_files**: Find text/patterns across the codebase. Great for finding usages, definitions, etc.
951
+ - **search_files**: Find text/patterns across the codebase with context lines. Uses ripgrep when available.
282
952
  - **run_command**: Execute shell commands (build, test, git, npm, etc.)
283
953
 
284
954
  ## Best Practices
285
955
  1. **Read before writing**: Always read relevant files before modifying them.
286
956
  2. **Minimal changes**: Make the smallest change necessary to accomplish the task.
287
957
  3. **Preserve style**: Match existing formatting, naming conventions, and patterns.
288
- 4. **Explain when helpful**: For complex changes, briefly explain the approach.
958
+ 4. **Prefer apply_patch**: For multi-line edits, use apply_patch with unified diff format.
959
+ 5. **Explain when helpful**: For complex changes, briefly explain the approach.
289
960
 
290
961
  ## Response Style
291
962
  - Use markdown for formatting code blocks, lists, and emphasis.
292
963
  - For code explanations, be thorough but focused.
293
964
  - For code changes, be precise and action-oriented.
294
965
  - If unsure about requirements, ask clarifying questions.`;
966
+ if (projectInstructions) {
967
+ systemMessage += `
968
+
969
+ ## Project Instructions (from CODEA.md)
970
+ ${projectInstructions}`;
971
+ }
295
972
  if (codebaseContext) {
296
973
  systemMessage += `
297
974
 
@@ -300,12 +977,57 @@ ${codebaseContext}`;
300
977
  }
301
978
  return systemMessage;
302
979
  }
980
+ async function loadProjectInstructions() {
981
+ const parts = [];
982
+ const home = process.env.HOME || "";
983
+ if (home) {
984
+ const globalPath = path3.join(home, ".codea", "CODEA.md");
985
+ try {
986
+ const content = await fs3.readFile(globalPath, "utf-8");
987
+ if (content.trim()) {
988
+ parts.push(`# Global Instructions (~/.codea/CODEA.md)
989
+ ${content.trim()}`);
990
+ }
991
+ } catch {
992
+ }
993
+ }
994
+ let gitRoot = "";
995
+ try {
996
+ const { stdout } = await execAsync2("git rev-parse --show-toplevel", { cwd: process.cwd() });
997
+ gitRoot = stdout.trim();
998
+ } catch {
999
+ }
1000
+ if (gitRoot) {
1001
+ const projectPath = path3.join(gitRoot, "CODEA.md");
1002
+ try {
1003
+ const content = await fs3.readFile(projectPath, "utf-8");
1004
+ if (content.trim()) {
1005
+ parts.push(`# Project Instructions (CODEA.md)
1006
+ ${content.trim()}`);
1007
+ }
1008
+ } catch {
1009
+ }
1010
+ }
1011
+ const cwd = process.cwd();
1012
+ if (cwd !== gitRoot) {
1013
+ const dirPath = path3.join(cwd, "CODEA.md");
1014
+ try {
1015
+ const content = await fs3.readFile(dirPath, "utf-8");
1016
+ if (content.trim()) {
1017
+ parts.push(`# Directory Instructions (./CODEA.md)
1018
+ ${content.trim()}`);
1019
+ }
1020
+ } catch {
1021
+ }
1022
+ }
1023
+ return parts.join("\n\n---\n\n");
1024
+ }
303
1025
  async function getCodebaseContext() {
304
1026
  const cwd = process.cwd();
305
1027
  const contextParts = [];
306
1028
  try {
307
- const pkgPath = path2.join(cwd, "package.json");
308
- const pkgContent = await fs2.readFile(pkgPath, "utf-8");
1029
+ const pkgPath = path3.join(cwd, "package.json");
1030
+ const pkgContent = await fs3.readFile(pkgPath, "utf-8");
309
1031
  const pkg = JSON.parse(pkgContent);
310
1032
  contextParts.push(`Project: ${pkg.name || "Unknown"} (${pkg.description || "No description"})`);
311
1033
  if (pkg.dependencies) {
@@ -342,16 +1064,16 @@ async function getRelevantFiles(dir, maxFiles = 20) {
342
1064
  async function walk(currentDir, depth = 0) {
343
1065
  if (depth > 3 || relevantFiles.length >= maxFiles) return;
344
1066
  try {
345
- const entries = await fs2.readdir(currentDir, { withFileTypes: true });
1067
+ const entries = await fs3.readdir(currentDir, { withFileTypes: true });
346
1068
  for (const entry of entries) {
347
1069
  if (relevantFiles.length >= maxFiles) break;
348
1070
  if (ignoreDirs.includes(entry.name)) continue;
349
- const fullPath = path2.join(currentDir, entry.name);
350
- const relativePath = path2.relative(dir, fullPath);
1071
+ const fullPath = path3.join(currentDir, entry.name);
1072
+ const relativePath = path3.relative(dir, fullPath);
351
1073
  if (entry.isDirectory()) {
352
1074
  await walk(fullPath, depth + 1);
353
1075
  } else {
354
- const ext = path2.extname(entry.name);
1076
+ const ext = path3.extname(entry.name);
355
1077
  if (relevantExtensions.includes(ext) || entry.name === "README.md" || entry.name === "package.json") {
356
1078
  relevantFiles.push(relativePath);
357
1079
  }
@@ -364,326 +1086,369 @@ async function getRelevantFiles(dir, maxFiles = 20) {
364
1086
  return relevantFiles;
365
1087
  }
366
1088
 
367
- // src/commands/repl.ts
368
- async function startRepl(options) {
369
- const session = createSession();
370
- const messages = [];
371
- let isProcessing = false;
372
- let contextUsed = 0;
373
- const maxContext = 128e3;
374
- printTips();
375
- let codebaseContext = "";
376
- if (options.context !== false) {
377
- printInfo("Analyzing codebase...");
378
- codebaseContext = await getCodebaseContext();
379
- if (codebaseContext) {
380
- printInfo(`Loaded context from ${codebaseContext.split("\n").length} files`);
381
- }
382
- }
383
- const rl = readline2.createInterface({
384
- input: process.stdin,
385
- output: process.stdout,
386
- terminal: true
387
- });
388
- rl.on("SIGINT", () => {
389
- if (isProcessing) {
390
- isProcessing = false;
391
- hideThinkingStatus();
392
- console.log(chalk3.yellow("\nCancelled."));
393
- printPrompt();
394
- } else {
395
- console.log(chalk3.gray("\nGoodbye!"));
396
- process.exit(0);
397
- }
398
- });
399
- const askQuestion = () => {
400
- printPrompt();
401
- rl.question("", async (input) => {
402
- const trimmed = input.trim();
403
- if (!trimmed) {
404
- askQuestion();
405
- return;
406
- }
407
- if (trimmed.startsWith("/")) {
408
- await handleSlashCommand(trimmed, messages, session, options);
409
- askQuestion();
410
- return;
411
- }
412
- messages.push({ role: "user", content: trimmed });
413
- isProcessing = true;
414
- const systemMessage = buildSystemMessage(options.model, codebaseContext);
415
- await processConversation(messages, systemMessage, options.model, () => isProcessing);
416
- isProcessing = false;
417
- session.messages = messages.map((m) => ({ role: m.role, content: m.content }));
418
- session.title = messages[0]?.content.slice(0, 50) || "New conversation";
419
- session.updatedAt = Date.now();
420
- saveSession(session);
421
- contextUsed = Math.min(95, Math.floor(messages.reduce((acc, m) => acc + m.content.length, 0) / maxContext * 100));
422
- printStatusBar(process.cwd(), getModelDisplayName(options.model), 100 - contextUsed);
423
- askQuestion();
424
- });
425
- };
426
- askQuestion();
1089
+ // src/app.tsx
1090
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1091
+ var msgCounter = 0;
1092
+ function nextId() {
1093
+ return `msg-${++msgCounter}`;
427
1094
  }
428
- async function processConversation(messages, systemMessage, model, isActive) {
429
- while (isActive()) {
430
- console.log();
431
- printAssistantPrefix();
432
- let fullContent = "";
433
- let toolCalls;
434
- showThinkingStatus("Thinking");
435
- try {
436
- await streamChat(messages, systemMessage, model, {
437
- onContent: (content) => {
438
- if (!isActive()) return;
439
- hideThinkingStatus();
440
- process.stdout.write(content);
441
- fullContent += content;
442
- },
443
- onToolCall: (tc) => {
444
- },
445
- onDone: (content, tcs) => {
446
- hideThinkingStatus();
447
- toolCalls = tcs;
448
- },
449
- onError: (error) => {
450
- hideThinkingStatus();
451
- printError(error.message);
1095
+ function App({ options }) {
1096
+ const { exit } = useApp();
1097
+ const [displayMessages, setDisplayMessages] = useState3([]);
1098
+ const [isProcessing, setIsProcessing] = useState3(false);
1099
+ const [thinkingLabel, setThinkingLabel] = useState3("Thinking");
1100
+ const [approvalMode, setApprovalMode] = useState3(options.approvalMode);
1101
+ const [contextPercent, setContextPercent] = useState3(100);
1102
+ const [pendingApproval, setPendingApproval] = useState3(null);
1103
+ const [ready, setReady] = useState3(false);
1104
+ const [codebaseContext, setCodebaseContext] = useState3("");
1105
+ const [instructions, setInstructions] = useState3("");
1106
+ const messagesRef = useRef([]);
1107
+ const sessionRef = useRef(createSession());
1108
+ const activeRef = useRef(true);
1109
+ const streamingIdRef = useRef(null);
1110
+ useEffect2(() => {
1111
+ let cancelled = false;
1112
+ (async () => {
1113
+ let ctx = "";
1114
+ let instr = "";
1115
+ if (options.context !== false) {
1116
+ ctx = await getCodebaseContext();
1117
+ if (ctx && !cancelled) {
1118
+ setDisplayMessages((prev) => [
1119
+ ...prev,
1120
+ { id: nextId(), type: "info", content: `Loaded context from ${ctx.split("\n").length} lines` }
1121
+ ]);
452
1122
  }
453
- });
454
- } catch (error) {
455
- hideThinkingStatus();
456
- printError(error.message);
457
- break;
458
- }
459
- if (!isActive()) break;
460
- if (toolCalls && toolCalls.length > 0) {
461
- messages.push({
462
- role: "assistant",
463
- content: fullContent,
464
- tool_calls: toolCalls
465
- });
466
- if (fullContent) {
467
- console.log();
468
1123
  }
469
- for (const tc of toolCalls) {
470
- if (!isActive()) break;
471
- const args = JSON.parse(tc.function.arguments);
472
- printToolExecution(tc.function.name, formatToolArgs(tc.function.name, args));
473
- showThinkingStatus(`Executing ${tc.function.name}`);
474
- const result = await executeTool(tc.function.name, args);
475
- hideThinkingStatus();
476
- printToolResult(result.success, result.result);
477
- messages.push({
478
- role: "tool",
479
- tool_call_id: tc.id,
480
- content: result.result
481
- });
1124
+ instr = await loadProjectInstructions();
1125
+ if (instr && !cancelled) {
1126
+ const count = instr.split("\n---\n").length;
1127
+ setDisplayMessages((prev) => [
1128
+ ...prev,
1129
+ { id: nextId(), type: "info", content: `Loaded ${count} CODEA.md instruction file(s)` }
1130
+ ]);
482
1131
  }
483
- continue;
484
- } else {
485
- if (fullContent) {
486
- messages.push({ role: "assistant", content: fullContent });
487
- console.log();
1132
+ if (!cancelled) {
1133
+ setCodebaseContext(ctx);
1134
+ setInstructions(instr);
1135
+ setReady(true);
488
1136
  }
489
- break;
490
- }
491
- }
492
- }
493
- async function handleSlashCommand(command, messages, session, options) {
494
- const [cmd, ...args] = command.slice(1).split(" ");
495
- switch (cmd.toLowerCase()) {
496
- case "help":
497
- console.log();
498
- console.log(chalk3.bold("Available commands:"));
499
- console.log(chalk3.cyan(" /help") + chalk3.gray(" - Show this help"));
500
- console.log(chalk3.cyan(" /clear") + chalk3.gray(" - Clear conversation"));
501
- console.log(chalk3.cyan(" /model") + chalk3.gray(" - Switch model"));
502
- console.log(chalk3.cyan(" /context") + chalk3.gray(" - Show current context"));
503
- console.log(chalk3.cyan(" /save") + chalk3.gray(" - Save conversation"));
504
- console.log(chalk3.cyan(" /exit") + chalk3.gray(" - Exit Codea"));
505
- console.log();
506
- break;
507
- case "clear":
508
- messages.length = 0;
509
- console.log(chalk3.green("Conversation cleared."));
510
- break;
511
- case "model":
512
- const modelArg = args[0];
513
- if (modelArg) {
514
- options.model = modelArg.startsWith("alia-") ? modelArg : `alia-v1-${modelArg}`;
515
- console.log(chalk3.green(`Model switched to ${options.model}`));
1137
+ })();
1138
+ return () => {
1139
+ cancelled = true;
1140
+ };
1141
+ }, []);
1142
+ useInput2((_input, key) => {
1143
+ if (key.ctrl && (_input === "c" || _input === "C")) {
1144
+ if (isProcessing) {
1145
+ activeRef.current = false;
1146
+ setIsProcessing(false);
1147
+ setPendingApproval(null);
1148
+ setDisplayMessages((prev) => [
1149
+ ...prev,
1150
+ { id: nextId(), type: "info", content: "Cancelled." }
1151
+ ]);
516
1152
  } else {
517
- console.log(chalk3.gray("Current model: ") + chalk3.cyan(options.model));
518
- try {
519
- const { fetchModels } = await import("./api-X2G5QROW.js");
520
- const apiModels = await fetchModels();
521
- if (apiModels.length > 0) {
522
- console.log(chalk3.gray("Available models:"));
523
- for (const m of apiModels) {
524
- console.log(chalk3.gray(" ") + chalk3.cyan(m.id) + chalk3.gray(` - ${m.name}`));
525
- }
1153
+ exit();
1154
+ }
1155
+ }
1156
+ });
1157
+ const addMessage = useCallback((msg) => {
1158
+ setDisplayMessages((prev) => [...prev, msg]);
1159
+ }, []);
1160
+ const updateLastAssistant = useCallback((text) => {
1161
+ setDisplayMessages((prev) => {
1162
+ const last = prev[prev.length - 1];
1163
+ if (last && last.type === "assistant" && last.streaming) {
1164
+ return [...prev.slice(0, -1), { ...last, content: last.content + text }];
1165
+ }
1166
+ return prev;
1167
+ });
1168
+ }, []);
1169
+ const finalizeAssistant = useCallback(() => {
1170
+ setDisplayMessages((prev) => {
1171
+ const last = prev[prev.length - 1];
1172
+ if (last && last.type === "assistant" && last.streaming) {
1173
+ return [...prev.slice(0, -1), { ...last, streaming: false }];
1174
+ }
1175
+ return prev;
1176
+ });
1177
+ }, []);
1178
+ const handleSubmit = useCallback(async (input) => {
1179
+ if (input.startsWith("/")) {
1180
+ const [cmd, ...args] = input.slice(1).split(" ");
1181
+ switch (cmd.toLowerCase()) {
1182
+ case "help":
1183
+ addMessage({
1184
+ id: nextId(),
1185
+ type: "info",
1186
+ content: "Commands: /help, /clear, /mode <suggest|auto-edit|full-auto>, /model <name>, /exit"
1187
+ });
1188
+ return;
1189
+ case "clear":
1190
+ messagesRef.current = [];
1191
+ setDisplayMessages([]);
1192
+ setContextPercent(100);
1193
+ return;
1194
+ case "mode":
1195
+ if (args[0] && ["suggest", "auto-edit", "full-auto"].includes(args[0])) {
1196
+ setApprovalMode(args[0]);
1197
+ addMessage({ id: nextId(), type: "info", content: `Approval mode: ${args[0]}` });
526
1198
  } else {
527
- console.log(chalk3.gray("Available: codea, codea-pro, codea-thinking"));
1199
+ addMessage({ id: nextId(), type: "info", content: `Current mode: ${approvalMode}. Options: suggest, auto-edit, full-auto` });
528
1200
  }
529
- } catch {
530
- console.log(chalk3.gray("Available: codea, codea-pro, codea-thinking"));
1201
+ return;
1202
+ case "model":
1203
+ if (args[0]) {
1204
+ options.model = args[0].startsWith("alia-") ? args[0] : `alia-v1-${args[0]}`;
1205
+ addMessage({ id: nextId(), type: "info", content: `Model: ${options.model}` });
1206
+ } else {
1207
+ addMessage({ id: nextId(), type: "info", content: `Current model: ${options.model}` });
1208
+ }
1209
+ return;
1210
+ case "exit":
1211
+ case "quit":
1212
+ exit();
1213
+ return;
1214
+ default:
1215
+ addMessage({ id: nextId(), type: "info", content: `Unknown command: /${cmd}` });
1216
+ return;
1217
+ }
1218
+ }
1219
+ addMessage({ id: nextId(), type: "user", content: input });
1220
+ messagesRef.current.push({ role: "user", content: input });
1221
+ setIsProcessing(true);
1222
+ activeRef.current = true;
1223
+ streamingIdRef.current = null;
1224
+ const systemMessage = buildSystemMessage(options.model, codebaseContext, instructions);
1225
+ await processConversation({
1226
+ messages: messagesRef.current,
1227
+ systemMessage,
1228
+ model: options.model,
1229
+ approvalMode,
1230
+ isActive: () => activeRef.current,
1231
+ requestApproval: (execution) => {
1232
+ return new Promise((resolve3) => {
1233
+ setPendingApproval({ execution, resolve: resolve3 });
1234
+ });
1235
+ },
1236
+ onEvent: (event) => {
1237
+ switch (event.type) {
1238
+ case "thinking":
1239
+ setThinkingLabel("Thinking");
1240
+ streamingIdRef.current = nextId();
1241
+ setDisplayMessages((prev) => [
1242
+ ...prev,
1243
+ { id: streamingIdRef.current, type: "assistant", content: "", streaming: true }
1244
+ ]);
1245
+ break;
1246
+ case "content":
1247
+ updateLastAssistant(event.text);
1248
+ break;
1249
+ case "tool_start":
1250
+ finalizeAssistant();
1251
+ setThinkingLabel(`Running ${event.execution.tool}`);
1252
+ addMessage({
1253
+ id: nextId(),
1254
+ type: "tool",
1255
+ content: "",
1256
+ toolExecution: { ...event.execution }
1257
+ });
1258
+ break;
1259
+ case "tool_done":
1260
+ setDisplayMessages((prev) => {
1261
+ const idx = prev.findLastIndex(
1262
+ (m) => m.type === "tool" && m.toolExecution?.id === event.execution.id
1263
+ );
1264
+ if (idx >= 0) {
1265
+ const updated = [...prev];
1266
+ updated[idx] = {
1267
+ ...updated[idx],
1268
+ toolExecution: { ...event.execution }
1269
+ };
1270
+ return updated;
1271
+ }
1272
+ return prev;
1273
+ });
1274
+ break;
1275
+ case "done":
1276
+ finalizeAssistant();
1277
+ break;
1278
+ case "error":
1279
+ finalizeAssistant();
1280
+ addMessage({ id: nextId(), type: "info", content: `Error: ${event.message}` });
1281
+ break;
531
1282
  }
532
1283
  }
533
- break;
534
- case "context":
535
- console.log(chalk3.gray(`Messages in context: ${messages.length}`));
536
- console.log(chalk3.gray(`Working directory: ${process.cwd()}`));
537
- break;
538
- case "save":
539
- session.messages = messages.map((m) => ({ role: m.role, content: m.content }));
540
- session.updatedAt = Date.now();
541
- saveSession(session);
542
- console.log(chalk3.green("Conversation saved."));
543
- break;
544
- case "exit":
545
- case "quit":
546
- console.log(chalk3.gray("Goodbye!"));
547
- process.exit(0);
548
- break;
549
- default:
550
- console.log(chalk3.yellow(`Unknown command: /${cmd}`));
551
- console.log(chalk3.gray("Type /help for available commands."));
552
- }
553
- }
554
- function formatToolArgs(name, args) {
555
- switch (name) {
556
- case "read_file":
557
- case "write_file":
558
- case "edit_file":
559
- return args.path || "";
560
- case "list_files":
561
- return args.path || ".";
562
- case "search_files":
563
- return `"${args.pattern}" in ${args.path || "."}`;
564
- case "run_command":
565
- return args.command || "";
566
- default:
567
- return JSON.stringify(args).slice(0, 50);
568
- }
1284
+ });
1285
+ setIsProcessing(false);
1286
+ setPendingApproval(null);
1287
+ const session = sessionRef.current;
1288
+ session.messages = messagesRef.current.map((m) => ({ role: m.role, content: m.content }));
1289
+ session.title = messagesRef.current[0]?.content.slice(0, 50) || "New conversation";
1290
+ session.updatedAt = Date.now();
1291
+ saveSession(session);
1292
+ const totalChars = messagesRef.current.reduce((acc, m) => acc + m.content.length, 0);
1293
+ const maxContext = 128e3;
1294
+ setContextPercent(Math.max(5, 100 - Math.floor(totalChars / maxContext * 100)));
1295
+ }, [approvalMode, codebaseContext, instructions, options]);
1296
+ const handleApprovalResolve = useCallback((approved) => {
1297
+ if (pendingApproval) {
1298
+ pendingApproval.resolve(approved);
1299
+ setPendingApproval(null);
1300
+ }
1301
+ }, [pendingApproval]);
1302
+ const modelDisplay = options.model.replace("alia-v1-", "");
1303
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1304
+ /* @__PURE__ */ jsx7(
1305
+ Header,
1306
+ {
1307
+ cwd: process.cwd(),
1308
+ model: modelDisplay,
1309
+ approvalMode,
1310
+ contextPercent
1311
+ }
1312
+ ),
1313
+ /* @__PURE__ */ jsx7(MessageList, { messages: displayMessages }),
1314
+ pendingApproval ? /* @__PURE__ */ jsx7(
1315
+ ApprovalPrompt,
1316
+ {
1317
+ execution: pendingApproval.execution,
1318
+ onResolve: handleApprovalResolve
1319
+ }
1320
+ ) : /* @__PURE__ */ jsx7(
1321
+ InputBar,
1322
+ {
1323
+ onSubmit: handleSubmit,
1324
+ isProcessing,
1325
+ thinkingLabel
1326
+ }
1327
+ )
1328
+ ] });
569
1329
  }
570
- function getModelDisplayName(model) {
571
- const names = {
572
- "alia-v1-codea": "codea",
573
- "alia-v1-pro": "codea-pro",
574
- "alia-v1-thinking": "codea-thinking"
1330
+
1331
+ // src/commands/repl.ts
1332
+ async function startRepl(options) {
1333
+ const appOptions = {
1334
+ model: options.model,
1335
+ approvalMode: options.approvalMode || "suggest",
1336
+ context: options.context
575
1337
  };
576
- return names[model] || model;
1338
+ const { waitUntilExit } = render(React4.createElement(App, { options: appOptions }));
1339
+ await waitUntilExit();
577
1340
  }
578
1341
 
579
1342
  // src/commands/run.ts
580
- import chalk4 from "chalk";
1343
+ import chalk2 from "chalk";
1344
+ import * as readline from "readline";
581
1345
  async function runPrompt(prompt, options) {
582
1346
  const messages = [];
1347
+ const toolResults = [];
583
1348
  let codebaseContext = "";
584
1349
  if (options.context !== false) {
585
1350
  codebaseContext = await getCodebaseContext();
586
1351
  }
1352
+ const instructions = await loadProjectInstructions();
587
1353
  messages.push({ role: "user", content: prompt });
588
- const systemMessage = buildSystemMessage(options.model, codebaseContext);
589
- await processConversation2(messages, systemMessage, options.model, options.yes);
590
- }
591
- async function processConversation2(messages, systemMessage, model, autoApprove) {
592
- let continueProcessing = true;
593
- while (continueProcessing) {
594
- printAssistantPrefix();
595
- let fullContent = "";
596
- let toolCalls;
597
- showThinkingStatus("Thinking");
598
- try {
599
- await streamChat(messages, systemMessage, model, {
600
- onContent: (content) => {
601
- hideThinkingStatus();
602
- process.stdout.write(content);
603
- fullContent += content;
604
- },
605
- onToolCall: () => {
606
- },
607
- onDone: (content, tcs) => {
608
- hideThinkingStatus();
609
- toolCalls = tcs;
610
- },
611
- onError: (error) => {
612
- hideThinkingStatus();
613
- printError(error.message);
614
- continueProcessing = false;
615
- }
616
- });
617
- } catch (error) {
618
- hideThinkingStatus();
619
- printError(error.message);
620
- break;
621
- }
622
- if (toolCalls && toolCalls.length > 0) {
623
- messages.push({
624
- role: "assistant",
625
- content: fullContent,
626
- tool_calls: toolCalls
627
- });
628
- if (fullContent) console.log();
629
- for (const tc of toolCalls) {
630
- const args = JSON.parse(tc.function.arguments);
631
- const isDestructive = ["write_file", "edit_file", "run_command"].includes(tc.function.name);
632
- if (isDestructive && !autoApprove) {
633
- console.log();
634
- console.log(chalk4.yellow("\u26A0 ") + chalk4.bold("Approval required:"));
635
- console.log(formatToolCall(tc.function.name, args));
636
- console.log();
637
- const approved = await askApproval();
638
- if (!approved) {
639
- messages.push({
640
- role: "tool",
641
- tool_call_id: tc.id,
642
- content: "User declined this action."
1354
+ const systemMessage = buildSystemMessage(options.model, codebaseContext, instructions);
1355
+ const approvalMode = options.yes ? "full-auto" : options.approvalMode || "suggest";
1356
+ let fullResponse = "";
1357
+ await processConversation({
1358
+ messages,
1359
+ systemMessage,
1360
+ model: options.model,
1361
+ approvalMode,
1362
+ isActive: () => true,
1363
+ requestApproval: async (execution) => {
1364
+ if (options.quiet || options.json) return false;
1365
+ return askApproval(execution);
1366
+ },
1367
+ onEvent: (event) => {
1368
+ switch (event.type) {
1369
+ case "thinking":
1370
+ if (!options.quiet && !options.json) {
1371
+ process.stdout.write(chalk2.magenta("\u2726 "));
1372
+ }
1373
+ break;
1374
+ case "content":
1375
+ fullResponse += event.text;
1376
+ if (!options.quiet && !options.json) {
1377
+ process.stdout.write(event.text);
1378
+ }
1379
+ break;
1380
+ case "tool_start":
1381
+ if (!options.quiet && !options.json) {
1382
+ console.log();
1383
+ console.log(chalk2.cyan(" \u2192 ") + chalk2.bold(event.execution.tool) + " " + chalk2.gray(formatArgs3(event.execution)));
1384
+ }
1385
+ break;
1386
+ case "tool_done":
1387
+ if (event.execution.result !== void 0) {
1388
+ toolResults.push({
1389
+ tool: event.execution.tool,
1390
+ args: event.execution.args,
1391
+ result: event.execution.result,
1392
+ success: event.execution.success ?? false
643
1393
  });
644
- continue;
645
1394
  }
646
- }
647
- printToolExecution(tc.function.name, formatToolArgs2(tc.function.name, args));
648
- showThinkingStatus(`Executing ${tc.function.name}`);
649
- const result = await executeTool(tc.function.name, args);
650
- hideThinkingStatus();
651
- printToolResult(result.success, result.result);
652
- messages.push({
653
- role: "tool",
654
- tool_call_id: tc.id,
655
- content: result.result
656
- });
657
- }
658
- continue;
659
- } else {
660
- if (fullContent) {
661
- messages.push({ role: "assistant", content: fullContent });
662
- console.log();
1395
+ if (!options.quiet && !options.json) {
1396
+ const icon = event.execution.success ? chalk2.green(" \u2713") : chalk2.red(" \u2717");
1397
+ const preview = (event.execution.result || "").slice(0, 100).replace(/\n/g, " ");
1398
+ console.log(`${icon} ${chalk2.gray(preview)}`);
1399
+ }
1400
+ break;
1401
+ case "done":
1402
+ if (!options.quiet && !options.json) {
1403
+ console.log();
1404
+ }
1405
+ break;
1406
+ case "error":
1407
+ if (!options.json) {
1408
+ console.error(chalk2.red("Error: ") + event.message);
1409
+ }
1410
+ break;
663
1411
  }
664
- break;
1412
+ }
1413
+ });
1414
+ if (options.json) {
1415
+ const output = {
1416
+ model: options.model,
1417
+ prompt,
1418
+ response: fullResponse,
1419
+ tool_calls: toolResults
1420
+ };
1421
+ console.log(JSON.stringify(output, null, 2));
1422
+ } else if (options.quiet) {
1423
+ if (fullResponse) {
1424
+ console.log(fullResponse);
665
1425
  }
666
1426
  }
667
1427
  }
668
- async function askApproval() {
669
- const readline5 = await import("readline");
670
- const rl = readline5.createInterface({
1428
+ async function askApproval(execution) {
1429
+ const rl = readline.createInterface({
671
1430
  input: process.stdin,
672
1431
  output: process.stdout
673
1432
  });
674
- return new Promise((resolve2) => {
675
- rl.question(chalk4.cyan("Allow? [y/N] "), (answer) => {
1433
+ const desc = formatArgs3(execution);
1434
+ console.log();
1435
+ console.log(chalk2.yellow("\u26A0 ") + chalk2.bold(execution.tool) + " " + desc);
1436
+ return new Promise((resolve3) => {
1437
+ rl.question(chalk2.cyan(" Allow? [y/N] "), (answer) => {
676
1438
  rl.close();
677
- resolve2(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
1439
+ resolve3(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
678
1440
  });
679
1441
  });
680
1442
  }
681
- function formatToolArgs2(name, args) {
682
- switch (name) {
1443
+ function formatArgs3(execution) {
1444
+ const { tool, args } = execution;
1445
+ switch (tool) {
683
1446
  case "read_file":
684
1447
  case "write_file":
685
1448
  case "edit_file":
686
1449
  return args.path || "";
1450
+ case "apply_patch":
1451
+ return "applying patch...";
687
1452
  case "list_files":
688
1453
  return args.path || ".";
689
1454
  case "search_files":
@@ -691,16 +1456,25 @@ function formatToolArgs2(name, args) {
691
1456
  case "run_command":
692
1457
  return args.command || "";
693
1458
  default:
694
- return JSON.stringify(args).slice(0, 50);
1459
+ return JSON.stringify(args).slice(0, 60);
695
1460
  }
696
1461
  }
697
1462
 
698
1463
  // src/commands/auth.ts
699
- import * as readline3 from "readline";
1464
+ import * as readline2 from "readline";
700
1465
  import * as crypto from "crypto";
701
1466
  import * as http from "http";
702
1467
  import { exec as exec3 } from "child_process";
703
- import chalk5 from "chalk";
1468
+ import chalk3 from "chalk";
1469
+ function printSuccess(message) {
1470
+ console.log(chalk3.green("\u2713 ") + message);
1471
+ }
1472
+ function printError(message) {
1473
+ console.log(chalk3.red("\u2717 Error: ") + message);
1474
+ }
1475
+ function printInfo(message) {
1476
+ console.log(chalk3.blue("\u2139 ") + message);
1477
+ }
704
1478
  function openBrowser(url) {
705
1479
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? 'start ""' : "xdg-open";
706
1480
  exec3(`${cmd} "${url}"`);
@@ -708,7 +1482,7 @@ function openBrowser(url) {
708
1482
  async function loginWithBrowser() {
709
1483
  const codeVerifier = crypto.randomBytes(32).toString("base64url");
710
1484
  const codeChallenge = crypto.createHash("sha256").update(codeVerifier).digest("base64url");
711
- return new Promise((resolve2) => {
1485
+ return new Promise((resolve3) => {
712
1486
  const server = http.createServer(async (req, res) => {
713
1487
  const url = new URL(req.url, `http://localhost`);
714
1488
  if (url.pathname !== "/callback") {
@@ -724,7 +1498,7 @@ async function loginWithBrowser() {
724
1498
  '<html><body style="font-family:system-ui;text-align:center;padding:60px"><h2>Authorization cancelled</h2><p>You can close this window.</p></body></html>'
725
1499
  );
726
1500
  server.close();
727
- resolve2(false);
1501
+ resolve3(false);
728
1502
  return;
729
1503
  }
730
1504
  try {
@@ -749,7 +1523,7 @@ async function loginWithBrowser() {
749
1523
  console.log();
750
1524
  printSuccess("Logged in successfully!");
751
1525
  server.close();
752
- resolve2(true);
1526
+ resolve3(true);
753
1527
  } else {
754
1528
  throw new Error("No token received");
755
1529
  }
@@ -760,7 +1534,7 @@ async function loginWithBrowser() {
760
1534
  );
761
1535
  printError("Failed to exchange authorization code.");
762
1536
  server.close();
763
- resolve2(false);
1537
+ resolve3(false);
764
1538
  }
765
1539
  });
766
1540
  server.listen(0, () => {
@@ -770,29 +1544,29 @@ async function loginWithBrowser() {
770
1544
  printInfo("Opening browser for authorization...");
771
1545
  openBrowser(authorizeUrl);
772
1546
  console.log(
773
- chalk5.gray("\nIf the browser doesn't open, visit:\n") + chalk5.cyan(authorizeUrl) + "\n"
1547
+ chalk3.gray("\nIf the browser doesn't open, visit:\n") + chalk3.cyan(authorizeUrl) + "\n"
774
1548
  );
775
- console.log(chalk5.gray("Waiting for authorization..."));
1549
+ console.log(chalk3.gray("Waiting for authorization..."));
776
1550
  });
777
1551
  setTimeout(() => {
778
1552
  server.close();
779
1553
  printError("Authorization timed out.");
780
- resolve2(false);
1554
+ resolve3(false);
781
1555
  }, 5 * 60 * 1e3);
782
1556
  });
783
1557
  }
784
1558
  async function loginWithApiKey() {
785
- const rl = readline3.createInterface({
1559
+ const rl = readline2.createInterface({
786
1560
  input: process.stdin,
787
1561
  output: process.stdout
788
1562
  });
789
- return new Promise((resolve2) => {
790
- rl.question(chalk5.cyan("API Key: "), async (apiKey) => {
1563
+ return new Promise((resolve3) => {
1564
+ rl.question(chalk3.cyan("API Key: "), async (apiKey) => {
791
1565
  rl.close();
792
1566
  const trimmedKey = apiKey.trim();
793
1567
  if (!trimmedKey) {
794
1568
  printError("No API key provided.");
795
- resolve2(false);
1569
+ resolve3(false);
796
1570
  return;
797
1571
  }
798
1572
  printInfo("Validating API key...");
@@ -807,63 +1581,63 @@ async function loginWithApiKey() {
807
1581
  console.log();
808
1582
  printSuccess("Logged in successfully!");
809
1583
  if (data.name) {
810
- console.log(chalk5.gray(`Welcome, ${data.name}!`));
1584
+ console.log(chalk3.gray(`Welcome, ${data.name}!`));
811
1585
  }
812
- resolve2(true);
1586
+ resolve3(true);
813
1587
  } else {
814
1588
  printError("Invalid API key. Please check and try again.");
815
- resolve2(false);
1589
+ resolve3(false);
816
1590
  }
817
1591
  } catch (error) {
818
1592
  printError(`Could not validate API key: ${error.message}`);
819
- resolve2(false);
1593
+ resolve3(false);
820
1594
  }
821
1595
  });
822
1596
  });
823
1597
  }
824
1598
  async function login() {
825
1599
  console.log();
826
- console.log(chalk5.bold("Codea CLI Login"));
1600
+ console.log(chalk3.bold("Codea CLI Login"));
827
1601
  console.log();
828
1602
  const success = await loginWithBrowser();
829
1603
  if (success) return true;
830
1604
  console.log();
831
1605
  console.log(
832
- chalk5.gray("Alternatively, paste your API key from: ") + chalk5.cyan("https://alia.onl/settings/api")
1606
+ chalk3.gray("Alternatively, paste your API key from: ") + chalk3.cyan("https://alia.onl/settings/api")
833
1607
  );
834
1608
  console.log();
835
1609
  return loginWithApiKey();
836
1610
  }
837
1611
 
838
1612
  // src/commands/sessions.ts
839
- import chalk6 from "chalk";
840
- import * as readline4 from "readline";
1613
+ import chalk4 from "chalk";
1614
+ import * as readline3 from "readline";
841
1615
  async function listSessions() {
842
1616
  const sessions = getSessions();
843
1617
  if (sessions.length === 0) {
844
- printInfo("No saved sessions found.");
845
- console.log(chalk6.gray("Start a new session with: ") + chalk6.cyan("codea"));
1618
+ console.log(chalk4.blue("\u2139 ") + "No saved sessions found.");
1619
+ console.log(chalk4.gray("Start a new session with: ") + chalk4.cyan("codea"));
846
1620
  return;
847
1621
  }
848
1622
  console.log();
849
- console.log(chalk6.bold("Recent Sessions"));
850
- console.log(chalk6.gray("\u2500".repeat(60)));
1623
+ console.log(chalk4.bold("Recent Sessions"));
1624
+ console.log(chalk4.gray("\u2500".repeat(60)));
851
1625
  sessions.slice(0, 10).forEach((session, index) => {
852
1626
  const date = new Date(session.updatedAt).toLocaleDateString();
853
1627
  const time = new Date(session.updatedAt).toLocaleTimeString();
854
1628
  const messageCount = session.messages?.length || 0;
855
1629
  const title = session.title.slice(0, 40) + (session.title.length > 40 ? "..." : "");
856
1630
  console.log(
857
- chalk6.cyan(`${index + 1}.`) + " " + chalk6.white(title) + " " + chalk6.gray(`(${messageCount} msgs, ${date} ${time})`)
1631
+ chalk4.cyan(`${index + 1}.`) + " " + chalk4.white(title) + " " + chalk4.gray(`(${messageCount} msgs, ${date} ${time})`)
858
1632
  );
859
1633
  });
860
1634
  console.log();
861
- console.log(chalk6.gray("Resume a session with: ") + chalk6.cyan("codea resume <number>"));
1635
+ console.log(chalk4.gray("Resume a session with: ") + chalk4.cyan("codea resume <number>"));
862
1636
  }
863
1637
  async function resumeSession(sessionId) {
864
1638
  const sessions = getSessions();
865
1639
  if (sessions.length === 0) {
866
- printInfo("No saved sessions found.");
1640
+ console.log(chalk4.blue("\u2139 ") + "No saved sessions found.");
867
1641
  return;
868
1642
  }
869
1643
  let selectedSession;
@@ -875,34 +1649,34 @@ async function resumeSession(sessionId) {
875
1649
  selectedSession = getSession(sessionId);
876
1650
  }
877
1651
  if (!selectedSession) {
878
- printError(`Session not found: ${sessionId}`);
1652
+ console.log(chalk4.red("\u2717 Error: ") + `Session not found: ${sessionId}`);
879
1653
  return;
880
1654
  }
881
1655
  } else {
882
1656
  console.log();
883
- console.log(chalk6.bold("Select a session to resume:"));
1657
+ console.log(chalk4.bold("Select a session to resume:"));
884
1658
  console.log();
885
1659
  sessions.slice(0, 10).forEach((session, index) => {
886
1660
  const date = new Date(session.updatedAt).toLocaleDateString();
887
1661
  const title = session.title.slice(0, 50) + (session.title.length > 50 ? "..." : "");
888
- console.log(chalk6.cyan(` ${index + 1}.`) + " " + title + " " + chalk6.gray(`(${date})`));
1662
+ console.log(chalk4.cyan(` ${index + 1}.`) + " " + title + " " + chalk4.gray(`(${date})`));
889
1663
  });
890
1664
  console.log();
891
- const rl = readline4.createInterface({
1665
+ const rl = readline3.createInterface({
892
1666
  input: process.stdin,
893
1667
  output: process.stdout
894
1668
  });
895
- return new Promise((resolve2) => {
896
- rl.question(chalk6.cyan("Enter number: "), (answer) => {
1669
+ return new Promise((resolve3) => {
1670
+ rl.question(chalk4.cyan("Enter number: "), (answer) => {
897
1671
  rl.close();
898
1672
  const index = parseInt(answer) - 1;
899
1673
  if (isNaN(index) || index < 0 || index >= sessions.length) {
900
- printError("Invalid selection.");
901
- resolve2();
1674
+ console.log(chalk4.red("\u2717 Error: ") + "Invalid selection.");
1675
+ resolve3();
902
1676
  return;
903
1677
  }
904
1678
  selectedSession = sessions[index];
905
- startRestoredSession(selectedSession).then(resolve2);
1679
+ startRestoredSession(selectedSession).then(resolve3);
906
1680
  });
907
1681
  });
908
1682
  }
@@ -911,41 +1685,41 @@ async function resumeSession(sessionId) {
911
1685
  }
912
1686
  }
913
1687
  async function startRestoredSession(session) {
914
- printInfo(`Resuming: ${session.title}`);
1688
+ console.log(chalk4.blue("\u2139 ") + `Resuming: ${session.title}`);
915
1689
  console.log();
916
1690
  for (const msg of session.messages || []) {
917
1691
  if (msg.role === "user") {
918
- console.log(chalk6.cyan("\u276F ") + msg.content);
1692
+ console.log(chalk4.cyan("\u276F ") + msg.content);
919
1693
  } else if (msg.role === "assistant") {
920
- console.log(chalk6.magenta("\u2726 ") + msg.content.slice(0, 200) + (msg.content.length > 200 ? "..." : ""));
1694
+ console.log(chalk4.magenta("\u2726 ") + msg.content.slice(0, 200) + (msg.content.length > 200 ? "..." : ""));
921
1695
  }
922
1696
  console.log();
923
1697
  }
924
- console.log(chalk6.gray("\u2500".repeat(60)));
925
- console.log(chalk6.gray("Session restored. Continue the conversation below."));
1698
+ console.log(chalk4.gray("\u2500".repeat(60)));
1699
+ console.log(chalk4.gray("Session restored. Continue the conversation below."));
926
1700
  console.log();
927
1701
  const model = config.get("defaultModel") || "alia-v1-codea";
928
1702
  await startRepl({ model, context: true });
929
1703
  }
930
1704
 
931
1705
  // src/index.ts
932
- import chalk7 from "chalk";
933
- var VERSION = "1.0.0";
1706
+ import chalk5 from "chalk";
1707
+ var VERSION = "2.0.0";
934
1708
  var program = new Command();
935
1709
  var banner = `
936
- ${chalk7.cyan(" ____ _ ")}
937
- ${chalk7.cyan(" / ___|___ __| | ___ __ _ ")}
938
- ${chalk7.cyan(" | | / _ \\ / _` |/ _ \\/ _` |")}
939
- ${chalk7.cyan(" | |__| (_) | (_| | __/ (_| |")}
940
- ${chalk7.cyan(" \\____\\___/ \\__,_|\\___|\\__,_|")}
941
- ${chalk7.gray(" AI Coding Assistant by Alia")}
1710
+ ${chalk5.cyan(" ____ _ ")}
1711
+ ${chalk5.cyan(" / ___|___ __| | ___ __ _ ")}
1712
+ ${chalk5.cyan(" | | / _ \\ / _` |/ _ \\/ _` |")}
1713
+ ${chalk5.cyan(" | |__| (_) | (_| | __/ (_| |")}
1714
+ ${chalk5.cyan(" \\____\\___/ \\__,_|\\___|\\__,_|")}
1715
+ ${chalk5.gray(" AI Coding Assistant by Alia")}
942
1716
  `;
943
1717
  program.name("codea").description("Codea CLI - AI coding assistant for your terminal").version(VERSION).hook("preAction", async () => {
944
1718
  const command = program.args[0];
945
1719
  if (command === "login" || command === "help") return;
946
1720
  if (!config.get("apiKey")) {
947
1721
  console.log(banner);
948
- console.log(chalk7.yellow("No API key found. Let's get you logged in.\n"));
1722
+ console.log(chalk5.yellow("No API key found. Let's get you logged in.\n"));
949
1723
  const success = await login();
950
1724
  if (!success) {
951
1725
  process.exit(1);
@@ -953,13 +1727,15 @@ program.name("codea").description("Codea CLI - AI coding assistant for your term
953
1727
  console.log();
954
1728
  }
955
1729
  });
956
- program.command("chat", { isDefault: true }).description("Start an interactive chat session").option("-m, --model <model>", "Model to use (codea, codea-pro, codea-thinking)", "alia-v1-codea").option("--no-context", "Disable automatic codebase context").action(async (options) => {
957
- console.log(banner);
1730
+ program.command("chat", { isDefault: true }).description("Start an interactive chat session").option("-m, --model <model>", "Model to use (codea, codea-pro, codea-thinking)", "alia-v1-codea").option("-a, --approval-mode <mode>", "Approval mode: suggest, auto-edit, full-auto", "suggest").option("--no-context", "Disable automatic codebase context").option("--no-instructions", "Disable CODEA.md project instructions").action(async (options) => {
958
1731
  await startRepl(options);
959
1732
  });
960
- program.command("run <prompt>").alias("r").description("Run a single prompt and exit").option("-m, --model <model>", "Model to use", "alia-v1-codea").option("-y, --yes", "Auto-approve all file changes").option("--no-context", "Disable automatic codebase context").action(async (prompt, options) => {
1733
+ program.command("run <prompt>").alias("r").description("Run a single prompt and exit").option("-m, --model <model>", "Model to use", "alia-v1-codea").option("-y, --yes", "Auto-approve all actions (full-auto mode)").option("-a, --approval-mode <mode>", "Approval mode: suggest, auto-edit, full-auto", "suggest").option("-q, --quiet", "Suppress UI, output only response text").option("--json", "Output structured JSON").option("--no-context", "Disable automatic codebase context").action(async (prompt, options) => {
961
1734
  await runPrompt(prompt, options);
962
1735
  });
1736
+ program.command("exec <prompt>").alias("x").description("Execute a prompt in full-auto mode with JSON output").option("-m, --model <model>", "Model to use", "alia-v1-codea").option("--no-context", "Disable automatic codebase context").action(async (prompt, options) => {
1737
+ await runPrompt(prompt, { ...options, yes: true, quiet: false, json: true });
1738
+ });
963
1739
  program.command("login").description("Configure your Alia API key").action(async () => {
964
1740
  await login();
965
1741
  });