docuking-mcp 2.1.2 → 2.2.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.
Files changed (2) hide show
  1. package/index.js +219 -80
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -40,6 +40,29 @@ import crypto from 'crypto';
40
40
  // 환경변수에서 API 엔드포인트 설정 (키는 로컬 config에서 읽음)
41
41
  const API_ENDPOINT = process.env.DOCUKING_API_ENDPOINT || 'https://docuking.ai/api';
42
42
 
43
+ /**
44
+ * API 키에서 협업자 폴더명 추출
45
+ * API 키 형식: sk_프로젝트8자_cw_폴더명_랜덤48자(hex)
46
+ * hex는 0-9, a-f만 포함하므로 언더스코어 없음
47
+ * 따라서 _cw_ 이후 마지막 _ 전까지가 폴더명
48
+ */
49
+ function parseCoworkerFromApiKey(apiKey) {
50
+ if (!apiKey) return { isCoworker: false, coworkerFolder: null };
51
+
52
+ const cwIndex = apiKey.indexOf('_cw_');
53
+ if (cwIndex === -1) return { isCoworker: false, coworkerFolder: null };
54
+
55
+ const afterCw = apiKey.substring(cwIndex + 4);
56
+ const lastUnderscore = afterCw.lastIndexOf('_');
57
+ if (lastUnderscore === -1) return { isCoworker: false, coworkerFolder: null };
58
+
59
+ const folderName = afterCw.substring(0, lastUnderscore);
60
+ return {
61
+ isCoworker: !!folderName,
62
+ coworkerFolder: folderName || null,
63
+ };
64
+ }
65
+
43
66
  // 파일 크기 제한 (150MB) - Base64 인코딩 시 약 200MB, 서버 제한 200MB와 일치
44
67
  const MAX_FILE_SIZE_MB = 150;
45
68
  const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;
@@ -93,72 +116,117 @@ function saveLocalConfig(localPath, config) {
93
116
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
94
117
  }
95
118
 
119
+ /**
120
+ * 협업자 여부 및 zz_ai 폴더 기본 경로 반환
121
+ * - 오너: localPath (루트에 zz_ai_* 폴더)
122
+ * - 협업자: localPath/yy_Coworker_{폴더명} (협업자 폴더 안에 zz_ai_* 폴더)
123
+ */
124
+ function getAiBasePath(localPath) {
125
+ const config = getLocalConfig(localPath);
126
+ if (!config || !config.apiKey) {
127
+ return { isCoworker: false, basePath: localPath, coworkerFolder: null };
128
+ }
129
+
130
+ const apiKey = config.apiKey;
131
+ const { isCoworker, coworkerFolder } = parseCoworkerFromApiKey(apiKey);
132
+
133
+ if (isCoworker && coworkerFolder) {
134
+ const coworkerFolderPath = path.join(localPath, `yy_Coworker_${coworkerFolder}`);
135
+ return { isCoworker: true, basePath: coworkerFolderPath, coworkerFolder };
136
+ }
137
+
138
+ return { isCoworker: false, basePath: localPath, coworkerFolder: null };
139
+ }
140
+
96
141
  /**
97
142
  * CLAUDE.md에 DocuKing MCP 작업 기록 규칙 추가
98
143
  * - 파일이 없으면 새로 생성
99
144
  * - 파일이 있으면 DocuKing 섹션이 없을 때만 추가
145
+ * - Talk → Todo → Plan → Done 흐름 포함
100
146
  */
