zames_pro 1.2.1 → 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.
Files changed (58) hide show
  1. package/dist/agent-loop.js +652 -0
  2. package/dist/agent-loop.js.map +1 -0
  3. package/dist/browser.js +573 -0
  4. package/dist/browser.js.map +1 -0
  5. package/dist/config.js +74 -0
  6. package/dist/config.js.map +1 -0
  7. package/dist/confirm.js +82 -0
  8. package/dist/confirm.js.map +1 -0
  9. package/dist/diff.js +38 -0
  10. package/dist/diff.js.map +1 -0
  11. package/dist/gitTools.js +219 -0
  12. package/dist/gitTools.js.map +1 -0
  13. package/dist/index.js +1365 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/input.js +524 -0
  16. package/dist/input.js.map +1 -0
  17. package/dist/markdown.js +48 -0
  18. package/dist/markdown.js.map +1 -0
  19. package/dist/self-review.js +281 -0
  20. package/dist/self-review.js.map +1 -0
  21. package/dist/sessions.js +123 -0
  22. package/dist/sessions.js.map +1 -0
  23. package/dist/spinner.js +138 -0
  24. package/dist/spinner.js.map +1 -0
  25. package/{src → dist}/system-prompt.js +10 -14
  26. package/dist/system-prompt.js.map +1 -0
  27. package/{src → dist}/theme.js +17 -19
  28. package/dist/theme.js.map +1 -0
  29. package/dist/tools.js +189 -0
  30. package/dist/tools.js.map +1 -0
  31. package/dist/transcript.js +51 -0
  32. package/dist/transcript.js.map +1 -0
  33. package/dist/types.js +4 -0
  34. package/dist/types.js.map +1 -0
  35. package/dist/undo.js +100 -0
  36. package/dist/undo.js.map +1 -0
  37. package/dist/web.js +220 -0
  38. package/dist/web.js.map +1 -0
  39. package/dist/xml-toolcall.js +68 -0
  40. package/dist/xml-toolcall.js.map +1 -0
  41. package/package.json +13 -5
  42. package/src/agent-loop.js +0 -634
  43. package/src/browser.js +0 -627
  44. package/src/config.js +0 -81
  45. package/src/confirm.js +0 -85
  46. package/src/diff.js +0 -32
  47. package/src/gitTools.js +0 -256
  48. package/src/index.js +0 -1515
  49. package/src/input.js +0 -447
  50. package/src/markdown.js +0 -49
  51. package/src/self-review.js +0 -319
  52. package/src/sessions.js +0 -110
  53. package/src/spinner.js +0 -144
  54. package/src/tools.js +0 -206
  55. package/src/transcript.js +0 -46
  56. package/src/undo.js +0 -101
  57. package/src/web.js +0 -266
  58. package/src/xml-toolcall.js +0 -69
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
- }
package/src/transcript.js DELETED
@@ -1,46 +0,0 @@
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
- }
package/src/undo.js DELETED
@@ -1,101 +0,0 @@
1
- import fs from 'fs/promises'
2
- import path from 'path'
3
- import os from 'os'
4
-
5
- const UNDO_DIR = path.join(os.homedir(), '.zames', 'undo')
6
- const INDEX = path.join(UNDO_DIR, 'index.json')
7
-
8
- export class UndoStore {
9
- constructor({ enabled = true, maxBackups = 200 } = {}) {
10
- this.enabled = enabled
11
- this.maxBackups = maxBackups
12
- }
13
-
14
- async _ensure() {
15
- await fs.mkdir(UNDO_DIR, { recursive: true })
16
- }
17
-
18
- async _readIndex() {
19
- try {
20
- return JSON.parse(await fs.readFile(INDEX, 'utf-8'))
21
- } catch {
22
- return []
23
- }
24
- }
25
-
26
- async _writeIndex(history) {
27
- await fs.writeFile(INDEX, JSON.stringify(history, null, 2))
28
- }
29
-
30
- async backup(filePath) {
31
- if (!this.enabled) return null
32
- await this._ensure()
33
-
34
- let content = null
35
- let existed = false
36
- try {
37
- content = await fs.readFile(filePath)
38
- existed = true
39
- } catch (e) {
40
- if (e.code !== 'ENOENT') throw e
41
- }
42
-
43
- const stamp = Date.now()
44
- const hash = Buffer.from(filePath).toString('hex').slice(0, 16)
45
-
46
- let backupFile = null
47
- if (existed) {
48
- backupFile = path.join(UNDO_DIR, `${stamp}-${hash}.bak`)
49
- await fs.writeFile(backupFile, content)
50
- }
51
-
52
- const history = await this._readIndex()
53
- history.push({ originalPath: filePath, existed, stamp, backupFile })
54
-
55
- while (history.length > this.maxBackups) {
56
- const old = history.shift()
57
- if (old.backupFile) {
58
- try {
59
- await fs.unlink(old.backupFile)
60
- } catch {}
61
- }
62
- }
63
-
64
- await this._writeIndex(history)
65
- return { originalPath: filePath, existed, stamp }
66
- }
67
-
68
- async undoLast() {
69
- if (!this.enabled) return { ok: false, reason: 'undo отключён в конфиге' }
70
- await this._ensure()
71
-
72
- const history = await this._readIndex()
73
- if (!history.length) return { ok: false, reason: 'история пуста' }
74
-
75
- const record = history.pop()
76
- const { originalPath, existed, backupFile } = record
77
-
78
- try {
79
- if (existed && backupFile) {
80
- const data = await fs.readFile(backupFile)
81
- await fs.writeFile(originalPath, data)
82
- try {
83
- await fs.unlink(backupFile)
84
- } catch {}
85
- } else {
86
- try {
87
- await fs.unlink(originalPath)
88
- } catch {}
89
- }
90
- await this._writeIndex(history)
91
- return { ok: true, record }
92
- } catch (e) {
93
- return { ok: false, reason: e.message }
94
- }
95
- }
96
-
97
- async list(count = 10) {
98
- const history = await this._readIndex()
99
- return history.slice(-count).reverse()
100
- }
101
- }
package/src/web.js DELETED
@@ -1,266 +0,0 @@
1
- import { chromium } from 'playwright'
2
-
3
- const DEFAULT_TIMEOUT = 20_000
4
- const MAX_TEXT = 12_000
5
-
6
- // Убирает скрипты, стили, и превращает HTML в читабельный текст.
7
- function htmlToText(html) {
8
- // Удаляем блоки, которые не нужны
9
- let s = html
10
- .replace(/<script[\s\S]*?<\/script>/gi, '')
11
- .replace(/<style[\s\S]*?<\/style>/gi, '')
12
- .replace(/<noscript[\s\S]*?<\/noscript>/gi, '')
13
- .replace(/<svg[\s\S]*?<\/svg>/gi, '')
14
- .replace(/<!--[\s\S]*?-->/g, '')
15
-
16
- // Ссылки: <a href="URL">text</a> → text (URL)
17
- s = s.replace(
18
- /<a\b[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi,
19
- (_, href, text) => {
20
- const t = text.replace(/<[^>]+>/g, '').trim()
21
- return t ? `${t} (${href})` : href
22
- },
23
- )
24
-
25
- // Заголовки, параграфы, BR → переводы строк
26
- s = s
27
- .replace(/<\/(h[1-6]|p|div|li|tr|section|article)>/gi, '\n')
28
- .replace(/<br\s*\/?>/gi, '\n')
29
- .replace(/<li[^>]*>/gi, '- ')
30
- .replace(/<[^>]+>/g, '')
31
-
32
- // HTML entities — базовые
33
- s = s
34
- .replace(/&nbsp;/g, ' ')
35
- .replace(/&amp;/g, '&')
36
- .replace(/&lt;/g, '<')
37
- .replace(/&gt;/g, '>')
38
- .replace(/&quot;/g, '"')
39
- .replace(/&#39;/g, "'")
40
- .replace(/&mdash;/g, '—')
41
- .replace(/&ndash;/g, '–')
42
- .replace(/&hellip;/g, '…')
43
- .replace(/&#(\d+);/g, (_, n) => String.fromCodePoint(Number(n)))
44
- .replace(/&#x([0-9a-f]+);/gi, (_, n) =>
45
- String.fromCodePoint(parseInt(n, 16)),
46
- )
47
-
48
- // Сжимаем пустые строки
49
- s = s
50
- .split('\n')
51
- .map((l) => l.replace(/[ \t]+/g, ' ').trim())
52
- .filter(Boolean)
53
- .join('\n')
54
-
55
- return s
56
- }
57
-
58
- // fetch с редиректами и таймаутом
59
- async function httpFetch(
60
- url,
61
- { timeout = DEFAULT_TIMEOUT, headers = {} } = {},
62
- ) {
63
- const ctrl = new AbortController()
64
- const t = setTimeout(() => ctrl.abort(), timeout)
65
- try {
66
- const res = await fetch(url, {
67
- signal: ctrl.signal,
68
- redirect: 'follow',
69
- headers: {
70
- 'User-Agent':
71
- 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) ds-agent/1.0',
72
- 'Accept-Language': 'en-US,en;q=0.9,ru;q=0.8',
73
- ...headers,
74
- },
75
- })
76
- const ct = res.headers.get('content-type') || ''
77
- const body = await res.text()
78
- return { status: res.status, url: res.url, contentType: ct, body }
79
- } finally {
80
- clearTimeout(t)
81
- }
82
- }
83
-
84
- // Отдельный headless-браузер для JS-страниц.
85
- // Ленивая инициализация, чтобы не тратить ресурсы впустую.
86
- let _headless = null
87
- async function getHeadless() {
88
- if (_headless) return _headless
89
- _headless = await chromium.launch({
90
- headless: true,
91
- args: ['--disable-blink-features=AutomationControlled'],
92
- })
93
- return _headless
94
- }
95
-
96
- export async function closeWeb() {
97
- if (_headless) {
98
- try {
99
- await _headless.close()
100
- } catch {}
101
- _headless = null
102
- }
103
- }
104
-
105
- async function renderWithHeadless(url, { timeout = DEFAULT_TIMEOUT } = {}) {
106
- const browser = await getHeadless()
107
- const ctx = await browser.newContext({
108
- userAgent:
109
- 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
110
- })
111
- const page = await ctx.newPage()
112
- try {
113
- const resp = await page.goto(url, {
114
- waitUntil: 'domcontentloaded',
115
- timeout,
116
- })
117
- // Дать JS немного времени дорисовать
118
- await page.waitForTimeout(1500)
119
- const html = await page.content()
120
- const status = resp ? resp.status() : 0
121
- const finalUrl = page.url()
122
- return { status, url: finalUrl, html }
123
- } finally {
124
- await ctx.close()
125
- }
126
- }
127
-
128
- // ---------- инструменты ----------
129
-
130
- export function createWebTools() {
131
- return [
132
- {
133
- name: 'WebFetch',
134
- description:
135
- 'Загрузить URL и вернуть очищенный текст страницы (без HTML-тегов). ' +
136
- 'Используй для чтения документации, статей, README на GitHub. ' +
137
- 'Если страница рендерится JavaScript-ом и текст пустой — попробуй ещё раз с render=true.',
138
- parameters: {
139
- url: 'string',
140
- render: 'boolean?',
141
- maxChars: 'number?',
142
- },
143
- fn: async ({ url, render, maxChars }) => {
144
- if (!/^https?:\/\//i.test(url)) {
145
- return `Ошибка: URL должен начинаться с http:// или https://`
146
- }
147
-
148
- const limit = Math.min(Number(maxChars) || MAX_TEXT, 60_000)
149
-
150
- try {
151
- if (render) {
152
- const r = await renderWithHeadless(url)
153
- const text = htmlToText(r.html)
154
- return formatResult(r.status, r.url, text, limit)
155
- }
156
-
157
- const r = await httpFetch(url)
158
-
159
- // Если это JSON/plain text — вернуть как есть
160
- if (
161
- /application\/json|text\/plain|text\/markdown/i.test(r.contentType)
162
- ) {
163
- const trimmed = r.body.slice(0, limit)
164
- return `HTTP ${r.status} ${r.url}\nContent-Type: ${r.contentType}\n\n${trimmed}`
165
- }
166
-
167
- // HTML — почистить
168
- const text = htmlToText(r.body)
169
-
170
- // Если текста почти нет — вероятно JS-страница, дать намёк
171
- if (text.length < 200) {
172
- return (
173
- `HTTP ${r.status} ${r.url}\n` +
174
- `Content-Type: ${r.contentType}\n\n` +
175
- `Страница почти пустая в сыром HTML (${text.length} символов). ` +
176
- `Похоже, контент рендерится JavaScript-ом. ` +
177
- `Повтори вызов с render=true.\n\n---\n${text}`
178
- )
179
- }
180
-
181
- return formatResult(r.status, r.url, text, limit)
182
- } catch (e) {
183
- return `Ошибка загрузки ${url}: ${e.message}`
184
- }
185
- },
186
- },
187
-
188
- {
189
- name: 'WebSearch',
190
- description:
191
- 'Поиск в интернете через DuckDuckGo HTML (без API-ключа). ' +
192
- 'Возвращает список результатов: заголовок, URL, краткое описание.',
193
- parameters: {
194
- query: 'string',
195
- maxResults: 'number?',
196
- },
197
- fn: async ({ query, maxResults }) => {
198
- const q = encodeURIComponent(query || '')
199
- if (!q) return 'Ошибка: query пустой.'
200
-
201
- const limit = Math.min(Math.max(Number(maxResults) || 8, 1), 20)
202
-
203
- try {
204
- const r = await httpFetch(
205
- `https://html.duckduckgo.com/html/?q=${q}`,
206
- {
207
- timeout: 25_000,
208
- headers: {
209
- Accept: 'text/html,application/xhtml+xml',
210
- },
211
- },
212
- )
213
-
214
- if (r.status !== 200) {
215
- return `DuckDuckGo вернул HTTP ${r.status}`
216
- }
217
-
218
- // Парсим простыми регексами. Формат html.duckduckgo.com стабильный.
219
- const results = []
220
- const re =
221
- /<a[^>]*class="[^"]*result__a[^"]*"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?(?:<a[^>]*class="[^"]*result__snippet[^"]*"[^>]*>([\s\S]*?)<\/a>)?/gi
222
-
223
- let m
224
- while ((m = re.exec(r.body)) && results.length < limit) {
225
- let href = m[1]
226
- // DuckDuckGo заворачивает ссылки в редирект вида /l/?uddg=...
227
- const uddgMatch = href.match(/[?&]uddg=([^&]+)/)
228
- if (uddgMatch) href = decodeURIComponent(uddgMatch[1])
229
-
230
- const title = htmlToText(m[2] || '').trim()
231
- const snippet = htmlToText(m[3] || '').trim()
232
-
233
- if (!title || !href) continue
234
- results.push({ title, url: href, snippet })
235
- }
236
-
237
- if (!results.length) {
238
- return `Результатов не найдено. Возможно, изменился формат выдачи DuckDuckGo.`
239
- }
240
-
241
- const lines = results.map(
242
- (r, i) =>
243
- `${i + 1}. ${r.title}\n ${r.url}` +
244
- (r.snippet ? `\n ${r.snippet}` : ''),
245
- )
246
- return lines.join('\n\n')
247
- } catch (e) {
248
- return `Ошибка поиска: ${e.message}`
249
- }
250
- },
251
- },
252
- ]
253
- }
254
-
255
- function formatResult(status, url, text, limit) {
256
- let out = text
257
- let truncated = false
258
- if (out.length > limit) {
259
- out = out.slice(0, limit)
260
- truncated = true
261
- }
262
- return (
263
- `HTTP ${status} ${url}\n\n${out}` +
264
- (truncated ? `\n\n[...обрезано на ${limit} символах]` : '')
265
- )
266
- }
@@ -1,69 +0,0 @@
1
- function unescapeXml(s) {
2
- const A = String.fromCharCode(38) // ampersand
3
- return String(s)
4
- .split(A + "lt;").join(String.fromCharCode(60))
5
- .split(A + "gt;").join(String.fromCharCode(62))
6
- .split(A + "quot;").join(String.fromCharCode(34))
7
- .split(A + "apos;").join(String.fromCharCode(39))
8
- .split(A + "amp;").join(A)
9
- }
10
-
11
- function readAttr(attrs, name) {
12
- const Q = String.fromCharCode(34)
13
- const re = new RegExp(name + "[ ]*=[ ]*([" + Q + "]([^" + Q + "]*)[" + Q + "])")
14
- const m = attrs.match(re)
15
- return m ? m[2] : null
16
- }
17
-
18
- export function parseXmlToolCalls(text) {
19
- if (!text || typeof text !== "string") return null
20
- if (!/<[^>]*invoke/i.test(text)) return null
21
-
22
- const invokeRe = /<[^>]*?invoke([^>]*)>/gi
23
- const calls = []
24
- let m
25
- while ((m = invokeRe.exec(text)) !== null) {
26
- const attrs = m[1] || ""
27
- const name = readAttr(attrs, "name")
28
- if (!name) continue
29
-
30
- const rest = text.slice(invokeRe.lastIndex)
31
- const closeRe = /<\/[^>]*?invoke[^>]*>/i
32
- const closeMatch = closeRe.exec(rest)
33
- const body = closeMatch ? rest.slice(0, closeMatch.index) : rest
34
-
35
- const args = {}
36
- const paramRe = /<[^>]*?parameter([^>]*)>([\s\S]*?)<\/[^>]*?parameter[^>]*>/gi
37
- let p
38
- while ((p = paramRe.exec(body)) !== null) {
39
- const pname = readAttr(p[1] || "", "name")
40
- if (!pname) continue
41
- const isString = /string[ ]*=[ ]*"?true"?/i.test(p[1] || "")
42
- let value = unescapeXml(p[2])
43
- if (!isString) {
44
- try { value = JSON.parse(value) } catch (e) {}
45
- }
46
- args[pname] = value
47
- }
48
-
49
- // Некоторые модели кладут весь JSON-объект аргументов в один
50
- // parameter с именем args. Разворачиваем, чтобы не было
51
- // {args: {args: {...}}}.
52
- let finalArgs = args
53
- const keys = Object.keys(args)
54
- if (
55
- keys.length === 1 &&
56
- keys[0] === 'args' &&
57
- args.args &&
58
- typeof args.args === 'object' &&
59
- !Array.isArray(args.args)
60
- ) {
61
- finalArgs = args.args
62
- }
63
-
64
- calls.push({ tool: name, args: finalArgs })
65
- }
66
-
67
- if (!calls.length) return null
68
- return calls.length === 1 ? calls[0] : calls
69
- }