pi-skill-shiori 0.6.12 → 0.6.13

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
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## [0.6.13] - 2026-10-05
6
+
7
+ ### Added
8
+
9
+ - Add session-local `/shiori:stats` timing diagnostics for inventory refresh and skill retrieval, with additive JSON counters and no prompt, skill-body, or cross-session analytics storage.
10
+
5
11
  ## [0.6.12] - 2026-09-30
6
12
 
7
13
  ### Changed
package/Docs/usage.md CHANGED
@@ -167,6 +167,17 @@ command: offers=1, loaded=2, zeroHits=1
167
167
  top skills: auth-helper(1/2), deploy-kit(1/1)
168
168
  ```
169
169
 
170
+ ## Timing diagnostics
171
+
172
+ Run `/shiori:stats` to see a compact timing line alongside the existing recommendation feedback summary. The line separates inventory refresh cost from retrieval cost; the JSON payload exposes the same machine-readable counters under `timing.inventoryRefresh` and `timing.retrieval`:
173
+
174
+ - `count` — successful operations recorded in the current session
175
+ - `lastMs` — duration of the most recent successful operation
176
+ - `totalMs` — cumulative duration of successful operations
177
+ - `maxMs` — longest successful operation
178
+
179
+ Inventory refresh timing covers the successful session-start, manual `/shiori:reload`, and automatic refresh rebuilds. Retrieval timing covers successful auto-injection, `shiori_recommend`, and `/shiori:recommend` retrievals. These are in-memory, session-local diagnostics: Shiori does not store prompt text, full queries, skill bodies, or cross-session analytics.
180
+
170
181
  ## Agent tools
171
182
 
172
183
  Shiori registers two tools the agent can call after candidates appear:
package/README.md CHANGED
@@ -19,7 +19,7 @@ For maintenance priorities and the phased plan, see [`ROADMAP.md`](ROADMAP.md).
19
19
 
20
20
  Pi Skill Shiori is a [Pi](https://pi.dev/) package for vaults or projects with many Agent Skills where the default catalog becomes noisy and expensive. It hides the normal skill catalog when safe, recommends triggerable skills with compact summaries, and loads full `SKILL.md` bodies on demand.
21
21
 
22
- `0.6.12` is the current working release. Prompt-boundary suppression is intentionally conservative: if Shiori cannot safely recognize a catalog boundary, it leaves the prompt untouched and warns instead of deleting too much.
22
+ `0.6.13` is the current working release. Prompt-boundary suppression is intentionally conservative: if Shiori cannot safely recognize a catalog boundary, it leaves the prompt untouched and warns instead of deleting too much.
23
23
 
24
24
  ## Features
25
25
 
@@ -31,13 +31,14 @@ Pi Skill Shiori is a [Pi](https://pi.dev/) package for vaults or projects with m
31
31
  - **SQLite FTS retrieval** — indexes skill names, descriptions, and policy triggers with `node:sqlite` + FTS5, with token-match fallback.
32
32
  - **Recommendation reason badges** — compact `[trigger]`, `[description]`, and `[low match]` labels on surfaced skills.
33
33
  - **Session-local feedback metrics** — `/shiori:stats` tracks offers, loads, and follow-through without storing prompts.
34
+ - **Timing diagnostics** — `/shiori:stats` separates inventory refresh and retrieval cost without storing prompts, skill bodies, or cross-session analytics.
34
35
 
35
36
  ## Install
36
37
 
37
38
  ### Global install from npm
38
39
 
39
40
  ```bash
40
- pi install npm:pi-skill-shiori@0.6.12
41
+ pi install npm:pi-skill-shiori@0.6.13
41
42
  ```
42
43
 
43
44
  Without a version pin:
@@ -49,7 +50,7 @@ pi install npm:pi-skill-shiori
49
50
  ### Project-local install from npm
50
51
 
51
52
  ```bash
52
- pi install -l npm:pi-skill-shiori@0.6.12
53
+ pi install -l npm:pi-skill-shiori@0.6.13
53
54
  ```
54
55
 
55
56
  Without a version pin:
@@ -61,7 +62,7 @@ pi install -l npm:pi-skill-shiori
61
62
  ### Install from GitHub
62
63
 
63
64
  ```bash
64
- pi install git:github.com/eiei114/pi-skill-shiori@v0.6.12
65
+ pi install git:github.com/eiei114/pi-skill-shiori@v0.6.13
65
66
  pi install -l git:github.com/eiei114/pi-skill-shiori
