@henryqw/pi-add-dir 3.1.0 → 3.1.1
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/extensions/add-dir-helpers.ts +2 -6
- package/extensions/add-dir.ts +35 -70
- package/package.json +1 -1
|
@@ -23,7 +23,7 @@ function isMissingPathError(error: unknown): boolean {
|
|
|
23
23
|
return error.code === "ENOENT" || error.code === "ENOTDIR";
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
function expandUserPath(input: string): string {
|
|
27
27
|
if (input === "~") return homedir();
|
|
28
28
|
if (input.startsWith("~/") || input.startsWith(`~${path.sep}`)) return path.join(homedir(), input.slice(2));
|
|
29
29
|
return input;
|
|
@@ -49,7 +49,7 @@ export function dirExists(dir: string): boolean {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
function readFileSafe(filePath: string): string | null {
|
|
53
53
|
try {
|
|
54
54
|
return readFileSync(filePath, "utf8");
|
|
55
55
|
} catch {
|
|
@@ -113,8 +113,6 @@ export function collectSkillPaths(dirs: AddedDir[]): string[] {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
export function buildContextInjection(dirs: AddedDir[]): string {
|
|
116
|
-
if (dirs.length === 0) return "";
|
|
117
|
-
|
|
118
116
|
const sections = [
|
|
119
117
|
"\n\n## External Directories (added via pi-add-dir)",
|
|
120
118
|
`\nThe following ${dirs.length} external director${dirs.length === 1 ? "y is" : "ies are"} included in this session. You can read, edit, and write files in these directories using absolute paths.\n`,
|
|
@@ -154,7 +152,6 @@ export async function findFiles(
|
|
|
154
152
|
|
|
155
153
|
const directories = [root];
|
|
156
154
|
while (directories.length > 0) {
|
|
157
|
-
signal?.throwIfAborted();
|
|
158
155
|
const dir = directories.pop()!;
|
|
159
156
|
let entries;
|
|
160
157
|
try {
|
|
@@ -166,7 +163,6 @@ export async function findFiles(
|
|
|
166
163
|
}
|
|
167
164
|
signal?.throwIfAborted();
|
|
168
165
|
for (const entry of entries) {
|
|
169
|
-
signal?.throwIfAborted();
|
|
170
166
|
if (entry.isSymbolicLink()) continue;
|
|
171
167
|
|
|
172
168
|
const fullPath = path.join(dir, entry.name);
|
package/extensions/add-dir.ts
CHANGED
|
@@ -84,7 +84,6 @@ interface AddDirectoryDetails {
|
|
|
84
84
|
directory: string;
|
|
85
85
|
hasAgentsMd: boolean;
|
|
86
86
|
hasClaudeMd: boolean;
|
|
87
|
-
skillCount: number;
|
|
88
87
|
skillNames: string[];
|
|
89
88
|
}
|
|
90
89
|
|
|
@@ -94,40 +93,34 @@ interface SearchDetails {
|
|
|
94
93
|
dirCount: number;
|
|
95
94
|
}
|
|
96
95
|
|
|
97
|
-
|
|
96
|
+
interface SessionDir {
|
|
97
|
+
absolutePath: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isAddedDir(value: unknown): value is SessionDir {
|
|
98
101
|
if (!value || typeof value !== "object") return false;
|
|
99
|
-
const dir = value as Partial<
|
|
100
|
-
return typeof dir.absolutePath === "string" && isAbsolute(dir.absolutePath)
|
|
102
|
+
const dir = value as Partial<SessionDir>;
|
|
103
|
+
return typeof dir.absolutePath === "string" && isAbsolute(dir.absolutePath);
|
|
101
104
|
}
|
|
102
105
|
|
|
103
|
-
function readState(data: unknown):
|
|
106
|
+
function readState(data: unknown): SessionDir[] {
|
|
104
107
|
if (!data || typeof data !== "object") return [];
|
|
105
108
|
const dirs = (data as { dirs?: unknown }).dirs;
|
|
106
109
|
return Array.isArray(dirs) ? dirs.filter(isAddedDir) : [];
|
|
107
110
|
}
|
|
108
111
|
|
|
109
|
-
function parseAddArgs(args: string | undefined
|
|
110
|
-
const input = args
|
|
112
|
+
function parseAddArgs(args: string): ParsedAddArgs | undefined {
|
|
113
|
+
const input = args.trim();
|
|
111
114
|
if (!input) return { source: "session", path: "" };
|
|
112
115
|
for (const source of ["project", "global"] as const) {
|
|
113
116
|
const flag = `--${source}`;
|
|
114
117
|
if (input === flag) return { source, path: "" };
|
|
115
118
|
if (input.startsWith(`${flag} `)) return { source, path: input.slice(flag.length).trim() };
|
|
116
119
|
}
|
|
117
|
-
if (input.startsWith("--"))
|
|
120
|
+
if (input.startsWith("--")) return undefined;
|
|
118
121
|
return { source: "session", path: input };
|
|
119
122
|
}
|
|
120
123
|
|
|
121
|
-
function validateStoredPaths(paths: string[], source: DirSource): void {
|
|
122
|
-
if (
|
|
123
|
-
paths.some(
|
|
124
|
-
(path) => typeof path !== "string" || !isAbsolute(path) || /\p{C}/u.test(path),
|
|
125
|
-
)
|
|
126
|
-
) {
|
|
127
|
-
throw new Error(`Invalid ${source} pi-add-dir configuration: expected absolute paths.`);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
124
|
function hasGitMarker(cwd: string): boolean {
|
|
132
125
|
let directory = resolveDir(cwd, cwd);
|
|
133
126
|
while (true) {
|
|
@@ -150,12 +143,15 @@ function isWithinDir(dir: string, candidate: string): boolean {
|
|
|
150
143
|
return relativePath === "" || (!relativePath.startsWith(`..${sep}`) && relativePath !== ".." && !isAbsolute(relativePath));
|
|
151
144
|
}
|
|
152
145
|
|
|
146
|
+
function overlaps(a: string, b: string): boolean {
|
|
147
|
+
return isWithinDir(a, b) || isWithinDir(b, a);
|
|
148
|
+
}
|
|
149
|
+
|
|
153
150
|
function contextDetails(dirCtx: DirContext, absolutePath: string): AddDirectoryDetails {
|
|
154
151
|
return {
|
|
155
152
|
directory: absolutePath,
|
|
156
153
|
hasAgentsMd: dirCtx.agentsMd !== null,
|
|
157
154
|
hasClaudeMd: dirCtx.claudeMd !== null,
|
|
158
|
-
skillCount: dirCtx.skills.size,
|
|
159
155
|
skillNames: [...dirCtx.skills],
|
|
160
156
|
};
|
|
161
157
|
}
|
|
@@ -231,7 +227,7 @@ export function createExternalAutocompleteProvider(
|
|
|
231
227
|
|
|
232
228
|
export default function addDirExtension(pi: ExtensionAPI, options: ExtensionOptions = {}): void {
|
|
233
229
|
const globalStore = createAddDirConfigStore(options.agentDir);
|
|
234
|
-
let sessionDirs:
|
|
230
|
+
let sessionDirs: SessionDir[] = [];
|
|
235
231
|
let projectPaths: string[] = [];
|
|
236
232
|
let globalPaths: string[] = [];
|
|
237
233
|
let managedDirs: ScopedDir[] = [];
|
|
@@ -318,17 +314,11 @@ export default function addDirExtension(pi: ExtensionAPI, options: ExtensionOpti
|
|
|
318
314
|
}
|
|
319
315
|
const duplicate = nextActive.find((dir) => dir.absolutePath === absolutePath);
|
|
320
316
|
if (!inactiveReason && duplicate) inactiveReason = `shadowed by ${duplicate.source} scope`;
|
|
321
|
-
if (
|
|
322
|
-
!inactiveReason &&
|
|
323
|
-
(isWithinDir(cwdPath, absolutePath) || isWithinDir(absolutePath, cwdPath))
|
|
324
|
-
) {
|
|
317
|
+
if (!inactiveReason && overlaps(cwdPath, absolutePath)) {
|
|
325
318
|
inactiveReason = "overlaps current working directory scope";
|
|
326
319
|
}
|
|
327
320
|
const overlap = !inactiveReason
|
|
328
|
-
? nextActive.find(
|
|
329
|
-
(dir) =>
|
|
330
|
-
isWithinDir(dir.absolutePath, absolutePath) || isWithinDir(absolutePath, dir.absolutePath),
|
|
331
|
-
)
|
|
321
|
+
? nextActive.find((dir) => overlaps(dir.absolutePath, absolutePath))
|
|
332
322
|
: undefined;
|
|
333
323
|
if (overlap) inactiveReason = `overlaps ${overlap.source} directory ${overlap.absolutePath}`;
|
|
334
324
|
const scoped = {
|
|
@@ -361,7 +351,7 @@ export default function addDirExtension(pi: ExtensionAPI, options: ExtensionOpti
|
|
|
361
351
|
}
|
|
362
352
|
|
|
363
353
|
function persistState(): void {
|
|
364
|
-
pi.appendEntry(STATE_TYPE, { dirs: sessionDirs.map((
|
|
354
|
+
pi.appendEntry(STATE_TYPE, { dirs: sessionDirs.map(({ absolutePath }) => ({ absolutePath })) });
|
|
365
355
|
}
|
|
366
356
|
|
|
367
357
|
async function readProjectPaths(cwd: string, required: boolean): Promise<string[]> {
|
|
@@ -381,7 +371,9 @@ export default function addDirExtension(pi: ExtensionAPI, options: ExtensionOpti
|
|
|
381
371
|
throw new Error(`Cannot read project pi-add-dir configuration: ${result.stderr.trim() || `git exited ${result.code}`}`);
|
|
382
372
|
}
|
|
383
373
|
const paths = result.stdout.split("\0").filter((path) => path.length > 0);
|
|
384
|
-
|
|
374
|
+
if (paths.some((path) => !isAbsolute(path) || /\p{C}/u.test(path))) {
|
|
375
|
+
throw new Error("Invalid project pi-add-dir configuration: expected absolute paths.");
|
|
376
|
+
}
|
|
385
377
|
return [...new Set(paths)];
|
|
386
378
|
}
|
|
387
379
|
|
|
@@ -424,7 +416,6 @@ export default function addDirExtension(pi: ExtensionAPI, options: ExtensionOpti
|
|
|
424
416
|
async function addDir(
|
|
425
417
|
dirPath: string,
|
|
426
418
|
source: DirSource,
|
|
427
|
-
cwd: string,
|
|
428
419
|
ctx: ExtensionContext,
|
|
429
420
|
): Promise<{ ok: boolean; message: string; resourcesChanged: boolean; absolutePath?: string; context?: DirContext }> {
|
|
430
421
|
const input = dirPath.trim();
|
|
@@ -432,7 +423,7 @@ export default function addDirExtension(pi: ExtensionAPI, options: ExtensionOpti
|
|
|
432
423
|
|
|
433
424
|
let absolutePath: string;
|
|
434
425
|
try {
|
|
435
|
-
absolutePath = resolveDir(input, cwd);
|
|
426
|
+
absolutePath = resolveDir(input, ctx.cwd);
|
|
436
427
|
if (!dirExists(absolutePath)) {
|
|
437
428
|
return { ok: false, message: `Directory does not exist: ${absolutePath}`, resourcesChanged: false };
|
|
438
429
|
}
|
|
@@ -453,14 +444,11 @@ export default function addDirExtension(pi: ExtensionAPI, options: ExtensionOpti
|
|
|
453
444
|
resourcesChanged: false,
|
|
454
445
|
};
|
|
455
446
|
}
|
|
456
|
-
|
|
457
|
-
if (isWithinDir(cwdPath, absolutePath) || isWithinDir(absolutePath, cwdPath)) {
|
|
447
|
+
if (overlaps(resolveDir(ctx.cwd, ctx.cwd), absolutePath)) {
|
|
458
448
|
return { ok: false, message: "Directory overlaps current working directory scope.", resourcesChanged: false };
|
|
459
449
|
}
|
|
460
450
|
const overlap = addedDirs.find(
|
|
461
|
-
(dir) =>
|
|
462
|
-
dir.absolutePath !== absolutePath &&
|
|
463
|
-
(isWithinDir(dir.absolutePath, absolutePath) || isWithinDir(absolutePath, dir.absolutePath)),
|
|
451
|
+
(dir) => dir.absolutePath !== absolutePath && overlaps(dir.absolutePath, absolutePath),
|
|
464
452
|
);
|
|
465
453
|
if (overlap) {
|
|
466
454
|
return {
|
|
@@ -475,32 +463,13 @@ export default function addDirExtension(pi: ExtensionAPI, options: ExtensionOpti
|
|
|
475
463
|
const label = basename(absolutePath) || absolutePath;
|
|
476
464
|
try {
|
|
477
465
|
if (source === "session") {
|
|
478
|
-
sessionDirs.push({ absolutePath
|
|
466
|
+
sessionDirs.push({ absolutePath });
|
|
479
467
|
persistState();
|
|
480
468
|
} else {
|
|
481
|
-
|
|
469
|
+
await writePersistentPath(source, absolutePath, ctx);
|
|
482
470
|
if (sessionMatch) {
|
|
483
471
|
sessionDirs = sessionDirs.filter((dir) => resolveDir(dir.absolutePath, ctx.cwd) !== absolutePath);
|
|
484
|
-
|
|
485
|
-
persistState();
|
|
486
|
-
} catch (error) {
|
|
487
|
-
sessionDirs = previousSessionDirs;
|
|
488
|
-
throw error;
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
try {
|
|
492
|
-
await writePersistentPath(source, absolutePath, ctx);
|
|
493
|
-
} catch (error) {
|
|
494
|
-
if (sessionMatch) {
|
|
495
|
-
sessionDirs = previousSessionDirs;
|
|
496
|
-
try {
|
|
497
|
-
persistState();
|
|
498
|
-
} catch (recoveryError) {
|
|
499
|
-
const recoveryMessage = recoveryError instanceof Error ? recoveryError.message : String(recoveryError);
|
|
500
|
-
throw new Error(`Persistent write failed and session recovery failed: ${recoveryMessage}`, { cause: error });
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
throw error;
|
|
472
|
+
persistState();
|
|
504
473
|
}
|
|
505
474
|
}
|
|
506
475
|
} catch (error) {
|
|
@@ -577,18 +546,15 @@ export default function addDirExtension(pi: ExtensionAPI, options: ExtensionOpti
|
|
|
577
546
|
description: "Reload external directory resources",
|
|
578
547
|
handler: async (_args, ctx) => {
|
|
579
548
|
await ctx.reload();
|
|
580
|
-
return;
|
|
581
549
|
},
|
|
582
550
|
});
|
|
583
551
|
|
|
584
552
|
pi.registerCommand("dir-add", {
|
|
585
553
|
description: "Add an external directory to this session, project, or global scope",
|
|
586
554
|
handler: async (args, ctx) => {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
} catch (error) {
|
|
591
|
-
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
555
|
+
const parsed = parseAddArgs(args);
|
|
556
|
+
if (!parsed) {
|
|
557
|
+
ctx.ui.notify(DIR_ADD_USAGE, "error");
|
|
592
558
|
return;
|
|
593
559
|
}
|
|
594
560
|
if (!parsed.path) {
|
|
@@ -597,7 +563,7 @@ export default function addDirExtension(pi: ExtensionAPI, options: ExtensionOpti
|
|
|
597
563
|
parsed.path = prompted;
|
|
598
564
|
}
|
|
599
565
|
|
|
600
|
-
const result = await addDir(parsed.path, parsed.source, ctx
|
|
566
|
+
const result = await addDir(parsed.path, parsed.source, ctx);
|
|
601
567
|
const message = result.ok && result.resourcesChanged ? `${result.message} Reloading external skills...` : result.message;
|
|
602
568
|
ctx.ui.notify(message, result.ok ? "info" : "error");
|
|
603
569
|
if (result.ok && result.resourcesChanged) await ctx.reload();
|
|
@@ -645,8 +611,7 @@ export default function addDirExtension(pi: ExtensionAPI, options: ExtensionOpti
|
|
|
645
611
|
annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
646
612
|
|
|
647
613
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
648
|
-
const
|
|
649
|
-
const result = await addDir(inputPath, "session", ctx.cwd, ctx);
|
|
614
|
+
const result = await addDir(params.path, "session", ctx);
|
|
650
615
|
if (!result.ok) throw new Error(result.message);
|
|
651
616
|
|
|
652
617
|
const absolutePath = result.absolutePath!;
|
|
@@ -679,7 +644,7 @@ export default function addDirExtension(pi: ExtensionAPI, options: ExtensionOpti
|
|
|
679
644
|
|
|
680
645
|
renderResult(result, { expanded }, theme) {
|
|
681
646
|
const details = result.details as AddDirectoryDetails | undefined;
|
|
682
|
-
if (!details) {
|
|
647
|
+
if (!details?.directory) {
|
|
683
648
|
const content = result.content[0];
|
|
684
649
|
const text = content?.type === "text" ? content.text : "Done";
|
|
685
650
|
return new Text(theme.fg("success", `✓ ${text}`), 0, 0);
|
|
@@ -689,7 +654,7 @@ export default function addDirExtension(pi: ExtensionAPI, options: ExtensionOpti
|
|
|
689
654
|
const badges: string[] = [];
|
|
690
655
|
if (details.hasAgentsMd) badges.push(theme.fg("accent", "AGENTS.md"));
|
|
691
656
|
if (details.hasClaudeMd) badges.push(theme.fg("accent", "CLAUDE.md"));
|
|
692
|
-
if (details.
|
|
657
|
+
if (details.skillNames.length > 0) badges.push(theme.fg("warning", `${details.skillNames.length} skills`));
|
|
693
658
|
if (badges.length > 0) parts.push(theme.fg("dim", " | ") + badges.join(theme.fg("dim", ", ")));
|
|
694
659
|
if (expanded && details.skillNames.length > 0) {
|
|
695
660
|
parts.push("\n" + theme.fg("muted", " Skills: ") + details.skillNames.map((name) => theme.fg("text", name)).join(", "));
|