pi-shepherd 0.1.1 → 0.1.2
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.en.md +2 -0
- package/README.md +2 -0
- package/index.ts +1 -1
- package/package.json +1 -4
- package/rules.json +22 -206
- package/shepherd/index.ts +1 -0
- package/shepherd/rules-editor.ts +80 -0
- package/shepherd/rules-tool-helpers.ts +120 -0
- package/shepherd/rules-tool-list.ts +126 -0
- package/shepherd/rules-tool.ts +74 -31
- package/shepherd/rules.ts +15 -3
- package/shepherd/tool-hooks.ts +3 -2
- package/node_modules/@pi-atelier/shared-utils/README.en.md +0 -182
- package/node_modules/@pi-atelier/shared-utils/README.md +0 -182
- package/node_modules/@pi-atelier/shared-utils/package.json +0 -51
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/agents.test.ts +0 -120
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/ephemeral.test.ts +0 -100
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/file-lock.test.ts +0 -152
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/filter-match.test.ts +0 -187
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/memory-parser.test.ts +0 -170
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/paths.test.ts +0 -126
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/project-config-edge.test.ts +0 -138
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/project-config.test.ts +0 -257
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/project-tools-mcp.test.ts +0 -189
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/project-tools.test.ts +0 -204
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-backup-advanced.test.ts +0 -269
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-backup-array.test.ts +0 -267
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-backup.test.ts +0 -520
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-read.test.ts +0 -116
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-write.test.ts +0 -119
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/tool-output.test.ts +0 -145
- package/node_modules/@pi-atelier/shared-utils/src/agents.ts +0 -39
- package/node_modules/@pi-atelier/shared-utils/src/ephemeral.ts +0 -42
- package/node_modules/@pi-atelier/shared-utils/src/file-lock.ts +0 -62
- package/node_modules/@pi-atelier/shared-utils/src/filter-match.ts +0 -100
- package/node_modules/@pi-atelier/shared-utils/src/index.ts +0 -71
- package/node_modules/@pi-atelier/shared-utils/src/memory-parser.ts +0 -96
- package/node_modules/@pi-atelier/shared-utils/src/paths.ts +0 -23
- package/node_modules/@pi-atelier/shared-utils/src/project-config.ts +0 -241
- package/node_modules/@pi-atelier/shared-utils/src/project-tools.ts +0 -191
- package/node_modules/@pi-atelier/shared-utils/src/settings-array.ts +0 -73
- package/node_modules/@pi-atelier/shared-utils/src/settings-backup-rollback.ts +0 -104
- package/node_modules/@pi-atelier/shared-utils/src/settings-backup-utils.ts +0 -75
- package/node_modules/@pi-atelier/shared-utils/src/settings-backup.ts +0 -172
- package/node_modules/@pi-atelier/shared-utils/src/settings.ts +0 -104
- package/node_modules/@pi-atelier/shared-utils/src/tool-output.ts +0 -149
- package/node_modules/@pi-atelier/shared-utils/tsconfig.json +0 -9
- package/node_modules/@pi-atelier/shared-utils/vitest.config.ts +0 -24
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shepherd 规则工具 — list 输出格式化
|
|
3
|
+
*
|
|
4
|
+
* 处理 list 的两种输出模式:摘要模式和 verbose(完整 JSON)模式。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
getRuleDetail,
|
|
9
|
+
listRules,
|
|
10
|
+
listRulesDetail,
|
|
11
|
+
} from "./rules-editor";
|
|
12
|
+
import {
|
|
13
|
+
type Scope,
|
|
14
|
+
getRulesFilePath,
|
|
15
|
+
listRulesByScope,
|
|
16
|
+
} from "./rules-tool-helpers";
|
|
17
|
+
|
|
18
|
+
/** 构造 pi 工具 execute 的标准返回格式 */
|
|
19
|
+
export function textResult(text: string) {
|
|
20
|
+
return { content: [{ type: "text" as const, text }] };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const scopeLabel = (s: Scope) => (s === "global" ? "全局" : "项目级");
|
|
24
|
+
|
|
25
|
+
/** 按编号查看单条规则的完整 JSON */
|
|
26
|
+
export function handleListByIndex(
|
|
27
|
+
scope: Scope | undefined,
|
|
28
|
+
index: number,
|
|
29
|
+
rulesDir: string,
|
|
30
|
+
effectiveCwd: string,
|
|
31
|
+
) {
|
|
32
|
+
const effectiveScope = scope || "global";
|
|
33
|
+
const filePath = getRulesFilePath(effectiveScope, rulesDir, effectiveCwd);
|
|
34
|
+
const detail = getRuleDetail(filePath, index);
|
|
35
|
+
if ("error" in detail) return textResult(`❌ ${detail.error}`);
|
|
36
|
+
const { index: _, ...ruleData } = detail;
|
|
37
|
+
return textResult(
|
|
38
|
+
`📋 规则 [${effectiveScope}:${index}] 完整内容:\n${JSON.stringify(ruleData, null, "\t")}`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** verbose 模式:显示每条规则的完整 JSON */
|
|
43
|
+
export function handleListVerbose(
|
|
44
|
+
scope: Scope | undefined,
|
|
45
|
+
rulesDir: string,
|
|
46
|
+
effectiveCwd: string,
|
|
47
|
+
) {
|
|
48
|
+
if (!scope) {
|
|
49
|
+
// 全部 scope
|
|
50
|
+
const parts: string[] = [];
|
|
51
|
+
const globalPath = getRulesFilePath("global", rulesDir, effectiveCwd);
|
|
52
|
+
const projectPath = getRulesFilePath("project", rulesDir, effectiveCwd);
|
|
53
|
+
|
|
54
|
+
const gResult = listRulesDetail(globalPath);
|
|
55
|
+
if (gResult.rules.length > 0) {
|
|
56
|
+
parts.push("── 全局规则 ──");
|
|
57
|
+
for (const r of gResult.rules) {
|
|
58
|
+
const { index: _, ...data } = r;
|
|
59
|
+
parts.push(`[global:${r.index}] ${JSON.stringify(data, null, "\t")}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const pResult = listRulesDetail(projectPath);
|
|
63
|
+
if (pResult.rules.length > 0) {
|
|
64
|
+
parts.push("── 项目级规则 ──");
|
|
65
|
+
for (const r of pResult.rules) {
|
|
66
|
+
const { index: _, ...data } = r;
|
|
67
|
+
parts.push(`[project:${r.index}] ${JSON.stringify(data, null, "\t")}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (parts.length === 0) return textResult("暂无规则(全局和项目级均为空)。");
|
|
71
|
+
return textResult(parts.join("\n\n"));
|
|
72
|
+
}
|
|
73
|
+
// 指定 scope
|
|
74
|
+
const filePath = getRulesFilePath(scope, rulesDir, effectiveCwd);
|
|
75
|
+
const result = listRulesDetail(filePath);
|
|
76
|
+
if (result.error) return textResult(`❌ ${result.error}`);
|
|
77
|
+
if (result.count === 0) return textResult(`暂无${scopeLabel(scope)}规则。`);
|
|
78
|
+
return textResult(
|
|
79
|
+
result.rules
|
|
80
|
+
.map((r) => {
|
|
81
|
+
const { index: _, ...data } = r;
|
|
82
|
+
return `[${scope}:${r.index}] ${JSON.stringify(data, null, "\t")}`;
|
|
83
|
+
})
|
|
84
|
+
.join("\n\n"),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** 摘要模式:一行一条,只显示 comment + action + tool + hook */
|
|
89
|
+
export function handleListSummary(
|
|
90
|
+
scope: Scope | undefined,
|
|
91
|
+
rulesDir: string,
|
|
92
|
+
effectiveCwd: string,
|
|
93
|
+
) {
|
|
94
|
+
if (!scope) {
|
|
95
|
+
const items = listRulesByScope(rulesDir, effectiveCwd);
|
|
96
|
+
if (items.length === 0) return textResult("暂无规则(全局和项目级均为空)。");
|
|
97
|
+
return textResult(
|
|
98
|
+
items
|
|
99
|
+
.map(
|
|
100
|
+
(r) =>
|
|
101
|
+
`[${r.scope}:${r.index}] ${r.comment}` +
|
|
102
|
+
(r.enabled === false ? " (disabled)" : "") +
|
|
103
|
+
(r.action ? ` — ${r.action}` : "") +
|
|
104
|
+
(r.tool ? ` on ${r.tool}` : "") +
|
|
105
|
+
(r.hook ? ` @ ${r.hook}` : ""),
|
|
106
|
+
)
|
|
107
|
+
.join("\n"),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
const filePath = getRulesFilePath(scope, rulesDir, effectiveCwd);
|
|
111
|
+
const result = listRules(filePath);
|
|
112
|
+
if (result.error) return textResult(`❌ ${result.error}`);
|
|
113
|
+
if (result.count === 0) return textResult(`暂无${scopeLabel(scope)}规则。`);
|
|
114
|
+
return textResult(
|
|
115
|
+
result.rules
|
|
116
|
+
.map(
|
|
117
|
+
(r) =>
|
|
118
|
+
`[${scope}:${r.index}] ${r.comment}` +
|
|
119
|
+
(r.enabled === false ? " (disabled)" : "") +
|
|
120
|
+
(r.action ? ` — ${r.action}` : "") +
|
|
121
|
+
(r.tool ? ` on ${r.tool}` : "") +
|
|
122
|
+
(r.hook ? ` @ ${r.hook}` : ""),
|
|
123
|
+
)
|
|
124
|
+
.join("\n"),
|
|
125
|
+
);
|
|
126
|
+
}
|
package/shepherd/rules-tool.ts
CHANGED
|
@@ -2,23 +2,37 @@
|
|
|
2
2
|
* Shepherd 规则编辑工具注册
|
|
3
3
|
*
|
|
4
4
|
* 注册 shepherd_rules 工具到 pi,提供规则文件的安全增删改查。
|
|
5
|
+
* 支持 scope 参数区分全局/项目级规则。
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
|
-
import { addRule, deleteRule,
|
|
9
|
+
import { addRule, deleteRule, updateRule } from "./rules-editor";
|
|
10
|
+
import {
|
|
11
|
+
type Scope,
|
|
12
|
+
checkCrossScopeDuplicate,
|
|
13
|
+
ensureProjectDir,
|
|
14
|
+
getRulesFilePath,
|
|
15
|
+
} from "./rules-tool-helpers";
|
|
16
|
+
import {
|
|
17
|
+
handleListByIndex,
|
|
18
|
+
handleListSummary,
|
|
19
|
+
handleListVerbose,
|
|
20
|
+
scopeLabel,
|
|
21
|
+
textResult,
|
|
22
|
+
} from "./rules-tool-list";
|
|
9
23
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return { content: [{ type: "text" as const, text }] };
|
|
13
|
-
}
|
|
24
|
+
export function registerRulesEditorTool(pi: ExtensionAPI, rulesDir: string, cwd?: string) {
|
|
25
|
+
const effectiveCwd = cwd || process.cwd();
|
|
14
26
|
|
|
15
|
-
export function registerRulesEditorTool(pi: ExtensionAPI, rulesFilePath: string) {
|
|
16
27
|
pi.registerTool({
|
|
17
28
|
name: "shepherd_rules",
|
|
18
29
|
label: "Shepherd Rules Editor",
|
|
19
30
|
description:
|
|
20
31
|
"安全编辑 shepherd 规则文件。支持 list(列出所有规则)、add(添加规则)、update(部分更新规则)、delete(删除规则)。" +
|
|
21
|
-
"
|
|
32
|
+
"scope='global' 操作全局规则 (~/.pi/agent/extensions/shepherd/rules.json);" +
|
|
33
|
+
"scope='project' 操作当前项目规则 (<cwd>/.pi/extensions/shepherd-rules.json)。" +
|
|
34
|
+
"写入前自动校验必填字段和正则合法性,写入后回读验证,失败自动从备份恢复。" +
|
|
35
|
+
"同签名规则(tool+hook+pattern/check+action)自动覆盖而非追加。",
|
|
22
36
|
parameters: {
|
|
23
37
|
type: "object",
|
|
24
38
|
properties: {
|
|
@@ -27,13 +41,25 @@ export function registerRulesEditorTool(pi: ExtensionAPI, rulesFilePath: string)
|
|
|
27
41
|
enum: ["list", "add", "update", "delete"],
|
|
28
42
|
description: "操作类型",
|
|
29
43
|
},
|
|
44
|
+
scope: {
|
|
45
|
+
type: "string",
|
|
46
|
+
enum: ["global", "project"],
|
|
47
|
+
description:
|
|
48
|
+
"操作目标:global=全局规则(默认),project=当前项目规则。list 不传 scope 时返回全局+项目合并列表(标注来源),写操作默认 global。",
|
|
49
|
+
},
|
|
30
50
|
rule: {
|
|
31
51
|
type: "object",
|
|
32
52
|
description: "add 时传入的完整规则对象(必须含 comment 和 reason)",
|
|
33
53
|
},
|
|
34
54
|
index: {
|
|
35
55
|
type: "number",
|
|
36
|
-
description:
|
|
56
|
+
description:
|
|
57
|
+
"规则编号(0-based,仅在对应 scope 文件内的索引)。list 时传 index 显示该条规则的完整 JSON;update/delete 时指定要操作的规则。",
|
|
58
|
+
},
|
|
59
|
+
verbose: {
|
|
60
|
+
type: "boolean",
|
|
61
|
+
description:
|
|
62
|
+
"list 时传 true 显示每条规则的完整字段(含 reason、conditions、pattern 等),默认 false 只显示摘要。",
|
|
37
63
|
},
|
|
38
64
|
changes: {
|
|
39
65
|
type: "object",
|
|
@@ -46,49 +72,66 @@ export function registerRulesEditorTool(pi: ExtensionAPI, rulesFilePath: string)
|
|
|
46
72
|
_toolCallId: string,
|
|
47
73
|
params: {
|
|
48
74
|
action: "list" | "add" | "update" | "delete";
|
|
75
|
+
scope?: Scope;
|
|
49
76
|
rule?: Record<string, unknown>;
|
|
50
77
|
index?: number;
|
|
78
|
+
verbose?: boolean;
|
|
51
79
|
changes?: Record<string, unknown>;
|
|
52
80
|
},
|
|
53
81
|
) {
|
|
82
|
+
const scope = params.scope;
|
|
83
|
+
|
|
54
84
|
switch (params.action) {
|
|
55
85
|
case "list": {
|
|
56
|
-
|
|
57
|
-
if (
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
(r.tool ? ` on ${r.tool}` : "") +
|
|
67
|
-
(r.hook ? ` @ ${r.hook}` : ""),
|
|
68
|
-
)
|
|
69
|
-
.join("\n"),
|
|
70
|
-
);
|
|
86
|
+
// index 指定 → 显示单条完整 JSON
|
|
87
|
+
if (params.index !== undefined) {
|
|
88
|
+
return handleListByIndex(scope, params.index, rulesDir, effectiveCwd);
|
|
89
|
+
}
|
|
90
|
+
// verbose=true → 显示所有规则的完整信息
|
|
91
|
+
if (params.verbose === true) {
|
|
92
|
+
return handleListVerbose(scope, rulesDir, effectiveCwd);
|
|
93
|
+
}
|
|
94
|
+
// 默认摘要模式
|
|
95
|
+
return handleListSummary(scope, rulesDir, effectiveCwd);
|
|
71
96
|
}
|
|
72
97
|
case "add": {
|
|
73
98
|
if (!params.rule) return textResult("❌ add 需要 rule 参数");
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
99
|
+
const targetScope = scope || "global";
|
|
100
|
+
const filePath = getRulesFilePath(targetScope, rulesDir, effectiveCwd);
|
|
101
|
+
if (targetScope === "project") ensureProjectDir(effectiveCwd);
|
|
102
|
+
const result = addRule(filePath, params.rule);
|
|
103
|
+
if (!result.success) return textResult(`❌ ${result.error}`);
|
|
104
|
+
const warning = checkCrossScopeDuplicate(
|
|
105
|
+
targetScope,
|
|
106
|
+
rulesDir,
|
|
107
|
+
effectiveCwd,
|
|
108
|
+
params.rule,
|
|
109
|
+
);
|
|
110
|
+
const overwrittenMsg = result.overwritten ? " (覆盖已有同签名规则)" : "";
|
|
111
|
+
const warningMsg = warning ? `\n${warning}` : "";
|
|
112
|
+
return textResult(
|
|
113
|
+
`✅ ${scopeLabel(targetScope)}规则已添加 [${targetScope}:${result.index}]${overwrittenMsg}${warningMsg}`,
|
|
114
|
+
);
|
|
78
115
|
}
|
|
79
116
|
case "update": {
|
|
80
117
|
if (params.index === undefined) return textResult("❌ update 需要 index 参数");
|
|
81
118
|
if (!params.changes) return textResult("❌ update 需要 changes 参数");
|
|
82
|
-
const
|
|
119
|
+
const targetScope = scope || "global";
|
|
120
|
+
const filePath = getRulesFilePath(targetScope, rulesDir, effectiveCwd);
|
|
121
|
+
const result = updateRule(filePath, params.index, params.changes);
|
|
83
122
|
return result.success
|
|
84
|
-
? textResult(`✅ 规则 [${params.index}] 已更新`)
|
|
123
|
+
? textResult(`✅ ${scopeLabel(targetScope)}规则 [${targetScope}:${params.index}] 已更新`)
|
|
85
124
|
: textResult(`❌ ${result.error}`);
|
|
86
125
|
}
|
|
87
126
|
case "delete": {
|
|
88
127
|
if (params.index === undefined) return textResult("❌ delete 需要 index 参数");
|
|
89
|
-
const
|
|
128
|
+
const targetScope = scope || "global";
|
|
129
|
+
const filePath = getRulesFilePath(targetScope, rulesDir, effectiveCwd);
|
|
130
|
+
const result = deleteRule(filePath, params.index);
|
|
90
131
|
return result.success
|
|
91
|
-
? textResult(
|
|
132
|
+
? textResult(
|
|
133
|
+
`✅ ${scopeLabel(targetScope)}规则已删除: ${(result.deleted as any)?.comment || ""}`,
|
|
134
|
+
)
|
|
92
135
|
: textResult(`❌ ${result.error}`);
|
|
93
136
|
}
|
|
94
137
|
default:
|
package/shepherd/rules.ts
CHANGED
|
@@ -21,7 +21,7 @@ export interface Condition {
|
|
|
21
21
|
export interface Rule {
|
|
22
22
|
comment: string;
|
|
23
23
|
hook?: "tool_call" | "tool_result" | "agent_end" | "session_shutdown"; // 默认 "tool_call"
|
|
24
|
-
tool?: string; // 默认 "bash"
|
|
24
|
+
tool?: string; // 默认 "bash",支持 "|" 分隔多值匹配(如 "edit|write")
|
|
25
25
|
// 单条件模式(向后兼容):pattern 匹配 command(bash)或 path(edit/write)
|
|
26
26
|
pattern?: string;
|
|
27
27
|
flags?: string;
|
|
@@ -177,11 +177,11 @@ export function loadRules(
|
|
|
177
177
|
if (result.error) errors.push(result.error);
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
// 2. 项目级规则(<cwd>/.pi/extensions/{prefix}*.json)
|
|
180
|
+
// 2. 项目级规则(<cwd>/.pi/extensions/{prefix}*.json 或 shepherd-rules.json)
|
|
181
181
|
const projectExtDir = path.join(process.cwd(), ".pi", "extensions");
|
|
182
182
|
if (fs.existsSync(projectExtDir)) {
|
|
183
183
|
for (const file of fs.readdirSync(projectExtDir).sort()) {
|
|
184
|
-
if (file.
|
|
184
|
+
if (file.endsWith(".json") && (file.startsWith(prefix) || file === "shepherd-rules.json")) {
|
|
185
185
|
const result = loadRulesFromFile(path.join(projectExtDir, file));
|
|
186
186
|
allRules.push(...result.rules);
|
|
187
187
|
if (result.error) errors.push(result.error);
|
|
@@ -247,6 +247,12 @@ export function getMatchTargets(
|
|
|
247
247
|
}
|
|
248
248
|
} else if (tool === "write") {
|
|
249
249
|
text = (event.input as any)?.content || "";
|
|
250
|
+
} else {
|
|
251
|
+
// 其他工具:把所有参数序列化为 text,供 conditions 的 text field 匹配
|
|
252
|
+
const input = event.input as any;
|
|
253
|
+
if (input && typeof input === "object") {
|
|
254
|
+
text = JSON.stringify(input);
|
|
255
|
+
}
|
|
250
256
|
}
|
|
251
257
|
return { path: pathVal, text, command: "", glob: "" };
|
|
252
258
|
}
|
|
@@ -272,6 +278,12 @@ export function ruleMatches(
|
|
|
272
278
|
return false;
|
|
273
279
|
}
|
|
274
280
|
|
|
281
|
+
/** tool 字段匹配:支持 "|" 分隔的多值(如 "edit|write") */
|
|
282
|
+
export function toolMatches(ruleTool: string | undefined, eventTool: string): boolean {
|
|
283
|
+
if (!ruleTool) return true; // 未指定 tool 时默认匹配所有(由 hook 类型决定范围)
|
|
284
|
+
return ruleTool.split("|").map((t) => t.trim()).includes(eventTool);
|
|
285
|
+
}
|
|
286
|
+
|
|
275
287
|
/** rtk 可用性(模块加载时检测) */
|
|
276
288
|
export const isRtkAvailable: boolean = (() => {
|
|
277
289
|
try {
|
package/shepherd/tool-hooks.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
loadRules,
|
|
21
21
|
type Rule,
|
|
22
22
|
ruleMatches,
|
|
23
|
+
toolMatches,
|
|
23
24
|
} from "./rules.js";
|
|
24
25
|
import type { ResettableRule, StateTracker } from "./state-tracker.js";
|
|
25
26
|
|
|
@@ -63,7 +64,7 @@ export function registerToolCall(
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
const rules = loadRules(rulesDir, rulesOptions).filter(
|
|
66
|
-
(r) => r.hook === "tool_call" && r.tool
|
|
67
|
+
(r) => r.hook === "tool_call" && toolMatches(r.tool, event.toolName!),
|
|
67
68
|
);
|
|
68
69
|
if (rules.length === 0) return;
|
|
69
70
|
|
|
@@ -139,7 +140,7 @@ export function registerToolResult(
|
|
|
139
140
|
if (isSubagent() && rule.subagent === false) continue;
|
|
140
141
|
if (!toolsAvailable(rule, pi, state)) continue;
|
|
141
142
|
if (rule.requireSuccess && event.isError) continue;
|
|
142
|
-
if (rule.tool && rule.tool
|
|
143
|
+
if (rule.tool && !toolMatches(rule.tool, event.toolName!)) continue;
|
|
143
144
|
|
|
144
145
|
// 正则条件匹配
|
|
145
146
|
if (rule.conditions || rule.pattern) {
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
[中文文档](README.md) | English
|
|
2
|
-
|
|
3
|
-
# pi-shared-utils
|
|
4
|
-
|
|
5
|
-
Shared utility library for the [pi](https://github.com/earendil-works/pi-coding-agent) extension ecosystem — memory file parsing, path constants, settings management, tool output truncation, and more. Used by 7+ pi extensions.
|
|
6
|
-
|
|
7
|
-
## Why You Need It
|
|
8
|
-
|
|
9
|
-
If you're building a pi extension, you'll inevitably need the same building blocks: reading settings, parsing memory files, truncating tool output, finding agent directories. pi-shared-utils provides these as a single dependency so every extension doesn't reinvent the wheel.
|
|
10
|
-
|
|
11
|
-
**Used by**: pi-memory, pi-context, pi-shepherd, pi-roadmap, pi-session-analyzer, pi-workflow, and more.
|
|
12
|
-
|
|
13
|
-
## How It Works
|
|
14
|
-
|
|
15
|
-
```
|
|
16
|
-
pi-shared-utils provides 6 independent modules:
|
|
17
|
-
|
|
18
|
-
┌─────────────────────────────────────────────────┐
|
|
19
|
-
│ memory-parser ── parse topic--kw1,kw2.md file names
|
|
20
|
-
│ paths ── standard pi agent path constants
|
|
21
|
-
│ settings ── read/write extension config sections in settings.json
|
|
22
|
-
│ tool-output ── truncate tool output (prevent context overflow)
|
|
23
|
-
│ agents ── discover sub-agent definition files
|
|
24
|
-
│ ephemeral ── session-scoped hint/label stack
|
|
25
|
-
└─────────────────────────────────────────────────┘
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
Each module is independently importable — use only what you need.
|
|
29
|
-
|
|
30
|
-
## Installation
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
pi install git:github.com/catlain/pi-atelier
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
> This is a workspace package inside the pi-atelier monorepo and typically doesn't need to be installed standalone. Other independent extensions include it automatically via `bundledDependencies`.
|
|
37
|
-
|
|
38
|
-
## Exported Modules
|
|
39
|
-
|
|
40
|
-
### Memory File Parsing (`memory-parser`)
|
|
41
|
-
|
|
42
|
-
Parses `topic--kw1,kw2,kw3.md`-format memory file names and scans directories to generate an index.
|
|
43
|
-
|
|
44
|
-
```ts
|
|
45
|
-
import { parseFileName, buildFileName, scanMemoryDir } from "@pi-atelier/shared-utils";
|
|
46
|
-
|
|
47
|
-
// Parse file name → { topic, keywords }
|
|
48
|
-
const { topic, keywords } = parseFileName("coding_standards--编码,git,lint.md");
|
|
49
|
-
// topic = "coding_standards", keywords = ["编码", "git", "lint"]
|
|
50
|
-
|
|
51
|
-
// Build file name from parts
|
|
52
|
-
const name = buildFileName("coding_standards", ["编码", "git", "lint"]);
|
|
53
|
-
// "coding_standards--编码,git,lint.md"
|
|
54
|
-
|
|
55
|
-
// Scan directory, returns MemoryEntry[]
|
|
56
|
-
const entries = await scanMemoryDir("/path/to/memory");
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
### Path Constants (`paths`)
|
|
60
|
-
|
|
61
|
-
Standard pi agent paths, so you never hardcode them.
|
|
62
|
-
|
|
63
|
-
| Constant | Path | Description |
|
|
64
|
-
|------|------|------|
|
|
65
|
-
| `AGENT_DIR` | `~/.pi/agent/` | Agent root directory |
|
|
66
|
-
| `SETTINGS_PATH` | `~/.pi/agent/settings.json` | Global settings |
|
|
67
|
-
| `MODELS_CONFIG_PATH` | `~/.pi/agent/models.json` | Model configuration |
|
|
68
|
-
| `MCP_CONFIG_PATH` | `~/.pi/agent/mcp.json` | MCP server configuration |
|
|
69
|
-
| `MCP_CACHE_PATH` | `~/.pi/agent/mcp-cache/` | MCP tool cache |
|
|
70
|
-
| `AGENTS_DIR` | `~/.pi/agent/agents/` | Sub-agent definitions |
|
|
71
|
-
| `GLOBAL_RULES_PATH` | `~/.pi/agent/rules.md` | Global rules |
|
|
72
|
-
| `MEMORY_DIR` | `~/.pi/agent/memory/` | Global memory |
|
|
73
|
-
| `MEMORY_MD_PATH` | `MEMORY.md` | Memory index file name |
|
|
74
|
-
|
|
75
|
-
### Settings Management (`settings`)
|
|
76
|
-
|
|
77
|
-
Read and write extension-specific config sections in `settings.json`.
|
|
78
|
-
|
|
79
|
-
```ts
|
|
80
|
-
import { getSettingsSection, patchSettingsSection, getSettingsValue, setSettingsValue } from "@pi-atelier/shared-utils";
|
|
81
|
-
|
|
82
|
-
// Read extension config section
|
|
83
|
-
const config = await getSettingsSection("my-extension");
|
|
84
|
-
|
|
85
|
-
// Update config incrementally
|
|
86
|
-
await patchSettingsSection("my-extension", { enabled: true });
|
|
87
|
-
|
|
88
|
-
// Read/write a single value
|
|
89
|
-
const val = await getSettingsValue("my-extension", "key", "default");
|
|
90
|
-
await setSettingsValue("my-extension", "key", "new-value");
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
### Tool Output Truncation (`tool-output`)
|
|
94
|
-
|
|
95
|
-
Prevent large tool results from overflowing the LLM context.
|
|
96
|
-
|
|
97
|
-
```ts
|
|
98
|
-
import { truncateToolOutput, truncatedResult, TOOL_OUTPUT_MAX_LINES } from "@pi-atelier/shared-utils";
|
|
99
|
-
|
|
100
|
-
// Truncate overly long output
|
|
101
|
-
const result = truncateToolOutput(longText, { maxLines: 200 });
|
|
102
|
-
// { text: "...", truncated: true, originalLines: 1500, keptLines: 200 }
|
|
103
|
-
|
|
104
|
-
// Shortcut: returns pi tool result format
|
|
105
|
-
return truncatedResult(text); // auto-truncates + returns { content: [{ type: "text", text }] }
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
### Sub-Agent Discovery (`agents`)
|
|
109
|
-
|
|
110
|
-
Scan the `~/.pi/agent/agents/` directory for sub-agent definition files.
|
|
111
|
-
|
|
112
|
-
```ts
|
|
113
|
-
import { discoverAgents, getAgentDescription, formatAgentsList } from "@pi-atelier/shared-utils";
|
|
114
|
-
|
|
115
|
-
// Discover all available sub-agents
|
|
116
|
-
const agents = await discoverAgents();
|
|
117
|
-
// [{ name: "pv-executor", description: "...", filePath: "..." }, ...]
|
|
118
|
-
|
|
119
|
-
// Get description for a single agent
|
|
120
|
-
const desc = await getAgentDescription("pv-executor");
|
|
121
|
-
|
|
122
|
-
// Format as a readable list
|
|
123
|
-
const list = formatAgentsList(agents);
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
### Session-Scoped Data (`ephemeral`)
|
|
127
|
-
|
|
128
|
-
A hint/label stack for the current session that vanishes when the session ends. Useful for lightweight state passing across tool calls.
|
|
129
|
-
|
|
130
|
-
```ts
|
|
131
|
-
import { pushHint, hasHints, peekHints, drainHints, peekLabels } from "@pi-atelier/shared-utils";
|
|
132
|
-
|
|
133
|
-
pushHint({ key: "recent-files", values: ["file1.ts", "file2.ts"] });
|
|
134
|
-
const has = hasHints("recent-files");
|
|
135
|
-
const hints = peekHints("recent-files"); // peek without removing
|
|
136
|
-
const all = drainHints(); // retrieve and clear
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
## Best Practices
|
|
140
|
-
|
|
141
|
-
### ✅ Recommended
|
|
142
|
-
- Import only the modules you need to keep bundle size small
|
|
143
|
-
- Use `truncatedResult()` for all tool outputs — prevents context overflow
|
|
144
|
-
- Use `paths` constants instead of hardcoding `~/.pi/agent/...`
|
|
145
|
-
- Use `settings` module for any persistent configuration
|
|
146
|
-
|
|
147
|
-
### ❌ Not Recommended
|
|
148
|
-
- Don't hardcode pi paths — they may change between versions
|
|
149
|
-
- Don't return raw tool output without truncation
|
|
150
|
-
- Don't use `ephemeral` for persistent data — it's session-scoped only
|
|
151
|
-
|
|
152
|
-
## Limitations
|
|
153
|
-
|
|
154
|
-
| Limitation | Detail |
|
|
155
|
-
|------------|--------|
|
|
156
|
-
| Memory file format only | Only supports `topic--kw1,kw2.md` naming convention |
|
|
157
|
-
| No validation | Settings reads don't validate schema — caller must handle |
|
|
158
|
-
| Ephemeral is in-memory | Lost on process restart, not persisted to disk |
|
|
159
|
-
| Token estimation | `tool-output` truncates by lines, not by token count |
|
|
160
|
-
|
|
161
|
-
## Architecture
|
|
162
|
-
|
|
163
|
-
```
|
|
164
|
-
pi-shared-utils/
|
|
165
|
-
├── src/
|
|
166
|
-
│ ├── index.ts # Re-exports all modules
|
|
167
|
-
│ ├── memory-parser.ts # Memory file name parsing + directory scanning
|
|
168
|
-
│ ├── paths.ts # Path constants (AGENT_DIR, SETTINGS_PATH, ...)
|
|
169
|
-
│ ├── settings.ts # settings.json section read/write
|
|
170
|
-
│ ├── tool-output.ts # Output truncation + truncatedResult helper
|
|
171
|
-
│ ├── agents.ts # Sub-agent discovery from ~/.pi/agent/agents/
|
|
172
|
-
│ ├── ephemeral.ts # Session-scoped hint/label stack
|
|
173
|
-
│ └── __tests__/ # Unit tests
|
|
174
|
-
├── package.json
|
|
175
|
-
└── tsconfig.json
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
**Dependencies**: Zero runtime dependencies (pure Node.js).
|
|
179
|
-
|
|
180
|
-
## License
|
|
181
|
-
|
|
182
|
-
MIT
|