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 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.1",
3
+ "version": "0.14.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.1";
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("/teach")} 교육모드 · ${c.cyan("/context")} 컨텍스트 · ${c.cyan("/help")} 도움말 · ${c.cyan("/quit")} 종료\n`
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
- // ① 파일: .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 서버(다른 에이전트와 공용 표준)의 도구도 플러그인처럼 등록
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
- 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);
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
- 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));