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.
|
|
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
|
-
从
|
|
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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cc-extensions",
|
|
3
|
-
"version": "0.
|
|
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
|
-
"
|
|
26
|
-
"
|
|
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": "*",
|