pi-information-protecter 0.7.1 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog / 更新日志
2
2
 
3
+ ## 0.8.1 — Example Bearer rule no longer matches short strings / 示例 Bearer 规则不再匹配短字符串
4
+
5
+ ### Fixed / 修复
6
+
7
+ - Require at least 16 characters in the shipped Bearer rule. `(?<=Bearer )[A-Za-z0-9._~+/-]+=*` matched a single character and ordinary words such as `token`. A short match has few shape-preserving candidates, and on a compaction payload, which holds the whole conversation, every candidate already occurs, so `/compact` failed with `SCAN_UNIQUE_FAILED`.
8
+ 示例 Bearer 规则要求至少 16 个字符。原规则会匹配单个字符和 `token` 这类普通单词;短匹配的同形候选值很少,而压缩载荷包含整个会话,每个候选值都已出现,导致 `/compact` 以 `SCAN_UNIQUE_FAILED` 失败。
9
+ - Add a regression asserting that no shipped rule matches a string of four characters or fewer.
10
+ 新增回归,断言示例中任何规则都不会匹配四个及以下字符的字符串。
11
+
12
+ ### Documentation / 文档
13
+
14
+ - Warn against rules that can match a very short string, and show a minimum-length pattern instead.
15
+ 提示避免可能匹配极短字符串的规则,并给出带最小长度的写法。
16
+
17
+ ## 0.8.0 — Protected summarization / 受保护的摘要生成
18
+
19
+ ### Added / 新增
20
+
21
+ - Support context compaction by generating the summary from redacted text. Pi builds the summarization request internally and passes no `onPayload` callback, so `before_provider_request` never fires for it; compaction was therefore cancelled outright. The extension now redacts the messages, previous summary and custom instructions, summarizes the redacted text, and restores the summary locally before it is stored.
22
+ 通过基于脱敏文本生成摘要来支持上下文压缩。Pi 在内部构造摘要请求且不传递 `onPayload` 回调,`before_provider_request` 对其不会触发,因此之前直接取消压缩。现在扩展会先脱敏消息、历史摘要和自定义指令,对脱敏文本生成摘要,并在保存前于本地还原。
23
+ - Add the `compaction` configuration field: `ask` (default) confirms each compaction, `protected` summarizes without asking, and `off` always refuses. The prompt offers allow once, allow for this session, deny once and deny for this session; dismissing it denies that compaction.
24
+ 新增 `compaction` 配置字段:`ask`(默认)每次确认,`protected` 不询问直接生成,`off` 始终拒绝。弹窗提供允许一次、本会话全部允许、拒绝一次、本会话全部拒绝;关闭对话框视为拒绝。
25
+ - Add `COMPACT_DENIED`, `COMPACT_UNAVAILABLE`, `COMPACT_FAILED` and `CONFIG_COMPACTION` diagnostics. Any compaction failure cancels instead of falling back to Pi's unredacted summarization.
26
+ 新增四个诊断码。压缩任何失败均取消,而不回退到 Pi 未脱敏的摘要流程。
27
+ - Add regressions asserting that no protected value reaches the model, that the previous summary and custom instructions are redacted too, that provider errors never carry the original, and that the mode is validated and survives configuration merging.
28
+ 新增回归:断言受保护值不会到达模型、历史摘要与自定义指令同样脱敏、提供商错误不携带原文,以及模式经校验且能在配置合并中保留。
29
+
30
+ - Protect summarized `/tree` navigation the same way, from redacted session entries, sharing the `compaction` setting and the same prompt. Navigation without a summary sends nothing and is never gated.
31
+ 以同样方式保护带摘要的 `/tree` 导航:基于脱敏后的会话条目生成,并共用 `compaction` 设置与同一弹窗。不生成摘要的导航不发送任何内容,也不会被拦截。
32
+
3
33
  ## 0.7.1 — Install the latest release by default / 默认安装最新版本
4
34
 
5
35
  ### Documentation / 文档
package/README.md CHANGED
@@ -67,6 +67,7 @@ More examples are in [`protecter.example.json`](protecter.example.json), coverin
67
67
  - Overlapping matches are merged to avoid exposing a longer secret's suffix. Equal values reuse a token within the extension instance.
68
68
  - Learned originals remain exact-match protected within the instance even if regex context disappears or a rule is removed. Exit/reload clears these records.
69
69
  - `/protecter` opens the local audit viewer; `/protecter logs 50` shows up to 50 recent records. `/protecter status` reports memory counts without reading configuration. `/protecter reload` invokes Pi's full reload after idle. Do not enter secrets as command arguments.
70
+ - Avoid rules that can match a very short string. A short match has few shape-preserving candidates, and on a large payload such as a compaction request every candidate already occurs elsewhere, so the request fails with `SCAN_UNIQUE_FAILED`. Require a realistic minimum length, for example `[A-Za-z0-9._~+/-]{16,}` rather than `+`.
70
71
  - Limits: 1000 rules, 8192 characters per literal/pattern, 2-second scan timeout and 8 MiB request JSON. Exceeding limits rejects the request rather than sending plaintext.
71
72
 
72
73
  ### Fixed replacement values
@@ -132,6 +133,40 @@ Replacements are checked so restoration stays exact: a candidate is rejected if
132
133
 
133
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.
134
135
 
