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.
Files changed (48) hide show
  1. package/README.en.md +2 -0
  2. package/README.md +2 -0
  3. package/index.ts +1 -1
  4. package/package.json +1 -4
  5. package/rules.json +22 -206
  6. package/shepherd/index.ts +1 -0
  7. package/shepherd/rules-editor.ts +80 -0
  8. package/shepherd/rules-tool-helpers.ts +120 -0
  9. package/shepherd/rules-tool-list.ts +126 -0
  10. package/shepherd/rules-tool.ts +74 -31
  11. package/shepherd/rules.ts +15 -3
  12. package/shepherd/tool-hooks.ts +3 -2
  13. package/node_modules/@pi-atelier/shared-utils/README.en.md +0 -182
  14. package/node_modules/@pi-atelier/shared-utils/README.md +0 -182
  15. package/node_modules/@pi-atelier/shared-utils/package.json +0 -51
  16. package/node_modules/@pi-atelier/shared-utils/src/__tests__/agents.test.ts +0 -120
  17. package/node_modules/@pi-atelier/shared-utils/src/__tests__/ephemeral.test.ts +0 -100
  18. package/node_modules/@pi-atelier/shared-utils/src/__tests__/file-lock.test.ts +0 -152
  19. package/node_modules/@pi-atelier/shared-utils/src/__tests__/filter-match.test.ts +0 -187
  20. package/node_modules/@pi-atelier/shared-utils/src/__tests__/memory-parser.test.ts +0 -170
  21. package/node_modules/@pi-atelier/shared-utils/src/__tests__/paths.test.ts +0 -126
  22. package/node_modules/@pi-atelier/shared-utils/src/__tests__/project-config-edge.test.ts +0 -138
  23. package/node_modules/@pi-atelier/shared-utils/src/__tests__/project-config.test.ts +0 -257
  24. package/node_modules/@pi-atelier/shared-utils/src/__tests__/project-tools-mcp.test.ts +0 -189
  25. package/node_modules/@pi-atelier/shared-utils/src/__tests__/project-tools.test.ts +0 -204
  26. package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-backup-advanced.test.ts +0 -269
  27. package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-backup-array.test.ts +0 -267
  28. package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-backup.test.ts +0 -520
  29. package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-read.test.ts +0 -116
  30. package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-write.test.ts +0 -119
  31. package/node_modules/@pi-atelier/shared-utils/src/__tests__/tool-output.test.ts +0 -145
  32. package/node_modules/@pi-atelier/shared-utils/src/agents.ts +0 -39
  33. package/node_modules/@pi-atelier/shared-utils/src/ephemeral.ts +0 -42
  34. package/node_modules/@pi-atelier/shared-utils/src/file-lock.ts +0 -62
  35. package/node_modules/@pi-atelier/shared-utils/src/filter-match.ts +0 -100
  36. package/node_modules/@pi-atelier/shared-utils/src/index.ts +0 -71
  37. package/node_modules/@pi-atelier/shared-utils/src/memory-parser.ts +0 -96
  38. package/node_modules/@pi-atelier/shared-utils/src/paths.ts +0 -23
  39. package/node_modules/@pi-atelier/shared-utils/src/project-config.ts +0 -241
  40. package/node_modules/@pi-atelier/shared-utils/src/project-tools.ts +0 -191
  41. package/node_modules/@pi-atelier/shared-utils/src/settings-array.ts +0 -73
  42. package/node_modules/@pi-atelier/shared-utils/src/settings-backup-rollback.ts +0 -104
  43. package/node_modules/@pi-atelier/shared-utils/src/settings-backup-utils.ts +0 -75
  44. package/node_modules/@pi-atelier/shared-utils/src/settings-backup.ts +0 -172
  45. package/node_modules/@pi-atelier/shared-utils/src/settings.ts +0 -104
  46. package/node_modules/@pi-atelier/shared-utils/src/tool-output.ts +0 -149
  47. package/node_modules/@pi-atelier/shared-utils/tsconfig.json +0 -9
  48. 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
+ }
@@ -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, listRules, updateRule } from "./rules-editor";
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
- /** 构造 pi 工具 execute 的标准返回格式 */
11
- function textResult(text: string) {
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: "update/delete 时指定规则编号(0-based)",
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
- const result = listRules(rulesFilePath);
57
- if (result.error) return textResult(`❌ ${result.error}`);
58
- if (result.count === 0) return textResult("暂无规则。");
59
- return textResult(
60
- result.rules
61
- .map(
62
- (r) =>
63
- `[${r.index}] ${r.comment}` +
64
- (r.enabled === false ? " (disabled)" : "") +
65
- (r.action ? ` — ${r.action}` : "") +
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 result = addRule(rulesFilePath, params.rule);
75
- return result.success
76
- ? textResult(`✅ 规则已添加 [${result.index}]`)
77
- : textResult(`❌ ${result.error}`);
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 result = updateRule(rulesFilePath, params.index, params.changes);
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 result = deleteRule(rulesFilePath, params.index);
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(`✅ 规则已删除: ${(result.deleted as any)?.comment || ""}`)
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.startsWith(prefix) && file.endsWith(".json")) {
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 {
@@ -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 === event.toolName,
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 !== event.toolName) continue;
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