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.
@@ -1,19 +1,26 @@
1
1
  /**
2
- * DocuKing MCP - 로컬 인덱스 캐시 모듈
2
+ * DocuKing MCP - 로컬 인덱스 캐시 모듈 (v2)
3
3
  *
4
4
  * Git의 .git/index처럼 마지막 동기화 상태를 저장하여
5
5
  * mtime 기반으로 변경된 파일만 빠르게 탐지합니다.
6
6
  *
7
+ * v2 변경사항 (2026-01-24):
8
+ * - deletedFiles 추적 (좀비 방지 핵심)
9
+ * - serverHash 캐시 (충돌 감지)
10
+ * - syncedAt 타임스탬프
11
+ * - lastPush, lastPull 분리
12
+ *
7
13
  * 효과:
8
14
  * - 변경되지 않은 파일은 해시 계산 스킵
9
15
  * - Pull/Push 시 변경된 파일만 처리
16
+ * - 로컬에서 삭제한 파일이 서버에서 다시 내려오지 않음 (좀비 방지)
10
17
  */
11
18
 
12
19
  import fs from 'fs';
13
20
  import path from 'path';
14
21
  import crypto from 'crypto';
15
22
 
16
- const INDEX_VERSION = 1;
23
+ const INDEX_VERSION = 2;
17
24
  const INDEX_FILENAME = 'index.json';
18
25
 
19
26
  /**
@@ -25,7 +32,21 @@ function getIndexPath(localPath) {
25
32
 
26
33
  /**
27
34
  * 로컬 인덱스 로드
28
- * @returns {{ version: number, lastSync: string, files: Object }} 인덱스 객체
35
+ * @returns {IndexV2} 인덱스 객체
36
+ *
37
+ * v2 스키마:
38
+ * {
39
+ * version: 2,
40
+ * lastSync: string,
41
+ * lastPush: string,
42
+ * lastPull: string,
43
+ * files: {
44
+ * [path]: { hash, mtime, size, serverHash?, syncedAt?, source? }
45
+ * },
46
+ * deletedFiles: {
47
+ * [path]: { deletedAt, lastHash }
48
+ * }
49
+ * }
29
50
  */
