@black942026/pi-ask-user 0.0.0-stage → 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 black
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,390 @@
1
- # Temporary Holding Version
1
+ # @black942026/pi-ask-user
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A Pi extension for asking structured questions through interactive terminal
4
+ or native input dialogs. Supports single-choice, multiple-choice, and free-text
5
+ answers, with configurable timeouts and cancellation.
6
+
7
+ Install the package to add the **`ask_user`** tool to Pi. Other extensions can
8
+ reuse the same interaction through the TypeScript API.
9
+
10
+ Two entry points share one strict questionnaire contract and one route resolution:
11
+
12
+ - the registered model-facing tool **`ask_user`**;
13
+ - the host-bound TypeScript API **`createAskUser(ctx)`**, for other extensions.
14
+
15
+ A questionnaire is answered through one of two routes, resolved once when the host
16
+ is created:
17
+
18
+ | Route | Runs when | The user sees |
19
+ | --- | --- | --- |
20
+ | `custom` | the host is a real Pi TUI with a bound renderer | a tabbed terminal dialog: options, a pinned free-input row, and a Markdown preview |
21
+ | `native` | `ctx.hasUI` with a callable `ctx.ui.input` (TUI, RPC/ACP) | native input dialogs, one question at a time (invalid input re-prompts) |
22
+
23
+ Only these two routes exist, and the route belongs to the host: it is configured
24
+ or probed when the host is created, never per request.
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ pi install npm:@black942026/pi-ask-user
30
+ ```
31
+
32
+ Start a new Pi session, or run `/reload` in an existing session, to load the
33
+ extension. The `ask_user` tool is registered automatically.
34
+
35
+ Requires Node.js ≥ 22.19.0 and Pi ≥ 0.87.0. Pi supplies the host dependencies
36
+ `@earendil-works/pi-coding-agent`, `@earendil-works/pi-tui`, and `typebox`;
37
+ no separate dependency installation is needed when using `pi install`.
38
+
39
+ ## Tool parameters
40
+
41
+ The registered tool name is exactly **`ask_user`**.
42
+
43
+ | Field | Type | Notes |
44
+ | --- | --- | --- |
45
+ | `questions` | `Question[]` | 1–5 questions (required) |
46
+ | `questions[].title` | `string` | heading (required) |
47
+ | `questions[].prompt` | `string?` | extra hint under the heading |
48
+ | `questions[].kind` | `"single" \| "multi" \| "input"?` | inferred from `options` when omitted: present ⇒ `single`, absent ⇒ `input`; `multi` is explicit |
49
+ | `questions[].options` | `Option[]?` | 1–5 × `{ label, description?, preview? }`; required for `single`/`multi`, ignored for `input` |
50
+ | `questions[].default` | `string?` | makes the question skippable |
51
+ | `questions[].id` | `string?` | non-empty; generated as `q1`, `q2`, … when omitted |
52
+ | `header` | `string?` | heading above the questionnaire |
53
+ | `displayMode` | `"overlay" \| "inline"?` | custom route only; default `overlay`; a user-configured preference overrides it |
54
+ | `timeoutPerQuestionMs` | `number?` | base timeout per question; default `60000`; a user-configured preference overrides it |
55
+
56
+ These fields are the whole input contract, for the tool and for direct calls
57
+ alike. Unknown fields, wrong types, empty ids, and over-long values are rejected
58
+ as `invalid_request`; a `single`/`multi` question without options is an error. An
59
+ explicit `kind: "input"` ignores `options` — if present they are still validated,
60
+ then dropped. Duplicate option labels are de-duplicated and reported in
61
+ `warnings`. Limits: 1–5 questions, ≤5 options each.
62
+
63
+ ```json
64
+ {
65
+ "questions": [
66
+ {
67
+ "title": "Which deploy target?",
68
+ "kind": "single",
69
+ "options": [
70
+ { "label": "staging", "description": "Internal smoke test" },
71
+ { "label": "production", "description": "Customer-facing" }
72
+ ]
73
+ }
74
+ ]
75
+ }
76
+ ```
77
+
78
+ ## User config file
79
+
80
+ There are **no environment variables**. User preferences live in a documented
81
+ JSON file:
82
+
83
+ ```
84
+ ~/.pi/ask-user/config.json
85
+ ```
86
+
87
+ No file — or an empty/whitespace-only file — means the built-in defaults. The
88
+ knobs are:
89
+
90
+ | Key | Type | Default | Meaning |
91
+ | --- | --- | --- | --- |
92
+ | `mode` | `"custom" \| "native"` | omitted ⇒ probe | force the UI route |
93
+ | `displayMode` | `"overlay" \| "inline"` | `overlay` | custom-UI presentation |
94
+ | `overlayToggleKey` | KeyId chord, or `null`/`"off"`/`"none"`/`"disabled"` | `alt+o` | overlay show/hide key; the disable values turn it off |
95
+ | `timeoutPerQuestionMs` | non-negative number | `60000` | base timeout per question |
96
+
97
+ ```json
98
+ {
99
+ "mode": "custom",
100
+ "displayMode": "overlay",
101
+ "overlayToggleKey": "alt+o",
102
+ "timeoutPerQuestionMs": 60000
103
+ }
104
+ ```
105
+
106
+ Validation is strict and actionable. Invalid JSON, an unknown key, a wrong type,
107
+ or an out-of-range value is reported as `invalid_config` naming the file and the
108
+ offending field — never ignored, coerced, or fallen back from. A bad config
109
+ makes the host unusable: every `ask_user` call is refused with the actionable
110
+ reason, and the model is told to ask in ordinary text instead.
111
+
112
+ `overlayToggleKey` is validated against the supported **KeyId** grammar and
113
+ against the questionnaire's own controls. A malformed spec (`alt+banana`), a
114
+ reserved key (`escape`/`esc` — which must keep cancelling — plus
115
+ `enter`/`tab`/arrows/`space`/`pageUp`/…), a bare printable key that would swallow
116
+ typing (`o`), and conflicting chords (`ctrl+c`, `ctrl+d`, `ctrl+u`, `ctrl+p`,
117
+ `ctrl+n`, `ctrl+enter`, `alt+q`) are each rejected with a specific reason. A key
118
+ is never silently substituted.
119
+
120
+ ### Effective precedence and read timing
121
+
122
+ Per knob:
123
+
124
+ ```
125
+ explicit programmatic option > user config file > built-in default
126
+ ```
127
+
128
+ and, for the two request-overridable knobs only:
129
+
130
+ ```
131
+ user displayMode / timeoutPerQuestionMs > the model's request value > built-in default
132
+ ```
133
+
134
+ So a user who configures `displayMode` or `timeoutPerQuestionMs` always wins over
135
+ whatever the model sends; when the user configured nothing, the model's value
136
+ applies; when neither is set, the built-in default applies.
137
+
138
+ Timing: the config file is read **once per host/session creation**, never per
139
+ request. For the registered tool the host is built in `session_start`, so the
140
+ route and preferences are fixed for the session; a reload
141
+ (`session_shutdown` + `session_start`) re-reads. `createPiHost` / `createAskUser`
142
+ read once at construction. Route resolution and preference resolution happen
143
+ once, together, and are then immutable.
144
+
145
+ Programmatic options are accepted by all three entry points:
146
+ `registerAskUser(pi, { mode, displayMode, overlayToggleKey, timeoutPerQuestionMs, configFile })`,
147
+ `createPiHost(ctx, { ... })`, and `createAskUser(ctx, { ... })`. `configFile`
148
+ may point at another path, or be `false` to skip the file (used by embedding and
149
+ by this repo's tests). `overlayToggleKey: null` disables the toggle explicitly.
150
+
151
+ ## Routing and capabilities
152
+
153
+ The route is resolved once, at host creation, from exactly two inputs:
154
+
155
+ 1. an explicit `mode` (`custom` | `native`) — used verbatim when its
156
+ implementation is bound; otherwise refused with `unsupported_mode`, and the
157
+ other bound mode is **not** substituted;
158
+ 2. otherwise an initialisation-time **probe**: `custom` when both can run (custom
159
+ preferred), else the single mode that can run.
160
+
161
+ With neither available there is no route and the call is refused with
162
+ `unsupported_mode`; an invalid configured value is refused with `invalid_config`.
163
+ A render failure never silently switches `custom` → `native`: it is reported as
164
+ `custom_ui_failed`/`native_ui_failed`.
165
+
166
+ `askUserSupport(host)` reports whether — and how — a host can prompt, without
167
+ prompting. It is a discriminated union on `status`:
168
+
169
+ | `status` | Meaning |
170
+ | --- | --- |
171
+ | `available` | can prompt; `route` is `custom`/`native`, `source` is `configured`/`probed`, `available` lists every mode that can run |
172
+ | `configured_unavailable` | the configured mode cannot run; `configured` names it; no fallback |
173
+ | `no_available_ui` | neither a custom TUI nor native dialogs can run |
174
+ | `invalid_config` | the configured value is not a known route |
175
+
176
+ With `createPiHost(ctx, { mode })`, `customUI` is bound only in `tui` mode with a
177
+ callable `ctx.ui.custom()`, and `nativeDialogs` only when `ctx.hasUI` and
178
+ `ctx.ui.input` is callable (TUI, RPC). RPC reports `hasUI: true`, so a
179
+ nonresponsive client is not pre-judged: the deadline turns silence into a
180
+ `timeout`. JSON/print have no dialog-capable UI and are refused with
181
+ `unsupported_mode`.
182
+
183
+ In an extension, `registerAskUser(pi, { mode })` captures the mode at registration
184
+ and builds the host once per session from the context delivered to
185
+ `pi.on("session_start", ...)`, releasing it on `session_shutdown`. A call before
186
+ the session has started fails with `not_initialized`.
187
+
188
+ ## Direct API
189
+
190
+ For another extension that wants to ask without going through the model, bind the
191
+ interaction to its host and check availability first:
192
+
193
+ ```ts
194
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
195
+ import { createAskUser } from "@black942026/pi-ask-user";
196
+
197
+ async function askWhereToDeploy(ctx: ExtensionContext) {
198
+ const ask = createAskUser(ctx); // or createAskUser(host), or { mode }
199
+ if (!ask.isAvailable) {
200
+ // ask.notAvailableReason explains why, before any UI is shown
201
+ throw new Error(ask.notAvailableReason ?? "no interactive UI available");
202
+ }
203
+
204
+ const result = await ask({
205
+ questions: [{ title: "Deploy where?", kind: "single", options: [{ label: "staging" }, { label: "prod" }] }],
206
+ });
207
+ // result.status === "answered" | "aborted" | "timeout" | "error"
208
+ return result;
209
+ }
210
+ ```
211
+
212
+ `createAskUser` returns a callable object:
213
+
214
+ - `ask(request)` → `Promise<AskUserResult>`; it never throws for a bad request or
215
+ an unavailable host, and it never forwards anything to the model — a direct call
216
+ belongs to the caller, with no plain-text fallback.
217
+ - `ask.isAvailable` → `true` when the bound host has a ready interactive route
218
+ (its structural/configured capability, not a promise that a render will succeed).
219
+ - `ask.notAvailableReason` → the reason string when it has none, `undefined` when
220
+ it has one.
221
+ - `ask.host` → the bound `AskUserHost`.
222
+
223
+ It accepts a Pi `ExtensionContext` (with optional options) and probes once,
224
+ reading the user config file once at the same time, or a pre-built
225
+ `AskUserHost` to reuse one. Availability reflects that host's structural +
226
+ configured state. Pass `events` to observe the interactive wait; a direct call
227
+ otherwise emits no runtime events (there is no fabricated bus).
228
+
229
+ ## Tool result and model fallback
230
+
231
+ The tool returns its result to the model as tool output. Its text depends on the
232
+ outcome, and it always tells the model **not to invent an answer**:
233
+
234
+ - `answered` — the answers.
235
+ - `aborted` / `timeout` — no answer; reported plainly, with no instruction to
236
+ re-ask in text (the UI may already have been shown, so a text re-ask would
237
+ prompt the user twice).
238
+ - Interactive UI failed — `unsupported_mode`, `invalid_config`, `not_initialized`,
239
+ `custom_ui_failed`, `native_ui_failed`: the UI could not complete the request, so
240
+ the text advises the model to **ask the user the question itself in ordinary
241
+ text**, including the question and options.
242
+ - Invalid questionnaire — `invalid_request`: a request error, **not** a UI failure;
243
+ the model is told to fix the call. Other errors (`empty_answer`, `internal`) are
244
+ reported plainly with no retry instruction.
245
+
246
+ The ordinary-text advice is tool output only. It is not a route, not an answer,
247
+ and no questionnaire is handed back as if the user had answered it.
248
+
249
+ ## Runtime events
250
+
251
+ Pi already emits generic `ui_prompt_start` / `ui_prompt_end` around a blocking
252
+ extension UI, but they carry no result semantics. The tool adds result-bearing
253
+ events on `pi.events` (default payloads never include the question, the answers,
254
+ or free text):
255
+
256
+ | Event | When | Payload (default) |
257
+ | --- | --- | --- |
258
+ | `herdr:blocked` | entering the interactive wait | `{ active: true, label: "Waiting for user response", callId? }` |
259
+ | `herdr:blocked` | the wait terminated, once, for every outcome | `{ active: false, callId? }` |
260
+ | `ask:answered` | the user answered | `{ callId?, route, status: "answered" }` |
261
+ | `ask:aborted` | dismissed or caller-aborted | `{ callId?, route, status: "aborted", cancelledReason: "user" \| "abort" }` |
262
+ | `ask:timeout` | the shared deadline expired | `{ callId?, route, status: "timeout" }` |
263
+ | `ask:error` | the renderer failed / the UI could not finish | `{ callId?, route?, status: "error", errorCode }` |
264
+
265
+ Guarantees:
266
+
267
+ - `herdr:blocked` `{ active: true }` is emitted **only when the tool actually
268
+ enters the interactive wait**, and is always followed by exactly one matching
269
+ `{ active: false }`, including abort, timeout, and error.
270
+ - Invalid (`invalid_request`) and unavailable/refused requests emit **nothing**:
271
+ no UI is attempted, so neither a blocked pair nor an outcome event is sent.
272
+ - Each outcome event is emitted **exactly once per UI attempt** and carries the
273
+ correlation id (the tool call id, when available) plus route/status.
274
+ - The full question, answers, and free text are never broadcast by default.
275
+
276
+ A direct `createAskUser` caller has no Pi event bus. Instead of faking one, pass
277
+ an optional sink — `createAskUser(ctx, { events: { waitStarted, waitEnded } })`,
278
+ or `createAskUser(host, { events })` — or accept that no runtime events are
279
+ emitted. `AskUserEventSink` is exported for this.
280
+
281
+ ## Cancel countdown shortcut
282
+
283
+ In the **custom terminal UI** (overlay or inline), press **`Alt+Q`** to cancel
284
+ this questionnaire's countdown without dismissing it or changing your answers.
285
+ The header shows **`No timeout`**; you can keep answering, submit normally, or
286
+ press `Esc` to dismiss. Caller aborts still work. This is a one-way change for
287
+ the current questionnaire only; the next call starts with its configured timeout.
288
+ An already-expired deadline cannot be cancelled.
289
+
290
+ The key is shown in the bottom hints and reserved against `overlayToggleKey`
291
+ conflicts. Native input dialogs do not support this shortcut. If an overlay is
292
+ hidden, restore it first with its show/hide key before pressing `Alt+Q`.
293
+
294
+ ## Overlay show/hide key
295
+
296
+ On the **custom overlay route only** (never inline, never native), the overlay
297
+ can be hidden and restored with the same key:
298
+
299
+ - default `alt+o`; configurable via `overlayToggleKey` and disableable with
300
+ `null` / `"off"` / `"none"` / `"disabled"`; reserved/conflicting keys are
301
+ rejected by config validation (see above) so `Esc` always keeps cancelling;
302
+ - implemented with `ctx.ui.custom(..., { onHandle })` →
303
+ `OverlayHandle.setHidden()`, plus a raw `ctx.ui.onTerminalInput` listener so the
304
+ **same** key restores the overlay even while hidden (a hidden overlay receives
305
+ no component input);
306
+ - the toggle key is consumed; Kitty press/repeat/release events are handled so a
307
+ single physical press cannot hide and immediately re-show it;
308
+ - the first hide shows a one-time discoverable restore notice
309
+ (`ask_user hidden — press <key> to reopen`);
310
+ - cleanup is idempotent and **does not depend on `ui.custom()` resolving**. The
311
+ core settles a timeout/abort independently, so the renderer listens to the
312
+ interaction signal and removes the raw listener, drops the handle, and aborts
313
+ the component immediately; the same cleanup also runs from `finally`. A
314
+ renderer that ignores its signal and never resolves therefore cannot keep
315
+ capturing keys or leave UI side effects after termination;
316
+ - hiding neither resolves the interaction nor pauses its deadline — aborts and
317
+ timeouts still complete while hidden — and `Esc` still cancels;
318
+ - inline display and the native route register no listener at all.
319
+
320
+ ## Results
321
+
322
+ ```ts
323
+ type AskUserStatus = "answered" | "aborted" | "timeout" | "error";
324
+
325
+ interface AskUserResult {
326
+ status: AskUserStatus;
327
+ route?: "custom" | "native"; // absent when no route ran or none exists
328
+ answers: AskUserAnswer[]; // selections (labels), optional freeText, usedDefault
329
+ cancelledReason?: "user" | "abort";
330
+ warnings?: string[];
331
+ error?: { code: AskUserErrorCode; message: string };
332
+ }
333
+ ```
334
+
335
+ `aborted` covers both a user dismissal and a caller abort, distinguished by
336
+ `cancelledReason`. Errors use `empty_answer`, `invalid_request`,
337
+ `custom_ui_failed`, `native_ui_failed`, `invalid_config`, `unsupported_mode`,
338
+ `not_initialized`, or `internal`. `route` is present only when a route ran — or,
339
+ for a refused explicit config, names the requested route.
340
+
341
+ ## Key constraints
342
+
343
+ - **The deadline is shared and owned by the core.** Per questionnaire it is
344
+ initially `timeoutPerQuestionMs × questionCount` (default 60 s each), clamped to
345
+ `MAX_SAFE_TIMEOUT_MS` (2³¹−1); there is no absolute override. The deadline and
346
+ the caller's cancellation are raced independently of the renderer, so a renderer
347
+ that never resolves or ignores its `AbortSignal` still yields
348
+ `timeout`/`aborted`, unless the user explicitly cancels the timeout with `Alt+Q`
349
+ in the custom UI (caller aborts remain effective); late results are discarded.
350
+ Cancellation is checked before any route branch, so an already-aborted call
351
+ never renders a UI.
352
+ - **A missing route is an error, not a downgrade.** JSON/print, or an explicit
353
+ `mode` that cannot run, is refused with `unsupported_mode` and never replaced by
354
+ another route.
355
+ - **Configuration is a file, never the environment.** No `process.env` knob
356
+ exists; `~/.pi/ask-user/config.json` is read once per host/session and
357
+ strictly validated. An invalid config is `invalid_config`, never a fallback.
358
+ - **Events never leak content by default.** `herdr:blocked` bounds a real wait
359
+ exactly once on each side; outcome events are one per UI attempt and carry only
360
+ correlation id, route, and status.
361
+ - **The overlay toggle is overlay-only.** Inline and native register no raw
362
+ input listener; hiding never pauses the deadline.
363
+ - **The native route uses single-line `input`**, one question at a time (not
364
+ `editor`, which accepts no timeout/signal). The number and an optional note
365
+ share one box: `2`, `2 | note`, or free text; a number list may be
366
+ comma/space/`,`/`、` separated (`1,3`). Out-of-range or malformed numbers
367
+ re-prompt, and an empty box is rejected unless the question has a `default`.
368
+ - **Tool calls run sequentially** (`executionMode: "sequential"`).
369
+ - **Short terminals degrade in a defined order:** the pinned free-input row and
370
+ the key hints survive first, down to `MIN_USABLE_ROWS` (3); 0 rows renders
371
+ nothing. `render(width)` never exceeds the requested width or `terminal.rows`.
372
+
373
+ ## Development
374
+
375
+ ```bash
376
+ npm install
377
+ npm run typecheck # tsc --noEmit
378
+ npm test # node --test test/*.test.ts
379
+ npm run check # typecheck + tests
380
+
381
+ # Load a local checkout for development
382
+ pi --extension ./index.ts
383
+ # Or install it as a local package
384
+ pi install /absolute/path/to/pi-ask-user
385
+ ```
386
+
387
+ `@earendil-works/pi-tui` and `typebox` are host-provided peer dependencies used
388
+ by the custom UI and parameter schema (`src/schema.ts`). They are also declared
389
+ as development dependencies for local checks. The routing, parsing, and deadline
390
+ layers are pure, non-TUI, and free of both.
package/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./src/index.ts";
2
+ export { default } from "./src/index.ts";
package/package.json CHANGED
@@ -1,6 +1,64 @@
1
1
  {
2
2
  "name": "@black942026/pi-ask-user",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Structured user questions for Pi, with interactive terminal dialogs, native input dialogs, and a reusable TypeScript API.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/black-94/pi-ask-user.git"
10
+ },
11
+ "homepage": "https://github.com/black-94/pi-ask-user#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/black-94/pi-ask-user/issues"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "engines": {
19
+ "node": ">=22.19.0"
20
+ },
21
+ "keywords": [
22
+ "pi-package",
23
+ "pi-extension",
24
+ "pi",
25
+ "pi-coding-agent",
26
+ "extension",
27
+ "ask_user",
28
+ "AskUser",
29
+ "interactive"
30
+ ],
31
+ "pi": {
32
+ "extensions": [
33
+ "./index.ts"
34
+ ]
35
+ },
36
+ "exports": {
37
+ ".": "./index.ts",
38
+ "./types": "./src/types.ts"
39
+ },
40
+ "files": [
41
+ "index.ts",
42
+ "src",
43
+ "README.md",
44
+ "LICENSE"
45
+ ],
46
+ "scripts": {
47
+ "test": "node --test test/*.test.ts",
48
+ "typecheck": "tsc --noEmit",
49
+ "check": "npm run typecheck && npm run test",
50
+ "prepublishOnly": "npm run check"
51
+ },
52
+ "peerDependencies": {
53
+ "@earendil-works/pi-coding-agent": ">=0.87.0",
54
+ "@earendil-works/pi-tui": ">=0.87.0",
55
+ "typebox": "*"
56
+ },
57
+ "devDependencies": {
58
+ "@earendil-works/pi-coding-agent": "0.87.1",
59
+ "@earendil-works/pi-tui": "0.87.1",
60
+ "@types/node": "22.10.2",
61
+ "typebox": "1.3.27",
62
+ "typescript": "5.9.3"
63
+ }
64
+ }