@zhongqian97-code/ecode 0.0.7 → 0.1.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 +375 -103
- package/package.json +2 -1
- package/skills/caveman/SKILL.md +49 -0
- package/skills/diagnose/SKILL.md +117 -0
- package/skills/grill-me/SKILL.md +10 -0
- package/skills/grill-with-docs/ADR-FORMAT.md +47 -0
- package/skills/grill-with-docs/CONTEXT-FORMAT.md +77 -0
- package/skills/grill-with-docs/SKILL.md +88 -0
- package/skills/improve-codebase-architecture/DEEPENING.md +37 -0
- package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +44 -0
- package/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
- package/skills/improve-codebase-architecture/SKILL.md +71 -0
- package/skills/plan/SKILL.md +16 -0
- package/skills/search-first/SKILL.md +20 -0
- package/skills/security-review/SKILL.md +26 -0
- package/skills/setup-matt-pocock-skills/SKILL.md +121 -0
- package/skills/setup-matt-pocock-skills/domain.md +51 -0
- package/skills/setup-matt-pocock-skills/issue-tracker-github.md +22 -0
- package/skills/setup-matt-pocock-skills/issue-tracker-gitlab.md +23 -0
- package/skills/setup-matt-pocock-skills/issue-tracker-local.md +19 -0
- package/skills/setup-matt-pocock-skills/triage-labels.md +15 -0
- package/skills/tdd/SKILL.md +109 -0
- package/skills/tdd/deep-modules.md +33 -0
- package/skills/tdd/interface-design.md +31 -0
- package/skills/tdd/mocking.md +59 -0
- package/skills/tdd/refactoring.md +10 -0
- package/skills/tdd/tests.md +61 -0
- package/skills/to-issues/SKILL.md +83 -0
- package/skills/to-prd/SKILL.md +76 -0
- package/skills/triage/AGENT-BRIEF.md +168 -0
- package/skills/triage/OUT-OF-SCOPE.md +101 -0
- package/skills/triage/SKILL.md +103 -0
- package/skills/write-a-skill/SKILL.md +117 -0
- package/skills/zoom-out/SKILL.md +7 -0
package/dist/index.js
CHANGED
|
@@ -3,6 +3,8 @@ 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 { resolve, dirname as dirname2 } from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
6
8
|
import React4 from "react";
|
|
7
9
|
import { render } from "ink";
|
|
8
10
|
|
|
@@ -10,6 +12,38 @@ import { render } from "ink";
|
|
|
10
12
|
import { existsSync, readFileSync } from "fs";
|
|
11
13
|
import { homedir } from "os";
|
|
12
14
|
import { join } from "path";
|
|
15
|
+
var MODEL_CONTEXT_LIMITS = {
|
|
16
|
+
// OpenAI GPT 系列
|
|
17
|
+
"gpt-4o": 128e3,
|
|
18
|
+
"gpt-4o-mini": 128e3,
|
|
19
|
+
"gpt-4-turbo": 128e3,
|
|
20
|
+
"gpt-4": 8192,
|
|
21
|
+
// 老版 GPT-4 上下文极小,需特别注意
|
|
22
|
+
"gpt-3.5-turbo": 16385,
|
|
23
|
+
// OpenAI o 系列推理模型
|
|
24
|
+
"o1": 2e5,
|
|
25
|
+
"o1-mini": 128e3,
|
|
26
|
+
"o1-preview": 128e3,
|
|
27
|
+
"o3": 2e5,
|
|
28
|
+
"o3-mini": 2e5,
|
|
29
|
+
// Anthropic Claude 系列(全线支持 200K)
|
|
30
|
+
"claude-3-5-sonnet-20241022": 2e5,
|
|
31
|
+
"claude-3-5-haiku-20241022": 2e5,
|
|
32
|
+
"claude-3-opus-20240229": 2e5,
|
|
33
|
+
"claude-3-sonnet-20240229": 2e5,
|
|
34
|
+
"claude-3-haiku-20240307": 2e5,
|
|
35
|
+
"claude-sonnet-4-6": 2e5,
|
|
36
|
+
"claude-opus-4-7": 2e5,
|
|
37
|
+
"claude-haiku-4-5-20251001": 2e5,
|
|
38
|
+
// DeepSeek 系列(上下文较小,截断策略需更激进)
|
|
39
|
+
"deepseek-chat": 65536,
|
|
40
|
+
"deepseek-reasoner": 65536
|
|
41
|
+
};
|
|
42
|
+
var DEFAULT_CONTEXT_LIMIT = 128e3;
|
|
43
|
+
function getContextLimit(model, override) {
|
|
44
|
+
if (override !== void 0) return override;
|
|
45
|
+
return MODEL_CONTEXT_LIMITS[model] ?? DEFAULT_CONTEXT_LIMIT;
|
|
46
|
+
}
|
|
13
47
|
var DEFAULTS = {
|
|
14
48
|
baseUrl: "https://api.openai.com/v1",
|
|
15
49
|
apiKey: "",
|
|
@@ -44,13 +78,19 @@ function loadConfig() {
|
|
|
44
78
|
}
|
|
45
79
|
}
|
|
46
80
|
return {
|
|
81
|
+
// ?? 运算符:仅在左侧为 null / undefined 时才取右侧值,
|
|
82
|
+
// 与 || 的区别在于不会跳过空字符串,保证显式设置 "" 也能生效
|
|
47
83
|
baseUrl: process.env.ECODE_BASE_URL ?? fileConfig.baseUrl ?? DEFAULTS.baseUrl,
|
|
48
84
|
apiKey: process.env.ECODE_API_KEY ?? fileConfig.apiKey ?? DEFAULTS.apiKey,
|
|
49
85
|
model: process.env.ECODE_MODEL ?? fileConfig.model ?? DEFAULTS.model,
|
|
50
|
-
// dangerousPatterns
|
|
86
|
+
// dangerousPatterns 不支持环境变量注入:命令数组通过单个环境变量传递需要转义,
|
|
87
|
+
// 容易引入歧义,因此仅支持配置文件覆盖
|
|
51
88
|
dangerousPatterns: fileConfig.dangerousPatterns ?? DEFAULTS.dangerousPatterns,
|
|
52
|
-
// logDir: ECODE_LOG_DIR >
|
|
53
|
-
logDir: process.env.ECODE_LOG_DIR ?? fileConfig.logDir ?? DEFAULTS.logDir
|
|
89
|
+
// logDir: ECODE_LOG_DIR 环境变量 > 配置文件 > undefined(禁用日志)
|
|
90
|
+
logDir: process.env.ECODE_LOG_DIR ?? fileConfig.logDir ?? DEFAULTS.logDir,
|
|
91
|
+
// contextLimit 仅支持配置文件配置:数值类型在文件中更直观,
|
|
92
|
+
// 环境变量还需要 parseInt 转换,增加出错风险
|
|
93
|
+
contextLimit: fileConfig.contextLimit
|
|
54
94
|
};
|
|
55
95
|
}
|
|
56
96
|
|
|
@@ -66,6 +106,20 @@ function createLLMClient(config2) {
|
|
|
66
106
|
apiKey: config2.apiKey
|
|
67
107
|
});
|
|
68
108
|
return {
|
|
109
|
+
/**
|
|
110
|
+
* stream 方法向 LLM 发起一次流式对话请求,返回异步可迭代的 chunk 序列。
|
|
111
|
+
*
|
|
112
|
+
* 实现细节:
|
|
113
|
+
* - 使用 `Symbol.asyncIterator` + async generator 实现懒执行:
|
|
114
|
+
* 只有调用方执行 `for await` 时才真正发起 HTTP 请求,避免浪费连接
|
|
115
|
+
* - 内部维护两个累加器:
|
|
116
|
+
* 1. `tcAccumulator`:按 index 聚合工具调用的分片参数(JSON 字符串)
|
|
117
|
+
* 2. `reasoningAccumulator`:拼接思考链的所有分片文本
|
|
118
|
+
* - 只在最终 chunk(`finish_reason !== null`)中 yield done=true 的完整信息
|
|
119
|
+
*
|
|
120
|
+
* @param messages 完整的对话历史,包含 user/assistant/tool 所有轮次
|
|
121
|
+
* @param tools 可选的工具列表,不传或传空数组时不附加 tools 字段
|
|
122
|
+
*/
|
|
69
123
|
stream(messages, tools) {
|
|
70
124
|
return {
|
|
71
125
|
[Symbol.asyncIterator]: async function* () {
|
|
@@ -112,8 +166,11 @@ function createLLMClient(config2) {
|
|
|
112
166
|
text: delta.content ?? "",
|
|
113
167
|
done: true,
|
|
114
168
|
finishReason: choice.finish_reason,
|
|
169
|
+
// tcAccumulator 为空说明本轮没有工具调用,传 undefined 而非空数组,
|
|
170
|
+
// 让调用方用 if (chunk.toolCalls) 做简洁判断
|
|
115
171
|
toolCalls: tcAccumulator.size > 0 ? Array.from(tcAccumulator.values()) : void 0,
|
|
116
172
|
reasoning: reasoningAccumulator || void 0,
|
|
173
|
+
// 将 snake_case 的原始字段映射为 camelCase,对外接口保持一致
|
|
117
174
|
usage: rawUsage ? {
|
|
118
175
|
promptTokens: rawUsage.prompt_tokens,
|
|
119
176
|
completionTokens: rawUsage.completion_tokens,
|
|
@@ -188,13 +245,13 @@ function classifyCommand(cmd, dangerPatterns) {
|
|
|
188
245
|
import { exec } from "child_process";
|
|
189
246
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
190
247
|
function executeBash(cmd, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
191
|
-
return new Promise((
|
|
248
|
+
return new Promise((resolve2) => {
|
|
192
249
|
exec(cmd, { timeout: timeoutMs }, (err, stdout, stderr) => {
|
|
193
250
|
if (err) {
|
|
194
251
|
const exitCode = err.code ?? 1;
|
|
195
|
-
|
|
252
|
+
resolve2({ stdout: stdout ?? "", stderr: stderr ?? "", exitCode });
|
|
196
253
|
} else {
|
|
197
|
-
|
|
254
|
+
resolve2({ stdout: stdout ?? "", stderr: stderr ?? "", exitCode: 0 });
|
|
198
255
|
}
|
|
199
256
|
});
|
|
200
257
|
});
|
|
@@ -252,6 +309,15 @@ function createLogger(logDir, sessionStart) {
|
|
|
252
309
|
const filePath = path.join(logDir, filename);
|
|
253
310
|
return {
|
|
254
311
|
filePath,
|
|
312
|
+
/**
|
|
313
|
+
* 将单条日志条目序列化为 JSON 并同步追加到文件(末尾加换行符)。
|
|
314
|
+
*
|
|
315
|
+
* 使用 appendFileSync 而非 appendFile(异步版本)的原因:
|
|
316
|
+
* 进程崩溃或 Ctrl-C 退出时,异步写操作可能尚未完成,导致最后几条记录丢失。
|
|
317
|
+
* 同步写入虽然阻塞事件循环,但日志条目通常很小(< 1KB),延迟可忽略。
|
|
318
|
+
*
|
|
319
|
+
* 写入失败时输出到 stderr 而非抛出异常,防止日志错误中断正常业务流程。
|
|
320
|
+
*/
|
|
255
321
|
append(entry) {
|
|
256
322
|
try {
|
|
257
323
|
fs.appendFileSync(filePath, JSON.stringify(entry) + "\n");
|
|
@@ -263,6 +329,39 @@ function createLogger(logDir, sessionStart) {
|
|
|
263
329
|
};
|
|
264
330
|
}
|
|
265
331
|
|
|
332
|
+
// src/skills/resolver.ts
|
|
333
|
+
function isSkillCommand(input) {
|
|
334
|
+
return input.length > 1 && input.startsWith("/");
|
|
335
|
+
}
|
|
336
|
+
function parseSkillCommand(input) {
|
|
337
|
+
const withoutSlash = input.slice(1);
|
|
338
|
+
const spaceIndex = withoutSlash.search(/\s/);
|
|
339
|
+
if (spaceIndex === -1) {
|
|
340
|
+
return { name: withoutSlash, args: "" };
|
|
341
|
+
}
|
|
342
|
+
const name = withoutSlash.slice(0, spaceIndex);
|
|
343
|
+
const args = withoutSlash.slice(spaceIndex).trim();
|
|
344
|
+
return { name, args };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// src/skills/handler.ts
|
|
348
|
+
function handleSkillInput(input, registry2) {
|
|
349
|
+
if (!isSkillCommand(input)) return { type: "passthrough" };
|
|
350
|
+
const { name, args } = parseSkillCommand(input);
|
|
351
|
+
const skill = registry2.find(name);
|
|
352
|
+
if (!skill) {
|
|
353
|
+
const available = registry2.list().map((s) => s.name).join(", ") || "none";
|
|
354
|
+
return {
|
|
355
|
+
type: "error",
|
|
356
|
+
message: `Unknown skill: /${name}. Available: ${available}`
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
const content = args ? `${skill.body}
|
|
360
|
+
|
|
361
|
+
${args}` : skill.body;
|
|
362
|
+
return { type: "skill", message: { role: "user", content } };
|
|
363
|
+
}
|
|
364
|
+
|
|
266
365
|
// src/ui/StatusBar.tsx
|
|
267
366
|
import { useEffect, useState } from "react";
|
|
268
367
|
import { Box, Text } from "ink";
|
|
@@ -326,7 +425,7 @@ function StatusBar({
|
|
|
326
425
|
const ctxStr = `ctx: ${tokenUsage.estimated ? "~" : ""}${usedStr}/${limitStr}`;
|
|
327
426
|
const versionStr = `v${version2}`;
|
|
328
427
|
return (
|
|
329
|
-
// justifyContent="space-between"
|
|
428
|
+
// 外层 Box:行方向布局,justifyContent="space-between" 实现左右分列
|
|
330
429
|
// 避免用 string.length 手动计算填充(CJK 双宽字符会导致计算偏差)
|
|
331
430
|
/* @__PURE__ */ jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [
|
|
332
431
|
/* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
|
|
@@ -335,13 +434,13 @@ function StatusBar({
|
|
|
335
434
|
" "
|
|
336
435
|
] }),
|
|
337
436
|
confirmPrompt ? (
|
|
338
|
-
//
|
|
437
|
+
// 确认模式:整体用黄色显示,吸引用户注意
|
|
339
438
|
/* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
|
|
340
439
|
icon,
|
|
341
440
|
label
|
|
342
441
|
] })
|
|
343
442
|
) : status === "thinking" || status === "tool_calling" ? (
|
|
344
|
-
//
|
|
443
|
+
// 进行中状态:图标用绿色(活跃感),标签用白色(可读性)
|
|
345
444
|
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
346
445
|
/* @__PURE__ */ jsxs(Text, { color: "green", children: [
|
|
347
446
|
icon,
|
|
@@ -350,7 +449,7 @@ function StatusBar({
|
|
|
350
449
|
/* @__PURE__ */ jsx(Text, { color: "white", children: label })
|
|
351
450
|
] })
|
|
352
451
|
) : (
|
|
353
|
-
// 空闲 /
|
|
452
|
+
// 空闲 / 等待确认过渡态:整体淡灰,降低视觉权重
|
|
354
453
|
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
355
454
|
/* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
356
455
|
icon,
|
|
@@ -384,7 +483,7 @@ function UserMessage({
|
|
|
384
483
|
content
|
|
385
484
|
}) {
|
|
386
485
|
return (
|
|
387
|
-
//
|
|
486
|
+
// marginBottom={0} 避免 Ink 默认添加的底部间距,使历史区域更紧凑
|
|
388
487
|
/* @__PURE__ */ jsx2(Box2, { flexDirection: "row", marginBottom: 0, children: /* @__PURE__ */ jsxs2(Text2, { color: "white", children: [
|
|
389
488
|
"> ",
|
|
390
489
|
content
|
|
@@ -398,32 +497,47 @@ function AssistantMessage({
|
|
|
398
497
|
expandTools
|
|
399
498
|
}) {
|
|
400
499
|
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginBottom: 0, children: [
|
|
401
|
-
reasoning_content && reasoning_content.length > 0 && (expandTools ?
|
|
402
|
-
|
|
403
|
-
/* @__PURE__ */
|
|
404
|
-
|
|
500
|
+
reasoning_content && reasoning_content.length > 0 && (expandTools ? (
|
|
501
|
+
// 展开:显示 <thinking> 标题和完整推理内容
|
|
502
|
+
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
503
|
+
/* @__PURE__ */ jsx2(Text2, { color: "gray", children: "<thinking>" }),
|
|
504
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: reasoning_content })
|
|
505
|
+
] })
|
|
506
|
+
) : (
|
|
507
|
+
// 折叠:一行简短占位符,告知用户有隐藏的思考内容
|
|
508
|
+
/* @__PURE__ */ jsx2(Text2, { color: "gray", children: "[+ thinking]" })
|
|
509
|
+
)),
|
|
405
510
|
content && content.trim().length > 0 && /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: content }),
|
|
406
|
-
tool_calls && tool_calls.length > 0 && (expandTools ?
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
511
|
+
tool_calls && tool_calls.length > 0 && (expandTools ? (
|
|
512
|
+
// 展开:逐条渲染每个工具调用
|
|
513
|
+
tool_calls.map((tc, idx) => {
|
|
514
|
+
let argsDisplay = tc.function.arguments;
|
|
515
|
+
try {
|
|
516
|
+
const parsed = JSON.parse(tc.function.arguments);
|
|
517
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
518
|
+
argsDisplay = Object.values(parsed).map(String).join(", ");
|
|
519
|
+
}
|
|
520
|
+
} catch {
|
|
412
521
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
522
|
+
return (
|
|
523
|
+
// 黄色 ⚙ 图标 + 工具名 + 格式化参数
|
|
524
|
+
/* @__PURE__ */ jsxs2(Text2, { color: "yellow", children: [
|
|
525
|
+
"\u2699 \u8C03\u7528\u5DE5\u5177: ",
|
|
526
|
+
tc.function.name,
|
|
527
|
+
"(",
|
|
528
|
+
argsDisplay,
|
|
529
|
+
")"
|
|
530
|
+
] }, idx)
|
|
531
|
+
);
|
|
532
|
+
})
|
|
533
|
+
) : (
|
|
534
|
+
// 折叠:用计数占位符替代,避免占用多行
|
|
535
|
+
/* @__PURE__ */ jsxs2(Text2, { color: "gray", children: [
|
|
536
|
+
"[+ ",
|
|
537
|
+
tool_calls.length,
|
|
538
|
+
" \u4E2A\u5DE5\u5177\u8C03\u7528]"
|
|
539
|
+
] })
|
|
540
|
+
))
|
|
427
541
|
] });
|
|
428
542
|
}
|
|
429
543
|
function ToolMessage({
|
|
@@ -439,6 +553,39 @@ function ToolMessage({
|
|
|
439
553
|
truncated
|
|
440
554
|
] }) });
|
|
441
555
|
}
|
|
556
|
+
function estimateLines(msg, expandTools) {
|
|
557
|
+
if (msg.role === "user") {
|
|
558
|
+
return Math.max(1, msg.content.split("\n").length);
|
|
559
|
+
}
|
|
560
|
+
if (msg.role === "assistant") {
|
|
561
|
+
const contentLines = msg.content ? msg.content.split("\n").length : 0;
|
|
562
|
+
const assistantMsg = msg;
|
|
563
|
+
let reasoningLines = 0;
|
|
564
|
+
if (assistantMsg.reasoning_content && assistantMsg.reasoning_content.length > 0) {
|
|
565
|
+
if (expandTools) {
|
|
566
|
+
reasoningLines = 1 + assistantMsg.reasoning_content.split("\n").length;
|
|
567
|
+
} else {
|
|
568
|
+
reasoningLines = 1;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
let toolLines = 0;
|
|
572
|
+
if (assistantMsg.tool_calls && assistantMsg.tool_calls.length > 0) {
|
|
573
|
+
if (expandTools) {
|
|
574
|
+
toolLines = assistantMsg.tool_calls.length;
|
|
575
|
+
} else {
|
|
576
|
+
toolLines = 1;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return Math.max(1, contentLines + reasoningLines + toolLines);
|
|
580
|
+
}
|
|
581
|
+
if (msg.role === "tool") {
|
|
582
|
+
if (!expandTools) {
|
|
583
|
+
return 1;
|
|
584
|
+
}
|
|
585
|
+
return Math.min(TOOL_RESULT_MAX_LINES, msg.content.split("\n").length) + 1;
|
|
586
|
+
}
|
|
587
|
+
return 1;
|
|
588
|
+
}
|
|
442
589
|
function ConversationHistory({
|
|
443
590
|
messages,
|
|
444
591
|
maxHeight = 20,
|
|
@@ -447,51 +594,40 @@ function ConversationHistory({
|
|
|
447
594
|
const visible = messages.filter(
|
|
448
595
|
(m) => m.role !== "system"
|
|
449
596
|
);
|
|
450
|
-
function estimateLines(msg) {
|
|
451
|
-
if (msg.role === "user") {
|
|
452
|
-
return Math.max(1, msg.content.split("\n").length);
|
|
453
|
-
}
|
|
454
|
-
if (msg.role === "assistant") {
|
|
455
|
-
const contentLines = msg.content ? msg.content.split("\n").length : 0;
|
|
456
|
-
const toolLines = msg.tool_calls?.length ?? 0;
|
|
457
|
-
return Math.max(1, contentLines + toolLines);
|
|
458
|
-
}
|
|
459
|
-
if (msg.role === "tool") {
|
|
460
|
-
return Math.min(TOOL_RESULT_MAX_LINES, msg.content.split("\n").length) + 1;
|
|
461
|
-
}
|
|
462
|
-
return 1;
|
|
463
|
-
}
|
|
464
597
|
let totalLines = 0;
|
|
465
598
|
let startIdx = visible.length;
|
|
466
599
|
for (let i = visible.length - 1; i >= 0; i--) {
|
|
467
|
-
const lines = estimateLines(visible[i]);
|
|
600
|
+
const lines = estimateLines(visible[i], expandTools);
|
|
468
601
|
if (totalLines + lines > maxHeight) break;
|
|
469
602
|
totalLines += lines;
|
|
470
603
|
startIdx = i;
|
|
471
604
|
}
|
|
472
605
|
const displayMessages = visible.slice(startIdx);
|
|
473
|
-
return
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
606
|
+
return (
|
|
607
|
+
// 纵向堆叠所有消息
|
|
608
|
+
/* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: displayMessages.map((msg, idx) => {
|
|
609
|
+
if (msg.role === "user") {
|
|
610
|
+
return /* @__PURE__ */ jsx2(UserMessage, { content: msg.content }, idx);
|
|
611
|
+
}
|
|
612
|
+
if (msg.role === "assistant") {
|
|
613
|
+
const assistantMsg = msg;
|
|
614
|
+
return /* @__PURE__ */ jsx2(
|
|
615
|
+
AssistantMessage,
|
|
616
|
+
{
|
|
617
|
+
content: assistantMsg.content,
|
|
618
|
+
tool_calls: assistantMsg.tool_calls,
|
|
619
|
+
reasoning_content: assistantMsg.reasoning_content,
|
|
620
|
+
expandTools
|
|
621
|
+
},
|
|
622
|
+
idx
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
if (msg.role === "tool") {
|
|
626
|
+
return /* @__PURE__ */ jsx2(ToolMessage, { content: msg.content, expandTools }, idx);
|
|
627
|
+
}
|
|
628
|
+
return null;
|
|
629
|
+
}) })
|
|
630
|
+
);
|
|
495
631
|
}
|
|
496
632
|
|
|
497
633
|
// src/ui/Input.tsx
|
|
@@ -554,6 +690,7 @@ function Input({ isActive, onSubmit, placeholder }) {
|
|
|
554
690
|
});
|
|
555
691
|
}
|
|
556
692
|
},
|
|
693
|
+
// 第二个参数:仅在 isActive=true 时注册键盘监听
|
|
557
694
|
{ isActive }
|
|
558
695
|
);
|
|
559
696
|
const isEmpty = lines.every((line) => line === "");
|
|
@@ -575,21 +712,28 @@ function Input({ isActive, onSubmit, placeholder }) {
|
|
|
575
712
|
] }, idx);
|
|
576
713
|
}) });
|
|
577
714
|
};
|
|
578
|
-
return /* @__PURE__ */ jsx3(Box3, { children: isActive ?
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
715
|
+
return /* @__PURE__ */ jsx3(Box3, { children: isActive ? (
|
|
716
|
+
// 激活状态:调用完整的多行渲染逻辑
|
|
717
|
+
renderLines()
|
|
718
|
+
) : (
|
|
719
|
+
// 禁用状态:暗色单行显示,不响应键盘
|
|
720
|
+
/* @__PURE__ */ jsxs3(Box3, { children: [
|
|
721
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "> " }),
|
|
722
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: isEmpty ? placeholder ?? "" : lines.join(" ") })
|
|
723
|
+
] })
|
|
724
|
+
) });
|
|
582
725
|
}
|
|
583
726
|
|
|
584
727
|
// src/ui/App.tsx
|
|
585
728
|
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
586
|
-
function App({ config: config2, version: version2, autoMode: autoMode2 = false }) {
|
|
729
|
+
function App({ config: config2, version: version2, autoMode: autoMode2 = false, registry: registry2 }) {
|
|
587
730
|
const [messages, setMessages] = useState3([]);
|
|
588
731
|
const [status, setStatus] = useState3("idle");
|
|
732
|
+
const contextLimit = getContextLimit(config2.model, config2.contextLimit);
|
|
589
733
|
const [tokenUsage, setTokenUsage] = useState3({
|
|
590
734
|
used: 0,
|
|
591
735
|
estimated: true,
|
|
592
|
-
limit:
|
|
736
|
+
limit: contextLimit
|
|
593
737
|
});
|
|
594
738
|
const [toolName, setToolName] = useState3(void 0);
|
|
595
739
|
const [confirmPrompt, setConfirmPrompt] = useState3(void 0);
|
|
@@ -610,8 +754,11 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false }
|
|
|
610
754
|
loggerRef.current.append({
|
|
611
755
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
612
756
|
role: msg.role,
|
|
757
|
+
// content 可能为 null(纯工具调用的 assistant 消息),日志中保留 null
|
|
613
758
|
content: typeof msg.content === "string" ? msg.content : null,
|
|
759
|
+
// tool_call_id 只在 tool 角色消息中存在,用于关联工具调用与结果
|
|
614
760
|
tool_call_id: "tool_call_id" in msg ? msg.tool_call_id : void 0,
|
|
761
|
+
// tool_calls 只在 assistant 角色消息中存在(当 LLM 决定调用工具时)
|
|
615
762
|
tool_calls: "tool_calls" in msg ? msg.tool_calls : void 0
|
|
616
763
|
});
|
|
617
764
|
}
|
|
@@ -623,10 +770,10 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false }
|
|
|
623
770
|
}
|
|
624
771
|
});
|
|
625
772
|
const confirm = useCallback((prompt) => {
|
|
626
|
-
return new Promise((
|
|
773
|
+
return new Promise((resolve2) => {
|
|
627
774
|
setStatus("awaiting_confirm");
|
|
628
775
|
setConfirmPrompt(prompt);
|
|
629
|
-
pendingConfirmRef.current = { resolve };
|
|
776
|
+
pendingConfirmRef.current = { resolve: resolve2 };
|
|
630
777
|
});
|
|
631
778
|
}, []);
|
|
632
779
|
const runLlmLoop = useCallback(
|
|
@@ -638,6 +785,7 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false }
|
|
|
638
785
|
confirm,
|
|
639
786
|
print,
|
|
640
787
|
dangerousPatterns: config2.dangerousPatterns,
|
|
788
|
+
// autoMode=true 时自动同意 normal 级工具调用,无需用户确认
|
|
641
789
|
autoApproveNormal: autoMode2
|
|
642
790
|
};
|
|
643
791
|
let currentMessages = history;
|
|
@@ -666,7 +814,7 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false }
|
|
|
666
814
|
setTokenUsage({
|
|
667
815
|
used: chunk.usage.totalTokens,
|
|
668
816
|
estimated: false,
|
|
669
|
-
limit:
|
|
817
|
+
limit: getContextLimit(config2.model, config2.contextLimit)
|
|
670
818
|
});
|
|
671
819
|
}
|
|
672
820
|
} else {
|
|
@@ -682,12 +830,14 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false }
|
|
|
682
830
|
if (toolCalls.length > 0) {
|
|
683
831
|
const assistantMsg = {
|
|
684
832
|
role: "assistant",
|
|
833
|
+
// content 可能为空字符串(纯工具调用),存 null 符合 OpenAI API 规范
|
|
685
834
|
content: assistantText || null,
|
|
686
835
|
tool_calls: toolCalls.map((tc) => ({
|
|
687
836
|
id: tc.id,
|
|
688
837
|
type: "function",
|
|
689
838
|
function: { name: tc.name, arguments: tc.arguments }
|
|
690
839
|
})),
|
|
840
|
+
// 仅在存在思考内容时扩展 reasoning_content 字段
|
|
691
841
|
...assistantReasoning ? { reasoning_content: assistantReasoning } : {}
|
|
692
842
|
};
|
|
693
843
|
setMessages((prev) => {
|
|
@@ -761,6 +911,29 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false }
|
|
|
761
911
|
return;
|
|
762
912
|
}
|
|
763
913
|
if (!trimmed) return;
|
|
914
|
+
if (registry2) {
|
|
915
|
+
const skillResult = handleSkillInput(trimmed, registry2);
|
|
916
|
+
if (skillResult.type === "error") {
|
|
917
|
+
setMessages((prev) => [
|
|
918
|
+
...prev,
|
|
919
|
+
{ role: "assistant", content: skillResult.message }
|
|
920
|
+
]);
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
if (skillResult.type === "skill") {
|
|
924
|
+
const nextMessages2 = [...messages, skillResult.message];
|
|
925
|
+
setMessages(nextMessages2);
|
|
926
|
+
runLlmLoop(nextMessages2).catch((err) => {
|
|
927
|
+
setStatus("idle");
|
|
928
|
+
setToolName(void 0);
|
|
929
|
+
setMessages((prev) => [
|
|
930
|
+
...prev,
|
|
931
|
+
{ role: "assistant", content: `[error] ${String(err)}` }
|
|
932
|
+
]);
|
|
933
|
+
});
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
764
937
|
const userMsg = { role: "user", content: trimmed };
|
|
765
938
|
const nextMessages = [...messages, userMsg];
|
|
766
939
|
setMessages(nextMessages);
|
|
@@ -769,6 +942,7 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false }
|
|
|
769
942
|
setToolName(void 0);
|
|
770
943
|
setMessages((prev) => [
|
|
771
944
|
...prev,
|
|
945
|
+
// 错误消息前缀 "[error]" 便于用户识别和日志筛选
|
|
772
946
|
{ role: "assistant", content: `[error] ${String(err)}` }
|
|
773
947
|
]);
|
|
774
948
|
});
|
|
@@ -776,27 +950,116 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false }
|
|
|
776
950
|
[status, messages, runLlmLoop]
|
|
777
951
|
);
|
|
778
952
|
const isInputActive = status === "idle" || status === "awaiting_confirm";
|
|
779
|
-
return
|
|
780
|
-
|
|
781
|
-
/* @__PURE__ */
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
953
|
+
return (
|
|
954
|
+
// 顶层容器:纵向堆叠,撑满终端高度(height="100%")
|
|
955
|
+
/* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", height: "100%", children: [
|
|
956
|
+
/* @__PURE__ */ jsx4(ConversationHistory, { messages, expandTools }),
|
|
957
|
+
/* @__PURE__ */ jsx4(
|
|
958
|
+
StatusBar,
|
|
959
|
+
{
|
|
960
|
+
status,
|
|
961
|
+
toolName,
|
|
962
|
+
confirmPrompt,
|
|
963
|
+
version: version2,
|
|
964
|
+
tokenUsage
|
|
965
|
+
}
|
|
966
|
+
),
|
|
967
|
+
/* @__PURE__ */ jsx4(
|
|
968
|
+
Input,
|
|
969
|
+
{
|
|
970
|
+
isActive: isInputActive,
|
|
971
|
+
onSubmit: handleSubmit,
|
|
972
|
+
placeholder: status === "awaiting_confirm" ? "y / n" : void 0
|
|
973
|
+
}
|
|
974
|
+
)
|
|
975
|
+
] })
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// src/skills/registry.ts
|
|
980
|
+
var SkillRegistry = class {
|
|
981
|
+
skills = /* @__PURE__ */ new Map();
|
|
982
|
+
/** 注册或覆盖一个 skill(键不区分大小写)。 */
|
|
983
|
+
register(skill) {
|
|
984
|
+
this.skills.set(skill.name.toLowerCase(), skill);
|
|
985
|
+
}
|
|
986
|
+
/** 按名称查找 skill;未找到时返回 undefined。 */
|
|
987
|
+
find(name) {
|
|
988
|
+
return this.skills.get(name.toLowerCase());
|
|
989
|
+
}
|
|
990
|
+
/** 返回所有 skill,按名称字母顺序排序。 */
|
|
991
|
+
list() {
|
|
992
|
+
return [...this.skills.values()].sort(
|
|
993
|
+
(a, b) => a.name.localeCompare(b.name)
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
/** 已注册的 skill 数量。 */
|
|
997
|
+
size() {
|
|
998
|
+
return this.skills.size;
|
|
999
|
+
}
|
|
1000
|
+
};
|
|
1001
|
+
|
|
1002
|
+
// src/skills/loader.ts
|
|
1003
|
+
import { readFile, readdir, stat } from "fs/promises";
|
|
1004
|
+
import { join as join3, dirname, basename } from "path";
|
|
1005
|
+
function parseFrontmatter(content) {
|
|
1006
|
+
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
1007
|
+
const match = FRONTMATTER_RE.exec(content);
|
|
1008
|
+
if (!match) {
|
|
1009
|
+
return { data: {}, body: content };
|
|
1010
|
+
}
|
|
1011
|
+
const rawFrontmatter = match[1];
|
|
1012
|
+
const body = match[2] ?? "";
|
|
1013
|
+
const data = {};
|
|
1014
|
+
for (const line of rawFrontmatter.split(/\r?\n/)) {
|
|
1015
|
+
const colonIdx = line.indexOf(":");
|
|
1016
|
+
if (colonIdx === -1) continue;
|
|
1017
|
+
const key = line.slice(0, colonIdx).trim();
|
|
1018
|
+
const value = line.slice(colonIdx + 1).trim();
|
|
1019
|
+
if (key) {
|
|
1020
|
+
data[key] = value;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
return { data, body };
|
|
1024
|
+
}
|
|
1025
|
+
async function loadSkillFile(skillMdPath) {
|
|
1026
|
+
const content = await readFile(skillMdPath, "utf-8");
|
|
1027
|
+
const { data, body } = parseFrontmatter(content);
|
|
1028
|
+
const dirName = basename(dirname(skillMdPath));
|
|
1029
|
+
return {
|
|
1030
|
+
name: data["name"] ?? dirName,
|
|
1031
|
+
description: data["description"] ?? "",
|
|
1032
|
+
body,
|
|
1033
|
+
source: skillMdPath
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
async function loadSkillsFromDir(dir) {
|
|
1037
|
+
let entries;
|
|
1038
|
+
try {
|
|
1039
|
+
entries = await readdir(dir);
|
|
1040
|
+
} catch {
|
|
1041
|
+
return [];
|
|
1042
|
+
}
|
|
1043
|
+
const skills = [];
|
|
1044
|
+
for (const entry of entries) {
|
|
1045
|
+
const entryPath = join3(dir, entry);
|
|
1046
|
+
let entryStat;
|
|
1047
|
+
try {
|
|
1048
|
+
entryStat = await stat(entryPath);
|
|
1049
|
+
} catch {
|
|
1050
|
+
continue;
|
|
1051
|
+
}
|
|
1052
|
+
if (!entryStat.isDirectory()) continue;
|
|
1053
|
+
const skillMdPath = join3(entryPath, "SKILL.md");
|
|
1054
|
+
try {
|
|
1055
|
+
await stat(skillMdPath);
|
|
1056
|
+
} catch {
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
const skill = await loadSkillFile(skillMdPath);
|
|
1060
|
+
skills.push(skill);
|
|
1061
|
+
}
|
|
1062
|
+
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
800
1063
|
}
|
|
801
1064
|
|
|
802
1065
|
// src/index.ts
|
|
@@ -831,4 +1094,13 @@ if (!finalConfig.apiKey) {
|
|
|
831
1094
|
);
|
|
832
1095
|
process.exit(1);
|
|
833
1096
|
}
|
|
834
|
-
|
|
1097
|
+
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
1098
|
+
var builtinSkillsDir = resolve(__dirname, "../skills");
|
|
1099
|
+
var userSkillsDir = resolve(process.env.HOME ?? "~", ".ecode/skills");
|
|
1100
|
+
var projectSkillsDir = resolve(process.cwd(), ".ecode/skills");
|
|
1101
|
+
var registry = new SkillRegistry();
|
|
1102
|
+
for (const dir of [builtinSkillsDir, userSkillsDir, projectSkillsDir]) {
|
|
1103
|
+
const skills = await loadSkillsFromDir(dir);
|
|
1104
|
+
for (const skill of skills) registry.register(skill);
|
|
1105
|
+
}
|
|
1106
|
+
render(React4.createElement(App, { config: finalConfig, version: VERSION, autoMode, registry }));
|