136
+ ### Context compaction
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.
139
+
140
+ Instead the extension produces the summary itself:
141
+
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.
145
+
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`.
147
+
148
+ Set `compaction` in the local configuration:
149
+
150
+ | Value | Behaviour |
151
+ | --- | --- |
152
+ | `ask` (default) | Confirm each compaction, then summarize redacted text |
153
+ | `protected` | Summarize redacted text without asking |
154
+ | `off` | Always refuse; use `/new` for long sessions |
155
+
156
+ ```json
157
+ {
158
+ "version": 1,
159
+ "compaction": "protected",
160
+ "sensitiveWords": []
161
+ }
162
+ ```
163
+
164
+ 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
+
166
+ Two costs are unavoidable: the extra summarization request consumes tokens, and the model reasons about replacements, so the summary can describe protected values inaccurately.
167
+
168
+ 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.
169
+
135
170
  ### Multi-turn consistency and prompt caching
136
171
 
137
172
  Random placeholders are generated once per exact original and reused within the same extension instance, including after local response restoration. Starting another scan worker does not reset the mapping. This supports stable prompt prefixes but does not guarantee provider KV-cache hits; model, tools, message ordering, cache lifetime and routing also matter. `/reload`, process restart and session-instance replacement clear mappings. Different processes do not share mappings, and audit timestamps/request IDs are never included in the model payload.
@@ -198,7 +233,7 @@ Local response and tool-argument restoration
198
233
  ## Limitations — read first
199
234
 
200
235
  - 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.
201
- - `/compact`, automatic compaction and summarized `/tree` navigation are cancelled pending separate lifecycle/restoration verification. Tree navigation without summaries works; use `/new` for long sessions.
236
+ - 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).
202
237
  - 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.
203
238
  - 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.
204
239
  - Reused tokens reveal equality relationships; context may imply identity. This is not formal anonymization.
@@ -290,6 +325,7 @@ pi update npm:pi-information-protecter
290
325
  - 合并重叠匹配,避免暴露长敏感值的后缀;同一实例内相同原文复用占位符。
291
326
  - 已识别原文在当前实例内持续受到精确匹配保护,即使正则上下文消失或规则被删除;退出或重载清除记录。
292
327
  - `/protecter` 打开本地审计查看器,`/protecter logs 50` 查看最近最多 50 条;`/protecter status` 只报告内存数量,不读取配置。`/protecter reload` 等待空闲后完整重载。不要在命令参数中填写秘密。
328
+ - 避免可能匹配极短字符串的规则。短匹配的同形候选值很少,而在压缩请求这类大载荷下每个候选值都已出现在别处,请求会以 `SCAN_UNIQUE_FAILED` 失败。请设定合理的最小长度,例如用 `[A-Za-z0-9._~+/-]{16,}` 而非 `+`。
293
329
  - 限制为 1000 条规则、每个词或模式 8192 字符、扫描 2 秒、请求 JSON 8 MiB;超限拒绝,不降级发送明文。
294
330
 
295
331
  ### 固定替换值
@@ -355,6 +391,40 @@ pi update npm:pi-information-protecter
355
391
 
356
392
  所有通知、提示和诊断仅以**单一语言**呈现,语言按环境变量依次选取:`PI_PROTECTER_LANG`、`LC_ALL`、`LC_MESSAGES`、`LANG`、`LANGUAGE`,最后是运行时区域。`zh*` 选择简体中文,其余回退英文。可设置 `PI_PROTECTER_LANG=en` 或 `PI_PROTECTER_LANG=zh` 覆盖。错误码、阶段和数值细节保持语言无关,便于搜索和反馈。
357
393
 
394
+ ### 上下文压缩
395
+
396
+ Pi 在自己的压缩代码中构造摘要请求并直接发给提供商。该请求不携带 `onPayload` 回调,因此 **`before_provider_request` 根本不会触发**,本扩展无法对其脱敏。直接交给 Pi 生成摘要,等于把存有还原明文的会话内容完整发给提供商。
397
+
398
+ 因此改由扩展自行生成摘要:
399
+
400
+ 1. 将 `preparation.messagesToSummarize`、历史摘要和自定义指令,通过与出站请求相同的限时 worker 脱敏。
401
+ 2. 基于脱敏后的文本生成摘要。
402
+ 3. 摘要在保存前于本地还原,与助手文本一致;下一次出站请求会再次脱敏。
403
+
404
+ 任何失败都取消压缩,而不回退到 Pi 未脱敏的路径,并报出 `COMPACT_UNAVAILABLE`(无可用模型或无法获取凭证)或 `COMPACT_FAILED`。
405
+
406
+ 在本地配置中设置 `compaction`:
407
+
408
+ | 取值 | 行为 |
409
+ | --- | --- |
410
+ | `ask`(默认) | 每次压缩先确认,再对脱敏文本生成摘要 |
411
+ | `protected` | 不询问,直接对脱敏文本生成摘要 |
412
+ | `off` | 始终拒绝;长会话请使用 `/new` |
413
+
414
+ ```json
415
+ {
416
+ "version": 1,
417
+ "compaction": "protected",
418
+ "sensitiveWords": []
419
+ }
420
+ ```
421
+
422
+ 弹窗提供四个选项:允许一次、本会话内全部允许、拒绝一次、本会话内全部拒绝。会话级选择在重载或退出前有效;配置中的 `compaction` 才是永久设置。关闭对话框视为拒绝本次压缩。
423
+
424
+ 有两项代价无法避免:额外的摘要请求会消耗 token;模型推理的是替换值,摘要对受保护值的描述可能不准确。
425
+
426
+ 带摘要的 `/tree` 导航走同一条内部路径,采用相同保护方式,并共用同一设置与弹窗。不生成摘要的导航不发送任何内容,也不会被拦截。
427
+
358
428
  ### 多轮一致性与提示词缓存
359
429
 
360
430
  随机占位符按精确原文首次生成,在同一扩展实例内持续复用,包括本地回复还原后的再次脱敏;新建扫描 worker 不会重置映射。这有助于前缀稳定,但不保证提供商 KV Cache 命中,模型、工具、消息顺序、缓存有效期和路由也有影响。重载、进程重启和会话实例替换会清除映射;不同进程不共享映射,审计时间和请求 ID 不进入模型请求体。
@@ -421,7 +491,7 @@ before_provider_request:最终 JSON 文本扫描
421
491
  ## 当前限制——请先阅读
422
492
 
423
493
  - 仅保护命中规则的文本,不覆盖改写、拆字和任意编码。常见多模态附件因无法可靠审查而拒绝整个请求,不提供 OCR。
424
- - 暂时取消压缩和带摘要的树导航,等待单独验证生命周期及还原;不带摘要的导航可用,长会话请使用 `/new`。
494
+ - 压缩与带摘要的 `/tree` 导航均改为在本地对脱敏文本生成摘要,因为 Pi 在内部构造这些请求,不经过 `before_provider_request`。不带摘要的导航不发送任何内容。参见[上下文压缩](#上下文压缩)。
425
495
  - 退出、重载和切换会话会丢弃映射。最终已还原消息仍可读,但崩溃残留、旧摘要和原始增量无法跨重启恢复占位符。
426
496
  - 过宽规则可能改写协议字段、模型名、工具结构和 ID,导致请求失败;命中数字变为占位符字符串,应优先使用精确规则。
427
497
  - 复用占位符会暴露值相等的关系,上下文也可能揭示身份,这不是形式化匿名化。
package/SECURITY.md CHANGED
@@ -43,8 +43,10 @@ Prevent configured personal information from accidentally entering Pi LLM reques
43
43
  启动或重载时校验并合并机器哈希配置后缓存,请求只使用内存;文件删除不撤销缓存秘密,重载或退出才清理实例。迁移采用先提交后删除及协作进程锁,并非多文件事务或恶意本地写入防护;残留锁需本地检查,Windows 无 POSIX 目录同步。
44
44
  - Whole-configuration text detection does not guarantee that every reformatted rule definition is masked.
45
45
  整体配置文本检测不保证所有重新格式化的规则定义都被遮盖。
46
- - Compaction and tree summaries are conservatively disabled. 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.
47
- 保守禁用压缩和树摘要,未经验证不扩大 Pi 兼容范围:误拒可用版本会拦截所有请求,误受损坏版本则可能发送明文。每个受支持版本均以完整测试验证,并保持运行时门禁与声明的 peer 范围一致。
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
50
 
49
51
  ## Fixed aliases and caching / 固定别名与缓存
50
52
 
package/dist/index.mjs CHANGED
@@ -1,13 +1,23 @@
1
1
  /*! SPI Protecter | MIT | see LICENSE / SPI 隐私保护插件 | MIT 许可 */
2
- import{getAgentDir as tt,VERSION as rt}from"@earendil-works/pi-coding-agent";import{Worker as We}from"node:worker_threads";import{fileURLToPath as Xe}from"node:url";import*as d from"node:fs";import{join as C,resolve as Me}from"node:path";import{createHash as ke,randomUUID as Ue}from"node:crypto";import{Worker as ve}from"node:worker_threads";import{fileURLToPath as xe}from"node:url";var z="0.84.0-0.84.x, 0.85.1-0.87.x";function Z(n){let e=/^(\d+)\.(\d+)\.(\d+)/.exec(n);if(!e)return!1;let[r,t,o]=e.slice(1).map(Number);return r!==0?!1:t===84?!0:t===85?o>=1:t===86||t===87}var H={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 ${z}`],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"],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"]},Ae={INTERNAL:["未预期的本地错误","重载并检查安装包完整性"],NOT_READY:["防护尚未初始化","查看初始化错误,修复后重载"],UNSUPPORTED_PI:["Pi 版本不受支持",`请使用 Pi ${z}`],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:["还原后对象键重复","使用更具体的规则或固定替换目标,避免与结构键冲突"],AUDIT_BATCH_LIMIT:["审计批次超过上限","减少单个请求的命中内容"],AUDIT_FULL:["审计日志将超过上限","检查记录后清空日志或本地归档"],AUDIT_PARTIAL:["审计日志末行不完整","本地检查或清空日志,不要上传日志"],AUDIT_LOCKED:["审计锁持续被占用","等待写入完成,全部停止后再检查残留锁"],AUDIT_UNSAFE:["审计文件不安全","使用本人普通文件且不含链接,不会绕过检查"],AUDIT_IO:["审计文件操作失败","检查错误码、权限和磁盘空间,不暴露路径或内容"]},V={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.",compactUnsupported:"SPI Protecter does not support remote context compaction yet; use /new to start a new session."},Se={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 已阻止工具访问受保护的配置或审计日志,请在本地编辑。",compactUnsupported:"SPI Protecter 暂不支持远程上下文压缩,请使用 /new 开始新会话。"};function J(n=process.env){let e=n.PI_PROTECTER_LANG||n.LC_ALL||n.LC_MESSAGES||n.LANG||n.LANGUAGE||Te();return/^zh\b|^zh[-_]/i.test(e.trim())?"zh":"en"}function Te(){try{return new Intl.DateTimeFormat().resolvedOptions().locale}catch{return""}}var _=J();function ee(){return _}function te(n){_=n}function m(n,e={}){return((_==="zh"?Se:V)[n]??V[n]).replace(/\{(\w+)\}/g,(t,o)=>Object.hasOwn(e,o)?String(e[o]):t)}var Ce={en:{rules:{one:"{n} rule",other:"{n} rules"},mappings:{one:"{n} mapping",other:"{n} mappings"},bytes:{one:"{n} byte",other:"{n} bytes"}},zh:{rules:{other:"规则 {n} 条"},mappings:{other:"映射 {n} 个"},bytes:{other:"{n} 字节"}}},Q=new Map;function R(n,e){let r=_==="zh"?"zh-CN":"en",t=Q.get(_);if(!t){try{t=new Intl.PluralRules(r)}catch{t=void 0}t&&Q.set(_,t)}let o=t?.select(e)??"other",s=Ce[_][n],l=s[o]??s.other,i=new Intl.NumberFormat(r).format(e);return l.replace("{n}",i)}function re(n){let r=(_==="zh"?Ae[n]:H[n])??H[n]??H.INTERNAL;return{reason:r[0],action:r[1]}}var ne={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",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",AUDIT_BATCH_LIMIT:"audit",AUDIT_FULL:"audit",AUDIT_PARTIAL:"audit",AUDIT_LOCKED:"audit",AUDIT_UNSAFE:"audit",AUDIT_IO:"audit"},oe=["rule","otherRule","node","bytes","limit","timeoutMs"],we=/^(EACCES|EPERM|ENOENT|ENOSPC|EDQUOT|EROFS|ELOOP|EISDIR|ENOTDIR|EMFILE|ENFILE|EIO|EEXIST|EBADF|EBUSY|ENOTEMPTY)$/;function Re(n,e={}){let{reason:r,action:t}=re(n),o={};for(let s of oe){let l=e[s];typeof l=="number"&&Number.isSafeInteger(l)&&l>=0&&(o[s]=l)}return typeof e.errno=="string"&&we.test(e.errno)&&(o.errno=e.errno),{code:n,stage:ne[n],reason:r,action:t,...o}}function S(n){let e=ee()==="zh"?":":": ",r=[...oe,"errno"].filter(t=>n[t]!==void 0).map(t=>`${t}=${n[t]}`).join(" ");return[m("blockedHeader"),`code=${n.code} stage=${n.stage}${r?` ${r}`:""}`,`${m("reasonLabel")}${e}${n.reason}`,`${m("actionLabel")}${e}${n.action}`,`${m("detailsLabel")}${e}/protecter status`].join(`
3
- `)}var A=class extends Error{diagnostic;constructor(e,r={}){let t=Re(e,r);super(S(t)),this.name="ProtectionError",this.diagnostic=t}};function a(n,e={}){return new A(n,e)}function y(n,e,r={}){if(n instanceof A)return a(n.diagnostic.code,{...n.diagnostic,...r});let t=n&&typeof n=="object"&&"code"in n?n.code:void 0;return a(e,{...r,errno:typeof t=="string"?t:void 0})}function b(n,e){if(!n||typeof n!="object")return a(e);let r=n,t=typeof r.code=="string"&&Object.hasOwn(ne,r.code)?r.code:e;return a(t,r)}import{constants as T,closeSync as ie,fchmodSync as De,fstatSync as Le,mkdirSync as Pe,openSync as se,readFileSync as Fe,writeFileSync as be}from"node:fs";import{dirname as Ge}from"node:path";var B={version:1,sensitiveWords:[]};function $(n){let e;try{e=JSON.parse(n)}catch{throw a("CONFIG_JSON")}let r=0;try{if(!e||e.version!==1||!Array.isArray(e.sensitiveWords)||Object.keys(e).some(o=>!["version","sensitiveWords"].includes(o)))throw a("CONFIG_SCHEMA");if(e.sensitiveWords.length>1e3)throw a("CONFIG_SIZE",{limit:1e3});let t=new Map;for(let o of e.sensitiveWords){if(r++,typeof o=="string"){if(!o||o.length>8192)throw a("CONFIG_SCHEMA",{rule:r,limit:8192});let l=JSON.stringify(["literal",o]);if(t.has(l)&&t.get(l)!==void 0)throw a("CONFIG_CONFLICT",{rule:r});t.set(l,void 0);continue}if(!o||typeof o!="object")throw a("CONFIG_SCHEMA",{rule:r});if(Object.hasOwn(o,"replacement")&&(typeof o.replacement!="string"||!o.replacement.trim()||o.replacement.length>8192||o.replacement.includes("__PIP_")))throw a("CONFIG_TARGET",{rule:r,limit:8192});if(o.type==="literal"){if(Object.keys(o).some(l=>!["type","value","replacement"].includes(l))||typeof o.value!="string"||!o.value||o.value.length>8192)throw a("CONFIG_SCHEMA",{rule:r,limit:8192})}else if(o.type==="regex"){if(Object.keys(o).some(l=>!["type","pattern","flags","replacement"].includes(l))||typeof o.pattern!="string"||!o.pattern||o.pattern.length>8192||o.flags!==void 0&&(typeof o.flags!="string"||!/^[gimsu]*$/.test(o.flags)))throw a("CONFIG_REGEX",{rule:r,limit:8192});try{new RegExp(o.pattern,o.flags??"")}catch{throw a("CONFIG_REGEX",{rule:r})}}else throw a("CONFIG_SCHEMA",{rule:r});let s=o.type==="literal"?JSON.stringify(["literal",o.value]):JSON.stringify(["regex",o.pattern,[...new Set((o.flags??"")+"g")].sort().join("")]);if(t.has(s)&&t.get(s)!==o.replacement)throw a("CONFIG_CONFLICT",{rule:r});t.set(s,o.replacement)}return e}catch(t){throw t instanceof A?t:a("CONFIG_SCHEMA",{rule:r})}}function ae(n,e=!1){let r;try{if(e){Pe(Ge(n),{recursive:!0,mode:448});try{let s=se(n,T.O_WRONLY|T.O_CREAT|T.O_EXCL,384);try{be(s,JSON.stringify(B,null,2)+`
4
- `)}finally{ie(s)}}catch(s){if(s.code!=="EEXIST")throw y(s,"CONFIG_IO")}}r=se(n,T.O_RDONLY|T.O_NOFOLLOW|T.O_NONBLOCK);let t=Le(r);if(t.size>1024*1024)throw a("CONFIG_SIZE",{bytes:t.size,limit:1024*1024});if(!t.isFile()||t.nlink!==1||process.getuid&&t.uid!==process.getuid())throw a("CONFIG_UNSAFE");process.platform!=="win32"&&De(r,384);let o=Fe(r,"utf8");if(Buffer.byteLength(o)>1024*1024)throw a("CONFIG_SIZE",{bytes:Buffer.byteLength(o),limit:1024*1024});return{config:$(o),raw:o}}catch(t){let o=t&&typeof t=="object"&&"code"in t?t.code:void 0;throw y(t,o==="ELOOP"||o==="EISDIR"||o==="ENOTDIR"?"CONFIG_UNSAFE":"CONFIG_IO")}finally{r!==void 0&&ie(r)}}var W=/^protecter(?:\.[a-f0-9]{32})?\.json$/;function ze(n){return new Promise((e,r)=>{let t=new ve(xe(new URL("./validate-worker.mjs",import.meta.url)),{workerData:n,execArgv:[],resourceLimits:{maxOldGenerationSizeMb:64}}),o=!1,s=(i,c=a("WORKER_FAILED"))=>{o||(o=!0,clearTimeout(l),t.terminate(),i?e():r(c))},l=setTimeout(()=>s(!1,a("CONFIG_TIMEOUT",{timeoutMs:2e3})),2e3);t.once("message",i=>i===!0?s(!0):s(!1,b(i,"CONFIG_SCHEMA"))),t.once("error",()=>s(!1)),t.once("exit",()=>s(!1))})}function He(n){let e=new Set,r=[];for(let o of n)for(let s of o.sensitiveWords){let l=typeof s=="string"?JSON.stringify(["literal",s,null]):s.type==="literal"?JSON.stringify(["literal",s.value,s.replacement??null]):JSON.stringify(["regex",s.pattern,[...new Set((s.flags??"")+"g")].sort().join(""),s.replacement??null]);e.has(l)||(e.add(l),r.push(s))}let t=JSON.stringify({version:1,sensitiveWords:r});if(Buffer.byteLength(t)>1024*1024)throw a("CONFIG_SIZE",{bytes:Buffer.byteLength(t),limit:1024*1024});return $(t)}function ce(n){let e;try{e=d.lstatSync(n,{bigint:!0})}catch(r){throw y(r,"CONFIG_IO")}if(!e.isFile()||e.nlink!==1n||process.getuid&&e.uid!==BigInt(process.getuid()))throw a("CONFIG_UNSAFE");return`${e.dev}:${e.ino}:${e.size}:${e.mtimeNs}`}function Y(n,e){let r=C(n,e),t=ce(r),o=ae(r);if(ce(r)!==t)throw a("MIGRATION_CHANGED");return{name:e,...o,identity:t,digest:ke("sha256").update(o.raw).digest("hex")}}function D(n,e){let r=Y(n,e.name);if(r.identity!==e.identity||r.digest!==e.digest)throw a("MIGRATION_CHANGED")}function G(n){return d.readdirSync(n).filter(e=>W.test(e)).sort()}function le(n){if(process.platform==="win32")return;let e=d.openSync(n,d.constants.O_RDONLY);try{d.fsyncSync(e)}finally{d.closeSync(e)}}async function ue(n,e,r={}){if(!/^[a-f0-9]{32}$/.test(e))throw a("MACHINE_ID");let t=Me(n),o=`protecter.${e}.json`,s=C(t,o);d.mkdirSync(t,{recursive:!0,mode:448});let l=C(t,".protecter-migration.lock"),i=Date.now()+(r.lockTimeoutMs??5e3);for(;;)try{d.mkdirSync(l,{mode:448});break}catch(u){if(u.code!=="EEXIST")throw y(u,"MIGRATION_IO");if(Date.now()>=i)throw a("MIGRATION_LOCKED",{timeoutMs:r.lockTimeoutMs??5e3});await new Promise(O=>setTimeout(O,50))}let c;try{let u=G(t);if(u.length>64)throw a("MIGRATION_LIMIT",{limit:64});let g=[...u].sort((I,w)=>I===o?-1:w===o?1:I.localeCompare(w)).map(I=>Y(t,I)),h=He(g.length?g.map(I=>I.config):[B]);if(await ze([h]),g.length===1&&g[0].name===o){if(D(t,g[0]),JSON.stringify(G(t))!==JSON.stringify(u))throw a("MIGRATION_CHANGED");return{path:s,config:g[0].config,raw:g[0].raw,sourceRaws:[g[0].raw],migrated:0}}let p=JSON.stringify(h,null,2)+`
5
- `;if(Buffer.byteLength(p)>1024*1024)throw a("CONFIG_SIZE",{bytes:Buffer.byteLength(p),limit:1024*1024});c=C(t,`.protecter-migration.${Ue()}.tmp`);let N=d.openSync(c,d.constants.O_WRONLY|d.constants.O_CREAT|d.constants.O_EXCL,384);try{d.writeFileSync(N,p),d.fsyncSync(N)}finally{d.closeSync(N)}if(r.checkpoint?.("beforeCommit",s),JSON.stringify(G(t))!==JSON.stringify(u))throw a("MIGRATION_CHANGED");for(let I of g)D(t,I);d.renameSync(c,s),c=void 0,le(t);let E=Y(t,o);if(E.raw!==p)throw a("MIGRATION_CHANGED");r.checkpoint?.("afterCommit",s);for(let I of g)I.name!==o&&(r.checkpoint?.("beforeDelete",C(t,I.name)),D(t,E),D(t,I),d.unlinkSync(C(t,I.name)));le(t);let P=[o];if(JSON.stringify(G(t))!==JSON.stringify(P))throw a("MIGRATION_CHANGED");return D(t,E),{path:s,config:h,raw:p,sourceRaws:[...new Set([...g.map(I=>I.raw),p])],migrated:g.filter(I=>I.name!==o).length}}catch(u){throw u instanceof A?u:y(u,"MIGRATION_IO")}finally{if(c)try{d.unlinkSync(c)}catch{}d.rmdirSync(l)}}import{createHash as Je}from"node:crypto";import{readFileSync as Be}from"node:fs";import{execFileSync as fe}from"node:child_process";function X(n){let e=n.trim().toLowerCase().replace(/-/g,"");if(!/^[a-f0-9]{32}$/.test(e)||/^0+$/.test(e))throw a("MACHINE_ID");return Je("sha256").update(`pi-information-protecter:v1:${e}`).digest("hex").slice(0,32)}function de(){try{if(process.platform==="linux")return X(Be("/etc/machine-id","utf8"));if(process.platform==="darwin"){let n=fe("/usr/sbin/ioreg",["-rd1","-c","IOPlatformExpertDevice"],{encoding:"utf8",timeout:5e3,maxBuffer:1048576,stdio:["ignore","pipe","pipe"]});return X(n.match(/"IOPlatformUUID"\s*=\s*"([a-fA-F0-9-]+)"/)?.[1]??"")}if(process.platform==="win32"){let n=fe("reg.exe",["query","HKLM\\SOFTWARE\\Microsoft\\Cryptography","/v","MachineGuid","/reg:64"],{encoding:"utf8",timeout:5e3,maxBuffer:65536,stdio:["ignore","pipe","pipe"]});return X(n.match(/MachineGuid\s+REG_SZ\s+([a-fA-F0-9-]+)/i)?.[1]??"")}}catch{}throw a("MACHINE_ID")}import*as f from"node:fs";import{randomUUID as $e}from"node:crypto";var ge=32*1024*1024,me=8*1024*1024;function Ye(n){let e=r=>String(r).padStart(2,"0");return`${n.getFullYear()}-${e(n.getMonth()+1)}-${e(n.getDate())} ${e(n.getHours())}:${e(n.getMinutes())}:${e(n.getSeconds())}`}function j(n,e,r=e){let t=f.openSync(n,(e?f.constants.O_RDWR|(r?f.constants.O_CREAT:0):f.constants.O_RDONLY)|f.constants.O_NOFOLLOW|f.constants.O_NONBLOCK,384);try{let o=f.fstatSync(t);if(!o.isFile()||o.nlink!==1||process.getuid&&o.uid!==process.getuid())throw a("AUDIT_UNSAFE");return process.platform!=="win32"&&f.fchmodSync(t,384),t}catch(o){throw f.closeSync(t),y(o,"AUDIT_UNSAFE")}}function M(n){let e=n&&typeof n=="object"&&"code"in n?n.code:void 0;return y(n,e==="ELOOP"||e==="EISDIR"||e==="ENOTDIR"?"AUDIT_UNSAFE":"AUDIT_IO")}async function pe(n){let e=`${n}.lock`,r=Date.now()+2e3;for(;;)try{return f.mkdirSync(e,{mode:448}),e}catch(t){if(t.code!=="EEXIST")throw y(t,"AUDIT_IO");if(Date.now()>=r)throw a("AUDIT_LOCKED",{timeoutMs:2e3});await new Promise(o=>setTimeout(o,25))}}async function he(n){let e=await pe(n),r;try{try{r=j(n,!0,!1)}catch(o){if(o.code==="ENOENT")return 0;throw o}let t=f.fstatSync(r).size;return f.ftruncateSync(r,0),f.fsyncSync(r),t}catch(t){throw M(t)}finally{try{r!==void 0&&f.closeSync(r)}finally{f.rmdirSync(e)}}}async function Ie(n,e,r){if(!r.length)return;let t=new Date,o=$e(),s=Buffer.from(r.map(([c,u])=>JSON.stringify({version:1,time:Ye(t),timezoneOffset:-t.getTimezoneOffset(),requestId:o,provider:e||"unknown",original:u,replacement:c})).join(`
2
+ import{getAgentDir as ut,VERSION as dt}from"@earendil-works/pi-coding-agent";import{Worker as Qe}from"node:worker_threads";import{fileURLToPath as et}from"node:url";import*as p from"node:fs";import{join as T,resolve as He}from"node:path";import{createHash as $e,randomUUID as Je}from"node:crypto";import{Worker as We}from"node:worker_threads";import{fileURLToPath as Ye}from"node:url";var z="0.84.0-0.84.x, 0.85.1-0.87.x";function Q(n){let e=/^(\d+)\.(\d+)\.(\d+)/.exec(n);if(!e)return!1;let[r,t,o]=e.slice(1).map(Number);return r!==0?!1:t===84?!0:t===85?o>=1:t===86||t===87}var B={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 ${z}`],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"],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"]},De={INTERNAL:["未预期的本地错误","重载并检查安装包完整性"],NOT_READY:["防护尚未初始化","查看初始化错误,修复后重载"],UNSUPPORTED_PI:["Pi 版本不受支持",`请使用 Pi ${z}`],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:["还原后对象键重复","使用更具体的规则或固定替换目标,避免与结构键冲突"],AUDIT_BATCH_LIMIT:["审计批次超过上限","减少单个请求的命中内容"],AUDIT_FULL:["审计日志将超过上限","检查记录后清空日志或本地归档"],AUDIT_PARTIAL:["审计日志末行不完整","本地检查或清空日志,不要上传日志"],AUDIT_LOCKED:["审计锁持续被占用","等待写入完成,全部停止后再检查残留锁"],AUDIT_UNSAFE:["审计文件不安全","使用本人普通文件且不含链接,不会绕过检查"],AUDIT_IO:["审计文件操作失败","检查错误码、权限和磁盘空间,不暴露路径或内容"]},ee={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.
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.
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.",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."},Pe={compactAskTitle:"是否压缩当前会话?",compactAskBody:`Pi 在内部构造摘要请求,不经过本扩展的脱敏环节,因此直接让 Pi 生成摘要会将会话内容(包括受保护的值)以明文发送。
7
+
8
+ 在此处允许**不会**这样做:先对文本脱敏,再用脱敏后的内容生成摘要,最后在本地还原。这次额外请求会消耗 token;且模型推理的是替换值,摘要对受保护值的描述可能不准确。
9
+
10
+ 若选择允许,本次请求不会被拦截。`,compactAllowOnce:"允许一次",compactAllowAlways:"本会话内全部允许",compactDenyOnce:"拒绝一次",compactDenyAlways:"本会话内全部拒绝",compactWorking:"SPI Protecter:正在对脱敏内容生成摘要…",compactDone:"SPI Protecter:已基于脱敏内容生成压缩摘要。",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 H(n=process.env){let e=n.PI_PROTECTER_LANG||n.LC_ALL||n.LC_MESSAGES||n.LANG||n.LANGUAGE||Le();return/^zh\b|^zh[-_]/i.test(e.trim())?"zh":"en"}function Le(){try{return new Intl.DateTimeFormat().resolvedOptions().locale}catch{return""}}var E=H();function re(){return E}function ne(n){E=n}function m(n,e={}){return((E==="zh"?Pe:ee)[n]??ee[n]).replace(/\{(\w+)\}/g,(t,o)=>Object.hasOwn(e,o)?String(e[o]):t)}var Fe={en:{rules:{one:"{n} rule",other:"{n} rules"},mappings:{one:"{n} mapping",other:"{n} mappings"},bytes:{one:"{n} byte",other:"{n} bytes"}},zh:{rules:{other:"规则 {n} 条"},mappings:{other:"映射 {n} 个"},bytes:{other:"{n} 字节"}}},te=new Map;function R(n,e){let r=E==="zh"?"zh-CN":"en",t=te.get(E);if(!t){try{t=new Intl.PluralRules(r)}catch{t=void 0}t&&te.set(E,t)}let o=t?.select(e)??"other",c=Fe[E][n],u=c[o]??c.other,d=new Intl.NumberFormat(r).format(e);return u.replace("{n}",d)}function oe(n){let r=(E==="zh"?De[n]:B[n])??B[n]??B.INTERNAL;return{reason:r[0],action:r[1]}}var ie={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",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",AUDIT_BATCH_LIMIT:"audit",AUDIT_FULL:"audit",AUDIT_PARTIAL:"audit",AUDIT_LOCKED:"audit",AUDIT_UNSAFE:"audit",AUDIT_IO:"audit"},se=["rule","otherRule","node","bytes","limit","timeoutMs"],Me=/^(EACCES|EPERM|ENOENT|ENOSPC|EDQUOT|EROFS|ELOOP|EISDIR|ENOTDIR|EMFILE|ENFILE|EIO|EEXIST|EBADF|EBUSY|ENOTEMPTY)$/;function ke(n,e={}){let{reason:r,action:t}=oe(n),o={};for(let c of se){let u=e[c];typeof u=="number"&&Number.isSafeInteger(u)&&u>=0&&(o[c]=u)}return typeof e.errno=="string"&&Me.test(e.errno)&&(o.errno=e.errno),{code:n,stage:ie[n],reason:r,action:t,...o}}function _(n){let e=re()==="zh"?":":": ",r=[...se,"errno"].filter(t=>n[t]!==void 0).map(t=>`${t}=${n[t]}`).join(" ");return[m("blockedHeader"),`code=${n.code} stage=${n.stage}${r?` ${r}`:""}`,`${m("reasonLabel")}${e}${n.reason}`,`${m("actionLabel")}${e}${n.action}`,`${m("detailsLabel")}${e}/protecter status`].join(`
11
+ `)}var S=class extends Error{diagnostic;constructor(e,r={}){let t=ke(e,r);super(_(t)),this.name="ProtectionError",this.diagnostic=t}};function s(n,e={}){return new S(n,e)}function I(n,e,r={}){if(n instanceof S)return s(n.diagnostic.code,{...n.diagnostic,...r});let t=n&&typeof n=="object"&&"code"in n?n.code:void 0;return s(e,{...r,errno:typeof t=="string"?t:void 0})}function F(n,e){if(!n||typeof n!="object")return s(e);let r=n,t=typeof r.code=="string"&&Object.hasOwn(ie,r.code)?r.code:e;return s(t,r)}import{constants as C,closeSync as ae,fchmodSync as be,fstatSync as xe,mkdirSync as Ge,openSync as ce,readFileSync as Ue,writeFileSync as ve}from"node:fs";import{dirname as ze}from"node:path";var Be=["ask","protected","off"],$={version:1,sensitiveWords:[]},le="ask";function J(n){let e;try{e=JSON.parse(n)}catch{throw s("CONFIG_JSON")}let r=0;try{if(!e||e.version!==1||!Array.isArray(e.sensitiveWords)||Object.keys(e).some(o=>!["version","sensitiveWords","compaction"].includes(o)))throw s("CONFIG_SCHEMA");if(Object.hasOwn(e,"compaction")&&!Be.includes(e.compaction))throw s("CONFIG_COMPACTION");if(e.sensitiveWords.length>1e3)throw s("CONFIG_SIZE",{limit:1e3});let t=new Map;for(let o of e.sensitiveWords){if(r++,typeof o=="string"){if(!o||o.length>8192)throw s("CONFIG_SCHEMA",{rule:r,limit:8192});let u=JSON.stringify(["literal",o]);if(t.has(u)&&t.get(u)!==void 0)throw s("CONFIG_CONFLICT",{rule:r});t.set(u,void 0);continue}if(!o||typeof o!="object")throw s("CONFIG_SCHEMA",{rule:r});if(Object.hasOwn(o,"replacement")&&(typeof o.replacement!="string"||!o.replacement.trim()||o.replacement.length>8192||o.replacement.includes("__PIP_")))throw s("CONFIG_TARGET",{rule:r,limit:8192});if(o.type==="literal"){if(Object.keys(o).some(u=>!["type","value","replacement"].includes(u))||typeof o.value!="string"||!o.value||o.value.length>8192)throw s("CONFIG_SCHEMA",{rule:r,limit:8192})}else if(o.type==="regex"){if(Object.keys(o).some(u=>!["type","pattern","flags","replacement"].includes(u))||typeof o.pattern!="string"||!o.pattern||o.pattern.length>8192||o.flags!==void 0&&(typeof o.flags!="string"||!/^[gimsu]*$/.test(o.flags)))throw s("CONFIG_REGEX",{rule:r,limit:8192});try{new RegExp(o.pattern,o.flags??"")}catch{throw s("CONFIG_REGEX",{rule:r})}}else throw s("CONFIG_SCHEMA",{rule:r});let c=o.type==="literal"?JSON.stringify(["literal",o.value]):JSON.stringify(["regex",o.pattern,[...new Set((o.flags??"")+"g")].sort().join("")]);if(t.has(c)&&t.get(c)!==o.replacement)throw s("CONFIG_CONFLICT",{rule:r});t.set(c,o.replacement)}return e}catch(t){throw t instanceof S?t:s("CONFIG_SCHEMA",{rule:r})}}function ue(n,e=!1){let r;try{if(e){Ge(ze(n),{recursive:!0,mode:448});try{let c=ce(n,C.O_WRONLY|C.O_CREAT|C.O_EXCL,384);try{ve(c,JSON.stringify($,null,2)+`
12
+ `)}finally{ae(c)}}catch(c){if(c.code!=="EEXIST")throw I(c,"CONFIG_IO")}}r=ce(n,C.O_RDONLY|C.O_NOFOLLOW|C.O_NONBLOCK);let t=xe(r);if(t.size>1024*1024)throw s("CONFIG_SIZE",{bytes:t.size,limit:1024*1024});if(!t.isFile()||t.nlink!==1||process.getuid&&t.uid!==process.getuid())throw s("CONFIG_UNSAFE");process.platform!=="win32"&&be(r,384);let o=Ue(r,"utf8");if(Buffer.byteLength(o)>1024*1024)throw s("CONFIG_SIZE",{bytes:Buffer.byteLength(o),limit:1024*1024});return{config:J(o),raw:o}}catch(t){let o=t&&typeof t=="object"&&"code"in t?t.code:void 0;throw I(t,o==="ELOOP"||o==="EISDIR"||o==="ENOTDIR"?"CONFIG_UNSAFE":"CONFIG_IO")}finally{r!==void 0&&ae(r)}}var Y=/^protecter(?:\.[a-f0-9]{32})?\.json$/;function Ke(n){return new Promise((e,r)=>{let t=new We(Ye(new URL("./validate-worker.mjs",import.meta.url)),{workerData:n,execArgv:[],resourceLimits:{maxOldGenerationSizeMb:64}}),o=!1,c=(d,a=s("WORKER_FAILED"))=>{o||(o=!0,clearTimeout(u),t.terminate(),d?e():r(a))},u=setTimeout(()=>c(!1,s("CONFIG_TIMEOUT",{timeoutMs:2e3})),2e3);t.once("message",d=>d===!0?c(!0):c(!1,F(d,"CONFIG_SCHEMA"))),t.once("error",()=>c(!1)),t.once("exit",()=>c(!1))})}function je(n){let e=new Set,r=[];for(let a of n)for(let i of a.sensitiveWords){let l=typeof i=="string"?JSON.stringify(["literal",i,null]):i.type==="literal"?JSON.stringify(["literal",i.value,i.replacement??null]):JSON.stringify(["regex",i.pattern,[...new Set((i.flags??"")+"g")].sort().join(""),i.replacement??null]);e.has(l)||(e.add(l),r.push(i))}let t=n.map(a=>a.compaction),c=["off","ask","protected"].find(a=>t.includes(a)),u={version:1,sensitiveWords:r};c!==void 0&&(u.compaction=c);let d=JSON.stringify(u);if(Buffer.byteLength(d)>1024*1024)throw s("CONFIG_SIZE",{bytes:Buffer.byteLength(d),limit:1024*1024});return J(d)}function de(n){let e;try{e=p.lstatSync(n,{bigint:!0})}catch(r){throw I(r,"CONFIG_IO")}if(!e.isFile()||e.nlink!==1n||process.getuid&&e.uid!==BigInt(process.getuid()))throw s("CONFIG_UNSAFE");return`${e.dev}:${e.ino}:${e.size}:${e.mtimeNs}`}function W(n,e){let r=T(n,e),t=de(r),o=ue(r);if(de(r)!==t)throw s("MIGRATION_CHANGED");return{name:e,...o,identity:t,digest:$e("sha256").update(o.raw).digest("hex")}}function D(n,e){let r=W(n,e.name);if(r.identity!==e.identity||r.digest!==e.digest)throw s("MIGRATION_CHANGED")}function M(n){return p.readdirSync(n).filter(e=>Y.test(e)).sort()}function fe(n){if(process.platform==="win32")return;let e=p.openSync(n,p.constants.O_RDONLY);try{p.fsyncSync(e)}finally{p.closeSync(e)}}async function me(n,e,r={}){if(!/^[a-f0-9]{32}$/.test(e))throw s("MACHINE_ID");let t=He(n),o=`protecter.${e}.json`,c=T(t,o);p.mkdirSync(t,{recursive:!0,mode:448});let u=T(t,".protecter-migration.lock"),d=Date.now()+(r.lockTimeoutMs??5e3);for(;;)try{p.mkdirSync(u,{mode:448});break}catch(i){if(i.code!=="EEXIST")throw I(i,"MIGRATION_IO");if(Date.now()>=d)throw s("MIGRATION_LOCKED",{timeoutMs:r.lockTimeoutMs??5e3});await new Promise(l=>setTimeout(l,50))}let a;try{let i=M(t);if(i.length>64)throw s("MIGRATION_LIMIT",{limit:64});let f=[...i].sort((O,w)=>O===o?-1:w===o?1:O.localeCompare(w)).map(O=>W(t,O)),h=je(f.length?f.map(O=>O.config):[$]);if(await Ke([h]),f.length===1&&f[0].name===o){if(D(t,f[0]),JSON.stringify(M(t))!==JSON.stringify(i))throw s("MIGRATION_CHANGED");return{path:c,config:f[0].config,raw:f[0].raw,sourceRaws:[f[0].raw],migrated:0}}let y=JSON.stringify(h,null,2)+`
13
+ `;if(Buffer.byteLength(y)>1024*1024)throw s("CONFIG_SIZE",{bytes:Buffer.byteLength(y),limit:1024*1024});a=T(t,`.protecter-migration.${Je()}.tmp`);let N=p.openSync(a,p.constants.O_WRONLY|p.constants.O_CREAT|p.constants.O_EXCL,384);try{p.writeFileSync(N,y),p.fsyncSync(N)}finally{p.closeSync(N)}if(r.checkpoint?.("beforeCommit",c),JSON.stringify(M(t))!==JSON.stringify(i))throw s("MIGRATION_CHANGED");for(let O of f)D(t,O);p.renameSync(a,c),a=void 0,fe(t);let A=W(t,o);if(A.raw!==y)throw s("MIGRATION_CHANGED");r.checkpoint?.("afterCommit",c);for(let O of f)O.name!==o&&(r.checkpoint?.("beforeDelete",T(t,O.name)),D(t,A),D(t,O),p.unlinkSync(T(t,O.name)));fe(t);let G=[o];if(JSON.stringify(M(t))!==JSON.stringify(G))throw s("MIGRATION_CHANGED");return D(t,A),{path:c,config:h,raw:y,sourceRaws:[...new Set([...f.map(O=>O.raw),y])],migrated:f.filter(O=>O.name!==o).length}}catch(i){throw i instanceof S?i:I(i,"MIGRATION_IO")}finally{if(a)try{p.unlinkSync(a)}catch{}p.rmdirSync(u)}}import{createHash as Xe}from"node:crypto";import{readFileSync as qe}from"node:fs";import{execFileSync as ge}from"node:child_process";function K(n){let e=n.trim().toLowerCase().replace(/-/g,"");if(!/^[a-f0-9]{32}$/.test(e)||/^0+$/.test(e))throw s("MACHINE_ID");return Xe("sha256").update(`pi-information-protecter:v1:${e}`).digest("hex").slice(0,32)}function pe(){try{if(process.platform==="linux")return K(qe("/etc/machine-id","utf8"));if(process.platform==="darwin"){let n=ge("/usr/sbin/ioreg",["-rd1","-c","IOPlatformExpertDevice"],{encoding:"utf8",timeout:5e3,maxBuffer:1048576,stdio:["ignore","pipe","pipe"]});return K(n.match(/"IOPlatformUUID"\s*=\s*"([a-fA-F0-9-]+)"/)?.[1]??"")}if(process.platform==="win32"){let n=ge("reg.exe",["query","HKLM\\SOFTWARE\\Microsoft\\Cryptography","/v","MachineGuid","/reg:64"],{encoding:"utf8",timeout:5e3,maxBuffer:65536,stdio:["ignore","pipe","pipe"]});return K(n.match(/MachineGuid\s+REG_SZ\s+([a-fA-F0-9-]+)/i)?.[1]??"")}}catch{}throw s("MACHINE_ID")}import*as g from"node:fs";import{randomUUID as Ze}from"node:crypto";var he=32*1024*1024,ye=8*1024*1024;function Ve(n){let e=r=>String(r).padStart(2,"0");return`${n.getFullYear()}-${e(n.getMonth()+1)}-${e(n.getDate())} ${e(n.getHours())}:${e(n.getMinutes())}:${e(n.getSeconds())}`}function j(n,e,r=e){let t=g.openSync(n,(e?g.constants.O_RDWR|(r?g.constants.O_CREAT:0):g.constants.O_RDONLY)|g.constants.O_NOFOLLOW|g.constants.O_NONBLOCK,384);try{let o=g.fstatSync(t);if(!o.isFile()||o.nlink!==1||process.getuid&&o.uid!==process.getuid())throw s("AUDIT_UNSAFE");return process.platform!=="win32"&&g.fchmodSync(t,384),t}catch(o){throw g.closeSync(t),I(o,"AUDIT_UNSAFE")}}function k(n){let e=n&&typeof n=="object"&&"code"in n?n.code:void 0;return I(n,e==="ELOOP"||e==="EISDIR"||e==="ENOTDIR"?"AUDIT_UNSAFE":"AUDIT_IO")}async function Ie(n){let e=`${n}.lock`,r=Date.now()+2e3;for(;;)try{return g.mkdirSync(e,{mode:448}),e}catch(t){if(t.code!=="EEXIST")throw I(t,"AUDIT_IO");if(Date.now()>=r)throw s("AUDIT_LOCKED",{timeoutMs:2e3});await new Promise(o=>setTimeout(o,25))}}async function Oe(n){let e=await Ie(n),r;try{try{r=j(n,!0,!1)}catch(o){if(o.code==="ENOENT")return 0;throw o}let t=g.fstatSync(r).size;return g.ftruncateSync(r,0),g.fsyncSync(r),t}catch(t){throw k(t)}finally{try{r!==void 0&&g.closeSync(r)}finally{g.rmdirSync(e)}}}async function Ne(n,e,r){if(!r.length)return;let t=new Date,o=Ze(),c=Buffer.from(r.map(([a,i])=>JSON.stringify({version:1,time:Ve(t),timezoneOffset:-t.getTimezoneOffset(),requestId:o,provider:e||"unknown",original:i,replacement:a})).join(`
6
14
  `)+`
7
- `);if(s.length>me)throw a("AUDIT_BATCH_LIMIT",{bytes:s.length,limit:me});let l=await pe(n),i;try{i=j(n,!0);let c=f.fstatSync(i).size;if(c+s.length>ge)throw a("AUDIT_FULL",{bytes:c+s.length,limit:ge});if(c){let u=Buffer.alloc(1);if(f.readSync(i,u,0,1,c-1),u[0]!==10)throw a("AUDIT_PARTIAL")}try{let u=0;for(;u<s.length;){let O=f.writeSync(i,s,u,s.length-u,c+u);if(!O)throw a("AUDIT_IO");u+=O}f.fsyncSync(i)}catch(u){throw f.ftruncateSync(i,c),f.fsyncSync(i),M(u)}}catch(c){throw M(c)}finally{i!==void 0&&f.closeSync(i),f.rmdirSync(l)}}function ye(n,e=20){if(!Number.isInteger(e)||e<1||e>100)throw a("INTERNAL",{limit:100});let r;try{try{r=j(n,!1)}catch(h){if(h.code==="ENOENT")return{records:[],truncated:!1};throw h}let t=f.fstatSync(r).size,o=Math.max(0,t-1024*1024),s=Buffer.alloc(t-o),l=f.readSync(r,s,0,s.length,o),i=s.subarray(0,l).toString("utf8");if(o){let h=i.indexOf(`
8
- `);i=h<0?"":i.slice(h+1)}let c=i.split(`
9
- `),u=c.pop()!=="",O=[],g=!1;for(let h of c.slice(-e))try{let p=JSON.parse(h);if(p.version!==1||typeof p.time!="string"||typeof p.provider!="string"||typeof p.original!="string"||typeof p.replacement!="string"||typeof p.requestId!="string"||typeof p.timezoneOffset!="number")throw new Error;O.push(p)}catch{g=!0}return{records:O,truncated:o>0||c.length>e||u||g}}catch(t){throw M(t)}finally{r!==void 0&&f.closeSync(r)}}var k=class{constructor(e,r=2e3,t=de){this.configDir=e;this.timeoutMs=r;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?S(this.lastError):void 0}async initialize(){try{if(this.closed)throw a("NOT_READY");this.snapshot=void 0;let e;try{e=this.machineId()}catch{throw a("MACHINE_ID")}let r=await ue(this.configDir,e);if(this.closed)throw a("NOT_READY");this.snapshot=r,this.lastError=void 0}catch(e){let r=y(e,"CONFIG_IO");throw this.lastError=r.diagnostic,r}}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 ye(this.auditPath,e)}clearAuditRecords(){let e=this.queue.then(()=>{if(!this.ready)throw this.notReady();return he(this.auditPath)});return this.queue=e.catch(()=>{}),e}get ruleCount(){return this.snapshot?.config.sensitiveWords.length??0}get ready(){return!!this.snapshot&&!this.closed}get mappingCount(){return this.tokens.size}redact(e,r="unknown"){let t=this.queue.then(()=>this.scan(e,r)).catch(o=>{let s=y(o,"INTERNAL");throw this.lastError=s.diagnostic,s});return this.queue=t.catch(()=>{}),t}notReady(){return this.lastError?a(this.lastError.code,this.lastError):a("NOT_READY")}async scan(e,r){if(!this.ready)throw this.notReady();let{config:t,sourceRaws:o}=this.snapshot,s;try{s=JSON.stringify(e)}catch{throw a("PAYLOAD_JSON")}if(!s)throw a("PAYLOAD_JSON");if(Buffer.byteLength(s)>8*1024*1024)throw a("PAYLOAD_SIZE",{bytes:Buffer.byteLength(s),limit:8*1024*1024});let l;try{l=JSON.parse(s)}catch{throw a("PAYLOAD_JSON")}let i=await new Promise((c,u)=>{let O=!1,g;try{g=new We(Xe(new URL("./scan-worker.mjs",import.meta.url)),{workerData:{payload:l,rules:t.sensitiveWords,entries:[...this.tokens],configRaws:o},execArgv:[],resourceLimits:{maxOldGenerationSizeMb:128}})}catch(N){u(y(N,"WORKER_FAILED"));return}this.workers.add(g);let h=(N,E=a("WORKER_FAILED"))=>{if(!O){if(O=!0,clearTimeout(p),this.workers.delete(g),g.terminate(),!N||this.closed){u(this.closed?a("NOT_READY"):E);return}c(N)}},p=setTimeout(()=>h(void 0,a("SCAN_TIMEOUT",{timeoutMs:this.timeoutMs})),this.timeoutMs);g.once("message",N=>{N?.failed?h(void 0,b(N.diagnostic,"SCAN_INTERNAL")):N&&Array.isArray(N.additions)&&Array.isArray(N.hits)?h(N):h()}),g.once("error",()=>h()),g.once("exit",()=>h())});if(this.closed)throw a("NOT_READY");if(await Ie(this.auditPath,r,i.hits),this.closed)throw a("NOT_READY");for(let[c,u]of i.additions)this.tokens.set(c,u);return i.payload}restoreText(e){if(!this.tokens.size)return e;if(this.restoreSize!==this.tokens.size){let r=t=>t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");this.restorePattern=new RegExp([...this.tokens.keys()].sort((t,o)=>o.length-t.length).map(r).join("|"),"g"),this.restoreSize=this.tokens.size}return e.replace(this.restorePattern,r=>this.tokens.get(r)??r)}restore(e,r=0){if(r>80)throw a("RESTORE_COMPLEXITY",{node:r});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 o=Number(t);return!Number.isFinite(o)||String(o)!==t?e:o}if(Array.isArray(e))return e.map(t=>this.restore(t,r+1));if(e&&typeof e=="object"){let t=Object.create(null);for(let[o,s]of Object.entries(e)){let l=this.restoreText(o);if(Object.hasOwn(t,l))throw a("RESTORE_KEY_COLLISION");t[l]=this.restore(s,r+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 je,statSync as Ne,readdirSync as Ke}from"node:fs";import{homedir as qe}from"node:os";import{basename as K,dirname as L,isAbsolute as Ze,relative as Ee,resolve as U}from"node:path";import{fileURLToPath as Ve}from"node:url";function Qe(n,e){let r=n.replace(/^@/,"");return r.startsWith("file://")&&(r=Ve(r)),(r==="~"||r.startsWith("~/"))&&(r=qe()+r.slice(1)),U(e,r)}function q(n){try{return je(n)}catch{let e=L(n);return e===n?n:U(q(e),Ee(e,n))}}function et(n,e){let r=Ee(n,e);return r===""||!r.startsWith("..")&&!Ze(r)}function Oe(n,e){try{let r=Ne(n),t=Ne(e);return r.dev===t.dev&&r.ino===t.ino}catch{return!1}}function _e(n,e,r,t){if(!t)return!0;let o=q(U(t));if(["bash","powershell"].includes(n)){let i=typeof e.command=="string"?e.command:"";if(/protecter(?:\.[a-f0-9]{32})?\.jsonl?|\.protecter-migration/i.test(i)||i.includes(t))return!0}let s=e.path??e.file_path??e.filePath,l=["grep","find","ls"].includes(n);if(typeof s!="string"&&!l)return!1;try{let i=Qe(typeof s=="string"?s:".",r),c=q(i),u=/^protecter\.[a-f0-9]{32}\.jsonl(?:\.lock)?$/,O=Ke(L(o)).filter(g=>u.test(g)).some(g=>Oe(i,U(L(o),g)));return L(c)===L(o)&&(W.test(K(c))||u.test(K(c))||K(c).startsWith(".protecter-migration"))||c===o||Oe(i,t)||O||l&&et(c,o)}catch{return!0}}function nt(n){te(J());let e=new k(tt()),r=Z(rt),t=!1,o,s=l=>{let i=S(l);return o={at:new Date().toISOString(),text:i},i};n.on("session_start",async(l,i)=>{try{if(!r)throw a("UNSUPPORTED_PI");await e.initialize(),t=!0;let c=e.ruleCount;i.ui.setStatus("protecter",m("statusBarRules",{rules:R("rules",c)})),c===0&&i.ui.notify(m("emptyRules"),"warning")}catch(c){t=!1,i.ui.setStatus("protecter",m("statusBarNotReady")),i.ui.notify(s(y(c,"CONFIG_IO").diagnostic),"error")}}),n.on("before_provider_request",async(l,i)=>{try{if(!r)throw a("UNSUPPORTED_PI");let c=await e.redact(l.payload,i.model?.provider??"unknown");return t=!0,c}catch(c){t=!1;let u=s(y(c,"INTERNAL").diagnostic);try{await i.abort()}catch{}try{i.ui.notify(u,"error")}catch{}return{}}}),n.on("message_end",l=>{if(l.message.role!=="assistant")return;let i=l.message;return{message:{...i,content:i.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),...i.errorMessage?{errorMessage:e.restoreText(i.errorMessage)}:{}}}}),n.registerMarkdownTransformer((l,i)=>i.messageType==="user"?l:e.restoreText(l)),n.on("tool_call",(l,i)=>{if(!t)return{block:!0,reason:e.lastDiagnosticText??o?.text??S(a("NOT_READY").diagnostic)};let c=e.restore(l.input);for(let u of Object.keys(l.input))delete l.input[u];if(Object.assign(l.input,c),_e(l.toolName,l.input,i.cwd,e.configPath))return{block:!0,reason:m("toolBlocked")}}),n.on("session_before_compact",(l,i)=>(i.ui.notify(m("compactUnsupported"),"warning"),{cancel:!0})),n.on("session_before_tree",l=>l.preparation.userWantsSummary?{cancel:!0}:void 0),n.registerCommand("protecter",{description:m("commandDescription"),handler:async(l,i)=>{let c=l.trim().split(/\s+/).filter(Boolean),u=c[0]??"logs";if(!["status","reload","logs"].includes(u)||u!=="logs"&&c.length>1||c.length>2||c[1]!==void 0&&c[1]!=="clear"&&!/^(?:[1-9]\d?|100)$/.test(c[1])){i.ui.notify(m("usage"),"info");return}if(u==="logs"){if(i.mode!=="tui"){i.ui.notify(m("logsTuiOnly"),"warning");return}if(c[1]==="clear"){try{if(!e.ready)throw a("NOT_READY");if(!await i.ui.confirm(m("clearTitle"),m("clearBody")))return;await i.waitForIdle();let p=await e.clearAuditRecords();t=e.ready,i.ui.notify(m("cleared",{bytes:R("bytes",p)}),"info")}catch(p){i.ui.notify(s(y(p,"AUDIT_IO").diagnostic),"error")}return}try{let{records:p,truncated:N}=e.auditRecords(Number(c[1]??20));if(!p.length){i.ui.notify(m("logsNone"),"info");return}let E=F=>JSON.stringify(F).replace(/[\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g,x=>`\\u${x.charCodeAt(0).toString(16).padStart(4,"0")}`),P=p.map((F,x)=>`${x+1}. ${E(F.time)} | ${E(F.provider).slice(0,120)}`),I=await i.ui.select(m("logsSelectTitle")+(N?m("logsTruncated"):""),P),w=I===void 0?-1:P.indexOf(I);if(w<0||!await i.ui.confirm(m("logsRevealTitle"),m("logsRevealBody")))return;let v=E(p[w]);await i.ui.editor(m("logsPreviewTitle"),v.length>2e4?`${v.slice(0,2e4)}
10
- ${m("logsPreviewTruncated")}`:v)}catch(p){i.ui.notify(s(y(p,"AUDIT_IO").diagnostic),"error")}return}if(u==="reload"){await i.waitForIdle(),await i.reload();return}let O=e.lastDiagnosticText??o?.text,g=m("statusLine",{state:m(e.ready?"statusReady":"statusNotReady"),rules:R("rules",e.ruleCount),mappings:R("mappings",e.mappingCount)}),h=O?`
15
+ `);if(c.length>ye)throw s("AUDIT_BATCH_LIMIT",{bytes:c.length,limit:ye});let u=await Ie(n),d;try{d=j(n,!0);let a=g.fstatSync(d).size;if(a+c.length>he)throw s("AUDIT_FULL",{bytes:a+c.length,limit:he});if(a){let i=Buffer.alloc(1);if(g.readSync(d,i,0,1,a-1),i[0]!==10)throw s("AUDIT_PARTIAL")}try{let i=0;for(;i<c.length;){let l=g.writeSync(d,c,i,c.length-i,a+i);if(!l)throw s("AUDIT_IO");i+=l}g.fsyncSync(d)}catch(i){throw g.ftruncateSync(d,a),g.fsyncSync(d),k(i)}}catch(a){throw k(a)}finally{d!==void 0&&g.closeSync(d),g.rmdirSync(u)}}function Ae(n,e=20){if(!Number.isInteger(e)||e<1||e>100)throw s("INTERNAL",{limit:100});let r;try{try{r=j(n,!1)}catch(h){if(h.code==="ENOENT")return{records:[],truncated:!1};throw h}let t=g.fstatSync(r).size,o=Math.max(0,t-1024*1024),c=Buffer.alloc(t-o),u=g.readSync(r,c,0,c.length,o),d=c.subarray(0,u).toString("utf8");if(o){let h=d.indexOf(`
16
+ `);d=h<0?"":d.slice(h+1)}let a=d.split(`
17
+ `),i=a.pop()!=="",l=[],f=!1;for(let h of a.slice(-e))try{let y=JSON.parse(h);if(y.version!==1||typeof y.time!="string"||typeof y.provider!="string"||typeof y.original!="string"||typeof y.replacement!="string"||typeof y.requestId!="string"||typeof y.timezoneOffset!="number")throw new Error;l.push(y)}catch{f=!0}return{records:l,truncated:o>0||a.length>e||i||f}}catch(t){throw k(t)}finally{r!==void 0&&g.closeSync(r)}}var b=class{constructor(e,r=2e3,t=pe){this.configDir=e;this.timeoutMs=r;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 s("NOT_READY");this.snapshot=void 0;let e;try{e=this.machineId()}catch{throw s("MACHINE_ID")}let r=await me(this.configDir,e);if(this.closed)throw s("NOT_READY");this.snapshot=r,this.lastError=void 0}catch(e){let r=I(e,"CONFIG_IO");throw this.lastError=r.diagnostic,r}}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 Ae(this.auditPath,e)}clearAuditRecords(){let e=this.queue.then(()=>{if(!this.ready)throw this.notReady();return Oe(this.auditPath)});return this.queue=e.catch(()=>{}),e}get ruleCount(){return this.snapshot?.config.sensitiveWords.length??0}get compactionMode(){return this.snapshot?.config.compaction??le}get ready(){return!!this.snapshot&&!this.closed}get mappingCount(){return this.tokens.size}redact(e,r="unknown"){let t=this.queue.then(()=>this.scan(e,r)).catch(o=>{let c=I(o,"INTERNAL");throw this.lastError=c.diagnostic,c});return this.queue=t.catch(()=>{}),t}notReady(){return this.lastError?s(this.lastError.code,this.lastError):s("NOT_READY")}async scan(e,r){if(!this.ready)throw this.notReady();let{config:t,sourceRaws:o}=this.snapshot,c;try{c=JSON.stringify(e)}catch{throw s("PAYLOAD_JSON")}if(!c)throw s("PAYLOAD_JSON");if(Buffer.byteLength(c)>8*1024*1024)throw s("PAYLOAD_SIZE",{bytes:Buffer.byteLength(c),limit:8*1024*1024});let u;try{u=JSON.parse(c)}catch{throw s("PAYLOAD_JSON")}let d=await new Promise((a,i)=>{let l=!1,f;try{f=new Qe(et(new URL("./scan-worker.mjs",import.meta.url)),{workerData:{payload:u,rules:t.sensitiveWords,entries:[...this.tokens],configRaws:o},execArgv:[],resourceLimits:{maxOldGenerationSizeMb:128}})}catch(N){i(I(N,"WORKER_FAILED"));return}this.workers.add(f);let h=(N,A=s("WORKER_FAILED"))=>{if(!l){if(l=!0,clearTimeout(y),this.workers.delete(f),f.terminate(),!N||this.closed){i(this.closed?s("NOT_READY"):A);return}a(N)}},y=setTimeout(()=>h(void 0,s("SCAN_TIMEOUT",{timeoutMs:this.timeoutMs})),this.timeoutMs);f.once("message",N=>{N?.failed?h(void 0,F(N.diagnostic,"SCAN_INTERNAL")):N&&Array.isArray(N.additions)&&Array.isArray(N.hits)?h(N):h()}),f.once("error",()=>h()),f.once("exit",()=>h())});if(this.closed)throw s("NOT_READY");if(await Ne(this.auditPath,r,d.hits),this.closed)throw s("NOT_READY");for(let[a,i]of d.additions)this.tokens.set(a,i);return d.payload}restoreText(e){if(!this.tokens.size)return e;if(this.restoreSize!==this.tokens.size){let r=t=>t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");this.restorePattern=new RegExp([...this.tokens.keys()].sort((t,o)=>o.length-t.length).map(r).join("|"),"g"),this.restoreSize=this.tokens.size}return e.replace(this.restorePattern,r=>this.tokens.get(r)??r)}restore(e,r=0){if(r>80)throw s("RESTORE_COMPLEXITY",{node:r});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 o=Number(t);return!Number.isFinite(o)||String(o)!==t?e:o}if(Array.isArray(e))return e.map(t=>this.restore(t,r+1));if(e&&typeof e=="object"){let t=Object.create(null);for(let[o,c]of Object.entries(e)){let u=this.restoreText(o);if(Object.hasOwn(t,u))throw s("RESTORE_KEY_COLLISION");t[u]=this.restore(c,r+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 tt,statSync as Ee,readdirSync as rt}from"node:fs";import{homedir as nt}from"node:os";import{basename as X,dirname as P,isAbsolute as ot,relative as _e,resolve as x}from"node:path";import{fileURLToPath as it}from"node:url";function st(n,e){let r=n.replace(/^@/,"");return r.startsWith("file://")&&(r=it(r)),(r==="~"||r.startsWith("~/"))&&(r=nt()+r.slice(1)),x(e,r)}function q(n){try{return tt(n)}catch{let e=P(n);return e===n?n:x(q(e),_e(e,n))}}function at(n,e){let r=_e(n,e);return r===""||!r.startsWith("..")&&!ot(r)}function Se(n,e){try{let r=Ee(n),t=Ee(e);return r.dev===t.dev&&r.ino===t.ino}catch{return!1}}function Ce(n,e,r,t){if(!t)return!0;let o=q(x(t));if(["bash","powershell"].includes(n)){let d=typeof e.command=="string"?e.command:"";if(/protecter(?:\.[a-f0-9]{32})?\.jsonl?|\.protecter-migration/i.test(d)||d.includes(t))return!0}let c=e.path??e.file_path??e.filePath,u=["grep","find","ls"].includes(n);if(typeof c!="string"&&!u)return!1;try{let d=st(typeof c=="string"?c:".",r),a=q(d),i=/^protecter\.[a-f0-9]{32}\.jsonl(?:\.lock)?$/,l=rt(P(o)).filter(f=>i.test(f)).some(f=>Se(d,x(P(o),f)));return P(a)===P(o)&&(Y.test(X(a))||i.test(X(a))||X(a).startsWith(".protecter-migration"))||a===o||Se(d,t)||l||u&&at(a,o)}catch{return!0}}import{generateBranchSummary as ct,generateSummaryWithUsage as lt}from"@earendil-works/pi-coding-agent";async function Te(n){let e=n.model;if(!e)throw s("COMPACT_UNAVAILABLE");let r;try{r=await n.modelRegistry.getApiKeyAndHeaders(e)}catch{throw s("COMPACT_UNAVAILABLE")}if(!r.ok)throw s("COMPACT_UNAVAILABLE");let t=Object.fromEntries(Object.entries(r.headers??{}).filter(o=>typeof o[1]=="string"));return{model:e,apiKey:r.apiKey,headers:t,env:r.env}}async function we(n,e,r,t,o=lt){let{preparation:c}=n,u=await r.redact(c.messagesToSummarize,t),d=c.previousSummary===void 0?void 0:await r.redact(c.previousSummary,t),a=n.customInstructions===void 0?void 0:await r.redact(n.customInstructions,t),i=await Te(e),l,f;try{let h=await o(u,i.model,c.settings.reserveTokens,i.apiKey,i.headers,n.signal,a,d,e.thinkingLevel,void 0,i.env);l=h.text,f=h.usage}catch{throw s("COMPACT_FAILED")}if(typeof l!="string"||!l.trim())throw s("COMPACT_FAILED");return{summary:r.restoreText(l),firstKeptEntryId:c.firstKeptEntryId,tokensBefore:c.tokensBefore,usage:f}}async function Re(n,e,r,t,o=ct){let{preparation:c}=n,u=await r.redact(c.entriesToSummarize,t),d=c.customInstructions===void 0?void 0:await r.redact(c.customInstructions,t),a=await Te(e),i;try{i=await o(u,{model:a.model,apiKey:a.apiKey,headers:a.headers,env:a.env,signal:n.signal,customInstructions:d,replaceInstructions:c.replaceInstructions})}catch{throw s("COMPACT_FAILED")}let l=i?.summary;if(typeof l!="string"||!l.trim())throw s("COMPACT_FAILED");return{summary:r.restoreText(l),usage:i.usage}}function ft(n){ne(H());let e=new b(ut()),r=Q(dt),t=!1,o,c,u=a=>{let i=_(a);return o={at:new Date().toISOString(),text:i},i};n.on("session_start",async(a,i)=>{try{if(!r)throw s("UNSUPPORTED_PI");await e.initialize(),t=!0;let l=e.ruleCount;i.ui.setStatus("protecter",m("statusBarRules",{rules:R("rules",l)})),l===0&&i.ui.notify(m("emptyRules"),"warning")}catch(l){t=!1,i.ui.setStatus("protecter",m("statusBarNotReady")),i.ui.notify(u(I(l,"CONFIG_IO").diagnostic),"error")}}),n.on("before_provider_request",async(a,i)=>{try{if(!r)throw s("UNSUPPORTED_PI");let l=await e.redact(a.payload,i.model?.provider??"unknown");return t=!0,l}catch(l){t=!1;let f=u(I(l,"INTERNAL").diagnostic);try{await i.abort()}catch{}try{i.ui.notify(f,"error")}catch{}return{}}}),n.on("message_end",a=>{if(a.message.role!=="assistant")return;let i=a.message;return{message:{...i,content:i.content.map(l=>l.type==="text"?{...l,text:e.restoreText(l.text)}:l.type==="thinking"?{...l,thinking:e.restoreText(l.thinking)}:l.type==="toolCall"?{...l,arguments:e.restore(l.arguments)}:l),...i.errorMessage?{errorMessage:e.restoreText(i.errorMessage)}:{}}}}),n.registerMarkdownTransformer((a,i)=>i.messageType==="user"?a:e.restoreText(a)),n.on("tool_call",(a,i)=>{if(!t)return{block:!0,reason:e.lastDiagnosticText??o?.text??_(s("NOT_READY").diagnostic)};let l=e.restore(a.input);for(let f of Object.keys(a.input))delete a.input[f];if(Object.assign(a.input,l),Ce(a.toolName,a.input,i.cwd,e.configPath))return{block:!0,reason:m("toolBlocked")}});let d=async(a,i)=>{if(!r)throw s("UNSUPPORTED_PI");if(!e.ready)throw e.lastDiagnostic?s(e.lastDiagnostic.code,e.lastDiagnostic):s("NOT_READY");let l=e.compactionMode;if(l==="off")throw s("COMPACT_DENIED");if(c==="deny")throw s("COMPACT_DENIED");if(l!=="ask"||c==="allow")return;let f=[m("compactAllowOnce"),m("compactAllowAlways"),m("compactDenyOnce"),m("compactDenyAlways")],h=await a.ui.select(`${m("compactAskTitle")}
18
+
19
+ ${m("compactAskBody")}`,f,{signal:i}),y=h===void 0?-1:f.indexOf(h);if(y===1?(c="allow",a.ui.notify(m("compactAlwaysNotice"),"info")):y===3&&(c="deny",a.ui.notify(m("compactDenyNotice"),"warning")),y!==0&&y!==1)throw s("COMPACT_DENIED")};n.on("session_before_compact",async(a,i)=>{try{await d(i,a.signal),i.ui.notify(m("compactWorking"),"info");let l=await we(a,i,e,i.model?.provider??"unknown");return i.ui.notify(m("compactDone"),"info"),{compaction:l}}catch(l){return i.ui.notify(u(I(l,"COMPACT_FAILED").diagnostic),"warning"),{cancel:!0}}}),n.on("session_before_tree",async(a,i)=>{if(a.preparation.userWantsSummary)try{await d(i,a.signal),i.ui.notify(m("compactWorking"),"info");let l=await Re(a,i,e,i.model?.provider??"unknown");return i.ui.notify(m("compactDone"),"info"),{summary:l}}catch(l){return i.ui.notify(u(I(l,"COMPACT_FAILED").diagnostic),"warning"),{cancel:!0}}}),n.registerCommand("protecter",{description:m("commandDescription"),handler:async(a,i)=>{let l=a.trim().split(/\s+/).filter(Boolean),f=l[0]??"logs";if(!["status","reload","logs"].includes(f)||f!=="logs"&&l.length>1||l.length>2||l[1]!==void 0&&l[1]!=="clear"&&!/^(?:[1-9]\d?|100)$/.test(l[1])){i.ui.notify(m("usage"),"info");return}if(f==="logs"){if(i.mode!=="tui"){i.ui.notify(m("logsTuiOnly"),"warning");return}if(l[1]==="clear"){try{if(!e.ready)throw s("NOT_READY");if(!await i.ui.confirm(m("clearTitle"),m("clearBody")))return;await i.waitForIdle();let A=await e.clearAuditRecords();t=e.ready,i.ui.notify(m("cleared",{bytes:R("bytes",A)}),"info")}catch(A){i.ui.notify(u(I(A,"AUDIT_IO").diagnostic),"error")}return}try{let{records:A,truncated:G}=e.auditRecords(Number(l[1]??20));if(!A.length){i.ui.notify(m("logsNone"),"info");return}let O=L=>JSON.stringify(L).replace(/[\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g,v=>`\\u${v.charCodeAt(0).toString(16).padStart(4,"0")}`),w=A.map((L,v)=>`${v+1}. ${O(L.time)} | ${O(L.provider).slice(0,120)}`),Z=await i.ui.select(m("logsSelectTitle")+(G?m("logsTruncated"):""),w),V=Z===void 0?-1:w.indexOf(Z);if(V<0||!await i.ui.confirm(m("logsRevealTitle"),m("logsRevealBody")))return;let U=O(A[V]);await i.ui.editor(m("logsPreviewTitle"),U.length>2e4?`${U.slice(0,2e4)}
20
+ ${m("logsPreviewTruncated")}`:U)}catch(A){i.ui.notify(u(I(A,"AUDIT_IO").diagnostic),"error")}return}if(f==="reload"){await i.waitForIdle(),await i.reload();return}let h=e.lastDiagnosticText??o?.text,y=m("statusLine",{state:m(e.ready?"statusReady":"statusNotReady"),rules:R("rules",e.ruleCount),mappings:R("mappings",e.mappingCount)}),N=h?`
11
21
  ${m("statusLastBlock",{at:o?.at??""})}
12
- ${O}`:`
13
- ${m("statusNoBlock")}`;i.ui.notify(g+h,e.ready?"info":"error")}}),n.on("session_shutdown",()=>e.close())}export{nt as default};
22
+ ${h}`:`
23
+ ${m("statusNoBlock")}`;i.ui.notify(y+N,e.ready?"info":"error")}}),n.on("session_shutdown",()=>e.close())}export{ft as default};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-information-protecter",
3
- "version": "0.7.1",
3
+ "version": "0.8.1",
4
4
  "description": "Sensitive Personal Information (SPI) Protecter — local reversible privacy redaction for Pi",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,19 +1,65 @@