30
51
  export function loadIndex(localPath) {
31
52
  const indexPath = getIndexPath(localPath);
@@ -34,13 +55,24 @@ export function loadIndex(localPath) {
34
55
  if (fs.existsSync(indexPath)) {
35
56
  const data = JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
36
57
 
37
- // 버전 체크 (호환되지 않으면 초기화)
38
- if (data.version !== INDEX_VERSION) {
39
- console.error('[DocuKing] 인덱스 버전 불일치, 재생성합니다.');
40
- return createEmptyIndex();
58
+ // v1 v2 마이그레이션
59
+ if (data.version === 1) {
60
+ console.error('[DocuKing] 인덱스 v1 v2 마이그레이션 중...');
61
+ return migrateV1toV2(data);
62
+ }
63
+
64
+ // v2 확인
65
+ if (data.version === INDEX_VERSION) {
66
+ // deletedFiles가 없으면 추가 (안전성)
67
+ if (!data.deletedFiles) {
68
+ data.deletedFiles = {};
69
+ }
70
+ return data;
41
71
  }
42
72
 
43
- return data;
73
+ // 알 수 없는 버전
74
+ console.error('[DocuKing] 알 수 없는 인덱스 버전, 재생성합니다.');
75
+ return createEmptyIndex();
44
76
  }
45
77
  } catch (e) {
46
78
  console.error('[DocuKing] 인덱스 로드 실패:', e.message);
@@ -50,13 +82,46 @@ export function loadIndex(localPath) {
50
82
  }
51
83
 
52
84
  /**
53
- * 인덱스 생성
85
+ * v1 v2 마이그레이션
86
+ */
87
+ function migrateV1toV2(v1Data) {
88
+ const v2Index = {
89
+ version: 2,
90
+ lastSync: v1Data.lastSync,
91
+ lastPush: v1Data.lastSync, // v1에서는 구분 없었음
92
+ lastPull: v1Data.lastSync,
93
+ files: {},
94
+ deletedFiles: {},
95
+ };
96
+
97
+ // 기존 파일 정보 마이그레이션
98
+ for (const [path, entry] of Object.entries(v1Data.files || {})) {
99
+ v2Index.files[path] = {
100
+ hash: entry.hash,
101
+ mtime: entry.mtime,
102
+ size: entry.size,
103
+ // v2 신규 필드 (값 없음)
104
+ serverHash: entry.hash, // 마지막 동기화 시점의 해시를 서버 해시로 간주
105
+ syncedAt: v1Data.lastSync,
106
+ source: 'local', // v1에서는 MCP만 사용했으므로 local로 간주
107
+ };
108
+ }
109
+
110
+ console.error(`[DocuKing] 마이그레이션 완료: ${Object.keys(v2Index.files).length}개 파일`);
111
+ return v2Index;
112
+ }
113
+
114
+ /**
115
+ * 빈 인덱스 생성 (v2)
54
116
  */
55
117
  function createEmptyIndex() {
56
118
  return {
57
119
  version: INDEX_VERSION,
58
120
  lastSync: null,
121
+ lastPush: null,
122
+ lastPull: null,
59
123
  files: {},
124
+ deletedFiles: {}, // 좀비 방지 핵심
60
125
  };
61
126
  }
62
127
 
@@ -149,9 +214,13 @@ export function detectChanges(localPath, index, filePaths, getFullPath) {
149
214
  }
150
215
 
151
216
  /**
152
- * 인덱스에 파일 정보 업데이트
217
+ * 인덱스에 파일 정보 업데이트 (v2)
218
+ *
219
+ * @param {Object} options - 추가 옵션
220
+ * @param {string} options.serverHash - 서버 해시 (서버와 동기화된 경우)
221
+ * @param {string} options.source - 파일 출처 (local | web)
153
222
  */
154
- export function updateIndexEntry(index, serverPath, fullPath, hash) {
223
+ export function updateIndexEntry(index, serverPath, fullPath, hash, options = {}) {
155
224
  try {
156
225
  const stat = fs.statSync(fullPath);
157
226
 
@@ -159,8 +228,17 @@ export function updateIndexEntry(index, serverPath, fullPath, hash) {
159
228
  hash,
160
229
  mtime: stat.mtimeMs,
161
230
  size: stat.size,
231
+ // v2 필드
232
+ serverHash: options.serverHash || hash,
233
+ syncedAt: new Date().toISOString(),
234
+ source: options.source || 'local',
162
235
  };
163
236
 
237
+ // deletedFiles에서 제거 (부활한 경우)
238
+ if (index.deletedFiles && index.deletedFiles[serverPath]) {
239
+ delete index.deletedFiles[serverPath];
240
+ }
241
+
164
242
  return true;
165
243
  } catch (e) {
166
244
  return false;
@@ -174,6 +252,67 @@ export function removeIndexEntry(index, serverPath) {
174
252
  delete index.files[serverPath];
175
253
  }
176
254
 
255
+ /**
256
+ * 파일을 삭제된 것으로 표시 (좀비 방지 핵심)
257
+ *
258
+ * 로컬에서 파일을 삭제하면 이 함수를 호출하여 deletedFiles에 기록.
259
+ * 이후 Pull 시 이 파일은 서버에 있어도 다운로드하지 않음.
260
+ *
261
+ * @param {Object} index - 인덱스 객체
262
+ * @param {string} serverPath - 서버 경로
263
+ * @param {string} lastHash - 삭제 전 마지막 해시
264
+ */
265
+ export function markAsDeleted(index, serverPath, lastHash) {
266
+ // files에서 가져올 수 있으면 가져옴
267
+ const entry = index.files[serverPath];
268
+ const hash = lastHash || (entry ? entry.hash : null);
269
+
270
+ // deletedFiles에 기록
271
+ if (!index.deletedFiles) {
272
+ index.deletedFiles = {};
273
+ }
274
+
275
+ index.deletedFiles[serverPath] = {
276
+ deletedAt: new Date().toISOString(),
277
+ lastHash: hash,
278
+ };
279
+
280
+ // files에서 제거
281
+ delete index.files[serverPath];
282
+
283
+ console.error(`[DocuKing] 삭제 표시: ${serverPath}`);
284
+ }
285
+
286
+ /**
287
+ * 로컬에서 삭제된 파일인지 확인 (좀비 방지)
288
+ *
289
+ * Pull 시 이 함수로 확인하여 true면 다운로드 스킵.
290
+ *
291
+ * @param {Object} index - 인덱스 객체
292
+ * @param {string} serverPath - 서버 경로
293
+ * @returns {{ isDeleted: boolean, entry: Object | null }}
294
+ */
295
+ export function wasDeletedLocally(index, serverPath) {
296
+ if (!index.deletedFiles) {
297
+ return { isDeleted: false, entry: null };
298
+ }
299
+
300
+ const entry = index.deletedFiles[serverPath];
301
+ return {
302
+ isDeleted: !!entry,
303
+ entry: entry || null,
304
+ };
305
+ }
306
+
307
+ /**
308
+ * 삭제 기록 제거 (서버에서도 삭제 완료된 경우)
309
+ */
310
+ export function clearDeletedEntry(index, serverPath) {
311
+ if (index.deletedFiles) {
312
+ delete index.deletedFiles[serverPath];
313
+ }
314
+ }
315
+
177
316
  /**
178
317
  * 인덱스의 해시와 비교하여 실제 변경 여부 확인
179
318
  * (mtime 변경됐지만 내용은 같을 수 있음)
@@ -208,35 +347,272 @@ export function verifyChanges(index, changedPaths, getFullPath) {
208
347
  }
209
348
 
210
349
  /**
211
- * 동기화 후 인덱스 일괄 업데이트
350
+ * 동기화 후 인덱스 일괄 업데이트 (v2)
212
351
  *
213
352
  * @param {Object} index - 인덱스 객체
214
- * @param {Array} syncedFiles - 동기화된 파일 목록 [{ serverPath, fullPath, hash }]
353
+ * @param {Array} syncedFiles - 동기화된 파일 목록 [{ serverPath, fullPath, hash, serverHash?, source? }]
215
354
  * @param {Array} deletedPaths - 삭제된 파일 경로 목록
355
+ * @param {Object} options - 추가 옵션
356
+ * @param {string} options.syncType - 'push' | 'pull' (lastPush/lastPull 업데이트용)
216
357
  */
217
- export function updateIndexAfterSync(index, syncedFiles, deletedPaths = []) {
358
+ export function updateIndexAfterSync(index, syncedFiles, deletedPaths = [], options = {}) {
218
359
  // 동기화된 파일 업데이트
219
360
  for (const file of syncedFiles) {
220
- updateIndexEntry(index, file.serverPath, file.fullPath, file.hash);
361
+ updateIndexEntry(index, file.serverPath, file.fullPath, file.hash, {
362
+ serverHash: file.serverHash || file.hash,
363
+ source: file.source || 'local',
364
+ });
221
365
  }
222
366
 
223
- // 삭제된 파일 제거
367
+ // 삭제된 파일 처리
224
368
  for (const serverPath of deletedPaths) {
369
+ // Push로 서버에서도 삭제 완료된 경우: deletedFiles에서도 제거
370
+ if (options.syncType === 'push') {
371
+ clearDeletedEntry(index, serverPath);
372
+ }
373
+ // files에서 제거
225
374
  removeIndexEntry(index, serverPath);
226
375
  }
376
+
377
+ // syncType에 따라 lastPush/lastPull 업데이트
378
+ if (options.syncType === 'push') {
379
+ index.lastPush = new Date().toISOString();
380
+ } else if (options.syncType === 'pull') {
381
+ index.lastPull = new Date().toISOString();
382
+ }
227
383
  }
228
384
 
229
385
  /**
230
- * 인덱스 통계 반환
386
+ * 인덱스 통계 반환 (v2)
231
387
  */
232
388
  export function getIndexStats(index) {
233
389
  const files = Object.keys(index.files);
390
+ const deletedFiles = Object.keys(index.deletedFiles || {});
234
391
  const totalSize = Object.values(index.files).reduce((sum, f) => sum + (f.size || 0), 0);
235
392
 
236
393
  return {
237
394
  fileCount: files.length,
395
+ deletedCount: deletedFiles.length,
238
396
  totalSize,
239
397
  totalSizeMB: (totalSize / 1024 / 1024).toFixed(2),
240
398
  lastSync: index.lastSync,
399
+ lastPush: index.lastPush,
400
+ lastPull: index.lastPull,
401
+ };
402
+ }
403
+
404
+ /**
405
+ * 3-way 비교를 위한 파일 분류 (Push용)
406
+ *
407
+ * @param {Object} localFiles - 로컬 파일 맵 { serverPath: { hash, mtime, fullPath } }
408
+ * @param {Object} index - 인덱스 객체
409
+ * @param {Object} serverMeta - 서버 메타데이터 { pathToMeta, pathToHash }
410
+ * @returns {Object} 분류된 액션
411
+ */
412
+ export function classifyForPush(localFiles, index, serverMeta) {
413
+ const actions = {
414
+ upload: [], // 새 파일 또는 수정된 파일
415
+ delete: [], // 로컬에서 삭제된 파일 (서버에서도 삭제)
416
+ skip: [], // 변경 없음
417
+ conflict: [], // 충돌 (양쪽 수정)
418
+ resurrect: [], // 서버에서 삭제됐지만 로컬에서 수정됨
241
419
  };
420
+
421
+ // 1. 로컬 파일 처리
422
+ for (const [serverPath, local] of Object.entries(localFiles)) {
423
+ const inIndex = index.files[serverPath];
424
+ const onServer = serverMeta.pathToMeta?.[serverPath];
425
+
426
+ if (!inIndex && !onServer) {
427
+ // 새 파일 (인덱스에도, 서버에도 없음)
428
+ actions.upload.push({ path: serverPath, hash: local.hash, action: 'create' });
429
+ }
430
+ else if (!inIndex && onServer) {
431
+ // 인덱스에 없지만 서버에 있음
432
+ if (local.hash === onServer.hash) {
433
+ // 내용 동일 - 이미 동기화됨
434
+ actions.skip.push({ path: serverPath, reason: 'already-synced' });
435
+ } else {
436
+ // 내용 다름 - 업로드
437
+ actions.upload.push({ path: serverPath, hash: local.hash, action: 'update' });
438
+ }
439
+ }
440
+ else if (inIndex && !onServer) {
441
+ // 인덱스에 있지만 서버에 없음 (서버에서 삭제됨)
442
+ if (local.hash === inIndex.hash) {
443
+ // 로컬 수정 없음 - 서버 삭제 반영? 아니, 로컬 우선이므로 부활
444
+ actions.upload.push({ path: serverPath, hash: local.hash, action: 'create' });
445
+ } else {
446
+ // 로컬 수정됨 - 부활 (수정된 버전으로)
447
+ actions.resurrect.push({ path: serverPath, hash: local.hash });
448
+ }
449
+ }
450
+ else if (inIndex && onServer) {
451
+ // 인덱스에도 서버에도 있음 (정상 케이스)
452
+ const localModified = local.hash !== inIndex.hash;
453
+ const serverModified = onServer.hash !== (inIndex.serverHash || inIndex.hash);
454
+
455
+ if (!localModified && !serverModified) {
456
+ // 양쪽 다 수정 없음
457
+ actions.skip.push({ path: serverPath, reason: 'unchanged' });
458
+ }
459
+ else if (localModified && !serverModified) {
460
+ // 로컬만 수정됨 - 업로드
461
+ actions.upload.push({ path: serverPath, hash: local.hash, action: 'update' });
462
+ }
463
+ else if (!localModified && serverModified) {
464
+ // 서버만 수정됨 - Push에서는 무시 (Pull에서 처리)
465
+ actions.skip.push({ path: serverPath, reason: 'server-modified' });
466
+ }
467
+ else {
468
+ // 양쪽 다 수정됨 - 충돌 (로컬 우선 정책: 업로드)
469
+ actions.upload.push({ path: serverPath, hash: local.hash, action: 'update', conflict: true });
470
+ }
471
+ }
472
+ }
473
+
474
+ // 2. 삭제된 파일 처리 (인덱스에 있는데 로컬에 없는 파일)
475
+ for (const [serverPath, entry] of Object.entries(index.files)) {
476
+ if (localFiles[serverPath]) continue; // 로컬에 있음 - 스킵
477
+
478
+ const onServer = serverMeta.pathToMeta?.[serverPath];
479
+ if (!onServer) continue; // 서버에도 없음 - 스킵
480
+
481
+ // 로컬 삭제됨, 서버에 있음
482
+ if (onServer.source === 'web') {
483
+ // web 출처 파일은 삭제 보호
484
+ actions.skip.push({ path: serverPath, reason: 'protected-web-source' });
485
+ }
486
+ else if (onServer.hash !== (entry.serverHash || entry.hash)) {
487
+ // 서버가 수정됨 - 충돌 (로컬 삭제 vs 서버 수정)
488
+ actions.conflict.push({
489
+ path: serverPath,
490
+ type: 'delete-vs-modify',
491
+ localAction: 'delete',
492
+ serverHash: onServer.hash,
493
+ });
494
+ }
495
+ else {
496
+ // 안전하게 삭제 가능
497
+ actions.delete.push({ path: serverPath });
498
+ }
499
+ }
500
+
501
+ return actions;
502
+ }
503
+
504
+ /**
505
+ * 3-way 비교를 위한 파일 분류 (Pull용)
506
+ *
507
+ * @param {Object} localFiles - 로컬 파일 맵 { serverPath: { hash, mtime, fullPath } }
508
+ * @param {Object} index - 인덱스 객체
509
+ * @param {Object} serverMeta - 서버 메타데이터 { pathToMeta, deletedFiles }
510
+ * @returns {Object} 분류된 액션
511
+ */
512
+ export function classifyForPull(localFiles, index, serverMeta) {
513
+ const actions = {
514
+ download: [], // 새 서버 파일
515
+ update: [], // 서버 수정됨, 로컬 수정 없음
516
+ deleteLocal: [], // 서버에서 삭제됨
517
+ skip: [], // 변경 없음
518
+ conflict: [], // 충돌
519
+ };
520
+
521
+ // 1. 서버 파일 처리
522
+ for (const [serverPath, server] of Object.entries(serverMeta.pathToMeta || {})) {
523
+ if (server.deletedAt) continue; // 삭제된 파일은 별도 처리
524
+
525
+ const inIndex = index.files[serverPath];
526
+ const local = localFiles[serverPath];
527
+ const deleted = wasDeletedLocally(index, serverPath);
528
+
529
+ if (!inIndex && !local && !deleted.isDeleted) {
530
+ // 새 서버 파일 (인덱스에도, 로컬에도 없음)
531
+ actions.download.push({ path: serverPath, hash: server.hash, source: server.source });
532
+ }
533
+ else if (!inIndex && !local && deleted.isDeleted) {
534
+ // 로컬에서 삭제한 파일 (좀비 방지)
535
+ if (server.hash === deleted.entry.lastHash) {
536
+ // 서버 변경 없음 - 스킵 (좀비 방지)
537
+ actions.skip.push({ path: serverPath, reason: 'locally-deleted-zombie-prevention' });
538
+ } else {
539
+ // 서버가 수정됨 - 충돌 (로컬 삭제 vs 서버 수정)
540
+ actions.conflict.push({
541
+ path: serverPath,
542
+ type: 'delete-vs-modify',
543
+ localAction: 'deleted',
544
+ serverHash: server.hash,
545
+ });
546
+ }
547
+ }
548
+ else if (inIndex && !local) {
549
+ // 인덱스에 있지만 로컬에 없음 (로컬에서 삭제됨)
550
+ // deletedFiles에 기록하고 스킵
551
+ actions.skip.push({ path: serverPath, reason: 'locally-deleted', markDeleted: true });
552
+ }
553
+ else if (inIndex && local) {
554
+ // 인덱스에도 로컬에도 있음
555
+ const localModified = local.hash !== inIndex.hash;
556
+ const serverModified = server.hash !== (inIndex.serverHash || inIndex.hash);
557
+
558
+ if (!localModified && !serverModified) {
559
+ actions.skip.push({ path: serverPath, reason: 'unchanged' });
560
+ }
561
+ else if (!localModified && serverModified) {
562
+ // 서버만 수정됨 - 다운로드
563
+ actions.update.push({ path: serverPath, hash: server.hash, source: server.source });
564
+ }
565
+ else if (localModified && !serverModified) {
566
+ // 로컬만 수정됨 - 스킵
567
+ actions.skip.push({ path: serverPath, reason: 'local-modified' });
568
+ }
569
+ else {
570
+ // 양쪽 다 수정됨 - 충돌
571
+ actions.conflict.push({
572
+ path: serverPath,
573
+ type: 'both-modified',
574
+ localHash: local.hash,
575
+ serverHash: server.hash,
576
+ });
577
+ }
578
+ }
579
+ else if (!inIndex && local) {
580
+ // 인덱스에 없지만 로컬에 있음
581
+ if (local.hash === server.hash) {
582
+ // 내용 동일 - 인덱스에 추가만
583
+ actions.skip.push({ path: serverPath, reason: 'already-have', addToIndex: true });
584
+ } else {
585
+ // 내용 다름 - 로컬 수정 보호
586
+ actions.skip.push({ path: serverPath, reason: 'local-different' });
587
+ }
588
+ }
589
+ }
590
+
591
+ // 2. 서버 삭제 파일 처리
592
+ for (const deleted of (serverMeta.deletedFiles || [])) {
593
+ const inIndex = index.files[deleted.path];
594
+ const local = localFiles[deleted.path];
595
+
596
+ if (inIndex && local) {
597
+ const localModified = local.hash !== inIndex.hash;
598
+
599
+ if (!localModified) {
600
+ // 로컬 수정 없음 - 삭제
601
+ const serverDeletedAt = new Date(deleted.deletedAt);
602
+ const localMtime = new Date(local.mtime);
603
+
604
+ if (serverDeletedAt > localMtime) {
605
+ actions.deleteLocal.push({ path: deleted.path });
606
+ } else {
607
+ // 로컬 mtime이 더 최신 - 유지 (다음 Push에서 부활)
608
+ actions.skip.push({ path: deleted.path, reason: 'local-newer-than-delete' });
609
+ }
610
+ } else {
611
+ // 로컬 수정됨 - 유지 (다음 Push에서 부활)
612
+ actions.skip.push({ path: deleted.path, reason: 'local-modified-server-deleted' });
613
+ }
614
+ }
615
+ }
616
+
617
+ return actions;
242
618
  }
package/lib/init.js CHANGED
@@ -64,7 +64,7 @@ ${marker}
64
64
  - 의미 있는 작업 시작 시 (추적용)
65
65
 
66
66
  ### 규칙
67
- 1. 동기화 대상: xx_*/ + yy_All_Docu/ + zz_ai_*/ 폴더 모두
67
+ 1. 동기화 대상: xx_*/ + xy_TalkTodoPlan/ + yy_All_Docu/ + zz_Coworker_*/ 폴더 모두
68
68
  2. 킹푸시는 Plan 완료(done) 시 자동 실행
69
69
  3. **Talk → Todo → Plan → Done** 순서 준수
70
70
  4. Plan 전에 반드시 Talk로 배경 기록
@@ -85,8 +85,8 @@ ${marker}
85
85
 
86
86
  | 역할 | Push (올리기) | Pull (내리기) | 삭제 |
87
87
  |------|---------------|---------------|------|
88
- | **오너** | \`xx_*/\` + \`yy_All_Docu/\` + \`zz_ai_*/\` | 합집합 전체 | 자기 영역만 |
89
- | **협업자** | \`yy_Coworker_{본인}/\` (안에 zz_ai_* 포함) | 합집합 전체 | 자기 영역만 |
88
+ | **오너** | \`xx_*/\` + \`xy_TalkTodoPlan/\` + \`yy_All_Docu/\` | 합집합 전체 | 자기 영역만 |
89
+ | **협업자** | \`zz_Coworker_{본인}/\` | 합집합 전체 | 자기 영역만 |
90
90
 
91
91
  ### 합집합이란?
92
92
  - 서버 = 오너 파일 + 모든 협업자 파일
@@ -97,7 +97,7 @@ ${marker}
97
97
  1. **초대 수락** (웹) → 프로젝트 진입, 자기 빈폴더 생성됨
98
98
  2. **MCP init** → 로컬에 자기 폴더 생성
99
99
  3. **Pull** → 합집합 전체 내려옴
100
- 4. **작업** → \`yy_Coworker_{본인}/\` 안에서
100
+ 4. **작업** → \`zz_Coworker_{본인}/\` 안에서
101
101
  5. **Push** → 자기 폴더만 올라감
102
102
 
103
103
  ### 폴더 구조 (Pull 후)
@@ -108,14 +108,14 @@ project/
108
108
  ├── xx_Urgent/ ← 긴급 보고 (킹어전트)
109
109
  ├── xy_TalkTodoPlan/ ← 오너 AI 기록 (Talk+Todo+Plan 통합)
110
110
  ├── yy_All_Docu/ ← 오너 문서
111
- ├── yy_Coworker_a_Kim/ ← 협업자 Kim
111
+ ├── zz_Coworker_Kim/ ← 협업자 Kim
112
112
  │ ├── 작업문서.md
113
- │ └── zz_TalkTodoPlan/ ← Kim의 AI 기록
114
- └── yy_Coworker_b_Lee/ ← 협업자 Lee
113
+ │ └── Kim_TalkTodoPlan/ ← Kim의 AI 기록
114
+ └── zz_Coworker_Lee/ ← 협업자 Lee
115
115
  \`\`\`
116
116
 
117
117
  ### AI가 협업자에게 안내할 것
118
- - 작업 폴더: \`yy_Coworker_{폴더명}/\`
118
+ - 작업 폴더: \`zz_Coworker_{폴더명}/\`
119
119
  - 남의 파일: 읽기만 가능, 수정 불가
120
120
  - 수정 제안: 자기 폴더에 작성 또는 직접 연락
121
121
 
@@ -248,7 +248,7 @@ xx_Policy/
248
248
  xx_Urgent/
249
249
  xy_TalkTodoPlan/
250
250
  yy_All_Docu/
251
- yy_Coworker_*/
251
+ zz_Coworker_*/
252
252
  .docuking/config.json
253
253
  .claude/rules/local/_coworker_config.md
254
254
  `;
package/lib/utils.js CHANGED
@@ -47,7 +47,7 @@ export function generatePlanId() {
47
47
 
48
48
  /**
49
49
  * 계획 파일 찾기 (planId로 검색)
50
- * @param {string} planDir - AI 폴더 경로 (xy_TalkTodoPlan 또는 zz_TalkTodoPlan)
50
+ * @param {string} planDir - AI 폴더 경로 (xy_TalkTodoPlan 또는 {폴더명}_TalkTodoPlan)
51
51
  * @param {string} planId - 찾을 계획 ID
52
52
  */
53
53
  export function findPlanFiles(planDir, planId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docuking-mcp",
3
- "version": "3.1.0",
3
+ "version": "3.6.0",
4
4
  "description": "DocuKing MCP Server - AI 시대의 문서 협업 플랫폼",
5
5
  "type": "module",
6
6
  "main": "index.js",