@zhongqian97-code/ecode 0.0.8 → 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 +317 -88
- 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
|
|
|
@@ -11,16 +13,20 @@ import { existsSync, readFileSync } from "fs";
|
|
|
11
13
|
import { homedir } from "os";
|
|
12
14
|
import { join } from "path";
|
|
13
15
|
var MODEL_CONTEXT_LIMITS = {
|
|
16
|
+
// OpenAI GPT 系列
|
|
14
17
|
"gpt-4o": 128e3,
|
|
15
18
|
"gpt-4o-mini": 128e3,
|
|
16
19
|
"gpt-4-turbo": 128e3,
|
|
17
20
|
"gpt-4": 8192,
|
|
21
|
+
// 老版 GPT-4 上下文极小,需特别注意
|
|
18
22
|
"gpt-3.5-turbo": 16385,
|
|
23
|
+
// OpenAI o 系列推理模型
|
|
19
24
|
"o1": 2e5,
|
|
20
25
|
"o1-mini": 128e3,
|
|
21
26
|
"o1-preview": 128e3,
|
|
22
27
|
"o3": 2e5,
|
|
23
28
|
"o3-mini": 2e5,
|
|
29
|
+
// Anthropic Claude 系列(全线支持 200K)
|
|
24
30
|
"claude-3-5-sonnet-20241022": 2e5,
|
|
25
31
|
"claude-3-5-haiku-20241022": 2e5,
|
|
26
32
|
"claude-3-opus-20240229": 2e5,
|
|
@@ -29,6 +35,7 @@ var MODEL_CONTEXT_LIMITS = {
|
|
|
29
35
|
"claude-sonnet-4-6": 2e5,
|
|
30
36
|
"claude-opus-4-7": 2e5,
|
|
31
37
|
"claude-haiku-4-5-20251001": 2e5,
|
|
38
|
+
// DeepSeek 系列(上下文较小,截断策略需更激进)
|
|
32
39
|
"deepseek-chat": 65536,
|
|
33
40
|
"deepseek-reasoner": 65536
|
|
34
41
|
};
|
|
@@ -71,14 +78,18 @@ function loadConfig() {
|
|
|
71
78
|
}
|
|
72
79
|
}
|
|
73
80
|
return {
|
|
81
|
+
// ?? 运算符:仅在左侧为 null / undefined 时才取右侧值,
|
|
82
|
+
// 与 || 的区别在于不会跳过空字符串,保证显式设置 "" 也能生效
|
|
74
83
|
baseUrl: process.env.ECODE_BASE_URL ?? fileConfig.baseUrl ?? DEFAULTS.baseUrl,
|
|
75
84
|
apiKey: process.env.ECODE_API_KEY ?? fileConfig.apiKey ?? DEFAULTS.apiKey,
|
|
76
85
|
model: process.env.ECODE_MODEL ?? fileConfig.model ?? DEFAULTS.model,
|
|
77
|
-
// dangerousPatterns
|
|
86
|
+
// dangerousPatterns 不支持环境变量注入:命令数组通过单个环境变量传递需要转义,
|
|
87
|
+
// 容易引入歧义,因此仅支持配置文件覆盖
|
|
78
88
|
dangerousPatterns: fileConfig.dangerousPatterns ?? DEFAULTS.dangerousPatterns,
|
|
79
|
-
// logDir: ECODE_LOG_DIR >
|
|
89
|
+
// logDir: ECODE_LOG_DIR 环境变量 > 配置文件 > undefined(禁用日志)
|
|
80
90
|
logDir: process.env.ECODE_LOG_DIR ?? fileConfig.logDir ?? DEFAULTS.logDir,
|
|
81
|
-
// contextLimit
|
|
91
|
+
// contextLimit 仅支持配置文件配置:数值类型在文件中更直观,
|
|
92
|
+
// 环境变量还需要 parseInt 转换,增加出错风险
|
|
82
93
|
contextLimit: fileConfig.contextLimit
|
|
83
94
|
};
|
|
84
95
|
}
|
|
@@ -95,6 +106,20 @@ function createLLMClient(config2) {
|
|
|
95
106
|
apiKey: config2.apiKey
|
|
96
107
|
});
|
|
97
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
|
+
*/
|
|
98
123
|
stream(messages, tools) {
|
|
99
124
|
return {
|
|
100
125
|
[Symbol.asyncIterator]: async function* () {
|
|
@@ -141,8 +166,11 @@ function createLLMClient(config2) {
|
|
|
141
166
|
text: delta.content ?? "",
|
|
142
167
|
done: true,
|
|
143
168
|
finishReason: choice.finish_reason,
|
|
169
|
+
// tcAccumulator 为空说明本轮没有工具调用,传 undefined 而非空数组,
|
|
170
|
+
// 让调用方用 if (chunk.toolCalls) 做简洁判断
|
|
144
171
|
toolCalls: tcAccumulator.size > 0 ? Array.from(tcAccumulator.values()) : void 0,
|
|
145
172
|
reasoning: reasoningAccumulator || void 0,
|
|
173
|
+
// 将 snake_case 的原始字段映射为 camelCase,对外接口保持一致
|
|
146
174
|
usage: rawUsage ? {
|
|
147
175
|
promptTokens: rawUsage.prompt_tokens,
|
|
148
176
|
completionTokens: rawUsage.completion_tokens,
|
|
@@ -217,13 +245,13 @@ function classifyCommand(cmd, dangerPatterns) {
|
|
|
217
245
|
import { exec } from "child_process";
|
|
218
246
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
219
247
|
function executeBash(cmd, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
220
|
-
return new Promise((
|
|
248
|
+
return new Promise((resolve2) => {
|
|
221
249
|
exec(cmd, { timeout: timeoutMs }, (err, stdout, stderr) => {
|
|
222
250
|
if (err) {
|
|
223
251
|
const exitCode = err.code ?? 1;
|
|
224
|
-
|
|
252
|
+
resolve2({ stdout: stdout ?? "", stderr: stderr ?? "", exitCode });
|
|
225
253
|
} else {
|
|
226
|
-
|
|
254
|
+
resolve2({ stdout: stdout ?? "", stderr: stderr ?? "", exitCode: 0 });
|
|
227
255
|
}
|
|
228
256
|
});
|
|
229
257
|
});
|
|
@@ -281,6 +309,15 @@ function createLogger(logDir, sessionStart) {
|
|
|
281
309
|
const filePath = path.join(logDir, filename);
|
|
282
310
|
return {
|
|
283
311
|
filePath,
|
|
312
|
+
/**
|
|
313
|
+
* 将单条日志条目序列化为 JSON 并同步追加到文件(末尾加换行符)。
|
|
314
|
+
*
|
|
315
|
+
* 使用 appendFileSync 而非 appendFile(异步版本)的原因:
|
|
316
|
+
* 进程崩溃或 Ctrl-C 退出时,异步写操作可能尚未完成,导致最后几条记录丢失。
|
|
317
|
+
* 同步写入虽然阻塞事件循环,但日志条目通常很小(< 1KB),延迟可忽略。
|
|
318
|
+
*
|
|
319
|
+
* 写入失败时输出到 stderr 而非抛出异常,防止日志错误中断正常业务流程。
|
|
320
|
+
*/
|
|
284
321
|
append(entry) {
|
|
285
322
|
try {
|
|
286
323
|
fs.appendFileSync(filePath, JSON.stringify(entry) + "\n");
|
|
@@ -292,6 +329,39 @@ function createLogger(logDir, sessionStart) {
|
|
|
292
329
|
};
|
|
293
330
|
}
|
|
294
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
|
+
|
|
295
365
|
// src/ui/StatusBar.tsx
|
|
296
366
|
import { useEffect, useState } from "react";
|
|
297
367
|
import { Box, Text } from "ink";
|
|
@@ -355,7 +425,7 @@ function StatusBar({
|
|
|
355
425
|
const ctxStr = `ctx: ${tokenUsage.estimated ? "~" : ""}${usedStr}/${limitStr}`;
|
|
356
426
|
const versionStr = `v${version2}`;
|
|
357
427
|
return (
|
|
358
|
-
// justifyContent="space-between"
|
|
428
|
+
// 外层 Box:行方向布局,justifyContent="space-between" 实现左右分列
|
|
359
429
|
// 避免用 string.length 手动计算填充(CJK 双宽字符会导致计算偏差)
|
|
360
430
|
/* @__PURE__ */ jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [
|
|
361
431
|
/* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
|
|
@@ -364,13 +434,13 @@ function StatusBar({
|
|
|
364
434
|
" "
|
|
365
435
|
] }),
|
|
366
436
|
confirmPrompt ? (
|
|
367
|
-
//
|
|
437
|
+
// 确认模式:整体用黄色显示,吸引用户注意
|
|
368
438
|
/* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
|
|
369
439
|
icon,
|
|
370
440
|
label
|
|
371
441
|
] })
|
|
372
442
|
) : status === "thinking" || status === "tool_calling" ? (
|
|
373
|
-
//
|
|
443
|
+
// 进行中状态:图标用绿色(活跃感),标签用白色(可读性)
|
|
374
444
|
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
375
445
|
/* @__PURE__ */ jsxs(Text, { color: "green", children: [
|
|
376
446
|
icon,
|
|
@@ -379,7 +449,7 @@ function StatusBar({
|
|
|
379
449
|
/* @__PURE__ */ jsx(Text, { color: "white", children: label })
|
|
380
450
|
] })
|
|
381
451
|
) : (
|
|
382
|
-
// 空闲 /
|
|
452
|
+
// 空闲 / 等待确认过渡态:整体淡灰,降低视觉权重
|
|
383
453
|
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
384
454
|
/* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
385
455
|
icon,
|
|
@@ -413,7 +483,7 @@ function UserMessage({
|
|
|
413
483
|
content
|
|
414
484
|
}) {
|
|
415
485
|
return (
|
|
416
|
-
//
|
|
486
|
+
// marginBottom={0} 避免 Ink 默认添加的底部间距,使历史区域更紧凑
|
|
417
487
|
/* @__PURE__ */ jsx2(Box2, { flexDirection: "row", marginBottom: 0, children: /* @__PURE__ */ jsxs2(Text2, { color: "white", children: [
|
|
418
488
|
"> ",
|
|
419
489
|
content
|
|
@@ -427,32 +497,47 @@ function AssistantMessage({
|
|
|
427
497
|
expandTools
|
|
428
498
|
}) {
|
|
429
499
|
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginBottom: 0, children: [
|
|
430
|
-
reasoning_content && reasoning_content.length > 0 && (expandTools ?
|
|
431
|
-
|
|
432
|
-
/* @__PURE__ */
|
|
433
|
-
|
|
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
|
+
)),
|
|
434
510
|
content && content.trim().length > 0 && /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: content }),
|
|
435
|
-
tool_calls && tool_calls.length > 0 && (expandTools ?
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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 {
|
|
441
521
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
+
))
|
|
456
541
|
] });
|
|
457
542
|
}
|
|
458
543
|
function ToolMessage({
|
|
@@ -485,12 +570,18 @@ function estimateLines(msg, expandTools) {
|
|
|
485
570
|
}
|
|
486
571
|
let toolLines = 0;
|
|
487
572
|
if (assistantMsg.tool_calls && assistantMsg.tool_calls.length > 0) {
|
|
488
|
-
|
|
573
|
+
if (expandTools) {
|
|
574
|
+
toolLines = assistantMsg.tool_calls.length;
|
|
575
|
+
} else {
|
|
576
|
+
toolLines = 1;
|
|
577
|
+
}
|
|
489
578
|
}
|
|
490
579
|
return Math.max(1, contentLines + reasoningLines + toolLines);
|
|
491
580
|
}
|
|
492
581
|
if (msg.role === "tool") {
|
|
493
|
-
if (!expandTools)
|
|
582
|
+
if (!expandTools) {
|
|
583
|
+
return 1;
|
|
584
|
+
}
|
|
494
585
|
return Math.min(TOOL_RESULT_MAX_LINES, msg.content.split("\n").length) + 1;
|
|
495
586
|
}
|
|
496
587
|
return 1;
|
|
@@ -512,28 +603,31 @@ function ConversationHistory({
|
|
|
512
603
|
startIdx = i;
|
|
513
604
|
}
|
|
514
605
|
const displayMessages = visible.slice(startIdx);
|
|
515
|
-
return
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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
|
+
);
|
|
537
631
|
}
|
|
538
632
|
|
|
539
633
|
// src/ui/Input.tsx
|
|
@@ -596,6 +690,7 @@ function Input({ isActive, onSubmit, placeholder }) {
|
|
|
596
690
|
});
|
|
597
691
|
}
|
|
598
692
|
},
|
|
693
|
+
// 第二个参数:仅在 isActive=true 时注册键盘监听
|
|
599
694
|
{ isActive }
|
|
600
695
|
);
|
|
601
696
|
const isEmpty = lines.every((line) => line === "");
|
|
@@ -617,15 +712,21 @@ function Input({ isActive, onSubmit, placeholder }) {
|
|
|
617
712
|
] }, idx);
|
|
618
713
|
}) });
|
|
619
714
|
};
|
|
620
|
-
return /* @__PURE__ */ jsx3(Box3, { children: isActive ?
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
+
) });
|
|
624
725
|
}
|
|
625
726
|
|
|
626
727
|
// src/ui/App.tsx
|
|
627
728
|
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
628
|
-
function App({ config: config2, version: version2, autoMode: autoMode2 = false }) {
|
|
729
|
+
function App({ config: config2, version: version2, autoMode: autoMode2 = false, registry: registry2 }) {
|
|
629
730
|
const [messages, setMessages] = useState3([]);
|
|
630
731
|
const [status, setStatus] = useState3("idle");
|
|
631
732
|
const contextLimit = getContextLimit(config2.model, config2.contextLimit);
|
|
@@ -653,8 +754,11 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false }
|
|
|
653
754
|
loggerRef.current.append({
|
|
654
755
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
655
756
|
role: msg.role,
|
|
757
|
+
// content 可能为 null(纯工具调用的 assistant 消息),日志中保留 null
|
|
656
758
|
content: typeof msg.content === "string" ? msg.content : null,
|
|
759
|
+
// tool_call_id 只在 tool 角色消息中存在,用于关联工具调用与结果
|
|
657
760
|
tool_call_id: "tool_call_id" in msg ? msg.tool_call_id : void 0,
|
|
761
|
+
// tool_calls 只在 assistant 角色消息中存在(当 LLM 决定调用工具时)
|
|
658
762
|
tool_calls: "tool_calls" in msg ? msg.tool_calls : void 0
|
|
659
763
|
});
|
|
660
764
|
}
|
|
@@ -666,10 +770,10 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false }
|
|
|
666
770
|
}
|
|
667
771
|
});
|
|
668
772
|
const confirm = useCallback((prompt) => {
|
|
669
|
-
return new Promise((
|
|
773
|
+
return new Promise((resolve2) => {
|
|
670
774
|
setStatus("awaiting_confirm");
|
|
671
775
|
setConfirmPrompt(prompt);
|
|
672
|
-
pendingConfirmRef.current = { resolve };
|
|
776
|
+
pendingConfirmRef.current = { resolve: resolve2 };
|
|
673
777
|
});
|
|
674
778
|
}, []);
|
|
675
779
|
const runLlmLoop = useCallback(
|
|
@@ -681,6 +785,7 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false }
|
|
|
681
785
|
confirm,
|
|
682
786
|
print,
|
|
683
787
|
dangerousPatterns: config2.dangerousPatterns,
|
|
788
|
+
// autoMode=true 时自动同意 normal 级工具调用,无需用户确认
|
|
684
789
|
autoApproveNormal: autoMode2
|
|
685
790
|
};
|
|
686
791
|
let currentMessages = history;
|
|
@@ -725,12 +830,14 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false }
|
|
|
725
830
|
if (toolCalls.length > 0) {
|
|
726
831
|
const assistantMsg = {
|
|
727
832
|
role: "assistant",
|
|
833
|
+
// content 可能为空字符串(纯工具调用),存 null 符合 OpenAI API 规范
|
|
728
834
|
content: assistantText || null,
|
|
729
835
|
tool_calls: toolCalls.map((tc) => ({
|
|
730
836
|
id: tc.id,
|
|
731
837
|
type: "function",
|
|
732
838
|
function: { name: tc.name, arguments: tc.arguments }
|
|
733
839
|
})),
|
|
840
|
+
// 仅在存在思考内容时扩展 reasoning_content 字段
|
|
734
841
|
...assistantReasoning ? { reasoning_content: assistantReasoning } : {}
|
|
735
842
|
};
|
|
736
843
|
setMessages((prev) => {
|
|
@@ -804,6 +911,29 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false }
|
|
|
804
911
|
return;
|
|
805
912
|
}
|
|
806
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
|
+
}
|
|
807
937
|
const userMsg = { role: "user", content: trimmed };
|
|
808
938
|
const nextMessages = [...messages, userMsg];
|
|
809
939
|
setMessages(nextMessages);
|
|
@@ -812,6 +942,7 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false }
|
|
|
812
942
|
setToolName(void 0);
|
|
813
943
|
setMessages((prev) => [
|
|
814
944
|
...prev,
|
|
945
|
+
// 错误消息前缀 "[error]" 便于用户识别和日志筛选
|
|
815
946
|
{ role: "assistant", content: `[error] ${String(err)}` }
|
|
816
947
|
]);
|
|
817
948
|
});
|
|
@@ -819,27 +950,116 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false }
|
|
|
819
950
|
[status, messages, runLlmLoop]
|
|
820
951
|
);
|
|
821
952
|
const isInputActive = status === "idle" || status === "awaiting_confirm";
|
|
822
|
-
return
|
|
823
|
-
|
|
824
|
-
/* @__PURE__ */
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
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));
|
|
843
1063
|
}
|
|
844
1064
|
|
|
845
1065
|
// src/index.ts
|
|
@@ -874,4 +1094,13 @@ if (!finalConfig.apiKey) {
|
|
|
874
1094
|
);
|
|
875
1095
|
process.exit(1);
|
|
876
1096
|
}
|
|
877
|
-
|
|
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 }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhongqian97-code/ecode",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "A minimal Claude Code clone with REPL interface and bash tool calling",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "zhongqian97-code",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"files": [
|
|
30
30
|
"dist",
|
|
31
|
+
"skills",
|
|
31
32
|
"README.md",
|
|
32
33
|
"LICENSE"
|
|
33
34
|
],
|