pi-resource-search 0.1.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/LICENSE +21 -0
- package/README.md +33 -0
- package/extensions/index.ts +179 -0
- package/package.json +37 -0
- package/tsconfig.json +11 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pi-resource-search contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# pi-resource-search
|
|
2
|
+
|
|
3
|
+
Pi 扩展:在输入 `/关键词` 时搜索当前可用的命令、Skill、Extension 和已配置 Package。
|
|
4
|
+
|
|
5
|
+
## 使用
|
|
6
|
+
|
|
7
|
+
安装或通过本地路径加载后,执行 `/reload`,在输入框中输入:
|
|
8
|
+
|
|
9
|
+
- `/hand`:查找 handoff 等相关资源
|
|
10
|
+
- `/cache`:查找缓存相关资源
|
|
11
|
+
- `/review`:查找审查相关资源
|
|
12
|
+
|
|
13
|
+
选择 Skill 会插入 `/skill:名称`。Package 和 Extension 结果仅用于识别,不会自动安装或执行。
|
|
14
|
+
|
|
15
|
+
## 特点
|
|
16
|
+
|
|
17
|
+
- 使用 Pi 原生 autocomplete provider,不修改 Pi 核心。
|
|
18
|
+
- 仅读取配置、Package 清单和 Skill frontmatter。
|
|
19
|
+
- 支持 Windows、macOS 和 Linux 的用户目录。
|
|
20
|
+
- 不执行被扫描 Package 的代码,不写入 Skill 或 Package 文件。
|
|
21
|
+
|
|
22
|
+
## 本地测试
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pi -e .
|
|
26
|
+
npm install
|
|
27
|
+
npm run typecheck
|
|
28
|
+
npm pack --dry-run
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 兼容性
|
|
32
|
+
|
|
33
|
+
需要 Pi `>=0.85.0`。
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import {
|
|
6
|
+
fuzzyFilter,
|
|
7
|
+
type AutocompleteItem,
|
|
8
|
+
type AutocompleteProvider,
|
|
9
|
+
type AutocompleteSuggestions,
|
|
10
|
+
} from "@earendil-works/pi-tui";
|
|
11
|
+
|
|
12
|
+
type Resource = {
|
|
13
|
+
kind: "Command" | "Skill" | "Extension" | "Package";
|
|
14
|
+
name: string;
|
|
15
|
+
description: string;
|
|
16
|
+
value: string;
|
|
17
|
+
executable: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const MAX_RESULTS = 30;
|
|
21
|
+
const INFO_PREFIX = "__pi_resource_info__:";
|
|
22
|
+
const agentDir = process.env.PI_CODING_AGENT_DIR || join(homedir(), ".pi", "agent");
|
|
23
|
+
|
|
24
|
+
function readJson(path: string): any | undefined {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
27
|
+
} catch {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readSkill(path: string): Resource | undefined {
|
|
33
|
+
try {
|
|
34
|
+
const text = readFileSync(path, "utf8").replace(/^\uFEFF/, "");
|
|
35
|
+
const frontmatter = text.match(/^---\s*[\r\n]+([\s\S]*?)[\r\n]+---/m)?.[1];
|
|
36
|
+
if (!frontmatter) return undefined;
|
|
37
|
+
const name = frontmatter.match(/^name:\s*["']?([^"'\r\n]+)["']?\s*$/m)?.[1]?.trim();
|
|
38
|
+
const description = frontmatter.match(/^description:\s*["']?([^"'\r\n]+)["']?\s*$/m)?.[1]?.trim();
|
|
39
|
+
if (!name || !description) return undefined;
|
|
40
|
+
return { kind: "Skill", name, description, value: `/skill:${name}`, executable: true };
|
|
41
|
+
} catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function collectSkills(root: string, output: Resource[], seen: Set<string>): void {
|
|
47
|
+
let entries: Array<{ name: string; isDirectory: () => boolean }>;
|
|
48
|
+
try {
|
|
49
|
+
entries = readdirSync(root, { withFileTypes: true, encoding: "utf8" }) as unknown as Array<{
|
|
50
|
+
name: string;
|
|
51
|
+
isDirectory: () => boolean;
|
|
52
|
+
}>;
|
|
53
|
+
} catch {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
const path = join(root, entry.name);
|
|
58
|
+
if (entry.isDirectory()) {
|
|
59
|
+
if (!entry.name.startsWith(".")) collectSkills(path, output, seen);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (entry.name !== "SKILL.md") continue;
|
|
63
|
+
const skill = readSkill(path);
|
|
64
|
+
if (skill && !seen.has(skill.name)) {
|
|
65
|
+
seen.add(skill.name);
|
|
66
|
+
output.push(skill);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function packageRoot(source: string): string | undefined {
|
|
72
|
+
if (source.startsWith("npm:")) {
|
|
73
|
+
const spec = source.slice(4).match(/^(@[^/]+\/[^@]+|[^@]+)/)?.[1];
|
|
74
|
+
return spec ? join(agentDir, "npm", "node_modules", spec) : undefined;
|
|
75
|
+
}
|
|
76
|
+
if (source.startsWith("git:") || source.startsWith("http:")) return undefined;
|
|
77
|
+
return resolve(agentDir, source.startsWith("file:") ? source.slice(5) : source);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function collectPackage(source: string, output: Resource[], seen: Set<string>): void {
|
|
81
|
+
const root = packageRoot(source);
|
|
82
|
+
if (!root) return;
|
|
83
|
+
const manifest = readJson(join(root, "package.json"));
|
|
84
|
+
if (!manifest?.name || seen.has(manifest.name)) return;
|
|
85
|
+
seen.add(manifest.name);
|
|
86
|
+
output.push({
|
|
87
|
+
kind: "Package",
|
|
88
|
+
name: manifest.name,
|
|
89
|
+
description: manifest.description || "已配置的 Pi 包(仅信息)",
|
|
90
|
+
value: `${INFO_PREFIX}package:${manifest.name}`,
|
|
91
|
+
executable: false,
|
|
92
|
+
});
|
|
93
|
+
const packageManifest = manifest.pi || manifest;
|
|
94
|
+
for (const extension of Array.isArray(packageManifest.extensions) ? packageManifest.extensions : []) {
|
|
95
|
+
const name = String(extension).replace(/^\.\//, "").replace(/\.(ts|js)$/, "");
|
|
96
|
+
output.push({
|
|
97
|
+
kind: "Extension",
|
|
98
|
+
name,
|
|
99
|
+
description: `${manifest.name} · 扩展资源(仅信息)`,
|
|
100
|
+
value: `${INFO_PREFIX}extension:${manifest.name}/${name}`,
|
|
101
|
+
executable: false,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
for (const skillPath of Array.isArray(packageManifest.skills) ? packageManifest.skills : []) {
|
|
105
|
+
collectSkills(join(root, String(skillPath)), output, new Set<string>());
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function loadResources(): Resource[] {
|
|
110
|
+
const resources: Resource[] = [];
|
|
111
|
+
collectSkills(join(agentDir, "skills"), resources, new Set<string>());
|
|
112
|
+
collectSkills(join(homedir(), ".agents", "skills"), resources, new Set<string>());
|
|
113
|
+
const settings = readJson(join(agentDir, "settings.json"));
|
|
114
|
+
const packages = Array.isArray(settings?.packages) ? settings.packages : [];
|
|
115
|
+
for (const entry of packages) {
|
|
116
|
+
const source = typeof entry === "string" ? entry : String(entry?.source || "");
|
|
117
|
+
collectPackage(source, resources, new Set<string>());
|
|
118
|
+
}
|
|
119
|
+
return resources;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function queryToken(text: string): string | undefined {
|
|
123
|
+
return text.match(/(?:^|[ \t])\/([^\s/]*)$/)?.[1];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function toItem(resource: Resource): AutocompleteItem {
|
|
127
|
+
return { value: resource.value, label: `${resource.kind} ${resource.name}`, description: resource.description };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function createProvider(pi: ExtensionAPI, current: AutocompleteProvider, resources: Resource[]): AutocompleteProvider {
|
|
131
|
+
return {
|
|
132
|
+
triggerCharacters: ["/"],
|
|
133
|
+
async getSuggestions(lines, cursorLine, cursorCol, options): Promise<AutocompleteSuggestions | null> {
|
|
134
|
+
const before = (lines[cursorLine] || "").slice(0, cursorCol);
|
|
135
|
+
const query = queryToken(before);
|
|
136
|
+
if (query === undefined || query.length === 0) return current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
137
|
+
|
|
138
|
+
const commands: Resource[] = [];
|
|
139
|
+
try {
|
|
140
|
+
for (const command of pi.getCommands()) {
|
|
141
|
+
commands.push({ kind: "Command", name: command.name, description: command.description || "可执行命令", value: `/${command.name}`, executable: true });
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
// 某些 Pi 运行模式不提供命令清单,仍保留 Skill/Package 搜索。
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const all = [...commands, ...resources];
|
|
148
|
+
const matches = fuzzyFilter(all, query, (item) => `${item.name} ${item.description} ${item.kind}`).slice(0, MAX_RESULTS);
|
|
149
|
+
if (options.signal.aborted) return null;
|
|
150
|
+
const native = await current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
151
|
+
if (options.signal.aborted) return null;
|
|
152
|
+
const nativeItems = native?.items || [];
|
|
153
|
+
const seen = new Set(nativeItems.map((item) => item.value));
|
|
154
|
+
const extra = matches.filter((item) => !seen.has(item.value)).map(toItem);
|
|
155
|
+
return extra.length > 0 ? { items: [...extra, ...nativeItems].slice(0, MAX_RESULTS), prefix: `/${query}` } : native;
|
|
156
|
+
},
|
|
157
|
+
applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
|
|
158
|
+
if (item.value.startsWith(INFO_PREFIX)) return { lines, cursorLine, cursorCol };
|
|
159
|
+
return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
|
|
160
|
+
},
|
|
161
|
+
shouldTriggerFileCompletion: (lines, cursorLine, cursorCol) => current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export default function (pi: ExtensionAPI): void {
|
|
166
|
+
let resources: Resource[] = [];
|
|
167
|
+
let providerInstalled = false;
|
|
168
|
+
pi.on("session_start", async () => {
|
|
169
|
+
resources = loadResources();
|
|
170
|
+
providerInstalled = false;
|
|
171
|
+
});
|
|
172
|
+
pi.on("resources_discover", async (_event, ctx) => {
|
|
173
|
+
resources = loadResources();
|
|
174
|
+
if (!providerInstalled) {
|
|
175
|
+
ctx.ui.addAutocompleteProvider((current) => createProvider(pi, current, resources));
|
|
176
|
+
providerInstalled = true;
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-resource-search",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Search Pi commands, skills, extensions, and installed packages from /keyword autocomplete.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi",
|
|
8
|
+
"extension",
|
|
9
|
+
"skills",
|
|
10
|
+
"autocomplete"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"files": [
|
|
15
|
+
"extensions",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE",
|
|
18
|
+
"tsconfig.json"
|
|
19
|
+
],
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"@earendil-works/pi-coding-agent": ">=0.85.0",
|
|
22
|
+
"@earendil-works/pi-tui": ">=0.85.0"
|
|
23
|
+
},
|
|
24
|
+
"pi": {
|
|
25
|
+
"extensions": [
|
|
26
|
+
"./extensions/index.ts"
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"typecheck": "tsc --noEmit"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"install": "^0.13.0",
|
|
34
|
+
"typescript": "^7.0.2",
|
|
35
|
+
"typescriptnpm": "^1.0.1"
|
|
36
|
+
}
|
|
37
|
+
}
|