@zxh126/pi-qqbot 2.4.1 → 2.6.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.en.md +611 -0
- package/{CHANGELOG.md → CHANGELOG.zh-CN.md} +81 -0
- package/LICENSE +21 -21
- package/README.md +22 -9
- package/approver.ts +2 -2
- package/billing.ts +579 -579
- package/constants.ts +2 -0
- package/docs/README.zh-CN.md +19 -9
- package/docs/accounts.md +88 -88
- package/index.ts +131 -20
- package/lock.ts +237 -237
- package/logger.ts +58 -58
- package/package.json +4 -3
- package/progress.ts +176 -176
- package/qq/api.ts +17 -0
- package/qq/auth.ts +35 -35
- package/qq/gateway.ts +20 -3
- package/qq/keyboard.ts +141 -0
- package/queue.ts +120 -120
- package/redact.ts +136 -136
- package/selfupdate.ts +212 -212
- package/slash.ts +67 -5
- package/stream-buffer.ts +22 -22
- package/util/chunk.ts +107 -42
package/CHANGELOG.en.md
ADDED
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to pi-qqbot are documented in this file. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
4
|
+
and versioning follows [SemVer](https://semver.org/spec/v2.0.0.html).
|
|
5
|
+
|
|
6
|
+
> Chinese changelog: [CHANGELOG.zh-CN.md](CHANGELOG.zh-CN.md)
|
|
7
|
+
|
|
8
|
+
## [2.6.2] - 2026-11-01
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **`/qq …` bypassed the command gates**: `/qq` is itself an extension command, so sending `/qq reload`,
|
|
13
|
+
`/qq quit` or `/qq new` landed in the passthrough branch of `routeSlash`, which checks neither
|
|
14
|
+
`commands.allowReload` / `allowQuit` / `allowUpdate` / `allowSession` nor the busy state (`needsIdle`).
|
|
15
|
+
With the defaults (`allowQuit: false`) `/qq quit` from QQ could shut the host pi down. `/qq` is now routed
|
|
16
|
+
*before* passthrough: `bridgeGate()` maps the sub-command back to its built-in name (multi-word sub-commands
|
|
17
|
+
`list tree` / `list fork` / `list resume` are matched as a whole first) and reuses the same
|
|
18
|
+
`allowed()` / `needsIdle` checks.
|
|
19
|
+
- **Dispatch receipts swallowed the argument**: `/qq reload` used to be answered with
|
|
20
|
+
`已派发 /qq 给 pi。` — `parseSlash` only takes the first word as the command name, so it looked like `reload`
|
|
21
|
+
had been ignored. Receipts and logs now echo the whole command (truncated at 80 characters with `…`).
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Destructive bridge sub-commands (`reload` / `new` / `quit` / `clone`) now answer **before** they are
|
|
26
|
+
dispatched, so the receipt cannot lose the race against `ctx.reload()` / `newSession()`.
|
|
27
|
+
- `/qq sendfile …`, `/qq usage …`, `/qq pick …` and unknown sub-commands no longer get an extra generic
|
|
28
|
+
receipt (the bridge answers for itself), which removes one useless message.
|
|
29
|
+
- Cleaned up a leftover `+` line in `slash.ts` from an older hand-patched edit
|
|
30
|
+
(`+reply(formatPicker(picker))` — harmless but dirty).
|
|
31
|
+
|
|
32
|
+
### Compatibility
|
|
33
|
+
|
|
34
|
+
- `/reload`, `/new`, `/quit`, `/compact` and friends behave exactly as before; only `/qq <sub>` is now
|
|
35
|
+
held to the same gates.
|
|
36
|
+
- No config change required: deployments with `allowReload: true` keep working, and `/qq quit` is answered
|
|
37
|
+
with a refusal when `allowQuit` is false (the default).
|
|
38
|
+
|
|
39
|
+
### Tests
|
|
40
|
+
|
|
41
|
+
- New `test/qq-bridge-gate.test.mjs` (21 assertions): sub-command mapping, three gate refusals, busy
|
|
42
|
+
refusal, receipt-before-dispatch ordering, multi-word sub-commands, receipt truncation, `/reload` regression.
|
|
43
|
+
- Full suite: 274 assertions across 22 test files pass; `tsc --noEmit --strict` is clean for the changed
|
|
44
|
+
`slash.ts`.
|
|
45
|
+
|
|
46
|
+
## [2.6.1] - 2026-11-01
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- **Per-language changelogs**: the Chinese changelog is renamed to `CHANGELOG.zh-CN.md` and this English
|
|
51
|
+
`CHANGELOG.en.md` is added. `README.md` links to `CHANGELOG.en.md`, `docs/README.zh-CN.md` links to
|
|
52
|
+
`CHANGELOG.zh-CN.md`.
|
|
53
|
+
|
|
54
|
+
## [2.6.0] - 2026-11-01
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
|
|
58
|
+
- **Inline buttons for approvals and single-choice lists**: approval cards carry ✅ 允许一次 / ❌ 拒绝 /
|
|
59
|
+
🚀 以后都同意 buttons (strict tools show only the first two); the `/tree`, `/fork`, `/resume` and
|
|
60
|
+
model-selection lists carry option buttons (2 columns, at most 5 rows, labels truncated at 10 characters
|
|
61
|
+
while the full list stays in the message body). Buttons are attached only to markdown replies
|
|
62
|
+
(`markdownReply=true`).
|
|
63
|
+
- **`INTERACTION_CREATE` event pipeline**: the gateway subscribes to intent `1<<26` and dispatches
|
|
64
|
+
interaction events; `QQApi.acknowledgeInteraction` replies with `PUT /interactions/{id}` inside the
|
|
65
|
+
5-second window (matched → ACK 0; expired / unauthorized / out-of-range → ACK 3, duplicate operation).
|
|
66
|
+
- **`qq/keyboard.ts`**: keyboard construction plus `button_data` encode/decode, routed by the top-level
|
|
67
|
+
`t` discriminator (`approval` / `pick`).
|
|
68
|
+
|
|
69
|
+
### Changed
|
|
70
|
+
|
|
71
|
+
- `QQApi.sendC2C`'s `SendOptions.keyboard` is only sent with markdown messages; when a markdown send
|
|
72
|
+
fails and falls back to plain text, the keyboard is dropped automatically.
|
|
73
|
+
- `replyTo` / `bridgeReply` gain an optional `keyboard` passthrough. Picker-button clicks reuse the
|
|
74
|
+
"reply with a number" `/qq pick` dispatch path; approval-button clicks reuse `Approver.confirm` /
|
|
75
|
+
`allowAll`. Text replies (同意/拒绝 N, numbers) all remain as fallbacks.
|
|
76
|
+
|
|
77
|
+
### Compatibility
|
|
78
|
+
|
|
79
|
+
- With `markdownReply=false` the behaviour is exactly as before (no keyboard, plain-text replies).
|
|
80
|
+
- Custom buttons require the "invited" enablement on the QQ Open Platform; if it is not granted, the
|
|
81
|
+
buttons do not render and numbered/text replies are unaffected.
|
|
82
|
+
|
|
83
|
+
## [2.5.0] - 2026-11-01
|
|
84
|
+
|
|
85
|
+
### Added
|
|
86
|
+
|
|
87
|
+
- **`markdownReply` takes effect**: set `true` and all replies go through QQ markdown messages
|
|
88
|
+
(`msg_type=2`); GFM tables and code blocks are rendered by the QQ client. Previously this config was
|
|
89
|
+
parsed but never consumed. On send failure (e.g. no markdown permission) `qq/api.ts` automatically
|
|
90
|
+
falls back to plain text — safe by design.
|
|
91
|
+
- **`util/chunk.ts` splitter keeps tables intact**: `splitText` preserves GFM tables as whole blocks
|
|
92
|
+
(table rows are buffered and flushed as a block when a non-table line appears) and never splits at
|
|
93
|
+
code-fence boundaries; long replies no longer show tables cut in half.
|
|
94
|
+
|
|
95
|
+
### Changed
|
|
96
|
+
|
|
97
|
+
- The reply format is driven by `state.cfg.markdownReply`: `replyTo`'s `format` defaults to `auto` and
|
|
98
|
+
applies to final answers, error notices, approval prompts and every receipt; an explicit `"text"`
|
|
99
|
+
still forces plain text.
|
|
100
|
+
|
|
101
|
+
### Compatibility
|
|
102
|
+
|
|
103
|
+
- `markdownReply` still defaults to `false`; behaviour is unchanged when it is disabled.
|
|
104
|
+
|
|
105
|
+
## [2.4.1] - 2026-11-01
|
|
106
|
+
|
|
107
|
+
### Added
|
|
108
|
+
|
|
109
|
+
- **`!` / `!!` get an explicit receipt instead of being silently dropped**: these prefixes (full-width
|
|
110
|
+
`!` / `!!` included) are shell shortcuts only in pi's host TUI / RPC front ends (`interactive-mode`'s
|
|
111
|
+
`text.startsWith("!")`, the RPC `bash` control command); the extension API has no equivalent
|
|
112
|
+
(`pi.sendUserMessage()` does not dispatch them, `pi.exec()` bypasses approval and session history).
|
|
113
|
+
`routeSlash()` now intercepts them first and replies explaining why they cannot run and what to do
|
|
114
|
+
instead (describe the task in words; it then goes through the `bash` tool with a QQ approval first).
|
|
115
|
+
Neither executed nor forwarded to the model as chat.
|
|
116
|
+
- **`/help` gains a line**: `!` / `!!` are not supported.
|
|
117
|
+
|
|
118
|
+
### Changed
|
|
119
|
+
|
|
120
|
+
- **Detection deliberately tightened**: a bang command requires "prefix + non-empty command" (a bare
|
|
121
|
+
`!`, `!!`, `!!!`, `/xxx`, or a sentence-final exclamation such as `你好!` stays ordinary chat), so
|
|
122
|
+
emotional exclamation marks are not swallowed.
|
|
123
|
+
|
|
124
|
+
### Fixed
|
|
125
|
+
|
|
126
|
+
- **`/qq reload` no longer errors with `This extension ctx is stale after session replacement or reload`**
|
|
127
|
+
(same for the numbered picks of `/new`, `/clone`, `/resume`, `/fork`, `/tree`): after `runBridge()`
|
|
128
|
+
the handler still called `syncModelFromCtx()` to read `ctx.model`, while `reload()` / `newSession()` /
|
|
129
|
+
`fork()` / `switchSession()` / `navigateTree()` make pi turn every guard getter of the old
|
|
130
|
+
`ExtensionCommandContext` into a throw (`runner.js`'s `createCommandContext` keeps lazy descriptors on
|
|
131
|
+
purpose). Now a `REPLACES_RUNTIME` set (`reload` / `update` / `new` / `quit` / `clone`) skips the ctx
|
|
132
|
+
teardown entirely when the runtime was replaced; `syncModelFromCtx()` itself is fault-tolerant (it
|
|
133
|
+
backs the picks of tree/fork/resume, whose branches only the runtime knows); and after a bridge error
|
|
134
|
+
the code stops touching ctx. Regression test `test/bridge-stale-ctx.test.mjs` reproduces the original
|
|
135
|
+
stack (`syncModelFromCtx` ← `handler`) with a fake ctx whose every getter throws: 3 cases failed
|
|
136
|
+
before the fix, all 5 pass after.
|
|
137
|
+
|
|
138
|
+
### Compatibility
|
|
139
|
+
|
|
140
|
+
- Pure additions: slash-command routing, approval, queue, progress and credential intake are unaffected.
|
|
141
|
+
- One behaviour change: `!xxx` / `!!xxx` used to be forwarded to the model as ordinary text; they are
|
|
142
|
+
now answered without execution. If you relied on sending `!xxx` to make the model call `bash` itself,
|
|
143
|
+
describe the task normally instead.
|
|
144
|
+
- Messages with attachments still bypass all command routing (including bang detection), as before.
|
|
145
|
+
- `/reload`, `/update`, `/new`, `/clone` still do what they did (the session is really replaced or
|
|
146
|
+
reloaded); they just no longer report an extra error.
|
|
147
|
+
- Tests: new `test/bang-notice.test.mjs` (6 cases) and `test/bridge-stale-ctx.test.mjs` (5 cases); all
|
|
148
|
+
19 test files green under `npm test`.
|
|
149
|
+
- Painless upgrade: config format, command names, approval and queue semantics unchanged; `!` / `!!`
|
|
150
|
+
were already "not executed" — only the silence became an explicit receipt.
|
|
151
|
+
|
|
152
|
+
## [2.4.0] - 2026-11-01
|
|
153
|
+
|
|
154
|
+
### Added
|
|
155
|
+
|
|
156
|
+
- **No more silence when the model runs out of credit or its key is invalid**: on a provider error pi
|
|
157
|
+
only leaves `stopReason: "error"` + `errorMessage` on the assistant message and produces **no body
|
|
158
|
+
text**, so QQ used to see just "no reaction". Now `message_end` classifies the error (`billing` /
|
|
159
|
+
`auth` / `permission` / `rate` / `other`) and `agent_settled` replies with the **current model**
|
|
160
|
+
(`provider/model`), the server's original message and three recovery paths. 401 / 402 / `1311 plan
|
|
161
|
+
not open` all take the "recoverable" branch; errors with body text, user-cancelled runs and 500-class
|
|
162
|
+
errors behave as before.
|
|
163
|
+
- **Remote key replacement**: after running out of credit, sending a bare key (`sk-…` / `ark-…` / `AIza…`
|
|
164
|
+
/ hex / base64 / local-service placeholders like `ollama`, quoted or backticked also accepted) writes
|
|
165
|
+
it into the failing provider in `~/.pi/agent/auth.json` and really probes it once with
|
|
166
|
+
`GET <baseUrl>/models`, replying with a masked fragment and the probe result. **No `sk-` prefix is
|
|
167
|
+
assumed**: the only criteria are "non-empty + printable ASCII + no whitespace"; a bare string (the
|
|
168
|
+
whole message is the key) additionally requires length ≥ 12 with digits / uppercase / separators, so
|
|
169
|
+
ordinary text like `configuration.ts` is not swallowed.
|
|
170
|
+
- **Onboarding a new endpoint from QQ**: send `https://host/v1 <key>` or `endpoint <url> <key> models a,b`
|
|
171
|
+
→ probes `/models` (falls back to `/v1` without a version suffix; 401/403 immediately marks the key
|
|
172
|
+
invalid) → filters out non-chat ids (`embedding` / `rerank` / speech / image generation) → writes
|
|
173
|
+
`models.json` + `pi.registerProvider()` (usable in the running process immediately) → sends a
|
|
174
|
+
**numbered list with context sizes**; replying with a number calls `pi.setModel()` (reuses the
|
|
175
|
+
existing picker, valid 10 minutes). The provider id is auto-derived as `qq-<host>`, numbered on
|
|
176
|
+
collision.
|
|
177
|
+
- **Model parameters align with the built-in catalog**: `/models` usually returns only ids, so
|
|
178
|
+
`borrowModelMeta()` borrows `contextWindow` / `maxTokens` / `reasoning` / `input` / `thinkingLevelMap` /
|
|
179
|
+
`cost` / `compat` / `inputLimits` from `modelRegistry.getAll()` (built-in catalog + configured
|
|
180
|
+
providers) by model id, ordered "same host > same-vendor domain family > `openai-completions`".
|
|
181
|
+
Conservative defaults (128k / 16k) apply only when nothing can be borrowed, and the receipt says how
|
|
182
|
+
many were filled in and how many are defaults.
|
|
183
|
+
- **Self-update from QQ**: `/update` (or 「更新自己」「升级一下插件」「自更新」) → `npm view` for the
|
|
184
|
+
latest version → **`pi update npm:@zxh126/pi-qqbot`** → **version-verification gate** (the on-disk
|
|
185
|
+
`package.json` must equal the registry version, otherwise it only reports and does not reload) →
|
|
186
|
+
`/qq reload` (`ctx.reload()`). Natural-language triggers are deliberately narrow — a bare 「更新」
|
|
187
|
+
is not recognised.
|
|
188
|
+
|
|
189
|
+
### Changed
|
|
190
|
+
|
|
191
|
+
- **Config `commands.allowUpdate`** (default `false`, in the style of `allowReload` / `allowQuit`):
|
|
192
|
+
gates `/update` and 「更新自己」; a top-level default plus per-bot override.
|
|
193
|
+
- **`PickerKind` gains `model`**: numbered selection supports "models of a newly onboarded service";
|
|
194
|
+
picking one calls `pi.setModel()` directly; when the model is no longer in the catalog it says so
|
|
195
|
+
instead of silently proceeding, prompting a fresh `/model`.
|
|
196
|
+
- **Error notices and logs**: `errorMessage` is uniformly run through `redactSecrets()` + whitespace
|
|
197
|
+
collapse + 600-char clip; the error class and current model are written to `pi-qqbot.log`
|
|
198
|
+
(`[reply] 本轮模型接口报错: <class> <provider/model> <original>`), inspectable afterwards.
|
|
199
|
+
|
|
200
|
+
### Compatibility
|
|
201
|
+
|
|
202
|
+
- Pure additions: without `commands.allowUpdate` nothing changes; outside the billing/auth-failure state
|
|
203
|
+
a bare key is not treated as a credential (the explicit `key …` form excepted).
|
|
204
|
+
- Requires pi to provide `message_end`'s `stopReason` / `errorMessage`, `pi.registerProvider()` and
|
|
205
|
+
`pi.setModel()` (all present in pi 1.4.x).
|
|
206
|
+
- Disk writes touch only pi's own `auth.json` / `models.json`, using pi's own `proper-lockfile` +
|
|
207
|
+
atomic rename; `models.json` only adds or edits its own provider, never touching others.
|
|
208
|
+
- The install directory is shared: `/update` only updates the disk; other pi processes still running
|
|
209
|
+
old code must reload or restart on their own.
|
|
210
|
+
|
|
211
|
+
### Tests
|
|
212
|
+
|
|
213
|
+
- **14 suites / 191 cases** all green (`npm test`).
|
|
214
|
+
- New `test/billing.test.mjs` (26): error classification, Chinese copy, `parseCredentialMessage` forms,
|
|
215
|
+
`looksLikeSecret` (including non-`sk-` prefixes, explicit/bare tightness), `stripWrapping`,
|
|
216
|
+
`normalizeBaseUrl` / `extractModelIds`, `probeEndpoint` (success / `/v1` fallback / immediate 401 /
|
|
217
|
+
network error), `providerIdForEndpoint`, `borrowModelMeta`, `filterChatModelIds`, `modelLabel`,
|
|
218
|
+
`updateProviderKey` / `upsertEndpointProvider` persistence.
|
|
219
|
+
- New `test/credential-intake.test.mjs` (11): drives the real `handleMessage` — bare key while broke,
|
|
220
|
+
no swallow when not broke, explicit prefix, endpoint registration + numbered list, metadata
|
|
221
|
+
borrowing, probe 401, resend with `models`, numbered pick `setModel`, non-`sk` key, quoted key.
|
|
222
|
+
- New `test/selfupdate.test.mjs` (13): version comparison (incl. prerelease), npm output parsing, five
|
|
223
|
+
flow outcomes (latest / updated / check failure / update failure / version mismatch),
|
|
224
|
+
`isUpdateSelfIntent`, `/update` gating.
|
|
225
|
+
- New `test/error-notice.test.mjs` (8): 402 / 401 / 500 / with body / cancelled / cleared after a good
|
|
226
|
+
run / credential gate / redaction.
|
|
227
|
+
|
|
228
|
+
## [2.3.0] - 2026-10-31
|
|
229
|
+
|
|
230
|
+
### Added
|
|
231
|
+
|
|
232
|
+
- **Remote-context notice**: in `before_agent_start` it first checks "was this turn dispatched by this
|
|
233
|
+
extension as a QQ message", and only then writes a notice into the system prompt's
|
|
234
|
+
`qq-remote-context` section (before the user message, invisible on both QQ and the TUI): the
|
|
235
|
+
conversation comes from a remote QQ user who cannot see the local screen or dialogs; desktop
|
|
236
|
+
screenshots / screen OCR / UI automation / keyboard-mouse simulation / headed browsers are **off by
|
|
237
|
+
default** (`mcp__screenshot`, `mcp__winui` named explicitly) — render images off-screen to PNG and
|
|
238
|
+
send them back with `qq_send_file`; capturing the desktop once is allowed only to check whether the
|
|
239
|
+
machine is locked. Merely loading the extension or chatting in the local TUI injects nothing — it
|
|
240
|
+
must wait for the first QQ message to actually start a run.
|
|
241
|
+
- **System-prompt rules**: every run **from QQ** appends three short rules (remote user / locked screen
|
|
242
|
+
→ off-screen rendering / say so plainly when human help is needed) so they survive compaction of a
|
|
243
|
+
long session; no stacking within one turn, and local TUI runs get nothing.
|
|
244
|
+
- **Re-injection after compaction**: `session_compact` clears the "noticed" flag so the next message
|
|
245
|
+
injects the notice again.
|
|
246
|
+
- **Config `remote`**: `notice` (default `true`, once per session), `guidelines` (default `true`),
|
|
247
|
+
`extra` (appended to the notice, empty by default); a top-level default plus per-bot override.
|
|
248
|
+
- **Local hand-off correction**: when, within one session, the previous turn came from QQ and the next
|
|
249
|
+
is typed directly in the local TUI, that local turn injects a one-time `qq-local-handoff` section
|
|
250
|
+
("remote QQ user" premise void: screenshots are fine, no `qq_send_file` needed); consecutive local
|
|
251
|
+
turns do not repeat it. Detected via the `input` event's `source=interactive` (this extension's
|
|
252
|
+
`sendUserMessage` is `extension`, so it never misfires); interjecting mid-run of a QQ turn does not
|
|
253
|
+
count as a hand-off (no new run). `/status` gains `来源: QQ(远程)/ 本机(TUI)`.
|
|
254
|
+
|
|
255
|
+
### Fixed
|
|
256
|
+
|
|
257
|
+
- **No automatic progress while an approval is pending**: when a tool approval waits for the user,
|
|
258
|
+
automatic progress pushes are held back (otherwise one line every 15 s would push the approval
|
|
259
|
+
prompt off-screen); they resume on the next interval after the approval is handled (or times out).
|
|
260
|
+
Asking `/progress` yourself still works; `/status` shows 「等审批期间暂缓」. The in-session manual
|
|
261
|
+
pause (`/progress off`) and quota-silence semantics are unchanged.
|
|
262
|
+
|
|
263
|
+
### Tests
|
|
264
|
+
|
|
265
|
+
- New `test/remote-notice.test.mjs` (defaults / copy elements / `extra` append / system-rule length).
|
|
266
|
+
- `test/config.test.mjs` gains `remote` global-default + per-account override cases.
|
|
267
|
+
- `test/accounts.test.mjs` gains 3 cases: **local TUI chat injects nothing**, only the first QQ
|
|
268
|
+
message's run injects and once per session, re-injection after compaction; `remote.notice=false`
|
|
269
|
+
disables only the notice and `remote.extra` appends; `remote.guidelines=false` disables only the
|
|
270
|
+
rules, and repeated triggers within one turn do not stack.
|
|
271
|
+
- `test/progress.test.mjs` gains cases: no push while an approval is pending, `/status` shows the hold,
|
|
272
|
+
asking for progress still answers once, pushes resume after the approval is handled.
|
|
273
|
+
- `test/accounts.test.mjs` gains 2 more cases: the local hand-off corrects exactly once (`/status`
|
|
274
|
+
shows the origin); the pending hand-off is overwritten by a new QQ turn, and an `extension`-source
|
|
275
|
+
input is not a hand-off.
|
|
276
|
+
- `test/remote-notice.test.mjs` gains `qq-local-handoff` tag and copy assertions.
|
|
277
|
+
|
|
278
|
+
## [2.2.1] - 2026-02-11
|
|
279
|
+
|
|
280
|
+
### Fixed
|
|
281
|
+
|
|
282
|
+
- **Copy text was treated as a command**: when a user quoted the whole approval prompt back (or
|
|
283
|
+
discussed it in a sentence like 「你这句话里说『取消自动同意』」), the old **unanchored** regex
|
|
284
|
+
matched 「取消自动同意」 / 「以后都同意」 inside the sentence and acted on it (the log even showed
|
|
285
|
+
a reply of 「当前没有开启自动同意」). `parseReply` now strips whitespace and punctuation and does
|
|
286
|
+
**whole-sentence matching** with a length cap (≤ 12 chars); quoted prompts, long sentences and
|
|
287
|
+
exclamations are never parsed — they go to pi as ordinary chat.
|
|
288
|
+
- **The approval prompt was too wordy**: compressed from 3 lines (with 「停止」/「以后都同意」/
|
|
289
|
+
「取消自动同意」) to 2 lines: `[审批 #N] <summary>` + `回「同意 N」或「拒绝 N」;想全部放行回
|
|
290
|
+
「以后都同意」。`; tools that must be confirmed every time (file sending) use the strict copy
|
|
291
|
+
(no 「以后都同意」).
|
|
292
|
+
|
|
293
|
+
### Tests
|
|
294
|
+
|
|
295
|
+
- New `test/approver.test.mjs` (6 cases: approve/reject variants, stop / always-allow / revoke-always,
|
|
296
|
+
quoted prompt not parsed, mid-sentence keywords not parsed, 「停止汇报」 not a command, overlong
|
|
297
|
+
sentence not parsed, prompt ≤ 2 lines).
|
|
298
|
+
- `test/queue.test.mjs` syncs the strict-copy assertions (19 cases).
|
|
299
|
+
|
|
300
|
+
## [2.2.0] - 2026-02-11
|
|
301
|
+
|
|
302
|
+
### Added
|
|
303
|
+
|
|
304
|
+
- **Sending local files to QQ**: new tool `qq_send_file(path, name?)` and command `/sendfile <path>`.
|
|
305
|
+
Uploads through the official rich-media API in chunks (`upload_prepare` → chunked `PUT` →
|
|
306
|
+
`upload_part_finish` → merge for `file_info`), then sends with `msg_type=7` + `media.file_info` as a
|
|
307
|
+
**passive reply** (`srv_send_msg:false`, no active-quota burn). Local files have no public URL and
|
|
308
|
+
the one-shot binary upload caps at 5 MB, so everything goes through chunking, no relay dependency.
|
|
309
|
+
- **`file_type` mapping**: png/jpg/jpeg → image (1), mp4 → video (2), silk → voice (3), everything
|
|
310
|
+
else → file (4); otherwise QQ answers `850019`. Images/videos/voice display inline; other formats
|
|
311
|
+
arrive as a downloadable file card.
|
|
312
|
+
- **Outbound files are force-gated (asked every time)**: `qq_send_file` is added to
|
|
313
|
+
`FORCE_APPROVAL_TOOLS` — a QQ approval is mandatory no matter what `approve.tools` says, and it
|
|
314
|
+
**skips the session-wide 「以后都同意」 and remembered decisions**; each file is confirmed on its
|
|
315
|
+
own and is never written into remember. The prompt shows the file path and display name, with
|
|
316
|
+
「需单独确认」.
|
|
317
|
+
- **Config `files`**: `enabled` (default true), `maxBytes` (default 30 MB; QQ hard cap 200 MB),
|
|
318
|
+
overridable per bot.
|
|
319
|
+
- **Error-code translation**: `850031` too large, `850019` format unsupported, `40093002` daily
|
|
320
|
+
capacity reached, `850027` timeout, `850018` muted/rate-limited, `40093001` transfer failed — all
|
|
321
|
+
turned into Chinese receipts.
|
|
322
|
+
|
|
323
|
+
### Changed
|
|
324
|
+
|
|
325
|
+
- `qq/api.ts`: new `sendC2CMedia(openid, fileInfo, { msgId })`, reusing the existing send queue and
|
|
326
|
+
`msg_seq` counting.
|
|
327
|
+
- `slash.ts`: new takable command `sendfile` (not gated by `commands.allowSession`; guarded by
|
|
328
|
+
`files.enabled` + approval).
|
|
329
|
+
- `index.ts`: `State` records `restBase`; `/help` gains the `/sendfile` entry.
|
|
330
|
+
- `package.json`: new peer dependency `typebox` (tool-parameter schema; aliased by the pi runtime).
|
|
331
|
+
|
|
332
|
+
### Tests
|
|
333
|
+
|
|
334
|
+
- New `test/upload.test.mjs` (9 cases: `file_type` mapping, md5/sha1/md5_10m, the full chunked
|
|
335
|
+
pipeline with byte reassembly, image type, chunk retry, local validation (missing / directory /
|
|
336
|
+
empty / over limit), merge without `file_info`, error-code translation, `sendC2CMedia`'s
|
|
337
|
+
`msg_type=7` body).
|
|
338
|
+
- Added to `npm test`; the whole suite went 130 → 140 cases green (queue 18 → 19, with the new
|
|
339
|
+
"file sends are always approved" case).
|
|
340
|
+
|
|
341
|
+
## [2.1.0] - 2026-02-11
|
|
342
|
+
|
|
343
|
+
### Added
|
|
344
|
+
|
|
345
|
+
- **Receiving QQ attachments (images / voice / video / documents / archives / plain files)**: incoming
|
|
346
|
+
attachments are downloaded to temp files immediately and their paths are handed to pi as marker
|
|
347
|
+
lines — images as `图片路径 <path>`, the rest by type as `语音路径` / `视频路径` / `文档路径` /
|
|
348
|
+
`压缩包路径` / `文件路径`. The original text stays on the first line, in the same turn as the
|
|
349
|
+
paths; pi's `read` tool can open the image directly and `bash` can process the files. (Before
|
|
350
|
+
2.1.0 only images were kept and never saved to disk; a pure-image message delivered an empty
|
|
351
|
+
string to pi.)
|
|
352
|
+
- **QQ receipt**: after saving, the bot replies 「已收到 N 个附件(图片 ×1、文档 ×1),已存为临时文件
|
|
353
|
+
并交给 pi:」 followed by each path; failed downloads are listed with the reason, and the text part
|
|
354
|
+
still runs.
|
|
355
|
+
- **Automatic temp-file cleanup**: the save directory is split by date
|
|
356
|
+
(`<system temp>/pi-qqbot/<bot>/<YYYY-MM-DD>/`) and swept at session start according to
|
|
357
|
+
`attachments.retentionHours` (default 24 h) and `attachments.maxFiles` (default 200); `<date>`
|
|
358
|
+
subdirectories not created by this extension are never touched.
|
|
359
|
+
- **Config `attachments`**: `enabled` / `dir` / `maxBytes` / `maxPerMessage` / `retentionHours` /
|
|
360
|
+
`maxFiles`, overridable per bot in `qqbots[]` like `queue` / `progress`. With `enabled: false` it
|
|
361
|
+
replies with an ignore notice and processes text only.
|
|
362
|
+
- **New `attachment.ts`**: six-type detection, filename sanitising (only `[0-9a-zA-Z._-]` kept),
|
|
363
|
+
concurrent downloads (default 3), 25 MB per-attachment cap, 30 s download timeout, one retry with
|
|
364
|
+
credentials on 401/403, a failed attachment never blocks the others.
|
|
365
|
+
|
|
366
|
+
### Changed
|
|
367
|
+
|
|
368
|
+
- `qq/gateway.ts`: `QQMessage.attachments` becomes `{ url, contentType, filename?, size? }[]` and
|
|
369
|
+
**no longer drops non-image attachments**; when a voice message body only has `voice_wav_url`, that
|
|
370
|
+
is used as the download URL.
|
|
371
|
+
- The gateway log no longer prints the whole JSON packet (it carries the short-lived `rkey`
|
|
372
|
+
credential); it logs openid / content summary / attachment count / source host instead.
|
|
373
|
+
- `handleMessage()`: messages with attachments **bypass** slash commands, approval parsing, numbered
|
|
374
|
+
selection and built-in commands — always ordinary content (still queued when busy).
|
|
375
|
+
|
|
376
|
+
### Tests
|
|
377
|
+
|
|
378
|
+
- New `test/attachment.test.mjs` (12 cases: six-type detection, extension rules, saved content, over
|
|
379
|
+
limit, 401 retry, partial failure, per-message cap, receipt copy, directory cleanup).
|
|
380
|
+
- `test/queue.test.mjs` 12 → 18 cases (pure-image dispatch, text+document line joining, busy queueing,
|
|
381
|
+
`enabled=false` ignore notice, `/status` bypass, failed download still delivers the text).
|
|
382
|
+
|
|
383
|
+
## [2.0.1] - 2026-10-25
|
|
384
|
+
|
|
385
|
+
### Fixed
|
|
386
|
+
|
|
387
|
+
- **Word order: 「暂停汇报进度」 was not recognised** and leaked to the model as ordinary chat. Both
|
|
388
|
+
orders — 「暂停**进度汇报**」 and 「暂停**汇报进度**」 — are now recognised; trailing particles
|
|
389
|
+
like 「别汇报了**吧**」 are also caught.
|
|
390
|
+
|
|
391
|
+
### Changed
|
|
392
|
+
|
|
393
|
+
- **Colloquial mute words expanded**: 「别汇报了」「别说了」「别讲了」「别推了」「别发了」「别报了」
|
|
394
|
+
「闭嘴」「安静」「打住」「甭」 etc. are equivalent to `/progress off` (resume with 「恢复汇报」 /
|
|
395
|
+
「接着说」).
|
|
396
|
+
- **「停」 now interrupts the task** (per user feedback): `停`/`停下`/`停一下`/`停停`/`停掉`/`别跑了`
|
|
397
|
+
join `RE_CANCEL` and are fully equivalent to `/cancel`; only noun-carrying phrases like 「停止汇报」
|
|
398
|
+
or pure shut-up words mute the reports.
|
|
399
|
+
- **Asking for progress is a command**: 「汇报一下当前进度」「看一下进度」「说说进度」「进度如何」
|
|
400
|
+
「进度怎么样」 are no longer forwarded to pi as chat; while busy they answer once with a summary
|
|
401
|
+
without queueing, while idle they answer 「当前没有正在运行的任务」.
|
|
402
|
+
- `/help` distinguishes the two meanings: to stop only the pushes use `/progress off` / 「别汇报了」 /
|
|
403
|
+
「闭嘴」; 「停」 interrupts the task.
|
|
404
|
+
- Both READMEs gain the colloquial-shortcut table (intent → things to say).
|
|
405
|
+
|
|
406
|
+
### Tests
|
|
407
|
+
|
|
408
|
+
- `test/progress.test.mjs` 16 → 22 cases (new/rewritten: both word orders, colloquial mute words,
|
|
409
|
+
「停/停下/停一下/停止/取消」 all interrupt, 「停止汇报」 still mutes, three poses of asking for
|
|
410
|
+
progress, no false positives).
|
|
411
|
+
|
|
412
|
+
## [2.0.0] - 2026-10-25
|
|
413
|
+
|
|
414
|
+
### Added
|
|
415
|
+
|
|
416
|
+
- **Multiple QQ bots (one config, one machine, several pi processes)**: the config takes
|
|
417
|
+
`qqbots: [...]` with multiple `appId` / `appSecret` pairs, supporting `name` and per-account
|
|
418
|
+
overrides of `sandbox` / `allowlist` / `queue` / `progress` / `approve` / `commands` (unwritten keys
|
|
419
|
+
inherit the global defaults, merged per key).
|
|
420
|
+
- **Automatic claiming**: every pi process, on start (`session_start`), claims the first **offline**
|
|
421
|
+
bot in config order; processes that cannot claim stay off QQ but rescan every 5 seconds and take
|
|
422
|
+
over when someone exits. One lock per process — never concurrent double-connection.
|
|
423
|
+
- **"Never steal a bot temporarily released by a reload"**: on session rebuild (`/reload`, `/new`,
|
|
424
|
+
`/fork`, `/resume`) a 20-second reservation is written **before** releasing the lock, so other
|
|
425
|
+
processes skip that bot; after claiming, the reservation record is re-read and yielded on collision
|
|
426
|
+
(closing the race window). The new runtime reclaims its own bot with the same pid, silently.
|
|
427
|
+
- **Visible identity**: the claim log carries bot name / appId / cwd / pid; the first QQ receipt is
|
|
428
|
+
prefixed with 「(已接入 qqbot「work」)」; `/status`'s first line shows `bot: work (…0143) · cwd: …`.
|
|
429
|
+
- **Takeover notices**: `~/.pi/agent/pi-qqbot-<appId>.owner.json` records the previous holder's pid and
|
|
430
|
+
working directory; on takeover the log writes the old and new pid/cwd and the first QQ receipt
|
|
431
|
+
carries 「(qqbot「work」已由新实例接管 · 当前工作目录 …)」.
|
|
432
|
+
- `PI_QQBOT_ACCOUNT=<name|appId>` pins an account (for debugging or a fixed directory); an unknown
|
|
433
|
+
name means no connection, with a log line.
|
|
434
|
+
- **Credentials are verified right after claiming**: the claim immediately exchanges an
|
|
435
|
+
`access_token`; on failure it **releases the lock** and skips that bot (trying the next one), so a
|
|
436
|
+
bot with a wrong secret can no longer hold the slot while never connecting; when nothing works it
|
|
437
|
+
stops rescanning pointlessly.
|
|
438
|
+
- **Fix** `AuthManager`: the QQ platform answers invalid credentials with HTTP 200 + `code 100016`;
|
|
439
|
+
the old code went on to connect the gateway with an empty token. It now throws immediately (with
|
|
440
|
+
code/message) and is caught during claiming.
|
|
441
|
+
- New doc [docs/accounts.md](docs/accounts.md); new tests `test/config.test.mjs` (9),
|
|
442
|
+
`test/lock.test.mjs` (8, real subprocesses + real lockfile, including "two processes in the same
|
|
443
|
+
working directory each get one bot"), `test/accounts.test.mjs` (9, end-to-end claiming / notices /
|
|
444
|
+
invalid-credential degradation), `test/auth.test.mjs` (3).
|
|
445
|
+
|
|
446
|
+
### Changed
|
|
447
|
+
|
|
448
|
+
- Logs are per account: `~/.pi/agent/pi-qqbot-<name>.log` (several processes on one machine no longer
|
|
449
|
+
interleave); the single-bot v1 format still writes `pi-qqbot.log`.
|
|
450
|
+
- `/status` gains the bot identity and working-directory lines; `/help` gains the multi-bot note.
|
|
451
|
+
- Config loading becomes `loadAccounts()` (returns the account list in order); `loadConfig()` stays as
|
|
452
|
+
a compatibility wrapper.
|
|
453
|
+
|
|
454
|
+
### Compatibility
|
|
455
|
+
|
|
456
|
+
- **Old single-bot configs work unchanged** (equivalent to an account named `default`, behaving like
|
|
457
|
+
1.5.0); `QQ_BOT_APPID` / `QQ_BOT_APPSECRET` still apply to the single-bot scenario only.
|
|
458
|
+
- Writing both the top-level `appId` and `qqbots` → error, no connection (ambiguity is not guessed);
|
|
459
|
+
an account with missing credentials / duplicate `appId` / duplicate `name` is skipped with a log
|
|
460
|
+
line, the rest work normally.
|
|
461
|
+
- **A bot is not bound to a working directory**: allocation depends only on config order and online
|
|
462
|
+
state, not on cwd — several pi processes in the same directory each get their own bot. cwd is only
|
|
463
|
+
used for logs, takeover notices and `/status`.
|
|
464
|
+
- Recommended: restart (or `/reload`) once after the upgrade — per-account log files only appear on a
|
|
465
|
+
fresh start.
|
|
466
|
+
|
|
467
|
+
## [1.5.0] - 2026-10-25
|
|
468
|
+
|
|
469
|
+
### Added
|
|
470
|
+
|
|
471
|
+
- **Pause / resume automatic progress reports (per session)**: `/progress off` (aliases `/p pause`,
|
|
472
|
+
`暂停`, `stop`, 「暂停汇报」「关闭自动汇报」) stops the timer; `/progress on` (aliases `resume`,
|
|
473
|
+
`start`, `恢复`, 「恢复汇报」「继续汇报」) turns it back on.
|
|
474
|
+
- The manual pause survives runs (`agent_start` no longer resets it) and only resets to enabled on
|
|
475
|
+
`session_start` (`/new`, `/reload`, restart).
|
|
476
|
+
- While manually paused, `/progress`, 「进度」, 「当前进度」, 「现在进度」, 「进展」 **answer once
|
|
477
|
+
with the current summary** without secretly re-opening the 15 s timer, and the reply ends with
|
|
478
|
+
「(自动汇报已暂停,发 /progress on 恢复)」.
|
|
479
|
+
- Strictly separate from the existing "quota silence": quota silence is the automatic pause after a
|
|
480
|
+
failed send, cleared by asking once; the manual pause is cleared only by `on` / 「恢复汇报」.
|
|
481
|
+
- `/progress <gibberish>` answers with usage instead of being silently ignored.
|
|
482
|
+
- `/status` gains an `自动汇报:` line distinguishing four modes: `开启(每 Ns)` / `已暂停(本会话…)`
|
|
483
|
+
/ `推送失败已静默` / `未启用(配置 progress.enabled=false)`.
|
|
484
|
+
- Bare-text queries 「当前进度」「现在进度」「目前进展」 (sub-commands allowed, e.g. 「进度 暂停」)
|
|
485
|
+
also route to the progress query.
|
|
486
|
+
- Tests: `test/progress.test.mjs` gains 7 cases (pause/resume immediate effect, survives runs, asking
|
|
487
|
+
while paused does not unpause, Chinese commands without false positives, idle 「当前进度」,
|
|
488
|
+
`/status` copy, unknown subcommand).
|
|
489
|
+
|
|
490
|
+
### Changed
|
|
491
|
+
|
|
492
|
+
- `/progress` (no argument) no longer clears the pause while manually paused (previously any query
|
|
493
|
+
wiped the pause flag).
|
|
494
|
+
- "Failed-send silence" and "user pause" are two separate states (`progressPaused` / `progressMuted`)
|
|
495
|
+
that no longer overwrite each other.
|
|
496
|
+
- README (English) and `docs/README.zh-CN.md` command table, progress section and `progress.enabled`
|
|
497
|
+
notes updated in sync.
|
|
498
|
+
|
|
499
|
+
## [1.4.0] - 2026-10-24
|
|
500
|
+
|
|
501
|
+
### Added
|
|
502
|
+
|
|
503
|
+
- While a task runs, a progress summary is pushed every `progress.intervalMs` (default 15 s): turns,
|
|
504
|
+
tool-call count with the top three tools, failure count, the most recently finished tool (with
|
|
505
|
+
duration), the tool currently running, and token usage. Built purely from local events — no model
|
|
506
|
+
call, no tokens spent.
|
|
507
|
+
- End-to-end secret redaction (`redact.ts`): progress summaries, approval prompts, queue previews and
|
|
508
|
+
log writes all pass through one scrubber covering `Authorization` / QQ bot tokens / `sk-…` /
|
|
509
|
+
`ghp_…` / `npm_…` / `AKIA…` / JWT / PEM / `token=` / `password=` / `--api-key` / URL `user:pass@` /
|
|
510
|
+
high-entropy strings.
|
|
511
|
+
- `/progress` (aliases `/p`, 「进度」) and the busy-time queries 「到哪了」 / 「怎么样了」.
|
|
512
|
+
- A final answer that cannot be delivered (quota) is cached and re-sent with
|
|
513
|
+
「(上一条回复发送失败,补发如下)」 on the next message.
|
|
514
|
+
- Config `progress.enabled` / `progress.intervalMs` (env override `PI_QQBOT_PROGRESS_MS`).
|
|
515
|
+
|
|
516
|
+
## [1.3.3] - 2026-10-24
|
|
517
|
+
|
|
518
|
+
### Fixed
|
|
519
|
+
|
|
520
|
+
- The `/usage` report read-back now polls: `appendEntry` lands after the command's `await` returns,
|
|
521
|
+
which occasionally missed the freshly written report.
|
|
522
|
+
|
|
523
|
+
## [1.3.2] - 2026-10-24
|
|
524
|
+
|
|
525
|
+
### Fixed
|
|
526
|
+
|
|
527
|
+
- `/usage` read-back fallback: when `getEntries` has not refreshed yet, read the tail of the session
|
|
528
|
+
file directly; `generatedAt` tolerates millisecond timestamps.
|
|
529
|
+
|
|
530
|
+
## [1.3.1] - 2026-10-24
|
|
531
|
+
|
|
532
|
+
### Fixed
|
|
533
|
+
|
|
534
|
+
- `/usage rate` now really executes and reads the config file back to confirm, instead of claiming
|
|
535
|
+
success in words only.
|
|
536
|
+
|
|
537
|
+
## [1.3.0] - 2026-10-24
|
|
538
|
+
|
|
539
|
+
### Added
|
|
540
|
+
|
|
541
|
+
- `/usage`, `/cost` (extension commands that only print to `ctx.ui.*`) are bridged back to QQ instead
|
|
542
|
+
of being silent.
|
|
543
|
+
- `/mcp`, `/llama` — commands that need host-TUI selection dialogs or login — answer with an explicit
|
|
544
|
+
explanation instead.
|
|
545
|
+
|
|
546
|
+
## [1.2.1] - 2026-10-24
|
|
547
|
+
|
|
548
|
+
### Changed
|
|
549
|
+
|
|
550
|
+
- Log timestamps use local time with a UTC offset (e.g. `[2026-10-24T07:39:09.762+08:00]`), aligning
|
|
551
|
+
directly with file mtimes and process start times — no timezone arithmetic needed.
|
|
552
|
+
|
|
553
|
+
## [1.2.0] - 2026-10-24
|
|
554
|
+
|
|
555
|
+
### Added
|
|
556
|
+
|
|
557
|
+
- Messages arriving while busy go into a queue and resume automatically after the current task
|
|
558
|
+
settles; `/cancel` is never queued and takes effect immediately.
|
|
559
|
+
- `/queue` (alias `/q`) to view the queue; `/queue clear`, `/queue drop <编号>` to manage it.
|
|
560
|
+
- Config `queue.max` (0 = queueing off) and `queue.ack`.
|
|
561
|
+
|
|
562
|
+
## [1.1.0] - 2026-10-24
|
|
563
|
+
|
|
564
|
+
### Added
|
|
565
|
+
|
|
566
|
+
- Numbered-selection takeover for `/tree`, `/fork`, `/resume`: replies with a list of at most 10
|
|
567
|
+
entries, valid only for the asking user, expiring in 10 minutes.
|
|
568
|
+
- `/clone` support.
|
|
569
|
+
|
|
570
|
+
## [1.0.0] - 2026-10-24
|
|
571
|
+
|
|
572
|
+
### Added
|
|
573
|
+
|
|
574
|
+
- Takes over pi's slash commands (allowlist `commands.passthrough` / `allowReload` / `allowQuit` /
|
|
575
|
+
`allowSession`): `/reload`, `/new`, `/compact`, `/session`, `/name`, `/model`, `/thinking`,
|
|
576
|
+
`/quit`, plus passthrough of extension commands, prompt templates and `/skill:name`.
|
|
577
|
+
- While a task is running, `/reload`, `/new`, `/compact`, `/quit`, `/tree`, `/fork`, `/clone`,
|
|
578
|
+
`/resume` refuse to execute.
|
|
579
|
+
|
|
580
|
+
## [0.3.0] - 2026-10-24
|
|
581
|
+
|
|
582
|
+
### Added
|
|
583
|
+
|
|
584
|
+
- `/cancel` (aliases `/stop`, `/abort`, 「取消」「停止」「中止」「终止」「算了」) interrupts the
|
|
585
|
+
running task: first terminates pending approvals and empties the queue, then `ctx.abort()` and
|
|
586
|
+
drops the half-written reply; an 8-second watchdog releases the busy flag as a fallback.
|
|
587
|
+
|
|
588
|
+
## [0.2.1] - 2026-10-24
|
|
589
|
+
|
|
590
|
+
### Changed
|
|
591
|
+
|
|
592
|
+
- The Chinese README moves into `docs/README.zh-CN.md`, fixing the npm page defaulting to the Chinese
|
|
593
|
+
readme.
|
|
594
|
+
|
|
595
|
+
## [0.2.0] - 2026-10-24
|
|
596
|
+
|
|
597
|
+
### Added
|
|
598
|
+
|
|
599
|
+
- Session-level "always allow" (`approve.remember`), including the request already waiting;
|
|
600
|
+
phrases that only look like cancelling — 「取消自动同意」「恢复审批」 — still go through the
|
|
601
|
+
approval branch.
|
|
602
|
+
- Bilingual README (Chinese and English).
|
|
603
|
+
|
|
604
|
+
## [0.1.0] - 2026-10-16
|
|
605
|
+
|
|
606
|
+
### Added
|
|
607
|
+
|
|
608
|
+
- Initial release: bridges the Pi coding agent into QQ private chats (C2C), single session +
|
|
609
|
+
`proper-lockfile` single-instance lock (host takeover), remote approval before dangerous tools
|
|
610
|
+
(`bash` / `powershell` / `write` / `edit` and `destructiveHint` tools) run, automatic segmentation
|
|
611
|
+
of long replies.
|