@geoqiao/pi-ask 1.1.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +194 -0
  2. package/LICENSE +22 -0
  3. package/README.md +282 -0
  4. package/docs/README.md +33 -0
  5. package/docs/configuration.md +406 -0
  6. package/docs/contract.md +309 -0
  7. package/docs/remote-events.md +187 -0
  8. package/package.json +130 -0
  9. package/skills/ask-user/SKILL.md +110 -0
  10. package/src/answer-commands.ts +361 -0
  11. package/src/answer-extraction.ts +354 -0
  12. package/src/ask-payload-store.ts +86 -0
  13. package/src/ask-settings-command.ts +14 -0
  14. package/src/ask-tool-helpers.ts +172 -0
  15. package/src/ask-tool.ts +84 -0
  16. package/src/config/defaults.ts +216 -0
  17. package/src/config/migrate.ts +70 -0
  18. package/src/config/migrations/index.ts +139 -0
  19. package/src/config/migrations/types.ts +10 -0
  20. package/src/config/schema.ts +287 -0
  21. package/src/config/store.ts +227 -0
  22. package/src/constants/keymaps.ts +721 -0
  23. package/src/constants/text.ts +12 -0
  24. package/src/constants/ui.ts +22 -0
  25. package/src/index.ts +30 -0
  26. package/src/math.ts +3 -0
  27. package/src/notifications.ts +119 -0
  28. package/src/remote-ask.ts +563 -0
  29. package/src/result-format.ts +157 -0
  30. package/src/result.ts +23 -0
  31. package/src/schema.ts +74 -0
  32. package/src/state/answers.ts +251 -0
  33. package/src/state/create.ts +18 -0
  34. package/src/state/editor.ts +70 -0
  35. package/src/state/navigation.ts +86 -0
  36. package/src/state/normalize.ts +326 -0
  37. package/src/state/question-type.ts +128 -0
  38. package/src/state/result.ts +263 -0
  39. package/src/state/selectors.ts +135 -0
  40. package/src/state/transitions.ts +330 -0
  41. package/src/state/view.ts +28 -0
  42. package/src/text.ts +98 -0
  43. package/src/types.ts +169 -0
  44. package/src/ui/auto-submit.ts +36 -0
  45. package/src/ui/autocomplete.ts +52 -0
  46. package/src/ui/controller.ts +645 -0
  47. package/src/ui/dismiss-guard.ts +26 -0
  48. package/src/ui/input.ts +160 -0
  49. package/src/ui/render-frame.ts +235 -0
  50. package/src/ui/render-helpers.ts +385 -0
  51. package/src/ui/render-question.ts +288 -0
  52. package/src/ui/render-submit.ts +168 -0
  53. package/src/ui/render-types.ts +33 -0
  54. package/src/ui/render.ts +53 -0
  55. package/src/ui/review-shortcuts.ts +43 -0
  56. package/src/ui/settings-list.ts +461 -0
  57. package/src/ui/show-settings.ts +37 -0
  58. package/src/ui/view-models/question.ts +203 -0
  59. package/src/ui/view-models/review.ts +100 -0
