zames_pro 2.9.0 → 2.9.2

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.
@@ -171,7 +171,7 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
171
171
  trimmed.length < 2 ||
172
172
  /^(reading|thinking|searching|analyzing|generating|stop|остановить|читаю|думаю|поиск|анализ)[\s.…]*$/i.test(trimmed) ||
173
173
  (trimmed.length <= 200 &&
174
- /(messages? too frequent|too many requests|rate limit|слишком часто|try again later)/i.test(trimmed));
174
+ /(messages? too frequent|too many requests|rate limit|server (is )?busy|service (is )?unavailable|слишком часто|try again later)/i.test(trimmed));
175
175
  if (looksService && stallRetries < MAX_STALL_RETRIES) {
176
176
  stallRetries++;
177
177
  transcript?.log('stall_retry', {
@@ -2,6 +2,8 @@ import fs from 'fs/promises';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
4
  import { execFileSync } from 'child_process';
5
+ import { extractPathToken } from './path-token.js';
6
+ export { extractPathToken };
5
7
  export const IMAGE_EXTS = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg', '.ico'];
6
8
  export const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
7
9
  function sanitize(name) {
@@ -182,6 +184,15 @@ export async function saveToTemp(tmpDir, name, data) {
182
184
  await fs.writeFile(target, data);
183
185
  return target;
184
186
  }
187
+ // True if any OS clipboard tool is available on this machine.
188
+ export function hasClipboardTool() {
189
+ if (process.platform === 'win32' || process.platform === 'darwin')
190
+ return true;
191
+ if (process.platform === 'linux') {
192
+ return Boolean(tryCommand('xclip', ['-version']) || tryCommand('xsel', ['--version']) || tryCommand('wl-paste', ['--version']));
193
+ }
194
+ return false;
195
+ }
185
196
  // Read an image from the OS clipboard. Tries, in order, every known tool for
186
197
  // the current platform. Returns the raw bytes plus a short note about which
187
198
  // tool was used (or the reason nothing was found) — the caller shows that note
@@ -189,38 +200,53 @@ export async function saveToTemp(tmpDir, name, data) {
189
200
  // True when running inside WSL. There the X11 clipboard is a SEPARATE
190
201
  // clipboard from the Windows one: the user copies an image in Windows, so we
191
202
  // must read it through powershell.exe, not xclip.
203
+ export function windowsReachable() {
204
+ if (process.platform === 'win32')
205
+ return true;
206
+ if (process.platform !== 'linux')
207
+ return false;
208
+ try {
209
+ return require('fs').existsSync('/mnt/c/Windows');
210
+ }
211
+ catch {
212
+ return false;
213
+ }
214
+ }
192
215
  export function isWsl() {
193
216
  if (process.platform !== 'linux')
194
217
  return false;
218
+ let kernelWsl = false;
195
219
  if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP)
196
- return true;
197
- try {
198
- const rel = os.release().toLowerCase();
199
- if (rel.includes('microsoft') || rel.includes('wsl'))
200
- return true;
220
+ kernelWsl = true;
221
+ if (!kernelWsl) {
222
+ try {
223
+ const rel = os.release().toLowerCase();
224
+ if (rel.includes('microsoft') || rel.includes('wsl'))
225
+ kernelWsl = true;
226
+ }
227
+ catch { }
201
228
  }
202
- catch { }
203
- return false;
229
+ return kernelWsl && windowsReachable();
204
230
  }
205
- // PowerShell one-liner: write the clipboard image as raw PNG bytes to stdout.
206
- const PS_GET_IMAGE = 'Add-Type -AssemblyName System.Windows.Forms,System.Drawing; ' +
207
- '$img=[System.Windows.Forms.Clipboard]::GetImage(); ' +
208
- 'if($img){$ms=New-Object System.IO.MemoryStream; ' +
209
- '$img.Save($ms,[System.Drawing.Imaging.ImageFormat]::Png); ' +
210
- '[Console]::OpenStandardOutput().Write($ms.ToArray(),0,$ms.Length)}';
231
+ // PowerShell image grabber, embedded as a UTF-16LE base64 script and run
232
+ // via powershell.exe -EncodedCommand: it saves the clipboard image to a
233
+ // Windows temp .png and prints the Windows path; we read it from WSL.
234
+ const PS_IMAGE_ENCODED = 'QQBkAGQALQBUAHkAcABlACAALQBBAHMAcwBlAG0AYgBsAHkATgBhAG0AZQAgAFMAeQBzAHQAZQBtAC4AVwBpAG4AZABvAHcAcwAuAEYAbwByAG0AcwAsAFMAeQBzAHQAZQBtAC4ARAByAGEAdwBpAG4AZwAKACQAaQBtAGcAPQBbAFMAeQBzAHQAZQBtAC4AVwBpAG4AZABvAHcAcwAuAEYAbwByAG0AcwAuAEMAbABpAHAAYgBvAGEAcgBkAF0AOgA6AEcAZQB0AEkAbQBhAGcAZQAoACkACgBpAGYAKAAkAGkAbQBnACkAewAkAHAAPQBKAG8AaQBuAC0AUABhAHQAaAAgACQAZQBuAHYAOgBUAEUATQBQACAAKAAiAHoAYQBtAGUAcwAtAGMAbABpAHAALQAiACsAWwBHAHUAaQBkAF0AOgA6AE4AZQB3AEcAdQBpAGQAKAApAC4AVABvAFMAdAByAGkAbgBnACgAKQArACIALgBwAG4AZwAiACkAOwAkAGkAbQBnAC4AUwBhAHYAZQAoACQAcAAsAFsAUwB5AHMAdABlAG0ALgBEAHIAYQB3AGkAbgBnAC4ASQBtAGEAZwBpAG4AZwAuAEkAbQBhAGcAZQBGAG8AcgBtAGEAdABdADoAOgBQAG4AZwApADsAVwByAGkAdABlAC0ATwB1AHQAcAB1AHQAIAAkAHAAfQAKAA==';
211
235
  // PowerShell one-liner: write the clipboard file paths (CF_HDROP) to stdout.
212
236
  const PS_GET_FILE_DROP = 'Add-Type -AssemblyName System.Windows.Forms; ' +
213
237
  '$d=[System.Windows.Forms.Clipboard]::GetDataObject(); ' +
214
238
  'if($d -and $d.GetDataPresent([System.Windows.Forms.DataFormats]::FileDrop)){' +
215
239
  '$f=$d.GetData([System.Windows.Forms.DataFormats]::FileDrop); $f -join [Environment]::NewLine}';
216
- export function readClipboardImageDetailed() {
240
+ export async function readClipboardImageDetailed() {
217
241
  if (process.platform === 'linux' || process.platform === 'freebsd' || process.platform === 'openbsd') {
218
242
  const wayland = process.env.WAYLAND_DISPLAY;
219
243
  const x11 = process.env.DISPLAY;
220
244
  const attempts = [];
221
245
  // On WSL the Windows clipboard is what the user copies into — read it first.
222
246
  if (isWsl()) {
223
- attempts.push({ bin: 'powershell.exe', args: ['-NoProfile', '-NonInteractive', '-Command', PS_GET_IMAGE], via: 'powershell.exe' });
247
+ const win = await readWindowsClipboardImage();
248
+ if (win)
249
+ return { data: win, via: 'powershell.exe' };
224
250
  }
225
251
  if (wayland || !x11)
226
252
  attempts.push({ bin: 'wl-paste', args: ['--type', 'image/png', '--no-newline'], via: 'wl-paste' });
@@ -261,8 +287,8 @@ export function readClipboardImageDetailed() {
261
287
  return { data: null, via: 'unsupported-platform' };
262
288
  }
263
289
  // Backwards-compatible wrapper used elsewhere.
264
- export function readClipboardImage() {
265
- return readClipboardImageDetailed().data;
290
+ export async function readClipboardImage() {
291
+ return (await readClipboardImageDetailed()).data;
266
292
  }
267
293
  function tryCommand(bin, args) {
268
294
  try {
@@ -303,7 +329,7 @@ export async function findFileByName(workdir, name) {
303
329
  export function readWindowsClipboardFiles() {
304
330
  if (!isWsl())
305
331
  return [];
306
- const out = tryCommand('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', PS_GET_FILE_DROP]);
332
+ const out = tryCommand('powershell.exe', ['-STA', '-NoProfile', '-NonInteractive', '-Command', PS_GET_FILE_DROP]);
307
333
  if (!out)
308
334
  return [];
309
335
  const text = out.toString('utf-8');
@@ -330,3 +356,23 @@ export function winPathToWsl(winPath) {
330
356
  const rest = p.slice(3).split('\\').join('/');
331
357
  return '/mnt/' + drive + '/' + rest;
332
358
  }
359
+ // Run the embedded PowerShell script and return the clipboard image bytes.
360
+ export async function readWindowsClipboardImage() {
361
+ if (!isWsl())
362
+ return null;
363
+ const out = tryCommand('powershell.exe', ['-STA', '-NoProfile', '-NonInteractive', '-EncodedCommand', PS_IMAGE_ENCODED]);
364
+ if (!out)
365
+ return null;
366
+ const first = out.toString('utf-8').trim().split(String.fromCharCode(10))[0].trim();
367
+ if (!first)
368
+ return null;
369
+ const wslPath = winPathToWsl(first);
370
+ if (!wslPath)
371
+ return null;
372
+ try {
373
+ return await fs.readFile(wslPath);
374
+ }
375
+ catch {
376
+ return null;
377
+ }
378
+ }
package/dist/browser.js CHANGED
@@ -40,9 +40,20 @@ const STOP_SELECTORS = [
40
40
  const STATUS_RE = /^(reading|thinking|searching|analyzing|generating|stop|остановить|читаю|думаю|поиск|анализ)[\s.…]*$/i;
41
41
  // DeepSeek's answer when the rate limit is exceeded.
42
42
  const RATE_LIMIT_RE = /(messages? too frequent|too many requests|rate limit|слишком часто|повторите позже|try again later)/i;
43
+ // Transient server-side hiccups (DeepSeek overloaded / hiccup). Unlike the
44
+ // rate limit, these usually clear in seconds, so we retry quickly instead of
45
+ // waiting minutes. "Server busy", 503, "temporarily unavailable", etc.
46
+ const SERVER_BUSY_RE = /(server (is )?busy|server error|service (is )?unavailable|temporarily unavailable|internal server error|502|503|504|server overloaded|сервер занят|сервер перегружен|сервис недоступен|внутренняя ошибка|попробуйте позже)/i;
43
47
  export function isRateLimitText(text) {
44
48
  return RATE_LIMIT_RE.test(String(text || ''));
45
49
  }
50
+ export function isServerBusyText(text) {
51
+ const t = String(text || '');
52
+ // Rate limit takes priority (it needs the long wait).
53
+ if (RATE_LIMIT_RE.test(t))
54
+ return false;
55
+ return SERVER_BUSY_RE.test(t);
56
+ }
46
57
  // Normalize text for comparison: collapse whitespace so that tiny DOM
47
58
  // differences (nbsp, trailing spaces) don't count as "a new answer".
48
59
  export function normText(s) {
@@ -59,6 +70,13 @@ export class RateLimitError extends Error {
59
70
  this.name = 'RateLimitError';
60
71
  }
61
72
  }
73
+ // DeepSeek transient server error (Server busy).
74
+ export class ServerBusyError extends Error {
75
+ constructor(detail) {
76
+ super('Server busy. Try again later. ' + detail);
77
+ this.name = 'ServerBusyError';
78
+ }
79
+ }
62
80
  // ---------- profile cleanup ----------
63
81
  async function killStaleChrome() {
64
82
  if (process.platform !== 'win32')
@@ -105,6 +123,8 @@ export class DeepSeekBrowser {
105
123
  minSendIntervalMs;
106
124
  rateLimitWaitMs;
107
125
  maxRateLimitRetries;
126
+ maxServerBusyRetries;
127
+ serverBusyWaitMs;
108
128
  _lastSentAt;
109
129
  _abort;
110
130
  // The user pressed Esc/Ctrl+C — a "stop" for the WHOLE current batch of
@@ -119,7 +139,7 @@ export class DeepSeekBrowser {
119
139
  _netSniff;
120
140
  _netSniffLimit;
121
141
  _netHookInstalled;
122
- constructor({ headless = false, debug = false, channel = 'chrome', answerTimeoutMs = 180000, askRetries = 3, stabilityChecks = 3, stabilityDelayMs = 1000, minSendIntervalMs = 15000, rateLimitWaitMs = 300000, maxRateLimitRetries = 6, } = {}) {
142
+ constructor({ headless = false, debug = false, channel = 'chrome', answerTimeoutMs = 180000, askRetries = 3, stabilityChecks = 3, stabilityDelayMs = 1000, minSendIntervalMs = 15000, rateLimitWaitMs = 300000, maxRateLimitRetries = 6, maxServerBusyRetries = 5, serverBusyWaitMs = 3000, } = {}) {
123
143
  this.headless = headless;
124
144
  this.debug = debug;
125
145
  this.channel = channel;
@@ -130,6 +150,8 @@ export class DeepSeekBrowser {
130
150
  this.minSendIntervalMs = minSendIntervalMs;
131
151
  this.rateLimitWaitMs = rateLimitWaitMs;
132
152
  this.maxRateLimitRetries = maxRateLimitRetries;
153
+ this.maxServerBusyRetries = maxServerBusyRetries;
154
+ this.serverBusyWaitMs = serverBusyWaitMs;
133
155
  this._lastSentAt = 0;
134
156
  this._abort = false;
135
157
  this._stopped = false;
@@ -420,6 +442,7 @@ export class DeepSeekBrowser {
420
442
  let lastErr = null;
421
443
  let attempt = 0;
422
444
  let rateLimitRetries = 0;
445
+ let serverBusyRetries = 0;
423
446
  while (attempt < this.askRetries) {
424
447
  attempt++;
425
448
  try {
@@ -629,6 +652,9 @@ export class DeepSeekBrowser {
629
652
  const pageText = await this._readPageText();
630
653
  if (isRateLimitText(pageText)) {
631
654
  throw new RateLimitError(pageText.slice(0, 300));
655
+ if (isServerBusyText(pageText)) {
656
+ throw new ServerBusyError(pageText.slice(0, 300));
657
+ }
632
658
  }
633
659
  const cur = await this._readLastAnswerTextClean().catch(() => '');
634
660
  const bodyLen = await this.page
@@ -672,6 +698,9 @@ export class DeepSeekBrowser {
672
698
  const pageText = await this._readPageText();
673
699
  if (isRateLimitText(pageText)) {
674
700
  throw new RateLimitError(pageText.slice(0, 300));
701
+ if (isServerBusyText(pageText)) {
702
+ throw new ServerBusyError(pageText.slice(0, 300));
703
+ }
675
704
  }
676
705
  }
677
706
  const cur = await this._readLastAnswerTextClean().catch(() => '');
package/dist/i18n.js CHANGED
@@ -150,8 +150,12 @@ const CATALOG = {
150
150
  en: '⚠ No image found in the clipboard (tried: {via}).',
151
151
  },
152
152
  'msg.clip_hint': {
153
- ru: ' Установите утилиту клипборда: Linux/X11 — xclip или xsel, Wayland — wl-clipboard; Windows/macOS — работает из коробки. Либо вставьте путь к файлу.',
154
- en: ' Install a clipboard tool: Linux/X11 — xclip or xsel, Wayland — wl-clipboard; Windows/macOS — works out of the box. Or paste a file path.',
153
+ ru: ' Проще всего: вставьте путь к файлу (например tmp/pic.png) — файл приложится к сообщению. Для клипборда нужна утилита (Linux/X11 — xclip или xsel, Wayland — wl-clipboard; Windows — PowerShell, macOS — pngpaste).',
154
+ en: ' Easiest: paste a path to a file (e.g. tmp/pic.png) — it will be attached to the message. Clipboard paste needs a tool (Linux/X11 — xclip or xsel, Wayland — wl-clipboard; Windows — PowerShell, macOS — pngpaste).',
155
+ },
156
+ 'msg.clip_container': {
157
+ ru: ' Похоже, вы в контейнере/Dev Container: клипборд Windows отсюда недоступен. Вставьте путь к файлу (например tmp/pic.png) — он приложится к сообщению.',
158
+ en: ' Looks like a container/Dev Container: the Windows clipboard is not reachable from here. Paste a path to a file (e.g. tmp/pic.png) — it will be attached to the message.',
155
159
  },
156
160
  'msg.from_queue': { ru: '▶ Из очереди: ', en: '▶ From queue: ' },
157
161
  'msg.agent_error': { ru: '✖ Ошибка агента:', en: '✖ Agent error:' },
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import { createTools } from './tools.js';
8
8
  import { runAgentLoop } from './agent-loop.js';
9
9
  import { createSpinner } from './spinner.js';
10
10
  import { LineEditor, expandPastes, pasteReplacement } from './input.js';
11
- import { parseImagePaste, extForMime, saveToTemp, guessMime, isImageName, formatSize, looksLikeFilePath, readClipboardImageDetailed, sniffMime, readWindowsClipboardFiles, } from './attachments.js';
11
+ import { parseImagePaste, extForMime, saveToTemp, guessMime, isImageName, formatSize, looksLikeFilePath, readClipboardImageDetailed, sniffMime, readWindowsClipboardFiles, hasClipboardTool, } from './attachments.js';
12
12
  import { loadConfig, DEFAULTS, CONFIG_PATHS, ZAMES_HOME, CONFIG_SCHEMA, getByPath, validateConfigValue, writeConfigValue, resetConfigValue, } from './config.js';
13
13
  import { runConfigMenu } from './config-menu.js';
14
14
  import { translate, normalizeLocale, localeDisplayName, isLocale, } from './i18n.js';
@@ -995,7 +995,7 @@ async function main() {
995
995
  return att;
996
996
  };
997
997
  ed.onClipboard = async () => {
998
- const res = readClipboardImageDetailed();
998
+ const res = await readClipboardImageDetailed();
999
999
  if (!res.data || !res.data.length) {
1000
1000
  // On WSL the user may have copied a FILE in Windows (not an image):
1001
1001
  // the Windows clipboard holds its path - attach it directly.
@@ -1021,7 +1021,9 @@ async function main() {
1021
1021
  clipboardWarned = true;
1022
1022
  ed.printAbove(theme.warn(t('msg.clip_empty', { via: res.via }) +
1023
1023
  String.fromCharCode(10) +
1024
- t('msg.clip_hint')));
1024
+ (process.platform === 'linux' && !hasClipboardTool()
1025
+ ? t('msg.clip_container')
1026
+ : t('msg.clip_hint'))));
1025
1027
  }
1026
1028
  return null;
1027
1029
  }
@@ -0,0 +1,71 @@
1
+ // Find a single file path inside arbitrary text (a pasted path surrounded by
2
+ // words, e.g. a quoted path followed by a question). Returns the cleaned path
3
+ // plus the text around it, or null. Implemented WITHOUT regular expressions so
4
+ // the source has no escaping pitfalls.
5
+ export function extractPathToken(text) {
6
+ const s = String(text || '');
7
+ const chars = Array.from(s);
8
+ const n = chars.length;
9
+ let i = 0;
10
+ while (i < n) {
11
+ const c = chars[i];
12
+ if (c === ' ' || c === ' ') {
13
+ i++;
14
+ continue;
15
+ }
16
+ let quote = '';
17
+ let start = i;
18
+ if (c === String.fromCharCode(39) || c === String.fromCharCode(34)) {
19
+ quote = c;
20
+ start = i + 1;
21
+ i = start;
22
+ while (i < n && chars[i] !== quote)
23
+ i++;
24
+ const tok = chars.slice(start, i).join('');
25
+ if (looksLikeToken(tok)) {
26
+ const before = s.slice(0, start - 1);
27
+ const after = i < n ? s.slice(i + 1) : '';
28
+ return { path: tok, before, after };
29
+ }
30
+ if (i < n)
31
+ i++;
32
+ continue;
33
+ }
34
+ i = start;
35
+ while (i < n && chars[i] !== ' ' && chars[i] !== ' ')
36
+ i++;
37
+ const tok = chars.slice(start, i).join('');
38
+ const cleaned = stripPunct(tok);
39
+ if (looksLikeToken(cleaned)) {
40
+ const before = s.slice(0, start);
41
+ const after = s.slice(start + tok.length);
42
+ return { path: cleaned, before, after };
43
+ }
44
+ }
45
+ return null;
46
+ }
47
+ function stripPunct(tok) {
48
+ let t = tok;
49
+ while (t.length && ',.!?;:)]'.includes(t[t.length - 1]))
50
+ t = t.slice(0, -1);
51
+ while (t.length && '(['.includes(t[0]))
52
+ t = t.slice(1);
53
+ return t;
54
+ }
55
+ function looksLikeToken(tok) {
56
+ const t = String(tok || '');
57
+ if (!t || t.length > 500)
58
+ return false;
59
+ const dot = t.lastIndexOf('.');
60
+ if (dot <= 0 || dot >= t.length - 1)
61
+ return false;
62
+ const ext = t.slice(dot + 1);
63
+ if (ext.length > 8)
64
+ return false;
65
+ for (const ch of ext) {
66
+ const ok = (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9');
67
+ if (!ok)
68
+ return false;
69
+ }
70
+ return true;
71
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.9.0",
3
+ "version": "2.9.2",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {