@fadhelhaidar/pi-ask-user 0.0.0-stage → 1.0.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.
@@ -0,0 +1,127 @@
1
+ # Tool schema
2
+
3
+ The complete programmatic surface of `ask_user_question`: what the model sends, what
4
+ validation rejects, what comes back, and the event other extensions can listen to.
5
+
6
+ ## Parameters
7
+
8
+ ```ts
9
+ ask_user_question({
10
+ questions: [
11
+ {
12
+ question: string, // full question text, ends with "?"
13
+ header: string, // chip label, max 16 chars
14
+ options: [
15
+ {
16
+ label: string, // 1-5 words, max 60 chars
17
+ description: string, // what the choice means / its trade-off
18
+ preview?: string, // markdown rendered next to the options
19
+ },
20
+ // … 2-4 options total
21
+ ],
22
+ multiSelect?: boolean, // default false
23
+ },
24
+ // … 1-4 questions total
25
+ ]
26
+ })
27
+ ```
28
+
29
+ ### Limits
30
+
31
+ | Field | Constraint | Enforced by |
32
+ | --- | --- | --- |
33
+ | `questions` | 1-4 entries | TypeBox schema + `validateQuestionnaire` |
34
+ | `questions[].header` | max 16 characters | TypeBox schema only |
35
+ | `questions[].options` | 2-4 entries | TypeBox schema (both bounds) + `validateQuestionnaire` (minimum only) |
36
+ | `options[].label` | max 60 characters | TypeBox schema only |
37
+ | `options[].preview` | single-select questions only | tool description (multi-select tabs render checkbox rows) |
38
+
39
+ The two `maxLength` limits are checked by the parameter schema before `execute` runs;
40
+ the runtime validator does not re-check them.
41
+
42
+ ### Reserved option labels
43
+
44
+ Authoring any of `"Other"`, `"Type something."`, or `"Next"` as an option label is
45
+ rejected with `reserved_label`. The last two are the runtime sentinel rows the dialog
46
+ appends itself; `"Other"` is reserved because models are conditioned to reach for it.
47
+ Reservation is unconditional — a single-select question rejects `"Next"` even though
48
+ that row is never appended there.
49
+
50
+ ## Validation errors
51
+
52
+ Every rejection returns `cancelled: true`, an empty `answers` array, and an `error`
53
+ code. The `content[0].text` string is written for the model, not for a log.
54
+
55
+ | `error` | Cause |
56
+ | --- | --- |
57
+ | `no_questions` | `questions` was empty |
58
+ | `too_many_questions` | more than 4 questions in one call |
59
+ | `duplicate_question` | two questions with identical text |
60
+ | `empty_options` | a question carried fewer than 2 options |
61
+ | `reserved_label` | an option used a reserved label |
62
+ | `duplicate_option_label` | two options in one question share a label |
63
+ | `no_ui` | the run has no UI (`ctx.hasUI === false`) |
64
+ | `no_custom_ui` | the host cannot render custom UI and exposes no `select`/`input` dialogs |
65
+ | `session_load_failed` | the dialog module failed to import (dependencies changed on disk mid-session) |
66
+ | `stale_module_cache` | the loader cached a broken module after an earlier failed import; needs a Pi restart |
67
+
68
+ `reserved_label` short-circuits before `duplicate_option_label`.
69
+
70
+ ## Result
71
+
72
+ ```ts
73
+ {
74
+ content: [{ type: "text", text: string }], // envelope prose, or the decline message
75
+ details: {
76
+ answers: Array<{
77
+ questionIndex: number,
78
+ question: string,
79
+ kind: "option" | "custom" | "multi",
80
+ answer: string | null, // option label, typed text, or null for multi
81
+ selected?: string[], // chosen labels, multi-select only
82
+ notes?: string, // free-text note, when you wrote one
83
+ preview?: string, // echoed back when the chosen option carried a preview
84
+ }>,
85
+ cancelled: boolean,
86
+ globalNote?: string, // Submit-tab note; present even when cancelled is true
87
+ error?: QuestionnaireError, // one of the codes above
88
+ }
89
+ }
90
+ ```
91
+
92
+ ### Envelope text
93
+
94
+ On success the text reads `User has answered your questions: "<question>"="<answer>". …
95
+ You can now continue with the user's answers in mind.` A chosen option's `preview` is
96
+ appended as `selected preview: <markdown>`, a per-question note as `user notes: <text>`,
97
+ and the Submit tab's global note as a trailing `global note: <text>` segment. A global
98
+ note alone still yields the answered envelope — it counts as an answer even when every
99
+ question is blank.
100
+
101
+ Cancelling, and any result with neither answer segments nor a global note, both collapse
102
+ to the single string `User declined to answer questions` so the model sees one canonical
103
+ signal. Partial submission is allowed: unanswered questions simply contribute no segment.
104
+ A cancelled result always reads as the decline in text; its note, if any, survives only
105
+ in `details.globalNote`.
106
+
107
+ ## Event contract
108
+
109
+ The package publishes one event on Pi's event bus, emitted after validation passes and
110
+ before the dialog is shown. Import it from the `/events` subpath:
111
+
112
+ ```ts
113
+ import { ASK_USER_PROMPT_EVENT, type AskUserPromptEventPayload } from "@juicesharp/rpiv-ask-user-question/events";
114
+
115
+ pi.events.on(ASK_USER_PROMPT_EVENT, (payload: AskUserPromptEventPayload) => {
116
+ // payload.questions[].{ question, header, multiSelect, options[] }
117
+ // payload.questions[].options[].{ label, description, hasPreview }
118
+ });
119
+ ```
120
+
121
+ The channel name is `rpiv:ask-user:prompt`. Preview *content* is deliberately not shipped
122
+ in the payload — only `hasPreview: boolean` — so listeners forwarding the event across a
123
+ process or network boundary stay cheap.
124
+
125
+ Stability policy for the `rpiv:*` namespace: channel names are immutable, payload changes
126
+ are append-only and always optional, payloads stay JSON-safe, and any breaking change ships
127
+ as a new channel (e.g. `rpiv:ask-user:prompt.v2`) rather than a version field.
package/package.json CHANGED
@@ -1,6 +1,66 @@
1
1
  {
2
2
  "name": "@fadhelhaidar/pi-ask-user",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.1",
4
+ "description": "Standalone Pi questionnaire with combined multi-select and custom answers, Enter-only toggling, and preserved drafts.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "FadhelHaidar",
8
+ "keywords": [
9
+ "pi-package",
10
+ "pi-extension"
11
+ ],
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/FadhelHaidar/pi-ask-user.git"
15
+ },
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ },
21
+ "./events": {
22
+ "types": "./dist/events.d.ts",
23
+ "default": "./dist/events.js"
24
+ }
25
+ },
26
+ "scripts": {
27
+ "build": "bun scripts/build.ts",
28
+ "build:check": "bun scripts/build.ts --check",
29
+ "test": "vitest run",
30
+ "typecheck": "tsc -p tests/types/tsconfig.json"
31
+ },
32
+ "files": [
33
+ "dist/",
34
+ "LICENSE",
35
+ "docs/",
36
+ "!docs/*.png",
37
+ "!docs/*.jpg",
38
+ "!docs/*.svg",
39
+ "README.md",
40
+ "NOTICE.md",
41
+ "!**/*.test.ts"
42
+ ],
43
+ "pi": {
44
+ "extensions": [
45
+ "./dist/index.js"
46
+ ]
47
+ },
48
+ "peerDependencies": {
49
+ "@earendil-works/pi-coding-agent": "*",
50
+ "@earendil-works/pi-tui": "*",
51
+ "typebox": "*",
52
+ "@juicesharp/rpiv-i18n": "*"
53
+ },
54
+ "peerDependenciesMeta": {
55
+ "@juicesharp/rpiv-i18n": {
56
+ "optional": true
57
+ }
58
+ },
59
+ "devDependencies": {
60
+ "@earendil-works/pi-coding-agent": "1.0.2",
61
+ "@earendil-works/pi-tui": "1.0.2",
62
+ "@juicesharp/rpiv-i18n": "2.12.0",
63
+ "typescript": "5.9.3",
64
+ "vitest": "4.1.10"
65
+ }
66
+ }