diff --git a/.gitignore b/.gitignore index aa8ae16..54ff5d9 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,6 @@ test/coverage.lcov .playwright-mcp/ /tiktok /.cuckoo/skills +# cookie/agents — НЕ игнорируем: агенты проекта должны попадать в git. +# Пользовательские агенты лежат в ~/cookie/agents (вне репозитория). +# Ранее здесь был /cookie/agents — убрано, иначе новые агенты untracked. diff --git a/CHANGELOG.md b/CHANGELOG.md index b771b3b..5b7ae5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,90 +1,40 @@ # 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 «разберись с этим куском проекта», а он не тонет в тысяче файлов сам, а зовёт специалиста. Каждый субагент работает в **отдельном окне** со своим контекстом — весь его рабочий шум (поиски, чтения, пробы) остаётся там, а вам прилетает только чистый итог. Основной чат не пухнет, голова не кругом: + - 🎯 `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 -- 📖 **Новая структура 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` (вне репо). Сканер, типы и документация обновлены. 🚚 +- 🎛 **Окно субагента получает полный системный промпт 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`, и агент начинал искать файлы «в никуда». Теперь папка кэшируется (`pendingProjectDir`) до готовности сессии и привязывается сама — относительные пути работают с первой секунды. ✅ diff --git a/README.en.md b/README.en.md index 0e0b267..5a3aa85 100644 --- a/README.en.md +++ b/README.en.md @@ -27,9 +27,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 +39,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 +49,57 @@ 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 — 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:** + +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 dd73361..ca278b4 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@

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

**Превратите DeepSeek в настоящего помощника по коду.** Cookie Code соединяет AI-чат с вашей реальной кодовой базой — без API-токенов, без копипасты, только чистая автоматизация. @@ -27,9 +27,10 @@ ✅ **Нулевая стоимость токенов** — использует ваш бесплатный веб-аккаунт DeepSeek, без API-ключей ✅ **Настоящий агентный цикл** — AI читает файлы → пишет код → запускает тесты → коммитит изменения +✅ **Субагенты** — AI делегирует задачи изолированным агентам (свой контекст, свой whitelist инструментов) ✅ **Интеграция Git** — смотрите diff, листайте историю, проверяйте изменения визуально ✅ **Управление через Telegram** — пишите код с телефона, получайте уведомления, подтверждайте команды удалённо -✅ **Красивый UI** — стеклянные эффекты, кастомные темы, анимированные петы, тёмная тема +✅ **Красивый UI** — стеклянные эффекты, кастомные темы, анимированные петы, тёмная тема --- @@ -38,6 +39,7 @@ ### 🤖 **AI-агент, который реально работает** Теперь AI умеет: + - 📖 **Читать файлы** — структура проекта, grep-паттерны, содержимое - ✏️ **Писать и редактировать код** — создавать файлы, применять патчи, удалять старый код - 💻 **Запускать команды** — bash, PowerShell, SQL-запросы, любые shell-скрипты @@ -45,13 +47,60 @@ - 📊 **Управлять задачами** — создавать, отслеживать и завершать todo-списки - 🌐 **Получать данные** — HTTP-запросы, API-вызовы, веб-скрапинг - 🔧 **MCP-инструменты** — подключать любой MCP-сервер для расширенных возможностей +- 🧠 **Память** — AI запоминает факты о вас и вашей системе («Сохрани в память, что…»), читает их в следующих сессиях через `memoryRead()`. Файл памяти лежит в папке пользователя, открывается и очищается в настройках. **Как это работает:** + 1. Вы спрашиваете: "Покажи последние изменения в auth.js" 2. AI генерирует: `` `cuckoo const r = await grep('auth.js', 'login');` `` 3. Cookie Code перехватывает, выполняет, стримит результат обратно в AI 4. AI видит вывод и продолжает работать +### 🚀 **Субагенты — AI создаёт своих помощников** + +

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

