docuking-mcp 2.9.0 → 2.9.2

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/handlers/docs.js CHANGED
@@ -406,6 +406,28 @@ ${notes || '(없음)'}
406
406
 
407
407
  const relativePath = path.relative(localPath, planFilePath).replace(/\\/g, '/');
408
408
 
409
+ // 새 Plan 생성 시 Talk/Todo 자동 생성 지시
410
+ let autoCreateHint = '';
411
+ if (isNew) {
412
+ autoCreateHint = `
413
+
414
+ ---
415
+ 📝 **AI 작업 흐름 (자동 체크)**
416
+
417
+ 아래 항목이 아직 없다면 생성하세요:
418
+
419
+ 1. **Talk 기록** (이 작업의 배경/논의)
420
+ - 이미 작성했으면 스킵
421
+ - 없으면: docuking_talk 호출 (왜 이 작업을 하게 됐는지)
422
+
423
+ 2. **킹투두 등록** (이 작업 추적용)
424
+ - 이미 등록했으면 스킵
425
+ - 없으면: docuking_todo(action: "add", todo: "[태그] ${title}")
426
+
427
+ 3. **작업 완료 후**
428
+ - docuking_done 호출 (planId: "${targetPlanId}")`;
429
+ }
430
+
409
431
  return {
410
432
  content: [
411
433
  {
@@ -418,7 +440,7 @@ ${notes || '(없음)'}
418
440
  📊 진행률: ${progress}% (${doneCount}/${totalCount})
419
441
 
420
442
  💡 이 planId를 기억해두세요. 나중에 업데이트하거나 완료 처리할 때 필요합니다.
421
- 💡 DocuKing에 Push하면 웹에서도 확인할 수 있습니다.`,
443
+ 💡 DocuKing에 Push하면 웹에서도 확인할 수 있습니다.${autoCreateHint}`,
422
444
  },
423
445
  ],
424
446
  };
@@ -501,6 +523,26 @@ ${artifactsMarkdown}
501
523
  pushMessage = `\n\n⚠️ DocuKing 동기화 실패: ${e.message}`;
502
524
  }
503
525
 
526
+ // 검증 지시 메시지
527
+ const validateHint = artifacts.length > 0 ? `
528
+
529
+ ---
530
+ 🔍 **정책 검증 필요**
531
+
532
+ 산출물이 xx_Policy/ 정책을 준수하는지 검증하세요:
533
+ ${artifacts.slice(0, 5).map(a => `- ${a}`).join('\n')}${artifacts.length > 5 ? `\n... 외 ${artifacts.length - 5}개` : ''}
534
+
535
+ **검증 방법:**
536
+ 1. Task 도구로 서브에이전트 실행:
537
+ - prompt: "위 파일들이 xx_Policy/ 정책을 준수하는지 확인. 위반 시 수정하거나 docuking_urgent로 보고"
538
+ - subagent_type: "Explore"
539
+
540
+ 2. 또는 직접 xx_Policy/ 읽고 비교
541
+
542
+ 위반 발견 시:
543
+ - 수정 가능하면 → 직접 수정
544
+ - 정책 불명확하면 → docuking_urgent 호출` : '';
545
+
504
546
  return {
505
547
  content: [
506
548
  {
@@ -512,7 +554,7 @@ ${artifactsMarkdown}
512
554
  🕐 완료 시간: ${timestamp}
513
555
 
514
556
  📝 요약: ${summary}
515
- ${artifacts.length > 0 ? `📦 산출물: ${artifacts.length}개` : ''}${pushMessage}`,
557
+ ${artifacts.length > 0 ? `📦 산출물: ${artifacts.length}개` : ''}${pushMessage}${validateHint}`,
516
558
  },
517
559
  ],
518
560
  };
package/handlers/sync.js CHANGED
@@ -234,31 +234,6 @@ Git처럼 무엇을 변경했는지 명확히 작성해주세요.
234
234
  const projectId = projectInfo.projectId;
235
235
  const projectName = projectInfo.projectName;
236
236
 
