zames_pro 2.53.1 → 2.55.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/README.md +6 -4
- package/dist/commands.js +5 -20
- package/dist/config.js +0 -33
- package/dist/i18n.js +26 -37
- package/dist/index.js +67 -13
- package/dist/input.js +5 -0
- package/dist/self-review.js +44 -6
- package/dist/shell.js +72 -0
- package/dist/tools.js +5 -70
- package/dist/web.js +101 -2
- package/package.json +2 -1
- package/dist/confirm.js +0 -105
package/README.md
CHANGED
|
@@ -90,6 +90,9 @@ The prompt is a small line editor with persistent history:
|
|
|
90
90
|
delete the word before the cursor, `Ctrl+←`/`Ctrl+→` — move by words.
|
|
91
91
|
- `\` + `Enter`, `Ctrl+J`, `Ctrl+Enter` or `Shift+Enter` — insert a newline.
|
|
92
92
|
- `/` + `Tab` — slash-command hints and completion.
|
|
93
|
+
- `!command` — run a shell command directly, bypassing the model (like
|
|
94
|
+
Claude Code's bash mode). The same sandbox guard as the `Bash` tool applies,
|
|
95
|
+
so a direct command cannot leave the project either.
|
|
93
96
|
|
|
94
97
|
Set `NO_COLOR=1` to disable colors (a calm default palette is used otherwise).
|
|
95
98
|
|
|
@@ -160,8 +163,6 @@ Codex CLI:
|
|
|
160
163
|
- /export [file] — write the session transcript to a Markdown file
|
|
161
164
|
(zames-export-<stamp>.md by default).
|
|
162
165
|
- /doctor — diagnose node, git, config, browser, clipboard and MCP.
|
|
163
|
-
- /permissions — show the confirmation settings (Write/Edit/Bash + the
|
|
164
|
-
alwaysConfirm regex list).
|
|
165
166
|
- /add-dir <path> — validate an extra directory (the sandbox is fixed at
|
|
166
167
|
startup; relaunch with --dir to write there).
|
|
167
168
|
- /resume <n> (after /chats) and /resume-id <id> — open a chat and PRINT its
|
|
@@ -207,7 +208,8 @@ workflows from your repository and from `~/.zames`.
|
|
|
207
208
|
will explore the project and write an AGENTS.md based on the real build/test
|
|
208
209
|
commands and conventions (use `/init --force` to overwrite an existing file).
|
|
209
210
|
- **MEMORY.md** — durable notes that persist between sessions. The agent
|
|
210
|
-
appends useful facts here; you can edit it by hand
|
|
211
|
+
appends useful facts here; you can edit it by hand, or add one from the
|
|
212
|
+
prompt with `/remember <text>`.
|
|
211
213
|
- **Skills** — a folder with a `SKILL.md` file (YAML frontmatter: `name`,
|
|
212
214
|
`description`, optional `allowed-tools`, `user-invokable`) plus any helper
|
|
213
215
|
files. Discovered under `.zames/skills/`, `.claude/skills/`,
|
|
@@ -223,6 +225,7 @@ Skills and custom commands show up in the «/» completion list and in `/help`.
|
|
|
223
225
|
```
|
|
224
226
|
/skills list discovered skills
|
|
225
227
|
/memory show AGENTS.md / MEMORY.md in effect
|
|
228
|
+
/remember <text> append a durable note to MEMORY.md
|
|
226
229
|
/init [--force] analyze the project and create AGENTS.md
|
|
227
230
|
```
|
|
228
231
|
|
|
@@ -286,7 +289,6 @@ Examples:
|
|
|
286
289
|
|
|
287
290
|
```
|
|
288
291
|
/config set maxIterations 20
|
|
289
|
-
/config set confirmation.bash false
|
|
290
292
|
/config set browser.deepThinking true # DeepSeek Deep thinking (slow; reasoning is hidden)
|
|
291
293
|
/config set browser.webSearch false # DeepSeek Smart web search
|
|
292
294
|
/config lang en
|
package/dist/commands.js
CHANGED
|
@@ -2,7 +2,7 @@ import path from 'path';
|
|
|
2
2
|
import { isImageName } from './attachments.js';
|
|
3
3
|
import { translate } from './i18n.js';
|
|
4
4
|
// Helpers for the extra slash commands (/diff, /cost, /export, /doctor,
|
|
5
|
-
// /
|
|
5
|
+
// /review, /add-dir). Pure functions, unit-tested without a
|
|
6
6
|
// live agent/browser. index.ts only renders their output.
|
|
7
7
|
const NL = String.fromCharCode(10);
|
|
8
8
|
const CR = String.fromCharCode(13);
|
|
@@ -234,22 +234,6 @@ export function renderDoctor(d, t = translate('en')) {
|
|
|
234
234
|
}
|
|
235
235
|
return t('doctor.title') + NL + rows.join(NL);
|
|
236
236
|
}
|
|
237
|
-
export function renderPermissions(p, t = translate('en')) {
|
|
238
|
-
const onoff = (b) => (b ? t('perm.ask') : t('perm.allow'));
|
|
239
|
-
const lines = [];
|
|
240
|
-
lines.push(t('perm.title'));
|
|
241
|
-
lines.push(t('perm.write', { v: onoff(p.write) }));
|
|
242
|
-
lines.push(t('perm.edit', { v: onoff(p.edit) }));
|
|
243
|
-
lines.push(t('perm.bash', { v: onoff(p.bash) }));
|
|
244
|
-
if (p.alwaysConfirm.length) {
|
|
245
|
-
lines.push(t('perm.always'));
|
|
246
|
-
for (const re of p.alwaysConfirm)
|
|
247
|
-
lines.push(' ' + re);
|
|
248
|
-
}
|
|
249
|
-
lines.push('');
|
|
250
|
-
lines.push(t('perm.change_hint'));
|
|
251
|
-
return lines.join(NL);
|
|
252
|
-
}
|
|
253
237
|
// ---------- /add-dir ----------
|
|
254
238
|
export function resolveExtraDir(input, workdir, t = translate('en')) {
|
|
255
239
|
const raw = String(input ?? '').trim();
|
|
@@ -488,9 +472,10 @@ export function hasQueuedJob(queue, jobId) {
|
|
|
488
472
|
// True when the queued message is a slash-command. It must NOT be merged into
|
|
489
473
|
// a task: the main loop has to execute it (e.g. /compact, /new).
|
|
490
474
|
export function isSlashCommand(text) {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
475
|
+
const s = String(text ?? '').trim();
|
|
476
|
+
// `!command` is also an operator command (run the shell directly) — it
|
|
477
|
+
// must NOT be batched into a task sent to the model.
|
|
478
|
+
return s.startsWith('/') || s.startsWith('!');
|
|
494
479
|
}
|
|
495
480
|
export function parseLiveToggle(text) {
|
|
496
481
|
const m = /^\/(thinking|web|websearch|search)(?:\s+(\S+))?\s*$/i.exec(String(text ?? '').trim());
|
package/dist/config.js
CHANGED
|
@@ -24,21 +24,6 @@ export const DEFAULTS = {
|
|
|
24
24
|
headless: true,
|
|
25
25
|
debug: false,
|
|
26
26
|
browserChannel: null,
|
|
27
|
-
confirmation: {
|
|
28
|
-
write: true,
|
|
29
|
-
edit: true,
|
|
30
|
-
bash: true,
|
|
31
|
-
alwaysConfirm: [
|
|
32
|
-
'rm\s+-rf',
|
|
33
|
-
'rmdir\s+/s',
|
|
34
|
-
'del\s+/[sqf]',
|
|
35
|
-
'format\s+[a-z]:',
|
|
36
|
-
'shutdown',
|
|
37
|
-
'reg\s+delete',
|
|
38
|
-
'remove-item.*-recurse',
|
|
39
|
-
'git\s+push\s+--force',
|
|
40
|
-
],
|
|
41
|
-
},
|
|
42
27
|
undo: {
|
|
43
28
|
enabled: true,
|
|
44
29
|
maxBackups: 200,
|
|
@@ -169,24 +154,6 @@ export const CONFIG_SCHEMA = [
|
|
|
169
154
|
labelKey: 'cfg.f.hotReload',
|
|
170
155
|
groupKey: 'cfg.group.agent',
|
|
171
156
|
},
|
|
172
|
-
{
|
|
173
|
-
path: 'confirmation.write',
|
|
174
|
-
type: 'boolean',
|
|
175
|
-
labelKey: 'cfg.f.confirmation_write',
|
|
176
|
-
groupKey: 'cfg.group.confirmation',
|
|
177
|
-
},
|
|
178
|
-
{
|
|
179
|
-
path: 'confirmation.edit',
|
|
180
|
-
type: 'boolean',
|
|
181
|
-
labelKey: 'cfg.f.confirmation_edit',
|
|
182
|
-
groupKey: 'cfg.group.confirmation',
|
|
183
|
-
},
|
|
184
|
-
{
|
|
185
|
-
path: 'confirmation.bash',
|
|
186
|
-
type: 'boolean',
|
|
187
|
-
labelKey: 'cfg.f.confirmation_bash',
|
|
188
|
-
groupKey: 'cfg.group.confirmation',
|
|
189
|
-
},
|
|
190
157
|
{
|
|
191
158
|
path: 'undo.enabled',
|
|
192
159
|
type: 'boolean',
|
package/dist/i18n.js
CHANGED
|
@@ -135,6 +135,10 @@ export const CATALOG = {
|
|
|
135
135
|
ru: 'Esc, Ctrl+C прервать текущую генерацию',
|
|
136
136
|
en: 'Esc, Ctrl+C abort current generation',
|
|
137
137
|
},
|
|
138
|
+
'help.key.bang': {
|
|
139
|
+
ru: '!команда выполнить shell-команду напрямую (мимо модели)',
|
|
140
|
+
en: '!command run a shell command directly (bypass the model)',
|
|
141
|
+
},
|
|
138
142
|
'help.commands': { ru: 'Обычные команды:', en: 'Commands:' },
|
|
139
143
|
'help.cmd.new': {
|
|
140
144
|
ru: '/new, /clear новый чат (сброс контекста)',
|
|
@@ -205,10 +209,6 @@ export const CATALOG = {
|
|
|
205
209
|
ru: '/doctor диагностика установки и конфига',
|
|
206
210
|
en: '/doctor diagnose install and config',
|
|
207
211
|
},
|
|
208
|
-
'help.cmd.permissions': {
|
|
209
|
-
ru: '/permissions настройки подтверждений',
|
|
210
|
-
en: '/permissions confirmation settings',
|
|
211
|
-
},
|
|
212
212
|
'help.cmd.add_dir': {
|
|
213
213
|
ru: '/add-dir <path> проверить директорию',
|
|
214
214
|
en: '/add-dir <path> validate a directory',
|
|
@@ -393,6 +393,14 @@ export const CATALOG = {
|
|
|
393
393
|
en: 'Instructions (AGENTS.md):',
|
|
394
394
|
},
|
|
395
395
|
'memory.memory': { ru: 'Память (MEMORY.md):', en: 'Memory (MEMORY.md):' },
|
|
396
|
+
'remember.usage': {
|
|
397
|
+
ru: 'Использование: /remember <текст заметки>',
|
|
398
|
+
en: 'Usage: /remember <note text>',
|
|
399
|
+
},
|
|
400
|
+
'remember.saved': {
|
|
401
|
+
ru: '🧠 Заметка добавлена в {v}',
|
|
402
|
+
en: '🧠 Note appended to {v}',
|
|
403
|
+
},
|
|
396
404
|
'init.exists': {
|
|
397
405
|
ru: 'AGENTS.md уже существует: {v}',
|
|
398
406
|
en: 'AGENTS.md already exists: {v}',
|
|
@@ -424,6 +432,10 @@ export const CATALOG = {
|
|
|
424
432
|
ru: '/memory показать MEMORY.md и AGENTS.md',
|
|
425
433
|
en: '/memory show MEMORY.md and AGENTS.md',
|
|
426
434
|
},
|
|
435
|
+
'help.cmd.remember': {
|
|
436
|
+
ru: '/remember <текст> дописать заметку в MEMORY.md',
|
|
437
|
+
en: '/remember <text> append a note to MEMORY.md',
|
|
438
|
+
},
|
|
427
439
|
'help.cmd.init': {
|
|
428
440
|
ru: '/init [--force] изучить проект и создать AGENTS.md',
|
|
429
441
|
en: '/init [--force] analyze the project and create AGENTS.md',
|
|
@@ -607,6 +619,14 @@ export const CATALOG = {
|
|
|
607
619
|
en: 'Unknown command: {v}. Type /help.',
|
|
608
620
|
},
|
|
609
621
|
'msg.bye': { ru: 'Выход.', en: 'Bye.' },
|
|
622
|
+
'shell.empty': {
|
|
623
|
+
ru: 'После ! нужна команда, например !git status.',
|
|
624
|
+
en: 'A command is required after !, e.g. !git status.',
|
|
625
|
+
},
|
|
626
|
+
'shell.running': {
|
|
627
|
+
ru: 'выполняю shell-команду...',
|
|
628
|
+
en: 'running shell command...',
|
|
629
|
+
},
|
|
610
630
|
'msg.exit_summary': {
|
|
611
631
|
ru: 'Транскрипт: {transcript} · чат: {chat}',
|
|
612
632
|
en: 'Transcript: {transcript} · chat: {chat}',
|
|
@@ -722,24 +742,6 @@ export const CATALOG = {
|
|
|
722
742
|
ru: 'UA ещё не закэширован (первый запуск перезапустится один раз)',
|
|
723
743
|
en: 'no cached UA yet (first start relaunches once)',
|
|
724
744
|
},
|
|
725
|
-
// ---------- /permissions ----------
|
|
726
|
-
'perm.title': {
|
|
727
|
-
ru: 'Права инструментов (настройки подтверждений):',
|
|
728
|
-
en: 'Tool permissions (confirmation settings):',
|
|
729
|
-
},
|
|
730
|
-
'perm.write': { ru: ' Write: {v}', en: ' Write: {v}' },
|
|
731
|
-
'perm.edit': { ru: ' Edit: {v}', en: ' Edit: {v}' },
|
|
732
|
-
'perm.bash': { ru: ' Bash: {v}', en: ' Bash: {v}' },
|
|
733
|
-
'perm.ask': { ru: 'спрашивать', en: 'ask' },
|
|
734
|
-
'perm.allow': { ru: 'разрешить', en: 'allow' },
|
|
735
|
-
'perm.always': {
|
|
736
|
-
ru: ' Всегда подтверждать (regex):',
|
|
737
|
-
en: ' Always confirm (regex):',
|
|
738
|
-
},
|
|
739
|
-
'perm.change_hint': {
|
|
740
|
-
ru: 'Изменить: /config set confirmation.write false (и .edit / .bash)',
|
|
741
|
-
en: 'Change with: /config set confirmation.write false (and .edit / .bash)',
|
|
742
|
-
},
|
|
743
745
|
// ---------- /add-dir ----------
|
|
744
746
|
'adddir.usage': {
|
|
745
747
|
ru: 'Использование: /add-dir <path>',
|
|
@@ -1213,7 +1215,6 @@ export const CATALOG = {
|
|
|
1213
1215
|
// ---------- config menu ----------
|
|
1214
1216
|
'cfg.group.ui': { ru: 'Интерфейс', en: 'Interface' },
|
|
1215
1217
|
'cfg.group.agent': { ru: 'Агент', en: 'Agent' },
|
|
1216
|
-
'cfg.group.confirmation': { ru: 'Подтверждения', en: 'Confirmations' },
|
|
1217
1218
|
'cfg.group.undo': { ru: 'Откат (undo)', en: 'Undo' },
|
|
1218
1219
|
'cfg.group.transcript': { ru: 'Транскрипт', en: 'Transcript' },
|
|
1219
1220
|
'cfg.group.browser': { ru: 'Браузер / DeepSeek', en: 'Browser / DeepSeek' },
|
|
@@ -1242,18 +1243,6 @@ export const CATALOG = {
|
|
|
1242
1243
|
ru: 'Авто-перечитывание модулей перед задачей',
|
|
1243
1244
|
en: 'Auto-reload logic modules before each task',
|
|
1244
1245
|
},
|
|
1245
|
-
'cfg.f.confirmation_write': {
|
|
1246
|
-
ru: 'Спрашивать перед Write',
|
|
1247
|
-
en: 'Ask before Write',
|
|
1248
|
-
},
|
|
1249
|
-
'cfg.f.confirmation_edit': {
|
|
1250
|
-
ru: 'Спрашивать перед Edit',
|
|
1251
|
-
en: 'Ask before Edit',
|
|
1252
|
-
},
|
|
1253
|
-
'cfg.f.confirmation_bash': {
|
|
1254
|
-
ru: 'Спрашивать перед Bash',
|
|
1255
|
-
en: 'Ask before Bash',
|
|
1256
|
-
},
|
|
1257
1246
|
'cfg.f.undo_enabled': {
|
|
1258
1247
|
ru: 'Включить бэкапы для отката',
|
|
1259
1248
|
en: 'Enable undo backups',
|
|
@@ -1493,8 +1482,8 @@ export const CATALOG = {
|
|
|
1493
1482
|
en: 'Config write scope: {v} ({file})',
|
|
1494
1483
|
},
|
|
1495
1484
|
'cfg.scope_usage': {
|
|
1496
|
-
ru: 'Использование: /config scope <project|home>. project = .zamesrc.json (в git), home = ~/.zames/. Для личных настроек
|
|
1497
|
-
en: 'Usage: /config scope <project|home>. project = .zamesrc.json (committed), home = ~/.zames/. Use home for personal settings
|
|
1485
|
+
ru: 'Использование: /config scope <project|home>. project = .zamesrc.json (в git), home = ~/.zames/. Для личных настроек выбери home, чтобы не коммитить их.',
|
|
1486
|
+
en: 'Usage: /config scope <project|home>. project = .zamesrc.json (committed), home = ~/.zames/. Use home for personal settings so they are not committed.',
|
|
1498
1487
|
},
|
|
1499
1488
|
'cfg.scope_set': {
|
|
1500
1489
|
ru: 'Запись конфига: {v} ({file})',
|
package/dist/index.js
CHANGED
|
@@ -18,11 +18,12 @@ import { translate, normalizeLocale, localeDisplayName, isLocale, } from './i18n
|
|
|
18
18
|
import { Transcript } from './transcript.js';
|
|
19
19
|
import { UndoStore } from './undo.js';
|
|
20
20
|
import { selfReview, selfDiff, selfApply, selfList } from './self-review.js';
|
|
21
|
-
import { formatDiff, diffGitArgs, parseTranscript, summarizeTranscript, renderCost, formatExport, defaultExportPath, renderDoctor,
|
|
21
|
+
import { formatDiff, diffGitArgs, parseTranscript, summarizeTranscript, renderCost, formatExport, defaultExportPath, renderDoctor, resolveExtraDir, buildReviewPrompt, formatDuration, formatRelativeTime, trimRestoredMessages, RESTORED_HISTORY_LIMIT, mergeMessages, hasQueuedJob, isSlashCommand, parseQueueCommand, parseLiveToggle, parseGoalCommand, isLiveConfigCommand, withGoal, formatQueueList, ctrlCEscalation, } from './commands.js';
|
|
22
22
|
import { performCompact } from './compact.js';
|
|
23
23
|
import { Scheduler, parseInterval, formatInterval, formatJobLine, parseCron, } from './scheduler.js';
|
|
24
24
|
import { renderMarkdown, setAnswerWidth } from './markdown.js';
|
|
25
25
|
import { closeWeb } from './web.js';
|
|
26
|
+
import { assertCommandInsideRoot, runShell } from './shell.js';
|
|
26
27
|
import { saveSession, loadLastSession, readSession, listSessions, sessionsDir, loadHistory, saveHistory, } from './sessions.js';
|
|
27
28
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
28
29
|
const NL = String.fromCharCode(10);
|
|
@@ -167,7 +168,6 @@ const RELOADABLE = [
|
|
|
167
168
|
'self-review',
|
|
168
169
|
'diff',
|
|
169
170
|
'undo',
|
|
170
|
-
'confirm',
|
|
171
171
|
'transcript',
|
|
172
172
|
'spinner',
|
|
173
173
|
'config',
|
|
@@ -309,6 +309,7 @@ ${theme.bold(t('help.while_working'))}
|
|
|
309
309
|
${t('help.key.backslash')}
|
|
310
310
|
${t('help.key.attach')}
|
|
311
311
|
${t('help.key.esc')}
|
|
312
|
+
${t('help.key.bang')}
|
|
312
313
|
|
|
313
314
|
${theme.bold(t('help.commands'))}
|
|
314
315
|
${t('help.cmd.new')}
|
|
@@ -329,7 +330,6 @@ ${theme.bold(t('help.commands'))}
|
|
|
329
330
|
${t('help.cmd.cost')}
|
|
330
331
|
${t('help.cmd.export')}
|
|
331
332
|
${t('help.cmd.doctor')}
|
|
332
|
-
${t('help.cmd.permissions')}
|
|
333
333
|
${t('help.cmd.add_dir')}
|
|
334
334
|
${t('help.cmd.review')}
|
|
335
335
|
${t('help.cmd.goal')}
|
|
@@ -343,6 +343,11 @@ ${theme.bold(t('help.commands'))}
|
|
|
343
343
|
${t('help.cmd.tasks')}
|
|
344
344
|
${t('help.cmd.config')}
|
|
345
345
|
${t('help.cmd.lang')}
|
|
346
|
+
${t('help.cmd.skills')}
|
|
347
|
+
${t('help.cmd.memory')}
|
|
348
|
+
${t('help.cmd.remember')}
|
|
349
|
+
${t('help.cmd.init')}
|
|
350
|
+
${t('help.cmd.mcp')}
|
|
346
351
|
${t('help.cmd.debug_dom')}
|
|
347
352
|
${t('help.cmd.help')}
|
|
348
353
|
${t('help.cmd.exit')}
|
|
@@ -396,7 +401,6 @@ const SLASH_COMMANDS = [
|
|
|
396
401
|
{ name: '/cost', key: 'help.cmd.cost' },
|
|
397
402
|
{ name: '/export', key: 'help.cmd.export' },
|
|
398
403
|
{ name: '/doctor', key: 'help.cmd.doctor' },
|
|
399
|
-
{ name: '/permissions', key: 'help.cmd.permissions' },
|
|
400
404
|
{ name: '/add-dir', key: 'help.cmd.add_dir' },
|
|
401
405
|
{ name: '/review', key: 'help.cmd.review' },
|
|
402
406
|
{ name: '/goal', key: 'help.cmd.goal' },
|
|
@@ -411,6 +415,7 @@ const SLASH_COMMANDS = [
|
|
|
411
415
|
{ name: '/config', key: 'help.cmd.config' },
|
|
412
416
|
{ name: '/skills', key: 'help.cmd.skills' },
|
|
413
417
|
{ name: '/memory', key: 'help.cmd.memory' },
|
|
418
|
+
{ name: '/remember', key: 'help.cmd.remember' },
|
|
414
419
|
{ name: '/init', key: 'help.cmd.init' },
|
|
415
420
|
{ name: '/mcp', key: 'help.cmd.mcp' },
|
|
416
421
|
{ name: '/debug-dom', key: 'help.cmd.debug_dom' },
|
|
@@ -2153,6 +2158,36 @@ async function main() {
|
|
|
2153
2158
|
const lower = trimmed.toLowerCase();
|
|
2154
2159
|
if (['/exit', '/quit', 'exit', 'quit'].includes(lower))
|
|
2155
2160
|
break;
|
|
2161
|
+
// `!command` — run a shell command directly, without asking the model
|
|
2162
|
+
// (like Claude Code's bash mode). The same sandbox guard as the Bash tool
|
|
2163
|
+
// applies, so a direct command cannot leave the project either.
|
|
2164
|
+
if (trimmed.startsWith('!')) {
|
|
2165
|
+
const cmd = trimmed.slice(1).trim();
|
|
2166
|
+
if (!cmd) {
|
|
2167
|
+
console.error(theme.warn(t('shell.empty')));
|
|
2168
|
+
continue;
|
|
2169
|
+
}
|
|
2170
|
+
try {
|
|
2171
|
+
assertCommandInsideRoot(currentWorkdir, cmd);
|
|
2172
|
+
}
|
|
2173
|
+
catch (e) {
|
|
2174
|
+
console.error(theme.error(e.message));
|
|
2175
|
+
continue;
|
|
2176
|
+
}
|
|
2177
|
+
transcript.log('shell_command', { command: cmd });
|
|
2178
|
+
if (editor)
|
|
2179
|
+
editor.lock(t('shell.running'));
|
|
2180
|
+
try {
|
|
2181
|
+
const out = await runShell(currentWorkdir, cmd);
|
|
2182
|
+
console.log(theme.system(out));
|
|
2183
|
+
transcript.log('shell_result', { command: cmd, result: out });
|
|
2184
|
+
}
|
|
2185
|
+
finally {
|
|
2186
|
+
if (editor)
|
|
2187
|
+
editor.unlock();
|
|
2188
|
+
}
|
|
2189
|
+
continue;
|
|
2190
|
+
}
|
|
2156
2191
|
if (lower === '/help' ||
|
|
2157
2192
|
lower === 'help' ||
|
|
2158
2193
|
lower.startsWith('/help ') ||
|
|
@@ -2771,6 +2806,34 @@ async function main() {
|
|
|
2771
2806
|
show(t('memory.memory'), ctx.memory);
|
|
2772
2807
|
continue;
|
|
2773
2808
|
}
|
|
2809
|
+
// `/remember <text>` — append a durable note to the project MEMORY.md.
|
|
2810
|
+
// Claude Code has the same idea (# shortcut): a fact worth keeping across
|
|
2811
|
+
// sessions is written to disk instead of living only in this chat. We
|
|
2812
|
+
// write to the workdir's own MEMORY.md (the deepest file the loader reads)
|
|
2813
|
+
// and create it when missing.
|
|
2814
|
+
if (lower === '/remember' || lower.startsWith('/remember ')) {
|
|
2815
|
+
const note = trimmed.slice('/remember'.length).trim();
|
|
2816
|
+
if (!note) {
|
|
2817
|
+
console.error(theme.warn(t('remember.usage')));
|
|
2818
|
+
continue;
|
|
2819
|
+
}
|
|
2820
|
+
const memFile = path.join(currentWorkdir, 'MEMORY.md');
|
|
2821
|
+
const stamp = new Date().toISOString().slice(0, 10);
|
|
2822
|
+
let body;
|
|
2823
|
+
try {
|
|
2824
|
+
body = await fs.readFile(memFile, 'utf-8');
|
|
2825
|
+
}
|
|
2826
|
+
catch {
|
|
2827
|
+
body = '# MEMORY\n\nDurable notes accumulated across sessions.\n';
|
|
2828
|
+
}
|
|
2829
|
+
if (body.length && !body.endsWith(NL))
|
|
2830
|
+
body += NL;
|
|
2831
|
+
body += NL + '- (' + stamp + ') ' + note + NL;
|
|
2832
|
+
await fs.writeFile(memFile, body, 'utf-8');
|
|
2833
|
+
transcript.log('remember', { note, file: memFile });
|
|
2834
|
+
console.log(theme.assistant(t('remember.saved', { v: memFile })));
|
|
2835
|
+
continue;
|
|
2836
|
+
}
|
|
2774
2837
|
if (lower === '/init' || lower.startsWith('/init ')) {
|
|
2775
2838
|
const force = lower.includes('--force');
|
|
2776
2839
|
const target = path.join(currentWorkdir, 'AGENTS.md');
|
|
@@ -3037,15 +3100,6 @@ async function main() {
|
|
|
3037
3100
|
}, t)));
|
|
3038
3101
|
continue;
|
|
3039
3102
|
}
|
|
3040
|
-
if (lower === '/permissions' || lower === '/allowed-tools') {
|
|
3041
|
-
console.log(theme.system(renderPermissions({
|
|
3042
|
-
write: config.confirmation.write,
|
|
3043
|
-
edit: config.confirmation.edit,
|
|
3044
|
-
bash: config.confirmation.bash,
|
|
3045
|
-
alwaysConfirm: config.confirmation.alwaysConfirm,
|
|
3046
|
-
}, t)));
|
|
3047
|
-
continue;
|
|
3048
|
-
}
|
|
3049
3103
|
if (lower === '/add-dir' || lower.startsWith('/add-dir ')) {
|
|
3050
3104
|
const arg = trimmed.slice('/add-dir'.length).trim();
|
|
3051
3105
|
const res = resolveExtraDir(arg, currentWorkdir, t);
|
package/dist/input.js
CHANGED
|
@@ -1298,6 +1298,11 @@ export class LineEditor {
|
|
|
1298
1298
|
// dots) and keep it running until toolResult()/assistant()/stop(). Esc now
|
|
1299
1299
|
// really aborts the tool (the Bash child process is killed via the
|
|
1300
1300
|
// AbortSignal), so the "Esc — стоп" hint is shown.
|
|
1301
|
+
//
|
|
1302
|
+
// The tool status is NOT a send phase: clear the lifecycle prefix first,
|
|
1303
|
+
// otherwise the previous answer's "✓ done" sticks and the status reads
|
|
1304
|
+
// "✓ done: running Bash" on one line.
|
|
1305
|
+
this._sendState = '';
|
|
1301
1306
|
this._startAnimated(translate(this.locale)('spinner.running_tool', { name }));
|
|
1302
1307
|
}
|
|
1303
1308
|
toolResult(result) {
|
package/dist/self-review.js
CHANGED
|
@@ -61,6 +61,45 @@ async function copyDirJsFiles(from, to) {
|
|
|
61
61
|
}
|
|
62
62
|
return copied;
|
|
63
63
|
}
|
|
64
|
+
// Root files and folders a review often needs but that are NOT part of the
|
|
65
|
+
// editable src/ set (scripts, lint/ts configs). They are copied into a
|
|
66
|
+
// read-only `_context/` subfolder so the reviewer can READ them while the
|
|
67
|
+
// .ts-only diff/apply logic stays untouched (a stray edit here would never be
|
|
68
|
+
// applied back and would only confuse the report).
|
|
69
|
+
const CONTEXT_FILES = [
|
|
70
|
+
'package.json',
|
|
71
|
+
'eslint.config.js',
|
|
72
|
+
'.prettierrc',
|
|
73
|
+
'tsconfig.json',
|
|
74
|
+
'tsconfig.build.json',
|
|
75
|
+
];
|
|
76
|
+
async function copyContextFiles(srcDir, snapDir) {
|
|
77
|
+
const repoRoot = path.resolve(srcDir, '..');
|
|
78
|
+
const ctxDir = path.join(snapDir, '_context');
|
|
79
|
+
for (const rel of CONTEXT_FILES) {
|
|
80
|
+
const data = await fs
|
|
81
|
+
.readFile(path.join(repoRoot, rel), 'utf-8')
|
|
82
|
+
.catch(() => null);
|
|
83
|
+
if (data === null)
|
|
84
|
+
continue;
|
|
85
|
+
await ensureDir(ctxDir);
|
|
86
|
+
await fs.writeFile(path.join(ctxDir, rel), data, 'utf-8');
|
|
87
|
+
}
|
|
88
|
+
const scripts = await fs
|
|
89
|
+
.readdir(path.join(repoRoot, 'scripts'))
|
|
90
|
+
.catch(() => []);
|
|
91
|
+
if (scripts.length) {
|
|
92
|
+
await ensureDir(path.join(ctxDir, 'scripts'));
|
|
93
|
+
for (const f of scripts) {
|
|
94
|
+
const data = await fs
|
|
95
|
+
.readFile(path.join(repoRoot, 'scripts', f), 'utf-8')
|
|
96
|
+
.catch(() => null);
|
|
97
|
+
if (data === null)
|
|
98
|
+
continue;
|
|
99
|
+
await fs.writeFile(path.join(ctxDir, 'scripts', f), data, 'utf-8');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
64
103
|
// ---------- /self-review ----------
|
|
65
104
|
export async function selfReview({ browser, focus, transcript, locale, }) {
|
|
66
105
|
const t = translator(locale);
|
|
@@ -74,12 +113,9 @@ export async function selfReview({ browser, focus, transcript, locale, }) {
|
|
|
74
113
|
console.error(theme.error('\n✖ ' + t('self.no_src', { dir: SRC_DIR })));
|
|
75
114
|
throw new Error(t('self.no_src_err'));
|
|
76
115
|
}
|
|
77
|
-
// Copy
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
await fs.writeFile(path.join(snapDir, 'package.json'), pkg, 'utf-8');
|
|
81
|
-
}
|
|
82
|
-
catch { }
|
|
116
|
+
// Copy root configs/scripts into a read-only _context/ subfolder so the
|
|
117
|
+
// reviewer can read them (the editable set stays .ts-only).
|
|
118
|
+
await copyContextFiles(SRC_DIR, snapDir);
|
|
83
119
|
console.log(theme.system('\n' + t('self.snapshot', { dir: snapDir })));
|
|
84
120
|
console.log(theme.system(t('self.files', { n: copied.length, list: copied.join(', ') })));
|
|
85
121
|
console.log(theme.system(t('self.starting') + '\n'));
|
|
@@ -248,6 +284,8 @@ function buildReviewPrompt({ focus, snapDir, }) {
|
|
|
248
284
|
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.
|
|
249
285
|
|
|
250
286
|
All source files are in your working directory: ${snapDir}
|
|
287
|
+
Root configs and scripts (package.json, tsconfig, eslint, scripts/) are copied
|
|
288
|
+
into the read-only _context/ subfolder there — read them when relevant.
|
|
251
289
|
${focusLine}
|
|
252
290
|
## Your job
|
|
253
291
|
|
package/dist/shell.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
// Shared shell runner for the Bash tool and the operator's `!command` escape.
|
|
4
|
+
// Kept in its own module so both call sites use the SAME sandbox guard and the
|
|
5
|
+
// SAME abort wiring; a second, ad-hoc runner would drift from the real one.
|
|
6
|
+
// Sandbox (option A): don't let a shell command go above the project root.
|
|
7
|
+
// This is a protective barrier, not full OS isolation.
|
|
8
|
+
export function assertCommandInsideRoot(root, command) {
|
|
9
|
+
const cmd = String(command || '');
|
|
10
|
+
const cdRe = /(?:^|[;&|]|\s)(?:cd|pushd)\s+([^;&|]+)/gi;
|
|
11
|
+
let m;
|
|
12
|
+
while ((m = cdRe.exec(cmd))) {
|
|
13
|
+
const raw = m[1].trim();
|
|
14
|
+
if (!raw || raw === '-')
|
|
15
|
+
continue;
|
|
16
|
+
const target = path.resolve(root, raw);
|
|
17
|
+
const rel = path.relative(root, target);
|
|
18
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
19
|
+
throw new Error('Sandbox: leaving ' + root + ' is forbidden (cd ' + raw + ')');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function runShell(workdir, command, timeout = 30_000, signal) {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
const options = {
|
|
26
|
+
cwd: workdir,
|
|
27
|
+
timeout,
|
|
28
|
+
maxBuffer: 1024 * 1024 * 8,
|
|
29
|
+
windowsHide: true,
|
|
30
|
+
env: { ...process.env },
|
|
31
|
+
// Abort the child process when the operator pressed Esc/Ctrl+C.
|
|
32
|
+
// Without it a long `npm test` kept running even after the stop.
|
|
33
|
+
signal,
|
|
34
|
+
};
|
|
35
|
+
if (process.platform === 'win32') {
|
|
36
|
+
options.shell = process.env.ComSpec || 'C:\\Windows\\System32\\cmd.exe';
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
options.shell = '/bin/sh';
|
|
40
|
+
}
|
|
41
|
+
exec(command, options, (err, stdout, stderr) => {
|
|
42
|
+
const out = (stdout || '').toString();
|
|
43
|
+
const errStr = (stderr || '').toString();
|
|
44
|
+
if (!err) {
|
|
45
|
+
const combined = (out + errStr).trim();
|
|
46
|
+
resolve(combined || '(command produced no output)');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const parts = [];
|
|
50
|
+
if (err.killed) {
|
|
51
|
+
parts.push('\u23f1 Timeout after ' + timeout + 'ms \u2014 process killed.');
|
|
52
|
+
}
|
|
53
|
+
else if (err.code === 'ABORT_ERR') {
|
|
54
|
+
parts.push('(aborted by the operator)');
|
|
55
|
+
}
|
|
56
|
+
else if (err.code !== undefined && err.code !== null) {
|
|
57
|
+
parts.push('Exit code: ' + err.code);
|
|
58
|
+
}
|
|
59
|
+
else if (err.signal) {
|
|
60
|
+
parts.push('Killed by signal: ' + err.signal);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
parts.push('Error: ' + err.message);
|
|
64
|
+
}
|
|
65
|
+
if (out.trim())
|
|
66
|
+
parts.push('stdout:\n' + out.trim());
|
|
67
|
+
if (errStr.trim())
|
|
68
|
+
parts.push('stderr:\n' + errStr.trim());
|
|
69
|
+
resolve(parts.join('\n'));
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
package/dist/tools.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import {
|
|
3
|
+
import { assertCommandInsideRoot, runShell } from './shell.js';
|
|
4
4
|
import { createGitTools } from './gitTools.js';
|
|
5
5
|
import { createWebTools } from './web.js';
|
|
6
6
|
import { createExtraTools } from './extraTools.js';
|
|
@@ -20,71 +20,6 @@ export function createTools(workdir, { undo, todos } = {}) {
|
|
|
20
20
|
}
|
|
21
21
|
return resolved;
|
|
22
22
|
};
|
|
23
|
-
// Sandbox (option A): don't let the command go above root.
|
|
24
|
-
// This is a protective barrier, not full OS isolation.
|
|
25
|
-
const assertCommandInsideRoot = (command) => {
|
|
26
|
-
const cmd = String(command || '');
|
|
27
|
-
const cdRe = /(?:^|[;&|]|\s)(?:cd|pushd)\s+([^;&|]+)/gi;
|
|
28
|
-
let m;
|
|
29
|
-
while ((m = cdRe.exec(cmd))) {
|
|
30
|
-
const raw = m[1].trim();
|
|
31
|
-
if (!raw || raw === '-')
|
|
32
|
-
continue;
|
|
33
|
-
const target = path.resolve(root, raw);
|
|
34
|
-
const rel = path.relative(root, target);
|
|
35
|
-
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
36
|
-
throw new Error('Sandbox: leaving ' + root + ' is forbidden (cd ' + raw + ')');
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
const runShell = (command, timeout = 30_000, signal) => new Promise((resolve) => {
|
|
41
|
-
const options = {
|
|
42
|
-
cwd: workdir,
|
|
43
|
-
timeout,
|
|
44
|
-
maxBuffer: 1024 * 1024 * 8,
|
|
45
|
-
windowsHide: true,
|
|
46
|
-
env: { ...process.env },
|
|
47
|
-
// Abort the child process when the operator pressed Esc/Ctrl+C.
|
|
48
|
-
// Without it a long `npm test` kept running even after the stop.
|
|
49
|
-
signal,
|
|
50
|
-
};
|
|
51
|
-
if (process.platform === 'win32') {
|
|
52
|
-
options.shell = process.env.ComSpec || 'C:\\Windows\\System32\\cmd.exe';
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
55
|
-
options.shell = '/bin/sh';
|
|
56
|
-
}
|
|
57
|
-
exec(command, options, (err, stdout, stderr) => {
|
|
58
|
-
const out = (stdout || '').toString();
|
|
59
|
-
const errStr = (stderr || '').toString();
|
|
60
|
-
if (!err) {
|
|
61
|
-
const combined = (out + errStr).trim();
|
|
62
|
-
resolve(combined || '(command produced no output)');
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
const parts = [];
|
|
66
|
-
if (err.killed) {
|
|
67
|
-
parts.push(`⏱ Timeout after ${timeout}ms — process killed.`);
|
|
68
|
-
}
|
|
69
|
-
else if (err.code === 'ABORT_ERR') {
|
|
70
|
-
parts.push('(aborted by the operator)');
|
|
71
|
-
}
|
|
72
|
-
else if (err.code !== undefined && err.code !== null) {
|
|
73
|
-
parts.push(`Exit code: ${err.code}`);
|
|
74
|
-
}
|
|
75
|
-
else if (err.signal) {
|
|
76
|
-
parts.push(`Killed by signal: ${err.signal}`);
|
|
77
|
-
}
|
|
78
|
-
else {
|
|
79
|
-
parts.push(`Error: ${err.message}`);
|
|
80
|
-
}
|
|
81
|
-
if (out.trim())
|
|
82
|
-
parts.push(`stdout:\n${out.trim()}`);
|
|
83
|
-
if (errStr.trim())
|
|
84
|
-
parts.push(`stderr:\n${errStr.trim()}`);
|
|
85
|
-
resolve(parts.join('\n'));
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
23
|
// Extracts text from content/content_base64. base64 is needed because the
|
|
89
24
|
// channel that delivers the model's answer may corrupt characters ($,
|
|
90
25
|
// backslashes, newlines). base64 consists only of [A-Za-z0-9+/=] and is not
|
|
@@ -202,8 +137,8 @@ export function createTools(workdir, { undo, todos } = {}) {
|
|
|
202
137
|
'For git use the Git* tools. For the web use WebFetch / WebSearch.',
|
|
203
138
|
parameters: { command: 'string', timeout: 'number?' },
|
|
204
139
|
fn: async ({ command, timeout }, ctx) => {
|
|
205
|
-
assertCommandInsideRoot(req(command, 'command'));
|
|
206
|
-
return runShell(req(command, 'command'), timeout, ctx?.signal);
|
|
140
|
+
assertCommandInsideRoot(root, req(command, 'command'));
|
|
141
|
+
return runShell(workdir, req(command, 'command'), timeout, ctx?.signal);
|
|
207
142
|
},
|
|
208
143
|
},
|
|
209
144
|
{
|
|
@@ -245,11 +180,11 @@ export function createTools(workdir, { undo, todos } = {}) {
|
|
|
245
180
|
if (process.platform === 'win32') {
|
|
246
181
|
const escaped = pat.replace(/"/g, '\\"');
|
|
247
182
|
const scope = searchPath ? '"' + target + '\\*' : '*';
|
|
248
|
-
return runShell(`findstr /s /n /r /c:"${escaped}" ` + scope);
|
|
183
|
+
return runShell(workdir, `findstr /s /n /r /c:"${escaped}" ` + scope);
|
|
249
184
|
}
|
|
250
185
|
const flags = mode === 'files_only' ? '-rlE' : mode === 'count' ? '-rcE' : '-rnE';
|
|
251
186
|
const includeArg = inc ? ' --include=' + JSON.stringify(inc) : '';
|
|
252
|
-
return runShell(`grep ${flags}${includeArg} ${JSON.stringify(pat)} ` +
|
|
187
|
+
return runShell(workdir, `grep ${flags}${includeArg} ${JSON.stringify(pat)} ` +
|
|
253
188
|
`${JSON.stringify(target)} || true`);
|
|
254
189
|
},
|
|
255
190
|
},
|
package/dist/web.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { chromium } from 'playwright';
|
|
2
|
+
import { lookup } from 'node:dns/promises';
|
|
3
|
+
import { isIP } from 'node:net';
|
|
2
4
|
const DEFAULT_TIMEOUT = 20_000;
|
|
3
5
|
const MAX_TEXT = 12_000;
|
|
4
6
|
// Removes scripts, styles, and turns HTML into readable text.
|
|
@@ -42,6 +44,102 @@ function htmlToText(html) {
|
|
|
42
44
|
.join('\n');
|
|
43
45
|
return s;
|
|
44
46
|
}
|
|
47
|
+
// ---------- SSRF guard ----------
|
|
48
|
+
//
|
|
49
|
+
// WebFetch takes an arbitrary URL from the model. Without a guard it could be
|
|
50
|
+
// talked into reading cloud metadata (169.254.169.254), internal admin panels
|
|
51
|
+
// or localhost services. We resolve the hostname and refuse private / loopback
|
|
52
|
+
// / link-local addresses. Best-effort: a DNS rebind between the check and the
|
|
53
|
+
// fetch is out of scope for a CLI tool, but the common cases are covered.
|
|
54
|
+
export function isPrivateIp(ip) {
|
|
55
|
+
const v4 = ip.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
56
|
+
if (v4) {
|
|
57
|
+
const [a, b] = v4.slice(1).map(Number);
|
|
58
|
+
if (a === 10 || a === 127 || a === 0)
|
|
59
|
+
return true;
|
|
60
|
+
if (a === 169 && b === 254)
|
|
61
|
+
return true; // link-local + cloud metadata
|
|
62
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
63
|
+
return true;
|
|
64
|
+
if (a === 192 && b === 168)
|
|
65
|
+
return true;
|
|
66
|
+
if (a === 100 && b >= 64 && b <= 127)
|
|
67
|
+
return true; // CGNAT
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
const low = ip.toLowerCase();
|
|
71
|
+
if (low === '::1' || low === '::')
|
|
72
|
+
return true;
|
|
73
|
+
if (low.startsWith('fe80') || low.startsWith('fc') || low.startsWith('fd')) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
const mapped = low.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
|
|
77
|
+
if (mapped)
|
|
78
|
+
return isPrivateIp(mapped[1]);
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
export function isPrivateHostname(host) {
|
|
82
|
+
const h = host.toLowerCase().replace(/\.$/, '');
|
|
83
|
+
if (!h)
|
|
84
|
+
return true;
|
|
85
|
+
if (h === 'localhost' || h.endsWith('.localhost'))
|
|
86
|
+
return true;
|
|
87
|
+
if (h === 'metadata.google.internal')
|
|
88
|
+
return true;
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
async function assertPublicUrl(rawUrl) {
|
|
92
|
+
const u = new URL(rawUrl);
|
|
93
|
+
if (u.protocol !== 'http:' && u.protocol !== 'https:') {
|
|
94
|
+
throw new Error(`unsupported protocol: ${u.protocol}`);
|
|
95
|
+
}
|
|
96
|
+
const host = u.hostname;
|
|
97
|
+
if (isPrivateHostname(host)) {
|
|
98
|
+
throw new Error(`blocked private host: ${host}`);
|
|
99
|
+
}
|
|
100
|
+
if (isIP(host)) {
|
|
101
|
+
if (isPrivateIp(host))
|
|
102
|
+
throw new Error(`blocked private address: ${host}`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const addrs = await lookup(host, { all: true });
|
|
107
|
+
for (const a of addrs) {
|
|
108
|
+
if (isPrivateIp(a.address)) {
|
|
109
|
+
throw new Error(`blocked private address for ${host}: ${a.address}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
// Re-throw our own block, but let a DNS failure fall through so fetch()
|
|
115
|
+
// surfaces the real network error instead of a misleading one.
|
|
116
|
+
if (e.message.startsWith('blocked'))
|
|
117
|
+
throw e;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
121
|
+
// fetch with retries for transient network errors and 5xx responses.
|
|
122
|
+
async function httpFetchWithRetry(url, opts = {}, retries = 2) {
|
|
123
|
+
let lastErr;
|
|
124
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
125
|
+
try {
|
|
126
|
+
const r = await httpFetch(url, opts);
|
|
127
|
+
if (r.status >= 500 && attempt < retries) {
|
|
128
|
+
await sleep(300 * 2 ** attempt);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
return r;
|
|
132
|
+
}
|
|
133
|
+
catch (e) {
|
|
134
|
+
lastErr = e;
|
|
135
|
+
if (attempt < retries) {
|
|
136
|
+
await sleep(300 * 2 ** attempt);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
|
|
142
|
+
}
|
|
45
143
|
// fetch with redirects and a timeout
|
|
46
144
|
async function httpFetch(url, { timeout = DEFAULT_TIMEOUT, headers = {}, } = {}) {
|
|
47
145
|
const ctrl = new AbortController();
|
|
@@ -126,12 +224,13 @@ export function createWebTools() {
|
|
|
126
224
|
}
|
|
127
225
|
const limit = Math.min(Number(maxChars) || MAX_TEXT, 60_000);
|
|
128
226
|
try {
|
|
227
|
+
await assertPublicUrl(String(url));
|
|
129
228
|
if (render) {
|
|
130
229
|
const r = await renderWithHeadless(String(url));
|
|
131
230
|
const text = htmlToText(r.html);
|
|
132
231
|
return formatResult(r.status, r.url, text, limit);
|
|
133
232
|
}
|
|
134
|
-
const r = await
|
|
233
|
+
const r = await httpFetchWithRetry(String(url));
|
|
135
234
|
// If it's JSON/plain text — return as is
|
|
136
235
|
if (/application\/json|text\/plain|text\/markdown/i.test(r.contentType)) {
|
|
137
236
|
const trimmed = r.body.slice(0, limit);
|
|
@@ -168,7 +267,7 @@ export function createWebTools() {
|
|
|
168
267
|
return 'Error: query is empty.';
|
|
169
268
|
const limit = Math.min(Math.max(Number(maxResults) || 8, 1), 20);
|
|
170
269
|
try {
|
|
171
|
-
const r = await
|
|
270
|
+
const r = await httpFetchWithRetry(`https://html.duckduckgo.com/html/?q=${q}`, {
|
|
172
271
|
timeout: 25_000,
|
|
173
272
|
headers: {
|
|
174
273
|
Accept: 'text/html,application/xhtml+xml',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.55.0",
|
|
4
4
|
"description": "Terminal coding agent over chat.deepseek.com via Playwright",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"engines": {
|
|
41
41
|
"node": ">=20"
|
|
42
42
|
},
|
|
43
|
+
"packageManager": "npm@11.19.0",
|
|
43
44
|
"dependencies": {
|
|
44
45
|
"@modelcontextprotocol/sdk": "^1.30.1",
|
|
45
46
|
"chalk": "^5.3.0",
|
package/dist/confirm.js
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import * as readlinePromises from 'readline/promises';
|
|
2
|
-
import { theme } from './theme.js';
|
|
3
|
-
import { unifiedDiff, colorDiff } from './diff.js';
|
|
4
|
-
import { translate, DEFAULT_LOCALE } from './i18n.js';
|
|
5
|
-
export class ConfirmManager {
|
|
6
|
-
settings;
|
|
7
|
-
alwaysConfirm;
|
|
8
|
-
allowedForSession;
|
|
9
|
-
locale;
|
|
10
|
-
constructor({ config, locale = DEFAULT_LOCALE, } = {}) {
|
|
11
|
-
const c = config || {};
|
|
12
|
-
this.locale = locale;
|
|
13
|
-
this.settings = {
|
|
14
|
-
write: c.write !== false,
|
|
15
|
-
edit: c.edit !== false,
|
|
16
|
-
bash: c.bash !== false,
|
|
17
|
-
};
|
|
18
|
-
// An invalid operator-supplied regex must NEVER crash the run. Fall back to
|
|
19
|
-
// a literal substring match (case-insensitive) for that pattern.
|
|
20
|
-
this.alwaysConfirm = (c.alwaysConfirm || []).map((p) => {
|
|
21
|
-
try {
|
|
22
|
-
return new RegExp(p, 'i');
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
return new RegExp(escapeRegExp(p), 'i');
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
this.allowedForSession = new Set();
|
|
29
|
-
}
|
|
30
|
-
_matchesAlwaysConfirm(command) {
|
|
31
|
-
return this.alwaysConfirm.some((re) => re.test(command));
|
|
32
|
-
}
|
|
33
|
-
shouldAsk(kind, target = null) {
|
|
34
|
-
if (kind === 'bash' && target && this._matchesAlwaysConfirm(target)) {
|
|
35
|
-
return true;
|
|
36
|
-
}
|
|
37
|
-
if (!this.settings[kind])
|
|
38
|
-
return false;
|
|
39
|
-
if (this.allowedForSession.has(kind))
|
|
40
|
-
return false;
|
|
41
|
-
return true;
|
|
42
|
-
}
|
|
43
|
-
// Hot-update the language (from /config lang). The manager holds the locale
|
|
44
|
-
// so its prompt follows the interface language without a restart.
|
|
45
|
-
setLocale(locale) {
|
|
46
|
-
this.locale = locale;
|
|
47
|
-
}
|
|
48
|
-
async ask({ kind, target, preview = null, message, }) {
|
|
49
|
-
if (!this.shouldAsk(kind, target ?? null))
|
|
50
|
-
return true;
|
|
51
|
-
if (preview) {
|
|
52
|
-
console.log(preview);
|
|
53
|
-
}
|
|
54
|
-
const t = translate(this.locale);
|
|
55
|
-
const label = message || t('confirm.ask_label') + ' ' + kind + '?';
|
|
56
|
-
const question = theme.warn(`${label} `) +
|
|
57
|
-
theme.system('[') +
|
|
58
|
-
theme.assistant('y') +
|
|
59
|
-
theme.system(' = ' + t('common.yes') + ', ') +
|
|
60
|
-
theme.error('n') +
|
|
61
|
-
theme.system(' = ' + t('common.no') + ', ') +
|
|
62
|
-
theme.user('a') +
|
|
63
|
-
theme.system(' = ' + t('confirm.hint') + '] ');
|
|
64
|
-
const answer = await this._prompt(question);
|
|
65
|
-
if (answer === 'a') {
|
|
66
|
-
if (kind !== 'bash' || !this._matchesAlwaysConfirm(target ?? '')) {
|
|
67
|
-
this.allowedForSession.add(kind);
|
|
68
|
-
}
|
|
69
|
-
return true;
|
|
70
|
-
}
|
|
71
|
-
return answer === 'y';
|
|
72
|
-
}
|
|
73
|
-
async _prompt(question) {
|
|
74
|
-
process.stdin.resume();
|
|
75
|
-
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
76
|
-
process.stdin.setRawMode(false);
|
|
77
|
-
}
|
|
78
|
-
const rl = readlinePromises.createInterface({
|
|
79
|
-
input: process.stdin,
|
|
80
|
-
output: process.stdout,
|
|
81
|
-
});
|
|
82
|
-
try {
|
|
83
|
-
const ans = await rl.question(question);
|
|
84
|
-
const v = (ans || '').trim().toLowerCase();
|
|
85
|
-
if (v === 'a' || v === 'always' || v === 'в')
|
|
86
|
-
return 'a';
|
|
87
|
-
if (v === 'y' || v === 'yes' || v === 'д' || v === 'да')
|
|
88
|
-
return 'y';
|
|
89
|
-
return 'n';
|
|
90
|
-
}
|
|
91
|
-
finally {
|
|
92
|
-
rl.close();
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
// Escape a string so it can be used as a literal regex (used when an
|
|
97
|
-
// operator-supplied alwaysConfirm pattern is invalid and we fall back to a
|
|
98
|
-
// substring match).
|
|
99
|
-
function escapeRegExp(s) {
|
|
100
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, (m) => '\\' + m);
|
|
101
|
-
}
|
|
102
|
-
export function formatDiffPreview(oldText, newText, { label } = {}) {
|
|
103
|
-
const diff = unifiedDiff(oldText, newText, { label });
|
|
104
|
-
return colorDiff(diff);
|
|
105
|
-
}
|