zames_pro 2.55.0 → 2.58.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/tools.js CHANGED
@@ -8,7 +8,7 @@ import { createExtraTools } from './extraTools.js';
8
8
  // user-facing. They must be in ENGLISH (like Claude Code / Codex). Only the
9
9
  // strings the OPERATOR sees (help, spinner, service messages) are localized —
10
10
  // those live in src/i18n.ts (CATALOG) and are picked by the locale setting.
11
- export function createTools(workdir, { undo, todos } = {}) {
11
+ export function createTools(workdir, { undo, todos, readOnly = false, } = {}) {
12
12
  const root = path.resolve(workdir);
13
13
  const safe = (p) => {
14
14
  const resolved = path.resolve(root, p);
@@ -164,7 +164,8 @@ export function createTools(workdir, { undo, todos } = {}) {
164
164
  {
165
165
  name: 'Grep',
166
166
  description: 'Search file contents (regular expression). include — file name ' +
167
- 'filter (e.g. "*.ts"). output: content (default, matching lines with ' +
167
+ 'filter, a single glob or a comma-separated list (e.g. "*.ts" or ' +
168
+ '"*.ts,*.tsx"). output: content (default, matching lines with ' +
168
169
  'numbers), files_only (paths only), count (match count per file).',
169
170
  parameters: {
170
171
  pattern: 'string',
@@ -175,7 +176,11 @@ export function createTools(workdir, { undo, todos } = {}) {
175
176
  fn: async ({ pattern, path: searchPath, include, output }) => {
176
177
  const pat = req(pattern, 'pattern');
177
178
  const target = searchPath ? safe(String(searchPath)) : workdir;
178
- const inc = include ? String(include).trim() : '';
179
+ // include may be a single glob or a comma-separated list.
180
+ const incs = (include ? String(include) : '')
181
+ .split(',')
182
+ .map((s) => s.trim())
183
+ .filter(Boolean);
179
184
  const mode = String(output || 'content').toLowerCase();
180
185
  if (process.platform === 'win32') {
181
186
  const escaped = pat.replace(/"/g, '\\"');
@@ -183,7 +188,9 @@ export function createTools(workdir, { undo, todos } = {}) {
183
188
  return runShell(workdir, `findstr /s /n /r /c:"${escaped}" ` + scope);
184
189
  }
185
190
  const flags = mode === 'files_only' ? '-rlE' : mode === 'count' ? '-rcE' : '-rnE';
186
- const includeArg = inc ? ' --include=' + JSON.stringify(inc) : '';
191
+ const includeArg = incs
192
+ .map((g) => ' --include=' + JSON.stringify(g))
193
+ .join('');
187
194
  return runShell(workdir, `grep ${flags}${includeArg} ${JSON.stringify(pat)} ` +
188
195
  `${JSON.stringify(target)} || true`);
189
196
  },
@@ -198,5 +205,31 @@ export function createTools(workdir, { undo, todos } = {}) {
198
205
  parameters: { message: 'string' },
199
206
  fn: async ({ message }) => message,
200
207
  };
201
- return [...baseTools, ...extraTools, ...gitTools, ...webTools, respondTool];
208
+ const all = [
209
+ ...baseTools,
210
+ ...extraTools,
211
+ ...gitTools,
212
+ ...webTools,
213
+ respondTool,
214
+ ];
215
+ return readOnly ? filterToolsForReadOnly(all) : all;
216
+ }
217
+ // Tools that mutate the working tree or run arbitrary commands. In "plan"
218
+ // (read-only) mode they are REMOVED from the tool set entirely, so the model
219
+ // cannot write by accident while it is still investigating. Removing (rather
220
+ // than rejecting at call time) is the honest signal: the agent never sees a
221
+ // tool it is not allowed to use, so it does not waste a turn trying.
222
+ export const MUTATING_TOOLS = new Set([
223
+ 'Write',
224
+ 'Edit',
225
+ 'MultiEdit',
226
+ 'ApplyPatch',
227
+ 'Bash',
228
+ 'GitAdd',
229
+ 'GitCommit',
230
+ 'GitPush',
231
+ ]);
232
+ /** Drop mutating tools for read-only / plan mode. Pure; unit-tested. */
233
+ export function filterToolsForReadOnly(tools) {
234
+ return tools.filter((t) => !MUTATING_TOOLS.has(t.name));
202
235
  }
@@ -5,11 +5,13 @@ export class Transcript {
5
5
  file;
6
6
  stream;
7
7
  startedAt;
8
- constructor({ dir, enabled = true, sessionName = 'session', }) {
8
+ onLine;
9
+ constructor({ dir, enabled = true, sessionName = 'session', onLine = null, }) {
9
10
  this.enabled = enabled;
10
11
  this.file = null;
11
12
  this.stream = null;
12
13
  this.startedAt = Date.now();
14
+ this.onLine = onLine;
13
15
  if (!enabled)
14
16
  return;
15
17
  try {
@@ -30,16 +32,24 @@ export class Transcript {
30
32
  }
31
33
  }
32
34
  log(type, data = {}) {
33
- if (!this.enabled || !this.stream)
34
- return;
35
35
  const entry = {
36
36
  ts: new Date().toISOString(),
37
37
  elapsed: Date.now() - this.startedAt,
38
38
  type,
39
39
  ...data,
40
40
  };
41
+ const line = JSON.stringify(entry) + String.fromCharCode(10);
42
+ // The mirror runs even when the file is disabled (jsonl mode).
43
+ if (this.onLine) {
44
+ try {
45
+ this.onLine(line);
46
+ }
47
+ catch { }
48
+ }
49
+ if (!this.enabled || !this.stream)
50
+ return;
41
51
  try {
42
- this.stream.write(JSON.stringify(entry) + String.fromCharCode(10));
52
+ this.stream.write(line);
43
53
  }
44
54
  catch { }
45
55
  }
package/dist/undo.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 { writeJsonAtomic } from './fsutil.js';
4
5
  const UNDO_DIR = path.join(os.homedir(), '.zames', 'undo');
5
6
  const INDEX = path.join(UNDO_DIR, 'index.json');
6
7
  export class UndoStore {
@@ -23,9 +24,7 @@ export class UndoStore {
23
24
  }
24
25
  async _writeIndex(history) {
25
26
  // 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);
27
+ writeJsonAtomic(INDEX, history);
29
28
  }
30
29
  async backup(filePath) {
31
30
  if (!this.enabled)
@@ -63,15 +62,18 @@ export class UndoStore {
63
62
  return { originalPath: filePath, existed, stamp };
64
63
  }
65
64
  async undoLast() {
65
+ // Machine codes, NOT prose: the caller (index.ts) localizes them through
66
+ // the i18n catalog. Hardcoding a Russian reason here leaked Russian into
67
+ // an English UI (`/undo` with an empty history).
66
68
  if (!this.enabled)
67
- return { ok: false, reason: 'undo отключён в конфиге' };
69
+ return { ok: false, reason: 'disabled' };
68
70
  await this._ensure();
69
71
  const history = await this._readIndex();
70
72
  if (!history.length)
71
- return { ok: false, reason: 'история пуста' };
73
+ return { ok: false, reason: 'empty' };
72
74
  const record = history.pop();
73
75
  if (!record)
74
- return { ok: false, reason: 'история пуста' };
76
+ return { ok: false, reason: 'empty' };
75
77
  const { originalPath, existed, backupFile } = record;
76
78
  try {
77
79
  if (existed && backupFile) {
package/dist/web.js CHANGED
@@ -118,6 +118,30 @@ async function assertPublicUrl(rawUrl) {
118
118
  }
119
119
  }
120
120
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
121
+ // True for content types whose bodies are not meant to be shown as text.
122
+ // WebFetch dumps the body into the context, so a PDF or an image would flood
123
+ // the model with garbage; the caller returns a short human note instead.
124
+ export function isBinaryContentType(contentType) {
125
+ const ct = String(contentType || '')
126
+ .toLowerCase()
127
+ .split(';')[0]
128
+ .trim();
129
+ if (!ct)
130
+ return false;
131
+ if (ct.startsWith('text/'))
132
+ return false;
133
+ // Textual application/* types must stay readable.
134
+ if (/^(application\/(json|xml|xhtml\+xml|javascript|ecmascript|x-httpd-php|rtf|sql|graphql|x-sh|x-yaml|yaml|toml|ld\+json|x-ndjson))$/.test(ct)) {
135
+ return false;
136
+ }
137
+ if (/\+(json|xml)$/.test(ct))
138
+ return false;
139
+ if (/^(image|audio|video|font|model)\//.test(ct) ||
140
+ /^application\/(pdf|zip|gzip|x-gzip|x-tar|octet-stream|msword|vnd\.|x-7z|x-rar|x-bzip|x-protobuf|wasm)/.test(ct)) {
141
+ return true;
142
+ }
143
+ return false;
144
+ }
121
145
  // fetch with retries for transient network errors and 5xx responses.
122
146
  async function httpFetchWithRetry(url, opts = {}, retries = 2) {
123
147
  let lastErr;
@@ -231,6 +255,13 @@ export function createWebTools() {
231
255
  return formatResult(r.status, r.url, text, limit);
232
256
  }
233
257
  const r = await httpFetchWithRetry(String(url));
258
+ // Binary content (PDF, images, archives, ...) must NOT be dumped
259
+ // into the context as raw bytes — tell the model what it is instead.
260
+ if (isBinaryContentType(r.contentType)) {
261
+ return (`HTTP ${r.status} ${r.url}\n` +
262
+ `Content-Type: ${r.contentType}\n\n` +
263
+ `(binary content is not returned as text; content-type: ${r.contentType})`);
264
+ }
234
265
  // If it's JSON/plain text — return as is
235
266
  if (/application\/json|text\/plain|text\/markdown/i.test(r.contentType)) {
236
267
  const trimmed = r.body.slice(0, limit);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.55.0",
4
- "description": "Terminal coding agent over chat.deepseek.com via Playwright",
3
+ "version": "2.58.0",
4
+ "description": "Terminal coding agent that drives chat.deepseek.com through Playwright: reads and edits files, runs commands, commits to git.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "zames": "dist/index.js"
@@ -10,13 +10,16 @@
10
10
  "dist",
11
11
  "scripts",
12
12
  "README.md",
13
+ "CHANGELOG.md",
13
14
  "LICENSE",
14
15
  "logo.jpg"
15
16
  ],
16
17
  "scripts": {
17
18
  "start": "node ./dist/index.js",
18
19
  "dev": "ZAMES_NET_DEBUG=1 tsx ./src/index.ts --dev --headed",
19
- "build": "tsc -p tsconfig.build.json",
20
+ "clean": "node scripts/clean-dist.mjs",
21
+ "build": "node scripts/clean-dist.mjs && tsc -p tsconfig.build.json",
22
+ "coverage:gate": "node scripts/coverage-gate.mjs",
20
23
  "typecheck": "tsc --noEmit",
21
24
  "lint": "node scripts/eslint-ts6.mjs",
22
25
  "lint:fix": "node scripts/eslint-ts6.mjs --fix",
@@ -34,7 +37,13 @@
34
37
  "ai",
35
38
  "deepseek",
36
39
  "cli",
37
- "coding-agent"
40
+ "coding-agent",
41
+ "claude-code",
42
+ "codex",
43
+ "terminal",
44
+ "playwright",
45
+ "llm",
46
+ "automation"
38
47
  ],
39
48
  "license": "MIT",
40
49
  "engines": {
@@ -0,0 +1,17 @@
1
+ import { rmSync, existsSync } from 'node:fs'
2
+ import path from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+
5
+ // tsc emits into dist/ without clearing it, so a source file DELETED from
6
+ // src/ leaves an orphan .js in the published tree (this really happened:
7
+ // dist/confirm.js survived after src/confirm.ts was removed). Wipe dist/ first
8
+ // so the build output mirrors src/ exactly.
9
+ //
10
+ // Cross-platform: use node's fs.rm instead of a shell `rm -rf` (Windows has no
11
+ // rm) and never touch anything outside this repo.
12
+ const here = path.dirname(fileURLToPath(import.meta.url))
13
+ const dist = path.resolve(here, '..', 'dist')
14
+ if (existsSync(dist)) {
15
+ rmSync(dist, { recursive: true, force: true })
16
+ console.log('clean-dist: removed', dist)
17
+ }
@@ -0,0 +1,83 @@
1
+ import { spawnSync } from 'node:child_process'
2
+
3
+ // Soft coverage FLOOR for the PURE logic modules — the ones a regression would
4
+ // silently break without any UI symptom (command formatting, schedule parsing,
5
+ // the raw SSE/model-answer parsing). The floors are deliberately a few points
6
+ // BELOW the current values so ordinary churn does not fail the build; they
7
+ // only catch a real drop (a module losing its tests).
8
+ //
9
+ // Node's test runner has no built-in coverage threshold, so we parse the
10
+ // per-file line % from the coverage report it prints and compare here. This is
11
+ // informational in the sense that the FLOORS are conservative, but a drop
12
+ // below one is a hard failure.
13
+ const FLOORS = {
14
+ 'commands.ts': 85,
15
+ 'scheduler.ts': 90,
16
+ 'net-capture.ts': 90,
17
+ 'xml-toolcall.ts': 90,
18
+ 'diff.ts': 90,
19
+ 'theme.ts': 95,
20
+ // Added later: core logic or already well-tested, so a silent regression
21
+ // that deletes their tests should be caught too. Floors sit a few points
22
+ // below the current values.
23
+ 'extraTools.ts': 85,
24
+ 'fsutil.ts': 90,
25
+ 'sessions.ts': 90,
26
+ 'context.ts': 85,
27
+ 'markdown.ts': 90,
28
+ 'tools.ts': 90,
29
+ 'system-prompt.ts': 90,
30
+ 'undo.ts': 85,
31
+ }
32
+
33
+ const run = spawnSync(
34
+ 'npx',
35
+ ['tsx', '--test', '--experimental-test-coverage', 'test/*.test.ts'],
36
+ { encoding: 'utf-8', shell: process.platform === 'win32', maxBuffer: 1024 * 1024 * 64 },
37
+ )
38
+
39
+ const out = (run.stdout || '') + (run.stderr || '')
40
+
41
+ if (run.status !== 0) {
42
+ console.error('coverage-gate: test run failed (exit ' + run.status + ')')
43
+ process.exit(1)
44
+ }
45
+
46
+ const linePct = new Map()
47
+ for (const line of out.split('\n')) {
48
+ // Per-file rows look like:
49
+ // ℹ commands.ts | 94.14 | 78.70 | 93.88 | 40-56 ...
50
+ const m = line.match(/^\s*\S?\s*([A-Za-z0-9._-]+\.ts)\s+\|\s+([\d.]+)\s+\|/)
51
+ if (m) linePct.set(m[1], Number(m[2]))
52
+ }
53
+
54
+ const failures = []
55
+ for (const [file, floor] of Object.entries(FLOORS)) {
56
+ const pct = linePct.get(file)
57
+ if (pct === undefined) {
58
+ failures.push(file + ': no coverage row found')
59
+ continue
60
+ }
61
+ if (pct < floor) {
62
+ failures.push(file + ': ' + pct + '% < floor ' + floor + '%')
63
+ } else {
64
+ console.log('coverage-gate: ' + file + ' ' + pct + '% >= ' + floor + '%')
65
+ }
66
+ }
67
+
68
+ // Print the lowest-covered modules, so a dead or untested module is visible
69
+ // in the CI log even when it has no floor yet. Informational only.
70
+ const sorted = [...linePct.entries()].sort((a, b) => a[1] - b[1]).slice(0, 8)
71
+ if (sorted.length) {
72
+ console.log('coverage-gate: lowest covered modules (informational):')
73
+ for (const [name, pct] of sorted) {
74
+ console.log(' ' + name + ' ' + pct + '%')
75
+ }
76
+ }
77
+
78
+ if (failures.length) {
79
+ console.error('coverage-gate: FAILED')
80
+ for (const f of failures) console.error(' ' + f)
81
+ process.exit(1)
82
+ }
83
+ console.log('coverage-gate: OK')