@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.
- package/dist/commands/crm/companies.js +1 -0
- package/dist/commands/crm/contacts.js +1 -0
- package/dist/commands/funnel.d.ts +1 -0
- package/dist/commands/funnel.js +184 -5
- package/dist/commands/llm.js +257 -24
- package/dist/commands/queue.js +1 -0
- package/dist/commands/workflow-validation.test.js +11 -2
- package/dist/commands/workflow.js +15 -2
- package/dist/completion.js +1 -1
- package/dist/skills/my-funnel-api/SKILL.md +33 -5
- package/dist/skills/my-llm-api/README.md +18 -8
- package/dist/skills/my-llm-api/SKILL.md +91 -62
- package/dist/skills/my-webhook-api/SKILL.md +4 -2
- package/package.json +5 -4
|
@@ -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>;
|
package/dist/commands/funnel.js
CHANGED
|
@@ -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, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
273
|
+
const escText = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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
|
}
|
package/dist/commands/llm.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 ·
|
|
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 ·
|
|
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.
|
|
173
|
+
context: m.context_window ?? '',
|
|
94
174
|
dimensions: m.dimensions ?? '',
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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 - --
|
|
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
|
-
|
|
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
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
}
|
package/dist/commands/queue.js
CHANGED
|
@@ -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
|
|
103
|
+
.toMatch(/must use http:\/\/ or https:\/\/ scheme/);
|
|
99
104
|
expect(_validateSteps([{ ...VALID_HTTP, url: 'javascript:alert(1)' }]))
|
|
100
|
-
.toMatch(/must
|
|
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
|
-
|
|
95
|
-
|
|
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())) {
|
package/dist/completion.js
CHANGED
|
@@ -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
|
|
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
|
|
62
|
+
## Form submissions (canonical recipe)
|
|
62
63
|
|
|
63
|
-
|
|
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
|
-
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
- **
|
|
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
|
-
|
|
16
|
-
|
|
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,
|
|
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
|
-
|
|
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.
|
|
3
|
+
version: 1.1.0
|
|
4
4
|
description: >
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
triggers: [llm, completion, chat, embed, embedding,
|
|
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
|
|
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,
|
|
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
|
-
|
|
28
|
+
### Raw vs. verbs
|
|
22
29
|
|
|
23
|
-
|
|
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": "
|
|
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
|
-
###
|
|
49
|
+
### Raw `complete` response
|
|
40
50
|
```json
|
|
41
51
|
{
|
|
42
|
-
"model": "
|
|
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, "
|
|
55
|
+
"usage": { "input_tokens": 42, "output_tokens": 87, "cost_cents": 0.029 }
|
|
46
56
|
}
|
|
47
57
|
```
|
|
48
58
|
|
|
49
|
-
`finish_reason` is
|
|
59
|
+
`finish_reason` is one of `stop` (normal), `length` (hit max_tokens), `filter` (blocked).
|
|
50
60
|
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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'`, `
|
|
64
|
-
- Embed models: `id`, `kind: 'embed'`, `dimensions`, `
|
|
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
|
|
74
|
-
| `myapi llm complete "<prompt>" [--model <id>] [--system "<s>"] [--max-tokens N] [--temperature 0..1] [--stop <csv>] [--file <path>] [--json]` |
|
|
75
|
-
| `myapi llm embed "<text>" --model <id> [--
|
|
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
|
|
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
|
-
|
|
86
|
-
myapi llm models --kind embed --json | jq '.models[].id'
|
|
114
|
+
myapi llm models --kind chat --json | jq '.models[].id'
|
|
87
115
|
|
|
88
|
-
#
|
|
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
|
-
#
|
|
92
|
-
myapi llm complete "
|
|
93
|
-
--model
|
|
94
|
-
--system "You are a
|
|
95
|
-
--max-tokens
|
|
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
|
-
#
|
|
98
|
-
cat email.txt | myapi llm complete - \
|
|
99
|
-
--system "Classify intent: support | sales | spam"
|
|
125
|
+
# ── Verbs (recommended for workflow steps) ──────────────────────────────
|
|
100
126
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
> doc.vec.json
|
|
104
|
-
```
|
|
127
|
+
myapi llm classify "I was charged twice — please refund." \
|
|
128
|
+
--labels billing,technical,sales,spam
|
|
105
129
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
--
|
|
113
|
-
|
|
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
|
-
-
|
|
120
|
-
- **
|
|
121
|
-
- **
|
|
122
|
-
|
|
123
|
-
- **
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
6
|
-
triggers: [webhook, inbound, receiver,
|
|
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.
|
|
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.
|
|
33
|
+
"@myapihq/sdk": "^1.3.8"
|
|
33
34
|
},
|
|
34
35
|
"devDependencies": {
|
|
35
36
|
"@types/node": "^25.6.0",
|