minara 0.1.4 → 0.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/LICENSE +21 -0
- package/README.md +172 -65
- package/dist/api/auth.d.ts +5 -1
- package/dist/api/auth.js +11 -0
- package/dist/api/payment.d.ts +17 -0
- package/dist/api/payment.js +39 -0
- package/dist/api/perps.d.ts +2 -0
- package/dist/api/perps.js +4 -0
- package/dist/api/tokens.d.ts +4 -0
- package/dist/api/tokens.js +8 -0
- package/dist/commands/assets.js +89 -8
- package/dist/commands/chat.js +153 -62
- package/dist/commands/config.js +82 -5
- package/dist/commands/copy-trade.js +10 -4
- package/dist/commands/deposit.js +5 -1
- package/dist/commands/discover.js +31 -4
- package/dist/commands/limit-order.js +16 -8
- package/dist/commands/login.js +88 -5
- package/dist/commands/perps.js +48 -13
- package/dist/commands/premium.d.ts +2 -0
- package/dist/commands/premium.js +398 -0
- package/dist/commands/swap.js +29 -13
- package/dist/commands/transfer.js +17 -11
- package/dist/commands/withdraw.js +17 -11
- package/dist/config.d.ts +2 -0
- package/dist/config.js +1 -0
- package/dist/formatters.d.ts +56 -0
- package/dist/formatters.js +376 -0
- package/dist/index.js +11 -1
- package/dist/touchid.d.ts +18 -0
- package/dist/touchid.js +181 -0
- package/dist/types.d.ts +67 -6
- package/dist/utils.d.ts +34 -0
- package/dist/utils.js +107 -1
- package/package.json +1 -1
package/dist/commands/login.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { input, select, confirm } from '@inquirer/prompts';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
-
import { sendEmailCode, verifyEmailCode, getOAuthUrl, getCurrentUser } from '../api/auth.js';
|
|
5
|
-
import { saveCredentials, loadCredentials } from '../config.js';
|
|
4
|
+
import { sendEmailCode, verifyEmailCode, getOAuthUrl, getCurrentUser, startDeviceAuth, getDeviceAuthStatus, } from '../api/auth.js';
|
|
5
|
+
import { saveCredentials, loadCredentials, loadConfig, saveConfig } from '../config.js';
|
|
6
6
|
import { success, error, info, warn, spinner, openBrowser, unwrapApi, wrapAction } from '../utils.js';
|
|
7
7
|
import { startOAuthServer } from '../oauth-server.js';
|
|
8
8
|
import { OAUTH_PROVIDERS } from '../types.js';
|
|
9
|
+
import { isTouchIdAvailable } from '../touchid.js';
|
|
9
10
|
// ─── Email login flow ─────────────────────────────────────────────────────
|
|
10
11
|
async function loginWithEmail(emailOpt) {
|
|
11
12
|
const email = emailOpt ?? await input({
|
|
@@ -107,12 +108,72 @@ async function loginWithOAuth(provider) {
|
|
|
107
108
|
if (result.email)
|
|
108
109
|
console.log(chalk.dim(` ${result.email}`));
|
|
109
110
|
}
|
|
111
|
+
// ─── Device login flow (RFC 8628) ─────────────────────────────────────────
|
|
112
|
+
async function loginWithDevice() {
|
|
113
|
+
info('Starting device login...');
|
|
114
|
+
const spin = spinner('Requesting device code...');
|
|
115
|
+
const startRes = await startDeviceAuth();
|
|
116
|
+
spin.stop();
|
|
117
|
+
if (!startRes.success || !startRes.data) {
|
|
118
|
+
error(startRes.error?.message ?? 'Failed to start device login');
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
const { device_code, user_code, verification_url, expires_in, interval } = startRes.data;
|
|
122
|
+
console.log('');
|
|
123
|
+
console.log(chalk.bold('To complete login:'));
|
|
124
|
+
console.log('');
|
|
125
|
+
console.log(` 1. Visit: ${chalk.cyan(verification_url)}`);
|
|
126
|
+
console.log(` 2. Enter code: ${chalk.bold.yellow(user_code)}`);
|
|
127
|
+
console.log('');
|
|
128
|
+
info(`Waiting for authentication (expires in ${Math.floor(expires_in / 60)} minutes)...`);
|
|
129
|
+
info(chalk.dim('(Press Ctrl+C to cancel)'));
|
|
130
|
+
console.log('');
|
|
131
|
+
// Try to open browser
|
|
132
|
+
openBrowser(`${verification_url}?user_code=${user_code}`);
|
|
133
|
+
// Poll for completion
|
|
134
|
+
const startTime = Date.now();
|
|
135
|
+
const expiresAt = startTime + expires_in * 1000;
|
|
136
|
+
let pollInterval = interval * 1000;
|
|
137
|
+
while (Date.now() < expiresAt) {
|
|
138
|
+
await new Promise((r) => setTimeout(r, pollInterval));
|
|
139
|
+
const statusRes = await getDeviceAuthStatus(device_code);
|
|
140
|
+
if (!statusRes.success || !statusRes.data) {
|
|
141
|
+
// Network error, keep polling
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const data = statusRes.data;
|
|
145
|
+
const { status, access_token, user } = data;
|
|
146
|
+
if (status === 'expired') {
|
|
147
|
+
error('Device login expired. Please try again.');
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
if (status === 'completed' && access_token && user) {
|
|
151
|
+
saveCredentials({
|
|
152
|
+
accessToken: access_token,
|
|
153
|
+
userId: user.id,
|
|
154
|
+
email: user.email,
|
|
155
|
+
displayName: user.displayName,
|
|
156
|
+
});
|
|
157
|
+
success('Login successful! Credentials saved to ~/.minara/');
|
|
158
|
+
if (user.displayName)
|
|
159
|
+
console.log(chalk.dim(` Welcome, ${user.displayName}`));
|
|
160
|
+
if (user.email)
|
|
161
|
+
console.log(chalk.dim(` ${user.email}`));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
// Still pending, show progress
|
|
165
|
+
process.stdout.write('.');
|
|
166
|
+
}
|
|
167
|
+
error('Device login timed out. Please try again.');
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
110
170
|
// ─── Command ──────────────────────────────────────────────────────────────
|
|
111
171
|
export const loginCommand = new Command('login')
|
|
112
172
|
.description('Login to your Minara account')
|
|
113
173
|
.option('-e, --email <email>', 'Login with email verification code')
|
|
114
174
|
.option('--google', 'Login with Google')
|
|
115
175
|
.option('--apple', 'Login with Apple ID')
|
|
176
|
+
.option('--device', 'Login with device code (for headless environments)')
|
|
116
177
|
.action(wrapAction(async (opts) => {
|
|
117
178
|
// Warn if already logged in
|
|
118
179
|
const existing = loadCredentials();
|
|
@@ -135,13 +196,17 @@ export const loginCommand = new Command('login')
|
|
|
135
196
|
else if (opts.apple) {
|
|
136
197
|
method = 'apple';
|
|
137
198
|
}
|
|
199
|
+
else if (opts.device) {
|
|
200
|
+
method = 'device';
|
|
201
|
+
}
|
|
138
202
|
else {
|
|
139
203
|
method = await select({
|
|
140
204
|
message: 'How would you like to login?',
|
|
141
205
|
choices: [
|
|
142
|
-
{ name: '
|
|
143
|
-
{ name: '
|
|
144
|
-
{ name: '
|
|
206
|
+
{ name: 'Email verification code', value: 'email' },
|
|
207
|
+
{ name: 'Google', value: 'google' },
|
|
208
|
+
{ name: 'Apple ID', value: 'apple' },
|
|
209
|
+
{ name: 'Device code (for headless environments)', value: 'device' },
|
|
145
210
|
],
|
|
146
211
|
});
|
|
147
212
|
}
|
|
@@ -149,7 +214,25 @@ export const loginCommand = new Command('login')
|
|
|
149
214
|
if (method === 'email') {
|
|
150
215
|
await loginWithEmail(opts.email);
|
|
151
216
|
}
|
|
217
|
+
else if (method === 'device') {
|
|
218
|
+
await loginWithDevice();
|
|
219
|
+
}
|
|
152
220
|
else {
|
|
153
221
|
await loginWithOAuth(method);
|
|
154
222
|
}
|
|
223
|
+
// ── Offer Touch ID setup (macOS only, if not already enabled) ────
|
|
224
|
+
const config = loadConfig();
|
|
225
|
+
if (!config.touchId && isTouchIdAvailable()) {
|
|
226
|
+
console.log('');
|
|
227
|
+
const enableTouchId = await confirm({
|
|
228
|
+
message: 'Enable Touch ID to protect fund operations (transfer, withdraw, swap, etc.)?',
|
|
229
|
+
default: true,
|
|
230
|
+
});
|
|
231
|
+
if (enableTouchId) {
|
|
232
|
+
saveConfig({ touchId: true });
|
|
233
|
+
success('Touch ID protection enabled!');
|
|
234
|
+
console.log(chalk.dim(' All fund-related operations now require fingerprint verification.'));
|
|
235
|
+
console.log(chalk.dim(` To disable, run: ${chalk.cyan('minara config')} → Touch ID`));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
155
238
|
}));
|
package/dist/commands/perps.js
CHANGED
|
@@ -3,7 +3,9 @@ import { input, select, confirm, number as numberPrompt } from '@inquirer/prompt
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import * as perpsApi from '../api/perps.js';
|
|
5
5
|
import { requireAuth } from '../config.js';
|
|
6
|
-
import { success, info, warn, spinner, assertApiOk, wrapAction } from '../utils.js';
|
|
6
|
+
import { success, info, warn, spinner, assertApiOk, formatOrderSide, wrapAction, requireTransactionConfirmation } from '../utils.js';
|
|
7
|
+
import { requireTouchId } from '../touchid.js';
|
|
8
|
+
import { printTxResult, printTable, POSITION_COLUMNS } from '../formatters.js';
|
|
7
9
|
// ─── deposit ─────────────────────────────────────────────────────────────
|
|
8
10
|
const depositCmd = new Command('deposit')
|
|
9
11
|
.description('Deposit USDC into Hyperliquid perps (min 5 USDC)')
|
|
@@ -24,13 +26,14 @@ const depositCmd = new Command('deposit')
|
|
|
24
26
|
if (!ok)
|
|
25
27
|
return;
|
|
26
28
|
}
|
|
29
|
+
await requireTransactionConfirmation(`Deposit ${amount} USDC → Perps`);
|
|
30
|
+
await requireTouchId();
|
|
27
31
|
const spin = spinner('Depositing…');
|
|
28
32
|
const res = await perpsApi.deposit(creds.accessToken, { usdcAmount: amount });
|
|
29
33
|
spin.stop();
|
|
30
34
|
assertApiOk(res, 'Deposit failed');
|
|
31
35
|
success(`Deposited ${amount} USDC`);
|
|
32
|
-
|
|
33
|
-
console.log(JSON.stringify(res.data, null, 2));
|
|
36
|
+
printTxResult(res.data);
|
|
34
37
|
}));
|
|
35
38
|
// ─── withdraw ────────────────────────────────────────────────────────────
|
|
36
39
|
const withdrawCmd = new Command('withdraw')
|
|
@@ -54,13 +57,14 @@ const withdrawCmd = new Command('withdraw')
|
|
|
54
57
|
if (!ok)
|
|
55
58
|
return;
|
|
56
59
|
}
|
|
60
|
+
await requireTransactionConfirmation(`Withdraw ${amount} USDC → ${toAddress}`);
|
|
61
|
+
await requireTouchId();
|
|
57
62
|
const spin = spinner('Withdrawing…');
|
|
58
63
|
const res = await perpsApi.withdraw(creds.accessToken, { usdcAmount: amount, toAddress });
|
|
59
64
|
spin.stop();
|
|
60
65
|
assertApiOk(res, 'Withdrawal failed');
|
|
61
66
|
success('Withdrawal submitted');
|
|
62
|
-
|
|
63
|
-
console.log(JSON.stringify(res.data, null, 2));
|
|
67
|
+
printTxResult(res.data);
|
|
64
68
|
}));
|
|
65
69
|
// ─── positions ───────────────────────────────────────────────────────────
|
|
66
70
|
const positionsCmd = new Command('positions')
|
|
@@ -72,7 +76,16 @@ const positionsCmd = new Command('positions')
|
|
|
72
76
|
const res = await perpsApi.getPositions(creds.accessToken);
|
|
73
77
|
spin.stop();
|
|
74
78
|
assertApiOk(res, 'Failed to fetch positions');
|
|
75
|
-
|
|
79
|
+
const positions = res.data;
|
|
80
|
+
if (!positions || (Array.isArray(positions) && positions.length === 0)) {
|
|
81
|
+
console.log(chalk.dim('No open positions.'));
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
console.log('');
|
|
85
|
+
console.log(chalk.bold('Open Positions:'));
|
|
86
|
+
printTable(positions, POSITION_COLUMNS);
|
|
87
|
+
console.log('');
|
|
88
|
+
}
|
|
76
89
|
}));
|
|
77
90
|
// ─── order ───────────────────────────────────────────────────────────────
|
|
78
91
|
const orderCmd = new Command('order')
|
|
@@ -132,7 +145,13 @@ const orderCmd = new Command('order')
|
|
|
132
145
|
};
|
|
133
146
|
console.log('');
|
|
134
147
|
console.log(chalk.bold('Order Preview:'));
|
|
135
|
-
console.log(
|
|
148
|
+
console.log(` Asset : ${chalk.bold(order.a)}`);
|
|
149
|
+
console.log(` Side : ${formatOrderSide(order.b ? 'buy' : 'sell')}`);
|
|
150
|
+
console.log(` Price : ${chalk.yellow(order.p)}`);
|
|
151
|
+
console.log(` Size : ${chalk.bold(order.s)}`);
|
|
152
|
+
console.log(` Reduce Only : ${order.r ? chalk.yellow('Yes') : 'No'}`);
|
|
153
|
+
console.log(` Type : ${'limit' in order.t ? 'Limit (GTC)' : 'Trigger'}`);
|
|
154
|
+
console.log(` Grouping : ${grouping}`);
|
|
136
155
|
console.log('');
|
|
137
156
|
if (!opts.yes) {
|
|
138
157
|
const ok = await confirm({ message: 'Submit order?', default: false });
|
|
@@ -141,13 +160,14 @@ const orderCmd = new Command('order')
|
|
|
141
160
|
return;
|
|
142
161
|
}
|
|
143
162
|
}
|
|
163
|
+
await requireTransactionConfirmation(`Perps ${order.b ? 'LONG' : 'SHORT'} ${order.a} · size ${order.s} @ ${order.p}`);
|
|
164
|
+
await requireTouchId();
|
|
144
165
|
const spin = spinner('Placing order…');
|
|
145
166
|
const res = await perpsApi.placeOrders(creds.accessToken, { orders: [order], grouping });
|
|
146
167
|
spin.stop();
|
|
147
168
|
assertApiOk(res, 'Order placement failed');
|
|
148
169
|
success('Order submitted!');
|
|
149
|
-
|
|
150
|
-
console.log(JSON.stringify(res.data, null, 2));
|
|
170
|
+
printTxResult(res.data);
|
|
151
171
|
}));
|
|
152
172
|
// ─── cancel ──────────────────────────────────────────────────────────────
|
|
153
173
|
const cancelCmd = new Command('cancel')
|
|
@@ -173,8 +193,7 @@ const cancelCmd = new Command('cancel')
|
|
|
173
193
|
spin.stop();
|
|
174
194
|
assertApiOk(res, 'Order cancellation failed');
|
|
175
195
|
success('Order cancelled');
|
|
176
|
-
|
|
177
|
-
console.log(JSON.stringify(res.data, null, 2));
|
|
196
|
+
printTxResult(res.data);
|
|
178
197
|
}));
|
|
179
198
|
// ─── leverage ────────────────────────────────────────────────────────────
|
|
180
199
|
const leverageCmd = new Command('leverage')
|
|
@@ -205,7 +224,15 @@ const tradesCmd = new Command('trades')
|
|
|
205
224
|
const res = await perpsApi.getCompletedTrades(creds.accessToken);
|
|
206
225
|
spin.stop();
|
|
207
226
|
assertApiOk(res, 'Failed to fetch trades');
|
|
208
|
-
console.log(
|
|
227
|
+
console.log('');
|
|
228
|
+
console.log(chalk.bold('Completed Trades:'));
|
|
229
|
+
if (Array.isArray(res.data) && res.data.length > 0) {
|
|
230
|
+
printTable(res.data);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
console.log(chalk.dim(' No completed trades.'));
|
|
234
|
+
}
|
|
235
|
+
console.log('');
|
|
209
236
|
}));
|
|
210
237
|
// ─── fund-records ────────────────────────────────────────────────────────
|
|
211
238
|
const fundRecordsCmd = new Command('fund-records')
|
|
@@ -218,7 +245,15 @@ const fundRecordsCmd = new Command('fund-records')
|
|
|
218
245
|
const res = await perpsApi.getFundRecords(creds.accessToken, parseInt(opts.page, 10), parseInt(opts.limit, 10));
|
|
219
246
|
spin.stop();
|
|
220
247
|
assertApiOk(res, 'Failed to fetch fund records');
|
|
221
|
-
console.log(
|
|
248
|
+
console.log('');
|
|
249
|
+
console.log(chalk.bold('Fund Records:'));
|
|
250
|
+
if (Array.isArray(res.data) && res.data.length > 0) {
|
|
251
|
+
printTable(res.data);
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
console.log(chalk.dim(' No fund records.'));
|
|
255
|
+
}
|
|
256
|
+
console.log('');
|
|
222
257
|
}));
|
|
223
258
|
// ═════════════════════════════════════════════════════════════════════════
|
|
224
259
|
// Parent
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { select, confirm } from '@inquirer/prompts';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import * as paymentApi from '../api/payment.js';
|
|
5
|
+
import { getCurrentUser } from '../api/auth.js';
|
|
6
|
+
import { requireAuth } from '../config.js';
|
|
7
|
+
import { success, info, warn, spinner, assertApiOk, wrapAction } from '../utils.js';
|
|
8
|
+
import { openBrowser } from '../utils.js';
|
|
9
|
+
import { printKV, isRawJson } from '../formatters.js';
|
|
10
|
+
// ─── helpers ────────────────────────────────────────────────────────────
|
|
11
|
+
function formatPrice(plan) {
|
|
12
|
+
if (plan.price === 0)
|
|
13
|
+
return chalk.green('Free');
|
|
14
|
+
const price = `$${plan.price}`;
|
|
15
|
+
const period = plan.interval === 'month' ? '/mo' : '/yr';
|
|
16
|
+
return chalk.bold(price) + chalk.dim(period);
|
|
17
|
+
}
|
|
18
|
+
function formatCredits(rules) {
|
|
19
|
+
const credits = rules.limitCredit ? Number(rules.limitCredit).toLocaleString() : '—';
|
|
20
|
+
return credits;
|
|
21
|
+
}
|
|
22
|
+
/** Group plans by tier name and display monthly + yearly side-by-side */
|
|
23
|
+
function groupPlansByTier(plans) {
|
|
24
|
+
const tiers = new Map();
|
|
25
|
+
for (const p of plans) {
|
|
26
|
+
if (p.status !== 'active')
|
|
27
|
+
continue;
|
|
28
|
+
const existing = tiers.get(p.name) ?? {};
|
|
29
|
+
if (p.interval === 'month')
|
|
30
|
+
existing.monthly = p;
|
|
31
|
+
else
|
|
32
|
+
existing.yearly = p;
|
|
33
|
+
tiers.set(p.name, existing);
|
|
34
|
+
}
|
|
35
|
+
return tiers;
|
|
36
|
+
}
|
|
37
|
+
// ─── plans ──────────────────────────────────────────────────────────────
|
|
38
|
+
const plansCmd = new Command('plans')
|
|
39
|
+
.description('View all available subscription plans')
|
|
40
|
+
.action(wrapAction(async () => {
|
|
41
|
+
const spin = spinner('Fetching plans…');
|
|
42
|
+
const res = await paymentApi.getPlans();
|
|
43
|
+
spin.stop();
|
|
44
|
+
assertApiOk(res, 'Failed to fetch plans');
|
|
45
|
+
const { plans, packages } = res.data;
|
|
46
|
+
if (isRawJson()) {
|
|
47
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// ── Subscription Plans ──────────────────────────────────────────
|
|
51
|
+
console.log('');
|
|
52
|
+
console.log(chalk.bold('Subscription Plans:'));
|
|
53
|
+
console.log('');
|
|
54
|
+
const tiers = groupPlansByTier(plans);
|
|
55
|
+
// Table header
|
|
56
|
+
const header = [
|
|
57
|
+
chalk.white.bold('Plan'),
|
|
58
|
+
chalk.white.bold('Monthly'),
|
|
59
|
+
chalk.white.bold('Yearly'),
|
|
60
|
+
chalk.white.bold('Credits'),
|
|
61
|
+
chalk.white.bold('Workflows'),
|
|
62
|
+
chalk.white.bold('Invites'),
|
|
63
|
+
];
|
|
64
|
+
const rows = [];
|
|
65
|
+
for (const [tierName, tier] of tiers) {
|
|
66
|
+
const ref = tier.monthly ?? tier.yearly;
|
|
67
|
+
const monthly = tier.monthly ? formatPrice(tier.monthly) : chalk.dim('—');
|
|
68
|
+
const yearly = tier.yearly ? formatPrice(tier.yearly) : chalk.dim('—');
|
|
69
|
+
const savings = tier.monthly && tier.yearly
|
|
70
|
+
? chalk.green(` (save ${Math.round((1 - tier.yearly.price / (tier.monthly.price * 12)) * 100)}%)`)
|
|
71
|
+
: '';
|
|
72
|
+
rows.push([
|
|
73
|
+
chalk.bold(tierName),
|
|
74
|
+
monthly,
|
|
75
|
+
yearly + savings,
|
|
76
|
+
formatCredits(ref.rules),
|
|
77
|
+
String(ref.rules.limitWorkflows ?? 0),
|
|
78
|
+
String(ref.inviteCount ?? 0),
|
|
79
|
+
]);
|
|
80
|
+
}
|
|
81
|
+
// Print using cli-table3 directly for custom layout
|
|
82
|
+
const Table = (await import('cli-table3')).default;
|
|
83
|
+
const table = new Table({
|
|
84
|
+
head: header,
|
|
85
|
+
style: { head: [], border: ['dim'] },
|
|
86
|
+
});
|
|
87
|
+
for (const row of rows)
|
|
88
|
+
table.push(row);
|
|
89
|
+
console.log(table.toString());
|
|
90
|
+
// ── Credit Packages ─────────────────────────────────────────────
|
|
91
|
+
if (packages.length > 0) {
|
|
92
|
+
console.log('');
|
|
93
|
+
console.log(chalk.bold('Credit Packages (one-time):'));
|
|
94
|
+
console.log('');
|
|
95
|
+
const pkgTable = new Table({
|
|
96
|
+
head: [
|
|
97
|
+
chalk.white.bold('Price'),
|
|
98
|
+
chalk.white.bold('Credits'),
|
|
99
|
+
],
|
|
100
|
+
style: { head: [], border: ['dim'] },
|
|
101
|
+
});
|
|
102
|
+
for (const pkg of packages) {
|
|
103
|
+
pkgTable.push([
|
|
104
|
+
chalk.bold(`$${pkg.amount}`),
|
|
105
|
+
Number(pkg.credit).toLocaleString(),
|
|
106
|
+
]);
|
|
107
|
+
}
|
|
108
|
+
console.log(pkgTable.toString());
|
|
109
|
+
}
|
|
110
|
+
console.log('');
|
|
111
|
+
console.log(chalk.dim(' Subscribe: ') + chalk.cyan('minara premium subscribe'));
|
|
112
|
+
console.log('');
|
|
113
|
+
}));
|
|
114
|
+
// ─── status ─────────────────────────────────────────────────────────────
|
|
115
|
+
const statusCmd = new Command('status')
|
|
116
|
+
.description('View your current subscription status')
|
|
117
|
+
.action(wrapAction(async () => {
|
|
118
|
+
const creds = requireAuth();
|
|
119
|
+
const spin = spinner('Fetching subscription status…');
|
|
120
|
+
const [userRes, plansRes] = await Promise.all([
|
|
121
|
+
getCurrentUser(creds.accessToken),
|
|
122
|
+
paymentApi.getPlans(),
|
|
123
|
+
]);
|
|
124
|
+
spin.stop();
|
|
125
|
+
assertApiOk(userRes, 'Failed to fetch account info');
|
|
126
|
+
const user = userRes.data;
|
|
127
|
+
if (isRawJson()) {
|
|
128
|
+
console.log(JSON.stringify({
|
|
129
|
+
subscription: user.subscription ?? null,
|
|
130
|
+
plan: user.plan ?? null,
|
|
131
|
+
}, null, 2));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
console.log('');
|
|
135
|
+
console.log(chalk.bold('Subscription Status:'));
|
|
136
|
+
console.log('');
|
|
137
|
+
// Try to extract subscription info from various possible response shapes
|
|
138
|
+
const sub = user.subscription;
|
|
139
|
+
const userPlan = user.plan;
|
|
140
|
+
if (sub && Object.keys(sub).length > 0) {
|
|
141
|
+
const planName = sub.planName ?? sub.plan ?? sub.name ?? '—';
|
|
142
|
+
const status = sub.status ?? '—';
|
|
143
|
+
const interval = sub.interval ?? '—';
|
|
144
|
+
const cancelAt = sub.cancelAtPeriodEnd;
|
|
145
|
+
const periodEnd = sub.currentPeriodEnd;
|
|
146
|
+
console.log(` ${chalk.dim('Plan'.padEnd(16))} : ${chalk.bold(String(planName))}`);
|
|
147
|
+
console.log(` ${chalk.dim('Status'.padEnd(16))} : ${status === 'active' ? chalk.green('Active') : chalk.yellow(String(status))}`);
|
|
148
|
+
console.log(` ${chalk.dim('Billing'.padEnd(16))} : ${String(interval)}`);
|
|
149
|
+
if (periodEnd) {
|
|
150
|
+
console.log(` ${chalk.dim('Renews On'.padEnd(16))} : ${new Date(periodEnd).toLocaleDateString()}`);
|
|
151
|
+
}
|
|
152
|
+
if (cancelAt) {
|
|
153
|
+
console.log(` ${chalk.dim(''.padEnd(16))} ${chalk.yellow('⚠ Will cancel at end of billing period')}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else if (userPlan && Object.keys(userPlan).length > 0) {
|
|
157
|
+
printKV(userPlan);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
// No subscription info found — assume free plan
|
|
161
|
+
console.log(` ${chalk.dim('Plan'.padEnd(16))} : ${chalk.bold('Free')}`);
|
|
162
|
+
console.log(` ${chalk.dim('Status'.padEnd(16))} : ${chalk.green('Active')}`);
|
|
163
|
+
console.log('');
|
|
164
|
+
console.log(chalk.dim(' Upgrade with: ') + chalk.cyan('minara premium subscribe'));
|
|
165
|
+
}
|
|
166
|
+
console.log('');
|
|
167
|
+
}));
|
|
168
|
+
// ─── subscribe ──────────────────────────────────────────────────────────
|
|
169
|
+
const subscribeCmd = new Command('subscribe')
|
|
170
|
+
.description('Subscribe to a plan or change your plan (upgrade / downgrade)')
|
|
171
|
+
.action(wrapAction(async () => {
|
|
172
|
+
const creds = requireAuth();
|
|
173
|
+
// 1. Fetch plans
|
|
174
|
+
const spin = spinner('Fetching plans…');
|
|
175
|
+
const plansRes = await paymentApi.getPlans();
|
|
176
|
+
spin.stop();
|
|
177
|
+
assertApiOk(plansRes, 'Failed to fetch plans');
|
|
178
|
+
const { plans } = plansRes.data;
|
|
179
|
+
const activePlans = plans.filter((p) => p.status === 'active' && p.price > 0);
|
|
180
|
+
if (activePlans.length === 0) {
|
|
181
|
+
info('No paid plans available.');
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
// 2. Select plan
|
|
185
|
+
const selectedPlanId = await select({
|
|
186
|
+
message: 'Select a plan:',
|
|
187
|
+
choices: activePlans.map((p) => ({
|
|
188
|
+
name: `${p.name} (${p.interval === 'month' ? 'Monthly' : 'Yearly'}) — ${formatPrice(p)} [${formatCredits(p.rules)} credits, ${p.rules.limitWorkflows ?? 0} workflows]`,
|
|
189
|
+
value: p._id,
|
|
190
|
+
})),
|
|
191
|
+
});
|
|
192
|
+
const selectedPlan = activePlans.find((p) => p._id === selectedPlanId);
|
|
193
|
+
// 3. Select payment method
|
|
194
|
+
const payMethod = await select({
|
|
195
|
+
message: 'Payment method:',
|
|
196
|
+
choices: [
|
|
197
|
+
{ name: 'Credit Card (Stripe)', value: 'stripe' },
|
|
198
|
+
{ name: 'Crypto (USDC on-chain)', value: 'crypto' },
|
|
199
|
+
],
|
|
200
|
+
});
|
|
201
|
+
// 4. Confirm
|
|
202
|
+
const priceStr = `$${selectedPlan.price}/${selectedPlan.interval === 'month' ? 'mo' : 'yr'}`;
|
|
203
|
+
console.log('');
|
|
204
|
+
console.log(chalk.bold('Order Summary:'));
|
|
205
|
+
console.log(` Plan : ${chalk.bold(selectedPlan.name)} (${selectedPlan.interval === 'month' ? 'Monthly' : 'Yearly'})`);
|
|
206
|
+
console.log(` Price : ${chalk.bold(priceStr)}`);
|
|
207
|
+
console.log(` Payment : ${payMethod === 'stripe' ? 'Credit Card (Stripe)' : 'Crypto (USDC)'}`);
|
|
208
|
+
console.log('');
|
|
209
|
+
const ok = await confirm({ message: 'Proceed to checkout?', default: true });
|
|
210
|
+
if (!ok) {
|
|
211
|
+
console.log(chalk.dim('Cancelled.'));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
// 5. Create checkout
|
|
215
|
+
if (payMethod === 'stripe') {
|
|
216
|
+
await handleStripeCheckout(creds.accessToken, selectedPlanId);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
await handleCryptoCheckout(creds.accessToken, selectedPlanId);
|
|
220
|
+
}
|
|
221
|
+
}));
|
|
222
|
+
async function handleStripeCheckout(token, planId) {
|
|
223
|
+
const spin = spinner('Creating checkout session…');
|
|
224
|
+
const res = await paymentApi.checkoutPlan(token, planId, 'https://minara.ai/payment/success', 'https://minara.ai/payment/cancel');
|
|
225
|
+
spin.stop();
|
|
226
|
+
assertApiOk(res, 'Failed to create checkout session');
|
|
227
|
+
const data = res.data;
|
|
228
|
+
const url = data.url ?? data.checkoutUrl;
|
|
229
|
+
if (url) {
|
|
230
|
+
success('Checkout session created!');
|
|
231
|
+
console.log('');
|
|
232
|
+
console.log(chalk.dim(' Opening browser for payment…'));
|
|
233
|
+
console.log(chalk.cyan(` ${url}`));
|
|
234
|
+
console.log('');
|
|
235
|
+
openBrowser(url);
|
|
236
|
+
info('Complete the payment in your browser. Your subscription will activate automatically.');
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
// Fallback: show all returned data
|
|
240
|
+
success('Checkout session created:');
|
|
241
|
+
printKV(data);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
async function handleCryptoCheckout(token, planId) {
|
|
245
|
+
const spin = spinner('Creating crypto checkout…');
|
|
246
|
+
const res = await paymentApi.cryptoCheckoutPlan(token, planId);
|
|
247
|
+
spin.stop();
|
|
248
|
+
assertApiOk(res, 'Failed to create crypto checkout');
|
|
249
|
+
const data = res.data;
|
|
250
|
+
const url = data.url ?? data.checkoutUrl;
|
|
251
|
+
if (url) {
|
|
252
|
+
success('Crypto checkout created!');
|
|
253
|
+
console.log('');
|
|
254
|
+
console.log(chalk.dim(' Opening browser for crypto payment…'));
|
|
255
|
+
console.log(chalk.cyan(` ${url}`));
|
|
256
|
+
console.log('');
|
|
257
|
+
openBrowser(url);
|
|
258
|
+
info('Complete the crypto payment in your browser. Your subscription will activate after confirmation.');
|
|
259
|
+
}
|
|
260
|
+
else if (data.address) {
|
|
261
|
+
success('Crypto payment details:');
|
|
262
|
+
console.log('');
|
|
263
|
+
printKV(data);
|
|
264
|
+
console.log('');
|
|
265
|
+
info('Send the exact amount to the address above. Your subscription will activate after on-chain confirmation.');
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
success('Crypto checkout created:');
|
|
269
|
+
printKV(data);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// ─── buy-credits ────────────────────────────────────────────────────────
|
|
273
|
+
const buyCreditsCmd = new Command('buy-credits')
|
|
274
|
+
.description('Buy a one-time credit package')
|
|
275
|
+
.action(wrapAction(async () => {
|
|
276
|
+
const creds = requireAuth();
|
|
277
|
+
const spin = spinner('Fetching packages…');
|
|
278
|
+
const plansRes = await paymentApi.getPlans();
|
|
279
|
+
spin.stop();
|
|
280
|
+
assertApiOk(plansRes, 'Failed to fetch packages');
|
|
281
|
+
const { packages } = plansRes.data;
|
|
282
|
+
if (packages.length === 0) {
|
|
283
|
+
info('No credit packages available.');
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
// Select package
|
|
287
|
+
const selectedPkgId = await select({
|
|
288
|
+
message: 'Select a credit package:',
|
|
289
|
+
choices: packages.map((pkg) => ({
|
|
290
|
+
name: `$${pkg.amount} — ${Number(pkg.credit).toLocaleString()} credits`,
|
|
291
|
+
value: pkg._id,
|
|
292
|
+
})),
|
|
293
|
+
});
|
|
294
|
+
const selectedPkg = packages.find((p) => p._id === selectedPkgId);
|
|
295
|
+
// Select payment method
|
|
296
|
+
const payMethod = await select({
|
|
297
|
+
message: 'Payment method:',
|
|
298
|
+
choices: [
|
|
299
|
+
{ name: 'Credit Card (Stripe)', value: 'stripe' },
|
|
300
|
+
{ name: 'Crypto (USDC on-chain)', value: 'crypto' },
|
|
301
|
+
],
|
|
302
|
+
});
|
|
303
|
+
console.log('');
|
|
304
|
+
console.log(chalk.bold('Package Summary:'));
|
|
305
|
+
console.log(` Price : ${chalk.bold('$' + selectedPkg.amount)}`);
|
|
306
|
+
console.log(` Credits : ${Number(selectedPkg.credit).toLocaleString()}`);
|
|
307
|
+
console.log(` Payment : ${payMethod === 'stripe' ? 'Credit Card (Stripe)' : 'Crypto (USDC)'}`);
|
|
308
|
+
console.log('');
|
|
309
|
+
const ok = await confirm({ message: 'Proceed to checkout?', default: true });
|
|
310
|
+
if (!ok) {
|
|
311
|
+
console.log(chalk.dim('Cancelled.'));
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (payMethod === 'stripe') {
|
|
315
|
+
const spin2 = spinner('Creating checkout session…');
|
|
316
|
+
const res = await paymentApi.checkoutPackage(creds.accessToken, selectedPkgId, 'https://minara.ai/payment/success', 'https://minara.ai/payment/cancel');
|
|
317
|
+
spin2.stop();
|
|
318
|
+
assertApiOk(res, 'Failed to create checkout');
|
|
319
|
+
const url = res.data?.url ?? res.data?.checkoutUrl;
|
|
320
|
+
if (url) {
|
|
321
|
+
success('Opening browser for payment…');
|
|
322
|
+
console.log(chalk.cyan(` ${url}`));
|
|
323
|
+
openBrowser(url);
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
success('Checkout created:');
|
|
327
|
+
printKV(res.data);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
const spin2 = spinner('Creating crypto checkout…');
|
|
332
|
+
const res = await paymentApi.cryptoCheckoutPackage(creds.accessToken, selectedPkgId);
|
|
333
|
+
spin2.stop();
|
|
334
|
+
assertApiOk(res, 'Failed to create crypto checkout');
|
|
335
|
+
const url = res.data?.url ?? res.data?.checkoutUrl;
|
|
336
|
+
if (url) {
|
|
337
|
+
success('Opening browser for crypto payment…');
|
|
338
|
+
console.log(chalk.cyan(` ${url}`));
|
|
339
|
+
openBrowser(url);
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
success('Crypto checkout:');
|
|
343
|
+
printKV(res.data);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}));
|
|
347
|
+
// ─── cancel ─────────────────────────────────────────────────────────────
|
|
348
|
+
const cancelCmd = new Command('cancel')
|
|
349
|
+
.description('Cancel your current subscription')
|
|
350
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
351
|
+
.action(wrapAction(async (opts) => {
|
|
352
|
+
const creds = requireAuth();
|
|
353
|
+
if (!opts.yes) {
|
|
354
|
+
warn('Cancelling your subscription will downgrade you to the Free plan at the end of your billing period.');
|
|
355
|
+
console.log('');
|
|
356
|
+
const ok = await confirm({
|
|
357
|
+
message: 'Are you sure you want to cancel your subscription?',
|
|
358
|
+
default: false,
|
|
359
|
+
});
|
|
360
|
+
if (!ok) {
|
|
361
|
+
console.log(chalk.dim('Kept your subscription.'));
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const spin = spinner('Cancelling subscription…');
|
|
366
|
+
const res = await paymentApi.cancelSubscription(creds.accessToken);
|
|
367
|
+
spin.stop();
|
|
368
|
+
assertApiOk(res, 'Failed to cancel subscription');
|
|
369
|
+
success('Subscription cancelled.');
|
|
370
|
+
info('You will continue to have access until the end of your current billing period.');
|
|
371
|
+
if (res.data && typeof res.data === 'object' && Object.keys(res.data).length > 0) {
|
|
372
|
+
console.log('');
|
|
373
|
+
printKV(res.data);
|
|
374
|
+
}
|
|
375
|
+
}));
|
|
376
|
+
// ─── parent ─────────────────────────────────────────────────────────────
|
|
377
|
+
export const premiumCommand = new Command('premium')
|
|
378
|
+
.description('Manage your Minara subscription — plans, subscribe, cancel')
|
|
379
|
+
.addCommand(plansCmd)
|
|
380
|
+
.addCommand(statusCmd)
|
|
381
|
+
.addCommand(subscribeCmd)
|
|
382
|
+
.addCommand(buyCreditsCmd)
|
|
383
|
+
.addCommand(cancelCmd)
|
|
384
|
+
.action(wrapAction(async () => {
|
|
385
|
+
const action = await select({
|
|
386
|
+
message: 'Premium:',
|
|
387
|
+
choices: [
|
|
388
|
+
{ name: 'View available plans', value: 'plans' },
|
|
389
|
+
{ name: 'View my subscription', value: 'status' },
|
|
390
|
+
{ name: 'Subscribe / Change plan', value: 'subscribe' },
|
|
391
|
+
{ name: 'Buy credit package', value: 'buy-credits' },
|
|
392
|
+
{ name: 'Cancel subscription', value: 'cancel' },
|
|
393
|
+
],
|
|
394
|
+
});
|
|
395
|
+
const sub = premiumCommand.commands.find((c) => c.name() === action);
|
|
396
|
+
if (sub)
|
|
397
|
+
await sub.parseAsync([], { from: 'user' });
|
|
398
|
+
}));
|