cdsa-harness 0.1.1 → 0.4.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/README.md +106 -44
- package/package.json +2 -2
- package/src/cli.js +321 -64
- package/src/config.js +23 -4
- package/src/llm.js +188 -49
- package/src/loop.js +65 -8
- package/src/plugins.js +191 -0
- package/src/skills.js +61 -0
- package/src/tools.js +43 -3
- package/workspace/.cdsa/plugins/word_count.mjs +31 -0
- package/workspace/.cdsa/skills/summarize.md +5 -0
package/src/cli.js
CHANGED
|
@@ -1,17 +1,29 @@
|
|
|
1
1
|
// CDSA Harness TUI 본체 — 터미널 REPL.
|
|
2
|
-
//
|
|
2
|
+
// 핵심 차별점: '교육 모드' — 실제 API 를 붙여도 에이전트 내부에서 벌어지는 일
|
|
3
|
+
// (컨텍스트 구성 → API 요청 → 모델 판단 → 토큰/지연 → 도구 실행 → 결과 되먹임)을 단계별로 드러낸다.
|
|
3
4
|
import readline from "node:readline/promises";
|
|
4
5
|
import { stdin, stdout } from "node:process";
|
|
5
6
|
|
|
6
7
|
import { renderBanner } from "./banner.js";
|
|
7
|
-
import {
|
|
8
|
-
|
|
8
|
+
import {
|
|
9
|
+
ENV_KEYS,
|
|
10
|
+
PROVIDERS,
|
|
11
|
+
SUGGESTED_MODELS,
|
|
12
|
+
configPath,
|
|
13
|
+
loadConfig,
|
|
14
|
+
saveConfig,
|
|
15
|
+
} from "./config.js";
|
|
16
|
+
import { execFileSync } from "node:child_process";
|
|
17
|
+
|
|
18
|
+
import { AgentLoop, Step } from "./loop.js";
|
|
9
19
|
import { LLMClient } from "./llm.js";
|
|
20
|
+
import { discoverNpmExtensions, loadPlugins } from "./plugins.js";
|
|
10
21
|
import { SessionLog, sessionsDir } from "./session.js";
|
|
22
|
+
import { loadSkills, renderSkill } from "./skills.js";
|
|
11
23
|
import { Toolbox } from "./tools.js";
|
|
12
24
|
import { c, panel, renderDiff } from "./ui.js";
|
|
13
25
|
|
|
14
|
-
const VERSION = "0.
|
|
26
|
+
const VERSION = "0.2.0";
|
|
15
27
|
|
|
16
28
|
const STEP_STYLE = {
|
|
17
29
|
[Step.USER_INPUT]: ["🧑", "cyan"],
|
|
@@ -22,39 +34,118 @@ const STEP_STYLE = {
|
|
|
22
34
|
[Step.APPROVAL]: ["🔐", "yellow"],
|
|
23
35
|
[Step.TOOL_RUN]: ["🔧", "blue"],
|
|
24
36
|
[Step.TOOL_RESULT]: ["📄", "grey"],
|
|
37
|
+
[Step.FEEDBACK]: ["↩️", "grey"],
|
|
25
38
|
[Step.DONE]: ["✅", "green"],
|
|
26
39
|
[Step.ERROR]: ["❌", "red"],
|
|
27
40
|
};
|
|
28
41
|
|
|
29
|
-
function
|
|
42
|
+
function clip(s, n) {
|
|
43
|
+
s = String(s ?? "");
|
|
44
|
+
return s.length > n ? s.slice(0, n) + " …" : s;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// cfg.teach_mode 를 실행 중 토글할 수 있으므로 closure 로 cfg 를 잡아둔다.
|
|
48
|
+
function makePrinter(cfg) {
|
|
30
49
|
return (ev) => {
|
|
31
|
-
|
|
32
|
-
|
|
50
|
+
if (cfg.teach_mode) return printTeach(ev);
|
|
51
|
+
return printCompact(ev);
|
|
52
|
+
};
|
|
53
|
+
}
|
|
33
54
|
|
|
34
|
-
|
|
35
|
-
|
|
55
|
+
// ---- 교육(teach) 렌더: 내부 과정을 패널로 펼쳐 보여준다 ----
|
|
56
|
+
function printTeach(ev) {
|
|
57
|
+
const d = ev.data || {};
|
|
58
|
+
switch (ev.step) {
|
|
59
|
+
case Step.USER_INPUT:
|
|
60
|
+
console.log(`${c.cyan("🧑 ①")} ${c.bold("사용자 입력")} ${c.grey(clip(ev.detail, 200))}`);
|
|
61
|
+
return;
|
|
62
|
+
|
|
63
|
+
case Step.BUILD_CONTEXT:
|
|
64
|
+
console.log(`${c.grey("🧱")} ${c.dim(ev.detail)}`);
|
|
65
|
+
return;
|
|
36
66
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
67
|
+
case Step.MODEL_CALL: {
|
|
68
|
+
const lines = [];
|
|
69
|
+
lines.push(`${c.grey("provider/model")} ${c.bold(`${d.provider} · ${d.model}`)}`);
|
|
70
|
+
lines.push(c.grey(`모델에 보내는 메시지 ${d.messages?.length || 0}개 · 추정 ${d.estTokens} 토큰 · ${d.totalChars}자`));
|
|
71
|
+
for (const m of d.messages || []) {
|
|
72
|
+
const roleColor = m.role === "system" ? c.magenta : m.role === "user" ? c.cyan : m.role === "assistant" ? c.green : c.yellow;
|
|
73
|
+
lines.push(` ${roleColor(m.role.padEnd(9))} ${c.grey(`${m.chars}자${m.extra || ""}`)}`);
|
|
74
|
+
}
|
|
75
|
+
lines.push(c.grey(`제공 도구(${d.tools?.length || 0}): ${(d.tools || []).join(", ")}`));
|
|
76
|
+
console.log(panel(lines, { title: `🧠 ② LLM 호출 — 반복 ${d.iteration}`, color: "magenta" }));
|
|
77
|
+
if (d.systemPrompt) {
|
|
78
|
+
console.log(panel(clip(d.systemPrompt, 600).split("\n"), {
|
|
79
|
+
title: "📜 시스템 프롬프트 (규칙+폴더가 여기 주입됨)",
|
|
80
|
+
color: "grey",
|
|
81
|
+
}));
|
|
40
82
|
}
|
|
41
83
|
return;
|
|
42
84
|
}
|
|
43
|
-
|
|
44
|
-
|
|
85
|
+
|
|
86
|
+
case Step.MODEL_REPLY: {
|
|
87
|
+
const lines = [];
|
|
88
|
+
if (ev.detail && ev.detail !== "(텍스트 없음)") lines.push(...clip(ev.detail, 1200).split("\n"));
|
|
89
|
+
for (const tc of d.toolCalls || []) {
|
|
90
|
+
lines.push(c.yellow(`↳ 도구 호출 요청: ${c.bold(tc.name)}(${clip(JSON.stringify(tc.args), 200)})`));
|
|
91
|
+
}
|
|
92
|
+
const meta = [];
|
|
93
|
+
if (d.latencyMs != null) meta.push(`응답 ${d.latencyMs}ms`);
|
|
94
|
+
if (d.usage) meta.push(`토큰 입력 ${d.usage.input ?? "?"}/출력 ${d.usage.output ?? "?"}/합계 ${d.usage.total ?? "?"}`);
|
|
95
|
+
if (d.request?.bodyBytes) meta.push(`요청 ${d.request.bodyBytes}B`);
|
|
96
|
+
if (meta.length) lines.push(c.grey("─ " + meta.join(" · ")));
|
|
97
|
+
else lines.push(c.dim("(mock: 토큰/지연 측정 없음)"));
|
|
98
|
+
console.log(panel(lines.length ? lines : ["(빈 응답)"], { title: "🤖 ③ 모델 응답 (원본 판단)", color: "green" }));
|
|
45
99
|
return;
|
|
46
100
|
}
|
|
47
|
-
|
|
101
|
+
|
|
102
|
+
case Step.TOOL_DECISION:
|
|
103
|
+
console.log(`${c.yellow("🤔 ④")} ${c.bold("도구 판단")} ${c.grey(clip(ev.detail, 200))}`);
|
|
104
|
+
return;
|
|
105
|
+
|
|
106
|
+
case Step.TOOL_RUN:
|
|
107
|
+
console.log(`${c.blue("🔧 ⑤")} ${c.bold(ev.title)} ${c.grey(clip(ev.detail, 200))}`);
|
|
108
|
+
return;
|
|
109
|
+
|
|
110
|
+
case Step.TOOL_RESULT:
|
|
111
|
+
console.log(panel(clip(ev.detail, 1500).split("\n"), { title: `📄 ${ev.title}`, color: "grey" }));
|
|
112
|
+
return;
|
|
113
|
+
|
|
114
|
+
case Step.FEEDBACK:
|
|
115
|
+
console.log(`${c.grey("↩️ ⑥ 결과 되먹임")} ${c.dim(clip(ev.detail, 200))}`);
|
|
116
|
+
console.log(c.dim(" └ 도구 결과가 컨텍스트에 더해진 채로 ②부터 다시 — 이 반복이 'Agent Loop' 입니다."));
|
|
117
|
+
return;
|
|
118
|
+
|
|
119
|
+
case Step.APPROVAL:
|
|
120
|
+
if (ev.title.includes("자동 승인")) console.log(`${c.yellow("🔓")} ${c.dim(ev.title)}`);
|
|
121
|
+
return;
|
|
122
|
+
|
|
123
|
+
case Step.DONE:
|
|
124
|
+
console.log(panel((ev.detail || "완료").split("\n"), { title: "✅ 완료", color: "green" }));
|
|
125
|
+
return;
|
|
126
|
+
|
|
127
|
+
case Step.ERROR:
|
|
48
128
|
console.log(panel((ev.detail || "").split("\n"), { title: `❌ ${ev.title}`, color: "red" }));
|
|
49
129
|
return;
|
|
50
|
-
|
|
130
|
+
}
|
|
131
|
+
}
|
|
51
132
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
133
|
+
// ---- 간결(compact) 렌더: 한 줄 위주 ----
|
|
134
|
+
function printCompact(ev) {
|
|
135
|
+
const [icon, color] = STEP_STYLE[ev.step] || ["•", "cyan"];
|
|
136
|
+
const paint = c[color] || ((x) => x);
|
|
137
|
+
if (ev.step === Step.APPROVAL && !ev.title.includes("자동 승인")) return;
|
|
138
|
+
if (ev.step === Step.MODEL_REPLY) {
|
|
139
|
+
if (ev.detail && ev.detail !== "(텍스트 없음)") console.log(panel(ev.detail.split("\n"), { title: "🤖 모델", color: "green" }));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (ev.step === Step.DONE) return console.log(panel((ev.detail || "완료").split("\n"), { title: "✅ 완료", color: "green" }));
|
|
143
|
+
if (ev.step === Step.ERROR) return console.log(panel((ev.detail || "").split("\n"), { title: `❌ ${ev.title}`, color: "red" }));
|
|
144
|
+
if (ev.step === Step.FEEDBACK) return;
|
|
145
|
+
let detail = clip((ev.detail || "").trim().replace(/\s+/g, " "), 110);
|
|
146
|
+
let line = `${paint(icon)} ${paint(c.bold(ev.title))}`;
|
|
147
|
+
if (detail && ev.step !== Step.USER_INPUT) line += ` ${c.grey(detail)}`;
|
|
148
|
+
console.log(line);
|
|
58
149
|
}
|
|
59
150
|
|
|
60
151
|
function makeApproval(rl) {
|
|
@@ -80,23 +171,36 @@ function makeApproval(rl) {
|
|
|
80
171
|
};
|
|
81
172
|
}
|
|
82
173
|
|
|
174
|
+
function makeClient(cfg) {
|
|
175
|
+
return new LLMClient({
|
|
176
|
+
provider: cfg.provider,
|
|
177
|
+
apiKey: cfg.resolvedKey(),
|
|
178
|
+
model: cfg.model,
|
|
179
|
+
temperature: cfg.temperature,
|
|
180
|
+
maxTokens: cfg.max_tokens,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
83
184
|
function printIntro(cfg) {
|
|
84
185
|
console.log(renderBanner());
|
|
85
|
-
console.log(c.dim("
|
|
186
|
+
console.log(c.dim("AI 에이전트의 내부 동작을 단계별로 드러내는 교육용 하네스"));
|
|
86
187
|
console.log();
|
|
188
|
+
const keySource = cfg.provider === "mock" ? "-" : cfg.api_key ? "config.json" : ENV_KEYS[cfg.provider] && process.env[ENV_KEYS[cfg.provider]] ? `환경변수 ${ENV_KEYS[cfg.provider]}` : c.red("없음");
|
|
87
189
|
const rows = [
|
|
88
190
|
["버전", `v${VERSION}`],
|
|
89
191
|
["provider", cfg.provider],
|
|
90
192
|
["model", cfg.model],
|
|
193
|
+
["API 키", keySource],
|
|
194
|
+
["교육 모드", cfg.teach_mode ? c.green("ON (과정 펼쳐보기)") : "OFF"],
|
|
91
195
|
["작업 폴더", cfg.workspacePath()],
|
|
92
196
|
["승인 모드", cfg.approval_mode],
|
|
93
197
|
["셸 실행", cfg.allow_shell ? "허용" : "차단"],
|
|
94
198
|
];
|
|
95
|
-
const lines = rows.map(([k, v]) => `${c.grey(k)}
|
|
199
|
+
const lines = rows.map(([k, v]) => `${c.grey(k.padEnd(9))} ${c.bold(v)}`);
|
|
96
200
|
console.log(panel(lines, { title: "⚙️ CDSA Harness 설정", color: "cyan" }));
|
|
97
201
|
console.log(
|
|
98
202
|
c.dim("명령: ") +
|
|
99
|
-
`${c.cyan("/
|
|
203
|
+
`${c.cyan("/setup")} 연결 · ${c.cyan("/teach")} 교육모드 · ${c.cyan("/context")} 컨텍스트 · ${c.cyan("/help")} 도움말 · ${c.cyan("/quit")} 종료\n`
|
|
100
204
|
);
|
|
101
205
|
}
|
|
102
206
|
|
|
@@ -105,29 +209,87 @@ function printHelp() {
|
|
|
105
209
|
panel(
|
|
106
210
|
[
|
|
107
211
|
c.bold("사용법"),
|
|
108
|
-
|
|
212
|
+
`시키고 싶은 일을 한국어로 입력하세요. 예) ${c.cyan("notes.txt 맨 아래에 할 일 3개 추가해줘")}`,
|
|
109
213
|
"",
|
|
110
214
|
c.bold("슬래시 명령"),
|
|
111
|
-
` ${c.cyan("/
|
|
112
|
-
` ${c.cyan("/
|
|
113
|
-
` ${c.cyan("/
|
|
114
|
-
` ${c.cyan("/
|
|
115
|
-
` ${c.cyan("/
|
|
215
|
+
` ${c.cyan("/setup")} 제공자·API 키·모델 연결(대화형)`,
|
|
216
|
+
` ${c.cyan("/provider")} <openai|anthropic|openrouter|mock> 제공자 변경`,
|
|
217
|
+
` ${c.cyan("/model")} <이름> 모델 변경`,
|
|
218
|
+
` ${c.cyan("/teach")} 교육 모드 켜기/끄기(내부 과정 펼쳐보기)`,
|
|
219
|
+
` ${c.cyan("/context")} 지금 모델에 보내는 컨텍스트 들여다보기`,
|
|
220
|
+
` ${c.cyan("/skills")} 스킬 목록(.cdsa/skills 의 /명령들)`,
|
|
221
|
+
` ${c.cyan("/plugins")} 플러그인 목록(.cdsa/plugins 의 추가 도구)`,
|
|
222
|
+
` ${c.cyan("/reset")} 대화/컨텍스트 초기화`,
|
|
223
|
+
` ${c.cyan("/config")} 현재 설정값`,
|
|
224
|
+
` ${c.cyan("/quit")} 종료 (Ctrl+D)`,
|
|
116
225
|
"",
|
|
117
|
-
c.bold("
|
|
118
|
-
" 입력 →
|
|
226
|
+
c.bold("교육 모드에서 보이는 단계"),
|
|
227
|
+
" ① 입력 → ② LLM 호출(컨텍스트·도구) → ③ 모델 응답(토큰·지연) →",
|
|
228
|
+
" ④ 도구 판단 → ⑤ 실행/승인 → ⑥ 결과 되먹임 → (반복)",
|
|
119
229
|
],
|
|
120
230
|
{ title: "도움말", color: "cyan" }
|
|
121
231
|
)
|
|
122
232
|
);
|
|
123
233
|
}
|
|
124
234
|
|
|
235
|
+
// 대화형 연결 설정. 키는 환경변수가 있으면 그걸 우선 안내(파일 저장 안 함).
|
|
236
|
+
async function runSetup(rl, cfg) {
|
|
237
|
+
console.log(panel(
|
|
238
|
+
[
|
|
239
|
+
"어떤 AI 에 연결할까요? 번호를 입력하세요.",
|
|
240
|
+
` ${c.bold("1")}) openai (GPT, 키: ${ENV_KEYS.openai})`,
|
|
241
|
+
` ${c.bold("2")}) anthropic (Claude, 키: ${ENV_KEYS.anthropic})`,
|
|
242
|
+
` ${c.bold("3")}) openrouter (여러 모델 중계, 키: ${ENV_KEYS.openrouter})`,
|
|
243
|
+
` ${c.bold("4")}) mock (키 없이 연습)`,
|
|
244
|
+
],
|
|
245
|
+
{ title: "🔌 연결 설정 (/setup)", color: "cyan" }
|
|
246
|
+
));
|
|
247
|
+
const pick = (await rl.question(c.cyan("제공자 번호 [1-4]: "))).trim();
|
|
248
|
+
const provider = { "1": "openai", "2": "anthropic", "3": "openrouter", "4": "mock" }[pick];
|
|
249
|
+
if (!provider) {
|
|
250
|
+
console.log(c.yellow("취소했습니다."));
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
cfg.provider = provider;
|
|
254
|
+
|
|
255
|
+
if (provider === "mock") {
|
|
256
|
+
cfg.model = "mock-agent";
|
|
257
|
+
} else {
|
|
258
|
+
const envName = ENV_KEYS[provider];
|
|
259
|
+
const envVal = (process.env[envName] || "").trim();
|
|
260
|
+
if (envVal) {
|
|
261
|
+
const useEnv = (await rl.question(c.cyan(`환경변수 ${envName} 에서 키를 찾았어요. 사용할까요? [Y/n] `))).trim().toLowerCase();
|
|
262
|
+
if (useEnv === "" || useEnv === "y" || useEnv === "yes") {
|
|
263
|
+
cfg.api_key = ""; // 환경변수 사용 → 파일엔 저장 안 함
|
|
264
|
+
} else {
|
|
265
|
+
const k = (await rl.question(c.cyan("API 키 붙여넣기(입력이 보일 수 있음): "))).trim();
|
|
266
|
+
cfg.api_key = k;
|
|
267
|
+
}
|
|
268
|
+
} else {
|
|
269
|
+
console.log(c.dim(`(또는 종료 후 환경변수 ${envName} 에 키를 넣어두면 파일에 저장하지 않아도 됩니다)`));
|
|
270
|
+
const k = (await rl.question(c.cyan("API 키 붙여넣기(입력이 보일 수 있음): "))).trim();
|
|
271
|
+
cfg.api_key = k;
|
|
272
|
+
}
|
|
273
|
+
const sugg = SUGGESTED_MODELS[provider] || [];
|
|
274
|
+
const def = sugg[0] || "";
|
|
275
|
+
const m = (await rl.question(c.cyan(`모델 [${def}] (추천: ${sugg.join(", ")}): `))).trim();
|
|
276
|
+
cfg.model = m || def;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const saved = saveConfig(cfg);
|
|
280
|
+
console.log(c.green(`설정을 저장했습니다 → ${saved}`));
|
|
281
|
+
if (!cfg.isReady()) console.log(c.yellow("⚠️ 아직 키가 없어 호출이 실패할 수 있어요. 환경변수나 /setup 으로 키를 넣어주세요."));
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
|
|
125
285
|
function parseArgs(argv) {
|
|
126
286
|
const out = { _: [] };
|
|
127
287
|
for (let i = 0; i < argv.length; i++) {
|
|
128
288
|
const a = argv[i];
|
|
129
289
|
if (a === "--auto") out.auto = true;
|
|
130
290
|
else if (a === "--help" || a === "-h") out.help = true;
|
|
291
|
+
else if (a === "--setup") out.setup = true;
|
|
292
|
+
else if (a === "--no-teach") out.noTeach = true;
|
|
131
293
|
else if (a === "--provider") out.provider = argv[++i];
|
|
132
294
|
else if (a === "--model") out.model = argv[++i];
|
|
133
295
|
else if (a === "--workspace") out.workspace = argv[++i];
|
|
@@ -140,49 +302,81 @@ export async function main(argv = []) {
|
|
|
140
302
|
const args = parseArgs(argv);
|
|
141
303
|
if (args.help) {
|
|
142
304
|
console.log(
|
|
143
|
-
"CDSA Harness —
|
|
305
|
+
"CDSA Harness — AI 에이전트 내부를 드러내는 교육용 하네스 (터미널)\n\n" +
|
|
144
306
|
"사용법: cdsa-harness [옵션]\n" +
|
|
145
|
-
"
|
|
307
|
+
" cdsa-harness add <npm-패키지> 플러그인 설치(이후 자동 로드)\n" +
|
|
308
|
+
" --provider <openai|anthropic|openrouter|mock>\n" +
|
|
146
309
|
" --model <모델명>\n" +
|
|
147
310
|
" --workspace <폴더경로>\n" +
|
|
311
|
+
" --setup 대화형 연결 설정 실행\n" +
|
|
312
|
+
" --no-teach 교육 모드 끄고 간결하게\n" +
|
|
148
313
|
" --auto 승인 자동(approval_mode=auto)\n" +
|
|
149
|
-
" -h, --help 도움말\n"
|
|
314
|
+
" -h, --help 도움말\n\n" +
|
|
315
|
+
"API 키는 환경변수로도 인식됩니다: OPENAI_API_KEY / ANTHROPIC_API_KEY / OPENROUTER_API_KEY\n"
|
|
150
316
|
);
|
|
151
317
|
return 0;
|
|
152
318
|
}
|
|
153
319
|
|
|
320
|
+
// `cdsa-harness add <패키지>` — 플러그인을 npm 으로 설치(이후 자동 로드).
|
|
321
|
+
if (args._[0] === "add" || args._[0] === "install") {
|
|
322
|
+
const pkgs = args._.slice(1);
|
|
323
|
+
if (!pkgs.length) {
|
|
324
|
+
console.log("사용법: cdsa-harness add <npm-패키지...> 예) cdsa-harness add cdsa-harness-plugin-git");
|
|
325
|
+
return 1;
|
|
326
|
+
}
|
|
327
|
+
console.log(c.cyan(`npm install ${pkgs.join(" ")} ...`));
|
|
328
|
+
try {
|
|
329
|
+
execFileSync("npm", ["install", ...pkgs], { stdio: "inherit", cwd: process.cwd() });
|
|
330
|
+
console.log(c.green("설치 완료. 다음 실행부터 플러그인이 자동으로 로드됩니다 (/plugins 로 확인)."));
|
|
331
|
+
return 0;
|
|
332
|
+
} catch (e) {
|
|
333
|
+
console.log(c.red(`설치 실패: ${e.message}`));
|
|
334
|
+
return 1;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
154
338
|
const cfg = loadConfig();
|
|
155
339
|
if (args.provider) cfg.provider = args.provider;
|
|
156
340
|
if (args.model) cfg.model = args.model;
|
|
157
341
|
if (args.workspace) cfg.workspace = args.workspace;
|
|
158
342
|
if (args.auto) cfg.approval_mode = "auto";
|
|
343
|
+
if (args.noTeach) cfg.teach_mode = false;
|
|
159
344
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
cfg.provider
|
|
166
|
-
|
|
345
|
+
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
346
|
+
|
|
347
|
+
if (args.setup) {
|
|
348
|
+
await runSetup(rl, cfg);
|
|
349
|
+
} else if (!cfg.isReady() && cfg.provider !== "mock") {
|
|
350
|
+
console.log(c.yellow(`provider=${cfg.provider} 인데 API 키가 없습니다. 연결 설정을 시작합니다.\n`));
|
|
351
|
+
await runSetup(rl, cfg);
|
|
167
352
|
}
|
|
168
353
|
|
|
169
354
|
printIntro(cfg);
|
|
170
355
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
});
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
|
|
356
|
+
// 플러그인(추가 도구)·스킬(프롬프트 템플릿)을 불러온다:
|
|
357
|
+
// ① 파일: .cdsa/plugins · .cdsa/skills (작업폴더/홈)
|
|
358
|
+
// ② npm 패키지: cdsa-harness-plugin-* 자동 발견 + config.plugins 강제 로드
|
|
359
|
+
const filePlugins = await loadPlugins(cfg.workspacePath());
|
|
360
|
+
const npm = await discoverNpmExtensions(process.cwd(), cfg.plugins || []);
|
|
361
|
+
const plugins = [...filePlugins, ...npm.plugins, ...npm.errors.map((e) => ({ error: e }))];
|
|
362
|
+
const skills = {};
|
|
363
|
+
for (const s of npm.skills) skills[s.name] = { name: s.name, description: s.description || "", body: s.body, source: "(npm)" };
|
|
364
|
+
Object.assign(skills, loadSkills(cfg.workspacePath())); // 로컬 파일 스킬이 우선
|
|
365
|
+
const toolbox = new Toolbox(cfg.workspacePath(), cfg.allow_shell, plugins);
|
|
366
|
+
if (toolbox.plugins.length || Object.keys(skills).length || toolbox.pluginErrors.length) {
|
|
367
|
+
const bits = [];
|
|
368
|
+
if (toolbox.plugins.length) bits.push(c.green(`플러그인 ${toolbox.plugins.length}개`) + c.grey(` (${toolbox.plugins.map((p) => p.name).join(", ")})`));
|
|
369
|
+
if (Object.keys(skills).length) bits.push(c.green(`스킬 ${Object.keys(skills).length}개`) + c.grey(` (${Object.keys(skills).map((s) => "/" + s).join(", ")})`));
|
|
370
|
+
if (toolbox.pluginErrors.length) bits.push(c.red(`플러그인 오류 ${toolbox.pluginErrors.length}개`));
|
|
371
|
+
console.log("🔌 " + bits.join(" · ") + "\n");
|
|
372
|
+
}
|
|
180
373
|
|
|
374
|
+
const session = SessionLog.create();
|
|
181
375
|
const loop = new AgentLoop({
|
|
182
376
|
config: cfg,
|
|
183
|
-
client,
|
|
377
|
+
client: makeClient(cfg),
|
|
184
378
|
toolbox,
|
|
185
|
-
onEvent: makePrinter(),
|
|
379
|
+
onEvent: makePrinter(cfg),
|
|
186
380
|
approvalCallback: makeApproval(rl),
|
|
187
381
|
session,
|
|
188
382
|
});
|
|
@@ -195,27 +389,90 @@ export async function main(argv = []) {
|
|
|
195
389
|
try {
|
|
196
390
|
user = (await rl.question(c.bold(c.cyan("› ")))).trim();
|
|
197
391
|
} catch {
|
|
198
|
-
break;
|
|
392
|
+
break;
|
|
199
393
|
}
|
|
200
394
|
if (!user) continue;
|
|
201
395
|
const low = user.toLowerCase();
|
|
396
|
+
|
|
202
397
|
if (["/quit", "/exit", "quit", "exit", ":q"].includes(low)) break;
|
|
203
|
-
if (low === "/help") {
|
|
204
|
-
|
|
398
|
+
if (low === "/help") { printHelp(); continue; }
|
|
399
|
+
if (low === "/reset") { loop.reset(); console.log(c.green("컨텍스트를 초기화했습니다.")); continue; }
|
|
400
|
+
if (low === "/config") { printIntro(cfg); console.log(c.dim(`config.json: ${configPath()}`)); continue; }
|
|
401
|
+
if (low === "/sessions") { console.log(c.dim(`세션 로그: ${sessionsDir()}`)); continue; }
|
|
402
|
+
if (low === "/teach") {
|
|
403
|
+
cfg.teach_mode = !cfg.teach_mode;
|
|
404
|
+
console.log(c.green(`교육 모드 ${cfg.teach_mode ? "ON" : "OFF"}.`));
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
if (low === "/setup" || low === "/login") {
|
|
408
|
+
await runSetup(rl, cfg);
|
|
409
|
+
loop.client = makeClient(cfg);
|
|
410
|
+
loop.reset();
|
|
205
411
|
continue;
|
|
206
412
|
}
|
|
207
|
-
if (low
|
|
413
|
+
if (low.startsWith("/provider")) {
|
|
414
|
+
const p = user.split(/\s+/)[1];
|
|
415
|
+
if (!PROVIDERS.includes(p)) { console.log(c.yellow(`provider 는 ${PROVIDERS.join("/")} 중 하나.`)); continue; }
|
|
416
|
+
cfg.provider = p;
|
|
417
|
+
if (SUGGESTED_MODELS[p]?.length) cfg.model = SUGGESTED_MODELS[p][0];
|
|
418
|
+
loop.client = makeClient(cfg);
|
|
208
419
|
loop.reset();
|
|
209
|
-
console.log(c.green(
|
|
420
|
+
console.log(c.green(`provider=${p}, model=${cfg.model} (키: ${cfg.isReady() ? "OK" : c.red("없음 — /setup")})`));
|
|
210
421
|
continue;
|
|
211
422
|
}
|
|
212
|
-
if (low
|
|
213
|
-
|
|
214
|
-
console.log(c.dim(
|
|
423
|
+
if (low.startsWith("/model")) {
|
|
424
|
+
const m = user.split(/\s+/).slice(1).join(" ").trim();
|
|
425
|
+
if (!m) { console.log(c.dim(`현재 모델: ${cfg.model} · 추천: ${(SUGGESTED_MODELS[cfg.provider] || []).join(", ")}`)); continue; }
|
|
426
|
+
cfg.model = m;
|
|
427
|
+
loop.client = makeClient(cfg);
|
|
428
|
+
console.log(c.green(`model=${m}`));
|
|
215
429
|
continue;
|
|
216
430
|
}
|
|
217
|
-
if (low === "/
|
|
218
|
-
|
|
431
|
+
if (low === "/context") {
|
|
432
|
+
const ctx = loop.contextSummary();
|
|
433
|
+
const lines = [c.grey(`메시지 ${ctx.rows.length}개 · 추정 ${ctx.estTokens} 토큰 · ${ctx.totalChars}자`)];
|
|
434
|
+
for (const m of ctx.rows) lines.push(` ${c.bold(m.role.padEnd(9))} ${c.grey(`${m.chars}자${m.extra || ""}`)}`);
|
|
435
|
+
console.log(panel(lines, { title: "🧩 현재 컨텍스트(다음 호출에 전송됨)", color: "magenta" }));
|
|
436
|
+
console.log(panel(clip(ctx.systemPrompt, 800).split("\n"), { title: "📜 시스템 프롬프트", color: "grey" }));
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
if (low === "/plugins") {
|
|
440
|
+
const lines = [];
|
|
441
|
+
if (!toolbox.plugins.length) lines.push(c.dim("등록된 플러그인이 없습니다."));
|
|
442
|
+
for (const p of toolbox.plugins) {
|
|
443
|
+
const src = p.source && p.source.includes("/") ? "📄 " + p.source.split("/").slice(-1)[0] : "📦 " + (p.source || "npm");
|
|
444
|
+
lines.push(`${c.bold(p.name)}${p.mutating ? c.yellow(" (승인필요)") : ""} ${c.grey(p.description || "")} ${c.dim(src)}`);
|
|
445
|
+
}
|
|
446
|
+
for (const e of toolbox.pluginErrors) lines.push(c.red("✖ " + e));
|
|
447
|
+
lines.push(c.dim("추가: npm 패키지 'cdsa-harness-plugin-*' 설치 → 자동 로드 (cdsa-harness add <pkg>)"));
|
|
448
|
+
lines.push(c.dim("또는: <작업폴더>/.cdsa/plugins/ 에 .js/.mjs 파일"));
|
|
449
|
+
console.log(panel(lines, { title: "🔌 플러그인 (모델이 쓸 수 있는 추가 도구)", color: "blue" }));
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
if (low === "/skills") {
|
|
453
|
+
const names = Object.keys(skills);
|
|
454
|
+
const lines = names.length ? names.map((n) => `${c.cyan("/" + n)} ${c.grey(skills[n].description || "")}`) : [c.dim("등록된 스킬이 없습니다.")];
|
|
455
|
+
lines.push(c.dim("위치: <작업폴더>/.cdsa/skills/ 또는 ~/.cdsa_harness/skills/ (.md). 본문의 $ARGUMENTS 치환."));
|
|
456
|
+
console.log(panel(lines, { title: "🎯 스킬 (프롬프트 템플릿, /이름 으로 실행)", color: "cyan" }));
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// 위 내장 명령에 안 걸린 '/...' → 스킬이면 실행, 아니면 안내.
|
|
461
|
+
if (user.startsWith("/")) {
|
|
462
|
+
const name = low.slice(1).split(/\s+/)[0];
|
|
463
|
+
if (skills[name]) {
|
|
464
|
+
const argStr = user.split(/\s+/).slice(1).join(" ");
|
|
465
|
+
console.log(c.dim(`(스킬 '/${name}' 실행)`));
|
|
466
|
+
rule();
|
|
467
|
+
try {
|
|
468
|
+
await loop.run(renderSkill(skills[name], argStr));
|
|
469
|
+
} catch (e) {
|
|
470
|
+
console.log(c.red(`실행 오류: ${e?.message || e}`));
|
|
471
|
+
}
|
|
472
|
+
rule();
|
|
473
|
+
} else {
|
|
474
|
+
console.log(c.yellow(`알 수 없는 명령/스킬: /${name} — ${c.cyan("/help")}, ${c.cyan("/skills")} 참고`));
|
|
475
|
+
}
|
|
219
476
|
continue;
|
|
220
477
|
}
|
|
221
478
|
|
package/src/config.js
CHANGED
|
@@ -4,18 +4,26 @@ import fs from "node:fs";
|
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
|
|
7
|
-
export const PROVIDERS = ["openai", "openrouter", "mock"];
|
|
7
|
+
export const PROVIDERS = ["openai", "anthropic", "openrouter", "mock"];
|
|
8
8
|
|
|
9
9
|
export const SUGGESTED_MODELS = {
|
|
10
|
-
openai: ["gpt-
|
|
10
|
+
openai: ["gpt-4o-mini", "gpt-4.1-mini", "gpt-4.1"],
|
|
11
|
+
anthropic: ["claude-3-5-haiku-latest", "claude-3-5-sonnet-latest", "claude-sonnet-4-5"],
|
|
11
12
|
openrouter: [
|
|
12
|
-
"openai/gpt-
|
|
13
|
+
"openai/gpt-4o-mini",
|
|
13
14
|
"anthropic/claude-3.5-sonnet",
|
|
14
15
|
"google/gemini-2.5-flash",
|
|
15
16
|
],
|
|
16
17
|
mock: ["mock-agent"],
|
|
17
18
|
};
|
|
18
19
|
|
|
20
|
+
// provider 별로 자동 감지하는 환경변수(파일에 키를 저장하지 않아도 됨)
|
|
21
|
+
export const ENV_KEYS = {
|
|
22
|
+
openai: "OPENAI_API_KEY",
|
|
23
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
24
|
+
openrouter: "OPENROUTER_API_KEY",
|
|
25
|
+
};
|
|
26
|
+
|
|
19
27
|
export const APPROVAL_MODES = ["manual", "auto"];
|
|
20
28
|
|
|
21
29
|
const DEFAULTS = {
|
|
@@ -27,6 +35,9 @@ const DEFAULTS = {
|
|
|
27
35
|
allow_shell: false,
|
|
28
36
|
max_steps: 8,
|
|
29
37
|
temperature: 0.2,
|
|
38
|
+
max_tokens: 1024,
|
|
39
|
+
teach_mode: true,
|
|
40
|
+
plugins: [], // 추가로 불러올 npm 플러그인 패키지 이름(이름 규칙과 무관하게 강제 로드)
|
|
30
41
|
};
|
|
31
42
|
|
|
32
43
|
export function configDir() {
|
|
@@ -54,9 +65,17 @@ export class Config {
|
|
|
54
65
|
return p;
|
|
55
66
|
}
|
|
56
67
|
|
|
68
|
+
// 파일에 저장된 키가 없으면 환경변수에서 찾는다.
|
|
69
|
+
resolvedKey() {
|
|
70
|
+
const direct = (this.api_key || "").trim();
|
|
71
|
+
if (direct) return direct;
|
|
72
|
+
const envName = ENV_KEYS[this.provider];
|
|
73
|
+
return envName ? (process.env[envName] || "").trim() : "";
|
|
74
|
+
}
|
|
75
|
+
|
|
57
76
|
isReady() {
|
|
58
77
|
if (this.provider === "mock") return true;
|
|
59
|
-
return Boolean(
|
|
78
|
+
return Boolean(this.resolvedKey());
|
|
60
79
|
}
|
|
61
80
|
|
|
62
81
|
toJSON() {
|