pi-skill-shiori 0.4.0 → 0.5.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/CHANGELOG.md CHANGED
@@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  This project follows semantic versioning.
6
6
 
7
+ ## [0.5.0] - 2026-06-28
8
+
9
+ ### Added
10
+
11
+ - Vault-wide skill inventory: Shiori indexes skills from project `.pi/skills`, `.agents/skills`, the user-global `~/.pi/agent/skills` root, and optional policy `inventory.roots`.
12
+ - Automatic inventory refresh when skill files change under any indexed root (enabled by default; disable with `inventory.autoRefreshOnChange: false`).
13
+ - `/shiori:doctor` now lists indexed roots with per-root skill counts and auto-refresh status.
14
+ - `/shiori:stats` counters for manual and automatic inventory refreshes.
15
+ - Tests for multi-root discovery, refresh after local changes, and policy-aware retrieval.
16
+
17
+ ### Changed
18
+
19
+ - `/shiori:reload` rebuilds the vault-wide inventory and SQLite FTS index instead of relying on the session-start snapshot only.
20
+
7
21
  ## [0.4.0] - 2026-06-28
8
22
 
9
23
  ### Added
package/README.md CHANGED
@@ -12,12 +12,13 @@ For maintenance priorities and the phased plan, see [`ROADMAP.md`](ROADMAP.md).
12
12
  - **Policy-based retrieval**: treats skills as explicit by default and only candidates skills marked as triggerable.
13
13
  - **Compact candidate injection**: injects short skill suggestions instead of full `SKILL.md` files.
14
14
  - **On-demand loading**: exposes `shiori_load_skill` so the model can load one selected skill body when needed.
15
+ - **Vault-wide skill inventory**: indexes skills from project and user-global roots, with optional extra roots in policy.
15
16
  - **SQLite FTS retrieval**: indexes skill names, descriptions, and policy triggers with `node:sqlite` + FTS5, with token-match fallback.
16
17
  - **Compact UI output**: keeps skill-load logs short while still passing full skill text to the model.
17
18
 
18
19
  ## Status
19
20
 
20
- `0.4.0` is an early working release. It is useful for local Pi workflows, but the prompt-boundary suppression logic is intentionally conservative: if Shiori cannot safely recognize a catalog boundary, it leaves the prompt untouched and warns instead of deleting too much.
21
+ `0.5.0` is an early working release. It is useful for local Pi workflows, but the prompt-boundary suppression logic is intentionally conservative: if Shiori cannot safely recognize a catalog boundary, it leaves the prompt untouched and warns instead of deleting too much.
21
22
 
22
23
  ## Requirements
23
24
 
@@ -39,7 +40,7 @@ Pi core packages are peer dependencies and should be supplied by the Pi runtime:
39
40
  After the package is published to npm:
40
41
 
41
42
  ```bash
42
- pi install npm:pi-skill-shiori@0.4.0
43
+ pi install npm:pi-skill-shiori@0.5.0
43
44
  ```
44
45
 
45
46
  Without a version pin:
@@ -53,7 +54,7 @@ pi install npm:pi-skill-shiori
53
54
  Use `-l` to write the package to the current project’s `.pi/settings.json`:
54
55
 
55
56
  ```bash
56
- pi install -l npm:pi-skill-shiori@0.4.0
57
+ pi install -l npm:pi-skill-shiori@0.5.0
57
58
  ```
58
59
 
59
60
  Without a version pin:
@@ -67,7 +68,7 @@ Equivalent pinned manual `.pi/settings.json` entry:
67
68
  ```json
68
69
  {
69
70
  "packages": [
70
- "npm:pi-skill-shiori@0.4.0"
71
+ "npm:pi-skill-shiori@0.5.0"
71
72
  ]
72
73
  }
73
74
  ```
@@ -85,7 +86,7 @@ Equivalent unpinned manual `.pi/settings.json` entry:
85
86
  ### Global install from GitHub
86
87
 
87
88
  ```bash
88
- pi install git:github.com/eiei114/pi-skill-shiori@v0.4.0
89
+ pi install git:github.com/eiei114/pi-skill-shiori@v0.5.0
89
90
  ```
90
91
 
91
92
  Without a tag pin:
@@ -97,7 +98,7 @@ pi install git:github.com/eiei114/pi-skill-shiori
97
98
  ### Project-local install from GitHub