237
- // ========================================
238
- // 1단계: Pull 먼저 실행 (git pull && git push 패턴)
239
- // ========================================
240
- let pullResultText = '';
241
- try {
242
- const pullResult = await handlePullInternal({ localPath, filePath });
243
- pullResultText = pullResult.text;
244
- } catch (e) {
245
- return {
246
- content: [
247
- {
248
- type: 'text',
249
- text: `❌ Pull 실패로 Push를 중단합니다.
250
- 오류: ${e.message}
251
-
252
- 먼저 Pull 문제를 해결한 후 다시 시도하세요.`,
253
- },
254
- ],
255
- };
256
- }
257
-
258
- // ========================================
259
- // 2단계: Push 진행
260
- // ========================================
261
-
262
237
  // Co-worker 권한은 API Key 형식에서 판단
263
238
  const { isCoworker, coworkerFolder } = parseCoworkerFromApiKey(apiKey);
264
239
  const coworkerFolderName = isCoworker ? `yy_Coworker_${coworkerFolder}` : null;
@@ -498,6 +473,35 @@ docuking_init을 먼저 실행하세요.`,
498
473
  }
499
474
  }
500
475
 
476
+ // ========================================
477
+ // 로컬 파일 경로 목록 저장 (Pull 전에!)
478
+ // soft-delete에서 "로컬에 없는 파일" 판단에 사용
479
+ // Pull이 파일을 내려받아도 이 목록은 변하지 않음
480
+ // ========================================
481
+ const localPathsBeforePull = new Set(filesToPush.map(f => f.serverPath));
482
+ console.error(`[DocuKing] Pull 전 로컬 파일 ${localPathsBeforePull.size}개 기록됨`);
483
+
484
+ // ========================================
485
+ // Pull 실행 (git pull && git push 패턴)
486
+ // ========================================
487
+ let pullResultText = '';
488
+ try {
489
+ const pullResult = await handlePullInternal({ localPath, filePath });
490
+ pullResultText = pullResult.text;
491
+ } catch (e) {
492
+ return {
493
+ content: [
494
+ {
495
+ type: 'text',
496
+ text: `❌ Pull 실패로 Push를 중단합니다.
497
+ 오류: ${e.message}
498
+
499
+ 먼저 Pull 문제를 해결한 후 다시 시도하세요.`,
500
+ },
501
+ ],
502
+ };
503
+ }
504
+
501
505
  if (filesToPush.length === 0 && emptyFolders.length === 0) {
502
506
  return {
503
507
  content: [
@@ -734,16 +738,21 @@ docuking_init을 먼저 실행하세요.`,
734
738
  // 단, 협업자 폴더(yy_Coworker_*)는 삭제하지 않음 (오너가 협업자 파일을 삭제하면 안됨)
735
739
  // 기존 hard delete(delete-batch) → soft delete(soft-delete)로 변경
736
740
  // 3일 후 크론잡에서 hard delete 수행
741
+ // 중요: localPathsBeforePull 사용 (Pull 전에 수집한 목록)
742
+ // processedLocalPaths는 Pull로 내려받은 파일도 포함하므로 사용 금지
737
743
  let deleted = 0;
738
744
  const deletedFilePaths = [];
739
745
  let protectedFiles = []; // source: 'web' 파일 (보호됨)
