@mailmodo/cli 0.0.26-beta.pr28.45 → 0.0.26-beta.pr29.44

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.
@@ -3,23 +3,24 @@ export default class Billing extends BaseCommand {
3
3
  static description: string;
4
4
  static examples: string[];
5
5
  static flags: {
6
- 'auto-charge-block-count': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
6
  cap: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
- checkout: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
- purchase: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
7
  status: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
8
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
9
  yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
10
  };
14
11
  run(): Promise<void>;
15
- private startCheckout;
12
+ /**
13
+ * Retrieves and displays the current billing status including card info,
14
+ * block usage, spending total, AI generation limits, and optionally
15
+ * opens Stripe Checkout if no card is on file.
16
+ */
16
17
  private showStatus;
17
- private formatAutoCharge;
18
- private formatCap;
19
- private formatCurrency;
20
- private formatPaymentMethod;
21
- private formatUsageBlock;
22
- private pluralize;
23
- private purchaseBlocks;
18
+ /**
19
+ * Updates the monthly block spending cap. This limits how many 10k-email
20
+ * blocks will be auto-charged per billing cycle.
21
+ *
22
+ * @param {number} cap - The maximum number of blocks per month.
23
+ * @param {boolean} jsonOutput - Whether to output JSON instead of formatted text.
24
+ */
24
25
  private setCap;
25
26
  }
@@ -4,29 +4,16 @@ import open from 'open';
4
4
  import { BaseCommand } from '../../lib/base-command.js';
5
5
  import { API_ENDPOINTS } from '../../lib/constants.js';