66
67
  ```
67
68
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-skill-shiori",
3
- "version": "0.6.12",
3
+ "version": "0.6.13",
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",
@@ -45,7 +45,7 @@
45
45
  ]
46
46
  },
47
47
  "dependencies": {
48
- "yaml": "^2.8.1"
48
+ "yaml": "^2.9.1"
49
49
  },
50
50
  "peerDependencies": {
51
51
  "@earendil-works/pi-ai": "*",
package/src/index.ts CHANGED
@@ -7,7 +7,7 @@ import { findDuplicates, formatInventoryRoots, isAutoRefreshEnabled, isInventory
7
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
- import { createStats } from "./metrics.js";
10
+ import { createStats, formatTimingSummary, measureTiming } from "./metrics.js";
11
11
  import { createRecommendationFeedback } from "./recommendation-feedback.js";
12
12
  import { getPolicyPath, loadPolicyWithSource, loadedPolicySignature, type LoadedPolicy } from "./policy.js";
13
13
  import {
@@ -48,7 +48,10 @@ export default function piSkillShiori(pi: ExtensionAPI) {
48
48
  async function reload(cwd: string, currentPolicy?: LoadedPolicy): Promise<SkillIndex> {
49
49
  currentPolicy ??= await loadPolicyWithSource(cwd);
50
50
  const policy = currentPolicy.policy;
51
- const inventory = await refreshSkillInventory(cwd, policy, index);
51
+ const inventory = await measureTiming(
52
+ stats.timing.inventoryRefresh,
53
+ () => refreshSkillInventory(cwd, policy, index),
54
+ );
52
55
  index = inventory.index;
53
56
  inventoryFingerprint = inventory.fingerprint;
54
57
  inventoryRoots = inventory.roots;
@@ -139,7 +142,10 @@ export default function piSkillShiori(pi: ExtensionAPI) {
139
142
  ctx.ui.notify("Pi Skill Shiori: Skill Catalog suppression pattern not found; leaving system prompt unchanged.", "warning");
140
143
  }
141
144
 
142
- const candidates = retrieveCandidatesExpanded(event.prompt, current.skills, current.policy, current);
145
+ const candidates = await measureTiming(
146
+ stats.timing.retrieval,
147
+ () => retrieveCandidatesExpanded(event.prompt, current.skills, current.policy, current),
148
+ );
143
149
  if (candidates.length === 0) {
144
150
  stats.zeroCandidateCount += 1;
145
151
  recommendationFeedback.recordZeroCandidates("auto-inject");
@@ -174,7 +180,10 @@ export default function piSkillShiori(pi: ExtensionAPI) {
174
180
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
175
181
  const current = await ensureIndex(ctx.cwd);
176
182
  const policy = withRecommendLimits(current);
177
- const candidates = retrieveCandidatesExpanded(params.task, current.skills, policy, current);
183
+ const candidates = await measureTiming(
184
+ stats.timing.retrieval,
185
+ () => retrieveCandidatesExpanded(params.task, current.skills, policy, current),
186
+ );
178
187
  const candidateNames = candidates.map((candidate) => candidate.skill.name);
179
188
  const loaded = await loadRecommendedSkills(candidates);
180
189
  recommendationFeedback.recordRecommendation(
@@ -340,7 +349,10 @@ export default function piSkillShiori(pi: ExtensionAPI) {
340
349
  }
341
350
 
342
351
  const policy = withRecommendLimits(current);
343
- const candidates = retrieveCandidatesExpanded(query, current.skills, policy, current);
352
+ const candidates = await measureTiming(
353
+ stats.timing.retrieval,
354
+ () => retrieveCandidatesExpanded(query, current.skills, policy, current),
355
+ );
344
356
  const candidateNames = candidates.map((candidate) => candidate.skill.name);
345
357
  const planning = isPlanningIntent(query);
346
358
  const reviewOnly = action === RECOMMEND_REVIEW_ONLY;
@@ -417,11 +429,16 @@ export default function piSkillShiori(pi: ExtensionAPI) {
417
429
  handler: async (_args, ctx) => {
418
430
  const payload = {
419
431
  ...stats,
432
+ timing: {
433
+ inventoryRefresh: { ...stats.timing.inventoryRefresh },
434
+ retrieval: { ...stats.timing.retrieval },
435
+ },
420
436
  recommendationFeedback: recommendationFeedback.summarize(),
421
437
  };
422
438
  ctx.ui.notify(
423
439
  [
424
440
  recommendationFeedback.formatSummary(),
441
+ formatTimingSummary(stats.timing),
425
442
  "",
426
443
  JSON.stringify(payload, null, 2),
427
444
  ].join("\n"),
package/src/metrics.ts CHANGED
@@ -1,4 +1,26 @@
1
- import type { ShioriStats } from "./types.js";
1
+ import { performance } from "node:perf_hooks";
2
+ import type { ShioriStats, ShioriTiming, TimingCounter } from "./types.js";
3
+
4
+ const TIMING_DECIMAL_PLACES = 2;
5
+ const TIMING_SCALE = 10 ** TIMING_DECIMAL_PLACES;
6
+
7
+ type MonotonicClock = () => number;
8
+
9
+ export function createTimingCounter(): TimingCounter {
10
+ return {
11
+ lastMs: 0,
12
+ totalMs: 0,
13
+ maxMs: 0,
14
+ count: 0,
15
+ };
16
+ }
17
+
18
+ export function createTiming(): ShioriTiming {
19
+ return {
20
+ inventoryRefresh: createTimingCounter(),
21
+ retrieval: createTimingCounter(),
22
+ };
23
+ }
2
24
 
3
25
  export function createStats(): ShioriStats {
4
26
  return {
@@ -12,5 +34,47 @@ export function createStats(): ShioriStats {
12
34
  suppressionStatus: "disabled",
13
35
  inventoryRefreshCount: 0,
14
36
  inventoryAutoRefreshCount: 0,
37
+ timing: createTiming(),
15
38
  };
16
39
  }
40
+
41
+ export function recordTiming(counter: TimingCounter, elapsedMs: number): void {
42
+ const duration = roundMilliseconds(elapsedMs);
43
+ counter.lastMs = duration;
44
+ counter.totalMs = roundMilliseconds(counter.totalMs + duration);
45
+ counter.maxMs = Math.max(counter.maxMs, duration);
46
+ counter.count += 1;
47
+ }
48
+
49
+ export async function measureTiming<T>(
50
+ counter: TimingCounter,
51
+ operation: () => T | PromiseLike<T>,
52
+ now: MonotonicClock = () => performance.now(),
53
+ ): Promise<T> {
54
+ const startedAt = now();
55
+ const result = await operation();
56
+ recordTiming(counter, now() - startedAt);
57
+ return result;
58
+ }
59
+
60
+ export function formatTimingSummary(timing: ShioriTiming): string {
61
+ return [
62
+ "Timing (session-local)",
63
+ `inventory refresh: ${formatTimingCounter(timing.inventoryRefresh)}`,
64
+ `retrieval: ${formatTimingCounter(timing.retrieval)}`,
65
+ ].join(" | ");
66
+ }
67
+
68
+ function formatTimingCounter(counter: TimingCounter): string {
69
+ const count = Number.isFinite(counter.count) && counter.count > 0 ? Math.trunc(counter.count) : 0;
70
+ return `count=${count} last=${formatMilliseconds(counter.lastMs)} total=${formatMilliseconds(counter.totalMs)} max=${formatMilliseconds(counter.maxMs)}`;
71
+ }
72
+
73
+ function formatMilliseconds(value: number): string {
74
+ return `${roundMilliseconds(value).toFixed(TIMING_DECIMAL_PLACES)}ms`;
75
+ }
76
+
77
+ function roundMilliseconds(value: number): number {
78
+ if (!Number.isFinite(value) || value <= 0) return 0;
79
+ return Math.round((value + Number.EPSILON) * TIMING_SCALE) / TIMING_SCALE;
80
+ }
package/src/types.ts CHANGED
@@ -48,6 +48,18 @@ export interface SkillCandidate {
48
48
  reason: RecommendationReasonKind | null;
49
49
  }
50
50
 
51
+ export interface TimingCounter {
52
+ lastMs: number;
53
+ totalMs: number;
54
+ maxMs: number;
55
+ count: number;
56
+ }
57
+
58
+ export interface ShioriTiming {
59
+ inventoryRefresh: TimingCounter;
60
+ retrieval: TimingCounter;
61
+ }
62
+
51
63
  export interface ShioriStats {
52
64
  inventoryCount: number;
53
65
  duplicateCount: number;
@@ -60,4 +72,5 @@ export interface ShioriStats {
60
72
  lastReloadAt?: string;
61
73
  inventoryRefreshCount: number;
62
74
  inventoryAutoRefreshCount: number;
75
+ timing: ShioriTiming;
63
76
  }