friday-mcp-v2 3.0.1 → 3.0.3

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.
@@ -207,13 +207,15 @@ function checkRevisionMismatch(expectedRevision, currentState, mode, postId, ses
207
207
  const currentRevision = computeRevision(currentState, mode);
208
208
  if (expectedRevision === currentRevision) return null;
209
209
 
210
- // 不一致 — 更新は拒否するが、最新 snapshot を返して再取得の往復を省く
210
+ // 不一致 — snapshot をキャッシュに登録(AI が新 snapshotId で refetch 可能に)
211
+ // Phase 2: blocks 一覧は出力しない(コンテキスト軽量化)
211
212
  const latestSnapshot = buildSnapshotFromState(currentState, mode, postId, sessionId, siteName);
212
- let text = `❌ REVISION_MISMATCH: 記事の構造が変更されています(expected: ${expectedRevision}, current: ${currentRevision})。\n` +
213
- `更新はスキップしました。以下の最新 snapshot で再試行してください。`;
214
- if (latestSnapshot) {
215
- text = appendSnapshotToText(text, latestSnapshot);
216
- }
213
+ const snapshotLine = latestSnapshot
214
+ ? `\n[snapshot:${latestSnapshot.snapshotId} rev:${latestSnapshot.revision}]`
215
+ : '';
216
+ const text = `❌ REVISION_MISMATCH: expected ${expectedRevision}, actual ${currentRevision}` +
217
+ snapshotLine +
218
+ `\n→ get_article_structure で最新を取得してください`;
217
219
  return { content: [{ type: "text", text }], isError: true };
218
220
  }
219
221
 
