@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.
@@ -23,7 +23,7 @@ function isMissingPathError(error: unknown): boolean {
23
23
  return error.code === "ENOENT" || error.code === "ENOTDIR";
24
24
  }
25
25
 
26
- export function expandUserPath(input: string): string {
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
- export function readFileSafe(filePath: string): string | null {
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);
@@ -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
- function isAddedDir(value: unknown): value is AddedDir {
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<AddedDir>;
100
- return typeof dir.absolutePath === "string" && isAbsolute(dir.absolutePath) && typeof dir.label === "string";
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): AddedDir[] {
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): ParsedAddArgs {
110
- const input = args?.trim() ?? "";
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("--")) throw new Error(DIR_ADD_USAGE);
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: AddedDir[] = [];
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((dir) => ({ ...dir })) });
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
- validateStoredPaths(paths, "project");
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
- const cwdPath = resolveDir(cwd, cwd);
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, label });
466
+ sessionDirs.push({ absolutePath });
479
467
  persistState();
480
468
  } else {
481
- const previousSessionDirs = sessionDirs;
469
+ await writePersistentPath(source, absolutePath, ctx);
482
470
  if (sessionMatch) {
483
471
  sessionDirs = sessionDirs.filter((dir) => resolveDir(dir.absolutePath, ctx.cwd) !== absolutePath);
484
- try {
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
- let parsed: ParsedAddArgs;
588
- try {
589
- parsed = parseAddArgs(args);
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.cwd, 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 inputPath = params.path.trim();
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.skillCount > 0) badges.push(theme.fg("warning", `${details.skillCount} skills`));
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(", "));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-add-dir",
3
- "version": "3.1.0",
3
+ "version": "3.1.1",
4
4
  "description": "Add external directories to a Pi session with context, skills, and file search.",
5
5
  "keywords": [
6
6
  "pi-package",