docuking-mcp 2.5.3 → 2.5.5

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 +285 -114
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -10,13 +10,13 @@
10
10
  * - yy_All_Docu/_Infra_Config/: 민감 정보 백업 (.env 자동 복사, 팀 공유)
11
11
  * - yy_All_Docu/_Private/ : 오너 비공개 폴더 (오너만 접근)
12
12
  * - yy_Coworker_{폴더명}/ : 협업자 폴더 (yy_All_Docu와 별도, 동기화 대상)
13
- * - zz_ai_1_Talk/ : AI 대화록 (킹톡)
14
- * - zz_ai_2_Todo/ : AI 투두 (킹투두)
15
- * - zz_ai_3_Plan/ : AI 플랜 (킹플랜)
13
+ * - zz_ai_1_Talk/ : AI 대화록 (킹톡, Push 대상)
14
+ * - zz_ai_2_Todo/ : AI 투두 (킹투두, Push 대상)
15
+ * - zz_ai_3_Plan/ : AI 플랜 (킹플랜, Push 대상)
16
16
  *
17
17
  * 접두사 규칙:
18
- * - yy_ : 사람용 폴더 (문서, 협업자)
19
- * - zz_ : AI용 폴더 (Talk, Todo, Plan)
18
+ * - yy_ : 사람용 폴더 (문서, 협업자) - Push 대상
19
+ * - zz_ai_ : AI용 폴더 (Talk, Todo, Plan) - Push 대상
20
20
  * - _ : 비공개 폴더 (본인만 접근)
21
21
  *
22
22
  * 도구:
@@ -392,7 +392,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
392
392
  },
