@myapihq/cli 1.3.6 → 1.3.11
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/account.d.ts +6 -0
- package/dist/commands/account.js +75 -0
- package/dist/commands/config.js +2 -2
- package/dist/commands/crm/companies.js +1 -0
- package/dist/commands/crm/contacts.js +1 -0
- package/dist/commands/doctor-setup.test.d.ts +1 -0
- package/dist/commands/doctor-setup.test.js +74 -0
- package/dist/commands/doctor.d.ts +5 -0
- package/dist/commands/doctor.js +73 -1
- package/dist/commands/domain.d.ts +1 -0
- package/dist/commands/domain.js +38 -1
- package/dist/commands/email/mailbox.js +19 -0
- package/dist/commands/funnel.d.ts +1 -0
- package/dist/commands/funnel.js +219 -16
- 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 +4 -3
- package/dist/exposes.test.js +1 -0
- package/dist/index.js +7 -0
- package/dist/skills/my-funnel-api/SKILL.md +35 -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
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
|
|
@@ -70,16 +83,12 @@ export async function get(id, flags) {
|
|
|
70
83
|
const orgId = requireOrg(flags, config, 'myapi funnel get <id> [--org <id>]');
|
|
71
84
|
if (!id)
|
|
72
85
|
error('Missing required arguments.\nUsage: myapi funnel get <id> [--org <id>]');
|
|
73
|
-
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// Coalesce both so this works regardless of which shape we get back.
|
|
78
|
-
const funnel = raw.funnel ?? raw;
|
|
79
|
-
const subdomain_url = raw.subdomain_url ?? funnel.subdomain_url;
|
|
80
|
-
const domain_url = raw.domain_url ?? funnel.domain_url;
|
|
86
|
+
// SDK normalizes the wire shape to the wrapped envelope. `--json`
|
|
87
|
+
// still emits the envelope (back-compat: legacy consumers parse it).
|
|
88
|
+
const result = await sdkFunnel.getFunnel(config.api_key, orgId, id);
|
|
89
|
+
const { funnel, subdomain_url, domain_url } = result;
|
|
81
90
|
if (flags.json) {
|
|
82
|
-
printJson(
|
|
91
|
+
printJson(result);
|
|
83
92
|
return;
|
|
84
93
|
}
|
|
85
94
|
info(`ID: ${funnel.id}`);
|
|
@@ -91,8 +100,8 @@ export async function get(id, flags) {
|
|
|
91
100
|
info(`Created: ${formatDate(funnel.created_at)}`);
|
|
92
101
|
if (funnel.updated_at)
|
|
93
102
|
info(`Updated: ${formatDate(funnel.updated_at)}`);
|
|
94
|
-
|
|
95
|
-
|
|
103
|
+
// `pages` isn't part of the typed Funnel; backend doesn't include it
|
|
104
|
+
// here (use `myapi funnel pages` for that). Drop the inline count.
|
|
96
105
|
}
|
|
97
106
|
export async function del(id, flags) {
|
|
98
107
|
const config = requireConfig();
|
|
@@ -178,6 +187,157 @@ export async function push(slug, flags) {
|
|
|
178
187
|
}
|
|
179
188
|
}
|
|
180
189
|
}
|
|
190
|
+
function parseField(token) {
|
|
191
|
+
// <name>[:<modifier>]... — modifiers are types or `required`, order-free.
|
|
192
|
+
const parts = token.split(':').map(p => p.trim()).filter(Boolean);
|
|
193
|
+
const name = parts[0];
|
|
194
|
+
if (!name || !/^[a-z][a-z0-9_]*$/i.test(name)) {
|
|
195
|
+
error(`Invalid field name "${name}". Lowercase letters/digits/underscores; starts with a letter.`);
|
|
196
|
+
}
|
|
197
|
+
let type;
|
|
198
|
+
let required = false;
|
|
199
|
+
for (const mod of parts.slice(1)) {
|
|
200
|
+
const m = mod.toLowerCase();
|
|
201
|
+
if (m === 'required')
|
|
202
|
+
required = true;
|
|
203
|
+
else if (m === 'text' || m === 'email' || m === 'number' || m === 'tel')
|
|
204
|
+
type = m;
|
|
205
|
+
else
|
|
206
|
+
error(`Unknown field modifier "${mod}" on "${name}". Use one of: text, email, number, tel, required.`);
|
|
207
|
+
}
|
|
208
|
+
if (!type) {
|
|
209
|
+
const n = name.toLowerCase();
|
|
210
|
+
type = n === 'email' ? 'email'
|
|
211
|
+
: n === 'phone' || n === 'tel' ? 'tel'
|
|
212
|
+
: 'text';
|
|
213
|
+
}
|
|
214
|
+
return { name, type, required };
|
|
215
|
+
}
|
|
216
|
+
function parseCaptureTo(raw) {
|
|
217
|
+
const m = raw.match(/^(webhook|workflow):([a-f0-9-]{36})$/i);
|
|
218
|
+
if (!m)
|
|
219
|
+
error(`Invalid --capture-to "${raw}". Expected webhook:<uuid> or workflow:<uuid>.`);
|
|
220
|
+
return { kind: m[1].toLowerCase(), id: m[2] };
|
|
221
|
+
}
|
|
222
|
+
export async function formCmd(funnelArg, flags) {
|
|
223
|
+
const config = requireConfig();
|
|
224
|
+
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]');
|
|
225
|
+
// Resolve funnel id the same way push/verify do.
|
|
226
|
+
let funnelId = funnelArg || flags.funnel || config.default_funnel;
|
|
227
|
+
if (!funnelId) {
|
|
228
|
+
const existing = await sdkFunnel.listFunnels(config.api_key, orgId);
|
|
229
|
+
if (existing.length === 0)
|
|
230
|
+
error('No funnel found for this org. Create one with: myapi funnel create');
|
|
231
|
+
if (existing.length > 1) {
|
|
232
|
+
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')}`);
|
|
233
|
+
}
|
|
234
|
+
funnelId = existing[0].id;
|
|
235
|
+
}
|
|
236
|
+
const rawSlug = flags.slug || 'join';
|
|
237
|
+
const slug = rawSlug.startsWith('/') ? rawSlug.slice(1) : rawSlug;
|
|
238
|
+
if (!/^[a-z0-9][a-z0-9-]{0,40}$/i.test(slug)) {
|
|
239
|
+
error(`Invalid --slug "${slug}". Lowercase letters/digits/hyphens, 1-41 chars, starts with a letter or digit.`);
|
|
240
|
+
}
|
|
241
|
+
// `--fields email:required,name,phone:tel,plan:text:required`
|
|
242
|
+
const fieldsArg = (flags.fields || 'email:email:required').trim();
|
|
243
|
+
const fieldSpecs = fieldsArg.split(',').map(s => s.trim()).filter(Boolean).map(parseField);
|
|
244
|
+
if (fieldSpecs.length === 0)
|
|
245
|
+
error('--fields must list at least one field. Example: --fields email:required,name');
|
|
246
|
+
// Honeypot field — match the backend default unless --honeypot overrides.
|
|
247
|
+
const honeypot = flags.honeypot || 'middle_name';
|
|
248
|
+
if (!/^[a-z][a-z0-9_]*$/i.test(honeypot))
|
|
249
|
+
error(`Invalid --honeypot "${honeypot}".`);
|
|
250
|
+
for (const f of fieldSpecs)
|
|
251
|
+
if (f.name === honeypot)
|
|
252
|
+
error(`Honeypot "${honeypot}" collides with a real field. Pick a different --honeypot name.`);
|
|
253
|
+
const cta = flags.cta || 'Sign up';
|
|
254
|
+
const successMsg = flags.success || "Thanks — we'll be in touch.";
|
|
255
|
+
// Resolve the binding destination.
|
|
256
|
+
//
|
|
257
|
+
// Without a binding, the backend falls submissions through to the
|
|
258
|
+
// funnel's default webhook with NO honeypot / field validation. That
|
|
259
|
+
// makes the on-page honeypot purely cosmetic and lets trivial bots
|
|
260
|
+
// write to your CRM. So: always register a binding. When
|
|
261
|
+
// `--capture-to` is omitted, target the funnel's auto-provisioned
|
|
262
|
+
// `org_webhook_id` — same destination as the fallback, just with
|
|
263
|
+
// server-side guards turned on. Backend POST upserts on duplicate
|
|
264
|
+
// slug, so re-runs are safe.
|
|
265
|
+
let registeredBinding;
|
|
266
|
+
let autoBound = false;
|
|
267
|
+
let dest = null;
|
|
268
|
+
if (typeof flags['capture-to'] === 'string' && flags['capture-to']) {
|
|
269
|
+
dest = parseCaptureTo(flags['capture-to']);
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
const { funnel } = await sdkFunnel.getFunnel(config.api_key, orgId, funnelId);
|
|
273
|
+
if (funnel.org_webhook_id) {
|
|
274
|
+
dest = { kind: 'webhook', id: funnel.org_webhook_id };
|
|
275
|
+
autoBound = true;
|
|
276
|
+
}
|
|
277
|
+
// Legacy funnels migrated before 2026-06-03 may briefly lack
|
|
278
|
+
// org_webhook_id — emit the HTML without a binding and warn.
|
|
279
|
+
}
|
|
280
|
+
if (dest) {
|
|
281
|
+
const binding = {
|
|
282
|
+
slug,
|
|
283
|
+
destination: `${dest.kind}:${dest.id}`,
|
|
284
|
+
fields: fieldSpecs.map(f => ({ name: f.name, required: f.required })),
|
|
285
|
+
honeypot_field: honeypot,
|
|
286
|
+
};
|
|
287
|
+
registeredBinding = await sdkFunnel.createFormBinding(config.api_key, orgId, funnelId, binding);
|
|
288
|
+
}
|
|
289
|
+
const url = `https://api.myapihq.com/funnel/funnels/${funnelId}/submit/${encodeURIComponent(slug)}`;
|
|
290
|
+
const escAttr = (s) => s.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
291
|
+
const escText = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
292
|
+
// Per-field labels — Email uses the canonical placeholder; others
|
|
293
|
+
// Title-Case the name.
|
|
294
|
+
const inputs = fieldSpecs.map(f => {
|
|
295
|
+
const label = f.name.charAt(0).toUpperCase() + f.name.slice(1).replace(/_/g, ' ');
|
|
296
|
+
const req = f.required ? ' required' : '';
|
|
297
|
+
return ` <label>${escText(label)} <input type="${f.type}" name="${escAttr(f.name)}"${req}></label>`;
|
|
298
|
+
}).join('\n');
|
|
299
|
+
// Honeypot — hidden via inline style, off the tab order, autocomplete
|
|
300
|
+
// disabled. Bots fill it; humans don't.
|
|
301
|
+
const honeypotInput = ` <input type="text" name="${escAttr(honeypot)}" style="display:none" tabindex="-1" autocomplete="off">`;
|
|
302
|
+
const html = `<form action="${escAttr(url)}" method="POST" data-myapi-form>
|
|
303
|
+
${inputs}
|
|
304
|
+
${honeypotInput}
|
|
305
|
+
<button type="submit">${escText(cta)}</button>
|
|
306
|
+
</form>
|
|
307
|
+
<script>
|
|
308
|
+
document.querySelectorAll('[data-myapi-form]').forEach(f => {
|
|
309
|
+
f.addEventListener('submit', async e => {
|
|
310
|
+
e.preventDefault();
|
|
311
|
+
const r = await fetch(f.action, { method:'POST', headers:{'Content-Type':'application/json'},
|
|
312
|
+
body: JSON.stringify(Object.fromEntries(new FormData(f))) });
|
|
313
|
+
if (r.ok) f.outerHTML = ${JSON.stringify(`<p>${escText(successMsg)}</p>`)};
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
</script>`;
|
|
317
|
+
if (flags.json) {
|
|
318
|
+
printJson({
|
|
319
|
+
action_url: url,
|
|
320
|
+
slug,
|
|
321
|
+
fields: fieldSpecs,
|
|
322
|
+
honeypot_field: honeypot,
|
|
323
|
+
binding_registered: !!registeredBinding,
|
|
324
|
+
binding: registeredBinding,
|
|
325
|
+
html,
|
|
326
|
+
});
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (registeredBinding) {
|
|
330
|
+
// Print confirmation on stderr so stdout stays the clean snippet.
|
|
331
|
+
const tag = autoBound ? ' (auto, default webhook)' : '';
|
|
332
|
+
process.stderr.write(`✓ Registered form binding: slug=${slug} → ${registeredBinding.destination}${tag}\n`);
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
// No org_webhook_id and no --capture-to → honeypot/field guards
|
|
336
|
+
// won't fire. Surface it so the agent can act.
|
|
337
|
+
process.stderr.write(`⚠ No binding registered (funnel has no org_webhook_id). Honeypot and field guards are NOT enforced for this slug. Pass --capture-to webhook:<id> or workflow:<id>, or wait for the backend migration to backfill org_webhook_id.\n`);
|
|
338
|
+
}
|
|
339
|
+
process.stdout.write(html + '\n');
|
|
340
|
+
}
|
|
181
341
|
// Verify uses the same shape as push: slug positional (default '/'), funnel
|
|
182
342
|
// resolved from --funnel / default / the org's only funnel.
|
|
183
343
|
export async function verify(slug, flags) {
|
|
@@ -260,10 +420,51 @@ export async function publish(dir, flags) {
|
|
|
260
420
|
}
|
|
261
421
|
// ── Dispatcher ───────────────────────────────────────────────────────────────
|
|
262
422
|
const SUBCOMMAND_USAGE = {
|
|
263
|
-
'list': 'myapi funnel list [--org <id>] [--json]',
|
|
264
423
|
'create': 'myapi funnel create [--name <name>] [--org <id>]',
|
|
265
|
-
'get': 'myapi funnel get <id> [--org <id>] [--json]',
|
|
266
424
|
'delete': 'myapi funnel delete <id> [--org <id>]',
|
|
425
|
+
'form': `myapi funnel form [funnel-id] [--slug <slug>] [--fields <spec>]
|
|
426
|
+
[--capture-to <dest>] [--honeypot <name>]
|
|
427
|
+
[--cta "<button text>"] [--success "<message>"] [--org <id>] [--json]
|
|
428
|
+
|
|
429
|
+
Emits a ready-to-paste HTML snippet for a form that posts to the canonical
|
|
430
|
+
funnel submit proxy. Submissions to any slug fall through to the funnel's
|
|
431
|
+
auto-provisioned webhook by default (CRM ingest + bound workflows fire
|
|
432
|
+
without further wiring). The proxy URL is platform-stable; slug names the
|
|
433
|
+
form so workflows can route on it; an auto-honeypot field stops casual
|
|
434
|
+
bots.
|
|
435
|
+
|
|
436
|
+
--slug <s> Form identifier — appears in the proxy URL.
|
|
437
|
+
Defaults to "join". Lowercase letters/digits/hyphens.
|
|
438
|
+
--fields <spec> Comma-separated field specs.
|
|
439
|
+
Each spec: <name>[:<type>][:required] (modifiers
|
|
440
|
+
order-free). Types: text (default), email, number,
|
|
441
|
+
tel. Name auto-promotes to email if name="email",
|
|
442
|
+
to tel if name="phone" or "tel".
|
|
443
|
+
Defaults to "email:email:required".
|
|
444
|
+
--capture-to <dest> Optional. Register a per-slug binding to the
|
|
445
|
+
destination via POST /funnel/.../forms. Format:
|
|
446
|
+
webhook:<uuid> or workflow:<uuid>. Without it,
|
|
447
|
+
submissions fall through to the funnel's default
|
|
448
|
+
webhook (most agents want this).
|
|
449
|
+
--honeypot <name> Hidden bot-trap input name. Default "middle_name".
|
|
450
|
+
--cta "<text>" Submit button text. Defaults to "Sign up".
|
|
451
|
+
--success "<text>" Message shown on successful submit.
|
|
452
|
+
|
|
453
|
+
Examples:
|
|
454
|
+
myapi funnel form # email-required on default funnel
|
|
455
|
+
myapi funnel form --slug waitlist --fields email:required,name
|
|
456
|
+
myapi funnel form <funnel> --slug checkout \\
|
|
457
|
+
--fields email:required,plan,phone:tel \\
|
|
458
|
+
--capture-to webhook:<checkout_webhook_id>
|
|
459
|
+
myapi funnel form --json # machine-readable
|
|
460
|
+
myapi funnel form --slug join > form.html # snippet to a file
|
|
461
|
+
|
|
462
|
+
Do NOT hardcode webhook inbound URLs into funnel HTML. Use this verb
|
|
463
|
+
instead — the proxy preserves slug routing, hides the URL, and gets
|
|
464
|
+
future platform features (rate-limit, captcha, field validation) for
|
|
465
|
+
free.`,
|
|
466
|
+
'get': 'myapi funnel get <id> [--org <id>] [--json]',
|
|
467
|
+
'list': 'myapi funnel list [--org <id>] [--json]',
|
|
267
468
|
'pages': 'myapi funnel pages [funnel_id] [--funnel <id>] [--org <id>] [--json]',
|
|
268
469
|
'publish': `myapi funnel publish <dir> [--funnel <id>] [--env dev|prod] [--api-fn <id>] [--org <id>]
|
|
269
470
|
|
|
@@ -314,6 +515,7 @@ export async function run(subcommand, args, flags) {
|
|
|
314
515
|
Subcommands:
|
|
315
516
|
create Create a funnel (optional --name; backend defaults to org's preview_subdomain)
|
|
316
517
|
delete Delete a funnel
|
|
518
|
+
form Emit HTML for a form that posts to the canonical submit proxy
|
|
317
519
|
get <id> Get funnel details
|
|
318
520
|
list List funnels
|
|
319
521
|
pages List the pages currently published to a funnel
|
|
@@ -333,14 +535,15 @@ All commands accept --org <id> (or set default: myapi config set-org <id>).`);
|
|
|
333
535
|
return;
|
|
334
536
|
}
|
|
335
537
|
switch (subcommand) {
|
|
336
|
-
case 'list': return list(flags);
|
|
337
538
|
case 'create': return create(flags);
|
|
338
|
-
case 'get': return get(args[0], flags);
|
|
339
539
|
case 'delete': return del(args[0], flags);
|
|
540
|
+
case 'form': return formCmd(args[0], flags);
|
|
541
|
+
case 'get': return get(args[0], flags);
|
|
542
|
+
case 'list': return list(flags);
|
|
543
|
+
case 'pages': return pages(args[0], flags);
|
|
340
544
|
case 'publish': return publish(args[0], flags);
|
|
341
545
|
case 'push': return push(args[0], flags);
|
|
342
546
|
case 'verify': return verify(args[0], flags);
|
|
343
|
-
case 'pages': return pages(args[0], flags);
|
|
344
547
|
default: error(`Unknown subcommand: ${subcommand}. Run "myapi funnel --help" for a list of valid subcommands.`);
|
|
345
548
|
}
|
|
346
549
|
}
|
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}',
|