1
1
  {
2
2
  "version": 1,
3
+ "compaction": "ask",
3
4
  "sensitiveWords": [
4
5
  "张三",
5
- { "type": "literal", "value": "Apple Inc.", "replacement": "Alphabet Inc." },
6
- { "type": "literal", "value": "example-private-key-123", "replacement": "MyPrivateKey" },
7
- { "type": "literal", "value": "replace-with-your-real-password" },
8
- { "type": "regex", "pattern": "https?://[^\\s\\\"<>/:]+", "flags": "i" },
9
- { "type": "literal", "value": "vpn.internal.example", "replacement": "gateway.internal.example" },
10
- { "type": "regex", "pattern": "(?<![\\w.-])(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+(?:corp|internal|intra|lan|local)(?![\\w.-])", "flags": "i" },
11
- { "type": "regex", "pattern": "(?<![\\w.-])[A-Za-z0-9._%+-]+@[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)*\\.[A-Za-z]{2,}(?![\\w.-])" },
12
- { "type": "regex", "pattern": "(?<![\\w.])(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(?![\\w.])" },
13
- { "type": "regex", "pattern": "(?<![\\w:.])(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}(?![\\w:.])" },
14
- { "type": "regex", "pattern": "(?<![\\w:.])(?:[0-9A-Fa-f]{1,4}:){1,6}:(?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,5})?(?![\\w:.])" },
15
- { "type": "regex", "pattern": "(?<!\\d)1[3-9]\\d{9}(?!\\d)" },
16
- { "type": "regex", "pattern": "(?<![0-9A-Za-z])\\d{17}[0-9Xx](?![0-9A-Za-z])" },
17
- { "type": "regex", "pattern": "(?<=Bearer )[A-Za-z0-9._~+/-]+=*", "flags": "i" }
6
+ {
7
+ "type": "literal",
8
+ "value": "Apple Inc.",
9
+ "replacement": "Alphabet Inc."
10
+ },
11
+ {
12
+ "type": "literal",
13
+ "value": "example-private-key-123",
14
+ "replacement": "MyPrivateKey"
15
+ },
16
+ {
17
+ "type": "literal",
18
+ "value": "replace-with-your-real-password"
19
+ },
20
+ {
21
+ "type": "regex",
22
+ "pattern": "https?://[^\\s\\\"<>/:]+",
23
+ "flags": "i"
24
+ },
25
+ {
26
+ "type": "literal",
27
+ "value": "vpn.internal.example",
28
+ "replacement": "gateway.internal.example"
29
+ },
30
+ {
31
+ "type": "regex",
32
+ "pattern": "(?<![\\w.-])(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+(?:corp|internal|intra|lan|local)(?![\\w.-])",
33
+ "flags": "i"
34
+ },
35
+ {
36
+ "type": "regex",
37
+ "pattern": "(?<![\\w.-])[A-Za-z0-9._%+-]+@[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)*\\.[A-Za-z]{2,}(?![\\w.-])"
38
+ },
39
+ {
40
+ "type": "regex",
41
+ "pattern": "(?<![\\w.])(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(?![\\w.])"
42
+ },
43
+ {
44
+ "type": "regex",
45
+ "pattern": "(?<![\\w:.])(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}(?![\\w:.])"
46
+ },
47
+ {
48
+ "type": "regex",
49
+ "pattern": "(?<![\\w:.])(?:[0-9A-Fa-f]{1,4}:){1,6}:(?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,5})?(?![\\w:.])"
50
+ },
51
+ {
52
+ "type": "regex",
53
+ "pattern": "(?<!\\d)1[3-9]\\d{9}(?!\\d)"
54
+ },
55
+ {
56
+ "type": "regex",
57
+ "pattern": "(?<![0-9A-Za-z])\\d{17}[0-9Xx](?![0-9A-Za-z])"
58
+ },
59
+ {
60
+ "type": "regex",
61
+ "pattern": "(?<=Bearer )[A-Za-z0-9._~+/-]{16,}={0,2}",
62
+ "flags": "i"
63
+ }
18
64
  ]
19
65
  }