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
@@ -1,172 +0,0 @@
1
- /**
2
- * Settings 备份与安全修改
3
- *
4
- * 提供带备份的 settings.json 修改能力:
5
- * - 修改前自动备份
6
- * - 类型校验(validateConfigSchema)
7
- * - 备份轮转(maxBackups)
8
- * - 回滚(rollbackSettings)
9
- *
10
- * 用法:
11
- * import { patchSettingsSectionWithBackup, rollbackSettings, listBackups } from "@pi-atelier/shared-utils/settings-backup";
12
- *
13
- * const result = patchSettingsSectionWithBackup("context", { distillThreshold: 8000 }, defaults, { settingsPath, backupDir });
14
- * rollbackSettings({ settingsPath, backupDir });
15
- */
16
-
17
- import { existsSync } from "node:fs";
18
- import { withFileLock } from "./file-lock";
19
- import { validateConfigSchema, type SchemaError } from "./project-config";
20
- import { isArrayPatch, applyArrayPatch } from "./settings-array";
21
- import { createBackup, getDefaultBackupDir, readFull, writeFull } from "./settings-backup-utils";
22
- import { rotateBackups } from "./settings-backup-rollback";
23
- export { rollbackSettings, listBackups } from "./settings-backup-rollback";
24
- export type { BackupEntry, RollbackOptions } from "./settings-backup-rollback";
25
-
26
- // ── 类型 ─────────────────────────────────────────────────
27
-
28
- export interface PatchBackupOptions {
29
- /** 要修改的 settings.json 路径(默认全局) */
30
- settingsPath?: string;
31
- /** 备份目录路径 */
32
- backupDir?: string;
33
- /** 是否创建备份(默认 true) */
34
- backup?: boolean;
35
- /** 最大保留备份数(默认 10) */
36
- maxBackups?: number;
37
- /** 是否校验类型(默认 true) */
38
- validate?: boolean;
39
- }
40
-
41
- export interface PatchBackupResult<T extends Record<string, any>> {
42
- /** 合并后的配置 */
43
- config: T;
44
- /** 类型校验错误 */
45
- errors: SchemaError[];
46
- /** 备份文件路径(如果创建了备份) */
47
- backupPath?: string;
48
- }
49
-
50
- // rotateBackups / rollbackSettings / listBackups 已拆分到 settings-backup-rollback.ts
51
-
52
- // ── API ─────────────────────────────────────────────────
53
-
54
- /**
55
- * 带备份的 settings.json 安全修改
56
- *
57
- * 执行流程:备份 → 校验 → 合并 → 写入
58
- *
59
- * @param section - settings.json 中的顶层 key
60
- * @param patch - 要修改的字段
61
- * @param defaults - 该段的默认值(用于校验)
62
- * @param options - 备份和校验选项
63
- * @returns 合并后的配置 + 校验错误
64
- */
65
- export function patchSettingsSectionWithBackup<T extends Record<string, any>>(
66
- section: string,
67
- patch: Partial<T>,
68
- defaults: T,
69
- options?: PatchBackupOptions,
70
- ): PatchBackupResult<T> {
71
- const settingsPath = options?.settingsPath ?? "";
72
- const backupDir = options?.backupDir ?? getDefaultBackupDir(settingsPath);
73
- const shouldBackup = options?.backup !== false;
74
- const maxBackups = options?.maxBackups ?? 10;
75
- const shouldValidate = options?.validate !== false;
76
-
77
- // 1. 读取当前配置
78
- const settings = readFull(settingsPath);
79
-
80
- // ── 数组 patch 分支 ──
81
- if (isArrayPatch(patch)) {
82
- const currentArr: any[] = Array.isArray(settings[section]) ? settings[section] : (Array.isArray(defaults) ? defaults : []);
83
- const mergedArr = applyArrayPatch(currentArr, patch);
84
-
85
- // 备份
86
- let backupPath: string | undefined;
87
- if (shouldBackup && backupDir && existsSync(settingsPath)) {
88
- backupPath = createBackup(settingsPath, backupDir);
89
- rotateBackups(backupDir, maxBackups);
90
- }
91
-
92
- // 加锁写入
93
- if (settingsPath) {
94
- withFileLock(settingsPath, () => {
95
- const latest = readFull(settingsPath);
96
- latest[section] = mergedArr;
97
- writeFull(settingsPath, latest);
98
- });
99
- } else {
100
- settings[section] = mergedArr;
101
- writeFull(settingsPath, settings);
102
- }
103
-
104
- return { config: mergedArr as any as T, errors: [], backupPath };
105
- }
106
-
107
- // ── 对象 merge 分支(原有逻辑) ──
108
- const current = settings[section] ?? {};
109
-
110
- // 2. 计算合并后的配置(用于校验)
111
- const merged: Record<string, any> = { ...current };
112
- for (const [key, value] of Object.entries(patch)) {
113
- if (value !== undefined) merged[key] = value;
114
- }
115
-
116
- // 3. 类型校验(校验合并后的结果 vs defaults)
117
- const errors: SchemaError[] = [];
118
- if (shouldValidate) {
119
- // 构造一个临时的"项目配置"用于校验
120
- for (const key of Object.keys(merged)) {
121
- if (key in defaults) {
122
- const defaultVal = (defaults as Record<string, any>)[key];
123
- const mergedVal = merged[key];
124
- const expectedType = Array.isArray(defaultVal) ? "array" : defaultVal === null ? "null" : typeof defaultVal;
125
- const actualType = Array.isArray(mergedVal) ? "array" : mergedVal === null ? "null" : typeof mergedVal;
126
-
127
- if (expectedType === "object" && actualType === "object") {
128
- // 嵌套对象暂不深度校验
129
- } else if (expectedType !== actualType) {
130
- errors.push({
131
- section,
132
- key,
133
- expectedType,
134
- actualType,
135
- description: `配置 "${section}.${key}" 类型错误:期望 ${expectedType},实际 ${actualType}`,
136
- });
137
- }
138
- }
139
- }
140
-
141
- // 有类型错误 → 不写入
142
- if (errors.length > 0) {
143
- return { config: { ...defaults, ...current } as T, errors };
144
- }
145
- }
146
-
147
- // 4. 备份
148
- let backupPath: string | undefined;
149
- if (shouldBackup && backupDir && existsSync(settingsPath)) {
150
- backupPath = createBackup(settingsPath, backupDir);
151
- rotateBackups(backupDir, maxBackups);
152
- }
153
-
154
- // 5. 加锁写入(与 pi SettingsManager 互斥)
155
- if (settingsPath) {
156
- withFileLock(settingsPath, () => {
157
- // 重新读取(可能在等锁期间被其他进程修改)
158
- const latest = readFull(settingsPath);
159
- latest[section] = merged;
160
- writeFull(settingsPath, latest);
161
- });
162
- } else {
163
- settings[section] = merged;
164
- writeFull(settingsPath, settings);
165
- }
166
-
167
- // 6. 返回合并后的配置
168
- const finalConfig = { ...defaults, ...merged } as T;
169
- return { config: finalConfig, errors, backupPath };
170
- }
171
-
172
- // rollbackSettings / listBackups 已拆分到 settings-backup-rollback.ts,通过 re-export 暴露
@@ -1,104 +0,0 @@
1
- /**
2
- * 通用 settings.json 读写工具
3
- *
4
- * 所有扩展共享同一份 ~/.pi/agent/settings.json,通过 namespace 区分各扩展的配置段。
5
- * 用法:
6
- * import { getSettingsSection, patchSettingsSection } from "@pi-atelier/shared-utils";
7
- * const cfg = getSettingsSection("context", defaults);
8
- * patchSettingsSection("context", { distillThreshold: 3000 });
9
- */
10
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
11
- import { homedir } from "node:os";
12
- import { join } from "node:path";
13
-
14
- const SETTINGS_PATH = join(homedir(), ".pi/agent/settings.json");
15
-
16
- function readFull(): Record<string, any> {
17
- try {
18
- if (!existsSync(SETTINGS_PATH)) return {};
19
- return JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
20
- } catch {
21
- return {};
22
- }
23
- }
24
-
25
- function writeFull(settings: Record<string, any>): void {
26
- writeFileSync(SETTINGS_PATH, `${JSON.stringify(settings, null, "\t")}\n`);
27
- }
28
-
29
- /**
30
- * 读取 settings.json 中某个命名空间的配置,与默认值合并
31
- *
32
- * @param section - settings.json 中的顶层 key,如 "context"、"smartContext"
33
- * @param defaults - 该段的默认值(完整对象)
34
- * @returns 合并后的配置(defaults 中存在的字段用 settings 覆盖,缺失则用 defaults 填充)
35
- */
36
- export function getSettingsSection<T extends Record<string, any>>(section: string, defaults: T): T {
37
- const settings = readFull();
38
- const stored = settings?.[section] ?? {};
39
- const result: Record<string, any> = {};
40
- for (const key of Object.keys(defaults)) {
41
- result[key] = key in stored ? stored[key] : (defaults as Record<string, any>)[key];
42
- }
43
- return result as T;
44
- }
45
-
46
- /**
47
- * 增量更新 settings.json 中某个命名空间的配置
48
- *
49
- * 只修改 patch 中指定的字段,其余字段保持不变。写入后返回合并后的完整配置。
50
- *
51
- * @param section - settings.json 中的顶层 key
52
- * @param patch - 要修改的字段(部分对象)
53
- * @returns 更新后的完整配置
54
- */
55
- export function patchSettingsSection<T extends Record<string, any>>(
56
- section: string,
57
- patch: Partial<T>,
58
- defaults: T,
59
- ): T {
60
- const settings = readFull();
61
- const current = settings[section] ?? {};
62
- for (const [key, value] of Object.entries(patch)) {
63
- if (value !== undefined) current[key] = value;
64
- }
65
- settings[section] = current;
66
- writeFull(settings);
67
- return getSettingsSection(section, defaults);
68
- }
69
-
70
- /**
71
- * 读取 settings.json 中某个叶子字段的值(如 recording.enabled)
72
- *
73
- * @param path - 点分隔的路径,如 "recording.enabled"
74
- * @param fallback - 字段不存在时的默认值
75
- */
76
- export function getSettingsValue<T>(path: string, fallback: T): T {
77
- const settings = readFull();
78
- const keys = path.split(".");
79
- let current: unknown = settings;
80
- for (const key of keys) {
81
- if (current == null || typeof current !== "object") return fallback;
82
- current = (current as Record<string, unknown>)[key];
83
- }
84
- return current !== undefined ? (current as T) : fallback;
85
- }
86
-
87
- /**
88
- * 设置 settings.json 中某个叶子字段的值
89
- *
90
- * @param path - 点分隔的路径,如 "recording.enabled"
91
- * @param value - 要写入的值
92
- */
93
- export function setSettingsValue<T>(path: string, value: T): void {
94
- const settings = readFull();
95
- const keys = path.split(".");
96
- let current: Record<string, unknown> = settings;
97
- for (let i = 0; i < keys.length - 1; i++) {
98
- const key = keys[i];
99
- if (current[key] == null || typeof current[key] !== "object") current[key] = {};
100
- current = current[key] as Record<string, unknown>;
101
- }
102
- current[keys[keys.length - 1]] = value;
103
- writeFull(settings);
104
- }
@@ -1,149 +0,0 @@
1
- /**
2
- * Tool Output Protection — 共享的工具输出截断模块
3
- *
4
- * 为所有自注册工具提供统一的输出保护:
5
- * - 超限时截断输出,保留头部内容
6
- * - 完整内容写入临时文件,供 AI 按需读取
7
- * - 附加截断提示,告诉 AI 如何获取完整内容
8
- *
9
- * 使用方式:
10
- * import { truncatedResult } from "../_shared/tool-output";
11
- * return truncatedResult(text, { toolName: "session_search" });
12
- */
13
-
14
- import { createHash } from "node:crypto";
15
- import { mkdirSync, writeFileSync } from "node:fs";
16
- import { tmpdir } from "node:os";
17
- import { join } from "node:path";
18
- import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead } from "@earendil-works/pi-coding-agent";
19
-
20
- // ── 配置 ─────────────────────────────────────────────────
21
-
22
- export const TOOL_OUTPUT_MAX_LINES = DEFAULT_MAX_LINES; // 2000
23
- export const TOOL_OUTPUT_MAX_BYTES = DEFAULT_MAX_BYTES; // 50KB
24
-
25
- // ── 类型 ─────────────────────────────────────────────────
26
-
27
- export interface TruncationOptions {
28
- maxLines?: number;
29
- maxBytes?: number;
30
- toolName?: string;
31
- label?: string;
32
- }
33
-
34
- export interface ToolOutputResult {
35
- content: string;
36
- truncated: boolean;
37
- totalLines: number;
38
- totalBytes: number;
39
- outputLines: number;
40
- outputBytes: number;
41
- savedPath?: string;
42
- }
43
-
44
- // ── 核心函数 ─────────────────────────────────────────────
45
-
46
- /**
47
- * 截断工具输出,超限时写入临时文件并返回截断内容 + 位置提示。
48
- *
49
- * 对 SDK truncateHead() 的包装,追加:
50
- * - 自动写临时文件
51
- * - 附加截断提示文本
52
- * - 错误 fallback
53
- */
54
- export function truncateToolOutput(text: string, options: TruncationOptions = {}): ToolOutputResult {
55
- const maxLines = options.maxLines ?? TOOL_OUTPUT_MAX_LINES;
56
- const maxBytes = options.maxBytes ?? TOOL_OUTPUT_MAX_BYTES;
57
-
58
- // SDK truncateHead 处理截断逻辑
59
- const result = truncateHead(text, { maxLines, maxBytes });
60
-
61
- if (!result.truncated) {
62
- return {
63
- content: text,
64
- truncated: false,
65
- totalLines: result.totalLines,
66
- totalBytes: result.totalBytes,
67
- outputLines: result.outputLines,
68
- outputBytes: result.outputBytes,
69
- };
70
- }
71
-
72
- // 超限 — 写完整内容到临时文件
73
- const toolName = options.toolName || "tool";
74
- const hash = createHash("sha256").update(text).digest("hex").slice(0, 12);
75
- const fileName = `${toolName}-${hash}.txt`;
76
- const toolDir = join(tmpdir(), "pi-tool-output");
77
- let savedPath: string | undefined;
78
-
79
- try {
80
- mkdirSync(toolDir, { recursive: true });
81
- savedPath = join(toolDir, fileName);
82
- writeFileSync(savedPath, text, "utf-8");
83
- } catch {
84
- // 写入失败时 fallback — 只截断不保存,不抛异常
85
- savedPath = undefined;
86
- }
87
-
88
- // 附加截断提示
89
- const label = options.label || toolName;
90
- let notice = `\n\n[输出已截断: ${label}] 显示 ${result.outputLines}/${result.totalLines} 行`;
91
- notice += ` (${formatSize(result.outputBytes)}/${formatSize(result.totalBytes)})`;
92
- if (savedPath) {
93
- notice += `。完整内容: ${savedPath}]`;
94
- notice += `\n提示: 用 read 工具的 offset/limit 参数精确读取需要的行范围。`;
95
- } else {
96
- notice += `。完整内容未能保存到临时文件。]`;
97
- }
98
-
99
- return {
100
- content: result.content + notice,
101
- truncated: true,
102
- totalLines: result.totalLines,
103
- totalBytes: result.totalBytes,
104
- outputLines: result.outputLines,
105
- outputBytes: result.outputBytes,
106
- savedPath,
107
- };
108
- }
109
-
110
- // ── 便捷封装 ─────────────────────────────────────────────
111
-
112
- /**
113
- * 便捷封装:截断 + 包装成 pi 工具需要的返回格式。
114
- *
115
- * 返回 { content: [{ type, text }], details: { truncation } }
116
- * 支持传入 existingDetails,截断信息会合并进去。
117
- */
118
- export function truncatedResult(
119
- text: string,
120
- options: TruncationOptions = {},
121
- existingDetails?: Record<string, unknown>,
122
- ): {
123
- content: Array<{ type: "text"; text: string }>;
124
- details: Record<string, unknown>;
125
- } {
126
- const result = truncateToolOutput(text, options);
127
-
128
- const details: Record<string, unknown> = {
129
- ...existingDetails,
130
- };
131
-
132
- if (result.truncated) {
133
- details.truncation = {
134
- truncated: true,
135
- totalLines: result.totalLines,
136
- totalBytes: result.totalBytes,
137
- outputLines: result.outputLines,
138
- outputBytes: result.outputBytes,
139
- savedPath: result.savedPath,
140
- };
141
- } else {
142
- details.truncation = null;
143
- }
144
-
145
- return {
146
- content: [{ type: "text", text: result.content }],
147
- details,
148
- };
149
- }
@@ -1,9 +0,0 @@
1
- {
2
- "extends": "../tsconfig.base.json",
3
- "compilerOptions": {
4
- "outDir": "dist",
5
- "rootDir": "src",
6
- "types": ["node"]
7
- },
8
- "include": ["src/**/*.ts"]
9
- }
@@ -1,24 +0,0 @@
1
- import { defineConfig } from "vitest/config";
2
- import path from "node:path";
3
-
4
- const ROOT = path.resolve(__dirname, "../..");
5
-
6
- export default defineConfig({
7
- resolve: {
8
- alias: {
9
- "pi-shared-utils": path.resolve(ROOT, "packages/pi-shared-utils/src/index"),
10
- "@pi-atelier/shared-utils": path.resolve(ROOT, "packages/pi-shared-utils/src/index"),
11
- },
12
- },
13
- test: {
14
- include: ["src/__tests__/**/*.test.ts"],
15
- environment: "node",
16
- testTimeout: 10000,
17
- fileParallelism: false,
18
- coverage: {
19
- provider: "v8",
20
- reporter: ["text", "html"],
21
- exclude: ["src/index.ts", "vitest.config.ts", "src/__tests__/**"],
22
- },
23
- },
24
- });