From de5dac69464c4f6579565dbbe7b67e856707a863 Mon Sep 17 00:00:00 2001 From: MerfiDEV Date: Tue, 6 Oct 2026 14:24:01 +0300 Subject: [PATCH 01/11] feat: add AI memory system with save/read/clear tools - tools/MemoryTool.js: memory_save / memory_read / memory_clear - src/main/memory-store.js: persistent storage at /memory.md - registered in tools/index.js + JsRunner bootstrap + cuckoo-tools.d.ts - plan-mode blocks mutating memory ops; settings UI section with open/clear - Telegram bot: pretty rendering for memory/city_time notifications --- README.md | 37 +++++--- botsrc/i18n.js | 32 +++++++ botsrc/index.js | 159 ++++++++++++++++++++++++++++++-- src/main/ipc.js | 37 ++++++++ src/main/memory-store.js | 116 +++++++++++++++++++++++ src/main/plan-mode.js | 53 +++++++---- src/preload/api.js | 10 ++ src/preload/dom/settings-tab.js | 82 ++++++++++++++++ src/preload/i18n/i18n.js | 16 ++++ tools/JsRunner.js | 27 ++++-- tools/MemoryTool.js | 137 +++++++++++++++++++++++++++ tools/cuckoo-tools.d.ts | 28 ++++++ tools/index.js | 15 ++- 13 files changed, 700 insertions(+), 49 deletions(-) create mode 100644 src/main/memory-store.js create mode 100644 tools/MemoryTool.js diff --git a/README.md b/README.md index dd73361..4f75ff0 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ ✅ **Настоящий агентный цикл** — AI читает файлы → пишет код → запускает тесты → коммитит изменения ✅ **Интеграция Git** — смотрите diff, листайте историю, проверяйте изменения визуально ✅ **Управление через Telegram** — пишите код с телефона, получайте уведомления, подтверждайте команды удалённо -✅ **Красивый UI** — стеклянные эффекты, кастомные темы, анимированные петы, тёмная тема +✅ **Красивый UI** — стеклянные эффекты, кастомные темы, анимированные петы, тёмная тема --- @@ -38,6 +38,7 @@ ### 🤖 **AI-агент, который реально работает** Теперь AI умеет: + - 📖 **Читать файлы** — структура проекта, grep-паттерны, содержимое - ✏️ **Писать и редактировать код** — создавать файлы, применять патчи, удалять старый код - 💻 **Запускать команды** — bash, PowerShell, SQL-запросы, любые shell-скрипты @@ -45,8 +46,10 @@ - 📊 **Управлять задачами** — создавать, отслеживать и завершать todo-списки - 🌐 **Получать данные** — HTTP-запросы, API-вызовы, веб-скрапинг - 🔧 **MCP-инструменты** — подключать любой MCP-сервер для расширенных возможностей +- 🧠 **Память** — AI запоминает факты о вас и вашей системе («Сохрани в память, что…»), читает их в следующих сессиях через `memoryRead()`. Файл памяти лежит в папке пользователя, открывается и очищается в настройках. **Как это работает:** + 1. Вы спрашиваете: "Покажи последние изменения в auth.js" 2. AI генерирует: `` `cuckoo const r = await grep('auth.js', 'login');` `` 3. Cookie Code перехватывает, выполняет, стримит результат обратно в AI @@ -86,6 +89,7 @@ ### 📊 **Интеграция Git** Встроенный просмотрщик diff: + - **Вкладка "Изменения"** — незакоммиченные изменения, построчные diff - **Вкладка "История"** — листайте коммиты, кликайте для просмотра полного diff - **Навигация** — прыгайте между hunks, фильтруйте добавленные/удалённые строки @@ -95,6 +99,7 @@ ### 📈 **Дашборд статистики** Отслеживайте продуктивность: + - **Тепловая карта активности** — график вкладов в стиле GitHub - **Использование токенов** — тренды по дням/неделям - **Любимые модели** — какой AI вы используете чаще @@ -151,6 +156,7 @@ AI: [git commit] Закоммичено: "feat(auth): добавлены тес ### Исследуйте Откройте боковую панель для: + - 🗂️ Просмотра сессий - 📝 Управления задачами - 🔄 Просмотра git diff @@ -162,19 +168,19 @@ AI: [git commit] Закоммичено: "feat(auth): добавлены тес 20+ инструментов из коробки: -| Инструмент | Описание | -|------|-------------| -| `read` | Прочитать содержимое файла | -| `write` | Создать/перезаписать файл | -| `edit` | Применить патч | -| `glob` | Найти файлы по паттерну | -| `grep` | Поиск по содержимому | -| `bash` / `pwsh` | Запуск shell-команд | -| `git` | Git-операции | -| `mysql` | SQL-запросы | -| `webFetch` | HTTP-запросы | -| `todoWrite` | Списки задач | -| `mcpCall` | Инструменты MCP-сервера | +| Инструмент | Описание | +| --------------- | -------------------------- | +| `read` | Прочитать содержимое файла | +| `write` | Создать/перезаписать файл | +| `edit` | Применить патч | +| `glob` | Найти файлы по паттерну | +| `grep` | Поиск по содержимому | +| `bash` / `pwsh` | Запуск shell-команд | +| `git` | Git-операции | +| `mysql` | SQL-запросы | +| `webFetch` | HTTP-запросы | +| `todoWrite` | Списки задач | +| `mcpCall` | Инструменты MCP-сервера | --- @@ -182,7 +188,7 @@ AI: [git commit] Закоммичено: "feat(auth): добавлены тес **Темы:** Размытие фона, прозрачность панелей, стеклянные цвета **Петы:** Бросьте PNG/GIF в `userData/pets/`, выберите из сетки -**Шрифты:** Бросьте `.ttf`/`.woff` в `userData/fonts/`, выберите из выпадающего списка +**Шрифты:** Бросьте `.ttf`/`.woff` в `userData/fonts/`, выберите из выпадающего списка --- @@ -196,6 +202,7 @@ AI: [git commit] Закоммичено: "feat(auth): добавлены тес 6. Пушьте и открывайте PR **Области, где нужна помощь:** + - 🌍 Переводы - 📚 Документация - 🐛 Отчёты об ошибках diff --git a/botsrc/i18n.js b/botsrc/i18n.js index b138418..0ef74e6 100644 --- a/botsrc/i18n.js +++ b/botsrc/i18n.js @@ -250,6 +250,22 @@ const STRINGS = { "tool.items": " пункт(ов)", "tool.allDone": "🎉 Все задачи выполнены ({n}/{n})", + // ===== Память (memory_save/read/clear) ===== + "mem.readTitle": "🧠 ИИ читает память", + "mem.saveTitle": "🧠 ИИ сохраняет в память", + "mem.clearTitle": "🧠 ИИ очищает память", + "mem.readEmpty": "🧠 Память пуста", + "mem.saved": "Записано:", + "mem.cleared": "🗑 Память очищена", + "mem.content": "Содержимое памяти:", + "mem.count": "записей: {n}", + + // ===== Время/локация (city_time) ===== + "city.title": "🕒 Время и местоположение", + "city.time": "Время", + "city.tz": "Часовой пояс", + "city.place": "Город / страна", + // ===== Approval ===== "approval.title": "🔐 Подтверждение команды", "approval.jsScript": "JS-скрипт", @@ -545,6 +561,22 @@ const STRINGS = { "tool.items": " item(s)", "tool.allDone": "🎉 All tasks completed ({n}/{n})", + // ===== Memory (memory_save/read/clear) ===== + "mem.readTitle": "🧠 AI reads memory", + "mem.saveTitle": "🧠 AI saves to memory", + "mem.clearTitle": "🧠 AI clears memory", + "mem.readEmpty": "🧠 Memory is empty", + "mem.saved": "Saved:", + "mem.cleared": "🗑 Memory cleared", + "mem.content": "Memory content:", + "mem.count": "entries: {n}", + + // ===== Time/location (city_time) ===== + "city.title": "🕒 Time & location", + "city.time": "Time", + "city.tz": "Timezone", + "city.place": "City / country", + // ===== Approval ===== "approval.title": "🔐 Command approval", "approval.jsScript": "JS script", diff --git a/botsrc/index.js b/botsrc/index.js index d941277..d2f0165 100644 --- a/botsrc/index.js +++ b/botsrc/index.js @@ -2772,7 +2772,7 @@ function formatToolArgs(toolName, args) { const old = a.old_string != null ? String(a.old_string) : ""; let s = ""; if (file) s += "📄 " + file + "\n"; - + // Построчный diff вместо двух больших блоков. if (old || neu) { try { @@ -2780,14 +2780,14 @@ function formatToolArgs(toolName, args) { const rows = buildLineDiff(old, neu); const MAX_LINES = 30; // показываем до 30 строк diff const slice = rows.slice(0, MAX_LINES); - const lines = slice.map(r => { - if (r.type === 'added') return '+ ' + r.text; - if (r.type === 'removed') return '- ' + r.text; - return ' ' + r.text; + const lines = slice.map((r) => { + if (r.type === "added") return "+ " + r.text; + if (r.type === "removed") return "- " + r.text; + return " " + r.text; }); - s += lines.join('\n'); + s += lines.join("\n"); if (rows.length > MAX_LINES) { - s += '\n' + _t("tool.truncatedTotal", { n: rows.length }); + s += "\n" + _t("tool.truncatedTotal", { n: rows.length }); } } catch (err) { // Fallback на старый формат при ошибке. @@ -3060,6 +3060,135 @@ function _techQueue(toolName, args, preview) { return null; } +// ==================== Спец-оформление для memory/city_time ==================== +// +// Память и время/локация получают собственный «человеческий» формат вместо +// общего 🔧 " + " " + "" + + // ===== Память AI ===== + '
' + + '
' + + t("settings.section.memory") + + "
" + + '
' + + ' " + + ' " + + "
" + + '
' + + t("settings.memory.hint") + + "
" + + "
" + // ===== Чубрики (петы) ===== buildPetsSection() + '
' + @@ -1704,6 +1723,69 @@ function bindFontGrid() { }); } +/** + * Секция «Память»: открыть файл памяти + очистить. + */ +function bindMemoryButtons() { + const openBtn = document.querySelector( + "#" + TAB_CONTENT_ID + " #cuckoo-memory-open-file", + ); + if (openBtn) { + openBtn.addEventListener("click", async () => { + try { + await window.electronAPI.openMemoryFile(); + } catch (err) { + console.error( + "[Cookie Code] Не удалось открыть файл памяти:", + err.message, + ); + } + }); + } + const clearBtn = document.querySelector( + "#" + TAB_CONTENT_ID + " #cuckoo-memory-clear", + ); + if (clearBtn) { + clearBtn.addEventListener("click", async () => { + const ok = window.confirm(t("settings.memory.clearConfirm")); + if (!ok) return; + try { + await window.electronAPI.clearMemory(); + await refreshMemoryStats(); + } catch (err) { + console.error("[Cookie Code] Не удалось очистить память:", err.message); + } + }); + } + refreshMemoryStats(); +} + +/** + * Обновить подпись под кнопками памяти (путь, размер, число записей). + */ +async function refreshMemoryStats() { + const el = document.querySelector( + "#" + TAB_CONTENT_ID + " #cuckoo-memory-stats", + ); + if (!el) return; + try { + const res = await window.electronAPI.getMemoryStats(); + if (!res || !res.success) return; + const kb = Math.max(1, Math.round((res.bytes || 0) / 1024)); + el.textContent = + (res.path || "") + + " · " + + (res.entries || 0) + + " " + + t("settings.memory.entries") + + " · " + + kb + + " KB"; + } catch (_) { + /* оставляем дефолтную подпись */ + } +} + /** * Кнопки «Открыть папку шрифтов» и «Обновить». */ diff --git a/src/preload/i18n/i18n.js b/src/preload/i18n/i18n.js index eaad759..92a0583 100644 --- a/src/preload/i18n/i18n.js +++ b/src/preload/i18n/i18n.js @@ -533,6 +533,22 @@ const KEYS = { ru: "Свои картинки: положите файлы (.webp/.jpg/.png/.gif) в папку и нажмите «Обновить».", en: "Custom images: drop files (.webp/.jpg/.png/.gif) into the folder and click Refresh.", }, + // ---- Память AI ---- + "settings.section.memory": { ru: "Память", en: "Memory" }, + "settings.memory.openFile": { + ru: "Открыть файл памяти", + en: "Open memory file", + }, + "settings.memory.clear": { ru: "Очистить память", en: "Clear memory" }, + "settings.memory.clearConfirm": { + ru: "Удалить все записи памяти? Действие необратимо.", + en: "Delete all memory entries? This cannot be undone.", + }, + "settings.memory.entries": { ru: "записей", en: "entries" }, + "settings.memory.hint": { + ru: "AI запоминает факты о вас и вашей системе. Файл памяти: /memory.md", + en: "The AI remembers facts about you and your system. Memory file: /memory.md", + }, // ---- Шрифт ---- "settings.section.font": { ru: "Шрифт", en: "Font" }, "settings.font.openFolder": { diff --git a/tools/JsRunner.js b/tools/JsRunner.js index 72e732b..ddf7eca 100644 --- a/tools/JsRunner.js +++ b/tools/JsRunner.js @@ -182,15 +182,24 @@ const BOOTSTRAP = [ " return await __call('read_photo', { file_path: filePath, caption: caption || '', send: send !== false });", " };", " globalThis.attachFile = async function (filePathOrPayload) {", - " var payload = typeof filePathOrPayload === 'string'", - " ? { filePath: filePathOrPayload }", - " : (filePathOrPayload || {});", - " return await __call('attach_file', payload);", - " };", - " globalThis.attachTelegram = async function (filePath, caption, comment) {", - " return await __call('attach_telegram', { filePath: filePath, caption: caption || '', comment: comment || '' });", - " };", - " globalThis.exitPlanMode = async function (plan) {", + " var payload = typeof filePathOrPayload === 'string'", + " ? { filePath: filePathOrPayload }", + " : (filePathOrPayload || {});", + " return await __call('attach_file', payload);", + " };", + " globalThis.attachTelegram = async function (filePath, caption, comment) {", + " return await __call('attach_telegram', { filePath: filePath, caption: caption || '', comment: comment || '' });", + " };", + " globalThis.memorySave = async function (text) {", + " return await __call('memory_save', { text: text });", + " };", + " globalThis.memoryRead = async function () {", + " return await __call('memory_read', {});", + " };", + " globalThis.memoryClear = async function () {", + " return await __call('memory_clear', {});", + " };", + " globalThis.exitPlanMode = async function (plan) {", " return await __call('exit_plan_mode', { plan: plan });", " };", " globalThis.exit_plan_mode = globalThis.exitPlanMode;", diff --git a/tools/MemoryTool.js b/tools/MemoryTool.js new file mode 100644 index 0000000..5d888dd --- /dev/null +++ b/tools/MemoryTool.js @@ -0,0 +1,137 @@ +const { Tool, ToolResult } = require("./ToolRegistry"); +const memoryStore = require("../src/main/memory-store"); + +/** + * memory_save — добавить одну заметку в долговременную память. + */ +class MemorySaveTool extends Tool { + constructor() { + super( + "memory_save", + "Сохранить одну заметку в долговременную память о пользователе.", + { + type: "object", + properties: { + text: { + type: "string", + description: + 'Короткая запись (одна мысль). Сохраняется как "- [дата] текст".', + }, + }, + required: ["text"], + additionalProperties: false, + }, + "memorySave(text)", + ); + } + + getPromptSection() { + return { + name: "tool:memory_save", + order: 111, + text: [ + "## Память — что это и когда использовать", + "", + "Память — это долговременный файл заметок о пользователе. В неё попадает", + "информация о пользователе, его системе или о том, что он прямо попросил", + "записать. Примеры:", + "- «Мне 10 лет», «Меня зовут Иван», «Я живу в Москве» — факты о пользователе.", + "- «У меня Windows 11, RTX 3060, VS Code, Python 3.12» — факты о его системе.", + "- «Пиши комментарии на русском», «Не трогай папку legacy/», «Используй tabs» — устойчивые предпочтения по работе.", + "- Любая прямая просьба: «Запомни, что…», «Сохрани в память…».", + "", + "Когда сохранять:", + "- Когда пользователь прямо просит что-то запомнить — сохраняй сразу, коротко и от третьего лица (memorySave('Пользователю 10 лет')).", + "- Когда в диалоге всплывает устойчивый факт о пользователе или его окружении, который пригодится в будущих сессиях, — сохраняй без напоминаний.", + "- Не сохраняй временное (текущая задача, разовый путь, содержимое файла), секреты, токены, пароли.", + "", + "Когда читать:", + "- Если ответ зависит от данных о пользователе/системе — сначала memoryRead().", + "- Не вызывай memoryRead() на каждый запрос; только когда это уместно.", + "", + "Формат и правила:", + '- memorySave(text) — добавляет одну строку "- [дата] текст". Одна запись — одна мысль. Без markdown-заголовков и переводов строк.', + "- Дубликаты не создавай: если запись уже есть — пропусти или переформулируй.", + "- memoryClear() — полная очистка. Только по прямой просьбе пользователя. Перед вызовом — предупреди.", + "- Память не попадает в системный промпт автоматически: её содержимое видно только после явного memoryRead().", + ].join("\n"), + }; + } + + async execute(params) { + const { text } = params; + const r = memoryStore.appendMemory(text); + if (!r.ok) + return ToolResult.error("Не удалось сохранить в память: " + r.error); + return ToolResult.success("Сохранено в память: " + r.entry); + } +} + +/** + * memory_read — прочитать всю память. + */ +class MemoryReadTool extends Tool { + constructor() { + super( + "memory_read", + "Прочитать долговременную память (заметки о пользователе).", + { + type: "object", + properties: {}, + additionalProperties: false, + }, + "memoryRead()", + ); + } + + getPromptSection() { + return { + name: "tool:memory_read", + order: 112, + text: "Вызывай memoryRead(), когда ответ зависит от данных о пользователе/его системе. Не вызывай на каждый запрос.", + }; + } + + async execute() { + const raw = memoryStore.readMemory(); + if (!raw || !raw.trim()) { + return ToolResult.success("(память пуста)"); + } + return ToolResult.success(raw); + } +} + +/** + * memory_clear — полностью очистить память. + */ +class MemoryClearTool extends Tool { + constructor() { + super( + "memory_clear", + "Полностью очистить долговременную память. Только по прямой просьбе пользователя.", + { + type: "object", + properties: {}, + additionalProperties: false, + }, + "memoryClear()", + ); + } + + getPromptSection() { + return { + name: "tool:memory_clear", + order: 113, + text: "memoryClear() удаляет все записи памяти. Используй только по прямой просьбе пользователя и предупреди его перед вызовом.", + }; + } + + async execute() { + const r = memoryStore.clearMemory(); + if (!r.ok) + return ToolResult.error("Не удалось очистить память: " + r.error); + return ToolResult.success("Память очищена."); + } +} + +module.exports = { MemorySaveTool, MemoryReadTool, MemoryClearTool }; diff --git a/tools/cuckoo-tools.d.ts b/tools/cuckoo-tools.d.ts index eb902dc..3461f61 100644 --- a/tools/cuckoo-tools.d.ts +++ b/tools/cuckoo-tools.d.ts @@ -375,6 +375,34 @@ declare function mcpListServers(): Promise; */ declare function mcpGetTools(serverName: string): Promise; +// ================= Memory ================= + +/** + * Сохранить одну заметку в долговременную память (файл /memory.md). + * Запись добавляется строкой "- [YYYY-MM-DD] текст". + * Используется для фактов о пользователе, его системе и устойчивых предпочтений. + * @param text короткая заметка (одна мысль); длиннее 2000 символов — обрезается + * @returns подтверждение с сохранённой строкой + * @throws text не строка/пустой, ошибка записи файла + */ +declare function memorySave(text: string): Promise; + +/** + * Прочитать всю долговременную память (файл /memory.md). + * Вызывай, только если ответ зависит от данных о пользователе/его системе. + * @returns содержимое файла или "(память пуста)" + * @throws ошибка чтения файла + */ +declare function memoryRead(): Promise; + +/** + * Полностью очистить долговременную память. + * Используй только по прямой просьбе пользователя. + * @returns подтверждение очистки + * @throws ошибка записи файла + */ +declare function memoryClear(): Promise; + // ================= Skill ================= /** diff --git a/tools/index.js b/tools/index.js index 4fa7deb..e2fdf14 100644 --- a/tools/index.js +++ b/tools/index.js @@ -37,6 +37,11 @@ const { ReadPhotoTool } = require("./ReadPhotoTool"); const { ExitPlanModeTool } = require("./ExitPlanModeTool"); const { AttachFileTool } = require("./AttachFileTool"); const { AttachTelegramTool } = require("./AttachTelegramTool"); +const { + MemorySaveTool, + MemoryReadTool, + MemoryClearTool, +} = require("./MemoryTool"); // 创建全局工具注册表 const registry = new ToolRegistry(); @@ -76,6 +81,9 @@ registry.register(new ReadPhotoTool()); registry.register(new ExitPlanModeTool()); registry.register(new AttachFileTool()); registry.register(new AttachTelegramTool()); +registry.register(new MemorySaveTool()); +registry.register(new MemoryReadTool()); +registry.register(new MemoryClearTool()); // 导出 module.exports = { @@ -110,8 +118,11 @@ module.exports = { ReadPhotoTool, ExitPlanModeTool, AttachFileTool, - AttachTelegramTool, - InjectPageJSTool, + AttachTelegramTool, + InjectPageJSTool, + MemorySaveTool, + MemoryReadTool, + MemoryClearTool, // 便捷方法 getAllTools: () => registry, getToolDescriptions: () => registry.getDescriptions(), From 17b6096657c1b96f6a22a7257d9dcdfb6fd7fa3e Mon Sep 17 00:00:00 2001 From: MerfiDEV Date: Tue, 6 Oct 2026 17:18:54 +0300 Subject: [PATCH 02/11] feat(agents): add subagent delegation with run_agent tool Implement subagent orchestration: run_agent/list_agents/read_agent tools, project agent scanner (.cuckoo/agents), child-window runner sharing the parent partition, and preload watcher that reports the final answer via IPC. Inject the AGENTS_SECTION placeholder into prompt templates and expose runAgent/listAgents/readAgent in the JS runner. --- .cuckoo/agents/code-reviewer.md | 20 +++ .cuckoo/agents/explore.md | 18 ++ .cuckoo/agents/git-committer.md | 144 ++++++++++++++++ src/main/agents-window-state.js | 12 ++ src/main/agents/frontmatter.js | 81 +++++++++ src/main/agents/index.js | 12 ++ src/main/agents/prompt.js | 40 +++++ src/main/agents/scanner.js | 117 +++++++++++++ src/main/agents/types.js | 24 +++ src/main/index.js | 53 +++++- src/main/ipc.js | 22 +++ src/main/project-context.js | 16 ++ src/main/subagent.js | 275 ++++++++++++++++++++++++++++++ src/main/window.js | 30 +++- src/preload/dom/subagent-watch.js | 154 +++++++++++++++++ src/preload/index.js | 7 +- src/prompt/deepseek.md | 25 ++- src/prompt/default.md | 23 ++- tools/AgentTools.js | 100 +++++++++++ tools/JsRunner.js | 15 ++ tools/RunAgentTool.js | 93 ++++++++++ tools/index.js | 8 + 22 files changed, 1266 insertions(+), 23 deletions(-) create mode 100644 .cuckoo/agents/code-reviewer.md create mode 100644 .cuckoo/agents/explore.md create mode 100644 .cuckoo/agents/git-committer.md create mode 100644 src/main/agents-window-state.js create mode 100644 src/main/agents/frontmatter.js create mode 100644 src/main/agents/index.js create mode 100644 src/main/agents/prompt.js create mode 100644 src/main/agents/scanner.js create mode 100644 src/main/agents/types.js create mode 100644 src/main/subagent.js create mode 100644 src/preload/dom/subagent-watch.js create mode 100644 tools/AgentTools.js create mode 100644 tools/RunAgentTool.js diff --git a/.cuckoo/agents/code-reviewer.md b/.cuckoo/agents/code-reviewer.md new file mode 100644 index 0000000..88eca36 --- /dev/null +++ b/.cuckoo/agents/code-reviewer.md @@ -0,0 +1,20 @@ +--- +name: code-reviewer +description: Проверка качества кода. Использовать после написания кода. +tools: read, readLines, glob, grep +maxTurns: 20 +--- + +Ты — опытный эксперт по ревью кода. Проверь указанный код, найди реальные проблемы и дай рекомендации. + +## Что смотреть + +1. Корректность (edge cases, ошибки логики) +2. Безопасность (инъекции, утечки, небезопасные API) +3. Сопровождаемость (дублирование, сложность) + +## Формат + +- Сначала критичные проблемы +- Затем второстепенные +- Для каждой: файл:строка + конкретное предложение diff --git a/.cuckoo/agents/explore.md b/.cuckoo/agents/explore.md new file mode 100644 index 0000000..35e8f09 --- /dev/null +++ b/.cuckoo/agents/explore.md @@ -0,0 +1,18 @@ +--- +name: explore +description: Только чтение — исследование кодовой базы: широкий поиск, локализация, анализ структуры. Ничего не изменяет. +tools: read, readLines, glob, grep +--- + +Ты — эксперт по исследованию кодовой базы. Работаешь только на чтение, **никогда** не изменяй файлы. + +## Как работаешь + +- Сначала glob/grep — понять структуру, затем читать ключевые файлы +- Выдаёшь сжатые выводы (они вернутся в родительский диалог как резюме) + +## Формат вывода + +- Сначала вывод +- Ключевые места: файл:строка +- Неуверенные места помечай явно diff --git a/.cuckoo/agents/git-committer.md b/.cuckoo/agents/git-committer.md new file mode 100644 index 0000000..25e1b9b --- /dev/null +++ b/.cuckoo/agents/git-committer.md @@ -0,0 +1,144 @@ +--- +name: git-committer +description: Профи по Git-коммитам и push. Анализирует изменения, пишет осмысленный commit message в Conventional Commits, безопасно пушит. Использовать когда нужно закоммитить/запушить работу. +tools: read, readLines, bash, grep, glob, todoWrite +maxTurns: 40 +--- + +Ты — эксперт по Git workflow. Твоя задача — превратить текущие изменения в чистую, +осмысленную историю коммитов и безопасно отправить её в удалённый репозиторий. + +## Принципы + +- **Никогда не коммить мусор.** Перед коммитом всегда проверяй, что попадает в индекс. +- **Никогда не пушь force без явной просьбы пользователя.** По умолчанию — обычный push. +- **Не коммить секреты.** Проверяй diff на .env, ключи, токены, пароли. +- **Атомарность.** Если изменения разнородны — разбей на несколько коммитов по смыслу. +- **Conventional Commits.** Формат: `type(scope): subject`. + +## Алгоритм работы + +### 1. Разведка + +```cuckoo +await bash("git status"); +await bash("git diff --stat"); +await bash("git diff --staged"); +await bash("git log --oneline -10"); +``` + +Пойми: + +- Какие файлы изменены/новые/удалены +- Есть ли чужие незакоммиченные изменения (не твои) +- В какой ветке работаешь +- Стиль сообщений в истории (чтобы не ломать конвенцию проекта) + +### 2. Проверка безопасности + +Перед `git add`: + +- Убедись, что `.gitignore` покрывает `node_modules/`, `dist/`, `.env`, временные файлы. +- Если видишь файл, похожий на секрет (строки `API_KEY=`, `token:`, `-----BEGIN`, длинные base64) — **остановись** и сообщи пользователю. +- Не коммить файлы >1 МБ без подтверждения. + +### 3. Группировка и staging + +Если изменения разнородны — группируй: + +```cuckoo +await bash("git add path/to/feature-files"); // только фича +``` + +Не используй `git add .` вслепую, если в `git status` есть чужие файлы. + +### 4. Commit message (Conventional Commits) + +Формат: + +``` +type(scope): краткое описание в императиве + +Опциональное тело: что и зачем (не как). Максимум 72 символа в строке. +Футеры: Closes #123, Co-authored-by: ... +``` + +**Типы:** + +| Тип | Когда | +| ---------- | ---------------------------------------- | +| `feat` | новая функциональность | +| `fix` | исправление бага | +| `refactor` | рефакторинг без изменения поведения | +| `perf` | оптимизация производительности | +| `docs` | документация | +| `test` | тесты | +| `chore` | сборка, зависимости, конфиги | +| `style` | форматирование, пробелы, точки с запятой | +| `build` | сборка, CI | +| `ci` | CI-конфиги | + +**Правила subject:** + +- Императив: «add», «fix», «remove» (не «added», «fixes»). +- Строчная первая буква, без точки в конце. +- До 72 символов. +- Английский (если в истории проекта не принят другой язык). + +**Примеры хороших:** + +- `feat(auth): add JWT refresh token rotation` +- `fix(chat): prevent duplicate messages on reconnect` +- `refactor(tools): extract registry into separate module` +- `chore(deps): bump electron to 33.4.11` + +**Плохие (не делай):** + +- `update code` (нет типа, нет смысла) +- `fix: fixed the bug` (прошедшее время) +- `WIP` (никогда не коммить WIP в основную ветку) +- `feat: added new feature and refactored stuff and fixed bug` (многоцелевое) + +### 5. Коммит + +```cuckoo +await bash('git commit -m "feat(scope): subject" -m "Optional body."'); +``` + +При многострочном — используй два `-m`. Не пихай `\n` в одну строку. + +### 6. Push + +```cuckoo +await bash("git rev-parse --abbrev-ref HEAD"); // текущая ветка +await bash("git push"); // обычный push +``` + +- Первый push новой ветки: `git push -u origin `. +- **Никогда не делай `git push --force`** без прямой просьбы пользователя. +- Если push отклонён (`non-fast-forward`) — **не форсируй**. Скажи пользователю и предложи `git pull --rebase`. + +## Что ты НЕ делаешь + +- Не пуш в `main`/`master` напрямую, если проект работает через PR (проверь по истории `git log --first-parent`). +- Не меняешь уже существующие коммиты (`git commit --amend` только если пользователь явно просит). +- Не резолвишь конфликты молча — сообщай. +- Не делаешь `git reset --hard` без предупреждения. +- Не создаёшь коммиты с сообщениями типа «wip», «fix», «temp». + +## Формат отчёта в родительский диалог + +Верни **краткую** сводку: + +``` +Закоммичено и запушено. + +Коммиты: +- abc1234 feat(auth): add JWT refresh token rotation (3 файла) +- def5678 fix(chat): prevent duplicate messages on reconnect (1 файл) + +Ветка: feature/auth-jwt +Удалённый: origin/feature/auth-jwt (обновлён) +``` + +Если что-то не сделал (например, остановился из-за секрета) — чётко напиши почему и что нужно от пользователя. diff --git a/src/main/agents-window-state.js b/src/main/agents-window-state.js new file mode 100644 index 0000000..4fdfcb2 --- /dev/null +++ b/src/main/agents-window-state.js @@ -0,0 +1,12 @@ +/** + * Определение «окно является субагентом» — для защиты от рекурсии. + * Достаётся из windowState (поле isSubagent в контексте окна). + */ +const windowState = require("./window"); + +function isSubagentWindow(windowId) { + const ctx = windowState.getWindowContext(windowId); + return !!(ctx && ctx.isSubagent); +} + +module.exports = { isSubagentWindow }; diff --git a/src/main/agents/frontmatter.js b/src/main/agents/frontmatter.js new file mode 100644 index 0000000..ad92588 --- /dev/null +++ b/src/main/agents/frontmatter.js @@ -0,0 +1,81 @@ +/** + * Парсер YAML-frontmatter (минимальный, без внешних зависимостей). + * + * Ожидаемый формат: + * --- + * name: value + * description: some text + * tools: read, grep, glob + * maxTurns: 20 + * --- + * <тело> + * + * Поддерживает: строки (без кавычек), строки в одинарных/двойных кавычках, + * массивы в виде "- item" на отдельной строке, массив через запятую в одной + * строке. Вложенные объекты и блоки "|" НЕ поддерживаются (нам не нужны). + */ + +function stripQuotes(s) { + if (typeof s !== "string") return s; + const t = s.trim(); + if (t.length >= 2) { + const a = t[0], + b = t[t.length - 1]; + if ((a === '"' && b === '"') || (a === "'" && b === "'")) { + return t.slice(1, -1); + } + } + return s; +} + +/** + * Распарсить frontmatter. + * @param {string} raw + * @returns {{data: Object, body: string}} + */ +function parseFrontmatter(raw) { + const text = String(raw || ""); + // Frontmatter должен начинаться с первой строки "---" + if (!/^---\s*\r?\n/.test(text)) { + return { data: {}, body: text }; + } + const rest = text.replace(/^---\s*\r?\n/, ""); + const endMatch = rest.match(/\r?\n---\s*(\r?\n|$)/); + if (!endMatch) return { data: {}, body: text }; + const fmRaw = rest.slice(0, endMatch.index); + const body = rest.slice(endMatch.index + endMatch[0].length); + + const data = {}; + let lastKey = null; + for (const rawLine of fmRaw.split(/\r?\n/)) { + if (!rawLine.trim()) continue; + // Массив "- item" + if (/^\s*-\s+/.test(rawLine) && lastKey) { + const item = stripQuotes(rawLine.replace(/^\s*-\s+/, "")); + if (!Array.isArray(data[lastKey])) data[lastKey] = []; + data[lastKey].push(item); + continue; + } + const m = rawLine.match(/^([A-Za-z0-9_-]+)\s*:\s*(.*)$/); + if (!m) continue; + const key = m[1]; + const valueRaw = m[2]; + lastKey = key; + if (valueRaw === "") { + data[key] = ""; + continue; + } + // Массив в одну строку через запятую (для tools) + if (key === "tools" || key === "paths") { + data[key] = valueRaw + .split(",") + .map((s) => stripQuotes(s.trim())) + .filter(Boolean); + } else { + data[key] = stripQuotes(valueRaw); + } + } + return { data, body }; +} + +module.exports = { parseFrontmatter }; diff --git a/src/main/agents/index.js b/src/main/agents/index.js new file mode 100644 index 0000000..a186f12 --- /dev/null +++ b/src/main/agents/index.js @@ -0,0 +1,12 @@ +/** + * Точка входа модуля субагентов. + */ +const { scanAgents, mergeAgents, scanAgentsRoot } = require("./scanner"); +const { buildAgentsSection } = require("./prompt"); + +module.exports = { + scanAgents, + mergeAgents, + scanAgentsRoot, + buildAgentsSection, +}; diff --git a/src/main/agents/prompt.js b/src/main/agents/prompt.js new file mode 100644 index 0000000..b502d72 --- /dev/null +++ b/src/main/agents/prompt.js @@ -0,0 +1,40 @@ +/** + * Секция "Доступные субагенты" для системного промпта. + * + * Прогрессивное раскрытие: только name + description. Как именно вызывать — + * описано в prompt-section самого инструмента run_agent. + */ + +function formatAgentLine(a) { + return "- " + a.name + ":" + a.description; +} + +/** + * Собрать секцию. Пустой список -> пустая строка (не занимает промпт). + * @param {Array<{name:string, description:string}>} agents + * @returns {string} + */ +function buildAgentsSection(agents) { + const list = Array.isArray(agents) ? agents : []; + if (list.length === 0) return ""; + const lines = []; + lines.push("## Доступные субагенты (Agents)"); + lines.push(""); + lines.push( + "Когда задача подходит для делегирования, вызови run_agent(name, task),", + ); + lines.push( + "чтобы передать её соответствующему субагенту. Субагент работает в отдельном", + ); + lines.push("контексте и возвращает только итоговое резюме."); + lines.push( + "Подходит для делегирования: широкий поиск/анализ, независимые подзадачи —", + ); + lines.push("чтобы не засорять текущий контекст."); + lines.push(""); + for (const a of list) lines.push(formatAgentLine(a)); + lines.push(""); + return lines.join("\n"); +} + +module.exports = { buildAgentsSection }; diff --git a/src/main/agents/scanner.js b/src/main/agents/scanner.js new file mode 100644 index 0000000..b455475 --- /dev/null +++ b/src/main/agents/scanner.js @@ -0,0 +1,117 @@ +/** + * Сканер субагентов: проектные + пользовательские каталоги -> AgentMeta[]. + * + * Правила: + * - Сканируется только /.cuckoo/agents/ — один уровень, только .md + * - Имя: frontmatter.name || basename(file, '.md') + * - Описание: frontmatter.description || первый непустой абзац тела + * - Без имени/описания — агент пропускается (нельзя показать в промпте) + * - При совпадении имён: project > user + */ +const fs = require("fs"); +const path = require("path"); +const os = require("os"); +const { parseFrontmatter } = require("./frontmatter"); + +/** Первый непустой не-заголовочный абзац тела -> fallback description */ +function firstParagraph(body) { + for (const raw of String(body || "").split(/\r?\n/)) { + const line = raw.trim(); + if (!line) continue; + if (line.startsWith("#")) continue; + return line; + } + return ""; +} + +/** + * Просканировать одну папку agents (без рекурсии). + * @param {string} agentsDir абсолютный путь к .cuckoo/agents + * @param {'project'|'user'} source + * @returns {AgentMeta[]} + */ +function scanAgentsRoot(agentsDir, source) { + let entries; + try { + entries = fs.readdirSync(agentsDir, { withFileTypes: true }); + } catch (_) { + return []; + } + const out = []; + for (const ent of entries) { + if (!ent.isFile()) continue; + if (!ent.name.toLowerCase().endsWith(".md")) continue; + const agentPath = path.join(agentsDir, ent.name); + let raw = ""; + try { + raw = fs.readFileSync(agentPath, "utf-8"); + } catch (_) { + continue; + } + const { data, body } = parseFrontmatter(raw); + const name = + (data.name && String(data.name).trim()) || ent.name.replace(/\.md$/i, ""); + const description = String( + data.description || firstParagraph(body) || "", + ).trim(); + if (!name.trim() || !description) continue; + + let tools; + if (data.tools) { + const arr = Array.isArray(data.tools) + ? data.tools + : String(data.tools) + .split(",") + .map((s) => s.trim()) + .filter(Boolean); + tools = arr.map((s) => String(s).trim()).filter(Boolean); + if (tools.length === 0) tools = undefined; + } + + let maxTurns; + if (data.maxTurns != null && data.maxTurns !== "") { + const n = Number(data.maxTurns); + if (Number.isInteger(n) && n > 0 && n <= 1000) maxTurns = n; + } + + out.push({ + name: name.trim(), + description, + tools, + maxTurns, + agentPath, + systemPrompt: String(body || "").trim(), + source, + }); + } + return out; +} + +/** Слить списки по имени: project > user (plugin не поддержан) */ +function mergeAgents(project, user) { + const byName = new Map(); + for (const a of user) byName.set(a.name, a); + for (const a of project) byName.set(a.name, a); + return Array.from(byName.values()); +} + +/** + * Просканировать все каталоги субагентов. + * @param {string|null} projectDir корень проекта (может быть null) + * @returns {AgentMeta[]} + */ +function scanAgents(projectDir) { + let userHome = ""; + try { + userHome = os.homedir(); + } catch (_) {} + const user = userHome + ? scanAgentsRoot(path.join(userHome, ".cuckoo", "agents"), "user") + : []; + const project = projectDir + ? scanAgentsRoot(path.join(projectDir, ".cuckoo", "agents"), "project") + : []; + return mergeAgents(project, user); +} + +module.exports = { scanAgents, mergeAgents, scanAgentsRoot }; diff --git a/src/main/agents/types.js b/src/main/agents/types.js new file mode 100644 index 0000000..ac1a770 --- /dev/null +++ b/src/main/agents/types.js @@ -0,0 +1,24 @@ +/** + * Agent (субагент) — типы и документация. + * + * Формат: markdown-файл с frontmatter, размещённый в: + * - /.cuckoo/agents/.md (проектный, приоритет выше) + * - ~/.cuckoo/agents/.md (пользовательский) + * + * Frontmatter-поля: + * - name : имя агента (по умолчанию — имя файла без .md) + * - description : когда делегировать (обязательно, иначе агент пропускается) + * - tools : список разрешённых инструментов через запятую (по умолчанию — все) + * - maxTurns : лимит шагов 1..1000 (по умолчанию — без лимита) + * + * @typedef {Object} AgentMeta + * @property {string} name + * @property {string} description + * @property {string[]|undefined} tools + * @property {number|undefined} maxTurns + * @property {string} agentPath — абсолютный путь к файлу + * @property {string} systemPrompt — тело без frontmatter + * @property {'project'|'user'} source + */ + +module.exports = {}; diff --git a/src/main/index.js b/src/main/index.js index 5fde39f..41fc587 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -107,6 +107,12 @@ if (RENDERER_LOG_DIR) { const { registerIpcHandlers } = require("./ipc"); const whatsNew = require("./whats-new"); +const subagent = require("./subagent"); +const { + injectAgentRunner, + injectSubagentChecker, +} = require("../../tools/RunAgentTool"); +const { isSubagentWindow } = require("./agents-window-state"); // Whats-new: проверить факт обновления ДО создания окна. // Внутри — атомарное сохранение новой lastSeenVersion, чтобы повторный @@ -146,10 +152,19 @@ function createWindow(profile) { windowState, ); + // Субагент-конфиг передаётся дочернему окну через additionalArguments. + // Bridge в preload сам распознаёт, что это окно-субагент, и запускает цикл. + const subagentArg = profileData.subagentConfig + ? "--cuckoo-subagent=" + + encodeURIComponent(JSON.stringify(profileData.subagentConfig)) + : null; + const mainWindow = new BrowserWindow({ width: 1280, height: 900, - title: "Cookie Code Pro - " + provider.name + " - " + profileData.name, + title: profileData.isSubagent + ? "Cookie Code Pro - " + provider.name + " - " + profileData.name + : "Cookie Code Pro - " + provider.name + " - " + profileData.name, icon: APP_ICON, webPreferences: { preload: path.join(__dirname, "..", "..", "preload.js"), @@ -158,7 +173,9 @@ function createWindow(profile) { sandbox: false, partition: profileData.partition, // 每个 profile 独立持久化 session backgroundThrottling: false, - additionalArguments: ["--cuckoo-user-data=" + app.getPath("userData")], + additionalArguments: subagentArg + ? ["--cuckoo-user-data=" + app.getPath("userData"), subagentArg] + : ["--cuckoo-user-data=" + app.getPath("userData")], }, }); @@ -166,7 +183,14 @@ function createWindow(profile) { const winSession = mainWindow.webContents.session; // Регистрируем контекст окна — providerId жёстко 'deepseek'. - windowState.addWindow(mainWindow, profileData.id, "deepseek", sessionStore); + windowState.addWindow( + mainWindow, + profileData.id, + "deepseek", + sessionStore, + profileData.isSubagent, + profileData.subagentConfig, + ); sessionsToFlush.add(winSession); // 更新主窗口引用 @@ -281,6 +305,9 @@ function createWindow(profile) { sessionsToFlush.delete(winSession); windowState.removeWindow(mainWindow.id); }); + + // Возвращаем id окна: нужно для субагента (родитель ждёт дочернее окно по id). + return mainWindow.id; } // ========== 应用菜单 ========== @@ -412,6 +439,26 @@ function setupAppMenu() { // ========== IPC 处理器 ========== registerIpcHandlers(); +// ========== Субагенты (Agents) ========== +// Инжектим в tool runAgent реальный раннер и определитель «окно-субагент», +// чтобы tools-слой не зависел от main-слоя (инверсия зависимости). +subagent.injectSubagentDeps({ createWindow, profileManager }); +injectAgentRunner(async ({ agent, task, currentWindowId, projectDir }) => { + const ctx = windowState.getWindowContext(currentWindowId); + if (!ctx) throw new Error("Контекст окна не найден"); + return subagent.runAgent({ + parentProfileId: ctx.profileId, + parentWindowId: currentWindowId, + agentName: agent.name, + task, + systemPrompt: agent.systemPrompt, + tools: agent.tools, + maxTurns: agent.maxTurns, + projectDir, // приоритетный источник рабочей папки + }); +}); +injectSubagentChecker(isSubagentWindow); + // ========== Telegram-бот (botsrc/) — применяем настройки при старте ========== try { require("../../botsrc") diff --git a/src/main/ipc.js b/src/main/ipc.js index 8714a6f..42c2a22 100644 --- a/src/main/ipc.js +++ b/src/main/ipc.js @@ -19,6 +19,7 @@ const { decodeOutput, normalizeCommand } = require("../../tools/decodeOutput"); const gitDiff = require("./git-diff"); const todoStore = require("./todo-store"); const planMode = require("./plan-mode"); +const subagent = require("./subagent"); /** * Если у окна все задачи выполнены (и список непустой) — пингуем Telegram один раз. @@ -328,6 +329,22 @@ function tryOpenInVSCode(filePath) { } function registerIpcHandlers() { + // ===== Субагенты: bridge дочернего окна сообщает финальный текст ===== + ipcMain.handle("subagent-response", async (event, payload) => { + try { + const ctx = windowState.getContextByWebContents(event.sender); + if (ctx && ctx.win && !ctx.win.isDestroyed()) { + subagent.onSubagentResponse( + ctx.win.id, + (payload && payload.text) || "", + ); + } + return { success: true }; + } catch (err) { + return { success: false, error: err.message }; + } + }); + // Мост Telegram → окно: ответ на вопрос из TG резолвит тот же promise. try { const bot = require("../../botsrc"); @@ -905,6 +922,8 @@ function registerIpcHandlers() { ...params, projectDir: selectedDir, senderId: event.sender.id, + currentWindowId: + ctx && ctx.win && !ctx.win.isDestroyed() ? ctx.win.id : null, askUserQuestion: (questions) => requestUserQuestion(event.sender, questions), pasteImage: (filePath, caption, send) => @@ -1087,6 +1106,8 @@ function registerIpcHandlers() { const ctx = windowState.getContextByWebContents(event.sender); const store = ctx ? ctx.sessionStore : null; const selectedDir = store ? store.state.selectedProjectDir : null; + const currentWindowId = + ctx && ctx.win && !ctx.win.isDestroyed() ? ctx.win.id : null; const taskToken = beginTask(event.sender.id); try { const before = JSON.stringify(todoStore.getList(event.sender.id)); @@ -1100,6 +1121,7 @@ function registerIpcHandlers() { (plan) => requestExitPlanMode(event.sender, plan), sessionIdOf(event), (payload) => attachFileToChat(event.sender, payload), + currentWindowId, ); if (isTaskCanceled(event.sender.id, taskToken)) { return { diff --git a/src/main/project-context.js b/src/main/project-context.js index 1285647..06c5fb4 100644 --- a/src/main/project-context.js +++ b/src/main/project-context.js @@ -438,6 +438,21 @@ async function buildInitPrompt(selectedDir, providerId) { "Skill 位于 /.cuckoo/skills//,其中 SKILL.md 为指令文件,tool.js 可选导出可执行函数。", ].join("\n"); + // Agents (субагенты) — прогрессивное раскрытие: только name + description. + // Сами определения (systemPrompt, tools, maxTurns) живут в .cuckoo/agents/*.md + // и подгружаются в момент вызова run_agent. + let agentsSection = ""; + try { + const { scanAgents, buildAgentsSection } = require("./agents"); + const agents = scanAgents(selectedDir); + agentsSection = buildAgentsSection(agents); + if (agents.length > 0) { + console.log("[Cookie Code] Найдено субагентов:", agents.length); + } + } catch (err) { + console.error("[Cookie Code] Ошибка сканирования субагентов:", err.message); + } + const placeholders = { "{{TOOL_API_TYPES}}": toolApiTypes, "{{TOOLS_LIST}}": toolsDescription, @@ -447,6 +462,7 @@ async function buildInitPrompt(selectedDir, providerId) { "{{PROJECT_INTRO_SECTION}}": projectIntroSection, "{{SKILL_SECTION}}": skillSection, "{{MCP_SECTION}}": mcpSection, + "{{AGENTS_SECTION}}": agentsSection, }; let combined = templateContent; for (const [key, value] of Object.entries(placeholders)) { diff --git a/src/main/subagent.js b/src/main/subagent.js new file mode 100644 index 0000000..d7a5fce --- /dev/null +++ b/src/main/subagent.js @@ -0,0 +1,275 @@ +/** + * Раннер субагентов (оркестрация на стороне main-процесса). + * + * Алгоритм: + * 1) создать дочернее окно с тем же partition (общая сессия DeepSeek) + * 2) проставить в его sessionStore projectDir + pendingLineage + * 3) передать конфиг через additionalArguments (--cuckoo-subagent=...) + * 4) ждать IPC subagent-response (или таймаут) + * 5) закрыть окно, вернуть текст родителю + * + * Зависимости (createWindow) инжектируются из index.js — чтобы избежать + * циклического require main → subagent → main. + */ +const windowState = require("./window"); +const { buildInitPrompt } = require("./project-context"); + +let _createWindow = null; +let _profileManager = null; + +/** Инжектировать зависимости из index.js */ +function injectSubagentDeps(deps) { + _createWindow = deps.createWindow; + _profileManager = deps.profileManager; +} + +/** Ожидающие окна: windowId -> resolve(text) */ +const pending = new Map(); + +/** Счётчик работающих субагентов на profileId (для блокировки уведомлений и т.п.) */ +const runningByProfile = new Map(); + +function hasRunningSubagent(profileId) { + return !!profileId && (runningByProfile.get(profileId) || 0) > 0; +} + +function incRunning(profileId) { + if (!profileId) return; + runningByProfile.set(profileId, (runningByProfile.get(profileId) || 0) + 1); +} + +function decRunning(profileId) { + if (!profileId) return; + const n = (runningByProfile.get(profileId) || 0) - 1; + if (n <= 0) runningByProfile.delete(profileId); + else runningByProfile.set(profileId, n); +} + +/** Вызывается из IPC при получении subagent-response */ +function onSubagentResponse(windowId, text) { + const resolve = pending.get(windowId); + if (resolve) { + pending.delete(windowId); + resolve(String(text || "")); + } +} + +/** + * Подготовить финальный промпт субагента: + * полный системный промпт Cookie Code (инструменты, формат cuckoo) + * + секция агента (systemPrompt + task). + * Отправляется в дочернее окно через initial-prompt (прелоад слушает канал). + */ +function buildAgentPrompt(agentSystemPrompt, task) { + const parts = []; + if (agentSystemPrompt) parts.push(String(agentSystemPrompt).trim()); + parts.push(""); + parts.push("---"); + parts.push("Задача: " + task); + parts.push(""); + parts.push( + "Когда закончишь, дай сразу финальный результат (не вызывай больше инструментов).", + ); + return parts.join("\n"); +} + +/** + * Дождаться готовности дочернего окна и отправить в него initial-prompt + * (system prompt Cookie Code + промпт агента + task). + */ +function sendInitPromptWhenReady(win, projectDir, extraPrompt, timeoutMs) { + return new Promise((resolve) => { + if (!win || win.isDestroyed()) return resolve(false); + let sent = false; + const send = async () => { + if (sent) return; + if (!win || win.isDestroyed()) return; + try { + const base = await buildInitPrompt( + projectDir || process.cwd(), + "deepseek", + ); + const combined = base + "\n\n" + extraPrompt; + win.webContents.send("initial-prompt", combined); + sent = true; + console.log( + "[Subagent] initial-prompt отправлен в окно (len=" + + combined.length + + ")", + ); + resolve(true); + } catch (err) { + console.error( + "[Subagent] не удалось собрать initial-prompt:", + err && err.message, + ); + resolve(false); + } + }; + win.webContents.once("did-finish-load", () => setTimeout(send, 1200)); + // На случай, если страница уже загрузилась до подписки + setTimeout(send, 3000); + // Абсолютный таймаут + setTimeout( + () => { + if (!sent) resolve(false); + }, + Math.min(timeoutMs, 60000), + ); + }); +} + +/** Ожидание завершения субагента с таймаутом */ +function waitForDone(windowId, timeoutMs) { + return new Promise((resolve) => { + let done = false; + const timer = setTimeout(() => { + if (done) return; + done = true; + pending.delete(windowId); + resolve("__SUBAGENT_TIMEOUT__"); + }, timeoutMs); + pending.set(windowId, (text) => { + if (done) return; + done = true; + clearTimeout(timer); + resolve(text); + }); + }); +} + +/** + * Запустить субагента. + * @param {Object} opts + * @param {string} opts.parentProfileId — profileId родителя (общий partition) + * @param {number} opts.parentWindowId — windowId родителя + * @param {string} opts.agentName + * @param {string} opts.task + * @param {string} opts.systemPrompt + * @param {string[]|undefined} opts.tools + * @param {number|undefined} opts.maxTurns + * @param {string} [opts.projectDir] — рабочая папка (приоритетно из RunAgentTool) + * @param {number} [opts.timeoutMs=600000] + * @returns {Promise} + */ +async function runAgent(opts) { + if (!_createWindow || !_profileManager) + throw new Error("Зависимости субагента не инжектированы"); + const timeoutMs = opts.timeoutMs || 600000; // 10 минут + + const parentCtx = windowState.getWindowContext(opts.parentWindowId); + if (!parentCtx) throw new Error("Контекст родительского окна не найден"); + const parentStore = parentCtx.sessionStore; + + // Рабочая папка: приоритет — явно переданная из RunAgentTool, + // fallback — из sessionStore родителя. + const projectDir = + opts.projectDir || + (parentStore && + parentStore.state && + parentStore.state.selectedProjectDir) || + null; + + if (!projectDir) { + throw new Error( + "Не задана рабочая папка проекта. Инициализируй проект в родительском окне (кнопка «Инициализировать проект») и повтори.", + ); + } + // Проверяем, что папка существует и это каталог — иначе субагент не сможет читать. + try { + const fs = require("fs"); + const st = fs.statSync(projectDir); + if (!st.isDirectory()) throw new Error("не каталог"); + } catch (err) { + throw new Error( + "Рабочая папка недоступна: " + + projectDir + + " (" + + (err && err.message) + + ")", + ); + } + + const parentSessionId = + (parentStore && parentStore.state && parentStore.state.currentSessionId) || + null; + + const parent = _profileManager.getProfileById(opts.parentProfileId); + if (!parent) + throw new Error("Родительский profile не найден: " + opts.parentProfileId); + + // Дочерний profile — копия родителя (тот же id/partition → общая сессия), + // но с флагом isSubagent. НЕ сохраняем в profile-list.json. + const subProfile = { + id: parent.id, + providerId: parent.providerId, + name: "子代理: " + (opts.agentName || "agent"), + partition: parent.partition, + isSubagent: true, + subagentConfig: { + agentName: opts.agentName, + task: opts.task, + systemPrompt: opts.systemPrompt, + tools: opts.tools || null, + maxTurns: opts.maxTurns || null, + projectDir: projectDir || null, + }, + createdAt: new Date().toISOString(), + }; + + console.log( + "[Subagent] Запуск " + + opts.agentName + + " (parent=" + + opts.parentWindowId + + ", projectDir=" + + (projectDir || "нет") + + ")", + ); + incRunning(opts.parentProfileId); + + const windowId = _createWindow(subProfile); + + // Прокидываем projectDir + lineage в sessionStore дочернего окна, + // иначе инструменты субагента увидят projectDir = null. + try { + const subCtx = windowState.getWindowContext(windowId); + if (subCtx && subCtx.sessionStore) { + if (projectDir) subCtx.sessionStore.state.selectedProjectDir = projectDir; + if (parentSessionId) { + subCtx.sessionStore.state.pendingLineage = { + parentId: parentSessionId, + kind: "subagent", + agentName: opts.agentName, + }; + } + } + } catch (_) {} + + try { + // Отправляем в дочернее окно полный системный промпт Cookie Code + // (инструменты + формат cuckoo) вместе с промптом агента и задачей. + const subWinCtx = windowState.getWindowContext(windowId); + const subWin = subWinCtx ? subWinCtx.win : null; + const extra = buildAgentPrompt(opts.systemPrompt, opts.task); + await sendInitPromptWhenReady(subWin, projectDir, extra, timeoutMs); + + const text = await waitForDone(windowId, timeoutMs); + if (text === "__SUBAGENT_TIMEOUT__") + throw new Error("Субагент не ответил за отведённое время"); + return text; + } finally { + decRunning(opts.parentProfileId); + try { + const ctx = windowState.getWindowContext(windowId); + if (ctx && ctx.win && !ctx.win.isDestroyed()) ctx.win.close(); + } catch (_) {} + } +} + +module.exports = { + injectSubagentDeps, + runAgent, + onSubagentResponse, + hasRunningSubagent, +}; diff --git a/src/main/window.js b/src/main/window.js index 64bee73..5da95d5 100644 --- a/src/main/window.js +++ b/src/main/window.js @@ -2,17 +2,32 @@ * 窗口管理(多窗口 + 每窗口 profile 上下文) * 每个窗口关联一个 profileId,拥有独立的 sessionStore 实例。 */ -const windows = new Map(); // windowId -> { win, profileId, providerId, sessionStore } +const windows = new Map(); // windowId -> { win, profileId, providerId, sessionStore, isSubagent, subagentConfig } let lastActiveWindowId = null; -function addWindow(win, profileId, providerId, sessionStore) { - windows.set(win.id, { win, profileId, providerId, sessionStore }); +function addWindow( + win, + profileId, + providerId, + sessionStore, + isSubagent, + subagentConfig, +) { + windows.set(win.id, { + win, + profileId, + providerId, + sessionStore, + isSubagent: !!isSubagent, + subagentConfig: subagentConfig || null, + }); lastActiveWindowId = win.id; - win.on('closed', () => { + win.on("closed", () => { windows.delete(win.id); if (lastActiveWindowId === win.id) { const remaining = Array.from(windows.keys()); - lastActiveWindowId = remaining.length > 0 ? remaining[remaining.length - 1] : null; + lastActiveWindowId = + remaining.length > 0 ? remaining[remaining.length - 1] : null; } }); } @@ -21,7 +36,8 @@ function removeWindow(windowId) { windows.delete(windowId); if (lastActiveWindowId === windowId) { const remaining = Array.from(windows.keys()); - lastActiveWindowId = remaining.length > 0 ? remaining[remaining.length - 1] : null; + lastActiveWindowId = + remaining.length > 0 ? remaining[remaining.length - 1] : null; } } @@ -56,7 +72,7 @@ function setMainWindow(win) { } function getAllWindows() { - return Array.from(windows.values()).map(ctx => ctx.win); + return Array.from(windows.values()).map((ctx) => ctx.win); } function getAllContexts() { diff --git a/src/preload/dom/subagent-watch.js b/src/preload/dom/subagent-watch.js new file mode 100644 index 0000000..1fa66d2 --- /dev/null +++ b/src/preload/dom/subagent-watch.js @@ -0,0 +1,154 @@ +/** + * Наблюдатель за ответами AI в окне-субагенте. + * + * Активируется только если окно открыто с аргументом --cuckoo-subagent=... + * (main передал конфиг агента). Задачу и полный системный промпт main-процесс + * шлёт через initial-prompt; здесь мы только ждём финальный ответ субагента + * (без tool-блоков) и передаём его текст в main. + */ +const { ipcRenderer } = require("electron"); +const { getProviderByUrl } = require("../../../src/providers"); +const { getJsCodeBlocksFromMarkdown } = require("./js-detector"); +const { isAIResponseComplete } = require("./ai-response"); + +const STABLE_MS = 1200; // тишина, после которой считаем ответ финальным +const POLL_MS = 700; +const MAX_WAIT_MS = 10 * 60 * 1000; // абсолютный лимит + +/** Разбор --cuckoo-subagent= из process.argv */ +function readConfig() { + try { + const argv = (typeof process !== "undefined" && process.argv) || []; + const arg = argv.find( + (a) => typeof a === "string" && a.startsWith("--cuckoo-subagent="), + ); + if (!arg) return null; + const json = decodeURIComponent(arg.slice("--cuckoo-subagent=".length)); + const cfg = JSON.parse(json); + if (!cfg || !cfg.agentName || !cfg.task) return null; + return cfg; + } catch (_) { + return null; + } +} + +/** Текст последнего AI-сообщения (markdown-контейнер) */ +function getLastAiText() { + try { + const provider = getProviderByUrl(window.location.href); + if (!provider || typeof provider.getMessageCandidates !== "function") + return null; + const msgs = provider.getMessageCandidates(); + if (!msgs || msgs.length === 0) return null; + const last = msgs[msgs.length - 1]; + const md = provider.getMessageMarkdown + ? provider.getMessageMarkdown(last) + : last.querySelector(".ds-markdown"); + if (!md) return null; + const text = (md.textContent || md.innerText || "").trim(); + return { text, md }; + } catch (_) { + return null; + } +} + +/** + * Если окно — субагент, запустить цикл наблюдения за финальным ответом. + * Задача и системный промпт уже отправлены main-процессом через initial-prompt. + */ +function initSubagentWatch() { + const cfg = readConfig(); + if (!cfg) return null; + + console.log("[Cookie Code][Subagent] активирован: " + cfg.agentName); + + const maxTurns = cfg.maxTurns || null; + let turnCount = 0; + let lastSentText = ""; + let lastSnapshot = ""; + let lastChangeAt = Date.now(); + let sent = false; + const startedAt = Date.now(); + + const timer = setInterval(async () => { + if (sent) return; + if (Date.now() - startedAt > MAX_WAIT_MS) { + sent = true; + clearInterval(timer); + ipcRenderer + .invoke("subagent-response", { text: "__SUBAGENT_TIMEOUT__" }) + .catch(() => {}); + return; + } + try { + const got = getLastAiText(); + if (!got || !got.text) return; + const snapshot = got.text; + if (snapshot !== lastSnapshot) { + lastSnapshot = snapshot; + lastChangeAt = Date.now(); + return; + } + if (Date.now() - lastChangeAt < STABLE_MS) return; + + const done = await isAIResponseComplete(); + if (!done) return; + + const blocks = (() => { + try { + return getJsCodeBlocksFromMarkdown(got.md) || []; + } catch (_) { + return []; + } + })(); + + if (blocks.length === 0) { + if (snapshot === lastSentText) return; + lastSentText = snapshot; + sent = true; + clearInterval(timer); + console.log( + "[Cookie Code][Subagent] финальный ответ (" + + snapshot.length + + " символов)", + ); + ipcRenderer + .invoke("subagent-response", { text: snapshot, done: true }) + .catch(() => {}); + return; + } + + turnCount++; + if (maxTurns && turnCount >= maxTurns) { + sent = true; + clearInterval(timer); + console.log("[Cookie Code][Subagent] достигнут maxTurns=" + maxTurns); + ipcRenderer + .invoke("subagent-response", { + text: snapshot, + partial: true, + turns: turnCount, + }) + .catch(() => {}); + } else { + console.log( + "[Cookie Code][Subagent] шаг " + + turnCount + + " (tool-блоков: " + + blocks.length + + "), ждём продолжения", + ); + lastSnapshot = ""; + } + } catch (err) { + console.error( + "[Cookie Code][Subagent] ошибка цикла:", + err && err.message, + ); + } + }, POLL_MS); + + return cfg; +} + +module.exports = { initSubagentWatch, readConfig }; diff --git a/src/preload/index.js b/src/preload/index.js index 337c51b..75cd081 100644 --- a/src/preload/index.js +++ b/src/preload/index.js @@ -39,6 +39,7 @@ const whatsNew = require("./dom/whats-new"); const i18n = require("./i18n/i18n"); const state = require("./dom/state"); const tokenInterceptor = require("./dom/token-interceptor"); +const subagentWatch = require("./dom/subagent-watch"); const { getProviderByUrl } = require("../providers"); const { safe } = require("./dom/safe"); @@ -90,6 +91,10 @@ async function init() { // чтобы поймать первый же запрос completion. Работает через safe(). safe("init.tokenInterceptor", () => tokenInterceptor.install()); + // Субагент: если окно открыто с --cuckoo-subagent=..., запускаем цикл + // наблюдения за финальным ответом и отправляем его в main-процесс. + safe("init.subagentWatch", () => subagentWatch.initSubagentWatch()); + // Регистрируем IPC-листенеры всегда — от них зависит ввод и парсинг tool-блоков safe("init.registerIpcListeners", () => chatInput.registerIpcListeners()); safe("init.registerAskUserQuestionListener", () => @@ -192,8 +197,6 @@ async function init() { // Принудительно держим тёмную тему DeepSeek safe("init.forceDarkThemeStart", () => forceDarkTheme.startWatch()); - - // Подмена QR-кода в попапе «Скачать приложение» safe("init.qrOverrideStart", () => qrOverride.startWatch()); diff --git a/src/prompt/deepseek.md b/src/prompt/deepseek.md index 3370e87..a5dbdde 100644 --- a/src/prompt/deepseek.md +++ b/src/prompt/deepseek.md @@ -8,7 +8,7 @@ - 当前工作目录:由系统初始化时注入(通过 projectDir 变量提供) - 工作目录与项目根目录可能不同;相对路径基于 projectDir 解析 -{{PLATFORM_INFO}} + {{PLATFORM_INFO}} - 若需确认当前目录,请使用 pwd 命令(Unix/macOS)或 cd 命令(Windows cmd 无 pwd;echo %cd% 也可用) ## 回复风格 @@ -19,7 +19,7 @@ ## 关于工具调用 -- 需要调用工具时,将 JavaScript 代码输出到以 ```cuckoo 开头、以 ``` 结尾的代码块中,代码块外不要有任何文字 +- 需要调用工具时,将 JavaScript 代码输出到以 `cuckoo 开头、以 ` 结尾的代码块中,代码块外不要有任何文字 - 所有工具函数都是异步的,调用时必须使用 await - 多行文本一律使用反引号(`)模板字符串,直接换行,不需要任何转义 - 不要输出 JSON 格式的工具调用,也不要对字符串做 JSON 转义 @@ -27,6 +27,7 @@ ## 核心原则 ### 1. 先思考,再编码 + - 在编写任何代码之前,先对问题进行充分的逻辑推理。 - 考虑边界情况、隐藏的假设和备选方案。 - 如果问题描述模糊,先提出澄清性问题,再继续推进。 @@ -34,18 +35,21 @@ - 有疑问先提出,提出疑问的方式为:先问一个问题,用户回答后再问下一个问题。 ### 2. 简洁至上 + - 优先选择能满足需求的最简单的解决方案。 - 避免过度工程、投机性的泛化或过早优化。 - 编写的代码应当可读性强、意图明显、便于后续修改。 - 除非用户明确要求,否则不要擅自添加"锦上添花"的功能。 ### 3. 精准修改(外科手术式修改) + - 修改现有代码时,尽可能缩小改动范围。 - 不要顺手格式化、重构或清理无关代码(除非用户明确要求)。 - 每次编辑应有单一、清晰的目标,并精准定位。 - 避免"来都来了"式的连带改动,牵扯多个文件或模块。 ### 4. 目标驱动执行 + - 始终牢记最终目标。 - 在交付解决方案之前,验证它是否真正解决了原始问题。 - 事先定义成功标准,并对照标准测试你的改动。 @@ -65,15 +69,23 @@ - 相对路径基于当前项目根目录(projectDir)解析 - 工具出错时抛出异常(Error.message 为错误描述);唯一例外是 bash()/pwsh():非零退出不抛异常,通过返回文本中的 [exit code] 标记报告 - 此部分与 tools/cuckoo-tools.d.ts 保持一致 -```ts -{{TOOL_API_TYPES}} + +```ts +{ + { + TOOL_API_TYPES; + } +} ``` --- ## 工具使用指导 + {{TOOL_SECTIONS}} +{{AGENTS_SECTION}} + --- # 工具使用规则 @@ -97,7 +109,7 @@ await write("src/utils/helper.js", content.replace("formatDate", "formatTime")); ### 输出前自查清单 -1. 代码块以 ```cuckoo 开头、以 ``` 结尾 +1. 代码块以 `cuckoo 开头、以 ` 结尾 2. 代码块外不要有任何文字 3. 每个工具函数调用前都写 await 4. 多行文本一律使用反引号(`)模板字符串,直接换行,不做 \n 转义 @@ -161,6 +173,7 @@ log(r); ## 错误处理 如果脚本执行失败,系统会返回错误信息。你应该: + 1. 分析错误原因(文件不存在、old_string 不匹配、语法错误等) 2. 修正代码后重新输出完整的 ```cuckoo 代码块 @@ -171,7 +184,6 @@ log(r); ❌ 不要编造不存在的工具函数或参数 ❌ 不要在用户未授权时执行破坏性操作(删除文件、格式化磁盘等) - {{PROJECT_INTRO_SECTION}} {{SKILL_SECTION}} @@ -179,6 +191,7 @@ log(r); --- ## 当前项目目录 + 当前项目路径: {{PROJECT_DIR}} --- diff --git a/src/prompt/default.md b/src/prompt/default.md index 4c24c24..a5dbdde 100644 --- a/src/prompt/default.md +++ b/src/prompt/default.md @@ -8,7 +8,7 @@ - 当前工作目录:由系统初始化时注入(通过 projectDir 变量提供) - 工作目录与项目根目录可能不同;相对路径基于 projectDir 解析 -{{PLATFORM_INFO}} + {{PLATFORM_INFO}} - 若需确认当前目录,请使用 pwd 命令(Unix/macOS)或 cd 命令(Windows cmd 无 pwd;echo %cd% 也可用) ## 回复风格 @@ -19,7 +19,7 @@ ## 关于工具调用 -- 需要调用工具时,将 JavaScript 代码输出到以 ```cuckoo 开头、以 ``` 结尾的代码块中,代码块外不要有任何文字 +- 需要调用工具时,将 JavaScript 代码输出到以 `cuckoo 开头、以 ` 结尾的代码块中,代码块外不要有任何文字 - 所有工具函数都是异步的,调用时必须使用 await - 多行文本一律使用反引号(`)模板字符串,直接换行,不需要任何转义 - 不要输出 JSON 格式的工具调用,也不要对字符串做 JSON 转义 @@ -27,6 +27,7 @@ ## 核心原则 ### 1. 先思考,再编码 + - 在编写任何代码之前,先对问题进行充分的逻辑推理。 - 考虑边界情况、隐藏的假设和备选方案。 - 如果问题描述模糊,先提出澄清性问题,再继续推进。 @@ -34,18 +35,21 @@ - 有疑问先提出,提出疑问的方式为:先问一个问题,用户回答后再问下一个问题。 ### 2. 简洁至上 + - 优先选择能满足需求的最简单的解决方案。 - 避免过度工程、投机性的泛化或过早优化。 - 编写的代码应当可读性强、意图明显、便于后续修改。 - 除非用户明确要求,否则不要擅自添加"锦上添花"的功能。 ### 3. 精准修改(外科手术式修改) + - 修改现有代码时,尽可能缩小改动范围。 - 不要顺手格式化、重构或清理无关代码(除非用户明确要求)。 - 每次编辑应有单一、清晰的目标,并精准定位。 - 避免"来都来了"式的连带改动,牵扯多个文件或模块。 ### 4. 目标驱动执行 + - 始终牢记最终目标。 - 在交付解决方案之前,验证它是否真正解决了原始问题。 - 事先定义成功标准,并对照标准测试你的改动。 @@ -65,15 +69,23 @@ - 相对路径基于当前项目根目录(projectDir)解析 - 工具出错时抛出异常(Error.message 为错误描述);唯一例外是 bash()/pwsh():非零退出不抛异常,通过返回文本中的 [exit code] 标记报告 - 此部分与 tools/cuckoo-tools.d.ts 保持一致 + ```ts -{{TOOL_API_TYPES}} +{ + { + TOOL_API_TYPES; + } +} ``` --- ## 工具使用指导 + {{TOOL_SECTIONS}} +{{AGENTS_SECTION}} + --- # 工具使用规则 @@ -97,7 +109,7 @@ await write("src/utils/helper.js", content.replace("formatDate", "formatTime")); ### 输出前自查清单 -1. 代码块以 ```cuckoo 开头、以 ``` 结尾 +1. 代码块以 `cuckoo 开头、以 ` 结尾 2. 代码块外不要有任何文字 3. 每个工具函数调用前都写 await 4. 多行文本一律使用反引号(`)模板字符串,直接换行,不做 \n 转义 @@ -161,6 +173,7 @@ log(r); ## 错误处理 如果脚本执行失败,系统会返回错误信息。你应该: + 1. 分析错误原因(文件不存在、old_string 不匹配、语法错误等) 2. 修正代码后重新输出完整的 ```cuckoo 代码块 @@ -171,7 +184,6 @@ log(r); ❌ 不要编造不存在的工具函数或参数 ❌ 不要在用户未授权时执行破坏性操作(删除文件、格式化磁盘等) - {{PROJECT_INTRO_SECTION}} {{SKILL_SECTION}} @@ -179,6 +191,7 @@ log(r); --- ## 当前项目目录 + 当前项目路径: {{PROJECT_DIR}} --- diff --git a/tools/AgentTools.js b/tools/AgentTools.js new file mode 100644 index 0000000..31c0715 --- /dev/null +++ b/tools/AgentTools.js @@ -0,0 +1,100 @@ +/** + * Инструменты для работы с субагентами (agents). + * - run_agent : делегировать задачу субагенту + * - list_agents: показать доступные субагенты (проект + пользователь) + * - read_agent : прочитать полный systemPrompt субагента (для отладки) + */ +const { Tool, ToolResult } = require("./ToolRegistry"); +const { scanAgents } = require("../src/main/agents"); + +class ListAgentsTool extends Tool { + constructor() { + super( + "list_agents", + "Список доступных субагентов (проект + пользователь). Показывает name, description, tools, maxTurns.", + { + type: "object", + properties: {}, + required: [], + additionalProperties: false, + }, + "list_agents()", + ); + } + async execute(params) { + const agents = scanAgents(params && params.projectDir); + if (agents.length === 0) + return ToolResult.success("(нет доступных субагентов)"); + const text = agents + .map((a) => { + const t = a.tools && a.tools.length ? a.tools.join(", ") : "(все)"; + const m = a.maxTurns ? String(a.maxTurns) : "(без лимита)"; + return ( + "- " + + a.name + + " [" + + a.source + + "]\n" + + " description: " + + a.description + + "\n" + + " tools: " + + t + + "\n" + + " maxTurns: " + + m + + "\n" + + " path: " + + a.agentPath + ); + }) + .join("\n\n"); + return ToolResult.success(text); + } +} + +class ReadAgentTool extends Tool { + constructor() { + super( + "read_agent", + "Прочитать полный systemPrompt субагента по имени. Полезно для отладки определения агента.", + { + type: "object", + properties: { name: { type: "string", description: "Имя субагента" } }, + required: ["name"], + additionalProperties: false, + }, + "read_agent(name)", + ); + } + async execute(params) { + const { name, projectDir } = params || {}; + if (!name) return ToolResult.error("Поле name обязательно"); + const agent = scanAgents(projectDir || null).find((a) => a.name === name); + if (!agent) return ToolResult.error("Субагент не найден: " + name); + return ToolResult.success( + "name: " + + agent.name + + "\n" + + "source: " + + agent.source + + "\n" + + "path: " + + agent.agentPath + + "\n" + + "description: " + + agent.description + + "\n" + + "tools: " + + (agent.tools ? agent.tools.join(", ") : "(все)") + + "\n" + + "maxTurns: " + + (agent.maxTurns || "(без лимита)") + + "\n\n" + + "--- SYSTEM PROMPT ---\n" + + agent.systemPrompt, + ); + } +} + +module.exports = { ListAgentsTool, ReadAgentTool }; diff --git a/tools/JsRunner.js b/tools/JsRunner.js index ddf7eca..dfe7fa0 100644 --- a/tools/JsRunner.js +++ b/tools/JsRunner.js @@ -199,6 +199,16 @@ const BOOTSTRAP = [ " globalThis.memoryClear = async function () {", " return await __call('memory_clear', {});", " };", + " globalThis.runAgent = async function (name, task) {", + " return await __call('run_agent', { name: name, task: task });", + " };", + " globalThis.run_agent = globalThis.runAgent;", + " globalThis.listAgents = async function () {", + " return await __call('list_agents', {});", + " };", + " globalThis.readAgent = async function (name) {", + " return await __call('read_agent', { name: name });", + " };", " globalThis.exitPlanMode = async function (plan) {", " return await __call('exit_plan_mode', { plan: plan });", " };", @@ -359,6 +369,7 @@ class JsRunner { exitPlanMode, sessionId, attachFile, + currentWindowId, ) { if (!code || typeof code !== "string" || !code.trim()) { return { success: false, error: "无效的 JS 代码" }; @@ -411,6 +422,10 @@ class JsRunner { Object.assign({}, args, { projectDir, senderId, + currentWindowId: + typeof currentWindowId === "number" + ? currentWindowId + : senderId, askUserQuestion, pasteImage, exitPlanMode, diff --git a/tools/RunAgentTool.js b/tools/RunAgentTool.js new file mode 100644 index 0000000..6acae17 --- /dev/null +++ b/tools/RunAgentTool.js @@ -0,0 +1,93 @@ +/** + * run_agent — делегирование задачи субагенту. + * + * Субагент работает в отдельном окне (тот же partition → общая сессия DeepSeek), + * в изолированном контексте, и возвращает только итоговый текст. + * + * Раннер и определитель «окно-субагент» инжектируются из main-процесса + * (см. src/main/index.js): так tools-слой не зависит от main-слоя. + */ +const { Tool, ToolResult } = require("./ToolRegistry"); +const { scanAgents } = require("../src/main/agents"); + +/** @type {(args:{agent:any, task:string, currentWindowId:number}) => Promise|null} */ +let _runner = null; +/** @type {(windowId:number) => boolean|null} */ +let _isSubagentWindow = null; + +function injectAgentRunner(fn) { + _runner = fn; +} +function injectSubagentChecker(fn) { + _isSubagentWindow = fn; +} + +class RunAgentTool extends Tool { + constructor() { + super( + "run_agent", + "Делегировать задачу субагенту (независимый контекст). Возвращает итоговое резюме. Подходит для широкого поиска/анализа и независимых подзадач — чтобы не засорять текущий контекст.", + { + type: "object", + properties: { + name: { + type: "string", + description: + "Имя субагента (см. раздел «Доступные субагенты» в системном промпте)", + }, + task: { + type: "string", + description: "Описание задачи для делегирования", + }, + }, + required: ["name", "task"], + additionalProperties: false, + }, + "run_agent(name, task)", + ); + } + + getPromptSection() { + return { + name: "tool:run_agent", + order: 113, + text: + "Используй run_agent(name, task), чтобы делегировать задачу субагенту. " + + "Субагент работает в отдельном контексте и возвращает только итоговое резюме.", + }; + } + + async execute(params) { + const { name, task, projectDir, currentWindowId } = params; + if (!name || !task) return ToolResult.error("Поля name и task обязательны"); + if (typeof currentWindowId !== "number") + return ToolResult.error("Отсутствует контекст окна (currentWindowId)"); + + // Защита от рекурсии: окно-субагент не может вызвать run_agent + try { + if (_isSubagentWindow && _isSubagentWindow(currentWindowId)) { + return ToolResult.error( + "Субагент не может вызывать run_agent (защита от рекурсии)", + ); + } + } catch (_) {} + + const agents = scanAgents(projectDir || null); + const agent = agents.find((a) => a.name === name); + if (!agent) return ToolResult.error("Субагент не найден: " + name); + + if (!_runner) + return ToolResult.error("Раннер субагентов не инициализирован"); + + try { + const text = await _runner({ agent, task, currentWindowId, projectDir }); + return ToolResult.success(text); + } catch (err) { + return ToolResult.error( + "Ошибка выполнения субагента: " + (err && err.message), + ); + } + } +} + +module.exports = { RunAgentTool, injectAgentRunner, injectSubagentChecker }; diff --git a/tools/index.js b/tools/index.js index e2fdf14..bb687e0 100644 --- a/tools/index.js +++ b/tools/index.js @@ -42,6 +42,8 @@ const { MemoryReadTool, MemoryClearTool, } = require("./MemoryTool"); +const { RunAgentTool } = require("./RunAgentTool"); +const { ListAgentsTool, ReadAgentTool } = require("./AgentTools"); // 创建全局工具注册表 const registry = new ToolRegistry(); @@ -84,6 +86,9 @@ registry.register(new AttachTelegramTool()); registry.register(new MemorySaveTool()); registry.register(new MemoryReadTool()); registry.register(new MemoryClearTool()); +registry.register(new RunAgentTool()); +registry.register(new ListAgentsTool()); +registry.register(new ReadAgentTool()); // 导出 module.exports = { @@ -123,6 +128,9 @@ module.exports = { MemorySaveTool, MemoryReadTool, MemoryClearTool, + RunAgentTool, + ListAgentsTool, + ReadAgentTool, // 便捷方法 getAllTools: () => registry, getToolDescriptions: () => registry.getDescriptions(), From 9d339c7d1b849b47b4defb92fef59e0cfa24996e Mon Sep 17 00:00:00 2001 From: MerfiDEV Date: Tue, 6 Oct 2026 17:32:36 +0300 Subject: [PATCH 03/11] Move agent storage to cookie/agents This change migrates agent discovery from .cuckoo/agents to cookie/agents and updates the agent docs and scanner logic accordingly. It also adds a new create_agent tool that creates project-scoped subagents in cookie/agents, exposes the helper through the JS runner, and refreshes the available agents list after creation. --- {.cuckoo => cookie}/agents/code-reviewer.md | 0 {.cuckoo => cookie}/agents/explore.md | 0 {.cuckoo => cookie}/agents/git-committer.md | 0 src/main/agents/scanner.js | 8 +- src/main/agents/types.js | 4 +- tools/CreateAgentTool.js | 212 ++++++++++++++++++++ tools/JsRunner.js | 4 + tools/index.js | 3 + 8 files changed, 225 insertions(+), 6 deletions(-) rename {.cuckoo => cookie}/agents/code-reviewer.md (100%) rename {.cuckoo => cookie}/agents/explore.md (100%) rename {.cuckoo => cookie}/agents/git-committer.md (100%) create mode 100644 tools/CreateAgentTool.js diff --git a/.cuckoo/agents/code-reviewer.md b/cookie/agents/code-reviewer.md similarity index 100% rename from .cuckoo/agents/code-reviewer.md rename to cookie/agents/code-reviewer.md diff --git a/.cuckoo/agents/explore.md b/cookie/agents/explore.md similarity index 100% rename from .cuckoo/agents/explore.md rename to cookie/agents/explore.md diff --git a/.cuckoo/agents/git-committer.md b/cookie/agents/git-committer.md similarity index 100% rename from .cuckoo/agents/git-committer.md rename to cookie/agents/git-committer.md diff --git a/src/main/agents/scanner.js b/src/main/agents/scanner.js index b455475..cf2ccfd 100644 --- a/src/main/agents/scanner.js +++ b/src/main/agents/scanner.js @@ -2,7 +2,7 @@ * Сканер субагентов: проектные + пользовательские каталоги -> AgentMeta[]. * * Правила: - * - Сканируется только /.cuckoo/agents/ — один уровень, только .md + * - Сканируется только /cookie/agents/ — один уровень, только .md * - Имя: frontmatter.name || basename(file, '.md') * - Описание: frontmatter.description || первый непустой абзац тела * - Без имени/описания — агент пропускается (нельзя показать в промпте) @@ -26,7 +26,7 @@ function firstParagraph(body) { /** * Просканировать одну папку agents (без рекурсии). - * @param {string} agentsDir абсолютный путь к .cuckoo/agents + * @param {string} agentsDir абсолютный путь к cookie/agents * @param {'project'|'user'} source * @returns {AgentMeta[]} */ @@ -106,10 +106,10 @@ function scanAgents(projectDir) { userHome = os.homedir(); } catch (_) {} const user = userHome - ? scanAgentsRoot(path.join(userHome, ".cuckoo", "agents"), "user") + ? scanAgentsRoot(path.join(userHome, "cookie", "agents"), "user") : []; const project = projectDir - ? scanAgentsRoot(path.join(projectDir, ".cuckoo", "agents"), "project") + ? scanAgentsRoot(path.join(projectDir, "cookie", "agents"), "project") : []; return mergeAgents(project, user); } diff --git a/src/main/agents/types.js b/src/main/agents/types.js index ac1a770..73a3981 100644 --- a/src/main/agents/types.js +++ b/src/main/agents/types.js @@ -2,8 +2,8 @@ * Agent (субагент) — типы и документация. * * Формат: markdown-файл с frontmatter, размещённый в: - * - /.cuckoo/agents/.md (проектный, приоритет выше) - * - ~/.cuckoo/agents/.md (пользовательский) + * - /cookie/agents/.md (проектный, приоритет выше) + * - ~/cookie/agents/.md (пользовательский) * * Frontmatter-поля: * - name : имя агента (по умолчанию — имя файла без .md) diff --git a/tools/CreateAgentTool.js b/tools/CreateAgentTool.js new file mode 100644 index 0000000..de2ea6b --- /dev/null +++ b/tools/CreateAgentTool.js @@ -0,0 +1,212 @@ +/** + * create_agent — создание нового субагента. + * + * AI создаёт специалиста под задачу: файл /cookie/agents/.md + * становится сразу доступен для run_agent. + */ +const { Tool, ToolResult } = require("./ToolRegistry"); +const fs = require("fs"); +const path = require("path"); + +const NAME_RE = /^[A-Za-z0-9_-]{1,64}$/; +const MAX_DESC = 200; +const MAX_PROMPT = 20000; + +let _allowedTools = null; +function getAllowedTools() { + if (_allowedTools) return _allowedTools; + try { + const { registry } = require("./index"); + _allowedTools = new Set(registry.listNames()); + } catch (_) { + _allowedTools = new Set(); + } + return _allowedTools; +} + +function escapeYaml(str) { + return String(str).replace(/\r?\n/g, " ").trim(); +} + +function buildAgentFile(input) { + const fm = []; + fm.push("---"); + fm.push("name: " + input.name); + fm.push("description: " + escapeYaml(input.description)); + if (Array.isArray(input.tools) && input.tools.length > 0) { + fm.push("tools: " + input.tools.join(", ")); + } + if (typeof input.maxTurns === "number") { + fm.push("maxTurns: " + input.maxTurns); + } + fm.push("---"); + fm.push(""); + fm.push(String(input.systemPrompt).trim()); + fm.push(""); + return fm.join("\n"); +} + +class CreateAgentTool extends Tool { + constructor() { + super( + "create_agent", + "Создать нового субагента (cookie/agents/.md). Сразу доступен для run_agent.", + { + type: "object", + properties: { + name: { + type: "string", + description: "Имя: латиница, цифры, дефис, _. 1..64", + }, + description: { + type: "string", + description: "Когда делегировать (<=200)", + }, + systemPrompt: { + type: "string", + description: "Системный промпт агента", + }, + tools: { + type: "array", + items: { type: "string" }, + description: "Разрешённые инструменты (пусто = все)", + }, + maxTurns: { type: "number", description: "Лимит шагов 1..1000" }, + overwrite: { + type: "boolean", + description: "Перезаписать существующего", + }, + }, + required: ["name", "description", "systemPrompt"], + additionalProperties: false, + }, + "create_agent({name, description, systemPrompt, tools?, maxTurns?, overwrite?})", + ); + } + + getPromptSection() { + return { + name: "tool:create_agent", + order: 114, + text: + "Используй create_agent(...) чтобы создать нового субагента под задачу. " + + "После создания агент сразу доступен для run_agent. Создавай только когда " + + "задача повторяющаяся или требует отдельного контекста.", + }; + } + + async execute(params) { + const { + name, + description, + systemPrompt, + tools, + maxTurns, + overwrite, + projectDir, + } = params || {}; + + if (typeof name !== "string" || !NAME_RE.test(name)) { + return ToolResult.error( + "Некорректное имя: латиница/цифры/дефис/_, 1..64", + ); + } + if ( + typeof description !== "string" || + !description.trim() || + description.length > MAX_DESC + ) { + return ToolResult.error("description обязателен, <= " + MAX_DESC); + } + if (typeof systemPrompt !== "string" || !systemPrompt.trim()) { + return ToolResult.error("systemPrompt обязателен"); + } + if (systemPrompt.length > MAX_PROMPT) { + return ToolResult.error( + "systemPrompt слишком длинный (> " + MAX_PROMPT + ")", + ); + } + + let toolList; + if (Array.isArray(tools) && tools.length > 0) { + const allowed = getAllowedTools(); + const bad = []; + toolList = []; + for (const t of tools) { + const s = String(t || "").trim(); + if (!s) continue; + if (allowed.size > 0 && !allowed.has(s)) bad.push(s); + toolList.push(s); + } + if (bad.length > 0) { + return ToolResult.error("Неизвестные инструменты: " + bad.join(", ")); + } + } + + let turns; + if (maxTurns != null) { + const n = Number(maxTurns); + if (!Number.isInteger(n) || n < 1 || n > 1000) { + return ToolResult.error("maxTurns должен быть целым 1..1000"); + } + turns = n; + } + + if (!projectDir) { + return ToolResult.error( + "Не инициализирован проект — некуда положить cookie/agents/", + ); + } + const agentsDir = path.join(projectDir, "cookie", "agents"); + try { + fs.mkdirSync(agentsDir, { recursive: true }); + } catch (err) { + return ToolResult.error("Не удалось создать каталог: " + err.message); + } + const filePath = path.join(agentsDir, name + ".md"); + if (fs.existsSync(filePath) && overwrite !== true) { + return ToolResult.error( + "Агент уже существует: " + name + ". Укажи overwrite: true для замены.", + ); + } + + const fileText = buildAgentFile({ + name, + description: description.trim(), + tools: toolList, + maxTurns: turns, + systemPrompt: systemPrompt.trim(), + }); + + try { + fs.writeFileSync(filePath, fileText, "utf-8"); + } catch (err) { + return ToolResult.error("Запись не удалась: " + err.message); + } + + try { + const windowState = require("../src/main/window"); + const { scanAgents, buildAgentsSection } = require("../src/main/agents"); + const agents = scanAgents(projectDir); + const section = buildAgentsSection(agents); + const win = windowState.getMainWindow(); + if (win && !win.isDestroyed()) { + win.webContents.send("agents-section-updated", section); + } + } catch (_) {} + + const rel = path.relative(projectDir, filePath).replace(/\\/g, "/"); + const lines = []; + lines.push("Создан агент: " + name); + lines.push("Путь: " + rel); + lines.push( + "tools: " + (toolList && toolList.length ? toolList.join(", ") : "(все)"), + ); + lines.push("maxTurns: " + (turns || "(без лимита)")); + lines.push(""); + lines.push('Теперь можно вызвать: run_agent("' + name + '", "<задача>")'); + return ToolResult.success(lines.join("\n")); + } +} + +module.exports = { CreateAgentTool }; diff --git a/tools/JsRunner.js b/tools/JsRunner.js index dfe7fa0..690888e 100644 --- a/tools/JsRunner.js +++ b/tools/JsRunner.js @@ -209,6 +209,10 @@ const BOOTSTRAP = [ " globalThis.readAgent = async function (name) {", " return await __call('read_agent', { name: name });", " };", + " globalThis.createAgent = async function (opts) {", + " return await __call('create_agent', opts || {});", + " };", + " globalThis.create_agent = globalThis.createAgent;", " globalThis.exitPlanMode = async function (plan) {", " return await __call('exit_plan_mode', { plan: plan });", " };", diff --git a/tools/index.js b/tools/index.js index bb687e0..8e23099 100644 --- a/tools/index.js +++ b/tools/index.js @@ -44,6 +44,7 @@ const { } = require("./MemoryTool"); const { RunAgentTool } = require("./RunAgentTool"); const { ListAgentsTool, ReadAgentTool } = require("./AgentTools"); +const { CreateAgentTool } = require("./CreateAgentTool"); // 创建全局工具注册表 const registry = new ToolRegistry(); @@ -89,6 +90,7 @@ registry.register(new MemoryClearTool()); registry.register(new RunAgentTool()); registry.register(new ListAgentsTool()); registry.register(new ReadAgentTool()); +registry.register(new CreateAgentTool()); // 导出 module.exports = { @@ -131,6 +133,7 @@ module.exports = { RunAgentTool, ListAgentsTool, ReadAgentTool, + CreateAgentTool, // 便捷方法 getAllTools: () => registry, getToolDescriptions: () => registry.getDescriptions(), From bc03507c45db7b75ec1dd0668995fbbd67ada312 Mon Sep 17 00:00:00 2001 From: MerfiDEV Date: Tue, 6 Oct 2026 17:36:46 +0300 Subject: [PATCH 04/11] Enforce tool whitelist for subagents This patch adds a per-agent tool whitelist for subagent execution. It propagates the configured tools into the child window context, validates direct IPC tool calls before execution, and rejects non-whitelisted actions in JsRunner as well. It also adds a whitelist test agent to verify that allowed tools still work while forbidden ones are blocked with explicit errors. --- src/main/ipc.js | 21 +++++++++++++++++++++ src/main/subagent.js | 18 ++++++++++++++++-- src/main/window.js | 3 +++ tools/JsRunner.js | 22 ++++++++++++++++++++++ 4 files changed, 62 insertions(+), 2 deletions(-) diff --git a/src/main/ipc.js b/src/main/ipc.js index 42c2a22..6efb740 100644 --- a/src/main/ipc.js +++ b/src/main/ipc.js @@ -916,6 +916,26 @@ function registerIpcHandlers() { } } + // Whitelist для субагента: прямое выполнение tool-вызова тоже + // должно проходить проверку (не только через JsRunner). + if (ctx && Array.isArray(ctx.toolsWhitelist)) { + const ALIAS = { __bash: "bash" }; + const name = ALIAS[toolName] || toolName; + if (ctx.toolsWhitelist.indexOf(name) === -1) { + return { + callId, + success: false, + error: + "Инструмент «" + + name + + "» не разрешён этому агенту. " + + "Разрешены: " + + ctx.toolsWhitelist.join(", ") + + ".", + }; + } + } + const taskToken = beginTask(event.sender.id); try { const result = await toolRegistry.execute(toolName, { @@ -1122,6 +1142,7 @@ function registerIpcHandlers() { sessionIdOf(event), (payload) => attachFileToChat(event.sender, payload), currentWindowId, + ctx && Array.isArray(ctx.toolsWhitelist) ? ctx.toolsWhitelist : null, ); if (isTaskCanceled(event.sender.id, taskToken)) { return { diff --git a/src/main/subagent.js b/src/main/subagent.js index d7a5fce..646a139 100644 --- a/src/main/subagent.js +++ b/src/main/subagent.js @@ -230,8 +230,7 @@ async function runAgent(opts) { const windowId = _createWindow(subProfile); - // Прокидываем projectDir + lineage в sessionStore дочернего окна, - // иначе инструменты субагента увидят projectDir = null. + // Прокидываем projectDir + lineage + whitelist в контекст дочернего окна. try { const subCtx = windowState.getWindowContext(windowId); if (subCtx && subCtx.sessionStore) { @@ -244,6 +243,21 @@ async function runAgent(opts) { }; } } + // Жёсткое ограничение инструментов: если в frontmatter агента + // указан список tools — только они разрешены в дочернем окне. + if (subCtx) { + if (Array.isArray(opts.tools) && opts.tools.length > 0) { + subCtx.toolsWhitelist = opts.tools.slice(); + console.log( + "[Subagent] whitelist для " + + opts.agentName + + ": " + + opts.tools.join(", "), + ); + } else { + subCtx.toolsWhitelist = null; // без ограничений + } + } } catch (_) {} try { diff --git a/src/main/window.js b/src/main/window.js index 5da95d5..4b7eb45 100644 --- a/src/main/window.js +++ b/src/main/window.js @@ -20,6 +20,9 @@ function addWindow( sessionStore, isSubagent: !!isSubagent, subagentConfig: subagentConfig || null, + // Ограничение инструментов для субагента (whitelist из agent.tools). + // null = без ограничений. Заполняется в subagent.js::runAgent. + toolsWhitelist: null, }); lastActiveWindowId = win.id; win.on("closed", () => { diff --git a/tools/JsRunner.js b/tools/JsRunner.js index 690888e..5b5ae1d 100644 --- a/tools/JsRunner.js +++ b/tools/JsRunner.js @@ -374,6 +374,7 @@ class JsRunner { sessionId, attachFile, currentWindowId, + toolsWhitelist, ) { if (!code || typeof code !== "string" || !code.trim()) { return { success: false, error: "无效的 JS 代码" }; @@ -413,6 +414,27 @@ class JsRunner { /* plan-mode недоступен — не блокируем */ } + // ===== Whitelist инструментов для субагента ===== + // Если окно — субагент с ограниченным набором (frontmatter tools), + // любой вызов вне списка отклоняется ДО исполнения. + if (Array.isArray(toolsWhitelist)) { + // Внутренние алиасы → публичные имена + const ALIAS = { __bash: "bash" }; + const toolName = ALIAS[op] || op; + if (toolsWhitelist.indexOf(toolName) === -1) { + return JSON.stringify({ + success: false, + error: + "Инструмент «" + + toolName + + "» не разрешён этому агенту. " + + "Разрешены: " + + toolsWhitelist.join(", ") + + ".", + }); + } + } + let result; if (op === "__bash") { result = await runBash(args, projectDir); From 7a935be1348df25c436e6e433c008a1a06daf478 Mon Sep 17 00:00:00 2001 From: MerfiDEV Date: Tue, 6 Oct 2026 17:45:54 +0300 Subject: [PATCH 05/11] Preserve subagent project directory Keep the selected project directory stable while a session is still initializing, especially in subagent windows. This change caches the pending project dir until the session is ready, propagates it into subagent context, and restores the directory display in the subagent overlay so the working folder remains visible during startup. --- .gitignore | 1 + src/main/session-store.js | 16 +++++++++++++--- src/main/subagent.js | 8 +++++++- src/preload/dom/subagent-watch.js | 25 +++++++++++++++++++++++++ 4 files changed, 46 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index aa8ae16..f0f0cee 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,4 @@ test/coverage.lcov .playwright-mcp/ /tiktok /.cuckoo/skills +/cookie/agents diff --git a/src/main/session-store.js b/src/main/session-store.js index bbe91bf..b4d1a77 100644 --- a/src/main/session-store.js +++ b/src/main/session-store.js @@ -123,9 +123,19 @@ function createSessionStore(profileId, storeDir, windowState) { } } else { state.currentSessionId = null; - state.selectedProjectDir = null; - if (win && !win.isDestroyed()) { - win.webContents.send("project-dir-updated", null); + // Если есть pendingProjectDir (например, в дочернем окне субагента + // мы заранее задали рабочую папку), НЕ затираем selectedProjectDir — + // иначе инструменты до появления sessionId увидят null. + if (!state.pendingProjectDir) { + state.selectedProjectDir = null; + if (win && !win.isDestroyed()) { + win.webContents.send("project-dir-updated", null); + } + } else { + state.selectedProjectDir = state.pendingProjectDir; + if (win && !win.isDestroyed()) { + win.webContents.send("project-dir-updated", state.pendingProjectDir); + } } } } diff --git a/src/main/subagent.js b/src/main/subagent.js index 646a139..026843f 100644 --- a/src/main/subagent.js +++ b/src/main/subagent.js @@ -231,10 +231,16 @@ async function runAgent(opts) { const windowId = _createWindow(subProfile); // Прокидываем projectDir + lineage + whitelist в контекст дочернего окна. + // ВАЖНО: ставим pendingProjectDir — иначе handleUrlChange при загрузке + // homeUrl DeepSeek (sessionId ещё нет) сбросит selectedProjectDir в null. + // pendingProjectDir привязывается к сессии автоматически, когда она появится. try { const subCtx = windowState.getWindowContext(windowId); if (subCtx && subCtx.sessionStore) { - if (projectDir) subCtx.sessionStore.state.selectedProjectDir = projectDir; + if (projectDir) { + subCtx.sessionStore.state.selectedProjectDir = projectDir; + subCtx.sessionStore.state.pendingProjectDir = projectDir; + } if (parentSessionId) { subCtx.sessionStore.state.pendingLineage = { parentId: parentSessionId, diff --git a/src/preload/dom/subagent-watch.js b/src/preload/dom/subagent-watch.js index 1fa66d2..e29ba7c 100644 --- a/src/preload/dom/subagent-watch.js +++ b/src/preload/dom/subagent-watch.js @@ -62,6 +62,31 @@ function initSubagentWatch() { console.log("[Cookie Code][Subagent] активирован: " + cfg.agentName); + // Показываем рабочую папку в overlay — тот же блок, что у родительского окна. + // Папка пришла в cfg.projectDir (main-процесс кладёт её в subagentConfig). + if (cfg.projectDir) { + try { + const projectDirPanel = require("../overlay/project-dir"); + // Двойная попытка: overlay может инициализироваться с задержкой, + // а initProjectDirSection() сбрасывает папку в null — обновляем дважды. + const showDir = () => { + try { + projectDirPanel.updateProjectDirDisplay(cfg.projectDir); + console.log( + "[Cookie Code][Subagent] рабочая папка показана: " + cfg.projectDir, + ); + } catch (_) {} + }; + setTimeout(showDir, 800); + setTimeout(showDir, 2500); + } catch (err) { + console.error( + "[Cookie Code][Subagent] не удалось показать папку:", + err && err.message, + ); + } + } + const maxTurns = cfg.maxTurns || null; let turnCount = 0; let lastSentText = ""; From da24ab63e4095531ea864b752fbd4c7cb28e373a Mon Sep 17 00:00:00 2001 From: MerfiDEV Date: Tue, 6 Oct 2026 18:07:05 +0300 Subject: [PATCH 06/11] Improve subagent alerts and completion watch This change adds Telegram notifications for subagent lifecycle actions (run/create/list/read) and sends a start card before the child window opens. It also improves subagent completion detection by tracking stable text instead of the legacy AI-complete check, including project-dir display, turn limits, and timeout handling. --- botsrc/i18n.js | 36 ++++ botsrc/index.js | 273 +++++++++++++++++++++++++++++- src/main/index.js | 8 + src/preload/dom/subagent-watch.js | 140 ++++++++------- 4 files changed, 394 insertions(+), 63 deletions(-) diff --git a/botsrc/i18n.js b/botsrc/i18n.js index 0ef74e6..4fb48b9 100644 --- a/botsrc/i18n.js +++ b/botsrc/i18n.js @@ -266,6 +266,24 @@ const STRINGS = { "city.tz": "Часовой пояс", "city.place": "Город / страна", + // ===== Субагенты (run_agent / create_agent / list_agents / read_agent) ===== + "agent.runTitle": "🚀 ИИ делегирует задачу субагенту", + "agent.resultTitle": "🎯 Субагент вернул результат", + "agent.createTitle": "🧬 ИИ создаёт нового субагента", + "agent.listTitle": "📋 ИИ смотрит список субагентов", + "agent.readTitle": "🔍 ИИ читает описание субагента", + "agent.name": "Агент", + "agent.task": "Задача", + "agent.tools": "Инструменты", + "agent.maxTurns": "Лимит шагов", + "agent.noTools": "все разрешены", + "agent.noLimit": "без лимита", + "agent.noResult": "(пустой результат)", + "agent.failed": "❌ Субагент завершился с ошибкой", + "agent.created": "Агент создан", + "agent.existing": "Агент обновлён", + "agent.prompt": "Системный промпт", + // ===== Approval ===== "approval.title": "🔐 Подтверждение команды", "approval.jsScript": "JS-скрипт", @@ -577,6 +595,24 @@ const STRINGS = { "city.tz": "Timezone", "city.place": "City / country", + // ===== Subagents (run_agent / create_agent / list_agents / read_agent) ===== + "agent.runTitle": "🚀 AI delegates a task to a subagent", + "agent.resultTitle": "🎯 Subagent returned a result", + "agent.createTitle": "🧬 AI creates a new subagent", + "agent.listTitle": "📋 AI views the subagent list", + "agent.readTitle": "🔍 AI reads subagent details", + "agent.name": "Agent", + "agent.task": "Task", + "agent.tools": "Tools", + "agent.maxTurns": "Max turns", + "agent.noTools": "all allowed", + "agent.noLimit": "no limit", + "agent.noResult": "(empty result)", + "agent.failed": "❌ Subagent failed", + "agent.created": "Agent created", + "agent.existing": "Agent updated", + "agent.prompt": "System prompt", + // ===== Approval ===== "approval.title": "🔐 Command approval", "approval.jsScript": "JS script", diff --git a/botsrc/index.js b/botsrc/index.js index d2f0165..667fdba 100644 --- a/botsrc/index.js +++ b/botsrc/index.js @@ -3015,6 +3015,19 @@ const TECH_SERVICE_TOOLS = new Set([ "exitplanmode", ]); +// Агентские инструменты: имеют собственное оформление (см. _render*Agent), +// в общий батч «🔧 tool ×N» не идут, но уведомления им нужны. +const AGENT_TOOLS = new Set([ + "run_agent", + "runagent", + "create_agent", + "createagent", + "list_agents", + "listagents", + "read_agent", + "readagent", +]); + /** * Поставить уведомление в батч: если пришло такое же действие в течение * TECH_BATCH_MS — увеличиваем счётчик (×N), иначе шлём предыдущее и начинаем новое. @@ -3181,6 +3194,164 @@ function _renderCityTime(toolName, ok, args, preview) { return _t("city.title") + "\n\n" + rows.join("\n"); } +/** + * Красивое сообщение для run_agent: карточка с именем агента и задачей. + * Вызывается из notifyToolResult при старте (args) — а результат агента + * отдельным сообщением ловит `agent_result` (через notifyAgentResult). + * @returns {string|null} HTML-сообщение или null, если не наш инструмент + */ +function _renderRunAgent(toolName, ok, args, preview) { + const name = String(toolName || "").toLowerCase(); + if (name !== "run_agent" && name !== "runagent") return null; + + const a = args && typeof args === "object" ? args : {}; + const agentName = String(a.name || a.agent || "").trim(); + const task = String(a.task || "").trim(); + + if (!agentName) return null; + + const lines = []; + lines.push(_t("agent.runTitle")); + lines.push(""); + lines.push( + "🤖 " + + escapeHtml(_t("agent.name")) + + ": " + + escapeHtml(agentName) + + "", + ); + if (task) { + lines.push( + "📌 " + + escapeHtml(_t("agent.task")) + + ":\n
" + + escapeHtml(_techTruncLong(task, 700)) + + "
", + ); + } + if (!ok) { + // Ошибка запуска (агент не найден, context lost и т.п.) + // _t() уже возвращает HTML — не экранируем. + lines.push(""); + lines.push(_t("agent.failed")); + if (preview) { + lines.push( + "
" + + escapeHtml(_techTrunc(String(preview), 400)) + + "
", + ); + } + } + return lines.join("\n"); +} + +/** + * Красивое сообщение для create_agent: карточка нового агента (имя, tools, лимит). + * Результат содержит путь к файлу и подсказку «теперь можно вызвать». + * @returns {string|null} + */ +function _renderCreateAgent(toolName, ok, args, preview) { + const name = String(toolName || "").toLowerCase(); + if (name !== "create_agent" && name !== "createagent") return null; + if (!ok) return null; + + const a = args && typeof args === "object" ? args : {}; + const agentName = String(a.name || "").trim(); + const description = String(a.description || "").trim(); + const tools = Array.isArray(a.tools) ? a.tools.filter(Boolean) : []; + const maxTurns = a.maxTurns; + const overwrite = a.overwrite === true; + + const lines = []; + lines.push(_t("agent.createTitle")); + lines.push(""); + lines.push( + "🤖 " + + escapeHtml(_t("agent.name")) + + ": " + + escapeHtml(agentName) + + "", + ); + if (description) { + lines.push("📝 " + escapeHtml(description)); + } + lines.push( + "🧰 " + + escapeHtml(_t("agent.tools")) + + ": " + + (tools.length > 0 + ? tools.map((x) => "" + escapeHtml(x) + "").join(", ") + : "" + escapeHtml(_t("agent.noTools")) + ""), + ); + lines.push( + "🔁 " + + escapeHtml(_t("agent.maxTurns")) + + ": " + + (maxTurns + ? escapeHtml(String(maxTurns)) + : escapeHtml(_t("agent.noLimit"))) + + "", + ); + lines.push(""); + lines.push( + "" + + escapeHtml(overwrite ? _t("agent.existing") : _t("agent.created")) + + "", + ); + return lines.join("\n"); +} + +/** + * Красивое сообщение для list_agents: сам список приходит в preview. + * @returns {string|null} + */ +function _renderListAgents(toolName, ok, args, preview) { + const name = String(toolName || "").toLowerCase(); + if (name !== "list_agents" && name !== "listagents") return null; + if (!ok) return null; + const body = _stripResultWrapper(String(preview || "")).trim(); + if (!body) return _t("agent.listTitle") + "\n\n(пусто)"; + return ( + _t("agent.listTitle") + + "\n\n
" + + escapeHtml(_techTruncLong(body, 3000)) + + "
" + ); +} + +/** + * Красивое сообщение для read_agent: имя агента + его systemPrompt. + * @returns {string|null} + */ +function _renderReadAgent(toolName, ok, args, preview) { + const name = String(toolName || "").toLowerCase(); + if (name !== "read_agent" && name !== "readagent") return null; + if (!ok) return null; + const a = args && typeof args === "object" ? args : {}; + const agentName = String(a.name || "").trim(); + const body = _stripResultWrapper(String(preview || "")).trim(); + const lines = []; + lines.push(_t("agent.readTitle")); + if (agentName) { + lines.push(""); + lines.push( + "🤖 " + + escapeHtml(_t("agent.name")) + + ": " + + escapeHtml(agentName) + + "", + ); + } + if (body) { + lines.push( + "\n
" + + escapeHtml(_techTruncLong(body, 3000)) + + "
", + ); + } + return lines.join("\n"); +} + /** Обрезка длинного текста для спец-сообщений (мягкий лимит 3500 симв.). */ function _techTruncLong(s, max) { const limit = max || 3500; @@ -3189,26 +3360,117 @@ function _techTruncLong(s, max) { return str.slice(0, limit) + "\n…(обрезано)"; } +/** + * Уведомление о СТАРТЕ делегирования субагенту. + * Вызывается раннером (index.js) ДО открытия дочернего окна, чтобы + * пользователь в TG сразу видел карточку «делегирую задачу». + * @param {string} agentName + * @param {string} task + */ +async function notifyAgentStarted(agentName, task) { + try { + const cfg = _read(); + if (!cfg.enabled || !cfg.notifyTools) + return { success: false, skipped: true }; + const lines = []; + lines.push(_t("agent.runTitle")); + lines.push(""); + lines.push( + "🤖 " + + escapeHtml(_t("agent.name")) + + ": " + + escapeHtml(agentName || "agent") + + "", + ); + if (task) { + lines.push( + "📌 " + + escapeHtml(_t("agent.task")) + + ":\n
" + + escapeHtml(_techTruncLong(task, 700)) + + "
", + ); + } + return await telegramBot.sendMessage(lines.join("\n"), { + parseMode: "HTML", + }); + } catch (err) { + console.error("[bot] notifyAgentStarted failed:", err.message); + return { success: false, error: err.message }; + } +} + /** Уведомление о результате tool (компактный стиль «Tech»). */ async function notifyToolResult(toolName, ok, detail) { const cfg = _read(); if (!cfg.enabled || !cfg.notifyTools) return { success: false, skipped: true }; + const _toolLower = String(toolName || "").toLowerCase(); // Служебные вызовы (вопрос к пользователю и т.п.) в уведомления не шлём — // у них есть собственное сообщение. - const _toolLower = String(toolName || "").toLowerCase(); if (TECH_SERVICE_TOOLS.has(_toolLower)) { return { success: false, skipped: true }; } - // Умные уведомления: пропускаем инструменты из списка исключений. - if (cfg.notifyIgnore.length > 0 && cfg.notifyIgnore.includes(_toolLower)) { - return { success: false, skipped: true }; - } const args = detail && typeof detail === "object" ? detail.args : null; const preview = detail && typeof detail === "object" ? detail.preview : detail; + // ===== Агентские инструменты: собственная карточка, минуя batch/notifyIgnore ===== + if (AGENT_TOOLS.has(_toolLower)) { + try { + const isRunAgent = + _toolLower === "run_agent" || _toolLower === "runagent"; + + if (isRunAgent) { + // Карточка запуска уже отправлена раннером (notifyAgentStarted). + // Здесь шлём ТОЛЬКО результат (или ошибку субагента). + if (ok && preview) { + const body = _stripResultWrapper(String(preview || "")).trim(); + if (body) { + const shown = + body.length > 3500 ? body.slice(0, 3500) + "\n…(обрезано)" : body; + const resultMsg = + _t("agent.resultTitle") + + "\n\n
" + + escapeHtml(shown) + + "
"; + await telegramBot.sendMessage(resultMsg, { parseMode: "HTML" }); + } + } else if (!ok && preview) { + const errMsg = + _t("agent.failed") + + "\n\n
" + + escapeHtml(_techTrunc(String(preview), 800)) + + "
"; + await telegramBot.sendMessage(errMsg, { parseMode: "HTML" }); + } + return { success: true, special: true }; + } + + const createMsg = _renderCreateAgent(toolName, ok, args, preview); + const listMsg = createMsg + ? null + : _renderListAgents(toolName, ok, args, preview); + const readMsg = + createMsg || listMsg + ? null + : _renderReadAgent(toolName, ok, args, preview); + const special = createMsg || listMsg || readMsg; + if (special) { + await telegramBot.sendMessage(special, { parseMode: "HTML" }); + return { success: true, special: true }; + } + } catch (err) { + console.error("[bot] agent render failed:", err.message); + } + } + + // Умные уведомления: пропускаем инструменты из списка исключений. + if (cfg.notifyIgnore.length > 0 && cfg.notifyIgnore.includes(_toolLower)) { + return { success: false, skipped: true }; + } + // Спец-оформление: память и время/локация — своими красивыми сообщениями, // без общего стиля «🔧 label path» и без батчинга. if (ok) { @@ -3406,6 +3668,7 @@ module.exports = { ping, testSend, notifyToolResult, + notifyAgentStarted, notifyAIResponse, startTyping, stopTyping, diff --git a/src/main/index.js b/src/main/index.js index 41fc587..49e3118 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -446,6 +446,14 @@ subagent.injectSubagentDeps({ createWindow, profileManager }); injectAgentRunner(async ({ agent, task, currentWindowId, projectDir }) => { const ctx = windowState.getWindowContext(currentWindowId); if (!ctx) throw new Error("Контекст окна не найден"); + + // Уведомляем TG о старте делегирования ДО открытия дочернего окна — + // чтобы пользователь сразу видел карточку «делегирую задачу», + // а не ждал до конца работы субагента. + try { + require("../../botsrc").notifyAgentStarted(agent.name, task); + } catch (_) {} + return subagent.runAgent({ parentProfileId: ctx.profileId, parentWindowId: currentWindowId, diff --git a/src/preload/dom/subagent-watch.js b/src/preload/dom/subagent-watch.js index e29ba7c..5a1a6d8 100644 --- a/src/preload/dom/subagent-watch.js +++ b/src/preload/dom/subagent-watch.js @@ -5,13 +5,17 @@ * (main передал конфиг агента). Задачу и полный системный промпт main-процесс * шлёт через initial-prompt; здесь мы только ждём финальный ответ субагента * (без tool-блоков) и передаём его текст в main. + * + * Детект финала: НЕ полагаемся на isAIResponseComplete() (в deepseek.js + * он инвертирован), а используем только стабильность текста: + * - последнее AI-сообщение не менялось STABLE_MS миллисекунд + * - в нём нет cuckoo-блоков (значит, агент не вызывает инструменты) */ const { ipcRenderer } = require("electron"); const { getProviderByUrl } = require("../../../src/providers"); const { getJsCodeBlocksFromMarkdown } = require("./js-detector"); -const { isAIResponseComplete } = require("./ai-response"); -const STABLE_MS = 1200; // тишина, после которой считаем ответ финальным +const STABLE_MS = 2500; // тишина, после которой считаем ответ финальным const POLL_MS = 700; const MAX_WAIT_MS = 10 * 60 * 1000; // абсолютный лимит @@ -32,7 +36,7 @@ function readConfig() { } } -/** Текст последнего AI-сообщения (markdown-контейнер) */ +/** Текст последнего AI-сообщения (markdown-контейнер) + сам DOM */ function getLastAiText() { try { const provider = getProviderByUrl(window.location.href); @@ -52,73 +56,93 @@ function getLastAiText() { } } +/** + * Показать рабочую папку в overlay дочернего окна (двойная попытка — + * initProjectDirSection сбрасывает панель в null при инициализации). + */ +function showProjectDir(cfg) { + if (!cfg.projectDir) return; + try { + const projectDirPanel = require("../overlay/project-dir"); + const show = () => { + try { + projectDirPanel.updateProjectDirDisplay(cfg.projectDir); + console.log( + "[Cookie Code][Subagent] рабочая папка показана: " + cfg.projectDir, + ); + } catch (_) {} + }; + setTimeout(show, 800); + setTimeout(show, 2500); + } catch (err) { + console.error( + "[Cookie Code][Subagent] не удалось показать папку:", + err && err.message, + ); + } +} + /** * Если окно — субагент, запустить цикл наблюдения за финальным ответом. - * Задача и системный промпт уже отправлены main-процессом через initial-prompt. */ function initSubagentWatch() { const cfg = readConfig(); if (!cfg) return null; console.log("[Cookie Code][Subagent] активирован: " + cfg.agentName); - - // Показываем рабочую папку в overlay — тот же блок, что у родительского окна. - // Папка пришла в cfg.projectDir (main-процесс кладёт её в subagentConfig). - if (cfg.projectDir) { - try { - const projectDirPanel = require("../overlay/project-dir"); - // Двойная попытка: overlay может инициализироваться с задержкой, - // а initProjectDirSection() сбрасывает папку в null — обновляем дважды. - const showDir = () => { - try { - projectDirPanel.updateProjectDirDisplay(cfg.projectDir); - console.log( - "[Cookie Code][Subagent] рабочая папка показана: " + cfg.projectDir, - ); - } catch (_) {} - }; - setTimeout(showDir, 800); - setTimeout(showDir, 2500); - } catch (err) { - console.error( - "[Cookie Code][Subagent] не удалось показать папку:", - err && err.message, - ); - } - } + showProjectDir(cfg); const maxTurns = cfg.maxTurns || null; let turnCount = 0; let lastSentText = ""; let lastSnapshot = ""; let lastChangeAt = Date.now(); + let everSawText = false; // начал ли субагент вообще отвечать let sent = false; const startedAt = Date.now(); - const timer = setInterval(async () => { + const finish = (text, partial) => { + sent = true; + clearInterval(timer); + console.log( + "[Cookie Code][Subagent] " + + (partial ? "частичный ответ" : "финальный ответ") + + " (" + + String(text || "").length + + " символов)", + ); + ipcRenderer + .invoke("subagent-response", { + text: String(text || ""), + done: !partial, + partial: !!partial, + turns: turnCount, + }) + .catch(() => {}); + }; + + const timer = setInterval(() => { if (sent) return; if (Date.now() - startedAt > MAX_WAIT_MS) { - sent = true; - clearInterval(timer); - ipcRenderer - .invoke("subagent-response", { text: "__SUBAGENT_TIMEOUT__" }) - .catch(() => {}); + finish("__SUBAGENT_TIMEOUT__", false); return; } try { const got = getLastAiText(); if (!got || !got.text) return; + const snapshot = got.text; if (snapshot !== lastSnapshot) { + // Текст изменился — ждём стабилизации. lastSnapshot = snapshot; lastChangeAt = Date.now(); + everSawText = true; return; } + // Текст не менялся — проверяем, устоялся ли. if (Date.now() - lastChangeAt < STABLE_MS) return; - const done = await isAIResponseComplete(); - if (!done) return; - + // Устоявшийся текст. Проверяем, есть ли в нём cuckoo-блоки. const blocks = (() => { try { return getJsCodeBlocksFromMarkdown(got.md) || []; @@ -128,33 +152,18 @@ function initSubagentWatch() { })(); if (blocks.length === 0) { + // Финальный текстовый ответ (либо без блоков вообще, либо уже не + // дописывает новые). Отправляем и выходим. if (snapshot === lastSentText) return; lastSentText = snapshot; - sent = true; - clearInterval(timer); - console.log( - "[Cookie Code][Subagent] финальный ответ (" + - snapshot.length + - " символов)", - ); - ipcRenderer - .invoke("subagent-response", { text: snapshot, done: true }) - .catch(() => {}); + finish(snapshot, false); return; } + // Есть tool-блоки = промежуточный шаг. Учитываем лимит шагов. turnCount++; if (maxTurns && turnCount >= maxTurns) { - sent = true; - clearInterval(timer); - console.log("[Cookie Code][Subagent] достигнут maxTurns=" + maxTurns); - ipcRenderer - .invoke("subagent-response", { - text: snapshot, - partial: true, - turns: turnCount, - }) - .catch(() => {}); + finish(snapshot, true); } else { console.log( "[Cookie Code][Subagent] шаг " + @@ -163,6 +172,7 @@ function initSubagentWatch() { blocks.length + "), ждём продолжения", ); + // Сброс, чтобы следующая правка текста засчиталась как новое изменение. lastSnapshot = ""; } } catch (err) { @@ -173,6 +183,20 @@ function initSubagentWatch() { } }, POLL_MS); + // Страховка: если субагент не начал отвечать за 3 минуты — сообщаем + // родителю, что, похоже, отправка промпта не сработала. + setTimeout( + () => { + if (!sent && !everSawText) { + console.warn( + "[Cookie Code][Subagent] ответ не начался за 3 минуты — возможно, " + + "initial-prompt не отправился", + ); + } + }, + 3 * 60 * 1000, + ); + return cfg; } From b1b9e78338c26d45022f18cbdbe55e01de3f8a98 Mon Sep 17 00:00:00 2001 From: MerfiDEV Date: Tue, 6 Oct 2026 18:17:45 +0300 Subject: [PATCH 07/11] feat(telegram): show subagent name in tool notifications MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prefix batched tool alerts with «🤖 Агент: » when the call comes from a subagent window, and skip duplicate AI-response notifications from subagent windows. Thread agentName through JsRunner and IPC. Extend git-committer agent docs with commit/repo link report format. --- botsrc/index.js | 25 ++++++++++--- cookie/agents/git-committer.md | 65 ++++++++++++++++++++++++++++++---- src/main/ipc.js | 9 ++++- tools/JsRunner.js | 9 ++++- 4 files changed, 94 insertions(+), 14 deletions(-) diff --git a/botsrc/index.js b/botsrc/index.js index 667fdba..4be2eb6 100644 --- a/botsrc/index.js +++ b/botsrc/index.js @@ -2970,10 +2970,19 @@ function _techFlush() { if (!b) return null; const suffix = b.count > 1 ? " (×" + b.count + ")" : ""; - // Заголовок: «🔧 Editing path (×N)» либо «💻 terminal (×N)» без детали. - const header = b.command + // Заголовок: «🤖 Агент: explore // 🔧 Editing path (×N)» — если из субагента. + // Иначе просто «🔧 Editing path (×N)». + const body_line = b.command ? b.emoji + " " + b.label + suffix : b.emoji + " " + b.label + " " + _techTrunc(b.detail) + suffix; + const agentLine = b.agentName + ? "🤖 " + + escapeHtml(_t("agent.name")) + + ": " + + escapeHtml(b.agentName) + + "\n" + : ""; + const header = agentLine + body_line; // Тело: 1) команда/путь, 2) результат — каждый блок своей цитатой. let msg = header; @@ -3032,7 +3041,7 @@ const AGENT_TOOLS = new Set([ * Поставить уведомление в батч: если пришло такое же действие в течение * TECH_BATCH_MS — увеличиваем счётчик (×N), иначе шлём предыдущее и начинаем новое. */ -function _techQueue(toolName, args, preview) { +function _techQueue(toolName, args, preview, agentName) { const icon = _techIcon(toolName); const detail = _techDetail(toolName, args); const name = String(toolName || "").toLowerCase(); @@ -3041,13 +3050,15 @@ function _techQueue(toolName, args, preview) { ? String((args && args.command) || "") : ""; const result = String(preview || ""); + const agent = agentName ? String(agentName).trim() : null; if ( _techBatch && _techBatch.emoji === icon.emoji && _techBatch.label === icon.label && _techBatch.detail === detail && - _techBatch.command === command + _techBatch.command === command && + _techBatch.agentName === agent ) { _techBatch.count++; // Результат последнего вызова в серии — показываем его. @@ -3062,6 +3073,7 @@ function _techQueue(toolName, args, preview) { preview: result, args, count: 1, + agentName: agent, }; if (prev) _techFlush(); if (_techBatchTimer) clearTimeout(_techBatchTimer); @@ -3503,7 +3515,10 @@ async function notifyToolResult(toolName, ok, detail) { } // Успех — в батч (одинаковые подряд схлопываются в ×N). - _techQueue(toolName, args, preview); + // Если вызов из субагента — в заголовок попадёт «🤖 Агент: <имя>». + const agentName = + detail && typeof detail === "object" ? detail.agentName || null : null; + _techQueue(toolName, args, preview, agentName); return { success: true, batched: true }; } diff --git a/cookie/agents/git-committer.md b/cookie/agents/git-committer.md index 25e1b9b..0449dd0 100644 --- a/cookie/agents/git-committer.md +++ b/cookie/agents/git-committer.md @@ -118,6 +118,41 @@ await bash("git push"); // обычный push - **Никогда не делай `git push --force`** без прямой просьбы пользователя. - Если push отклонён (`non-fast-forward`) — **не форсируй**. Скажи пользователю и предложи `git pull --rebase`. +### 7. Собрать ссылки на коммит и репозиторий + +После push **обязательно** собери ссылки: + +```cuckoo +// Полный SHA коммита (для ссылки) +const sha = await bash("git rev-parse HEAD"); +log(sha); + +// URL удалённого репозитория — нормализуем в https-формат для ссылки +const remote = await bash("git remote get-url origin"); +log(remote); + +// Текущая ветка +const branch = await bash("git rev-parse --abbrev-ref HEAD"); +log(branch); +``` + +**Преобразование SSH → HTTPS** (если `origin` в формате git@github.com:...): + +- `git@github.com:user/repo.git` → `https://github.com/user/repo` +- `https://github.com/user/repo.git` → `https://github.com/user/repo` + +**Формирование ссылок:** + +- **Репозиторий:** `` +- **Коммит:** `/commit/` +- **Для GitHub:** `https://github.com///commit/` +- **Для GitLab:** `https://gitlab.com///-/commit/` +- **Для Bitbucket:** `https://bitbucket.org///commits/` + +Используй `git config --get remote.origin.url` если `git remote get-url origin` недоступен. +Для мультиплатформенности можно получить хост через `git remote show origin` — но проще +парсить URL регулярно: определить хост (github/gitlab/bitbucket) и подставить правильный путь. + ## Что ты НЕ делаешь - Не пуш в `main`/`master` напрямую, если проект работает через PR (проверь по истории `git log --first-parent`). @@ -128,17 +163,33 @@ await bash("git push"); // обычный push ## Формат отчёта в родительский диалог -Верни **краткую** сводку: +Верни **структурированную** сводку с обязательными **ссылками**: ``` Закоммичено и запушено. -Коммиты: -- abc1234 feat(auth): add JWT refresh token rotation (3 файла) -- def5678 fix(chat): prevent duplicate messages on reconnect (1 файл) - +Репозиторий: https://github.com/user/repo Ветка: feature/auth-jwt -Удалённый: origin/feature/auth-jwt (обновлён) + +Коммиты: +1. https://github.com/user/repo/commit/abc1234... + feat(auth): add JWT refresh token rotation (3 файла) +2. https://github.com/user/repo/commit/def5678... + fix(chat): prevent duplicate messages on reconnect (1 файл) ``` -Если что-то не сделал (например, остановился из-за секрета) — чётко напиши почему и что нужно от пользователя. +**Обязательные элементы отчёта:** + +1. **Ссылка на репозиторий** — `` (нормализованный в https). +2. **Ссылка на каждый коммит** — `/commit/` (или аналог для GitLab/Bitbucket). +3. **Ветка** — куда запушено. +4. **Краткое описание** каждого коммита (subject). +5. **Список файлов** — количество или перечисление. + +Если push **не удался**: + +- Чётко напиши ошибку. +- НЕ выдумывай ссылки — покажи только то, что реально существует локально (SHA есть, ссылка — нет, т.к. коммит не на удалённом). +- Предложи действие: `git pull --rebase` / ручной push / разбор конфликта. + +Если остановился из-за секрета — сообщи: какой файл, что нашёл, что нужно от пользователя. diff --git a/src/main/ipc.js b/src/main/ipc.js index 6efb740..f3ba4ac 100644 --- a/src/main/ipc.js +++ b/src/main/ipc.js @@ -1143,6 +1143,7 @@ function registerIpcHandlers() { (payload) => attachFileToChat(event.sender, payload), currentWindowId, ctx && Array.isArray(ctx.toolsWhitelist) ? ctx.toolsWhitelist : null, + (ctx && ctx.subagentConfig && ctx.subagentConfig.agentName) || null, ); if (isTaskCanceled(event.sender.id, taskToken)) { return { @@ -1238,8 +1239,14 @@ function registerIpcHandlers() { }); // Ответ AI → в Telegram (для просмотра с телефона). - ipcMain.handle("telegram-notify-ai", async (_event, { text } = {}) => { + // ВАЖНО: если ответ пришёл из окна-субагента — НЕ шлём (у субагента + // отдельное оформление: notifyAgentStarted + результат через notifyToolResult). + ipcMain.handle("telegram-notify-ai", async (event, { text } = {}) => { try { + const ctx = windowState.getContextByWebContents(event.sender); + if (ctx && ctx.isSubagent) { + return { success: false, skipped: true, reason: "subagent window" }; + } const bot = require("../../botsrc"); return await bot.notifyAIResponse(text); } catch (err) { diff --git a/tools/JsRunner.js b/tools/JsRunner.js index 5b5ae1d..b5ddbff 100644 --- a/tools/JsRunner.js +++ b/tools/JsRunner.js @@ -375,6 +375,7 @@ class JsRunner { attachFile, currentWindowId, toolsWhitelist, + agentName, ) { if (!code || typeof code !== "string" || !code.trim()) { return { success: false, error: "无效的 JS 代码" }; @@ -494,7 +495,13 @@ class JsRunner { const exitMatch = String(preview).match(/\[exit code:\s*(-?\d+)\]/); const hasBadExit = !!(exitMatch && exitMatch[1] !== "0"); const ok = !!result.success && !hasBadExit; - notifyToolResult(label, ok, { args: args, preview: preview }); + notifyToolResult(label, ok, { + args: args, + preview: preview, + // Если вызов идёт из окна-субагента — указываем имя агента, + // чтобы в TG уведомление было в стиле «🤖 Агент: explore → 📖 read». + agentName: agentName || null, + }); } catch (_) {} return JSON.stringify(result); }; From 2f6dd205f452cbd4acd2973a2f14320a9db35e11 Mon Sep 17 00:00:00 2001 From: MerfiDEV Date: Tue, 6 Oct 2026 18:18:39 +0300 Subject: [PATCH 08/11] fix(git): track cookie/agents in repo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Папка cookie/agents была в .gitignore — новые агенты (созданные через create_agent) оставались untracked. Убрал игнор: агенты проекта должны быть в git. Пользовательские агенты и так лежат в ~/cookie/agents вне репозитория. --- .gitignore | 4 +- cookie/agents/changelog-writer.md | 133 ++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 cookie/agents/changelog-writer.md diff --git a/.gitignore b/.gitignore index f0f0cee..54ff5d9 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,6 @@ test/coverage.lcov .playwright-mcp/ /tiktok /.cuckoo/skills -/cookie/agents +# cookie/agents — НЕ игнорируем: агенты проекта должны попадать в git. +# Пользовательские агенты лежат в ~/cookie/agents (вне репозитория). +# Ранее здесь был /cookie/agents — убрано, иначе новые агенты untracked. diff --git a/cookie/agents/changelog-writer.md b/cookie/agents/changelog-writer.md new file mode 100644 index 0000000..7417398 --- /dev/null +++ b/cookie/agents/changelog-writer.md @@ -0,0 +1,133 @@ +--- +name: changelog-writer +description: Ведёт CHANGELOG.md по правилам проекта. Добавляет записи о новых версиях в правильном формате, который парсит What's New и CI. Использовать когда нужно задокументировать изменения перед релизом. +tools: read, readLines, edit, write, glob, grep, bash +maxTurns: 20 +--- + +Ты — релиз-инженер, отвечающий за `CHANGELOG.md` и `CHANGELOG_GUIDE.md`. +Твоя задача — превратить набор изменений в правильно оформленную запись, +которую корректно распарсит `src/main/changelog-parser.js` и покажет в What's New. + +## Первым делом + +**Прочитай `CHANGELOG_GUIDE.md`** — там описан точный формат, который понимает парсер. +Правила в гайде — истина в последней инстанции. Если гайд и твой опыт +расходятся — следуй гайду. + +Затем прочитай текущий `CHANGELOG.md` (последние 2-3 версии), чтобы: + +- понять стиль проекта (русский, эмодзи, жирные заголовки); +- узнать последнюю версию — новую ставим ПОСЛЕ неё; +- не дублировать уже описанные пункты. + +## Алгоритм + +### 1. Разведка изменений + +Узнай, что изменилось с последнего релиза: + +```cuckoo +const last = await read("CHANGELOG.md", { limit: 5 }); +log(last); +const status = await bash("git log --oneline --since=\"1 week ago\""); +log(status); +const diff = await bash("git diff --stat HEAD~20..HEAD"); +log(diff); +``` + +Собери из истории коммитов и diffs понимание: какие фичи, фиксы, рефакторинг. + +### 2. Определи версию + +Возьми последнюю версию из CHANGELOG.md и подними: + +- **patch** (x.y.**Z**) — багфиксы, мелкие правки; +- **minor** (x.**Y**.0) — новые фичи, совместимые; +- **major** (**X**.0.0) — breaking changes. + +### 3. Составь запись + +Строго по шаблону из гайда: + +```markdown +## [X.Y.Z] - YYYY-MM-DD + +### Added + +- **Краткое название фичи** — что и зачем. Детали: + - подпункт; + - ещё подпункт. + +### Changed + +- **Что изменилось** — было → стало. Причины. + +### Fixed + +- Что было сломано и как починили. + +### Removed + +- Что удалили и почему. +``` + +**Правила:** + +- Секции `###` (три решётки), только `Added`/`Changed`/`Fixed`/`Removed`. +- Пункты — `- ` или `* `, никогда нумерованные. +- Язык русский, стиль существующего CHANGELOG. +- Названия фич **жирным**, имена кода в `бэктиках`. +- Детали — вложенными пунктами (отступ 2 пробела). +- Не выдумывай фичи — пиши только то, что реально видно в diff/логе. + +### 4. Вставка в CHANGELOG.md + +Новая запись идёт **в самый верх**, сразу под `# Changelog`. +Используй `edit`: найди строку `# Changelog` и вставь блок после неё. + +```cuckoo +await edit("CHANGELOG.md", + "# Changelog\n\n## [5.0.0]", + "# Changelog\n\n## [5.1.0] - 2026-10-06\n\n### Added\n\n- ...\n\n## [5.0.0]" +); +``` + +### 5. Проверка + +Убедись, что парсер видит новую версию: + +```cuckoo +const check = await bash(`node -e "const fs=require('fs'); const {parseChangelog}=require('./src/main/changelog-parser'); const e=parseChangelog(fs.readFileSync('CHANGELOG.md','utf8')); console.log(e.slice(0,5).map(x=>x.version).join(', '));"`); +log(check); +``` + +Новая версия должна быть **первой** в выводе. + +## Жёсткие правила + +1. **НЕ трогай `package.json`** — версию туда выставит CI из git-тега. +2. **НЕ создавай коммит и НЕ пушь** — это делает родительский диалог отдельно. +3. **НЕ выдумывай изменения** — только то, что реально видно в git history. +4. **НЕ переписывай старые записи** — только добавь новую сверху. +5. **НЕ меняй `CHANGELOG_GUIDE.md`** — если найдёшь проблему в гайде, сообщи в отчёте. +6. Формат заголовка — **строго** `## [X.Y.Z] - YYYY-MM-DD`. Скобки, дефис, ISO-дата. + +## Формат отчёта родителю + +``` +Добавлена версия X.Y.Z (дата YYYY-MM-DD). + +Секции: +- Added: N пунктов +- Changed: N пунктов +- Fixed: N пунктов +- Removed: N пунктов + +Проверка парсером: ✅ новая версия первая в списке. + +Что НЕ вошло (сомнительные изменения): +- ... +``` + +Если не смог определить версию или изменения неоднозначны — **спроси**, не гадай. From 54428b65abd246437eaf2c275e0feaa030341235 Mon Sep 17 00:00:00 2001 From: MerfiDEV Date: Tue, 6 Oct 2026 18:45:54 +0300 Subject: [PATCH 09/11] docs(subagents): document subagents and rewrite changelog for 5.0.2 Add a Subagents section with screenshots to all three READMEs, rewrite CHANGELOG to keep only the 5.0.2 entry (patch+1 rule), and update changelog-writer agent to bump patch+1 and fully replace the file. --- CHANGELOG.md | 101 ++++----------- README.en.md | 104 ++++++++++++---- README.md | 48 +++++++- README.ru.md | 67 ++++++++++ assets/photo_2026-10-06_18-24-20.jpg | Bin 0 -> 41144 bytes assets/photo_2026-10-06_18-24-55.jpg | Bin 0 -> 43234 bytes cookie/agents/changelog-writer.md | 178 +++++++++++++++++++++------ 7 files changed, 354 insertions(+), 144 deletions(-) create mode 100644 assets/photo_2026-10-06_18-24-20.jpg create mode 100644 assets/photo_2026-10-06_18-24-55.jpg diff --git a/CHANGELOG.md b/CHANGELOG.md index b771b3b..41d8272 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,90 +1,31 @@ # Changelog -## [5.0.0] - 2024-10-05 - -### 🎉 Major Release — Cookie Code - -**Breaking Changes:** -- 🍪 **Rebrand: Cuckoo Code → Cookie Code** — новое название, новая идентичность: - - все пользовательские строки обновлены на "Cookie Code"; - - внутренние API и техническая терминология остались без изменений для совместимости. +## [5.0.2] - 2026-10-06 ### Added -- 📚 **Билингвальная документация** — теперь три README для разных аудиторий: - - `README.md` (12K, русский) — краткая версия для русских пользователей; - - `README.en.md` (12K, английский) — полная версия с чётким value proposition для международной аудитории; - - `README.ru.md` (25K, русский) — полная техническая документация на русском. -- 🎨 **SVG-иконки вместо эмодзи** — заменили 🔄 на чистые SVG: - - кнопка "Изменить" (проект) — иконка карандаша/редактирования; - - кнопка "Обновить" (сессии, окна, MCP, diff) — иконка круговой стрелки; - - `stroke="currentColor"` — автоматическая адаптация к цвету текста; - - компактные 14×14px иконки с чёткими линиями. -- 🔧 **Улучшенный diff viewer** — 8 крупных апгрейдов: - - навигация по hunks с кнопками ◀/▶ и хоткеями `n`/`p`; - - внутристроковый diff (алгоритм Myers) — подсветка изменённых символов; - - копирование — кнопка Copy для всего diff, клик на строку копирует без префикса; - - фильтры — чекбоксы Added/Deleted/Context для скрытия ненужных строк; - - мини-карта — вертикальная полоса с цветными сегментами изменений; - - статистика — `📊 +234 -89 в 12 файлах` в списке; - - кэширование — результаты git-команд сохраняются в Map; - - перетаскиваемое окно — захват за шапку, сохранение позиции в localStorage. -- 📱 **Построчный diff в Telegram-уведомлениях** — операции `edit` теперь показывают реальные изменения: - - вместо "было/стало" — unified diff с префиксами `+`/`-`/` `; - - до 30 строк diff, затем `…(обрезано)`; - - используется тот же `buildLineDiff` из `tools/line-diff.js`, что и в UI. +- 🚀 **Субагенты — AI создаёт своих помощников** — задачи можно делегировать изолированным AI-агентам, каждый в своём окне и со своим контекстом. Черновики и tool-вызовы субагента не засоряют основной диалог — родителю возвращается только финальное резюме: + - новый инструмент `run_agent(name, task)` — запускает агента в дочернем окне (тот же `partition` → общая сессия DeepSeek), ждёт финальный ответ и возвращает текст родителю; + - `list_agents` — список доступных субагентов (проект + пользователь) с `description`, `tools`, `maxTurns`; + - `read_agent(name)` — полный `systemPrompt` агента для отладки; + - `create_agent({name, description, systemPrompt, tools?, maxTurns?, overwrite?})` — AI создаёт специалиста под задачу, файл сразу доступен для `run_agent`; + - сканер агентов: `cookie/agents/*.md` (проект) и `~/cookie/agents/*.md` (пользовательские), формат — markdown с frontmatter (`name`, `description`, `tools`, `maxTurns`); + - готовые агенты в проекте: `explore`, `code-reviewer`, `git-committer`, `changelog-writer`. +- 🔒 **Whitelist инструментов для субагентов** — в frontmatter агента можно указать `tools: read, grep`, и агент физически не сможет вызвать `bash` или `mysql`: + - список прокидывается в контекст дочернего окна и проверяется в `JsRunner` до исполнения; + - запрещённый вызов отклоняется с явной ошибкой «Инструмент … не разрешён этому агенту»; + - защита от рекурсии: субагент не может вызвать `run_agent`. +- 🤖 **Уведомления о жизненном цикле субагента в Telegram** — стартовая карточка перед открытием дочернего окна, события `run`/`create`/`list`/`read`, а имена агентов в алертах вызовов инструментов помечаются префиксом «🤖 Агент: <имя>»: + - дублирующие уведомления об ответах AI из субагентских окон подавляются; + - завершение субагента отслеживается по стабилизации текста (а не по legacy-проверке «AI завершил»), с учётом лимита ходов, таймаута и отображения рабочей папки. +- 📚 **Новый раздел README про субагентов** — как делегировать, три способа завести агента, что защищает (whitelist, anti-recursion, `maxTurns`, timeout) и пример агента. ### Changed -- 📖 **Новая структура README** — English-first для GitHub-аудитории: - - чёткая проблема → решение в первых строках; - - value proposition: "Zero token cost + Real agent loop + Git integration"; - - конкретные примеры использования вместо технического описания; - - призыв к действию — скачать бинарники. - -### Removed - -- 🗑️ **Устаревшие документы удалены из репозитория**: - - `ROADMAP.ru.md` и `Roadmap.md` — 3-недельной давности, больше не актуальны; - - `.cuckooCode/AUTO_UPDATE_RELEASE.md` — документация по auto-update на китайском; - - `DIFF_IMPROVEMENTS.md` — временный документ с описанием улучшений (все реализованы). - -### Added (EN) - -- 📚 **Bilingual documentation** — three README files for different audiences: - - `README.md` (12K, Russian) — concise version for Russian users; - - `README.en.md` (12K, English) — full version with clear value proposition for international audience; - - `README.ru.md` (25K, Russian) — complete technical documentation in Russian. -- 🎨 **SVG icons instead of emoji** — replaced 🔄 with clean SVG: - - "Change" button (project) — pencil/edit icon; - - "Refresh" button (sessions, windows, MCP, diff) — circular arrow icon; - - `stroke="currentColor"` — automatic adaptation to text color; - - compact 14×14px icons with sharp lines. -- 🔧 **Enhanced diff viewer** — 8 major upgrades: - - hunk navigation with ◀/▶ buttons and `n`/`p` hotkeys; - - intra-line diff (Myers algorithm) — highlights changed characters; - - copy — Copy button for full diff, click on line copies without prefix; - - filters — Added/Deleted/Context checkboxes to hide unwanted lines; - - mini-map — vertical bar with colored change segments; - - statistics — `📊 +234 -89 in 12 files` in the list; - - caching — git command results saved in Map; - - draggable window — grab by header, position saved in localStorage. -- 📱 **Line-by-line diff in Telegram notifications** — `edit` operations now show real changes: - - instead of "before/after" — unified diff with `+`/`-`/` ` prefixes; - - up to 30 lines of diff, then `…(truncated)`; - - uses the same `buildLineDiff` from `tools/line-diff.js` as the UI. - -### Changed (EN) - -- 📖 **New README structure** — English-first for GitHub audience: - - clear problem → solution in the first lines; - - value proposition: "Zero token cost + Real agent loop + Git integration"; - - concrete usage examples instead of technical description; - - call to action — download binaries. +- 📁 **Хранилище агентов переехало `.cuckoo/agents` → `cookie/agents`** — проектные агенты лежат в репозитории рядом с кодом, пользовательские — в `~/cookie/agents`. Сканер, типы и документация обновлены. +- 🎛 **Диалог с AI в дочернем окне получает полный системный промпт Cookie Code** (инструменты + формат `cuckoo`) вместе с промптом агента и задачей — субагент работает так же, как основной ассистент. -### Removed (EN) +### Fixed -- 🗑️ **Obsolete documents removed from repository**: - - `ROADMAP.ru.md` and `Roadmap.md` — 3 weeks old, no longer relevant; - - `.cuckooCode/AUTO_UPDATE_RELEASE.md` — auto-update documentation in Chinese; - - `DIFF_IMPROVEMENTS.md` — temporary document describing improvements (all implemented). +- 🐛 **Потеря рабочей папки субагента** — при загрузке домашней страницы DeepSeek (когда `sessionId` ещё нет) выбранная папка проекта сбрасывалась в `null`, и инструменты субагента видели `projectDir = null`. Добавлен `pendingProjectDir`: папка кэшируется до готовности сессии, прокидывается в контекст субагента и привязывается к сессии автоматически. +- 🗂 **`cookie/agents` теперь трекается в git** — папка была в `.gitignore`, из-за чего агенты, созданные через `create_agent`, оставались untracked. Игнор убран: агенты проекта должны попадать в репозиторий (пользовательские лежат вне репо). diff --git a/README.en.md b/README.en.md index 0e0b267..d135eef 100644 --- a/README.en.md +++ b/README.en.md @@ -15,6 +15,11 @@ Cookie Code — main window

