@trim21/personal-pi-extensions 0.0.223 → 0.0.225

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/README.md CHANGED
@@ -38,7 +38,7 @@
38
38
 
39
39
  ### 提权机制
40
40
 
41
- bash 工具注册了 `request_full_access` `request_full_access_reason` 参数。模型需要全权限时须说明原因(如需要网络、写入 workspace 外部路径)。
41
+ bash 工具(opencode 风格 `bash`、Claude Code 风格 `Bash`)注册了 `dangerouslyDisableSandbox` 参数。模型需要全权限时置为 true 并说明原因(如需要网络、写入 workspace 外部路径)。
42
42
 
43
43
  建议模型不确定时先尝试沙箱模式,若因沙箱限制失败,再以完整权限重试。
44
44
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.0.223",
3
+ "version": "0.0.225",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -2,8 +2,9 @@ import { type ExtensionAPI, isToolCallEventType } from "@earendil-works/pi-codin
2
2
 
3
3
  export default function bashDefaultTimeout(pi: ExtensionAPI) {
4
4
  pi.on("tool_call", (event) => {
5
+ // opencode 风格 bash 的 timeout 单位是毫秒
5
6
  if (isToolCallEventType("bash", event) && event.input.timeout === undefined) {
6
- event.input.timeout = 180;
7
+ event.input.timeout = 180_000;
7
8
  }
8
9
  });
9
10
  }
@@ -1,7 +1,4 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { createBashTool } from "@earendil-works/pi-coding-agent";
3
- import { Type } from "typebox";
4
- import { Value } from "typebox/value";
5
2
 
6
3
  import { bwrapRuntime } from "./runtime.js";
7
4
 
@@ -13,46 +10,6 @@ export {
13
10
  } from "./core.js";
14
11
  export { type EscalationDecision, resolveEscalation } from "./runtime.js";
15
12
 
16
- const sandboxedBashSchema = Type.Object(
17
- {
18
- command: Type.String({ description: "Bash command to execute" }),
19
- timeout: Type.Optional(Type.Number({ description: "Timeout in seconds" })),
20
- request_full_access: Type.Optional(
21
- Type.Boolean({
22
- description:
23
- "Set true to request unsandboxed execution. The bwrap runtime will ask the user for approval.",
24
- }),
25
- ),
26
- request_full_access_reason: Type.Optional(
27
- Type.String({ description: "Explain why unsandboxed execution is required." }),
28
- ),
29
- },
30
- { additionalProperties: false },
31
- );
32
-
33
13
  export default function bwrapExtension(pi: ExtensionAPI): void {
34
14
  bwrapRuntime.setup(pi);
35
- const localBash = createBashTool(process.cwd());
36
- pi.registerTool({
37
- name: localBash.name,
38
- label: "bash (bwrap)",
39
- description:
40
- localBash.description +
41
- "\n\nSet request_full_access to true to request unsandboxed execution.",
42
- parameters: sandboxedBashSchema,
43
- prepareArguments: (args) => Value.Parse(sandboxedBashSchema, args),
44
- executionMode: localBash.executionMode,
45
- execute(id, params, signal, onUpdate, ctx) {
46
- return bwrapRuntime.execute({
47
- toolCallId: id,
48
- command: params.command,
49
- timeout: params.timeout,
50
- requestFullAccess: params.request_full_access,
51
- requestFullAccessReason: params.request_full_access_reason,
52
- signal,
53
- onUpdate,
54
- ctx,
55
- });
56
- },
57
- });
58
15
  }
@@ -2,40 +2,17 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "typebox";
3
3
 
4
4
  import { bwrapRuntime } from "../bwrap/runtime.js";
5
+ import { resolveWorkdir } from "../lib/path.js";
5
6
 
6
7
  const DEFAULT_TIMEOUT_MS = 120_000;
7
8
  const MAX_TIMEOUT_MS = 600_000;
8
9
 
