cdsa-harness 0.11.0 → 0.13.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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdsa-harness",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "AI 에이전트의 내부 동작을 단계별로 드러내는 교육용 터미널 하네스. 실시간 스트리밍 + OpenAI/Claude/OpenRouter + MCP + npm 플러그인·크로스포맷 스킬.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,6 @@
1
+ ---
2
+ description: 민원 답변서(회신문) 초안 작성 [공공]
3
+ ---
4
+ 다음 민원에 대한 답변서(회신문) 초안을 작성해줘($ARGUMENTS 가 파일명이면 read_file 로 읽어).
5
+ 구성: 인사말 / 민원요지 요약 / 검토결과(근거·관련규정[ ]) / 조치사항 / 맺음말.
6
+ 정중하고 공감하는 공공 민원 답변 문체로. 단정이 어려운 부분은 '확인 후 안내' 로 표시.
@@ -0,0 +1,6 @@
1
+ ---
2
+ description: 보도협조(브리핑) 요청문·요약 작성 [공공]
3
+ ---
4
+ "$ARGUMENTS" 를 주제로 언론 보도협조 요청문을 작성해줘.
5
+ 구성: 제목 / 협조요청 취지 / 핵심 메시지 3가지 / 일정·자료 / 담당자 연락처[ ].
6
+ 객관적이고 간결하게 한국어로. 과장 표현은 피해줘.
@@ -0,0 +1,6 @@
1
+ ---
2
+ description: 예산(안) 항목을 검토·요약하고 점검 포인트 제시 [공공]
3
+ ---
4
+ $ARGUMENTS (파일명이면 read_file 로 읽어)의 예산 내용을 한국어로 정리해줘.
5
+ 1) 총액·주요 항목 표(항목 / 금액 / 비중) 2) 전년 대비/특이사항 3) 점검 포인트(과다·누락·근거 불명).
6
+ 숫자는 그대로 인용하고, 계산이 필요하면 근거를 함께 보여줘. 파일은 수정하지 마.
package/skills/insa.md ADDED
@@ -0,0 +1,6 @@
1
+ ---
2
+ description: 인사발령 공지문 초안 작성 [공공]
3
+ ---
4
+ "$ARGUMENTS" 내용을 바탕으로 인사발령 공지문 초안을 작성해줘.
5
+ 형식: 제목 / 발령일자[ ] / 대상자(직위·성명·소속) 표 / 발령내용(전보·승진·신규 등) / 비고.
6
+ 정중한 공공 문체로. 확실하지 않은 항목은 [ ] 로 비워둬. 파일은 수정하지 마.
@@ -0,0 +1,6 @@
1
+ ---
2
+ description: 공고문(모집·입찰·안내) 초안 작성 [공공]
3
+ ---
4
+ "$ARGUMENTS" 를 주제로 공공 공고문 초안을 작성해줘.
5
+ 형식: 제목 / 공고번호[ ] / 목적 / 대상·자격 / 기간·일정 / 신청방법 / 문의처[ ] / 붙임.
6
+ 간결하고 명확한 공고 문체로 한국어 작성.
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.11.0";
5
+ export const VERSION = "0.13.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,27 @@ function makeApproval(ask) {
195
197
  };
196
198
  }
197
199
 
