docuking-mcp 3.1.0 → 3.6.0

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
@@ -46,6 +46,12 @@ import {
46
46
  updateIndexAfterSync,
47
47
  computeFileHash,
48
48
  getIndexStats,
49
+ // v2 추가
50
+ markAsDeleted,
51
+ wasDeletedLocally,
52
+ clearDeletedEntry,
53
+ classifyForPush,
54
+ classifyForPull,
49
55
  } from '../lib/index-cache.js';
50
56
 
51
57
  // 레포지토리 매핑 (메모리에 캐시)
@@ -74,6 +80,18 @@ docuking_init 호출 시 apiKey 파라미터를 포함해주세요.`,
74
80
  // Co-worker 권한은 API Key 형식에서 판단
75
81
  const { isCoworker, coworkerFolder } = parseCoworkerFromApiKey(apiKey);
76
82
 
83
+ // 기존 config 확인 - 프로젝트 ID가 변경되면 인덱스 초기화
84
+ const existingConfig = getLocalConfig(localPath);
85
+ if (existingConfig && existingConfig.projectId && existingConfig.projectId !== projectId) {
86
+ // 프로젝트 ID가 변경됨 - 인덱스 파일 삭제
87
+ const indexPath = path.join(localPath, '.docuking', 'index.json');
88
+ if (fs.existsSync(indexPath)) {
89
+ fs.unlinkSync(indexPath);
90
+ console.error(`[DocuKing] 프로젝트 ID 변경 감지: ${existingConfig.projectId} → ${projectId}`);
91
+ console.error(`[DocuKing] 기존 인덱스 초기화됨`);
92
+ }
93
+ }
94
+
77
95
  // .docuking/config.json에 설정 저장
78
96
  saveLocalConfig(localPath, {
79
97
  projectId,
@@ -84,7 +102,7 @@ docuking_init 호출 시 apiKey 파라미터를 포함해주세요.`,
84
102
  createdAt: new Date().toISOString(),
85
103
  });
86
104
 
87
- // 폴더 생성: 오너는 yy_All_Docu/, 협업자는 yy_Coworker_{폴더명}/ (별도)
105
+ // 폴더 생성: 오너는 yy_All_Docu/, 협업자는 zz_Coworker_{폴더명}/ (별도)
88
106
  const mainFolderName = 'yy_All_Docu';
89
107
  const mainFolderPath = path.join(localPath, mainFolderName);
90
108
 
@@ -96,9 +114,10 @@ docuking_init 호출 시 apiKey 파라미터를 포함해주세요.`,
96
114
  let coworkerFolderName = null;
97
115
  let coworkerFolderPath = null;
98
116
 
99
- // AI 폴더 (오너: xy_TalkTodoPlan, 협업자: zz_TalkTodoPlan)
117
+ // AI 폴더 (오너: xy_TalkTodoPlan, 협업자: {폴더명}_TalkTodoPlan)
100
118
  const ownerAiFolder = 'xy_TalkTodoPlan';
101
- const coworkerAiFolder = 'zz_TalkTodoPlan';
119
+ // 협업자 AI 폴더는 동적으로 생성 (coworkerFolder가 있을 때만)
120
+ const coworkerAiFolder = coworkerFolder ? `${coworkerFolder}_TalkTodoPlan` : null;
102
121
 
103
122
  // xx_ 시스템 폴더 목록 (오너 전용)
104
123
  const systemFolders = ['xx_Infra_Config', 'xx_Policy'];
@@ -110,8 +129,8 @@ docuking_init 호출 시 apiKey 파라미터를 포함해주세요.`,
110
129
  }
111
130
 
