@myapihq/cli 2.0.1 → 2.2.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.
- package/dist/commands/authproduct.d.ts +1 -1
- package/dist/commands/authproduct.js +111 -18
- package/dist/commands/billing-auto-recharge.test.d.ts +1 -0
- package/dist/commands/billing-auto-recharge.test.js +103 -0
- package/dist/commands/billing.d.ts +3 -0
- package/dist/commands/billing.js +115 -0
- package/dist/commands/doctor-setup.test.js +86 -1
- package/dist/commands/doctor.d.ts +8 -0
- package/dist/commands/doctor.js +70 -15
- package/dist/commands/status.js +12 -0
- package/dist/index.js +27 -6
- package/dist/skills/my-api-hq/README.md +1 -0
- package/dist/skills/my-api-hq/SKILL.md +14 -4
- package/dist/skills/my-auth-api/SKILL.md +9 -6
- package/dist/skills/my-domain-api/SKILL.md +1 -1
- package/dist/skills/my-email-api/README.md +2 -3
- package/dist/skills/my-funnel-api/SKILL.md +1 -1
- package/dist/skills/my-llm-api/README.md +1 -1
- package/package.json +2 -2
|
@@ -3,5 +3,5 @@ import { type Flags } from '../helpers.js';
|
|
|
3
3
|
import type { Exposes } from '../exposes.js';
|
|
4
4
|
export declare const SCHEMA: FlagSchema;
|
|
5
5
|
export declare const EXPOSES: Exposes;
|
|
6
|
-
export declare const HELP = "Usage: myapi auth <subcommand>\n\nAuthentication for your app's end users \u2014 a managed OIDC identity provider\n(\u00E0 la Kinde/Auth0). One auth tenant per org; register OIDC clients (apps)\nagainst it; sign users in via the hosted login page or the JS SDK; verify\ntokens locally against the tenant JWKS.\n\nEnd users sign in with managed Google, email/password, or magic links \u2014 pick\nwhich via `tenant create --connections`. (Operator/account commands moved to\n`myapi account`.)\n\nSubcommands:\n client Register and
|
|
6
|
+
export declare const HELP = "Usage: myapi auth <subcommand>\n\nAuthentication for your app's end users \u2014 a managed OIDC identity provider\n(\u00E0 la Kinde/Auth0). One auth tenant per org; register OIDC clients (apps)\nagainst it; sign users in via the hosted login page or the JS SDK; verify\ntokens locally against the tenant JWKS.\n\nEnd users sign in with managed Google, email/password, or magic links \u2014 pick\nwhich via `tenant create --connections`. (Operator/account commands moved to\n`myapi account`.)\n\nSubcommands:\n client Register, list, delete, and rotate OIDC clients (your apps)\n domain Serve auth on your own domain (auth.acme.com)\n tenant Show or create your org's OIDC auth tenant (+ sign-in methods)\n usage Monthly active users (MAU) for the current period";
|
|
7
7
|
export declare function run(subcommand: string | undefined, args: string[], flags: Flags): Promise<void>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { auth as sdkAuth } from '@myapihq/sdk';
|
|
2
2
|
import { requireConfig } from '../config.js';
|
|
3
|
+
import { confirm, isNonInteractive } from '../prompt.js';
|
|
3
4
|
import { success, error, info, printTable, printJson } from '../output.js';
|
|
4
5
|
import { requireOrg } from '../helpers.js';
|
|
5
6
|
// `myapi auth` — the END-USER auth product (my-auth-api): a managed OIDC IdP
|
|
@@ -21,8 +22,11 @@ export const EXPOSES = [
|
|
|
21
22
|
'POST /auth/orgs/{org_id}/clients',
|
|
22
23
|
'GET /auth/orgs/{org_id}/clients',
|
|
23
24
|
'GET /auth/orgs/{org_id}/usage',
|
|
25
|
+
'DELETE /auth/orgs/{org_id}/clients/{client_id}',
|
|
26
|
+
'POST /auth/orgs/{org_id}/clients/{client_id}/rotate',
|
|
24
27
|
'POST /auth/orgs/{org_id}/domain',
|
|
25
28
|
'GET /auth/orgs/{org_id}/domain',
|
|
29
|
+
'POST /auth/orgs/{org_id}/domain/verify',
|
|
26
30
|
'DELETE /auth/orgs/{org_id}/domain',
|
|
27
31
|
];
|
|
28
32
|
const SUBCOMMAND_USAGE = {
|
|
@@ -40,27 +44,35 @@ in against. One per org.
|
|
|
40
44
|
'usage': `myapi auth usage [--org <id>] [--json]
|
|
41
45
|
|
|
42
46
|
Monthly active users (MAU) for the current period — auth is billed per MAU.`,
|
|
43
|
-
'domain': `myapi auth domain [show|set|delete] [--domain <host>] [--org <id>] [--json]
|
|
47
|
+
'domain': `myapi auth domain [show|set|verify|delete] [--domain <host>] [--org <id>] [--json]
|
|
44
48
|
|
|
45
49
|
Serve auth on your own domain (e.g. auth.acme.com) instead of the default issuer.
|
|
50
|
+
Three steps: set → publish the ownership TXT → verify → publish the A record.
|
|
46
51
|
|
|
47
52
|
myapi auth domain Show the current custom domain + status
|
|
48
|
-
myapi auth domain set --domain auth.acme.com Register it (prints
|
|
53
|
+
myapi auth domain set --domain auth.acme.com Register it (prints a TXT challenge to publish)
|
|
54
|
+
myapi auth domain verify Check the TXT; on success prints the A record
|
|
49
55
|
myapi auth domain delete Remove the custom domain
|
|
50
56
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
Flow: 'set' returns a TXT record to prove ownership; create it, then 'verify'.
|
|
58
|
+
Once verified, create the printed A record; TLS provisions automatically (~30 min)
|
|
59
|
+
and the domain becomes your issuer when active.`,
|
|
60
|
+
'client': `myapi auth client <list|create|delete|rotate> [--org <id>] [--json]
|
|
54
61
|
|
|
55
62
|
OIDC clients are the apps that authenticate against your tenant.
|
|
56
63
|
|
|
57
64
|
myapi auth client list
|
|
58
65
|
myapi auth client create --name "My App" --type spa --redirect https://app.example.com/callback
|
|
66
|
+
myapi auth client delete <client_id> [--yes]
|
|
67
|
+
myapi auth client rotate <client_id>
|
|
59
68
|
|
|
60
|
-
--name <n> Human label for the app (required)
|
|
69
|
+
--name <n> Human label for the app (required for create)
|
|
61
70
|
--type spa|web spa = public (no secret); web = confidential (secret once)
|
|
62
|
-
--redirect <urls> Allowed redirect URIs, comma-separated (required).
|
|
63
|
-
Absolute https (or http://localhost for dev)
|
|
71
|
+
--redirect <urls> Allowed redirect URIs, comma-separated (required for create).
|
|
72
|
+
Absolute https (or http://localhost for dev).
|
|
73
|
+
|
|
74
|
+
delete Revoke a client (irreversible; stops authenticating immediately).
|
|
75
|
+
rotate Re-issue a 'web' client's secret (shown once; old secret stops working).`,
|
|
64
76
|
};
|
|
65
77
|
export const HELP = `Usage: myapi auth <subcommand>
|
|
66
78
|
|
|
@@ -74,7 +86,7 @@ which via \`tenant create --connections\`. (Operator/account commands moved to
|
|
|
74
86
|
\`myapi account\`.)
|
|
75
87
|
|
|
76
88
|
Subcommands:
|
|
77
|
-
client Register and
|
|
89
|
+
client Register, list, delete, and rotate OIDC clients (your apps)
|
|
78
90
|
domain Serve auth on your own domain (auth.acme.com)
|
|
79
91
|
tenant Show or create your org's OIDC auth tenant (+ sign-in methods)
|
|
80
92
|
usage Monthly active users (MAU) for the current period`;
|
|
@@ -165,25 +177,34 @@ async function usage(flags) {
|
|
|
165
177
|
info(`Period: ${u.period}`);
|
|
166
178
|
info(`Active users: ${u.active_users} MAU`);
|
|
167
179
|
info(`Price: $${(u.price_cents_each / 100).toFixed(2)} per MAU`);
|
|
180
|
+
if (u.as_of)
|
|
181
|
+
info(`As of: ${u.as_of} (MAU is aggregated, not real-time)`);
|
|
168
182
|
}
|
|
169
183
|
async function domain(args, flags) {
|
|
170
184
|
const action = args[0] || 'show';
|
|
171
185
|
const config = requireConfig();
|
|
172
|
-
const orgId = requireOrg(flags, config, 'myapi auth domain [show|set|delete] [--org <id>]');
|
|
186
|
+
const orgId = requireOrg(flags, config, 'myapi auth domain [show|set|verify|delete] [--org <id>]');
|
|
173
187
|
const showDomain = (d) => {
|
|
174
188
|
info(`Domain: ${d.domain}`);
|
|
175
189
|
info(`Status: ${d.status}`);
|
|
176
|
-
if (d.status
|
|
190
|
+
if (d.status === 'awaiting_verification' && d.verification) {
|
|
191
|
+
info('');
|
|
192
|
+
info('Prove ownership — create this DNS record:');
|
|
193
|
+
info(` ${d.verification.type} ${d.verification.name} → ${d.verification.value}`);
|
|
194
|
+
info('\nThen run: myapi auth domain verify');
|
|
195
|
+
}
|
|
196
|
+
else if (d.dns) {
|
|
177
197
|
info('');
|
|
178
|
-
info(
|
|
198
|
+
info('DNS — create this record:');
|
|
179
199
|
info(` ${d.dns.type} ${d.dns.name} → ${d.dns.value}`);
|
|
180
|
-
if (d.next)
|
|
181
|
-
info(`\n${d.next}`);
|
|
182
200
|
}
|
|
183
|
-
|
|
201
|
+
if (d.status === 'active') {
|
|
184
202
|
info(`Issuer: ${d.issuer}`);
|
|
185
203
|
info(`Login URL: ${d.login_url}`);
|
|
186
204
|
}
|
|
205
|
+
else if (d.next) {
|
|
206
|
+
info(`\n${d.next}`);
|
|
207
|
+
}
|
|
187
208
|
};
|
|
188
209
|
if (action === 'show') {
|
|
189
210
|
try {
|
|
@@ -215,6 +236,35 @@ async function domain(args, flags) {
|
|
|
215
236
|
showDomain(d);
|
|
216
237
|
return;
|
|
217
238
|
}
|
|
239
|
+
if (action === 'verify') {
|
|
240
|
+
try {
|
|
241
|
+
const d = await sdkAuth.verifyDomain(config.api_key, orgId);
|
|
242
|
+
if (flags.json) {
|
|
243
|
+
printJson(d);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (d.status === 'awaiting_verification') {
|
|
247
|
+
// Shouldn't normally happen (backend returns 422 when unverified), but
|
|
248
|
+
// be defensive.
|
|
249
|
+
info('Still awaiting verification — the TXT record was not found.');
|
|
250
|
+
showDomain(d);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
success(`Ownership verified — domain is now "${d.status}".`);
|
|
254
|
+
showDomain(d);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch (e) {
|
|
258
|
+
if (e?.code === 'DOMAIN_NOT_VERIFIED') {
|
|
259
|
+
const exp = e?.body?.expected;
|
|
260
|
+
error(`Ownership TXT record not found yet (DNS can take a few minutes). Create it and retry:\n ${exp ? `${exp.type} ${exp.name} → ${exp.value}` : 'see: myapi auth domain show'}`);
|
|
261
|
+
}
|
|
262
|
+
if (e?.code === 'NO_DOMAIN' || e?.status === 404)
|
|
263
|
+
error('No custom auth domain set. Set one first: myapi auth domain set --domain auth.example.com');
|
|
264
|
+
throw e;
|
|
265
|
+
}
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
218
268
|
if (action === 'delete' || action === 'remove') {
|
|
219
269
|
const host = flags.domain || args[1];
|
|
220
270
|
let target = host;
|
|
@@ -231,12 +281,12 @@ async function domain(args, flags) {
|
|
|
231
281
|
success(`Custom auth domain removed: ${target}`);
|
|
232
282
|
return;
|
|
233
283
|
}
|
|
234
|
-
error(`Unknown action "${action}". Use: myapi auth domain [show|set|delete]`);
|
|
284
|
+
error(`Unknown action "${action}". Use: myapi auth domain [show|set|verify|delete]`);
|
|
235
285
|
}
|
|
236
286
|
async function client(args, flags) {
|
|
237
287
|
const action = args[0] || 'list';
|
|
238
288
|
const config = requireConfig();
|
|
239
|
-
const orgId = requireOrg(flags, config, 'myapi auth client <list|create> [--org <id>]');
|
|
289
|
+
const orgId = requireOrg(flags, config, 'myapi auth client <list|create|delete|rotate> [--org <id>]');
|
|
240
290
|
if (action === 'list') {
|
|
241
291
|
const res = await sdkAuth.listClients(config.api_key, orgId);
|
|
242
292
|
if (flags.json) {
|
|
@@ -282,5 +332,48 @@ async function client(args, flags) {
|
|
|
282
332
|
}
|
|
283
333
|
return;
|
|
284
334
|
}
|
|
285
|
-
|
|
335
|
+
if (action === 'delete') {
|
|
336
|
+
const clientId = args[1];
|
|
337
|
+
if (!clientId)
|
|
338
|
+
error('Usage: myapi auth client delete <client_id> [--yes]');
|
|
339
|
+
// Destructive + irreversible — echo the exact client_id and confirm (the
|
|
340
|
+
// client stops authenticating immediately). --yes bypasses; refuse to hang
|
|
341
|
+
// in non-interactive contexts without it.
|
|
342
|
+
if (!flags.yes && !flags.y) {
|
|
343
|
+
if (isNonInteractive()) {
|
|
344
|
+
error(`Deleting client ${clientId} is irreversible. Re-run with --yes:\n myapi auth client delete ${clientId} --yes`);
|
|
345
|
+
}
|
|
346
|
+
const ok = await confirm(`› Delete OIDC client ${clientId}? It stops authenticating immediately. (y/N) `, false);
|
|
347
|
+
if (!ok) {
|
|
348
|
+
info('Cancelled.');
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const res = await sdkAuth.deleteClient(config.api_key, orgId, clientId);
|
|
353
|
+
if (flags.json) {
|
|
354
|
+
printJson(res);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
success(`Client deleted: ${clientId}`);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (action === 'rotate') {
|
|
361
|
+
const clientId = args[1];
|
|
362
|
+
if (!clientId)
|
|
363
|
+
error('Usage: myapi auth client rotate <client_id>');
|
|
364
|
+
const c = await sdkAuth.rotateClient(config.api_key, orgId, clientId);
|
|
365
|
+
if (flags.json) {
|
|
366
|
+
printJson(c);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
success(`Secret rotated for client: ${c.client_id || clientId}`);
|
|
370
|
+
info('The previous secret stops working immediately.');
|
|
371
|
+
if (c.client_secret) {
|
|
372
|
+
info('');
|
|
373
|
+
info('New client secret (shown ONCE — store it now, it cannot be retrieved again):');
|
|
374
|
+
info(` ${c.client_secret}`);
|
|
375
|
+
}
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
error(`Unknown action "${action}". Use: myapi auth client <list|create|delete|rotate>`);
|
|
286
379
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Tests for the auto-recharge client surface: the SDK `withFundsRetry`
|
|
2
|
+
// poll-and-retry helper + the CLI's `autoRechargeSummary` render helper.
|
|
3
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
4
|
+
import { MyApiError, withFundsRetry, isInsufficientFunds, isSpendCapExceeded, autoRechargeState } from '@myapihq/sdk';
|
|
5
|
+
import { autoRechargeSummary } from './billing.js';
|
|
6
|
+
// Build a 402 the way client.ts does — body is the unwrapped `error` object.
|
|
7
|
+
function funds(state, retryAfter) {
|
|
8
|
+
const body = { code: 'INSUFFICIENT_FUNDS', auto_recharge: state };
|
|
9
|
+
if (retryAfter != null)
|
|
10
|
+
body.retry_after_seconds = retryAfter;
|
|
11
|
+
return new MyApiError('INSUFFICIENT_FUNDS', 402, 'insufficient balance', body);
|
|
12
|
+
}
|
|
13
|
+
const noopSleep = () => Promise.resolve();
|
|
14
|
+
describe('withFundsRetry', () => {
|
|
15
|
+
it('returns the value when the call succeeds first try (no retry)', async () => {
|
|
16
|
+
const call = vi.fn().mockResolvedValue('ok');
|
|
17
|
+
await expect(withFundsRetry(call, { sleep: noopSleep })).resolves.toBe('ok');
|
|
18
|
+
expect(call).toHaveBeenCalledTimes(1);
|
|
19
|
+
});
|
|
20
|
+
it('retries on an in-flight refill, then succeeds', async () => {
|
|
21
|
+
const call = vi.fn()
|
|
22
|
+
.mockRejectedValueOnce(funds('in_flight', 0))
|
|
23
|
+
.mockResolvedValueOnce('ok');
|
|
24
|
+
const sleep = vi.fn(noopSleep);
|
|
25
|
+
await expect(withFundsRetry(call, { sleep })).resolves.toBe('ok');
|
|
26
|
+
expect(call).toHaveBeenCalledTimes(2);
|
|
27
|
+
expect(sleep).toHaveBeenCalledTimes(1);
|
|
28
|
+
});
|
|
29
|
+
it('honors the server retry_after_seconds for the sleep duration', async () => {
|
|
30
|
+
const call = vi.fn()
|
|
31
|
+
.mockRejectedValueOnce(funds('in_flight', 7))
|
|
32
|
+
.mockResolvedValueOnce('ok');
|
|
33
|
+
const sleep = vi.fn(noopSleep);
|
|
34
|
+
await withFundsRetry(call, { sleep });
|
|
35
|
+
expect(sleep).toHaveBeenCalledWith(7000);
|
|
36
|
+
});
|
|
37
|
+
it('gives up after maxRetries and rethrows the last error', async () => {
|
|
38
|
+
const call = vi.fn().mockRejectedValue(funds('in_flight', 0));
|
|
39
|
+
await expect(withFundsRetry(call, { maxRetries: 2, sleep: noopSleep }))
|
|
40
|
+
.rejects.toMatchObject({ code: 'INSUFFICIENT_FUNDS' });
|
|
41
|
+
expect(call).toHaveBeenCalledTimes(3); // initial + 2 retries
|
|
42
|
+
});
|
|
43
|
+
it('does NOT retry a capped refill — rethrows immediately (needs a human)', async () => {
|
|
44
|
+
const call = vi.fn().mockRejectedValue(funds('capped'));
|
|
45
|
+
await expect(withFundsRetry(call, { sleep: noopSleep })).rejects.toMatchObject({ code: 'INSUFFICIENT_FUNDS' });
|
|
46
|
+
expect(call).toHaveBeenCalledTimes(1);
|
|
47
|
+
});
|
|
48
|
+
it('does NOT retry no_pm / failed / disabled states', async () => {
|
|
49
|
+
for (const state of ['no_pm', 'failed', 'disabled']) {
|
|
50
|
+
const call = vi.fn().mockRejectedValue(funds(state));
|
|
51
|
+
await expect(withFundsRetry(call, { sleep: noopSleep })).rejects.toBeInstanceOf(MyApiError);
|
|
52
|
+
expect(call).toHaveBeenCalledTimes(1);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
it('does NOT retry a SPEND_CAP_EXCEEDED hard ceiling', async () => {
|
|
56
|
+
const err = new MyApiError('SPEND_CAP_EXCEEDED', 402, 'cap', { code: 'SPEND_CAP_EXCEEDED', cap_cents: 5000 });
|
|
57
|
+
const call = vi.fn().mockRejectedValue(err);
|
|
58
|
+
await expect(withFundsRetry(call, { sleep: noopSleep })).rejects.toBe(err);
|
|
59
|
+
expect(call).toHaveBeenCalledTimes(1);
|
|
60
|
+
});
|
|
61
|
+
it('rethrows non-funds errors untouched', async () => {
|
|
62
|
+
const err = new Error('network down');
|
|
63
|
+
const call = vi.fn().mockRejectedValue(err);
|
|
64
|
+
await expect(withFundsRetry(call, { sleep: noopSleep })).rejects.toBe(err);
|
|
65
|
+
expect(call).toHaveBeenCalledTimes(1);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
describe('funds predicates', () => {
|
|
69
|
+
it('isInsufficientFunds matches both new and legacy codes', () => {
|
|
70
|
+
expect(isInsufficientFunds(funds('disabled'))).toBe(true);
|
|
71
|
+
expect(isInsufficientFunds(new MyApiError('INSUFFICIENT_BALANCE', 402, 'x', {}))).toBe(true);
|
|
72
|
+
expect(isInsufficientFunds(new MyApiError('SPEND_CAP_EXCEEDED', 402, 'x', {}))).toBe(false);
|
|
73
|
+
expect(isInsufficientFunds(new Error('x'))).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
it('isSpendCapExceeded + autoRechargeState read the right fields', () => {
|
|
76
|
+
expect(isSpendCapExceeded(new MyApiError('SPEND_CAP_EXCEEDED', 402, 'x', {}))).toBe(true);
|
|
77
|
+
expect(autoRechargeState(funds('in_flight'))).toBe('in_flight');
|
|
78
|
+
expect(autoRechargeState(new Error('x'))).toBeUndefined();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
describe('autoRechargeSummary', () => {
|
|
82
|
+
const base = {
|
|
83
|
+
enabled: true, threshold_cents: 500, amount_cents: 2000, monthly_cap_cents: 10000,
|
|
84
|
+
month_to_date_recharged_cents: 4000, has_payment_method: true,
|
|
85
|
+
last_recharge_status: 'succeeded', last_recharge_attempt_at: null,
|
|
86
|
+
};
|
|
87
|
+
it('returns null when disabled', () => {
|
|
88
|
+
expect(autoRechargeSummary({ ...base, enabled: false })).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
it('summarizes an enabled config with dollar amounts', () => {
|
|
91
|
+
const s = autoRechargeSummary(base);
|
|
92
|
+
expect(s).toContain('$5.00');
|
|
93
|
+
expect(s).toContain('$20.00');
|
|
94
|
+
expect(s).toContain('$40.00'); // month-to-date
|
|
95
|
+
expect(s).toContain('$100.00'); // cap
|
|
96
|
+
});
|
|
97
|
+
it('flags a failed/capped status but not a healthy/pending one', () => {
|
|
98
|
+
expect(autoRechargeSummary({ ...base, last_recharge_status: 'failed' })).toContain('⚠ failed');
|
|
99
|
+
expect(autoRechargeSummary({ ...base, last_recharge_status: 'capped' })).toContain('⚠ capped');
|
|
100
|
+
expect(autoRechargeSummary({ ...base, last_recharge_status: 'pending' })).not.toContain('⚠');
|
|
101
|
+
expect(autoRechargeSummary(base)).not.toContain('⚠');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hq } from '@myapihq/sdk';
|
|
1
2
|
import type { FlagSchema } from '../flags.js';
|
|
2
3
|
import type { Flags } from '../helpers.js';
|
|
3
4
|
import type { Exposes } from '../exposes.js';
|
|
@@ -9,4 +10,6 @@ export declare function history(flags: Flags): Promise<void>;
|
|
|
9
10
|
export declare function usage(flags: Flags): Promise<void>;
|
|
10
11
|
export declare function topup(amountStr: string, flags: Flags): Promise<void>;
|
|
11
12
|
export declare function setup(_flags: Flags): Promise<void>;
|
|
13
|
+
export declare function autoRechargeSummary(cfg: hq.AutoRechargeConfig): string | null;
|
|
14
|
+
export declare function autoRecharge(action: string | undefined, flags: Flags): Promise<void>;
|
|
12
15
|
export declare function spendCap(arg: string | undefined, flags: Flags): Promise<void>;
|
package/dist/commands/billing.js
CHANGED
|
@@ -5,6 +5,9 @@ import { confirm, isNonInteractive } from '../prompt.js';
|
|
|
5
5
|
import { formatDate } from '../utils.js';
|
|
6
6
|
export const SCHEMA = {
|
|
7
7
|
period: 'string', // spend-cap window: month | day
|
|
8
|
+
threshold: 'string', // auto-recharge: refill when balance drops below ($)
|
|
9
|
+
amount: 'string', // auto-recharge: how much to refill each time ($)
|
|
10
|
+
'monthly-cap': 'string', // auto-recharge: max auto-recharged per month ($)
|
|
8
11
|
};
|
|
9
12
|
export const EXPOSES = [
|
|
10
13
|
'GET /hq/billing/balance',
|
|
@@ -14,8 +17,26 @@ export const EXPOSES = [
|
|
|
14
17
|
'POST /hq/billing/topup',
|
|
15
18
|
'GET /hq/account/me',
|
|
16
19
|
'PATCH /hq/account/spend-cap',
|
|
20
|
+
'GET /hq/billing/auto-recharge',
|
|
21
|
+
'PUT /hq/billing/auto-recharge',
|
|
22
|
+
'DELETE /hq/billing/auto-recharge',
|
|
17
23
|
];
|
|
18
24
|
const SUBCOMMAND_USAGE = {
|
|
25
|
+
'auto-recharge': `myapi billing auto-recharge [show | set | disable]
|
|
26
|
+
|
|
27
|
+
Keep the prepaid wallet funded without a human in the loop: when the balance
|
|
28
|
+
drops below the threshold, MyAPI charges your saved card to refill it, bounded
|
|
29
|
+
by a monthly cap. Off by default; enabling needs a payment method on file.
|
|
30
|
+
|
|
31
|
+
myapi billing auto-recharge Show current config + status
|
|
32
|
+
myapi billing auto-recharge set \\
|
|
33
|
+
--threshold 5 --amount 20 --monthly-cap 100
|
|
34
|
+
Refill to keep ≥$5, $20 at a time,
|
|
35
|
+
up to $100/month
|
|
36
|
+
myapi billing auto-recharge disable Turn off (settings are kept)
|
|
37
|
+
|
|
38
|
+
Amounts are whole dollars. The refill amount must be ≥ $5 and ≥ the threshold;
|
|
39
|
+
the monthly cap must be ≥ the refill amount.`,
|
|
19
40
|
'balance': 'myapi billing balance [--json]',
|
|
20
41
|
'history': 'myapi billing history [--json]',
|
|
21
42
|
'usage': `myapi billing usage [--period month|30d] [--json]
|
|
@@ -48,6 +69,7 @@ export async function run(subcommand, args, flags) {
|
|
|
48
69
|
info(`Usage: myapi billing <subcommand>
|
|
49
70
|
|
|
50
71
|
Subcommands:
|
|
72
|
+
auto-recharge Keep the wallet funded automatically (show/set/disable)
|
|
51
73
|
balance Check balance, credits, and payment method status
|
|
52
74
|
history View recent transactions and top-ups
|
|
53
75
|
setup Open a checkout link to add or update payment method
|
|
@@ -71,6 +93,7 @@ Subcommands:
|
|
|
71
93
|
case 'topup': return topup(args[0], flags);
|
|
72
94
|
case 'setup': return setup(flags);
|
|
73
95
|
case 'spend-cap': return spendCap(args[0], flags);
|
|
96
|
+
case 'auto-recharge': return autoRecharge(args[0], flags);
|
|
74
97
|
default: error(`Unknown subcommand: ${subcommand}. Run "myapi billing --help" for available subcommands.`);
|
|
75
98
|
}
|
|
76
99
|
}
|
|
@@ -165,6 +188,98 @@ export async function setup(_flags) {
|
|
|
165
188
|
const result = await hq.setupPayment(config.api_key);
|
|
166
189
|
success(`Open this URL in your browser to set up payment:\n${result.url}`);
|
|
167
190
|
}
|
|
191
|
+
function fmtCents(c) {
|
|
192
|
+
return c == null ? '—' : `$${(c / 100).toFixed(2)}`;
|
|
193
|
+
}
|
|
194
|
+
// One-line summary of auto-recharge state, reused by `billing auto-recharge`
|
|
195
|
+
// and `status`. Returns null when off (callers decide whether to show "off").
|
|
196
|
+
export function autoRechargeSummary(cfg) {
|
|
197
|
+
if (!cfg.enabled)
|
|
198
|
+
return null;
|
|
199
|
+
let s = `on — refill to ≥ ${fmtCents(cfg.threshold_cents)}, ${fmtCents(cfg.amount_cents)} each (${fmtCents(cfg.month_to_date_recharged_cents)}/${fmtCents(cfg.monthly_cap_cents)} this month)`;
|
|
200
|
+
if (cfg.last_recharge_status && cfg.last_recharge_status !== 'succeeded' && cfg.last_recharge_status !== 'pending') {
|
|
201
|
+
s += ` ⚠ ${cfg.last_recharge_status}`;
|
|
202
|
+
}
|
|
203
|
+
return s;
|
|
204
|
+
}
|
|
205
|
+
const RECHARGE_STATUS_HINT = {
|
|
206
|
+
failed: 'last refill was declined — check your card: myapi billing setup',
|
|
207
|
+
capped: 'monthly cap reached — refills paused until next month, or raise --monthly-cap',
|
|
208
|
+
no_pm: 'no payment method — add one: myapi billing setup',
|
|
209
|
+
pending: 'a refill is in progress',
|
|
210
|
+
};
|
|
211
|
+
function renderAutoRecharge(cfg) {
|
|
212
|
+
if (!cfg.enabled) {
|
|
213
|
+
info('Auto-recharge: off');
|
|
214
|
+
info(` Payment method on file: ${cfg.has_payment_method ? 'yes' : 'no'}`);
|
|
215
|
+
info(' Enable with: myapi billing auto-recharge set --threshold <$> --amount <$> --monthly-cap <$>');
|
|
216
|
+
if (!cfg.has_payment_method)
|
|
217
|
+
info(' (add a payment method first: myapi billing setup)');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
info('Auto-recharge: on');
|
|
221
|
+
info(` Refill to keep balance ≥ ${fmtCents(cfg.threshold_cents)}, adding ${fmtCents(cfg.amount_cents)} each time`);
|
|
222
|
+
info(` Monthly cap: ${fmtCents(cfg.month_to_date_recharged_cents)} of ${fmtCents(cfg.monthly_cap_cents)} used this month`);
|
|
223
|
+
info(` Payment method on file: ${cfg.has_payment_method ? 'yes' : 'no'}`);
|
|
224
|
+
if (cfg.last_recharge_status && cfg.last_recharge_status !== 'succeeded') {
|
|
225
|
+
const hint = RECHARGE_STATUS_HINT[cfg.last_recharge_status];
|
|
226
|
+
info(` ⚠ Status: ${cfg.last_recharge_status}${hint ? ` — ${hint}` : ''}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// Keep the wallet funded without a human. No arg / "show" → display config;
|
|
230
|
+
// "set" → enable/update from --threshold/--amount/--monthly-cap (whole
|
|
231
|
+
// dollars); "disable" → turn off (settings preserved for easy re-enable).
|
|
232
|
+
export async function autoRecharge(action, flags) {
|
|
233
|
+
const config = requireConfig();
|
|
234
|
+
const act = action ?? 'show';
|
|
235
|
+
if (act === 'show') {
|
|
236
|
+
const cfg = await hq.getAutoRecharge(config.api_key);
|
|
237
|
+
if (flags.json) {
|
|
238
|
+
printJson(cfg);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
renderAutoRecharge(cfg);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (act === 'disable') {
|
|
245
|
+
await hq.disableAutoRecharge(config.api_key);
|
|
246
|
+
success('Auto-recharge disabled. Your threshold/amount/cap are kept — re-enable with: myapi billing auto-recharge set');
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (act === 'set') {
|
|
250
|
+
const dollarsToCents = (v, name) => {
|
|
251
|
+
if (v == null)
|
|
252
|
+
return undefined;
|
|
253
|
+
const n = Number(v);
|
|
254
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
255
|
+
error(`Invalid --${name} "${v}". Use a whole dollar amount (e.g. --${name} 20).`);
|
|
256
|
+
}
|
|
257
|
+
return Math.round(n * 100);
|
|
258
|
+
};
|
|
259
|
+
const input = { enabled: true };
|
|
260
|
+
const threshold = dollarsToCents(flags.threshold, 'threshold');
|
|
261
|
+
const amount = dollarsToCents(flags.amount, 'amount');
|
|
262
|
+
const cap = dollarsToCents(flags['monthly-cap'], 'monthly-cap');
|
|
263
|
+
if (threshold != null)
|
|
264
|
+
input.threshold_cents = threshold;
|
|
265
|
+
if (amount != null)
|
|
266
|
+
input.amount_cents = amount;
|
|
267
|
+
if (cap != null)
|
|
268
|
+
input.monthly_cap_cents = cap;
|
|
269
|
+
// The backend enforces the invariants (≥$5 floor, amount ≥ threshold,
|
|
270
|
+
// cap ≥ amount, payment method present) and returns a 400 the top-level
|
|
271
|
+
// handler renders — no need to duplicate that validation here.
|
|
272
|
+
const cfg = await hq.setAutoRecharge(config.api_key, input);
|
|
273
|
+
if (flags.json) {
|
|
274
|
+
printJson(cfg);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
success('Auto-recharge enabled.');
|
|
278
|
+
renderAutoRecharge(cfg);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
error(`Unknown action "${act}". Use: myapi billing auto-recharge [show | set | disable]`);
|
|
282
|
+
}
|
|
168
283
|
// The account-level spend ceiling. No arg → show; "clear" → remove; a
|
|
169
284
|
// dollar amount → set. Distinct from per-key caps (myapi keys create
|
|
170
285
|
// --spend-cap): this is the aggregate backstop across the whole account.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// `domains` or `emails` sections come back empty (zero-state), the
|
|
4
4
|
// `setup` augmentation should escalate to actionable warns.
|
|
5
5
|
import { describe, it, expect } from 'vitest';
|
|
6
|
-
import { _setupSection } from './doctor.js';
|
|
6
|
+
import { _setupSection, sanitizeHint, _tallyTotals } from './doctor.js';
|
|
7
7
|
function mkReport(sections) {
|
|
8
8
|
return {
|
|
9
9
|
org_id: 'org-test',
|
|
@@ -20,6 +20,12 @@ function populated(name) {
|
|
|
20
20
|
id: 'x', severity: 'ok', scope: name, message: 'something',
|
|
21
21
|
}] };
|
|
22
22
|
}
|
|
23
|
+
// A section the backend reports as having resources, but with no issues
|
|
24
|
+
// emitted (all healthy, no per-resource ok rows). The issue-count proxy
|
|
25
|
+
// would mis-read this as zero-state; `resource_count` must win.
|
|
26
|
+
function healthyQuiet(name, count) {
|
|
27
|
+
return { name, summary: 'all checks passed', issues: [], resource_count: count };
|
|
28
|
+
}
|
|
23
29
|
describe('_setupSection', () => {
|
|
24
30
|
it('returns null when both domains + emails sections are populated', () => {
|
|
25
31
|
expect(_setupSection(mkReport([populated('domains'), populated('emails')]))).toBeNull();
|
|
@@ -71,4 +77,83 @@ describe('_setupSection', () => {
|
|
|
71
77
|
const s = _setupSection(mkReport([populated('domains'), populated('emails')]), { mailingAddress: undefined });
|
|
72
78
|
expect(s).toBeNull();
|
|
73
79
|
});
|
|
80
|
+
// resource_count is authoritative — it must override the fragile
|
|
81
|
+
// issues.length proxy in both directions.
|
|
82
|
+
it('does NOT flag a healthy-but-quiet domains section (resource_count > 0, no issues)', () => {
|
|
83
|
+
// The bug class this prevents: backend stops emitting per-domain `ok`
|
|
84
|
+
// rows → issues:[] → proxy would falsely say "No domain registered".
|
|
85
|
+
const s = _setupSection(mkReport([healthyQuiet('domains', 2), healthyQuiet('emails', 1)]));
|
|
86
|
+
expect(s).toBeNull();
|
|
87
|
+
});
|
|
88
|
+
it('flags zero-state via resource_count even if an issue happens to be present', () => {
|
|
89
|
+
const domains = {
|
|
90
|
+
name: 'domains', summary: 'x', resource_count: 0,
|
|
91
|
+
issues: [{ id: 'y', severity: 'ok', scope: 'domains', message: 'stale ok row' }],
|
|
92
|
+
};
|
|
93
|
+
const s = _setupSection(mkReport([domains, populated('emails')]));
|
|
94
|
+
expect(s).not.toBeNull();
|
|
95
|
+
expect(s.issues).toHaveLength(1);
|
|
96
|
+
expect(s.issues[0].scope).toBe('setup/domain');
|
|
97
|
+
});
|
|
98
|
+
it('falls back to the issue-count proxy when resource_count is absent', () => {
|
|
99
|
+
// Older backend: no resource_count → empty section still reads as zero-state.
|
|
100
|
+
const s = _setupSection(mkReport([empty('domains'), populated('emails')]));
|
|
101
|
+
expect(s.issues[0].scope).toBe('setup/domain');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
describe('sanitizeHint', () => {
|
|
105
|
+
it('drops a hint referencing an internal docs/ artifact', () => {
|
|
106
|
+
expect(sanitizeHint('see docs/security-debt-pixel-hypersearch-2026-05-27.md item #2 for the fix')).toBeUndefined();
|
|
107
|
+
});
|
|
108
|
+
it('drops a hint referencing an internal code path', () => {
|
|
109
|
+
expect(sanitizeHint('fix in internal/routes/billing/billing.go')).toBeUndefined();
|
|
110
|
+
expect(sanitizeHint('see packages/cli/src/commands/doctor.ts')).toBeUndefined();
|
|
111
|
+
});
|
|
112
|
+
it('keeps actionable command hints unchanged', () => {
|
|
113
|
+
const cmd = 'Create one with: myapi email mailbox create <username>@<your-domain>';
|
|
114
|
+
expect(sanitizeHint(cmd)).toBe(cmd);
|
|
115
|
+
expect(sanitizeHint('attach a workflow if you want a notification on each submission'))
|
|
116
|
+
.toBe('attach a workflow if you want a notification on each submission');
|
|
117
|
+
});
|
|
118
|
+
it('does not false-match a bare word with a dotted suffix (no slash)', () => {
|
|
119
|
+
// "node.js" / "data.object" have no path slash → not an internal artifact.
|
|
120
|
+
expect(sanitizeHint('deploy your node.js app')).toBe('deploy your node.js app');
|
|
121
|
+
});
|
|
122
|
+
it('passes through undefined', () => {
|
|
123
|
+
expect(sanitizeHint(undefined)).toBeUndefined();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
describe('_tallyTotals', () => {
|
|
127
|
+
function sec(name, issues) {
|
|
128
|
+
return {
|
|
129
|
+
name, summary: '', issues: issues.map((i, n) => ({
|
|
130
|
+
id: `${name}-${n}`, severity: 'warn', scope: name, message: 'm', ...i,
|
|
131
|
+
})),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
it('counts plain severities', () => {
|
|
135
|
+
const t = _tallyTotals([sec('a', [{ severity: 'crit' }, { severity: 'warn' }, { severity: 'ok' }])]);
|
|
136
|
+
expect(t).toEqual({ ok: 1, warn: 1, crit: 1, operator: 0 });
|
|
137
|
+
});
|
|
138
|
+
it('pulls an operator_only critical OUT of crit and into operator', () => {
|
|
139
|
+
// The pixel case: a platform-side critical must not count as a customer
|
|
140
|
+
// critical (so it won't set exit code 1).
|
|
141
|
+
const t = _tallyTotals([sec('pixel', [{ severity: 'crit', operator_only: true }])]);
|
|
142
|
+
expect(t.crit).toBe(0);
|
|
143
|
+
expect(t.operator).toBe(1);
|
|
144
|
+
});
|
|
145
|
+
it('pulls an operator_only warning into operator too', () => {
|
|
146
|
+
const t = _tallyTotals([sec('x', [{ severity: 'warn', operator_only: true }, { severity: 'warn' }])]);
|
|
147
|
+
expect(t.warn).toBe(1);
|
|
148
|
+
expect(t.operator).toBe(1);
|
|
149
|
+
});
|
|
150
|
+
it('still counts a real customer critical alongside an operator_only one', () => {
|
|
151
|
+
const t = _tallyTotals([sec('x', [{ severity: 'crit' }, { severity: 'crit', operator_only: true }])]);
|
|
152
|
+
expect(t.crit).toBe(1); // exit code WILL be 1 — there's a real one
|
|
153
|
+
expect(t.operator).toBe(1);
|
|
154
|
+
});
|
|
155
|
+
it('treats an operator_only ok row as a passing check, not an operator issue', () => {
|
|
156
|
+
const t = _tallyTotals([sec('x', [{ severity: 'ok', operator_only: true }])]);
|
|
157
|
+
expect(t).toEqual({ ok: 1, warn: 0, crit: 0, operator: 0 });
|
|
158
|
+
});
|
|
74
159
|
});
|
|
@@ -4,6 +4,14 @@ import { type Flags } from '../helpers.js';
|
|
|
4
4
|
import type { Exposes } from '../exposes.js';
|
|
5
5
|
export declare const EXPOSES: Exposes;
|
|
6
6
|
export declare const SCHEMA: FlagSchema;
|
|
7
|
+
export interface DoctorTotals {
|
|
8
|
+
ok: number;
|
|
9
|
+
warn: number;
|
|
10
|
+
crit: number;
|
|
11
|
+
operator: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function _tallyTotals(sections: sdkHq.DoctorSection[]): DoctorTotals;
|
|
14
|
+
export declare function sanitizeHint(hint: string | undefined): string | undefined;
|
|
7
15
|
export interface SetupContext {
|
|
8
16
|
mailingAddress?: string | null;
|
|
9
17
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -41,6 +41,33 @@ const MARK = {
|
|
|
41
41
|
warn: `${C.warn}⚠${C.reset}`,
|
|
42
42
|
crit: `${C.err}✗${C.reset}`,
|
|
43
43
|
};
|
|
44
|
+
// Operator-only issues are platform-side and not the customer's to fix. Render
|
|
45
|
+
// them with a distinct, non-alarming marker so they don't read as a failure the
|
|
46
|
+
// customer must act on — regardless of the backend severity.
|
|
47
|
+
const OPERATOR_MARK = `${C.dim}ℹ${C.reset}`;
|
|
48
|
+
// Tally issues for the exit code + summary. `operator_only` warn/crit issues
|
|
49
|
+
// are pulled into their own `operator` bucket and OUT of crit/warn: a
|
|
50
|
+
// platform-side problem the customer can't fix must not fail their `doctor`
|
|
51
|
+
// run (exit 1) or inflate their critical count. Everything else tallies by
|
|
52
|
+
// severity as before; any ok row (operator or not) counts as ok.
|
|
53
|
+
export function _tallyTotals(sections) {
|
|
54
|
+
const t = { ok: 0, warn: 0, crit: 0, operator: 0 };
|
|
55
|
+
for (const s of sections) {
|
|
56
|
+
for (const i of s.issues) {
|
|
57
|
+
if (i.severity === 'crit' || i.severity === 'warn') {
|
|
58
|
+
if (i.operator_only)
|
|
59
|
+
t.operator++;
|
|
60
|
+
else
|
|
61
|
+
t[i.severity]++;
|
|
62
|
+
}
|
|
63
|
+
else if (i.severity === 'ok') {
|
|
64
|
+
t.ok++;
|
|
65
|
+
}
|
|
66
|
+
// Unknown severities the backend might invent are ignored (no NaN).
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return t;
|
|
70
|
+
}
|
|
44
71
|
function rule(width = 60) {
|
|
45
72
|
return `${C.dim}${'─'.repeat(width)}${C.reset}`;
|
|
46
73
|
}
|
|
@@ -51,14 +78,43 @@ function rule(width = 60) {
|
|
|
51
78
|
function localIssueId(kind, key) {
|
|
52
79
|
return `${kind}/${createHash('sha256').update(key).digest('hex').slice(0, 16)}`;
|
|
53
80
|
}
|
|
81
|
+
// Backend hints are sometimes written for operators, not customers — e.g.
|
|
82
|
+
// "see docs/security-debt-pixel-hypersearch-2026-05-27.md item #2 for the fix".
|
|
83
|
+
// A reference to an internal repo artifact (a file path with a doc/code
|
|
84
|
+
// extension, or anything under `docs/`) is useless to — and leaks internals
|
|
85
|
+
// at — the customer running `myapi doctor`. Drop the hint entirely in that
|
|
86
|
+
// case: the issue's `message` still conveys what's wrong; a hint that points
|
|
87
|
+
// nowhere the user can go is worse than no hint. (Root fix is backend-side
|
|
88
|
+
// hint hygiene — see docs/cross-repo-prompts/backend-doctor-quality-*.md.)
|
|
89
|
+
const INTERNAL_ARTIFACT_RE = /\bdocs\/[\w.\-/]+|\b[\w.\-]+\/[\w.\-/]*\.(?:md|go|tsx?|jsx?|json|ya?ml|sql)\b/i;
|
|
90
|
+
export function sanitizeHint(hint) {
|
|
91
|
+
if (!hint)
|
|
92
|
+
return undefined;
|
|
93
|
+
return INTERNAL_ARTIFACT_RE.test(hint) ? undefined : hint;
|
|
94
|
+
}
|
|
54
95
|
function fmtIssue(i) {
|
|
55
96
|
// Prefix the entity name so sibling issues with identical messages
|
|
56
97
|
// (e.g. four "domain is active" rows) are distinguishable. Skip it when
|
|
57
98
|
// the message already names the entity — the local DNS section does.
|
|
58
99
|
const name = i.entity?.name;
|
|
59
100
|
const label = name && !i.message.includes(name) ? `${C.bold}${name}${C.reset} — ` : '';
|
|
60
|
-
const
|
|
61
|
-
|
|
101
|
+
const mark = i.operator_only ? OPERATOR_MARK : (MARK[i.severity] ?? '·');
|
|
102
|
+
const head = ` ${mark} ${label}${i.message}`;
|
|
103
|
+
const hint = sanitizeHint(i.hint);
|
|
104
|
+
return hint ? `${head}\n ${C.dim}→ ${hint}${C.reset}` : head;
|
|
105
|
+
}
|
|
106
|
+
// "Does this section's org have zero resources of its kind?" Prefer the
|
|
107
|
+
// backend's authoritative `resource_count` when present; fall back to the
|
|
108
|
+
// issue-count proxy only for older backends that omit it. The proxy is
|
|
109
|
+
// fragile — it reads "section emitted no issues" as "no resources", which
|
|
110
|
+
// holds today only because the backend emits an `ok` row per healthy
|
|
111
|
+
// resource. The moment a section returns `issues: []` for an all-healthy
|
|
112
|
+
// org, the proxy would mis-fire (e.g. "No domain registered" for an org
|
|
113
|
+
// that has a live domain). `resource_count` removes that coupling.
|
|
114
|
+
function isZeroState(section) {
|
|
115
|
+
if (typeof section.resource_count === 'number')
|
|
116
|
+
return section.resource_count === 0;
|
|
117
|
+
return section.issues.length === 0;
|
|
62
118
|
}
|
|
63
119
|
export function _setupSection(report, ctx = {}) {
|
|
64
120
|
const byName = new Map();
|
|
@@ -66,7 +122,7 @@ export function _setupSection(report, ctx = {}) {
|
|
|
66
122
|
byName.set(s.name, s);
|
|
67
123
|
const issues = [];
|
|
68
124
|
const dom = byName.get('domains');
|
|
69
|
-
if (dom && dom
|
|
125
|
+
if (dom && isZeroState(dom)) {
|
|
70
126
|
issues.push({
|
|
71
127
|
id: localIssueId('setup_no_domain', report.org_id),
|
|
72
128
|
severity: 'warn',
|
|
@@ -77,7 +133,7 @@ export function _setupSection(report, ctx = {}) {
|
|
|
77
133
|
});
|
|
78
134
|
}
|
|
79
135
|
const em = byName.get('emails');
|
|
80
|
-
if (em && em
|
|
136
|
+
if (em && isZeroState(em)) {
|
|
81
137
|
issues.push({
|
|
82
138
|
id: localIssueId('setup_no_mailbox', report.org_id),
|
|
83
139
|
severity: 'warn',
|
|
@@ -283,18 +339,12 @@ export async function run(_subcommand, _args, flags) {
|
|
|
283
339
|
const reachSection = await httpProbeSection(apiKey, orgId);
|
|
284
340
|
if (reachSection)
|
|
285
341
|
report.sections.push(reachSection);
|
|
286
|
-
// Re-tally totals after local augmentation.
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
for (const i of s.issues) {
|
|
290
|
-
// Ignore any severity the backend invents that we don't model — better
|
|
291
|
-
// a missed count than a NaN poisoning the whole tally.
|
|
292
|
-
if (i.severity in totals)
|
|
293
|
-
totals[i.severity]++;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
342
|
+
// Re-tally totals after local augmentation. `operator_only` issues land in
|
|
343
|
+
// their own bucket and out of crit/warn (see _tallyTotals).
|
|
344
|
+
const totals = _tallyTotals(report.sections);
|
|
296
345
|
// Set the exit code before any output branch — CI relies on it in both
|
|
297
|
-
// the human and the --json path.
|
|
346
|
+
// the human and the --json path. Only customer-actionable criticals fail
|
|
347
|
+
// the run; a platform-side (operator_only) critical does not.
|
|
298
348
|
if (totals.crit)
|
|
299
349
|
process.exitCode = 1;
|
|
300
350
|
if (wantJson) {
|
|
@@ -322,6 +372,11 @@ export async function run(_subcommand, _args, flags) {
|
|
|
322
372
|
else {
|
|
323
373
|
info(`${MARK.ok} ${totals.ok} check${totals.ok === 1 ? '' : 's'} passed`);
|
|
324
374
|
}
|
|
375
|
+
// Platform-side issues are surfaced for transparency but don't count against
|
|
376
|
+
// the org or fail the run — make that explicit.
|
|
377
|
+
if (totals.operator) {
|
|
378
|
+
info(`${OPERATOR_MARK} ${C.dim}${totals.operator} platform-side issue${totals.operator === 1 ? '' : 's'} the MyAPI team is handling (not counted against your org)${C.reset}`);
|
|
379
|
+
}
|
|
325
380
|
if (!verbose)
|
|
326
381
|
info(`${C.dim}(--verbose to show passing checks · --json for machine-readable)${C.reset}`);
|
|
327
382
|
}
|
package/dist/commands/status.js
CHANGED
|
@@ -3,9 +3,11 @@ import { hq, domain, funnel, webhook, workflow, MyApiError } from '@myapihq/sdk'
|
|
|
3
3
|
// filter, so there is no global "all mailboxes in this org" call. Use
|
|
4
4
|
// `myapi email mailbox list --domain <d>` for per-domain detail.
|
|
5
5
|
import { loadConfig } from '../config.js';
|
|
6
|
+
import { autoRechargeSummary } from './billing.js';
|
|
6
7
|
import { info, error, printJson } from '../output.js';
|
|
7
8
|
export const EXPOSES = [
|
|
8
9
|
'GET /hq/billing/balance',
|
|
10
|
+
'GET /hq/billing/auto-recharge',
|
|
9
11
|
'GET /hq/account/free-tier',
|
|
10
12
|
'GET /domain/orgs/{org_id}/list',
|
|
11
13
|
'GET /funnel/orgs/{org_id}/funnels',
|
|
@@ -44,8 +46,10 @@ export async function run(_subcommand, _args, flags = {}) {
|
|
|
44
46
|
const accountResults = await Promise.allSettled([
|
|
45
47
|
hq.getBalance(config.api_key),
|
|
46
48
|
hq.getFreeTier(config.api_key),
|
|
49
|
+
hq.getAutoRecharge(config.api_key),
|
|
47
50
|
]);
|
|
48
51
|
const balance = unwrap(accountResults[0]);
|
|
52
|
+
const autoR = unwrap(accountResults[2]);
|
|
49
53
|
// The backend currently returns an array of { service, used, allowance }
|
|
50
54
|
// per-service entries. The SDK type lies about this (claims scalar) — same
|
|
51
55
|
// bug surfaces in `whoami`. Treat as unknown here and narrow at render time.
|
|
@@ -78,6 +82,7 @@ export async function run(_subcommand, _args, flags = {}) {
|
|
|
78
82
|
key_rejected: keyRejected,
|
|
79
83
|
},
|
|
80
84
|
balance,
|
|
85
|
+
auto_recharge: autoR,
|
|
81
86
|
free_tier: freeTier,
|
|
82
87
|
org_resources: orgKnown ? {
|
|
83
88
|
domains, funnels, webhooks, workflows,
|
|
@@ -100,6 +105,13 @@ export async function run(_subcommand, _args, flags = {}) {
|
|
|
100
105
|
if (config.is_anonymous) {
|
|
101
106
|
info(' → Link an email to unlock $5 free credit + paid actions: myapi account link <email>');
|
|
102
107
|
}
|
|
108
|
+
// Auto-recharge: only worth a line when it's on (or off but a refill recently
|
|
109
|
+
// failed/capped and needs attention).
|
|
110
|
+
if (autoR) {
|
|
111
|
+
const summary = autoRechargeSummary(autoR);
|
|
112
|
+
if (summary)
|
|
113
|
+
info(`Recharge: ${summary}`);
|
|
114
|
+
}
|
|
103
115
|
if (Array.isArray(freeTier) && freeTier.length > 0) {
|
|
104
116
|
const parts = freeTier
|
|
105
117
|
.filter((e) => e && typeof e.service === 'string')
|
package/dist/index.js
CHANGED
|
@@ -296,19 +296,40 @@ async function main() {
|
|
|
296
296
|
if (err.status === 401)
|
|
297
297
|
error('Invalid API key. Run: myapi account setup');
|
|
298
298
|
else if (err.status === 402) {
|
|
299
|
+
const body = (err.body ?? {});
|
|
299
300
|
if (err.code === 'REGISTRATION_REQUIRED' || err.code === 'UPGRADE_REQUIRED')
|
|
300
301
|
error('A verified email is required. Run: myapi account link');
|
|
301
302
|
else if (err.code === 'NO_PAYMENT_METHOD')
|
|
302
303
|
error('No payment method on file. Run: myapi billing setup');
|
|
304
|
+
else if (err.code === 'SPEND_CAP_EXCEEDED') {
|
|
305
|
+
// A self-imposed ceiling, NOT an empty wallet — topping up won't
|
|
306
|
+
// help. Point at the cap, not the balance.
|
|
307
|
+
const cap = typeof body.cap_cents === 'number' ? `$${(body.cap_cents / 100).toFixed(2)}` : 'your cap';
|
|
308
|
+
const spent = typeof body.spent_cents === 'number' ? `$${(body.spent_cents / 100).toFixed(2)} of ` : '';
|
|
309
|
+
const period = typeof body.period === 'string' ? body.period : 'period';
|
|
310
|
+
error(`Spend cap reached (${spent}${cap} this ${period}). This is a ceiling you set, not an empty wallet — raise or clear it: myapi billing spend-cap`);
|
|
311
|
+
}
|
|
303
312
|
else {
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
//
|
|
313
|
+
// INSUFFICIENT_FUNDS (or legacy INSUFFICIENT_BALANCE): empty wallet.
|
|
314
|
+
// Tailor the unblock to the auto-recharge state the backend reports
|
|
315
|
+
// so an agent knows whether to wait, fix a card, or top up.
|
|
316
|
+
const state = body.auto_recharge;
|
|
307
317
|
const cfg = loadConfig();
|
|
308
|
-
if (
|
|
309
|
-
|
|
318
|
+
if (state === 'in_flight') {
|
|
319
|
+
const secs = typeof body.retry_after_seconds === 'number' ? body.retry_after_seconds : 5;
|
|
320
|
+
error(`Wallet empty — auto-recharge is in flight; retry in ~${secs}s.`);
|
|
310
321
|
}
|
|
311
|
-
|
|
322
|
+
else if (state === 'capped')
|
|
323
|
+
error('Wallet empty and auto-recharge hit your monthly cap. Raise it: myapi billing auto-recharge set --monthly-cap <amount>, or top up: myapi billing topup <amount>');
|
|
324
|
+
else if (state === 'failed')
|
|
325
|
+
error('Wallet empty and the last auto-recharge was declined. Fix your card: myapi billing setup, or top up: myapi billing topup <amount>');
|
|
326
|
+
else if (state === 'no_pm')
|
|
327
|
+
error('Wallet empty and auto-recharge has no payment method. Add one: myapi billing setup, or top up: myapi billing topup <amount>');
|
|
328
|
+
// Anonymous accounts can't top up (no payment surface) — link to unlock.
|
|
329
|
+
else if (cfg?.is_anonymous)
|
|
330
|
+
error('Insufficient balance. Anonymous accounts have no free credit — link an email to unlock $5: myapi account link <email>');
|
|
331
|
+
else
|
|
332
|
+
error('Insufficient balance. Top up: myapi billing topup <amount> — or keep it funded automatically: myapi billing auto-recharge set');
|
|
312
333
|
}
|
|
313
334
|
}
|
|
314
335
|
else
|
|
@@ -8,6 +8,7 @@ Core identity and billing hub for the MyAPI ecosystem. **Start here** — every
|
|
|
8
8
|
- API key creation and management
|
|
9
9
|
- Organization management (`org_id` used by all other services)
|
|
10
10
|
- Balance top-up and billing history
|
|
11
|
+
- Org-wide health check (`myapi doctor`)
|
|
11
12
|
|
|
12
13
|
## Quickstart
|
|
13
14
|
|
|
@@ -3,7 +3,7 @@ name: my-api-hq
|
|
|
3
3
|
version: 1.0.0
|
|
4
4
|
description: >
|
|
5
5
|
Auth, organizations, and billing hub. Start here to get an api_key and org_id — every other service depends on both.
|
|
6
|
-
triggers: [api key, account, organization, org, billing, balance, topup, credits, setup, defaults, brand, sync brand]
|
|
6
|
+
triggers: [api key, account, organization, org, billing, balance, topup, credits, setup, defaults, brand, sync brand, doctor, health check, is my org healthy]
|
|
7
7
|
checksum: sha256-pending
|
|
8
8
|
---
|
|
9
9
|
|
|
@@ -13,7 +13,7 @@ The root service. It manages accounts, API keys, organizations, and billing. No
|
|
|
13
13
|
|
|
14
14
|
## Capabilities
|
|
15
15
|
<!-- llm:start -->
|
|
16
|
-
MyApiHQ is the platform's foundation. Every other service (domain, funnel,
|
|
16
|
+
MyApiHQ is the platform's foundation. Every other service (domain, funnel, auth, payments, fn, workflow, database, storage, email, webhook, crm, llm, image, pixel, url) requires both an `api_key` and (for org-scoped resources) an `org_id` minted here. Setup is one command — `myapi account setup` — which provisions an account, generates an api_key, creates a default org, and stores everything in `~/.myapi/config.json`. Subsequent commands pick up those defaults automatically.
|
|
17
17
|
|
|
18
18
|
```
|
|
19
19
|
myapihq ──► org_id + api_key
|
|
@@ -30,7 +30,11 @@ Two tiers, chosen at setup time:
|
|
|
30
30
|
- **Anonymous** (`myapi account setup --anonymous`): zero-friction account creation. **Starts with $0 credit.** Good for catalog browsing, reading help, inspecting schemas — nothing that costs upstream money. The agent-onboarding path: provisions an account in one call, no email needed.
|
|
31
31
|
- **Registered** (verified email via `myapi account link <email>`): unlocks $5 free credit and the paid surface (LLM, image, email, domain register, etc.). Required for `myapi billing setup` and anything that hits Stripe.
|
|
32
32
|
|
|
33
|
-
An anonymous account can upgrade at any time via `myapi account link <email>` — the credit grants on successful verification. Anonymous accounts that need paid actions hit a friendly `
|
|
33
|
+
An anonymous account can upgrade at any time via `myapi account link <email>` — the credit grants on successful verification. Anonymous accounts that need paid actions hit a friendly `REGISTRATION_REQUIRED` error pointing at `myapi account link`; a registered account with an empty wallet hits `402 INSUFFICIENT_FUNDS` (top up, or enable auto-recharge — below).
|
|
34
|
+
|
|
35
|
+
### Health check
|
|
36
|
+
|
|
37
|
+
`myapi doctor` runs an org-wide consistency check across every slot (funnels, webhooks, domains, containers, workflows, emails, payments) and layers on customer-perspective DNS/HTTP probes from your machine. It returns per-section findings (`✓` pass / `⚠` warning / `✗` critical) with remediation hints; add `--json` for machine output. The exit code is non-zero **only** on customer-actionable criticals — platform-side issues the MyAPI team is already handling are surfaced with an `ℹ` marker but don't fail the run. Run it to self-check before building (is the org set up?) and after (did everything wire up?).
|
|
34
38
|
<!-- llm:end -->
|
|
35
39
|
|
|
36
40
|
## Commands
|
|
@@ -53,9 +57,11 @@ An anonymous account can upgrade at any time via `myapi account link <email>`
|
|
|
53
57
|
| `myapi billing history` | Recent transactions |
|
|
54
58
|
| `myapi billing usage [--period month|30d]` | Spend rolled up by service (this month, or trailing 30d) |
|
|
55
59
|
| `myapi billing spend-cap [<amount> | clear] [--period month|day]` | Set/show/clear the account-level spend ceiling (IAM Layer 2) |
|
|
60
|
+
| `myapi billing auto-recharge [show \| set \| disable]` | Keep the wallet funded automatically — off-session refill when balance drops below a threshold, bounded by a monthly cap |
|
|
56
61
|
| `myapi account mailing-address ["<address>"]` | Get or set the account's CAN-SPAM mailing address (required for email send) |
|
|
57
62
|
| `myapi config set-org <id>` / `set-funnel <id>` / `set-domain <name>` | Set CLI defaults |
|
|
58
63
|
| `myapi install-skills` | Install agent skill files into ~/.claude/, ~/.gemini/, ~/.cursor/ |
|
|
64
|
+
| `myapi doctor [--verbose] [--json]` | Org-wide health check: per-slot config/integrity findings + DNS/HTTP probes |
|
|
59
65
|
<!-- generated:end -->
|
|
60
66
|
|
|
61
67
|
## Examples
|
|
@@ -70,6 +76,10 @@ myapi account whoami # confirm what's active
|
|
|
70
76
|
myapi billing balance # before doing anything that costs credits
|
|
71
77
|
myapi billing topup 20 # add $20
|
|
72
78
|
|
|
79
|
+
# For unattended/autonomous runs: keep the wallet funded so a 402 never
|
|
80
|
+
# stalls the agent. Refill to ≥$5, $20 at a time, up to $100/month.
|
|
81
|
+
myapi billing auto-recharge set --threshold 5 --amount 20 --monthly-cap 100
|
|
82
|
+
|
|
73
83
|
# Sync brand info from an existing website
|
|
74
84
|
myapi org sync-brand acme.com
|
|
75
85
|
|
|
@@ -77,7 +87,7 @@ myapi org sync-brand acme.com
|
|
|
77
87
|
myapi account switch 2
|
|
78
88
|
```
|
|
79
89
|
|
|
80
|
-
If any service returns `402`,
|
|
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.
|
|
81
91
|
|
|
82
92
|
Each org gets a free preview subdomain (`*.makeautonomous.com`) usable before registering a custom domain.
|
|
83
93
|
<!-- llm:end -->
|
|
@@ -39,9 +39,10 @@ its clients.
|
|
|
39
39
|
returns a `client_secret` **once** — store it immediately. `--redirect` lists
|
|
40
40
|
allowed callback URIs (absolute https, or http://localhost for dev).
|
|
41
41
|
- **Usage** — `auth usage` shows monthly active users (auth is billed per MAU).
|
|
42
|
-
- **Custom domain** —
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
- **Custom domain** — serve auth on `auth.acme.com`. Three steps: `auth domain
|
|
43
|
+
set --domain auth.acme.com` prints a **TXT ownership challenge**; publish it,
|
|
44
|
+
then `auth domain verify`; that prints the **A record** to publish, TLS
|
|
45
|
+
provisions automatically, and the domain becomes your issuer once active.
|
|
45
46
|
|
|
46
47
|
**Sign-in methods** are chosen per tenant via
|
|
47
48
|
`auth tenant create --connections google,password,magic` (default `google`):
|
|
@@ -61,8 +62,10 @@ CLI is only the management surface.
|
|
|
61
62
|
| `myapi auth tenant create` | Create/enable the tenant (`--connections google,password,magic`; `--theme <json>`) |
|
|
62
63
|
| `myapi auth client list` | List the OIDC clients (apps) registered to your tenant |
|
|
63
64
|
| `myapi auth client create` | Register an OIDC client (`--name`, `--type spa\|web`, `--redirect`) |
|
|
64
|
-
| `myapi auth
|
|
65
|
-
| `myapi auth
|
|
65
|
+
| `myapi auth client delete <id>` | Revoke a client (irreversible); `--yes` to skip the confirm |
|
|
66
|
+
| `myapi auth client rotate <id>` | Re-issue a `web` client's secret (shown once) |
|
|
67
|
+
| `myapi auth usage` | Monthly active users (MAU) for the current period (`as_of` shows freshness) |
|
|
68
|
+
| `myapi auth domain` | Custom auth domain: `set` (TXT challenge) → `verify` → A record; also `show`/`delete` |
|
|
66
69
|
<!-- generated:end -->
|
|
67
70
|
|
|
68
71
|
## Examples
|
|
@@ -109,4 +112,4 @@ myapi auth client list
|
|
|
109
112
|
be retrieved later — store it immediately. SPA clients have no secret.
|
|
110
113
|
- Redirect URIs are matched exactly: absolute `https://…` (or
|
|
111
114
|
`http://localhost…` for local dev).
|
|
112
|
-
- `402`
|
|
115
|
+
- `402 INSUFFICIENT_FUNDS` = empty wallet → `myapi billing topup <amount>` (or keep it funded automatically: `myapi billing auto-recharge set`). `402 SPEND_CAP_EXCEEDED` = you hit your account spend ceiling → raise it with `myapi billing spend-cap`.
|
|
@@ -114,6 +114,6 @@ Set `essentially_off` + `browser-check=off` to allow AI crawlers and training bo
|
|
|
114
114
|
|
|
115
115
|
- `register`, `renew`, `assign`, and `unassign` always require an explicit domain argument — they don't fall back to a stored default, to prevent accidental destructive actions.
|
|
116
116
|
- All commands default to `--org` from your saved config (set with `myapi config set-org <id>`).
|
|
117
|
-
- `402`
|
|
117
|
+
- `402 INSUFFICIENT_FUNDS` = empty wallet → `myapi billing topup <amount>` (or keep it funded automatically: `myapi billing auto-recharge set`). `402 SPEND_CAP_EXCEEDED` = you hit your account spend ceiling → raise it with `myapi billing spend-cap`.
|
|
118
118
|
|
|
119
119
|
Run `myapi domain --help` or `myapi domain <subcommand> --help` for full flag reference.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
# my-email-api
|
|
3
3
|
|
|
4
|
-
Send transactional email
|
|
4
|
+
Send transactional email from mailboxes on your own registered domains. Includes AI template generation, warmup, and inbox/outbox reading.
|
|
5
5
|
|
|
6
6
|
## What it does
|
|
7
7
|
|
|
@@ -9,7 +9,6 @@ Send transactional email and run drip campaigns from mailboxes on your own regis
|
|
|
9
9
|
- Send transactional emails (one-shot or templated)
|
|
10
10
|
- Read inbox, outbox, sent history, and per-message status
|
|
11
11
|
- Generate HTML email templates with AI from a prompt
|
|
12
|
-
- Run paced drip campaigns against uploaded contact lists
|
|
13
12
|
- Manage IP/domain warmup for sender reputation
|
|
14
13
|
|
|
15
14
|
## Quickstart
|
|
@@ -36,7 +35,7 @@ export MYAPI_KEY=mak_...
|
|
|
36
35
|
Requires:
|
|
37
36
|
- An `api_key` from **myapihq**
|
|
38
37
|
- A registered domain via **mydomainapi**, assigned to your org
|
|
39
|
-
- Default `org_id` (for templates
|
|
38
|
+
- Default `org_id` (for templates) — set with `myapi account config set-org <id>`
|
|
40
39
|
|
|
41
40
|
## Documentation
|
|
42
41
|
|
|
@@ -115,6 +115,6 @@ myapi funnel form <funnel_id> --slug survey \
|
|
|
115
115
|
- 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
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
117
|
- Deleting a funnel purges all its edge pages immediately.
|
|
118
|
-
- `402`
|
|
118
|
+
- `402 INSUFFICIENT_FUNDS` → top up (`myapi billing topup`) or enable `myapi billing auto-recharge`.
|
|
119
119
|
|
|
120
120
|
Run `myapi funnel --help` or `myapi funnel <subcommand> --help` for full flag reference.
|
|
@@ -32,7 +32,7 @@ myapi llm draft --kind email --prompt "Friendly welcome, under 60 words"
|
|
|
32
32
|
export MYAPI_KEY=mak_...
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
Requires `api_key` and `org_id` from **myapihq**. Inference cost is debited from your MyAPI balance — top up via `myapi billing topup`.
|
|
35
|
+
Requires `api_key` and `org_id` from **myapihq**. Inference cost is debited from your MyAPI balance — top up via `myapi billing topup`, or keep it funded automatically with `myapi billing auto-recharge`.
|
|
36
36
|
|
|
37
37
|
## When to use
|
|
38
38
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myapihq/cli",
|
|
3
3
|
"license": "Apache-2.0",
|
|
4
|
-
"version": "2.0
|
|
4
|
+
"version": "2.2.0",
|
|
5
5
|
"description": "MyAPI command-line interface",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"files": [
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"lint:skills:strict": "node scripts/copy-skills.js && node scripts/lint-skills.js --strict"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@myapihq/sdk": "^2.0
|
|
34
|
+
"@myapihq/sdk": "^2.2.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^25.6.0",
|