pi-shepherd 0.1.2 → 0.2.0
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 +205 -84
- package/README.md +207 -88
- package/index.ts +262 -229
- package/package.json +60 -55
- package/rules.json +196 -33
- package/shepherd/compaction.ts +76 -0
- package/shepherd/conditions.ts +98 -0
- package/shepherd/ephemeral.ts +55 -52
- package/shepherd/git.ts +64 -0
- package/shepherd/index.ts +13 -0
- package/shepherd/line-count.ts +86 -86
- package/shepherd/message-end.ts +120 -0
- package/shepherd/rules-editor.ts +250 -215
- package/shepherd/rules-tool-helpers.ts +119 -120
- package/shepherd/rules-tool-list.ts +124 -126
- package/shepherd/rules-tool.ts +182 -142
- package/shepherd/rules-validate.ts +89 -44
- package/shepherd/rules.ts +366 -295
- package/shepherd/tool-event-types.ts +27 -14
- package/shepherd/tool-hooks.ts +165 -177
- package/shepherd/worktree-check.ts +130 -130
- package/tsconfig.json +21 -14
package/shepherd/rules-tool.ts
CHANGED
|
@@ -1,142 +1,182 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shepherd 规则编辑工具注册
|
|
3
|
-
*
|
|
4
|
-
* 注册 shepherd_rules 工具到 pi,提供规则文件的安全增删改查。
|
|
5
|
-
* 支持 scope 参数区分全局/项目级规则。
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
9
|
-
import { addRule, deleteRule, updateRule } from "./rules-editor";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
checkCrossScopeDuplicate,
|
|
13
|
-
ensureProjectDir,
|
|
14
|
-
getRulesFilePath,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
description:
|
|
57
|
-
"
|
|
58
|
-
},
|
|
59
|
-
|
|
60
|
-
type: "
|
|
61
|
-
description:
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
},
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Shepherd 规则编辑工具注册
|
|
3
|
+
*
|
|
4
|
+
* 注册 shepherd_rules 工具到 pi,提供规则文件的安全增删改查。
|
|
5
|
+
* 支持 scope 参数区分全局/项目级规则。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { addRule, deleteRule, updateRule } from "./rules-editor";
|
|
10
|
+
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import {
|
|
12
|
+
checkCrossScopeDuplicate,
|
|
13
|
+
ensureProjectDir,
|
|
14
|
+
getRulesFilePath,
|
|
15
|
+
type Scope,
|
|
16
|
+
} from "./rules-tool-helpers";
|
|
17
|
+
import {
|
|
18
|
+
handleListByIndex,
|
|
19
|
+
handleListSummary,
|
|
20
|
+
handleListVerbose,
|
|
21
|
+
scopeLabel,
|
|
22
|
+
textResult,
|
|
23
|
+
} from "./rules-tool-list";
|
|
24
|
+
|
|
25
|
+
export function registerRulesEditorTool(
|
|
26
|
+
pi: ExtensionAPI,
|
|
27
|
+
rulesDir: string,
|
|
28
|
+
cwd?: string,
|
|
29
|
+
) {
|
|
30
|
+
const effectiveCwd = cwd || process.cwd();
|
|
31
|
+
|
|
32
|
+
pi.registerTool({
|
|
33
|
+
name: "shepherd_rules",
|
|
34
|
+
label: "Shepherd Rules Editor",
|
|
35
|
+
description:
|
|
36
|
+
"安全编辑 shepherd 规则文件。支持 list(列出所有规则)、add(添加规则)、update(部分更新规则)、delete(删除规则)。" +
|
|
37
|
+
"scope='global' 操作全局规则 (~/.pi/agent/extensions/shepherd/rules.json);" +
|
|
38
|
+
"scope='project' 操作当前项目规则 (<cwd>/.pi/extensions/shepherd-rules.json)。" +
|
|
39
|
+
"写入前自动校验必填字段和正则合法性,写入后回读验证,失败自动从备份恢复。" +
|
|
40
|
+
"同签名规则(tool+hook+pattern/check+action)自动覆盖而非追加。" +
|
|
41
|
+
"\n\n⚠️ 关闭/禁用规则:action 是规则匹配后的执行动作(block/notify/rewrite/steer),不能用来关闭规则。" +
|
|
42
|
+
"想临时关掉某条提示请用 update(index=N, changes={enabled:false})(保留规则,以后改 enabled:true 恢复);" +
|
|
43
|
+
"彻底删除用 delete(index=N)。操作前先 list(verbose=true) 找到目标规则 index。",
|
|
44
|
+
promptSnippet: "编辑 shepherd 防护规则",
|
|
45
|
+
parameters: {
|
|
46
|
+
type: "object",
|
|
47
|
+
properties: {
|
|
48
|
+
action: {
|
|
49
|
+
type: "string",
|
|
50
|
+
enum: ["list", "add", "update", "delete"],
|
|
51
|
+
description: "操作类型",
|
|
52
|
+
},
|
|
53
|
+
scope: {
|
|
54
|
+
type: "string",
|
|
55
|
+
enum: ["global", "project"],
|
|
56
|
+
description:
|
|
57
|
+
"操作目标:global=全局规则(默认),project=当前项目规则。list 不传 scope 时返回全局+项目合并列表(标注来源),写操作默认 global。",
|
|
58
|
+
},
|
|
59
|
+
rule: {
|
|
60
|
+
type: "object",
|
|
61
|
+
description: "add 时传入的完整规则对象(必须含 comment 和 reason)",
|
|
62
|
+
},
|
|
63
|
+
index: {
|
|
64
|
+
type: "number",
|
|
65
|
+
description:
|
|
66
|
+
"规则编号(0-based,仅在对应 scope 文件内的索引)。list 时传 index 显示该条规则的完整 JSON;update/delete 时指定要操作的规则。",
|
|
67
|
+
},
|
|
68
|
+
verbose: {
|
|
69
|
+
type: "boolean",
|
|
70
|
+
description:
|
|
71
|
+
"list 时传 true 显示每条规则的完整字段(含 reason、conditions、pattern 等),默认 false 只显示摘要。",
|
|
72
|
+
},
|
|
73
|
+
changes: {
|
|
74
|
+
type: "object",
|
|
75
|
+
description: "update 时要修改的字段(只传需要改的)",
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
required: ["action"],
|
|
79
|
+
},
|
|
80
|
+
// 注:SDK 的 registerTool 用 TypeBox Static<TParams> 推断 params 类型,
|
|
81
|
+
// 但 shepherd 用手写 JSON schema,泛型退化为无意义类型(TS2322)。
|
|
82
|
+
// 此处 execute params 用宽松类型(SDK 设计层面的逃生舱),
|
|
83
|
+
// 内部用类型断言取精确值,any 不扩散到业务逻辑。
|
|
84
|
+
async execute(
|
|
85
|
+
_toolCallId: string,
|
|
86
|
+
rawParams: Record<string, unknown>,
|
|
87
|
+
_signal?: AbortSignal,
|
|
88
|
+
_onUpdate?: unknown,
|
|
89
|
+
_ctx?: unknown,
|
|
90
|
+
) {
|
|
91
|
+
const params = rawParams as {
|
|
92
|
+
action: "list" | "add" | "update" | "delete";
|
|
93
|
+
scope?: Scope;
|
|
94
|
+
rule?: Record<string, unknown>;
|
|
95
|
+
index?: number;
|
|
96
|
+
verbose?: boolean;
|
|
97
|
+
changes?: Record<string, unknown>;
|
|
98
|
+
};
|
|
99
|
+
const scope = params.scope;
|
|
100
|
+
|
|
101
|
+
switch (params.action) {
|
|
102
|
+
case "list": {
|
|
103
|
+
// index 指定 → 显示单条完整 JSON
|
|
104
|
+
if (params.index !== undefined) {
|
|
105
|
+
return handleListByIndex(
|
|
106
|
+
scope,
|
|
107
|
+
params.index,
|
|
108
|
+
rulesDir,
|
|
109
|
+
effectiveCwd,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
// verbose=true → 显示所有规则的完整信息
|
|
113
|
+
if (params.verbose === true) {
|
|
114
|
+
return handleListVerbose(scope, rulesDir, effectiveCwd);
|
|
115
|
+
}
|
|
116
|
+
// 默认摘要模式
|
|
117
|
+
return handleListSummary(scope, rulesDir, effectiveCwd);
|
|
118
|
+
}
|
|
119
|
+
case "add": {
|
|
120
|
+
if (!params.rule) return textResult("❌ add 需要 rule 参数");
|
|
121
|
+
const targetScope = scope || "global";
|
|
122
|
+
const filePath = getRulesFilePath(
|
|
123
|
+
targetScope,
|
|
124
|
+
rulesDir,
|
|
125
|
+
effectiveCwd,
|
|
126
|
+
);
|
|
127
|
+
if (targetScope === "project") ensureProjectDir(effectiveCwd);
|
|
128
|
+
const result = addRule(filePath, params.rule);
|
|
129
|
+
if (!result.success) return textResult(`❌ ${result.error}`);
|
|
130
|
+
const warning = checkCrossScopeDuplicate(
|
|
131
|
+
targetScope,
|
|
132
|
+
rulesDir,
|
|
133
|
+
effectiveCwd,
|
|
134
|
+
params.rule,
|
|
135
|
+
);
|
|
136
|
+
const overwrittenMsg = result.overwritten
|
|
137
|
+
? " (覆盖已有同签名规则)"
|
|
138
|
+
: "";
|
|
139
|
+
const warningMsg = warning ? `\n${warning}` : "";
|
|
140
|
+
return textResult(
|
|
141
|
+
`✅ ${scopeLabel(targetScope)}规则已添加 [${targetScope}:${result.index}]${overwrittenMsg}${warningMsg}`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
case "update": {
|
|
145
|
+
if (params.index === undefined)
|
|
146
|
+
return textResult("❌ update 需要 index 参数");
|
|
147
|
+
if (!params.changes) return textResult("❌ update 需要 changes 参数");
|
|
148
|
+
const targetScope = scope || "global";
|
|
149
|
+
const filePath = getRulesFilePath(
|
|
150
|
+
targetScope,
|
|
151
|
+
rulesDir,
|
|
152
|
+
effectiveCwd,
|
|
153
|
+
);
|
|
154
|
+
const result = updateRule(filePath, params.index, params.changes);
|
|
155
|
+
return result.success
|
|
156
|
+
? textResult(
|
|
157
|
+
`✅ ${scopeLabel(targetScope)}规则 [${targetScope}:${params.index}] 已更新`,
|
|
158
|
+
)
|
|
159
|
+
: textResult(`❌ ${result.error}`);
|
|
160
|
+
}
|
|
161
|
+
case "delete": {
|
|
162
|
+
if (params.index === undefined)
|
|
163
|
+
return textResult("❌ delete 需要 index 参数");
|
|
164
|
+
const targetScope = scope || "global";
|
|
165
|
+
const filePath = getRulesFilePath(
|
|
166
|
+
targetScope,
|
|
167
|
+
rulesDir,
|
|
168
|
+
effectiveCwd,
|
|
169
|
+
);
|
|
170
|
+
const result = deleteRule(filePath, params.index);
|
|
171
|
+
return result.success
|
|
172
|
+
? textResult(
|
|
173
|
+
`✅ ${scopeLabel(targetScope)}规则已删除: ${(result.deleted as any)?.comment || ""}`,
|
|
174
|
+
)
|
|
175
|
+
: textResult(`❌ ${result.error}`);
|
|
176
|
+
}
|
|
177
|
+
default:
|
|
178
|
+
return textResult(`❌ 未知操作: ${(params as any).action}`);
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
} as ToolDefinition<any, any, any>);
|
|
182
|
+
}
|
|
@@ -1,44 +1,89 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shepherd 规则校验逻辑
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
const VALID_ACTIONS = ["block", "notify", "rewrite", "steer"] as const;
|
|
6
|
-
const VALID_HOOKS = [
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Shepherd 规则校验逻辑
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const VALID_ACTIONS = ["block", "notify", "rewrite", "steer"] as const;
|
|
6
|
+
const VALID_HOOKS = [
|
|
7
|
+
"tool_call",
|
|
8
|
+
"tool_result",
|
|
9
|
+
"agent_end",
|
|
10
|
+
"message_end",
|
|
11
|
+
"session_shutdown",
|
|
12
|
+
"session_compact",
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
export interface ValidationResult {
|
|
16
|
+
valid: boolean;
|
|
17
|
+
errors: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 校验单条规则 */
|
|
21
|
+
export function validateRule(rule: Record<string, unknown>): ValidationResult {
|
|
22
|
+
const errors: string[] = [];
|
|
23
|
+
|
|
24
|
+
// 归一化:将 disabled 转为 enabled
|
|
25
|
+
if ("disabled" in rule) {
|
|
26
|
+
const v = rule.disabled;
|
|
27
|
+
delete rule.disabled;
|
|
28
|
+
if (v === true && rule.enabled === undefined) rule.enabled = false;
|
|
29
|
+
if (v === false && rule.enabled === undefined) rule.enabled = true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (
|
|
33
|
+
!rule.comment ||
|
|
34
|
+
typeof rule.comment !== "string" ||
|
|
35
|
+
!rule.comment.trim()
|
|
36
|
+
) {
|
|
37
|
+
errors.push("缺少必填字段: comment");
|
|
38
|
+
}
|
|
39
|
+
if (!rule.reason || typeof rule.reason !== "string" || !rule.reason.trim()) {
|
|
40
|
+
errors.push("缺少必填字段: reason");
|
|
41
|
+
}
|
|
42
|
+
if (rule.action && !VALID_ACTIONS.includes(rule.action as any)) {
|
|
43
|
+
let msg = `action 值 "${rule.action}" 不合法,可选: ${VALID_ACTIONS.join(", ")}`;
|
|
44
|
+
// 常见误用:试图用 skip/disable/off 等新增规则去"关闭"某条提示。
|
|
45
|
+
// action 表示规则匹配后的执行动作,不是"关闭"操作;正确做法是 update + enabled:false。
|
|
46
|
+
const actionStr = String(rule.action).toLowerCase();
|
|
47
|
+
if (/skip|disable|off|close|stop|mute/.test(actionStr)) {
|
|
48
|
+
msg +=
|
|
49
|
+
"。action 是规则匹配后的执行动作,无法用来「关闭」规则。" +
|
|
50
|
+
"想临时关闭某条已存在的规则请用 update(index=N, changes={enabled:false})(保留规则,日后改 enabled:true 恢复);彻底删除用 delete(index=N)。" +
|
|
51
|
+
"操作前先 list 找到目标规则的 index。";
|
|
52
|
+
}
|
|
53
|
+
errors.push(msg);
|
|
54
|
+
}
|
|
55
|
+
if (rule.hook && !VALID_HOOKS.includes(rule.hook as any)) {
|
|
56
|
+
errors.push(
|
|
57
|
+
`hook 值 "${rule.hook}" 不合法,可选: ${VALID_HOOKS.join(", ")}`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
if (rule.pattern) {
|
|
61
|
+
try {
|
|
62
|
+
new RegExp(rule.pattern as string, (rule.flags as string) || "");
|
|
63
|
+
} catch (e: unknown) {
|
|
64
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
65
|
+
errors.push(`pattern 正则编译失败: ${msg}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (Array.isArray(rule.conditions)) {
|
|
69
|
+
for (let i = 0; i < rule.conditions.length; i++) {
|
|
70
|
+
const cond = rule.conditions[i] as Record<string, unknown>;
|
|
71
|
+
const validFields = ["path", "text", "glob", "result", "command", "reason"];
|
|
72
|
+
if (cond.field && !validFields.includes(cond.field as string)) {
|
|
73
|
+
errors.push(
|
|
74
|
+
`conditions[${i}].field 值无效: "${cond.field}",合法值: ${validFields.join(", ")}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (cond.pattern) {
|
|
78
|
+
try {
|
|
79
|
+
new RegExp(cond.pattern as string, (cond.flags as string) || "");
|
|
80
|
+
} catch (e: unknown) {
|
|
81
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
82
|
+
errors.push(`conditions[${i}].pattern 正则编译失败: ${msg}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { valid: errors.length === 0, errors };
|
|
89
|
+
}
|