+

+ Cookie Code — subagents + Cookie Code — control panel +

+ **Turn DeepSeek into a real coding assistant.** Cookie Code bridges the gap between AI chat and your actual codebase — no API tokens, no copy-paste, just pure automation. --- @@ -27,9 +32,10 @@ ✅ **Zero token cost** — uses your free DeepSeek web account, no API keys ✅ **Real agent loop** — AI reads files → writes code → runs tests → commits changes +✅ **Subagents** — AI delegates tasks to isolated agents (own context, own tool whitelist) ✅ **Git integration** — see diffs, browse history, review changes in a visual panel ✅ **Telegram control** — code from your phone, get notifications, approve commands remotely -✅ **Beautiful UI** — glass effects, custom themes, animated pets, dark mode +✅ **Beautiful UI** — glass effects, custom themes, animated pets, dark mode --- @@ -38,6 +44,7 @@ ### 🤖 **AI Agent That Actually Does Things** The AI can now: + - 📖 **Read your files** — entire project structure, grep patterns, file contents - ✏️ **Write & edit code** — create files, apply patches, delete old code - 💻 **Run commands** — bash, PowerShell, database queries, any shell script @@ -47,11 +54,52 @@ The AI can now: - 🔧 **MCP tools** — connect any MCP server for extended capabilities **How it works:** + 1. You ask: "Show me recent changes in auth.js" 2. AI emits: `` `cuckoo const r = await grep('auth.js', 'login');` `` 3. Cookie Code intercepts, executes, streams result back to AI 4. AI sees the output and continues working +### 🚀 **Subagents — AI Builds Its Own Helpers** + +Cookie Code can **delegate tasks to isolated AI agents**, each in its own window with its own context. The subagent's drafts, searches, and tool calls **never pollute the main conversation** — only the final summary comes back to the parent. + +**How it works:** + +1. You: _"Delegate to explore: find all calls to apiFetch"_ +2. AI calls `run_agent("explore", "find all calls to apiFetch")` +3. A **separate window** opens using the same DeepSeek login +4. The subagent works in isolation: `glob → read → grep → ...` +5. When done, the window closes and **only the summary** appears in the chat + +**Three ways to create an agent:** + +- **Manually** — drop `cookie/agents/.md` with frontmatter into your project +- **Via AI** — say _"Create an agent for SQL migration review"_ → AI calls `create_agent` and uses it immediately +- **Built-in** — `explore`, `code-reviewer`, `git-committer`, `changelog-writer` ship with the project + +**Safety rails:** + +- 🔒 **Tool whitelist** — put `tools: read, grep` in the agent's frontmatter → it **physically cannot** call `bash` or `mysql` +- 🚫 **Anti-recursion** — a subagent cannot spawn another subagent +- ⏱ **maxTurns** — breaks infinite loops (e.g., 40 steps for the committer) +- ⏲ **10-min timeout** — force-closes a hung window + +**Agent example:** + +```markdown +--- +name: code-reviewer +description: Review code quality. Use after writing code. +tools: read, grep, glob +maxTurns: 20 +--- + +You are an experienced code review expert... +``` + +--- + ### 🎨 **Beautiful & Customizable**

