@zhongqian97-code/ecode 0.0.6 → 0.0.8
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 +706 -137
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -3,15 +3,62 @@ const _ew=process.emitWarning.bind(process);process.emitWarning=function(w,...a)
|
|
|
3
3
|
|
|
4
4
|
// src/index.ts
|
|
5
5
|
import { createRequire } from "module";
|
|
6
|
+
import React4 from "react";
|
|
7
|
+
import { render } from "ink";
|
|
6
8
|
|
|
7
9
|
// src/config.ts
|
|
8
10
|
import { existsSync, readFileSync } from "fs";
|
|
9
11
|
import { homedir } from "os";
|
|
10
12
|
import { join } from "path";
|
|
13
|
+
var MODEL_CONTEXT_LIMITS = {
|
|
14
|
+
"gpt-4o": 128e3,
|
|
15
|
+
"gpt-4o-mini": 128e3,
|
|
16
|
+
"gpt-4-turbo": 128e3,
|
|
17
|
+
"gpt-4": 8192,
|
|
18
|
+
"gpt-3.5-turbo": 16385,
|
|
19
|
+
"o1": 2e5,
|
|
20
|
+
"o1-mini": 128e3,
|
|
21
|
+
"o1-preview": 128e3,
|
|
22
|
+
"o3": 2e5,
|
|
23
|
+
"o3-mini": 2e5,
|
|
24
|
+
"claude-3-5-sonnet-20241022": 2e5,
|
|
25
|
+
"claude-3-5-haiku-20241022": 2e5,
|
|
26
|
+
"claude-3-opus-20240229": 2e5,
|
|
27
|
+
"claude-3-sonnet-20240229": 2e5,
|
|
28
|
+
"claude-3-haiku-20240307": 2e5,
|
|
29
|
+
"claude-sonnet-4-6": 2e5,
|
|
30
|
+
"claude-opus-4-7": 2e5,
|
|
31
|
+
"claude-haiku-4-5-20251001": 2e5,
|
|
32
|
+
"deepseek-chat": 65536,
|
|
33
|
+
"deepseek-reasoner": 65536
|
|
34
|
+
};
|
|
35
|
+
var DEFAULT_CONTEXT_LIMIT = 128e3;
|
|
36
|
+
function getContextLimit(model, override) {
|
|
37
|
+
if (override !== void 0) return override;
|
|
38
|
+
return MODEL_CONTEXT_LIMITS[model] ?? DEFAULT_CONTEXT_LIMIT;
|
|
39
|
+
}
|
|
11
40
|
var DEFAULTS = {
|
|
12
41
|
baseUrl: "https://api.openai.com/v1",
|
|
13
42
|
apiKey: "",
|
|
14
|
-
model: "gpt-4o"
|
|
43
|
+
model: "gpt-4o",
|
|
44
|
+
dangerousPatterns: [
|
|
45
|
+
"rm -rf",
|
|
46
|
+
"sudo",
|
|
47
|
+
"chmod",
|
|
48
|
+
"chown",
|
|
49
|
+
"mkfs",
|
|
50
|
+
"dd",
|
|
51
|
+
"fdisk",
|
|
52
|
+
"kill",
|
|
53
|
+
"pkill",
|
|
54
|
+
"killall",
|
|
55
|
+
"reboot",
|
|
56
|
+
"shutdown",
|
|
57
|
+
"halt",
|
|
58
|
+
"curl -X DELETE",
|
|
59
|
+
"wget --delete-after"
|
|
60
|
+
],
|
|
61
|
+
logDir: void 0
|
|
15
62
|
};
|
|
16
63
|
function loadConfig() {
|
|
17
64
|
const configPath = join(homedir(), ".ecode", "config.json");
|
|
@@ -26,58 +73,19 @@ function loadConfig() {
|
|
|
26
73
|
return {
|
|
27
74
|
baseUrl: process.env.ECODE_BASE_URL ?? fileConfig.baseUrl ?? DEFAULTS.baseUrl,
|
|
28
75
|
apiKey: process.env.ECODE_API_KEY ?? fileConfig.apiKey ?? DEFAULTS.apiKey,
|
|
29
|
-
model: process.env.ECODE_MODEL ?? fileConfig.model ?? DEFAULTS.model
|
|
76
|
+
model: process.env.ECODE_MODEL ?? fileConfig.model ?? DEFAULTS.model,
|
|
77
|
+
// dangerousPatterns: 文件配置覆盖默认值,不支持环境变量(命令数组不适合通过环境变量传递)
|
|
78
|
+
dangerousPatterns: fileConfig.dangerousPatterns ?? DEFAULTS.dangerousPatterns,
|
|
79
|
+
// logDir: ECODE_LOG_DIR > 文件配置 > undefined
|
|
80
|
+
logDir: process.env.ECODE_LOG_DIR ?? fileConfig.logDir ?? DEFAULTS.logDir,
|
|
81
|
+
// contextLimit: 仅支持配置文件,不支持环境变量(数值类型直接在文件中配置更清晰)
|
|
82
|
+
contextLimit: fileConfig.contextLimit
|
|
30
83
|
};
|
|
31
84
|
}
|
|
32
85
|
|
|
33
|
-
// src/
|
|
34
|
-
import
|
|
35
|
-
|
|
36
|
-
// src/safety.ts
|
|
37
|
-
var ALLOWLIST = [
|
|
38
|
-
"ls",
|
|
39
|
-
"cat",
|
|
40
|
-
"pwd",
|
|
41
|
-
"echo",
|
|
42
|
-
"head",
|
|
43
|
-
"tail",
|
|
44
|
-
"wc",
|
|
45
|
-
"date",
|
|
46
|
-
"whoami",
|
|
47
|
-
"which",
|
|
48
|
-
"env",
|
|
49
|
-
"printenv"
|
|
50
|
-
];
|
|
51
|
-
var DANGER_LIST = [
|
|
52
|
-
"rm -rf",
|
|
53
|
-
"sudo",
|
|
54
|
-
"chmod",
|
|
55
|
-
"chown",
|
|
56
|
-
"mkfs",
|
|
57
|
-
"dd",
|
|
58
|
-
"fdisk",
|
|
59
|
-
"kill",
|
|
60
|
-
"pkill",
|
|
61
|
-
"killall",
|
|
62
|
-
"reboot",
|
|
63
|
-
"shutdown",
|
|
64
|
-
"halt",
|
|
65
|
-
"curl -X DELETE",
|
|
66
|
-
"wget --delete-after"
|
|
67
|
-
];
|
|
68
|
-
function matchesEntry(cmd, entry) {
|
|
69
|
-
return cmd === entry || cmd.startsWith(entry + " ");
|
|
70
|
-
}
|
|
71
|
-
function classifyCommand(cmd) {
|
|
72
|
-
const trimmed = cmd.trim();
|
|
73
|
-
for (const entry of DANGER_LIST) {
|
|
74
|
-
if (matchesEntry(trimmed, entry)) return "danger";
|
|
75
|
-
}
|
|
76
|
-
for (const entry of ALLOWLIST) {
|
|
77
|
-
if (matchesEntry(trimmed, entry)) return "allow";
|
|
78
|
-
}
|
|
79
|
-
return "normal";
|
|
80
|
-
}
|
|
86
|
+
// src/ui/App.tsx
|
|
87
|
+
import { useState as useState3, useCallback, useRef, useEffect as useEffect3 } from "react";
|
|
88
|
+
import { Box as Box4, useInput as useInput2 } from "ink";
|
|
81
89
|
|
|
82
90
|
// src/llm.ts
|
|
83
91
|
import OpenAI from "openai";
|
|
@@ -93,12 +101,15 @@ function createLLMClient(config2) {
|
|
|
93
101
|
const requestParams = {
|
|
94
102
|
model: config2.model,
|
|
95
103
|
messages,
|
|
96
|
-
stream: true
|
|
104
|
+
stream: true,
|
|
105
|
+
stream_options: { include_usage: true }
|
|
97
106
|
};
|
|
98
107
|
if (tools && tools.length > 0) {
|
|
99
108
|
requestParams.tools = tools;
|
|
100
109
|
}
|
|
101
|
-
const response = await openai.chat.completions.create(
|
|
110
|
+
const response = await openai.chat.completions.create(
|
|
111
|
+
requestParams
|
|
112
|
+
);
|
|
102
113
|
const tcAccumulator = /* @__PURE__ */ new Map();
|
|
103
114
|
let reasoningAccumulator = "";
|
|
104
115
|
for await (const chunk of response) {
|
|
@@ -125,12 +136,18 @@ function createLLMClient(config2) {
|
|
|
125
136
|
}
|
|
126
137
|
const isLast = choice.finish_reason !== null;
|
|
127
138
|
if (isLast) {
|
|
139
|
+
const rawUsage = chunk.usage;
|
|
128
140
|
yield {
|
|
129
141
|
text: delta.content ?? "",
|
|
130
142
|
done: true,
|
|
131
143
|
finishReason: choice.finish_reason,
|
|
132
144
|
toolCalls: tcAccumulator.size > 0 ? Array.from(tcAccumulator.values()) : void 0,
|
|
133
|
-
reasoning: reasoningAccumulator || void 0
|
|
145
|
+
reasoning: reasoningAccumulator || void 0,
|
|
146
|
+
usage: rawUsage ? {
|
|
147
|
+
promptTokens: rawUsage.prompt_tokens,
|
|
148
|
+
completionTokens: rawUsage.completion_tokens,
|
|
149
|
+
totalTokens: rawUsage.total_tokens
|
|
150
|
+
} : void 0
|
|
134
151
|
};
|
|
135
152
|
} else {
|
|
136
153
|
yield {
|
|
@@ -146,6 +163,56 @@ function createLLMClient(config2) {
|
|
|
146
163
|
};
|
|
147
164
|
}
|
|
148
165
|
|
|
166
|
+
// src/repl.ts
|
|
167
|
+
import * as readline from "readline/promises";
|
|
168
|
+
|
|
169
|
+
// src/safety.ts
|
|
170
|
+
var ALLOWLIST = [
|
|
171
|
+
"ls",
|
|
172
|
+
"cat",
|
|
173
|
+
"pwd",
|
|
174
|
+
"echo",
|
|
175
|
+
"head",
|
|
176
|
+
"tail",
|
|
177
|
+
"wc",
|
|
178
|
+
"date",
|
|
179
|
+
"whoami",
|
|
180
|
+
"which",
|
|
181
|
+
"env",
|
|
182
|
+
"printenv"
|
|
183
|
+
];
|
|
184
|
+
var DEFAULT_DANGER_LIST = [
|
|
185
|
+
"rm -rf",
|
|
186
|
+
"sudo",
|
|
187
|
+
"chmod",
|
|
188
|
+
"chown",
|
|
189
|
+
"mkfs",
|
|
190
|
+
"dd",
|
|
191
|
+
"fdisk",
|
|
192
|
+
"kill",
|
|
193
|
+
"pkill",
|
|
194
|
+
"killall",
|
|
195
|
+
"reboot",
|
|
196
|
+
"shutdown",
|
|
197
|
+
"halt",
|
|
198
|
+
"curl -X DELETE",
|
|
199
|
+
"wget --delete-after"
|
|
200
|
+
];
|
|
201
|
+
function matchesEntry(cmd, entry) {
|
|
202
|
+
return cmd === entry || cmd.startsWith(entry + " ");
|
|
203
|
+
}
|
|
204
|
+
function classifyCommand(cmd, dangerPatterns) {
|
|
205
|
+
const trimmed = cmd.trim();
|
|
206
|
+
const patterns = dangerPatterns ?? DEFAULT_DANGER_LIST;
|
|
207
|
+
for (const entry of patterns) {
|
|
208
|
+
if (matchesEntry(trimmed, entry)) return "danger";
|
|
209
|
+
}
|
|
210
|
+
for (const entry of ALLOWLIST) {
|
|
211
|
+
if (matchesEntry(trimmed, entry)) return "allow";
|
|
212
|
+
}
|
|
213
|
+
return "normal";
|
|
214
|
+
}
|
|
215
|
+
|
|
149
216
|
// src/tools/bash.ts
|
|
150
217
|
import { exec } from "child_process";
|
|
151
218
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
@@ -179,12 +246,14 @@ var BASH_TOOL = {
|
|
|
179
246
|
}
|
|
180
247
|
};
|
|
181
248
|
async function handleBashTool(command, deps) {
|
|
182
|
-
const { confirm, print } = deps;
|
|
183
|
-
const cls = classifyCommand(command);
|
|
249
|
+
const { confirm, print, dangerousPatterns, autoApproveNormal } = deps;
|
|
250
|
+
const cls = classifyCommand(command, dangerousPatterns);
|
|
184
251
|
if (cls === "normal") {
|
|
185
|
-
|
|
252
|
+
if (!autoApproveNormal) {
|
|
253
|
+
const ok = await confirm(`Execute command: ${command}
|
|
186
254
|
Proceed? (y/n) `);
|
|
187
|
-
|
|
255
|
+
if (!ok) return SKIP_MESSAGE;
|
|
256
|
+
}
|
|
188
257
|
} else if (cls === "danger") {
|
|
189
258
|
print(`\u26A0\uFE0F DANGEROUS COMMAND: ${command}`);
|
|
190
259
|
const first = await confirm("Are you sure? (y/n) ");
|
|
@@ -202,107 +271,607 @@ Proceed? (y/n) `);
|
|
|
202
271
|
[exit code: ${result.exitCode}]`;
|
|
203
272
|
return output || "(no output)";
|
|
204
273
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
274
|
+
|
|
275
|
+
// src/logger.ts
|
|
276
|
+
import * as fs from "fs";
|
|
277
|
+
import * as path from "path";
|
|
278
|
+
function createLogger(logDir, sessionStart) {
|
|
279
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
280
|
+
const filename = sessionStart.toISOString().replace(/:/g, "-").replace(/\..+/, "") + ".jsonl";
|
|
281
|
+
const filePath = path.join(logDir, filename);
|
|
282
|
+
return {
|
|
283
|
+
filePath,
|
|
284
|
+
append(entry) {
|
|
285
|
+
try {
|
|
286
|
+
fs.appendFileSync(filePath, JSON.stringify(entry) + "\n");
|
|
287
|
+
} catch (err) {
|
|
288
|
+
process.stderr.write(`[logger] Failed to write log entry: ${err}
|
|
289
|
+
`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
216
292
|
};
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// src/ui/StatusBar.tsx
|
|
296
|
+
import { useEffect, useState } from "react";
|
|
297
|
+
import { Box, Text } from "ink";
|
|
298
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
299
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
300
|
+
var SPINNER_INTERVAL_MS = 80;
|
|
301
|
+
function formatTokenCount(n) {
|
|
302
|
+
if (n < 1e3) return String(n);
|
|
303
|
+
return (n / 1e3).toFixed(1) + "k";
|
|
304
|
+
}
|
|
305
|
+
function buildStatusLabel(status, toolName, confirmPrompt) {
|
|
306
|
+
if (confirmPrompt) {
|
|
307
|
+
return `[y/n] ${confirmPrompt}`;
|
|
308
|
+
}
|
|
309
|
+
switch (status) {
|
|
310
|
+
case "thinking":
|
|
311
|
+
return "\u601D\u8003\u4E2D";
|
|
312
|
+
case "tool_calling":
|
|
313
|
+
return `\u8C03\u7528\u5DE5\u5177: ${toolName ?? ""}`;
|
|
314
|
+
case "awaiting_confirm":
|
|
315
|
+
return "\u7B49\u5F85\u786E\u8BA4";
|
|
316
|
+
case "idle":
|
|
317
|
+
default:
|
|
318
|
+
return "\u7B49\u5F85\u8F93\u5165";
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function statusIcon(status, confirmPrompt) {
|
|
322
|
+
if (confirmPrompt) return "";
|
|
323
|
+
switch (status) {
|
|
324
|
+
case "thinking":
|
|
325
|
+
return "\u27F3";
|
|
326
|
+
case "tool_calling":
|
|
327
|
+
return "\u2699";
|
|
328
|
+
case "awaiting_confirm":
|
|
329
|
+
case "idle":
|
|
330
|
+
default:
|
|
331
|
+
return "\u25CE";
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function StatusBar({
|
|
335
|
+
status,
|
|
336
|
+
toolName,
|
|
337
|
+
confirmPrompt,
|
|
338
|
+
version: version2,
|
|
339
|
+
tokenUsage
|
|
340
|
+
}) {
|
|
341
|
+
const [spinnerIdx, setSpinnerIdx] = useState(0);
|
|
342
|
+
const isAnimating = !confirmPrompt && (status === "thinking" || status === "tool_calling");
|
|
343
|
+
useEffect(() => {
|
|
344
|
+
if (!isAnimating) return;
|
|
345
|
+
const timer = setInterval(() => {
|
|
346
|
+
setSpinnerIdx((prev) => (prev + 1) % SPINNER_FRAMES.length);
|
|
347
|
+
}, SPINNER_INTERVAL_MS);
|
|
348
|
+
return () => clearInterval(timer);
|
|
349
|
+
}, [isAnimating]);
|
|
350
|
+
const label = buildStatusLabel(status, toolName, confirmPrompt);
|
|
351
|
+
const icon = statusIcon(status, confirmPrompt);
|
|
352
|
+
const spinner = isAnimating ? SPINNER_FRAMES[spinnerIdx] : " ";
|
|
353
|
+
const usedStr = formatTokenCount(tokenUsage.used);
|
|
354
|
+
const limitStr = formatTokenCount(tokenUsage.limit);
|
|
355
|
+
const ctxStr = `ctx: ${tokenUsage.estimated ? "~" : ""}${usedStr}/${limitStr}`;
|
|
356
|
+
const versionStr = `v${version2}`;
|
|
357
|
+
return (
|
|
358
|
+
// justifyContent="space-between" 让 Ink 自动撑开左右,
|
|
359
|
+
// 避免用 string.length 手动计算填充(CJK 双宽字符会导致计算偏差)
|
|
360
|
+
/* @__PURE__ */ jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [
|
|
361
|
+
/* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
|
|
362
|
+
/* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
363
|
+
spinner,
|
|
364
|
+
" "
|
|
365
|
+
] }),
|
|
366
|
+
confirmPrompt ? (
|
|
367
|
+
// 确认模式:整体用黄色显示
|
|
368
|
+
/* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
|
|
369
|
+
icon,
|
|
370
|
+
label
|
|
371
|
+
] })
|
|
372
|
+
) : status === "thinking" || status === "tool_calling" ? (
|
|
373
|
+
// 进行中状态:icon 用绿色,文字用白色
|
|
374
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
375
|
+
/* @__PURE__ */ jsxs(Text, { color: "green", children: [
|
|
376
|
+
icon,
|
|
377
|
+
" "
|
|
378
|
+
] }),
|
|
379
|
+
/* @__PURE__ */ jsx(Text, { color: "white", children: label })
|
|
380
|
+
] })
|
|
381
|
+
) : (
|
|
382
|
+
// 空闲 / 等待确认:淡灰
|
|
383
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
384
|
+
/* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
385
|
+
icon,
|
|
386
|
+
" "
|
|
387
|
+
] }),
|
|
388
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: label })
|
|
389
|
+
] })
|
|
390
|
+
)
|
|
391
|
+
] }),
|
|
392
|
+
/* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
|
|
393
|
+
/* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
394
|
+
versionStr,
|
|
395
|
+
" "
|
|
396
|
+
] }),
|
|
397
|
+
/* @__PURE__ */ jsx(Text, { color: tokenUsage.estimated ? "yellow" : "cyan", children: ctxStr })
|
|
398
|
+
] })
|
|
399
|
+
] })
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// src/ui/ConversationHistory.tsx
|
|
404
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
405
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
406
|
+
var TOOL_RESULT_MAX_LINES = 3;
|
|
407
|
+
function truncateLines(text, maxLines) {
|
|
408
|
+
const lines = text.split("\n");
|
|
409
|
+
if (lines.length <= maxLines) return text;
|
|
410
|
+
return lines.slice(0, maxLines).join("\n") + "\n\u2026";
|
|
411
|
+
}
|
|
412
|
+
function UserMessage({
|
|
413
|
+
content
|
|
414
|
+
}) {
|
|
415
|
+
return (
|
|
416
|
+
// 用户消息使用 ">" 前缀,白色,与助手消息对比
|
|
417
|
+
/* @__PURE__ */ jsx2(Box2, { flexDirection: "row", marginBottom: 0, children: /* @__PURE__ */ jsxs2(Text2, { color: "white", children: [
|
|
418
|
+
"> ",
|
|
419
|
+
content
|
|
420
|
+
] }) })
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
function AssistantMessage({
|
|
424
|
+
content,
|
|
425
|
+
tool_calls,
|
|
426
|
+
reasoning_content,
|
|
427
|
+
expandTools
|
|
428
|
+
}) {
|
|
429
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginBottom: 0, children: [
|
|
430
|
+
reasoning_content && reasoning_content.length > 0 && (expandTools ? /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
431
|
+
/* @__PURE__ */ jsx2(Text2, { color: "gray", children: "<thinking>" }),
|
|
432
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: reasoning_content })
|
|
433
|
+
] }) : /* @__PURE__ */ jsx2(Text2, { color: "gray", children: "[+ thinking]" })),
|
|
434
|
+
content && content.trim().length > 0 && /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: content }),
|
|
435
|
+
tool_calls && tool_calls.length > 0 && (expandTools ? tool_calls.map((tc, idx) => {
|
|
436
|
+
let argsDisplay = tc.function.arguments;
|
|
437
|
+
try {
|
|
438
|
+
const parsed = JSON.parse(tc.function.arguments);
|
|
439
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
440
|
+
argsDisplay = Object.values(parsed).map(String).join(", ");
|
|
240
441
|
}
|
|
442
|
+
} catch {
|
|
443
|
+
}
|
|
444
|
+
return /* @__PURE__ */ jsxs2(Text2, { color: "yellow", children: [
|
|
445
|
+
"\u2699 \u8C03\u7528\u5DE5\u5177: ",
|
|
446
|
+
tc.function.name,
|
|
447
|
+
"(",
|
|
448
|
+
argsDisplay,
|
|
449
|
+
")"
|
|
450
|
+
] }, idx);
|
|
451
|
+
}) : /* @__PURE__ */ jsxs2(Text2, { color: "gray", children: [
|
|
452
|
+
"[+ ",
|
|
453
|
+
tool_calls.length,
|
|
454
|
+
" \u4E2A\u5DE5\u5177\u8C03\u7528]"
|
|
455
|
+
] }))
|
|
456
|
+
] });
|
|
457
|
+
}
|
|
458
|
+
function ToolMessage({
|
|
459
|
+
content,
|
|
460
|
+
expandTools
|
|
461
|
+
}) {
|
|
462
|
+
if (!expandTools) {
|
|
463
|
+
return /* @__PURE__ */ jsx2(Box2, { flexDirection: "row", marginBottom: 0, children: /* @__PURE__ */ jsx2(Text2, { color: "gray", children: "[+ \u5DE5\u5177\u7ED3\u679C]" }) });
|
|
464
|
+
}
|
|
465
|
+
const truncated = truncateLines(content, TOOL_RESULT_MAX_LINES);
|
|
466
|
+
return /* @__PURE__ */ jsx2(Box2, { flexDirection: "row", marginBottom: 0, children: /* @__PURE__ */ jsxs2(Text2, { color: "gray", children: [
|
|
467
|
+
"[\u5DE5\u5177\u7ED3\u679C] ",
|
|
468
|
+
truncated
|
|
469
|
+
] }) });
|
|
470
|
+
}
|
|
471
|
+
function estimateLines(msg, expandTools) {
|
|
472
|
+
if (msg.role === "user") {
|
|
473
|
+
return Math.max(1, msg.content.split("\n").length);
|
|
474
|
+
}
|
|
475
|
+
if (msg.role === "assistant") {
|
|
476
|
+
const contentLines = msg.content ? msg.content.split("\n").length : 0;
|
|
477
|
+
const assistantMsg = msg;
|
|
478
|
+
let reasoningLines = 0;
|
|
479
|
+
if (assistantMsg.reasoning_content && assistantMsg.reasoning_content.length > 0) {
|
|
480
|
+
if (expandTools) {
|
|
481
|
+
reasoningLines = 1 + assistantMsg.reasoning_content.split("\n").length;
|
|
482
|
+
} else {
|
|
483
|
+
reasoningLines = 1;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
let toolLines = 0;
|
|
487
|
+
if (assistantMsg.tool_calls && assistantMsg.tool_calls.length > 0) {
|
|
488
|
+
toolLines = expandTools ? assistantMsg.tool_calls.length : 1;
|
|
489
|
+
}
|
|
490
|
+
return Math.max(1, contentLines + reasoningLines + toolLines);
|
|
491
|
+
}
|
|
492
|
+
if (msg.role === "tool") {
|
|
493
|
+
if (!expandTools) return 1;
|
|
494
|
+
return Math.min(TOOL_RESULT_MAX_LINES, msg.content.split("\n").length) + 1;
|
|
495
|
+
}
|
|
496
|
+
return 1;
|
|
497
|
+
}
|
|
498
|
+
function ConversationHistory({
|
|
499
|
+
messages,
|
|
500
|
+
maxHeight = 20,
|
|
501
|
+
expandTools = false
|
|
502
|
+
}) {
|
|
503
|
+
const visible = messages.filter(
|
|
504
|
+
(m) => m.role !== "system"
|
|
505
|
+
);
|
|
506
|
+
let totalLines = 0;
|
|
507
|
+
let startIdx = visible.length;
|
|
508
|
+
for (let i = visible.length - 1; i >= 0; i--) {
|
|
509
|
+
const lines = estimateLines(visible[i], expandTools);
|
|
510
|
+
if (totalLines + lines > maxHeight) break;
|
|
511
|
+
totalLines += lines;
|
|
512
|
+
startIdx = i;
|
|
513
|
+
}
|
|
514
|
+
const displayMessages = visible.slice(startIdx);
|
|
515
|
+
return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: displayMessages.map((msg, idx) => {
|
|
516
|
+
if (msg.role === "user") {
|
|
517
|
+
return /* @__PURE__ */ jsx2(UserMessage, { content: msg.content }, idx);
|
|
518
|
+
}
|
|
519
|
+
if (msg.role === "assistant") {
|
|
520
|
+
const assistantMsg = msg;
|
|
521
|
+
return /* @__PURE__ */ jsx2(
|
|
522
|
+
AssistantMessage,
|
|
523
|
+
{
|
|
524
|
+
content: assistantMsg.content,
|
|
525
|
+
tool_calls: assistantMsg.tool_calls,
|
|
526
|
+
reasoning_content: assistantMsg.reasoning_content,
|
|
527
|
+
expandTools
|
|
528
|
+
},
|
|
529
|
+
idx
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
if (msg.role === "tool") {
|
|
533
|
+
return /* @__PURE__ */ jsx2(ToolMessage, { content: msg.content, expandTools }, idx);
|
|
534
|
+
}
|
|
535
|
+
return null;
|
|
536
|
+
}) });
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// src/ui/Input.tsx
|
|
540
|
+
import { useState as useState2, useEffect as useEffect2 } from "react";
|
|
541
|
+
import { Box as Box3, Text as Text3, useInput } from "ink";
|
|
542
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
543
|
+
var CURSOR_CHAR = "\u258C";
|
|
544
|
+
var BLINK_INTERVAL_MS = 530;
|
|
545
|
+
function Input({ isActive, onSubmit, placeholder }) {
|
|
546
|
+
const [lines, setLines] = useState2([""]);
|
|
547
|
+
const [cursorVisible, setCursorVisible] = useState2(true);
|
|
548
|
+
useEffect2(() => {
|
|
549
|
+
if (!isActive) {
|
|
550
|
+
setCursorVisible(true);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
const timer = setInterval(() => {
|
|
554
|
+
setCursorVisible((prev) => !prev);
|
|
555
|
+
}, BLINK_INTERVAL_MS);
|
|
556
|
+
return () => {
|
|
557
|
+
clearInterval(timer);
|
|
558
|
+
};
|
|
559
|
+
}, [isActive]);
|
|
560
|
+
useInput(
|
|
561
|
+
(input, key) => {
|
|
562
|
+
if (key.return && key.shift) {
|
|
563
|
+
setLines((prev) => [...prev, ""]);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (key.return) {
|
|
567
|
+
const text = lines.join("\n");
|
|
568
|
+
onSubmit(text);
|
|
569
|
+
setLines([""]);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (key.backspace || key.delete) {
|
|
573
|
+
setLines((prev) => {
|
|
574
|
+
const next = [...prev];
|
|
575
|
+
const lastIdx = next.length - 1;
|
|
576
|
+
const lastLine = next[lastIdx];
|
|
577
|
+
if (lastLine.length > 0) {
|
|
578
|
+
next[lastIdx] = lastLine.slice(0, -1);
|
|
579
|
+
return next;
|
|
580
|
+
}
|
|
581
|
+
if (next.length > 1) {
|
|
582
|
+
return next.slice(0, -1);
|
|
583
|
+
}
|
|
584
|
+
return next;
|
|
585
|
+
});
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (key.ctrl || key.escape || key.upArrow || key.downArrow || key.leftArrow || key.rightArrow || key.tab || key.pageUp || key.pageDown) {
|
|
589
|
+
return;
|
|
241
590
|
}
|
|
242
|
-
if (
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
id: tc.id,
|
|
248
|
-
type: "function",
|
|
249
|
-
function: { name: tc.name, arguments: tc.arguments }
|
|
250
|
-
})),
|
|
251
|
-
...assistantReasoning ? { reasoning_content: assistantReasoning } : {}
|
|
591
|
+
if (input.length > 0) {
|
|
592
|
+
setLines((prev) => {
|
|
593
|
+
const next = [...prev];
|
|
594
|
+
next[next.length - 1] = next[next.length - 1] + input;
|
|
595
|
+
return next;
|
|
252
596
|
});
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
597
|
+
}
|
|
598
|
+
},
|
|
599
|
+
{ isActive }
|
|
600
|
+
);
|
|
601
|
+
const isEmpty = lines.every((line) => line === "");
|
|
602
|
+
const renderLines = () => {
|
|
603
|
+
if (isEmpty && placeholder) {
|
|
604
|
+
return /* @__PURE__ */ jsxs3(Box3, { children: [
|
|
605
|
+
/* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "> " }),
|
|
606
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: placeholder }),
|
|
607
|
+
isActive && cursorVisible && /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: CURSOR_CHAR })
|
|
608
|
+
] });
|
|
609
|
+
}
|
|
610
|
+
return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", children: lines.map((line, idx) => {
|
|
611
|
+
const isLastLine = idx === lines.length - 1;
|
|
612
|
+
const prefix = idx === 0 ? "> " : " ";
|
|
613
|
+
return /* @__PURE__ */ jsxs3(Box3, { children: [
|
|
614
|
+
/* @__PURE__ */ jsx3(Text3, { color: "cyan", children: prefix }),
|
|
615
|
+
/* @__PURE__ */ jsx3(Text3, { children: line }),
|
|
616
|
+
isActive && isLastLine && cursorVisible && /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: CURSOR_CHAR })
|
|
617
|
+
] }, idx);
|
|
618
|
+
}) });
|
|
619
|
+
};
|
|
620
|
+
return /* @__PURE__ */ jsx3(Box3, { children: isActive ? renderLines() : /* @__PURE__ */ jsxs3(Box3, { children: [
|
|
621
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "> " }),
|
|
622
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: isEmpty ? placeholder ?? "" : lines.join(" ") })
|
|
623
|
+
] }) });
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// src/ui/App.tsx
|
|
627
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
628
|
+
function App({ config: config2, version: version2, autoMode: autoMode2 = false }) {
|
|
629
|
+
const [messages, setMessages] = useState3([]);
|
|
630
|
+
const [status, setStatus] = useState3("idle");
|
|
631
|
+
const contextLimit = getContextLimit(config2.model, config2.contextLimit);
|
|
632
|
+
const [tokenUsage, setTokenUsage] = useState3({
|
|
633
|
+
used: 0,
|
|
634
|
+
estimated: true,
|
|
635
|
+
limit: contextLimit
|
|
636
|
+
});
|
|
637
|
+
const [toolName, setToolName] = useState3(void 0);
|
|
638
|
+
const [confirmPrompt, setConfirmPrompt] = useState3(void 0);
|
|
639
|
+
const [expandTools, setExpandTools] = useState3(false);
|
|
640
|
+
const pendingConfirmRef = useRef(null);
|
|
641
|
+
const llmRef = useRef(createLLMClient(config2));
|
|
642
|
+
const loggerRef = useRef(null);
|
|
643
|
+
const loggedCountRef = useRef(0);
|
|
644
|
+
useEffect3(() => {
|
|
645
|
+
if (config2.logDir) {
|
|
646
|
+
loggerRef.current = createLogger(config2.logDir, /* @__PURE__ */ new Date());
|
|
647
|
+
}
|
|
648
|
+
}, []);
|
|
649
|
+
useEffect3(() => {
|
|
650
|
+
if (!loggerRef.current) return;
|
|
651
|
+
for (let i = loggedCountRef.current; i < messages.length; i++) {
|
|
652
|
+
const msg = messages[i];
|
|
653
|
+
loggerRef.current.append({
|
|
654
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
655
|
+
role: msg.role,
|
|
656
|
+
content: typeof msg.content === "string" ? msg.content : null,
|
|
657
|
+
tool_call_id: "tool_call_id" in msg ? msg.tool_call_id : void 0,
|
|
658
|
+
tool_calls: "tool_calls" in msg ? msg.tool_calls : void 0
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
loggedCountRef.current = messages.length;
|
|
662
|
+
}, [messages]);
|
|
663
|
+
useInput2((_input, key) => {
|
|
664
|
+
if (key.tab) {
|
|
665
|
+
setExpandTools((prev) => !prev);
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
const confirm = useCallback((prompt) => {
|
|
669
|
+
return new Promise((resolve) => {
|
|
670
|
+
setStatus("awaiting_confirm");
|
|
671
|
+
setConfirmPrompt(prompt);
|
|
672
|
+
pendingConfirmRef.current = { resolve };
|
|
673
|
+
});
|
|
674
|
+
}, []);
|
|
675
|
+
const runLlmLoop = useCallback(
|
|
676
|
+
async (history) => {
|
|
677
|
+
const print = (_text) => {
|
|
678
|
+
};
|
|
679
|
+
const deps = {
|
|
680
|
+
executeBash,
|
|
681
|
+
confirm,
|
|
682
|
+
print,
|
|
683
|
+
dangerousPatterns: config2.dangerousPatterns,
|
|
684
|
+
autoApproveNormal: autoMode2
|
|
685
|
+
};
|
|
686
|
+
let currentMessages = history;
|
|
687
|
+
let continueLoop = true;
|
|
688
|
+
while (continueLoop) {
|
|
689
|
+
continueLoop = false;
|
|
690
|
+
setStatus("thinking");
|
|
691
|
+
let assistantText = "";
|
|
692
|
+
let assistantReasoning;
|
|
693
|
+
const toolCalls = [];
|
|
694
|
+
for await (const chunk of llmRef.current.stream(currentMessages, [BASH_TOOL])) {
|
|
695
|
+
if (chunk.text) {
|
|
696
|
+
assistantText += chunk.text;
|
|
697
|
+
setMessages((prev) => {
|
|
698
|
+
const last = prev[prev.length - 1];
|
|
699
|
+
if (last?.role === "assistant" && !last.tool_calls) {
|
|
700
|
+
return [...prev.slice(0, -1), { ...last, content: assistantText }];
|
|
701
|
+
}
|
|
702
|
+
return [...prev, { role: "assistant", content: assistantText }];
|
|
270
703
|
});
|
|
271
704
|
}
|
|
705
|
+
if (chunk.done) {
|
|
706
|
+
if (chunk.toolCalls) toolCalls.push(...chunk.toolCalls);
|
|
707
|
+
if (chunk.reasoning) assistantReasoning = chunk.reasoning;
|
|
708
|
+
if (chunk.usage) {
|
|
709
|
+
setTokenUsage({
|
|
710
|
+
used: chunk.usage.totalTokens,
|
|
711
|
+
estimated: false,
|
|
712
|
+
limit: getContextLimit(config2.model, config2.contextLimit)
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
} else {
|
|
716
|
+
const estimatedUsed = Math.floor(
|
|
717
|
+
currentMessages.reduce(
|
|
718
|
+
(acc, m) => acc + (typeof m.content === "string" ? m.content.length : 0),
|
|
719
|
+
0
|
|
720
|
+
) * 0.25 + assistantText.length * 0.25
|
|
721
|
+
);
|
|
722
|
+
setTokenUsage((prev) => ({ ...prev, used: estimatedUsed, estimated: true }));
|
|
723
|
+
}
|
|
272
724
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
process.stdout.write("\n");
|
|
276
|
-
if (assistantText) {
|
|
277
|
-
messages.push({
|
|
725
|
+
if (toolCalls.length > 0) {
|
|
726
|
+
const assistantMsg = {
|
|
278
727
|
role: "assistant",
|
|
279
|
-
content: assistantText,
|
|
728
|
+
content: assistantText || null,
|
|
729
|
+
tool_calls: toolCalls.map((tc) => ({
|
|
730
|
+
id: tc.id,
|
|
731
|
+
type: "function",
|
|
732
|
+
function: { name: tc.name, arguments: tc.arguments }
|
|
733
|
+
})),
|
|
280
734
|
...assistantReasoning ? { reasoning_content: assistantReasoning } : {}
|
|
735
|
+
};
|
|
736
|
+
setMessages((prev) => {
|
|
737
|
+
const last = prev[prev.length - 1];
|
|
738
|
+
const withoutStreaming = last?.role === "assistant" && !last.tool_calls ? prev.slice(0, -1) : prev;
|
|
739
|
+
return [...withoutStreaming, assistantMsg];
|
|
740
|
+
});
|
|
741
|
+
currentMessages = [...currentMessages, assistantMsg];
|
|
742
|
+
setStatus("tool_calling");
|
|
743
|
+
for (const tc of toolCalls) {
|
|
744
|
+
if (tc.name === "bash") {
|
|
745
|
+
let args;
|
|
746
|
+
try {
|
|
747
|
+
args = JSON.parse(tc.arguments);
|
|
748
|
+
} catch {
|
|
749
|
+
args = { command: "" };
|
|
750
|
+
}
|
|
751
|
+
setToolName(tc.name);
|
|
752
|
+
const toolResult = await handleBashTool(args.command, deps);
|
|
753
|
+
const toolMsg = {
|
|
754
|
+
role: "tool",
|
|
755
|
+
tool_call_id: tc.id,
|
|
756
|
+
content: toolResult
|
|
757
|
+
};
|
|
758
|
+
setMessages((prev) => [...prev, toolMsg]);
|
|
759
|
+
currentMessages = [...currentMessages, toolMsg];
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
continueLoop = true;
|
|
763
|
+
} else {
|
|
764
|
+
setMessages((prev) => {
|
|
765
|
+
const last = prev[prev.length - 1];
|
|
766
|
+
if (last?.role === "assistant" && !last.tool_calls) {
|
|
767
|
+
const updated = {
|
|
768
|
+
role: "assistant",
|
|
769
|
+
content: assistantText,
|
|
770
|
+
...assistantReasoning ? { reasoning_content: assistantReasoning } : {}
|
|
771
|
+
};
|
|
772
|
+
return [...prev.slice(0, -1), updated];
|
|
773
|
+
}
|
|
774
|
+
return prev;
|
|
281
775
|
});
|
|
776
|
+
if (assistantText) {
|
|
777
|
+
currentMessages = [
|
|
778
|
+
...currentMessages,
|
|
779
|
+
{
|
|
780
|
+
role: "assistant",
|
|
781
|
+
content: assistantText,
|
|
782
|
+
...assistantReasoning ? { reasoning_content: assistantReasoning } : {}
|
|
783
|
+
}
|
|
784
|
+
];
|
|
785
|
+
}
|
|
282
786
|
}
|
|
283
787
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
788
|
+
setStatus("idle");
|
|
789
|
+
setToolName(void 0);
|
|
790
|
+
},
|
|
791
|
+
[confirm, config2.dangerousPatterns, autoMode2]
|
|
792
|
+
);
|
|
793
|
+
const handleSubmit = useCallback(
|
|
794
|
+
(text) => {
|
|
795
|
+
const trimmed = text.trim();
|
|
796
|
+
if (status === "awaiting_confirm") {
|
|
797
|
+
const pending = pendingConfirmRef.current;
|
|
798
|
+
if (pending) {
|
|
799
|
+
pendingConfirmRef.current = null;
|
|
800
|
+
setConfirmPrompt(void 0);
|
|
801
|
+
setStatus("tool_calling");
|
|
802
|
+
pending.resolve(trimmed.toLowerCase() === "y");
|
|
803
|
+
}
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
if (!trimmed) return;
|
|
807
|
+
const userMsg = { role: "user", content: trimmed };
|
|
808
|
+
const nextMessages = [...messages, userMsg];
|
|
809
|
+
setMessages(nextMessages);
|
|
810
|
+
runLlmLoop(nextMessages).catch((err) => {
|
|
811
|
+
setStatus("idle");
|
|
812
|
+
setToolName(void 0);
|
|
813
|
+
setMessages((prev) => [
|
|
814
|
+
...prev,
|
|
815
|
+
{ role: "assistant", content: `[error] ${String(err)}` }
|
|
816
|
+
]);
|
|
817
|
+
});
|
|
818
|
+
},
|
|
819
|
+
[status, messages, runLlmLoop]
|
|
820
|
+
);
|
|
821
|
+
const isInputActive = status === "idle" || status === "awaiting_confirm";
|
|
822
|
+
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", height: "100%", children: [
|
|
823
|
+
/* @__PURE__ */ jsx4(ConversationHistory, { messages, expandTools }),
|
|
824
|
+
/* @__PURE__ */ jsx4(
|
|
825
|
+
StatusBar,
|
|
826
|
+
{
|
|
827
|
+
status,
|
|
828
|
+
toolName,
|
|
829
|
+
confirmPrompt,
|
|
830
|
+
version: version2,
|
|
831
|
+
tokenUsage
|
|
832
|
+
}
|
|
833
|
+
),
|
|
834
|
+
/* @__PURE__ */ jsx4(
|
|
835
|
+
Input,
|
|
836
|
+
{
|
|
837
|
+
isActive: isInputActive,
|
|
838
|
+
onSubmit: handleSubmit,
|
|
839
|
+
placeholder: status === "awaiting_confirm" ? "y / n" : void 0
|
|
840
|
+
}
|
|
841
|
+
)
|
|
842
|
+
] });
|
|
288
843
|
}
|
|
289
844
|
|
|
290
845
|
// src/index.ts
|
|
291
846
|
var require2 = createRequire(import.meta.url);
|
|
292
847
|
var { version } = require2("../package.json");
|
|
293
|
-
var
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
848
|
+
var VERSION = version;
|
|
849
|
+
var rawArgs = process.argv.slice(2);
|
|
850
|
+
var autoMode = false;
|
|
851
|
+
var cliLogDir;
|
|
852
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
853
|
+
const arg = rawArgs[i];
|
|
854
|
+
if (arg === "-v" || arg === "--version") {
|
|
855
|
+
console.log(VERSION);
|
|
856
|
+
process.exit(0);
|
|
857
|
+
}
|
|
858
|
+
if (arg === "--auto") {
|
|
859
|
+
autoMode = true;
|
|
860
|
+
}
|
|
861
|
+
if (arg === "--log-dir") {
|
|
862
|
+
const next = rawArgs[i + 1];
|
|
863
|
+
if (next && !next.startsWith("-")) {
|
|
864
|
+
cliLogDir = next;
|
|
865
|
+
i++;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
297
868
|
}
|
|
298
869
|
var config = loadConfig();
|
|
299
|
-
|
|
870
|
+
var finalConfig = cliLogDir ? { ...config, logDir: cliLogDir } : config;
|
|
871
|
+
if (!finalConfig.apiKey) {
|
|
300
872
|
console.error(
|
|
301
873
|
"Error: no API key configured.\nSet ECODE_API_KEY or add apiKey to ~/.ecode/config.json"
|
|
302
874
|
);
|
|
303
875
|
process.exit(1);
|
|
304
876
|
}
|
|
305
|
-
|
|
306
|
-
console.error("Fatal error:", err);
|
|
307
|
-
process.exit(1);
|
|
308
|
-
});
|
|
877
|
+
render(React4.createElement(App, { config: finalConfig, version: VERSION, autoMode }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhongqian97-code/ecode",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "A minimal Claude Code clone with REPL interface and bash tool calling",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "zhongqian97-code",
|
|
@@ -39,7 +39,10 @@
|
|
|
39
39
|
"test:watch": "vitest"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"
|
|
42
|
+
"@types/react": "^19.2.14",
|
|
43
|
+
"ink": "^7.0.2",
|
|
44
|
+
"openai": "^4.0.0",
|
|
45
|
+
"react": "^19.2.5"
|
|
43
46
|
},
|
|
44
47
|
"devDependencies": {
|
|
45
48
|
"@types/node": "^18.0.0",
|