@oyasmi/pipiclaw 0.5.5 → 0.5.7
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 +54 -3
- package/dist/agent/channel-runner.d.ts +1 -0
- package/dist/agent/channel-runner.js +3 -0
- package/dist/agent/command-extension.js +6 -6
- package/dist/agent/commands.js +1 -1
- package/dist/agent/runner-factory.d.ts +2 -0
- package/dist/agent/runner-factory.js +6 -0
- package/dist/agent/types.d.ts +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/memory/lifecycle.d.ts +2 -1
- package/dist/memory/lifecycle.js +19 -1
- package/dist/models/utils.d.ts +4 -0
- package/dist/models/utils.js +15 -0
- package/dist/paths.js +1 -1
- package/dist/runtime/bootstrap.d.ts +22 -1
- package/dist/runtime/bootstrap.js +72 -26
- package/dist/security/command-guard.d.ts +16 -0
- package/dist/security/command-guard.js +447 -0
- package/dist/security/config.d.ts +4 -0
- package/dist/security/config.js +82 -0
- package/dist/security/logger.d.ts +2 -0
- package/dist/security/logger.js +18 -0
- package/dist/security/path-guard.d.ts +2 -0
- package/dist/security/path-guard.js +237 -0
- package/dist/security/types.d.ts +66 -0
- package/dist/security/types.js +1 -0
- package/dist/subagents/tool.d.ts +2 -0
- package/dist/subagents/tool.js +31 -7
- package/dist/tools/attach.d.ts +7 -1
- package/dist/tools/attach.js +36 -1
- package/dist/tools/bash.d.ts +4 -0
- package/dist/tools/bash.js +38 -0
- package/dist/tools/edit.d.ts +7 -1
- package/dist/tools/edit.js +42 -2
- package/dist/tools/index.d.ts +7 -1
- package/dist/tools/index.js +29 -3
- package/dist/tools/read.d.ts +7 -1
- package/dist/tools/read.js +36 -1
- package/dist/tools/write-content.d.ts +5 -0
- package/dist/tools/write-content.js +32 -11
- package/dist/tools/write.d.ts +7 -1
- package/dist/tools/write.js +10 -3
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ npm package: [`@oyasmi/pipiclaw`](https://www.npmjs.com/package/@oyasmi/pipiclaw
|
|
|
11
11
|
- 配置手册:[docs/configuration.md](./docs/configuration.md)
|
|
12
12
|
- 事件与子代理使用指南:[docs/events-and-sub-agents.md](./docs/events-and-sub-agents.md)
|
|
13
13
|
- 部署与运维指南:[docs/deployment-and-operations.md](./docs/deployment-and-operations.md)
|
|
14
|
+
- 安全文档:[docs/security.md](./docs/security.md)
|
|
14
15
|
|
|
15
16
|
## 功能特性(Features)
|
|
16
17
|
|
|
@@ -22,6 +23,20 @@ npm package: [`@oyasmi/pipiclaw`](https://www.npmjs.com/package/@oyasmi/pipiclaw
|
|
|
22
23
|
- 支持预定义子代理(sub-agent)和临时内联子代理(inline sub-agent)
|
|
23
24
|
- 支持立即、单次、周期三类事件调度
|
|
24
25
|
- 支持自定义模型提供方(provider)和模型(model)配置
|
|
26
|
+
- 内置工具层安全防护:`bash` 命令守卫、文件路径守卫、敏感路径拒绝、阻断审计日志
|
|
27
|
+
|
|
28
|
+
## 安全说明(Security)
|
|
29
|
+
|
|
30
|
+
Pipiclaw 当前已经内置一轮工具层安全增强:
|
|
31
|
+
|
|
32
|
+
- `bash` 会拦截明显高风险命令
|
|
33
|
+
- `read` / `write` / `edit` / `attach` 会做统一路径检查
|
|
34
|
+
- 默认允许访问用户主目录中的普通工作文件,但会拒绝常见凭据、私钥、浏览器资料、系统敏感文件等位置
|
|
35
|
+
- 可通过 `~/.pi/pipiclaw/security.json` 做实例级策略调整
|
|
36
|
+
|
|
37
|
+
如果你要了解默认策略、已知边界、推荐模板和完整配置示例,请直接看:
|
|
38
|
+
|
|
39
|
+
- [docs/security.md](./docs/security.md)
|
|
25
40
|
|
|
26
41
|
## 快速开始(Quickstart)
|
|
27
42
|
|
|
@@ -146,6 +161,14 @@ pipiclaw
|
|
|
146
161
|
└── sub-agents/
|
|
147
162
|
```
|
|
148
163
|
|
|
164
|
+
默认 app home 是 `~/.pi/pipiclaw/`。如果你希望把 Pipiclaw 的所有配置和运行文件放到别处,可以在启动前设置:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
export PIPICLAW_HOME=/your/custom/pipiclaw-home
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
设置后,`channel.json`、`auth.json`、`models.json`、`settings.json` 和整个 `workspace/` 都会改为从这个目录读取和写入。
|
|
171
|
+
|
|
149
172
|
如果 `channel.json` 仍然是初始化模板,程序会提示你补全配置后再启动。这是正常行为。
|
|
150
173
|
|
|
151
174
|
#### 4. 创建钉钉应用(Create a DingTalk App)
|
|
@@ -304,6 +327,9 @@ pipiclaw
|
|
|
304
327
|
|
|
305
328
|
确认当前可见模型和默认模型都符合预期后,再发送一条普通消息,例如:
|
|
306
329
|
|
|
330
|
+
- `/model` 会列出当前模型和可用模型
|
|
331
|
+
- 切换时支持精确的 `provider/modelId`、精确的 `modelId`,以及能唯一命中的片段字符串,例如 `/model turbo`
|
|
332
|
+
|
|
307
333
|
```text
|
|
308
334
|
请介绍一下你自己,并说明你现在能做什么
|
|
309
335
|
```
|
|
@@ -327,6 +353,8 @@ pipiclaw
|
|
|
327
353
|
|
|
328
354
|
### 配置文件(Config Files)
|
|
329
355
|
|
|
356
|
+
默认根目录是 `~/.pi/pipiclaw/`;如果设置了 `PIPICLAW_HOME`,下面这些路径都会切换到该目录下。
|
|
357
|
+
|
|
330
358
|
| 文件 | 用途 |
|
|
331
359
|
|------|------|
|
|
332
360
|
| `~/.pi/pipiclaw/channel.json` | 钉钉应用配置 |
|
|
@@ -339,6 +367,7 @@ pipiclaw
|
|
|
339
367
|
| 变量 | 用途 |
|
|
340
368
|
|----------|------|
|
|
341
369
|
| `ANTHROPIC_API_KEY` | Anthropic API Key |
|
|
370
|
+
| `PIPICLAW_HOME` | 覆盖默认的 `~/.pi/pipiclaw/` 根目录 |
|
|
342
371
|
| `PIPICLAW_DEBUG` | 调试模式,会把上下文写到 `last_prompt.json` |
|
|
343
372
|
| `DINGTALK_FORCE_PROXY` | 设为 `true` 时保留 axios 代理设置 |
|
|
344
373
|
|
|
@@ -368,9 +397,22 @@ Pipiclaw 有两层命令。
|
|
|
368
397
|
| `/new` | 开启一个新的会话 |
|
|
369
398
|
| `/compact [instructions]` | 手动压缩当前会话上下文,可附带额外说明 |
|
|
370
399
|
| `/session` | 查看当前会话状态、消息统计、token 使用量和当前模型 |
|
|
371
|
-
| `/model [provider/modelId|modelId]` | 查看当前模型,或切换到指定模型 |
|
|
400
|
+
| `/model [provider/modelId|modelId|substring]` | 查看当前模型,或切换到指定模型 |
|
|
401
|
+
|
|
402
|
+
`/model` 的匹配顺序是:
|
|
403
|
+
|
|
404
|
+
1. 精确匹配 `provider/modelId`
|
|
405
|
+
2. 精确匹配 `modelId`
|
|
406
|
+
3. 对完整的 `provider/modelId` 做子字符串匹配
|
|
372
407
|
|
|
373
|
-
|
|
408
|
+
只有当片段字符串能唯一命中一个可用模型时才会切换。例如:
|
|
409
|
+
|
|
410
|
+
- `/model qwen`
|
|
411
|
+
- `/model k2.5`
|
|
412
|
+
- `/model turbo`
|
|
413
|
+
- `/model zpai`
|
|
414
|
+
|
|
415
|
+
像 `/model glm5` 这种不构成子字符串的输入不会命中。
|
|
374
416
|
|
|
375
417
|
## 工作区结构(Workspace Layout)
|
|
376
418
|
|
|
@@ -476,7 +518,7 @@ Pipiclaw 不会把所有历史对话无上限地塞进 prompt,而是按层管
|
|
|
476
518
|
- 如果是 OpenAI-compatible 服务,是否需要:
|
|
477
519
|
- `"supportsDeveloperRole": false`
|
|
478
520
|
- `"supportsReasoningEffort": false`
|
|
479
|
-
- 给机器人发送 `/model
|
|
521
|
+
- 给机器人发送 `/model`,确认当前可见模型和默认模型是否正确;如需切换,也可以用唯一命中的片段,例如 `/model turbo`
|
|
480
522
|
|
|
481
523
|
### 机器人能收到消息,但没有回复(The Bot Receives Messages but Does Not Reply)
|
|
482
524
|
|
|
@@ -512,8 +554,17 @@ npm run check
|
|
|
512
554
|
|
|
513
555
|
- `npm run typecheck`
|
|
514
556
|
- `npm run test`
|
|
557
|
+
- `npm run test:e2e`
|
|
515
558
|
- `npm run check`
|
|
516
559
|
|
|
560
|
+
端到端测试说明:
|
|
561
|
+
|
|
562
|
+
- `npm run test:e2e` 会运行“除钉钉渠道外”的完整 E2E
|
|
563
|
+
- 它会使用真实 runtime、真实 `ChannelStore`、真实工具/记忆/Sidecar/LLM
|
|
564
|
+
- 只 mock 钉钉传输层,不连接真实钉钉 Stream
|
|
565
|
+
- 运行前需要可用模型凭据:优先读取 `${PIPICLAW_HOME:-~/.pi/pipiclaw}/auth.json`,否则回退到 `ANTHROPIC_API_KEY`
|
|
566
|
+
- E2E 默认不包含在 `npm run test` 中,避免日常测试被真实 LLM 依赖和调用成本影响
|
|
567
|
+
|
|
517
568
|
## 许可证(License)
|
|
518
569
|
|
|
519
570
|
Apache License 2.0. See [LICENSE](./LICENSE).
|
|
@@ -30,6 +30,7 @@ export declare class ChannelRunner implements AgentRunner {
|
|
|
30
30
|
handleBuiltinCommand(ctx: DingTalkContext, command: BuiltInCommand): Promise<void>;
|
|
31
31
|
queueSteer(text: string, userName?: string): Promise<void>;
|
|
32
32
|
queueFollowUp(text: string, userName?: string): Promise<void>;
|
|
33
|
+
flushMemoryForShutdown(): Promise<void>;
|
|
33
34
|
abort(): Promise<void>;
|
|
34
35
|
private sendCommandReply;
|
|
35
36
|
private requireQueuedMessage;
|
|
@@ -323,6 +323,9 @@ export class ChannelRunner {
|
|
|
323
323
|
async queueFollowUp(text, userName) {
|
|
324
324
|
await this.queueBusyMessage("followUp", this.requireQueuedMessage(text, "followup"), userName);
|
|
325
325
|
}
|
|
326
|
+
async flushMemoryForShutdown() {
|
|
327
|
+
await this.memoryLifecycle.flushForShutdown();
|
|
328
|
+
}
|
|
326
329
|
async abort() {
|
|
327
330
|
await this.session.abort();
|
|
328
331
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { basename } from "path";
|
|
2
|
-
import {
|
|
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
|
|
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
|
|
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 =
|
|
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()}\`
|
|
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
|
|
77
|
+
sendCommandResult(pi, `未找到模型 \`${args.trim()}\`。请使用精确的 \`provider/modelId\`、唯一的 \`modelId\`,或能唯一命中的片段字符串。
|
|
78
78
|
|
|
79
79
|
Available models:
|
|
80
80
|
${available}`);
|
package/dist/agent/commands.js
CHANGED
|
@@ -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\`
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import type { SandboxConfig } from "../sandbox.js";
|
|
2
2
|
import type { AgentRunner } from "./types.js";
|
|
3
3
|
export declare function getOrCreateRunner(sandboxConfig: SandboxConfig, channelId: string, channelDir: string): AgentRunner;
|
|
4
|
+
export declare function resetRunner(channelId: string): void;
|
|
5
|
+
export declare function resetAllRunners(): void;
|
|
@@ -8,3 +8,9 @@ export function getOrCreateRunner(sandboxConfig, channelId, channelDir) {
|
|
|
8
8
|
channelRunners.set(channelId, runner);
|
|
9
9
|
return runner;
|
|
10
10
|
}
|
|
11
|
+
export function resetRunner(channelId) {
|
|
12
|
+
channelRunners.delete(channelId);
|
|
13
|
+
}
|
|
14
|
+
export function resetAllRunners() {
|
|
15
|
+
channelRunners.clear();
|
|
16
|
+
}
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface AgentRunner {
|
|
|
10
10
|
handleBuiltinCommand(ctx: DingTalkContext, command: BuiltInCommand): Promise<void>;
|
|
11
11
|
queueSteer(text: string, userName?: string): Promise<void>;
|
|
12
12
|
queueFollowUp(text: string, userName?: string): Promise<void>;
|
|
13
|
+
flushMemoryForShutdown(): Promise<void>;
|
|
13
14
|
abort(): Promise<void>;
|
|
14
15
|
}
|
|
15
16
|
export type FinalOutcome = {
|
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";
|
|
@@ -2,7 +2,7 @@ import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
|
|
2
2
|
import type { Api, Model } from "@mariozechner/pi-ai";
|
|
3
3
|
import type { ExtensionFactory, SessionEntry } from "@mariozechner/pi-coding-agent";
|
|
4
4
|
import type { PipiclawSessionMemorySettings } from "../settings.js";
|
|
5
|
-
export type ConsolidationReason = "compaction" | "new-session" | "idle";
|
|
5
|
+
export type ConsolidationReason = "compaction" | "new-session" | "idle" | "shutdown";
|
|
6
6
|
export interface MemoryLifecycleOptions {
|
|
7
7
|
channelId: string;
|
|
8
8
|
channelDir: string;
|
|
@@ -33,6 +33,7 @@ export declare class MemoryLifecycle {
|
|
|
33
33
|
noteUserTurnStarted(): void;
|
|
34
34
|
noteToolCall(): void;
|
|
35
35
|
noteCompletedAssistantTurn(): void;
|
|
36
|
+
flushForShutdown(): Promise<void>;
|
|
36
37
|
private clearIdleConsolidationTimer;
|
|
37
38
|
private shouldForceRefreshFor;
|
|
38
39
|
private refreshSessionMemory;
|
package/dist/memory/lifecycle.js
CHANGED
|
@@ -73,6 +73,18 @@ export class MemoryLifecycle {
|
|
|
73
73
|
}
|
|
74
74
|
this.scheduleIdleConsolidation();
|
|
75
75
|
}
|
|
76
|
+
async flushForShutdown() {
|
|
77
|
+
this.clearIdleConsolidationTimer();
|
|
78
|
+
const run = async () => {
|
|
79
|
+
if (!this.hasPendingAssistantSnapshot()) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
await this.runPreflightConsolidation("shutdown");
|
|
83
|
+
};
|
|
84
|
+
const resultPromise = this.backgroundQueue.then(run, run);
|
|
85
|
+
this.backgroundQueue = resultPromise.then(() => undefined, () => undefined);
|
|
86
|
+
await resultPromise;
|
|
87
|
+
}
|
|
76
88
|
clearIdleConsolidationTimer() {
|
|
77
89
|
if (!this.idleConsolidationTimer) {
|
|
78
90
|
return;
|
|
@@ -84,7 +96,13 @@ export class MemoryLifecycle {
|
|
|
84
96
|
if (!settings.enabled) {
|
|
85
97
|
return false;
|
|
86
98
|
}
|
|
87
|
-
|
|
99
|
+
if (reason === "compaction") {
|
|
100
|
+
return settings.forceRefreshBeforeCompact;
|
|
101
|
+
}
|
|
102
|
+
if (reason === "new-session") {
|
|
103
|
+
return settings.forceRefreshBeforeNewSession;
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
88
106
|
}
|
|
89
107
|
async refreshSessionMemory(request) {
|
|
90
108
|
const settings = this.options.getSessionMemorySettings();
|
package/dist/models/utils.d.ts
CHANGED
|
@@ -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>;
|
package/dist/models/utils.js
CHANGED
|
@@ -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/paths.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { homedir } from "os";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
export const APP_NAME = "pipiclaw";
|
|
4
|
-
export const APP_HOME_DIR = join(homedir(), ".pi", APP_NAME);
|
|
4
|
+
export const APP_HOME_DIR = process.env.PIPICLAW_HOME ?? join(homedir(), ".pi", APP_NAME);
|
|
5
5
|
export const WORKSPACE_DIR = join(APP_HOME_DIR, "workspace");
|
|
6
6
|
export const SUB_AGENTS_DIR_NAME = "sub-agents";
|
|
7
7
|
export const SUB_AGENTS_DIR = join(WORKSPACE_DIR, SUB_AGENTS_DIR_NAME);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type SandboxConfig } from "../sandbox.js";
|
|
2
|
-
import { DingTalkBot, type DingTalkConfig } from "./dingtalk.js";
|
|
2
|
+
import { DingTalkBot, type DingTalkConfig, type DingTalkHandler } from "./dingtalk.js";
|
|
3
3
|
import { ChannelStore } from "./store.js";
|
|
4
4
|
export interface BootstrapPaths {
|
|
5
5
|
appName: string;
|
|
@@ -33,6 +33,11 @@ export interface AppContext {
|
|
|
33
33
|
store: ChannelStore;
|
|
34
34
|
shutdown: () => Promise<void>;
|
|
35
35
|
}
|
|
36
|
+
export interface RuntimeContext {
|
|
37
|
+
handler: DingTalkHandler;
|
|
38
|
+
store: ChannelStore;
|
|
39
|
+
shutdown: (reason?: NodeJS.Signals | "manual") => Promise<void>;
|
|
40
|
+
}
|
|
36
41
|
export declare const DEFAULT_BOOTSTRAP_PATHS: BootstrapPaths;
|
|
37
42
|
export declare class BootstrapExitError extends Error {
|
|
38
43
|
readonly code: number;
|
|
@@ -44,4 +49,20 @@ export declare function bootstrapAppHome(paths?: BootstrapPaths): BootstrapResul
|
|
|
44
49
|
export declare function printBootstrapSummary(result: BootstrapResult, io?: BootstrapIO, paths?: BootstrapPaths): void;
|
|
45
50
|
export declare function loadConfig(paths?: BootstrapPaths, io?: BootstrapIO): DingTalkConfig;
|
|
46
51
|
export declare function parseArgs(argv: string[], paths?: BootstrapPaths, io?: BootstrapIO): ParsedArgs;
|
|
52
|
+
interface RuntimeContextOptions {
|
|
53
|
+
paths: BootstrapPaths;
|
|
54
|
+
sandbox: SandboxConfig;
|
|
55
|
+
dingtalkConfig: DingTalkConfig;
|
|
56
|
+
createBot?: (handler: DingTalkHandler, config: DingTalkConfig) => DingTalkBot;
|
|
57
|
+
createEventsWatcher?: (workspaceDir: string, bot: DingTalkBot) => {
|
|
58
|
+
start(): void;
|
|
59
|
+
stop(): void;
|
|
60
|
+
};
|
|
61
|
+
startServices?: boolean;
|
|
62
|
+
registerSignalHandlers?: boolean;
|
|
63
|
+
}
|
|
64
|
+
export declare function createRuntimeContext(options: RuntimeContextOptions): RuntimeContext & {
|
|
65
|
+
bot: DingTalkBot;
|
|
66
|
+
};
|
|
47
67
|
export declare function bootstrap(argv: string[], options?: BootstrapOptions): Promise<AppContext>;
|
|
68
|
+
export {};
|
|
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { parseBuiltInCommand } from "../agent/commands.js";
|
|
4
4
|
import { getOrCreateRunner } from "../agent/index.js";
|
|
5
|
+
import { resetRunner } from "../agent/runner-factory.js";
|
|
5
6
|
import * as log from "../log.js";
|
|
6
7
|
import { ensureChannelMemoryFilesSync } from "../memory/files.js";
|
|
7
8
|
import { APP_HOME_DIR, APP_NAME, AUTH_CONFIG_PATH, CHANNEL_CONFIG_PATH, MODELS_CONFIG_PATH, SETTINGS_CONFIG_PATH, WORKSPACE_DIR, } from "../paths.js";
|
|
@@ -99,6 +100,7 @@ const CHANNEL_CONFIG_TEMPLATE = {
|
|
|
99
100
|
};
|
|
100
101
|
const MODELS_CONFIG_TEMPLATE = { providers: {} };
|
|
101
102
|
const SHUTDOWN_WAIT_MS = 15000;
|
|
103
|
+
const SHUTDOWN_FLUSH_WAIT_MS = 25000;
|
|
102
104
|
const SHUTDOWN_ABORT_WAIT_MS = 5000;
|
|
103
105
|
export const DEFAULT_BOOTSTRAP_PATHS = {
|
|
104
106
|
appName: APP_NAME,
|
|
@@ -269,25 +271,22 @@ function waitForTasks(tasks, timeoutMs) {
|
|
|
269
271
|
}),
|
|
270
272
|
]);
|
|
271
273
|
}
|
|
272
|
-
|
|
273
|
-
const
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const bootstrapResult = bootstrapAppHome(paths);
|
|
282
|
-
printBootstrapSummary(bootstrapResult, io, paths);
|
|
283
|
-
if (bootstrapResult.channelTemplateCreated) {
|
|
284
|
-
io.error(`Fill in ${paths.channelConfigPath} and run \`${paths.appName}\` again.`);
|
|
285
|
-
throw new BootstrapExitError(1);
|
|
274
|
+
function flushInactiveChannelMemory(channelStates) {
|
|
275
|
+
const flushes = [];
|
|
276
|
+
for (const [channelId, state] of channelStates) {
|
|
277
|
+
if (state.running) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
flushes.push(state.runner.flushMemoryForShutdown().catch((err) => {
|
|
281
|
+
log.logWarning(`[${channelId}] Failed to flush memory during shutdown`, err instanceof Error ? err.message : String(err));
|
|
282
|
+
}));
|
|
286
283
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const
|
|
284
|
+
return flushes;
|
|
285
|
+
}
|
|
286
|
+
export function createRuntimeContext(options) {
|
|
287
|
+
const startServices = options.startServices ?? true;
|
|
288
|
+
const registerSignalHandlers = options.registerSignalHandlers ?? true;
|
|
289
|
+
const store = new ChannelStore({ workingDir: options.paths.workspaceDir });
|
|
291
290
|
const channelStates = new Map();
|
|
292
291
|
const activeTasks = new Set();
|
|
293
292
|
let shuttingDown = false;
|
|
@@ -295,11 +294,11 @@ export async function bootstrap(argv, options = {}) {
|
|
|
295
294
|
const getState = (channelId) => {
|
|
296
295
|
let state = channelStates.get(channelId);
|
|
297
296
|
if (!state) {
|
|
298
|
-
const channelDir = ensureChannelDir(paths.workspaceDir, channelId);
|
|
297
|
+
const channelDir = ensureChannelDir(options.paths.workspaceDir, channelId);
|
|
299
298
|
ensureChannelMemoryFilesSync(channelDir);
|
|
300
299
|
state = {
|
|
301
300
|
running: false,
|
|
302
|
-
runner: getOrCreateRunner(sandbox, channelId, channelDir),
|
|
301
|
+
runner: getOrCreateRunner(options.sandbox, channelId, channelDir),
|
|
303
302
|
stopRequested: false,
|
|
304
303
|
};
|
|
305
304
|
channelStates.set(channelId, state);
|
|
@@ -405,10 +404,13 @@ export async function bootstrap(argv, options = {}) {
|
|
|
405
404
|
}
|
|
406
405
|
},
|
|
407
406
|
};
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const
|
|
407
|
+
const bot = options.createBot
|
|
408
|
+
? options.createBot(handler, options.dingtalkConfig)
|
|
409
|
+
: new DingTalkBot(handler, options.dingtalkConfig);
|
|
410
|
+
const eventsWatcher = options.createEventsWatcher
|
|
411
|
+
? options.createEventsWatcher(options.paths.workspaceDir, bot)
|
|
412
|
+
: createEventsWatcher(options.paths.workspaceDir, bot);
|
|
413
|
+
const shutdownWithReason = async (reason = "manual") => {
|
|
412
414
|
if (shutdownPromise) {
|
|
413
415
|
return shutdownPromise;
|
|
414
416
|
}
|
|
@@ -443,6 +445,17 @@ export async function bootstrap(argv, options = {}) {
|
|
|
443
445
|
}
|
|
444
446
|
}
|
|
445
447
|
}
|
|
448
|
+
const flushes = flushInactiveChannelMemory(channelStates);
|
|
449
|
+
if (flushes.length > 0) {
|
|
450
|
+
log.logInfo(`Flushing memory for ${flushes.length} inactive channel(s) before shutdown`);
|
|
451
|
+
const flushed = await waitForTasks(flushes, SHUTDOWN_FLUSH_WAIT_MS);
|
|
452
|
+
if (!flushed) {
|
|
453
|
+
log.logWarning(`Shutdown memory flush exceeded ${SHUTDOWN_FLUSH_WAIT_MS}ms`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
for (const channelId of channelStates.keys()) {
|
|
457
|
+
resetRunner(channelId);
|
|
458
|
+
}
|
|
446
459
|
})();
|
|
447
460
|
return shutdownPromise;
|
|
448
461
|
};
|
|
@@ -463,10 +476,43 @@ export async function bootstrap(argv, options = {}) {
|
|
|
463
476
|
void bot.start();
|
|
464
477
|
}
|
|
465
478
|
return {
|
|
466
|
-
|
|
479
|
+
handler,
|
|
467
480
|
store,
|
|
481
|
+
bot,
|
|
482
|
+
shutdown: shutdownWithReason,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
export async function bootstrap(argv, options = {}) {
|
|
486
|
+
const env = options.env ?? process.env;
|
|
487
|
+
const io = options.io ?? console;
|
|
488
|
+
const paths = options.paths ?? DEFAULT_BOOTSTRAP_PATHS;
|
|
489
|
+
const registerSignalHandlers = options.registerSignalHandlers ?? true;
|
|
490
|
+
const startServices = options.startServices ?? true;
|
|
491
|
+
sanitizeProxyEnv(env);
|
|
492
|
+
const parsedArgs = parseArgs(argv, paths, io);
|
|
493
|
+
const sandbox = parsedArgs.sandbox;
|
|
494
|
+
const bootstrapResult = bootstrapAppHome(paths);
|
|
495
|
+
printBootstrapSummary(bootstrapResult, io, paths);
|
|
496
|
+
if (bootstrapResult.channelTemplateCreated) {
|
|
497
|
+
io.error(`Fill in ${paths.channelConfigPath} and run \`${paths.appName}\` again.`);
|
|
498
|
+
throw new BootstrapExitError(1);
|
|
499
|
+
}
|
|
500
|
+
const dingtalkConfig = loadConfig(paths, io);
|
|
501
|
+
dingtalkConfig.stateDir = paths.workspaceDir;
|
|
502
|
+
await validateSandbox(sandbox);
|
|
503
|
+
log.logStartup(paths.workspaceDir, sandbox.type === "host" ? "host" : `docker:${sandbox.container}`);
|
|
504
|
+
const runtime = createRuntimeContext({
|
|
505
|
+
paths,
|
|
506
|
+
sandbox,
|
|
507
|
+
dingtalkConfig,
|
|
508
|
+
registerSignalHandlers,
|
|
509
|
+
startServices,
|
|
510
|
+
});
|
|
511
|
+
return {
|
|
512
|
+
bot: runtime.bot,
|
|
513
|
+
store: runtime.store,
|
|
468
514
|
shutdown: async () => {
|
|
469
|
-
await
|
|
515
|
+
await runtime.shutdown("manual");
|
|
470
516
|
},
|
|
471
517
|
};
|
|
472
518
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CommandGuardResult, SecurityConfig } from "./types.js";
|
|
2
|
+
interface ParsedCommand {
|
|
3
|
+
command: string;
|
|
4
|
+
args: string[];
|
|
5
|
+
normalized: string;
|
|
6
|
+
}
|
|
7
|
+
declare function splitCommandChain(command: string): string[];
|
|
8
|
+
declare function parseShellWords(command: string): string[];
|
|
9
|
+
declare function parseCommand(command: string): ParsedCommand | null;
|
|
10
|
+
export declare function guardCommand(command: string, config: SecurityConfig["commandGuard"]): CommandGuardResult;
|
|
11
|
+
export declare const internalCommandGuard: {
|
|
12
|
+
parseShellWords: typeof parseShellWords;
|
|
13
|
+
parseCommand: typeof parseCommand;
|
|
14
|
+
splitCommandChain: typeof splitCommandChain;
|
|
15
|
+
};
|
|
16
|
+
export {};
|