6
6
  export default class Billing extends BaseCommand {
7
- static description = 'View billing status, purchase blocks, set cap, or add a payment method';
7
+ static description = 'View billing status, manage payment, and set spending cap';
8
8
  static examples = [
9
9
  '<%= config.bin %> billing',
10
10
  '<%= config.bin %> billing --status',
11
11
  '<%= config.bin %> billing --cap 5',
12
- '<%= config.bin %> billing --cap 5 --auto-charge-block-count 2',
13
- '<%= config.bin %> billing --purchase 3',
14
- '<%= config.bin %> billing --checkout',
15
12
  ];
16
13
  static flags = {
17
14
  ...BaseCommand.baseFlags,
18
- 'auto-charge-block-count': Flags.integer({
19
- description: 'Blocks to auto-purchase when the quota runs low (use with --cap)',
20
- }),
21
15
  cap: Flags.integer({
22
- description: 'Set monthly sending cap in blocks',
23
- }),
24
- checkout: Flags.boolean({
25
- default: false,
26
- description: 'Open Stripe checkout to add or update a payment method',
27
- }),
28
- purchase: Flags.integer({
29
- description: 'Manually purchase this many 10,000-email blocks',
16
+ description: 'Set monthly block cap (max blocks to auto-charge)',
30
17
  }),
31
18
  status: Flags.boolean({
32
19
  default: false,
@@ -36,44 +23,17 @@ export default class Billing extends BaseCommand {
36
23
  async run() {
37
24
  const { flags } = await this.parse(Billing);
38
25
  await this.ensureAuth();
39
- const autoChargeBlockCount = flags['auto-charge-block-count'];
40
- if (autoChargeBlockCount !== undefined && flags.cap === undefined) {
41
- this.error(`Use ${chalk.cyan('--auto-charge-block-count')} together with ${chalk.cyan('--cap')}.`);
42
- }
43
- if (flags.checkout) {
44
- await this.startCheckout(flags.json);
45
- return;
46
- }
47
- if (flags.purchase !== undefined) {
48
- await this.purchaseBlocks(flags.purchase, flags.json);
49
- return;
50
- }
51
26
  if (flags.cap !== undefined) {
52
- await this.setCap(flags.cap, autoChargeBlockCount, flags.json);
27
+ await this.setCap(flags.cap, flags.json);
53
28
  return;
54
29
  }
55
30
  await this.showStatus(flags.json);
56
31
  }
57
- async startCheckout(jsonOutput) {
58
- const response = await this.withApiSpinner({ json: jsonOutput, text: ' Creating checkout session...' }, () => this.apiClient.post(API_ENDPOINTS.BILLING_CHECKOUT));
59
- if (!response.ok) {
60
- this.handleApiError(response);
61
- }
62
- const { checkoutUrl } = response.data;
63
- if (jsonOutput) {
64
- this.log(JSON.stringify({ checkoutUrl }, null, 2));
65
- return;
66
- }
67
- this.log(`\n ${chalk.bold('Stripe Checkout')} — add or update your payment method.`);
68
- this.log(` ${chalk.dim(checkoutUrl)}\n`);
69
- try {
70
- await open(checkoutUrl);
71
- this.log(` Opening in browser...\n`);
72
- }
73
- catch {
74
- this.log(` ${chalk.dim('Could not open browser. Visit the URL above manually.')}\n`);
75
- }
76
- }
32
+ /**
33
+ * Retrieves and displays the current billing status including card info,
34
+ * block usage, spending total, AI generation limits, and optionally
35
+ * opens Stripe Checkout if no card is on file.
36
+ */
77
37
  async showStatus(jsonOutput) {
78
38
  const response = await this.withApiSpinner({ json: jsonOutput, text: ' Loading billing status...' }, () => this.apiClient.get(API_ENDPOINTS.BILLING_STATUS));
79
39
  if (!response.ok) {
@@ -85,125 +45,55 @@ export default class Billing extends BaseCommand {
85
45
  return;
86
46
  }
87
47
  this.log('');
88
- this.log(` Tier: ${data.tier}`);
89
- this.log(` Payment: ${this.formatPaymentMethod(data)}`);
90
- this.log(` Auto-charge: ${this.formatAutoCharge(data)}`);
91
- this.log(` Monthly cap: ${this.formatCap(data.cap)}`);
92
- this.log(` Total spent: ${this.formatCurrency(data.totalSpent, data.spentCurrency)}`);
93
- if (data.activeBlocks.length === 0) {
94
- this.log(' Active blocks: none');
95
- this.log('');
96
- return;
48
+ if (data.cardOnFile) {
49
+ this.log(` Card: ${data.cardSummary || 'Card on file'}`);
97
50
  }
98
- this.log(' Active blocks:');
99
- for (const block of data.activeBlocks) {
100
- this.log(` - ${this.formatUsageBlock(block)}`);
51
+ else {
52
+ this.log(` Card: ${chalk.yellow('No card on file')}`);
101
53
  }
102
- this.log('');
103
- }
104
- formatAutoCharge(data) {
105
- if (!data.autoChargeEnabled) {
106
- return 'disabled';
54
+ if (data.freeExhausted) {
55
+ this.log(` Free tier: ${chalk.dim('exhausted')}`);
107
56
  }
108
- if (typeof data.autoChargeBlockCount !== 'number') {
109
- return 'enabled';
57
+ else {
58
+ this.log(` Free tier: active`);
110
59
  }
111
- return `enabled (${data.autoChargeBlockCount} ${this.pluralize('block', data.autoChargeBlockCount)})`;
112
- }
113
- formatCap(cap) {
114
- const parts = [];
115
- if (typeof cap.inBlocks === 'number') {
116
- parts.push(`${cap.inBlocks} ${this.pluralize('block', cap.inBlocks)}`);
60
+ this.log(` Current block: ${data.currentBlockUsed ?? 0} / 10,000 used`);
61
+ this.log(` Blocks used: ${data.blocksUsed ?? 0} ($${data.totalSpent || '0'} total)`);
62
+ this.log(` Monthly cap: ${data.cap ?? 'not set'} blocks${data.cap ? ` ($${data.cap * 19} max/month)` : ''}`);
63
+ if (data.aiRemaining) {
64
+ this.log(`\n AI generation:`);
65
+ this.log(` Init uses remaining: ${data.aiRemaining.inits}/5 this month`);
66
+ this.log(` Edit uses remaining: ${data.aiRemaining.edits}/50 this month`);
117
67
  }
118
- if (typeof cap.inEmails === 'number') {
119
- parts.push(`${cap.inEmails.toLocaleString()} emails`);
120
- }
121
- return parts.length > 0 ? parts.join(' / ') : 'not set';
122
- }
123
- formatCurrency(amount, currency) {
124
- const numericAmount = typeof amount === 'number' ? amount : Number.parseFloat(amount);
125
- const normalizedCurrency = currency.toUpperCase();
126
- if (Number.isFinite(numericAmount)) {
68
+ if (!data.cardOnFile && data.freeExhausted && data.checkoutUrl) {
69
+ this.log(`\n ${chalk.yellow('Free tier exhausted.')} Opening payment page...\n`);
127
70
  try {
128
- return new Intl.NumberFormat('en-US', {
129
- currency: normalizedCurrency,
130
- style: 'currency',
131
- }).format(numericAmount);
71
+ await open(data.checkoutUrl);
132
72
  }
133
73
  catch {
134
- return `${numericAmount.toFixed(2)} ${normalizedCurrency}`;
74
+ this.log(` Visit: ${chalk.cyan(data.checkoutUrl)}`);
135
75
  }
136
76
  }
137
- return `${String(amount)} ${normalizedCurrency}`;
138
- }
139
- formatPaymentMethod(data) {
140
- if (!data.hasPaymentMethod) {
141
- return 'No payment method on file';
142
- }
143
- const primaryMethod = data.paymentMethod[0];
144
- if (!primaryMethod) {
145
- return 'Payment method on file';
146
- }
147
- const brand = primaryMethod.brand || 'Card';
148
- return primaryMethod.last4
149
- ? `${brand} ending ${primaryMethod.last4}`
150
- : `${brand} on file`;
151
- }
152
- formatUsageBlock(block) {
153
- const allowance = block.blockAllowance ?? Math.max(block.blocksCount * block.blockSize, 0);
154
- const used = Math.max(block.emailsSent ?? 0, 0);
155
- const activationSuffix = block.activatedAt
156
- ? `, activated ${new Date(block.activatedAt).toLocaleDateString('en-US')}`
157
- : '';
158
- return `${block.type} block: ${used.toLocaleString()} / ${allowance.toLocaleString()} used (${block.status}${activationSuffix})`;
159
- }
160
- pluralize(word, count) {
161
- return count === 1 ? word : `${word}s`;
162
- }
163
- async purchaseBlocks(blocksCount, jsonOutput) {
164
- if (blocksCount < 1) {
165
- this.error('Purchase block count must be at least 1 block.');
166
- }
167
- const response = await this.withApiSpinner({ json: jsonOutput, text: ' Initiating block purchase...' }, () => this.apiClient.post(API_ENDPOINTS.BILLING_PURCHASE, {
168
- blocksCount,
169
- }));
170
- if (!response.ok) {
171
- this.handleApiError(response);
172
- }
173
- if (jsonOutput) {
174
- this.log(JSON.stringify(response.data, null, 2));
175
- return;
176
- }
177
- this.log(`\n ${chalk.green('✓')} ${response.data.message}`);
178
- this.log(` Blocks: ${blocksCount} ${this.pluralize('block', blocksCount)}`);
179
- this.log(` Payment intent:${` ${chalk.dim(response.data.paymentIntentId)}`}`);
180
77
  this.log('');
181
78
  }
182
- async setCap(cap, autoChargeBlockCount, jsonOutput) {
183
- if (cap < 1) {
184
- this.error('Cap must be at least 1 block.');
185
- }
186
- if (autoChargeBlockCount !== undefined && autoChargeBlockCount < 1) {
187
- this.error('Auto-charge block count must be at least 1 block.');
188
- }
189
- const response = await this.withApiSpinner({ json: jsonOutput, text: ' Updating spending cap...' }, () => {
190
- const payload = autoChargeBlockCount === undefined
191
- ? { cap }
192
- : { autoChargeBlockCount, cap };
193
- return this.apiClient.post(API_ENDPOINTS.BILLING_CAP, payload);
194
- });
79
+ /**
80
+ * Updates the monthly block spending cap. This limits how many 10k-email
81
+ * blocks will be auto-charged per billing cycle.
82
+ *
83
+ * @param {number} cap - The maximum number of blocks per month.
84
+ * @param {boolean} jsonOutput - Whether to output JSON instead of formatted text.
85
+ */
86
+ async setCap(cap, jsonOutput) {
87
+ const response = await this.withApiSpinner({ json: jsonOutput, text: ' Updating spending cap...' }, () => this.apiClient.patch(API_ENDPOINTS.BILLING_CAP, {
88
+ cap,
89
+ }));
195
90
  if (!response.ok) {
196
91
  this.handleApiError(response);
197
92
  }
198
93
  if (jsonOutput) {
199
- this.log(JSON.stringify(response.data, null, 2));
94
+ this.log(JSON.stringify({ cap, maxMonthlySpend: `$${cap * 19}`, status: 'updated' }, null, 2));
200
95
  return;
201
96
  }
202
- this.log(`\n ${chalk.green('✓')} ${response.data.message}`);
203
- this.log(` Monthly cap: ${chalk.bold(String(response.data.capBlocks))} ${this.pluralize('block', response.data.capBlocks)} (${response.data.capEmails.toLocaleString()} emails)`);
204
- if (response.data.autoChargeBlockCount !== undefined) {
205
- this.log(` Auto-charge: ${response.data.autoChargeBlockCount} ${this.pluralize('block', response.data.autoChargeBlockCount)}`);
206
- }
207
- this.log('');
97
+ this.log(`\n ${chalk.green('✓')} Cap set to ${chalk.bold(String(cap))} blocks ($${cap * 19} max/month).\n`);
208
98
  }
209
99
  }
@@ -2,7 +2,6 @@ import { Flags } from '@oclif/core';
2
2
  import { confirm } from '@inquirer/prompts';
3
3
  import chalk from 'chalk';
4
4
  import { writeFile } from 'node:fs/promises';
5
- import { resolve } from 'node:path';
6
5
  import { BaseCommand } from '../../lib/base-command.js';
7
6
  import { API_ENDPOINTS } from '../../lib/constants.js';
8
7
  export default class Contacts extends BaseCommand {
@@ -10,7 +9,7 @@ export default class Contacts extends BaseCommand {
10
9
  static examples = [
11
10
  '<%= config.bin %> contacts',
12
11
  '<%= config.bin %> contacts --search sarah@example.com',
13
- '<%= config.bin %> contacts --export # GDPR CSV → contacts.csv',
12
+ "<%= config.bin %> contacts --export # GDPR CSV → contacts.csv",
14
13
  '<%= config.bin %> contacts --delete sarah@example.com',
15
14
  ];
16
15
  static flags = {
@@ -113,14 +112,14 @@ export default class Contacts extends BaseCommand {
113
112
  this.log(`\n Export status: ${status ?? 'unknown'}. No download URL yet.\n`);
114
113
  return;
115
114
  }
116
- const fileResult = await this.apiClient.getPublicFile(downloadUrl.trim());
115
+ const url = `https://${downloadUrl}`;
116
+ const fileResult = await this.apiClient.getFile(url);
117
117
  if (!fileResult.ok) {
118
118
  this.error(`Download failed: ${fileResult.status} ${fileResult.error ?? ''}\n` +
119
119
  ` URL: ${fileResult.debug.fullUrl}`);
120
120
  }
121
- const outputPath = resolve('contacts.csv');
122
- await writeFile(outputPath, Buffer.from(fileResult.body));
123
- this.log(`\n ${chalk.green('✓')} Contact export saved to ${chalk.cyan(outputPath)}\n`);
121
+ await writeFile('contacts.csv', Buffer.from(fileResult.body));
122
+ this.log(`\n ${chalk.green('✓')} Contact export saved to ${chalk.cyan('contacts.csv')}\n`);
124
123
  }
125
124
  /**
126
125
  * Performs a GDPR-compliant hard delete of a contact and all their
@@ -23,6 +23,5 @@ export default class Domain extends BaseCommand {
23
23
  * bounce rate, and spam complaint rate.
24
24
  */
25
25
  private showDomainStatus;
26
- private collectDomainInputs;
27
26
  private recordLabel;
28
27
  }
@@ -45,22 +45,69 @@ export default class Domain extends BaseCommand {
45
45
  this.log(`\n ${'─'.repeat(53)}`);
46
46
  this.log(` ${chalk.bold('DOMAIN SETUP')}`);
47
47
  this.log(` ${'─'.repeat(53)}\n`);
48
- const { domain, senderEmail, address } = await this.collectDomainInputs(flags.yes, yamlConfig);
49
- const response = await this.withApiSpinner({ json: flags.json, text: ' Configuring domain...' }, () => this.apiClient.post(API_ENDPOINTS.DOMAIN, {
48
+ let domain;
49
+ let senderEmail;
50
+ let fromName;
51
+ let replyTo;
52
+ let address;
53
+ if (flags.yes) {
54
+ domain = yamlConfig.project?.domain || '';
55
+ senderEmail = yamlConfig.project?.fromEmail || '';
56
+ fromName = yamlConfig.project?.fromName || '';
57
+ replyTo = yamlConfig.project?.replyTo || '';
58
+ address = yamlConfig.project?.address || '';
59
+ if (!domain) {
60
+ this.error('Domain is required. Set it in mailmodo.yaml or use interactive mode.');
61
+ }
62
+ }
63
+ else {
64
+ domain = await input({
65
+ default: yamlConfig.project?.domain,
66
+ message: 'What domain will you send from?',
67
+ validate: (v) => (v?.trim() ? true : 'Domain is required'),
68
+ });
69
+ senderEmail = await input({
70
+ default: yamlConfig.project?.fromEmail,
71
+ message: 'Sender email address:',
72
+ validate: (v) => v?.includes('@') ? true : 'Please enter a valid email',
73
+ });
74
+ fromName = await input({
75
+ default: yamlConfig.project?.fromName || '',
76
+ message: 'Display name (optional, shown as sender name):',
77
+ });
78
+ replyTo = await input({
79
+ default: yamlConfig.project?.replyTo || '',
80
+ message: 'Reply-to address (optional, press Enter to use sender email):',
81
+ });
82
+ address = await input({
83
+ default: yamlConfig.project?.address,
84
+ message: 'Business address (required by law):',
85
+ validate: (v) => (v?.trim() ? true : 'Address is required'),
86
+ });
87
+ }
88
+ const apiPayload = {
50
89
  address,
51
90
  domain,
52
91
  fromEmail: senderEmail,
53
- }));
92
+ };
93
+ if (fromName)
94
+ apiPayload.fromName = fromName;
95
+ if (replyTo)
96
+ apiPayload.replyTo = replyTo;
97
+ const response = await this.withApiSpinner({ json: flags.json, text: ' Configuring domain...' }, () => this.apiClient.post(API_ENDPOINTS.DOMAIN, apiPayload));
54
98
  if (!response.ok) {
55
99
  this.handleApiError(response);
56
100
  }
57
101
  yamlConfig.project.domain = domain;
58
102
  yamlConfig.project.fromEmail = senderEmail;
59
103
  yamlConfig.project.address = address;
104
+ if (fromName)
105
+ yamlConfig.project.fromName = fromName;
106
+ if (replyTo)
107
+ yamlConfig.project.replyTo = replyTo;
60
108
  await saveYaml(yamlConfig);
61
109
  await saveConfig({ ...config, domain });
62
110
  const records = response.data?.dnsRecords || [];
63
- const guideUrl = response.data?.dnsGuideUrl ?? DNS_GUIDE_URL;
64
111
  if (flags.json) {
65
112
  this.log(JSON.stringify({ dnsRecords: records, domain }, null, 2));
66
113
  return;
@@ -73,7 +120,7 @@ export default class Domain extends BaseCommand {
73
120
  this.log(` Value: ${record.value}\n`);
74
121
  }
75
122
  this.log(` DNS changes take 5–30 minutes to propagate.`);
76
- this.log(` Full guide: ${chalk.cyan(guideUrl)}\n`);
123
+ this.log(` Full guide: ${chalk.cyan(DNS_GUIDE_URL)}\n`);
77
124
  if (!flags.yes) {
78
125
  const action = await input({
79
126
  default: '',
@@ -152,35 +199,6 @@ export default class Domain extends BaseCommand {
152
199
  this.log(` Bounce rate: ${data.bounceRate ?? 'N/A'}%`);
153
200
  this.log(` Spam rate: ${data.spamRate ?? 'N/A'}%\n`);
154
201
  }
155
- async collectDomainInputs(skipPrompts, yamlConfig) {
156
- if (skipPrompts) {
157
- const domain = yamlConfig.project?.domain || '';
158
- if (!domain) {
159
- this.error('Domain is required. Set it in mailmodo.yaml or use interactive mode.');
160
- }
161
- return {
162
- address: yamlConfig.project?.address || '',
163
- domain,
164
- senderEmail: yamlConfig.project?.fromEmail || '',
165
- };
166
- }
167
- const domain = await input({
168
- default: yamlConfig.project?.domain,
169
- message: 'What domain will you send from?',
170
- validate: (v) => (v?.trim() ? true : 'Domain is required'),
171
- });
172
- const senderEmail = await input({
173
- default: yamlConfig.project?.fromEmail,
174
- message: 'Sender email address:',
175
- validate: (v) => (v?.includes('@') ? true : 'Please enter a valid email'),
176
- });
177
- const address = await input({
178
- default: yamlConfig.project?.address,
179
- message: 'Business address (required by law):',
180
- validate: (v) => (v?.trim() ? true : 'Address is required'),
181
- });
182
- return { address, domain, senderEmail };
183
- }
184
202
  recordLabel(index) {
185
203
  const labels = ['DKIM', 'DMARC', 'Return Path'];
186
204
  return labels[index] || `Record ${index + 1}`;
@@ -44,10 +44,6 @@ export declare class ApiClient {
44
44
  * Bearer auth as other requests; does not parse JSON.
45
45
  */
46
46
  getFile(url: string): Promise<FileFetchResult>;
47
- /**
48
- * GET an external URL (e.g. blob storage) without auth headers.
49
- */
50
- getPublicFile(url: string): Promise<FileFetchResult>;
51
47
  patch<T = Record<string, unknown>>(path: string, body?: Record<string, unknown>): Promise<ApiResponse<T>>;
52
48
  post<T = Record<string, unknown>>(path: string, body?: Record<string, unknown> | unknown): Promise<ApiResponse<T>>;
53
49
  postFormData<T = Record<string, unknown>>(path: string, formData: FormData): Promise<ApiResponse<T>>;
@@ -1,5 +1,5 @@
1
1
  import { API_BASE_URL } from './constants.js';
2
- import { fetchFileNoAuth, fetchFileWithBearerAuth, } from './fetch-file.js';
2
+ import { fetchFileWithBearerAuth, } from './fetch-file.js';
3
3
  /**
4
4
  * HTTP client for the Mailmodo CLI API.
5
5
  * Wraps the native fetch API with Bearer token authentication,
@@ -123,12 +123,6 @@ export class ApiClient {
123
123
  async getFile(url) {
124
124
  return fetchFileWithBearerAuth(url, this.apiKey);
125
125
  }
126
- /**
127
- * GET an external URL (e.g. blob storage) without auth headers.
128
- */
129
- async getPublicFile(url) {
130
- return fetchFileNoAuth(url);
131
- }
132
126
  async patch(path, body) {
133
127
  return this.request('PATCH', path, body);
134
128
  }
@@ -5,8 +5,6 @@ export declare const API_ENDPOINTS: Readonly<{
5
5
  ASSETS_LOGO: "/assets/logo";
6
6
  AUTH_VALIDATE: "/auth/validate";
7
7
  BILLING_CAP: "/billing/cap";
8
- BILLING_CHECKOUT: "/billing/checkout";
9
- BILLING_PURCHASE: "/billing/purchase";
10
8
  BILLING_STATUS: "/billing/status";
11
9
  CONTACTS: "/contacts";
12
10
  CONTACTS_EXPORT: "/contacts/export";
@@ -11,8 +11,6 @@ export const API_ENDPOINTS = Object.freeze({
11
11
  ASSETS_LOGO: '/assets/logo',
12
12
  AUTH_VALIDATE: '/auth/validate',
13
13
  BILLING_CAP: '/billing/cap',
14
- BILLING_CHECKOUT: '/billing/checkout',
15
- BILLING_PURCHASE: '/billing/purchase',
16
14
  BILLING_STATUS: '/billing/status',
17
15
  CONTACTS: '/contacts',
18
16
  CONTACTS_EXPORT: '/contacts/export',
@@ -12,7 +12,3 @@ export interface FileFetchResult {
12
12
  * does not parse JSON.
13
13
  */
14
14
  export declare function fetchFileWithBearerAuth(url: string, apiKey: string): Promise<FileFetchResult>;
15
- /**
16
- * GET an absolute external URL (e.g. blob storage) without auth headers.
17
- */
18
- export declare function fetchFileNoAuth(url: string): Promise<FileFetchResult>;
@@ -1,8 +1,31 @@
1
1
  const USER_AGENT = '@mailmodo/cli';
2
- async function doFetch(href, headers) {
2
+ /**
3
+ * GET an absolute URL and return the raw body (e.g. CSV). Uses Bearer auth;
4
+ * does not parse JSON.
5
+ */
6
+ export async function fetchFileWithBearerAuth(url, apiKey) {
7
+ let href;
8
+ try {
9
+ href = new URL(url.trim()).toString();
10
+ }
11
+ catch {
12
+ return {
13
+ body: new ArrayBuffer(0),
14
+ debug: { fullUrl: url.trim() },
15
+ error: 'Invalid URL',
16
+ ok: false,
17
+ status: 0,
18
+ };
19
+ }
3
20
  const debug = { fullUrl: href };
4
21
  try {
5
- const response = await fetch(href, { headers, method: 'GET' });
22
+ const response = await fetch(href, {
23
+ headers: {
24
+ Authorization: `Bearer ${apiKey}`,
25
+ 'User-Agent': USER_AGENT,
26
+ },
27
+ method: 'GET',
28
+ });
6
29
  const body = await response.arrayBuffer();
7
30
  if (!response.ok) {
8
31
  return {
@@ -26,47 +49,3 @@ async function doFetch(href, headers) {
26
49
  };
27
50
  }
28
51
  }
29
- function parseUrl(url) {
30
- try {
31
- return new URL(url.trim()).toString();
32
- }
33
- catch {
34
- return null;
35
- }
36
- }
37
- /**
38
- * GET an absolute URL and return the raw body (e.g. CSV). Uses Bearer auth;
39
- * does not parse JSON.
40
- */
41
- export async function fetchFileWithBearerAuth(url, apiKey) {
42
- const href = parseUrl(url);
43
- if (!href) {
44
- return {
45
- body: new ArrayBuffer(0),
46
- debug: { fullUrl: url.trim() },
47
- error: 'Invalid URL',
48
- ok: false,
49
- status: 0,
50
- };
51
- }
52
- return doFetch(href, {
53
- Authorization: `Bearer ${apiKey}`,
54
- 'User-Agent': USER_AGENT,
55
- });
56
- }
57
- /**
58
- * GET an absolute external URL (e.g. blob storage) without auth headers.
59
- */
60
- export async function fetchFileNoAuth(url) {
61
- const href = parseUrl(url);
62
- if (!href) {
63
- return {
64
- body: new ArrayBuffer(0),
65
- debug: { fullUrl: url.trim() },
66
- error: 'Invalid URL',
67
- ok: false,
68
- status: 0,
69
- };
70
- }
71
- return doFetch(href, { 'User-Agent': USER_AGENT });
72
- }
@@ -3,14 +3,11 @@
3
3
  "billing": {
4
4
  "aliases": [],
5
5
  "args": {},
6
- "description": "View billing status, purchase blocks, set cap, or add a payment method",
6
+ "description": "View billing status, manage payment, and set spending cap",
7
7
  "examples": [
8
8
  "<%= config.bin %> billing",
9
9
  "<%= config.bin %> billing --status",
10
- "<%= config.bin %> billing --cap 5",
11
- "<%= config.bin %> billing --cap 5 --auto-charge-block-count 2",
12
- "<%= config.bin %> billing --purchase 3",
13
- "<%= config.bin %> billing --checkout"
10
+ "<%= config.bin %> billing --cap 5"
14
11
  ],
15
12
  "flags": {
16
13
  "json": {
@@ -26,33 +23,13 @@
26
23
  "allowNo": false,
27
24
  "type": "boolean"
28
25
  },
29
- "auto-charge-block-count": {
30
- "description": "Blocks to auto-purchase when the quota runs low (use with --cap)",
31
- "name": "auto-charge-block-count",
32
- "hasDynamicHelp": false,
33
- "multiple": false,
34
- "type": "option"
35
- },
36
26
  "cap": {
37
- "description": "Set monthly sending cap in blocks",
27
+ "description": "Set monthly block cap (max blocks to auto-charge)",
38
28
  "name": "cap",
39
29
  "hasDynamicHelp": false,
40
30
  "multiple": false,
41
31
  "type": "option"
42
32
  },
43
- "checkout": {
44
- "description": "Open Stripe checkout to add or update a payment method",
45
- "name": "checkout",
46
- "allowNo": false,
47
- "type": "boolean"
48
- },
49
- "purchase": {
50
- "description": "Manually purchase this many 10,000-email blocks",
51
- "name": "purchase",
52
- "hasDynamicHelp": false,
53
- "multiple": false,
54
- "type": "option"
55
- },
56
33
  "status": {
57
34
  "description": "Show billing status only",
58
35
  "name": "status",
@@ -570,14 +547,13 @@
570
547
  "index.js"
571
548
  ]
572
549
  },
573
- "settings": {
550
+ "status": {
574
551
  "aliases": [],
575
552
  "args": {},
576
- "description": "View and update project settings",
553
+ "description": "View email performance metrics and quota usage",
577
554
  "examples": [
578
- "<%= config.bin %> settings",
579
- "<%= config.bin %> settings --set brand_color=#0F3460",
580
- "<%= config.bin %> settings --json"
555
+ "<%= config.bin %> status",
556
+ "<%= config.bin %> status --json"
581
557
  ],
582
558
  "flags": {
583
559
  "json": {
@@ -592,18 +568,11 @@
592
568
  "name": "yes",
593
569
  "allowNo": false,
594
570
  "type": "boolean"
595
- },
596
- "set": {
597
- "description": "Set a setting (format: key=value)",
598
- "name": "set",
599
- "hasDynamicHelp": false,
600
- "multiple": false,
601
- "type": "option"
602
571
  }
603
572
  },
604
573
  "hasDynamicHelp": false,
605
574
  "hiddenAliases": [],
606
- "id": "settings",
575
+ "id": "status",
607
576
  "pluginAlias": "@mailmodo/cli",
608
577
  "pluginName": "@mailmodo/cli",
609
578
  "pluginType": "core",
@@ -613,17 +582,18 @@
613
582
  "relativePath": [
614
583
  "dist",
615
584
  "commands",
616
- "settings",
585
+ "status",
617
586
  "index.js"
618
587
  ]
619
588
  },
620
- "status": {
589
+ "settings": {
621
590
  "aliases": [],
622
591
  "args": {},
623
- "description": "View email performance metrics and quota usage",
592
+ "description": "View and update project settings",
624
593
  "examples": [
625
- "<%= config.bin %> status",
626
- "<%= config.bin %> status --json"
594
+ "<%= config.bin %> settings",
595
+ "<%= config.bin %> settings --set brand_color=#0F3460",
596
+ "<%= config.bin %> settings --json"
627
597
  ],
628
598
  "flags": {
629
599
  "json": {
@@ -638,11 +608,18 @@
638
608
  "name": "yes",
639
609
  "allowNo": false,
640
610
  "type": "boolean"
611
+ },
612
+ "set": {
613
+ "description": "Set a setting (format: key=value)",
614
+ "name": "set",
615
+ "hasDynamicHelp": false,
616
+ "multiple": false,
617
+ "type": "option"
641
618
  }
642
619
  },
643
620
  "hasDynamicHelp": false,
644
621
  "hiddenAliases": [],
645
- "id": "status",
622
+ "id": "settings",
646
623
  "pluginAlias": "@mailmodo/cli",
647
624
  "pluginName": "@mailmodo/cli",
648
625
  "pluginType": "core",
@@ -652,10 +629,10 @@
652
629
  "relativePath": [
653
630
  "dist",
654
631
  "commands",
655
- "status",
632
+ "settings",
656
633
  "index.js"
657
634
  ]
658
635
  }
659
636
  },
660
- "version": "0.0.26-beta.pr28.45"
637
+ "version": "0.0.26-beta.pr29.44"
661
638
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mailmodo/cli",
3
3
  "description": "Email lifecycle automation for the AI-native builder generation.",
4
- "version": "0.0.26-beta.pr28.45",
4
+ "version": "0.0.26-beta.pr29.44",
5
5
  "author": "provishalk",
6
6
  "bin": {
7
7
  "mailmodo": "bin/run.js"