inquirex-tty 0.5.0 → 0.9.4
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.
- checksums.yaml +4 -4
- data/.github_changelog_generator +5 -0
- data/CHANGELOG.md +36 -3
- data/README.md +91 -106
- data/Rakefile +5 -2
- data/docs/badges/coverage_badge.svg +2 -2
- data/examples/10_real_tax_preparer.rb +40 -2
- data/examples/burning-man.rb +52 -0
- data/exe/inqt +1 -0
- data/justfile +63 -11
- data/lib/inquirex/tty/commands/export.rb +4 -2
- data/lib/inquirex/tty/commands/graph.rb +6 -4
- data/lib/inquirex/tty/commands/run.rb +51 -3
- data/lib/inquirex/tty/commands/validate.rb +4 -4
- data/lib/inquirex/tty/commands/version.rb +3 -1
- data/lib/inquirex/tty/flow_loader.rb +6 -0
- data/lib/inquirex/tty/output_path.rb +5 -0
- data/lib/inquirex/tty/renderer.rb +33 -3
- data/lib/inquirex/tty/ui_helper.rb +22 -0
- data/lib/inquirex/tty/version.rb +1 -1
- data/lib/inquirex/tty.rb +8 -0
- data/sig/inquirex/tty.rbs +1 -0
- metadata +26 -8
- /data/exe/{inquirex → inquirex-tty} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b0f91188b8315da187113b379d8a265d91b231c8e6a4a8309dc7cc41febafe9c
|
|
4
|
+
data.tar.gz: f8b109d024b9cd492e3f71f1e04f5cd2fcefa6673e5debc47c19bf9b8df87fdc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ad0e8b2efc4eac5ee7c45c1615f8d1e3910ab9a66c608c73651e8eb087e38761ae8e68014bc8c6ed119369e9c4635c97a7f3ec3cbff8319f9181750330d9c3d1
|
|
7
|
+
data.tar.gz: 922fe5ee727a6392ae73ce6d893a4ab99610dffef64c918ad8e4cc74dabae103e60644d587d94670be506d309823ab755a4e7c20d5ca170ece91edea86512fee
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
|
-
|
|
1
|
+
# Changelog
|
|
2
2
|
|
|
3
|
-
## [
|
|
3
|
+
## [Unreleased](https://github.com/inquirex/inquirex-tty/tree/HEAD)
|
|
4
4
|
|
|
5
|
-
-
|
|
5
|
+
[Full Changelog](https://github.com/inquirex/inquirex-tty/compare/v0.6.0...HEAD)
|
|
6
|
+
|
|
7
|
+
**Merged pull requests:**
|
|
8
|
+
|
|
9
|
+
- Fix CI: broken Gemfile path override, RuboCop scanning vendor/ [\#9](https://github.com/inquirex/inquirex-tty/pull/9) ([kigster](https://github.com/kigster))
|
|
10
|
+
|
|
11
|
+
## [v0.6.0](https://github.com/inquirex/inquirex-tty/tree/v0.6.0) (2026-07-20)
|
|
12
|
+
|
|
13
|
+
[Full Changelog](https://github.com/inquirex/inquirex-tty/compare/v0.5.0...v0.6.0)
|
|
14
|
+
|
|
15
|
+
**Merged pull requests:**
|
|
16
|
+
|
|
17
|
+
- Document lib in YARD style to 100% coverage [\#8](https://github.com/inquirex/inquirex-tty/pull/8) ([kigster](https://github.com/kigster))
|
|
18
|
+
- Use $stderr.puts for CLI messages, not warn [\#7](https://github.com/inquirex/inquirex-tty/pull/7) ([kigster](https://github.com/kigster))
|
|
19
|
+
|
|
20
|
+
## [v0.5.0](https://github.com/inquirex/inquirex-tty/tree/v0.5.0) (2026-07-16)
|
|
21
|
+
|
|
22
|
+
[Full Changelog](https://github.com/inquirex/inquirex-tty/compare/v0.4.0...v0.5.0)
|
|
23
|
+
|
|
24
|
+
## [v0.4.0](https://github.com/inquirex/inquirex-tty/tree/v0.4.0) (2026-07-14)
|
|
25
|
+
|
|
26
|
+
[Full Changelog](https://github.com/inquirex/inquirex-tty/compare/971751bdfcbde3f1fd694cfb38d00bc3205b3e12...v0.4.0)
|
|
27
|
+
|
|
28
|
+
**Merged pull requests:**
|
|
29
|
+
|
|
30
|
+
- Raise inquirex-tty test coverage from 79% to 99% [\#5](https://github.com/inquirex/inquirex-tty/pull/5) ([kigster](https://github.com/kigster))
|
|
31
|
+
- Update bundled dependencies [\#4](https://github.com/inquirex/inquirex-tty/pull/4) ([kigster](https://github.com/kigster))
|
|
32
|
+
- Integrate inquirex-llm, drop inquirex-ui dep, add LLM-assisted tax example [\#3](https://github.com/inquirex/inquirex-tty/pull/3) ([kigster](https://github.com/kigster))
|
|
33
|
+
- Add inquirex export command and shared OutputPath helper [\#2](https://github.com/inquirex/inquirex-tty/pull/2) ([kigster](https://github.com/kigster))
|
|
34
|
+
- Simplify graph command: merge open-graph, add "both" format [\#1](https://github.com/inquirex/inquirex-tty/pull/1) ([kigster](https://github.com/kigster))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
|
data/README.md
CHANGED
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
Terminal adapter for the [Inquirex](https://github.com/inquirex/inquirex) questionnaire engine. Renders flow definitions as interactive ANSI terminal wizards using [tty-prompt](https://github.com/piotrmurach/tty-prompt), with ASCII-art headers, styled boxes, and automatic widget selection based on data types.
|
|
6
6
|
|
|
7
|
-
Ships as a CLI (`inquirex`) with commands to run flows interactively, validate definitions, and export Mermaid diagrams.
|
|
7
|
+
Ships as a CLI (`inquirex-tty`) with commands to run flows interactively, validate definitions, and export Mermaid diagrams.
|
|
8
|
+
|
|
9
|
+
## Inquirex CLI Shorthand Alias
|
|
10
|
+
|
|
11
|
+
This gem also installs a shorter command `inqt` which you can run in place of `inquirex-tty`.
|
|
8
12
|
|
|
9
13
|
## Installation
|
|
10
14
|
|
|
@@ -12,7 +16,7 @@ Ships as a CLI (`inquirex`) with commands to run flows interactively, validate d
|
|
|
12
16
|
gem "inquirex-tty"
|
|
13
17
|
```
|
|
14
18
|
|
|
15
|
-
The gem installs an `inquirex` executable.
|
|
19
|
+
The gem installs an `inquirex-tty` executable.
|
|
16
20
|
|
|
17
21
|
## Quick Start
|
|
18
22
|
|
|
@@ -47,30 +51,30 @@ end
|
|
|
47
51
|
Run it:
|
|
48
52
|
|
|
49
53
|
```bash
|
|
50
|
-
|
|
54
|
+
inqt run my_flow.rb
|
|
51
55
|
```
|
|
52
56
|
|
|
53
57
|
The CLI walks the user through each step, selecting the appropriate TTY widget for each data type, and prints collected answers as JSON when the flow completes.
|
|
54
58
|
|
|
55
59
|
## CLI Commands
|
|
56
60
|
|
|
57
|
-
### `inquirex run <flow_file>`
|
|
61
|
+
### `inquirex-tty run <flow_file>`
|
|
58
62
|
|
|
59
63
|
Execute a flow interactively. Each step is rendered with the appropriate tty-prompt widget based on the node's data type and widget hints.
|
|
60
64
|
|
|
61
65
|
```bash
|
|
62
66
|
# run it and then dump answers as json to stdout
|
|
63
|
-
|
|
67
|
+
inqt run examples/08_tax_preparer.rb
|
|
64
68
|
|
|
65
69
|
# run it and save the anawers to a json file
|
|
66
|
-
|
|
70
|
+
inqt run examples/08_tax_preparer.rb \
|
|
67
71
|
--output answers.json
|
|
68
72
|
```
|
|
69
73
|
|
|
70
74
|
Options:
|
|
71
75
|
|
|
72
|
-
| Flag
|
|
73
|
-
|
|
76
|
+
| Flag | Description |
|
|
77
|
+
| ---------------- | ---------------------------------------------- |
|
|
74
78
|
| `--output`, `-o` | Write JSON results to a file instead of stderr |
|
|
75
79
|
|
|
76
80
|
On completion, outputs a JSON summary:
|
|
@@ -98,7 +102,7 @@ On completion, outputs a JSON summary:
|
|
|
98
102
|
}
|
|
99
103
|
```
|
|
100
104
|
|
|
101
|
-
### `inquirex validate <flow_file>`
|
|
105
|
+
### `inquirex-tty validate <flow_file>`
|
|
102
106
|
|
|
103
107
|
Check that a flow definition is well-formed without running it. Validates:
|
|
104
108
|
|
|
@@ -107,49 +111,49 @@ Check that a flow definition is well-formed without running it. Validates:
|
|
|
107
111
|
- All steps are reachable from the start step (detects orphans)
|
|
108
112
|
|
|
109
113
|
```bash
|
|
110
|
-
|
|
114
|
+
inqt validate examples/08_tax_preparer.rb
|
|
111
115
|
```
|
|
112
116
|
|
|
113
|
-
### `inquirex graph <flow_file>`
|
|
117
|
+
### `inquirex-tty graph <flow_file>`
|
|
114
118
|
|
|
115
119
|
Export the flow as a [Mermaid](https://mermaid.js.org/) diagram source, an image, or both.
|
|
116
120
|
|
|
117
121
|
```bash
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
inqt graph examples/08_tax_preparer.rb # Mermaid source to stdout
|
|
123
|
+
inqt graph examples/08_tax_preparer.rb --output flow.mmd # write source to a file
|
|
124
|
+
inqt graph examples/08_tax_preparer.rb --format image -o flow.svg # SVG via mmdc
|
|
125
|
+
inqt graph examples/08_tax_preparer.rb --format both --output ~/Desktop # source + image into a directory
|
|
126
|
+
inqt graph examples/08_tax_preparer.rb --format image --open # SVG + open in viewer
|
|
123
127
|
```
|
|
124
128
|
|
|
125
129
|
Options:
|
|
126
130
|
|
|
127
|
-
| Flag
|
|
128
|
-
|
|
129
|
-
| `--output`, `-o` | Output file or directory (default: stdout)
|
|
130
|
-
| `--format`, `-f` | `source` (default), `image` (SVG via `mmdc`), or `both`
|
|
131
|
-
| `--open`, `-p`
|
|
131
|
+
| Flag | Description |
|
|
132
|
+
| ---------------- | -------------------------------------------------------------- |
|
|
133
|
+
| `--output`, `-o` | Output file or directory (default: stdout) |
|
|
134
|
+
| `--format`, `-f` | `source` (default), `image` (SVG via `mmdc`), or `both` |
|
|
135
|
+
| `--open`, `-p` | Open the generated image in the system viewer (default: false) |
|
|
132
136
|
|
|
133
137
|
Image generation requires [mermaid-cli](https://github.com/mermaid-js/mermaid-cli) (`npm install -g @mermaid-js/mermaid-cli`). The command attempts to install it automatically if `mmdc` is not on your `PATH`.
|
|
134
138
|
|
|
135
|
-
### `inquirex export <flow_file>`
|
|
139
|
+
### `inquirex-tty export <flow_file>`
|
|
136
140
|
|
|
137
141
|
Export the flow definition as JSON or YAML. Useful for serving flows to frontend adapters (the JS widget, Rails API, etc.) or for inspecting the wire format.
|
|
138
142
|
|
|
139
143
|
```bash
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
inqt export examples/08_tax_preparer.rb # pretty JSON to stdout
|
|
145
|
+
inqt export examples/08_tax_preparer.rb -f yml # YAML to stdout
|
|
146
|
+
inqt export examples/08_tax_preparer.rb -o . # write 08_tax_preparer.json to cwd
|
|
147
|
+
inqt export examples/08_tax_preparer.rb -f yml -o ~/flows # write 08_tax_preparer.yml to ~/flows
|
|
148
|
+
inqt export examples/08_tax_preparer.rb -o out.json # write to named file
|
|
149
|
+
inqt export examples/08_tax_preparer.rb -f yml -o out # appends .yml → out.yml
|
|
146
150
|
```
|
|
147
151
|
|
|
148
152
|
Options:
|
|
149
153
|
|
|
150
|
-
| Flag
|
|
151
|
-
|
|
152
|
-
| `--format`, `-f` | `json` (default), `yaml`, or `yml`
|
|
154
|
+
| Flag | Description |
|
|
155
|
+
| ---------------- | ------------------------------------------ |
|
|
156
|
+
| `--format`, `-f` | `json` (default), `yaml`, or `yml` |
|
|
153
157
|
| `--output`, `-o` | Output file or directory (default: stdout) |
|
|
154
158
|
|
|
155
159
|
Output path rules:
|
|
@@ -158,12 +162,12 @@ Output path rules:
|
|
|
158
162
|
- `--output <dir>` (existing directory) → write `<flow-basename>.<ext>` inside it
|
|
159
163
|
- `--output <file>` → use that filename; if the extension is missing or mismatched, the appropriate one (`.json`/`.yml`) is substituted
|
|
160
164
|
|
|
161
|
-
### `inquirex version`
|
|
165
|
+
### `inquirex-tty version`
|
|
162
166
|
|
|
163
167
|
Print version information for the TTY adapter and its dependencies.
|
|
164
168
|
|
|
165
169
|
```bash
|
|
166
|
-
|
|
170
|
+
inqt version
|
|
167
171
|
```
|
|
168
172
|
|
|
169
173
|
## Example Session
|
|
@@ -171,7 +175,7 @@ inquirex version
|
|
|
171
175
|
Running the tax preparation intake example:
|
|
172
176
|
|
|
173
177
|
```
|
|
174
|
-
$ inquirex run examples/08_tax_preparer.rb
|
|
178
|
+
$ inquirex-tty run examples/08_tax_preparer.rb
|
|
175
179
|
|
|
176
180
|
_____ _ __ __ ____ ____ _____ ____ _ ____ _ _____ ___ ___ _ _ ___ _ _ _____ _ _ __ _____
|
|
177
181
|
|_ _| / \ \ \/ / | _ \ | _ \ | ____| | _ \ / \ | _ \ / \ |_ _| |_ _| / _ \ | \ | | |_ _| | \ | | |_ _| / \ | |/ / | ____|
|
|
@@ -226,17 +230,17 @@ Press any key to continue...
|
|
|
226
230
|
|
|
227
231
|
The renderer selects a tty-prompt method for each node based on the `:tty` widget hint from `WidgetRegistry`:
|
|
228
232
|
|
|
229
|
-
| Widget Hint
|
|
230
|
-
|
|
231
|
-
| `text_input`
|
|
232
|
-
| `multiline`
|
|
233
|
-
| `number_input` | `prompt.ask` (with `convert:`) | `:integer`, `:decimal`, `:currency`
|
|
234
|
-
| `yes_no`
|
|
235
|
-
| `select`
|
|
236
|
-
| `multi_select` | `prompt.multi_select`
|
|
237
|
-
| `enum_select`
|
|
238
|
-
| `mask`
|
|
239
|
-
| `slider`
|
|
233
|
+
| Widget Hint | tty-prompt Method | Used For |
|
|
234
|
+
| -------------- | ------------------------------ | -------------------------------------- |
|
|
235
|
+
| `text_input` | `prompt.ask` | `:string`, `:date`, `:email`, `:phone` |
|
|
236
|
+
| `multiline` | `prompt.multiline` | `:text` |
|
|
237
|
+
| `number_input` | `prompt.ask` (with `convert:`) | `:integer`, `:decimal`, `:currency` |
|
|
238
|
+
| `yes_no` | `prompt.yes?` | `:boolean` / `confirm` |
|
|
239
|
+
| `select` | `prompt.select` | `:enum` |
|
|
240
|
+
| `multi_select` | `prompt.multi_select` | `:multi_enum` |
|
|
241
|
+
| `enum_select` | `prompt.enum_select` | Numbered menu variant |
|
|
242
|
+
| `mask` | `prompt.mask` | Password/hidden input |
|
|
243
|
+
| `slider` | `prompt.slider` | Numeric range |
|
|
240
244
|
|
|
241
245
|
You can override the default by setting an explicit `:tty` widget hint in the DSL:
|
|
242
246
|
|
|
@@ -252,49 +256,37 @@ end
|
|
|
252
256
|
|
|
253
257
|
## Display Verbs
|
|
254
258
|
|
|
255
|
-
| Verb
|
|
256
|
-
|
|
257
|
-
| `header`
|
|
258
|
-
| `say`
|
|
259
|
-
| `btw`
|
|
260
|
-
| `warning` | Warning-styled box (yellow/red)
|
|
259
|
+
| Verb | Rendering |
|
|
260
|
+
| --------- | ----------------------------------------------------------- |
|
|
261
|
+
| `header` | Large ASCII-art text via TTY::Font (falls back to TTY::Box) |
|
|
262
|
+
| `say` | Plain text with "Press any key to continue..." |
|
|
263
|
+
| `btw` | Info-styled box (blue border) |
|
|
264
|
+
| `warning` | Warning-styled box (yellow/red) |
|
|
261
265
|
|
|
262
266
|
## LLM Integration
|
|
263
267
|
|
|
264
|
-
When a flow contains [inquirex-llm](../inquirex-llm) verbs (`clarify`,
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
1.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
1.
|
|
271
|
-
below).
|
|
272
|
-
1. Shows a `🧠 Thinking — asking <provider> to extract structured data…`
|
|
273
|
-
banner while the LLM call is in flight.
|
|
274
|
-
1. For `clarify` steps, splats the extracted fields into the engine's
|
|
275
|
-
top-level answers via `Engine#prefill!`, so any downstream step with
|
|
276
|
-
`skip_if not_empty(:field)` is auto-skipped.
|
|
277
|
-
1. Prints a `✅` / `❓` extraction table showing which fields the LLM filled
|
|
278
|
-
in vs. which ones will still be asked.
|
|
268
|
+
When a flow contains [inquirex-llm](../inquirex-llm) verbs (`clarify`, `describe`, `summarize`, `detour`), the `inquirex-tty run` command automatically:
|
|
269
|
+
|
|
270
|
+
1. Loads `.env` files, walking up from the current directory and the flow file's directory. Shell-set values take precedence; empty-string keys are treated as unset.
|
|
271
|
+
1. Picks an adapter based on available credentials (see "Adapter selection" below).
|
|
272
|
+
1. Shows a `🧠 Thinking — asking <provider> to extract structured data…` banner while the LLM call is in flight.
|
|
273
|
+
1. For `clarify` steps, splats the extracted fields into the engine's top-level answers via `Engine#prefill!`, so any downstream step with `skip_if not_empty(:field)` is auto-skipped.
|
|
274
|
+
1. Prints a `✅` / `❓` extraction table showing which fields the LLM filled in vs. which ones will still be asked.
|
|
279
275
|
|
|
280
276
|
### Adapter selection (first match wins)
|
|
281
277
|
|
|
282
|
-
| Condition
|
|
283
|
-
|
|
284
|
-
| `INQUIREX_LLM_ADAPTER=null`
|
|
285
|
-
| `INQUIREX_LLM_ADAPTER=anthropic` | `Inquirex::LLM::AnthropicAdapter`
|
|
286
|
-
| `INQUIREX_LLM_ADAPTER=openai`
|
|
287
|
-
| `ANTHROPIC_API_KEY` is set
|
|
288
|
-
| `OPENAI_API_KEY` is set
|
|
289
|
-
| nothing set
|
|
278
|
+
| Condition | Adapter used |
|
|
279
|
+
| -------------------------------- | ----------------------------------- |
|
|
280
|
+
| `INQUIREX_LLM_ADAPTER=null` | `Inquirex::LLM::NullAdapter` |
|
|
281
|
+
| `INQUIREX_LLM_ADAPTER=anthropic` | `Inquirex::LLM::AnthropicAdapter` |
|
|
282
|
+
| `INQUIREX_LLM_ADAPTER=openai` | `Inquirex::LLM::OpenAIAdapter` |
|
|
283
|
+
| `ANTHROPIC_API_KEY` is set | `Inquirex::LLM::AnthropicAdapter` |
|
|
284
|
+
| `OPENAI_API_KEY` is set | `Inquirex::LLM::OpenAIAdapter` |
|
|
285
|
+
| nothing set | `Inquirex::LLM::NullAdapter` (demo) |
|
|
290
286
|
|
|
291
287
|
### End-to-end example
|
|
292
288
|
|
|
293
|
-
`examples/09_tax_preparer_llm.rb` is a complete LLM-assisted tax intake. The
|
|
294
|
-
user types one free-text description of their tax situation; the LLM extracts
|
|
295
|
-
`filing_status`, `dependents`, `income_types`, `state_filing`; the wizard only
|
|
296
|
-
asks for whatever the LLM couldn't determine, then runs a final `summarize`
|
|
297
|
-
for a complexity / fee-estimate write-up.
|
|
289
|
+
`examples/09_tax_preparer_llm.rb` is a complete LLM-assisted tax intake. The user types one free-text description of their tax situation; the LLM extracts `filing_status`, `dependents`, `income_types`, `state_filing`; the wizard only asks for whatever the LLM couldn't determine, then runs a final `summarize` for a complexity / fee-estimate write-up.
|
|
298
290
|
|
|
299
291
|
```ruby
|
|
300
292
|
# examples/09_tax_preparer_llm.rb (excerpt)
|
|
@@ -363,7 +355,7 @@ Run it:
|
|
|
363
355
|
# Put your key in any .env up the tree — OPENAI_API_KEY or ANTHROPIC_API_KEY
|
|
364
356
|
echo 'OPENAI_API_KEY=sk-…' >> ../.env
|
|
365
357
|
|
|
366
|
-
|
|
358
|
+
inqt run examples/09_tax_preparer_llm.rb
|
|
367
359
|
```
|
|
368
360
|
|
|
369
361
|
A typical session (input shortened):
|
|
@@ -384,52 +376,45 @@ A typical session (input shortened):
|
|
|
384
376
|
|
|
385
377
|
### Troubleshooting
|
|
386
378
|
|
|
387
|
-
- **"the null adapter"** shown in the thinking banner → no API key was found.
|
|
388
|
-
|
|
389
|
-
up the directory tree.
|
|
390
|
-
- **`NoMethodError: undefined method 'prefill!'`** → you're running the
|
|
391
|
-
`inquirex` binary against an older rubygems-installed copy of the core gem.
|
|
392
|
-
`exe/inquirex` now bootstraps Bundler against the repo's Gemfile
|
|
393
|
-
automatically, but a global `rake install`'d copy will still be
|
|
394
|
-
stale — run `rake install` in `../inquirex` to update, or run from a
|
|
395
|
-
checkout so the shim's Bundler bootstrap takes over.
|
|
379
|
+
- **"the null adapter"** shown in the thinking banner → no API key was found. Check `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` in your shell or in a `.env` up the directory tree.
|
|
380
|
+
- **`NoMethodError: undefined method 'prefill!'`** → you're running the `inquirex-tty` binary against an older rubygems-installed copy of the core gem. `exe/inquirex-tty` now bootstraps Bundler against the repo's Gemfile automatically, but a global `rake install`'d copy will still be stale — run `rake install` in `../inquirex` to update, or run from a checkout so the shim's Bundler bootstrap takes over.
|
|
396
381
|
|
|
397
382
|
## Examples
|
|
398
383
|
|
|
399
384
|
The gem ships with 10 examples of increasing complexity:
|
|
400
385
|
|
|
401
|
-
| Example
|
|
402
|
-
|
|
403
|
-
| `01_hello_world.rb`
|
|
404
|
-
| `02_yes_or_no.rb`
|
|
405
|
-
| `03_food_preferences.rb`
|
|
406
|
-
| `04_event_registration.rb` | Two-level branching
|
|
407
|
-
| `05_job_application.rb`
|
|
408
|
-
| `06_health_assessment.rb`
|
|
409
|
-
| `07_loan_application.rb`
|
|
410
|
-
| `08_tax_preparer.rb`
|
|
411
|
-
| `09_tax_preparer_llm.rb`
|
|
412
|
-
| `10_real_tax_preparer.rb`
|
|
386
|
+
| Example | Description | Steps | Features |
|
|
387
|
+
| -------------------------- | --------------------------- | ----- | ---------------------------------------------------------- |
|
|
388
|
+
| `01_hello_world.rb` | Minimal flow | 3 | String and integer input |
|
|
389
|
+
| `02_yes_or_no.rb` | Boolean branching | 3 | `confirm`, `equals` rule |
|
|
390
|
+
| `03_food_preferences.rb` | Multi-select branching | 6 | `multi_enum`, `contains` rule |
|
|
391
|
+
| `04_event_registration.rb` | Two-level branching | 9 | Nested conditionals |
|
|
392
|
+
| `05_job_application.rb` | Composed rules | 13 | `all()`, `any()`, `greater_than` |
|
|
393
|
+
| `06_health_assessment.rb` | Three-level branching | 18 | Complex composed rules |
|
|
394
|
+
| `07_loan_application.rb` | Real-world loan intake | 20+ | Currency, 3-level branching |
|
|
395
|
+
| `08_tax_preparer.rb` | Full tax preparation wizard | 18+ | All data types, deep branching |
|
|
396
|
+
| `09_tax_preparer_llm.rb` | **LLM-assisted tax intake** | 9 | `clarify` + `summarize`, `skip_if not_empty`, auto-prefill |
|
|
397
|
+
| `10_real_tax_preparer.rb` | Realistic tax preparer flow | 20+ | Full intake variant |
|
|
413
398
|
|
|
414
399
|
Run any example:
|
|
415
400
|
|
|
416
401
|
```bash
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
402
|
+
inqt run examples/01_hello_world.rb
|
|
403
|
+
inqt run examples/08_tax_preparer.rb
|
|
404
|
+
inqt run examples/09_tax_preparer_llm.rb
|
|
420
405
|
```
|
|
421
406
|
|
|
422
407
|
Validate all examples:
|
|
423
408
|
|
|
424
409
|
```bash
|
|
425
|
-
for f in examples/*.rb; do inquirex validate "$f"; done
|
|
410
|
+
for f in examples/*.rb; do inquirex-tty validate "$f"; done
|
|
426
411
|
```
|
|
427
412
|
|
|
428
413
|
## Architecture
|
|
429
414
|
|
|
430
415
|
```
|
|
431
416
|
inquirex-tty/
|
|
432
|
-
├── exe/inquirex
|
|
417
|
+
├── exe/inquirex-tty # CLI entry point (dry-cli)
|
|
433
418
|
└── lib/inquirex/tty/
|
|
434
419
|
├── commands/
|
|
435
420
|
│ ├── run.rb # Interactive flow execution
|
data/Rakefile
CHANGED
|
@@ -19,8 +19,11 @@ task gem: [:build] do
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
task permissions: [:clean] do
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
# One traversal replaces a six-level glob chain that printed "No such file
|
|
23
|
+
# or directory" for every level this project does not have, skipped dotfiles
|
|
24
|
+
# entirely, and silently stopped at depth six. .git is pruned — its objects
|
|
25
|
+
# have no business being group-readable.
|
|
26
|
+
shell("find . -path ./.git -prune -o -type d -exec chmod o+rx,g+rx {} + -o -type f -exec chmod o+r,g+r {} +")
|
|
24
27
|
end
|
|
25
28
|
|
|
26
29
|
task build: :permissions
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
|
|
16
16
|
<text x="31.5" y="15" fill="#010101" fill-opacity=".3">coverage</text>
|
|
17
17
|
<text x="31.5" y="14">coverage</text>
|
|
18
|
-
<text x="80" y="15" fill="#010101" fill-opacity=".3">
|
|
19
|
-
<text x="80" y="14">
|
|
18
|
+
<text x="80" y="15" fill="#010101" fill-opacity=".3">98%</text>
|
|
19
|
+
<text x="80" y="14">98%</text>
|
|
20
20
|
</g>
|
|
21
21
|
</svg>
|
|
@@ -49,6 +49,37 @@ Inquirex.define id: "tax-preparer-2025", version: "2.0.0" do
|
|
|
49
49
|
"Please do NOT enter any personal information: no names,\n" \
|
|
50
50
|
"SSN, ITIN, addresses, or account numbers. We only need\n" \
|
|
51
51
|
"enough to quote a fee."
|
|
52
|
+
transition to: :describe
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# -----------------------------------------------------------------------
|
|
56
|
+
# Opening
|
|
57
|
+
# -----------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
ask :describe do
|
|
60
|
+
type :text
|
|
61
|
+
question "Describe your last year's tax situation, your residency/citizenship status, your filing status, any dependents, various income types, which state(s) you are filing in,and whether you have your prior year returns available. If you also participate in businesses, please describe their tax status (S-Corp, LLC, etc), and if you have any foreign investments or property."
|
|
62
|
+
transition to: :summary
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
extract :summary do
|
|
66
|
+
from :describe
|
|
67
|
+
prompt "Extract the filer's tax intake details for scoping: filing status, " \
|
|
68
|
+
"dependents, income types, states filed in, residency status, prior " \
|
|
69
|
+
"return availability, and any business entities owned."
|
|
70
|
+
|
|
71
|
+
# Schema derived from the referenced questions: each symbol resolves to
|
|
72
|
+
# that ask step's type, and enum/multi_enum option values are folded in
|
|
73
|
+
# automatically — no duplication of the option lists here.
|
|
74
|
+
schema :filing_status,
|
|
75
|
+
:dependents,
|
|
76
|
+
:income_types,
|
|
77
|
+
:state_filing,
|
|
78
|
+
:residency_status,
|
|
79
|
+
:prior_return_available,
|
|
80
|
+
:business_entities
|
|
81
|
+
|
|
82
|
+
model :claude_sonnet
|
|
52
83
|
transition to: :residency_status
|
|
53
84
|
end
|
|
54
85
|
|
|
@@ -59,6 +90,7 @@ Inquirex.define id: "tax-preparer-2025", version: "2.0.0" do
|
|
|
59
90
|
ask :residency_status do
|
|
60
91
|
type :enum
|
|
61
92
|
question "Which best describes your US tax residency for 2025?"
|
|
93
|
+
skip_if not_empty(:residency_status)
|
|
62
94
|
options({
|
|
63
95
|
"us_person" => "US citizen or permanent resident",
|
|
64
96
|
"resident" => "Resident alien (substantial presence)",
|
|
@@ -74,6 +106,7 @@ Inquirex.define id: "tax-preparer-2025", version: "2.0.0" do
|
|
|
74
106
|
ask :prior_return_available do
|
|
75
107
|
type :enum
|
|
76
108
|
question "Do you have a copy of your most recent tax return?"
|
|
109
|
+
skip_if not_empty(:prior_return_available)
|
|
77
110
|
options({
|
|
78
111
|
"yes_last_year" => "Yes, last year's return",
|
|
79
112
|
"yes_older" => "Yes, but older than last year",
|
|
@@ -88,6 +121,7 @@ Inquirex.define id: "tax-preparer-2025", version: "2.0.0" do
|
|
|
88
121
|
ask :filing_status do
|
|
89
122
|
type :enum
|
|
90
123
|
question "What is your filing status for 2025?"
|
|
124
|
+
skip_if not_empty(:filing_status)
|
|
91
125
|
options({
|
|
92
126
|
"single" => "Single",
|
|
93
127
|
"married_filing_jointly" => "Married Filing Jointly",
|
|
@@ -98,12 +132,13 @@ Inquirex.define id: "tax-preparer-2025", version: "2.0.0" do
|
|
|
98
132
|
widget target: :tty, type: :select
|
|
99
133
|
widget target: :desktop, type: :radio_group, columns: 1
|
|
100
134
|
widget target: :mobile, type: :dropdown
|
|
101
|
-
transition to: :
|
|
135
|
+
transition to: :dependents
|
|
102
136
|
end
|
|
103
137
|
|
|
104
|
-
ask :
|
|
138
|
+
ask :dependents do
|
|
105
139
|
type :enum
|
|
106
140
|
question "How many dependents will you claim?"
|
|
141
|
+
skip_if not_empty(:dependents)
|
|
107
142
|
options %w[0 1 2 3 4+]
|
|
108
143
|
widget target: :tty, type: :select
|
|
109
144
|
widget target: :desktop, type: :radio_group, columns: 5
|
|
@@ -118,6 +153,7 @@ Inquirex.define id: "tax-preparer-2025", version: "2.0.0" do
|
|
|
118
153
|
ask :income_types do
|
|
119
154
|
type :multi_enum
|
|
120
155
|
question "Select every type of income you had in 2025."
|
|
156
|
+
skip_if not_empty(:income_types)
|
|
121
157
|
options({
|
|
122
158
|
"W2" => "W-2 wages",
|
|
123
159
|
"1099_nec" => "1099-NEC (contractor)",
|
|
@@ -166,6 +202,7 @@ Inquirex.define id: "tax-preparer-2025", version: "2.0.0" do
|
|
|
166
202
|
ask :business_entities do
|
|
167
203
|
type :multi_enum
|
|
168
204
|
question "Which business entity types do you own or partner in?"
|
|
205
|
+
skip_if not_empty(:business_entities)
|
|
169
206
|
options({
|
|
170
207
|
"sole_prop" => "Sole Proprietor / Schedule C",
|
|
171
208
|
"single_llc" => "Single-member LLC",
|
|
@@ -435,6 +472,7 @@ Inquirex.define id: "tax-preparer-2025", version: "2.0.0" do
|
|
|
435
472
|
ask :state_filing do
|
|
436
473
|
type :multi_enum
|
|
437
474
|
question "Select every state (and DC) you need to file a return in."
|
|
475
|
+
skip_if not_empty(:state_filing)
|
|
438
476
|
options US_STATES
|
|
439
477
|
widget target: :tty, type: :multi_select
|
|
440
478
|
widget target: :desktop, type: :checkbox_group, columns: 10, layout: :grid
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Inquirex.define do
|
|
2
|
+
meta title: "Coolness Questionnaire",
|
|
3
|
+
subtitle: "Let's figure out how cool you are :)"
|
|
4
|
+
|
|
5
|
+
start :welcome
|
|
6
|
+
|
|
7
|
+
say :welcome do
|
|
8
|
+
text "Hello there! This is a demo of the Qualified.At DSL."
|
|
9
|
+
transition to: :loves_to_dance
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
ask :loves_to_dance do
|
|
13
|
+
type :boolean
|
|
14
|
+
question "Might we ask, do you like to dance to electronic music?"
|
|
15
|
+
transition to: :no_problem, if_rule: equals(:loves_to_dance, false)
|
|
16
|
+
transition to: :burning_man
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
say :no_problem do
|
|
20
|
+
text "Not everyone loves to dance. It's quite personal, we get it."
|
|
21
|
+
transition to: :burning_man
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
ask :burning_man do
|
|
25
|
+
type :enum
|
|
26
|
+
question "How many times have you been to Burning Man?"
|
|
27
|
+
options ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30"]
|
|
28
|
+
transition to: :awesome, if_rule: greater_than(:burning_man, 20)
|
|
29
|
+
transition to: :good, if_rule: greater_than(:burning_man, 10)
|
|
30
|
+
transition to: :decent, if_rule: greater_than(:burning_man, 1)
|
|
31
|
+
transition to: :lame, if_rule: less_than(:burning_man, 1)
|
|
32
|
+
widget target: :tty, type: :select
|
|
33
|
+
widget target: :desktop, type: :radio_group, columns: 5
|
|
34
|
+
widget target: :mobile, type: :dropdown
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
say :awesome do
|
|
38
|
+
text "You are awesome! That must be a record!"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
say :good do
|
|
42
|
+
text "Not bad at all! This is a very respectable number."
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
say :decent do
|
|
46
|
+
text "It's decent, but not amazing, pardon our judgment :)"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
say :lame do
|
|
50
|
+
text "You've never been? Pardon the judgment, but you MUST go. It's beyond compare."
|
|
51
|
+
end
|
|
52
|
+
end
|
data/exe/inqt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
inquirex-tty
|
data/justfile
CHANGED
|
@@ -4,7 +4,26 @@ set shell := ["bash", "-c"]
|
|
|
4
4
|
|
|
5
5
|
repo := 'git@github.com:inquirex/inquirex-tty.git'
|
|
6
6
|
version := `grep VERSION lib/inquirex/tty/version.rb | awk '{print $3}' | tr -d '"' | tr -d '\n'`
|
|
7
|
-
|
|
7
|
+
# The `-` matters: `rbenv init bash` prints human instructions ("skipping
|
|
8
|
+
# ~/.bash_login: already configured"), which eval then tries to run and the
|
|
9
|
+
# recipe dies with "skipping: command not found". `rbenv init - bash` prints
|
|
10
|
+
# the shell code that is meant to be eval'd.
|
|
11
|
+
rbenv := 'eval "$(rbenv init - bash 2>/dev/null || true)"; bundle exec '
|
|
12
|
+
|
|
13
|
+
# 1Password secret reference for the RubyGems TOTP, and the full path to `op`
|
|
14
|
+
# because a recipe does not inherit an interactive shell's PATH.
|
|
15
|
+
# The account is NOT named here: it is a private 1Password address and this
|
|
16
|
+
# file is public. `op` reads it from OP_ACCOUNT, exported by the ecosystem
|
|
17
|
+
# root's .envrc, which lives outside every repo. Two accounts are
|
|
18
|
+
# registered and only one holds the open-source-repos vault, so without
|
|
19
|
+
# OP_ACCOUNT the read can resolve against the wrong one, return nothing,
|
|
20
|
+
# and publish silently falls back to prompting mid-release.
|
|
21
|
+
op := '/opt/homebrew/bin/op'
|
|
22
|
+
otp_ref := 'op://open-source-repos/ruby-gems/one-time password?attribute=otp'
|
|
23
|
+
|
|
24
|
+
gem_name := 'inquirex-tty'
|
|
25
|
+
gem_file := 'pkg/' + gem_name + '-' + version + '.gem'
|
|
26
|
+
gem_url := 'https://rubygems.org/gems/' + gem_name
|
|
8
27
|
|
|
9
28
|
[no-exit-message]
|
|
10
29
|
recipes:
|
|
@@ -29,15 +48,15 @@ format:
|
|
|
29
48
|
|
|
30
49
|
# Run a flow interactively
|
|
31
50
|
run flow_file:
|
|
32
|
-
{{ rbenv }} exe/
|
|
51
|
+
{{ rbenv }} exe/inqt run {{flow_file}}
|
|
33
52
|
|
|
34
53
|
# Validate a flow definition
|
|
35
54
|
validate flow_file:
|
|
36
|
-
{{ rbenv }} exe/
|
|
55
|
+
{{ rbenv }} exe/inqt validate {{flow_file}}
|
|
37
56
|
|
|
38
57
|
# Export a flow as a Mermaid diagram (stdout)
|
|
39
58
|
graph flow_file:
|
|
40
|
-
{{ rbenv }} exe/
|
|
59
|
+
{{ rbenv }} exe/inqt graph {{flow_file}}
|
|
41
60
|
|
|
42
61
|
# Validate all examples
|
|
43
62
|
examples:
|
|
@@ -45,7 +64,7 @@ examples:
|
|
|
45
64
|
set -e
|
|
46
65
|
for f in examples/*.rb; do
|
|
47
66
|
echo "=== $f ==="
|
|
48
|
-
{{ rbenv }} exe/
|
|
67
|
+
{{ rbenv }} exe/inqt validate "$f"
|
|
49
68
|
echo ""
|
|
50
69
|
done
|
|
51
70
|
|
|
@@ -59,12 +78,45 @@ clean:
|
|
|
59
78
|
/usr/bin/find . -name .DS_Store -delete -print || true
|
|
60
79
|
rm -rf tmp/*
|
|
61
80
|
|
|
62
|
-
#
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
81
|
+
# `gem push` rather than `rake release`: release also guards the tree, tags and
|
|
82
|
+
# pushes git — which `just release` does deliberately and separately — and it
|
|
83
|
+
# gives no way to pass a 2FA code, so it always stopped to prompt.
|
|
84
|
+
#
|
|
85
|
+
# The code comes from 1Password unless one is passed in:
|
|
86
|
+
#
|
|
87
|
+
# just publish # read the code from 1Password
|
|
88
|
+
# just publish 123456 # use this code
|
|
89
|
+
#
|
|
90
|
+
# `just publish-all` in inquirex-tools passes one, because a TOTP is single-use:
|
|
91
|
+
# four gems reading the same 30-second window would have the second push
|
|
92
|
+
# rejected as a replay.
|
|
93
|
+
#
|
|
94
|
+
# Build the .gem and push it to RubyGems, non-interactively
|
|
95
|
+
publish otp="": build
|
|
96
|
+
#!/usr/bin/env bash
|
|
97
|
+
set -euo pipefail
|
|
98
|
+
eval "$(rbenv init - bash 2>/dev/null || true)"
|
|
99
|
+
|
|
100
|
+
mkdir -p pkg
|
|
101
|
+
gem build {{ gem_name }}.gemspec --output "{{ gem_file }}"
|
|
102
|
+
|
|
103
|
+
# `|| true` is load-bearing: under `set -e` a failed `op read` — not signed
|
|
104
|
+
# in to 1Password, item renamed, op not installed — would abort the recipe
|
|
105
|
+
# before the prompting fallback below could run.
|
|
106
|
+
otp="{{ otp }}"
|
|
107
|
+
[[ -n "${otp}" ]] || otp=$({{ op }} read "{{ otp_ref }}" 2>/dev/null || true)
|
|
108
|
+
|
|
109
|
+
if [[ -n "${otp}" ]]; then
|
|
110
|
+
gem push "{{ gem_file }}" --otp "${otp}"
|
|
111
|
+
else
|
|
112
|
+
echo "rubygems: no OTP available — gem push will prompt if 2FA is required."
|
|
113
|
+
gem push "{{ gem_file }}"
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# Only reachable when the push succeeded: `set -e` aborts the recipe on a
|
|
117
|
+
# non-zero `gem push`, so the page never opens for a release that failed.
|
|
118
|
+
echo "published {{ gem_name }} {{ version }} → {{ gem_url }}"
|
|
119
|
+
open "{{ gem_url }}" 2>/dev/null || xdg-open "{{ gem_url }}" 2>/dev/null || true
|
|
68
120
|
|
|
69
121
|
# Tag v{{ version }}, publish the GH release, & refresh the Homebrew tap.
|
|
70
122
|
release:
|
|
@@ -12,9 +12,11 @@ module Inquirex
|
|
|
12
12
|
# inquirex export examples/08_tax_preparer.rb -f yml -o . # write 08_tax_preparer.yml to cwd
|
|
13
13
|
# inquirex export examples/08_tax_preparer.rb -o out.json # write to out.json
|
|
14
14
|
class Export < Dry::CLI::Command
|
|
15
|
+
# Multi-line help text shown by `inquirex export --help`.
|
|
15
16
|
LONG_DESCRIPTION = "Export a flow definition as JSON or YAML.\n\n" \
|
|
16
17
|
"Example:\n inquirex export examples/08_tax_preparer.rb -f yml -o ."
|
|
17
18
|
|
|
19
|
+
# One-line summary shown in the top-level command listing.
|
|
18
20
|
SHORT_DESCRIPTION = "Export a flow definition as JSON or YAML ."
|
|
19
21
|
|
|
20
22
|
if ARGV[0] == "export"
|
|
@@ -44,7 +46,7 @@ module Inquirex
|
|
|
44
46
|
content = serialize(definition, format)
|
|
45
47
|
write(content, flow_file, options[:output], extension_for(format))
|
|
46
48
|
rescue Inquirex::TTY::Error => e
|
|
47
|
-
|
|
49
|
+
$stderr.puts "Error: #{e.message}"
|
|
48
50
|
exit 1
|
|
49
51
|
end
|
|
50
52
|
|
|
@@ -73,7 +75,7 @@ module Inquirex
|
|
|
73
75
|
end
|
|
74
76
|
|
|
75
77
|
File.write(path, content)
|
|
76
|
-
|
|
78
|
+
$stderr.puts "Exported to #{path}"
|
|
77
79
|
end
|
|
78
80
|
end
|
|
79
81
|
end
|
|
@@ -5,11 +5,13 @@ module Inquirex
|
|
|
5
5
|
module Commands
|
|
6
6
|
# Exports a flow definition as a Mermaid flowchart (stdout or file).
|
|
7
7
|
class Graph < Dry::CLI::Command
|
|
8
|
+
# Multi-line help text shown by `inquirex graph --help`.
|
|
8
9
|
LONG_DESCRIPTION = "Export a flow definition as a Mermaid diagram source, an image, or both.\n " \
|
|
9
10
|
"Image generation requires mermaid-cli (npm install -g @mermaid-js/mermaid-cli)\n " \
|
|
10
11
|
"which this gem will attempt to install for you if mmdc command is not available.\n\n" \
|
|
11
12
|
"Example:\n inquirex graph qualify_dsl.rb --format both --output ~/Desktop --open"
|
|
12
13
|
|
|
14
|
+
# One-line summary shown in the top-level command listing.
|
|
13
15
|
SHORT_DESCRIPTION = "Export a flow definition as a Mermaid diagram source, an image, or both."
|
|
14
16
|
|
|
15
17
|
if ARGV[0] == "graph"
|
|
@@ -54,7 +56,7 @@ module Inquirex
|
|
|
54
56
|
write_image(source, OutputPath.resolve_with_default(flow_file, output, ".png"), options[:open])
|
|
55
57
|
end
|
|
56
58
|
rescue Inquirex::TTY::Error => e
|
|
57
|
-
|
|
59
|
+
$stderr.puts "Error: #{e.message}"
|
|
58
60
|
exit 1
|
|
59
61
|
end
|
|
60
62
|
|
|
@@ -66,7 +68,7 @@ module Inquirex
|
|
|
66
68
|
return
|
|
67
69
|
end
|
|
68
70
|
File.write(output_path, source)
|
|
69
|
-
|
|
71
|
+
$stderr.puts "Diagram written to #{output_path}"
|
|
70
72
|
end
|
|
71
73
|
|
|
72
74
|
def write_image(source, output_path, open_file)
|
|
@@ -81,14 +83,14 @@ module Inquirex
|
|
|
81
83
|
)
|
|
82
84
|
end
|
|
83
85
|
|
|
84
|
-
|
|
86
|
+
$stderr.puts "Diagram written to #{output_path}"
|
|
85
87
|
open_image_file(output_path) if open_file
|
|
86
88
|
end
|
|
87
89
|
|
|
88
90
|
def ensure_mermaid_cli_installed!
|
|
89
91
|
return if command_available?("mmdc")
|
|
90
92
|
|
|
91
|
-
|
|
93
|
+
$stderr.puts "Installing @mermaid-js/mermaid-cli..."
|
|
92
94
|
installed = system("npm install -g @mermaid-js/mermaid-cli")
|
|
93
95
|
return if installed && command_available?("mmdc")
|
|
94
96
|
|
|
@@ -22,7 +22,7 @@ module Inquirex
|
|
|
22
22
|
File.write(options[:output], json)
|
|
23
23
|
puts "\nResults saved to #{options[:output]}"
|
|
24
24
|
else
|
|
25
|
-
$stderr.puts json
|
|
25
|
+
$stderr.puts json
|
|
26
26
|
end
|
|
27
27
|
rescue Inquirex::TTY::Error => e
|
|
28
28
|
error(e.message)
|
|
@@ -43,6 +43,8 @@ module Inquirex
|
|
|
43
43
|
renderer = Renderer.new
|
|
44
44
|
adapter = build_llm_adapter
|
|
45
45
|
|
|
46
|
+
engine.after_completion { |eng| eng.completion_metadata = build_completion_metadata }
|
|
47
|
+
|
|
46
48
|
show_banner(definition)
|
|
47
49
|
|
|
48
50
|
until engine.finished?
|
|
@@ -55,7 +57,8 @@ module Inquirex
|
|
|
55
57
|
renderer.render(step)
|
|
56
58
|
engine.advance
|
|
57
59
|
else
|
|
58
|
-
engine.
|
|
60
|
+
suggestion = engine.suggestion_for(engine.current_step_id)
|
|
61
|
+
engine.answer(renderer.render(step, suggestion:))
|
|
59
62
|
end
|
|
60
63
|
end
|
|
61
64
|
|
|
@@ -183,11 +186,56 @@ module Inquirex
|
|
|
183
186
|
{
|
|
184
187
|
flow_file: flow_file,
|
|
185
188
|
path_taken: engine.history,
|
|
186
|
-
answers: engine.
|
|
189
|
+
answers: engine.answers_with_metadata,
|
|
187
190
|
steps_completed: engine.history.length,
|
|
188
191
|
completed_at: Time.now.iso8601
|
|
189
192
|
}
|
|
190
193
|
end
|
|
194
|
+
|
|
195
|
+
# Rich completion metadata for the answers. Only :engine and
|
|
196
|
+
# :engine_version are guaranteed — the environment probes are
|
|
197
|
+
# best-effort: a failing one (offline host, no login name, blocked
|
|
198
|
+
# egress) yields nil and its member is dropped.
|
|
199
|
+
#
|
|
200
|
+
# @return [Inquirex::CompletionMetadata]
|
|
201
|
+
def build_completion_metadata
|
|
202
|
+
optional = {
|
|
203
|
+
uname: best_effort { OpenStruct.new(Etc.uname) },
|
|
204
|
+
user: best_effort { Etc.getlogin },
|
|
205
|
+
local_ip: best_effort { local_ip },
|
|
206
|
+
public_ip: best_effort { public_ip },
|
|
207
|
+
terminal: ENV["LC_TERMINAL"] || ENV["TERM_PROGRAM"] || "Unknown"
|
|
208
|
+
}.compact
|
|
209
|
+
Inquirex::CompletionMetadata.new(
|
|
210
|
+
engine: "inquirex-tty",
|
|
211
|
+
engine_version: Inquirex::TTY::VERSION,
|
|
212
|
+
**optional
|
|
213
|
+
)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def best_effort
|
|
217
|
+
yield
|
|
218
|
+
rescue StandardError
|
|
219
|
+
nil
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# The local address the OS would route out through — a UDP "connect"
|
|
223
|
+
# picks the outbound interface without sending a single packet.
|
|
224
|
+
def local_ip
|
|
225
|
+
UDPSocket.open do |socket|
|
|
226
|
+
socket.connect("8.8.8.8", 1)
|
|
227
|
+
socket.addr.last
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Short timeouts so a slow or airgapped network can never stall the
|
|
232
|
+
# CLI at completion time.
|
|
233
|
+
def public_ip
|
|
234
|
+
Net::HTTP.start("api.ipify.org", 443, use_ssl: true, open_timeout: 2, read_timeout: 2) do |http|
|
|
235
|
+
response = http.get("/")
|
|
236
|
+
response.is_a?(Net::HTTPSuccess) ? response.body.to_s.strip : nil
|
|
237
|
+
end
|
|
238
|
+
end
|
|
191
239
|
end
|
|
192
240
|
end
|
|
193
241
|
end
|
|
@@ -23,10 +23,10 @@ module Inquirex
|
|
|
23
23
|
exit 1
|
|
24
24
|
end
|
|
25
25
|
rescue Inquirex::TTY::Error => e
|
|
26
|
-
|
|
26
|
+
$stderr.puts "Error: #{e.message}"
|
|
27
27
|
exit 1
|
|
28
28
|
rescue Inquirex::Errors::Error => e
|
|
29
|
-
|
|
29
|
+
$stderr.puts "Definition error: #{e.message}"
|
|
30
30
|
exit 1
|
|
31
31
|
end
|
|
32
32
|
|
|
@@ -48,8 +48,8 @@ module Inquirex
|
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
def print_errors(errors)
|
|
51
|
-
|
|
52
|
-
errors.each { |e|
|
|
51
|
+
$stderr.puts "Flow definition has #{errors.length} error(s):"
|
|
52
|
+
errors.each { |e| $stderr.puts " - #{e}" }
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
def validate_definition(definition)
|
|
@@ -7,7 +7,9 @@ module Inquirex
|
|
|
7
7
|
class Version < Dry::CLI::Command
|
|
8
8
|
desc "Print version information"
|
|
9
9
|
|
|
10
|
-
#
|
|
10
|
+
# Prints the version of this gem and of the core inquirex gem.
|
|
11
|
+
# Any CLI options are accepted and ignored.
|
|
12
|
+
#
|
|
11
13
|
# @return [void]
|
|
12
14
|
def call(**)
|
|
13
15
|
puts "inquirex-tty #{Inquirex::TTY::VERSION}"
|
|
@@ -6,6 +6,12 @@ module Inquirex
|
|
|
6
6
|
# binding. The file is expected to call +Inquirex.define+ and return an
|
|
7
7
|
# +Inquirex::Definition+.
|
|
8
8
|
class FlowLoader
|
|
9
|
+
# Loads and evaluates a flow definition file in one call.
|
|
10
|
+
#
|
|
11
|
+
# @example Load a flow definition from disk
|
|
12
|
+
# definition = Inquirex::TTY::FlowLoader.load("examples/08_tax_preparer.rb")
|
|
13
|
+
# definition.step_ids # => [:filing_status, :dependents, ...]
|
|
14
|
+
#
|
|
9
15
|
# @param path [String] path to a .rb flow definition file
|
|
10
16
|
# @return [Inquirex::Definition]
|
|
11
17
|
# @raise [Inquirex::TTY::Error] if file is missing, not .rb, or has syntax errors
|
|
@@ -18,6 +18,11 @@ module Inquirex
|
|
|
18
18
|
|
|
19
19
|
# Resolves an output path, returning nil when the caller should use stdout.
|
|
20
20
|
#
|
|
21
|
+
# @example Resolve the --output option for a JSON export
|
|
22
|
+
# OutputPath.resolve("flows/intake.rb", nil, ".json") # => nil (stdout)
|
|
23
|
+
# OutputPath.resolve("flows/intake.rb", "out", ".json") # => "out.json"
|
|
24
|
+
# OutputPath.resolve("flows/intake.rb", "/tmp", ".json") # => "/tmp/intake.json"
|
|
25
|
+
#
|
|
21
26
|
# @param flow_file [String] source .rb flow file (basename used for defaults)
|
|
22
27
|
# @param output [String, nil] value of the --output option
|
|
23
28
|
# @param extension [String] including leading dot, e.g. ".json"
|
|
@@ -63,15 +63,28 @@ module Inquirex
|
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
# Renders a node. Returns the collected answer, or +nil+ for display verbs.
|
|
66
|
+
#
|
|
67
|
+
# @example Collect an answer for the current step, pre-checking LLM suggestions
|
|
68
|
+
# renderer = Inquirex::TTY::Renderer.new
|
|
69
|
+
# step = engine.current_step
|
|
70
|
+
# answer = renderer.render(step, suggestion: engine.suggestion_for(engine.current_step_id))
|
|
71
|
+
# engine.answer(answer) unless step.display?
|
|
72
|
+
#
|
|
66
73
|
# @param node [Inquirex::Node]
|
|
74
|
+
# @param suggestion [Array, nil] prefill suggestion for the step (option
|
|
75
|
+
# form values, e.g. from Engine#suggestion_for) — multi-select prompts
|
|
76
|
+
# render these choices pre-checked so the user confirms or extends
|
|
67
77
|
# @return [Object, nil]
|
|
68
|
-
def render(node)
|
|
78
|
+
def render(node, suggestion: nil)
|
|
79
|
+
@current_suggestion = suggestion
|
|
69
80
|
if node.display?
|
|
70
81
|
render_display_verb(node)
|
|
71
82
|
nil
|
|
72
83
|
else
|
|
73
84
|
render_collecting(node)
|
|
74
85
|
end
|
|
86
|
+
ensure
|
|
87
|
+
@current_suggestion = nil
|
|
75
88
|
end
|
|
76
89
|
|
|
77
90
|
private
|
|
@@ -195,9 +208,26 @@ module Inquirex
|
|
|
195
208
|
prompt.select(node.question, select_options(node))
|
|
196
209
|
end
|
|
197
210
|
|
|
198
|
-
# Multiple-choice list (space to toggle, min 1 selection).
|
|
211
|
+
# Multiple-choice list (space to toggle, min 1 selection). Suggested
|
|
212
|
+
# choices (an LLM extraction the user should confirm) render pre-checked.
|
|
199
213
|
def render_multi_select(node)
|
|
200
|
-
prompt.multi_select(node.question, select_options(node), min: 1)
|
|
214
|
+
prompt.multi_select(node.question, select_options(node), min: 1, **multi_select_defaults(node))
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# tty-prompt pre-checks multi_select choices via default: choice NAMES —
|
|
218
|
+
# the labels, not the return values. Suggestions arrive as option form
|
|
219
|
+
# values (that is the wire contract), so translate value → label here;
|
|
220
|
+
# a value with no label entry falls back to itself.
|
|
221
|
+
#
|
|
222
|
+
# @param node [Inquirex::Node]
|
|
223
|
+
# @return [Hash] {} or { default: Array<String> }
|
|
224
|
+
def multi_select_defaults(node)
|
|
225
|
+
suggested = Array(@current_suggestion)
|
|
226
|
+
return {} if suggested.empty?
|
|
227
|
+
|
|
228
|
+
labels = node.respond_to?(:option_labels) ? node.option_labels : nil
|
|
229
|
+
names = suggested.map { |value| labels&.fetch(value.to_s, nil) || value.to_s }
|
|
230
|
+
{ default: names }
|
|
201
231
|
end
|
|
202
232
|
|
|
203
233
|
# Numbered choice menu.
|
|
@@ -12,6 +12,13 @@ module Inquirex
|
|
|
12
12
|
@pastel ||= Pastel.new
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
+
# Module hook that installs the UI helpers on the including class:
|
|
16
|
+
# +tty_box+, +tty_screen+, +pastel+, the TTY::Box wrappers (+frame+,
|
|
17
|
+
# +info+, +success+, +error+, +warning+), the +width+ delegator, and
|
|
18
|
+
# the +box+ / +next_step+ / +sep+ convenience methods.
|
|
19
|
+
#
|
|
20
|
+
# @param base [Class, Module] the class or module including UIHelper
|
|
21
|
+
# @return [void]
|
|
15
22
|
def included(base)
|
|
16
23
|
base.extend(Forwardable)
|
|
17
24
|
base.define_method(:tty_box) { ::TTY::Box }
|
|
@@ -27,6 +34,12 @@ module Inquirex
|
|
|
27
34
|
|
|
28
35
|
base.class_eval do
|
|
29
36
|
# Draw a bordered box with optional title.
|
|
37
|
+
#
|
|
38
|
+
# @param text [String] text to display inside the box
|
|
39
|
+
# @param title [String, nil] optional title on the top border
|
|
40
|
+
# @param bg [Symbol] background color name
|
|
41
|
+
# @param fg [Symbol] foreground color name
|
|
42
|
+
# @return [void]
|
|
30
43
|
def box(text, title: nil, bg: :green, fg: :white) # rubocop:disable Naming/MethodParameterName
|
|
31
44
|
w = [width, 80].min
|
|
32
45
|
args = {
|
|
@@ -41,12 +54,21 @@ module Inquirex
|
|
|
41
54
|
end
|
|
42
55
|
|
|
43
56
|
# Print step progress and separator.
|
|
57
|
+
#
|
|
58
|
+
# @param step_id [Symbol, String] id of the step about to run
|
|
59
|
+
# @param step_number [Integer] 1-based position in the flow
|
|
60
|
+
# @return [void]
|
|
44
61
|
def next_step(step_id, step_number)
|
|
62
|
+
puts
|
|
45
63
|
puts pastel.yellow("Step #{step_number}: #{step_id}")
|
|
46
64
|
sep(:yellow, "━")
|
|
47
65
|
end
|
|
48
66
|
|
|
49
67
|
# Print a full-width separator in the given color.
|
|
68
|
+
#
|
|
69
|
+
# @param color [Symbol] Pastel color name
|
|
70
|
+
# @param char [String] character repeated across the line
|
|
71
|
+
# @return [void]
|
|
50
72
|
def sep(color = :yellow, char = "━")
|
|
51
73
|
puts pastel.send(color, char * 80)
|
|
52
74
|
end
|
data/lib/inquirex/tty/version.rb
CHANGED
data/lib/inquirex/tty.rb
CHANGED
|
@@ -14,7 +14,15 @@ require "yaml"
|
|
|
14
14
|
require "time"
|
|
15
15
|
require "shellwords"
|
|
16
16
|
require "tempfile"
|
|
17
|
+
require "etc"
|
|
18
|
+
require "socket"
|
|
19
|
+
require "net/http"
|
|
20
|
+
require "uri"
|
|
21
|
+
require "ostruct"
|
|
17
22
|
|
|
23
|
+
# Top-level namespace shared by every gem in the Inquirex family.
|
|
24
|
+
# The core gem defines the DSL, rules, and engine; this gem adds the
|
|
25
|
+
# terminal adapter under {Inquirex::TTY}.
|
|
18
26
|
module Inquirex
|
|
19
27
|
# Terminal adapter for Inquirex flows. Renders questions as interactive
|
|
20
28
|
# TTY prompts via tty-prompt, mapping each data type to the appropriate
|
data/sig/inquirex/tty.rbs
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: inquirex-tty
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Konstantin Gredeskoul
|
|
@@ -29,28 +29,42 @@ dependencies:
|
|
|
29
29
|
requirements:
|
|
30
30
|
- - "~>"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '0.
|
|
32
|
+
version: '0.6'
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '0.
|
|
39
|
+
version: '0.6'
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: inquirex-llm
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
43
43
|
requirements:
|
|
44
44
|
- - "~>"
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: '0.
|
|
46
|
+
version: '0.6'
|
|
47
47
|
type: :runtime
|
|
48
48
|
prerelease: false
|
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
50
|
requirements:
|
|
51
51
|
- - "~>"
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: '0.
|
|
53
|
+
version: '0.6'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: ostruct
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0.6'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0.6'
|
|
54
68
|
- !ruby/object:Gem::Dependency
|
|
55
69
|
name: pastel
|
|
56
70
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -127,11 +141,13 @@ description: 'Renders Inquirex flow definitions as interactive ANSI terminal wiz
|
|
|
127
141
|
email:
|
|
128
142
|
- kigster@gmail.com
|
|
129
143
|
executables:
|
|
130
|
-
-
|
|
144
|
+
- inqt
|
|
145
|
+
- inquirex-tty
|
|
131
146
|
extensions: []
|
|
132
147
|
extra_rdoc_files: []
|
|
133
148
|
files:
|
|
134
149
|
- ".envrc"
|
|
150
|
+
- ".github_changelog_generator"
|
|
135
151
|
- ".relaxed_rubocop.yml"
|
|
136
152
|
- ".secrets.baseline"
|
|
137
153
|
- CHANGELOG.md
|
|
@@ -153,7 +169,9 @@ files:
|
|
|
153
169
|
- examples/10_real_tax_preparer.rb
|
|
154
170
|
- examples/11_llm_keywords.rb
|
|
155
171
|
- examples/README.md
|
|
156
|
-
-
|
|
172
|
+
- examples/burning-man.rb
|
|
173
|
+
- exe/inqt
|
|
174
|
+
- exe/inquirex-tty
|
|
157
175
|
- justfile
|
|
158
176
|
- lefthook.yml
|
|
159
177
|
- lib/inquirex/tty.rb
|
|
@@ -192,7 +210,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
192
210
|
- !ruby/object:Gem::Version
|
|
193
211
|
version: '0'
|
|
194
212
|
requirements: []
|
|
195
|
-
rubygems_version: 4.0.
|
|
213
|
+
rubygems_version: 4.0.17
|
|
196
214
|
specification_version: 4
|
|
197
215
|
summary: Terminal adapter for Inquirex flows — interactive TTY wizard via tty-prompt
|
|
198
216
|
test_files: []
|
|
File without changes
|