@@ -0,0 +1,309 @@
1
+ # Ask tool contract
2
+
3
+ `ask_user` is a pi-native clarification tool for cases where implementation depends on user preference or missing requirements.
4
+
5
+ This document defines the stable external behavior. It does not explain internal helper-by-helper implementation.
6
+
7
+ ## Input
8
+
9
+ ```ts
10
+ {
11
+ title?: string;
12
+ questions: Array<{
13
+ id: string;
14
+ label?: string;
15
+ prompt: string;
16
+ type?: "single" | "multi" | "preview";
17
+ required?: boolean;
18
+ options: Array<{
19
+ value: string;
20
+ label: string;
21
+ description?: string;
22
+ preview?: string;
23
+ }>;
24
+ }>;
25
+ }
26
+ ```
27
+
28
+ ## Input rules
29
+
30
+ - at least one question is required
31
+ - every question must have non-empty trimmed `id` and `prompt`
32
+ - every question must have at least one option
33
+ - question ids must be unique within one tool call
34
+ - option `value`s must be unique within a question
35
+ - optional `label`, `description`, and `preview` fields must not be blank when provided
36
+ - `label` falls back to `Q1`, `Q2`, ...
37
+ - `type` defaults to `single`
38
+ - `required` defaults to `false`
39
+ - `required` is metadata only; it never blocks submission
40
+ - preview questions require preview text for every declared option; option descriptions do not satisfy this requirement, and invalid preview payloads report a fix hint to add preview text or switch to `type: "single"`
41
+ - all questions get an internal `Type your own` option
42
+
43
+ ## Output
44
+
45
+ ```ts
46
+ {
47
+ content: [{ type: "text"; text: string }];
48
+ details: {
49
+ title?: string;
50
+ cancelled: boolean;
51
+ error?: {
52
+ kind: "invalid_input";
53
+ issues: Array<{
54
+ path: string;
55
+ message: string;
56
+ }>;
57
+ };
58
+ mode: "submit" | "elaborate";
59
+ questions: Array<{
60
+ id: string;
61
+ label: string;
62
+ prompt: string;
63
+ type: "single" | "multi" | "preview";
64
+ presentedType?: "single" | "multi" | "preview";
65
+ }>;
66
+ answers: Record<
67
+ string,
68
+ {
69
+ values: string[];
70
+ labels: string[];
71
+ indices: number[];
72
+ customText?: string;
73
+ note?: string;
74
+ optionNotes?: Record<string, string>;
75
+ }
76
+ >;
77
+ continuation?: {
78
+ strategy: "refine_only" | "resume";
79
+ affectedQuestionIds: string[];
80
+ preservedAnswers: Record<string, {
81
+ values: string[];
82
+ labels: string[];
83
+ indices: number[];
84
+ customText?: string;
85
+ note?: string;
86
+ optionNotes?: Record<string, string>;
87
+ }>;
88
+ questionStates: Record<string, {
89
+ status: "answered" | "needs_clarification" | "unanswered";
90
+ }>;
91
+ };
92
+ elaboration?: {
93
+ instruction: string;
94
+ nextAction: "clarify" | "clarify_then_reask";
95
+ items: Array<
96
+ | {
97
+ target: { kind: "question" };
98
+ question: {
99
+ id: string;
100
+ label: string;
101
+ prompt: string;
102
+ type: "single" | "multi" | "preview";
103
+ presentedType?: "single" | "multi" | "preview";
104
+ options: Array<{
105
+ value: string;
106
+ label: string;
107
+ description?: string;
108
+ preview?: string;
109
+ }>;
110
+ };
111
+ answered: boolean;
112
+ answer?: {
113
+ values: string[];
114
+ labels: string[];
115
+ indices: number[];
116
+ customText?: string;
117
+ note?: string;
118
+ optionNotes?: Record<string, string>;
119
+ };
120
+ note: string;
121
+ }
122
+ | {
123
+ target: { kind: "option"; optionValue: string };
124
+ question: {
125
+ id: string;
126
+ label: string;
127
+ prompt: string;
128
+ type: "single" | "multi" | "preview";
129
+ presentedType?: "single" | "multi" | "preview";
130
+ options: Array<{
131
+ value: string;
132
+ label: string;
133
+ description?: string;
134
+ preview?: string;
135
+ }>;
136
+ };
137
+ option: {
138
+ value: string;
139
+ label: string;
140
+ description?: string;
141
+ preview?: string;
142
+ };
143
+ selected: boolean;
144
+ answered: boolean;
145
+ answer?: {
146
+ values: string[];
147
+ labels: string[];
148
+ indices: number[];
149
+ customText?: string;
150
+ note?: string;
151
+ optionNotes?: Record<string, string>;
152
+ };
153
+ note: string;
154
+ }
155
+ >;
156
+ };
157
+ };
158
+ }
159
+ ```
160
+
161
+ ## Output rules
162
+
163
+ - `cancelled: true` means the user dismissed the flow, UI was unavailable, or the payload was invalid before UI opened
164
+ - invalid payloads return `error.kind === "invalid_input"` with structured `issues` and a transcript-friendly `Invalid ask_user payload:` message
165
+ - `mode: "submit"` is normal completion; `mode: "elaborate"` means the user asked the agent to continue with follow-up clarification based on notes
166
+ - unanswered questions are omitted from `answers`
167
+ - in `mode: "elaborate"`, `answers` contains only committed answers; note-only entries move to `elaboration.items`
168
+ - `continuation.strategy === "refine_only"` means the next ask should refine the current flow rather than restart it
169
+ - `continuation.preservedAnswers` contains previously committed answers that should be kept as context and not re-asked
170
+ - `continuation.affectedQuestionIds` lists the only questions that should be revisited
171
+ - `continuation.questionStates` marks each question as `answered`, `needs_clarification`, or `unanswered`
172
+ - single-select answers still use arrays
173
+ - when `behaviour.presentSingleAsMulti` is enabled, requested single-select questions are presented and handled as multi-select in future/replayed ask flows; result question metadata keeps the requested `type`, adds `presentedType` when final presentation differs, and result text uses one compact note when any answered questions were presented differently
174
+ - `indices` are 1-based rendered option positions
175
+ - `customText` stores the free-form answer
176
+ - on single-select questions, saving free-form text clears selected options for that question
177
+ - on multi-select questions, `values` and `labels` include both selected options and `customText` when both are present
178
+ - on multi-select questions, selected options keep their original order and `customText` is appended last
179
+ - submitting free-form text on a multi-select question stays on the same question tab and marks the custom row selected
180
+ - on multi-select questions, toggling an empty custom row opens the free-form editor, while toggling a custom row with saved free-form text selects or deselects it without opening the editor or clearing the text
181
+ - saving or clearing free-form text on a multi-select question does not clear other selected options
182
+ - `note` stores a question-level note
183
+ - `optionNotes` includes only notes for selected options
184
+ - question notes may exist without a selected answer
185
+ - `elaboration.items` includes all question notes and all option notes, even for unselected options
186
+ - every elaboration item includes the full normalized question and option list for that question so referential notes like `above` remain understandable to the agent
187
+ - option-targeted elaboration items include the specific noted option plus whether it is currently selected
188
+ - question-targeted elaboration items include whether the question already has a committed answer
189
+ - `elaboration.instruction` tells the agent to answer the clarification directly first, then re-ask only the affected questions if a choice is still needed
190
+ - after clarification, agents should prefer another structured follow-up over plain-text multiple choice when a decision is still unresolved
191
+ - once prior answers narrow the branch, agents should bundle the next 2-3 related unresolved questions into one follow-up ask when possible, instead of using a long sequence of single-question asks
192
+ - `elaboration` is only present when `mode === "elaborate"`
193
+ - elaborate `content` text and transcript rendering describe each note directly using the full question prompt and option label, and include the current committed answer text when available, instead of a generic elaboration banner
194
+ - when the user selects `Elaborate` without adding notes, elaborate `content` text and transcript rendering still include the committed answer text so the agent can elaborate on that answer directly
195
+
196
+ ## Supported UX
197
+
198
+ - tabbed multi-question flow
199
+ - single-select, multi-select, and preview questions
200
+ - active question type changes via configurable `main.changeQuestionType` hotkey, default `t`; non-preview questions toggle `single <-> multi`; preview questions toggle `preview <-> multi`
201
+ - inline free-form answers for all question types
202
+ - native pi-style `@` file path autocomplete inside free-form answer and note editors
203
+ - question notes via `Shift+N`
204
+ - option notes via `n`
205
+ - number-key quick selection
206
+ - submit/elaborate/cancel review tab
207
+ - on the review tab, `Submit` and `Cancel` preview notes only for answered questions
208
+ - on the review tab, `Elaborate` preview expands to all question notes and all option notes, including notes on unselected options
209
+ - transcript-friendly call and result rendering
210
+ - `/answer` command to extract a raw-JSON `AskParams` form from the latest completed assistant message and open the ask UI
211
+ - `/answer` extraction may use an internal `freeform: true` option for open-ended questions with no explicit choices; these render as user-input-only questions with the label `Type your answer:`, no numbered option row, and no selection caret; this marker is not part of the public `ask_user` tool contract
212
+ - `/answer:again` command to replay the latest `/answer`-extracted form on the current branch
213
+ - `/ask:replay` command to replay the latest real `ask_user` form on the current branch
214
+ - ask settings list with binary behaviour/notification toggles and a guarded reset-to-defaults action
215
+ - `?` in the ask flow and `/ask-settings` in pi open the same lightweight ask settings overlay
216
+ - settings attempt to persist immediately when changed: `Auto-submit when answered without notes`, `Confirm dismiss when dirty`, `Double-press review shortcuts`, `Notifications`, and `Show footer hints`; `Present single-select as multi-select` persists immediately when saving succeeds but applies only to new/replayed ask flows; save failures revert the setting and show a manual-edit message; resetting config to defaults requires pressing the reset action twice within a short confirmation window
217
+ - `Keymaps` is a persisted, context-aware config section for global, main-flow, editor, note-editor, and settings-modal actions
218
+ - the settings list shows the absolute config file path for customizing keymaps, notifications, and extraction settings
219
+ - if the flow is already on the review tab, all questions are answered, and no notes exist, enabling auto-submit can complete the current ask flow immediately
220
+ - elaborate results are phrased as direct follow-up instructions, for example: `User asked to elaborate on question "Which option would you like to select?" option "Option A" with note "why this one?"`
221
+
222
+ ## Keyboard behavior
223
+
224
+ Main flow:
225
+
226
+ - `global.settings` opens ask settings; default: `?`
227
+ - `global.dismiss` dismisses the active ask surface; default: `Ctrl+C`
228
+ - `main.nextTab` / `main.previousTab` move between tabs; defaults: `Tab`/`Right`, `Shift+Tab`/`Left`
229
+ - `main.nextOption` / `main.previousOption` move between options or review actions; defaults: `Down`, `Up`
230
+ - `main.confirm`, `main.cancel`, and `main.toggle` confirm, cancel, or toggle; defaults: `Enter`, `Esc`, `Space`
231
+ - `main.changeQuestionType` changes the active question type (non-preview: `single <-> multi`; preview: `preview <-> multi`); default: `t`; destructive `multi -> single` changes require pressing the type hotkey again, with no timeout, and the pending confirmation clears on other navigation/actions
232
+ - `main.optionNote` and `main.questionNote` open option/question notes; defaults: `n`, `Shift+N`
233
+ - `1..9` is fixed and selects or toggles the matching option; on the review tab, `1`, `2`, and `3` trigger `Submit`, `Elaborate`, and `Cancel`
234
+ - when `Double-press review shortcuts` is enabled, review-tab `1`, `2`, and `3` require the same key twice without a timeout, and the review screen shows an inline hint for the pending action
235
+
236
+ Editing flow:
237
+
238
+ - `editor.submit` submits the current custom-answer editor input and closes the editor; default: `Enter`
239
+ - `noteEditor.save` saves the current note editor and keeps the ask flow open; default: `Enter`
240
+ - `editor.close` / `noteEditor.close` save draft and close the editor; default: `Esc`
241
+ - `global.dismiss` dismisses the entire flow immediately without saving the current editor draft when no dirty-dismiss confirmation is pending
242
+ - `global.settings` opens ask settings when the editor is empty; otherwise the key is delegated to the editor as text/input
243
+ - when editor has text, arrow keys and `Tab` stay in the editor so the cursor can move while typing
244
+ - when editor is empty, editor-context `*WhenEmpty` navigation actions move options or tabs without requiring the editor close binding first
245
+ - `@` remains a fixed file-reference affordance in editors
246
+
247
+ Settings modal:
248
+
249
+ - `settingsModal.close` closes settings; defaults: `Esc`, `Ctrl+C`, `?`
250
+ - `settingsModal.nextOption` / `settingsModal.previousOption` move between settings; defaults: `Down`, `Up`
251
+ - `settingsModal.toggle` toggles the highlighted setting and attempts to save immediately; if saving fails, the setting reverts and an error is shown; on the reset action, the same binding must be pressed twice within a short confirmation window; defaults: `Enter`, `Space`
252
+
253
+ Dirty dismiss:
254
+
255
+ - when `Confirm dismiss when dirty` is enabled, cancelling or dismissing a dirty ask flow requires the same action a second time
256
+ - the dirty-dismiss warning stays visible until the user changes tabs in the ask flow
257
+
258
+ ## Non-TUI and non-interactive modes
259
+
260
+ The rich ask flow uses `ctx.ui.custom()` and opens only in TUI mode. In print, JSON, RPC, or any other non-TUI mode, the tool returns a `Needs user input: ask_user requires interactive TUI mode.` message in `content` and a cancelled result in `details` instead of opening custom UI.
261
+
262
+ Validation is handled inside the tool so malformed calls produce the same structured error shape as other invalid payloads instead of relying on pre-execution schema failures.
263
+
264
+ The ask flow subscribes to runtime settings updates while open. In practice, this means changing `Auto-submit when answered without notes`, `Confirm dismiss when dirty`, `Double-press review shortcuts`, `Notifications`, `Show footer hints`, resetting config to defaults, or reloading config-backed keymaps can affect the in-progress ask flow immediately instead of only future asks when the change is saved or otherwise applied in memory. Load-time migrations and invalid config handling do not rewrite, rename, or back up the config file; invalid files load defaults for the session and show a notice. `Present single-select as multi-select` is applied when an ask flow is created and does not rewrite question semantics for an already-open flow; use `main.changeQuestionType` for live per-question changes.
265
+
266
+ ## Notifications
267
+
268
+ When enabled, pi-ask emits one best-effort external notification per ask session after the ask UI opens and waits for input. The default title is `pi ask`; the message is `Question waiting: <label or prompt>`. Channels run in configured order and failures never fail or cancel the ask flow.
269
+
270
+ ## Remote inter-extension events
271
+
272
+ pi-ask exposes a local `pi.events` contract for trusted Pi extensions. It does not expose a network API and does not automate terminal keystrokes. RPC or headless integrations should use a trusted in-process bridge extension that consumes these events rather than expecting the TUI-only custom surface to open.
273
+
274
+ Channels:
275
+
276
+ - `@eko24ive/pi-ask:started`
277
+ - `@eko24ive/pi-ask:completed`
278
+ - `@eko24ive/pi-ask:submit`
279
+ - `@eko24ive/pi-ask:submit-result`
280
+
281
+ Remote submissions must be explicit `{ kind: "answer" }` or `{ kind: "cancel" }` responses. Remote answers use question ids and normalized option values from the started event. pi-ask validates ids and values, recomputes labels/indices, and does not infer approval semantics from labels.
282
+
283
+ See [`remote-events.md`](remote-events.md) for payload shapes, examples, and a local smoke test.
284
+
285
+ ## Slash command replay/extraction
286
+
287
+ - valid `ask_user` payloads are persisted as branch custom entries before the UI opens, so `/ask:replay` can reopen them after cancel, `/resume`, or `/tree`
288
+ - `/answer` scans the current branch for the latest assistant message; if that message did not finish with `stop`, extraction is refused
289
+ - `/answer` expects the extractor to return raw JSON only; JSON parse failures are retried according to `answer.extractionRetries`, then reported to the user without opening the ask UI
290
+ - `{ "questions": [] }` from extraction means no questions were found and is not treated as an invalid ask payload
291
+ - command-flow cancellation closes with a notification and does not send a message to the agent
292
+ - submitted or elaborated command-flow results are sent back with user-message semantics
293
+ - replay commands scan only `ctx.sessionManager.getBranch()`, ignore sibling/future branch payloads, and revalidate stored payloads before opening the UI
294
+
295
+ The fallback message includes normalized pending questions and options so the caller can re-ask them manually. `details.questions` still contains normalized question metadata, while `details.answers` stays empty until a user responds.
296
+
297
+ ## Skill alignment (advisory)
298
+
299
+ The auto-bundled skill profile at `skills/ask-user/SKILL.md` defines agent-side decision-gate guidance for when to call `ask_user`. It is enabled by default when the package is installed, but can be disabled via `pi config`.
300
+
301
+ It is advisory only. If there is any conflict, contract + tests win.
302
+
303
+ ## Source of truth
304
+
305
+ Behavior should be verified against:
306
+
307
+ 1. `src/types.ts` and exported state/result helpers
308
+ 2. `tests/*.test.ts`
309
+ 3. this contract
@@ -0,0 +1,187 @@
1
+ # Remote ask events
2
+
3
+ pi-ask exposes a local `pi.events` contract for trusted Pi extensions that run in the same Pi process.
4
+
5
+ Use this for local bridges: status cards, desktop helpers, or approval UIs. Do not use terminal keystroke automation. pi-ask does not expose a network API.
6
+
7
+ ## Channels
8
+
9
+ Lifecycle:
10
+
11
+ - `@eko24ive/pi-ask:started`
12
+ - `@eko24ive/pi-ask:completed`
13
+
14
+ Remote submit:
15
+
16
+ - `@eko24ive/pi-ask:submit`
17
+ - `@eko24ive/pi-ask:submit-result`
18
+
19
+ ## Started
20
+
21
+ Emitted after a validated ask UI flow opens.
22
+
23
+ ```ts
24
+ type PiAskStartedEvent = {
25
+ version: 1;
26
+ flowId: string;
27
+ toolCallId?: string;
28
+ source: "tool" | "answer" | "answer:again" | "ask:replay";
29
+ title?: string;
30
+ questions: AskQuestion[];
31
+ createdAt: number;
32
+ };
33
+ ```
34
+
35
+ Use `flowId` for submit/correlation. Use `questions[].id` and `questions[].options[].value` for answers.
36
+
37
+ ## Submit an answer
38
+
39
+ ```ts
40
+ pi.events.emit("@eko24ive/pi-ask:submit", {
41
+ version: 1,
42
+ requestId: `bridge-${Date.now()}`,
43
+ flowId,
44
+ response: {
45
+ kind: "answer",
46
+ mode: "submit",
47
+ answers: {
48
+ questionId: { values: ["option-value"] },
49
+ },
50
+ },
51
+ });
52
+ ```
53
+
54
+ Answer shape:
55
+
56
+ ```ts
57
+ type PiAskRemoteAnswer = {
58
+ values?: string[];
59
+ customText?: string;
60
+ note?: string;
61
+ optionNotes?: Record<string, string>;
62
+ };
63
+ ```
64
+
65
+ Rules:
66
+
67
+ - `values` must match option `value`s from the started event
68
+ - keys in `answers` must match question ids
69
+ - labels and indices are recomputed by pi-ask
70
+ - a remote `answer` replaces the current answer set; stale UI answers are not merged
71
+ - `mode` defaults to `"submit"`; use `"elaborate"` to complete as an elaboration request
72
+
73
+ ## Cancel
74
+
75
+ ```ts
76
+ pi.events.emit("@eko24ive/pi-ask:submit", {
77
+ version: 1,
78
+ requestId: `bridge-${Date.now()}`,
79
+ flowId,
80
+ response: { kind: "cancel" },
81
+ });
82
+ ```
83
+
84
+ Cancel must be explicit. pi-ask does not infer cancel/approve/deny from labels or button names.
85
+
86
+ ## Submit result
87
+
88
+ After a submit request, pi-ask emits:
89
+
90
+ ```ts
91
+ type PiAskSubmitResultEvent =
92
+ | { version: 1; requestId: string; flowId: string; ok: true }
93
+ | {
94
+ version: 1;
95
+ requestId: string;
96
+ flowId: string;
97
+ ok: false;
98
+ error: "flow_not_found" | "invalid_request" | "invalid_answer";
99
+ message: string;
100
+ };
101
+ ```
102
+
103
+ Correlate by `requestId` and `flowId`. Do not depend on strict ordering between `submit-result` and `completed`.
104
+
105
+ ## Completed
106
+
107
+ Emitted when the flow resolves.
108
+
109
+ ```ts
110
+ type PiAskCompletedEvent = {
111
+ version: 1;
112
+ flowId: string;
113
+ toolCallId?: string;
114
+ source: "tool" | "answer" | "answer:again" | "ask:replay";
115
+ result: AskResult;
116
+ completedAt: number;
117
+ };
118
+ ```
119
+
120
+ ## Minimal bridge
121
+
122
+ ```ts
123
+ export default function piAskBridge(pi: any) {
124
+ pi.events.on("@eko24ive/pi-ask:started", (event: any) => {
125
+ const question = event.questions[0];
126
+ const option = question.options[0];
127
+
128
+ pi.events.emit("@eko24ive/pi-ask:submit", {
129
+ version: 1,
130
+ requestId: `bridge-${Date.now()}`,
131
+ flowId: event.flowId,
132
+ response: {
133
+ kind: "answer",
134
+ answers: {
135
+ [question.id]: { values: [option.value] },
136
+ },
137
+ },
138
+ });
139
+ });
140
+
141
+ pi.events.on("@eko24ive/pi-ask:submit-result", (event: any) => {
142
+ if (!event.ok) console.error(event.error, event.message);
143
+ });
144
+ }
145
+ ```
146
+
147
+ Third-party integrations own their own UI policy and mappings. For example, a bridge may map a button to `{ values: ["yes"] }`, but pi-ask will never guess that mapping from the label.
148
+
149
+ ## Local smoke test
150
+
151
+ Create a temporary bridge and run pi with only this repo extension plus the bridge:
152
+
153
+ ```bash
154
+ cat > /tmp/pi-ask-smoke.ts <<'EOF'
155
+ export default function smoke(pi: any) {
156
+ pi.events.on("@eko24ive/pi-ask:started", (event: any) => {
157
+ const q = event.questions[0];
158
+ setTimeout(() => {
159
+ pi.events.emit("@eko24ive/pi-ask:submit", {
160
+ version: 1,
161
+ requestId: `smoke-${Date.now()}`,
162
+ flowId: event.flowId,
163
+ response: { kind: "answer", answers: { [q.id]: { values: ["tool"] } } },
164
+ });
165
+ }, 2500);
166
+ });
167
+ }
168
+ EOF
169
+
170
+ pi \
171
+ --no-extensions \
172
+ --no-skills \
173
+ --no-prompt-templates \
174
+ --no-themes \
175
+ --no-context-files \
176
+ -e "$PWD/src/index.ts" \
177
+ -e /tmp/pi-ask-smoke.ts \
178
+ --skill "$PWD/skills/ask-user"
179
+ ```
180
+
181
+ Then ask Pi:
182
+
183
+ ```txt
184
+ Use ask_user. Title: pi-ask smoke. Ask one single-select question id tool with options tool and nope.
185
+ ```
186
+
187
+ The ask UI should open and auto-submit `tool` after about 2.5 seconds.
package/package.json ADDED
@@ -0,0 +1,130 @@
1
+ {
2
+ "name": "@geoqiao/pi-ask",
3
+ "version": "1.1.0",
4
+ "description": "Pi package that adds an interactive ask_user clarification tool.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/geoqiao/pi-ask.git"
10
+ },
11
+ "homepage": "https://github.com/geoqiao/pi-ask#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/geoqiao/pi-ask/issues"
14
+ },
15
+ "keywords": [
16
+ "pi-package",
17
+ "pi",
18
+ "pi-coding-agent",
19
+ "ask_user",
20
+ "clarification",
21
+ "terminal-ui"
22
+ ],
23
+ "files": [
24
+ "src/",
25
+ "skills/",
26
+ "docs/configuration.md",
27
+ "docs/contract.md",
28
+ "docs/remote-events.md",
29
+ "README.md",
30
+ "LICENSE",
31
+ "CHANGELOG.md"
32
+ ],
33
+ "scripts": {
34
+ "dev": "sh -c 'ROOT=\"$PWD\"; TARGET=\"${1:-.}\"; if [ \"$TARGET\" = \"--\" ]; then TARGET=\"${2:-.}\"; fi; cd \"$TARGET\" && pi --no-extensions --no-skills --no-prompt-templates --no-themes --no-context-files -e \"$ROOT/src/index.ts\" --skill \"$ROOT/skills/ask-user\"' --",
35
+ "test": "node --test tests/*.test.ts",
36
+ "typecheck": "tsc -p tsconfig.json",
37
+ "format": "biome format --write .",
38
+ "lint": "biome lint --write .",
39
+ "check": "ultracite check",
40
+ "check:ci": "biome ci .",
41
+ "fix": "ultracite fix",
42
+ "commit": "cz",
43
+ "commitlint": "commitlint",
44
+ "release": "semantic-release"
45
+ },
46
+ "pi": {
47
+ "extensions": [
48
+ "./src/index.ts"
49
+ ],
50
+ "skills": [
51
+ "./skills"
52
+ ]
53
+ },
54
+ "publishConfig": {
55
+ "access": "public",
56
+ "provenance": false
57
+ },
58
+ "release": {
59
+ "repositoryUrl": "https://github.com/geoqiao/pi-ask.git",
60
+ "branches": [
61
+ "main"
62
+ ],
63
+ "plugins": [
64
+ "@semantic-release/commit-analyzer",
65
+ "@semantic-release/release-notes-generator",
66
+ [
67
+ "@semantic-release/changelog",
68
+ {
69
+ "changelogFile": "CHANGELOG.md"
70
+ }
71
+ ],
72
+ "@semantic-release/npm",
73
+ "@semantic-release/github",
74
+ [
75
+ "@semantic-release/git",
76
+ {
77
+ "assets": [
78
+ "package.json",
79
+ "CHANGELOG.md"
80
+ ],
81
+ "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
82
+ }
83
+ ]
84
+ ]
85
+ },
86
+ "peerDependencies": {
87
+ "@earendil-works/pi-coding-agent": "*",
88
+ "@earendil-works/pi-tui": "*",
89
+ "typebox": "*",
90
+ "@earendil-works/pi-ai": "*"
91
+ },
92
+ "devDependencies": {
93
+ "@biomejs/biome": "2.4.12",
94
+ "@commitlint/cli": "^19.8.1",
95
+ "@commitlint/config-conventional": "^19.8.1",
96
+ "@earendil-works/pi-ai": "0.79.9",
97
+ "@earendil-works/pi-coding-agent": "0.79.9",
98
+ "@earendil-works/pi-tui": "0.79.9",
99
+ "@semantic-release/changelog": "^6.0.3",
100
+ "@semantic-release/commit-analyzer": "^13.0.1",
101
+ "@semantic-release/git": "^10.0.1",
102
+ "@semantic-release/github": "^12.0.6",
103
+ "@semantic-release/npm": "^13.1.5",
104
+ "@semantic-release/release-notes-generator": "^14.1.0",
105
+ "typebox": "1.1.38",
106
+ "@types/node": "25.6.0",
107
+ "commitizen": "^4.3.1",
108
+ "cz-conventional-changelog": "^3.3.0",
109
+ "lefthook": "^2.1.6",
110
+ "semantic-release": "^25.0.3",
111
+ "typescript": "6.0.2",
112
+ "ultracite": "7.6.0"
113
+ },
114
+ "config": {
115
+ "commitizen": {
116
+ "path": "./node_modules/cz-conventional-changelog"
117
+ }
118
+ },
119
+ "pnpm": {
120
+ "overrides": {
121
+ "@earendil-works/pi-agent-core": "0.79.9",
122
+ "@earendil-works/pi-ai": "0.79.9",
123
+ "@earendil-works/pi-tui": "0.79.9",
124
+ "@earendil-works/pi-coding-agent": "0.79.9"
125
+ },
126
+ "ignoredBuiltDependencies": [
127
+ "lefthook"
128
+ ]
129
+ }
130
+ }