+ +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** — матовое стекло с настраиваемым размытием и прозрачностью @@ -86,6 +135,7 @@ ### 📊 **Интеграция Git** Встроенный просмотрщик diff: + - **Вкладка "Изменения"** — незакоммиченные изменения, построчные diff - **Вкладка "История"** — листайте коммиты, кликайте для просмотра полного diff - **Навигация** — прыгайте между hunks, фильтруйте добавленные/удалённые строки @@ -95,6 +145,7 @@ ### 📈 **Дашборд статистики** Отслеживайте продуктивность: + - **Тепловая карта активности** — график вкладов в стиле GitHub - **Использование токенов** — тренды по дням/неделям - **Любимые модели** — какой AI вы используете чаще @@ -151,6 +202,7 @@ AI: [git commit] Закоммичено: "feat(auth): добавлены тес ### Исследуйте Откройте боковую панель для: + - 🗂️ Просмотра сессий - 📝 Управления задачами - 🔄 Просмотра git diff @@ -162,19 +214,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 +234,7 @@ AI: [git commit] Закоммичено: "feat(auth): добавлены тес **Темы:** Размытие фона, прозрачность панелей, стеклянные цвета **Петы:** Бросьте PNG/GIF в `userData/pets/`, выберите из сетки -**Шрифты:** Бросьте `.ttf`/`.woff` в `userData/fonts/`, выберите из выпадающего списка +**Шрифты:** Бросьте `.ttf`/`.woff` в `userData/fonts/`, выберите из выпадающего списка --- @@ -196,6 +248,7 @@ AI: [git commit] Закоммичено: "feat(auth): добавлены тес 6. Пушьте и открывайте PR **Области, где нужна помощь:** + - 🌍 Переводы - 📚 Документация - 🐛 Отчёты об ошибках diff --git a/README.ru.md b/README.ru.md index f30c0b8..7b8b5b4 100644 --- a/README.ru.md +++ b/README.ru.md @@ -143,6 +143,69 @@ Поместите папку в `.cuckoo/skills//` с файлом `SKILL.md` (и опциональным `tool.js`) — она станет вызываемой через `skillList`, `skillLoad`, `skillExecute`. +### Субагенты + +

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

