@groeponline/pi-wishcraft 1.4.11 → 1.4.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/AGENTS.md CHANGED
@@ -1,102 +1,314 @@
1
- # AGENTS.md
2
-
3
- Guidance for AI agents (and humans) working on this repository.
4
-
5
- ## What this project is
6
-
7
- pi-wishcraft is a powerline-style status bar and wishcraft interaction layer for the `pi`
8
- coding agent: status segments, welcome overlay/header, working "vibes" loading
9
- messages, a queue + idea inbox, bash mode, prompt/stash history, and full
10
- user customization.
11
-
12
- ## Repository layout
13
-
14
- - `index.ts` — package entry: the public barrel. It re-exports the default
15
- activation function and the documented public API (`resolveShortcutConfig`,
16
- `parseBashModeSettings`, `PowerlineShortcuts`). Keep it a barrel; no
17
- implementation lives here.
18
- - `src/` — the proxy-free extension runtime, organized by domain:
19
- - `src/extension/` the extension runtime, organized into domain subfolders:
20
- `core/` (constants, types, `state.ts` hub, segment-context), `commands/`
21
- (slash commands, queue commands, bash-mode actions, vibe command),
22
- `session/` (activation, session lifecycle, git invalidation, stale-context),
23
- `ui/` (custom editor, layout, menu views, powerline widgets, status-line
24
- renderers), `history/` (prompt + stash history), `queue/` (queue context +
25
- integration), `settings/` (settings IO), `shortcuts/` (shortcut config +
26
- router), `welcome/` (welcome control + integration), `skills/` (inline
27
- invocation). Leaf modules keep the dependency graph acyclic: shared types
28
- and constants live in `core/`, queue/welcome callbacks are wired through
29
- leaves or `RuntimeState`, and `core/state.ts` is the hub that only has
30
- inbound edges. Verify with `npx madge --circular src index.ts bash-mode
31
- queue`.
32
- - `src/config/`powerline config parsing, presets, types, segment ids/options.
33
- - `src/segments/`segment registry and the segment renderers.
34
- - `src/theme/`colors, icons, separators, theme loading.
35
- - `src/usage/` — token stats, context usage, currency rates.
36
- - `src/welcome/`welcome header/overlay rendering and discovery.
37
- - `src/working-vibes/`vibe theme storage, generation, manager.
38
- - `src/git/`, `src/shell/`, `src/editor/`, `src/render/`, `src/paths/`,
39
- `src/shortcuts/`, `src/lifecycle/`small single-purpose domains.
40
- - `bash-mode/`managed shell session, transcript store, completion engine.
41
- - `queue/` — file-backed queue/inbox store and types.
42
- - `tests/` flat `node:test` suite (`tests/*.test.ts`); structural tests read
43
- the module under test from `src/`, never from `index.ts` source text.
44
- - `scripts/release.mjs` zero-dep release helper (bump, CHANGELOG, tag).
45
-
46
- ## Rules
47
-
48
- - **No root-level monofiles.** Implementation lives under `src/` organized by
49
- domain; `index.ts` is only a barrel. Import implementation via
50
- `src/**` paths (or `bash-mode/`, `queue/`).
51
- - **Module size:** when a file exceeds ~450 lines, split it along domain
52
- boundaries (e.g. command handlers out of `commands.ts`, git invalidation
53
- out of `session-lifecycle.ts`). Prefer many cohesive small modules.
54
- - **No circular imports.** Domain modules must not import back into
55
- `src/extension/core/state.ts` for callbacks; move the callback into a leaf module
56
- (e.g. `welcome-control.ts`) or wire it through `RuntimeState`.
57
- - **Bun/Node-native TS:** `import` paths carry `.ts` extensions
58
- (`allowImportingTsExtensions`), `node:`-prefixed builtins, `node --test`
59
- with type stripping. No build step.
60
- - **English UI:** operator overlays, notify strings, and `/wishcraft` copy are
61
- English. Do not add Dutch UI strings.
62
- - **Tests:** behavior changes in `src/` need a focused regression test near
63
- the existing tests for that subsystem; structural tests assert on module
64
- files in `src/` (never `index.ts`).
65
-
66
- ## Commands
1
+ # Repository Guidelines
2
+
3
+ ## Project Overview
4
+
5
+ `pi-wishcraft` is an operator cockpit extension for the **Pi** coding agent (`@earendil-works/pi-coding-agent`). It provides a powerline-style status bar, welcome overlay/header, working "vibes" loading messages, a file-backed idea queue, sticky Bash mode with PTY sessions, inline skill invocation, command-level hooks/policy controls, and a fullscreen Skill Studio. The package ships as a Pi extension rooted at `./index.ts` and is published to npm as `@groeponline/pi-wishcraft`.
6
+
7
+ ## Architecture & Data Flow
8
+
9
+ ### Bootstrap & Runtime Hub
10
+
11
+ The extension bootstraps from `index.ts`, which re-exports `powerlineFooter` from `src/extension/session/activate.ts`. Activation reads Pi settings, parses powerline config, creates a mutable `RuntimeState` (via `createRuntimeState` in `src/extension/core/state.ts`), and registers Pi lifecycle hooks, commands, segments, presets, shortcuts, skills, and bash-mode sessions.
12
+
13
+ `src/extension/core/state.ts` is the **central mutable hub** — it holds `config`, schedulers, and derived caches. All runtime mutations flow through `RuntimeState` and callback hooks (`requestStatusRender`, `requestImmediateStatusRender`, `resetLayoutCache`, `dispatchSignalEvent`). Leaf modules consume and extend this hub but never import sibling leaves directly.
14
+
15
+ ### Rendering Pipeline
16
+
17
+ The render pipeline is a pure, acyclic chain:
18
+
19
+ 1. **SegmentContext** is built read-only by `src/extension/core/segment-context.ts` from `RuntimeState`, config, theme, git state, and usage data.
20
+ 2. **Segment renderers** are dispatched through `src/segments/registry.ts` a fault-isolated `renderSegment` that maps segment IDs to pure render functions.
21
+ 3. **Status line composition** happens in `src/render/v2-entry.ts`, which orders segments, inserts the motion rail from `src/render/motion-rail.ts`, then delegates painting to `src/render/paint.ts` and `src/render/v2-adapter.ts`.
22
+
23
+ ### Motion & Signal
24
+
25
+ The motion engine (`src/motion/*`) defines a catalog of animations (`src/motion/catalog.ts`), a centralized scheduler (`src/motion/scheduler.ts`), and an accessibility policy (`src/motion/policy.ts`). The Signal controller (`src/signal/controller.ts`) leases one motion consumer per runtime event and manages the lifecycle of rendered signals on the status-line rail.
26
+
27
+ ### Leaf Domains
28
+
29
+ Each domain operates through `RuntimeState` callbacks:
30
+
31
+ - **bash-mode/** — PTY-managed shell sessions, transcripts, ghost suggestions, completion engine
32
+ - **queue/**file-backed JSONL queue/inbox store with aliases and retention
33
+ - **working-vibes/**themed loading message generation and persistence
34
+ - **welcome/**fullscreen TUI overlay with branded layout and countdown
35
+ - **usage/** — token/cost/context ledger, TPS ring, daily budget
36
+ - **theme/**color resolution, icons, separators, token mapping
37
+ - **studio/**Skill Studio UI component, Deck renderer, advice engine
38
+ - **skills/** skill registry, manager, inline invocation, doctor, status
39
+ - **hooks/**session/context/tool hooks, policy engine, repairs
40
+ - **settings/**settings IO, config commands, appearance write-back
41
+
42
+ ### Acyclic Dependency Rule
43
+
44
+ Domain modules must not import back into the hub for callbacks. Shared types and constants live in `src/extension/core/`. Verify no circular imports:
45
+
46
+ ```bash
47
+ npm run circular
48
+ # madge --circular src index.ts bash-mode queue
49
+ ```
50
+
51
+ ## Key Directories
52
+
53
+ | Directory | Purpose |
54
+ |---|---|
55
+ | `src/` | Extension runtime, organized by domain (core, config, segments, render, signal, motion, theme, welcome, working-vibes, usage, studio, skills, hooks, settings, shortcuts, history, queue, contrib) |
56
+ | `src/extension/` | Pi extension runtime: `core/` (hub, types, constants), `session/` (activation, lifecycle), `ui/` (deck, layout), `commands/`, `shortcuts/`, `queue/`, `welcome/`, `skills/`, `hooks/`, `settings/`, `history/`, `contrib/` |
57
+ | `src/config/` | Powerline config parsing, presets, settings registry, tokens |
58
+ | `src/segments/` | Segment registry and builtin renderers (core, system, usage, custom) |
59
+ | `src/theme/` | Colors, icons, separators, token mapping |
60
+ | `src/render/` | Paint primitives, v2 adapter, layout, motion-rail |
61
+ | `src/motion/` | Motion catalog, scheduler, policy, types, gallery, composer |
62
+ | `src/signal/` | Signal controller and event dispatching |
63
+ | `src/welcome/` | Welcome overlay, renderer, art, layout |
64
+ | `src/studio/` | Studio UI component, deck, advice engine |
65
+ | `bash-mode/` | Standalone PTY shell session management (editor, transcript, completion, forward mode) |
66
+ | `queue/` | Standalone file-backed queue store and types |
67
+ | `tests/` | Flat `node:test` suite (`tests/*.test.ts`), helpers in `tests/helpers/`, fixtures in `tests/fixtures/` |
68
+ | `docs/` | Operator guides (configuration, commands, bash-mode, segments, skills, etc.) |
69
+ | `docs/design/` | vNext design specs (not operator how-to docs) |
70
+ | `.compound-engineering/` | CE overlay: tracked `config.yaml`, artifacts, plans |
71
+
72
+ ## Development Commands
67
73
 
68
74
  ```bash
75
+ # Install dependencies
69
76
  npm ci
70
- npm run typecheck # tsc --noEmit (strict)
71
- npm test # node --experimental-strip-types --test tests/**/*.test.ts
77
+
78
+ # Type check (strict, no emit)
79
+ npm run typecheck
80
+
81
+ # Run all tests
82
+ npm test
83
+
84
+ # Check for circular imports
85
+ npm run circular
86
+
87
+ # Full package contract verification (for publish preparation)
88
+ npm run verify:package
89
+
90
+ # Local preview server (Deck/Signal/motion surfaces)
91
+ npm run preview
92
+
93
+ # Dockerized parallel testing (typecheck + test + circular)
94
+ scripts/docker-test.sh [-n N]
95
+
96
+ # Cloud agent bootstrap (Node 24, pi CLI install)
97
+ scripts/cloud-agent-install.sh
98
+ scripts/cloud-agent-start.sh
99
+ ```
100
+
101
+ ### Release Flow
102
+
103
+ Releases are CI-driven via GitHub Actions:
104
+
105
+ ```bash
106
+ # Locally: bump version, rewrite CHANGELOG, tag, optional push
107
+ node scripts/release.mjs # or: npm run release
108
+
109
+ # Publish to npm (CI handles auth)
110
+ scripts/npm-publish.sh
111
+
112
+ # Create GitHub Release
113
+ scripts/github-release.sh
114
+ ```
115
+
116
+ CI workflows (`.github/workflows/`):
117
+ - `test.yml` — Node 24, typecheck, test, circular, verify:package, npm audit
118
+ - `release.yml` — reuses test.yml, prepares release-candidate branch
119
+ - `promote-release-candidate.yml` — promotes verified SHA to main, tags, dispatches publish
120
+
121
+ ## Code Conventions & Common Patterns
122
+
123
+ ### Module Structure
124
+
125
+ - **No root-level monofiles**: implementation lives under `src/` by domain; `index.ts` is a barrel only
126
+ - **Module size**: split files exceeding ~450 lines along domain boundaries
127
+ - **TypeScript**: `.ts` extensions on all imports (`allowImportingTsExtensions`), `node:`-prefixed builtins, strict mode, no build step
128
+ - **Entry point**: `index.ts` re-exports `powerlineFooter` and shortcut helpers — keep it a barrel, no implementation
129
+
130
+ ### Import Paths & Dependency Direction
131
+
132
+ ```typescript
133
+ // Correct: import via domain paths
134
+ import { SEGMENTS } from "src/segments/registry.ts";
135
+ import { createRuntimeState } from "src/extension/core/state.ts";
136
+
137
+ // Incorrect: avoid importing implementation from index.ts barrel
138
+ import { powerlineFooter } from "./index.ts"; // tests must not do this
139
+ ```
140
+
141
+ - Tests import from `src/<domain>/*` module files directly, **never** from `index.ts`
142
+ - Domain modules must not import back into the hub for callbacks — use `RuntimeState` + callback hooks
143
+
144
+ ### State Management
145
+
146
+ `RuntimeState` (from `src/extension/core/state.ts`) is the singleton mutable hub. It owns:
147
+ - `config` — parsed powerline configuration
148
+ - Motion/signal/render schedulers
149
+ - Derived caches (segment context, layout)
150
+
151
+ Leaf consumers receive `RuntimeState` plus callback hooks rather than importing sibling leaves directly.
152
+
153
+ ### Callback Wiring Pattern
154
+
155
+ ```typescript
156
+ // Leaf modules receive hooks, do not import siblings
157
+ function createRuntimeState(hooks: {
158
+ requestStatusRender: () => void;
159
+ requestImmediateStatusRender: () => void;
160
+ resetLayoutCache: () => void;
161
+ }): RuntimeState
162
+ ```
163
+
164
+ ### Error Handling
165
+
166
+ - Segment rendering is fault-isolated — a failing segment does not crash the status line
167
+ - Policy engine evaluates pre/post tool hooks with deny/inject results
168
+ - Hooks runner uses stdin JSON + timeout + process-group kill
169
+
170
+ ### Naming Conventions
171
+
172
+ - Domain barrels: `src/<domain>/index.ts`
173
+ - Segment files: `src/segments/{core,system,usage,custom}.ts`
174
+ - Motion modules: `src/motion/{catalog,scheduler,policy,types}.ts`
175
+ - Test files: `tests/<subsystem>.test.ts` (flat suite, descriptive names)
176
+
177
+ ## Important Files
178
+
179
+ ### Entry Points
180
+ - `index.ts` — package barrel (re-exports `powerlineFooter`, `resolveShortcutConfig`, `parseBashModeSettings`, `PowerlineShortcuts`)
181
+ - `src/extension/session/activate.ts` — activation bootstrap
182
+ - `src/extension/core/state.ts` — `RuntimeState` hub, `createRuntimeState`
183
+ - `src/extension/core/types.ts` — `RuntimeState` type definitions
184
+
185
+ ### Configuration
186
+ - `src/config/parse.ts` — `parsePowerlineConfig`, `PowerlineConfig` interface
187
+ - `src/config/settings-registry.ts` — operator settings definitions, defaults, validation
188
+ - `src/config/structural-presets.ts` — structural preset definitions
189
+ - `src/config/tokens.ts` — semantic token mapping
190
+
191
+ ### Rendering
192
+ - `src/render/v2-entry.ts` — `renderStatusLineV2` (composed render entry)
193
+ - `src/render/v2-adapter.ts` — layout adaptation for painting
194
+ - `src/render/paint.ts` — painting primitives
195
+ - `src/render/motion-rail.ts` — motion rail surface rendering
196
+
197
+ ### Segments
198
+ - `src/segments/registry.ts` — `SEGMENTS` registry, `renderSegment` dispatcher
199
+ - `src/segments/core.ts` — model, shell_mode, path, git, time, session, hostname
200
+ - `src/segments/system.ts` — thinking, subagents, queue, extension_statuses, open ports/TPS
201
+ - `src/segments/usage.ts` — token_in/out/total, cost, context_pct, cache_read/write
202
+ - `src/segments/custom.ts` — custom registered/static segments
203
+
204
+ ### Signal & Motion
205
+ - `src/signal/controller.ts` — `SignalRuntime` lifecycle
206
+ - `src/signal/integration.ts` — event dispatcher
207
+ - `src/motion/catalog.ts` — motion definitions, channel matrix
208
+ - `src/motion/scheduler.ts` — shared `MotionScheduler`
209
+ - `src/motion/policy.ts` — accessibility/policy filter
210
+
211
+ ### Bash Mode
212
+ - `bash-mode/pty-session.ts` — PTY session abstraction
213
+ - `bash-mode/session-factory.ts` — shell session creation
214
+ - `bash-mode/transcript.ts` — `BashTranscriptStore`
215
+ - `bash-mode/completion.ts` — completion engine
216
+
217
+ ### Queue
218
+ - `queue/store.ts` — `PowerlineQueueStore` (filesystem-backed JSONL)
219
+ - `queue/types.ts` — queue item/target/status types
220
+
221
+ ### Studio & Skills
222
+ - `src/studio/component.ts` — fullscreen Skill Studio component
223
+ - `src/studio/deck/render.ts` — Deck renderer
224
+ - `src/studio/advise/engine.ts` — advice pane engine
225
+ - `src/extension/skills/skill-registry.ts` — skill catalog
226
+ - `src/extension/skills/skill-manager.ts` — skill management commands
227
+
228
+ ### Release & Verification
229
+ - `scripts/release.mjs` — version bump, CHANGELOG rewrite, tag
230
+ - `scripts/verify-package.mjs` — package contract checks (name, description, keywords, pi manifest, resources, peers)
231
+ - `scripts/verify-pi-package-contract.mjs` — Pi package contract validation
232
+ - `scripts/npm-publish.sh` — idempotent npm publish with marker-tag promotion
233
+ - `scripts/github-release.sh` — GitHub Release creation
234
+ - `scripts/gen-lantern.mjs` — generates lantern art pixel-grid module
235
+
236
+ ## Runtime/Tooling Preferences
237
+
238
+ | Requirement | Detail |
239
+ |---|---|
240
+ | **Node** | v22.14 system default; Node 24 via nvm for CI and pi CLI (pi requires ≥22.19) |
241
+ | **Package manager** | npm (lockfile v3) |
242
+ | **TypeScript** | 5.9.3, strict mode, `NodeNext` module/resolution, `allowImportingTsExtensions`, no build step |
243
+ | **Test runner** | Node built-in `node:test` with `--experimental-strip-types` type stripping |
244
+ | **Circular import check** | `madge --circular src index.ts bash-mode queue` (via `npm run circular`) |
245
+ | **Import style** | `.ts` extensions on all imports, `node:`-prefixed builtins |
246
+ | **Pi CLI** | Installed at `^0.84.0`; wrapped to run under Node 24 via nvm |
247
+ | **Docker** | `fuse-overlayfs` storage driver for nested VM testing |
248
+ | **Compound Engineering** | `.compound-engineering/` overlay; portable skills via `~/.agents/skills/ce-*`; native Cursor plugin disabled |
249
+
250
+ ## Testing & QA
251
+
252
+ ### Test Framework
253
+
254
+ - **Runner**: Node's built-in `node:test` (flat per-file suites)
255
+ - **Assertions**: `node:assert/strict`
256
+ - **Type stripping**: `--experimental-strip-types` (no `tsc` compilation in tests)
257
+ - **Invocation**: `npm test` runs `node --experimental-strip-types --test tests/**/*.test.ts`
258
+
259
+ ### Test Organization
260
+
261
+ Tests are flat in `tests/` with supporting helpers and fixtures:
262
+
263
+ - `tests/helpers/strip-ansi.ts` — shared ANSI stripping utility
264
+ - `tests/fixtures/skill-template-golden.ts` — golden fixture for skill templates
265
+
266
+ ### Test Types
267
+
268
+ | Type | Convention | Examples |
269
+ |---|---|---|
270
+ | **Structural tests** | Assert on module contracts: layout rules, config precedence, catalog/scheduler invariants, registry behavior | `effective-config.test.ts`, `render-v2.test.ts`, `tokens.test.ts`, `hooks.test.ts` |
271
+ | **Behavior tests** | Assert runtime output: rendered strings, command execution, state changes, temp FS flows | `signal.test.ts`, `system-segments.test.ts`, `queue-store.test.ts`, `deck.test.ts` |
272
+ | **Golden tests** | Pin exact rendered output or template text | `signal-golden.test.ts`, `skill-templates.test.ts` |
273
+
274
+ ### Import Conventions
275
+
276
+ Tests import from `src/<domain>/*` module files directly — **never** from `index.ts`. For example:
277
+
278
+ ```typescript
279
+ // Correct
280
+ import { SEGMENTS } from "src/segments/registry.ts";
281
+
282
+ // Incorrect — do not import from index.ts
283
+ import { powerlineFooter } from "./index.ts";
284
+ ```
285
+
286
+ ### CI Verification Gates
287
+
288
+ The `.github/workflows/test.yml` workflow enforces the full verification chain:
289
+
290
+ ```bash
291
+ npm run typecheck # tsc --noEmit
292
+ npm test # node --test
293
+ npm run circular # madge --circular
294
+ npm run verify:package # verify-package.mjs + verify-pi-package-contract.mjs
295
+ npm audit # security audit
296
+ ```
297
+
298
+ ### Release Verification
299
+
300
+ Before any publish, `prepublishOnly` runs the full chain:
301
+
302
+ ```bash
303
+ tsc --noEmit && verify-package.mjs && verify-pi-package-contract.mjs
304
+ ```
305
+
306
+ ### Dockerized Parallel Testing
307
+
308
+ For isolated, parallel CI runs:
309
+
310
+ ```bash
311
+ scripts/docker-test.sh -n 4 # 4 parallel containers
72
312
  ```
73
313
 
74
- Run both before proposing any non-trivial change.
75
-
76
- Compound Engineering overlay: `.compound-engineering/` (tracked `config.yaml`, gitignored `config.local.yaml`). Artifact root `.compound-engineering/artifacts/`. Portable skills `~/.agents/skills/ce-*`; native Cursor plugin is fallback only when this overlay is absent.
77
-
78
- ## Cursor Cloud specific instructions
79
-
80
- The Cloud Agent environment is provisioned so the extension can be tested both as
81
- a library and inside real `pi`, with Docker for containerized/parallel runs.
82
-
83
- - **Node:** the platform `node` on `PATH` is v22.14 (fine for `npm ci`,
84
- `typecheck`, and the test suite, which use `--experimental-strip-types`).
85
- Node 24 is available via `nvm` and is what CI uses.
86
- - **pi CLI:** installed and pinned to the wishcraft peer range
87
- (`@earendil-works/pi-coding-agent` `>=0.81.0 <0.85.0`). pi requires Node
88
- `>=22.19`, so the `pi` launcher is wrapped to run under the `nvm` Node 24.
89
- `~/.pi/agent/settings.json` loads this checkout (`packages: ["/workspace"]`,
90
- `preset: chef`) so `pi` renders the wishcraft welcome/powerline on startup.
91
- Running a model needs a provider key (`/login`); the extension loads without
92
- one.
93
- - **Docker:** `dockerd` runs with the `fuse-overlayfs` storage driver (required
94
- in the nested VM). `scripts/cloud-agent-start.sh` starts it per boot;
95
- `scripts/cloud-agent-install.sh` is the idempotent bootstrap (`npm ci` plus
96
- self-healing pi setup).
97
- - **Containerized / parallel testing:** `scripts/docker-test.sh [-n N]` runs the
98
- full check suite (`typecheck` + `test` + `circular`) in `node:24` containers,
99
- each with an isolated copy of the tree, so `N` runs are safe in parallel.
100
- - **Publishing / pi.dev:** merges to `main` auto-publish to npm via the org
101
- `NPM_TOKEN` (`.github/workflows/release.yml`), and pi.dev mirrors npm. Gate
102
- the catalog contract with `npm run verify:package`.
314
+ Each container runs `typecheck + test + circular` with a shared read-only `node_modules` bind mount.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.4.13] - 2026-09-05
6
+
7
+ ## [1.4.12] - 2026-09-05
8
+
5
9
  ## [1.4.11] - 2026-08-30
6
10
 
7
11
  ## [1.4.10] - 2026-08-30
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groeponline/pi-wishcraft",
3
- "version": "1.4.11",
3
+ "version": "1.4.13",
4
4
  "description": "Operator cockpit for Pi: live powerline status, searchable skills, idea queue, sticky Bash, hooks, policy controls, and session UX.",
5
5
  "type": "module",
6
6
  "files": [
@@ -63,7 +63,7 @@
63
63
  },
64
64
  "devDependencies": {
65
65
  "@earendil-works/pi-ai": "^0.84.0",
66
- "@earendil-works/pi-coding-agent": "^0.84.0",
66
+ "@earendil-works/pi-coding-agent": "^0.84.3",
67
67
  "@earendil-works/pi-tui": "^0.84.0",
68
68
  "@types/node": "24.13.3",
69
69
  "madge": "^8.0.0",
@@ -0,0 +1,63 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ export interface GlobalRegistrySkill {
6
+ name: string;
7
+ description: string;
8
+ category: string;
9
+ family: string;
10
+ metaSkill: string | null;
11
+ role: string | null;
12
+ routerParent: string | null;
13
+ mounts: string[];
14
+ drift: boolean;
15
+ filePath: string;
16
+ }
17
+
18
+ interface RegistryPayload {
19
+ schema?: string;
20
+ skills?: unknown[];
21
+ }
22
+
23
+ export function globalRegistryPath(): string {
24
+ return process.env.CHEFGROEP_SKILL_REGISTRY?.trim() || join(homedir(), ".config", "chefgroep", "skill-registry", "skills.json");
25
+ }
26
+
27
+ function expandHome(path: string): string {
28
+ if (path === "~") return homedir();
29
+ if (path.startsWith("~/")) return join(homedir(), path.slice(2));
30
+ return path;
31
+ }
32
+
33
+ export function loadGlobalSkillRegistry(path = globalRegistryPath()): GlobalRegistrySkill[] {
34
+ if (!existsSync(path)) return [];
35
+ let payload: RegistryPayload;
36
+ try {
37
+ payload = JSON.parse(readFileSync(path, "utf8")) as RegistryPayload;
38
+ } catch {
39
+ return [];
40
+ }
41
+ if (payload.schema !== "chefgroep-global-skill-registry/v1" || !Array.isArray(payload.skills)) return [];
42
+ const out: GlobalRegistrySkill[] = [];
43
+ for (const raw of payload.skills) {
44
+ if (!raw || typeof raw !== "object") continue;
45
+ const skill = raw as Record<string, any>;
46
+ if (typeof skill.name !== "string" || typeof skill.description !== "string") continue;
47
+ const canonical = skill.canonical && typeof skill.canonical === "object" ? skill.canonical : {};
48
+ const canonicalPath = typeof canonical.path === "string" ? canonical.path : "";
49
+ out.push({
50
+ name: skill.name,
51
+ description: skill.description,
52
+ category: typeof skill.category === "string" ? skill.category : "general",
53
+ family: typeof skill.family === "string" ? skill.family : "general",
54
+ metaSkill: typeof skill.meta_skill === "string" ? skill.meta_skill : null,
55
+ role: typeof skill.role === "string" ? skill.role : null,
56
+ routerParent: typeof skill.router_parent === "string" ? skill.router_parent : null,
57
+ mounts: Array.isArray(skill.mounts) ? skill.mounts.filter((x: unknown): x is string => typeof x === "string") : [],
58
+ drift: skill.drift === true,
59
+ filePath: expandHome(canonicalPath),
60
+ });
61
+ }
62
+ return out;
63
+ }
@@ -14,6 +14,7 @@ import { existsSync, readdirSync, readFileSync, statSync, writeFileSync, mkdirSy
14
14
  import { basename, dirname, join, relative } from "node:path";
15
15
  import { getAgentDir, getAgentPath } from "../../paths/agent-dirs.ts";
16
16
  import { parseSkillFrontmatter, stripFrontmatter } from "../../core/frontmatter.ts";
17
+ import { loadGlobalSkillRegistry } from "./global-registry.ts";
17
18
 
18
19
  /** Category: where the skill came from. */