@@ -94,6 +142,7 @@ Control everything from your phone:

Built-in diff viewer: + - **Changes tab** — see uncommitted changes, file-by-file diffs - **History tab** — browse commits, click to see full diff - **Navigation** — jump between hunks, filter added/deleted lines @@ -103,6 +152,7 @@ Built-in diff viewer: ### 📈 **Statistics Dashboard** Track your productivity: + - **Activity heatmap** — GitHub-style contribution graph - **Token usage** — daily/weekly trends - **Favorite models** — which AI you use most @@ -114,12 +164,14 @@ Track your productivity: ### Option 1: Download Binary (Recommended) -**Windows:** +**Windows:** + 1. Download [CookieCode-Setup.exe](https://github.com/merfiDEV/Cookie-code/releases/latest) 2. Run installer 3. Launch Cookie Code -**macOS:** +**macOS:** + 1. Download [CookieCode.dmg](https://github.com/merfiDEV/Cookie-code/releases/latest) 2. Drag to Applications 3. Open (right-click → Open if Gatekeeper blocks) @@ -178,6 +230,7 @@ AI: [runs git add + git commit] ### 3️⃣ **Explore Tools** Open the side panel (Cookie Code logo) to: + - 🗂️ **Browse sessions** — switch between project contexts - 📝 **Manage todos** — see AI-created task lists - 🔄 **View git diff** — visual file changes @@ -192,6 +245,7 @@ Open the side panel (Cookie Code logo) to: **Current limitation:** AI only knows files it explicitly reads. **Planned feature:** Full project indexing + - Embed all source files into a vector database - AI can semantic search: "Where's the auth logic?" - Instant jump-to-definition for any symbol @@ -222,13 +276,13 @@ Drop `.js` files into `tools/custom/` to extend Cookie Code: ```javascript // tools/custom/deploy.js module.exports = { - name: 'deploy', - description: 'Deploy to production', - params: { target: 'string' }, + name: "deploy", + description: "Deploy to production", + params: { target: "string" }, async execute({ target }) { // Your deployment logic - return { success: true, url: '...' }; - } + return { success: true, url: "..." }; + }, }; ``` @@ -240,20 +294,20 @@ AI can now call `deploy` like any built-in tool. Cookie Code ships with 20+ tools out of the box: -| Tool | Description | Example | -|------|-------------|---------| -| `read` | Read file contents | `read('src/auth.js')` | -| `write` | Create or overwrite file | `write('test.js', code)` | -| `edit` | Apply targeted patch | `edit('app.js', oldCode, newCode)` | -| `glob` | Find files by pattern | `glob('**/*.test.js')` | -| `grep` | Search file contents | `grep('TODO', { include: '*.js' })` | -| `bash` | Run shell command | `bash('npm test')` | -| `pwsh` | Run PowerShell script | `pwsh('Get-Process')` | -| `git` | Git operations | `git('status')` | -| `mysql` | SQL queries | `mysql('SELECT * FROM users')` | -| `webFetch` | HTTP requests | `webFetch('https://api.example.com')` | -| `todoWrite` | Manage task lists | `todoWrite([{content: 'Fix bug'}])` | -| `mcpCall` | Call MCP server tool | `mcpCall('github', 'create_pr', {...})` | +| Tool | Description | Example | +| ----------- | ------------------------ | --------------------------------------- | +| `read` | Read file contents | `read('src/auth.js')` | +| `write` | Create or overwrite file | `write('test.js', code)` | +| `edit` | Apply targeted patch | `edit('app.js', oldCode, newCode)` | +| `glob` | Find files by pattern | `glob('**/*.test.js')` | +| `grep` | Search file contents | `grep('TODO', { include: '*.js' })` | +| `bash` | Run shell command | `bash('npm test')` | +| `pwsh` | Run PowerShell script | `pwsh('Get-Process')` | +| `git` | Git operations | `git('status')` | +| `mysql` | SQL queries | `mysql('SELECT * FROM users')` | +| `webFetch` | HTTP requests | `webFetch('https://api.example.com')` | +| `todoWrite` | Manage task lists | `todoWrite([{content: 'Fix bug'}])` | +| `mcpCall` | Call MCP server tool | `mcpCall('github', 'create_pr', {...})` | [Full tool reference →](docs/TOOLS.md) @@ -316,12 +370,14 @@ Free to use, modify, and distribute. Commercial use allowed. ## 🙏 Credits Built with: + - [Electron](https://www.electronjs.org/) — desktop app framework - [DeepSeek](https://www.deepseek.com/) — AI chat interface - [Telegraf](https://github.com/telegraf/telegraf) — Telegram bot framework - [gifenc](https://github.com/mattdesl/gifenc) — GIF processing Inspired by: + - [Aider](https://github.com/paul-gauthier/aider) — AI pair programming in terminal - [Claude Desktop](https://claude.ai/download) — MCP protocol pioneer - [Cursor](https://cursor.sh/) — AI-first code editor @@ -341,8 +397,8 @@ If Cookie Code saves you time, consider giving it a star! It helps others discov - **Documentation:** [docs/](docs/) - **Changelog:** [CHANGELOG.md](CHANGELOG.md) - **Roadmap:** [GitHub Projects](https://github.com/merfiDEV/Cookie-code/projects) -- **Discord:** [Join the community](https://discord.gg/...) *(coming soon)* -- **Twitter:** [@CookieCodeAI](https://twitter.com/...) *(coming soon)* +- **Discord:** [Join the community](https://discord.gg/...) _(coming soon)_ +- **Twitter:** [@CookieCodeAI](https://twitter.com/...) _(coming soon)_ --- diff --git a/README.md b/README.md index 4f75ff0..069d039 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,12 @@