112
131
  if (isCoworker) {
113
- // 협업자: yy_Coworker_{폴더명}/ 폴더를 yy_All_Docu/ 밖에 별도 생성
114
- coworkerFolderName = `yy_Coworker_${coworkerFolder}`;
132
+ // 협업자: zz_Coworker_{폴더명}/ 폴더를 yy_All_Docu/ 밖에 별도 생성
133
+ coworkerFolderName = `zz_Coworker_${coworkerFolder}`;
115
134
  coworkerFolderPath = path.join(localPath, coworkerFolderName);
116
135
  if (!fs.existsSync(coworkerFolderPath)) {
117
136
  fs.mkdirSync(coworkerFolderPath, { recursive: true });
@@ -121,7 +140,7 @@ docuking_init 호출 시 apiKey 파라미터를 포함해주세요.`,
121
140
  if (!fs.existsSync(coworkerPrivatePath)) {
122
141
  fs.mkdirSync(coworkerPrivatePath, { recursive: true });
123
142
  }
124
- // 협업자 폴더 안에 zz_TalkTodoPlan 폴더 생성
143
+ // 협업자 폴더 안에 {폴더명}_TalkTodoPlan 폴더 생성
125
144
  const coworkerAiFolderPath = path.join(coworkerFolderPath, coworkerAiFolder);
126
145
  if (!fs.existsSync(coworkerAiFolderPath)) {
127
146
  fs.mkdirSync(coworkerAiFolderPath, { recursive: true });
@@ -244,11 +263,11 @@ Git처럼 무엇을 변경했는지 명확히 작성해주세요.
244
263
 
245
264
  // Co-worker 권한은 API Key 형식에서 판단
246
265
  const { isCoworker, coworkerFolder } = parseCoworkerFromApiKey(apiKey);
247
- const coworkerFolderName = isCoworker ? `yy_Coworker_${coworkerFolder}` : null;
266
+ const coworkerFolderName = isCoworker ? `zz_Coworker_${coworkerFolder}` : null;
248
267
 
249
268
  // 작업 폴더 결정: 프로젝트 루트 기준 절대경로 사용
250
- // 오너: yy_All_Docu/, zz_ai_*/ 폴더들 Push
251
- // 협업자: yy_Coworker_{폴더명}/ 폴더만 Push
269
+ // 오너: xx_*/ + xy_*/ + yy_All_Docu/ 폴더들 Push
270
+ // 협업자: zz_Coworker_{폴더명}/ 폴더만 Push
252
271
  const mainFolderPath = path.join(localPath, 'yy_All_Docu');
253
272
 
254
273
  // Push 대상 폴더 목록 (프로젝트 루트 기준)
@@ -258,7 +277,7 @@ Git처럼 무엇을 변경했는지 명확히 작성해주세요.
258
277
  console.error(`[DocuKing] Push 권한: isCoworker=${isCoworker}, coworkerFolder=${coworkerFolder}, coworkerFolderName=${coworkerFolderName}`);
259
278
 
260
279
  if (isCoworker) {
261
- // 협업자: yy_Coworker_{폴더명}/ 폴더 + xx_Urgent/ 폴더 Push
280
+ // 협업자: zz_Coworker_{폴더명}/ 폴더 + xx_Urgent/ 폴더 Push
262
281
  const coworkerPath = path.join(localPath, coworkerFolderName);
263
282
  console.error(`[DocuKing] 협업자 Push 대상 폴더: ${coworkerPath}`);
264
283
  if (!fs.existsSync(coworkerPath)) {
@@ -294,13 +313,17 @@ docuking_init을 먼저 실행하세요.`,
294
313
  };
295
314
  }
296
315
 
297
- // 오너 Push 대상: xx_*/ + yy_All_Docu/ + zz_ai_*/ 폴더들
316
+ // 오너 Push 대상: xx_*/ + xy_*/ + yy_All_Docu/ 폴더들
298
317
  pushTargetFolders.push({ localPath: mainFolderPath, serverPrefix: 'yy_All_Docu' });
299
318
 
300
- // xx_*, zz_ai_* 폴더들 찾기
319
+ // 특수폴더 패턴으로 Push 대상 폴더 찾기
320
+ // 특수폴더: xx_*, xy_TalkTodoPlan, yy_All_Docu, zz_Coworker_*
301
321
  const rootEntries = fs.readdirSync(localPath, { withFileTypes: true });