19
20
  export type SkillCategory = "global" | "project" | "prompts" | "extra";
@@ -39,6 +40,14 @@ export interface SkillEntry {
39
40
  frontmatterKeys: string[];
40
41
  /** Frontmatter trigger value (e.g. /test, /showcase). */
41
42
  trigger: string | null;
43
+ /** Machine-side ChefGroep routing metadata when the global registry is installed. */
44
+ routingCategory?: string;
45
+ routingFamily?: string;
46
+ metaSkill?: string | null;
47
+ role?: string | null;
48
+ routerParent?: string | null;
49
+ mounts?: string[];
50
+ registryDrift?: boolean;
42
51
  /** Diagnostic message (core diagnostics, or empty description). */
43
52
  warning?: string;
44
53
  }
@@ -108,6 +117,7 @@ export function setSkillCacheInvalidationHandler(handler: (() => void) | null):
108
117
 
109
118
  const usageCache = new Map<string, SkillUsage>();
110
119
  let usageLoaded = false;
120
+ let usageLoadedFrom: string | null = null;
111
121
 
112
122
  /** Drop the discovery cache (the next read scans again). */
113
123
  export function invalidateSkillCache(): void {
@@ -340,11 +350,24 @@ export function loadSkillCatalog(cwd: string = process.cwd()): SkillEntry[] {
340
350
  );
341
351
  cachedAt = now;
342
352
  cachedCwd = cwd;
343
- cachedEntries = [
353
+ const localEntries = [
344
354
  ...deduped.filter((e) => !looseNames.has(e.name)),
345
355
  ...loose,
346
356
  ...rejected,
347
- ].sort((a, b) => a.name.localeCompare(b.name));
357
+ ];
358
+
359
+ // Enrich local Pi entries with routing metadata, but do not append
360
+ // registry-only skills here: generic Pi catalog/count semantics stay local.
361
+ const registry = loadGlobalSkillRegistry();
362
+ const registryByName = new Map(registry.map((r) => [r.name, r] as const));
363
+ for (const entry of localEntries) {
364
+ const r = registryByName.get(entry.name);
365
+ if (!r) continue;
366
+ entry.routingCategory = r.category; entry.routingFamily = r.family;
367
+ entry.metaSkill = r.metaSkill; entry.role = r.role; entry.routerParent = r.routerParent;
368
+ entry.mounts = r.mounts; entry.registryDrift = r.drift;
369
+ }
370
+ cachedEntries = localEntries.sort((a, b) => a.name.localeCompare(b.name));
348
371
  cachedPathMap = new Map(cachedEntries.map((e) => [e.name, e.filePath] as const));
349
372
  cachedTriggerMap = new Map(
350
373
  cachedEntries
@@ -354,6 +377,32 @@ export function loadSkillCatalog(cwd: string = process.cwd()): SkillEntry[] {
354
377
  return cachedEntries;
355
378
  }
356
379
 
380
+ /** Full machine-side catalog for Skill Studio only. */
381
+ export function loadSkillStudioCatalog(cwd: string = process.cwd()): SkillEntry[] {
382
+ const local = loadSkillCatalog(cwd).map((e) => ({ ...e }));
383
+ const localNames = new Set(local.map((e) => e.name));
384
+ const registryOnly: SkillEntry[] = loadGlobalSkillRegistry()
385
+ .filter((r) => !localNames.has(r.name))
386
+ .map((r) => {
387
+ let sizeBytes = 0, lineCount = 0, mtimeMs = 0;
388
+ let warning: string | undefined;
389
+ try {
390
+ const content = readFileSync(r.filePath, "utf8");
391
+ sizeBytes = Buffer.byteLength(content, "utf8");
392
+ lineCount = content.split("\n").length;
393
+ mtimeMs = statSync(r.filePath).mtimeMs;
394
+ } catch { warning = "registry source not readable on this host"; }
395
+ return {
396
+ name: r.name, description: r.description, filePath: r.filePath, baseDir: dirname(r.filePath),
397
+ isDirectorySkill: basename(r.filePath) === "SKILL.md", category: "extra" as SkillCategory,
398
+ disableModelInvocation: false, sizeBytes, lineCount, mtimeMs, frontmatterKeys: [], trigger: null, warning,
399
+ routingCategory: r.category, routingFamily: r.family, metaSkill: r.metaSkill, role: r.role,
400
+ routerParent: r.routerParent, mounts: r.mounts, registryDrift: r.drift,
401
+ };
402
+ });
403
+ return [...local, ...registryOnly].sort((a, b) => a.name.localeCompare(b.name));
404
+ }
405
+
357
406
  /** Compat: name → file path (for inline-invocation $skill). */
358
407
  export function getAvailableSkills(): Map<string, string> {
359
408
  loadSkillCatalog();
@@ -376,10 +425,13 @@ function usageFile(): string {
376
425
  }
377
426
 
378
427
  function loadUsage(): void {
379
- if (usageLoaded) return;
428
+ const file = usageFile();
429
+ if (usageLoaded && usageLoadedFrom === file) return;
380
430
  usageLoaded = true;
431
+ usageLoadedFrom = file;
432
+ usageCache.clear();
381
433
  try {
382
- const raw = readFileSync(usageFile(), "utf8");
434
+ const raw = readFileSync(file, "utf8");
383
435
  const parsed = JSON.parse(raw) as Record<string, SkillUsage>;
384
436
  for (const [name, u] of Object.entries(parsed)) {
385
437
  usageCache.set(name, { count: u.count ?? 0, lastUsed: u.lastUsed ?? 0 });
@@ -36,6 +36,7 @@ export {
36
36
  allowedChannels,
37
37
  allowsColorTransition,
38
38
  cadenceFor,
39
+ channelsForMotion,
39
40
  describeMotionEvent,
40
41
  effectiveLevel,
41
42
  prefersAsciiGlyphs,