docuking-mcp 1.8.0 → 1.9.1

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 +201 -43
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -508,7 +508,26 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
508
508
  },
509
509
  {
510
510
  name: 'docuking_todo',
511
- description: '할일을 z_DocuKing/z_Todo/z_Todo.md 파일에 추가하거나 완료 표시합니다. 단일 파일에 계속 누적되어 전체 작업 히스토리가 됩니다. 논의 후 할일이 생기면 바로 등록하세요.',
511
+ description: `킹투두(King Todo) - 프로젝트 공식 할일을 z_DocuKing/z_King_Todo/z_King_Todo.md 관리합니다.
512
+
513
+ **AI 내장 TodoWrite와 다름!** 킹투두는 웹에 동기화되고 팀과 공유됩니다.
514
+
515
+ **작성 형식:**
516
+ \`\`\`
517
+ 1. ✅ **[태그] 키워드** 12.30/12.30
518
+ 설명 (7칸 들여쓰기)
519
+
520
+ 2. ⚙️ **[태그] 키워드** 12.30
521
+ 설명
522
+ \`\`\`
523
+
524
+ **형식 규칙:**
525
+ - ✅ 완료 / ⚙️ 진행중
526
+ - **[태그] 키워드** 볼드 처리
527
+ - 날짜 1개 = 등록일(진행중), 2개 = 등록일/완료일(완료)
528
+ - 설명은 7칸 들여쓰기 ([ 기호 아래 정렬)
529
+
530
+ **사용자에게 "킹투두에 등록했습니다" 형식으로 보고하세요.**`,
512
531
  inputSchema: {
513
532
  type: 'object',
514
533
  properties: {
@@ -523,7 +542,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
523
542
  },
524
543
  todo: {
525
544
  type: 'string',
526
- description: '할일 내용 (add 시 필수)',
545
+ description: '할일 내용 (add 시 필수). 형식: "[태그] 키워드 - 설명" 예: "[보안] API 인증 강화 - JWT 토큰 검증"',
527
546
  },
528
547
  todoId: {
529
548
  type: 'number',
@@ -1318,11 +1337,13 @@ docuking_init을 먼저 실행하세요.`,
1318
1337
  console.error('[DocuKing] Sync 시작 알림 실패:', e.message);
1319
1338
  }
1320
1339
 
1321
- // 서버에서 파일 해시 조회 (변경 감지용)
1322
- let serverFileHashes = {};
1340
+ // 서버에서 파일 해시 조회 (Git 스타일 동기화용)
1341
+ let serverPathToHash = {};
1342
+ let serverHashToPath = {};
1343
+ let serverAllPaths = [];
1323
1344
  try {
1324
1345
  const hashResponse = await fetch(
1325
- `${API_ENDPOINT}/files/hashes?projectId=${projectId}`,
1346
+ `${API_ENDPOINT}/files/hashes-for-sync?projectId=${projectId}`,
1326
1347
  {
1327
1348
  headers: {
1328
1349
  'Authorization': `Bearer ${apiKey}`,
@@ -1331,16 +1352,23 @@ docuking_init을 먼저 실행하세요.`,
1331
1352
  );
1332
1353
  if (hashResponse.ok) {
1333
1354
  const hashData = await hashResponse.json();
1334
- serverFileHashes = hashData.hashes || {};
1355
+ serverPathToHash = hashData.pathToHash || {};
1356
+ serverHashToPath = hashData.hashToPath || {};
1357
+ serverAllPaths = hashData.allPaths || [];
1335
1358
  }
1336
1359
  } catch (e) {
1337
1360
  // 해시 조회 실패는 무시 (처음 Push하는 경우 등)
1338
1361
  console.error('[DocuKing] 파일 해시 조회 실패:', e.message);
1339
1362
  }
1340
1363
 
1364
+ // 처리된 로컬 파일 경로 (서버 삭제용)
1365
+ const processedLocalPaths = new Set();
1366
+ let moved = 0;
1367
+
1341
1368
  for (const file of filesToPush) {
1342
1369
  current++;
1343
1370
  const progress = `${current}/${total}`;
1371
+ processedLocalPaths.add(file.serverPath);
1344
1372
 
1345
1373
  try {
1346
1374
  // 파일 해시 계산 (변경 감지)
@@ -1360,15 +1388,50 @@ docuking_init을 먼저 실행하세요.`,
1360
1388
  fileHash = crypto.createHash('sha256').update(content, 'utf-8').digest('hex');
1361
1389
  }
1362
1390
 
1363
- // 변경 감지: 서버에 같은 해시가 있으면 스킵
1364
- if (serverFileHashes[file.path] === fileHash) {
1365
- const resultText = `${progress} ⊘ ${file.path} (변경 없음)`;
1391
+ // Git 스타일 동기화:
1392
+ // 1. 같은 경로 + 같은 해시 → 스킵 (변경 없음)
1393
+ if (serverPathToHash[file.serverPath] === fileHash) {
1394
+ const resultText = `${progress} ⊘ ${file.serverPath} (변경 없음)`;
1366
1395
  results.push(resultText);
1367
1396
  console.log(resultText);
1368
1397
  skipped++;
1369
1398
  continue;
1370
1399
  }
1371
1400
 
1401
+ // 2. 같은 해시가 다른 경로에 있음 → Move (경로 변경)
1402
+ const existingPath = serverHashToPath[fileHash];
1403
+ if (existingPath && existingPath !== file.serverPath) {
1404
+ try {
1405
+ const moveResponse = await fetch(`${API_ENDPOINT}/files/move`, {
1406
+ method: 'POST',
1407
+ headers: {
1408
+ 'Content-Type': 'application/json',
1409
+ 'Authorization': `Bearer ${apiKey}`,
1410
+ },
1411
+ body: JSON.stringify({
1412
+ projectId,
1413
+ oldPath: existingPath,
1414
+ newPath: file.serverPath,
1415
+ }),
1416
+ });
1417
+
1418
+ if (moveResponse.ok) {
1419
+ const resultText = `${progress} ↷ ${existingPath} → ${file.serverPath}`;
1420
+ results.push(resultText);
1421
+ console.log(resultText);
1422
+ moved++;
1423
+ // 이동된 파일의 해시 맵 업데이트
1424
+ delete serverHashToPath[fileHash];
1425
+ serverHashToPath[fileHash] = file.serverPath;
1426
+ continue;
1427
+ }
1428
+ } catch (e) {
1429
+ console.error(`[DocuKing] Move 실패: ${e.message}, Upload로 대체`);
1430
+ }
1431
+ }
1432
+
1433
+ // 3. 새 파일 또는 내용 변경 → Upload
1434
+
1372
1435
  // 재시도 로직 (최대 3회)
1373
1436
  let lastError = null;
1374
1437
  let success = false;
@@ -1467,6 +1530,41 @@ docuking_init을 먼저 실행하세요.`,
1467
1530
  }
1468
1531
  }
1469
1532
 
1533
+ // 4. 서버에만 있고 로컬에 없는 파일 삭제 (Git rm과 동일)
1534
+ let deleted = 0;
1535
+ const deletedFiles = [];
1536
+ if (serverAllPaths.length > 0 && !isCoworker) {
1537
+ // 코워커가 아닌 경우에만 삭제 수행 (오너 전용)
1538
+ const pathsToDelete = serverAllPaths.filter(p => !processedLocalPaths.has(p));
1539
+
1540
+ if (pathsToDelete.length > 0) {
1541
+ console.log(`[DocuKing] 서버에만 있는 파일 ${pathsToDelete.length}개 삭제 중...`);
1542
+
1543
+ try {
1544
+ const deleteResponse = await fetch(`${API_ENDPOINT}/files/delete-batch`, {
1545
+ method: 'POST',
1546
+ headers: {
1547
+ 'Content-Type': 'application/json',
1548
+ 'Authorization': `Bearer ${apiKey}`,
1549
+ },
1550
+ body: JSON.stringify({
1551
+ projectId,
1552
+ paths: pathsToDelete,
1553
+ }),
1554
+ });
1555
+
1556
+ if (deleteResponse.ok) {
1557
+ const deleteResult = await deleteResponse.json();
1558
+ deleted = deleteResult.deleted || 0;
1559
+ deletedFiles.push(...pathsToDelete.slice(0, deleted));
1560
+ console.log(`[DocuKing] ${deleted}개 파일 삭제 완료`);
1561
+ }
1562
+ } catch (e) {
1563
+ console.error('[DocuKing] 파일 삭제 실패:', e.message);
1564
+ }
1565
+ }
1566
+ }
1567
+
1470
1568
  // Sync 완료 알림
1471
1569
  try {
1472
1570
  await fetch(`${API_ENDPOINT}/projects/${projectId}/sync/complete`, {
@@ -1484,9 +1582,10 @@ docuking_init을 먼저 실행하세요.`,
1484
1582
  const failCount = results.filter(r => r.includes('✗')).length;
1485
1583
  const skippedCount = skipped; // 이미 계산된 스킵 개수 사용
1486
1584
  const excludedCount = excludedFiles.length;
1585
+ const movedCount = moved;
1487
1586
 
1488
1587
  // 요약 정보
1489
- let summary = `\n📦 커밋 메시지: "${message}"\n\n📊 처리 결과:\n - 총 파일: ${total}개\n - 업로드: ${successCount}개\n - 스킵 (변경 없음): ${skippedCount}개\n - 실패: ${failCount}개`;
1588
+ let summary = `\n📦 커밋 메시지: "${message}"\n\n📊 처리 결과:\n - 총 파일: ${total}개\n - 업로드: ${successCount}개\n - 이동: ${movedCount}개\n - 삭제: ${deleted}개\n - 스킵 (변경 없음): ${skippedCount}개\n - 실패: ${failCount}개`;
1490
1589
  if (excludedCount > 0) {
1491
1590
  summary += `\n - 제외 (압축/설치파일): ${excludedCount}개`;
1492
1591
  }
@@ -1500,6 +1599,20 @@ docuking_init을 먼저 실행하세요.`,
1500
1599
  resultText += `\n\n📤 업로드된 파일 (${successCount}개):\n${uploadedFiles.map(r => ` ${r.replace(/^\d+\/\d+ /, '')}`).join('\n')}`;
1501
1600
  }
1502
1601
 
1602
+ // 이동된 파일이 있으면 표시 (Git 스타일)
1603
+ if (movedCount > 0) {
1604
+ const movedFiles = results.filter(r => r.includes('↷'));
1605
+ resultText += `\n\n↷ 이동된 파일 (${movedCount}개):\n${movedFiles.map(r => ` ${r.replace(/^\d+\/\d+ /, '')}`).join('\n')}`;
1606
+ }
1607
+
1608
+ // 삭제된 파일이 있으면 표시 (Git 스타일)
1609
+ if (deleted > 0) {
1610
+ resultText += `\n\n🗑️ 삭제된 파일 (${deleted}개, 로컬에 없음):\n${deletedFiles.slice(0, 20).map(f => ` ✗ ${f}`).join('\n')}`;
1611
+ if (deleted > 20) {
1612
+ resultText += `\n ... 외 ${deleted - 20}개`;
1613
+ }
1614
+ }
1615
+
1503
1616
  // 스킵된 파일이 있으면 표시
1504
1617
  if (skippedCount > 0) {
1505
1618
  const skippedFiles = results.filter(r => r.includes('⊘'));
@@ -1829,10 +1942,15 @@ function getFileType(fileName) {
1829
1942
  }
1830
1943
 
1831
1944
  // 유틸: DocuKing 폴더 찾기 (docuking 포함, 대소문자 무관)
1945
+ // .docuking (설정 폴더)는 제외
1832
1946
  function findDocuKingFolder(projectPath) {
1833
1947
  try {
1834
1948
  const entries = fs.readdirSync(projectPath, { withFileTypes: true });
1835
1949
  for (const entry of entries) {
1950
+ // .docuking은 설정 폴더이므로 제외 (숨김 폴더)
1951
+ if (entry.name.startsWith('.')) {
1952
+ continue;
1953
+ }
1836
1954
  if (entry.isDirectory() && entry.name.toLowerCase().includes('docuking')) {
1837
1955
  return entry.name;
1838
1956
  }
@@ -2122,13 +2240,13 @@ function generatePlanId() {
2122
2240
  return id;
2123
2241
  }
2124
2242
 
2125
- // docuking_todo 구현 - 할일 추가/완료/조회 (단일 파일 누적)
2243
+ // docuking_todo 구현 - 킹투두 (단일 파일 누적)
2126
2244
  async function handleTodo(args) {
2127
2245
  const { localPath, action, todo, todoId } = args;
2128
2246
 
2129
- // z_Todo 폴더 경로
2130
- const todoBasePath = path.join(localPath, 'z_DocuKing', 'z_Todo');
2131
- const todoFilePath = path.join(todoBasePath, 'z_Todo.md');
2247
+ // z_King_Todo 폴더 경로
2248
+ const todoBasePath = path.join(localPath, 'z_DocuKing', 'z_King_Todo');
2249
+ const todoFilePath = path.join(todoBasePath, 'z_King_Todo.md');
2132
2250
 
2133
2251
  // 폴더 생성
2134
2252
  if (!fs.existsSync(todoBasePath)) {
@@ -2139,8 +2257,7 @@ async function handleTodo(args) {
2139
2257
  if (!fs.existsSync(todoFilePath)) {
2140
2258
  const header = `# TODO 목록
2141
2259
 
2142
- > 파일은 모든 할일을 누적 기록합니다.
2143
- > - [ ] 미완료 / - [x] 완료 (완료 시 날짜 추가)
2260
+ > 날짜 1개 = 등록일 (진행중) / 날짜 2개 = 등록일/완료일 (완료)
2144
2261
 
2145
2262
  ---
2146
2263
 
@@ -2161,8 +2278,9 @@ async function handleTodo(args) {
2161
2278
  }
2162
2279
 
2163
2280
  const now = new Date();
2164
- const dateStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
2165
- const timeStr = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
2281
+ const month = String(now.getMonth() + 1).padStart(2, '0');
2282
+ const day = String(now.getDate()).padStart(2, '0');
2283
+ const dateStr = `${month}.${day}`;
2166
2284
 
2167
2285
  if (action === 'add') {
2168
2286
  if (!todo) {
@@ -2172,7 +2290,39 @@ async function handleTodo(args) {
2172
2290
  }
2173
2291
 
2174
2292
  const newId = maxId + 1;
2175
- const newTodo = `${newId}. - [ ] ${todo} (${dateStr})\n`;
2293
+
2294
+ // todo에서 태그와 키워드, 설명 분리
2295
+ // 형식: "[태그] 키워드 - 설명" 또는 "[태그] 키워드"
2296
+ let tag = '';
2297
+ let keyword = '';
2298
+ let description = '';
2299
+
2300
+ const tagMatch = todo.match(/^\[([^\]]+)\]\s*/);
2301
+ if (tagMatch) {
2302
+ tag = tagMatch[1];
2303
+ const rest = todo.slice(tagMatch[0].length);
2304
+ const descSplit = rest.split(/\s*[-–]\s*/);
2305
+ keyword = descSplit[0].trim();
2306
+ description = descSplit.slice(1).join(' - ').trim();
2307
+ } else {
2308
+ // 태그 없이 입력된 경우
2309
+ const descSplit = todo.split(/\s*[-–]\s*/);
2310
+ keyword = descSplit[0].trim();
2311
+ description = descSplit.slice(1).join(' - ').trim();
2312
+ }
2313
+
2314
+ // 킹투두 형식으로 작성
2315
+ let newTodo;
2316
+ if (tag) {
2317
+ newTodo = `${newId}. ⚙️ **[${tag}] ${keyword}** ${dateStr}\n`;
2318
+ } else {
2319
+ newTodo = `${newId}. ⚙️ **${keyword}** ${dateStr}\n`;
2320
+ }
2321
+
2322
+ if (description) {
2323
+ newTodo += ` ${description}\n`;
2324
+ }
2325
+ newTodo += '\n';
2176
2326
 
2177
2327
  // 파일 끝에 추가
2178
2328
  fs.appendFileSync(todoFilePath, newTodo, 'utf-8');
@@ -2180,10 +2330,10 @@ async function handleTodo(args) {
2180
2330
  return {
2181
2331
  content: [{
2182
2332
  type: 'text',
2183
- text: `✓ TODO 추가 완료!
2333
+ text: `✓ 킹투두에 등록했습니다!
2184
2334
 
2185
- 📝 #${newId}: ${todo}
2186
- 📅 등록: ${dateStr} ${timeStr}
2335
+ 📝 #${newId}: ${tag ? `[${tag}] ` : ''}${keyword}${description ? ` - ${description}` : ''}
2336
+ 📅 등록: ${dateStr}
2187
2337
 
2188
2338
  💡 완료 시: docuking_todo(action: "done", todoId: ${newId})`,
2189
2339
  }],
@@ -2197,73 +2347,81 @@ async function handleTodo(args) {
2197
2347
  };
2198
2348
  }
2199
2349
 
2200
- // 해당 번호의 TODO 찾아서 완료 표시
2201
- const todoLinePattern = new RegExp(`^(${todoId}\\. - \\[)( )(\\].*)$`, 'm');
2350
+ // 해당 번호의 TODO 찾아서 ⚙️ -> ✅ 변경 및 날짜 추가
2351
+ const todoLinePattern = new RegExp(`^(${todoId}\\. )⚙️( \\*\\*.*\\*\\* )(\\d+\\.\\d+)(.*)$`, 'm');
2202
2352
  const matched = content.match(todoLinePattern);
2203
2353
 
2204
2354
  if (!matched) {
2355
+ // 이미 완료된 항목인지 확인
2356
+ const completedPattern = new RegExp(`^${todoId}\\. ✅`, 'm');
2357
+ if (completedPattern.test(content)) {
2358
+ return {
2359
+ content: [{ type: 'text', text: `킹투두 #${todoId}는 이미 완료 상태입니다.` }],
2360
+ };
2361
+ }
2205
2362
  return {
2206
- content: [{ type: 'text', text: `오류: TODO #${todoId}를 찾을 수 없습니다.` }],
2363
+ content: [{ type: 'text', text: `오류: 킹투두 #${todoId}를 찾을 수 없습니다.` }],
2207
2364
  };
2208
2365
  }
2209
2366
 
2210
- // [ ] -> [x] 변경 + 완료 날짜 추가
2367
+ // ⚙️ -> 변경 + 완료 날짜 추가 (등록일/완료일)
2211
2368
  const updatedContent = content.replace(
2212
2369
  todoLinePattern,
2213
- `$1x$3 ✓${dateStr}`
2370
+ `$1✅$2$3/${dateStr}$4`
2214
2371
  );
2215
2372
 
2216
2373
  fs.writeFileSync(todoFilePath, updatedContent, 'utf-8');
2217
2374
 
2218
2375
  // 완료된 TODO 내용 추출
2219
- const todoContent = matched[0].replace(/^\d+\. - \[ \] /, '').replace(/ \(\d{4}-\d{2}-\d{2}\)$/, '');
2376
+ const keywordMatch = matched[2].match(/\*\*(.+)\*\*/);
2377
+ const todoKeyword = keywordMatch ? keywordMatch[1] : '';
2220
2378
 
2221
2379
  return {
2222
2380
  content: [{
2223
2381
  type: 'text',
2224
- text: `✓ TODO #${todoId} 완료!
2382
+ text: `✓ 킹투두 #${todoId} 완료!
2225
2383
 
2226
- ✅ ${todoContent}
2227
- 📅 완료: ${dateStr} ${timeStr}`,
2384
+ ✅ ${todoKeyword}
2385
+ 📅 완료: ${dateStr}`,
2228
2386
  }],
2229
2387
  };
2230
2388
  }
2231
2389
 
2232
2390
  if (action === 'list') {
2233
- // 미완료 TODO 추출
2234
- const pendingPattern = /^(\d+)\. - \[ \] (.+)$/gm;
2391
+ // 미완료(⚙️) TODO 추출
2392
+ const pendingPattern = /^(\d+)\. ⚙️ \*\*(.+)\*\* (\d+\.\d+)/gm;
2235
2393
  const pendingTodos = [];
2236
2394
  let listMatch;
2237
2395
  while ((listMatch = pendingPattern.exec(content)) !== null) {
2238
- pendingTodos.push({ id: listMatch[1], content: listMatch[2] });
2396
+ pendingTodos.push({ id: listMatch[1], keyword: listMatch[2], date: listMatch[3] });
2239
2397
  }
2240
2398
 
2241
- // 완료된 TODO 수 세기
2242
- const completedCount = (content.match(/- \[x\]/g) || []).length;
2399
+ // 완료된(✅) TODO 수 세기
2400
+ const completedCount = (content.match(/^(\d+)\. ✅/gm) || []).length;
2243
2401
 
2244
2402
  if (pendingTodos.length === 0) {
2245
2403
  return {
2246
2404
  content: [{
2247
2405
  type: 'text',
2248
- text: `📋 미완료 TODO: 없음
2406
+ text: `📋 킹투두 미결: 없음
2249
2407
 
2250
- 완료된 TODO: ${completedCount}개
2251
- 📁 전체 기록: z_DocuKing/z_Todo/z_Todo.md`,
2408
+ 완료: ${completedCount}개
2409
+ 📁 전체 기록: z_DocuKing/z_King_Todo/z_King_Todo.md`,
2252
2410
  }],
2253
2411
  };
2254
2412
  }
2255
2413
 
2256
- const listText = pendingTodos.map(t => ` #${t.id}: ${t.content}`).join('\n');
2414
+ const listText = pendingTodos.map(t => ` #${t.id}: ${t.keyword} (${t.date})`).join('\n');
2257
2415
 
2258
2416
  return {
2259
2417
  content: [{
2260
2418
  type: 'text',
2261
- text: `📋 미완료 TODO: ${pendingTodos.length}개
2419
+ text: `📋 킹투두 미결: ${pendingTodos.length}개
2262
2420
 
2263
2421
  ${listText}
2264
2422
 
2265
- 완료된 TODO: ${completedCount}개
2266
- 📁 전체 기록: z_DocuKing/z_Todo/z_Todo.md`,
2423
+ 완료: ${completedCount}개
2424
+ 📁 전체 기록: z_DocuKing/z_King_Todo/z_King_Todo.md`,
2267
2425
  }],
2268
2426
  };
2269
2427
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docuking-mcp",
3
- "version": "1.8.0",
3
+ "version": "1.9.1",
4
4
  "description": "DocuKing MCP Server - AI 시대의 문서 협업 플랫폼",
5
5
  "type": "module",
6
6
  "main": "index.js",