302
322
  for (const entry of rootEntries) {
303
- if (entry.isDirectory() && (entry.name.startsWith('xx_') || entry.name.startsWith('zz_ai_'))) {
323
+ if (entry.isDirectory() && (
324
+ entry.name.startsWith('xx_') ||
325
+ entry.name === 'xy_TalkTodoPlan'
326
+ )) {
304
327
  const folderPath = path.join(localPath, entry.name);
305
328
  pushTargetFolders.push({ localPath: folderPath, serverPrefix: entry.name });
306
329
  }
@@ -704,7 +727,7 @@ docuking_init을 먼저 실행하세요.`,
704
727
  },
705
728
  body: JSON.stringify({
706
729
  projectId,
707
- path: file.serverPath, // 서버 경로 (코워커는 yy_Coworker_{폴더명}/파일경로)
730
+ path: file.serverPath, // 서버 경로 (코워커는 zz_Coworker_{폴더명}/파일경로)
708
731
  content,
709
732
  encoding, // 'utf-8' 또는 'base64'
710
733
  message, // 커밋 메시지
@@ -783,16 +806,18 @@ docuking_init을 먼저 실행하세요.`,
783
806
  }
784
807
 
785
808
  // ========================================
786
- // 4. 인덱스 기반 삭제 전파 (2026-01-24 재구현)
809
+ // 4. 인덱스 기반 삭제 전파 (v2: 2026-01-24 강화)
787
810
  // ========================================
788
- // 이전 문제: "서버에 있고 로컬에 없으면 삭제" → 다른 프로젝트에서 문제
789
- // 해결: 인덱스 기반으로 판단
790
- // - 인덱스에 있었는데 + 로컬에 없음 = 의도적 삭제 서버에서도 삭제
791
- // - 인덱스에 없었고 + 로컬에 없음 = 처음부터 없던 파일 → 무시
811
+ // 3-way 비교:
812
+ // - 인덱스에 있음 + 로컬에 없음 + 서버에 있음 = 로컬 삭제 → 서버도 삭제
813
+ // - 인덱스에 있음 + 로컬에 없음 + 서버 수정됨 = 충돌 (로컬 삭제 vs 서버 수정)
814
+ // - 인덱스에 없음 + 로컬에 없음 + 서버에 있음 = 다른 곳에서 생성 → 무시
815
+ // - source: 'web' 파일은 삭제 보호
792
816
  // ========================================
793
817
  let deleted = 0;
794
818
  const deletedFilePaths = [];
795
819
  let protectedFiles = [];
820
+ const conflicts = [];
796
821
 
797
822
  // 인덱스에서 삭제된 파일 찾기 (인덱스에 있었는데 로컬에 없는 파일)
798
823
  const locallyDeletedFiles = [];
@@ -814,8 +839,39 @@ docuking_init을 먼저 실행하세요.`,
814
839
 
815
840
  // 로컬에 파일이 없으면 = 삭제된 파일
816
841
  if (!fs.existsSync(fullPath)) {
842
+ const serverMeta = serverPathToMeta[indexPath];
843
+ const indexEntry = localIndex.files[indexPath];
844
+
845
+ // 서버에 없으면 이미 삭제됨 → 스킵
846
+ if (!serverMeta) {
847
+ console.error(`[DocuKing] 이미 삭제됨: ${indexPath}`);
848
+ // deletedFiles 기록 정리
849
+ clearDeletedEntry(localIndex, indexPath);
850
+ continue;
851
+ }
852
+
853
+ // source: 'web' 파일은 삭제 보호
854
+ if (serverMeta.source === 'web') {
855
+ console.error(`[DocuKing] 삭제 보호 (source:web): ${indexPath}`);
856
+ protectedFiles.push(indexPath);
857
+ continue;
858
+ }
859
+
860
+ // 서버가 수정되었는지 확인 (충돌 감지)
861
+ const serverModified = serverMeta.hash !== (indexEntry.serverHash || indexEntry.hash);
862
+ if (serverModified) {
863
+ console.error(`[DocuKing] ⚠️ 충돌: ${indexPath} (로컬 삭제 vs 서버 수정)`);
864
+ conflicts.push({
865
+ path: indexPath,
866
+ type: 'delete-vs-modify',
867
+ localAction: 'delete',
868
+ serverHash: serverMeta.hash,
869
+ });
870
+ // 충돌 시에도 로컬 우선 정책으로 삭제 진행 (경고만 출력)
871
+ }
872
+
817
873
  locallyDeletedFiles.push(indexPath);
818
- console.error(`[DocuKing] 삭제 감지: ${indexPath} (인덱스에 있었으나 로컬에 없음)`);
874
+ console.error(`[DocuKing] 삭제 감지: ${indexPath}`);
819
875
  }
820
876
  }
821
877
 
@@ -839,11 +895,11 @@ docuking_init을 먼저 실행하세요.`,
839
895
  const deleteResult = await deleteResponse.json();
840
896
  deleted = deleteResult.deleted || 0;
841
897
  deletedFilePaths.push(...(deleteResult.deletedPaths || []));
842
- protectedFiles = deleteResult.protected || [];
843
898
 
844
- // 인덱스에서도 삭제
899
+ // 인덱스에서 삭제 처리 (v2: markAsDeleted 사용)
845
900
  for (const deletedPath of locallyDeletedFiles) {
846
- delete localIndex.files[deletedPath];
901
+ const entry = localIndex.files[deletedPath];
902
+ markAsDeleted(localIndex, deletedPath, entry?.hash);
847
903
  }
848
904
 
849
905
  console.error(`[DocuKing] 서버 삭제 완료: ${deleted}개`);
@@ -858,6 +914,15 @@ docuking_init을 먼저 실행하세요.`,
858
914
  }
859
915
  }
860
916
 
917
+ // 충돌 경고 출력
918
+ if (conflicts.length > 0) {
919
+ console.error(`\n[DocuKing] ⚠️ ${conflicts.length}개 충돌 발생 (로컬 우선 정책으로 처리됨):`);
920
+ for (const c of conflicts) {
921
+ console.error(` - ${c.path}: ${c.type}`);
922
+ }
923
+ console.error('');
924
+ }
925
+
861
926
  // 5. 빈 폴더 생성
862
927
  let createdEmptyFolders = 0;
863
928
  if (emptyFolders.length > 0) {
@@ -888,12 +953,13 @@ docuking_init을 먼저 실행하세요.`,
888
953
  }
