zames_pro 2.42.0 → 2.44.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.
@@ -773,10 +773,20 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
773
773
  export function truncateToolResult(text, limit) {
774
774
  if (text.length <= limit)
775
775
  return text;
776
- const omitted = text.length - limit;
777
- return (text.slice(0, limit) +
778
- String.fromCharCode(10) +
779
- `[...truncated ${omitted} chars]`);
776
+ // Prefer a LINE boundary near the limit so the model does not read a line
777
+ // cut in half (a half line is easy to misread as the real content). We look
778
+ // for the last newline within the last 20% of the window; if none, fall back
779
+ // to the hard char cut.
780
+ let cut = limit;
781
+ const NL = String.fromCharCode(10);
782
+ const searchFrom = Math.floor(limit * 0.8);
783
+ const nl = text.lastIndexOf(NL, limit);
784
+ // Cut AFTER the newline (include it) so the kept part is a whole number of
785
+ // lines — a cut that EXCLUDED the newline left a half line before the marker.
786
+ if (nl >= searchFrom)
787
+ cut = nl + 1;
788
+ const omitted = text.length - cut;
789
+ return text.slice(0, cut) + NL + `[...truncated ${omitted} chars]`;
780
790
  }
781
791
  // The answer looks like a tool call, but parseToolCall() did not recognize it.
782
792
  // Used as a safeguard against "the agent called a tool and stopped": in that
@@ -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 { createHash } from 'crypto';
4
5
  import { execFileSync } from 'child_process';
5
6
  import { extractPathToken } from './path-token.js';
6
7
  export { extractPathToken };
@@ -15,6 +16,11 @@ export const IMAGE_EXTS = [
15
16
  '.ico',
16
17
  ];
17
18
  export const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
