@pi-unipi/ask-user 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/README.md +99 -0
- package/ask-ui.ts +477 -0
- package/commands.ts +50 -0
- package/config.ts +133 -0
- package/index.ts +44 -0
- package/package.json +52 -0
- package/settings-tui.ts +164 -0
- package/skills/ask-user/SKILL.md +100 -0
- package/tools.ts +291 -0
- package/types.ts +48 -0
package/config.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/ask-user — Config system
|
|
3
|
+
*
|
|
4
|
+
* Reads/writes ask-user settings in ~/.pi/agent/settings.json
|
|
5
|
+
* under the "unipi.askUser" key.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
|
|
12
|
+
/** Ask-user settings */
|
|
13
|
+
export interface AskUserSettings {
|
|
14
|
+
/** Whether the ask_user tool is enabled */
|
|
15
|
+
enabled: boolean;
|
|
16
|
+
/** Allowed question formats */
|
|
17
|
+
allowedFormats: {
|
|
18
|
+
/** Allow single-select questions */
|
|
19
|
+
singleSelect: boolean;
|
|
20
|
+
/** Allow multi-select questions */
|
|
21
|
+
multiSelect: boolean;
|
|
22
|
+
/** Allow freeform text input */
|
|
23
|
+
freeform: boolean;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Default settings */
|
|
28
|
+
export const DEFAULT_SETTINGS: AskUserSettings = {
|
|
29
|
+
enabled: true,
|
|
30
|
+
allowedFormats: {
|
|
31
|
+
singleSelect: true,
|
|
32
|
+
multiSelect: true,
|
|
33
|
+
freeform: true,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Settings path */
|
|
38
|
+
const SETTINGS_PATH = join(homedir(), ".pi", "agent", "settings.json");
|
|
39
|
+
|
|
40
|
+
/** Settings key within settings.json */
|
|
41
|
+
const SETTINGS_KEY = "unipi";
|
|
42
|
+
|
|
43
|
+
/** Cached settings */
|
|
44
|
+
let cachedSettings: AskUserSettings | null = null;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Check if value is a plain object.
|
|
48
|
+
*/
|
|
49
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
50
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Read the full settings file.
|
|
55
|
+
*/
|
|
56
|
+
function readSettingsFile(): Record<string, unknown> {
|
|
57
|
+
if (!existsSync(SETTINGS_PATH)) return {};
|
|
58
|
+
try {
|
|
59
|
+
const parsed = JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
|
|
60
|
+
return isRecord(parsed) ? parsed : {};
|
|
61
|
+
} catch {
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Write the full settings file.
|
|
68
|
+
*/
|
|
69
|
+
function writeSettingsFile(data: Record<string, unknown>): void {
|
|
70
|
+
const dir = require("node:path").dirname(SETTINGS_PATH);
|
|
71
|
+
if (!existsSync(dir)) {
|
|
72
|
+
require("node:fs").mkdirSync(dir, { recursive: true });
|
|
73
|
+
}
|
|
74
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get ask-user settings from settings.json.
|
|
79
|
+
*/
|
|
80
|
+
export function getAskUserSettings(): AskUserSettings {
|
|
81
|
+
if (cachedSettings) return cachedSettings;
|
|
82
|
+
|
|
83
|
+
const settings = readSettingsFile();
|
|
84
|
+
const unipi = settings[SETTINGS_KEY];
|
|
85
|
+
|
|
86
|
+
if (!isRecord(unipi) || !isRecord(unipi.askUser)) {
|
|
87
|
+
cachedSettings = { ...DEFAULT_SETTINGS };
|
|
88
|
+
return cachedSettings;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const askUser = unipi.askUser as Record<string, unknown>;
|
|
92
|
+
|
|
93
|
+
const enabled = typeof askUser.enabled === "boolean" ? askUser.enabled : DEFAULT_SETTINGS.enabled;
|
|
94
|
+
|
|
95
|
+
let allowedFormats = DEFAULT_SETTINGS.allowedFormats;
|
|
96
|
+
if (isRecord(askUser.allowedFormats)) {
|
|
97
|
+
const fmt = askUser.allowedFormats;
|
|
98
|
+
allowedFormats = {
|
|
99
|
+
singleSelect: typeof fmt.singleSelect === "boolean" ? fmt.singleSelect : DEFAULT_SETTINGS.allowedFormats.singleSelect,
|
|
100
|
+
multiSelect: typeof fmt.multiSelect === "boolean" ? fmt.multiSelect : DEFAULT_SETTINGS.allowedFormats.multiSelect,
|
|
101
|
+
freeform: typeof fmt.freeform === "boolean" ? fmt.freeform : DEFAULT_SETTINGS.allowedFormats.freeform,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
cachedSettings = { enabled, allowedFormats };
|
|
106
|
+
return cachedSettings;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Save ask-user settings to settings.json.
|
|
111
|
+
*/
|
|
112
|
+
export function saveAskUserSettings(settings: AskUserSettings): void {
|
|
113
|
+
const file = readSettingsFile();
|
|
114
|
+
|
|
115
|
+
if (!isRecord(file[SETTINGS_KEY])) {
|
|
116
|
+
file[SETTINGS_KEY] = {};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
(file[SETTINGS_KEY] as Record<string, unknown>).askUser = {
|
|
120
|
+
enabled: settings.enabled,
|
|
121
|
+
allowedFormats: settings.allowedFormats,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
writeSettingsFile(file);
|
|
125
|
+
cachedSettings = settings;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Clear cached settings (for testing or reload).
|
|
130
|
+
*/
|
|
131
|
+
export function clearSettingsCache(): void {
|
|
132
|
+
cachedSettings = null;
|
|
133
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/ask-user — Extension entry
|
|
3
|
+
*
|
|
4
|
+
* Provides ask_user tool for structured user input with single-select,
|
|
5
|
+
* multi-select, and freeform modes. Includes bundled skill for agent guidance.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import {
|
|
10
|
+
UNIPI_EVENTS,
|
|
11
|
+
MODULES,
|
|
12
|
+
ASK_USER_TOOLS,
|
|
13
|
+
emitEvent,
|
|
14
|
+
getPackageVersion,
|
|
15
|
+
} from "@pi-unipi/core";
|
|
16
|
+
import { registerAskUserTools } from "./tools.js";
|
|
17
|
+
import { registerAskUserCommands } from "./commands.js";
|
|
18
|
+
|
|
19
|
+
/** Package version */
|
|
20
|
+
const VERSION = getPackageVersion(new URL(".", import.meta.url).pathname);
|
|
21
|
+
|
|
22
|
+
export default function (pi: ExtensionAPI) {
|
|
23
|
+
// Register skills directory
|
|
24
|
+
const skillsDir = new URL("./skills", import.meta.url).pathname;
|
|
25
|
+
pi.on("resources_discover", async () => {
|
|
26
|
+
return {
|
|
27
|
+
skillPaths: [skillsDir],
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Register tools and commands
|
|
32
|
+
registerAskUserTools(pi);
|
|
33
|
+
registerAskUserCommands(pi);
|
|
34
|
+
|
|
35
|
+
// Session lifecycle — announce module
|
|
36
|
+
pi.on("session_start", async () => {
|
|
37
|
+
emitEvent(pi, UNIPI_EVENTS.MODULE_READY, {
|
|
38
|
+
name: MODULES.ASK_USER,
|
|
39
|
+
version: VERSION,
|
|
40
|
+
commands: ["unipi:ask-user-settings"],
|
|
41
|
+
tools: [ASK_USER_TOOLS.ASK],
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pi-unipi/ask-user",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Structured user input tool for Pi coding agent — single-select, multi-select, freeform",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Neuron Mr White",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Neuron-Mr-White/unipi.git",
|
|
11
|
+
"directory": "packages/ask-user"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"pi-package",
|
|
15
|
+
"pi-extension",
|
|
16
|
+
"pi-coding-agent",
|
|
17
|
+
"unipi",
|
|
18
|
+
"ask-user",
|
|
19
|
+
"interactive",
|
|
20
|
+
"user-input"
|
|
21
|
+
],
|
|
22
|
+
"files": [
|
|
23
|
+
"index.ts",
|
|
24
|
+
"types.ts",
|
|
25
|
+
"tools.ts",
|
|
26
|
+
"ask-ui.ts",
|
|
27
|
+
"commands.ts",
|
|
28
|
+
"config.ts",
|
|
29
|
+
"settings-tui.ts",
|
|
30
|
+
"skills/**/*",
|
|
31
|
+
"README.md"
|
|
32
|
+
],
|
|
33
|
+
"pi": {
|
|
34
|
+
"extensions": [
|
|
35
|
+
"index.ts"
|
|
36
|
+
],
|
|
37
|
+
"skills": [
|
|
38
|
+
"skills"
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@pi-unipi/core": "*"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
49
|
+
"@mariozechner/pi-tui": "*",
|
|
50
|
+
"@sinclair/typebox": "*"
|
|
51
|
+
}
|
|
52
|
+
}
|
package/settings-tui.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/ask-user — Settings TUI Component
|
|
3
|
+
*
|
|
4
|
+
* Interactive settings editor for ask_user tool.
|
|
5
|
+
* Allows enabling/disabling the tool and configuring allowed question formats.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Component } from "@mariozechner/pi-tui";
|
|
9
|
+
import { truncateToWidth } from "@mariozechner/pi-tui";
|
|
10
|
+
import { getAskUserSettings, saveAskUserSettings, type AskUserSettings } from "./config.js";
|
|
11
|
+
|
|
12
|
+
/** ANSI escape codes */
|
|
13
|
+
const ansi = {
|
|
14
|
+
reset: "\x1b[0m",
|
|
15
|
+
bold: "\x1b[1m",
|
|
16
|
+
dim: "\x1b[2m",
|
|
17
|
+
// Colors
|
|
18
|
+
cyan: "\x1b[36m",
|
|
19
|
+
green: "\x1b[32m",
|
|
20
|
+
yellow: "\x1b[33m",
|
|
21
|
+
red: "\x1b[31m",
|
|
22
|
+
gray: "\x1b[90m",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Toggle symbols */
|
|
26
|
+
const TOGGLE_ON = `${ansi.green}●${ansi.reset}`;
|
|
27
|
+
const TOGGLE_OFF = `${ansi.dim}○${ansi.reset}`;
|
|
28
|
+
|
|
29
|
+
/** Setting items */
|
|
30
|
+
interface SettingItem {
|
|
31
|
+
key: string;
|
|
32
|
+
label: string;
|
|
33
|
+
description: string;
|
|
34
|
+
getValue: (settings: AskUserSettings) => boolean;
|
|
35
|
+
setValue: (settings: AskUserSettings, value: boolean) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** List of configurable settings */
|
|
39
|
+
const SETTINGS: SettingItem[] = [
|
|
40
|
+
{
|
|
41
|
+
key: "enabled",
|
|
42
|
+
label: "Enable ask_user tool",
|
|
43
|
+
description: "Allow the agent to ask structured questions",
|
|
44
|
+
getValue: (s) => s.enabled,
|
|
45
|
+
setValue: (s, v) => (s.enabled = v),
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
key: "singleSelect",
|
|
49
|
+
label: "Allow single-select",
|
|
50
|
+
description: "Questions with one correct answer",
|
|
51
|
+
getValue: (s) => s.allowedFormats.singleSelect,
|
|
52
|
+
setValue: (s, v) => (s.allowedFormats.singleSelect = v),
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
key: "multiSelect",
|
|
56
|
+
label: "Allow multi-select",
|
|
57
|
+
description: "Questions with multiple correct answers",
|
|
58
|
+
getValue: (s) => s.allowedFormats.multiSelect,
|
|
59
|
+
setValue: (s, v) => (s.allowedFormats.multiSelect = v),
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
key: "freeform",
|
|
63
|
+
label: "Allow freeform text",
|
|
64
|
+
description: "Open-ended questions with text input",
|
|
65
|
+
getValue: (s) => s.allowedFormats.freeform,
|
|
66
|
+
setValue: (s, v) => (s.allowedFormats.freeform = v),
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Settings overlay component.
|
|
72
|
+
*/
|
|
73
|
+
export class AskUserSettingsOverlay implements Component {
|
|
74
|
+
private settings: AskUserSettings;
|
|
75
|
+
private selectedIndex = 0;
|
|
76
|
+
/** Callback when overlay should close */
|
|
77
|
+
onClose?: () => void;
|
|
78
|
+
|
|
79
|
+
constructor() {
|
|
80
|
+
this.settings = getAskUserSettings();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Invalidate cached render state.
|
|
85
|
+
*/
|
|
86
|
+
invalidate(): void {
|
|
87
|
+
// No cached state
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Handle keyboard input.
|
|
92
|
+
*/
|
|
93
|
+
handleInput(data: string): void {
|
|
94
|
+
switch (data) {
|
|
95
|
+
case "\x1b[A": // Up
|
|
96
|
+
case "k":
|
|
97
|
+
this.selectedIndex = (this.selectedIndex - 1 + SETTINGS.length) % SETTINGS.length;
|
|
98
|
+
break;
|
|
99
|
+
case "\x1b[B": // Down
|
|
100
|
+
case "j":
|
|
101
|
+
this.selectedIndex = (this.selectedIndex + 1) % SETTINGS.length;
|
|
102
|
+
break;
|
|
103
|
+
case " ": // Space - toggle
|
|
104
|
+
this.toggleSetting(SETTINGS[this.selectedIndex].key);
|
|
105
|
+
break;
|
|
106
|
+
case "\r": // Enter - save and close
|
|
107
|
+
this.save();
|
|
108
|
+
this.onClose?.();
|
|
109
|
+
break;
|
|
110
|
+
case "\x1b": // Escape - close without saving
|
|
111
|
+
this.onClose?.();
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Toggle a setting by key.
|
|
118
|
+
*/
|
|
119
|
+
private toggleSetting(key: string): void {
|
|
120
|
+
const item = SETTINGS.find((s) => s.key === key);
|
|
121
|
+
if (!item) return;
|
|
122
|
+
const current = item.getValue(this.settings);
|
|
123
|
+
item.setValue(this.settings, !current);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Save settings to disk.
|
|
128
|
+
*/
|
|
129
|
+
private save(): void {
|
|
130
|
+
saveAskUserSettings(this.settings);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Render the overlay.
|
|
135
|
+
*/
|
|
136
|
+
render(width: number): string[] {
|
|
137
|
+
const lines: string[] = [];
|
|
138
|
+
const add = (s: string) => lines.push(truncateToWidth(s, width));
|
|
139
|
+
|
|
140
|
+
// Header
|
|
141
|
+
add(`${ansi.bold}${ansi.cyan}Ask User Settings${ansi.reset}`);
|
|
142
|
+
add(`${ansi.dim}Configure how the agent can ask you questions${ansi.reset}`);
|
|
143
|
+
add("");
|
|
144
|
+
|
|
145
|
+
// Settings list
|
|
146
|
+
for (let i = 0; i < SETTINGS.length; i++) {
|
|
147
|
+
const item = SETTINGS[i];
|
|
148
|
+
const isSelected = i === this.selectedIndex;
|
|
149
|
+
const value = item.getValue(this.settings);
|
|
150
|
+
const toggle = value ? TOGGLE_ON : TOGGLE_OFF;
|
|
151
|
+
const labelColor = isSelected ? ansi.bold : ansi.dim;
|
|
152
|
+
const descColor = ansi.gray;
|
|
153
|
+
|
|
154
|
+
add(`${isSelected ? ansi.cyan + "▸" + ansi.reset : " "} ${toggle} ${labelColor}${item.label}${ansi.reset}`);
|
|
155
|
+
add(` ${descColor}${item.description}${ansi.reset}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Footer
|
|
159
|
+
add("");
|
|
160
|
+
add(`${ansi.dim}↑↓ navigate • Space toggle • Enter save • Esc cancel${ansi.reset}`);
|
|
161
|
+
|
|
162
|
+
return lines;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ask-user
|
|
3
|
+
description: >
|
|
4
|
+
Interactive decision-gating tool for structured user input.
|
|
5
|
+
Use ask_user when you need user confirmation, preferences, or decisions
|
|
6
|
+
before proceeding with high-impact or ambiguous choices.
|
|
7
|
+
allowed-tools:
|
|
8
|
+
- ask_user
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Ask User
|
|
12
|
+
|
|
13
|
+
Use the `ask_user` tool to collect structured input from the user.
|
|
14
|
+
|
|
15
|
+
## When to use ask_user
|
|
16
|
+
|
|
17
|
+
- Architectural trade-offs with high impact
|
|
18
|
+
- Requirements are ambiguous or conflicting
|
|
19
|
+
- Assumptions would materially change implementation
|
|
20
|
+
- User preferences needed (style, approach, priority)
|
|
21
|
+
- Confirming before destructive operations
|
|
22
|
+
|
|
23
|
+
## Decision Handshake Flow
|
|
24
|
+
|
|
25
|
+
1. Gather evidence and summarize context
|
|
26
|
+
2. Ask ONE focused question via `ask_user`
|
|
27
|
+
3. Wait for explicit user choice
|
|
28
|
+
4. Confirm the decision, then proceed
|
|
29
|
+
|
|
30
|
+
## Parameters
|
|
31
|
+
|
|
32
|
+
| Parameter | Type | Default | Description |
|
|
33
|
+
|-----------|------|---------|-------------|
|
|
34
|
+
| `question` | string | required | The question to ask |
|
|
35
|
+
| `context` | string? | — | Additional context shown before question |
|
|
36
|
+
| `options` | array? | [] | Multiple-choice options with labels, descriptions, values |
|
|
37
|
+
| `allowMultiple` | boolean? | false | Enable multi-select |
|
|
38
|
+
| `allowFreeform` | boolean? | true | Add "Custom response" checkable option |
|
|
39
|
+
| `timeout` | number? | — | Auto-dismiss after N ms |
|
|
40
|
+
|
|
41
|
+
## Examples
|
|
42
|
+
|
|
43
|
+
Single choice:
|
|
44
|
+
```
|
|
45
|
+
ask_user({
|
|
46
|
+
question: "Which database should we use?",
|
|
47
|
+
options: [
|
|
48
|
+
{ label: "PostgreSQL", description: "Reliable, feature-rich" },
|
|
49
|
+
{ label: "SQLite", description: "Simple, serverless" }
|
|
50
|
+
]
|
|
51
|
+
})
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Multi-select:
|
|
55
|
+
```
|
|
56
|
+
ask_user({
|
|
57
|
+
question: "Which features to implement?",
|
|
58
|
+
options: [
|
|
59
|
+
{ label: "Auth", value: "auth" },
|
|
60
|
+
{ label: "Cache", value: "cache" },
|
|
61
|
+
{ label: "Logging", value: "logging" }
|
|
62
|
+
],
|
|
63
|
+
allowMultiple: true
|
|
64
|
+
})
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
With context:
|
|
68
|
+
```
|
|
69
|
+
ask_user({
|
|
70
|
+
question: "Which approach?",
|
|
71
|
+
context: "Current bottleneck: network I/O. Goal: reduce latency.",
|
|
72
|
+
options: [
|
|
73
|
+
{ label: "Cache-first" },
|
|
74
|
+
{ label: "DB-first" }
|
|
75
|
+
]
|
|
76
|
+
})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Freeform only:
|
|
80
|
+
```
|
|
81
|
+
ask_user({
|
|
82
|
+
question: "What should we name this module?",
|
|
83
|
+
options: [],
|
|
84
|
+
allowFreeform: true
|
|
85
|
+
})
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Combined (multi-select + freeform):
|
|
89
|
+
```
|
|
90
|
+
ask_user({
|
|
91
|
+
question: "Which features and what custom feature?",
|
|
92
|
+
options: [
|
|
93
|
+
{ label: "Auth", value: "auth" },
|
|
94
|
+
{ label: "Cache", value: "cache" }
|
|
95
|
+
],
|
|
96
|
+
allowMultiple: true,
|
|
97
|
+
allowFreeform: true
|
|
98
|
+
})
|
|
99
|
+
```
|
|
100
|
+
User can check "Auth", "Cache", and "Custom response" to type additional features.
|