@myapihq/cli 1.1.0-wip.1 → 1.1.0-wip.3

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.
@@ -45,7 +45,7 @@ async function activateSending(flags) {
45
45
  if (!address)
46
46
  error('Missing required arguments.\nUsage: myapi email mailbox activate-sending --address <email>');
47
47
  const res = await sdkEmail.activateSending(config.api_key, address);
48
- success(`Sending activated: ${address} (status: ${res.status})`);
48
+ success(`Sending activated: ${address} (${res.emails_quota_remaining} emails/day quota)`);
49
49
  }
50
50
  const USAGE = {
51
51
  'create': `myapi email mailbox create <user@domain> [--display-name <name>]
@@ -6,5 +6,5 @@ export declare function create(flags: Flags): Promise<void>;
6
6
  export declare function get(id: string, flags: Flags): Promise<void>;
7
7
  export declare function del(id: string, flags: Flags): Promise<void>;
8
8
  export declare function push(slug: string, flags: Flags): Promise<void>;
9
- export declare function verify(id: string, flags: Flags): Promise<void>;
9
+ export declare function verify(slug: string, flags: Flags): Promise<void>;
10
10
  export declare function run(subcommand: string | undefined, args: string[], flags: Flags): Promise<void>;
@@ -108,25 +108,48 @@ export async function push(slug, flags) {
108
108
  if (resolvedFromOrg) {
109
109
  info(`(Used the org's only funnel: ${funnelId}. Set a default with: myapi config set-funnel ${funnelId})`);
110
110
  }
111
- if (result?.url) {
112
- info(`Preview: ${result.url}`);
113
- }
114
- else {
111
+ let liveUrl = result?.url;
112
+ if (!liveUrl) {
115
113
  const org = await hq.getOrg(config.api_key, orgId);
116
114
  if (org.preview_subdomain) {
117
- info(`Preview: https://${org.preview_subdomain}.makeautonomous.com${finalSlug}`);
115
+ liveUrl = `https://${org.preview_subdomain}.makeautonomous.com${finalSlug}`;
116
+ }
117
+ }
118
+ if (liveUrl)
119
+ info(`Preview: ${liveUrl}`);
120
+ // If we're still on the preview subdomain, hint at how to serve on a
121
+ // custom domain. Tailor the hint: if a default_domain is already
122
+ // configured (registered + meant to be used), suggest assigning it
123
+ // directly; otherwise point at register + assign.
124
+ const onPreview = liveUrl?.includes('.makeautonomous.com');
125
+ if (onPreview) {
126
+ const dom = config.default_domain;
127
+ if (dom) {
128
+ info(`(Serving on the preview subdomain. To serve on ${dom} instead: myapi domain assign ${dom})`);
129
+ }
130
+ else {
131
+ info(`(Serving on the preview subdomain. To serve on a custom domain: myapi domain register <domain> && myapi domain assign <domain>)`);
118
132
  }
119
133
  }
120
134
  }
121
- export async function verify(id, flags) {
135
+ // Verify uses the same shape as push: slug positional (default '/'), funnel
136
+ // resolved from --funnel / default / the org's only funnel.
137
+ export async function verify(slug, flags) {
122
138
  const config = requireConfig();
123
- const orgId = requireOrg(flags, config, 'myapi funnel verify <id> [--slug <slug>] [--org <id>]');
124
- if (!id)
125
- error('Missing required arguments.\nUsage: myapi funnel verify <id> [--slug <slug>] [--org <id>]');
126
- const opts = {};
127
- if (flags.slug)
128
- opts.slug = flags.slug;
129
- const v = await sdkFunnel.verifyFunnel(config.api_key, orgId, id, opts);
139
+ const orgId = requireOrg(flags, config, 'myapi funnel verify [slug] [--funnel <id>] [--org <id>]');
140
+ const rawSlug = slug || flags.slug || '/';
141
+ const finalSlug = rawSlug.startsWith('/') ? rawSlug : `/${rawSlug}`;
142
+ let funnelId = flags.funnel || config.default_funnel;
143
+ if (!funnelId) {
144
+ const existing = await sdkFunnel.listFunnels(config.api_key, orgId);
145
+ if (existing.length === 0)
146
+ error('No funnel found for this org. Create one with: myapi funnel create');
147
+ if (existing.length > 1) {
148
+ error(`Multiple funnels exist for this org and no default is set.\nPick one with --funnel <id>, or set a default:\n myapi config set-funnel <id>\n\nFunnels:\n${existing.map(f => ` ${f.id}`).join('\n')}`);
149
+ }
150
+ funnelId = existing[0].id;
151
+ }
152
+ const v = await sdkFunnel.verifyFunnel(config.api_key, orgId, funnelId, { slug: finalSlug });
130
153
  printJson(v);
131
154
  }
132
155
  // ── Dispatcher ───────────────────────────────────────────────────────────────
@@ -140,12 +163,26 @@ const SUBCOMMAND_USAGE = {
140
163
  Reads HTML from stdin and publishes to <slug> on your funnel. Funnel is resolved
141
164
  from --funnel, the default funnel, or (only if the org has exactly one) auto-picked.
142
165
 
166
+ By default, your funnel is served on a preview subdomain (*.makeautonomous.com).
167
+ To serve on your own domain, register and assign one with:
168
+ myapi domain register <domain>
169
+ myapi domain assign <domain>
170
+
143
171
  Examples:
144
172
  echo '<h1>Hello</h1>' | myapi funnel push /
145
173
  cat about.html | myapi funnel push /about
146
174
  myapi funnel push < index.html
147
175
  cat p.html | myapi funnel push /pricing --funnel <uuid>`,
148
- 'verify': 'myapi funnel verify <id> [--slug <slug>] [--org <id>]',
176
+ 'verify': `myapi funnel verify [slug] [--funnel <id>] [--org <id>]
177
+
178
+ Verifies the page at <slug> on a funnel. Slug defaults to '/'. Funnel is
179
+ resolved from --funnel, the default funnel, or (only if the org has exactly
180
+ one) auto-picked — same shape as "funnel push".
181
+
182
+ Examples:
183
+ myapi funnel verify # verifies '/' on the default funnel
184
+ myapi funnel verify /pricing
185
+ myapi funnel verify / --funnel <uuid>`,
149
186
  };
150
187
  export async function run(subcommand, args, flags) {
151
188
  if (!subcommand || (flags.help && !subcommand)) {
@@ -9,6 +9,63 @@ export const SCHEMA = {
9
9
  steps: 'string',
10
10
  'no-enable': 'boolean',
11
11
  };
12
+ // Mirrors the backend's SupportedStepTypes list. Both alias and underscore
13
+ // forms are accepted by the workflow runner. Keep this in sync if the
14
+ // backend grows new step types.
15
+ const SUPPORTED_STEP_TYPES = ['send_email', 'email', 'slack_message', 'slack'];
16
+ const SLACK_HOOK_RE = /^https:\/\/hooks\.slack\.com\/services\/[A-Za-z0-9_-]+\/[A-Za-z0-9_-]+\/[A-Za-z0-9_-]+/;
17
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
18
+ // Per-step-type required fields. Validated client-side so typos and
19
+ // hallucinated shapes fail fast, before the network call. Backend
20
+ // performs the same validation as defense in depth.
21
+ function validateSteps(steps) {
22
+ if (!Array.isArray(steps))
23
+ error('--steps must be a JSON array of step objects.');
24
+ if (steps.length === 0)
25
+ error('--steps cannot be an empty array.');
26
+ steps.forEach((s, i) => {
27
+ const where = `step ${i}`;
28
+ if (!s || typeof s !== 'object')
29
+ error(`${where}: must be a JSON object.`);
30
+ if (!s.type)
31
+ error(`${where}: missing required field "type". Supported: ${SUPPORTED_STEP_TYPES.join(', ')}`);
32
+ if (!SUPPORTED_STEP_TYPES.includes(s.type)) {
33
+ error(`${where}: unknown type "${s.type}". Supported: ${SUPPORTED_STEP_TYPES.join(', ')}`);
34
+ }
35
+ if (s.type === 'send_email' || s.type === 'email') {
36
+ const required = ['from', 'to', 'subject'];
37
+ for (const f of required) {
38
+ if (!s[f] || typeof s[f] !== 'string') {
39
+ error(`${where} (${s.type}): missing required field "${f}".`);
40
+ }
41
+ }
42
+ const bodyForms = ['body', 'html', 'template_id'].filter(f => s[f] !== undefined && s[f] !== '');
43
+ if (bodyForms.length === 0) {
44
+ error(`${where} (${s.type}): must include exactly one of "body", "html", or "template_id".`);
45
+ }
46
+ if (bodyForms.length > 1) {
47
+ error(`${where} (${s.type}): can only use one of "body", "html", "template_id" — got: ${bodyForms.join(', ')}.`);
48
+ }
49
+ if (s.template_vars !== undefined && !s.template_id) {
50
+ error(`${where} (${s.type}): "template_vars" only makes sense with "template_id".`);
51
+ }
52
+ if (s.template_vars !== undefined && (typeof s.template_vars !== 'object' || Array.isArray(s.template_vars))) {
53
+ error(`${where} (${s.type}): "template_vars" must be a JSON object.`);
54
+ }
55
+ }
56
+ if (s.type === 'slack_message' || s.type === 'slack') {
57
+ if (!s.webhook_url || typeof s.webhook_url !== 'string') {
58
+ error(`${where} (${s.type}): missing required field "webhook_url".`);
59
+ }
60
+ if (!SLACK_HOOK_RE.test(s.webhook_url)) {
61
+ error(`${where} (${s.type}): "webhook_url" must look like https://hooks.slack.com/services/T.../B.../xxx — got "${s.webhook_url}".`);
62
+ }
63
+ if (!s.text || typeof s.text !== 'string') {
64
+ error(`${where} (${s.type}): missing required field "text".`);
65
+ }
66
+ }
67
+ });
68
+ }
12
69
  function summarizeWorkflow(w) {
13
70
  return {
14
71
  id: w.id,
@@ -60,6 +117,9 @@ export async function create(nameArg, flags) {
60
117
  if (!name || !endpointId || !flags.steps) {
61
118
  error('Missing required arguments.\nUsage: myapi workflow create <name> --endpoint-id <id> --steps <json> [--no-enable] [--org <id>]\n or: myapi workflow create --name <name> --endpoint-id <id> --steps <json> [--no-enable] [--org <id>]');
62
119
  }
120
+ if (!UUID_RE.test(endpointId)) {
121
+ error(`Invalid --endpoint-id "${endpointId}". Expected a webhook endpoint UUID.\nList your endpoints with: myapi webhook list`);
122
+ }
63
123
  let steps;
64
124
  try {
65
125
  steps = JSON.parse(flags.steps);
@@ -67,6 +127,7 @@ export async function create(nameArg, flags) {
67
127
  catch {
68
128
  error('Invalid JSON for --steps');
69
129
  }
130
+ validateSteps(steps);
70
131
  const wf = await sdkWorkflow.createWorkflow(config.api_key, orgId, {
71
132
  name,
72
133
  trigger_config: { endpoint_id: endpointId },
@@ -89,8 +150,13 @@ export async function update(id, flags) {
89
150
  const payload = {};
90
151
  if (typeof flags.name === 'string')
91
152
  payload.name = flags.name;
92
- if (typeof flags['endpoint-id'] === 'string')
93
- payload.trigger_config = { endpoint_id: flags['endpoint-id'] };
153
+ if (typeof flags['endpoint-id'] === 'string') {
154
+ const eid = flags['endpoint-id'];
155
+ if (!UUID_RE.test(eid)) {
156
+ error(`Invalid --endpoint-id "${eid}". Expected a webhook endpoint UUID.\nList your endpoints with: myapi webhook list`);
157
+ }
158
+ payload.trigger_config = { endpoint_id: eid };
159
+ }
94
160
  if (typeof flags.steps === 'string') {
95
161
  try {
96
162
  payload.steps = JSON.parse(flags.steps);
@@ -98,6 +164,7 @@ export async function update(id, flags) {
98
164
  catch {
99
165
  error('Invalid JSON for --steps');
100
166
  }
167
+ validateSteps(payload.steps);
101
168
  }
102
169
  if (!payload.name && !payload.trigger_config && !payload.steps) {
103
170
  error('Nothing to update. Provide at least one of --name, --endpoint-id, --steps.');
@@ -161,18 +228,48 @@ const SUBCOMMAND_USAGE = {
161
228
 
162
229
  Either form works; the positional name is the recommended shape.
163
230
 
164
- --steps is a JSON array of step objects. Supported step types:
165
- send_email — fields: from, to, subject, body | template_id, template_vars
166
- slack — fields: webhook_url, text
167
- http — fields: method, url, body, headers
231
+ --steps is a JSON array of step objects. Each step has a "type" field
232
+ plus type-specific fields. Two types are supported today:
233
+
234
+ type: "send_email" (alias: "email")
235
+ Required:
236
+ from Sender mailbox address (must be activated for sending)
237
+ to Recipient address (templating allowed)
238
+ subject Email subject (templating allowed)
239
+ Body — exactly one of:
240
+ body Plain text body
241
+ html Raw HTML body
242
+ template_id UUID of an AI-generated template (myapi email template
243
+ generate). Recommended for nice-looking emails.
244
+ Optional:
245
+ template_vars JSON object of variable substitutions, only with
246
+ template_id (e.g. {"name": "{{ payload.name }}"}).
247
+
248
+ type: "slack_message" (alias: "slack")
249
+ Required:
250
+ webhook_url https://hooks.slack.com/services/T.../B.../xxx
251
+ Get one from https://api.slack.com/apps → your app →
252
+ Incoming Webhooks. Other URLs are rejected.
253
+ text Message text (templating allowed)
254
+
255
+ Templating: any string field can reference the incoming webhook payload
256
+ with {{ payload.field }} (whitespace optional). Example:
257
+ "to": "{{ payload.email }}" → the email field from the form submission.
258
+
259
+ Unknown step types AND missing/invalid fields are rejected at create
260
+ time — you find out about typos before any workflow run happens.
168
261
 
169
- Field values support {{ payload.field }} templating to reference the
170
- incoming webhook payload, e.g. "to": "{{ payload.email }}".
262
+ There is NO on-failure notification mechanism today. Inspect run history
263
+ with: myapi workflow runs <id> · myapi workflow get-run <run_id>.
171
264
 
172
- Example — fire on every webhook POST, send a thank-you email:
265
+ Examples:
266
+ # send a thank-you email using an AI template, then ping Slack
173
267
  myapi workflow create "Contact handler" --endpoint-id <wid> --steps '[
174
- {"type":"send_email","from":"hello@x.com","to":"{{ payload.email }}",
175
- "subject":"Thanks!","body":"We got your message."}
268
+ {"type":"email","from":"hello@x.com","to":"{{ payload.email }}",
269
+ "subject":"Thanks!","template_id":"<tid>",
270
+ "template_vars":{"name":"{{ payload.name }}"}},
271
+ {"type":"slack","webhook_url":"https://hooks.slack.com/services/T0/B0/xxx",
272
+ "text":"New: {{ payload.message }}"}
176
273
  ]'
177
274
 
178
275
  See the my-workflow-api skill for the full form-to-email recipe.`,
package/dist/flags.js CHANGED
@@ -16,6 +16,7 @@ export function parseFlags(argv, schema = {}) {
16
16
  const merged = { ...GLOBAL_FLAGS, ...schema };
17
17
  const args = [];
18
18
  const flags = {};
19
+ const unknownFlags = [];
19
20
  for (let i = 0; i < argv.length; i++) {
20
21
  const arg = argv[i];
21
22
  if (arg === '-h') {
@@ -44,9 +45,24 @@ export function parseFlags(argv, schema = {}) {
44
45
  }
45
46
  const type = merged[key];
46
47
  // Unknown flags: tolerate as boolean (with --key=value still honored)
47
- // so additions to the schema don't silently break someone's script.
48
+ // so additions to the schema don't silently break someone's script
49
+ // BUT also collect them and emit a stderr warning at the end of
50
+ // parsing. Surfaces typos and AI-agent hallucinations like
51
+ // `--on-error <email>` without breaking back-compat.
48
52
  if (type === undefined) {
49
53
  flags[key] = inlineValue ?? true;
54
+ unknownFlags.push(`--${key}`);
55
+ // If no inline value and the next token doesn't look like another
56
+ // flag, the user almost certainly intended it as the flag's value
57
+ // (e.g. `--on-error someone@x.com`). Consume it so it doesn't
58
+ // become a stray positional arg later.
59
+ if (inlineValue === undefined) {
60
+ const next = argv[i + 1];
61
+ if (next !== undefined && !next.startsWith('--') && !next.startsWith('-h') && !next.startsWith('-v')) {
62
+ flags[key] = next;
63
+ i++;
64
+ }
65
+ }
50
66
  continue;
51
67
  }
52
68
  if (type === 'boolean') {
@@ -84,5 +100,11 @@ export function parseFlags(argv, schema = {}) {
84
100
  }
85
101
  args.push(arg);
86
102
  }
103
+ // Surface unknown flags so typos and hallucinated flags don't silently
104
+ // disappear. Don't block — the value is still in `flags` for any handler
105
+ // that wants it — just print one line on stderr.
106
+ if (unknownFlags.length > 0 && !process.env.MYAPI_QUIET_UNKNOWN_FLAGS) {
107
+ process.stderr.write(`› Note: ignoring unknown flag(s): ${unknownFlags.join(', ')}\n`);
108
+ }
87
109
  return { args, flags };
88
110
  }
package/dist/index.js CHANGED
@@ -58,6 +58,9 @@ const ERROR_MESSAGES = {
58
58
  RATE_LIMITED: 'Too many requests. Please wait a moment and try again.',
59
59
  INSUFFICIENT_BALANCE: 'Insufficient balance. Run: myapi billing topup <amount>',
60
60
  INVALID_AMOUNT: 'Amount out of range. Maximum single top-up is $100. Run: myapi billing topup <amount>',
61
+ // invalid_json_response intentionally absent — the SDK's MyApiError now
62
+ // builds a useful detailed message for that case (status + URL + body
63
+ // snippet), and friendlyError(err.code) would override it.
61
64
  };
62
65
  function friendlyError(code) {
63
66
  return ERROR_MESSAGES[code] || code;
@@ -6,7 +6,7 @@ Run a chain of actions (send email, post to Slack, call an HTTP endpoint) every
6
6
  ## What it does
7
7
 
8
8
  - Bind ordered step chains to webhook endpoints
9
- - Steps can: send transactional email, hit Slack, call any HTTP URL
9
+ - Steps can: send transactional email, post to Slack
10
10
  - Template values from the inbound webhook payload (`{{ payload.field }}`)
11
11
  - Per-run status tracking (attempt, error, started/finished timestamps)
12
12
  - Enable/disable without losing config
@@ -51,25 +51,36 @@ myapi workflow get-run <run_id>
51
51
 
52
52
  `--steps` is a JSON array of step objects. Each step has a `type` and type-specific fields.
53
53
 
54
+ Supported step types (alias forms in parentheses):
55
+
56
+ | `type` | Aliases | Required fields |
57
+ |---|---|---|
58
+ | `send_email` | `email` | `from`, `to`, `subject`, plus one of `body` / `html` / `template_id` |
59
+ | `slack_message` | `slack` | `webhook_url`, `text` |
60
+
54
61
  ```json
55
62
  [
56
63
  {
57
- "type": "send_email",
64
+ "type": "email",
58
65
  "from": "hello@yourdomain.com",
59
66
  "to": "{{ payload.email }}",
60
67
  "subject": "Welcome, {{ payload.name }}",
61
68
  "template_id": "<template_id>"
62
69
  },
63
70
  {
64
- "type": "http",
65
- "method": "POST",
66
- "url": "https://crm.example.com/contacts",
67
- "body": "{{ payload | json }}"
71
+ "type": "slack",
72
+ "webhook_url": "https://hooks.slack.com/services/T.../B.../xxx",
73
+ "text": "New submission from {{ payload.name }}"
68
74
  }
69
75
  ]
70
76
  ```
71
77
 
72
- The webhook payload is available as `{{ payload }}` and individual fields as `{{ payload.fieldname }}`.
78
+ Unknown step types are rejected at workflow create time, so typos surface
79
+ immediately instead of after 3 failed retries during execution.
80
+
81
+ The webhook payload is available as `{{ payload }}` and individual fields
82
+ as `{{ payload.fieldname }}`. Whitespace inside the braces is fine —
83
+ both `{{ payload.email }}` and `{{payload.email}}` work.
73
84
 
74
85
  ## End-to-end recipe — react to a contact form submission
75
86
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myapihq/cli",
3
- "version": "1.1.0-wip.1",
3
+ "version": "1.1.0-wip.3",
4
4
  "description": "MyAPI command-line interface",
5
5
  "type": "module",
6
6
  "files": [