@dawnai/cli 1.0.7 → 1.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/index.js +132 -128
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
|
-
import http from 'node:http';
|
|
4
3
|
import os from 'node:os';
|
|
5
4
|
import path from 'node:path';
|
|
6
5
|
import process from 'node:process';
|
|
6
|
+
import readline from 'node:readline/promises';
|
|
7
7
|
import { spawn } from 'node:child_process';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
|
-
const CLI_VERSION = '1.0
|
|
9
|
+
const CLI_VERSION = '1.1.0';
|
|
10
10
|
const DAWN_API_BASE_URL = 'https://api.dawn.ai';
|
|
11
11
|
const FUNDING_LINK_TEMPLATE = process.env.DAWN_HELIO_LINK_TEMPLATE || '';
|
|
12
12
|
const CLI_HOME = process.env.DAWN_CLI_HOME || path.join(os.homedir(), '.dawn-cli');
|
|
@@ -136,6 +136,12 @@ function formatNumeric(value, fallback = 0) {
|
|
|
136
136
|
const n = Number(value);
|
|
137
137
|
return Number.isFinite(n) ? n : fallback;
|
|
138
138
|
}
|
|
139
|
+
function parseDecimalAmount(value, fallback = 0) {
|
|
140
|
+
if (typeof value === 'string') {
|
|
141
|
+
return formatNumeric(value.replace(/,/g, '').trim(), fallback);
|
|
142
|
+
}
|
|
143
|
+
return formatNumeric(value, fallback);
|
|
144
|
+
}
|
|
139
145
|
function formatUsd(value) {
|
|
140
146
|
return `$${formatNumeric(value).toLocaleString('en-US', {
|
|
141
147
|
minimumFractionDigits: 2,
|
|
@@ -353,6 +359,76 @@ async function getLatestStrategyVersion(config, conversationId, withCode) {
|
|
|
353
359
|
const latest = (await apiFetch(config, `/conversations/${conversationId}/latest-version?withCode=${withCode ? 'true' : 'false'}`));
|
|
354
360
|
return latest || null;
|
|
355
361
|
}
|
|
362
|
+
async function resolveLaunchVaultId(config, isPaper, budget) {
|
|
363
|
+
if (isPaper) {
|
|
364
|
+
console.log('Creating new paper vault...');
|
|
365
|
+
const createResponse = (await apiFetch(config, '/v1/vaults/create', {
|
|
366
|
+
method: 'POST',
|
|
367
|
+
body: JSON.stringify({ walletType: 'paper' }),
|
|
368
|
+
}));
|
|
369
|
+
const createdVaultId = createResponse?.vault?.id;
|
|
370
|
+
if (!createResponse?.success || !createdVaultId) {
|
|
371
|
+
throw new Error(createResponse?.error ||
|
|
372
|
+
'Failed to create paper vault: invalid create-vault response.');
|
|
373
|
+
}
|
|
374
|
+
const vaultId = String(createdVaultId);
|
|
375
|
+
console.log(`Paper vault created: ${shortId(vaultId)}.`);
|
|
376
|
+
console.log(`Auto-funding paper vault with ${formatUsd(budget)}...`);
|
|
377
|
+
await apiFetch(config, `/v1/vaults/${vaultId}/fund`, {
|
|
378
|
+
method: 'POST',
|
|
379
|
+
body: JSON.stringify({ amount: String(budget) }),
|
|
380
|
+
});
|
|
381
|
+
console.log(`Paper vault funded: ${formatUsd(budget)}.`);
|
|
382
|
+
return vaultId;
|
|
383
|
+
}
|
|
384
|
+
const vaultsResponse = (await apiFetch(config, '/v1/vaults/all/strategy?isArchived=false'));
|
|
385
|
+
const vaults = vaultsResponse?.vaults || [];
|
|
386
|
+
let strategyVaultId = '';
|
|
387
|
+
let strategyVaultCurrentAmount = 0;
|
|
388
|
+
if (vaults.length > 0) {
|
|
389
|
+
strategyVaultId = String(vaults[0].id);
|
|
390
|
+
strategyVaultCurrentAmount = formatNumeric(vaults[0].currentAmount);
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
console.log('No strategy vault found, creating strategy vault...');
|
|
394
|
+
const createResponse = (await apiFetch(config, '/v1/vaults/create', {
|
|
395
|
+
method: 'POST',
|
|
396
|
+
body: JSON.stringify({ walletType: 'strategy' }),
|
|
397
|
+
}));
|
|
398
|
+
const createdVaultId = createResponse?.vault?.id;
|
|
399
|
+
if (!createResponse?.success || !createdVaultId) {
|
|
400
|
+
throw new Error(createResponse?.error ||
|
|
401
|
+
'Failed to create strategy vault: invalid create-vault response.');
|
|
402
|
+
}
|
|
403
|
+
strategyVaultId = String(createdVaultId);
|
|
404
|
+
strategyVaultCurrentAmount = 0;
|
|
405
|
+
console.log(`Strategy vault created: ${shortId(createdVaultId)}.`);
|
|
406
|
+
}
|
|
407
|
+
const fundingDeficit = budget - strategyVaultCurrentAmount;
|
|
408
|
+
if (fundingDeficit > 1e-9) {
|
|
409
|
+
const walletBalance = (await apiFetch(config, '/wallet/balance'));
|
|
410
|
+
const availableFundingBalance = parseDecimalAmount(walletBalance.availableBalance ?? walletBalance.usdcBalance ?? '0', 0);
|
|
411
|
+
console.log(`Strategy vault funding is ${formatUsd(strategyVaultCurrentAmount)}; funding additional ${formatUsd(fundingDeficit)} to reach ${formatUsd(budget)}...`);
|
|
412
|
+
if (availableFundingBalance + 1e-9 < fundingDeficit) {
|
|
413
|
+
throw new Error(`Insufficient available funding balance for live launch: available ${formatUsd(availableFundingBalance)}, required ${formatUsd(fundingDeficit)}.`);
|
|
414
|
+
}
|
|
415
|
+
try {
|
|
416
|
+
await apiFetch(config, `/v1/vaults/${strategyVaultId}/fund`, {
|
|
417
|
+
method: 'POST',
|
|
418
|
+
body: JSON.stringify({ amount: String(fundingDeficit) }),
|
|
419
|
+
});
|
|
420
|
+
console.log(`Strategy vault funded: +${formatUsd(fundingDeficit)}.`);
|
|
421
|
+
}
|
|
422
|
+
catch (error) {
|
|
423
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
424
|
+
console.log(`Failed to auto-fund strategy vault: ${message}`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
console.log(`Strategy vault already funded (${formatUsd(strategyVaultCurrentAmount)} >= ${formatUsd(budget)}).`);
|
|
429
|
+
}
|
|
430
|
+
return strategyVaultId;
|
|
431
|
+
}
|
|
356
432
|
async function apiFetch(config, endpoint, options = {}) {
|
|
357
433
|
const apiBaseUrl = await getApiBaseUrl(config);
|
|
358
434
|
const token = getAuthToken(config);
|
|
@@ -386,20 +462,27 @@ async function apiFetch(config, endpoint, options = {}) {
|
|
|
386
462
|
}
|
|
387
463
|
return body;
|
|
388
464
|
}
|
|
389
|
-
function
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
spawn('open', [url], { stdio: 'ignore', detached: true }).unref();
|
|
393
|
-
return;
|
|
465
|
+
async function promptForHeadlessCode() {
|
|
466
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
467
|
+
throw new Error('Interactive input is unavailable. Set DAWN_JWT_TOKEN for non-interactive environments.');
|
|
394
468
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
469
|
+
const rl = readline.createInterface({
|
|
470
|
+
input: process.stdin,
|
|
471
|
+
output: process.stdout,
|
|
472
|
+
});
|
|
473
|
+
try {
|
|
474
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
475
|
+
const raw = (await rl.question('Enter the 6-digit code: ')).trim();
|
|
476
|
+
if (/^\d{6}$/.test(raw)) {
|
|
477
|
+
return raw;
|
|
478
|
+
}
|
|
479
|
+
console.log('Invalid code format. Please enter exactly 6 digits.');
|
|
480
|
+
}
|
|
401
481
|
}
|
|
402
|
-
|
|
482
|
+
finally {
|
|
483
|
+
rl.close();
|
|
484
|
+
}
|
|
485
|
+
throw new Error('Too many invalid code entry attempts.');
|
|
403
486
|
}
|
|
404
487
|
async function handleAuth(args) {
|
|
405
488
|
if (args.includes('--help') || args.includes('-h')) {
|
|
@@ -408,11 +491,11 @@ async function handleAuth(args) {
|
|
|
408
491
|
dawn auth status
|
|
409
492
|
dawn auth logout
|
|
410
493
|
|
|
411
|
-
|
|
412
|
-
|
|
494
|
+
Login uses a browser URL + 6-digit code flow.
|
|
495
|
+
For non-interactive headless/CI environments, set DAWN_JWT_TOKEN.`);
|
|
413
496
|
return;
|
|
414
497
|
}
|
|
415
|
-
const [subcommand
|
|
498
|
+
const [subcommand] = args;
|
|
416
499
|
if (!subcommand) {
|
|
417
500
|
throw new Error('Usage: dawn auth <login|status|logout>');
|
|
418
501
|
}
|
|
@@ -437,103 +520,36 @@ instead of using interactive login.`);
|
|
|
437
520
|
if (subcommand !== 'login') {
|
|
438
521
|
throw new Error(`Unknown auth command: ${subcommand}`);
|
|
439
522
|
}
|
|
440
|
-
const loginArgs = rest;
|
|
441
|
-
const { flags } = parseFlags(loginArgs);
|
|
442
523
|
const config = await loadConfig();
|
|
443
|
-
await
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
const server = http.createServer(async (req, res) => {
|
|
473
|
-
try {
|
|
474
|
-
const reqUrl = new URL(req.url || '/', 'http://127.0.0.1');
|
|
475
|
-
if (reqUrl.pathname !== '/callback') {
|
|
476
|
-
res.statusCode = 404;
|
|
477
|
-
res.end('Not found');
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
480
|
-
const token = reqUrl.searchParams.get('token');
|
|
481
|
-
const error = reqUrl.searchParams.get('error');
|
|
482
|
-
if (error) {
|
|
483
|
-
res.statusCode = 200;
|
|
484
|
-
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
485
|
-
res.end('<h3>Authentication failed. You can close this window.</h3>');
|
|
486
|
-
closeServer();
|
|
487
|
-
rejectOnce(new Error(`Auth failed: ${error}`));
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
if (!token) {
|
|
491
|
-
res.statusCode = 200;
|
|
492
|
-
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
493
|
-
res.end('<h3>Missing token. You can close this window.</h3>');
|
|
494
|
-
closeServer();
|
|
495
|
-
rejectOnce(new Error('Missing token in auth callback.'));
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
498
|
-
const latestConfig = await loadConfig();
|
|
499
|
-
await saveConfig({ ...latestConfig, token });
|
|
500
|
-
res.statusCode = 200;
|
|
501
|
-
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
502
|
-
res.end('<h3>Authentication complete. You can close this window.</h3>');
|
|
503
|
-
closeServer();
|
|
504
|
-
resolveOnce();
|
|
505
|
-
}
|
|
506
|
-
catch (error) {
|
|
507
|
-
closeServer();
|
|
508
|
-
rejectOnce(error);
|
|
509
|
-
}
|
|
510
|
-
});
|
|
511
|
-
server.listen(0, '127.0.0.1', () => {
|
|
512
|
-
const addr = server.address();
|
|
513
|
-
if (!addr || typeof addr === 'string') {
|
|
514
|
-
rejectOnce(new Error('Failed to open local auth callback server.'));
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
const redirect = `http://127.0.0.1:${addr.port}/callback`;
|
|
518
|
-
void (async () => {
|
|
519
|
-
try {
|
|
520
|
-
const apiBaseUrl = await getApiBaseUrl(config);
|
|
521
|
-
const authUrl = `${apiBaseUrl}/v1/auth/google/?redirect=${encodeURIComponent(redirect)}`;
|
|
522
|
-
console.log(`Using API: ${apiBaseUrl}`);
|
|
523
|
-
console.log('Opening browser for Dawn auth...');
|
|
524
|
-
openBrowser(authUrl);
|
|
525
|
-
console.log('Waiting for authentication callback...');
|
|
526
|
-
}
|
|
527
|
-
catch (error) {
|
|
528
|
-
rejectOnce(error);
|
|
529
|
-
}
|
|
530
|
-
})();
|
|
531
|
-
});
|
|
532
|
-
timeoutId = setTimeout(() => {
|
|
533
|
-
closeServer();
|
|
534
|
-
rejectOnce(new Error('Timed out waiting for authentication callback.'));
|
|
535
|
-
}, timeoutMs);
|
|
536
|
-
});
|
|
524
|
+
const apiBaseUrl = await getApiBaseUrl(config);
|
|
525
|
+
const initiateResponse = (await apiFetch(config, '/v1/auth/google/?headless=true'));
|
|
526
|
+
const authUrl = initiateResponse.authUrl;
|
|
527
|
+
const loginRequestId = initiateResponse.loginRequestId;
|
|
528
|
+
if (!authUrl || !loginRequestId) {
|
|
529
|
+
throw new Error('Failed to initialize headless authentication flow.');
|
|
530
|
+
}
|
|
531
|
+
console.log(`Using API: ${apiBaseUrl}`);
|
|
532
|
+
console.log('');
|
|
533
|
+
console.log('Open this URL in any browser/device and complete Google login:');
|
|
534
|
+
console.log(authUrl);
|
|
535
|
+
if (typeof initiateResponse.expiresInSeconds === 'number') {
|
|
536
|
+
console.log(`Code expires in about ${Math.max(1, Math.floor(initiateResponse.expiresInSeconds / 60))} minute(s).`);
|
|
537
|
+
}
|
|
538
|
+
console.log('');
|
|
539
|
+
const code = await promptForHeadlessCode();
|
|
540
|
+
const completeResponse = (await apiFetch(config, '/v1/auth/google/headless/complete', {
|
|
541
|
+
method: 'POST',
|
|
542
|
+
body: JSON.stringify({
|
|
543
|
+
loginRequestId,
|
|
544
|
+
code,
|
|
545
|
+
}),
|
|
546
|
+
}));
|
|
547
|
+
const token = completeResponse.jwtToken;
|
|
548
|
+
if (!token) {
|
|
549
|
+
throw new Error('Authentication did not return a JWT token.');
|
|
550
|
+
}
|
|
551
|
+
const latestConfig = await loadConfig();
|
|
552
|
+
await saveConfig({ ...latestConfig, token });
|
|
537
553
|
console.log('Authentication successful.');
|
|
538
554
|
}
|
|
539
555
|
async function ensureWallet(config) {
|
|
@@ -583,13 +599,7 @@ async function handleFund(args) {
|
|
|
583
599
|
console.log('Deposit instructions:');
|
|
584
600
|
console.log('- Send USDC on Polygon (Polygon PoS) to the wallet above.');
|
|
585
601
|
console.log('- Avoid sending tokens on other networks unless you bridge first.');
|
|
586
|
-
|
|
587
|
-
if (link) {
|
|
588
|
-
console.log(`Funding link: ${link}`);
|
|
589
|
-
}
|
|
590
|
-
else {
|
|
591
|
-
console.log('Funding link: not generated (legacy Helio /pay links are deprecated).');
|
|
592
|
-
}
|
|
602
|
+
console.log('Use @moonpay/cli to deposit USDC.e to the wallet above.');
|
|
593
603
|
}
|
|
594
604
|
async function handleOverview(args) {
|
|
595
605
|
const { flags } = parseFlags(args);
|
|
@@ -1152,13 +1162,6 @@ async function handleStrategy(args) {
|
|
|
1152
1162
|
throw new Error('--budget must be a positive number.');
|
|
1153
1163
|
}
|
|
1154
1164
|
const isPaper = !flags.live;
|
|
1155
|
-
const vaultType = isPaper ? 'paper' : 'strategy';
|
|
1156
|
-
const vaultsResponse = (await apiFetch(config, `/v1/vaults/all/${vaultType}?isArchived=false`));
|
|
1157
|
-
const vaults = vaultsResponse?.vaults || [];
|
|
1158
|
-
if (vaults.length === 0) {
|
|
1159
|
-
throw new Error(`No ${vaultType} vault found. Create one in the Dawn app before launching.`);
|
|
1160
|
-
}
|
|
1161
|
-
const vault = vaults[0];
|
|
1162
1165
|
const latestVersion = await getLatestStrategyVersion(config, conversationId, true);
|
|
1163
1166
|
if (!latestVersion?.id) {
|
|
1164
1167
|
throw new Error(`No code-ready strategy version found. Run: dawn strategy rules ${conversationId} approve-all, then dawn strategy code ${conversationId} generate.`);
|
|
@@ -1175,13 +1178,14 @@ async function handleStrategy(args) {
|
|
|
1175
1178
|
throw new Error('--hours must be a positive number.');
|
|
1176
1179
|
}
|
|
1177
1180
|
const terminateAt = new Date(Date.now() + hours * 60 * 60 * 1000).toISOString();
|
|
1181
|
+
const vaultId = await resolveLaunchVaultId(config, isPaper, budget);
|
|
1178
1182
|
const result = (await apiFetch(config, `/strategy/${conversationId}/run-strategy-v2`, {
|
|
1179
1183
|
method: 'POST',
|
|
1180
1184
|
body: JSON.stringify({
|
|
1181
1185
|
strategyVersionId: latestVersion.id,
|
|
1182
1186
|
isPaper,
|
|
1183
1187
|
terminateAt,
|
|
1184
|
-
walletId:
|
|
1188
|
+
walletId: vaultId,
|
|
1185
1189
|
}),
|
|
1186
1190
|
}));
|
|
1187
1191
|
console.log(`Launched strategy. Strategy ID: ${result?.strategy?.id ?? 'unknown'}${isPaper ? ' (paper)' : ' (live)'}`);
|