zames_pro 1.0.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.
@@ -0,0 +1,304 @@
1
+ import path from 'path'
2
+ import fs from 'fs/promises'
3
+ import chalk from 'chalk'
4
+ import { fileURLToPath } from 'url'
5
+
6
+ import { createTools } from './tools.js'
7
+ import { runAgentLoop } from './agent-loop.js'
8
+ import { ZAMES_HOME } from './config.js'
9
+
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
11
+ const SRC_DIR = __dirname
12
+ const SNAP_ROOT = path.join(ZAMES_HOME, 'snapshots')
13
+
14
+ async function ensureDir(p) {
15
+ await fs.mkdir(p, { recursive: true })
16
+ }
17
+
18
+ async function copyDirJsFiles(from, to) {
19
+ await ensureDir(to)
20
+ const entries = await fs.readdir(from)
21
+ const copied = []
22
+ for (const e of entries) {
23
+ if (!e.endsWith('.js')) continue
24
+ const data = await fs.readFile(path.join(from, e), 'utf-8')
25
+ await fs.writeFile(path.join(to, e), data, 'utf-8')
26
+ copied.push(e)
27
+ }
28
+ return copied
29
+ }
30
+
31
+
32
+ // ---------- /self-review ----------
33
+
34
+ export async function selfReview({ browser, config, focus, transcript }) {
35
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-')
36
+ const snapRoot = path.join(SNAP_ROOT)
37
+ const snapDir = path.join(snapRoot, `run-${stamp}`)
38
+ await ensureDir(snapRoot)
39
+ await ensureDir(snapDir)
40
+
41
+ const copied = await copyDirJsFiles(SRC_DIR, snapDir)
42
+
43
+ if (copied.length === 0) {
44
+ console.error(
45
+ chalk.red(
46
+ `\n✖ Самообзор отменён: в ${SRC_DIR} нет .js файлов.\n` +
47
+ `Проверь, что src/ не пуст и ты запускаешь агента из корня проекта.\n`,
48
+ ),
49
+ )
50
+ throw new Error('SRC_DIR пуст — нечего ревьюить')
51
+ }
52
+
53
+ // Копируем package.json для контекста — чтобы агент видел зависимости
54
+ try {
55
+ const pkg = await fs.readFile(
56
+ path.resolve(SRC_DIR, '..', 'package.json'),
57
+ 'utf-8',
58
+ )
59
+ await fs.writeFile(path.join(snapDir, 'package.json'), pkg, 'utf-8')
60
+ } catch {}
61
+
62
+ console.log(chalk.gray(`\n📸 Снапшот: ${snapDir}`))
63
+ console.log(chalk.gray(`Файлов: ${copied.length} — ${copied.join(', ')}`))
64
+ console.log(chalk.gray('Начинаю самообзор...\n'))
65
+
66
+ const taskPrompt = buildReviewPrompt({ focus, snapDir })
67
+
68
+ const tools = createTools(snapDir, { undo: null })
69
+ let finalMessage = ''
70
+
71
+ await runAgentLoop({
72
+ browser,
73
+ tools,
74
+ task: taskPrompt,
75
+ workdir: snapDir,
76
+ maxIterations: 80,
77
+ freshChat: true,
78
+ sendSystemPrompt: true,
79
+ transcript,
80
+ onThinking: () => {},
81
+ onToolCall: (name, args) => {
82
+ const preview = JSON.stringify(args).slice(0, 120)
83
+ console.log(chalk.yellow(`🔧 ${name}`), chalk.gray(preview))
84
+ },
85
+ onToolResult: (r) => {
86
+ const t = typeof r === 'string' ? r : JSON.stringify(r)
87
+ console.log(chalk.gray(` → ${t.slice(0, 200).replace(/\n/g, ' ↵ ')}\n`))
88
+ },
89
+ onAssistantMessage: (msg) => {
90
+ finalMessage = msg
91
+ console.log(chalk.green('\n📋 Отчёт:\n'))
92
+ console.log(msg)
93
+ console.log()
94
+ },
95
+ })
96
+
97
+ // Сохраняем отчёт
98
+ const reportPath = path.join(snapDir, '_report.md')
99
+ await fs.writeFile(reportPath, finalMessage || '(пусто)', 'utf-8')
100
+
101
+ // Считаем, что изменилось
102
+ const changed = await diffFiles(SRC_DIR, snapDir)
103
+
104
+ console.log(chalk.gray('─'.repeat(60)))
105
+ console.log(chalk.gray(`Отчёт: ${reportPath}`))
106
+ console.log(chalk.gray(`Снапшот: ${snapDir}`))
107
+ if (changed.length) {
108
+ console.log(
109
+ chalk.green(
110
+ `Изменено: ${changed.length} файл(ов): ${changed.join(', ')}`,
111
+ ),
112
+ )
113
+ } else {
114
+ console.log(chalk.gray('Изменено: (ничего — только отчёт)'))
115
+ }
116
+ console.log(chalk.cyan(`\nДальше:`))
117
+ console.log(
118
+ chalk.cyan(
119
+ ` /self-diff ${path.basename(snapDir)} — посмотреть различия`,
120
+ ),
121
+ )
122
+ console.log(
123
+ chalk.cyan(
124
+ ` /self-apply ${path.basename(snapDir)} — применить к живому src/`,
125
+ ),
126
+ )
127
+ console.log()
128
+
129
+ return { snapDir, reportPath, changed }
130
+ }
131
+
132
+ // ---------- /self-diff ----------
133
+
134
+ export async function selfDiff({ config, name }) {
135
+ const snapDir = path.join(SNAP_ROOT, name)
136
+ const snapStat = await fs.stat(snapDir).catch(() => null)
137
+ if (!snapStat) throw new Error(`Снапшот не найден: ${snapDir}`)
138
+
139
+ const { unifiedDiff, colorDiff } = await import('./diff.js')
140
+ const files = await fs.readdir(snapDir)
141
+ let anyDiff = false
142
+
143
+ for (const f of files) {
144
+ if (!f.endsWith('.js')) continue
145
+ const orig = await fs
146
+ .readFile(path.join(SRC_DIR, f), 'utf-8')
147
+ .catch(() => '')
148
+ const next = await fs
149
+ .readFile(path.join(snapDir, f), 'utf-8')
150
+ .catch(() => '')
151
+ if (orig === next) continue
152
+
153
+ anyDiff = true
154
+ console.log(chalk.bold(`\n=== src/${f} ===`))
155
+ const diff = unifiedDiff(orig, next, { label: f })
156
+ console.log(colorDiff(diff, chalk))
157
+ }
158
+
159
+ if (!anyDiff) {
160
+ console.log(chalk.gray('Различий нет.'))
161
+ }
162
+ }
163
+
164
+ // ---------- /self-apply ----------
165
+
166
+ export async function selfApply({ config, name }) {
167
+ const snapDir = path.join(SNAP_ROOT, name)
168
+ const snapStat = await fs.stat(snapDir).catch(() => null)
169
+ if (!snapStat) throw new Error(`Снапшот не найден: ${snapDir}`)
170
+
171
+ // Бэкап текущего src перед перезаписью
172
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-')
173
+ const backupDir = path.join(
174
+ SNAP_ROOT,
175
+ `backup-before-apply-${stamp}`,
176
+ )
177
+ await copyDirJsFiles(SRC_DIR, backupDir)
178
+
179
+ const files = await fs.readdir(snapDir)
180
+ const jsFiles = files.filter((f) => f.endsWith('.js'))
181
+
182
+ if (jsFiles.length === 0) {
183
+ throw new Error(
184
+ `В снапшоте ${snapDir} нет .js файлов. Apply отменён, чтобы не стирать src/.`,
185
+ )
186
+ }
187
+
188
+ const applied = []
189
+ for (const f of jsFiles) {
190
+ const data = await fs.readFile(path.join(snapDir, f), 'utf-8')
191
+ await fs.writeFile(path.join(SRC_DIR, f), data, 'utf-8')
192
+ applied.push(f)
193
+ }
194
+
195
+ console.log(chalk.green(`\n✅ Применено: ${applied.length} файл(ов)`))
196
+ console.log(chalk.gray(applied.join(', ')))
197
+ console.log(chalk.gray(`Бэкап: ${backupDir}`))
198
+ console.log(
199
+ chalk.yellow(`\nПерезапусти агента, чтобы изменения вступили в силу.\n`),
200
+ )
201
+ }
202
+
203
+ // ---------- /self-list ----------
204
+
205
+ export async function selfList({ config }) {
206
+ const snapRoot = path.join(SNAP_ROOT)
207
+ let entries = []
208
+ try {
209
+ entries = await fs.readdir(snapRoot)
210
+ } catch {
211
+ console.log(chalk.gray('Снапшотов нет.'))
212
+ return
213
+ }
214
+
215
+ if (!entries.length) {
216
+ console.log(chalk.gray('Снапшотов нет.'))
217
+ return
218
+ }
219
+
220
+ entries.sort()
221
+ console.log(chalk.gray('Снапшоты самообзора:'))
222
+ for (const e of entries) {
223
+ const full = path.join(snapRoot, e)
224
+ const stat = await fs.stat(full).catch(() => null)
225
+ if (!stat || !stat.isDirectory()) continue
226
+ const report = path.join(full, '_report.md')
227
+ const hasReport = await fs
228
+ .stat(report)
229
+ .then(() => true)
230
+ .catch(() => false)
231
+ const changedCount = (await diffFiles(SRC_DIR, full)).length
232
+ const tag = changedCount
233
+ ? chalk.green(`[${changedCount} изменено]`)
234
+ : chalk.gray('[без правок]')
235
+ const rep = hasReport ? chalk.cyan('📋') : ' '
236
+ console.log(` ${rep} ${e} ${tag}`)
237
+ }
238
+ console.log()
239
+ console.log(chalk.gray('Команды: /self-diff <name>, /self-apply <name>\n'))
240
+ }
241
+
242
+ // ---------- helpers ----------
243
+
244
+ async function diffFiles(origDir, newDir) {
245
+ const files = await fs.readdir(newDir).catch(() => [])
246
+ const changed = []
247
+ for (const f of files) {
248
+ if (!f.endsWith('.js')) continue
249
+ const a = await fs.readFile(path.join(origDir, f), 'utf-8').catch(() => '')
250
+ const b = await fs.readFile(path.join(newDir, f), 'utf-8').catch(() => '')
251
+ if (a !== b) changed.push(f)
252
+ }
253
+ return changed
254
+ }
255
+
256
+ function buildReviewPrompt({ focus, snapDir }) {
257
+ const focusLine = focus
258
+ ? `\nThe user asked you to focus especially on: ${focus}\n`
259
+ : ''
260
+
261
+ 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.
262
+
263
+ All source files are in your working directory: ${snapDir}
264
+ ${focusLine}
265
+ ## Your job
266
+
267
+ 1. Read EVERY .js file in your working directory. Use Glob("**/*.js") to list them, then Read each one. Do not skip files.
268
+ 2. Identify real, concrete problems. Focus on:
269
+ - Race conditions and async/promise bugs (missing await, unhandled rejections, timing issues)
270
+ - Error handling gaps: places that can throw and crash the agent
271
+ - JSON parsing edge cases in agent-loop.js (LLM output is unreliable)
272
+ - Fragile DOM selectors in browser.js (DeepSeek changes layout)
273
+ - Cross-platform issues (Windows paths, cmd.exe vs bash, encoding)
274
+ - Undo/transcript consistency: could undo leave files in bad state?
275
+ - Self-review mode safety: could /self-apply corrupt src/?
276
+ - Anything that breaks state between interactive tasks (chat context, workdir, spinner)
277
+ 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.
278
+ 4. Do NOT invent problems. If you're not sure, mention it in the report, don't change it.
279
+ 5. Do NOT touch package.json, node_modules, or files outside the working directory.
280
+
281
+ ## Constraints
282
+
283
+ - 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.
284
+ - Always read a file before editing it.
285
+ - After all edits, run \`node --check\` on every file you changed. If any fails, fix the syntax error.
286
+
287
+ ## Report format
288
+
289
+ When done, respond with a markdown report as the message:
290
+
291
+ ## Найдено
292
+ - (bullet list of issues you found, with file:line if possible)
293
+
294
+ ## Исправлено
295
+ - (bullet list: file — что именно изменил)
296
+
297
+ ## Не исправлено (осознанно)
298
+ - (bullet list: what you left alone and why)
299
+
300
+ ## Проверка
301
+ - (bullet list: what you verified, e.g. \`node --check\` results)
302
+
303
+ Be honest and specific. If the code is fine in some area, say so.`
304
+ }
package/src/spinner.js ADDED
@@ -0,0 +1,55 @@
1
+ import ora from 'ora'
2
+ import chalk from 'chalk'
3
+
4
+ export function createSpinner() {
5
+ let spinner = null
6
+
7
+ const start = (text) => {
8
+ if (!spinner) spinner = ora(text).start()
9
+ else spinner.start(text)
10
+ }
11
+
12
+ const stop = () => {
13
+ if (spinner) spinner.stop()
14
+ }
15
+
16
+ const succeed = (text) => {
17
+ if (spinner) spinner.succeed(text)
18
+ }
19
+
20
+ const fail = (text) => {
21
+ if (spinner) spinner.fail(text)
22
+ }
23
+
24
+ return {
25
+ // Единственный метод, который ЗАПУСКАЕТ спиннер
26
+ thinking: () => start(chalk.gray('Думаю...')),
27
+
28
+ // Оба метода только останавливают и печатают — не перезапускают
29
+ toolCall: (name, args) => {
30
+ stop()
31
+ const preview = JSON.stringify(args).slice(0, 120)
32
+ console.log(chalk.yellow(`🔧 ${name}`), chalk.gray(preview))
33
+ },
34
+
35
+ toolResult: (result) => {
36
+ stop()
37
+ const text = typeof result === 'string' ? result : JSON.stringify(result)
38
+ const preview = text.slice(0, 200).replace(/\n/g, ' ↵ ')
39
+ console.log(chalk.gray(` → ${preview}\n`))
40
+ },
41
+
42
+ assistant: (msg) => {
43
+ stop()
44
+ console.log(chalk.green('\n✅ ' + msg) + '\n')
45
+ },
46
+
47
+ taskHeader: (task) => {
48
+ console.log(chalk.bold.cyan(`\n🎯 Задача: ${task}\n`))
49
+ },
50
+
51
+ stop,
52
+ succeed,
53
+ fail,
54
+ }
55
+ }
@@ -0,0 +1,84 @@
1
+ export function buildSystemPrompt({ workdir, tools, gitContext = null }) {
2
+ const toolDescriptions = tools
3
+ .map(
4
+ (t) =>
5
+ `### ${t.name}\n${t.description}\nПараметры: ${JSON.stringify(t.parameters)}`,
6
+ )
7
+ .join('\n\n')
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.
14
+
15
+ You work in the directory: ${workdir}
16
+
17
+ You have access to the following tools:
18
+
19
+ ${toolDescriptions}
20
+ ${gitSection}
21
+ ## How to use tools
22
+
23
+ To call ONE tool, respond with ONLY a JSON object (no markdown fences, no extra text):
24
+
25
+ {"tool": "tool_name", "args": {"param1": "value1"}}
26
+
27
+ To call MULTIPLE tools at once (only if they are independent — e.g. reading several files), respond with a JSON array:
28
+
29
+ [{"tool": "Read", "args": {"path": "a.js"}}, {"tool": "Read", "args": {"path": "b.js"}}]
30
+
31
+ After the tool result(s) come back, decide the next action.
32
+
33
+ To give a final answer to the user, respond with:
34
+
35
+ {"tool": "respond", "args": {"message": "your final answer here"}}
36
+
37
+ ## Git
38
+
39
+ - You CAN commit and push using GitAdd / GitCommit / GitPush. Prefer these over raw \`git\` through Bash.
40
+ - Do NOT run \`git push --force\`, \`git reset --hard\`, \`git clean -fd\`, or rewrite history (rebase, amend published commits) unless the user explicitly asks.
41
+ - Do NOT commit or push unless the user asked for it, or the task clearly implies it.
42
+ - Before committing, check GitStatus and GitDiff so you know what goes in.
43
+ - Commit messages: short imperative subject line, then optional body. Use \`git commit\` style, not emoji.
44
+ - If \`git push\` fails due to credentials or non-fast-forward, report it to the user and stop — do NOT try to work around it.
45
+
46
+ ## CRITICAL: JSON escaping
47
+
48
+ Your response MUST be valid JSON. This means:
49
+ - Backslashes MUST be escaped: "C:\\\\Users\\\\name\\\\project"
50
+ - OR use forward slashes for Windows paths: "C:/Users/name/project"
51
+ - Quotes inside strings MUST be escaped: \\"
52
+ - Newlines inside strings must be \\n
53
+
54
+ When in doubt — use forward slashes for all file paths.
55
+
56
+ ## CRITICAL: quoting inside shell commands
57
+
58
+ When you call Bash, put the whole shell command in a single JSON string.
59
+ Inside that string:
60
+ - Escape any double quote as \\"
61
+ - Escape any backslash as \\\\
62
+ - OR avoid quotes entirely. For Windows paths use forward slashes.
63
+ - Pipes, redirects and chained commands work: "cd dir && dir /b"
64
+
65
+ ## Rules
66
+
67
+ - Always read a file before editing it.
68
+ - Prefer targeted edits over rewriting entire files.
69
+ - Run tests or commands to verify your changes when possible.
70
+ - If a tool call fails, read the error and try a different approach.
71
+ - Keep responses concise. The user sees the terminal output.
72
+ - Never output anything except JSON when you want to call a tool.
73
+ - Your ENTIRE response must be exactly one JSON object (or array of objects).
74
+
75
+ REMINDER: respond with exactly one JSON object or array. No text, no markdown, no explanations.
76
+ ## Web access
77
+
78
+ - You have WebSearch and WebFetch. Use them when the answer requires current information (versions, changelogs, recent bugs, docs).
79
+ - WebSearch uses DuckDuckGo. If results look stale or missing, try a different phrasing.
80
+ - WebFetch strips HTML. If a page is JS-rendered and text comes back empty, call WebFetch again with render=true.
81
+ - Do NOT fetch the same URL repeatedly. If content is missing, report that instead.
82
+ - Do NOT use WebFetch on localhost, private IPs, or 'file://' — it will fail.
83
+ `
84
+ }
package/src/tools.js ADDED
@@ -0,0 +1,206 @@
1
+ import fs from 'fs/promises'
2
+ import path from 'path'
3
+ import { exec } from 'child_process'
4
+ import { createGitTools } from './gitTools.js'
5
+ import { createWebTools } from './web.js'
6
+
7
+ export function createTools(workdir, { undo } = {}) {
8
+ const root = path.resolve(workdir)
9
+ const safe = (p) => {
10
+ const resolved = path.resolve(root, p)
11
+ // startsWith(root) пропускал бы соседние пути с общим префиксом
12
+ // (C:\work\proj vs C:\work\proj-old). Считаем через relative().
13
+ const rel = path.relative(root, resolved)
14
+ if (rel.startsWith('..') || path.isAbsolute(rel)) {
15
+ throw new Error(`Доступ за пределы рабочей директории: ${p}`)
16
+ }
17
+ return resolved
18
+ }
19
+
20
+ // Sandbox (вариант A): не даём команде выйти выше root.
21
+ // Это защитный барьер, а не полноценная изоляция ОС.
22
+ const assertCommandInsideRoot = (command) => {
23
+ const cmd = String(command || '')
24
+ const cdRe = /(?:^|[;&|]|\s)(?:cd|pushd)\s+([^;&|]+)/gi
25
+ let m
26
+ while ((m = cdRe.exec(cmd))) {
27
+ const raw = m[1].trim()
28
+ if (!raw || raw === '-') continue
29
+ const target = path.resolve(root, raw)
30
+ const rel = path.relative(root, target)
31
+ if (rel.startsWith('..') || path.isAbsolute(rel)) {
32
+ throw new Error('Sandbox: выход за пределы ' + root + ' запрещён (cd ' + raw + ')')
33
+ }
34
+ }
35
+ }
36
+ const runShell = (command, timeout = 30_000) =>
37
+ new Promise((resolve) => {
38
+ const options = {
39
+ cwd: workdir,
40
+ timeout,
41
+ maxBuffer: 1024 * 1024 * 8,
42
+ windowsHide: true,
43
+ env: { ...process.env },
44
+ }
45
+
46
+ if (process.platform === 'win32') {
47
+ options.shell = process.env.ComSpec || 'C:\\Windows\\System32\\cmd.exe'
48
+ } else {
49
+ options.shell = '/bin/sh'
50
+ }
51
+
52
+ exec(command, options, (err, stdout, stderr) => {
53
+ const out = (stdout || '').toString()
54
+ const errStr = (stderr || '').toString()
55
+
56
+ if (!err) {
57
+ const combined = (out + errStr).trim()
58
+ resolve(combined || '(команда выполнена без вывода)')
59
+ return
60
+ }
61
+
62
+ const parts = []
63
+
64
+ if (err.killed) {
65
+ parts.push(`⏱ Таймаут после ${timeout}ms — процесс убит.`)
66
+ } else if (err.code !== undefined && err.code !== null) {
67
+ parts.push(`Exit code: ${err.code}`)
68
+ } else if (err.signal) {
69
+ parts.push(`Убит сигналом: ${err.signal}`)
70
+ } else {
71
+ parts.push(`Ошибка: ${err.message}`)
72
+ }
73
+
74
+ if (out.trim()) parts.push(`stdout:\n${out.trim()}`)
75
+ if (errStr.trim()) parts.push(`stderr:\n${errStr.trim()}`)
76
+
77
+ resolve(parts.join('\n'))
78
+ })
79
+ })
80
+
81
+ const baseTools = [
82
+ {
83
+ name: 'Read',
84
+ description:
85
+ 'Прочитать содержимое файла. Опционально: offset и limit (строки).',
86
+ parameters: { path: 'string', offset: 'number?', limit: 'number?' },
87
+ fn: async ({ path: p, offset, limit }) => {
88
+ const file = safe(p)
89
+ const content = await fs.readFile(file, 'utf-8')
90
+ const lines = content.split('\n')
91
+ const start = offset ?? 0
92
+ const end = limit ? start + limit : lines.length
93
+ return lines.slice(start, end).join('\n')
94
+ },
95
+ },
96
+
97
+ {
98
+ name: 'Write',
99
+ description: 'Создать или перезаписать файл.',
100
+ parameters: { path: 'string', content: 'string' },
101
+ fn: async ({ path: p, content }) => {
102
+ const file = safe(p)
103
+ if (undo) await undo.backup(file)
104
+ await fs.mkdir(path.dirname(file), { recursive: true })
105
+ await fs.writeFile(file, content, 'utf-8')
106
+ return `Файл записан: ${p}`
107
+ },
108
+ },
109
+
110
+ {
111
+ name: 'Edit',
112
+ description:
113
+ 'Точечная замена строки в файле. old_string должен встречаться один раз.',
114
+ parameters: {
115
+ path: 'string',
116
+ old_string: 'string',
117
+ new_string: 'string',
118
+ },
119
+ fn: async ({ path: p, old_string, new_string }) => {
120
+ const file = safe(p)
121
+ if (typeof old_string !== 'string' || old_string === '') {
122
+ throw new Error('old_string пустой — нечего заменять.')
123
+ }
124
+ let content = await fs.readFile(file, 'utf-8')
125
+ const occurrences = content.split(old_string).length - 1
126
+ if (occurrences === 0) {
127
+ throw new Error(
128
+ `Строка не найдена в ${p}: "${old_string.slice(0, 60)}..."`,
129
+ )
130
+ }
131
+ if (occurrences > 1) {
132
+ throw new Error(
133
+ `Строка встречается ${occurrences} раз в ${p}. Уточните old_string.`,
134
+ )
135
+ }
136
+ if (undo) await undo.backup(file)
137
+ content = content.replace(old_string, new_string)
138
+ await fs.writeFile(file, content, 'utf-8')
139
+ return `Отредактирован: ${p}`
140
+ },
141
+ },
142
+
143
+ {
144
+ name: 'Bash',
145
+ description:
146
+ 'Выполнить shell-команду в рабочей директории (cmd.exe на Windows, sh на Linux/macOS). ' +
147
+ 'Не использовать для long-running процессов (серверы) — уйдут в таймаут. ' +
148
+ 'Не использовать для команд, требующих интерактивного ввода. ' +
149
+ 'Для git — инструменты Git*. Для интернета — WebFetch / WebSearch.',
150
+ parameters: { command: 'string', timeout: 'number?' },
151
+ fn: async ({ command, timeout }) => {
152
+ assertCommandInsideRoot(command)
153
+ return runShell(command, timeout)
154
+ },
155
+ },
156
+
157
+ {
158
+ name: 'Glob',
159
+ description: 'Найти файлы по glob-паттерну (например, "**/*.js").',
160
+ parameters: { pattern: 'string' },
161
+ fn: async ({ pattern }) => {
162
+ const { glob } = await import('fs/promises')
163
+ const results = []
164
+ for await (const f of glob(pattern, { cwd: workdir })) {
165
+ // Sandbox: игнорируем всё, что выходит за пределы root.
166
+ const abs = path.resolve(workdir, f)
167
+ const rel = path.relative(root, abs)
168
+ if (rel.startsWith('..') || path.isAbsolute(rel)) continue
169
+ results.push(f)
170
+ }
171
+ return results.length ? results.join('\n') : 'Ничего не найдено.'
172
+ },
173
+ },
174
+
175
+ {
176
+ name: 'Grep',
177
+ description: 'Поиск по содержимому файлов (регулярное выражение).',
178
+ parameters: { pattern: 'string', path: 'string?' },
179
+ fn: async ({ pattern, path: searchPath }) => {
180
+ const target = searchPath ? safe(searchPath) : workdir
181
+ if (process.platform === 'win32') {
182
+ const escaped = pattern.replace(/"/g, '\\"')
183
+ const scope = searchPath
184
+ ? '"' + target + '\\*'
185
+ : '*'
186
+ return runShell(`findstr /s /n /r /c:"${escaped}" ` + scope)
187
+ }
188
+ return runShell(
189
+ `grep -rn -E ${JSON.stringify(pattern)} ${JSON.stringify(target)} || true`,
190
+ )
191
+ },
192
+ },
193
+ ]
194
+
195
+ const gitTools = createGitTools(workdir)
196
+ const webTools = createWebTools()
197
+
198
+ const respondTool = {
199
+ name: 'respond',
200
+ description: 'Дать финальный ответ пользователю и завершить задачу.',
201
+ parameters: { message: 'string' },
202
+ fn: async ({ message }) => message,
203
+ }
204
+
205
+ return [...baseTools, ...gitTools, ...webTools, respondTool]
206
+ }
@@ -0,0 +1,46 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+
4
+ export class Transcript {
5
+ constructor({ dir, enabled = true, sessionName = 'session' } = {}) {
6
+ this.enabled = enabled
7
+ this.file = null
8
+ this.stream = null
9
+ this.startedAt = Date.now()
10
+
11
+ if (!enabled) return
12
+
13
+ try {
14
+ fs.mkdirSync(dir, { recursive: true })
15
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-')
16
+ this.file = path.join(dir, `${sessionName}-${stamp}.jsonl`)
17
+ this.stream = fs.createWriteStream(this.file, { flags: 'a' })
18
+ // Ошибки записи (диск переполнен, файл удалён и т.п.) приходят
19
+ // событием 'error'; без слушателя это uncaught exception.
20
+ this.stream.on('error', (e) => {
21
+ console.error(`transcript: ошибка записи: ${e.message}`)
22
+ this.enabled = false
23
+ })
24
+ } catch (e) {
25
+ console.error(`transcript: не удалось создать файл: ${e.message}`)
26
+ this.enabled = false
27
+ }
28
+ }
29
+
30
+ log(type, data = {}) {
31
+ if (!this.enabled || !this.stream) return
32
+ const entry = {
33
+ ts: new Date().toISOString(),
34
+ elapsed: Date.now() - this.startedAt,
35
+ type,
36
+ ...data,
37
+ }
38
+ try {
39
+ this.stream.write(JSON.stringify(entry) + '\n')
40
+ } catch {}
41
+ }
42
+
43
+ close() {
44
+ if (this.stream) this.stream.end()
45
+ }
46
+ }