+ +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 0000000..f302498 Binary files /dev/null and b/assets/photo_2026-10-06_18-24-20.jpg differ 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 0000000..b6da867 Binary files /dev/null and b/assets/photo_2026-10-06_18-24-55.jpg differ diff --git a/botsrc/i18n.js b/botsrc/i18n.js index b138418..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 — эта справка", @@ -250,6 +251,54 @@ 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": "Город / страна", + + // ===== Субагенты (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": "Системный промпт", + "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": "🔐 Подтверждение команды", "approval.jsScript": "JS-скрипт", @@ -338,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", @@ -545,6 +595,54 @@ 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", + + // ===== 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", + "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", "approval.jsScript": "JS script", diff --git a/botsrc/index.js b/botsrc/index.js index d941277..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; } @@ -1761,6 +1780,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(); @@ -1887,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; } @@ -2210,6 +2298,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"), @@ -2350,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); @@ -2402,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); @@ -2698,6 +2791,11 @@ async function _handleIncoming(chatId, text, msg) { await _cmdScreen(); return; } + // Команда /agents — список субагентов проекта. + if (cmd === "/agents" || cmd === "/agent") { + await _cmdAgents(); + return; + } // ---- Ожидание ввода значения настройки ---- if (_settingsWaiting.has(chatId)) { @@ -2772,7 +2870,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 +2878,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 на старый формат при ошибке. @@ -2970,10 +3068,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; @@ -3015,11 +3122,24 @@ 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), иначе шлём предыдущее и начинаем новое. */ -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(); @@ -3028,13 +3148,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++; // Результат последнего вызова в серии — показываем его. @@ -3049,6 +3171,7 @@ function _techQueue(toolName, args, preview) { preview: result, args, count: 1, + agentName: agent, }; if (prev) _techFlush(); if (_techBatchTimer) clearTimeout(_techBatchTimer); @@ -3060,25 +3183,419 @@ 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/dom/subagent-watch.js b/src/preload/dom/subagent-watch.js new file mode 100644 index 0000000..5a1a6d8 --- /dev/null +++ b/src/preload/dom/subagent-watch.js @@ -0,0 +1,203 @@ +/** + * Наблюдатель за ответами AI в окне-субагенте. + * + * Активируется только если окно открыто с аргументом --cuckoo-subagent=... + * (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 STABLE_MS = 2500; // тишина, после которой считаем ответ финальным +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-контейнер) + сам DOM */ +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; + } +} + +/** + * Показать рабочую папку в 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, + ); + } +} + +/** + * Если окно — субагент, запустить цикл наблюдения за финальным ответом. + */ +function initSubagentWatch() { + const cfg = readConfig(); + if (!cfg) return null; + + console.log("[Cookie Code][Subagent] активирован: " + cfg.agentName); + 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 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) { + 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; + + // Устоявшийся текст. Проверяем, есть ли в нём cuckoo-блоки. + const blocks = (() => { + try { + return getJsCodeBlocksFromMarkdown(got.md) || []; + } catch (_) { + return []; + } + })(); + + if (blocks.length === 0) { + // Финальный текстовый ответ (либо без блоков вообще, либо уже не + // дописывает новые). Отправляем и выходим. + if (snapshot === lastSentText) return; + lastSentText = snapshot; + finish(snapshot, false); + return; + } + + // Есть tool-блоки = промежуточный шаг. Учитываем лимит шагов. + turnCount++; + if (maxTurns && turnCount >= maxTurns) { + finish(snapshot, true); + } else { + console.log( + "[Cookie Code][Subagent] шаг " + + turnCount + + " (tool-блоков: " + + blocks.length + + "), ждём продолжения", + ); + // Сброс, чтобы следующая правка текста засчиталась как новое изменение. + lastSnapshot = ""; + } + } catch (err) { + console.error( + "[Cookie Code][Subagent] ошибка цикла:", + err && err.message, + ); + } + }, POLL_MS); + + // Страховка: если субагент не начал отвечать за 3 минуты — сообщаем + // родителю, что, похоже, отправка промпта не сработала. + setTimeout( + () => { + if (!sent && !everSawText) { + console.warn( + "[Cookie Code][Subagent] ответ не начался за 3 минуты — возможно, " + + "initial-prompt не отправился", + ); + } + }, + 3 * 60 * 1000, + ); + + return cfg; +} + +module.exports = { initSubagentWatch, readConfig }; 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/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/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 72e732b..b5ddbff 100644 --- a/tools/JsRunner.js +++ b/tools/JsRunner.js @@ -182,15 +182,38 @@ 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.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.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 });", " };", " globalThis.exit_plan_mode = globalThis.exitPlanMode;", @@ -350,6 +373,9 @@ class JsRunner { exitPlanMode, sessionId, attachFile, + currentWindowId, + toolsWhitelist, + agentName, ) { if (!code || typeof code !== "string" || !code.trim()) { return { success: false, error: "无效的 JS 代码" }; @@ -389,6 +415,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); @@ -402,6 +449,10 @@ class JsRunner { Object.assign({}, args, { projectDir, senderId, + currentWindowId: + typeof currentWindowId === "number" + ? currentWindowId + : senderId, askUserQuestion, pasteImage, exitPlanMode, @@ -444,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); }; 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/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/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..8e23099 100644 --- a/tools/index.js +++ b/tools/index.js @@ -37,6 +37,14 @@ const { ReadPhotoTool } = require("./ReadPhotoTool"); const { ExitPlanModeTool } = require("./ExitPlanModeTool"); const { AttachFileTool } = require("./AttachFileTool"); const { AttachTelegramTool } = require("./AttachTelegramTool"); +const { + MemorySaveTool, + MemoryReadTool, + MemoryClearTool, +} = require("./MemoryTool"); +const { RunAgentTool } = require("./RunAgentTool"); +const { ListAgentsTool, ReadAgentTool } = require("./AgentTools"); +const { CreateAgentTool } = require("./CreateAgentTool"); // 创建全局工具注册表 const registry = new ToolRegistry(); @@ -76,6 +84,13 @@ 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()); +registry.register(new RunAgentTool()); +registry.register(new ListAgentsTool()); +registry.register(new ReadAgentTool()); +registry.register(new CreateAgentTool()); // 导出 module.exports = { @@ -110,8 +125,15 @@ module.exports = { ReadPhotoTool, ExitPlanModeTool, AttachFileTool, - AttachTelegramTool, - InjectPageJSTool, + AttachTelegramTool, + InjectPageJSTool, + MemorySaveTool, + MemoryReadTool, + MemoryClearTool, + RunAgentTool, + ListAgentsTool, + ReadAgentTool, + CreateAgentTool, // 便捷方法 getAllTools: () => registry, getToolDescriptions: () => registry.getDescriptions(),