98
99
 
99
100
  ```bash
100
- pi install -l git:github.com/eiei114/pi-skill-shiori@v0.4.0
101
+ pi install -l git:github.com/eiei114/pi-skill-shiori@v0.5.0
101
102
  ```
102
103
 
103
104
  Without a tag pin:
@@ -131,7 +132,38 @@ Or add a relative local path to `.pi/settings.json`:
131
132
  }
132
133
  ```
133
134
 
134
- > Note: policy/index changes can be refreshed with `/shiori:reload`, but extension code changes require restarting the Pi process because Node keeps imported extension modules cached.
135
+ > Note: policy/index changes can be refreshed with `/shiori:reload`, and Shiori auto-refreshes the vault-wide inventory when skill files change under indexed roots. Extension code changes still require restarting the Pi process because Node keeps imported extension modules cached.
136
+
137
+ ## Vault-wide skill inventory
138
+
139
+ Shiori builds a searchable inventory from these roots (in discovery order; first match wins on duplicate skill names):
140
+
141
+ | Root | Scope |
142
+ |---|---|
143
+ | `<cwd>/.pi/skills` | Project-local Pi skills |
144
+ | `<cwd>/.agents/skills` | Project-local Agent skills |
145
+ | `~/.pi/agent/skills` | User-global Pi agent skills |
146
+ | `inventory.roots` entries | Optional extra roots (relative to `<cwd>` or absolute) |
147
+
148
+ Refresh behavior:
149
+
150
+ - **Session start** builds the initial inventory and SQLite FTS index.
151
+ - **Auto-refresh** (default) rescans roots before retrieval when any `SKILL.md` path changes. This is a directory walk only; it rebuilds the FTS index when the fingerprint changes.
152
+ - **Manual refresh** via `/shiori:reload` always rebuilds the inventory and FTS index immediately.
153
+ - Disable auto-refresh when you prefer a fixed snapshot:
154
+
155
+ ```yaml
156
+ inventory:
157
+ autoRefreshOnChange: false
158
+ roots:
159
+ - custom-skills
160
+ ```
161
+
162
+ Cost/latency trade-offs:
163
+
164
+ - Auto-refresh adds a shallow filesystem walk on each agent turn while enabled. It avoids stale search results after local skill edits without restarting Pi.
165
+ - A full refresh rebuilds the SQLite FTS database and scales with total indexed skills and description length. Large vaults with many roots may prefer `autoRefreshOnChange: false` plus explicit `/shiori:reload` after bulk imports.
166
+ - Search stays description-first and policy-aware: only `triggerable` skills become candidates, with FTS ranking over names, descriptions, and policy triggers.
135
167
 
136
168
  ## Configure
137
169
 
@@ -185,7 +217,7 @@ This writes a generated review file next to `.pi/skill-shiori.yml`. Review it be
185
217
 
186
218
  | Command | Purpose |
187
219
  |---|---|
188
- | `/shiori:doctor` | Show policy path, inventory count, retrieval backend, suppression status, and code marker. |
220
+ | `/shiori:doctor` | Show policy path, indexed roots, inventory count, retrieval backend, suppression status, and code marker. |
189
221
  | `/shiori:bootstrap` | Generate a review draft policy from discovered skill descriptions. |
190
222
  | `/shiori:reload` | Ask what to reload, then rebuild Shiori’s skill inventory and optionally ask Pi to reload runtime resources. Code changes may still need full restart. |
191
223
  | `/shiori:recommend` | Ask what you need help with, then either pre-load matches and queue the task for the agent or review recommendations only. Planning phrases (e.g. `計画を立てたい`) return the dev-plan intake template without dumping SKILL bodies. |
@@ -281,7 +313,7 @@ npm login
281
313
  After publishing, verify the Pi install path:
282
314
 
283
315
  ```bash
284
- pi install npm:pi-skill-shiori@0.4.0
316
+ pi install npm:pi-skill-shiori@0.5.0
285
317
  pi install npm:pi-skill-shiori
286
318
  pi list
287
319
  /shiori:doctor
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-skill-shiori",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Pi extension that reduces Agent Skill catalog tokens with policy-based skill retrieval and on-demand skill loading.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/discovery.ts CHANGED
@@ -1,18 +1,42 @@
1
1
  import { readdir, readFile, stat } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