101
147
  function updateClaudeMd(localPath) {
102
148
  const claudeMdPath = path.join(localPath, 'CLAUDE.md');
103
- const marker = '## DocuKing AI 작업 기록 (필수)';
149
+ const marker = '## DocuKing 연결됨';
150
+ const oldMarker = '## DocuKing AI 작업 기록 (필수)';
104
151
 
105
152
  const docukingSection = `
106
153
  ---
107
154
 
108
155
  ${marker}
109
156
 
110
- **모든 작업은 MCP 도구로 기록한다. 예외 없음.**
111
-
112
- ### 작업 시작 시 → \`docuking_plan\`
113
- \`\`\`
114
- docuking_plan({
115
- localPath: "${localPath.replace(/\\/g, '\\\\')}",
116
- title: "작업 제목",
117
- goal: "작업 목표",
118
- steps: [
119
- { name: "단계1", status: "pending" },
120
- { name: "단계2", status: "pending" }
121
- ]
122
- })
123
- \`\`\`
124
-
125
- ### 단계 완료 시 → \`docuking_plan\` (업데이트)
126
- \`\`\`
127
- docuking_plan({
128
- localPath: "${localPath.replace(/\\/g, '\\\\')}",
129
- planId: "기존 planId",
130
- title: "작업 제목",
131
- steps: [
132
- { name: "단계1", status: "done", result: "결과 요약" },
133
- { name: "단계2", status: "in_progress" }
134
- ]
135
- })
136
- \`\`\`
137
-
138
- ### 작업 완료 → \`docuking_done\`
139
- \`\`\`
140
- docuking_done({
141
- localPath: "${localPath.replace(/\\/g, '\\\\')}",
142
- planId: "planId",
143
- summary: "작업 완료 요약",
144
- artifacts: ["변경된/파일/경로.ts"]
145
- })
146
- \`\`\`
147
-
148
- ### 절대 규칙
149
- - **작업 시작 반드시 \`docuking_plan\` 호출**
150
- - **작업 완료 반드시 \`docuking_done\` 호출**
151
- - 결과는 \`zz_ai_3_Plan/\`에 자동 저장됨 (로컬 전용)
157
+ ### MCP 도구
158
+ | 도구 | 용도 |
159
+ |------|------|
160
+ | docuking_push | 문서 업로드 ("올려줘") |
161
+ | docuking_pull | 문서 다운로드 ("가져와") |
162
+ | docuking_list | 서버 파일 목록 조회 |
163
+ | docuking_status | 동기화 상태 확인 |
164
+ | docuking_log | 커밋 히스토리 조회 |
165
+ | docuking_diff | 버전 간 차이 비교 |
166
+ | docuking_rollback | 특정 커밋으로 되돌림 |
167
+ | docuking_plan | 작업 시작 시 계획 기록 |
168
+ | docuking_done | 작업 완료 시 결과 기록 |
169
+ | docuking_todo | 킹투두 (프로젝트 할일) |
170
+ | docuking_talk | 대화록 저장 |
171
+
172
+ ### 작업 흐름 (필수)
173
+
174
+ **1단계: Talk 기록 (작업 원인/배경)**
175
+ - 사용자 요청의 배경, 논의 내용을 먼저 기록
176
+ - Plan을 세우기 전에 반드시 Talk로 " 이 작업을 하는지" 남김
177
+
178
+ **2단계: Todo 등록 (할일 등록)**
179
+ - 의미 있는 작업이면 킹투두에 등록
180
+ - 나중에 추적/공유할 가치가 있는 작업
181
+
182
+ **3단계: Plan 생성 → 작업 → Done 완료**
183
+ - 기존대로 plan → 작업 → done
184
+
185
+ ### Talk 자동 기록 (docuking_talk)
186
+
187
+ **다음 상황에서 AI가 자동으로 기록:**
188
+ - 설계 방향 결정 (A vs B 선택, "왜 이렇게?")
189
+ - 버그 원인 분석 및 해결책 논의
190
+ - 아키텍처/구조 변경 논의
191
+ - 사용자가 중요한 결정을 내릴 때
192
+ - Plan을 세우게 된 원인/배경 논의
193
+
194
+ ### Todo 자동 등록 (docuking_todo)
195
+
196
+ **다음 상황에서 AI가 자동으로 등록:**
197
+ - 사용자가 "나중에 해야 것" 언급 시
198
+ - 작업 발견한 추가 개선사항
199
+ - "TODO", "FIXME" 성격의 발견 사항
200
+ - 의미 있는 작업 시작 시 (추적용)
201
+
202
+ ### 규칙
203
+ 1. yy_All_Docu/ 폴더만 동기화 대상
204
+ 2. Push는 사용자 요청 시에만
205
+ 3. **Talk → Todo → Plan → Done** 순서 준수
206
+ 4. Plan 전에 반드시 Talk로 배경 기록
152
207
  `;
153
208
 
154
209
  try {
155
210
  if (fs.existsSync(claudeMdPath)) {
156
211
  // 파일이 있으면 DocuKing 섹션 존재 여부 확인
157
- const content = fs.readFileSync(claudeMdPath, 'utf-8');
212
+ let content = fs.readFileSync(claudeMdPath, 'utf-8');
213
+
214
+ // 새 마커가 이미 있으면 스킵
158
215
  if (content.includes(marker)) {
159
- // 이미 DocuKing 섹션이 있으면 스킵
160
216
  return;
161
217
  }
218
+
219
+ // 구버전 마커가 있으면 교체
220
+ if (content.includes(oldMarker)) {
221
+ // 구버전 섹션 찾아서 교체 (--- 부터 끝까지 또는 다음 --- 전까지)
222
+ const oldSectionStart = content.lastIndexOf('---\n\n' + oldMarker);
223
+ if (oldSectionStart !== -1) {
224
+ content = content.substring(0, oldSectionStart) + docukingSection;
225
+ fs.writeFileSync(claudeMdPath, content, 'utf-8');
226
+ return;
227
+ }
228
+ }
229
+
162
230
  // 섹션이 없으면 끝에 추가
163
231
  fs.appendFileSync(claudeMdPath, docukingSection, 'utf-8');
164
232
  } else {
@@ -1148,12 +1216,8 @@ docuking_init 호출 시 apiKey 파라미터를 포함해주세요.`,
1148
1216
  };
1149
1217
  }
1150
1218
 
1151
- // Co-worker 권한은 API Key 형식에서 판단 (sk_xxx_cw_폴더명_)
1152
- // 폴더명이 비어있는 경우도 처리 (sk_xxx_cw__)
1153
- const coworkerMatch = apiKey.match(/^sk_[a-f0-9]+_cw_([^_]*)_/);
1154
- const isCoworker = !!coworkerMatch;
1155
- // 코워커 폴더명이 비어있으면 'default'로 기본값 설정
1156
- const coworkerFolder = coworkerMatch ? (coworkerMatch[1] || 'default') : null;
1219
+ // Co-worker 권한은 API Key 형식에서 판단
1220
+ const { isCoworker, coworkerFolder } = parseCoworkerFromApiKey(apiKey);
1157
1221
 
1158
1222
  // .docuking/config.json에 설정 저장
1159
1223
  saveLocalConfig(localPath, {
@@ -1174,18 +1238,12 @@ docuking_init 호출 시 apiKey 파라미터를 포함해주세요.`,
1174
1238
  fs.mkdirSync(mainFolderPath, { recursive: true });
1175
1239
  }
1176
1240
 
1177
- // zz_ai_* 폴더도 함께 생성 (로컬 전용)
1178
- const aiFolders = ['zz_ai_1_Talk', 'zz_ai_2_Todo', 'zz_ai_3_Plan'];
1179
- for (const folder of aiFolders) {
1180
- const folderPath = path.join(localPath, folder);
1181
- if (!fs.existsSync(folderPath)) {
1182
- fs.mkdirSync(folderPath, { recursive: true });
1183
- }
1184
- }
1185
-
1186
1241
  let coworkerFolderName = null;
1187
1242
  let coworkerFolderPath = null;
1188
1243
 
1244
+ // zz_ai_* 폴더 목록
1245
+ const aiFolders = ['zz_ai_1_Talk', 'zz_ai_2_Todo', 'zz_ai_3_Plan'];
1246
+
1189
1247
  if (isCoworker) {
1190
1248
  // 협업자: yy_Coworker_{폴더명}/ 폴더를 yy_All_Docu/ 밖에 별도 생성
1191
1249
  coworkerFolderName = `yy_Coworker_${coworkerFolder}`;
@@ -1193,6 +1251,35 @@ docuking_init 호출 시 apiKey 파라미터를 포함해주세요.`,
1193
1251
  if (!fs.existsSync(coworkerFolderPath)) {
1194
1252
  fs.mkdirSync(coworkerFolderPath, { recursive: true });
1195
1253
  }
1254
+ // 협업자 폴더 안에 _Private/ 생성
1255
+ const coworkerPrivatePath = path.join(coworkerFolderPath, '_Private');
1256
+ if (!fs.existsSync(coworkerPrivatePath)) {
1257
+ fs.mkdirSync(coworkerPrivatePath, { recursive: true });
1258
+ }
1259
+ // 협업자 폴더 안에 zz_ai_* 폴더 생성
1260
+ for (const folder of aiFolders) {
1261
+ const folderPath = path.join(coworkerFolderPath, folder);
1262
+ if (!fs.existsSync(folderPath)) {
1263
+ fs.mkdirSync(folderPath, { recursive: true });
1264
+ }
1265
+ }
1266
+ } else {
1267
+ // 오너: yy_All_Docu/ 안에 _Infra_Config/와 _Private/ 생성
1268
+ const infraConfigPath = path.join(mainFolderPath, '_Infra_Config');
1269
+ if (!fs.existsSync(infraConfigPath)) {
1270
+ fs.mkdirSync(infraConfigPath, { recursive: true });
1271
+ }
1272
+ const ownerPrivatePath = path.join(mainFolderPath, '_Private');
1273
+ if (!fs.existsSync(ownerPrivatePath)) {
1274
+ fs.mkdirSync(ownerPrivatePath, { recursive: true });
1275
+ }
1276
+ // 오너: 루트에 zz_ai_* 폴더 생성
1277
+ for (const folder of aiFolders) {
1278
+ const folderPath = path.join(localPath, folder);
1279
+ if (!fs.existsSync(folderPath)) {
1280
+ fs.mkdirSync(folderPath, { recursive: true });
1281
+ }
1282
+ }
1196
1283
  }
1197
1284
 
1198
1285
  // 연결 완료 안내 (오너/코워커에 따라 다른 메시지)
@@ -1297,12 +1384,8 @@ Git처럼 무엇을 변경했는지 명확히 작성해주세요.
1297
1384
  const projectId = projectInfo.projectId;
1298
1385
  const projectName = projectInfo.projectName;
1299
1386
 
1300
- // Co-worker 권한은 API Key 형식에서 판단 (sk_xxx_cw_폴더명_)
1301
- // 폴더명이 비어있는 경우도 처리 (sk_xxx_cw__)
1302
- const coworkerMatch = apiKey.match(/^sk_[a-f0-9]+_cw_([^_]*)_/);
1303
- const isCoworker = !!coworkerMatch;
1304
- // 코워커 폴더명이 비어있으면 'default'로 기본값 설정
1305
- const coworkerFolder = coworkerMatch ? (coworkerMatch[1] || 'default') : null;
1387
+ // Co-worker 권한은 API Key 형식에서 판단
1388
+ const { isCoworker, coworkerFolder } = parseCoworkerFromApiKey(apiKey);
1306
1389
  const coworkerFolderName = isCoworker ? `yy_Coworker_${coworkerFolder}` : null;
1307
1390
 
1308
1391
  // 작업 폴더 결정: 오너는 yy_All_Docu/, 협업자는 yy_Coworker_{폴더명}/ (별도 폴더)
@@ -1388,6 +1471,54 @@ docuking_init을 먼저 실행하세요.`,
1388
1471
  console.error(`[DocuKing] frontend/.env.local 백업 실패: ${err.message}`);
1389
1472
  }
1390
1473
  }
1474
+
1475
+ // package.json 파일 백업 (루트)
1476
+ const packageJsonPath = path.join(localPath, 'package.json');
1477
+ if (fs.existsSync(packageJsonPath)) {
1478
+ if (!fs.existsSync(infraConfigPath)) {
1479
+ fs.mkdirSync(infraConfigPath, { recursive: true });
1480
+ }
1481
+
1482
+ const packageJsonBackupPath = path.join(infraConfigPath, 'package.json');
1483
+ try {
1484
+ fs.copyFileSync(packageJsonPath, packageJsonBackupPath);
1485
+ console.error(`[DocuKing] package.json → _Infra_Config/package.json 자동 백업 완료`);
1486
+ } catch (err) {
1487
+ console.error(`[DocuKing] package.json 백업 실패: ${err.message}`);
1488
+ }
1489
+ }
1490
+
1491
+ // backend/package.json 파일도 백업
1492
+ const backendPackageJsonPath = path.join(localPath, 'backend', 'package.json');
1493
+ if (fs.existsSync(backendPackageJsonPath)) {
1494
+ if (!fs.existsSync(infraConfigPath)) {
1495
+ fs.mkdirSync(infraConfigPath, { recursive: true });
1496
+ }
1497
+
1498
+ const backendPackageJsonBackupPath = path.join(infraConfigPath, 'backend.package.json');
1499
+ try {
1500
+ fs.copyFileSync(backendPackageJsonPath, backendPackageJsonBackupPath);
1501
+ console.error(`[DocuKing] backend/package.json → _Infra_Config/backend.package.json 자동 백업 완료`);
1502
+ } catch (err) {
1503
+ console.error(`[DocuKing] backend/package.json 백업 실패: ${err.message}`);
1504
+ }
1505
+ }
1506
+
1507
+ // frontend/package.json 파일도 백업
1508
+ const frontendPackageJsonPath = path.join(localPath, 'frontend', 'package.json');
1509
+ if (fs.existsSync(frontendPackageJsonPath)) {
1510
+ if (!fs.existsSync(infraConfigPath)) {
1511
+ fs.mkdirSync(infraConfigPath, { recursive: true });
1512
+ }
1513
+
1514
+ const frontendPackageJsonBackupPath = path.join(infraConfigPath, 'frontend.package.json');
1515
+ try {
1516
+ fs.copyFileSync(frontendPackageJsonPath, frontendPackageJsonBackupPath);
1517
+ console.error(`[DocuKing] frontend/package.json → _Infra_Config/frontend.package.json 자동 백업 완료`);
1518
+ } catch (err) {
1519
+ console.error(`[DocuKing] frontend/package.json 백업 실패: ${err.message}`);
1520
+ }
1521
+ }
1391
1522
  }
