@myapihq/cli 2.0.1 → 2.1.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/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 +13 -3
- package/dist/skills/my-auth-api/SKILL.md +1 -1
- 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
|
@@ -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
|
|
|
@@ -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 -->
|
|
@@ -109,4 +109,4 @@ myapi auth client list
|
|
|
109
109
|
be retrieved later — store it immediately. SPA clients have no secret.
|
|
110
110
|
- Redirect URIs are matched exactly: absolute `https://…` (or
|
|
111
111
|
`http://localhost…` for local dev).
|
|
112
|
-
- `402`
|
|
112
|
+
- `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.1.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.1.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^25.6.0",
|