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
|
@@ -1,120 +1,119 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shepherd 规则工具辅助函数
|
|
3
|
-
*
|
|
4
|
-
* scope 路径推导、合并列表、跨 scope 去重检测。
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import * as
|
|
8
|
-
import * as
|
|
9
|
-
import { listRules } from "./rules-editor";
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
const
|
|
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
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Shepherd 规则工具辅助函数
|
|
3
|
+
*
|
|
4
|
+
* scope 路径推导、合并列表、跨 scope 去重检测。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import { findDuplicateBySignature, listRules } from "./rules-editor";
|
|
10
|
+
|
|
11
|
+
/** 项目级规则文件名 */
|
|
12
|
+
const PROJECT_RULES_FILE = "shepherd-rules.json";
|
|
13
|
+
|
|
14
|
+
export type Scope = "global" | "project";
|
|
15
|
+
|
|
16
|
+
/** 根据 scope 推导规则文件路径 */
|
|
17
|
+
export function getRulesFilePath(
|
|
18
|
+
scope: Scope | undefined,
|
|
19
|
+
globalDir: string,
|
|
20
|
+
cwd: string,
|
|
21
|
+
): string {
|
|
22
|
+
if (scope === "project") {
|
|
23
|
+
return path.join(cwd, ".pi", "extensions", PROJECT_RULES_FILE);
|
|
24
|
+
}
|
|
25
|
+
// 默认 global
|
|
26
|
+
return path.join(globalDir, "rules.json");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ListRuleItem {
|
|
30
|
+
index: number;
|
|
31
|
+
comment: string;
|
|
32
|
+
scope: Scope;
|
|
33
|
+
enabled?: boolean;
|
|
34
|
+
action?: string;
|
|
35
|
+
tool?: string;
|
|
36
|
+
hook?: string;
|
|
37
|
+
[key: string]: unknown;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 按_scope 列出规则。scope 不传时返回全局+项目合并列表。
|
|
42
|
+
*/
|
|
43
|
+
export function listRulesByScope(
|
|
44
|
+
globalDir: string,
|
|
45
|
+
cwd: string,
|
|
46
|
+
scope?: Scope,
|
|
47
|
+
): ListRuleItem[] {
|
|
48
|
+
const results: ListRuleItem[] = [];
|
|
49
|
+
|
|
50
|
+
if (!scope || scope === "global") {
|
|
51
|
+
const globalPath = path.join(globalDir, "rules.json");
|
|
52
|
+
if (fs.existsSync(globalPath)) {
|
|
53
|
+
const r = listRules(globalPath);
|
|
54
|
+
if (!r.error && r.rules) {
|
|
55
|
+
for (const rule of r.rules) {
|
|
56
|
+
results.push({ ...rule, scope: "global" });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!scope || scope === "project") {
|
|
63
|
+
const projectPath = path.join(cwd, ".pi", "extensions", PROJECT_RULES_FILE);
|
|
64
|
+
if (fs.existsSync(projectPath)) {
|
|
65
|
+
const r = listRules(projectPath);
|
|
66
|
+
if (!r.error && r.rules) {
|
|
67
|
+
for (const rule of r.rules) {
|
|
68
|
+
results.push({ ...rule, scope: "project" });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return results;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 跨 scope 去重检测:写入 A scope 时检查 B scope 是否有签名相同的规则。
|
|
79
|
+
* 返回 warning 字符串或 null。
|
|
80
|
+
*/
|
|
81
|
+
export function checkCrossScopeDuplicate(
|
|
82
|
+
targetScope: Scope,
|
|
83
|
+
globalDir: string,
|
|
84
|
+
cwd: string,
|
|
85
|
+
newRule: Record<string, unknown>,
|
|
86
|
+
): string | null {
|
|
87
|
+
const otherScope: Scope = targetScope === "global" ? "project" : "global";
|
|
88
|
+
const otherPath = getRulesFilePath(otherScope, globalDir, cwd);
|
|
89
|
+
|
|
90
|
+
if (!fs.existsSync(otherPath)) return null;
|
|
91
|
+
|
|
92
|
+
const content = fs.readFileSync(otherPath, "utf-8");
|
|
93
|
+
let rules: Record<string, unknown>[];
|
|
94
|
+
try {
|
|
95
|
+
const parsed = JSON.parse(content);
|
|
96
|
+
if (!Array.isArray(parsed)) return null;
|
|
97
|
+
rules = parsed;
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const dupIdx = findDuplicateBySignature(rules, newRule);
|
|
103
|
+
if (dupIdx !== null) {
|
|
104
|
+
const scopeLabel = otherScope === "global" ? "全局" : "项目级";
|
|
105
|
+
return `⚠ ${scopeLabel}已有相似规则 [${otherScope}:${dupIdx}]`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 确保项目级规则文件的目录存在
|
|
113
|
+
*/
|
|
114
|
+
export function ensureProjectDir(cwd: string): void {
|
|
115
|
+
const dir = path.join(cwd, ".pi", "extensions");
|
|
116
|
+
if (!fs.existsSync(dir)) {
|
|
117
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -1,126 +1,124 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shepherd 规则工具 — list 输出格式化
|
|
3
|
-
*
|
|
4
|
-
* 处理 list 的两种输出模式:摘要模式和 verbose(完整 JSON)模式。
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
export
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
rulesDir
|
|
30
|
-
|
|
31
|
-
) {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
scope
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
(r.
|
|
103
|
-
(r.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
(r.
|
|
120
|
-
(r.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
);
|
|
126
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Shepherd 规则工具 — list 输出格式化
|
|
3
|
+
*
|
|
4
|
+
* 处理 list 的两种输出模式:摘要模式和 verbose(完整 JSON)模式。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getRuleDetail, listRules, listRulesDetail } from "./rules-editor";
|
|
8
|
+
import {
|
|
9
|
+
getRulesFilePath,
|
|
10
|
+
listRulesByScope,
|
|
11
|
+
type Scope,
|
|
12
|
+
} from "./rules-tool-helpers";
|
|
13
|
+
|
|
14
|
+
/** 构造 pi 工具 execute 的标准返回格式 */
|
|
15
|
+
export function textResult(text: string) {
|
|
16
|
+
return { content: [{ type: "text" as const, text }] };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const scopeLabel = (s: Scope) => (s === "global" ? "全局" : "项目级");
|
|
20
|
+
|
|
21
|
+
/** 按编号查看单条规则的完整 JSON */
|
|
22
|
+
export function handleListByIndex(
|
|
23
|
+
scope: Scope | undefined,
|
|
24
|
+
index: number,
|
|
25
|
+
rulesDir: string,
|
|
26
|
+
effectiveCwd: string,
|
|
27
|
+
) {
|
|
28
|
+
const effectiveScope = scope || "global";
|
|
29
|
+
const filePath = getRulesFilePath(effectiveScope, rulesDir, effectiveCwd);
|
|
30
|
+
const detail = getRuleDetail(filePath, index);
|
|
31
|
+
if ("error" in detail) return textResult(`❌ ${detail.error}`);
|
|
32
|
+
const { index: _, ...ruleData } = detail;
|
|
33
|
+
return textResult(
|
|
34
|
+
`📋 规则 [${effectiveScope}:${index}] 完整内容:\n${JSON.stringify(ruleData, null, "\t")}`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** verbose 模式:显示每条规则的完整 JSON */
|
|
39
|
+
export function handleListVerbose(
|
|
40
|
+
scope: Scope | undefined,
|
|
41
|
+
rulesDir: string,
|
|
42
|
+
effectiveCwd: string,
|
|
43
|
+
) {
|
|
44
|
+
if (!scope) {
|
|
45
|
+
// 全部 scope
|
|
46
|
+
const parts: string[] = [];
|
|
47
|
+
const globalPath = getRulesFilePath("global", rulesDir, effectiveCwd);
|
|
48
|
+
const projectPath = getRulesFilePath("project", rulesDir, effectiveCwd);
|
|
49
|
+
|
|
50
|
+
const gResult = listRulesDetail(globalPath);
|
|
51
|
+
if (gResult.rules.length > 0) {
|
|
52
|
+
parts.push("── 全局规则 ──");
|
|
53
|
+
for (const r of gResult.rules) {
|
|
54
|
+
const { index: _, ...data } = r;
|
|
55
|
+
parts.push(`[global:${r.index}] ${JSON.stringify(data, null, "\t")}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const pResult = listRulesDetail(projectPath);
|
|
59
|
+
if (pResult.rules.length > 0) {
|
|
60
|
+
parts.push("── 项目级规则 ──");
|
|
61
|
+
for (const r of pResult.rules) {
|
|
62
|
+
const { index: _, ...data } = r;
|
|
63
|
+
parts.push(`[project:${r.index}] ${JSON.stringify(data, null, "\t")}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (parts.length === 0)
|
|
67
|
+
return textResult("暂无规则(全局和项目级均为空)。");
|
|
68
|
+
return textResult(parts.join("\n\n"));
|
|
69
|
+
}
|
|
70
|
+
// 指定 scope
|
|
71
|
+
const filePath = getRulesFilePath(scope, rulesDir, effectiveCwd);
|
|
72
|
+
const result = listRulesDetail(filePath);
|
|
73
|
+
if (result.error) return textResult(`❌ ${result.error}`);
|
|
74
|
+
if (result.count === 0) return textResult(`暂无${scopeLabel(scope)}规则。`);
|
|
75
|
+
return textResult(
|
|
76
|
+
result.rules
|
|
77
|
+
.map((r) => {
|
|
78
|
+
const { index: _, ...data } = r;
|
|
79
|
+
return `[${scope}:${r.index}] ${JSON.stringify(data, null, "\t")}`;
|
|
80
|
+
})
|
|
81
|
+
.join("\n\n"),
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 摘要模式:一行一条,只显示 comment + action + tool + hook */
|
|
86
|
+
export function handleListSummary(
|
|
87
|
+
scope: Scope | undefined,
|
|
88
|
+
rulesDir: string,
|
|
89
|
+
effectiveCwd: string,
|
|
90
|
+
) {
|
|
91
|
+
if (!scope) {
|
|
92
|
+
const items = listRulesByScope(rulesDir, effectiveCwd);
|
|
93
|
+
if (items.length === 0)
|
|
94
|
+
return textResult("暂无规则(全局和项目级均为空)。");
|
|
95
|
+
return textResult(
|
|
96
|
+
items
|
|
97
|
+
.map(
|
|
98
|
+
(r) =>
|
|
99
|
+
`[${r.scope}:${r.index}] ${r.comment}` +
|
|
100
|
+
(r.enabled === false ? " (disabled)" : "") +
|
|
101
|
+
(r.action ? ` — ${r.action}` : "") +
|
|
102
|
+
(r.tool ? ` on ${r.tool}` : "") +
|
|
103
|
+
(r.hook ? ` @ ${r.hook}` : ""),
|
|
104
|
+
)
|
|
105
|
+
.join("\n"),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
const filePath = getRulesFilePath(scope, rulesDir, effectiveCwd);
|
|
109
|
+
const result = listRules(filePath);
|
|
110
|
+
if (result.error) return textResult(`❌ ${result.error}`);
|
|
111
|
+
if (result.count === 0) return textResult(`暂无${scopeLabel(scope)}规则。`);
|
|
112
|
+
return textResult(
|
|
113
|
+
result.rules
|
|
114
|
+
.map(
|
|
115
|
+
(r) =>
|
|
116
|
+
`[${scope}:${r.index}] ${r.comment}` +
|
|
117
|
+
(r.enabled === false ? " (disabled)" : "") +
|
|
118
|
+
(r.action ? ` — ${r.action}` : "") +
|
|
119
|
+
(r.tool ? ` on ${r.tool}` : "") +
|
|
120
|
+
(r.hook ? ` @ ${r.hook}` : ""),
|
|
121
|
+
)
|
|
122
|
+
.join("\n"),
|
|
123
|
+
);
|
|
124
|
+
}
|