@groeponline/pi-wishcraft 0.27.0 → 0.27.2
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 +2 -0
- package/CHANGELOG.md +13 -0
- package/README.md +1 -1
- package/ROADMAP.md +97 -39
- package/package.json +1 -1
- package/src/extension/settings/wishcraft-config.ts +54 -55
- package/src/extension/skills/inline-invocation.ts +9 -9
- package/src/extension/skills/skill-manager.ts +47 -47
- package/src/extension/skills/skill-registry.ts +28 -28
- package/src/extension/ui/overlay-chrome.ts +1 -1
package/AGENTS.md
CHANGED
|
@@ -57,6 +57,8 @@ user customization.
|
|
|
57
57
|
- **Bun/Node-native TS:** `import` paths carry `.ts` extensions
|
|
58
58
|
(`allowImportingTsExtensions`), `node:`-prefixed builtins, `node --test`
|
|
59
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.
|
|
60
62
|
- **Tests:** behavior changes in `src/` need a focused regression test near
|
|
61
63
|
the existing tests for that subsystem; structural tests assert on module
|
|
62
64
|
files in `src/` (never `index.ts`).
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.27.2] - 2026-08-20
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- `examples/settings.example.json` — every setting at its default, including
|
|
9
|
+
`wishcraft.policy`, `readHints`, and other post-0.22 keys.
|
|
10
|
+
- ROADMAP top-15 maturity track rebaselined on 0.27.x (English); landed
|
|
11
|
+
0.23–0.27 items moved to "Already shipped".
|
|
12
|
+
|
|
13
|
+
## [0.27.1] - 2026-08-20
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- Operator UI is English: skill manager, `/wishcraft` TUI, overlay empty-match row, and skill-health warnings.
|
|
17
|
+
|
|
5
18
|
## [0.27.0] - 2026-08-20
|
|
6
19
|
|
|
7
20
|
### Added
|
package/README.md
CHANGED
|
@@ -91,7 +91,7 @@ Keybinds (`powerlineShortcuts`, applied after `/reload`; `null` disables):
|
|
|
91
91
|
}
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
-
`chef` is muted colors, slash separators, live TPS in/out, and TCP port count. Built-in presets: `default`, `minimal`, `compact`, `full`, `nerd`, `ascii`, `chef`. Custom segments, labels, layout, and presets are documented in [docs/configuration.md](docs/configuration.md).
|
|
94
|
+
`chef` is muted colors, slash separators, live TPS in/out, and TCP port count. Built-in presets: `default`, `minimal`, `compact`, `full`, `nerd`, `ascii`, `chef`. Custom segments, labels, layout, and presets are documented in [docs/configuration.md](docs/configuration.md). For every setting at its default, see [`examples/settings.example.json`](examples/settings.example.json).
|
|
95
95
|
|
|
96
96
|
Nerd Fonts auto-detect for iTerm, WezTerm, Kitty, Ghostty, and Alacritty; ASCII otherwise. `POWERLINE_NERD_FONTS=0` forces ASCII.
|
|
97
97
|
|
package/ROADMAP.md
CHANGED
|
@@ -98,6 +98,63 @@ Niet de fork. Niet de SaaS-agent.
|
|
|
98
98
|
documentatie die waar is. Done = README dekt alles wat we shipten,
|
|
99
99
|
geen kapotte footer-belofte.
|
|
100
100
|
|
|
101
|
+
## Top-15 track: remaining maturity gaps
|
|
102
|
+
|
|
103
|
+
Written 2026-08-20, rebaselined on 0.27.1 (code review, not this ROADMAP
|
|
104
|
+
alone). Feature density is high; several original gaps closed in
|
|
105
|
+
0.23–0.27. What remains is the maturity layer that separates a top-15 pi
|
|
106
|
+
extension from a feature-rich prototype.
|
|
107
|
+
|
|
108
|
+
### Already shipped (0.22.x–0.27.x)
|
|
109
|
+
- **CHE-41 per-segment detail** — `→` in Navigate, snapshot on open (0.22.1).
|
|
110
|
+
- **CHE-42 drill-down** — #19 + Configure in #13.
|
|
111
|
+
- **Changelog roll** — version headers drive the what's-new panel.
|
|
112
|
+
- **Per-segment fault isolation** — throwing custom segments show `!id`
|
|
113
|
+
instead of blanking the footer (#34, 0.23.1).
|
|
114
|
+
- **macOS open_ports** — netstat dot-address parsing (#34, 0.23.1).
|
|
115
|
+
- **GitHub Release per npm tag** (#25, 0.23.2).
|
|
116
|
+
- **CodeQL proto-pollution hardening** (#27, 0.23.3).
|
|
117
|
+
- **setupHooks wired** (#26, 0.23.4).
|
|
118
|
+
- **Policy engine** — declarative deny/inject, no spawn (GRO-1418, #29,
|
|
119
|
+
0.24.0).
|
|
120
|
+
- **`/skills doctor`** — broken frontmatter, dupes, unused, budget
|
|
121
|
+
(GRO-1416, #28, 0.24.0 / 0.25.0 tag).
|
|
122
|
+
- **`/skills new` templates** — no marketplace (GRO-1417, #32, 0.26.0).
|
|
123
|
+
- **`/ideas` review overlay** — status, tags, skill insert (GRO-1419, #31,
|
|
124
|
+
0.27.0).
|
|
125
|
+
- **English operator UI** — overlays, skill manager, `/wishcraft` TUI
|
|
126
|
+
(GRO-1422, #24, 0.27.1).
|
|
127
|
+
- **`powerline.skills.count` + read hints + Status trim** (GRO-1420, 0.23.0).
|
|
128
|
+
|
|
129
|
+
### Remaining open gaps (maturity)
|
|
130
|
+
P0 — release gate for 1.0:
|
|
131
|
+
1. **Settings contract to pi core.** No `contributes.settings`/schema;
|
|
132
|
+
users hand-edit JSON. *Fix: typed settings schema + contributions.*
|
|
133
|
+
|
|
134
|
+
P1 — differentiation and quality:
|
|
135
|
+
2. **Zero-config first run.** Install still expects JSON edits for the most
|
|
136
|
+
useful features. *Fix: sensible defaults + first-run setup overlay.*
|
|
137
|
+
3. **Perf budget / low-power mode.** Status renders every ~33ms; heavy
|
|
138
|
+
segments (bash-history, git) can hit the hot path. *Fix: configurable
|
|
139
|
+
refresh + lite mode.*
|
|
140
|
+
4. **Accessibility (no-color / reduced-motion).** Truecolor + animations
|
|
141
|
+
(vibes, rainbow think) break on terminals without truecolor. *Fix:
|
|
142
|
+
`NO_COLOR`/8-color + reduced-motion respect.*
|
|
143
|
+
5. **Semver policy / public API stability.** Still 0.x; top-15 extensions
|
|
144
|
+
ship a stability contract + deprecation path. *Fix: documented API
|
|
145
|
+
surface + semver policy. **1.0 cut: GRO-1421 (next).***
|
|
146
|
+
|
|
147
|
+
P2 — full product, post-1.0 or ride-along on GRO-1415:
|
|
148
|
+
6. **Preset editor in-menu** — custom JSON-only today.
|
|
149
|
+
7. **Skill install from repo/npm** — discovery + doctor exist; install and
|
|
150
|
+
curate missing.
|
|
151
|
+
8. **Host-status integration** — `ctx.ui.setStatus` beside the footer so
|
|
152
|
+
status also shows in host UI (`skills.count` is a start; full coverage
|
|
153
|
+
remains).
|
|
154
|
+
|
|
155
|
+
Linear tickets for open gaps are still to be filed (not invented in this
|
|
156
|
+
ROADMAP).
|
|
157
|
+
|
|
101
158
|
---
|
|
102
159
|
|
|
103
160
|
## 0.19.0 — Correctheid
|
|
@@ -219,50 +276,49 @@ de zes gevallen + de parse-vóór-wrap invariant.
|
|
|
219
276
|
|
|
220
277
|
### PR G — overlay-submenus (CHE-42) — ✅ #19 + expansion (#13 reopen)
|
|
221
278
|
|
|
222
|
-
|
|
223
|
-
descriptions)
|
|
224
|
-
|
|
225
|
-
Status). Configure
|
|
226
|
-
is Done (#18). CHE-41 is Done: `→` in Navigate, snapshot
|
|
279
|
+
The `alt+p` menu gets stacked `SelectList` overlays (arrows +
|
|
280
|
+
descriptions) instead of flat `ctx.ui.select`. Maximum three top-level
|
|
281
|
+
entries. #19 lands the top-level tree (Navigate / Configure /
|
|
282
|
+
Status). Configure now uses `showSelectOverlay` (CHE-42 remainder). CHE-40
|
|
283
|
+
is Done (#18). CHE-41 is Done: `→` in Navigate, snapshot on open, no second `alt+i` path.
|
|
227
284
|
|
|
228
285
|
### PR H — skills manager v2 UI + token-overlays
|
|
229
286
|
|
|
230
|
-
|
|
287
|
+
Replaces `skill-manager.ts` (242 lines). The data layer builds on Pi
|
|
231
288
|
core `loadSkills` / `loadSkillsFromDir` / `Skill` /
|
|
232
|
-
`SkillFrontmatter` (
|
|
289
|
+
`SkillFrontmatter` (publicly exported in
|
|
233
290
|
`@earendil-works/pi-coding-agent`).
|
|
234
291
|
|
|
235
|
-
UI,
|
|
236
|
-
|
|
237
|
-
-
|
|
238
|
-
case-insensitive; `ctrl+u`
|
|
239
|
-
`
|
|
240
|
-
-
|
|
241
|
-
`tab`
|
|
242
|
-
- `→` detail (frontmatter
|
|
243
|
-
`enter`
|
|
244
|
-
spawn
|
|
245
|
-
`~/.pi/agent/skills/<
|
|
246
|
-
- Usage-ledger `~/.pi/agent/skill-usage.json` (
|
|
247
|
-
trigger
|
|
248
|
-
- Skill health: core-diagnostics
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
dezelfde ring als het segment (geen tweede waarheid).
|
|
292
|
+
UI, non-negotiable:
|
|
293
|
+
|
|
294
|
+
- Working search: substring on name + description + path,
|
|
295
|
+
case-insensitive; `ctrl+u` clears; empty match = row
|
|
296
|
+
`no skills for '<q>'`.
|
|
297
|
+
- Categories with headings (bundled / global / project / prompts);
|
|
298
|
+
`tab` switches the filter; `s` sorts name ↔ usage.
|
|
299
|
+
- `→` detail (frontmatter table, usage, path, body + scroll);
|
|
300
|
+
`enter` inserts; `e` opens `$EDITOR` (default nvim) via external
|
|
301
|
+
spawn like pi's `!`; `n` creates a new skill in
|
|
302
|
+
`~/.pi/agent/skills/<name>/SKILL.md`; `d` delete + confirm.
|
|
303
|
+
- Usage-ledger `~/.pi/agent/skill-usage.json` (name, timestamp,
|
|
304
|
+
trigger type). Best-effort, never blocking on the input hot path.
|
|
305
|
+
- Skill health: core-diagnostics as a warning icon; `?` explains it.
|
|
306
|
+
|
|
307
|
+
Same chrome as the segment navigator. English, direct.
|
|
308
|
+
|
|
309
|
+
At the same time, small additions:
|
|
310
|
+
|
|
311
|
+
- `/tps` overlay: in/out, peak + average over the existing ring.
|
|
312
|
+
No new sampler.
|
|
313
|
+
- `/usage` overlay: today / this week / this session; per model;
|
|
314
|
+
cache-hit %; ASCII sparkline. File
|
|
315
|
+
`~/.pi/agent/wishcraft-usage.json` (append-only, compaction at
|
|
316
|
+
threshold).
|
|
317
|
+
- `wishcraft.tokenBudget.daily` colors the segment red and warns
|
|
318
|
+
in the welcome at 80% / 100%. Never blocking.
|
|
319
|
+
|
|
320
|
+
Done: substring filter tests; usage-ledger tests; `/tps` reads
|
|
321
|
+
the same ring as the segment (no second source of truth).
|
|
266
322
|
|
|
267
323
|
---
|
|
268
324
|
|
|
@@ -314,7 +370,9 @@ die package nog leeft.
|
|
|
314
370
|
|
|
315
371
|
## Kwaliteit (altijd)
|
|
316
372
|
|
|
317
|
-
- Overlay
|
|
373
|
+
- Overlay logic is testable via pure functions. No headless `ctx.ui`.
|
|
374
|
+
- **English UI:** operator overlays, notify strings, and `/wishcraft` copy are
|
|
375
|
+
English. Do not add Dutch UI strings.
|
|
318
376
|
- `prepublishOnly` = `tsc --noEmit`. Een slecht type ship't niet.
|
|
319
377
|
- `madge --circular` blijft CI. Nieuwe map in `src/` → check mee.
|
|
320
378
|
- Dependabot-vulns (devDep-transitief): waiven, track op GRO-603.
|
package/package.json
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* wishcraft-config.ts
|
|
3
3
|
* ---------------------------------------------------------------------------
|
|
4
|
-
* `/wishcraft` —
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* rendert en bewerkt ze generiek.
|
|
4
|
+
* `/wishcraft` — one configuration TUI for every wishcraft setting:
|
|
5
|
+
* grouped, directly editable (toggle, choice, text, number), written live
|
|
6
|
+
* to settings and visible immediately. Data-driven: settings are declared
|
|
7
|
+
* as ConfigItem[]; the overlay renders and edits them generically.
|
|
9
8
|
* ---------------------------------------------------------------------------
|
|
10
9
|
*/
|
|
11
10
|
|
|
@@ -24,17 +23,17 @@ import { parsePowerlineConfig } from "../../config/powerline-config.ts";
|
|
|
24
23
|
export type ConfigValue = boolean | string | number | null;
|
|
25
24
|
|
|
26
25
|
export interface ConfigItem {
|
|
27
|
-
/** Label in
|
|
26
|
+
/** Label in the list. */
|
|
28
27
|
label: string;
|
|
29
|
-
/**
|
|
28
|
+
/** Path inside settings ("powerline.placement", "wishcraft.hooksEnabled", ...). */
|
|
30
29
|
path: string;
|
|
31
|
-
/**
|
|
30
|
+
/** Edit kind. */
|
|
32
31
|
kind: "toggle" | "select" | "text" | "number";
|
|
33
|
-
/**
|
|
32
|
+
/** For select: the choices. */
|
|
34
33
|
choices?: string[];
|
|
35
|
-
/**
|
|
34
|
+
/** Hint under the group. */
|
|
36
35
|
hint?: string;
|
|
37
|
-
/**
|
|
36
|
+
/** Detail explanation (optional). */
|
|
38
37
|
description?: string;
|
|
39
38
|
}
|
|
40
39
|
|
|
@@ -43,7 +42,7 @@ export interface ConfigGroup {
|
|
|
43
42
|
items: ConfigItem[];
|
|
44
43
|
}
|
|
45
44
|
|
|
46
|
-
/**
|
|
45
|
+
/** Nested read: "wishcraft.hooksEnabled" → settings.wishcraft.hooksEnabled. */
|
|
47
46
|
export function readConfigPath(settings: Record<string, unknown>, path: string): ConfigValue {
|
|
48
47
|
let cur: unknown = settings;
|
|
49
48
|
for (const part of path.split(".")) {
|
|
@@ -130,61 +129,61 @@ const SEPARATORS = [
|
|
|
130
129
|
"star",
|
|
131
130
|
];
|
|
132
131
|
|
|
133
|
-
/**
|
|
132
|
+
/** Build groups from current settings (values shown live). */
|
|
134
133
|
export function buildConfigGroups(settings: Record<string, unknown>): ConfigGroup[] {
|
|
135
134
|
return [
|
|
136
135
|
{
|
|
137
|
-
title: "
|
|
136
|
+
title: "Status bar",
|
|
138
137
|
items: [
|
|
139
138
|
{ label: "Preset", path: "powerline.preset", kind: "select", choices: ["default", "minimal", "compact", "full", "nerd", "ascii", "chef"] },
|
|
140
139
|
{ label: "Separator", path: "powerline.separator", kind: "select", choices: SEPARATORS },
|
|
141
|
-
{ label: "
|
|
142
|
-
{ label: "
|
|
143
|
-
{ label: "
|
|
144
|
-
{ label: "
|
|
145
|
-
{ label: "
|
|
146
|
-
{ label: "Git host
|
|
140
|
+
{ label: "Placement", path: "powerline.placement", kind: "select", choices: ["above", "below"] },
|
|
141
|
+
{ label: "Path mode", path: "powerline.segmentOptions.path.mode", kind: "select", choices: ["basename", "abbreviated", "full"] },
|
|
142
|
+
{ label: "Path max length", path: "powerline.segmentOptions.path.maxLength", kind: "number", hint: "0 = unlimited" },
|
|
143
|
+
{ label: "Time format", path: "powerline.segmentOptions.time.format", kind: "select", choices: ["12h", "24h"] },
|
|
144
|
+
{ label: "Time seconds", path: "powerline.segmentOptions.time.showSeconds", kind: "toggle" },
|
|
145
|
+
{ label: "Git host icons", path: "powerline.segmentOptions.git.hostIcon", kind: "toggle" },
|
|
147
146
|
{ label: "Git ahead/behind", path: "powerline.segmentOptions.git.showAheadBehind", kind: "toggle" },
|
|
148
|
-
{ label: "Git
|
|
149
|
-
{ label: "Context
|
|
150
|
-
{ label: "Cache-read
|
|
151
|
-
{ label: "
|
|
152
|
-
{ label: "
|
|
153
|
-
{ label: "Ports
|
|
154
|
-
{ label: "TPS
|
|
155
|
-
{ label: "TPS
|
|
156
|
-
{ label: "TPS label", path: "powerline.segmentLabels.tps", kind: "text", hint: "
|
|
147
|
+
{ label: "Git latest commit", path: "powerline.segmentOptions.git.showCommit", kind: "toggle" },
|
|
148
|
+
{ label: "Context format", path: "powerline.segmentOptions.context.format", kind: "select", choices: ["full", "percent"] },
|
|
149
|
+
{ label: "Cache-read format", path: "powerline.segmentOptions.cache_read.format", kind: "select", choices: ["tokens", "percent", "both"] },
|
|
150
|
+
{ label: "Cost display", path: "powerline.segmentOptions.cost.subscriptionDisplay", kind: "select", choices: ["subscription", "reported-cost", "both"] },
|
|
151
|
+
{ label: "Currency", path: "powerline.segmentOptions.cost.currency", kind: "text" },
|
|
152
|
+
{ label: "Ports include UDP", path: "powerline.segmentOptions.openPorts.includeUdp", kind: "toggle" },
|
|
153
|
+
{ label: "TPS window (ms)", path: "powerline.segmentOptions.tps.windowMs", kind: "number", hint: "default 1000" },
|
|
154
|
+
{ label: "TPS mode", path: "powerline.segmentOptions.tps.mode", kind: "select", choices: ["both", "out", "in"] },
|
|
155
|
+
{ label: "TPS label", path: "powerline.segmentLabels.tps", kind: "text", hint: "empty = no label" },
|
|
157
156
|
],
|
|
158
157
|
},
|
|
159
158
|
{
|
|
160
|
-
title: "
|
|
159
|
+
title: "Welcome & vibes",
|
|
161
160
|
items: [
|
|
162
|
-
{ label: "Welcome overlay", path: "powerline.welcome", kind: "toggle", hint: "
|
|
163
|
-
{ label: "
|
|
161
|
+
{ label: "Welcome overlay", path: "powerline.welcome", kind: "toggle", hint: "on = overlay at startup, off = no welcome" },
|
|
162
|
+
{ label: "Animate wishcraft lantern", path: "wishcraft.welcome.animateLantern", kind: "toggle", hint: "flicker on the lantern" },
|
|
164
163
|
],
|
|
165
164
|
},
|
|
166
165
|
{
|
|
167
166
|
title: "Skills",
|
|
168
167
|
items: [
|
|
169
|
-
{ label: "Inline
|
|
168
|
+
{ label: "Inline expand /command and $skill", path: "wishcraft.inlineSkills", kind: "toggle", hint: "needs a restart to take effect" },
|
|
170
169
|
{ label: "Read hints", path: "wishcraft.readHints", kind: "toggle", hint: "off = no continuation hint after partial reads" },
|
|
171
170
|
],
|
|
172
171
|
},
|
|
173
172
|
{
|
|
174
|
-
title: "Hooks & repairs (harness
|
|
173
|
+
title: "Hooks & repairs (harness)",
|
|
175
174
|
items: [
|
|
176
|
-
{ label: "Hooks
|
|
175
|
+
{ label: "Hooks enabled", path: "wishcraft.hooksEnabled", kind: "toggle", hint: "preToolUse / postToolUse / sessionStart command hooks" },
|
|
177
176
|
{ label: "Tool-input repairs", path: "wishcraft.repairsEnabled", kind: "toggle", hint: "null-for-optional, auto-link, json-array, path aliases" },
|
|
178
|
-
{ label: "
|
|
177
|
+
{ label: "Daily token budget", path: "wishcraft.tokenBudget.daily", kind: "number", hint: "colours the cost segment; never blocks. 0 = off" },
|
|
179
178
|
],
|
|
180
179
|
},
|
|
181
180
|
{
|
|
182
|
-
title: "
|
|
181
|
+
title: "Shortcuts",
|
|
183
182
|
items: [
|
|
184
|
-
{ label: "Menu", path: "powerlineShortcuts.menu", kind: "text", hint: "
|
|
183
|
+
{ label: "Menu", path: "powerlineShortcuts.menu", kind: "text", hint: "e.g. alt+p" },
|
|
185
184
|
{ label: "Info", path: "powerlineShortcuts.info", kind: "text" },
|
|
186
185
|
{ label: "Stash", path: "powerlineShortcuts.stashHistory", kind: "text" },
|
|
187
|
-
{ label: "
|
|
186
|
+
{ label: "Idea", path: "powerlineShortcuts.ideaCapture", kind: "text" },
|
|
188
187
|
{ label: "Queue", path: "powerlineShortcuts.queueOpen", kind: "text" },
|
|
189
188
|
],
|
|
190
189
|
},
|
|
@@ -202,8 +201,8 @@ function isPrintable(data: string): boolean {
|
|
|
202
201
|
}
|
|
203
202
|
|
|
204
203
|
function displayValue(item: ConfigItem, value: ConfigValue): string {
|
|
205
|
-
if (value === null || value === undefined || value === "") return item.kind === "toggle" ? "
|
|
206
|
-
if (item.kind === "toggle") return value ? "
|
|
204
|
+
if (value === null || value === undefined || value === "") return item.kind === "toggle" ? "off" : "—";
|
|
205
|
+
if (item.kind === "toggle") return value ? "on" : "off";
|
|
207
206
|
return String(value);
|
|
208
207
|
}
|
|
209
208
|
|
|
@@ -220,7 +219,7 @@ export async function showWishcraftConfig(rt: RuntimeState, ctx: any): Promise<v
|
|
|
220
219
|
let settings = readSettings(cwd);
|
|
221
220
|
let groups = buildConfigGroups(settings);
|
|
222
221
|
|
|
223
|
-
//
|
|
222
|
+
// flat row list: group titles + items
|
|
224
223
|
type Row =
|
|
225
224
|
| { type: "group"; title: string }
|
|
226
225
|
| { type: "item"; group: number; item: ConfigItem };
|
|
@@ -239,7 +238,7 @@ export async function showWishcraftConfig(rt: RuntimeState, ctx: any): Promise<v
|
|
|
239
238
|
const wrapRow = (t: string, w: number) =>
|
|
240
239
|
`${border("│")}${truncateToWidth(t, w, "…", true)}${border("│")}`;
|
|
241
240
|
|
|
242
|
-
let selected = 1; //
|
|
241
|
+
let selected = 1; // first item (row 0 is a group title)
|
|
243
242
|
let editing = false;
|
|
244
243
|
let editBuffer = "";
|
|
245
244
|
|
|
@@ -254,12 +253,12 @@ export async function showWishcraftConfig(rt: RuntimeState, ctx: any): Promise<v
|
|
|
254
253
|
if (!cur) return;
|
|
255
254
|
const { item } = cur;
|
|
256
255
|
let value: ConfigValue;
|
|
257
|
-
if (item.kind === "toggle") value = next === "
|
|
256
|
+
if (item.kind === "toggle") value = next === "on";
|
|
258
257
|
else value = coerce(item, readConfigPath(settings, item.path), next);
|
|
259
258
|
const ok = writeConfigPath(cwd, item.path, value);
|
|
260
259
|
settings = readSettings(cwd);
|
|
261
260
|
groups = buildConfigGroups(settings);
|
|
262
|
-
// live
|
|
261
|
+
// live-reload powerline config + status bar
|
|
263
262
|
if (item.path.startsWith("powerline")) {
|
|
264
263
|
setConfig({
|
|
265
264
|
...stateConfig,
|
|
@@ -268,7 +267,7 @@ export async function showWishcraftConfig(rt: RuntimeState, ctx: any): Promise<v
|
|
|
268
267
|
rt.tuiRef?.requestRender?.();
|
|
269
268
|
}
|
|
270
269
|
ctx.ui.notify(
|
|
271
|
-
ok ? `${item.label}: ${displayValue(item, value)} (
|
|
270
|
+
ok ? `${item.label}: ${displayValue(item, value)} (saved)` : `${item.label} not saved (settings.json?)`,
|
|
272
271
|
ok ? "info" : "warning",
|
|
273
272
|
);
|
|
274
273
|
};
|
|
@@ -282,7 +281,7 @@ export async function showWishcraftConfig(rt: RuntimeState, ctx: any): Promise<v
|
|
|
282
281
|
settings = readSettings(cwd);
|
|
283
282
|
groups = buildConfigGroups(settings);
|
|
284
283
|
ctx.ui.notify(
|
|
285
|
-
ok ? `${item.label}: ${next} (
|
|
284
|
+
ok ? `${item.label}: ${next} (saved)` : `${item.label} not saved`,
|
|
286
285
|
ok ? "info" : "warning",
|
|
287
286
|
);
|
|
288
287
|
};
|
|
@@ -293,7 +292,7 @@ export async function showWishcraftConfig(rt: RuntimeState, ctx: any): Promise<v
|
|
|
293
292
|
settings = readSettings(cwd);
|
|
294
293
|
groups = buildConfigGroups(settings);
|
|
295
294
|
ctx.ui.notify(
|
|
296
|
-
ok ? `${item.label}: ${!(cur === true) ? "
|
|
295
|
+
ok ? `${item.label}: ${!(cur === true) ? "on" : "off"} (saved)` : `${item.label} not saved`,
|
|
297
296
|
ok ? "info" : "warning",
|
|
298
297
|
);
|
|
299
298
|
};
|
|
@@ -304,12 +303,12 @@ export async function showWishcraftConfig(rt: RuntimeState, ctx: any): Promise<v
|
|
|
304
303
|
const lines: string[] = [];
|
|
305
304
|
lines.push(border(`╭${"─".repeat(innerWidth)}╮`));
|
|
306
305
|
lines.push(
|
|
307
|
-
wrapRow(theme.fg("accent", theme.bold("Wishcraft ·
|
|
306
|
+
wrapRow(theme.fg("accent", theme.bold("Wishcraft · configuration")), innerWidth),
|
|
308
307
|
);
|
|
309
308
|
lines.push(border(`├${"─".repeat(innerWidth)}┤`));
|
|
310
309
|
|
|
311
310
|
const rows = buildRows();
|
|
312
|
-
// scroll
|
|
311
|
+
// scroll window around the selection
|
|
313
312
|
let start = Math.max(0, selected - Math.floor(LIST_ROWS / 2));
|
|
314
313
|
let end = Math.min(start + LIST_ROWS, rows.length);
|
|
315
314
|
if (end - start < Math.min(LIST_ROWS, rows.length)) start = Math.max(0, end - LIST_ROWS);
|
|
@@ -341,8 +340,8 @@ export async function showWishcraftConfig(rt: RuntimeState, ctx: any): Promise<v
|
|
|
341
340
|
theme.fg(
|
|
342
341
|
"dim",
|
|
343
342
|
editing
|
|
344
|
-
? "
|
|
345
|
-
: "↑↓ · enter=
|
|
343
|
+
? "type=value · enter=save · esc=cancel"
|
|
344
|
+
: "↑↓ · enter=select/edit (←→ cycles) · esc=close",
|
|
346
345
|
),
|
|
347
346
|
innerWidth,
|
|
348
347
|
),
|
|
@@ -388,7 +387,7 @@ export async function showWishcraftConfig(rt: RuntimeState, ctx: any): Promise<v
|
|
|
388
387
|
} while (rows[selected]!.type === "group");
|
|
389
388
|
} else if (matchesKey(data, "pageUp")) {
|
|
390
389
|
selected = Math.max(1, selected - LIST_ROWS);
|
|
391
|
-
//
|
|
390
|
+
// do not land on a group header: skip to the next item
|
|
392
391
|
while (selected < rows.length - 1 && rows[selected]!.type === "group")
|
|
393
392
|
selected++;
|
|
394
393
|
} else if (matchesKey(data, "pageDown")) {
|
|
@@ -424,10 +423,10 @@ export async function showWishcraftConfig(rt: RuntimeState, ctx: any): Promise<v
|
|
|
424
423
|
);
|
|
425
424
|
}
|
|
426
425
|
|
|
427
|
-
/**
|
|
426
|
+
/** Register /wishcraft. */
|
|
428
427
|
export function registerWishcraftConfigCommand(pi: ExtensionAPI, rt: RuntimeState): void {
|
|
429
428
|
pi.registerCommand("wishcraft", {
|
|
430
|
-
description: "
|
|
429
|
+
description: "Configure all wishcraft settings in one overlay (status bar, welcome, hooks, shortcuts)",
|
|
431
430
|
handler: async (_args: string, ctx: any) => {
|
|
432
431
|
if (!rt.enabled || !ctx.hasUI) {
|
|
433
432
|
ctx.ui.notify("Powerline UI is disabled", "info");
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* inline-invocation.ts
|
|
3
3
|
* ------------------------------------------------------------------------
|
|
4
|
-
* Inline trigger
|
|
4
|
+
* Inline trigger expansion like Cursor (`/`) and Codex (`$`).
|
|
5
5
|
*
|
|
6
|
-
* /command ->
|
|
7
|
-
* $skill ->
|
|
6
|
+
* /command -> replaced with the command.md skill body
|
|
7
|
+
* $skill -> replaced with the skill.md skill body
|
|
8
8
|
*
|
|
9
|
-
* Discovery
|
|
10
|
-
*
|
|
9
|
+
* Discovery and cache live in skill-registry.ts (pi core loadSkills + TTL)
|
|
10
|
+
* so new skills work without a restart and usage is tracked.
|
|
11
11
|
* ------------------------------------------------------------------------
|
|
12
12
|
*/
|
|
13
13
|
|
|
@@ -64,7 +64,7 @@ function isExcluded(index: number, ranges: Array<[number, number]>): boolean {
|
|
|
64
64
|
export function expandInlineTriggers(text: string): string {
|
|
65
65
|
const availableSkills = getAvailableSkills();
|
|
66
66
|
|
|
67
|
-
// Pattern
|
|
67
|
+
// Pattern for both /command and $skill
|
|
68
68
|
const TRIGGER_REGEX = /(^|[\s(])(\/|\$)([a-zA-Z0-9_-]+)/g;
|
|
69
69
|
const excluded = findExcludedRanges(text);
|
|
70
70
|
|
|
@@ -96,7 +96,7 @@ export function expandInlineTriggers(text: string): string {
|
|
|
96
96
|
// Geen matches -> originele tekst teruggeven
|
|
97
97
|
if (matches.length === 0) return text;
|
|
98
98
|
|
|
99
|
-
//
|
|
99
|
+
// Sort by position and drop overlaps
|
|
100
100
|
matches.sort((a, b) => a.start - b.start);
|
|
101
101
|
const deduped: typeof matches = [];
|
|
102
102
|
let lastEnd = -1;
|
|
@@ -107,7 +107,7 @@ export function expandInlineTriggers(text: string): string {
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
//
|
|
110
|
+
// Rebuild the text with expansions
|
|
111
111
|
let result = "";
|
|
112
112
|
let cursor = 0;
|
|
113
113
|
|
|
@@ -160,7 +160,7 @@ export function setupInlineInvocation(
|
|
|
160
160
|
return { action: "continue" };
|
|
161
161
|
});
|
|
162
162
|
|
|
163
|
-
//
|
|
163
|
+
// Refresh the cache on restart/reload so new skills show up immediately
|
|
164
164
|
pi.on("session_start", () => {
|
|
165
165
|
invalidateSkillCache();
|
|
166
166
|
});
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* skill-manager.ts
|
|
3
3
|
* ---------------------------------------------------------------------------
|
|
4
|
-
* Skills Manager v2 —
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
8
|
-
* - detail
|
|
9
|
-
* - e=
|
|
10
|
-
* n=
|
|
4
|
+
* Skills Manager v2 — list ⇄ detail overlay:
|
|
5
|
+
* - search that actually filters (type=filter, ctrl+u clears)
|
|
6
|
+
* - category headers (global/project/prompts/extra) + tab category filter
|
|
7
|
+
* - sort by name or usage (s), usage counts from the ledger
|
|
8
|
+
* - detail panel: metadata, frontmatter, body with scroll
|
|
9
|
+
* - e=edit (edit command in the editor via pi's ! flow),
|
|
10
|
+
* n=new skill, d=delete with confirm
|
|
11
11
|
* ---------------------------------------------------------------------------
|
|
12
12
|
*/
|
|
13
13
|
|
|
@@ -30,7 +30,7 @@ import { runSkillDoctor } from "./skill-doctor.ts";
|
|
|
30
30
|
import { runSkillsNew } from "./skill-templates.ts";
|
|
31
31
|
|
|
32
32
|
const CATEGORY_LABELS: Record<SkillCategory | "all", string> = {
|
|
33
|
-
all: "
|
|
33
|
+
all: "all",
|
|
34
34
|
global: "global",
|
|
35
35
|
project: "project",
|
|
36
36
|
prompts: "prompts",
|
|
@@ -59,18 +59,18 @@ function formatBytes(n: number): string {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
function formatLastUsed(ms: number): string {
|
|
62
|
-
if (!ms) return "
|
|
62
|
+
if (!ms) return "never";
|
|
63
63
|
const diff = Date.now() - ms;
|
|
64
64
|
const min = Math.floor(diff / 60_000);
|
|
65
|
-
if (min < 1) return "
|
|
66
|
-
if (min < 60) return `${min}m
|
|
65
|
+
if (min < 1) return "just now";
|
|
66
|
+
if (min < 60) return `${min}m ago`;
|
|
67
67
|
const h = Math.floor(min / 60);
|
|
68
|
-
if (h < 24) return `${h}
|
|
68
|
+
if (h < 24) return `${h}h ago`;
|
|
69
69
|
const d = Math.floor(h / 24);
|
|
70
|
-
return `${d}d
|
|
70
|
+
return `${d}d ago`;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
/**
|
|
73
|
+
/** Append text as a new editor line; the user presses enter to run it. */
|
|
74
74
|
function appendToEditor(ctx: any, text: string, notify: string): void {
|
|
75
75
|
const current = ctx.ui.getEditorText?.() ?? "";
|
|
76
76
|
const separator = current && !current.endsWith("\n") ? "\n" : "";
|
|
@@ -78,8 +78,8 @@ function appendToEditor(ctx: any, text: string, notify: string): void {
|
|
|
78
78
|
ctx.ui.notify(notify, "info");
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
/** POSIX single-quote
|
|
82
|
-
* (
|
|
81
|
+
/** POSIX single-quote a path so a skill name with shell metacharacters
|
|
82
|
+
* (e.g. `a; curl … | sh` from an untrusted cloned repo) cannot inject. */
|
|
83
83
|
function shellQuote(value: string): string {
|
|
84
84
|
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
85
85
|
}
|
|
@@ -144,10 +144,10 @@ export async function showSkillManager(ctx: any): Promise<"new" | null> {
|
|
|
144
144
|
} else {
|
|
145
145
|
rmSync(entry.filePath, { force: true });
|
|
146
146
|
}
|
|
147
|
-
ctx.ui.notify(`Skill
|
|
147
|
+
ctx.ui.notify(`Skill deleted: ${entry.name}`, "info");
|
|
148
148
|
} catch (error) {
|
|
149
149
|
ctx.ui.notify(
|
|
150
|
-
`
|
|
150
|
+
`Delete failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
151
151
|
"error",
|
|
152
152
|
);
|
|
153
153
|
}
|
|
@@ -160,7 +160,7 @@ export async function showSkillManager(ctx: any): Promise<"new" | null> {
|
|
|
160
160
|
|
|
161
161
|
const insertBody = (entry: SkillEntry) => {
|
|
162
162
|
insertSkillBody(ctx, entry.name, readSkillBody(entry.filePath));
|
|
163
|
-
ctx.ui.notify("Skill
|
|
163
|
+
ctx.ui.notify("Skill inserted into your prompt", "info");
|
|
164
164
|
};
|
|
165
165
|
|
|
166
166
|
return {
|
|
@@ -171,8 +171,8 @@ export async function showSkillManager(ctx: any): Promise<"new" | null> {
|
|
|
171
171
|
|
|
172
172
|
if (mode === "list") {
|
|
173
173
|
const f = filtered();
|
|
174
|
-
//
|
|
175
|
-
const head = `Skills · ${f.length}/${entries.length} · ${sort === "name" ? "
|
|
174
|
+
// Header: total + sort + live filter
|
|
175
|
+
const head = `Skills · ${f.length}/${entries.length} · ${sort === "name" ? "name" : "usage"} · ${CATEGORY_LABELS[category]}${query ? ` · search "${query}"` : ""}`;
|
|
176
176
|
lines.push(wrapRow(theme.fg("accent", theme.bold(head)), innerWidth));
|
|
177
177
|
lines.push(border(`├${"─".repeat(innerWidth)}┤`));
|
|
178
178
|
|
|
@@ -180,12 +180,12 @@ export async function showSkillManager(ctx: any): Promise<"new" | null> {
|
|
|
180
180
|
const emptyMsg =
|
|
181
181
|
entries.length === 0 && !query
|
|
182
182
|
? "No skills installed — ctrl+n to create one"
|
|
183
|
-
: `
|
|
183
|
+
: `No skills for "${query}"`;
|
|
184
184
|
lines.push(
|
|
185
185
|
wrapRow(theme.fg("warning", emptyMsg), innerWidth),
|
|
186
186
|
);
|
|
187
187
|
} else {
|
|
188
|
-
// scroll
|
|
188
|
+
// scroll window around the selection
|
|
189
189
|
const start = Math.max(
|
|
190
190
|
0,
|
|
191
191
|
Math.min(selected - Math.floor(LIST_ROWS / 2), f.length - LIST_ROWS),
|
|
@@ -238,7 +238,7 @@ export async function showSkillManager(ctx: any): Promise<"new" | null> {
|
|
|
238
238
|
wrapRow(
|
|
239
239
|
theme.fg(
|
|
240
240
|
"warning",
|
|
241
|
-
`ctrl+d
|
|
241
|
+
`ctrl+d or enter = delete "${selectedEntry()?.name ?? ""}", any other key cancels`,
|
|
242
242
|
),
|
|
243
243
|
innerWidth,
|
|
244
244
|
),
|
|
@@ -248,7 +248,7 @@ export async function showSkillManager(ctx: any): Promise<"new" | null> {
|
|
|
248
248
|
wrapRow(
|
|
249
249
|
theme.fg(
|
|
250
250
|
"dim",
|
|
251
|
-
"
|
|
251
|
+
"type=filter · ↑↓ · →/enter=detail · tab=category · ctrl+s=sort",
|
|
252
252
|
),
|
|
253
253
|
innerWidth,
|
|
254
254
|
),
|
|
@@ -257,7 +257,7 @@ export async function showSkillManager(ctx: any): Promise<"new" | null> {
|
|
|
257
257
|
wrapRow(
|
|
258
258
|
theme.fg(
|
|
259
259
|
"dim",
|
|
260
|
-
"ctrl+e=
|
|
260
|
+
"ctrl+e=edit · ctrl+n=new · ctrl+d=delete · esc=close",
|
|
261
261
|
),
|
|
262
262
|
innerWidth,
|
|
263
263
|
),
|
|
@@ -283,15 +283,15 @@ export async function showSkillManager(ctx: any): Promise<"new" | null> {
|
|
|
283
283
|
);
|
|
284
284
|
lines.push(
|
|
285
285
|
meta(
|
|
286
|
-
"
|
|
287
|
-
`${u?.count ?? 0}× ·
|
|
286
|
+
"used",
|
|
287
|
+
`${u?.count ?? 0}× · last ${formatLastUsed(u?.lastUsed ?? 0)}`,
|
|
288
288
|
),
|
|
289
289
|
);
|
|
290
|
-
lines.push(meta("
|
|
290
|
+
lines.push(meta("file", e.filePath));
|
|
291
291
|
lines.push(
|
|
292
292
|
meta(
|
|
293
|
-
"
|
|
294
|
-
`${formatBytes(e.sizeBytes)} · ${e.lineCount}
|
|
293
|
+
"size",
|
|
294
|
+
`${formatBytes(e.sizeBytes)} · ${e.lineCount} lines · changed ${formatLastUsed(e.mtimeMs)}`,
|
|
295
295
|
),
|
|
296
296
|
);
|
|
297
297
|
lines.push(
|
|
@@ -299,13 +299,13 @@ export async function showSkillManager(ctx: any): Promise<"new" | null> {
|
|
|
299
299
|
"frontmatter",
|
|
300
300
|
e.frontmatterKeys.length
|
|
301
301
|
? e.frontmatterKeys.join(", ")
|
|
302
|
-
: "(
|
|
302
|
+
: "(none)",
|
|
303
303
|
),
|
|
304
304
|
);
|
|
305
305
|
if (e.disableModelInvocation) {
|
|
306
306
|
lines.push(
|
|
307
307
|
wrapRow(
|
|
308
|
-
theme.fg("warning", "model
|
|
308
|
+
theme.fg("warning", "model invocation off — insert only"),
|
|
309
309
|
innerWidth,
|
|
310
310
|
),
|
|
311
311
|
);
|
|
@@ -316,7 +316,7 @@ export async function showSkillManager(ctx: any): Promise<"new" | null> {
|
|
|
316
316
|
lines.push(border(`├${"─".repeat(innerWidth)}┤`));
|
|
317
317
|
const shown = bodyLines.slice(scroll, scroll + DETAIL_ROWS);
|
|
318
318
|
if (shown.length === 0 || shown.join("").trim() === "") {
|
|
319
|
-
lines.push(wrapRow(theme.fg("dim", "(
|
|
319
|
+
lines.push(wrapRow(theme.fg("dim", "(empty body)"), innerWidth));
|
|
320
320
|
}
|
|
321
321
|
for (const line of shown) {
|
|
322
322
|
lines.push(wrapRow(theme.fg("text", line || " "), innerWidth));
|
|
@@ -325,7 +325,7 @@ export async function showSkillManager(ctx: any): Promise<"new" | null> {
|
|
|
325
325
|
if (confirmDelete) {
|
|
326
326
|
lines.push(
|
|
327
327
|
wrapRow(
|
|
328
|
-
theme.fg("warning", `ctrl+d
|
|
328
|
+
theme.fg("warning", `ctrl+d or enter = delete "${e.name}", any other key cancels`),
|
|
329
329
|
innerWidth,
|
|
330
330
|
),
|
|
331
331
|
);
|
|
@@ -334,7 +334,7 @@ export async function showSkillManager(ctx: any): Promise<"new" | null> {
|
|
|
334
334
|
wrapRow(
|
|
335
335
|
theme.fg(
|
|
336
336
|
"dim",
|
|
337
|
-
"↑↓ scroll · enter=
|
|
337
|
+
"↑↓ scroll · enter=insert · ctrl+e=edit · ctrl+d=delete · ←/esc=back",
|
|
338
338
|
),
|
|
339
339
|
innerWidth,
|
|
340
340
|
),
|
|
@@ -356,7 +356,7 @@ export async function showSkillManager(ctx: any): Promise<"new" | null> {
|
|
|
356
356
|
if (mode === "list") {
|
|
357
357
|
const f = filtered();
|
|
358
358
|
if (confirmDelete) {
|
|
359
|
-
//
|
|
359
|
+
// Confirm delete: ctrl+d or enter; any other key cancels.
|
|
360
360
|
if (data === "\x04" || matchesKey(data, "enter")) {
|
|
361
361
|
const entry = selectedEntry();
|
|
362
362
|
if (entry) doDelete(entry);
|
|
@@ -375,17 +375,17 @@ export async function showSkillManager(ctx: any): Promise<"new" | null> {
|
|
|
375
375
|
category = CATEGORY_ORDER[(idx + 1) % CATEGORY_ORDER.length]!;
|
|
376
376
|
selected = 0;
|
|
377
377
|
} else if (data === "\x13") {
|
|
378
|
-
// ctrl+s =
|
|
378
|
+
// ctrl+s = sort
|
|
379
379
|
sort = sort === "name" ? "usage" : "name";
|
|
380
380
|
selected = 0;
|
|
381
381
|
} else if (data === "\x05") {
|
|
382
|
-
// ctrl+e =
|
|
382
|
+
// ctrl+e = edit
|
|
383
383
|
const entry = selectedEntry();
|
|
384
384
|
if (entry) {
|
|
385
385
|
appendToEditor(
|
|
386
386
|
ctx,
|
|
387
387
|
editorCommand(entry.filePath),
|
|
388
|
-
"Edit
|
|
388
|
+
"Edit command in the editor — press enter to run it",
|
|
389
389
|
);
|
|
390
390
|
close();
|
|
391
391
|
return;
|
|
@@ -395,7 +395,7 @@ export async function showSkillManager(ctx: any): Promise<"new" | null> {
|
|
|
395
395
|
done("new");
|
|
396
396
|
return;
|
|
397
397
|
} else if (data === "\x04") {
|
|
398
|
-
// ctrl+d =
|
|
398
|
+
// ctrl+d = delete (with confirm)
|
|
399
399
|
if (selectedEntry()) confirmDelete = true;
|
|
400
400
|
} else if (matchesKey(data, "enter") || matchesKey(data, "right")) {
|
|
401
401
|
const entry = selectedEntry();
|
|
@@ -405,7 +405,7 @@ export async function showSkillManager(ctx: any): Promise<"new" | null> {
|
|
|
405
405
|
matchesKey(data, "backspace") ||
|
|
406
406
|
data === "\x15"
|
|
407
407
|
) {
|
|
408
|
-
//
|
|
408
|
+
// typing filters; ctrl+u clears the filter
|
|
409
409
|
if (data === "\x15") query = "";
|
|
410
410
|
else if (matchesKey(data, "backspace")) query = query.slice(0, -1);
|
|
411
411
|
else query += data;
|
|
@@ -429,16 +429,16 @@ export async function showSkillManager(ctx: any): Promise<"new" | null> {
|
|
|
429
429
|
scroll + 1,
|
|
430
430
|
);
|
|
431
431
|
} else if (data === "\x05") {
|
|
432
|
-
// ctrl+e =
|
|
432
|
+
// ctrl+e = edit
|
|
433
433
|
appendToEditor(
|
|
434
434
|
ctx,
|
|
435
435
|
editorCommand(detail!.filePath),
|
|
436
|
-
"Edit
|
|
436
|
+
"Edit command in the editor — press enter to run it",
|
|
437
437
|
);
|
|
438
438
|
close();
|
|
439
439
|
return;
|
|
440
440
|
} else if (data === "\x04") {
|
|
441
|
-
// ctrl+d =
|
|
441
|
+
// ctrl+d = delete
|
|
442
442
|
confirmDelete = true;
|
|
443
443
|
} else if (matchesKey(data, "enter") || matchesKey(data, "tab")) {
|
|
444
444
|
insertBody(detail!);
|
|
@@ -460,7 +460,7 @@ export async function showSkillManager(ctx: any): Promise<"new" | null> {
|
|
|
460
460
|
);
|
|
461
461
|
}
|
|
462
462
|
|
|
463
|
-
/**
|
|
463
|
+
/** Register the `/skills` command. */
|
|
464
464
|
export type SkillManagerCommandDeps = {
|
|
465
465
|
runDoctor?: (ctx: any) => Promise<void>;
|
|
466
466
|
};
|
|
@@ -493,5 +493,5 @@ export function registerSkillManagerCommand(
|
|
|
493
493
|
});
|
|
494
494
|
}
|
|
495
495
|
|
|
496
|
-
// Re-export
|
|
496
|
+
// Re-export for existing importers/tests.
|
|
497
497
|
export { listSkills, readSkillBody } from "./skill-registry.ts";
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* skill-registry.ts
|
|
3
3
|
* ---------------------------------------------------------------------------
|
|
4
|
-
* Data
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Data layer for skills manager v2: discovery via pi core `loadSkills()`
|
|
5
|
+
* (recursive SKILL.md discovery, loose .md children, SourceInfo,
|
|
6
|
+
* diagnostics) plus rich per-skill metadata and a usage ledger. Cache with
|
|
7
|
+
* TTL + explicit invalidation (session_start, manager-open) so new skills
|
|
8
|
+
* show up without a restart.
|
|
9
9
|
* ---------------------------------------------------------------------------
|
|
10
10
|
*/
|
|
11
11
|
|
|
@@ -15,7 +15,7 @@ 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
17
|
|
|
18
|
-
/**
|
|
18
|
+
/** Category: where the skill came from. */
|
|
19
19
|
export type SkillCategory = "global" | "project" | "prompts" | "extra";
|
|
20
20
|
|
|
21
21
|
export interface SkillUsage {
|
|
@@ -28,22 +28,22 @@ export interface SkillEntry {
|
|
|
28
28
|
description: string;
|
|
29
29
|
filePath: string;
|
|
30
30
|
baseDir: string;
|
|
31
|
-
/**
|
|
31
|
+
/** True only for core skills that own a skill directory. */
|
|
32
32
|
isDirectorySkill?: boolean;
|
|
33
33
|
category: SkillCategory;
|
|
34
34
|
disableModelInvocation: boolean;
|
|
35
35
|
sizeBytes: number;
|
|
36
36
|
lineCount: number;
|
|
37
37
|
mtimeMs: number;
|
|
38
|
-
/**
|
|
38
|
+
/** All frontmatter keys (raw, including unknown). */
|
|
39
39
|
frontmatterKeys: string[];
|
|
40
|
-
/**
|
|
40
|
+
/** Diagnostic message (core diagnostics, or empty description). */
|
|
41
41
|
warning?: string;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
/**
|
|
45
|
-
* frontmatter
|
|
46
|
-
*
|
|
44
|
+
/** Loose .md/.txt in extra dirs (prompts/legacy) that core rejects (no
|
|
45
|
+
* frontmatter description). Core dirs keep core's strict rules; only extra
|
|
46
|
+
* dirs get this lax fallback so old prompts keep working. */
|
|
47
47
|
function scanLooseExtraFiles(
|
|
48
48
|
extras: { path: string; category: SkillCategory }[],
|
|
49
49
|
knownPaths: Set<string>,
|
|
@@ -82,7 +82,7 @@ function scanLooseExtraFiles(
|
|
|
82
82
|
lineCount: content.split("\n").length,
|
|
83
83
|
mtimeMs,
|
|
84
84
|
frontmatterKeys: parseFrontmatterKeys(content),
|
|
85
|
-
warning: "
|
|
85
|
+
warning: "no description — the model will not see this skill in the prompt",
|
|
86
86
|
});
|
|
87
87
|
knownPaths.add(filePath);
|
|
88
88
|
}
|
|
@@ -105,7 +105,7 @@ export function setSkillCacheInvalidationHandler(handler: (() => void) | null):
|
|
|
105
105
|
const usageCache = new Map<string, SkillUsage>();
|
|
106
106
|
let usageLoaded = false;
|
|
107
107
|
|
|
108
|
-
/**
|
|
108
|
+
/** Drop the discovery cache (the next read scans again). */
|
|
109
109
|
export function invalidateSkillCache(): void {
|
|
110
110
|
cachedAt = 0;
|
|
111
111
|
cachedEntries = null;
|
|
@@ -114,7 +114,7 @@ export function invalidateSkillCache(): void {
|
|
|
114
114
|
onCacheInvalidated?.();
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
/** Legacy prompts/loose-md dirs
|
|
117
|
+
/** Legacy prompts/loose-md dirs that inline-invocation already scanned. */
|
|
118
118
|
function extraSkillPaths(cwd: string): { path: string; category: SkillCategory }[] {
|
|
119
119
|
const agent = getAgentDir();
|
|
120
120
|
return [
|
|
@@ -144,7 +144,7 @@ function categorize(
|
|
|
144
144
|
return "extra";
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
/** Parse
|
|
147
|
+
/** Parse all top-level frontmatter keys (name only; value is unused). */
|
|
148
148
|
function parseFrontmatterKeys(content: string): string[] {
|
|
149
149
|
const lines = content.split("\n");
|
|
150
150
|
if (lines[0]?.trim() !== "---") return [];
|
|
@@ -275,10 +275,10 @@ export function loadSkillCatalog(cwd: string = process.cwd()): SkillEntry[] {
|
|
|
275
275
|
lineCount = content.split("\n").length;
|
|
276
276
|
mtimeMs = statSync(s.filePath).mtimeMs;
|
|
277
277
|
} catch {
|
|
278
|
-
warning = "
|
|
278
|
+
warning = "file not readable";
|
|
279
279
|
}
|
|
280
280
|
if (!warning && !s.description.trim()) {
|
|
281
|
-
warning = "
|
|
281
|
+
warning = "no description — the model will not see this skill in the prompt";
|
|
282
282
|
}
|
|
283
283
|
if (!warning && s.filePath && diagByPath.has(s.filePath)) {
|
|
284
284
|
warning = diagByPath.get(s.filePath);
|
|
@@ -299,7 +299,7 @@ export function loadSkillCatalog(cwd: string = process.cwd()): SkillEntry[] {
|
|
|
299
299
|
};
|
|
300
300
|
});
|
|
301
301
|
|
|
302
|
-
// loose entries
|
|
302
|
+
// loose entries last; on a name collision core wins
|
|
303
303
|
const looseNames = new Set(loose.map((e) => e.name));
|
|
304
304
|
const catalogPaths = new Set([
|
|
305
305
|
...entries.map((e) => e.filePath),
|
|
@@ -322,7 +322,7 @@ export function loadSkillCatalog(cwd: string = process.cwd()): SkillEntry[] {
|
|
|
322
322
|
return cachedEntries;
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
-
/** Compat:
|
|
325
|
+
/** Compat: name → file path (for inline-invocation). */
|
|
326
326
|
export function getAvailableSkills(): Map<string, string> {
|
|
327
327
|
loadSkillCatalog();
|
|
328
328
|
return cachedPathMap ?? new Map();
|
|
@@ -346,11 +346,11 @@ function loadUsage(): void {
|
|
|
346
346
|
usageCache.set(name, { count: u.count ?? 0, lastUsed: u.lastUsed ?? 0 });
|
|
347
347
|
}
|
|
348
348
|
} catch {
|
|
349
|
-
//
|
|
349
|
+
// missing file or broken JSON → empty ledger
|
|
350
350
|
}
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
-
/** Usage
|
|
353
|
+
/** Usage for every skill (name → {count, lastUsed}). */
|
|
354
354
|
export function getSkillUsage(): Map<string, SkillUsage> {
|
|
355
355
|
loadUsage();
|
|
356
356
|
return usageCache;
|
|
@@ -359,7 +359,7 @@ export function getSkillUsage(): Map<string, SkillUsage> {
|
|
|
359
359
|
let usageFlushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
360
360
|
let exitFlushRegistered = false;
|
|
361
361
|
|
|
362
|
-
/**
|
|
362
|
+
/** Write the ledger to disk now (best-effort, sync). */
|
|
363
363
|
export function flushSkillUsage(): void {
|
|
364
364
|
if (usageFlushTimer) {
|
|
365
365
|
clearTimeout(usageFlushTimer);
|
|
@@ -371,14 +371,14 @@ export function flushSkillUsage(): void {
|
|
|
371
371
|
mkdirSync(getAgentDir(), { recursive: true });
|
|
372
372
|
writeFileSync(usageFile(), JSON.stringify(obj, null, 2), "utf8");
|
|
373
373
|
} catch {
|
|
374
|
-
// best-effort: tracking
|
|
374
|
+
// best-effort: tracking must never break the hot path
|
|
375
375
|
}
|
|
376
376
|
}
|
|
377
377
|
|
|
378
378
|
/**
|
|
379
|
-
* Log
|
|
380
|
-
* getSkillUsage()
|
|
381
|
-
* input hot path
|
|
379
|
+
* Log a skill use. The in-memory ledger updates immediately (so
|
|
380
|
+
* getSkillUsage() is correct), but the disk write is debounced so the
|
|
381
|
+
* input hot path does not do a sync writeFileSync per keystroke.
|
|
382
382
|
*/
|
|
383
383
|
export function recordSkillUsage(name: string): void {
|
|
384
384
|
loadUsage();
|
|
@@ -436,7 +436,7 @@ export function listSkills(): { name: string; description: string; path: string;
|
|
|
436
436
|
}));
|
|
437
437
|
}
|
|
438
438
|
|
|
439
|
-
/** Pure filter/
|
|
439
|
+
/** Pure filter/sort for the manager list (testable without a TUI). */
|
|
440
440
|
export function applySkillFilter(
|
|
441
441
|
entries: SkillEntry[],
|
|
442
442
|
query: string,
|