@oyasmi/pipiclaw 0.5.6 → 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)
@@ -327,6 +340,9 @@ pipiclaw
327
340
 
328
341
  确认当前可见模型和默认模型都符合预期后,再发送一条普通消息,例如:
329
342
 
343
+ - `/model` 会列出当前模型和可用模型
344
+ - 切换时支持精确的 `provider/modelId`、精确的 `modelId`,以及能唯一命中的片段字符串,例如 `/model turbo`
345
+
330
346
  ```text
331
347
  请介绍一下你自己,并说明你现在能做什么
332
348
  ```
@@ -394,9 +410,22 @@ Pipiclaw 有两层命令。
394
410
  | `/new` | 开启一个新的会话 |
395
411
  | `/compact [instructions]` | 手动压缩当前会话上下文,可附带额外说明 |
396
412
  | `/session` | 查看当前会话状态、消息统计、token 使用量和当前模型 |
397
- | `/model [provider/modelId|modelId]` | 查看当前模型,或切换到指定模型 |
413
+ | `/model [provider/modelId|modelId|substring]` | 查看当前模型,或切换到指定模型 |
414
+
415
+ `/model` 的匹配顺序是:
416
+
417
+ 1. 精确匹配 `provider/modelId`
418
+ 2. 精确匹配 `modelId`
419
+ 3. 对完整的 `provider/modelId` 做子字符串匹配
420
+
421
+ 只有当片段字符串能唯一命中一个可用模型时才会切换。例如:
422
+
423
+ - `/model qwen`
424
+ - `/model k2.5`
425
+ - `/model turbo`
426
+ - `/model zpai`
398
427
 
399
- `/model` 只支持精确匹配切换。
428
+ `/model glm5` 这种不构成子字符串的输入不会命中。
400
429
 
401
430
  ## 工作区结构(Workspace Layout)
402
431
 
@@ -502,7 +531,7 @@ Pipiclaw 不会把所有历史对话无上限地塞进 prompt,而是按层管
502
531
  - 如果是 OpenAI-compatible 服务,是否需要:
503
532
  - `"supportsDeveloperRole": false`
504
533
  - `"supportsReasoningEffort": false`
505
- - 给机器人发送 `/model`,确认当前可见模型和默认模型是否正确
534
+ - 给机器人发送 `/model`,确认当前可见模型和默认模型是否正确;如需切换,也可以用唯一命中的片段,例如 `/model turbo`
506
535
 
507
536
  ### 机器人能收到消息,但没有回复(The Bot Receives Messages but Does Not Reply)
508
537
 
@@ -1,5 +1,5 @@
1
1
  import { basename } from "path";
2
- import { findExactModelReferenceMatch, formatModelList, formatModelReference } from "../models/utils.js";
2
+ import { findModelReferenceMatch, formatModelList, formatModelReference } from "../models/utils.js";
3
3
  export const COMMAND_RESULT_CUSTOM_TYPE = "pipiclaw.command_result";
4
4
  function buildSessionText(stats, currentModel, thinkingLevel) {
5
5
  const modelText = currentModel ? `\`${formatModelReference(currentModel)}\`` : "(none)";
@@ -43,7 +43,7 @@ export function createCommandExtension(options) {
43
43
  },
44
44
  });
