zames_pro 2.96.9 → 2.96.10

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,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.96.10] - 2026-11-06
11
+
12
+ ### Fixed
13
+
14
+ - **export:** keep a tool result on one line and mark a clipped one
15
+ - **config:** validate a live store value before writing it
16
+ - **config:** an unknown language is rejected instead of silently defaulted
17
+ - **config:** a password keeps its spaces when set from the command line
18
+ - **transcript:** cap the session label so the file name fits the filesystem
19
+
10
20
  ## [2.96.9] - 2026-11-06
11
21
 
12
22
  ### Fixed
package/dist/commands.js CHANGED
@@ -299,10 +299,17 @@ export function formatExport(entries, meta = {}) {
299
299
  JSON.stringify(e.args ?? {}));
300
300
  }
301
301
  else if (type === 'tool_result') {
302
+ // A tool result is TEXT: a newline in it started a bare report line and
303
+ // broke the one-line list. Flatten it, and MARK a clipped result so the
304
+ // export does not look like the whole output.
305
+ const raw = String(e.result ?? '');
306
+ const flat = raw.split(/\s*\r?\n\s*/).join(' ↵ ');
307
+ const clipped = clipExportText(flat, 500);
302
308
  out.push(' -> ' +
303
309
  String(e.tool ?? '?') +
304
310
  ': ' +
305
- clipExportText(String(e.result ?? ''), 500));
311
+ clipped +
312
+ (clipped.length < flat.length ? ' …' : ''));
306
313
  }
307
314
  else if (type === 'assistant_final') {
308
315
  out.push('## Answer');
@@ -8,6 +8,7 @@
8
8
  // The interactive menu (`/config` bare or `menu`) is delegated to openMenu()
9
9
  // because it pauses the editor and reads keys itself.
10
10
  import { theme } from './theme.js';
11
+ import { isLocale } from './i18n.js';
11
12
  import { configGetValue, configValueHint } from './config-commands.js';
12
13
  /**
13
14
  * Handle a `/config <sub>` text command. Never throws for a bad subcommand or
@@ -65,6 +66,18 @@ export async function handleConfigCommand(input, deps) {
65
66
  const field = schema.find((f) => f.path === 'ui.locale');
66
67
  if (!field)
67
68
  return;
69
+ // `normalizeLocale` FALLS BACK to the default for anything it does not
70
+ // understand, so `/config lang xx` silently SAVED English and reported it
71
+ // as set — the operator's typo was accepted. Reject an unknown value.
72
+ const normalized = String(val).trim().toLowerCase();
73
+ if (!isLocale(normalized) &&
74
+ !['russian', 'русский', 'english', 'английский'].includes(normalized)) {
75
+ printErr(theme.error(t('cfg.bad_value', {
76
+ v: 'ui.locale',
77
+ type: configValueHint(field, t),
78
+ })));
79
+ return;
80
+ }
68
81
  const loc = normalizeLocale(val);
69
82
  try {
70
83
  setValue(field, loc);
@@ -94,7 +107,16 @@ export async function handleConfigCommand(input, deps) {
94
107
  }
95
108
  if (sub === 'set') {
96
109
  const key = parts[2];
97
- const raw = parts.slice(3).join(' ');
110
+ // A password/username is a SECRET: it may legally contain repeated,
111
+ // leading or trailing spaces, and joining the tokens with ONE space
112
+ // silently changed it (auto-login then failed with "wrong password").
113
+ // Take the text after the key VERBATIM for those fields.
114
+ const isSecret = /(password|username)\/?$/i.test(key || '');
115
+ const raw = isSecret
116
+ ? String(input)
117
+ .replace(/^\s*\/config\s+set\s+\S+\s?/i, '')
118
+ .replace(/\s+$/, '')
119
+ : parts.slice(3).join(' ');
98
120
  const field = schema.find((f) => f.path === key);
99
121
  if (!field) {
100
122
  printErr(theme.error(t('cfg.unknown_key', { v: key || '' })));
@@ -59,16 +59,37 @@ export function applyBrowserLiveConfig(path, value, browser) {
59
59
  * their config at construction, so a change must be written to the instance
60
60
  * too. */
61
61
  export function applyStoreLiveConfig(path, value, stores) {
62
+ // The browser map VALIDATES its values; these stores were written blindly,
63
+ // so a negative `maxBackups` (rejected by the schema) or a string could
64
+ // reach a live store and change retention silently.
65
+ const isCount = (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0;
66
+ const isBool = (v) => typeof v === 'boolean';
62
67
  if (path === 'undo.enabled' || path === 'undo.maxBackups') {
63
68
  if (!stores.undo)
64
69
  return false;
65
- stores.undo[path === 'undo.enabled' ? 'enabled' : 'maxBackups'] = value;
70
+ if (path === 'undo.enabled') {
71
+ if (!isBool(value))
72
+ return false;
73
+ stores.undo.enabled = value;
74
+ return true;
75
+ }
76
+ if (!isCount(value))
77
+ return false;
78
+ stores.undo.maxBackups = value;
66
79
  return true;
67
80
  }
68
81
  if (path === 'checkpoint.enabled' || path === 'checkpoint.maxBackups') {
69
82
  if (!stores.checkpoints)
70
83
  return false;
71
- stores.checkpoints[path === 'checkpoint.enabled' ? 'enabled' : 'maxBackups'] = value;
84
+ if (path === 'checkpoint.enabled') {
85
+ if (!isBool(value))
86
+ return false;
87
+ stores.checkpoints.enabled = value;
88
+ return true;
89
+ }
90
+ if (!isCount(value))
91
+ return false;
92
+ stores.checkpoints.maxBackups = value;
72
93
  return true;
73
94
  }
74
95
  return false;
@@ -129,10 +129,15 @@ export class Transcript {
129
129
  const safeName = String(sessionName)
130
130
  .replace(/[\\/:*?"<>|\u0000-\u001f]/g, '_')
131
131
  .trim() || 'session';
132
+ // The stamp and the `-`/`.jsonl` suffix take ~40 bytes of the 255-byte
133
+ // NAME limit: a deeply nested project path produced an over-long file
134
+ // name, the stream failed with ENAMETOOLONG and the WHOLE run had no
135
+ // transcript. Cap the label, keeping its head.
136
+ const cappedName = safeName.slice(0, 120);
132
137
  // Rotation BEFORE the create: the new file then has the newest stamp,
133
138
  // so the current run's transcript is never a deletion candidate.
134
139
  rotateTranscripts(dir);
135
- this.file = path.join(dir, `${safeName}-${stamp}.jsonl`);
140
+ this.file = path.join(dir, `${cappedName}-${stamp}.jsonl`);
136
141
  this.stream = fs.createWriteStream(this.file, { flags: 'a' });
137
142
  // Write errors (disk full, file deleted, etc.) arrive as an 'error'
138
143
  // event; without a listener this is an uncaught exception.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.96.9",
3
+ "version": "2.96.10",
4
4
  "description": "Terminal coding agent that drives a real browser to chat with DeepSeek, Qwen or GLM — 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": {