zames_pro 2.54.0 → 2.58.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 ADDED
@@ -0,0 +1,209 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [2.58.0]
11
+
12
+ ### Added
13
+
14
+ - `--output-format json|jsonl` for one-shot runs: transcript events are
15
+ streamed as JSON lines on stdout (human text goes to stderr), so zames
16
+ pipes into `jq` and CI pipelines. `Transcript` gained an `onLine` mirror.
17
+ - Loop detection: four identical tool calls in a row trigger a nudge to
18
+ change approach instead of repeating the same call forever.
19
+ - MCP configs pinned to `@latest`/`@next` now warn (in `/mcp` and at
20
+ startup) so the tool set does not silently change between runs.
21
+ - `/help` is grouped into sections (session / workspace / git / agent /
22
+ context / files) instead of one long list.
23
+ - `Grep` accepts a comma-separated `include` list.
24
+ - System prompt: a "Choosing the right tool" section (Edit vs MultiEdit vs
25
+ ApplyPatch, Grep vs Glob vs LS, Git tools vs Bash).
26
+
27
+ ### Changed
28
+
29
+ - `coverage-gate` enforces floors for 8 more modules and prints the
30
+ lowest-covered modules in the CI log.
31
+ - README: Why zames?, an FAQ and a machine-readable-output section.
32
+ - Dependabot config for npm and GitHub Actions.
33
+
34
+ ## [2.57.1]
35
+
36
+ ### Changed
37
+
38
+ - CI workflows use actions/checkout@v5 and actions/setup-node@v5 (the v4
39
+ actions run on the deprecated Node 20 runtime and showed up as an
40
+ annotation on every run).
41
+
42
+ ### Added
43
+
44
+ - README badges (npm version/downloads, CI status, license, Node, PRs) and a
45
+ Features section, so the GitHub landing page and the npm page show what the
46
+ project is at a glance.
47
+ - Community files: issue forms (bug report, feature request), a PR template
48
+ and CODE_OF_CONDUCT.md.
49
+ - package.json: a fuller description and more keywords (npm search), and
50
+ CHANGELOG.md is now included in the published files.
51
+
52
+ ## [2.57.0]
53
+
54
+ ### Added
55
+
56
+ - Plan mode (read-only): start with --plan or toggle with /plan [on|off].
57
+ In this mode mutating tools (Write/Edit/MultiEdit/ApplyPatch/Bash, GitAdd/
58
+ GitCommit/GitPush) are removed from the tool set entirely, so the agent can
59
+ investigate without touching the tree.
60
+ - src/fsutil.ts - one shared atomic writer (writeFileAtomic/writeJsonAtomic).
61
+
62
+ ### Changed
63
+
64
+ - config, sessions and undo now share the single atomic writer (the
65
+ temp-file+rename logic used to be copy-pasted in three places).
66
+
67
+ ### Fixed
68
+
69
+ - BACKLOG.md is now git-ignored and untracked: it is the agent's own
70
+ improvement-notes file, rewritten constantly, and used to be published with
71
+ the package.
72
+ - GitPush validates the branch name before building the shell command, so a
73
+ model-supplied branch cannot smuggle shell metacharacters.
74
+ - SECURITY.md no longer promises a /permissions command / alwaysConfirm list
75
+ that was removed in 2.55.0; AGENTS.md cleaned of the same dead refs.
76
+
77
+ ## [2.56.0]
78
+
79
+ ### Added
80
+
81
+ - `/context` — show what is loaded into the prompt (AGENTS.md, MEMORY.md,
82
+ skills, custom commands) plus the system-prompt size.
83
+ - `/retry` — resend the last task into the same chat (handy after a truncated
84
+ or empty answer).
85
+ - `/rename <title>` — set the current session title (shown in `/sessions`).
86
+ - `/diffstat` — a one-line `git diff --stat` change summary.
87
+ - `/copy` — copy the last assistant answer to the OS clipboard (wl-copy /
88
+ xclip / xsel / pbcopy / clip).
89
+ - `GitShow` and `GitBranchList` git tools (read-only).
90
+ - The status line now shows the elapsed time of the current phase ("45s",
91
+ "2m 05s") in BOTH the LineEditor and the non-TTY spinner, and the spinner
92
+ shows the same "tasks: 2/5" badge the editor does.
93
+ - A one-time hint when the context reaches ~80% while auto-compact is off,
94
+ pointing at `/compact`.
95
+ - Tests: dead-i18n-key guard, agent-facing-no-Cyrillic guard, CLI flag
96
+ consistency, binary content-type handling for `WebFetch`.
97
+
98
+ ### Changed
99
+
100
+ - `WebFetch` returns a short human note for binary content types (PDF, images,
101
+ archives, ...) instead of dumping raw bytes into the model context.
102
+ - `npm run build` now wipes `dist/` first (`scripts/clean-dist.mjs`), so a
103
+ removed source file no longer leaves an orphan `.js` in the package.
104
+
105
+ ### Fixed
106
+
107
+ - `/undo` failure reasons are localized: `src/undo.ts` returned hardcoded
108
+ Russian strings that showed up in an English UI. It now returns machine codes
109
+ ('empty' / 'disabled') localized by the caller.
110
+ - Removed the dead `--project` flag (it only swallowed the following argument)
111
+ and the no-op `--calibrate` flag together with their help/i18n entries.
112
+ - Removed the orphaned `confirm.hint` / `confirm.ask_label` i18n keys left by
113
+ the confirm-subsystem removal.
114
+ - README Node requirement aligned with `engines` / `.nvmrc` (>= 20).
115
+ - `GitBranchList` quotes its `--format` argument so the POSIX shell does not
116
+ choke on the parentheses/brackets in the format string.
117
+
118
+ ## [2.55.0]
119
+
120
+ ### Added
121
+
122
+ - `!command` in the prompt runs a shell command directly, bypassing the model
123
+ (like Claude Code's bash mode). The same sandbox guard as the `Bash` tool
124
+ applies, so a direct command cannot leave the project.
125
+ - `/remember <text>` appends a durable note to the project `MEMORY.md`, so a
126
+ fact worth keeping across sessions is written to disk instead of living only
127
+ in the chat.
128
+ - `/skills`, `/memory`, `/remember`, `/init` and `/mcp` are now listed in the
129
+ `/help` output (they were only in the «/» completion list before).
130
+
131
+ ### Changed
132
+
133
+ - The shell runner moved to `src/shell.ts`, shared by the `Bash` tool and the
134
+ new `!command` escape (one sandbox guard, one abort wiring).
135
+ - `/self-review` now copies root configs (`package.json`, `eslint.config.js`,
136
+ `tsconfig*.json`, `.prettierrc`) and `scripts/**` into a read-only `_context/`
137
+ folder of the snapshot, so the reviewer can read them. The editable set
138
+ (diff/apply) stays `.ts`-only.
139
+ - `package.json` gained `packageManager: npm@11.19.0`.
140
+
141
+ ### Fixed
142
+
143
+ - The status line no longer prefixes a running-tool indicator with the
144
+ previous answer's "done" phase (e.g. `✓ done: running Bash`): `toolCall()`
145
+ clears the send-phase state before starting the tool animation.
146
+
147
+ ### Removed
148
+
149
+ - The never-wired tool-confirmation subsystem: `src/confirm.ts`
150
+ (`ConfirmManager`/`formatDiffPreview`) was fully implemented and tested but
151
+ never called by the runtime, so `confirmation.write/edit/bash`,
152
+ `alwaysConfirm` and the `/permissions` command did nothing. Removed the
153
+ module, its tests, the config keys, the `/permissions` command and the
154
+ related i18n strings.
155
+
156
+ ## [2.54.0]
157
+
158
+ ### Added
159
+
160
+ - New unit tests for previously uncovered modules: `diff.ts` (unified/color
161
+ diff), `spinner.ts` (ellipsis/dots/UI surface), `confirm.ts`
162
+ (confirmation logic + alwaysConfirm fallback) and `markdown.ts`
163
+ (rendering of headings, lists, code, tables).
164
+
165
+ - `WebFetch` now blocks private/loopback/link-local addresses (SSRF guard,
166
+ including cloud metadata `169.254.169.254`) and retries transient network
167
+ errors and 5xx responses with exponential backoff.
168
+ - CI prints per-file test coverage (`node --experimental-test-coverage`) so
169
+ untested modules are visible in the log (informational, does not fail).
170
+
171
+ ### Changed
172
+
173
+ - The `pre-push` git hook no longer runs the full test suite (it was fragile
174
+ and timed out on tag pushes); tests stay in `pre-commit` and CI.
175
+
176
+ ## [2.53.1]
177
+
178
+ ### Fixed
179
+
180
+ - `require('fs')` in ESM modules (`src/attachments.ts`, `src/self-review.ts`)
181
+ threw `ReferenceError` at runtime: WSL clipboard detection always returned
182
+ `false` and `/self-review` found no sources when run from the built `dist/`.
183
+ - Config, session and undo index writes are now atomic (temp file + rename),
184
+ so a crash or Ctrl+C mid-write can no longer corrupt `~/.zames/config.json`
185
+ or a saved session.
186
+
187
+ ### Changed
188
+
189
+ - Removed the unused `marked` dependency (Markdown is rendered by `markdansi`).
190
+ - CI installs dependencies with `npm ci` for reproducible builds.
191
+ - The i18n test now validates the whole `CATALOG` instead of a small sample.
192
+ - Added `.nvmrc` (Node 24).
193
+
194
+ ## [2.53.0]
195
+
196
+ ### Added
197
+
198
+ - Session banner warns when no saved DeepSeek session exists.
199
+ - `--no-color` flag (explicit `NO_COLOR`).
200
+
201
+ [Unreleased]: https://github.com/Viqto0r/zames_pro/compare/v2.58.0...HEAD
202
+ [2.58.0]: https://github.com/Viqto0r/zames_pro/compare/v2.57.1...v2.58.0
203
+ [2.57.1]: https://github.com/Viqto0r/zames_pro/compare/v2.57.0...v2.57.1
204
+ [2.57.0]: https://github.com/Viqto0r/zames_pro/compare/v2.56.0...v2.57.0
205
+ [2.56.0]: https://github.com/Viqto0r/zames_pro/compare/v2.55.0...v2.56.0
206
+ [2.55.0]: https://github.com/Viqto0r/zames_pro/compare/v2.54.0...v2.55.0
207
+ [2.54.0]: https://github.com/Viqto0r/zames_pro/compare/v2.53.1...v2.54.0
208
+ [2.53.1]: https://github.com/Viqto0r/zames_pro/compare/v2.53.0...v2.53.1
209
+ [2.53.0]: https://github.com/Viqto0r/zames_pro/releases/tag/v2.53.0
package/README.md CHANGED
@@ -1,14 +1,53 @@
1
1
  # zames_pro
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/zames_pro.svg)](https://www.npmjs.com/package/zames_pro)
4
+ [![npm downloads](https://img.shields.io/npm/dm/zames_pro.svg)](https://www.npmjs.com/package/zames_pro)
5
+ [![tests](https://github.com/Viqto0r/zames_pro/actions/workflows/test.yml/badge.svg)](https://github.com/Viqto0r/zames_pro/actions/workflows/test.yml)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg)](package.json)
8
+ [![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md)
9
+
3
10
  ![zames logo](https://raw.githubusercontent.com/Viqto0r/zames_pro/master/logo.jpg)
4
11
 
5
12
  A terminal coding agent that works on top of [chat.deepseek.com](https://chat.deepseek.com/) through Playwright.
6
13
  In spirit it is similar to Claude Code / Codex CLI: it starts in the current
7
14
  directory, reads and edits files, runs commands, and commits to git.
8
15
 
16
+ > No API key required — it drives the DeepSeek web chat like a regular user
17
+ > through a real (headless) browser.
18
+
19
+ ## Features
20
+
21
+ - **Tools like Claude Code / Codex** — `Read`, `Write`, `Edit`, `Bash`,
22
+ `Glob`, `Grep`, plus `MultiEdit`, `ApplyPatch`, `LS`, `TodoWrite`, git and web
23
+ tools. Every edit is backed by `/undo`.
24
+ - **Runs while you keep typing** — queue messages during a task; they run right
25
+ after it, in the same chat (like typing during generation on the web).
26
+ - **Plan mode** — `/plan` (or `--plan`) drops all mutating tools, so the agent
27
+ can investigate the code without touching the tree.
28
+ - **Project context** — reads `AGENTS.md`, `MEMORY.md`, skills (`SKILL.md`) and
29
+ custom commands from the repo and `~/.zames`, the same idea as Codex / Claude
30
+ Code.
31
+ - **MCP support** — plug in external tool servers (e.g. `@playwright/mcp`).
32
+ - **Self-review** — `/self-review` snapshots `src/` so the agent can review and
33
+ fix itself in a sandbox (`/self-fix`, `/self-apply`).
34
+ - **Scheduling** — `/loop`, `/cron` and `/jobs` repeat tasks on a timer.
35
+ - **Bilingual UI** — Russian / English (`/config lang`).
36
+
37
+ ## Why zames?
38
+
39
+ - **No API key, no per-token bill** — it uses your own DeepSeek chat account,
40
+ not the paid API. Good for long, tool-heavy tasks.
41
+ - **Same workflow as Claude Code / Codex** — tools, `AGENTS.md`, skills, MCP
42
+ and slash commands, so it feels familiar from day one.
43
+ - **Runs unattended** — headless by default, resumable sessions, and `/loop`
44
+ plus `/cron` for scheduled work.
45
+ - **Local-first** — the browser profile, credentials and logs never leave your
46
+ machine, and the agent is sandboxed to the project directory.
47
+
9
48
  ## Requirements
10
49
 
11
- - Node.js >= 18
50
+ - Node.js >= 20 (CI and development use Node 24; see `.nvmrc`)
12
51
  - A DeepSeek account. On first launch zames asks for your DeepSeek
13
52
  login/password in the terminal (and stores them in `~/.zames/config.json`
14
53
  after a successful sign-in, so a later logout is handled automatically
@@ -46,6 +85,13 @@ version). You do not need `--headed` just to log in.
46
85
  Credentials and toggles can also be edited from `/config`
47
86
  (`browser.auth.username`, `browser.auth.password`, `browser.auth.saveSession`).
48
87
 
88
+ ## Links
89
+
90
+ - npm: <https://www.npmjs.com/package/zames_pro>
91
+ - Changelog: [`CHANGELOG.md`](CHANGELOG.md)
92
+ - Contributing: [`CONTRIBUTING.md`](CONTRIBUTING.md)
93
+ - Security policy: [`SECURITY.md`](SECURITY.md)
94
+
49
95
  ## Installation
50
96
 
51
97
  ```bash
@@ -90,6 +136,9 @@ The prompt is a small line editor with persistent history:
90
136
  delete the word before the cursor, `Ctrl+←`/`Ctrl+→` — move by words.
91
137
  - `\` + `Enter`, `Ctrl+J`, `Ctrl+Enter` or `Shift+Enter` — insert a newline.
92
138
  - `/` + `Tab` — slash-command hints and completion.
139
+ - `!command` — run a shell command directly, bypassing the model (like
140
+ Claude Code's bash mode). The same sandbox guard as the `Bash` tool applies,
141
+ so a direct command cannot leave the project either.
93
142
 
94
143
  Set `NO_COLOR=1` to disable colors (a calm default palette is used otherwise).
95
144
 
@@ -124,11 +173,30 @@ zames --resend-prompt
124
173
  zames --dir <path>
125
174
  zames --headless
126
175
  zames --headed
176
+ zames --plan
177
+ zames --output-format jsonl
127
178
  zames --debug
128
179
  zames --version
129
180
  zames --help
130
181
  ```
131
182
 
183
+ ### Machine-readable output (scripts / CI)
184
+
185
+ For one-shot runs (`--task`) the agent can emit its event stream as JSON lines
186
+ on stdout, keeping all human text on stderr:
187
+
188
+ ```bash
189
+ zames --task "summarize the diff" --output-format jsonl
190
+ ```
191
+
192
+ Each line is one event (`tool_call`, `tool_result`, `assistant_final`, ...), so
193
+ it pipes straight into `jq`:
194
+
195
+ ```bash
196
+ zames --task "..." --output-format jsonl \
197
+ | jq -r 'select(.type=="assistant_final") | .message'
198
+ ```
199
+
132
200
  ## Tools
133
201
 
134
202
  The agent has the same style of tools as Claude Code / Codex CLI:
@@ -139,7 +207,8 @@ The agent has the same style of tools as Claude Code / Codex CLI:
139
207
  - **Extra tools** — `LS` (list a directory), `MultiEdit` (several edits to one
140
208
  file applied atomically), `TodoWrite` (session task checklist), `ApplyPatch`
141
209
  (multi-file patch in Codex's V4A format: `*** Begin Patch` … `*** End Patch`).
142
- - **Git** — `GitStatus`, `GitDiff`, `GitLog`, `GitAdd`, `GitCommit`, `GitPush`.
210
+ - **Git** — `GitStatus`, `GitDiff`, `GitLog`, `GitShow`, `GitBranchList`,
211
+ `GitAdd`, `GitCommit`, `GitPush`.
143
212
  - **Web** — `WebFetch`, `WebSearch`.
144
213
  - **Service** — `respond` (final answer to the operator, ends the task).
145
214
 
@@ -155,13 +224,18 @@ config commands (/new, /chats, /resume, /cd, /status, /config,
155
224
  Codex CLI:
156
225
 
157
226
  - /diff [--staged] — show the working-tree git diff (--staged for the index).
227
+ - /diffstat — a one-line change summary (`git diff --stat`).
228
+ - /context — show what is loaded into the prompt (AGENTS.md, MEMORY.md,
229
+ skills, custom commands) and the system-prompt size.
230
+ - /retry — resend the last task into the same chat (handy after a truncated
231
+ or empty answer).
232
+ - /rename <title> — set the current session title (shown in /sessions).
233
+ - /copy — copy the last assistant answer to the OS clipboard.
158
234
  - /cost (alias /usage) — session stats: tasks, tool calls, duration, and the
159
235
  context size in tokens (DeepSeek's `accumulated_token_usage`).
160
236
  - /export [file] — write the session transcript to a Markdown file
161
237
  (zames-export-<stamp>.md by default).
162
238
  - /doctor — diagnose node, git, config, browser, clipboard and MCP.
163
- - /permissions — show the confirmation settings (Write/Edit/Bash + the
164
- alwaysConfirm regex list).
165
239
  - /add-dir <path> — validate an extra directory (the sandbox is fixed at
166
240
  startup; relaunch with --dir to write there).
167
241
  - /resume <n> (after /chats) and /resume-id <id> — open a chat and PRINT its
@@ -173,6 +247,10 @@ Codex CLI:
173
247
  instead of the whole list.
174
248
  - /review [focus] [--staged] — ask the agent to review uncommitted changes
175
249
  and report findings (no code changes).
250
+ - /plan [on|off] — plan (read-only) mode. While it is on, the mutating tools
251
+ (Write/Edit/MultiEdit/ApplyPatch/Bash, GitAdd/GitCommit/GitPush) are removed
252
+ from the tool set, so the agent can investigate without touching the tree.
253
+ Start in it with `--plan`.
176
254
  - /compact — ask DeepSeek to compress the current chat into a handover
177
255
  summary, then open a NEW chat, resend the system prompt and post the summary
178
256
  as the carried-over context. Use it when the context gets long.
@@ -207,7 +285,8 @@ workflows from your repository and from `~/.zames`.
207
285
  will explore the project and write an AGENTS.md based on the real build/test
208
286
  commands and conventions (use `/init --force` to overwrite an existing file).
209
287
  - **MEMORY.md** — durable notes that persist between sessions. The agent
210
- appends useful facts here; you can edit it by hand.
288
+ appends useful facts here; you can edit it by hand, or add one from the
289
+ prompt with `/remember <text>`.
211
290
  - **Skills** — a folder with a `SKILL.md` file (YAML frontmatter: `name`,
212
291
  `description`, optional `allowed-tools`, `user-invokable`) plus any helper
213
292
  files. Discovered under `.zames/skills/`, `.claude/skills/`,
@@ -223,6 +302,7 @@ Skills and custom commands show up in the «/» completion list and in `/help`.
223
302
  ```
224
303
  /skills list discovered skills
225
304
  /memory show AGENTS.md / MEMORY.md in effect
305
+ /remember <text> append a durable note to MEMORY.md
226
306
  /init [--force] analyze the project and create AGENTS.md
227
307
  ```
228
308
 
@@ -286,7 +366,6 @@ Examples:
286
366
 
287
367
  ```
288
368
  /config set maxIterations 20
289
- /config set confirmation.bash false
290
369
  /config set browser.deepThinking true # DeepSeek Deep thinking (slow; reasoning is hidden)
291
370
  /config set browser.webSearch false # DeepSeek Smart web search
292
371
  /config lang en
@@ -303,6 +382,34 @@ locale lives in `ui.locale` in the config file.
303
382
 
304
383
  Agent data is stored in `~/.zames`: browser profile, logs, undo history, self-review snapshots.
305
384
 
385
+ ## FAQ
386
+
387
+ **Is this an official DeepSeek product?**
388
+ No. zames drives the public chat.deepseek.com web UI through a real browser,
389
+ like a regular user. It is not affiliated with DeepSeek.
390
+
391
+ **Do I need an API key?**
392
+ No. You sign in with your own DeepSeek account once; the session is stored in
393
+ `~/.zames/profile` and reused.
394
+
395
+ **Which model does it use?**
396
+ Whichever the DeepSeek web chat uses (DeepSeek-V3, or the reasoning model with
397
+ "Deep thinking" on). zames never calls the API directly.
398
+
399
+ **Does it work headless / on a server?**
400
+ Yes — headless is the default, and `--output-format jsonl` makes it scriptable.
401
+ `--headed` is only needed for a manual sign-in or selector debugging.
402
+
403
+ **Is automating the web UI allowed?**
404
+ That depends on DeepSeek's terms; automating a website may violate them. Use at
405
+ your own risk and keep the send throttle (15s by default) so you do not hammer
406
+ the rate limit.
407
+
408
+ **How is it different from Claude Code / Codex?**
409
+ Same shape (tools, `AGENTS.md`, skills, MCP, slash commands) but it runs on your
410
+ DeepSeek account instead of an API, as a browser automation rather than a
411
+ first-party API client.
412
+
306
413
  ## License
307
414
 
308
415
  MIT
@@ -146,6 +146,12 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
146
146
  'Look at the uploaded images in the chat; file copies are in <project>/tmp.)';
147
147
  }
148
148
  transcript?.log('task', { task });
149
+ // Each guard keeps its OWN cap IN ADDITION to the shared budget below.
150
+ // Reason: the shared budget is replenished after every successful tool, so
151
+ // over a long chain of tools a single misbehaving guard could re-ask more
152
+ // than intended; the per-guard cap keeps any one failure mode bounded. The
153
+ // named counters are all reset together after a tool runs (see the reset
154
+ // block after the tool loop).
149
155
  let malformedRetries = 0;
150
156
  const MAX_MALFORMED_RETRIES = 3;
151
157
  let stallRetries = 0;
@@ -190,6 +196,16 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
190
196
  // Retry budget for a browser.ask() TIMEOUT (not for content): the send did
191
197
  // not come back in time. This is separate from unparsedRetries because a
192
198
  // timeout is an infrastructure failure, not a model protocol violation.
199
+ // Loop detection: the model sometimes repeats the SAME tool call with the
200
+ // SAME arguments (e.g. Read the same file) turn after turn. There was no
201
+ // explicit detector for that — the stale-answer guard only catches a
202
+ // repeated ANSWER, not a repeated call. Track the signature of the last
203
+ // batch of calls; after MAX_REPEAT_CALLS identical batches in a row, stop
204
+ // running it and nudge the model to change approach.
205
+ let lastCallSig = '';
206
+ let repeatCallCount = 0;
207
+ const MAX_REPEAT_CALLS = 4;
208
+ let repeatWarned = false;
193
209
  let afterToolRetries = 0;
194
210
  // toolsRanInTask: how many tools actually ran in THIS task. The protocol
195
211
  // guard uses it to tell "started, then slipped into chat mode" (a reasoning
@@ -197,6 +213,10 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
197
213
  // short answer. We key on the STRUCTURE (work already started), not words.
198
214
  let toolsRanInTask = 0;
199
215
  const MAX_AFTER_TOOL_RETRIES = Math.max(0, Math.floor(maxAfterToolRetries));
216
+ // One-time hint when the context nears full while auto-compact is OFF:
217
+ // the operator is told about /compact instead of silently degrading. Set
218
+ // once per task so the terminal is not spammed.
219
+ let contextHintShown = false;
200
220
  // Token count at which the last auto-compact fired. Re-arm only after
201
221
  // the (fresh) chat grows past this plus a margin, so a chat that starts
202
222
  // above the threshold does not compact on every tool call.
@@ -639,6 +659,38 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
639
659
  // message must NOT be delivered before the tools' results are known.
640
660
  // The model will get the tool results and can call respond again.
641
661
  const callsToRun = realCalls.length > 0 ? realCalls : calls;
662
+ // Stable signature of this batch (sorted keys) for loop detection.
663
+ const callSig = callsToRun
664
+ .map((c) => c.tool +
665
+ ':' +
666
+ JSON.stringify(c.args, Object.keys(c.args || {}).sort()))
667
+ .join('|');
668
+ if (callSig && callSig === lastCallSig)
669
+ repeatCallCount++;
670
+ else {
671
+ lastCallSig = callSig;
672
+ repeatCallCount = 1;
673
+ repeatWarned = false;
674
+ }
675
+ if (repeatCallCount >= MAX_REPEAT_CALLS) {
676
+ transcript?.log('tool_loop_detected', {
677
+ sig: callSig.slice(0, 300),
678
+ count: repeatCallCount,
679
+ });
680
+ if (!repeatWarned) {
681
+ repeatWarned = true;
682
+ safeWarning(translate(locale)('msg.tool_loop'));
683
+ }
684
+ // Reset so a single nudge does not carry into the next different call.
685
+ repeatCallCount = 0;
686
+ lastCallSig = '';
687
+ message =
688
+ 'You have called the SAME tool with the SAME arguments several times ' +
689
+ 'in a row. Stop repeating it. Either try a different tool or approach, ' +
690
+ 'or, if the task is done, call respond with the final message. Do NOT ' +
691
+ 'repeat the identical call.';
692
+ continue;
693
+ }
642
694
  for (const call of callsToRun) {
643
695
  const tool = tools.find((t) => t.name === call.tool);
644
696
  if (!tool) {
@@ -784,6 +836,21 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
784
836
  }
785
837
  }
786
838
  }
839
+ else if (getTokenUsage && !contextHintShown) {
840
+ // Auto-compact is OFF (the default). The context can still fill up and
841
+ // silently degrade the answers, so tell the operator ONCE that /compact
842
+ // exists. Same 80% threshold the status line uses to turn red.
843
+ const tokens = getTokenUsage();
844
+ if (tokens !== null && Number.isFinite(tokens) && tokens > 0) {
845
+ const pct = (tokens / contextLimit) * 100;
846
+ if (pct >= 80) {
847
+ contextHintShown = true;
848
+ safeWarning(translate(locale)('context.near_full', {
849
+ pct: String(Math.round(pct)),
850
+ }));
851
+ }
852
+ }
853
+ }
787
854
  }
788
855
  return 'Iteration limit reached.';
789
856
  }
@@ -367,6 +367,38 @@ function tryCommand(bin, args) {
367
367
  return null;
368
368
  }
369
369
  }
370
+ // Write text to the OS clipboard. Mirrors readClipboardImageDetailed: every
371
+ // platform has its own tool, and a failure is returned (not thrown) so the
372
+ // caller can tell the operator which tool was missing.
373
+ export function writeClipboardText(text) {
374
+ const data = Buffer.from(String(text ?? ''), 'utf-8');
375
+ const win = process.platform === 'win32';
376
+ const mac = process.platform === 'darwin';
377
+ const candidates = win
378
+ ? [{ bin: 'clip', args: [] }]
379
+ : mac
380
+ ? [{ bin: 'pbcopy', args: [] }]
381
+ : [
382
+ { bin: 'wl-copy', args: [] },
383
+ { bin: 'xclip', args: ['-selection', 'clipboard'] },
384
+ { bin: 'xsel', args: ['--clipboard', '--input'] },
385
+ ];
386
+ for (const c of candidates) {
387
+ try {
388
+ execFileSync(c.bin, c.args, {
389
+ input: data,
390
+ stdio: ['pipe', 'ignore', 'ignore'],
391
+ timeout: 8000,
392
+ windowsHide: true,
393
+ });
394
+ return { ok: true, via: c.bin };
395
+ }
396
+ catch {
397
+ // try the next tool
398
+ }
399
+ }
400
+ return { ok: false, via: candidates.map((c) => c.bin).join('/') };
401
+ }
370
402
  export async function findFileByName(workdir, name) {
371
403
  const candidate = path.resolve(workdir, name);
372
404
  if (await fs.stat(candidate).catch(() => null))
package/dist/commands.js CHANGED
@@ -2,7 +2,7 @@ import path from 'path';
2
2
  import { isImageName } from './attachments.js';
3
3
  import { translate } from './i18n.js';
4
4
  // Helpers for the extra slash commands (/diff, /cost, /export, /doctor,
5
- // /permissions, /review, /add-dir). Pure functions, unit-tested without a
5
+ // /review, /add-dir). Pure functions, unit-tested without a
6
6
  // live agent/browser. index.ts only renders their output.
7
7
  const NL = String.fromCharCode(10);
8
8
  const CR = String.fromCharCode(13);
@@ -25,6 +25,58 @@ export function formatDiff(diffText, opts = {}, t = translate('en')) {
25
25
  export function diffGitArgs(staged = false) {
26
26
  return staged ? 'git diff --staged' : 'git diff';
27
27
  }
28
+ // ---------- /diffstat ----------
29
+ /** Render `git diff --stat` output (a compact change summary) for display. */
30
+ export function formatDiffStat(statText, opts = {}, t = translate('en')) {
31
+ const maxLines = opts.maxLines ?? 60;
32
+ const trimmed = String(statText ?? '')
33
+ .split(CR + NL)
34
+ .join(NL)
35
+ .trimEnd();
36
+ if (!trimmed || trimmed === '(command produced no output)') {
37
+ return t('diff.no_changes');
38
+ }
39
+ const lines = trimmed.split(NL);
40
+ if (lines.length <= maxLines)
41
+ return trimmed;
42
+ const head = lines.slice(0, maxLines).join(NL);
43
+ return (head + NL + t('diff.more_lines', { n: String(lines.length - maxLines) }));
44
+ }
45
+ // ---------- /context ----------
46
+ /** One renderable line per loaded context source (file + char count). */
47
+ export function formatContextSources(agents, memory, skills, commands, opts = {}, t = translate('en')) {
48
+ const lines = [];
49
+ lines.push(t('context.title'));
50
+ if (typeof opts.systemPromptChars === 'number') {
51
+ lines.push(t('context.system_prompt', { n: String(opts.systemPromptChars) }));
52
+ }
53
+ const fileLine = (p, content) => ' ' + p + ' (' + content.length + ' chars)';
54
+ lines.push(t('context.agents'));
55
+ if (agents.length)
56
+ for (const a of agents)
57
+ lines.push(fileLine(a.path, a.content));
58
+ else
59
+ lines.push(' ' + t('context.none'));
60
+ lines.push(t('context.memory'));
61
+ if (memory.length)
62
+ for (const m of memory)
63
+ lines.push(fileLine(m.path, m.content));
64
+ else
65
+ lines.push(' ' + t('context.none'));
66
+ lines.push(t('context.skills'));
67
+ if (skills.length)
68
+ for (const s of skills)
69
+ lines.push(' ' + s.name + (s.path ? ' ' + s.path : ''));
70
+ else
71
+ lines.push(' ' + t('context.none'));
72
+ lines.push(t('context.commands'));
73
+ if (commands.length)
74
+ for (const c of commands)
75
+ lines.push(' /' + c.name);
76
+ else
77
+ lines.push(' ' + t('context.none'));
78
+ return lines.join(NL);
79
+ }
28
80
  export function summarizeTranscript(entries) {
29
81
  const stats = {
30
82
  turns: 0,
@@ -234,22 +286,6 @@ export function renderDoctor(d, t = translate('en')) {
234
286
  }
235
287
  return t('doctor.title') + NL + rows.join(NL);
236
288
  }
237
- export function renderPermissions(p, t = translate('en')) {
238
- const onoff = (b) => (b ? t('perm.ask') : t('perm.allow'));
239
- const lines = [];
240
- lines.push(t('perm.title'));
241
- lines.push(t('perm.write', { v: onoff(p.write) }));
242
- lines.push(t('perm.edit', { v: onoff(p.edit) }));
243
- lines.push(t('perm.bash', { v: onoff(p.bash) }));
244
- if (p.alwaysConfirm.length) {
245
- lines.push(t('perm.always'));
246
- for (const re of p.alwaysConfirm)
247
- lines.push(' ' + re);
248
- }
249
- lines.push('');
250
- lines.push(t('perm.change_hint'));
251
- return lines.join(NL);
252
- }
253
289
  // ---------- /add-dir ----------
254
290
  export function resolveExtraDir(input, workdir, t = translate('en')) {
255
291
  const raw = String(input ?? '').trim();
@@ -488,9 +524,10 @@ export function hasQueuedJob(queue, jobId) {
488
524
  // True when the queued message is a slash-command. It must NOT be merged into
489
525
  // a task: the main loop has to execute it (e.g. /compact, /new).
490
526
  export function isSlashCommand(text) {
491
- return String(text ?? '')
492
- .trim()
493
- .startsWith('/');
527
+ const s = String(text ?? '').trim();
528
+ // `!command` is also an operator command (run the shell directly) — it
529
+ // must NOT be batched into a task sent to the model.
530
+ return s.startsWith('/') || s.startsWith('!');
494
531
  }
495
532
  export function parseLiveToggle(text) {
496
533
  const m = /^\/(thinking|web|websearch|search)(?:\s+(\S+))?\s*$/i.exec(String(text ?? '').trim());