45
45
  pi.registerCommand("model", {
46
- description: "Show the current model or switch models using an exact match",
46
+ description: "Show the current model or switch models using an exact or uniquely matching substring",
47
47
  handler: async (args) => {
48
48
  const availableModels = await options.getAvailableModels();
49
49
  const currentModel = options.getCurrentModel();
@@ -54,13 +54,13 @@ export function createCommandExtension(options) {
54
54
 
55
55
  Current model: ${current}
56
56
 
57
- Use \`/model <provider/modelId>\` or \`/model <modelId>\` to switch. Bare model IDs must resolve uniquely.
57
+ Use \`/model <provider/modelId>\`, \`/model <modelId>\`, or any uniquely matching substring to switch.
58
58
 
59
59
  Available models:
60
60
  ${available}`);
61
61
  return;
62
62
  }
63
- const match = findExactModelReferenceMatch(args, availableModels);
63
+ const match = findModelReferenceMatch(args, availableModels);
64
64
  const available = availableModels.length > 0 ? formatModelList(availableModels, currentModel, 10) : "- (none)";
65
65
  if (match.match) {
66
66
  await options.switchModel(match.match);
@@ -68,13 +68,13 @@ ${available}`);
68
68
  return;
69
69
  }
70
70
  if (match.ambiguous) {
71
- sendCommandResult(pi, `未切换模型:\`${args.trim()}\` 匹配到多个模型。请改用精确的 \`provider/modelId\` 形式。
71
+ sendCommandResult(pi, `未切换模型:\`${args.trim()}\` 匹配到多个模型。请提供更精确的 \`provider/modelId\`、\`modelId\` 或更长的片段。
72
72
 
73
73
  Available models:
74
74
  ${available}`);
75
75
  return;
76
76
  }
77
- sendCommandResult(pi, `未找到模型 \`${args.trim()}\`。请使用精确的 \`provider/modelId\` 或唯一的 \`modelId\`。
77
+ sendCommandResult(pi, `未找到模型 \`${args.trim()}\`。请使用精确的 \`provider/modelId\`、唯一的 \`modelId\`,或能唯一命中的片段字符串。
78
78
 
79
79
  Available models:
80
80
  ${available}`);
@@ -29,7 +29,7 @@ These are handled inside the Pipiclaw session layer:
29
29
  Show current session state, message stats, token usage, and model info
30
30
  Example: \`/session\`
31
31
  - \`/model [provider/modelId|modelId]\`
32
- Show the current model, or switch models using an exact match
32
+ Show the current model, or switch models using an exact match or a uniquely matching substring
33
33
  Example: \`/model\`
34
34
  Example: \`/model anthropic/claude-opus-4-6\`
35
35
  - \`/new\`
package/dist/index.d.ts CHANGED
@@ -11,7 +11,7 @@ export { type RecalledMemory, type RecallRequest, type RecallResult, recallRelev
11
11
  export { renderSessionMemory, type SessionMemoryState, type SessionMemoryUpdateOptions, updateChannelSessionMemory, } from "./memory/session.js";
12
12
  export { runSidecarTask, type SidecarResult, type SidecarTask, } from "./memory/sidecar-worker.js";
13
13
  export { getApiKeyForModel } from "./models/api-keys.js";
14
- export { findExactModelReferenceMatch, formatModelList, formatModelReference, resolveInitialModel, } from "./models/utils.js";
14
+ export { findExactModelReferenceMatch, findModelReferenceMatch, formatModelList, formatModelReference, resolveInitialModel, } from "./models/utils.js";
15
15
  export { APP_HOME_DIR, APP_NAME, AUTH_CONFIG_PATH, CHANNEL_CONFIG_PATH, MODELS_CONFIG_PATH, SETTINGS_CONFIG_PATH, SUB_AGENTS_DIR, SUB_AGENTS_DIR_NAME, WORKSPACE_DIR, } from "./paths.js";
16
16
  export { ensureChannelDir, getChannelDir, getChannelDirName, } from "./runtime/channel-paths.js";
17
17
  export { createDingTalkContext } from "./runtime/delivery.js";
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ export { recallRelevantMemory, } from "./memory/recall.js";
11
11
  export { renderSessionMemory, updateChannelSessionMemory, } from "./memory/session.js";
12
12
  export { runSidecarTask, } from "./memory/sidecar-worker.js";
13
13
  export { getApiKeyForModel } from "./models/api-keys.js";
14
- export { findExactModelReferenceMatch, formatModelList, formatModelReference, resolveInitialModel, } from "./models/utils.js";
14
+ export { findExactModelReferenceMatch, findModelReferenceMatch, formatModelList, formatModelReference, resolveInitialModel, } from "./models/utils.js";
15
15
  export { APP_HOME_DIR, APP_NAME, AUTH_CONFIG_PATH, CHANNEL_CONFIG_PATH, MODELS_CONFIG_PATH, SETTINGS_CONFIG_PATH, SUB_AGENTS_DIR, SUB_AGENTS_DIR_NAME, WORKSPACE_DIR, } from "./paths.js";
16
16
  export { ensureChannelDir, getChannelDir, getChannelDirName, } from "./runtime/channel-paths.js";
17
17
  export { createDingTalkContext } from "./runtime/delivery.js";
@@ -6,5 +6,9 @@ export declare function findExactModelReferenceMatch(modelReference: string, ava
6
6
  match?: Model<Api>;
7
7
  ambiguous: boolean;
8
8
  };
9
+ export declare function findModelReferenceMatch(modelReference: string, availableModels: Model<Api>[]): {
10
+ match?: Model<Api>;
11
+ ambiguous: boolean;
12
+ };
9
13
  export declare function formatModelList(models: Model<Api>[], currentModel: Model<Api> | undefined, limit?: number): string;
10
14
  export declare function resolveInitialModel(modelRegistry: ModelRegistry, settingsManager: PipiclawSettingsManager): Model<Api>;
@@ -38,6 +38,21 @@ export function findExactModelReferenceMatch(modelReference, availableModels) {
38
38
  }
39
39
  return { ambiguous: idMatches.length > 1 };
40
40
  }
41
+ export function findModelReferenceMatch(modelReference, availableModels) {
42
+ const exactMatch = findExactModelReferenceMatch(modelReference, availableModels);
43
+ if (exactMatch.match || exactMatch.ambiguous) {
44
+ return exactMatch;
45
+ }
46
+ const normalizedReference = modelReference.trim().toLowerCase();
47
+ if (!normalizedReference) {
48
+ return { ambiguous: false };
49
+ }
50
+ const substringMatches = availableModels.filter((model) => formatModelReference(model).toLowerCase().includes(normalizedReference));
51
+ if (substringMatches.length === 1) {
52
+ return { match: substringMatches[0], ambiguous: false };
53
+ }
54
+ return { ambiguous: substringMatches.length > 1 };
55
+ }
41
56
  export function formatModelList(models, currentModel, limit = 20) {
42
57
  const refs = models
43
58
  .slice()
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.6",
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": {