@trim21/personal-pi-extensions 0.0.262 → 0.0.268

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.0.262",
3
+ "version": "0.0.268",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -42,6 +42,7 @@
42
42
  "eslint": "^10.8.1",
43
43
  "eslint-config-prettier": "10.1.8",
44
44
  "eslint-plugin-erasable-syntax-only": "0.4.2",
45
+ "eslint-plugin-import-x": "^4.17.1",
45
46
  "eslint-plugin-promise": "7.3.0",
46
47
  "eslint-plugin-simple-import-sort": "14.0.0",
47
48
  "eslint-plugin-unicorn": "73.0.0",
@@ -70,7 +71,7 @@
70
71
  ]
71
72
  },
72
73
  "lint-staged": {
73
- "*.{ts,tsx,js,jsx}": [
74
+ "*.{ts,tsx,js,jsx,mjs}": [
74
75
  "eslint --fix",
75
76
  "prettier --write"
76
77
  ],
package/src/bwrap/core.ts CHANGED
@@ -1,7 +1,5 @@
1
- import type { ChildProcess } from "node:child_process";
2
- import { spawn } from "node:child_process";
3
- import { constants } from "node:fs";
4
- import { closeSync, existsSync, openSync, readFileSync } from "node:fs";
1
+ import { type ChildProcess, spawn } from "node:child_process";
2
+ import { closeSync, constants, existsSync, openSync, readFileSync } from "node:fs";
5
3
  import { access as fsAccess } from "node:fs/promises";
6
4
  import { delimiter, join } from "node:path";
7
5
  import { fileURLToPath } from "node:url";
@@ -0,0 +1,127 @@
1
+ /**
2
+ * dcg-scan —— 可选的 dcg(Destructive Command Guard)扫描建议层。
3
+ *
4
+ * 系统安装了 dcg 时,在 full-access 人工审批弹窗里附加一段破坏性命令
5
+ * 扫描建议,作为人工 review 的参考;dcg 未安装、调用失败或超时时静默
6
+ * 跳过,不影响审批流程。dcg 输出只作建议文本,不参与任何执行决策。
7
+ */
8
+ import { spawn } from "node:child_process";
9
+
10
+ /** `dcg test --format json` 输出中我们关心的字段。 */
11
+ interface DcgTestOutput {
12
+ decision?: "allow" | "deny" | "indeterminate";
13
+ severity?: "critical" | "high" | "medium" | "low";
14
+ rule_id?: string;
15
+ reason?: string;
16
+ }
17
+
18
+ /** dcg 扫描建议(纯文本,外部字段已转义,可直接拼进弹窗 description)。 */
19
+ export interface DcgSuggestion {
20
+ kind: "danger" | "clean";
21
+ text: string;
22
+ }
23
+
24
+ /** dcg 扫描结果:正常判定 / 未安装(静默跳过)/ 扫描失败(应提示用户)。 */
25
+ export type DcgScanOutcome =
26
+ | { kind: "suggestion"; suggestion: DcgSuggestion }
27
+ | { kind: "not-installed" }
28
+ | { kind: "failed"; detail: string };
29
+
30
+ /** dcg 扫描的超时预算:超时视为无建议,不让审批弹窗被拖住。 */
31
+ const DCG_SCAN_TIMEOUT_MS = 2000;
32
+
33
+ function escapeHtml(text: string): string {
34
+ return text
35
+ .replaceAll("&", "&")
36
+ .replaceAll("<", "&lt;")
37
+ .replaceAll(">", "&gt;")
38
+ .replaceAll('"', "&quot;");
39
+ }
40
+
41
+ /**
42
+ * 对命令做 dcg 扫描。返回三种结果:
43
+ * - `suggestion`:正常判定,携带建议文本
44
+ * - `not-installed`:dcg 未安装(调用方静默跳过)
45
+ * - `failed`:dcg 已安装但扫描失败(退出码异常/超时/输出不可解析),
46
+ * 调用方应通过 `ui.notify` 提示用户
47
+ */
48
+ export async function dcgSuggestion(command: string): Promise<DcgScanOutcome> {
49
+ let stdout: string;
50
+ try {
51
+ stdout = await runDcgScan(command);
52
+ } catch (error) {
53
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return { kind: "not-installed" };
54
+ return { kind: "failed", detail: error instanceof Error ? error.message : String(error) };
55
+ }
56
+
57
+ let output: DcgTestOutput;
58
+ try {
59
+ output = JSON.parse(stdout) as DcgTestOutput;
60
+ } catch {
61
+ return { kind: "failed", detail: "无法解析 dcg 输出" };
62
+ }
63
+
64
+ if (output.decision === "deny") {
65
+ const details = [
66
+ output.rule_id ? `rule: ${escapeHtml(output.rule_id)}` : undefined,
67
+ output.severity ? `severity: ${escapeHtml(output.severity)}` : undefined,
68
+ ]
69
+ .filter(Boolean)
70
+ .join(", ");
71
+ const reason = output.reason ? escapeHtml(output.reason) : "检测到破坏性命令模式";
72
+ return {
73
+ kind: "suggestion",
74
+ suggestion: {
75
+ kind: "danger",
76
+ text: `dcg 建议拦截: ${reason}${details ? ` (${details})` : ""}`,
77
+ },
78
+ };
79
+ }
80
+ if (output.decision === "allow") {
81
+ return {
82
+ kind: "suggestion",
83
+ suggestion: { kind: "clean", text: "dcg 未检测到破坏性命令模式" },
84
+ };
85
+ }
86
+ // indeterminate / 缺字段:dcg 未能给出可用判定
87
+ return {
88
+ kind: "failed",
89
+ detail: `dcg 未返回可用判定 (decision=${output.decision ?? "missing"})`,
90
+ };
91
+ }
92
+
93
+ function runDcgScan(command: string): Promise<string> {
94
+ return new Promise((resolve, reject) => {
95
+ const proc = spawn("dcg", ["test", "--stdin", "--format", "json"], {
96
+ stdio: ["pipe", "pipe", "pipe"],
97
+ });
98
+ let stdout = "";
99
+ let settled = false;
100
+ const timeoutId = setTimeout(() => {
101
+ proc.kill("SIGKILL");
102
+ settle(new Error("dcg scan timed out"));
103
+ }, DCG_SCAN_TIMEOUT_MS);
104
+ const settle = (error?: Error) => {
105
+ if (settled) return;
106
+ settled = true;
107
+ clearTimeout(timeoutId);
108
+ if (error) reject(error);
109
+ else resolve(stdout);
110
+ };
111
+ proc.stdout.setEncoding("utf8");
112
+ proc.stdout.on("data", (chunk: string) => {
113
+ stdout += chunk;
114
+ });
115
+ proc.on("error", (error: NodeJS.ErrnoException) => {
116
+ // ENOENT = dcg 未安装:静默跳过
117
+ settle(error);
118
+ });
119
+ proc.on("close", (code) => {
120
+ // dcg 的退出码是决策结果(deny 时非 0),不是失败标志:只要 stdout
121
+ // 有内容就交给上层解析;真正出错时(参数错误等)stdout 为空。
122
+ if (code === 0 || stdout.length > 0) settle();
123
+ else settle(new Error(`dcg exited with code ${String(code)}`));
124
+ });
125
+ proc.stdin.end(command);
126
+ });
127
+ }
@@ -3,14 +3,12 @@ import { createWriteStream, existsSync, mkdirSync, readFileSync, type WriteStrea
3
3
  import { mkdir, writeFile } from "node:fs/promises";
4
4
  import { dirname, join } from "node:path";
5
5
 
6
- import type {
7
- AgentToolUpdateCallback,
8
- ExtensionAPI,
9
- ExtensionCommandContext,
10
- ExtensionContext,
11
- } from "@earendil-works/pi-coding-agent";
12
6
  import {
7
+ type AgentToolUpdateCallback,
13
8
  createLocalBashOperations,
9
+ type ExtensionAPI,
10
+ type ExtensionCommandContext,
11
+ type ExtensionContext,
14
12
  getAgentDir,
15
13
  truncateTail,
16
14
  type TruncationResult,
@@ -31,6 +29,7 @@ import {
31
29
  type ResolvedBwrap,
32
30
  resolveHeadlessBwrap,
33
31
  } from "./core.js";
32
+ import { dcgSuggestion } from "./dcg-scan.js";
34
33
 
35
34
  export type EscalationDecision = { kind: "dialog" } | { kind: "deny"; reason: string };
36
35
 
@@ -390,7 +389,15 @@ export class BwrapRuntime {
390
389
  ): Promise<void> {
391
390
  const policy = resolveEscalation({ hasUI: ctx.hasUI });
392
391
  if (policy.kind === "deny") throw new Error(policy.reason);
393
- const description = `Allow this command to run without sandbox?\n---\n\nReason: ${escapeHtml(reason ?? "(No reason provided by model)")}\n---\n${fenceCodeBlock(command)}`;
392
+ // dcg 扫描建议是可选的参考文本:未安装时静默跳过;已安装但扫描失败
393
+ // 时 notify 提示,弹窗本身与无 dcg 时一致
394
+ const outcome = await dcgSuggestion(command);
395
+ const suggestionBlock =
396
+ outcome.kind === "suggestion" ? `\n${outcome.suggestion.text}\n---\n` : "";
397
+ if (outcome.kind === "failed") {
398
+ ctx.ui.notify(`dcg 扫描失败,本次无破坏性命令建议: ${outcome.detail}`, "warning");
399
+ }
400
+ const description = `Allow this command to run without sandbox?\n---\n\nReason: ${escapeHtml(reason ?? "(No reason provided by model)")}\n---\n${suggestionBlock}${fenceCodeBlock(command)}`;
394
401
 
395
402
  // 单选:允许一次 / 永久允许(写入规则)/ 拒绝 / 拒绝并附理由(弹输入框)
396
403
  const verdict = await selectWithOptionalInput(description, FULL_ACCESS_CHOICES, ctx.ui, {
@@ -5,8 +5,8 @@ import { dirname, extname } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
 
7
7
  import type { ImageContent, TextContent } from "@earendil-works/pi-ai";
8
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
9
8
  import {
9
+ type ExtensionAPI,
10
10
  generateDiffString,
11
11
  generateUnifiedPatch,
12
12
  withFileMutationQueue,
@@ -2,8 +2,11 @@ import { readFileSync } from "node:fs";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { fileURLToPath } from "node:url";
4
4
 
5
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
- import { type BashToolDetails, formatSize } from "@earendil-works/pi-coding-agent";
5
+ import {
6
+ type BashToolDetails,
7
+ type ExtensionAPI,
8
+ formatSize,
9
+ } from "@earendil-works/pi-coding-agent";
7
10
  import { Type } from "typebox";
8
11
 
9
12
  import { type BwrapRuntime, createBwrapRuntime } from "../bwrap/runtime.js";
@@ -34,8 +34,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
34
34
  import { homedir } from "node:os";
35
35
  import { delimiter, dirname, join, resolve } from "node:path";
36
36
 
37
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
38
- import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
37
+ import { type ExtensionAPI, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
39
38
  import { Type } from "typebox";
40
39
  import { Value } from "typebox/value";
41
40
 
@@ -21,8 +21,8 @@ import { constants } from "node:fs";
21
21
  import { access, mkdir, readFile, writeFile } from "node:fs/promises";
22
22
  import { dirname, isAbsolute, resolve } from "node:path";
23
23
 
24
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
25
24
  import {
25
+ type ExtensionAPI,
26
26
  generateDiffString,
27
27
  generateUnifiedPatch,
28
28
  withFileMutationQueue,
@@ -24,8 +24,7 @@
24
24
  import { mkdir, open, writeFile } from "node:fs/promises";
25
25
  import { dirname, resolve as resolvePath } from "node:path";
26
26
 
27
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
28
- import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
27
+ import { type ExtensionAPI, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
29
28
  import { Type } from "typebox";
30
29
 
31
30
  import { guardWriteAccess } from "../lib/write-guard.js";
package/src/talk/index.ts CHANGED
@@ -13,8 +13,12 @@ import * as os from "node:os";
13
13
  import * as path from "node:path";
14
14
  import { fileURLToPath } from "node:url";
15
15
 
16
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
17
- import { getAgentDir, truncateToVisualLines } from "@earendil-works/pi-coding-agent";
16
+ import {
17
+ type ExtensionAPI,
18
+ type ExtensionContext,
19
+ getAgentDir,
20
+ truncateToVisualLines,
21
+ } from "@earendil-works/pi-coding-agent";
18
22
  import { type TObject, Type } from "typebox";
19
23
 
20
24
  import { type CommandResult, type CommandSpec, parseCommand } from "../lib/cli.js";