zames_pro 2.81.0 → 2.81.2

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,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.81.2] - 2026-10-21
11
+
12
+ ### Fixed
13
+
14
+ - **tools:** read-only mode drops concatenated/camelCase MCP writers
15
+
16
+ ## [2.81.1] - 2026-10-21
17
+
18
+ ### Added
19
+
20
+ - send telemetry is opt-in (on automatically in dev)
21
+
22
+ ### Fixed
23
+
24
+ - **config:** mask a password in the /config set confirmation
25
+
26
+ ### Changed
27
+
28
+ - apply prettier to the GLM provider and send runtime
29
+
10
30
  ## [2.81.0] - 2026-10-21
11
31
 
12
32
  ### Added
@@ -113,7 +113,9 @@ async function runHeadedLogin(deps) {
113
113
  // may still hold the session cookies, so verify once more and accept.
114
114
  if (deps.windowClosed()) {
115
115
  deps.notice('login.window_closed');
116
- const again = await deps.probeLogin().catch(() => 'unknown');
116
+ const again = await deps
117
+ .probeLogin()
118
+ .catch(() => 'unknown');
117
119
  return again === 'in';
118
120
  }
119
121
  await deps.sleep(1000);
@@ -90,7 +90,11 @@ export async function handleConfigCommand(input, deps) {
90
90
  }
91
91
  print(theme.assistant(t('cfg.saved', {
92
92
  v: key,
93
- value: JSON.stringify(getValue(key)),
93
+ // Mask a SECRET here too: this line goes to the terminal scrollback,
94
+ // the transcript and screenshots. `configGetValue` already masks the
95
+ // password for /config get and the list, but the save confirmation
96
+ // echoed it in the clear.
97
+ value: configGetValue(key, getValue(key), t),
94
98
  file: homeConfigPath,
95
99
  })));
96
100
  return;
package/dist/config.js CHANGED
@@ -115,7 +115,11 @@ export const DEFAULTS = {
115
115
  maxSubagents: 3,
116
116
  deepThinking: false,
117
117
  webSearch: true,
118
- sendTelemetry: true,
118
+ // OFF by default: the telemetry file only serves diagnosis and manual
119
+ // calibration of the send pauses (the adaptive throttle needs the EVENTS,
120
+ // not the file, and keeps working). A regular user gains nothing from it,
121
+ // so it is opted into with `/config set browser.sendTelemetry true`.
122
+ sendTelemetry: false,
119
123
  auth: {
120
124
  username: '',
121
125
  password: '',
@@ -292,9 +292,7 @@ export class GlmBrowser {
292
292
  // re-apply them BEFORE the first goto, or the SPA answers 401 and may wipe
293
293
  // its own token.
294
294
  if (this._carryCookies && this._carryCookies.length) {
295
- await this.context
296
- .addCookies(this._carryCookies)
297
- .catch(() => { });
295
+ await this.context.addCookies(this._carryCookies).catch(() => { });
298
296
  }
299
297
  this.page = this.context.pages()[0] || (await this.context.newPage());
300
298
  this._windowClosed = false;
@@ -366,9 +364,7 @@ export class GlmBrowser {
366
364
  throw new Error('glm: the browser profile is busy (' + e.message + ')');
367
365
  }
368
366
  if (this._carryCookies && this._carryCookies.length) {
369
- await this.context
370
- .addCookies(this._carryCookies)
371
- .catch(() => { });
367
+ await this.context.addCookies(this._carryCookies).catch(() => { });
372
368
  }
373
369
  this.page = this.context.pages()[0] || (await this.context.newPage());
374
370
  this._windowClosed = false;
@@ -1110,7 +1106,7 @@ export class GlmBrowser {
1110
1106
  const el = document.querySelector(sel);
1111
1107
  if (!el)
1112
1108
  return false;
1113
- return el.disabled === true || el.getAttribute('aria-disabled') === 'true';
1109
+ return (el.disabled === true || el.getAttribute('aria-disabled') === 'true');
1114
1110
  }, INPUT_SELECTORS[0])
1115
1111
  .catch(() => false);
1116
1112
  return !!disabled;
@@ -1297,7 +1293,11 @@ export class GlmBrowser {
1297
1293
  return [
1298
1294
  { capability: 'input', selectors: INPUT_SELECTORS, critical: true },
1299
1295
  { capability: 'toggles', selectors: [], critical: false },
1300
- { capability: 'newChat', selectors: [NEW_CHAT_SELECTOR], critical: false },
1296
+ {
1297
+ capability: 'newChat',
1298
+ selectors: [NEW_CHAT_SELECTOR],
1299
+ critical: false,
1300
+ },
1301
1301
  ];
1302
1302
  }
1303
1303
  async probeUiHealth() {
@@ -1385,7 +1385,8 @@ export class GlmBrowser {
1385
1385
  this.sendRuntime.onOutcome('guest_limit', { errorText: msg });
1386
1386
  throw new Error(this._t('glm.guest_mode'));
1387
1387
  }
1388
- if (isQuotaExhaustedText(msg) || isQuotaExhaustedText(this._netBody)) {
1388
+ if (isQuotaExhaustedText(msg) ||
1389
+ isQuotaExhaustedText(this._netBody)) {
1389
1390
  this.sendRuntime.onOutcome('rate_limit', { errorText: 'quota' });
1390
1391
  throw new Error(this._t('glm.quota_exhausted'));
1391
1392
  }
@@ -1501,7 +1502,10 @@ export class GlmBrowser {
1501
1502
  try {
1502
1503
  const [chooser] = await Promise.all([
1503
1504
  this.page.waitForEvent('filechooser', { timeout: 5000 }),
1504
- this.page.locator(UPLOAD_BUTTON_SELECTOR).first().click({ timeout: 3000 }),
1505
+ this.page
1506
+ .locator(UPLOAD_BUTTON_SELECTOR)
1507
+ .first()
1508
+ .click({ timeout: 3000 }),
1505
1509
  ]);
1506
1510
  await chooser.setFiles(bufPayload);
1507
1511
  await this._awaitUploadPreview(files.map((f) => f.name), 5000);
package/dist/glm-chats.js CHANGED
@@ -31,9 +31,7 @@ export function scrapeGlmChatList(limit) {
31
31
  if (seen.has(m[1]))
32
32
  continue;
33
33
  seen.add(m[1]);
34
- const title = (a.textContent || '')
35
- .trim()
36
- .replace(/\s+/g, ' ');
34
+ const title = (a.textContent || '').trim().replace(/\s+/g, ' ');
37
35
  out.push({ id: m[1], title });
38
36
  if (out.length >= limit)
39
37
  break;
package/dist/index.js CHANGED
@@ -525,9 +525,7 @@ ${theme.bold(t('help.sec.context'))}
525
525
  ${t('help.cmd.lang')}
526
526
 
527
527
  ${theme.bold(t('help.sec.provider'))}
528
- ${t('help.cmd.provider')}${providerHasModelPicker(config.provider)
529
- ? '\n ' + t('help.cmd.model')
530
- : ''}
528
+ ${t('help.cmd.provider')}${providerHasModelPicker(config.provider) ? '\n ' + t('help.cmd.model') : ''}
531
529
 
532
530
  ${theme.bold(t('help.sec.files'))}
533
531
  ${t('help.cmd.transcript')}
@@ -778,7 +776,9 @@ function applyModelContextLimit(model) {
778
776
  const section = providerConfigSection(config.provider);
779
777
  if (!section)
780
778
  return;
781
- const window = config.provider === 'qwen' ? qwenContextWindow(model) : glmContextWindow(model);
779
+ const window = config.provider === 'qwen'
780
+ ? qwenContextWindow(model)
781
+ : glmContextWindow(model);
782
782
  if (window === null)
783
783
  return;
784
784
  const sec = activeProviderSection();
@@ -851,7 +851,11 @@ function createProviderBrowser(provider) {
851
851
  answerTimeoutMs: config.glm.answerTimeoutMs,
852
852
  model: config.glm.model,
853
853
  auth: config.glm.auth,
854
- sendTelemetry: config.browser.sendTelemetry,
854
+ // Dev runs (`npm run dev`, i.e. `--dev`) record the telemetry
855
+ // automatically: it is a diagnostic for the developer. For a regular
856
+ // user it stays OFF by default (see DEFAULTS in config.ts) and is
857
+ // opted into with `/config set browser.sendTelemetry true`.
858
+ sendTelemetry: config.browser.sendTelemetry || devMode,
855
859
  logsDir: config.transcript.dir,
856
860
  loginTimeoutMs: config.browser.loginTimeoutMs,
857
861
  locale: currentLocale,
@@ -1151,7 +1155,8 @@ async function main() {
1151
1155
  await transcript.close();
1152
1156
  return;
1153
1157
  }
1154
- if (providerNeedsVisibleBrowser(config.provider) && headlessFor(config.provider)) {
1158
+ if (providerNeedsVisibleBrowser(config.provider) &&
1159
+ headlessFor(config.provider)) {
1155
1160
  console.log(theme.warn(t('provider.needs_headed', { v: providerLabel(config.provider) })));
1156
1161
  }
1157
1162
  console.log(theme.system(t('msg.interactive')));
@@ -2735,7 +2740,9 @@ async function main() {
2735
2740
  // (Qwen, GLM). DeepSeek has none, so the command is hidden from the
2736
2741
  // hints there and only explains itself when typed by hand.
2737
2742
  if (!providerHasModelPicker(config.provider)) {
2738
- console.log(theme.system(t('model.unsupported', { provider: providerLabel(config.provider) })));
2743
+ console.log(theme.system(t('model.unsupported', {
2744
+ provider: providerLabel(config.provider),
2745
+ })));
2739
2746
  continue;
2740
2747
  }
2741
2748
  const section = providerConfigSection(config.provider);
@@ -214,8 +214,8 @@ export function buildSystemPrompt({ workdir, tools, gitContext = null, locale =
214
214
  // runs: otherwise it asks for a sandbox ("Limit Sandbox Concurrency") or
215
215
  // claims it ran something it did not.
216
216
  const runtimeSection = provider === 'glm'
217
- ? '\n## You run on the OPERATOR\'S MACHINE (GLM)\n\n' +
218
- 'The tools above execute on the operator\'s computer, in the working ' +
217
+ ? "\n## You run on the OPERATOR'S MACHINE (GLM)\n\n" +
218
+ "The tools above execute on the operator's computer, in the working " +
219
219
  'directory shown at the top. They are the ONLY way to touch files or ' +
220
220
  'run commands.\n\n' +
221
221
  '- There is NO cloud sandbox for you. Do NOT request or use a z.ai ' +
@@ -223,7 +223,7 @@ export function buildSystemPrompt({ workdir, tools, gitContext = null, locale =
223
223
  "site's own agent mode and is NOT part of this task.\n" +
224
224
  '- Never claim a command ran unless you emitted its tool call and ' +
225
225
  'received a tool result. If you have not called the tool, say so.\n' +
226
- '- Paths like `/home/z/...` or `/mnt/data` are NOT the operator\'s ' +
226
+ "- Paths like `/home/z/...` or `/mnt/data` are NOT the operator's " +
227
227
  'machine.\n' +
228
228
  '- Do NOT use web search / WebSearch unless the task explicitly asks ' +
229
229
  'for current web information: it is slow, it burns the account quota ' +
package/dist/tools.js CHANGED
@@ -403,13 +403,34 @@ export function filterToolsByAllowList(tools, allowed) {
403
403
  // mutating VERBS in the tool name. It is a heuristic deny-list, not a proof:
404
404
  // an exotic server could name a mutating tool innocuously. The honest default
405
405
  // in read-only mode is therefore to drop everything that looks like a writer.
406
- const MCP_MUTATING_RE = /(^|_)(click|type|navigate|press|upload|select|drag|fill|hover|evaluate|run_code|run_|handle_dialog|key|screenshot|write|create|delete|update|set|put|post|patch|execute|exec|insert|remove|drop|restart|stop|start|install|kill|move|rename|copy|mkdir|rmdir|touch|chmod|chown)(_|$)/;
406
+ const MCP_MUTATING_WORDS = new Set(('click type navigate press upload select drag fill hover evaluate key ' +
407
+ 'screenshot write create delete update set put post patch execute exec ' +
408
+ 'insert remove drop restart stop start install kill move rename copy ' +
409
+ 'mkdir rmdir touch chmod chown').split(' '));
410
+ // Multi-word verbs are matched as a substring of the normalized name: they
411
+ // never stand alone as ONE `_word_`, so a word-set alone missed them.
412
+ const MCP_MUTATING_PHRASES = ['run_code', 'run_', 'handle_dialog'];
413
+ // A camelCase name is split BEFORE it is lowercased: `browserClick` used to
414
+ // become `browserclick` (one word, no verb) and slipped through read-only mode.
415
+ function splitCamelCase(s) {
416
+ return s.replace(/([a-z0-9])([A-Z])/g, '$1_$2');
417
+ }
407
418
  export function isMutatingMcpTool(name) {
408
419
  // Strip the server prefix and normalize separators to underscores so the
409
420
  // verb matcher sees "browser_click" -> "click".
410
421
  const tail = String(name).split('__').pop() || String(name);
411
- const norm = tail.toLowerCase().replace(/[^a-z0-9]+/g, '_');
412
- return MCP_MUTATING_RE.test('_' + norm + '_');
422
+ const norm = splitCamelCase(tail)
423
+ .toLowerCase()
424
+ .replace(/[^a-z0-9]+/g, '_');
425
+ if (MCP_MUTATING_PHRASES.some((p) => norm.includes(p)))
426
+ return true;
427
+ const words = norm.split('_').filter(Boolean);
428
+ if (words.some((w) => MCP_MUTATING_WORDS.has(w)))
429
+ return true;
430
+ // A CONCATENATED style (all lowercase, no separator: `browserclick`,
431
+ // `pageevaluate`) still hides a writer. A verb as a substring is safe here
432
+ // because every verb is a whole action word no read-only tool contains.
433
+ return [...MCP_MUTATING_WORDS].some((v) => norm.includes(v));
413
434
  }
414
435
  // Merge MCP tools into the base set. In read-only (plan) mode the MCP tools
415
436
  // must be filtered too: an MCP server (e.g. @playwright/mcp) exposes mutating
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.81.0",
3
+ "version": "2.81.2",
4
4
  "description": "Terminal coding agent that drives a real browser to chat with DeepSeek, Qwen and other web LLMs — 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": {