@alia-codea/cli 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1362 -491
- package/package.json +5 -5
- package/src/app.tsx +281 -0
- package/src/commands/auth.ts +149 -20
- package/src/commands/repl.ts +11 -299
- package/src/commands/run.ts +103 -126
- package/src/commands/sessions.ts +5 -6
- package/src/components/ApprovalPrompt.tsx +60 -0
- package/src/components/Header.tsx +39 -0
- package/src/components/InputBar.tsx +36 -0
- package/src/components/MessageList.tsx +81 -0
- package/src/components/ThinkingIndicator.tsx +28 -0
- package/src/components/ToolCallCard.tsx +68 -0
- package/src/index.ts +31 -11
- package/src/tools/executor.ts +140 -14
- package/src/tools/patch.ts +167 -0
- package/src/utils/api.ts +22 -3
- package/src/utils/approval.ts +31 -0
- package/src/utils/context.ts +65 -4
- package/src/utils/conversation.ts +141 -0
- package/dist/api-X2G5QROW.js +0 -10
- package/dist/chunk-SVPL4GNV.js +0 -230
- package/dist/index.d.ts +0 -1
- package/src/utils/ui.ts +0 -153
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
|
|
16
|
-
import
|
|
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
|
|
20
|
-
import * as
|
|
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
|
|
597
|
+
return await readFile3(args.path);
|
|
30
598
|
case "write_file":
|
|
31
|
-
return await
|
|
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
|
|
48
|
-
const absolutePath =
|
|
49
|
-
const content = await
|
|
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
|
|
53
|
-
const absolutePath =
|
|
54
|
-
await
|
|
55
|
-
await
|
|
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 =
|
|
60
|
-
const content = await
|
|
61
|
-
if (
|
|
62
|
-
|
|
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}` };
|
|
63
635
|
}
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
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;
|
|
650
|
+
}
|
|
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 =
|
|
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
|
|
668
|
+
const entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
74
669
|
for (const entry of entries) {
|
|
75
|
-
const fullPath =
|
|
76
|
-
const relativePath =
|
|
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
|
|
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 =
|
|
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
|
|
739
|
+
const entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
102
740
|
for (const entry of entries) {
|
|
103
|
-
const fullPath =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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 ?
|
|
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/
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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";
|
|
182
831
|
}
|
|
183
|
-
function
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
844
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
|
261
|
-
import * as
|
|
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
|
|
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
|
|
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. **
|
|
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 =
|
|
308
|
-
const pkgContent = await
|
|
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
|
|
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 =
|
|
350
|
-
const relativePath =
|
|
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 =
|
|
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/
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
console.log();
|
|
1132
|
+
if (!cancelled) {
|
|
1133
|
+
setCodebaseContext(ctx);
|
|
1134
|
+
setInstructions(instr);
|
|
1135
|
+
setReady(true);
|
|
488
1136
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
|
|
1199
|
+
addMessage({ id: nextId(), type: "info", content: `Current mode: ${approvalMode}. Options: suggest, auto-edit, full-auto` });
|
|
528
1200
|
}
|
|
529
|
-
|
|
530
|
-
|
|
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
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
-
|
|
1338
|
+
const { waitUntilExit } = render(React4.createElement(App, { options: appOptions }));
|
|
1339
|
+
await waitUntilExit();
|
|
577
1340
|
}
|
|
578
1341
|
|
|
579
1342
|
// src/commands/run.ts
|
|
580
|
-
import
|
|
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
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
675
|
-
|
|
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
|
-
|
|
1439
|
+
resolve3(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
678
1440
|
});
|
|
679
1441
|
});
|
|
680
1442
|
}
|
|
681
|
-
function
|
|
682
|
-
|
|
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,90 +1456,188 @@ function formatToolArgs2(name, args) {
|
|
|
691
1456
|
case "run_command":
|
|
692
1457
|
return args.command || "";
|
|
693
1458
|
default:
|
|
694
|
-
return JSON.stringify(args).slice(0,
|
|
1459
|
+
return JSON.stringify(args).slice(0, 60);
|
|
695
1460
|
}
|
|
696
1461
|
}
|
|
697
1462
|
|
|
698
1463
|
// src/commands/auth.ts
|
|
699
|
-
import * as
|
|
700
|
-
import
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
console.log(
|
|
706
|
-
|
|
707
|
-
|
|
1464
|
+
import * as readline2 from "readline";
|
|
1465
|
+
import * as crypto from "crypto";
|
|
1466
|
+
import * as http from "http";
|
|
1467
|
+
import { exec as exec3 } from "child_process";
|
|
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
|
+
}
|
|
1478
|
+
function openBrowser(url) {
|
|
1479
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? 'start ""' : "xdg-open";
|
|
1480
|
+
exec3(`${cmd} "${url}"`);
|
|
1481
|
+
}
|
|
1482
|
+
async function loginWithBrowser() {
|
|
1483
|
+
const codeVerifier = crypto.randomBytes(32).toString("base64url");
|
|
1484
|
+
const codeChallenge = crypto.createHash("sha256").update(codeVerifier).digest("base64url");
|
|
1485
|
+
return new Promise((resolve3) => {
|
|
1486
|
+
const server = http.createServer(async (req, res) => {
|
|
1487
|
+
const url = new URL(req.url, `http://localhost`);
|
|
1488
|
+
if (url.pathname !== "/callback") {
|
|
1489
|
+
res.writeHead(404);
|
|
1490
|
+
res.end();
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
const code = url.searchParams.get("code");
|
|
1494
|
+
const error = url.searchParams.get("error");
|
|
1495
|
+
if (error || !code) {
|
|
1496
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1497
|
+
res.end(
|
|
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>'
|
|
1499
|
+
);
|
|
1500
|
+
server.close();
|
|
1501
|
+
resolve3(false);
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
try {
|
|
1505
|
+
const baseUrl = config.get("apiBaseUrl") || "https://api.alia.onl";
|
|
1506
|
+
const response = await fetch(`${baseUrl}/auth/token`, {
|
|
1507
|
+
method: "POST",
|
|
1508
|
+
headers: { "Content-Type": "application/json" },
|
|
1509
|
+
body: JSON.stringify({
|
|
1510
|
+
grant_type: "authorization_code",
|
|
1511
|
+
code,
|
|
1512
|
+
code_verifier: codeVerifier,
|
|
1513
|
+
client_id: "codea"
|
|
1514
|
+
})
|
|
1515
|
+
});
|
|
1516
|
+
const data = await response.json();
|
|
1517
|
+
if (data.token) {
|
|
1518
|
+
config.set("apiKey", data.token);
|
|
1519
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1520
|
+
res.end(
|
|
1521
|
+
'<html><body style="font-family:system-ui;text-align:center;padding:60px"><h2>Logged in!</h2><p>You can close this window and return to the terminal.</p></body></html>'
|
|
1522
|
+
);
|
|
1523
|
+
console.log();
|
|
1524
|
+
printSuccess("Logged in successfully!");
|
|
1525
|
+
server.close();
|
|
1526
|
+
resolve3(true);
|
|
1527
|
+
} else {
|
|
1528
|
+
throw new Error("No token received");
|
|
1529
|
+
}
|
|
1530
|
+
} catch {
|
|
1531
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1532
|
+
res.end(
|
|
1533
|
+
'<html><body style="font-family:system-ui;text-align:center;padding:60px"><h2>Login failed</h2><p>Please try again.</p></body></html>'
|
|
1534
|
+
);
|
|
1535
|
+
printError("Failed to exchange authorization code.");
|
|
1536
|
+
server.close();
|
|
1537
|
+
resolve3(false);
|
|
1538
|
+
}
|
|
1539
|
+
});
|
|
1540
|
+
server.listen(0, () => {
|
|
1541
|
+
const port = server.address().port;
|
|
1542
|
+
const callback = encodeURIComponent(`http://localhost:${port}/callback`);
|
|
1543
|
+
const authorizeUrl = `https://alia.onl/authorize?app=codea&callback=${callback}&code_challenge=${codeChallenge}&code_challenge_method=S256`;
|
|
1544
|
+
printInfo("Opening browser for authorization...");
|
|
1545
|
+
openBrowser(authorizeUrl);
|
|
1546
|
+
console.log(
|
|
1547
|
+
chalk3.gray("\nIf the browser doesn't open, visit:\n") + chalk3.cyan(authorizeUrl) + "\n"
|
|
1548
|
+
);
|
|
1549
|
+
console.log(chalk3.gray("Waiting for authorization..."));
|
|
1550
|
+
});
|
|
1551
|
+
setTimeout(() => {
|
|
1552
|
+
server.close();
|
|
1553
|
+
printError("Authorization timed out.");
|
|
1554
|
+
resolve3(false);
|
|
1555
|
+
}, 5 * 60 * 1e3);
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
async function loginWithApiKey() {
|
|
1559
|
+
const rl = readline2.createInterface({
|
|
708
1560
|
input: process.stdin,
|
|
709
1561
|
output: process.stdout
|
|
710
1562
|
});
|
|
711
|
-
return new Promise((
|
|
712
|
-
rl.question(
|
|
1563
|
+
return new Promise((resolve3) => {
|
|
1564
|
+
rl.question(chalk3.cyan("API Key: "), async (apiKey) => {
|
|
713
1565
|
rl.close();
|
|
714
1566
|
const trimmedKey = apiKey.trim();
|
|
715
1567
|
if (!trimmedKey) {
|
|
716
1568
|
printError("No API key provided.");
|
|
717
|
-
|
|
1569
|
+
resolve3(false);
|
|
718
1570
|
return;
|
|
719
1571
|
}
|
|
720
1572
|
printInfo("Validating API key...");
|
|
721
1573
|
try {
|
|
722
1574
|
const baseUrl = config.get("apiBaseUrl") || "https://api.alia.onl";
|
|
723
1575
|
const response = await fetch(`${baseUrl}/codea/me`, {
|
|
724
|
-
headers: {
|
|
725
|
-
"Authorization": `Bearer ${trimmedKey}`
|
|
726
|
-
}
|
|
1576
|
+
headers: { Authorization: `Bearer ${trimmedKey}` }
|
|
727
1577
|
});
|
|
728
1578
|
if (response.ok) {
|
|
729
1579
|
const data = await response.json();
|
|
730
1580
|
config.set("apiKey", trimmedKey);
|
|
731
1581
|
console.log();
|
|
732
|
-
printSuccess(
|
|
1582
|
+
printSuccess("Logged in successfully!");
|
|
733
1583
|
if (data.name) {
|
|
734
|
-
console.log(
|
|
1584
|
+
console.log(chalk3.gray(`Welcome, ${data.name}!`));
|
|
735
1585
|
}
|
|
736
|
-
|
|
737
|
-
console.log(chalk5.gray("Run ") + chalk5.cyan("codea") + chalk5.gray(" to start coding."));
|
|
1586
|
+
resolve3(true);
|
|
738
1587
|
} else {
|
|
739
1588
|
printError("Invalid API key. Please check and try again.");
|
|
1589
|
+
resolve3(false);
|
|
740
1590
|
}
|
|
741
1591
|
} catch (error) {
|
|
742
1592
|
printError(`Could not validate API key: ${error.message}`);
|
|
1593
|
+
resolve3(false);
|
|
743
1594
|
}
|
|
744
|
-
resolve2();
|
|
745
1595
|
});
|
|
746
1596
|
});
|
|
747
1597
|
}
|
|
1598
|
+
async function login() {
|
|
1599
|
+
console.log();
|
|
1600
|
+
console.log(chalk3.bold("Codea CLI Login"));
|
|
1601
|
+
console.log();
|
|
1602
|
+
const success = await loginWithBrowser();
|
|
1603
|
+
if (success) return true;
|
|
1604
|
+
console.log();
|
|
1605
|
+
console.log(
|
|
1606
|
+
chalk3.gray("Alternatively, paste your API key from: ") + chalk3.cyan("https://alia.onl/settings/api")
|
|
1607
|
+
);
|
|
1608
|
+
console.log();
|
|
1609
|
+
return loginWithApiKey();
|
|
1610
|
+
}
|
|
748
1611
|
|
|
749
1612
|
// src/commands/sessions.ts
|
|
750
|
-
import
|
|
751
|
-
import * as
|
|
1613
|
+
import chalk4 from "chalk";
|
|
1614
|
+
import * as readline3 from "readline";
|
|
752
1615
|
async function listSessions() {
|
|
753
1616
|
const sessions = getSessions();
|
|
754
1617
|
if (sessions.length === 0) {
|
|
755
|
-
|
|
756
|
-
console.log(
|
|
1618
|
+
console.log(chalk4.blue("\u2139 ") + "No saved sessions found.");
|
|
1619
|
+
console.log(chalk4.gray("Start a new session with: ") + chalk4.cyan("codea"));
|
|
757
1620
|
return;
|
|
758
1621
|
}
|
|
759
1622
|
console.log();
|
|
760
|
-
console.log(
|
|
761
|
-
console.log(
|
|
1623
|
+
console.log(chalk4.bold("Recent Sessions"));
|
|
1624
|
+
console.log(chalk4.gray("\u2500".repeat(60)));
|
|
762
1625
|
sessions.slice(0, 10).forEach((session, index) => {
|
|
763
1626
|
const date = new Date(session.updatedAt).toLocaleDateString();
|
|
764
1627
|
const time = new Date(session.updatedAt).toLocaleTimeString();
|
|
765
1628
|
const messageCount = session.messages?.length || 0;
|
|
766
1629
|
const title = session.title.slice(0, 40) + (session.title.length > 40 ? "..." : "");
|
|
767
1630
|
console.log(
|
|
768
|
-
|
|
1631
|
+
chalk4.cyan(`${index + 1}.`) + " " + chalk4.white(title) + " " + chalk4.gray(`(${messageCount} msgs, ${date} ${time})`)
|
|
769
1632
|
);
|
|
770
1633
|
});
|
|
771
1634
|
console.log();
|
|
772
|
-
console.log(
|
|
1635
|
+
console.log(chalk4.gray("Resume a session with: ") + chalk4.cyan("codea resume <number>"));
|
|
773
1636
|
}
|
|
774
1637
|
async function resumeSession(sessionId) {
|
|
775
1638
|
const sessions = getSessions();
|
|
776
1639
|
if (sessions.length === 0) {
|
|
777
|
-
|
|
1640
|
+
console.log(chalk4.blue("\u2139 ") + "No saved sessions found.");
|
|
778
1641
|
return;
|
|
779
1642
|
}
|
|
780
1643
|
let selectedSession;
|
|
@@ -786,34 +1649,34 @@ async function resumeSession(sessionId) {
|
|
|
786
1649
|
selectedSession = getSession(sessionId);
|
|
787
1650
|
}
|
|
788
1651
|
if (!selectedSession) {
|
|
789
|
-
|
|
1652
|
+
console.log(chalk4.red("\u2717 Error: ") + `Session not found: ${sessionId}`);
|
|
790
1653
|
return;
|
|
791
1654
|
}
|
|
792
1655
|
} else {
|
|
793
1656
|
console.log();
|
|
794
|
-
console.log(
|
|
1657
|
+
console.log(chalk4.bold("Select a session to resume:"));
|
|
795
1658
|
console.log();
|
|
796
1659
|
sessions.slice(0, 10).forEach((session, index) => {
|
|
797
1660
|
const date = new Date(session.updatedAt).toLocaleDateString();
|
|
798
1661
|
const title = session.title.slice(0, 50) + (session.title.length > 50 ? "..." : "");
|
|
799
|
-
console.log(
|
|
1662
|
+
console.log(chalk4.cyan(` ${index + 1}.`) + " " + title + " " + chalk4.gray(`(${date})`));
|
|
800
1663
|
});
|
|
801
1664
|
console.log();
|
|
802
|
-
const rl =
|
|
1665
|
+
const rl = readline3.createInterface({
|
|
803
1666
|
input: process.stdin,
|
|
804
1667
|
output: process.stdout
|
|
805
1668
|
});
|
|
806
|
-
return new Promise((
|
|
807
|
-
rl.question(
|
|
1669
|
+
return new Promise((resolve3) => {
|
|
1670
|
+
rl.question(chalk4.cyan("Enter number: "), (answer) => {
|
|
808
1671
|
rl.close();
|
|
809
1672
|
const index = parseInt(answer) - 1;
|
|
810
1673
|
if (isNaN(index) || index < 0 || index >= sessions.length) {
|
|
811
|
-
|
|
812
|
-
|
|
1674
|
+
console.log(chalk4.red("\u2717 Error: ") + "Invalid selection.");
|
|
1675
|
+
resolve3();
|
|
813
1676
|
return;
|
|
814
1677
|
}
|
|
815
1678
|
selectedSession = sessions[index];
|
|
816
|
-
startRestoredSession(selectedSession).then(
|
|
1679
|
+
startRestoredSession(selectedSession).then(resolve3);
|
|
817
1680
|
});
|
|
818
1681
|
});
|
|
819
1682
|
}
|
|
@@ -822,49 +1685,57 @@ async function resumeSession(sessionId) {
|
|
|
822
1685
|
}
|
|
823
1686
|
}
|
|
824
1687
|
async function startRestoredSession(session) {
|
|
825
|
-
|
|
1688
|
+
console.log(chalk4.blue("\u2139 ") + `Resuming: ${session.title}`);
|
|
826
1689
|
console.log();
|
|
827
1690
|
for (const msg of session.messages || []) {
|
|
828
1691
|
if (msg.role === "user") {
|
|
829
|
-
console.log(
|
|
1692
|
+
console.log(chalk4.cyan("\u276F ") + msg.content);
|
|
830
1693
|
} else if (msg.role === "assistant") {
|
|
831
|
-
console.log(
|
|
1694
|
+
console.log(chalk4.magenta("\u2726 ") + msg.content.slice(0, 200) + (msg.content.length > 200 ? "..." : ""));
|
|
832
1695
|
}
|
|
833
1696
|
console.log();
|
|
834
1697
|
}
|
|
835
|
-
console.log(
|
|
836
|
-
console.log(
|
|
1698
|
+
console.log(chalk4.gray("\u2500".repeat(60)));
|
|
1699
|
+
console.log(chalk4.gray("Session restored. Continue the conversation below."));
|
|
837
1700
|
console.log();
|
|
838
1701
|
const model = config.get("defaultModel") || "alia-v1-codea";
|
|
839
1702
|
await startRepl({ model, context: true });
|
|
840
1703
|
}
|
|
841
1704
|
|
|
842
1705
|
// src/index.ts
|
|
843
|
-
import
|
|
844
|
-
var VERSION = "
|
|
1706
|
+
import chalk5 from "chalk";
|
|
1707
|
+
var VERSION = "2.0.0";
|
|
845
1708
|
var program = new Command();
|
|
846
1709
|
var banner = `
|
|
847
|
-
${
|
|
848
|
-
${
|
|
849
|
-
${
|
|
850
|
-
${
|
|
851
|
-
${
|
|
852
|
-
${
|
|
1710
|
+
${chalk5.cyan(" ____ _ ")}
|
|
1711
|
+
${chalk5.cyan(" / ___|___ __| | ___ __ _ ")}
|
|
1712
|
+
${chalk5.cyan(" | | / _ \\ / _` |/ _ \\/ _` |")}
|
|
1713
|
+
${chalk5.cyan(" | |__| (_) | (_| | __/ (_| |")}
|
|
1714
|
+
${chalk5.cyan(" \\____\\___/ \\__,_|\\___|\\__,_|")}
|
|
1715
|
+
${chalk5.gray(" AI Coding Assistant by Alia")}
|
|
853
1716
|
`;
|
|
854
|
-
program.name("codea").description("Codea CLI - AI coding assistant for your terminal").version(VERSION).hook("preAction", () => {
|
|
1717
|
+
program.name("codea").description("Codea CLI - AI coding assistant for your terminal").version(VERSION).hook("preAction", async () => {
|
|
855
1718
|
const command = program.args[0];
|
|
856
|
-
if (command
|
|
857
|
-
|
|
858
|
-
|
|
1719
|
+
if (command === "login" || command === "help") return;
|
|
1720
|
+
if (!config.get("apiKey")) {
|
|
1721
|
+
console.log(banner);
|
|
1722
|
+
console.log(chalk5.yellow("No API key found. Let's get you logged in.\n"));
|
|
1723
|
+
const success = await login();
|
|
1724
|
+
if (!success) {
|
|
1725
|
+
process.exit(1);
|
|
1726
|
+
}
|
|
1727
|
+
console.log();
|
|
859
1728
|
}
|
|
860
1729
|
});
|
|
861
|
-
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) => {
|
|
862
|
-
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) => {
|
|
863
1731
|
await startRepl(options);
|
|
864
1732
|
});
|
|
865
|
-
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
|
|
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) => {
|
|
866
1734
|
await runPrompt(prompt, options);
|
|
867
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
|
+
});
|
|
868
1739
|
program.command("login").description("Configure your Alia API key").action(async () => {
|
|
869
1740
|
await login();
|
|
870
1741
|
});
|