docuking-mcp 3.6.0 → 3.7.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.
- package/handlers/sync.js +132 -15
- package/index.js +27 -3
- package/lib/init.js +14 -12
- package/package.json +1 -1
package/handlers/sync.js
CHANGED
|
@@ -1326,16 +1326,29 @@ export async function handlePullInternal(args) {
|
|
|
1326
1326
|
}
|
|
1327
1327
|
|
|
1328
1328
|
// ========================================
|
|
1329
|
-
// [
|
|
1330
|
-
// 해시가 다르면 →
|
|
1331
|
-
//
|
|
1332
|
-
//
|
|
1329
|
+
// [v3.7.0] 최신 파일 보존 정책
|
|
1330
|
+
// 해시가 다르면 → timestamp 비교해서 최신 파일 유지
|
|
1331
|
+
// 로컬이 더 최신 → 로컬 유지 (다음 Push에서 업로드)
|
|
1332
|
+
// 서버가 더 최신 → 서버 버전 다운로드
|
|
1333
1333
|
// ========================================
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1334
|
+
const serverMeta = serverPathToMeta[file.path];
|
|
1335
|
+
const serverUpdatedAt = serverMeta?.updatedAt ? new Date(serverMeta.updatedAt).getTime() : 0;
|
|
1336
|
+
const localMtime = localStat.mtimeMs;
|
|
1337
|
+
|
|
1338
|
+
if (localMtime >= serverUpdatedAt) {
|
|
1339
|
+
// 로컬이 더 최신 (또는 동일) → 로컬 유지
|
|
1340
|
+
console.error(`[DocuKing] 로컬 최신: ${file.path} (local: ${new Date(localMtime).toISOString()}, server: ${serverMeta?.updatedAt || 'N/A'})`);
|
|
1341
|
+
results.push({ type: 'skip', path: file.path, reason: 'local-newer' });
|
|
1342
|
+
skipped++;
|
|
1343
|
+
current++;
|
|
1344
|
+
continue;
|
|
1345
|
+
} else {
|
|
1346
|
+
// 서버가 더 최신 → 다운로드 대상에 추가
|
|
1347
|
+
console.error(`[DocuKing] 서버 최신: ${file.path} (local: ${new Date(localMtime).toISOString()}, server: ${serverMeta?.updatedAt || 'N/A'})`);
|
|
1348
|
+
filesToDownload.push({ ...file, fullPath });
|
|
1349
|
+
current++;
|
|
1350
|
+
continue;
|
|
1351
|
+
}
|
|
1339
1352
|
} catch (e) {
|
|
1340
1353
|
// 해시 계산 실패 시 다운로드 대상
|
|
1341
1354
|
}
|
|
@@ -1986,17 +1999,115 @@ export async function handleStatus(args) {
|
|
|
1986
1999
|
pushableFiles = localFiles; // 오너는 모든 파일 Push 가능
|
|
1987
2000
|
}
|
|
1988
2001
|
|
|
2002
|
+
// 서버 해시 정보 조회 (변경 감지용)
|
|
2003
|
+
let serverPathToHash = {};
|
|
2004
|
+
try {
|
|
2005
|
+
const hashResponse = await fetch(
|
|
2006
|
+
`${API_ENDPOINT}/files/hashes-for-sync?projectId=${projectId}`,
|
|
2007
|
+
{
|
|
2008
|
+
headers: {
|
|
2009
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
2010
|
+
},
|
|
2011
|
+
}
|
|
2012
|
+
);
|
|
2013
|
+
if (hashResponse.ok) {
|
|
2014
|
+
const hashData = await hashResponse.json();
|
|
2015
|
+
serverPathToHash = hashData.pathToHash || {};
|
|
2016
|
+
}
|
|
2017
|
+
} catch (e) {
|
|
2018
|
+
console.error('[DocuKing] 해시 조회 실패:', e.message);
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
// 로컬 파일 해시 계산 + 변경 상태 비교
|
|
2022
|
+
const addedFiles = []; // 로컬에만 있음 (새 파일)
|
|
2023
|
+
const modifiedFiles = []; // 해시가 다름 (수정됨)
|
|
2024
|
+
const deletedFiles = []; // 서버에만 있음 (삭제됨)
|
|
2025
|
+
const unchangedFiles = []; // 동일
|
|
2026
|
+
|
|
2027
|
+
// 로컬 파일 해시 계산
|
|
2028
|
+
const localPathToHash = {};
|
|
2029
|
+
for (const file of localFiles) {
|
|
2030
|
+
let fullPath;
|
|
2031
|
+
// 경로에 따라 로컬 전체 경로 결정
|
|
2032
|
+
if (file.startsWith('xx_') || file.startsWith('xy_') || file.startsWith('yy_') || file.startsWith('zz_')) {
|
|
2033
|
+
fullPath = path.join(localPath, file);
|
|
2034
|
+
} else {
|
|
2035
|
+
fullPath = path.join(mainFolderPath, file);
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
if (fs.existsSync(fullPath)) {
|
|
2039
|
+
try {
|
|
2040
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
2041
|
+
const hash = crypto.createHash('sha256').update(content).digest('hex').substring(0, 16);
|
|
2042
|
+
localPathToHash[file] = hash;
|
|
2043
|
+
} catch (e) {
|
|
2044
|
+
// 바이너리 파일 등은 스킵
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
// 비교
|
|
2050
|
+
const serverPaths = Object.keys(serverPathToHash);
|
|
2051
|
+
const localPaths = Object.keys(localPathToHash);
|
|
2052
|
+
|
|
2053
|
+
// 로컬 파일 분류
|
|
2054
|
+
for (const localFile of localPaths) {
|
|
2055
|
+
if (!serverPathToHash[localFile]) {
|
|
2056
|
+
addedFiles.push(localFile);
|
|
2057
|
+
} else if (serverPathToHash[localFile] !== localPathToHash[localFile]) {
|
|
2058
|
+
modifiedFiles.push(localFile);
|
|
2059
|
+
} else {
|
|
2060
|
+
unchangedFiles.push(localFile);
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
// 서버에만 있는 파일 (삭제됨)
|
|
2065
|
+
for (const serverFile of serverPaths) {
|
|
2066
|
+
if (!localPathToHash[serverFile]) {
|
|
2067
|
+
deletedFiles.push(serverFile);
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
|
|
1989
2071
|
const projectNameInfo = projectName ? ` (${projectName})` : '';
|
|
1990
2072
|
const workingFolder = isCoworker ? coworkerFolderName : 'yy_All_Docu';
|
|
1991
|
-
|
|
2073
|
+
|
|
2074
|
+
let statusText = `DocuKing 동기화 상태
|
|
1992
2075
|
|
|
1993
2076
|
**프로젝트**: ${projectId}${projectNameInfo}
|
|
1994
2077
|
**작업 폴더**: ${workingFolder}/
|
|
1995
2078
|
**로컬 파일**: ${localFiles.length}개
|
|
1996
|
-
**서버 파일**: ${serverFiles.length}
|
|
1997
|
-
|
|
2079
|
+
**서버 파일**: ${serverFiles.length}개${permissionInfo}`;
|
|
2080
|
+
|
|
2081
|
+
// 변경 사항 표시
|
|
2082
|
+
const hasChanges = addedFiles.length > 0 || modifiedFiles.length > 0 || deletedFiles.length > 0;
|
|
2083
|
+
|
|
2084
|
+
if (hasChanges) {
|
|
2085
|
+
statusText += `\n\n## 📊 변경 사항 (Push 대상)`;
|
|
2086
|
+
|
|
2087
|
+
if (addedFiles.length > 0) {
|
|
2088
|
+
statusText += `\n\n**➕ 새 파일 (${addedFiles.length}개)**:`;
|
|
2089
|
+
addedFiles.slice(0, 10).forEach(f => statusText += `\n + ${f}`);
|
|
2090
|
+
if (addedFiles.length > 10) statusText += `\n ... 외 ${addedFiles.length - 10}개`;
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
if (modifiedFiles.length > 0) {
|
|
2094
|
+
statusText += `\n\n**✏️ 수정됨 (${modifiedFiles.length}개)**:`;
|
|
2095
|
+
modifiedFiles.slice(0, 10).forEach(f => statusText += `\n ~ ${f}`);
|
|
2096
|
+
if (modifiedFiles.length > 10) statusText += `\n ... 외 ${modifiedFiles.length - 10}개`;
|
|
2097
|
+
}
|
|
1998
2098
|
|
|
1999
|
-
|
|
2099
|
+
if (deletedFiles.length > 0) {
|
|
2100
|
+
statusText += `\n\n**🗑️ 삭제됨 (${deletedFiles.length}개, 로컬에 없음)**:`;
|
|
2101
|
+
deletedFiles.slice(0, 10).forEach(f => statusText += `\n - ${f}`);
|
|
2102
|
+
if (deletedFiles.length > 10) statusText += `\n ... 외 ${deletedFiles.length - 10}개`;
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
statusText += `\n\n💡 Push하려면: docuking_push({ localPath, message: "변경 내용" })`;
|
|
2106
|
+
} else {
|
|
2107
|
+
statusText += `\n\n✅ 변경 사항 없음 (동기화됨: ${unchangedFiles.length}개 파일)`;
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
statusText += `\n\n## 사용 가능한 작업
|
|
2000
2111
|
- **Push**: docuking_push({ localPath, message: "..." })
|
|
2001
2112
|
- **Pull**: docuking_pull({ localPath })
|
|
2002
2113
|
- **목록 조회**: docuking_list({ localPath })`;
|
|
@@ -2016,13 +2127,19 @@ export async function handleStatus(args) {
|
|
|
2016
2127
|
*/
|
|
2017
2128
|
export async function handleDelete(args) {
|
|
2018
2129
|
const localPath = args.localPath || process.cwd();
|
|
2019
|
-
const {
|
|
2130
|
+
const { force = false } = args;
|
|
2131
|
+
|
|
2132
|
+
// path (단수) 또는 paths (복수) 둘 다 지원
|
|
2133
|
+
let paths = args.paths;
|
|
2134
|
+
if (!paths && args.path) {
|
|
2135
|
+
paths = [args.path]; // 단수를 배열로 변환
|
|
2136
|
+
}
|
|
2020
2137
|
|
|
2021
2138
|
if (!paths || paths.length === 0) {
|
|
2022
2139
|
return {
|
|
2023
2140
|
content: [{
|
|
2024
2141
|
type: 'text',
|
|
2025
|
-
text: '오류: 삭제할 파일 경로(paths)가 필요합니다.',
|
|
2142
|
+
text: '오류: 삭제할 파일 경로(path 또는 paths)가 필요합니다.',
|
|
2026
2143
|
}],
|
|
2027
2144
|
};
|
|
2028
2145
|
}
|
package/index.js
CHANGED
|
@@ -200,9 +200,23 @@ init 완료 후 오너에게 "정책 문서를 작성하시겠습니까?" 제안
|
|
|
200
200
|
name: 'docuking_pull',
|
|
201
201
|
description: `서버에서 문서를 다운로드합니다. "DocuKing에서 가져와" 요청 시 사용.
|
|
202
202
|
|
|
203
|
-
|
|
203
|
+
**⚠️ 최신 파일 보존 정책 (AI 필독)**
|
|
204
204
|
|
|
205
|
-
|
|
205
|
+
Pull 시 로컬과 서버 파일이 다르면 **timestamp를 비교**합니다:
|
|
206
|
+
- **로컬이 더 최신** → 로컬 유지 (다음 Push에서 서버로 업로드)
|
|
207
|
+
- **서버가 더 최신** → 서버 버전 다운로드
|
|
208
|
+
|
|
209
|
+
이렇게 하면:
|
|
210
|
+
- 로컬에서 작업한 최신 파일이 서버의 과거 버전으로 덮어쓰이지 않음
|
|
211
|
+
- 서버에서 다른 사람이 수정한 최신 파일은 로컬로 내려옴
|
|
212
|
+
|
|
213
|
+
**좀비 방지**: 로컬에서 삭제한 파일은 서버에서 다시 내려오지 않습니다.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
**킹캐스트**: Pull 후 xx_Policy/, xx_Infra_Config/ 변경 감지 시 .claude/rules/local/에 자동 로컬화.
|
|
218
|
+
|
|
219
|
+
**긴급 보고 알림**: xx_Urgent/ 폴더에 파일이 있으면 "긴급 보고 N건 있습니다" 표시.`,
|
|
206
220
|
inputSchema: {
|
|
207
221
|
type: 'object',
|
|
208
222
|
properties: {
|
|
@@ -234,7 +248,17 @@ init 완료 후 오너에게 "정책 문서를 작성하시겠습니까?" 제안
|
|
|
234
248
|
},
|
|
235
249
|
{
|
|
236
250
|
name: 'docuking_status',
|
|
237
|
-
description:
|
|
251
|
+
description: `로컬과 서버의 동기화 상태를 확인합니다. git status와 유사.
|
|
252
|
+
|
|
253
|
+
**표시 내용:**
|
|
254
|
+
- 사용자 권한 (오너/협업자)
|
|
255
|
+
- 로컬에만 있는 파일 (Added) → Push 시 서버에 업로드
|
|
256
|
+
- 내용이 다른 파일 (Modified) → Push 시 서버에 업로드
|
|
257
|
+
- 서버에만 있는 파일 (Deleted) → Pull 시 로컬로 다운로드
|
|
258
|
+
|
|
259
|
+
**사용 시점:**
|
|
260
|
+
- Push/Pull 전에 변경 사항 확인
|
|
261
|
+
- "무슨 파일이 바뀌었지?" 확인할 때`,
|
|
238
262
|
inputSchema: {
|
|
239
263
|
type: 'object',
|
|
240
264
|
properties: {
|
package/lib/init.js
CHANGED
|
@@ -158,20 +158,22 @@ project/
|
|
|
158
158
|
if (fs.existsSync(claudeMdPath)) {
|
|
159
159
|
let content = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
160
160
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
161
|
+
// 기존 DocuKing 섹션 모두 삭제 후 새로 추가
|
|
162
|
+
// 패턴: "---\n\n## DocuKing" 으로 시작하는 섹션 찾아서 끝까지 삭제
|
|
163
|
+
const docukingPatterns = [
|
|
164
|
+
/---\n\n## DocuKing 연결됨[\s\S]*$/,
|
|
165
|
+
/---\n\n## DocuKing AI 작업 기록[\s\S]*$/,
|
|
166
|
+
/## DocuKing 연결됨[\s\S]*$/,
|
|
167
|
+
/## DocuKing AI 작업 기록[\s\S]*$/,
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
for (const pattern of docukingPatterns) {
|
|
171
|
+
content = content.replace(pattern, '');
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
|
|
174
|
+
// 끝에 새 섹션 추가
|
|
175
|
+
content = content.trimEnd() + '\n' + docukingSection;
|
|
176
|
+
fs.writeFileSync(claudeMdPath, content, 'utf-8');
|
|
175
177
|
} else {
|
|
176
178
|
const newContent = `# Project Instructions
|
|
177
179
|
|