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.
- package/README.en.md +206 -83
- package/README.md +209 -88
- package/index.ts +262 -229
- package/package.json +60 -58
- package/rules.json +197 -218
- 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 +14 -0
- package/shepherd/line-count.ts +86 -86
- package/shepherd/message-end.ts +120 -0
- package/shepherd/rules-editor.ts +250 -135
- package/shepherd/rules-tool-helpers.ts +119 -0
- package/shepherd/rules-tool-list.ts +124 -0
- package/shepherd/rules-tool.ts +182 -99
- package/shepherd/rules-validate.ts +89 -44
- package/shepherd/rules.ts +366 -283
- package/shepherd/tool-event-types.ts +27 -14
- package/shepherd/tool-hooks.ts +165 -176
- package/shepherd/worktree-check.ts +130 -130
- package/tsconfig.json +21 -14
- 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
package/tsconfig.json
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "../tsconfig.base.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"strict":
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
{
|
|
2
|
+
"extends": "../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"strict": true,
|
|
5
|
+
"types": ["node"],
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": ".",
|
|
8
|
+
"paths": {
|
|
9
|
+
"@pi-atelier/shared-utils": [
|
|
10
|
+
"./node_modules/@pi-atelier/shared-utils/src/index.ts"
|
|
11
|
+
],
|
|
12
|
+
"@earendil-works/pi-coding-agent": [
|
|
13
|
+
"../_sdk-link/dist/index.d.ts"
|
|
14
|
+
],
|
|
15
|
+
"@pi-atelier/shepherd": [
|
|
16
|
+
"./shepherd/index.ts"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"include": ["index.ts", "shepherd/**/*.ts", "tests/**/*.ts"]
|
|
21
|
+
}
|
|
@@ -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
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
[English](README.en.md) | 程序中文文档
|
|
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 ── 解析 topic--kw1,kw2.md 文件名 │
|
|
20
|
-
│ paths ── pi agent 标准路径常量 │
|
|
21
|
-
│ settings ── settings.json 读写扩展配置段 │
|
|
22
|
-
│ tool-output ── 工具输出截断(防上下文溢出) │
|
|
23
|
-
│ agents ── 子代理定义文件发现 │
|
|
24
|
-
│ ephemeral ── 会话临时 hint/label 栈 │
|
|
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
|
-
> 这是 pi-atelier monorepo 内的 workspace 包,通常不需要单独安装。其他独立扩展通过 `bundledDependencies` 自动包含它。
|
|
37
|
-
|
|
38
|
-
## 导出模块
|
|
39
|
-
|
|
40
|
-
### 记忆文件解析 (`memory-parser`)
|
|
41
|
-
|
|
42
|
-
解析 `topic--kw1,kw2,kw3.md` 格式的记忆文件名,扫描目录生成索引。
|
|
43
|
-
|
|
44
|
-
```ts
|
|
45
|
-
import { parseFileName, buildFileName, scanMemoryDir } from "@pi-atelier/shared-utils";
|
|
46
|
-
|
|
47
|
-
// 解析文件名 → { topic, keywords }
|
|
48
|
-
const { topic, keywords } = parseFileName("coding_standards--编码,git,lint.md");
|
|
49
|
-
// topic = "coding_standards", keywords = ["编码", "git", "lint"]
|
|
50
|
-
|
|
51
|
-
// 反向构建文件名
|
|
52
|
-
const name = buildFileName("coding_standards", ["编码", "git", "lint"]);
|
|
53
|
-
// "coding_standards--编码,git,lint.md"
|
|
54
|
-
|
|
55
|
-
// 扫描目录,返回 MemoryEntry[]
|
|
56
|
-
const entries = await scanMemoryDir("/path/to/memory");
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
### 路径常量 (`paths`)
|
|
60
|
-
|
|
61
|
-
pi agent 标准路径,避免硬编码。
|
|
62
|
-
|
|
63
|
-
| 常量 | 路径 | 说明 |
|
|
64
|
-
|------|------|------|
|
|
65
|
-
| `AGENT_DIR` | `~/.pi/agent/` | agent 根目录 |
|
|
66
|
-
| `SETTINGS_PATH` | `~/.pi/agent/settings.json` | 全局设置 |
|
|
67
|
-
| `MODELS_CONFIG_PATH` | `~/.pi/agent/models.json` | 模型配置 |
|
|
68
|
-
| `MCP_CONFIG_PATH` | `~/.pi/agent/mcp.json` | MCP 服务器配置 |
|
|
69
|
-
| `MCP_CACHE_PATH` | `~/.pi/agent/mcp-cache/` | MCP 工具缓存 |
|
|
70
|
-
| `AGENTS_DIR` | `~/.pi/agent/agents/` | 子代理定义 |
|
|
71
|
-
| `GLOBAL_RULES_PATH` | `~/.pi/agent/rules.md` | 全局规则 |
|
|
72
|
-
| `MEMORY_DIR` | `~/.pi/agent/memory/` | 全局记忆 |
|
|
73
|
-
| `MEMORY_MD_PATH` | `MEMORY.md` | 记忆索引文件名 |
|
|
74
|
-
|
|
75
|
-
### 设置管理 (`settings`)
|
|
76
|
-
|
|
77
|
-
读写 `settings.json` 中扩展的自定义配置段。
|
|
78
|
-
|
|
79
|
-
```ts
|
|
80
|
-
import { getSettingsSection, patchSettingsSection, getSettingsValue, setSettingsValue } from "@pi-atelier/shared-utils";
|
|
81
|
-
|
|
82
|
-
// 读取扩展配置段
|
|
83
|
-
const config = await getSettingsSection("my-extension");
|
|
84
|
-
|
|
85
|
-
// 增量更新配置
|
|
86
|
-
await patchSettingsSection("my-extension", { enabled: true });
|
|
87
|
-
|
|
88
|
-
// 读取/写入单个值
|
|
89
|
-
const val = await getSettingsValue("my-extension", "key", "default");
|
|
90
|
-
await setSettingsValue("my-extension", "key", "new-value");
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
### 工具输出截断 (`tool-output`)
|
|
94
|
-
|
|
95
|
-
防止工具返回超大结果撑爆 LLM 上下文。
|
|
96
|
-
|
|
97
|
-
```ts
|
|
98
|
-
import { truncateToolOutput, truncatedResult, TOOL_OUTPUT_MAX_LINES } from "@pi-atelier/shared-utils";
|
|
99
|
-
|
|
100
|
-
// 截断过长的输出
|
|
101
|
-
const result = truncateToolOutput(longText, { maxLines: 200 });
|
|
102
|
-
// { text: "...", truncated: true, originalLines: 1500, keptLines: 200 }
|
|
103
|
-
|
|
104
|
-
// 快捷方式:返回 pi tool result 格式
|
|
105
|
-
return truncatedResult(text); // 自动截断 + 返回 { content: [{ type: "text", text }] }
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
### 子代理发现 (`agents`)
|
|
109
|
-
|
|
110
|
-
扫描 `~/.pi/agent/agents/` 目录下的子代理定义文件。
|
|
111
|
-
|
|
112
|
-
```ts
|
|
113
|
-
import { discoverAgents, getAgentDescription, formatAgentsList } from "@pi-atelier/shared-utils";
|
|
114
|
-
|
|
115
|
-
// 发现所有可用子代理
|
|
116
|
-
const agents = await discoverAgents();
|
|
117
|
-
// [{ name: "pv-executor", description: "...", filePath: "..." }, ...]
|
|
118
|
-
|
|
119
|
-
// 获取单个描述
|
|
120
|
-
const desc = await getAgentDescription("pv-executor");
|
|
121
|
-
|
|
122
|
-
// 格式化为可读列表
|
|
123
|
-
const list = formatAgentsList(agents);
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
### 会话临时数据 (`ephemeral`)
|
|
127
|
-
|
|
128
|
-
当前会话的临时 hint/label 栈,会话结束即消失。用于跨工具调用的轻量状态传递。
|
|
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"); // 查看不移除
|
|
136
|
-
const all = drainHints(); // 取出并清空
|
|
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
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@pi-atelier/shared-utils",
|
|
3
|
-
"version": "1.0.0",
|
|
4
|
-
"description": "Shared utilities for pi-atelier extensions — settings, project config, memory parsing, tool output truncation, and more",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "src/index.ts",
|
|
7
|
-
"types": "src/index.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": "./src/index.ts",
|
|
10
|
-
"./*": "./src/*.ts"
|
|
11
|
-
},
|
|
12
|
-
"scripts": {
|
|
13
|
-
"build": "tsup",
|
|
14
|
-
"test": "vitest run",
|
|
15
|
-
"lint": "biome check lib src index.ts",
|
|
16
|
-
"typecheck": "tsc --noEmit"
|
|
17
|
-
},
|
|
18
|
-
"peerDependencies": {
|
|
19
|
-
"@earendil-works/pi-coding-agent": ">=0.6.0"
|
|
20
|
-
},
|
|
21
|
-
"peerDependenciesMeta": {
|
|
22
|
-
"@earendil-works/pi-coding-agent": {
|
|
23
|
-
"optional": true
|
|
24
|
-
}
|
|
25
|
-
},
|
|
26
|
-
"devDependencies": {
|
|
27
|
-
"@biomejs/biome": "^2.4.15",
|
|
28
|
-
"@vitest/coverage-v8": "^3.2.4",
|
|
29
|
-
"tsup": "^8.0.0",
|
|
30
|
-
"typescript": "^5.7.0",
|
|
31
|
-
"vitest": "^3.0.0"
|
|
32
|
-
},
|
|
33
|
-
"tsup": {
|
|
34
|
-
"entry": [
|
|
35
|
-
"src/index.ts"
|
|
36
|
-
],
|
|
37
|
-
"format": [
|
|
38
|
-
"esm"
|
|
39
|
-
],
|
|
40
|
-
"dts": true,
|
|
41
|
-
"clean": true
|
|
42
|
-
},
|
|
43
|
-
"files": [
|
|
44
|
-
"src/",
|
|
45
|
-
"README.md",
|
|
46
|
-
"README.en.md",
|
|
47
|
-
"tsconfig.json",
|
|
48
|
-
"vitest.config.ts",
|
|
49
|
-
"package.json"
|
|
50
|
-
]
|
|
51
|
-
}
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
|
|
3
|
-
// vi.mock factory is hoisted, use vi.hoisted to declare variables at the hoisted position
|
|
4
|
-
const mockFs = vi.hoisted(() => ({
|
|
5
|
-
existsSync: vi.fn(),
|
|
6
|
-
readdirSync: vi.fn(),
|
|
7
|
-
readFileSync: vi.fn(),
|
|
8
|
-
}));
|
|
9
|
-
|
|
10
|
-
vi.mock("node:fs", () => mockFs);
|
|
11
|
-
|
|
12
|
-
import { discoverAgents, getAgentDescription, formatAgentsList } from "../agents";
|
|
13
|
-
|
|
14
|
-
beforeEach(() => {
|
|
15
|
-
vi.clearAllMocks();
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
describe("discoverAgents", () => {
|
|
19
|
-
it("returns empty array when AGENTS_DIR does not exist", () => {
|
|
20
|
-
mockFs.existsSync.mockReturnValue(false);
|
|
21
|
-
expect(discoverAgents()).toEqual([]);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("filters .md files and strips leading underscore", () => {
|
|
25
|
-
mockFs.existsSync.mockReturnValue(true);
|
|
26
|
-
mockFs.readdirSync.mockReturnValue([
|
|
27
|
-
"coder.md",
|
|
28
|
-
"_private.md",
|
|
29
|
-
"reviewer.md",
|
|
30
|
-
"readme.txt",
|
|
31
|
-
"notes.md",
|
|
32
|
-
]);
|
|
33
|
-
const result = discoverAgents();
|
|
34
|
-
expect(result).toEqual(["coder", "reviewer", "notes"]);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("returns empty when only non-md files exist", () => {
|
|
38
|
-
mockFs.existsSync.mockReturnValue(true);
|
|
39
|
-
mockFs.readdirSync.mockReturnValue(["file.txt", "file.json"]);
|
|
40
|
-
expect(discoverAgents()).toEqual([]);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("returns empty when only underscored md files exist", () => {
|
|
44
|
-
mockFs.existsSync.mockReturnValue(true);
|
|
45
|
-
mockFs.readdirSync.mockReturnValue(["_private.md", "_template.md"]);
|
|
46
|
-
expect(discoverAgents()).toEqual([]);
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
describe("getAgentDescription", () => {
|
|
51
|
-
it("extracts description from frontmatter", () => {
|
|
52
|
-
const content = [
|
|
53
|
-
"---",
|
|
54
|
-
"description: 代码审查助手",
|
|
55
|
-
"version: 1.0",
|
|
56
|
-
"---",
|
|
57
|
-
"# Coder Agent",
|
|
58
|
-
"Some content",
|
|
59
|
-
].join("\n");
|
|
60
|
-
mockFs.readFileSync.mockReturnValue(content);
|
|
61
|
-
const result = getAgentDescription("coder");
|
|
62
|
-
expect(result).toBe("代码审查助手");
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("uses default description when no frontmatter", () => {
|
|
66
|
-
mockFs.readFileSync.mockReturnValue("plain content without frontmatter");
|
|
67
|
-
const result = getAgentDescription("coder");
|
|
68
|
-
expect(result).toBe("read, grep, find, ls");
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("uses default description when file read fails", () => {
|
|
72
|
-
mockFs.readFileSync.mockImplementation(() => {
|
|
73
|
-
throw new Error("ENOENT");
|
|
74
|
-
});
|
|
75
|
-
const result = getAgentDescription("nonexistent");
|
|
76
|
-
expect(result).toBe("read, grep, find, ls");
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("uses default description when frontmatter has no description field", () => {
|
|
80
|
-
const content = [
|
|
81
|
-
"---",
|
|
82
|
-
"title: Agent",
|
|
83
|
-
"---",
|
|
84
|
-
"# Content",
|
|
85
|
-
].join("\n");
|
|
86
|
-
mockFs.readFileSync.mockReturnValue(content);
|
|
87
|
-
const result = getAgentDescription("agent");
|
|
88
|
-
expect(result).toBe("read, grep, find, ls");
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("handles description with trailing whitespace", () => {
|
|
92
|
-
const content = [
|
|
93
|
-
"---",
|
|
94
|
-
"description: 帮我写代码 ",
|
|
95
|
-
"---",
|
|
96
|
-
].join("\n");
|
|
97
|
-
mockFs.readFileSync.mockReturnValue(content);
|
|
98
|
-
const result = getAgentDescription("agent");
|
|
99
|
-
expect(result).toBe("帮我写代码");
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
describe("formatAgentsList", () => {
|
|
104
|
-
it("returns placeholder when no agents", () => {
|
|
105
|
-
mockFs.existsSync.mockReturnValue(false);
|
|
106
|
-
expect(formatAgentsList()).toBe("(无可用子代理)");
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("formats agent list with descriptions", () => {
|
|
110
|
-
mockFs.existsSync.mockReturnValue(true);
|
|
111
|
-
mockFs.readdirSync.mockReturnValue(["coder.md", "reviewer.md"]);
|
|
112
|
-
mockFs.readFileSync.mockImplementation((filePath: string) => {
|
|
113
|
-
if (filePath.includes("coder")) return "---\ndescription: Coder助手\n---\n";
|
|
114
|
-
if (filePath.includes("reviewer")) return "---\ndescription: Reviewer助手\n---\n";
|
|
115
|
-
return "";
|
|
116
|
-
});
|
|
117
|
-
const result = formatAgentsList();
|
|
118
|
-
expect(result).toBe("- **coder**: Coder助手\n- **reviewer**: Reviewer助手");
|
|
119
|
-
});
|
|
120
|
-
});
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ephemeral.ts 测试
|
|
3
|
-
*
|
|
4
|
-
* 模块内 _hints/_labels 是模块级变量,每次 import 可能返回缓存模块。
|
|
5
|
-
* 使用 vi.resetModules() 确保每个测试得到干净的模块状态。
|
|
6
|
-
*/
|
|
7
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
8
|
-
|
|
9
|
-
function importEphemeral() {
|
|
10
|
-
return import("../ephemeral");
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
beforeEach(() => {
|
|
14
|
-
vi.resetModules();
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
describe("ephemeral hints lifecycle", () => {
|
|
18
|
-
it("starts empty: hasHints returns false", async () => {
|
|
19
|
-
const { hasHints } = await importEphemeral();
|
|
20
|
-
expect(hasHints()).toBe(false);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("peekHints returns null when empty", async () => {
|
|
24
|
-
const { peekHints } = await importEphemeral();
|
|
25
|
-
expect(peekHints()).toBeNull();
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("drainHints returns null when empty", async () => {
|
|
29
|
-
const { drainHints } = await importEphemeral();
|
|
30
|
-
expect(drainHints()).toBeNull();
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("peekLabels returns empty array when empty", async () => {
|
|
34
|
-
const { peekLabels } = await importEphemeral();
|
|
35
|
-
expect(peekLabels()).toEqual([]);
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
describe("pushHint and peek", () => {
|
|
40
|
-
it("pushHint adds hint, peekHints returns it without consuming", async () => {
|
|
41
|
-
const mod = await importEphemeral();
|
|
42
|
-
mod.pushHint("提示A");
|
|
43
|
-
expect(mod.hasHints()).toBe(true);
|
|
44
|
-
expect(mod.peekHints()).toBe("提示A");
|
|
45
|
-
// peek does not consume
|
|
46
|
-
expect(mod.peekHints()).toBe("提示A");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("pushHint with label adds label to peekLabels", async () => {
|
|
50
|
-
const mod = await importEphemeral();
|
|
51
|
-
mod.pushHint("提示B", "label-b");
|
|
52
|
-
expect(mod.peekLabels()).toEqual(["label-b"]);
|
|
53
|
-
expect(mod.peekHints()).toBe("提示B");
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("multiple hints joined by double newline", async () => {
|
|
57
|
-
const mod = await importEphemeral();
|
|
58
|
-
mod.pushHint("first");
|
|
59
|
-
mod.pushHint("second");
|
|
60
|
-
expect(mod.peekHints()).toBe("first\n\nsecond");
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
describe("drainHints", () => {
|
|
65
|
-
it("drainHints returns hints and clears state", async () => {
|
|
66
|
-
const mod = await importEphemeral();
|
|
67
|
-
mod.pushHint("hint1", "lbl1");
|
|
68
|
-
mod.pushHint("hint2", "lbl2");
|
|
69
|
-
const result = mod.drainHints();
|
|
70
|
-
expect(result).toBe("hint1\n\nhint2");
|
|
71
|
-
// after drain, state is cleared
|
|
72
|
-
expect(mod.hasHints()).toBe(false);
|
|
73
|
-
expect(mod.peekHints()).toBeNull();
|
|
74
|
-
expect(mod.peekLabels()).toEqual([]);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("labels are cleared after drain", async () => {
|
|
78
|
-
const mod = await importEphemeral();
|
|
79
|
-
mod.pushHint("test", "mylabel");
|
|
80
|
-
mod.drainHints();
|
|
81
|
-
expect(mod.peekLabels()).toEqual([]);
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
describe("multiple push then drain", () => {
|
|
86
|
-
it("push 3 hints without labels", async () => {
|
|
87
|
-
const mod = await importEphemeral();
|
|
88
|
-
mod.pushHint("a");
|
|
89
|
-
mod.pushHint("b");
|
|
90
|
-
mod.pushHint("c");
|
|
91
|
-
expect(mod.drainHints()).toBe("a\n\nb\n\nc");
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("push with labels, peekLabels returns all labels", async () => {
|
|
95
|
-
const mod = await importEphemeral();
|
|
96
|
-
mod.pushHint("x", "l1");
|
|
97
|
-
mod.pushHint("y", "l2");
|
|
98
|
-
expect(mod.peekLabels()).toEqual(["l1", "l2"]);
|
|
99
|
-
});
|
|
100
|
-
});
|