pi-information-protecter 0.15.0 → 0.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog / 更新日志
2
2
 
3
+ ## 0.15.2 — View images in any terminal / 任意终端均可查看图片
4
+
5
+ ### Fixed / 修复
6
+
7
+ - **Open image** now works in terminals without an inline image protocol (Apple Terminal, tmux and others), where the TUI preview could only show a text placeholder. The exact image to be sent is written to a read-only private temporary copy, opened in the system image viewer, and deleted when the user returns to the decision.
8
+ 在不支持内联图片协议的终端(Apple Terminal、tmux 等)中,TUI 预览只能显示文字占位;“打开图片”现在改为将即将发送的原始图片写入只读私有临时副本,用系统看图软件打开,返回确认菜单后即删除。
9
+ - When an image cannot be shown, its full path is printed so it can be opened manually: the dialog always shows the read-only copy's absolute path (and the original file's path when known), states when the system viewer could not be launched, and keeps the copy until the user returns. If no copy can be made, the notification shows the original file's path when known.
10
+ 无法展示图片时会打印完整路径以便手动打开:对话框始终显示只读副本的绝对路径(已知时同时显示原文件路径),系统看图软件无法启动时会明确说明,副本保留到用户返回为止;若无法生成副本,通知中会给出已知的原文件路径。
11
+ - Tool images without a file path (such as screenshots) now show the tool that produced them, including the inner tool of MCP gateway calls. Only arguments naming an image file are shown as the image's path, so non-image paths such as design files are no longer misreported.
12
+ 无文件路径的工具图片(如截图)现在会展示产生它的工具,包括 MCP 网关调用的实际工具。只有指向图片文件的参数才会作为图片路径展示,设计文件等非图片路径不再被误报。
13
+
14
+ ## 0.15.1 — Exact-content path approval / 路径授权校验内容一致
15
+
16
+ ### Changed / 变更
17
+
18
+ - Path-based attachment approval now requires both the same path and the same content. If the content at an approved path changes, the user is asked again; attachments whose content cannot be read are never remembered by path.
19
+ 按路径免询问现在要求路径和内容都相同。已允许路径的内容若有变化会重新询问;无法读取内容的附件不会按路径记住。
20
+
3
21
  ## 0.15.0 — Attachment paths and explicit rule conflicts / 附件路径与明确的规则冲突
4
22
 
5
23
  ### Added / 新增
package/README.md CHANGED
@@ -151,7 +151,7 @@ Never type a secret as a command argument — `test` is the one exception, becau
151
151
 
152
152
  ### Attachments
153
153
 
154
- The default `attachments: "ask"` prompts separately for each new attachment. For inline images, choose **Open image** to preview it in the TUI, or **Edit image in default app** to open a private temporary copy with the operating system's image app. Save edits over that same temporary file and return to the menu; only the saved image copy is substituted into the provider request, and the original attachment is not changed. Temporary files are removed after editing. Editing is available for inline PNG, JPEG, WebP and GIF images up to 25 MiB; other image types can still be previewed and allowed. Inline images in Pi, Gemini, Anthropic (base64 source) and OpenAI (data URL) request formats can be previewed; remote image URLs and non-image files can only be allowed or cancelled. The prompt shows the attachment's local file path when one is known — for images read by a tool such as `read`, the path from that tool call; images pasted or attached directly carry no path in Pi, so the prompt says so. Approval applies only to that image's exact content; a different image requires its own confirmation, except that once a known path is allowed, later attachments from the same path are allowed for the rest of the session without asking. Edited images never approve their path, so the unedited original is not sent silently later. The same confirmed image content is remembered for the current session so it is not repeatedly prompted from conversation history. Attachments are never inspected or redacted, while other text in the request still goes through normal redaction. Choose `"allow"` to always pass attachments through, or `"deny"` to always cancel them.
154
+ The default `attachments: "ask"` prompts separately for each new attachment. For inline images, choose **Open image** to preview it in the TUI; terminals without an inline image protocol (such as Apple Terminal or tmux) instead open a read-only private copy of the exact image to be sent in the system image viewer, deleted once you return to the menu. The full path of that copy (and of the original file, when known) is always printed, so it can be opened manually if no viewer appears, for example over SSH. Alternatively, choose **Edit image in default app** to open a private temporary copy with the operating system's image app. Save edits over that same temporary file and return to the menu; only the saved image copy is substituted into the provider request, and the original attachment is not changed. Temporary files are removed after editing. Editing is available for inline PNG, JPEG, WebP and GIF images up to 25 MiB; other image types can still be previewed and allowed. Inline images in Pi, Gemini, Anthropic (base64 source) and OpenAI (data URL) request formats can be previewed; remote image URLs and non-image files can only be allowed or cancelled. The prompt shows the attachment's local file path when one is known — for images returned by a tool whose input names an image file (such as `read`), that path. Other tool images (for example screenshots) show the tool that produced them; images pasted or attached directly carry no path in Pi, so the prompt says so. Approval applies only to that image's exact content; a different image requires its own confirmation, except that once an image from a known path is allowed, the same path with the same content is allowed for the rest of the session without asking; if the content at that path changes, it asks again. Attachments whose content cannot be read (such as non-image files) are never remembered by path. Edited images never approve their path, so the unedited original is not sent silently later. The same confirmed image content is remembered for the current session so it is not repeatedly prompted from conversation history. Attachments are never inspected or redacted, while other text in the request still goes through normal redaction. Choose `"allow"` to always pass attachments through, or `"deny"` to always cancel them.
155
155
 
156
156
  ```json
157
157
  { "version": 1, "attachments": "ask", "sensitiveWords": [] }
@@ -437,7 +437,7 @@ pi update npm:pi-information-protecter
437
437
 
438
438
  ### 附件
439
439
 
440
- 默认 `attachments: "ask"`:每个新附件都会单独询问。对于内嵌图片,可选“打开图片”在 TUI 中预览,或选“用系统默认软件编辑图片”打开私有临时副本。将修改覆盖保存到同一临时文件后返回菜单;只有保存后的图片副本会替换提供商请求中的附件,原附件不会被修改,编辑结束后临时文件会删除。内嵌 PNG、JPEG、WebP、GIF 图片(不超过 25 MiB)可编辑,其他图片格式仍可预览并选择放行。Pi、Gemini、Anthropic(base64 source)和 OpenAI(data URL)请求格式中的内嵌图片均可预览;远程图片 URL 与非图片文件只能放行或取消。若已知附件的本地路径,确认菜单会展示该路径——对于由 `read` 等工具读取的图片,取自该工具调用的路径;直接粘贴或附加的图片在 Pi 中不携带路径,菜单会如实说明。确认仅适用于该图片的具体内容;不同图片必须分别确认,但已知路径一经允许,本会话内来自同一路径的后续附件将直接放行,不再询问。编辑过的图片不会记住其路径,避免之后静默发送未编辑的原图。相同图片内容在本会话中会记住已确认状态,避免随历史记录重复询问。附件本身不会被检查或脱敏,但请求中的其他文本仍会正常脱敏。设为 `"allow"` 可始终放行附件,设为 `"deny"` 则始终取消。
440
+ 默认 `attachments: "ask"`:每个新附件都会单独询问。对于内嵌图片,可选“打开图片”在 TUI 中预览;不支持内联图片的终端(如 Apple Terminal、tmux)会改用系统看图软件打开即将发送图片的只读私有副本,返回菜单后即删除。界面上始终会显示该副本(以及已知时原文件)的完整路径;若看图软件未能显示(如通过 SSH 使用),可按路径手动打开。也可选“用系统默认软件编辑图片”打开私有临时副本。将修改覆盖保存到同一临时文件后返回菜单;只有保存后的图片副本会替换提供商请求中的附件,原附件不会被修改,编辑结束后临时文件会删除。内嵌 PNG、JPEG、WebP、GIF 图片(不超过 25 MiB)可编辑,其他图片格式仍可预览并选择放行。Pi、Gemini、Anthropic(base64 source)和 OpenAI(data URL)请求格式中的内嵌图片均可预览;远程图片 URL 与非图片文件只能放行或取消。若已知附件的本地路径,确认菜单会展示该路径——对于由工具返回、且调用参数指向图片文件的图片(如 `read`),展示该路径;其他工具图片(如截图)展示产生它的工具名;直接粘贴或附加的图片在 Pi 中不携带路径,菜单会如实说明。确认仅适用于该图片的具体内容;不同图片必须分别确认,但已知路径的图片一经允许,本会话内路径和内容都相同的附件将直接放行,不再询问;同一路径的内容若有变化则重新询问。无法读取内容的附件(如非图片文件)不会按路径记住。编辑过的图片不会记住其路径,避免之后静默发送未编辑的原图。相同图片内容在本会话中会记住已确认状态,避免随历史记录重复询问。附件本身不会被检查或脱敏,但请求中的其他文本仍会正常脱敏。设为 `"allow"` 可始终放行附件,设为 `"deny"` 则始终取消。
441
441
 
442
442
  ```json
443
443
  { "version": 1, "attachments": "ask", "sensitiveWords": [] }
package/dist/index.mjs CHANGED
@@ -1,38 +1,40 @@
1
1
  /*! SPI Protecter | MIT | see LICENSE / SPI 隐私保护插件 | MIT 许可 */