740
746
  if (serverAllPaths.length > 0 && !isCoworker) {
741
747
  // 코워커가 아닌 경우에만 삭제 수행 (오너 전용)
742
748
  // yy_Coworker_*로 시작하는 경로는 삭제 대상에서 제외
749
+ // localPathsBeforePull: Pull 전에 수집한 로컬 파일 목록 (좀비 파일 방지)
743
750
  const pathsToDelete = serverAllPaths.filter(p =>
744
- !processedLocalPaths.has(p) && !p.startsWith('yy_Coworker_')
751
+ !localPathsBeforePull.has(p) && !p.startsWith('yy_Coworker_')
745
752
  );
746
753
 
754
+ console.error(`[DocuKing] soft-delete 대상: ${pathsToDelete.length}개 (서버에만 있고 Pull 전 로컬에 없던 파일)`);
755
+
747
756
  if (pathsToDelete.length > 0) {
748
757
  try {
749
758
  // soft-delete API 호출 (deleted_at 설정, 3일 후 hard delete)
@@ -943,6 +952,65 @@ export async function handlePullInternal(args) {
943
952
 
944
953
  const projectId = projectInfo.projectId;
945
954
 
955
+ // Co-worker 권한은 API Key 형식에서 판단
956
+ const { isCoworker, coworkerFolder } = parseCoworkerFromApiKey(apiKey);
957
+
958
+ // ========================================
959
+ // 필수 폴더 체크 & 생성 (init 이후 추가된 폴더들)
960
+ // ========================================
961
+ const createdFolders = [];
962
+
963
+ // 공통 필수 폴더
964
+ const commonFolders = ['xx_Urgent'];
965
+
966
+ // 오너 전용 폴더
967
+ const ownerFolders = ['xx_Infra_Config', 'xx_Policy', 'zz_ai_1_Talk', 'zz_ai_2_Todo', 'zz_ai_3_Plan'];
968
+
969
+ // 협업자 전용 폴더 (yy_Coworker_{폴더명}/ 안에)
970
+ const coworkerSubFolders = ['zz_ai_1_Talk', 'zz_ai_2_Todo', 'zz_ai_3_Plan', '_Private'];
971
+
972
+ // 공통 폴더 체크
973
+ for (const folder of commonFolders) {
974
+ const folderPath = path.join(localPath, folder);
975
+ if (!fs.existsSync(folderPath)) {
976
+ fs.mkdirSync(folderPath, { recursive: true });
977
+ createdFolders.push(folder);
978
+ console.error(`[DocuKing] 누락 폴더 생성: ${folder}/`);
979
+ }
980
+ }
981
+
982
+ if (isCoworker) {
983
+ // 협업자: yy_Coworker_{폴더명}/ 안에 하위 폴더 체크
984
+ const coworkerFolderName = `yy_Coworker_${coworkerFolder}`;
985
+ const coworkerBasePath = path.join(localPath, coworkerFolderName);
986
+
987
+ // 협업자 루트 폴더
988
+ if (!fs.existsSync(coworkerBasePath)) {
989
+ fs.mkdirSync(coworkerBasePath, { recursive: true });
990
+ createdFolders.push(coworkerFolderName);
991
+ }
992
+
993
+ // 협업자 하위 폴더
994
+ for (const subFolder of coworkerSubFolders) {
995
+ const subFolderPath = path.join(coworkerBasePath, subFolder);
996
+ if (!fs.existsSync(subFolderPath)) {
997
+ fs.mkdirSync(subFolderPath, { recursive: true });
998
+ createdFolders.push(`${coworkerFolderName}/${subFolder}`);
999
+ console.error(`[DocuKing] 누락 폴더 생성: ${coworkerFolderName}/${subFolder}/`);
1000
+ }
1001
+ }
1002
+ } else {
1003
+ // 오너: 루트에 폴더 체크
1004
+ for (const folder of ownerFolders) {
1005
+ const folderPath = path.join(localPath, folder);
1006
+ if (!fs.existsSync(folderPath)) {
1007
+ fs.mkdirSync(folderPath, { recursive: true });
1008
+ createdFolders.push(folder);
1009
+ console.error(`[DocuKing] 누락 폴더 생성: ${folder}/`);
1010
+ }
1011
+ }
1012
+ }
1013
+
946
1014
  // yy_All_Docu 폴더 (없으면 생성)
947
1015
  const mainFolderName = 'yy_All_Docu';
948
1016
  const mainFolderPath = path.join(localPath, mainFolderName);
@@ -1127,6 +1195,14 @@ export async function handlePullInternal(args) {
1127
1195
  - 스킵 (변경 없음): ${skipped}개
1128
1196
  - 실패: ${failed}개`;
1129
1197
 
1198
+ // 누락 폴더 생성 알림
1199
+ if (createdFolders.length > 0) {
1200
+ resultText += `\n\n📁 누락 폴더 생성됨 (${createdFolders.length}개):`;
1201
+ createdFolders.forEach(f => {
1202
+ resultText += `\n + ${f}/`;
1203
+ });
1204
+ }
1205
+
1130
1206
  // 다운로드된 파일 목록
1131
1207
  const downloadedFiles = results.filter(r => r.type === 'download');
1132
1208
  if (downloadedFiles.length > 0) {
@@ -1274,7 +1350,6 @@ export async function handlePullInternal(args) {
1274
1350
  // 킹어전트 감지 (xx_Urgent/ 폴더의 긴급 보고 확인)
1275
1351
  // 오너에게만 알림 (협업자는 자기가 작성한 거니까)
1276
1352
  // ========================================
1277
- const { isCoworker } = parseCoworkerFromApiKey(apiKey);
1278
1353
  if (!isCoworker) {
1279
1354
  const urgentFolderPath = path.join(localPath, 'xx_Urgent');
1280
1355
  if (fs.existsSync(urgentFolderPath)) {
@@ -1566,3 +1641,176 @@ export async function handleStatus(args) {
1566
1641
  ],
1567
1642
  };
1568
1643
  }
1644
+
1645
+ /**
1646
+ * docuking_delete 구현 - 서버 파일 삭제
1647
+ */
1648
+ export async function handleDelete(args) {
1649
+ const localPath = args.localPath || process.cwd();
1650
+ const { paths, force = false } = args;
1651
+
1652
+ if (!paths || paths.length === 0) {
1653
+ return {
1654
+ content: [{
1655
+ type: 'text',
1656
+ text: '오류: 삭제할 파일 경로(paths)가 필요합니다.',
1657
+ }],
1658
+ };
1659
+ }
1660
+
1661
+ // 로컬 config에서 API 키 읽기
1662
+ const apiKey = getApiKey(localPath);
1663
+ if (!apiKey) {
1664
+ return {
1665
+ content: [{
1666
+ type: 'text',
1667
+ text: '오류: API 키를 찾을 수 없습니다. 먼저 docuking_init을 실행하세요.',
1668
+ }],
1669
+ };
1670
+ }
1671
+
1672
+ // 프로젝트 정보 조회
1673
+ const projectInfo = getProjectInfo(localPath);
1674
+ if (projectInfo.error) {
1675
+ return {
1676
+ content: [{
1677
+ type: 'text',
1678
+ text: projectInfo.error,
1679
+ }],
1680
+ };
1681
+ }
1682
+
1683
+ const projectId = projectInfo.projectId;
1684
+
1685
+ // Co-worker 권한 체크
1686
+ const { isCoworker, coworkerFolder } = parseCoworkerFromApiKey(apiKey);
1687
+
1688
+ // 협업자는 자기 폴더만 삭제 가능
1689
+ if (isCoworker) {
1690
+ const coworkerPrefix = `yy_Coworker_${coworkerFolder}/`;
1691
+ const invalidPaths = paths.filter(p => !p.startsWith(coworkerPrefix) && !p.startsWith('xx_Urgent/'));
1692
+ if (invalidPaths.length > 0) {
1693
+ return {
1694
+ content: [{
1695
+ type: 'text',
1696
+ text: `오류: 협업자는 자기 폴더(${coworkerPrefix})와 xx_Urgent/만 삭제할 수 있습니다.
1697
+ 삭제 불가: ${invalidPaths.join(', ')}`,
1698
+ }],
1699
+ };
1700
+ }
1701
+ }
1702
+
1703
+ // 오너도 협업자 폴더는 삭제 금지
1704
+ if (!isCoworker) {
1705
+ const coworkerPaths = paths.filter(p => p.startsWith('yy_Coworker_'));
1706
+ if (coworkerPaths.length > 0 && !force) {
1707
+ return {
1708
+ content: [{
1709
+ type: 'text',
1710
+ text: `오류: 협업자 폴더는 삭제할 수 없습니다.
1711
+ 대상: ${coworkerPaths.join(', ')}
1712
+
1713
+ 정말 삭제하려면 force: true 옵션을 사용하세요. (권장하지 않음)`,
1714
+ }],
1715
+ };
1716
+ }
1717
+ }
1718
+
1719
+ // soft-delete API 호출
1720
+ try {
1721
+ const deleteResponse = await fetch(`${API_ENDPOINT}/files/soft-delete`, {
1722
+ method: 'POST',
1723
+ headers: {
1724
+ 'Content-Type': 'application/json',
1725
+ 'Authorization': `Bearer ${apiKey}`,
1726
+ },
1727
+ body: JSON.stringify({
1728
+ projectId,
1729
+ paths,
1730
+ source: 'mcp-delete', // 명시적 삭제 요청 표시
1731
+ }),
1732
+ });
1733
+
1734
+ if (!deleteResponse.ok) {
1735
+ const error = await deleteResponse.text();
1736
+ return {
1737
+ content: [{
1738
+ type: 'text',
1739
+ text: `오류: 삭제 실패 - ${error}`,
1740
+ }],
1741
+ };
1742
+ }
1743
+
1744
+ const result = await deleteResponse.json();
1745
+ const deleted = result.deleted || 0;
1746
+ const deletedPaths = result.deletedPaths || [];
1747
+ const protectedFiles = result.protected || [];
1748
+
1749
+ let resultText = `✓ 삭제 완료!
1750
+
1751
+ 📊 처리 결과:
1752
+ - 삭제됨: ${deleted}개 (3일 후 영구 삭제)
1753
+ - 보호됨: ${protectedFiles.length}개`;
1754
+
1755
+ if (deletedPaths.length > 0) {
1756
+ resultText += `\n\n🗑️ 삭제된 파일:`;
1757
+ deletedPaths.slice(0, 20).forEach(p => {
1758
+ resultText += `\n ✗ ${p}`;
1759
+ });
1760
+ if (deletedPaths.length > 20) {
1761
+ resultText += `\n ... 외 ${deletedPaths.length - 20}개`;
1762
+ }
1763
+ }
1764
+
1765
+ if (protectedFiles.length > 0) {
1766
+ resultText += `\n\n🛡️ 보호된 파일 (웹에서 생성됨):`;
1767
+ protectedFiles.slice(0, 10).forEach(p => {
1768
+ resultText += `\n 🔒 ${p}`;
1769
+ });
1770
+ }
1771
+
1772
+ resultText += `\n\n💡 삭제된 파일은 3일 후 영구 삭제됩니다.
1773
+ 💡 복구가 필요하면 3일 이내에 docuking_rollback을 사용하세요.`;
1774
+
1775
+ // 로컬 파일도 삭제
1776
+ let localDeleted = 0;
1777
+ for (const serverPath of deletedPaths) {
1778
+ try {
1779
+ let localFilePath;
1780
+ if (serverPath.startsWith('xx_') ||
1781
+ serverPath.startsWith('yy_All_Docu/') ||
1782
+ serverPath.startsWith('yy_Coworker_') ||
1783
+ serverPath.startsWith('zz_ai_')) {
1784
+ localFilePath = path.join(localPath, serverPath);
1785
+ } else {
1786
+ localFilePath = path.join(localPath, 'yy_All_Docu', serverPath);
1787
+ }
1788
+
1789
+ if (fs.existsSync(localFilePath)) {
1790
+ fs.unlinkSync(localFilePath);
1791
+ localDeleted++;
1792
+ }
1793
+ } catch (e) {
1794
+ console.error(`[DocuKing] 로컬 파일 삭제 실패: ${serverPath} - ${e.message}`);
1795
+ }
1796
+ }
1797
+
1798
+ if (localDeleted > 0) {
1799
+ resultText += `\n\n📁 로컬 파일도 ${localDeleted}개 삭제됨`;
1800
+ }
1801
+
1802
+ return {
1803
+ content: [{
1804
+ type: 'text',
1805
+ text: resultText,
1806
+ }],
1807
+ };
1808
+ } catch (e) {
1809
+ return {
1810
+ content: [{
1811
+ type: 'text',
1812
+ text: `오류: ${e.message}`,
1813
+ }],
1814
+ };
1815
+ }
1816
+ }