- Cookie Code — main window + Cookie Code — главное окно +

+ +

+ Cookie Code — субагенты + Cookie Code — панель управления

**Превратите DeepSeek в настоящего помощника по коду.** Cookie Code соединяет AI-чат с вашей реальной кодовой базой — без API-токенов, без копипасты, только чистая автоматизация. @@ -27,6 +32,7 @@ ✅ **Нулевая стоимость токенов** — использует ваш бесплатный веб-аккаунт DeepSeek, без API-ключей ✅ **Настоящий агентный цикл** — AI читает файлы → пишет код → запускает тесты → коммитит изменения +✅ **Субагенты** — AI делегирует задачи изолированным агентам (свой контекст, свой whitelist инструментов) ✅ **Интеграция Git** — смотрите diff, листайте историю, проверяйте изменения визуально ✅ **Управление через Telegram** — пишите код с телефона, получайте уведомления, подтверждайте команды удалённо ✅ **Красивый UI** — стеклянные эффекты, кастомные темы, анимированные петы, тёмная тема @@ -55,6 +61,46 @@ 3. Cookie Code перехватывает, выполняет, стримит результат обратно в AI 4. AI видит вывод и продолжает работать +### 🚀 **Субагенты — AI создаёт своих помощников** + +Cookie Code умеет **делегировать задачи изолированным AI-агентам**, каждый в своём окне и со своим контекстом. Черновики, поиски и tool-вызовы субагента **не засоряют основной диалог** — родителю возвращается только финальное резюме. + +**Как это работает:** + +1. Вы: _«Делегируй explore: найди все вызовы apiFetch»_ +2. AI вызывает `run_agent("explore", "...", "...")` +3. Открывается **отдельное окно** с тем же логином DeepSeek +4. Субагент работает в изоляции: `glob → read → grep → ...` +5. Когда закончит — окно закрывается, в чат приходит **только резюме** + +**Три способа завести агента:** + +- **Вручную** — положите `cookie/agents/<имя>.md` с frontmatter в проект +- **Через AI** — скажите _«Создай агента для ревью SQL-миграций»_ → AI вызовет `create_agent` и сразу его использует +- **Готовые** — `explore`, `code-reviewer`, `git-committer`, `changelog-writer` уже в проекте + +**Что защищает:** + +- 🔒 **Whitelist инструментов** — в frontmatter агента указываете `tools: read, grep` → агент **физически не сможет** вызвать `bash` или `mysql` +- 🚫 **Anti-recursion** — субагент не может запустить ещё одного субагента +- ⏱ **maxTurns** — обрубает зацикливание (например, 40 шагов для коммиттера) +- ⏲ **Timeout 10 мин** — принудительно закрывает зависшее окно + +**Пример агента:** + +```markdown +--- +name: code-reviewer +description: Проверка качества кода. Использовать после написания кода. +tools: read, grep, glob +maxTurns: 20 +--- + +Ты — опытный эксперт по ревью кода... +``` + +--- + ### 🎨 **Красиво и настраиваемо** - **Стеклянный UI** — матовое стекло с настраиваемым размытием и прозрачностью diff --git a/README.ru.md b/README.ru.md index f30c0b8..3266286 100644 --- a/README.ru.md +++ b/README.ru.md @@ -12,6 +12,11 @@ English · Русский

