@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 +309 -97
- package/CHANGELOG.md +4 -0
- package/package.json +2 -2
- package/src/extension/skills/global-registry.ts +63 -0
- package/src/extension/skills/skill-registry.ts +56 -4
- package/src/motion/index.ts +1 -0
- package/src/motion/policy.ts +28 -1
- package/src/render/motion-rail.ts +70 -16
- package/src/render/v2-entry.ts +1 -1
- package/src/signal/controller.ts +10 -10
- package/src/studio/component.ts +213 -37
- package/src/studio/deepwiki/cache.ts +80 -6
- package/src/studio/list.ts +11 -1
- package/src/studio/open.ts +207 -15
- package/src/theme/colors.ts +2 -6
- package/src/tools/patch.ts +0 -179
- package/src/tools/ripgrep.ts +0 -104
package/AGENTS.md
CHANGED
|
@@ -1,102 +1,314 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@groeponline/pi-wishcraft",
|
|
3
|
-
"version": "1.4.
|
|
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.
|
|
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
|
-
|
|
353
|
+
const localEntries = [
|
|
344
354
|
...deduped.filter((e) => !looseNames.has(e.name)),
|
|
345
355
|
...loose,
|
|
346
356
|
...rejected,
|
|
347
|
-
]
|
|
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
|
-
|
|
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(
|
|
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 });
|