@solidnumber/cli 2.15.1 → 2.17.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.
- package/README.md +17 -0
- package/dist/commands/dev.d.ts +12 -0
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +29 -1
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/forms.d.ts.map +1 -1
- package/dist/commands/forms.js +829 -67
- package/dist/commands/forms.js.map +1 -1
- package/dist/commands/offer.d.ts +18 -0
- package/dist/commands/offer.d.ts.map +1 -0
- package/dist/commands/offer.js +117 -0
- package/dist/commands/offer.js.map +1 -0
- package/dist/commands/orders.d.ts.map +1 -1
- package/dist/commands/orders.js +143 -1
- package/dist/commands/orders.js.map +1 -1
- package/dist/commands/payment_links.d.ts.map +1 -1
- package/dist/commands/payment_links.js +41 -1
- package/dist/commands/payment_links.js.map +1 -1
- package/dist/commands/verbs.d.ts.map +1 -1
- package/dist/commands/verbs.js +25 -59
- package/dist/commands/verbs.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/json-arg.d.ts +2 -0
- package/dist/lib/json-arg.d.ts.map +1 -0
- package/dist/lib/json-arg.js +71 -0
- package/dist/lib/json-arg.js.map +1 -0
- package/package.json +6 -4
- package/platform-docs/llms.txt +9 -8
package/dist/commands/forms.js
CHANGED
|
@@ -47,6 +47,7 @@ const chalk_1 = __importDefault(require("chalk"));
|
|
|
47
47
|
const config_1 = require("../lib/config");
|
|
48
48
|
const api_client_1 = require("../lib/api-client");
|
|
49
49
|
const json_output_1 = require("../lib/json-output");
|
|
50
|
+
const json_arg_1 = require("../lib/json-arg");
|
|
50
51
|
function requireAuth() {
|
|
51
52
|
if (!config_1.config.isLoggedIn()) {
|
|
52
53
|
console.error(chalk_1.default.red('Not logged in. Run `solid auth login` first.'));
|
|
@@ -58,31 +59,54 @@ exports.formsCommand = new commander_1.Command('forms')
|
|
|
58
59
|
.alias('surveys')
|
|
59
60
|
.description('Forms & surveys — CRUD, AI generate, export CSV/Excel/PDF');
|
|
60
61
|
{
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
// ⛔ `list` GOES THROUGH VERBS. It used to read /api/v1/surveys directly, so it
|
|
63
|
+
// showed a different set than `solid forms status` (no lifecycle, native only,
|
|
64
|
+
// and blind to lead forms). Two commands in one CLI disagreeing about what
|
|
65
|
+
// forms exist is worse than either being wrong. One seam, one answer.
|
|
66
|
+
const listCmd = exports.formsCommand.command('list').alias('ls')
|
|
67
|
+
.description('Every form, with its lifecycle — across every connected provider');
|
|
68
|
+
listCmd.option('--provider <name>', 'Only this provider');
|
|
69
|
+
listCmd.option('--json', 'Output as JSON');
|
|
64
70
|
listCmd.action(async (opts) => {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return;
|
|
71
|
+
requireAuth();
|
|
72
|
+
const s2 = (0, ora_1.default)('Loading forms...').start();
|
|
73
|
+
try {
|
|
74
|
+
const providers = opts.provider
|
|
75
|
+
? [opts.provider]
|
|
76
|
+
: ((await callVerb('form.sources')).connected ?? [])
|
|
77
|
+
.map((c) => String(c.provider));
|
|
78
|
+
const rows = [];
|
|
79
|
+
for (const provider of providers.length ? providers : ['native']) {
|
|
80
|
+
try {
|
|
81
|
+
const out = await callVerb('form.list', { provider });
|
|
82
|
+
for (const f of (out.forms ?? [])) {
|
|
83
|
+
rows.push({ ...f, provider });
|
|
84
|
+
}
|
|
80
85
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
catch { /* one provider down must not blank the list */ }
|
|
87
|
+
}
|
|
88
|
+
if ((0, json_output_1.isJsonOutput)(opts)) {
|
|
89
|
+
s2.stop();
|
|
90
|
+
console.log(JSON.stringify({ forms: rows }, null, 2));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
s2.stop();
|
|
94
|
+
if (!rows.length) {
|
|
95
|
+
console.log(chalk_1.default.dim(' No forms yet.'));
|
|
96
|
+
console.log(chalk_1.default.dim(' Build one: solid forms build --intent intake --save'));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
for (const f of rows) {
|
|
100
|
+
const state = lifecycleOf(f);
|
|
101
|
+
const answered = Number(f.response_count ?? 0);
|
|
102
|
+
console.log(` ${chalk_1.default.bold(String(f.external_id).padEnd(5))} ${lifecycleTag(state).padEnd(16)} ` +
|
|
103
|
+
`${String(f.title ?? '').slice(0, 34).padEnd(34)} ` +
|
|
104
|
+
`${chalk_1.default.dim(`${answered} answered`)} ${chalk_1.default.dim(String(f.provider))}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
fail(s2, 'Failed to load forms', e);
|
|
109
|
+
}
|
|
86
110
|
});
|
|
87
111
|
}
|
|
88
112
|
exports.formsCommand
|
|
@@ -93,17 +117,15 @@ exports.formsCommand
|
|
|
93
117
|
requireAuth();
|
|
94
118
|
const s = (0, ora_1.default)(`Loading ${id}...`).start();
|
|
95
119
|
try {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
s.succeed(chalk_1.default.green(`Form ${id}`));
|
|
103
|
-
console.log(JSON.stringify(res.data, null, 2));
|
|
120
|
+
// Through the verb, so `get` and `describe` can never disagree about a
|
|
121
|
+
// form. The verb also carries the lifecycle and the public link, which
|
|
122
|
+
// /api/v1/surveys/<id> knows nothing about.
|
|
123
|
+
const out = await callVerb('form.describe', { form_id: String(id), provider: 'native' });
|
|
124
|
+
s.stop();
|
|
125
|
+
console.log(JSON.stringify(out, null, 2));
|
|
104
126
|
}
|
|
105
127
|
catch (e) {
|
|
106
|
-
fail(s, 'Failed', e);
|
|
128
|
+
fail(s, 'Failed to load the form', e);
|
|
107
129
|
}
|
|
108
130
|
});
|
|
109
131
|
exports.formsCommand
|
|
@@ -122,13 +144,37 @@ exports.formsCommand
|
|
|
122
144
|
}
|
|
123
145
|
const s = (0, ora_1.default)('Creating form...').start();
|
|
124
146
|
try {
|
|
125
|
-
const
|
|
126
|
-
|
|
147
|
+
const out = await callVerbConfirmed('survey.create', {
|
|
148
|
+
title: body.title,
|
|
149
|
+
questions: body.questions ?? body.fields ?? [],
|
|
150
|
+
...(body.description ? { description: body.description } : {}),
|
|
151
|
+
});
|
|
152
|
+
if (out.status === 'error' || out.status === 'invalid') {
|
|
153
|
+
s.fail(chalk_1.default.red(String(out.summary ?? 'Could not create it')));
|
|
154
|
+
process.exitCode = 1;
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
s.succeed(chalk_1.default.green(`Form created: ${out.survey_id ?? out.id ?? ''}`));
|
|
158
|
+
console.log(chalk_1.default.dim(' It starts as a draft. Publish when ready:'));
|
|
159
|
+
console.log(chalk_1.default.dim(` solid forms publish ${out.survey_id ?? out.id ?? '<id>'}`));
|
|
127
160
|
}
|
|
128
161
|
catch (e) {
|
|
129
|
-
fail(s, 'Failed', e);
|
|
162
|
+
fail(s, 'Failed to create the form', e);
|
|
130
163
|
}
|
|
131
164
|
});
|
|
165
|
+
// ⛔ WHAT IS STILL ON REST, AND WHY — checked 2026-08-26, not assumed.
|
|
166
|
+
//
|
|
167
|
+
// update / delete no survey.update or survey.delete verb exists. Inventing
|
|
168
|
+
// a CLI-only path to a destructive operation is worse than
|
|
169
|
+
// an honest REST call; these move when the verbs do.
|
|
170
|
+
// export no verb — it streams a file (csv/excel/pdf), which the
|
|
171
|
+
// JSON verb envelope has no shape for.
|
|
172
|
+
// followup /surveys/<id>/followup drafts follow-ups to RESPONSES.
|
|
173
|
+
// review.followup is a different thing entirely (it takes a
|
|
174
|
+
// rating and drafts a REVIEW reply), so routing one to the
|
|
175
|
+
// other would silently change what the command does.
|
|
176
|
+
//
|
|
177
|
+
// Everything else in this file goes through the verb layer.
|
|
132
178
|
exports.formsCommand
|
|
133
179
|
.command('update <id>')
|
|
134
180
|
.description('Update a form')
|
|
@@ -173,46 +219,68 @@ exports.formsCommand
|
|
|
173
219
|
requireAuth();
|
|
174
220
|
const s = (0, ora_1.default)('Generating form...').start();
|
|
175
221
|
try {
|
|
176
|
-
const
|
|
177
|
-
|
|
222
|
+
const out = await callVerbConfirmed('survey.generate', { prompt });
|
|
223
|
+
if (out.status === 'error' || out.status === 'invalid') {
|
|
224
|
+
s.fail(chalk_1.default.red(String(out.summary ?? 'Could not generate it')));
|
|
225
|
+
process.exitCode = 1;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
s.succeed(chalk_1.default.green(`Form generated: ${out.survey_id ?? out.id ?? ''}`));
|
|
229
|
+
console.log(chalk_1.default.dim(' Read it before publishing: solid forms describe '
|
|
230
|
+
+ String(out.survey_id ?? out.id ?? '<id>')));
|
|
178
231
|
}
|
|
179
232
|
catch (e) {
|
|
180
|
-
fail(s, 'Failed', e);
|
|
233
|
+
fail(s, 'Failed to generate the form', e);
|
|
181
234
|
}
|
|
182
235
|
});
|
|
183
236
|
exports.formsCommand
|
|
184
237
|
.command('optimize <id>')
|
|
185
|
-
.description('
|
|
186
|
-
.
|
|
238
|
+
.description('ADA reviews the questions — suggestions only, nothing changes')
|
|
239
|
+
.option('--json', 'Output as JSON')
|
|
240
|
+
.action(async (id, opts) => {
|
|
187
241
|
requireAuth();
|
|
188
|
-
const
|
|
242
|
+
const sp = (0, ora_1.default)('Looking it over...').start();
|
|
189
243
|
try {
|
|
190
|
-
await
|
|
191
|
-
|
|
244
|
+
const out = await callVerb('survey.optimize', { survey_id: Number(id) });
|
|
245
|
+
if ((0, json_output_1.isJsonOutput)(opts)) {
|
|
246
|
+
sp.stop();
|
|
247
|
+
console.log(JSON.stringify(out, null, 2));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
sp.stop();
|
|
251
|
+
const analysis = (out.analysis ?? {});
|
|
252
|
+
const suggestions = (analysis.suggested_improvements ?? []);
|
|
253
|
+
if (!suggestions.length) {
|
|
254
|
+
console.log(chalk_1.default.dim(' Nothing to change — it reads well.'));
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
console.log(chalk_1.default.dim(' Suggestions only — nothing changes until you change it.'));
|
|
258
|
+
for (const sg of suggestions) {
|
|
259
|
+
console.log(` ${chalk_1.default.yellow('*')} ${sg.issue}`);
|
|
260
|
+
if (sg.fix)
|
|
261
|
+
console.log(` ${chalk_1.default.dim('fix:')} ${sg.fix}`);
|
|
262
|
+
if (sg.impact)
|
|
263
|
+
console.log(` ${chalk_1.default.dim('why:')} ${sg.impact}`);
|
|
264
|
+
}
|
|
192
265
|
}
|
|
193
266
|
catch (e) {
|
|
194
|
-
fail(
|
|
267
|
+
fail(sp, 'Failed to look it over', e);
|
|
195
268
|
}
|
|
196
269
|
});
|
|
197
270
|
exports.formsCommand
|
|
198
271
|
.command('analyze <id>')
|
|
199
|
-
.description('
|
|
272
|
+
.description('What the answers add up to')
|
|
200
273
|
.option('--json', 'Output as JSON')
|
|
201
|
-
.action(async (id
|
|
274
|
+
.action(async (id) => {
|
|
202
275
|
requireAuth();
|
|
203
|
-
const
|
|
276
|
+
const sp = (0, ora_1.default)('Analyzing...').start();
|
|
204
277
|
try {
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
console.log(JSON.stringify(res.data, null, 2));
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
s.succeed(chalk_1.default.green('Analysis'));
|
|
212
|
-
console.log(JSON.stringify(res.data, null, 2));
|
|
278
|
+
const out = await callVerb('survey.analyze', { survey_id: Number(id) });
|
|
279
|
+
sp.stop();
|
|
280
|
+
console.log(JSON.stringify(out, null, 2));
|
|
213
281
|
}
|
|
214
282
|
catch (e) {
|
|
215
|
-
fail(
|
|
283
|
+
fail(sp, 'Failed to analyze', e);
|
|
216
284
|
}
|
|
217
285
|
});
|
|
218
286
|
exports.formsCommand
|
|
@@ -232,17 +300,47 @@ exports.formsCommand
|
|
|
232
300
|
});
|
|
233
301
|
exports.formsCommand
|
|
234
302
|
.command('embed <id>')
|
|
235
|
-
.description('
|
|
236
|
-
.
|
|
303
|
+
.description('Paste-ready embed for a live form — iframe, link, or button')
|
|
304
|
+
.option('--provider <name>', 'Form provider', 'native')
|
|
305
|
+
.option('--as <kind>', 'iframe | link | button', 'iframe')
|
|
306
|
+
.action(async (id, opts) => {
|
|
237
307
|
requireAuth();
|
|
238
|
-
|
|
308
|
+
// ⛔ THE DOOR, NOT THE LEGACY LINK. This read /api/v1/surveys/<id>/embed —
|
|
309
|
+
// the pre-lifecycle system's public URL — so after the standing door shipped
|
|
310
|
+
// (2026-08-20) `solid forms embed` was handing out the OLD address while
|
|
311
|
+
// `solid forms link` handed out the new one. Two commands, two answers, one
|
|
312
|
+
// of them wrong.
|
|
313
|
+
const sp = (0, ora_1.default)('Building the embed...').start();
|
|
314
|
+
let out;
|
|
239
315
|
try {
|
|
240
|
-
|
|
241
|
-
s.succeed(chalk_1.default.green('Embed'));
|
|
242
|
-
console.log(JSON.stringify(res.data, null, 2));
|
|
316
|
+
out = await callVerb('form.describe', { form_id: String(id), provider: opts.provider });
|
|
243
317
|
}
|
|
244
318
|
catch (e) {
|
|
245
|
-
fail(
|
|
319
|
+
fail(sp, 'Failed to build the embed', e);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const url = out.public_url ?? out.public_path;
|
|
323
|
+
if (!url) {
|
|
324
|
+
sp.fail(chalk_1.default.yellow(`No public address — this form is ${String(out.lifecycle ?? 'not live')}.`));
|
|
325
|
+
console.error(chalk_1.default.dim(' Only a live form can be embedded. Publish it first:'));
|
|
326
|
+
console.error(chalk_1.default.dim(` solid forms publish ${id}`));
|
|
327
|
+
process.exitCode = 1;
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
sp.stop();
|
|
331
|
+
const title = String(out.title ?? 'Form');
|
|
332
|
+
const kind = String(opts.as).toLowerCase();
|
|
333
|
+
if (kind === 'link') {
|
|
334
|
+
console.log(`<a href="${url}">${title}</a>`);
|
|
335
|
+
}
|
|
336
|
+
else if (kind === 'button') {
|
|
337
|
+
console.log(`<a href="${url}" style="display:inline-block;padding:12px 20px;border-radius:8px;` +
|
|
338
|
+
`background:#0f5346;color:#fff;text-decoration:none;font-weight:600">${title}</a>`);
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
console.log(`<!-- ${title} — answers land in your CRM -->\n` +
|
|
342
|
+
`<iframe src="${url}" title="${title}" width="100%" height="640" ` +
|
|
343
|
+
`style="border:0;border-radius:12px" loading="lazy"></iframe>`);
|
|
246
344
|
}
|
|
247
345
|
});
|
|
248
346
|
exports.formsCommand
|
|
@@ -274,11 +372,675 @@ exports.formsCommand
|
|
|
274
372
|
fail(s, 'Failed', e);
|
|
275
373
|
}
|
|
276
374
|
});
|
|
375
|
+
// ── the verb seam ──────────────────────────────────────────────────────────
|
|
376
|
+
//
|
|
377
|
+
// ⛔ EVERYTHING BELOW CALLS VERBS, NOT REST. The commands above this line wrap
|
|
378
|
+
// /api/v1/surveys — the authoring endpoints that predate the forms platform —
|
|
379
|
+
// and they are why `solid forms` knew nothing about the lifecycle, the
|
|
380
|
+
// respondent-facing reads, or a form's public address. New work goes through
|
|
381
|
+
// the verb layer so the CLI, the dashboard, MCP and an agent on a live call
|
|
382
|
+
// all get the same validation, consent and tenant scoping.
|
|
383
|
+
//
|
|
384
|
+
// ⛔ NEVER SEND company_id. The backend binds the tenant from the authenticated
|
|
385
|
+
// principal; a CLI that could pass one is a CLI that could pass someone else's.
|
|
386
|
+
async function callVerb(name, payload = {}) {
|
|
387
|
+
// ⛔ ONLY THE VERB IS HYPHENATED, NEVER THE NAMESPACE. The backend routes
|
|
388
|
+
// `<ns>/<verb-with-hyphens>` (controllers/agent_verb_index.py::_verb_http_endpoint),
|
|
389
|
+
// and plenty of namespaces carry underscores — call_flow, comms_workflow,
|
|
390
|
+
// agent_config. Hyphenating the whole name would 404 every one of them.
|
|
391
|
+
const [ns, ...rest] = name.split('.');
|
|
392
|
+
const verb = rest.join('.').replace(/_/g, '-');
|
|
393
|
+
const endpoint = verb ? `/api/v1/agent/${ns}/${verb}` : `/api/v1/agent/${ns}`;
|
|
394
|
+
const res = await api_client_1.apiClient.post(endpoint, payload);
|
|
395
|
+
const body = res.data;
|
|
396
|
+
// The envelope is {ok, result} on some surfaces and the bare result on others.
|
|
397
|
+
return (body && typeof body === 'object' && 'result' in body ? body.result : body);
|
|
398
|
+
}
|
|
399
|
+
/** A write verb. Consent travels as `confirm: true` — the backend refuses without it. */
|
|
400
|
+
async function callVerbConfirmed(name, payload = {}) {
|
|
401
|
+
return callVerb(name, { ...payload, confirm: true });
|
|
402
|
+
}
|
|
403
|
+
function lifecycleOf(f) {
|
|
404
|
+
if ((f.status ?? 'published') === 'draft')
|
|
405
|
+
return 'draft';
|
|
406
|
+
return f.is_active === false ? 'paused' : 'live';
|
|
407
|
+
}
|
|
408
|
+
function lifecycleTag(state) {
|
|
409
|
+
return state === 'live' ? chalk_1.default.green('live')
|
|
410
|
+
: state === 'draft' ? chalk_1.default.yellow('draft')
|
|
411
|
+
: chalk_1.default.dim('paused');
|
|
412
|
+
}
|
|
413
|
+
exports.formsCommand
|
|
414
|
+
.command('describe <id>')
|
|
415
|
+
.description('The questions as a respondent meets them, plus lifecycle + public link')
|
|
416
|
+
.option('--provider <name>', 'Form provider', 'native')
|
|
417
|
+
.option('--json', 'Output as JSON')
|
|
418
|
+
.action(async (id, opts) => {
|
|
419
|
+
requireAuth();
|
|
420
|
+
const s = (0, ora_1.default)(`Reading ${id}...`).start();
|
|
421
|
+
try {
|
|
422
|
+
const out = await callVerb('form.describe', { form_id: String(id), provider: opts.provider });
|
|
423
|
+
if ((0, json_output_1.isJsonOutput)(opts)) {
|
|
424
|
+
s.stop();
|
|
425
|
+
console.log(JSON.stringify(out, null, 2));
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
s.stop();
|
|
429
|
+
const questions = (out.questions ?? []);
|
|
430
|
+
console.log(` ${chalk_1.default.bold(String(out.title ?? id))} ${lifecycleTag(String(out.lifecycle ?? ''))}`);
|
|
431
|
+
if (out.public_url)
|
|
432
|
+
console.log(` ${chalk_1.default.dim('public link')} ${chalk_1.default.cyan(String(out.public_url))}`);
|
|
433
|
+
else if (out.public_path)
|
|
434
|
+
console.log(` ${chalk_1.default.dim('public path')} ${String(out.public_path)}`);
|
|
435
|
+
console.log(` ${chalk_1.default.dim(`${questions.length} question${questions.length === 1 ? '' : 's'}`)}`);
|
|
436
|
+
questions.forEach((q, i) => {
|
|
437
|
+
const req = q.required ? chalk_1.default.dim(' (required)') : '';
|
|
438
|
+
console.log(` ${String(i + 1).padStart(2, '0')} ${chalk_1.default.dim(String(q.kind).padEnd(8))} ${q.prompt}${req}`);
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
catch (e) {
|
|
442
|
+
fail(s, 'Failed to read the form', e);
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
exports.formsCommand
|
|
446
|
+
.command('publish <id>')
|
|
447
|
+
.description('Publish a draft — it starts taking answers')
|
|
448
|
+
.action(async (id) => {
|
|
449
|
+
requireAuth();
|
|
450
|
+
const s = (0, ora_1.default)(`Publishing ${id}...`).start();
|
|
451
|
+
try {
|
|
452
|
+
const out = await callVerbConfirmed('survey.publish', { survey_id: Number(id) });
|
|
453
|
+
if (out.status === 'error' || out.status === 'not_found') {
|
|
454
|
+
s.fail(chalk_1.default.red(String(out.summary ?? 'Could not publish')));
|
|
455
|
+
process.exit(1);
|
|
456
|
+
}
|
|
457
|
+
s.succeed(chalk_1.default.green('Published — live and taking answers'));
|
|
458
|
+
try {
|
|
459
|
+
const d = await callVerb('form.describe', { form_id: String(id), provider: 'native' });
|
|
460
|
+
if (d.public_url)
|
|
461
|
+
console.log(` ${chalk_1.default.dim('public link')} ${chalk_1.default.cyan(String(d.public_url))}`);
|
|
462
|
+
}
|
|
463
|
+
catch { /* the publish still succeeded */ }
|
|
464
|
+
}
|
|
465
|
+
catch (e) {
|
|
466
|
+
fail(s, 'Failed to publish', e);
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
exports.formsCommand
|
|
470
|
+
.command('pause <id>')
|
|
471
|
+
.description('Stop taking new answers. Every answer already given is kept')
|
|
472
|
+
.action(async (id) => {
|
|
473
|
+
requireAuth();
|
|
474
|
+
const s = (0, ora_1.default)(`Pausing ${id}...`).start();
|
|
475
|
+
try {
|
|
476
|
+
const out = await callVerbConfirmed('survey.set_live', { survey_id: Number(id), live: false });
|
|
477
|
+
if (out.status === 'invalid' || out.status === 'error') {
|
|
478
|
+
s.fail(chalk_1.default.red(String(out.summary ?? 'Could not pause it')));
|
|
479
|
+
process.exit(1);
|
|
480
|
+
}
|
|
481
|
+
s.succeed(chalk_1.default.green('Paused — every answer kept, no new ones'));
|
|
482
|
+
}
|
|
483
|
+
catch (e) {
|
|
484
|
+
fail(s, 'Failed to pause', e);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
exports.formsCommand
|
|
488
|
+
.command('resume <id>')
|
|
489
|
+
.description('Take answers again on a paused form')
|
|
490
|
+
.action(async (id) => {
|
|
491
|
+
requireAuth();
|
|
492
|
+
const s = (0, ora_1.default)(`Resuming ${id}...`).start();
|
|
493
|
+
try {
|
|
494
|
+
const out = await callVerbConfirmed('survey.set_live', { survey_id: Number(id), live: true });
|
|
495
|
+
if (out.status === 'invalid' || out.status === 'error') {
|
|
496
|
+
s.fail(chalk_1.default.red(String(out.summary ?? 'Could not resume it')));
|
|
497
|
+
process.exit(1);
|
|
498
|
+
}
|
|
499
|
+
s.succeed(chalk_1.default.green('Live — taking answers again'));
|
|
500
|
+
}
|
|
501
|
+
catch (e) {
|
|
502
|
+
fail(s, 'Failed to resume', e);
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
exports.formsCommand
|
|
506
|
+
.command('link <id>')
|
|
507
|
+
.description("The form's standing public URL — the one address you can share anywhere")
|
|
508
|
+
.option('--provider <name>', 'Form provider', 'native')
|
|
509
|
+
.action(async (id, opts) => {
|
|
510
|
+
requireAuth();
|
|
511
|
+
const s = (0, ora_1.default)('Resolving...').start();
|
|
512
|
+
// ⛔ THE EXIT LIVES OUTSIDE THE try. An intentional process.exit inside it
|
|
513
|
+
// is caught by the error handler below and reported as "Failed to resolve
|
|
514
|
+
// the link" — the command would then exit 0 with a misleading message.
|
|
515
|
+
let out;
|
|
516
|
+
try {
|
|
517
|
+
out = await callVerb('form.describe', { form_id: String(id), provider: opts.provider });
|
|
518
|
+
}
|
|
519
|
+
catch (e) {
|
|
520
|
+
fail(s, 'Failed to resolve the link', e);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
if (out.public_url || out.public_path) {
|
|
524
|
+
s.stop();
|
|
525
|
+
// Bare URL on stdout so it pipes into pbcopy, a QR generator, anything.
|
|
526
|
+
console.log(String(out.public_url ?? out.public_path));
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
s.fail(chalk_1.default.yellow(`No public link — this form is ${String(out.lifecycle ?? 'not live')}.`));
|
|
530
|
+
console.error(chalk_1.default.dim(' Only a live form has a public address. Publish it first:'));
|
|
531
|
+
console.error(chalk_1.default.dim(` solid forms publish ${id}`));
|
|
532
|
+
process.exit(1);
|
|
533
|
+
});
|
|
534
|
+
exports.formsCommand
|
|
535
|
+
.command('responses <id>')
|
|
536
|
+
.description('What people actually answered')
|
|
537
|
+
.option('--provider <name>', 'Form provider', 'native')
|
|
538
|
+
.option('--limit <n>', 'How many to show', '25')
|
|
539
|
+
.option('--json', 'Output as JSON')
|
|
540
|
+
.action(async (id, opts) => {
|
|
541
|
+
requireAuth();
|
|
542
|
+
const s = (0, ora_1.default)('Loading responses...').start();
|
|
543
|
+
try {
|
|
544
|
+
const out = await callVerb('form.responses', {
|
|
545
|
+
form_id: String(id), provider: opts.provider, per_page: Number(opts.limit),
|
|
546
|
+
});
|
|
547
|
+
if ((0, json_output_1.isJsonOutput)(opts)) {
|
|
548
|
+
s.stop();
|
|
549
|
+
console.log(JSON.stringify(out, null, 2));
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
s.stop();
|
|
553
|
+
const rows = (out.responses ?? []);
|
|
554
|
+
if (!rows.length) {
|
|
555
|
+
console.log(chalk_1.default.dim(' Nothing answered yet.'));
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
for (const r of rows) {
|
|
559
|
+
const when = String(r.completed_at ?? r.created_at ?? '').split('T')[0];
|
|
560
|
+
const channels = Array.isArray(r.channels) ? r.channels.join('+') : '';
|
|
561
|
+
console.log(` ${chalk_1.default.bold(String(r.external_id ?? r.id ?? ''))} ${chalk_1.default.dim(when)} ${chalk_1.default.dim(channels)}`);
|
|
562
|
+
for (const [k, v] of Object.entries((r.answers ?? {}))) {
|
|
563
|
+
console.log(` ${chalk_1.default.dim(k)}: ${String(v)}`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
catch (e) {
|
|
568
|
+
fail(s, 'Failed to load responses', e);
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
exports.formsCommand
|
|
572
|
+
.command('status')
|
|
573
|
+
.description('Every form with its lifecycle — the CLI view of the library')
|
|
574
|
+
.option('--provider <name>', 'Form provider', 'native')
|
|
575
|
+
.option('--json', 'Output as JSON')
|
|
576
|
+
.action(async (opts) => {
|
|
577
|
+
requireAuth();
|
|
578
|
+
const s = (0, ora_1.default)('Loading...').start();
|
|
579
|
+
try {
|
|
580
|
+
const out = await callVerb('form.list', { provider: opts.provider });
|
|
581
|
+
if ((0, json_output_1.isJsonOutput)(opts)) {
|
|
582
|
+
s.stop();
|
|
583
|
+
console.log(JSON.stringify(out, null, 2));
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
s.stop();
|
|
587
|
+
const rows = (out.forms ?? []);
|
|
588
|
+
if (!rows.length) {
|
|
589
|
+
console.log(chalk_1.default.dim(' No forms yet.'));
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
for (const f of rows) {
|
|
593
|
+
const state = lifecycleOf(f);
|
|
594
|
+
const answered = Number(f.response_count ?? 0);
|
|
595
|
+
console.log(` ${chalk_1.default.bold(String(f.external_id).padEnd(5))} ${lifecycleTag(state).padEnd(16)} ` +
|
|
596
|
+
`${String(f.title ?? '').padEnd(34)} ${chalk_1.default.dim(`${answered} answered`)}`);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
catch (e) {
|
|
600
|
+
fail(s, 'Failed to load forms', e);
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
// ── moments: WHEN a form goes out ──────────────────────────────────────────
|
|
604
|
+
//
|
|
605
|
+
// The dashboard's most-prompted next step after publishing, and it had no CLI
|
|
606
|
+
// surface at all — so an AI could publish a form and then nothing could ever
|
|
607
|
+
// send it.
|
|
608
|
+
const momentsCmd = exports.formsCommand.command('moments')
|
|
609
|
+
.description('When a form goes out — the tenant\'s moments');
|
|
610
|
+
momentsCmd
|
|
611
|
+
.command('list', { isDefault: true })
|
|
612
|
+
.description('Every moment, and which form (if any) it sends')
|
|
613
|
+
.option('--json', 'Output as JSON')
|
|
614
|
+
.action(async (opts) => {
|
|
615
|
+
requireAuth();
|
|
616
|
+
const sp = (0, ora_1.default)('Loading moments...').start();
|
|
617
|
+
try {
|
|
618
|
+
const out = await callVerb('form.triggers');
|
|
619
|
+
if ((0, json_output_1.isJsonOutput)(opts)) {
|
|
620
|
+
sp.stop();
|
|
621
|
+
console.log(JSON.stringify(out, null, 2));
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
sp.stop();
|
|
625
|
+
const rows = (out.triggers ?? []);
|
|
626
|
+
if (!rows.length) {
|
|
627
|
+
console.log(chalk_1.default.dim(' No moments available.'));
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
for (const t of rows) {
|
|
631
|
+
const on = t.configured ? chalk_1.default.green('on ') : chalk_1.default.dim('off');
|
|
632
|
+
const what = t.configured && t.form_id
|
|
633
|
+
? `sends form ${t.form_id} ${t.stage === 'before' ? 'beforehand' : 'afterwards'}`
|
|
634
|
+
: chalk_1.default.dim('nothing set');
|
|
635
|
+
console.log(` ${on} ${String(t.event).padEnd(24)} ${what}`);
|
|
636
|
+
}
|
|
637
|
+
console.log(chalk_1.default.dim('\n Wire one: solid forms moments set <event> --form <id>'));
|
|
638
|
+
}
|
|
639
|
+
catch (e) {
|
|
640
|
+
fail(sp, 'Failed to load moments', e);
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
momentsCmd
|
|
644
|
+
.command('set <event>')
|
|
645
|
+
.description('Send a form at this moment')
|
|
646
|
+
.requiredOption('--form <id>', 'The form to send')
|
|
647
|
+
.option('--provider <name>', 'Form provider', 'native')
|
|
648
|
+
.option('--stage <when>', 'before | after')
|
|
649
|
+
.action(async (event, opts) => {
|
|
650
|
+
requireAuth();
|
|
651
|
+
const sp = (0, ora_1.default)(`Wiring ${event}...`).start();
|
|
652
|
+
try {
|
|
653
|
+
const out = await callVerbConfirmed('form.configure_trigger', {
|
|
654
|
+
event, form_id: String(opts.form), provider: opts.provider,
|
|
655
|
+
enabled: true, ...(opts.stage ? { stage: opts.stage } : {}),
|
|
656
|
+
});
|
|
657
|
+
if (out.status === 'invalid' || out.status === 'error') {
|
|
658
|
+
sp.fail(chalk_1.default.red(String(out.summary ?? 'Could not wire it')));
|
|
659
|
+
process.exitCode = 1;
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
sp.succeed(chalk_1.default.green(`${event} now sends form ${opts.form}`));
|
|
663
|
+
}
|
|
664
|
+
catch (e) {
|
|
665
|
+
fail(sp, 'Failed to wire the moment', e);
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
momentsCmd
|
|
669
|
+
.command('off <event>')
|
|
670
|
+
.description('Stop sending anything at this moment')
|
|
671
|
+
.action(async (event) => {
|
|
672
|
+
requireAuth();
|
|
673
|
+
const sp = (0, ora_1.default)(`Switching ${event} off...`).start();
|
|
674
|
+
try {
|
|
675
|
+
const out = await callVerbConfirmed('form.configure_trigger', { event, enabled: false });
|
|
676
|
+
if (out.status === 'invalid' || out.status === 'error') {
|
|
677
|
+
sp.fail(chalk_1.default.red(String(out.summary ?? 'Could not switch it off')));
|
|
678
|
+
process.exitCode = 1;
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
sp.succeed(chalk_1.default.green(`${event} sends nothing now`));
|
|
682
|
+
}
|
|
683
|
+
catch (e) {
|
|
684
|
+
fail(sp, 'Failed to switch it off', e);
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
// ── reviews: where a happy customer is sent ────────────────────────────────
|
|
688
|
+
const reviewsCmd = exports.formsCommand.command('reviews')
|
|
689
|
+
.description('Where happy customers are asked to leave a review');
|
|
690
|
+
reviewsCmd
|
|
691
|
+
.command('list', { isDefault: true })
|
|
692
|
+
.description('The review destinations this tenant has set')
|
|
693
|
+
.option('--json', 'Output as JSON')
|
|
694
|
+
.action(async (opts) => {
|
|
695
|
+
requireAuth();
|
|
696
|
+
const sp = (0, ora_1.default)('Loading destinations...').start();
|
|
697
|
+
try {
|
|
698
|
+
const out = await callVerb('review.destinations');
|
|
699
|
+
if ((0, json_output_1.isJsonOutput)(opts)) {
|
|
700
|
+
sp.stop();
|
|
701
|
+
console.log(JSON.stringify(out, null, 2));
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
sp.stop();
|
|
705
|
+
const rows = (out.destinations ?? []);
|
|
706
|
+
if (!rows.length) {
|
|
707
|
+
console.log(chalk_1.default.dim(' No review destinations yet.'));
|
|
708
|
+
console.log(chalk_1.default.dim(' Set one: solid forms reviews set google <url>'));
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
for (const d of rows)
|
|
712
|
+
console.log(` ${String(d.platform).padEnd(12)} ${d.url}`);
|
|
713
|
+
}
|
|
714
|
+
catch (e) {
|
|
715
|
+
fail(sp, 'Failed to load destinations', e);
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
reviewsCmd
|
|
719
|
+
.command('set <platform> <url>')
|
|
720
|
+
.description('Point future review requests at this link')
|
|
721
|
+
.action(async (platform, url) => {
|
|
722
|
+
requireAuth();
|
|
723
|
+
const sp = (0, ora_1.default)('Saving...').start();
|
|
724
|
+
try {
|
|
725
|
+
const out = await callVerbConfirmed('review.set_destination', { platform, url });
|
|
726
|
+
if (out.status === 'invalid' || out.status === 'error') {
|
|
727
|
+
sp.fail(chalk_1.default.red(String(out.summary ?? 'Could not save it')));
|
|
728
|
+
process.exitCode = 1;
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
sp.succeed(chalk_1.default.green(`${platform} review link saved`));
|
|
732
|
+
}
|
|
733
|
+
catch (e) {
|
|
734
|
+
fail(sp, 'Failed to save', e);
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
// ── build: a starter playbook, in THIS tenant's words ──────────────────────
|
|
738
|
+
exports.formsCommand
|
|
739
|
+
.command('build')
|
|
740
|
+
.description("Draft a form for this business — in its own industry's words")
|
|
741
|
+
.option('--intent <kind>', 'intake | feedback | lead_qualify | onboarding', 'intake')
|
|
742
|
+
.option('--save', 'Save it as a draft (nothing is saved without this)')
|
|
743
|
+
.option('--title <text>', 'Title to save it under')
|
|
744
|
+
.option('--json', 'Output as JSON')
|
|
745
|
+
.action(async (opts) => {
|
|
746
|
+
requireAuth();
|
|
747
|
+
const sp = (0, ora_1.default)('Drafting...').start();
|
|
748
|
+
try {
|
|
749
|
+
// ⛔ kb_sub_code IS NOT PASSED. The verb resolves THIS tenant's industry
|
|
750
|
+
// from their own company row; sending one from the CLI would be guessing
|
|
751
|
+
// at someone's business. See mcp/tools/form_session_verbs._tenant_kb_sub_code.
|
|
752
|
+
const draft = await callVerb('playbook.suggest', { intent: opts.intent });
|
|
753
|
+
if (draft.status === 'invalid' || draft.status === 'error') {
|
|
754
|
+
sp.fail(chalk_1.default.red(String(draft.summary ?? 'Could not draft it')));
|
|
755
|
+
process.exitCode = 1;
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
if ((0, json_output_1.isJsonOutput)(opts) && !opts.save) {
|
|
759
|
+
sp.stop();
|
|
760
|
+
console.log(JSON.stringify(draft, null, 2));
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
sp.stop();
|
|
764
|
+
const preview = (draft.preview ?? draft.steps ?? []);
|
|
765
|
+
console.log(` ${chalk_1.default.bold(String(draft.title ?? opts.intent))} ${chalk_1.default.dim(`(${preview.length} steps)`)}`);
|
|
766
|
+
preview.forEach((st, i) => {
|
|
767
|
+
console.log(` ${String(i + 1).padStart(2, '0')} ${chalk_1.default.dim(String(st.kind ?? 'question').padEnd(8))} ${st.prompt ?? ''}`);
|
|
768
|
+
});
|
|
769
|
+
if (!opts.save) {
|
|
770
|
+
console.log(chalk_1.default.dim('\n Nothing saved. Add --save to keep it as a draft.'));
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
const sp2 = (0, ora_1.default)('Saving as a draft...').start();
|
|
774
|
+
const saved = await callVerbConfirmed('playbook.save', {
|
|
775
|
+
title: opts.title ?? String(draft.title ?? `${opts.intent} form`),
|
|
776
|
+
steps: draft.steps ?? [],
|
|
777
|
+
...(draft.outcomes ? { outcomes: draft.outcomes } : {}),
|
|
778
|
+
});
|
|
779
|
+
if (saved.status === 'invalid' || saved.status === 'error') {
|
|
780
|
+
sp2.fail(chalk_1.default.red(String(saved.summary ?? 'Could not save it')));
|
|
781
|
+
process.exitCode = 1;
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
sp2.succeed(chalk_1.default.green(`Saved as a draft (${saved.playbook_id ?? saved.form_id ?? '?'})`));
|
|
785
|
+
console.log(chalk_1.default.dim(` Publish when it's ready: solid forms publish ${saved.playbook_id ?? saved.form_id ?? '<id>'}`));
|
|
786
|
+
}
|
|
787
|
+
catch (e) {
|
|
788
|
+
fail(sp, 'Failed to build', e);
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
// ── walk: answer a form from the terminal, exactly as an agent does ─────────
|
|
792
|
+
//
|
|
793
|
+
// The CLI equivalent of the Live Form bench: next_question → capture → submit,
|
|
794
|
+
// the same three verbs a voice agent uses mid-call. Scriptable on purpose, so
|
|
795
|
+
// an AI can drive it non-interactively.
|
|
796
|
+
exports.formsCommand
|
|
797
|
+
.command('walk <id>')
|
|
798
|
+
.description('Answer a form the way an agent does — next question, capture, submit')
|
|
799
|
+
.option('--provider <name>', 'Form provider', 'native')
|
|
800
|
+
.option('--session <ref>', 'Continue an existing session')
|
|
801
|
+
.option('--question <id>', 'The question being answered (with --answer)')
|
|
802
|
+
.option('--answer <value>', 'Record an answer, then show what is next')
|
|
803
|
+
.option('--submit', 'Finish and persist the response')
|
|
804
|
+
.option('--branching', 'Walk the PLAYBOOK — branches, message and action steps, skips honoured')
|
|
805
|
+
.option('--answers <json>', 'With --branching: answers so far, as {"q1":"yes"} or @file.json')
|
|
806
|
+
.option('--json', 'Output as JSON')
|
|
807
|
+
.action(async (id, opts) => {
|
|
808
|
+
requireAuth();
|
|
809
|
+
const base = { form_id: String(id), provider: opts.provider };
|
|
810
|
+
const sp = (0, ora_1.default)('Working...').start();
|
|
811
|
+
// ⛔ BRANCHES DO NOT TRAVEL THROUGH form.next_question. That verb walks by
|
|
812
|
+
// POSITION — a branched playbook served through it asks every question in
|
|
813
|
+
// order (the superset), which is safe for a form but wrong for a quiz whose
|
|
814
|
+
// skips are the point. playbook.next_step is the real walk, and it takes the
|
|
815
|
+
// answers as a MAP rather than a session ref, so this mode is stateless and
|
|
816
|
+
// scriptable: pass back what you have, get the next step.
|
|
817
|
+
if (opts.branching) {
|
|
818
|
+
let answered = {};
|
|
819
|
+
if (opts.answers) {
|
|
820
|
+
const parsed = (0, json_arg_1.parseJsonArg)('answers', opts.answers);
|
|
821
|
+
if (parsed && typeof parsed === 'object')
|
|
822
|
+
answered = parsed;
|
|
823
|
+
}
|
|
824
|
+
if (opts.answer !== undefined) {
|
|
825
|
+
if (!opts.question) {
|
|
826
|
+
sp.fail(chalk_1.default.red('--answer needs --question <id>'));
|
|
827
|
+
process.exitCode = 1;
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
answered[String(opts.question)] = opts.answer;
|
|
831
|
+
}
|
|
832
|
+
try {
|
|
833
|
+
const out = await callVerb('playbook.next_step', {
|
|
834
|
+
playbook_id: String(id), answered,
|
|
835
|
+
});
|
|
836
|
+
sp.stop();
|
|
837
|
+
if (out.status === 'error' || out.status === 'invalid') {
|
|
838
|
+
console.error(chalk_1.default.red(String(out.summary ?? 'Could not walk it')));
|
|
839
|
+
process.exitCode = 1;
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
if ((0, json_output_1.isJsonOutput)(opts)) {
|
|
843
|
+
console.log(JSON.stringify({ ...out, answered }, null, 2));
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
console.log(` ${chalk_1.default.bold(String(out.title ?? id))} ${chalk_1.default.dim(`${out.answered_count ?? 0} answered`)}`);
|
|
847
|
+
if (out.complete) {
|
|
848
|
+
console.log(chalk_1.default.green(' The playbook is finished.'));
|
|
849
|
+
console.log(chalk_1.default.dim(` Score it: solid forms quiz ${id} --answers '${JSON.stringify(answered)}'`));
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
const step = (out.step ?? {});
|
|
853
|
+
const kind = String(step.kind ?? 'question');
|
|
854
|
+
console.log(` ${chalk_1.default.dim(kind.padEnd(8))} ${chalk_1.default.bold(String(step.prompt ?? ''))}`);
|
|
855
|
+
if (kind === 'action') {
|
|
856
|
+
// The verb NAME comes back; the AGENT calls it. Say so rather than
|
|
857
|
+
// implying the CLI just did something.
|
|
858
|
+
console.log(` ${chalk_1.default.dim('the agent would call')} ${chalk_1.default.cyan(String(step.verb ?? '—'))}`);
|
|
859
|
+
if (step.verb_args && Object.keys(step.verb_args).length) {
|
|
860
|
+
console.log(` ${chalk_1.default.dim('with')} ${JSON.stringify(step.verb_args)}`);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
if (Array.isArray(step.choices) && step.choices.length) {
|
|
864
|
+
console.log(` ${chalk_1.default.dim('choices:')} ${step.choices.join(' / ')}`);
|
|
865
|
+
}
|
|
866
|
+
const next = { ...answered, [String(step.id ?? 'q')]: '...' };
|
|
867
|
+
console.log(chalk_1.default.dim(`\n Answer it: solid forms walk ${id} --branching --question ${step.id}` +
|
|
868
|
+
` --answer "..." --answers '${JSON.stringify(answered)}'`));
|
|
869
|
+
void next;
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
catch (e) {
|
|
873
|
+
fail(sp, 'Failed to walk the playbook', e);
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
try {
|
|
878
|
+
if (opts.submit) {
|
|
879
|
+
const out = await callVerbConfirmed('form.submit', {
|
|
880
|
+
...base, ...(opts.session ? { session_ref: opts.session } : {}),
|
|
881
|
+
});
|
|
882
|
+
sp.stop();
|
|
883
|
+
if (out.status === 'invalid') {
|
|
884
|
+
console.error(chalk_1.default.yellow(String(out.summary)));
|
|
885
|
+
process.exitCode = 1;
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
if ((0, json_output_1.isJsonOutput)(opts)) {
|
|
889
|
+
console.log(JSON.stringify(out, null, 2));
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
console.log(chalk_1.default.green(` Submitted — response ${out.response_id ?? ''}`));
|
|
893
|
+
if (out.contact_id) {
|
|
894
|
+
console.log(` ${chalk_1.default.dim('landed on contact')} ${out.contact_id}` +
|
|
895
|
+
(out.contact_created ? chalk_1.default.dim(' (new lead)') : ''));
|
|
896
|
+
}
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
let session = opts.session;
|
|
900
|
+
if (opts.answer !== undefined) {
|
|
901
|
+
if (!opts.question) {
|
|
902
|
+
sp.fail(chalk_1.default.red('--answer needs --question <id>'));
|
|
903
|
+
process.exitCode = 1;
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
const cap = await callVerbConfirmed('form.capture', {
|
|
907
|
+
...base, question_id: String(opts.question), value: opts.answer,
|
|
908
|
+
channel: 'cli', ...(session ? { session_ref: session } : {}),
|
|
909
|
+
});
|
|
910
|
+
session = String(cap.session_ref ?? session ?? '');
|
|
911
|
+
}
|
|
912
|
+
const next = await callVerb('form.next_question', {
|
|
913
|
+
...base, ...(session ? { session_ref: session } : {}),
|
|
914
|
+
});
|
|
915
|
+
sp.stop();
|
|
916
|
+
if ((0, json_output_1.isJsonOutput)(opts)) {
|
|
917
|
+
console.log(JSON.stringify({ ...next, session_ref: session }, null, 2));
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
if (session)
|
|
921
|
+
console.log(` ${chalk_1.default.dim('session')} ${session}`);
|
|
922
|
+
if (next.complete) {
|
|
923
|
+
console.log(chalk_1.default.green(' Every question answered.'));
|
|
924
|
+
console.log(chalk_1.default.dim(` Finish it: solid forms walk ${id} --session ${session ?? ''} --submit`));
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
const q = (next.next_question ?? {});
|
|
928
|
+
console.log(` ${chalk_1.default.bold(String(q.prompt ?? ''))}`);
|
|
929
|
+
console.log(` ${chalk_1.default.dim(`${q.kind}${q.required ? ', required' : ''}`)}`);
|
|
930
|
+
if (Array.isArray(q.choices) && q.choices.length) {
|
|
931
|
+
console.log(` ${chalk_1.default.dim('choices:')} ${q.choices.join(' / ')}`);
|
|
932
|
+
}
|
|
933
|
+
console.log(chalk_1.default.dim(`\n Answer it: solid forms walk ${id} --question ${q.id}` +
|
|
934
|
+
` --answer "..."${session ? ` --session ${session}` : ''}`));
|
|
935
|
+
}
|
|
936
|
+
catch (e) {
|
|
937
|
+
fail(sp, 'Failed', e);
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
// ── quiz: score a run and see which band it lands in ───────────────────────
|
|
941
|
+
//
|
|
942
|
+
// A quiz's scoring is server-side ONLY — the answer key never travels with the
|
|
943
|
+
// questions, which is right, and it meant a tenant could not check their own
|
|
944
|
+
// bands without finding a real respondent. This scores a hypothetical run.
|
|
945
|
+
exports.formsCommand
|
|
946
|
+
.command('quiz <id>')
|
|
947
|
+
.description('Score a set of answers against a quiz and show the outcome band')
|
|
948
|
+
.requiredOption('--answers <json>', 'The answers to score: \'{"q1":"yes"}\' or @file.json')
|
|
949
|
+
.option('--json', 'Output as JSON')
|
|
950
|
+
.action(async (id, opts) => {
|
|
951
|
+
requireAuth();
|
|
952
|
+
const answered = (0, json_arg_1.parseJsonArg)('answers', opts.answers);
|
|
953
|
+
if (!answered || typeof answered !== 'object' || Array.isArray(answered)) {
|
|
954
|
+
console.error(chalk_1.default.red('--answers must be a JSON object of question_id -> value'));
|
|
955
|
+
console.error(chalk_1.default.dim(' e.g. --answers \'{"experience":"none","budget":"high"}\''));
|
|
956
|
+
process.exitCode = 1;
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
const sp = (0, ora_1.default)('Scoring...').start();
|
|
960
|
+
let out;
|
|
961
|
+
try {
|
|
962
|
+
// kb_sub_code omitted — the saved playbook carries its own industry.
|
|
963
|
+
out = await callVerb('quiz.result', { playbook_id: String(id), answered });
|
|
964
|
+
}
|
|
965
|
+
catch (e) {
|
|
966
|
+
fail(sp, 'Failed to score it', e);
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
if (out.status === 'error' || out.status === 'invalid') {
|
|
970
|
+
sp.fail(chalk_1.default.red(String(out.summary ?? 'Could not score it')));
|
|
971
|
+
process.exitCode = 1;
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
if ((0, json_output_1.isJsonOutput)(opts)) {
|
|
975
|
+
sp.stop();
|
|
976
|
+
console.log(JSON.stringify(out, null, 2));
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
sp.stop();
|
|
980
|
+
console.log(` ${chalk_1.default.bold(String(out.title ?? id))}`);
|
|
981
|
+
if (out.is_quiz === false) {
|
|
982
|
+
console.log(chalk_1.default.yellow(' This playbook does not score — no points on its steps and no outcome bands.'));
|
|
983
|
+
console.log(chalk_1.default.dim(' Add scoring to its steps, or walk it as a form: solid forms walk ' + id));
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
const pct = out.percent !== undefined && out.percent !== null ? ` (${Math.round(Number(out.percent))}%)` : '';
|
|
987
|
+
console.log(` ${chalk_1.default.dim('score')} ${out.score ?? 0}${out.max !== undefined ? `/${out.max}` : ''}${pct}`);
|
|
988
|
+
const band = (out.outcome ?? null);
|
|
989
|
+
if (!band || (!band.title && !band.message)) {
|
|
990
|
+
// ⛔ A SCORE THAT LANDS NOWHERE IS THE DEFECT WORTH SEEING. The dashboard
|
|
991
|
+
// flags it too: a respondent who finishes and is told nothing.
|
|
992
|
+
console.log(chalk_1.default.yellow(' ⚠ This score lands in NO outcome band — a respondent would be told nothing.'));
|
|
993
|
+
console.log(chalk_1.default.dim(' Check the band bounds cover every possible score.'));
|
|
994
|
+
process.exitCode = 1;
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
if (band.title)
|
|
998
|
+
console.log(` ${chalk_1.default.green(String(band.title))}`);
|
|
999
|
+
if (band.message)
|
|
1000
|
+
console.log(` ${String(band.message)}`);
|
|
1001
|
+
if (band.verb) {
|
|
1002
|
+
console.log(` ${chalk_1.default.dim('the agent would then call')} ${chalk_1.default.cyan(String(band.verb))}` +
|
|
1003
|
+
(band.verb_args && Object.keys(band.verb_args).length
|
|
1004
|
+
? ` ${chalk_1.default.dim(JSON.stringify(band.verb_args))}` : ''));
|
|
1005
|
+
}
|
|
1006
|
+
});
|
|
1007
|
+
// ── vocabulary: the nouns this tenant's customers actually use ──────────────
|
|
1008
|
+
exports.formsCommand
|
|
1009
|
+
.command('vocabulary')
|
|
1010
|
+
.alias('words')
|
|
1011
|
+
.description("This business's own words — what its customers are called, and its appointments")
|
|
1012
|
+
.option('--lang <code>', 'Language', 'en')
|
|
1013
|
+
.option('--json', 'Output as JSON')
|
|
1014
|
+
.action(async (opts) => {
|
|
1015
|
+
requireAuth();
|
|
1016
|
+
const sp = (0, ora_1.default)('Loading...').start();
|
|
1017
|
+
try {
|
|
1018
|
+
// kb_sub_code omitted deliberately — the verb resolves this tenant's own.
|
|
1019
|
+
const out = await callVerb('playbook.vocabulary', { lang: opts.lang });
|
|
1020
|
+
if ((0, json_output_1.isJsonOutput)(opts)) {
|
|
1021
|
+
sp.stop();
|
|
1022
|
+
console.log(JSON.stringify(out, null, 2));
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
sp.stop();
|
|
1026
|
+
const terms = (out.terms ?? {});
|
|
1027
|
+
console.log(` ${chalk_1.default.dim('industry code')} ${out.kb_sub_code ?? chalk_1.default.dim('none set')}`);
|
|
1028
|
+
for (const [k, v] of Object.entries(terms)) {
|
|
1029
|
+
console.log(` ${chalk_1.default.dim(k.padEnd(16))} ${v}`);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
catch (e) {
|
|
1033
|
+
fail(sp, 'Failed to load the vocabulary', e);
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
277
1036
|
const command_kit_1 = require("../lib/command-kit");
|
|
278
1037
|
(0, command_kit_1.appendExamples)(exports.formsCommand, [
|
|
279
|
-
{ cmd: 'solid forms list', why: '
|
|
280
|
-
{ cmd: 'solid forms
|
|
281
|
-
{ cmd: 'solid forms
|
|
282
|
-
{ cmd: 'solid forms
|
|
1038
|
+
{ cmd: 'solid forms list', why: 'Every form with its lifecycle, across every provider' },
|
|
1039
|
+
{ cmd: 'solid forms build --intent intake --save', why: "A starter in this industry's own words" },
|
|
1040
|
+
{ cmd: 'solid forms publish <id>', why: 'A draft starts taking answers' },
|
|
1041
|
+
{ cmd: 'solid forms link <id>', why: "The form's shareable URL, bare on stdout" },
|
|
1042
|
+
{ cmd: 'solid forms moments set appointment_booked --form <id>', why: 'Make something actually send it' },
|
|
1043
|
+
{ cmd: 'solid forms walk <id>', why: 'Answer it the way an agent does, from the terminal' },
|
|
1044
|
+
{ cmd: 'solid forms responses <id>', why: 'What people actually answered' },
|
|
283
1045
|
]);
|
|
284
1046
|
//# sourceMappingURL=forms.js.map
|