@oyasmi/pipiclaw 0.5.7 → 0.5.8

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
@@ -129,6 +129,13 @@ Pipiclaw 当前已经内置一轮工具层安全增强:
129
129
  - 直接使用 Anthropic 默认模型
130
130
  - 或在 `models.json` 中配置自定义模型提供方(provider)
131
131
 
132
+ Windows 补充说明:
133
+
134
+ - Pipiclaw 的工具执行层默认按 POSIX shell 语义工作
135
+ - 在 Windows host 模式下,建议安装 Git Bash,并确保 `bash` 可在 PATH 中找到
136
+ - 如果 `bash` 不在 PATH 中,可以设置 `PIPICLAW_SHELL` 指向具体可执行文件,例如 `C:\Program Files\Git\bin\bash.exe`
137
+ - 如果你不想依赖本机 shell 环境,推荐直接使用 Docker sandbox
138
+
132
139
  #### 2. 安装(Install)
133
140
 
134
141
  ```bash
@@ -169,6 +176,12 @@ export PIPICLAW_HOME=/your/custom/pipiclaw-home
169
176
 
170
177
  设置后,`channel.json`、`auth.json`、`models.json`、`settings.json` 和整个 `workspace/` 都会改为从这个目录读取和写入。
171
178
 
179
+ 如果你在 Windows host 模式下运行,并且 `bash` 不在 PATH 中,也可以一并设置:
180
+
181
+ ```powershell
182
+ $env:PIPICLAW_SHELL = "C:\Program Files\Git\bin\bash.exe"
183
+ ```
184
+
172
185
  如果 `channel.json` 仍然是初始化模板,程序会提示你补全配置后再启动。这是正常行为。
173
186
 
174
187
  #### 4. 创建钉钉应用(Create a DingTalk App)
package/dist/sandbox.js CHANGED
@@ -1,4 +1,4 @@
1
- import { spawn } from "child_process";
1
+ import { spawn, spawnSync } from "child_process";
2
2
  import { shellEscape } from "./shared/shell-escape.js";
3
3
  export function parseSandboxArg(value) {
4
4
  if (value === "host") {
@@ -17,6 +17,15 @@ export function parseSandboxArg(value) {
17
17
  }
18
18
  export async function validateSandbox(config) {
19
19
  if (config.type === "host") {
20
+ if (process.platform === "win32") {
21
+ try {
22
+ resolveWindowsHostShell();
23
+ }
24
+ catch (error) {
25
+ console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
26
+ process.exit(1);
27
+ }
28
+ }
20
29
  return;
21
30
  }
22
31
  // Check if Docker is available
@@ -45,7 +54,10 @@ export async function validateSandbox(config) {
45
54
  }
46
55
  function execSimple(cmd, args) {
47
56
  return new Promise((resolve, reject) => {
48
- const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
57
+ const child = spawn(cmd, args, {
58
+ stdio: ["ignore", "pipe", "pipe"],
59
+ windowsHide: true,
60
+ });
49
61
  let stdout = "";
50
62
  let stderr = "";
51
63
  child.stdout?.on("data", (d) => {
@@ -62,6 +74,51 @@ function execSimple(cmd, args) {
62
74
  });
63
75
  });
64
76
  }
77
+ const WINDOWS_POSIX_SHELL_CANDIDATES = [
78
+ "bash",
79
+ "sh",
80
+ "C:\\Program Files\\Git\\bin\\bash.exe",
81
+ "C:\\Program Files\\Git\\usr\\bin\\bash.exe",
82
+ "C:\\Program Files (x86)\\Git\\bin\\bash.exe",
83
+ "C:\\Program Files (x86)\\Git\\usr\\bin\\bash.exe",
84
+ ];
85
+ let cachedWindowsHostShell;
86
+ function looksLikeUnixShellPath(shell) {
87
+ return shell.endsWith("/bash") || shell.endsWith("/sh");
88
+ }
89
+ function isUsablePosixShell(command) {
90
+ const result = spawnSync(command, ["-lc", "printf pipiclaw"], {
91
+ stdio: ["ignore", "pipe", "ignore"],
92
+ encoding: "utf-8",
93
+ windowsHide: true,
94
+ });
95
+ return !result.error && result.status === 0 && result.stdout === "pipiclaw";
96
+ }
97
+ function resolveWindowsHostShell() {
98
+ if (cachedWindowsHostShell) {
99
+ return cachedWindowsHostShell;
100
+ }
101
+ const configuredShell = process.env.PIPICLAW_SHELL?.trim();
102
+ const inheritedShell = process.env.SHELL?.trim();
103
+ const shellCandidates = [
104
+ configuredShell,
105
+ inheritedShell && looksLikeUnixShellPath(inheritedShell) ? inheritedShell.split("/").pop() : undefined,
106
+ ...WINDOWS_POSIX_SHELL_CANDIDATES,
107
+ ].filter((value) => Boolean(value));
108
+ for (const command of shellCandidates) {
109
+ if (isUsablePosixShell(command)) {
110
+ cachedWindowsHostShell = { command, args: ["-lc"] };
111
+ return cachedWindowsHostShell;
112
+ }
113
+ }
114
+ throw new Error("Windows host sandbox requires a POSIX shell. Install Git Bash and ensure `bash` is on PATH, set `PIPICLAW_SHELL`, or use the Docker sandbox.");
115
+ }
116
+ function resolveHostShell() {
117
+ if (process.platform === "win32") {
118
+ return resolveWindowsHostShell();
119
+ }
120
+ return { command: "sh", args: ["-c"] };
121
+ }
65
122
  /**
66
123
  * Create an executor that runs commands either on host or in Docker container
67
124
  */
@@ -74,13 +131,13 @@ export function createExecutor(config) {
74
131
  class HostExecutor {
75
132
  async exec(command, options) {
76
133
  return new Promise((resolve, reject) => {
77
- const shell = process.platform === "win32" ? "cmd" : "sh";
78
- const shellArgs = process.platform === "win32" ? ["/c"] : ["-c"];
134
+ const shell = resolveHostShell();
79
135
  const child = (() => {
80
136
  try {
81
- return spawn(shell, [...shellArgs, command], {
137
+ return spawn(shell.command, [...shell.args, command], {
82
138
  detached: true,
83
139
  stdio: ["pipe", "pipe", "pipe"],
140
+ windowsHide: true,
84
141
  });
85
142
  }
86
143
  catch (err) {
@@ -199,6 +256,7 @@ function killProcessTree(pid) {
199
256
  spawn("taskkill", ["/F", "/T", "/PID", String(pid)], {
200
257
  stdio: "ignore",
201
258
  detached: true,
259
+ windowsHide: true,
202
260
  });
203
261
  }
204
262
  catch {
@@ -3,3 +3,10 @@
3
3
  * Wraps in single quotes and escapes internal single quotes.
4
4
  */
5
5
  export declare function shellEscape(s: string): string;
6
+ /**
7
+ * Normalize filesystem paths for POSIX-style shells.
8
+ * On Windows we convert backslashes to forward slashes so Git Bash/MSYS tools
9
+ * can consume the path consistently.
10
+ */
11
+ export declare function toShellPath(path: string): string;
12
+ export declare function shellEscapePath(path: string): string;
@@ -5,3 +5,14 @@
5
5
  export function shellEscape(s) {
6
6
  return `'${s.replace(/'/g, "'\\''")}'`;
