@henryqw/pi-add-dir 3.1.1 → 3.1.3

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
@@ -1,6 +1,6 @@
1
1
  # `@henryqw/pi-add-dir`
2
2
 
3
- Give the current Pi session context, skills, and searchable files from directories outside the workspace. Reuse those resources in place without copying or linking their directories.
3
+ Give the current Pi session the context, skills, and searchable files of external directories. Pi uses these resources in place, so you do not copy or link the directories.
4
4
 
5
5
  ![Pi using added external directories by name](https://raw.githubusercontent.com/HenryQW/pi-harness/main/extensions/pi-add-dir/example.png)
6
6
 
@@ -14,19 +14,19 @@ Requires Pi 1.1.0 or later in the 1.x series.
14
14
 
15
15
  ## Use
16
16
 
17
- Run `/dir-add`, enter a directory, then run `/dir-ls` to confirm it was added. Pi lists the directory and exposes its supported resources.
17
+ Run `/dir-add` and enter a directory. Then run `/dir-ls` to make sure that Pi added it. Pi lists the directory and makes its supported resources available.
18
18
 
19
- Use slash commands from the Pi prompt as a human; Pi agents use the tools.
19
+ Humans use the slash commands from the Pi prompt. Pi agents use the tools.
20
20
 
21
21
  | Surface | Type | Purpose |
22
22
  | --- | --- | --- |
23
- | `/dir-add [path]` | command | Add to this session by default; pass `--project` or `--global` before the path to persist for this Git repository's linked worktrees or every workspace. Omit the path to enter it when prompted; supports `~`. |
24
- | `/dir-ls` | command | List directories with their scope; select one to remove from that scope. |
23
+ | `/dir-add [path]` | command | Add to this session by default. To persist for the linked worktrees of this Git repository or for every workspace, pass `--project` or `--global` before the path. Omit the path to enter it at the prompt. Supports `~`. |
24
+ | `/dir-ls` | command | List directories with their scope. Select one to remove it from that scope. |
25
25
  | `/dir-reload` | command | Reload external directory resources. |
26
26
  | `add_directory(path)` | tool | Add a directory to this session and return its context and instructions. Model-only: codemode scripts cannot call it. |
27
- | `search_external_files(pattern, maxResults?)` | tool | Glob-search added directories; returns up to 50 results by default, configurable to 1,000. Returns a structured result to codemode scripts. |
27
+ | `search_external_files(pattern, maxResults?)` | tool | Search added directories with a glob. Returns up to 50 results by default; `maxResults` accepts up to 1,000. Returns a structured result to codemode scripts. |
28
28
  | `@` file autocomplete | ui | Find files in added directories and insert their absolute paths. |
29
- | External directory widget | ui | Show added directories and point to `/dir-ls` for management. |
29
+ | External directory widget | ui | Show added directories and refer to `/dir-ls` for management. |
30
30
 
31
31
  Added directories give Pi these resources:
32
32
 
@@ -34,13 +34,13 @@ Added directories give Pi these resources:
34
34
  - Skills that Pi loads from `.pi/skills`, `.agents/skills`, and `.claude/skills`.
35
35
  - Files in the editor's `@` autocomplete, with absolute paths.
36
36
 
37
- `/dir-add` reloads only when the discovered external skill paths change, not merely because the directory contains skills. `add_directory` stays session-local; when its result asks for `/reload`, run it to update external skills.
37
+ `/dir-add` reloads only when the discovered external skill paths change. A directory that contains skills does not alone cause a reload. `add_directory` stays session-local. When its result asks for `/reload`, run `/reload` to update external skills.
38
38
 
39
39
  ### Programmatic use
40
40
 
41
- `search_external_files` declares an output schema. Programmatic callers such as Pi `codemode` scripts receive `{ pattern, maxResults, searchedDirectories, totalFound, truncated, directories }`. `directories` lists each directory with at least one match, with its label and absolute file paths. `searchedDirectories` counts only directories actually searched, including those with no matches; directories skipped after reaching the cap are not counted. `truncated` is true when the result cap stopped the search, so later directories or files may hold more matches. The model receives the same matches as text. Calling the tool with no added directories is a tool error, and scripts reject with the message.
41
+ `search_external_files` declares an output schema. Programmatic callers, such as Pi `codemode` scripts, receive `{ pattern, maxResults, searchedDirectories, totalFound, truncated, directories }`. `directories` lists each directory that has at least one match, with its label and absolute file paths. `searchedDirectories` counts only the directories that the tool searched, including directories with no matches. It does not count directories that the tool skipped after it reached the cap. `truncated` is true when the result cap stopped the search. In that case, later directories or files can contain more matches. The model receives the same matches as text. If there are no added directories, a call to the tool is a tool error, and scripts reject with the message.
42
42
 
43
- `add_directory` is declared to the model only (Pi `model-only` exposure). Codemode scripts and other tools cannot call it, so another directory's `AGENTS.md`, `CLAUDE.md`, and skills enter the session only through a model-visible tool call or a user command. This package does not enable codemode.
43
+ `add_directory` is declared to the model only (Pi `model-only` exposure). Codemode scripts and other tools cannot call it. Thus, the `AGENTS.md`, `CLAUDE.md`, and skills of another directory enter the session only through a model-visible tool call or a user command. This package does not enable codemode.
44
44
 
45
45
  ## Config
46
46
 
@@ -50,16 +50,16 @@ Package-owned: `~/.pi/agent/config/pi-add-dir/config.json`
50
50
  | --- | --- | --- | --- |
51
51
  | `directories` | Directories loaded for every workspace. `/dir-add --global` updates this list. | Array of unique absolute paths without control characters. | `[]` |
52
52
 
53
- A missing file uses the default. Invalid config stops loading and is not overwritten. Reload Pi after editing the file manually.
53
+ If the file is missing, the extension uses the default. At launch, the extension shows a warning for each unknown key and each invalid or duplicate directory entry. It ignores that key or entry and loads the other directories. If the file is malformed or `directories` is not an array, it shows a warning and uses the default. It does not change the file. `/dir-add --global` and global removal with `/dir-ls` fail until you fix the reported issues. Reload Pi after you edit the file manually.
54
54
 
55
55
  ## State and storage
56
56
 
57
- Session directories live in the current session branch. New session additions reject directories inside or containing the current working directory, or overlapping another active added directory. Restored session entries are not rechecked for overlap when the working directory changes. Project directories use the repository's repeatable local Git config key `pi-add-dir.directory`. Local Git config is shared by linked worktrees, cannot be injected by cloning a repository, and remains machine-local. `/dir-add --project` therefore requires a Git repository.
57
+ Session directories are stored in the current session branch. A new session addition rejects a directory that is inside the current working directory or that contains it. It also rejects a directory that overlaps another active added directory. When the working directory changes, the extension does not check restored session entries for overlap again. Project directories use the repeatable local Git config key `pi-add-dir.directory` of the repository. Linked worktrees share local Git config. A cloned repository cannot inject this config, and it stays on the local machine. Thus, `/dir-add --project` requires a Git repository.
58
58
 
59
59
  Persistent scopes are explicit because added directories can inject `AGENTS.md`, `CLAUDE.md`, and skills.
60
60
 
61
61
  ## Limits and recovery
62
62
 
63
- Search supports basename and relative-path globs. It skips `.git` and `node_modules` and does not follow symbolic links. It uses Node filesystem traversal and returns at most 1,000 results per call.
63
+ Search supports basename and relative-path globs. It skips `.git` and `node_modules` and does not follow symbolic links. It uses Node filesystem traversal and returns at most 1,000 results for each call.
64
64
 
65
- Missing or overlapping **project and global** directories remain configured but are skipped with a warning. Restore a missing directory at its original path or resolve the overlap, then run `/dir-reload` to rescan. To change or discard a configured path, use `/dir-ls` to **remove** it from its scope, then `/dir-add` with the corrected path if needed; `/dir-ls` cannot repair paths in place.
65
+ Missing or overlapping **project and global** directories stay in the config, but the extension skips them with a warning. Restore a missing directory at its original path, or resolve the overlap. Then run `/dir-reload` to scan again. To change or discard a configured path, use `/dir-ls` to **remove** it from its scope. If necessary, run `/dir-add` with the corrected path. `/dir-ls` cannot repair paths in place.
@@ -1,40 +1,11 @@
1
- import { isAbsolute } from "node:path";
2
- import { createConfigStore } from "@henryqw/pi-config-store";
3
-
4
- export interface AddDirConfig {
5
- directories: string[];
6
- }
7
-
8
- function parseConfig(value: unknown): AddDirConfig {
9
- if (!value || typeof value !== "object" || Array.isArray(value)) {
10
- throw new Error("Invalid pi-add-dir config: expected an object.");
11
- }
12
- const config = value as Record<string, unknown>;
13
- if (Object.keys(config).length !== 1 || !Array.isArray(config.directories)) {
14
- throw new Error('Invalid pi-add-dir config: expected only a "directories" array.');
15
- }
16
- const directories = config.directories;
17
- if (
18
- directories.some(
19
- (directory) =>
20
- typeof directory !== "string" ||
21
- !isAbsolute(directory) ||
22
- /\p{C}/u.test(directory),
23
- )
24
- ) {
25
- throw new Error("Invalid pi-add-dir config: directories must be absolute paths without control characters.");
26
- }
27
- if (new Set(directories).size !== directories.length) {
28
- throw new Error("Invalid pi-add-dir config: directories must be unique.");
29
- }
30
- return { directories: [...directories] };
31
- }
1
+ import { createConfigStore, field, parsers } from "@henryqw/pi-config-store";
32
2
 
33
3
  export function createAddDirConfigStore(agentDir?: string) {
34
- return createConfigStore<AddDirConfig>({
4
+ return createConfigStore({
35
5
  extensionId: "pi-add-dir",
36
6
  agentDir,
37
- defaults: () => ({ directories: [] }),
38
- parse: parseConfig,
7
+ fields: {
8
+ directories: field(parsers.array(parsers.absolutePath(), { unique: (directory) => directory }), []),
9
+ },
39
10
  });
40
11
  }
@@ -9,6 +9,7 @@ import {
9
9
  type AutocompleteSuggestions,
10
10
  } from "@earendil-works/pi-tui";
11
11
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
12
+ import { registerConfigReporting } from "@henryqw/pi-config-store";
12
13
  import { Type } from "typebox";
13
14
  import {
14
15
  buildContextInjection,
@@ -227,6 +228,9 @@ export function createExternalAutocompleteProvider(
227
228
 
228
229
  export default function addDirExtension(pi: ExtensionAPI, options: ExtensionOptions = {}): void {
229
230
  const globalStore = createAddDirConfigStore(options.agentDir);
231
+ const configReporter = registerConfigReporting(pi, [globalStore]);
232
+ // Report the latest file before a global write, so issues found after launch use the same deduplicated report.
233
+ const reportGlobalConfig = (ctx: ExtensionContext) => configReporter.report(ctx, globalStore, globalStore.loadSync());
230
234
  let sessionDirs: SessionDir[] = [];
231
235
  let projectPaths: string[] = [];
232
236
  let globalPaths: string[] = [];
@@ -234,17 +238,10 @@ export default function addDirExtension(pi: ExtensionAPI, options: ExtensionOpti
234
238
  let addedDirs: ScopedDir[] = [];
235
239
  let currentCwd = "";
236
240
 
237
- function globalConfigError(error: unknown): Error {
238
- const message = error instanceof Error ? error.message : String(error);
239
- return new Error(`Cannot load pi-add-dir global config at ${globalStore.path}: ${message}`, { cause: error });
240
- }
241
-
241
+ // No field disables this config, so a load always has a value; bad entries are dropped and reported at launch.
242
242
  function loadGlobalPaths(): string[] {
243
- try {
244
- return globalStore.loadSync().value.directories;
245
- } catch (error) {
246
- throw globalConfigError(error);
247
- }
243
+ const loaded = globalStore.loadSync();
244
+ return loaded.status === "ok" ? loaded.value.directories : [];
248
245
  }
249
246
 
250
247
  function updateWidget(ctx: ExtensionContext): void {
@@ -391,17 +388,14 @@ export default function addDirExtension(pi: ExtensionAPI, options: ExtensionOpti
391
388
 
392
389
  async function writePersistentPath(source: "project" | "global", absolutePath: string, ctx: ExtensionContext): Promise<void> {
393
390
  if (source === "global") {
394
- try {
395
- const config = await globalStore.update((current) => ({
396
- directories: current.directories.includes(absolutePath)
397
- ? current.directories
398
- : [...current.directories, absolutePath],
399
- }));
400
- globalPaths = config.directories;
401
- return;
402
- } catch (error) {
403
- throw globalConfigError(error);
404
- }
391
+ reportGlobalConfig(ctx);
392
+ const config = await globalStore.update((current) => ({
393
+ directories: current.directories.includes(absolutePath)
394
+ ? current.directories
395
+ : [...current.directories, absolutePath],
396
+ }));
397
+ globalPaths = config.directories;
398
+ return;
405
399
  }
406
400
  projectPaths = await readProjectPaths(ctx.cwd, true);
407
401
  if (projectPaths.some((path) => resolveDir(path, ctx.cwd) === absolutePath)) return;
@@ -498,14 +492,11 @@ export default function addDirExtension(pi: ExtensionAPI, options: ExtensionOpti
498
492
  );
499
493
  persistState();
500
494
  } else if (dir.source === "global") {
501
- try {
502
- const config = await globalStore.update((current) => ({
503
- directories: current.directories.filter((path) => path !== dir.storedPath),
504
- }));
505
- globalPaths = config.directories;
506
- } catch (error) {
507
- throw globalConfigError(error);
508
- }
495
+ reportGlobalConfig(ctx);
496
+ const config = await globalStore.update((current) => ({
497
+ directories: current.directories.filter((path) => path !== dir.storedPath),
498
+ }));
499
+ globalPaths = config.directories;
509
500
  } else {
510
501
  await mutateProjectConfig(
511
502
  ["--unset-all", "--fixed-value", PROJECT_CONFIG_KEY, dir.storedPath],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-add-dir",
3
- "version": "3.1.1",
3
+ "version": "3.1.3",
4
4
  "description": "Add external directories to a Pi session with context, skills, and file search.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -47,7 +47,7 @@
47
47
  "image": "https://raw.githubusercontent.com/HenryQW/pi-harness/main/extensions/pi-add-dir/example.png"
48
48
  },
49
49
  "dependencies": {
50
- "@henryqw/pi-config-store": "^3.0.0"
50
+ "@henryqw/pi-config-store": "^4.0.0"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@earendil-works/pi-coding-agent": "1.1.0",