zames_pro 2.8.0 → 2.9.1
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 +29 -0
- package/dist/attachments.js +88 -3
- package/dist/index.js +22 -2
- package/package.json +1 -1
package/dist/agent-loop.js
CHANGED
|
@@ -80,6 +80,14 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
80
80
|
// chatty model.
|
|
81
81
|
let looksDoneRetries = 0;
|
|
82
82
|
const MAX_LOOKSDONE_RETRIES = 3;
|
|
83
|
+
// Watchdog against the agent emitting a tool call and then going silent.
|
|
84
|
+
// After a tool result the expected next answer is a fresh tool call; if we
|
|
85
|
+
// instead get an EMPTY answer or the EXACT same answer as the previous turn
|
|
86
|
+
// (a sign the new message was not sent), we nudge instead of stopping.
|
|
87
|
+
let lastRaw = '';
|
|
88
|
+
let justRanTool = false;
|
|
89
|
+
let watchdogRetries = 0;
|
|
90
|
+
const MAX_WATCHDOG_RETRIES = 3;
|
|
83
91
|
for (let i = 0; i < maxIterations; i++) {
|
|
84
92
|
onThinking();
|
|
85
93
|
// The first message (task) is user input: no throttle.
|
|
@@ -97,6 +105,23 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
97
105
|
transcript?.log('user_aborted');
|
|
98
106
|
return rawResponse;
|
|
99
107
|
}
|
|
108
|
+
// Watchdog: after a tool result we expect a FRESH tool call. If the answer
|
|
109
|
+
// is empty or identical to the previous turn (the new message was not
|
|
110
|
+
// sent), nudge instead of stopping.
|
|
111
|
+
const wdEmpty = !String(rawResponse || '').trim();
|
|
112
|
+
const wdStale = justRanTool && lastRaw.trim() !== '' && rawResponse.trim() === lastRaw.trim();
|
|
113
|
+
if (!isFirst && (wdEmpty || wdStale) && watchdogRetries < MAX_WATCHDOG_RETRIES) {
|
|
114
|
+
watchdogRetries++;
|
|
115
|
+
transcript?.log('watchdog_nudge', {
|
|
116
|
+
attempt: watchdogRetries,
|
|
117
|
+
empty: wdEmpty,
|
|
118
|
+
stale: wdStale,
|
|
119
|
+
response: String(rawResponse || '').slice(0, 200),
|
|
120
|
+
});
|
|
121
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
lastRaw = rawResponse;
|
|
100
125
|
const parsed = parseToolCall(rawResponse);
|
|
101
126
|
if (parsed) {
|
|
102
127
|
const thought = extractPreToolText(rawResponse);
|
|
@@ -259,6 +284,10 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
259
284
|
});
|
|
260
285
|
results.push({ tool: call.tool, result });
|
|
261
286
|
}
|
|
287
|
+
// A tool just ran — the next answer is expected to be a fresh tool call.
|
|
288
|
+
// Reset the watchdog so the next empty/repeated answer is nudged.
|
|
289
|
+
justRanTool = true;
|
|
290
|
+
watchdogRetries = 0;
|
|
262
291
|
if (results.length === 1) {
|
|
263
292
|
const r = results[0];
|
|
264
293
|
const resultStr = typeof r.result === 'string' ? r.result : JSON.stringify(r.result);
|
package/dist/attachments.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
3
4
|
import { execFileSync } from 'child_process';
|
|
4
5
|
export const IMAGE_EXTS = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg', '.ico'];
|
|
5
6
|
export const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
|
|
@@ -185,11 +186,42 @@ export async function saveToTemp(tmpDir, name, data) {
|
|
|
185
186
|
// the current platform. Returns the raw bytes plus a short note about which
|
|
186
187
|
// tool was used (or the reason nothing was found) — the caller shows that note
|
|
187
188
|
// to the user so a failed paste is never silent.
|
|
188
|
-
|
|
189
|
+
// True when running inside WSL. There the X11 clipboard is a SEPARATE
|
|
190
|
+
// clipboard from the Windows one: the user copies an image in Windows, so we
|
|
191
|
+
// must read it through powershell.exe, not xclip.
|
|
192
|
+
export function isWsl() {
|
|
193
|
+
if (process.platform !== 'linux')
|
|
194
|
+
return false;
|
|
195
|
+
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;
|
|
201
|
+
}
|
|
202
|
+
catch { }
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
// PowerShell image grabber, embedded as a UTF-16LE base64 script and run
|
|
206
|
+
// via powershell.exe -EncodedCommand: it saves the clipboard image to a
|
|
207
|
+
// Windows temp .png and prints the Windows path; we read it from WSL.
|
|
208
|
+
const PS_IMAGE_ENCODED = 'QQBkAGQALQBUAHkAcABlACAALQBBAHMAcwBlAG0AYgBsAHkATgBhAG0AZQAgAFMAeQBzAHQAZQBtAC4AVwBpAG4AZABvAHcAcwAuAEYAbwByAG0AcwAsAFMAeQBzAHQAZQBtAC4ARAByAGEAdwBpAG4AZwAKACQAaQBtAGcAPQBbAFMAeQBzAHQAZQBtAC4AVwBpAG4AZABvAHcAcwAuAEYAbwByAG0AcwAuAEMAbABpAHAAYgBvAGEAcgBkAF0AOgA6AEcAZQB0AEkAbQBhAGcAZQAoACkACgBpAGYAKAAkAGkAbQBnACkAewAkAHAAPQBKAG8AaQBuAC0AUABhAHQAaAAgACQAZQBuAHYAOgBUAEUATQBQACAAKAAiAHoAYQBtAGUAcwAtAGMAbABpAHAALQAiACsAWwBHAHUAaQBkAF0AOgA6AE4AZQB3AEcAdQBpAGQAKAApAC4AVABvAFMAdAByAGkAbgBnACgAKQArACIALgBwAG4AZwAiACkAOwAkAGkAbQBnAC4AUwBhAHYAZQAoACQAcAAsAFsAUwB5AHMAdABlAG0ALgBEAHIAYQB3AGkAbgBnAC4ASQBtAGEAZwBpAG4AZwAuAEkAbQBhAGcAZQBGAG8AcgBtAGEAdABdADoAOgBQAG4AZwApADsAVwByAGkAdABlAC0ATwB1AHQAcAB1AHQAIAAkAHAAfQAKAA==';
|
|
209
|
+
// PowerShell one-liner: write the clipboard file paths (CF_HDROP) to stdout.
|
|
210
|
+
const PS_GET_FILE_DROP = 'Add-Type -AssemblyName System.Windows.Forms; ' +
|
|
211
|
+
'$d=[System.Windows.Forms.Clipboard]::GetDataObject(); ' +
|
|
212
|
+
'if($d -and $d.GetDataPresent([System.Windows.Forms.DataFormats]::FileDrop)){' +
|
|
213
|
+
'$f=$d.GetData([System.Windows.Forms.DataFormats]::FileDrop); $f -join [Environment]::NewLine}';
|
|
214
|
+
export async function readClipboardImageDetailed() {
|
|
189
215
|
if (process.platform === 'linux' || process.platform === 'freebsd' || process.platform === 'openbsd') {
|
|
190
216
|
const wayland = process.env.WAYLAND_DISPLAY;
|
|
191
217
|
const x11 = process.env.DISPLAY;
|
|
192
218
|
const attempts = [];
|
|
219
|
+
// On WSL the Windows clipboard is what the user copies into — read it first.
|
|
220
|
+
if (isWsl()) {
|
|
221
|
+
const win = await readWindowsClipboardImage();
|
|
222
|
+
if (win)
|
|
223
|
+
return { data: win, via: 'powershell.exe' };
|
|
224
|
+
}
|
|
193
225
|
if (wayland || !x11)
|
|
194
226
|
attempts.push({ bin: 'wl-paste', args: ['--type', 'image/png', '--no-newline'], via: 'wl-paste' });
|
|
195
227
|
if (x11 || !wayland) {
|
|
@@ -229,8 +261,8 @@ export function readClipboardImageDetailed() {
|
|
|
229
261
|
return { data: null, via: 'unsupported-platform' };
|
|
230
262
|
}
|
|
231
263
|
// Backwards-compatible wrapper used elsewhere.
|
|
232
|
-
export function readClipboardImage() {
|
|
233
|
-
return readClipboardImageDetailed().data;
|
|
264
|
+
export async function readClipboardImage() {
|
|
265
|
+
return (await readClipboardImageDetailed()).data;
|
|
234
266
|
}
|
|
235
267
|
function tryCommand(bin, args) {
|
|
236
268
|
try {
|
|
@@ -265,3 +297,56 @@ export async function findFileByName(workdir, name) {
|
|
|
265
297
|
catch { }
|
|
266
298
|
return null;
|
|
267
299
|
}
|
|
300
|
+
// Read file paths from the Windows clipboard (copied files, CF_HDROP) and
|
|
301
|
+
// translate them to WSL paths. On WSL the user often copies a FILE, not an
|
|
302
|
+
// image. Returns [] on non-WSL / when nothing is there.
|
|
303
|
+
export function readWindowsClipboardFiles() {
|
|
304
|
+
if (!isWsl())
|
|
305
|
+
return [];
|
|
306
|
+
const out = tryCommand('powershell.exe', ['-STA', '-NoProfile', '-NonInteractive', '-Command', PS_GET_FILE_DROP]);
|
|
307
|
+
if (!out)
|
|
308
|
+
return [];
|
|
309
|
+
const text = out.toString('utf-8');
|
|
310
|
+
return text
|
|
311
|
+
.split(/\r?\n/)
|
|
312
|
+
.map((s) => s.trim())
|
|
313
|
+
.filter(Boolean)
|
|
314
|
+
.map(winPathToWsl)
|
|
315
|
+
.filter((p) => !!p);
|
|
316
|
+
}
|
|
317
|
+
// Convert a Windows path (C:\\Users\\me\\a.png) into a WSL path
|
|
318
|
+
// (/mnt/c/Users/me/a.png). Leaves already-POSIX paths untouched.
|
|
319
|
+
export function winPathToWsl(winPath) {
|
|
320
|
+
let p = String(winPath || '').trim();
|
|
321
|
+
if (p.startsWith('"') && p.endsWith('"'))
|
|
322
|
+
p = p.slice(1, -1);
|
|
323
|
+
if (!p)
|
|
324
|
+
return null;
|
|
325
|
+
if (p.startsWith('/'))
|
|
326
|
+
return p;
|
|
327
|
+
if (p.charAt(1) !== ':' || (p.charAt(2) !== '/' && p.charAt(2) !== '\\'))
|
|
328
|
+
return null;
|
|
329
|
+
const drive = p.charAt(0).toLowerCase();
|
|
330
|
+
const rest = p.slice(3).split('\\').join('/');
|
|
331
|
+
return '/mnt/' + drive + '/' + rest;
|
|
332
|
+
}
|
|
333
|
+
// Run the embedded PowerShell script and return the clipboard image bytes.
|
|
334
|
+
export async function readWindowsClipboardImage() {
|
|
335
|
+
if (!isWsl())
|
|
336
|
+
return null;
|
|
337
|
+
const out = tryCommand('powershell.exe', ['-STA', '-NoProfile', '-NonInteractive', '-EncodedCommand', PS_IMAGE_ENCODED]);
|
|
338
|
+
if (!out)
|
|
339
|
+
return null;
|
|
340
|
+
const first = out.toString('utf-8').trim().split(String.fromCharCode(10))[0].trim();
|
|
341
|
+
if (!first)
|
|
342
|
+
return null;
|
|
343
|
+
const wslPath = winPathToWsl(first);
|
|
344
|
+
if (!wslPath)
|
|
345
|
+
return null;
|
|
346
|
+
try {
|
|
347
|
+
return await fs.readFile(wslPath);
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
}
|
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, } from './attachments.js';
|
|
11
|
+
import { parseImagePaste, extForMime, saveToTemp, guessMime, isImageName, formatSize, looksLikeFilePath, readClipboardImageDetailed, sniffMime, readWindowsClipboardFiles, } 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,8 +995,28 @@ 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
|
+
// On WSL the user may have copied a FILE in Windows (not an image):
|
|
1001
|
+
// the Windows clipboard holds its path - attach it directly.
|
|
1002
|
+
for (const fp of readWindowsClipboardFiles()) {
|
|
1003
|
+
const data = await fs.readFile(fp).catch(() => null);
|
|
1004
|
+
if (!data)
|
|
1005
|
+
continue;
|
|
1006
|
+
const nm = path.basename(fp);
|
|
1007
|
+
const att = ed.attachments.add({
|
|
1008
|
+
path: fp,
|
|
1009
|
+
name: nm,
|
|
1010
|
+
mime: guessMime(nm),
|
|
1011
|
+
size: data.length,
|
|
1012
|
+
});
|
|
1013
|
+
ed.printAbove(theme.system(t('msg.attached_file', {
|
|
1014
|
+
marker: att.marker,
|
|
1015
|
+
name: nm,
|
|
1016
|
+
size: formatSize(data.length),
|
|
1017
|
+
}) + theme.dim(' (windows-clipboard)')));
|
|
1018
|
+
return att;
|
|
1019
|
+
}
|
|
1000
1020
|
if (!clipboardWarned) {
|
|
1001
1021
|
clipboardWarned = true;
|
|
1002
1022
|
ed.printAbove(theme.warn(t('msg.clip_empty', { via: res.via }) +
|