docuking-mcp 2.5.4 → 2.5.6
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/index.js +285 -87
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -392,7 +392,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
392
392
|
},
|
|
393
393
|
{
|
|
394
394
|
name: 'docuking_push',
|
|
395
|
-
description:
|
|
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: {
|
|
@@ -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
|
-
-
|
|
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
|
|
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/ ← 오너 문서 (
|
|
908
|
+
├── yy_All_Docu/ ← 오너 문서 (읽기만)
|
|
884
909
|
│ ├── policy/
|
|
885
910
|
│ │ └── README.md ← 오너의 파일
|
|
886
911
|
│ └── plan/
|
|
887
912
|
│ └── requirements.md ← 오너의 파일
|
|
888
|
-
├──
|
|
889
|
-
│ ├── proposal.md
|
|
890
|
-
│ └──
|
|
891
|
-
├──
|
|
892
|
-
├──
|
|
893
|
-
└──
|
|
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
|
-
-
|
|
898
|
-
-
|
|
899
|
-
-
|
|
923
|
+
- Pull하면 합집합 전체가 내려옴 (오너 + 모든 협업자)
|
|
924
|
+
- Push는 자기 영역만 올라감 (남의 제사상 건드리지 않음)
|
|
925
|
+
- 협업자의 \`zz_ai_*/\`는 자기 폴더 안에 있음 (\`yy_Coworker_{폴더명}/zz_ai_*/\`)
|
|
900
926
|
- \`docuking_status\`로 현재 권한과 작업 폴더 확인 가능
|
|
901
927
|
|
|
902
|
-
**참여자가
|
|
903
|
-
1. Pull로
|
|
904
|
-
2.
|
|
905
|
-
|
|
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
|
|
|
@@ -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;
|
|
@@ -1398,13 +1460,25 @@ Git처럼 무엇을 변경했는지 명확히 작성해주세요.
|
|
|
1398
1460
|
// Push 대상 폴더 목록 (프로젝트 루트 기준)
|
|
1399
1461
|
let pushTargetFolders = [];
|
|
1400
1462
|
|
|
1463
|
+
// 디버그: Push 대상 판단 로그
|
|
1464
|
+
console.error(`[DocuKing] Push 권한: isCoworker=${isCoworker}, coworkerFolder=${coworkerFolder}, coworkerFolderName=${coworkerFolderName}`);
|
|
1465
|
+
|
|
1401
1466
|
if (isCoworker) {
|
|
1402
1467
|
// 협업자: yy_Coworker_{폴더명}/ 폴더만 Push
|
|
1403
1468
|
const coworkerPath = path.join(localPath, coworkerFolderName);
|
|
1469
|
+
console.error(`[DocuKing] 협업자 Push 대상 폴더: ${coworkerPath}`);
|
|
1404
1470
|
if (!fs.existsSync(coworkerPath)) {
|
|
1405
1471
|
fs.mkdirSync(coworkerPath, { recursive: true });
|
|
1406
1472
|
}
|
|
1407
1473
|
pushTargetFolders.push({ localPath: coworkerPath, serverPrefix: coworkerFolderName });
|
|
1474
|
+
|
|
1475
|
+
// 협업자 폴더 내용 디버그 출력
|
|
1476
|
+
try {
|
|
1477
|
+
const entries = fs.readdirSync(coworkerPath, { withFileTypes: true });
|
|
1478
|
+
console.error(`[DocuKing] 협업자 폴더 내용: ${entries.map(e => e.name + (e.isDirectory() ? '/' : '')).join(', ')}`);
|
|
1479
|
+
} catch (e) {
|
|
1480
|
+
console.error(`[DocuKing] 협업자 폴더 읽기 실패: ${e.message}`);
|
|
1481
|
+
}
|
|
1408
1482
|
} else {
|
|
1409
1483
|
// 오너: yy_All_Docu/ 폴더 확인
|
|
1410
1484
|
if (!fs.existsSync(mainFolderPath)) {
|
|
@@ -1571,6 +1645,15 @@ docuking_init을 먼저 실행하세요.`,
|
|
|
1571
1645
|
const folderFiles = [];
|
|
1572
1646
|
collectFilesSimple(targetFolder.localPath, '', folderFiles, excludedFiles, largeFiles);
|
|
1573
1647
|
|
|
1648
|
+
// 디버그: 수집된 파일 목록 출력
|
|
1649
|
+
console.error(`[DocuKing] ${targetFolder.serverPrefix}/ 에서 수집된 파일 ${folderFiles.length}개:`);
|
|
1650
|
+
for (const f of folderFiles.slice(0, 10)) {
|
|
1651
|
+
console.error(` - ${f.path}`);
|
|
1652
|
+
}
|
|
1653
|
+
if (folderFiles.length > 10) {
|
|
1654
|
+
console.error(` ... 외 ${folderFiles.length - 10}개`);
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1574
1657
|
// 서버 경로 추가: prefix/상대경로 (예: yy_All_Docu/회의록/test.md)
|
|
1575
1658
|
for (const file of folderFiles) {
|
|
1576
1659
|
file.serverPath = `${targetFolder.serverPrefix}/${file.path}`;
|
|
@@ -1831,11 +1914,15 @@ docuking_init을 먼저 실행하세요.`,
|
|
|
1831
1914
|
}
|
|
1832
1915
|
|
|
1833
1916
|
// 4. 서버에만 있고 로컬에 없는 파일 삭제 (Git rm과 동일)
|
|
1917
|
+
// 단, 협업자 폴더(yy_Coworker_*)는 삭제하지 않음 (오너가 협업자 파일을 삭제하면 안됨)
|
|
1834
1918
|
let deleted = 0;
|
|
1835
1919
|
const deletedFiles = [];
|
|
1836
1920
|
if (serverAllPaths.length > 0 && !isCoworker) {
|
|
1837
1921
|
// 코워커가 아닌 경우에만 삭제 수행 (오너 전용)
|
|
1838
|
-
|
|
1922
|
+
// yy_Coworker_*로 시작하는 경로는 삭제 대상에서 제외
|
|
1923
|
+
const pathsToDelete = serverAllPaths.filter(p =>
|
|
1924
|
+
!processedLocalPaths.has(p) && !p.startsWith('yy_Coworker_')
|
|
1925
|
+
);
|
|
1839
1926
|
|
|
1840
1927
|
if (pathsToDelete.length > 0) {
|
|
1841
1928
|
try {
|
|
@@ -1965,47 +2052,49 @@ docuking_init을 먼저 실행하세요.`,
|
|
|
1965
2052
|
|
|
1966
2053
|
resultText += `\n\n🌐 웹 탐색기에서 커밋 히스토리를 확인할 수 있습니다: https://docuking.ai`;
|
|
1967
2054
|
|
|
2055
|
+
// Pull 결과가 있으면 앞에 추가
|
|
2056
|
+
let finalText = '';
|
|
2057
|
+
if (pullResultText) {
|
|
2058
|
+
// Pull에서 실제로 받은 파일이 있는 경우만 표시
|
|
2059
|
+
const pullHasChanges = pullResultText.includes('다운로드 (신규):') && !pullResultText.includes('다운로드 (신규): 0개');
|
|
2060
|
+
const pullHasUpdates = pullResultText.includes('업데이트 (변경됨):') && !pullResultText.includes('업데이트 (변경됨): 0개');
|
|
2061
|
+
|
|
2062
|
+
if (pullHasChanges || pullHasUpdates) {
|
|
2063
|
+
finalText = `📥 [1단계] Pull (서버 → 로컬)\n${pullResultText}\n\n${'─'.repeat(50)}\n\n📤 [2단계] Push (로컬 → 서버)\n${resultText}`;
|
|
2064
|
+
} else {
|
|
2065
|
+
// Pull에서 변경 없으면 간단히 표시
|
|
2066
|
+
finalText = `📥 Pull: 변경 없음 (최신 상태)\n\n📤 Push:\n${resultText}`;
|
|
2067
|
+
}
|
|
2068
|
+
} else {
|
|
2069
|
+
finalText = resultText;
|
|
2070
|
+
}
|
|
2071
|
+
|
|
1968
2072
|
return {
|
|
1969
2073
|
content: [
|
|
1970
2074
|
{
|
|
1971
2075
|
type: 'text',
|
|
1972
|
-
text:
|
|
2076
|
+
text: finalText,
|
|
1973
2077
|
},
|
|
1974
2078
|
],
|
|
1975
2079
|
};
|
|
1976
2080
|
}
|
|
1977
2081
|
|
|
1978
|
-
// docuking_pull 구현
|
|
1979
|
-
|
|
1980
|
-
|
|
2082
|
+
// docuking_pull 내부 구현 (Push에서도 호출)
|
|
2083
|
+
// 반환값: { text: 결과문자열, downloaded, updated, skipped, failed }
|
|
2084
|
+
async function handlePullInternal(args) {
|
|
1981
2085
|
const localPath = args.localPath || process.cwd();
|
|
1982
2086
|
const { filePath } = args;
|
|
1983
2087
|
|
|
1984
2088
|
// 로컬 config에서 API 키 읽기
|
|
1985
2089
|
const apiKey = getApiKey(localPath);
|
|
1986
2090
|
if (!apiKey) {
|
|
1987
|
-
|
|
1988
|
-
content: [
|
|
1989
|
-
{
|
|
1990
|
-
type: 'text',
|
|
1991
|
-
text: `오류: API 키를 찾을 수 없습니다.
|
|
1992
|
-
먼저 docuking_init을 실행하세요.`,
|
|
1993
|
-
},
|
|
1994
|
-
],
|
|
1995
|
-
};
|
|
2091
|
+
throw new Error('API 키를 찾을 수 없습니다. 먼저 docuking_init을 실행하세요.');
|
|
1996
2092
|
}
|
|
1997
2093
|
|
|
1998
2094
|
// 프로젝트 정보 조회 (로컬 config에서)
|
|
1999
2095
|
const projectInfo = getProjectInfo(localPath);
|
|
2000
2096
|
if (projectInfo.error) {
|
|
2001
|
-
|
|
2002
|
-
content: [
|
|
2003
|
-
{
|
|
2004
|
-
type: 'text',
|
|
2005
|
-
text: projectInfo.error,
|
|
2006
|
-
},
|
|
2007
|
-
],
|
|
2008
|
-
};
|
|
2097
|
+
throw new Error(projectInfo.error);
|
|
2009
2098
|
}
|
|
2010
2099
|
|
|
2011
2100
|
const projectId = projectInfo.projectId;
|
|
@@ -2037,14 +2126,7 @@ async function handlePull(args) {
|
|
|
2037
2126
|
const data = await response.json();
|
|
2038
2127
|
files = flattenTree(data.tree || []);
|
|
2039
2128
|
} catch (e) {
|
|
2040
|
-
|
|
2041
|
-
content: [
|
|
2042
|
-
{
|
|
2043
|
-
type: 'text',
|
|
2044
|
-
text: `오류: 파일 목록 조회 실패 - ${e.message}`,
|
|
2045
|
-
},
|
|
2046
|
-
],
|
|
2047
|
-
};
|
|
2129
|
+
throw new Error(`파일 목록 조회 실패 - ${e.message}`);
|
|
2048
2130
|
}
|
|
2049
2131
|
|
|
2050
2132
|
if (filePath) {
|
|
@@ -2052,19 +2134,30 @@ async function handlePull(args) {
|
|
|
2052
2134
|
}
|
|
2053
2135
|
|
|
2054
2136
|
if (files.length === 0) {
|
|
2137
|
+
// 빈 파일은 에러가 아님 - 정상 결과 반환
|
|
2055
2138
|
return {
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2139
|
+
text: 'Pull할 파일이 없습니다.',
|
|
2140
|
+
total: 0,
|
|
2141
|
+
downloaded: 0,
|
|
2142
|
+
updated: 0,
|
|
2143
|
+
skipped: 0,
|
|
2144
|
+
failed: 0,
|
|
2062
2145
|
};
|
|
2063
2146
|
}
|
|
2064
2147
|
|
|
2065
2148
|
// 파일 다운로드
|
|
2066
2149
|
const results = [];
|
|
2150
|
+
const total = files.length;
|
|
2151
|
+
let current = 0;
|
|
2152
|
+
let downloaded = 0; // 새로 다운로드
|
|
2153
|
+
let updated = 0; // 업데이트 (변경됨)
|
|
2154
|
+
let skipped = 0; // 스킵 (변경 없음)
|
|
2155
|
+
let failed = 0; // 실패
|
|
2156
|
+
|
|
2067
2157
|
for (const file of files) {
|
|
2158
|
+
current++;
|
|
2159
|
+
const progress = `${current}/${total}`;
|
|
2160
|
+
|
|
2068
2161
|
try {
|
|
2069
2162
|
const response = await fetch(
|
|
2070
2163
|
`${API_ENDPOINT}/files?projectId=${projectId}&path=${encodeURIComponent(file.path)}`,
|
|
@@ -2076,7 +2169,8 @@ async function handlePull(args) {
|
|
|
2076
2169
|
);
|
|
2077
2170
|
|
|
2078
2171
|
if (!response.ok) {
|
|
2079
|
-
results.push(
|
|
2172
|
+
results.push({ type: 'fail', path: file.path, error: await response.text() });
|
|
2173
|
+
failed++;
|
|
2080
2174
|
continue;
|
|
2081
2175
|
}
|
|
2082
2176
|
|
|
@@ -2098,53 +2192,157 @@ async function handlePull(args) {
|
|
|
2098
2192
|
fullPath = path.join(mainFolderPath, file.path);
|
|
2099
2193
|
}
|
|
2100
2194
|
|
|
2101
|
-
// 디렉토리 생성
|
|
2102
|
-
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
2103
|
-
|
|
2104
2195
|
// 인코딩에 따라 저장
|
|
2105
2196
|
const content = data.file?.content || data.content || '';
|
|
2106
2197
|
const encoding = data.file?.encoding || data.encoding || 'utf-8';
|
|
2107
2198
|
|
|
2199
|
+
// 서버 파일 해시 계산
|
|
2200
|
+
let serverBuffer;
|
|
2201
|
+
if (encoding === 'base64') {
|
|
2202
|
+
serverBuffer = Buffer.from(content, 'base64');
|
|
2203
|
+
} else {
|
|
2204
|
+
serverBuffer = Buffer.from(content, 'utf-8');
|
|
2205
|
+
}
|
|
2206
|
+
const serverHash = crypto.createHash('sha256').update(serverBuffer).digest('hex');
|
|
2207
|
+
|
|
2208
|
+
// 로컬 파일 존재 여부 및 해시 비교
|
|
2209
|
+
let localExists = fs.existsSync(fullPath);
|
|
2210
|
+
let localHash = null;
|
|
2211
|
+
|
|
2212
|
+
if (localExists) {
|
|
2213
|
+
try {
|
|
2214
|
+
const localBuffer = fs.readFileSync(fullPath);
|
|
2215
|
+
localHash = crypto.createHash('sha256').update(localBuffer).digest('hex');
|
|
2216
|
+
} catch (e) {
|
|
2217
|
+
localExists = false;
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
// 변경 감지
|
|
2222
|
+
if (localExists && localHash === serverHash) {
|
|
2223
|
+
// 변경 없음 - 스킵
|
|
2224
|
+
results.push({ type: 'skip', path: file.path });
|
|
2225
|
+
skipped++;
|
|
2226
|
+
continue;
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
// 디렉토리 생성
|
|
2230
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
2231
|
+
|
|
2232
|
+
// 파일 저장
|
|
2108
2233
|
if (encoding === 'base64') {
|
|
2109
|
-
|
|
2110
|
-
const buffer = Buffer.from(content, 'base64');
|
|
2111
|
-
fs.writeFileSync(fullPath, buffer);
|
|
2234
|
+
fs.writeFileSync(fullPath, serverBuffer);
|
|
2112
2235
|
} else {
|
|
2113
|
-
// UTF-8 텍스트로 저장
|
|
2114
2236
|
fs.writeFileSync(fullPath, content, 'utf-8');
|
|
2115
2237
|
}
|
|
2116
2238
|
|
|
2117
|
-
|
|
2239
|
+
if (localExists) {
|
|
2240
|
+
// 기존 파일 업데이트
|
|
2241
|
+
results.push({ type: 'update', path: file.path });
|
|
2242
|
+
updated++;
|
|
2243
|
+
} else {
|
|
2244
|
+
// 새로 다운로드
|
|
2245
|
+
results.push({ type: 'download', path: file.path });
|
|
2246
|
+
downloaded++;
|
|
2247
|
+
}
|
|
2118
2248
|
} catch (e) {
|
|
2119
|
-
results.push(
|
|
2249
|
+
results.push({ type: 'fail', path: file.path, error: e.message });
|
|
2250
|
+
failed++;
|
|
2120
2251
|
}
|
|
2121
2252
|
}
|
|
2122
2253
|
|
|
2123
|
-
// 결과
|
|
2124
|
-
|
|
2125
|
-
const failCount = results.filter(r => r.startsWith('✗')).length;
|
|
2254
|
+
// 상세 결과 구성 (Push 스타일)
|
|
2255
|
+
let resultText = `✓ Pull 완료!
|
|
2126
2256
|
|
|
2127
|
-
|
|
2128
|
-
|
|
2257
|
+
📊 처리 결과:
|
|
2258
|
+
- 총 파일: ${total}개
|
|
2259
|
+
- 다운로드 (신규): ${downloaded}개
|
|
2260
|
+
- 업데이트 (변경됨): ${updated}개
|
|
2261
|
+
- 스킵 (변경 없음): ${skipped}개
|
|
2262
|
+
- 실패: ${failed}개`;
|
|
2129
2263
|
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2264
|
+
// 다운로드된 파일 목록
|
|
2265
|
+
const downloadedFiles = results.filter(r => r.type === 'download');
|
|
2266
|
+
if (downloadedFiles.length > 0) {
|
|
2267
|
+
resultText += `\n\n📥 다운로드된 파일 (${downloadedFiles.length}개):`;
|
|
2268
|
+
downloadedFiles.slice(0, 20).forEach(f => {
|
|
2269
|
+
resultText += `\n ✓ ${f.path}`;
|
|
2270
|
+
});
|
|
2271
|
+
if (downloadedFiles.length > 20) {
|
|
2272
|
+
resultText += `\n ... 외 ${downloadedFiles.length - 20}개`;
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
// 업데이트된 파일 목록
|
|
2277
|
+
const updatedFiles = results.filter(r => r.type === 'update');
|
|
2278
|
+
if (updatedFiles.length > 0) {
|
|
2279
|
+
resultText += `\n\n🔄 업데이트된 파일 (${updatedFiles.length}개):`;
|
|
2280
|
+
updatedFiles.slice(0, 20).forEach(f => {
|
|
2281
|
+
resultText += `\n ↻ ${f.path}`;
|
|
2282
|
+
});
|
|
2283
|
+
if (updatedFiles.length > 20) {
|
|
2284
|
+
resultText += `\n ... 외 ${updatedFiles.length - 20}개`;
|
|
2285
|
+
}
|
|
2136
2286
|
}
|
|
2137
2287
|
|
|
2288
|
+
// 스킵된 파일 목록
|
|
2289
|
+
const skippedFiles = results.filter(r => r.type === 'skip');
|
|
2290
|
+
if (skippedFiles.length > 0) {
|
|
2291
|
+
resultText += `\n\n⏭️ 스킵된 파일 (${skippedFiles.length}개, 변경 없음):`;
|
|
2292
|
+
skippedFiles.slice(0, 10).forEach(f => {
|
|
2293
|
+
resultText += `\n ⊘ ${f.path}`;
|
|
2294
|
+
});
|
|
2295
|
+
if (skippedFiles.length > 10) {
|
|
2296
|
+
resultText += `\n ... 외 ${skippedFiles.length - 10}개`;
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
// 실패한 파일 목록
|
|
2301
|
+
const failedFiles = results.filter(r => r.type === 'fail');
|
|
2302
|
+
if (failedFiles.length > 0) {
|
|
2303
|
+
resultText += `\n\n❌ 실패한 파일 (${failedFiles.length}개):`;
|
|
2304
|
+
failedFiles.forEach(f => {
|
|
2305
|
+
resultText += `\n ✗ ${f.path}: ${f.error}`;
|
|
2306
|
+
});
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
resultText += `\n\n🌐 웹 탐색기: https://docuking.ai`;
|
|
2310
|
+
|
|
2311
|
+
// 내부용 객체 반환 (Push에서 사용)
|
|
2138
2312
|
return {
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2313
|
+
text: resultText,
|
|
2314
|
+
total,
|
|
2315
|
+
downloaded,
|
|
2316
|
+
updated,
|
|
2317
|
+
skipped,
|
|
2318
|
+
failed,
|
|
2145
2319
|
};
|
|
2146
2320
|
}
|
|
2147
2321
|
|
|
2322
|
+
// docuking_pull 구현 (MCP 도구용 래퍼)
|
|
2323
|
+
async function handlePull(args) {
|
|
2324
|
+
try {
|
|
2325
|
+
const result = await handlePullInternal(args);
|
|
2326
|
+
return {
|
|
2327
|
+
content: [
|
|
2328
|
+
{
|
|
2329
|
+
type: 'text',
|
|
2330
|
+
text: result.text,
|
|
2331
|
+
},
|
|
2332
|
+
],
|
|
2333
|
+
};
|
|
2334
|
+
} catch (e) {
|
|
2335
|
+
return {
|
|
2336
|
+
content: [
|
|
2337
|
+
{
|
|
2338
|
+
type: 'text',
|
|
2339
|
+
text: `오류: ${e.message}`,
|
|
2340
|
+
},
|
|
2341
|
+
],
|
|
2342
|
+
};
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2148
2346
|
// docuking_list 구현
|
|
2149
2347
|
async function handleList(args) {
|
|
2150
2348
|
const localPath = args.localPath || process.cwd();
|