zames_pro 2.67.4 → 2.68.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/CHANGELOG.md CHANGED
@@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.68.1] - 2026-10-08
11
+
12
+ ### Fixed
13
+
14
+ - **ui:** long service messages (send failures, rate limits) are now
15
+ word-wrapped to the same margin as the tool previews (`cols - 1`).
16
+ Before, they were printed at the full terminal width and the terminal
17
+ wrapped them mid-word, so some lines ran to the edge and some stopped
18
+ short. Applies to `LineEditor.warning()` and the non-TTY spinner.
19
+
20
+ ## [2.68.0] - 2026-10-08
21
+
22
+ ### Changed
23
+
24
+ - **git:** every git invocation (GitStatus/GitLog/GitBranchList/GitAdd/
25
+ GitCommit and the `/diff`, `/diffstat`, `/doctor` helpers) now goes
26
+ through `runGitArgs()` (`execFile` with an argument array, no shell). The
27
+ last shell-string variant (`runGit`) is gone, so no git argument can ever
28
+ be interpreted by a shell. `diffGitArgs()` now returns an array.
29
+ - **cleanup:** removed the dead `shellQuote` export (nothing used it after
30
+ the move to `execFile`).
31
+ - **build:** `prepublishOnly` now runs `build:readme` before `build`, so a
32
+ published README.md can never drift from `docs/readme.*.md`.
33
+
10
34
  ## [2.67.4] - 2026-10-08
11
35
 
12
36
  ### Fixed
package/dist/commands.js CHANGED
@@ -22,8 +22,10 @@ export function formatDiff(diffText, opts = {}, t = translate('en')) {
22
22
  const rest = lines.length - maxLines;
23
23
  return head + NL + t('diff.more_lines', { n: String(rest) });
24
24
  }
25
+ // Git arguments for the `/diff` command. An ARRAY (no shell): the caller runs
26
+ // them through runGitArgs(), so nothing is interpreted by a shell. Pure.
25
27
  export function diffGitArgs(staged = false) {
26
- return staged ? 'git diff --staged' : 'git diff';
28
+ return staged ? ['diff', '--staged'] : ['diff'];
27
29
  }
28
30
  // ---------- /diffstat ----------
29
31
  /** Render `git diff --stat` output (a compact change summary) for display. */
package/dist/gitTools.js CHANGED
@@ -1,44 +1,9 @@
1
- import { exec, execFile } from 'child_process';
1
+ import { execFile } from 'child_process';
2
2
  const BS = String.fromCharCode(92);
