zames_pro 2.67.3 → 2.68.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/CHANGELOG.md +23 -0
- package/dist/commands.js +3 -1
- package/dist/gitTools.js +28 -60
- package/dist/index.js +10 -10
- package/dist/shell.js +0 -8
- package/package.json +2 -2
- package/scripts/build-readme.mts +21 -28
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.68.0] - 2026-10-08
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **git:** every git invocation (GitStatus/GitLog/GitBranchList/GitAdd/
|
|
15
|
+
GitCommit and the `/diff`, `/diffstat`, `/doctor` helpers) now goes
|
|
16
|
+
through `runGitArgs()` (`execFile` with an argument array, no shell). The
|
|
17
|
+
last shell-string variant (`runGit`) is gone, so no git argument can ever
|
|
18
|
+
be interpreted by a shell. `diffGitArgs()` now returns an array.
|
|
19
|
+
- **cleanup:** removed the dead `shellQuote` export (nothing used it after
|
|
20
|
+
the move to `execFile`).
|
|
21
|
+
- **build:** `prepublishOnly` now runs `build:readme` before `build`, so a
|
|
22
|
+
published README.md can never drift from `docs/readme.*.md`.
|
|
23
|
+
|
|
24
|
+
## [2.67.4] - 2026-10-08
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **readme:** `npm run build:readme` generated a `README.ru.md` again even
|
|
29
|
+
though it is no longer published (removed from `files[]` in 2.67.1) and a
|
|
30
|
+
test asserted it must not exist — so a local build wrote an untracked file
|
|
31
|
+
and could trip the drift test. The generator now writes ONLY `README.md`.
|
|
32
|
+
|
|
10
33
|
## [2.67.3] - 2026-10-08
|
|
11
34
|
|
|
12
35
|
### 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 ? '
|
|
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 {
|
|
1
|
+
import { execFile } from 'child_process';
|
|
2
2
|
const BS = String.fromCharCode(92);
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
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
|
|
83
|
-
const statusR = await
|
|
84
|
-
const remoteR = await
|
|
85
|
-
const aheadR = await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
//
|
|
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 {
|
|
2858
|
-
const probe = await
|
|
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
|
|
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 {
|
|
2869
|
-
const probe = await
|
|
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
|
|
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 {
|
|
2920
|
+
const { runGitArgs } = await import('./gitTools.js');
|
|
2921
2921
|
let gitOk = false;
|
|
2922
2922
|
let gitBranch = null;
|
|
2923
2923
|
try {
|
|
2924
|
-
const probe = await
|
|
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
|
|
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
|
|
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),
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.68.0",
|
|
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",
|
package/scripts/build-readme.mts
CHANGED
|
@@ -1,22 +1,21 @@
|
|
|
1
|
-
// Assemble README.md
|
|
1
|
+
// Assemble README.md from the two language sources.
|
|
2
2
|
//
|
|
3
3
|
// Why: npm renders exactly ONE readme per package (README.md in the root) and
|
|
4
|
-
// has no language tabs
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// it would just duplicate the folded block.
|
|
4
|
+
// has no language tabs. npm renders the readme with GitHub Flavored Markdown
|
|
5
|
+
// (via GitHub's API), so a <details> block DOES work there -- and on GitHub
|
|
6
|
+
// too. We keep the English text on top and fold the Russian translation into
|
|
7
|
+
// a collapsible block at the bottom. The language switcher is an in-page
|
|
8
|
+
// `#readme-ru` anchor, so one README.md serves both languages -- there is
|
|
9
|
+
// no separate README.ru.md anymore (it was invisible on npm and just
|
|
10
|
+
// duplicated the folded block).
|
|
12
11
|
//
|
|
13
12
|
// The two sources (docs/readme.en.md, docs/readme.ru.md) are the single point
|
|
14
13
|
// of edit for each language; this script wires the switcher and the collapsible
|
|
15
|
-
// cross-language block so neither
|
|
16
|
-
// generated
|
|
17
|
-
// and fails if
|
|
14
|
+
// cross-language block so neither language has to be maintained by hand. The
|
|
15
|
+
// generated file is committed; test/readme-built.test.ts re-runs buildReadmes()
|
|
16
|
+
// and fails if it drifts from the sources.
|
|
18
17
|
//
|
|
19
|
-
// Usage:
|
|
18
|
+
// Usage: npom run build:readme
|
|
20
19
|
|
|
21
20
|
import fs from 'node:fs'
|
|
22
21
|
import path from 'node:path'
|
|
@@ -70,19 +69,14 @@ function details(summary: string, body: string): string {
|
|
|
70
69
|
].join(NL)
|
|
71
70
|
}
|
|
72
71
|
|
|
73
|
-
// Returns the
|
|
74
|
-
// compare
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
// without fetching the other one.
|
|
79
|
-
export function buildReadmes(rootDir: string): {
|
|
80
|
-
readmeEn: string
|
|
81
|
-
readmeRu: string
|
|
82
|
-
} {
|
|
72
|
+
// Returns the generated README.md WITHOUT touching the disk, so a test can
|
|
73
|
+
// compare it against the committed file. Only README.md is generated: the
|
|
74
|
+
// Russian translation lives inside it as a <details> block, and the switcher
|
|
75
|
+
// is an in-page `#readme-ru` anchor.
|
|
76
|
+
export function buildReadmes(rootDir: string): { readmeEn: string } {
|
|
83
77
|
const en = readSource(rootDir, 'readme.en.md')
|
|
84
78
|
const ru = readSource(rootDir, 'readme.ru.md')
|
|
85
|
-
const
|
|
79
|
+
const readmeEn = [
|
|
86
80
|
en.head,
|
|
87
81
|
'',
|
|
88
82
|
EN_SWITCH,
|
|
@@ -93,7 +87,7 @@ export function buildReadmes(rootDir: string): {
|
|
|
93
87
|
details('🇷🇺 Читать по-русски (Russian)', ru.body),
|
|
94
88
|
'',
|
|
95
89
|
].join(NL)
|
|
96
|
-
return { readmeEn
|
|
90
|
+
return { readmeEn }
|
|
97
91
|
}
|
|
98
92
|
|
|
99
93
|
// Run only when invoked directly (not when imported by a test).
|
|
@@ -101,8 +95,7 @@ const invoked =
|
|
|
101
95
|
process.argv[1] &&
|
|
102
96
|
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
103
97
|
if (invoked) {
|
|
104
|
-
const { readmeEn
|
|
98
|
+
const { readmeEn } = buildReadmes(root)
|
|
105
99
|
fs.writeFileSync(path.join(root, 'README.md'), readmeEn)
|
|
106
|
-
|
|
107
|
-
console.log('build-readme: wrote README.md and README.ru.md')
|
|
100
|
+
console.log('build-readme: wrote README.md')
|
|
108
101
|
}
|