2
- import{getAgentDir as kn,VERSION as Pt}from"@earendil-works/pi-coding-agent";import{Worker as ln}from"node:worker_threads";import{fileURLToPath as dn}from"node:url";import*as w from"node:fs";import{join as q,resolve as Vt}from"node:path";import{createHash as Zt,randomUUID as Qt}from"node:crypto";import{Worker as en}from"node:worker_threads";import{fileURLToPath as tn}from"node:url";var F="https://github.com/taills/pi-information-protecter/issues/new",xe="0.84.0-0.84.x, 0.85.1-0.87.x, 0.99.x, 1.2.0-1.4.x",Mt=["on","registerCommand","registerMarkdownTransformer"],Ft=["abort"],xt=["notify","setStatus"];function me(t,e){return!t||typeof t!="object"&&typeof t!="function"?!1:typeof t[e]=="function"}function pe(t,e){let o=[];for(let r of Mt)me(t,r)||o.push(`pi.${r}()`);if(e===void 0)return o;let n=e;for(let r of Ft)me(n,r)||o.push(`ctx.${r}()`);for(let r of xt)me(n.ui,r)||o.push(`ctx.ui.${r}()`);return(!n.modelRegistry||typeof n.modelRegistry!="object")&&o.push("ctx.modelRegistry"),o}function ve(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<=4:o!==0?!1:n===84?!0:n===85?r>=1:n===86||n===87||n===99}var ge={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"],PI_CAPABILITY:["This Pi build is missing a capability this extension requires",`Update Pi; if the newest Pi still fails, report the listed capability at ${F}`],GUARD_UNAVAILABLE:["Cannot cover the model calls that have no extension hook","This Pi build exposes no model runtime to wrap; update Pi, or report this build at "+F],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'],CONFIG_ATTACHMENT:["Invalid attachment setting",'Use "ask", "allow" or "deny"'],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"],TEST_INPUT:["Cannot read the offline test input","Pass text, or @<path> pointing to a readable regular text file"],TEST_SIZE:["Offline test input exceeds the size limit","Test a smaller file or a shorter excerpt"]},vt={INTERNAL:["未预期的本地错误","重载并检查安装包完整性"],NOT_READY:["防护尚未初始化","查看初始化错误,修复后重载"],PI_CAPABILITY:["当前 Pi 构建缺少本扩展所需的能力",`请升级 Pi;若最新版 Pi 仍然失败,请将所列能力反馈至 ${F}`],GUARD_UNAVAILABLE:["无法覆盖没有扩展钩子的模型调用","该 Pi 构建未暴露可包装的模型运行时:请升级 Pi,或将该构建反馈至 "+F],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 的对象'],CONFIG_ATTACHMENT:["附件配置值无效",'请使用 "ask"、"allow" 或 "deny"'],AUDIT_BATCH_LIMIT:["审计批次超过上限","减少单个请求的命中内容"],AUDIT_FULL:["审计日志将超过上限","检查记录后清空日志或本地归档"],AUDIT_PARTIAL:["审计日志末行不完整","本地检查或清空日志,不要上传日志"],AUDIT_LOCKED:["审计锁持续被占用","等待写入完成,全部停止后再检查残留锁"],AUDIT_UNSAFE:["审计文件不安全","使用本人普通文件且不含链接,不会绕过检查"],AUDIT_IO:["审计文件操作失败","检查错误码、权限和磁盘空间,不暴露路径或内容"],TEST_INPUT:["无法读取离线测试输入","请直接传入文本,或用 @<路径> 指向可读的普通文本文件"],TEST_SIZE:["离线测试输入超出大小上限","改用更小的文件或更短的片段"]},Ge={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|test <text>|test @<path>]. Only `test` takes content; never type secrets as other arguments.",testPreviewTitle:"Offline redaction test; local only, edits are discarded",testRedacted:"SPI Protecter: redacted locally ({chars}); nothing was sent to a model. Replacements are now learned for this session.",testUnchanged:"SPI Protecter: no rule matched ({chars}); the text below is unchanged.",testFileLabel:"Input: local file",testTextLabel:"Input: inline text",capabilityBlocked:"SPI Protecter: this Pi build does not expose {names}, so protection is off and requests are blocked instead of sent as plaintext. Report it: {url}",piNewer:"SPI Protecter: Pi {version} is newer than the suite-verified releases ({verified}). Every required hook is present, so protection is active and each request is verified; report anything that looks wrong: {url}",hookBypassed:"SPI Protecter: Pi did not run the request hook for a model call, so the runtime guard redacted it instead. Protection held, but please report it: {url}",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 its summary request internally, outside this extension's normal redaction hook. If sent as-is, the conversation—including protected values—could be sent in plaintext.
2
+ import{getAgentDir as vn,VERSION as Gt}from"@earendil-works/pi-coding-agent";import{Worker as An}from"node:worker_threads";import{fileURLToPath as Tn}from"node:url";import*as A from"node:fs";import{join as K,resolve as ln}from"node:path";import{createHash as dn,randomUUID as un}from"node:crypto";import{Worker as fn}from"node:worker_threads";import{fileURLToPath as mn}from"node:url";var v="https://github.com/taills/pi-information-protecter/issues/new",ze="0.84.0-0.84.x, 0.85.1-0.87.x, 0.99.x, 1.2.0-1.4.x",Wt=["on","registerCommand","registerMarkdownTransformer"],Yt=["abort"],Jt=["notify","setStatus"];function we(t,e){return!t||typeof t!="object"&&typeof t!="function"?!1:typeof t[e]=="function"}function Ae(t,e){let o=[];for(let i of Wt)we(t,i)||o.push(`pi.${i}()`);if(e===void 0)return o;let n=e;for(let i of Yt)we(n,i)||o.push(`ctx.${i}()`);for(let i of Jt)we(n.ui,i)||o.push(`ctx.ui.${i}()`);return(!n.modelRegistry||typeof n.modelRegistry!="object")&&o.push("ctx.modelRegistry"),o}function He(t){let e=/^(\d+)\.(\d+)\.(\d+)/.exec(t);if(!e)return!1;let[o,n,i]=e.slice(1).map(Number);return o===1?n>=2&&n<=4:o!==0?!1:n===84?!0:n===85?i>=1:n===86||n===87||n===99}var Te={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"],PI_CAPABILITY:["This Pi build is missing a capability this extension requires",`Update Pi; if the newest Pi still fails, report the listed capability at ${v}`],GUARD_UNAVAILABLE:["Cannot cover the model calls that have no extension hook","This Pi build exposes no model runtime to wrap; update Pi, or report this build at "+v],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'],CONFIG_ATTACHMENT:["Invalid attachment setting",'Use "ask", "allow" or "deny"'],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"],TEST_INPUT:["Cannot read the offline test input","Pass text, or @<path> pointing to a readable regular text file"],TEST_SIZE:["Offline test input exceeds the size limit","Test a smaller file or a shorter excerpt"]},Vt={INTERNAL:["未预期的本地错误","重载并检查安装包完整性"],NOT_READY:["防护尚未初始化","查看初始化错误,修复后重载"],PI_CAPABILITY:["当前 Pi 构建缺少本扩展所需的能力",`请升级 Pi;若最新版 Pi 仍然失败,请将所列能力反馈至 ${v}`],GUARD_UNAVAILABLE:["无法覆盖没有扩展钩子的模型调用","该 Pi 构建未暴露可包装的模型运行时:请升级 Pi,或将该构建反馈至 "+v],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 的对象'],CONFIG_ATTACHMENT:["附件配置值无效",'请使用 "ask"、"allow" 或 "deny"'],AUDIT_BATCH_LIMIT:["审计批次超过上限","减少单个请求的命中内容"],AUDIT_FULL:["审计日志将超过上限","检查记录后清空日志或本地归档"],AUDIT_PARTIAL:["审计日志末行不完整","本地检查或清空日志,不要上传日志"],AUDIT_LOCKED:["审计锁持续被占用","等待写入完成,全部停止后再检查残留锁"],AUDIT_UNSAFE:["审计文件不安全","使用本人普通文件且不含链接,不会绕过检查"],AUDIT_IO:["审计文件操作失败","检查错误码、权限和磁盘空间,不暴露路径或内容"],TEST_INPUT:["无法读取离线测试输入","请直接传入文本,或用 @<路径> 指向可读的普通文本文件"],TEST_SIZE:["离线测试输入超出大小上限","改用更小的文件或更短的片段"]},je={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|test <text>|test @<path>]. Only `test` takes content; never type secrets as other arguments.",testPreviewTitle:"Offline redaction test; local only, edits are discarded",testRedacted:"SPI Protecter: redacted locally ({chars}); nothing was sent to a model. Replacements are now learned for this session.",testUnchanged:"SPI Protecter: no rule matched ({chars}); the text below is unchanged.",testFileLabel:"Input: local file",testTextLabel:"Input: inline text",capabilityBlocked:"SPI Protecter: this Pi build does not expose {names}, so protection is off and requests are blocked instead of sent as plaintext. Report it: {url}",piNewer:"SPI Protecter: Pi {version} is newer than the suite-verified releases ({verified}). Every required hook is present, so protection is active and each request is verified; report anything that looks wrong: {url}",hookBypassed:"SPI Protecter: Pi did not run the request hook for a model call, so the runtime guard redacted it instead. Protection held, but please report it: {url}",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 its summary request internally, outside this extension's normal redaction hook. If sent as-is, the conversation—including protected values—could be sent in plaintext.
3
3
 
4
4
  If you allow compaction, this extension will redact the content first. Pi will then summarize the redacted content with the current model; no additional model is needed. The summary will retain the replacement values, so the model may not describe protected information accurately.
5
5
 
6
- Allowing compaction does not bypass redaction. It allows this compaction to proceed.`,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.",attachmentAskTitle:"Review attachment {index} of {total}",attachmentAskBody:"This attachment cannot be inspected or redacted by SPI Protecter. Review it before deciding whether to send it.",attachmentPathLabel:"Local file path",diagnosticRuleEntries:"Matched configuration entries:",attachmentPathUnavailable:"Not provided by Pi; locate the original in the conversation or file picker",attachmentOpenImage:"Open image",attachmentEditImage:"Edit image in default app",attachmentEditWait:"The app receives a private temporary copy. Save your edits over that same file, then return here; only the saved image will be sent.",attachmentEditDone:"Done editing",attachmentAllow:"Allow this attachment",attachmentDeny:"Cancel request",attachmentAllowed:"SPI Protecter: reviewed attachment allowed; its contents are not redacted.",attachmentPreviewTitle:"Attachment preview",attachmentPreviewHint:"Press Esc or Enter to return to the attachment decision."},Gt={compactAskTitle:"是否压缩当前会话?",compactAskBody:`Pi 会在内部构造摘要请求,该请求不会经过本扩展的常规脱敏钩子。若直接发送,原始会话内容(包括受保护的值)可能会以明文发出。
6
+ Allowing compaction does not bypass redaction. It allows this compaction to proceed.`,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.",attachmentAskTitle:"Review attachment {index} of {total}",attachmentAskBody:"This attachment cannot be inspected or redacted by SPI Protecter. Review it before deciding whether to send it.",attachmentPathLabel:"Local file path",attachmentSourceTool:"Source: image returned by tool {tool} (no local file path)",attachmentViewWait:"This terminal cannot show images inline, so a read-only private copy of the exact image to be sent was opened in the system image viewer. Return here when done; the copy is then deleted.",attachmentViewDone:"Done viewing, back to the decision",attachmentViewManual:"This terminal cannot show images inline and the system image viewer could not be opened automatically. Open the read-only private copy below manually; it is deleted when you return here.",attachmentViewPath:"Full image path",attachmentViewFailed:"SPI Protecter: could not open this image in the system viewer. Allow it only if you know its content.",diagnosticRuleEntries:"Matched configuration entries:",attachmentPathUnavailable:"Not provided by Pi; locate the original in the conversation or file picker",attachmentOpenImage:"Open image",attachmentEditImage:"Edit image in default app",attachmentEditWait:"The app receives a private temporary copy. Save your edits over that same file, then return here; only the saved image will be sent.",attachmentEditDone:"Done editing",attachmentAllow:"Allow this attachment",attachmentDeny:"Cancel request",attachmentAllowed:"SPI Protecter: reviewed attachment allowed; its contents are not redacted.",attachmentPreviewTitle:"Attachment preview",attachmentPreviewHint:"Press Esc or Enter to return to the attachment decision."},Kt={compactAskTitle:"是否压缩当前会话?",compactAskBody:`Pi 会在内部构造摘要请求,该请求不会经过本扩展的常规脱敏钩子。若直接发送,原始会话内容(包括受保护的值)可能会以明文发出。
7
7
 
8
8
  选择允许后,本扩展会先对待压缩内容脱敏,再由 Pi 使用当前模型生成摘要,无需额外模型。摘要会保留替换值,因此模型可能无法准确描述相关敏感信息。
9
9
 
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 开启新会话。",attachmentAskTitle:"查看附件({index}/{total})",attachmentAskBody:"SPI Protecter 无法检查或脱敏此附件。请先查看,再决定是否发送。",attachmentPathLabel:"本地文件路径",diagnosticRuleEntries:"涉及的配置条目内容:",attachmentPathUnavailable:"Pi 未提供本地路径;请在会话记录或文件选择器中定位原文件",attachmentOpenImage:"打开图片",attachmentEditImage:"用系统默认软件编辑图片",attachmentEditWait:"打开的软件收到的是私有临时副本。请将修改覆盖保存到同一文件,再返回此处;最终只会发送保存后的图片。",attachmentEditDone:"编辑完成",attachmentAllow:"允许发送此附件",attachmentDeny:"取消本次请求",attachmentAllowed:"SPI Protecter:已确认放行此附件;附件内容未经脱敏。",attachmentPreviewTitle:"附件预览",attachmentPreviewHint:"按 Esc 或 Enter 返回附件确认菜单。",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|test <文本>|test @<路径>]。仅 test 接受内容,其他参数不要输入敏感内容。",testPreviewTitle:"离线脱敏测试,仅本地显示,编辑不会保存",testRedacted:"SPI Protecter:已在本地脱敏({chars}),未向任何模型发送内容。替换关系已记入本会话映射。",testUnchanged:"SPI Protecter:没有规则命中({chars}),下方文本未改动。",testFileLabel:"输入:本地文件",testTextLabel:"输入:行内文本",capabilityBlocked:"SPI Protecter:当前 Pi 构建未暴露 {names},因此保护未启用,请求会被拦截而不以明文发送。请反馈:{url}",piNewer:"SPI Protecter:Pi {version} 高于已经完整测试验证的版本({verified})。所需钩子均存在,保护已生效并会逐次校验;如发现异常请反馈:{url}",hookBypassed:"SPI Protecter:Pi 未对某次模型调用执行请求钩子,已由运行时闸门脱敏。保护仍然有效,但请反馈:{url}",logsTuiOnly:"请在本地 TUI 查看保护记录。",logsNone:"暂无可读记录。",logsSelectTitle:"保护记录",logsTruncated:"(仅显示有限尾部)",logsRevealTitle:"敏感明文",logsRevealBody:"确认在本地显示?请勿分享此界面。",logsPreviewTitle:"本地预览,编辑不会保存",logsPreviewTruncated:"[预览已截断]",clearTitle:"确认清空当前审计日志?",clearBody:"不可撤销。仅清空本机日志,配置和内存映射保留;后续请求可能写入新记录。",cleared:"已清空审计日志:{bytes}。",toolBlocked:"SPI Protecter 已阻止工具访问受保护的配置或审计日志,请在本地编辑。"};function he(t=process.env){let e=t.PI_PROTECTER_LANG||t.LC_ALL||t.LC_MESSAGES||t.LANG||t.LANGUAGE||Ut();return/^zh\b|^zh[-_]/i.test(e.trim())?"zh":"en"}function Ut(){try{return new Intl.DateTimeFormat().resolvedOptions().locale}catch{return""}}var B=he();function Be(){return B}function ze(t){B=t}function l(t,e={}){return((B==="zh"?Gt:Ge)[t]??Ge[t]).replace(/\{(\w+)\}/g,(n,r)=>Object.hasOwn(e,r)?String(e[r]):n)}var Bt={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"},chars:{one:"{n} character",other:"{n} characters"}},zh:{rules:{other:"规则 {n} 条"},mappings:{other:"映射 {n} 个"},bytes:{other:"{n} 字节"},tokens:{other:"{n} token"},chars:{other:"{n} 个字符"}}},Ue=new Map;function W(t,e){let o=B==="zh"?"zh-CN":"en",n=Ue.get(B);if(!n){try{n=new Intl.PluralRules(o)}catch{n=void 0}n&&Ue.set(B,n)}let r=n?.select(e)??"other",i=Bt[B][t],c=i[r]??i.other,d=new Intl.NumberFormat(o).format(e);return c.replace("{n}",d)}function He(t){let o=(B==="zh"?vt[t]:ge[t])??ge[t]??ge.INTERNAL;return{reason:o[0],action:o[1]}}var $e={INTERNAL:"internal",NOT_READY:"initialization",PI_CAPABILITY:"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",CONFIG_ATTACHMENT:"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",TEST_INPUT:"test",TEST_SIZE:"test"};var je=["rule","otherRule","node","bytes","limit","timeoutMs","status"],zt=/^[A-Za-z][A-Za-z0-9]{0,39}$/,Ht=/^(EACCES|EPERM|ENOENT|ENOSPC|EDQUOT|EROFS|ELOOP|EISDIR|ENOTDIR|EMFILE|ENFILE|EIO|EEXIST|EBADF|EBUSY|ENOTEMPTY)$/;function $t(t,e={}){let{reason:o,action:n}=He(t),r={};for(let i of je){let c=e[i];typeof c=="number"&&Number.isSafeInteger(c)&&c>=0&&(r[i]=c)}return typeof e.errno=="string"&&Ht.test(e.errno)&&(r.errno=e.errno),typeof e.errorName=="string"&&zt.test(e.errorName)&&(r.errorName=e.errorName),{code:t,stage:$e[t],reason:o,action:n,...r}}function $(t){let e=Be()==="zh"?":":": ",o=[...je,"errno","errorName"].filter(n=>t[n]!==void 0).map(n=>`${n}=${t[n]}`).join(" ");return[l("blockedHeader"),`code=${t.code} stage=${t.stage}${o?` ${o}`:""}`,`${l("reasonLabel")}${e}${t.reason}`,`${l("actionLabel")}${e}${t.action}`,`${l("detailsLabel")}${e}/protecter status`].join(`
11
- `)}var z=class extends Error{diagnostic;constructor(e,o={}){let n=$t(e,o);super($(n)),this.name="ProtectionError",this.diagnostic=n}};function s(t,e={}){return new z(t,e)}function O(t,e,o={}){if(t instanceof z)return s(t.diagnostic.code,{...t.diagnostic,...o});let n=t&&typeof t=="object"&&"code"in t?t.code:void 0;return s(e,{...o,errno:typeof n=="string"?n:void 0})}function te(t,e){if(!t||typeof t!="object")return s(e);let o=t,n=typeof o.code=="string"&&Object.hasOwn($e,o.code)?o.code:e;return s(n,o)}import{constants as J,closeSync as Ye,fchmodSync as jt,fstatSync as Yt,mkdirSync as Wt,openSync as We,readFileSync as Jt,writeFileSync as qt}from"node:fs";import{dirname as Kt}from"node:path";var Je=["ask","protected","off"],ye=["provider","model","contextWindow","reserveTokens"];function Ke(t){return!t||typeof t!="object"?[]:ye.filter(e=>Object.hasOwn(t,e))}var Ie={version:1,sensitiveWords:[]},qe="ask";function Xt(t){if(typeof t=="string"){if(!Je.includes(t))throw s("CONFIG_COMPACTION");return}if(!t||typeof t!="object"||Array.isArray(t))throw s("CONFIG_COMPACTION");let e=t,o=["mode",...ye];if(Object.keys(e).some(n=>!o.includes(n)))throw s("CONFIG_COMPACTION");if(e.mode!==void 0&&!Je.includes(e.mode))throw s("CONFIG_COMPACTION");for(let n of ye){let r=e[n];if(r!==void 0&&typeof r!="string"&&typeof r!="number")throw s("CONFIG_COMPACTION")}}function ne(t){return t===void 0?{mode:qe}:typeof t=="string"?{mode:t}:{mode:t.mode??qe}}function we(t){let e;try{e=JSON.parse(t)}catch{throw s("CONFIG_JSON")}let o=0;try{if(!e||e.version!==1||!Array.isArray(e.sensitiveWords)||Object.keys(e).some(r=>!["version","sensitiveWords","compaction","attachments"].includes(r)))throw s("CONFIG_SCHEMA");if(Object.hasOwn(e,"compaction")&&Xt(e.compaction),Object.hasOwn(e,"attachments")&&!["ask","allow","deny"].includes(e.attachments??""))throw s("CONFIG_ATTACHMENT");if(e.sensitiveWords.length>1e3)throw s("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 s("CONFIG_SCHEMA",{rule:o,limit:8192});let c=JSON.stringify(["literal",r]);if(n.has(c)&&n.get(c)!==void 0)throw s("CONFIG_CONFLICT",{rule:o});n.set(c,void 0);continue}if(!r||typeof r!="object")throw s("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 s("CONFIG_TARGET",{rule:o,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 s("CONFIG_SCHEMA",{rule:o,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 s("CONFIG_REGEX",{rule:o,limit:8192});try{new RegExp(r.pattern,r.flags??"")}catch{throw s("CONFIG_REGEX",{rule:o})}}else throw s("CONFIG_SCHEMA",{rule:o});let i=r.type==="literal"?JSON.stringify(["literal",r.value]):JSON.stringify(["regex",r.pattern,[...new Set((r.flags??"")+"g")].sort().join("")]);if(n.has(i)&&n.get(i)!==r.replacement)throw s("CONFIG_CONFLICT",{rule:o});n.set(i,r.replacement)}return e}catch(n){throw n instanceof z?n:s("CONFIG_SCHEMA",{rule:o})}}function Xe(t,e=!1){let o;try{if(e){Wt(Kt(t),{recursive:!0,mode:448});try{let i=We(t,J.O_WRONLY|J.O_CREAT|J.O_EXCL,384);try{qt(i,JSON.stringify(Ie,null,2)+`
12
- `)}finally{Ye(i)}}catch(i){if(i.code!=="EEXIST")throw O(i,"CONFIG_IO")}}o=We(t,J.O_RDONLY|J.O_NOFOLLOW|J.O_NONBLOCK);let n=Yt(o);if(n.size>1024*1024)throw s("CONFIG_SIZE",{bytes:n.size,limit:1024*1024});if(!n.isFile()||n.nlink!==1||process.getuid&&n.uid!==process.getuid())throw s("CONFIG_UNSAFE");process.platform!=="win32"&&jt(o,384);let r=Jt(o,"utf8");if(Buffer.byteLength(r)>1024*1024)throw s("CONFIG_SIZE",{bytes:Buffer.byteLength(r),limit:1024*1024});return{config:we(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&&Ye(o)}}var Te=/^protecter(?:\.[a-f0-9]{32})?\.json$/;function nn(t){return new Promise((e,o)=>{let n=new en(tn(new URL("./validate-worker.mjs",import.meta.url)),{workerData:t,execArgv:[],resourceLimits:{maxOldGenerationSizeMb:64}}),r=!1,i=(d,u=s("WORKER_FAILED"))=>{r||(r=!0,clearTimeout(c),n.terminate(),d?e():o(u))},c=setTimeout(()=>i(!1,s("CONFIG_TIMEOUT",{timeoutMs:2e3})),2e3);n.once("message",d=>d===!0?i(!0):i(!1,te(d,"CONFIG_SCHEMA"))),n.once("error",()=>i(!1)),n.once("exit",()=>i(!1))})}function on(t){let e=new Set,o=[];for(let f of t)for(let h of f.sensitiveWords){let g=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(g)||(e.add(g),o.push(h))}let n=[];for(let f of t)f.compaction!==void 0&&n.push(ne(f.compaction));let i=["off","ask","protected"].find(f=>n.some(h=>h.mode===f)),c={version:1,sensitiveWords:o};i!==void 0&&(c.compaction={mode:i});let d=t.map(f=>f.attachments).filter(f=>f!==void 0);d.length&&(c.attachments=["deny","ask","allow"].find(f=>d.includes(f)));let u=JSON.stringify(c);if(Buffer.byteLength(u)>1024*1024)throw s("CONFIG_SIZE",{bytes:Buffer.byteLength(u),limit:1024*1024});return we(u)}function Ve(t){let e;try{e=w.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 s("CONFIG_UNSAFE");return`${e.dev}:${e.ino}:${e.size}:${e.mtimeNs}`}function Ae(t,e){let o=q(t,e),n=Ve(o),r=Xe(o);if(Ve(o)!==n)throw s("MIGRATION_CHANGED");return{name:e,...r,identity:n,digest:Zt("sha256").update(r.raw).digest("hex")}}function Z(t,e){let o=Ae(t,e.name);if(o.identity!==e.identity||o.digest!==e.digest)throw s("MIGRATION_CHANGED")}function oe(t){return w.readdirSync(t).filter(e=>Te.test(e)).sort()}function Ze(t){if(process.platform==="win32")return;let e=w.openSync(t,w.constants.O_RDONLY);try{w.fsyncSync(e)}finally{w.closeSync(e)}}async function Qe(t,e,o={}){if(!/^[a-f0-9]{32}$/.test(e))throw s("MACHINE_ID");let n=Vt(t),r=`protecter.${e}.json`,i=q(n,r);w.mkdirSync(n,{recursive:!0,mode:448});let c=q(n,".protecter-migration.lock"),d=Date.now()+(o.lockTimeoutMs??5e3);for(;;)try{w.mkdirSync(c,{mode:448});break}catch(f){if(f.code!=="EEXIST")throw O(f,"MIGRATION_IO");if(Date.now()>=d)throw s("MIGRATION_LOCKED",{timeoutMs:o.lockTimeoutMs??5e3});await new Promise(h=>setTimeout(h,50))}let u;try{let f=oe(n);if(f.length>64)throw s("MIGRATION_LIMIT",{limit:64});let g=[...f].sort((E,K)=>E===r?-1:K===r?1:E.localeCompare(K)).map(E=>Ae(n,E)),I=on(g.length?g.map(E=>E.config):[Ie]);if(await nn([I]),g.length===1&&g[0].name===r){if(Z(n,g[0]),JSON.stringify(oe(n))!==JSON.stringify(f))throw s("MIGRATION_CHANGED");return{path:i,config:g[0].config,raw:g[0].raw,sourceRaws:[g[0].raw],migrated:0}}let N=JSON.stringify(I,null,2)+`
13
- `;if(Buffer.byteLength(N)>1024*1024)throw s("CONFIG_SIZE",{bytes:Buffer.byteLength(N),limit:1024*1024});u=q(n,`.protecter-migration.${Qt()}.tmp`);let _=w.openSync(u,w.constants.O_WRONLY|w.constants.O_CREAT|w.constants.O_EXCL,384);try{w.writeFileSync(_,N),w.fsyncSync(_)}finally{w.closeSync(_)}if(o.checkpoint?.("beforeCommit",i),JSON.stringify(oe(n))!==JSON.stringify(f))throw s("MIGRATION_CHANGED");for(let E of g)Z(n,E);w.renameSync(u,i),u=void 0,Ze(n);let x=Ae(n,r);if(x.raw!==N)throw s("MIGRATION_CHANGED");o.checkpoint?.("afterCommit",i);for(let E of g)E.name!==r&&(o.checkpoint?.("beforeDelete",q(n,E.name)),Z(n,x),Z(n,E),w.unlinkSync(q(n,E.name)));Ze(n);let D=[r];if(JSON.stringify(oe(n))!==JSON.stringify(D))throw s("MIGRATION_CHANGED");return Z(n,x),{path:i,config:I,raw:N,sourceRaws:[...new Set([...g.map(E=>E.raw),N])],migrated:g.filter(E=>E.name!==r).length}}catch(f){throw f instanceof z?f:O(f,"MIGRATION_IO")}finally{if(u)try{w.unlinkSync(u)}catch{}w.rmdirSync(c)}}import{createHash as rn}from"node:crypto";import{readFileSync as sn}from"node:fs";import{execFileSync as et}from"node:child_process";function Oe(t){let e=t.trim().toLowerCase().replace(/-/g,"");if(!/^[a-f0-9]{32}$/.test(e)||/^0+$/.test(e))throw s("MACHINE_ID");return rn("sha256").update(`pi-information-protecter:v1:${e}`).digest("hex").slice(0,32)}function tt(){try{if(process.platform==="linux")return Oe(sn("/etc/machine-id","utf8"));if(process.platform==="darwin"){let t=et("/usr/sbin/ioreg",["-rd1","-c","IOPlatformExpertDevice"],{encoding:"utf8",timeout:5e3,maxBuffer:1048576,stdio:["ignore","pipe","pipe"]});return Oe(t.match(/"IOPlatformUUID"\s*=\s*"([a-fA-F0-9-]+)"/)?.[1]??"")}if(process.platform==="win32"){let t=et("reg.exe",["query","HKLM\\SOFTWARE\\Microsoft\\Cryptography","/v","MachineGuid","/reg:64"],{encoding:"utf8",timeout:5e3,maxBuffer:65536,stdio:["ignore","pipe","pipe"]});return Oe(t.match(/MachineGuid\s+REG_SZ\s+([a-fA-F0-9-]+)/i)?.[1]??"")}}catch{}throw s("MACHINE_ID")}import*as y from"node:fs";import{randomUUID as an}from"node:crypto";var nt=32*1024*1024,ot=8*1024*1024;function cn(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 Ne(t,e,o=e){let n=y.openSync(t,(e?y.constants.O_RDWR|(o?y.constants.O_CREAT:0):y.constants.O_RDONLY)|y.constants.O_NOFOLLOW|y.constants.O_NONBLOCK,384);try{let r=y.fstatSync(n);if(!r.isFile()||r.nlink!==1||process.getuid&&r.uid!==process.getuid())throw s("AUDIT_UNSAFE");return process.platform!=="win32"&&y.fchmodSync(n,384),n}catch(r){throw y.closeSync(n),O(r,"AUDIT_UNSAFE")}}function re(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 rt(t){let e=`${t}.lock`,o=Date.now()+2e3;for(;;)try{return y.mkdirSync(e,{mode:448}),e}catch(n){if(n.code!=="EEXIST")throw O(n,"AUDIT_IO");if(Date.now()>=o)throw s("AUDIT_LOCKED",{timeoutMs:2e3});await new Promise(r=>setTimeout(r,25))}}async function it(t){let e=await rt(t),o;try{try{o=Ne(t,!0,!1)}catch(r){if(r.code==="ENOENT")return 0;throw r}let n=y.fstatSync(o).size;return y.ftruncateSync(o,0),y.fsyncSync(o),n}catch(n){throw re(n)}finally{try{o!==void 0&&y.closeSync(o)}finally{y.rmdirSync(e)}}}async function st(t,e,o){if(!o.length)return;let n=new Date,r=an(),i=Buffer.from(o.map(([u,f])=>JSON.stringify({version:1,time:cn(n),timezoneOffset:-n.getTimezoneOffset(),requestId:r,provider:e||"unknown",original:f,replacement:u})).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 开启新会话。",attachmentAskTitle:"查看附件({index}/{total})",attachmentAskBody:"SPI Protecter 无法检查或脱敏此附件。请先查看,再决定是否发送。",attachmentPathLabel:"本地文件路径",attachmentSourceTool:"来源:工具 {tool} 返回的图片(无本地文件路径)",attachmentViewWait:"当前终端无法内联显示图片,已用系统看图软件打开即将发送图片的只读私有副本。查看完毕后返回此处,副本随即删除。",attachmentViewDone:"已查看,返回确认菜单",attachmentViewManual:"当前终端无法内联显示图片,且未能自动打开系统看图软件。请手动打开下方的只读私有副本;返回此处后副本即删除。",attachmentViewPath:"图片完整路径",attachmentViewFailed:"SPI Protecter:无法用系统看图软件打开此图片。仅在确知其内容时才允许发送。",diagnosticRuleEntries:"涉及的配置条目内容:",attachmentPathUnavailable:"Pi 未提供本地路径;请在会话记录或文件选择器中定位原文件",attachmentOpenImage:"打开图片",attachmentEditImage:"用系统默认软件编辑图片",attachmentEditWait:"打开的软件收到的是私有临时副本。请将修改覆盖保存到同一文件,再返回此处;最终只会发送保存后的图片。",attachmentEditDone:"编辑完成",attachmentAllow:"允许发送此附件",attachmentDeny:"取消本次请求",attachmentAllowed:"SPI Protecter:已确认放行此附件;附件内容未经脱敏。",attachmentPreviewTitle:"附件预览",attachmentPreviewHint:"按 Esc 或 Enter 返回附件确认菜单。",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|test <文本>|test @<路径>]。仅 test 接受内容,其他参数不要输入敏感内容。",testPreviewTitle:"离线脱敏测试,仅本地显示,编辑不会保存",testRedacted:"SPI Protecter:已在本地脱敏({chars}),未向任何模型发送内容。替换关系已记入本会话映射。",testUnchanged:"SPI Protecter:没有规则命中({chars}),下方文本未改动。",testFileLabel:"输入:本地文件",testTextLabel:"输入:行内文本",capabilityBlocked:"SPI Protecter:当前 Pi 构建未暴露 {names},因此保护未启用,请求会被拦截而不以明文发送。请反馈:{url}",piNewer:"SPI Protecter:Pi {version} 高于已经完整测试验证的版本({verified})。所需钩子均存在,保护已生效并会逐次校验;如发现异常请反馈:{url}",hookBypassed:"SPI Protecter:Pi 未对某次模型调用执行请求钩子,已由运行时闸门脱敏。保护仍然有效,但请反馈:{url}",logsTuiOnly:"请在本地 TUI 查看保护记录。",logsNone:"暂无可读记录。",logsSelectTitle:"保护记录",logsTruncated:"(仅显示有限尾部)",logsRevealTitle:"敏感明文",logsRevealBody:"确认在本地显示?请勿分享此界面。",logsPreviewTitle:"本地预览,编辑不会保存",logsPreviewTruncated:"[预览已截断]",clearTitle:"确认清空当前审计日志?",clearBody:"不可撤销。仅清空本机日志,配置和内存映射保留;后续请求可能写入新记录。",cleared:"已清空审计日志:{bytes}。",toolBlocked:"SPI Protecter 已阻止工具访问受保护的配置或审计日志,请在本地编辑。"};function Ne(t=process.env){let e=t.PI_PROTECTER_LANG||t.LC_ALL||t.LC_MESSAGES||t.LANG||t.LANGUAGE||qt();return/^zh\b|^zh[-_]/i.test(e.trim())?"zh":"en"}function qt(){try{return new Intl.DateTimeFormat().resolvedOptions().locale}catch{return""}}var z=Ne();function Ye(){return z}function Je(t){z=t}function c(t,e={}){return((z==="zh"?Kt:je)[t]??je[t]).replace(/\{(\w+)\}/g,(n,i)=>Object.hasOwn(e,i)?String(e[i]):n)}var Xt={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"},chars:{one:"{n} character",other:"{n} characters"}},zh:{rules:{other:"规则 {n} 条"},mappings:{other:"映射 {n} 个"},bytes:{other:"{n} 字节"},tokens:{other:"{n} token"},chars:{other:"{n} 个字符"}}},We=new Map;function J(t,e){let o=z==="zh"?"zh-CN":"en",n=We.get(z);if(!n){try{n=new Intl.PluralRules(o)}catch{n=void 0}n&&We.set(z,n)}let i=n?.select(e)??"other",r=Xt[z][t],l=r[i]??r.other,d=new Intl.NumberFormat(o).format(e);return l.replace("{n}",d)}function Ve(t){let o=(z==="zh"?Vt[t]:Te[t])??Te[t]??Te.INTERNAL;return{reason:o[0],action:o[1]}}var Ke={INTERNAL:"internal",NOT_READY:"initialization",PI_CAPABILITY:"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",CONFIG_ATTACHMENT:"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",TEST_INPUT:"test",TEST_SIZE:"test"};var qe=["rule","otherRule","node","bytes","limit","timeoutMs","status"],Zt=/^[A-Za-z][A-Za-z0-9]{0,39}$/,Qt=/^(EACCES|EPERM|ENOENT|ENOSPC|EDQUOT|EROFS|ELOOP|EISDIR|ENOTDIR|EMFILE|ENFILE|EIO|EEXIST|EBADF|EBUSY|ENOTEMPTY)$/;function en(t,e={}){let{reason:o,action:n}=Ve(t),i={};for(let r of qe){let l=e[r];typeof l=="number"&&Number.isSafeInteger(l)&&l>=0&&(i[r]=l)}return typeof e.errno=="string"&&Qt.test(e.errno)&&(i.errno=e.errno),typeof e.errorName=="string"&&Zt.test(e.errorName)&&(i.errorName=e.errorName),{code:t,stage:Ke[t],reason:o,action:n,...i}}function W(t){let e=Ye()==="zh"?":":": ",o=[...qe,"errno","errorName"].filter(n=>t[n]!==void 0).map(n=>`${n}=${t[n]}`).join(" ");return[c("blockedHeader"),`code=${t.code} stage=${t.stage}${o?` ${o}`:""}`,`${c("reasonLabel")}${e}${t.reason}`,`${c("actionLabel")}${e}${t.action}`,`${c("detailsLabel")}${e}/protecter status`].join(`
11
+ `)}var H=class extends Error{diagnostic;constructor(e,o={}){let n=en(e,o);super(W(n)),this.name="ProtectionError",this.diagnostic=n}};function s(t,e={}){return new H(t,e)}function N(t,e,o={}){if(t instanceof H)return s(t.diagnostic.code,{...t.diagnostic,...o});let n=t&&typeof t=="object"&&"code"in t?t.code:void 0;return s(e,{...o,errno:typeof n=="string"?n:void 0})}function re(t,e){if(!t||typeof t!="object")return s(e);let o=t,n=typeof o.code=="string"&&Object.hasOwn(Ke,o.code)?o.code:e;return s(n,o)}import{constants as V,closeSync as Xe,fchmodSync as tn,fstatSync as nn,mkdirSync as on,openSync as Ze,readFileSync as rn,writeFileSync as sn}from"node:fs";import{dirname as an}from"node:path";var Qe=["ask","protected","off"],Oe=["provider","model","contextWindow","reserveTokens"];function tt(t){return!t||typeof t!="object"?[]:Oe.filter(e=>Object.hasOwn(t,e))}var Ee={version:1,sensitiveWords:[]},et="ask";function cn(t){if(typeof t=="string"){if(!Qe.includes(t))throw s("CONFIG_COMPACTION");return}if(!t||typeof t!="object"||Array.isArray(t))throw s("CONFIG_COMPACTION");let e=t,o=["mode",...Oe];if(Object.keys(e).some(n=>!o.includes(n)))throw s("CONFIG_COMPACTION");if(e.mode!==void 0&&!Qe.includes(e.mode))throw s("CONFIG_COMPACTION");for(let n of Oe){let i=e[n];if(i!==void 0&&typeof i!="string"&&typeof i!="number")throw s("CONFIG_COMPACTION")}}function se(t){return t===void 0?{mode:et}:typeof t=="string"?{mode:t}:{mode:t.mode??et}}function Ce(t){let e;try{e=JSON.parse(t)}catch{throw s("CONFIG_JSON")}let o=0;try{if(!e||e.version!==1||!Array.isArray(e.sensitiveWords)||Object.keys(e).some(i=>!["version","sensitiveWords","compaction","attachments"].includes(i)))throw s("CONFIG_SCHEMA");if(Object.hasOwn(e,"compaction")&&cn(e.compaction),Object.hasOwn(e,"attachments")&&!["ask","allow","deny"].includes(e.attachments??""))throw s("CONFIG_ATTACHMENT");if(e.sensitiveWords.length>1e3)throw s("CONFIG_SIZE",{limit:1e3});let n=new Map;for(let i of e.sensitiveWords){if(o++,typeof i=="string"){if(!i||i.length>8192)throw s("CONFIG_SCHEMA",{rule:o,limit:8192});let l=JSON.stringify(["literal",i]);if(n.has(l)&&n.get(l)!==void 0)throw s("CONFIG_CONFLICT",{rule:o});n.set(l,void 0);continue}if(!i||typeof i!="object")throw s("CONFIG_SCHEMA",{rule:o});if(Object.hasOwn(i,"replacement")&&(typeof i.replacement!="string"||!i.replacement.trim()||i.replacement.length>8192||i.replacement.includes("__PIP_")))throw s("CONFIG_TARGET",{rule:o,limit:8192});if(i.type==="literal"){if(Object.keys(i).some(l=>!["type","value","replacement"].includes(l))||typeof i.value!="string"||!i.value||i.value.length>8192)throw s("CONFIG_SCHEMA",{rule:o,limit:8192})}else if(i.type==="regex"){if(Object.keys(i).some(l=>!["type","pattern","flags","replacement"].includes(l))||typeof i.pattern!="string"||!i.pattern||i.pattern.length>8192||i.flags!==void 0&&(typeof i.flags!="string"||!/^[gimsu]*$/.test(i.flags)))throw s("CONFIG_REGEX",{rule:o,limit:8192});try{new RegExp(i.pattern,i.flags??"")}catch{throw s("CONFIG_REGEX",{rule:o})}}else throw s("CONFIG_SCHEMA",{rule:o});let r=i.type==="literal"?JSON.stringify(["literal",i.value]):JSON.stringify(["regex",i.pattern,[...new Set((i.flags??"")+"g")].sort().join("")]);if(n.has(r)&&n.get(r)!==i.replacement)throw s("CONFIG_CONFLICT",{rule:o});n.set(r,i.replacement)}return e}catch(n){throw n instanceof H?n:s("CONFIG_SCHEMA",{rule:o})}}function nt(t,e=!1){let o;try{if(e){on(an(t),{recursive:!0,mode:448});try{let r=Ze(t,V.O_WRONLY|V.O_CREAT|V.O_EXCL,384);try{sn(r,JSON.stringify(Ee,null,2)+`
12
+ `)}finally{Xe(r)}}catch(r){if(r.code!=="EEXIST")throw N(r,"CONFIG_IO")}}o=Ze(t,V.O_RDONLY|V.O_NOFOLLOW|V.O_NONBLOCK);let n=nn(o);if(n.size>1024*1024)throw s("CONFIG_SIZE",{bytes:n.size,limit:1024*1024});if(!n.isFile()||n.nlink!==1||process.getuid&&n.uid!==process.getuid())throw s("CONFIG_UNSAFE");process.platform!=="win32"&&tn(o,384);let i=rn(o,"utf8");if(Buffer.byteLength(i)>1024*1024)throw s("CONFIG_SIZE",{bytes:Buffer.byteLength(i),limit:1024*1024});return{config:Ce(i),raw:i}}catch(n){let i=n&&typeof n=="object"&&"code"in n?n.code:void 0;throw N(n,i==="ELOOP"||i==="EISDIR"||i==="ENOTDIR"?"CONFIG_UNSAFE":"CONFIG_IO")}finally{o!==void 0&&Xe(o)}}var Se=/^protecter(?:\.[a-f0-9]{32})?\.json$/;function pn(t){return new Promise((e,o)=>{let n=new fn(mn(new URL("./validate-worker.mjs",import.meta.url)),{workerData:t,execArgv:[],resourceLimits:{maxOldGenerationSizeMb:64}}),i=!1,r=(d,u=s("WORKER_FAILED"))=>{i||(i=!0,clearTimeout(l),n.terminate(),d?e():o(u))},l=setTimeout(()=>r(!1,s("CONFIG_TIMEOUT",{timeoutMs:2e3})),2e3);n.once("message",d=>d===!0?r(!0):r(!1,re(d,"CONFIG_SCHEMA"))),n.once("error",()=>r(!1)),n.once("exit",()=>r(!1))})}function gn(t){let e=new Set,o=[];for(let f of t)for(let h of f.sensitiveWords){let g=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(g)||(e.add(g),o.push(h))}let n=[];for(let f of t)f.compaction!==void 0&&n.push(se(f.compaction));let r=["off","ask","protected"].find(f=>n.some(h=>h.mode===f)),l={version:1,sensitiveWords:o};r!==void 0&&(l.compaction={mode:r});let d=t.map(f=>f.attachments).filter(f=>f!==void 0);d.length&&(l.attachments=["deny","ask","allow"].find(f=>d.includes(f)));let u=JSON.stringify(l);if(Buffer.byteLength(u)>1024*1024)throw s("CONFIG_SIZE",{bytes:Buffer.byteLength(u),limit:1024*1024});return Ce(u)}function ot(t){let e;try{e=A.lstatSync(t,{bigint:!0})}catch(o){throw N(o,"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 _e(t,e){let o=K(t,e),n=ot(o),i=nt(o);if(ot(o)!==n)throw s("MIGRATION_CHANGED");return{name:e,...i,identity:n,digest:dn("sha256").update(i.raw).digest("hex")}}function Z(t,e){let o=_e(t,e.name);if(o.identity!==e.identity||o.digest!==e.digest)throw s("MIGRATION_CHANGED")}function ae(t){return A.readdirSync(t).filter(e=>Se.test(e)).sort()}function it(t){if(process.platform==="win32")return;let e=A.openSync(t,A.constants.O_RDONLY);try{A.fsyncSync(e)}finally{A.closeSync(e)}}async function rt(t,e,o={}){if(!/^[a-f0-9]{32}$/.test(e))throw s("MACHINE_ID");let n=ln(t),i=`protecter.${e}.json`,r=K(n,i);A.mkdirSync(n,{recursive:!0,mode:448});let l=K(n,".protecter-migration.lock"),d=Date.now()+(o.lockTimeoutMs??5e3);for(;;)try{A.mkdirSync(l,{mode:448});break}catch(f){if(f.code!=="EEXIST")throw N(f,"MIGRATION_IO");if(Date.now()>=d)throw s("MIGRATION_LOCKED",{timeoutMs:o.lockTimeoutMs??5e3});await new Promise(h=>setTimeout(h,50))}let u;try{let f=ae(n);if(f.length>64)throw s("MIGRATION_LIMIT",{limit:64});let g=[...f].sort((C,q)=>C===i?-1:q===i?1:C.localeCompare(q)).map(C=>_e(n,C)),w=gn(g.length?g.map(C=>C.config):[Ee]);if(await pn([w]),g.length===1&&g[0].name===i){if(Z(n,g[0]),JSON.stringify(ae(n))!==JSON.stringify(f))throw s("MIGRATION_CHANGED");return{path:r,config:g[0].config,raw:g[0].raw,sourceRaws:[g[0].raw],migrated:0}}let E=JSON.stringify(w,null,2)+`
13
+ `;if(Buffer.byteLength(E)>1024*1024)throw s("CONFIG_SIZE",{bytes:Buffer.byteLength(E),limit:1024*1024});u=K(n,`.protecter-migration.${un()}.tmp`);let S=A.openSync(u,A.constants.O_WRONLY|A.constants.O_CREAT|A.constants.O_EXCL,384);try{A.writeFileSync(S,E),A.fsyncSync(S)}finally{A.closeSync(S)}if(o.checkpoint?.("beforeCommit",r),JSON.stringify(ae(n))!==JSON.stringify(f))throw s("MIGRATION_CHANGED");for(let C of g)Z(n,C);A.renameSync(u,r),u=void 0,it(n);let G=_e(n,i);if(G.raw!==E)throw s("MIGRATION_CHANGED");o.checkpoint?.("afterCommit",r);for(let C of g)C.name!==i&&(o.checkpoint?.("beforeDelete",K(n,C.name)),Z(n,G),Z(n,C),A.unlinkSync(K(n,C.name)));it(n);let M=[i];if(JSON.stringify(ae(n))!==JSON.stringify(M))throw s("MIGRATION_CHANGED");return Z(n,G),{path:r,config:w,raw:E,sourceRaws:[...new Set([...g.map(C=>C.raw),E])],migrated:g.filter(C=>C.name!==i).length}}catch(f){throw f instanceof H?f:N(f,"MIGRATION_IO")}finally{if(u)try{A.unlinkSync(u)}catch{}A.rmdirSync(l)}}import{createHash as hn}from"node:crypto";import{readFileSync as yn}from"node:fs";import{execFileSync as st}from"node:child_process";function Pe(t){let e=t.trim().toLowerCase().replace(/-/g,"");if(!/^[a-f0-9]{32}$/.test(e)||/^0+$/.test(e))throw s("MACHINE_ID");return hn("sha256").update(`pi-information-protecter:v1:${e}`).digest("hex").slice(0,32)}function at(){try{if(process.platform==="linux")return Pe(yn("/etc/machine-id","utf8"));if(process.platform==="darwin"){let t=st("/usr/sbin/ioreg",["-rd1","-c","IOPlatformExpertDevice"],{encoding:"utf8",timeout:5e3,maxBuffer:1048576,stdio:["ignore","pipe","pipe"]});return Pe(t.match(/"IOPlatformUUID"\s*=\s*"([a-fA-F0-9-]+)"/)?.[1]??"")}if(process.platform==="win32"){let t=st("reg.exe",["query","HKLM\\SOFTWARE\\Microsoft\\Cryptography","/v","MachineGuid","/reg:64"],{encoding:"utf8",timeout:5e3,maxBuffer:65536,stdio:["ignore","pipe","pipe"]});return Pe(t.match(/MachineGuid\s+REG_SZ\s+([a-fA-F0-9-]+)/i)?.[1]??"")}}catch{}throw s("MACHINE_ID")}import*as I from"node:fs";import{randomUUID as In}from"node:crypto";var ct=32*1024*1024,lt=8*1024*1024;function wn(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 Re(t,e,o=e){let n=I.openSync(t,(e?I.constants.O_RDWR|(o?I.constants.O_CREAT:0):I.constants.O_RDONLY)|I.constants.O_NOFOLLOW|I.constants.O_NONBLOCK,384);try{let i=I.fstatSync(n);if(!i.isFile()||i.nlink!==1||process.getuid&&i.uid!==process.getuid())throw s("AUDIT_UNSAFE");return process.platform!=="win32"&&I.fchmodSync(n,384),n}catch(i){throw I.closeSync(n),N(i,"AUDIT_UNSAFE")}}function ce(t){let e=t&&typeof t=="object"&&"code"in t?t.code:void 0;return N(t,e==="ELOOP"||e==="EISDIR"||e==="ENOTDIR"?"AUDIT_UNSAFE":"AUDIT_IO")}async function dt(t){let e=`${t}.lock`,o=Date.now()+2e3;for(;;)try{return I.mkdirSync(e,{mode:448}),e}catch(n){if(n.code!=="EEXIST")throw N(n,"AUDIT_IO");if(Date.now()>=o)throw s("AUDIT_LOCKED",{timeoutMs:2e3});await new Promise(i=>setTimeout(i,25))}}async function ut(t){let e=await dt(t),o;try{try{o=Re(t,!0,!1)}catch(i){if(i.code==="ENOENT")return 0;throw i}let n=I.fstatSync(o).size;return I.ftruncateSync(o,0),I.fsyncSync(o),n}catch(n){throw ce(n)}finally{try{o!==void 0&&I.closeSync(o)}finally{I.rmdirSync(e)}}}async function ft(t,e,o){if(!o.length)return;let n=new Date,i=In(),r=Buffer.from(o.map(([u,f])=>JSON.stringify({version:1,time:wn(n),timezoneOffset:-n.getTimezoneOffset(),requestId:i,provider:e||"unknown",original:f,replacement:u})).join(`
14
14
  `)+`
15
- `);if(i.length>ot)throw s("AUDIT_BATCH_LIMIT",{bytes:i.length,limit:ot});let c=await rt(t),d;try{d=Ne(t,!0);let u=y.fstatSync(d).size;if(u+i.length>nt)throw s("AUDIT_FULL",{bytes:u+i.length,limit:nt});if(u){let f=Buffer.alloc(1);if(y.readSync(d,f,0,1,u-1),f[0]!==10)throw s("AUDIT_PARTIAL")}try{let f=0;for(;f<i.length;){let h=y.writeSync(d,i,f,i.length-f,u+f);if(!h)throw s("AUDIT_IO");f+=h}y.fsyncSync(d)}catch(f){throw y.ftruncateSync(d,u),y.fsyncSync(d),re(f)}}catch(u){throw re(u)}finally{d!==void 0&&y.closeSync(d),y.rmdirSync(c)}}function at(t,e=20){if(!Number.isInteger(e)||e<1||e>100)throw s("INTERNAL",{limit:100});let o;try{try{o=Ne(t,!1)}catch(I){if(I.code==="ENOENT")return{records:[],truncated:!1};throw I}let n=y.fstatSync(o).size,r=Math.max(0,n-1024*1024),i=Buffer.alloc(n-r),c=y.readSync(o,i,0,i.length,r),d=i.subarray(0,c).toString("utf8");if(r){let I=d.indexOf(`
16
- `);d=I<0?"":d.slice(I+1)}let u=d.split(`
17
- `),f=u.pop()!=="",h=[],g=!1;for(let I of u.slice(-e))try{let N=JSON.parse(I);if(N.version!==1||typeof N.time!="string"||typeof N.provider!="string"||typeof N.original!="string"||typeof N.replacement!="string"||typeof N.requestId!="string"||typeof N.timezoneOffset!="number")throw new Error;h.push(N)}catch{g=!0}return{records:h,truncated:r>0||u.length>e||f||g}}catch(n){throw re(n)}finally{o!==void 0&&y.closeSync(o)}}var ie=class{constructor(e,o=2e3,n=tt){this.configDir=e;this.timeoutMs=o;this.machineId=n}configDir;timeoutMs;machineId;tokens=new Map;queue=Promise.resolve();workers=new Set;closed=!1;attachmentOptions={};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 o=await Qe(this.configDir,e);if(this.closed)throw s("NOT_READY");this.snapshot=o,this.attachmentOptions.mode=o.config.attachments??"ask",this.lastError=void 0}catch(e){this.attachmentOptions.mode="deny";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 at(this.auditPath,e)}clearAuditRecords(){let e=this.queue.then(()=>{if(!this.ready)throw this.notReady();return it(this.auditPath)});return this.queue=e.catch(()=>{}),e}get ruleCount(){return this.snapshot?.config.sensitiveWords.length??0}ruleAt(e){if(!(!Number.isSafeInteger(e)||e<1))return this.snapshot?.config.sensitiveWords[e-1]}get compaction(){return ne(this.snapshot?.config.compaction)}get compactionMode(){return this.compaction.mode}get attachmentMode(){return this.attachmentOptions.mode??"ask"}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 i=O(r,"INTERNAL");throw this.lastError=i.diagnostic,i});return this.queue=n.catch(()=>{}),n}notReady(){return this.lastError?s(this.lastError.code,this.lastError):s("NOT_READY")}async scan(e,o){if(!this.ready)throw this.notReady();let{config:n,sourceRaws:r}=this.snapshot,i;try{i=JSON.stringify(e)}catch{throw s("PAYLOAD_JSON")}if(!i)throw s("PAYLOAD_JSON");if(Buffer.byteLength(i)>8*1024*1024)throw s("PAYLOAD_SIZE",{bytes:Buffer.byteLength(i),limit:8*1024*1024});let c;try{c=JSON.parse(i)}catch{throw s("PAYLOAD_JSON")}let d=await new Promise((u,f)=>{let h=!1,g;try{g=new ln(dn(new URL("./scan-worker.mjs",import.meta.url)),{workerData:{payload:c,rules:n.sensitiveWords,entries:[...this.tokens],configRaws:r},execArgv:[],resourceLimits:{maxOldGenerationSizeMb:128}})}catch(_){f(O(_,"WORKER_FAILED"));return}this.workers.add(g);let I=(_,x=s("WORKER_FAILED"))=>{if(!h){if(h=!0,clearTimeout(N),this.workers.delete(g),g.terminate(),!_||this.closed){f(this.closed?s("NOT_READY"):x);return}u(_)}},N=setTimeout(()=>I(void 0,s("SCAN_TIMEOUT",{timeoutMs:this.timeoutMs})),this.timeoutMs);g.once("message",_=>{_?.failed?I(void 0,te(_.diagnostic,"SCAN_INTERNAL")):_&&Array.isArray(_.additions)&&Array.isArray(_.hits)?I(_):I()}),g.once("error",()=>I()),g.once("exit",()=>I())});if(this.closed)throw s("NOT_READY");if(await st(this.auditPath,o,d.hits),this.closed)throw s("NOT_READY");for(let[u,f]of d.additions)this.tokens.set(u,f);return d.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 s("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,i]of Object.entries(e)){let c=this.restoreText(r);if(Object.hasOwn(n,c))throw s("RESTORE_KEY_COLLISION");n[c]=this.restore(i,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 un,statSync as ct,readdirSync as fn}from"node:fs";import{homedir as mn}from"node:os";import{basename as Ee,dirname as Q,isAbsolute as pn,relative as dt,resolve as se}from"node:path";import{fileURLToPath as gn}from"node:url";function hn(t,e){let o=t.replace(/^@/,"");return o.startsWith("file://")&&(o=gn(o)),(o==="~"||o.startsWith("~/"))&&(o=mn()+o.slice(1)),se(e,o)}function Ce(t){try{return un(t)}catch{let e=Q(t);return e===t?t:se(Ce(e),dt(e,t))}}function yn(t,e){let o=dt(t,e);return o===""||!o.startsWith("..")&&!pn(o)}function lt(t,e){try{let o=ct(t),n=ct(e);return o.dev===n.dev&&o.ino===n.ino}catch{return!1}}function ut(t,e,o,n){if(!n)return!0;let r=Ce(se(n));if(["bash","powershell"].includes(t)){let d=typeof e.command=="string"?e.command:"";if(/protecter(?:\.[a-f0-9]{32})?\.jsonl?|\.protecter-migration/i.test(d)||d.includes(n))return!0}let i=e.path??e.file_path??e.filePath,c=["grep","find","ls"].includes(t);if(typeof i!="string"&&!c)return!1;try{let d=hn(typeof i=="string"?i:".",o),u=Ce(d),f=/^protecter\.[a-f0-9]{32}\.jsonl(?:\.lock)?$/,h=fn(Q(r)).filter(g=>f.test(g)).some(g=>lt(d,se(Q(r),g)));return Q(u)===Q(r)&&(Te.test(Ee(u))||f.test(Ee(u))||Ee(u).startsWith(".protecter-migration"))||u===r||lt(d,n)||h||c&&yn(u,r)}catch{return!0}}function _e(t,e){t.length=0;for(let o of e)t.push(o)}function Se(t){if(!Array.isArray(t))throw s("COMPACT_FAILED");return t}function ft(t){if(typeof t!="string")throw s("COMPACT_FAILED");return t}async function mt(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 s("COMPACT_FAILED");if(typeof t.customInstructions=="string"&&r.instructions!==t.customInstructions)throw s("COMPACT_INSTRUCTIONS");let i=Se(r.messages),c=Se(r.prefix),d=n.previousSummary===void 0?void 0:ft(r.previous);_e(n.messagesToSummarize,i),_e(n.turnPrefixMessages,c),d!==void 0&&(n.previousSummary=d)}async function pt(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 s("COMPACT_FAILED");let i=Se(r.entries),c=n.customInstructions===void 0?void 0:ft(r.instructions);return _e(n.entriesToSummarize,i),c===void 0?{}:{customInstructions:c}}var gt=["stream","streamSimple"],In=["classify","generateImages"],ht=gt,Re=Symbol.for("pi-information-protecter.runtime-guard");function wn(t){let e=t?.runtime;if(!e||typeof e!="object")throw s("GUARD_UNAVAILABLE");let o=e;for(let n of ht)if(typeof o[n]!="function")throw s("GUARD_UNAVAILABLE");return o}function An(t){return!t||typeof t!="object"?!1:t.onPayload!==void 0}function Tn(t,e,o,n){if(!e||typeof e!="object")return;let r=e.onPayload;if(!n||typeof r!="function")return e;let i=r;return{...e,onPayload:async c=>{let d=n.runs(),u=await i(c);return n.runs()>d?u:(n.onBypass?.(),o(t,u===void 0?c:u))}}}function yt(t,e,o){let n=wn(t);n[Re]?.();let r=[...ht,...In.filter(c=>typeof n[c]=="function")].map(c=>[c,n[c]]);for(let[c,d]of r){let u=gt.includes(c),f=function(h,g,I){return u&&An(I)?d.call(this,h,g,Tn(h,I,e,o)):(async()=>d.call(this,h,await e(h,g),I))()};n[c]=f}let i=()=>{for(let[c,d]of r)n[c]=d;delete n[Re]};return Object.defineProperty(n,Re,{value:i,configurable:!0,enumerable:!1,writable:!0}),i}function ce(t,e=0){if(e>80||!t||typeof t!="object")return!1;if(Array.isArray(t))return t.some(n=>ce(n,e+1));let o=t;return Pe(o)||Object.keys(o).some(ke)?!0:Object.values(o).some(n=>ce(n,e+1))}var On=/^data:(image\/[a-z0-9.+-]+);base64,([a-z0-9+/=\s]*)$/i;function It(t){if(typeof t!="string")return;let e=On.exec(t);return e?{type:"image",mimeType:e[1].toLowerCase(),data:e[2].replace(/\s+/g,"")}:void 0}var wt=(t,e)=>`data:${t.mimeType??e??"image/png"};base64,${t.data}`;function At(t){let e=typeof t.mimeType=="string"?t.mimeType:typeof t.mime_type=="string"?t.mime_type:void 0;if(typeof t.data=="string"&&(t.type==="image"||e?.startsWith("image/"))){let r=typeof t.mimeType=="string"||typeof t.mime_type!="string"?"mimeType":"mime_type";return{image:{type:"image",data:t.data,mimeType:e},write:i=>({...t,data:i.data,[r]:i.mimeType??e})}}let o=t.source;if(t.type==="image"&&o&&typeof o=="object"&&o.type==="base64"&&typeof o.data=="string"){let r=typeof o.media_type=="string"?o.media_type:void 0;return{image:{type:"image",data:o.data,mimeType:r},write:i=>({...t,source:{...o,data:i.data,media_type:i.mimeType??r}})}}let n=t.image_url;if(n&&typeof n=="object"){let r=It(n.url);if(r)return{image:r,write:i=>({...t,image_url:{...n,url:wt(i,r.mimeType)}})}}if(typeof t.image_url=="string"){let r=It(t.image_url);if(r)return{image:r,write:i=>({...t,image_url:wt(i,r.mimeType)})}}}var Pe=t=>typeof t.type=="string"&&/image|audio|video|document|(^|_)file($|_)/i.test(t.type);function ee(t,e,o=0){if(o>80||!t||typeof t!="object")return t;if(Array.isArray(t))return t.map(c=>ee(c,e,o+1));let n=t,r=At(n);if(r)return e({image:r.image,path:ae(n)},r)??t;if(Pe(n))return e({path:ae(n)})??t;let i=Object.create(null);for(let[c,d]of Object.entries(n))if(ke(c)){let u=d&&typeof d=="object"&&!Array.isArray(d)?d:void 0,f=u?At(u):void 0;i[c]=e({image:f?.image,path:u?ae(u):void 0},f)??d}else i[c]=ee(d,e,o+1);return i}function Tt(t){let e=[];return ee(t,o=>{e.push(o.path)}),e}function ae(t){for(let e of["filePath","file_path","path"]){let o=t[e];if(typeof o=="string"&&o.trim())return o}for(let e of["source","file","metadata"]){let o=t[e];if(o&&typeof o=="object"){let n=ae(o);if(n)return n}}}function Ot(t){let e=[];return ee(t,o=>{e.push(o.image)}),e}function Nt(t,e){let o=0;return ee(t,(n,r)=>{if(!n.image||!r)return;let i=e[o++];return i?r.write(i):void 0})}var ke=t=>/^(inlineData|inline_data|fileData|file_data|image_url|input_audio)$/.test(t);async function le(t,e,o=0){if(o>80)throw s("SCAN_COMPLEXITY");if(typeof t=="string")return e(t);if(Array.isArray(t))return Promise.all(t.map(n=>le(n,e,o+1)));if(t&&typeof t=="object"){let n=t;if(Pe(n))return t;let r=Object.create(null);for(let[i,c]of Object.entries(n))ke(i)||i==="type"&&typeof c=="string"?r[i]=c:r[i]=await le(c,e,o+1);return r}return t}import{constants as Et}from"node:fs";import{spawn as Nn}from"node:child_process";import{lstat as En,mkdtemp as Cn,open as _n,rm as Sn,writeFile as Rn}from"node:fs/promises";import{tmpdir as Pn}from"node:os";import{join as Ct}from"node:path";var De={"image/png":{extension:".png",mimeType:"image/png"},"image/jpeg":{extension:".jpg",mimeType:"image/jpeg"},"image/webp":{extension:".webp",mimeType:"image/webp"},"image/gif":{extension:".gif",mimeType:"image/gif"}},de=25*1024*1024;function _t(t){let e=t.mimeType??"image/png";return!De[e]||t.data.length>Math.ceil(de*4/3)+4?!1:be(Buffer.from(t.data,"base64"))===e}async function St(t){let e="";if(process.platform==="darwin"?e="open":process.platform==="win32"?e="explorer.exe":process.platform==="linux"&&(e="xdg-open"),!e)throw s("SCAN_ATTACHMENT");let o=[t];await new Promise((n,r)=>{let i=Nn(e,o,{detached:!0,stdio:"ignore"});i.once("error",r),i.once("spawn",()=>{i.unref(),n()})})}function be(t){if(t.length>=8&&t.subarray(0,8).equals(Buffer.from([137,80,78,71,13,10,26,10])))return"image/png";if(t.length>=3&&t[0]===255&&t[1]===216&&t[2]===255)return"image/jpeg";if(t.length>=6&&(t.toString("ascii",0,6)==="GIF87a"||t.toString("ascii",0,6)==="GIF89a"))return"image/gif";if(t.length>=12&&t.toString("ascii",0,4)==="RIFF"&&t.toString("ascii",8,12)==="WEBP")return"image/webp"}async function Rt(t,e){let o=t.mimeType??"image/png",n=De[o];if(!n)throw s("SCAN_ATTACHMENT");let r=Buffer.from(t.data,"base64"),i=r.toString("base64").replace(/=+$/,"");if(!r.length||r.length>de||i!==t.data.replace(/=+$/,"")||be(r)!==o)throw s("SCAN_ATTACHMENT");let c=await Cn(Ct(Pn(),"pi-protecter-image-")),d=Ct(c,`attachment${n.extension}`);try{await Rn(d,r,{mode:384,flag:"wx"}),await e(d);let u=await En(d);if(!u.isFile()||u.isSymbolicLink()||u.nlink!==1||u.size<=0||u.size>de)throw s("SCAN_ATTACHMENT");let f=await _n(d,Et.O_RDONLY|(Et.O_NOFOLLOW??0)),h;try{let I=await f.stat();if(!I.isFile()||I.nlink!==1||I.size<=0||I.size>de)throw s("SCAN_ATTACHMENT");h=await f.readFile()}finally{await f.close()}let g=be(h);if(!g||!De[g]||h.equals(r))throw s("SCAN_ATTACHMENT");return{data:h.toString("base64"),mimeType:g}}finally{await Sn(c,{recursive:!0,force:!0})}}import{Image as Dn}from"@earendil-works/pi-tui";import{lstat as bn,readFile as Ln}from"node:fs/promises";import{createHash as kt}from"node:crypto";import{resolve as bt}from"node:path";var Dt=256*1024;async function Mn(t){let e=i=>/^(['"]).*\1$/s.test(i)?i.slice(1,-1):i,o=e(t);if(!o.startsWith("@"))return{text:o,fromFile:!1};let n=bt(e(o.slice(1).trim())),r;try{r=await bn(n)}catch(i){throw O(i,"TEST_INPUT")}if(!r.isFile())throw s("TEST_INPUT");if(r.size>Dt)throw s("TEST_SIZE",{bytes:r.size,limit:Dt});try{return{text:await Ln(n,"utf8"),fromFile:!0}}catch(i){throw O(i,"TEST_INPUT")}}function Fn(t){ze(he());let e=new ie(kn()),o=pe(t),n=()=>o.length===0,r=0,i=!1,c=new Set,d=new Set,u=new Map,f=p=>kt("sha256").update(p).digest("hex"),h=!1,g,I,N,_=p=>{let a=p?.provider;return typeof a=="string"?a:"unknown"},x=p=>{let a=[...new Set([p.rule,p.otherRule])].filter(m=>m!==void 0).flatMap(m=>{let A=e.ruleAt(m);return A===void 0?[]:[`#${m} ${JSON.stringify(A)}`]});return a.length?`${l("diagnosticRuleEntries")}
15
+ `);if(r.length>lt)throw s("AUDIT_BATCH_LIMIT",{bytes:r.length,limit:lt});let l=await dt(t),d;try{d=Re(t,!0);let u=I.fstatSync(d).size;if(u+r.length>ct)throw s("AUDIT_FULL",{bytes:u+r.length,limit:ct});if(u){let f=Buffer.alloc(1);if(I.readSync(d,f,0,1,u-1),f[0]!==10)throw s("AUDIT_PARTIAL")}try{let f=0;for(;f<r.length;){let h=I.writeSync(d,r,f,r.length-f,u+f);if(!h)throw s("AUDIT_IO");f+=h}I.fsyncSync(d)}catch(f){throw I.ftruncateSync(d,u),I.fsyncSync(d),ce(f)}}catch(u){throw ce(u)}finally{d!==void 0&&I.closeSync(d),I.rmdirSync(l)}}function mt(t,e=20){if(!Number.isInteger(e)||e<1||e>100)throw s("INTERNAL",{limit:100});let o;try{try{o=Re(t,!1)}catch(w){if(w.code==="ENOENT")return{records:[],truncated:!1};throw w}let n=I.fstatSync(o).size,i=Math.max(0,n-1024*1024),r=Buffer.alloc(n-i),l=I.readSync(o,r,0,r.length,i),d=r.subarray(0,l).toString("utf8");if(i){let w=d.indexOf(`
16
+ `);d=w<0?"":d.slice(w+1)}let u=d.split(`
17
+ `),f=u.pop()!=="",h=[],g=!1;for(let w of u.slice(-e))try{let E=JSON.parse(w);if(E.version!==1||typeof E.time!="string"||typeof E.provider!="string"||typeof E.original!="string"||typeof E.replacement!="string"||typeof E.requestId!="string"||typeof E.timezoneOffset!="number")throw new Error;h.push(E)}catch{g=!0}return{records:h,truncated:i>0||u.length>e||f||g}}catch(n){throw ce(n)}finally{o!==void 0&&I.closeSync(o)}}var le=class{constructor(e,o=2e3,n=at){this.configDir=e;this.timeoutMs=o;this.machineId=n}configDir;timeoutMs;machineId;tokens=new Map;queue=Promise.resolve();workers=new Set;closed=!1;attachmentOptions={};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 s("NOT_READY");this.snapshot=void 0;let e;try{e=this.machineId()}catch{throw s("MACHINE_ID")}let o=await rt(this.configDir,e);if(this.closed)throw s("NOT_READY");this.snapshot=o,this.attachmentOptions.mode=o.config.attachments??"ask",this.lastError=void 0}catch(e){this.attachmentOptions.mode="deny";let o=N(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 mt(this.auditPath,e)}clearAuditRecords(){let e=this.queue.then(()=>{if(!this.ready)throw this.notReady();return ut(this.auditPath)});return this.queue=e.catch(()=>{}),e}get ruleCount(){return this.snapshot?.config.sensitiveWords.length??0}ruleAt(e){if(!(!Number.isSafeInteger(e)||e<1))return this.snapshot?.config.sensitiveWords[e-1]}get compaction(){return se(this.snapshot?.config.compaction)}get compactionMode(){return this.compaction.mode}get attachmentMode(){return this.attachmentOptions.mode??"ask"}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(i=>{let r=N(i,"INTERNAL");throw this.lastError=r.diagnostic,r});return this.queue=n.catch(()=>{}),n}notReady(){return this.lastError?s(this.lastError.code,this.lastError):s("NOT_READY")}async scan(e,o){if(!this.ready)throw this.notReady();let{config:n,sourceRaws:i}=this.snapshot,r;try{r=JSON.stringify(e)}catch{throw s("PAYLOAD_JSON")}if(!r)throw s("PAYLOAD_JSON");if(Buffer.byteLength(r)>8*1024*1024)throw s("PAYLOAD_SIZE",{bytes:Buffer.byteLength(r),limit:8*1024*1024});let l;try{l=JSON.parse(r)}catch{throw s("PAYLOAD_JSON")}let d=await new Promise((u,f)=>{let h=!1,g;try{g=new An(Tn(new URL("./scan-worker.mjs",import.meta.url)),{workerData:{payload:l,rules:n.sensitiveWords,entries:[...this.tokens],configRaws:i},execArgv:[],resourceLimits:{maxOldGenerationSizeMb:128}})}catch(S){f(N(S,"WORKER_FAILED"));return}this.workers.add(g);let w=(S,G=s("WORKER_FAILED"))=>{if(!h){if(h=!0,clearTimeout(E),this.workers.delete(g),g.terminate(),!S||this.closed){f(this.closed?s("NOT_READY"):G);return}u(S)}},E=setTimeout(()=>w(void 0,s("SCAN_TIMEOUT",{timeoutMs:this.timeoutMs})),this.timeoutMs);g.once("message",S=>{S?.failed?w(void 0,re(S.diagnostic,"SCAN_INTERNAL")):S&&Array.isArray(S.additions)&&Array.isArray(S.hits)?w(S):w()}),g.once("error",()=>w()),g.once("exit",()=>w())});if(this.closed)throw s("NOT_READY");if(await ft(this.auditPath,o,d.hits),this.closed)throw s("NOT_READY");for(let[u,f]of d.additions)this.tokens.set(u,f);return d.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,i)=>i.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 s("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 i=Number(n);return!Number.isFinite(i)||String(i)!==n?e:i}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[i,r]of Object.entries(e)){let l=this.restoreText(i);if(Object.hasOwn(n,l))throw s("RESTORE_KEY_COLLISION");n[l]=this.restore(r,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 Nn,statSync as pt,readdirSync as On}from"node:fs";import{homedir as En}from"node:os";import{basename as ke,dirname as Q,isAbsolute as Cn,relative as ht,resolve as de}from"node:path";import{fileURLToPath as _n}from"node:url";function Sn(t,e){let o=t.replace(/^@/,"");return o.startsWith("file://")&&(o=_n(o)),(o==="~"||o.startsWith("~/"))&&(o=En()+o.slice(1)),de(e,o)}function be(t){try{return Nn(t)}catch{let e=Q(t);return e===t?t:de(be(e),ht(e,t))}}function Pn(t,e){let o=ht(t,e);return o===""||!o.startsWith("..")&&!Cn(o)}function gt(t,e){try{let o=pt(t),n=pt(e);return o.dev===n.dev&&o.ino===n.ino}catch{return!1}}function yt(t,e,o,n){if(!n)return!0;let i=be(de(n));if(["bash","powershell"].includes(t)){let d=typeof e.command=="string"?e.command:"";if(/protecter(?:\.[a-f0-9]{32})?\.jsonl?|\.protecter-migration/i.test(d)||d.includes(n))return!0}let r=e.path??e.file_path??e.filePath,l=["grep","find","ls"].includes(t);if(typeof r!="string"&&!l)return!1;try{let d=Sn(typeof r=="string"?r:".",o),u=be(d),f=/^protecter\.[a-f0-9]{32}\.jsonl(?:\.lock)?$/,h=On(Q(i)).filter(g=>f.test(g)).some(g=>gt(d,de(Q(i),g)));return Q(u)===Q(i)&&(Se.test(ke(u))||f.test(ke(u))||ke(u).startsWith(".protecter-migration"))||u===i||gt(d,n)||h||l&&Pn(u,i)}catch{return!0}}function De(t,e){t.length=0;for(let o of e)t.push(o)}function Le(t){if(!Array.isArray(t))throw s("COMPACT_FAILED");return t}function It(t){if(typeof t!="string")throw s("COMPACT_FAILED");return t}async function wt(t,e,o){let{preparation:n}=t,i=await e.redact({messages:n.messagesToSummarize,prefix:n.turnPrefixMessages,previous:n.previousSummary??null,instructions:t.customInstructions??null},o);if(!i||typeof i!="object")throw s("COMPACT_FAILED");if(typeof t.customInstructions=="string"&&i.instructions!==t.customInstructions)throw s("COMPACT_INSTRUCTIONS");let r=Le(i.messages),l=Le(i.prefix),d=n.previousSummary===void 0?void 0:It(i.previous);De(n.messagesToSummarize,r),De(n.turnPrefixMessages,l),d!==void 0&&(n.previousSummary=d)}async function At(t,e,o){let{preparation:n}=t,i=await e.redact({entries:n.entriesToSummarize,instructions:n.customInstructions??null},o);if(!i||typeof i!="object")throw s("COMPACT_FAILED");let r=Le(i.entries),l=n.customInstructions===void 0?void 0:It(i.instructions);return De(n.entriesToSummarize,r),l===void 0?{}:{customInstructions:l}}var Tt=["stream","streamSimple"],Rn=["classify","generateImages"],Nt=Tt,Me=Symbol.for("pi-information-protecter.runtime-guard");function kn(t){let e=t?.runtime;if(!e||typeof e!="object")throw s("GUARD_UNAVAILABLE");let o=e;for(let n of Nt)if(typeof o[n]!="function")throw s("GUARD_UNAVAILABLE");return o}function bn(t){return!t||typeof t!="object"?!1:t.onPayload!==void 0}function Dn(t,e,o,n){if(!e||typeof e!="object")return;let i=e.onPayload;if(!n||typeof i!="function")return e;let r=i;return{...e,onPayload:async l=>{let d=n.runs(),u=await r(l);return n.runs()>d?u:(n.onBypass?.(),o(t,u===void 0?l:u))}}}function Ot(t,e,o){let n=kn(t);n[Me]?.();let i=[...Nt,...Rn.filter(l=>typeof n[l]=="function")].map(l=>[l,n[l]]);for(let[l,d]of i){let u=Tt.includes(l),f=function(h,g,w){return u&&bn(w)?d.call(this,h,g,Dn(h,w,e,o)):(async()=>d.call(this,h,await e(h,g),w))()};n[l]=f}let r=()=>{for(let[l,d]of i)n[l]=d;delete n[Me]};return Object.defineProperty(n,Me,{value:r,configurable:!0,enumerable:!1,writable:!0}),r}function fe(t,e=0){if(e>80||!t||typeof t!="object")return!1;if(Array.isArray(t))return t.some(n=>fe(n,e+1));let o=t;return Fe(o)||Object.keys(o).some(xe)?!0:Object.values(o).some(n=>fe(n,e+1))}var Ln=/^data:(image\/[a-z0-9.+-]+);base64,([a-z0-9+/=\s]*)$/i;function Et(t){if(typeof t!="string")return;let e=Ln.exec(t);return e?{type:"image",mimeType:e[1].toLowerCase(),data:e[2].replace(/\s+/g,"")}:void 0}var Ct=(t,e)=>`data:${t.mimeType??e??"image/png"};base64,${t.data}`;function _t(t){let e=typeof t.mimeType=="string"?t.mimeType:typeof t.mime_type=="string"?t.mime_type:void 0;if(typeof t.data=="string"&&(t.type==="image"||e?.startsWith("image/"))){let i=typeof t.mimeType=="string"||typeof t.mime_type!="string"?"mimeType":"mime_type";return{image:{type:"image",data:t.data,mimeType:e},write:r=>({...t,data:r.data,[i]:r.mimeType??e})}}let o=t.source;if(t.type==="image"&&o&&typeof o=="object"&&o.type==="base64"&&typeof o.data=="string"){let i=typeof o.media_type=="string"?o.media_type:void 0;return{image:{type:"image",data:o.data,mimeType:i},write:r=>({...t,source:{...o,data:r.data,media_type:r.mimeType??i}})}}let n=t.image_url;if(n&&typeof n=="object"){let i=Et(n.url);if(i)return{image:i,write:r=>({...t,image_url:{...n,url:Ct(r,i.mimeType)}})}}if(typeof t.image_url=="string"){let i=Et(t.image_url);if(i)return{image:i,write:r=>({...t,image_url:Ct(r,i.mimeType)})}}}var Fe=t=>typeof t.type=="string"&&/image|audio|video|document|(^|_)file($|_)/i.test(t.type);function ee(t,e,o=0){if(o>80||!t||typeof t!="object")return t;if(Array.isArray(t))return t.map(l=>ee(l,e,o+1));let n=t,i=_t(n);if(i)return e({image:i.image,path:ue(n)},i)??t;if(Fe(n))return e({path:ue(n)})??t;let r=Object.create(null);for(let[l,d]of Object.entries(n))if(xe(l)){let u=d&&typeof d=="object"&&!Array.isArray(d)?d:void 0,f=u?_t(u):void 0;r[l]=e({image:f?.image,path:u?ue(u):void 0},f)??d}else r[l]=ee(d,e,o+1);return r}function St(t){let e=[];return ee(t,o=>{e.push(o.path)}),e}function ue(t){for(let e of["filePath","file_path","path"]){let o=t[e];if(typeof o=="string"&&o.trim())return o}for(let e of["source","file","metadata"]){let o=t[e];if(o&&typeof o=="object"){let n=ue(o);if(n)return n}}}function Pt(t){let e=[];return ee(t,o=>{e.push(o.image)}),e}function Rt(t,e){let o=0;return ee(t,(n,i)=>{if(!n.image||!i)return;let r=e[o++];return r?i.write(r):void 0})}var xe=t=>/^(inlineData|inline_data|fileData|file_data|image_url|input_audio)$/.test(t);async function me(t,e,o=0){if(o>80)throw s("SCAN_COMPLEXITY");if(typeof t=="string")return e(t);if(Array.isArray(t))return Promise.all(t.map(n=>me(n,e,o+1)));if(t&&typeof t=="object"){let n=t;if(Fe(n))return t;let i=Object.create(null);for(let[r,l]of Object.entries(n))xe(r)||r==="type"&&typeof l=="string"?i[r]=l:i[r]=await me(l,e,o+1);return i}return t}import{constants as kt}from"node:fs";import{spawn as Mn}from"node:child_process";import{lstat as Fn,mkdtemp as bt,open as xn,rm as Dt,writeFile as Lt}from"node:fs/promises";import{tmpdir as Mt}from"node:os";import{join as pe}from"node:path";var ge={"image/png":{extension:".png",mimeType:"image/png"},"image/jpeg":{extension:".jpg",mimeType:"image/jpeg"},"image/webp":{extension:".webp",mimeType:"image/webp"},"image/gif":{extension:".gif",mimeType:"image/gif"}},te=25*1024*1024;function Ft(t){let e=t.mimeType??"image/png";return!ge[e]||t.data.length>Math.ceil(te*4/3)+4?!1:he(Buffer.from(t.data,"base64"))===e}async function ve(t){let e="";if(process.platform==="darwin"?e="open":process.platform==="win32"?e="explorer.exe":process.platform==="linux"&&(e="xdg-open"),!e)throw s("SCAN_ATTACHMENT");let o=[t];await new Promise((n,i)=>{let r=Mn(e,o,{detached:!0,stdio:"ignore"});r.once("error",i),r.once("spawn",()=>{r.unref(),n()})})}function he(t){if(t.length>=8&&t.subarray(0,8).equals(Buffer.from([137,80,78,71,13,10,26,10])))return"image/png";if(t.length>=3&&t[0]===255&&t[1]===216&&t[2]===255)return"image/jpeg";if(t.length>=6&&(t.toString("ascii",0,6)==="GIF87a"||t.toString("ascii",0,6)==="GIF89a"))return"image/gif";if(t.length>=12&&t.toString("ascii",0,4)==="RIFF"&&t.toString("ascii",8,12)==="WEBP")return"image/webp"}async function xt(t,e){let o=Buffer.from(t.data,"base64"),n=ge[he(o)??""];if(!n||o.length>te)throw s("SCAN_ATTACHMENT");let i=await bt(pe(Mt(),"pi-protecter-view-"));try{let r=pe(i,`attachment${n.extension}`);await Lt(r,o,{mode:256,flag:"wx"}),await e(r)}finally{await Dt(i,{recursive:!0,force:!0})}}async function vt(t,e){let o=t.mimeType??"image/png",n=ge[o];if(!n)throw s("SCAN_ATTACHMENT");let i=Buffer.from(t.data,"base64"),r=i.toString("base64").replace(/=+$/,"");if(!i.length||i.length>te||r!==t.data.replace(/=+$/,"")||he(i)!==o)throw s("SCAN_ATTACHMENT");let l=await bt(pe(Mt(),"pi-protecter-image-")),d=pe(l,`attachment${n.extension}`);try{await Lt(d,i,{mode:384,flag:"wx"}),await e(d);let u=await Fn(d);if(!u.isFile()||u.isSymbolicLink()||u.nlink!==1||u.size<=0||u.size>te)throw s("SCAN_ATTACHMENT");let f=await xn(d,kt.O_RDONLY|(kt.O_NOFOLLOW??0)),h;try{let w=await f.stat();if(!w.isFile()||w.nlink!==1||w.size<=0||w.size>te)throw s("SCAN_ATTACHMENT");h=await f.readFile()}finally{await f.close()}let g=he(h);if(!g||!ge[g]||h.equals(i))throw s("SCAN_ATTACHMENT");return{data:h.toString("base64"),mimeType:g}}finally{await Dt(l,{recursive:!0,force:!0})}}import*as ne from"@earendil-works/pi-tui";import{lstat as Gn,readFile as Un}from"node:fs/promises";import{createHash as Ut}from"node:crypto";import{resolve as Bt}from"node:path";var $n=/\.(png|jpe?g|gif|webp|bmp|tiff?|heic|heif|avif|svg)$/i,$t=256*1024;async function Bn(t){let e=r=>/^(['"]).*\1$/s.test(r)?r.slice(1,-1):r,o=e(t);if(!o.startsWith("@"))return{text:o,fromFile:!1};let n=Bt(e(o.slice(1).trim())),i;try{i=await Gn(n)}catch(r){throw N(r,"TEST_INPUT")}if(!i.isFile())throw s("TEST_INPUT");if(i.size>$t)throw s("TEST_SIZE",{bytes:i.size,limit:$t});try{return{text:await Un(n,"utf8"),fromFile:!0}}catch(r){throw N(r,"TEST_INPUT")}}function zn(t){Je(Ne());let e=new le(vn()),o=Ae(t),n=()=>o.length===0,i=0,r=!1,l=new Set,d=new Set,u=new Map,f=m=>Ut("sha256").update(m).digest("hex"),h=!1,g,w,E,S=m=>{let a=m?.provider;return typeof a=="string"?a:"unknown"},G=m=>{let a=[...new Set([m.rule,m.otherRule])].filter(p=>p!==void 0).flatMap(p=>{let y=e.ruleAt(p);return y===void 0?[]:[`#${p} ${JSON.stringify(y)}`]});return a.length?`${c("diagnosticRuleEntries")}
18
18
  ${a.join(`
19
- `)}`:void 0},D=p=>{let a=$(p),m=x(p);return g={at:new Date().toISOString(),text:a,local:m},m?`${a}
20
- ${m}`:a},E=async(p,a,m)=>{await p.ui.custom((A,C,S,j)=>{let k=new Dn(a,m,{fallbackColor:R=>C.fg("dim",R)},{maxWidthCells:72,maxHeightCells:24});return{render(R){return[...k.render(R),"",C.fg("dim",l("attachmentPreviewHint"))]},handleInput(R){(R==="\x1B"||R==="\r"||R.toLowerCase()==="q")&&j(void 0)},invalidate(){k.invalidate()}}},{overlay:!0})};t.on("session_start",async(p,a)=>{try{if(o=pe(t,a),!n())throw a.ui.notify(l("capabilityBlocked",{names:o.join(", "),url:F}),"error"),s("PI_CAPABILITY");await e.initialize(),c.clear(),d.clear(),u.clear(),N=yt(a.modelRegistry,(C,S)=>e.redact(S,_(C)),{runs:()=>r,onBypass:()=>{if(!i){i=!0;try{a.ui.notify(l("hookBypassed",{url:F}),"warning")}catch{}}}}),h=!0,ve(Pt)||a.ui.notify(l("piNewer",{version:Pt,verified:xe,url:F}),"warning");let m=e.ruleCount;a.ui.setStatus("protecter",l("statusBarRules",{rules:W("rules",m)})),m===0&&a.ui.notify(l("emptyRules"),"warning");let A=Ke(e.rawCompaction);A.length&&a.ui.notify(l("compactIgnoredKeys",{keys:A.join(", ")}),"warning")}catch(m){h=!1,a.ui.setStatus("protecter",l("statusBarNotReady")),a.ui.notify(D(O(m,"CONFIG_IO").diagnostic),"error")}}),t.on("before_provider_request",async(p,a)=>{r++;try{if(!n())throw s("PI_CAPABILITY");let m=e.attachmentMode;if(ce(p.payload)){if(m==="deny")throw s("SCAN_ATTACHMENT");let C=p.payload;if(m==="ask"){let k=Ot(p.payload),R=Tt(p.payload),X=[],P=new Set(c),b=new Map,H=0;for(let L=0;L<k.length;L++){let T=k[L],v=T?H++:-1,M=T?kt("sha256").update(T.mimeType??"image/png").update("\0").update(T.data).digest("hex"):void 0,G=0;M&&(G=(b.get(M)??0)+1,b.set(M,G));let U=M?`${M}:${G}`:void 0,V=R[L]??(T?u.get(f(T.data)):void 0);if(V&&d.has(V)||U&&P.has(U)){X[v]=void 0;continue}let Le=T!==void 0&&_t(T),Y;T?(Y=[l("attachmentOpenImage")],Le&&Y.push(l("attachmentEditImage")),Y.push(l("attachmentAllow"),l("attachmentDeny"))):Y=[l("attachmentAllow"),l("attachmentDeny")];let Me=!1,ue=!1;for(;!Me;){let fe=await a.ui.select(`${l("attachmentAskTitle",{index:L+1,total:k.length})}
19
+ `)}`:void 0},M=m=>{let a=W(m),p=G(m);return g={at:new Date().toISOString(),text:a,local:p},p?`${a}
20
+ ${p}`:a},C=()=>{try{return typeof ne.getCapabilities=="function"&&!!ne.getCapabilities().images}catch{return!1}},q=async(m,a,p,y)=>{if(!C()){try{await xt({data:a,mimeType:p},async O=>{let _=!0;try{await ve(O)}catch{_=!1}let b=[c(_?"attachmentViewWait":"attachmentViewManual"),"",`${c("attachmentViewPath")}: ${O}`];y&&b.push(`${c("attachmentPathLabel")}: ${y}`),await m.ui.select(b.join(`
21
+ `),[c("attachmentViewDone")])})}catch{m.ui.notify(y?`${c("attachmentViewFailed")}
22
+ ${c("attachmentPathLabel")}: ${y}`:c("attachmentViewFailed"),"warning")}return}await zt(m,a,p)},zt=async(m,a,p)=>{await m.ui.custom((y,O,_,b)=>{let R=new ne.Image(a,p,{fallbackColor:P=>O.fg("dim",P)},{maxWidthCells:72,maxHeightCells:24});return{render(P){return[...R.render(P),"",O.fg("dim",c("attachmentPreviewHint"))]},handleInput(P){(P==="\x1B"||P==="\r"||P.toLowerCase()==="q")&&b(void 0)},invalidate(){R.invalidate()}}},{overlay:!0})};t.on("session_start",async(m,a)=>{try{if(o=Ae(t,a),!n())throw a.ui.notify(c("capabilityBlocked",{names:o.join(", "),url:v}),"error"),s("PI_CAPABILITY");await e.initialize(),l.clear(),d.clear(),u.clear(),E=Ot(a.modelRegistry,(O,_)=>e.redact(_,S(O)),{runs:()=>i,onBypass:()=>{if(!r){r=!0;try{a.ui.notify(c("hookBypassed",{url:v}),"warning")}catch{}}}}),h=!0,He(Gt)||a.ui.notify(c("piNewer",{version:Gt,verified:ze,url:v}),"warning");let p=e.ruleCount;a.ui.setStatus("protecter",c("statusBarRules",{rules:J("rules",p)})),p===0&&a.ui.notify(c("emptyRules"),"warning");let y=tt(e.rawCompaction);y.length&&a.ui.notify(c("compactIgnoredKeys",{keys:y.join(", ")}),"warning")}catch(p){h=!1,a.ui.setStatus("protecter",c("statusBarNotReady")),a.ui.notify(M(N(p,"CONFIG_IO").diagnostic),"error")}}),t.on("before_provider_request",async(m,a)=>{i++;try{if(!n())throw s("PI_CAPABILITY");let p=e.attachmentMode;if(fe(m.payload)){if(p==="deny")throw s("SCAN_ATTACHMENT");let O=m.payload;if(p==="ask"){let R=Pt(m.payload),P=St(m.payload),D=[],k=new Set(l),F=new Map,j=0;for(let x=0;x<R.length;x++){let T=R[x],U=T?j++:-1,L=T?Ut("sha256").update(T.mimeType??"image/png").update("\0").update(T.data).digest("hex"):void 0,$=0;L&&($=(F.get(L)??0)+1,F.set(L,$));let B=L?`${L}:${$}`:void 0,ye=T?u.get(f(T.data)):void 0,X=P[x]??ye?.path,Ht=X?`${c("attachmentPathLabel")}: ${X}`:ye?c("attachmentSourceTool",{tool:ye.tool}):`${c("attachmentPathLabel")}: ${c("attachmentPathUnavailable")}`,oe=X&&L?`${X}\0${L}`:void 0;if(oe&&d.has(oe)||B&&k.has(B)){D[U]=void 0;continue}let Ue=T!==void 0&&Ft(T),Y;T?(Y=[c("attachmentOpenImage")],Ue&&Y.push(c("attachmentEditImage")),Y.push(c("attachmentAllow"),c("attachmentDeny"))):Y=[c("attachmentAllow"),c("attachmentDeny")];let $e=!1,ie=!1;for(;!$e;){let Ie=await a.ui.select(`${c("attachmentAskTitle",{index:x+1,total:R.length})}
21
23
 
22
- ${l("attachmentAskBody")}
24
+ ${c("attachmentAskBody")}
23
25
 
24
- ${l("attachmentPathLabel")}: ${V??l("attachmentPathUnavailable")}`,Y);if(T&&fe===Y[0]){await E(a,T.data,T.mimeType??"image/png");continue}if(T&&Le&&fe===l("attachmentEditImage")){let Fe=await Rt(T,async Lt=>{if(await St(Lt),await a.ui.select(l("attachmentEditWait"),[l("attachmentEditDone"),l("attachmentDeny")])!==l("attachmentEditDone"))throw s("SCAN_ATTACHMENT")});T={type:"image",...Fe},X[v]=Fe,ue=!0,await E(a,T.data,T.mimeType??"image/png");continue}if(fe===l("attachmentAllow"))Me=!0,V&&!ue&&d.add(V),U&&!ue&&c.add(U);else throw s("SCAN_ATTACHMENT")}}C=Nt(p.payload,X)}a.ui.notify(l("attachmentAllowed"),"warning");let S=a.model?.provider??"unknown",j=await le(C,async k=>{let R=await e.redact(k,S);return typeof R=="string"?R:k});return h=!0,j}let A=await e.redact(p.payload,a.model?.provider??"unknown");return h=!0,A}catch(m){h=!1;let A=D(O(m,"INTERNAL").diagnostic);try{a.abort()}catch{}try{a.ui.notify(A,"error")}catch{}return{}}}),t.on("message_end",p=>{if(p.message.role!=="assistant")return;let a=p.message;return{message:{...a,content:a.content.map(m=>m.type==="text"?{...m,text:e.restoreText(m.text)}:m.type==="thinking"?{...m,thinking:e.restoreText(m.thinking)}:m.type==="toolCall"?{...m,arguments:e.restore(m.arguments)}:m),...a.errorMessage?{errorMessage:e.restoreText(a.errorMessage)}:{}}}}),t.registerMarkdownTransformer((p,a)=>a.messageType==="user"?p:e.restoreText(p)),t.on("tool_result",(p,a)=>{try{let m=p.input,A=m.path??m.file_path??m.filePath;if(typeof A!="string"||!A.trim())return;let C=bt(a.cwd,A.replace(/^@/,""));for(let S of p.content)S.type==="image"&&u.set(f(S.data),C)}catch{}}),t.on("tool_call",(p,a)=>{if(!h)return{block:!0,reason:e.lastDiagnosticText??g?.text??$(s("NOT_READY").diagnostic)};let m=e.restore(p.input);for(let A of Object.keys(p.input))delete p.input[A];if(Object.assign(p.input,m),ut(p.toolName,p.input,a.cwd,e.configPath))return{block:!0,reason:l("toolBlocked")}});let K=async(p,a)=>{if(!n())throw s("PI_CAPABILITY");if(!e.ready)throw e.lastDiagnostic?s(e.lastDiagnostic.code,e.lastDiagnostic):s("NOT_READY");let m=e.compactionMode;if(m==="off")throw s("COMPACT_DENIED");if(I==="deny")throw s("COMPACT_DENIED");if(m!=="ask"||I==="allow")return;let A=[l("compactAllowOnce"),l("compactAllowAlways"),l("compactDenyOnce"),l("compactDenyAlways")],C=await p.ui.select(`${l("compactAskTitle")}
26
+ ${Ht}`,Y);if(T&&Ie===Y[0]){await q(a,T.data,T.mimeType??"image/png",ie?void 0:X);continue}if(T&&Ue&&Ie===c("attachmentEditImage")){let Be=await vt(T,async jt=>{if(await ve(jt),await a.ui.select(c("attachmentEditWait"),[c("attachmentEditDone"),c("attachmentDeny")])!==c("attachmentEditDone"))throw s("SCAN_ATTACHMENT")});T={type:"image",...Be},D[U]=Be,ie=!0,await q(a,T.data,T.mimeType??"image/png");continue}if(Ie===c("attachmentAllow"))$e=!0,oe&&!ie&&d.add(oe),B&&!ie&&l.add(B);else throw s("SCAN_ATTACHMENT")}}O=Rt(m.payload,D)}a.ui.notify(c("attachmentAllowed"),"warning");let _=a.model?.provider??"unknown",b=await me(O,async R=>{let P=await e.redact(R,_);return typeof P=="string"?P:R});return h=!0,b}let y=await e.redact(m.payload,a.model?.provider??"unknown");return h=!0,y}catch(p){h=!1;let y=M(N(p,"INTERNAL").diagnostic);try{a.abort()}catch{}try{a.ui.notify(y,"error")}catch{}return{}}}),t.on("message_end",m=>{if(m.message.role!=="assistant")return;let a=m.message;return{message:{...a,content:a.content.map(p=>p.type==="text"?{...p,text:e.restoreText(p.text)}:p.type==="thinking"?{...p,thinking:e.restoreText(p.thinking)}:p.type==="toolCall"?{...p,arguments:e.restore(p.arguments)}:p),...a.errorMessage?{errorMessage:e.restoreText(a.errorMessage)}:{}}}}),t.registerMarkdownTransformer((m,a)=>a.messageType==="user"?m:e.restoreText(m)),t.on("tool_result",(m,a)=>{try{let p=m.content.filter(D=>D.type==="image");if(!p.length)return;let y=m.input??{},O=typeof y.tool=="string"?y.tool:void 0,_=O?`${m.toolName} → ${O}`:m.toolName,b=y.args&&typeof y.args=="object"?y.args:{},R=[y.path,y.file_path,y.filePath,y.filename,b.path,b.filename,b.filePath].find(D=>typeof D=="string"&&$n.test(D.trim())),P=R?Bt(a.cwd,R.trim().replace(/^@/,"")):void 0;for(let D of p)u.set(f(D.data),{tool:_,path:P})}catch{}}),t.on("tool_call",(m,a)=>{if(!h)return{block:!0,reason:e.lastDiagnosticText??g?.text??W(s("NOT_READY").diagnostic)};let p=e.restore(m.input);for(let y of Object.keys(m.input))delete m.input[y];if(Object.assign(m.input,p),yt(m.toolName,m.input,a.cwd,e.configPath))return{block:!0,reason:c("toolBlocked")}});let Ge=async(m,a)=>{if(!n())throw s("PI_CAPABILITY");if(!e.ready)throw e.lastDiagnostic?s(e.lastDiagnostic.code,e.lastDiagnostic):s("NOT_READY");let p=e.compactionMode;if(p==="off")throw s("COMPACT_DENIED");if(w==="deny")throw s("COMPACT_DENIED");if(p!=="ask"||w==="allow")return;let y=[c("compactAllowOnce"),c("compactAllowAlways"),c("compactDenyOnce"),c("compactDenyAlways")],O=await m.ui.select(`${c("compactAskTitle")}
25
27
 
26
- ${l("compactAskBody")}`,A,{signal:a}),S=C===void 0?-1:A.indexOf(C);if(S===1?(I="allow",p.ui.notify(l("compactAlwaysNotice"),"info")):S===3&&(I="deny",p.ui.notify(l("compactDenyNotice"),"warning")),S!==0&&S!==1)throw s("COMPACT_DENIED")};t.on("session_before_compact",async(p,a)=>{try{await K(a,p.signal),await mt(p,e,a.model?.provider??"unknown"),a.ui.notify(l("compactRedacted"),"info");return}catch(m){return a.ui.notify(D(O(m,"COMPACT_FAILED").diagnostic),"warning"),{cancel:!0}}}),t.on("session_before_tree",async(p,a)=>{if(p.preparation.userWantsSummary)try{await K(a,p.signal);let m=await pt(p,e,a.model?.provider??"unknown");return a.ui.notify(l("compactRedacted"),"info"),m}catch(m){return a.ui.notify(D(O(m,"COMPACT_FAILED").diagnostic),"warning"),{cancel:!0}}}),t.registerCommand("protecter",{description:l("commandDescription"),handler:async(p,a)=>{let m=p.trim(),A=m.split(/\s+/).filter(Boolean),C=A[0]??"logs";if(C==="test"){let P=m.slice(C.length).trim();if(!P){a.ui.notify(l("usage"),"info");return}try{if(!e.ready)throw s("NOT_READY");let{text:b,fromFile:H}=await Mn(P),T=(await e.redact({text:b},"test")).text;if(typeof T!="string")throw s("INTERNAL");let v=[l(H?"testFileLabel":"testTextLabel"),l(T===b?"testUnchanged":"testRedacted",{chars:W("chars",T.length)})].join(`
27
- `);a.mode==="tui"?await a.ui.editor(l("testPreviewTitle"),`${v}
28
+ ${c("compactAskBody")}`,y,{signal:a}),_=O===void 0?-1:y.indexOf(O);if(_===1?(w="allow",m.ui.notify(c("compactAlwaysNotice"),"info")):_===3&&(w="deny",m.ui.notify(c("compactDenyNotice"),"warning")),_!==0&&_!==1)throw s("COMPACT_DENIED")};t.on("session_before_compact",async(m,a)=>{try{await Ge(a,m.signal),await wt(m,e,a.model?.provider??"unknown"),a.ui.notify(c("compactRedacted"),"info");return}catch(p){return a.ui.notify(M(N(p,"COMPACT_FAILED").diagnostic),"warning"),{cancel:!0}}}),t.on("session_before_tree",async(m,a)=>{if(m.preparation.userWantsSummary)try{await Ge(a,m.signal);let p=await At(m,e,a.model?.provider??"unknown");return a.ui.notify(c("compactRedacted"),"info"),p}catch(p){return a.ui.notify(M(N(p,"COMPACT_FAILED").diagnostic),"warning"),{cancel:!0}}}),t.registerCommand("protecter",{description:c("commandDescription"),handler:async(m,a)=>{let p=m.trim(),y=p.split(/\s+/).filter(Boolean),O=y[0]??"logs";if(O==="test"){let k=p.slice(O.length).trim();if(!k){a.ui.notify(c("usage"),"info");return}try{if(!e.ready)throw s("NOT_READY");let{text:F,fromFile:j}=await Bn(k),T=(await e.redact({text:F},"test")).text;if(typeof T!="string")throw s("INTERNAL");let U=[c(j?"testFileLabel":"testTextLabel"),c(T===F?"testUnchanged":"testRedacted",{chars:J("chars",T.length)})].join(`
29
+ `);a.mode==="tui"?await a.ui.editor(c("testPreviewTitle"),`${U}
28
30
 
29
- ${T}`):a.ui.notify(`${v}
31
+ ${T}`):a.ui.notify(`${U}
30
32
 
31
- ${T}`,"info")}catch(b){a.ui.notify(D(O(b,"TEST_INPUT").diagnostic),"error")}return}if(!["status","reload","logs"].includes(C)||C!=="logs"&&A.length>1||A.length>2||A[1]!==void 0&&A[1]!=="clear"&&!/^(?:[1-9]\d?|100)$/.test(A[1])){a.ui.notify(l("usage"),"info");return}if(C==="logs"){if(a.mode!=="tui"){a.ui.notify(l("logsTuiOnly"),"warning");return}if(A[1]==="clear"){try{if(!e.ready)throw s("NOT_READY");if(!await a.ui.confirm(l("clearTitle"),l("clearBody")))return;await a.waitForIdle();let P=await e.clearAuditRecords();h=e.ready,a.ui.notify(l("cleared",{bytes:W("bytes",P)}),"info")}catch(P){a.ui.notify(D(O(P,"AUDIT_IO").diagnostic),"error")}return}try{let{records:P,truncated:b}=e.auditRecords(Number(A[1]??20));if(!P.length){a.ui.notify(l("logsNone"),"info");return}let H=G=>JSON.stringify(G).replace(/[\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g,U=>`\\u${U.charCodeAt(0).toString(16).padStart(4,"0")}`),L=P.map((G,U)=>`${U+1}. ${H(G.time)} | ${H(G.provider).slice(0,120)}`),T=await a.ui.select(l("logsSelectTitle")+(b?l("logsTruncated"):""),L),v=T===void 0?-1:L.indexOf(T);if(v<0||!await a.ui.confirm(l("logsRevealTitle"),l("logsRevealBody")))return;let M=H(P[v]);await a.ui.editor(l("logsPreviewTitle"),M.length>2e4?`${M.slice(0,2e4)}
32
- ${l("logsPreviewTruncated")}`:M)}catch(P){a.ui.notify(D(O(P,"AUDIT_IO").diagnostic),"error")}return}if(C==="reload"){await a.waitForIdle(),await a.reload();return}let S=e.lastDiagnostic,j=S?[$(S),x(S)].filter(Boolean).join(`
33
+ ${T}`,"info")}catch(F){a.ui.notify(M(N(F,"TEST_INPUT").diagnostic),"error")}return}if(!["status","reload","logs"].includes(O)||O!=="logs"&&y.length>1||y.length>2||y[1]!==void 0&&y[1]!=="clear"&&!/^(?:[1-9]\d?|100)$/.test(y[1])){a.ui.notify(c("usage"),"info");return}if(O==="logs"){if(a.mode!=="tui"){a.ui.notify(c("logsTuiOnly"),"warning");return}if(y[1]==="clear"){try{if(!e.ready)throw s("NOT_READY");if(!await a.ui.confirm(c("clearTitle"),c("clearBody")))return;await a.waitForIdle();let k=await e.clearAuditRecords();h=e.ready,a.ui.notify(c("cleared",{bytes:J("bytes",k)}),"info")}catch(k){a.ui.notify(M(N(k,"AUDIT_IO").diagnostic),"error")}return}try{let{records:k,truncated:F}=e.auditRecords(Number(y[1]??20));if(!k.length){a.ui.notify(c("logsNone"),"info");return}let j=$=>JSON.stringify($).replace(/[\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g,B=>`\\u${B.charCodeAt(0).toString(16).padStart(4,"0")}`),x=k.map(($,B)=>`${B+1}. ${j($.time)} | ${j($.provider).slice(0,120)}`),T=await a.ui.select(c("logsSelectTitle")+(F?c("logsTruncated"):""),x),U=T===void 0?-1:x.indexOf(T);if(U<0||!await a.ui.confirm(c("logsRevealTitle"),c("logsRevealBody")))return;let L=j(k[U]);await a.ui.editor(c("logsPreviewTitle"),L.length>2e4?`${L.slice(0,2e4)}
34
+ ${c("logsPreviewTruncated")}`:L)}catch(k){a.ui.notify(M(N(k,"AUDIT_IO").diagnostic),"error")}return}if(O==="reload"){await a.waitForIdle(),await a.reload();return}let _=e.lastDiagnostic,b=_?[W(_),G(_)].filter(Boolean).join(`
33
35
  `):g?[g.text,g.local].filter(Boolean).join(`
34
- `):void 0,k=l("statusLine",{state:l(e.ready?"statusReady":"statusNotReady"),rules:W("rules",e.ruleCount),mappings:W("mappings",e.mappingCount)}),R=`
35
- ${l("statusCompaction",{mode:e.compactionMode,guard:l(N?"guardInstalled":"guardMissing")})}`,X=j?`
36
- ${l("statusLastBlock",{at:g?.at??""})}
37
- ${j}`:`
38
- ${l("statusNoBlock")}`;a.ui.notify(k+R+X,e.ready?"info":"error")}}),t.on("session_shutdown",()=>{N?.(),N=void 0,e.close()})}export{Fn as default};
36
+ `):void 0,R=c("statusLine",{state:c(e.ready?"statusReady":"statusNotReady"),rules:J("rules",e.ruleCount),mappings:J("mappings",e.mappingCount)}),P=`
37
+ ${c("statusCompaction",{mode:e.compactionMode,guard:c(E?"guardInstalled":"guardMissing")})}`,D=b?`
38
+ ${c("statusLastBlock",{at:g?.at??""})}
39
+ ${b}`:`
40
+ ${c("statusNoBlock")}`;a.ui.notify(R+P+D,e.ready?"info":"error")}}),t.on("session_shutdown",()=>{E?.(),E=void 0,e.close()})}export{zn as default};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-information-protecter",
3
- "version": "0.15.0",
3
+ "version": "0.15.2",
4
4
  "description": "Sensitive Personal Information (SPI) Protecter — local reversible privacy redaction for Pi",
5
5
  "type": "module",
6
6
  "license": "MIT",