dedsession 2.1.0 → 2.3.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.1.0",
11
+ VERSION: "2.3.0",
12
12
  };
13
13
  const state = {
14
14
  loadedContext: null,
@@ -435,6 +435,13 @@ function listContexts() {
435
435
  return [];
436
436
  }
437
437
  }
438
+ // Находит путь к контексту по номеру (для context_smart_focus)
439
+ function findContextByNumber(number) {
440
+ const contexts = listContexts();
441
+ const numInt = parseInt(number, 10);
442
+ const found = contexts.find(c => c.number === numInt);
443
+ return found?.path || null;
444
+ }
438
445
  // ============================================================================
439
446
  // АДАПТИВНЫЕ ФУНКЦИИ v1.4.3
440
447
  // ============================================================================
@@ -1153,6 +1160,157 @@ function saveSolutions(solutions) {
1153
1160
  const filePath = path.join(getMemoryPath(), "solutions.json");
1154
1161
  fs.writeFileSync(filePath, JSON.stringify(solutions, null, 2), "utf-8");
1155
1162
  }
1163
+ /**
1164
+ * Извлекает решения из текста контекста (legacy и новый формат)
1165
+ * Ищет паттерны: "Проблема:", "Решение:", "fix", "исправил", "причина" и т.д.
1166
+ */
1167
+ function extractSolutionsFromText(text, contextId) {
1168
+ const solutions = [];
1169
+ if (!text || text.length < 50)
1170
+ return solutions;
1171
+ // Паттерн 1: Структурированный формат "Проблема: X / Решение: Y"
1172
+ const structuredPattern = /(?:проблема|problem|issue|баг|bug|ошибка)[::]\s*([^\n]+)[\s\S]*?(?:решение|solution|fix|исправление)[::]\s*([^\n]+(?:\n(?!(?:проблема|problem|issue|баг|bug|ошибка))[^\n]+)*)/gi;
1173
+ let match;
1174
+ while ((match = structuredPattern.exec(text)) !== null) {
1175
+ const problem = match[1].trim();
1176
+ const solutionText = match[2].trim();
1177
+ if (problem.length > 10 && solutionText.length > 10) {
1178
+ solutions.push({
1179
+ problem,
1180
+ solution: solutionText.split("\n").map(s => s.trim()).filter(s => s.length > 0),
1181
+ tags: extractSolutionTags(problem + " " + solutionText),
1182
+ });
1183
+ }
1184
+ }
1185
+ // Паттерн 2: Нумерованный список "1. Проблема: X"
1186
+ const numberedPattern = /\d+\.\s*(?:проблема|problem)[::]?\s*([^\n]+)[\s\S]*?(?:решение|solution|fix)[::]?\s*([^\n]+(?:\n(?!\d+\.)[^\n]+)*)/gi;
1187
+ while ((match = numberedPattern.exec(text)) !== null) {
1188
+ const problem = match[1].trim();
1189
+ const solutionText = match[2].trim();
1190
+ if (problem.length > 10 && solutionText.length > 10) {
1191
+ // Проверяем что не дубликат
1192
+ if (!solutions.some(s => s.problem.toLowerCase() === problem.toLowerCase())) {
1193
+ solutions.push({
1194
+ problem,
1195
+ solution: solutionText.split("\n").map(s => s.trim()).filter(s => s.length > 0),
1196
+ tags: extractSolutionTags(problem + " " + solutionText),
1197
+ });
1198
+ }
1199
+ }
1200
+ }
1201
+ // Паттерн 3: "исправил X" / "fix: X" / "fixed X"
1202
+ const fixPattern = /(?:исправил|fixed|fix[::])\s+([^.!?\n]{15,100})/gi;
1203
+ while ((match = fixPattern.exec(text)) !== null) {
1204
+ const fixText = match[1].trim();
1205
+ // Извлекаем контекст вокруг fix
1206
+ const startIdx = Math.max(0, match.index - 200);
1207
+ const context = text.slice(startIdx, match.index);
1208
+ const problemHint = context.match(/(?:проблема|ошибка|баг|issue|bug)[::]?\s*([^\n]{10,80})/i);
1209
+ if (!solutions.some(s => s.solution.some(sol => sol.includes(fixText.slice(0, 30))))) {
1210
+ solutions.push({
1211
+ problem: problemHint ? problemHint[1].trim() : `Исправление в контексте #${contextId}`,
1212
+ solution: [fixText],
1213
+ tags: extractSolutionTags(fixText),
1214
+ });
1215
+ }
1216
+ }
1217
+ // Паттерн 4: "причина: X" с последующим решением
1218
+ const causePattern = /(?:причина|cause|root cause)[::]\s*([^\n]+)[\s\S]{0,300}?(?:решение|solution|fix)[::]\s*([^\n]+)/gi;
1219
+ while ((match = causePattern.exec(text)) !== null) {
1220
+ const cause = match[1].trim();
1221
+ const fix = match[2].trim();
1222
+ if (cause.length > 10 && fix.length > 10) {
1223
+ if (!solutions.some(s => s.problem.includes(cause.slice(0, 30)))) {
1224
+ solutions.push({
1225
+ problem: cause,
1226
+ solution: [fix],
1227
+ tags: extractSolutionTags(cause + " " + fix),
1228
+ });
1229
+ }
1230
+ }
1231
+ }
1232
+ return solutions;
1233
+ }
1234
+ /**
1235
+ * Извлекает теги из текста решения
1236
+ */
1237
+ function extractSolutionTags(text) {
1238
+ const tags = [];
1239
+ const lowerText = text.toLowerCase();
1240
+ const tagPatterns = {
1241
+ git: ["git", "commit", "push", "merge", "rebase", "branch"],
1242
+ npm: ["npm", "package.json", "node_modules", "yarn", "pnpm"],
1243
+ typescript: ["typescript", "ts", "type", "interface", "generic"],
1244
+ react: ["react", "component", "hook", "useState", "useEffect"],
1245
+ api: ["api", "endpoint", "fetch", "axios", "request", "response"],
1246
+ cache: ["cache", "кеш", "npx", "очистка"],
1247
+ build: ["build", "сборка", "compile", "webpack", "vite"],
1248
+ ios: ["ios", "swift", "xcode", "cocoapods"],
1249
+ android: ["android", "kotlin", "gradle"],
1250
+ database: ["database", "db", "sql", "mongo", "postgres"],
1251
+ auth: ["auth", "token", "jwt", "login", "password"],
1252
+ performance: ["performance", "оптимизация", "память", "memory"],
1253
+ };
1254
+ for (const [tag, patterns] of Object.entries(tagPatterns)) {
1255
+ if (patterns.some(p => lowerText.includes(p))) {
1256
+ tags.push(tag);
1257
+ }
1258
+ }
1259
+ return tags.slice(0, 5); // Максимум 5 тегов
1260
+ }
1261
+ /**
1262
+ * Добавляет извлечённые решения в базу
1263
+ */
1264
+ function addSolutionsToIndex(extracted, contextId) {
1265
+ if (extracted.length === 0)
1266
+ return 0;
1267
+ const solutionsIndex = loadSolutions();
1268
+ let added = 0;
1269
+ for (const ext of extracted) {
1270
+ // Проверяем на дубликаты по проблеме
1271
+ const isDuplicate = solutionsIndex.solutions.some(s => s.problem.toLowerCase().includes(ext.problem.toLowerCase().slice(0, 30)) ||
1272
+ ext.problem.toLowerCase().includes(s.problem.toLowerCase().slice(0, 30)));
1273
+ if (!isDuplicate) {
1274
+ const newId = `sol_${contextId}_${solutionsIndex.solutions.length + 1}`;
1275
+ solutionsIndex.solutions.push({
1276
+ id: newId,
1277
+ problem: ext.problem,
1278
+ solution: ext.solution,
1279
+ sourceContexts: [contextId],
1280
+ codeExample: undefined,
1281
+ });
1282
+ added++;
1283
+ }
1284
+ else {
1285
+ // Добавляем контекст к существующему решению
1286
+ const existing = solutionsIndex.solutions.find(s => s.problem.toLowerCase().includes(ext.problem.toLowerCase().slice(0, 30)));
1287
+ if (existing && !existing.sourceContexts.includes(contextId)) {
1288
+ existing.sourceContexts.push(contextId);
1289
+ }
1290
+ }
1291
+ }
1292
+ if (added > 0) {
1293
+ saveSolutions(solutionsIndex);
1294
+ }
1295
+ return added;
1296
+ }
1297
+ // ============================================================================
1298
+ // ПРАВИЛА v2.2: ЛОКАЛЬНЫЕ + ГЛОБАЛЬНЫЕ
1299
+ // ============================================================================
1300
+ /**
1301
+ * Путь к глобальной конфигурации ~/.dedsession/
1302
+ */
1303
+ function getGlobalConfigPath() {
1304
+ const home = process.env.HOME || process.env.USERPROFILE || "/tmp";
1305
+ const globalPath = path.join(home, ".dedsession");
1306
+ if (!fs.existsSync(globalPath)) {
1307
+ fs.mkdirSync(globalPath, { recursive: true });
1308
+ }
1309
+ return globalPath;
1310
+ }
1311
+ /**
1312
+ * Загрузить ЛОКАЛЬНЫЕ правила (из MD_HISTORY/memory/)
1313
+ */
1156
1314
  function loadRules() {
1157
1315
  const filePath = path.join(getMemoryPath(), "rules.json");
1158
1316
  try {
@@ -1163,23 +1321,65 @@ function loadRules() {
1163
1321
  catch { /* ignore */ }
1164
1322
  return { rules: [] };
1165
1323
  }
1324
+ /**
1325
+ * Сохранить ЛОКАЛЬНЫЕ правила
1326
+ */
1166
1327
  function saveRules(rules) {
1167
1328
  ensureMemoryStructure();
1168
1329
  const filePath = path.join(getMemoryPath(), "rules.json");
1169
1330
  fs.writeFileSync(filePath, JSON.stringify(rules, null, 2), "utf-8");
1170
1331
  }
1332
+ /**
1333
+ * Загрузить ГЛОБАЛЬНЫЕ правила (из ~/.dedsession/)
1334
+ */
1335
+ function loadGlobalRules() {
1336
+ const filePath = path.join(getGlobalConfigPath(), "global-rules.json");
1337
+ try {
1338
+ if (fs.existsSync(filePath)) {
1339
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
1340
+ }
1341
+ }
1342
+ catch { /* ignore */ }
1343
+ return { rules: [] };
1344
+ }
1345
+ /**
1346
+ * Сохранить ГЛОБАЛЬНЫЕ правила
1347
+ */
1348
+ function saveGlobalRules(rules) {
1349
+ const filePath = path.join(getGlobalConfigPath(), "global-rules.json");
1350
+ fs.writeFileSync(filePath, JSON.stringify(rules, null, 2), "utf-8");
1351
+ }
1352
+ /**
1353
+ * Форматирует блок правил (глобальные + локальные)
1354
+ */
1171
1355
  function formatRulesBlock() {
1172
- const rulesIndex = loadRules();
1173
- if (rulesIndex.rules.length === 0)
1356
+ const globalRules = loadGlobalRules();
1357
+ const localRules = loadRules();
1358
+ if (globalRules.rules.length === 0 && localRules.rules.length === 0)
1174
1359
  return "";
1175
- let output = "# \u{1F534} \u041f\u0420\u0418\u041e\u0420\u0418\u0422\u0415\u0422\u041d\u042b\u0415 \u041f\u0420\u0410\u0412\u0418\u041b\u0410\n\n";
1176
- output += "> \u042d\u0442\u0438 \u043f\u0440\u0430\u0432\u0438\u043b\u0430 Claude \u041e\u0411\u042f\u0417\u0410\u041d \u0441\u043e\u0431\u043b\u044e\u0434\u0430\u0442\u044c \u0432\u043e \u0412\u0421\u0415\u0425 \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u0430\u0445!\n\n";
1177
- for (const rule of rulesIndex.rules) {
1178
- output += `**${rule.id}.** ${rule.rule}`;
1179
- if (rule.context)
1180
- output += ` _(${rule.context})_`;
1360
+ let output = "# 🔴 ПРИОРИТЕТНЫЕ ПРАВИЛА\n\n";
1361
+ output += "> Эти правила Claude ОБЯЗАН соблюдать во ВСЕХ контекстах!\n\n";
1362
+ // Глобальные правила
1363
+ if (globalRules.rules.length > 0) {
1364
+ output += "**🌍 Глобальные (все проекты):**\n";
1365
+ for (const rule of globalRules.rules) {
1366
+ output += `- **G${rule.id}.** ${rule.rule}`;
1367
+ if (rule.context)
1368
+ output += ` _(${rule.context})_`;
1369
+ output += "\n";
1370
+ }
1181
1371
  output += "\n";
1182
1372
  }
1373
+ // Локальные правила
1374
+ if (localRules.rules.length > 0) {
1375
+ output += "**📁 Локальные (этот проект):**\n";
1376
+ for (const rule of localRules.rules) {
1377
+ output += `- **L${rule.id}.** ${rule.rule}`;
1378
+ if (rule.context)
1379
+ output += ` _(${rule.context})_`;
1380
+ output += "\n";
1381
+ }
1382
+ }
1183
1383
  output += "\n---\n\n";
1184
1384
  return output;
1185
1385
  }
@@ -1194,6 +1394,14 @@ const PROJECT_PATTERNS = {
1194
1394
  dedsession: ["dedsession", "mcp", "context", "memory", "session"],
1195
1395
  web: ["web", "react", "vue", "angular", "nextjs", "frontend", "html", "css"],
1196
1396
  };
1397
+ // Паттерны для определения людей (для context_smart)
1398
+ const PERSON_PATTERNS = {
1399
+ muradbek: ["muradbek", "мурадбек", "мурад", "ibraghimovmuradbek"],
1400
+ vlad: ["vlad", "vlad0sminer", "влад", "stweekmine"],
1401
+ makhach: ["makhach", "махач", "makhach1717"],
1402
+ akhmad: ["akhmad", "ахмад", "blck6rd"],
1403
+ eldad: ["eldad", "эльдар"],
1404
+ };
1197
1405
  function detectProject(content, folderName) {
1198
1406
  const lowerContent = (content + " " + folderName).toLowerCase();
1199
1407
  let bestMatch = null;
@@ -1210,6 +1418,92 @@ function detectProject(content, folderName) {
1210
1418
  }
1211
1419
  return bestMatch?.project || null;
1212
1420
  }
1421
+ // Определяет человека по содержимому (для context_smart)
1422
+ function detectPerson(content) {
1423
+ const lower = content.toLowerCase();
1424
+ for (const [person, patterns] of Object.entries(PERSON_PATTERNS)) {
1425
+ for (const pattern of patterns) {
1426
+ if (lower.includes(pattern)) {
1427
+ return person;
1428
+ }
1429
+ }
1430
+ }
1431
+ return null;
1432
+ }
1433
+ // Парсит HISTORY.md и возвращает последние N записей (для context_smart)
1434
+ function parseHistoryEntries(historyContent, limit = 25) {
1435
+ const entries = [];
1436
+ // Формат записи в HISTORY.md:
1437
+ // ---
1438
+ // #001 | 2026-01-07 | task-name | ✅
1439
+ // **Сводка:** текст сводки...
1440
+ // **Файлы:** список файлов
1441
+ const entryRegex = /#(\d{3})\s*\|\s*(\d{4}-\d{2}-\d{2})\s*\|\s*([^\|]+)\s*\|\s*(✅|⏳|❌)/g;
1442
+ const sections = historyContent.split(/^---$/m);
1443
+ for (const section of sections) {
1444
+ const match = entryRegex.exec(section);
1445
+ if (match) {
1446
+ const [, number, date, name, status] = match;
1447
+ const cleanName = name.trim();
1448
+ // Извлекаем сводку (всё после заголовка записи)
1449
+ const summaryStart = section.indexOf(status) + status.length;
1450
+ const summary = section.slice(summaryStart).trim();
1451
+ // Определяем человека по названию и сводке
1452
+ const person = detectPerson(cleanName + " " + summary);
1453
+ // Извлекаем ключевые слова из названия
1454
+ const keywords = cleanName.split(/[-_\s]+/).filter(w => w.length > 2);
1455
+ entries.push({
1456
+ number,
1457
+ date,
1458
+ name: cleanName,
1459
+ status,
1460
+ summary,
1461
+ person,
1462
+ keywords,
1463
+ });
1464
+ }
1465
+ // Сбрасываем regex для следующей итерации
1466
+ entryRegex.lastIndex = 0;
1467
+ }
1468
+ // Сортируем по номеру (новые первые) и берём limit
1469
+ return entries
1470
+ .sort((a, b) => parseInt(b.number) - parseInt(a.number))
1471
+ .slice(0, limit);
1472
+ }
1473
+ // Вычисляет релевантность контекста к запросу (для context_smart_focus)
1474
+ function scoreContextRelevance(entry, queryPerson, queryKeywords, memoryIndex) {
1475
+ let score = 0;
1476
+ // +10 за совпадение человека
1477
+ if (queryPerson && entry.person === queryPerson) {
1478
+ score += 10;
1479
+ }
1480
+ // +5 за совпадение ключевого слова в названии
1481
+ for (const kw of queryKeywords) {
1482
+ if (entry.name.toLowerCase().includes(kw)) {
1483
+ score += 5;
1484
+ }
1485
+ }
1486
+ // +2 за совпадение ключевого слова в сводке
1487
+ for (const kw of queryKeywords) {
1488
+ if (entry.summary.toLowerCase().includes(kw)) {
1489
+ score += 2;
1490
+ }
1491
+ }
1492
+ // Бонус из Memory v2.0: +3 за совпадение project
1493
+ const contextMeta = memoryIndex.contexts[entry.number];
1494
+ if (contextMeta) {
1495
+ for (const kw of queryKeywords) {
1496
+ if (contextMeta.project && contextMeta.project.toLowerCase().includes(kw)) {
1497
+ score += 3;
1498
+ }
1499
+ // +1 за совпадение в keywords из Memory
1500
+ if (contextMeta.keywords.some(k => k.includes(kw) || kw.includes(k))) {
1501
+ score += 1;
1502
+ }
1503
+ }
1504
+ }
1505
+ return score;
1506
+ }
1213
1507
  function extractKeywords(content) {
1214
1508
  const keywords = new Set();
1215
1509
  // Извлекаем технологии и инструменты
@@ -1287,6 +1581,7 @@ function migrateToMemorySystem() {
1287
1581
  migrated: 0,
1288
1582
  projects: [],
1289
1583
  epics: [],
1584
+ solutions: 0,
1290
1585
  errors: [],
1291
1586
  };
1292
1587
  try {
@@ -1311,7 +1606,20 @@ function migrateToMemorySystem() {
1311
1606
  if (fs.existsSync(changesPath)) {
1312
1607
  changes = fs.readFileSync(changesPath, "utf-8");
1313
1608
  }
1609
+ // v2.2: Читаем problems-solutions если есть
1610
+ const problemsPath = path.join(ctx.path, "08-problems-solutions.md");
1611
+ let problemsSolutions = "";
1612
+ if (fs.existsSync(problemsPath)) {
1613
+ problemsSolutions = fs.readFileSync(problemsPath, "utf-8");
1614
+ }
1615
+ // v2.2: Читаем session-log для legacy решений
1616
+ const sessionLogPath = path.join(ctx.path, "05-session-log.md");
1617
+ let sessionLog = "";
1618
+ if (fs.existsSync(sessionLogPath)) {
1619
+ sessionLog = fs.readFileSync(sessionLogPath, "utf-8");
1620
+ }
1314
1621
  const fullContent = readme + "\n" + changes;
1622
+ const solutionsContent = problemsSolutions + "\n" + readme + "\n" + changes + "\n" + sessionLog;
1315
1623
  // Определяем проект
1316
1624
  const project = detectProject(fullContent, ctx.name);
1317
1625
  // Извлекаем ключевые слова
@@ -1381,6 +1689,12 @@ function migrateToMemorySystem() {
1381
1689
  keywordsIndex.keywords[keyword].push(contextMeta.id);
1382
1690
  }
1383
1691
  }
1692
+ // v2.2: Извлекаем решения из контекста
1693
+ const extractedSolutions = extractSolutionsFromText(solutionsContent, contextMeta.id);
1694
+ if (extractedSolutions.length > 0) {
1695
+ const added = addSolutionsToIndex(extractedSolutions, contextMeta.id);
1696
+ result.solutions += added;
1697
+ }
1384
1698
  result.migrated++;
1385
1699
  }
1386
1700
  catch (err) {
@@ -1774,7 +2088,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
1774
2088
  },
1775
2089
  {
1776
2090
  name: "context_save_detailed",
1777
- description: "📚 ПОДРОБНОЕ сохранение контекста (10 файлов). ОБЯЗАТЕЛЬНО вызывай когда пользователь говорит: 'сохрани контекст подробно', 'подробно сохрани', 'детальное сохранение', 'сохрани всё подробно'. Создаёт 10 файлов с максимально качественной информацией. Перед вызовом Claude ОБЯЗАН прочитать гайд по заполнению!",
2091
+ description: "📚 МАКСИМАЛЬНО ПОДРОБНОЕ сохранение контекста (15 файлов). ОБЯЗАТЕЛЬНО вызывай когда пользователь говорит: 'сохрани контекст подробно', 'подробно сохрани', 'детальное сохранение', 'сохрани всё подробно', '15 файлов'. Создаёт 15 файлов с максимально качественной информацией. Перед вызовом Claude ОБЯЗАН прочитать гайд по заполнению!",
1778
2092
  inputSchema: {
1779
2093
  type: "object",
1780
2094
  properties: {
@@ -1817,12 +2131,36 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
1817
2131
  },
1818
2132
  problems_solutions: {
1819
2133
  type: "string",
1820
- description: "Для КАЖДОЙ проблемы: симптомы, root cause, решение, как предотвратить. Минимум 2 проблемы!",
2134
+ description: "Для КАЖДОЙ проблемы: симптомы, root cause, решение, как предотвратить. Формат: 'Проблема: X / Решение: Y'. Минимум 2 проблемы!",
1821
2135
  },
1822
2136
  architecture: {
1823
2137
  type: "string",
1824
2138
  description: "Структура модулей, потоки данных, интеграции. Можно ASCII-диаграммы.",
1825
2139
  },
2140
+ testing: {
2141
+ type: "string",
2142
+ description: "Что тестировали, как тестировали, результаты тестов, покрытие.",
2143
+ },
2144
+ api_docs: {
2145
+ type: "string",
2146
+ description: "API документация: эндпоинты, параметры, примеры запросов/ответов.",
2147
+ },
2148
+ migration: {
2149
+ type: "string",
2150
+ description: "Миграции данных, breaking changes, инструкции по обновлению.",
2151
+ },
2152
+ performance: {
2153
+ type: "string",
2154
+ description: "Оптимизации производительности, метрики до/после, узкие места.",
2155
+ },
2156
+ security: {
2157
+ type: "string",
2158
+ description: "Анализ безопасности, уязвимости, исправления, рекомендации.",
2159
+ },
2160
+ full_context: {
2161
+ type: "string",
2162
+ description: "Полный контекст сессии: всё что не вошло в другие файлы, дополнительные заметки.",
2163
+ },
1826
2164
  history_summary: {
1827
2165
  type: "string",
1828
2166
  description: "⭐ ОБЯЗАТЕЛЬНО: Качественная сводка для HISTORY.md (2-4 предложения). Формат: **Сводка:** суть задачи, что сделано, результат. **Файлы:** список изменённых файлов.",
@@ -2025,7 +2363,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2025
2363
  },
2026
2364
  {
2027
2365
  name: "rules",
2028
- description: "🔴 Управление ПРИОРИТЕТНЫМИ ПРАВИЛАМИ. ОБЯЗАТЕЛЬНО вызывай когда пользователь говорит: 'добавь правило', 'правила', 'покажи правила', 'удали правило', 'новое правило'. Правила показываются ПЕРВЫМИ при каждой загрузке контекста и Claude ОБЯЗАН их соблюдать.",
2366
+ description: "🔴 Управление ПРИОРИТЕТНЫМИ ПРАВИЛАМИ. ОБЯЗАТЕЛЬНО вызывай когда пользователь говорит: 'добавь правило', 'правила', 'покажи правила', 'удали правило', 'новое правило', 'глобальное правило'. Правила показываются ПЕРВЫМИ при каждой загрузке контекста и Claude ОБЯЗАН их соблюдать.",
2029
2367
  inputSchema: {
2030
2368
  type: "object",
2031
2369
  properties: {
@@ -2034,6 +2372,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2034
2372
  enum: ["add", "list", "remove"],
2035
2373
  description: "Действие: add (добавить), list (показать все), remove (удалить по ID)",
2036
2374
  },
2375
+ scope: {
2376
+ type: "string",
2377
+ enum: ["local", "global"],
2378
+ description: "Область: local (только этот проект, по умолчанию), global (все проекты, ~/.dedsession/)",
2379
+ },
2037
2380
  rule: {
2038
2381
  type: "string",
2039
2382
  description: "Текст правила (для action=add)",
@@ -2050,6 +2393,29 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2050
2393
  required: ["action"],
2051
2394
  },
2052
2395
  },
2396
+ // ========== ТЕСТОВЫЕ КОМАНДЫ SMART CONTEXT ==========
2397
+ {
2398
+ name: "context_smart",
2399
+ description: "🧪 ТЕСТОВАЯ команда. Умная загрузка 25 контекстов с группировкой по людям/темам. ОБЯЗАТЕЛЬНО вызывай когда пользователь говорит: 'умный контекст', 'smart context', 'контекст по людям', 'тестовый контекст'. После обзора опиши задачу для точной подгрузки.",
2400
+ inputSchema: {
2401
+ type: "object",
2402
+ properties: {},
2403
+ },
2404
+ },
2405
+ {
2406
+ name: "context_smart_focus",
2407
+ description: "🧪 ТЕСТОВАЯ команда. Подгружает 2-4 релевантных контекста ПОЛНОСТЬЮ по описанию задачи. Вызывай ПОСЛЕ context_smart или когда пользователь описывает задачу после умного контекста.",
2408
+ inputSchema: {
2409
+ type: "object",
2410
+ properties: {
2411
+ task: {
2412
+ type: "string",
2413
+ description: "Описание задачи в свободном формате: 'доска мурадбека', 'влад скрипты', 'рефакторинг как делали'",
2414
+ },
2415
+ },
2416
+ required: ["task"],
2417
+ },
2418
+ },
2053
2419
  ],
2054
2420
  }));
2055
2421
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -2203,6 +2569,18 @@ ${args?.changes?.split("\n").map(l => l.trim() ? `- ${l}` : "").join("\n") || "-
2203
2569
  args?.session_log,
2204
2570
  ].filter(Boolean).join("\n");
2205
2571
  const memoryInfo = addContextToMemory(contextNumber, result.name, taskName, status, fullContent, new Date().toISOString().split("T")[0]);
2572
+ // v2.2: Извлекаем решения при сохранении
2573
+ let solutionsAdded = 0;
2574
+ if (args?.problems_solutions) {
2575
+ const solutionsContent = [
2576
+ args?.problems_solutions,
2577
+ args?.changes,
2578
+ args?.session_log,
2579
+ ].filter(Boolean).join("\n");
2580
+ const contextId = String(contextNumber).padStart(3, "0");
2581
+ const extracted = extractSolutionsFromText(solutionsContent, contextId);
2582
+ solutionsAdded = addSolutionsToIndex(extracted, contextId);
2583
+ }
2206
2584
  let output = `✅ **Контекст сохранён**\n\n`;
2207
2585
  output += `📁 **Папка:** \`${result.name}\`\n`;
2208
2586
  output += `📄 **Файлов:** ${result.filesCount}\n`;
@@ -2214,6 +2592,9 @@ ${args?.changes?.split("\n").map(l => l.trim() ? `- ${l}` : "").join("\n") || "-
2214
2592
  output += `\n🧠 **Memory v2.0:**\n`;
2215
2593
  output += `- Проект: **${memoryInfo.project}**\n`;
2216
2594
  output += `- Теги: ${memoryInfo.keywords.slice(0, 5).join(", ") || "-"}\n`;
2595
+ if (solutionsAdded > 0) {
2596
+ output += `- 💡 Решений добавлено: **${solutionsAdded}**\n`;
2597
+ }
2217
2598
  }
2218
2599
  if (state.loadedContext && state.loadedContext !== result.name) {
2219
2600
  output += `\n🔗 **Parent:** ${state.loadedContext}\n`;
@@ -2260,7 +2641,7 @@ ${args?.changes?.split("\n").map(l => l.trim() ? `- ${l}` : "").join("\n") || "-
2260
2641
  blocked: "❌ Заблокировано",
2261
2642
  };
2262
2643
  const status = statusMap[args?.status || "completed"] || "✅ Завершено";
2263
- // Всегда 10 файлов для детального сохранения
2644
+ // Всегда 15 файлов для максимально детального сохранения
2264
2645
  const detailedFiles = [
2265
2646
  "README.md",
2266
2647
  "01-task-overview.md",
@@ -2272,13 +2653,19 @@ ${args?.changes?.split("\n").map(l => l.trim() ? `- ${l}` : "").join("\n") || "-
2272
2653
  "07-decisions.md",
2273
2654
  "08-problems-solutions.md",
2274
2655
  "09-architecture.md",
2656
+ "10-testing.md",
2657
+ "11-api-docs.md",
2658
+ "12-migration.md",
2659
+ "13-performance.md",
2660
+ "14-security.md",
2661
+ "15-full-context.md",
2275
2662
  ];
2276
2663
  // Формируем README
2277
2664
  const readme = `# ${taskName}
2278
2665
 
2279
2666
  **Дата:** ${new Date().toISOString().split("T")[0]}
2280
2667
  **Статус:** ${status}
2281
- **Масштаб:** detailed (10 файлов)
2668
+ **Масштаб:** detailed (15 файлов)
2282
2669
 
2283
2670
  ## Краткое описание
2284
2671
 
@@ -2304,6 +2691,13 @@ ${state.loadedContext ? `**Parent:** ${state.loadedContext}` : "Нет parent к
2304
2691
  decisions: args?.decisions || "Решения не документированы",
2305
2692
  problemsSolutions: args?.problems_solutions || "Проблемы не описаны",
2306
2693
  architecture: args?.architecture || "Архитектура не описана",
2694
+ // v2.2: Новые поля для 15 файлов
2695
+ testing: args?.testing || "Тестирование не описано",
2696
+ apiDocs: args?.api_docs || "API не документировано",
2697
+ migration: args?.migration || "Миграции отсутствуют",
2698
+ performance: args?.performance || "Оптимизации не проводились",
2699
+ security: args?.security || "Безопасность не анализировалась",
2700
+ fullContext: args?.full_context || "Полный контекст сессии не записан",
2307
2701
  // v1.4.10: Готовая сводка для HISTORY.md
2308
2702
  historySummary: args?.history_summary,
2309
2703
  }, undefined, detailedFiles);
@@ -2324,9 +2718,19 @@ ${state.loadedContext ? `**Parent:** ${state.loadedContext}` : "Нет parent к
2324
2718
  args?.architecture,
2325
2719
  ].filter(Boolean).join("\n");
2326
2720
  const memoryInfo = addContextToMemory(contextNumber, result.name, taskName, status, fullContent, new Date().toISOString().split("T")[0]);
2721
+ // v2.2: Извлекаем решения при сохранении (detailed всегда имеет problems_solutions)
2722
+ let solutionsAdded = 0;
2723
+ const solutionsContent = [
2724
+ args?.problems_solutions,
2725
+ args?.changes,
2726
+ args?.session_log,
2727
+ ].filter(Boolean).join("\n");
2728
+ const contextId = String(contextNumber).padStart(3, "0");
2729
+ const extracted = extractSolutionsFromText(solutionsContent, contextId);
2730
+ solutionsAdded = addSolutionsToIndex(extracted, contextId);
2327
2731
  let output = `✅ **Контекст сохранён ПОДРОБНО**\n\n`;
2328
2732
  output += `📁 **Папка:** \`${result.name}\`\n`;
2329
- output += `📄 **Файлов:** 10\n`;
2733
+ output += `📄 **Файлов:** 15\n`;
2330
2734
  output += `🎯 **Масштаб:** detailed (максимальная детализация)\n`;
2331
2735
  output += `📍 **Путь:** \`${result.path}\`\n`;
2332
2736
  // v2.0: Показываем Memory info
@@ -2334,6 +2738,9 @@ ${state.loadedContext ? `**Parent:** ${state.loadedContext}` : "Нет parent к
2334
2738
  output += `\n🧠 **Memory v2.0:**\n`;
2335
2739
  output += `- Проект: **${memoryInfo.project}**\n`;
2336
2740
  output += `- Теги: ${memoryInfo.keywords.slice(0, 5).join(", ") || "-"}\n`;
2741
+ if (solutionsAdded > 0) {
2742
+ output += `- 💡 Решений добавлено: **${solutionsAdded}**\n`;
2743
+ }
2337
2744
  }
2338
2745
  if (state.loadedContext && state.loadedContext !== result.name) {
2339
2746
  output += `\n🔗 **Parent:** ${state.loadedContext}\n`;
@@ -3158,7 +3565,8 @@ MD_HISTORY/
3158
3565
  output += `📊 Результаты:\n`;
3159
3566
  output += `- Проиндексировано контекстов: ${result.migrated}\n`;
3160
3567
  output += `- Обнаружено проектов: ${result.projects.length} (${result.projects.join(", ") || "-"})\n`;
3161
- output += `- Эпиков: ${result.epics.length}\n\n`;
3568
+ output += `- Эпиков: ${result.epics.length}\n`;
3569
+ output += `- 💡 Извлечено решений: ${result.solutions}\n\n`;
3162
3570
  const stats = getMemoryStats();
3163
3571
  output += `## 🌡️ Распределение по температуре\n\n`;
3164
3572
  output += `- 🔥 Hot: ${stats.temperatures.hot}\n`;
@@ -3186,17 +3594,23 @@ MD_HISTORY/
3186
3594
  // ==================== RULES ====================
3187
3595
  case "rules": {
3188
3596
  const action = args.action || "list";
3597
+ const scope = args.scope || "local";
3189
3598
  const ruleText = args.rule;
3190
3599
  const ruleId = args.id;
3191
3600
  const ruleContext = args.context;
3192
- const rulesIndex = loadRules();
3601
+ const isGlobal = scope === "global";
3602
+ const rulesIndex = isGlobal ? loadGlobalRules() : loadRules();
3603
+ const scopeLabel = isGlobal ? "🌍 Глобальное" : "📁 Локальное";
3604
+ const scopePath = isGlobal ? "~/.dedsession/" : "MD_HISTORY/memory/";
3193
3605
  switch (action) {
3194
3606
  case "add": {
3195
3607
  if (!ruleText) {
3196
3608
  return {
3197
3609
  content: [{
3198
3610
  type: "text",
3199
- text: `⚠️ Укажи текст правила.\n\n**Пример:** \`rules add "При работе с Trello пиши 'сгенерировано Claude Code'"\``
3611
+ text: `⚠️ Укажи текст правила.\n\n**Примеры:**\n` +
3612
+ `- Локальное: \`rules add "Правило для этого проекта"\`\n` +
3613
+ `- Глобальное: \`rules add "Правило для ВСЕХ проектов" scope:global\``
3200
3614
  }]
3201
3615
  };
3202
3616
  }
@@ -3210,12 +3624,18 @@ MD_HISTORY/
3210
3624
  context: ruleContext,
3211
3625
  };
3212
3626
  rulesIndex.rules.push(newRule);
3213
- saveRules(rulesIndex);
3214
- let output = `# ✅ Правило добавлено\n\n`;
3215
- output += `**ID:** ${newRule.id}\n`;
3627
+ if (isGlobal) {
3628
+ saveGlobalRules(rulesIndex);
3629
+ }
3630
+ else {
3631
+ saveRules(rulesIndex);
3632
+ }
3633
+ let output = `# ✅ ${scopeLabel} правило добавлено\n\n`;
3634
+ output += `**ID:** ${isGlobal ? "G" : "L"}${newRule.id}\n`;
3216
3635
  output += `**Правило:** ${newRule.rule}\n`;
3217
3636
  if (newRule.context)
3218
3637
  output += `**Контекст:** ${newRule.context}\n`;
3638
+ output += `**Хранится:** \`${scopePath}\`\n`;
3219
3639
  output += `\nТеперь это правило будет показываться при каждой загрузке контекста.\n`;
3220
3640
  output += `\n---\n\n`;
3221
3641
  output += formatRulesBlock();
@@ -3226,7 +3646,9 @@ MD_HISTORY/
3226
3646
  return {
3227
3647
  content: [{
3228
3648
  type: "text",
3229
- text: `⚠️ Укажи ID правила для удаления.\n\n**Пример:** \`rules remove 1\``
3649
+ text: `⚠️ Укажи ID правила для удаления.\n\n**Примеры:**\n` +
3650
+ `- Локальное: \`rules remove 1\`\n` +
3651
+ `- Глобальное: \`rules remove 1 scope:global\``
3230
3652
  }]
3231
3653
  };
3232
3654
  }
@@ -3235,36 +3657,221 @@ MD_HISTORY/
3235
3657
  return {
3236
3658
  content: [{
3237
3659
  type: "text",
3238
- text: `⚠️ Правило с ID ${ruleId} не найдено.`
3660
+ text: `⚠️ ${scopeLabel} правило с ID ${ruleId} не найдено.`
3239
3661
  }]
3240
3662
  };
3241
3663
  }
3242
3664
  const removed = rulesIndex.rules.splice(ruleIndex, 1)[0];
3243
- saveRules(rulesIndex);
3244
- let output = `# ✅ Правило удалено\n\n`;
3245
- output += `**ID:** ${removed.id}\n`;
3246
- output += `**Было:** ${removed.rule}\n`;
3247
- output += `\n---\n\n`;
3248
- if (rulesIndex.rules.length > 0) {
3249
- output += formatRulesBlock();
3665
+ if (isGlobal) {
3666
+ saveGlobalRules(rulesIndex);
3250
3667
  }
3251
3668
  else {
3669
+ saveRules(rulesIndex);
3670
+ }
3671
+ let output = `# ✅ ${scopeLabel} правило удалено\n\n`;
3672
+ output += `**ID:** ${isGlobal ? "G" : "L"}${removed.id}\n`;
3673
+ output += `**Было:** ${removed.rule}\n`;
3674
+ output += `\n---\n\n`;
3675
+ output += formatRulesBlock();
3676
+ if (loadGlobalRules().rules.length === 0 && loadRules().rules.length === 0) {
3252
3677
  output += `_Нет активных правил._\n`;
3253
3678
  }
3254
3679
  return { content: [{ type: "text", text: output }] };
3255
3680
  }
3256
3681
  case "list":
3257
3682
  default: {
3258
- if (rulesIndex.rules.length === 0) {
3683
+ const globalRules = loadGlobalRules();
3684
+ const localRules = loadRules();
3685
+ if (globalRules.rules.length === 0 && localRules.rules.length === 0) {
3259
3686
  let output = `# 🔴 Приоритетные правила\n\n`;
3260
3687
  output += `_Нет активных правил._\n\n`;
3261
3688
  output += `**Добавить правило:**\n`;
3262
- output += `\`rules add "При работе с Trello пиши 'сгенерировано Claude Code'" context:trello\`\n`;
3689
+ output += `- Локальное (этот проект): \`rules add "Текст правила"\`\n`;
3690
+ output += `- Глобальное (все проекты): \`rules add "Текст правила" scope:global\`\n`;
3263
3691
  return { content: [{ type: "text", text: output }] };
3264
3692
  }
3265
- return { content: [{ type: "text", text: formatRulesBlock() }] };
3693
+ let output = formatRulesBlock();
3694
+ output += `\n**Управление:**\n`;
3695
+ output += `- Добавить локальное: \`rules add "..."\`\n`;
3696
+ output += `- Добавить глобальное: \`rules add "..." scope:global\`\n`;
3697
+ output += `- Удалить: \`rules remove ID [scope:global]\`\n`;
3698
+ return { content: [{ type: "text", text: output }] };
3699
+ }
3700
+ }
3701
+ }
3702
+ // ========== ТЕСТОВЫЕ HANDLERS SMART CONTEXT ==========
3703
+ case "context_smart": {
3704
+ const history = readHistory();
3705
+ if (!history) {
3706
+ return {
3707
+ content: [{
3708
+ type: "text",
3709
+ text: `❌ **HISTORY.md не найден**\n\nСначала создайте контексты или запустите \`context_migrate\`.`,
3710
+ }],
3711
+ };
3712
+ }
3713
+ const entries = parseHistoryEntries(history, 25);
3714
+ const memoryIndex = loadMemoryIndex();
3715
+ // Группируем по людям
3716
+ const byPerson = {};
3717
+ for (const entry of entries) {
3718
+ const person = entry.person || "other";
3719
+ if (!byPerson[person])
3720
+ byPerson[person] = [];
3721
+ byPerson[person].push(entry);
3722
+ }
3723
+ // Группируем по проектам из Memory v2.0
3724
+ const byProject = {};
3725
+ for (const entry of entries) {
3726
+ const meta = memoryIndex.contexts[entry.number];
3727
+ const project = meta?.project || "unknown";
3728
+ if (!byProject[project])
3729
+ byProject[project] = [];
3730
+ byProject[project].push(entry);
3731
+ }
3732
+ // Формируем компактный вывод
3733
+ let output = `# 🧠 Smart Context — Обзор ${entries.length} контекстов\n\n`;
3734
+ // По людям
3735
+ output += `## 👥 По людям:\n`;
3736
+ for (const [person, list] of Object.entries(byPerson)) {
3737
+ if (person !== "other") {
3738
+ const ids = list.slice(0, 8).map(e => `#${e.number}`).join(", ");
3739
+ const more = list.length > 8 ? ` (+${list.length - 8})` : "";
3740
+ output += `- **${person}** (${list.length}): ${ids}${more}\n`;
3741
+ }
3742
+ }
3743
+ if (byPerson["other"]) {
3744
+ output += `- _другие_ (${byPerson["other"].length})\n`;
3745
+ }
3746
+ // По проектам (если есть Memory v2.0 данные)
3747
+ const projectsWithData = Object.entries(byProject).filter(([p]) => p !== "unknown");
3748
+ if (projectsWithData.length > 0) {
3749
+ output += `\n## 📁 По проектам:\n`;
3750
+ for (const [project, list] of projectsWithData) {
3751
+ output += `- **${project}** (${list.length})\n`;
3752
+ }
3753
+ }
3754
+ // Последние 10 с краткими сводками
3755
+ output += `\n## 📋 Последние 10:\n`;
3756
+ output += `| # | Название | Человек | Сводка |\n|---|----------|---------|--------|\n`;
3757
+ for (const entry of entries.slice(0, 10)) {
3758
+ const shortSummary = entry.summary.slice(0, 50).replace(/\n/g, " ") + "...";
3759
+ output += `| ${entry.number} | ${entry.name} | ${entry.person || "-"} | ${shortSummary} |\n`;
3760
+ }
3761
+ // Генерируем динамические примеры на основе реальных данных
3762
+ output += `\n---\n`;
3763
+ output += `💡 **Опиши задачу** в свободном формате, и я подгружу релевантные контексты:\n`;
3764
+ const examples = [];
3765
+ // Пример с человеком (берём первого найденного)
3766
+ const persons = Object.keys(byPerson).filter(p => p !== "other");
3767
+ if (persons.length > 0) {
3768
+ const person = persons[0];
3769
+ const personEntry = byPerson[person][0];
3770
+ // Извлекаем ключевое слово из названия
3771
+ const keyword = personEntry.name.split("-").find(w => w.length > 3 && !w.includes(person)) || "";
3772
+ examples.push(`"${keyword ? keyword + " " : ""}${person}"`);
3773
+ }
3774
+ // Пример с проектом (если есть)
3775
+ if (projectsWithData.length > 0) {
3776
+ const [project, list] = projectsWithData[0];
3777
+ const entry = list[0];
3778
+ const keyword = entry.name.split("-").find(w => w.length > 3) || "";
3779
+ examples.push(`"${keyword} ${project}"`);
3780
+ }
3781
+ // Пример из последнего контекста
3782
+ if (entries.length > 0) {
3783
+ const lastEntry = entries[0];
3784
+ const words = lastEntry.name.split("-").filter(w => w.length > 3).slice(0, 2);
3785
+ if (words.length > 0) {
3786
+ examples.push(`"${words.join(" ")}"`);
3787
+ }
3788
+ }
3789
+ // Выводим примеры (максимум 3)
3790
+ for (const example of examples.slice(0, 3)) {
3791
+ output += `- ${example}\n`;
3792
+ }
3793
+ output += `\nClaude автоматически найдёт и загрузит 2-4 релевантных контекста.`;
3794
+ return { content: [{ type: "text", text: output }] };
3795
+ }
3796
+ case "context_smart_focus": {
3797
+ const task = args.task;
3798
+ if (!task) {
3799
+ return {
3800
+ content: [{
3801
+ type: "text",
3802
+ text: `❌ **Укажи описание задачи**\n\nПример: "доска мурадбека", "влад скрипты", "рефакторинг"`,
3803
+ }],
3804
+ };
3805
+ }
3806
+ const history = readHistory();
3807
+ if (!history) {
3808
+ return {
3809
+ content: [{
3810
+ type: "text",
3811
+ text: `❌ **HISTORY.md не найден**`,
3812
+ }],
3813
+ };
3814
+ }
3815
+ // Парсим запрос
3816
+ const queryPerson = detectPerson(task);
3817
+ const queryKeywords = task.toLowerCase()
3818
+ .split(/\s+/)
3819
+ .filter(w => w.length > 2 && !["над", "для", "как", "что", "это", "мы", "работаем"].includes(w));
3820
+ // Загружаем данные
3821
+ const entries = parseHistoryEntries(history, 25);
3822
+ const memoryIndex = loadMemoryIndex();
3823
+ // Скорим контексты
3824
+ const scored = entries.map(entry => ({
3825
+ entry,
3826
+ score: scoreContextRelevance(entry, queryPerson, queryKeywords, memoryIndex),
3827
+ }));
3828
+ // Топ 2-4 по score (минимум score > 0)
3829
+ const top = scored
3830
+ .filter(s => s.score > 0)
3831
+ .sort((a, b) => b.score - a.score)
3832
+ .slice(0, 4);
3833
+ if (top.length === 0) {
3834
+ let output = `❌ **Не найдено релевантных контекстов для:** "${task}"\n\n`;
3835
+ output += `**Искали:**\n`;
3836
+ if (queryPerson)
3837
+ output += `- Человек: ${queryPerson}\n`;
3838
+ output += `- Ключевые слова: ${queryKeywords.join(", ")}\n\n`;
3839
+ output += `**Попробуй:**\n`;
3840
+ output += `- Уточнить имя человека (muradbek, vlad, makhach, akhmad)\n`;
3841
+ output += `- Использовать ключевые слова из названий контекстов\n`;
3842
+ return { content: [{ type: "text", text: output }] };
3843
+ }
3844
+ // Загружаем контексты полностью
3845
+ let output = `# 🎯 Фокус: "${task}"\n\n`;
3846
+ output += `**Найдено:** ${top.length} релевантных контекстов\n`;
3847
+ if (queryPerson)
3848
+ output += `**Человек:** ${queryPerson}\n`;
3849
+ output += `**Ключевые слова:** ${queryKeywords.join(", ")}\n\n`;
3850
+ output += `---\n\n`;
3851
+ for (const { entry, score } of top) {
3852
+ output += `## #${entry.number}: ${entry.name} (score: ${score})\n\n`;
3853
+ // Загружаем полный контекст
3854
+ const contextPath = findContextByNumber(entry.number);
3855
+ if (contextPath) {
3856
+ // README.md
3857
+ const readmePath = path.join(contextPath, "README.md");
3858
+ if (fs.existsSync(readmePath)) {
3859
+ const readme = fs.readFileSync(readmePath, "utf-8");
3860
+ output += readme + "\n\n";
3861
+ }
3862
+ // 03-changes.md (если есть)
3863
+ const changesPath = path.join(contextPath, "03-changes.md");
3864
+ if (fs.existsSync(changesPath)) {
3865
+ const changes = fs.readFileSync(changesPath, "utf-8");
3866
+ output += `### 🔧 Изменения:\n${changes}\n\n`;
3867
+ }
3868
+ // Обновляем accessCount в Memory v2.0
3869
+ updateContextAccess(entry.number);
3266
3870
  }
3871
+ output += `---\n\n`;
3267
3872
  }
3873
+ output += `💡 **Контексты загружены.** Теперь можешь работать над задачей!`;
3874
+ return { content: [{ type: "text", text: output }] };
3268
3875
  }
3269
3876
  default:
3270
3877
  return { content: [{ type: "text", text: `❌ Неизвестный tool: ${name}` }] };