- import { basename, dirname, join, resolve } from "node:path";
3
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
4
4
  import { parse as parseYaml } from "yaml";
5
- import type { SkillRecord } from "./types.js";
5
+ import type { ShioriPolicy, SkillRecord } from "./types.js";
6
6
 
7
7
  const SKILL_FILE = "SKILL.md";
8
8
 
9
- export async function discoverSkills(cwd: string): Promise<SkillRecord[]> {
9
+ /** Project-local skill roots scanned on every vault-wide inventory build. */
10
+ export const VAULT_LOCAL_SKILL_ROOT_SEGMENTS = [".pi/skills", ".agents/skills"] as const;
11
+
12
+ /** User-global Pi agent skills root (lowest discovery precedence). */
13
+ export const GLOBAL_AGENT_SKILL_ROOT_SEGMENTS = ".pi/agent/skills";
14
+
15
+ export function resolveVaultSkillRoots(cwd: string, policy?: Pick<ShioriPolicy, "inventory">): string[] {
10
16
  const roots = [
11
17
  join(cwd, ".pi", "skills"),
12
18
  join(cwd, ".agents", "skills"),
13
19
  join(homedir(), ".pi", "agent", "skills"),
14
20
  ];
15
21
 
22
+ for (const entry of policy?.inventory?.roots ?? []) {
23
+ const trimmed = entry.trim();
24
+ if (!trimmed) continue;
25
+ const resolved = isAbsolute(trimmed) ? resolve(trimmed) : resolve(cwd, trimmed);
26
+ if (!roots.includes(resolved)) {
27
+ roots.push(resolved);
28
+ }
29
+ }
30
+
31
+ return roots;
32
+ }
33
+
34
+ export async function discoverSkills(cwd: string, policy?: ShioriPolicy): Promise<SkillRecord[]> {
35
+ const roots = resolveVaultSkillRoots(cwd, policy);
36
+ return discoverSkillsFromRoots(roots);
37
+ }
38
+
39
+ export async function discoverSkillsFromRoots(roots: string[]): Promise<SkillRecord[]> {
16
40
  const records: SkillRecord[] = [];
17
41
  for (const root of roots) {
18
42
  const files = await findSkillFiles(root);
@@ -23,6 +47,15 @@ export async function discoverSkills(cwd: string): Promise<SkillRecord[]> {
23
47
  return dedupeByDiscoveryOrder(records);
24
48
  }
25
49
 
50
+ export async function computeInventoryFingerprint(roots: string[]): Promise<string> {
51
+ const parts: string[] = [];
52
+ for (const root of roots) {
53
+ const files = await findSkillFiles(root);
54
+ parts.push(`${root}:${files.sort().join(",")}`);
55
+ }
56
+ return parts.join("|");
57
+ }
58
+
26
59
  export function findDuplicates(records: SkillRecord[]): Map<string, SkillRecord[]> {
27
60
  const groups = new Map<string, SkillRecord[]>();
28
61
  for (const record of records) {
package/src/index.ts CHANGED
@@ -3,8 +3,8 @@ import { Text } from "@earendil-works/pi-tui";
3
3
  import { Type } from "typebox";
4
4
  import { readFile, writeFile } from "node:fs/promises";
5
5
  import { evaluateAlwaysVisible, formatAlwaysVisibleDiagnostics } from "./always-visible.js";
6
- import { discoverSkills, findDuplicates } from "./discovery.js";
7
- import { buildSkillIndex, type SkillIndex } from "./indexer.js";
6
+ import { findDuplicates, formatInventoryRoots, isAutoRefreshEnabled, isInventoryStale, refreshSkillInventory } from "./inventory.js";
7
+ import type { SkillIndex } from "./indexer.js";
8
8
  import { loadRecommendedSkills } from "./load-skills.js";
9
9
  import { formatPlanningKickoffMessage, isPlanningIntent } from "./planning-kickoff.js";
10
10
  import { createStats } from "./metrics.js";
@@ -36,23 +36,42 @@ const CANCEL_CHOICE = "Cancel";
36
36
 
37
37
  export default function piSkillShiori(pi: ExtensionAPI) {
38
38
  let index: SkillIndex | undefined;
39
+ let inventoryFingerprint: string | undefined;
40
+ let inventoryRoots: string[] = [];
39
41
  let warnedAlwaysVisibleMissing = new Set<string>();
40
42
  const stats = createStats();
41
43
 
42
44
  async function reload(cwd: string): Promise<SkillIndex> {
43
- const [policy, skills] = await Promise.all([loadPolicy(cwd), discoverSkills(cwd)]);
44
- index?.close?.();
45
- index = await buildSkillIndex(cwd, skills, policy);
46
- stats.inventoryCount = skills.length;
47
- stats.duplicateCount = findDuplicates(skills).size;
45
+ const policy = await loadPolicy(cwd);
46
+ const inventory = await refreshSkillInventory(cwd, policy, index);
47
+ index = inventory.index;
48
+ inventoryFingerprint = inventory.fingerprint;
49
+ inventoryRoots = inventory.roots;
50
+ stats.inventoryCount = index.skills.length;
51
+ stats.duplicateCount = findDuplicates(index.skills).size;
48
52
  stats.lastReloadAt = index.builtAt;
49
53
  stats.retrievalBackend = index.retrievalBackend;
50
54
  stats.suppressionStatus = policy.zeroCatalog.enabled ? "not-needed" : "disabled";
55
+ stats.inventoryRefreshCount += 1;
51
56
  return index;
52
57
  }
53
58
 
54
59
  async function ensureIndex(cwd: string): Promise<SkillIndex> {
55
- return index ?? reload(cwd);
60
+ if (!index || !inventoryFingerprint) {
61
+ return reload(cwd);
62
+ }
63
+
64
+ const policy = await loadPolicy(cwd);
65
+ if (!isAutoRefreshEnabled(policy)) {
66
+ return index;
67
+ }
68
+
69
+ if (await isInventoryStale(cwd, policy, inventoryFingerprint)) {
70
+ stats.inventoryAutoRefreshCount += 1;
71
+ return reload(cwd);
72
+ }
73
+
74
+ return index;
56
75
  }
57
76
 
58
77
  function withRecommendLimits(current: SkillIndex) {
@@ -72,6 +91,8 @@ export default function piSkillShiori(pi: ExtensionAPI) {
72
91
  pi.on("session_shutdown", () => {
73
92
  index?.close?.();
74
93
  index = undefined;
94
+ inventoryFingerprint = undefined;
95
+ inventoryRoots = [];
75
96
  });
76
97
 
77
98
  pi.on("input", async (event) => {
@@ -210,6 +231,9 @@ export default function piSkillShiori(pi: ExtensionAPI) {
210
231
  `duplicates: ${duplicates.size}`,
211
232
  `alwaysVisible: ${current.policy.alwaysVisible.join(", ") || "none"}`,
212
233
  ...formatAlwaysVisibleDiagnostics(alwaysVisible),
234
+ "inventoryRoots:",
235
+ ...formatInventoryRoots(inventoryRoots, current.skills).map((line) => ` - ${line}`),
236
+ `autoRefreshOnChange: ${isAutoRefreshEnabled(current.policy) ? "enabled" : "disabled"}`,
213
237
  `retrievalBackend: ${current.retrievalBackend}`,
214
238
  `suppression: ${stats.suppressionStatus}`,
215
239
  `code: ${SHIORI_CODE_MARKER}`,
@@ -0,0 +1,58 @@
1
+ import {
2
+ computeInventoryFingerprint,
3
+ discoverSkillsFromRoots,
4
+ findDuplicates,
5
+ resolveVaultSkillRoots,
6
+ } from "./discovery.js";
7
+ import { buildSkillIndex, type SkillIndex } from "./indexer.js";
8
+ import type { ShioriPolicy } from "./types.js";
9
+
10
+ export interface SkillInventory {
11
+ index: SkillIndex;
12
+ roots: string[];
13
+ fingerprint: string;
14
+ }
15
+
16
+ export function isAutoRefreshEnabled(policy: ShioriPolicy): boolean {
17
+ return policy.inventory?.autoRefreshOnChange !== false;
18
+ }
19
+
20
+ export async function refreshSkillInventory(
21
+ cwd: string,
22
+ policy: ShioriPolicy,
23
+ previous?: SkillIndex,
24
+ ): Promise<SkillInventory> {
25
+ previous?.close?.();
26
+ const roots = resolveVaultSkillRoots(cwd, policy);
27
+ const [skills, fingerprint] = await Promise.all([
28
+ discoverSkillsFromRoots(roots),
29
+ computeInventoryFingerprint(roots),
30
+ ]);
31
+ const index = await buildSkillIndex(cwd, skills, policy);
32
+ return { index, roots, fingerprint };
33
+ }
34
+
35
+ export async function isInventoryStale(
36
+ cwd: string,
37
+ policy: ShioriPolicy,
38
+ fingerprint: string,
39
+ ): Promise<boolean> {
40
+ const roots = resolveVaultSkillRoots(cwd, policy);
41
+ const current = await computeInventoryFingerprint(roots);
42
+ return current !== fingerprint;
43
+ }
44
+
45
+ export function countSkillsByRoot(skills: SkillIndex["skills"], roots: string[]): Map<string, number> {
46
+ const counts = new Map(roots.map((root) => [root, 0]));
47
+ for (const skill of skills) {
48
+ counts.set(skill.source, (counts.get(skill.source) ?? 0) + 1);
49
+ }
50
+ return counts;
51
+ }
52
+
53
+ export function formatInventoryRoots(roots: string[], skills: SkillIndex["skills"]): string[] {
54
+ const counts = countSkillsByRoot(skills, roots);
55
+ return roots.map((root) => `${root} (${counts.get(root) ?? 0} skills)`);
56
+ }
57
+
58
+ export { findDuplicates };
package/src/metrics.ts CHANGED
@@ -10,5 +10,7 @@ export function createStats(): ShioriStats {
10
10
  explicitInvocationCount: 0,
11
11
  retrievalBackend: "token-match",
12
12
  suppressionStatus: "disabled",
13
+ inventoryRefreshCount: 0,
14
+ inventoryAutoRefreshCount: 0,
13
15
  };
14
16
  }
package/src/policy.ts CHANGED
@@ -30,6 +30,10 @@ export async function loadPolicy(cwd: string): Promise<ShioriPolicy> {
30
30
  }
31
31
 
32
32
  function normalizePolicy(policy: Partial<ShioriPolicy> | undefined): ShioriPolicy {
33
+ const extraRoots = (policy?.inventory?.roots ?? [])
34
+ .map((root) => (typeof root === "string" ? root.trim() : ""))
35
+ .filter(Boolean);
36
+
33
37
  return {
34
38
  zeroCatalog: {
35
39
  enabled: policy?.zeroCatalog?.enabled ?? DEFAULT_POLICY.zeroCatalog.enabled,
@@ -42,6 +46,13 @@ function normalizePolicy(policy: Partial<ShioriPolicy> | undefined): ShioriPolic
42
46
  minScore: policy?.candidateInjection?.minScore ?? DEFAULT_POLICY.candidateInjection.minScore,
43
47
  },
44
48
  alwaysVisible: normalizeAlwaysVisible(policy?.alwaysVisible ?? DEFAULT_POLICY.alwaysVisible).allowlist,
49
+ inventory:
50
+ extraRoots.length > 0 || policy?.inventory?.autoRefreshOnChange === false
51
+ ? {
52
+ roots: extraRoots.length > 0 ? extraRoots : undefined,
53
+ autoRefreshOnChange: policy?.inventory?.autoRefreshOnChange,
54
+ }
55
+ : undefined,
45
56
  skills: policy?.skills ?? DEFAULT_POLICY.skills,
46
57
  };
47
58
  }
package/src/types.ts CHANGED
@@ -14,6 +14,12 @@ export interface ShioriPolicy {
14
14
  minScore: number;
15
15
  };
16
16
  alwaysVisible: string[];
17
+ inventory?: {
18
+ /** Extra skill roots relative to cwd or absolute paths. */
19
+ roots?: string[];
20
+ /** Rebuild the index when skill files change under any root (default: true). */
21
+ autoRefreshOnChange?: boolean;
22
+ };
17
23
  skills: Record<string, SkillPolicy>;
18
24
  }
19
25
 
@@ -49,4 +55,6 @@ export interface ShioriStats {
49
55
  retrievalBackend: RetrievalBackend;
50
56
  suppressionStatus: SuppressionStatus;
51
57
  lastReloadAt?: string;
58
+ inventoryRefreshCount: number;
59
+ inventoryAutoRefreshCount: number;
52
60
  }