pi-cc-extensions 0.1.3 → 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.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  <p align="center">
6
6
  <a href="#安装"><img alt="Pi package" src="https://img.shields.io/badge/Pi-package-58B7FF?style=flat-square"></a>
7
- <a href="./package.json"><img alt="Version 0.1.1" src="https://img.shields.io/badge/version-0.1.1-66E3C4?style=flat-square"></a>
7
+ <a href="./package.json"><img alt="Version 0.1.3" src="https://img.shields.io/badge/version-0.1.3-66E3C4?style=flat-square"></a>
8
8
  <a href="#兼容性"><img alt="Node.js 22.19 or newer" src="https://img.shields.io/badge/Node.js-%E2%89%A522.19-66E3C4?style=flat-square"></a>
9
9
  <a href="./extensions"><img alt="TypeScript extensions" src="https://img.shields.io/badge/TypeScript-extensions-3178C6?style=flat-square"></a>
10
10
  </p>
@@ -97,7 +97,13 @@ Session 模糊查询默认显示 5 个候选;若已加载 `pi-subagents`,也
97
97
 
98
98
  ## 安装
99
99
 
100
- 从 Git 安装:
100
+ 从 npm 安装(推荐):
101
+
102
+ ```bash
103
+ pi install npm:pi-cc-extensions
104
+ ```
105
+
106
+ 或从 Git 安装:
101
107
 
