@myapihq/cli 2.2.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/commands/account.d.ts +1 -1
  2. package/dist/commands/account.js +1 -0
  3. package/dist/commands/authproduct.js +7 -3
  4. package/dist/commands/billing.js +7 -2
  5. package/dist/commands/container.js +4 -3
  6. package/dist/commands/database.js +4 -3
  7. package/dist/commands/domain.js +39 -14
  8. package/dist/commands/email/message.js +6 -3
  9. package/dist/commands/email/template.js +3 -3
  10. package/dist/commands/fn.js +4 -3
  11. package/dist/commands/funnel.js +33 -13
  12. package/dist/commands/git.js +108 -35
  13. package/dist/commands/image.js +7 -4
  14. package/dist/commands/keys-validation.test.js +5 -0
  15. package/dist/commands/keys.js +6 -2
  16. package/dist/commands/llm.js +41 -20
  17. package/dist/commands/login-validation.test.d.ts +1 -0
  18. package/dist/commands/login-validation.test.js +43 -0
  19. package/dist/commands/login.d.ts +14 -0
  20. package/dist/commands/login.js +447 -0
  21. package/dist/commands/org.d.ts +1 -1
  22. package/dist/commands/org.js +24 -7
  23. package/dist/commands/queue.js +4 -3
  24. package/dist/commands/setup.js +6 -1
  25. package/dist/commands/storage.js +1 -1
  26. package/dist/commands/workflow.js +5 -4
  27. package/dist/completion.js +3 -3
  28. package/dist/config.js +3 -0
  29. package/dist/errors.d.ts +3 -0
  30. package/dist/errors.js +62 -0
  31. package/dist/errors.test.d.ts +1 -0
  32. package/dist/errors.test.js +28 -0
  33. package/dist/exposes.test.js +1 -0
  34. package/dist/flags.js +5 -3
  35. package/dist/flags.test.js +1 -1
  36. package/dist/helpers.d.ts +1 -0
  37. package/dist/helpers.js +22 -0
  38. package/dist/index.js +10 -51
  39. package/dist/skills/my-api-hq/SKILL.md +2 -2
  40. package/dist/skills/my-domain-api/SKILL.md +5 -5
  41. package/dist/skills/my-email-verify-api/SKILL.md +6 -4
  42. package/dist/skills/my-funnel-api/SKILL.md +5 -4
  43. package/dist/skills/my-git-api/SKILL.md +14 -7
  44. package/dist/skills/my-image-api/SKILL.md +1 -1
  45. package/dist/skills/my-llm-api/SKILL.md +11 -11
  46. package/dist/skills/my-storage-api/SKILL.md +5 -5
  47. package/dist/skills/my-webhook-api/SKILL.md +1 -1
  48. package/dist/skills/my-workflow-api/SKILL.md +1 -1
  49. package/dist/utils.d.ts +1 -0
  50. package/dist/utils.js +13 -1
  51. package/package.json +3 -2
