@groeponline/pi-wishcraft 0.21.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +81 -0
- package/ROADMAP.md +13 -16
- package/docs/commands.md +2 -1
- package/docs/configuration.md +18 -0
- package/docs/index.md +2 -2
- package/package.json +1 -1
- package/src/config/types.ts +3 -0
- package/src/extension/commands/commands.ts +11 -3
- package/src/extension/core/segment-context.ts +19 -0
- package/src/extension/core/state.ts +1 -0
- package/src/extension/core/types.ts +2 -0
- package/src/extension/hooks/repairs.ts +89 -16
- package/src/extension/session/session-lifecycle.ts +41 -1
- package/src/extension/settings/wishcraft-config.ts +2 -1
- package/src/extension/ui/menu-views.ts +33 -90
- package/src/extension/ui/overlay-chrome.ts +163 -0
- package/src/extension/ui/powerline-menu-view.ts +2 -4
- package/src/extension/ui/token-overlays.ts +82 -0
- package/src/segments/system.ts +21 -40
- package/src/segments/usage.ts +10 -4
- package/src/usage/token-budget.ts +57 -0
- package/src/usage/tps-ring.ts +151 -0
- package/src/usage/usage-store.ts +351 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.22.0] - 2026-08-20
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- `/tps` overlay reads the same 1s/5s ring as the segment; `/usage` shows session / today / week from `wishcraft-usage.json`.
|
|
9
|
+
- Rest of the tool-input repairs on custom tools: JSON-string arrays, `{}` placeholders, bare-string wrap, and path aliases.
|
|
10
|
+
- README hook examples (bash-guard, write-audit, SessionStart git-status) plus `wishcraft.hooksEnabled` kill-switch docs.
|
|
11
|
+
- Overlay type-to-filter uses substring on label, value, and description.
|
|
12
|
+
- `wishcraft.tokenBudget.daily` colours the cost segment at 80%/100% and warns on welcome; it never blocks.
|
|
13
|
+
|
|
5
14
|
## [0.21.0] - 2026-08-20
|
|
6
15
|
|
|
7
16
|
### Added
|
package/README.md
CHANGED
|
@@ -248,6 +248,7 @@ Define your own preset in settings; it merges over built-ins and is selectable v
|
|
|
248
248
|
Interactivity (Pi core renders the footer as static text, so live click is not possible; actions live in commands and a navigable overlay):
|
|
249
249
|
|
|
250
250
|
- `/tps [value]`: show or set `POWERLINE_TPS`
|
|
251
|
+
- `/usage`: today / week / session token overlay (same ledger as the cost segment)
|
|
251
252
|
- `/open-ports`: list listening ports and pick one
|
|
252
253
|
- `alt+p`: **powerline menu**: three overlays (Navigate, Configure, Status). Status drills down to ports, TPS, and toggle.
|
|
253
254
|
- `alt+i`: **powerline info**: full open-ports list
|
|
@@ -474,6 +475,86 @@ Browse and insert your installed skills (`SKILL.md` files and `*.md`/`*.txt` pro
|
|
|
474
475
|
|
|
475
476
|
The manager reuses the same skill discovery as inline `/command`/`$skill` triggers, so anything you can inline you can also browse and insert manually.
|
|
476
477
|
|
|
478
|
+
## Hooks (harness)
|
|
479
|
+
|
|
480
|
+
Wishcraft can run Command Code-style hooks on Pi's native events (`tool_call`, `tool_result`, `session_start`, `turn_end`). Each hook is a command that reads JSON on stdin. Set `wishcraft.hooksEnabled` to `false` to kill-switch every hook without deleting the config. Hook *definitions* are read from the global agent settings file only (project `.pi/settings.json` cannot install new commands).
|
|
481
|
+
|
|
482
|
+
Put the scripts somewhere executable (example: `~/.pi/agent/hooks/`) and point settings at them:
|
|
483
|
+
|
|
484
|
+
```json
|
|
485
|
+
{
|
|
486
|
+
"wishcraft": {
|
|
487
|
+
"hooksEnabled": true,
|
|
488
|
+
"hooks": {
|
|
489
|
+
"preToolUse": [
|
|
490
|
+
{
|
|
491
|
+
"matcher": "bash",
|
|
492
|
+
"hooks": [{ "command": "~/.pi/agent/hooks/bash-guard.sh", "timeout": 5 }]
|
|
493
|
+
}
|
|
494
|
+
],
|
|
495
|
+
"postToolUse": [
|
|
496
|
+
{
|
|
497
|
+
"matcher": "write",
|
|
498
|
+
"hooks": [{ "command": "~/.pi/agent/hooks/write-audit.sh", "timeout": 5 }]
|
|
499
|
+
}
|
|
500
|
+
],
|
|
501
|
+
"sessionStart": [
|
|
502
|
+
{
|
|
503
|
+
"hooks": [{ "command": "~/.pi/agent/hooks/session-git-status.sh", "timeout": 10 }]
|
|
504
|
+
}
|
|
505
|
+
]
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
**1. bash-guard** — deny `rm -rf /` (and close variants) before bash runs. Exit 2 is deny; the first stderr line or `permissionDecisionReason` is what the model sees.
|
|
512
|
+
|
|
513
|
+
```bash
|
|
514
|
+
#!/usr/bin/env bash
|
|
515
|
+
# ~/.pi/agent/hooks/bash-guard.sh
|
|
516
|
+
payload=$(cat)
|
|
517
|
+
cmd=$(printf '%s' "$payload" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("tool_input",{}).get("command",""))')
|
|
518
|
+
if printf '%s' "$cmd" | grep -Eq '(^|[[:space:]])rm[[:space:]]+(-[a-zA-Z]*[[:space:]]+)*-r[a-zA-Z]*f|-fr[a-zA-Z]*|[[:space:]]/[[:space:]]*$'; then
|
|
519
|
+
printf '%s\n' '{"hookSpecificOutput":{"permissionDecision":"deny","permissionDecisionReason":"blocked destructive rm"}}'
|
|
520
|
+
echo "blocked destructive rm" >&2
|
|
521
|
+
exit 2
|
|
522
|
+
fi
|
|
523
|
+
exit 0
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
**2. write-audit** — append-only log of write tool calls (does not block).
|
|
527
|
+
|
|
528
|
+
```bash
|
|
529
|
+
#!/usr/bin/env bash
|
|
530
|
+
# ~/.pi/agent/hooks/write-audit.sh
|
|
531
|
+
mkdir -p "$HOME/.pi/agent/logs"
|
|
532
|
+
printf '%s\n' "$(date -Is) $1" >> "$HOME/.pi/agent/logs/write-audit.jsonl"
|
|
533
|
+
cat >> "$HOME/.pi/agent/logs/write-audit.jsonl"
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
The hook receives the JSON payload on stdin; the snippet above stores the raw event. Trim or jq-filter as you like.
|
|
537
|
+
|
|
538
|
+
**3. SessionStart git-status** — inject `git status --short` as extra context at session start.
|
|
539
|
+
|
|
540
|
+
```bash
|
|
541
|
+
#!/usr/bin/env bash
|
|
542
|
+
# ~/.pi/agent/hooks/session-git-status.sh
|
|
543
|
+
status=$(git status --short 2>/dev/null | head -n 40)
|
|
544
|
+
CTX="$status" python3 - <<'PY'
|
|
545
|
+
import json, os
|
|
546
|
+
print(json.dumps({
|
|
547
|
+
"hookSpecificOutput": {
|
|
548
|
+
"additionalContext": "git status:\n" + os.environ.get("CTX", "")
|
|
549
|
+
}
|
|
550
|
+
}))
|
|
551
|
+
PY
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
Tool-input repairs (custom/extension tools only) run before hooks: null-for-optional, JSON-string arrays, `{}` → `[]` on array keys, bare-string wrap, path aliases, and markdown auto-link unwrap. `/repairs` shows the counters. Core tools (`bash`, `read`, `edit`, `write`, `grep`, `find`, `ls`) are never rewritten.
|
|
555
|
+
|
|
556
|
+
`wishcraft.tokenBudget.daily` (token count) paints the cost segment warning/red at 80%/100% and notifies on welcome. It never blocks a turn. `/usage` shows session / today / week from `~/.pi/agent/wishcraft-usage.json`. `/tps` with no args opens the live ring overlay (same sampler as the segment).
|
|
557
|
+
|
|
477
558
|
## Working Vibes
|
|
478
559
|
|
|
479
560
|
Transform boring "Working..." messages into themed phrases that match your style:
|
package/ROADMAP.md
CHANGED
|
@@ -89,10 +89,10 @@ Niet de fork. Niet de SaaS-agent.
|
|
|
89
89
|
manager v2 UI gingen mee in #12, eerder dan deze sectie beloofde.
|
|
90
90
|
Done = npm 0.19.2 live, `/skills` filtert, `$test` expandeert geen
|
|
91
91
|
debris, verify-trio groen op de tag.
|
|
92
|
-
- **0.20.0 — "Harness"
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
- **0.20.0–0.22 — "Harness"** (0.20.0 + 0.21.0 op npm; leftovers in GRO-1414).
|
|
93
|
+
Overlay-chrome + CHE-42 drill-down, Configure als SelectList, token-overlays,
|
|
94
|
+
rest-repairs, README-hooks. Done = drie README-hookvoorbeelden, repair-teller,
|
|
95
|
+
`alt+p` overlay-boom, `/tps` deelt de ring met het segment.
|
|
96
96
|
- **1.0 — "Cockpit"**. Skills-doctor/install, declaratieve policy,
|
|
97
97
|
preset-editor, idee-review, stabiele ChefGroep-statuskeys,
|
|
98
98
|
documentatie die waar is. Done = README dekt alles wat we shipten,
|
|
@@ -172,13 +172,9 @@ GRO-1061 (runner-queue) is ops, geen product-slice.
|
|
|
172
172
|
|
|
173
173
|
## 0.20.0 — Harness
|
|
174
174
|
|
|
175
|
-
Vier stacked PRs. 0.19.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
`/powerline doctor` + `export`, queue-archive + retention (P1 single-clock),
|
|
179
|
-
`customItems.auto`, fleet-SSH `openPorts.host`, what's-new welcome widget,
|
|
180
|
-
cost-alert, bash editor-history split, `docs/` guides, custom-preset builder in
|
|
181
|
-
Configure. Nog open: token-overlays, rest-repairs, README-hookvoorbeelden.
|
|
175
|
+
Vier stacked PRs. 0.20.0 (#19) en 0.21.0 (#13) staan op npm. CHE-42
|
|
176
|
+
Configure-overlay, doctor/export, queue-archive en `docs/` zijn geland.
|
|
177
|
+
GRO-1414 sluit README-hooks, rest-repairs, `/tps`+`/usage`, substring-filter.
|
|
182
178
|
|
|
183
179
|
Overlay-chrome kit, één keer, daarna hergebruiken: box + ronde hoeken,
|
|
184
180
|
accent-kop, dim metadata, rechts uitgelijnde counts, `→` detail /
|
|
@@ -186,7 +182,7 @@ accent-kop, dim metadata, rechts uitgelijnde counts, `→` detail /
|
|
|
186
182
|
skills v2; tweede = `/usage`; derde = queue/idea. Pure render-
|
|
187
183
|
functies, geen `ctx.ui`-mock.
|
|
188
184
|
|
|
189
|
-
### PR E — hooks — ✅ geland in `feat/wishcraft-0.19
|
|
185
|
+
### PR E — hooks — ✅ geland in `feat/wishcraft-0.19`; README-voorbeelden in GRO-1414
|
|
190
186
|
|
|
191
187
|
Settings: `wishcraft.hooks` met events
|
|
192
188
|
`preToolUse | postToolUse | sessionStart | turnEnd`. Per hook
|
|
@@ -201,7 +197,7 @@ Done: `parseHookOutput` unit-testen. README met drie werkende
|
|
|
201
197
|
voorbeelden: bash-guard (`rm -rf /` blokkeren), write-audit
|
|
202
198
|
(append-only log), SessionStart git-status injectie.
|
|
203
199
|
|
|
204
|
-
### PR F — tool-input repairs — ✅
|
|
200
|
+
### PR F — tool-input repairs — ✅ schema-loze subset in 0.19; rest (JSON-array, `{}`, bare-wrap, path aliases) in GRO-1414. Core tools blijven met rust.
|
|
205
201
|
|
|
206
202
|
`tool_call`-handler repareert bekende malformaties vóór executie
|
|
207
203
|
(mutable input). Volgorde vast: json-parse vóór bare-wrap.
|
|
@@ -308,7 +304,8 @@ Pas na 0.20. Geen parallelle 1.0-tak.
|
|
|
308
304
|
| GRO-1061 CI queue | Ops, niet deze roadmap. |
|
|
309
305
|
| CHE-40 `/powerline` tab | Done (#18 / 0.19.2). |
|
|
310
306
|
| CHE-41 per-segment detail | 1.0. Ticket hernoemd; geen tweede `alt+i`-pad. |
|
|
311
|
-
| CHE-42 drill-down |
|
|
307
|
+
| CHE-42 drill-down | Done (#19 + Configure in #13). |
|
|
308
|
+
| GRO-1414 0.20 leftovers | In Progress. Hooks docs, rest-repairs, `/tps` `/usage`, substring filter. |
|
|
312
309
|
|
|
313
310
|
Oude `pi-powerline-footer`-projecttickets niet laten staan alsof
|
|
314
311
|
die package nog leeft.
|
|
@@ -331,8 +328,8 @@ die package nog leeft.
|
|
|
331
328
|
|
|
332
329
|
## Residual risks
|
|
333
330
|
|
|
334
|
-
- `SelectList.setFilter` matcht alleen prefix op `value`.
|
|
335
|
-
|
|
331
|
+
- `SelectList.setFilter` matcht alleen prefix op `value`. Overlay-chrome
|
|
332
|
+
filtert zelf op substring (GRO-1414). Skills-manager had dat al.
|
|
336
333
|
- `npm deprecate` van de oude naam faalt tot de scope-owner het
|
|
337
334
|
token verruimt. Gebruikers die `pi-powerline-footer` installeren
|
|
338
335
|
blijven op 0.17.2.
|
package/docs/commands.md
CHANGED
|
@@ -96,7 +96,8 @@ Preset selection is saved under `powerline` in the agent settings file and resto
|
|
|
96
96
|
|
|
97
97
|
Pi core renders the footer as static text, so live click is not possible; actions live in commands and a navigable overlay.
|
|
98
98
|
|
|
99
|
-
- `/tps
|
|
99
|
+
- `/tps`: overlay of the live 1s window (same ring as the segment). `/tps <value>` sets `POWERLINE_TPS`
|
|
100
|
+
- `/usage`: session / today / week overlay from `~/.pi/agent/wishcraft-usage.json`
|
|
100
101
|
- `/open-ports`: list listening ports and pick one
|
|
101
102
|
- `/powerline doctor`: diagnostics overlay — settings file validity, unknown presets, Nerd Font detection, git polling, bash-mode status, and queue file health
|
|
102
103
|
- `/powerline export`: export the current preset + effective layout + labels as a JSON snippet (Enter copies it to the clipboard)
|
package/docs/configuration.md
CHANGED
|
@@ -214,6 +214,24 @@ Set `powerline.costAlert` to a USD threshold to get a single warning notificatio
|
|
|
214
214
|
}
|
|
215
215
|
```
|
|
216
216
|
|
|
217
|
+
## Hooks and repairs
|
|
218
|
+
|
|
219
|
+
Command hooks live under `wishcraft.hooks` in the **global** agent settings file. `wishcraft.hooksEnabled: false` is the kill-switch. See the README Hooks section for three copy-paste examples (bash-guard, write-audit, SessionStart git-status).
|
|
220
|
+
|
|
221
|
+
Tool-input repairs apply to custom/extension tools only (`wishcraft.repairsEnabled`, default on). `/repairs` prints the counters.
|
|
222
|
+
|
|
223
|
+
## Token budget
|
|
224
|
+
|
|
225
|
+
`wishcraft.tokenBudget.daily` is a token count (input + output + cache). At 80% the cost segment turns warning-coloured; at 100% it turns red and welcome notifies. It never blocks a turn.
|
|
226
|
+
|
|
227
|
+
```json
|
|
228
|
+
{
|
|
229
|
+
"wishcraft": {
|
|
230
|
+
"tokenBudget": { "daily": 500000 }
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
217
235
|
## Custom layout
|
|
218
236
|
|
|
219
237
|
Use `powerline.layout` to override segment order and grouping while keeping the selected preset's colors and segment options. Set `powerline.separator` when you want a separator style independent of the preset:
|
package/docs/index.md
CHANGED
|
@@ -5,8 +5,8 @@ The README stays a short landing page; everything below lives here.
|
|
|
5
5
|
|
|
6
6
|
## Guides
|
|
7
7
|
|
|
8
|
-
- [Commands & interactivity](./commands.md) — `/powerline`, `/queue`, `/idea`, placement, presets, keybinds, and the navigable overlay.
|
|
9
|
-
- [Configuration](./configuration.md) — custom items,
|
|
8
|
+
- [Commands & interactivity](./commands.md) — `/powerline`, `/tps`, `/usage`, `/queue`, `/idea`, placement, presets, keybinds, and the navigable overlay.
|
|
9
|
+
- [Configuration](./configuration.md) — custom items, hooks, repairs, token budget, labels, templates, layout, cost alert, and display formats.
|
|
10
10
|
- [Bash mode](./bash-mode.md) — sticky shell, ghost suggestions, and shell config.
|
|
11
11
|
- [Stash & shortcuts](./stash-and-shortcuts.md) — editor stash, prompt history, clipboard/navigation shortcuts, and shortcut config.
|
|
12
12
|
- [Skill manager](./skill-manager.md) — browsing and inserting installed skills.
|
package/package.json
CHANGED
package/src/config/types.ts
CHANGED
|
@@ -259,6 +259,9 @@ export interface SegmentContext {
|
|
|
259
259
|
/** Per-segment custom text labels (from powerline.segmentLabels). */
|
|
260
260
|
segmentLabels: ReadonlyMap<string, string>;
|
|
261
261
|
|
|
262
|
+
/** Daily token budget progress (wishcraft.tokenBudget.daily). */
|
|
263
|
+
tokenBudget?: { dailyLimit: number | null; dailyUsed: number };
|
|
264
|
+
|
|
262
265
|
// Theming
|
|
263
266
|
theme: ThemeLike;
|
|
264
267
|
colors: ColorScheme;
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
writePowerlinePresetSetting,
|
|
18
18
|
} from "../settings/settings-io.ts";
|
|
19
19
|
import { showOpenPortsList, showSelectOverlay } from "../ui/menu-views.ts";
|
|
20
|
+
import { showTpsOverlay, showUsageOverlay } from "../ui/token-overlays.ts";
|
|
20
21
|
import { showPowerlineMainMenu } from "../ui/powerline-menu-view.ts";
|
|
21
22
|
import { openStashHistory } from "../shortcuts/shortcuts-router.ts";
|
|
22
23
|
import { ensureShellSession, setBashModeActive } from "./bash-mode-actions.ts";
|
|
@@ -232,13 +233,12 @@ export function registerCommands(pi: ExtensionAPI, rt: RuntimeState): void {
|
|
|
232
233
|
});
|
|
233
234
|
|
|
234
235
|
pi.registerCommand("tps", {
|
|
235
|
-
description: "Show or set POWERLINE_TPS
|
|
236
|
+
description: "Show the live TPS overlay, or set POWERLINE_TPS",
|
|
236
237
|
handler: async (args, ctx) => {
|
|
237
238
|
rt.currentCtx = ctx;
|
|
238
239
|
const value = args?.trim();
|
|
239
240
|
if (!value) {
|
|
240
|
-
|
|
241
|
-
ctx.ui.notify(`TPS: ${current}`, "info");
|
|
241
|
+
await showTpsOverlay(rt, ctx);
|
|
242
242
|
return;
|
|
243
243
|
}
|
|
244
244
|
process.env.POWERLINE_TPS = value;
|
|
@@ -248,6 +248,14 @@ export function registerCommands(pi: ExtensionAPI, rt: RuntimeState): void {
|
|
|
248
248
|
},
|
|
249
249
|
});
|
|
250
250
|
|
|
251
|
+
pi.registerCommand("usage", {
|
|
252
|
+
description: "Show session / today / week token usage overlay",
|
|
253
|
+
handler: async (_args, ctx) => {
|
|
254
|
+
rt.currentCtx = ctx;
|
|
255
|
+
await showUsageOverlay(rt, ctx);
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
|
|
251
259
|
pi.registerCommand("open-ports", {
|
|
252
260
|
description: "Show open ports",
|
|
253
261
|
handler: async (_args, ctx) => {
|
|
@@ -14,6 +14,14 @@ import { getDefaultColors } from "../../theme/theme.ts";
|
|
|
14
14
|
import { getGitStatus } from "../../git/status.ts";
|
|
15
15
|
import { getQueueContext } from "../queue/queue-context.ts";
|
|
16
16
|
import { getUsageTokenTotal } from "../../usage/ledger.ts";
|
|
17
|
+
import {
|
|
18
|
+
dayKey,
|
|
19
|
+
loadUsageFileFromDisk,
|
|
20
|
+
tokenTotal,
|
|
21
|
+
totalsForRange,
|
|
22
|
+
} from "../../usage/usage-store.ts";
|
|
23
|
+
import { parseTokenBudget } from "../../usage/token-budget.ts";
|
|
24
|
+
import { readSettings } from "../settings/settings-io.ts";
|
|
17
25
|
import {
|
|
18
26
|
CUSTOM_COMPACTION_STATUS_KEY,
|
|
19
27
|
EDITOR_STATUS_DEFER_MS,
|
|
@@ -220,6 +228,17 @@ export function buildSegmentContext(
|
|
|
220
228
|
effectiveCustomItems,
|
|
221
229
|
options: segmentOptions,
|
|
222
230
|
segmentLabels: new Map(Object.entries(config.segmentLabels)),
|
|
231
|
+
tokenBudget: (() => {
|
|
232
|
+
const dailyLimit = parseTokenBudget(
|
|
233
|
+
readSettings(ctx.cwd ?? process.cwd()).wishcraft,
|
|
234
|
+
).daily;
|
|
235
|
+
const now = Date.now();
|
|
236
|
+
const todayStart = Date.parse(`${dayKey(now)}T00:00:00`);
|
|
237
|
+
const dailyUsed = tokenTotal(
|
|
238
|
+
totalsForRange(loadUsageFileFromDisk(), todayStart, now + 1),
|
|
239
|
+
);
|
|
240
|
+
return { dailyLimit, dailyUsed };
|
|
241
|
+
})(),
|
|
223
242
|
theme,
|
|
224
243
|
colors,
|
|
225
244
|
};
|
|
@@ -112,6 +112,7 @@ export function createRuntimeState(
|
|
|
112
112
|
stashedPromptHistory: readPersistedStashHistory(),
|
|
113
113
|
currentEditor: null,
|
|
114
114
|
costAlertNotified: false,
|
|
115
|
+
tokenBudgetNotifiedLevel: 0,
|
|
115
116
|
bashModeActive: false,
|
|
116
117
|
bashTranscript: new BashTranscriptStore(bashModeSettings),
|
|
117
118
|
bashCompletionEngine: new BashCompletionEngine(),
|
|
@@ -64,6 +64,8 @@ export interface RuntimeState {
|
|
|
64
64
|
currentEditor: any;
|
|
65
65
|
/** True once the configured `costAlert` threshold has been notified this session. */
|
|
66
66
|
costAlertNotified: boolean;
|
|
67
|
+
/** Highest daily-budget warning already shown this session (0 / 80 / 100). */
|
|
68
|
+
tokenBudgetNotifiedLevel: 0 | 80 | 100;
|
|
67
69
|
bashModeActive: boolean;
|
|
68
70
|
bashTranscript: BashTranscriptStore;
|
|
69
71
|
bashCompletionEngine: BashCompletionEngine;
|
|
@@ -2,20 +2,74 @@
|
|
|
2
2
|
* repairs.ts
|
|
3
3
|
* ---------------------------------------------------------------------------
|
|
4
4
|
* Tool-input repairs vóór executie (pi's `tool_call` event heeft mutable
|
|
5
|
-
* input).
|
|
6
|
-
*
|
|
7
|
-
* placeholder-object) vereisen de validator-issue-lijst en blijven bewust
|
|
8
|
-
* weg tot Pi die expose't.
|
|
5
|
+
* input). Custom/extension tools only. Order is fixed: json-parse before
|
|
6
|
+
* bare-string wrap so `'["a"]'` becomes `["a"]`, not `[['["a"]']]`.
|
|
9
7
|
* ---------------------------------------------------------------------------
|
|
10
8
|
*/
|
|
11
9
|
|
|
12
10
|
/** Degenerate markdown auto-link: link-tekst == url zonder protocol. */
|
|
13
11
|
const AUTO_LINK_RE = /^\[([^\]\s]+)\]\((https?:\/\/|file:\/\/)?([^)\s]*)\)$/;
|
|
14
12
|
|
|
13
|
+
const CORE_TOOLS = new Set([
|
|
14
|
+
"bash",
|
|
15
|
+
"read",
|
|
16
|
+
"edit",
|
|
17
|
+
"write",
|
|
18
|
+
"grep",
|
|
19
|
+
"find",
|
|
20
|
+
"ls",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const PATH_ALIASES = ["filePath", "absolutePath", "target_file", "file_path"];
|
|
24
|
+
|
|
25
|
+
const ARRAY_KEYS = new Set([
|
|
26
|
+
"files",
|
|
27
|
+
"paths",
|
|
28
|
+
"globs",
|
|
29
|
+
"args",
|
|
30
|
+
"items",
|
|
31
|
+
"patterns",
|
|
32
|
+
"include",
|
|
33
|
+
"exclude",
|
|
34
|
+
"queries",
|
|
35
|
+
"urls",
|
|
36
|
+
"commands",
|
|
37
|
+
]);
|
|
38
|
+
|
|
15
39
|
function isDegenerateAutoLink(linkText: string, url: string): boolean {
|
|
16
40
|
return linkText === url || linkText === decodeURI(url);
|
|
17
41
|
}
|
|
18
42
|
|
|
43
|
+
function isArrayKey(key: string): boolean {
|
|
44
|
+
if (ARRAY_KEYS.has(key)) return true;
|
|
45
|
+
return /_(files|paths|globs|args|items|patterns)$/.test(key);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function looksLikePath(text: string): boolean {
|
|
49
|
+
if (/\s/.test(text)) return false;
|
|
50
|
+
return text.includes("/") || text.includes(".") || /^[a-zA-Z0-9_-]+$/.test(text);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseJsonArray(value: string): unknown[] | null {
|
|
54
|
+
const trimmed = value.trim();
|
|
55
|
+
if (!trimmed.startsWith("[")) return null;
|
|
56
|
+
try {
|
|
57
|
+
const parsed = JSON.parse(trimmed);
|
|
58
|
+
return Array.isArray(parsed) ? parsed : null;
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isEmptyObject(value: unknown): boolean {
|
|
65
|
+
return (
|
|
66
|
+
typeof value === "object" &&
|
|
67
|
+
value !== null &&
|
|
68
|
+
!Array.isArray(value) &&
|
|
69
|
+
Object.keys(value as Record<string, unknown>).length === 0
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
19
73
|
export interface RepairResult {
|
|
20
74
|
toolName: string;
|
|
21
75
|
repairs: string[];
|
|
@@ -31,39 +85,58 @@ export function repairToolInput(
|
|
|
31
85
|
input: Record<string, unknown>,
|
|
32
86
|
): RepairResult {
|
|
33
87
|
const repairs: string[] = [];
|
|
34
|
-
|
|
35
|
-
toolName,
|
|
36
|
-
);
|
|
37
|
-
if (isBuiltin || !input || typeof input !== "object") {
|
|
88
|
+
if (CORE_TOOLS.has(toolName) || !input || typeof input !== "object") {
|
|
38
89
|
return { toolName, repairs };
|
|
39
90
|
}
|
|
40
91
|
|
|
92
|
+
if (typeof input.path !== "string" || !input.path) {
|
|
93
|
+
for (const alias of PATH_ALIASES) {
|
|
94
|
+
const value = input[alias];
|
|
95
|
+
if (typeof value === "string" && looksLikePath(value)) {
|
|
96
|
+
input.path = value;
|
|
97
|
+
delete input[alias];
|
|
98
|
+
repairs.push(`path-alias:${alias}`);
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
41
104
|
for (const key of Object.keys(input)) {
|
|
42
105
|
const value = input[key];
|
|
43
|
-
// null-for-optional: model stuurde null, schema wil afwezig
|
|
44
106
|
if (value === null) {
|
|
45
107
|
delete input[key];
|
|
46
108
|
repairs.push(`null-for-optional:${key}`);
|
|
47
109
|
continue;
|
|
48
110
|
}
|
|
49
|
-
// markdown auto-link op pad-achtige velden: [x.md](http://x.md) → x.md
|
|
50
111
|
if (typeof value === "string") {
|
|
51
112
|
const m = AUTO_LINK_RE.exec(value);
|
|
52
113
|
if (m && m[1] && isDegenerateAutoLink(m[1], m[3] ?? "")) {
|
|
53
114
|
input[key] = m[1];
|
|
54
115
|
repairs.push(`auto-link-unwrap:${key}`);
|
|
55
116
|
}
|
|
117
|
+
const current = input[key];
|
|
118
|
+
if (typeof current === "string") {
|
|
119
|
+
const parsed = parseJsonArray(current);
|
|
120
|
+
if (parsed) {
|
|
121
|
+
input[key] = parsed;
|
|
122
|
+
repairs.push(`json-string-array:${key}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const afterParse = input[key];
|
|
127
|
+
if (isArrayKey(key) && isEmptyObject(afterParse)) {
|
|
128
|
+
input[key] = [];
|
|
129
|
+
repairs.push(`empty-object-placeholder:${key}`);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (isArrayKey(key) && typeof afterParse === "string") {
|
|
133
|
+
input[key] = [afterParse];
|
|
134
|
+
repairs.push(`bare-string-wrap:${key}`);
|
|
56
135
|
}
|
|
57
136
|
}
|
|
58
137
|
return { toolName, repairs };
|
|
59
138
|
}
|
|
60
139
|
|
|
61
|
-
function looksLikePath(text: string): boolean {
|
|
62
|
-
if (/\s/.test(text)) return false;
|
|
63
|
-
return text.includes("/") || text.includes(".") || /^[a-zA-Z0-9_-]+$/.test(text);
|
|
64
|
-
}
|
|
65
|
-
void looksLikePath;
|
|
66
|
-
|
|
67
140
|
/** Repair-teller per (tool, repair) — zichtbaar via /repairs. */
|
|
68
141
|
const repairCounts = new Map<string, number>();
|
|
69
142
|
|
|
@@ -17,13 +17,24 @@ import {
|
|
|
17
17
|
import {
|
|
18
18
|
getSessionTotalCost,
|
|
19
19
|
getUsageTokenTotal,
|
|
20
|
-
hasSessionAssistantUsage,
|
|
21
20
|
isSessionAssistantMessage,
|
|
22
21
|
} from "../../usage/ledger.ts";
|
|
23
22
|
import {
|
|
24
23
|
formatCostAlertMessage,
|
|
25
24
|
shouldTriggerCostAlert,
|
|
26
25
|
} from "./cost-alert.ts";
|
|
26
|
+
import {
|
|
27
|
+
formatTokenBudgetWarning,
|
|
28
|
+
parseTokenBudget,
|
|
29
|
+
tokenBudgetLevel,
|
|
30
|
+
} from "../../usage/token-budget.ts";
|
|
31
|
+
import {
|
|
32
|
+
recordUsageEvent,
|
|
33
|
+
loadUsageFileFromDisk,
|
|
34
|
+
tokenTotal,
|
|
35
|
+
totalsForRange,
|
|
36
|
+
dayKey,
|
|
37
|
+
} from "../../usage/usage-store.ts";
|
|
27
38
|
import {
|
|
28
39
|
detectCustomCompactionEnabled,
|
|
29
40
|
readSettings,
|
|
@@ -94,6 +105,22 @@ function maybeNotifyCostAlert(rt: RuntimeState, ctx: any): void {
|
|
|
94
105
|
);
|
|
95
106
|
}
|
|
96
107
|
|
|
108
|
+
function maybeNotifyTokenBudget(rt: RuntimeState, ctx: any): void {
|
|
109
|
+
if (!ctx?.hasUI) return;
|
|
110
|
+
const daily = parseTokenBudget(readSettings(ctx.cwd ?? process.cwd()).wishcraft)
|
|
111
|
+
.daily;
|
|
112
|
+
if (!daily) return;
|
|
113
|
+
const now = Date.now();
|
|
114
|
+
const todayStart = Date.parse(`${dayKey(now)}T00:00:00`);
|
|
115
|
+
const used = tokenTotal(
|
|
116
|
+
totalsForRange(loadUsageFileFromDisk(), todayStart, now + 1),
|
|
117
|
+
);
|
|
118
|
+
const { level } = tokenBudgetLevel(used, daily);
|
|
119
|
+
if (level === 0 || level <= rt.tokenBudgetNotifiedLevel) return;
|
|
120
|
+
rt.tokenBudgetNotifiedLevel = level;
|
|
121
|
+
ctx.ui.notify(formatTokenBudgetWarning(used, daily, level), "warning");
|
|
122
|
+
}
|
|
123
|
+
|
|
97
124
|
// Helper to extract recent agent response text (skipping thinking blocks)
|
|
98
125
|
function getRecentAgentContext(ctx: any): string | undefined {
|
|
99
126
|
const sessionEvents = ctx.sessionManager?.getBranch?.() ?? [];
|
|
@@ -143,6 +170,7 @@ export function registerSessionLifecycle(
|
|
|
143
170
|
rt.isStreaming = false;
|
|
144
171
|
rt.liveAssistantUsage = null;
|
|
145
172
|
rt.costAlertNotified = false;
|
|
173
|
+
rt.tokenBudgetNotifiedLevel = 0;
|
|
146
174
|
rt.powerlineCompacting = false;
|
|
147
175
|
rt.deliverAfterRetrySettles = false;
|
|
148
176
|
rt.stashedEditorText = null;
|
|
@@ -193,6 +221,7 @@ export function registerSessionLifecycle(
|
|
|
193
221
|
} else {
|
|
194
222
|
dismissWelcome(rt, ctx);
|
|
195
223
|
}
|
|
224
|
+
maybeNotifyTokenBudget(rt, ctx);
|
|
196
225
|
}
|
|
197
226
|
});
|
|
198
227
|
|
|
@@ -310,10 +339,21 @@ export function registerSessionLifecycle(
|
|
|
310
339
|
rt.liveAssistantUsage = null;
|
|
311
340
|
} else if (getUsageTokenTotal(event.message.usage) > 0) {
|
|
312
341
|
rt.liveAssistantUsage = event.message.usage;
|
|
342
|
+
const usage = event.message.usage;
|
|
343
|
+
recordUsageEvent({
|
|
344
|
+
at: Date.now(),
|
|
345
|
+
model: ctx.model?.id ?? ctx.model?.name,
|
|
346
|
+
input: usage.input,
|
|
347
|
+
output: usage.output,
|
|
348
|
+
cacheRead: usage.cacheRead,
|
|
349
|
+
cacheWrite: usage.cacheWrite,
|
|
350
|
+
cost: usage.cost.total,
|
|
351
|
+
});
|
|
313
352
|
}
|
|
314
353
|
}
|
|
315
354
|
requestImmediateStatusRender(rt, { deferDuringTyping: false });
|
|
316
355
|
maybeNotifyCostAlert(rt, ctx);
|
|
356
|
+
maybeNotifyTokenBudget(rt, ctx);
|
|
317
357
|
});
|
|
318
358
|
|
|
319
359
|
pi.on("turn_end", async (_event, ctx) => {
|
|
@@ -145,7 +145,8 @@ export function buildConfigGroups(settings: Record<string, unknown>): ConfigGrou
|
|
|
145
145
|
title: "Hooks & repairs (harness-laag)",
|
|
146
146
|
items: [
|
|
147
147
|
{ label: "Hooks ingeschakeld", path: "wishcraft.hooksEnabled", kind: "toggle", hint: "commandcode-achtige preToolUse/postToolUse/sessionStart hooks" },
|
|
148
|
-
{ label: "Tool-input repairs", path: "wishcraft.repairsEnabled", kind: "toggle", hint: "null-for-optional, auto-link
|
|
148
|
+
{ label: "Tool-input repairs", path: "wishcraft.repairsEnabled", kind: "toggle", hint: "null-for-optional, auto-link, json-array, path aliases" },
|
|
149
|
+
{ label: "Dagelijks tokenbudget", path: "wishcraft.tokenBudget.daily", kind: "number", hint: "kleurt cost-segment; blokkeert nooit. 0 = uit" },
|
|
149
150
|
],
|
|
150
151
|
},
|
|
151
152
|
{
|