zames_pro 1.2.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-loop.js +671 -0
- package/dist/browser.js +656 -0
- package/dist/config.js +73 -0
- package/dist/confirm.js +81 -0
- package/dist/diff.js +37 -0
- package/dist/gitTools.js +218 -0
- package/dist/index.js +1378 -0
- package/dist/input.js +523 -0
- package/dist/markdown.js +47 -0
- package/dist/net-capture.js +100 -0
- package/dist/self-review.js +280 -0
- package/dist/sessions.js +122 -0
- package/dist/spinner.js +137 -0
- package/{src → dist}/system-prompt.js +23 -14
- package/{src → dist}/theme.js +16 -19
- package/dist/tools.js +211 -0
- package/dist/transcript.js +50 -0
- package/dist/types.js +3 -0
- package/dist/undo.js +99 -0
- package/dist/web.js +219 -0
- package/dist/xml-toolcall.js +67 -0
- package/package.json +17 -5
- package/src/agent-loop.js +0 -634
- package/src/browser.js +0 -627
- package/src/config.js +0 -81
- package/src/confirm.js +0 -85
- package/src/diff.js +0 -32
- package/src/gitTools.js +0 -256
- package/src/index.js +0 -1515
- package/src/input.js +0 -447
- package/src/markdown.js +0 -49
- package/src/self-review.js +0 -319
- package/src/sessions.js +0 -110
- package/src/spinner.js +0 -144
- package/src/tools.js +0 -206
- package/src/transcript.js +0 -46
- package/src/undo.js +0 -101
- package/src/web.js +0 -266
- package/src/xml-toolcall.js +0 -69
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import { theme } from './theme.js';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { ZAMES_HOME } from './config.js';
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
// Исходники для самообзора — это .ts файлы. После сборки (tsc → dist/)
|
|
8
|
+
// __dirname указывает на dist, где лежат .js. Поэтому ищем каталог с
|
|
9
|
+
// исходниками: либо рядом (запуск из src/ через tsx), либо в ../src
|
|
10
|
+
// (запуск собранного dist/ из корня репозитория).
|
|
11
|
+
function resolveSrcDir() {
|
|
12
|
+
const candidates = [__dirname, path.join(__dirname, '..', 'src')];
|
|
13
|
+
for (const dir of candidates) {
|
|
14
|
+
try {
|
|
15
|
+
const entries = require('fs').readdirSync(dir);
|
|
16
|
+
if (entries.some((e) => e.endsWith('.ts')))
|
|
17
|
+
return dir;
|
|
18
|
+
}
|
|
19
|
+
catch { }
|
|
20
|
+
}
|
|
21
|
+
return __dirname;
|
|
22
|
+
}
|
|
23
|
+
const SRC_DIR = resolveSrcDir();
|
|
24
|
+
const SNAP_ROOT = path.join(ZAMES_HOME, 'snapshots');
|
|
25
|
+
// Динамический импорт логики с timestamp — чтобы при /reload (или авто-reload)
|
|
26
|
+
// self-review использовал СВЕЖИЕ tools/agent-loop, а не закэшированные при
|
|
27
|
+
// первой загрузке. Иначе reload не доходил бы до зависимостей self-review.
|
|
28
|
+
async function loadFresh() {
|
|
29
|
+
const stamp = Date.now();
|
|
30
|
+
const toolsUrl = new URL('./tools.js', import.meta.url);
|
|
31
|
+
toolsUrl.searchParams.set('t', String(stamp));
|
|
32
|
+
const loopUrl = new URL('./agent-loop.js', import.meta.url);
|
|
33
|
+
loopUrl.searchParams.set('t', String(stamp));
|
|
34
|
+
const [{ createTools }, { runAgentLoop }] = await Promise.all([
|
|
35
|
+
import(toolsUrl.href),
|
|
36
|
+
import(loopUrl.href),
|
|
37
|
+
]);
|
|
38
|
+
return { createTools, runAgentLoop };
|
|
39
|
+
}
|
|
40
|
+
async function ensureDir(p) {
|
|
41
|
+
await fs.mkdir(p, { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
async function copyDirJsFiles(from, to) {
|
|
44
|
+
await ensureDir(to);
|
|
45
|
+
const entries = await fs.readdir(from);
|
|
46
|
+
const copied = [];
|
|
47
|
+
for (const e of entries) {
|
|
48
|
+
if (!e.endsWith('.ts'))
|
|
49
|
+
continue;
|
|
50
|
+
const data = await fs.readFile(path.join(from, e), 'utf-8');
|
|
51
|
+
await fs.writeFile(path.join(to, e), data, 'utf-8');
|
|
52
|
+
copied.push(e);
|
|
53
|
+
}
|
|
54
|
+
return copied;
|
|
55
|
+
}
|
|
56
|
+
// ---------- /self-review ----------
|
|
57
|
+
export async function selfReview({ browser, focus, transcript }) {
|
|
58
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
59
|
+
const snapRoot = path.join(SNAP_ROOT);
|
|
60
|
+
const snapDir = path.join(snapRoot, `run-${stamp}`);
|
|
61
|
+
await ensureDir(snapRoot);
|
|
62
|
+
await ensureDir(snapDir);
|
|
63
|
+
const copied = await copyDirJsFiles(SRC_DIR, snapDir);
|
|
64
|
+
if (copied.length === 0) {
|
|
65
|
+
console.error(theme.error(`\n✖ Самообзор отменён: в ${SRC_DIR} нет .ts файлов.\n` +
|
|
66
|
+
`Проверь, что src/ не пуст и ты запускаешь агента из корня проекта.\n`));
|
|
67
|
+
throw new Error('SRC_DIR пуст — нечего ревьюить');
|
|
68
|
+
}
|
|
69
|
+
// Копируем package.json для контекста — чтобы агент видел зависимости
|
|
70
|
+
try {
|
|
71
|
+
const pkg = await fs.readFile(path.resolve(SRC_DIR, '..', 'package.json'), 'utf-8');
|
|
72
|
+
await fs.writeFile(path.join(snapDir, 'package.json'), pkg, 'utf-8');
|
|
73
|
+
}
|
|
74
|
+
catch { }
|
|
75
|
+
console.log(theme.system(`\n📸 Снапшот: ${snapDir}`));
|
|
76
|
+
console.log(theme.system(`Файлов: ${copied.length} — ${copied.join(', ')}`));
|
|
77
|
+
console.log(theme.system('Начинаю самообзор...\n'));
|
|
78
|
+
const taskPrompt = buildReviewPrompt({ focus, snapDir });
|
|
79
|
+
const { createTools, runAgentLoop } = await loadFresh();
|
|
80
|
+
const tools = createTools(snapDir, { undo: null });
|
|
81
|
+
let finalMessage = '';
|
|
82
|
+
await runAgentLoop({
|
|
83
|
+
browser,
|
|
84
|
+
tools,
|
|
85
|
+
task: taskPrompt,
|
|
86
|
+
workdir: snapDir,
|
|
87
|
+
maxIterations: 80,
|
|
88
|
+
freshChat: true,
|
|
89
|
+
sendSystemPrompt: true,
|
|
90
|
+
transcript,
|
|
91
|
+
onThinking: () => { },
|
|
92
|
+
onToolCall: (name, args) => {
|
|
93
|
+
const preview = JSON.stringify(args).slice(0, 120);
|
|
94
|
+
console.log(theme.warn(`🔧 ${name}`), theme.system(preview));
|
|
95
|
+
},
|
|
96
|
+
onToolResult: (r) => {
|
|
97
|
+
const t = typeof r === 'string' ? r : JSON.stringify(r);
|
|
98
|
+
console.log(theme.system(` → ${t.slice(0, 200).replace(/\n/g, ' ↵ ')}\n`));
|
|
99
|
+
},
|
|
100
|
+
onAssistantMessage: (msg) => {
|
|
101
|
+
finalMessage = msg;
|
|
102
|
+
console.log(theme.assistant('\n📋 Отчёт:\n'));
|
|
103
|
+
console.log(msg);
|
|
104
|
+
console.log();
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
// Сохраняем отчёт
|
|
108
|
+
const reportPath = path.join(snapDir, '_report.md');
|
|
109
|
+
await fs.writeFile(reportPath, finalMessage || '(пусто)', 'utf-8');
|
|
110
|
+
// Считаем, что изменилось
|
|
111
|
+
const changed = await diffFiles(SRC_DIR, snapDir);
|
|
112
|
+
console.log(theme.system('─'.repeat(60)));
|
|
113
|
+
console.log(theme.system(`Отчёт: ${reportPath}`));
|
|
114
|
+
console.log(theme.system(`Снапшот: ${snapDir}`));
|
|
115
|
+
if (changed.length) {
|
|
116
|
+
console.log(theme.assistant(`Изменено: ${changed.length} файл(ов): ${changed.join(', ')}`));
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
console.log(theme.system('Изменено: (ничего — только отчёт)'));
|
|
120
|
+
}
|
|
121
|
+
console.log(theme.user(`\nДальше:`));
|
|
122
|
+
console.log(theme.user(` /self-diff ${path.basename(snapDir)} — посмотреть различия`));
|
|
123
|
+
console.log(theme.user(` /self-apply ${path.basename(snapDir)} — применить к живому src/`));
|
|
124
|
+
console.log();
|
|
125
|
+
return { snapDir, reportPath, changed };
|
|
126
|
+
}
|
|
127
|
+
// ---------- /self-diff ----------
|
|
128
|
+
export async function selfDiff({ name }) {
|
|
129
|
+
const snapDir = path.join(SNAP_ROOT, name);
|
|
130
|
+
const snapStat = await fs.stat(snapDir).catch(() => null);
|
|
131
|
+
if (!snapStat)
|
|
132
|
+
throw new Error(`Снапшот не найден: ${snapDir}`);
|
|
133
|
+
const { unifiedDiff, colorDiff } = await import('./diff.js');
|
|
134
|
+
const files = await fs.readdir(snapDir);
|
|
135
|
+
let anyDiff = false;
|
|
136
|
+
for (const f of files) {
|
|
137
|
+
if (!f.endsWith('.ts'))
|
|
138
|
+
continue;
|
|
139
|
+
const orig = await fs
|
|
140
|
+
.readFile(path.join(SRC_DIR, f), 'utf-8')
|
|
141
|
+
.catch(() => '');
|
|
142
|
+
const next = await fs
|
|
143
|
+
.readFile(path.join(snapDir, f), 'utf-8')
|
|
144
|
+
.catch(() => '');
|
|
145
|
+
if (orig === next)
|
|
146
|
+
continue;
|
|
147
|
+
anyDiff = true;
|
|
148
|
+
console.log(theme.bold(`\n=== src/${f} ===`));
|
|
149
|
+
const diff = unifiedDiff(orig, next, { label: f });
|
|
150
|
+
console.log(colorDiff(diff));
|
|
151
|
+
}
|
|
152
|
+
if (!anyDiff) {
|
|
153
|
+
console.log(theme.system('Различий нет.'));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// ---------- /self-apply ----------
|
|
157
|
+
export async function selfApply({ name }) {
|
|
158
|
+
const snapDir = path.join(SNAP_ROOT, name);
|
|
159
|
+
const snapStat = await fs.stat(snapDir).catch(() => null);
|
|
160
|
+
if (!snapStat)
|
|
161
|
+
throw new Error(`Снапшот не найден: ${snapDir}`);
|
|
162
|
+
// Бэкап текущего src перед перезаписью
|
|
163
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
164
|
+
const backupDir = path.join(SNAP_ROOT, `backup-before-apply-${stamp}`);
|
|
165
|
+
await copyDirJsFiles(SRC_DIR, backupDir);
|
|
166
|
+
const files = await fs.readdir(snapDir);
|
|
167
|
+
const jsFiles = files.filter((f) => f.endsWith('.ts'));
|
|
168
|
+
if (jsFiles.length === 0) {
|
|
169
|
+
throw new Error(`В снапшоте ${snapDir} нет .ts файлов. Apply отменён, чтобы не стирать src/.`);
|
|
170
|
+
}
|
|
171
|
+
const applied = [];
|
|
172
|
+
for (const f of jsFiles) {
|
|
173
|
+
const data = await fs.readFile(path.join(snapDir, f), 'utf-8');
|
|
174
|
+
await fs.writeFile(path.join(SRC_DIR, f), data, 'utf-8');
|
|
175
|
+
applied.push(f);
|
|
176
|
+
}
|
|
177
|
+
console.log(theme.assistant(`\n✅ Применено: ${applied.length} файл(ов)`));
|
|
178
|
+
console.log(theme.system(applied.join(', ')));
|
|
179
|
+
console.log(theme.system(`Бэкап: ${backupDir}`));
|
|
180
|
+
console.log(theme.warn(`\nПерезапусти агента, чтобы изменения вступили в силу.\n`));
|
|
181
|
+
}
|
|
182
|
+
// ---------- /self-list ----------
|
|
183
|
+
export async function selfList({ config } = {}) {
|
|
184
|
+
const snapRoot = path.join(SNAP_ROOT);
|
|
185
|
+
let entries = [];
|
|
186
|
+
try {
|
|
187
|
+
entries = await fs.readdir(snapRoot);
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
console.log(theme.system('Снапшотов нет.'));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (!entries.length) {
|
|
194
|
+
console.log(theme.system('Снапшотов нет.'));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
entries.sort();
|
|
198
|
+
console.log(theme.system('Снапшоты самообзора:'));
|
|
199
|
+
for (const e of entries) {
|
|
200
|
+
const full = path.join(snapRoot, e);
|
|
201
|
+
const stat = await fs.stat(full).catch(() => null);
|
|
202
|
+
if (!stat || !stat.isDirectory())
|
|
203
|
+
continue;
|
|
204
|
+
const report = path.join(full, '_report.md');
|
|
205
|
+
const hasReport = await fs
|
|
206
|
+
.stat(report)
|
|
207
|
+
.then(() => true)
|
|
208
|
+
.catch(() => false);
|
|
209
|
+
const changedCount = (await diffFiles(SRC_DIR, full)).length;
|
|
210
|
+
const tag = changedCount
|
|
211
|
+
? theme.assistant(`[${changedCount} изменено]`)
|
|
212
|
+
: theme.system('[без правок]');
|
|
213
|
+
const rep = hasReport ? theme.user('📋') : ' ';
|
|
214
|
+
console.log(` ${rep} ${e} ${tag}`);
|
|
215
|
+
}
|
|
216
|
+
console.log();
|
|
217
|
+
console.log(theme.system('Команды: /self-diff <name>, /self-apply <name>\n'));
|
|
218
|
+
}
|
|
219
|
+
// ---------- helpers ----------
|
|
220
|
+
async function diffFiles(origDir, newDir) {
|
|
221
|
+
const files = await fs.readdir(newDir).catch(() => []);
|
|
222
|
+
const changed = [];
|
|
223
|
+
for (const f of files) {
|
|
224
|
+
if (!f.endsWith('.ts'))
|
|
225
|
+
continue;
|
|
226
|
+
const a = await fs.readFile(path.join(origDir, f), 'utf-8').catch(() => '');
|
|
227
|
+
const b = await fs.readFile(path.join(newDir, f), 'utf-8').catch(() => '');
|
|
228
|
+
if (a !== b)
|
|
229
|
+
changed.push(f);
|
|
230
|
+
}
|
|
231
|
+
return changed;
|
|
232
|
+
}
|
|
233
|
+
function buildReviewPrompt({ focus, snapDir }) {
|
|
234
|
+
const focusLine = focus
|
|
235
|
+
? `\nThe user asked you to focus especially on: ${focus}\n`
|
|
236
|
+
: '';
|
|
237
|
+
return `You are a senior JavaScript/Node.js engineer. You are reviewing your own source code — a Playwright-based coding agent that talks to the DeepSeek web chat as an LLM backend.
|
|
238
|
+
|
|
239
|
+
All source files are in your working directory: ${snapDir}
|
|
240
|
+
${focusLine}
|
|
241
|
+
## Your job
|
|
242
|
+
|
|
243
|
+
1. Read EVERY .ts file in your working directory. Use Glob("**/*.ts") to list them, then Read each one. Do not skip files.
|
|
244
|
+
2. Identify real, concrete problems. Focus on:
|
|
245
|
+
- Race conditions and async/promise bugs (missing await, unhandled rejections, timing issues)
|
|
246
|
+
- Error handling gaps: places that can throw and crash the agent
|
|
247
|
+
- JSON parsing edge cases in agent-loop.ts (LLM output is unreliable)
|
|
248
|
+
- Fragile DOM selectors in browser.ts (DeepSeek changes layout)
|
|
249
|
+
- Cross-platform issues (Windows paths, cmd.exe vs bash, encoding)
|
|
250
|
+
- Undo/transcript consistency: could undo leave files in bad state?
|
|
251
|
+
- Self-review mode safety: could /self-apply corrupt src/?
|
|
252
|
+
- Anything that breaks state between interactive tasks (chat context, workdir, spinner)
|
|
253
|
+
3. Fix what you can fix confidently. Use Edit (small targeted change) or Write (rewrite a file only if really needed). Keep the existing architecture and naming.
|
|
254
|
+
4. Do NOT invent problems. If you're not sure, mention it in the report, don't change it.
|
|
255
|
+
5. Do NOT touch package.json, node_modules, or files outside the working directory.
|
|
256
|
+
|
|
257
|
+
## Constraints
|
|
258
|
+
|
|
259
|
+
- You CANNOT run the agent itself (node_modules is not here). But you CAN use \`node --check <file>\` via Bash to verify syntax after edits.
|
|
260
|
+
- Always read a file before editing it.
|
|
261
|
+
- After all edits, run \`node --check\` on every file you changed. If any fails, fix the syntax error.
|
|
262
|
+
|
|
263
|
+
## Report format
|
|
264
|
+
|
|
265
|
+
When done, respond with a markdown report as the message:
|
|
266
|
+
|
|
267
|
+
## Найдено
|
|
268
|
+
- (bullet list of issues you found, with file:line if possible)
|
|
269
|
+
|
|
270
|
+
## Исправлено
|
|
271
|
+
- (bullet list: file — что именно изменил)
|
|
272
|
+
|
|
273
|
+
## Не исправлено (осознанно)
|
|
274
|
+
- (bullet list: what you left alone and why)
|
|
275
|
+
|
|
276
|
+
## Проверка
|
|
277
|
+
- (bullet list: what you verified, e.g. \`node --check\` results)
|
|
278
|
+
|
|
279
|
+
Be honest and specific. If the code is fine in some area, say so.`;
|
|
280
|
+
}
|
package/dist/sessions.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
// Сессии (чаты DeepSeek) храним в отдельной папке, чтобы они не терялись
|
|
5
|
+
// после перезапуска процесса. Каждая сессия — отдельный JSON-файл
|
|
6
|
+
// <id>.json в ~/.zames/.sessions. last.json указывает на последнюю
|
|
7
|
+
// открытую сессию для конкретной рабочей директории.
|
|
8
|
+
const SESSIONS_DIR = path.join(os.homedir(), '.zames', '.sessions');
|
|
9
|
+
const INDEX_FILE = path.join(SESSIONS_DIR, 'last.json');
|
|
10
|
+
function ensureDir() {
|
|
11
|
+
fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
function safeName(id) {
|
|
14
|
+
return String(id).replace(/[^a-zA-Z0-9_.-]/g, '_');
|
|
15
|
+
}
|
|
16
|
+
// Сохраняем/обновляем сессию. workdir нужен, чтобы при запуске из того же
|
|
17
|
+
// проекта восстанавливать именно его последний чат.
|
|
18
|
+
export function saveSession({ id, title = '', workdir = '', }) {
|
|
19
|
+
if (!id)
|
|
20
|
+
return null;
|
|
21
|
+
try {
|
|
22
|
+
ensureDir();
|
|
23
|
+
const file = path.join(SESSIONS_DIR, safeName(id) + '.json');
|
|
24
|
+
let prev = {};
|
|
25
|
+
try {
|
|
26
|
+
prev = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
27
|
+
}
|
|
28
|
+
catch { }
|
|
29
|
+
const now = new Date().toISOString();
|
|
30
|
+
const data = {
|
|
31
|
+
id,
|
|
32
|
+
title: title || prev.title || '',
|
|
33
|
+
workdir: workdir || prev.workdir || '',
|
|
34
|
+
createdAt: prev.createdAt || now,
|
|
35
|
+
updatedAt: now,
|
|
36
|
+
};
|
|
37
|
+
fs.writeFileSync(file, JSON.stringify(data, null, 2), 'utf-8');
|
|
38
|
+
writeIndex({ id, workdir: data.workdir });
|
|
39
|
+
return data;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function writeIndex({ id, workdir }) {
|
|
46
|
+
try {
|
|
47
|
+
ensureDir();
|
|
48
|
+
let index = {};
|
|
49
|
+
try {
|
|
50
|
+
index = JSON.parse(fs.readFileSync(INDEX_FILE, 'utf-8'));
|
|
51
|
+
}
|
|
52
|
+
catch { }
|
|
53
|
+
if (!index || typeof index !== 'object')
|
|
54
|
+
index = {};
|
|
55
|
+
if (!index.byWorkdir || typeof index.byWorkdir !== 'object') {
|
|
56
|
+
index.byWorkdir = {};
|
|
57
|
+
}
|
|
58
|
+
const key = workdir || '';
|
|
59
|
+
index.byWorkdir[key] = id;
|
|
60
|
+
index.last = id;
|
|
61
|
+
index.updatedAt = new Date().toISOString();
|
|
62
|
+
fs.writeFileSync(INDEX_FILE, JSON.stringify(index, null, 2), 'utf-8');
|
|
63
|
+
}
|
|
64
|
+
catch { }
|
|
65
|
+
}
|
|
66
|
+
// Последняя сессия для рабочей директории. Если для неё ничего нет —
|
|
67
|
+
// возвращаем последнюю сессию вообще (полезно при запуске из нового места).
|
|
68
|
+
export function loadLastSession(workdir = '') {
|
|
69
|
+
try {
|
|
70
|
+
const index = JSON.parse(fs.readFileSync(INDEX_FILE, 'utf-8'));
|
|
71
|
+
const byWorkdir = index && index.byWorkdir ? index.byWorkdir : {};
|
|
72
|
+
const candidates = [byWorkdir[workdir], index.last].filter((x) => Boolean(x));
|
|
73
|
+
for (const id of candidates) {
|
|
74
|
+
// Файл сессии мог быть удалён — индекс тогда устарел, и сессию
|
|
75
|
+
// восстанавливать нельзя. Пробуем следующий кандидат.
|
|
76
|
+
const s = readSession(id);
|
|
77
|
+
if (s)
|
|
78
|
+
return s;
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export function readSession(id) {
|
|
87
|
+
if (!id)
|
|
88
|
+
return null;
|
|
89
|
+
try {
|
|
90
|
+
const file = path.join(SESSIONS_DIR, safeName(id) + '.json');
|
|
91
|
+
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Список всех сессий, свежие — первыми.
|
|
98
|
+
export function listSessions() {
|
|
99
|
+
try {
|
|
100
|
+
ensureDir();
|
|
101
|
+
const files = fs
|
|
102
|
+
.readdirSync(SESSIONS_DIR)
|
|
103
|
+
.filter((f) => f.endsWith('.json') && f !== 'last.json');
|
|
104
|
+
const out = [];
|
|
105
|
+
for (const f of files) {
|
|
106
|
+
try {
|
|
107
|
+
const data = JSON.parse(fs.readFileSync(path.join(SESSIONS_DIR, f), 'utf-8'));
|
|
108
|
+
if (data && data.id)
|
|
109
|
+
out.push(data);
|
|
110
|
+
}
|
|
111
|
+
catch { }
|
|
112
|
+
}
|
|
113
|
+
out.sort((a, b) => String(b.updatedAt || '').localeCompare(String(a.updatedAt || '')));
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
export function sessionsDir() {
|
|
121
|
+
return SESSIONS_DIR;
|
|
122
|
+
}
|
package/dist/spinner.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
import { theme } from './theme.js';
|
|
3
|
+
import { renderMarkdown } from './markdown.js';
|
|
4
|
+
// Фразы для спиннера «агент работает». Выбираются в случайном порядке.
|
|
5
|
+
const THINKING_PHRASES = [
|
|
6
|
+
'Замешиваю кал…',
|
|
7
|
+
'Взбиваю кал до однородной массы…',
|
|
8
|
+
'Мешаю кал с логикой…',
|
|
9
|
+
'Взбалтываю кал в коктейль…',
|
|
10
|
+
'Кручу венчиком по калу…',
|
|
11
|
+
'Смешиваю кал с кофеином…',
|
|
12
|
+
'Замешиваю тесто из кала…',
|
|
13
|
+
'Взбиваю пену из кала…',
|
|
14
|
+
'Перемешиваю кал лопатой…',
|
|
15
|
+
'Замешиваю глину из кала…',
|
|
16
|
+
'Гомогенизирую кал до состояния бетона…',
|
|
17
|
+
'Взбалтываю кал до просветления…',
|
|
18
|
+
'Блендерю кал в смузи…',
|
|
19
|
+
'Замешиваю цемент из кала…',
|
|
20
|
+
'Взбиваю кашу из кала…',
|
|
21
|
+
'Мешаю кал с надеждой…',
|
|
22
|
+
'Взбалтываю осадок из кала…',
|
|
23
|
+
'Кручу блендером по калу…',
|
|
24
|
+
'Смешиваю кал до неразличимости…',
|
|
25
|
+
'Замешиваю раствор из кала…',
|
|
26
|
+
'Взбиваю коктейль «кал»…',
|
|
27
|
+
'Перемешиваю кал лопатой дедлайна…',
|
|
28
|
+
'Замешиваю кашу из кала…',
|
|
29
|
+
'Взбиваю пенку из кала…',
|
|
30
|
+
'Гомогенизирую кал до однородности…',
|
|
31
|
+
'Взбалтываю кал в бетономешалке…',
|
|
32
|
+
'Замешиваю кал в тесто…',
|
|
33
|
+
'Сбиваю кал в однородную массу…',
|
|
34
|
+
'Размешиваю кал до просветления…',
|
|
35
|
+
'Замешиваю кал из всего подряд…',
|
|
36
|
+
];
|
|
37
|
+
export function randomThinkingPhrase() {
|
|
38
|
+
return THINKING_PHRASES[Math.floor(Math.random() * THINKING_PHRASES.length)];
|
|
39
|
+
}
|
|
40
|
+
// Убираем завершающее многоточие из фразы — точки анимируем отдельно.
|
|
41
|
+
export function stripEllipsis(phrase) {
|
|
42
|
+
return phrase.replace(/[.…]+\s*$/, '');
|
|
43
|
+
}
|
|
44
|
+
export function createSpinner() {
|
|
45
|
+
let spinner = null;
|
|
46
|
+
let dotTimer = null;
|
|
47
|
+
let dotPhase = 0;
|
|
48
|
+
let pending = null;
|
|
49
|
+
const DOTS = ['.', '..', '...'];
|
|
50
|
+
const start = (text) => {
|
|
51
|
+
if (!spinner)
|
|
52
|
+
spinner = ora(text).start();
|
|
53
|
+
else
|
|
54
|
+
spinner.start(text);
|
|
55
|
+
};
|
|
56
|
+
const stop = () => {
|
|
57
|
+
if (dotTimer) {
|
|
58
|
+
clearInterval(dotTimer);
|
|
59
|
+
dotTimer = null;
|
|
60
|
+
}
|
|
61
|
+
if (spinner)
|
|
62
|
+
spinner.stop();
|
|
63
|
+
};
|
|
64
|
+
// Подсказка в строке статуса: во время работы агента терминал живой,
|
|
65
|
+
// можно печатать следующее сообщение. Без неё это неочевидно.
|
|
66
|
+
const HINT = theme.dim(' · печатай — Enter в очередь, Esc стоп');
|
|
67
|
+
// Запуск анимированного статуса: коричневый текст + «бегущие» точки.
|
|
68
|
+
const startThinking = () => {
|
|
69
|
+
const base = theme.brown(stripEllipsis(randomThinkingPhrase()));
|
|
70
|
+
start(base + DOTS[0] + HINT);
|
|
71
|
+
dotPhase = 0;
|
|
72
|
+
if (dotTimer)
|
|
73
|
+
clearInterval(dotTimer);
|
|
74
|
+
dotTimer = setInterval(() => {
|
|
75
|
+
if (!spinner)
|
|
76
|
+
return;
|
|
77
|
+
dotPhase = (dotPhase + 1) % DOTS.length;
|
|
78
|
+
spinner.text = base + DOTS[dotPhase] + HINT;
|
|
79
|
+
}, 400);
|
|
80
|
+
if (dotTimer.unref)
|
|
81
|
+
dotTimer.unref();
|
|
82
|
+
};
|
|
83
|
+
// Показать набранный, но ещё не отправленный текст вместо спиннера.
|
|
84
|
+
// Позволяет печатать сообщение прямо во время работы агента.
|
|
85
|
+
const showPending = () => {
|
|
86
|
+
if (dotTimer) {
|
|
87
|
+
clearInterval(dotTimer);
|
|
88
|
+
dotTimer = null;
|
|
89
|
+
}
|
|
90
|
+
if (!spinner)
|
|
91
|
+
spinner = ora('');
|
|
92
|
+
spinner.start();
|
|
93
|
+
spinner.text = theme.prompt('✎ ') + pending + HINT;
|
|
94
|
+
};
|
|
95
|
+
return {
|
|
96
|
+
thinking: () => {
|
|
97
|
+
if (pending) {
|
|
98
|
+
showPending();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
startThinking();
|
|
102
|
+
},
|
|
103
|
+
// Текст, который пользователь набирает во время работы агента.
|
|
104
|
+
// Пустая строка / null — вернуть обычный спиннер.
|
|
105
|
+
setPending: (text) => {
|
|
106
|
+
pending = text && String(text).length ? String(text) : null;
|
|
107
|
+
if (pending)
|
|
108
|
+
showPending();
|
|
109
|
+
else
|
|
110
|
+
startThinking();
|
|
111
|
+
},
|
|
112
|
+
toolCall: (name, args) => {
|
|
113
|
+
stop();
|
|
114
|
+
const preview = JSON.stringify(args).slice(0, 120);
|
|
115
|
+
console.log(theme.tool('🔧 ' + name), theme.dim(preview));
|
|
116
|
+
},
|
|
117
|
+
toolResult: (result) => {
|
|
118
|
+
stop();
|
|
119
|
+
const text = typeof result === 'string' ? result : JSON.stringify(result);
|
|
120
|
+
const preview = text.slice(0, 200).split(String.fromCharCode(10)).join(' ↵ ');
|
|
121
|
+
console.log(theme.toolResult(' → ' + preview + String.fromCharCode(10)));
|
|
122
|
+
},
|
|
123
|
+
assistant: (msg) => {
|
|
124
|
+
stop();
|
|
125
|
+
const NL = String.fromCharCode(10);
|
|
126
|
+
const rendered = renderMarkdown(msg);
|
|
127
|
+
// Маркер ответа модели: помогает визуально отделить его от ввода
|
|
128
|
+
// пользователя (который подсвечен приглашением с золотой стрелкой).
|
|
129
|
+
console.log(NL + theme.assistant('● Ответ') + NL);
|
|
130
|
+
console.log(rendered);
|
|
131
|
+
console.log(theme.dim('─'.repeat(60)) + NL);
|
|
132
|
+
},
|
|
133
|
+
stop,
|
|
134
|
+
succeed: () => { },
|
|
135
|
+
fail: () => { },
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -1,16 +1,11 @@
|
|
|
1
|
-
export function buildSystemPrompt({ workdir, tools, gitContext = null }) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const gitSection = gitContext
|
|
10
|
-
? `\n## Git context\n\n${gitContext}\n`
|
|
11
|
-
: '\n## Git context\n\nNot a git repository (or git is not installed).\n'
|
|
12
|
-
|
|
13
|
-
return `You are a coding agent running in a terminal. You help the user with software engineering tasks by reading files, writing code, running commands, and iterating until the task is done.
|
|
1
|
+
export function buildSystemPrompt({ workdir, tools, gitContext = null, }) {
|
|
2
|
+
const toolDescriptions = tools
|
|
3
|
+
.map((t) => `### ${t.name}\n${t.description}\nПараметры: ${JSON.stringify(t.parameters)}`)
|
|
4
|
+
.join('\n\n');
|
|
5
|
+
const gitSection = gitContext
|
|
6
|
+
? `\n## Git context\n\n${gitContext}\n`
|
|
7
|
+
: '\n## Git context\n\nNot a git repository (or git is not installed).\n';
|
|
8
|
+
return `You are a coding agent running in a terminal. You help the user with software engineering tasks by reading files, writing code, running commands, and iterating until the task is done.
|
|
14
9
|
|
|
15
10
|
You work in the directory: ${workdir}
|
|
16
11
|
|
|
@@ -83,6 +78,20 @@ Your response MUST be valid JSON. This means:
|
|
|
83
78
|
|
|
84
79
|
When in doubt — use forward slashes for all file paths.
|
|
85
80
|
|
|
81
|
+
## CRITICAL: preserving special characters in Write/Edit
|
|
82
|
+
|
|
83
|
+
The channel that delivers your answer may corrupt certain characters in tool
|
|
84
|
+
arguments: the dollar sign in template literals, backslashes, newlines, and
|
|
85
|
+
identifiers (e.g. 'x.name'). To write or edit files containing such characters
|
|
86
|
+
reliably, pass the content in base64 instead of plain text:
|
|
87
|
+
|
|
88
|
+
- Write: use 'content_base64' instead of 'content'.
|
|
89
|
+
- Edit: use 'old_base64' / 'new_base64' instead of 'old_string' / 'new_string'.
|
|
90
|
+
|
|
91
|
+
base64 alphabet is [A-Za-z0-9+/=] and is not corrupted. Encode with the
|
|
92
|
+
base64 command or in Node via Buffer.from(text).toString('base64').
|
|
93
|
+
Use plain content/old_string only when the text has no special characters.
|
|
94
|
+
|
|
86
95
|
## CRITICAL: quoting inside shell commands
|
|
87
96
|
|
|
88
97
|
When you call Bash, put the whole shell command in a single JSON string.
|
|
@@ -124,5 +133,5 @@ Anything you write outside that one respond is not read by the operator.
|
|
|
124
133
|
- WebFetch strips HTML. If a page is JS-rendered and text comes back empty, call WebFetch again with render=true.
|
|
125
134
|
- Do NOT fetch the same URL repeatedly. If content is missing, report that instead.
|
|
126
135
|
- Do NOT use WebFetch on localhost, private IPs, or 'file://' — it will fail.
|
|
127
|
-
|
|
136
|
+
`;
|
|
128
137
|
}
|
package/{src → dist}/theme.js
RENAMED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import chalk from 'chalk'
|
|
2
|
-
|
|
1
|
+
import chalk from 'chalk';
|
|
3
2
|
// Спокойная палитра для тёмного терминала.
|
|
4
3
|
// Приглушённые, слегка «выцветшие» тона без высокой насыщенности
|
|
5
4
|
// (в духе Tokyo Night / Nord). Задача — комфортное чтение длинных
|
|
@@ -15,21 +14,19 @@ import chalk from 'chalk'
|
|
|
15
14
|
// - system — нейтральный серый
|
|
16
15
|
// - warn — спокойный охра
|
|
17
16
|
// - error — приглушённый терракот
|
|
18
|
-
|
|
19
17
|
export const theme = {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export default theme
|
|
18
|
+
user: chalk.hex('#b4b8d0'), // мягкий лавандово-серый
|
|
19
|
+
prompt: chalk.hex('#c8b06a').bold, // приглашение ввода — тёплое золотистое, заметное
|
|
20
|
+
dir: chalk.hex('#7fc4f0'), // имя рабочей директории — заметно голубое
|
|
21
|
+
assistant: chalk.hex('#cfc9b0'), // тёплый песочный
|
|
22
|
+
tool: chalk.hex('#c6a97e'), // приглушённый янтарный
|
|
23
|
+
toolResult: chalk.hex('#8a9bb5'), // серо-голубой
|
|
24
|
+
system: chalk.hex('#808896'), // нейтральный серый
|
|
25
|
+
dim: chalk.hex('#5b616e'), // тёмно-серый
|
|
26
|
+
warn: chalk.hex('#c9a86a'), // спокойный охра
|
|
27
|
+
error: chalk.hex('#c98a80'), // приглушённый терракот
|
|
28
|
+
success: chalk.hex('#a9c08c'), // мягкий шалфейный
|
|
29
|
+
brown: chalk.hex('#a1723f'), // коричневый (статус/спиннер)
|
|
30
|
+
bold: chalk.bold,
|
|
31
|
+
};
|
|
32
|
+
export default theme;
|