+

+ Cookie Code — субагенты + Cookie Code — панель управления +

+ **Cookie Code** — AI-агент для рабочего стола с **нулевой стоимостью токенов**. Приложение встраивает веб-чат DeepSeek в нативное окно Electron, добавляет боковой оверлей и превращает чат в локальный исполнитель: AI генерирует вызовы инструментов (блоки JavaScript-кода), которые перехватываются, подтверждаются, выполняются в локальной песочнице и стримятся обратно в чат. Никакого API-ключа, никакой оплаты за токены — вы используете обычный веб-аккаунт. @@ -143,6 +148,64 @@ Поместите папку в `.cuckoo/skills//` с файлом `SKILL.md` (и опциональным `tool.js`) — она станет вызываемой через `skillList`, `skillLoad`, `skillExecute`. +### Субагенты + +Cookie Code умеет **делегировать задачи изолированным AI-агентам**. Каждый субагент работает в **отдельном окне** с тем же partition (общий логин DeepSeek), в **своём контексте**, и возвращает родителю **только финальное резюме** — черновики, поиски и tool-вызовы не засоряют основной диалог. + +**Пример:** + +1. Вы: _«Делегируй explore: найди все вызовы apiFetch»_ +2. AI вызывает `run_agent("explore", "найди все вызовы apiFetch")` +3. Открывается второе окно, туда автоматически уходит системный промпт + задача +4. Субагент работает: `glob → read → grep → ...` +5. Когда напишет финал **без** `cuckoo`-блоков — окно закрывается, в родительский чат приходит только резюме + +**Три способа завести агента:** + +- **Вручную** — положите `cookie/agents/.md` с frontmatter в проект +- **Через AI** — попросите: _«Создай агента для ревью SQL-миграций»_ — AI вызовет `create_agent` и сможет его сразу использовать +- **Готовые** — в проекте уже есть `explore`, `code-reviewer`, `git-committer`, `changelog-writer` + +**Формат файла агента** (`<проект>/cookie/agents/<имя>.md`): + +```markdown +--- +name: code-reviewer +description: Проверка качества кода. Использовать после написания кода. +tools: read, grep, glob +maxTurns: 20 +--- + +Ты — опытный эксперт по ревью кода. Проверь указанный код, найди +реальные проблемы и дай рекомендации. +``` + +**Поля frontmatter:** + +| Поле | Обязательно | Описание | +| ------------- | ----------- | ---------------------------------------------------------- | +| `name` | нет | Имя агента (по умолчанию — имя файла без `.md`) | +| `description` | **да** | Когда делегировать. Без него агент игнорируется | +| `tools` | нет | Список разрешённых инструментов через запятую. Пусто = все | +| `maxTurns` | нет | Лимит шагов 1..1000 (защита от зацикливания) | + +**Изоляция и защита:** + +- 🔒 **Whitelist инструментов** — `tools: read, grep` → агент **физически не сможет** вызвать `bash`, `mysql`, `deleteFile` — блокировка на двух уровнях (JsRunner + main IPC) +- 🚫 **Anti-recursion** — субагент не может вызвать `run_agent` +- ⏱ **maxTurns** — обрубает бесконечные циклы +- ⏲ **Timeout 10 мин** — принудительно закрывает зависшее окно +- 📁 **Общий projectDir** — субагент видит ту же рабочую папку, что родитель +- 🎯 **Прогрессивное раскрытие** — в системный промпт родителя идут только `name` + `description` каждого агента, полный `systemPrompt` читается только при запуске + +**Уведомления в Telegram:** + +При делегировании в TG приходят: + +1. Карточка запуска: `🚀 ИИ делегирует задачу субагенту` + имя + задача +2. Каждый tool-вызов агента: `🤖 Агент: explore` + `📖 Reading file.js` +3. Финальный результат: `🎯 Субагент вернул результат` + резюме + ### Авто-форматтеры Каждый `write` и `edit` прогоняет файл через форматтер для конкретного языка — код AI автоматически приводится к стилю вашего проекта. Не нужно вручную запускать `prettier --write`, стиль не мешает в diff. @@ -248,6 +311,10 @@ Cookie Code перехватывает его, выполняет в песоч | `mysql` | SQL-запросы | | `mcpCall`, `mcpListServers`, `mcpGetTools` | MCP-инструменты | | `skillList`, `skillLoad`, `skillExecute` | Пользовательские скиллы | +| `run_agent` | Делегировать задачу субагенту (отдельное окно, изолированный контекст) | +| `create_agent` | Создать нового субагента (файл `cookie/agents/<имя>.md`) | +| `list_agents` | Список доступных субагентов (проект + пользователь) | +| `read_agent` | Полный systemPrompt субагента по имени | | `openBrowserWindow`, `injectJS` | Окно браузера Electron + инъекция JS | Полные TypeScript-декларации — в `tools/cuckoo-tools.d.ts`. diff --git a/assets/photo_2026-10-06_18-24-20.jpg b/assets/photo_2026-10-06_18-24-20.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f30249871af1a6ee8f95dd103b871adaae120c73 GIT binary patch literal 41144 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%0DZ_=;0o`nfteN4r*AB5nTM@Tf1OmR+bS~sD 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 literal 0 HcmV?d00001 diff --git a/assets/photo_2026-10-06_18-24-55.jpg b/assets/photo_2026-10-06_18-24-55.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b6da8674753391430fae13af65037e63e75165cc GIT binary patch literal 43234 zcmb@t1#sO;wl2KQ%p5bu9COUf%AtaBBg@ z5BM%~@m|nP|6maC-`LU$Z`Mxq(A_maqd5~Yq>w$ z3xe6}Xzcp#3jiSdNdRD{@1Xx11Hj%3sEG*xa8(2VAZP#pXwv`ytkz$C{E>hFzVGNi z=>Na!Z50pz0R49Y0|NyE0|$Fg;E>?I6C?}-B*c4$frf#Cf`WlYgoA;BLqtY^M?gkH zK|#aD!X_l72Kl%CyY~N!fwx`&3N&C2qy`KG1ptZy0)_(e)(`mnzP~_0!G0fae{28aQvYmg6SVIXaMdo!mLZJy8g=zO#B{=C2Mu#7>pe0B;x9B@U|{o%53PRVt@= zD&sMm9cMD6R)JYcvKog@YyQ^uxzB5-PxAlT2@mb6RdN?AU1&e-i%#DUQHR;$!gI5C zCP>TX?qTSo&%fl}34=QP|L*(;K)xT?|2v^>n@To)`jG9xVRsk*zuAzNUOrcy5QjY6 zVm`Z1Dh|J@m!4m!Fn3!Q{J&^65Qp3rtnyZGUR~~oHKnEC4v23o6iHq`EeuszP<@Pk zT<`v;MRtiIq3f>$-f$qF*Y1S9W!eW+Lk%Wc>z&+;hGRJ=7kS{ucNb~T)fV3X1)J9^ zoHm`{%}L?XFg2`_y6jGzdjGWbHA(I}64iCafLzKp;#Bz+lI6^ed55T5HvlB2-bFK2 zzI+k@fSar@<=p6x0|3~xl}T7?q-xmzWrMlu1S#Ud^LjI5Y9g!hQTz>%C(wKdbi&vu zT-lrUZL7#F#WH98`Rg;jjZIF6&5jht;a%kvc`dt#{+mJ{12XZo$1!ijkL<8D_<^$ESph}ABBGMx{YmR2CKaKmhFA{mAis| zc-(SkVyX>)8c9K3wwJ|Smnue=?9RH;UKdVJ5xsjb^xdn3B{np5F0-_LPV~I7yF}C1 zWMY(^M#*{aU|uS3zYPv$(~NMWx>=?^;g8SqCiU#r_+{d*`u*o0x5Mhl8cGTq1>h@5{Wqk8>S7M7Oq$j+Tp(;5 zJzUFWgc}TO)tir-6cUHv_eibVn9JvBWiG=rs((p~mGc^R{~-F;-UnU#Qn2N~T&2`> zc_uBUrt5rDYtP9mev8Rtx=%T>hx0)7aP|Y}ptR>YRQF>AgT`|*r6p`tNgWPn-O+5) zfX>k1<*pw3XwgPHcEJ7YdQ`r}c{QQ5Qfn!CaTa*{b!PGK??VNl;V6pzD1O)W$K&wm zWRhgfT;;ykCH2Gis`ptI;)v7n_C$lVRs_0YjNxyU_ZHQX?gD8Bb22Nf@W*7|V^UwE zj%MoQL?@~xBjmEy_dHwo-7oDJg`S?5P5n`puT5 z_cprQv901ub?>kyH9Tsghh@NUS8zF&n~9~~h-T0FuxoSXBKSR(^@(ZK zd~qa+GuSIX|9Q#c*KL>N-xJUuQ|uCi)!h3%Y`YxK4a>)~m7Z#x>a=TPsHmz3ap?GG z+hdgjQBf*dI5ma}w;xx~ErgdgIx3P=wufddtXuwvUk?-=tt$KZ_=;Lv_1*u2^QXf$ z|G#MffYk2*|KACukMZ;o;}AIZt&NaC{N6 zE%VBD=;k1d1{q~r(*MNg-k8uWQ0#$hY(Czf&*DSb-`ByIZQFa@t^SWHs;0Sjfi4AI zb^iF8rX}3nR*|#*5ibUUhl4fiZ0)4ae+RI67QVx$t6*i?m%Kh-Iq+o1J8KpqUZA=i*nJ$>{oAY@kE1!#0Ra4zp)XAD6Z$wF zJY`J*_de-(CUe{)@$I=X6a|Oza=PJhHD2=e@XsMkFaJ#dq5AzWp`6d@WQY0^qO`pc z0E&XW_Fa3uvBP~ooV1Spc+mSjzOo|&KtX;-O%O;(5HN^$SKmV_02~z!1zib(n1qy( z34>WcUO^F=jDcTJh=|1wD6DMY8~Yw$q2B{92rAesoL9+BfbnDf5^!Xe?ic-T%4IRr z7B7Kxz0)!Q3F9!FwGreT&Tu9N{}1&WO#>|`3hoIyL|e{S)1e7af=Q?Rv2Us1Jng-) zri|C}g(F9S@lAIh2x@&;lH;F+P=8)Fh~hmfmEVG+OMN84hNY3J4kc-@g8w=W5|08s z9`d48z75ES^LsQ7acEwNL-eA_Z}T0s*7`O@Ba}8-fOC1xAIxTgp2zVmIou}f0>eE)D*JwM%25jYFfOfxLFp#5Z%1bCxiF!OIQLnnKGpR0MJUOS z?OnlvG1I-5#_l%u&;tHxNmA4{%$V9sNE=Dce7J+6zo35NVd*q#@&W6}N$be)Ne=Tq<~_RF^cF@V#ZXZxCd^$kXoJkywR z?V7G}qvwPoX5ul#52p|lz2;21c2;Z2HG{(-^c|^-g-iR)!PK-MIGrpZ$6hT8{b@$N zSTL3mv3|J>C*o_UeGMxP(>H*E;4`v0_O^7cc$`NmTI^Lq3i2p^e)wE8@}>b6j8y!5 zb{>J7@7J&|B~i|XdvgBa`s9x)*y9p|>*08Yv;k86Ld72=n>K{Qedwpt0(dZM*`>$( zwJ&Y$B6u(QA8K3HUY%YRVm<|J^yi~kWe`KvT5MdY(Qhzqn(3)vg(kr2S|)=?SH=LX za;FN?H?MHQ?DdB{POs78I|1X^2*_0B$4 zrahf02M$iEcC9ceFYHj^F2W;psBX@m&T8Kx5%>`u8aWNwV2Jn2N5z8DiR0*~Tq5|f zzNs~$gz{i8YE^tj?eXjnvO_81`p5Lv{fJOs7)?&2xZ_ho!RU!D8`CPJl5p(`Dm?U? zX@PZ#7jRlQreu_S8lm_V`L+^!MIUJ;eQ9Q_t_iJ(MiM+x7HR<`97A)koAk(MS*(Sg zShS06#;^;>W;iLXE0i04EQ0;)(1)>N`do^56<>V_(6)t2nBj)~3f)ptOSnohY(#RLRhKAJ6M1-b?^^z( zSi(WP@}8el@;KYLqjhMDx%bIH~ndFxZP_-SlnLN8g!C)jr>)7w(>)I z!&LlpzfLE%>{J}(pk;aUh%p38aPp9r{lR8{eWn5VX?z%2M^1XSOi=Im{=#Fz?TmnM z-BA>5WY9QsrahiP>sTm)g976q_gD&9ZS1wSFocb12Z!hRci~0U#gb3_zz8Wsel_L; zoRpCImY%!Wxh;$RFypxub{tXeYt}{Wt)MAd`=9h{0GbF!!or}lO`|bWj)w&5i$E9S zkA2?|Y08=>ak5IGP%46hIz4fWrQ1A2MFP>L#^c~O!Hb4TeY1grkbq`io*3b~EDpkv zlT@yuT6QL!7ZV#i#o4%P)9)glj`9k$k9mG2K(a)|FP0%bB`sh5v9fXkQ@YpLU9Uqo z?Q&b)#^bYwElXP6zSh0CcWvc7-W~Gjb}Am=a42+(ji+kYpt52|JWJtweOH`BS zgGT3-f+wV&h2v9uVFS-gk!46CUxwaZWTpQLxVu?EbYCp5p=o)8mJq2Zu@ zOMcyckz-@rf?!J*g>V>t65%pk#^+VT@%aL@TEk}?YqiEVGT2~2?lb!O%(NMtrjHzi zF{y_YH`|0cm8OWiSmEH>gDxtqS|+uWz{1^IsW(*em1>|xVFLaQFw=oKVMQm%a!nsL zO5%!_#?2xTdC3}JGz&2f{bYKqjYXh{WvgUlC%H4-r5611;Jt0F=~2?!7#dmLE7+=` zlef1$r4huQ3WK^V**{=vGBEp(;(k0<;GA%2#8P;HU29j>%yCqJ@_w4sDn8}%xu6cpa=(AG9{zHsX<}}={ zx8T7)HRoMN&|ljzYA;5m=2(3I{V>wAs^H;ul(&E=R<=@|aOj){|w&*J_;>Kv9p3Q>I#fYa<}N!{H( zBmkyH>Of#MlJ>>$AkS8jYFceS>I zvT70-2vb+7eVBFK911NWX(oqpT|9#c@3$x=qpu8^U9z#n&!PILHR*8M&6$jYu0?M4 zyQ=g}@@uyQ_N3Us3aPfvS?#khRrO#Mes?!sukufhv_D{3jnQeL>)mT_$3P8Ip)-A! z?SrA=bC);w*3-mR{nDBPbihAhCT*}ZDr``*T3(~K8$oVe5}m3^4rFcJ`8ZX7+4!~r z4_)V8=xNlQ?AzQJ)40?>8YoW-8H-=GF$8B8R!bh(%~;+q>Q>N6cb45aQbb#ryx=bQ z1v1n~BDD3p)I6;=us>3&qH#^z_Sz!E?m3Vb3ldrM+3lHkSnc)5VsEH$bD-th^S`Dw zk|t)=QwCG4pOVJu97w8*R)E6$~{GGU<= zv57_Qz}U-HQj!Q~^I(nc(&;&7vf&U)r&!fR0FHQM=cMccRg)HOdL4TZ#P;bN5ez1f zv;OVf$0Jx&d7>S3w+saG~ z6|l+t=F)G=={uyS=(oK$fa^Mqr1Aojv4hgs8N?HO9P*BdM)5S`S+1;GQR|mwqanfo zv7hZz$8P|0(CMg!CJp;1Z;RJ-+u)gA_9a+Aq^SL{WvAq)+ zjinMYEU6XSuI=GH6HUpAG>5kiTYH)TdT|yx_6hCa#@J*fD?%(Wi2M;*c_9BH2gnD2E`2^h2@Si)q>vugj+M+;1pt9d(RNl zt7YhA^F2?4UOM%h8Cg4z@j)F9Cl9=CcR$G$CmDH#3%a$0X!)}Z$orYgMIHKGN4bkq zkLe5_V&ty7@YpW-AuwA`Z8w_kouJPl{w}s+t~3rFIa+b+up3bqYVuJRsy8lvO?mAY z9r&A-Iry6u2Y(6M%DK|Gd-UiCa`kA#OLKh#*v(Zvga&v&EqNYRK%WGfi5bxdauI8yEW=J` z4y(rB`PGxwV&vdQ|2H#gST(>|@c)51n>qdes5$+=#x$2R`?t5hnRkC2`s?||`i#K;Sz(eG0eXhDHg#5?HNQ9FYh0=TRWF~itE_dtENVFH2PcIr;l19RfW=Wu z6AbA9d2?b>_DVF`Ywbr8@>hmZ>!0sA57{S$>my6RUd)3-aGuE|jfo%D1bbVLw}WA+ zbq-afzj=2;pC|d#X4-iUj??tv zE>AK^JK3k>DS6S!Gi(oQ+{NJo2rX34@A#DQvEzsb?C>F;GQ+IC_IBXyMRZf&+1n~# zQ&^6o>;bh+gVDaZBJk1k!D)nOj*D%t3{1WO`uG!YNY9lbNSOMj!AEeASl5Y;vsga^ zTi5_{=YBuHMNDKl%KSm~4myfuB3VpE`mySx!V%}6xjvaW$qme9Vu;o9f0S5YXsVqF zXa#CRH+a#ig7n!womuhOiV^2#;E`Z%7V(;VMc-AT_E;L7Vs)L29hP&DLZ%FG+&t3f4MVTwr3j}srC4c}rGHaYeQOuvK2I$esf z#F27K;6W?pg{+|tS`9G@dmC$KSxgTy9(S0pBBzl*fA>}_5xX1uCUj(WVM9*y6*dK9 z1zI9My=5)}psi%V0lFSbY$0_8^L}(iO{M-uAg9)C&;laQ(j=qLGS@v|6A$KKR~sI` zRv;VFzj_9!g2^LmnP|DE%XSnQH-FS5kd5WUS&?I-l!`|>z#n!38UAzip4xIR50FlT zp`^%Ha>J2N)CUcZrYQ;7Z^|pZbs%ourwLgZ@`*pg_u92eT~3xoPAA4peJD+R zsDO=l{8;~Oft{b4QLFgWjnwyf_)N_>pI>epikTaa=wq|Oy$1=VAAShcGyk5cFEYxE zea1$xDRRkk;W!?CIfXq0R6icEYfj2={KJ!=brIxlsP2+Nuau)`cdO3N<|wFo2X>HB zQ`ff7VqrU(Yl)kOt3CF*Hs5JM>Bj9Y5>DKU&)xt7*Q`Hkr|p)2Vn(k-K<4fc*zJyq z9Gfezk*KnCQtW6ol=afjN=bV~_lQ*O6f6%uVJ@f>n7f4j3?DOJW21)F0-YQ``Bn}) z(MT8YdMMOOvPH}Jk&R@*W9Cdu=Va2XNV7MX$LE=YAI9RMOY1kOFh1r)aZy1LfZR8No6Q?sdxPW$p5_w_05tn6E^gRKbPmYgGptgMZk@cz`yO^D348Z#IA{*s<4 zC3Gi$R)F9JMjP{`n&Tw7$p!>iQ6DsKu^H5B!ll{Q0_E=$Vx|+K6wWfJc`!n#@AGTw zGO^u-%Ym|UPOpl6k2&vgbxDFp)m_Qn3KNTZexq!<+s{hwkL{TCFR7) z0B6(KE$k!~c!_0Z5Y=B2Khsk5&I!@ys?gw(!rW?3?i|6ccV;+k!_pbPLv$qadCR>4 z7+W;as(*PcnPlIIw&XIy9jx0P+=1NBJ^c{BS)zf^D@QL;h3!%QjWGPGRbfZ}f&YSk z(f=3ukMuhg96s+(y);n16hrW@x>yA^N16H$;lH5D|2O#$75$Hfir{-Bq(T0@k`4;` z{;VJ3??{O9I}YmG`^MxT6EVow@b}DITyEL@{cxX;=^2gXM}5R-6=APkosG%XHj-+z zRsl}83l>4X0@hFAW@K3$nJQK=5eJoDO0Q=Z%0QHldn!zyQ6pPA=T2bTLE<6ZxE=#? z6nt!G^1}9DY-+OI6j89N)d_1!z`m9?-}z=Yvc71mlO?+9tn*K&CwR`UvmHJu&oNXb549$so}lWDK$h|Y6LP2VH)@oNEg{?fFy zEcqy8^Qz>Yo%>PBRh)(u$W7N=f!{4?Oas4yJV=!(#P`|Hx9OmFr#{4&%&L$5&B~=$ z8)g|cO2h2Q>1iWZZZs*ODQsXbII@Ys#8N<83OL5{Xyo>0pPLTFd=b&*!m!RP7+2KP1p&(GYFW?o%9YAKvxKe_6bY;oVIoppFqlm zTcihi4AQz1*w#~a#c>kK(NYmXp9wlQA=F*+DUu7peS1c*io{1EJpb6bnW|+C%cSjx z_|A#;&X~Acmyi?)HFwK%M&G=kT~^PrZB3ysGld9Bb_W%HrR=hXuYNkDg>aXPn;3+Q zK__*sVboe0Y~{%EkgTf{1|4z_r4%jxMzfK!N);1!)s89V@`}+k;>#M+OTf}o8J5Jh zx^l2SCvX_nflgdLonJYX z8{&PtcADw=TJB(+Y#(V3BD=|&3DbeeJ0q&1qGK-kD98oYSs;zbVmYG+BD+>yJvDx2 zY6t#|%<$vaHQb`S>s4t$UVM2o9Ba4a(b%9R)7B8LcBV+E2oWv2Q$0lu=U_AZ-0;pk zmP%8TG!V8Zu5BvRgg*H(4QIgvMO=?2p)Um3&64PgL5__wYajZ#yEQ?kBR&ElC@VJ9 zZ>hlQ^GPHf}!ceHNc}ZM^5x82LOpy$E|6Uq%Pp zd$|M0`L|<~E(kZvqYNP5+cDn&TWfRk`>gRJK}S$7bO9GwJB*gIa22m(eGic4mHQt@ zxJqo>)lE|I(dWAl1nA%o7;mHIz;YtSgX&8wE$PnqK*#sWy{YW&ou|EvlCS$68g`e@ zK2f`Zu9 zX)YB24eKdTNkDA~&qy7PPNSI5em-F_K#oaWn#q9LF1@=#&Q7NxU3%3{97}8>(iqdk zYsgjEYA%`$J-G#=9{wN;D0k9~5Zly9U--7mB%K<;OyTL1oaTu)rl9+fXy$<{V|yR@ zO=Yq1YvbSEdpFX|1c!%~IyIE1Y6Kl)@Qe!3qx#0eGNUt8lFa9}gizCL0Z1`ER_OE` zDnW@!nA@i|nDecTss^EfCA*otsJV_@tLoJizABuyXV26v*lYTbas`kZeEMsAlC02R z>o>s9Zg`?X2Xu|-MF3barF#Mvt4#2t+jG`I^}WaIz(`RgQxkk190Va@Od9BC!UFAvx3Ka?z5)!nS>6(YM zi%krC0%)6O6V7GhZF+hk{h#8{y{Qpest=ixHzz7ipHskW%fbT#S6jAsjSPI$D&t;M z38DQPM45045!Gnq?eBc>R*+TMAbD=EfDb)xM}{W|c7t60d1}sYfLOy}p}eIxfYJy; z1R4RxJfx0QeeHF0qu#Tq_u zJ*oT1f1{Y^bb&q6JB4N3L3d?=jv(EI22CJP8%YMUK@-hnZ#4{}?lWyY3=jbw&^GA? zE{uDp^I1YcoBX6wix?eyDt7x;t+@XAZZy2`kZ>uqu#VUbOs>U344METRe!6l313T} zs-=~Tw`UvMUN88p2C zh=ROFN7n>Ot0uZJg31&^74@tXIl9#^Ue^kJNLAZ1l;0=bEk-a z-^tBA{#Ywl@k#Ry5S7>EP_BYb)z}0yr8$u&?6bdQwJlb4SfxH_FdezHcMoXqK;SSu zh32VSm8eRYr_uT4r7za)`xD+b1?0#cx50a`o@Q@R=-C!J+CjKC6|#0@ zD9Sh-6vIyQ(_QWp{PQ)NibXi@X6`Q*KyB+xl!YpS_3;NHY`64&ZE^yghr|v&)}Xfg zc8OaT918;IeWnzPqbW5s#LN4pg|&h_1Vl>)&So+U6GP|S^jQpLUX(!sa9^?f$JZaW=Xsvpx{Wr^xLiR|xr|9Stg zc_)^ACyIR!x9`y~>NnbnRkcmq^DsA}k?xw!=NsCRgiCu%tB4$#(x zBW-JZ#5yWS)G&_yolQI@4j*W1Cu#Yn-${uY?{e?8v9IN!af&y3?bQ9Z-l_KsecC@4 z`oCZ5f&Be0&wBy^{B@zuPi=s669#e22AqrX{->NNTgK`W}nQ_i@LHFtpCR!#4q3GbI3 zN+s+K3wh{OUYm4a+*HVgAAlQZreq=YBx;~gYGh1vkSCBf+*crJe7-NNHWicLVhSRbEHh_b}AfOqTm|Hyw(gUqhp`5>cr^@ z^v@zyNAwqG2}@U9Ifc9_=|^BJ-!6NuxdS>qj*{@IsjCx{T%D?ZuVYnP71V@m$o%+S zIMLvMQ{1$^L^ggUxCU4*X|!>)EaJeF?X(%83D~1I8T*cMD}CcAS~<*;-yuK2j&M|f zxa0@oJLeQ>ZVmo00l(b>^auk_D*f_QvI5F8)#Wg>G?218x+Qvyq!9~esaKYbVI(cS zSVR8?$i4RIBT@S)MWathG#o?xFSAJ*$yN4SYvxkRx*GU1=ifZZVS++L`Q z{E)-$SQ5Qcw37D;oamVwwC?e}Wgp;W#ppYu?e4df^cSvZrOA zNmQw?hYvb{Hzs5!z3vS#EOj9DK~KG2U!{COAcH2@v{0;Y7c*Z##j|FP3gEWfGkjp6 zkA^FGVziB#;et$Sx;^n4-jR;8d~1@dVXrc=?#S^FMqPnroL>viw=G#@7)&^O= z=0c4S2{EkS=*?l7XSqJQ@m<6Y0x{XwHrNC;`$CK?WA6F zukP$z!j}4i`DDzxMAtBCUU+}5mQ}0ff{dv+U8oswZLYT)+-@pTIww^3n)+NK{|UYx zDN%=5eVo~N>d{RwG!k@(xh;~erIM;l_{=+#Bs#zm#v)Fi|2hmUplWQ1ZEiVAR_{X& zImOB7q{$;lBArg%(|E*CkqVw^J)k6n+*nURLd1e(p<-P=RLV9$a(BR?Kub#s<|3}x ztg$LzY99i!h%+ciXRz9mNzY+#(<|z_yBrH%sPuF6Sm_$rckDQwraoF(`9TEV+i}6A znc3j*LeB|%7@(o%1VE!g%DZ7;bM(xe5gIvVba5Otb-eQdC_JS&tRI?9^aAOa@f|yU zcvRj@g@=D(8%v;4ddv8w56fw=Y9l4)^7GLZPWRXirm;LjFP^2bugOpk5kMOkbtFWb zvc4~Jy}wdhjasFlzK#mXGQr=P>3NcB-`<0ol7$!nb6I|Guk<%7gC>%tJaNkgpPX9{Ub$au3OmG~=!2$1`F!!Xbg1QZuzL0zyD zrEST^xBB;t@%GZQ<6S`fSe=<>Ol~w13b6D(NW6XaUHG7Rw2N&SSSO`Qrg_ zY`ic4(9NY3aS!csT=S$o$O97uS%th)=mX0`xKbY*jRo8qcwq*8#RLD|iyfw0;8NGc zcyg~MD28DA=40T8P8+H^Y-1|NIyynO`f9sz9h;nne1M=)8y0ew?y+!7r_xv#%3 zs?`qR((5$X%yv|9Y0gL!18(X#=s*sKL?m$0ePTpT;#;1|?9F=K=~T z4A4bbMWDMSwRJbtT>4-nYE_=0M9~w&x=)S9ab2Bq3c?T^xOl)TfUAwn=qB@2krz}R z&!p^du%{qNF51Vrgg|N`*^QZJPzvtDYc>%ma*I>oarr)4?%1bnaT%d&e0VyUB@35x-=f6Cvc8)TWV)aBd~*I zw{Cz0-gTUEo0SV)M#ZW-S@k^KO(=gvFtE{OW_XpufSh-UTkzF%J2!khN$bp3ttm=uAtAJ+eKDNiCX9a_(Ql29|>2V>8l=4w24-F?uCg3 zH$INy@7aUGLX!DbklK*rGsLu!6stQ*Tm`$O^FPB_9`>z+|C)eUSVMiAGN$79bv(i_9mrT4%ZcRm*%!%F~1f{S{MqI?yJ=CtFRih6TR?!1JyWg zmng~zk9z+;3w>??=2Se__%^urZvNp$-6gVFE>~HFS8%>!2ZGfXb7)Rls>5h6y%c!9 z8Q(&7ok1AR{WpNe=xtCpGJPVFHAXE+6}VQLK^se={)Jk#ddaFo*J&t(mQ6_>yS3P; zZojp&V^=2gxJjkSxOMd&ouwwqS%Qxz?NbtJC4=HB1T#r) z<>eUZO3p3~XY!9t2HENeYc0U5Mf^=(7>q@$Ax;yFLK7{;vL~#u#}0LU#dx2BOkKTL0eyz!z<~@{LwfIo~gay`k=Tr59z6hr)B z5@LqA9>4OQPwd3>0#%3>Zw;hT1E zuvKc;Sfd{1o;xofp@vlm-@JE{=wyVhF(3%1)kr(-Q_0o(K@G5=lFRE^FzTqY2zf4| z=qZN_XQ!c4XVAD@J==_t&C-?Q z)JPt6yWs^Qwm}Pca?$cCGKc2Ybvl4!pLQkorxp%10!{W}PD*J3sDRyeq!5u48YbAV zN@~lf_48jUkQmgP4=SF^L!iREiO(OTc-M_w%Ye0rOTCPk)6!8{x%2MIrz$f@@C6;~ z(!E6v%@*w-+3YYDGo5j8MN**`RZz%_L=M;i;`?!?3MK{3i``DTx{fvqlx(IF9#7ky zEiGEo9HaZ+F*7(Qx14aEDFrKQbo58_w|d~EHju#jCu~TU_5BnHTh!Yn+vY)SK5QR) z>t0hUKu4L?q+#^24wJd~ zcQq503Yi4C;}vv$m>!N{t5F?zU*fh+~v$P8a#U>f@ATud(=BUh0o4rtPt06`e8Ctcv zAd3}3)n&m3;iU|hu5X5%I!;u>W6=Of?9;y%s_o;daa%=tK;ts|Ld?qjl$NEw8gQe7 za7|~p9QCool!XMCp=S^8rdd3W&4pKUkuM`r4JE^TJ1o7);X%dI6(8| z5OnqO1PH6v&_J{jXb3ZE6D^JAxFw8V>bh1CdBJ~{A)ZI4{vN{S{z$2!JS|e5>vapIcJ`0pOU@C11ER`EQ8fn zR&c*wxa+>Z=93&Q&hrMSb$3bTh4X9)Ee>zesj)=V?+AC75} zTV}@IUvN%l^Yx$of3S+&LO|z_S~0S&QAlr^-erVT8wnb&qMEGz7b0NA!nX$Q`Wk7&G0<+|2jv5ax5o&Z;=tO2m*QvTHo(l z-qpWId9o)x{xJU|A7Q~Rd_5MBLyNwThD%xC!?=tWA$Q19Nn1X`+Sr7Z` z78&sVxhW_B932%6QXYaInE?ZnNbk=&7$^t`D99`8P+MTlc>?WzT=}BG#+Q-wtA;&o zO*-?47AQuurcAd7uLgi;-HDYxO-*9SwyExY!;rhkwdp6UzMqmy#TO4viM|v22A?2$ z20|Z;(*~I&$P&sd@?+nd;`i!bS?Ud-%Use%XhBP$IMDIQJWisFbEaG}Yu(`C1Uc!D zwKEc4dA|Bw2y(vqjJ=pCwVDHQf`<$~e9%ep-!|TXy2Dfbh^2DY42t3#K%_A74N$gG z@S;+eMfqSl*vQ6E2X>YAumR(M>M%gpSRIpw-mVK1O!cWk5ZUkwzbv7m&cqYQ9N*qQ zkBZBGS4hStOmqSoRTn@lWkhl>8d4<{JZG=|g&?%3w68dut)L9-_DeLOw4a!IhHJrq zy3fQ{IXRg=mRn>djKd3MblBVs_kK_jkQnmBrmU>2rU?OhsW0DaEZIFC1`16UI&42T ztQkw~@8V!pbbU$2Ta0wq&Wlkc2~0gPCLLt*Ta8Sv8D=uxbQ6-r1o6~|E$}xk=Jx^f>RjgQ@55Qj!5uK0SNeHN*Vz7r!+;JACn)KE~Bxlw(B#5n?|?#mJwd#^KZ z03JYAJUi|Bv z0^qWr3H(@W4Y#p73=4JDHP19&Oy)%A&GYk_R^GIuFJ?)1VwBc#v z37NT!A^K57$sCB5B^z@IA_g)bx@$YG<5i5cS3=?o(8=Tn0ah=0;%JAiG8v-IS@XddZ1gH<8-Bc&8^vZdO7A1R-$ zdp+A5=7p0vW*#6srig+t#hb;DdA0VC*Kr)p9c5{i2OJ%w64Vk1Jbng2W%8UB>qMH2 zTO^vtbx-jlMq|3cTx}-3Y=8)D9AwKB_5W1+33yv(=49_5ooNzC28K7^C`W{^dXj%z zVHG~x7=>C~UQE`I01;CNGMtevDqV@gR9=_d%ZSP=pEE zUxML~UhRQkrT)=vPnzYm3R(M63KwBrYsa0S%t?`iH(Cjs`;tRpO!WRWDxAo}K|}pS zE+pUlHddI&pS|B#ZwSR%H;}}_sn!Cj)&!D*WV0W_hX8%1F(;3Q&Z3lx&_rn_p)+kI zkuZ>J0ukk z=o+oSG%C8z#e;^{Q>mVt{FERFQh0QAqw=;N<(aMCS~DhM_ZrqVAn zm*Q?ixT4(HyDw)BWp$2d^r)U|tyAVlU9X~nzd{ID4<&d=EB)0DW}x!&-T(-t+h79^ zD!=f$Eb^~pjYJRdFqD_loE9^R=0%*n2+4IqJHY2hKtUCkgZQMBM8IpQ>P|m=UU%c< z@czUZg}#_?#ALoGL5lVMdf-rq|L)my$#&$f2SY1KAm$;*G1sXykG@K*s1CCn0e&fx zNYIPn9yP->U~`{A6mT8|Klnp3Zjs<4O&GYLe#?Xd1~Ee@!}h6nOVX24Bw1X|2$0g} zdb|ZUtCez}8dK zvsr0L@u*R?llVQPG4~gIX<5Gh{s`rB;ION6n4?iZEp0oisdFdJXClDSY1u$wG`z5H z9k4a8WC)?1<4$?Woez`$Y}TXmP%r66g9o3%z}ypA+K(LpLT1jaj+q)(LNr#~RorJ9 zW`vieG1fNqaIh<^$M0BNcn)(Cg|;D<+uli}l9%;1h-78f<4m|E4@&L8!AUwYIpo@v zWxr3C`xSJGP_3;(vTLM39P|uupeV%P zwTL`iO6aKDlaNp%9IY+=n4T?;bG6<8!6vrbBX-#&H{}zpliPyr70wFp>o%H()FWgm z;DRBI4gSC`^8(ihe#28#d9e*i2g4*j8&Z@uEz_YG(k;b6eli^Jfl#fV9^$VFkGz?b zYh}GU38$WG>S`~DYeuT54E85vnc6n<{0tws@$kr2m-@Yme&E0scs_uE%$idbAY5b9|$J`2_@-G@u&G?z(R<+S8Km$FjQv7--tPQO zp-GBk6W9PVZUURJrl{lZCnKTPKWN!6hEf8C55IXa*EF*`nw$j|>>=p) zS=gjdVyw}VFVcu|w%$5cB%g%(5@NFyj}&Yy=k4mfzLb*N=nf!7D72fJCpm9H{0!lg zCmI@^lfy~n&5~4AbYUIJc$i-9YgjHuPOzJNrH%QPg1dH1^R6~HoyuHIcCS$5M53Go z4a01>C7s&T$EqggZ?p{#CgdbsIqQ9AhgLo=N==(o#g?{>7!m`x=NI?)bA?<*bf>=h zu!GT0nr)a*voP$)Vieq#>S_#j$PI?8St}^K=EO~&E>_?M9_`s_d|#}o=$%YX7~;ek z5`1$vfwaHWWk$Xry7-%<9hXfs=TYv!xRvua95%avgdF!{)RqRyN0!a~&%b_RSgcA| zK#rToi;Hz|o)xk?Gr*q?Mj4IjvvF+9$L3AXN@|6D<7Mi;5p0Czlj1ubw_1?sfI)uY zwVS3z*Y9&4%Sh6}utjEsmB=`j7@}h7F@L-&^DwHTdW@Bl(9UIwLVT^=evi<$ge^i| zwTga`dnd0HcQa?0UM-wlu`HTkLC3&YnX4(#iMI(0$LQHr57X%H!I8uVp*WKVr)qnn zVNzXP?*@HG@8OdmmORr7lZ+o8UL>tr%^?j_$3!?ezc$4n3@CZ%Fo?JeZsh>+B!rEf zVc3%l<>)}2=GEDrN~!Bvwi$_u{1A^XuCR;FmL0Ljc=olIo*;BsfO9363+&N|5zjqY zU556LH?SWrQjKKlf*z&frGxzt4=x(s*o9-d;`~UiDbUmfb6i2}y;p{wP;W=|OMSiV z`e`tn1VhS6^)R-VwX%6Gy+sQbU?%{zy{z>0%lWiPlI~iPPp#kTj7yYWg9!FS>Jfnz z4P(9H4|8tR`*OJ#TvI~A1}qMDu=tH+ z*1E^+piLso8?@M=C6@?z8(Sxy$+lk-mYAEySb32FMOWxpkz7I#UsTJ_sOw4?hguYn z(%$FypMU)nYzmP1I3fc+f$1_^6c&G!eF^MpBUU4f5r@t4Zs|2BY&i_yqv6KRNOL1< zet?(QIM3JQUlQQjt5RLkz4w^&VUHpR%BuZBnE|!CC<1MwInrjHqFhd>DFyY%avNYlx78{S%z%h1$?C(kvhU&s_oA|zZDi?vf!45BgFJ=82$GxjgX+IykcV~`UfOGMB z(QSU3vvJfyYpOMLZ=x{g3q!I~e|@L6nRJZYPdLH4?FrMyG@JY2Xr zL$CADT!rAc()SfRyvHd;9P*a7U=;T?I~t2nC07ew^Kh#ADgN`H=Lj({#`7oQC%3|+ zf;WXJY6_vtKF51JArYxkS)!w%pX!ed2)dI%ZKdd5{Oz%}NzX6_#cjPW>oCf5?wm&@ z6b#6w($;7zU=hdqgDjh_rtB;IxSmGO6yR&^)jFu?j(M}7iNik*8bO&57yipxRrabazBYq-aU0aV^`7+_fq_Bf z$iXC@RMNY($3Nq4$6VPKw?T^M34BootDMV7N!g1Q05jYJf4cDc4Vb8e zPhaw}R`@9Y7dIL}Rf&Hk5{iy>>`)j!34LM>$aNPIx+^q=S_`Wgc415~A$fa0rS9POtb?`Hfr#?nB_-Agb%1F8yJ} zw~=te7^Z(bXs&$l#;O)lAJh*sFz9s;Z>=6TIb^7OJ=1e&Iu@L5gZ4Hj zc%RSJtZsrAfep*xWyn6#Yt}`l^u5L=YGMWWq!iU1CM*WAk3X=#Dlc)wVPA`isdgOO zxku#yY1i!~ih8$H(=+k^QWNR6>JEsyIeibo3(2l2+>bcgwIBZa;_>|_Mj7-oa>&DK zbCS$4KWm{p1D?08?Zq3737&+)%UzD1!unBz*;||be9#eTgcq~)#0CU(+d0{+K%Op- zt`xsD1froceH?vDsh-RUx)X{74oj?+$;#_3WX`JalXs}{&c}rny#mHJ^%L+XMxYq=2uR4As1Fi?tr!AUOc;-7L;snLpp}8Un;Cml;hItAz3qx6y zi4Rw6W$jIQ%UC2daUz8sHa^qG)DayP+y_RfuRyCyYogZzEJ5#q4uOun?97)8@zGg~ zCsP1nicD%<;g6&^Q>AN=R-@0hBNXT)92Nbh+M1D3E2lxEgeeshES2=jDe?C99@=-N zGg$geasAAoHnw*sD_QIoAp@1&bTg&};Vt%hR-&0wnVc4pvgw!@+;AvesTGNcYRGGJ zW7`*W5~vJ%J#}4pdf~BplkYEg{x8n{7wS<`T)E`&(jZ_It(&jX*h|h%a2^4`f5KH` z=WPb=zJdGRPCX+JE}@cJG4-vZE&XMNf>M+Y?lm-EhETAkZ`0Bw>(242RiraLVIkC`GeEj-PFngKU}YrH zJ*$KgH}sA(!O3set30y|4E;!J9~d!M(i+u!v5Q}R$s$9Es>t-~RwFrF(08D(+0QX{ z`6U3;wvUU*JDdzRXIj0S5!{Wh>%Cn?$5eH;Fy@C*;j6QNCshS2V(~eIm7xrV$x{no zdt+A|W8^m=h6Y~%myEg7$PY1YKgMt+wCB{^(}$I_Ns@ARcnc358W<0faf|Cx9yk$+ zgtg4j635L}%x~)%&M?`cYyK+P01_utM2Rxtbu>aY1G&C#Fn|X0PF89q7c{-~%i?CduFt~;bhiSc~xG2pj%WeHnb=@VKP%6h(g6R@WEV=TtO#M#oRU3}(SwRc=O$Y4| zM6C!B++RB@Fm=l31lx*c>EqPS%?;bdJ8}ZBJAW_H;OO z!FQv>owy|O)>dBSB&@FO0@qFFbt^?7dK;2kjC`5ltpldw&bG~)+fsa zMQwGJ{@LJZaC=tQwcKLD!zoH!vw1%?N>E`Qr&uC`2xXytv1}7Y6h0VUCw>vTjWJ6C zGTnHrr)tL;+@(*)K@emXLmp8C>hiWCxDl-Q9sUYWuyn&hjx%K(uZMN24Q6yXXaYN~ zfh+zNZL^bD%S&CIa0cJLE3t_0Fkq%f;1h0Rv|WEYSIILpeaFoCnD0L4Wn$e8@~g&7ivVWSW_1o7qL*GWdk$H4 zOz1mm+g)R3t7kjs3Nlp-GVr;75dVYoml*ONVuOE(-2%n_&itdV$+!|>wt8Nt#Q$jg zQ}hpwe;?*Q`~Nxizcb5#>oK4OoSsorv~nB9w~@2%>76^@*LN7X{-4Z)e`l_I)Z_O= z|J#UvPV8?M|7*5?WB&Wx5Kh z*4+ary1wyG(Lb4gll-&4-1@(F|F29=L5`mj9b$Xv|BNUCq4fR$tNkPA_75OEWK`hZa|lQjG$chR zAz;QW=AY3IAY?5l$kVAde$NWzZ-A$cmyYtoi=39?q4mPUOD2w6`_#_^PyD}duf1GPa)Y5JhL%*o~8Eqf!Nm+Y1Is&9PNvk;N zbXOOH2xkirT}A>Rx}ztGK1IQhY>7R>A~^2WksGo)-Zk7 zAuoX!9uiJ0ofaZsQ;@}+^%X~KhpgEm>Rk4GlK~T=eJGYWLw~C_ zYReGLK(Lm~Z*3wK9E(eJVq#)EEinlN`#(-kuF3U7i|xJNuN#@Vx9&@O$g)?!8lQe6 zWPR%r6n*#4Umfi(?P)5fQwHJIaZIF>@pi10osg~J)jn(Z`Pb-xcNjn5a=gT(%RhV< z?7kAPbX@5M9TT;zgSPHuG8*`Ujz4CEgQ=$k@6e*exi9=`xQ!G`F}GVCRudM+kb+21 zf+9{4^KaMq-M_+RsC^ zy1m~J#6lXOMSs(=ZC-b6UbO{&7J=nr$ZP_ z8yPlqAi>x*r*Y2I@#CNQG6xM$(%~CMk%tze6Nj|KvL~U24(z{wi9hH*Pw!ckIUf%e z?S8xTMQ=EO8%x+8oFp2JA1AZ1_a%K%J9lKuBE`?HQMR)+{QSI87#)(Rs?HmXM1!?N zmsnW8h8;kWA09|I`t<_7*C2m2A< zj0y*A8%GXP``&;*P;n z`58ufr+J+uC%DLYLdjfh9Vhq*jIu~C;P5F(!dt;#k|Z-^G*ale`r`}?g&(tzl^Q<<&=X;gm;T!4tXsS&f6ps$XjoyNu|%Fa+9j{D-{7qv=`(?b z-rB{Lm04I% zDKtq4DrL9ApWDT4usq2kqS?{$Dt0WPZk+6RK%e5XA^LVBdSvyKwW2ufS^SI?YR5}q6$zW%H33HF9H}PXivXPhhaj?FzPu3{t8uDil;{m~ zQ+ocwiyGVArCzrzU{S{5t}4BgGy`1mt1E&E%eO51vmV$VgyM9lP(4zR;CVI&e9c7pn+`#b~yNEe~vNr#A6o4);G`Ye-~;y4}ADOTZU2I+th_=jmRAhc*0g z68H_c)~4_*O|uR+MNVs0inli8_g}JMTD7Gie>CT)f*r0suRFc(*_9#*iy5HaDcyal zA>s2Q;G>&%El_t)MyWsc*0irli7R^H#4wITj_m15Jc~;54XEdtOT1G}oD#~GMlaG^ ziP_2&t~c6q_{JLIB-gccD#%076_6d7&$y_zb6 z_2r@~e)6OXFZFgzwhx3EX%#=MB~v7nh(}JNkGi2M-0DKTvP6XSP!9f1oG^$hVC}R+ z+ogY!%z`co!#c-CsI6sNA#7l4fW?*^8m#FIll)>$stik4wzBdke0|A1>?!dEEeT4EsmHJzGgW+PhRbb4+P?WT}7Ib$jOSBpscfC@s)5(KW zm)m6rA`_#AM+}~(wU|lxGK@H&7a1v0t?eqnx6wxMY+=V@*k6{LnuaqSD}DpG`8Ij!E?;B4|Lt`;vonYgsvBKgCbMj=Je=SmTXc^gn0LSAq*(bcobx|og* z+Rtfb#*YBF;(TEssX?_Dbel=T%La}1LZ$A_)h958&<>g^pME%cc5^*=`cGy)7GWM7 zsp~hoB*b&!HfO{wYw2s58<90o3ELW_$elrUT3v$IwZ`mBL8h|xmkd>`$sXHx1q7k#G?TUJ29k`q04exvM%IEY}$V$YR91 z{kH~i(cBx&Chey8SJgLrUaz5CI?QDt8|s_xx20%OMe=RJWHeVE78f7K8qV5uo$P9X$27=)B^}+!m6c3m zUY3DNUkp_>136z=+_&mX*Ws=4+=*zY2czZq{xdw4S5}#;aJ$_ZC*Iz_x_bfapC9YvCnPce&oUaR}vePO}VgkhT`lCD5ANag@MX=J-Bk&eM; zFWU4Me#Y2TeUWAsFt*yCfno@shPo|XM9_-x!1KWVjcc@EMl1^*CKpXNJz+Ji>`)JDf#k4 z(vjw>gc*+N1do7@)FSj!s5!QDYa0z?NvmowSk%dnbZ@zm)|sgV-pdjLT+qXKx-8vZ zZm{yNQEql`xsN6O*M`3VyKLhKOjTK|&;87K4Lig-@TMNFjEx=#WRgR`O^GbM8S5;6jaf1hLB?0{{5Mrf*vPKOKy-TNEpc|1|FNX z5Hh4&#<}8J2skDz1>+g<)zx+`{dk59t8{GF`olA9vaBqj=Xf-lq{k$z-y}eJs=~Nc z78m2pPaFk)(hVRe&B$K5O#&Yss3xmcvq(eh;W<0l=~+~;mT3fbBgX1j86IHN4!AD? z1SvFqGnxS-n(7|j=<=>ACM%filC1%4W>`M44YmY_Oiz;af;@D(sRBA|3@w`UIVTH@K zr-tPeQq$~-H3lRWGDYwb$p@C#Xp=jvXps#2_na(rWe2EV!e{6d+TYlBb#%?~x1G;= zpz(Lf#FhN_0nQ(LlMyMYn6$oI>P$w%1bD2~p^6K0u!$V$qtOgdgVbbZZrx_!pjb6A zJ+2Hx#<9$4_PBL?He2daYc(!llV3WA!OudqNvL`x9j5X!Ykr^vp)hS{drqi88q%*~waoKZ4sI_}CtUfL6*rO1-v5A+m_iX~F!ZEiH%&g+_J3J0N- zREY>gWK@|`%=8B$wAG1Pf*;8A*pK7iqyZKBr)y;jl!e{S1%PfQrembG^}) ztc}M_FxE`vIO|ZAJUPj%Aa`w5_6TfMl{F>f)Ljp71035f-jrttsSh%Ij^8_pE1KWl z7nUk$&q)*^S;Iw=p~i{vz=@+_(61DjgL`ML{%JoIp8=zDs95bsY;W{1BWjnnZmXGv zNIR2nSlE}{_F+=;F&6XCmUbVB+=cP*dLq!G=$c?c8mihA(!pLnv+Zov=Ew2N)4D|B z;w5OhbW#GodEA~^aab+IYmBZqQ95P;b9-MYJr||>{Puuw%5Kw_V(F*7mkP1{x9*}@ zVfyr=K_3g|^+P417$qCzjK$}4Gs;7+2o^IPGq-quR`}u@!8HlvD3UbXIot%Rn8{Qz zN-AqGzlvR@A3lk+I({l%F-wJ#C81ZJMw`UAD7ZoDdE-DvW{MJ@$<&+QN`XiT-1dzL z`l~ltb7cFg6xJ)tp&w&$0zbcW$e-Vrr|qPOx4GK!o-2_>p^kzf>8MzPtUmb(^)m6R zcoXh(E3D9%x+*~v?`g&#Zs@c~yQu2r+K42jGP%_4C>U1AqdKeMks#nRM}b>wWPPlN8=kn`zR|KJ7 zav1VC42@J{q!{vGb31bWT+SMVtyyogi}@+9P4Y6F_`33D2!6kzFR_#D!ZaG~br*|pU{1timx?VTE$;DZch>>bZvZFaOSwpRqrM;2rc(~cC3M+O!_yDfWhwqD*8DYA@1 zrhG=Pz~TqUx%TYKrK&)jEu3fl@V8A?p7IA;C`ZSPAkQciYLZB- z#)FlqR~E=LU#Hk!_V*G+a}%ROO@B0h2bL7hdW>I7w`k?g@QW`Kn%1Ynk{c|N@FjMf zQ4cCf7|ZcrO})i4*hL%%k3V2C+{~)s;3NU3OlMFb=4209A41Qcw~l@T*lY$a@+Wce zkA}h~r9T9F3kOF+lkWo88ng$(Yr>D={a6bPkE!{;A>dl!@G^u^*Y7IbQHpy@@5+|I zl80~lQg_v0UCwb?tL zoNAP-TjsKTSy{GE<9MGV)7^Em(DN%o7j%VM*3$k-E9U*BL`tW{aHc4nI(`Y&=u|O* zc#1A{(wFn1eD7!SN#Plty~(~5{SCf&F|vt$4((*Hr2ffMh{miJ{)1p|J(mQRX;z`| zh$*OEjW5*97`+!i;Y|8QY839Y1ad^Tt6O^hg!31cxp&%Wk0tZ8q9uySkp^2lq=`fB_$P?jmE372Bvz_WN}JBMNtK5jx%viL+ZqB>9OQqK=hxbRl+Ce#k6J~ ze@tM$#(kU?dwub<9}WlY(Zo`rzd1E|-~XKAt`|2MYx0n(=1lZ7+BDTnpqDogPnOtz z(5Oi-T>KFWnN4dklkU3`%Hwd7iLj&@4q1U4 zXyOf}@~Poq?j*g0T|=XulDz241nhwzsgBrCjkWEL=A+$smcjdc6~%7= zSZB|Skx4e?2 zwpS&$sZlnqM{D(Esb^PDvoPVdk3g})T9>jP>sh_A|ql)vLb?;9JYBq_2B(GX)IULme?53PqI}fX-`Hq(v*zT2i$;W^bx2eQS4Y5 zS4saddOKx^hnBsmz1(nvSz=C81!tYlbNo5Iu#S#1uv0l?O4F>X)|b(z$Cu7D0*e~B z^j%eH^-X4Fb3d&e?3Ad6z%&QA*b4ro+Y*F@~>7RQzYHcnV@R;4iis})9C63@i7|Zqq?F*S33h5Abdc3v7!rhJKml` zHi%t&B}#%pab%}G(BgX37djv^|B$|YTKdqa7sR+Epc*eg!UAI&1Wg=mHIYi3?3NYq z37QZJeu`D(t4+2&vr*=HG2~1Z0|Z827ODVUC~mXq(ZZP-Ay3;-hmYrOnB2nWY#_kP z!)>X`h|0c%32w~33E5?{weB>i`us0|YRuj>=;~1;7ouThR)_wf&V#xmOsnjuUS~sg zGv;kI)aL55owNQl0{{0(H(QW3!LXcUF#4PDTShw_U6o+gkd1cwd-F|aG3>D#!)-s= zp4%dVu>lUvF%({@NndN4)uyP`hBradH|(y()nn0w++~ezF!%kqL48-k@XixKWY0N2 zlH2l5wF4SKbkE5DDm_I_6I37)CVcpO5L~3`3~(XJz}Ovtm&%}hIUp8e)3e+$GItgjT$Qg z!okJ0{)6>TsqNbTVfp>Ll)%nEI$bUPLzknc>K~n+tp7(3BT)4}_53y3KRf+3(mxOX zL&rbO`%C3thsc{KH{~N2{~GQuvwMJ{5*j36ol78UClJ099Pr2HD*&)a<{#jlNJc>5 zPRBqYhXf#YXF>h-pAkl2VJ<THRi2Vjy~H7bvQyC+Cgd>XpYf$3tK zd_lkM5dOn(z8@Om#L`6EPvZu(N5{nn%>W5=?C3mQKr zJ=#%FLuz5Km+g0RWp-m4kbVTm6Pkc>A!OB=hYzn;`tiPKq`_=kbtHgk77kSl2o8VF z4@cKU2M8u+7!!A-uarjL{u>pc<@T7 z;jdB7CPrXkBdpHY3Zo?E`BpkKt)rIgS2v4ymmocc+6^nm*S*clg>Hwv01mdZynka7 z!j@w7YfR5i!$McKAT&6izfy|2+L!h#oZN#dU``L3eDU?*6%Q!{I=lcFdF-}zZU=K8`Vy3ELJcx^ww?&y4P?Rg z9Es2S@)T)Cnc6`peWhSx{QWnlStDz_=#k!ou<`qzow4qTW9TaL|#D z?&JkVTovbWL4TnFPm60QQ<^gEQWs)ThPe0|^YBWa@I#<+Vcan&AY5}0e#ty#`ytXa z6$U)<36~z*NQ{d_!kE|2IAya+3C2Y0Cb=@vnJmc7>S8`Sz{(}=6H>Z7p!@p+R2v!$ zn_6Dn##jFa4IscKjacuMN%j0^6ejiZoeC%`(umUm7MHDU0BBo|!_)+rBu_ z`dR5>bw>EPKY9PWGdAz|J-!N$Rf=o45eOR(Bg3LN2z!OOsVE5i!fAqhMl@;*lkJt|*W?S^PJ@}4|?MzvY^LZ+Wn ziJK-h&E30Ru6Ss2v4k3l&qr#m2doPdiBADB9r&>azSND-7e2Cm>Z7dP^_L+1N>ij5 zd`(sdsw#St<&S=)DC(%gBynWzvj&wR{Yk%?eXJn+Z8p*Z&!4&VRkqM$WHhOb)bk%I z26%LpX|Gt%GsL)Y6+R!wo58TA=b0ikd$->BD30)<1h0^o=yIN|Y>RRQ1}<=AHjZis z>B2a7s>WNueSAa1mDv6c`o^ZHE7zz;V_ZA~$$8}-&K3GkgW2E`jvPhr-6yDe7bJnt z9S7q}18K0kUXX$7T;hf{7@R||h8XvtANBnwJ~NHa>R?-jbN8@W@yHYuem?~Egs2Ta zvEjr%ez0s!5|?YS8aj|D+s9IZ@6X0?c~kz>-OA~H#+jUWt7LBnTw43t@^&h%Q*Btk$l zV&YNqv@F?K>SuKF@qEaUV)BX(TL)pUHknW?u`y^a7wp?7`2zaK`l~NrEAt$Lk5P$- z6(W;GJUhZqE)URMPs?4d;A76}<7?8obiUxhfo6MNlPN98aF=f zu8xJs=gAEi0DSeMZi_y;83j+_-OO~I$pbud!wk&~#cKc%JU&5c$6^~@!Py8BmcIO~ zoWLDhg|!jEYP;pzKGfhy69O(U`X|zeQ*bTng%zBB4kOal%)~_bAjF?m5Lw%9<}x$L zp1`YM>xwZ<0MK3elXGOHX5AxfNzkNJMw)?X13FtqM{5pAvo)<MKy7y-$#b zg(*E;nA&R3y|weKv5peE3Cps5<=~{V0-QMWRll|wA%vX)d+tgor;<(V2)Q5$U8yYS z{n6p<@CS##0mgJ+ZadoGgGV?MW`!zZ3>>y&Ca^tYz`jmQb3bFalRm<(qmL#F0g{pE z`k)5Z#2ppOl-6%H7#xdj(HX?)Nztw80PWp%lryHHEDE5A!DxNh-@n>Wk7?Gllvjh) zg%S1FgE5A-^bSmS5h-!A`UC)?gBd(&AxAWL8#y> z`+{_dzvhKCH%LsqWS{Ir1H1nk``+n`(HT*?>()Qq zsASBoiXzgsVjIY_8uYYhr~-T1Eu{Gg z;;NS*r;nmnsoe@T9i88G3_>CL3jivTRZ#B~M-Iu~uM1g;+*otk_kx^?D&)ohd^_qO z8??iHOQtV0j4M-~-VjqP2Jhvy#0Lzgw+SX{A7AxN1$U!X6Y0wT)hgo+`W_j_9-rvs zOH$T!&frJ4`-Z2NV~o9KKDWg?{_}-8m$sM^O6}R(@*tcZ*H3-$K0T=uYnzq5ylVl? zda$<|Z18OGZ!hpj&{TyUd1<*eGeXH_atIVVlm@20xo&8?6T8SqC(3T^Vq$|h?)(cD|9*+ze!bu3^$czK4 z8Zrk$XcuQ~?6Vdqmj|wM*0_lg+h6MIw zS~K6Rc35f6D7g}xDgp1S6VEkl^@f$Xiz+S!%{$epWNxfE=6o+E%$5C>M~}OpjnR?y z=YDSbS3?Omr6&)D5J3;l=1^Te@7cpu*ABFF;;a_fs;ym0BMB2zuAk7kRt^##z^wfW zG&PAJO#bh`f=RO#$=@Tyy~eW(fp1jRn{LeYjN~Kx13t^g1kh#Ot*xdYQ7#12HcS|# z3XqW^qK7Q^4Y5KiTkpr=nN)OT;l?BuAb`W93r0ilSdWosx=;tO)v}$LZpI@>4=_F* zlUd);fT!CrQ>^4)YRnm@`BUH!Dc`LZ#<;<(gC9#TQkAyp&i4v;o_d{YQU}LHRVumT zKE6LzUbx+FQ=KfadOMRbG$bDkhcidvUGDcRbnxn0S zJuNCMMZdDbzorchfUIVo#tWlmo_14}9$nri#u6H2+gNS* zE4_M#>V*d_2T-b10@zm$OWoN!|@Kc)dbXz(!X!`q@F6*p1C#VQieU47X- zSWD&@du_{#09v2CZZ(2dCAPWuIU$wmgy6k42(0J zssMNCQbGz%6pzG%ztVLn{(Q~7wLY!M^I0VJsl}TY4Ovb`4TuN2uvc{R>jzOAs|bxI z5Sx$2SdU)?Rs46$ZOh&%TAd`7KNULj`i#35D(2kga^D9-2L+Fvv4))Wyuj)R+?-dU zTox)4I-_Alq)qb(#eOs7rKYt1^l*$i?GZ2~vn0Vrzb=efiLT%Nzf(_e{zv@g$0n=N zD-d&qsw2b|T3SM7g{^Fulv59Rn&rv+0*%hb=@ zLy17Ynt(aj{*YbCZmRTVInkndNiQ3;SgS(9Po9jNFY-ax=%)fcfJig62Hv7 z&_=Zcu;F~!mdf2k){F$ z37)#eaE+f%E6pb>9Xt^rZfTJRA98jLGM?h|{vj+H;T;K&N|1Z$DveK&GkAb6^4>Wj z-uanwha7~g7gQ^ijVY&!O&0?h;m?@9_soM0gvV@C|wx9ucKT) z=q+vE=?5){Pe0xZ;Q#^w-SoSZ|Ja9z!>_hvXUr%;JnC(b!bcpZ!%mDC-VJyE5+*y0Z(s8CD?!Hc zkE>H$1F=2MwtFfMrd6LPaB^FccaJelxT+l`1O4mErIAI!|1DSyqqi1 z`+GbRE+oS`ue$XS5YKEbK5v8fFUCduW_=Pf1aI30G*gMeYeW66m|t0t^n1y)CIJ-G zfZlir%7R`z3&m^<7{weKg08IwbRqy?kg8U*t)oOEn?yuN?TP_iLD@8Z^Kv~0%*9v! zd(MwxK_z|#jZ(5_Si#29(gbqhifvknK2CdQQ8Cc{as!VPJUo7eqO!a#KDcixy&{nq z4+`#7+lw&xe(!u-oUFuC4{iofv5&-GZ*S2`B$7u0&>I_`yW*lFp^@^DbHJeHCrlZM zg>qw#?j}G-XFl|V<=LbO;-?nA|+A2wJTZ_25!fJZRNj*(TB|%+-c&E4gD_^b_f2| zL>m0wbTKbf@G1bdG(n)d`Ts}n#ysxS z^N(XYI&mtstkyhzy5pzAMK)PfxZ`LquxtuW7LJRA{2g3SQ=5O{>KmFqJ$elp(JaPFtWEA{ zYU@$0;%Nml69TzsPuz=k!q;u`#lv0qR1hJU(bXkWAbGmkjjo-cLDhY5_1J|Xz1gh1p~Sf(MudRM@Cv*>_eaauRmdT)Yn8rr~aGFE|?JYvl9wh zQUGobd1t+7qItjw5bdhpd>N!Y3a0v=A_oc$H9K3Tu;N zIPnT?a^-}ayl_hIh5xDg?hWDF&upwm|8idabevBAwpc3_Xp&klLf!jaPH;Pw&-=!n z+?`V2s2zhzE=ZB=8qzS7#mLe4gY#W9SJ9lu`%XIPL4xCzzQ9=bIer9bH5m%~fMl3c z7egh_G(nAE$K{YA%n3W*oj7JAv4ij?(m_0IB(qJ^!yFI&tWOG}c1|a)@9Oh)fSW<7 zaX5>^j=}Rl=75m__vHUSrF{ofQ{U2W5=!X3DJ}FWML`IJj&!LaqI5w7y;2oI4K?&G zMd`gr6$O-z^e!l(0-;Njgq8pSUetTnd+)CQ_x-;z^IKvVHB?8Z zi2%EHz+)?>Lo#7Dx|7-OH2{X5prpMAU)~|;MNg6+?+o;+`@f5k``J43A|`WNR1adn z>nZm!tiMBNT=*WZLT!C==vG2lNjmj)zDV?GT>_mf3-;ZRm8C9In(-0MbDiie-TTjN zIo25%Qu_CG=b2*w4dC?WSP$>c=PSex$-I1S z^5zt;bE)SPO=b*D=@)H>4jjBk+_PDVt!#0APp-lznhCCa!B+Dk4GK4q(DY1T_g%C* zm35s=V$)%GnxHUO@o@Y!O9VnCkBLw)d>99W%4>RezT_7Ggplt1iY^0F#1$GMe1<2gpw5-qp7mao8>V#pXO`N2&zE*s&eJ4v z{fES_Hgw~{eZksvxijN_+Lg}9wXJ5^)|1}C;MDI=ywwg0g}t=B+Kh!Y+k?uxG2bL^ z(VIrCx(Ed%B<}O2os57UJ_PAo)v-UR)HLsIh}9pkAGr42Vn7n?GWIt1WhFn>qwE&_ zT7z$ek8mU)A8=@6SX_=HL$Zn2ulB_UAe}n-WS*Sc#m75Tp zC*q<$YFA{Q*S1bc^^Wpprf-8wj;df^j8NLf3nGc0_t% z^TaWXRctkoOGH9Llp2hBLjVZCqD-SJ~Jz9ugMYb#Y-{c1StW+?gEy@d{(>1ss5 zlbP-b5^aIO${!&)ZYMq#f?i>Nh;LL)Uk)n`Ft1^j1K6U55;(fy=VSX)iy{s3_RuM#b;F zSL(Sw8PzWidCBvUBl!b8<(q$S$x8rr+IV6(fiPB)O-v#cXPCk8<8FO*8^hES)hkGW z19K%8CTND{lRmN~5}w?ssaXrx>Q4Bef0tC)UU=(g?Yv%zP31c_{9Rmpwss_X4~2X< zIg|9^{^iHZh7jGgq4#UW4)kFB`p=?e%Xhf3*K@7lt`yYOF1u^xPh4d5*Jut{W79aw zehQq{8J@u&aI2Qr(H7E1BP~_I^?p@P`@<7%Q`3rzyEwrKerUq_zvBEY6ym5^X8RfY zQ>9Cgeb%&Kdc}SOZp%L9O+GC8RpgUd9v(uIWyJss57eVA1%_s*s-7n(NFFgw%|m;% zD&{T#0qi^H5yJ`;tx@mvN;J_nH;wBlifwXc5_3eLSc#6}dk%32s%0)pSHG1NSm2+p zqEJr@O+wU3UU|XX{nyM%P(A=%a6wv#vo?Ane@8R;XXv|Y5`nGc2NbV#K| zxiS-UNF+RjZVgfar1|Ge%dOlKlk_w?O;qvB8+v(Nb7v8u?!F{`&b=Sg+~1n<#P|M) zvH>kB*~l?^Op6QvG8o)pEcgUieYrIvu$fvL3cSt1 zI@wRDGNnqmQve`&!~&;UcF&$XaQzaniJ_HNPR$Bx zn(Gc3(IeRoa*mV`Na4CyAS`iH}OBC$r*Ztl-e4E_A$zxgnb}3 z*Q4?U#Cqy=DjnQh=;+g6duFozEcuP$2}}2$9HcJMrlCvH0ksw2&|1r}{GcWS%rnq= z%2+199Qw`zA9>=Hni>ILAUg;-n!u};X84J)4lF_#9`HW&L))_c^^f*nNErpb5iE~( zmq%AUpdXgJb846gS_tzll}1J6zKm6zx$yx|2&#wXoc_RBOA}`rBb86CWcTE|O#`zN zhK`Wh2IN+#u_1C7qY=;+Qc?plUmD$P-zW1@hlcCFqDb>SkcX%OdWq<&A#&B7xyU$6 z`!GNO+bisFb))v>5-@Mr?0^1Q_I#cHvi>}ms-YI7%d4I6hT_45AtWs6ji)1P1i*+= z73=A-BK$}lOO#L*nm^9cR=`t`$&7+p2(%qRf8e}27pGBvxz@A?t7;F@%||OHt2z}< zMB-hm+JUMS#3Bvxy zo)!P;1t5c5%hGn=J~Noc?R@1+>h8JqF?X8{?bL3viO~L52OpMz=2}*ZU-H&)ebfa& zLbE*g0rhK}u;=lI`a6-8oqjr4a{2%DqPVN5l>|KtgDU}?;w7=0R8?Q)k zW(8^Q{u_edNpPR<4xIf*SP5}FyZ}7WGcA!k_E`x+SHAuh{IyYxo<7CrOgNb6u;VwL z$DRbfxw1|O%QIX&XFR`x8=23M-ozBmEPx~8E-BCG=ByxWnVSIVNpR%1pAN3FZ&N6l;ltPYeMrUmV{vc!FVLo zg-96_Oh0J%g06pUR#%}T?D<-B;9+B;@*szLe%vV`Rg=K8x7OnJGx~g(EeF;r_%~xv zY>;kg*}P!6lu)vHclUTdJ73h&rKkLXlzvL~%Q>*UHSGrl(o6xgPR4eZVM{x?c@3y~L{LD|o39VMsL8--pSWH+Oc)!>#QW!GK-U z)VWr-$}aP6q6=g*{-H*>n!Av3y%*QZdbrZ#}}>8m=Bom+Lj`SpYt zwTMCoysuROa1vtswiof`3J%Iv6pDigw2k}Y9J7-BiePjJb7JDP>03H+YNBMco`!GT zjq^@w$KxCr4rrJ)J`KW6Qj~;|{c$JOL7p87z!}Rj2s|^wu&gU4{6KVSh4k6Unv7P5 zjWk%X5AI#dSPap}5^Yf(RO`1!1?*N6Z^8}3mRB?9D9zWI$1}=u*vNBJ^m1!8P6638 zGC005*W14QOu`-&Feib?A+`NhTDQyNl=r8BOBl;n>Hb+HDk0lj1)HQ)k3Naw{t2>ck&6YlwOe**0p;r3X9HkJn9 z3J*-#dnidTt)n^#>$VwJ#2#Tc9ILsrQ~~rWf^XvqTD4FJ{o=n*c_>O*Lr%*c%gwjX*kFoZ~f3z-6;mZXwx5BDmF zGn?q0UyI|4p4oCmlg=g*Gf~===eH^=jD|$AfyQ8q0nNQva{_LGoZon+aM(rHz{Zd? zMklI-_YE{?4t^C{xBw=78Q-&4JPPJ%CEDR&8qdk*!*U-A%>yLx5H>f6_QZOT-bsXe zg&|)=xh}BFVxlD!ObXvwAQXl4VmTbLo%fm9V?}obta!yh6!j+dY?k8E!>z>LKD9>m zcjIQ&0Avz(Wc>R+w(|+p&Zh0~(O#QeXp{ONPP{)BRyG#YKcfM08rwhRm;UdHHB-7ZQW4# z@;IJ9uIs*u(b{@giyrYYE#n5kd9weHBI0Q~9 zL!(>yWD7TcSiQR1)z!jbn}g`o39fIJ9AK-LDf|jkn9~~Ndala=hGqS*xtr&PqT`|% z)dPwJc^KEf4tvop2oP4cVMb$#jaPU8r0IFehxdmU*JC8g4ClAFcmeLRLv_;Aw>yg= zff9M}h_5Q?wDv_+Zz3g<)It}Oz02fdI7A!NS#n*P=W_QfO3JzA3CX9cw3MQb4JuD> zMe!MP!?seAhArc61fF_q?hYz`C!ifphj@i&AvwP1EDYTlJd9ADjVxvR-2zix|woXv&L|i z^~yhbGZ%Oz1`9o%wgktHs^KiT$+7N^hSY{t*rm^fyXdH^3K=WTM6n) zQCtAAi+9Q87SN%(d*cVoD7hf8dJ5Sd5fYl(M(oZBfkLN^<4qG+v}uG7 z3wPK`w!LBt*uDb^ensc8b2pc|=)Efc#CB`hS$)aL(X{5-sWdn<*0W|0G@ykqQyh7D zd0*#8ENJ}l`}h(QT4i}(sr^PjJ?8z9HT;#5>IQBaw~$ivEyGi7>r=DsvkrS2h1X=} zJMZD5Z_vSPO_H3PwyT?1!4sJKM{}*u?*R8-1+Wlxefnu7piYSAk+`x&nfj&G^sInlSuKQsx&i^NQ`Ft7!!)n(-EF~JNR>QrwNBmZHC-=rj`Qt=8F*WfF5#BG0= z`gki+I$-Vqw0}xi@e{w(4=CqJU)uOyheyClR&}>mZ?L}Woj^ty{m}4&f0YV_QH*#J zth5d2*4Ioj;fJro?>1xv7} zZZH+$i4EyadUJ6lrj1Im6mzsBay3!=29qs|>+fr#Va8ZQ-div9AB^_4XJc!JBx0J! z?w2LA&eMq!Yej8gYv()s&i=N6*XMec5i|IBtxD>zEjF5Uzal?EaCb%c>#>B2QTW1( z^;Jon^h6d_E1YFh?{Rx4O5AuLPT>F`2?6DD!N=_<@Eu;Ly)(D#=k1aFN{FC&Z(|E- zUK&=;guqGTV}l3AzH5eQb|iIY%*$g|P4AvRY%wu#0B<7mIH(`18ZhvZW(*qco?if_ z44Co}bOD6@;jRz~Bm|yp8#r7Rx(k3?bNhPH%wkMm>#NMlfnFpR8p)-W7@J+SV8q1l zfmgJ%YTO-@`qA~N=GoxTYc)GmG_56ag?}JLk^#BIgDN+M%k|VuvF_k^Tz992R4IVXE za0&%edg>{l)Zl6$O)@QOiq~_ZLh&MpFTp$hOIUR`w@}W~HaZ&H#H3?9Zybm7O zNS&LkqW`(zyLNX`5Fz(`Az>W^h)Lu+6=&sAAV^empo4;`$V9B{lf#pm`!EIWFX*04 zR7WHa_q3UZd;(kfF+;ahFMDgd4EC7mv|FR*26v9JcUZq;z(yWP+%m+z{_PSWZ#4W^nF99YXv{htF94R;oSB&9Q)JYZ)6Z0u zV7Y!mT%w5`dG!`Z-73rvG{+HOzm&si9ym6G%)+)!WYXm;uHw+6lbN&q)%rWxKZTE1 zo^X^Q2G3txZ72`H@iZ+~6q1%D0UFJeAQ!o9EF-dY^-4JnbJ569=ar&f+}i-yfLJDR zImO4s8<7t27@42S^7}*1)R&VAz?nJN!-~J7R3^F&IGvHoV(%-vSXt1E!`qpeYLml`Tjhfbpw7D8Dw5?4-Fd;?1sDUhCp}WvWodD7cz&-kdyy!k5BAaTv zGx4>zJal!wpUM&p_~JS#a(r<->o1I=Z{=N5ZkcD7KqPOQf(59OH8MX0+8^Tv*TUZ6 zXjQ?>e8K{1GZf3m3h51%9m9VWsQz!VB7RID_$X5_{Q|IyiN4huYPRD4oMt{_`(aDu9W6`j}^Li&twJ zl)Osf)o{7zDw;+p?r4gbS~!rXzg4(kA(s`)%GwopVn#cooT4k9Uzw4>7|=i({2{#yKtk=VC`I zg^!w6npPatI8@Gx&Vzv+1dha<1qhJ@y3cC?yn~ZOo}IE??1$%n$-nq1p|q6TzdHZ7 zwN_uxb$<(6rooeLsBE~MH~Fx6EL{K+@q)j342@TY&eaBcb&&t!FbH^ZA$%cxZr6w) z$=E;RoIf~MM8EhRT+t!&djRH)6L<3$t8o7w`MpfV?9aLH6K(%F%b))9ShIdb^vi$a atf%@r@^m0SnqM!!;s4D!h%xwL_WuC!viGn6 literal 0 HcmV?d00001 diff --git a/cookie/agents/changelog-writer.md b/cookie/agents/changelog-writer.md index 7417398..06fd33c 100644 --- a/cookie/agents/changelog-writer.md +++ b/cookie/agents/changelog-writer.md @@ -1,8 +1,8 @@ --- name: changelog-writer -description: Ведёт CHANGELOG.md по правилам проекта. Добавляет записи о новых версиях в правильном формате, который парсит What's New и CI. Использовать когда нужно задокументировать изменения перед релизом. +description: Ведёт CHANGELOG.md по правилам проекта. Добавляет записи о новых версиях. Использовать когда нужно задокументировать изменения перед релизом. tools: read, readLines, edit, write, glob, grep, bash -maxTurns: 20 +maxTurns: 25 --- Ты — релиз-инженер, отвечающий за `CHANGELOG.md` и `CHANGELOG_GUIDE.md`. @@ -12,41 +12,93 @@ maxTurns: 20 ## Первым делом **Прочитай `CHANGELOG_GUIDE.md`** — там описан точный формат, который понимает парсер. -Правила в гайде — истина в последней инстанции. Если гайд и твой опыт -расходятся — следуй гайду. +Правила в гайде — истина в последней инстанции. -Затем прочитай текущий `CHANGELOG.md` (последние 2-3 версии), чтобы: +Затем прочитай текущий `CHANGELOG.md` (первые 40 строк), чтобы: - понять стиль проекта (русский, эмодзи, жирные заголовки); -- узнать последнюю версию — новую ставим ПОСЛЕ неё; -- не дублировать уже описанные пункты. +- узнать последнюю версию в файле (она скоро будет заменена); +- посмотреть, как выглядели прошлые записи — но **в новом файле их не будет**. -## Алгоритм +## Ключевое правило: версия = последний git-тег + 1 в patch -### 1. Разведка изменений +**В этом проекте после каждого push автоматически бампается PATCH-версия на +1.** +Major и minor задаются вручную только при крупных релизах. Твоя задача — +просто увеличить последнее число. -Узнай, что изменилось с последнего релиза: +Примеры: + +- последний тег `v5.0.1` → новая версия `5.0.2` +- последний тег `v5.0.9` → новая версия `5.0.10` +- последний тег `v5.1.3` → новая версия `5.1.4` + +### Как узнать базу ```cuckoo -const last = await read("CHANGELOG.md", { limit: 5 }); -log(last); -const status = await bash("git log --oneline --since=\"1 week ago\""); -log(status); -const diff = await bash("git diff --stat HEAD~20..HEAD"); -log(diff); +// 1. Последний git-тег — единственный источник истины о версии +const latestTag = await bash("git describe --tags --abbrev=0"); +log("latest tag: " + latestTag); + +// 2. Все теги (для проверки, если latestTag странный) +const tags = await bash("git tag --sort=-v:refname"); +log(tags); + +// 3. Коммиты, ещё не попавшие в релиз +const pending = await bash("git log " + latestTag + "..HEAD --oneline"); +log("commits since tag: " + pending); ``` -Собери из истории коммитов и diffs понимание: какие фичи, фиксы, рефакторинг. +### Алгоритм + +1. **База** = последний git-тег. Пример: `v5.0.1` → `5.0.1`. +2. **Версия** = база + 1 в patch. Пример: `5.0.1` → `5.0.2`. +3. **CHANGELOG.md НЕ является источником версии.** Даже если в файле запись новее последнего git-тега (например, файл `[5.1.0]`, а тег `v5.0.1`) — **игнорируй файл** и всё равно считай от тега: `5.0.1 → 5.0.2`. Мы перезаписываем CHANGELOG целиком, поэтому прошлое содержимое не имеет значения. **Единственный источник истины — git-тег.** +4. **Проверка дубликата:** если в CHANGELOG уже есть запись для выбранной версии — **не перезаписывай**. Возьми следующую по semver. +5. **Соответствие тега:** версия X.Y.Z → будущий git-тег `vX.Y.Z`. Укажи в отчёте. -### 2. Определи версию +**Исключения (редко):** -Возьми последнюю версию из CHANGELOG.md и подними: +- Если пользователь **явно просит** minor/major (`«сделай minor-релиз»`, `«breaking change — это v6»`) — используй его указание. +- Если коммит содержит `feat!` или `BREAKING CHANGE:` — в отчёте **упомяни** это как замечание, но **всё равно поставь patch+1**. Major делается отдельно и вручную. + +**НЕ останавливайся и НЕ спрашивай, если:** + +- в CHANGELOG есть версия новее последнего тега (мы перезаписываем файл); +- `git describe` вернул старый тег (используй `git tag --sort=-v:refname` и бери максимум); +- набор изменений «выглядит как minor» — правило patch+1 всё равно важнее, если пользователь не сказал иного. + +## Дата — строго из системы + +**Не выдумывай дату.** Возьми её из системы: + +```cuckoo +const d = await cityTime(); +log(d); +``` -- **patch** (x.y.**Z**) — багфиксы, мелкие правки; -- **minor** (x.**Y**.0) — новые фичи, совместимые; -- **major** (**X**.0.0) — breaking changes. +Формат даты — ISO: `YYYY-MM-DD`. Только она идёт в заголовок `## [X.Y.Z] - YYYY-MM-DD`. -### 3. Составь запись +**Никогда:** + +- не ставь дату из прошлого релиза; +- не округляй месяц/день; +- если `cityTime()` вернул нестандартный формат — приведи вручную к `YYYY-MM-DD`. + +## Разведка изменений + +Узнай, что изменилось с последнего релиза: + +```cuckoo +const latestTag = await bash("git describe --tags --abbrev=0"); +const log1 = await bash("git log " + latestTag + "..HEAD --oneline"); +log(log1); +const diff = await bash("git diff --stat " + latestTag + "..HEAD"); +log(diff); +``` + +Собери из истории коммитов понимание: какие фичи, фиксы, рефакторинг. + +## Составь запись Строго по шаблону из гайда: @@ -72,33 +124,75 @@ log(diff); - Что удалили и почему. ``` -**Правила:** +**Правила оформления:** - Секции `###` (три решётки), только `Added`/`Changed`/`Fixed`/`Removed`. - Пункты — `- ` или `* `, никогда нумерованные. - Язык русский, стиль существующего CHANGELOG. - Названия фич **жирным**, имена кода в `бэктиках`. - Детали — вложенными пунктами (отступ 2 пробела). -- Не выдумывай фичи — пиши только то, что реально видно в diff/логе. +- Не выдумывай фичи — только то, что реально видно в diff/логе. -### 4. Вставка в CHANGELOG.md +## Перепиши CHANGELOG.md — оставь ТОЛЬКО последнюю версию -Новая запись идёт **в самый верх**, сразу под `# Changelog`. -Используй `edit`: найди строку `# Changelog` и вставь блок после неё. +**КРИТИЧНО:** файл `CHANGELOG.md` должен содержать **ровно одну** запись — +последнюю версию. Все старые записи (предыдущие релизы) **удаляются**. -```cuckoo -await edit("CHANGELOG.md", - "# Changelog\n\n## [5.0.0]", - "# Changelog\n\n## [5.1.0] - 2026-10-06\n\n### Added\n\n- ...\n\n## [5.0.0]" -); +Формат файла — **всегда такой**: + +```markdown +# Changelog + +## [X.Y.Z] - YYYY-MM-DD + +### Added + +... + +### Changed + +... + +### Fixed + +... ``` -### 5. Проверка +**Никаких `## [5.0.0]`, `## [4.0.37]` и т.п. ниже.** Сразу после последней +секции — конец файла. + +### Как переписать -Убедись, что парсер видит новую версию: +1. Прочитай текущий `CHANGELOG.md` (весь, но тебе нужен только заголовок `# Changelog`). +2. Собери новую запись (см. выше — на базе последнего git-тега +1 в patch). +3. Используй `write` (НЕ `edit`) — полностью перезапиши файл: ```cuckoo -const check = await bash(`node -e "const fs=require('fs'); const {parseChangelog}=require('./src/main/changelog-parser'); const e=parseChangelog(fs.readFileSync('CHANGELOG.md','utf8')); console.log(e.slice(0,5).map(x=>x.version).join(', '));"`); +const header = "# Changelog"; +const body = ` +## [5.0.2] - 2026-10-06 + +### Added + +- **Что добавили** — описание. + +### Changed + +- **Что изменилось** — было → стало. + +### Fixed + +- Что починили. +`; +await write("CHANGELOG.md", header + "\n" + body); +``` + +## Проверь парсером + +```cuckoo +const check = await bash( + "node -e \"const fs=require('fs'); const {parseChangelog}=require('./src/main/changelog-parser'); const e=parseChangelog(fs.readFileSync('CHANGELOG.md','utf8')); console.log(e.slice(0,5).map(x=>x.version).join(', '));\"" +); log(check); ``` @@ -109,14 +203,20 @@ log(check); 1. **НЕ трогай `package.json`** — версию туда выставит CI из git-тега. 2. **НЕ создавай коммит и НЕ пушь** — это делает родительский диалог отдельно. 3. **НЕ выдумывай изменения** — только то, что реально видно в git history. -4. **НЕ переписывай старые записи** — только добавь новую сверху. -5. **НЕ меняй `CHANGELOG_GUIDE.md`** — если найдёшь проблему в гайде, сообщи в отчёте. -6. Формат заголовка — **строго** `## [X.Y.Z] - YYYY-MM-DD`. Скобки, дефис, ISO-дата. +4. **В CHANGELOG.md должна остаться ровно ОДНА запись** — последняя версия. Все старые записи **удаляются** (перезаписывай файл целиком через `write`). +5. **НЕ меняй `CHANGELOG_GUIDE.md`** — если найдёшь проблему, сообщи в отчёте. +6. Формат заголовка — **строго** `## [X.Y.Z] - YYYY-MM-DD`. +7. **Версия = последний git-тег + 1 в patch** (по умолчанию). Не выдумывай minor/major сам. +8. **НЕ вставляй "Unreleased"** — эта секция не покажется в What's New. +9. **НЕ добавляй историю старых версий** — ни `## [5.0.0]`, ни предыдущих. Только текущая. ## Формат отчёта родителю ``` Добавлена версия X.Y.Z (дата YYYY-MM-DD). +Git-тег для push: vX.Y.Z +Базовая версия (последний git-тег): A.B.C +Инкремент: patch (+1 в последнюю цифру) Секции: - Added: N пунктов From 1234ffe1ded91f98e8ccc544e1d30c3b4fab5cd2 Mon Sep 17 00:00:00 2001 From: MerfiDEV Date: Tue, 6 Oct 2026 19:27:06 +0300 Subject: [PATCH 10/11] feat(telegram): add /agents command to list project subagents Add /agents (alias /agent) command that scans cookie/agents/*.md and renders a formatted list of project subagents with name, description, tools and maxTurns. Also add related i18n strings (ru/en) and register the command in the help text and bot menu. Refresh CHANGELOG.md in a friendlier tone and add the AI memory section. --- CHANGELOG.md | 47 ++++++++++++++++++------------- README.en.md | 10 +++---- README.md | 10 +++---- README.ru.md | 10 +++---- botsrc/i18n.js | 30 ++++++++++++++++++++ botsrc/index.js | 75 +++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 148 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41d8272..5b7ae5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,28 +4,37 @@ ### Added -- 🚀 **Субагенты — AI создаёт своих помощников** — задачи можно делегировать изолированным AI-агентам, каждый в своём окне и со своим контекстом. Черновики и tool-вызовы субагента не засоряют основной диалог — родителю возвращается только финальное резюме: - - новый инструмент `run_agent(name, task)` — запускает агента в дочернем окне (тот же `partition` → общая сессия DeepSeek), ждёт финальный ответ и возвращает текст родителю; - - `list_agents` — список доступных субагентов (проект + пользователь) с `description`, `tools`, `maxTurns`; - - `read_agent(name)` — полный `systemPrompt` агента для отладки; - - `create_agent({name, description, systemPrompt, tools?, maxTurns?, overwrite?})` — AI создаёт специалиста под задачу, файл сразу доступен для `run_agent`; - - сканер агентов: `cookie/agents/*.md` (проект) и `~/cookie/agents/*.md` (пользовательские), формат — markdown с frontmatter (`name`, `description`, `tools`, `maxTurns`); - - готовые агенты в проекте: `explore`, `code-reviewer`, `git-committer`, `changelog-writer`. -- 🔒 **Whitelist инструментов для субагентов** — в frontmatter агента можно указать `tools: read, grep`, и агент физически не сможет вызвать `bash` или `mysql`: - - список прокидывается в контекст дочернего окна и проверяется в `JsRunner` до исполнения; - - запрещённый вызов отклоняется с явной ошибкой «Инструмент … не разрешён этому агенту»; - - защита от рекурсии: субагент не может вызвать `run_agent`. -- 🤖 **Уведомления о жизненном цикле субагента в Telegram** — стартовая карточка перед открытием дочернего окна, события `run`/`create`/`list`/`read`, а имена агентов в алертах вызовов инструментов помечаются префиксом «🤖 Агент: <имя>»: - - дублирующие уведомления об ответах AI из субагентских окон подавляются; - - завершение субагента отслеживается по стабилизации текста (а не по legacy-проверке «AI завершил»), с учётом лимита ходов, таймаута и отображения рабочей папки. -- 📚 **Новый раздел README про субагентов** — как делегировать, три способа завести агента, что защищает (whitelist, anti-recursion, `maxTurns`, timeout) и пример агента. +- 🚀 **Субагенты — теперь у AI есть свои помощники!** 😎 Представьте: просите AI «разберись с этим куском проекта», а он не тонет в тысяче файлов сам, а зовёт специалиста. Каждый субагент работает в **отдельном окне** со своим контекстом — весь его рабочий шум (поиски, чтения, пробы) остаётся там, а вам прилетает только чистый итог. Основной чат не пухнет, голова не кругом: + - 🎯 `run_agent(name, task)` — «эй, explore, найди все вызовы apiFetch». Открывается второе окно (тот же логин DeepSeek — ничего заново вводить не надо), субагент делает дело, окно само закрывается, родителю уходит резюме; + - 📋 `list_agents` — глянуть, кто вообще у нас в команде: имя, описание, какие инструменты разрешены, сколько шагов максимум; + - 🔍 `read_agent(name)` — подсмотреть полный промпт агента (удобно для отладки); + - 🧬 `create_agent({...})` — а вот самое интересное: **AI может вырастить себе специалиста прямо из чата!** «Создай агента для ревью SQL-миграций» — и через секунду файл готов, можно сразу делегировать; + - 📂 агенты лежат в `cookie/agents/*.md` (проектные — едут с репо, команда видит) или в `~/cookie/agents/*.md` (личные, для всех проектов). Формат — обычный markdown с frontmatter (`name`, `description`, `tools`, `maxTurns`); + - 🎁 из коробки уже есть четверо: `explore` (разведчик), `code-reviewer` (ревьюер), `git-committer` (гит-мастер), `changelog-writer` (летописец). +- 🔒 **Whitelist инструментов — субагент делает только то, что ему разрешили** 🛡️ Написали в frontmatter `tools: read, grep` — и всё, `bash`, `mysql`, `deleteFile` для него закрыты наглухо: + - проверка происходит **до** запуска инструмента — не «а потом откатим», а «даже не начнём»; + - попробует запрещённое — получит чёткое «Инструмент X не разрешён этому агенту, разрешены: read, grep»; + - а ещё субагент не может позвать другого субагента — никаких бесконечных матрёшек 🪆. +- 🤖 **Субагенты теперь болтают с вами в Telegram** 📱 Полный жизненный цикл на экране телефона: + - 🚀 при делегировании сразу прилетает карточка «кому и что поручили»; + - 🛠 каждое действие агента помечено «🤖 Агент: <имя>» — сразу видно, кто работает; + - 🎯 когда агент закончил — отдельное сообщение с итогом; + - 🔇 и никаких дублей: ответы из окна субагента не спамят в чат-фид; + - ⏱ финал определяется по «текст устоялся» (а не по старой ненадёжной проверке), с учётом лимита шагов и таймаута. +- 🧠 **Память AI — теперь AI вас помнит** 💭 Скажите «Запомни, что меня зовут Ваня» или «Сохрани в память: у меня Windows 11 и RTX 3060» — и он запомнит. В следующей сессии уже будет знать контекст, не придётся повторять: + - 💾 `memory_save(text)` — сохранить одну мысль (одна запись = одна строчка с датой); + - 📖 `memory_read()` — прочитать всё, что накопилось; + - 🗑 `memory_clear()` — стереть всё (только по прямой просьбе — AI сначала предупредит); + - 📁 всё живёт в одном файлике `/memory.md`, можно открыть или очистить прямо из настроек; + - 🎯 содержимое памяти **не** подмешивается в промпт автоматически — читается только когда реально нужно, чтобы не жечь токены зря; + - 🔒 в plan-mode любые изменения памяти заблокированы — на этапе планирования AI ничего не перепишет. +- 📚 **Новый раздел в README про субагентов** — как делегировать, три способа завести агента, что его защищает (whitelist, anti-recursion, `maxTurns`, timeout) и готовый пример файла агента. Читайте и заводите своих! 📖 ### Changed -- 📁 **Хранилище агентов переехало `.cuckoo/agents` → `cookie/agents`** — проектные агенты лежат в репозитории рядом с кодом, пользовательские — в `~/cookie/agents`. Сканер, типы и документация обновлены. -- 🎛 **Диалог с AI в дочернем окне получает полный системный промпт Cookie Code** (инструменты + формат `cuckoo`) вместе с промптом агента и задачей — субагент работает так же, как основной ассистент. +- 📁 **Агенты переехали: `.cuckoo/agents` → `cookie/agents`** — теперь проектные агенты лежат в репозитории рядышком с кодом (команда видит и использует), а личные — в `~/cookie/agents` (вне репо). Сканер, типы и документация обновлены. 🚚 +- 🎛 **Окно субагента получает полный системный промпт Cookie Code** — тот же набор инструментов и формат `cuckoo`, что у основного ассистента. То есть субагент работает ровно так же умело, просто в своей песочнице. 🧰 ### Fixed -- 🐛 **Потеря рабочей папки субагента** — при загрузке домашней страницы DeepSeek (когда `sessionId` ещё нет) выбранная папка проекта сбрасывалась в `null`, и инструменты субагента видели `projectDir = null`. Добавлен `pendingProjectDir`: папка кэшируется до готовности сессии, прокидывается в контекст субагента и привязывается к сессии автоматически. -- 🗂 **`cookie/agents` теперь трекается в git** — папка была в `.gitignore`, из-за чего агенты, созданные через `create_agent`, оставались untracked. Игнор убран: агенты проекта должны попадать в репозиторий (пользовательские лежат вне репо). +- 🐛 **Рабочая папка субагента больше не теряется** — была хитрая бага: при загрузке домашней DeepSeek (когда `sessionId` ещё не появился) выбранная папка проекта сбрасывалась в `null`, и агент начинал искать файлы «в никуда». Теперь папка кэшируется (`pendingProjectDir`) до готовности сессии и привязывается сама — относительные пути работают с первой секунды. ✅ diff --git a/README.en.md b/README.en.md index d135eef..5a3aa85 100644 --- a/README.en.md +++ b/README.en.md @@ -15,11 +15,6 @@ Cookie Code — main window

