@myapihq/cli 1.3.5 → 1.3.8

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.
Files changed (40) hide show
  1. package/dist/commands/audience.js +3 -3
  2. package/dist/commands/auth.d.ts +1 -1
  3. package/dist/commands/auth.js +10 -10
  4. package/dist/commands/billing.js +3 -3
  5. package/dist/commands/company.js +1 -1
  6. package/dist/commands/config.js +3 -3
  7. package/dist/commands/container.js +3 -3
  8. package/dist/commands/crm/companies.js +6 -5
  9. package/dist/commands/crm/contacts.js +7 -6
  10. package/dist/commands/crm/index.js +1 -1
  11. package/dist/commands/database.js +1 -1
  12. package/dist/commands/doctor.js +2 -2
  13. package/dist/commands/domain.js +11 -11
  14. package/dist/commands/email/campaign.js +5 -5
  15. package/dist/commands/email/index.js +2 -2
  16. package/dist/commands/email/mailbox.js +3 -3
  17. package/dist/commands/email/message.js +4 -4
  18. package/dist/commands/email/template.js +3 -3
  19. package/dist/commands/fn.js +3 -3
  20. package/dist/commands/funnel.d.ts +1 -0
  21. package/dist/commands/funnel.js +187 -8
  22. package/dist/commands/image.js +2 -2
  23. package/dist/commands/keys.js +1 -1
  24. package/dist/commands/llm.js +257 -24
  25. package/dist/commands/org.js +4 -4
  26. package/dist/commands/payments.js +3 -3
  27. package/dist/commands/people.js +1 -1
  28. package/dist/commands/pixel.js +3 -3
  29. package/dist/commands/queue.js +1 -0
  30. package/dist/commands/storage.js +4 -4
  31. package/dist/commands/webhook.js +2 -2
  32. package/dist/commands/workflow-validation.test.js +11 -2
  33. package/dist/commands/workflow.js +21 -8
  34. package/dist/completion.js +1 -1
  35. package/dist/index.js +21 -21
  36. package/dist/skills/my-funnel-api/SKILL.md +33 -5
  37. package/dist/skills/my-llm-api/README.md +18 -8
  38. package/dist/skills/my-llm-api/SKILL.md +91 -62
  39. package/dist/skills/my-webhook-api/SKILL.md +4 -2
  40. package/package.json +5 -4
