pi-information-protecter 0.9.0 → 0.11.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 CHANGED
@@ -1,5 +1,50 @@
1
1
  # Changelog / 更新日志
2
2
 
3
+ ## 0.11.0 — Pi summarizes again, and every hookless path is covered / 摘要交回 Pi,并覆盖所有无钩子路径
4
+
5
+ ### Changed / 变更
6
+
7
+ - Stop summarizing in this extension. Pi hands `session_before_compact` the same `preparation` object it then summarizes, so what Pi is about to summarize is now redacted in place and Pi summarizes it with the session's own model. A single configured model is therefore enough, and routing, credentials, retries and the token budget stay with Pi. Deleted with it: the second model's credential resolution, the stream-function compatibility shim, the local degraded summary and the early-compaction threshold — the most failure-prone code in the extension.
8
+ 本扩展不再生成摘要。Pi 交给 `session_before_compact` 的 `preparation` 正是它随后用于摘要的同一对象,因此现在对 Pi 即将摘要的内容原地脖敏,再由 Pi 用会话自身的模型生成摘要。因此只配一个模型即可,且路由、凭证、重试与 token 预算仍由 Pi 负责。同时删除:第二个模型的凭证解析、流函数兼容层、本地降级摘要与提前压缩阈值——这些是本扩展最易出错的代码。
9
+ - Keep the stored compaction summary redacted instead of restoring it. The session file now holds no plaintext summary, which also keeps it out of `/bug`, `/share` and `/export`. The summary is displayed with its replacements.
10
+ 保存的压缩摘要不再还原。会话文件中不再存在明文摘要,也因此不会进入 `/bug`、`/share` 与 `/export`。摘要以替换值展示。
11
+ - Accept but ignore `compaction.provider`, `compaction.model`, `compaction.contextWindow` and `compaction.reserveTokens`, and report them at startup. Rejecting them would fail the whole configuration closed and block every request of a setup that worked before the upgrade.
12
+ `compaction.provider`、`compaction.model`、`compaction.contextWindow` 与 `compaction.reserveTokens` 仍被接收但不生效,并在启动时提示。直接拒绍会使整份配置失败,并拦住升级前原本可用环境的所有请求。
13
+
14
+ ### Added / 新增
15
+
16
+ - Cover every provider call that has no extension hook. Only the agent loop's requests carry Pi's `onPayload` callback; the `/bug` report summary has no event at all and its command cannot be shadowed, because the interactive editor handles `/bug` before extension dispatch, and nested calls from `codemode` scripts (`models.classify()`, `models.generateImages()`) or other extensions are equally invisible. `session_start` now wraps the call methods of the one model runtime the session shares: requests carrying the payload hook pass through untouched, keeping the synchronous lazy stream the prompt-cache warmer needs, and every other call is redacted first.
17
+ 覆盖所有没有扩展钩子的提供商调用。只有智能体循环的请求携带 Pi 的 `onPayload` 回调;`/bug` 报告摘要没有任何事件,其命令也无法被遮蔽(交互式编辑器在扩展分发之前就处理了 `/bug`),`codemode` 脚本(`models.classify()`、`models.generateImages()`)与其他扩展的嵌套调用同样不可见。`session_start` 现在包装会话共享的那一个模型运行时的调用方法:携带请求钩子的请求原样直通,以保留提示缓存预热所需的同步惰性流,其余调用一律先脖敏。
18
+ - Wrap the classifier and image-model calls only where Pi has them: they joined the model runtime in 0.99, and earlier supported releases have no such provider path, so coverage stays complete there without them. `complete` and `completeSimple` delegate through `stream` and `streamSimple` in every supported release, so wrapping those two covers them too. A regression asserts this surface against Pi's own classes, so a rename fails the suite instead of quietly dropping coverage. Verified by running the full suite against 0.84.4, 0.85.1, 0.86.1, 0.87.1, 0.99.2, 1.2.0 and 1.3.0.
19
+ 分类与图像模型调用仅在 Pi 具备时包装:它们在 0.99 加入模型运行时,更早的受支持版本没有该提供商路径,因此没有它们也仍然覆盖完整。在所有受支持版本中,`complete` 与 `completeSimple` 都经 `stream`、`streamSimple` 转发,因此包装这两个即可连带覆盖。新增回归直接对 Pi 自身的类做断言,重命会导致测试失败而不是静默丢掉覆盖。已在 0.84.4、0.85.1、0.86.1、0.87.1、0.99.2、1.2.0 与 1.3.0 上运行完整测试验证。
20
+ - Add `GUARD_UNAVAILABLE`: the guard wraps methods internal to Pi, so its target is verified at startup and a runtime that cannot be wrapped blocks every request instead of running unprotected. `/protecter status` reports whether the hookless paths are covered.
21
+ 新增 `GUARD_UNAVAILABLE`:该闸门包装的是 Pi 内部方法,因此在启动时校验其目标;无法包装时拦住所有请求,而不在无保护状态下运行。`/protecter status` 会显示无钩子路径是否已覆盖。
22
+ - Add `COMPACT_INSTRUCTIONS`: `/compact <instruction>` is cancelled when the instruction holds a protected value, because Pi reads those instructions from its own state rather than from the event, so a redacted copy would be ignored. `/tree` does read its instructions back, so there they are returned redacted. Automatic compaction has no instructions and is unaffected.
23
+ 新增 `COMPACT_INSTRUCTIONS`:当 `/compact <指令>` 的指令含受保护值时取消,因为 Pi 从自身状态而非事件读取指令,脖敏副本不会被采用。`/tree` 会读回指令,因此在那里返回脖敏后的指令。自动压缩没有指令,不受影响。
24
+ - Document what still cannot be intercepted: `/bug`'s uploaded bundle, `/share` and `/export` send the stored transcript over their own HTTP paths, which no extension API can reach. Only the `/bug` summarization request is covered.
25
+ 记录仍无法拦截的部分:`/bug` 上传的报告包、`/share` 与 `/export` 通过各自的 HTTP 路径发送已保存的会话记录,任何扩展 API 都无法触及。仅 `/bug` 的摘要请求在覆盖范围内。
26
+
27
+ ### Removed / 删除
28
+
29
+ - Remove `COMPACT_UNAVAILABLE`, `COMPACT_EMPTY` and `CONFIG_COMPACT_MODEL`: no credential resolution, empty-summary check or model lookup happens in this extension any more.
30
+ 删除 `COMPACT_UNAVAILABLE`、`COMPACT_EMPTY` 与 `CONFIG_COMPACT_MODEL`:本扩展不再做凭证解析、空摘要判定或模型查找。
31
+
32
+ ## 0.10.0 — Pi 1.x compatibility / 兼容 Pi 1.x
33
+
34
+ ### Added / 新增
35
+
36
+ - Accept Pi 0.99.x and 1.2.0–1.3.x, established by running the full suite, including the real loader and worker integration tests, against 0.87.1, 0.99.2, 1.2.0 and 1.3.0. The runtime gate refused every 0.88+ version, so on current Pi releases the extension failed every request closed with `UNSUPPORTED_PI` while the status bar still showed it as loaded.
37
+ 接收 Pi 0.99.x 与 1.2.0–1.3.x,依据是在 0.87.1、0.99.2、1.2.0 和 1.3.0 上运行完整测试(含真实加载器与 worker 集成测试)。之前运行时门禁拒绍所有 0.88+ 版本,因此在当前 Pi 上每个请求都以 `UNSUPPORTED_PI` 被拦,状态栏却仍显示已加载。
38
+ - Document that nested model calls outside the agent loop are not redacted: a `codemode` script's `models.classify()` or `models.generateImages()`, and any `modelRegistry` call another extension makes, carry no `onPayload` callback. Pi 0.99.0 added `codemode`, MCP and tool search as built-in extensions, so this boundary is new.
39
+ 记录智能体循环之外的嵌套模型调用不会脖敏:`codemode` 脚本的 `models.classify()`、`models.generateImages()` 以及其他扩展的 `modelRegistry` 调用都不携带 `onPayload` 回调。Pi 0.99.0 新增了内置的 `codemode`、MCP 与工具检索扩展,因此这是新的边界。
40
+
41
+ ### Changed / 变更
42
+
43
+ - Refuse Pi 1.0.x and 1.1.0: both depend on `@earendil-works/pi-codemode: ^1.0.4`, which now resolves to 1.3.0, whose missing `MCP_TYPESCRIPT_PREAMBLE` export makes Pi itself throw while loading. This mirrors the existing 0.85.0 exclusion — Pi's own unresolvable import, not an extension defect.
44
+ 拒绝 Pi 1.0.x 与 1.1.0:二者依赖 `@earendil-works/pi-codemode: ^1.0.4`,它如今解析到 1.3.0,而该版本已不导出 `MCP_TYPESCRIPT_PREAMBLE`,导致 Pi 自身在加载时抛错。这与现有的 0.85.0 排除同理:问题出在 Pi 自身无法解析的引用,而非扩展缺陷。
45
+ - Build and verify against Pi 1.3.0 and widen the declared peer range to match the runtime gate exactly.
46
+ 构建与验证基线改为 Pi 1.3.0,并将声明的 peer 范围与运行时门禁保持完全一致。
47
+
3
48
  ## 0.9.0 — A dedicated summarization model / 专用摘要模型
4
49
 
5
50
  ### Added / 新增
package/README.md CHANGED
@@ -10,14 +10,14 @@ A Pi extension that inspects outgoing LLM request bodies locally, replaces confi
10
10
 
11
11
  ## Installation
12
12
 
13
- Requires Node.js 22+ and `@earendil-works/pi-coding-agent` **0.84.0–0.84.x or 0.85.1–0.87.x**. Every accepted release was verified by running the full suite, including the real loader and worker integration tests. 0.83.0 and earlier lack `registerMarkdownTransformer`, and 0.85.0 imports `@earendil-works/pi-server`, which no Pi release declares, so both fail to load. Legacy `@mariozechner/*` releases are not supported.
13
+ Requires Node.js 22+ and `@earendil-works/pi-coding-agent` **0.84.0–0.84.x, 0.85.1–0.87.x, 0.99.x or 1.2.0–1.3.x**. Every accepted release was verified by running the full suite, including the real loader and worker integration tests. 0.83.0 and earlier lack `registerMarkdownTransformer`; 0.85.0 imports `@earendil-works/pi-server`, which no Pi release declares; and 1.0.x and 1.1.0 resolve `@earendil-works/pi-codemode` to a version that no longer exports `MCP_TYPESCRIPT_PREAMBLE`, so all of them fail to load. Legacy `@mariozechner/*` releases are not supported.
14
14
 
15
15
  ```bash
16
16
  # Install the latest npm release.
17
17
  pi install npm:pi-information-protecter
18
18
 
19
19
  # Pin a version only when you have a reason to; older releases miss fixes.
20
- pi install npm:pi-information-protecter@0.7.0
20
+ pi install npm:pi-information-protecter@0.11.0
21
21
 
22
22
  # Alternatively, build and install from this repository without copying it.
23
23
  npm ci
@@ -133,24 +133,33 @@ Replacements are checked so restoration stays exact: a candidate is rejected if
133
133
 
134
134
  All notifications, prompts and diagnostics render in a single language, chosen from the environment: `PI_PROTECTER_LANG`, then `LC_ALL`, `LC_MESSAGES`, `LANG`, `LANGUAGE`, then the runtime locale. `zh*` selects Simplified Chinese; anything else falls back to English. Set `PI_PROTECTER_LANG=en` or `PI_PROTECTER_LANG=zh` to override. Error codes, stages and numeric details stay language-neutral so they can be searched and reported.
135
135
 
136
- ### Context compaction
136
+ ### Context compaction and every hookless path
137
137
 
138
- Pi builds the summarization request inside its own compaction code and passes it straight to the provider. That request carries no `onPayload` callback, so **`before_provider_request` never fires for it** and this extension cannot redact it. Letting Pi summarize would send the stored conversation, which holds restored plaintext, to the provider in full.
138
+ Only the agent loop's own requests carry Pi's `onPayload` callback, and that callback is what fires `before_provider_request`. Pi builds the requests below from scratch, with no callback and, for most of them, no extension event at all:
139
139
 
140
- Instead the extension produces the summary itself:
140
+ | Path | Pi's own hook | How it is covered |
141
+ | --- | --- | --- |
142
+ | Agent requests, prompt-cache warming | `onPayload` | `before_provider_request` |
143
+ | Context compaction, split-turn prefix summaries | `session_before_compact` | The `preparation` Pi is about to summarize is redacted in place |
144
+ | `/tree` branch summaries | `session_before_tree` | `entriesToSummarize` and the instructions are redacted in place |
145
+ | `/bug` report summary | none | Model-runtime guard |
146
+ | `codemode` `models.classify()`, `models.generateImages()` | none | Model-runtime guard |
147
+ | Nested model calls by other extensions | none | Model-runtime guard |
148
+
149
+ **Compaction.** Pi hands the hook the same `preparation` object it then summarizes, so the extension redacts that object in place and returns no summary of its own. Pi summarizes the redacted text with the session's own model, which means **one configured model is enough** and Pi keeps routing, credentials, retries and the token budget. The stored summary keeps the replacements, so the session file holds no plaintext summary and the next outgoing request needs no further work. Any failure cancels compaction instead of falling through to Pi summarizing stored plaintext, and reports `COMPACT_FAILED`.
141
150
 
142
- 1. `preparation.messagesToSummarize`, the previous summary and any custom instructions are redacted through the same bounded worker as an outgoing request.
143
- 2. The summary is generated from the redacted text.
144
- 3. The summary is restored locally before it is stored, exactly like assistant text; the next outgoing request redacts it again.
151
+ `/compact <instruction>` is the one exception: Pi reads those instructions from its own state, not from the event, so an instruction containing a protected value cannot be redacted. That compaction is cancelled with `COMPACT_INSTRUCTIONS`; run `/compact` without the protected value. Automatic compaction has no instructions and is unaffected.
145
152
 
146
- Any failure cancels compaction rather than falling back to Pi's unredacted path, and reports `COMPACT_UNAVAILABLE` (no model or no resolved credentials) or `COMPACT_FAILED`.
153
+ **The model-runtime guard.** The remaining paths have no event to hook, and `/bug` cannot even be shadowed by an extension command because the interactive editor handles it before extension dispatch. They all reach the provider through the one model runtime the session shares, so `session_start` wraps its call methods: a request that carries Pi's payload hook is passed through untouched (it is already covered, and its caller needs the synchronous lazy stream), and every other call is redacted first. Redaction failure cancels the call. If the runtime cannot be wrapped, startup fails with `GUARD_UNAVAILABLE` and every request is blocked rather than silently unprotected — `/protecter status` shows whether the hookless paths are covered.
154
+
155
+ The guard wraps `stream` and `streamSimple`, which `complete` and `completeSimple` delegate through, plus `classify` and `generateImages` where Pi has them (they joined the runtime in 0.99; earlier supported releases have no such provider path). A regression asserts this surface against Pi's own classes, so a rename fails the test suite instead of quietly dropping coverage.
147
156
 
148
157
  Set `compaction` in the local configuration:
149
158
 
150
159
  | Value | Behaviour |
151
160
  | --- | --- |
152
- | `ask` (default) | Confirm each compaction, then summarize redacted text |
153
- | `protected` | Summarize redacted text without asking |
161
+ | `ask` (default) | Confirm each compaction, then redact what Pi summarizes |
162
+ | `protected` | Redact without asking |
154
163
  | `off` | Always refuse; use `/new` for long sessions |
155
164
 
156
165
  ```json
@@ -163,31 +172,9 @@ Set `compaction` in the local configuration:
163
172
 
164
173
  The prompt offers allow once, allow for this session, deny once and deny for this session. A session choice lasts until reload or exit; `compaction` in the configuration is the permanent setting. Dismissing the dialog denies that compaction.
165
174
 
166
- ### A dedicated summarization model
175
+ `compaction.provider`, `compaction.model`, `compaction.contextWindow` and `compaction.reserveTokens` selected a separate summarization model before 0.11.0. They are still accepted so an upgrade cannot block a working setup, but they do nothing and are reported at startup; remove them.
167
176
 
168
- Summarizing sends the whole conversation, redacted, to a second destination. Point it at a local or otherwise trusted endpoint:
169
-
170
- ```json
171
- {
172
- "version": 1,
173
- "compaction": {
174
- "mode": "protected",
175
- "provider": "A6000-vLLM",
176
- "model": "Qwen38-27B-FP8",
177
- "contextWindow": 262144,
178
- "reserveTokens": 16384
179
- },
180
- "sensitiveWords": []
181
- }
182
- ```
183
-
184
- Both `contextWindow` and `reserveTokens` are inherited and only needed as overrides: the window comes from Pi's model catalogue, and the reserve from Pi's own `compaction.reserveTokens` setting. Set the window for a private endpoint Pi does not know, and the reserve only when the summarization model needs a different margin. A minimal configuration is therefore just `mode`, `provider` and `model`. A configured model that cannot be resolved is an error, reported at startup rather than when the context fills up, and compaction never falls back to the main model, which would send the conversation somewhere you did not choose.
185
-
186
- **The threshold follows the summarization model, not the main one.** Pi compacts when the main model's window is nearly full; with a 1M main model that is around 983k tokens, which a 256k summarizer cannot read. The extension therefore compacts earlier, at the summarization model's window minus `reserveTokens`. `/protecter status` shows the target and the effective threshold.
187
-
188
- The conversation is redacted before it reaches this model as well, so a trusted endpoint improves reliability, cost and privacy, but the summary still describes replacements rather than the original values.
189
-
190
- Two costs are unavoidable: the extra summarization request consumes tokens, and the model reasons about replacements, so the summary can describe protected values inaccurately.
177
+ The model still reasons about replacements, so a summary can describe protected values inaccurately, and the stored summary shows replacements rather than the original values.
191
178
 
192
179
  Summarized `/tree` navigation takes the same internal Pi path and is protected the same way, using the same setting and prompt. Navigation without a summary sends nothing and is never gated.
193
180
 
@@ -257,11 +244,11 @@ Local response and tool-argument restoration
257
244
  ## Limitations — read first
258
245
 
259
246
  - Only rule-matching text is protected, not paraphrases, split characters or arbitrary encodings. Common image/audio/video/file blocks reject the entire request because SPI cannot be reliably inspected; no OCR is provided.