19
+ // Stable content hash for de-duplicating identical attachments (a pasted image
20
+ // is saved to a NEW temp file every time, so its path is not a stable identity).
21
+ export function contentHash(data) {
22
+ return createHash('sha1').update(data).digest('hex');
23
+ }
18
24
  function sanitize(name) {
19
25
  return (String(name || '')
20
26
  .replace(/[\u0000-\u001f\u007f]/g, '')
@@ -139,10 +145,10 @@ export class AttachmentStore {
139
145
  this.fileCount = 0;
140
146
  }
141
147
  add(item) {
142
- // De-duplicate by real path: pasting the SAME file twice must reuse one
143
- // attachment (one marker, one upload), not create [file#1] and [file#2].
144
- // The operator saw exactly that when inserting the same file repeatedly.
145
- const existing = this.items.find((a) => a.path === item.path);
148
+ // De-duplicate by real path (files/paths) OR by content hash (images and
149
+ // clipboard data, which are re-saved to a fresh temp file on every paste).
150
+ // The operator saw the SAME file become [file#1] and [file#2].
151
+ const existing = this.items.find((a) => a.path === item.path || (item.hash && a.hash && a.hash === item.hash));
146
152
  if (existing)
147
153
  return existing;
148
154
  const image = isImageName(item.name) || String(item.mime).startsWith('image/');
@@ -156,6 +162,7 @@ export class AttachmentStore {
156
162
  image,
157
163
  marker,
158
164
  size: Number(item.size) || 0,
165
+ hash: item.hash,
159
166
  };
160
167
  this.items.push(att);
161
168
  return att;
package/dist/browser.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { chromium, } from 'playwright';
2
2
  import { extractAnswer, extractTokenUsage, dumpNetBody, isFinishedWithoutAnswer, } from './net-capture.js';
3
+ import { rewriteFailedAttachments } from './commands.js';
3
4
  import path from 'path';
4
5
  import os from 'os';
5
6
  import fs from 'fs/promises';
@@ -1859,7 +1860,12 @@ export class DeepSeekBrowser {
1859
1860
  // Attach files/images to the chat via the hidden <input type=file> of the
1860
1861
  // DeepSeek upload widget. The element is not visible, so we set the files
1861
1862
  // programmatically (Playwright setInputFiles works on hidden inputs too).
1863
+ // Returns the files that could NOT be attached (read error, no upload
1864
+ // widget, or a failed upload), so the caller can rewrite their path in the
1865
+ // message to [attach-failed: name]. The model must not believe a file was
1866
+ // attached when it was not.
1862
1867
  async _attachFiles(files) {
1868
+ const failed = [];
1863
1869
  const bufPayload = [];
1864
1870
  for (const f of files) {
1865
1871
  try {
@@ -1867,6 +1873,7 @@ export class DeepSeekBrowser {
1867
1873
  bufPayload.push({ name: f.name, mimeType: f.mime, buffer });
1868
1874
  }
1869
1875
  catch (e) {
1876
+ failed.push({ path: f.path, name: f.name });
1870
1877
  this._notice(theme.warn(this._t('ds.attach_read_failed', {
1871
1878
  name: f.name,
1872
1879
  error: e.message,
@@ -1874,7 +1881,7 @@ export class DeepSeekBrowser {
1874
1881
  }
1875
1882
  }
1876
1883
  if (!bufPayload.length)
1877
- return;
1884
+ return failed;
1878
1885
  // Preferred path: click the attach button and let the file chooser event
1879
1886
  // carry the files. This is how a real user attaches a file and it reliably
1880
1887
  // triggers DeepSeek's upload handler.
@@ -1900,7 +1907,7 @@ export class DeepSeekBrowser {
1900
1907
  await chooser.setFiles(bufPayload);
1901
1908
  // Wait for the upload preview instead of a fixed 2.5s sleep.
1902
1909
  await this._awaitUploadPreview(files.map((f) => f.name), 2500);
1903
- return;
1910
+ return failed;
1904
1911
  }
1905
1912
  catch { }
1906
1913
  }
@@ -1908,17 +1915,22 @@ export class DeepSeekBrowser {
1908
1915
  const input = this.page.locator('input[type="file"]').first();
1909
1916
  if ((await input.count()) === 0) {
1910
1917
  this._notice(theme.warn(this._t('ds.attach_no_input')));
1911
- return;
1918
+ for (const f of files)
1919
+ failed.push({ path: f.path, name: f.name });
1920
+ return failed;
1912
1921
  }
1913
1922
  try {
1914
1923
  await input.setInputFiles(bufPayload, { timeout: 15_000 });
1915
1924
  }
1916
1925
  catch (e) {
1917
1926
  this._notice(theme.warn(this._t('ds.attach_failed', { error: e.message })));
1918
- return;
1927
+ for (const f of files)
1928
+ failed.push({ path: f.path, name: f.name });
1929
+ return failed;
1919
1930
  }
1920
1931
  // Wait for the upload to finish (the attach preview to appear).
1921
1932
  await this._awaitUploadPreview(files.map((f) => f.name), 2000);
1933
+ return failed;
1922
1934
  }
1923
1935
  // Wait for the upload preview (the attached-file chip above the
1924
1936
  // input) to appear, instead of a fixed 2.5s/2s sleep. The signal is the
@@ -2103,10 +2115,15 @@ export class DeepSeekBrowser {
2103
2115
  await this._applyToggles();
2104
2116
  // Attach files/images FIRST (before the text): the DeepSeek upload widget
2105
2117
  // shows them above the input, and only then the message can be sent.
2118
+ // A file that FAILED to attach has its real path in the prompt text (see
2119
+ // substituteAttachmentMarkers); rewrite it to [attach-failed: name] so the
2120
+ // model does not believe the file is available.
2121
+ let finalPrompt = prompt;
2106
2122
  if (attachments.length) {
2107
- await this._attachFiles(attachments);
2123
+ const failed = await this._attachFiles(attachments);
2124
+ finalPrompt = rewriteFailedAttachments(finalPrompt, failed);
2108
2125
  }
2109
- await this._setInputText(input, prompt);
2126
+ await this._setInputText(input, finalPrompt);
2110
2127
  // Send immediately: the input is already verified by _setInputText, and a
2111
2128
  // clickable send button normally appears right away. The old code slept a
2112
2129
  // fixed 50ms here, which only added latency (the button does not need it).
package/dist/commands.js CHANGED
@@ -532,6 +532,19 @@ export function substituteAttachmentMarkers(text, attachments) {
532
532
  out = out.replace(/\[file#(\d+)\]/g, (whole, n) => filePath.get(Number(n)) ?? whole);
533
533
  return out;
534
534
  }
535
+ // Rewrite the paths of files that FAILED to attach into [attach-failed: name]
536
+ // in the task text, so the model does not believe an unavailable file was
537
+ // attached. The prompt carries real paths (substituteAttachmentMarkers), so a
538
+ // failed file's path is replaced in place. Pure, so it is unit-tested.
539
+ export function rewriteFailedAttachments(text, failed) {
540
+ let out = String(text ?? '');
541
+ for (const f of failed || []) {
542
+ if (!f || !f.path)
543
+ continue;
544
+ out = out.split(f.path).join('[attach-failed: ' + f.name + ']');
545
+ }
546
+ return out;
547
+ }
535
548
  // True when a `/config ...` line can be handled LIVE (while the agent is busy):
536
549
  // the text subcommands only touch config values, never the chat. The bare
537
550
  // `/config` and `/config menu` (which pause the editor and read keys) are NOT
package/dist/context.js CHANGED
@@ -1,6 +1,65 @@
1
1
  import fs from 'fs/promises';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
+ // Built-in skills, always available unless a project/global skill with the
5
+ // same name overrides them (real files win). They give the agent proven,
6
+ // reusable workflows in the spirit of Claude Code / Codex. English only (the
7
+ // body goes into the agent-facing task text).
8
+ const BUILTIN_SKILLS = [
9
+ {
10
+ name: 'commit',
11
+ description: 'Prepare a clean git commit from the current changes (stage, message).',
12
+ body: 'Prepare a commit from the current working-tree changes. Steps:\n' +
13
+ '1. Run `git status` and `git diff` (and `git diff --staged`) to see exactly what changed.\n' +
14
+ '2. Stage ONLY the files that belong to the intended change — never `git add -A` blindly.\n' +
15
+ '3. Write a short imperative subject line (<=72 chars) and an optional body explaining WHY.\n' +
16
+ '4. Commit with `git commit`. Do NOT push.\n' +
17
+ 'If the changes are unrelated or include junk, report that and ask before committing.',
18
+ },
19
+ {
20
+ name: 'review',
21
+ description: 'Review the current diff for bugs, risks and style issues (no code changes).',
22
+ body: 'Review the current uncommitted diff. Do NOT change any code — only report.\n' +
23
+ '1. Get the diff with `git diff` (and `git diff --staged`).\n' +
24
+ '2. Look for real problems: correctness bugs, unhandled edge cases, races, ' +
25
+ 'silent failures, security issues, broken error handling.\n' +
26
+ '3. For each finding give file:line, what is wrong and WHY it matters.\n' +
27
+ '4. End with a short verdict: safe to commit / needs fixes, and list the must-fix items first.',
28
+ },
29
+ {
30
+ name: 'test',
31
+ description: 'Run the project tests and fix failures until green.',
32
+ body: 'Run the project test suite and make it green. Steps:\n' +
33
+ '1. Find the test command (package.json scripts, Makefile, README).\n' +
34
+ '2. Run it. If it passes, report the result and stop.\n' +
35
+ '3. On failure, read the failing test and the code it exercises. Fix the ROOT cause ' +
36
+ '(do not weaken or delete the test to make it pass, unless the test itself is wrong).\n' +
37
+ '4. Re-run until it passes. Report what changed and the final result.',
38
+ },
39
+ {
40
+ name: 'debug',
41
+ description: 'Systematically diagnose and fix a bug.',
42
+ body: 'Diagnose a bug systematically. Steps:\n' +
43
+ '1. Reproduce it: find the smallest command/steps that show the failure.\n' +
44
+ '2. Form a hypothesis about the cause and find evidence in the code (read the actual code path).\n' +
45
+ '3. Narrow it down (logs, a focused test, a minimal repro) BEFORE editing.\n' +
46
+ '4. Fix the ROOT cause, not the symptom. Add a regression test that fails before and passes after.\n' +
47
+ '5. Verify the fix and report the cause, the change and the test.',
48
+ },
49
+ {
50
+ name: 'explain',
51
+ description: 'Explain how a file/module/feature works.',
52
+ body: 'Explain how the requested code works, for a developer new to it.\n' +
53
+ '1. Read the relevant files (start from the entry point and follow the calls).\n' +
54
+ '2. Give a concise overview: what it does, its inputs/outputs, the main flow.\n' +
55
+ '3. Call out non-obvious details: side effects, ordering, error handling, ' +
56
+ 'couplings, and any WHY-comments that matter.\n' +
57
+ '4. Do NOT change code. Answer in the operator language.',
58
+ },
59
+ ];
60
+ export function builtinSkills() {
61
+ return BUILTIN_SKILLS;
62
+ }
4
63
  const MAX_FILE_BASE = 60000;
5
64
  const MAX_TOTAL = 240000;
6
65
  const CRLF_RE = /\r\n/g;
@@ -190,6 +249,24 @@ export async function loadSkills(workdir) {
190
249
  }
191
250
  }
192
251
  found.sort((a, b) => a.name.localeCompare(b.name));
252
+ // Built-in skills come LAST (lowest priority): any project/global skill
253
+ // with the same name already claimed `seen` and wins. Only the not-overridden
254
+ // built-ins are appended, with an empty path and the body inline.
255
+ for (const b of BUILTIN_SKILLS) {
256
+ const key = b.name.toLowerCase();
257
+ if (seen.has(key))
258
+ continue;
259
+ seen.add(key);
260
+ found.push({
261
+ name: b.name,
262
+ description: b.description,
263
+ path: '',
264
+ dir: '',
265
+ source: 'global',
266
+ userInvokable: true,
267
+ builtinBody: b.body,
268
+ });
269
+ }
193
270
  return found;
194
271
  }
195
272
  const PROJECT_COMMAND_DIRS = [
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import { createTools } from './tools.js';
9
9
  import { runAgentLoop } from './agent-loop.js';
10
10
  import { createSpinner } from './spinner.js';
11
11
  import { LineEditor, expandPastes, pasteReplacement, formatCompactTokens, } from './input.js';
12
- import { parseImagePaste, extForMime, saveToTemp, guessMime, isImageName, formatSize, looksLikeFilePath, readClipboardImageDetailed, sniffMime, readWindowsClipboardFiles, hasClipboardTool, } from './attachments.js';
12
+ import { parseImagePaste, extForMime, saveToTemp, guessMime, isImageName, formatSize, looksLikeFilePath, readClipboardImageDetailed, sniffMime, readWindowsClipboardFiles, hasClipboardTool, contentHash, } from './attachments.js';
13
13
  import { loadConfig, DEFAULTS, CONFIG_PATHS, ZAMES_HOME, CONFIG_SCHEMA, getByPath, validateConfigValue, writeConfigValue, resetConfigValue, } from './config.js';
14
14
  import { runConfigMenu } from './config-menu.js';
15
15
  import { translate, normalizeLocale, localeDisplayName, isLocale, } from './i18n.js';
@@ -454,14 +454,20 @@ async function expandSlashTarget(workdir, name, rest) {
454
454
  const skill = skills.find((s) => s.name.toLowerCase() === name.toLowerCase() && s.userInvokable);
455
455
  if (skill) {
456
456
  let body = '';
457
- try {
458
- const raw = await fs.readFile(skill.path, 'utf-8');
459
- body = skillBody(raw);
457
+ if (skill.builtinBody !== undefined) {
458
+ body = skill.builtinBody;
460
459
  }
461
- catch {
462
- body = '';
460
+ else {
461
+ try {
462
+ const raw = await fs.readFile(skill.path, 'utf-8');
463
+ body = skillBody(raw);
464
+ }
465
+ catch {
466
+ body = '';
467
+ }
463
468
  }
464
- const header = 'Follow the skill "' + skill.name + '" (from ' + skill.path + ').';
469
+ const from = skill.path ? ' (from ' + skill.path + ')' : ' (built-in)';
470
+ const header = 'Follow the skill "' + skill.name + '"' + from + '.';
465
471
  const extra = rest
466
472
  ? '\n\nAdditional instructions from the operator: ' + rest
467
473
  : '';
@@ -1500,6 +1506,7 @@ async function main() {
1500
1506
  name,
1501
1507
  mime: image.mime,
1502
1508
  size: image.data.length,
1509
+ hash: contentHash(image.data),
1503
1510
  });
1504
1511
  ed.printAbove(theme.system(t('msg.attached_image', {
1505
1512
  marker: att.marker,
@@ -1573,6 +1580,7 @@ async function main() {
1573
1580
  name,
1574
1581
  mime,
1575
1582
  size: res.data.length,
1583
+ hash: contentHash(res.data),
1576
1584
  });
1577
1585
  ed.printAbove(theme.system(t('msg.attached_image', {
1578
1586
  marker: att.marker,
@@ -2537,11 +2545,13 @@ async function main() {
2537
2545
  else {
2538
2546
  console.log(theme.system(t('skills.title', { n: String(skills.length) })));
2539
2547
  for (const s of skills) {
2548
+ const src = s.path ? s.source : 'built-in';
2540
2549
  console.log(' ' +
2541
2550
  theme.user('/' + s.name) +
2542
- theme.dim(' [' + s.source + '] ') +
2551
+ theme.dim(' [' + src + '] ') +
2543
2552
  (s.description || theme.dim(t('common.none'))));
2544
- console.log(theme.dim(' ' + s.path));
2553
+ if (s.path)
2554
+ console.log(theme.dim(' ' + s.path));
2545
2555
  }
2546
2556
  }
2547
2557
  continue;
@@ -37,17 +37,18 @@ export function renderContextSection(context) {
37
37
  const tools = s.allowedTools && s.allowedTools.length
38
38
  ? ' [tools: ' + s.allowedTools.join(', ') + ']'
39
39
  : '';
40
+ // A built-in skill (no file on disk) says so instead of an empty path.
41
+ const loc = s.path ? '\n file: ' + s.path : '\n (built-in)';
40
42
  return ('- ' +
41
43
  s.name +
42
44
  tools +
43
45
  ': ' +
44
46
  (s.description || '(no description)') +
45
- '\n file: ' +
46
- s.path);
47
+ loc);
47
48
  });
48
49
  out +=
49
50
  '\n## Skills\n\n' +
50
- 'Specialized workflows available as SKILL.md files. When a task matches a skill description, read its file and follow its instructions. Skills are progressive disclosure: only read the body when the skill applies.\n\n' +
51
+ 'Specialized workflows. A skill with a `file:` is a SKILL.md on disk (read it when it applies); a `(built-in)` skill has no file — the operator can invoke it with /<name> and its instructions arrive as the task. Skills are progressive disclosure: only read the body when the skill applies.\n\n' +
51
52
  lines.join('\n') +
52
53
  '\n';
53
54
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.42.0",
3
+ "version": "2.44.0",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {