dedsession 2.3.1 → 3.0.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/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import * as path from "path";
8
8
  // КОНФИГУРАЦИЯ
9
9
  // ============================================================================
10
10
  const CONFIG = {
11
- VERSION: "2.3.1",
11
+ VERSION: "3.0.0",
12
12
  };
13
13
  const state = {
14
14
  loadedContext: null,
@@ -1084,6 +1084,8 @@ function ensureMemoryStructure() {
1084
1084
  fs.writeFileSync(filePath, JSON.stringify(file.default, null, 2), "utf-8");
1085
1085
  }
1086
1086
  }
1087
+ // v3.0: Создаём _DIGEST/ директорию
1088
+ ensureDigestDir();
1087
1089
  }
1088
1090
  function loadMemoryIndex() {
1089
1091
  const filePath = path.join(getMemoryPath(), "index.json");
@@ -1160,6 +1162,363 @@ function saveSolutions(solutions) {
1160
1162
  const filePath = path.join(getMemoryPath(), "solutions.json");
1161
1163
  fs.writeFileSync(filePath, JSON.stringify(solutions, null, 2), "utf-8");
1162
1164
  }
1165
+ // ============================================================================
1166
+ // PROJECT DIGEST v3.0 — Инфраструктура
1167
+ // ============================================================================
1168
+ function getDigestPath() {
1169
+ return path.join(getMdHistoryPath(), "_DIGEST");
1170
+ }
1171
+ function ensureDigestDir() {
1172
+ const digestPath = getDigestPath();
1173
+ if (!fs.existsSync(digestPath)) {
1174
+ fs.mkdirSync(digestPath, { recursive: true });
1175
+ }
1176
+ }
1177
+ function createEmptyDigest(project) {
1178
+ const today = new Date().toISOString().split("T")[0];
1179
+ return {
1180
+ version: CONFIG.VERSION,
1181
+ project,
1182
+ lastUpdated: today,
1183
+ statistics: {
1184
+ totalContexts: 0,
1185
+ completed: 0,
1186
+ inProgress: 0,
1187
+ blocked: 0,
1188
+ firstDate: today,
1189
+ lastDate: today,
1190
+ },
1191
+ currentState: {
1192
+ description: "",
1193
+ keyFiles: [],
1194
+ techStack: [],
1195
+ activeEpics: [],
1196
+ },
1197
+ recentTasks: [],
1198
+ knownIssues: [],
1199
+ keyDecisions: [],
1200
+ solutions: [],
1201
+ phases: [],
1202
+ };
1203
+ }
1204
+ function loadDigest() {
1205
+ const jsonPath = path.join(getDigestPath(), "digest.json");
1206
+ try {
1207
+ if (fs.existsSync(jsonPath)) {
1208
+ const raw = JSON.parse(fs.readFileSync(jsonPath, "utf-8"));
1209
+ // Schema migration — гарантируем все поля
1210
+ raw.phases = raw.phases || [];
1211
+ raw.solutions = raw.solutions || [];
1212
+ raw.keyDecisions = raw.keyDecisions || [];
1213
+ raw.knownIssues = raw.knownIssues || [];
1214
+ raw.recentTasks = raw.recentTasks || [];
1215
+ raw.currentState = raw.currentState || { description: "", keyFiles: [], techStack: [], activeEpics: [] };
1216
+ raw.currentState.keyFiles = raw.currentState.keyFiles || [];
1217
+ raw.currentState.techStack = raw.currentState.techStack || [];
1218
+ raw.currentState.activeEpics = raw.currentState.activeEpics || [];
1219
+ raw.statistics = raw.statistics || { totalContexts: 0, completed: 0, inProgress: 0, blocked: 0, firstDate: "", lastDate: "" };
1220
+ return raw;
1221
+ }
1222
+ }
1223
+ catch { /* ignore */ }
1224
+ return null;
1225
+ }
1226
+ function saveDigest(digest) {
1227
+ ensureDigestDir();
1228
+ const digestPath = getDigestPath();
1229
+ // Сохраняем JSON
1230
+ fs.writeFileSync(path.join(digestPath, "digest.json"), JSON.stringify(digest, null, 2), "utf-8");
1231
+ // Рендерим все 5 MD файлов
1232
+ fs.writeFileSync(path.join(digestPath, "00-overview.md"), renderOverviewMd(digest), "utf-8");
1233
+ fs.writeFileSync(path.join(digestPath, "01-timeline.md"), renderTimelineMd(digest), "utf-8");
1234
+ fs.writeFileSync(path.join(digestPath, "02-architecture.md"), renderArchitectureMd(digest), "utf-8");
1235
+ fs.writeFileSync(path.join(digestPath, "03-decisions.md"), renderDecisionsMd(digest), "utf-8");
1236
+ fs.writeFileSync(path.join(digestPath, "04-solutions.md"), renderSolutionsMd(digest), "utf-8");
1237
+ }
1238
+ // ============================================================================
1239
+ // PROJECT DIGEST v3.0 — 5 рендеров
1240
+ // ============================================================================
1241
+ function renderOverviewMd(digest) {
1242
+ let md = `# 📦 Project Digest: ${digest.project}\n`;
1243
+ md += `> Обновлено: ${digest.lastUpdated} | Контекстов: ${digest.statistics.totalContexts}\n\n`;
1244
+ md += `## Что это за проект\n`;
1245
+ md += `${digest.currentState.description || "_Описание будет добавлено после digest_refresh_"}\n\n`;
1246
+ md += `## Текущее состояние\n`;
1247
+ md += `- **Контекстов:** ${digest.statistics.totalContexts} (✅ ${digest.statistics.completed} | ⏳ ${digest.statistics.inProgress})\n`;
1248
+ md += `- **Период:** ${digest.statistics.firstDate} — ${digest.statistics.lastDate}\n`;
1249
+ if (digest.currentState.techStack.length > 0) {
1250
+ md += `- **Стек:** ${digest.currentState.techStack.join(", ")}\n`;
1251
+ }
1252
+ if (digest.currentState.keyFiles.length > 0) {
1253
+ md += `- **Ключевые файлы:** ${digest.currentState.keyFiles.slice(0, 10).join(", ")}\n`;
1254
+ }
1255
+ if (digest.currentState.activeEpics.length > 0) {
1256
+ md += `- **Активные эпики:** ${digest.currentState.activeEpics.join(", ")}\n`;
1257
+ }
1258
+ md += "\n";
1259
+ if (digest.phases.length > 0) {
1260
+ md += `## Эволюция проекта\n`;
1261
+ for (const phase of digest.phases) {
1262
+ md += `### ${phase.name} (${phase.dateRange})\n`;
1263
+ md += `${phase.description}\n\n`;
1264
+ }
1265
+ }
1266
+ // В работе сейчас
1267
+ const inProgress = digest.recentTasks.filter(t => t.status === "⏳" || t.status === "⏳ В процессе" || t.status === "in_progress");
1268
+ if (inProgress.length > 0) {
1269
+ md += `## В работе сейчас\n`;
1270
+ for (const task of inProgress.slice(0, 5)) {
1271
+ md += `- **#${task.contextId}: ${task.name}** — ${task.summary}\n`;
1272
+ }
1273
+ md += "\n";
1274
+ }
1275
+ if (digest.knownIssues.length > 0) {
1276
+ md += `## Известные проблемы\n`;
1277
+ for (const issue of digest.knownIssues) {
1278
+ md += `- ${issue}\n`;
1279
+ }
1280
+ md += "\n";
1281
+ }
1282
+ return md;
1283
+ }
1284
+ function renderTimelineMd(digest) {
1285
+ let md = `# 📅 Timeline: последние ${digest.recentTasks.length} задач\n\n`;
1286
+ if (digest.recentTasks.length === 0) {
1287
+ md += "_Нет задач_\n";
1288
+ return md;
1289
+ }
1290
+ // Таблица всех задач
1291
+ md += `| # | Дата | Задача | Статус | Суть |\n`;
1292
+ md += `|---|------|--------|--------|------|\n`;
1293
+ for (const task of digest.recentTasks) {
1294
+ const summary = task.summary || "";
1295
+ const shortSummary = summary.length > 60 ? summary.slice(0, 57) + "..." : summary;
1296
+ md += `| ${task.contextId} | ${task.date} | ${task.name} | ${task.status} | ${shortSummary} |\n`;
1297
+ }
1298
+ md += "\n";
1299
+ // Детали последних 10
1300
+ md += `## Детали последних ${Math.min(10, digest.recentTasks.length)} задач\n\n`;
1301
+ for (const task of digest.recentTasks.slice(0, 10)) {
1302
+ md += `### #${task.contextId}: ${task.name} (${task.date}) ${task.status}\n`;
1303
+ md += `Суть: ${task.summary || ""}\n`;
1304
+ if (task.keyChanges && task.keyChanges.length > 0) {
1305
+ md += `Изменения:\n`;
1306
+ for (const change of task.keyChanges) {
1307
+ md += `- ${change}\n`;
1308
+ }
1309
+ }
1310
+ md += "\n";
1311
+ }
1312
+ return md;
1313
+ }
1314
+ function renderArchitectureMd(digest) {
1315
+ let md = `# 🏗️ Архитектура проекта: ${digest.project}\n\n`;
1316
+ if (digest.currentState.keyFiles.length > 0) {
1317
+ md += `## Ключевые файлы\n`;
1318
+ md += `| Файл | Упоминания |\n`;
1319
+ md += `|------|------------|\n`;
1320
+ for (const file of digest.currentState.keyFiles.slice(0, 30)) {
1321
+ md += `| ${file} | — |\n`;
1322
+ }
1323
+ md += "\n";
1324
+ }
1325
+ if (digest.currentState.techStack.length > 0) {
1326
+ md += `## Стек технологий\n`;
1327
+ for (const tech of digest.currentState.techStack) {
1328
+ md += `- ${tech}\n`;
1329
+ }
1330
+ md += "\n";
1331
+ }
1332
+ return md;
1333
+ }
1334
+ function renderDecisionsMd(digest) {
1335
+ let md = `# 💡 Ключевые решения\n\n`;
1336
+ if (digest.keyDecisions.length === 0) {
1337
+ md += "_Решения будут добавлены при сохранении контекстов с полем decisions_\n";
1338
+ return md;
1339
+ }
1340
+ for (const dec of digest.keyDecisions) {
1341
+ md += `## Решение (${dec.date}, #${dec.contextId})\n`;
1342
+ md += `${dec.decision}\n\n`;
1343
+ }
1344
+ return md;
1345
+ }
1346
+ function renderSolutionsMd(digest) {
1347
+ let md = `# 🔧 Проблемы и решения\n\n`;
1348
+ if (digest.solutions.length === 0) {
1349
+ md += "_Решения будут добавлены при сохранении контекстов с полем problems_solutions_\n";
1350
+ return md;
1351
+ }
1352
+ for (const sol of digest.solutions) {
1353
+ md += `## Проблема (#${sol.contextId})\n`;
1354
+ md += `**Проблема:** ${sol.problem}\n`;
1355
+ md += `**Решение:** ${sol.solution}\n\n`;
1356
+ }
1357
+ return md;
1358
+ }
1359
+ // ============================================================================
1360
+ // PROJECT DIGEST v3.0 — detectProjectFromWorkDir + update
1361
+ // ============================================================================
1362
+ function detectProjectFromWorkDir() {
1363
+ try {
1364
+ const workDir = getWorkingDir();
1365
+ const dirName = path.basename(workDir).toLowerCase();
1366
+ // Проверяем PROJECT_PATTERNS
1367
+ for (const [project, patterns] of Object.entries(PROJECT_PATTERNS)) {
1368
+ for (const pattern of patterns) {
1369
+ if (dirName.includes(pattern)) {
1370
+ return project;
1371
+ }
1372
+ }
1373
+ }
1374
+ // Возвращаем имя директории КАК ЕСТЬ
1375
+ return path.basename(workDir);
1376
+ }
1377
+ catch {
1378
+ return null;
1379
+ }
1380
+ }
1381
+ function extractKeyChangesFromText(changes) {
1382
+ if (!changes)
1383
+ return [];
1384
+ const lines = changes.split("\n")
1385
+ .map(l => l.trim())
1386
+ .filter(l => l.length > 5)
1387
+ .filter(l => l.startsWith("-") || l.startsWith("*") || l.startsWith("•") || /^\d+\./.test(l) || /^[A-Za-zА-Яа-я]/.test(l));
1388
+ return lines
1389
+ .map(l => l.replace(/^[-*•]\s*/, "").replace(/^\d+\.\s*/, ""))
1390
+ .filter(l => l.length > 5)
1391
+ .slice(0, 5);
1392
+ }
1393
+ function extractFilesFromChanges(changes) {
1394
+ if (!changes)
1395
+ return [];
1396
+ const filePatterns = changes.match(/(?:^|\s|`)([\w./\\-]+\.\w{1,10})(?:\s|$|`|,|:|\))/gm);
1397
+ if (!filePatterns)
1398
+ return [];
1399
+ const files = filePatterns
1400
+ .map(m => m.trim().replace(/[`,:)]/g, "").trim())
1401
+ .filter(f => f.includes(".") && !f.startsWith("http") && f.length < 100);
1402
+ return [...new Set(files)].slice(0, 30);
1403
+ }
1404
+ function extractSolutionsFromChanges(text, contextId) {
1405
+ if (!text)
1406
+ return [];
1407
+ const results = [];
1408
+ // Паттерны "Проблема: X / Решение: Y"
1409
+ const pairs = text.match(/(?:проблема|problem|ошибка|баг|bug)[:\s]*([^\n]+)[\s\S]*?(?:решение|solution|фикс|fix)[:\s]*([^\n]+)/gi);
1410
+ if (pairs) {
1411
+ for (const pair of pairs.slice(0, 5)) {
1412
+ const probMatch = pair.match(/(?:проблема|problem|ошибка|баг|bug)[:\s]*([^\n]+)/i);
1413
+ const solMatch = pair.match(/(?:решение|solution|фикс|fix)[:\s]*([^\n]+)/i);
1414
+ if (probMatch && solMatch) {
1415
+ results.push({
1416
+ problem: probMatch[1].trim(),
1417
+ solution: solMatch[1].trim(),
1418
+ contextId,
1419
+ });
1420
+ }
1421
+ }
1422
+ }
1423
+ return results.slice(0, 5);
1424
+ }
1425
+ function extractDecisionsFromText(text, contextId, date) {
1426
+ if (!text)
1427
+ return [];
1428
+ const results = [];
1429
+ // Разбиваем на секции/абзацы
1430
+ const sections = text.split(/\n(?=##?\s|[-*]\s)/);
1431
+ for (const section of sections) {
1432
+ const trimmed = section.trim();
1433
+ if (trimmed.length > 20 && trimmed.length < 500) {
1434
+ results.push({ decision: trimmed, date, contextId });
1435
+ }
1436
+ }
1437
+ return results.slice(0, 5);
1438
+ }
1439
+ function updateDigestOnSave(data) {
1440
+ try {
1441
+ let digest = loadDigest();
1442
+ if (!digest) {
1443
+ digest = createEmptyDigest(data.project);
1444
+ }
1445
+ const today = data.date || new Date().toISOString().split("T")[0];
1446
+ // 1. Проверяем дубликат (если тот же contextId уже есть)
1447
+ const isDuplicate = digest.recentTasks.some(t => t.contextId === data.contextId);
1448
+ // 2. Обновляем statistics (только если новый контекст)
1449
+ if (!isDuplicate) {
1450
+ digest.statistics.totalContexts++;
1451
+ if (data.status.includes("✅") || data.status === "completed") {
1452
+ digest.statistics.completed++;
1453
+ }
1454
+ else if (data.status.includes("⏳") || data.status === "in_progress") {
1455
+ digest.statistics.inProgress++;
1456
+ }
1457
+ else if (data.status.includes("❌") || data.status === "blocked") {
1458
+ digest.statistics.blocked++;
1459
+ }
1460
+ }
1461
+ digest.statistics.lastDate = today;
1462
+ if (!digest.statistics.firstDate || today < digest.statistics.firstDate) {
1463
+ digest.statistics.firstDate = today;
1464
+ }
1465
+ // 3. Prepend/update в recentTasks (лимит 30)
1466
+ const summaryText = data.historySummary || data.summary || "";
1467
+ const statusIcon = data.status.includes("✅") ? "✅" :
1468
+ data.status.includes("⏳") ? "⏳" :
1469
+ data.status.includes("❌") ? "❌" : data.status;
1470
+ // Убираем дубликат если есть
1471
+ digest.recentTasks = digest.recentTasks.filter(t => t.contextId !== data.contextId);
1472
+ digest.recentTasks.unshift({
1473
+ contextId: data.contextId,
1474
+ date: today,
1475
+ name: data.taskName,
1476
+ status: statusIcon,
1477
+ summary: summaryText.slice(0, 200),
1478
+ keyChanges: extractKeyChangesFromText(data.changes || ""),
1479
+ });
1480
+ digest.recentTasks = digest.recentTasks.slice(0, 30);
1481
+ // 3. Извлекаем файлы из changes → обновляем keyFiles
1482
+ const newFiles = extractFilesFromChanges(data.changes || "");
1483
+ for (const f of newFiles) {
1484
+ if (!digest.currentState.keyFiles.includes(f)) {
1485
+ digest.currentState.keyFiles.push(f);
1486
+ }
1487
+ }
1488
+ digest.currentState.keyFiles = digest.currentState.keyFiles.slice(0, 30);
1489
+ // 4. decisions → keyDecisions (лимит 20)
1490
+ if (data.decisions) {
1491
+ const newDecisions = extractDecisionsFromText(data.decisions, data.contextId, today);
1492
+ digest.keyDecisions = [...newDecisions, ...digest.keyDecisions].slice(0, 20);
1493
+ }
1494
+ // 5. problems_solutions → solutions (лимит 20)
1495
+ if (data.problemsSolutions) {
1496
+ const newSolutions = extractSolutionsFromChanges(data.problemsSolutions, data.contextId);
1497
+ digest.solutions = [...newSolutions, ...digest.solutions].slice(0, 20);
1498
+ }
1499
+ // 6. knownIssues для in_progress
1500
+ if (data.status.includes("⏳") || data.status === "in_progress") {
1501
+ const issue = `[#${data.contextId}] ${data.taskName}: ${(data.summary || "").slice(0, 100)}`;
1502
+ if (!digest.knownIssues.some(i => i.includes(`#${data.contextId}`))) {
1503
+ digest.knownIssues.unshift(issue);
1504
+ digest.knownIssues = digest.knownIssues.slice(0, 10);
1505
+ }
1506
+ }
1507
+ // 7. digest_update → обновляет описание
1508
+ if (data.digestUpdate) {
1509
+ digest.currentState.description = data.digestUpdate;
1510
+ }
1511
+ // 8. Обновляем метаданные
1512
+ digest.lastUpdated = today;
1513
+ digest.project = data.project;
1514
+ // 9. Сохраняем
1515
+ saveDigest(digest);
1516
+ return true;
1517
+ }
1518
+ catch {
1519
+ return false;
1520
+ }
1521
+ }
1163
1522
  /**
1164
1523
  * Извлекает решения из текста контекста (legacy и новый формат)
1165
1524
  * Ищет паттерны: "Проблема:", "Решение:", "fix", "исправил", "причина" и т.д.
@@ -2082,6 +2441,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2082
2441
  type: "string",
2083
2442
  description: "⭐ ВАЖНО: Готовая качественная сводка для HISTORY.md (2-4 предложения). Формат: **Сводка:** что сделано, суть работы, результат. **Файлы:** список изменённых файлов. Если не передать — будет автогенерация низкого качества!",
2084
2443
  },
2444
+ digest_update: {
2445
+ type: "string",
2446
+ description: "(v3.0) Обновление описания/состояния проекта для дайджеста. Если передано — обновляет описание проекта в 00-overview.md",
2447
+ },
2085
2448
  },
2086
2449
  required: ["task_name", "changes", "summary", "session_log"],
2087
2450
  },
@@ -2165,6 +2528,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2165
2528
  type: "string",
2166
2529
  description: "⭐ ОБЯЗАТЕЛЬНО: Качественная сводка для HISTORY.md (2-4 предложения). Формат: **Сводка:** суть задачи, что сделано, результат. **Файлы:** список изменённых файлов.",
2167
2530
  },
2531
+ digest_update: {
2532
+ type: "string",
2533
+ description: "(v3.0) Обновление описания/состояния проекта для дайджеста. Если передано — обновляет описание проекта в 00-overview.md",
2534
+ },
2168
2535
  },
2169
2536
  required: ["task_name", "task_overview", "analysis", "changes", "summary", "session_log", "code_snippets", "decisions", "problems_solutions", "architecture", "history_summary"],
2170
2537
  },
@@ -2393,6 +2760,68 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2393
2760
  required: ["action"],
2394
2761
  },
2395
2762
  },
2763
+ // ========== PROJECT DIGEST v3.0 ==========
2764
+ {
2765
+ name: "context_digest",
2766
+ description: "📦 Просмотр дайджеста проекта. ОБЯЗАТЕЛЬНО вызывай когда пользователь говорит: 'дайджест', 'digest', 'обзор проекта'. Загружает и показывает ВСЕ 5 MD файлов из _DIGEST/.",
2767
+ inputSchema: {
2768
+ type: "object",
2769
+ properties: {},
2770
+ },
2771
+ },
2772
+ {
2773
+ name: "context_digest_migrate",
2774
+ description: "📦 Миграция: создать дайджест из существующих контекстов. ОБЯЗАТЕЛЬНО вызывай когда пользователь говорит: 'миграция дайджеста', 'создай дайджест', 'digest migrate'. Сканирует ВСЕ контексты и строит полный дайджест. Безопасен для повторного запуска.",
2775
+ inputSchema: {
2776
+ type: "object",
2777
+ properties: {},
2778
+ },
2779
+ },
2780
+ {
2781
+ name: "context_digest_refresh",
2782
+ description: "🔄 Claude перечитывает и обновляет дайджест. ОБЯЗАТЕЛЬНО вызывай когда пользователь говорит: 'обнови дайджест', 'refresh digest'. Возвращает текущий digest.json + README последних 10 контекстов. Claude анализирует и вызывает context_digest_write.",
2783
+ inputSchema: {
2784
+ type: "object",
2785
+ properties: {},
2786
+ },
2787
+ },
2788
+ {
2789
+ name: "context_digest_write",
2790
+ description: "📝 Запись качественного дайджеста от Claude. Вызывается ПОСЛЕ context_digest_refresh. Claude передаёт проанализированные данные для обновления дайджеста.",
2791
+ inputSchema: {
2792
+ type: "object",
2793
+ properties: {
2794
+ description: {
2795
+ type: "string",
2796
+ description: "Описание проекта: цель, суть, для кого (3-5 предложений)",
2797
+ },
2798
+ tech_stack: {
2799
+ type: "string",
2800
+ description: "Стек технологий через запятую: TypeScript, Node.js, MCP, etc.",
2801
+ },
2802
+ key_files: {
2803
+ type: "string",
2804
+ description: "Ключевые файлы через запятую: src/index.ts, package.json, etc.",
2805
+ },
2806
+ active_epics: {
2807
+ type: "string",
2808
+ description: "Активные эпики через запятую",
2809
+ },
2810
+ known_issues: {
2811
+ type: "string",
2812
+ description: "Известные проблемы, каждая на новой строке",
2813
+ },
2814
+ phases: {
2815
+ type: "string",
2816
+ description: "Фазы эволюции проекта. Формат: 'Название фазы | даты | описание' на каждой строке",
2817
+ },
2818
+ key_decisions: {
2819
+ type: "string",
2820
+ description: "Ключевые решения, каждое на новой строке",
2821
+ },
2822
+ },
2823
+ },
2824
+ },
2396
2825
  // ========== ТЕСТОВЫЕ КОМАНДЫ SMART CONTEXT ==========
2397
2826
  {
2398
2827
  name: "context_smart",
@@ -2456,12 +2885,36 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2456
2885
  output += lastContextMatch[0] + "\n---\n\n";
2457
2886
  }
2458
2887
  }
2459
- output += `## 📊 Статистика\n\n`;
2460
- output += `| Показатель | Значение |\n|------------|----------|\n`;
2461
- output += `| Всего контекстов | ${stats.total} |\n`;
2462
- output += `| За неделю | ${stats.thisWeek} |\n`;
2463
- output += `| Завершено | ${stats.completed} |\n`;
2464
- output += `| В процессе | ${stats.inProgress} |\n\n`;
2888
+ // v3.0: Показываем дайджест если есть
2889
+ const startDigest = loadDigest();
2890
+ if (startDigest) {
2891
+ output += `## 📦 Project Digest: ${startDigest.project}\n\n`;
2892
+ output += `${(startDigest.currentState.description || "_Описание не задано_").slice(0, 500)}\n\n`;
2893
+ output += `| Показатель | Значение |\n|------------|----------|\n`;
2894
+ output += `| Всего контекстов | ${startDigest.statistics.totalContexts} |\n`;
2895
+ output += `| Завершено | ${startDigest.statistics.completed} |\n`;
2896
+ output += `| В процессе | ${startDigest.statistics.inProgress} |\n`;
2897
+ output += `| Период | ${startDigest.statistics.firstDate} — ${startDigest.statistics.lastDate} |\n\n`;
2898
+ // Последние 5 задач из таймлайна
2899
+ if (startDigest.recentTasks.length > 0) {
2900
+ output += `### Последние задачи\n\n`;
2901
+ output += `| # | Дата | Задача | Статус |\n|---|------|--------|--------|\n`;
2902
+ for (const task of startDigest.recentTasks.slice(0, 5)) {
2903
+ output += `| ${task.contextId} | ${task.date} | ${task.name} | ${task.status} |\n`;
2904
+ }
2905
+ output += "\n";
2906
+ }
2907
+ output += `💡 Полный дайджест: \`context_digest\`\n\n`;
2908
+ output += `---\n\n`;
2909
+ }
2910
+ else {
2911
+ output += `## 📊 Статистика\n\n`;
2912
+ output += `| Показатель | Значение |\n|------------|----------|\n`;
2913
+ output += `| Всего контекстов | ${stats.total} |\n`;
2914
+ output += `| За неделю | ${stats.thisWeek} |\n`;
2915
+ output += `| Завершено | ${stats.completed} |\n`;
2916
+ output += `| В процессе | ${stats.inProgress} |\n\n`;
2917
+ }
2465
2918
  // Архитектура проекта если есть
2466
2919
  if (architecture.length > 0) {
2467
2920
  output += `## 🏗️ Архитектура проекта (из MD/)\n\n`;
@@ -2581,6 +3034,23 @@ ${args?.changes?.split("\n").map(l => l.trim() ? `- ${l}` : "").join("\n") || "-
2581
3034
  const extracted = extractSolutionsFromText(solutionsContent, contextId);
2582
3035
  solutionsAdded = addSolutionsToIndex(extracted, contextId);
2583
3036
  }
3037
+ // v3.0: Обновляем дайджест
3038
+ const digestProject = detectProjectFromWorkDir() || memoryInfo.project || "unknown";
3039
+ const digestContextId = String(contextNumber).padStart(3, "0");
3040
+ const digestUpdated = updateDigestOnSave({
3041
+ project: digestProject,
3042
+ contextId: digestContextId,
3043
+ taskName,
3044
+ status,
3045
+ changes: args?.changes,
3046
+ summary: args?.summary,
3047
+ historySummary: args?.history_summary,
3048
+ problemsSolutions: args?.problems_solutions,
3049
+ decisions: args?.decisions,
3050
+ architecture: args?.architecture,
3051
+ date: new Date().toISOString().split("T")[0],
3052
+ digestUpdate: args?.digest_update,
3053
+ });
2584
3054
  let output = `✅ **Контекст сохранён**\n\n`;
2585
3055
  output += `📁 **Папка:** \`${result.name}\`\n`;
2586
3056
  output += `📄 **Файлов:** ${result.filesCount}\n`;
@@ -2596,6 +3066,10 @@ ${args?.changes?.split("\n").map(l => l.trim() ? `- ${l}` : "").join("\n") || "-
2596
3066
  output += `- 💡 Решений добавлено: **${solutionsAdded}**\n`;
2597
3067
  }
2598
3068
  }
3069
+ // v3.0: Дайджест статус
3070
+ if (digestUpdated) {
3071
+ output += `\n📦 **Дайджест обновлён** (${digestProject})\n`;
3072
+ }
2599
3073
  if (state.loadedContext && state.loadedContext !== result.name) {
2600
3074
  output += `\n🔗 **Parent:** ${state.loadedContext}\n`;
2601
3075
  }
@@ -2728,6 +3202,22 @@ ${state.loadedContext ? `**Parent:** ${state.loadedContext}` : "Нет parent к
2728
3202
  const contextId = String(contextNumber).padStart(3, "0");
2729
3203
  const extracted = extractSolutionsFromText(solutionsContent, contextId);
2730
3204
  solutionsAdded = addSolutionsToIndex(extracted, contextId);
3205
+ // v3.0: Обновляем дайджест (detailed передаёт больше данных)
3206
+ const digestProjectD = detectProjectFromWorkDir() || memoryInfo.project || "unknown";
3207
+ const digestUpdatedD = updateDigestOnSave({
3208
+ project: digestProjectD,
3209
+ contextId,
3210
+ taskName,
3211
+ status,
3212
+ changes: args?.changes,
3213
+ summary: args?.summary,
3214
+ historySummary: args?.history_summary,
3215
+ problemsSolutions: args?.problems_solutions,
3216
+ decisions: args?.decisions,
3217
+ architecture: args?.architecture,
3218
+ date: new Date().toISOString().split("T")[0],
3219
+ digestUpdate: args?.digest_update,
3220
+ });
2731
3221
  let output = `✅ **Контекст сохранён ПОДРОБНО**\n\n`;
2732
3222
  output += `📁 **Папка:** \`${result.name}\`\n`;
2733
3223
  output += `📄 **Файлов:** 15\n`;
@@ -2742,6 +3232,10 @@ ${state.loadedContext ? `**Parent:** ${state.loadedContext}` : "Нет parent к
2742
3232
  output += `- 💡 Решений добавлено: **${solutionsAdded}**\n`;
2743
3233
  }
2744
3234
  }
3235
+ // v3.0: Дайджест статус
3236
+ if (digestUpdatedD) {
3237
+ output += `\n📦 **Дайджест обновлён** (${digestProjectD})\n`;
3238
+ }
2745
3239
  if (state.loadedContext && state.loadedContext !== result.name) {
2746
3240
  output += `\n🔗 **Parent:** ${state.loadedContext}\n`;
2747
3241
  }
@@ -2765,7 +3259,7 @@ ${state.loadedContext ? `**Parent:** ${state.loadedContext}` : "Нет parent к
2765
3259
  .map(l => l.startsWith("-") ? l : `- ${l}`)
2766
3260
  .join("\n");
2767
3261
  const commitTitle = `📝 Context: ${contextShortName}`;
2768
- const commitBody = `Статус: ${status} | Файлов: 10\n\nЧто сделано:\n${changeLines}`;
3262
+ const commitBody = `Статус: ${status} | Файлов: 15\n\nЧто сделано:\n${changeLines}`;
2769
3263
  output += `\n\n---\n\n`;
2770
3264
  output += `📋 **Для коммита:**\n\n`;
2771
3265
  output += `**Title:**\n\`\`\`\n${commitTitle}\n\`\`\`\n\n`;
@@ -3006,15 +3500,20 @@ MD_HISTORY/
3006
3500
  if (allContexts.length === 0) {
3007
3501
  return { content: [{ type: "text", text: "📭 Нет сохранённых контекстов" }] };
3008
3502
  }
3503
+ // v3.0: Проверяем наличие дайджеста
3504
+ const digest = loadDigest();
3505
+ const hasDigest = digest !== null;
3009
3506
  // v2.0: Получаем Memory stats
3010
3507
  const memoryStats = getMemoryStats();
3011
3508
  const hasMemory = memoryStats.totalContexts > 0;
3012
3509
  // v1.4.10: Читаем HISTORY.md и учитываем его размер
3013
3510
  const history = readHistory();
3014
3511
  const historySize = history ? history.length : 0;
3512
+ // v3.0: Когда есть дайджест — другой режим работы
3513
+ const contextCount = hasDigest ? Math.min(3, allContexts.length) : Math.min(5, allContexts.length);
3015
3514
  // Определяем стратегию с учётом размера HISTORY.md
3016
3515
  const strategy = determineQuickLoadStrategy(allContexts, historySize);
3017
- const contexts = allContexts.slice(0, strategy.count);
3516
+ const contexts = allContexts.slice(0, contextCount);
3018
3517
  // Функция обрезки текста с ... в конце
3019
3518
  const truncate = (text, maxLen) => {
3020
3519
  if (text.length <= maxLen)
@@ -3024,10 +3523,40 @@ MD_HISTORY/
3024
3523
  let output = `# ⚡ Краткий контекст — Dedsession v${CONFIG.VERSION}\n\n`;
3025
3524
  output += `📁 **Рабочая директория:** \`${workDir}\`\n`;
3026
3525
  output += `📊 **Статистика:** Всего ${stats.total} | ✅ ${stats.completed} | ⏳ ${stats.inProgress}\n`;
3027
- output += `🎯 **Режим:** ${strategy.reason}\n`;
3028
- output += `📦 **Загружено контекстов:** ${contexts.length}\n\n`;
3526
+ if (hasDigest) {
3527
+ output += `📦 **Режим:** Дайджест + ${contextCount} контекстов (v3.0)\n\n`;
3528
+ }
3529
+ else {
3530
+ output += `🎯 **Режим:** ${strategy.reason}\n`;
3531
+ output += `📦 **Загружено контекстов:** ${contexts.length}\n\n`;
3532
+ }
3029
3533
  // v2.1: Правила показываются ПЕРВЫМИ
3030
3534
  output += formatRulesBlock();
3535
+ // v3.0: ДАЙДЖЕСТ ПЕРВЫМ когда есть
3536
+ if (hasDigest) {
3537
+ output += `<claude-instruction>Дайджест содержит полный контекст проекта. Начинай работу сразу без дополнительных вопросов.</claude-instruction>\n\n`;
3538
+ const digestPath = getDigestPath();
3539
+ const digestFiles = ["00-overview.md", "01-timeline.md", "02-architecture.md", "03-decisions.md", "04-solutions.md"];
3540
+ for (const fileName of digestFiles) {
3541
+ const filePath = path.join(digestPath, fileName);
3542
+ if (fs.existsSync(filePath)) {
3543
+ const content = fs.readFileSync(filePath, "utf-8");
3544
+ // Лимит ~4KB на каждый файл дайджеста
3545
+ output += truncate(content, 4 * 1024) + "\n\n---\n\n";
3546
+ }
3547
+ }
3548
+ }
3549
+ else {
3550
+ // v3.0: Баннер для legacy формата
3551
+ output += `# ⚡ Обнаружен legacy формат — дайджест не найден!\n\n`;
3552
+ output += `> Вы используете старый формат без Project Digest.\n`;
3553
+ output += `> Создайте дайджест для повышения эффективности в **3-5 раз**:\n`;
3554
+ output += `> - Мгновенное восстановление контекста проекта\n`;
3555
+ output += `> - Полная история вместо последних 5 контекстов\n`;
3556
+ output += `> - Автообновление при каждом сохранении\n`;
3557
+ output += `>\n`;
3558
+ output += `> **Команда:** скажите "миграция дайджеста" или "digest migrate"\n\n`;
3559
+ }
3031
3560
  // v2.0: Memory System summary
3032
3561
  if (hasMemory) {
3033
3562
  output += `🧠 **Memory v2.0:** `;
@@ -3049,11 +3578,12 @@ MD_HISTORY/
3049
3578
  else {
3050
3579
  output += `💡 *Memory не инициализирована. Выполни \`context_migrate\` для индексации.*\n\n`;
3051
3580
  }
3052
- // v1.4.10: HISTORY.md с лимитом 10KB
3581
+ // v3.0: HISTORY.md урезанный когда есть дайджест
3582
+ const historyLimit = hasDigest ? 2 * 1024 : 10 * 1024;
3053
3583
  if (history) {
3054
3584
  output += `---\n\n`;
3055
- output += `# 📜 HISTORY.md — Полная история\n\n`;
3056
- output += truncate(history, 10 * 1024);
3585
+ output += `# 📜 HISTORY.md — ${hasDigest ? "Последние записи" : "Полная история"}\n\n`;
3586
+ output += truncate(history, historyLimit);
3057
3587
  output += `\n\n---\n\n`;
3058
3588
  }
3059
3589
  else {
@@ -3061,24 +3591,26 @@ MD_HISTORY/
3061
3591
  output += `💡 *HISTORY.md не найден. Выполни \`context_to_history\` для создания индекса.*\n\n`;
3062
3592
  output += `---\n\n`;
3063
3593
  }
3064
- // v1.4.10: Загружаем контексты с лимитом на каждый
3065
- let contextBudget = strategy.maxPerContext;
3594
+ // v3.0: Загружаем контексты с лимитом на каждый
3595
+ const maxPerCtx = hasDigest
3596
+ ? Math.floor(9 * 1024 / contextCount) // ~3KB per context when digest present
3597
+ : strategy.maxPerContext;
3066
3598
  for (const ctx of contexts) {
3067
3599
  const result = loadContext(ctx.name);
3068
3600
  if (!result.success)
3069
3601
  continue;
3070
3602
  let ctxOutput = `# 📁 #${String(ctx.number).padStart(3, "0")}: ${ctx.title}\n`;
3071
3603
  ctxOutput += `📅 ${ctx.date} | ${ctx.status} | ${ctx.filesCount} файлов\n\n`;
3072
- if (strategy.mode === "readme_only") {
3073
- // Только README (обрезанный)
3604
+ if (hasDigest || strategy.mode === "readme_only") {
3605
+ // Дайджест есть или мало места — только README
3074
3606
  if (result.files["README.md"]) {
3075
- ctxOutput += truncate(result.files["README.md"], contextBudget - 200);
3607
+ ctxOutput += truncate(result.files["README.md"], maxPerCtx - 200);
3076
3608
  }
3077
3609
  }
3078
3610
  else if (strategy.mode === "summary") {
3079
3611
  // README + summary + changes (с лимитами)
3080
3612
  const summaryFiles = ["README.md", "04-summary.md", "03-changes.md"];
3081
- const perFile = Math.floor((contextBudget - 200) / 3);
3613
+ const perFile = Math.floor((maxPerCtx - 200) / 3);
3082
3614
  for (const fileName of summaryFiles) {
3083
3615
  if (result.files[fileName]) {
3084
3616
  ctxOutput += `### ${fileName}\n${truncate(result.files[fileName], perFile)}\n\n`;
@@ -3088,7 +3620,7 @@ MD_HISTORY/
3088
3620
  else {
3089
3621
  // Полная загрузка (с лимитами)
3090
3622
  const fileOrder = ["README.md", "04-summary.md", "03-changes.md", "01-task-overview.md", "02-analysis.md"];
3091
- const perFile = Math.floor((contextBudget - 200) / fileOrder.length);
3623
+ const perFile = Math.floor((maxPerCtx - 200) / fileOrder.length);
3092
3624
  for (const fileName of fileOrder) {
3093
3625
  if (result.files[fileName]) {
3094
3626
  ctxOutput += `### ${fileName}\n${truncate(result.files[fileName], perFile)}\n\n`;
@@ -3097,7 +3629,7 @@ MD_HISTORY/
3097
3629
  }
3098
3630
  output += ctxOutput + `\n---\n\n`;
3099
3631
  }
3100
- output += `\n💡 Загружено ${contexts.length} контекстов. Продолжаем работу?`;
3632
+ output += `\n💡 Загружено ${contexts.length} контекстов${hasDigest ? " + дайджест" : ""}. Продолжаем работу?`;
3101
3633
  return { content: [{ type: "text", text: output }] };
3102
3634
  }
3103
3635
  // ==================== CONTEXT_FULL ====================
@@ -3699,6 +4231,238 @@ MD_HISTORY/
3699
4231
  }
3700
4232
  }
3701
4233
  }
4234
+ // ========== PROJECT DIGEST v3.0 HANDLERS ==========
4235
+ case "context_digest": {
4236
+ const digestPath = getDigestPath();
4237
+ if (!fs.existsSync(path.join(digestPath, "digest.json"))) {
4238
+ return {
4239
+ content: [{
4240
+ type: "text",
4241
+ text: `📦 **Дайджест не найден**\n\nПапка \`_DIGEST/\` пуста или не существует.\n\n**Создайте дайджест:**\n- Скажите "миграция дайджеста" или "digest migrate"\n- Или просто сохраните контекст — дайджест создастся автоматически`,
4242
+ }],
4243
+ };
4244
+ }
4245
+ let output = `# 📦 Project Digest\n\n`;
4246
+ const digestFiles = ["00-overview.md", "01-timeline.md", "02-architecture.md", "03-decisions.md", "04-solutions.md"];
4247
+ for (const fileName of digestFiles) {
4248
+ const filePath = path.join(digestPath, fileName);
4249
+ if (fs.existsSync(filePath)) {
4250
+ const content = fs.readFileSync(filePath, "utf-8");
4251
+ output += content + "\n\n---\n\n";
4252
+ }
4253
+ }
4254
+ return { content: [{ type: "text", text: output }] };
4255
+ }
4256
+ case "context_digest_migrate": {
4257
+ const allContexts = listContexts();
4258
+ const memoryIndex = loadMemoryIndex();
4259
+ const solutionsIdx = loadSolutions();
4260
+ const keywordsIdx = loadKeywords();
4261
+ const projectName = detectProjectFromWorkDir() || "unknown";
4262
+ const digest = createEmptyDigest(projectName);
4263
+ // Сканируем все контексты
4264
+ for (const ctx of allContexts) {
4265
+ const contextId = String(ctx.number).padStart(3, "0");
4266
+ // Читаем README
4267
+ let summary = "";
4268
+ const readmePath = path.join(ctx.path, "README.md");
4269
+ if (fs.existsSync(readmePath)) {
4270
+ const readme = fs.readFileSync(readmePath, "utf-8");
4271
+ const descMatch = readme.match(/## Краткое описание\n\n([^\n#]+)/);
4272
+ summary = descMatch ? descMatch[1].trim() : readme.split("\n").slice(0, 3).join(" ").slice(0, 200);
4273
+ }
4274
+ // Читаем 03-changes.md для файлов
4275
+ let keyChanges = [];
4276
+ const changesPath = path.join(ctx.path, "03-changes.md");
4277
+ if (fs.existsSync(changesPath)) {
4278
+ const changesContent = fs.readFileSync(changesPath, "utf-8");
4279
+ keyChanges = extractKeyChangesFromText(changesContent);
4280
+ const files = extractFilesFromChanges(changesContent);
4281
+ for (const f of files) {
4282
+ if (!digest.currentState.keyFiles.includes(f)) {
4283
+ digest.currentState.keyFiles.push(f);
4284
+ }
4285
+ }
4286
+ }
4287
+ // Статистика
4288
+ digest.statistics.totalContexts++;
4289
+ if (ctx.status.includes("✅"))
4290
+ digest.statistics.completed++;
4291
+ else if (ctx.status.includes("⏳"))
4292
+ digest.statistics.inProgress++;
4293
+ else if (ctx.status.includes("❌"))
4294
+ digest.statistics.blocked++;
4295
+ if (!digest.statistics.firstDate || ctx.date < digest.statistics.firstDate) {
4296
+ digest.statistics.firstDate = ctx.date;
4297
+ }
4298
+ if (!digest.statistics.lastDate || ctx.date > digest.statistics.lastDate) {
4299
+ digest.statistics.lastDate = ctx.date;
4300
+ }
4301
+ // recentTasks (последние 30)
4302
+ digest.recentTasks.push({
4303
+ contextId,
4304
+ date: ctx.date,
4305
+ name: ctx.title,
4306
+ status: ctx.status,
4307
+ summary: summary.slice(0, 200),
4308
+ keyChanges,
4309
+ });
4310
+ }
4311
+ // Сортируем по номеру (новые первые) и обрезаем
4312
+ digest.recentTasks.sort((a, b) => parseInt(b.contextId) - parseInt(a.contextId));
4313
+ digest.recentTasks = digest.recentTasks.slice(0, 30);
4314
+ // Лимит keyFiles
4315
+ digest.currentState.keyFiles = digest.currentState.keyFiles.slice(0, 30);
4316
+ // techStack из keywords
4317
+ const techKeywords = ["typescript", "javascript", "python", "swift", "kotlin", "react", "vue", "node", "mcp", "trello", "obsidian"];
4318
+ for (const [kw] of Object.entries(keywordsIdx.keywords)) {
4319
+ if (techKeywords.includes(kw.toLowerCase())) {
4320
+ digest.currentState.techStack.push(kw);
4321
+ }
4322
+ }
4323
+ // solutions из solutions.json
4324
+ for (const sol of solutionsIdx.solutions.slice(0, 20)) {
4325
+ digest.solutions.push({
4326
+ problem: sol.problem,
4327
+ solution: sol.solution.join("; "),
4328
+ contextId: sol.sourceContexts[0] || "?",
4329
+ });
4330
+ }
4331
+ // Фазы — группировка по месяцам
4332
+ const monthMap = new Map();
4333
+ for (const task of digest.recentTasks) {
4334
+ const month = task.date.slice(0, 7); // YYYY-MM
4335
+ if (!monthMap.has(month)) {
4336
+ monthMap.set(month, { count: 0, tasks: [] });
4337
+ }
4338
+ const m = monthMap.get(month);
4339
+ m.count++;
4340
+ if (m.tasks.length < 3)
4341
+ m.tasks.push(task.name);
4342
+ }
4343
+ for (const [month, data] of monthMap) {
4344
+ digest.phases.push({
4345
+ name: month,
4346
+ dateRange: month,
4347
+ description: `${data.count} задач: ${data.tasks.join(", ")}${data.count > 3 ? " и др." : ""}`,
4348
+ });
4349
+ }
4350
+ // activeEpics из memory
4351
+ const epics = loadEpics();
4352
+ for (const [name, epic] of Object.entries(epics.epics)) {
4353
+ if (epic.status === "in_progress") {
4354
+ digest.currentState.activeEpics.push(name);
4355
+ }
4356
+ }
4357
+ // Сохраняем
4358
+ saveDigest(digest);
4359
+ let output = `✅ **Дайджест создан!**\n\n`;
4360
+ output += `📦 **Проект:** ${projectName}\n`;
4361
+ output += `📊 **Контекстов обработано:** ${allContexts.length}\n`;
4362
+ output += `📁 **Путь:** \`${getDigestPath()}\`\n\n`;
4363
+ output += `**Создано файлов:**\n`;
4364
+ output += `- 00-overview.md\n- 01-timeline.md\n- 02-architecture.md\n- 03-decisions.md\n- 04-solutions.md\n- digest.json\n\n`;
4365
+ output += `---\n\n`;
4366
+ output += `💡 **Следующий шаг:** скажите "обнови дайджест" (refresh) — Claude проанализирует и добавит качественное описание проекта, фазы эволюции и т.д.\n\n`;
4367
+ output += `📦 Дайджест будет автоматически обновляться при каждом \`context_save\`.`;
4368
+ return { content: [{ type: "text", text: output }] };
4369
+ }
4370
+ case "context_digest_refresh": {
4371
+ const digest = loadDigest();
4372
+ if (!digest) {
4373
+ return {
4374
+ content: [{
4375
+ type: "text",
4376
+ text: `📦 **Дайджест не найден**\n\nСначала создайте дайджест: "миграция дайджеста" или "digest migrate"`,
4377
+ }],
4378
+ };
4379
+ }
4380
+ // Собираем README последних 10 контекстов
4381
+ const contexts = listContexts().slice(0, 10);
4382
+ let readmes = "";
4383
+ for (const ctx of contexts) {
4384
+ const readmePath = path.join(ctx.path, "README.md");
4385
+ if (fs.existsSync(readmePath)) {
4386
+ readmes += `\n---\n### #${String(ctx.number).padStart(3, "0")} | ${ctx.date} | ${ctx.title} | ${ctx.status}\n\n`;
4387
+ const content = fs.readFileSync(readmePath, "utf-8");
4388
+ readmes += content.slice(0, 1500) + "\n";
4389
+ }
4390
+ }
4391
+ let output = `# 🔄 Refresh дайджеста\n\n`;
4392
+ output += `## Текущий digest.json:\n\n`;
4393
+ output += `\`\`\`json\n${JSON.stringify(digest, null, 2)}\n\`\`\`\n\n`;
4394
+ output += `---\n\n`;
4395
+ output += `## README последних 10 контекстов:\n\n`;
4396
+ output += readmes;
4397
+ output += `\n\n---\n\n`;
4398
+ output += `<claude-instruction>\n`;
4399
+ output += `Проанализируй digest.json и README контекстов выше.\n`;
4400
+ output += `Затем вызови context_digest_write с параметрами:\n`;
4401
+ output += `- description: описание проекта (3-5 предложений)\n`;
4402
+ output += `- tech_stack: стек через запятую\n`;
4403
+ output += `- key_files: ключевые файлы через запятую\n`;
4404
+ output += `- phases: фазы эволюции (Название | даты | описание на каждой строке)\n`;
4405
+ output += `- key_decisions: ключевые решения\n`;
4406
+ output += `- known_issues: текущие проблемы\n`;
4407
+ output += `- active_epics: активные эпики\n`;
4408
+ output += `</claude-instruction>`;
4409
+ return { content: [{ type: "text", text: output }] };
4410
+ }
4411
+ case "context_digest_write": {
4412
+ let digest = loadDigest();
4413
+ if (!digest) {
4414
+ const projectName = detectProjectFromWorkDir() || "unknown";
4415
+ digest = createEmptyDigest(projectName);
4416
+ }
4417
+ // Обновляем currentState
4418
+ if (args?.description) {
4419
+ digest.currentState.description = args.description;
4420
+ }
4421
+ if (args?.tech_stack) {
4422
+ digest.currentState.techStack = args.tech_stack.split(",").map(s => s.trim()).filter(Boolean);
4423
+ }
4424
+ if (args?.key_files) {
4425
+ digest.currentState.keyFiles = args.key_files.split(",").map(s => s.trim()).filter(Boolean);
4426
+ }
4427
+ if (args?.active_epics) {
4428
+ digest.currentState.activeEpics = args.active_epics.split(",").map(s => s.trim()).filter(Boolean);
4429
+ }
4430
+ // Обновляем known_issues
4431
+ if (args?.known_issues) {
4432
+ digest.knownIssues = args.known_issues.split("\n").map(s => s.trim()).filter(Boolean).slice(0, 10);
4433
+ }
4434
+ // Обновляем phases
4435
+ if (args?.phases) {
4436
+ const phaseLines = args.phases.split("\n").filter(l => l.trim());
4437
+ digest.phases = phaseLines.map(line => {
4438
+ const parts = line.split("|").map(p => p.trim());
4439
+ return {
4440
+ name: parts[0] || "Phase",
4441
+ dateRange: parts[1] || "",
4442
+ description: parts[2] || "",
4443
+ };
4444
+ });
4445
+ }
4446
+ // Обновляем key_decisions
4447
+ if (args?.key_decisions) {
4448
+ const decLines = args.key_decisions.split("\n").filter(l => l.trim());
4449
+ const today = new Date().toISOString().split("T")[0];
4450
+ digest.keyDecisions = decLines.map(line => ({
4451
+ decision: line.trim(),
4452
+ date: today,
4453
+ contextId: "refresh",
4454
+ })).slice(0, 20);
4455
+ }
4456
+ digest.lastUpdated = new Date().toISOString().split("T")[0];
4457
+ // Сохраняем и перерендериваем
4458
+ saveDigest(digest);
4459
+ return {
4460
+ content: [{
4461
+ type: "text",
4462
+ text: `✅ **Дайджест обновлён!**\n\n📦 Проект: **${digest.project}**\n📝 Описание: ${(digest.currentState.description || "").slice(0, 100)}...\n🔧 Стек: ${digest.currentState.techStack.join(", ") || "-"}\n📁 Файлов: ${digest.currentState.keyFiles.length}\n\nВсе 5 MD файлов перерендерены. Проверьте: "дайджест" или "digest"`,
4463
+ }],
4464
+ };
4465
+ }
3702
4466
  // ========== ТЕСТОВЫЕ HANDLERS SMART CONTEXT ==========
3703
4467
  case "context_smart": {
3704
4468
  const history = readHistory();