pi-shepherd 0.1.1 → 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.
Files changed (58) hide show
  1. package/README.en.md +206 -83
  2. package/README.md +209 -88
  3. package/index.ts +262 -229
  4. package/package.json +60 -58
  5. package/rules.json +197 -218
  6. package/shepherd/compaction.ts +76 -0
  7. package/shepherd/conditions.ts +98 -0
  8. package/shepherd/ephemeral.ts +55 -52
  9. package/shepherd/git.ts +64 -0
  10. package/shepherd/index.ts +14 -0
  11. package/shepherd/line-count.ts +86 -86
  12. package/shepherd/message-end.ts +120 -0
  13. package/shepherd/rules-editor.ts +250 -135
  14. package/shepherd/rules-tool-helpers.ts +119 -0
  15. package/shepherd/rules-tool-list.ts +124 -0
  16. package/shepherd/rules-tool.ts +182 -99
  17. package/shepherd/rules-validate.ts +89 -44
  18. package/shepherd/rules.ts +366 -283
  19. package/shepherd/tool-event-types.ts +27 -14
  20. package/shepherd/tool-hooks.ts +165 -176
  21. package/shepherd/worktree-check.ts +130 -130
  22. package/tsconfig.json +21 -14
  23. package/node_modules/@pi-atelier/shared-utils/README.en.md +0 -182
  24. package/node_modules/@pi-atelier/shared-utils/README.md +0 -182
  25. package/node_modules/@pi-atelier/shared-utils/package.json +0 -51
  26. package/node_modules/@pi-atelier/shared-utils/src/__tests__/agents.test.ts +0 -120
  27. package/node_modules/@pi-atelier/shared-utils/src/__tests__/ephemeral.test.ts +0 -100
  28. package/node_modules/@pi-atelier/shared-utils/src/__tests__/file-lock.test.ts +0 -152
  29. package/node_modules/@pi-atelier/shared-utils/src/__tests__/filter-match.test.ts +0 -187
  30. package/node_modules/@pi-atelier/shared-utils/src/__tests__/memory-parser.test.ts +0 -170
  31. package/node_modules/@pi-atelier/shared-utils/src/__tests__/paths.test.ts +0 -126
  32. package/node_modules/@pi-atelier/shared-utils/src/__tests__/project-config-edge.test.ts +0 -138
  33. package/node_modules/@pi-atelier/shared-utils/src/__tests__/project-config.test.ts +0 -257
  34. package/node_modules/@pi-atelier/shared-utils/src/__tests__/project-tools-mcp.test.ts +0 -189
  35. package/node_modules/@pi-atelier/shared-utils/src/__tests__/project-tools.test.ts +0 -204
  36. package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-backup-advanced.test.ts +0 -269
  37. package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-backup-array.test.ts +0 -267
  38. package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-backup.test.ts +0 -520
  39. package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-read.test.ts +0 -116
  40. package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-write.test.ts +0 -119
  41. package/node_modules/@pi-atelier/shared-utils/src/__tests__/tool-output.test.ts +0 -145
  42. package/node_modules/@pi-atelier/shared-utils/src/agents.ts +0 -39
  43. package/node_modules/@pi-atelier/shared-utils/src/ephemeral.ts +0 -42
  44. package/node_modules/@pi-atelier/shared-utils/src/file-lock.ts +0 -62
  45. package/node_modules/@pi-atelier/shared-utils/src/filter-match.ts +0 -100
  46. package/node_modules/@pi-atelier/shared-utils/src/index.ts +0 -71
  47. package/node_modules/@pi-atelier/shared-utils/src/memory-parser.ts +0 -96
  48. package/node_modules/@pi-atelier/shared-utils/src/paths.ts +0 -23
  49. package/node_modules/@pi-atelier/shared-utils/src/project-config.ts +0 -241
  50. package/node_modules/@pi-atelier/shared-utils/src/project-tools.ts +0 -191
  51. package/node_modules/@pi-atelier/shared-utils/src/settings-array.ts +0 -73
  52. package/node_modules/@pi-atelier/shared-utils/src/settings-backup-rollback.ts +0 -104
  53. package/node_modules/@pi-atelier/shared-utils/src/settings-backup-utils.ts +0 -75
  54. package/node_modules/@pi-atelier/shared-utils/src/settings-backup.ts +0 -172
  55. package/node_modules/@pi-atelier/shared-utils/src/settings.ts +0 -104
  56. package/node_modules/@pi-atelier/shared-utils/src/tool-output.ts +0 -149
  57. package/node_modules/@pi-atelier/shared-utils/tsconfig.json +0 -9
  58. package/node_modules/@pi-atelier/shared-utils/vitest.config.ts +0 -24
