@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 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"**. Overlay-chrome + CHE-42 drill-down, Configure
93
- uit `ctx.ui.select`, token-overlays, rest-repairs, README-hooks die
94
- waar zijn. Done = drie README-hookvoorbeelden werken, repair-teller
95
- zichtbaar, `alt+p` overlay-boom, `/tps` deelt de ring met het segment.
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.2 staat op npm. Hooks, repairs-subset en
176
- skills-manager v2 UI zijn al op `main` (#12). Op `feat/complete-wishcraft-backlog`
177
- (reopen #13) landen nu de unieke 0.20-expansies bovenop CHE-42 (#19):
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` (`src/extension/hooks/`, `/repairs` stats-command; README-voorbeelden volgen in de release-PR)
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 — ✅ geland in `feat/wishcraft-0.19` (schema-loze subset: null-for-optional + auto-link unwrap; schema-afhankelijke repairs wachten op validator-issues in pi core)
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 | In Progress. #19 = drie top-level overlays + Status. Configure-overlay volgt. |
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`. 0.19
335
- accepteert dat; 0.20 vervangt het.
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 [value]`: show or set `POWERLINE_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)
@@ -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, custom segments, custom presets, labels, templates, layout, cost alert, and display formats.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groeponline/pi-wishcraft",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "description": "Wishcraft cockpit for the pi coding agent — powerline status, vibes, idea inbox, bash mode, and full customization.",
5
5
  "type": "module",
6
6
  "files": [
@@ -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 value",
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
- const current = process.env.POWERLINE_TPS || "not set";
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). Alleen de reparaties die zonder schema-kennis universeel veilig
6
- * zijn; schema-afhankelijke repairs (bare-string-wrap, JSON-string-array,
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
- const isBuiltin = ["bash", "read", "edit", "write", "grep", "find", "ls"].includes(
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 unwrap op custom tools" },
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
  {