dedsession 2.3.1 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +790 -22
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import * as path from "path";
|
|
|
8
8
|
// КОНФИГУРАЦИЯ
|
|
9
9
|
// ============================================================================
|
|
10
10
|
const CONFIG = {
|
|
11
|
-
VERSION: "
|
|
11
|
+
VERSION: "3.0.1",
|
|
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,41 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2456
2885
|
output += lastContextMatch[0] + "\n---\n\n";
|
|
2457
2886
|
}
|
|
2458
2887
|
}
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
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
|
+
// v3.0: Предложить создать дайджест если контексты есть
|
|
2918
|
+
if (stats.total > 0) {
|
|
2919
|
+
output += `<claude-instruction>Project Digest НЕ найден. Предложи пользователю создать дайджест командой "миграция дайджеста" — это даст полную историю проекта при каждой загрузке контекста.</claude-instruction>\n`;
|
|
2920
|
+
output += `> 💡 **Dedsession v3.0:** Создайте Project Digest для мгновенного восстановления контекста: "миграция дайджеста"\n\n`;
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2465
2923
|
// Архитектура проекта если есть
|
|
2466
2924
|
if (architecture.length > 0) {
|
|
2467
2925
|
output += `## 🏗️ Архитектура проекта (из MD/)\n\n`;
|
|
@@ -2581,6 +3039,23 @@ ${args?.changes?.split("\n").map(l => l.trim() ? `- ${l}` : "").join("\n") || "-
|
|
|
2581
3039
|
const extracted = extractSolutionsFromText(solutionsContent, contextId);
|
|
2582
3040
|
solutionsAdded = addSolutionsToIndex(extracted, contextId);
|
|
2583
3041
|
}
|
|
3042
|
+
// v3.0: Обновляем дайджест
|
|
3043
|
+
const digestProject = detectProjectFromWorkDir() || memoryInfo.project || "unknown";
|
|
3044
|
+
const digestContextId = String(contextNumber).padStart(3, "0");
|
|
3045
|
+
const digestUpdated = updateDigestOnSave({
|
|
3046
|
+
project: digestProject,
|
|
3047
|
+
contextId: digestContextId,
|
|
3048
|
+
taskName,
|
|
3049
|
+
status,
|
|
3050
|
+
changes: args?.changes,
|
|
3051
|
+
summary: args?.summary,
|
|
3052
|
+
historySummary: args?.history_summary,
|
|
3053
|
+
problemsSolutions: args?.problems_solutions,
|
|
3054
|
+
decisions: args?.decisions,
|
|
3055
|
+
architecture: args?.architecture,
|
|
3056
|
+
date: new Date().toISOString().split("T")[0],
|
|
3057
|
+
digestUpdate: args?.digest_update,
|
|
3058
|
+
});
|
|
2584
3059
|
let output = `✅ **Контекст сохранён**\n\n`;
|
|
2585
3060
|
output += `📁 **Папка:** \`${result.name}\`\n`;
|
|
2586
3061
|
output += `📄 **Файлов:** ${result.filesCount}\n`;
|
|
@@ -2596,6 +3071,10 @@ ${args?.changes?.split("\n").map(l => l.trim() ? `- ${l}` : "").join("\n") || "-
|
|
|
2596
3071
|
output += `- 💡 Решений добавлено: **${solutionsAdded}**\n`;
|
|
2597
3072
|
}
|
|
2598
3073
|
}
|
|
3074
|
+
// v3.0: Дайджест статус
|
|
3075
|
+
if (digestUpdated) {
|
|
3076
|
+
output += `\n📦 **Дайджест обновлён** (${digestProject})\n`;
|
|
3077
|
+
}
|
|
2599
3078
|
if (state.loadedContext && state.loadedContext !== result.name) {
|
|
2600
3079
|
output += `\n🔗 **Parent:** ${state.loadedContext}\n`;
|
|
2601
3080
|
}
|
|
@@ -2728,6 +3207,22 @@ ${state.loadedContext ? `**Parent:** ${state.loadedContext}` : "Нет parent к
|
|
|
2728
3207
|
const contextId = String(contextNumber).padStart(3, "0");
|
|
2729
3208
|
const extracted = extractSolutionsFromText(solutionsContent, contextId);
|
|
2730
3209
|
solutionsAdded = addSolutionsToIndex(extracted, contextId);
|
|
3210
|
+
// v3.0: Обновляем дайджест (detailed передаёт больше данных)
|
|
3211
|
+
const digestProjectD = detectProjectFromWorkDir() || memoryInfo.project || "unknown";
|
|
3212
|
+
const digestUpdatedD = updateDigestOnSave({
|
|
3213
|
+
project: digestProjectD,
|
|
3214
|
+
contextId,
|
|
3215
|
+
taskName,
|
|
3216
|
+
status,
|
|
3217
|
+
changes: args?.changes,
|
|
3218
|
+
summary: args?.summary,
|
|
3219
|
+
historySummary: args?.history_summary,
|
|
3220
|
+
problemsSolutions: args?.problems_solutions,
|
|
3221
|
+
decisions: args?.decisions,
|
|
3222
|
+
architecture: args?.architecture,
|
|
3223
|
+
date: new Date().toISOString().split("T")[0],
|
|
3224
|
+
digestUpdate: args?.digest_update,
|
|
3225
|
+
});
|
|
2731
3226
|
let output = `✅ **Контекст сохранён ПОДРОБНО**\n\n`;
|
|
2732
3227
|
output += `📁 **Папка:** \`${result.name}\`\n`;
|
|
2733
3228
|
output += `📄 **Файлов:** 15\n`;
|
|
@@ -2742,6 +3237,10 @@ ${state.loadedContext ? `**Parent:** ${state.loadedContext}` : "Нет parent к
|
|
|
2742
3237
|
output += `- 💡 Решений добавлено: **${solutionsAdded}**\n`;
|
|
2743
3238
|
}
|
|
2744
3239
|
}
|
|
3240
|
+
// v3.0: Дайджест статус
|
|
3241
|
+
if (digestUpdatedD) {
|
|
3242
|
+
output += `\n📦 **Дайджест обновлён** (${digestProjectD})\n`;
|
|
3243
|
+
}
|
|
2745
3244
|
if (state.loadedContext && state.loadedContext !== result.name) {
|
|
2746
3245
|
output += `\n🔗 **Parent:** ${state.loadedContext}\n`;
|
|
2747
3246
|
}
|
|
@@ -2765,7 +3264,7 @@ ${state.loadedContext ? `**Parent:** ${state.loadedContext}` : "Нет parent к
|
|
|
2765
3264
|
.map(l => l.startsWith("-") ? l : `- ${l}`)
|
|
2766
3265
|
.join("\n");
|
|
2767
3266
|
const commitTitle = `📝 Context: ${contextShortName}`;
|
|
2768
|
-
const commitBody = `Статус: ${status} | Файлов:
|
|
3267
|
+
const commitBody = `Статус: ${status} | Файлов: 15\n\nЧто сделано:\n${changeLines}`;
|
|
2769
3268
|
output += `\n\n---\n\n`;
|
|
2770
3269
|
output += `📋 **Для коммита:**\n\n`;
|
|
2771
3270
|
output += `**Title:**\n\`\`\`\n${commitTitle}\n\`\`\`\n\n`;
|
|
@@ -3006,15 +3505,20 @@ MD_HISTORY/
|
|
|
3006
3505
|
if (allContexts.length === 0) {
|
|
3007
3506
|
return { content: [{ type: "text", text: "📭 Нет сохранённых контекстов" }] };
|
|
3008
3507
|
}
|
|
3508
|
+
// v3.0: Проверяем наличие дайджеста
|
|
3509
|
+
const digest = loadDigest();
|
|
3510
|
+
const hasDigest = digest !== null;
|
|
3009
3511
|
// v2.0: Получаем Memory stats
|
|
3010
3512
|
const memoryStats = getMemoryStats();
|
|
3011
3513
|
const hasMemory = memoryStats.totalContexts > 0;
|
|
3012
3514
|
// v1.4.10: Читаем HISTORY.md и учитываем его размер
|
|
3013
3515
|
const history = readHistory();
|
|
3014
3516
|
const historySize = history ? history.length : 0;
|
|
3517
|
+
// v3.0: Когда есть дайджест — другой режим работы
|
|
3518
|
+
const contextCount = hasDigest ? Math.min(3, allContexts.length) : Math.min(5, allContexts.length);
|
|
3015
3519
|
// Определяем стратегию с учётом размера HISTORY.md
|
|
3016
3520
|
const strategy = determineQuickLoadStrategy(allContexts, historySize);
|
|
3017
|
-
const contexts = allContexts.slice(0,
|
|
3521
|
+
const contexts = allContexts.slice(0, contextCount);
|
|
3018
3522
|
// Функция обрезки текста с ... в конце
|
|
3019
3523
|
const truncate = (text, maxLen) => {
|
|
3020
3524
|
if (text.length <= maxLen)
|
|
@@ -3024,10 +3528,39 @@ MD_HISTORY/
|
|
|
3024
3528
|
let output = `# ⚡ Краткий контекст — Dedsession v${CONFIG.VERSION}\n\n`;
|
|
3025
3529
|
output += `📁 **Рабочая директория:** \`${workDir}\`\n`;
|
|
3026
3530
|
output += `📊 **Статистика:** Всего ${stats.total} | ✅ ${stats.completed} | ⏳ ${stats.inProgress}\n`;
|
|
3027
|
-
|
|
3028
|
-
|
|
3531
|
+
if (hasDigest) {
|
|
3532
|
+
output += `📦 **Режим:** Дайджест + ${contextCount} контекстов (v3.0)\n\n`;
|
|
3533
|
+
}
|
|
3534
|
+
else {
|
|
3535
|
+
output += `🎯 **Режим:** ${strategy.reason}\n`;
|
|
3536
|
+
output += `📦 **Загружено контекстов:** ${contexts.length}\n\n`;
|
|
3537
|
+
}
|
|
3029
3538
|
// v2.1: Правила показываются ПЕРВЫМИ
|
|
3030
3539
|
output += formatRulesBlock();
|
|
3540
|
+
// v3.0: ДАЙДЖЕСТ ПЕРВЫМ когда есть
|
|
3541
|
+
if (hasDigest) {
|
|
3542
|
+
output += `<claude-instruction>Дайджест содержит полный контекст проекта. Начинай работу сразу без дополнительных вопросов.</claude-instruction>\n\n`;
|
|
3543
|
+
const digestPath = getDigestPath();
|
|
3544
|
+
const digestFiles = ["00-overview.md", "01-timeline.md", "02-architecture.md", "03-decisions.md", "04-solutions.md"];
|
|
3545
|
+
for (const fileName of digestFiles) {
|
|
3546
|
+
const filePath = path.join(digestPath, fileName);
|
|
3547
|
+
if (fs.existsSync(filePath)) {
|
|
3548
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
3549
|
+
// Лимит ~4KB на каждый файл дайджеста
|
|
3550
|
+
output += truncate(content, 4 * 1024) + "\n\n---\n\n";
|
|
3551
|
+
}
|
|
3552
|
+
}
|
|
3553
|
+
}
|
|
3554
|
+
else {
|
|
3555
|
+
// v3.0: Баннер для legacy формата + ИНСТРУКЦИЯ для Claude
|
|
3556
|
+
output += `<claude-instruction>ВАЖНО: Project Digest НЕ найден для этого проекта! Ты ОБЯЗАН в своём ответе пользователю ПЕРВЫМ ДЕЛОМ предложить создать дайджест командой "миграция дайджеста" или "digest migrate". Объясни что это даст: полная история проекта вместо последних 5 контекстов, автообновление при каждом сохранении, мгновенный старт новых сессий. Это ключевое улучшение v3.0!</claude-instruction>\n\n`;
|
|
3557
|
+
output += `# ⚡ Project Digest не найден!\n\n`;
|
|
3558
|
+
output += `> **Dedsession v3.0** использует Project Digest — полную память проекта.\n`;
|
|
3559
|
+
output += `> Сейчас загружается legacy формат (только 5 последних контекстов = 13% истории).\n`;
|
|
3560
|
+
output += `>\n`;
|
|
3561
|
+
output += `> **Создайте дайджест:** скажите "миграция дайджеста" или "digest migrate"\n`;
|
|
3562
|
+
output += `> После этого каждый context_save будет автоматически обогащать дайджест.\n\n`;
|
|
3563
|
+
}
|
|
3031
3564
|
// v2.0: Memory System summary
|
|
3032
3565
|
if (hasMemory) {
|
|
3033
3566
|
output += `🧠 **Memory v2.0:** `;
|
|
@@ -3049,11 +3582,12 @@ MD_HISTORY/
|
|
|
3049
3582
|
else {
|
|
3050
3583
|
output += `💡 *Memory не инициализирована. Выполни \`context_migrate\` для индексации.*\n\n`;
|
|
3051
3584
|
}
|
|
3052
|
-
//
|
|
3585
|
+
// v3.0: HISTORY.md — урезанный когда есть дайджест
|
|
3586
|
+
const historyLimit = hasDigest ? 2 * 1024 : 10 * 1024;
|
|
3053
3587
|
if (history) {
|
|
3054
3588
|
output += `---\n\n`;
|
|
3055
|
-
output += `# 📜 HISTORY.md — Полная
|
|
3056
|
-
output += truncate(history,
|
|
3589
|
+
output += `# 📜 HISTORY.md — ${hasDigest ? "Последние записи" : "Полная история"}\n\n`;
|
|
3590
|
+
output += truncate(history, historyLimit);
|
|
3057
3591
|
output += `\n\n---\n\n`;
|
|
3058
3592
|
}
|
|
3059
3593
|
else {
|
|
@@ -3061,24 +3595,26 @@ MD_HISTORY/
|
|
|
3061
3595
|
output += `💡 *HISTORY.md не найден. Выполни \`context_to_history\` для создания индекса.*\n\n`;
|
|
3062
3596
|
output += `---\n\n`;
|
|
3063
3597
|
}
|
|
3064
|
-
//
|
|
3065
|
-
|
|
3598
|
+
// v3.0: Загружаем контексты с лимитом на каждый
|
|
3599
|
+
const maxPerCtx = hasDigest
|
|
3600
|
+
? Math.floor(9 * 1024 / contextCount) // ~3KB per context when digest present
|
|
3601
|
+
: strategy.maxPerContext;
|
|
3066
3602
|
for (const ctx of contexts) {
|
|
3067
3603
|
const result = loadContext(ctx.name);
|
|
3068
3604
|
if (!result.success)
|
|
3069
3605
|
continue;
|
|
3070
3606
|
let ctxOutput = `# 📁 #${String(ctx.number).padStart(3, "0")}: ${ctx.title}\n`;
|
|
3071
3607
|
ctxOutput += `📅 ${ctx.date} | ${ctx.status} | ${ctx.filesCount} файлов\n\n`;
|
|
3072
|
-
if (strategy.mode === "readme_only") {
|
|
3073
|
-
//
|
|
3608
|
+
if (hasDigest || strategy.mode === "readme_only") {
|
|
3609
|
+
// Дайджест есть или мало места — только README
|
|
3074
3610
|
if (result.files["README.md"]) {
|
|
3075
|
-
ctxOutput += truncate(result.files["README.md"],
|
|
3611
|
+
ctxOutput += truncate(result.files["README.md"], maxPerCtx - 200);
|
|
3076
3612
|
}
|
|
3077
3613
|
}
|
|
3078
3614
|
else if (strategy.mode === "summary") {
|
|
3079
3615
|
// README + summary + changes (с лимитами)
|
|
3080
3616
|
const summaryFiles = ["README.md", "04-summary.md", "03-changes.md"];
|
|
3081
|
-
const perFile = Math.floor((
|
|
3617
|
+
const perFile = Math.floor((maxPerCtx - 200) / 3);
|
|
3082
3618
|
for (const fileName of summaryFiles) {
|
|
3083
3619
|
if (result.files[fileName]) {
|
|
3084
3620
|
ctxOutput += `### ${fileName}\n${truncate(result.files[fileName], perFile)}\n\n`;
|
|
@@ -3088,7 +3624,7 @@ MD_HISTORY/
|
|
|
3088
3624
|
else {
|
|
3089
3625
|
// Полная загрузка (с лимитами)
|
|
3090
3626
|
const fileOrder = ["README.md", "04-summary.md", "03-changes.md", "01-task-overview.md", "02-analysis.md"];
|
|
3091
|
-
const perFile = Math.floor((
|
|
3627
|
+
const perFile = Math.floor((maxPerCtx - 200) / fileOrder.length);
|
|
3092
3628
|
for (const fileName of fileOrder) {
|
|
3093
3629
|
if (result.files[fileName]) {
|
|
3094
3630
|
ctxOutput += `### ${fileName}\n${truncate(result.files[fileName], perFile)}\n\n`;
|
|
@@ -3097,7 +3633,7 @@ MD_HISTORY/
|
|
|
3097
3633
|
}
|
|
3098
3634
|
output += ctxOutput + `\n---\n\n`;
|
|
3099
3635
|
}
|
|
3100
|
-
output += `\n💡 Загружено ${contexts.length}
|
|
3636
|
+
output += `\n💡 Загружено ${contexts.length} контекстов${hasDigest ? " + дайджест" : ""}. Продолжаем работу?`;
|
|
3101
3637
|
return { content: [{ type: "text", text: output }] };
|
|
3102
3638
|
}
|
|
3103
3639
|
// ==================== CONTEXT_FULL ====================
|
|
@@ -3699,6 +4235,238 @@ MD_HISTORY/
|
|
|
3699
4235
|
}
|
|
3700
4236
|
}
|
|
3701
4237
|
}
|
|
4238
|
+
// ========== PROJECT DIGEST v3.0 HANDLERS ==========
|
|
4239
|
+
case "context_digest": {
|
|
4240
|
+
const digestPath = getDigestPath();
|
|
4241
|
+
if (!fs.existsSync(path.join(digestPath, "digest.json"))) {
|
|
4242
|
+
return {
|
|
4243
|
+
content: [{
|
|
4244
|
+
type: "text",
|
|
4245
|
+
text: `📦 **Дайджест не найден**\n\nПапка \`_DIGEST/\` пуста или не существует.\n\n**Создайте дайджест:**\n- Скажите "миграция дайджеста" или "digest migrate"\n- Или просто сохраните контекст — дайджест создастся автоматически`,
|
|
4246
|
+
}],
|
|
4247
|
+
};
|
|
4248
|
+
}
|
|
4249
|
+
let output = `# 📦 Project Digest\n\n`;
|
|
4250
|
+
const digestFiles = ["00-overview.md", "01-timeline.md", "02-architecture.md", "03-decisions.md", "04-solutions.md"];
|
|
4251
|
+
for (const fileName of digestFiles) {
|
|
4252
|
+
const filePath = path.join(digestPath, fileName);
|
|
4253
|
+
if (fs.existsSync(filePath)) {
|
|
4254
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
4255
|
+
output += content + "\n\n---\n\n";
|
|
4256
|
+
}
|
|
4257
|
+
}
|
|
4258
|
+
return { content: [{ type: "text", text: output }] };
|
|
4259
|
+
}
|
|
4260
|
+
case "context_digest_migrate": {
|
|
4261
|
+
const allContexts = listContexts();
|
|
4262
|
+
const memoryIndex = loadMemoryIndex();
|
|
4263
|
+
const solutionsIdx = loadSolutions();
|
|
4264
|
+
const keywordsIdx = loadKeywords();
|
|
4265
|
+
const projectName = detectProjectFromWorkDir() || "unknown";
|
|
4266
|
+
const digest = createEmptyDigest(projectName);
|
|
4267
|
+
// Сканируем все контексты
|
|
4268
|
+
for (const ctx of allContexts) {
|
|
4269
|
+
const contextId = String(ctx.number).padStart(3, "0");
|
|
4270
|
+
// Читаем README
|
|
4271
|
+
let summary = "";
|
|
4272
|
+
const readmePath = path.join(ctx.path, "README.md");
|
|
4273
|
+
if (fs.existsSync(readmePath)) {
|
|
4274
|
+
const readme = fs.readFileSync(readmePath, "utf-8");
|
|
4275
|
+
const descMatch = readme.match(/## Краткое описание\n\n([^\n#]+)/);
|
|
4276
|
+
summary = descMatch ? descMatch[1].trim() : readme.split("\n").slice(0, 3).join(" ").slice(0, 200);
|
|
4277
|
+
}
|
|
4278
|
+
// Читаем 03-changes.md для файлов
|
|
4279
|
+
let keyChanges = [];
|
|
4280
|
+
const changesPath = path.join(ctx.path, "03-changes.md");
|
|
4281
|
+
if (fs.existsSync(changesPath)) {
|
|
4282
|
+
const changesContent = fs.readFileSync(changesPath, "utf-8");
|
|
4283
|
+
keyChanges = extractKeyChangesFromText(changesContent);
|
|
4284
|
+
const files = extractFilesFromChanges(changesContent);
|
|
4285
|
+
for (const f of files) {
|
|
4286
|
+
if (!digest.currentState.keyFiles.includes(f)) {
|
|
4287
|
+
digest.currentState.keyFiles.push(f);
|
|
4288
|
+
}
|
|
4289
|
+
}
|
|
4290
|
+
}
|
|
4291
|
+
// Статистика
|
|
4292
|
+
digest.statistics.totalContexts++;
|
|
4293
|
+
if (ctx.status.includes("✅"))
|
|
4294
|
+
digest.statistics.completed++;
|
|
4295
|
+
else if (ctx.status.includes("⏳"))
|
|
4296
|
+
digest.statistics.inProgress++;
|
|
4297
|
+
else if (ctx.status.includes("❌"))
|
|
4298
|
+
digest.statistics.blocked++;
|
|
4299
|
+
if (!digest.statistics.firstDate || ctx.date < digest.statistics.firstDate) {
|
|
4300
|
+
digest.statistics.firstDate = ctx.date;
|
|
4301
|
+
}
|
|
4302
|
+
if (!digest.statistics.lastDate || ctx.date > digest.statistics.lastDate) {
|
|
4303
|
+
digest.statistics.lastDate = ctx.date;
|
|
4304
|
+
}
|
|
4305
|
+
// recentTasks (последние 30)
|
|
4306
|
+
digest.recentTasks.push({
|
|
4307
|
+
contextId,
|
|
4308
|
+
date: ctx.date,
|
|
4309
|
+
name: ctx.title,
|
|
4310
|
+
status: ctx.status,
|
|
4311
|
+
summary: summary.slice(0, 200),
|
|
4312
|
+
keyChanges,
|
|
4313
|
+
});
|
|
4314
|
+
}
|
|
4315
|
+
// Сортируем по номеру (новые первые) и обрезаем
|
|
4316
|
+
digest.recentTasks.sort((a, b) => parseInt(b.contextId) - parseInt(a.contextId));
|
|
4317
|
+
digest.recentTasks = digest.recentTasks.slice(0, 30);
|
|
4318
|
+
// Лимит keyFiles
|
|
4319
|
+
digest.currentState.keyFiles = digest.currentState.keyFiles.slice(0, 30);
|
|
4320
|
+
// techStack из keywords
|
|
4321
|
+
const techKeywords = ["typescript", "javascript", "python", "swift", "kotlin", "react", "vue", "node", "mcp", "trello", "obsidian"];
|
|
4322
|
+
for (const [kw] of Object.entries(keywordsIdx.keywords)) {
|
|
4323
|
+
if (techKeywords.includes(kw.toLowerCase())) {
|
|
4324
|
+
digest.currentState.techStack.push(kw);
|
|
4325
|
+
}
|
|
4326
|
+
}
|
|
4327
|
+
// solutions из solutions.json
|
|
4328
|
+
for (const sol of solutionsIdx.solutions.slice(0, 20)) {
|
|
4329
|
+
digest.solutions.push({
|
|
4330
|
+
problem: sol.problem,
|
|
4331
|
+
solution: sol.solution.join("; "),
|
|
4332
|
+
contextId: sol.sourceContexts[0] || "?",
|
|
4333
|
+
});
|
|
4334
|
+
}
|
|
4335
|
+
// Фазы — группировка по месяцам
|
|
4336
|
+
const monthMap = new Map();
|
|
4337
|
+
for (const task of digest.recentTasks) {
|
|
4338
|
+
const month = task.date.slice(0, 7); // YYYY-MM
|
|
4339
|
+
if (!monthMap.has(month)) {
|
|
4340
|
+
monthMap.set(month, { count: 0, tasks: [] });
|
|
4341
|
+
}
|
|
4342
|
+
const m = monthMap.get(month);
|
|
4343
|
+
m.count++;
|
|
4344
|
+
if (m.tasks.length < 3)
|
|
4345
|
+
m.tasks.push(task.name);
|
|
4346
|
+
}
|
|
4347
|
+
for (const [month, data] of monthMap) {
|
|
4348
|
+
digest.phases.push({
|
|
4349
|
+
name: month,
|
|
4350
|
+
dateRange: month,
|
|
4351
|
+
description: `${data.count} задач: ${data.tasks.join(", ")}${data.count > 3 ? " и др." : ""}`,
|
|
4352
|
+
});
|
|
4353
|
+
}
|
|
4354
|
+
// activeEpics из memory
|
|
4355
|
+
const epics = loadEpics();
|
|
4356
|
+
for (const [name, epic] of Object.entries(epics.epics)) {
|
|
4357
|
+
if (epic.status === "in_progress") {
|
|
4358
|
+
digest.currentState.activeEpics.push(name);
|
|
4359
|
+
}
|
|
4360
|
+
}
|
|
4361
|
+
// Сохраняем
|
|
4362
|
+
saveDigest(digest);
|
|
4363
|
+
let output = `✅ **Дайджест создан!**\n\n`;
|
|
4364
|
+
output += `📦 **Проект:** ${projectName}\n`;
|
|
4365
|
+
output += `📊 **Контекстов обработано:** ${allContexts.length}\n`;
|
|
4366
|
+
output += `📁 **Путь:** \`${getDigestPath()}\`\n\n`;
|
|
4367
|
+
output += `**Создано файлов:**\n`;
|
|
4368
|
+
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`;
|
|
4369
|
+
output += `---\n\n`;
|
|
4370
|
+
output += `💡 **Следующий шаг:** скажите "обнови дайджест" (refresh) — Claude проанализирует и добавит качественное описание проекта, фазы эволюции и т.д.\n\n`;
|
|
4371
|
+
output += `📦 Дайджест будет автоматически обновляться при каждом \`context_save\`.`;
|
|
4372
|
+
return { content: [{ type: "text", text: output }] };
|
|
4373
|
+
}
|
|
4374
|
+
case "context_digest_refresh": {
|
|
4375
|
+
const digest = loadDigest();
|
|
4376
|
+
if (!digest) {
|
|
4377
|
+
return {
|
|
4378
|
+
content: [{
|
|
4379
|
+
type: "text",
|
|
4380
|
+
text: `📦 **Дайджест не найден**\n\nСначала создайте дайджест: "миграция дайджеста" или "digest migrate"`,
|
|
4381
|
+
}],
|
|
4382
|
+
};
|
|
4383
|
+
}
|
|
4384
|
+
// Собираем README последних 10 контекстов
|
|
4385
|
+
const contexts = listContexts().slice(0, 10);
|
|
4386
|
+
let readmes = "";
|
|
4387
|
+
for (const ctx of contexts) {
|
|
4388
|
+
const readmePath = path.join(ctx.path, "README.md");
|
|
4389
|
+
if (fs.existsSync(readmePath)) {
|
|
4390
|
+
readmes += `\n---\n### #${String(ctx.number).padStart(3, "0")} | ${ctx.date} | ${ctx.title} | ${ctx.status}\n\n`;
|
|
4391
|
+
const content = fs.readFileSync(readmePath, "utf-8");
|
|
4392
|
+
readmes += content.slice(0, 1500) + "\n";
|
|
4393
|
+
}
|
|
4394
|
+
}
|
|
4395
|
+
let output = `# 🔄 Refresh дайджеста\n\n`;
|
|
4396
|
+
output += `## Текущий digest.json:\n\n`;
|
|
4397
|
+
output += `\`\`\`json\n${JSON.stringify(digest, null, 2)}\n\`\`\`\n\n`;
|
|
4398
|
+
output += `---\n\n`;
|
|
4399
|
+
output += `## README последних 10 контекстов:\n\n`;
|
|
4400
|
+
output += readmes;
|
|
4401
|
+
output += `\n\n---\n\n`;
|
|
4402
|
+
output += `<claude-instruction>\n`;
|
|
4403
|
+
output += `Проанализируй digest.json и README контекстов выше.\n`;
|
|
4404
|
+
output += `Затем вызови context_digest_write с параметрами:\n`;
|
|
4405
|
+
output += `- description: описание проекта (3-5 предложений)\n`;
|
|
4406
|
+
output += `- tech_stack: стек через запятую\n`;
|
|
4407
|
+
output += `- key_files: ключевые файлы через запятую\n`;
|
|
4408
|
+
output += `- phases: фазы эволюции (Название | даты | описание на каждой строке)\n`;
|
|
4409
|
+
output += `- key_decisions: ключевые решения\n`;
|
|
4410
|
+
output += `- known_issues: текущие проблемы\n`;
|
|
4411
|
+
output += `- active_epics: активные эпики\n`;
|
|
4412
|
+
output += `</claude-instruction>`;
|
|
4413
|
+
return { content: [{ type: "text", text: output }] };
|
|
4414
|
+
}
|
|
4415
|
+
case "context_digest_write": {
|
|
4416
|
+
let digest = loadDigest();
|
|
4417
|
+
if (!digest) {
|
|
4418
|
+
const projectName = detectProjectFromWorkDir() || "unknown";
|
|
4419
|
+
digest = createEmptyDigest(projectName);
|
|
4420
|
+
}
|
|
4421
|
+
// Обновляем currentState
|
|
4422
|
+
if (args?.description) {
|
|
4423
|
+
digest.currentState.description = args.description;
|
|
4424
|
+
}
|
|
4425
|
+
if (args?.tech_stack) {
|
|
4426
|
+
digest.currentState.techStack = args.tech_stack.split(",").map(s => s.trim()).filter(Boolean);
|
|
4427
|
+
}
|
|
4428
|
+
if (args?.key_files) {
|
|
4429
|
+
digest.currentState.keyFiles = args.key_files.split(",").map(s => s.trim()).filter(Boolean);
|
|
4430
|
+
}
|
|
4431
|
+
if (args?.active_epics) {
|
|
4432
|
+
digest.currentState.activeEpics = args.active_epics.split(",").map(s => s.trim()).filter(Boolean);
|
|
4433
|
+
}
|
|
4434
|
+
// Обновляем known_issues
|
|
4435
|
+
if (args?.known_issues) {
|
|
4436
|
+
digest.knownIssues = args.known_issues.split("\n").map(s => s.trim()).filter(Boolean).slice(0, 10);
|
|
4437
|
+
}
|
|
4438
|
+
// Обновляем phases
|
|
4439
|
+
if (args?.phases) {
|
|
4440
|
+
const phaseLines = args.phases.split("\n").filter(l => l.trim());
|
|
4441
|
+
digest.phases = phaseLines.map(line => {
|
|
4442
|
+
const parts = line.split("|").map(p => p.trim());
|
|
4443
|
+
return {
|
|
4444
|
+
name: parts[0] || "Phase",
|
|
4445
|
+
dateRange: parts[1] || "",
|
|
4446
|
+
description: parts[2] || "",
|
|
4447
|
+
};
|
|
4448
|
+
});
|
|
4449
|
+
}
|
|
4450
|
+
// Обновляем key_decisions
|
|
4451
|
+
if (args?.key_decisions) {
|
|
4452
|
+
const decLines = args.key_decisions.split("\n").filter(l => l.trim());
|
|
4453
|
+
const today = new Date().toISOString().split("T")[0];
|
|
4454
|
+
digest.keyDecisions = decLines.map(line => ({
|
|
4455
|
+
decision: line.trim(),
|
|
4456
|
+
date: today,
|
|
4457
|
+
contextId: "refresh",
|
|
4458
|
+
})).slice(0, 20);
|
|
4459
|
+
}
|
|
4460
|
+
digest.lastUpdated = new Date().toISOString().split("T")[0];
|
|
4461
|
+
// Сохраняем и перерендериваем
|
|
4462
|
+
saveDigest(digest);
|
|
4463
|
+
return {
|
|
4464
|
+
content: [{
|
|
4465
|
+
type: "text",
|
|
4466
|
+
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"`,
|
|
4467
|
+
}],
|
|
4468
|
+
};
|
|
4469
|
+
}
|
|
3702
4470
|
// ========== ТЕСТОВЫЕ HANDLERS SMART CONTEXT ==========
|
|
3703
4471
|
case "context_smart": {
|
|
3704
4472
|
const history = readHistory();
|