3
- export function runGit(cmd, cwd, timeout = 15_000) {
4
- return new Promise((resolve) => {
5
- exec(cmd, {
6
- cwd,
7
- timeout,
8
- windowsHide: true,
9
- maxBuffer: 1024 * 1024 * 8,
10
- env: {
11
- ...process.env,
12
- GIT_TERMINAL_PROMPT: '0',
13
- GIT_PAGER: 'cat',
14
- PAGER: 'cat',
15
- },
16
- }, (err, stdout, stderr) => {
17
- const out = (stdout || '').toString();
18
- const errStr = (stderr || '').toString();
19
- if (!err) {
20
- const combined = (out + errStr).trim();
21
- resolve(combined || '(command produced no output)');
22
- return;
23
- }
24
- const parts = [];
25
- if (err.killed)
26
- parts.push(`⏱ Timeout ${timeout}ms`);
27
- else if (err.code !== undefined && err.code !== null)
28
- parts.push(`Exit code: ${err.code}`);
29
- else
30
- parts.push(`Error: ${err.message}`);
31
- if (out.trim())
32
- parts.push(out.trim());
33
- if (errStr.trim())
34
- parts.push(errStr.trim());
35
- resolve(parts.join(String.fromCharCode(10)));
36
- });
37
- });
38
- }
39
- // Run git with an argument ARRAY (no shell). Use this whenever an argument
40
- // comes from the model: the shell never sees it, so $(...), backticks and a
41
- // leading `-` (option injection) cannot turn a read-only call into a command.
3
+ // Run git with an argument ARRAY (no shell). ALL git invocations go through
4
+ // this: an argument from the model can never be seen by a shell, so $(...),
5
+ // backticks and a leading `-` (option injection) cannot turn a read-only call
6
+ // into a command. There is deliberately NO shell-string variant left.
42
7
  export function runGitArgs(args, cwd, timeout = 15_000) {
43
8
  return new Promise((resolve) => {
44
9
  execFile('git', args, {
@@ -76,13 +41,13 @@ export function runGitArgs(args, cwd, timeout = 15_000) {
76
41
  });
77
42
  }
78
43
  export async function getGitContext(workdir) {
79
- const probe = await runGit('git rev-parse --is-inside-work-tree', workdir, 5000);
44
+ const probe = await runGitArgs(['rev-parse', '--is-inside-work-tree'], workdir, 5000);
80
45
  if (probe.trim() !== 'true')
81
46
  return null;
82
- const branchR = await runGit('git branch --show-current', workdir, 5000);
83
- const statusR = await runGit('git status --porcelain', workdir, 5000);
84
- const remoteR = await runGit('git remote', workdir, 5000);
85
- const aheadR = await runGit('git rev-list --left-right --count @{upstream}...HEAD', workdir, 5000);
47
+ const branchR = await runGitArgs(['branch', '--show-current'], workdir, 5000);
48
+ const statusR = await runGitArgs(['status', '--porcelain'], workdir, 5000);
49
+ const remoteR = await runGitArgs(['remote'], workdir, 5000);
50
+ const aheadR = await runGitArgs(['rev-list', '--left-right', '--count', '@{upstream}...HEAD'], workdir, 5000);
86
51
  const branch = branchR.trim() || '(detached HEAD)';
87
52
  const statusLines = statusR
88
53
  .split(String.fromCharCode(10))
@@ -136,7 +101,7 @@ export function formatGitContext(ctx) {
136
101
  }
137
102
  export function createGitTools(workdir) {
138
103
  const ensureRepo = async () => {
139
- const probe = await runGit('git rev-parse --is-inside-work-tree', workdir, 5000);
104
+ const probe = await runGitArgs(['rev-parse', '--is-inside-work-tree'], workdir, 5000);
140
105
  if (probe.trim() !== 'true') {
141
106
  return (`Not a git repository: ${workdir}\n` +
142
107
  `Hint: run \`git init\` here, or cd into an existing repo.\n` +
@@ -153,7 +118,7 @@ export function createGitTools(workdir) {
153
118
  const err = await ensureRepo();
154
119
  if (err)
155
120
  return err;
156
- return runGit('git status', workdir, 10_000);
121
+ return runGitArgs(['status'], workdir, 10_000);
157
122
  },
158
123
  },
159
124
  {
@@ -185,7 +150,7 @@ export function createGitTools(workdir) {
185
150
  const err = await ensureRepo();
186
151
  if (err)
187
152
  return err;
188
- return runGit(`git log --oneline --decorate -n ${Number(count) || 10}`, workdir, 10_000);
153
+ return runGitArgs(['log', '--oneline', '--decorate', '-n', String(Number(count) || 10)], workdir, 10_000);
189
154
  },
190
155
  },
191
156
  {
@@ -220,10 +185,13 @@ export function createGitTools(workdir) {
220
185
  const err = await ensureRepo();
221
186
  if (err)
222
187
  return err;
223
- // The format string contains parentheses/brackets, which the POSIX
224
- // shell would treat as syntax — quote it as one JSON arg.
225
- const format = JSON.stringify('%(HEAD) %(refname:short) %(committerdate:relative) %(subject)');
226
- return runGit('git branch --sort=-committerdate --format=' + format, workdir, 10_000);
188
+ // The format string contains parentheses/brackets; as ONE array
189
+ // element it is passed literally (no shell to choke on it).
190
+ return runGitArgs([
191
+ 'branch',
192
+ '--sort=-committerdate',
193
+ '--format=%(HEAD) %(refname:short) %(committerdate:relative) %(subject)',
194
+ ], workdir, 10_000);
227
195
  },
228
196
  },
229
197
  {
@@ -244,7 +212,7 @@ export function createGitTools(workdir) {
244
212
  }
245
213
  return runGitArgs(['add', '--', ...list], workdir, 20_000);
246
214
  }
247
- return runGit('git add -A', workdir, 20_000);
215
+ return runGitArgs(['add', '-A'], workdir, 20_000);
248
216
  },
249
217
  },
250
218
  {
@@ -260,13 +228,13 @@ export function createGitTools(workdir) {
260
228
  return 'Error: message is empty.';
261
229
  }
262
230
  // We stage everything only if the index is empty (as the description promises).
263
- const staged = await runGit('git diff --cached --name-only', workdir, 10_000);
231
+ const staged = await runGitArgs(['diff', '--cached', '--name-only'], workdir, 10_000);
264
232
  const stagedTrimmed = staged.trim();
265
233
  const hasStaged = stagedTrimmed !== '' &&
266
234
  stagedTrimmed !== '(command produced no output)';
267
235
  const addResult = hasStaged
268
236
  ? '(index is not empty — add -A skipped)'
269
- : await runGit('git add -A', workdir, 20_000);
237
+ : await runGitArgs(['add', '-A'], workdir, 20_000);
270
238
  const fs = await import('fs/promises');
271
239
  const path = await import('path');
272
240
  const os = await import('os');
@@ -291,13 +259,13 @@ export function createGitTools(workdir) {
291
259
  return err;
292
260
  const b = branch && String(branch).trim()
293
261
  ? String(branch).trim()
294
- : (await runGit('git branch --show-current', workdir, 5000)).trim();
262
+ : (await runGitArgs(['branch', '--show-current'], workdir, 5000)).trim();
295
263
  if (!b)
296
264
  return 'Error: could not determine the branch to push.';
297
- // The branch name reaches the shell command below. Validate it as a
298
- // real ref so a model-supplied value cannot smuggle shell metacharacters
299
- // (`;`, backticks, `$()`) into `git push`. The remote stays a literal
300
- // 'origin' and is not user-controlled.
265
+ // The branch name reaches git as an argument. Validate it as a real ref
266
+ // so a model-supplied value cannot smuggle shell metacharacters or a
267
+ // leading dash. The remote stays a literal 'origin' and is not
268
+ // user-controlled.
301
269
  if (!/^[A-Za-z0-9._/-]+$/.test(b) || b.startsWith('-')) {
302
270
  return `Error: invalid branch name: ${b}`;
303
271
  }
package/dist/index.js CHANGED
@@ -2854,24 +2854,24 @@ async function main() {
2854
2854
  }
2855
2855
  if (lower === '/diff' || lower.startsWith('/diff ')) {
2856
2856
  const staged = lower.indexOf('--staged') !== -1;
2857
- const { runGit } = await import('./gitTools.js');
2858
- const probe = await runGit('git rev-parse --is-inside-work-tree', currentWorkdir, 5000);
2857
+ const { runGitArgs } = await import('./gitTools.js');
2858
+ const probe = await runGitArgs(['rev-parse', '--is-inside-work-tree'], currentWorkdir, 5000);
2859
2859
  if (probe.trim() !== 'true') {
2860
2860
  console.error(theme.error(t('diff.not_repo')));
2861
2861
  continue;
2862
2862
  }
2863
- const out = await runGit(mod.commands.diffGitArgs(staged), currentWorkdir, 20_000);
2863
+ const out = await runGitArgs(mod.commands.diffGitArgs(staged), currentWorkdir, 20_000);
2864
2864
  console.log(theme.system(mod.commands.formatDiff(out, { maxLines: 400 }, t)));
2865
2865
  continue;
2866
2866
  }
2867
2867
  if (lower === '/diffstat') {
2868
- const { runGit } = await import('./gitTools.js');
2869
- const probe = await runGit('git rev-parse --is-inside-work-tree', currentWorkdir, 5000);
2868
+ const { runGitArgs } = await import('./gitTools.js');
2869
+ const probe = await runGitArgs(['rev-parse', '--is-inside-work-tree'], currentWorkdir, 5000);
2870
2870
  if (probe.trim() !== 'true') {
2871
2871
  console.error(theme.error(t('diff.not_repo')));
2872
2872
  continue;
2873
2873
  }
2874
- const out = await runGit('git diff --stat', currentWorkdir, 20_000);
2874
+ const out = await runGitArgs(['diff', '--stat'], currentWorkdir, 20_000);
2875
2875
  console.log(theme.system(mod.commands.formatDiffStat(out, { maxLines: 60 }, t)));
2876
2876
  continue;
2877
2877
  }
@@ -2917,14 +2917,14 @@ async function main() {
2917
2917
  continue;
2918
2918
  }
2919
2919
  if (lower === '/doctor') {
2920
- const { runGit } = await import('./gitTools.js');
2920
+ const { runGitArgs } = await import('./gitTools.js');
2921
2921
  let gitOk = false;
2922
2922
  let gitBranch = null;
2923
2923
  try {
2924
- const probe = await runGit('git rev-parse --is-inside-work-tree', currentWorkdir, 5000);
2924
+ const probe = await runGitArgs(['rev-parse', '--is-inside-work-tree'], currentWorkdir, 5000);
2925
2925
  gitOk = probe.trim() === 'true';
2926
2926
  if (gitOk) {
2927
- gitBranch = (await runGit('git branch --show-current', currentWorkdir, 5000)).trim();
2927
+ gitBranch = (await runGitArgs(['branch', '--show-current'], currentWorkdir, 5000)).trim();
2928
2928
  }
2929
2929
  }
2930
2930
  catch {
@@ -2955,7 +2955,7 @@ async function main() {
2955
2955
  authSaved: authMarkerExists(),
2956
2956
  contextLimit: config.ui.contextLimit,
2957
2957
  hasOrigin: gitOk
2958
- ? (await runGit('git remote', currentWorkdir, 5000)).includes('origin')
2958
+ ? (await runGitArgs(['remote'], currentWorkdir, 5000)).includes('origin')
2959
2959
  : undefined,
2960
2960
  minSendIntervalMs: config.browser.minSendIntervalMs,
2961
2961
  sshRemote: !!(process.env.SSH_CONNECTION || process.env.SSH_TTY),
@@ -154,6 +154,61 @@ export function charWidth(cp) {
154
154
  // appending an ellipsis when it was cut. Used for one-line previews (tool call
155
155
  // args, tool results) so a long argument cannot print a line wider than the
156
156
  // terminal — which made some terminals show a horizontal scrollbar.
157
+ // Word-wrap a PLAIN (no-ANSI) string to at most `maxCols` visible columns per
158
+ // line. Used for service messages printed above the input (warnings): the
159
+ // terminal's OWN wrap breaks at the full width and mid-word, while our previews
160
+ // stop at cols-1 — the mismatch looked ragged. Wrapping here keeps every line
161
+ // inside the same margin. A single word longer than the width is hard-split so
162
+ // it never overflows.
163
+ export function wrapToWidth(s, maxCols) {
164
+ const width = Math.max(1, Math.floor(maxCols));
165
+ const text = String(s ?? '');
166
+ const out = [];
167
+ for (const raw of text.split(NL)) {
168
+ if (raw === '') {
169
+ out.push('');
170
+ continue;
171
+ }
172
+ let line = '';
173
+ let lineW = 0;
174
+ for (const word of raw.split(' ')) {
175
+ const w = visLen(word);
176
+ // Hard-split a word longer than a whole line.
177
+ if (w > width) {
178
+ if (line) {
179
+ out.push(line);
180
+ }
181
+ let piece = '';
182
+ let pieceW = 0;
183
+ for (const ch of word) {
184
+ const cw = charWidth(ch.codePointAt(0));
185
+ if (pieceW + cw > width) {
186
+ out.push(piece);
187
+ piece = '';
188
+ pieceW = 0;
189
+ }
190
+ piece += ch;
191
+ pieceW += cw;
192
+ }
193
+ line = piece;
194
+ lineW = pieceW;
195
+ continue;
196
+ }
197
+ const sep = line ? 1 : 0;
198
+ if (lineW + sep + w > width) {
199
+ out.push(line);
200
+ line = word;
201
+ lineW = w;
202
+ }
203
+ else {
204
+ line = line ? line + ' ' + word : word;
205
+ lineW += sep + w;
206
+ }
207
+ }
208
+ out.push(line);
209
+ }
210
+ return out.join(NL);
211
+ }
157
212
  export function truncateToWidth(s, maxCols) {
158
213
  const width = Math.max(1, Math.floor(maxCols));
159
214
  const text = String(s ?? '');
package/dist/input.js CHANGED
@@ -3,12 +3,12 @@ import { renderMarkdown } from './markdown.js';
3
3
  import { randomThinkingPhrase, stripEllipsis, DOTS, renderDots, } from './spinner.js';
4
4
  import { translate } from './i18n.js';
5
5
  import { AttachmentStore, parseImagePaste, looksLikeFilePath, } from './attachments.js';
6
- import { NL, CR, ESC, PASTE_START, PASTE_END, SUGGEST_PAGE, isSubsequence, pasteReplacement, expandPastes, safeJson, visRows, visLen, charWidth, layoutInput, formatTokenStatus, tokenStatusLevel, truncateToWidth, CONTEXT_LIMIT, } from './input/layout.js';
6
+ import { NL, CR, ESC, PASTE_START, PASTE_END, SUGGEST_PAGE, isSubsequence, pasteReplacement, expandPastes, safeJson, visRows, visLen, charWidth, layoutInput, formatTokenStatus, tokenStatusLevel, truncateToWidth, wrapToWidth, CONTEXT_LIMIT, } from './input/layout.js';
7
7
  // The pure layout/formatting helpers (visual rows, cursor position, visible
8
8
  // width, paste markers, token formatting) live in ./input/layout.js. They are
9
9
  // re-exported here so every existing import from './input.js' keeps working,
10
10
  // and so tests can import them from either module (C3 step 3).
11
- export { PASTE_MIN_LINES, SUGGEST_PAGE, isSubsequence, countPasteLines, formatPasteMarker, pasteReplacement, expandPastes, visRows, visLen, charWidth, layoutInput, tokenStatusLevel, formatTokenStatus, formatCompactTokens, truncateToWidth, CONTEXT_LIMIT, CONTEXT_YELLOW_PCT, CONTEXT_RED_PCT, } from './input/layout.js';
11
+ export { PASTE_MIN_LINES, SUGGEST_PAGE, isSubsequence, countPasteLines, formatPasteMarker, pasteReplacement, expandPastes, visRows, visLen, charWidth, layoutInput, tokenStatusLevel, formatTokenStatus, formatCompactTokens, truncateToWidth, wrapToWidth, CONTEXT_LIMIT, CONTEXT_YELLOW_PCT, CONTEXT_RED_PCT, } from './input/layout.js';
12
12
  // How long a lone ESC is held before it is treated as the Escape key (and not
13
13
  // as the head of a control sequence split across two reads). 40ms is below a
14
14
  // human "two Escapes" interval but above typical chunk-split latency.
@@ -1115,7 +1115,12 @@ export class LineEditor {
1115
1115
  // Warning to the operator (e.g. the agent stopped suspiciously).
1116
1116
  // Printed above the input line without overwriting it.
1117
1117
  warning(msg) {
1118
- this.printAbove(NL + theme.warn('⚠ ' + msg));
1118
+ // Wrap long service messages (send failures, rate limits) to the same
1119
+ // margin the tool previews use, instead of letting the terminal wrap them
1120
+ // at the full width and mid-word — that mismatch looked ragged.
1121
+ const maxW = Math.max(20, (process.stdout.columns || 80) - 1);
1122
+ const wrapped = wrapToWidth('⚠ ' + msg, maxW);
1123
+ this.printAbove(NL + theme.warn(wrapped));
1119
1124
  }
1120
1125
  // ---------- input ----------
1121
1126
  _submit() {
package/dist/shell.js CHANGED
@@ -4,14 +4,6 @@ import os from 'os';
4
4
  // Shared shell runner for the Bash tool and the operator's `!command` escape.
5
5
  // Kept in its own module so both call sites use the SAME sandbox guard and the
6
6
  // SAME abort wiring; a second, ad-hoc runner would drift from the real one.
7
- // Quote one argument for POSIX sh: wrap in single quotes and turn every inner
8
- // single quote into '\''. Inside single quotes sh expands NOTHING, so a value
9
- // carrying $(...), backticks or $VAR is passed through literally. A plain
10
- // JSON.stringify is NOT a safe substitute: it produces double quotes, inside
11
- // which sh still performs command substitution.
12
- export function shellQuote(arg) {
13
- return "'" + String(arg).replace(/'/g, "'\\''") + "'";
14
- }
15
7
  // Sandbox (option A): don't let a shell command go above the project root.
16
8
  // This is a protective barrier, not full OS isolation.
17
9
  export function assertCommandInsideRoot(root, command) {
package/dist/spinner.js CHANGED
@@ -2,7 +2,7 @@ import ora from 'ora';
2
2
  import { theme, divider } from './theme.js';
3
3
  import { renderMarkdown } from './markdown.js';
4
4
  import { translate } from './i18n.js';
5
- import { truncateToWidth } from './input/layout.js';
5
+ import { truncateToWidth, wrapToWidth } from './input/layout.js';
6
6
  // Phrases for the "agent is working" spinner. Chosen in random order.
7
7
  // The text comes from i18n via the key 'spinner.phrases' (strings separated by "|").
8
8
  export function randomThinkingPhrase(locale = 'ru') {
@@ -199,7 +199,8 @@ export function createSpinner(locale = 'ru') {
199
199
  },
200
200
  warning: (msg) => {
201
201
  stop();
202
- console.log(String.fromCharCode(10) + theme.warn('⚠ ' + msg));
202
+ const maxW = Math.max(20, (process.stdout.columns || 80) - 1);
203
+ console.log(String.fromCharCode(10) + theme.warn(wrapToWidth('⚠ ' + msg, maxW)));
203
204
  },
204
205
  stop,
205
206
  succeed: () => { },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.67.4",
3
+ "version": "2.68.1",
4
4
  "description": "Terminal coding agent that drives chat.deepseek.com through Playwright — no API key required. Reads and edits files, runs commands, commits to git, like Claude Code / Codex CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,7 +27,7 @@
27
27
  "lint:fix": "node scripts/eslint-ts6.mjs --fix",
28
28
  "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"*.{js,json,md}\"",
29
29
  "format:check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\"",
30
- "prepublishOnly": "npm run build",
30
+ "prepublishOnly": "npm run build:readme && npm run build",
31
31
  "test": "tsx --test test/*.test.ts",
32
32
  "test:watch": "tsx --test --watch test/*.test.ts",
33
33
  "self-smoke": "tsx scripts/self-smoke.mjs",