7
7
  }
8
+ /**
9
+ * Normalize filesystem paths for POSIX-style shells.
10
+ * On Windows we convert backslashes to forward slashes so Git Bash/MSYS tools
11
+ * can consume the path consistently.
12
+ */
13
+ export function toShellPath(path) {
14
+ return process.platform === "win32" ? path.replace(/\\/g, "/") : path;
15
+ }
16
+ export function shellEscapePath(path) {
17
+ return shellEscape(toShellPath(path));
18
+ }
@@ -3,7 +3,7 @@ import * as Diff from "diff";
3
3
  import { DEFAULT_SECURITY_CONFIG } from "../security/config.js";
4
4
  import { logSecurityEvent } from "../security/logger.js";
5
5
  import { guardPath } from "../security/path-guard.js";
6
- import { shellEscape } from "../shared/shell-escape.js";
6
+ import { shellEscapePath } from "../shared/shell-escape.js";
7
7
  import { writeContent } from "./write-content.js";
8
8
  /**
9
9
  * Generate a unified diff string with line numbers and context
@@ -123,7 +123,7 @@ export function createEditTool(executor, options = {}) {
123
123
  }
124
124
  }
125
125
  // Read the file
126
- const readResult = await executor.exec(`cat ${shellEscape(path)}`, { signal });
126
+ const readResult = await executor.exec(`cat ${shellEscapePath(path)}`, { signal });
127
127
  if (readResult.code !== 0) {
128
128
  throw new Error(readResult.stderr || `File not found: ${path}`);
129
129
  }
@@ -3,7 +3,7 @@ import { extname } from "path";
3
3
  import { DEFAULT_SECURITY_CONFIG } from "../security/config.js";
4
4
  import { logSecurityEvent } from "../security/logger.js";
5
5
  import { guardPath } from "../security/path-guard.js";
6
- import { shellEscape } from "../shared/shell-escape.js";
6
+ import { shellEscapePath, toShellPath } from "../shared/shell-escape.js";
7
7
  import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead } from "./truncate.js";
8
8
  /**
9
9
  * Map of file extensions to MIME types for common image formats
@@ -70,7 +70,7 @@ export function createReadTool(executor, options = {}) {
70
70
  const mimeType = isImageFile(path);
71
71
  if (mimeType) {
72
72
  // Read as image (binary) - use base64
73
- const result = await executor.exec(`base64 < ${shellEscape(path)}`, { signal });
73
+ const result = await executor.exec(`base64 < ${shellEscapePath(path)}`, { signal });
74
74
  if (result.code !== 0) {
75
75
  throw new Error(result.stderr || `Failed to read file: ${path}`);
76
76
  }
@@ -84,7 +84,7 @@ export function createReadTool(executor, options = {}) {
84
84
  };
85
85
  }
86
86
  // Get total line count first
87
- const countResult = await executor.exec(`wc -l < ${shellEscape(path)}`, { signal });
87
+ const countResult = await executor.exec(`wc -l < ${shellEscapePath(path)}`, { signal });
88
88
  if (countResult.code !== 0) {
89
89
  throw new Error(countResult.stderr || `Failed to read file: ${path}`);
90
90
  }
@@ -99,10 +99,10 @@ export function createReadTool(executor, options = {}) {
99
99
  // Read content with offset
100
100
  let cmd;
101
101
  if (startLine === 1) {
102
- cmd = `cat ${shellEscape(path)}`;
102
+ cmd = `cat ${shellEscapePath(path)}`;
103
103
  }
104
104
  else {
105
- cmd = `tail -n +${startLine} ${shellEscape(path)}`;
105
+ cmd = `tail -n +${startLine} ${shellEscapePath(path)}`;
106
106
  }
107
107
  const result = await executor.exec(cmd, { signal });
108
108
  if (result.code !== 0) {
@@ -124,7 +124,7 @@ export function createReadTool(executor, options = {}) {
124
124
  if (truncation.firstLineExceedsLimit) {
125
125
  // First line at offset exceeds 50KB - tell model to use bash
126
126
  const firstLineSize = formatSize(Buffer.byteLength(selectedContent.split("\n")[0], "utf-8"));
127
- outputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${DEFAULT_MAX_BYTES}]`;
127
+ outputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLineDisplay}p' ${toShellPath(path)} | head -c ${DEFAULT_MAX_BYTES}]`;
128
128
  details = { truncation };
129
129
  }
130
130
  else if (truncation.truncated) {
@@ -1,9 +1,10 @@
1
+ import { dirname } from "node:path";
1
2
  import { DEFAULT_SECURITY_CONFIG } from "../security/config.js";
2
3
  import { logSecurityEvent } from "../security/logger.js";
3
4
  import { guardPath } from "../security/path-guard.js";
4
- import { shellEscape } from "../shared/shell-escape.js";
5
+ import { shellEscapePath } from "../shared/shell-escape.js";
5
6
  function getDir(path) {
6
- return path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : ".";
7
+ return dirname(path);
7
8
  }
8
9
  function ensureSuccess(result, path) {
9
10
  if (result.code !== 0) {
@@ -41,8 +42,8 @@ export async function writeContent(executor, path, content, signal, options) {
41
42
  throw new Error(lines.join("\n"));
42
43
  }
43
44
  }
44
- const dirPrefix = createParentDir ? `mkdir -p ${shellEscape(getDir(path))} && ` : "";
45
- const result = await executor.exec(`${dirPrefix}cat > ${shellEscape(path)}`, {
45
+ const dirPrefix = createParentDir ? `mkdir -p ${shellEscapePath(getDir(path))} && ` : "";
46
+ const result = await executor.exec(`${dirPrefix}cat > ${shellEscapePath(path)}`, {
46
47
  signal,
47
48
  stdin: content,
48
49
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oyasmi/pipiclaw",
3
- "version": "0.5.7",
3
+ "version": "0.5.8",
4
4
  "description": "An AI assistant runtime for coding and team workflows, with DingTalk AI Cards, sub-agents, memory, and scheduled events.",
5
5
  "type": "module",
6
6
  "bin": {