1392
1523
 
1393
1524
  // 파일 목록 수집
@@ -2176,12 +2307,8 @@ async function handleStatus(args) {
2176
2307
  const projectId = projectInfo.projectId;
2177
2308
  const projectName = projectInfo.projectName;
2178
2309
 
2179
- // Co-worker 권한은 API Key 형식에서 판단 (sk_xxx_cw_폴더명_)
2180
- // 폴더명이 비어있는 경우도 처리 (sk_xxx_cw__)
2181
- const coworkerMatch = apiKey.match(/^sk_[a-f0-9]+_cw_([^_]*)_/);
2182
- const isCoworker = !!coworkerMatch;
2183
- // 코워커 폴더명이 비어있으면 'default'로 기본값 설정
2184
- const coworkerFolder = coworkerMatch ? (coworkerMatch[1] || 'default') : null;
2310
+ // Co-worker 권한은 API Key 형식에서 판단
2311
+ const { isCoworker, coworkerFolder } = parseCoworkerFromApiKey(apiKey);
2185
2312
  const coworkerFolderName = isCoworker ? `yy_Coworker_${coworkerFolder}` : null;
2186
2313
 
2187
2314
  // 권한 정보 구성
@@ -2381,8 +2508,11 @@ function generatePlanId() {
2381
2508
  async function handleTodo(args) {
2382
2509
  const { localPath, action, todo, todoId } = args;
2383
2510
 
2384
- // zz_ai_2_Todo 폴더 경로 (로컬 전용, 킹투두)
2385
- const todoBasePath = path.join(localPath, 'zz_ai_2_Todo');
2511
+ // 협업자 여부에 따라 zz_ai 경로 결정
2512
+ // - 오너: localPath/zz_ai_2_Todo/
2513
+ // - 협업자: localPath/yy_Coworker_{폴더명}/zz_ai_2_Todo/
2514
+ const { basePath } = getAiBasePath(localPath);
2515
+ const todoBasePath = path.join(basePath, 'zz_ai_2_Todo');
2386
2516
  const todoFilePath = path.join(todoBasePath, 'z_King_Todo.md');
2387
2517
 
2388
2518
  // 폴더 생성
@@ -2572,8 +2702,11 @@ ${listText}
2572
2702
  async function handleTalk(args) {
2573
2703
  const { localPath, title, content, tags = [] } = args;
2574
2704
 
2575
- // zz_ai_1_Talk 폴더 경로 (로컬 전용, 킹톡)
2576
- const talkBasePath = path.join(localPath, 'zz_ai_1_Talk');
2705
+ // 협업자 여부에 따라 zz_ai 경로 결정
2706
+ // - 오너: localPath/zz_ai_1_Talk/
2707
+ // - 협업자: localPath/yy_Coworker_{폴더명}/zz_ai_1_Talk/
2708
+ const { basePath } = getAiBasePath(localPath);
2709
+ const talkBasePath = path.join(basePath, 'zz_ai_1_Talk');
2577
2710
 
2578
2711
  // 날짜 기반 파일명 생성 (T_ 접두사)
2579
2712
  const { fileName, timestamp } = generateDateFileName(title, 'T');
@@ -2625,8 +2758,11 @@ ${content}
2625
2758
  async function handlePlan(args) {
2626
2759
  const { localPath, planId, title, goal, steps = [], notes } = args;
2627
2760
 
2628
- // zz_ai_3_Plan 폴더 경로 (로컬 전용, 킹플랜)
2629
- const planBasePath = path.join(localPath, 'zz_ai_3_Plan');
2761
+ // 협업자 여부에 따라 zz_ai 경로 결정
2762
+ // - 오너: localPath/zz_ai_3_Plan/
2763
+ // - 협업자: localPath/yy_Coworker_{폴더명}/zz_ai_3_Plan/
2764
+ const { basePath } = getAiBasePath(localPath);
2765
+ const planBasePath = path.join(basePath, 'zz_ai_3_Plan');
2630
2766
 
2631
2767
  // 기존 계획 업데이트 또는 새 계획 생성
2632
2768
  let targetPlanId = planId;
@@ -2775,8 +2911,11 @@ function findPlanFiles(basePath, planId) {
2775
2911
  async function handleDone(args) {
2776
2912
  const { localPath, planId, summary, artifacts = [] } = args;
2777
2913
 
2778
- // zz_ai_3_Plan 폴더 경로 (로컬 전용, 킹플랜)
2779
- const planBasePath = path.join(localPath, 'zz_ai_3_Plan');
2914
+ // 협업자 여부에 따라 zz_ai 경로 결정
2915
+ // - 오너: localPath/zz_ai_3_Plan/
2916
+ // - 협업자: localPath/yy_Coworker_{폴더명}/zz_ai_3_Plan/
2917
+ const { basePath } = getAiBasePath(localPath);
2918
+ const planBasePath = path.join(basePath, 'zz_ai_3_Plan');
2780
2919
 
2781
2920
  // 계획 파일 찾기
2782
2921
  const planFiles = findPlanFiles(planBasePath, planId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docuking-mcp",
3
- "version": "2.1.2",
3
+ "version": "2.2.0",
4
4
  "description": "DocuKing MCP Server - AI 시대의 문서 협업 플랫폼",
5
5
  "type": "module",
6
6
  "main": "index.js",