@@ -211,12 +211,12 @@ export async function run(subcommand, args, flags) {
211
211
 
212
212
  Subcommands:
213
213
  create Save a filter as a named audience (people or company source)
214
- list List all audiences in the org
215
- get Get a single audience (with its filter + current member_count)
216
- update Patch name / description / filter (re-evaluates count if filter changes)
217
214
  delete Remove an audience
215
+ get Get a single audience (with its filter + current member_count)
216
+ list List all audiences in the org
218
217
  members Get paged member snapshot (people or companies depending on source)
219
218
  refresh Re-evaluate filter against current data; returns delta vs previous
219
+ update Patch name / description / filter (re-evaluates count if filter changes)
220
220
 
221
221
  All commands accept --org <id> (or set a default: myapi config set-org <id>).
222
222
 
@@ -3,7 +3,7 @@ import type { Flags } from '../helpers.js';
3
3
  import type { Exposes } from '../exposes.js';
4
4
  export declare const EXPOSES: Exposes;
5
5
  export declare const SCHEMA: FlagSchema;
6
- export declare const HELP = "Usage: myapi auth <subcommand>\n\nSubcommands:\n setup Configure your account\n import-key Import an existing API key non-interactively\n whoami Show current account \u00B7 supports --json\n link [email] Upgrade anonymous account to registered (or add a second session)\n Use myapi auth setup to create a completely new account\n switch [index] Switch active account by index or email\n config Manage CLI defaults (org, funnel, domain) \u00B7 supports set-org / set-funnel / set-domain\n registrant Manage stored WHOIS contact info for domain registration \u00B7 set / get / clear\n install-skills DEPRECATED \u2014 use `myapi install-skills` instead\n api-keys Manage API keys \u00B7 list / create / revoke\n keys Alias for api-keys";
6
+ export declare const HELP = "Usage: myapi auth <subcommand>\n\nSubcommands:\n api-keys Manage API keys \u00B7 list / create / revoke\n config Manage CLI defaults (org, funnel, domain) \u00B7 supports set-org / set-funnel / set-domain\n import-key Import an existing API key non-interactively\n install-skills DEPRECATED \u2014 use `myapi install-skills` instead\n keys Alias for api-keys\n link [email] Upgrade anonymous account to registered (or add a second session)\n Use myapi auth setup to create a completely new account\n registrant Manage stored WHOIS contact info for domain registration \u00B7 set / get / clear\n setup Configure your account\n switch [index] Switch active account by index or email\n whoami Show current account \u00B7 supports --json";
7
7
  export declare const INSTALL_SKILLS_HELP = "Usage: myapi install-skills\n\nInstalls the MyAPI skills pack for AI coding agents (Claude, Gemini, Cursor).\n\nThis command writes skill definition files to:\n ~/.agents/skills/myapi/\n\nAnd creates symlinks in the appropriate agent config directories:\n ~/.claude/ (Claude)\n ~/.gemini/ (Gemini)\n ~/.cursor/ (Cursor, if detected)\n\nThese files teach agents how to use the MyAPI CLI and API directly.\nRun this command again to update existing skills to the latest version.\n\nNote: `myapi auth install-skills` is deprecated and will be removed in the next minor.\nUse `myapi install-skills` going forward.";
8
8
  export declare function link(flags?: Flags, emailArg?: string): Promise<void>;
9
9
  export declare function whoami(flags?: Flags): Promise<void>;
@@ -20,17 +20,17 @@ export const SCHEMA = {
20
20
  export const HELP = `Usage: myapi auth <subcommand>
21
21
 
22
22
  Subcommands:
23
- setup Configure your account
23
+ api-keys Manage API keys · list / create / revoke
24
+ config Manage CLI defaults (org, funnel, domain) · supports set-org / set-funnel / set-domain
24
25
  import-key Import an existing API key non-interactively
25
- whoami Show current account · supports --json
26
+ install-skills DEPRECATED use \`myapi install-skills\` instead
27
+ keys Alias for api-keys
26
28
  link [email] Upgrade anonymous account to registered (or add a second session)
27
29
  Use myapi auth setup to create a completely new account
28
- switch [index] Switch active account by index or email
29
- config Manage CLI defaults (org, funnel, domain) · supports set-org / set-funnel / set-domain
30
30
  registrant Manage stored WHOIS contact info for domain registration · set / get / clear
31
- install-skills DEPRECATED use \`myapi install-skills\` instead
32
- api-keys Manage API keys · list / create / revoke
33
- keys Alias for api-keys`;
31
+ setup Configure your account
32
+ switch [index] Switch active account by index or email
33
+ whoami Show current account · supports --json`;
34
34
  export const INSTALL_SKILLS_HELP = `Usage: myapi install-skills
35
35
 
36
36
  Installs the MyAPI skills pack for AI coding agents (Claude, Gemini, Cursor).
@@ -274,10 +274,10 @@ requires (ICANN-mandated). Stored in ~/.myapi/config.json under the active
274
274
  account; never synced to the backend except as a per-request field.
275
275
 
276
276
  Subcommands:
277
- set Interactive prompt for all fields. Or pass --registrant-json
278
- '<json>' to set non-interactively (agent-friendly).
277
+ clear Remove the stored registrant from the active account.
279
278
  get Print the stored registrant (or "not set").
280
- clear Remove the stored registrant from the active account.`;
279
+ set Interactive prompt for all fields. Or pass --registrant-json
280
+ '<json>' to set non-interactively (agent-friendly).`;
281
281
  export async function registrant(sub, flags = {}) {
282
282
  if (flags.help || !sub) {
283
283
  info(REGISTRANT_HELP);
@@ -50,10 +50,10 @@ export async function run(subcommand, args, flags) {
50
50
  Subcommands:
51
51
  balance Check balance, credits, and payment method status
52
52
  history View recent transactions and top-ups
53
- usage Spend rolled up by service (this month, or --period 30d)
54
- topup Top up your balance (whole dollars)
55
53
  setup Open a checkout link to add or update payment method
56
- spend-cap Set/show/clear the account-level spend ceiling (IAM Layer 2)`);
54
+ spend-cap Set/show/clear the account-level spend ceiling (IAM Layer 2)
55
+ topup Top up your balance (whole dollars)
56
+ usage Spend rolled up by service (this month, or --period 30d)`);
57
57
  return;
58
58
  }
59
59
  if (flags.help) {
@@ -126,8 +126,8 @@ export async function run(subcommand, args, flags) {
126
126
  info(`Usage: myapi company <subcommand>
127
127
 
128
128
  Subcommands:
129
- search Filter companies across the Goldfox database
130
129
  get Get a single company (optionally with embedded people)
130
+ search Filter companies across the Goldfox database
131
131
 
132
132
  All commands accept --org <id> (or set a default: myapi config set-org <id>).
133
133
  Filter shape is shared with people + audience — see myapi audience --help.`);
@@ -98,10 +98,10 @@ export async function run(subcommand, args, flags) {
98
98
  Manages CLI defaults (org, funnel, domain) so you don't have to pass --org / --domain on every command.
99
99
 
100
100
  Subcommands:
101
- view Show current defaults
102
- set-org Set default organization (validates access)
103
- set-funnel Set default funnel (must be owned by default org)
104
101
  set-domain Set default domain
102
+ set-funnel Set default funnel (must be owned by default org)
103
+ set-org Set default organization (validates access)
104
+ view Show current defaults
105
105
 
106
106
  Examples:
107
107
  myapi ${via} set-org <id>
@@ -280,12 +280,12 @@ dependencies and long execution.
280
280
 
281
281
  Subcommands:
282
282
  create Register a container and get its scoped API key (returned once)
283
+ delete <id> Soft-delete and revoke its scoped API key
283
284
  deploy <id> <image> Ship a pre-built image and go live
284
- list List containers in your org
285
+ domain <id> <domain> Bind a custom domain (--remove to unbind)
285
286
  get <id> Inspect a container
287
+ list List containers in your org
286
288
  logs <id> Show recent runtime logs (--tail <n>)
287
- domain <id> <domain> Bind a custom domain (--remove to unbind)
288
- delete <id> Soft-delete and revoke its scoped API key
289
289
 
290
290
  All commands accept --org <id> (or set default: myapi config set-org <id>).`);
291
291
  return;
@@ -10,6 +10,7 @@ export const EXPOSES = [
10
10
  'GET /crm/orgs/{org_id}/companies/{id}',
11
11
  'PATCH /crm/orgs/{org_id}/companies/{id}',
12
12
  'DELETE /crm/orgs/{org_id}/companies/{id}',
13
+ 'POST /crm/orgs/{org_id}/companies/{id}/restore',
13
14
  ];
14
15
  export const SCHEMA = {};
15
16
  function csv(v) {
@@ -159,14 +160,14 @@ export async function run(subcommand, args, flags) {
159
160
  info(`Usage: myapi crm companies <subcommand>
160
161
 
161
162
  Subcommands:
162
- list List all companies
163
- search Filter by stage / source / domain
164
163
  create <domain> Manually create (source='manual')
165
- get <id> Fetch one company
166
- update <id> Patch stage, name, custom JSON
167
164
  delete <id> Soft-delete
168
- restore <id> Undo a soft-delete
165
+ get <id> Fetch one company
166
+ list List all companies
169
167
  promote <domain> Promote a Goldfox company → CRM (Goldfox company id IS the domain)
168
+ restore <id> Undo a soft-delete
169
+ search Filter by stage / source / domain
170
+ update <id> Patch stage, name, custom JSON
170
171
 
171
172
  All commands accept --org <id> (or set default: myapi config set-org <id>).`);
172
173
  return;
@@ -10,6 +10,7 @@ export const EXPOSES = [
10
10
  'GET /crm/orgs/{org_id}/contacts/{id}',
11
11
  'PATCH /crm/orgs/{org_id}/contacts/{id}',
12
12
  'DELETE /crm/orgs/{org_id}/contacts/{id}',
13
+ 'POST /crm/orgs/{org_id}/contacts/{id}/restore',
13
14
  'GET /crm/orgs/{org_id}/contacts/{id}/events',
14
15
  ];
15
16
  export const SCHEMA = {};
@@ -205,15 +206,15 @@ export async function run(subcommand, args, flags) {
205
206
  info(`Usage: myapi crm contacts <subcommand>
206
207
 
207
208
  Subcommands:
208
- list Most-recently-engaged contacts (no filter)
209
- search Filter by stage / source / email / engagement window
210
209
  create <email> Manually create (source='manual')
211
- get <id> Fetch one contact (with embedded Goldfox enrichment, when available)
212
- update <id> Patch stage, names, company_id, custom JSON
213
210
  delete <id> Soft-delete (events retained)
214
- restore <id> Undo a soft-delete
215
- promote <goldfox_person_id> Promote a Goldfox lead → CRM contact (idempotent)
216
211
  events <id> Timeline of events on a contact (newest first)
212
+ get <id> Fetch one contact (with embedded Goldfox enrichment, when available)
213
+ list Most-recently-engaged contacts (no filter)
214
+ promote <goldfox_person_id> Promote a Goldfox lead → CRM contact (idempotent)
215
+ restore <id> Undo a soft-delete
216
+ search Filter by stage / source / email / engagement window
217
+ update <id> Patch stage, names, company_id, custom JSON
217
218
 
218
219
  All commands accept --org <id> (or set default: myapi config set-org <id>).`);
219
220
  return;
@@ -39,8 +39,8 @@ export async function run(subcommand, args, flags) {
39
39
  info(`Usage: myapi crm <namespace> <subcommand>
40
40
 
41
41
  Namespaces:
42
- contacts The people you've engaged with (private to org)
43
42
  companies The companies you've engaged with (private to org)
43
+ contacts The people you've engaged with (private to org)
44
44
 
45
45
  Each namespace shares the same subcommands:
46
46
  list List with no filter (alias for search)
@@ -224,9 +224,9 @@ export async function run(subcommand, args, flags) {
224
224
  info(`Usage: myapi database <subcommand>
225
225
 
226
226
  Namespaces:
227
- namespaces List namespaces in the org
228
227
  create <name> Create a namespace
229
228
  delete-namespace <name> Delete a namespace AND all its keys (irreversible)
229
+ namespaces List namespaces in the org
230
230
 
231
231
  Keys (require --ns <namespace>):
232
232
  keys List keys in a namespace
@@ -276,6 +276,6 @@ Exit codes:
276
276
  1 one or more critical issues
277
277
 
278
278
  Options:
279
- --verbose Show passing checks too.
280
279
  --json Machine-readable output.
281
- --org <id> Override the default org.`;
280
+ --org <id> Override the default org.
281
+ --verbose Show passing checks too.`;
@@ -443,12 +443,12 @@ async function recordsDelete(domainArg, recordId, flags) {
443
443
  const RECORDS_HELP = `Usage: myapi domain records <subcommand> <domain> [...]
444
444
 
445
445
  Subcommands:
446
- list <domain> [--type A|AAAA|CNAME|MX|TXT]
447
- get <domain> <record-id>
448
446
  create <domain> --type <T> --name <n> --content <c> [--ttl <n>]
449
447
  [--priority <n>] [--proxied]
450
- update <domain> <record-id> [--content ...] [--ttl ...] [--priority ...] [--proxied]
451
448
  delete <domain> <record-id> [--yes]
449
+ get <domain> <record-id>
450
+ list <domain> [--type A|AAAA|CNAME|MX|TXT]
451
+ update <domain> <record-id> [--content ...] [--ttl ...] [--priority ...] [--proxied]
452
452
 
453
453
  Name normalization:
454
454
  --name=@ Apex (e.g. example.com)
@@ -605,19 +605,19 @@ export async function run(subcommand, args, flags) {
605
605
  info(`Usage: myapi domain <subcommand>
606
606
 
607
607
  Subcommands:
608
- list List domains
608
+ assign Assign domain to an org
609
609
  check Check domain availability
610
- register Register a domain
610
+ email-setup Opt in to MyAPI-managed email on a subdomain (default: mail.<domain>)
611
611
  import Bring your own domain (BYOD) — registrar-agnostic
612
+ list List domains
613
+ records Manage DNS records in the zone (list / get / create / update / delete)
614
+ register Register a domain
612
615
  renew Renew a registered domain for another period
613
- assign Assign domain to an org
614
- unassign Unassign domain from its current org
615
- status Get domain status
616
+ retry-provisioning Re-run provisioning when status=infra_error and the failure is retryable
616
617
  settings Get edge/CDN settings
618
+ status Get domain status
619
+ unassign Unassign domain from its current org
617
620
  update-settings Update edge/CDN settings
618
- records Manage DNS records in the zone (list / get / create / update / delete)
619
- email-setup Opt in to MyAPI-managed email on a subdomain (default: mail.<domain>)
620
- retry-provisioning Re-run provisioning when status=infra_error and the failure is retryable
621
621
 
622
622
  Tip: Set defaults with "myapi config set-org <id>" / "set-domain <domain>" to skip flags on every command.`);
623
623
  return;
@@ -163,15 +163,15 @@ export async function run(sub, args, flags) {
163
163
 
164
164
  Subcommands:
165
165
  create <name> Create a campaign (--template-id --from)
166
- list List campaigns
167
166
  get <id> Get a single campaign
168
- update <id> Patch name / per-day-limit
169
- upload-contacts <id> Upload contact list (--emails a@x,b@y)
170
- upload-contacts-file <id> Upload contacts from a CSV file (--file path/to/contacts.csv)
171
- start <id> Start a campaign
167
+ list List campaigns
172
168
  pause <id> Pause a campaign
173
169
  resume <id> Resume a campaign
170
+ start <id> Start a campaign
174
171
  stats <id> Get campaign stats
172
+ update <id> Patch name / per-day-limit
173
+ upload-contacts <id> Upload contact list (--emails a@x,b@y)
174
+ upload-contacts-file <id> Upload contacts from a CSV file (--file path/to/contacts.csv)
175
175
 
176
176
  All commands accept --org <id> (or set default: myapi config set-org <id>).`);
177
177
  return;
@@ -79,12 +79,12 @@ export async function run(subcommand, args, flags) {
79
79
  info(`Usage: myapi email <namespace> <subcommand>
80
80
 
81
81
  Namespaces:
82
+ campaign Email campaigns (org-scoped) — create, list, get, update, upload-contacts, start, pause, resume, stats
82
83
  mailbox Mailboxes (account-scoped) — create, list, activate-sending
83
84
  message Send & read mail (account-scoped) — send, status, sent, inbox, outbox, get
84
- warmup Warmup controls (account-scoped) — start, pause, resume, stop, stats
85
85
  template Email templates (org-scoped) — generate, list, edit, send-test, delete
86
- campaign Email campaigns (org-scoped) — create, list, get, update, upload-contacts, start, pause, resume, stats
87
86
  verify Sync single-address email verification (org-scoped) — syntax + DNS + Microsoft probe
87
+ warmup Warmup controls (account-scoped) — start, pause, resume, stop, stats
88
88
 
89
89
  Examples:
90
90
  myapi email mailbox create hello@example.com
@@ -94,11 +94,11 @@ export async function run(sub, args, flags) {
94
94
  info(`Usage: myapi email mailbox <subcommand>
95
95
 
96
96
  Subcommands:
97
+ activate-sending Activate outbound sending for a mailbox
98
+ clear-forwarding Stop forwarding for a mailbox
97
99
  create Create a mailbox (positional <user@domain> or --username/--domain)
98
100
  list List mailboxes on a domain (--domain) or orphaned ones (--filter unassigned)
99
- activate-sending Activate outbound sending for a mailbox
100
- set-forwarding Forward a copy of incoming mail to an external address
101
- clear-forwarding Stop forwarding for a mailbox`);
101
+ set-forwarding Forward a copy of incoming mail to an external address`);
102
102
  return;
103
103
  }
104
104
  if (flags.help) {
@@ -118,12 +118,12 @@ export async function run(sub, args, flags) {
118
118
  info(`Usage: myapi email message <subcommand>
119
119
 
120
120
  Subcommands:
121
- send Send an email (transactional or templated)
122
- status <id> Get delivery status of a sent message
123
- sent List sent emails
121
+ get <id> Get a single message (--address <email>)
124
122
  inbox <addr> Read inbox for a mailbox address
125
123
  outbox <addr> Read outbox for a mailbox address
126
- get <id> Get a single message (--address <email>)`);
124
+ send Send an email (transactional or templated)
125
+ sent List sent emails
126
+ status <id> Get delivery status of a sent message`);
127
127
  return;
128
128
  }
129
129
  if (flags.help) {
@@ -135,13 +135,13 @@ export async function run(sub, args, flags) {
135
135
  info(`Usage: myapi email template <subcommand>
136
136
 
137
137
  Subcommands:
138
+ delete <id> Delete a template
139
+ edit <id> Edit a template via prompt
138
140
  generate <name> Generate AI template (--prompt)
139
- list List templates
140
141
  get <id> Get a template's metadata (subject, preview URL, ...)
142
+ list List templates
141
143
  preview <id> Get a template's rendered preview (no auth required)
142
- edit <id> Edit a template via prompt
143
144
  send-test <id> Send a test email for a template (--to)
144
- delete <id> Delete a template
145
145
 
146
146
  All commands accept --org <id> (or set default: myapi config set-org <id>).`);
147
147
  return;
@@ -240,12 +240,12 @@ Create, deploy, and manage functions on the MyAPI edge runtime.
240
240
 
241
241
  Subcommands:
242
242
  create Register a function and get its scoped API key (returned once)
243
+ delete <id> Soft-delete and revoke its scoped API key
243
244
  deploy <id> <file> Upload a JS bundle and go live
244
245
  env <id> <k> <v> Set a Worker Secret on a deployed function
245
- runs <id> List recent invocation records
246
- list List functions in your org
247
246
  get <id> Inspect a function
248
- delete <id> Soft-delete and revoke its scoped API key
247
+ list List functions in your org
248
+ runs <id> List recent invocation records
249
249
 
250
250
  All commands accept --org <id> (or set default: myapi config set-org <id>).`);
251
251
  return;
@@ -9,6 +9,7 @@ export declare function get(id: string, flags: Flags): Promise<void>;
9
9
  export declare function del(id: string, flags: Flags): Promise<void>;
10
10
  export declare function pages(funnelArg: string, flags: Flags): Promise<void>;
11
11
  export declare function push(slug: string, flags: Flags): Promise<void>;
12
+ export declare function formCmd(funnelArg: string | undefined, flags: Flags): Promise<void>;
12
13
  export declare function verify(slug: string, flags: Flags): Promise<void>;
13
14
  export declare function publish(dir: string, flags: Flags): Promise<void>;
14
15
  export declare function run(subcommand: string | undefined, args: string[], flags: Flags): Promise<void>;
@@ -14,6 +14,13 @@ export const EXPOSES = [
14
14
  'POST /funnel/orgs/{org_id}/funnels/{funnel_id}/verify',
15
15
  'POST /funnel/orgs/{org_id}/funnels/{funnel_id}/files',
16
16
  'GET /funnel/orgs/{org_id}/funnels/{funnel_id}/pages',
17
+ // form bindings — created/listed/removed via `funnel form --capture-to`
18
+ // and the underlying SDK helpers. List/Delete don't have dedicated CLI
19
+ // subcommands yet; the SDK functions are wired and customers using the
20
+ // SDK directly need them in EXPOSES for coverage to be honest.
21
+ 'POST /funnel/orgs/{org_id}/funnels/{funnel_id}/forms',
22
+ 'GET /funnel/orgs/{org_id}/funnels/{funnel_id}/forms',
23
+ 'DELETE /funnel/orgs/{org_id}/funnels/{funnel_id}/forms/{slug}',
17
24
  'GET /hq/orgs/{org_id}',
18
25
  ];
19
26
  export const SCHEMA = {
@@ -21,6 +28,12 @@ export const SCHEMA = {
21
28
  slug: 'string',
22
29
  env: 'string',
23
30
  'api-fn': 'string',
31
+ // form
32
+ fields: 'string',
33
+ cta: 'string',
34
+ success: 'string',
35
+ 'capture-to': 'string',
36
+ honeypot: 'string',
24
37
  };
25
38
  // Backend (2026-05-15): POST /funnels accepts an optional `name` (defaults
26
39
  // to the org's preview_subdomain for back-compat). Mirror the backend's
@@ -178,6 +191,129 @@ export async function push(slug, flags) {
178
191
  }
179
192
  }
180
193
  }
194
+ function parseField(token) {
195
+ // <name>[:<modifier>]... — modifiers are types or `required`, order-free.
196
+ const parts = token.split(':').map(p => p.trim()).filter(Boolean);
197
+ const name = parts[0];
198
+ if (!name || !/^[a-z][a-z0-9_]*$/i.test(name)) {
199
+ error(`Invalid field name "${name}". Lowercase letters/digits/underscores; starts with a letter.`);
200
+ }
201
+ let type;
202
+ let required = false;
203
+ for (const mod of parts.slice(1)) {
204
+ const m = mod.toLowerCase();
205
+ if (m === 'required')
206
+ required = true;
207
+ else if (m === 'text' || m === 'email' || m === 'number' || m === 'tel')
208
+ type = m;
209
+ else
210
+ error(`Unknown field modifier "${mod}" on "${name}". Use one of: text, email, number, tel, required.`);
211
+ }
212
+ if (!type) {
213
+ const n = name.toLowerCase();
214
+ type = n === 'email' ? 'email'
215
+ : n === 'phone' || n === 'tel' ? 'tel'
216
+ : 'text';
217
+ }
218
+ return { name, type, required };
219
+ }
220
+ function parseCaptureTo(raw) {
221
+ const m = raw.match(/^(webhook|workflow):([a-f0-9-]{36})$/i);
222
+ if (!m)
223
+ error(`Invalid --capture-to "${raw}". Expected webhook:<uuid> or workflow:<uuid>.`);
224
+ return { kind: m[1].toLowerCase(), id: m[2] };
225
+ }
226
+ export async function formCmd(funnelArg, flags) {
227
+ const config = requireConfig();
228
+ const orgId = requireOrg(flags, config, 'myapi funnel form [funnel-id] [--slug <s>] [--fields <spec>] [--capture-to <dest>] [--honeypot <name>] [--cta "<text>"] [--success "<text>"] [--org <id>] [--json]');
229
+ // Resolve funnel id the same way push/verify do.
230
+ let funnelId = funnelArg || flags.funnel || config.default_funnel;
231
+ if (!funnelId) {
232
+ const existing = await sdkFunnel.listFunnels(config.api_key, orgId);
233
+ if (existing.length === 0)
234
+ error('No funnel found for this org. Create one with: myapi funnel create');
235
+ if (existing.length > 1) {
236
+ error(`Multiple funnels exist and no default is set.\nPass the funnel id, or set a default:\n myapi config set-funnel <id>\n\nFunnels:\n${existing.map(f => ` ${f.id}`).join('\n')}`);
237
+ }
238
+ funnelId = existing[0].id;
239
+ }
240
+ const rawSlug = flags.slug || 'join';
241
+ const slug = rawSlug.startsWith('/') ? rawSlug.slice(1) : rawSlug;
242
+ if (!/^[a-z0-9][a-z0-9-]{0,40}$/i.test(slug)) {
243
+ error(`Invalid --slug "${slug}". Lowercase letters/digits/hyphens, 1-41 chars, starts with a letter or digit.`);
244
+ }
245
+ // `--fields email:required,name,phone:tel,plan:text:required`
246
+ const fieldsArg = (flags.fields || 'email:email:required').trim();
247
+ const fieldSpecs = fieldsArg.split(',').map(s => s.trim()).filter(Boolean).map(parseField);
248
+ if (fieldSpecs.length === 0)
249
+ error('--fields must list at least one field. Example: --fields email:required,name');
250
+ // Honeypot field — match the backend default unless --honeypot overrides.
251
+ const honeypot = flags.honeypot || 'middle_name';
252
+ if (!/^[a-z][a-z0-9_]*$/i.test(honeypot))
253
+ error(`Invalid --honeypot "${honeypot}".`);
254
+ for (const f of fieldSpecs)
255
+ if (f.name === honeypot)
256
+ error(`Honeypot "${honeypot}" collides with a real field. Pick a different --honeypot name.`);
257
+ const cta = flags.cta || 'Sign up';
258
+ const successMsg = flags.success || "Thanks — we'll be in touch.";
259
+ // Optional binding via --capture-to.
260
+ let registeredBinding;
261
+ if (typeof flags['capture-to'] === 'string' && flags['capture-to']) {
262
+ const dest = parseCaptureTo(flags['capture-to']);
263
+ const binding = {
264
+ slug,
265
+ destination: `${dest.kind}:${dest.id}`,
266
+ fields: fieldSpecs.map(f => ({ name: f.name, required: f.required })),
267
+ honeypot_field: honeypot,
268
+ };
269
+ registeredBinding = await sdkFunnel.createFormBinding(config.api_key, orgId, funnelId, binding);
270
+ }
271
+ const url = `https://api.myapihq.com/funnel/funnels/${funnelId}/submit/${encodeURIComponent(slug)}`;
272
+ const escAttr = (s) => s.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
273
+ const escText = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
274
+ // Per-field labels — Email uses the canonical placeholder; others
275
+ // Title-Case the name.
276
+ const inputs = fieldSpecs.map(f => {
277
+ const label = f.name.charAt(0).toUpperCase() + f.name.slice(1).replace(/_/g, ' ');
278
+ const req = f.required ? ' required' : '';
279
+ return ` <label>${escText(label)} <input type="${f.type}" name="${escAttr(f.name)}"${req}></label>`;
280
+ }).join('\n');
281
+ // Honeypot — hidden via inline style, off the tab order, autocomplete
282
+ // disabled. Bots fill it; humans don't.
283
+ const honeypotInput = ` <input type="text" name="${escAttr(honeypot)}" style="display:none" tabindex="-1" autocomplete="off">`;
284
+ const html = `<form action="${escAttr(url)}" method="POST" data-myapi-form>
285
+ ${inputs}
286
+ ${honeypotInput}
287
+ <button type="submit">${escText(cta)}</button>
288
+ </form>
289
+ <script>
290
+ document.querySelectorAll('[data-myapi-form]').forEach(f => {
291
+ f.addEventListener('submit', async e => {
292
+ e.preventDefault();
293
+ const r = await fetch(f.action, { method:'POST', headers:{'Content-Type':'application/json'},
294
+ body: JSON.stringify(Object.fromEntries(new FormData(f))) });
295
+ if (r.ok) f.outerHTML = ${JSON.stringify(`<p>${escText(successMsg)}</p>`)};
296
+ });
297
+ });
298
+ </script>`;
299
+ if (flags.json) {
300
+ printJson({
301
+ action_url: url,
302
+ slug,
303
+ fields: fieldSpecs,
304
+ honeypot_field: honeypot,
305
+ binding_registered: !!registeredBinding,
306
+ binding: registeredBinding,
307
+ html,
308
+ });
309
+ return;
310
+ }
311
+ if (registeredBinding) {
312
+ // Print confirmation on stderr so stdout stays the clean snippet.
313
+ process.stderr.write(`✓ Registered form binding: slug=${slug} → ${registeredBinding.destination}\n`);
314
+ }
315
+ process.stdout.write(html + '\n');
316
+ }
181
317
  // Verify uses the same shape as push: slug positional (default '/'), funnel
182
318
  // resolved from --funnel / default / the org's only funnel.
183
319
  export async function verify(slug, flags) {
@@ -260,10 +396,51 @@ export async function publish(dir, flags) {
260
396
  }
261
397
  // ── Dispatcher ───────────────────────────────────────────────────────────────
262
398
  const SUBCOMMAND_USAGE = {
263
- 'list': 'myapi funnel list [--org <id>] [--json]',
264
399
  'create': 'myapi funnel create [--name <name>] [--org <id>]',
265
- 'get': 'myapi funnel get <id> [--org <id>] [--json]',
266
400
  'delete': 'myapi funnel delete <id> [--org <id>]',
401
+ 'form': `myapi funnel form [funnel-id] [--slug <slug>] [--fields <spec>]
402
+ [--capture-to <dest>] [--honeypot <name>]
403
+ [--cta "<button text>"] [--success "<message>"] [--org <id>] [--json]
404
+
405
+ Emits a ready-to-paste HTML snippet for a form that posts to the canonical
406
+ funnel submit proxy. Submissions to any slug fall through to the funnel's
407
+ auto-provisioned webhook by default (CRM ingest + bound workflows fire
408
+ without further wiring). The proxy URL is platform-stable; slug names the
409
+ form so workflows can route on it; an auto-honeypot field stops casual
410
+ bots.
411
+
412
+ --slug <s> Form identifier — appears in the proxy URL.
413
+ Defaults to "join". Lowercase letters/digits/hyphens.
414
+ --fields <spec> Comma-separated field specs.
415
+ Each spec: <name>[:<type>][:required] (modifiers
416
+ order-free). Types: text (default), email, number,
417
+ tel. Name auto-promotes to email if name="email",
418
+ to tel if name="phone" or "tel".
419
+ Defaults to "email:email:required".
420
+ --capture-to <dest> Optional. Register a per-slug binding to the
421
+ destination via POST /funnel/.../forms. Format:
422
+ webhook:<uuid> or workflow:<uuid>. Without it,
423
+ submissions fall through to the funnel's default
424
+ webhook (most agents want this).
425
+ --honeypot <name> Hidden bot-trap input name. Default "middle_name".
426
+ --cta "<text>" Submit button text. Defaults to "Sign up".
427
+ --success "<text>" Message shown on successful submit.
428
+
429
+ Examples:
430
+ myapi funnel form # email-required on default funnel
431
+ myapi funnel form --slug waitlist --fields email:required,name
432
+ myapi funnel form <funnel> --slug checkout \\
433
+ --fields email:required,plan,phone:tel \\
434
+ --capture-to webhook:<checkout_webhook_id>
435
+ myapi funnel form --json # machine-readable
436
+ myapi funnel form --slug join > form.html # snippet to a file
437
+
438
+ Do NOT hardcode webhook inbound URLs into funnel HTML. Use this verb
439
+ instead — the proxy preserves slug routing, hides the URL, and gets
440
+ future platform features (rate-limit, captcha, field validation) for
441
+ free.`,
442
+ 'get': 'myapi funnel get <id> [--org <id>] [--json]',
443
+ 'list': 'myapi funnel list [--org <id>] [--json]',
267
444
  'pages': 'myapi funnel pages [funnel_id] [--funnel <id>] [--org <id>] [--json]',
268
445
  'publish': `myapi funnel publish <dir> [--funnel <id>] [--env dev|prod] [--api-fn <id>] [--org <id>]
269
446
 
@@ -312,13 +489,14 @@ export async function run(subcommand, args, flags) {
312
489
  info(`Usage: myapi funnel <subcommand>
313
490
 
314
491
  Subcommands:
315
- list List funnels
316
492
  create Create a funnel (optional --name; backend defaults to org's preview_subdomain)
317
- get <id> Get funnel details
318
493
  delete Delete a funnel
494
+ form Emit HTML for a form that posts to the canonical submit proxy
495
+ get <id> Get funnel details
496
+ list List funnels
497
+ pages List the pages currently published to a funnel
319
498
  publish Publish a whole local directory as the site (dev or prod)
320
499
  push Push a raw HTML page from stdin to a slug
321
- pages List the pages currently published to a funnel
322
500
  verify Verify a published page is reachable
323
501
 
324
502
  All commands accept --org <id> (or set default: myapi config set-org <id>).`);
@@ -333,14 +511,15 @@ All commands accept --org <id> (or set default: myapi config set-org <id>).`);
333
511
  return;
334
512
  }
335
513
  switch (subcommand) {
336
- case 'list': return list(flags);
337
514
  case 'create': return create(flags);
338
- case 'get': return get(args[0], flags);
339
515
  case 'delete': return del(args[0], flags);
516
+ case 'form': return formCmd(args[0], flags);
517
+ case 'get': return get(args[0], flags);
518
+ case 'list': return list(flags);
519
+ case 'pages': return pages(args[0], flags);
340
520
  case 'publish': return publish(args[0], flags);
341
521
  case 'push': return push(args[0], flags);
342
522
  case 'verify': return verify(args[0], flags);
343
- case 'pages': return pages(args[0], flags);
344
523
  default: error(`Unknown subcommand: ${subcommand}. Run "myapi funnel --help" for a list of valid subcommands.`);
345
524
  }
346
525
  }