@zhushanwen/pi-unified-hooks 0.2.1 → 0.2.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-unified-hooks",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Unified hooks extension - collect scattered hooks in one place for easy maintenance",
5
5
  "main": "index.ts",
6
6
  "type": "module",
@@ -16,7 +16,7 @@
16
16
  ],
17
17
  "license": "MIT",
18
18
  "dependencies": {
19
- "@zhushanwen/pi-extension-logger": "0.2.0"
19
+ "@zhushanwen/pi-extension-logger": "0.2.1"
20
20
  },
21
21
  "peerDependencies": {
22
22
  "@earendil-works/pi-coding-agent": "*"
@@ -18,15 +18,10 @@ vi.mock("../hooks/test-timeout-guard.ts", () => ({
18
18
  setupTestTimeoutGuard: vi.fn(),
19
19
  }));
20
20
 
21
- vi.mock("../hooks/subagent-list-injector.ts", () => ({
22
- setupSubagentListInjector: vi.fn(),
23
- }));
24
-
25
21
  // Re-import after mocking so the mocked versions are used
26
22
  import { setupToolErrorHandler } from "../hooks/tool-error-handler.ts";
27
23
  import { setupNetworkTimeoutGuard } from "../hooks/network-timeout-guard.ts";
28
24
  import { setupTestTimeoutGuard } from "../hooks/test-timeout-guard.ts";
29
- import { setupSubagentListInjector } from "../hooks/subagent-list-injector.ts";
30
25
 
31
26
  import unifiedHooksExtension from "../index.ts";
32
27
 
@@ -63,7 +58,6 @@ describe("session_start handler", () => {
63
58
  (setupToolErrorHandler as ReturnType<typeof vi.fn>).mockImplementation(() => {});
64
59
  (setupNetworkTimeoutGuard as ReturnType<typeof vi.fn>).mockImplementation(() => {});
65
60
  (setupTestTimeoutGuard as ReturnType<typeof vi.fn>).mockImplementation(() => {});
66
- (setupSubagentListInjector as ReturnType<typeof vi.fn>).mockImplementation(() => {});
67
61
 
68
62
  unifiedHooksExtension(pi as unknown as ExtensionAPI);
69
63
 
@@ -73,7 +67,7 @@ describe("session_start handler", () => {
73
67
  // 全成功时不 notify(避免刷屏),只 appendEntry
74
68
  expect(notify).not.toHaveBeenCalled();
75
69
  expect(pi.appendEntry).toHaveBeenCalledWith("unified-hooks:loaded", {
76
- enabled: ["tool-error-handler", "network-timeout-guard", "test-timeout-guard", "subagent-list-injector"],
70
+ enabled: ["tool-error-handler", "network-timeout-guard", "test-timeout-guard"],
77
71
  disabled: [],
78
72
  });
79
73
  });
@@ -90,7 +84,6 @@ describe("session_start handler", () => {
90
84
  (setupTestTimeoutGuard as ReturnType<typeof vi.fn>).mockImplementation(() => {
91
85
  throw new Error("timeout");
92
86
  });
93
- (setupSubagentListInjector as ReturnType<typeof vi.fn>).mockImplementation(() => {});
94
87
 
95
88
  unifiedHooksExtension(pi as unknown as ExtensionAPI);
96
89
 
@@ -102,7 +95,7 @@ describe("session_start handler", () => {
102
95
  expect(level).toBe("warning");
103
96
  expect(msg).toContain("Failed: network-timeout-guard, test-timeout-guard");
104
97
  expect(pi.appendEntry).toHaveBeenCalledWith("unified-hooks:loaded", {
105
- enabled: ["tool-error-handler", "subagent-list-injector"],
98
+ enabled: ["tool-error-handler"],
106
99
  disabled: ["network-timeout-guard", "test-timeout-guard"],
107
100
  });
108
101
  });
@@ -121,9 +114,6 @@ describe("session_start handler", () => {
121
114
  (setupTestTimeoutGuard as ReturnType<typeof vi.fn>).mockImplementation(() => {
122
115
  throw new Error("c");
123
116
  });
124
- (setupSubagentListInjector as ReturnType<typeof vi.fn>).mockImplementation(() => {
125
- throw new Error("d");
126
- });
127
117
 
128
118
  unifiedHooksExtension(pi as unknown as ExtensionAPI);
129
119
 
@@ -132,10 +122,10 @@ describe("session_start handler", () => {
132
122
 
133
123
  expect(notify.mock.calls[0]![1]).toBe("warning");
134
124
  const msg = notify.mock.calls[0]![0] as string;
135
- expect(msg).toContain("Failed: tool-error-handler, network-timeout-guard, test-timeout-guard, subagent-list-injector");
125
+ expect(msg).toContain("Failed: tool-error-handler, network-timeout-guard, test-timeout-guard");
136
126
  expect(pi.appendEntry).toHaveBeenCalledWith("unified-hooks:loaded", {
137
127
  enabled: [],
138
- disabled: ["tool-error-handler", "network-timeout-guard", "test-timeout-guard", "subagent-list-injector"],
128
+ disabled: ["tool-error-handler", "network-timeout-guard", "test-timeout-guard"],
139
129
  });
140
130
  });
141
131
 
package/src/index.ts CHANGED
@@ -11,7 +11,6 @@ import { getLogger, setPiHandle } from "@zhushanwen/pi-extension-logger";
11
11
  // Re-export hook modules for easy access
12
12
 
13
13
  import { setupNetworkTimeoutGuard } from "./hooks/network-timeout-guard";
14
- import { setupSubagentListInjector } from "./hooks/subagent-list-injector";
15
14
  import { setupTestTimeoutGuard } from "./hooks/test-timeout-guard";
16
15
  import { type HookContext, setupToolErrorHandler } from "./hooks/tool-error-handler";
17
16
 
@@ -34,7 +33,6 @@ export default function unifiedHooksExtension(pi: ExtensionAPI): void {
34
33
  { name: "tool-error-handler", setup: setupToolErrorHandler },
35
34
  { name: "network-timeout-guard", setup: setupNetworkTimeoutGuard },
36
35
  { name: "test-timeout-guard", setup: setupTestTimeoutGuard },
37
- { name: "subagent-list-injector", setup: setupSubagentListInjector },
38
36
  ];
39
37
 
40
38
  for (const hook of hookModules) {
@@ -1,179 +0,0 @@
1
- /**
2
- * Subagent List Injector Hook
3
- *
4
- * Discovers all available subagents (builtin + user + project scope) and
5
- * injects their names and descriptions into the system prompt on every turn,
6
- * so the AI model can pick the correct agent name instead of fabricating one.
7
- *
8
- * Injection format mirrors Pi's built-in skill injection (XML tags).
9
- */
10
-
11
- import * as fs from "node:fs";
12
- import * as os from "node:os";
13
- import * as path from "node:path";
14
-
15
- import { getLogger } from "@zhushanwen/pi-extension-logger";
16
-
17
- const logger = getLogger("unified-hooks");
18
-
19
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
20
-
21
- /** Minimal agent info extracted from .md frontmatter */
22
- interface AgentEntry {
23
- name: string;
24
- description: string;
25
- }
26
-
27
- /**
28
- * Parse YAML frontmatter from a markdown file.
29
- * Returns null if the file has no valid frontmatter or missing name/description.
30
- */
31
- function parseAgentFrontmatter(content: string): AgentEntry | null {
32
- if (!content.startsWith("---")) return null;
33
-
34
- const FRONTMATTER_OPEN_LEN = 3;
35
- const endIndex = content.indexOf("\n---", FRONTMATTER_OPEN_LEN);
36
- if (endIndex === -1) return null;
37
-
38
- const block = content.slice(FRONTMATTER_OPEN_LEN, endIndex);
39
- let name = "";
40
- let description = "";
41
-
42
- for (const line of block.split("\n")) {
43
- const match = line.match(/^([\w-]+):\s*(.*)$/);
44
- if (!match) continue;
45
-
46
- const key = match[1]!;
47
- let value = match[2]!.trim();
48
- // Strip surrounding quotes
49
- if (
50
- (value.startsWith('"') && value.endsWith('"')) ||
51
- (value.startsWith("'") && value.endsWith("'"))
52
- ) {
53
- value = value.slice(1, -1);
54
- }
55
-
56
- if (key === "name") name = value;
57
- if (key === "description") description = value;
58
- }
59
-
60
- if (!name || !description) return null;
61
- return { name, description };
62
- }
63
-
64
- /** Read agent .md files from a directory, return parsed entries */
65
- function loadAgentsFromDir(dir: string): AgentEntry[] {
66
- if (!fs.existsSync(dir)) return [];
67
-
68
- const entries: AgentEntry[] = [];
69
- let dirents: fs.Dirent[];
70
- try {
71
- dirents = fs.readdirSync(dir, { withFileTypes: true });
72
- } catch {
73
- return [];
74
- }
75
-
76
- for (const entry of dirents) {
77
- if (!entry.name.endsWith(".md")) continue;
78
- if (entry.name.endsWith(".chain.md")) continue;
79
- if (!entry.isFile() && !entry.isSymbolicLink()) continue;
80
-
81
- const filePath = path.join(dir, entry.name);
82
- try {
83
- const content = fs.readFileSync(filePath, "utf8");
84
- const agent = parseAgentFrontmatter(content);
85
- if (agent) {
86
- entries.push(agent);
87
- }
88
- } catch (err) {
89
- // Individual file read failure should not block the entire agent list injection
90
- logger.error(`[subagent-list-injector] skip unreadable file ${filePath}`, {
91
- reason: err instanceof Error ? err.message : String(err),
92
- });
93
- }
94
- }
95
-
96
- return entries;
97
- }
98
-
99
- /** Escape special XML characters */
100
- function escapeXml(str: string): string {
101
- return str
102
- .replace(/&/g, "&amp;")
103
- .replace(/</g, "&lt;")
104
- .replace(/>/g, "&gt;")
105
- .replace(/"/g, "&quot;")
106
- .replace(/'/g, "&apos;");
107
- }
108
-
109
- /**
110
- * Discover all available agents across scopes.
111
- * Deduplicates by name: project > user > builtin.
112
- */
113
- function discoverAllAgents(cwd: string): AgentEntry[] {
114
- // Builtin agents from pi-subagents package
115
- const builtinDir = path.join(
116
- os.homedir(),
117
- ".pi/agent/npm/node_modules/pi-subagents/agents",
118
- );
119
-
120
- // User scope (both legacy and new paths)
121
- const userDirLegacy = path.join(os.homedir(), ".pi/agent/agents");
122
- const userDirNew = path.join(os.homedir(), ".agents");
123
-
124
- // Project scope (both legacy and new paths)
125
- const projectDirNew = path.join(cwd, ".pi/agents");
126
- const projectDirLegacy = path.join(cwd, ".agents");
127
-
128
- const agentMap = new Map<string, AgentEntry>();
129
-
130
- // Load in priority order: builtin first, then user overrides, then project overrides
131
- for (const agent of loadAgentsFromDir(builtinDir)) {
132
- agentMap.set(agent.name, agent);
133
- }
134
- for (const dir of [userDirLegacy, userDirNew]) {
135
- for (const agent of loadAgentsFromDir(dir)) {
136
- agentMap.set(agent.name, agent);
137
- }
138
- }
139
- for (const dir of [projectDirLegacy, projectDirNew]) {
140
- for (const agent of loadAgentsFromDir(dir)) {
141
- agentMap.set(agent.name, agent);
142
- }
143
- }
144
-
145
- return [...agentMap.values()];
146
- }
147
-
148
- /** Format agent list as XML injection block */
149
- function formatAgentList(agents: AgentEntry[]): string {
150
- if (agents.length === 0) return "";
151
-
152
- const lines = [
153
- "\n\n<available_subagents>",
154
- "The following agents are available for the subagent tool. When using the subagent tool, ONLY use agent names from this list. If no agent matches your task, pass systemPrompt alongside the agent name to create a dynamic agent.",
155
- ];
156
- for (const agent of agents) {
157
- lines.push(
158
- ` <agent><name>${escapeXml(agent.name)}</name><description>${escapeXml(agent.description)}</description></agent>`,
159
- );
160
- }
161
- lines.push("</available_subagents>");
162
- return lines.join("\n");
163
- }
164
-
165
- export function setupSubagentListInjector(pi: ExtensionAPI): void {
166
- pi.on(
167
- "before_agent_start",
168
- (event: unknown, _ctx: unknown) => {
169
- const e = event as { systemPrompt?: string };
170
- const cwd = process.cwd();
171
- const agents = discoverAllAgents(cwd);
172
- const injection = formatAgentList(agents);
173
-
174
- if (!injection) return;
175
-
176
- return { systemPrompt: (e.systemPrompt ?? "") + injection };
177
- },
178
- );
179
- }