cdsa-harness 0.11.1 → 0.14.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 +4 -1
- package/package.json +1 -1
- package/skills/answer.md +6 -0
- package/skills/briefing.md +6 -0
- package/skills/budget.md +6 -0
- package/skills/insa.md +6 -0
- package/skills/notice.md +6 -0
- package/src/builtins.js +26 -1
- package/src/cli.js +237 -22
- package/src/config.js +2 -0
- package/src/ui.js +10 -3
package/README.md
CHANGED
|
@@ -68,6 +68,9 @@ export OPENROUTER_API_KEY=sk-or-...
|
|
|
68
68
|
|
|
69
69
|
| 명령 | 설명 |
|
|
70
70
|
|------|------|
|
|
71
|
+
| `/guide` · `/tutorial` | 빠른 시작 안내 · 단계별 인터랙티브 튜토리얼 |
|
|
72
|
+
| `/workspace <폴더>` | 작업 폴더 보기/변경 (`.` = 현재 폴더) |
|
|
73
|
+
| `/color` | 색상 켜기/끄기(흑백) |
|
|
71
74
|
| `/setup` | 제공자·API 키·모델 대화형 연결 |
|
|
72
75
|
| `/provider <이름>` | openai · anthropic · openrouter · mock 전환 |
|
|
73
76
|
| `/model <이름>` | 모델 변경 |
|
|
@@ -153,7 +156,7 @@ export default {
|
|
|
153
156
|
- **기본 내장 스킬**(설치하면 누구에게나 제공):
|
|
154
157
|
- 실용: `/explain` 쉽게 설명 · `/review` 코드 리뷰 · `/summarize` 3줄 요약 · `/tour` 프로젝트 브리핑 · `/todo` 미완성(TODO) 수집 · `/plan` 실행 전 계획만
|
|
155
158
|
- 교육/재미: `/eli5` 5살도 알게 · `/rubberduck` 질문으로 디버깅 · `/quiz` 학습 퀴즈 · `/haiku` 하이쿠 · `/loop` 이 요청을 에이전트 루프로 어떻게 처리할지 해설
|
|
156
|
-
- **🇰🇷 공공(대한민국)**: `/minwon` 민원분류 · `/gongmun` 공문 기안 · `/privacy` 개인정보 점검 · `/press` 보도자료 · `/report` 개조식 보고 · `/minutes` 회의록 · `/policyqa` 정책 Q&A · `/hwpx` 한컴 문서 요약
|
|
159
|
+
- **🇰🇷 공공(대한민국)**: `/minwon` 민원분류 · `/gongmun` 공문 기안 · `/privacy` 개인정보 점검 · `/press` 보도자료 · `/report` 개조식 보고 · `/minutes` 회의록 · `/insa` 인사발령 · `/budget` 예산 검토 · `/notice` 공고문 · `/answer` 민원답변 · `/briefing` 보도협조 · `/policyqa` 정책 Q&A · `/hwpx` 한컴 문서 요약
|
|
157
160
|
|
|
158
161
|
## 🇰🇷 공공 특화 + HWPX
|
|
159
162
|
|
package/package.json
CHANGED
package/skills/answer.md
ADDED
package/skills/budget.md
ADDED
package/skills/insa.md
ADDED
package/skills/notice.md
ADDED
package/src/builtins.js
CHANGED
|
@@ -2,11 +2,26 @@
|
|
|
2
2
|
// 스킬/플러그인/버전을 바꾼 뒤 `node scripts/gen-builtins.mjs` 로 재생성하세요.
|
|
3
3
|
import p0 from "../plugins/hwpx_read.mjs";
|
|
4
4
|
|
|
5
|
-
export const VERSION = "0.
|
|
5
|
+
export const VERSION = "0.14.0";
|
|
6
6
|
|
|
7
7
|
export const BUILTIN_PLUGINS = [p0];
|
|
8
8
|
|
|
9
9
|
export const BUILTIN_SKILLS = [
|
|
10
|
+
{
|
|
11
|
+
"name": "answer",
|
|
12
|
+
"description": "민원 답변서(회신문) 초안 작성 [공공]",
|
|
13
|
+
"body": "다음 민원에 대한 답변서(회신문) 초안을 작성해줘($ARGUMENTS 가 파일명이면 read_file 로 읽어).\n구성: 인사말 / 민원요지 요약 / 검토결과(근거·관련규정[ ]) / 조치사항 / 맺음말.\n정중하고 공감하는 공공 민원 답변 문체로. 단정이 어려운 부분은 '확인 후 안내' 로 표시."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"name": "briefing",
|
|
17
|
+
"description": "보도협조(브리핑) 요청문·요약 작성 [공공]",
|
|
18
|
+
"body": "\"$ARGUMENTS\" 를 주제로 언론 보도협조 요청문을 작성해줘.\n구성: 제목 / 협조요청 취지 / 핵심 메시지 3가지 / 일정·자료 / 담당자 연락처[ ].\n객관적이고 간결하게 한국어로. 과장 표현은 피해줘."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "budget",
|
|
22
|
+
"description": "예산(안) 항목을 검토·요약하고 점검 포인트 제시 [공공]",
|
|
23
|
+
"body": "$ARGUMENTS (파일명이면 read_file 로 읽어)의 예산 내용을 한국어로 정리해줘.\n1) 총액·주요 항목 표(항목 / 금액 / 비중) 2) 전년 대비/특이사항 3) 점검 포인트(과다·누락·근거 불명).\n숫자는 그대로 인용하고, 계산이 필요하면 근거를 함께 보여줘. 파일은 수정하지 마."
|
|
24
|
+
},
|
|
10
25
|
{
|
|
11
26
|
"name": "eli5",
|
|
12
27
|
"description": "지정한 파일/개념을 5살도 알게 아주 쉽게 설명",
|
|
@@ -32,6 +47,11 @@ export const BUILTIN_SKILLS = [
|
|
|
32
47
|
"description": "한컴 HWPX(.hwpx) 문서를 읽어 핵심을 개조식으로 요약 [공공]",
|
|
33
48
|
"body": "$ARGUMENTS 파일을 hwpx_read 도구로 읽어 본문 텍스트를 추출한 뒤,\n핵심 내용을 한국어 개조식('ㅇ' 글머리)으로 요약해줘. 파일은 수정하지 마.\n(.hwp 구버전이면 .hwpx 로 저장 후 다시 시도하라고 안내해줘.)"
|
|
34
49
|
},
|
|
50
|
+
{
|
|
51
|
+
"name": "insa",
|
|
52
|
+
"description": "인사발령 공지문 초안 작성 [공공]",
|
|
53
|
+
"body": "\"$ARGUMENTS\" 내용을 바탕으로 인사발령 공지문 초안을 작성해줘.\n형식: 제목 / 발령일자[ ] / 대상자(직위·성명·소속) 표 / 발령내용(전보·승진·신규 등) / 비고.\n정중한 공공 문체로. 확실하지 않은 항목은 [ ] 로 비워둬. 파일은 수정하지 마."
|
|
54
|
+
},
|
|
35
55
|
{
|
|
36
56
|
"name": "loop",
|
|
37
57
|
"description": "이 요청을 '에이전트 루프' 관점에서 어떻게 처리할지 메타 해설 (CDSA 특별)",
|
|
@@ -47,6 +67,11 @@ export const BUILTIN_SKILLS = [
|
|
|
47
67
|
"description": "민원 내용을 분류(분야·담당부서·긴급도)하고 처리 방향 제안 [공공]",
|
|
48
68
|
"body": "다음 민원을 분석해줘($ARGUMENTS 가 파일명이면 read_file 로 읽어).\n표 형태로 한국어로 깔끔하게 정리해줘:\n1) 분야 분류 2) 담당 부서(추정) 3) 긴급도(상/중/하) 4) 핵심 요지 1줄 5) 처리 방향 제안\n개인정보(연락처·주민번호 등)가 보이면 마스킹해서 표시해줘. 파일은 수정하지 마."
|
|
49
69
|
},
|
|
70
|
+
{
|
|
71
|
+
"name": "notice",
|
|
72
|
+
"description": "공고문(모집·입찰·안내) 초안 작성 [공공]",
|
|
73
|
+
"body": "\"$ARGUMENTS\" 를 주제로 공공 공고문 초안을 작성해줘.\n형식: 제목 / 공고번호[ ] / 목적 / 대상·자격 / 기간·일정 / 신청방법 / 문의처[ ] / 붙임.\n간결하고 명확한 공고 문체로 한국어 작성."
|
|
74
|
+
},
|
|
50
75
|
{
|
|
51
76
|
"name": "plan",
|
|
52
77
|
"description": "시킨 작업을 '실행하지 말고' 단계별 계획만 세우기",
|
package/src/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// CDSA Harness TUI 본체 — 터미널 REPL.
|
|
2
2
|
// 핵심 차별점: '교육 모드' — 실제 API 를 붙여도 에이전트 내부에서 벌어지는 일
|
|
3
3
|
// (컨텍스트 구성 → API 요청 → 모델 판단 → 토큰/지연 → 도구 실행 → 결과 되먹임)을 단계별로 드러낸다.
|
|
4
|
+
import fs from "node:fs";
|
|
4
5
|
import readline from "node:readline/promises";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
import { stdin, stdout } from "node:process";
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
ENV_KEYS,
|
|
12
13
|
PROVIDERS,
|
|
13
14
|
SUGGESTED_MODELS,
|
|
15
|
+
configDir,
|
|
14
16
|
configPath,
|
|
15
17
|
loadConfig,
|
|
16
18
|
saveConfig,
|
|
@@ -24,7 +26,7 @@ import { discoverNpmExtensions, loadPlugins } from "./plugins.js";
|
|
|
24
26
|
import { SessionLog, sessionsDir } from "./session.js";
|
|
25
27
|
import { loadSkills, renderSkill } from "./skills.js";
|
|
26
28
|
import { Toolbox } from "./tools.js";
|
|
27
|
-
import { c, panel, renderDiff } from "./ui.js";
|
|
29
|
+
import { c, panel, renderDiff, setColor } from "./ui.js";
|
|
28
30
|
|
|
29
31
|
// VERSION 은 src/builtins.js(생성물)에서 가져온다 — npm/exe 양쪽에서 동일.
|
|
30
32
|
|
|
@@ -195,6 +197,65 @@ function makeApproval(ask) {
|
|
|
195
197
|
};
|
|
196
198
|
}
|
|
197
199
|
|
|
200
|
+
function isNewer(a, b) {
|
|
201
|
+
const pa = String(a).split(".").map((n) => parseInt(n, 10) || 0);
|
|
202
|
+
const pb = String(b).split(".").map((n) => parseInt(n, 10) || 0);
|
|
203
|
+
for (let i = 0; i < 3; i++) {
|
|
204
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return true;
|
|
205
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return false;
|
|
206
|
+
}
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 시작 시 새 버전 확인(하루 1회, 네트워크 실패는 조용히 무시 → 폐쇄망 안전).
|
|
211
|
+
async function maybeCheckUpdate(cfg) {
|
|
212
|
+
if (cfg.update_check === false) return null;
|
|
213
|
+
const stamp = path.join(configDir(), ".update_check");
|
|
214
|
+
try {
|
|
215
|
+
if (Date.now() - Number(fs.readFileSync(stamp, "utf8")) < 24 * 3600 * 1000) return null;
|
|
216
|
+
} catch {
|
|
217
|
+
/* 첫 확인 */
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
fs.mkdirSync(configDir(), { recursive: true });
|
|
221
|
+
fs.writeFileSync(stamp, String(Date.now())); // 결과와 무관하게 하루 1회로 제한
|
|
222
|
+
} catch {
|
|
223
|
+
/* ignore */
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
const ctrl = new AbortController();
|
|
227
|
+
const t = setTimeout(() => ctrl.abort(), 1500);
|
|
228
|
+
const res = await fetch("https://registry.npmjs.org/cdsa-harness/latest", { signal: ctrl.signal });
|
|
229
|
+
clearTimeout(t);
|
|
230
|
+
if (!res.ok) return null;
|
|
231
|
+
const latest = (await res.json()).version;
|
|
232
|
+
return latest && isNewer(latest, VERSION) ? latest : null;
|
|
233
|
+
} catch {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 작업 폴더 기준으로 플러그인·스킬·도구상자를 구성한다(시작 시 + /workspace 변경 시).
|
|
239
|
+
async function buildExtensions(cfg, mcp) {
|
|
240
|
+
const filePlugins = await loadPlugins(cfg.workspacePath());
|
|
241
|
+
const npm = await discoverNpmExtensions(process.cwd(), cfg.plugins || []);
|
|
242
|
+
const plugins = [
|
|
243
|
+
...filePlugins,
|
|
244
|
+
...npm.plugins,
|
|
245
|
+
...mcp.tools,
|
|
246
|
+
...npm.errors.map((e) => ({ error: e })),
|
|
247
|
+
...mcp.errors.map((e) => ({ error: `MCP ${e}` })),
|
|
248
|
+
];
|
|
249
|
+
const skills = {};
|
|
250
|
+
for (const s of npm.skills) skills[s.name] = { name: s.name, description: s.description || "", body: s.body, source: "(npm)" };
|
|
251
|
+
Object.assign(
|
|
252
|
+
skills,
|
|
253
|
+
loadSkills(cfg.workspacePath(), { importForeign: cfg.import_foreign_skills, extraDirs: cfg.skill_dirs || [] })
|
|
254
|
+
); // 로컬 파일 스킬이 우선
|
|
255
|
+
const toolbox = new Toolbox(cfg.workspacePath(), cfg.allow_shell, plugins);
|
|
256
|
+
return { toolbox, skills };
|
|
257
|
+
}
|
|
258
|
+
|
|
198
259
|
function makeClient(cfg) {
|
|
199
260
|
return new LLMClient({
|
|
200
261
|
provider: cfg.provider,
|
|
@@ -225,12 +286,98 @@ function printIntro(cfg) {
|
|
|
225
286
|
];
|
|
226
287
|
const lines = rows.map(([k, v]) => `${c.grey(k.padEnd(9))} ${c.bold(v)}`);
|
|
227
288
|
console.log(panel(lines, { title: "⚙️ CDSA Harness 설정", color: "cyan" }));
|
|
289
|
+
console.log(
|
|
290
|
+
c.bold(c.cyan("👉 처음이세요? /guide ")) + c.dim("입력하면 빠른 시작 안내가 떠요.")
|
|
291
|
+
);
|
|
228
292
|
console.log(
|
|
229
293
|
c.dim("명령: ") +
|
|
230
|
-
`${c.cyan("/setup")} 연결 · ${c.cyan("/
|
|
294
|
+
`${c.cyan("/setup")} 연결 · ${c.cyan("/skills")} 명령목록 · ${c.cyan("/teach")} 교육모드 · ${c.cyan("/help")} 도움말 · ${c.cyan("/quit")} 종료\n`
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function printGuide() {
|
|
299
|
+
console.log(
|
|
300
|
+
panel(
|
|
301
|
+
[
|
|
302
|
+
c.bold("CDSA Harness 에 오신 걸 환영해요!") + c.dim(" AI 에게 일을 시키고, 그 과정을 눈으로 보는 도구예요."),
|
|
303
|
+
"",
|
|
304
|
+
c.bold("🚀 3단계면 끝"),
|
|
305
|
+
` ${c.cyan("1) AI 연결")} ${c.bold("/setup")} 입력 → OpenAI·Claude·OpenRouter 중 선택 + 키 입력`,
|
|
306
|
+
` ${c.dim("키가 없어도 OK — 자동 '연습(mock)' 모드로 흐름을 체험할 수 있어요.")}`,
|
|
307
|
+
` ${c.cyan("2) 시키기")} 하고 싶은 일을 ${c.bold("한국어로 그냥 입력")}`,
|
|
308
|
+
` 예) ${c.green("notes.txt 에 오늘 할 일 3개 추가해줘")}`,
|
|
309
|
+
` ${c.cyan("3) 승인")} 파일을 고칠 땐 ${c.bold("바뀔 내용(diff)")} 을 먼저 보여줘요 → ${c.green("y")} 누르면 적용`,
|
|
310
|
+
"",
|
|
311
|
+
c.bold("💡 알아두면 좋은 명령"),
|
|
312
|
+
` ${c.cyan("/skills")} 쓸 수 있는 명령 목록(민원분류·요약 등)`,
|
|
313
|
+
` ${c.cyan("/workspace")} 작업할 폴더 보기/바꾸기 (${c.dim("/workspace . = 지금 폴더")})`,
|
|
314
|
+
` ${c.cyan("/teach")} AI 내부 동작 펼쳐보기 켜기/끄기`,
|
|
315
|
+
` ${c.cyan("/about")} 이 도구 정보 · ${c.cyan("/help")} 전체 도움말 · ${c.cyan("/quit")} 종료`,
|
|
316
|
+
"",
|
|
317
|
+
c.bold("🇰🇷 공공 업무 예시 (바로 써보기)"),
|
|
318
|
+
` ${c.green("/minwon")} (민원 내용 붙여넣기) → 분류·담당부서·처리방향`,
|
|
319
|
+
` ${c.green("/gongmun")} 도서관 행사 안내 → 공문 기안 초안`,
|
|
320
|
+
` ${c.green("/privacy")} report.txt → 개인정보 점검`,
|
|
321
|
+
` ${c.green("/hwpx")} 보고서.hwpx → 한컴 문서 요약`,
|
|
322
|
+
"",
|
|
323
|
+
c.dim("막히면 언제든 /help 를 입력하세요. 즐겁게 써보세요! 😊"),
|
|
324
|
+
],
|
|
325
|
+
{ title: "📖 빠른 시작 가이드 (/guide)", color: "cyan" }
|
|
326
|
+
)
|
|
231
327
|
);
|
|
232
328
|
}
|
|
233
329
|
|
|
330
|
+
// 인터랙티브 튜토리얼 — 엔터로 한 페이지씩 진행(첫 실행 시 자동 제안).
|
|
331
|
+
async function runTutorial(ask) {
|
|
332
|
+
const pages = [
|
|
333
|
+
[
|
|
334
|
+
"👋 환영합니다! CDSA Harness 가 처음이라면 이 튜토리얼이 딱이에요.",
|
|
335
|
+
"",
|
|
336
|
+
"이 도구는 'AI 에이전트'예요. 일을 시키면 AI 가 스스로 파일을 보고·읽고·고치며 일합니다.",
|
|
337
|
+
"그 모든 과정을 화면에 단계별로 보여줘서, 보면서 배울 수 있어요.",
|
|
338
|
+
],
|
|
339
|
+
[
|
|
340
|
+
c.bold("1단계. AI 연결"),
|
|
341
|
+
"",
|
|
342
|
+
`${c.cyan("/setup")} 을 입력하면 OpenAI·Claude·OpenRouter 중 고르고 API 키를 넣어요.`,
|
|
343
|
+
"키가 없어도 괜찮아요 — 자동 '연습(mock)' 모드로 흐름을 그대로 체험할 수 있어요.",
|
|
344
|
+
],
|
|
345
|
+
[
|
|
346
|
+
c.bold("2단계. 그냥 시키기"),
|
|
347
|
+
"",
|
|
348
|
+
"하고 싶은 일을 한국어로 입력하면 됩니다. 예를 들면:",
|
|
349
|
+
` ${c.green("notes.txt 에 오늘 할 일 3개 추가해줘")}`,
|
|
350
|
+
` ${c.green("이 폴더에 어떤 파일이 있는지 알려줘")}`,
|
|
351
|
+
],
|
|
352
|
+
[
|
|
353
|
+
c.bold("3단계. 승인 [y/N]"),
|
|
354
|
+
"",
|
|
355
|
+
"AI 가 파일을 고치려 하면, 바뀔 내용(diff)을 먼저 보여줍니다.",
|
|
356
|
+
`${c.green("y")} 를 누르면 적용, ${c.red("n")} 이면 취소. → 위험한 일은 항상 사람이 확인해요(안전장치).`,
|
|
357
|
+
],
|
|
358
|
+
[
|
|
359
|
+
c.bold("알아두면 좋은 명령"),
|
|
360
|
+
` ${c.cyan("/guide")} 빠른 요약 · ${c.cyan("/skills")} 명령 목록 · ${c.cyan("/workspace")} 작업 폴더`,
|
|
361
|
+
` ${c.cyan("/teach")} 내부 펼쳐보기 · ${c.cyan("/help")} 전체 · ${c.cyan("/quit")} 종료`,
|
|
362
|
+
"",
|
|
363
|
+
c.bold("🇰🇷 공공 업무도 바로"),
|
|
364
|
+
` ${c.green("/minwon")} 민원분류 · ${c.green("/gongmun")} 공문 · ${c.green("/privacy")} 개인정보점검 · ${c.green("/hwpx")} 한컴요약`,
|
|
365
|
+
"",
|
|
366
|
+
c.dim("이제 끝! 직접 한번 시켜보세요. 막히면 /guide 또는 /help 를 입력하면 돼요 😊"),
|
|
367
|
+
],
|
|
368
|
+
];
|
|
369
|
+
for (let i = 0; i < pages.length; i++) {
|
|
370
|
+
console.log(panel(pages[i], { title: `📊 튜토리얼 (${i + 1}/${pages.length})`, color: "cyan" }));
|
|
371
|
+
if (i < pages.length - 1) {
|
|
372
|
+
const a = await ask(c.dim(" [엔터] 다음 · [q] 그만 "));
|
|
373
|
+
if (a === null || a.trim().toLowerCase() === "q") {
|
|
374
|
+
console.log(c.dim("튜토리얼을 건너뜁니다. 언제든 /tutorial 로 다시 볼 수 있어요."));
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
234
381
|
function printHelp() {
|
|
235
382
|
console.log(
|
|
236
383
|
panel(
|
|
@@ -239,6 +386,9 @@ function printHelp() {
|
|
|
239
386
|
`시키고 싶은 일을 한국어로 입력하세요. 예) ${c.cyan("notes.txt 맨 아래에 할 일 3개 추가해줘")}`,
|
|
240
387
|
"",
|
|
241
388
|
c.bold("슬래시 명령"),
|
|
389
|
+
` ${c.cyan("/guide")} 처음 사용자용 빠른 시작 안내`,
|
|
390
|
+
` ${c.cyan("/tutorial")} 단계별 인터랙티브 튜토리얼`,
|
|
391
|
+
` ${c.cyan("/color")} 색상 켜기/끄기(흑백)`,
|
|
242
392
|
` ${c.cyan("/about")} 이 도구 정보(made by CDSA)`,
|
|
243
393
|
` ${c.cyan("/setup")} 제공자·API 키·모델 연결(대화형)`,
|
|
244
394
|
` ${c.cyan("/provider")} <openai|anthropic|openrouter|mock> 제공자 변경`,
|
|
@@ -246,6 +396,7 @@ function printHelp() {
|
|
|
246
396
|
` ${c.cyan("/teach")} 교육 모드 켜기/끄기(내부 과정 펼쳐보기)`,
|
|
247
397
|
` ${c.cyan("/stream")} 실시간 스트리밍 출력 켜기/끄기`,
|
|
248
398
|
` ${c.cyan("/context")} 지금 모델에 보내는 컨텍스트 들여다보기`,
|
|
399
|
+
` ${c.cyan("/workspace")} <폴더> 작업 폴더 보기/변경 ('.' = 현재 폴더)`,
|
|
249
400
|
` ${c.cyan("/skills")} 스킬 목록(.cdsa/skills 의 /명령들)`,
|
|
250
401
|
` ${c.cyan("/plugins")} 플러그인 목록(파일·npm 추가 도구)`,
|
|
251
402
|
` ${c.cyan("/mcp")} 연결된 MCP 서버/도구(다른 에이전트와 공용)`,
|
|
@@ -337,6 +488,7 @@ function parseArgs(argv) {
|
|
|
337
488
|
else if (a === "--setup") out.setup = true;
|
|
338
489
|
else if (a === "--no-teach") out.noTeach = true;
|
|
339
490
|
else if (a === "--no-stream") out.noStream = true;
|
|
491
|
+
else if (a === "--no-color") out.noColor = true;
|
|
340
492
|
else if (a === "--provider") out.provider = argv[++i];
|
|
341
493
|
else if (a === "--model") out.model = argv[++i];
|
|
342
494
|
else if (a === "--workspace") out.workspace = argv[++i];
|
|
@@ -358,6 +510,7 @@ export async function main(argv = []) {
|
|
|
358
510
|
" --setup 대화형 연결 설정 실행\n" +
|
|
359
511
|
" --no-teach 교육 모드 끄고 간결하게\n" +
|
|
360
512
|
" --no-stream 실시간 스트리밍 끄기\n" +
|
|
513
|
+
" --no-color 색상 끄기(흑백)\n" +
|
|
361
514
|
" --auto 승인 자동(approval_mode=auto)\n" +
|
|
362
515
|
" -h, --help 도움말\n\n" +
|
|
363
516
|
"API 키는 환경변수로도 인식됩니다: OPENAI_API_KEY / ANTHROPIC_API_KEY / OPENROUTER_API_KEY\n"
|
|
@@ -390,6 +543,8 @@ export async function main(argv = []) {
|
|
|
390
543
|
if (args.auto) cfg.approval_mode = "auto";
|
|
391
544
|
if (args.noTeach) cfg.teach_mode = false;
|
|
392
545
|
if (args.noStream) cfg.stream = false;
|
|
546
|
+
if (args.noColor) cfg.no_color = true;
|
|
547
|
+
if (cfg.no_color) setColor(false); // 색상 끄기(흑백)
|
|
393
548
|
|
|
394
549
|
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
395
550
|
let session = null;
|
|
@@ -422,30 +577,23 @@ export async function main(argv = []) {
|
|
|
422
577
|
|
|
423
578
|
printIntro(cfg);
|
|
424
579
|
|
|
425
|
-
//
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
580
|
+
// 새 버전 안내(있을 때만, 하루 1회)
|
|
581
|
+
const newer = await maybeCheckUpdate(cfg);
|
|
582
|
+
if (newer) {
|
|
583
|
+
console.log(
|
|
584
|
+
c.yellow(`⬆️ 새 버전 v${newer} 가 나왔어요!`) +
|
|
585
|
+
c.dim(` 업데이트: npm i -g cdsa-harness@latest · exe 는 Releases 에서 새로 받기`) +
|
|
586
|
+
"\n"
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// ③ MCP 서버(다른 에이전트와 공용 표준) 연결 — 1회
|
|
431
591
|
if (cfg.mcpServers && Object.keys(cfg.mcpServers).length) {
|
|
432
592
|
process.stdout.write(c.dim("MCP 서버 연결 중...\r"));
|
|
433
593
|
mcp = await connectMcpServers(cfg.mcpServers);
|
|
434
594
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
...npm.plugins,
|
|
438
|
-
...mcp.tools,
|
|
439
|
-
...npm.errors.map((e) => ({ error: e })),
|
|
440
|
-
...mcp.errors.map((e) => ({ error: `MCP ${e}` })),
|
|
441
|
-
];
|
|
442
|
-
const skills = {};
|
|
443
|
-
for (const s of npm.skills) skills[s.name] = { name: s.name, description: s.description || "", body: s.body, source: "(npm)" };
|
|
444
|
-
Object.assign(
|
|
445
|
-
skills,
|
|
446
|
-
loadSkills(cfg.workspacePath(), { importForeign: cfg.import_foreign_skills, extraDirs: cfg.skill_dirs || [] })
|
|
447
|
-
); // 로컬 파일 스킬이 우선
|
|
448
|
-
const toolbox = new Toolbox(cfg.workspacePath(), cfg.allow_shell, plugins);
|
|
595
|
+
// 플러그인·스킬·도구상자를 작업 폴더 기준으로 구성(작업 폴더 변경 시 재사용)
|
|
596
|
+
let { toolbox, skills } = await buildExtensions(cfg, mcp);
|
|
449
597
|
if (toolbox.plugins.length || Object.keys(skills).length || toolbox.pluginErrors.length || mcp.servers.length) {
|
|
450
598
|
const bits = [];
|
|
451
599
|
if (toolbox.plugins.length) bits.push(c.green(`도구 +${toolbox.plugins.length}개`));
|
|
@@ -478,6 +626,46 @@ export async function main(argv = []) {
|
|
|
478
626
|
|
|
479
627
|
const rule = () => console.log(c.grey("─".repeat(Math.min(80, stdout.columns || 80))));
|
|
480
628
|
|
|
629
|
+
// 첫 실행 온보딩(한 번만 — ~/.cdsa_harness/.welcomed 표시): 작업 폴더 설정 + 튜토리얼
|
|
630
|
+
const markerPath = path.join(configDir(), ".welcomed");
|
|
631
|
+
if (stdin.isTTY && !fs.existsSync(markerPath)) {
|
|
632
|
+
console.log(panel(
|
|
633
|
+
[
|
|
634
|
+
"AI 가 파일을 다룰 ‘작업 폴더’를 정하세요.",
|
|
635
|
+
c.dim("이 폴더 밖은 절대 건드리지 않아요(안전장치)."),
|
|
636
|
+
"",
|
|
637
|
+
` ${c.bold("엔터")} 기본값 ${c.cyan("./workspace")} (하위 폴더 자동 생성)`,
|
|
638
|
+
` ${c.bold(".")} 지금 이 폴더를 그대로 사용`,
|
|
639
|
+
` ${c.bold("경로")} 예) ${c.cyan("./문서")} 또는 ${c.cyan("C:\\작업\\프로젝트")}`,
|
|
640
|
+
],
|
|
641
|
+
{ title: "📁 작업 폴더 설정 (처음 한 번)", color: "cyan" }
|
|
642
|
+
));
|
|
643
|
+
const wsAns = await ask(c.cyan("작업 폴더 [엔터=기본]: "));
|
|
644
|
+
if (wsAns !== null && wsAns.trim()) {
|
|
645
|
+
cfg.workspace = wsAns.trim();
|
|
646
|
+
const rebuilt = await buildExtensions(cfg, mcp);
|
|
647
|
+
toolbox = rebuilt.toolbox;
|
|
648
|
+
skills = rebuilt.skills;
|
|
649
|
+
loop.toolbox = toolbox;
|
|
650
|
+
loop.reset();
|
|
651
|
+
}
|
|
652
|
+
try {
|
|
653
|
+
saveConfig(cfg);
|
|
654
|
+
} catch {
|
|
655
|
+
/* 저장 실패 무시 */
|
|
656
|
+
}
|
|
657
|
+
console.log(c.green(`작업 폴더: ${cfg.workspacePath()}`) + c.dim(" (나중에 /workspace 로 변경 가능)\n"));
|
|
658
|
+
|
|
659
|
+
const a = await ask(c.cyan("짧은 튜토리얼을 볼까요? [Y/n] "));
|
|
660
|
+
if (a !== null && ["", "y", "yes"].includes(a.trim().toLowerCase())) await runTutorial(ask);
|
|
661
|
+
try {
|
|
662
|
+
fs.mkdirSync(configDir(), { recursive: true });
|
|
663
|
+
fs.writeFileSync(markerPath, new Date().toISOString());
|
|
664
|
+
} catch {
|
|
665
|
+
/* 표시 실패는 무시 */
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
481
669
|
while (true) {
|
|
482
670
|
const raw = await ask(c.bold(c.cyan("› ")));
|
|
483
671
|
if (raw === null) break; // Ctrl+D / Ctrl+C / 스트림 종료
|
|
@@ -487,6 +675,14 @@ export async function main(argv = []) {
|
|
|
487
675
|
|
|
488
676
|
if (["/quit", "/exit", "quit", "exit", ":q"].includes(low)) break;
|
|
489
677
|
if (low === "/help") { printHelp(); continue; }
|
|
678
|
+
if (low === "/guide" || low === "/start") { printGuide(); continue; }
|
|
679
|
+
if (low === "/tutorial") { await runTutorial(ask); continue; }
|
|
680
|
+
if (low === "/color") {
|
|
681
|
+
cfg.no_color = !cfg.no_color;
|
|
682
|
+
setColor(!cfg.no_color);
|
|
683
|
+
console.log(cfg.no_color ? "색상 끔(흑백)." : c.green("색상 켬."));
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
490
686
|
if (low === "/about") {
|
|
491
687
|
console.log(panel([
|
|
492
688
|
c.bold("CDSA Harness") + c.grey(` v${VERSION}`),
|
|
@@ -501,6 +697,25 @@ export async function main(argv = []) {
|
|
|
501
697
|
}
|
|
502
698
|
if (low === "/reset") { loop.reset(); console.log(c.green("컨텍스트를 초기화했습니다.")); continue; }
|
|
503
699
|
if (low === "/config") { printIntro(cfg); console.log(c.dim(`config.json: ${configPath()}`)); continue; }
|
|
700
|
+
if (low.startsWith("/workspace") || low.startsWith("/cd")) {
|
|
701
|
+
const arg = user.split(/\s+/).slice(1).join(" ").trim();
|
|
702
|
+
if (!arg) {
|
|
703
|
+
console.log(c.dim(`현재 작업 폴더: ${cfg.workspacePath()}`));
|
|
704
|
+
console.log(c.dim("변경: /workspace <폴더경로> (현재 폴더 그대로: /workspace .)"));
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
cfg.workspace = arg;
|
|
708
|
+
const rebuilt = await buildExtensions(cfg, mcp);
|
|
709
|
+
toolbox = rebuilt.toolbox;
|
|
710
|
+
skills = rebuilt.skills;
|
|
711
|
+
loop.toolbox = toolbox;
|
|
712
|
+
loop.reset();
|
|
713
|
+
console.log(
|
|
714
|
+
c.green(`작업 폴더 변경 → ${cfg.workspacePath()}`) +
|
|
715
|
+
c.dim(` (도구 ${toolbox.plugins.length} · 스킬 ${Object.keys(skills).length})`)
|
|
716
|
+
);
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
504
719
|
if (low === "/sessions") { console.log(c.dim(`세션 로그: ${sessionsDir()}`)); continue; }
|
|
505
720
|
if (low === "/teach") {
|
|
506
721
|
cfg.teach_mode = !cfg.teach_mode;
|
package/src/config.js
CHANGED
|
@@ -42,6 +42,8 @@ const DEFAULTS = {
|
|
|
42
42
|
stream: true, // 모델 응답을 실시간(토큰 단위)으로 출력
|
|
43
43
|
import_foreign_skills: true, // .claude/commands 등 외부 포맷 스킬도 읽기(프로젝트+전역)
|
|
44
44
|
skill_dirs: [], // 스킬을 추가로 읽어올 폴더(절대/상대 경로)
|
|
45
|
+
no_color: false, // 색상 끄기(흑백)
|
|
46
|
+
update_check: true, // 시작 시 새 버전 확인(하루 1회, 실패 시 조용히 무시)
|
|
45
47
|
plugins: [], // 추가로 불러올 npm 플러그인 패키지 이름(이름 규칙과 무관하게 강제 로드)
|
|
46
48
|
mcpServers: {}, // MCP 서버 설정 (Claude Code/Cursor 와 동일한 형식)
|
|
47
49
|
};
|
package/src/ui.js
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
// 터미널 출력 헬퍼: ANSI 색, 박스 패널, diff 렌더. (외부 의존성 없음)
|
|
2
2
|
|
|
3
3
|
const ESC = "\x1b[";
|
|
4
|
-
|
|
4
|
+
// 런타임에 켜고 끌 수 있다(/color, --no-color). 기본은 TTY + NO_COLOR 미설정.
|
|
5
|
+
let colorEnabled = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
|
|
6
|
+
export function setColor(on) {
|
|
7
|
+
colorEnabled = Boolean(on);
|
|
8
|
+
}
|
|
9
|
+
export function isColorOn() {
|
|
10
|
+
return colorEnabled;
|
|
11
|
+
}
|
|
5
12
|
|
|
6
13
|
function wrap(open, close) {
|
|
7
|
-
return (s) => (
|
|
14
|
+
return (s) => (colorEnabled ? `${ESC}${open}m${s}${ESC}${close}m` : String(s));
|
|
8
15
|
}
|
|
9
16
|
|
|
10
17
|
export const c = {
|
|
@@ -22,7 +29,7 @@ export const c = {
|
|
|
22
29
|
|
|
23
30
|
// 24bit truecolor (그라데이션 배너용). hex "#rrggbb"
|
|
24
31
|
export function hex(s, hexColor) {
|
|
25
|
-
if (!
|
|
32
|
+
if (!colorEnabled) return String(s);
|
|
26
33
|
const m = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hexColor);
|
|
27
34
|
if (!m) return String(s);
|
|
28
35
|
const [r, g, b] = [m[1], m[2], m[3]].map((x) => parseInt(x, 16));
|