@myapihq/cli 1.3.6 → 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.
@@ -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) {
@@ -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 = {};
@@ -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
 
@@ -314,6 +491,7 @@ export async function run(subcommand, args, flags) {
314
491
  Subcommands:
315
492
  create Create a funnel (optional --name; backend defaults to org's preview_subdomain)
316
493
  delete Delete a funnel
494
+ form Emit HTML for a form that posts to the canonical submit proxy
317
495
  get <id> Get funnel details
318
496
  list List funnels
319
497
  pages List the pages currently published to a funnel
@@ -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
  }
@@ -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
  }
@@ -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}',
@@ -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())) {
@@ -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'],
@@ -17,7 +17,7 @@ Funnels are the publishing surface. You create a funnel under an org (one comman
17
17
 
18
18
  By default, your funnel lives on a free preview subdomain (`*.makeautonomous.com`) you get with every org. To serve on a custom domain, register and assign one via **mydomainapi** first.
19
19
 
20
- Every funnel includes two public proxy endpoints your HTML can call directly (no API key needed): a **form submit** endpoint that forwards POSTs to your org's webhook, and an **analytics/event ingest** for pageviews and click tracking.
20
+ Every funnel auto-provisions a **webhook** at creation time (`org_webhook_id`). The funnel exposes two public proxy endpoints your HTML can call directly (no API key needed): a **form submit** at `POST /funnel/funnels/{id}/submit/{slug}` that delivers submissions through that webhook (CRM upsert + bound workflows fire automatically), and an **analytics/event ingest** for pageviews and click tracking. For multi-form funnels you can bind individual slugs to specific destinations with `myapi funnel form ... --capture-to webhook:<id>`.
21
21
  <!-- llm:end -->
22
22
 
23
23
  ## Commands
@@ -30,6 +30,7 @@ Every funnel includes two public proxy endpoints your HTML can call directly (no
30
30
  | `myapi funnel delete <id>` | Delete the funnel and purge its edge pages |
31
31
  | `myapi funnel push [slug]` | Push HTML from stdin to a slug (default: `/`) |
32
32
  | `myapi funnel pages [funnel_id]` | List the pages currently published to a funnel |
33
+ | `myapi funnel form [funnel_id]` | Emit canonical form HTML (and register a binding with `--capture-to`) |
33
34
  | `myapi funnel verify [slug]` | Verify a published page is reachable + check links/webhooks |
34
35
  <!-- generated:end -->
35
36
 
@@ -58,12 +59,39 @@ myapi funnel delete <funnel_id>
58
59
  Omitting `[slug]` defaults to `/`. The funnel id is resolved from `--funnel`, the saved default funnel, or — only if the org has exactly one funnel — auto-picked.
59
60
  <!-- llm:end -->
60
61
 
61
- ## Form Submissions & Analytics
62
+ ## Form submissions (canonical recipe)
62
63
 
63
- Funnels expose two public proxy endpoints your published HTML can hit directly (no API key needed):
64
+ The funnel submit proxy is the canonical form-capture path. **Never hardcode `https://api.mywebhookapi.com/webhook/in/<slug>` into funnel HTML** that URL leaks into your git history, breaks on rotation, gets scraped, and locks the platform out of future form-quality features (rate-limit, captcha, anti-bot, field validation). Use the proxy instead — same plumbing, hidden URL, automatic CRM ingest on `email`.
64
65
 
65
- - **Form submit:** `POST /funnel/funnels/{id}/submit/{slug}` validates payload and forwards to your org's configured webhook.
66
- - **Analytics/tracking:** `POST /funnel/funnels/{id}/event` — proxies pageviews, clicks, and pixel events to your webhook. Rate-limited to 60 req/min per funnel.
66
+ Zero-config form (the happy path most agents want):
67
+
68
+ ```bash
69
+ myapi funnel create
70
+ myapi funnel form --slug join --fields email:required,name > snippet.html
71
+ # paste snippet.html into your page (or pipe through funnel push):
72
+ cat page-with-snippet.html | myapi funnel push /
73
+ # Submissions land in the funnel's auto-provisioned webhook → CRM upsert on `email` → any bound workflow fires.
74
+ ```
75
+
76
+ Multi-form funnel (each slug to a different destination):
77
+
78
+ ```bash
79
+ # Bind /checkout to a specific webhook:
80
+ myapi funnel form <funnel_id> --slug checkout \
81
+ --fields email:required,plan,phone:tel \
82
+ --capture-to webhook:<checkout_webhook_id>
83
+
84
+ # Bind /survey to a workflow directly:
85
+ myapi funnel form <funnel_id> --slug survey \
86
+ --fields email:required,score:number \
87
+ --capture-to workflow:<survey_workflow_id>
88
+ ```
89
+
90
+ `--capture-to` registers the per-slug binding via `POST /funnel/orgs/.../funnels/{id}/forms`. Without it, submissions fall through to the funnel's default webhook.
91
+
92
+ ## Analytics
93
+
94
+ `POST /funnel/funnels/{id}/event` — public proxy for pageviews, clicks, and pixel events (no API key needed). Rate-limited to 60 req/min per funnel.
67
95
 
68
96
  ## Notes
69
97
 
@@ -1,19 +1,29 @@
1
1
  # my-llm-api
2
2
 
3
- Provider-agnostic LLM completions and embeddings, billed at upstream cost. Today routes to Gemini; the model id is just a string, so additional providers land without breaking callers.
3
+ Two-surface LLM primitive. Self-hosted open-source inference on the raw surface, objective verbs on the verb surface. Pricing is in cents per 1M tokens at upstream rate.
4
4
 
5
5
  ## What it does
6
6
 
7
- - **Chat completion** — messages array (role/content), get reply + usage + cost
8
- - **Embeddings** — single string or batch, returns vector(s) + usage
9
- - **Model catalog** — list available models with per-1M-token pricing
7
+ Two surfaces, one credential, one balance:
8
+
9
+ - **Raw** — `complete`, `embed`, `models`. You pick a self-hosted catalog model and shape the prompt yourself. Today's catalog: `Qwen/Qwen3-Coder-30B-A3B-Instruct` (1 chat model, no embed model). Proprietary models are not callable here — that's how MyAPI stays off the reseller framing.
10
+ - **Verbs** — `classify`, `extract`, `summarize`, `draft`. You ask for a task done; the model is implementation detail and is never named in the response. This is where any future proprietary model lives, wrapped behind the verb contract.
10
11
 
11
12
  ## Quickstart
12
13
 
13
14
  ```bash
15
+ # Catalog (live — reflects what the gateway actually serves)
14
16
  myapi llm models
15
- myapi llm complete "explain HMAC in two sentences" # defaults to gemini-3.1-flash-lite-preview
16
- myapi llm embed "the quick brown fox" --model gemini-embedding-001
17
+
18
+ # Raw picks the first chat model from the catalog automatically
19
+ myapi llm complete "explain HMAC in two sentences"
20
+
21
+ # Verbs — recommended for workflow steps
22
+ myapi llm classify "I want a refund" --labels billing,sales,support
23
+ myapi llm extract "Acme Corp has 250 employees" \
24
+ --schema '{"type":"object","properties":{"company":{"type":"string"},"employees":{"type":"integer"}}}'
25
+ myapi llm summarize --file long.md --style exec
26
+ myapi llm draft --kind email --prompt "Friendly welcome, under 60 words"
17
27
  ```
18
28
 
19
29
  ## Authentication
@@ -26,10 +36,10 @@ Requires `api_key` and `org_id` from **myapihq**. Inference cost is debited from
26
36
 
27
37
  ## When to use
28
38
 
29
- This is the **workflow-step LLM** — use it inside scripted pipelines (summarize a doc, classify an email, embed text for similarity). It is **not** a replacement for your own reasoning if you're an agent; you already have a model.
39
+ This is the **workflow-step LLM** — use it inside scripted pipelines (summarize a doc, classify an email, extract structured data, draft a reply). It is **not** a replacement for your own reasoning if you're an agent; you already have a model.
30
40
 
31
41
  ## Documentation
32
42
 
33
- Model catalog, message shapes, and cost semantics: see `SKILL.md`.
43
+ Two-surface design, raw/verb shapes, and cost semantics: see `SKILL.md`.
34
44
 
35
45
  Run `myapi llm --help` for inline reference.
@@ -1,29 +1,39 @@
1
1
  ---
2
2
  name: my-llm-api
3
- version: 1.0.0
3
+ version: 1.1.0
4
4
  description: >
5
- Provider-agnostic LLM completions and embeddings, billed at upstream cost.
6
- The model id is just a string — today routes to Gemini, more providers land
7
- without breaking callers. Use this inside workflow steps and scripted
8
- pipelines, not as a replacement for your own reasoning.
9
- triggers: [llm, completion, chat, embed, embedding, gemini, inference, summarize, classify, vector]
5
+ Two-surface LLM primitive. Raw chat completion against self-hosted
6
+ open-source models (you pick the model), and objective verbs
7
+ (classify / extract / summarize / draft) that hide the model behind a
8
+ task. Pricing in cents per 1M tokens; charged from your MyAPI balance.
9
+ triggers: [llm, completion, chat, embed, embedding, inference, classify, extract, summarize, draft, qwen]
10
10
  checksum: sha256-pending
11
11
  ---
12
12
 
13
13
  # MyLLMAPI
14
14
 
15
- A thin, OpenAI-compatible-shape LLM gateway. Three endpoints: `complete` (chat), `embed` (vectors), `models` (catalog). Pricing is per-1M tokens at the upstream rate while we build our own inference; cost is debited from your MyAPI balance.
15
+ A two-surface LLM gateway:
16
+
17
+ - **Raw** — `complete` / `embed` / `models`. You pick a self-hosted catalog model (today: `Qwen/Qwen3-Coder-30B-A3B-Instruct`). Proprietary models are not callable here.
18
+ - **Verbs** — `classify` / `extract` / `summarize` / `draft`. You ask for a task done; the model is implementation detail and is never named in the response.
19
+
20
+ Pricing is in cents per 1M tokens at the actual upstream rate; cost is debited from your MyAPI balance.
16
21
 
17
22
  ## Capabilities
18
23
  <!-- llm:start -->
19
- Use this for workflow tasks — summarize a doc, classify an inbound email, embed text for similarity, expand a one-line prompt into structured copy. The reply goes to stdout; a one-line usage footer (tokens + cost + finish reason) goes to stderr, so `myapi llm complete ... | jq` and similar pipelines work as expected.
24
+ Use this for workflow tasks — summarize a doc, classify an inbound email, extract structured data, draft a reply. The reply goes to stdout; a one-line usage footer (tokens + cost + finish reason) goes to stderr, so `myapi llm complete ... | jq` and similar pipelines work as expected.
25
+
26
+ **Don't use this as your own model.** If you're an agent reading this, you already have a more capable model than what's exposed here. Reach for the LLM verbs when you're scripting a recurring step where a small/cheap model is the right tool — not for one-shot reasoning you can just do yourself.
20
27
 
21
- **Don't use this as your own model.** If you're an agent reading this, you already have a more capable model than what's exposed here. Reach for `llm complete` when you're scripting a recurring step where a small/cheap model is the right tool — not for one-shot reasoning that you can just do yourself.
28
+ ### Raw vs. verbs
22
29
 
23
- ### Messages shape
30
+ - **Raw `complete`** — full control: pick the model, build the `messages` array, set `max_tokens`/`temperature`/`stop`. Use when shape matters.
31
+ - **Verbs** — you want a *result* (label, JSON, summary, draft). Use when you don't care which model runs underneath.
32
+
33
+ ### Raw `complete` request
24
34
  ```json
25
35
  {
26
- "model": "gemini-3.1-flash-lite-preview",
36
+ "model": "Qwen/Qwen3-Coder-30B-A3B-Instruct",
27
37
  "messages": [
28
38
  { "role": "system", "content": "You are a terse editor." },
29
39
  { "role": "user", "content": "Tighten this paragraph: ..." }
@@ -36,90 +46,109 @@ Use this for workflow tasks — summarize a doc, classify an inbound email, embe
36
46
 
37
47
  Roles: `system | user | assistant`. Multiple system messages collapse to one instruction. `max_tokens`, `temperature`, and `stop` are optional — per-model defaults apply.
38
48
 
39
- ### Complete response
49
+ ### Raw `complete` response
40
50
  ```json
41
51
  {
42
- "model": "gemini-3.1-flash-lite-preview",
52
+ "model": "Qwen/Qwen3-Coder-30B-A3B-Instruct",
43
53
  "content": "...assistant reply...",
44
54
  "finish_reason": "stop",
45
- "usage": { "input_tokens": 42, "output_tokens": 87, "cost_usd": 0.000029 }
55
+ "usage": { "input_tokens": 42, "output_tokens": 87, "cost_cents": 0.029 }
46
56
  }
47
57
  ```
48
58
 
49
- `finish_reason` is typically `stop` (model returned naturally), `length` (hit max_tokens — increase if you need more), or `safety` (blocked).
59
+ `finish_reason` is one of `stop` (normal), `length` (hit max_tokens), `filter` (blocked).
50
60
 
51
- ### Embed response
52
- ```json
53
- {
54
- "model": "gemini-embedding-001",
55
- "embeddings": [[-0.0226, 0.0118, ...]],
56
- "usage": { "input_tokens": 5, "cost_usd": 0.0000007 }
57
- }
58
- ```
61
+ If `model` isn't in the self-hosted catalog the server returns `MODEL_NOT_IN_RAW_CATALOG` — that's the signal to use a verb instead, not to retry with a different `--model`.
62
+
63
+ ### Raw `embed`
59
64
 
60
- `embeddings` is always `number[][]` even a single-string input returns a one-element array of vectors. Embed-only usage has no `output_tokens`.
65
+ No embedding model is served on the raw catalog today; calls return `EMBED_NOT_AVAILABLE`. Use a dedicated embedding API for now.
61
66
 
62
67
  ### Model catalog
63
- - Chat models: `id`, `kind: 'chat'`, `context` (token window), `input_per_1m`, `output_per_1m`
64
- - Embed models: `id`, `kind: 'embed'`, `dimensions`, `input_per_1m`
68
+ - Chat models: `id`, `kind: 'chat'`, `context_window`, `input_cost_per_1m_cents`, `output_cost_per_1m_cents`
69
+ - Embed models: `id`, `kind: 'embed'`, `dimensions`, `input_cost_per_1m_cents`
70
+
71
+ The catalog is **live** — it reflects what the inference gateway actually serves, refreshed every 15 minutes. Always query `models` rather than hard-coding ids.
72
+
73
+ ### Verb requests + responses
74
+
75
+ Every verb takes an optional `tier` (`fast | reasoning | cheap`) — opaque routing hint, server picks the model. Response usage block is identical across verbs.
76
+
77
+ | Verb | Request body | Response `data` |
78
+ |---|---|---|
79
+ | `classify` | `{ input, labels: string[], multi?: boolean, tier? }` | `{ label }` or `{ labels: string[] }` (when `multi`) |
80
+ | `extract` | `{ input, schema: <json-schema>, tier? }` | `{ data: <object conforming to schema> }` |
81
+ | `summarize`| `{ input, style?: 'brief'\|'exec'\|'bullet', tier? }` | `{ summary }` |
82
+ | `draft` | `{ input?, kind: string, context?: object, prompt?: string, tier? }` | `{ text }` |
83
+
84
+ Shared usage block on every verb:
85
+
86
+ ```json
87
+ { "tier_used": "fast", "tokens_in": 65, "tokens_out": 37, "cost_cents": 0.005 }
88
+ ```
89
+
90
+ The model/provider is **never** named in the verb response. That's the point — the verb is the contract, the model is implementation.
65
91
 
66
- Today: `gemini-3.1-pro-preview`, `gemini-3.1-flash-lite-preview`, `gemini-embedding-001`. Catalog is live — always query `models` rather than hard-coding ids.
67
92
  <!-- llm:end -->
68
93
 
69
94
  ## Commands
70
95
  <!-- generated:start -->
71
96
  | Command | What it does |
72
97
  |---|---|
73
- | `myapi llm models [--kind chat\|embed] [--json]` | List available models with pricing |
74
- | `myapi llm complete "<prompt>" [--model <id>] [--system "<s>"] [--max-tokens N] [--temperature 0..1] [--stop <csv>] [--file <path>] [--json]` | Chat completion; reply to stdout, usage to stderr. Default model: `gemini-3.1-flash-lite-preview` |
75
- | `myapi llm embed "<text>" --model <id> [--file <path>] [--json]` | Embed a string; default render shows model + dim + first 5 values |
98
+ | `myapi llm models [--kind chat\|embed] [--json]` | List the live model catalog with pricing (cents/1M) |
99
+ | `myapi llm complete "<prompt>" [--model <id>] [--system "<s>"] [--max-tokens N] [--temperature 0..1] [--stop <csv>] [--file <path>] [--json]` | Raw chat completion; reply to stdout, usage to stderr. Defaults to the first chat model in the catalog |
100
+ | `myapi llm embed "<text>" --model <id> [--json]` | Embed a string (no model served today returns EMBED_NOT_AVAILABLE) |
101
+ | `myapi llm classify "<input>" --labels <csv> [--multi] [--tier <t>] [--json]` | Pick a label from a set |
102
+ | `myapi llm extract "<input>" --schema <path\|json> [--tier <t>] [--json]` | Pull structured data conforming to a JSON Schema |
103
+ | `myapi llm summarize "<input>" [--style brief\|exec\|bullet] [--tier <t>] [--json]` | Summarize text |
104
+ | `myapi llm draft --kind <what> [--prompt "<s>"] [--context <json>] ["<src>"] [--tier <t>] [--json]` | Draft an email / reply / message / … |
76
105
  <!-- generated:end -->
77
106
 
78
- Pass `-` as the prompt to read from stdin. Pass `--file <path>` to read a longer prompt from disk.
107
+ Pass `-` as the prompt/input to read from stdin. Pass `--file <path>` to read longer content from disk.
79
108
 
80
109
  ## Examples
81
110
  <!-- llm:start -->
82
111
  ```bash
83
- # List the catalog
112
+ # List the live catalog
84
113
  myapi llm models
85
- # Filter to embed-only
86
- myapi llm models --kind embed --json | jq '.models[].id'
114
+ myapi llm models --kind chat --json | jq '.models[].id'
87
115
 
88
- # One-shot completion (default model = gemini-3.1-flash-lite-preview)
116
+ # Raw completion picks the first chat model from the catalog
89
117
  myapi llm complete "Summarize in 12 words: $(cat README.md)"
90
118
 
91
- # Override the model (e.g. when you need pro-tier quality)
92
- myapi llm complete "Polish this draft: ..." \
93
- --model gemini-3.1-pro-preview \
94
- --system "You are a terse copy editor" \
95
- --max-tokens 300
119
+ # Pin a specific model
120
+ myapi llm complete "Refactor this function: ..." \
121
+ --model Qwen/Qwen3-Coder-30B-A3B-Instruct \
122
+ --system "You are a careful Go reviewer." \
123
+ --max-tokens 600
96
124
 
97
- # Read from stdin (useful in pipelines)
98
- cat email.txt | myapi llm complete - \
99
- --system "Classify intent: support | sales | spam"
125
+ # ── Verbs (recommended for workflow steps) ──────────────────────────────
100
126
 
101
- # Embed and save the vector
102
- myapi llm embed --file doc.txt --model gemini-embedding-001 --json \
103
- > doc.vec.json
104
- ```
127
+ myapi llm classify "I was charged twice — please refund." \
128
+ --labels billing,technical,sales,spam
105
129
 
106
- ### End-to-end recipe classify inbound webhooks
107
- ```bash
108
- # Pull the last delivery body, classify with a tiny model, route accordingly
109
- BODY=$(myapi webhook delivery <delivery_id> --json | jq -r '.body')
110
- INTENT=$(printf '%s' "$BODY" | myapi llm complete - \
111
- --system 'Respond with one word: support, sales, or spam.' \
112
- --max-tokens 5)
113
- echo "Routing $INTENT"
130
+ myapi llm extract "Acme Corp employs 250 people in Berlin." \
131
+ --schema '{"type":"object","properties":{"company":{"type":"string"},"employees":{"type":"integer"}}}'
132
+
133
+ myapi llm summarize --file long-thread.txt --style bullet
134
+
135
+ myapi llm draft --kind email \
136
+ --prompt "Friendly welcome, under 60 words." \
137
+ --context '{"recipient":"a new signup","product":"MyAPI"}'
138
+
139
+ # Classify + route an inbound webhook delivery
140
+ BODY=$(myapi webhook delivery <id> --json | jq -r '.body')
141
+ INTENT=$(printf '%s' "$BODY" | myapi llm classify - \
142
+ --labels support,sales,spam --json | jq -r '.data.label')
114
143
  ```
115
144
  <!-- llm:end -->
116
145
 
117
146
  ## Notes
118
147
 
119
- - **No upstream tokens involved.** MyAPI never holds your Gemini/OpenAI/Anthropic credentials we hold ours, you pay us at-cost while we build our own inference. The model id is just a string; routing happens server-side.
120
- - **Latency.** Flash-lite 200–800ms first token. Pro 600ms–2s. Embeddings sub-second.
121
- - **Cost.** `usage.cost_usd` is the authoritative number debited at upstream rate (no MyAPI markup today). Use this for cost-accounting in workflows.
122
- - **Streaming.** Not exposed yet `complete` returns the full reply. Add streaming support when there's a use case that requires it.
123
- - **Bring-your-own-key.** Not supported by design. MyAPI's value here is unified billing + a stable API across providers; managing your upstream keys would defeat both.
124
-
125
- Run `myapi llm --help` for inline reference.
148
+ - **`draft` context safety — what the platform does and doesn't do.** Every field in `context` is quoted into the prompt as referent data the model is told to incorporate. Sensitive-named keys (`secret`, `api_key`, `password`, `token`, etc.) are **not** automatically redacted — if you pass `{"secret": "abc123"}`, the model may include `"abc123"` in the output. The verb does two things on top:
149
+ - **Injection defense.** Keys named `instructions`, `system`, `system_prompt`, `prompt`, `override`, `directives`, and the JS prototype-pollution sentinels are stripped at the boundary and surfaced in `meta.warnings`; the model can't be hijacked through them.
150
+ - **Output guardrail.** After generation, every fact value (length ≥ 4) is substring-scanned in the output; matches surface under `meta.guardrails.facts_in_output: ["secret", "api_key", ...]`. This is a **signal, not a redaction** you see when a value landed in the body.
151
+ Rule of thumb: if a value must not appear in the output, **do not put it in `context`**. Pass identifiers (recipient name, account id, topic) and let the prompt reference them indirectly; keep credentials, PII, and any internal-only metadata out of the body entirely.
152
+ - **Self-hosted raw, server-picked verbs.** Raw runs on MyAPI's TPU; verbs route wherever the server picks (future proprietary models land here, wrapped behind the verb contract).
153
+ - **Cost + latency.** `usage.cost_cents` is authoritative — no markup today. Qwen3-Coder-30B-A3B: 200–600ms to first token, 1–3s end-to-end on a few-hundred-token reply.
154
+ - **Live catalog, no streaming, no BYOK.** Don't hard-code model ids — `models` is the source of truth (CLI picks if `--model` omitted). Full reply only.
@@ -2,8 +2,8 @@
2
2
  name: my-webhook-api
3
3
  version: 1.0.0
4
4
  description: >
5
- Inbound webhook endpoints. Receive HTTP POSTs from third parties (forms, Stripe, GitHub, etc.) and either inspect deliveries directly or wire them to a workflow.
6
- triggers: [webhook, inbound, receiver, form submission, stripe events, slack notification, delivery, payload, event ingest]
5
+ Inbound webhook endpoints for non-funnel sources Stripe, GitHub, custom services. Funnel forms use the my-funnel-api proxy.
6
+ triggers: [webhook, inbound, receiver, stripe events, github webhook, slack notification, delivery, payload, event ingest]
7
7
  checksum: sha256-pending
8
8
  ---
9
9
 
@@ -13,6 +13,8 @@ Per-org HTTP endpoints that accept inbound POSTs and durably store every deliver
13
13
 
14
14
  ## Capabilities
15
15
  <!-- llm:start -->
16
+ **Scope: non-funnel inbound.** Use this for Stripe → MyAPI, GitHub → MyAPI, custom services → MyAPI, anything where an *external* system POSTs to you. **Funnel forms use the funnel proxy** (see `my-funnel-api`: `myapi funnel form ... --capture-to webhook:<id>` if you need to bind a specific destination; otherwise the funnel's auto-provisioned webhook handles it). Don't hardcode `api.mywebhookapi.com/webhook/in/<slug>` URLs into funnel HTML.
17
+
16
18
  Each endpoint has a unique inbound URL minted at create time. Anyone who knows the URL can POST to it; the body is stored verbatim along with headers and a timestamp. Deliveries are kept indefinitely (until you delete the endpoint).
17
19
 
18
20
  The inbound URL accepts **any JSON body** — no enforced schema, no required fields. Whatever you POST is what gets stored. Two consequences:
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@myapihq/cli",
3
3
  "license": "Apache-2.0",
4
- "version": "1.3.6",
4
+ "version": "1.3.8",
5
5
  "description": "MyAPI command-line interface",
6
6
  "type": "module",
7
7
  "files": [
@@ -24,12 +24,13 @@
24
24
  "check-coverage": "npm run build && node scripts/check-coverage.js",
25
25
  "check-coverage:live": "npm run build && node scripts/check-coverage.js --live",
26
26
  "update-schema": "node scripts/update-schema.js",
27
+ "lint:changelog": "node ../../scripts/lint-changelog.js",
28
+ "lint:help-order": "node scripts/lint-help-order.js",
27
29
  "lint:skills": "node scripts/lint-skills.js",
28
- "lint:skills:strict": "node scripts/lint-skills.js --strict",
29
- "lint:changelog": "node ../../scripts/lint-changelog.js"
30
+ "lint:skills:strict": "node scripts/lint-skills.js --strict"
30
31
  },
31
32
  "dependencies": {
32
- "@myapihq/sdk": "^1.3.6"
33
+ "@myapihq/sdk": "^1.3.8"
33
34
  },
34
35
  "devDependencies": {
35
36
  "@types/node": "^25.6.0",