mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-27 23:06:54 +05:00
1144 lines
41 KiB
Markdown
1144 lines
41 KiB
Markdown
# ipuaro: Локальный AI-агент для работы с кодовой базой
|
||
|
||
## Цель
|
||
|
||
Один пользователь, одна сессия, работа с кодовой базой любого размера. Ощущение «бесконечного» контекста через ленивую загрузку и умное управление контекстом.
|
||
|
||
---
|
||
|
||
## Технологический стек
|
||
|
||
| Компонент | Технология |
|
||
|-----------|------------|
|
||
| **Язык** | TypeScript |
|
||
| **Runtime** | Node.js |
|
||
| **TUI** | Ink (React для терминала) |
|
||
| **Storage** | Redis с AOF persistence |
|
||
| **AST парсинг** | tree-sitter (js, ts, jsx, tsx, json, yaml) |
|
||
| **LLM** | Ollama API (qwen2.5-coder:7b-instruct, 128K контекст) |
|
||
| **Git** | simple-git |
|
||
| **File watching** | chokidar |
|
||
| **CLI** | Commander.js |
|
||
| **Тестирование** | Vitest |
|
||
| **Сборка** | tsup |
|
||
| **Gitignore парсинг** | ignore (npm) |
|
||
| **Подсчёт токенов** | Ollama API |
|
||
| **Хеширование файлов** | MD5 |
|
||
|
||
### Поддерживаемые языки
|
||
|
||
Фокус на TypeScript/JavaScript экосистеме:
|
||
- TypeScript (.ts, .tsx)
|
||
- JavaScript (.js, .jsx)
|
||
- JSON, YAML конфиги
|
||
|
||
### Структура проекта
|
||
|
||
```
|
||
src/
|
||
├── cli/ # Точка входа, CLI парсинг
|
||
│ └── index.ts
|
||
├── tui/ # Терминальный интерфейс
|
||
│ ├── App.tsx # Главный компонент (Ink)
|
||
│ ├── components/ # UI компоненты
|
||
│ │ ├── Chat.tsx
|
||
│ │ ├── StatusBar.tsx
|
||
│ │ ├── DiffView.tsx # Отображение изменений (inline highlights)
|
||
│ │ ├── ConfirmDialog.tsx # Подтверждение действий
|
||
│ │ └── Input.tsx
|
||
│ └── hooks/ # React hooks
|
||
├── core/ # Ядро системы
|
||
│ ├── orchestrator.ts # Оркестратор
|
||
│ ├── context.ts # Управление контекстом (ленивая загрузка)
|
||
│ ├── undo.ts # Стек отмены (5-10 изменений)
|
||
│ └── session.ts # Управление сессией
|
||
├── llm/ # Работа с моделью
|
||
│ ├── client.ts # Ollama клиент
|
||
│ ├── tools.ts # Определения тулов (18 штук)
|
||
│ ├── prompts.ts # Системные промпты
|
||
│ └── parser.ts # Парсинг ответов модели
|
||
├── indexer/ # Индексация проекта
|
||
│ ├── scanner.ts # Сканирование файлов
|
||
│ ├── parser.ts # AST парсинг (tree-sitter)
|
||
│ ├── analyzer.ts # Анализ связей
|
||
│ └── watcher.ts # Watchdog (chokidar)
|
||
├── redis/ # Работа с Redis
|
||
│ ├── client.ts # Подключение + AOF config
|
||
│ ├── schema.ts # Типы ключей (группировка по типу)
|
||
│ └── queries.ts # Запросы
|
||
├── tools/ # Реализация тулов
|
||
│ ├── read.ts # get_lines, get_function
|
||
│ ├── search.ts # find_references, find_definition
|
||
│ ├── edit.ts # edit_lines, create_file
|
||
│ ├── git.ts # git_status, git_diff, git_commit
|
||
│ ├── run.ts # run_command, run_tests
|
||
│ └── index.ts # Регистрация всех тулов
|
||
├── security/ # Безопасность
|
||
│ ├── blacklist.ts # Blacklist опасных команд
|
||
│ ├── whitelist.ts # Whitelist разрешённых команд
|
||
│ └── validator.ts # Валидация параметров
|
||
├── types/ # TypeScript типы
|
||
│ ├── ast.ts
|
||
│ ├── redis.ts
|
||
│ ├── tools.ts
|
||
│ └── session.ts
|
||
└── utils/ # Утилиты
|
||
├── hash.ts
|
||
└── tokens.ts # Подсчёт токенов
|
||
|
||
tests/
|
||
├── unit/
|
||
├── integration/
|
||
└── fixtures/
|
||
|
||
config/
|
||
├── default.json # Настройки по умолчанию
|
||
├── blacklist.json # Blacklist команд
|
||
└── whitelist.json # Whitelist команд (расширяемый)
|
||
```
|
||
|
||
---
|
||
|
||
## Архитектура
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Пользователь │
|
||
└─────────────────────────────┬───────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ TUI (Ink/React) │
|
||
│ Chat режим │
|
||
│ - подтверждение изменений (или auto-apply режим) │
|
||
│ - diff с inline highlights │
|
||
│ - статистика: токены, время, вызванные tools │
|
||
└─────────────────────────────┬───────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Оркестратор │
|
||
│ - начальный контекст: структура + AST (без кода) │
|
||
│ - код подгружается через tools по запросу модели │
|
||
│ - user choice при ошибках (retry/skip/abort) │
|
||
│ - авто-сжатие контекста при >80% │
|
||
│ - обработка конфликтов редактирования │
|
||
└───────────┬─────────────────┬───────────────────────────────┘
|
||
│ │
|
||
▼ ▼
|
||
┌───────────────────┐ ┌───────────────────────────────────────┐
|
||
│ qwen2.5-coder │ │ Redis + AOF │
|
||
│ 7B, 128K ctx │ │ │
|
||
│ │ │ - код файлов (lines) │
|
||
│ - основная │ │ - AST структуры │
|
||
│ - рефакторинг │ │ - метаданные и индексы │
|
||
│ - ревью │ │ - сессии (бессрочно) │
|
||
│ - tools │ │ │
|
||
└───────────────────┘ └───────────────────────────────────────┘
|
||
```
|
||
|
||
### Ключевые принципы
|
||
|
||
| Принцип | Реализация |
|
||
|---------|------------|
|
||
| **Одна модель** | qwen2.5-coder:7b-instruct для всех задач |
|
||
| **Ленивая загрузка кода** | Код НЕ в начальном контексте. Модель запрашивает через tools |
|
||
| **User choice** | При ошибке — выбор пользователя: retry/skip/abort |
|
||
| **Код в Redis** | Дублирование для быстрого доступа без I/O |
|
||
| **Подтверждение изменений** | По умолчанию спрашивать, опционально auto-apply |
|
||
| **Undo stack** | История 5-10 изменений для отката |
|
||
| **Без лимитов** | Нет ограничений на размер проекта и файлов |
|
||
| **Язык промптов** | Системный промпт на EN, ответы адаптируются к языку пользователя |
|
||
| **Монорепозитории** | Индексируется весь репозиторий |
|
||
| **Tool format** | XML в промпте (как Claude Code: `<tool_call>`) |
|
||
| **Encoding** | Только UTF-8, остальные пропускаются |
|
||
| **Symlinks** | Хранятся как метаданные (имя, target) |
|
||
|
||
---
|
||
|
||
## Redis: структура данных
|
||
|
||
### Принцип: группировка по типу
|
||
|
||
Вместо множества мелких ключей — несколько больших JSON-структур:
|
||
|
||
```
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# ПРОЕКТ (5 основных ключей)
|
||
# Имя проекта: {parent-folder}-{project-folder}
|
||
# Пример: projects-myapp
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
project:{name}:files # Hash: path → {lines[], hash, size, lastModified}
|
||
project:{name}:ast # Hash: path → {imports[], exports[], functions[], classes[], parseError?}
|
||
project:{name}:meta # Hash: path → {complexity, deps, dependents, isHub, isEntry}
|
||
project:{name}:indexes # Hash: symbols, deps_graph, call_graph
|
||
project:{name}:config # Hash: settings, whitelist, last_indexed
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# СЕССИИ (бессрочное хранение)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
session:{id}:data # Hash: history[], context, created_at, last_activity, stats
|
||
session:{id}:undo # List: стек изменений для отмены (5-10 элементов)
|
||
sessions:list # List: все session_id для проекта
|
||
```
|
||
|
||
### Итого ключей
|
||
|
||
| Категория | Ключей | Описание |
|
||
|-----------|--------|----------|
|
||
| Проект | 5 | files, ast, meta, indexes, config |
|
||
| Сессия | 3 на сессию | data + undo + общий list |
|
||
| **Всего** | ~10-20 | Компактная структура |
|
||
|
||
### Пример данных в project:{name}:files
|
||
|
||
```json
|
||
{
|
||
"src/auth/login.ts": {
|
||
"lines": ["import jwt from 'jsonwebtoken'", "import { User } from '../models'", "..."],
|
||
"hash": "a3f2b1c...",
|
||
"size": 2431,
|
||
"lastModified": 1705312200
|
||
}
|
||
}
|
||
```
|
||
|
||
### Пример данных в project:{name}:ast
|
||
|
||
```json
|
||
{
|
||
"src/auth/login.ts": {
|
||
"imports": [
|
||
{"name": "jwt", "from": "jsonwebtoken", "line": 1, "type": "external"},
|
||
{"name": "User", "from": "../models", "line": 2, "type": "internal"}
|
||
],
|
||
"exports": [
|
||
{"name": "AuthManager", "type": "class", "line": 10},
|
||
{"name": "refreshToken", "type": "function", "line": 55}
|
||
],
|
||
"functions": [
|
||
{"name": "refreshToken", "lineStart": 55, "lineEnd": 70, "params": ["token"], "isAsync": true}
|
||
],
|
||
"classes": [
|
||
{
|
||
"name": "AuthManager",
|
||
"lineStart": 10,
|
||
"lineEnd": 52,
|
||
"methods": [
|
||
{"name": "login", "lineStart": 15, "lineEnd": 30, "params": ["username", "password"]},
|
||
{"name": "validateToken", "lineStart": 32, "lineEnd": 50, "params": ["token"]}
|
||
]
|
||
}
|
||
],
|
||
"parseError": false
|
||
}
|
||
}
|
||
```
|
||
|
||
### Redis конфигурация (AOF)
|
||
|
||
```conf
|
||
# redis.conf
|
||
appendonly yes
|
||
appendfsync everysec
|
||
auto-aof-rewrite-percentage 100
|
||
auto-aof-rewrite-min-size 64mb
|
||
```
|
||
|
||
---
|
||
|
||
## Индексация проекта
|
||
|
||
### Первичная индексация
|
||
|
||
```
|
||
При первом запуске:
|
||
|
||
0. ПРОВЕРКА РАЗМЕРА
|
||
└→ Если >10K файлов → показать предупреждение
|
||
└→ Спросить: продолжить / выбрать поддиректорию / отмена
|
||
|
||
1. СКАНИРОВАНИЕ (с прогресс-баром)
|
||
└→ [=====> ] 45% (120/267 files)
|
||
└→ Обход файловой системы проекта
|
||
└→ Фильтрация: .gitignore, node_modules, dist, etc
|
||
└→ TypeScript/JavaScript файлы + JSON/YAML
|
||
└→ Только UTF-8 файлы (остальные пропускаются)
|
||
|
||
2. ПАРСИНГ (для каждого файла, без ограничений размера)
|
||
└→ Чтение файла → lines[] → Redis (project:{name}:files)
|
||
└→ Парсинг через tree-sitter → Redis (project:{name}:ast)
|
||
└→ При ошибке парсинга → parseError: true в AST
|
||
└→ Базовые метрики → Redis (project:{name}:meta)
|
||
|
||
3. ПОСТРОЕНИЕ ИНДЕКСОВ
|
||
└→ Граф импортов между файлами
|
||
└→ Индекс символов (функции, классы)
|
||
└→ Redis (project:{name}:indexes)
|
||
```
|
||
|
||
### Watchdog (chokidar)
|
||
|
||
```
|
||
Фоновый процесс отслеживает изменения:
|
||
|
||
1. chokidar.watch() на директорию проекта
|
||
2. Debounce: 500ms (группировка быстрых изменений)
|
||
3. При изменении файла:
|
||
└→ Пересчитать MD5 hash
|
||
└→ Если изменился:
|
||
└→ Тихо обновить lines в Redis
|
||
└→ Перепарсить AST через tree-sitter
|
||
└→ Обновить метрики
|
||
4. Модель получит свежие данные при следующем запросе через tools
|
||
|
||
Бинарные файлы (.png, .pdf, etc):
|
||
└→ Индексируются только метаданные (имя, размер, тип)
|
||
└→ Содержимое не хранится
|
||
```
|
||
|
||
---
|
||
|
||
## Tools модели (18 штук)
|
||
|
||
### Core Tools — Чтение
|
||
|
||
| Tool | Описание | Параметры |
|
||
|------|----------|-----------|
|
||
| `get_lines` | Получить строки кода | `path`, `start?`, `end?` |
|
||
| `get_function` | Получить функцию по имени | `path`, `name` |
|
||
| `get_class` | Получить класс по имени | `path`, `name` |
|
||
|
||
### Core Tools — Редактирование (требуют подтверждения или auto-apply)
|
||
|
||
| Tool | Описание | Параметры |
|
||
|------|----------|-----------|
|
||
| `edit_lines` | Заменить строки | `path`, `start`, `end`, `content` |
|
||
| `create_file` | Создать новый файл | `path`, `content` |
|
||
| `delete_file` | Удалить файл | `path` |
|
||
|
||
### Core Tools — Поиск и навигация
|
||
|
||
| Tool | Описание | Параметры |
|
||
|------|----------|-----------|
|
||
| `find_references` | Где используется символ | `symbol` |
|
||
| `find_definition` | Где определён символ | `symbol` |
|
||
| `get_structure` | Получить структуру проекта | `path?` |
|
||
|
||
### Core Tools — Анализ
|
||
|
||
| Tool | Описание | Параметры |
|
||
|------|----------|-----------|
|
||
| `get_dependencies` | От чего зависит файл | `path` |
|
||
| `get_dependents` | Что зависит от файла | `path` |
|
||
| `get_complexity` | Метрики сложности | `path?` |
|
||
| `get_todos` | TODO/FIXME в проекте | `path?` |
|
||
|
||
### Core Tools — Git и выполнение
|
||
|
||
| Tool | Описание | Параметры |
|
||
|------|----------|-----------|
|
||
| `git_status` | Статус репозитория | — |
|
||
| `git_diff` | Незакоммиченные изменения | `path?` |
|
||
| `git_commit` | Коммит изменений | `message`, `files?` |
|
||
| `run_command` | Shell команда (whitelist) | `command` |
|
||
| `run_tests` | Запустить тесты | `path?`, `filter?` |
|
||
|
||
### Итого: 18 tools
|
||
|
||
Модель получает структуру проекта и AST в начальном контексте, а код запрашивает через tools по мере необходимости.
|
||
|
||
---
|
||
|
||
## Безопасность
|
||
|
||
### Blacklist опасных команд
|
||
|
||
```json
|
||
{
|
||
"blacklist": [
|
||
"rm -rf",
|
||
"rm -r",
|
||
"git push --force",
|
||
"git reset --hard",
|
||
"git clean -fd",
|
||
"npm publish",
|
||
"sudo",
|
||
"chmod",
|
||
"chown"
|
||
]
|
||
}
|
||
```
|
||
|
||
### Whitelist команд для run_command
|
||
|
||
```json
|
||
{
|
||
"default": [
|
||
"npm", "pnpm", "yarn",
|
||
"git",
|
||
"node", "npx", "tsx",
|
||
"vitest", "jest",
|
||
"tsc", "eslint", "prettier"
|
||
],
|
||
"user": []
|
||
}
|
||
```
|
||
|
||
Пользователь может расширять whitelist через конфиг:
|
||
|
||
```json
|
||
{
|
||
"whitelist": {
|
||
"user": ["bun", "deno", "cargo"]
|
||
}
|
||
}
|
||
```
|
||
|
||
### Логика выполнения
|
||
|
||
```typescript
|
||
async function runCommand(command: string): Promise<ToolResult> {
|
||
const [cmd, ...args] = command.split(' ')
|
||
|
||
// 1. Проверка blacklist — безусловный отказ
|
||
if (isBlacklisted(command)) {
|
||
return { success: false, error: `Команда запрещена: ${command}` }
|
||
}
|
||
|
||
// 2. Проверка whitelist (default + user)
|
||
if (!isWhitelisted(cmd)) {
|
||
// 3. Спросить пользователя для неизвестных команд
|
||
const confirmed = await askUserConfirmation(
|
||
`Команда '${cmd}' не в whitelist. Выполнить?`
|
||
)
|
||
if (!confirmed) {
|
||
return { success: false, error: `Команда отклонена пользователем` }
|
||
}
|
||
}
|
||
|
||
// 4. Выполнение
|
||
return execute(command)
|
||
}
|
||
```
|
||
|
||
### Валидация параметров tools
|
||
|
||
```typescript
|
||
function validatePath(path: string): boolean {
|
||
// Запрет path traversal
|
||
if (path.includes('..')) return false
|
||
// Только внутри проекта
|
||
if (!path.startsWith(projectRoot)) return false
|
||
return true
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Управление контекстом
|
||
|
||
### Начальный контекст (при старте сессии)
|
||
|
||
```
|
||
ФИКСИРОВАННАЯ ЧАСТЬ:
|
||
├── Системный промпт с ролью и правилами
|
||
├── Список tools с описаниями
|
||
└── ВСЁ из Redis КРОМЕ кода:
|
||
├── Структура проекта (папки, файлы)
|
||
├── AST метаданные (imports, exports, функции, классы — БЕЗ тел)
|
||
├── Граф зависимостей между файлами
|
||
└── Метрики (complexity, hubs, entry points)
|
||
|
||
КОД НЕ ВКЛЮЧАЕТСЯ — модель запрашивает через tools по необходимости
|
||
```
|
||
|
||
### Бюджет токенов (128K окно)
|
||
|
||
| Компонент | Токены | % |
|
||
|-----------|--------|---|
|
||
| Системный промпт | ~2,000 | 1.5% |
|
||
| Структура + AST meta | ~10,000 | 8% |
|
||
| **Доступно для работы** | ~116,000 | 90% |
|
||
|
||
### Авто-сжатие контекста
|
||
|
||
```
|
||
При заполнении >80%:
|
||
|
||
1. Комбинированное сжатие:
|
||
└→ LLM суммаризирует старые сообщения
|
||
└→ Удаляются tool results (код из get_lines, etc.)
|
||
└→ Остаются ключевые решения и структура диалога
|
||
|
||
2. Удалить код файлов не упомянутых 5+ сообщений
|
||
└→ Можно запросить снова через tools
|
||
|
||
3. Уведомление: только в status bar (ctx% меняется)
|
||
└→ Нет модального уведомления
|
||
```
|
||
|
||
---
|
||
|
||
## Error Handling
|
||
|
||
### Принцип: User Choice
|
||
|
||
```typescript
|
||
interface ErrorResult {
|
||
type: 'redis' | 'parse' | 'llm' | 'file' | 'command' | 'conflict'
|
||
message: string
|
||
recoverable: boolean
|
||
}
|
||
|
||
interface UserChoice {
|
||
retry: boolean
|
||
skip: boolean
|
||
abort: boolean
|
||
}
|
||
|
||
async function handleError(error: ErrorResult): Promise<UserChoice> {
|
||
// 1. Логировать (локальная статистика)
|
||
stats.recordError(error)
|
||
|
||
// 2. Показать ошибку
|
||
ui.showError(`❌ ${error.type}: ${error.message}`)
|
||
|
||
// 3. Дать выбор пользователю (ВСЕГДА)
|
||
return ui.askChoice({
|
||
message: 'Что делать?',
|
||
options: [
|
||
{ key: 'r', label: 'Retry — попробовать снова' },
|
||
{ key: 's', label: 'Skip — пропустить и продолжить' },
|
||
{ key: 'a', label: 'Abort — остановить' }
|
||
]
|
||
})
|
||
}
|
||
```
|
||
|
||
### Обработка конфликтов редактирования
|
||
|
||
```typescript
|
||
async function handleEditConflict(
|
||
path: string,
|
||
expectedHash: string,
|
||
currentHash: string
|
||
): Promise<ConflictChoice> {
|
||
ui.showWarning(`⚠️ Файл ${path} был изменён во время генерации`)
|
||
|
||
return ui.askChoice({
|
||
message: 'Файл изменился. Что делать?',
|
||
options: [
|
||
{ key: 'a', label: 'Apply — применить поверх новой версии' },
|
||
{ key: 's', label: 'Skip — пропустить это изменение' },
|
||
{ key: 'r', label: 'Regenerate — попросить модель заново' }
|
||
]
|
||
})
|
||
}
|
||
```
|
||
|
||
### Типичные ошибки и варианты
|
||
|
||
| Ошибка | Варианты |
|
||
|--------|----------|
|
||
| Redis недоступен | Retry / Abort |
|
||
| AST парсинг упал | Skip файл (пометить parseError) / Abort |
|
||
| LLM timeout | Retry / Skip запрос / Abort |
|
||
| Файл не найден | Skip / Abort |
|
||
| Команда не в whitelist | Подтвердить / Skip / Abort |
|
||
| Конфликт редактирования | Apply / Skip / Regenerate |
|
||
|
||
---
|
||
|
||
## TUI Интерфейс
|
||
|
||
### Структура экрана
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ [ipuaro] [ctx: 12%] [project: projects-myapp] [47m] ✓ │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ User: объясни как работает авторизация │
|
||
│ │
|
||
│ Agent: [get_function src/auth/login.ts login] │
|
||
│ Авторизация построена на JWT... │
|
||
│ ⏱ 3.2s │ 1,247 tokens │ 1 tool call │
|
||
│ │
|
||
│ User: отрефактори функцию validateToken │
|
||
│ │
|
||
│ Agent: Предлагаю следующие изменения: │
|
||
│ │
|
||
│ ┌─── src/auth/login.ts (строки 32-50) ───┐ │
|
||
│ │ const isValid = jwt.verify(token) │ │
|
||
│ │ const isValid = this.verifyToken() │ ← изменено │
|
||
│ │ if (!isValid) throw new AuthError() │ ← добавлено │
|
||
│ └────────────────────────────────────────┘ │
|
||
│ │
|
||
│ Изменения: выделил валидацию, добавил проверку expiry │
|
||
│ │
|
||
│ ┌────────────────────────────────────────┐ │
|
||
│ │ [Y] Применить [N] Отменить [E] Edit │ │
|
||
│ └────────────────────────────────────────┘ │
|
||
│ │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ > _ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Команды (7 штук)
|
||
|
||
| Команда | Действие |
|
||
|---------|----------|
|
||
| `/help` | Показать справку |
|
||
| `/clear` | Очистить историю чата |
|
||
| `/undo` | Отменить изменение (стек 5-10) |
|
||
| `/sessions` | Управление сессиями (list, load, delete) |
|
||
| `/status` | Состояние системы (Redis, контекст, модель, статистика) |
|
||
| `/reindex` | Принудительная переиндексация проекта |
|
||
| `/eval` | Самопроверка модели на галлюцинации (LLM проверяет свой ответ) |
|
||
|
||
**Всё остальное — через естественный язык в чате.**
|
||
|
||
Примеры:
|
||
- "покажи все TODO" → модель вызывает `get_todos`
|
||
- "найди где используется AuthManager" → модель вызывает `find_references`
|
||
- "запусти тесты для auth" → модель вызывает `run_tests`
|
||
|
||
### Режим работы
|
||
|
||
**Только Chat.** Один режим для всех задач:
|
||
- Код-ревью
|
||
- Рефакторинг
|
||
- Написание тестов
|
||
- Документирование
|
||
- Навигация по коду
|
||
- Git операции
|
||
|
||
### Режим Auto-Apply
|
||
|
||
Опциональный режим для опытных пользователей:
|
||
|
||
```typescript
|
||
// В конфиге или через команду
|
||
{
|
||
"edit": {
|
||
"autoApply": false // по умолчанию выключен
|
||
}
|
||
}
|
||
|
||
// Включить на сессию
|
||
> /auto-apply on
|
||
|
||
// При включённом режиме изменения применяются автоматически
|
||
// Но всегда можно отменить через /undo
|
||
```
|
||
|
||
### Индикаторы статус-бара
|
||
|
||
| Индикатор | Описание |
|
||
|-----------|----------|
|
||
| `ctx: 12%` | Заполненность контекста |
|
||
| `project: projects-myapp` | Текущий проект (parent-folder) |
|
||
| `main` | Текущая git branch |
|
||
| `47m` | Время сессии |
|
||
| `✓` / `⟳` / `✗` | ready / thinking / error |
|
||
|
||
### Статистика ответа
|
||
|
||
После каждого ответа модели показывается:
|
||
- ⏱ Время генерации
|
||
- 📊 Количество токенов
|
||
- 🔧 Количество вызванных tools
|
||
|
||
### Горячие клавиши
|
||
|
||
| Клавиша | Действие |
|
||
|---------|----------|
|
||
| `Ctrl+C` | Прервать генерацию (1-й раз), выход (2-й раз) |
|
||
| `Ctrl+D` | Выход с автосохранением сессии |
|
||
| `Ctrl+Z` | Undo (= /undo) |
|
||
| `↑/↓` | История ввода |
|
||
| `Tab` | Автодополнение путей |
|
||
|
||
### Подтверждение изменений
|
||
|
||
При каждом edit_lines, create_file, delete_file (если не auto-apply):
|
||
|
||
1. **Diff с inline highlights** — подсветка изменённых символов
|
||
2. **Syntax highlighting** — полная подсветка синтаксиса в diff
|
||
3. **Описание** — краткое объяснение что изменится
|
||
4. **Длинные строки** — wrap в терминале (хранятся полностью)
|
||
5. **Выбор**:
|
||
- `[Y]` — применить изменение
|
||
- `[N]` — отменить
|
||
- `[E]` — редактировать предложенный код
|
||
|
||
---
|
||
|
||
## TypeScript типы
|
||
|
||
### Метаданные файла
|
||
|
||
```typescript
|
||
interface FileData {
|
||
lines: string[]
|
||
hash: string
|
||
size: number
|
||
lastModified: number
|
||
}
|
||
|
||
interface FileAST {
|
||
imports: ImportInfo[]
|
||
exports: ExportInfo[]
|
||
functions: FunctionInfo[]
|
||
classes: ClassInfo[]
|
||
parseError: boolean // true если tree-sitter не смог распарсить
|
||
}
|
||
|
||
interface FileMeta {
|
||
complexity: ComplexityScore // Простой скор: LOC + вложенность + imports
|
||
dependencies: string[]
|
||
dependents: string[]
|
||
isHub: boolean // Комбинированно: >5 dependents ИЛИ часто импортируется
|
||
isEntryPoint: boolean // Комбинированно: index.ts/main.ts ИЛИ 0 dependents
|
||
}
|
||
|
||
interface ComplexityScore {
|
||
loc: number // Lines of code
|
||
nesting: number // Макс. вложенность
|
||
imports: number // Количество imports
|
||
score: number // Общий скор
|
||
}
|
||
```
|
||
|
||
### AST структуры
|
||
|
||
```typescript
|
||
interface ImportInfo {
|
||
name: string
|
||
from: string
|
||
line: number
|
||
type: 'internal' | 'external' | 'builtin'
|
||
isDefault: boolean
|
||
}
|
||
|
||
interface ExportInfo {
|
||
name: string
|
||
type: 'function' | 'class' | 'interface' | 'type' | 'const'
|
||
line: number
|
||
}
|
||
|
||
interface FunctionInfo {
|
||
name: string
|
||
lineStart: number
|
||
lineEnd: number
|
||
params: string[]
|
||
returnType?: string
|
||
isAsync: boolean
|
||
isExported: boolean
|
||
}
|
||
|
||
interface ClassInfo {
|
||
name: string
|
||
lineStart: number
|
||
lineEnd: number
|
||
methods: MethodInfo[]
|
||
properties: PropertyInfo[]
|
||
extends?: string
|
||
implements: string[]
|
||
isExported: boolean
|
||
}
|
||
|
||
interface MethodInfo {
|
||
name: string
|
||
lineStart: number
|
||
lineEnd: number
|
||
params: string[]
|
||
visibility: 'public' | 'private' | 'protected'
|
||
isStatic: boolean
|
||
isAsync: boolean
|
||
}
|
||
|
||
interface PropertyInfo {
|
||
name: string
|
||
line: number
|
||
type?: string
|
||
visibility: 'public' | 'private' | 'protected'
|
||
isStatic: boolean
|
||
isReadonly: boolean
|
||
}
|
||
```
|
||
|
||
### Сессия
|
||
|
||
```typescript
|
||
interface Session {
|
||
id: string
|
||
projectName: string // формат: parent-folder
|
||
createdAt: number
|
||
lastActivityAt: number
|
||
history: ChatMessage[]
|
||
context: ContextState
|
||
undoStack: UndoEntry[]
|
||
stats: SessionStats
|
||
inputHistory: string[] // История ввода для ↑/↓ (хранится в Redis)
|
||
}
|
||
|
||
interface SessionStats {
|
||
totalTokens: number
|
||
totalTime: number
|
||
toolCalls: number
|
||
editsApplied: number
|
||
editsRejected: number
|
||
}
|
||
|
||
interface ChatMessage {
|
||
role: 'user' | 'assistant' | 'tool'
|
||
content: string
|
||
timestamp: number
|
||
toolCalls?: ToolCall[]
|
||
toolResults?: ToolResult[]
|
||
stats?: MessageStats
|
||
}
|
||
|
||
interface MessageStats {
|
||
tokens: number
|
||
timeMs: number
|
||
toolCalls: number
|
||
}
|
||
|
||
interface ContextState {
|
||
loadedFiles: string[] // Файлы загруженные в контекст
|
||
tokensUsed: number
|
||
tokensLimit: number // 128000
|
||
}
|
||
|
||
interface UndoEntry {
|
||
id: string
|
||
timestamp: number
|
||
filePath: string
|
||
previousContent: string[]
|
||
newContent: string[]
|
||
description: string
|
||
}
|
||
```
|
||
|
||
### Tools
|
||
|
||
```typescript
|
||
interface ToolDefinition {
|
||
name: string
|
||
description: string
|
||
parameters: ToolParameter[]
|
||
handler: (params: Record<string, unknown>) => Promise<ToolResult>
|
||
requiresConfirmation: boolean // true для edit/create/delete (если не auto-apply)
|
||
}
|
||
|
||
interface ToolParameter {
|
||
name: string
|
||
type: 'string' | 'number' | 'boolean'
|
||
description: string
|
||
required: boolean
|
||
default?: unknown
|
||
}
|
||
|
||
interface ToolCall {
|
||
id: string
|
||
name: string
|
||
parameters: Record<string, unknown>
|
||
}
|
||
|
||
interface ToolResult {
|
||
callId: string
|
||
success: boolean
|
||
data?: unknown
|
||
error?: string
|
||
}
|
||
```
|
||
|
||
### Error Handling
|
||
|
||
```typescript
|
||
interface ErrorResult {
|
||
type: 'redis' | 'parse' | 'llm' | 'file' | 'command' | 'conflict'
|
||
message: string
|
||
recoverable: boolean
|
||
suggestion?: string
|
||
}
|
||
|
||
type UserErrorChoice = 'retry' | 'skip' | 'abort'
|
||
type ConflictChoice = 'apply' | 'skip' | 'regenerate'
|
||
```
|
||
|
||
---
|
||
|
||
## Флоу работы
|
||
|
||
### Общий флоу
|
||
|
||
```
|
||
0. ONBOARDING (минимальный, при первом запуске)
|
||
└→ Проверка Redis: подключение → если нет, ошибка с инструкцией
|
||
└→ Проверка Ollama: доступность → если нет, ошибка и выход
|
||
└→ Проверка модели: qwen2.5-coder:7b-instruct → если нет, предложить скачать
|
||
|
||
1. ИНИЦИАЛИЗАЦИЯ
|
||
└→ Подключение к Redis
|
||
└→ Проверка проекта в Redis
|
||
└→ Если нет → первичная индексация (tree-sitter)
|
||
└→ Запуск watchdog (chokidar, debounce 500ms)
|
||
└→ Загрузка последней сессии или создание новой
|
||
|
||
2. ЗАПРОС ПОЛЬЗОВАТЕЛЯ
|
||
└→ Добавить в историю
|
||
└→ Сформировать контекст:
|
||
- Системный промпт
|
||
- Структура проекта + AST (без кода!)
|
||
- История (последние N сообщений)
|
||
└→ Отправить в модель
|
||
└→ Показать индикатор [⟳ thinking...]
|
||
|
||
3. ОБРАБОТКА ОТВЕТА (ждём полный ответ)
|
||
└→ Показать все tool calls по мере получения
|
||
└→ Если tool call (чтение):
|
||
└→ Валидация параметров
|
||
└→ Выполнение
|
||
└→ Результат обратно в модель
|
||
└→ Если tool call (редактирование):
|
||
└→ Проверить конфликт (hash изменился?)
|
||
└→ Если конфликт → спросить пользователя
|
||
└→ Показать diff с inline highlights + описание
|
||
└→ Если auto-apply → применить автоматически
|
||
└→ Иначе → СПРОСИТЬ подтверждение (Y/N/E)
|
||
└→ Если Y:
|
||
└→ Сохранить в undo stack
|
||
└→ Применить изменения
|
||
└→ Обновить Redis (lines, AST)
|
||
└→ Показать статистику: время, токены, tools
|
||
└→ Показать ответ пользователю
|
||
|
||
4. ОШИБКА → User Choice (всегда)
|
||
└→ Показать ошибку
|
||
└→ Спросить: Retry / Skip / Abort
|
||
└→ Выполнить выбор пользователя
|
||
|
||
5. ПОВТОРИТЬ с шага 2
|
||
```
|
||
|
||
### Флоу код-ревью (через чат)
|
||
|
||
```
|
||
User: "сделай ревью src/auth/login.ts"
|
||
|
||
Agent:
|
||
1. get_lines(src/auth/login.ts) → получить код
|
||
2. get_dependencies(src/auth/login.ts) → понять контекст
|
||
3. get_todos(src/auth/login.ts) → найти TODO
|
||
4. Анализ:
|
||
- Баги и edge cases
|
||
- Стиль кода
|
||
- Security issues
|
||
- Performance issues
|
||
5. Вывод: список замечаний с номерами строк
|
||
6. Статистика: ⏱ 5.2s │ 2,847 tokens │ 3 tool calls
|
||
```
|
||
|
||
### Флоу рефакторинга (через чат)
|
||
|
||
```
|
||
User: "отрефактори функцию validateToken"
|
||
|
||
Agent:
|
||
1. find_definition(validateToken) → найти где
|
||
2. get_function(path, validateToken) → получить код
|
||
3. find_references(validateToken) → где используется
|
||
4. Планирование изменений
|
||
5. Проверить не изменился ли файл
|
||
6. Показать diff с inline highlights + описание
|
||
7. Спросить подтверждение [Y/N/E] (или auto-apply)
|
||
8. Если Y: edit_lines(...) → применить
|
||
9. Добавить в undo stack
|
||
10. Статистика ответа
|
||
```
|
||
|
||
### Флоу написания тестов (через чат)
|
||
|
||
```
|
||
User: "напиши тесты для AuthManager.login"
|
||
|
||
Agent:
|
||
1. get_class(src/auth/login.ts, AuthManager) → получить класс
|
||
2. get_dependencies(src/auth/login.ts) → что мокать
|
||
3. Генерация тестов:
|
||
- Happy path
|
||
- Edge cases
|
||
- Error cases
|
||
4. Показать diff для нового файла
|
||
5. Спросить подтверждение [Y/N/E]
|
||
6. Если Y: create_file(tests/auth/login.test.ts, content)
|
||
7. run_tests(tests/auth/login.test.ts) → проверить
|
||
```
|
||
|
||
---
|
||
|
||
## Что НЕ делаем
|
||
|
||
| Отвергнуто | Причина |
|
||
|------------|---------|
|
||
| RAG с векторной БД | Оверкилл, tree-sitter + Redis достаточно |
|
||
| Вторая модель для ревью | Overhead, одна модель справляется |
|
||
| TypeScript Compiler API | tree-sitter легче и мультиязычнее |
|
||
| Regex поиск по коду | Модель сама запрашивает нужный код через tools |
|
||
| Understanding через LLM | Модель понимает код при чтении через tools |
|
||
| Множество режимов работы | Один Chat режим покрывает всё |
|
||
| Сложная структура Redis | Группировка по типу проще |
|
||
| Python/Go/Rust поддержка | Фокус на TypeScript/JS |
|
||
| Автоматическое восстановление | User choice надёжнее |
|
||
| Стриминг по токенам | Ждём полный ответ + статистика |
|
||
| Внешние API интеграции | Только TUI, монолит |
|
||
| Плагины/расширения | Всё встроено |
|
||
| Файловые логи | Только локальная статистика |
|
||
| Миграции данных | При несовместимости — /reindex |
|
||
|
||
---
|
||
|
||
## Конфигурация
|
||
|
||
### config/default.json
|
||
|
||
```json
|
||
{
|
||
"redis": {
|
||
"host": "localhost",
|
||
"port": 6379,
|
||
"db": 0
|
||
},
|
||
"llm": {
|
||
"provider": "ollama",
|
||
"model": "qwen2.5-coder:7b-instruct",
|
||
"contextWindow": 128000,
|
||
"temperature": 0.1,
|
||
"systemPromptLanguage": "en"
|
||
},
|
||
"project": {
|
||
"scope": "repository",
|
||
"ignorePatterns": [
|
||
"node_modules",
|
||
"dist",
|
||
"build",
|
||
".git",
|
||
"*.min.js"
|
||
],
|
||
"binaryExtensions": [".png", ".jpg", ".pdf", ".zip", ".woff"]
|
||
},
|
||
"session": {
|
||
"persistIndefinitely": true,
|
||
"maxHistoryMessages": 100,
|
||
"saveInputHistory": true
|
||
},
|
||
"context": {
|
||
"systemPromptTokens": 2000,
|
||
"maxContextUsage": 0.8,
|
||
"autoCompressAt": 0.8,
|
||
"compressionMethod": "llm-summary"
|
||
},
|
||
"watchdog": {
|
||
"debounceMs": 500
|
||
},
|
||
"commands": {
|
||
"timeout": null
|
||
},
|
||
"undo": {
|
||
"stackSize": 10
|
||
},
|
||
"edit": {
|
||
"alwaysConfirm": true,
|
||
"autoApply": false,
|
||
"showDiff": true,
|
||
"diffFormat": "inline-highlights",
|
||
"syntaxHighlight": true,
|
||
"showDescription": true,
|
||
"wrapLongLines": true
|
||
},
|
||
"display": {
|
||
"showStats": true,
|
||
"showToolCalls": true,
|
||
"theme": "dark",
|
||
"bellOnComplete": true,
|
||
"progressBar": true
|
||
},
|
||
"autocomplete": {
|
||
"source": "redis-index"
|
||
},
|
||
"input": {
|
||
"multiline": "auto"
|
||
},
|
||
"llm": {
|
||
"toolFormat": "xml",
|
||
"autoRetry": false,
|
||
"cache": false
|
||
},
|
||
"limits": {
|
||
"maxFilesWarning": 10000,
|
||
"encoding": "utf-8"
|
||
}
|
||
}
|
||
```
|
||
|
||
### config/blacklist.json
|
||
|
||
```json
|
||
{
|
||
"commands": [
|
||
"rm -rf",
|
||
"rm -r",
|
||
"git push --force",
|
||
"git reset --hard",
|
||
"git clean -fd",
|
||
"npm publish",
|
||
"sudo",
|
||
"chmod",
|
||
"chown"
|
||
]
|
||
}
|
||
```
|
||
|
||
### config/whitelist.json
|
||
|
||
```json
|
||
{
|
||
"default": [
|
||
"npm",
|
||
"pnpm",
|
||
"yarn",
|
||
"git",
|
||
"node",
|
||
"npx",
|
||
"tsx",
|
||
"vitest",
|
||
"jest",
|
||
"tsc",
|
||
"eslint",
|
||
"prettier"
|
||
],
|
||
"user": [],
|
||
"askForUnknown": true
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Метрики успеха
|
||
|
||
| Метрика | Цель |
|
||
|---------|------|
|
||
| Время первой индексации | Зависит от размера проекта |
|
||
| Время ответа модели | Стриминг, показывать статистику |
|
||
| Использование памяти Redis | Зависит от проекта |
|
||
| Точность AST парсинга (tree-sitter) | > 99% для TypeScript |
|
||
| Время синхронизации watchdog | < 1 секунда |
|
||
| Локальная статистика | Токены, время, tools за сессию |
|