9
- interface MarkerResult {
10
- text: string;
11
- cwd: string | undefined;
12
- }
13
-
14
- function stripCwdMarker(text: string, marker: string): MarkerResult {
15
- const match = new RegExp(String.raw`${marker}([^\n]+)${marker}`).exec(text);
16
- if (!match) return { text, cwd: undefined };
17
- return { text: text.replace(match[0], "").trimEnd(), cwd: match[1] };
18
- }
19
-
20
- function wrapCommand(command: string, marker: string): string {
21
- return [
22
- command,
23
- "__pi_cc_status=$?",
24
- "wait",
25
- String.raw`printf '\n${marker}%s${marker}\n' "$PWD"`,
26
- "exit $__pi_cc_status",
27
- ].join("\n");
28
- }
29
-
30
10
  export function registerShellTools(pi: ExtensionAPI): void {
31
- let persistentCwd: string | undefined;
32
-
33
11
  pi.registerTool({
34
12
  name: "Bash",
35
13
  label: "Bash",
36
14
  description: [
37
15
  "Executes a given bash command synchronously and returns its output.",
38
- "The working directory persists between commands, but shell state does not.",
39
16
  "timeout is in milliseconds, defaults to 120000, and may not exceed 600000.",
40
17
  "Every command runs in the foreground. Background command execution is not supported; shell jobs are waited for before the tool returns.",
41
18
  ].join("\n"),
@@ -48,6 +25,12 @@ export function registerShellTools(pi: ExtensionAPI): void {
48
25
  description: Type.Optional(
49
26
  Type.String({ description: "Clear, concise description of the command" }),
50
27
  ),
28
+ workdir: Type.Optional(
29
+ Type.String({
30
+ description:
31
+ "Working directory to execute the command in. Defaults to the current directory; relative paths resolve from there.",
32
+ }),
33
+ ),
51
34
  dangerouslyDisableSandbox: Type.Optional(
52
35
  Type.Boolean({
53
36
  description:
@@ -63,43 +46,27 @@ export function registerShellTools(pi: ExtensionAPI): void {
63
46
  throw new Error(`timeout must be between 1 and ${MAX_TIMEOUT_MS} milliseconds`);
64
47
  }
65
48
 
66
- const marker = `__PI_CC_CWD_${id.replaceAll("-", "_")}_${Date.now()}__`;
49
+ const cwd = params.workdir ? await resolveWorkdir(params.workdir, ctx.cwd) : ctx.cwd;
50
+
67
51
  try {
68
- const result = await bwrapRuntime.execute({
69
- ctx: { ...ctx, cwd: persistentCwd ?? ctx.cwd },
52
+ return await bwrapRuntime.execute({
53
+ ctx: { ...ctx, cwd },
70
54
  toolCallId: id,
71
- command: wrapCommand(params.command, marker),
55
+ command: params.command,
72
56
  timeout: timeout / 1000,
73
57
  requestFullAccess: params.dangerouslyDisableSandbox,
74
58
  requestFullAccessReason: params.description,
75
59
  signal,
76
- onUpdate: onUpdate
77
- ? (update) => {
78
- const content = update.content.map((item) => {
79
- if (item.type !== "text") return item;
80
- return { ...item, text: stripCwdMarker(item.text, marker).text };
81
- });
82
- onUpdate({ ...update, content });
83
- }
84
- : undefined,
85
- });
86
- const content = result.content.map((item) => {
87
- if (item.type !== "text") return item;
88
- const cleaned = stripCwdMarker(item.text, marker);
89
- if (cleaned.cwd) persistentCwd = cleaned.cwd;
90
- return { ...item, text: cleaned.text || "(no output)" };
60
+ onUpdate,
91
61
  });
92
- return { ...result, content };
93
62
  } catch (error) {
94
63
  if (!(error instanceof Error)) throw error;
95
- const cleaned = stripCwdMarker(error.message, marker);
96
- if (cleaned.cwd) persistentCwd = cleaned.cwd;
97
- const timeoutMatch = /Command timed out after [\d.]+ seconds/.exec(cleaned.text);
64
+ const timeoutMatch = /Command timed out after [\d.]+ seconds/.exec(error.message);
98
65
  const message = timeoutMatch
99
- ? cleaned.text.slice(0, timeoutMatch.index) +
66
+ ? error.message.slice(0, timeoutMatch.index) +
100
67
  `Command timed out after ${timeout} milliseconds` +
101
- cleaned.text.slice(timeoutMatch.index + timeoutMatch[0].length)
102
- : cleaned.text;
68
+ error.message.slice(timeoutMatch.index + timeoutMatch[0].length)
69
+ : error.message;
103
70
  throw new Error(message, { cause: error });
104
71
  }
105
72
  },
package/src/lib/path.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  * Shared path helpers used by multiple extensions.
3
3
  */
4
4
 
5
+ import { stat } from "node:fs/promises";
5
6
  import { homedir } from "node:os";
6
7
  import { isAbsolute, join, resolve } from "node:path";
7
8
 
@@ -26,3 +27,22 @@ export function resolveHomePath(p: string, baseDir: string): string {
26
27
  const expanded = expandHome(p);
27
28
  return isAbsolute(expanded) ? resolve(expanded) : resolve(baseDir, expanded);
28
29
  }
30
+
31
+ /**
32
+ * Resolve a command `workdir` argument: absolute paths are used as-is, relative
33
+ * paths resolve against `baseDir`. Throws if the target does not exist or is
34
+ * not a directory.
35
+ */
36
+ export async function resolveWorkdir(workdir: string, baseDir: string): Promise<string> {
37
+ const target = isAbsolute(workdir) ? workdir : resolve(baseDir, workdir);
38
+ let info;
39
+ try {
40
+ info = await stat(target);
41
+ } catch {
42
+ throw new Error(`Working directory does not exist: ${target}`);
43
+ }
44
+ if (!info.isDirectory()) {
45
+ throw new Error(`Working directory is not a directory: ${target}`);
46
+ }
47
+ return target;
48
+ }
@@ -0,0 +1,71 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+
4
+ import { bwrapRuntime } from "../bwrap/runtime.js";
5
+ import { resolveWorkdir } from "../lib/path.js";
6
+
7
+ const DEFAULT_TIMEOUT_MS = 120_000;
8
+ const MAX_TIMEOUT_MS = 600_000;
9
+
10
+ export default function opencodeBash(pi: ExtensionAPI): void {
11
+ pi.registerTool({
12
+ name: "bash",
13
+ label: "bash",
14
+ description: [
15
+ "Executes a given bash command synchronously and returns its output.",
16
+ "The default working directory is the current directory; use workdir to run elsewhere.",
17
+ "timeout is in milliseconds, defaults to 120000, and may not exceed 600000.",
18
+ "Every command runs in the foreground. Background command execution is not supported; shell jobs are waited for before the tool returns.",
19
+ ].join("\n"),
20
+ parameters: Type.Object(
21
+ {
22
+ command: Type.String({ description: "The command to execute" }),
23
+ workdir: Type.Optional(
24
+ Type.String({
25
+ description:
26
+ "Working directory to execute the command in. Defaults to the current directory; relative paths resolve from there.",
27
+ }),
28
+ ),
29
+ timeout: Type.Optional(
30
+ Type.Number({ description: "Optional timeout in milliseconds (max 600000)" }),
31
+ ),
32
+ dangerouslyDisableSandbox: Type.Optional(
33
+ Type.Boolean({
34
+ description:
35
+ "Request one-time unsandboxed execution. The user must approve this request.",
36
+ }),
37
+ ),
38
+ },
39
+ { additionalProperties: false },
40
+ ),
41
+ async execute(id, params, signal, onUpdate, ctx) {
42
+ const timeout = params.timeout ?? DEFAULT_TIMEOUT_MS;
43
+ if (!Number.isFinite(timeout) || timeout <= 0 || timeout > MAX_TIMEOUT_MS) {
44
+ throw new Error(`timeout must be between 1 and ${MAX_TIMEOUT_MS} milliseconds`);
45
+ }
46
+
47
+ const cwd = params.workdir ? await resolveWorkdir(params.workdir, ctx.cwd) : ctx.cwd;
48
+
49
+ try {
50
+ return await bwrapRuntime.execute({
51
+ ctx: { ...ctx, cwd },
52
+ toolCallId: id,
53
+ command: params.command,
54
+ timeout: timeout / 1000,
55
+ requestFullAccess: params.dangerouslyDisableSandbox,
56
+ signal,
57
+ onUpdate,
58
+ });
59
+ } catch (error) {
60
+ if (!(error instanceof Error)) throw error;
61
+ const timeoutMatch = /Command timed out after [\d.]+ seconds/.exec(error.message);
62
+ const message = timeoutMatch
63
+ ? error.message.slice(0, timeoutMatch.index) +
64
+ `Command timed out after ${timeout} milliseconds` +
65
+ error.message.slice(timeoutMatch.index + timeoutMatch[0].length)
66
+ : error.message;
67
+ throw new Error(message, { cause: error });
68
+ }
69
+ },
70
+ });
71
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * opencode —— 统一注册 opencode 风格工具扩展。
3
3
  *
4
- * 聚合 read / edit / write / todo / question 五个工具,一次加载全部注册;
4
+ * 聚合 read / edit / write / todo / question / bash 六个工具,一次加载全部注册;
5
5
  * 各工具的公开 API(匹配引擎、纯函数等)也从这里重新导出,方便
6
6
  * 测试与其他模块(如 workspace-guard)引用。
7
7
  *
@@ -14,12 +14,14 @@
14
14
 
15
15
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
16
16
 
17
+ import opencodeBash from "./bash.js";
17
18
  import opencodeEdit from "./edit.js";
18
19
  import opencodeQuestion from "./question.js";
19
20
  import opencodeRead from "./read.js";
20
21
  import opencodeTodo from "./todo.js";
21
22
  import opencodeWrite from "./write.js";
22
23
 
24
+ export { default as opencodeBash } from "./bash.js";
23
25
  export { default as opencodeEdit } from "./edit.js";
24
26
  export {
25
27
  detectLineEnding,
@@ -40,4 +42,5 @@ export default function opencode(pi: ExtensionAPI) {
40
42
  opencodeWrite(pi);
41
43
  opencodeTodo(pi);
42
44
  opencodeQuestion(pi);
45
+ opencodeBash(pi);
43
46
  }