889
954
 
890
955
  // ========================================
891
- // 로컬 인덱스 업데이트 및 저장
956
+ // 로컬 인덱스 업데이트 및 저장 (v2)
892
957
  // ========================================
893
- if (syncedFiles.length > 0) {
894
- updateIndexAfterSync(localIndex, syncedFiles, deletedFilePaths);
958
+ if (syncedFiles.length > 0 || deletedFilePaths.length > 0) {
959
+ // syncType: 'push'로 lastPush 타임스탬프 업데이트
960
+ updateIndexAfterSync(localIndex, syncedFiles, deletedFilePaths, { syncType: 'push' });
895
961
  if (saveIndex(localPath, localIndex)) {
896
- console.error(`[DocuKing] 로컬 인덱스 업데이트: ${syncedFiles.length}개 파일`);
962
+ console.error(`[DocuKing] 로컬 인덱스 업데이트: ${syncedFiles.length}개 동기화, ${deletedFilePaths.length}개 삭제`);
897
963
  }
898
964
  }
899
965
 
@@ -1033,8 +1099,8 @@ export async function handlePullInternal(args) {
1033
1099
  // 오너 전용 폴더
1034
1100
  const ownerFolders = ['xx_Infra_Config', 'xx_Policy', 'xy_TalkTodoPlan'];
1035
1101
 
1036
- // 협업자 전용 폴더 (yy_Coworker_{폴더명}/ 안에)
1037
- const coworkerSubFolders = ['zz_TalkTodoPlan', '_Private'];
1102
+ // 협업자 전용 폴더 (zz_Coworker_{폴더명}/ 안에)
1103
+ // AI 폴더는 {폴더명}_TalkTodoPlan 형식 (coworkerFolder 기반 동적 생성)
1038
1104
 
1039
1105
  // 공통 폴더 체크
1040
1106
  for (const folder of commonFolders) {
@@ -1047,8 +1113,9 @@ export async function handlePullInternal(args) {
1047
1113
  }
1048
1114
 
1049
1115
  if (isCoworker) {
1050
- // 협업자: yy_Coworker_{폴더명}/ 안에 하위 폴더 체크
1051
- const coworkerFolderName = `yy_Coworker_${coworkerFolder}`;
1116
+ // 협업자: zz_Coworker_{폴더명}/ 안에 하위 폴더 체크
1117
+ const coworkerFolderName = `zz_Coworker_${coworkerFolder}`;
1118
+ const coworkerSubFolders = [`${coworkerFolder}_TalkTodoPlan`, '_Private'];
1052
1119
  const coworkerBasePath = path.join(localPath, coworkerFolderName);
1053
1120
 
1054
1121
  // 협업자 루트 폴더
@@ -1197,10 +1264,9 @@ export async function handlePullInternal(args) {
1197
1264
  const serverMeta = serverPathToMeta[folder.path];
1198
1265
  let fullPath;
1199
1266
  if (folder.path.startsWith('xx_') ||
1200
- folder.path.startsWith('yy_All_Docu/') ||
1267
+ folder.path.startsWith('xy_TalkTodoPlan') ||
1201
1268
  folder.path.startsWith('yy_All_Docu') ||
1202
- folder.path.startsWith('yy_Coworker_') ||
1203
- folder.path.startsWith('zz_ai_')) {
1269
+ folder.path.startsWith('zz_Coworker_')) {
1204
1270
  fullPath = path.join(localPath, folder.path);
1205
1271
  } else {
1206
1272
  fullPath = path.join(mainFolderPath, folder.path);
@@ -1230,10 +1296,9 @@ export async function handlePullInternal(args) {
1230
1296
  for (const file of files_only) {
1231
1297
  let fullPath;
1232
1298
  if (file.path.startsWith('xx_') ||
1233
- file.path.startsWith('yy_All_Docu/') ||
1299
+ file.path.startsWith('xy_TalkTodoPlan') ||
1234
1300
  file.path.startsWith('yy_All_Docu') ||
1235
- file.path.startsWith('yy_Coworker_') ||
1236
- file.path.startsWith('zz_ai_')) {
1301
+ file.path.startsWith('zz_Coworker_')) {
1237
1302
  fullPath = path.join(localPath, file.path);
1238
1303
  } else {
1239
1304
  fullPath = path.join(mainFolderPath, file.path);
@@ -1276,17 +1341,46 @@ export async function handlePullInternal(args) {
1276
1341
  }
1277
1342
  } else {
1278
1343
  // ========================================
1279
- // 로컬에 파일이 없는 경우
1280
- // 인덱스에 있었으면 = 로컬에서 삭제한 파일 → 스킵 (좀비 방지)
1281
- // 인덱스에 없으면 = 서버에서 새로 생성된 파일 다운로드
1344
+ // 로컬에 파일이 없는 경우 (v2 좀비 방지 강화)
1345
+ // 1. files에 있음 = 로컬에서 삭제한 파일 → markAsDeleted 스킵
1346
+ // 2. deletedFiles에 있음 = 이미 삭제 표시됨서버 변경 확인 후 처리
1347
+ // 3. 둘 다 없음 = 서버에서 새로 생성된 파일 → 다운로드
1282
1348
  // ========================================
1349
+
1350
+ // Case 1: files에 아직 있음 (첫 Pull 시점)
1283
1351
  if (localIndex.files && localIndex.files[file.path]) {
1284
- console.error(`[DocuKing] 로컬 삭제 유지: ${file.path} (인덱스에 있었음 스킵, 다음 Push에서 서버 삭제됨)`);
1352
+ console.error(`[DocuKing] 로컬 삭제 감지: ${file.path} → deletedFiles에 기록`);
1353
+ // deletedFiles에 기록
1354
+ const entry = localIndex.files[file.path];
1355
+ markAsDeleted(localIndex, file.path, entry.hash);
1285
1356
  results.push({ type: 'skip', path: file.path, reason: 'locally-deleted' });
1286
1357
  skipped++;
1287
1358
  current++;
1288
1359
  continue;
1289
1360
  }
1361
+
1362
+ // Case 2: deletedFiles에 있음 (이전에 삭제 표시됨)
1363
+ const deleted = wasDeletedLocally(localIndex, file.path);
1364
+ if (deleted.isDeleted) {
1365
+ // 서버가 수정되었는지 확인
1366
+ if (serverHash === deleted.entry.lastHash) {
1367
+ // 서버 변경 없음 → 스킵 (좀비 방지)
1368
+ console.error(`[DocuKing] 좀비 방지: ${file.path} (로컬 삭제 유지)`);
1369
+ results.push({ type: 'skip', path: file.path, reason: 'zombie-prevention' });
1370
+ } else {
1371
+ // 서버가 수정됨 → 충돌! (로컬 삭제 vs 서버 수정)
1372
+ console.error(`[DocuKing] ⚠️ 충돌: ${file.path} (로컬 삭제 vs 서버 수정) → 서버 버전 다운로드`);
1373
+ // 충돌 시 서버 버전 다운로드 (사용자가 결정할 수 있도록)
1374
+ filesToDownload.push({ ...file, fullPath, conflict: true });
1375
+ current++;
1376
+ continue;
1377
+ }
1378
+ skipped++;
1379
+ current++;
1380
+ continue;
1381
+ }
1382
+
1383
+ // Case 3: 둘 다 없음 = 새 파일
1290
1384
  }
1291
1385
 
1292
1386
  // 인덱스에 없고 로컬에 없음 = 서버에서 새로 생성된 파일 → 다운로드
@@ -1437,12 +1531,17 @@ export async function handlePullInternal(args) {
1437
1531
  }
1438
1532
 
1439
1533
  // ========================================
1440
- // 로컬 인덱스 업데이트 및 저장
1534
+ // 로컬 인덱스 업데이트 및 저장 (v2)
1441
1535
  // ========================================
1536
+ // Pull 중 발견된 삭제 파일들도 인덱스에 기록됨 (markAsDeleted 호출됨)
1537
+ // 따라서 syncedFiles가 0이어도 인덱스가 변경되었을 수 있음
1538
+ if (saveIndex(localPath, localIndex)) {
1539
+ console.error(`[DocuKing] 로컬 인덱스 업데이트 (lastPull 갱신)`);
1540
+ }
1442
1541
  if (syncedFiles.length > 0) {
1443
- updateIndexAfterSync(localIndex, syncedFiles, []);
1542
+ updateIndexAfterSync(localIndex, syncedFiles, [], { syncType: 'pull' });
1444
1543
  if (saveIndex(localPath, localIndex)) {
1445
- console.error(`[DocuKing] 로컬 인덱스 업데이트: ${syncedFiles.length}개 파일`);
1544
+ console.error(`[DocuKing] 로컬 인덱스: ${syncedFiles.length}개 파일 동기화됨`);
1446
1545
  }
1447
1546
  }
1448
1547
 
@@ -1524,9 +1623,9 @@ export async function handlePullInternal(args) {
1524
1623
  // 서버 경로에 따라 로컬 경로 결정
1525
1624
  let fullPath;
1526
1625
  if (deletedFile.path.startsWith('xx_') ||
1527
- deletedFile.path.startsWith('yy_All_Docu/') ||
1528
- deletedFile.path.startsWith('yy_Coworker_') ||
1529
- deletedFile.path.startsWith('zz_ai_')) {
1626
+ deletedFile.path.startsWith('xy_TalkTodoPlan') ||
1627
+ deletedFile.path.startsWith('yy_All_Docu') ||
1628
+ deletedFile.path.startsWith('zz_Coworker_')) {
1530
1629
  fullPath = path.join(localPath, deletedFile.path);
1531
1630
  } else {
1532
1631
  fullPath = path.join(mainFolderPath, deletedFile.path);
@@ -1550,6 +1649,10 @@ export async function handlePullInternal(args) {
1550
1649
  deletedLocallyPaths.push(deletedFile.path);
1551
1650
  console.error(`[DocuKing] 로컬 삭제 (서버에서 삭제됨): ${deletedFile.path}`);
1552
1651
 
1652
+ // 인덱스에서도 제거 (v2)
1653
+ removeIndexEntry(localIndex, deletedFile.path);
1654
+ clearDeletedEntry(localIndex, deletedFile.path);
1655
+
1553
1656
  // 빈 폴더 정리
1554
1657
  const parentDir = path.dirname(fullPath);
1555
1658
  try {
@@ -1563,6 +1666,8 @@ export async function handlePullInternal(args) {
1563
1666
  }
1564
1667
  } else {
1565
1668
  console.error(`[DocuKing] 로컬 유지 (로컬 수정이 최신): ${deletedFile.path}`);
1669
+ // 다음 Push에서 부활할 것이므로 deletedFiles에서 제거
1670
+ clearDeletedEntry(localIndex, deletedFile.path);
1566
1671
  }
1567
1672
  } catch (e) {
1568
1673
  console.error(`[DocuKing] 삭제 동기화 실패: ${deletedFile.path} - ${e.message}`);
@@ -1814,7 +1919,7 @@ export async function handleStatus(args) {
1814
1919
 
1815
1920
  // Co-worker 권한은 API Key 형식에서 판단
1816
1921
  const { isCoworker, coworkerFolder } = parseCoworkerFromApiKey(apiKey);
1817
- const coworkerFolderName = isCoworker ? `yy_Coworker_${coworkerFolder}` : null;
1922
+ const coworkerFolderName = isCoworker ? `zz_Coworker_${coworkerFolder}` : null;
1818
1923
 
1819
1924
  // 권한 정보 구성
1820
1925
  let permissionInfo = '';
@@ -1856,21 +1961,24 @@ export async function handleStatus(args) {
1856
1961
  const mainFolderPath = path.join(localPath, 'yy_All_Docu');
1857
1962
 
1858
1963
  if (isCoworker) {
1859
- // 협업자: yy_Coworker_{폴더명}/ 폴더에서 파일 수집
1964
+ // 협업자: zz_Coworker_{폴더명}/ 폴더에서 파일 수집
1860
1965
  const coworkerPath = path.join(localPath, coworkerFolderName);
1861
1966
  if (fs.existsSync(coworkerPath)) {
1862
1967
  collectFilesSimple(coworkerPath, '', localFiles);
1863
1968
  }
1864
1969
  pushableFiles = localFiles; // 협업자는 자기 폴더의 모든 파일 Push 가능
1865
1970
  } else {
1866
- // 오너: xx_*/ + yy_All_Docu/ + zz_ai_*/ 폴더에서 파일 수집
1971
+ // 오너: xx_*/ + xy_*/ + yy_All_Docu/ 폴더에서 파일 수집
1867
1972
  if (fs.existsSync(mainFolderPath)) {
1868
1973
  collectFilesSimple(mainFolderPath, '', localFiles);
1869
1974
  }
1870
- // xx_*, zz_ai_* 폴더들도 수집
1975
+ // 특수폴더들도 수집: xx_*, xy_TalkTodoPlan
1871
1976
  const rootEntries = fs.readdirSync(localPath, { withFileTypes: true });
1872
1977
  for (const entry of rootEntries) {
1873
- if (entry.isDirectory() && (entry.name.startsWith('xx_') || entry.name.startsWith('zz_ai_'))) {
1978
+ if (entry.isDirectory() && (
1979
+ entry.name.startsWith('xx_') ||
1980
+ entry.name === 'xy_TalkTodoPlan'
1981
+ )) {
1874
1982
  const folderPath = path.join(localPath, entry.name);
1875
1983
  collectFilesSimple(folderPath, '', localFiles);
1876
1984
  }
@@ -1948,7 +2056,7 @@ export async function handleDelete(args) {
1948
2056
 
1949
2057
  // 협업자는 자기 폴더만 삭제 가능
1950
2058
  if (isCoworker) {
1951
- const coworkerPrefix = `yy_Coworker_${coworkerFolder}/`;
2059
+ const coworkerPrefix = `zz_Coworker_${coworkerFolder}/`;
1952
2060
  const invalidPaths = paths.filter(p => !p.startsWith(coworkerPrefix) && !p.startsWith('xx_Urgent/'));
1953
2061
  if (invalidPaths.length > 0) {
1954
2062
  return {
@@ -1963,7 +2071,7 @@ export async function handleDelete(args) {
1963
2071
 
1964
2072
  // 오너도 협업자 폴더는 삭제 금지
1965
2073
  if (!isCoworker) {
1966
- const coworkerPaths = paths.filter(p => p.startsWith('yy_Coworker_'));
2074
+ const coworkerPaths = paths.filter(p => p.startsWith('zz_Coworker_'));
1967
2075
  if (coworkerPaths.length > 0 && !force) {
1968
2076
  return {
1969
2077
  content: [{
@@ -2039,9 +2147,9 @@ export async function handleDelete(args) {
2039
2147
  try {
2040
2148
  let localFilePath;
2041
2149
  if (serverPath.startsWith('xx_') ||
2042
- serverPath.startsWith('yy_All_Docu/') ||
2043
- serverPath.startsWith('yy_Coworker_') ||
2044
- serverPath.startsWith('zz_ai_')) {
2150
+ serverPath.startsWith('xy_TalkTodoPlan') ||
2151
+ serverPath.startsWith('yy_All_Docu') ||
2152
+ serverPath.startsWith('zz_Coworker_')) {
2045
2153
  localFilePath = path.join(localPath, serverPath);
2046
2154
  } else {
2047
2155
  localFilePath = path.join(localPath, 'yy_All_Docu', serverPath);