@lawrenceliang-btc/atel-sdk 1.1.23 → 1.1.25
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 +56 -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,35 @@ 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
|
+
installed = true;
|
|
2080
|
+
break;
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
if (!installed) {
|
|
2084
|
+
console.log('\n📋 To teach your AI agent about ATEL, send it this file:');
|
|
2085
|
+
console.log(` ${sdkSkillPath}`);
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
} catch (e) { /* skill install is best-effort */ }
|
|
2060
2089
|
}
|
|
2061
2090
|
|
|
2062
2091
|
async function cmdAnchor(subcommand) {
|
|
@@ -2386,38 +2415,38 @@ async function cmdStart(port) {
|
|
|
2386
2415
|
|
|
2387
2416
|
async function verifyAnchorFromChain(chain, txHash, traceRoot) {
|
|
2388
2417
|
try {
|
|
2389
|
-
console.error('[DEBUG] verifyAnchorFromChain input:', { chain, txHash, traceRoot });
|
|
2418
|
+
if (process.env.ATEL_DEBUG) console.error('[DEBUG] verifyAnchorFromChain input:', { chain, txHash, traceRoot });
|
|
2390
2419
|
|
|
2391
2420
|
if (!txHash || !traceRoot) return { checked: false, verified: false, reason: 'missing_anchor_or_root' };
|
|
2392
2421
|
const c = (chain || 'solana').toLowerCase();
|
|
2393
2422
|
|
|
2394
2423
|
if (c === 'solana') {
|
|
2395
2424
|
const rpcUrl = process.env.ATEL_SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com';
|
|
2396
|
-
console.error('[DEBUG] Solana RPC URL:', rpcUrl);
|
|
2425
|
+
if (process.env.ATEL_DEBUG) console.error('[DEBUG] Solana RPC URL:', rpcUrl);
|
|
2397
2426
|
const provider = new SolanaAnchorProvider({ rpcUrl });
|
|
2398
2427
|
const r = await provider.verify(traceRoot, txHash);
|
|
2399
|
-
console.error('[DEBUG] Solana verify result:', r);
|
|
2428
|
+
if (process.env.ATEL_DEBUG) console.error('[DEBUG] Solana verify result:', r);
|
|
2400
2429
|
return { checked: true, verified: !!r?.valid, chain: 'solana', detail: r?.detail };
|
|
2401
2430
|
}
|
|
2402
2431
|
if (c === 'base') {
|
|
2403
2432
|
const rpcUrl = process.env.ATEL_BASE_RPC_URL || 'https://mainnet.base.org';
|
|
2404
|
-
console.error('[DEBUG] Base RPC URL:', rpcUrl);
|
|
2433
|
+
if (process.env.ATEL_DEBUG) console.error('[DEBUG] Base RPC URL:', rpcUrl);
|
|
2405
2434
|
const provider = new BaseAnchorProvider({ rpcUrl });
|
|
2406
2435
|
const r = await provider.verify(traceRoot, txHash);
|
|
2407
|
-
console.error('[DEBUG] Base verify result:', r);
|
|
2436
|
+
if (process.env.ATEL_DEBUG) console.error('[DEBUG] Base verify result:', r);
|
|
2408
2437
|
return { checked: true, verified: !!r?.valid, chain: 'base', detail: r?.detail };
|
|
2409
2438
|
}
|
|
2410
2439
|
if (c === 'bsc') {
|
|
2411
2440
|
const rpcUrl = process.env.ATEL_BSC_RPC_URL || 'https://bsc-dataseed.binance.org';
|
|
2412
|
-
console.error('[DEBUG] BSC RPC URL:', rpcUrl);
|
|
2441
|
+
if (process.env.ATEL_DEBUG) console.error('[DEBUG] BSC RPC URL:', rpcUrl);
|
|
2413
2442
|
const provider = new BSCAnchorProvider({ rpcUrl });
|
|
2414
2443
|
const r = await provider.verify(traceRoot, txHash);
|
|
2415
|
-
console.error('[DEBUG] BSC verify result:', r);
|
|
2444
|
+
if (process.env.ATEL_DEBUG) console.error('[DEBUG] BSC verify result:', r);
|
|
2416
2445
|
return { checked: true, verified: !!r?.valid, chain: 'bsc', detail: r?.detail };
|
|
2417
2446
|
}
|
|
2418
2447
|
return { checked: false, verified: false, reason: `unsupported_chain:${c}` };
|
|
2419
2448
|
} catch (e) {
|
|
2420
|
-
console.error('[DEBUG] verifyAnchorFromChain error:', e);
|
|
2449
|
+
if (process.env.ATEL_DEBUG) console.error('[DEBUG] verifyAnchorFromChain error:', e);
|
|
2421
2450
|
return { checked: true, verified: false, reason: e.message };
|
|
2422
2451
|
}
|
|
2423
2452
|
}
|
|
@@ -5608,8 +5637,8 @@ async function signedFetch(method, path, payload = {}) {
|
|
|
5608
5637
|
const sig = Buffer.from(nacl.sign.detached(Buffer.from(signable), id.secretKey)).toString('base64');
|
|
5609
5638
|
const body = JSON.stringify({ did: id.did, payload, timestamp: ts, signature: sig });
|
|
5610
5639
|
// 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);
|
|
5640
|
+
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 });
|
|
5641
|
+
const text = await res.text(); if (process.env.ATEL_DEBUG) console.error("DEBUG Response:", text); const data = JSON.parse(text);
|
|
5613
5642
|
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
|
|
5614
5643
|
return data;
|
|
5615
5644
|
}
|
|
@@ -5914,7 +5943,7 @@ async function cmdOrder(executorDid, capType, price) {
|
|
|
5914
5943
|
async function cmdOrderInfo(orderId) {
|
|
5915
5944
|
if (!orderId) { console.error('Usage: atel order-info <orderId>'); process.exit(1); }
|
|
5916
5945
|
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);
|
|
5946
|
+
const text = await res.text(); if (process.env.ATEL_DEBUG) console.error("DEBUG Response:", text); const data = JSON.parse(text);
|
|
5918
5947
|
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
|
|
5919
5948
|
console.log(JSON.stringify(data, null, 2));
|
|
5920
5949
|
}
|
|
@@ -6619,7 +6648,7 @@ async function cmdDisputes() {
|
|
|
6619
6648
|
async function cmdDisputeInfo(disputeId) {
|
|
6620
6649
|
if (!disputeId) { console.error('Usage: atel dispute-info <disputeId>'); process.exit(1); }
|
|
6621
6650
|
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);
|
|
6651
|
+
const text = await res.text(); if (process.env.ATEL_DEBUG) console.error("DEBUG Response:", text); const data = JSON.parse(text);
|
|
6623
6652
|
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
|
|
6624
6653
|
console.log(JSON.stringify(data, null, 2));
|
|
6625
6654
|
}
|
|
@@ -6634,7 +6663,7 @@ async function cmdCertApply(level) {
|
|
|
6634
6663
|
async function cmdCertStatus(did) {
|
|
6635
6664
|
const targetDid = did || requireIdentity().did;
|
|
6636
6665
|
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);
|
|
6666
|
+
const text = await res.text(); if (process.env.ATEL_DEBUG) console.error("DEBUG Response:", text); const data = JSON.parse(text);
|
|
6638
6667
|
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
|
|
6639
6668
|
console.log(JSON.stringify(data, null, 2));
|
|
6640
6669
|
|
|
@@ -6670,7 +6699,7 @@ async function cmdBoost(tier, weeks) {
|
|
|
6670
6699
|
async function cmdBoostStatus(did) {
|
|
6671
6700
|
const targetDid = did || requireIdentity().did;
|
|
6672
6701
|
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);
|
|
6702
|
+
const text = await res.text(); if (process.env.ATEL_DEBUG) console.error("DEBUG Response:", text); const data = JSON.parse(text);
|
|
6674
6703
|
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
|
|
6675
6704
|
console.log(JSON.stringify(data, null, 2));
|
|
6676
6705
|
}
|
|
@@ -8100,6 +8129,7 @@ const commands = {
|
|
|
8100
8129
|
// Auth (Dashboard authorization code login)
|
|
8101
8130
|
auth: () => cmdAuth(args[0]),
|
|
8102
8131
|
// Send (Rich Media P2P Message)
|
|
8132
|
+
hub: () => cmdHub(args[0], args.slice(1), rawArgs),
|
|
8103
8133
|
send: () => {
|
|
8104
8134
|
if (rawArgs.includes('--help') || rawArgs.includes('-h') || args.length === 0) {
|
|
8105
8135
|
showSendHelp();
|
|
@@ -8305,6 +8335,15 @@ Account Commands:
|
|
|
8305
8335
|
withdraw <amount> [channel] [address] Withdraw funds (address required for crypto)
|
|
8306
8336
|
transactions List payment history
|
|
8307
8337
|
|
|
8338
|
+
Hub Commands:
|
|
8339
|
+
hub balance Show ATELToken balance
|
|
8340
|
+
hub usage [--model <id>] [--days 7] Usage history
|
|
8341
|
+
hub topup Top-up instructions
|
|
8342
|
+
hub swap <usdc> [--chain bsc|base] Swap USDC → ATELToken
|
|
8343
|
+
hub models [--search <kw>] List available models
|
|
8344
|
+
hub chat <model> "<prompt>" [--stream] Quick chat
|
|
8345
|
+
hub key <create|list|revoke|use> Manage TokenHub API keys
|
|
8346
|
+
|
|
8308
8347
|
Trade Commands:
|
|
8309
8348
|
trade-task <cap> <desc> [--budget N] One-shot: search → order → wait → confirm (requester)
|
|
8310
8349
|
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
|
+
}
|