@solidnumber/cli 2.15.0 → 2.16.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/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.js +649 -52
- package/dist/commands/forms.js.map +1 -1
- package/dist/commands/transaction.js +7 -1
- package/dist/commands/transaction.js.map +1 -1
- package/dist/commands/verbs.js +17 -2
- package/dist/commands/verbs.js.map +1 -1
- package/dist/index.js +0 -0
- package/dist/lib/browser-install.d.ts.map +1 -1
- package/dist/lib/browser-install.js +8 -3
- package/dist/lib/browser-install.js.map +1 -1
- package/package.json +3 -3
- package/platform-docs/llms.txt +5 -5
package/dist/commands/forms.js
CHANGED
|
@@ -58,31 +58,54 @@ exports.formsCommand = new commander_1.Command('forms')
|
|
|
58
58
|
.alias('surveys')
|
|
59
59
|
.description('Forms & surveys — CRUD, AI generate, export CSV/Excel/PDF');
|
|
60
60
|
{
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
// ⛔ `list` GOES THROUGH VERBS. It used to read /api/v1/surveys directly, so it
|
|
62
|
+
// showed a different set than `solid forms status` (no lifecycle, native only,
|
|
63
|
+
// and blind to lead forms). Two commands in one CLI disagreeing about what
|
|
64
|
+
// forms exist is worse than either being wrong. One seam, one answer.
|
|
65
|
+
const listCmd = exports.formsCommand.command('list').alias('ls')
|
|
66
|
+
.description('Every form, with its lifecycle — across every connected provider');
|
|
67
|
+
listCmd.option('--provider <name>', 'Only this provider');
|
|
68
|
+
listCmd.option('--json', 'Output as JSON');
|
|
64
69
|
listCmd.action(async (opts) => {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return;
|
|
70
|
+
requireAuth();
|
|
71
|
+
const s2 = (0, ora_1.default)('Loading forms...').start();
|
|
72
|
+
try {
|
|
73
|
+
const providers = opts.provider
|
|
74
|
+
? [opts.provider]
|
|
75
|
+
: ((await callVerb('form.sources')).connected ?? [])
|
|
76
|
+
.map((c) => String(c.provider));
|
|
77
|
+
const rows = [];
|
|
78
|
+
for (const provider of providers.length ? providers : ['native']) {
|
|
79
|
+
try {
|
|
80
|
+
const out = await callVerb('form.list', { provider });
|
|
81
|
+
for (const f of (out.forms ?? [])) {
|
|
82
|
+
rows.push({ ...f, provider });
|
|
83
|
+
}
|
|
80
84
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
catch { /* one provider down must not blank the list */ }
|
|
86
|
+
}
|
|
87
|
+
if ((0, json_output_1.isJsonOutput)(opts)) {
|
|
88
|
+
s2.stop();
|
|
89
|
+
console.log(JSON.stringify({ forms: rows }, null, 2));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
s2.stop();
|
|
93
|
+
if (!rows.length) {
|
|
94
|
+
console.log(chalk_1.default.dim(' No forms yet.'));
|
|
95
|
+
console.log(chalk_1.default.dim(' Build one: solid forms build --intent intake --save'));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
for (const f of rows) {
|
|
99
|
+
const state = lifecycleOf(f);
|
|
100
|
+
const answered = Number(f.response_count ?? 0);
|
|
101
|
+
console.log(` ${chalk_1.default.bold(String(f.external_id).padEnd(5))} ${lifecycleTag(state).padEnd(16)} ` +
|
|
102
|
+
`${String(f.title ?? '').slice(0, 34).padEnd(34)} ` +
|
|
103
|
+
`${chalk_1.default.dim(`${answered} answered`)} ${chalk_1.default.dim(String(f.provider))}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
fail(s2, 'Failed to load forms', e);
|
|
108
|
+
}
|
|
86
109
|
});
|
|
87
110
|
}
|
|
88
111
|
exports.formsCommand
|
|
@@ -182,37 +205,52 @@ exports.formsCommand
|
|
|
182
205
|
});
|
|
183
206
|
exports.formsCommand
|
|
184
207
|
.command('optimize <id>')
|
|
185
|
-
.description('
|
|
186
|
-
.
|
|
208
|
+
.description('ADA reviews the questions — suggestions only, nothing changes')
|
|
209
|
+
.option('--json', 'Output as JSON')
|
|
210
|
+
.action(async (id, opts) => {
|
|
187
211
|
requireAuth();
|
|
188
|
-
const
|
|
212
|
+
const sp = (0, ora_1.default)('Looking it over...').start();
|
|
189
213
|
try {
|
|
190
|
-
await
|
|
191
|
-
|
|
214
|
+
const out = await callVerb('survey.optimize', { survey_id: Number(id) });
|
|
215
|
+
if ((0, json_output_1.isJsonOutput)(opts)) {
|
|
216
|
+
sp.stop();
|
|
217
|
+
console.log(JSON.stringify(out, null, 2));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
sp.stop();
|
|
221
|
+
const analysis = (out.analysis ?? {});
|
|
222
|
+
const suggestions = (analysis.suggested_improvements ?? []);
|
|
223
|
+
if (!suggestions.length) {
|
|
224
|
+
console.log(chalk_1.default.dim(' Nothing to change — it reads well.'));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
console.log(chalk_1.default.dim(' Suggestions only — nothing changes until you change it.'));
|
|
228
|
+
for (const sg of suggestions) {
|
|
229
|
+
console.log(` ${chalk_1.default.yellow('*')} ${sg.issue}`);
|
|
230
|
+
if (sg.fix)
|
|
231
|
+
console.log(` ${chalk_1.default.dim('fix:')} ${sg.fix}`);
|
|
232
|
+
if (sg.impact)
|
|
233
|
+
console.log(` ${chalk_1.default.dim('why:')} ${sg.impact}`);
|
|
234
|
+
}
|
|
192
235
|
}
|
|
193
236
|
catch (e) {
|
|
194
|
-
fail(
|
|
237
|
+
fail(sp, 'Failed to look it over', e);
|
|
195
238
|
}
|
|
196
239
|
});
|
|
197
240
|
exports.formsCommand
|
|
198
241
|
.command('analyze <id>')
|
|
199
|
-
.description('
|
|
242
|
+
.description('What the answers add up to')
|
|
200
243
|
.option('--json', 'Output as JSON')
|
|
201
|
-
.action(async (id
|
|
244
|
+
.action(async (id) => {
|
|
202
245
|
requireAuth();
|
|
203
|
-
const
|
|
246
|
+
const sp = (0, ora_1.default)('Analyzing...').start();
|
|
204
247
|
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));
|
|
248
|
+
const out = await callVerb('survey.analyze', { survey_id: Number(id) });
|
|
249
|
+
sp.stop();
|
|
250
|
+
console.log(JSON.stringify(out, null, 2));
|
|
213
251
|
}
|
|
214
252
|
catch (e) {
|
|
215
|
-
fail(
|
|
253
|
+
fail(sp, 'Failed to analyze', e);
|
|
216
254
|
}
|
|
217
255
|
});
|
|
218
256
|
exports.formsCommand
|
|
@@ -232,17 +270,47 @@ exports.formsCommand
|
|
|
232
270
|
});
|
|
233
271
|
exports.formsCommand
|
|
234
272
|
.command('embed <id>')
|
|
235
|
-
.description('
|
|
236
|
-
.
|
|
273
|
+
.description('Paste-ready embed for a live form — iframe, link, or button')
|
|
274
|
+
.option('--provider <name>', 'Form provider', 'native')
|
|
275
|
+
.option('--as <kind>', 'iframe | link | button', 'iframe')
|
|
276
|
+
.action(async (id, opts) => {
|
|
237
277
|
requireAuth();
|
|
238
|
-
|
|
278
|
+
// ⛔ THE DOOR, NOT THE LEGACY LINK. This read /api/v1/surveys/<id>/embed —
|
|
279
|
+
// the pre-lifecycle system's public URL — so after the standing door shipped
|
|
280
|
+
// (2026-08-20) `solid forms embed` was handing out the OLD address while
|
|
281
|
+
// `solid forms link` handed out the new one. Two commands, two answers, one
|
|
282
|
+
// of them wrong.
|
|
283
|
+
const sp = (0, ora_1.default)('Building the embed...').start();
|
|
284
|
+
let out;
|
|
239
285
|
try {
|
|
240
|
-
|
|
241
|
-
s.succeed(chalk_1.default.green('Embed'));
|
|
242
|
-
console.log(JSON.stringify(res.data, null, 2));
|
|
286
|
+
out = await callVerb('form.describe', { form_id: String(id), provider: opts.provider });
|
|
243
287
|
}
|
|
244
288
|
catch (e) {
|
|
245
|
-
fail(
|
|
289
|
+
fail(sp, 'Failed to build the embed', e);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const url = out.public_url ?? out.public_path;
|
|
293
|
+
if (!url) {
|
|
294
|
+
sp.fail(chalk_1.default.yellow(`No public address — this form is ${String(out.lifecycle ?? 'not live')}.`));
|
|
295
|
+
console.error(chalk_1.default.dim(' Only a live form can be embedded. Publish it first:'));
|
|
296
|
+
console.error(chalk_1.default.dim(` solid forms publish ${id}`));
|
|
297
|
+
process.exitCode = 1;
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
sp.stop();
|
|
301
|
+
const title = String(out.title ?? 'Form');
|
|
302
|
+
const kind = String(opts.as).toLowerCase();
|
|
303
|
+
if (kind === 'link') {
|
|
304
|
+
console.log(`<a href="${url}">${title}</a>`);
|
|
305
|
+
}
|
|
306
|
+
else if (kind === 'button') {
|
|
307
|
+
console.log(`<a href="${url}" style="display:inline-block;padding:12px 20px;border-radius:8px;` +
|
|
308
|
+
`background:#0f5346;color:#fff;text-decoration:none;font-weight:600">${title}</a>`);
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
console.log(`<!-- ${title} — answers land in your CRM -->\n` +
|
|
312
|
+
`<iframe src="${url}" title="${title}" width="100%" height="640" ` +
|
|
313
|
+
`style="border:0;border-radius:12px" loading="lazy"></iframe>`);
|
|
246
314
|
}
|
|
247
315
|
});
|
|
248
316
|
exports.formsCommand
|
|
@@ -274,11 +342,540 @@ exports.formsCommand
|
|
|
274
342
|
fail(s, 'Failed', e);
|
|
275
343
|
}
|
|
276
344
|
});
|
|
345
|
+
// ── the verb seam ──────────────────────────────────────────────────────────
|
|
346
|
+
//
|
|
347
|
+
// ⛔ EVERYTHING BELOW CALLS VERBS, NOT REST. The commands above this line wrap
|
|
348
|
+
// /api/v1/surveys — the authoring endpoints that predate the forms platform —
|
|
349
|
+
// and they are why `solid forms` knew nothing about the lifecycle, the
|
|
350
|
+
// respondent-facing reads, or a form's public address. New work goes through
|
|
351
|
+
// the verb layer so the CLI, the dashboard, MCP and an agent on a live call
|
|
352
|
+
// all get the same validation, consent and tenant scoping.
|
|
353
|
+
//
|
|
354
|
+
// ⛔ NEVER SEND company_id. The backend binds the tenant from the authenticated
|
|
355
|
+
// principal; a CLI that could pass one is a CLI that could pass someone else's.
|
|
356
|
+
async function callVerb(name, payload = {}) {
|
|
357
|
+
// ⛔ ONLY THE VERB IS HYPHENATED, NEVER THE NAMESPACE. The backend routes
|
|
358
|
+
// `<ns>/<verb-with-hyphens>` (controllers/agent_verb_index.py::_verb_http_endpoint),
|
|
359
|
+
// and plenty of namespaces carry underscores — call_flow, comms_workflow,
|
|
360
|
+
// agent_config. Hyphenating the whole name would 404 every one of them.
|
|
361
|
+
const [ns, ...rest] = name.split('.');
|
|
362
|
+
const verb = rest.join('.').replace(/_/g, '-');
|
|
363
|
+
const endpoint = verb ? `/api/v1/agent/${ns}/${verb}` : `/api/v1/agent/${ns}`;
|
|
364
|
+
const res = await api_client_1.apiClient.post(endpoint, payload);
|
|
365
|
+
const body = res.data;
|
|
366
|
+
// The envelope is {ok, result} on some surfaces and the bare result on others.
|
|
367
|
+
return (body && typeof body === 'object' && 'result' in body ? body.result : body);
|
|
368
|
+
}
|
|
369
|
+
/** A write verb. Consent travels as `confirm: true` — the backend refuses without it. */
|
|
370
|
+
async function callVerbConfirmed(name, payload = {}) {
|
|
371
|
+
return callVerb(name, { ...payload, confirm: true });
|
|
372
|
+
}
|
|
373
|
+
function lifecycleOf(f) {
|
|
374
|
+
if ((f.status ?? 'published') === 'draft')
|
|
375
|
+
return 'draft';
|
|
376
|
+
return f.is_active === false ? 'paused' : 'live';
|
|
377
|
+
}
|
|
378
|
+
function lifecycleTag(state) {
|
|
379
|
+
return state === 'live' ? chalk_1.default.green('live')
|
|
380
|
+
: state === 'draft' ? chalk_1.default.yellow('draft')
|
|
381
|
+
: chalk_1.default.dim('paused');
|
|
382
|
+
}
|
|
383
|
+
exports.formsCommand
|
|
384
|
+
.command('describe <id>')
|
|
385
|
+
.description('The questions as a respondent meets them, plus lifecycle + public link')
|
|
386
|
+
.option('--provider <name>', 'Form provider', 'native')
|
|
387
|
+
.option('--json', 'Output as JSON')
|
|
388
|
+
.action(async (id, opts) => {
|
|
389
|
+
requireAuth();
|
|
390
|
+
const s = (0, ora_1.default)(`Reading ${id}...`).start();
|
|
391
|
+
try {
|
|
392
|
+
const out = await callVerb('form.describe', { form_id: String(id), provider: opts.provider });
|
|
393
|
+
if ((0, json_output_1.isJsonOutput)(opts)) {
|
|
394
|
+
s.stop();
|
|
395
|
+
console.log(JSON.stringify(out, null, 2));
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
s.stop();
|
|
399
|
+
const questions = (out.questions ?? []);
|
|
400
|
+
console.log(` ${chalk_1.default.bold(String(out.title ?? id))} ${lifecycleTag(String(out.lifecycle ?? ''))}`);
|
|
401
|
+
if (out.public_url)
|
|
402
|
+
console.log(` ${chalk_1.default.dim('public link')} ${chalk_1.default.cyan(String(out.public_url))}`);
|
|
403
|
+
else if (out.public_path)
|
|
404
|
+
console.log(` ${chalk_1.default.dim('public path')} ${String(out.public_path)}`);
|
|
405
|
+
console.log(` ${chalk_1.default.dim(`${questions.length} question${questions.length === 1 ? '' : 's'}`)}`);
|
|
406
|
+
questions.forEach((q, i) => {
|
|
407
|
+
const req = q.required ? chalk_1.default.dim(' (required)') : '';
|
|
408
|
+
console.log(` ${String(i + 1).padStart(2, '0')} ${chalk_1.default.dim(String(q.kind).padEnd(8))} ${q.prompt}${req}`);
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
catch (e) {
|
|
412
|
+
fail(s, 'Failed to read the form', e);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
exports.formsCommand
|
|
416
|
+
.command('publish <id>')
|
|
417
|
+
.description('Publish a draft — it starts taking answers')
|
|
418
|
+
.action(async (id) => {
|
|
419
|
+
requireAuth();
|
|
420
|
+
const s = (0, ora_1.default)(`Publishing ${id}...`).start();
|
|
421
|
+
try {
|
|
422
|
+
const out = await callVerbConfirmed('survey.publish', { survey_id: Number(id) });
|
|
423
|
+
if (out.status === 'error' || out.status === 'not_found') {
|
|
424
|
+
s.fail(chalk_1.default.red(String(out.summary ?? 'Could not publish')));
|
|
425
|
+
process.exit(1);
|
|
426
|
+
}
|
|
427
|
+
s.succeed(chalk_1.default.green('Published — live and taking answers'));
|
|
428
|
+
try {
|
|
429
|
+
const d = await callVerb('form.describe', { form_id: String(id), provider: 'native' });
|
|
430
|
+
if (d.public_url)
|
|
431
|
+
console.log(` ${chalk_1.default.dim('public link')} ${chalk_1.default.cyan(String(d.public_url))}`);
|
|
432
|
+
}
|
|
433
|
+
catch { /* the publish still succeeded */ }
|
|
434
|
+
}
|
|
435
|
+
catch (e) {
|
|
436
|
+
fail(s, 'Failed to publish', e);
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
exports.formsCommand
|
|
440
|
+
.command('pause <id>')
|
|
441
|
+
.description('Stop taking new answers. Every answer already given is kept')
|
|
442
|
+
.action(async (id) => {
|
|
443
|
+
requireAuth();
|
|
444
|
+
const s = (0, ora_1.default)(`Pausing ${id}...`).start();
|
|
445
|
+
try {
|
|
446
|
+
const out = await callVerbConfirmed('survey.set_live', { survey_id: Number(id), live: false });
|
|
447
|
+
if (out.status === 'invalid' || out.status === 'error') {
|
|
448
|
+
s.fail(chalk_1.default.red(String(out.summary ?? 'Could not pause it')));
|
|
449
|
+
process.exit(1);
|
|
450
|
+
}
|
|
451
|
+
s.succeed(chalk_1.default.green('Paused — every answer kept, no new ones'));
|
|
452
|
+
}
|
|
453
|
+
catch (e) {
|
|
454
|
+
fail(s, 'Failed to pause', e);
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
exports.formsCommand
|
|
458
|
+
.command('resume <id>')
|
|
459
|
+
.description('Take answers again on a paused form')
|
|
460
|
+
.action(async (id) => {
|
|
461
|
+
requireAuth();
|
|
462
|
+
const s = (0, ora_1.default)(`Resuming ${id}...`).start();
|
|
463
|
+
try {
|
|
464
|
+
const out = await callVerbConfirmed('survey.set_live', { survey_id: Number(id), live: true });
|
|
465
|
+
if (out.status === 'invalid' || out.status === 'error') {
|
|
466
|
+
s.fail(chalk_1.default.red(String(out.summary ?? 'Could not resume it')));
|
|
467
|
+
process.exit(1);
|
|
468
|
+
}
|
|
469
|
+
s.succeed(chalk_1.default.green('Live — taking answers again'));
|
|
470
|
+
}
|
|
471
|
+
catch (e) {
|
|
472
|
+
fail(s, 'Failed to resume', e);
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
exports.formsCommand
|
|
476
|
+
.command('link <id>')
|
|
477
|
+
.description("The form's standing public URL — the one address you can share anywhere")
|
|
478
|
+
.option('--provider <name>', 'Form provider', 'native')
|
|
479
|
+
.action(async (id, opts) => {
|
|
480
|
+
requireAuth();
|
|
481
|
+
const s = (0, ora_1.default)('Resolving...').start();
|
|
482
|
+
// ⛔ THE EXIT LIVES OUTSIDE THE try. An intentional process.exit inside it
|
|
483
|
+
// is caught by the error handler below and reported as "Failed to resolve
|
|
484
|
+
// the link" — the command would then exit 0 with a misleading message.
|
|
485
|
+
let out;
|
|
486
|
+
try {
|
|
487
|
+
out = await callVerb('form.describe', { form_id: String(id), provider: opts.provider });
|
|
488
|
+
}
|
|
489
|
+
catch (e) {
|
|
490
|
+
fail(s, 'Failed to resolve the link', e);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (out.public_url || out.public_path) {
|
|
494
|
+
s.stop();
|
|
495
|
+
// Bare URL on stdout so it pipes into pbcopy, a QR generator, anything.
|
|
496
|
+
console.log(String(out.public_url ?? out.public_path));
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
s.fail(chalk_1.default.yellow(`No public link — this form is ${String(out.lifecycle ?? 'not live')}.`));
|
|
500
|
+
console.error(chalk_1.default.dim(' Only a live form has a public address. Publish it first:'));
|
|
501
|
+
console.error(chalk_1.default.dim(` solid forms publish ${id}`));
|
|
502
|
+
process.exit(1);
|
|
503
|
+
});
|
|
504
|
+
exports.formsCommand
|
|
505
|
+
.command('responses <id>')
|
|
506
|
+
.description('What people actually answered')
|
|
507
|
+
.option('--provider <name>', 'Form provider', 'native')
|
|
508
|
+
.option('--limit <n>', 'How many to show', '25')
|
|
509
|
+
.option('--json', 'Output as JSON')
|
|
510
|
+
.action(async (id, opts) => {
|
|
511
|
+
requireAuth();
|
|
512
|
+
const s = (0, ora_1.default)('Loading responses...').start();
|
|
513
|
+
try {
|
|
514
|
+
const out = await callVerb('form.responses', {
|
|
515
|
+
form_id: String(id), provider: opts.provider, per_page: Number(opts.limit),
|
|
516
|
+
});
|
|
517
|
+
if ((0, json_output_1.isJsonOutput)(opts)) {
|
|
518
|
+
s.stop();
|
|
519
|
+
console.log(JSON.stringify(out, null, 2));
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
s.stop();
|
|
523
|
+
const rows = (out.responses ?? []);
|
|
524
|
+
if (!rows.length) {
|
|
525
|
+
console.log(chalk_1.default.dim(' Nothing answered yet.'));
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
for (const r of rows) {
|
|
529
|
+
const when = String(r.completed_at ?? r.created_at ?? '').split('T')[0];
|
|
530
|
+
const channels = Array.isArray(r.channels) ? r.channels.join('+') : '';
|
|
531
|
+
console.log(` ${chalk_1.default.bold(String(r.external_id ?? r.id ?? ''))} ${chalk_1.default.dim(when)} ${chalk_1.default.dim(channels)}`);
|
|
532
|
+
for (const [k, v] of Object.entries((r.answers ?? {}))) {
|
|
533
|
+
console.log(` ${chalk_1.default.dim(k)}: ${String(v)}`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
catch (e) {
|
|
538
|
+
fail(s, 'Failed to load responses', e);
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
exports.formsCommand
|
|
542
|
+
.command('status')
|
|
543
|
+
.description('Every form with its lifecycle — the CLI view of the library')
|
|
544
|
+
.option('--provider <name>', 'Form provider', 'native')
|
|
545
|
+
.option('--json', 'Output as JSON')
|
|
546
|
+
.action(async (opts) => {
|
|
547
|
+
requireAuth();
|
|
548
|
+
const s = (0, ora_1.default)('Loading...').start();
|
|
549
|
+
try {
|
|
550
|
+
const out = await callVerb('form.list', { provider: opts.provider });
|
|
551
|
+
if ((0, json_output_1.isJsonOutput)(opts)) {
|
|
552
|
+
s.stop();
|
|
553
|
+
console.log(JSON.stringify(out, null, 2));
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
s.stop();
|
|
557
|
+
const rows = (out.forms ?? []);
|
|
558
|
+
if (!rows.length) {
|
|
559
|
+
console.log(chalk_1.default.dim(' No forms yet.'));
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
for (const f of rows) {
|
|
563
|
+
const state = lifecycleOf(f);
|
|
564
|
+
const answered = Number(f.response_count ?? 0);
|
|
565
|
+
console.log(` ${chalk_1.default.bold(String(f.external_id).padEnd(5))} ${lifecycleTag(state).padEnd(16)} ` +
|
|
566
|
+
`${String(f.title ?? '').padEnd(34)} ${chalk_1.default.dim(`${answered} answered`)}`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
catch (e) {
|
|
570
|
+
fail(s, 'Failed to load forms', e);
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
// ── moments: WHEN a form goes out ──────────────────────────────────────────
|
|
574
|
+
//
|
|
575
|
+
// The dashboard's most-prompted next step after publishing, and it had no CLI
|
|
576
|
+
// surface at all — so an AI could publish a form and then nothing could ever
|
|
577
|
+
// send it.
|
|
578
|
+
const momentsCmd = exports.formsCommand.command('moments')
|
|
579
|
+
.description('When a form goes out — the tenant\'s moments');
|
|
580
|
+
momentsCmd
|
|
581
|
+
.command('list', { isDefault: true })
|
|
582
|
+
.description('Every moment, and which form (if any) it sends')
|
|
583
|
+
.option('--json', 'Output as JSON')
|
|
584
|
+
.action(async (opts) => {
|
|
585
|
+
requireAuth();
|
|
586
|
+
const sp = (0, ora_1.default)('Loading moments...').start();
|
|
587
|
+
try {
|
|
588
|
+
const out = await callVerb('form.triggers');
|
|
589
|
+
if ((0, json_output_1.isJsonOutput)(opts)) {
|
|
590
|
+
sp.stop();
|
|
591
|
+
console.log(JSON.stringify(out, null, 2));
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
sp.stop();
|
|
595
|
+
const rows = (out.triggers ?? []);
|
|
596
|
+
if (!rows.length) {
|
|
597
|
+
console.log(chalk_1.default.dim(' No moments available.'));
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
for (const t of rows) {
|
|
601
|
+
const on = t.configured ? chalk_1.default.green('on ') : chalk_1.default.dim('off');
|
|
602
|
+
const what = t.configured && t.form_id
|
|
603
|
+
? `sends form ${t.form_id} ${t.stage === 'before' ? 'beforehand' : 'afterwards'}`
|
|
604
|
+
: chalk_1.default.dim('nothing set');
|
|
605
|
+
console.log(` ${on} ${String(t.event).padEnd(24)} ${what}`);
|
|
606
|
+
}
|
|
607
|
+
console.log(chalk_1.default.dim('\n Wire one: solid forms moments set <event> --form <id>'));
|
|
608
|
+
}
|
|
609
|
+
catch (e) {
|
|
610
|
+
fail(sp, 'Failed to load moments', e);
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
momentsCmd
|
|
614
|
+
.command('set <event>')
|
|
615
|
+
.description('Send a form at this moment')
|
|
616
|
+
.requiredOption('--form <id>', 'The form to send')
|
|
617
|
+
.option('--provider <name>', 'Form provider', 'native')
|
|
618
|
+
.option('--stage <when>', 'before | after')
|
|
619
|
+
.action(async (event, opts) => {
|
|
620
|
+
requireAuth();
|
|
621
|
+
const sp = (0, ora_1.default)(`Wiring ${event}...`).start();
|
|
622
|
+
try {
|
|
623
|
+
const out = await callVerbConfirmed('form.configure_trigger', {
|
|
624
|
+
event, form_id: String(opts.form), provider: opts.provider,
|
|
625
|
+
enabled: true, ...(opts.stage ? { stage: opts.stage } : {}),
|
|
626
|
+
});
|
|
627
|
+
if (out.status === 'invalid' || out.status === 'error') {
|
|
628
|
+
sp.fail(chalk_1.default.red(String(out.summary ?? 'Could not wire it')));
|
|
629
|
+
process.exitCode = 1;
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
sp.succeed(chalk_1.default.green(`${event} now sends form ${opts.form}`));
|
|
633
|
+
}
|
|
634
|
+
catch (e) {
|
|
635
|
+
fail(sp, 'Failed to wire the moment', e);
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
momentsCmd
|
|
639
|
+
.command('off <event>')
|
|
640
|
+
.description('Stop sending anything at this moment')
|
|
641
|
+
.action(async (event) => {
|
|
642
|
+
requireAuth();
|
|
643
|
+
const sp = (0, ora_1.default)(`Switching ${event} off...`).start();
|
|
644
|
+
try {
|
|
645
|
+
const out = await callVerbConfirmed('form.configure_trigger', { event, enabled: false });
|
|
646
|
+
if (out.status === 'invalid' || out.status === 'error') {
|
|
647
|
+
sp.fail(chalk_1.default.red(String(out.summary ?? 'Could not switch it off')));
|
|
648
|
+
process.exitCode = 1;
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
sp.succeed(chalk_1.default.green(`${event} sends nothing now`));
|
|
652
|
+
}
|
|
653
|
+
catch (e) {
|
|
654
|
+
fail(sp, 'Failed to switch it off', e);
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
// ── reviews: where a happy customer is sent ────────────────────────────────
|
|
658
|
+
const reviewsCmd = exports.formsCommand.command('reviews')
|
|
659
|
+
.description('Where happy customers are asked to leave a review');
|
|
660
|
+
reviewsCmd
|
|
661
|
+
.command('list', { isDefault: true })
|
|
662
|
+
.description('The review destinations this tenant has set')
|
|
663
|
+
.option('--json', 'Output as JSON')
|
|
664
|
+
.action(async (opts) => {
|
|
665
|
+
requireAuth();
|
|
666
|
+
const sp = (0, ora_1.default)('Loading destinations...').start();
|
|
667
|
+
try {
|
|
668
|
+
const out = await callVerb('review.destinations');
|
|
669
|
+
if ((0, json_output_1.isJsonOutput)(opts)) {
|
|
670
|
+
sp.stop();
|
|
671
|
+
console.log(JSON.stringify(out, null, 2));
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
sp.stop();
|
|
675
|
+
const rows = (out.destinations ?? []);
|
|
676
|
+
if (!rows.length) {
|
|
677
|
+
console.log(chalk_1.default.dim(' No review destinations yet.'));
|
|
678
|
+
console.log(chalk_1.default.dim(' Set one: solid forms reviews set google <url>'));
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
for (const d of rows)
|
|
682
|
+
console.log(` ${String(d.platform).padEnd(12)} ${d.url}`);
|
|
683
|
+
}
|
|
684
|
+
catch (e) {
|
|
685
|
+
fail(sp, 'Failed to load destinations', e);
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
reviewsCmd
|
|
689
|
+
.command('set <platform> <url>')
|
|
690
|
+
.description('Point future review requests at this link')
|
|
691
|
+
.action(async (platform, url) => {
|
|
692
|
+
requireAuth();
|
|
693
|
+
const sp = (0, ora_1.default)('Saving...').start();
|
|
694
|
+
try {
|
|
695
|
+
const out = await callVerbConfirmed('review.set_destination', { platform, url });
|
|
696
|
+
if (out.status === 'invalid' || out.status === 'error') {
|
|
697
|
+
sp.fail(chalk_1.default.red(String(out.summary ?? 'Could not save it')));
|
|
698
|
+
process.exitCode = 1;
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
sp.succeed(chalk_1.default.green(`${platform} review link saved`));
|
|
702
|
+
}
|
|
703
|
+
catch (e) {
|
|
704
|
+
fail(sp, 'Failed to save', e);
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
// ── build: a starter playbook, in THIS tenant's words ──────────────────────
|
|
708
|
+
exports.formsCommand
|
|
709
|
+
.command('build')
|
|
710
|
+
.description("Draft a form for this business — in its own industry's words")
|
|
711
|
+
.option('--intent <kind>', 'intake | feedback | lead_qualify | onboarding', 'intake')
|
|
712
|
+
.option('--save', 'Save it as a draft (nothing is saved without this)')
|
|
713
|
+
.option('--title <text>', 'Title to save it under')
|
|
714
|
+
.option('--json', 'Output as JSON')
|
|
715
|
+
.action(async (opts) => {
|
|
716
|
+
requireAuth();
|
|
717
|
+
const sp = (0, ora_1.default)('Drafting...').start();
|
|
718
|
+
try {
|
|
719
|
+
// ⛔ kb_sub_code IS NOT PASSED. The verb resolves THIS tenant's industry
|
|
720
|
+
// from their own company row; sending one from the CLI would be guessing
|
|
721
|
+
// at someone's business. See mcp/tools/form_session_verbs._tenant_kb_sub_code.
|
|
722
|
+
const draft = await callVerb('playbook.suggest', { intent: opts.intent });
|
|
723
|
+
if (draft.status === 'invalid' || draft.status === 'error') {
|
|
724
|
+
sp.fail(chalk_1.default.red(String(draft.summary ?? 'Could not draft it')));
|
|
725
|
+
process.exitCode = 1;
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
if ((0, json_output_1.isJsonOutput)(opts) && !opts.save) {
|
|
729
|
+
sp.stop();
|
|
730
|
+
console.log(JSON.stringify(draft, null, 2));
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
sp.stop();
|
|
734
|
+
const preview = (draft.preview ?? draft.steps ?? []);
|
|
735
|
+
console.log(` ${chalk_1.default.bold(String(draft.title ?? opts.intent))} ${chalk_1.default.dim(`(${preview.length} steps)`)}`);
|
|
736
|
+
preview.forEach((st, i) => {
|
|
737
|
+
console.log(` ${String(i + 1).padStart(2, '0')} ${chalk_1.default.dim(String(st.kind ?? 'question').padEnd(8))} ${st.prompt ?? ''}`);
|
|
738
|
+
});
|
|
739
|
+
if (!opts.save) {
|
|
740
|
+
console.log(chalk_1.default.dim('\n Nothing saved. Add --save to keep it as a draft.'));
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
const sp2 = (0, ora_1.default)('Saving as a draft...').start();
|
|
744
|
+
const saved = await callVerbConfirmed('playbook.save', {
|
|
745
|
+
title: opts.title ?? String(draft.title ?? `${opts.intent} form`),
|
|
746
|
+
steps: draft.steps ?? [],
|
|
747
|
+
...(draft.outcomes ? { outcomes: draft.outcomes } : {}),
|
|
748
|
+
});
|
|
749
|
+
if (saved.status === 'invalid' || saved.status === 'error') {
|
|
750
|
+
sp2.fail(chalk_1.default.red(String(saved.summary ?? 'Could not save it')));
|
|
751
|
+
process.exitCode = 1;
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
sp2.succeed(chalk_1.default.green(`Saved as a draft (${saved.playbook_id ?? saved.form_id ?? '?'})`));
|
|
755
|
+
console.log(chalk_1.default.dim(` Publish when it's ready: solid forms publish ${saved.playbook_id ?? saved.form_id ?? '<id>'}`));
|
|
756
|
+
}
|
|
757
|
+
catch (e) {
|
|
758
|
+
fail(sp, 'Failed to build', e);
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
// ── walk: answer a form from the terminal, exactly as an agent does ─────────
|
|
762
|
+
//
|
|
763
|
+
// The CLI equivalent of the Live Form bench: next_question → capture → submit,
|
|
764
|
+
// the same three verbs a voice agent uses mid-call. Scriptable on purpose, so
|
|
765
|
+
// an AI can drive it non-interactively.
|
|
766
|
+
exports.formsCommand
|
|
767
|
+
.command('walk <id>')
|
|
768
|
+
.description('Answer a form the way an agent does — next question, capture, submit')
|
|
769
|
+
.option('--provider <name>', 'Form provider', 'native')
|
|
770
|
+
.option('--session <ref>', 'Continue an existing session')
|
|
771
|
+
.option('--question <id>', 'The question being answered (with --answer)')
|
|
772
|
+
.option('--answer <value>', 'Record an answer, then show what is next')
|
|
773
|
+
.option('--submit', 'Finish and persist the response')
|
|
774
|
+
.option('--json', 'Output as JSON')
|
|
775
|
+
.action(async (id, opts) => {
|
|
776
|
+
requireAuth();
|
|
777
|
+
const base = { form_id: String(id), provider: opts.provider };
|
|
778
|
+
const sp = (0, ora_1.default)('Working...').start();
|
|
779
|
+
try {
|
|
780
|
+
if (opts.submit) {
|
|
781
|
+
const out = await callVerbConfirmed('form.submit', {
|
|
782
|
+
...base, ...(opts.session ? { session_ref: opts.session } : {}),
|
|
783
|
+
});
|
|
784
|
+
sp.stop();
|
|
785
|
+
if (out.status === 'invalid') {
|
|
786
|
+
console.error(chalk_1.default.yellow(String(out.summary)));
|
|
787
|
+
process.exitCode = 1;
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
if ((0, json_output_1.isJsonOutput)(opts)) {
|
|
791
|
+
console.log(JSON.stringify(out, null, 2));
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
console.log(chalk_1.default.green(` Submitted — response ${out.response_id ?? ''}`));
|
|
795
|
+
if (out.contact_id) {
|
|
796
|
+
console.log(` ${chalk_1.default.dim('landed on contact')} ${out.contact_id}` +
|
|
797
|
+
(out.contact_created ? chalk_1.default.dim(' (new lead)') : ''));
|
|
798
|
+
}
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
let session = opts.session;
|
|
802
|
+
if (opts.answer !== undefined) {
|
|
803
|
+
if (!opts.question) {
|
|
804
|
+
sp.fail(chalk_1.default.red('--answer needs --question <id>'));
|
|
805
|
+
process.exitCode = 1;
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
const cap = await callVerbConfirmed('form.capture', {
|
|
809
|
+
...base, question_id: String(opts.question), value: opts.answer,
|
|
810
|
+
channel: 'cli', ...(session ? { session_ref: session } : {}),
|
|
811
|
+
});
|
|
812
|
+
session = String(cap.session_ref ?? session ?? '');
|
|
813
|
+
}
|
|
814
|
+
const next = await callVerb('form.next_question', {
|
|
815
|
+
...base, ...(session ? { session_ref: session } : {}),
|
|
816
|
+
});
|
|
817
|
+
sp.stop();
|
|
818
|
+
if ((0, json_output_1.isJsonOutput)(opts)) {
|
|
819
|
+
console.log(JSON.stringify({ ...next, session_ref: session }, null, 2));
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
if (session)
|
|
823
|
+
console.log(` ${chalk_1.default.dim('session')} ${session}`);
|
|
824
|
+
if (next.complete) {
|
|
825
|
+
console.log(chalk_1.default.green(' Every question answered.'));
|
|
826
|
+
console.log(chalk_1.default.dim(` Finish it: solid forms walk ${id} --session ${session ?? ''} --submit`));
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
const q = (next.next_question ?? {});
|
|
830
|
+
console.log(` ${chalk_1.default.bold(String(q.prompt ?? ''))}`);
|
|
831
|
+
console.log(` ${chalk_1.default.dim(`${q.kind}${q.required ? ', required' : ''}`)}`);
|
|
832
|
+
if (Array.isArray(q.choices) && q.choices.length) {
|
|
833
|
+
console.log(` ${chalk_1.default.dim('choices:')} ${q.choices.join(' / ')}`);
|
|
834
|
+
}
|
|
835
|
+
console.log(chalk_1.default.dim(`\n Answer it: solid forms walk ${id} --question ${q.id}` +
|
|
836
|
+
` --answer "..."${session ? ` --session ${session}` : ''}`));
|
|
837
|
+
}
|
|
838
|
+
catch (e) {
|
|
839
|
+
fail(sp, 'Failed', e);
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
// ── vocabulary: the nouns this tenant's customers actually use ──────────────
|
|
843
|
+
exports.formsCommand
|
|
844
|
+
.command('vocabulary')
|
|
845
|
+
.alias('words')
|
|
846
|
+
.description("This business's own words — what its customers are called, and its appointments")
|
|
847
|
+
.option('--lang <code>', 'Language', 'en')
|
|
848
|
+
.option('--json', 'Output as JSON')
|
|
849
|
+
.action(async (opts) => {
|
|
850
|
+
requireAuth();
|
|
851
|
+
const sp = (0, ora_1.default)('Loading...').start();
|
|
852
|
+
try {
|
|
853
|
+
// kb_sub_code omitted deliberately — the verb resolves this tenant's own.
|
|
854
|
+
const out = await callVerb('playbook.vocabulary', { lang: opts.lang });
|
|
855
|
+
if ((0, json_output_1.isJsonOutput)(opts)) {
|
|
856
|
+
sp.stop();
|
|
857
|
+
console.log(JSON.stringify(out, null, 2));
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
sp.stop();
|
|
861
|
+
const terms = (out.terms ?? {});
|
|
862
|
+
console.log(` ${chalk_1.default.dim('industry code')} ${out.kb_sub_code ?? chalk_1.default.dim('none set')}`);
|
|
863
|
+
for (const [k, v] of Object.entries(terms)) {
|
|
864
|
+
console.log(` ${chalk_1.default.dim(k.padEnd(16))} ${v}`);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
catch (e) {
|
|
868
|
+
fail(sp, 'Failed to load the vocabulary', e);
|
|
869
|
+
}
|
|
870
|
+
});
|
|
277
871
|
const command_kit_1 = require("../lib/command-kit");
|
|
278
872
|
(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
|
|
873
|
+
{ cmd: 'solid forms list', why: 'Every form with its lifecycle, across every provider' },
|
|
874
|
+
{ cmd: 'solid forms build --intent intake --save', why: "A starter in this industry's own words" },
|
|
875
|
+
{ cmd: 'solid forms publish <id>', why: 'A draft starts taking answers' },
|
|
876
|
+
{ cmd: 'solid forms link <id>', why: "The form's shareable URL, bare on stdout" },
|
|
877
|
+
{ cmd: 'solid forms moments set appointment_booked --form <id>', why: 'Make something actually send it' },
|
|
878
|
+
{ cmd: 'solid forms walk <id>', why: 'Answer it the way an agent does, from the terminal' },
|
|
879
|
+
{ cmd: 'solid forms responses <id>', why: 'What people actually answered' },
|
|
283
880
|
]);
|
|
284
881
|
//# sourceMappingURL=forms.js.map
|