package/dist/errors.js ADDED
@@ -0,0 +1,62 @@
1
+ // Per-code friendly messages for MyApiError.code. Kept in its own module (not
2
+ // index.ts) so it can be unit-tested without importing the CLI entrypoint,
3
+ // which runs main() on import.
4
+ export const ERROR_MESSAGES = {
5
+ DOMAIN_NOT_FOUND: 'Domain not found.',
6
+ INVALID_DOMAIN: 'Invalid domain name.',
7
+ ORG_NOT_FOUND: 'Organization not found.',
8
+ org_not_found: 'Organization not found.',
9
+ FUNNEL_NOT_FOUND: 'Funnel not found.',
10
+ funnel_not_found: 'Funnel not found.',
11
+ funnel_already_exists: 'Your org already has a funnel. Use: myapi funnel list',
12
+ FUNNEL_ALREADY_EXISTS: 'Your org already has a funnel. Use: myapi funnel list',
13
+ domain_already_registered: 'This domain is already registered under your account.',
14
+ DOMAIN_ALREADY_REGISTERED: 'This domain is already registered under your account.',
15
+ domain_not_available: 'This domain is not available for registration.',
16
+ DOMAIN_NOT_AVAILABLE: 'This domain is not available for registration.',
17
+ db_error: 'Resource not found or invalid ID.',
18
+ NOT_FOUND: 'Resource not found.',
19
+ domain_not_found: 'Domain not found.',
20
+ invalid_domain: 'Invalid domain name.',
21
+ invalid_org: 'Invalid organization.',
22
+ FORBIDDEN: 'You do not have permission to perform this action.',
23
+ RATE_LIMITED: 'Too many requests. Please wait a moment and try again.',
24
+ INSUFFICIENT_BALANCE: 'Insufficient balance. Run: myapi billing topup <amount>',
25
+ INVALID_AMOUNT: 'Amount out of range. Maximum single top-up is $100. Run: myapi billing topup <amount>',
26
+ SERVICE_NOT_LAUNCHED: 'This service is disabled pre-launch. Track availability via: myapi status',
27
+ RECORD_NOT_FOUND: 'DNS record not found in this zone.',
28
+ INVALID_RECORD_TYPE: 'Unsupported record type. Allowed: A, AAAA, CNAME, MX, TXT.',
29
+ MX_PRIORITY_REQUIRED: 'MX records require --priority (typical value: 10).',
30
+ INVALID_TTL: 'Invalid TTL. Use the auto sentinel (1) or a value between 60 and 86400 seconds.',
31
+ INVALID_RECORD_CONTENT: 'Invalid record content for this type.',
32
+ RECORD_LIMIT_EXCEEDED: 'Cloudflare per-zone record limit reached.',
33
+ CF_API_ERROR: 'Cloudflare API error.',
34
+ // invalid_json_response intentionally absent — the SDK's MyApiError now
35
+ // builds a useful detailed message for that case (status + URL + body
36
+ // snippet), and friendlyError(err.code) would override it.
37
+ };
38
+ export function friendlyError(err) {
39
+ const body = err.body ?? {};
40
+ // CF_API_ERROR: the cf_message already includes a "Cloudflare API error"
41
+ // prefix, so use it verbatim (with cf_status if present) instead of doubling.
42
+ if (err.code === 'CF_API_ERROR' && typeof body.cf_message === 'string') {
43
+ return typeof body.cf_status === 'number'
44
+ ? `${body.cf_message} (HTTP ${body.cf_status})`
45
+ : body.cf_message;
46
+ }
47
+ // Registrar outages are platform-side and transient. Say so plainly and,
48
+ // crucially, tell the caller NOT to hammer retries — a retry storm on a
49
+ // balance/credit failure is what got the registrar to rate-limit us.
50
+ if (err.code === 'REGISTRAR_UNAVAILABLE') {
51
+ return 'Domain registration is temporarily unavailable on our side — please try again in a little while. This is a platform issue, not your account, so retrying immediately (or in a loop) will not help.';
52
+ }
53
+ if (err.code === 'REGISTRAR_RATE_LIMITED') {
54
+ return 'Domain registration is busy right now — please wait a few minutes before trying again (avoid automatic retries).';
55
+ }
56
+ const base = ERROR_MESSAGES[err.code] || err.code;
57
+ // Generic fallback: append the backend's `message` when it adds info beyond
58
+ // the friendly mapping. Future per-service detail fields can be added here.
59
+ if (err.detail && err.detail !== base)
60
+ return `${base} — ${err.detail}`;
61
+ return base;
62
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,28 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { friendlyError, ERROR_MESSAGES } from './errors.js';
3
+ import { MyApiError } from '@myapihq/sdk';
4
+ describe('friendlyError', () => {
5
+ it('REGISTRAR_UNAVAILABLE → clear temporary message that discourages retries', () => {
6
+ const msg = friendlyError(new MyApiError('REGISTRAR_UNAVAILABLE', 500, 'domain registration is temporarily unavailable'));
7
+ expect(msg).toMatch(/temporarily unavailable/i);
8
+ expect(msg).toMatch(/not help|avoid|later|loop/i); // steers away from a retry storm
9
+ expect(msg).not.toMatch(/openprovider|OP310/i); // never leak the provider
10
+ });
11
+ it('REGISTRAR_RATE_LIMITED → back-off message', () => {
12
+ const msg = friendlyError(new MyApiError('REGISTRAR_RATE_LIMITED', 503));
13
+ expect(msg).toMatch(/busy|wait a few minutes/i);
14
+ expect(msg).toMatch(/avoid automatic retries|wait/i);
15
+ expect(msg).not.toMatch(/openprovider/i);
16
+ });
17
+ it('maps a known code verbatim from the table', () => {
18
+ expect(friendlyError(new MyApiError('DOMAIN_ALREADY_REGISTERED', 409))).toBe(ERROR_MESSAGES.DOMAIN_ALREADY_REGISTERED);
19
+ });
20
+ it('appends the backend detail for generic mapped codes', () => {
21
+ const msg = friendlyError(new MyApiError('INVALID_DOMAIN', 400, 'bad tld'));
22
+ expect(msg).toContain('Invalid domain name.');
23
+ expect(msg).toContain('bad tld');
24
+ });
25
+ it('falls back to the raw code when unmapped with no detail', () => {
26
+ expect(friendlyError(new MyApiError('SOME_NEW_CODE', 400))).toBe('SOME_NEW_CODE');
27
+ });
28
+ });
@@ -44,6 +44,7 @@ const COMMAND_MODULES = [
44
44
  './commands/queue.js',
45
45
  './commands/task.js',
46
46
  './commands/doctor.js',
47
+ './commands/login.js',
47
48
  ];
48
49
  const ENDPOINT_PATTERN = /^(GET|POST|PATCH|PUT|DELETE) \/[A-Za-z0-9_\-./{}]*$/;
49
50
  describe('every CLI command exports a typed EXPOSES array (S-101)', () => {
package/dist/flags.js CHANGED
@@ -23,7 +23,7 @@ export function parseFlags(argv, schema = {}) {
23
23
  flags.help = true;
24
24
  continue;
25
25
  }
26
- if (arg === '-v') {
26
+ if (arg === '-v' || arg === '-V') {
27
27
  flags.version = true;
28
28
  continue;
29
29
  }
@@ -51,7 +51,7 @@ export function parseFlags(argv, schema = {}) {
51
51
  // `--on-error <email>` without breaking back-compat.
52
52
  if (type === undefined) {
53
53
  flags[key] = inlineValue ?? true;
54
- unknownFlags.push(`--${key}`);
54
+ let note = `--${key}`;
55
55
  // If no inline value and the next token doesn't look like another
56
56
  // flag, the user almost certainly intended it as the flag's value
57
57
  // (e.g. `--on-error someone@x.com`). Consume it so it doesn't
@@ -60,9 +60,11 @@ export function parseFlags(argv, schema = {}) {
60
60
  const next = argv[i + 1];
61
61
  if (next !== undefined && !next.startsWith('--') && !next.startsWith('-h') && !next.startsWith('-v')) {
62
62
  flags[key] = next;
63
+ note = `--${key} (took value "${next}")`;
63
64
  i++;
64
65
  }
65
66
  }
67
+ unknownFlags.push(note);
66
68
  continue;
67
69
  }
68
70
  if (type === 'boolean') {
@@ -104,7 +106,7 @@ export function parseFlags(argv, schema = {}) {
104
106
  // disappear. Don't block — the value is still in `flags` for any handler
105
107
  // that wants it — just print one line on stderr.
106
108
  if (unknownFlags.length > 0 && !process.env.MYAPI_QUIET_UNKNOWN_FLAGS) {
107
- process.stderr.write(`› Note: ignoring unknown flag(s): ${unknownFlags.join(', ')}\n`);
109
+ process.stderr.write(`› Note: unknown flag(s) not recognized by this command: ${unknownFlags.join(', ')} — check for typos.\n`);
108
110
  }
109
111
  return { args, flags };
110
112
  }
@@ -103,7 +103,7 @@ describe('parseFlags', () => {
103
103
  process.stderr.write = origWrite;
104
104
  }
105
105
  const out = stderrChunks.join('');
106
- expect(out).toMatch(/ignoring unknown flag/i);
106
+ expect(out).toMatch(/unknown flag/i);
107
107
  expect(out).toMatch(/--mystery/);
108
108
  expect(out).toMatch(/--also-unknown/);
109
109
  });
package/dist/helpers.d.ts CHANGED
@@ -3,4 +3,5 @@ export type Flags = Record<string, string | boolean | number>;
3
3
  export declare function requireOrg(flags: Flags, config: Config, usage: string): string;
4
4
  export declare function requireDomain(arg: string | undefined, flags: Flags, config: Config, usage: string): string;
5
5
  export declare function requireArg(value: string | undefined, name: string, usage: string): string;
6
+ export declare function confirmDestructive(flags: Flags, description: string, usage: string): Promise<void>;
6
7
  export declare function requireFlag(flags: Flags, name: string, usage: string): string;
package/dist/helpers.js CHANGED
@@ -1,7 +1,14 @@
1
1
  import { error } from './output.js';
2
+ import { confirm, isNonInteractive } from './prompt.js';
2
3
  // error() returns `never`, so after `if (!x) error(...)` TS narrows x to a
3
4
  // non-falsy value and the casts disappear.
4
5
  export function requireOrg(flags, config, usage) {
6
+ // A present-but-empty --org (e.g. --org "$ORG" with $ORG unset, or a bare
7
+ // --org that swallowed no value) must never degrade to the default org —
8
+ // that is exactly the silent wrong-tenant path. Fail loudly instead.
9
+ if ('org' in flags && (flags.org === '' || flags.org === true)) {
10
+ error(`--org was passed without a value. Refusing to fall back to the default org.\nUsage: ${usage}`);
11
+ }
5
12
  const orgId = (typeof flags.org === 'string' ? flags.org : '') || config.default_org;
6
13
  if (!orgId) {
7
14
  error(`Missing required arguments.\nUsage: ${usage}\n(Or set default: myapi config set-org <id>)`);
@@ -22,6 +29,21 @@ export function requireArg(value, name, usage) {
22
29
  error(`Missing required argument: ${name}.\nUsage: ${usage}`);
23
30
  return value;
24
31
  }
32
+ // Gate for irreversible writes. Interactive runs get a y/N prompt; --yes
33
+ // skips it; non-interactive runs (CI, agents) hard-refuse without --yes so a
34
+ // destructive command can never ride a silent default. `description` should
35
+ // name the exact target incl. the org it lives in, e.g.
36
+ // `delete funnel "landing" in org abc-123`.
37
+ export async function confirmDestructive(flags, description, usage) {
38
+ if (flags.yes)
39
+ return;
40
+ if (isNonInteractive()) {
41
+ error(`Refusing to ${description} without --yes.\nUsage: ${usage}`);
42
+ }
43
+ const ok = await confirm(`› Really ${description}? This cannot be undone. (y/N) `, false);
44
+ if (!ok)
45
+ error('Aborted.');
46
+ }
25
47
  export function requireFlag(flags, name, usage) {
26
48
  const v = flags[name];
27
49
  if (v === undefined || v === true || v === false || v === '') {
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { error, info, success, banner } from './output.js';
3
3
  import { loadConfig } from './config.js';
4
4
  import { MyApiError } from '@myapihq/sdk';
5
+ import { friendlyError } from './errors.js';
5
6
  import * as fs from 'fs';
6
7
  import { parseFlags } from './flags.js';
7
8
  const pkgPath = new URL('../package.json', import.meta.url);
@@ -37,6 +38,7 @@ import * as gitCmd from './commands/git.js';
37
38
  import * as queueCmd from './commands/queue.js';
38
39
  import * as taskCmd from './commands/task.js';
39
40
  import * as doctorCmd from './commands/doctor.js';
41
+ import * as loginCmd from './commands/login.js';
40
42
  // Each command file declares the value flags it understands. We union them
41
43
  // into a single schema for the upfront parse, so adding a new value flag in
42
44
  // one command means editing one file (its SCHEMA), not a global allowlist.
@@ -70,60 +72,11 @@ const COMBINED_SCHEMA = {
70
72
  ...queueCmd.SCHEMA,
71
73
  ...taskCmd.SCHEMA,
72
74
  ...doctorCmd.SCHEMA,
75
+ ...loginCmd.SCHEMA,
73
76
  // Top-level flags
74
77
  version: 'boolean',
75
78
  V: 'boolean',
76
79
  };
77
- const ERROR_MESSAGES = {
78
- DOMAIN_NOT_FOUND: 'Domain not found.',
79
- INVALID_DOMAIN: 'Invalid domain name.',
80
- ORG_NOT_FOUND: 'Organization not found.',
81
- org_not_found: 'Organization not found.',
82
- FUNNEL_NOT_FOUND: 'Funnel not found.',
83
- funnel_not_found: 'Funnel not found.',
84
- funnel_already_exists: 'Your org already has a funnel. Use: myapi funnel list',
85
- FUNNEL_ALREADY_EXISTS: 'Your org already has a funnel. Use: myapi funnel list',
86
- domain_already_registered: 'This domain is already registered under your account.',
87
- DOMAIN_ALREADY_REGISTERED: 'This domain is already registered under your account.',
88
- domain_not_available: 'This domain is not available for registration.',
89
- DOMAIN_NOT_AVAILABLE: 'This domain is not available for registration.',
90
- db_error: 'Resource not found or invalid ID.',
91
- NOT_FOUND: 'Resource not found.',
92
- domain_not_found: 'Domain not found.',
93
- invalid_domain: 'Invalid domain name.',
94
- invalid_org: 'Invalid organization.',
95
- FORBIDDEN: 'You do not have permission to perform this action.',
96
- RATE_LIMITED: 'Too many requests. Please wait a moment and try again.',
97
- INSUFFICIENT_BALANCE: 'Insufficient balance. Run: myapi billing topup <amount>',
98
- INVALID_AMOUNT: 'Amount out of range. Maximum single top-up is $100. Run: myapi billing topup <amount>',
99
- SERVICE_NOT_LAUNCHED: 'This service is disabled pre-launch. Track availability via: myapi status',
100
- RECORD_NOT_FOUND: 'DNS record not found in this zone.',
101
- INVALID_RECORD_TYPE: 'Unsupported record type. Allowed: A, AAAA, CNAME, MX, TXT.',
102
- MX_PRIORITY_REQUIRED: 'MX records require --priority (typical value: 10).',
103
- INVALID_TTL: 'Invalid TTL. Use the auto sentinel (1) or a value between 60 and 86400 seconds.',
104
- INVALID_RECORD_CONTENT: 'Invalid record content for this type.',
105
- RECORD_LIMIT_EXCEEDED: 'Cloudflare per-zone record limit reached.',
106
- CF_API_ERROR: 'Cloudflare API error.',
107
- // invalid_json_response intentionally absent — the SDK's MyApiError now
108
- // builds a useful detailed message for that case (status + URL + body
109
- // snippet), and friendlyError(err.code) would override it.
110
- };
111
- function friendlyError(err) {
112
- const body = err.body ?? {};
113
- // CF_API_ERROR: the cf_message already includes a "Cloudflare API error"
114
- // prefix, so use it verbatim (with cf_status if present) instead of doubling.
115
- if (err.code === 'CF_API_ERROR' && typeof body.cf_message === 'string') {
116
- return typeof body.cf_status === 'number'
117
- ? `${body.cf_message} (HTTP ${body.cf_status})`
118
- : body.cf_message;
119
- }
120
- const base = ERROR_MESSAGES[err.code] || err.code;
121
- // Generic fallback: append the backend's `message` when it adds info beyond
122
- // the friendly mapping. Future per-service detail fields can be added here.
123
- if (err.detail && err.detail !== base)
124
- return `${base} — ${err.detail}`;
125
- return base;
126
- }
127
80
  async function main() {
128
81
  // Shell tab-completion: when the shell invokes us for completion it passes
129
82
  // a --comp* marker. Handle it via a lazily-imported module so completion
@@ -246,6 +199,9 @@ async function main() {
246
199
  case 'doctor':
247
200
  await doctorCmd.run(subcommand, restArgs, flags);
248
201
  break;
202
+ case 'login':
203
+ await loginCmd.login(flags);
204
+ break;
249
205
  // Convenience aliases
250
206
  case 'setup':
251
207
  await setupCmd.setup(flags);
@@ -353,6 +309,7 @@ async function dispatchAccount(subcommand, restArgs, flags) {
353
309
  case 'setup': return setupCmd.setup(flags);
354
310
  case 'import-key': return setupCmd.importKey(restArgs[0], flags);
355
311
  case 'whoami': return accountCmd.whoami(flags);
312
+ case 'login': return loginCmd.login(flags);
356
313
  case 'link': return accountCmd.link(flags, restArgs[0]);
357
314
  case 'switch': return accountCmd.switchCmd(flags, restArgs[0]);
358
315
  case 'install-skills':
@@ -370,7 +327,7 @@ async function dispatchAccount(subcommand, restArgs, flags) {
370
327
  case 'registrant': return accountCmd.registrant(restArgs[0], flags);
371
328
  case 'api-keys':
372
329
  case 'keys': return keysCmd.runApiKeys(restArgs[0], restArgs.slice(1), flags);
373
- default: info('Unknown subcommand. Run: myapi account --help');
330
+ default: error('Unknown subcommand. Run: myapi account --help');
374
331
  }
375
332
  }
376
333
  const HELP_TARGETS = {
@@ -400,6 +357,7 @@ const HELP_TARGETS = {
400
357
  billing: f => billingCmd.run(undefined, [], f),
401
358
  keys: f => keysCmd.run(undefined, [], f),
402
359
  config: f => configCmd.run(undefined, [], f),
360
+ login: f => loginCmd.login(f),
403
361
  };
404
362
  async function dispatchHelp(target) {
405
363
  if (!target) {
@@ -446,6 +404,7 @@ Commands:
446
404
  image Generate AI images and manage them in storage
447
405
  install-skills Install or update the MyAPI skills pack for AI agents
448
406
  llm Run LLM completions and embeddings (chat + embed, with usage/cost)
407
+ login Sign in via your browser — Google or email code (preview: --mock)
449
408
  org Manage organizations (tip: myapi org create "name" --yes to auto-set as default)
450
409
  payments Take payments with Stripe Checkout (connect, charge, refund)
451
410
  people Search the contact database (filter by industry, seniority, country, ...)
@@ -49,7 +49,7 @@ An anonymous account can upgrade at any time via `myapi account link <email>`
49
49
  | `myapi org create --name "..."` | Create a new org (`--yes` auto-sets as default) |
50
50
  | `myapi org get [id]` | Inspect one org (defaults to current default) |
51
51
  | `myapi org update [id]` | Update fields (name, tagline, description, business-sector, logo-url) |
52
- | `myapi org delete <id>` | Delete an org and cascade |
52
+ | `myapi org delete <id>` | Delete an org and cascade (funnels included). Verify the target with `myapi org get <id>` first; asks for confirmation — `--yes` required in non-interactive runs |
53
53
  | `myapi org sync-brand <domain>` | Scrape a live site and auto-fill brand info |
54
54
  | `myapi keys list / create / revoke <id>` | Manage API keys (alias `myapi account api-keys`) |
55
55
  | `myapi billing balance` | Check balance |
@@ -87,7 +87,7 @@ myapi org sync-brand acme.com
87
87
  myapi account switch 2
88
88
  ```
89
89
 
90
- If any service returns `402 INSUFFICIENT_FUNDS`, top up (`myapi billing topup`) — or enable `myapi billing auto-recharge` so it refills itself. A `402 SPEND_CAP_EXCEEDED` is different: you hit a spend ceiling you set, so raise it with `myapi billing spend-cap` rather than topping up.
90
+ If any service returns `402 INSUFFICIENT_FUNDS`, top up (`myapi billing topup`) — or enable `myapi billing auto-recharge` so it refills itself. With auto-recharge enabled, billable CLI commands (llm, image, email send, domain register/renew, git commit) handle a refill-in-flight automatically: they wait the server-hinted interval and retry instead of failing, so don't add your own retry loop. A `402 SPEND_CAP_EXCEEDED` is different: you hit a spend ceiling you set, so raise it with `myapi billing spend-cap` rather than topping up.
91
91
 
92
92
  Each org gets a free preview subdomain (`*.makeautonomous.com`) usable before registering a custom domain.
93
93
  <!-- llm:end -->
@@ -48,9 +48,9 @@ Resolution at register time (highest wins): `--registrant-json` → per-field fl
48
48
  |---|---|
49
49
  | `myapi domain check <domain>` | Check availability and yearly price |
50
50
  | `myapi domain register <domain> [--years N] <registrant flags>` | Register a new domain (deducts credits). Requires ICANN WHOIS contact info — store once with `myapi account registrant set`, or pass per-call via `--registrant-json` / `--registrant-*` flags |
51
- | `myapi domain renew <domain>` | Renew a registered domain for another period |
51
+ | `myapi domain renew <domain> [--yes]` | Renew a registered domain for another period. Charges the org balance; asks for confirmation — pass `--yes` in non-interactive runs |
52
52
  | `myapi domain list [--filter all\|unassigned\|org]` | List domains in your account |
53
- | `myapi domain assign <domain>` | Assign domain to your default (or `--org`) org |
53
+ | `myapi domain assign <domain> --org <id>` | Assign domain to an org. **Also the reassign path** — re-running moves a domain already assigned elsewhere. Pre-flight with `myapi domain list --filter all` and always pass `--org` explicitly |
54
54
  | `myapi domain unassign <domain>` | Remove domain from its org |
55
55
  | `myapi domain import <domain>` | Bring-your-own-domain. Snapshots current DNS, returns nameservers to set at your existing registrar — no registrar credentials needed |
56
56
  | `myapi domain status <domain> [--watch]` | Registration + DNS propagation status. `--watch` polls until terminal (10s × 30 → 30s × 60) |
@@ -74,11 +74,11 @@ myapi account registrant set
74
74
  # Register and bring online
75
75
  myapi domain check example.com
76
76
  myapi domain register example.com
77
- myapi domain assign example.com
77
+ myapi domain assign example.com --org <org_id> # explicit --org: assign also reassigns
78
78
  myapi domain status example.com # poll until status = active
79
79
 
80
- # Renew before expiry
81
- myapi domain renew example.com
80
+ # Renew before expiry (charges the org balance; --yes for non-interactive runs)
81
+ myapi domain renew example.com --yes
82
82
 
83
83
  # Tune the edge for AI bot traffic
84
84
  myapi domain update-settings example.com \
@@ -9,7 +9,7 @@ checksum: sha256-pending
9
9
 
10
10
  # MyEmailVerifyAPI
11
11
 
12
- A cheap, fast quality gate for a single email address. Runs three layers in sequence: syntax check (instant) → DNS / MX lookup (sub-second) → Microsoft GetCredentialType probe (sub-second for non-Microsoft, slower for federated domains). Returns a definitive `deliverable` / `undeliverable` verdict for about half of inputs; the rest get `verdict: 'unknown'` with `smtp_recommended: true`, suggesting a downstream SMTP probe that isn't part of this API today.
12
+ A cheap, fast quality gate for a single email address. Runs three layers in sequence: syntax check (instant) → DNS / MX lookup (sub-second) → Microsoft GetCredentialType probe (sub-second for non-Microsoft, slower for federated domains). Returns a definitive `deliverable` / `undeliverable` verdict for about half of inputs; the rest get `verdict: 'unknown'` with `smtp_recommended: true` feed those into `verify bulk`, which SMTP-probes the uncertain addresses asynchronously.
13
13
 
14
14
  ## Capabilities
15
15
  <!-- llm:start -->
@@ -18,7 +18,7 @@ Use this before any campaign send — pipe the audience members through `verify`
18
18
  Verdicts:
19
19
  - **`deliverable`** — high-confidence acceptance. Safe to send.
20
20
  - **`undeliverable`** — high-confidence reject. Skip. Most common causes: malformed syntax (`missing '@'`), no MX records, Microsoft positively returns "no such mailbox".
21
- - **`unknown`** — couldn't determine cheaply. The `smtp_recommended` flag will be `true`; in practice, treat as "send but watch the bounce signal" or run a more expensive SMTP probe externally.
21
+ - **`unknown`** — couldn't determine cheaply. The `smtp_recommended` flag will be `true`; run those addresses through `myapi email verify bulk`, which SMTP-probes the uncertain ones (or treat as "send but watch the bounce signal").
22
22
 
23
23
  Response shape (full check breakdown available via `--json`):
24
24
  ```json
@@ -44,6 +44,8 @@ Latency notes: syntax + DNS are <100ms. Microsoft GetCredentialType is fast for
44
44
  | Command | What it does |
45
45
  |---|---|
46
46
  | `myapi email verify <email> [--json]` | Run all three verification layers on one address; print verdict + confidence + key checks |
47
+ | `myapi email verify bulk [--quick] < emails.txt` | Async batch (newline-separated stdin, ≤500 per job): cheap verdict on every address, then SMTP-probes the uncertain ones. `--quick` skips the catch-all check. Prints a `job_id` |
48
+ | `myapi email verify job <job_id> [--json]` | Poll a bulk job: status + per-address verdicts |
47
49
  <!-- generated:end -->
48
50
 
49
51
  Pass `--json` for the full check breakdown (syntax detail, MX records, Microsoft probe result). The default human render shows the verdict + a one-line summary per check that fired.
@@ -60,7 +62,7 @@ myapi email verify not-an-email
60
62
  # Generic domain — unknown verdict, MX visible, confidence 0.5
61
63
  myapi email verify hello@gmail.com
62
64
  # → Verdict: ? unknown (confidence 0.50)
63
- # → SMTP next: yes — consider an SMTP probe
65
+ # → SMTP next: yes — consider a bulk job (SMTP-probes the uncertain ones)
64
66
  # → MX: gmail-smtp-in.l.google.com, alt1...
65
67
 
66
68
  # Full breakdown
@@ -83,7 +85,7 @@ done < emails.txt | grep -v ' undeliverable$' > verified.txt
83
85
 
84
86
  ## Notes
85
87
 
86
- - This is the **cheap layer only**. The full pipeline at outreach scale typically chains: this verify (free signal) SMTP probe (medium cost) catch-all detector (slow). Only the first is exposed today.
88
+ - `verify <email>` is the **cheap layer** (syntax DNS Microsoft probe). `verify bulk` completes the pipeline: it re-runs the cheap verdict, then SMTP-probes the uncertain addresses and runs catch-all detection (skip catch-all with `--quick`).
87
89
  - The `confidence` field is a hint, not a guarantee. A `deliverable` verdict at 0.95 confidence is still ≈5% bounce risk in practice.
88
90
  - For bulk verification use the async batch endpoint: `myapi email verify bulk < emails.txt` (one address per line) returns a `job_id`, then poll `myapi email verify job <job_id>` for status + per-address results.
89
91
  - Verification is per-org; you'll get rate-limited if you blast more than ~1 req/sec per key.
@@ -30,7 +30,8 @@ Every funnel auto-provisions a **webhook** at creation time (`org_webhook_id`).
30
30
  | `myapi funnel list` | List all funnels with preview/domain URLs |
31
31
  | `myapi funnel get <id>` | Inspect a funnel's metadata + preview URL |
32
32
  | `myapi funnel delete <id>` | Delete the funnel and purge its edge pages |
33
- | `myapi funnel push [slug]` | Push HTML from stdin to a slug (default: `/`). **Overwrites** an existing page — refused without `--force`, prints the resolved org + funnel on success |
33
+ | `myapi funnel push [slug]` | Push HTML from stdin to a slug (default: `/`). **Overwrites** an existing page — refused without `--force`. `--json` prints `{slug, subdomain_url, overwritten, org_id, funnel_id}` |
34
+ | `myapi funnel publish <dir>` | Upload a whole directory as the funnel's site (`--env dev\|prod`, default prod; `--api-fn <id>`; `--json`). Prod refuses to replace a live site without `--force` |
34
35
  | `myapi funnel pages [funnel_id]` | List the pages currently published to a funnel |
35
36
  | `myapi funnel form [funnel_id]` | Emit canonical form HTML (and register a binding with `--capture-to`) |
36
37
  | `myapi funnel verify [slug]` | Verify a published page is reachable + check links/webhooks |
@@ -45,7 +46,7 @@ A write targets a `(org, funnel, slug)` address. Get all three right *before* pu
45
46
  3. **A new demo = a new funnel.** Don't reuse an org's existing funnel for an unrelated demo. `myapi funnel create --name <demo> --org <id>` gives you a clean namespace and its own preview subdomain.
46
47
  4. **Pin the funnel.** When an org has exactly one funnel, `push`/`publish` auto-pick it — convenient, but it's how a demo lands on the wrong site. Pass `--funnel <id>` (or set a default) so the target is explicit, not inferred.
47
48
 
48
- The CLI backstops you: `push` refuses to overwrite an existing slug (and `publish --env prod` refuses to replace a live site) without `--force`, and both print the resolved **org + funnel** on success. `--force` is the deliberate "yes, replace it" — never reach for it just to clear the error; first check whether the clash means you're aimed at the wrong place.
49
+ The CLI backstops you: `push` refuses to overwrite an existing slug (and `publish --env prod` refuses to replace a live site) without `--force`, and both print the resolved **org + funnel** on success.
49
50
 
50
51
  ## Examples
51
52
  <!-- llm:start -->
@@ -71,7 +72,7 @@ cat new-home.html | myapi funnel push / --funnel <funnel_id> --force
71
72
  myapi funnel delete <funnel_id> --org <org_id>
72
73
  ```
73
74
 
74
- 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 (so an unintended push can silently land on an existing site; pass `--funnel` to be sure). `push` refuses to overwrite an occupied slug without `--force`.
75
+ 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; pass `--funnel` to be sure. `push --json` returns the live URL as `subdomain_url` no follow-up `funnel get` needed.
75
76
  <!-- llm:end -->
76
77
 
77
78
  ## Form submissions (canonical recipe)
@@ -113,7 +114,7 @@ myapi funnel form <funnel_id> --slug survey \
113
114
  ## Notes
114
115
 
115
116
  - Set defaults with `myapi config set-org <id>` and `myapi config set-funnel <id>` to skip flags on every command — but a stale default is exactly how a push lands on the wrong org/funnel. For demos and one-offs, pass `--org`/`--funnel` explicitly instead of relying on whatever default was set last.
116
- - `push` (any slug) and `publish --env prod` overwrite live content and are refused without `--force` when something already exists at the target. `--force` means "yes, replace the live page/site" — confirm you're aimed at the right `(org, funnel, slug)` before using it, don't use it to silence the error.
117
+ - `--force` means "yes, replace the live page/site" — confirm the `(org, funnel, slug)` target first; don't use it to silence the overwrite error.
117
118
  - Deleting a funnel purges all its edge pages immediately.
118
119
  - `402 INSUFFICIENT_FUNDS` → top up (`myapi billing topup`) or enable `myapi billing auto-recharge`.
119
120
 
@@ -15,7 +15,7 @@ Per-org hosted git repositories, served by a go-git engine (objects in GCS packf
15
15
  <!-- llm:start -->
16
16
  Git is the source-control layer. Each repo lives under your org and is reachable two ways:
17
17
 
18
- 1. **CLI / SDK** — no local clone. `commit` applies a JSON array of file edits atomically; reads (`log`, `show`, `tree`, `blob`, `diff`, `refs`) round-trip the API. Perfect for an agent that edits files server-side.
18
+ 1. **CLI / SDK** — no local clone. `commit` writes atomically from three sources: `--dir <path>` (every file under a local directory the preferred path for committing a generated project), `--file <path> [--as <repo-path>]` (one local file), or an explicit `--changes` JSON array. Reads (`log`, `show`, `tree`, `blob`, `diff`, `refs`) round-trip the API.
19
19
  2. **Real git over HTTPS** — `git clone`/`fetch`/`push` against `https://git.mygitapi.com/<org-slug>/<repo>.git` with your MyAPI API key as the password. This is how a normal git workflow connects (see *Clone & push with real git*).
20
20
 
21
21
  Both surfaces share one engine: a `git push` and a `myapi git commit` converge on the same refs.
@@ -32,14 +32,14 @@ Both surfaces share one engine: a `git push` and a `myapi git commit` converge o
32
32
  | `myapi git create <name>` | Create a repository (`--default-branch <b>`) |
33
33
  | `myapi git list` | List repositories |
34
34
  | `myapi git get <repo>` | Show branch/tag counts + default branch |
35
- | `myapi git delete <repo>` | Delete a repository |
35
+ | `myapi git delete <repo>` | Delete a repository. Look before you delete: `myapi git list` first, pass `--org` explicitly; asks for confirmation — `--yes` required in non-interactive runs |
36
36
  | `myapi git refs <repo>` | List HEAD, branches, and tags |
37
37
  | `myapi git log <repo>` | List commits (`--ref`, `--limit`) |
38
38
  | `myapi git show <repo> <sha>` | Show a single commit |
39
39
  | `myapi git tree <repo> <ref>` | List a ref's file tree (`--path`) |
40
40
  | `myapi git blob <repo> <ref> <path>` | Print a file's content (decoded) |
41
41
  | `myapi git diff <repo>` | Unified diff of two refs (`--base`, `--head`) |
42
- | `myapi git commit <repo>` | Atomic commit (`--branch`, `--message`, `--changes`) |
42
+ | `myapi git commit <repo>` | Atomic commit (`--branch`, `--message`, plus `--dir <path>` \| `--file <path> [--as <repo-path>]` \| `--changes <json>`) |
43
43
  | `myapi git create-branch <repo> <name>` | Create a branch (`--from <ref>`) |
44
44
  | `myapi git delete-branch <repo> <branch>` | Delete a branch |
45
45
  | `myapi git tag <repo> <name>` | Create a tag (`--ref <ref>`) |
@@ -55,9 +55,16 @@ myapi git create my-app --default-branch main
55
55
  myapi git list
56
56
  myapi git refs my-app
57
57
 
58
- # Atomic commit --changes is a JSON array of file edits
59
- myapi git commit my-app --branch main --message "init" \
60
- --changes '[{"path":"README.md","content":"# My App"}]'
58
+ # Commit a generated project point --dir at the directory (preferred)
59
+ myapi git commit my-app --branch main --message "init" --dir ./dist
60
+
61
+ # Commit one local file (--as places/renames it in the repo)
62
+ myapi git commit my-app --branch main --message "add page" \
63
+ --file ./out.html --as index.html
64
+
65
+ # Or hand-author edits as JSON (deletes, modes, base64 content)
66
+ myapi git commit my-app --branch main --message "rm" \
67
+ --changes '[{"path":"old.txt","delete":true}]'
61
68
  # Each entry: {path, content | content_base64 | delete:true, mode?}
62
69
  # mode: "100644" (default), "100755" (exec), "120000" (symlink)
63
70
 
@@ -105,7 +112,7 @@ surface never leaks which repos exist).
105
112
 
106
113
  ## Notes
107
114
 
108
- - **Clone-free is the default mode.** The CLI never needs a working copy — `commit` sends file edits as JSON, reads come back over the API. Use real git only when you want a local checkout.
115
+ - **Clone-free is the default mode.** The CLI never needs a working copy — `commit` sends local files (`--dir`/`--file`) or JSON edits (`--changes`), reads come back over the API. Use real git only when you want a local checkout.
109
116
  - **`merge` is fast-forward only.** No merge commits; the target must be an ancestor of the source. Rebase/merge locally and push instead for non-FF cases.
110
117
  - **`commit --base`** is the concurrency guard: pass the expected tip SHA to reject a stale write, or `--base ""` to require the branch be newly created. Omit it to create-or-update.
111
118
  - **Metering.** A `git push` (and `myapi git commit`) is metered like a commit. Clone/fetch (`upload-pack`) is free.
@@ -73,6 +73,6 @@ Tips for agents:
73
73
 
74
74
  - Cost is per-image at the selected model's catalog rate (`myapi image models`). Failed jobs aren't charged.
75
75
  - The asset URL is public — don't generate sensitive content.
76
- - `delete` is permanent for the asset; the job record (prompt + metadata) stays for history.
76
+ - `delete` is permanent for the asset; the job record (prompt + metadata) stays for history. Look before you delete: `myapi image list` first, pass `--org` explicitly; delete verbs require `--yes` in non-interactive runs.
77
77
 
78
78
  Run `myapi image --help` or `myapi image <subcommand> --help` for full flag reference.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: my-llm-api
3
- version: 1.1.0
3
+ version: 1.2.0
4
4
  description: >
5
5
  Two-surface LLM primitive. Raw chat completion against self-hosted
6
6
  open-source models (you pick the model), and objective verbs
@@ -14,7 +14,7 @@ checksum: sha256-pending
14
14
 
15
15
  A two-surface LLM gateway:
16
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.
17
+ - **Raw** — `complete` / `embed` / `models`. You pick a self-hosted catalog model (run `myapi llm models` for the live list). Proprietary models are not callable here.
18
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
19
 
20
20
  Pricing is in cents per 1M tokens at the actual upstream rate; cost is debited from your MyAPI balance.
@@ -33,7 +33,7 @@ Use this for workflow tasks — summarize a doc, classify an inbound email, extr
33
33
  ### Raw `complete` request
34
34
  ```json
35
35
  {
36
- "model": "Qwen/Qwen3-Coder-30B-A3B-Instruct",
36
+ "model": "<model-id>",
37
37
  "messages": [
38
38
  { "role": "system", "content": "You are a terse editor." },
39
39
  { "role": "user", "content": "Tighten this paragraph: ..." }
@@ -49,7 +49,7 @@ Roles: `system | user | assistant`. Multiple system messages collapse to one ins
49
49
  ### Raw `complete` response
50
50
  ```json
51
51
  {
52
- "model": "Qwen/Qwen3-Coder-30B-A3B-Instruct",
52
+ "model": "<model-id>",
53
53
  "content": "...assistant reply...",
54
54
  "finish_reason": "stop",
55
55
  "usage": { "input_tokens": 42, "output_tokens": 87, "cost_cents": 0.029 }
@@ -62,7 +62,7 @@ If `model` isn't in the self-hosted catalog the server returns `MODEL_NOT_IN_RAW
62
62
 
63
63
  ### Raw `embed`
64
64
 
65
- No embedding model is served on the raw catalog today; calls return `EMBED_NOT_AVAILABLE`. Use a dedicated embedding API for now.
65
+ Embed text into a dense vector. `--model` is optional when the catalog serves exactly one embed model; else pass one from `myapi llm models --kind embed`. Returns `EMBED_NOT_AVAILABLE` when none is served.
66
66
 
67
67
  ### Model catalog
68
68
  - Chat models: `id`, `kind: 'chat'`, `context_window`, `input_cost_per_1m_cents`, `output_cost_per_1m_cents`
@@ -99,11 +99,11 @@ client = OpenAI(
99
99
  api_key="hq_live_…",
100
100
  base_url="https://api.myapihq.com/llm/orgs/<org_id>/v1",
101
101
  )
102
- r = client.chat.completions.create(model="Qwen/Qwen3-Coder-30B-A3B-Instruct",
102
+ r = client.chat.completions.create(model="<model-id>",
103
103
  messages=[{"role":"user","content":"Hi"}])
104
104
  ```
105
105
 
106
- Use raw `complete` (MyAPI shape) for first-party integrations; use the OpenAI-compat path for compatibility with existing client code.
106
+ Use raw `complete` for first-party code; the compat path for existing OpenAI/LangChain tooling.
107
107
 
108
108
  <!-- llm:end -->
109
109
 
@@ -113,7 +113,7 @@ Use raw `complete` (MyAPI shape) for first-party integrations; use the OpenAI-co
113
113
  |---|---|
114
114
  | `myapi llm models [--kind chat\|embed] [--json]` | List the live model catalog with pricing (cents/1M) |
115
115
  | `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 |
116
- | `myapi llm embed "<text>" --model <id> [--json]` | Embed a string (no model served today returns EMBED_NOT_AVAILABLE) |
116
+ | `myapi llm embed "<text>" [--model <id>] [--json]` | Embed a string into a vector; `--model` optional when the catalog has one embed model |
117
117
  | `myapi llm classify "<input>" --labels <csv> [--multi] [--tier <t>] [--json]` | Pick a label from a set |
118
118
  | `myapi llm extract "<input>" --schema <path\|json> [--tier <t>] [--json]` | Pull structured data conforming to a JSON Schema |
119
119
  | `myapi llm summarize "<input>" [--style brief\|exec\|bullet] [--tier <t>] [--json]` | Summarize text |
@@ -132,9 +132,9 @@ myapi llm models --kind chat --json | jq '.models[].id'
132
132
  # Raw completion — picks the first chat model from the catalog
133
133
  myapi llm complete "Summarize in 12 words: $(cat README.md)"
134
134
 
135
- # Pin a specific model
135
+ # Pin a specific model (ids come from `myapi llm models`)
136
136
  myapi llm complete "Refactor this function: ..." \
137
- --model Qwen/Qwen3-Coder-30B-A3B-Instruct \
137
+ --model <model-id> \
138
138
  --system "You are a careful Go reviewer." \
139
139
  --max-tokens 600
140
140
 
@@ -163,5 +163,5 @@ INTENT=$(printf '%s' "$BODY" | myapi llm classify - \
163
163
 
164
164
  - **`draft` context safety.** `context` fields are quoted into the prompt verbatim; sensitive-named keys (`secret`, `api_key`, `password`, …) are NOT redacted. Two guards on top: (a) injection-defense strips `instructions`/`system`/`prompt`/`override` keys and surfaces them in `meta.warnings`; (b) output guardrail substring-scans fact values (length ≥ 4) in the response and lists matches in `meta.guardrails.facts_in_output` (signal, not redaction). Rule of thumb: never put credentials, PII, or internal metadata in `context` — pass identifiers, reference them indirectly.
165
165
  - **Self-hosted raw, server-picked verbs.** Raw runs on MyAPI's TPU; verbs route wherever the server picks.
166
- - **Cost + latency.** `usage.cost_cents` is authoritative — no markup. Qwen3-Coder-30B: 200–600 ms to first token, 1–3 s end-to-end.
166
+ - **Cost + latency.** `usage.cost_cents` is authoritative — no markup. Varies by tier: 200–600 ms to first token, 1–3 s end-to-end.
167
167
  - **Live catalog, no streaming, no BYOK.** Don't hard-code ids — `models` is truth (CLI auto-picks if `--model` omitted). Full reply only.