260
- - Compaction and summarized `/tree` navigation summarize redacted text locally, because Pi builds those requests internally and never routes them through `before_provider_request`. Navigation without summaries sends nothing. See [Context compaction](#context-compaction).
247
+ - Compaction and summarized `/tree` navigation redact what Pi is about to summarize, in place, because Pi builds those requests internally and never routes them through `before_provider_request`; Pi then summarizes the redacted text with the session's own model. Navigation without summaries sends nothing. Pi's other hookless model calls, the `/bug` summary and nested `codemode` or extension calls, are covered by the model-runtime guard. See [Context compaction and every hookless path](#context-compaction-and-every-hookless-path).
261
248
  - Exit, reload and session switching discard mappings. Final restored local messages remain readable; crash leftovers, old summaries and raw deltas cannot recover tokens across restarts.
262
249
  - Broad rules such as `.` or all digits may alter protocol fields, model names, tool schemas or IDs and break requests. Matching numbers become token strings. Prefer precise rules.
263
250
  - Reused tokens reveal equality relationships; context may imply identity. This is not formal anonymization.
264
- - Hooks do not cover HTTP headers, authentication keys, gateway endpoints, Pi telemetry, `/share`, independent extension SDK/fetch calls or tool networking. The gateway still receives its authentication credentials.
251
+ - Model calls are covered; transfers that are not model calls are not. `/bug`'s uploaded bundle, `/share` and `/export` send the stored transcript over their own HTTP paths, which no extension API can intercept, and the transcript holds restored plaintext. Hooks also do not cover HTTP headers, authentication keys, gateway endpoints, Pi telemetry, an extension's own `fetch`, or tool networking, including arguments handed to MCP servers. The gateway still receives its authentication credentials. Treat `/bug`, `/share` and `/export` as plaintext disclosure and review what they contain first.
265
252
  - **Load after other payload rewriters and trust every installed extension.** Later handlers can reintroduce originals. Extensions have local permissions, and Pi may continue if this extension fails to load. Check status before entering secrets.
266
253
  - Local sessions, terminals, exports and files written by tools may contain plaintext. They are not encrypted or cleaned by this project.
267
254
 
@@ -290,14 +277,14 @@ API references: [Pi Extensions](https://pi.dev/docs/latest/extensions), local 0.
290
277
 
291
278
  ## 安装
292
279
 
293
- 要求 Node.js 22+ 和 `@earendil-works/pi-coding-agent` **0.84.0–0.84.x 或 0.85.1–0.87.x**。每个受支持版本都经过完整测试验证(含真实加载器与 worker 集成测试)。0.83.0 及更早缺少 `registerMarkdownTransformer`,0.85.0 引用了任何 Pi 版本都未声明的 `@earendil-works/pi-server`,两者均无法加载。不支持旧 `@mariozechner/*` 版本。
280
+ 要求 Node.js 22+ 和 `@earendil-works/pi-coding-agent` **0.84.0–0.84.x、0.85.1–0.87.x、0.99.x 或 1.2.0–1.3.x**。每个受支持版本都经过完整测试验证(含真实加载器与 worker 集成测试)。0.83.0 及更早缺少 `registerMarkdownTransformer`;0.85.0 引用了任何 Pi 版本都未声明的 `@earendil-works/pi-server`;1.0.x 与 1.1.0 将 `@earendil-works/pi-codemode` 解析到已不再导出 `MCP_TYPESCRIPT_PREAMBLE` 的版本,三者均无法加载。不支持旧 `@mariozechner/*` 版本。
294
281
 
295
282
  ```bash
296
283
  # 安装最新 npm 版本。
297
284
  pi install npm:pi-information-protecter
298
285
 
299
286
  # 确有需要时才固定版本;旧版本缺少后续修复。
300
- pi install npm:pi-information-protecter@0.7.0
287
+ pi install npm:pi-information-protecter@0.11.0
301
288
 
302
289
  # 或在本仓库构建后本地安装,不复制仓库。
303
290
  npm ci
@@ -415,24 +402,33 @@ pi update npm:pi-information-protecter
415
402
 
416
403
  所有通知、提示和诊断仅以**单一语言**呈现,语言按环境变量依次选取:`PI_PROTECTER_LANG`、`LC_ALL`、`LC_MESSAGES`、`LANG`、`LANGUAGE`,最后是运行时区域。`zh*` 选择简体中文,其余回退英文。可设置 `PI_PROTECTER_LANG=en` 或 `PI_PROTECTER_LANG=zh` 覆盖。错误码、阶段和数值细节保持语言无关,便于搜索和反馈。
417
404
 
418
- ### 上下文压缩
405
+ ### 上下文压缩与所有无钩子路径
419
406
 
420
- Pi 在自己的压缩代码中构造摘要请求并直接发给提供商。该请求不携带 `onPayload` 回调,因此 **`before_provider_request` 根本不会触发**,本扩展无法对其脱敏。直接交给 Pi 生成摘要,等于把存有还原明文的会话内容完整发给提供商。
407
+ 只有智能体循环自身的请求携带 Pi 的 `onPayload` 回调,而正是该回调触发 `before_provider_request`。下列请求由 Pi 从零构造,不带该回调,其中大多数甚至没有任何扩展事件:
421
408
 
422
- 因此改由扩展自行生成摘要:
409
+ | 路径 | Pi 自身的钩子 | 覆盖方式 |
410
+ | --- | --- | --- |
411
+ | 智能体请求、提示缓存预热 | `onPayload` | `before_provider_request` |
412
+ | 上下文压缩、split-turn 前缀摘要 | `session_before_compact` | 对 Pi 即将摘要的 `preparation` 原地脱敏 |
413
+ | `/tree` 分支摘要 | `session_before_tree` | 对 `entriesToSummarize` 与指令原地脱敏 |
414
+ | `/bug` 报告摘要 | 无 | 模型运行时闸门 |
415
+ | `codemode` 的 `models.classify()`、`models.generateImages()` | 无 | 模型运行时闸门 |
416
+ | 其他扩展发起的嵌套模型调用 | 无 | 模型运行时闸门 |
423
417
 
424
- 1. 将 `preparation.messagesToSummarize`、历史摘要和自定义指令,通过与出站请求相同的限时 worker 脱敏。
425
- 2. 基于脱敏后的文本生成摘要。
426
- 3. 摘要在保存前于本地还原,与助手文本一致;下一次出站请求会再次脱敏。
418
+ **压缩。** Pi 交给钩子的 `preparation` 就是它随后用于摘要的同一对象,因此本扩展对该对象原地脱敏,并不返回自己生成的摘要。Pi 用会话自身的模型对脱敏后的文本生成摘要,这意味着**只配一个模型就够了**,且路由、凭证、重试与 token 预算仍由 Pi 负责。保存下来的摘要保留替换值,因此会话文件中不存在明文摘要,下一次出站请求也无需额外处理。任何失败都会取消压缩,而不回退到 Pi 对已存明文生成摘要,并上报 `COMPACT_FAILED`。
427
419
 
428
- 任何失败都取消压缩,而不回退到 Pi 未脱敏的路径,并报出 `COMPACT_UNAVAILABLE`(无可用模型或无法获取凭证)或 `COMPACT_FAILED`。
420
+ 唯一例外是 `/compact <指令>`:Pi 从自身状态而非事件读取该指令,因此含受保护值的指令无法脱敏。这类压缩会以 `COMPACT_INSTRUCTIONS` 取消;请不带受保护值执行 `/compact`。自动压缩没有指令,不受影响。
421
+
422
+ **模型运行时闸门。** 其余路径没有可挂的事件,且 `/bug` 连扩展命令都无法遮蔽——交互式编辑器在扩展分发之前就处理了它。它们都经过会话共享的那一个模型运行时发往提供商,因此 `session_start` 会包装其调用方法:携带 Pi 请求钩子的调用原样直通(它已被覆盖,且调用方需要同步的惰性流),其余调用一律先脱敏。脱敏失败即取消该调用。若运行时无法被包装,启动将以 `GUARD_UNAVAILABLE` 失败并拦截所有请求,而不是在无保护的状态下继续——`/protecter status` 会显示无钩子路径是否已覆盖。
423
+
424
+ 闸门包装 `stream` 与 `streamSimple`(`complete`、`completeSimple` 经由二者转发),并在 Pi 具备时一并包装 `classify` 与 `generateImages`(它们在 0.99 加入运行时,更早的受支持版本没有该提供商路径)。新增回归直接对 Pi 自身的类做断言,因此重命名会导致测试失败,而不是静默丢掉覆盖。
429
425
 
430
426
  在本地配置中设置 `compaction`:
431
427
 
432
428
  | 取值 | 行为 |
433
429
  | --- | --- |
434
- | `ask`(默认) | 每次压缩先确认,再对脱敏文本生成摘要 |
435
- | `protected` | 不询问,直接对脱敏文本生成摘要 |
430
+ | `ask`(默认) | 每次压缩先确认,再对 Pi 要摘要的内容脱敏 |
431
+ | `protected` | 不询问,直接脱敏 |
436
432
  | `off` | 始终拒绝;长会话请使用 `/new` |
437
433
 
438
434
  ```json
@@ -445,34 +441,11 @@ Pi 在自己的压缩代码中构造摘要请求并直接发给提供商。该
445
441
 
446
442
  弹窗提供四个选项:允许一次、本会话内全部允许、拒绝一次、本会话内全部拒绝。会话级选择在重载或退出前有效;配置中的 `compaction` 才是永久设置。关闭对话框视为拒绝本次压缩。
447
443
 
448
- ### 专用摘要模型
449
-
450
- 生成摘要会把脱敏后的整段对话发往第二个目的地。请指向本地或其他可信端点:
451
-
452
- ```json
453
- {
454
- "version": 1,
455
- "compaction": {
456
- "mode": "protected",
457
- "provider": "A6000-vLLM",
458
- "model": "Qwen38-27B-FP8",
459
- "contextWindow": 262144,
460
- "reserveTokens": 16384
461
- },
462
- "sensitiveWords": []
463
- }
464
- ```
465
-
466
- `contextWindow` 与 `reserveTokens` 都会继承,仅在需要覆盖时填写:窗口取自 Pi 的模型目录,预留值取自 Pi 自身的 `compaction.reserveTokens` 设置。Pi 未收录的私有端点需填写窗口;只有当摘要模型需要不同余量时才填预留值。因此最小配置只需 `mode`、`provider`、`model`。配置了却无法解析的模型属于错误,会在启动时报出而非等到上下文写满;压缩也不会回退到主模型——那会把对话发往你未选择的地方。
467
-
468
- **阈值跟随摘要模型,而非主模型。** Pi 在主模型窗口将满时压缩,1M 主模型约为 98.3 万 token,而 256k 的摘要模型读不下。因此扩展会提前压缩,阈值为摘要模型窗口减去 `reserveTokens`。`/protecter status` 会显示目标模型与生效阈值。
469
-
470
- 发往该模型的内容同样先经脱敏,因此可信端点改善的是可靠性、成本与隐私,摘要描述的仍是替换值而非原始值。
471
-
472
- 有两项代价无法避免:额外的摘要请求会消耗 token;模型推理的是替换值,摘要对受保护值的描述可能不准确。
444
+ `compaction.provider`、`compaction.model`、`compaction.contextWindow` 与 `compaction.reserveTokens` 在 0.11.0 之前用于选择独立的摘要模型。它们仍被接收,以免升级后拦住可用环境,但已不起任何作用并会在启动时提示,请删除。
473
445
 
474
- 带摘要的 `/tree` 导航走同一条内部路径,采用相同保护方式,并共用同一设置与弹窗。不生成摘要的导航不发送任何内容,也不会被拦截。
446
+ 模型推理的仍是替换值,因此摘要对受保护值的描述可能不准确,保存下来的摘要显示的也是替换值而非原值。
475
447
 
448
+ 带摘要的 `/tree` 导航走 Pi 同一条内部路径,以相同方式保护,并共用同一设置与弹窗。不带摘要的导航不发送任何内容,也不会被询问。
476
449
  ### 多轮一致性与提示词缓存
477
450
 
478
451
  随机占位符按精确原文首次生成,在同一扩展实例内持续复用,包括本地回复还原后的再次脱敏;新建扫描 worker 不会重置映射。这有助于前缀稳定,但不保证提供商 KV Cache 命中,模型、工具、消息顺序、缓存有效期和路由也有影响。重载、进程重启和会话实例替换会清除映射;不同进程不共享映射,审计时间和请求 ID 不进入模型请求体。
@@ -539,11 +512,11 @@ before_provider_request:最终 JSON 文本扫描
539
512
  ## 当前限制——请先阅读
540
513
 
541
514
  - 仅保护命中规则的文本,不覆盖改写、拆字和任意编码。常见多模态附件因无法可靠审查而拒绝整个请求,不提供 OCR。
542
- - 压缩与带摘要的 `/tree` 导航均改为在本地对脱敏文本生成摘要,因为 Pi 在内部构造这些请求,不经过 `before_provider_request`。不带摘要的导航不发送任何内容。参见[上下文压缩](#上下文压缩)。
515
+ - 压缩与带摘要的 `/tree` 导航改为对 Pi 即将摘要的内容原地脱敏(Pi 在内部构造这些请求,不经过 `before_provider_request`),随后由 Pi 用会话自身的模型对脱敏文本生成摘要。不带摘要的导航不发送任何内容。Pi 其余无钩子的模型调用(`/bug` 摘要、`codemode` 与其他扩展的嵌套调用)由模型运行时闸门覆盖。参见[上下文压缩与所有无钩子路径](#上下文压缩与所有无钩子路径)。
543
516
  - 退出、重载和切换会话会丢弃映射。最终已还原消息仍可读,但崩溃残留、旧摘要和原始增量无法跨重启恢复占位符。
544
517
  - 过宽规则可能改写协议字段、模型名、工具结构和 ID,导致请求失败;命中数字变为占位符字符串,应优先使用精确规则。
545
518
  - 复用占位符会暴露值相等的关系,上下文也可能揭示身份,这不是形式化匿名化。
546
- - 钩子不覆盖认证头、API key、网关地址、遥测、分享、扩展独立网络调用和工具联网;网关仍收到其认证凭证。
519
+ - 模型调用已覆盖,但非模型调用的传输不在覆盖范围:`/bug` 上传的报告包、`/share` 与 `/export` 通过各自的 HTTP 路径发送已保存的会话记录,任何扩展 API 都无法拦截,而会话记录中保存的是已还原的明文。钩子同样不覆盖认证头、API key、网关地址、遥测、扩展自身的 `fetch`,以及工具联网(含交给 MCP 服务器的参数);网关仍收到其认证凭证。请把 `/bug`、`/share`、`/export` 视为明文披露,使用前先检查其内容。
547
520
  - **在其他请求改写扩展之后加载,并只使用可信扩展。** 后续处理器可重新加入原文;扩展有本机权限,插件加载失败时 Pi 可能继续运行,请先检查状态再输入秘密。
548
521
  - 本地会话、终端、导出和工具写出的文件可能包含明文,本项目不加密或清理它们。
549
522
 
package/SECURITY.md CHANGED
@@ -12,8 +12,8 @@ Prevent configured personal information from accidentally entering Pi LLM reques
12
12
  不提供操作系统沙箱,不防御同 UID 程序、恶意扩展、任意代码执行、调试器或内存检查。
13
13
  - No protection against arbitrary encoding, splitting, translation or screenshot/OCR reconstruction; no automatic privacy classification or formal anonymization.
14
14
  不防御任意编码、拆分、翻译和截图/OCR 重构,不提供自动隐私分类或形式化匿名化。
15
- - No coverage for gateway authentication headers/endpoints, tool requests, independent SDK calls, telemetry, session exports or `/share`.
16
- 不覆盖网关认证头与地址、工具请求、独立 SDK 调用、遥测、会话导出或分享。
15
+ - No coverage for gateway authentication headers/endpoints, tool requests, an extension's own `fetch`, telemetry, session exports, `/share` or the `/bug` bundle. Model calls are covered, including Pi's own summarizers and nested calls; transfers of the stored transcript are not, and no extension API can intercept them.
16
+ 不覆盖网关认证头与地址、工具请求、扩展自身的 `fetch`、遥测、会话导出、分享或 `/bug` 报告包。模型调用(含 Pi 自身的摘要器与嵌套调用)已覆盖;已保存会话记录的传输不在覆盖范围,且任何扩展 API 都无法拦截。
17
17
  - No local configuration/session/terminal encryption, nor automatic prevention of exfiltration after a model infers a secret.
18
18
  不加密本地配置、会话和终端,也不会在模型推断出秘密后自动阻止外传。
19
19
 
@@ -33,8 +33,10 @@ Prevent configured personal information from accidentally entering Pi LLM reques
33
33
  拦截诊断仅包含目录错误码、阶段、非负整数定位和白名单系统错误码。规则序号和节点数只是位置,不是内容。不输出原始异常消息、请求文本、命中原文、替换值、配置内容和文件路径,worker 边界同样适用,因此诊断可安全分享;错误码有意保持粗粒度,精确定位规则仍可能需要本地查看。
34
34
  - Limit worker regex scans to 2 seconds, with resource and input limits. Errors exclude original configuration, rules and request text.
35
35
  worker 正则扫描限制为 2 秒,并限制资源和输入;错误不包含原始配置、规则或请求文本。
36
- - Pi 0.84.0–0.87.x catches `before_provider_request` errors and continues with the current payload. Error paths therefore return `{}` and attempt cancellation. This does not guarantee zero HTTP requests; it prevents this handler from falling back to the original payload.
37
- Pi 0.84.0–0.87.x 会捕获该钩子异常并继续使用当前请求体,因此错误路径返回空对象并尝试取消。这不保证没有 HTTP 请求,只保证本处理器不回退原始请求体。
36
+ - Every supported Pi release, 0.84.0–0.87.x, 0.99.x and 1.2.0–1.3.x, catches `before_provider_request` errors and continues with the current payload. Error paths therefore return `{}` and attempt cancellation. This does not guarantee zero HTTP requests; it prevents this handler from falling back to the original payload.
37
+ 所有受支持的 Pi 版本(0.84.0–0.87.x、0.99.x 与 1.2.0–1.3.x)都会捕获该钩子异常并继续使用当前请求体,因此错误路径返回空对象并尝试取消。这不保证没有 HTTP 请求,只保证本处理器不回退原始请求体。
38
+ - Only the agent's own model requests carry the `onPayload` callback that fires `before_provider_request`. Every other provider call reaches the one `ModelRuntime` the session shares, so `session_start` wraps its `stream`, `streamSimple`, `classify` and `generateImages` methods: calls carrying the payload hook pass through untouched, because they are already redacted and their callers need the synchronous lazy stream, and every other call is redacted first, with a redaction failure cancelling it. This covers Pi's `/bug` summary, which has no extension event and whose command cannot be shadowed, plus nested calls from `codemode` scripts and other extensions. It wraps methods that are internal to Pi, so it is verified at startup and a runtime that cannot be wrapped fails closed with `GUARD_UNAVAILABLE` instead of running unprotected. MCP servers are external programs that receive tool arguments directly, which this does not change.
39
+ 只有智能体自身的模型请求会携带本扩展挂接的 `onPayload` 回调。循环之外的嵌套模型调用(如 `codemode` 脚本的 `models.classify()`、`models.generateImages()`、其他扩展的 `modelRegistry` 调用或任何直接 SDK 调用)不经脖敏即发出。Pi 0.99.0 将 `codemode`、MCP 与工具检索内置为扩展;`codemode` 仅在 `defaultTools` 或 `--tools` 启用时生效,MCP 服务器则是直接接收工具参数的外部程序。高风险环境下应将二者保持关闭。
38
40
  - Create configuration separately with Unix mode 0600. Reject links, non-regular files, multiple links and files owned by others. Users remain responsible for directories and OS permissions; TOCTOU races are not prevented.
39
41
  独立创建配置,Unix 权限为 0600;拒绝链接、非普通文件、多链接及他人所有文件。用户仍需维护目录和系统权限,无法防止检查与使用之间的竞态。
40
42
  - Direct-access blocking is supplementary. File tools inspect target paths rather than document bodies. Shell can bypass string checks, and command mentions or ancestor directory checks can still cause false positives.
@@ -43,10 +45,12 @@ Prevent configured personal information from accidentally entering Pi LLM reques
43
45
  启动或重载时校验并合并机器哈希配置后缓存,请求只使用内存;文件删除不撤销缓存秘密,重载或退出才清理实例。迁移采用先提交后删除及协作进程锁,并非多文件事务或恶意本地写入防护;残留锁需本地检查,Windows 无 POSIX 目录同步。
44
46
  - Whole-configuration text detection does not guarantee that every reformatted rule definition is masked.
45
47
  整体配置文本检测不保证所有重新格式化的规则定义都被遮盖。
46
- - Compaction summaries are generated by this extension from redacted text, because Pi builds the summarization request internally and passes no `onPayload` callback, so `before_provider_request` never fires for it. Letting Pi summarize would send the stored conversation, which holds restored plaintext, to the provider in full. Any failure cancels compaction instead of falling back to that path. The summary still reaches the provider as redacted text, and the model reasons about replacements, so it may describe protected values inaccurately.
47
- 压缩摘要由本扩展基于脱敏文本生成,因为 Pi 在内部构造摘要请求且不传递 `onPayload` 回调,`before_provider_request` 对其不会触发。直接交给 Pi 生成摘要会把存有还原明文的会话内容完整发给提供商。任何失败均取消压缩,而不回退到该路径。摘要仍以脱敏文本到达提供商,且模型推理的是替换值,对受保护值的描述可能不准确。
48
- - Summarized `/tree` navigation takes the same internal path and is produced the same way, from redacted entries. Do not widen Pi compatibility without verification: refusing a working release blocks every request, and accepting a broken one can send plaintext. Each supported version is established by running the full suite against it, and the runtime gate and declared peer range are kept identical.
49
- 带摘要的 `/tree` 导航走同一条内部路径,同样基于脱敏条目生成。未经验证不扩大 Pi 兼容范围:误拒可用版本会拦截所有请求,误受损坏版本则可能发送明文。每个受支持版本均以完整测试验证,并保持运行时门禁与声明的 peer 范围一致。
48
+ - Compaction, split-turn prefix summaries and `/tree` branch summaries are built by Pi, which passes no `onPayload` callback, so `before_provider_request` never fires for them. Pi does hand the hook the same `preparation` object it then summarizes, so it is redacted in place and Pi summarizes the redacted text with the session's own model; routing, credentials, retries and the token budget stay with Pi. The stored summary keeps the replacements, so no plaintext summary is written to the session. Any failure cancels compaction instead of letting Pi summarize stored plaintext. `/compact <instruction>` is cancelled when the instruction holds a protected value, because Pi reads it from its own state and a redacted copy would be ignored.
49
+ 压缩、split-turn 前缀摘要与 `/tree` 分支摘要都由 Pi 构造,且不传 `onPayload` 回调,`before_provider_request` 对其不会触发。但 Pi 交给钩子的 `preparation` 正是它随后用于摘要的同一对象,因此对其原地脱敏,再由 Pi 用会话自身的模型对脱敏文本生成摘要;路由、凭证、重试与 token 预算仍由 Pi 负责。保存的摘要保留替换值,因此不会向会话写入明文摘要。任何失败都取消压缩,而不让 Pi 对已存明文生成摘要。当 `/compact <指令>` 的指令含受保护值时取消,因为 Pi 从自身状态读取该指令,脱敏副本不会被采用。
50
+ - Transfers that are not model calls are outside this boundary: `/bug`'s uploaded bundle, `/share` and `/export` send the stored transcript over their own HTTP paths, which no extension API can intercept, and that transcript holds restored plaintext. Only the `/bug` summarization request is covered. Review what these produce before sending them.
51
+ 非模型调用的传输不在此边界内:`/bug` 上传的报告包、`/share` 与 `/export` 通过各自的 HTTP 路径发送已保存的会话记录,任何扩展 API 都无法拦截,而该记录中保存的是已还原的明文。仅 `/bug` 的摘要请求在覆盖范围内。发送前请先检查这些产物的内容。
52
+ - Summarized `/tree` navigation takes the same internal path and is redacted the same way; that event does read its instructions back, so they are returned redacted rather than cancelling. Do not widen Pi compatibility without verification: refusing a working release blocks every request, and accepting a broken one can send plaintext. Each supported version is established by running the full suite against it, and the runtime gate and declared peer range are kept identical.
53
+ 带摘要的 `/tree` 导航走同一条内部路径,以同样方式脱敏;该事件会读回指令,因此返回脱敏后的指令而非取消。未经验证不扩大 Pi 兼容范围:误拒可用版本会拦截所有请求,误受损坏版本则可能发送明文。每个受支持版本均以完整测试验证,并保持运行时门禁与声明的 peer 范围一致。
50
54
 
51
55
  ## Fixed aliases and caching / 固定别名与缓存
52
56
 
package/dist/index.mjs CHANGED
@@ -1,25 +1,24 @@
1
1
  /*! SPI Protecter | MIT | see LICENSE / SPI 隐私保护插件 | MIT 许可 */
2
- import{getAgentDir as St,VERSION as _t}from"@earendil-works/pi-coding-agent";import{Worker as mt}from"node:worker_threads";import{fileURLToPath as pt}from"node:url";import*as I from"node:fs";import{join as P,resolve as ot}from"node:path";import{createHash as nt,randomUUID as rt}from"node:crypto";import{Worker as it}from"node:worker_threads";import{fileURLToPath as st}from"node:url";var J="0.84.0-0.84.x, 0.85.1-0.87.x";function se(o){let e=/^(\d+)\.(\d+)\.(\d+)/.exec(o);if(!e)return!1;let[n,t,r]=e.slice(1).map(Number);return n!==0?!1:t===84?!0:t===85?r>=1:t===86||t===87}var Y={INTERNAL:["Unexpected local failure","Reload; check the installed package integrity"],NOT_READY:["Protection is not initialized","Run /protecter status for the initialization error, fix it, then /reload"],UNSUPPORTED_PI:["Unsupported Pi version",`Use Pi ${J}`],MACHINE_ID:["Machine identifier unavailable","Check local OS machine-ID access; no random fallback is used"],CONFIG_JSON:["Invalid configuration JSON","Fix JSON syntax locally, then /reload"],CONFIG_SCHEMA:["Invalid configuration structure or rule","Use version 1, a sensitiveWords array and documented fields; rule shows the 1-based index"],CONFIG_REGEX:["Invalid regex or flags","Fix the numbered rule; flags support gimsu only"],CONFIG_TARGET:["Invalid fixed replacement target","Use a non-blank string under 8192 characters without reserved __PIP_ names"],CONFIG_CONFLICT:["Conflicting rule definitions","Give the same matcher one replacement policy across all configs"],CONFIG_SIZE:["Configuration exceeds size or rule limit","Reduce configuration size or rule count; rules are never truncated"],CONFIG_IO:["Cannot access configuration","Check local permissions, ownership and free space"],CONFIG_UNSAFE:["Unsafe configuration file","Use an owned regular file without symbolic or hard links"],CONFIG_EMPTY_MATCH:["Regex matches an empty string","Require a non-empty match in the numbered rule"],CONFIG_SENSITIVE_TARGET:["Fixed target overlaps a sensitive rule","Choose a target unrelated to every sensitive matcher"],CONFIG_ALIAS_CONFLICT:["Fixed aliases overlap each other","Use distinct aliases that are not substrings of one another"],CONFIG_TIMEOUT:["Rule validation timed out","Simplify regex rules, then /reload"],MIGRATION_LOCKED:["Configuration migration lock is busy","Wait for other Pi processes; inspect a stale lock only after stopping them"],MIGRATION_CHANGED:["Configuration changed during migration","Stop concurrent edits and /reload; source files are preserved"],MIGRATION_LIMIT:["Too many configuration candidates","Consolidate old configuration files locally"],MIGRATION_IO:["Migration file operation failed","Check permissions, free space and leftover files before retrying"],PAYLOAD_JSON:["Request is not serializable JSON","Inspect the provider or another payload-modifying extension; no payload is logged"],PAYLOAD_SIZE:["Request exceeds the size limit","Reduce history or tool output size, or start a new session"],SCAN_TIMEOUT:["Redaction worker timed out","Simplify regex rules or reduce input size"],WORKER_FAILED:["Worker failed to start or exited early","Verify all dist files and the Node version; reinstall if needed"],SCAN_INTERNAL:["Unexpected scan failure","Review recent rule changes and retry with smaller input"],SCAN_NUMBER_UNSAFE:["Replacement would turn a numeric JSON value into an invalid number","Narrow the rule so it does not match inside numeric fields such as timestamps or schema limits"],SCAN_UNIQUE_FAILED:["Could not generate a unique shape-preserving replacement","The matched value is too short; match a longer value or set an explicit replacement"],SCAN_ZERO_WIDTH:["Regex produced a zero-width match","Require a non-empty match in the numbered rule"],SCAN_MATCH_LIMIT:["Too many matches in one request","Narrow overly broad rules or reduce input"],SCAN_MAPPING_LIMIT:["Mapping limit reached","Start a new session or /reload; in-memory mappings reset"],FIXED_MAPPING_CONFLICT:["One original has conflicting replacements","Use one replacement for overlapping matchers, then /reload"],FIXED_ALIAS_REUSED:["Fixed alias already maps to another original","A fixed regex represents one original per instance; use random replacement or split literal rules"],FIXED_SENSITIVE_TARGET:["Replacement contains the matched original","Choose a target without sensitive content"],FIXED_OVERLAP:["Fixed replacement partially overlaps another match","Remove the partial overlap or use random rules; rule and otherRule show both rules"],FIXED_ALIAS_CROSSING:["Regex crosses a learned fixed alias","Narrow the regex or choose a different alias"],SCAN_COMPLEXITY:["Request exceeds depth or node limits","Reduce nested data and tool schema size"],SCAN_JSON_REWRITE:["Replacement breaks embedded JSON syntax","Avoid rules spanning JSON syntax; match field values instead"],SCAN_ATTACHMENT:["Unsupported image, audio, video or file content","Remove attachments or send reviewed text only"],SCAN_KEY_COLLISION:["Replacement creates duplicate object keys","Use distinct targets or avoid matching schema keys"],RESTORE_COMPLEXITY:["Response nesting is too deep to restore","Reduce nested structures or start a new session with /new"],RESTORE_KEY_COLLISION:["Restoring produced duplicate object keys","Use more specific rules or fixed targets that cannot collide with schema keys"],COMPACT_DENIED:["Compaction refused",'Allow it when asked, or set "compaction" to "protected" in the local config'],COMPACT_UNAVAILABLE:["No model or credentials available to summarize","Select a model and sign in to its provider, then retry"],COMPACT_FAILED:["Could not summarize the redacted conversation",'Retry; if it persists, set "compaction" to "off" and use /new'],COMPACT_EMPTY:["The model returned an empty summary","A local summary was used instead; no action is required"],CONFIG_COMPACTION:["Invalid compaction setting",'Use "ask", "protected" or "off", or an object with mode, provider, model, contextWindow and reserveTokens'],CONFIG_COMPACT_MODEL:["The compaction model cannot be resolved","Set both provider and model, check their spelling, and sign in to that provider"],AUDIT_BATCH_LIMIT:["Audit batch exceeds the size limit","Reduce matched content per request"],AUDIT_FULL:["Audit log would exceed the size limit","Review records, then run /protecter logs clear or archive locally"],AUDIT_PARTIAL:["Audit log has an incomplete last line","Inspect locally or run /protecter logs clear; never upload the log"],AUDIT_LOCKED:["Audit lock stayed busy","Wait for writers; stop all Pi processes before inspecting a stale lock"],AUDIT_UNSAFE:["Unsafe audit file","Use an owned regular file without links; checks are not bypassed"],AUDIT_IO:["Audit file operation failed","Check errno, permissions and free space; no path or content is exposed"]},$e={INTERNAL:["未预期的本地错误","重载并检查安装包完整性"],NOT_READY:["防护尚未初始化","查看初始化错误,修复后重载"],UNSUPPORTED_PI:["Pi 版本不受支持",`请使用 Pi ${J}`],MACHINE_ID:["无法获取机器标识","检查系统机器标识访问权限,不使用随机回退"],CONFIG_JSON:["配置 JSON 无效","本地修复 JSON 语法后重载"],CONFIG_SCHEMA:["配置结构或规则无效","使用版本1、敏感词数组及文档字段,rule 为规则序号"],CONFIG_REGEX:["正则或标志无效","修复指定规则,标志仅支持 gimsu"],CONFIG_TARGET:["固定替换目标无效","使用非空且少于8192字符的目标,不含保留名称"],CONFIG_CONFLICT:["规则定义冲突","为同一匹配器设置一致的替换策略"],CONFIG_SIZE:["配置超过体积或规则数上限","减少配置体积或规则数,规则不会被截断"],CONFIG_IO:["无法访问配置","检查本地权限、所有者和磁盘空间"],CONFIG_UNSAFE:["配置文件不安全","使用本人所有的普通文件,不含符号或硬链接"],CONFIG_EMPTY_MATCH:["正则匹配空字符串","修改指定规则,要求非空匹配"],CONFIG_SENSITIVE_TARGET:["固定目标与敏感规则重叠","选择与所有敏感匹配器无关的目标"],CONFIG_ALIAS_CONFLICT:["固定别名相互重叠","使用互不包含的不同别名"],CONFIG_TIMEOUT:["规则验证超时","简化正则规则后重载"],MIGRATION_LOCKED:["配置迁移锁被占用","等待其他进程,全部停止后再检查残留锁"],MIGRATION_CHANGED:["迁移期间配置发生变化","停止并发编辑后重载,源文件会保留"],MIGRATION_LIMIT:["候选配置过多","在本地整理旧配置文件"],MIGRATION_IO:["迁移文件操作失败","检查权限、空间和残留文件后重试"],PAYLOAD_JSON:["请求无法序列化为 JSON","检查提供商或其他请求改写扩展,不记录请求内容"],PAYLOAD_SIZE:["请求超过体积上限","减少历史或工具输出,或新建会话"],SCAN_TIMEOUT:["脱敏 worker 超时","简化正则规则或缩小输入"],WORKER_FAILED:["worker 启动失败或提前退出","检查 dist 文件完整性和 Node 版本,必要时重装"],SCAN_INTERNAL:["未预期的扫描错误","检查最近的规则改动并缩小输入"],SCAN_NUMBER_UNSAFE:["替换会把 JSON 数值变成无效数字","缩小规则范围,避免匹配时间戳或结构上限等数值字段"],SCAN_UNIQUE_FAILED:["无法生成唯一的同形替换值","命中内容过短或可选空间过小,请匹配更长内容或设置固定 replacement"],SCAN_ZERO_WIDTH:["正则产生零宽匹配","修改指定规则,要求非空匹配"],SCAN_MATCH_LIMIT:["单次请求匹配次数过多","缩小过宽规则或减少输入"],SCAN_MAPPING_LIMIT:["映射数量达到上限","新建会话或重载,内存映射将重置"],FIXED_MAPPING_CONFLICT:["同一原文存在冲突替换","为重叠匹配器使用同一目标后重载"],FIXED_ALIAS_REUSED:["固定别名已映射其他原文","固定正则在同一实例仅代表一种原文,改用随机替换或拆分字面规则"],FIXED_SENSITIVE_TARGET:["替换目标包含命中原文","选择不含敏感内容的目标"],FIXED_OVERLAP:["固定替换与其他匹配部分重叠","消除部分重叠或改用随机规则,rule 与 otherRule 指出两条规则"],FIXED_ALIAS_CROSSING:["正则跨越已学习的固定别名","缩小正则范围或更换别名"],SCAN_COMPLEXITY:["请求超过层级或节点上限","减少嵌套数据和工具结构体积"],SCAN_JSON_REWRITE:["替换破坏内嵌 JSON 语法","避免跨 JSON 语法的规则,改为匹配字段值"],SCAN_ATTACHMENT:["不支持图片、音频、视频或文件内容","移除附件或仅发送已审查的文本"],SCAN_KEY_COLLISION:["替换后对象键重复","使用不同目标或避免匹配结构键"],RESTORE_COMPLEXITY:["还原响应时层级过深","减少嵌套结构,或使用 /new 开始新会话"],RESTORE_KEY_COLLISION:["还原后对象键重复","使用更具体的规则或固定替换目标,避免与结构键冲突"],COMPACT_DENIED:["压缩被拒绝",'询问时选择允许,或在本地配置中将 "compaction" 设为 "protected"'],COMPACT_UNAVAILABLE:["没有可用于生成摘要的模型或凭证","选择模型并登录对应提供商后重试"],COMPACT_FAILED:["无法对脱敏后的会话生成摘要",'请重试;若持续失败,将 "compaction" 设为 "off" 并使用 /new'],COMPACT_EMPTY:["模型返回了空摘要","已改用本地摘要,无需处理"],CONFIG_COMPACTION:["compaction 配置值无效",'请使用 "ask"、"protected"、"off",或含 mode、provider、model、contextWindow、reserveTokens 的对象'],CONFIG_COMPACT_MODEL:["无法解析压缩模型","请同时填写 provider 和 model、核对拼写,并登录该提供商"],AUDIT_BATCH_LIMIT:["审计批次超过上限","减少单个请求的命中内容"],AUDIT_FULL:["审计日志将超过上限","检查记录后清空日志或本地归档"],AUDIT_PARTIAL:["审计日志末行不完整","本地检查或清空日志,不要上传日志"],AUDIT_LOCKED:["审计锁持续被占用","等待写入完成,全部停止后再检查残留锁"],AUDIT_UNSAFE:["审计文件不安全","使用本人普通文件且不含链接,不会绕过检查"],AUDIT_IO:["审计文件操作失败","检查错误码、权限和磁盘空间,不暴露路径或内容"]},ae={blockedHeader:"SPI Protecter blocked this request",reasonLabel:"Reason",actionLabel:"Action",detailsLabel:"Details",statusReady:"ready",statusNotReady:"not ready",statusLine:"SPI Protecter: {state}; {rules}; {mappings}. Memory snapshot; /reload to refresh.",statusLastBlock:"Last block @ {at}:",statusNoBlock:"No recorded block in this session.",statusBarRules:"SPI Protecter · {rules}",statusBarNotReady:"SPI Protecter · not ready",emptyRules:"SPI Protecter: no rules configured; edit the local machine config, then /reload.",commandDescription:"View local SPI protection records",usage:"Usage: /protecter [logs [1-100|clear]|status|reload]. Never type secrets as arguments.",logsTuiOnly:"Open the local TUI to view protection records.",logsNone:"No readable records.",logsSelectTitle:"Protection records",logsTruncated:" (limited tail)",logsRevealTitle:"Sensitive plaintext",logsRevealBody:"Reveal locally? Never share this view.",logsPreviewTitle:"Local preview; edits are discarded",logsPreviewTruncated:"[Preview truncated]",clearTitle:"Clear the current audit log?",clearBody:"Irreversible. Only this machine's log is cleared; configuration and in-memory mappings stay. Later requests may add new records.",cleared:"Audit log cleared: {bytes}.",toolBlocked:"SPI Protecter blocked tool access to the protected configuration or audit log; edit it locally.",compactAskTitle:"Compact this conversation?",compactAskBody:`Pi builds the summarization request internally and never routes it through this extension's redaction, so allowing Pi to summarize directly would send the conversation, including protected values, as plaintext.
2
+ import{getAgentDir as wt,VERSION as St}from"@earendil-works/pi-coding-agent";import{Worker as mt}from"node:worker_threads";import{fileURLToPath as pt}from"node:url";import*as I from"node:fs";import{join as T,resolve as nt}from"node:path";import{createHash as ot,randomUUID as rt}from"node:crypto";import{Worker as it}from"node:worker_threads";import{fileURLToPath as st}from"node:url";var R="0.84.0-0.84.x, 0.85.1-0.87.x, 0.99.x, 1.2.0-1.3.x";function se(t){let e=/^(\d+)\.(\d+)\.(\d+)/.exec(t);if(!e)return!1;let[o,n,r]=e.slice(1).map(Number);return o===1?n===2||n===3:o!==0?!1:n===84?!0:n===85?r>=1:n===86||n===87||n===99}var $={INTERNAL:["Unexpected local failure","Reload; check the installed package integrity"],NOT_READY:["Protection is not initialized","Run /protecter status for the initialization error, fix it, then /reload"],UNSUPPORTED_PI:["Unsupported Pi version",`Use Pi ${R}`],GUARD_UNAVAILABLE:["Cannot cover the model calls that have no extension hook","This Pi build exposes no model runtime to wrap; use Pi "+R+" or set a trusted Pi version"],MACHINE_ID:["Machine identifier unavailable","Check local OS machine-ID access; no random fallback is used"],CONFIG_JSON:["Invalid configuration JSON","Fix JSON syntax locally, then /reload"],CONFIG_SCHEMA:["Invalid configuration structure or rule","Use version 1, a sensitiveWords array and documented fields; rule shows the 1-based index"],CONFIG_REGEX:["Invalid regex or flags","Fix the numbered rule; flags support gimsu only"],CONFIG_TARGET:["Invalid fixed replacement target","Use a non-blank string under 8192 characters without reserved __PIP_ names"],CONFIG_CONFLICT:["Conflicting rule definitions","Give the same matcher one replacement policy across all configs"],CONFIG_SIZE:["Configuration exceeds size or rule limit","Reduce configuration size or rule count; rules are never truncated"],CONFIG_IO:["Cannot access configuration","Check local permissions, ownership and free space"],CONFIG_UNSAFE:["Unsafe configuration file","Use an owned regular file without symbolic or hard links"],CONFIG_EMPTY_MATCH:["Regex matches an empty string","Require a non-empty match in the numbered rule"],CONFIG_SENSITIVE_TARGET:["Fixed target overlaps a sensitive rule","Choose a target unrelated to every sensitive matcher"],CONFIG_ALIAS_CONFLICT:["Fixed aliases overlap each other","Use distinct aliases that are not substrings of one another"],CONFIG_TIMEOUT:["Rule validation timed out","Simplify regex rules, then /reload"],MIGRATION_LOCKED:["Configuration migration lock is busy","Wait for other Pi processes; inspect a stale lock only after stopping them"],MIGRATION_CHANGED:["Configuration changed during migration","Stop concurrent edits and /reload; source files are preserved"],MIGRATION_LIMIT:["Too many configuration candidates","Consolidate old configuration files locally"],MIGRATION_IO:["Migration file operation failed","Check permissions, free space and leftover files before retrying"],PAYLOAD_JSON:["Request is not serializable JSON","Inspect the provider or another payload-modifying extension; no payload is logged"],PAYLOAD_SIZE:["Request exceeds the size limit","Reduce history or tool output size, or start a new session"],SCAN_TIMEOUT:["Redaction worker timed out","Simplify regex rules or reduce input size"],WORKER_FAILED:["Worker failed to start or exited early","Verify all dist files and the Node version; reinstall if needed"],SCAN_INTERNAL:["Unexpected scan failure","Review recent rule changes and retry with smaller input"],SCAN_NUMBER_UNSAFE:["Replacement would turn a numeric JSON value into an invalid number","Narrow the rule so it does not match inside numeric fields such as timestamps or schema limits"],SCAN_UNIQUE_FAILED:["Could not generate a unique shape-preserving replacement","The matched value is too short; match a longer value or set an explicit replacement"],SCAN_ZERO_WIDTH:["Regex produced a zero-width match","Require a non-empty match in the numbered rule"],SCAN_MATCH_LIMIT:["Too many matches in one request","Narrow overly broad rules or reduce input"],SCAN_MAPPING_LIMIT:["Mapping limit reached","Start a new session or /reload; in-memory mappings reset"],FIXED_MAPPING_CONFLICT:["One original has conflicting replacements","Use one replacement for overlapping matchers, then /reload"],FIXED_ALIAS_REUSED:["Fixed alias already maps to another original","A fixed regex represents one original per instance; use random replacement or split literal rules"],FIXED_SENSITIVE_TARGET:["Replacement contains the matched original","Choose a target without sensitive content"],FIXED_OVERLAP:["Fixed replacement partially overlaps another match","Remove the partial overlap or use random rules; rule and otherRule show both rules"],FIXED_ALIAS_CROSSING:["Regex crosses a learned fixed alias","Narrow the regex or choose a different alias"],SCAN_COMPLEXITY:["Request exceeds depth or node limits","Reduce nested data and tool schema size"],SCAN_JSON_REWRITE:["Replacement breaks embedded JSON syntax","Avoid rules spanning JSON syntax; match field values instead"],SCAN_ATTACHMENT:["Unsupported image, audio, video or file content","Remove attachments or send reviewed text only"],SCAN_KEY_COLLISION:["Replacement creates duplicate object keys","Use distinct targets or avoid matching schema keys"],RESTORE_COMPLEXITY:["Response nesting is too deep to restore","Reduce nested structures or start a new session with /new"],RESTORE_KEY_COLLISION:["Restoring produced duplicate object keys","Use more specific rules or fixed targets that cannot collide with schema keys"],COMPACT_DENIED:["Compaction refused",'Allow it when asked, or set "compaction" to "protected" in the local config'],COMPACT_FAILED:["Could not redact what Pi is about to summarize",'Retry; if it persists, set "compaction" to "off" and use /new'],COMPACT_INSTRUCTIONS:["The compaction instruction contains a protected value","Pi reads that instruction from its own state, so it cannot be redacted: run /compact without it"],CONFIG_COMPACTION:["Invalid compaction setting",'Use "ask", "protected" or "off", or an object with mode'],AUDIT_BATCH_LIMIT:["Audit batch exceeds the size limit","Reduce matched content per request"],AUDIT_FULL:["Audit log would exceed the size limit","Review records, then run /protecter logs clear or archive locally"],AUDIT_PARTIAL:["Audit log has an incomplete last line","Inspect locally or run /protecter logs clear; never upload the log"],AUDIT_LOCKED:["Audit lock stayed busy","Wait for writers; stop all Pi processes before inspecting a stale lock"],AUDIT_UNSAFE:["Unsafe audit file","Use an owned regular file without links; checks are not bypassed"],AUDIT_IO:["Audit file operation failed","Check errno, permissions and free space; no path or content is exposed"]},He={INTERNAL:["未预期的本地错误","重载并检查安装包完整性"],NOT_READY:["防护尚未初始化","查看初始化错误,修复后重载"],UNSUPPORTED_PI:["Pi 版本不受支持",`请使用 Pi ${R}`],GUARD_UNAVAILABLE:["无法覆盖没有扩展钩子的模型调用","该 Pi 构建未暴露可包装的模型运行时:请使用 Pi "+R+" 或改用可信的 Pi 版本"],MACHINE_ID:["无法获取机器标识","检查系统机器标识访问权限,不使用随机回退"],CONFIG_JSON:["配置 JSON 无效","本地修复 JSON 语法后重载"],CONFIG_SCHEMA:["配置结构或规则无效","使用版本1、敏感词数组及文档字段,rule 为规则序号"],CONFIG_REGEX:["正则或标志无效","修复指定规则,标志仅支持 gimsu"],CONFIG_TARGET:["固定替换目标无效","使用非空且少于8192字符的目标,不含保留名称"],CONFIG_CONFLICT:["规则定义冲突","为同一匹配器设置一致的替换策略"],CONFIG_SIZE:["配置超过体积或规则数上限","减少配置体积或规则数,规则不会被截断"],CONFIG_IO:["无法访问配置","检查本地权限、所有者和磁盘空间"],CONFIG_UNSAFE:["配置文件不安全","使用本人所有的普通文件,不含符号或硬链接"],CONFIG_EMPTY_MATCH:["正则匹配空字符串","修改指定规则,要求非空匹配"],CONFIG_SENSITIVE_TARGET:["固定目标与敏感规则重叠","选择与所有敏感匹配器无关的目标"],CONFIG_ALIAS_CONFLICT:["固定别名相互重叠","使用互不包含的不同别名"],CONFIG_TIMEOUT:["规则验证超时","简化正则规则后重载"],MIGRATION_LOCKED:["配置迁移锁被占用","等待其他进程,全部停止后再检查残留锁"],MIGRATION_CHANGED:["迁移期间配置发生变化","停止并发编辑后重载,源文件会保留"],MIGRATION_LIMIT:["候选配置过多","在本地整理旧配置文件"],MIGRATION_IO:["迁移文件操作失败","检查权限、空间和残留文件后重试"],PAYLOAD_JSON:["请求无法序列化为 JSON","检查提供商或其他请求改写扩展,不记录请求内容"],PAYLOAD_SIZE:["请求超过体积上限","减少历史或工具输出,或新建会话"],SCAN_TIMEOUT:["脱敏 worker 超时","简化正则规则或缩小输入"],WORKER_FAILED:["worker 启动失败或提前退出","检查 dist 文件完整性和 Node 版本,必要时重装"],SCAN_INTERNAL:["未预期的扫描错误","检查最近的规则改动并缩小输入"],SCAN_NUMBER_UNSAFE:["替换会把 JSON 数值变成无效数字","缩小规则范围,避免匹配时间戳或结构上限等数值字段"],SCAN_UNIQUE_FAILED:["无法生成唯一的同形替换值","命中内容过短或可选空间过小,请匹配更长内容或设置固定 replacement"],SCAN_ZERO_WIDTH:["正则产生零宽匹配","修改指定规则,要求非空匹配"],SCAN_MATCH_LIMIT:["单次请求匹配次数过多","缩小过宽规则或减少输入"],SCAN_MAPPING_LIMIT:["映射数量达到上限","新建会话或重载,内存映射将重置"],FIXED_MAPPING_CONFLICT:["同一原文存在冲突替换","为重叠匹配器使用同一目标后重载"],FIXED_ALIAS_REUSED:["固定别名已映射其他原文","固定正则在同一实例仅代表一种原文,改用随机替换或拆分字面规则"],FIXED_SENSITIVE_TARGET:["替换目标包含命中原文","选择不含敏感内容的目标"],FIXED_OVERLAP:["固定替换与其他匹配部分重叠","消除部分重叠或改用随机规则,rule 与 otherRule 指出两条规则"],FIXED_ALIAS_CROSSING:["正则跨越已学习的固定别名","缩小正则范围或更换别名"],SCAN_COMPLEXITY:["请求超过层级或节点上限","减少嵌套数据和工具结构体积"],SCAN_JSON_REWRITE:["替换破坏内嵌 JSON 语法","避免跨 JSON 语法的规则,改为匹配字段值"],SCAN_ATTACHMENT:["不支持图片、音频、视频或文件内容","移除附件或仅发送已审查的文本"],SCAN_KEY_COLLISION:["替换后对象键重复","使用不同目标或避免匹配结构键"],RESTORE_COMPLEXITY:["还原响应时层级过深","减少嵌套结构,或使用 /new 开始新会话"],RESTORE_KEY_COLLISION:["还原后对象键重复","使用更具体的规则或固定替换目标,避免与结构键冲突"],COMPACT_DENIED:["压缩被拒绝",'询问时选择允许,或在本地配置中将 "compaction" 设为 "protected"'],COMPACT_FAILED:["无法对 Pi 即将摘要的内容脱敏",'请重试;若持续失败,将 "compaction" 设为 "off" 并使用 /new'],COMPACT_INSTRUCTIONS:["压缩指令中包含受保护值","Pi 从自身状态读取该指令,无法脱敏:请不带该指令执行 /compact"],CONFIG_COMPACTION:["compaction 配置值无效",'请使用 "ask"、"protected"、"off",或含 mode 的对象'],AUDIT_BATCH_LIMIT:["审计批次超过上限","减少单个请求的命中内容"],AUDIT_FULL:["审计日志将超过上限","检查记录后清空日志或本地归档"],AUDIT_PARTIAL:["审计日志末行不完整","本地检查或清空日志,不要上传日志"],AUDIT_LOCKED:["审计锁持续被占用","等待写入完成,全部停止后再检查残留锁"],AUDIT_UNSAFE:["审计文件不安全","使用本人普通文件且不含链接,不会绕过检查"],AUDIT_IO:["审计文件操作失败","检查错误码、权限和磁盘空间,不暴露路径或内容"]},ae={blockedHeader:"SPI Protecter blocked this request",reasonLabel:"Reason",actionLabel:"Action",detailsLabel:"Details",statusReady:"ready",statusNotReady:"not ready",statusLine:"SPI Protecter: {state}; {rules}; {mappings}. Memory snapshot; /reload to refresh.",statusLastBlock:"Last block @ {at}:",statusNoBlock:"No recorded block in this session.",statusBarRules:"SPI Protecter · {rules}",statusBarNotReady:"SPI Protecter · not ready",emptyRules:"SPI Protecter: no rules configured; edit the local machine config, then /reload.",commandDescription:"View local SPI protection records",usage:"Usage: /protecter [logs [1-100|clear]|status|reload]. Never type secrets as arguments.",logsTuiOnly:"Open the local TUI to view protection records.",logsNone:"No readable records.",logsSelectTitle:"Protection records",logsTruncated:" (limited tail)",logsRevealTitle:"Sensitive plaintext",logsRevealBody:"Reveal locally? Never share this view.",logsPreviewTitle:"Local preview; edits are discarded",logsPreviewTruncated:"[Preview truncated]",clearTitle:"Clear the current audit log?",clearBody:"Irreversible. Only this machine's log is cleared; configuration and in-memory mappings stay. Later requests may add new records.",cleared:"Audit log cleared: {bytes}.",toolBlocked:"SPI Protecter blocked tool access to the protected configuration or audit log; edit it locally.",compactAskTitle:"Compact this conversation?",compactAskBody:`Pi builds the summarization request internally and never routes it through this extension's redaction, so letting Pi summarize as-is would send the conversation, including protected values, as plaintext.
3
3
 
4
- Allowing here does NOT do that: the text is redacted first, the summary is generated from the redacted text, and the summary is restored locally afterwards. The extra request costs tokens, and the model reasons about replacements, so the summary may describe protected values inaccurately.
4
+ Allowing here does NOT do that: the conversation Pi is about to summarize is redacted first, and Pi then summarizes the redacted text with the current model. No extra model is needed. The stored summary keeps the replacements, and the model reasons about replacements, so it may describe protected values inaccurately.
5
5
 
6
- This request will not be blocked if you allow it.`,compactAllowOnce:"Allow once",compactAllowAlways:"Allow for this session",compactDenyOnce:"Deny once",compactDenyAlways:"Deny for this session",compactWorking:"SPI Protecter: summarizing redacted text…",compactDone:"SPI Protecter: compaction summary generated from redacted text.",compactDegraded:"SPI Protecter: the model produced no usable summary, so a local one was used. It lists facts rather than interpreting them, so earlier context is less detailed.",compactModelConfigured:"SPI Protecter: compaction will summarize with {model}. It receives the whole conversation, redacted, so prefer a local or otherwise trusted endpoint.",compactEarly:"SPI Protecter: compacting early at {tokens} so the summarization model can still read the conversation.",compactMainModel:"the current model",compactNoThreshold:"Pi's own threshold",statusCompaction:"Compaction: {mode} via {target}; compacts above {threshold}.",compactAlwaysNotice:'SPI Protecter: compaction allowed for this session. Set "compaction" in the local config to make it permanent.',compactDenyNotice:"SPI Protecter: compaction refused for this session; use /new to start a new session."},We={compactAskTitle:"是否压缩当前会话?",compactAskBody:`Pi 在内部构造摘要请求,不经过本扩展的脱敏环节,因此直接让 Pi 生成摘要会将会话内容(包括受保护的值)以明文发送。
6
+ This request will not be blocked if you allow it.`,compactAllowOnce:"Allow once",compactAllowAlways:"Allow for this session",compactDenyOnce:"Deny once",compactDenyAlways:"Deny for this session",compactRedacted:"SPI Protecter: redacted what Pi is about to summarize; Pi now summarizes with the current model. The stored summary keeps the replacements.",compactIgnoredKeys:"SPI Protecter: the compaction settings {keys} no longer do anything and are ignored. Pi summarizes with the session's own model; remove them from the local config.",guardInstalled:"covered",guardMissing:"NOT covered",statusCompaction:"Compaction: {mode}; Pi summarizes with the current model. Hookless model calls: {guard}.",compactAlwaysNotice:'SPI Protecter: compaction allowed for this session. Set "compaction" in the local config to make it permanent.',compactDenyNotice:"SPI Protecter: compaction refused for this session; use /new to start a new session."},$e={compactAskTitle:"是否压缩当前会话?",compactAskBody:`Pi 在内部构造摘要请求,不经过本扩展的脱敏环节,因此原样让 Pi 生成摘要会将会话内容(包括受保护的值)以明文发送。
7
7
 
8
- 在此处允许**不会**这样做:先对文本脱敏,再用脱敏后的内容生成摘要,最后在本地还原。这次额外请求会消耗 token;且模型推理的是替换值,摘要对受保护值的描述可能不准确。
8
+ 在此处允许**不会**这样做:先对 Pi 即将摘要的内容脱敏,再由 Pi 用当前模型对脱敏后的内容生成摘要,无需额外模型。保存下来的摘要保留替换值;且模型推理的是替换值,对受保护值的描述可能不准确。
9
9
 
10
- 若选择允许,本次请求不会被拦截。`,compactAllowOnce:"允许一次",compactAllowAlways:"本会话内全部允许",compactDenyOnce:"拒绝一次",compactDenyAlways:"本会话内全部拒绝",compactWorking:"SPI Protecter:正在对脱敏内容生成摘要…",compactDone:"SPI Protecter:已基于脱敏内容生成压缩摘要。",compactDegraded:"SPI Protecter:模型未产出可用摘要,已改用本地摘要。它只罗列事实而不做理解,因此早期上下文会更粗略。",compactModelConfigured:"SPI Protecter:压缩将使用 {model} 生成摘要。它会收到脱敏后的整段对话,请优先使用本地或可信端点。",compactEarly:"SPI Protecter:已在 {tokens} 提前压缩,以便摘要模型仍能读取该对话。",compactMainModel:"当前模型",compactNoThreshold:"Pi 自身阈值",statusCompaction:"压缩:{mode},经由 {target};超过 {threshold} 时压缩。",compactAlwaysNotice:'SPI Protecter:本会话内允许压缩。如需永久生效,请在本地配置中设置 "compaction"。',compactDenyNotice:"SPI Protecter:本会话内拒绝压缩;可使用 /new 开启新会话。",blockedHeader:"SPI Protecter 已阻止该请求",reasonLabel:"原因",actionLabel:"处理",detailsLabel:"详情",statusReady:"就绪",statusNotReady:"未就绪",statusLine:"SPI Protecter:{state};{rules};{mappings}。内存快照,重载后更新。",statusLastBlock:"最近一次拦截 @ {at}:",statusNoBlock:"本会话暂无拦截记录。",statusBarRules:"SPI Protecter · {rules}",statusBarNotReady:"SPI Protecter · 未就绪",emptyRules:"SPI Protecter:尚未配置规则,请编辑本地机器配置后重载。",commandDescription:"查看本地保护记录",usage:"用法:/protecter [logs [1-100|clear]|status|reload]。不要在参数中输入敏感内容。",logsTuiOnly:"请在本地 TUI 查看保护记录。",logsNone:"暂无可读记录。",logsSelectTitle:"保护记录",logsTruncated:"(仅显示有限尾部)",logsRevealTitle:"敏感明文",logsRevealBody:"确认在本地显示?请勿分享此界面。",logsPreviewTitle:"本地预览,编辑不会保存",logsPreviewTruncated:"[预览已截断]",clearTitle:"确认清空当前审计日志?",clearBody:"不可撤销。仅清空本机日志,配置和内存映射保留;后续请求可能写入新记录。",cleared:"已清空审计日志:{bytes}。",toolBlocked:"SPI Protecter 已阻止工具访问受保护的配置或审计日志,请在本地编辑。"};function j(o=process.env){let e=o.PI_PROTECTER_LANG||o.LC_ALL||o.LC_MESSAGES||o.LANG||o.LANGUAGE||He();return/^zh\b|^zh[-_]/i.test(e.trim())?"zh":"en"}function He(){try{return new Intl.DateTimeFormat().resolvedOptions().locale}catch{return""}}var w=j();function le(){return w}function de(o){w=o}function g(o,e={}){return((w==="zh"?We:ae)[o]??ae[o]).replace(/\{(\w+)\}/g,(t,r)=>Object.hasOwn(e,r)?String(e[r]):t)}var Je={en:{rules:{one:"{n} rule",other:"{n} rules"},mappings:{one:"{n} mapping",other:"{n} mappings"},bytes:{one:"{n} byte",other:"{n} bytes"},tokens:{one:"{n} token",other:"{n} tokens"}},zh:{rules:{other:"规则 {n} 条"},mappings:{other:"映射 {n} 个"},bytes:{other:"{n} 字节"},tokens:{other:"{n} token"}}},ce=new Map;function S(o,e){let n=w==="zh"?"zh-CN":"en",t=ce.get(w);if(!t){try{t=new Intl.PluralRules(n)}catch{t=void 0}t&&ce.set(w,t)}let r=t?.select(e)??"other",a=Je[w][o],c=a[r]??a.other,u=new Intl.NumberFormat(n).format(e);return c.replace("{n}",u)}function ue(o){let n=(w==="zh"?$e[o]:Y[o])??Y[o]??Y.INTERNAL;return{reason:n[0],action:n[1]}}var fe={INTERNAL:"internal",NOT_READY:"initialization",UNSUPPORTED_PI:"initialization",MACHINE_ID:"initialization",CONFIG_JSON:"configuration",CONFIG_SCHEMA:"configuration",CONFIG_REGEX:"configuration",CONFIG_TARGET:"configuration",CONFIG_CONFLICT:"configuration",CONFIG_SIZE:"configuration",CONFIG_IO:"configuration",CONFIG_UNSAFE:"configuration",CONFIG_EMPTY_MATCH:"configuration",CONFIG_SENSITIVE_TARGET:"configuration",CONFIG_ALIAS_CONFLICT:"configuration",CONFIG_TIMEOUT:"configuration",CONFIG_COMPACTION:"configuration",CONFIG_COMPACT_MODEL:"configuration",MIGRATION_LOCKED:"migration",MIGRATION_CHANGED:"migration",MIGRATION_LIMIT:"migration",MIGRATION_IO:"migration",PAYLOAD_JSON:"request",PAYLOAD_SIZE:"request",SCAN_TIMEOUT:"scan",WORKER_FAILED:"worker",SCAN_INTERNAL:"scan",SCAN_NUMBER_UNSAFE:"scan",SCAN_UNIQUE_FAILED:"scan",SCAN_ZERO_WIDTH:"scan",SCAN_MATCH_LIMIT:"scan",SCAN_MAPPING_LIMIT:"scan",FIXED_MAPPING_CONFLICT:"scan",FIXED_ALIAS_REUSED:"scan",FIXED_SENSITIVE_TARGET:"scan",FIXED_OVERLAP:"scan",FIXED_ALIAS_CROSSING:"scan",SCAN_COMPLEXITY:"scan",SCAN_JSON_REWRITE:"scan",SCAN_ATTACHMENT:"scan",SCAN_KEY_COLLISION:"scan",RESTORE_COMPLEXITY:"restore",RESTORE_KEY_COLLISION:"restore",COMPACT_DENIED:"compaction",COMPACT_UNAVAILABLE:"compaction",COMPACT_FAILED:"compaction",COMPACT_EMPTY:"compaction",AUDIT_BATCH_LIMIT:"audit",AUDIT_FULL:"audit",AUDIT_PARTIAL:"audit",AUDIT_LOCKED:"audit",AUDIT_UNSAFE:"audit",AUDIT_IO:"audit"};var me=["rule","otherRule","node","bytes","limit","timeoutMs","status"],Ye=/^[A-Za-z][A-Za-z0-9]{0,39}$/,je=/^(EACCES|EPERM|ENOENT|ENOSPC|EDQUOT|EROFS|ELOOP|EISDIR|ENOTDIR|EMFILE|ENFILE|EIO|EEXIST|EBADF|EBUSY|ENOTEMPTY)$/;function Ke(o,e={}){let{reason:n,action:t}=ue(o),r={};for(let a of me){let c=e[a];typeof c=="number"&&Number.isSafeInteger(c)&&c>=0&&(r[a]=c)}return typeof e.errno=="string"&&je.test(e.errno)&&(r.errno=e.errno),typeof e.errorName=="string"&&Ye.test(e.errorName)&&(r.errorName=e.errorName),{code:o,stage:fe[o],reason:n,action:t,...r}}function _(o){let e=le()==="zh"?":":": ",n=[...me,"errno","errorName"].filter(t=>o[t]!==void 0).map(t=>`${t}=${o[t]}`).join(" ");return[g("blockedHeader"),`code=${o.code} stage=${o.stage}${n?` ${n}`:""}`,`${g("reasonLabel")}${e}${o.reason}`,`${g("actionLabel")}${e}${o.action}`,`${g("detailsLabel")}${e}/protecter status`].join(`
11
- `)}var T=class extends Error{diagnostic;constructor(e,n={}){let t=Ke(e,n);super(_(t)),this.name="ProtectionError",this.diagnostic=t}};function i(o,e={}){return new T(o,e)}function O(o,e,n={}){if(o instanceof T)return i(o.diagnostic.code,{...o.diagnostic,...n});let t=o&&typeof o=="object"&&"code"in o?o.code:void 0;return i(e,{...n,errno:typeof t=="string"?t:void 0})}function b(o,e){if(!o||typeof o!="object")return i(e);let n=o,t=typeof n.code=="string"&&Object.hasOwn(fe,n.code)?n.code:e;return i(t,n)}import{constants as R,closeSync as pe,fchmodSync as Xe,fstatSync as qe,mkdirSync as Ze,openSync as ge,readFileSync as Ve,writeFileSync as Qe}from"node:fs";import{dirname as et}from"node:path";var he=["ask","protected","off"],K={version:1,sensitiveWords:[]},F="ask";function tt(o){if(typeof o=="string"){if(!he.includes(o))throw i("CONFIG_COMPACTION");return}if(!o||typeof o!="object"||Array.isArray(o))throw i("CONFIG_COMPACTION");let e=o,n=["mode","provider","model","contextWindow","reserveTokens"];if(Object.keys(e).some(a=>!n.includes(a)))throw i("CONFIG_COMPACTION");if(e.mode!==void 0&&!he.includes(e.mode))throw i("CONFIG_COMPACTION");for(let a of["provider","model"]){let c=e[a];if(c!==void 0&&(typeof c!="string"||!c.trim()||c.length>200))throw i("CONFIG_COMPACTION")}if(e.provider===void 0!=(e.model===void 0))throw i("CONFIG_COMPACT_MODEL");for(let a of["contextWindow","reserveTokens"]){let c=e[a];if(c!==void 0&&(typeof c!="number"||!Number.isSafeInteger(c)||c<=0||c>1e8))throw i("CONFIG_COMPACTION")}let t=e.contextWindow,r=e.reserveTokens;if(t!==void 0&&r!==void 0&&r>=t)throw i("CONFIG_COMPACTION")}function v(o){return o===void 0?{mode:F}:typeof o=="string"?{mode:o}:{...o,mode:o.mode??F}}function X(o){let e;try{e=JSON.parse(o)}catch{throw i("CONFIG_JSON")}let n=0;try{if(!e||e.version!==1||!Array.isArray(e.sensitiveWords)||Object.keys(e).some(r=>!["version","sensitiveWords","compaction"].includes(r)))throw i("CONFIG_SCHEMA");if(Object.hasOwn(e,"compaction")&&tt(e.compaction),e.sensitiveWords.length>1e3)throw i("CONFIG_SIZE",{limit:1e3});let t=new Map;for(let r of e.sensitiveWords){if(n++,typeof r=="string"){if(!r||r.length>8192)throw i("CONFIG_SCHEMA",{rule:n,limit:8192});let c=JSON.stringify(["literal",r]);if(t.has(c)&&t.get(c)!==void 0)throw i("CONFIG_CONFLICT",{rule:n});t.set(c,void 0);continue}if(!r||typeof r!="object")throw i("CONFIG_SCHEMA",{rule:n});if(Object.hasOwn(r,"replacement")&&(typeof r.replacement!="string"||!r.replacement.trim()||r.replacement.length>8192||r.replacement.includes("__PIP_")))throw i("CONFIG_TARGET",{rule:n,limit:8192});if(r.type==="literal"){if(Object.keys(r).some(c=>!["type","value","replacement"].includes(c))||typeof r.value!="string"||!r.value||r.value.length>8192)throw i("CONFIG_SCHEMA",{rule:n,limit:8192})}else if(r.type==="regex"){if(Object.keys(r).some(c=>!["type","pattern","flags","replacement"].includes(c))||typeof r.pattern!="string"||!r.pattern||r.pattern.length>8192||r.flags!==void 0&&(typeof r.flags!="string"||!/^[gimsu]*$/.test(r.flags)))throw i("CONFIG_REGEX",{rule:n,limit:8192});try{new RegExp(r.pattern,r.flags??"")}catch{throw i("CONFIG_REGEX",{rule:n})}}else throw i("CONFIG_SCHEMA",{rule:n});let a=r.type==="literal"?JSON.stringify(["literal",r.value]):JSON.stringify(["regex",r.pattern,[...new Set((r.flags??"")+"g")].sort().join("")]);if(t.has(a)&&t.get(a)!==r.replacement)throw i("CONFIG_CONFLICT",{rule:n});t.set(a,r.replacement)}return e}catch(t){throw t instanceof T?t:i("CONFIG_SCHEMA",{rule:n})}}function ye(o,e=!1){let n;try{if(e){Ze(et(o),{recursive:!0,mode:448});try{let a=ge(o,R.O_WRONLY|R.O_CREAT|R.O_EXCL,384);try{Qe(a,JSON.stringify(K,null,2)+`
12
- `)}finally{pe(a)}}catch(a){if(a.code!=="EEXIST")throw O(a,"CONFIG_IO")}}n=ge(o,R.O_RDONLY|R.O_NOFOLLOW|R.O_NONBLOCK);let t=qe(n);if(t.size>1024*1024)throw i("CONFIG_SIZE",{bytes:t.size,limit:1024*1024});if(!t.isFile()||t.nlink!==1||process.getuid&&t.uid!==process.getuid())throw i("CONFIG_UNSAFE");process.platform!=="win32"&&Xe(n,384);let r=Ve(n,"utf8");if(Buffer.byteLength(r)>1024*1024)throw i("CONFIG_SIZE",{bytes:Buffer.byteLength(r),limit:1024*1024});return{config:X(r),raw:r}}catch(t){let r=t&&typeof t=="object"&&"code"in t?t.code:void 0;throw O(t,r==="ELOOP"||r==="EISDIR"||r==="ENOTDIR"?"CONFIG_UNSAFE":"CONFIG_IO")}finally{n!==void 0&&pe(n)}}var Z=/^protecter(?:\.[a-f0-9]{32})?\.json$/;function at(o){return new Promise((e,n)=>{let t=new it(st(new URL("./validate-worker.mjs",import.meta.url)),{workerData:o,execArgv:[],resourceLimits:{maxOldGenerationSizeMb:64}}),r=!1,a=(u,m=i("WORKER_FAILED"))=>{r||(r=!0,clearTimeout(c),t.terminate(),u?e():n(m))},c=setTimeout(()=>a(!1,i("CONFIG_TIMEOUT",{timeoutMs:2e3})),2e3);t.once("message",u=>u===!0?a(!0):a(!1,b(u,"CONFIG_SCHEMA"))),t.once("error",()=>a(!1)),t.once("exit",()=>a(!1))})}function ct(o){let e=new Set,n=[];for(let f of o)for(let h of f.sensitiveWords){let l=typeof h=="string"?JSON.stringify(["literal",h,null]):h.type==="literal"?JSON.stringify(["literal",h.value,h.replacement??null]):JSON.stringify(["regex",h.pattern,[...new Set((h.flags??"")+"g")].sort().join(""),h.replacement??null]);e.has(l)||(e.add(l),n.push(h))}let t=[];for(let f of o)f.compaction!==void 0&&t.push(v(f.compaction));let a=["off","ask","protected"].find(f=>t.some(h=>h.mode===f)),c=t.find(f=>f.provider&&f.model),u={version:1,sensitiveWords:n};if(a!==void 0||c!==void 0){let f={mode:a??F};c&&(f.provider=c.provider,f.model=c.model,c.contextWindow!==void 0&&(f.contextWindow=c.contextWindow),c.reserveTokens!==void 0&&(f.reserveTokens=c.reserveTokens)),u.compaction=f}let m=JSON.stringify(u);if(Buffer.byteLength(m)>1024*1024)throw i("CONFIG_SIZE",{bytes:Buffer.byteLength(m),limit:1024*1024});return X(m)}function Ie(o){let e;try{e=I.lstatSync(o,{bigint:!0})}catch(n){throw O(n,"CONFIG_IO")}if(!e.isFile()||e.nlink!==1n||process.getuid&&e.uid!==BigInt(process.getuid()))throw i("CONFIG_UNSAFE");return`${e.dev}:${e.ino}:${e.size}:${e.mtimeNs}`}function q(o,e){let n=P(o,e),t=Ie(n),r=ye(n);if(Ie(n)!==t)throw i("MIGRATION_CHANGED");return{name:e,...r,identity:t,digest:nt("sha256").update(r.raw).digest("hex")}}function L(o,e){let n=q(o,e.name);if(n.identity!==e.identity||n.digest!==e.digest)throw i("MIGRATION_CHANGED")}function x(o){return I.readdirSync(o).filter(e=>Z.test(e)).sort()}function Oe(o){if(process.platform==="win32")return;let e=I.openSync(o,I.constants.O_RDONLY);try{I.fsyncSync(e)}finally{I.closeSync(e)}}async function Ce(o,e,n={}){if(!/^[a-f0-9]{32}$/.test(e))throw i("MACHINE_ID");let t=ot(o),r=`protecter.${e}.json`,a=P(t,r);I.mkdirSync(t,{recursive:!0,mode:448});let c=P(t,".protecter-migration.lock"),u=Date.now()+(n.lockTimeoutMs??5e3);for(;;)try{I.mkdirSync(c,{mode:448});break}catch(f){if(f.code!=="EEXIST")throw O(f,"MIGRATION_IO");if(Date.now()>=u)throw i("MIGRATION_LOCKED",{timeoutMs:n.lockTimeoutMs??5e3});await new Promise(h=>setTimeout(h,50))}let m;try{let f=x(t);if(f.length>64)throw i("MIGRATION_LIMIT",{limit:64});let l=[...f].sort((C,D)=>C===r?-1:D===r?1:C.localeCompare(D)).map(C=>q(t,C)),s=ct(l.length?l.map(C=>C.config):[K]);if(await at([s]),l.length===1&&l[0].name===r){if(L(t,l[0]),JSON.stringify(x(t))!==JSON.stringify(f))throw i("MIGRATION_CHANGED");return{path:a,config:l[0].config,raw:l[0].raw,sourceRaws:[l[0].raw],migrated:0}}let d=JSON.stringify(s,null,2)+`
13
- `;if(Buffer.byteLength(d)>1024*1024)throw i("CONFIG_SIZE",{bytes:Buffer.byteLength(d),limit:1024*1024});m=P(t,`.protecter-migration.${rt()}.tmp`);let p=I.openSync(m,I.constants.O_WRONLY|I.constants.O_CREAT|I.constants.O_EXCL,384);try{I.writeFileSync(p,d),I.fsyncSync(p)}finally{I.closeSync(p)}if(n.checkpoint?.("beforeCommit",a),JSON.stringify(x(t))!==JSON.stringify(f))throw i("MIGRATION_CHANGED");for(let C of l)L(t,C);I.renameSync(m,a),m=void 0,Oe(t);let A=q(t,r);if(A.raw!==d)throw i("MIGRATION_CHANGED");n.checkpoint?.("afterCommit",a);for(let C of l)C.name!==r&&(n.checkpoint?.("beforeDelete",P(t,C.name)),L(t,A),L(t,C),I.unlinkSync(P(t,C.name)));Oe(t);let N=[r];if(JSON.stringify(x(t))!==JSON.stringify(N))throw i("MIGRATION_CHANGED");return L(t,A),{path:a,config:s,raw:d,sourceRaws:[...new Set([...l.map(C=>C.raw),d])],migrated:l.filter(C=>C.name!==r).length}}catch(f){throw f instanceof T?f:O(f,"MIGRATION_IO")}finally{if(m)try{I.unlinkSync(m)}catch{}I.rmdirSync(c)}}import{createHash as lt}from"node:crypto";import{readFileSync as dt}from"node:fs";import{execFileSync as Ae}from"node:child_process";function V(o){let e=o.trim().toLowerCase().replace(/-/g,"");if(!/^[a-f0-9]{32}$/.test(e)||/^0+$/.test(e))throw i("MACHINE_ID");return lt("sha256").update(`pi-information-protecter:v1:${e}`).digest("hex").slice(0,32)}function Ne(){try{if(process.platform==="linux")return V(dt("/etc/machine-id","utf8"));if(process.platform==="darwin"){let o=Ae("/usr/sbin/ioreg",["-rd1","-c","IOPlatformExpertDevice"],{encoding:"utf8",timeout:5e3,maxBuffer:1048576,stdio:["ignore","pipe","pipe"]});return V(o.match(/"IOPlatformUUID"\s*=\s*"([a-fA-F0-9-]+)"/)?.[1]??"")}if(process.platform==="win32"){let o=Ae("reg.exe",["query","HKLM\\SOFTWARE\\Microsoft\\Cryptography","/v","MachineGuid","/reg:64"],{encoding:"utf8",timeout:5e3,maxBuffer:65536,stdio:["ignore","pipe","pipe"]});return V(o.match(/MachineGuid\s+REG_SZ\s+([a-fA-F0-9-]+)/i)?.[1]??"")}}catch{}throw i("MACHINE_ID")}import*as y from"node:fs";import{randomUUID as ut}from"node:crypto";var Ee=32*1024*1024,we=8*1024*1024;function ft(o){let e=n=>String(n).padStart(2,"0");return`${o.getFullYear()}-${e(o.getMonth()+1)}-${e(o.getDate())} ${e(o.getHours())}:${e(o.getMinutes())}:${e(o.getSeconds())}`}function Q(o,e,n=e){let t=y.openSync(o,(e?y.constants.O_RDWR|(n?y.constants.O_CREAT:0):y.constants.O_RDONLY)|y.constants.O_NOFOLLOW|y.constants.O_NONBLOCK,384);try{let r=y.fstatSync(t);if(!r.isFile()||r.nlink!==1||process.getuid&&r.uid!==process.getuid())throw i("AUDIT_UNSAFE");return process.platform!=="win32"&&y.fchmodSync(t,384),t}catch(r){throw y.closeSync(t),O(r,"AUDIT_UNSAFE")}}function G(o){let e=o&&typeof o=="object"&&"code"in o?o.code:void 0;return O(o,e==="ELOOP"||e==="EISDIR"||e==="ENOTDIR"?"AUDIT_UNSAFE":"AUDIT_IO")}async function Te(o){let e=`${o}.lock`,n=Date.now()+2e3;for(;;)try{return y.mkdirSync(e,{mode:448}),e}catch(t){if(t.code!=="EEXIST")throw O(t,"AUDIT_IO");if(Date.now()>=n)throw i("AUDIT_LOCKED",{timeoutMs:2e3});await new Promise(r=>setTimeout(r,25))}}async function Se(o){let e=await Te(o),n;try{try{n=Q(o,!0,!1)}catch(r){if(r.code==="ENOENT")return 0;throw r}let t=y.fstatSync(n).size;return y.ftruncateSync(n,0),y.fsyncSync(n),t}catch(t){throw G(t)}finally{try{n!==void 0&&y.closeSync(n)}finally{y.rmdirSync(e)}}}async function _e(o,e,n){if(!n.length)return;let t=new Date,r=ut(),a=Buffer.from(n.map(([m,f])=>JSON.stringify({version:1,time:ft(t),timezoneOffset:-t.getTimezoneOffset(),requestId:r,provider:e||"unknown",original:f,replacement:m})).join(`
10
+ 若选择允许,本次请求不会被拦截。`,compactAllowOnce:"允许一次",compactAllowAlways:"本会话内全部允许",compactDenyOnce:"拒绝一次",compactDenyAlways:"本会话内全部拒绝",compactRedacted:"SPI Protecter:已对 Pi 即将摘要的内容脱敏,现由 Pi 用当前模型生成摘要。保存下来的摘要保留替换值。",compactIgnoredKeys:"SPI Protecter:compaction 配置项 {keys} 已不再生效并被忽略。Pi 使用会话自身的模型生成摘要,请从本地配置中删除它们。",guardInstalled:"已覆盖",guardMissing:"未覆盖",statusCompaction:"压缩:{mode};由 Pi 用当前模型生成摘要。无钩子的模型调用:{guard}。",compactAlwaysNotice:'SPI Protecter:本会话内允许压缩。如需永久生效,请在本地配置中设置 "compaction"。',compactDenyNotice:"SPI Protecter:本会话内拒绝压缩;可使用 /new 开启新会话。",blockedHeader:"SPI Protecter 已阻止该请求",reasonLabel:"原因",actionLabel:"处理",detailsLabel:"详情",statusReady:"就绪",statusNotReady:"未就绪",statusLine:"SPI Protecter:{state};{rules};{mappings}。内存快照,重载后更新。",statusLastBlock:"最近一次拦截 @ {at}:",statusNoBlock:"本会话暂无拦截记录。",statusBarRules:"SPI Protecter · {rules}",statusBarNotReady:"SPI Protecter · 未就绪",emptyRules:"SPI Protecter:尚未配置规则,请编辑本地机器配置后重载。",commandDescription:"查看本地保护记录",usage:"用法:/protecter [logs [1-100|clear]|status|reload]。不要在参数中输入敏感内容。",logsTuiOnly:"请在本地 TUI 查看保护记录。",logsNone:"暂无可读记录。",logsSelectTitle:"保护记录",logsTruncated:"(仅显示有限尾部)",logsRevealTitle:"敏感明文",logsRevealBody:"确认在本地显示?请勿分享此界面。",logsPreviewTitle:"本地预览,编辑不会保存",logsPreviewTruncated:"[预览已截断]",clearTitle:"确认清空当前审计日志?",clearBody:"不可撤销。仅清空本机日志,配置和内存映射保留;后续请求可能写入新记录。",cleared:"已清空审计日志:{bytes}。",toolBlocked:"SPI Protecter 已阻止工具访问受保护的配置或审计日志,请在本地编辑。"};function J(t=process.env){let e=t.PI_PROTECTER_LANG||t.LC_ALL||t.LC_MESSAGES||t.LANG||t.LANGUAGE||Je();return/^zh\b|^zh[-_]/i.test(e.trim())?"zh":"en"}function Je(){try{return new Intl.DateTimeFormat().resolvedOptions().locale}catch{return""}}var E=J();function le(){return E}function ue(t){E=t}function p(t,e={}){return((E==="zh"?$e:ae)[t]??ae[t]).replace(/\{(\w+)\}/g,(n,r)=>Object.hasOwn(e,r)?String(e[r]):n)}var Ye={en:{rules:{one:"{n} rule",other:"{n} rules"},mappings:{one:"{n} mapping",other:"{n} mappings"},bytes:{one:"{n} byte",other:"{n} bytes"},tokens:{one:"{n} token",other:"{n} tokens"}},zh:{rules:{other:"规则 {n} 条"},mappings:{other:"映射 {n} 个"},bytes:{other:"{n} 字节"},tokens:{other:"{n} token"}}},ce=new Map;function P(t,e){let o=E==="zh"?"zh-CN":"en",n=ce.get(E);if(!n){try{n=new Intl.PluralRules(o)}catch{n=void 0}n&&ce.set(E,n)}let r=n?.select(e)??"other",a=Ye[E][t],d=a[r]??a.other,m=new Intl.NumberFormat(o).format(e);return d.replace("{n}",m)}function de(t){let o=(E==="zh"?He[t]:$[t])??$[t]??$.INTERNAL;return{reason:o[0],action:o[1]}}var fe={INTERNAL:"internal",NOT_READY:"initialization",UNSUPPORTED_PI:"initialization",GUARD_UNAVAILABLE:"initialization",MACHINE_ID:"initialization",CONFIG_JSON:"configuration",CONFIG_SCHEMA:"configuration",CONFIG_REGEX:"configuration",CONFIG_TARGET:"configuration",CONFIG_CONFLICT:"configuration",CONFIG_SIZE:"configuration",CONFIG_IO:"configuration",CONFIG_UNSAFE:"configuration",CONFIG_EMPTY_MATCH:"configuration",CONFIG_SENSITIVE_TARGET:"configuration",CONFIG_ALIAS_CONFLICT:"configuration",CONFIG_TIMEOUT:"configuration",CONFIG_COMPACTION:"configuration",MIGRATION_LOCKED:"migration",MIGRATION_CHANGED:"migration",MIGRATION_LIMIT:"migration",MIGRATION_IO:"migration",PAYLOAD_JSON:"request",PAYLOAD_SIZE:"request",SCAN_TIMEOUT:"scan",WORKER_FAILED:"worker",SCAN_INTERNAL:"scan",SCAN_NUMBER_UNSAFE:"scan",SCAN_UNIQUE_FAILED:"scan",SCAN_ZERO_WIDTH:"scan",SCAN_MATCH_LIMIT:"scan",SCAN_MAPPING_LIMIT:"scan",FIXED_MAPPING_CONFLICT:"scan",FIXED_ALIAS_REUSED:"scan",FIXED_SENSITIVE_TARGET:"scan",FIXED_OVERLAP:"scan",FIXED_ALIAS_CROSSING:"scan",SCAN_COMPLEXITY:"scan",SCAN_JSON_REWRITE:"scan",SCAN_ATTACHMENT:"scan",SCAN_KEY_COLLISION:"scan",RESTORE_COMPLEXITY:"restore",RESTORE_KEY_COLLISION:"restore",COMPACT_DENIED:"compaction",COMPACT_FAILED:"compaction",COMPACT_INSTRUCTIONS:"compaction",AUDIT_BATCH_LIMIT:"audit",AUDIT_FULL:"audit",AUDIT_PARTIAL:"audit",AUDIT_LOCKED:"audit",AUDIT_UNSAFE:"audit",AUDIT_IO:"audit"};var me=["rule","otherRule","node","bytes","limit","timeoutMs","status"],je=/^[A-Za-z][A-Za-z0-9]{0,39}$/,We=/^(EACCES|EPERM|ENOENT|ENOSPC|EDQUOT|EROFS|ELOOP|EISDIR|ENOTDIR|EMFILE|ENFILE|EIO|EEXIST|EBADF|EBUSY|ENOTEMPTY)$/;function Ke(t,e={}){let{reason:o,action:n}=de(t),r={};for(let a of me){let d=e[a];typeof d=="number"&&Number.isSafeInteger(d)&&d>=0&&(r[a]=d)}return typeof e.errno=="string"&&We.test(e.errno)&&(r.errno=e.errno),typeof e.errorName=="string"&&je.test(e.errorName)&&(r.errorName=e.errorName),{code:t,stage:fe[t],reason:o,action:n,...r}}function w(t){let e=le()==="zh"?":":": ",o=[...me,"errno","errorName"].filter(n=>t[n]!==void 0).map(n=>`${n}=${t[n]}`).join(" ");return[p("blockedHeader"),`code=${t.code} stage=${t.stage}${o?` ${o}`:""}`,`${p("reasonLabel")}${e}${t.reason}`,`${p("actionLabel")}${e}${t.action}`,`${p("detailsLabel")}${e}/protecter status`].join(`
11
+ `)}var _=class extends Error{diagnostic;constructor(e,o={}){let n=Ke(e,o);super(w(n)),this.name="ProtectionError",this.diagnostic=n}};function i(t,e={}){return new _(t,e)}function O(t,e,o={}){if(t instanceof _)return i(t.diagnostic.code,{...t.diagnostic,...o});let n=t&&typeof t=="object"&&"code"in t?t.code:void 0;return i(e,{...o,errno:typeof n=="string"?n:void 0})}function M(t,e){if(!t||typeof t!="object")return i(e);let o=t,n=typeof o.code=="string"&&Object.hasOwn(fe,o.code)?o.code:e;return i(n,o)}import{constants as S,closeSync as pe,fchmodSync as Xe,fstatSync as qe,mkdirSync as Ze,openSync as ge,readFileSync as Ve,writeFileSync as Qe}from"node:fs";import{dirname as et}from"node:path";var he=["ask","protected","off"],Y=["provider","model","contextWindow","reserveTokens"];function ye(t){return!t||typeof t!="object"?[]:Y.filter(e=>Object.hasOwn(t,e))}var j={version:1,sensitiveWords:[]},Ie="ask";function tt(t){if(typeof t=="string"){if(!he.includes(t))throw i("CONFIG_COMPACTION");return}if(!t||typeof t!="object"||Array.isArray(t))throw i("CONFIG_COMPACTION");let e=t,o=["mode",...Y];if(Object.keys(e).some(n=>!o.includes(n)))throw i("CONFIG_COMPACTION");if(e.mode!==void 0&&!he.includes(e.mode))throw i("CONFIG_COMPACTION");for(let n of Y){let r=e[n];if(r!==void 0&&typeof r!="string"&&typeof r!="number")throw i("CONFIG_COMPACTION")}}function F(t){return t===void 0?{mode:Ie}:typeof t=="string"?{mode:t}:{mode:t.mode??Ie}}function W(t){let e;try{e=JSON.parse(t)}catch{throw i("CONFIG_JSON")}let o=0;try{if(!e||e.version!==1||!Array.isArray(e.sensitiveWords)||Object.keys(e).some(r=>!["version","sensitiveWords","compaction"].includes(r)))throw i("CONFIG_SCHEMA");if(Object.hasOwn(e,"compaction")&&tt(e.compaction),e.sensitiveWords.length>1e3)throw i("CONFIG_SIZE",{limit:1e3});let n=new Map;for(let r of e.sensitiveWords){if(o++,typeof r=="string"){if(!r||r.length>8192)throw i("CONFIG_SCHEMA",{rule:o,limit:8192});let d=JSON.stringify(["literal",r]);if(n.has(d)&&n.get(d)!==void 0)throw i("CONFIG_CONFLICT",{rule:o});n.set(d,void 0);continue}if(!r||typeof r!="object")throw i("CONFIG_SCHEMA",{rule:o});if(Object.hasOwn(r,"replacement")&&(typeof r.replacement!="string"||!r.replacement.trim()||r.replacement.length>8192||r.replacement.includes("__PIP_")))throw i("CONFIG_TARGET",{rule:o,limit:8192});if(r.type==="literal"){if(Object.keys(r).some(d=>!["type","value","replacement"].includes(d))||typeof r.value!="string"||!r.value||r.value.length>8192)throw i("CONFIG_SCHEMA",{rule:o,limit:8192})}else if(r.type==="regex"){if(Object.keys(r).some(d=>!["type","pattern","flags","replacement"].includes(d))||typeof r.pattern!="string"||!r.pattern||r.pattern.length>8192||r.flags!==void 0&&(typeof r.flags!="string"||!/^[gimsu]*$/.test(r.flags)))throw i("CONFIG_REGEX",{rule:o,limit:8192});try{new RegExp(r.pattern,r.flags??"")}catch{throw i("CONFIG_REGEX",{rule:o})}}else throw i("CONFIG_SCHEMA",{rule:o});let a=r.type==="literal"?JSON.stringify(["literal",r.value]):JSON.stringify(["regex",r.pattern,[...new Set((r.flags??"")+"g")].sort().join("")]);if(n.has(a)&&n.get(a)!==r.replacement)throw i("CONFIG_CONFLICT",{rule:o});n.set(a,r.replacement)}return e}catch(n){throw n instanceof _?n:i("CONFIG_SCHEMA",{rule:o})}}function Oe(t,e=!1){let o;try{if(e){Ze(et(t),{recursive:!0,mode:448});try{let a=ge(t,S.O_WRONLY|S.O_CREAT|S.O_EXCL,384);try{Qe(a,JSON.stringify(j,null,2)+`
12
+ `)}finally{pe(a)}}catch(a){if(a.code!=="EEXIST")throw O(a,"CONFIG_IO")}}o=ge(t,S.O_RDONLY|S.O_NOFOLLOW|S.O_NONBLOCK);let n=qe(o);if(n.size>1024*1024)throw i("CONFIG_SIZE",{bytes:n.size,limit:1024*1024});if(!n.isFile()||n.nlink!==1||process.getuid&&n.uid!==process.getuid())throw i("CONFIG_UNSAFE");process.platform!=="win32"&&Xe(o,384);let r=Ve(o,"utf8");if(Buffer.byteLength(r)>1024*1024)throw i("CONFIG_SIZE",{bytes:Buffer.byteLength(r),limit:1024*1024});return{config:W(r),raw:r}}catch(n){let r=n&&typeof n=="object"&&"code"in n?n.code:void 0;throw O(n,r==="ELOOP"||r==="EISDIR"||r==="ENOTDIR"?"CONFIG_UNSAFE":"CONFIG_IO")}finally{o!==void 0&&pe(o)}}var X=/^protecter(?:\.[a-f0-9]{32})?\.json$/;function at(t){return new Promise((e,o)=>{let n=new it(st(new URL("./validate-worker.mjs",import.meta.url)),{workerData:t,execArgv:[],resourceLimits:{maxOldGenerationSizeMb:64}}),r=!1,a=(m,f=i("WORKER_FAILED"))=>{r||(r=!0,clearTimeout(d),n.terminate(),m?e():o(f))},d=setTimeout(()=>a(!1,i("CONFIG_TIMEOUT",{timeoutMs:2e3})),2e3);n.once("message",m=>m===!0?a(!0):a(!1,M(m,"CONFIG_SCHEMA"))),n.once("error",()=>a(!1)),n.once("exit",()=>a(!1))})}function ct(t){let e=new Set,o=[];for(let f of t)for(let l of f.sensitiveWords){let u=typeof l=="string"?JSON.stringify(["literal",l,null]):l.type==="literal"?JSON.stringify(["literal",l.value,l.replacement??null]):JSON.stringify(["regex",l.pattern,[...new Set((l.flags??"")+"g")].sort().join(""),l.replacement??null]);e.has(u)||(e.add(u),o.push(l))}let n=[];for(let f of t)f.compaction!==void 0&&n.push(F(f.compaction));let a=["off","ask","protected"].find(f=>n.some(l=>l.mode===f)),d={version:1,sensitiveWords:o};a!==void 0&&(d.compaction={mode:a});let m=JSON.stringify(d);if(Buffer.byteLength(m)>1024*1024)throw i("CONFIG_SIZE",{bytes:Buffer.byteLength(m),limit:1024*1024});return W(m)}function Ne(t){let e;try{e=I.lstatSync(t,{bigint:!0})}catch(o){throw O(o,"CONFIG_IO")}if(!e.isFile()||e.nlink!==1n||process.getuid&&e.uid!==BigInt(process.getuid()))throw i("CONFIG_UNSAFE");return`${e.dev}:${e.ino}:${e.size}:${e.mtimeNs}`}function K(t,e){let o=T(t,e),n=Ne(o),r=Oe(o);if(Ne(o)!==n)throw i("MIGRATION_CHANGED");return{name:e,...r,identity:n,digest:ot("sha256").update(r.raw).digest("hex")}}function D(t,e){let o=K(t,e.name);if(o.identity!==e.identity||o.digest!==e.digest)throw i("MIGRATION_CHANGED")}function b(t){return I.readdirSync(t).filter(e=>X.test(e)).sort()}function Ae(t){if(process.platform==="win32")return;let e=I.openSync(t,I.constants.O_RDONLY);try{I.fsyncSync(e)}finally{I.closeSync(e)}}async function Ce(t,e,o={}){if(!/^[a-f0-9]{32}$/.test(e))throw i("MACHINE_ID");let n=nt(t),r=`protecter.${e}.json`,a=T(n,r);I.mkdirSync(n,{recursive:!0,mode:448});let d=T(n,".protecter-migration.lock"),m=Date.now()+(o.lockTimeoutMs??5e3);for(;;)try{I.mkdirSync(d,{mode:448});break}catch(l){if(l.code!=="EEXIST")throw O(l,"MIGRATION_IO");if(Date.now()>=m)throw i("MIGRATION_LOCKED",{timeoutMs:o.lockTimeoutMs??5e3});await new Promise(u=>setTimeout(u,50))}let f;try{let l=b(n);if(l.length>64)throw i("MIGRATION_LIMIT",{limit:64});let s=[...l].sort((N,C)=>N===r?-1:C===r?1:N.localeCompare(C)).map(N=>K(n,N)),c=ct(s.length?s.map(N=>N.config):[j]);if(await at([c]),s.length===1&&s[0].name===r){if(D(n,s[0]),JSON.stringify(b(n))!==JSON.stringify(l))throw i("MIGRATION_CHANGED");return{path:a,config:s[0].config,raw:s[0].raw,sourceRaws:[s[0].raw],migrated:0}}let h=JSON.stringify(c,null,2)+`
13
+ `;if(Buffer.byteLength(h)>1024*1024)throw i("CONFIG_SIZE",{bytes:Buffer.byteLength(h),limit:1024*1024});f=T(n,`.protecter-migration.${rt()}.tmp`);let y=I.openSync(f,I.constants.O_WRONLY|I.constants.O_CREAT|I.constants.O_EXCL,384);try{I.writeFileSync(y,h),I.fsyncSync(y)}finally{I.closeSync(y)}if(o.checkpoint?.("beforeCommit",a),JSON.stringify(b(n))!==JSON.stringify(l))throw i("MIGRATION_CHANGED");for(let N of s)D(n,N);I.renameSync(f,a),f=void 0,Ae(n);let A=K(n,r);if(A.raw!==h)throw i("MIGRATION_CHANGED");o.checkpoint?.("afterCommit",a);for(let N of s)N.name!==r&&(o.checkpoint?.("beforeDelete",T(n,N.name)),D(n,A),D(n,N),I.unlinkSync(T(n,N.name)));Ae(n);let U=[r];if(JSON.stringify(b(n))!==JSON.stringify(U))throw i("MIGRATION_CHANGED");return D(n,A),{path:a,config:c,raw:h,sourceRaws:[...new Set([...s.map(N=>N.raw),h])],migrated:s.filter(N=>N.name!==r).length}}catch(l){throw l instanceof _?l:O(l,"MIGRATION_IO")}finally{if(f)try{I.unlinkSync(f)}catch{}I.rmdirSync(d)}}import{createHash as lt}from"node:crypto";import{readFileSync as ut}from"node:fs";import{execFileSync as Ee}from"node:child_process";function q(t){let e=t.trim().toLowerCase().replace(/-/g,"");if(!/^[a-f0-9]{32}$/.test(e)||/^0+$/.test(e))throw i("MACHINE_ID");return lt("sha256").update(`pi-information-protecter:v1:${e}`).digest("hex").slice(0,32)}function _e(){try{if(process.platform==="linux")return q(ut("/etc/machine-id","utf8"));if(process.platform==="darwin"){let t=Ee("/usr/sbin/ioreg",["-rd1","-c","IOPlatformExpertDevice"],{encoding:"utf8",timeout:5e3,maxBuffer:1048576,stdio:["ignore","pipe","pipe"]});return q(t.match(/"IOPlatformUUID"\s*=\s*"([a-fA-F0-9-]+)"/)?.[1]??"")}if(process.platform==="win32"){let t=Ee("reg.exe",["query","HKLM\\SOFTWARE\\Microsoft\\Cryptography","/v","MachineGuid","/reg:64"],{encoding:"utf8",timeout:5e3,maxBuffer:65536,stdio:["ignore","pipe","pipe"]});return q(t.match(/MachineGuid\s+REG_SZ\s+([a-fA-F0-9-]+)/i)?.[1]??"")}}catch{}throw i("MACHINE_ID")}import*as g from"node:fs";import{randomUUID as dt}from"node:crypto";var we=32*1024*1024,Se=8*1024*1024;function ft(t){let e=o=>String(o).padStart(2,"0");return`${t.getFullYear()}-${e(t.getMonth()+1)}-${e(t.getDate())} ${e(t.getHours())}:${e(t.getMinutes())}:${e(t.getSeconds())}`}function Z(t,e,o=e){let n=g.openSync(t,(e?g.constants.O_RDWR|(o?g.constants.O_CREAT:0):g.constants.O_RDONLY)|g.constants.O_NOFOLLOW|g.constants.O_NONBLOCK,384);try{let r=g.fstatSync(n);if(!r.isFile()||r.nlink!==1||process.getuid&&r.uid!==process.getuid())throw i("AUDIT_UNSAFE");return process.platform!=="win32"&&g.fchmodSync(n,384),n}catch(r){throw g.closeSync(n),O(r,"AUDIT_UNSAFE")}}function G(t){let e=t&&typeof t=="object"&&"code"in t?t.code:void 0;return O(t,e==="ELOOP"||e==="EISDIR"||e==="ENOTDIR"?"AUDIT_UNSAFE":"AUDIT_IO")}async function Te(t){let e=`${t}.lock`,o=Date.now()+2e3;for(;;)try{return g.mkdirSync(e,{mode:448}),e}catch(n){if(n.code!=="EEXIST")throw O(n,"AUDIT_IO");if(Date.now()>=o)throw i("AUDIT_LOCKED",{timeoutMs:2e3});await new Promise(r=>setTimeout(r,25))}}async function Re(t){let e=await Te(t),o;try{try{o=Z(t,!0,!1)}catch(r){if(r.code==="ENOENT")return 0;throw r}let n=g.fstatSync(o).size;return g.ftruncateSync(o,0),g.fsyncSync(o),n}catch(n){throw G(n)}finally{try{o!==void 0&&g.closeSync(o)}finally{g.rmdirSync(e)}}}async function Pe(t,e,o){if(!o.length)return;let n=new Date,r=dt(),a=Buffer.from(o.map(([f,l])=>JSON.stringify({version:1,time:ft(n),timezoneOffset:-n.getTimezoneOffset(),requestId:r,provider:e||"unknown",original:l,replacement:f})).join(`
14
14
  `)+`
15
- `);if(a.length>we)throw i("AUDIT_BATCH_LIMIT",{bytes:a.length,limit:we});let c=await Te(o),u;try{u=Q(o,!0);let m=y.fstatSync(u).size;if(m+a.length>Ee)throw i("AUDIT_FULL",{bytes:m+a.length,limit:Ee});if(m){let f=Buffer.alloc(1);if(y.readSync(u,f,0,1,m-1),f[0]!==10)throw i("AUDIT_PARTIAL")}try{let f=0;for(;f<a.length;){let h=y.writeSync(u,a,f,a.length-f,m+f);if(!h)throw i("AUDIT_IO");f+=h}y.fsyncSync(u)}catch(f){throw y.ftruncateSync(u,m),y.fsyncSync(u),G(f)}}catch(m){throw G(m)}finally{u!==void 0&&y.closeSync(u),y.rmdirSync(c)}}function Re(o,e=20){if(!Number.isInteger(e)||e<1||e>100)throw i("INTERNAL",{limit:100});let n;try{try{n=Q(o,!1)}catch(s){if(s.code==="ENOENT")return{records:[],truncated:!1};throw s}let t=y.fstatSync(n).size,r=Math.max(0,t-1024*1024),a=Buffer.alloc(t-r),c=y.readSync(n,a,0,a.length,r),u=a.subarray(0,c).toString("utf8");if(r){let s=u.indexOf(`
16
- `);u=s<0?"":u.slice(s+1)}let m=u.split(`
17
- `),f=m.pop()!=="",h=[],l=!1;for(let s of m.slice(-e))try{let d=JSON.parse(s);if(d.version!==1||typeof d.time!="string"||typeof d.provider!="string"||typeof d.original!="string"||typeof d.replacement!="string"||typeof d.requestId!="string"||typeof d.timezoneOffset!="number")throw new Error;h.push(d)}catch{l=!0}return{records:h,truncated:r>0||m.length>e||f||l}}catch(t){throw G(t)}finally{n!==void 0&&y.closeSync(n)}}var U=class{constructor(e,n=2e3,t=Ne){this.configDir=e;this.timeoutMs=n;this.machineId=t}configDir;timeoutMs;machineId;tokens=new Map;queue=Promise.resolve();workers=new Set;closed=!1;snapshot;lastError;restorePattern;restoreSize=-1;get lastDiagnostic(){return this.lastError?{...this.lastError}:void 0}get lastDiagnosticText(){return this.lastError?_(this.lastError):void 0}async initialize(){try{if(this.closed)throw i("NOT_READY");this.snapshot=void 0;let e;try{e=this.machineId()}catch{throw i("MACHINE_ID")}let n=await Ce(this.configDir,e);if(this.closed)throw i("NOT_READY");this.snapshot=n,this.lastError=void 0}catch(e){let n=O(e,"CONFIG_IO");throw this.lastError=n.diagnostic,n}}get configPath(){return this.snapshot?.path??""}get auditPath(){return this.configPath?this.configPath.replace(/\.json$/,".jsonl"):""}auditRecords(e=20){if(!this.ready)throw this.notReady();return Re(this.auditPath,e)}clearAuditRecords(){let e=this.queue.then(()=>{if(!this.ready)throw this.notReady();return Se(this.auditPath)});return this.queue=e.catch(()=>{}),e}get ruleCount(){return this.snapshot?.config.sensitiveWords.length??0}get compaction(){return v(this.snapshot?.config.compaction)}get compactionMode(){return this.compaction.mode}get ready(){return!!this.snapshot&&!this.closed}get mappingCount(){return this.tokens.size}redact(e,n="unknown"){let t=this.queue.then(()=>this.scan(e,n)).catch(r=>{let a=O(r,"INTERNAL");throw this.lastError=a.diagnostic,a});return this.queue=t.catch(()=>{}),t}notReady(){return this.lastError?i(this.lastError.code,this.lastError):i("NOT_READY")}async scan(e,n){if(!this.ready)throw this.notReady();let{config:t,sourceRaws:r}=this.snapshot,a;try{a=JSON.stringify(e)}catch{throw i("PAYLOAD_JSON")}if(!a)throw i("PAYLOAD_JSON");if(Buffer.byteLength(a)>8*1024*1024)throw i("PAYLOAD_SIZE",{bytes:Buffer.byteLength(a),limit:8*1024*1024});let c;try{c=JSON.parse(a)}catch{throw i("PAYLOAD_JSON")}let u=await new Promise((m,f)=>{let h=!1,l;try{l=new mt(pt(new URL("./scan-worker.mjs",import.meta.url)),{workerData:{payload:c,rules:t.sensitiveWords,entries:[...this.tokens],configRaws:r},execArgv:[],resourceLimits:{maxOldGenerationSizeMb:128}})}catch(p){f(O(p,"WORKER_FAILED"));return}this.workers.add(l);let s=(p,A=i("WORKER_FAILED"))=>{if(!h){if(h=!0,clearTimeout(d),this.workers.delete(l),l.terminate(),!p||this.closed){f(this.closed?i("NOT_READY"):A);return}m(p)}},d=setTimeout(()=>s(void 0,i("SCAN_TIMEOUT",{timeoutMs:this.timeoutMs})),this.timeoutMs);l.once("message",p=>{p?.failed?s(void 0,b(p.diagnostic,"SCAN_INTERNAL")):p&&Array.isArray(p.additions)&&Array.isArray(p.hits)?s(p):s()}),l.once("error",()=>s()),l.once("exit",()=>s())});if(this.closed)throw i("NOT_READY");if(await _e(this.auditPath,n,u.hits),this.closed)throw i("NOT_READY");for(let[m,f]of u.additions)this.tokens.set(m,f);return u.payload}restoreText(e){if(!this.tokens.size)return e;if(this.restoreSize!==this.tokens.size){let n=t=>t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");this.restorePattern=new RegExp([...this.tokens.keys()].sort((t,r)=>r.length-t.length).map(n).join("|"),"g"),this.restoreSize=this.tokens.size}return e.replace(this.restorePattern,n=>this.tokens.get(n)??n)}restore(e,n=0){if(n>80)throw i("RESTORE_COMPLEXITY",{node:n});if(typeof e=="string")return this.restoreText(e);if(typeof e=="number"){let t=this.tokens.get(String(e));if(t===void 0)return e;let r=Number(t);return!Number.isFinite(r)||String(r)!==t?e:r}if(Array.isArray(e))return e.map(t=>this.restore(t,n+1));if(e&&typeof e=="object"){let t=Object.create(null);for(let[r,a]of Object.entries(e)){let c=this.restoreText(r);if(Object.hasOwn(t,c))throw i("RESTORE_KEY_COLLISION");t[c]=this.restore(a,n+1)}return t}return e}close(){this.closed=!0,this.snapshot=void 0,this.tokens.clear();for(let e of this.workers)e.terminate();this.workers.clear()}};import{realpathSync as gt,statSync as Pe,readdirSync as ht}from"node:fs";import{homedir as yt}from"node:os";import{basename as ee,dirname as k,isAbsolute as It,relative as Le,resolve as z}from"node:path";import{fileURLToPath as Ot}from"node:url";function Ct(o,e){let n=o.replace(/^@/,"");return n.startsWith("file://")&&(n=Ot(n)),(n==="~"||n.startsWith("~/"))&&(n=yt()+n.slice(1)),z(e,n)}function te(o){try{return gt(o)}catch{let e=k(o);return e===o?o:z(te(e),Le(e,o))}}function At(o,e){let n=Le(o,e);return n===""||!n.startsWith("..")&&!It(n)}function De(o,e){try{let n=Pe(o),t=Pe(e);return n.dev===t.dev&&n.ino===t.ino}catch{return!1}}function ke(o,e,n,t){if(!t)return!0;let r=te(z(t));if(["bash","powershell"].includes(o)){let u=typeof e.command=="string"?e.command:"";if(/protecter(?:\.[a-f0-9]{32})?\.jsonl?|\.protecter-migration/i.test(u)||u.includes(t))return!0}let a=e.path??e.file_path??e.filePath,c=["grep","find","ls"].includes(o);if(typeof a!="string"&&!c)return!1;try{let u=Ct(typeof a=="string"?a:".",n),m=te(u),f=/^protecter\.[a-f0-9]{32}\.jsonl(?:\.lock)?$/,h=ht(k(r)).filter(l=>f.test(l)).some(l=>De(u,z(k(r),l)));return k(m)===k(r)&&(Z.test(ee(m))||f.test(ee(m))||ee(m).startsWith(".protecter-migration"))||m===r||De(u,t)||h||c&&At(m,r)}catch{return!0}}import{DEFAULT_COMPACTION_SETTINGS as Nt,generateBranchSummary as Et,generateSummaryWithUsage as wt}from"@earendil-works/pi-coding-agent";function Me(o){let e={};if(!o||typeof o!="object")return e;let n=o.name;typeof n=="string"?e.errorName=n:typeof o.constructor?.name=="string"&&(e.errorName=o.constructor.name);for(let t of["status","statusCode"]){let r=o[t];if(typeof r=="number"&&Number.isSafeInteger(r)&&r>=0){e.status=r;break}}return e}function B(o,e){if(!e?.provider||!e.model)return o.model;let n=o.modelRegistry.find(e.provider,e.model);if(!n)throw i("CONFIG_COMPACT_MODEL");return n}function oe(o,e,n){let t;try{t=B(o,e)}catch{return}let r=e.contextWindow??t?.contextWindow;if(typeof r!="number"||r<=0)return;let a=e.reserveTokens??n??Nt.reserveTokens;if(!(a>=r))return{window:r,reserve:a,threshold:r-a}}async function be(o,e){let n=B(o,e);if(!n)throw i("COMPACT_UNAVAILABLE");let t;try{t=await o.modelRegistry.getApiKeyAndHeaders(n)}catch{throw i("COMPACT_UNAVAILABLE")}if(!t.ok)throw i("COMPACT_UNAVAILABLE");let r=Object.fromEntries(Object.entries(t.headers??{}).filter(a=>typeof a[1]=="string"));return{model:n,apiKey:t.apiKey,headers:r,env:t.env}}function Fe(o){let e=o.modelRegistry;if(typeof e.streamSimple=="function")return(n,t,r)=>e.streamSimple(n,t,r);if(typeof e.complete=="function")return(n,t,r)=>({result:()=>e.complete(n,t,r)})}function Tt(o){let e=[],n=[],t=new Map,r=(u,m)=>u.length>m?`${u.slice(0,m)}…`:u;for(let u of o){let m=u;if(Array.isArray(m.content))for(let f of m.content){let h=f;if(h.type==="toolCall"&&typeof h.name=="string"&&t.set(h.name,(t.get(h.name)??0)+1),h.type!=="text"||typeof h.text!="string")continue;let l=h.text.trim();l&&(m.role==="user"?e.push(r(l,300)):m.role==="assistant"&&n.push(r(l,300)))}}let a=[...t.entries()].sort((u,m)=>m[1]-u[1]).slice(0,12).map(([u,m])=>`${u} x${m}`);return["# Local compaction summary","","Generated locally without a model call, so it lists facts rather than","interpreting them. Protected values appear as their replacements.","",`- Messages summarized: ${o.length}`,a.length?`- Tools used: ${a.join(", ")}`:"","","## Requests",...e.length?e.slice(-12).map(u=>`- ${u}`):["- (none recorded)"],"","## Latest replies",...n.length?n.slice(-6).map(u=>`- ${u}`):["- (none recorded)"]].filter(u=>u!=="").join(`
18
- `)}async function ve(o,e,n,t,r=wt,a){let{preparation:c}=o,u=await n.redact(c.messagesToSummarize,t),m=c.previousSummary===void 0?void 0:await n.redact(c.previousSummary,t),f=o.customInstructions===void 0?void 0:await n.redact(o.customInstructions,t),h="",l,s;try{let d=await be(e,a),p=await r(u,d.model,c.settings.reserveTokens,d.apiKey,d.headers,o.signal,f,m,e.thinkingLevel,Fe(e),d.env);h=typeof p.text=="string"?p.text:"",l=p.usage,h.trim()||(s=i("COMPACT_EMPTY").diagnostic)}catch(d){let p=O(d,"COMPACT_FAILED");s=p.diagnostic.code==="COMPACT_FAILED"?i("COMPACT_FAILED",Me(d)).diagnostic:p.diagnostic}return s?{summary:n.restoreText(Tt(u)),firstKeptEntryId:c.firstKeptEntryId,tokensBefore:c.tokensBefore,details:{protecter:s}}:{summary:n.restoreText(h),firstKeptEntryId:c.firstKeptEntryId,tokensBefore:c.tokensBefore,usage:l}}async function xe(o,e,n,t,r=Et,a){let{preparation:c}=o,u=await n.redact(c.entriesToSummarize,t),m=c.customInstructions===void 0?void 0:await n.redact(c.customInstructions,t),f=await be(e,a),h;try{h=await r(u,{model:f.model,apiKey:f.apiKey,headers:f.headers,env:f.env,signal:o.signal,customInstructions:m,replaceInstructions:c.replaceInstructions,streamFn:Fe(e)})}catch(s){let d=O(s,"COMPACT_FAILED");throw d.diagnostic.code==="COMPACT_FAILED"?i("COMPACT_FAILED",Me(s)):d}let l=h?.summary;if(typeof l!="string"||!l.trim())throw i("COMPACT_FAILED");return{summary:n.restoreText(l),usage:h.usage}}function Rt(o){de(j());let e=new U(St()),n=se(_t),t=!1,r,a,c=l=>{let s=_(l);return r={at:new Date().toISOString(),text:s},s};o.on("session_start",async(l,s)=>{try{if(!n)throw i("UNSUPPORTED_PI");await e.initialize(),t=!0;let d=e.ruleCount;s.ui.setStatus("protecter",g("statusBarRules",{rules:S("rules",d)})),d===0&&s.ui.notify(g("emptyRules"),"warning");let p=e.compaction;if(p.provider&&p.model)try{B(s,p),s.ui.notify(g("compactModelConfigured",{model:`${p.model} @ ${p.provider}`}),"info")}catch(A){s.ui.notify(c(O(A,"CONFIG_COMPACT_MODEL").diagnostic),"error")}}catch(d){t=!1,s.ui.setStatus("protecter",g("statusBarNotReady")),s.ui.notify(c(O(d,"CONFIG_IO").diagnostic),"error")}}),o.on("before_provider_request",async(l,s)=>{try{if(!n)throw i("UNSUPPORTED_PI");let d=await e.redact(l.payload,s.model?.provider??"unknown");return t=!0,d}catch(d){t=!1;let p=c(O(d,"INTERNAL").diagnostic);try{await s.abort()}catch{}try{s.ui.notify(p,"error")}catch{}return{}}}),o.on("message_end",l=>{if(l.message.role!=="assistant")return;let s=l.message;return{message:{...s,content:s.content.map(d=>d.type==="text"?{...d,text:e.restoreText(d.text)}:d.type==="thinking"?{...d,thinking:e.restoreText(d.thinking)}:d.type==="toolCall"?{...d,arguments:e.restore(d.arguments)}:d),...s.errorMessage?{errorMessage:e.restoreText(s.errorMessage)}:{}}}}),o.registerMarkdownTransformer((l,s)=>s.messageType==="user"?l:e.restoreText(l)),o.on("tool_call",(l,s)=>{if(!t)return{block:!0,reason:e.lastDiagnosticText??r?.text??_(i("NOT_READY").diagnostic)};let d=e.restore(l.input);for(let p of Object.keys(l.input))delete l.input[p];if(Object.assign(l.input,d),ke(l.toolName,l.input,s.cwd,e.configPath))return{block:!0,reason:g("toolBlocked")}});let u=!1,m,f=l=>e.compaction.provider??l.model?.provider??"unknown";o.on("agent_settled",(l,s)=>{if(!t||u)return;let d=e.compaction;if(d.mode==="off")return;let p=oe(s,d,m);if(!p)return;let A=s.getContextUsage(),N=A?.tokens;typeof N=="number"&&(N<=p.threshold||A&&N>A.contextWindow-p.reserve||(u=!0,s.ui.notify(g("compactEarly",{tokens:S("tokens",N)}),"info"),s.compact({onComplete:()=>{u=!1},onError:()=>{u=!1}})))});let h=async(l,s)=>{if(!n)throw i("UNSUPPORTED_PI");if(!e.ready)throw e.lastDiagnostic?i(e.lastDiagnostic.code,e.lastDiagnostic):i("NOT_READY");let d=e.compactionMode;if(d==="off")throw i("COMPACT_DENIED");if(a==="deny")throw i("COMPACT_DENIED");if(d!=="ask"||a==="allow")return;let p=[g("compactAllowOnce"),g("compactAllowAlways"),g("compactDenyOnce"),g("compactDenyAlways")],A=await l.ui.select(`${g("compactAskTitle")}
15
+ `);if(a.length>Se)throw i("AUDIT_BATCH_LIMIT",{bytes:a.length,limit:Se});let d=await Te(t),m;try{m=Z(t,!0);let f=g.fstatSync(m).size;if(f+a.length>we)throw i("AUDIT_FULL",{bytes:f+a.length,limit:we});if(f){let l=Buffer.alloc(1);if(g.readSync(m,l,0,1,f-1),l[0]!==10)throw i("AUDIT_PARTIAL")}try{let l=0;for(;l<a.length;){let u=g.writeSync(m,a,l,a.length-l,f+l);if(!u)throw i("AUDIT_IO");l+=u}g.fsyncSync(m)}catch(l){throw g.ftruncateSync(m,f),g.fsyncSync(m),G(l)}}catch(f){throw G(f)}finally{m!==void 0&&g.closeSync(m),g.rmdirSync(d)}}function De(t,e=20){if(!Number.isInteger(e)||e<1||e>100)throw i("INTERNAL",{limit:100});let o;try{try{o=Z(t,!1)}catch(c){if(c.code==="ENOENT")return{records:[],truncated:!1};throw c}let n=g.fstatSync(o).size,r=Math.max(0,n-1024*1024),a=Buffer.alloc(n-r),d=g.readSync(o,a,0,a.length,r),m=a.subarray(0,d).toString("utf8");if(r){let c=m.indexOf(`
16
+ `);m=c<0?"":m.slice(c+1)}let f=m.split(`
17
+ `),l=f.pop()!=="",u=[],s=!1;for(let c of f.slice(-e))try{let h=JSON.parse(c);if(h.version!==1||typeof h.time!="string"||typeof h.provider!="string"||typeof h.original!="string"||typeof h.replacement!="string"||typeof h.requestId!="string"||typeof h.timezoneOffset!="number")throw new Error;u.push(h)}catch{s=!0}return{records:u,truncated:r>0||f.length>e||l||s}}catch(n){throw G(n)}finally{o!==void 0&&g.closeSync(o)}}var v=class{constructor(e,o=2e3,n=_e){this.configDir=e;this.timeoutMs=o;this.machineId=n}configDir;timeoutMs;machineId;tokens=new Map;queue=Promise.resolve();workers=new Set;closed=!1;snapshot;lastError;restorePattern;restoreSize=-1;get lastDiagnostic(){return this.lastError?{...this.lastError}:void 0}get lastDiagnosticText(){return this.lastError?w(this.lastError):void 0}async initialize(){try{if(this.closed)throw i("NOT_READY");this.snapshot=void 0;let e;try{e=this.machineId()}catch{throw i("MACHINE_ID")}let o=await Ce(this.configDir,e);if(this.closed)throw i("NOT_READY");this.snapshot=o,this.lastError=void 0}catch(e){let o=O(e,"CONFIG_IO");throw this.lastError=o.diagnostic,o}}get configPath(){return this.snapshot?.path??""}get auditPath(){return this.configPath?this.configPath.replace(/\.json$/,".jsonl"):""}auditRecords(e=20){if(!this.ready)throw this.notReady();return De(this.auditPath,e)}clearAuditRecords(){let e=this.queue.then(()=>{if(!this.ready)throw this.notReady();return Re(this.auditPath)});return this.queue=e.catch(()=>{}),e}get ruleCount(){return this.snapshot?.config.sensitiveWords.length??0}get compaction(){return F(this.snapshot?.config.compaction)}get compactionMode(){return this.compaction.mode}get rawCompaction(){return this.snapshot?.config.compaction}get ready(){return!!this.snapshot&&!this.closed}get mappingCount(){return this.tokens.size}redact(e,o="unknown"){let n=this.queue.then(()=>this.scan(e,o)).catch(r=>{let a=O(r,"INTERNAL");throw this.lastError=a.diagnostic,a});return this.queue=n.catch(()=>{}),n}notReady(){return this.lastError?i(this.lastError.code,this.lastError):i("NOT_READY")}async scan(e,o){if(!this.ready)throw this.notReady();let{config:n,sourceRaws:r}=this.snapshot,a;try{a=JSON.stringify(e)}catch{throw i("PAYLOAD_JSON")}if(!a)throw i("PAYLOAD_JSON");if(Buffer.byteLength(a)>8*1024*1024)throw i("PAYLOAD_SIZE",{bytes:Buffer.byteLength(a),limit:8*1024*1024});let d;try{d=JSON.parse(a)}catch{throw i("PAYLOAD_JSON")}let m=await new Promise((f,l)=>{let u=!1,s;try{s=new mt(pt(new URL("./scan-worker.mjs",import.meta.url)),{workerData:{payload:d,rules:n.sensitiveWords,entries:[...this.tokens],configRaws:r},execArgv:[],resourceLimits:{maxOldGenerationSizeMb:128}})}catch(y){l(O(y,"WORKER_FAILED"));return}this.workers.add(s);let c=(y,A=i("WORKER_FAILED"))=>{if(!u){if(u=!0,clearTimeout(h),this.workers.delete(s),s.terminate(),!y||this.closed){l(this.closed?i("NOT_READY"):A);return}f(y)}},h=setTimeout(()=>c(void 0,i("SCAN_TIMEOUT",{timeoutMs:this.timeoutMs})),this.timeoutMs);s.once("message",y=>{y?.failed?c(void 0,M(y.diagnostic,"SCAN_INTERNAL")):y&&Array.isArray(y.additions)&&Array.isArray(y.hits)?c(y):c()}),s.once("error",()=>c()),s.once("exit",()=>c())});if(this.closed)throw i("NOT_READY");if(await Pe(this.auditPath,o,m.hits),this.closed)throw i("NOT_READY");for(let[f,l]of m.additions)this.tokens.set(f,l);return m.payload}restoreText(e){if(!this.tokens.size)return e;if(this.restoreSize!==this.tokens.size){let o=n=>n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");this.restorePattern=new RegExp([...this.tokens.keys()].sort((n,r)=>r.length-n.length).map(o).join("|"),"g"),this.restoreSize=this.tokens.size}return e.replace(this.restorePattern,o=>this.tokens.get(o)??o)}restore(e,o=0){if(o>80)throw i("RESTORE_COMPLEXITY",{node:o});if(typeof e=="string")return this.restoreText(e);if(typeof e=="number"){let n=this.tokens.get(String(e));if(n===void 0)return e;let r=Number(n);return!Number.isFinite(r)||String(r)!==n?e:r}if(Array.isArray(e))return e.map(n=>this.restore(n,o+1));if(e&&typeof e=="object"){let n=Object.create(null);for(let[r,a]of Object.entries(e)){let d=this.restoreText(r);if(Object.hasOwn(n,d))throw i("RESTORE_KEY_COLLISION");n[d]=this.restore(a,o+1)}return n}return e}close(){this.closed=!0,this.snapshot=void 0,this.tokens.clear();for(let e of this.workers)e.terminate();this.workers.clear()}};import{realpathSync as gt,statSync as Le,readdirSync as ht}from"node:fs";import{homedir as It}from"node:os";import{basename as V,dirname as L,isAbsolute as yt,relative as Me,resolve as x}from"node:path";import{fileURLToPath as Ot}from"node:url";function Nt(t,e){let o=t.replace(/^@/,"");return o.startsWith("file://")&&(o=Ot(o)),(o==="~"||o.startsWith("~/"))&&(o=It()+o.slice(1)),x(e,o)}function Q(t){try{return gt(t)}catch{let e=L(t);return e===t?t:x(Q(e),Me(e,t))}}function At(t,e){let o=Me(t,e);return o===""||!o.startsWith("..")&&!yt(o)}function ke(t,e){try{let o=Le(t),n=Le(e);return o.dev===n.dev&&o.ino===n.ino}catch{return!1}}function Fe(t,e,o,n){if(!n)return!0;let r=Q(x(n));if(["bash","powershell"].includes(t)){let m=typeof e.command=="string"?e.command:"";if(/protecter(?:\.[a-f0-9]{32})?\.jsonl?|\.protecter-migration/i.test(m)||m.includes(n))return!0}let a=e.path??e.file_path??e.filePath,d=["grep","find","ls"].includes(t);if(typeof a!="string"&&!d)return!1;try{let m=Nt(typeof a=="string"?a:".",o),f=Q(m),l=/^protecter\.[a-f0-9]{32}\.jsonl(?:\.lock)?$/,u=ht(L(r)).filter(s=>l.test(s)).some(s=>ke(m,x(L(r),s)));return L(f)===L(r)&&(X.test(V(f))||l.test(V(f))||V(f).startsWith(".protecter-migration"))||f===r||ke(m,n)||u||d&&At(f,r)}catch{return!0}}function ee(t,e){t.length=0;for(let o of e)t.push(o)}function te(t){if(!Array.isArray(t))throw i("COMPACT_FAILED");return t}function be(t){if(typeof t!="string")throw i("COMPACT_FAILED");return t}async function Ge(t,e,o){let{preparation:n}=t,r=await e.redact({messages:n.messagesToSummarize,prefix:n.turnPrefixMessages,previous:n.previousSummary??null,instructions:t.customInstructions??null},o);if(!r||typeof r!="object")throw i("COMPACT_FAILED");if(typeof t.customInstructions=="string"&&r.instructions!==t.customInstructions)throw i("COMPACT_INSTRUCTIONS");let a=te(r.messages),d=te(r.prefix),m=n.previousSummary===void 0?void 0:be(r.previous);ee(n.messagesToSummarize,a),ee(n.turnPrefixMessages,d),m!==void 0&&(n.previousSummary=m)}async function ve(t,e,o){let{preparation:n}=t,r=await e.redact({entries:n.entriesToSummarize,instructions:n.customInstructions??null},o);if(!r||typeof r!="object")throw i("COMPACT_FAILED");let a=te(r.entries),d=n.customInstructions===void 0?void 0:be(r.instructions);return ee(n.entriesToSummarize,a),d===void 0?{}:{customInstructions:d}}var xe=["stream","streamSimple"],Ct=["classify","generateImages"],Ue=xe,ne=Symbol.for("pi-information-protecter.runtime-guard");function Et(t){let e=t?.runtime;if(!e||typeof e!="object")throw i("GUARD_UNAVAILABLE");let o=e;for(let n of Ue)if(typeof o[n]!="function")throw i("GUARD_UNAVAILABLE");return o}function _t(t){return!t||typeof t!="object"?!1:t.onPayload!==void 0}function ze(t,e){let o=Et(t);o[ne]?.();let n=[...Ue,...Ct.filter(a=>typeof o[a]=="function")].map(a=>[a,o[a]]);for(let[a,d]of n){let m=xe.includes(a),f=function(l,u,s){return m&&_t(s)?d.call(this,l,u,s):(async()=>d.call(this,l,await e(l,u),s))()};o[a]=f}let r=()=>{for(let[a,d]of n)o[a]=d;delete o[ne]};return Object.defineProperty(o,ne,{value:r,configurable:!0,enumerable:!1,writable:!0}),r}function Tt(t){ue(J());let e=new v(wt()),o=se(St),n=!1,r,a,d,m=u=>{let s=u?.provider;return typeof s=="string"?s:"unknown"},f=u=>{let s=w(u);return r={at:new Date().toISOString(),text:s},s};t.on("session_start",async(u,s)=>{try{if(!o)throw i("UNSUPPORTED_PI");await e.initialize(),d=ze(s.modelRegistry,(y,A)=>e.redact(A,m(y))),n=!0;let c=e.ruleCount;s.ui.setStatus("protecter",p("statusBarRules",{rules:P("rules",c)})),c===0&&s.ui.notify(p("emptyRules"),"warning");let h=ye(e.rawCompaction);h.length&&s.ui.notify(p("compactIgnoredKeys",{keys:h.join(", ")}),"warning")}catch(c){n=!1,s.ui.setStatus("protecter",p("statusBarNotReady")),s.ui.notify(f(O(c,"CONFIG_IO").diagnostic),"error")}}),t.on("before_provider_request",async(u,s)=>{try{if(!o)throw i("UNSUPPORTED_PI");let c=await e.redact(u.payload,s.model?.provider??"unknown");return n=!0,c}catch(c){n=!1;let h=f(O(c,"INTERNAL").diagnostic);try{s.abort()}catch{}try{s.ui.notify(h,"error")}catch{}return{}}}),t.on("message_end",u=>{if(u.message.role!=="assistant")return;let s=u.message;return{message:{...s,content:s.content.map(c=>c.type==="text"?{...c,text:e.restoreText(c.text)}:c.type==="thinking"?{...c,thinking:e.restoreText(c.thinking)}:c.type==="toolCall"?{...c,arguments:e.restore(c.arguments)}:c),...s.errorMessage?{errorMessage:e.restoreText(s.errorMessage)}:{}}}}),t.registerMarkdownTransformer((u,s)=>s.messageType==="user"?u:e.restoreText(u)),t.on("tool_call",(u,s)=>{if(!n)return{block:!0,reason:e.lastDiagnosticText??r?.text??w(i("NOT_READY").diagnostic)};let c=e.restore(u.input);for(let h of Object.keys(u.input))delete u.input[h];if(Object.assign(u.input,c),Fe(u.toolName,u.input,s.cwd,e.configPath))return{block:!0,reason:p("toolBlocked")}});let l=async(u,s)=>{if(!o)throw i("UNSUPPORTED_PI");if(!e.ready)throw e.lastDiagnostic?i(e.lastDiagnostic.code,e.lastDiagnostic):i("NOT_READY");let c=e.compactionMode;if(c==="off")throw i("COMPACT_DENIED");if(a==="deny")throw i("COMPACT_DENIED");if(c!=="ask"||a==="allow")return;let h=[p("compactAllowOnce"),p("compactAllowAlways"),p("compactDenyOnce"),p("compactDenyAlways")],y=await u.ui.select(`${p("compactAskTitle")}
19
18
 
20
- ${g("compactAskBody")}`,p,{signal:s}),N=A===void 0?-1:p.indexOf(A);if(N===1?(a="allow",l.ui.notify(g("compactAlwaysNotice"),"info")):N===3&&(a="deny",l.ui.notify(g("compactDenyNotice"),"warning")),N!==0&&N!==1)throw i("COMPACT_DENIED")};o.on("session_before_compact",async(l,s)=>{let d=l.preparation.settings?.reserveTokens;typeof d=="number"&&Number.isSafeInteger(d)&&(m=d);try{await h(s,l.signal),s.ui.notify(g("compactWorking"),"info");let p=await ve(l,s,e,f(s),void 0,e.compaction),A=p.details?.protecter;return A?(s.ui.notify(g("compactDegraded"),"warning"),s.ui.notify(c(A),"warning")):s.ui.notify(g("compactDone"),"info"),{compaction:p}}catch(p){return s.ui.notify(c(O(p,"COMPACT_FAILED").diagnostic),"warning"),{cancel:!0}}}),o.on("session_before_tree",async(l,s)=>{if(l.preparation.userWantsSummary)try{await h(s,l.signal),s.ui.notify(g("compactWorking"),"info");let d=await xe(l,s,e,f(s),void 0,e.compaction);return s.ui.notify(g("compactDone"),"info"),{summary:d}}catch(d){return s.ui.notify(c(O(d,"COMPACT_FAILED").diagnostic),"warning"),{cancel:!0}}}),o.registerCommand("protecter",{description:g("commandDescription"),handler:async(l,s)=>{let d=l.trim().split(/\s+/).filter(Boolean),p=d[0]??"logs";if(!["status","reload","logs"].includes(p)||p!=="logs"&&d.length>1||d.length>2||d[1]!==void 0&&d[1]!=="clear"&&!/^(?:[1-9]\d?|100)$/.test(d[1])){s.ui.notify(g("usage"),"info");return}if(p==="logs"){if(s.mode!=="tui"){s.ui.notify(g("logsTuiOnly"),"warning");return}if(d[1]==="clear"){try{if(!e.ready)throw i("NOT_READY");if(!await s.ui.confirm(g("clearTitle"),g("clearBody")))return;await s.waitForIdle();let E=await e.clearAuditRecords();t=e.ready,s.ui.notify(g("cleared",{bytes:S("bytes",E)}),"info")}catch(E){s.ui.notify(c(O(E,"AUDIT_IO").diagnostic),"error")}return}try{let{records:E,truncated:Be}=e.auditRecords(Number(d[1]??20));if(!E.length){s.ui.notify(g("logsNone"),"info");return}let $=M=>JSON.stringify(M).replace(/[\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g,H=>`\\u${H.charCodeAt(0).toString(16).padStart(4,"0")}`),ne=E.map((M,H)=>`${H+1}. ${$(M.time)} | ${$(M.provider).slice(0,120)}`),re=await s.ui.select(g("logsSelectTitle")+(Be?g("logsTruncated"):""),ne),ie=re===void 0?-1:ne.indexOf(re);if(ie<0||!await s.ui.confirm(g("logsRevealTitle"),g("logsRevealBody")))return;let W=$(E[ie]);await s.ui.editor(g("logsPreviewTitle"),W.length>2e4?`${W.slice(0,2e4)}
21
- ${g("logsPreviewTruncated")}`:W)}catch(E){s.ui.notify(c(O(E,"AUDIT_IO").diagnostic),"error")}return}if(p==="reload"){await s.waitForIdle(),await s.reload();return}let A=e.lastDiagnosticText??r?.text,N=g("statusLine",{state:g(e.ready?"statusReady":"statusNotReady"),rules:S("rules",e.ruleCount),mappings:S("mappings",e.mappingCount)}),C=e.compaction,D=oe(s,C,m),Ge=C.provider&&C.model?`${C.model} @ ${C.provider}`:g("compactMainModel"),Ue=`
22
- ${g("statusCompaction",{mode:C.mode,target:Ge,threshold:D?S("tokens",D.threshold):g("compactNoThreshold")})}`,ze=A?`
23
- ${g("statusLastBlock",{at:r?.at??""})}
24
- ${A}`:`
25
- ${g("statusNoBlock")}`;s.ui.notify(N+Ue+ze,e.ready?"info":"error")}}),o.on("session_shutdown",()=>e.close())}export{Rt as default};
19
+ ${p("compactAskBody")}`,h,{signal:s}),A=y===void 0?-1:h.indexOf(y);if(A===1?(a="allow",u.ui.notify(p("compactAlwaysNotice"),"info")):A===3&&(a="deny",u.ui.notify(p("compactDenyNotice"),"warning")),A!==0&&A!==1)throw i("COMPACT_DENIED")};t.on("session_before_compact",async(u,s)=>{try{await l(s,u.signal),await Ge(u,e,s.model?.provider??"unknown"),s.ui.notify(p("compactRedacted"),"info");return}catch(c){return s.ui.notify(f(O(c,"COMPACT_FAILED").diagnostic),"warning"),{cancel:!0}}}),t.on("session_before_tree",async(u,s)=>{if(u.preparation.userWantsSummary)try{await l(s,u.signal);let c=await ve(u,e,s.model?.provider??"unknown");return s.ui.notify(p("compactRedacted"),"info"),c}catch(c){return s.ui.notify(f(O(c,"COMPACT_FAILED").diagnostic),"warning"),{cancel:!0}}}),t.registerCommand("protecter",{description:p("commandDescription"),handler:async(u,s)=>{let c=u.trim().split(/\s+/).filter(Boolean),h=c[0]??"logs";if(!["status","reload","logs"].includes(h)||h!=="logs"&&c.length>1||c.length>2||c[1]!==void 0&&c[1]!=="clear"&&!/^(?:[1-9]\d?|100)$/.test(c[1])){s.ui.notify(p("usage"),"info");return}if(h==="logs"){if(s.mode!=="tui"){s.ui.notify(p("logsTuiOnly"),"warning");return}if(c[1]==="clear"){try{if(!e.ready)throw i("NOT_READY");if(!await s.ui.confirm(p("clearTitle"),p("clearBody")))return;await s.waitForIdle();let C=await e.clearAuditRecords();n=e.ready,s.ui.notify(p("cleared",{bytes:P("bytes",C)}),"info")}catch(C){s.ui.notify(f(O(C,"AUDIT_IO").diagnostic),"error")}return}try{let{records:C,truncated:Be}=e.auditRecords(Number(c[1]??20));if(!C.length){s.ui.notify(p("logsNone"),"info");return}let z=k=>JSON.stringify(k).replace(/[\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g,H=>`\\u${H.charCodeAt(0).toString(16).padStart(4,"0")}`),oe=C.map((k,H)=>`${H+1}. ${z(k.time)} | ${z(k.provider).slice(0,120)}`),re=await s.ui.select(p("logsSelectTitle")+(Be?p("logsTruncated"):""),oe),ie=re===void 0?-1:oe.indexOf(re);if(ie<0||!await s.ui.confirm(p("logsRevealTitle"),p("logsRevealBody")))return;let B=z(C[ie]);await s.ui.editor(p("logsPreviewTitle"),B.length>2e4?`${B.slice(0,2e4)}
20
+ ${p("logsPreviewTruncated")}`:B)}catch(C){s.ui.notify(f(O(C,"AUDIT_IO").diagnostic),"error")}return}if(h==="reload"){await s.waitForIdle(),await s.reload();return}let y=e.lastDiagnosticText??r?.text,A=p("statusLine",{state:p(e.ready?"statusReady":"statusNotReady"),rules:P("rules",e.ruleCount),mappings:P("mappings",e.mappingCount)}),U=`
21
+ ${p("statusCompaction",{mode:e.compactionMode,guard:p(d?"guardInstalled":"guardMissing")})}`,N=y?`
22
+ ${p("statusLastBlock",{at:r?.at??""})}
23
+ ${y}`:`
24
+ ${p("statusNoBlock")}`;s.ui.notify(A+U+N,e.ready?"info":"error")}}),t.on("session_shutdown",()=>{d?.(),d=void 0,e.close()})}export{Tt as default};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-information-protecter",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Sensitive Personal Information (SPI) Protecter — local reversible privacy redaction for Pi",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -40,10 +40,10 @@
40
40
  "node": ">=22"
41
41
  },
42
42
  "peerDependencies": {
43
- "@earendil-works/pi-coding-agent": ">=0.84.0 <0.85.0 || >=0.85.1 <0.88.0"
43
+ "@earendil-works/pi-coding-agent": ">=0.84.0 <0.85.0 || >=0.85.1 <0.88.0 || >=0.99.0 <1.0.0 || >=1.2.0 <1.4.0"
44
44
  },
45
45
  "devDependencies": {
46
- "@earendil-works/pi-coding-agent": "0.86.1",
46
+ "@earendil-works/pi-coding-agent": "^1.3.0",
47
47
  "@types/node": "^22.0.0",
48
48
  "esbuild": "0.28.2",
49
49
  "tsx": "^4.20.0",