200
+ // 작업 폴더 기준으로 플러그인·스킬·도구상자를 구성한다(시작 시 + /workspace 변경 시).
201
+ async function buildExtensions(cfg, mcp) {
202
+ const filePlugins = await loadPlugins(cfg.workspacePath());
203
+ const npm = await discoverNpmExtensions(process.cwd(), cfg.plugins || []);
204
+ const plugins = [
205
+ ...filePlugins,
206
+ ...npm.plugins,
207
+ ...mcp.tools,
208
+ ...npm.errors.map((e) => ({ error: e })),
209
+ ...mcp.errors.map((e) => ({ error: `MCP ${e}` })),
210
+ ];
211
+ const skills = {};
212
+ for (const s of npm.skills) skills[s.name] = { name: s.name, description: s.description || "", body: s.body, source: "(npm)" };
213
+ Object.assign(
214
+ skills,
215
+ loadSkills(cfg.workspacePath(), { importForeign: cfg.import_foreign_skills, extraDirs: cfg.skill_dirs || [] })
216
+ ); // 로컬 파일 스킬이 우선
217
+ const toolbox = new Toolbox(cfg.workspacePath(), cfg.allow_shell, plugins);
218
+ return { toolbox, skills };
219
+ }
220
+
198
221
  function makeClient(cfg) {
199
222
  return new LLMClient({
200
223
  provider: cfg.provider,
@@ -225,12 +248,98 @@ function printIntro(cfg) {
225
248
  ];
226
249
  const lines = rows.map(([k, v]) => `${c.grey(k.padEnd(9))} ${c.bold(v)}`);
227
250
  console.log(panel(lines, { title: "⚙️ CDSA Harness 설정", color: "cyan" }));
251
+ console.log(
252
+ c.bold(c.cyan("👉 처음이세요? /guide ")) + c.dim("입력하면 빠른 시작 안내가 떠요.")
253
+ );
228
254
  console.log(
229
255
  c.dim("명령: ") +
230
- `${c.cyan("/setup")} 연결 · ${c.cyan("/teach")} 교육모드 · ${c.cyan("/context")} 컨텍스트 · ${c.cyan("/help")} 도움말 · ${c.cyan("/quit")} 종료\n`
256
+ `${c.cyan("/setup")} 연결 · ${c.cyan("/skills")} 명령목록 · ${c.cyan("/teach")} 교육모드 · ${c.cyan("/help")} 도움말 · ${c.cyan("/quit")} 종료\n`
231
257
  );
232
258
  }
233
259
 
260
+ function printGuide() {
261
+ console.log(
262
+ panel(
263
+ [
264
+ c.bold("CDSA Harness 에 오신 걸 환영해요!") + c.dim(" AI 에게 일을 시키고, 그 과정을 눈으로 보는 도구예요."),
265
+ "",
266
+ c.bold("🚀 3단계면 끝"),
267
+ ` ${c.cyan("1) AI 연결")} ${c.bold("/setup")} 입력 → OpenAI·Claude·OpenRouter 중 선택 + 키 입력`,
268
+ ` ${c.dim("키가 없어도 OK — 자동 '연습(mock)' 모드로 흐름을 체험할 수 있어요.")}`,
269
+ ` ${c.cyan("2) 시키기")} 하고 싶은 일을 ${c.bold("한국어로 그냥 입력")}`,
270
+ ` 예) ${c.green("notes.txt 에 오늘 할 일 3개 추가해줘")}`,
271
+ ` ${c.cyan("3) 승인")} 파일을 고칠 땐 ${c.bold("바뀔 내용(diff)")} 을 먼저 보여줘요 → ${c.green("y")} 누르면 적용`,
272
+ "",
273
+ c.bold("💡 알아두면 좋은 명령"),
274
+ ` ${c.cyan("/skills")} 쓸 수 있는 명령 목록(민원분류·요약 등)`,
275
+ ` ${c.cyan("/workspace")} 작업할 폴더 보기/바꾸기 (${c.dim("/workspace . = 지금 폴더")})`,
276
+ ` ${c.cyan("/teach")} AI 내부 동작 펼쳐보기 켜기/끄기`,
277
+ ` ${c.cyan("/about")} 이 도구 정보 · ${c.cyan("/help")} 전체 도움말 · ${c.cyan("/quit")} 종료`,
278
+ "",
279
+ c.bold("🇰🇷 공공 업무 예시 (바로 써보기)"),
280
+ ` ${c.green("/minwon")} (민원 내용 붙여넣기) → 분류·담당부서·처리방향`,
281
+ ` ${c.green("/gongmun")} 도서관 행사 안내 → 공문 기안 초안`,
282
+ ` ${c.green("/privacy")} report.txt → 개인정보 점검`,
283
+ ` ${c.green("/hwpx")} 보고서.hwpx → 한컴 문서 요약`,
284
+ "",
285
+ c.dim("막히면 언제든 /help 를 입력하세요. 즐겁게 써보세요! 😊"),
286
+ ],
287
+ { title: "📖 빠른 시작 가이드 (/guide)", color: "cyan" }
288
+ )
289
+ );
290
+ }
291
+
292
+ // 인터랙티브 튜토리얼 — 엔터로 한 페이지씩 진행(첫 실행 시 자동 제안).
293
+ async function runTutorial(ask) {
294
+ const pages = [
295
+ [
296
+ "👋 환영합니다! CDSA Harness 가 처음이라면 이 튜토리얼이 딱이에요.",
297
+ "",
298
+ "이 도구는 'AI 에이전트'예요. 일을 시키면 AI 가 스스로 파일을 보고·읽고·고치며 일합니다.",
299
+ "그 모든 과정을 화면에 단계별로 보여줘서, 보면서 배울 수 있어요.",
300
+ ],
301
+ [
302
+ c.bold("1단계. AI 연결"),
303
+ "",
304
+ `${c.cyan("/setup")} 을 입력하면 OpenAI·Claude·OpenRouter 중 고르고 API 키를 넣어요.`,
305
+ "키가 없어도 괜찮아요 — 자동 '연습(mock)' 모드로 흐름을 그대로 체험할 수 있어요.",
306
+ ],
307
+ [
308
+ c.bold("2단계. 그냥 시키기"),
309
+ "",
310
+ "하고 싶은 일을 한국어로 입력하면 됩니다. 예를 들면:",
311
+ ` ${c.green("notes.txt 에 오늘 할 일 3개 추가해줘")}`,
312
+ ` ${c.green("이 폴더에 어떤 파일이 있는지 알려줘")}`,
313
+ ],
314
+ [
315
+ c.bold("3단계. 승인 [y/N]"),
316
+ "",
317
+ "AI 가 파일을 고치려 하면, 바뀔 내용(diff)을 먼저 보여줍니다.",
318
+ `${c.green("y")} 를 누르면 적용, ${c.red("n")} 이면 취소. → 위험한 일은 항상 사람이 확인해요(안전장치).`,
319
+ ],
320
+ [
321
+ c.bold("알아두면 좋은 명령"),
322
+ ` ${c.cyan("/guide")} 빠른 요약 · ${c.cyan("/skills")} 명령 목록 · ${c.cyan("/workspace")} 작업 폴더`,
323
+ ` ${c.cyan("/teach")} 내부 펼쳐보기 · ${c.cyan("/help")} 전체 · ${c.cyan("/quit")} 종료`,
324
+ "",
325
+ c.bold("🇰🇷 공공 업무도 바로"),
326
+ ` ${c.green("/minwon")} 민원분류 · ${c.green("/gongmun")} 공문 · ${c.green("/privacy")} 개인정보점검 · ${c.green("/hwpx")} 한컴요약`,
327
+ "",
328
+ c.dim("이제 끝! 직접 한번 시켜보세요. 막히면 /guide 또는 /help 를 입력하면 돼요 😊"),
329
+ ],
330
+ ];
331
+ for (let i = 0; i < pages.length; i++) {
332
+ console.log(panel(pages[i], { title: `📊 튜토리얼 (${i + 1}/${pages.length})`, color: "cyan" }));
333
+ if (i < pages.length - 1) {
334
+ const a = await ask(c.dim(" [엔터] 다음 · [q] 그만 "));
335
+ if (a === null || a.trim().toLowerCase() === "q") {
336
+ console.log(c.dim("튜토리얼을 건너뜁니다. 언제든 /tutorial 로 다시 볼 수 있어요."));
337
+ return;
338
+ }
339
+ }
340
+ }
341
+ }
342
+
234
343
  function printHelp() {
235
344
  console.log(
236
345
  panel(
@@ -239,6 +348,9 @@ function printHelp() {
239
348
  `시키고 싶은 일을 한국어로 입력하세요. 예) ${c.cyan("notes.txt 맨 아래에 할 일 3개 추가해줘")}`,
240
349
  "",
241
350
  c.bold("슬래시 명령"),
351
+ ` ${c.cyan("/guide")} 처음 사용자용 빠른 시작 안내`,
352
+ ` ${c.cyan("/tutorial")} 단계별 인터랙티브 튜토리얼`,
353
+ ` ${c.cyan("/color")} 색상 켜기/끄기(흑백)`,
242
354
  ` ${c.cyan("/about")} 이 도구 정보(made by CDSA)`,
243
355
  ` ${c.cyan("/setup")} 제공자·API 키·모델 연결(대화형)`,
244
356
  ` ${c.cyan("/provider")} <openai|anthropic|openrouter|mock> 제공자 변경`,
@@ -246,6 +358,7 @@ function printHelp() {
246
358
  ` ${c.cyan("/teach")} 교육 모드 켜기/끄기(내부 과정 펼쳐보기)`,
247
359
  ` ${c.cyan("/stream")} 실시간 스트리밍 출력 켜기/끄기`,
248
360
  ` ${c.cyan("/context")} 지금 모델에 보내는 컨텍스트 들여다보기`,
361
+ ` ${c.cyan("/workspace")} <폴더> 작업 폴더 보기/변경 ('.' = 현재 폴더)`,
249
362
  ` ${c.cyan("/skills")} 스킬 목록(.cdsa/skills 의 /명령들)`,
250
363
  ` ${c.cyan("/plugins")} 플러그인 목록(파일·npm 추가 도구)`,
251
364
  ` ${c.cyan("/mcp")} 연결된 MCP 서버/도구(다른 에이전트와 공용)`,
@@ -337,6 +450,7 @@ function parseArgs(argv) {
337
450
  else if (a === "--setup") out.setup = true;
338
451
  else if (a === "--no-teach") out.noTeach = true;
339
452
  else if (a === "--no-stream") out.noStream = true;
453
+ else if (a === "--no-color") out.noColor = true;
340
454
  else if (a === "--provider") out.provider = argv[++i];
341
455
  else if (a === "--model") out.model = argv[++i];
342
456
  else if (a === "--workspace") out.workspace = argv[++i];
@@ -358,6 +472,7 @@ export async function main(argv = []) {
358
472
  " --setup 대화형 연결 설정 실행\n" +
359
473
  " --no-teach 교육 모드 끄고 간결하게\n" +
360
474
  " --no-stream 실시간 스트리밍 끄기\n" +
475
+ " --no-color 색상 끄기(흑백)\n" +
361
476
  " --auto 승인 자동(approval_mode=auto)\n" +
362
477
  " -h, --help 도움말\n\n" +
363
478
  "API 키는 환경변수로도 인식됩니다: OPENAI_API_KEY / ANTHROPIC_API_KEY / OPENROUTER_API_KEY\n"
@@ -390,6 +505,8 @@ export async function main(argv = []) {
390
505
  if (args.auto) cfg.approval_mode = "auto";
391
506
  if (args.noTeach) cfg.teach_mode = false;
392
507
  if (args.noStream) cfg.stream = false;
508
+ if (args.noColor) cfg.no_color = true;
509
+ if (cfg.no_color) setColor(false); // 색상 끄기(흑백)
393
510
 
394
511
  const rl = readline.createInterface({ input: stdin, output: stdout });
395
512
  let session = null;
@@ -422,30 +539,13 @@ export async function main(argv = []) {
422
539
 
423
540
  printIntro(cfg);
424
541
 
425
- // 플러그인(추가 도구)·스킬(프롬프트 템플릿) 불러온다:
426
- // ① 파일: .cdsa/plugins · .cdsa/skills (작업폴더/홈)
427
- // ② npm 패키지: cdsa-harness-plugin-* 자동 발견 + config.plugins 강제 로드
428
- const filePlugins = await loadPlugins(cfg.workspacePath());
429
- const npm = await discoverNpmExtensions(process.cwd(), cfg.plugins || []);
430
- // ③ MCP 서버(다른 에이전트와 공용 표준)의 도구도 플러그인처럼 등록
542
+ // MCP 서버(다른 에이전트와 공용 표준) 연결 — 1회
431
543
  if (cfg.mcpServers && Object.keys(cfg.mcpServers).length) {
432
544
  process.stdout.write(c.dim("MCP 서버 연결 중...\r"));
433
545
  mcp = await connectMcpServers(cfg.mcpServers);
434
546
  }
435
- const plugins = [
436
- ...filePlugins,
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);
547
+ // 플러그인·스킬·도구상자를 작업 폴더 기준으로 구성(작업 폴더 변경 시 재사용)
548
+ let { toolbox, skills } = await buildExtensions(cfg, mcp);
449
549
  if (toolbox.plugins.length || Object.keys(skills).length || toolbox.pluginErrors.length || mcp.servers.length) {
450
550
  const bits = [];
451
551
  if (toolbox.plugins.length) bits.push(c.green(`도구 +${toolbox.plugins.length}개`));
@@ -478,6 +578,19 @@ export async function main(argv = []) {
478
578
 
479
579
  const rule = () => console.log(c.grey("─".repeat(Math.min(80, stdout.columns || 80))));
480
580
 
581
+ // 첫 실행이면 튜토리얼을 권한다(한 번만 — ~/.cdsa_harness/.welcomed 표시).
582
+ const markerPath = path.join(configDir(), ".welcomed");
583
+ if (stdin.isTTY && !fs.existsSync(markerPath)) {
584
+ const a = await ask(c.cyan("처음 오셨네요! 👋 짧은 튜토리얼을 볼까요? [Y/n] "));
585
+ if (a !== null && ["", "y", "yes"].includes(a.trim().toLowerCase())) await runTutorial(ask);
586
+ try {
587
+ fs.mkdirSync(configDir(), { recursive: true });
588
+ fs.writeFileSync(markerPath, new Date().toISOString());
589
+ } catch {
590
+ /* 표시 실패는 무시 */
591
+ }
592
+ }
593
+
481
594
  while (true) {
482
595
  const raw = await ask(c.bold(c.cyan("› ")));
483
596
  if (raw === null) break; // Ctrl+D / Ctrl+C / 스트림 종료
@@ -487,6 +600,14 @@ export async function main(argv = []) {
487
600
 
488
601
  if (["/quit", "/exit", "quit", "exit", ":q"].includes(low)) break;
489
602
  if (low === "/help") { printHelp(); continue; }
603
+ if (low === "/guide" || low === "/start") { printGuide(); continue; }
604
+ if (low === "/tutorial") { await runTutorial(ask); continue; }
605
+ if (low === "/color") {
606
+ cfg.no_color = !cfg.no_color;
607
+ setColor(!cfg.no_color);
608
+ console.log(cfg.no_color ? "색상 끔(흑백)." : c.green("색상 켬."));
609
+ continue;
610
+ }
490
611
  if (low === "/about") {
491
612
  console.log(panel([
492
613
  c.bold("CDSA Harness") + c.grey(` v${VERSION}`),
@@ -501,6 +622,25 @@ export async function main(argv = []) {
501
622
  }
502
623
  if (low === "/reset") { loop.reset(); console.log(c.green("컨텍스트를 초기화했습니다.")); continue; }
503
624
  if (low === "/config") { printIntro(cfg); console.log(c.dim(`config.json: ${configPath()}`)); continue; }
625
+ if (low.startsWith("/workspace") || low.startsWith("/cd")) {
626
+ const arg = user.split(/\s+/).slice(1).join(" ").trim();
627
+ if (!arg) {
628
+ console.log(c.dim(`현재 작업 폴더: ${cfg.workspacePath()}`));
629
+ console.log(c.dim("변경: /workspace <폴더경로> (현재 폴더 그대로: /workspace .)"));
630
+ continue;
631
+ }
632
+ cfg.workspace = arg;
633
+ const rebuilt = await buildExtensions(cfg, mcp);
634
+ toolbox = rebuilt.toolbox;
635
+ skills = rebuilt.skills;
636
+ loop.toolbox = toolbox;
637
+ loop.reset();
638
+ console.log(
639
+ c.green(`작업 폴더 변경 → ${cfg.workspacePath()}`) +
640
+ c.dim(` (도구 ${toolbox.plugins.length} · 스킬 ${Object.keys(skills).length})`)
641
+ );
642
+ continue;
643
+ }
504
644
  if (low === "/sessions") { console.log(c.dim(`세션 로그: ${sessionsDir()}`)); continue; }
505
645
  if (low === "/teach") {
506
646
  cfg.teach_mode = !cfg.teach_mode;
package/src/config.js CHANGED
@@ -42,6 +42,7 @@ const DEFAULTS = {
42
42
  stream: true, // 모델 응답을 실시간(토큰 단위)으로 출력
43
43
  import_foreign_skills: true, // .claude/commands 등 외부 포맷 스킬도 읽기(프로젝트+전역)
44
44
  skill_dirs: [], // 스킬을 추가로 읽어올 폴더(절대/상대 경로)
45
+ no_color: false, // 색상 끄기(흑백)
45
46
  plugins: [], // 추가로 불러올 npm 플러그인 패키지 이름(이름 규칙과 무관하게 강제 로드)
46
47
  mcpServers: {}, // MCP 서버 설정 (Claude Code/Cursor 와 동일한 형식)
47
48
  };
package/src/ui.js CHANGED
@@ -1,10 +1,17 @@
1
1
  // 터미널 출력 헬퍼: ANSI 색, 박스 패널, diff 렌더. (외부 의존성 없음)
2
2
 
3
3
  const ESC = "\x1b[";
4
- const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
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) => (useColor ? `${ESC}${open}m${s}${ESC}${close}m` : String(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 (!useColor) return String(s);
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));