zames_pro 2.65.1 → 2.66.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +29 -1
- package/README.ru.md +502 -0
- package/dist/index.js +63 -43
- package/dist/system-prompt.js +12 -3
- package/package.json +13 -4
- package/scripts/release.mts +157 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.66.0] - 2026-10-07
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **release:** add scripts/release.mts for one-command release prep (G4)
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **reload:** include commands, run-task and attach-refs in hot-reload (G2)
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- apply prettier to files touched by G2/F1
|
|
23
|
+
|
|
10
24
|
## [2.65.1] - 2026-10-07
|
|
11
25
|
|
|
12
26
|
### Fixed
|
package/README.md
CHANGED
|
@@ -14,6 +14,12 @@
|
|
|
14
14
|
<a href="https://github.com/Viqto0r/zames_pro/actions/workflows/test.yml"><img src="https://github.com/Viqto0r/zames_pro/actions/workflows/test.yml/badge.svg" alt="tests"></a>
|
|
15
15
|
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
|
|
16
16
|
<a href="package.json"><img src="https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg" alt="Node.js"></a>
|
|
17
|
+
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-strict-3178c6.svg" alt="TypeScript"></a>
|
|
18
|
+
<a href="CONTRIBUTING.md"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs welcome"></a>
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
<p align="center">
|
|
22
|
+
<strong>English</strong> | <a href="https://github.com/Viqto0r/zames_pro/blob/master/README.ru.md">Русский</a>
|
|
17
23
|
</p>
|
|
18
24
|
|
|
19
25
|
A terminal coding agent that works on top of [chat.deepseek.com](https://chat.deepseek.com/) through Playwright.
|
|
@@ -27,6 +33,28 @@ directory, reads and edits files, runs commands, and commits to git.
|
|
|
27
33
|
<img src="https://raw.githubusercontent.com/Viqto0r/zames_pro/master/docs/demo.png" alt="zames session in the terminal" width="860">
|
|
28
34
|
</p>
|
|
29
35
|
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install -g zames_pro # Chromium for Playwright is downloaded automatically
|
|
40
|
+
cd your-project # any folder the agent should work in
|
|
41
|
+
zames # sign in once, then describe your task
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
There is **no API key and no per-token bill**: zames signs in to **your own
|
|
45
|
+
[chat.deepseek.com](https://chat.deepseek.com/) account** in a real (headless)
|
|
46
|
+
Chromium and drives the web chat like a regular user. The first launch asks for
|
|
47
|
+
your DeepSeek login once (the session is stored in `~/.zames/profile`); after
|
|
48
|
+
that just run `zames` and type a task in plain language:
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
❯ refactor the config loader and add a test for the new default
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
While the agent works you can keep typing — a message sent mid-task is queued
|
|
55
|
+
and runs right after it, in the same chat. `Esc` aborts the current generation,
|
|
56
|
+
`/help` lists commands, `/exit` quits.
|
|
57
|
+
|
|
30
58
|
## How it works
|
|
31
59
|
|
|
32
60
|
zames does not call the model API. It launches a headless Chromium with a
|
|
@@ -54,7 +82,7 @@ Key pieces:
|
|
|
54
82
|
|
|
55
83
|
## Table of contents
|
|
56
84
|
|
|
57
|
-
- [Features](#features) · [Why zames?](#why-zames) · [Requirements](#requirements)
|
|
85
|
+
- [Quick start](#quick-start) · [Features](#features) · [Why zames?](#why-zames) · [Requirements](#requirements)
|
|
58
86
|
- [How it works](#how-it-works) · [Installation](#installation) · [Signing in](#signing-in) · [Usage](#usage)
|
|
59
87
|
- [Tools](#tools) · [Slash commands](#slash-commands)
|
|
60
88
|
- [Project context, skills and memory](#project-context-skills-and-memory) · [MCP (external tools)](#mcp-external-tools) · [Configuration](#configuration)
|
package/README.ru.md
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/Viqto0r/zames_pro/master/logo-small.jpg" alt="zames logo" width="430">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">zames_pro</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>Терминальный coding-агент, который управляет chat.deepseek.com через Playwright — API-ключ не нужен.</strong>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://www.npmjs.com/package/zames_pro"><img src="https://img.shields.io/npm/v/zames_pro.svg" alt="npm version"></a>
|
|
13
|
+
<a href="https://www.npmjs.com/package/zames_pro"><img src="https://img.shields.io/npm/dm/zames_pro.svg" alt="npm downloads"></a>
|
|
14
|
+
<a href="https://github.com/Viqto0r/zames_pro/actions/workflows/test.yml"><img src="https://github.com/Viqto0r/zames_pro/actions/workflows/test.yml/badge.svg" alt="tests"></a>
|
|
15
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
|
|
16
|
+
<a href="package.json"><img src="https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg" alt="Node.js"></a>
|
|
17
|
+
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-strict-3178c6.svg" alt="TypeScript"></a>
|
|
18
|
+
<a href="CONTRIBUTING.md"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs welcome"></a>
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
<p align="center">
|
|
22
|
+
<a href="https://github.com/Viqto0r/zames_pro/blob/master/README.md">English</a> | <strong>Русский</strong>
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
Терминальный coding-агент, работающий поверх [chat.deepseek.com](https://chat.deepseek.com/) через Playwright.
|
|
26
|
+
По духу он похож на Claude Code / Codex CLI: запускается в текущей
|
|
27
|
+
директории, читает и правит файлы, выполняет команды и коммитит в git.
|
|
28
|
+
|
|
29
|
+
> API-ключ не нужен — агент управляет веб-чатом DeepSeek как обычный
|
|
30
|
+
> пользователь через настоящий (headless) браузер.
|
|
31
|
+
|
|
32
|
+
<p align="center">
|
|
33
|
+
<img src="https://raw.githubusercontent.com/Viqto0r/zames_pro/master/docs/demo.png" alt="сессия zames в терминале" width="860">
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
## Быстрый старт
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install -g zames_pro # Chromium для Playwright скачается автоматически
|
|
40
|
+
cd your-project # любая папка, в которой должен работать агент
|
|
41
|
+
zames # один раз войти, дальше просто описывайте задачу
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Здесь **нет API-ключа и нет оплаты за токены**: zames входит в **ваш
|
|
45
|
+
собственный аккаунт [chat.deepseek.com](https://chat.deepseek.com/)** в
|
|
46
|
+
настоящем (headless) Chromium и управляет веб-чатом как обычный пользователь.
|
|
47
|
+
При первом запуске он один раз спросит логин DeepSeek (сессия сохраняется в
|
|
48
|
+
`~/.zames/profile`); после этого достаточно запустить `zames` и написать
|
|
49
|
+
задачу обычным языком:
|
|
50
|
+
|
|
51
|
+
```text
|
|
52
|
+
❯ отрефактори загрузчик конфига и добавь тест на новый дефолт
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Пока агент работает, можно продолжать печатать — сообщение, отправленное
|
|
56
|
+
посреди задачи, встанет в очередь и уйдёт сразу после неё, в тот же чат.
|
|
57
|
+
`Esc` прерывает текущую генерацию, `/help` показывает команды, `/exit` — выход.
|
|
58
|
+
|
|
59
|
+
## Как это работает
|
|
60
|
+
|
|
61
|
+
zames не вызывает API модели. Он запускает headless Chromium с постоянным
|
|
62
|
+
профилем, входит в `chat.deepseek.com` как человек, печатает задачу в поле
|
|
63
|
+
чата и читает ответ обратно.
|
|
64
|
+
|
|
65
|
+
<p align="center">
|
|
66
|
+
<img src="https://raw.githubusercontent.com/Viqto0r/zames_pro/master/docs/how-it-works.png" alt="как работает zames" width="860">
|
|
67
|
+
</p>
|
|
68
|
+
|
|
69
|
+
Словами: вы вводите задачу → zames отправляет системный промпт и задачу в
|
|
70
|
+
веб-чат DeepSeek → модель отвечает → zames разбирает вызов инструмента,
|
|
71
|
+
выполняет его локально, возвращает результат обратно и печатает финальный
|
|
72
|
+
ответ.
|
|
73
|
+
|
|
74
|
+
Ключевые детали:
|
|
75
|
+
|
|
76
|
+
- **Ответ читается из сырого сетевого потока** (SSE), а не из отрендеренного
|
|
77
|
+
DOM, поэтому JSON вызова инструмента с шаблонными строками и экранированием
|
|
78
|
+
остаётся целым.
|
|
79
|
+
- **Постоянный профиль** (`~/.zames/profile`) держит вас залогиненным;
|
|
80
|
+
User-Agent headless-браузера подменяется, чтобы CDN DeepSeek не отдавал 403
|
|
81
|
+
на вход.
|
|
82
|
+
- **Троттлинг отправки** (15 с по умолчанию) щадит рейт-лимит веб-чата во
|
|
83
|
+
время долгих прогонов с инструментами.
|
|
84
|
+
- **Агент запесочен** в директорию запуска — ни один инструмент не может
|
|
85
|
+
читать или писать выше неё.
|
|
86
|
+
|
|
87
|
+
## Содержание
|
|
88
|
+
|
|
89
|
+
- [Быстрый старт](#быстрый-старт) · [Возможности](#возможности) · [Почему zames?](#почему-zames) · [Требования](#требования)
|
|
90
|
+
- [Как это работает](#как-это-работает) · [Установка](#установка) · [Вход в аккаунт](#вход-в-аккаунт) · [Использование](#использование)
|
|
91
|
+
- [Инструменты](#инструменты) · [Слэш-команды](#слэш-команды)
|
|
92
|
+
- [Контекст проекта, навыки и память](#контекст-проекта-навыки-и-память) · [MCP (внешние инструменты)](#mcp-внешние-инструменты) · [Конфигурация](#конфигурация)
|
|
93
|
+
- [FAQ](#faq) · [Ссылки](#ссылки) · [Лицензия](#лицензия)
|
|
94
|
+
|
|
95
|
+
## Возможности
|
|
96
|
+
|
|
97
|
+
- **Инструменты как в Claude Code / Codex** — `Read`, `Write`, `Edit`, `Bash`,
|
|
98
|
+
`Glob`, `Grep`, плюс `MultiEdit`, `ApplyPatch`, `LS`, `TodoWrite`, git- и
|
|
99
|
+
веб-инструменты. Каждая правка подкреплена `/undo`.
|
|
100
|
+
- **Работает, пока вы печатаете** — сообщения, набранные во время задачи,
|
|
101
|
+
встают в очередь и уходят сразу после неё, в тот же чат (как набор текста
|
|
102
|
+
во время генерации на сайте).
|
|
103
|
+
- **Режим плана** — `/plan` (или `--plan`) убирает все мутирующие инструменты,
|
|
104
|
+
чтобы агент изучал код, не трогая дерево.
|
|
105
|
+
- **Контекст проекта** — читает `AGENTS.md`, `MEMORY.md`, навыки (`SKILL.md`) и
|
|
106
|
+
пользовательские команды из репозитория и `~/.zames`, как в Codex / Claude
|
|
107
|
+
Code.
|
|
108
|
+
- **Поддержка MCP** — подключение внешних серверов инструментов (например,
|
|
109
|
+
`@playwright/mcp`).
|
|
110
|
+
- **Планировщик** — `/loop`, `/cron` и `/jobs` повторяют задачи по таймеру.
|
|
111
|
+
- **Двуязычный интерфейс** — русский / английский (`/config lang`).
|
|
112
|
+
|
|
113
|
+
## Почему zames?
|
|
114
|
+
|
|
115
|
+
- **Нет API-ключа и счёта за токены** — используется ваш собственный аккаунт
|
|
116
|
+
чата DeepSeek, а не платный API. Удобно для долгих задач с инструментами.
|
|
117
|
+
- **Тот же рабочий процесс, что в Claude Code / Codex** — инструменты,
|
|
118
|
+
`AGENTS.md`, навыки, MCP и слэш-команды, так что всё знакомо с первого дня.
|
|
119
|
+
- **Работает без присмотра** — headless по умолчанию, возобновляемые сессии,
|
|
120
|
+
`/loop` и `/cron` для задач по расписанию.
|
|
121
|
+
- **Local-first** — профиль браузера, учётные данные и логи не покидают вашу
|
|
122
|
+
машину, а агент запесочен в директорию проекта.
|
|
123
|
+
|
|
124
|
+
## Требования
|
|
125
|
+
|
|
126
|
+
- Node.js >= 20
|
|
127
|
+
- Аккаунт DeepSeek. При первом запуске zames спросит логин/пароль DeepSeek в
|
|
128
|
+
терминале (и сохранит их в `~/.zames/config.json` после успешного входа,
|
|
129
|
+
поэтому более поздний разлогин обрабатывается автоматически, без повторных
|
|
130
|
+
вопросов). Можно также войти вручную в окне браузера, когда он headed.
|
|
131
|
+
|
|
132
|
+
## Вход в аккаунт
|
|
133
|
+
|
|
134
|
+
Браузер по умолчанию работает **headless**. Когда DeepSeek требует входа,
|
|
135
|
+
zames:
|
|
136
|
+
|
|
137
|
+
1. переиспользует сессию из постоянного профиля (`~/.zames/profile`), если она
|
|
138
|
+
ещё валидна;
|
|
139
|
+
2. иначе входит автоматически по сохранённым учётным данным
|
|
140
|
+
(`browser.auth.username` / `browser.auth.password`);
|
|
141
|
+
3. иначе спрашивает логин/пароль в терминале (в TTY) и после успешного входа
|
|
142
|
+
запоминает их для следующего раза;
|
|
143
|
+
4. иначе показывает подсказку про ручной вход.
|
|
144
|
+
|
|
145
|
+
Чтобы войти руками (например, если DeepSeek показывает капчу), запустите с
|
|
146
|
+
видимым окном:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
zames --headed
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Флаг `--headless` (значение по умолчанию) и `headless: true` в конфиге держат
|
|
153
|
+
браузер без окна; `--headed` / `headless: false` показывают его.
|
|
154
|
+
|
|
155
|
+
Headless работает из коробки: headless Chrome обычно представляется как
|
|
156
|
+
`HeadlessChrome/...`, и CDN DeepSeek блокирует такой User-Agent ответом 403,
|
|
157
|
+
поэтому zames срезает этот маркер перед загрузкой страницы (сохраняя реальную
|
|
158
|
+
версию движка). `--headed` нужен не только ради входа.
|
|
159
|
+
|
|
160
|
+
Учётные данные и переключатели можно также править из `/config`
|
|
161
|
+
(`browser.auth.username`, `browser.auth.password`, `browser.auth.saveSession`).
|
|
162
|
+
|
|
163
|
+
## Установка
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
npm install -g zames_pro
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Chromium для Playwright скачивается автоматически при установке. На Linux/WSL
|
|
170
|
+
необходимые системные библиотеки тоже ставятся, если доступен `sudo` без
|
|
171
|
+
пароля; иначе выполните один раз вручную:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
npx playwright install chromium
|
|
175
|
+
sudo npx playwright install-deps chromium
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Использование
|
|
179
|
+
|
|
180
|
+
Перейдите в папку проекта и запустите:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
zames
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Агент работает внутри директории, из которой он запущен, и не может её покинуть (песочница).
|
|
187
|
+
|
|
188
|
+
Пока агент работает, можно продолжать печатать: Enter ставит сообщение в
|
|
189
|
+
очередь (оно уйдёт сразу после текущей задачи, в тот же чат), а Esc / Ctrl+C
|
|
190
|
+
прерывают текущую генерацию. Это повторяет набор текста во время генерации на
|
|
191
|
+
сайте DeepSeek.
|
|
192
|
+
|
|
193
|
+
### Строка ввода
|
|
194
|
+
|
|
195
|
+
Промпт — это небольшой редактор строки с постоянной историей:
|
|
196
|
+
|
|
197
|
+
- `↑` / `↓` — переход по истории сообщений (сохраняется в
|
|
198
|
+
`~/.zames/history.json`, поэтому переживает перезапуск); внутри
|
|
199
|
+
многострочного сообщения стрелки двигают по строкам.
|
|
200
|
+
- `Ctrl+R` — инкрементальный поиск по истории в обратном порядке (как в bash):
|
|
201
|
+
печатайте для фильтра, `Ctrl+R` — за более старыми совпадениями, `Enter` —
|
|
202
|
+
принять, `Esc` — отменить.
|
|
203
|
+
- `Ctrl+_` — отменить последнюю правку строки ввода (случайно нажатые
|
|
204
|
+
`Ctrl+U` / `Ctrl+K` можно вернуть).
|
|
205
|
+
- `Ctrl+U` — очистить строку, `Ctrl+K` — удалить до конца строки, `Ctrl+W` —
|
|
206
|
+
удалить слово перед курсором, `Ctrl+←`/`Ctrl+→` — переход по словам.
|
|
207
|
+
- `\` + `Enter`, `Ctrl+J`, `Ctrl+Enter` или `Shift+Enter` — вставить новую
|
|
208
|
+
строку.
|
|
209
|
+
- `/` + `Tab` — подсказки и автодополнение слэш-команд.
|
|
210
|
+
- `!команда` — выполнить shell-команду напрямую, минуя модель (как bash-режим
|
|
211
|
+
Claude Code). Действует та же песочница, что и для инструмента `Bash`, так
|
|
212
|
+
что прямая команда тоже не может выйти за пределы проекта.
|
|
213
|
+
|
|
214
|
+
Установите `NO_COLOR=1`, чтобы отключить цвета (иначе используется спокойная
|
|
215
|
+
палитра по умолчанию).
|
|
216
|
+
|
|
217
|
+
### Картинки и файлы
|
|
218
|
+
|
|
219
|
+
В строку ввода можно вставить картинку или файл (Ctrl+Shift+V / Shift+Insert
|
|
220
|
+
или собственной вставкой терминала). zames сохраняет его в `<project>/tmp` и
|
|
221
|
+
показывает маркер в строке — `[image#1]` для картинок, `[file#1]` для прочих
|
|
222
|
+
файлов — затем загружает реальный файл в чат вместе с вашим сообщением. Это те
|
|
223
|
+
же форматы, что принимает веб-чат DeepSeek (PNG, JPEG, GIF, WEBP, BMP, SVG и
|
|
224
|
+
обычные типы документов).
|
|
225
|
+
|
|
226
|
+
Вставка картинки работает, когда терминал передаёт её как `data:` URL или как
|
|
227
|
+
base64-блок с узнаваемой сигнатурой изображения. Можно также вставить путь к
|
|
228
|
+
локальному файлу (перетащите файл в терминал или скопируйте его путь); если
|
|
229
|
+
файл существует — он прикрепляется, иначе текст вставляется как обычно.
|
|
230
|
+
|
|
231
|
+
Нажмите Ctrl+V (или сделайте пустую вставку), и zames прочитает картинку прямо
|
|
232
|
+
из буфера обмена: Linux через wl-paste (Wayland) или xclip/xsel (X11), macOS
|
|
233
|
+
через pngpaste, Windows через PowerShell. Нужные Linux-утилиты ставятся
|
|
234
|
+
автоматически при `npm install` (best-effort, через определённый пакетный
|
|
235
|
+
менеджер). Если картинка не найдена, zames сообщит об этом и напишет, какой
|
|
236
|
+
инструмент пробовал, вместо молчания.
|
|
237
|
+
|
|
238
|
+
### Опции
|
|
239
|
+
|
|
240
|
+
```
|
|
241
|
+
zames --task <текст задачи>
|
|
242
|
+
zames --chat <id>
|
|
243
|
+
zames --new-chat
|
|
244
|
+
zames --resend-prompt
|
|
245
|
+
zames --dir <path>
|
|
246
|
+
zames --headless
|
|
247
|
+
zames --headed
|
|
248
|
+
zames --plan
|
|
249
|
+
zames --output-format jsonl
|
|
250
|
+
zames --debug
|
|
251
|
+
zames --version
|
|
252
|
+
zames --help
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Машиночитаемый вывод (скрипты / CI)
|
|
256
|
+
|
|
257
|
+
Для одноразовых запусков (`--task`) агент может выдавать поток событий как JSON
|
|
258
|
+
lines в stdout, оставляя весь человеческий текст в stderr:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
zames --task "summarize the diff" --output-format jsonl
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Каждая строка — одно событие (`tool_call`, `tool_result`, `assistant_final`, …),
|
|
265
|
+
так что это сразу пайпится в `jq`:
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
zames --task "..." --output-format jsonl \
|
|
269
|
+
| jq -r 'select(.type=="assistant_final") | .message'
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Инструменты
|
|
273
|
+
|
|
274
|
+
У агента тот же стиль инструментов, что в Claude Code / Codex CLI:
|
|
275
|
+
|
|
276
|
+
- **Файловые** — `Read`, `Write`, `Edit`, `Bash`, `Glob`, `Grep`. `Read` по
|
|
277
|
+
умолчанию отдаёт сырое содержимое; передайте `numbered=true`, чтобы получить
|
|
278
|
+
нумерацию строк в стиле `cat -n` (только для справки — не вставляйте её в
|
|
279
|
+
`Edit`).
|
|
280
|
+
- **Дополнительные** — `LS` (список директории), `MultiEdit` (несколько правок
|
|
281
|
+
одного файла атомарно), `TodoWrite` (чек-лист задач сессии), `ApplyPatch`
|
|
282
|
+
(мультифайловый патч в формате V4A от Codex: `*** Begin Patch` … `*** End Patch`).
|
|
283
|
+
- **Git** — `GitStatus`, `GitDiff`, `GitLog`, `GitShow`, `GitBranchList`,
|
|
284
|
+
`GitAdd`, `GitCommit`, `GitPush`.
|
|
285
|
+
- **Web** — `WebFetch`, `WebSearch`.
|
|
286
|
+
- **Служебный** — `respond` (финальный ответ оператору, завершает задачу).
|
|
287
|
+
|
|
288
|
+
Все файловые инструменты остаются внутри рабочей директории (песочница).
|
|
289
|
+
`Write`/`Edit` и `MultiEdit`/`ApplyPatch` делают резервную копию (undo) перед
|
|
290
|
+
изменением файла.
|
|
291
|
+
|
|
292
|
+
## Слэш-команды
|
|
293
|
+
|
|
294
|
+
Введите / в промпте для подсказок (Tab дополняет). Кроме команд сессии и
|
|
295
|
+
конфига (/new, /chats, /resume, /cd, /status, /config, /undo, /transcript,
|
|
296
|
+
/mcp, /skills, /memory, /init, /reload, /debug-dom, /help, /exit) есть
|
|
297
|
+
несколько в духе Claude Code / Codex CLI:
|
|
298
|
+
|
|
299
|
+
- /diff [--staged] — показать git-диф рабочего дерева (--staged для индекса).
|
|
300
|
+
- /diffstat — сводка изменений одной строкой (`git diff --stat`).
|
|
301
|
+
- /context — показать, что загружено в промпт (AGENTS.md, MEMORY.md, навыки,
|
|
302
|
+
пользовательские команды) и размер системного промпта.
|
|
303
|
+
- /retry — отправить последнюю задачу в тот же чат заново (удобно после
|
|
304
|
+
обрезанного или пустого ответа).
|
|
305
|
+
- /rename <title> — задать заголовок текущей сессии (виден в /sessions).
|
|
306
|
+
- /copy — скопировать последний ответ ассистента в буфер обмена ОС.
|
|
307
|
+
- /cost (алиас /usage) — статистика сессии: задачи, вызовы инструментов,
|
|
308
|
+
длительность и размер контекста в токенах (DeepSeek
|
|
309
|
+
`accumulated_token_usage`).
|
|
310
|
+
- /export [file] — записать транскрипт сессии в Markdown-файл
|
|
311
|
+
(по умолчанию zames-export-<stamp>.md).
|
|
312
|
+
- /doctor — диагностика node, git, конфига, браузера, буфера обмена и MCP.
|
|
313
|
+
- /add-dir <path> — проверить дополнительную директорию (песочница
|
|
314
|
+
фиксируется при старте; для записи туда перезапустите с --dir).
|
|
315
|
+
- /resume <n> (после /chats) и /resume-id <id> — открыть чат и НАПЕЧАТАТЬ его
|
|
316
|
+
диалог в терминал, чтобы восстановленный контекст был виден. Показываются
|
|
317
|
+
только последние 20 сообщений (`RESTORED_HISTORY_LIMIT`). /last открывает
|
|
318
|
+
последний чат текущей директории без поиска; /sessions показывает сохранённые
|
|
319
|
+
сессии с их относительным возрастом.
|
|
320
|
+
- /help <command> — полное описание одной команды (например, `/help diff`)
|
|
321
|
+
вместо всего списка.
|
|
322
|
+
- /review [focus] [--staged] — попросить агента проверить незакоммиченные
|
|
323
|
+
изменения и сообщить находки (без правок кода).
|
|
324
|
+
- /plan [on|off] — режим плана (только чтение). Пока он включён, мутирующие
|
|
325
|
+
инструменты (Write/Edit/MultiEdit/ApplyPatch/Bash, GitAdd/GitCommit/GitPush)
|
|
326
|
+
убраны из набора, чтобы агент изучал код, не трогая дерево. Стартовать в нём
|
|
327
|
+
можно с `--plan`.
|
|
328
|
+
- /compact — попросить DeepSeek сжать текущий чат в передаточное резюме, затем
|
|
329
|
+
открыть НОВЫЙ чат, заново отправить системный промпт и положить резюме как
|
|
330
|
+
перенесённый контекст. Используйте, когда контекст разрастается.
|
|
331
|
+
- /goal [text|clear] — задать долгоживущую цель сессии. Она добавляется в
|
|
332
|
+
начало каждого сообщения задачи, поэтому модель держит общую картину через
|
|
333
|
+
много ходов. Хранится в `<project>/.zames-goal` (git-ignored) и
|
|
334
|
+
восстанавливается при следующем запуске.
|
|
335
|
+
- /loop <interval> <task> — повторять задачу периодически (например,
|
|
336
|
+
`/loop 10m run the tests`). /cron "<min> <hour> <dom> <month> <dow>" <task> —
|
|
337
|
+
запуск по расписанию. /jobs [rm <id>|clear] перечисляет и останавливает их.
|
|
338
|
+
Сработавшая задача попадает в ту же очередь сообщений, что и ваш ввод, так
|
|
339
|
+
что выполняется, когда агент свободен (никогда посреди генерации), и всё
|
|
340
|
+
равно соблюдает троттлинг отправки.
|
|
341
|
+
- /thinking [on|off] и /web [on|off] — переключить Deep thinking / Smart search.
|
|
342
|
+
Их (а также /queue, /jobs, /goal, `/config <sub>`) можно использовать, ПОКА
|
|
343
|
+
агент работает — они не трогают текущую генерацию.
|
|
344
|
+
- /queue [clear] — показать или очистить сообщения, ждущие отправки после
|
|
345
|
+
текущей задачи.
|
|
346
|
+
|
|
347
|
+
Контекст в токенах показывается и вживую: в строке статуса над вводом слева
|
|
348
|
+
спиннер/текст, справа — контекст (например, `125k · 13%`, процент от 1M
|
|
349
|
+
контекста). Он ОКРАШЕН по уровню заполнения: зелёный ниже 50%, жёлтый 50-80%,
|
|
350
|
+
красный выше 80%. Значение приходит из DeepSeek `accumulated_token_usage` и
|
|
351
|
+
скрыто до первого ответа.
|
|
352
|
+
|
|
353
|
+
## Контекст проекта, навыки и память
|
|
354
|
+
|
|
355
|
+
Как в Codex / Claude Code, zames читает инструкции проекта и переиспользуемые
|
|
356
|
+
процессы из вашего репозитория и из `~/.zames`.
|
|
357
|
+
|
|
358
|
+
- **AGENTS.md** — инструкции проекта. Положите его в корень репозитория (или в
|
|
359
|
+
любую родительскую папку рабочей директории). Глобальные инструкции живут в
|
|
360
|
+
`~/.zames/AGENTS.md` (и `~/.claude/CLAUDE.md`). Запустите `/init`, и агент
|
|
361
|
+
изучит проект и напишет AGENTS.md на основе реальных команд сборки/тестов и
|
|
362
|
+
соглашений (`/init --force` перезапишет существующий файл).
|
|
363
|
+
- **MEMORY.md** — долговечные заметки, переживающие сессии. Агент дописывает
|
|
364
|
+
сюда полезные факты; можно править вручную или добавить из промпта через
|
|
365
|
+
`/remember <text>`.
|
|
366
|
+
- **Навыки** — папка с файлом `SKILL.md` (YAML-фронтматтер: `name`,
|
|
367
|
+
`description`, опционально `allowed-tools`, `user-invokable`) плюс любые
|
|
368
|
+
вспомогательные файлы. Ищутся в `.zames/skills/`, `.claude/skills/`,
|
|
369
|
+
`.agents/skills/`, `skills/` и `~/.zames/skills/`. Агент читает тело только
|
|
370
|
+
когда задача совпадает с описанием. Список — `/skills`; вызов —
|
|
371
|
+
`/<skill-name>`.
|
|
372
|
+
- **Пользовательские команды** — `.md`-файлы в `.zames/commands/` (или
|
|
373
|
+
`.claude/commands/`). Поддерживают подстановки `$ARGUMENTS` / `{{args}}` и
|
|
374
|
+
вызываются как `/<command-name>`.
|
|
375
|
+
|
|
376
|
+
Навыки и пользовательские команды видны в списке автодополнения «/» и в
|
|
377
|
+
`/help`.
|
|
378
|
+
|
|
379
|
+
```
|
|
380
|
+
/skills список найденных навыков
|
|
381
|
+
/memory показать действующие AGENTS.md / MEMORY.md
|
|
382
|
+
/remember <text> дописать долговечную заметку в MEMORY.md
|
|
383
|
+
/init [--force] проанализировать проект и создать AGENTS.md
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## MCP (внешние инструменты)
|
|
387
|
+
|
|
388
|
+
zames может использовать инструменты с серверов [MCP](https://modelcontextprotocol.io).
|
|
389
|
+
Главный пример — @playwright/mcp: он даёт агенту настоящий браузер
|
|
390
|
+
(navigate, click, snapshot, type, ...) поверх того, который zames уже
|
|
391
|
+
использует для чата DeepSeek.
|
|
392
|
+
|
|
393
|
+
Положите файл конфига (той же формы, что в Claude Code / Cursor):
|
|
394
|
+
|
|
395
|
+
- `~/.zames/mcp.json` — глобальный
|
|
396
|
+
- `<project>/.zames/mcp.json` — на уровне проекта (поздние файлы побеждают)
|
|
397
|
+
- `<project>/.mcp.json` — привычное имя MCP
|
|
398
|
+
|
|
399
|
+
```json
|
|
400
|
+
{
|
|
401
|
+
"mcpServers": {
|
|
402
|
+
"playwright": {
|
|
403
|
+
"command": "npx",
|
|
404
|
+
"args": ["-y", "@playwright/mcp@latest", "--headless", "--isolated"]
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
ВАЖНО: держите MCP-браузер изолированным. @playwright/mcp по умолчанию
|
|
411
|
+
использует ТУ ЖЕ директорию профиля, что и zames (~/.zames/profile). Если
|
|
412
|
+
запустить его без --isolated (или без собственного --user-data-dir),
|
|
413
|
+
MCP-браузер и браузер агента подерутся за один профиль, и чат DeepSeek
|
|
414
|
+
покажет «Something went wrong when opening your profile». Всегда передавайте
|
|
415
|
+
--isolated, как в примере выше.
|
|
416
|
+
Серверы бывают и удалёнными ("url": "https://...", "transport": "sse").
|
|
417
|
+
Их инструменты видны агенту как `server__tool` (например,
|
|
418
|
+
`playwright__browser_navigate`) и перечисляются в `/mcp` и `/status`.
|
|
419
|
+
Сервер, который не смог подключиться, пропускается с предупреждением и
|
|
420
|
+
никогда не роняет агента.
|
|
421
|
+
|
|
422
|
+
## Конфигурация
|
|
423
|
+
|
|
424
|
+
Глобальный конфиг: `~/.zames/config.json`
|
|
425
|
+
Локальный (на проект): `.zamesrc.json`
|
|
426
|
+
|
|
427
|
+
Просматривать и менять настройки можно не выходя из агента — командой
|
|
428
|
+
`/config`. Запустите её без аргументов, чтобы открыть интерактивное меню
|
|
429
|
+
(↑/↓ — навигация, Enter — изменение, `d` — сброс, `q` — выход). Булевы и
|
|
430
|
+
enum переключаются на месте; числа и строки открывают ввод.
|
|
431
|
+
|
|
432
|
+
```
|
|
433
|
+
/config интерактивное меню настроек
|
|
434
|
+
/config list показать все изменяемые настройки
|
|
435
|
+
/config get <path> показать настройку
|
|
436
|
+
/config set <path> <value> изменить настройку
|
|
437
|
+
/config reset <path> сбросить настройку к дефолту
|
|
438
|
+
/config path показать пути файлов конфига
|
|
439
|
+
/config lang <ru|en> переключить язык интерфейса и агента
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
Примеры:
|
|
443
|
+
|
|
444
|
+
```
|
|
445
|
+
/config set maxIterations 20
|
|
446
|
+
/config set browser.deepThinking true # DeepSeek Deep thinking (медленно; рассуждения скрыты)
|
|
447
|
+
/config set browser.webSearch false # DeepSeek Smart web search
|
|
448
|
+
/config lang en
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
Изменения пишутся в проектный `.zamesrc.json` и применяются сразу (где это
|
|
452
|
+
возможно без перезапуска).
|
|
453
|
+
|
|
454
|
+
### Язык
|
|
455
|
+
|
|
456
|
+
`/config lang ru` или `/config lang en` переключает и язык интерфейса
|
|
457
|
+
(справка, сообщения, спиннер), и язык, на котором агент вам отвечает. Локаль
|
|
458
|
+
хранится в `ui.locale` в файле конфига.
|
|
459
|
+
|
|
460
|
+
Данные агента хранятся в `~/.zames`: профиль браузера, логи, история undo,
|
|
461
|
+
сессии.
|
|
462
|
+
|
|
463
|
+
## FAQ
|
|
464
|
+
|
|
465
|
+
**Это официальный продукт DeepSeek?**
|
|
466
|
+
Нет. zames управляет публичным веб-интерфейсом chat.deepseek.com через
|
|
467
|
+
настоящий браузер, как обычный пользователь. Он не связан с DeepSeek.
|
|
468
|
+
|
|
469
|
+
**Нужен ли API-ключ?**
|
|
470
|
+
Нет. Вы один раз входите в свой аккаунт DeepSeek; сессия хранится в
|
|
471
|
+
`~/.zames/profile` и переиспользуется.
|
|
472
|
+
|
|
473
|
+
**Какую модель он использует?**
|
|
474
|
+
Ту, которую использует веб-чат DeepSeek (DeepSeek-V3 или reasoning-модель при
|
|
475
|
+
включённом «Deep thinking»). zames никогда не вызывает API напрямую.
|
|
476
|
+
|
|
477
|
+
**Работает ли headless / на сервере?**
|
|
478
|
+
Да — headless по умолчанию, а `--output-format jsonl` делает его скриптуемым.
|
|
479
|
+
`--headed` нужен только для ручного входа или отладки селекторов.
|
|
480
|
+
|
|
481
|
+
**Разрешена ли автоматизация веб-интерфейса?**
|
|
482
|
+
Это зависит от условий DeepSeek; автоматизация сайта может их нарушать.
|
|
483
|
+
Используйте на свой риск и держите троттлинг отправки (15 с по умолчанию),
|
|
484
|
+
чтобы не бить по рейт-лимиту.
|
|
485
|
+
|
|
486
|
+
**Чем отличается от Claude Code / Codex?**
|
|
487
|
+
Та же форма (инструменты, `AGENTS.md`, навыки, MCP, слэш-команды), но работает
|
|
488
|
+
на вашем аккаунте DeepSeek вместо API, как автоматизация браузера, а не
|
|
489
|
+
first-party API-клиент.
|
|
490
|
+
|
|
491
|
+
## Ссылки
|
|
492
|
+
|
|
493
|
+
- **npm:** <https://www.npmjs.com/package/zames_pro>
|
|
494
|
+
- **GitHub:** <https://github.com/Viqto0r/zames_pro>
|
|
495
|
+
- **Changelog:** [`CHANGELOG.md`](CHANGELOG.md)
|
|
496
|
+
- **Contributing:** [`CONTRIBUTING.md`](CONTRIBUTING.md)
|
|
497
|
+
- **Security policy:** [`SECURITY.md`](SECURITY.md)
|
|
498
|
+
- **Code of conduct:** [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md)
|
|
499
|
+
|
|
500
|
+
## Лицензия
|
|
501
|
+
|
|
502
|
+
MIT
|
package/dist/index.js
CHANGED
|
@@ -19,13 +19,10 @@ import { Transcript } from './transcript.js';
|
|
|
19
19
|
import { UndoStore } from './undo.js';
|
|
20
20
|
import { CheckpointStore, pickRecord as pickCheckpoint } from './checkpoint.js';
|
|
21
21
|
import { selfReview, selfDiff, selfApply, selfList } from './self-review.js';
|
|
22
|
-
import { formatDiff, formatDiffStat, formatContextSources, diffGitArgs, parseTranscript, summarizeTranscript, renderCost, formatExport, defaultExportPath, renderDoctor, resolveExtraDir, buildReviewPrompt, isDevOnlyCommand, parseBacklogItems, nextBacklogItem, buildImprovePrompt, formatRelativeTime, trimRestoredMessages, RESTORED_HISTORY_LIMIT, hasQueuedJob, buildSlashCommandHints, parseQueueCommand, parseLiveToggle, parseGoalCommand, isLiveConfigCommand, formatQueueList, ctrlCEscalation, expandCommandArgs, missingCommandArgs, } from './commands.js';
|
|
23
22
|
import { performCompact } from './compact.js';
|
|
24
23
|
import { Scheduler, parseInterval, formatInterval, formatJobLine, parseCron, } from './scheduler.js';
|
|
25
24
|
import { renderMarkdown, setAnswerWidth } from './markdown.js';
|
|
26
|
-
import { resolveAttachPath, inlineAtRefs } from './attach-refs.js';
|
|
27
25
|
import { parseBacklogNote } from './backlog.js';
|
|
28
|
-
import { runTask } from './run-task.js';
|
|
29
26
|
import { closeWeb } from './web.js';
|
|
30
27
|
import { assertCommandInsideRoot, runShell } from './shell.js';
|
|
31
28
|
import { saveSession, loadLastSession, readSession, listSessions, sessionsDir, loadHistory, saveHistory, } from './sessions.js';
|
|
@@ -198,8 +195,19 @@ const RELOADABLE = [
|
|
|
198
195
|
'config',
|
|
199
196
|
'fsutil',
|
|
200
197
|
'backlog',
|
|
198
|
+
// C3 modules index.ts now calls through the mod bag (G2): without this a
|
|
199
|
+
// /reload silently kept the OLD commands/run-task/attach-refs until a restart.
|
|
200
|
+
// 'input/layout' is NOT listed: its only live consumers are input.ts (the
|
|
201
|
+
// long-lived LineEditor, itself not reloadable) and spinner.ts, which already
|
|
202
|
+
// picks up a fresh copy transitively.
|
|
203
|
+
'commands',
|
|
204
|
+
'run-task',
|
|
205
|
+
'attach-refs',
|
|
201
206
|
];
|
|
202
207
|
const mod = {
|
|
208
|
+
commands: null,
|
|
209
|
+
attach: null,
|
|
210
|
+
runTask: null,
|
|
203
211
|
createTools,
|
|
204
212
|
runAgentLoop,
|
|
205
213
|
buildSystemPrompt: null,
|
|
@@ -260,6 +268,12 @@ async function reloadModules() {
|
|
|
260
268
|
mod.backlogStats = pick('backlog', 'backlogStats');
|
|
261
269
|
if (pick('backlog', 'backlogNeedsPruning'))
|
|
262
270
|
mod.backlogNeedsPruning = pick('backlog', 'backlogNeedsPruning');
|
|
271
|
+
if (pick('commands', 'missingCommandArgs'))
|
|
272
|
+
mod.commands = loaded.get('commands');
|
|
273
|
+
if (pick('attach-refs', 'resolveAttachPath'))
|
|
274
|
+
mod.attach = loaded.get('attach-refs');
|
|
275
|
+
if (pick('run-task', 'runTask'))
|
|
276
|
+
mod.runTask = pick('run-task', 'runTask');
|
|
263
277
|
return { count: loaded.size, errors };
|
|
264
278
|
}
|
|
265
279
|
// Initial load so that mod.buildSystemPrompt and the rest are populated.
|
|
@@ -508,10 +522,10 @@ async function expandSlashTarget(workdir, name, rest) {
|
|
|
508
522
|
const cmd = commands.find((c) => c.name.toLowerCase() === name.toLowerCase());
|
|
509
523
|
if (cmd) {
|
|
510
524
|
// B7: refuse to run when declared positional arguments are missing.
|
|
511
|
-
const missing = missingCommandArgs(cmd.arguments, rest);
|
|
525
|
+
const missing = mod.commands.missingCommandArgs(cmd.arguments, rest);
|
|
512
526
|
if (missing.length)
|
|
513
527
|
return { missing };
|
|
514
|
-
const body = expandCommandArgs(cmd.body, rest, cmd.arguments);
|
|
528
|
+
const body = mod.commands.expandCommandArgs(cmd.body, rest, cmd.arguments);
|
|
515
529
|
return { text: body || rest };
|
|
516
530
|
}
|
|
517
531
|
const skills = await loadSkills(workdir);
|
|
@@ -549,7 +563,11 @@ async function expandSlashTarget(workdir, name, rest) {
|
|
|
549
563
|
// in commands.ts (buildSlashCommandHints); here we only supply the runtime
|
|
550
564
|
// data (dev flag, translator, dynamic skills/commands).
|
|
551
565
|
function buildSlashCommands() {
|
|
552
|
-
return buildSlashCommandHints({
|
|
566
|
+
return mod.commands.buildSlashCommandHints({
|
|
567
|
+
devMode,
|
|
568
|
+
t,
|
|
569
|
+
dynamic: dynamicCommands,
|
|
570
|
+
});
|
|
553
571
|
}
|
|
554
572
|
// We put the agent's temporary files (one-off scripts, etc.) in
|
|
555
573
|
// <project>/tmp — this folder is in .gitignore and is cleaned on every launch.
|
|
@@ -805,8 +823,8 @@ async function printRestoredHistory(browser, ui, chatId = null) {
|
|
|
805
823
|
// those. The "truncated" note must compare the DISPLAYABLE count with the
|
|
806
824
|
// limit (comparing `all.length` always fired, because tool-calls are
|
|
807
825
|
// dropped and the raw history is always longer).
|
|
808
|
-
const displayable = trimRestoredMessages(all, 0);
|
|
809
|
-
const messages = trimRestoredMessages(all);
|
|
826
|
+
const displayable = mod.commands.trimRestoredMessages(all, 0);
|
|
827
|
+
const messages = mod.commands.trimRestoredMessages(all);
|
|
810
828
|
// Always surface the diagnostic line when the history could not be turned
|
|
811
829
|
// into anything printable — otherwise "the dialogue is empty" is a dead end
|
|
812
830
|
// (was the fetch blocked? did the chat really have no user turns?).
|
|
@@ -857,7 +875,9 @@ async function printRestoredHistory(browser, ui, chatId = null) {
|
|
|
857
875
|
}
|
|
858
876
|
}
|
|
859
877
|
if (displayable.length > messages.length) {
|
|
860
|
-
out(theme.dim(t('chats.history_truncated', {
|
|
878
|
+
out(theme.dim(t('chats.history_truncated', {
|
|
879
|
+
n: String(mod.commands.RESTORED_HISTORY_LIMIT),
|
|
880
|
+
})));
|
|
861
881
|
}
|
|
862
882
|
}
|
|
863
883
|
// Build the explicit dependency bag for runTask() (src/run-task.ts). The
|
|
@@ -1011,7 +1031,7 @@ async function main() {
|
|
|
1011
1031
|
}
|
|
1012
1032
|
}
|
|
1013
1033
|
await autoReload();
|
|
1014
|
-
await runTask(runTaskDeps(), browser, tools, task, currentWorkdir, {
|
|
1034
|
+
await mod.runTask(runTaskDeps(), browser, tools, task, currentWorkdir, {
|
|
1015
1035
|
transcript,
|
|
1016
1036
|
freshChat,
|
|
1017
1037
|
sendSystemPrompt,
|
|
@@ -1132,7 +1152,7 @@ async function main() {
|
|
|
1132
1152
|
// this job already has a pending message in the queue, skip this fire.
|
|
1133
1153
|
// Otherwise a 1-minute loop would add a new task every minute while a
|
|
1134
1154
|
// long task is in flight, and they would all run back-to-back.
|
|
1135
|
-
if (hasQueuedJob(pendingQueue, job.id)) {
|
|
1155
|
+
if (mod.commands.hasQueuedJob(pendingQueue, job.id)) {
|
|
1136
1156
|
transcript.log('scheduled_skip_queued', {
|
|
1137
1157
|
id: job.id,
|
|
1138
1158
|
task: job.task,
|
|
@@ -1300,7 +1320,7 @@ async function main() {
|
|
|
1300
1320
|
return att;
|
|
1301
1321
|
}
|
|
1302
1322
|
// Case 2: the paste is a path to a local file (drag&drop or copy path).
|
|
1303
|
-
const filePath = await resolveAttachPath(currentWorkdir, raw);
|
|
1323
|
+
const filePath = await mod.attach.resolveAttachPath(currentWorkdir, raw);
|
|
1304
1324
|
if (!filePath)
|
|
1305
1325
|
return null;
|
|
1306
1326
|
const data = await fs.readFile(filePath).catch(() => null);
|
|
@@ -1378,7 +1398,7 @@ async function main() {
|
|
|
1378
1398
|
// `await runTask()`, so ONLY commands that make sense without the main
|
|
1379
1399
|
// loop are handled here (a queued slash-command would run too late).
|
|
1380
1400
|
if (ed.busy) {
|
|
1381
|
-
const q = parseQueueCommand(text);
|
|
1401
|
+
const q = mod.commands.parseQueueCommand(text);
|
|
1382
1402
|
if (q) {
|
|
1383
1403
|
if (q.sub === 'clear') {
|
|
1384
1404
|
const n = pendingQueue.length;
|
|
@@ -1390,7 +1410,7 @@ async function main() {
|
|
|
1390
1410
|
}
|
|
1391
1411
|
else {
|
|
1392
1412
|
ed.printAbove(theme.system(t('msg.queue_title')));
|
|
1393
|
-
for (const line of formatQueueList(pendingQueue)) {
|
|
1413
|
+
for (const line of mod.commands.formatQueueList(pendingQueue)) {
|
|
1394
1414
|
ed.printAbove(theme.assistant(line));
|
|
1395
1415
|
}
|
|
1396
1416
|
ed.printAbove(theme.dim(t('msg.queue_cleared_hint')));
|
|
@@ -1401,7 +1421,7 @@ async function main() {
|
|
|
1401
1421
|
// browser's DESIRED state and the status icons; it does not touch the
|
|
1402
1422
|
// in-flight generation, so it is safe mid-run. The actual DeepSeek
|
|
1403
1423
|
// toggles are applied on the next send by _applyToggles().
|
|
1404
|
-
const tg = parseLiveToggle(text);
|
|
1424
|
+
const tg = mod.commands.parseLiveToggle(text);
|
|
1405
1425
|
if (tg) {
|
|
1406
1426
|
applyLiveToggle(tg);
|
|
1407
1427
|
return;
|
|
@@ -1412,7 +1432,7 @@ async function main() {
|
|
|
1412
1432
|
// it pauses the editor and would fight the running task, so it stays
|
|
1413
1433
|
// queued. `setConfigRuntime` already applies hot values (locale,
|
|
1414
1434
|
// toggles, context limit) to the live objects immediately.
|
|
1415
|
-
if (isLiveConfigCommand(text)) {
|
|
1435
|
+
if (mod.commands.isLiveConfigCommand(text)) {
|
|
1416
1436
|
void handleConfigCommand(text);
|
|
1417
1437
|
return;
|
|
1418
1438
|
}
|
|
@@ -1458,7 +1478,7 @@ async function main() {
|
|
|
1458
1478
|
// (sets _stopped, which also halts the queue). This makes the two
|
|
1459
1479
|
// intents distinguishable instead of one press doing everything.
|
|
1460
1480
|
const now = Date.now();
|
|
1461
|
-
if (ctrlCEscalation(now - lastCtrlCAt) === 'run') {
|
|
1481
|
+
if (mod.commands.ctrlCEscalation(now - lastCtrlCAt) === 'run') {
|
|
1462
1482
|
ed.printAbove(theme.warn(t('msg.abort_ctrlc_short')));
|
|
1463
1483
|
browser.stopGeneration().catch(() => { });
|
|
1464
1484
|
}
|
|
@@ -1605,7 +1625,7 @@ async function main() {
|
|
|
1605
1625
|
// variable and a file, never the chat). Output goes above the input line
|
|
1606
1626
|
// when the editor is active, otherwise to stdout.
|
|
1607
1627
|
async function handleGoal(input) {
|
|
1608
|
-
const g = parseGoalCommand(input);
|
|
1628
|
+
const g = mod.commands.parseGoalCommand(input);
|
|
1609
1629
|
if (!g)
|
|
1610
1630
|
return;
|
|
1611
1631
|
const emit = (line) => {
|
|
@@ -1864,7 +1884,7 @@ async function main() {
|
|
|
1864
1884
|
// Hiding them from /help is not enough: a user could type the command by
|
|
1865
1885
|
// hand, and it would rewrite their project's BACKLOG.md or snapshot src/.
|
|
1866
1886
|
if (!devMode) {
|
|
1867
|
-
const devCmd = isDevOnlyCommand(lower);
|
|
1887
|
+
const devCmd = mod.commands.isDevOnlyCommand(lower);
|
|
1868
1888
|
if (devCmd) {
|
|
1869
1889
|
console.error(theme.warn(t('msg.dev_only', { v: devCmd })));
|
|
1870
1890
|
continue;
|
|
@@ -2240,7 +2260,7 @@ async function main() {
|
|
|
2240
2260
|
const mark = s.id === currentChatId ? theme.user(' *') : '';
|
|
2241
2261
|
const title = s.title ? ` ${s.title}` : '';
|
|
2242
2262
|
const wd = s.workdir ? theme.dim(` [${dirLabel(s.workdir)}]`) : '';
|
|
2243
|
-
const age = formatRelativeTime(s.updatedAt, t);
|
|
2263
|
+
const age = mod.commands.formatRelativeTime(s.updatedAt, t);
|
|
2244
2264
|
const ageText = age ? theme.dim(' ' + age) : '';
|
|
2245
2265
|
console.log(` ${theme.user(n)}. ${s.id.slice(0, 8)}…${title}${wd}${ageText}${mark}`);
|
|
2246
2266
|
});
|
|
@@ -2415,7 +2435,7 @@ async function main() {
|
|
|
2415
2435
|
continue;
|
|
2416
2436
|
}
|
|
2417
2437
|
if (lower === '/queue' || lower.startsWith('/queue ')) {
|
|
2418
|
-
const q = parseQueueCommand(trimmed);
|
|
2438
|
+
const q = mod.commands.parseQueueCommand(trimmed);
|
|
2419
2439
|
if (!q) {
|
|
2420
2440
|
console.error(theme.error(t('msg.queue_usage')));
|
|
2421
2441
|
continue;
|
|
@@ -2431,7 +2451,7 @@ async function main() {
|
|
|
2431
2451
|
continue;
|
|
2432
2452
|
}
|
|
2433
2453
|
console.log(theme.system(t('msg.queue_title')));
|
|
2434
|
-
for (const line of formatQueueList(pendingQueue)) {
|
|
2454
|
+
for (const line of mod.commands.formatQueueList(pendingQueue)) {
|
|
2435
2455
|
console.log(theme.assistant(line));
|
|
2436
2456
|
}
|
|
2437
2457
|
console.log(theme.dim(t('msg.queue_cleared_hint')));
|
|
@@ -2452,7 +2472,7 @@ async function main() {
|
|
|
2452
2472
|
continue;
|
|
2453
2473
|
}
|
|
2454
2474
|
if (lower.startsWith('/thinking') || lower.startsWith('/web')) {
|
|
2455
|
-
const tg = parseLiveToggle(trimmed);
|
|
2475
|
+
const tg = mod.commands.parseLiveToggle(trimmed);
|
|
2456
2476
|
if (tg) {
|
|
2457
2477
|
applyLiveToggle(tg);
|
|
2458
2478
|
continue;
|
|
@@ -2751,7 +2771,7 @@ async function main() {
|
|
|
2751
2771
|
locale: currentLocale,
|
|
2752
2772
|
context: ctx,
|
|
2753
2773
|
}).length;
|
|
2754
|
-
console.log(theme.system(formatContextSources(ctx.agents, ctx.memory, ctx.skills, ctx.commands, { systemPromptChars: sysLen }, t)));
|
|
2774
|
+
console.log(theme.system(mod.commands.formatContextSources(ctx.agents, ctx.memory, ctx.skills, ctx.commands, { systemPromptChars: sysLen }, t)));
|
|
2755
2775
|
}
|
|
2756
2776
|
catch (e) {
|
|
2757
2777
|
console.error(theme.error(e.message));
|
|
@@ -2807,7 +2827,7 @@ async function main() {
|
|
|
2807
2827
|
if (editor)
|
|
2808
2828
|
editor.busy = true;
|
|
2809
2829
|
try {
|
|
2810
|
-
await runTask(runTaskDeps(), browser, tools, lastTaskText, currentWorkdir, {
|
|
2830
|
+
await mod.runTask(runTaskDeps(), browser, tools, lastTaskText, currentWorkdir, {
|
|
2811
2831
|
transcript,
|
|
2812
2832
|
freshChat: false,
|
|
2813
2833
|
sendSystemPrompt: false,
|
|
@@ -2838,8 +2858,8 @@ async function main() {
|
|
|
2838
2858
|
console.error(theme.error(t('diff.not_repo')));
|
|
2839
2859
|
continue;
|
|
2840
2860
|
}
|
|
2841
|
-
const out = await runGit(diffGitArgs(staged), currentWorkdir, 20_000);
|
|
2842
|
-
console.log(theme.system(formatDiff(out, { maxLines: 400 }, t)));
|
|
2861
|
+
const out = await runGit(mod.commands.diffGitArgs(staged), currentWorkdir, 20_000);
|
|
2862
|
+
console.log(theme.system(mod.commands.formatDiff(out, { maxLines: 400 }, t)));
|
|
2843
2863
|
continue;
|
|
2844
2864
|
}
|
|
2845
2865
|
if (lower === '/diffstat') {
|
|
@@ -2850,28 +2870,28 @@ async function main() {
|
|
|
2850
2870
|
continue;
|
|
2851
2871
|
}
|
|
2852
2872
|
const out = await runGit('git diff --stat', currentWorkdir, 20_000);
|
|
2853
|
-
console.log(theme.system(formatDiffStat(out, { maxLines: 60 }, t)));
|
|
2873
|
+
console.log(theme.system(mod.commands.formatDiffStat(out, { maxLines: 60 }, t)));
|
|
2854
2874
|
continue;
|
|
2855
2875
|
}
|
|
2856
2876
|
if (lower === '/cost' || lower === '/usage') {
|
|
2857
|
-
let stats = summarizeTranscript([]);
|
|
2877
|
+
let stats = mod.commands.summarizeTranscript([]);
|
|
2858
2878
|
if (transcript.file) {
|
|
2859
2879
|
try {
|
|
2860
2880
|
const body = await fs.readFile(transcript.file, 'utf-8');
|
|
2861
|
-
stats = summarizeTranscript(parseTranscript(body));
|
|
2881
|
+
stats = mod.commands.summarizeTranscript(mod.commands.parseTranscript(body));
|
|
2862
2882
|
}
|
|
2863
2883
|
catch {
|
|
2864
2884
|
// best-effort
|
|
2865
2885
|
}
|
|
2866
2886
|
}
|
|
2867
|
-
console.log(theme.system(renderCost(stats, transcript.file, browser.getLastTokenUsage(), t)));
|
|
2887
|
+
console.log(theme.system(mod.commands.renderCost(stats, transcript.file, browser.getLastTokenUsage(), t)));
|
|
2868
2888
|
continue;
|
|
2869
2889
|
}
|
|
2870
2890
|
if (lower === '/export' || lower.startsWith('/export ')) {
|
|
2871
2891
|
const arg = trimmed.slice('/export'.length).trim();
|
|
2872
2892
|
const target = arg
|
|
2873
2893
|
? path.resolve(currentWorkdir, arg)
|
|
2874
|
-
: defaultExportPath(currentWorkdir);
|
|
2894
|
+
: mod.commands.defaultExportPath(currentWorkdir);
|
|
2875
2895
|
const rel = path.relative(sandboxRoot, target);
|
|
2876
2896
|
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
2877
2897
|
console.error(theme.error(t('export.outside')));
|
|
@@ -2880,13 +2900,13 @@ async function main() {
|
|
|
2880
2900
|
let entries = [];
|
|
2881
2901
|
if (transcript.file) {
|
|
2882
2902
|
try {
|
|
2883
|
-
entries = parseTranscript(await fs.readFile(transcript.file, 'utf-8'));
|
|
2903
|
+
entries = mod.commands.parseTranscript(await fs.readFile(transcript.file, 'utf-8'));
|
|
2884
2904
|
}
|
|
2885
2905
|
catch {
|
|
2886
2906
|
entries = [];
|
|
2887
2907
|
}
|
|
2888
2908
|
}
|
|
2889
|
-
const md = formatExport(entries, {
|
|
2909
|
+
const md = mod.commands.formatExport(entries, {
|
|
2890
2910
|
chatId: currentChatId,
|
|
2891
2911
|
workdir: currentWorkdir,
|
|
2892
2912
|
});
|
|
@@ -2918,7 +2938,7 @@ async function main() {
|
|
|
2918
2938
|
const mcpStatus = mcpPool
|
|
2919
2939
|
? mcpPool.status()
|
|
2920
2940
|
: { servers: [], toolCount: 0 };
|
|
2921
|
-
console.log(theme.system(renderDoctor({
|
|
2941
|
+
console.log(theme.system(mod.commands.renderDoctor({
|
|
2922
2942
|
nodeVersion: process.version,
|
|
2923
2943
|
platform: process.platform,
|
|
2924
2944
|
workdir: currentWorkdir,
|
|
@@ -2944,7 +2964,7 @@ async function main() {
|
|
|
2944
2964
|
}
|
|
2945
2965
|
if (lower === '/add-dir' || lower.startsWith('/add-dir ')) {
|
|
2946
2966
|
const arg = trimmed.slice('/add-dir'.length).trim();
|
|
2947
|
-
const res = resolveExtraDir(arg, currentWorkdir, t);
|
|
2967
|
+
const res = mod.commands.resolveExtraDir(arg, currentWorkdir, t);
|
|
2948
2968
|
if ('error' in res) {
|
|
2949
2969
|
console.error(theme.warn(res.error));
|
|
2950
2970
|
continue;
|
|
@@ -3000,8 +3020,8 @@ async function main() {
|
|
|
3000
3020
|
console.error(theme.warn(t('improve.no_backlog')));
|
|
3001
3021
|
continue;
|
|
3002
3022
|
}
|
|
3003
|
-
const items = parseBacklogItems(backlogText);
|
|
3004
|
-
const item = nextBacklogItem(items, arg || undefined);
|
|
3023
|
+
const items = mod.commands.parseBacklogItems(backlogText);
|
|
3024
|
+
const item = mod.commands.nextBacklogItem(items, arg || undefined);
|
|
3005
3025
|
if (!item) {
|
|
3006
3026
|
console.log(theme.dim(arg ? t('improve.not_found', { v: arg }) : t('improve.all_done')));
|
|
3007
3027
|
continue;
|
|
@@ -3017,7 +3037,7 @@ async function main() {
|
|
|
3017
3037
|
if (editor)
|
|
3018
3038
|
editor.busy = true;
|
|
3019
3039
|
try {
|
|
3020
|
-
await runTask(runTaskDeps(), browser, improveTools, buildImprovePrompt(item), currentWorkdir, {
|
|
3040
|
+
await mod.runTask(runTaskDeps(), browser, improveTools, mod.commands.buildImprovePrompt(item), currentWorkdir, {
|
|
3021
3041
|
transcript,
|
|
3022
3042
|
freshChat: false,
|
|
3023
3043
|
sendSystemPrompt: false,
|
|
@@ -3094,7 +3114,7 @@ async function main() {
|
|
|
3094
3114
|
const rest = trimmed.slice('/review'.length).trim();
|
|
3095
3115
|
const staged = rest.indexOf('--staged') !== -1;
|
|
3096
3116
|
const focus = rest.replace(/--staged/g, '').trim();
|
|
3097
|
-
const reviewTask = buildReviewPrompt(focus, staged);
|
|
3117
|
+
const reviewTask = mod.commands.buildReviewPrompt(focus, staged);
|
|
3098
3118
|
const reviewTools = mod.createTools(currentWorkdir, {
|
|
3099
3119
|
undo,
|
|
3100
3120
|
todos: todoStore,
|
|
@@ -3102,7 +3122,7 @@ async function main() {
|
|
|
3102
3122
|
if (editor)
|
|
3103
3123
|
editor.busy = true;
|
|
3104
3124
|
try {
|
|
3105
|
-
await runTask(runTaskDeps(), browser, reviewTools, reviewTask, currentWorkdir, {
|
|
3125
|
+
await mod.runTask(runTaskDeps(), browser, reviewTools, reviewTask, currentWorkdir, {
|
|
3106
3126
|
transcript,
|
|
3107
3127
|
freshChat: false,
|
|
3108
3128
|
sendSystemPrompt: false,
|
|
@@ -3317,7 +3337,7 @@ async function main() {
|
|
|
3317
3337
|
// existing files are inlined; a bare `@name` in prose is left untouched.
|
|
3318
3338
|
let finalTaskText = taskText;
|
|
3319
3339
|
{
|
|
3320
|
-
const refRes = await inlineAtRefs(currentWorkdir, taskText);
|
|
3340
|
+
const refRes = await mod.attach.inlineAtRefs(currentWorkdir, taskText);
|
|
3321
3341
|
if (refRes.inlined.length) {
|
|
3322
3342
|
finalTaskText = refRes.text;
|
|
3323
3343
|
console.log(theme.system(t('msg.at_refs', { n: refRes.inlined.length }) +
|
|
@@ -3357,7 +3377,7 @@ async function main() {
|
|
|
3357
3377
|
if (editor)
|
|
3358
3378
|
editor.busy = true;
|
|
3359
3379
|
try {
|
|
3360
|
-
await runTask(runTaskDeps(), browser, tools, finalTaskText, currentWorkdir, {
|
|
3380
|
+
await mod.runTask(runTaskDeps(), browser, tools, finalTaskText, currentWorkdir, {
|
|
3361
3381
|
transcript,
|
|
3362
3382
|
freshChat: freshChatNext,
|
|
3363
3383
|
sendSystemPrompt: sendSystemPromptNext,
|
package/dist/system-prompt.js
CHANGED
|
@@ -98,11 +98,20 @@ export function renderContextSection(context) {
|
|
|
98
98
|
}
|
|
99
99
|
return out;
|
|
100
100
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
// The tool list in the prompt is GENERATED from the ToolDefs (createTools),
|
|
102
|
+
// never hand-written: a second copy drifts (the prompt promises a tool that
|
|
103
|
+
// no longer exists, or misses one that was added). This is the single source
|
|
104
|
+
// of truth for `### <Tool>` blocks; the prose that cannot be derived
|
|
105
|
+
// ("Choosing the right tool", the call formats) stays hard-coded below.
|
|
106
|
+
// Guarded by test/system-prompt-tools.test.ts.
|
|
107
|
+
export function renderToolSection(tools) {
|
|
108
|
+
return tools
|
|
104
109
|
.map((t2) => `### ${t2.name}\n${t2.description}\nParameters: ${JSON.stringify(t2.parameters)}`)
|
|
105
110
|
.join('\n\n');
|
|
111
|
+
}
|
|
112
|
+
export function buildSystemPrompt({ workdir, tools, gitContext = null, locale = 'ru', attachments = [], context = null, selfImprovement = false, }) {
|
|
113
|
+
const t = translate(locale);
|
|
114
|
+
const toolDescriptions = renderToolSection(tools);
|
|
106
115
|
const gitSection = gitContext
|
|
107
116
|
? `\n## Git context\n\n${gitContext}\n`
|
|
108
117
|
: '\n## Git context\n\nNot a git repository (or git is not installed).\n';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Terminal coding agent that drives chat.deepseek.com through Playwright
|
|
3
|
+
"version": "2.66.0",
|
|
4
|
+
"description": "Terminal coding agent that drives chat.deepseek.com through Playwright — no API key required. Reads and edits files, runs commands, commits to git, like Claude Code / Codex CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"zames": "dist/index.js"
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"dist",
|
|
11
11
|
"scripts",
|
|
12
12
|
"README.md",
|
|
13
|
+
"README.ru.md",
|
|
13
14
|
"CHANGELOG.md",
|
|
14
15
|
"LICENSE",
|
|
15
16
|
"logo-small.jpg"
|
|
@@ -21,6 +22,7 @@
|
|
|
21
22
|
"build": "node scripts/clean-dist.mjs && tsc -p tsconfig.build.json",
|
|
22
23
|
"coverage:gate": "node scripts/coverage-gate.mjs",
|
|
23
24
|
"changelog:draft": "tsx scripts/changelog-draft.mts",
|
|
25
|
+
"release": "tsx scripts/release.mts",
|
|
24
26
|
"typecheck": "tsc --noEmit",
|
|
25
27
|
"lint": "node scripts/eslint-ts6.mjs",
|
|
26
28
|
"lint:fix": "node scripts/eslint-ts6.mjs --fix",
|
|
@@ -37,14 +39,21 @@
|
|
|
37
39
|
"keywords": [
|
|
38
40
|
"agent",
|
|
39
41
|
"ai",
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
+
"ai-agent",
|
|
43
|
+
"ai-coding-assistant",
|
|
42
44
|
"coding-agent",
|
|
45
|
+
"code-assistant",
|
|
46
|
+
"terminal-agent",
|
|
47
|
+
"deepseek",
|
|
48
|
+
"deepseek-chat",
|
|
43
49
|
"claude-code",
|
|
44
50
|
"codex",
|
|
51
|
+
"cli",
|
|
45
52
|
"terminal",
|
|
46
53
|
"playwright",
|
|
54
|
+
"browser-automation",
|
|
47
55
|
"llm",
|
|
56
|
+
"no-api-key",
|
|
48
57
|
"automation"
|
|
49
58
|
],
|
|
50
59
|
"license": "MIT",
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import {
|
|
6
|
+
parseConventionalCommit,
|
|
7
|
+
groupCommits,
|
|
8
|
+
renderChangelogDraft,
|
|
9
|
+
} from '../src/changelog.js'
|
|
10
|
+
|
|
11
|
+
// One-command release prep (BACKLOG G4). The manual sequence — bump
|
|
12
|
+
// package.json, write a dated CHANGELOG section, commit `chore: release X.Y.Z`,
|
|
13
|
+
// tag — is easy to get wrong, so this script does the deterministic parts and
|
|
14
|
+
// STOPS before the push. Pushing the branch and the tag is deliberately a
|
|
15
|
+
// separate, explicit step: the tag triggers the npm publish workflow, and that
|
|
16
|
+
// must never happen by accident.
|
|
17
|
+
//
|
|
18
|
+
// Usage:
|
|
19
|
+
// npm run release patch # bug fixes
|
|
20
|
+
// npm run release minor # new features
|
|
21
|
+
// npm run release major # breaking changes
|
|
22
|
+
// npm run release patch --no-tests # skip the local test run (CI still runs)
|
|
23
|
+
// npm run release patch --dry-run # print the plan, change nothing
|
|
24
|
+
//
|
|
25
|
+
// Run via tsx so it can reuse src/changelog.ts (the conventional-commit
|
|
26
|
+
// parser) — a plain .mjs cannot import the TypeScript sources.
|
|
27
|
+
|
|
28
|
+
const NL = String.fromCharCode(10)
|
|
29
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
30
|
+
|
|
31
|
+
function git(args: string[], allowFail = false): string {
|
|
32
|
+
const r = spawnSync('git', args, { cwd: root, encoding: 'utf-8' })
|
|
33
|
+
if (r.status !== 0 && !allowFail) {
|
|
34
|
+
console.error('git ' + args.join(' ') + ' failed:' + NL + (r.stderr || ''))
|
|
35
|
+
process.exit(1)
|
|
36
|
+
}
|
|
37
|
+
return (r.stdout || '').toString()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function fail(msg: string): never {
|
|
41
|
+
console.error('release: ' + msg)
|
|
42
|
+
process.exit(1)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function run(cmd: string, args: string[]): void {
|
|
46
|
+
const r = spawnSync(cmd, args, { cwd: root, stdio: 'inherit', shell: process.platform === 'win32' })
|
|
47
|
+
if (r.status !== 0) fail(cmd + ' ' + args.join(' ') + ' exited ' + r.status)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const argv = process.argv.slice(2)
|
|
51
|
+
const flags = new Set(argv.filter((a) => a.startsWith('--')))
|
|
52
|
+
const bump = argv.find((a) => !a.startsWith('--'))
|
|
53
|
+
const dryRun = flags.has('--dry-run')
|
|
54
|
+
const skipTests = flags.has('--no-tests')
|
|
55
|
+
|
|
56
|
+
if (bump !== 'patch' && bump !== 'minor' && bump !== 'major') {
|
|
57
|
+
fail('usage: npm run release patch|minor|major [--no-tests] [--dry-run]')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 1. Clean tree: a release commit must contain ONLY the version bump and the
|
|
61
|
+
// changelog section, never half-finished work.
|
|
62
|
+
const dirty = git(['status', '--porcelain']).trim()
|
|
63
|
+
if (dirty) {
|
|
64
|
+
fail('working tree is not clean:' + NL + dirty)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 2. Gates. The git hooks run these on commit/push too, but a release should
|
|
68
|
+
// fail EARLY, before package.json/CHANGELOG are touched.
|
|
69
|
+
if (!skipTests) {
|
|
70
|
+
run('npm', ['run', 'typecheck'])
|
|
71
|
+
run('npm', ['test'])
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 3. Compute the next version from package.json.
|
|
75
|
+
const pkgPath = path.join(root, 'package.json')
|
|
76
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as {
|
|
77
|
+
version: string
|
|
78
|
+
}
|
|
79
|
+
const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(pkg.version)
|
|
80
|
+
if (!m) fail('package.json version is not a plain x.y.z: ' + pkg.version)
|
|
81
|
+
let [major, minor, patch] = [Number(m[1]), Number(m[2]), Number(m[3])]
|
|
82
|
+
if (bump === 'major') {
|
|
83
|
+
major += 1
|
|
84
|
+
minor = 0
|
|
85
|
+
patch = 0
|
|
86
|
+
} else if (bump === 'minor') {
|
|
87
|
+
minor += 1
|
|
88
|
+
patch = 0
|
|
89
|
+
} else {
|
|
90
|
+
patch += 1
|
|
91
|
+
}
|
|
92
|
+
const next = major + '.' + minor + '.' + patch
|
|
93
|
+
const tag = 'v' + next
|
|
94
|
+
|
|
95
|
+
if (git(['tag', '--list', tag]).trim()) {
|
|
96
|
+
fail('tag ' + tag + ' already exists')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 4. Build the dated CHANGELOG section from conventional commits since the
|
|
100
|
+
// last tag. English commit subjects become the section body; the operator can
|
|
101
|
+
// still reword the section afterwards.
|
|
102
|
+
const latestTag = git(['describe', '--tags', '--abbrev=0'], true).trim()
|
|
103
|
+
const ref = latestTag || git(['rev-list', '--max-parents=0', 'HEAD']).trim().split(NL).pop() || 'HEAD'
|
|
104
|
+
const subjects = git(['log', '--no-merges', '--pretty=format:%s', ref + '..HEAD'])
|
|
105
|
+
.split(NL)
|
|
106
|
+
.map((s) => s.trim())
|
|
107
|
+
.filter(Boolean)
|
|
108
|
+
const parsed = subjects
|
|
109
|
+
.map(parseConventionalCommit)
|
|
110
|
+
.filter((c): c is NonNullable<typeof c> => c !== null)
|
|
111
|
+
const body = renderChangelogDraft(groupCommits(parsed))
|
|
112
|
+
|
|
113
|
+
// CHANGELOG groups are `### Added` (Keep a Changelog), the draft renders
|
|
114
|
+
// `## Added` — normalize here.
|
|
115
|
+
const sectionBody = body
|
|
116
|
+
.split(NL)
|
|
117
|
+
.map((line) => (line.startsWith('## ') ? '#' + line : line))
|
|
118
|
+
.join(NL)
|
|
119
|
+
const date = new Date().toISOString().slice(0, 10)
|
|
120
|
+
const section =
|
|
121
|
+
'## [' + next + '] - ' + date + NL + NL + (sectionBody || '### Changed' + NL + NL + '- Internal improvements.') + NL
|
|
122
|
+
|
|
123
|
+
if (dryRun) {
|
|
124
|
+
console.log('release(dry-run): ' + pkg.version + ' -> ' + next)
|
|
125
|
+
console.log('tag: ' + tag)
|
|
126
|
+
console.log('since: ' + ref)
|
|
127
|
+
console.log('CHANGELOG section:' + NL + section)
|
|
128
|
+
process.exit(0)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 5. Insert the section right after the `## [Unreleased]` heading.
|
|
132
|
+
const changelogPath = path.join(root, 'CHANGELOG.md')
|
|
133
|
+
const changelog = fs.readFileSync(changelogPath, 'utf-8')
|
|
134
|
+
const anchor = '## [Unreleased]'
|
|
135
|
+
const idx = changelog.indexOf(anchor)
|
|
136
|
+
if (idx < 0) fail('CHANGELOG.md has no "' + anchor + '" heading')
|
|
137
|
+
const afterAnchor = idx + anchor.length
|
|
138
|
+
const updated =
|
|
139
|
+
changelog.slice(0, afterAnchor) +
|
|
140
|
+
NL + NL + section +
|
|
141
|
+
changelog.slice(afterAnchor).replace(/^\n+/, NL)
|
|
142
|
+
fs.writeFileSync(changelogPath, updated)
|
|
143
|
+
|
|
144
|
+
// 6. Bump package.json, preserving its exact formatting (single-line replace
|
|
145
|
+
// of the version value only).
|
|
146
|
+
const pkgText = fs.readFileSync(pkgPath, 'utf-8')
|
|
147
|
+
fs.writeFileSync(pkgPath, pkgText.replace('"version": "' + pkg.version + '"', '"version": "' + next + '"'))
|
|
148
|
+
|
|
149
|
+
// 7. Commit + tag. The push is intentionally NOT done here.
|
|
150
|
+
git(['add', 'package.json', 'CHANGELOG.md'])
|
|
151
|
+
git(['commit', '-m', 'chore: release ' + next])
|
|
152
|
+
git(['tag', tag])
|
|
153
|
+
|
|
154
|
+
console.log(NL + 'release: committed and tagged ' + tag)
|
|
155
|
+
console.log('Next (explicit) step — publish via CI:')
|
|
156
|
+
console.log(' git push')
|
|
157
|
+
console.log(' git push origin ' + tag)
|