@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
@@ -191,12 +191,12 @@ export async function run(subcommand, args, flags) {
191
191
  info(`Usage: myapi image <subcommand>
192
192
 
193
193
  Subcommands:
194
- list List all your generated images
194
+ delete <job_id> Delete the asset (job history retained)
195
195
  generate <prompt> Generate a new AI image (async, polls up to 90s; pricing per model — see "image models")
196
196
  get <job_id> Fetch full job metadata (JSON or human-readable)
197
197
  get-url <job_id> Print just the asset URL (curl-friendly)
198
+ list List all your generated images
198
199
  models List available image models with per-image pricing
199
- delete <job_id> Delete the asset (job history retained)
200
200
 
201
201
  All commands accept --org <id> (or set default: myapi config set-org <id>).
202
202
  The asset lands in your org's storage and is also visible via "myapi storage list".`);
@@ -174,8 +174,8 @@ slot grants, and an optional spend cap. A minted key is always a subset of the
174
174
  key that minted it (no privilege escalation).
175
175
 
176
176
  Subcommands:
177
- list List keys with kind, scope, grants, and spend cap
178
177
  create Mint a new key (value shown once)
178
+ list List keys with kind, scope, grants, and spend cap
179
179
  revoke <id> Revoke one key by ID
180
180
  revoke-all Kill switch — revoke every key (or --kind function|manual|account)
181
181
 
@@ -7,6 +7,7 @@ export const EXPOSES = [
7
7
  'POST /llm/orgs/{org_id}/complete',
8
8
  'POST /llm/orgs/{org_id}/embed',
9
9
  'GET /llm/orgs/{org_id}/models',
10
+ 'POST /llm/orgs/{org_id}/tasks/{verb}',
10
11
  ];
11
12
  export const SCHEMA = {
12
13
  org: 'string',
@@ -16,8 +17,36 @@ export const SCHEMA = {
16
17
  temperature: 'number',
17
18
  stop: 'string',
18
19
  file: 'string',
20
+ // verb-specific
21
+ labels: 'string',
22
+ multi: 'boolean',
23
+ schema: 'string',
24
+ style: 'string',
19
25
  kind: 'string',
26
+ context: 'string',
27
+ prompt: 'string',
28
+ tier: 'string',
20
29
  };
30
+ // Cache the catalog once per CLI invocation so verbs (which don't take a
31
+ // --model flag) can fall back to "the first chat model" without an extra
32
+ // network round-trip per command.
33
+ let cachedModels = null;
34
+ async function loadModels(apiKey, orgId) {
35
+ if (cachedModels)
36
+ return cachedModels;
37
+ const res = await sdkLlm.listModels(apiKey, orgId);
38
+ cachedModels = res.models;
39
+ return cachedModels;
40
+ }
41
+ async function defaultChatModel(apiKey, orgId) {
42
+ const models = await loadModels(apiKey, orgId);
43
+ const chat = models.find(m => m.kind === 'chat');
44
+ if (!chat) {
45
+ error('No chat model available — run "myapi llm models" to inspect the catalog.');
46
+ throw new Error('unreachable'); // satisfy the type checker
47
+ }
48
+ return chat.id;
49
+ }
21
50
  function readPromptArg(promptArg, flags) {
22
51
  if (typeof flags.file === 'string' && flags.file) {
23
52
  return fs.readFileSync(flags.file, 'utf-8');
@@ -27,11 +56,62 @@ function readPromptArg(promptArg, flags) {
27
56
  }
28
57
  return promptArg ?? '';
29
58
  }
30
- const DEFAULT_CHAT_MODEL = 'gemini-3.1-flash-lite-preview';
59
+ function fmtCostCents(cents) {
60
+ // Sub-cent calls (the common case for short prompts) get four decimals so
61
+ // the footer stays informative — "0.0063¢" reads honestly as "less than a
62
+ // cent." Larger numbers collapse to two decimals.
63
+ return cents < 1 ? `${cents.toFixed(4)}¢` : `${cents.toFixed(2)}¢`;
64
+ }
65
+ function parseSchemaFlag(flags) {
66
+ if (typeof flags.schema !== 'string' || !flags.schema)
67
+ return null;
68
+ // Accept either a path to a JSON file or an inline JSON string.
69
+ let raw = flags.schema;
70
+ if (fs.existsSync(raw)) {
71
+ try {
72
+ raw = fs.readFileSync(raw, 'utf-8');
73
+ }
74
+ catch (e) {
75
+ error(`Could not read --schema file: ${e.message}`);
76
+ }
77
+ }
78
+ try {
79
+ return JSON.parse(raw);
80
+ }
81
+ catch {
82
+ error('--schema must be valid JSON (inline) or a path to a JSON file');
83
+ return null;
84
+ }
85
+ }
86
+ function parseContextFlag(flags) {
87
+ if (typeof flags.context !== 'string' || !flags.context)
88
+ return undefined;
89
+ try {
90
+ const parsed = JSON.parse(flags.context);
91
+ if (typeof parsed === 'object' && parsed !== null)
92
+ return parsed;
93
+ error('--context must be a JSON object');
94
+ return undefined;
95
+ }
96
+ catch {
97
+ error('--context must be valid JSON');
98
+ return undefined;
99
+ }
100
+ }
101
+ function tierFromFlag(flags) {
102
+ if (typeof flags.tier !== 'string' || !flags.tier)
103
+ return undefined;
104
+ if (flags.tier === 'fast' || flags.tier === 'reasoning' || flags.tier === 'cheap')
105
+ return flags.tier;
106
+ error('--tier must be one of: fast, reasoning, cheap');
107
+ }
108
+ // ── raw ──────────────────────────────────────────────────────────────────
31
109
  async function complete(promptArg, flags) {
32
110
  const config = requireConfig();
33
111
  const orgId = requireOrg(flags, config, 'myapi llm complete "<prompt>" [--model <id>] [--system <s>] [--max-tokens N] [--temperature 0..1] [--stop <csv>] [--org <id>]');
34
- const model = typeof flags.model === 'string' && flags.model ? flags.model : DEFAULT_CHAT_MODEL;
112
+ const model = typeof flags.model === 'string' && flags.model
113
+ ? flags.model
114
+ : await defaultChatModel(config.api_key, orgId);
35
115
  const prompt = readPromptArg(promptArg, flags);
36
116
  if (!prompt.trim())
37
117
  error('Empty prompt. Pass <prompt> as an argument, "-" to read stdin, or --file <path>.');
@@ -56,7 +136,7 @@ async function complete(promptArg, flags) {
56
136
  }
57
137
  info(res.content);
58
138
  // Usage footer on stderr so it doesn't pollute piped output.
59
- process.stderr.write(`\n— ${res.model} · ${res.usage.input_tokens}+${res.usage.output_tokens} tokens · $${res.usage.cost_usd.toFixed(6)} · ${res.finish_reason}\n`);
139
+ process.stderr.write(`\n— ${res.model} · ${res.usage.input_tokens}+${res.usage.output_tokens ?? 0} tokens · ${fmtCostCents(res.usage.cost_cents)} · ${res.finish_reason}\n`);
60
140
  }
61
141
  async function embed(inputArg, flags) {
62
142
  const config = requireConfig();
@@ -73,7 +153,7 @@ async function embed(inputArg, flags) {
73
153
  return;
74
154
  }
75
155
  const first = res.embeddings[0] ?? [];
76
- info(`${res.model} · dim=${first.length} · ${res.usage.input_tokens} tokens · $${res.usage.cost_usd.toFixed(6)}`);
156
+ info(`${res.model} · dim=${first.length} · ${res.usage.input_tokens} tokens · ${fmtCostCents(res.usage.cost_cents)}`);
77
157
  info(`first 5: [${first.slice(0, 5).map(n => n.toFixed(6)).join(', ')}${first.length > 5 ? ', ...' : ''}]`);
78
158
  info(`(pass --json for the full vector)`);
79
159
  }
@@ -90,49 +170,198 @@ async function listModels(flags) {
90
170
  printTable(models.map(m => ({
91
171
  id: m.id,
92
172
  kind: m.kind,
93
- context: m.context ?? '',
173
+ context: m.context_window ?? '',
94
174
  dimensions: m.dimensions ?? '',
95
- in_per_1m: `$${m.input_per_1m}`,
96
- out_per_1m: m.output_per_1m != null ? `$${m.output_per_1m}` : '',
175
+ 'in ¢/1M': m.input_cost_per_1m_cents,
176
+ 'out ¢/1M': m.output_cost_per_1m_cents != null ? m.output_cost_per_1m_cents : '',
97
177
  })), { flags, empty: 'No models available.' });
98
178
  }
179
+ // ── verbs ────────────────────────────────────────────────────────────────
180
+ function printVerbFooter(usage) {
181
+ process.stderr.write(`\n— tier=${usage.tier_used} · ${usage.tokens_in}+${usage.tokens_out} tokens · ${fmtCostCents(usage.cost_cents)}\n`);
182
+ }
183
+ async function classify(inputArg, flags) {
184
+ const config = requireConfig();
185
+ const orgId = requireOrg(flags, config, 'myapi llm classify "<input>" --labels <csv> [--multi] [--tier <t>] [--org <id>]');
186
+ const input = readPromptArg(inputArg, flags);
187
+ if (!input.trim())
188
+ error('Empty input. Pass <input> as an argument, "-" to read stdin, or --file <path>.');
189
+ if (typeof flags.labels !== 'string' || !flags.labels) {
190
+ error('Missing required flag: --labels <comma-separated>');
191
+ return;
192
+ }
193
+ const labels = flags.labels.split(',').map(s => s.trim()).filter(Boolean);
194
+ if (labels.length === 0)
195
+ error('--labels must contain at least one label');
196
+ const multi = flags.multi === true;
197
+ const res = await sdkLlm.classify(config.api_key, orgId, { input, labels, multi, tier: tierFromFlag(flags) });
198
+ if (flags.json) {
199
+ printJson(res);
200
+ return;
201
+ }
202
+ if (multi) {
203
+ const arr = res.data.labels ?? [];
204
+ info(arr.length ? arr.join(', ') : '(no matching labels)');
205
+ }
206
+ else {
207
+ info(res.data.label ?? '(no label)');
208
+ }
209
+ printVerbFooter(res.usage);
210
+ }
211
+ async function extract(inputArg, flags) {
212
+ const config = requireConfig();
213
+ const orgId = requireOrg(flags, config, 'myapi llm extract "<input>" --schema <path|json> [--tier <t>] [--org <id>]');
214
+ const input = readPromptArg(inputArg, flags);
215
+ if (!input.trim())
216
+ error('Empty input. Pass <input> as an argument, "-" to read stdin, or --file <path>.');
217
+ const schema = parseSchemaFlag(flags);
218
+ if (!schema) {
219
+ error('Missing required flag: --schema <path|json>');
220
+ return;
221
+ }
222
+ const res = await sdkLlm.extract(config.api_key, orgId, { input, schema, tier: tierFromFlag(flags) });
223
+ if (flags.json) {
224
+ printJson(res);
225
+ return;
226
+ }
227
+ info(JSON.stringify(res.data.data, null, 2));
228
+ printVerbFooter(res.usage);
229
+ }
230
+ async function summarize(inputArg, flags) {
231
+ const config = requireConfig();
232
+ const orgId = requireOrg(flags, config, 'myapi llm summarize "<input>" [--style brief|exec|bullet] [--tier <t>] [--org <id>]');
233
+ const input = readPromptArg(inputArg, flags);
234
+ if (!input.trim())
235
+ error('Empty input. Pass <input> as an argument, "-" to read stdin, or --file <path>.');
236
+ const style = typeof flags.style === 'string' && flags.style ? flags.style : undefined;
237
+ if (style && !['brief', 'exec', 'bullet'].includes(style)) {
238
+ error('--style must be one of: brief, exec, bullet');
239
+ }
240
+ const res = await sdkLlm.summarize(config.api_key, orgId, {
241
+ input,
242
+ style: style,
243
+ tier: tierFromFlag(flags),
244
+ });
245
+ if (flags.json) {
246
+ printJson(res);
247
+ return;
248
+ }
249
+ info(res.data.summary);
250
+ printVerbFooter(res.usage);
251
+ }
252
+ async function draft(inputArg, flags) {
253
+ const config = requireConfig();
254
+ const orgId = requireOrg(flags, config, 'myapi llm draft --kind <what> [--prompt "<instructions>"] [--context <json>] ["<source text>"] [--tier <t>] [--org <id>]');
255
+ if (typeof flags.kind !== 'string' || !flags.kind) {
256
+ error('Missing required flag: --kind <email|message|reply|...>');
257
+ return;
258
+ }
259
+ const input = readPromptArg(inputArg, flags);
260
+ const promptText = typeof flags.prompt === 'string' ? flags.prompt : '';
261
+ const ctx = parseContextFlag(flags);
262
+ if (!input.trim() && !promptText.trim() && (!ctx || Object.keys(ctx).length === 0)) {
263
+ error('draft needs at least one of: <source text> (arg or --file), --prompt, or --context.');
264
+ }
265
+ const res = await sdkLlm.draft(config.api_key, orgId, {
266
+ input: input || undefined,
267
+ kind: flags.kind,
268
+ context: ctx,
269
+ prompt: promptText || undefined,
270
+ tier: tierFromFlag(flags),
271
+ });
272
+ if (flags.json) {
273
+ printJson(res);
274
+ return;
275
+ }
276
+ info(res.data.text);
277
+ printVerbFooter(res.usage);
278
+ }
279
+ // ── help ─────────────────────────────────────────────────────────────────
99
280
  const SUBCOMMAND_USAGE = {
100
281
  complete: `myapi llm complete "<prompt>" [--model <id>]
101
282
  [--system "<system msg>"] [--max-tokens N] [--temperature 0..1]
102
283
  [--stop <csv>] [--file <path>] [--org <id>] [--json]
103
284
 
104
- Default model: gemini-3.1-flash-lite-preview (cheap + fast). Pass --model
105
- to override; run "myapi llm models" to see the catalog.
285
+ Raw chat completion against a self-hosted catalog model. Without --model,
286
+ picks the first chat model from "myapi llm models" (today:
287
+ Qwen/Qwen3-Coder-30B-A3B-Instruct).
106
288
 
107
- Pass "-" as the prompt to read from stdin. The reply goes to stdout;
108
- a one-line usage footer (tokens + cost) goes to stderr so it doesn't
109
- pollute piped output.
289
+ Pass "-" as the prompt to read from stdin. The reply goes to stdout; a
290
+ one-line usage footer (tokens + cost in cents) goes to stderr so it
291
+ doesn't pollute piped output.
110
292
 
111
293
  Examples:
112
294
  myapi llm complete "summarize: $(cat README.md)"
113
- cat draft.md | myapi llm complete - --model gemini-3.1-pro-preview --system "You are an editor"
114
- myapi llm complete --file prompt.txt --max-tokens 200`,
295
+ cat draft.md | myapi llm complete - --system "You are an editor" --max-tokens 200`,
115
296
  embed: `myapi llm embed "<text>" --model <id> [--file <path>] [--org <id>] [--json]
116
297
 
117
- Returns a vector summary by default; pass --json for the full vector.
298
+ No embedding model is served on the raw catalog today this returns
299
+ EMBED_NOT_AVAILABLE until one is. Use a dedicated embedding API for now.`,
300
+ models: `myapi llm models [--kind chat|embed] [--org <id>] [--json]
301
+
302
+ Lists the live model catalog with per-1M-token pricing in cents.
303
+ The catalog reflects exactly what the inference gateway serves.`,
304
+ classify: `myapi llm classify "<input>" --labels <a,b,c> [--multi]
305
+ [--tier fast|reasoning|cheap] [--file <path>] [--org <id>] [--json]
306
+
307
+ Pick the best label (or with --multi, all applicable labels) from a set.
308
+ Output goes to stdout; usage footer to stderr.
118
309
 
119
310
  Examples:
120
- myapi llm embed "the quick brown fox" --model gemini-embedding-001
121
- myapi llm embed --file doc.txt --model gemini-embedding-001 --json > doc.vec.json`,
122
- models: `myapi llm models [--kind chat|embed] [--org <id>] [--json]
311
+ myapi llm classify "I was charged twice and need a refund" \\
312
+ --labels billing,technical,sales,spam
313
+ cat email.txt | myapi llm classify - --labels urgent,bug,question --multi`,
314
+ extract: `myapi llm extract "<input>" --schema <path|json>
315
+ [--tier <t>] [--file <path>] [--org <id>] [--json]
316
+
317
+ Extract data from <input> conforming to a JSON Schema. --schema accepts
318
+ an inline JSON string OR a path to a .json file. Stdout is the extracted
319
+ JSON object.
320
+
321
+ Examples:
322
+ myapi llm extract "Acme Corp has 250 employees" \\
323
+ --schema '{"type":"object","properties":{"company":{"type":"string"},"employees":{"type":"integer"}}}'
324
+ myapi llm extract - --schema ./order.schema.json < order.eml`,
325
+ summarize: `myapi llm summarize "<input>" [--style brief|exec|bullet]
326
+ [--tier <t>] [--file <path>] [--org <id>] [--json]
123
327
 
124
- Lists the model catalog with per-1M-token pricing at upstream rates.
125
- Models prefixed with vendor name pass through to that vendor; un-prefixed
126
- models (when available) run on MyAPI's own inference.`,
328
+ Summarize text. Default style 'brief' (1-2 sentences); 'exec' is a
329
+ decision-maker summary; 'bullet' is a short list.
330
+
331
+ Examples:
332
+ myapi llm summarize --file long-thread.txt --style bullet
333
+ cat report.md | myapi llm summarize - --style exec`,
334
+ draft: `myapi llm draft --kind <what> [--prompt "<instructions>"]
335
+ [--context '<json>'] ["<source text>"] [--file <path>]
336
+ [--tier <t>] [--org <id>] [--json]
337
+
338
+ Draft a piece of writing (email, reply, message, …). Provide at least one
339
+ of: <source text> (arg or --file), --prompt, or --context (JSON of facts).
340
+
341
+ Examples:
342
+ myapi llm draft --kind email \\
343
+ --prompt "Friendly welcome to the beta. Under 60 words." \\
344
+ --context '{"recipient":"a new signup","product":"MyAPI"}'
345
+ cat inbound.eml | myapi llm draft - --kind reply --prompt "Acknowledge and ask for the order ID"`,
127
346
  };
128
347
  export async function run(subcommand, args, flags) {
129
348
  if (!subcommand || (flags.help && !subcommand)) {
130
349
  info(`Usage: myapi llm <subcommand>
131
350
 
351
+ Two surfaces:
352
+
353
+ Raw — you pick a self-hosted catalog model and shape the prompt yourself.
354
+ Verbs — you ask for a task done; the model is implementation detail and
355
+ is never named in the response.
356
+
132
357
  Subcommands:
133
- complete Chat completion returns reply + usage (default model: gemini-3.1-flash-lite-preview)
134
- embed Text embedding returns vector + usage
135
- models List available models (chat + embed) with pricing
358
+ classify Pick a label from a set
359
+ complete Raw chat completion against a catalog model
360
+ draft Write something (email | reply | message | …)
361
+ embed Raw embeddings (no model served on raw today)
362
+ extract Pull structured data conforming to a JSON Schema
363
+ models List the live model catalog (id, kind, context, cents/1M)
364
+ summarize Summarize text (brief | exec | bullet)
136
365
 
137
366
  All commands accept --org <id> (or set default: myapi config set-org <id>).
138
367
  Use this for workflow steps and scripted pipelines — not as a replacement
@@ -151,6 +380,10 @@ for your own reasoning. The agent has its own model already.`);
151
380
  case 'complete': return complete(args[0], flags);
152
381
  case 'embed': return embed(args[0], flags);
153
382
  case 'models': return listModels(flags);
383
+ case 'classify': return classify(args[0], flags);
384
+ case 'extract': return extract(args[0], flags);
385
+ case 'summarize': return summarize(args[0], flags);
386
+ case 'draft': return draft(args[0], flags);
154
387
  default: error(`Unknown subcommand: ${subcommand}. Run "myapi llm --help" for valid subcommands.`);
155
388
  }
156
389
  }
@@ -217,12 +217,12 @@ export async function run(subcommand, args, flags) {
217
217
  info(`Usage: myapi org <subcommand>
218
218
 
219
219
  Subcommands:
220
- list List organizations
221
220
  create Create an organization (e.g. myapi org create "Name" --yes)
222
- update Update an organization's fields (name, tagline, description, etc.)
223
- get Get details of an organization
224
221
  delete Delete an organization
225
- sync-brand Sync brand info (name, logo, description) from an existing website`);
222
+ get Get details of an organization
223
+ list List organizations
224
+ sync-brand Sync brand info (name, logo, description) from an existing website
225
+ update Update an organization's fields (name, tagline, description, etc.)`);
226
226
  return;
227
227
  }
228
228
  if (flags.help) {
@@ -189,12 +189,12 @@ Take payments with Stripe Checkout. Connect your Stripe account, then
189
189
  create one-off or recurring charges and refund them.
190
190
 
191
191
  Subcommands:
192
- connect Link your Stripe account (T0 — bring your own key)
193
- status Show the org's Stripe connection status
194
192
  charge Create a charge and get a hosted checkout URL
195
- list List charges in your org
193
+ connect Link your Stripe account (T0 — bring your own key)
196
194
  get <charge_id> Inspect a charge
195
+ list List charges in your org
197
196
  refund <charge_id> Full-refund a charge
197
+ status Show the org's Stripe connection status
198
198
 
199
199
  All commands accept --org <id> (or set default: myapi config set-org <id>).`);
200
200
  return;
@@ -113,8 +113,8 @@ export async function run(subcommand, args, flags) {
113
113
  info(`Usage: myapi people <subcommand>
114
114
 
115
115
  Subcommands:
116
- search Filter people across the Goldfox database
117
116
  get Get a single person (with embedded company)
117
+ search Filter people across the Goldfox database
118
118
 
119
119
  All commands accept --org <id> (or set a default: myapi config set-org <id>).
120
120
  Filter shape is shared with company + audience — see myapi audience --help.`);
@@ -156,12 +156,12 @@ export async function run(subcommand, args, flags) {
156
156
  info(`Usage: myapi pixel <subcommand>
157
157
 
158
158
  Subcommands:
159
+ audience Geographic distribution sample of your pixel audience
160
+ events Engagement events (sent / open / click / page_visit) by campaign or domain
161
+ identity Resolve the identity graph (emails, IPs, profiles) for a pixel ID
159
162
  interactions Get a unified timeline of visits and events
160
163
  (requires at least one filter: --website, --campaign-id, or --domain)
161
164
  visits Page-visit timeline scoped to a website host
162
- events Engagement events (sent / open / click / page_visit) by campaign or domain
163
- audience Geographic distribution sample of your pixel audience
164
- identity Resolve the identity graph (emails, IPs, profiles) for a pixel ID
165
165
 
166
166
  All commands accept --org <id> (or set default: myapi config set-org <id>).`);
167
167
  return;
@@ -7,6 +7,7 @@ export const EXPOSES = [
7
7
  'POST /queue/orgs/{org_id}/queues',
8
8
  'GET /queue/orgs/{org_id}/queues',
9
9
  'GET /queue/orgs/{org_id}/queues/{name}',
10
+ 'DELETE /queue/orgs/{org_id}/queues/{name}',
10
11
  'POST /queue/orgs/{org_id}/queues/{name}/jobs',
11
12
  'GET /queue/orgs/{org_id}/queues/{name}/jobs',
12
13
  'GET /queue/orgs/{org_id}/jobs/{id}',
@@ -141,12 +141,12 @@ export async function run(subcommand, args, flags) {
141
141
  info(`Usage: myapi storage <subcommand>
142
142
 
143
143
  Subcommands:
144
- list List all your stored assets
145
- ingest <url> Server pulls a public URL into storage
146
- upload <file> Direct upload of a local file
144
+ delete <asset_id> Permanently delete a stored asset
147
145
  get <asset_id> Fetch asset metadata (name, URL, created_at)
148
146
  get-url <asset_id> Print the public CDN URL (no API call)
149
- delete <asset_id> Permanently delete a stored asset
147
+ ingest <url> Server pulls a public URL into storage
148
+ list List all your stored assets
149
+ upload <file> Direct upload of a local file
150
150
 
151
151
  All commands accept --org <id> (or set default: myapi config set-org <id>).`);
152
152
  return;
@@ -159,11 +159,11 @@ export async function run(subcommand, args, flags) {
159
159
  info(`Usage: myapi webhook <subcommand>
160
160
 
161
161
  Subcommands:
162
- list List all inbound webhook endpoints
163
162
  create Create a new endpoint to receive data (returns an inbound URL)
164
- update Patch endpoint name / description / crm-email-path / forward-url
165
163
  delete Delete a webhook endpoint
166
164
  delivery Inspect a specific webhook delivery (payload, received_at)
165
+ list List all inbound webhook endpoints
166
+ update Patch endpoint name / description / crm-email-path / forward-url
167
167
 
168
168
  All commands accept --org <id> (or set default: myapi config set-org <id>).
169
169
 
@@ -94,10 +94,19 @@ describe('_validateSteps — http_request / http (2026-05-15)', () => {
94
94
  .toMatch(/missing required field "url"/);
95
95
  });
96
96
  it('rejects non-http(s) URLs', () => {
97
+ // post-brutalize: real URL parse instead of prefix regex —
98
+ // /must use http:\/\/ or https:\/\/ scheme/ applies to anything
99
+ // that parses to a non-http(s) protocol (ftp, javascript, file,
100
+ // gopher, data, …); strings that don't parse as URLs at all get
101
+ // the separate "not a valid URL" message.
97
102
  expect(_validateSteps([{ ...VALID_HTTP, url: 'ftp://example.com' }]))
98
- .toMatch(/must start with http:\/\/ or https:\/\//);
103
+ .toMatch(/must use http:\/\/ or https:\/\/ scheme/);
99
104
  expect(_validateSteps([{ ...VALID_HTTP, url: 'javascript:alert(1)' }]))
100
- .toMatch(/must start with http:\/\/ or https:\/\//);
105
+ .toMatch(/must use http:\/\/ or https:\/\/ scheme/);
106
+ expect(_validateSteps([{ ...VALID_HTTP, url: 'file:///etc/passwd' }]))
107
+ .toMatch(/must use http:\/\/ or https:\/\/ scheme/);
108
+ expect(_validateSteps([{ ...VALID_HTTP, url: 'not a url' }]))
109
+ .toMatch(/is not a valid URL/);
101
110
  });
102
111
  it('rejects unknown HTTP methods', () => {
103
112
  expect(_validateSteps([{ ...VALID_HTTP, method: 'TRACE' }]))
@@ -91,8 +91,21 @@ export function _validateSteps(steps) {
91
91
  if (!s.url || typeof s.url !== 'string') {
92
92
  return `${where} (${s.type}): missing required field "url".`;
93
93
  }
94
- if (!/^https?:\/\//.test(s.url)) {
95
- return `${where} (${s.type}): "url" must start with http:// or https:// — got "${s.url}".`;
94
+ // Real URL parse instead of prefix regex — catches javascript:,
95
+ // file:, gopher:, data:, and whitespace/null-byte hosts that pass
96
+ // /^https?:\/\// but fail proper parsing. Mirrors the scheme check
97
+ // on webhook.forward_url and queue.consumer_url; same shape as the
98
+ // SDK helper. The backend ALSO validates — see
99
+ // docs/cross-repo-prompts/backend-workflow-step-validation.md.
100
+ let parsedUrl;
101
+ try {
102
+ parsedUrl = new URL(s.url);
103
+ }
104
+ catch {
105
+ return `${where} (${s.type}): "url" is not a valid URL — got "${s.url}".`;
106
+ }
107
+ if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
108
+ return `${where} (${s.type}): "url" must use http:// or https:// scheme — got "${parsedUrl.protocol}" in "${s.url}".`;
96
109
  }
97
110
  if (s.method !== undefined) {
98
111
  if (typeof s.method !== 'string' || !HTTP_METHODS.has(s.method.toUpperCase())) {
@@ -360,15 +373,15 @@ export async function run(subcommand, args, flags) {
360
373
  myapi workflow create --help
361
374
 
362
375
  Subcommands:
363
- list List all workflows
364
- get <id> Show a single workflow
365
376
  create Create a workflow (enabled by default; --no-enable to stage)
366
- update <id> Patch name / endpoint-id / steps
367
- enable <id> Enable a workflow
368
- disable <id> Disable a workflow
369
377
  delete <id> Delete a workflow
370
- runs <workflow_id> List recent executions of a workflow
378
+ disable <id> Disable a workflow
379
+ enable <id> Enable a workflow
380
+ get <id> Show a single workflow
371
381
  get-run <run_id> Show a single run with full payload
382
+ list List all workflows
383
+ runs <workflow_id> List recent executions of a workflow
384
+ update <id> Patch name / endpoint-id / steps
372
385
 
373
386
  Step types for --steps:
374
387
  send_email {"type":"send_email","from":"<mailbox>","to":"{{payload.email}}","subject":"...","template_id":"<id>"|"html":"..."}
@@ -49,7 +49,7 @@ export const SUBCOMMANDS = {
49
49
  people: ['search', 'get'],
50
50
  company: ['search', 'get'],
51
51
  audience: ['create', 'list', 'get', 'update', 'delete', 'members', 'refresh'],
52
- llm: ['complete', 'embed', 'models'],
52
+ llm: ['complete', 'embed', 'models', 'classify', 'extract', 'summarize', 'draft'],
53
53
  database: ['namespaces', 'create', 'delete-namespace', 'keys', 'get', 'set', 'del'],
54
54
  crm: ['contacts', 'companies'],
55
55
  url: ['shorten'],
package/dist/index.js CHANGED
@@ -399,40 +399,40 @@ Usage: myapi <command> [subcommand] [args]
399
399
  myapi --version
400
400
 
401
401
  Commands:
402
+ audience Save filter snapshots as named audiences (people or companies)
402
403
  auth Manage account · setup · whoami · link
403
- status Single-screen view of account + resources in the default org
404
- install-skills Install or update the MyAPI skills pack for AI agents
405
404
  billing Check balance and manage billing
406
- org Manage organizations (tip: myapi org create "name" --yes to auto-set as default)
407
- update Update CLI and skills to the latest version
405
+ company Search the company database (filter by industry, size, country, ...)
406
+ container Run containers services, workers, and scheduled jobs
407
+ crm Canonical store of engaged contacts + companies, with auto-ingest from webhooks
408
+ database KV store with namespaces + CAS — the substrate for stateful agent apps
409
+ doctor Org-wide consistency check — funnels, webhooks, domains, containers
408
410
  domain Manage domain configurations
409
- funnel Manage websites (publish pages, custom domains, funnels)
411
+ email Manage mailboxes, send/read email, templates, and campaigns
410
412
  fn Create and deploy functions on the edge runtime
411
- container Run containers services, workers, and scheduled jobs
413
+ funnel Manage websites (publish pages, custom domains, funnels)
412
414
  git Hosted git repositories — repos, commits, branches, history
415
+ image Generate AI images and manage them in storage
416
+ install-skills Install or update the MyAPI skills pack for AI agents
417
+ llm Run LLM completions and embeddings (chat + embed, with usage/cost)
418
+ org Manage organizations (tip: myapi org create "name" --yes to auto-set as default)
419
+ payments Take payments with Stripe Checkout (connect, charge, refund)
420
+ people Search the contact database (filter by industry, seniority, country, ...)
421
+ pixel Read pixel analytics: visits, events, identity resolution
413
422
  queue Durable job queue — enqueue work, retried against an HTTP consumer
423
+ status Single-screen view of account + resources in the default org
424
+ storage Upload, ingest, list, and serve assets from edge storage
414
425
  task Agent-task queue — file, claim, and resolve units of work
415
- doctor Org-wide consistency check funnels, webhooks, domains, containers
416
- payments Take payments with Stripe Checkout (connect, charge, refund)
426
+ update Update CLI and skills to the latest version
427
+ url Shorten URLs to compact myurlto.com links
417
428
  webhook Manage inbound webhook endpoints and inspect deliveries
418
- email Manage mailboxes, send/read email, templates, and campaigns
419
429
  workflow Run actions (send email, post to Slack) when a webhook fires
420
- image Generate AI images and manage them in storage
421
- storage Upload, ingest, list, and serve assets from edge storage
422
- pixel Read pixel analytics: visits, events, identity resolution
423
- people Search the contact database (filter by industry, seniority, country, ...)
424
- company Search the company database (filter by industry, size, country, ...)
425
- audience Save filter snapshots as named audiences (people or companies)
426
- llm Run LLM completions and embeddings (chat + embed, with usage/cost)
427
- database KV store with namespaces + CAS — the substrate for stateful agent apps
428
- crm Canonical store of engaged contacts + companies, with auto-ingest from webhooks
429
- url Shorten URLs to compact myurlto.com links
430
430
 
431
431
  Aliases:
432
- whoami → myapi auth whoami
432
+ config → myapi auth config
433
433
  keys → myapi auth api-keys
434
434
  setup → myapi auth setup
435
- config → myapi auth config
435
+ whoami → myapi auth whoami
436
436
 
437
437
  Run "myapi <command> --help" for subcommand help.
438
438