zames_pro 2.78.0 → 2.78.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,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.78.1] - 2026-10-18
11
+
12
+ ### Fixed
13
+
14
+ - **permissions:** match a command rule against every plausible command key
15
+ - **hooks:** cap a PreToolUse denial so it cannot flood the tool result
16
+ - **diagnostics:** treat a zero-error watch banner as clean
17
+ - **statusline:** cap the hook output so a chatty command cannot flood the row
18
+
10
19
  ## [2.78.0] - 2026-10-18
11
20
 
12
21
  ### Added
@@ -23,11 +23,17 @@ export function summarizeDiagnostics(raw, maxChars) {
23
23
  const text = String(raw || '').trim();
24
24
  if (!text)
25
25
  return null;
26
- // tsc prints nothing on success; eslint prints nothing too. Guard against a
27
- // benign banner that some tools emit.
28
- if (/^(no errors?\.?|0 problems?.*)$/im.test(text) && text.length < 40) {
26
+ // tsc prints nothing on success, but with `--watch` it prints a banner
27
+ // ("Found 0 errors. Watching for file changes.") that is longer than the
28
+ // old 40-char cutoff — so every edit in watch mode appended a pointless
29
+ // diagnostics block to the tool result. Treat a ZERO-count banner as clean
30
+ // however long it is; a non-zero count still falls through to the lines.
31
+ if (/^(?:found\s+)?0\s+errors?\b/im.test(text))
32
+ return null;
33
+ if (/^no errors?\.?$/im.test(text))
34
+ return null;
35
+ if (/^0 problems?\b/im.test(text))
29
36
  return null;
30
- }
31
37
  // Keep only error-looking lines when we can recognise them, so a long build
32
38
  // log collapses to the actual problems. Fall back to the whole output.
33
39
  const lines = text.split(/\r?\n/);
package/dist/hooks.js CHANGED
@@ -12,6 +12,9 @@ const LIFECYCLE_EVENTS = [
12
12
  // tool call must not be held hostage by it.
13
13
  const HOOK_TIMEOUT_MS = 10_000;
14
14
  const HOOK_MAX_BUFFER = 1024 * 1024;
15
+ // A PreToolUse denial becomes the tool result the model reads, so it is capped
16
+ // like any other tool output.
17
+ const HOOK_DENIAL_CAP = 4000;
15
18
  export function hooksConfigPath(workdir) {
16
19
  return path.join(workdir, '.zames', 'hooks.json');
17
20
  }
@@ -142,7 +145,15 @@ export async function runPreToolUse(hooks, tool, args, workdir) {
142
145
  const r = await runHook(entry, 'PreToolUse', tool, args, workdir);
143
146
  if (r.code !== 0) {
144
147
  const reason = (r.stderr || r.stdout).trim();
145
- return reason || `hook exited with code ${r.code}`;
148
+ // The denial text becomes a TOOL RESULT the model reads: a hook that
149
+ // dumps a log must not flood the context with one call.
150
+ const capped = reason.slice(0, HOOK_DENIAL_CAP);
151
+ const suffix = reason.length > HOOK_DENIAL_CAP
152
+ ? '\n… [denial truncated: ' +
153
+ (reason.length - HOOK_DENIAL_CAP) +
154
+ ' chars omitted]'
155
+ : '';
156
+ return capped + suffix || `hook exited with code ${r.code}`;
146
157
  }
147
158
  }
148
159
  return null;
@@ -63,6 +63,10 @@ function safeRegex(source) {
63
63
  // A file path may arrive in any of these keys depending on the tool.
64
64
  // We collect them all and match the path regex against ANY of them.
65
65
  const PATH_KEYS = ['path', 'file_path', 'notebook_path', 'from', 'to'];
66
+ // The command may be named differently by an MCP tool (its JSON Schema picks
67
+ // the field name) or invented by the model. Checking every plausible key keeps
68
+ // a `command` rule from being bypassed just because the field is called `cmd`.
69
+ const COMMAND_KEYS = ['command', 'cmd', 'shell_command', 'script', 'argv'];
66
70
  // Best-effort check whether a string looks like a filesystem path. Used for
67
71
  // tools whose arguments are not covered by PATH_KEYS (MCP tools take arbitrary
68
72
  // arg names from their JSON Schema), so a `path` rule is not silently
@@ -106,7 +110,7 @@ export function decidePermission(policy, tool, args) {
106
110
  if (!policy)
107
111
  return { action: 'allow', reason: 'no policy' };
108
112
  const a = (args ?? {});
109
- const command = typeof a['command'] === 'string' ? a['command'] : '';
113
+ const commands = COMMAND_KEYS.filter((k) => typeof a[k] === 'string').map((k) => a[k]);
110
114
  const paths = collectPathCandidates(tool, args);
111
115
  for (const r of policy.rules) {
112
116
  if (r.tool) {
@@ -116,7 +120,7 @@ export function decidePermission(policy, tool, args) {
116
120
  }
117
121
  if (r.command) {
118
122
  const re = safeRegex(r.command);
119
- if (!re || !command || !re.test(command))
123
+ if (!re || !commands.some((c) => re.test(c)))
120
124
  continue;
121
125
  }
122
126
  if (r.path) {
@@ -26,6 +26,10 @@ export function runStatusLineCommand(workdir, command, info, timeoutMs = 4000) {
26
26
  const first = String(out).split(/\r?\n/)[0].trim();
27
27
  resolve(first);
28
28
  };
29
+ // Only the FIRST LINE is shown, so there is no reason to keep collecting a
30
+ // big blob: a chatty command (a JSON dump, `ls -R`) would grow `out`
31
+ // unboundedly and the status row then repainted megabytes. Stop appending
32
+ // once a short line is buffered (the rest of that line does not matter).
29
33
  const timer = setTimeout(() => {
30
34
  try {
31
35
  child.kill();
@@ -33,8 +37,16 @@ export function runStatusLineCommand(workdir, command, info, timeoutMs = 4000) {
33
37
  catch { }
34
38
  finish();
35
39
  }, timeoutMs);
40
+ // A status line is a short badge: cap what is buffered so a chatty
41
+ // command cannot grow it (and the rendered row) without bound.
42
+ const MAX_OUT = 400;
36
43
  child.stdout?.on('data', (c) => {
37
- out += c.toString();
44
+ if (out.length >= MAX_OUT)
45
+ return;
46
+ // A chunk can be huge (one 64 KB read); clip it to the remaining space
47
+ // so the buffer never exceeds MAX_OUT. An unbounded `out` made the
48
+ // status row repaint megabytes on every tick.
49
+ out += c.toString().slice(0, MAX_OUT - out.length);
38
50
  });
39
51
  child.on('error', () => {
40
52
  clearTimeout(timer);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.78.0",
3
+ "version": "2.78.1",
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": {