zames_pro 2.53.0 → 2.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/attachments.js +2 -1
- package/dist/config.js +10 -3
- package/dist/i18n.js +1 -1
- package/dist/self-review.js +2 -1
- package/dist/undo.js +4 -1
- package/dist/web.js +101 -2
- package/package.json +1 -2
package/dist/attachments.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
|
+
import { existsSync } from 'fs';
|
|
4
5
|
import { createHash } from 'crypto';
|
|
5
6
|
import { execFileSync } from 'child_process';
|
|
6
7
|
import { extractPathToken } from './path-token.js';
|
|
@@ -233,7 +234,7 @@ export function windowsReachable() {
|
|
|
233
234
|
if (process.platform !== 'linux')
|
|
234
235
|
return false;
|
|
235
236
|
try {
|
|
236
|
-
return
|
|
237
|
+
return existsSync('/mnt/c/Windows');
|
|
237
238
|
}
|
|
238
239
|
catch {
|
|
239
240
|
return false;
|
package/dist/config.js
CHANGED
|
@@ -10,6 +10,13 @@ const HOME_CONFIG = path.join(ZAMES_HOME, 'config.json');
|
|
|
10
10
|
export function projectConfigPath() {
|
|
11
11
|
return path.join(process.cwd(), '.zamesrc.json');
|
|
12
12
|
}
|
|
13
|
+
// Write via a temp file + rename so a crash or Ctrl+C mid-write can never
|
|
14
|
+
// leave a truncated/broken config (it is read at every startup).
|
|
15
|
+
function atomicWriteJson(file, value) {
|
|
16
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
17
|
+
fs.writeFileSync(tmp, JSON.stringify(value, null, 2) + '\n', 'utf-8');
|
|
18
|
+
fs.renameSync(tmp, file);
|
|
19
|
+
}
|
|
13
20
|
export const DEFAULTS = {
|
|
14
21
|
maxIterations: 0,
|
|
15
22
|
// Headless by default: the browser runs without a visible window. Set
|
|
@@ -426,7 +433,7 @@ export function readConfigFile(file) {
|
|
|
426
433
|
catch (e) {
|
|
427
434
|
const err = e;
|
|
428
435
|
if (err.code !== 'ENOENT') {
|
|
429
|
-
throw new Error(
|
|
436
|
+
throw new Error(`failed to read ${file}: ${err.message}`);
|
|
430
437
|
}
|
|
431
438
|
}
|
|
432
439
|
return {};
|
|
@@ -444,7 +451,7 @@ export function writeConfigValue(scope, key, raw) {
|
|
|
444
451
|
const data = readConfigFile(file);
|
|
445
452
|
setByPath(data, key, value);
|
|
446
453
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
447
|
-
|
|
454
|
+
atomicWriteJson(file, data);
|
|
448
455
|
return { value, file };
|
|
449
456
|
}
|
|
450
457
|
/** Reset a setting to the default (remove the key from the file). */
|
|
@@ -467,7 +474,7 @@ export function resetConfigValue(scope, key) {
|
|
|
467
474
|
if (cur)
|
|
468
475
|
delete cur[parts[parts.length - 1]];
|
|
469
476
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
470
|
-
|
|
477
|
+
atomicWriteJson(file, data);
|
|
471
478
|
return file;
|
|
472
479
|
}
|
|
473
480
|
// 'PROJECT_CONFIG' stays a getter so existing callers that only DISPLAY the
|
package/dist/i18n.js
CHANGED
|
@@ -26,7 +26,7 @@ export function normalizeLocale(v) {
|
|
|
26
26
|
return DEFAULT_LOCALE;
|
|
27
27
|
}
|
|
28
28
|
// String catalog. Key -> { ru, en }. If a key is missing — we return the key itself.
|
|
29
|
-
const CATALOG = {
|
|
29
|
+
export const CATALOG = {
|
|
30
30
|
'app.tagline': {
|
|
31
31
|
ru: 'агент поверх chat.deepseek.com через Playwright',
|
|
32
32
|
en: 'coding agent over chat.deepseek.com via Playwright',
|
package/dist/self-review.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import fs from 'fs/promises';
|
|
3
|
+
import { readdirSync } from 'fs';
|
|
3
4
|
import { theme, divider } from './theme.js';
|
|
4
5
|
import { translate, DEFAULT_LOCALE } from './i18n.js';
|
|
5
6
|
import { fileURLToPath } from 'url';
|
|
@@ -13,7 +14,7 @@ function resolveSrcDir() {
|
|
|
13
14
|
const candidates = [__dirname, path.join(__dirname, '..', 'src')];
|
|
14
15
|
for (const dir of candidates) {
|
|
15
16
|
try {
|
|
16
|
-
const entries =
|
|
17
|
+
const entries = readdirSync(dir);
|
|
17
18
|
if (entries.some((e) => e.endsWith('.ts')))
|
|
18
19
|
return dir;
|
|
19
20
|
}
|
package/dist/undo.js
CHANGED
|
@@ -22,7 +22,10 @@ export class UndoStore {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
async _writeIndex(history) {
|
|
25
|
-
|
|
25
|
+
// Atomic write: a crash mid-write must not corrupt the undo history.
|
|
26
|
+
const tmp = `${INDEX}.tmp-${process.pid}`;
|
|
27
|
+
await fs.writeFile(tmp, JSON.stringify(history, null, 2));
|
|
28
|
+
await fs.rename(tmp, INDEX);
|
|
26
29
|
}
|
|
27
30
|
async backup(filePath) {
|
|
28
31
|
if (!this.enabled)
|
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.54.0",
|
|
4
4
|
"description": "Terminal coding agent over chat.deepseek.com via Playwright",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -45,7 +45,6 @@
|
|
|
45
45
|
"chalk": "^5.3.0",
|
|
46
46
|
"cli-highlight": "^2.1.11",
|
|
47
47
|
"markdansi": "^0.3.4",
|
|
48
|
-
"marked": "^15.0.12",
|
|
49
48
|
"ora": "^8.0.1",
|
|
50
49
|
"playwright": "^1.48.0"
|
|
51
50
|
},
|