@@ -1,135 +1,250 @@
1
- /**
2
- * Shepherd 规则文件安全编辑器
3
- *
4
- * 提供 rules.json 的安全增删改查:
5
- * - 写入前校验(必填字段、正则合法性、枚举值)
6
- * - 自动备份(.bak)
7
- * - 写入后回读验证
8
- * - 失败自动回滚
9
- */
10
-
11
- import * as fs from "node:fs";
12
- import * as path from "node:path";
13
- import { validateRule } from "./rules-validate";
14
- export { validateRule } from "./rules-validate";
15
- export type { ValidationResult } from "./rules-validate";
16
-
17
- // ── 类型 ──────────────────────────────────────────────────
18
-
19
- export interface RuleSummary {
20
- index: number;
21
- comment: string;
22
- action?: string;
23
- tool?: string;
24
- hook?: string;
25
- enabled?: boolean;
26
- pattern?: string;
27
- }
28
-
29
- export interface ListResult {
30
- rules: RuleSummary[];
31
- count: number;
32
- error?: string;
33
- }
34
-
35
- export interface WriteResult {
36
- success: boolean;
37
- error?: string;
38
- index?: number;
39
- deleted?: { comment: string; [key: string]: unknown };
40
- }
41
-
42
- // ── 文件操作辅助 ──────────────────────────────────────────
43
-
44
- interface FileData {
45
- rules: Record<string, unknown>[];
46
- error?: string;
47
- }
48
-
49
- function readFile(filePath: string): FileData {
50
- if (!fs.existsSync(filePath)) return { rules: [] };
51
- try {
52
- const raw = fs.readFileSync(filePath, "utf-8");
53
- const parsed = JSON.parse(raw);
54
- if (!Array.isArray(parsed)) {
55
- return { rules: [], error: `顶层必须是 JSON 数组,当前是 ${typeof parsed}` };
56
- }
57
- return { rules: parsed };
58
- } catch (e: unknown) {
59
- return { rules: [], error: `JSON 解析失败: ${e.message}` };
60
- }
61
- }
62
-
63
- function safeWrite(filePath: string, rules: Record<string, unknown>[]): WriteResult {
64
- const backupPath = `${filePath}.bak`;
65
- if (fs.existsSync(filePath)) fs.copyFileSync(filePath, backupPath);
66
- const dir = path.dirname(filePath);
67
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
68
- fs.writeFileSync(filePath, JSON.stringify(rules, null, "\t"), "utf-8");
69
- try {
70
- const raw = fs.readFileSync(filePath, "utf-8");
71
- const parsed = JSON.parse(raw);
72
- if (!Array.isArray(parsed)) throw new Error("not array");
73
- return { success: true };
74
- } catch {
75
- if (fs.existsSync(backupPath)) fs.copyFileSync(backupPath, filePath);
76
- return { success: false, error: "写入后验证失败,已从备份恢复" };
77
- }
78
- }
79
-
80
- // ── 公开 API ─────────────────────────────────────────────
81
-
82
- export function listRules(filePath: string): ListResult {
83
- const { rules, error } = readFile(filePath);
84
- if (error) return { rules: [], count: 0, error };
85
- return {
86
- rules: rules.map((r, i) => ({
87
- index: i,
88
- comment: (r.comment as string) || "",
89
- action: r.action as string | undefined,
90
- tool: r.tool as string | undefined,
91
- hook: r.hook as string | undefined,
92
- enabled: r.enabled as boolean | undefined,
93
- pattern: r.pattern as string | undefined,
94
- })),
95
- count: rules.length,
96
- };
97
- }
98
-
99
- export function addRule(filePath: string, rule: Record<string, unknown>): WriteResult {
100
- const { rules, error } = readFile(filePath);
101
- if (error) return { success: false, error };
102
- const validation = validateRule(rule);
103
- if (!validation.valid) return { success: false, error: validation.errors.join("; ") };
104
- rules.push(rule);
105
- const writeResult = safeWrite(filePath, rules);
106
- return writeResult.success ? { success: true, index: rules.length - 1 } : writeResult;
107
- }
108
-
109
- export function updateRule(
110
- filePath: string,
111
- index: number,
112
- changes: Record<string, unknown>,
113
- ): WriteResult {
114
- if (index < 0) return { success: false, error: `编号越界: ${index}` };
115
- const { rules, error } = readFile(filePath);
116
- if (error) return { success: false, error };
117
- if (index >= rules.length) return { success: false, error: `编号越界: ${index}(共 ${rules.length} 条)` };
118
- const merged = { ...rules[index], ...changes };
119
- const validation = validateRule(merged);
120
- if (!validation.valid) return { success: false, error: validation.errors.join("; ") };
121
- rules[index] = merged;
122
- return safeWrite(filePath, rules);
123
- }
124
-
125
- export function deleteRule(filePath: string, index: number): WriteResult {
126
- if (index < 0) return { success: false, error: `编号越界: ${index}` };
127
- const { rules, error } = readFile(filePath);
128
- if (error) return { success: false, error };
129
- if (index >= rules.length) return { success: false, error: `编号越界: ${index}(共 ${rules.length} 条)` };
130
- const deleted = rules.splice(index, 1)[0];
131
- const writeResult = safeWrite(filePath, rules);
132
- return writeResult.success
133
- ? { success: true, deleted: { comment: (deleted.comment as string) || "", ...deleted } }
134
- : writeResult;
135
- }
1
+ /**
2
+ * Shepherd 规则文件安全编辑器
3
+ *
4
+ * 提供 rules.json 的安全增删改查:
5
+ * - 写入前校验(必填字段、正则合法性、枚举值)
6
+ * - 自动备份(.bak)
7
+ * - 写入后回读验证
8
+ * - 失败自动回滚
9
+ */
10
+
11
+ import * as fs from "node:fs";
12
+ import * as path from "node:path";
13
+ import { validateRule } from "./rules-validate";
14
+
15
+ export type { ValidationResult } from "./rules-validate";
16
+ export { validateRule } from "./rules-validate";
17
+
18
+ // ── 类型 ──────────────────────────────────────────────────
19
+
20
+ export interface RuleSummary {
21
+ index: number;
22
+ comment: string;
23
+ action?: string;
24
+ tool?: string;
25
+ hook?: string;
26
+ enabled?: boolean;
27
+ pattern?: string;
28
+ }
29
+
30
+ export interface ListResult {
31
+ rules: RuleSummary[];
32
+ count: number;
33
+ error?: string;
34
+ }
35
+
36
+ export interface RuleDetail {
37
+ index: number;
38
+ [key: string]: unknown;
39
+ }
40
+
41
+ export interface ListDetailResult {
42
+ rules: RuleDetail[];
43
+ count: number;
44
+ error?: string;
45
+ }
46
+
47
+ export interface WriteResult {
48
+ success: boolean;
49
+ error?: string;
50
+ index?: number;
51
+ deleted?: { comment: string; [key: string]: unknown };
52
+ overwritten?: boolean;
53
+ warning?: string;
54
+ }
55
+
56
+ // ── 文件操作辅助 ──────────────────────────────────────────
57
+
58
+ interface FileData {
59
+ rules: Record<string, unknown>[];
60
+ error?: string;
61
+ }
62
+
63
+ function readFile(filePath: string): FileData {
64
+ if (!fs.existsSync(filePath)) return { rules: [] };
65
+ try {
66
+ const raw = fs.readFileSync(filePath, "utf-8");
67
+ const parsed = JSON.parse(raw);
68
+ if (!Array.isArray(parsed)) {
69
+ return {
70
+ rules: [],
71
+ error: `顶层必须是 JSON 数组,当前是 ${typeof parsed}`,
72
+ };
73
+ }
74
+ return { rules: parsed };
75
+ } catch (e: unknown) {
76
+ const msg = e instanceof Error ? e.message : String(e);
77
+ return { rules: [], error: `JSON 解析失败: ${msg}` };
78
+ }
79
+ }
80
+
81
+ function safeWrite(
82
+ filePath: string,
83
+ rules: Record<string, unknown>[],
84
+ ): WriteResult {
85
+ const backupPath = `${filePath}.bak`;
86
+ if (fs.existsSync(filePath)) fs.copyFileSync(filePath, backupPath);
87
+ const dir = path.dirname(filePath);
88
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
89
+ fs.writeFileSync(filePath, JSON.stringify(rules, null, "\t"), "utf-8");
90
+ try {
91
+ const raw = fs.readFileSync(filePath, "utf-8");
92
+ const parsed = JSON.parse(raw);
93
+ if (!Array.isArray(parsed)) throw new Error("not array");
94
+ return { success: true };
95
+ } catch {
96
+ if (fs.existsSync(backupPath)) fs.copyFileSync(backupPath, filePath);
97
+ return { success: false, error: "写入后验证失败,已从备份恢复" };
98
+ }
99
+ }
100
+
101
+ // ── 去重签名 ─────────────────────────────────────────────
102
+
103
+ /** 规范化 conditions 数组:按 field+pattern 排序,取关键属性 */
104
+ function normalizeConditions(
105
+ conditions: Array<Record<string, unknown>>,
106
+ ): string {
107
+ const normalized = conditions
108
+ .map((c) => ({
109
+ f: (c.field as string) || "",
110
+ p: (c.pattern as string) || "",
111
+ }))
112
+ .sort((a, b) => (a.f + a.p).localeCompare(b.f + b.p));
113
+ return JSON.stringify(normalized);
114
+ }
115
+
116
+ /** 生成规则去重签名 key */
117
+ export function dedupKey(rule: Record<string, unknown>): string {
118
+ const tool = (rule.tool as string) || "";
119
+ const hook = (rule.hook as string) || "";
120
+ const action = (rule.action as string) || "";
121
+ let trigger: string;
122
+ if (Array.isArray(rule.conditions) && rule.conditions.length > 0) {
123
+ trigger = normalizeConditions(
124
+ rule.conditions as Array<Record<string, unknown>>,
125
+ );
126
+ } else if (rule.pattern) {
127
+ trigger = rule.pattern as string;
128
+ } else if (rule.check) {
129
+ trigger = rule.check as string;
130
+ } else {
131
+ trigger = "";
132
+ }
133
+ return `${tool}|${hook}|${trigger}|${action}`;
134
+ }
135
+
136
+ /** 在规则数组中查找签名相同的规则,返回 index 或 null */
137
+ export function findDuplicateBySignature(
138
+ rules: Record<string, unknown>[],
139
+ newRule: Record<string, unknown>,
140
+ ): number | null {
141
+ const newKey = dedupKey(newRule);
142
+ for (let i = 0; i < rules.length; i++) {
143
+ if (dedupKey(rules[i]) === newKey) return i;
144
+ }
145
+ return null;
146
+ }
147
+
148
+ // ── 公开 API ─────────────────────────────────────────────
149
+
150
+ export function listRules(filePath: string): ListResult {
151
+ const { rules, error } = readFile(filePath);
152
+ if (error) return { rules: [], count: 0, error };
153
+ return {
154
+ rules: rules.map((r, i) => ({
155
+ index: i,
156
+ comment: (r.comment as string) || "",
157
+ action: r.action as string | undefined,
158
+ tool: r.tool as string | undefined,
159
+ hook: r.hook as string | undefined,
160
+ enabled: r.enabled as boolean | undefined,
161
+ pattern: r.pattern as string | undefined,
162
+ })),
163
+ count: rules.length,
164
+ };
165
+ }
166
+
167
+ export function listRulesDetail(filePath: string): ListDetailResult {
168
+ const { rules, error } = readFile(filePath);
169
+ if (error) return { rules: [], count: 0, error };
170
+ return {
171
+ rules: rules.map((r, i) => ({ index: i, ...r })),
172
+ count: rules.length,
173
+ };
174
+ }
175
+
176
+ export function getRuleDetail(
177
+ filePath: string,
178
+ index: number,
179
+ ): RuleDetail | { error: string } {
180
+ const { rules, error } = readFile(filePath);
181
+ if (error) return { error };
182
+ if (index < 0 || index >= rules.length)
183
+ return { error: `编号越界: ${index}(共 ${rules.length} 条)` };
184
+ return { index, ...rules[index] };
185
+ }
186
+
187
+ export function addRule(
188
+ filePath: string,
189
+ rule: Record<string, unknown>,
190
+ ): WriteResult {
191
+ const { rules, error } = readFile(filePath);
192
+ if (error) return { success: false, error };
193
+ const validation = validateRule(rule);
194
+ if (!validation.valid)
195
+ return { success: false, error: validation.errors.join("; ") };
196
+ // 去重检测:签名相同则覆盖
197
+ const dupIdx = findDuplicateBySignature(rules, rule);
198
+ if (dupIdx !== null) {
199
+ rules[dupIdx] = rule;
200
+ const writeResult = safeWrite(filePath, rules);
201
+ return writeResult.success
202
+ ? { success: true, index: dupIdx, overwritten: true }
203
+ : writeResult;
204
+ }
205
+ rules.push(rule);
206
+ const writeResult = safeWrite(filePath, rules);
207
+ return writeResult.success
208
+ ? { success: true, index: rules.length - 1 }
209
+ : writeResult;
210
+ }
211
+
212
+ export function updateRule(
213
+ filePath: string,
214
+ index: number,
215
+ changes: Record<string, unknown>,
216
+ ): WriteResult {
217
+ if (index < 0) return { success: false, error: `编号越界: ${index}` };
218
+ const { rules, error } = readFile(filePath);
219
+ if (error) return { success: false, error };
220
+ if (index >= rules.length)
221
+ return {
222
+ success: false,
223
+ error: `编号越界: ${index}(共 ${rules.length} 条)`,
224
+ };
225
+ const merged = { ...rules[index], ...changes };
226
+ const validation = validateRule(merged);
227
+ if (!validation.valid)
228
+ return { success: false, error: validation.errors.join("; ") };
229
+ rules[index] = merged;
230
+ return safeWrite(filePath, rules);
231
+ }
232
+
233
+ export function deleteRule(filePath: string, index: number): WriteResult {
234
+ if (index < 0) return { success: false, error: `编号越界: ${index}` };
235
+ const { rules, error } = readFile(filePath);
236
+ if (error) return { success: false, error };
237
+ if (index >= rules.length)
238
+ return {
239
+ success: false,
240
+ error: `编号越界: ${index}(共 ${rules.length} 条)`,
241
+ };
242
+ const deleted = rules.splice(index, 1)[0];
243
+ const writeResult = safeWrite(filePath, rules);
244
+ return writeResult.success
245
+ ? {
246
+ success: true,
247
+ deleted: { comment: (deleted.comment as string) || "", ...deleted },
248
+ }
249
+ : writeResult;
250
+ }
@@ -0,0 +1,119 @@
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
+ }
@@ -0,0 +1,124 @@
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
+ }