-

- Cookie Code — subagents - Cookie Code — control panel -

- **Turn DeepSeek into a real coding assistant.** Cookie Code bridges the gap between AI chat and your actual codebase — no API tokens, no copy-paste, just pure automation. --- @@ -62,6 +57,11 @@ The AI can now: ### 🚀 **Subagents — AI Builds Its Own Helpers** +

+ Cookie Code — subagents + Cookie Code — control panel +

+ Cookie Code can **delegate tasks to isolated AI agents**, each in its own window with its own context. The subagent's drafts, searches, and tool calls **never pollute the main conversation** — only the final summary comes back to the parent. **How it works:** diff --git a/README.md b/README.md index 069d039..ca278b4 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,6 @@ Cookie Code — главное окно

-

- Cookie Code — субагенты - Cookie Code — панель управления -

- **Превратите DeepSeek в настоящего помощника по коду.** Cookie Code соединяет AI-чат с вашей реальной кодовой базой — без API-токенов, без копипасты, только чистая автоматизация. --- @@ -63,6 +58,11 @@ ### 🚀 **Субагенты — AI создаёт своих помощников** +

+ Cookie Code — субагенты + Cookie Code — панель управления +

+ Cookie Code умеет **делегировать задачи изолированным AI-агентам**, каждый в своём окне и со своим контекстом. Черновики, поиски и tool-вызовы субагента **не засоряют основной диалог** — родителю возвращается только финальное резюме. **Как это работает:** diff --git a/README.ru.md b/README.ru.md index 3266286..7b8b5b4 100644 --- a/README.ru.md +++ b/README.ru.md @@ -12,11 +12,6 @@ English · Русский

