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 CHANGED
@@ -1326,16 +1326,29 @@ export async function handlePullInternal(args) {
1326
1326
  }
1327
1327
 
1328
1328
  // ========================================
1329
- // [버그 수정] 로컬 파일 우선 정책
1330
- // 해시가 다르면 → 로컬 수정이 있다는
1331
- // 서버 파일로 덮어쓰지 않고 로컬 유지
1332
- // 다음 Push에서 로컬서버로 업로드됨
1329
+ // [v3.7.0] 최신 파일 보존 정책
1330
+ // 해시가 다르면 → timestamp 비교해서 최신 파일 유지
1331
+ // 로컬이 최신 로컬 유지 (다음 Push에서 업로드)
1332
+ // 서버가 최신서버 버전 다운로드
1333
1333
  // ========================================
1334
- console.error(`[DocuKing] 로컬 우선: ${file.path} (해시 다름 → 로컬 유지, Push 시 업로드됨)`);
1335
- results.push({ type: 'skip', path: file.path, reason: 'local-modified' });
1336
- skipped++;
1337
- current++;
1338
- continue;
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
- const statusText = `DocuKing 동기화 상태
2073
+
2074
+ let statusText = `DocuKing 동기화 상태
1992
2075
 
1993
2076
  **프로젝트**: ${projectId}${projectNameInfo}
1994
2077
  **작업 폴더**: ${workingFolder}/
1995
2078
  **로컬 파일**: ${localFiles.length}개
1996
- **서버 파일**: ${serverFiles.length}
1997
- **Push 가능한 파일**: ${pushableFiles.length}개${permissionInfo}
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 { paths, force = false } = args;
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
- **킹캐스트 포함**: Pull xx_Policy/, xx_Infra_Config/ 폴더 변경이 감지되면 자동으로 .claude/rules/local/에 로컬화됩니다. 오너가 정책을 수정하고 Push하면, 협업자가 Pull할 때 자동으로 정책이 전파됩니다.
203
+ **⚠️ 최신 파일 보존 정책 (AI 필독)**
204
204
 
205
- **긴급 보고 알림**: Pull xx_Urgent/ 폴더에 파일이 있으면 "긴급 보고 N건 있습니다" 알림을 표시합니다.`,
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
- if (content.includes(marker)) {
162
- return;
163
- }
164
-
165
- if (content.includes(oldMarker)) {
166
- const oldSectionStart = content.lastIndexOf('---\n\n' + oldMarker);
167
- if (oldSectionStart !== -1) {
168
- content = content.substring(0, oldSectionStart) + docukingSection;
169
- fs.writeFileSync(claudeMdPath, content, 'utf-8');
170
- return;
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
- fs.appendFileSync(claudeMdPath, docukingSection, 'utf-8');
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docuking-mcp",
3
- "version": "3.6.0",
3
+ "version": "3.7.1",
4
4
  "description": "DocuKing MCP Server - AI 시대의 문서 협업 플랫폼",
5
5
  "type": "module",
6
6
  "main": "index.js",