@@ -1005,11 +1007,11 @@ async function resolveRefsAndCheckRevision({
1005
1007
  try {
1006
1008
  if (ref) {
1007
1009
  const index = resolveRefFromState(snapshotId, ref, mode, sessionId, postId, currentState);
1008
- return { index };
1010
+ return { index, currentState };
1009
1011
  }
1010
1012
  if (refs) {
1011
1013
  const indices = refs.map(r => resolveRefFromState(snapshotId, r, mode, sessionId, postId, currentState));
1012
- return { indices };
1014
+ return { indices, currentState };
1013
1015
  }
1014
1016
  } catch (e) {
1015
1017
  return { error: {
@@ -1021,10 +1023,11 @@ async function resolveRefsAndCheckRevision({
1021
1023
 
1022
1024
  // Case 2: ref なし + expectedRevision のみ
1023
1025
  if (expectedRevision) {
1024
- const { error } = await acquireFreshState({
1026
+ const { currentState, error } = await acquireFreshState({
1025
1027
  expectedRevision, mode, client, postId, sessionId, siteName,
1026
1028
  });
1027
1029
  if (error) return { error };
1030
+ return { currentState };
1028
1031
  }
1029
1032
 
1030
1033
  // Case 3: どちらもなし
@@ -1032,15 +1035,62 @@ async function resolveRefsAndCheckRevision({
1032
1035
  }
1033
1036
 
1034
1037
  /**
1035
- * 更新後のレスポンス snapshot を構築する
1038
+ * 下流レスポンスの blocks を buildSnapshotBlocks / computeRevision が
1039
+ * 期待する state.allBlocks 形式に正規化する。
1040
+ * @param {Array} blocks - 下流 formatBlocksForSnapshot() の結果
1041
+ * @returns {{ allBlocks: Array }}
1042
+ */
1043
+ function normalizeDownstreamBlocks(blocks) {
1044
+ return {
1045
+ allBlocks: blocks.map(b => ({
1046
+ index: b.index,
1047
+ type: b.type,
1048
+ html: b.html || '',
1049
+ attributes: b.attributes || {},
1050
+ section: b.section || null,
1051
+ depth: b.depth || 0,
1052
+ parentIndex: b.parentIndex ?? null,
1053
+ })),
1054
+ };
1055
+ }
1056
+
1057
+ /**
1058
+ * 下流レスポンスの result.blocks から snapshot を構築する。
1059
+ * blocks がない場合は null を返す(呼び出し元でフォールバック判断)。
1060
+ * @param {object} result - 下流レスポンス
1061
+ * @param {string} mode
1062
+ * @param {number} postId
1063
+ * @param {string|null} sessionId
1064
+ * @param {string} siteName
1065
+ * @returns {object|null} { snapshotId, revision, blocks[] } or null
1066
+ */
1067
+ function buildResponseSnapshotFromResult(result, mode, postId, sessionId, siteName) {
1068
+ const blocks = result?.blocks;
1069
+ if (!blocks || !Array.isArray(blocks) || blocks.length === 0) {
1070
+ return null;
1071
+ }
1072
+ const state = normalizeDownstreamBlocks(blocks);
1073
+ return buildSnapshotFromState(state, mode, postId, sessionId, siteName);
1074
+ }
1075
+
1076
+ /**
1077
+ * 更新後のレスポンス snapshot を構築する。
1078
+ * result が渡され、result.blocks があればそこから構築(再取得不要)。
1079
+ * なければ従来通り getCurrentStructure() で再取得する。
1036
1080
  * @param {string} mode
1037
1081
  * @param {object} client
1038
1082
  * @param {number} postId
1039
1083
  * @param {string|null} sessionId
1040
1084
  * @param {string} siteName
1085
+ * @param {object} [result] - 下流レスポンス(Phase 1: blocks 再利用)
1041
1086
  * @returns {Promise<object|null>} { snapshotId, revision, blocks[] } or null
1042
1087
  */
1043
- async function buildResponseSnapshot(mode, client, postId, sessionId, siteName) {
1088
+ async function buildResponseSnapshot(mode, client, postId, sessionId, siteName, result) {
1089
+ // Phase 1: 下流 result.blocks があればそこから構築(再取得不要)
1090
+ if (result) {
1091
+ const fromResult = buildResponseSnapshotFromResult(result, mode, postId, sessionId, siteName);
1092
+ if (fromResult) return fromResult;
1093
+ }
1044
1094
  let newState;
1045
1095
  try {
1046
1096
  newState = await getCurrentStructure(mode, client, postId, sessionId);
@@ -1083,6 +1133,7 @@ async function buildResponseSnapshot(mode, client, postId, sessionId, siteName)
1083
1133
  }
1084
1134
 
1085
1135
  /**
1136
+ * @deprecated Phase 7 で削除予定。batch 経路および index 経路の後方互換用。
1086
1137
  * snapshot 情報をテキストレスポンスに付加する
1087
1138
  * blocks[] を含めて AI が新しい ref を取得できるようにする
1088
1139
  * @param {string} text - 元のレスポンステキスト
@@ -1093,7 +1144,7 @@ async function buildResponseSnapshot(mode, client, postId, sessionId, siteName)
1093
1144
  * @param {number} [refInfo.resolvedIndex] - 解決された元の index
1094
1145
  * @returns {string} snapshot 行と blocks 一覧が付加されたテキスト
1095
1146
  */
1096
- function appendSnapshotToText(text, snapshot, refInfo) {
1147
+ function appendSnapshotToTextLegacy(text, snapshot, refInfo) {
1097
1148
  if (!snapshot) return text;
1098
1149
 
1099
1150
  // 1→N 展開チェック(単体操作時のみ)
@@ -1115,16 +1166,494 @@ function appendSnapshotToText(text, snapshot, refInfo) {
1115
1166
  }
1116
1167
 
1117
1168
  let out = text + `\n\n[snapshot:${snapshot.snapshotId} rev:${snapshot.revision}]${expandedLine}`;
1118
- if (snapshot.blocks && snapshot.blocks.length > 0) {
1119
- out += '\nblocks:';
1120
- for (const b of snapshot.blocks) {
1121
- const parent = b.parentIndex !== null ? `,p:${b.parentIndex}` : '';
1122
- out += `\n [${b.index}|${b.ref}] ${b.type} (d:${b.depth}${parent})`;
1169
+ return out;
1170
+ }
1171
+
1172
+ /**
1173
+ * Phase 5: update_blocks / insert_block の差分/Legacy 自動分岐。
1174
+ * inputRef が null なら即 Legacy。差分構築失敗時も Legacy フォールバック。
1175
+ * @param {string} text - メッセージ本文
1176
+ * @param {object|null} snap - buildResponseSnapshot の結果
1177
+ * @param {object} result - 下流レスポンス
1178
+ * @param {object|null} preState - _preState
1179
+ * @param {string|null} inputRef - 入力 ref(null = Legacy 固定)
1180
+ * @param {boolean} isInsert - insertOnly 操作か
1181
+ * @param {object|null} refInfo - Legacy フォールバック用 _refInfo
1182
+ * @returns {string}
1183
+ */
1184
+ function buildUpdateDiffResponse(text, snap, result, preState, inputRef, isInsert, refInfo) {
1185
+ if (!inputRef || !snap) {
1186
+ return appendSnapshotToTextLegacy(text, snap, refInfo);
1187
+ }
1188
+
1189
+ if (isInsert) {
1190
+ const changeInfo = buildChangeInfoFromResult('inserted', snap, result, preState);
1191
+ if (changeInfo) return appendRefChangesToText(text, changeInfo);
1192
+ return appendSnapshotToTextLegacy(text, snap, refInfo);
1193
+ }
1194
+
1195
+ // update / expand 判定
1196
+ // success:false の change を除外(invalid regex, parse failure 等)
1197
+ const changes = result?.changes?.filter(c => c.success !== false);
1198
+ if (!changes || changes.length === 0) {
1199
+ return appendSnapshotToTextLegacy(text, snap, refInfo);
1200
+ }
1201
+ // filtered 済み changes で result を差し替え(buildChangeInfoFromResult が result.changes を直接参照するため)
1202
+ const filteredResult = { ...result, changes };
1203
+ if (changes.length === 1 && changes[0]?.newIndices?.length > 1) {
1204
+ // 1→N 展開(単体 target のみ)
1205
+ const changeInfo = buildChangeInfoFromResult('expanded', snap, filteredResult, preState, { inputRef });
1206
+ if (changeInfo) return appendRefChangesToText(text, changeInfo);
1207
+ } else if (changes.length === 1) {
1208
+ // 1→1 更新(単体 ref で成功 1 件のときだけ差分化)
1209
+ const changeInfo = buildChangeInfoFromResult('updated', snap, filteredResult, preState);
1210
+ if (changeInfo) return appendRefChangesToText(text, changeInfo);
1211
+ }
1212
+
1213
+ return appendSnapshotToTextLegacy(text, snap, refInfo);
1214
+ }
1215
+
1216
+ // ============================================================
1217
+ // Phase 2: ref 変化分類ロジック(差分通知方式の基盤)
1218
+ // ============================================================
1219
+
1220
+ /**
1221
+ * @typedef {Object} DeletedChange
1222
+ * @property {string} snapshotId
1223
+ * @property {string} revision
1224
+ * @property {'deleted'} type
1225
+ * @property {string[]} deletedRefs - 削除された ref 配列 ['r5', 'r8']
1226
+ * @property {boolean} [refetchRequired]
1227
+ */
1228
+
1229
+ /**
1230
+ * @typedef {Object} InsertedChange
1231
+ * @property {string} snapshotId
1232
+ * @property {string} revision
1233
+ * @property {'inserted'} type
1234
+ * @property {Array<{index: number, ref: string, blockType: string, depth: number}>} inserted
1235
+ * @property {boolean} [refetchRequired]
1236
+ */
1237
+
1238
+ /**
1239
+ * @typedef {Object} MovedChange
1240
+ * @property {string} snapshotId
1241
+ * @property {string} revision
1242
+ * @property {'moved'} type
1243
+ * @property {Array<{oldRef: string, newRef: string}>} moved
1244
+ * @property {boolean} [refetchRequired]
1245
+ */
1246
+
1247
+ /**
1248
+ * @typedef {Object} UpdatedChange
1249
+ * @property {string} snapshotId
1250
+ * @property {string} revision
1251
+ * @property {'updated'} type
1252
+ * @property {string[]} updatedRefs - fingerprint 変化した ref 配列
1253
+ * @property {boolean} [refetchRequired]
1254
+ */
1255
+
1256
+ /**
1257
+ * @typedef {Object} ExpandedChange
1258
+ * @property {string} snapshotId
1259
+ * @property {string} revision
1260
+ * @property {'expanded'} type
1261
+ * @property {{oldRef: string, newRefs: string[]}} expanded
1262
+ * @property {boolean} [refetchRequired]
1263
+ */
1264
+
1265
+ /**
1266
+ * @typedef {Object} DuplicatedChange
1267
+ * @property {string} snapshotId
1268
+ * @property {string} revision
1269
+ * @property {'duplicated'} type
1270
+ * @property {string} sourceRef
1271
+ * @property {string} newRef
1272
+ * @property {number} sourceIndex
1273
+ * @property {number} newIndex
1274
+ * @property {boolean} [refetchRequired]
1275
+ */
1276
+
1277
+ /**
1278
+ * @typedef {DeletedChange|InsertedChange|MovedChange|UpdatedChange|ExpandedChange|DuplicatedChange} ChangeInfo
1279
+ */
1280
+
1281
+ /**
1282
+ * ref 変化情報をテキストに付加する��差分モード)��
1283
+ * blocks 一��は出力しない。
1284
+ * @param {string} text
1285
+ * @param {ChangeInfo|null} changeInfo
1286
+ * @returns {string}
1287
+ */
1288
+ function appendRefChangesToText(text, changeInfo) {
1289
+ if (!changeInfo) return text;
1290
+
1291
+ let out = text + `\n\n[snapshot:${changeInfo.snapshotId} rev:${changeInfo.revision}]`;
1292
+
1293
+ switch (changeInfo.type) {
1294
+ case 'deleted':
1295
+ out += `\ndeleted: [${changeInfo.deletedRefs.join(', ')}] (invalid in new snapshot)`;
1296
+ break;
1297
+ case 'inserted':
1298
+ out += '\ninserted: ' + changeInfo.inserted
1299
+ .map(b => `[${b.index}|${b.ref}] ${b.blockType} (d:${b.depth})`)
1300
+ .join(', ');
1301
+ break;
1302
+ case 'moved':
1303
+ out += '\nmoved: ' + changeInfo.moved
1304
+ .map(m => `${m.oldRef}\u2192${m.newRef}`)
1305
+ .join(', ');
1306
+ break;
1307
+ case 'updated':
1308
+ out += `\nupdated: [${changeInfo.updatedRefs.join(', ')}] (use new snapshot)`;
1309
+ break;
1310
+ case 'expanded':
1311
+ out += `\nexpanded: ${changeInfo.expanded.oldRef} \u2192 [${changeInfo.expanded.newRefs.join(', ')}]`;
1312
+ break;
1313
+ case 'duplicated':
1314
+ out += `\nduplicated: ${changeInfo.sourceRef} \u2192 ${changeInfo.newRef} (source:[${changeInfo.sourceIndex}], new:[${changeInfo.newIndex}]) (use new snapshot)`;
1315
+ break;
1316
+ }
1317
+
1318
+ if (changeInfo.refetchRequired) {
1319
+ out += '\n\u26a0 refetchRequired: \u5b50\u30d6\u30ed\u30c3\u30af\u5909\u66f4\u3042\u308a\u3002get_article_structure \u3067\u6700\u65b0\u3092\u53d6\u5f97\u3057\u3066\u304f\u3060\u3055\u3044\u3002';
1320
+ }
1321
+
1322
+ return out;
1323
+ }
1324
+
1325
+ /**
1326
+ * @typedef {Object} BatchDiffEntry
1327
+ * @property {number} opIndex
1328
+ * @property {'updated'|'expanded'|'failed'|'skipped'} type
1329
+ * @property {string} [ref] - updated/failed/skipped 時
1330
+ * @property {string} [oldRef] - expanded 時
1331
+ * @property {string[]} [newRefs] - expanded 時
1332
+ * @property {string} [error] - failed 時
1333
+ * @property {boolean} [refetchRequired] - この entry 単体の refetch 判定
1334
+ */
1335
+
1336
+ /**
1337
+ * @typedef {Object} BatchDiffInfo
1338
+ * @property {string} snapshotId
1339
+ * @property {string} revision
1340
+ * @property {'batch'} type
1341
+ * @property {BatchDiffEntry[]} entries
1342
+ * @property {boolean} [refetchRequired] - batch 全体の OR
1343
+ */
1344
+
1345
+ /**
1346
+ * BatchDiffInfo をテキストに付加する(batch 差分モード)。
1347
+ * @param {string} text
1348
+ * @param {BatchDiffInfo|null} batchInfo
1349
+ * @returns {string}
1350
+ */
1351
+ function appendBatchRefChangesToText(text, batchInfo) {
1352
+ if (!batchInfo) return text;
1353
+
1354
+ let out = text + `\n\n[snapshot:${batchInfo.snapshotId} rev:${batchInfo.revision}]`;
1355
+ out += '\nchanges:';
1356
+
1357
+ for (const entry of batchInfo.entries) {
1358
+ switch (entry.type) {
1359
+ case 'updated':
1360
+ out += `\n [${entry.opIndex}] updated: [${entry.ref}]`;
1361
+ break;
1362
+ case 'expanded':
1363
+ out += `\n [${entry.opIndex}] expanded: ${entry.oldRef} \u2192 [${entry.newRefs.join(', ')}]`;
1364
+ break;
1365
+ case 'failed':
1366
+ out += `\n [${entry.opIndex}] failed: ${entry.ref}`;
1367
+ if (entry.error) out += ` \u2014 ${entry.error}`;
1368
+ break;
1369
+ case 'skipped':
1370
+ out += `\n [${entry.opIndex}] skipped: ${entry.ref}`;
1371
+ break;
1123
1372
  }
1373
+ if (entry.refetchRequired) out += ' (refetch)';
1124
1374
  }
1375
+
1376
+ if (batchInfo.refetchRequired) {
1377
+ out += '\n\u26a0 refetchRequired: \u5b50\u30d6\u30ed\u30c3\u30af\u5909\u66f4\u3042\u308a\u3002get_article_structure \u3067\u6700\u65b0\u3092\u53d6\u5f97\u3057\u3066\u304f\u3060\u3055\u3044\u3002';
1378
+ }
1379
+
1125
1380
  return out;
1126
1381
  }
1127
1382
 
1383
+ // ============================================================
1384
+ // Phase 7: batch 差分化 helper
1385
+ // ============================================================
1386
+
1387
+ /**
1388
+ * allBlocks の parentIndex チェーンを辿り、resolvedOps 間に祖先/子孫関係があるか検出。
1389
+ * @param {Array<{resolvedIndex: number, ref: string}>} resolvedOps
1390
+ * @param {Array<{index: number, parentIndex: number|null}>} allBlocks
1391
+ * @returns {{childRef: string, childIndex: number, ancestorRef: string, ancestorIndex: number}|null}
1392
+ */
1393
+ function checkAncestorOverlap(resolvedOps, allBlocks) {
1394
+ const targetMap = new Map(resolvedOps.map(o => [o.resolvedIndex, o.ref]));
1395
+ const parentMap = new Map(allBlocks.map(b => [b.index, b.parentIndex ?? null]));
1396
+
1397
+ for (const op of resolvedOps) {
1398
+ let current = parentMap.get(op.resolvedIndex);
1399
+ while (current != null) {
1400
+ if (targetMap.has(current)) {
1401
+ return {
1402
+ childRef: op.ref,
1403
+ childIndex: op.resolvedIndex,
1404
+ ancestorRef: targetMap.get(current),
1405
+ ancestorIndex: current,
1406
+ };
1407
+ }
1408
+ current = parentMap.get(current) ?? null;
1409
+ }
1410
+ }
1411
+ return null;
1412
+ }
1413
+
1414
+ /**
1415
+ * allBlocks 内の rootIndex を含む subtree の flat size を返す。
1416
+ * @param {Array<{index: number, parentIndex: number|null}>} allBlocks
1417
+ * @param {number} rootIndex
1418
+ * @returns {number}
1419
+ */
1420
+ function countFlatSubtreeSize(allBlocks, rootIndex) {
1421
+ const childrenOf = new Map();
1422
+ for (const b of allBlocks) {
1423
+ if (b.parentIndex != null) {
1424
+ if (!childrenOf.has(b.parentIndex)) childrenOf.set(b.parentIndex, []);
1425
+ childrenOf.get(b.parentIndex).push(b.index);
1426
+ }
1427
+ }
1428
+ let count = 1;
1429
+ const stack = childrenOf.get(rootIndex) ? [...childrenOf.get(rootIndex)] : [];
1430
+ while (stack.length > 0) {
1431
+ const idx = stack.pop();
1432
+ count++;
1433
+ const children = childrenOf.get(idx);
1434
+ if (children) stack.push(...children);
1435
+ }
1436
+ return count;
1437
+ }
1438
+
1439
+ /**
1440
+ * opResult.blocks から新 subtree の flat size を計算する。
1441
+ * rawNewIndices のうち集合内に親を持たないものを root とみなし、各 root の subtree size を合算。
1442
+ * editor(top-level のみ)/ headless(flat 全体)両対応。
1443
+ * @param {Array<{index: number, parentIndex?: number|null}>} opResultBlocks
1444
+ * @param {number[]} rawNewIndices
1445
+ * @returns {number}
1446
+ */
1447
+ function computeNewFlatSize(opResultBlocks, rawNewIndices) {
1448
+ if (!rawNewIndices || rawNewIndices.length === 0) return 0;
1449
+ const indexSet = new Set(rawNewIndices);
1450
+ const newRoots = rawNewIndices.filter(idx => {
1451
+ const b = opResultBlocks.find(bl => bl.index === idx);
1452
+ return !b || b.parentIndex == null || !indexSet.has(b.parentIndex);
1453
+ });
1454
+ let total = 0;
1455
+ const blocksForSize = opResultBlocks.map(b => ({ index: b.index, parentIndex: b.parentIndex ?? null }));
1456
+ for (const root of newRoots) {
1457
+ total += countFlatSubtreeSize(blocksForSize, root);
1458
+ }
1459
+ return total;
1460
+ }
1461
+
1462
+ /**
1463
+ * [pre-state 戦略] 操作前の状態で対象ブロックにネスト子があるか判定。
1464
+ * delete, move, duplicate 用。操作後は対象が消える/移動するため pre-state で見る。
1465
+ * 下流 hasChildren フラグがあればそちらを優先(duplicate, move が返す)。
1466
+ * @param {object|null} preState - 操作前の state
1467
+ * @param {number[]} targetIndices - 操作対象 index 配列
1468
+ * @param {object} [downstream] - 下流 result(hasChildren があれば優先)
1469
+ * @returns {boolean}
1470
+ */
1471
+ function checkRefetchFromPreState(preState, targetIndices, downstream) {
1472
+ if (downstream?.hasChildren === true) return true;
1473
+ if (downstream?.hasChildren === false) return false;
1474
+
1475
+ if (!preState?.allBlocks) return true;
1476
+
1477
+ for (const idx of targetIndices) {
1478
+ const hasChild = preState.allBlocks.some(b => b.parentIndex === idx);
1479
+ if (hasChild) return true;
1480
+ }
1481
+ return false;
1482
+ }
1483
+
1484
+ /**
1485
+ * [post-result 戦略] 操作結果にネストブロックが含ま���るか判定。
1486
+ * insert, update 用��非ネストだったブロックを nested HTML に更新したケースを検知。
1487
+ * 新 snapshot の blocks から、対象 index を parentIndex に持つブロックがあるかで判定。
1488
+ * @param {object} snapshot - buildResponseSnapshot() の結果(新 snapshot)
1489
+ * @param {number[]} resultIndices - 操作結果の index 配列(挿入/更新された位置)
1490
+ * @returns {boolean}
1491
+ */
1492
+ function checkRefetchFromResult(snapshot, resultIndices) {
1493
+ if (!snapshot?.blocks || resultIndices.length === 0) return false;
1494
+
1495
+ for (const idx of resultIndices) {
1496
+ const hasNested = snapshot.blocks.some(b => b.parentIndex === idx);
1497
+ if (hasNested) return true;
1498
+ }
1499
+ return false;
1500
+ }
1501
+
1502
+ /**
1503
+ * 下流 result + 新 snapshot → ChangeInfo を構築する。
1504
+ * 構築不能な場合は null を返す(呼び出し側は Legacy フォールバック必須)。
1505
+ * @param {'deleted'|'inserted'|'moved'|'updated'|'expanded'|'duplicated'} type
1506
+ * @param {object} snapshot - buildResponseSnapshot() の結果
1507
+ * @param {object} result - 下流レスポンス
1508
+ * @param {object|null} preState - _preState
1509
+ * @param {object} [opts] - 追加オプション
1510
+ * @returns {ChangeInfo|null} null = Legacy フォールバック
1511
+ */
1512
+ function buildChangeInfoFromResult(type, snapshot, result, preState, opts) {
1513
+ if (!snapshot?.snapshotId || !result) return null;
1514
+
1515
+ const base = { snapshotId: snapshot.snapshotId, revision: snapshot.revision };
1516
+
1517
+ switch (type) {
1518
+ case 'deleted': {
1519
+ const deleted = result.deleted;
1520
+ if (!deleted || deleted.length === 0) return null;
1521
+ const deletedRefs = opts?.inputRefs || deleted.map(d => generateBlockRef(d.index));
1522
+ const refetch = checkRefetchFromPreState(preState, deleted.map(d => d.index), result);
1523
+ return { ...base, type: 'deleted', deletedRefs, ...(refetch ? { refetchRequired: true } : {}) };
1524
+ }
1525
+ case 'inserted': {
1526
+ let indices = result.insertedIndices;
1527
+ if (!indices && result.changes) {
1528
+ indices = result.changes.flatMap(c => c.newIndices || []).filter(i => i != null);
1529
+ }
1530
+ if (!indices || indices.length === 0) return null;
1531
+ // refetchRequired は全 indices で判定(子孫含む)
1532
+ const refetch = checkRefetchFromResult(snapshot, indices);
1533
+ // top-level のみ: 他の inserted index を parentIndex に持つものを除外
1534
+ // (headless は subtree 全体の flat index を返すが、editor は top-level のみ。差分レスポンスは top-level に統一)
1535
+ const indexSet = new Set(indices);
1536
+ const topLevelIndices = indices.filter(idx => {
1537
+ const b = snapshot.blocks?.find(bl => bl.index === idx);
1538
+ return !b || b.parentIndex == null || !indexSet.has(b.parentIndex);
1539
+ });
1540
+ const inserted = topLevelIndices.map(idx => {
1541
+ const b = snapshot.blocks?.find(bl => bl.index === idx);
1542
+ return b
1543
+ ? { index: b.index, ref: b.ref, blockType: b.type, depth: b.depth }
1544
+ : { index: idx, ref: generateBlockRef(idx), blockType: 'unknown', depth: 0 };
1545
+ });
1546
+ if (inserted.length === 0) return null;
1547
+ return { ...base, type: 'inserted', inserted, ...(refetch ? { refetchRequired: true } : {}) };
1548
+ }
1549
+ case 'moved': {
1550
+ const mapping = result.movedMapping;
1551
+ if (!mapping || mapping.length === 0) return null;
1552
+ // inputRefs 長さ不一致 → Legacy フォールバック(count > 1 安全弁)
1553
+ if (opts?.inputRefs && opts.inputRefs.length !== mapping.length) return null;
1554
+ const moved = mapping.map((m, i) => ({
1555
+ oldRef: opts?.inputRefs ? opts.inputRefs[i] : generateBlockRef(m.oldIndex),
1556
+ newRef: generateBlockRef(m.newIndex),
1557
+ }));
1558
+ const refetch = checkRefetchFromPreState(preState, mapping.map(m => m.oldIndex), result);
1559
+ return { ...base, type: 'moved', moved, ...(refetch ? { refetchRequired: true } : {}) };
1560
+ }
1561
+ case 'updated': {
1562
+ const changes = result.changes;
1563
+ if (!changes || changes.length === 0) return null;
1564
+ const updatedRefs = changes.map(c => generateBlockRef(c.newIndices?.[0] ?? c.oldIndex));
1565
+ const resultIndices = changes.flatMap(c => c.newIndices || []).filter(i => i != null);
1566
+ const refetch = checkRefetchFromResult(snapshot, resultIndices);
1567
+ return { ...base, type: 'updated', updatedRefs, ...(refetch ? { refetchRequired: true } : {}) };
1568
+ }
1569
+ case 'expanded': {
1570
+ const changes = result.changes || [];
1571
+ if (changes.length !== 1) return null;
1572
+ const change = changes[0];
1573
+ if (!change || !change.newIndices || change.newIndices.length <= 1) return null;
1574
+ const oldRef = opts?.inputRef || generateBlockRef(change.oldIndex);
1575
+ const newRefs = change.newIndices.map(idx => generateBlockRef(idx));
1576
+ const refetch = checkRefetchFromResult(snapshot, change.newIndices);
1577
+ return { ...base, type: 'expanded', expanded: { oldRef, newRefs }, ...(refetch ? { refetchRequired: true } : {}) };
1578
+ }
1579
+ case 'duplicated': {
1580
+ const srcIdx = result.sourceIndex ?? result.duplicated?.index;
1581
+ const newIdx = result.newIndex ?? result.duplicated?.newIndex;
1582
+ if (srcIdx == null || newIdx == null) return null;
1583
+ const refetch = checkRefetchFromPreState(preState, [srcIdx], result);
1584
+ return {
1585
+ ...base, type: 'duplicated',
1586
+ sourceRef: opts?.inputRef || generateBlockRef(srcIdx),
1587
+ newRef: generateBlockRef(newIdx),
1588
+ sourceIndex: srcIdx, newIndex: newIdx,
1589
+ ...(refetch ? { refetchRequired: true } : {}),
1590
+ };
1591
+ }
1592
+ default:
1593
+ return null;
1594
+ }
1595
+ }
1596
+
1597
+ /**
1598
+ * 実行完了した resolvedOps + snapshot から BatchDiffInfo を構築する。
1599
+ * @param {Array} resolvedOps - rebasing 済みの op 配列
1600
+ * @param {object|null} snapshot - 最終 snapshot
1601
+ * @returns {BatchDiffInfo|null}
1602
+ */
1603
+ function buildBatchDiffInfo(resolvedOps, snapshot) {
1604
+ if (!snapshot?.snapshotId) return null;
1605
+
1606
+ let refetchRequired = false;
1607
+ const entries = [];
1608
+
1609
+ // opIndex 順(入力順)でソート
1610
+ const opsByInput = [...resolvedOps].sort((a, b) => a.opIndex - b.opIndex);
1611
+
1612
+ for (const op of opsByInput) {
1613
+ if (op.status === 'failed') {
1614
+ entries.push({ opIndex: op.opIndex, type: 'failed', ref: op.ref, error: op.error || null });
1615
+ continue;
1616
+ }
1617
+ if (op.status === 'skipped') {
1618
+ entries.push({ opIndex: op.opIndex, type: 'skipped', ref: op.ref });
1619
+ continue;
1620
+ }
1621
+
1622
+ // rebased な新 ref を snapshot から取得
1623
+ const newRefs = (op.rebasedNewIndices || []).map(idx => {
1624
+ const b = snapshot.blocks?.find(bl => bl.index === idx);
1625
+ return b ? b.ref : generateBlockRef(idx);
1626
+ });
1627
+
1628
+ // refetch 判定
1629
+ const opRefetch = checkRefetchFromResult(snapshot, op.rebasedNewIndices || []);
1630
+ if (opRefetch) refetchRequired = true;
1631
+
1632
+ if (op.status === 'expanded') {
1633
+ entries.push({
1634
+ opIndex: op.opIndex, type: 'expanded',
1635
+ oldRef: op.ref, newRefs,
1636
+ ...(opRefetch ? { refetchRequired: true } : {}),
1637
+ });
1638
+ } else {
1639
+ // updated
1640
+ entries.push({
1641
+ opIndex: op.opIndex, type: 'updated',
1642
+ ref: newRefs[0] || op.ref,
1643
+ ...(opRefetch ? { refetchRequired: true } : {}),
1644
+ });
1645
+ }
1646
+ }
1647
+
1648
+ return {
1649
+ snapshotId: snapshot.snapshotId,
1650
+ revision: snapshot.revision,
1651
+ type: 'batch',
1652
+ entries,
1653
+ ...(refetchRequired ? { refetchRequired: true } : {}),
1654
+ };
1655
+ }
1656
+
1128
1657
  /**
1129
1658
  * batch operations の実行
1130
1659
  * @param {Array} operations - 各 operation: { target, newHTML?, replacements?, attributeUpdates? }
@@ -1221,17 +1750,30 @@ async function handleBatchOperations(operations, snapshotId, mode, client, postI
1221
1750
  indexSet.add(op.resolvedIndex);
1222
1751
  }
1223
1752
 
1224
- // 2. 降順ソート(index が大きい方から処理 1 op = 1 index なので安全)
1753
+ // ancestor/descendant overlap チェック(親子関係の target batch 不可)
1754
+ const ancestorOverlap = checkAncestorOverlap(resolvedOps, currentState.allBlocks);
1755
+ if (ancestorOverlap) {
1756
+ return {
1757
+ content: [{ type: "text", text: `❌ operations に親子関係の target が含まれるため batch 不可です。ref ${ancestorOverlap.childRef} (index ${ancestorOverlap.childIndex}) は ref ${ancestorOverlap.ancestorRef} (index ${ancestorOverlap.ancestorIndex}) の子孫です。` }],
1758
+ isError: true,
1759
+ };
1760
+ }
1761
+
1762
+ // 2. oldFlatSize を計算(rebasing 用、降順ソート前に実行)
1763
+ for (const op of resolvedOps) {
1764
+ op.oldFlatSize = countFlatSubtreeSize(currentState.allBlocks, op.resolvedIndex);
1765
+ }
1766
+
1767
+ // 3. 降順ソート(index が大きい方から処理 — 1 op = 1 index なので安全)
1225
1768
  resolvedOps.sort((a, b) => b.resolvedIndex - a.resolvedIndex);
1226
1769
 
1227
- // 3. 各 operation を順次実行
1228
- const results = [];
1770
+ // 4. 各 operation を順次実行(結果情報を保持)
1771
+ let lastSuccessfulResult = null;
1229
1772
  for (const op of resolvedOps) {
1230
1773
  const index = op.resolvedIndex;
1231
1774
 
1232
1775
  let opResult;
1233
1776
  try {
1234
- // batch は ref → index 解決済み。1 op = 1 index で下流に渡す
1235
1777
  const downstreamParams = {
1236
1778
  index,
1237
1779
  replacements: op.replacements,
@@ -1246,39 +1788,95 @@ async function handleBatchOperations(operations, snapshotId, mode, client, postI
1246
1788
  } else {
1247
1789
  opResult = await client.sendEditorCommand("update_blocks", downstreamParams, 10000, postId, sessionId);
1248
1790
  }
1249
-
1250
- results.push({
1251
- ref: op.ref,
1252
- resolvedIndex: op.resolvedIndex,
1253
- status: (opResult && opResult.success !== false && opResult.ok !== false) ? 'updated' : 'failed',
1254
- error: opResult?.error || null,
1255
- });
1256
1791
  } catch (e) {
1257
- results.push({
1258
- ref: op.ref,
1259
- resolvedIndex: op.resolvedIndex,
1260
- status: 'failed',
1261
- error: e.message,
1262
- });
1792
+ op.status = 'failed';
1793
+ op.error = e.message;
1794
+ op.rawNewIndices = [];
1795
+ op.newFlatSize = op.oldFlatSize;
1796
+ op.opResult = null;
1797
+ continue;
1263
1798
  }
1799
+
1800
+ // op 成功判定
1801
+ const topLevelFail = !opResult || opResult.success === false || opResult.ok === false;
1802
+ if (topLevelFail) {
1803
+ op.status = 'failed';
1804
+ op.error = opResult?.error || 'unknown error';
1805
+ op.rawNewIndices = [];
1806
+ op.newFlatSize = op.oldFlatSize;
1807
+ op.opResult = opResult;
1808
+ continue;
1809
+ }
1810
+
1811
+ // skipped 判定(attributeUpdates の filter mismatch)
1812
+ const successfulChanges = (opResult.changes || []).filter(c => c.success !== false);
1813
+ const hasSkippedResult = opResult.results?.some(r => r.skipped === true);
1814
+ if (successfulChanges.length === 0 && !topLevelFail && !opResult.error) {
1815
+ if (hasSkippedResult || op.attributeUpdates != null) {
1816
+ op.status = 'skipped';
1817
+ op.rawNewIndices = [];
1818
+ op.newFlatSize = op.oldFlatSize;
1819
+ op.opResult = opResult;
1820
+ continue;
1821
+ }
1822
+ // changes 空で skipped でもない → failed
1823
+ op.status = 'failed';
1824
+ op.error = 'no successful changes';
1825
+ op.rawNewIndices = [];
1826
+ op.newFlatSize = op.oldFlatSize;
1827
+ op.opResult = opResult;
1828
+ continue;
1829
+ }
1830
+
1831
+ // 成功: rawNewIndices を取得
1832
+ op.rawNewIndices = successfulChanges.flatMap(c => c.newIndices || []);
1833
+ op.newFlatSize = (opResult.blocks && opResult.blocks.length > 0)
1834
+ ? computeNewFlatSize(opResult.blocks, op.rawNewIndices)
1835
+ : op.rawNewIndices.length || op.oldFlatSize;
1836
+ op.status = (op.rawNewIndices.length > 1) ? 'expanded' : 'updated';
1837
+ op.opResult = opResult;
1838
+ lastSuccessfulResult = opResult;
1264
1839
  }
1265
1840
 
1266
- // 4. レスポンス snapshot 構築
1267
- const snapshot = await buildResponseSnapshot(mode, client, postId, sessionId, siteName);
1841
+ // 5. rebasing(resolvedIndex 昇順で走査、低 index の展開が高 index をずらす)
1842
+ const opsForRebase = [...resolvedOps].sort((a, b) => a.resolvedIndex - b.resolvedIndex);
1843
+ let cumulativeDelta = 0;
1844
+ for (const op of opsForRebase) {
1845
+ if (op.status === 'failed' || op.status === 'skipped') {
1846
+ op.rebasedNewIndices = [];
1847
+ continue;
1848
+ }
1849
+ op.rebasedNewIndices = op.rawNewIndices.map(idx => idx + cumulativeDelta);
1850
+ cumulativeDelta += (op.newFlatSize - op.oldFlatSize);
1851
+ }
1268
1852
 
1269
- // レスポンス組み立て
1270
- // 注: 1→N 展開の newRefs[] は snapshot.blocks[] 全体を返しているため、
1271
- // AI は新 snapshot から直接新しい ref を特定できる。
1272
- // per-operation newRefs 追跡は複数変更時に不正確になるため省略。
1273
- const successCount = results.filter(r => r.status !== 'failed').length;
1274
- const failCount = results.filter(r => r.status === 'failed').length;
1275
- let text = `✅ batch 完了\n\n成功: ${successCount} / 失敗: ${failCount}`;
1276
- for (const r of results) {
1277
- const refLabel = r.ref ? ` (${r.ref})` : '';
1278
- text += `\n [${r.resolvedIndex}]${refLabel}: ${r.status}`;
1279
- if (r.error) text += ` — ${r.error}`;
1853
+ // 6. 最終 snapshot 構築
1854
+ let snapshot;
1855
+ if (lastSuccessfulResult) {
1856
+ snapshot = buildResponseSnapshotFromResult(lastSuccessfulResult, mode, postId, sessionId, siteName);
1857
+ }
1858
+ if (!snapshot) {
1859
+ // op 失敗/skipped or result.blocks なし currentState から構築
1860
+ snapshot = buildSnapshotFromState(currentState, mode, postId, sessionId, siteName);
1861
+ }
1862
+ if (!snapshot) {
1863
+ // 最終手段: 再取得
1864
+ snapshot = await buildResponseSnapshot(mode, client, postId, sessionId, siteName);
1865
+ }
1866
+
1867
+ // 7. BatchDiffInfo 構築
1868
+ const batchDiff = buildBatchDiffInfo(resolvedOps, snapshot);
1869
+
1870
+ // 8. レスポンス組み立て
1871
+ const successCount = resolvedOps.filter(r => r.status !== 'failed' && r.status !== 'skipped').length;
1872
+ const totalCount = resolvedOps.length;
1873
+ let text = `\u2705 batch \u5b8c\u4e86 (${successCount}/${totalCount} \u6210\u529f)`;
1874
+
1875
+ if (batchDiff) {
1876
+ text = appendBatchRefChangesToText(text + _modeTag, batchDiff);
1877
+ } else {
1878
+ text = appendSnapshotToTextLegacy(text + _modeTag, snapshot);
1280
1879
  }
1281
- text = appendSnapshotToText(text + _modeTag, snapshot);
1282
1880
 
1283
1881
  return { content: [{ type: "text", text }] };
1284
1882
  }
@@ -1901,6 +2499,7 @@ async function handleUpdateBlocksTool(args, toolName) {
1901
2499
  mode, client, postId, sessionId: _sessionId, siteName: _siteName,
1902
2500
  });
1903
2501
  if (resolved.error) return resolved.error;
2502
+ const _preState = resolved.currentState || null; // Phase 2: 差分計算用
1904
2503
  if (resolved.index !== undefined) {
1905
2504
  tp.index = resolved.index;
1906
2505
  delete tp._ref;
@@ -1924,6 +2523,12 @@ async function handleUpdateBlocksTool(args, toolName) {
1924
2523
  _fromInsertBlock, appendToEnd } = (args || {});
1925
2524
  let { newHTML } = (args || {});
1926
2525
 
2526
+ // Phase 5: 差分化オプション
2527
+ // - 単体 target.ref かつ snapshotId ありのみ差分化
2528
+ // - appendToEnd は Legacy 固定(早期リターンで自然に除外)
2529
+ const _inputRef = (snapshotId && args?.target?.ref) ? args.target.ref : null;
2530
+ const _isRefInsert = !!(insertOnly && _inputRef);
2531
+
1927
2532
  // [INSERT_OBSERVE] Step 5: insert_block 経由時の送信パラメータログ
1928
2533
  if (process.env.FRIDAY_DEBUG === '1' && _fromInsertBlock) {
1929
2534
  console.log(`[INSERT_OBSERVE] mcp-send: index=${index}, postId=${postId}, sessionId=${_sessionId || 'none'}, mode=${mode}, appendToEnd=${appendToEnd}, htmlLen=${(newHTML || '').length}`);
@@ -2011,8 +2616,8 @@ async function handleUpdateBlocksTool(args, toolName) {
2011
2616
  _fromInsertBlock: _fromInsertBlock || false,
2012
2617
  });
2013
2618
  const count = result.results?.[0]?.count || 1;
2014
- const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
2015
- return { content: [{ type: "text", text: appendSnapshotToText(`✅ ${count}ブロック挿入 at end${deprecationWarning}${_modeTag}`, _snap, _refInfo) }] };
2619
+ const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
2620
+ return { content: [{ type: "text", text: appendSnapshotToTextLegacy(`✅ ${count}ブロック挿入 at end${deprecationWarning}${_modeTag}`, _snap, _refInfo) }] };
2016
2621
  }
2017
2622
 
2018
2623
  const result = await client.headlessUpdate(postId, {
@@ -2037,14 +2642,14 @@ async function handleUpdateBlocksTool(args, toolName) {
2037
2642
  return { content: [{ type: "text", text: `🔍 プレビュー\n\n対象: ${result.results.length} ブロック\n\n${previewText}${deprecationWarning}` }] };
2038
2643
  }
2039
2644
 
2040
- const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
2645
+ const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
2041
2646
  if (result.results) {
2042
2647
  const successCount = result.results.filter(r => r.success).length;
2043
2648
  const failCount = result.results.filter(r => !r.success).length;
2044
- return { content: [{ type: "text", text: appendSnapshotToText(`✅ 完了\n\n成功: ${successCount} / 失敗: ${failCount}${deprecationWarning}`, _snap, _refInfo) }] };
2649
+ return { content: [{ type: "text", text: buildUpdateDiffResponse(`✅ 完了\n\n成功: ${successCount} / 失敗: ${failCount}${deprecationWarning}`, _snap, result, _preState, _inputRef, _isRefInsert, _refInfo) }] };
2045
2650
  }
2046
2651
 
2047
- return { content: [{ type: "text", text: appendSnapshotToText(`✅ 更新完了${deprecationWarning}${_modeTag}`, _snap, _refInfo) }] };
2652
+ return { content: [{ type: "text", text: buildUpdateDiffResponse(`✅ 更新完了${deprecationWarning}${_modeTag}`, _snap, result, _preState, _inputRef, _isRefInsert, _refInfo) }] };
2048
2653
  } catch (e) {
2049
2654
  const formatted = formatHeadlessConflictError(e);
2050
2655
  if (formatted) return formatted;
@@ -2066,8 +2671,8 @@ async function handleUpdateBlocksTool(args, toolName) {
2066
2671
  if (!result || result.timeout) return timeoutResponse(name, client, args?.site);
2067
2672
  if (!result.success) return errorResponse(name, result.error, args?.site);
2068
2673
  const count = result.inserted || 1;
2069
- const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
2070
- return { content: [{ type: "text", text: appendSnapshotToText(`✅ ${count}ブロック挿入 at end${deprecationWarning}${_modeTag}`, _snap, _refInfo) }] };
2674
+ const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
2675
+ return { content: [{ type: "text", text: appendSnapshotToTextLegacy(`✅ ${count}ブロック挿入 at end${deprecationWarning}${_modeTag}`, _snap, _refInfo) }] };
2071
2676
  }
2072
2677
 
2073
2678
  // target: "selected" の場合 → エディタ状態から選択情報を取得してMCP側で処理
@@ -2121,8 +2726,8 @@ async function handleUpdateBlocksTool(args, toolName) {
2121
2726
  if (!result.success)
2122
2727
  return errorResponse(name, result.error, args?.site);
2123
2728
  {
2124
- const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
2125
- return { content: [{ type: "text", text: appendSnapshotToText(`✅ ${result.mode === "insert" ? "挿入" : "更新"}完了${hasTextSelection ? "(カーソル選択モード)" : "(差分)"}${deprecationWarning}`, _snap, _refInfo) }] };
2729
+ const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
2730
+ return { content: [{ type: "text", text: appendSnapshotToTextLegacy(`✅ ${result.mode === "insert" ? "挿入" : "更新"}完了${hasTextSelection ? "(カーソル選択モード)" : "(差分)"}${deprecationWarning}`, _snap, _refInfo) }] };
2126
2731
  }
2127
2732
  }
2128
2733
 
@@ -2140,8 +2745,8 @@ async function handleUpdateBlocksTool(args, toolName) {
2140
2745
  if (!result.success)
2141
2746
  return errorResponse(name, result.error, args?.site);
2142
2747
  {
2143
- const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
2144
- return { content: [{ type: "text", text: appendSnapshotToText(`✅ ${result.mode === "insert" ? "挿入" : "更新"}完了${deprecationWarning}`, _snap, _refInfo) }] };
2748
+ const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
2749
+ return { content: [{ type: "text", text: appendSnapshotToTextLegacy(`✅ ${result.mode === "insert" ? "挿入" : "更新"}完了${deprecationWarning}`, _snap, _refInfo) }] };
2145
2750
  }
2146
2751
  }
2147
2752
 
@@ -2157,8 +2762,8 @@ async function handleUpdateBlocksTool(args, toolName) {
2157
2762
  if (!result.success)
2158
2763
  return errorResponse(name, result.error, args?.site);
2159
2764
  {
2160
- const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
2161
- return { content: [{ type: "text", text: appendSnapshotToText(`✅ 属性更新完了${deprecationWarning}`, _snap, _refInfo) }] };
2765
+ const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
2766
+ return { content: [{ type: "text", text: appendSnapshotToTextLegacy(`✅ 属性更新完了${deprecationWarning}`, _snap, _refInfo) }] };
2162
2767
  }
2163
2768
  }
2164
2769
 
@@ -2198,16 +2803,16 @@ async function handleUpdateBlocksTool(args, toolName) {
2198
2803
  return { content: [{ type: "text", text: `🔍 プレビュー\n\n対象: ${result.results.length} ブロック\n\n${previewText || "(対象なし)"}${deprecationWarning}` }] };
2199
2804
  }
2200
2805
 
2201
- const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
2806
+ const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
2202
2807
  if (result.results) {
2203
2808
  const successCount = result.results.filter(r => r.success && !r.skipped).length;
2204
2809
  const skipCount = result.results.filter(r => r.skipped).length;
2205
2810
  const failCount = result.results.filter(r => !r.success).length;
2206
2811
  const detailText = result.results.filter(r => r.success && r.replaceCount > 0).map(r => ` [${r.index}]: ${r.replaceCount}箇所`).join('\n');
2207
- return { content: [{ type: "text", text: appendSnapshotToText(`✅ 完了\n\n成功: ${successCount} / スキップ: ${skipCount} / 失敗: ${failCount}\n\n${detailText}${deprecationWarning}${_modeTag}`, _snap, _refInfo) }] };
2812
+ return { content: [{ type: "text", text: buildUpdateDiffResponse(`✅ 完了\n\n成功: ${successCount} / スキップ: ${skipCount} / 失敗: ${failCount}\n\n${detailText}${deprecationWarning}${_modeTag}`, _snap, result, _preState, _inputRef, _isRefInsert, _refInfo) }] };
2208
2813
  }
2209
2814
 
2210
- return { content: [{ type: "text", text: appendSnapshotToText(`✅ ${result.mode === "insert" ? "挿入" : "更新"}完了${deprecationWarning}${_modeTag}`, _snap, _refInfo) }] };
2815
+ return { content: [{ type: "text", text: buildUpdateDiffResponse(`✅ ${result.mode === "insert" ? "挿入" : "更新"}完了${deprecationWarning}${_modeTag}`, _snap, result, _preState, _inputRef, _isRefInsert, _refInfo) }] };
2211
2816
  }
2212
2817
 
2213
2818
  // ツール実行のハンドラ
@@ -2763,11 +3368,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2763
3368
  mode, client, postId, sessionId: _sessionId, siteName: _siteName,
2764
3369
  });
2765
3370
  if (resolved.error) return resolved.error;
3371
+ const _preState = resolved.currentState || null; // Phase 2: 差分計算用
2766
3372
 
2767
3373
  let { index, count, indices } = args;
2768
3374
  if (resolved.index !== undefined) { index = resolved.index; count = count || 1; }
2769
3375
  if (resolved.indices !== undefined) { indices = resolved.indices; }
2770
3376
 
3377
+ // Phase 3: 入力 ref を保持(ref + count > 1 は差分化できないので Legacy)
3378
+ const _inputRefs = args?.refs ? [...new Set(args.refs)]
3379
+ : (args?.ref && (count || 1) <= 1) ? [args.ref]
3380
+ : null;
3381
+
2771
3382
  // indices と index/count の排他バリデーション
2772
3383
  if (indices !== undefined && (index !== undefined || count !== undefined)) {
2773
3384
  return { content: [{ type: "text", text: "❌ indices と index/count は同時に指定できません。" }], isError: true };
@@ -2788,8 +3399,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2788
3399
  try {
2789
3400
  const result = await client.headlessDeleteMultiple(postId, uniqueIndices);
2790
3401
  const details = result.deleted.map(d => ` [${d.index}] ${d.type}`).join('\n');
2791
- const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
2792
- return { content: [{ type: "text", text: appendSnapshotToText(`✅ ${result.count}ブロック削除\n\n${details}${_mt_db}`, _snap) }] };
3402
+ const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
3403
+ const _msg = `✅ ${result.count}ブロック削除\n\n${details}${_mt_db}`;
3404
+ if (_inputRefs) {
3405
+ const changeInfo = buildChangeInfoFromResult('deleted', _snap, result, _preState, { inputRefs: _inputRefs });
3406
+ if (changeInfo) return { content: [{ type: "text", text: appendRefChangesToText(_msg, changeInfo) }] };
3407
+ }
3408
+ return { content: [{ type: "text", text: appendSnapshotToTextLegacy(_msg, _snap) }] };
2793
3409
  } catch (e) {
2794
3410
  const formatted = formatHeadlessConflictError(e);
2795
3411
  if (formatted) return formatted;
@@ -2803,8 +3419,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2803
3419
  if (!result.success)
2804
3420
  return errorResponse(name, result.error, args?.site);
2805
3421
  const details = result.deleted.map(d => ` [${d.index}] ${d.type}`).join('\n');
2806
- const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
2807
- return { content: [{ type: "text", text: appendSnapshotToText(`✅ ${result.count}ブロック削除\n\n${details}${_mt_db}`, _snap) }] };
3422
+ const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
3423
+ const _msg = `✅ ${result.count}ブロック削除\n\n${details}${_mt_db}`;
3424
+ if (_inputRefs) {
3425
+ const changeInfo = buildChangeInfoFromResult('deleted', _snap, result, _preState, { inputRefs: _inputRefs });
3426
+ if (changeInfo) return { content: [{ type: "text", text: appendRefChangesToText(_msg, changeInfo) }] };
3427
+ }
3428
+ return { content: [{ type: "text", text: appendSnapshotToTextLegacy(_msg, _snap) }] };
2808
3429
  }
2809
3430
 
2810
3431
  // 既存モード(index + count)
@@ -2815,8 +3436,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2815
3436
  try {
2816
3437
  const result = await client.headlessDelete(postId, index, count || 1);
2817
3438
  const details = result.deleted.map(d => ` [${d.index}] ${d.type}`).join('\n');
2818
- const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
2819
- return { content: [{ type: "text", text: appendSnapshotToText(`✅ ${result.count}ブロック削除\n\n${details}${_mt_db}`, _snap) }] };
3439
+ const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
3440
+ const _msg = `✅ ${result.count}ブロック削除\n\n${details}${_mt_db}`;
3441
+ if (_inputRefs) {
3442
+ const changeInfo = buildChangeInfoFromResult('deleted', _snap, result, _preState, { inputRefs: _inputRefs });
3443
+ if (changeInfo) return { content: [{ type: "text", text: appendRefChangesToText(_msg, changeInfo) }] };
3444
+ }
3445
+ return { content: [{ type: "text", text: appendSnapshotToTextLegacy(_msg, _snap) }] };
2820
3446
  } catch (e) {
2821
3447
  const formatted = formatHeadlessConflictError(e);
2822
3448
  if (formatted) return formatted;
@@ -2830,8 +3456,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2830
3456
  if (!result.success)
2831
3457
  return errorResponse(name, result.error, args?.site);
2832
3458
  const details = result.deleted.map(d => ` [${d.index}] ${d.type}`).join('\n');
2833
- const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
2834
- return { content: [{ type: "text", text: appendSnapshotToText(`✅ ${result.count}ブロック削除\n\n${details}${_mt_db}`, _snap) }] };
3459
+ const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
3460
+ const _msg = `✅ ${result.count}ブロック削除\n\n${details}${_mt_db}`;
3461
+ if (_inputRefs) {
3462
+ const changeInfo = buildChangeInfoFromResult('deleted', _snap, result, _preState, { inputRefs: _inputRefs });
3463
+ if (changeInfo) return { content: [{ type: "text", text: appendRefChangesToText(_msg, changeInfo) }] };
3464
+ }
3465
+ return { content: [{ type: "text", text: appendSnapshotToTextLegacy(_msg, _snap) }] };
2835
3466
  }
2836
3467
 
2837
3468
  case "move_block": {
@@ -2875,12 +3506,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2875
3506
 
2876
3507
  // expectedRevision チェック(全モード共通)
2877
3508
  // ref モード以外でも expectedRevision が指定されていれば revision チェックを実施
3509
+ let _preState = null; // Phase 2: 差分計算用
2878
3510
  if (args?.expectedRevision && !hasRef) {
2879
- const { error: _revError } = await acquireFreshState({
3511
+ const { currentState, error: _revError } = await acquireFreshState({
2880
3512
  expectedRevision: args.expectedRevision,
2881
3513
  mode, client, postId, sessionId: _sessionId, siteName: _siteName,
2882
3514
  });
2883
3515
  if (_revError) return _revError;
3516
+ _preState = currentState || null;
2884
3517
  }
2885
3518
 
2886
3519
  // ref モード: fromRef + beforeRef/afterRef
@@ -2904,6 +3537,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2904
3537
  mode, client, postId, sessionId: _sessionId, siteName: _siteName,
2905
3538
  });
2906
3539
  if (_stateError) return _stateError;
3540
+ _preState = currentState || null; // Phase 2: 差分計算用
2907
3541
 
2908
3542
  // ref 解決(同じ currentState で 2 つの ref を解決)
2909
3543
  try {
@@ -2919,6 +3553,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2919
3553
  // ref → flat に変換完了。以降 flat モードに合流
2920
3554
  }
2921
3555
 
3556
+ // Phase 4: 入力 ref 保持(ref モード + count <= 1 のみ差分化)
3557
+ const _inputRefs = (hasRef && count <= 1) ? [args.fromRef] : null;
3558
+
2922
3559
  // 移動結果のレスポンス生成ヘルパー
2923
3560
  const moveMsg = (moved) => {
2924
3561
  if (moved?.noop) return `✅ 移動不要(同じ位置)${_mt_mb}`;
@@ -2941,8 +3578,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2941
3578
  if (mode === 'headless') {
2942
3579
  try {
2943
3580
  const result = await client.headlessMoveFlat(postId, fromFlat, toFlat, count);
2944
- const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
2945
- return { content: [{ type: "text", text: appendSnapshotToText(moveMsg(result.moved), _snap) }] };
3581
+ const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
3582
+ const _msg = moveMsg(result.moved);
3583
+ if (_inputRefs) {
3584
+ const changeInfo = buildChangeInfoFromResult('moved', _snap, result, _preState, { inputRefs: _inputRefs });
3585
+ if (changeInfo) return { content: [{ type: "text", text: appendRefChangesToText(_msg, changeInfo) }] };
3586
+ }
3587
+ return { content: [{ type: "text", text: appendSnapshotToTextLegacy(_msg, _snap) }] };
2946
3588
  } catch (e) {
2947
3589
  const formatted = formatHeadlessConflictError(e);
2948
3590
  if (formatted) return formatted;
@@ -2955,8 +3597,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2955
3597
  return timeoutResponse(name, client, args?.site);
2956
3598
  if (!result.success)
2957
3599
  return errorResponse(name, result.error, args?.site);
2958
- const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
2959
- return { content: [{ type: "text", text: appendSnapshotToText(moveMsg(result.moved), _snap) }] };
3600
+ const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
3601
+ const _msg = moveMsg(result.moved);
3602
+ if (_inputRefs) {
3603
+ const changeInfo = buildChangeInfoFromResult('moved', _snap, result, _preState, { inputRefs: _inputRefs });
3604
+ if (changeInfo) return { content: [{ type: "text", text: appendRefChangesToText(_msg, changeInfo) }] };
3605
+ }
3606
+ return { content: [{ type: "text", text: appendSnapshotToTextLegacy(_msg, _snap) }] };
2960
3607
  }
2961
3608
 
2962
3609
  // 既存モード(from/to トップレベル)
@@ -2967,8 +3614,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2967
3614
  if (mode === 'headless') {
2968
3615
  try {
2969
3616
  const result = await client.headlessMove(postId, from, to, count);
2970
- const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
2971
- return { content: [{ type: "text", text: appendSnapshotToText(moveMsg(result.moved), _snap) }] };
3617
+ const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
3618
+ return { content: [{ type: "text", text: appendSnapshotToTextLegacy(moveMsg(result.moved), _snap) }] };
2972
3619
  } catch (e) {
2973
3620
  const formatted = formatHeadlessConflictError(e);
2974
3621
  if (formatted) return formatted;
@@ -2981,8 +3628,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2981
3628
  return timeoutResponse(name, client, args?.site);
2982
3629
  if (!result.success)
2983
3630
  return errorResponse(name, result.error, args?.site);
2984
- const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
2985
- return { content: [{ type: "text", text: appendSnapshotToText(moveMsg(result.moved), _snap) }] };
3631
+ const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
3632
+ return { content: [{ type: "text", text: appendSnapshotToTextLegacy(moveMsg(result.moved), _snap) }] };
2986
3633
  }
2987
3634
 
2988
3635
  case "undo": {
@@ -3041,9 +3688,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3041
3688
  mode, client, postId, sessionId: _sessionId, siteName: _siteName,
3042
3689
  });
3043
3690
  if (resolved.error) return resolved.error;
3691
+ const _preState = resolved.currentState || null; // Phase 2: 差分計算用
3044
3692
 
3045
3693
  const index = resolved.index !== undefined ? resolved.index : args?.index;
3046
3694
 
3695
+ // Phase 3: 入力 ref を保持(index 経路は差分化しない)
3696
+ const _inputRef = args?.ref || null;
3697
+
3047
3698
  if (mode === 'headless') {
3048
3699
  const _guard = await guardHeadlessConflict(postId, client, name);
3049
3700
  if (_guard) return _guard;
@@ -3052,8 +3703,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3052
3703
  }
3053
3704
  try {
3054
3705
  const result = await client.headlessDuplicate(postId, index);
3055
- const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
3056
- return { content: [{ type: "text", text: appendSnapshotToText(`✅ ブロック複製完了 (index ${result.duplicated.index} → ${result.duplicated.newIndex})${_mt_dup}`, _snap) }] };
3706
+ const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
3707
+ const _msg = `✅ ブロック複製完了 (index ${result.duplicated.index} → ${result.duplicated.newIndex})${_mt_dup}`;
3708
+ if (_inputRef) {
3709
+ const changeInfo = buildChangeInfoFromResult('duplicated', _snap, result, _preState, { inputRef: _inputRef });
3710
+ if (changeInfo) return { content: [{ type: "text", text: appendRefChangesToText(_msg, changeInfo) }] };
3711
+ }
3712
+ return { content: [{ type: "text", text: appendSnapshotToTextLegacy(_msg, _snap) }] };
3057
3713
  } catch (e) {
3058
3714
  const formatted = formatHeadlessConflictError(e);
3059
3715
  if (formatted) return formatted;
@@ -3066,8 +3722,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3066
3722
  return timeoutResponse(name, client, args?.site);
3067
3723
  if (!result.success)
3068
3724
  return errorResponse(name, result.error, args?.site);
3069
- const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
3070
- return { content: [{ type: "text", text: appendSnapshotToText(`✅ ブロック複製完了${_mt_dup}`, _snap) }] };
3725
+ const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
3726
+ const _msg = `✅ ブロック複製完了${_mt_dup}`;
3727
+ if (_inputRef) {
3728
+ const changeInfo = buildChangeInfoFromResult('duplicated', _snap, result, _preState, { inputRef: _inputRef });
3729
+ if (changeInfo) return { content: [{ type: "text", text: appendRefChangesToText(_msg, changeInfo) }] };
3730
+ }
3731
+ return { content: [{ type: "text", text: appendSnapshotToTextLegacy(_msg, _snap) }] };
3071
3732
  }
3072
3733
 
3073
3734
  case "save_post": {
@@ -3421,7 +4082,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3421
4082
  }
3422
4083
 
3423
4084
  case "get_selection": {
3424
- const { mode, postId: _postId, sessionId: _sessionId, message, client } = await resolveMode(args, name);
4085
+ const { mode, postId: _postId, sessionId: _sessionId, message, client, siteName } = await resolveMode(args, name);
3425
4086
  if (mode === 'error') {
3426
4087
  return errorResponse(name, message, args?.site);
3427
4088
  }
@@ -3437,16 +4098,48 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3437
4098
  content: [{ type: "text", text: `ブロックが選択されていません。${_mt_gs}` }],
3438
4099
  };
3439
4100
  }
4101
+
4102
+ // ref 対応: fresh state から snapshot を毎回新規作成
4103
+ // safetyCritical: true — stale Store へのフォールバックを防ぐ
4104
+ const _siteName_gs = siteName || args?.site || 'default';
4105
+ let _snapInfo = null;
4106
+ try {
4107
+ const freshState = await getCurrentStructure(mode, client, _postId, _sessionId, { safetyCritical: true });
4108
+ if (freshState?.allBlocks?.length > 0) {
4109
+ _snapInfo = buildSnapshotFromState(freshState, mode, _postId, _sessionId, _siteName_gs);
4110
+ }
4111
+ } catch (_e) {
4112
+ // fresh state 取得失敗 → ref なしで index-only にフォールバック
4113
+ }
4114
+
3440
4115
  // 複数選択
3441
4116
  if (sel.isMultiSelect && sel.blockIds) {
3442
4117
  let text = `選択中: ${sel.blockIds.length}ブロック\n` +
3443
4118
  `タイプ: ${sel.blockTypes?.join(", ")}\n` +
3444
- `位置: ${sel.blockIndices?.join(", ")}${_mt_gs}`;
4119
+ `位置: ${sel.blockIndices?.join(", ")}`;
4120
+ if (_snapInfo && sel.blockIndices?.length > 0) {
4121
+ const refs = sel.blockIndices
4122
+ .map(idx => _snapInfo.blocks?.find(b => b.index === idx)?.ref)
4123
+ .filter(Boolean);
4124
+ // 全数一致のときだけ refs を返す(部分一致は AI を誤誘導するため)
4125
+ if (refs.length === sel.blockIndices.length) {
4126
+ text += `\nrefs: ${refs.join(", ")}`;
4127
+ text += `\n[snapshot:${_snapInfo.snapshotId} rev:${_snapInfo.revision}]`;
4128
+ }
4129
+ }
4130
+ text += _mt_gs;
3445
4131
  return { content: [{ type: "text", text }] };
3446
4132
  }
3447
4133
  // 単一選択
3448
4134
  let text = `選択中: ${sel.blockType}\n` +
3449
4135
  `位置: index ${sel.blockIndex}`;
4136
+ if (_snapInfo) {
4137
+ const refEntry = _snapInfo.blocks?.find(b => b.index === sel.blockIndex);
4138
+ if (refEntry) {
4139
+ text += `\nref: ${refEntry.ref}`;
4140
+ text += `\n[snapshot:${_snapInfo.snapshotId} rev:${_snapInfo.revision}]`;
4141
+ }
4142
+ }
3450
4143
  if (sel.textSelection?.text) {
3451
4144
  text += `\n\nカーソル選択テキスト: "${sel.textSelection.text}"`;
3452
4145
  if (sel.textSelection.context) {
@@ -3474,6 +4167,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3474
4167
  const _siteName = siteName || args?.site || 'default';
3475
4168
 
3476
4169
  const { action } = (args || {});
4170
+ const _inputRef = args?.ref || null;
3477
4171
  if (!action) {
3478
4172
  return { content: [{ type: "text", text: "❌ action は必須です" }], isError: true };
3479
4173
  }
@@ -3490,6 +4184,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3490
4184
  mode, client, postId, sessionId: _sessionId, siteName: _siteName,
3491
4185
  });
3492
4186
  if (resolved.error) return resolved.error;
4187
+ const _preState = resolved.currentState || null; // Phase 2: 差分計算用
3493
4188
 
3494
4189
  const index = resolved.index !== undefined ? resolved.index : args?.index;
3495
4190
  if (index === undefined) {
@@ -3536,8 +4231,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3536
4231
  if (!_isWrite) {
3537
4232
  return { content: [{ type: "text", text: formatStructure(result) + _mt_to }] };
3538
4233
  }
3539
- const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
3540
- return { content: [{ type: "text", text: appendSnapshotToText(`✅ ${action} 完了: ${JSON.stringify(result.detail || result)}${_mt_to}`, _snap) }] };
4234
+ const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
4235
+ const _msg = `✅ ${action} 完了: ${JSON.stringify(result.detail || result)}${_mt_to}`;
4236
+ if (_inputRef) {
4237
+ const changedIndex = result.targetIndex ?? index;
4238
+ const syntheticResult = { changes: [{ oldIndex: changedIndex, newIndices: [changedIndex], success: true }] };
4239
+ const changeInfo = buildChangeInfoFromResult('updated', _snap, syntheticResult, _preState);
4240
+ if (changeInfo) return { content: [{ type: "text", text: appendRefChangesToText(_msg, changeInfo) }] };
4241
+ }
4242
+ return { content: [{ type: "text", text: appendSnapshotToTextLegacy(_msg, _snap) }] };
3541
4243
  } catch (e) {
3542
4244
  const formatted = formatHeadlessConflictError(e);
3543
4245
  if (formatted) return formatted;
@@ -3555,8 +4257,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3555
4257
  if (!_isWrite) {
3556
4258
  return { content: [{ type: "text", text: formatStructure(result.structure) + _mt_to }] };
3557
4259
  }
3558
- const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName);
3559
- return { content: [{ type: "text", text: appendSnapshotToText(`✅ ${action} 完了: ${JSON.stringify(result.detail || {})}${_mt_to}`, _snap) }] };
4260
+ const _snap = await buildResponseSnapshot(mode, client, postId, _sessionId, _siteName, result);
4261
+ const _msg = `✅ ${action} 完了: ${JSON.stringify(result.detail || {})}${_mt_to}`;
4262
+ if (_inputRef) {
4263
+ const changedIndex = result.targetIndex ?? index;
4264
+ const syntheticResult = { changes: [{ oldIndex: changedIndex, newIndices: [changedIndex], success: true }] };
4265
+ const changeInfo = buildChangeInfoFromResult('updated', _snap, syntheticResult, _preState);
4266
+ if (changeInfo) return { content: [{ type: "text", text: appendRefChangesToText(_msg, changeInfo) }] };
4267
+ }
4268
+ return { content: [{ type: "text", text: appendSnapshotToTextLegacy(_msg, _snap) }] };
3560
4269
  }
3561
4270
 
3562
4271
  case "open_in_browser": {
package/dist/ws-server.js CHANGED
@@ -42,7 +42,7 @@ export class FridayWSServer {
42
42
  throw err;
43
43
  }
44
44
  this._startHeartbeatCheck();
45
- console.error(`[F.R.I.D.A.Y][WS] Token: ${this.token.slice(0, 8)}...`);
45
+ dbg(`Token: ${this.token.slice(0, 8)}...`);
46
46
  }
47
47
 
48
48
  stop() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "friday-mcp-v2",
3
- "version": "3.0.1",
3
+ "version": "3.0.3",
4
4
  "description": "WordPress MCP Server for Claude Code - REST API direct communication",
5
5
  "type": "module",
6
6
  "main": "dist/mcp-server.js",