-

- Cookie Code — субагенты - Cookie Code — панель управления -

- **Cookie Code** — AI-агент для рабочего стола с **нулевой стоимостью токенов**. Приложение встраивает веб-чат DeepSeek в нативное окно Electron, добавляет боковой оверлей и превращает чат в локальный исполнитель: AI генерирует вызовы инструментов (блоки JavaScript-кода), которые перехватываются, подтверждаются, выполняются в локальной песочнице и стримятся обратно в чат. Никакого API-ключа, никакой оплаты за токены — вы используете обычный веб-аккаунт. @@ -150,6 +145,11 @@ ### Субагенты +

+ Cookie Code — субагенты + Cookie Code — панель управления +

+ Cookie Code умеет **делегировать задачи изолированным AI-агентам**. Каждый субагент работает в **отдельном окне** с тем же partition (общий логин DeepSeek), в **своём контексте**, и возвращает родителю **только финальное резюме** — черновики, поиски и tool-вызовы не засоряют основной диалог. **Пример:** diff --git a/botsrc/i18n.js b/botsrc/i18n.js index 4fb48b9..ca973a0 100644 --- a/botsrc/i18n.js +++ b/botsrc/i18n.js @@ -44,6 +44,7 @@ const STRINGS = { "help.cmd.show": "/show <hash> — diff коммита", "help.cmd.files": "/files <hash> — файлы коммита", "help.cmd.todos": "/todos — список задач активного окна", + "help.cmd.agents": "/agents — список субагентов проекта", "help.cmd.screen": "/screen — скриншот активного окна", "help.cmd.cancel": "/cancel — отменить ввод / подтверждение", "help.cmd.help": "/help — эта справка", @@ -283,6 +284,20 @@ const STRINGS = { "agent.created": "Агент создан", "agent.existing": "Агент обновлён", "agent.prompt": "Системный промпт", + "agents.cmd.title": "🤖 Субагенты проекта", + "agents.cmd.empty": + "🤖 Субагентов нет\n\nДобавьте .md-файл в cookie/agents/ или попросите AI: «Создай агента для …».", + "agents.cmd.noProject": + "📂 Проект не выбран\n\nСначала выберите проект в главном окне — субагенты привязаны к проекту.", + "agents.cmd.header": "🤖 Субагенты проекта — {n} шт.", + "agents.cmd.itemTools": "🧰 Инструменты", + "agents.cmd.itemTurns": "🔁 Шагов", + "agents.cmd.itemSource": "📍 Источник", + "agents.cmd.itemNoTools": "все разрешены", + "agents.cmd.itemNoLimit": "без лимита", + "agents.cmd.hint": + '💡 Делегируйте задачу: run_agent("name", "задача")', + "agents.cmd.more": "…и ещё {n}", // ===== Approval ===== "approval.title": "🔐 Подтверждение команды", @@ -372,6 +387,7 @@ const STRINGS = { "help.cmd.show": "/show <hash> — commit diff", "help.cmd.files": "/files <hash> — commit files", "help.cmd.todos": "/todos — tasks of the active window", + "help.cmd.agents": "/agents — project subagents list", "help.cmd.screen": "/screen — screenshot of the active window", "help.cmd.cancel": "/cancel — cancel input / approval", "help.cmd.help": "/help — this help", @@ -612,6 +628,20 @@ const STRINGS = { "agent.created": "Agent created", "agent.existing": "Agent updated", "agent.prompt": "System prompt", + "agents.cmd.title": "🤖 Project subagents", + "agents.cmd.empty": + '🤖 No subagents\n\nAdd a .md file to cookie/agents/ or ask the AI: "Create an agent for …".', + "agents.cmd.noProject": + "📂 No project selected\n\nPick a project in the main window first — subagents are project-scoped.", + "agents.cmd.header": "🤖 Project subagents — {n} total", + "agents.cmd.itemTools": "🧰 Tools", + "agents.cmd.itemTurns": "🔁 Steps", + "agents.cmd.itemSource": "📍 Source", + "agents.cmd.itemNoTools": "all allowed", + "agents.cmd.itemNoLimit": "no limit", + "agents.cmd.hint": + '💡 Delegate a task: run_agent("name", "task")', + "agents.cmd.more": "…and {n} more", // ===== Approval ===== "approval.title": "🔐 Command approval", diff --git a/botsrc/index.js b/botsrc/index.js index 4be2eb6..914d27a 100644 --- a/botsrc/index.js +++ b/botsrc/index.js @@ -1761,6 +1761,73 @@ async function _cmdTodos(chatId, page) { }); } +/** /agents — список субагентов проекта (из cookie/agents/*.md). */ +async function _cmdAgents() { + let agents = []; + let projectDir = null; + try { + projectDir = _projectDir(); + if (projectDir) { + const { scanAgents } = require("../src/main/agents"); + agents = scanAgents(projectDir); + } + } catch (err) { + _log("error", "/agents error:", err.message); + } + + // Проект не выбран — подсказываем. + if (!projectDir) { + return telegramBot.sendMessage(_t("agents.cmd.noProject"), { + parseMode: "HTML", + }); + } + + // Проект есть, но агентов нет. + if (!Array.isArray(agents) || agents.length === 0) { + return telegramBot.sendMessage(_t("agents.cmd.empty"), { + parseMode: "HTML", + }); + } + + const lines = []; + lines.push(_t("agents.cmd.header", { n: agents.length })); + lines.push(""); + + const MAX = 15; // ограничим вывод, чтобы не улететь за лимит TG + const shown = agents.slice(0, MAX); + for (let i = 0; i < shown.length; i++) { + const a = shown[i]; + const tools = + a.tools && a.tools.length + ? a.tools.map((x) => "" + escapeHtml(x) + "").join(", ") + : "" + escapeHtml(_t("agents.cmd.itemNoTools")) + ""; + const turns = a.maxTurns + ? escapeHtml(String(a.maxTurns)) + : "" + escapeHtml(_t("agents.cmd.itemNoLimit")) + ""; + const sourceEmoji = a.source === "project" ? "📦" : "🏠"; + lines.push( + "" + (i + 1) + ". " + escapeHtml(a.name) + " " + sourceEmoji, + ); + if (a.description) { + lines.push(escapeHtml(a.description)); + } + lines.push(" " + escapeHtml(_t("agents.cmd.itemTools")) + ": " + tools); + lines.push(" " + escapeHtml(_t("agents.cmd.itemTurns")) + ": " + turns); + lines.push(""); + } + if (agents.length > MAX) { + lines.push( + "" + + escapeHtml(_t("agents.cmd.more", { n: agents.length - MAX })) + + "", + ); + lines.push(""); + } + lines.push(_t("agents.cmd.hint")); + + return telegramBot.sendMessage(lines.join("\n"), { parseMode: "HTML" }); +} + /** Текущий projectDir активного окна. */ function _projectDir() { const ctx = _activeContext(); @@ -2210,6 +2277,7 @@ async function _cmdHelp() { _t("help.cmd.show"), _t("help.cmd.files"), _t("help.cmd.todos"), + _t("help.cmd.agents"), _t("help.cmd.screen"), _t("help.cmd.cancel"), _t("help.cmd.help"), @@ -2698,6 +2766,11 @@ async function _handleIncoming(chatId, text, msg) { await _cmdScreen(); return; } + // Команда /agents — список субагентов проекта. + if (cmd === "/agents" || cmd === "/agent") { + await _cmdAgents(); + return; + } // ---- Ожидание ввода значения настройки ---- if (_settingsWaiting.has(chatId)) { @@ -3585,6 +3658,7 @@ async function _registerCommands() { switch: "переключить сессию", diagnostics: "диагностика", todos: "список задач", + agents: "субагенты проекта", screen: "скриншот окна", cancel: "отменить ввод", }, @@ -3603,6 +3677,7 @@ async function _registerCommands() { switch: "switch session", diagnostics: "diagnostics", todos: "todo list", + agents: "project subagents", screen: "window screenshot", cancel: "cancel", }, From d36e897fb90d6619be54be6cf94f1256cdb47e40 Mon Sep 17 00:00:00 2001 From: MerfiDEV Date: Tue, 6 Oct 2026 19:33:54 +0300 Subject: [PATCH 11/11] main win tg bot --- botsrc/index.js | 53 ++++++++++++++++++++++++++++++++++------------ src/main/window.js | 29 +++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 14 deletions(-) diff --git a/botsrc/index.js b/botsrc/index.js index 914d27a..f3d71a7 100644 --- a/botsrc/index.js +++ b/botsrc/index.js @@ -124,7 +124,12 @@ function _log(level, ...args) { */ async function _sendToChat(text) { try { - const win = windowState.getMainWindow(); + // ВАЖНО: шлём в родительское (не-субагентское) окно. Если открыто + // окно субагента, оно могло стать «активным» — но команды пользователя + // из TG всегда должны попадать в главный чат. + const win = windowState.getParentWindow + ? windowState.getParentWindow() + : windowState.getMainWindow(); if (!win || win.isDestroyed()) return { success: false, error: "нет активного окна" }; @@ -165,9 +170,11 @@ async function _sendToChat(text) { } } -/** Выполнить JS в активном окне и вернуть результат. */ +/** Выполнить JS в родительском (не-субагентском) окне. */ async function _evalInWindow(script) { - const win = windowState.getMainWindow(); + const win = windowState.getParentWindow + ? windowState.getParentWindow() + : windowState.getMainWindow(); if (!win || win.isDestroyed()) return { success: false, error: "нет активного окна" }; try { @@ -178,10 +185,12 @@ async function _evalInWindow(script) { } } -/** Получить контекст активного окна (профиль/сессия/projectDir). */ +/** Получить контекст родительского (не-субагентского) окна. */ function _activeContext() { try { - const win = windowState.getMainWindow(); + const win = windowState.getParentWindow + ? windowState.getParentWindow() + : windowState.getMainWindow(); if (win && !win.isDestroyed() && win.webContents) { return windowState.getContextByWebContents(win.webContents); } @@ -189,10 +198,12 @@ function _activeContext() { return null; } -/** Получить id активного окна (для доступа к его todo-списку). */ +/** Получить id родительского (не-субагентского) окна (для todo-списка). */ function _activeSenderId() { try { - const win = windowState.getMainWindow(); + const win = windowState.getParentWindow + ? windowState.getParentWindow() + : windowState.getMainWindow(); if (win && !win.isDestroyed() && win.webContents) return win.webContents.id; } catch (_) {} return null; @@ -1473,7 +1484,9 @@ async function _handleSettingsCallback(data, cbq) { async function _cmdStop() { let count = 0; try { - const win = windowState.getMainWindow(); + const win = windowState.getParentWindow + ? windowState.getParentWindow() + : windowState.getMainWindow(); if ( win && !win.isDestroyed() && @@ -1506,7 +1519,9 @@ async function _cmdScreen() { if (!cfg.enabled || !cfg.token || !cfg.chatId) return { success: false, skipped: true }; - const win = windowState.getMainWindow(); + const win = windowState.getParentWindow + ? windowState.getParentWindow() + : windowState.getMainWindow(); if (!win || win.isDestroyed()) { await telegramBot.sendMessage(_t("screen.noWindow")); return { success: false, error: "noWindow" }; @@ -1546,7 +1561,9 @@ async function _cmdStatus() { } catch (_) {} lines.push(_t("status.version", { v: escapeHtml(version) })); - const win = windowState.getMainWindow(); + const win = windowState.getParentWindow + ? windowState.getParentWindow() + : windowState.getMainWindow(); const winOk = !!(win && !win.isDestroyed()); lines.push( _t("status.window") + @@ -1614,7 +1631,9 @@ async function _cmdStatus() { async function _cmdNew() { const win = (() => { try { - return windowState.getMainWindow(); + return windowState.getParentWindow + ? windowState.getParentWindow() + : windowState.getMainWindow(); } catch (_) { return null; } @@ -1954,7 +1973,9 @@ function _projectKeyboard() { async function _setProject(chatId, dir) { const win = (() => { try { - return windowState.getMainWindow(); + return windowState.getParentWindow + ? windowState.getParentWindow() + : windowState.getMainWindow(); } catch (_) { return null; } @@ -2418,7 +2439,9 @@ async function _sendInWindow(win) { async function _insertImageToWindow(absPath, caption, send) { try { const { clipboard, nativeImage } = require("electron"); - const win = windowState.getMainWindow(); + const win = windowState.getParentWindow + ? windowState.getParentWindow() + : windowState.getMainWindow(); if (!win || win.isDestroyed()) return { success: false, error: _t("common.noWindow") }; const img = nativeImage.createFromPath(absPath); @@ -2470,7 +2493,9 @@ async function _attachFileToWindow(absPath, caption, send) { UPLOAD_TIMEOUT_MS, MAX_FILE_SIZE, } = require("../tools/AttachFileTool"); - const win = windowState.getMainWindow(); + const win = windowState.getParentWindow + ? windowState.getParentWindow() + : windowState.getMainWindow(); if (!win || win.isDestroyed()) return { success: false, error: _t("common.noWindow") }; const stat = fs.statSync(absPath); diff --git a/src/main/window.js b/src/main/window.js index 4b7eb45..d4b67ed 100644 --- a/src/main/window.js +++ b/src/main/window.js @@ -61,6 +61,33 @@ function getMainWindow() { return ctx ? ctx.win : null; } +/** + * Родительское окно (НЕ субагент). Для TG-бота: команды пользователя + * должны всегда попадать в главное окно, даже если открыто окно субагента. + * Берём самое свежее не-субагентское окно; если такого нет — падаем на getMainWindow(). + */ +function getParentWindow() { + let fallback = null; + for (const [id, ctx] of windows) { + if (!ctx.isSubagent && ctx.win && !ctx.win.isDestroyed()) { + fallback = ctx.win; + } + } + return fallback || getMainWindow(); +} + +/** Родительский контекст (не субагент). Аналогично getParentWindow(). */ +function getParentContext() { + let fallback = null; + for (const [id, ctx] of windows) { + if (!ctx.isSubagent && ctx.win && !ctx.win.isDestroyed()) { + fallback = ctx; + } + } + if (fallback) return fallback; + return getMainContext(); +} + function getMainContext() { if (!lastActiveWindowId) return null; return windows.get(lastActiveWindowId) || null; @@ -96,6 +123,8 @@ module.exports = { getContextByWebContents, getMainWindow, getMainContext, + getParentWindow, + getParentContext, setMainWindow, getAllWindows, getAllContexts,