102
108
  ```bash
103
109
  pi install git:github.com/minuque/pi-cc-extensions
@@ -0,0 +1,175 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
5
+ import {
6
+ type AutocompleteItem,
7
+ type AutocompleteProvider,
8
+ type AutocompleteSuggestions,
9
+ fuzzyFilter,
10
+ } from "@earendil-works/pi-tui";
11
+
12
+ type AgentInfo = {
13
+ name: string;
14
+ displayName: string;
15
+ description: string;
16
+ model?: string;
17
+ filePath: string;
18
+ };
19
+
20
+ const MAX_SUGGESTIONS = 10;
21
+ // Match `@name` but NOT `@session:` (reserved by session-reference extension).
22
+ const AGENT_NAME_PATTERN = /(?:^|[\t ])@([^\s:@][^\s:]*)$/;
23
+
24
+ function getAgentDir(): string {
25
+ const override = process.env.PI_CODING_AGENT_DIR;
26
+ return join(override ? override : homedir(), ".pi", "agent", "agents");
27
+ }
28
+
29
+ function parseFrontmatter(content: string): Record<string, string> {
30
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
31
+ if (!match) return {};
32
+ const result: Record<string, string> = {};
33
+ for (const line of match[1]!.split("\n")) {
34
+ const kv = line.match(/^(\w+):\s*(.*)$/);
35
+ if (kv) result[kv[1]!] = kv[2]!.trim();
36
+ }
37
+ return result;
38
+ }
39
+
40
+ function loadAgents(): AgentInfo[] {
41
+ const dir = getAgentDir();
42
+ if (!existsSync(dir)) return [];
43
+
44
+ return readdirSync(dir)
45
+ .filter((f) => f.endsWith(".md"))
46
+ .map((file) => {
47
+ const filePath = join(dir, file);
48
+ const content = readFileSync(filePath, "utf-8");
49
+ const fm = parseFrontmatter(content);
50
+ const name = file.replace(/\.md$/, "");
51
+ return {
52
+ name,
53
+ displayName: fm.display_name || name,
54
+ description: fm.description || "",
55
+ model: fm.model,
56
+ filePath,
57
+ };
58
+ });
59
+ }
60
+
61
+ function createAgentAutocompleteProvider(
62
+ current: AutocompleteProvider,
63
+ getAgents: () => AgentInfo[],
64
+ ): AutocompleteProvider {
65
+ return {
66
+ async getSuggestions(
67
+ lines: string[],
68
+ cursorLine: number,
69
+ cursorCol: number,
70
+ options: { signal: AbortSignal; force?: boolean },
71
+ ): Promise<AutocompleteSuggestions | null> {
72
+ const currentLine = lines[cursorLine] ?? "";
73
+ const textBeforeCursor = currentLine.slice(0, cursorCol);
74
+ const match = textBeforeCursor.match(AGENT_NAME_PATTERN);
75
+
76
+ if (!match) {
77
+ return current.getSuggestions(lines, cursorLine, cursorCol, options);
78
+ }
79
+
80
+ const query = match[1]!;
81
+ const agents = getAgents();
82
+
83
+ if (options.signal.aborted || agents.length === 0) {
84
+ return current.getSuggestions(lines, cursorLine, cursorCol, options);
85
+ }
86
+
87
+ const matches = query.trim()
88
+ ? fuzzyFilter(agents, query, (a) => `${a.name} ${a.displayName} ${a.description}`)
89
+ : agents;
90
+
91
+ const items: AutocompleteItem[] = matches
92
+ .slice(0, MAX_SUGGESTIONS)
93
+ .map((agent) => ({
94
+ value: `@${agent.name}`,
95
+ label: `[SubAgent] ${agent.displayName}`,
96
+ description: agent.description
97
+ + (agent.model ? ` · ${agent.model}` : ""),
98
+ }));
99
+
100
+ if (items.length === 0) {
101
+ return current.getSuggestions(lines, cursorLine, cursorCol, options);
102
+ }
103
+
104
+ return { items, prefix: `@${query}` };
105
+ },
106
+
107
+ applyCompletion(
108
+ lines: string[],
109
+ cursorLine: number,
110
+ cursorCol: number,
111
+ item: AutocompleteItem,
112
+ prefix: string,
113
+ ) {
114
+ return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
115
+ },
116
+
117
+ shouldTriggerFileCompletion(lines: string[], cursorLine: number, cursorCol: number) {
118
+ return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;
119
+ },
120
+ };
121
+ }
122
+
123
+ export default function agentAutocompleteExtension(pi: ExtensionAPI): void {
124
+ let cachedAgents: AgentInfo[] | undefined;
125
+
126
+ const getAgents = (): AgentInfo[] => {
127
+ if (!cachedAgents) {
128
+ cachedAgents = loadAgents();
129
+ }
130
+ return cachedAgents;
131
+ };
132
+
133
+ // Reload agents on /reload
134
+ pi.on("resources_discover", () => {
135
+ cachedAgents = undefined;
136
+ });
137
+
138
+ pi.on("session_start", (_event, ctx) => {
139
+ const agents = getAgents();
140
+ if (agents.length === 0 || ctx.mode !== "tui") return;
141
+ ctx.ui.addAutocompleteProvider((current) =>
142
+ createAgentAutocompleteProvider(current, getAgents),
143
+ );
144
+ });
145
+
146
+ // Inject instruction when user types @agent-name (not @session:) in prompt.
147
+ // Supports multiple different subagents in a single prompt.
148
+ const AGENT_PROMPT_PATTERN = /(?:^|[\s])@([^\s:@][^\s:]*)/g;
149
+ pi.on("before_agent_start", async (event, _ctx) => {
150
+ const agents = getAgents();
151
+ if (agents.length === 0) return;
152
+
153
+ const mentions: string[] = [];
154
+ for (const m of event.prompt.matchAll(AGENT_PROMPT_PATTERN)) {
155
+ const name = m[1]!;
156
+ if (agents.some((a) => a.name === name) && !mentions.includes(name)) {
157
+ mentions.push(name);
158
+ }
159
+ }
160
+ if (mentions.length === 0) return;
161
+
162
+ const agentMap = new Map(agents.map((a) => [a.name, a]));
163
+ const agentList = mentions.map((n) => `"${n}" (${agentMap.get(n)!.displayName})`).join(", ");
164
+ const agentTypes = mentions.map((n) => `"${n}"`).join(" | ");
165
+
166
+ return {
167
+ systemPrompt:
168
+ event.systemPrompt
169
+ + `\n\nThe user's prompt references these subagent types: ${agentList}. `
170
+ + `You MUST use the Agent tool for EACH mentioned subagent to delegate the relevant parts of the request. `
171
+ + `Handle different subagents separately — do NOT merge their tasks into a single Agent call. `
172
+ + `For example, if the user mentions @coder and @explore, make two separate Agent tool calls, one with subagent_type="coder" and another with subagent_type="explore".`,
173
+ };
174
+ });
175
+ }
@@ -1,5 +1,3 @@
1
- import { Buffer } from "node:buffer";
2
-
3
1
  export const SESSION_REFERENCE_CUSTOM_TYPE = "session-reference";
4
2
  export const SESSION_REFERENCE_PREFIX = "@session:";
5
3
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cc-extensions",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "Personal extensions for the Pi coding agent",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -16,15 +16,18 @@
16
16
  "extensions": [
17
17
  "./extensions/claude-code-style.ts",
18
18
  "./extensions/context.ts",
19
- "./extensions/session-reference/index.ts"
19
+ "./extensions/session-reference/index.ts",
20
+ "./extensions/agent-autocomplete.ts"
20
21
  ]
21
22
  },
22
23
  "scripts": {
23
24
  "test": "node --test tests/**/*.test.ts",
24
25
  "typecheck": "node scripts/typecheck.mjs",
25
- "prepublishOnly": "npm run typecheck && npm test",
26
- "postversion": "git push && git push --tags",
27
- "release": "npm publish"
26
+ "preversion": "npm run typecheck && npm test",
27
+ "release": "npm version patch && npm publish",
28
+ "release:minor": "npm version minor && npm publish",
29
+ "release:major": "npm version major && npm publish",
30
+ "postpublish": "git push && git push --tags"
28
31
  },
29
32
  "peerDependencies": {
30
33
  "@earendil-works/pi-ai": "*",