@henryqw/pi-add-dir 0.1.10 → 0.1.12
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 +2 -1
- package/extensions/add-dir-helpers.ts +23 -30
- package/extensions/add-dir.ts +36 -9
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ pi install npm:@henryqw/pi-add-dir
|
|
|
19
19
|
| --- | --- | --- |
|
|
20
20
|
| `/dir-add` | command | Add directory; no path opens input. Supports `~`. |
|
|
21
21
|
| `/dir-ls` | command | List directories; select one to remove. |
|
|
22
|
+
| `/dir-reload` | command | Reload external directory resources. |
|
|
22
23
|
| `add_directory` | tool | Add a directory. |
|
|
23
24
|
| `search_external_files` | tool | Glob-search added directories. |
|
|
24
25
|
|
|
@@ -28,4 +29,4 @@ Search uses Node filesystem traversal, skips `.git` and `node_modules`, supports
|
|
|
28
29
|
|
|
29
30
|
## State
|
|
30
31
|
|
|
31
|
-
Pi session entry `add-dir:state` stores added directories; package-managed, do not edit.
|
|
32
|
+
Pi session entry `add-dir:state` stores added directories; package-managed, do not edit. Tree navigation restores the active branch's directories and reloads resources when that set changes.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { glob } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
|
|
@@ -18,6 +18,11 @@ const CONTEXT_FILES = ["AGENTS.md", "CLAUDE.md"] as const;
|
|
|
18
18
|
const SKILL_DIRS = [".pi/skills", ".agents/skills", ".claude/skills"] as const;
|
|
19
19
|
const SKIPPED_SEARCH_DIRS = new Set([".git", "node_modules"]);
|
|
20
20
|
|
|
21
|
+
function isMissingPathError(error: unknown): boolean {
|
|
22
|
+
if (!error || typeof error !== "object" || !("code" in error)) return false;
|
|
23
|
+
return error.code === "ENOENT" || error.code === "ENOTDIR";
|
|
24
|
+
}
|
|
25
|
+
|
|
21
26
|
export function expandUserPath(input: string): string {
|
|
22
27
|
if (input === "~") return homedir();
|
|
23
28
|
if (input.startsWith("~/") || input.startsWith(`~${path.sep}`)) return path.join(homedir(), input.slice(2));
|
|
@@ -29,16 +34,18 @@ export function resolveDir(input: string, cwd: string): string {
|
|
|
29
34
|
const resolved = path.isAbsolute(expanded) ? expanded : path.resolve(cwd, expanded);
|
|
30
35
|
try {
|
|
31
36
|
return realpathSync(resolved);
|
|
32
|
-
} catch {
|
|
33
|
-
return path.resolve(resolved);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (isMissingPathError(error)) return path.resolve(resolved);
|
|
39
|
+
throw error;
|
|
34
40
|
}
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
export function dirExists(dir: string): boolean {
|
|
38
44
|
try {
|
|
39
45
|
return statSync(dir).isDirectory();
|
|
40
|
-
} catch {
|
|
41
|
-
return false;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (isMissingPathError(error)) return false;
|
|
48
|
+
throw error;
|
|
42
49
|
}
|
|
43
50
|
}
|
|
44
51
|
|
|
@@ -63,7 +70,6 @@ function skillFiles(dir: string): Array<{ name: string; path: string }> {
|
|
|
63
70
|
|
|
64
71
|
for (const skillDir of SKILL_DIRS) {
|
|
65
72
|
const fullSkillDir = path.join(dir, skillDir);
|
|
66
|
-
if (!dirExists(fullSkillDir)) continue;
|
|
67
73
|
try {
|
|
68
74
|
for (const entry of readdirSync(fullSkillDir, { withFileTypes: true })) {
|
|
69
75
|
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
@@ -97,7 +103,6 @@ export function collectSkillPaths(dirs: AddedDir[]): string[] {
|
|
|
97
103
|
const paths: string[] = [];
|
|
98
104
|
const names = new Set<string>();
|
|
99
105
|
for (const dir of dirs) {
|
|
100
|
-
if (!dirExists(dir.absolutePath)) continue;
|
|
101
106
|
for (const skill of skillFiles(dir.absolutePath)) {
|
|
102
107
|
if (names.has(skill.name)) continue;
|
|
103
108
|
names.add(skill.name);
|
|
@@ -146,33 +151,21 @@ export async function findFiles(
|
|
|
146
151
|
|
|
147
152
|
const matchPath = normalizedPattern.includes(path.sep);
|
|
148
153
|
const results: string[] = [];
|
|
149
|
-
const pending = [root];
|
|
150
154
|
|
|
151
155
|
signal?.throwIfAborted();
|
|
152
|
-
|
|
156
|
+
for await (const entry of glob("**/@(*|.*)", {
|
|
157
|
+
cwd: root,
|
|
158
|
+
withFileTypes: true,
|
|
159
|
+
exclude: (entry) => entry.isSymbolicLink() || (entry.isDirectory() && SKIPPED_SEARCH_DIRS.has(entry.name)),
|
|
160
|
+
})) {
|
|
153
161
|
signal?.throwIfAborted();
|
|
154
|
-
|
|
155
|
-
let entries;
|
|
156
|
-
try {
|
|
157
|
-
entries = await readdir(current, { withFileTypes: true });
|
|
158
|
-
} catch {
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
162
|
+
if (!entry.isFile()) continue;
|
|
161
163
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
if (!entry.isFile()) continue;
|
|
170
|
-
|
|
171
|
-
const candidate = matchPath ? path.relative(root, fullPath) : entry.name;
|
|
172
|
-
if (!path.matchesGlob(candidate, normalizedPattern)) continue;
|
|
173
|
-
results.push(fullPath);
|
|
174
|
-
if (results.length >= maxResults) return results;
|
|
175
|
-
}
|
|
164
|
+
const fullPath = path.join(entry.parentPath, entry.name);
|
|
165
|
+
const candidate = matchPath ? path.relative(root, fullPath) : entry.name;
|
|
166
|
+
if (!path.matchesGlob(candidate, normalizedPattern)) continue;
|
|
167
|
+
results.push(fullPath);
|
|
168
|
+
if (results.length >= maxResults) return results;
|
|
176
169
|
}
|
|
177
170
|
|
|
178
171
|
return results;
|
package/extensions/add-dir.ts
CHANGED
|
@@ -117,13 +117,21 @@ export default function addDirExtension(pi: ExtensionAPI): void {
|
|
|
117
117
|
}));
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
function reconstructState(ctx: ExtensionContext):
|
|
120
|
+
function reconstructState(ctx: ExtensionContext): boolean {
|
|
121
121
|
currentCwd = ctx.cwd;
|
|
122
122
|
const stateEntry = [...ctx.sessionManager.getBranch()]
|
|
123
123
|
.reverse()
|
|
124
124
|
.find((entry) => entry.type === "custom" && entry.customType === STATE_TYPE);
|
|
125
|
-
|
|
125
|
+
const nextDirs = stateEntry?.type === "custom" ? readState(stateEntry.data) : [];
|
|
126
|
+
const changed =
|
|
127
|
+
addedDirs.length !== nextDirs.length ||
|
|
128
|
+
addedDirs.some(
|
|
129
|
+
(dir, index) =>
|
|
130
|
+
dir.absolutePath !== nextDirs[index]?.absolutePath || dir.label !== nextDirs[index]?.label,
|
|
131
|
+
);
|
|
132
|
+
addedDirs = nextDirs;
|
|
126
133
|
updateWidget(ctx);
|
|
134
|
+
return changed;
|
|
127
135
|
}
|
|
128
136
|
|
|
129
137
|
function persistState(): void {
|
|
@@ -138,17 +146,24 @@ export default function addDirExtension(pi: ExtensionAPI): void {
|
|
|
138
146
|
const input = dirPath.trim();
|
|
139
147
|
if (!input) return { ok: false, message: "Directory path must not be blank.", hasNewSkills: false };
|
|
140
148
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
149
|
+
let absolutePath: string;
|
|
150
|
+
try {
|
|
151
|
+
absolutePath = resolveDir(input, cwd);
|
|
152
|
+
if (!dirExists(absolutePath)) {
|
|
153
|
+
return { ok: false, message: `Directory does not exist: ${absolutePath}`, hasNewSkills: false };
|
|
154
|
+
}
|
|
155
|
+
} catch (error) {
|
|
156
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
157
|
+
return { ok: false, message: `Cannot access directory: ${message}`, hasNewSkills: false };
|
|
144
158
|
}
|
|
145
159
|
if (addedDirs.some((dir) => dir.absolutePath === absolutePath)) {
|
|
146
160
|
return { ok: false, message: `Already added: ${absolutePath}`, hasNewSkills: false };
|
|
147
161
|
}
|
|
148
|
-
|
|
162
|
+
const cwdPath = resolveDir(cwd, cwd);
|
|
163
|
+
if (isWithinDir(cwdPath, absolutePath) || isWithinDir(absolutePath, cwdPath)) {
|
|
149
164
|
return {
|
|
150
165
|
ok: false,
|
|
151
|
-
message: "Directory
|
|
166
|
+
message: "Directory overlaps current working directory scope.",
|
|
152
167
|
hasNewSkills: false,
|
|
153
168
|
};
|
|
154
169
|
}
|
|
@@ -195,14 +210,26 @@ export default function addDirExtension(pi: ExtensionAPI): void {
|
|
|
195
210
|
return skillPaths.length > 0 ? { skillPaths } : undefined;
|
|
196
211
|
});
|
|
197
212
|
|
|
198
|
-
pi.on("session_start", async (_event, ctx) =>
|
|
199
|
-
|
|
213
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
214
|
+
reconstructState(ctx);
|
|
215
|
+
});
|
|
216
|
+
pi.on("session_tree", async (_event, ctx) => {
|
|
217
|
+
if (reconstructState(ctx)) pi.sendUserMessage("/dir-reload", { expandPromptTemplates: true });
|
|
218
|
+
});
|
|
200
219
|
|
|
201
220
|
pi.on("before_agent_start", async (event) => {
|
|
202
221
|
if (addedDirs.length === 0) return;
|
|
203
222
|
return { systemPrompt: event.systemPrompt + buildContextInjection(addedDirs) };
|
|
204
223
|
});
|
|
205
224
|
|
|
225
|
+
pi.registerCommand("dir-reload", {
|
|
226
|
+
description: "Reload external directory resources",
|
|
227
|
+
handler: async (_args, ctx) => {
|
|
228
|
+
await ctx.reload();
|
|
229
|
+
return;
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
|
|
206
233
|
pi.registerCommand("dir-add", {
|
|
207
234
|
description: "Add an external directory to this session",
|
|
208
235
|
handler: async (args, ctx) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henryqw/pi-add-dir",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"description": "Add external directories to a Pi session with context, skills, and file search.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"pack:check": "npm pack --dry-run"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"@earendil-works/pi-coding-agent": "
|
|
29
|
-
"@earendil-works/pi-tui": "
|
|
28
|
+
"@earendil-works/pi-coding-agent": "^0.84.4",
|
|
29
|
+
"@earendil-works/pi-tui": "^0.84.4",
|
|
30
30
|
"typebox": "^1.3.15"
|
|
31
31
|
},
|
|
32
32
|
"repository": {
|