@lawrenceliang-btc/atel-sdk 1.1.23 → 1.1.26
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/bin/atel.mjs +59 -17
- package/bin/hub-helpers.mjs +348 -0
- package/package.json +1 -1
- package/skill/SKILL.md +323 -186
- package/skill/atel-agent/SKILL.md +315 -21
package/bin/atel.mjs
CHANGED
|
@@ -51,7 +51,9 @@
|
|
|
51
51
|
* atel boost-status [did] Check boost status
|
|
52
52
|
*/
|
|
53
53
|
|
|
54
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from 'node:fs';
|
|
54
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync, copyFileSync } from 'node:fs';
|
|
55
|
+
import { fileURLToPath } from 'node:url';
|
|
56
|
+
import { cmdHub } from './hub-helpers.mjs';
|
|
55
57
|
import { resolve, join, dirname } from 'node:path';
|
|
56
58
|
import crypto from 'node:crypto';
|
|
57
59
|
import {
|
|
@@ -1202,7 +1204,7 @@ async function signTaskRequest(taskRequest, secretKey) {
|
|
|
1202
1204
|
version: taskRequest.version
|
|
1203
1205
|
});
|
|
1204
1206
|
|
|
1205
|
-
console.error('[DEBUG] SDK signable JSON:', signable);
|
|
1207
|
+
if (process.env.ATEL_DEBUG) console.error('[DEBUG] SDK signable JSON:', signable);
|
|
1206
1208
|
|
|
1207
1209
|
const signature = nacl.sign.detached(Buffer.from(signable), secretKey);
|
|
1208
1210
|
return Buffer.from(signature).toString('base64');
|
|
@@ -2055,8 +2057,38 @@ async function cmdInit(agentId) {
|
|
|
2055
2057
|
did: identity.did,
|
|
2056
2058
|
policy: POLICY_FILE,
|
|
2057
2059
|
anchor: anchorConfigured ? 'configured' : 'disabled',
|
|
2058
|
-
next: 'Run: atel start [port] — auto-configures network and registers'
|
|
2060
|
+
next: 'Run: atel start [port] — auto-configures network and registers'
|
|
2059
2061
|
}, null, 2));
|
|
2062
|
+
|
|
2063
|
+
// Auto-install SKILL.md to OpenClaw skills directory
|
|
2064
|
+
try {
|
|
2065
|
+
const sdkSkillPath = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'skill', 'atel-agent', 'SKILL.md');
|
|
2066
|
+
if (existsSync(sdkSkillPath)) {
|
|
2067
|
+
const home = process.env.HOME || '';
|
|
2068
|
+
const candidates = [
|
|
2069
|
+
join(home, '.openclaw', 'skills', 'atel-agent'),
|
|
2070
|
+
join(home, '.openclaw', 'workspace', 'skills', 'atel-agent'),
|
|
2071
|
+
];
|
|
2072
|
+
let installed = false;
|
|
2073
|
+
for (const dir of candidates) {
|
|
2074
|
+
if (existsSync(dirname(dir))) {
|
|
2075
|
+
mkdirSync(dir, { recursive: true });
|
|
2076
|
+
copyFileSync(sdkSkillPath, join(dir, 'SKILL.md'));
|
|
2077
|
+
console.log(`\n✅ ATEL Skill auto-installed to ${dir}`);
|
|
2078
|
+
console.log(' Your AI agent will automatically know how to use ATEL.');
|
|
2079
|
+
console.log('');
|
|
2080
|
+
console.log('📌 Tell your agent this message to get started:');
|
|
2081
|
+
console.log(' "Read the atel-agent skill at ~/.openclaw/skills/atel-agent/SKILL.md carefully, then help me set up ATEL and start earning."');
|
|
2082
|
+
installed = true;
|
|
2083
|
+
break;
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
if (!installed) {
|
|
2087
|
+
console.log('\n📋 To teach your AI agent about ATEL, send it this message:');
|
|
2088
|
+
console.log(` "Read ${sdkSkillPath} carefully, then help me set up ATEL and start earning."`);
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
} catch (e) { /* skill install is best-effort */ }
|
|
2060
2092
|
}
|
|
2061
2093
|
|
|
2062
2094
|
async function cmdAnchor(subcommand) {
|
|
@@ -2386,38 +2418,38 @@ async function cmdStart(port) {
|
|
|
2386
2418
|
|
|
2387
2419
|
async function verifyAnchorFromChain(chain, txHash, traceRoot) {
|
|
2388
2420
|
try {
|
|
2389
|
-
console.error('[DEBUG] verifyAnchorFromChain input:', { chain, txHash, traceRoot });
|
|
2421
|
+
if (process.env.ATEL_DEBUG) console.error('[DEBUG] verifyAnchorFromChain input:', { chain, txHash, traceRoot });
|
|
2390
2422
|
|
|
2391
2423
|
if (!txHash || !traceRoot) return { checked: false, verified: false, reason: 'missing_anchor_or_root' };
|
|
2392
2424
|
const c = (chain || 'solana').toLowerCase();
|
|
2393
2425
|
|
|
2394
2426
|
if (c === 'solana') {
|
|
2395
2427
|
const rpcUrl = process.env.ATEL_SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com';
|
|
2396
|
-
console.error('[DEBUG] Solana RPC URL:', rpcUrl);
|
|
2428
|
+
if (process.env.ATEL_DEBUG) console.error('[DEBUG] Solana RPC URL:', rpcUrl);
|
|
2397
2429
|
const provider = new SolanaAnchorProvider({ rpcUrl });
|
|
2398
2430
|
const r = await provider.verify(traceRoot, txHash);
|
|
2399
|
-
console.error('[DEBUG] Solana verify result:', r);
|
|
2431
|
+
if (process.env.ATEL_DEBUG) console.error('[DEBUG] Solana verify result:', r);
|
|
2400
2432
|
return { checked: true, verified: !!r?.valid, chain: 'solana', detail: r?.detail };
|
|
2401
2433
|
}
|
|
2402
2434
|
if (c === 'base') {
|
|
2403
2435
|
const rpcUrl = process.env.ATEL_BASE_RPC_URL || 'https://mainnet.base.org';
|
|
2404
|
-
console.error('[DEBUG] Base RPC URL:', rpcUrl);
|
|
2436
|
+
if (process.env.ATEL_DEBUG) console.error('[DEBUG] Base RPC URL:', rpcUrl);
|
|
2405
2437
|
const provider = new BaseAnchorProvider({ rpcUrl });
|
|
2406
2438
|
const r = await provider.verify(traceRoot, txHash);
|
|
2407
|
-
console.error('[DEBUG] Base verify result:', r);
|
|
2439
|
+
if (process.env.ATEL_DEBUG) console.error('[DEBUG] Base verify result:', r);
|
|
2408
2440
|
return { checked: true, verified: !!r?.valid, chain: 'base', detail: r?.detail };
|
|
2409
2441
|
}
|
|
2410
2442
|
if (c === 'bsc') {
|
|
2411
2443
|
const rpcUrl = process.env.ATEL_BSC_RPC_URL || 'https://bsc-dataseed.binance.org';
|
|
2412
|
-
console.error('[DEBUG] BSC RPC URL:', rpcUrl);
|
|
2444
|
+
if (process.env.ATEL_DEBUG) console.error('[DEBUG] BSC RPC URL:', rpcUrl);
|
|
2413
2445
|
const provider = new BSCAnchorProvider({ rpcUrl });
|
|
2414
2446
|
const r = await provider.verify(traceRoot, txHash);
|
|
2415
|
-
console.error('[DEBUG] BSC verify result:', r);
|
|
2447
|
+
if (process.env.ATEL_DEBUG) console.error('[DEBUG] BSC verify result:', r);
|
|
2416
2448
|
return { checked: true, verified: !!r?.valid, chain: 'bsc', detail: r?.detail };
|
|
2417
2449
|
}
|
|
2418
2450
|
return { checked: false, verified: false, reason: `unsupported_chain:${c}` };
|
|
2419
2451
|
} catch (e) {
|
|
2420
|
-
console.error('[DEBUG] verifyAnchorFromChain error:', e);
|
|
2452
|
+
if (process.env.ATEL_DEBUG) console.error('[DEBUG] verifyAnchorFromChain error:', e);
|
|
2421
2453
|
return { checked: true, verified: false, reason: e.message };
|
|
2422
2454
|
}
|
|
2423
2455
|
}
|
|
@@ -5608,8 +5640,8 @@ async function signedFetch(method, path, payload = {}) {
|
|
|
5608
5640
|
const sig = Buffer.from(nacl.sign.detached(Buffer.from(signable), id.secretKey)).toString('base64');
|
|
5609
5641
|
const body = JSON.stringify({ did: id.did, payload, timestamp: ts, signature: sig });
|
|
5610
5642
|
// Always use POST for signed requests (DIDAuth reads body, GET cannot have body)
|
|
5611
|
-
console.error("DEBUG URL:", `${PLATFORM_URL}${path}`); const res = await fetch(`${PLATFORM_URL}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body });
|
|
5612
|
-
const text = await res.text(); console.error("DEBUG Response:", text); const data = JSON.parse(text);
|
|
5643
|
+
if (process.env.ATEL_DEBUG) console.error("DEBUG URL:", `${PLATFORM_URL}${path}`); const res = await fetch(`${PLATFORM_URL}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body });
|
|
5644
|
+
const text = await res.text(); if (process.env.ATEL_DEBUG) console.error("DEBUG Response:", text); const data = JSON.parse(text);
|
|
5613
5645
|
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
|
|
5614
5646
|
return data;
|
|
5615
5647
|
}
|
|
@@ -5914,7 +5946,7 @@ async function cmdOrder(executorDid, capType, price) {
|
|
|
5914
5946
|
async function cmdOrderInfo(orderId) {
|
|
5915
5947
|
if (!orderId) { console.error('Usage: atel order-info <orderId>'); process.exit(1); }
|
|
5916
5948
|
const res = await fetch(`${PLATFORM_URL}/trade/v1/order/${orderId}`);
|
|
5917
|
-
const text = await res.text(); console.error("DEBUG Response:", text); const data = JSON.parse(text);
|
|
5949
|
+
const text = await res.text(); if (process.env.ATEL_DEBUG) console.error("DEBUG Response:", text); const data = JSON.parse(text);
|
|
5918
5950
|
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
|
|
5919
5951
|
console.log(JSON.stringify(data, null, 2));
|
|
5920
5952
|
}
|
|
@@ -6619,7 +6651,7 @@ async function cmdDisputes() {
|
|
|
6619
6651
|
async function cmdDisputeInfo(disputeId) {
|
|
6620
6652
|
if (!disputeId) { console.error('Usage: atel dispute-info <disputeId>'); process.exit(1); }
|
|
6621
6653
|
const res = await fetch(`${PLATFORM_URL}/dispute/v1/${disputeId}`);
|
|
6622
|
-
const text = await res.text(); console.error("DEBUG Response:", text); const data = JSON.parse(text);
|
|
6654
|
+
const text = await res.text(); if (process.env.ATEL_DEBUG) console.error("DEBUG Response:", text); const data = JSON.parse(text);
|
|
6623
6655
|
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
|
|
6624
6656
|
console.log(JSON.stringify(data, null, 2));
|
|
6625
6657
|
}
|
|
@@ -6634,7 +6666,7 @@ async function cmdCertApply(level) {
|
|
|
6634
6666
|
async function cmdCertStatus(did) {
|
|
6635
6667
|
const targetDid = did || requireIdentity().did;
|
|
6636
6668
|
const res = await fetch(`${PLATFORM_URL}/cert/v1/status/${encodeURIComponent(targetDid)}`);
|
|
6637
|
-
const text = await res.text(); console.error("DEBUG Response:", text); const data = JSON.parse(text);
|
|
6669
|
+
const text = await res.text(); if (process.env.ATEL_DEBUG) console.error("DEBUG Response:", text); const data = JSON.parse(text);
|
|
6638
6670
|
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
|
|
6639
6671
|
console.log(JSON.stringify(data, null, 2));
|
|
6640
6672
|
|
|
@@ -6670,7 +6702,7 @@ async function cmdBoost(tier, weeks) {
|
|
|
6670
6702
|
async function cmdBoostStatus(did) {
|
|
6671
6703
|
const targetDid = did || requireIdentity().did;
|
|
6672
6704
|
const res = await fetch(`${PLATFORM_URL}/boost/v1/status/${encodeURIComponent(targetDid)}`);
|
|
6673
|
-
const text = await res.text(); console.error("DEBUG Response:", text); const data = JSON.parse(text);
|
|
6705
|
+
const text = await res.text(); if (process.env.ATEL_DEBUG) console.error("DEBUG Response:", text); const data = JSON.parse(text);
|
|
6674
6706
|
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
|
|
6675
6707
|
console.log(JSON.stringify(data, null, 2));
|
|
6676
6708
|
}
|
|
@@ -8100,6 +8132,7 @@ const commands = {
|
|
|
8100
8132
|
// Auth (Dashboard authorization code login)
|
|
8101
8133
|
auth: () => cmdAuth(args[0]),
|
|
8102
8134
|
// Send (Rich Media P2P Message)
|
|
8135
|
+
hub: () => cmdHub(args[0], args.slice(1), rawArgs),
|
|
8103
8136
|
send: () => {
|
|
8104
8137
|
if (rawArgs.includes('--help') || rawArgs.includes('-h') || args.length === 0) {
|
|
8105
8138
|
showSendHelp();
|
|
@@ -8305,6 +8338,15 @@ Account Commands:
|
|
|
8305
8338
|
withdraw <amount> [channel] [address] Withdraw funds (address required for crypto)
|
|
8306
8339
|
transactions List payment history
|
|
8307
8340
|
|
|
8341
|
+
Hub Commands:
|
|
8342
|
+
hub balance Show ATELToken balance
|
|
8343
|
+
hub usage [--model <id>] [--days 7] Usage history
|
|
8344
|
+
hub topup Top-up instructions
|
|
8345
|
+
hub swap <usdc> [--chain bsc|base] Swap USDC → ATELToken
|
|
8346
|
+
hub models [--search <kw>] List available models
|
|
8347
|
+
hub chat <model> "<prompt>" [--stream] Quick chat
|
|
8348
|
+
hub key <create|list|revoke|use> Manage TokenHub API keys
|
|
8349
|
+
|
|
8308
8350
|
Trade Commands:
|
|
8309
8351
|
trade-task <cap> <desc> [--budget N] One-shot: search → order → wait → confirm (requester)
|
|
8310
8352
|
order <executorDid> <cap> <price> Create a trade order
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
// hub-helpers.mjs — atel hub command implementation
|
|
2
|
+
// No new npm dependencies: uses native fetch, fs, crypto
|
|
3
|
+
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { createReadStream } from 'fs';
|
|
7
|
+
|
|
8
|
+
const HUB_CONFIG_PATH = join(process.env.HOME || '/root', '.atel', 'hub.json');
|
|
9
|
+
const DEFAULT_BASE = 'https://api.atelai.org/tokenhub/v1';
|
|
10
|
+
|
|
11
|
+
// ─── Config ──────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
function getConfig() {
|
|
14
|
+
const envKey = process.env.ATEL_HUB_KEY;
|
|
15
|
+
const envBase = process.env.ATEL_HUB_BASE || DEFAULT_BASE;
|
|
16
|
+
if (envKey) return { key: envKey, base: envBase };
|
|
17
|
+
if (!existsSync(HUB_CONFIG_PATH)) {
|
|
18
|
+
throw new Error('No hub API key configured. Run: atel hub key create');
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(readFileSync(HUB_CONFIG_PATH, 'utf-8'));
|
|
22
|
+
} catch {
|
|
23
|
+
throw new Error('Corrupted hub config. Run: atel hub key create');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function saveConfig(key, base) {
|
|
28
|
+
const dir = join(process.env.HOME || '/root', '.atel');
|
|
29
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
30
|
+
writeFileSync(
|
|
31
|
+
HUB_CONFIG_PATH,
|
|
32
|
+
JSON.stringify({ key, base: base || DEFAULT_BASE }),
|
|
33
|
+
{ mode: 0o600 }
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ─── HTTP ────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
async function hubFetch(path, options = {}) {
|
|
40
|
+
const cfg = getConfig();
|
|
41
|
+
const url = cfg.base + path;
|
|
42
|
+
const res = await fetch(url, {
|
|
43
|
+
...options,
|
|
44
|
+
headers: {
|
|
45
|
+
Authorization: 'Bearer ' + cfg.key,
|
|
46
|
+
'Content-Type': 'application/json',
|
|
47
|
+
...(options.headers || {}),
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
if (!res.ok) {
|
|
51
|
+
let errBody = {};
|
|
52
|
+
try { errBody = await res.json(); } catch {}
|
|
53
|
+
const code = errBody?.error?.code || 'ERROR';
|
|
54
|
+
const msg = errBody?.error?.message || res.statusText;
|
|
55
|
+
const err = new Error(`[${code}] ${msg}`);
|
|
56
|
+
err.httpStatus = res.status;
|
|
57
|
+
err.code = code;
|
|
58
|
+
if (errBody?.error?.action === 'topup') {
|
|
59
|
+
err.hint = 'Run: atel hub topup';
|
|
60
|
+
}
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
63
|
+
return res;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ─── Commands ────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
async function cmdHubSwap(usdcAmount, flags) {
|
|
69
|
+
if (!usdcAmount || isNaN(parseFloat(usdcAmount))) {
|
|
70
|
+
console.error('Usage: atel hub swap <usdc_amount> [--chain bsc|base]');
|
|
71
|
+
console.error('Example: atel hub swap 1.0 --chain bsc');
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
const amount = parseFloat(usdcAmount);
|
|
75
|
+
const chain = flags.chain || 'bsc';
|
|
76
|
+
|
|
77
|
+
// swap via platform (not tokenhub)
|
|
78
|
+
const { loadIdentity } = await import('./atel.mjs');
|
|
79
|
+
const id = loadIdentity();
|
|
80
|
+
const PLATFORM_URL = process.env.ATEL_PLATFORM || process.env.ATEL_REGISTRY || 'https://api.atelai.org';
|
|
81
|
+
|
|
82
|
+
// Use signedFetch from atel.mjs
|
|
83
|
+
const body = JSON.stringify({ usdc_amount: amount, chain });
|
|
84
|
+
const res = await fetch(PLATFORM_URL + '/account/v1/swap', {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
headers: { 'Content-Type': 'application/json' },
|
|
87
|
+
body,
|
|
88
|
+
signal: AbortSignal.timeout(10000),
|
|
89
|
+
});
|
|
90
|
+
const data = await res.json();
|
|
91
|
+
if (!res.ok) {
|
|
92
|
+
console.error('Swap failed:', data.error || JSON.stringify(data));
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
const tokenPerUSDC = data.token_per_usdc || 10000;
|
|
96
|
+
console.log('✓ Swap successful');
|
|
97
|
+
console.log(` USDC spent: $${amount.toFixed(6)}`);
|
|
98
|
+
console.log(` ATELToken received: ${data.token_amount.toLocaleString()}`);
|
|
99
|
+
console.log(` Rate: 1 USDC = ${tokenPerUSDC.toLocaleString()} ATEL`);
|
|
100
|
+
console.log(` Platform balance after: $${parseFloat(data.balance_after).toFixed(6)} USDC`);
|
|
101
|
+
console.log('');
|
|
102
|
+
console.log('Run `atel hub balance` to check your ATELToken balance.');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function cmdHubBalance() {
|
|
106
|
+
const res = await hubFetch('/balance');
|
|
107
|
+
const data = await res.json();
|
|
108
|
+
console.log(`ATELToken Balance: ${data.balance.toLocaleString()}`);
|
|
109
|
+
console.log(`USDC Equivalent: $${data.usdc_equiv}`);
|
|
110
|
+
if (data.overdraft) {
|
|
111
|
+
console.warn('⚠ Account is in overdraft. Top up to resume service.');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function cmdHubUsage(flags) {
|
|
116
|
+
const params = new URLSearchParams();
|
|
117
|
+
const modelIdx = flags.indexOf('--model');
|
|
118
|
+
if (modelIdx !== -1 && flags[modelIdx + 1]) params.set('model', flags[modelIdx + 1]);
|
|
119
|
+
const daysIdx = flags.indexOf('--days');
|
|
120
|
+
if (daysIdx !== -1 && flags[daysIdx + 1]) {
|
|
121
|
+
const days = parseInt(flags[daysIdx + 1], 10);
|
|
122
|
+
const start = new Date(Date.now() - days * 86400000).toISOString().slice(0, 10);
|
|
123
|
+
params.set('start', start);
|
|
124
|
+
}
|
|
125
|
+
const res = await hubFetch('/usage?' + params.toString());
|
|
126
|
+
const data = await res.json();
|
|
127
|
+
console.log(`Usage (${data.total} total, showing page ${data.page}):`);
|
|
128
|
+
if (data.items.length === 0) {
|
|
129
|
+
console.log(' No usage records found.');
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
console.log(' Time Model In Out Cost');
|
|
133
|
+
console.log(' ' + '-'.repeat(80));
|
|
134
|
+
for (const item of data.items) {
|
|
135
|
+
const t = new Date(item.created_at).toLocaleString();
|
|
136
|
+
const model = item.model_id.padEnd(30);
|
|
137
|
+
console.log(` ${t} ${model} ${String(item.tokens_in).padStart(6)} ${String(item.tokens_out).padStart(6)} ${item.cost}`);
|
|
138
|
+
}
|
|
139
|
+
const totalCost = data.items.reduce((s, i) => s + i.cost, 0);
|
|
140
|
+
console.log(' ' + '-'.repeat(80));
|
|
141
|
+
console.log(` Total cost this page: ${totalCost} ATELToken`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function cmdHubTopup() {
|
|
145
|
+
// Show topup instructions (chain-based, no automation yet)
|
|
146
|
+
const res = await hubFetch('/balance');
|
|
147
|
+
const data = await res.json();
|
|
148
|
+
console.log('Current balance: ' + data.balance.toLocaleString() + ' ATELToken ($' + data.usdc_equiv + ')');
|
|
149
|
+
console.log('');
|
|
150
|
+
console.log('To top up, send USDC to your ATEL deposit address.');
|
|
151
|
+
console.log('Exchange rate: 1 USDC = 10,000 ATELToken');
|
|
152
|
+
console.log('');
|
|
153
|
+
console.log('Contact your platform admin or visit https://atelai.org/topup for instructions.');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function cmdHubKeyCreate(flags) {
|
|
157
|
+
const nameIdx = flags.indexOf('--name');
|
|
158
|
+
const name = nameIdx !== -1 && flags[nameIdx + 1] ? flags[nameIdx + 1] : 'default';
|
|
159
|
+
const res = await hubFetch('/apikeys', {
|
|
160
|
+
method: 'POST',
|
|
161
|
+
body: JSON.stringify({ name }),
|
|
162
|
+
});
|
|
163
|
+
const data = await res.json();
|
|
164
|
+
console.log('API Key created:');
|
|
165
|
+
console.log(' ' + data.key);
|
|
166
|
+
console.log('');
|
|
167
|
+
console.log('This key will NOT be shown again. Save it securely.');
|
|
168
|
+
console.log('Saving to ~/.atel/hub.json ...');
|
|
169
|
+
const cfg = getConfig();
|
|
170
|
+
saveConfig(data.key, cfg.base);
|
|
171
|
+
console.log('Saved. You can now use atel hub commands.');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function cmdHubKeyList() {
|
|
175
|
+
const res = await hubFetch('/apikeys');
|
|
176
|
+
const data = await res.json();
|
|
177
|
+
const keys = data.keys || [];
|
|
178
|
+
if (keys.length === 0) {
|
|
179
|
+
console.log('No API keys found.');
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
console.log(`API Keys (${keys.length}):`);
|
|
183
|
+
for (const k of keys) {
|
|
184
|
+
const revoked = k.Revoked || k.revoked;
|
|
185
|
+
const status = revoked ? '[revoked]' : '[active] ';
|
|
186
|
+
const created = new Date(k.CreatedAt || k.created_at).toLocaleDateString();
|
|
187
|
+
const id = k.ID || k.id;
|
|
188
|
+
const prefix = k.KeyPrefix || k.key_prefix || '';
|
|
189
|
+
const name = k.Name || k.name;
|
|
190
|
+
console.log(` ${status} id=${id} prefix=${prefix}... name=${name} created=${created}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function cmdHubKeyRevoke(id) {
|
|
195
|
+
if (!id) {
|
|
196
|
+
console.error('Usage: atel hub key revoke <id>');
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
await hubFetch(`/apikeys/${id}`, { method: 'DELETE' });
|
|
200
|
+
console.log(`Key ${id} revoked successfully.`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function cmdHubKeyUse() {
|
|
204
|
+
const cfg = getConfig();
|
|
205
|
+
console.log(`export OPENAI_BASE_URL=${cfg.base}`);
|
|
206
|
+
console.log(`export OPENAI_API_KEY=${cfg.key}`);
|
|
207
|
+
console.log('');
|
|
208
|
+
console.log('# Tip: eval $(atel hub key use) to apply in current shell');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function cmdHubModels(flags) {
|
|
212
|
+
const searchIdx = flags.indexOf('--search');
|
|
213
|
+
const keyword = searchIdx !== -1 && flags[searchIdx + 1] ? flags[searchIdx + 1].toLowerCase() : '';
|
|
214
|
+
const res = await hubFetch('/models');
|
|
215
|
+
const data = await res.json();
|
|
216
|
+
let models = data.models || [];
|
|
217
|
+
if (keyword) {
|
|
218
|
+
models = models.filter(m => m.model_id.toLowerCase().includes(keyword));
|
|
219
|
+
}
|
|
220
|
+
if (models.length === 0) {
|
|
221
|
+
console.log('No models found.');
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
console.log(`Available Models (${models.length}):`);
|
|
225
|
+
console.log(' Model ID Rate Cost/1M tokens');
|
|
226
|
+
console.log(' ' + '-'.repeat(75));
|
|
227
|
+
for (const m of models) {
|
|
228
|
+
const rate = (m.multiplier_milli / 1000).toFixed(3) + 'x';
|
|
229
|
+
const costUSD = '$' + (m.multiplier_milli / 10).toFixed(2);
|
|
230
|
+
console.log(` ${m.model_id.padEnd(50)} ${rate.padStart(7)} ${costUSD}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function cmdHubChat(model, prompt, flags) {
|
|
235
|
+
if (!model || !prompt) {
|
|
236
|
+
console.error('Usage: atel hub chat <model> "<prompt>" [--stream]');
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
const stream = flags.includes('--stream');
|
|
240
|
+
const cfg = getConfig();
|
|
241
|
+
const body = JSON.stringify({
|
|
242
|
+
model,
|
|
243
|
+
messages: [{ role: 'user', content: prompt }],
|
|
244
|
+
stream,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const res = await fetch(cfg.base + '/chat/completions', {
|
|
248
|
+
method: 'POST',
|
|
249
|
+
headers: {
|
|
250
|
+
Authorization: 'Bearer ' + cfg.key,
|
|
251
|
+
'Content-Type': 'application/json',
|
|
252
|
+
},
|
|
253
|
+
body,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
if (!res.ok) {
|
|
257
|
+
let errBody = {};
|
|
258
|
+
try { errBody = await res.json(); } catch {}
|
|
259
|
+
const msg = errBody?.error?.message || res.statusText;
|
|
260
|
+
const code = errBody?.error?.code || 'ERROR';
|
|
261
|
+
console.error(`[${code}] ${msg}`);
|
|
262
|
+
if (errBody?.error?.action === 'topup') console.error('Run: atel hub topup');
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (!stream) {
|
|
267
|
+
const data = await res.json();
|
|
268
|
+
const content = data?.choices?.[0]?.message?.content || '';
|
|
269
|
+
console.log(content);
|
|
270
|
+
if (data.usage) {
|
|
271
|
+
const cost = res.headers.get('X-ATELToken-Cost');
|
|
272
|
+
console.error(`\n[tokens: in=${data.usage.prompt_tokens} out=${data.usage.completion_tokens}${cost ? ' cost=' + cost : ''}]`);
|
|
273
|
+
}
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Streaming: parse SSE
|
|
278
|
+
const reader = res.body.getReader();
|
|
279
|
+
const decoder = new TextDecoder();
|
|
280
|
+
let buf = '';
|
|
281
|
+
while (true) {
|
|
282
|
+
const { done, value } = await reader.read();
|
|
283
|
+
if (done) break;
|
|
284
|
+
buf += decoder.decode(value, { stream: true });
|
|
285
|
+
const lines = buf.split('\n');
|
|
286
|
+
buf = lines.pop(); // keep incomplete line
|
|
287
|
+
for (const line of lines) {
|
|
288
|
+
if (!line.startsWith('data: ')) continue;
|
|
289
|
+
const data = line.slice(6).trim();
|
|
290
|
+
if (data === '[DONE]') { process.stdout.write('\n'); return; }
|
|
291
|
+
try {
|
|
292
|
+
const chunk = JSON.parse(data);
|
|
293
|
+
const delta = chunk?.choices?.[0]?.delta?.content;
|
|
294
|
+
if (delta) process.stdout.write(delta);
|
|
295
|
+
} catch {}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ─── Router ──────────────────────────────────────────────────────
|
|
301
|
+
|
|
302
|
+
export async function cmdHub(sub, args, rawArgs) {
|
|
303
|
+
const flags = rawArgs || [];
|
|
304
|
+
try {
|
|
305
|
+
switch (sub) {
|
|
306
|
+
case 'balance': return await cmdHubBalance();
|
|
307
|
+
case 'swap': return await cmdHubSwap(args[0], flags);
|
|
308
|
+
case 'usage': return await cmdHubUsage(flags);
|
|
309
|
+
case 'topup': return await cmdHubTopup();
|
|
310
|
+
case 'models': return await cmdHubModels(flags);
|
|
311
|
+
case 'chat': return await cmdHubChat(args[0], args[1], flags);
|
|
312
|
+
case 'key': {
|
|
313
|
+
const keySub = args[0];
|
|
314
|
+
const keyFlags = flags;
|
|
315
|
+
switch (keySub) {
|
|
316
|
+
case 'create': return await cmdHubKeyCreate(keyFlags);
|
|
317
|
+
case 'list': return await cmdHubKeyList();
|
|
318
|
+
case 'revoke': return await cmdHubKeyRevoke(args[1]);
|
|
319
|
+
case 'use': return await cmdHubKeyUse();
|
|
320
|
+
default:
|
|
321
|
+
console.log('Usage: atel hub key <create|list|revoke|use>');
|
|
322
|
+
process.exit(1);
|
|
323
|
+
}
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
default:
|
|
327
|
+
console.log(`
|
|
328
|
+
atel hub — ATELToken management
|
|
329
|
+
|
|
330
|
+
Commands:
|
|
331
|
+
atel hub balance Show ATELToken balance
|
|
332
|
+
atel hub usage [--model <id>] [--days 7] Usage history
|
|
333
|
+
atel hub topup Top-up instructions
|
|
334
|
+
atel hub swap <usdc> [--chain bsc|base] Swap USDC → ATELToken
|
|
335
|
+
atel hub models [--search <kw>] List available models
|
|
336
|
+
atel hub chat <model> "<prompt>" [--stream] Quick chat
|
|
337
|
+
atel hub key create [--name <name>] Create API key
|
|
338
|
+
atel hub key list List API keys
|
|
339
|
+
atel hub key revoke <id> Revoke a key
|
|
340
|
+
atel hub key use Output env vars for external tools
|
|
341
|
+
`);
|
|
342
|
+
}
|
|
343
|
+
} catch (err) {
|
|
344
|
+
console.error(err.message);
|
|
345
|
+
if (err.hint) console.error(err.hint);
|
|
346
|
+
process.exit(1);
|
|
347
|
+
}
|
|
348
|
+
}
|