zames_pro 1.2.0 → 2.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.
- package/dist/agent-loop.js +652 -0
- package/dist/agent-loop.js.map +1 -0
- package/dist/browser.js +573 -0
- package/dist/browser.js.map +1 -0
- package/dist/config.js +74 -0
- package/dist/config.js.map +1 -0
- package/dist/confirm.js +82 -0
- package/dist/confirm.js.map +1 -0
- package/dist/diff.js +38 -0
- package/dist/diff.js.map +1 -0
- package/dist/gitTools.js +219 -0
- package/dist/gitTools.js.map +1 -0
- package/dist/index.js +1365 -0
- package/dist/index.js.map +1 -0
- package/dist/input.js +524 -0
- package/dist/input.js.map +1 -0
- package/dist/markdown.js +48 -0
- package/dist/markdown.js.map +1 -0
- package/dist/self-review.js +281 -0
- package/dist/self-review.js.map +1 -0
- package/dist/sessions.js +123 -0
- package/dist/sessions.js.map +1 -0
- package/dist/spinner.js +138 -0
- package/dist/spinner.js.map +1 -0
- package/{src → dist}/system-prompt.js +10 -14
- package/dist/system-prompt.js.map +1 -0
- package/{src → dist}/theme.js +17 -19
- package/dist/theme.js.map +1 -0
- package/dist/tools.js +189 -0
- package/dist/tools.js.map +1 -0
- package/dist/transcript.js +51 -0
- package/dist/transcript.js.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/dist/undo.js +100 -0
- package/dist/undo.js.map +1 -0
- package/dist/web.js +220 -0
- package/dist/web.js.map +1 -0
- package/dist/xml-toolcall.js +68 -0
- package/dist/xml-toolcall.js.map +1 -0
- package/package.json +13 -5
- package/src/agent-loop.js +0 -575
- 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 -1461
- package/src/input.js +0 -447
- package/src/markdown.js +0 -49
- package/src/self-review.js +0 -319
- 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
package/src/self-review.js
DELETED
|
@@ -1,319 +0,0 @@
|
|
|
1
|
-
import path from 'path'
|
|
2
|
-
import fs from 'fs/promises'
|
|
3
|
-
import { theme } from './theme.js'
|
|
4
|
-
import { fileURLToPath } from 'url'
|
|
5
|
-
|
|
6
|
-
import { ZAMES_HOME } from './config.js'
|
|
7
|
-
|
|
8
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
9
|
-
const SRC_DIR = __dirname
|
|
10
|
-
const SNAP_ROOT = path.join(ZAMES_HOME, 'snapshots')
|
|
11
|
-
|
|
12
|
-
// Динамический импорт логики с timestamp — чтобы при /reload (или авто-reload)
|
|
13
|
-
// self-review использовал СВЕЖИЕ tools/agent-loop, а не закэшированные при
|
|
14
|
-
// первой загрузке. Иначе reload не доходил бы до зависимостей self-review.
|
|
15
|
-
async function loadFresh() {
|
|
16
|
-
const stamp = Date.now()
|
|
17
|
-
const toolsUrl = new URL('./tools.js', import.meta.url)
|
|
18
|
-
toolsUrl.searchParams.set('t', String(stamp))
|
|
19
|
-
const loopUrl = new URL('./agent-loop.js', import.meta.url)
|
|
20
|
-
loopUrl.searchParams.set('t', String(stamp))
|
|
21
|
-
const [{ createTools }, { runAgentLoop }] = await Promise.all([
|
|
22
|
-
import(toolsUrl.href),
|
|
23
|
-
import(loopUrl.href),
|
|
24
|
-
])
|
|
25
|
-
return { createTools, runAgentLoop }
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async function ensureDir(p) {
|
|
29
|
-
await fs.mkdir(p, { recursive: true })
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async function copyDirJsFiles(from, to) {
|
|
33
|
-
await ensureDir(to)
|
|
34
|
-
const entries = await fs.readdir(from)
|
|
35
|
-
const copied = []
|
|
36
|
-
for (const e of entries) {
|
|
37
|
-
if (!e.endsWith('.js')) continue
|
|
38
|
-
const data = await fs.readFile(path.join(from, e), 'utf-8')
|
|
39
|
-
await fs.writeFile(path.join(to, e), data, 'utf-8')
|
|
40
|
-
copied.push(e)
|
|
41
|
-
}
|
|
42
|
-
return copied
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// ---------- /self-review ----------
|
|
47
|
-
|
|
48
|
-
export async function selfReview({ browser, config, focus, transcript }) {
|
|
49
|
-
const stamp = new Date().toISOString().replace(/[:.]/g, '-')
|
|
50
|
-
const snapRoot = path.join(SNAP_ROOT)
|
|
51
|
-
const snapDir = path.join(snapRoot, `run-${stamp}`)
|
|
52
|
-
await ensureDir(snapRoot)
|
|
53
|
-
await ensureDir(snapDir)
|
|
54
|
-
|
|
55
|
-
const copied = await copyDirJsFiles(SRC_DIR, snapDir)
|
|
56
|
-
|
|
57
|
-
if (copied.length === 0) {
|
|
58
|
-
console.error(
|
|
59
|
-
theme.error(
|
|
60
|
-
`\n✖ Самообзор отменён: в ${SRC_DIR} нет .js файлов.\n` +
|
|
61
|
-
`Проверь, что src/ не пуст и ты запускаешь агента из корня проекта.\n`,
|
|
62
|
-
),
|
|
63
|
-
)
|
|
64
|
-
throw new Error('SRC_DIR пуст — нечего ревьюить')
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Копируем package.json для контекста — чтобы агент видел зависимости
|
|
68
|
-
try {
|
|
69
|
-
const pkg = await fs.readFile(
|
|
70
|
-
path.resolve(SRC_DIR, '..', 'package.json'),
|
|
71
|
-
'utf-8',
|
|
72
|
-
)
|
|
73
|
-
await fs.writeFile(path.join(snapDir, 'package.json'), pkg, 'utf-8')
|
|
74
|
-
} catch {}
|
|
75
|
-
|
|
76
|
-
console.log(theme.system(`\n📸 Снапшот: ${snapDir}`))
|
|
77
|
-
console.log(theme.system(`Файлов: ${copied.length} — ${copied.join(', ')}`))
|
|
78
|
-
console.log(theme.system('Начинаю самообзор...\n'))
|
|
79
|
-
|
|
80
|
-
const taskPrompt = buildReviewPrompt({ focus, snapDir })
|
|
81
|
-
|
|
82
|
-
const { createTools, runAgentLoop } = await loadFresh()
|
|
83
|
-
const tools = createTools(snapDir, { undo: null })
|
|
84
|
-
let finalMessage = ''
|
|
85
|
-
|
|
86
|
-
await runAgentLoop({
|
|
87
|
-
browser,
|
|
88
|
-
tools,
|
|
89
|
-
task: taskPrompt,
|
|
90
|
-
workdir: snapDir,
|
|
91
|
-
maxIterations: 80,
|
|
92
|
-
freshChat: true,
|
|
93
|
-
sendSystemPrompt: true,
|
|
94
|
-
transcript,
|
|
95
|
-
onThinking: () => {},
|
|
96
|
-
onToolCall: (name, args) => {
|
|
97
|
-
const preview = JSON.stringify(args).slice(0, 120)
|
|
98
|
-
console.log(theme.warn(`🔧 ${name}`), theme.system(preview))
|
|
99
|
-
},
|
|
100
|
-
onToolResult: (r) => {
|
|
101
|
-
const t = typeof r === 'string' ? r : JSON.stringify(r)
|
|
102
|
-
console.log(theme.system(` → ${t.slice(0, 200).replace(/\n/g, ' ↵ ')}\n`))
|
|
103
|
-
},
|
|
104
|
-
onAssistantMessage: (msg) => {
|
|
105
|
-
finalMessage = msg
|
|
106
|
-
console.log(theme.assistant('\n📋 Отчёт:\n'))
|
|
107
|
-
console.log(msg)
|
|
108
|
-
console.log()
|
|
109
|
-
},
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
// Сохраняем отчёт
|
|
113
|
-
const reportPath = path.join(snapDir, '_report.md')
|
|
114
|
-
await fs.writeFile(reportPath, finalMessage || '(пусто)', 'utf-8')
|
|
115
|
-
|
|
116
|
-
// Считаем, что изменилось
|
|
117
|
-
const changed = await diffFiles(SRC_DIR, snapDir)
|
|
118
|
-
|
|
119
|
-
console.log(theme.system('─'.repeat(60)))
|
|
120
|
-
console.log(theme.system(`Отчёт: ${reportPath}`))
|
|
121
|
-
console.log(theme.system(`Снапшот: ${snapDir}`))
|
|
122
|
-
if (changed.length) {
|
|
123
|
-
console.log(
|
|
124
|
-
theme.assistant(
|
|
125
|
-
`Изменено: ${changed.length} файл(ов): ${changed.join(', ')}`,
|
|
126
|
-
),
|
|
127
|
-
)
|
|
128
|
-
} else {
|
|
129
|
-
console.log(theme.system('Изменено: (ничего — только отчёт)'))
|
|
130
|
-
}
|
|
131
|
-
console.log(theme.user(`\nДальше:`))
|
|
132
|
-
console.log(
|
|
133
|
-
theme.user(
|
|
134
|
-
` /self-diff ${path.basename(snapDir)} — посмотреть различия`,
|
|
135
|
-
),
|
|
136
|
-
)
|
|
137
|
-
console.log(
|
|
138
|
-
theme.user(
|
|
139
|
-
` /self-apply ${path.basename(snapDir)} — применить к живому src/`,
|
|
140
|
-
),
|
|
141
|
-
)
|
|
142
|
-
console.log()
|
|
143
|
-
|
|
144
|
-
return { snapDir, reportPath, changed }
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// ---------- /self-diff ----------
|
|
148
|
-
|
|
149
|
-
export async function selfDiff({ config, name }) {
|
|
150
|
-
const snapDir = path.join(SNAP_ROOT, name)
|
|
151
|
-
const snapStat = await fs.stat(snapDir).catch(() => null)
|
|
152
|
-
if (!snapStat) throw new Error(`Снапшот не найден: ${snapDir}`)
|
|
153
|
-
|
|
154
|
-
const { unifiedDiff, colorDiff } = await import('./diff.js')
|
|
155
|
-
const files = await fs.readdir(snapDir)
|
|
156
|
-
let anyDiff = false
|
|
157
|
-
|
|
158
|
-
for (const f of files) {
|
|
159
|
-
if (!f.endsWith('.js')) continue
|
|
160
|
-
const orig = await fs
|
|
161
|
-
.readFile(path.join(SRC_DIR, f), 'utf-8')
|
|
162
|
-
.catch(() => '')
|
|
163
|
-
const next = await fs
|
|
164
|
-
.readFile(path.join(snapDir, f), 'utf-8')
|
|
165
|
-
.catch(() => '')
|
|
166
|
-
if (orig === next) continue
|
|
167
|
-
|
|
168
|
-
anyDiff = true
|
|
169
|
-
console.log(theme.bold(`\n=== src/${f} ===`))
|
|
170
|
-
const diff = unifiedDiff(orig, next, { label: f })
|
|
171
|
-
console.log(colorDiff(diff))
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (!anyDiff) {
|
|
175
|
-
console.log(theme.system('Различий нет.'))
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// ---------- /self-apply ----------
|
|
180
|
-
|
|
181
|
-
export async function selfApply({ config, name }) {
|
|
182
|
-
const snapDir = path.join(SNAP_ROOT, name)
|
|
183
|
-
const snapStat = await fs.stat(snapDir).catch(() => null)
|
|
184
|
-
if (!snapStat) throw new Error(`Снапшот не найден: ${snapDir}`)
|
|
185
|
-
|
|
186
|
-
// Бэкап текущего src перед перезаписью
|
|
187
|
-
const stamp = new Date().toISOString().replace(/[:.]/g, '-')
|
|
188
|
-
const backupDir = path.join(
|
|
189
|
-
SNAP_ROOT,
|
|
190
|
-
`backup-before-apply-${stamp}`,
|
|
191
|
-
)
|
|
192
|
-
await copyDirJsFiles(SRC_DIR, backupDir)
|
|
193
|
-
|
|
194
|
-
const files = await fs.readdir(snapDir)
|
|
195
|
-
const jsFiles = files.filter((f) => f.endsWith('.js'))
|
|
196
|
-
|
|
197
|
-
if (jsFiles.length === 0) {
|
|
198
|
-
throw new Error(
|
|
199
|
-
`В снапшоте ${snapDir} нет .js файлов. Apply отменён, чтобы не стирать src/.`,
|
|
200
|
-
)
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const applied = []
|
|
204
|
-
for (const f of jsFiles) {
|
|
205
|
-
const data = await fs.readFile(path.join(snapDir, f), 'utf-8')
|
|
206
|
-
await fs.writeFile(path.join(SRC_DIR, f), data, 'utf-8')
|
|
207
|
-
applied.push(f)
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
console.log(theme.assistant(`\n✅ Применено: ${applied.length} файл(ов)`))
|
|
211
|
-
console.log(theme.system(applied.join(', ')))
|
|
212
|
-
console.log(theme.system(`Бэкап: ${backupDir}`))
|
|
213
|
-
console.log(
|
|
214
|
-
theme.warn(`\nПерезапусти агента, чтобы изменения вступили в силу.\n`),
|
|
215
|
-
)
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// ---------- /self-list ----------
|
|
219
|
-
|
|
220
|
-
export async function selfList({ config }) {
|
|
221
|
-
const snapRoot = path.join(SNAP_ROOT)
|
|
222
|
-
let entries = []
|
|
223
|
-
try {
|
|
224
|
-
entries = await fs.readdir(snapRoot)
|
|
225
|
-
} catch {
|
|
226
|
-
console.log(theme.system('Снапшотов нет.'))
|
|
227
|
-
return
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (!entries.length) {
|
|
231
|
-
console.log(theme.system('Снапшотов нет.'))
|
|
232
|
-
return
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
entries.sort()
|
|
236
|
-
console.log(theme.system('Снапшоты самообзора:'))
|
|
237
|
-
for (const e of entries) {
|
|
238
|
-
const full = path.join(snapRoot, e)
|
|
239
|
-
const stat = await fs.stat(full).catch(() => null)
|
|
240
|
-
if (!stat || !stat.isDirectory()) continue
|
|
241
|
-
const report = path.join(full, '_report.md')
|
|
242
|
-
const hasReport = await fs
|
|
243
|
-
.stat(report)
|
|
244
|
-
.then(() => true)
|
|
245
|
-
.catch(() => false)
|
|
246
|
-
const changedCount = (await diffFiles(SRC_DIR, full)).length
|
|
247
|
-
const tag = changedCount
|
|
248
|
-
? theme.assistant(`[${changedCount} изменено]`)
|
|
249
|
-
: theme.system('[без правок]')
|
|
250
|
-
const rep = hasReport ? theme.user('📋') : ' '
|
|
251
|
-
console.log(` ${rep} ${e} ${tag}`)
|
|
252
|
-
}
|
|
253
|
-
console.log()
|
|
254
|
-
console.log(theme.system('Команды: /self-diff <name>, /self-apply <name>\n'))
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// ---------- helpers ----------
|
|
258
|
-
|
|
259
|
-
async function diffFiles(origDir, newDir) {
|
|
260
|
-
const files = await fs.readdir(newDir).catch(() => [])
|
|
261
|
-
const changed = []
|
|
262
|
-
for (const f of files) {
|
|
263
|
-
if (!f.endsWith('.js')) continue
|
|
264
|
-
const a = await fs.readFile(path.join(origDir, f), 'utf-8').catch(() => '')
|
|
265
|
-
const b = await fs.readFile(path.join(newDir, f), 'utf-8').catch(() => '')
|
|
266
|
-
if (a !== b) changed.push(f)
|
|
267
|
-
}
|
|
268
|
-
return changed
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function buildReviewPrompt({ focus, snapDir }) {
|
|
272
|
-
const focusLine = focus
|
|
273
|
-
? `\nThe user asked you to focus especially on: ${focus}\n`
|
|
274
|
-
: ''
|
|
275
|
-
|
|
276
|
-
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.
|
|
277
|
-
|
|
278
|
-
All source files are in your working directory: ${snapDir}
|
|
279
|
-
${focusLine}
|
|
280
|
-
## Your job
|
|
281
|
-
|
|
282
|
-
1. Read EVERY .js file in your working directory. Use Glob("**/*.js") to list them, then Read each one. Do not skip files.
|
|
283
|
-
2. Identify real, concrete problems. Focus on:
|
|
284
|
-
- Race conditions and async/promise bugs (missing await, unhandled rejections, timing issues)
|
|
285
|
-
- Error handling gaps: places that can throw and crash the agent
|
|
286
|
-
- JSON parsing edge cases in agent-loop.js (LLM output is unreliable)
|
|
287
|
-
- Fragile DOM selectors in browser.js (DeepSeek changes layout)
|
|
288
|
-
- Cross-platform issues (Windows paths, cmd.exe vs bash, encoding)
|
|
289
|
-
- Undo/transcript consistency: could undo leave files in bad state?
|
|
290
|
-
- Self-review mode safety: could /self-apply corrupt src/?
|
|
291
|
-
- Anything that breaks state between interactive tasks (chat context, workdir, spinner)
|
|
292
|
-
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.
|
|
293
|
-
4. Do NOT invent problems. If you're not sure, mention it in the report, don't change it.
|
|
294
|
-
5. Do NOT touch package.json, node_modules, or files outside the working directory.
|
|
295
|
-
|
|
296
|
-
## Constraints
|
|
297
|
-
|
|
298
|
-
- 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.
|
|
299
|
-
- Always read a file before editing it.
|
|
300
|
-
- After all edits, run \`node --check\` on every file you changed. If any fails, fix the syntax error.
|
|
301
|
-
|
|
302
|
-
## Report format
|
|
303
|
-
|
|
304
|
-
When done, respond with a markdown report as the message:
|
|
305
|
-
|
|
306
|
-
## Найдено
|
|
307
|
-
- (bullet list of issues you found, with file:line if possible)
|
|
308
|
-
|
|
309
|
-
## Исправлено
|
|
310
|
-
- (bullet list: file — что именно изменил)
|
|
311
|
-
|
|
312
|
-
## Не исправлено (осознанно)
|
|
313
|
-
- (bullet list: what you left alone and why)
|
|
314
|
-
|
|
315
|
-
## Проверка
|
|
316
|
-
- (bullet list: what you verified, e.g. \`node --check\` results)
|
|
317
|
-
|
|
318
|
-
Be honest and specific. If the code is fine in some area, say so.`
|
|
319
|
-
}
|
package/src/spinner.js
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import ora from 'ora'
|
|
2
|
-
import { theme } from './theme.js'
|
|
3
|
-
import { renderMarkdown } from './markdown.js'
|
|
4
|
-
|
|
5
|
-
// Фразы для спиннера «агент работает». Выбираются в случайном порядке.
|
|
6
|
-
const THINKING_PHRASES = [
|
|
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
|
-
]
|
|
38
|
-
|
|
39
|
-
export function randomThinkingPhrase() {
|
|
40
|
-
return THINKING_PHRASES[Math.floor(Math.random() * THINKING_PHRASES.length)]
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Убираем завершающее многоточие из фразы — точки анимируем отдельно.
|
|
44
|
-
export function stripEllipsis(phrase) {
|
|
45
|
-
return phrase.replace(/[.…]+\s*$/, '')
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function createSpinner() {
|
|
49
|
-
let spinner = null
|
|
50
|
-
let dotTimer = null
|
|
51
|
-
let dotPhase = 0
|
|
52
|
-
let pending = null
|
|
53
|
-
|
|
54
|
-
const DOTS = ['.', '..', '...']
|
|
55
|
-
|
|
56
|
-
const start = (text) => {
|
|
57
|
-
if (!spinner) spinner = ora(text).start()
|
|
58
|
-
else spinner.start(text)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const stop = () => {
|
|
62
|
-
if (dotTimer) {
|
|
63
|
-
clearInterval(dotTimer)
|
|
64
|
-
dotTimer = null
|
|
65
|
-
}
|
|
66
|
-
if (spinner) spinner.stop()
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Подсказка в строке статуса: во время работы агента терминал живой,
|
|
70
|
-
// можно печатать следующее сообщение. Без неё это неочевидно.
|
|
71
|
-
const HINT = theme.dim(' · печатай — Enter в очередь, Esc стоп')
|
|
72
|
-
|
|
73
|
-
// Запуск анимированного статуса: коричневый текст + «бегущие» точки.
|
|
74
|
-
const startThinking = () => {
|
|
75
|
-
const base = theme.brown(stripEllipsis(randomThinkingPhrase()))
|
|
76
|
-
start(base + DOTS[0] + HINT)
|
|
77
|
-
dotPhase = 0
|
|
78
|
-
if (dotTimer) clearInterval(dotTimer)
|
|
79
|
-
dotTimer = setInterval(() => {
|
|
80
|
-
if (!spinner) return
|
|
81
|
-
dotPhase = (dotPhase + 1) % DOTS.length
|
|
82
|
-
spinner.text = base + DOTS[dotPhase] + HINT
|
|
83
|
-
}, 400)
|
|
84
|
-
if (dotTimer.unref) dotTimer.unref()
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Показать набранный, но ещё не отправленный текст вместо спиннера.
|
|
88
|
-
// Позволяет печатать сообщение прямо во время работы агента.
|
|
89
|
-
const showPending = () => {
|
|
90
|
-
if (dotTimer) {
|
|
91
|
-
clearInterval(dotTimer)
|
|
92
|
-
dotTimer = null
|
|
93
|
-
}
|
|
94
|
-
if (!spinner) spinner = ora('')
|
|
95
|
-
spinner.start()
|
|
96
|
-
spinner.text = theme.prompt('✎ ') + pending + HINT
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return {
|
|
100
|
-
thinking: () => {
|
|
101
|
-
if (pending) {
|
|
102
|
-
showPending()
|
|
103
|
-
return
|
|
104
|
-
}
|
|
105
|
-
startThinking()
|
|
106
|
-
},
|
|
107
|
-
|
|
108
|
-
// Текст, который пользователь набирает во время работы агента.
|
|
109
|
-
// Пустая строка / null — вернуть обычный спиннер.
|
|
110
|
-
setPending: (text) => {
|
|
111
|
-
pending = text && String(text).length ? String(text) : null
|
|
112
|
-
if (pending) showPending()
|
|
113
|
-
else startThinking()
|
|
114
|
-
},
|
|
115
|
-
|
|
116
|
-
toolCall: (name, args) => {
|
|
117
|
-
stop()
|
|
118
|
-
const preview = JSON.stringify(args).slice(0, 120)
|
|
119
|
-
console.log(theme.tool('🔧 ' + name), theme.dim(preview))
|
|
120
|
-
},
|
|
121
|
-
|
|
122
|
-
toolResult: (result) => {
|
|
123
|
-
stop()
|
|
124
|
-
const text = typeof result === 'string' ? result : JSON.stringify(result)
|
|
125
|
-
const preview = text.slice(0, 200).split(String.fromCharCode(10)).join(' ↵ ')
|
|
126
|
-
console.log(theme.toolResult(' → ' + preview + String.fromCharCode(10)))
|
|
127
|
-
},
|
|
128
|
-
|
|
129
|
-
assistant: (msg) => {
|
|
130
|
-
stop()
|
|
131
|
-
const NL = String.fromCharCode(10)
|
|
132
|
-
const rendered = renderMarkdown(msg)
|
|
133
|
-
// Маркер ответа модели: помогает визуально отделить его от ввода
|
|
134
|
-
// пользователя (который подсвечен приглашением с золотой стрелкой).
|
|
135
|
-
console.log(NL + theme.assistant('● Ответ') + NL)
|
|
136
|
-
console.log(rendered)
|
|
137
|
-
console.log(theme.dim('─'.repeat(60)) + NL)
|
|
138
|
-
},
|
|
139
|
-
|
|
140
|
-
stop,
|
|
141
|
-
succeed: () => {},
|
|
142
|
-
fail: () => {},
|
|
143
|
-
}
|
|
144
|
-
}
|
package/src/tools.js
DELETED
|
@@ -1,206 +0,0 @@
|
|
|
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
|
-
}
|