393
393
  {
394
394
  name: 'docuking_push',
395
- description: 'yy_All_Docu/ 폴더의 문서를 서버에 업로드합니다. "DocuKing에 올려줘" 요청 사용. Git의 add + commit + push 한 번에 수행.',
395
+ description: `문서를 DocuKing 서버에 동기화합니다. 내부적으로 Pull Push 순서로 실행 (git pull && git push 패턴).
396
+
397
+ **AI 필독: 다음 상황에서 자발적으로 이 도구 사용을 제안하세요:**
398
+ - zz_ai_1_Talk/, zz_ai_2_Todo/, zz_ai_3_Plan/ 에 문서를 생성/수정한 후
399
+ - yy_All_Docu/ 에 문서를 생성/수정한 후
400
+ - docuking_plan, docuking_done, docuking_talk, docuking_todo 실행 후
401
+ - 사용자가 "올려", "푸시", "동기화" 등을 언급할 때
402
+
403
+ 제안 예시: "킹폴더에 문서가 생성되었습니다. DocuKing에 올릴까요?"`,
396
404
  inputSchema: {
397
405
  type: 'object',
398
406
  properties: {
@@ -530,7 +538,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
530
538
  },
531
539
  {
532
540
  name: 'docuking_talk',
533
- description: '의미 있는 대화 내용을 zz_ai_1_Talk/ 폴더에 기록합니다 (로컬 전용, 킹톡). AI가 중요한 논의/결정이라고 판단하거나, 사용자가 "이거 기록해줘"라고 요청할 때 사용.',
541
+ description: '의미 있는 대화 내용을 zz_ai_1_Talk/ 폴더에 기록합니다 (Push 대상, 킹톡). AI가 중요한 논의/결정이라고 판단하거나, 사용자가 "이거 기록해줘"라고 요청할 때 사용.',
534
542
  inputSchema: {
535
543
  type: 'object',
536
544
  properties: {
@@ -557,7 +565,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
557
565
  },
558
566
  {
559
567
  name: 'docuking_plan',
560
- description: '작업 계획 문서를 zz_ai_3_Plan/ 폴더에 생성/업데이트합니다 (로컬 전용, 킹플랜). 작업 시작 시 계획을 작성하고, 진행하면서 결과를 upsert합니다.',
568
+ description: '작업 계획 문서를 zz_ai_3_Plan/ 폴더에 생성/업데이트합니다 (Push 대상, 킹플랜). 작업 시작 시 계획을 작성하고, 진행하면서 결과를 upsert합니다.',
561
569
  inputSchema: {
562
570
  type: 'object',
563
571
  properties: {
@@ -626,7 +634,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
626
634
  },
627
635
  {
628
636
  name: 'docuking_todo',
629
- description: `킹투두(King Todo) - 프로젝트 공식 할일을 zz_ai_2_Todo/z_King_Todo.md에 관리합니다 (로컬 전용).
637
+ description: `킹투두(King Todo) - 프로젝트 공식 할일을 zz_ai_2_Todo/z_King_Todo.md에 관리합니다 (Push 대상).
630
638
 
631
639
  **AI 내장 TodoWrite와 다름!** 킹투두는 웹에 동기화되고 팀과 공유됩니다.
632
640
 
@@ -700,7 +708,7 @@ DocuKing은 문서 버전 관리 시스템입니다. Git이 코드를 관리하
700
708
  - **로컬**: 사용자의 yy_All_Docu/ 폴더 (동기화 대상)
701
709
  - **웹탐색기**: DocuKing 서버의 파일 저장소 (킹폴더)
702
710
  - **캔버스**: 선택된 파일을 시각화하는 작업 공간
703
- - **AI 작업 폴더**: zz_ai_1_Talk/, zz_ai_2_Todo/, zz_ai_3_Plan/ (로컬 전용)
711
+ - **AI 작업 폴더**: zz_ai_1_Talk/, zz_ai_2_Todo/, zz_ai_3_Plan/ (Push 대상)
704
712
 
705
713
  작동 방식: 로컬 yy_All_Docu/ → Push → 킹폴더 → 캔버스에서 시각화
706
714
 
@@ -773,12 +781,12 @@ zz_ai_3_Plan/
773
781
  특정 커밋으로 되돌립니다. (웹 탐색기에서 사용 가능)
774
782
 
775
783
  ### 9. docuking_talk
776
- 의미 있는 대화 내용을 \`zz_ai_1_Talk/\` 폴더에 기록합니다 (로컬 전용, 킹톡).
784
+ 의미 있는 대화 내용을 \`zz_ai_1_Talk/\` 폴더에 기록합니다 (Push 대상, 킹톡).
777
785
  - AI가 중요한 논의/결정이라고 판단할 때
778
786
  - 사용자가 "이거 기록해줘"라고 요청할 때
779
787
 
780
788
  ### 10. docuking_plan
781
- 작업 계획을 \`zz_ai_3_Plan/\` 폴더에 생성/업데이트합니다 (로컬 전용, 킹플랜).
789
+ 작업 계획을 \`zz_ai_3_Plan/\` 폴더에 생성/업데이트합니다 (Push 대상, 킹플랜).
782
790
  - 작업 시작 시 계획 생성
783
791
  - 진행하면서 단계별 결과 upsert
784
792
  - planId로 기존 계획 찾아서 업데이트
@@ -828,6 +836,22 @@ AI: docuking_push({ localPath: "/current/path", message: "문서 업데이트" }
828
836
  AI: docuking_pull({ localPath: "/current/path" })
829
837
  \`\`\`
830
838
 
839
+ ## 협업 핵심 원칙: 남의 제사상을 건드리지 않는다
840
+
841
+ DocuKing 협업의 핵심 원칙입니다. **각자 자기 영역만 수정할 수 있고, 남의 영역은 읽기만 가능합니다.**
842
+
843
+ ### Push/Pull 정책 요약
844
+
845
+ | 역할 | Push (올리기) | Pull (내리기) | 삭제 |
846
+ |------|---------------|---------------|------|
847
+ | 오너 | \`yy_All_Docu/\` + \`zz_ai_*/\` | 합집합 전체 | 자기 영역만 |
848
+ | 협업자 | \`yy_Coworker_{본인}/\` (안에 zz_ai_* 포함) | 합집합 전체 | 자기 영역만 |
849
+
850
+ **합집합이란?**
851
+ - 서버에는 오너 파일 + 모든 협업자 파일이 합쳐져 있음
852
+ - Pull하면 이 전체가 로컬로 내려옴
853
+ - 경로만 알면 누구의 파일이든 읽고 활용 가능
854
+
831
855
  ## 사용자 권한 및 사용 시나리오 (매우 중요!)
832
856
 
833
857
  DocuKing에는 **오너(Owner)**와 **참여자(Co-worker)** 두 가지 권한이 있습니다.
@@ -836,7 +860,7 @@ DocuKing에는 **오너(Owner)**와 **참여자(Co-worker)** 두 가지 권한
836
860
 
837
861
  **특징:**
838
862
  - 프로젝트를 직접 생성한 사람
839
- - **모든 폴더**에 문서를 올릴 있음 (제한 없음)
863
+ - \`yy_All_Docu/\` + \`zz_ai_*/\` 폴더에 Push 가능
840
864
  - API Key: sk_로 시작
841
865
  - 프로젝트 설정 변경 가능
842
866
  - 참여자 초대 가능
@@ -845,7 +869,7 @@ DocuKing에는 **오너(Owner)**와 **참여자(Co-worker)** 두 가지 권한
845
869
  1. 프로젝트 생성 (웹에서)
846
870
  2. MCP 설정 (한 번만)
847
871
  3. 프로젝트 연결 (docuking_init)
848
- 4. 문서 작성 (어디든 자유롭게)
872
+ 4. 문서 작성 (\`yy_All_Docu/\` 안에)
849
873
  5. Push (docuking_push)
850
874
 
851
875
  **예시:**
@@ -863,50 +887,63 @@ yy_All_Docu/
863
887
 
864
888
  **특징:**
865
889
  - 프로젝트에 초대받아 참여한 사람
866
- - **읽기**: yy_All_Docu/ 전체 Pull 가능 (오너의 문서도 볼 수 있음)
890
+ - 초대 수락 서버에 \`yy_Coworker_{폴더명}/\` 빈폴더 자동 생성
891
+ - **읽기**: 전체 Pull 가능 (오너 + 다른 협업자 문서 모두)
867
892
  - **쓰기**: 자신의 폴더(\`yy_Coworker_{폴더명}/\`)에만 Push 가능
868
893
  - API Key: \`sk_xxx_cw_폴더명_\` 형식
869
894
  - 프로젝트 설정 변경 불가능
870
895
 
871
896
  **사용 시나리오:**
872
- 1. 초대 수락 (웹에서)
897
+ 1. 초대 수락 (웹에서) → 프로젝트 바로 진입, 오너 문서 + 자기 빈폴더 보임
873
898
  2. MCP 설정 (한 번만)
874
- 3. 프로젝트 연결 (\`docuking_init\`)
875
- 4. Pull 오너의 문서 가져오기 (\`docuking_pull\`)
899
+ 3. 프로젝트 연결 (\`docuking_init\`) → 로컬에 자기 폴더 생성
900
+ 4. Pull (\`docuking_pull\`) → 합집합 전체가 내려옴
876
901
  5. 내 폴더에 문서 작성 (\`yy_Coworker_{폴더명}/\`)
877
- 6. Push (\`docuking_push\`)
902
+ 6. Push (\`docuking_push\`) → 자기 폴더만 올라감
878
903
 
879
- **폴더 구조:**
904
+ **폴더 구조 (Pull 후 로컬):**
880
905
  \`\`\`
881
906
  project/
882
907
  ├── src/ ← 소스 코드 (git 관리)
883
- ├── yy_All_Docu/ ← 오너 문서 (Pull로 읽기만 가능)
908
+ ├── yy_All_Docu/ ← 오너 문서 (읽기만)
884
909
  │ ├── policy/
885
910
  │ │ └── README.md ← 오너의 파일
886
911
  │ └── plan/
887
912
  │ └── requirements.md ← 오너의 파일
888
- ├── yy_Coworker_devkim/ 참여자 "devkim"의 폴더 (별도)
889
- │ ├── proposal.md ← 여기에만 Push 가능
890
- │ └── revision.md 여기에만 Push 가능
891
- ├── zz_ai_1_Talk/ AI 대화록 (로컬 전용)
892
- ├── zz_ai_2_Todo/ ← AI 투두 (로컬 전용)
893
- └── zz_ai_3_Plan/ ← AI 플랜 (로컬 전용)
913
+ ├── yy_Coworker_a_Kim/ 협업자 Kim의 폴더
914
+ │ ├── proposal.md
915
+ │ └── zz_ai_1_Talk/ Kim의 AI 대화록
916
+ ├── yy_Coworker_b_Lee/ 협업자 Lee의 폴더
917
+ ├── design.md
918
+ └── zz_ai_1_Talk/ Lee의 AI 대화록
919
+ └── zz_ai_*/ ← 오너의 AI 폴더
894
920
  \`\`\`
895
921
 
896
922
  **중요 규칙:**
897
- - 코워커 폴더(\`yy_Coworker_{폴더명}/\`)는 yy_All_Docu/ 밖에 독립 생성
898
- - 참여자는 Pull로 yy_All_Docu/ 폴더의 오너 문서를 수 있음
899
- - 참여자는 자신의 폴더에만 Push 가능
923
+ - Pull하면 합집합 전체가 내려옴 (오너 + 모든 협업자)
924
+ - Push는 자기 영역만 올라감 (남의 제사상 건드리지 않음)
925
+ - 협업자의 \`zz_ai_*/\`는 자기 폴더 안에 있음 (\`yy_Coworker_{폴더명}/zz_ai_*/\`)
900
926
  - \`docuking_status\`로 현재 권한과 작업 폴더 확인 가능
901
927
 
902
- **참여자가 오너의 파일을 수정하고 싶을 때:**
903
- 1. Pull로 오너의 파일을 로컬에 가져옴 (yy_All_Docu/에 저장됨)
904
- 2. 내용을 참고하여 자신의 폴더에 수정 제안 작성
905
- - 예: \`yy_Coworker_devkim/policy_README_revision.md\`로 작성 후 Push
928
+ **참여자가 남의 파일을 활용하고 싶을 때:**
929
+ 1. Pull로 전체를 로컬에 가져옴
930
+ 2. 경로로 찾아서 읽고 참고
931
+ 3. 수정이 필요하면:
932
+ - 복사해서 자기 폴더에 가공 후 Push
933
+ - 또는 카톡/메일로 원작자에게 전달 → 업데이트 요청
934
+ 4. 이건 스팟한 일, **대세는 킹폴더에 있는 것**
935
+
936
+ **오너 관점에서 협업자 상태 파악:**
937
+ | 상태 | 의미 |
938
+ |------|------|
939
+ | 폴더 없음 | 초대만 함, 아직 수락 안 함 |
940
+ | 빈폴더 | 수락함, 작업 대기 중 |
941
+ | 파일 있음 | 일하고 있음 |
906
942
 
907
943
  **AI가 참여자에게 안내해야 할 내용:**
908
944
  - 참여자의 작업 폴더는 \`yy_Coworker_{폴더명}/\`
909
- - 오너의 파일을 직접 수정할 없으므로, 제안서 형태로 작성하도록 안내
945
+ - 남의 파일은 읽기만 가능, 수정 불가
946
+ - 수정 제안은 자기 폴더에 작성하거나 직접 연락
910
947
 
911
948
  ## AI 응답 가이드 (중요!)
912
949
 
@@ -989,7 +1026,7 @@ AI: (결정이 내려졌으므로 docuking_talk 호출)
989
1026
  })
990
1027
  \`\`\`
991
1028
 
992
- **저장 위치:** \`zz_ai_1_Talk/YYYY-MM-DD_HHMM__제목.md\` (플랫 구조, 로컬 전용)
1029
+ **저장 위치:** \`zz_ai_1_Talk/YYYY-MM-DD_HHMM__제목.md\` (플랫 구조, Push 대상)
993
1030
 
994
1031
  ### 작업 계획 관리 (docuking_plan, docuking_done)
995
1032
 
@@ -1040,7 +1077,7 @@ AI: docuking_done({
1040
1077
  })
1041
1078
  \`\`\`
1042
1079
 
1043
- **저장 위치:** \`zz_ai_3_Plan/YYYY-MM-DD_HHMM__제목__planId.md\` (플랫 구조, 로컬 전용)
1080
+ **저장 위치:** \`zz_ai_3_Plan/YYYY-MM-DD_HHMM__제목__planId.md\` (플랫 구조, Push 대상)
1044
1081
 
1045
1082
  **핵심 가치:**
1046
1083
  - AI 세션이 끊겨도 (컴팩션, 세션 종료) 다음 AI가 계획 문서를 보고 이어서 작업 가능
@@ -1116,15 +1153,15 @@ C:\\Projects\\MyApp\\
1116
1153
  ├── src/
1117
1154
  ├── package.json
1118
1155
  ├── yy_All_Docu/ ← 프로젝트 A와 연결 (동기화)
1119
- ├── zz_ai_1_Talk/ ← AI 대화록 (로컬 전용)
1120
- ├── zz_ai_2_Todo/ ← AI 투두 (로컬 전용)
1121
- └── zz_ai_3_Plan/ ← AI 플랜 (로컬 전용)
1156
+ ├── zz_ai_1_Talk/ ← AI 대화록 (Push 대상)
1157
+ ├── zz_ai_2_Todo/ ← AI 투두 (Push 대상)
1158
+ └── zz_ai_3_Plan/ ← AI 플랜 (Push 대상)
1122
1159
 
1123
1160
  C:\\Projects\\MyWebsite\\
1124
1161
  ├── pages/
1125
1162
  ├── components/
1126
1163
  ├── yy_All_Docu/ ← 프로젝트 B와 연결
1127
- └── zz_ai_*/ ← 로컬 전용 폴더들
1164
+ └── zz_ai_*/ ← AI 작업 폴더들 (Push 대상)
1128
1165
  \`\`\`
1129
1166
 
1130
1167
  **AI가 해야 할 일:**
@@ -1326,9 +1363,9 @@ docuking_init 호출 시 apiKey 파라미터를 포함해주세요.`,
1326
1363
 
1327
1364
  폴더 구조:
1328
1365
  - yy_All_Docu/ : 동기화 대상 (킹폴더)
1329
- - zz_ai_1_Talk/ : AI 대화록 (로컬 전용, 킹톡)
1330
- - zz_ai_2_Todo/ : AI 투두 (로컬 전용, 킹투두)
1331
- - zz_ai_3_Plan/ : AI 플랜 (로컬 전용, 킹플랜)`,
1366
+ - zz_ai_1_Talk/ : AI 대화록 (Push 대상, 킹톡)
1367
+ - zz_ai_2_Todo/ : AI 투두 (Push 대상, 킹투두)
1368
+ - zz_ai_3_Plan/ : AI 플랜 (Push 대상, 킹플랜)`,
1332
1369
  },
1333
1370
  ],
1334
1371
  };
@@ -1386,6 +1423,31 @@ Git처럼 무엇을 변경했는지 명확히 작성해주세요.
1386
1423
  const projectId = projectInfo.projectId;
1387
1424
  const projectName = projectInfo.projectName;
1388
1425
 
1426
+ // ========================================
1427
+ // 1단계: Pull 먼저 실행 (git pull && git push 패턴)
1428
+ // ========================================
1429
+ let pullResultText = '';
1430
+ try {
1431
+ const pullResult = await handlePullInternal({ localPath, filePath });
1432
+ pullResultText = pullResult.text;
1433
+ } catch (e) {
1434
+ return {
1435
+ content: [
1436
+ {
1437
+ type: 'text',
1438
+ text: `❌ Pull 실패로 Push를 중단합니다.
1439
+ 오류: ${e.message}
1440
+
1441
+ 먼저 Pull 문제를 해결한 후 다시 시도하세요.`,
1442
+ },
1443
+ ],
1444
+ };
1445
+ }
1446
+
1447
+ // ========================================
1448
+ // 2단계: Push 진행
1449
+ // ========================================
1450
+
1389
1451
  // Co-worker 권한은 API Key 형식에서 판단
1390
1452
  const { isCoworker, coworkerFolder } = parseCoworkerFromApiKey(apiKey);
1391
1453
  const coworkerFolderName = isCoworker ? `yy_Coworker_${coworkerFolder}` : null;
@@ -1831,11 +1893,15 @@ docuking_init을 먼저 실행하세요.`,
1831
1893
  }
1832
1894
 
1833
1895
  // 4. 서버에만 있고 로컬에 없는 파일 삭제 (Git rm과 동일)
1896
+ // 단, 협업자 폴더(yy_Coworker_*)는 삭제하지 않음 (오너가 협업자 파일을 삭제하면 안됨)
1834
1897
  let deleted = 0;
1835
1898
  const deletedFiles = [];
1836
1899
  if (serverAllPaths.length > 0 && !isCoworker) {
1837
1900
  // 코워커가 아닌 경우에만 삭제 수행 (오너 전용)
1838
- const pathsToDelete = serverAllPaths.filter(p => !processedLocalPaths.has(p));
1901
+ // yy_Coworker_*로 시작하는 경로는 삭제 대상에서 제외
1902
+ const pathsToDelete = serverAllPaths.filter(p =>
1903
+ !processedLocalPaths.has(p) && !p.startsWith('yy_Coworker_')
1904
+ );
1839
1905
 
1840
1906
  if (pathsToDelete.length > 0) {
1841
1907
  try {
@@ -1965,47 +2031,49 @@ docuking_init을 먼저 실행하세요.`,
1965
2031
 
1966
2032
  resultText += `\n\n🌐 웹 탐색기에서 커밋 히스토리를 확인할 수 있습니다: https://docuking.ai`;
1967
2033
 
2034
+ // Pull 결과가 있으면 앞에 추가
2035
+ let finalText = '';
2036
+ if (pullResultText) {
2037
+ // Pull에서 실제로 받은 파일이 있는 경우만 표시
2038
+ const pullHasChanges = pullResultText.includes('다운로드 (신규):') && !pullResultText.includes('다운로드 (신규): 0개');
2039
+ const pullHasUpdates = pullResultText.includes('업데이트 (변경됨):') && !pullResultText.includes('업데이트 (변경됨): 0개');
2040
+
2041
+ if (pullHasChanges || pullHasUpdates) {
2042
+ finalText = `📥 [1단계] Pull (서버 → 로컬)\n${pullResultText}\n\n${'─'.repeat(50)}\n\n📤 [2단계] Push (로컬 → 서버)\n${resultText}`;
2043
+ } else {
2044
+ // Pull에서 변경 없으면 간단히 표시
2045
+ finalText = `📥 Pull: 변경 없음 (최신 상태)\n\n📤 Push:\n${resultText}`;
2046
+ }
2047
+ } else {
2048
+ finalText = resultText;
2049
+ }
2050
+
1968
2051
  return {
1969
2052
  content: [
1970
2053
  {
1971
2054
  type: 'text',
1972
- text: resultText,
2055
+ text: finalText,
1973
2056
  },
1974
2057
  ],
1975
2058
  };
1976
2059
  }
1977
2060
 
1978
- // docuking_pull 구현
1979
- async function handlePull(args) {
1980
- // localPath가 없으면 현재 작업 디렉토리 사용
2061
+ // docuking_pull 내부 구현 (Push에서도 호출)
2062
+ // 반환값: { text: 결과문자열, downloaded, updated, skipped, failed }
2063
+ async function handlePullInternal(args) {
1981
2064
  const localPath = args.localPath || process.cwd();
1982
2065
  const { filePath } = args;
1983
2066
 
1984
2067
  // 로컬 config에서 API 키 읽기
1985
2068
  const apiKey = getApiKey(localPath);
1986
2069
  if (!apiKey) {
1987
- return {
1988
- content: [
1989
- {
1990
- type: 'text',
1991
- text: `오류: API 키를 찾을 수 없습니다.
1992
- 먼저 docuking_init을 실행하세요.`,
1993
- },
1994
- ],
1995
- };
2070
+ throw new Error('API 키를 찾을 수 없습니다. 먼저 docuking_init을 실행하세요.');
1996
2071
  }
1997
2072
 
1998
2073
  // 프로젝트 정보 조회 (로컬 config에서)
1999
2074
  const projectInfo = getProjectInfo(localPath);
2000
2075
  if (projectInfo.error) {
2001
- return {
2002
- content: [
2003
- {
2004
- type: 'text',
2005
- text: projectInfo.error,
2006
- },
2007
- ],
2008
- };
2076
+ throw new Error(projectInfo.error);
2009
2077
  }
2010
2078
 
2011
2079
  const projectId = projectInfo.projectId;
@@ -2037,14 +2105,7 @@ async function handlePull(args) {
2037
2105
  const data = await response.json();
2038
2106
  files = flattenTree(data.tree || []);
2039
2107
  } catch (e) {
2040
- return {
2041
- content: [
2042
- {
2043
- type: 'text',
2044
- text: `오류: 파일 목록 조회 실패 - ${e.message}`,
2045
- },
2046
- ],
2047
- };
2108
+ throw new Error(`파일 목록 조회 실패 - ${e.message}`);
2048
2109
  }
2049
2110
 
2050
2111
  if (filePath) {
@@ -2052,19 +2113,30 @@ async function handlePull(args) {
2052
2113
  }
2053
2114
 
2054
2115
  if (files.length === 0) {
2116
+ // 빈 파일은 에러가 아님 - 정상 결과 반환
2055
2117
  return {
2056
- content: [
2057
- {
2058
- type: 'text',
2059
- text: 'Pull할 파일이 없습니다.',
2060
- },
2061
- ],
2118
+ text: 'Pull할 파일이 없습니다.',
2119
+ total: 0,
2120
+ downloaded: 0,
2121
+ updated: 0,
2122
+ skipped: 0,
2123
+ failed: 0,
2062
2124
  };
2063
2125
  }
2064
2126
 
2065
2127
  // 파일 다운로드
2066
2128
  const results = [];
2129
+ const total = files.length;
2130
+ let current = 0;
2131
+ let downloaded = 0; // 새로 다운로드
2132
+ let updated = 0; // 업데이트 (변경됨)
2133
+ let skipped = 0; // 스킵 (변경 없음)
2134
+ let failed = 0; // 실패
2135
+
2067
2136
  for (const file of files) {
2137
+ current++;
2138
+ const progress = `${current}/${total}`;
2139
+
2068
2140
  try {
2069
2141
  const response = await fetch(
2070
2142
  `${API_ENDPOINT}/files?projectId=${projectId}&path=${encodeURIComponent(file.path)}`,
@@ -2076,7 +2148,8 @@ async function handlePull(args) {
2076
2148
  );
2077
2149
 
2078
2150
  if (!response.ok) {
2079
- results.push(`✗ ${file.path}: ${await response.text()}`);
2151
+ results.push({ type: 'fail', path: file.path, error: await response.text() });
2152
+ failed++;
2080
2153
  continue;
2081
2154
  }
2082
2155
 
@@ -2098,53 +2171,157 @@ async function handlePull(args) {
2098
2171
  fullPath = path.join(mainFolderPath, file.path);
2099
2172
  }
2100
2173
 
2101
- // 디렉토리 생성
2102
- fs.mkdirSync(path.dirname(fullPath), { recursive: true });
2103
-
2104
2174
  // 인코딩에 따라 저장
2105
2175
  const content = data.file?.content || data.content || '';
2106
2176
  const encoding = data.file?.encoding || data.encoding || 'utf-8';
2107
2177
 
2178
+ // 서버 파일 해시 계산
2179
+ let serverBuffer;
2108
2180
  if (encoding === 'base64') {
2109
- // Base64 디코딩 후 바이너리로 저장
2110
- const buffer = Buffer.from(content, 'base64');
2111
- fs.writeFileSync(fullPath, buffer);
2181
+ serverBuffer = Buffer.from(content, 'base64');
2182
+ } else {
2183
+ serverBuffer = Buffer.from(content, 'utf-8');
2184
+ }
2185
+ const serverHash = crypto.createHash('sha256').update(serverBuffer).digest('hex');
2186
+
2187
+ // 로컬 파일 존재 여부 및 해시 비교
2188
+ let localExists = fs.existsSync(fullPath);
2189
+ let localHash = null;
2190
+
2191
+ if (localExists) {
2192
+ try {
2193
+ const localBuffer = fs.readFileSync(fullPath);
2194
+ localHash = crypto.createHash('sha256').update(localBuffer).digest('hex');
2195
+ } catch (e) {
2196
+ localExists = false;
2197
+ }
2198
+ }
2199
+
2200
+ // 변경 감지
2201
+ if (localExists && localHash === serverHash) {
2202
+ // 변경 없음 - 스킵
2203
+ results.push({ type: 'skip', path: file.path });
2204
+ skipped++;
2205
+ continue;
2206
+ }
2207
+
2208
+ // 디렉토리 생성
2209
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
2210
+
2211
+ // 파일 저장
2212
+ if (encoding === 'base64') {
2213
+ fs.writeFileSync(fullPath, serverBuffer);
2112
2214
  } else {
2113
- // UTF-8 텍스트로 저장
2114
2215
  fs.writeFileSync(fullPath, content, 'utf-8');
2115
2216
  }
2116
2217
 
2117
- results.push(`✓ ${file.path}`);
2218
+ if (localExists) {
2219
+ // 기존 파일 업데이트
2220
+ results.push({ type: 'update', path: file.path });
2221
+ updated++;
2222
+ } else {
2223
+ // 새로 다운로드
2224
+ results.push({ type: 'download', path: file.path });
2225
+ downloaded++;
2226
+ }
2118
2227
  } catch (e) {
2119
- results.push(`✗ ${file.path}: ${e.message}`);
2228
+ results.push({ type: 'fail', path: file.path, error: e.message });
2229
+ failed++;
2120
2230
  }
2121
2231
  }
2122
2232
 
2123
- // 결과 요약
2124
- const successCount = results.filter(r => r.startsWith('✓')).length;
2125
- const failCount = results.filter(r => r.startsWith('✗')).length;
2233
+ // 상세 결과 구성 (Push 스타일)
2234
+ let resultText = `✓ Pull 완료!
2126
2235
 
2127
- // 실패한 파일만 상세 표시, 성공은 개수만
2128
- const failedFiles = results.filter(r => r.startsWith('✗'));
2236
+ 📊 처리 결과:
2237
+ - 파일: ${total}개
2238
+ - 다운로드 (신규): ${downloaded}개
2239
+ - 업데이트 (변경됨): ${updated}개
2240
+ - 스킵 (변경 없음): ${skipped}개
2241
+ - 실패: ${failed}개`;
2129
2242
 
2130
- let summary = `Pull 완료! (${successCount}개 성공`;
2131
- if (failCount > 0) {
2132
- summary += `, ${failCount}개 실패)`;
2133
- summary += `\n\n실패 목록:\n${failedFiles.join('\n')}`;
2134
- } else {
2135
- summary += ')';
2243
+ // 다운로드된 파일 목록
2244
+ const downloadedFiles = results.filter(r => r.type === 'download');
2245
+ if (downloadedFiles.length > 0) {
2246
+ resultText += `\n\n📥 다운로드된 파일 (${downloadedFiles.length}개):`;
2247
+ downloadedFiles.slice(0, 20).forEach(f => {
2248
+ resultText += `\n ✓ ${f.path}`;
2249
+ });
2250
+ if (downloadedFiles.length > 20) {
2251
+ resultText += `\n ... 외 ${downloadedFiles.length - 20}개`;
2252
+ }
2253
+ }
2254
+
2255
+ // 업데이트된 파일 목록
2256
+ const updatedFiles = results.filter(r => r.type === 'update');
2257
+ if (updatedFiles.length > 0) {
2258
+ resultText += `\n\n🔄 업데이트된 파일 (${updatedFiles.length}개):`;
2259
+ updatedFiles.slice(0, 20).forEach(f => {
2260
+ resultText += `\n ↻ ${f.path}`;
2261
+ });
2262
+ if (updatedFiles.length > 20) {
2263
+ resultText += `\n ... 외 ${updatedFiles.length - 20}개`;
2264
+ }
2265
+ }
2266
+
2267
+ // 스킵된 파일 목록
2268
+ const skippedFiles = results.filter(r => r.type === 'skip');
2269
+ if (skippedFiles.length > 0) {
2270
+ resultText += `\n\n⏭️ 스킵된 파일 (${skippedFiles.length}개, 변경 없음):`;
2271
+ skippedFiles.slice(0, 10).forEach(f => {
2272
+ resultText += `\n ⊘ ${f.path}`;
2273
+ });
2274
+ if (skippedFiles.length > 10) {
2275
+ resultText += `\n ... 외 ${skippedFiles.length - 10}개`;
2276
+ }
2136
2277
  }
2137
2278
 
2279
+ // 실패한 파일 목록
2280
+ const failedFiles = results.filter(r => r.type === 'fail');
2281
+ if (failedFiles.length > 0) {
2282
+ resultText += `\n\n❌ 실패한 파일 (${failedFiles.length}개):`;
2283
+ failedFiles.forEach(f => {
2284
+ resultText += `\n ✗ ${f.path}: ${f.error}`;
2285
+ });
2286
+ }
2287
+
2288
+ resultText += `\n\n🌐 웹 탐색기: https://docuking.ai`;
2289
+
2290
+ // 내부용 객체 반환 (Push에서 사용)
2138
2291
  return {
2139
- content: [
2140
- {
2141
- type: 'text',
2142
- text: summary,
2143
- },
2144
- ],
2292
+ text: resultText,
2293
+ total,
2294
+ downloaded,
2295
+ updated,
2296
+ skipped,
2297
+ failed,
2145
2298
  };
2146
2299
  }
2147
2300
 
2301
+ // docuking_pull 구현 (MCP 도구용 래퍼)
2302
+ async function handlePull(args) {
2303
+ try {
2304
+ const result = await handlePullInternal(args);
2305
+ return {
2306
+ content: [
2307
+ {
2308
+ type: 'text',
2309
+ text: result.text,
2310
+ },
2311
+ ],
2312
+ };
2313
+ } catch (e) {
2314
+ return {
2315
+ content: [
2316
+ {
2317
+ type: 'text',
2318
+ text: `오류: ${e.message}`,
2319
+ },
2320
+ ],
2321
+ };
2322
+ }
2323
+ }
2324
+
2148
2325
  // docuking_list 구현
2149
2326
  async function handleList(args) {
2150
2327
  const localPath = args.localPath || process.cwd();
@@ -2368,10 +2545,9 @@ function collectFilesSimple(basePath, relativePath, results, excludedFiles = nul
2368
2545
  }
2369
2546
  }
2370
2547
 
2371
- // 유틸: 디렉토리 재귀 탐색 (기존 호환용)
2548
+ // 유틸: 디렉토리 재귀 탐색 (기존 호환용, 현재 미사용)
2372
2549
  // excludedFiles: 제외된 파일 목록을 수집할 배열 (선택)
2373
2550
  // largeFiles: 대용량 파일 목록을 수집할 배열 (선택)
2374
- // zz_* 폴더는 로컬 전용이므로 동기화에서 제외
2375
2551
  function collectFiles(basePath, relativePath, results, excludedFiles = null, largeFiles = null) {
2376
2552
  const fullPath = path.join(basePath, relativePath);
2377
2553
  const entries = fs.readdirSync(fullPath, { withFileTypes: true });
@@ -2380,11 +2556,6 @@ function collectFiles(basePath, relativePath, results, excludedFiles = null, lar
2380
2556
  const entryRelPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
2381
2557
 
2382
2558
  if (entry.isDirectory()) {
2383
- // zz_* 폴더는 로컬 전용 (AI 작업 기록 등)이므로 동기화에서 제외
2384
- if (entry.name.startsWith('zz_')) {
2385
- console.error(`[DocuKing] 제외됨: ${entryRelPath}/ (zz_* 폴더 - 로컬 전용)`);
2386
- continue;
2387
- }
2388
2559
  collectFiles(basePath, entryRelPath, results, excludedFiles, largeFiles);
2389
2560
  } else if (entry.isFile()) {
2390
2561
  const fileType = getFileType(entry.name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docuking-mcp",
3
- "version": "2.5.3",
3
+ "version": "2.5.5",
4
4
  "description": "DocuKing MCP Server - AI 시대의 문서 협업 플랫폼",
5
5
  "type": "module",
6
6
  "main": "index.js",