@t2000/cli 0.17.29 → 0.18.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/README.md +65 -22
- package/dist/chunk-FILMHRDQ.js +2055 -0
- package/dist/chunk-FILMHRDQ.js.map +1 -0
- package/dist/chunk-G2C4JKWH.js +5337 -0
- package/dist/chunk-G2C4JKWH.js.map +1 -0
- package/dist/{chunk-D4HDZEJT.js → chunk-MKTCPK75.js} +16 -3
- package/dist/{dist-F6YRISIO.js → dist-6ADOH23K.js} +2853 -7983
- package/dist/dist-6ADOH23K.js.map +1 -0
- package/dist/dist-IHO4UPEB.js +26875 -0
- package/dist/dist-IHO4UPEB.js.map +1 -0
- package/dist/fileFromPath-WHCJFE2P.js +131 -0
- package/dist/fileFromPath-WHCJFE2P.js.map +1 -0
- package/dist/index.js +3196 -2
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/dist/commands/address.d.ts +0 -3
- package/dist/commands/address.d.ts.map +0 -1
- package/dist/commands/address.js +0 -26
- package/dist/commands/address.js.map +0 -1
- package/dist/commands/balance.d.ts +0 -3
- package/dist/commands/balance.d.ts.map +0 -1
- package/dist/commands/balance.js +0 -134
- package/dist/commands/balance.js.map +0 -1
- package/dist/commands/borrow.d.ts +0 -3
- package/dist/commands/borrow.d.ts.map +0 -1
- package/dist/commands/borrow.js +0 -40
- package/dist/commands/borrow.js.map +0 -1
- package/dist/commands/claimRewards.d.ts +0 -3
- package/dist/commands/claimRewards.d.ts.map +0 -1
- package/dist/commands/claimRewards.js +0 -49
- package/dist/commands/claimRewards.js.map +0 -1
- package/dist/commands/config.d.ts +0 -3
- package/dist/commands/config.d.ts.map +0 -1
- package/dist/commands/config.js +0 -139
- package/dist/commands/config.js.map +0 -1
- package/dist/commands/contacts.d.ts +0 -3
- package/dist/commands/contacts.d.ts.map +0 -1
- package/dist/commands/contacts.js +0 -83
- package/dist/commands/contacts.js.map +0 -1
- package/dist/commands/deposit.d.ts +0 -3
- package/dist/commands/deposit.d.ts.map +0 -1
- package/dist/commands/deposit.js +0 -27
- package/dist/commands/deposit.js.map +0 -1
- package/dist/commands/earn.d.ts +0 -3
- package/dist/commands/earn.d.ts.map +0 -1
- package/dist/commands/earn.js +0 -186
- package/dist/commands/earn.js.map +0 -1
- package/dist/commands/earnings.d.ts +0 -3
- package/dist/commands/earnings.d.ts.map +0 -1
- package/dist/commands/earnings.js +0 -38
- package/dist/commands/earnings.js.map +0 -1
- package/dist/commands/exchange.d.ts +0 -3
- package/dist/commands/exchange.d.ts.map +0 -1
- package/dist/commands/exchange.js +0 -45
- package/dist/commands/exchange.js.map +0 -1
- package/dist/commands/exportKey.d.ts +0 -3
- package/dist/commands/exportKey.d.ts.map +0 -1
- package/dist/commands/exportKey.js +0 -81
- package/dist/commands/exportKey.js.map +0 -1
- package/dist/commands/fundStatus.d.ts +0 -3
- package/dist/commands/fundStatus.d.ts.map +0 -1
- package/dist/commands/fundStatus.js +0 -50
- package/dist/commands/fundStatus.js.map +0 -1
- package/dist/commands/health.d.ts +0 -3
- package/dist/commands/health.d.ts.map +0 -1
- package/dist/commands/health.js +0 -44
- package/dist/commands/health.js.map +0 -1
- package/dist/commands/history.d.ts +0 -3
- package/dist/commands/history.d.ts.map +0 -1
- package/dist/commands/history.js +0 -37
- package/dist/commands/history.js.map +0 -1
- package/dist/commands/importKey.d.ts +0 -3
- package/dist/commands/importKey.d.ts.map +0 -1
- package/dist/commands/importKey.js +0 -39
- package/dist/commands/importKey.js.map +0 -1
- package/dist/commands/init.d.ts +0 -3
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js +0 -56
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/invest.d.ts +0 -3
- package/dist/commands/invest.d.ts.map +0 -1
- package/dist/commands/invest.js +0 -637
- package/dist/commands/invest.js.map +0 -1
- package/dist/commands/lock.d.ts +0 -3
- package/dist/commands/lock.d.ts.map +0 -1
- package/dist/commands/lock.js +0 -66
- package/dist/commands/lock.js.map +0 -1
- package/dist/commands/mcp.d.ts +0 -3
- package/dist/commands/mcp.d.ts.map +0 -1
- package/dist/commands/mcp.js +0 -141
- package/dist/commands/mcp.js.map +0 -1
- package/dist/commands/pay.d.ts +0 -3
- package/dist/commands/pay.d.ts.map +0 -1
- package/dist/commands/pay.js +0 -96
- package/dist/commands/pay.js.map +0 -1
- package/dist/commands/portfolio.d.ts +0 -3
- package/dist/commands/portfolio.d.ts.map +0 -1
- package/dist/commands/portfolio.js +0 -104
- package/dist/commands/portfolio.js.map +0 -1
- package/dist/commands/positions.d.ts +0 -3
- package/dist/commands/positions.d.ts.map +0 -1
- package/dist/commands/positions.js +0 -73
- package/dist/commands/positions.js.map +0 -1
- package/dist/commands/rates.d.ts +0 -3
- package/dist/commands/rates.d.ts.map +0 -1
- package/dist/commands/rates.js +0 -64
- package/dist/commands/rates.js.map +0 -1
- package/dist/commands/rebalance.d.ts +0 -3
- package/dist/commands/rebalance.d.ts.map +0 -1
- package/dist/commands/rebalance.js +0 -109
- package/dist/commands/rebalance.js.map +0 -1
- package/dist/commands/repay.d.ts +0 -3
- package/dist/commands/repay.d.ts.map +0 -1
- package/dist/commands/repay.js +0 -35
- package/dist/commands/repay.js.map +0 -1
- package/dist/commands/save.d.ts +0 -3
- package/dist/commands/save.d.ts.map +0 -1
- package/dist/commands/save.js +0 -57
- package/dist/commands/save.js.map +0 -1
- package/dist/commands/send.d.ts +0 -3
- package/dist/commands/send.d.ts.map +0 -1
- package/dist/commands/send.js +0 -52
- package/dist/commands/send.js.map +0 -1
- package/dist/commands/sentinel.d.ts +0 -3
- package/dist/commands/sentinel.d.ts.map +0 -1
- package/dist/commands/sentinel.js +0 -142
- package/dist/commands/sentinel.js.map +0 -1
- package/dist/commands/serve.d.ts +0 -3
- package/dist/commands/serve.d.ts.map +0 -1
- package/dist/commands/serve.js +0 -341
- package/dist/commands/serve.js.map +0 -1
- package/dist/commands/withdraw.d.ts +0 -3
- package/dist/commands/withdraw.d.ts.map +0 -1
- package/dist/commands/withdraw.js +0 -34
- package/dist/commands/withdraw.js.map +0 -1
- package/dist/dist-F6YRISIO.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/output.d.ts +0 -16
- package/dist/output.d.ts.map +0 -1
- package/dist/output.js +0 -85
- package/dist/output.js.map +0 -1
- package/dist/program.d.ts +0 -3
- package/dist/program.d.ts.map +0 -1
- package/dist/program.js +0 -80
- package/dist/program.js.map +0 -1
- package/dist/prompts.d.ts +0 -16
- package/dist/prompts.d.ts.map +0 -1
- package/dist/prompts.js +0 -69
- package/dist/prompts.js.map +0 -1
- /package/dist/{chunk-D4HDZEJT.js.map → chunk-MKTCPK75.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,5 +1,3199 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
#!/usr/bin/env node
|
|
3
|
+
import "./chunk-MKTCPK75.js";
|
|
4
|
+
|
|
5
|
+
// src/program.ts
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import { createRequire } from "module";
|
|
8
|
+
|
|
9
|
+
// src/output.ts
|
|
10
|
+
import pc from "picocolors";
|
|
11
|
+
var jsonMode = false;
|
|
12
|
+
function setJsonMode(enabled) {
|
|
13
|
+
jsonMode = enabled;
|
|
14
|
+
}
|
|
15
|
+
function isJsonMode() {
|
|
16
|
+
return jsonMode;
|
|
17
|
+
}
|
|
18
|
+
function printJson(data) {
|
|
19
|
+
console.log(JSON.stringify(data, null, 2));
|
|
20
|
+
}
|
|
21
|
+
function printSuccess(message) {
|
|
22
|
+
if (jsonMode) return;
|
|
23
|
+
console.log(` ${pc.green("\u2713")} ${message}`);
|
|
24
|
+
}
|
|
25
|
+
function printError(message) {
|
|
26
|
+
if (jsonMode) return;
|
|
27
|
+
console.error(` ${pc.red("\u2717")} ${message}`);
|
|
28
|
+
}
|
|
29
|
+
function printWarning(message) {
|
|
30
|
+
if (jsonMode) return;
|
|
31
|
+
console.log(` ${pc.yellow("\u26A0")} ${message}`);
|
|
32
|
+
}
|
|
33
|
+
function printInfo(message) {
|
|
34
|
+
if (jsonMode) return;
|
|
35
|
+
console.log(` ${pc.dim(message)}`);
|
|
36
|
+
}
|
|
37
|
+
function printHeader(title) {
|
|
38
|
+
if (jsonMode) return;
|
|
39
|
+
console.log();
|
|
40
|
+
console.log(` ${pc.bold(title)}`);
|
|
41
|
+
console.log();
|
|
42
|
+
}
|
|
43
|
+
function printKeyValue(key, value, indent = 2) {
|
|
44
|
+
if (jsonMode) return;
|
|
45
|
+
const pad = " ".repeat(indent);
|
|
46
|
+
console.log(`${pad}${pc.dim(key + ":")} ${value}`);
|
|
47
|
+
}
|
|
48
|
+
function printBlank() {
|
|
49
|
+
if (jsonMode) return;
|
|
50
|
+
console.log();
|
|
51
|
+
}
|
|
52
|
+
function printDivider(width = 53) {
|
|
53
|
+
if (jsonMode) return;
|
|
54
|
+
console.log(` ${pc.dim("\u2500".repeat(width))}`);
|
|
55
|
+
}
|
|
56
|
+
function printLine(text) {
|
|
57
|
+
if (jsonMode) return;
|
|
58
|
+
console.log(` ${text}`);
|
|
59
|
+
}
|
|
60
|
+
function printSeparator() {
|
|
61
|
+
if (jsonMode) return;
|
|
62
|
+
console.log(` ${pc.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")}`);
|
|
63
|
+
}
|
|
64
|
+
function explorerUrl(txHash, network = "mainnet") {
|
|
65
|
+
const base = network === "testnet" ? "https://suiscan.xyz/testnet/tx" : "https://suiscan.xyz/mainnet/tx";
|
|
66
|
+
const suffix = "";
|
|
67
|
+
return `${base}/${txHash}${suffix}`;
|
|
68
|
+
}
|
|
69
|
+
function handleError(error) {
|
|
70
|
+
if (jsonMode) {
|
|
71
|
+
const data = error instanceof Error && "toJSON" in error ? error.toJSON() : { error: "UNKNOWN", message: String(error) };
|
|
72
|
+
printJson(data);
|
|
73
|
+
} else {
|
|
74
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
75
|
+
printError(msg);
|
|
76
|
+
}
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/commands/init.ts
|
|
81
|
+
import pc2 from "picocolors";
|
|
82
|
+
import { T2000, walletExists, SafeguardEnforcer } from "@t2000/sdk";
|
|
83
|
+
|
|
84
|
+
// src/prompts.ts
|
|
85
|
+
import { password, confirm } from "@inquirer/prompts";
|
|
86
|
+
import { readFile, writeFile, unlink, mkdir } from "fs/promises";
|
|
87
|
+
import { resolve } from "path";
|
|
88
|
+
import { homedir } from "os";
|
|
89
|
+
var SESSION_PATH = resolve(homedir(), ".t2000", ".session");
|
|
90
|
+
var MIN_PIN_LENGTH = 4;
|
|
91
|
+
async function askPin(message = "Enter PIN:") {
|
|
92
|
+
const value = await password({ message });
|
|
93
|
+
if (!value || value.length < MIN_PIN_LENGTH) {
|
|
94
|
+
throw new Error(`PIN must be at least ${MIN_PIN_LENGTH} characters`);
|
|
95
|
+
}
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
async function askPinConfirm() {
|
|
99
|
+
const pin = await password({ message: `Create PIN (min ${MIN_PIN_LENGTH} chars):` });
|
|
100
|
+
if (!pin || pin.length < MIN_PIN_LENGTH) {
|
|
101
|
+
throw new Error(`PIN must be at least ${MIN_PIN_LENGTH} characters`);
|
|
102
|
+
}
|
|
103
|
+
const confirm_ = await password({ message: "Confirm PIN:" });
|
|
104
|
+
if (pin !== confirm_) {
|
|
105
|
+
throw new Error("PINs do not match");
|
|
106
|
+
}
|
|
107
|
+
return pin;
|
|
108
|
+
}
|
|
109
|
+
async function askConfirm(message) {
|
|
110
|
+
return confirm({ message });
|
|
111
|
+
}
|
|
112
|
+
function getPinFromEnv() {
|
|
113
|
+
return process.env.T2000_PIN ?? process.env.T2000_PASSPHRASE;
|
|
114
|
+
}
|
|
115
|
+
async function readSession() {
|
|
116
|
+
try {
|
|
117
|
+
const content = await readFile(SESSION_PATH, "utf-8");
|
|
118
|
+
return content.trim() || void 0;
|
|
119
|
+
} catch {
|
|
120
|
+
return void 0;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async function saveSession(pin) {
|
|
124
|
+
await mkdir(resolve(homedir(), ".t2000"), { recursive: true });
|
|
125
|
+
await writeFile(SESSION_PATH, pin, { mode: 384 });
|
|
126
|
+
}
|
|
127
|
+
async function clearSession() {
|
|
128
|
+
try {
|
|
129
|
+
await unlink(SESSION_PATH);
|
|
130
|
+
} catch {
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function resolvePin(opts) {
|
|
134
|
+
const envPin = getPinFromEnv();
|
|
135
|
+
if (envPin) return envPin;
|
|
136
|
+
const sessionPin = await readSession();
|
|
137
|
+
if (sessionPin) return sessionPin;
|
|
138
|
+
const pin = opts?.confirm ? await askPinConfirm() : await askPin();
|
|
139
|
+
await saveSession(pin);
|
|
140
|
+
return pin;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/commands/init.ts
|
|
144
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
145
|
+
import { join } from "path";
|
|
146
|
+
import { homedir as homedir2, platform } from "os";
|
|
147
|
+
import { exec } from "child_process";
|
|
148
|
+
var LLM_KEY_URLS = {
|
|
149
|
+
anthropic: "https://console.anthropic.com/settings/keys",
|
|
150
|
+
openai: "https://platform.openai.com/api-keys"
|
|
151
|
+
};
|
|
152
|
+
function openBrowser(url) {
|
|
153
|
+
const cmd = platform() === "darwin" ? "open" : platform() === "win32" ? "start" : "xdg-open";
|
|
154
|
+
exec(`${cmd} "${url}"`, () => {
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
var CONFIG_DIR = join(homedir2(), ".t2000");
|
|
158
|
+
var CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
159
|
+
function loadConfig() {
|
|
160
|
+
try {
|
|
161
|
+
return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
162
|
+
} catch {
|
|
163
|
+
return {};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function saveConfig(config) {
|
|
167
|
+
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
|
|
168
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
|
|
169
|
+
}
|
|
170
|
+
function registerInit(program2) {
|
|
171
|
+
program2.command("init").description("Create a new agent bank account \u2014 guided setup with AI + Telegram + safeguards").option("--key <path>", "Key file path").option("--no-sponsor", "Skip gas sponsorship").action(async (opts) => {
|
|
172
|
+
try {
|
|
173
|
+
const { select, input, password: password3, confirm: confirm2 } = await import("@inquirer/prompts");
|
|
174
|
+
console.log("");
|
|
175
|
+
console.log(` \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510`);
|
|
176
|
+
console.log(` \u2502 ${pc2.bold("Welcome to t2000")} \u2502`);
|
|
177
|
+
console.log(` \u2502 Your personal AI financial advisor \u2502`);
|
|
178
|
+
console.log(` \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518`);
|
|
179
|
+
console.log("");
|
|
180
|
+
const hasWallet = await walletExists(opts.key);
|
|
181
|
+
let address = "";
|
|
182
|
+
const skipWallet = hasWallet;
|
|
183
|
+
if (skipWallet) {
|
|
184
|
+
printSuccess("Existing wallet detected");
|
|
185
|
+
const pin = await password3({ message: "Enter your PIN:" });
|
|
186
|
+
if (!pin || pin.length < 4) throw new Error("PIN must be at least 4 characters");
|
|
187
|
+
const agent = await T2000.create({ pin, keyPath: opts.key });
|
|
188
|
+
address = agent.address();
|
|
189
|
+
await saveSession(pin);
|
|
190
|
+
printSuccess(`Wallet unlocked (${address.slice(0, 6)}...${address.slice(-4)})`);
|
|
191
|
+
} else {
|
|
192
|
+
console.log(` ${pc2.bold("Step 1 of 5")} \u2014 Create wallet`);
|
|
193
|
+
printBlank();
|
|
194
|
+
const pin = await password3({ message: `Create PIN (min 4 chars):` });
|
|
195
|
+
if (!pin || pin.length < 4) throw new Error("PIN must be at least 4 characters");
|
|
196
|
+
const pinConfirm = await password3({ message: "Confirm PIN:" });
|
|
197
|
+
if (pin !== pinConfirm) throw new Error("PINs do not match");
|
|
198
|
+
const { agent, address: addr } = await T2000.init({ pin, keyPath: opts.key, sponsored: true });
|
|
199
|
+
address = addr;
|
|
200
|
+
await saveSession(pin);
|
|
201
|
+
printSuccess("Keypair generated");
|
|
202
|
+
printSuccess("Sui mainnet");
|
|
203
|
+
printSuccess("5 accounts: Checking, Savings, Credit, Exchange, Investment");
|
|
204
|
+
printBlank();
|
|
205
|
+
}
|
|
206
|
+
const stepNum = skipWallet ? 1 : 3;
|
|
207
|
+
console.log(` ${pc2.bold(`Step ${stepNum} of ${skipWallet ? 3 : 5}`)} \u2014 Connect AI`);
|
|
208
|
+
printBlank();
|
|
209
|
+
const llmProvider = await select({
|
|
210
|
+
message: "Which LLM provider?",
|
|
211
|
+
choices: [
|
|
212
|
+
{ name: "Claude (Anthropic)", value: "anthropic" },
|
|
213
|
+
{ name: "GPT (OpenAI)", value: "openai" },
|
|
214
|
+
{ name: "Skip (CLI only, no chat)", value: "skip" }
|
|
215
|
+
]
|
|
216
|
+
});
|
|
217
|
+
if (llmProvider !== "skip") {
|
|
218
|
+
const providerName = llmProvider === "anthropic" ? "Anthropic" : "OpenAI";
|
|
219
|
+
const keyUrl = LLM_KEY_URLS[llmProvider];
|
|
220
|
+
printBlank();
|
|
221
|
+
printInfo(`Opening ${providerName} API keys page in your browser...`);
|
|
222
|
+
openBrowser(keyUrl);
|
|
223
|
+
printLine(` ${pc2.dim(keyUrl)}`);
|
|
224
|
+
printBlank();
|
|
225
|
+
const apiKey = await password3({
|
|
226
|
+
message: `Paste your ${providerName} API key:`
|
|
227
|
+
});
|
|
228
|
+
if (!apiKey) throw new Error("API key is required");
|
|
229
|
+
const config = loadConfig();
|
|
230
|
+
config.llm = { provider: llmProvider, apiKey };
|
|
231
|
+
saveConfig(config);
|
|
232
|
+
const modelName = llmProvider === "anthropic" ? "claude-sonnet-4-20250514" : "gpt-4o";
|
|
233
|
+
printSuccess(`${providerName} connected \u2014 model: ${modelName}`);
|
|
234
|
+
} else {
|
|
235
|
+
printSuccess("Skipped \u2014 use CLI commands directly");
|
|
236
|
+
}
|
|
237
|
+
printBlank();
|
|
238
|
+
const telegramStepNum = skipWallet ? 2 : 4;
|
|
239
|
+
console.log(` ${pc2.bold(`Step ${telegramStepNum} of ${skipWallet ? 3 : 5}`)} \u2014 Connect Telegram ${pc2.dim("(optional)")}`);
|
|
240
|
+
printBlank();
|
|
241
|
+
const wantsTelegram = await confirm2({
|
|
242
|
+
message: "Want to chat with your agent on Telegram?",
|
|
243
|
+
default: true
|
|
244
|
+
});
|
|
245
|
+
if (wantsTelegram) {
|
|
246
|
+
printBlank();
|
|
247
|
+
printInfo("Opening BotFather in Telegram...");
|
|
248
|
+
openBrowser("https://t.me/BotFather");
|
|
249
|
+
printBlank();
|
|
250
|
+
printLine(`1. Send ${pc2.cyan("/newbot")} to BotFather`);
|
|
251
|
+
printLine(`2. Pick a name (e.g. "My t2000 Agent")`);
|
|
252
|
+
printLine(`3. Copy the bot token`);
|
|
253
|
+
printBlank();
|
|
254
|
+
const botToken = await input({ message: "Paste the bot token:" });
|
|
255
|
+
if (!botToken) throw new Error("Bot token is required");
|
|
256
|
+
printBlank();
|
|
257
|
+
printInfo("Opening @userinfobot to get your Telegram user ID...");
|
|
258
|
+
openBrowser("https://t.me/userinfobot");
|
|
259
|
+
printBlank();
|
|
260
|
+
printLine(`Send any message to ${pc2.cyan("@userinfobot")} \u2014 it will reply with your ID.`);
|
|
261
|
+
printBlank();
|
|
262
|
+
const userId = await input({ message: "Paste your Telegram user ID:" });
|
|
263
|
+
const config = loadConfig();
|
|
264
|
+
config.channels = {
|
|
265
|
+
...config.channels ?? {},
|
|
266
|
+
telegram: {
|
|
267
|
+
enabled: true,
|
|
268
|
+
botToken,
|
|
269
|
+
allowedUsers: userId ? [userId] : []
|
|
270
|
+
},
|
|
271
|
+
webchat: { enabled: true, port: 2e3 }
|
|
272
|
+
};
|
|
273
|
+
saveConfig(config);
|
|
274
|
+
printSuccess("Telegram connected");
|
|
275
|
+
} else {
|
|
276
|
+
printSuccess("Skipped \u2014 you can add Telegram later");
|
|
277
|
+
}
|
|
278
|
+
printBlank();
|
|
279
|
+
const safeguardStepNum = skipWallet ? 3 : 5;
|
|
280
|
+
console.log(` ${pc2.bold(`Step ${safeguardStepNum} of ${skipWallet ? 3 : 5}`)} \u2014 Set safeguards`);
|
|
281
|
+
printBlank();
|
|
282
|
+
const maxPerTx = await input({
|
|
283
|
+
message: "Max per transaction ($):",
|
|
284
|
+
default: "500"
|
|
285
|
+
});
|
|
286
|
+
const maxDaily = await input({
|
|
287
|
+
message: "Max daily sends ($):",
|
|
288
|
+
default: "1000"
|
|
289
|
+
});
|
|
290
|
+
const enforcer = new SafeguardEnforcer(CONFIG_DIR);
|
|
291
|
+
enforcer.load();
|
|
292
|
+
enforcer.set("maxPerTx", Number(maxPerTx));
|
|
293
|
+
enforcer.set("maxDailySend", Number(maxDaily));
|
|
294
|
+
printSuccess("Safeguards configured");
|
|
295
|
+
printBlank();
|
|
296
|
+
console.log(` \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510`);
|
|
297
|
+
console.log(` \u2502 ${pc2.green("\u2713 You're all set")} \u2502`);
|
|
298
|
+
console.log(` \u2502 \u2502`);
|
|
299
|
+
if (llmProvider !== "skip") {
|
|
300
|
+
console.log(` \u2502 Start your agent: \u2502`);
|
|
301
|
+
console.log(` \u2502 ${pc2.cyan("t2000 gateway")} \u2502`);
|
|
302
|
+
console.log(` \u2502 \u2502`);
|
|
303
|
+
}
|
|
304
|
+
console.log(` \u2502 Or use the CLI directly: \u2502`);
|
|
305
|
+
console.log(` \u2502 ${pc2.cyan("t2000 balance")} \u2502`);
|
|
306
|
+
console.log(` \u2502 ${pc2.cyan("t2000 invest buy 100 SUI")} \u2502`);
|
|
307
|
+
console.log(` \u2502 \u2502`);
|
|
308
|
+
console.log(` \u2502 Deposit USDC to get started: \u2502`);
|
|
309
|
+
console.log(` \u2502 ${pc2.yellow(address)} \u2502`);
|
|
310
|
+
console.log(` \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518`);
|
|
311
|
+
console.log("");
|
|
312
|
+
} catch (error) {
|
|
313
|
+
handleError(error);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// src/commands/send.ts
|
|
319
|
+
import { T2000 as T20002 } from "@t2000/sdk";
|
|
320
|
+
import { truncateAddress, formatUsd } from "@t2000/sdk";
|
|
321
|
+
var KNOWN_ASSETS = /* @__PURE__ */ new Set(["USDC", "USDT", "USDE", "USDSUI", "SUI"]);
|
|
322
|
+
function parseSendArgs(args) {
|
|
323
|
+
const filtered = args.filter((a) => a.toLowerCase() !== "to");
|
|
324
|
+
if (filtered.length >= 3 && KNOWN_ASSETS.has(filtered[1].toUpperCase())) {
|
|
325
|
+
return { amount: parseFloat(filtered[0]), asset: filtered[1].toUpperCase(), recipient: filtered[2] };
|
|
326
|
+
}
|
|
327
|
+
if (filtered.length >= 2) {
|
|
328
|
+
return { amount: parseFloat(filtered[0]), asset: "USDC", recipient: filtered[filtered.length - 1] };
|
|
329
|
+
}
|
|
330
|
+
throw new Error("Usage: t2000 send <amount> [asset] [to] <address_or_contact>");
|
|
331
|
+
}
|
|
332
|
+
function registerSend(program2) {
|
|
333
|
+
program2.command("send").argument("<amount>", "Amount to send").argument("[args...]", 'Asset, "to" keyword, and recipient address or contact name').description("Send USDC (or other asset) to an address or contact name").option("--key <path>", "Key file path").action(async (amount, args, opts) => {
|
|
334
|
+
try {
|
|
335
|
+
const { amount: parsedAmount, asset, recipient } = parseSendArgs([amount, ...args]);
|
|
336
|
+
const pin = await resolvePin();
|
|
337
|
+
const agent = await T20002.create({ pin, keyPath: opts.key });
|
|
338
|
+
const result = await agent.send({
|
|
339
|
+
to: recipient,
|
|
340
|
+
amount: parsedAmount,
|
|
341
|
+
asset
|
|
342
|
+
});
|
|
343
|
+
if (isJsonMode()) {
|
|
344
|
+
printJson(result);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
printBlank();
|
|
348
|
+
const displayTo = result.contactName ? `${result.contactName} (${truncateAddress(result.to)})` : truncateAddress(result.to);
|
|
349
|
+
printSuccess(`Sent ${formatUsd(result.amount)} ${asset.toUpperCase()} \u2192 ${displayTo}`);
|
|
350
|
+
printKeyValue("Gas", `${result.gasCost.toFixed(4)} ${result.gasCostUnit} (${result.gasMethod})`);
|
|
351
|
+
printKeyValue("Balance", formatUsd(result.balance.available) + " USDC");
|
|
352
|
+
printKeyValue("Tx", explorerUrl(result.tx));
|
|
353
|
+
printBlank();
|
|
354
|
+
} catch (error) {
|
|
355
|
+
handleError(error);
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// src/commands/balance.ts
|
|
361
|
+
import pc3 from "picocolors";
|
|
362
|
+
import { T2000 as T20003, formatUsd as formatUsd2 } from "@t2000/sdk";
|
|
363
|
+
async function fetchLimits(agent) {
|
|
364
|
+
const [maxWithdraw, maxBorrow, hf] = await Promise.all([
|
|
365
|
+
agent.maxWithdraw(),
|
|
366
|
+
agent.maxBorrow(),
|
|
367
|
+
agent.healthFactor()
|
|
368
|
+
]);
|
|
369
|
+
return {
|
|
370
|
+
maxWithdraw: maxWithdraw.maxAmount.toFixed(2),
|
|
371
|
+
maxBorrow: maxBorrow.maxAmount.toFixed(2),
|
|
372
|
+
healthFactor: hf.borrowed >= 0.01 ? hf.healthFactor : null
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
function registerBalance(program2) {
|
|
376
|
+
program2.command("balance").description("Show wallet balance").option("--key <path>", "Key file path").option("--show-limits", "Include maxWithdraw, maxBorrow, and health factor").action(async (opts) => {
|
|
377
|
+
try {
|
|
378
|
+
const pin = await resolvePin();
|
|
379
|
+
const agent = await T20003.create({ pin, keyPath: opts.key });
|
|
380
|
+
const bal = await agent.balance();
|
|
381
|
+
const limits = opts.showLimits ? await fetchLimits(agent) : void 0;
|
|
382
|
+
if (isJsonMode()) {
|
|
383
|
+
const output = limits ? { ...bal, limits } : bal;
|
|
384
|
+
printJson(output);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
printBlank();
|
|
388
|
+
const stables = bal.stables ?? {};
|
|
389
|
+
const stableEntries = Object.entries(stables).filter(([, v]) => v >= 0.01);
|
|
390
|
+
if (stableEntries.length <= 1) {
|
|
391
|
+
printKeyValue("Available", `${formatUsd2(bal.available)} ${pc3.dim("(checking \u2014 spendable)")}`);
|
|
392
|
+
} else {
|
|
393
|
+
printKeyValue("Available", `${formatUsd2(bal.available)} ${pc3.dim("(total stablecoins)")}`);
|
|
394
|
+
for (const [symbol, amount] of stableEntries) {
|
|
395
|
+
printLine(` ${pc3.dim(symbol)} ${formatUsd2(amount)}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (bal.savings > 0.01) {
|
|
399
|
+
const positions = await agent.positions();
|
|
400
|
+
const saves = positions.positions.filter((p) => p.type === "save");
|
|
401
|
+
const borrows = positions.positions.filter((p) => p.type === "borrow");
|
|
402
|
+
const weightedApy = saves.length > 0 ? saves.reduce((sum, p) => sum + p.amount * p.apy, 0) / saves.reduce((sum, p) => sum + p.amount, 0) : 0;
|
|
403
|
+
const dailyEarning = bal.savings * (weightedApy / 100) / 365;
|
|
404
|
+
printKeyValue("Savings", `${formatUsd2(bal.savings)} ${pc3.dim(`(earning ${weightedApy.toFixed(2)}% APY)`)}`);
|
|
405
|
+
if (bal.debt > 0.01) {
|
|
406
|
+
const borrowApy = borrows.length > 0 ? borrows.reduce((sum, p) => sum + p.amount * p.apy, 0) / borrows.reduce((sum, p) => sum + p.amount, 0) : 0;
|
|
407
|
+
printKeyValue("Credit", `${pc3.red(`-${formatUsd2(bal.debt)}`)} ${pc3.dim(`(${borrowApy.toFixed(2)}% APY)`)}`);
|
|
408
|
+
}
|
|
409
|
+
if (bal.investment > 0.01) {
|
|
410
|
+
const pnlColor = bal.investmentPnL >= 0 ? pc3.green : pc3.red;
|
|
411
|
+
const pnlSign = bal.investmentPnL >= 0 ? "+" : "";
|
|
412
|
+
const pnlPct = bal.investment > 0 ? bal.investmentPnL / (bal.investment - bal.investmentPnL) * 100 : 0;
|
|
413
|
+
let earningInfo = "";
|
|
414
|
+
try {
|
|
415
|
+
const portfolio = await agent.getPortfolio();
|
|
416
|
+
const earningPositions = portfolio.positions.filter((p) => p.earning && p.earningApy);
|
|
417
|
+
if (earningPositions.length > 0) {
|
|
418
|
+
const avgApy = earningPositions.reduce((s, p) => s + (p.earningApy ?? 0) * p.currentValue, 0) / earningPositions.reduce((s, p) => s + p.currentValue, 0);
|
|
419
|
+
earningInfo = `, earning ${avgApy.toFixed(1)}% APY`;
|
|
420
|
+
}
|
|
421
|
+
} catch {
|
|
422
|
+
}
|
|
423
|
+
printKeyValue("Investment", `${formatUsd2(bal.investment)} ${pnlColor(`(${pnlSign}${pnlPct.toFixed(1)}%${earningInfo})`)}`);
|
|
424
|
+
} else {
|
|
425
|
+
printKeyValue("Investment", pc3.dim("\u2014"));
|
|
426
|
+
}
|
|
427
|
+
printSeparator();
|
|
428
|
+
printKeyValue("Total", `${formatUsd2(bal.total)}`);
|
|
429
|
+
if (dailyEarning >= 5e-3) {
|
|
430
|
+
printLine(` ${pc3.dim(`Earning ~${formatUsd2(dailyEarning)}/day`)}`);
|
|
431
|
+
}
|
|
432
|
+
} else {
|
|
433
|
+
if (bal.debt > 0.01) {
|
|
434
|
+
printKeyValue("Credit", `${pc3.red(`-${formatUsd2(bal.debt)}`)}`);
|
|
435
|
+
}
|
|
436
|
+
printKeyValue("Savings", `${formatUsd2(bal.savings)}`);
|
|
437
|
+
if (bal.investment > 0.01) {
|
|
438
|
+
const pnlColor = bal.investmentPnL >= 0 ? pc3.green : pc3.red;
|
|
439
|
+
const pnlSign = bal.investmentPnL >= 0 ? "+" : "";
|
|
440
|
+
const pnlPct = bal.investment > 0 ? bal.investmentPnL / (bal.investment - bal.investmentPnL) * 100 : 0;
|
|
441
|
+
let earningInfo = "";
|
|
442
|
+
try {
|
|
443
|
+
const portfolio = await agent.getPortfolio();
|
|
444
|
+
const earningPositions = portfolio.positions.filter((p) => p.earning && p.earningApy);
|
|
445
|
+
if (earningPositions.length > 0) {
|
|
446
|
+
const avgApy = earningPositions.reduce((s, p) => s + (p.earningApy ?? 0) * p.currentValue, 0) / earningPositions.reduce((s, p) => s + p.currentValue, 0);
|
|
447
|
+
earningInfo = `, earning ${avgApy.toFixed(1)}% APY`;
|
|
448
|
+
}
|
|
449
|
+
} catch {
|
|
450
|
+
}
|
|
451
|
+
printKeyValue("Investment", `${formatUsd2(bal.investment)} ${pnlColor(`(${pnlSign}${pnlPct.toFixed(1)}%${earningInfo})`)}`);
|
|
452
|
+
} else {
|
|
453
|
+
printKeyValue("Investment", pc3.dim("\u2014"));
|
|
454
|
+
}
|
|
455
|
+
printSeparator();
|
|
456
|
+
printKeyValue("Total", `${formatUsd2(bal.total)}`);
|
|
457
|
+
}
|
|
458
|
+
if (limits) {
|
|
459
|
+
printBlank();
|
|
460
|
+
printHeader("Limits");
|
|
461
|
+
printKeyValue("Max withdraw", `${formatUsd2(Number(limits.maxWithdraw))} USDC`, 4);
|
|
462
|
+
printKeyValue("Max borrow", `${formatUsd2(Number(limits.maxBorrow))} USDC`, 4);
|
|
463
|
+
const hfDisplay = limits.healthFactor !== null ? limits.healthFactor.toFixed(2) : `${pc3.green("\u221E")} ${pc3.dim("(no active loan)")}`;
|
|
464
|
+
printKeyValue("Health factor", hfDisplay, 4);
|
|
465
|
+
}
|
|
466
|
+
printBlank();
|
|
467
|
+
} catch (error) {
|
|
468
|
+
handleError(error);
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/commands/address.ts
|
|
474
|
+
import { T2000 as T20004 } from "@t2000/sdk";
|
|
475
|
+
function registerAddress(program2) {
|
|
476
|
+
program2.command("address").description("Show wallet address").option("--key <path>", "Key file path").action(async (opts) => {
|
|
477
|
+
try {
|
|
478
|
+
const pin = await resolvePin();
|
|
479
|
+
const agent = await T20004.create({ pin, keyPath: opts.key });
|
|
480
|
+
if (isJsonMode()) {
|
|
481
|
+
printJson({ address: agent.address() });
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
printBlank();
|
|
485
|
+
printKeyValue("Address", agent.address());
|
|
486
|
+
printBlank();
|
|
487
|
+
} catch (error) {
|
|
488
|
+
handleError(error);
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// src/commands/deposit.ts
|
|
494
|
+
import { T2000 as T20005 } from "@t2000/sdk";
|
|
495
|
+
function registerDeposit(program2) {
|
|
496
|
+
program2.command("deposit").description("Show funding instructions").option("--key <path>", "Key file path").action(async (opts) => {
|
|
497
|
+
try {
|
|
498
|
+
const pin = await resolvePin();
|
|
499
|
+
const agent = await T20005.create({ pin, keyPath: opts.key });
|
|
500
|
+
const info = await agent.deposit();
|
|
501
|
+
if (isJsonMode()) {
|
|
502
|
+
printJson(info);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
printHeader("Fund your wallet");
|
|
506
|
+
console.log(info.instructions);
|
|
507
|
+
printBlank();
|
|
508
|
+
} catch (error) {
|
|
509
|
+
handleError(error);
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// src/commands/history.ts
|
|
515
|
+
import { T2000 as T20006, truncateAddress as truncateAddress2 } from "@t2000/sdk";
|
|
516
|
+
function registerHistory(program2) {
|
|
517
|
+
program2.command("history").description("Show transaction history").option("--limit <n>", "Number of transactions", "20").option("--key <path>", "Key file path").action(async (opts) => {
|
|
518
|
+
try {
|
|
519
|
+
const pin = await resolvePin();
|
|
520
|
+
const agent = await T20006.create({ pin, keyPath: opts.key });
|
|
521
|
+
const txns = await agent.history({ limit: parseInt(opts.limit, 10) });
|
|
522
|
+
if (isJsonMode()) {
|
|
523
|
+
printJson(txns);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
printHeader("Transaction History");
|
|
527
|
+
if (txns.length === 0) {
|
|
528
|
+
printInfo("No transactions yet.");
|
|
529
|
+
} else {
|
|
530
|
+
for (const tx of txns) {
|
|
531
|
+
const time = tx.timestamp ? new Date(tx.timestamp).toLocaleString() : "unknown";
|
|
532
|
+
const gas = tx.gasMethod ? ` (${tx.gasMethod})` : "";
|
|
533
|
+
printLine(`${truncateAddress2(tx.digest)} ${tx.action}${gas} ${time}`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
printBlank();
|
|
537
|
+
} catch (error) {
|
|
538
|
+
handleError(error);
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// src/commands/exportKey.ts
|
|
544
|
+
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
545
|
+
import { resolve as resolve2 } from "path";
|
|
546
|
+
import { homedir as homedir3 } from "os";
|
|
547
|
+
import { T2000 as T20007 } from "@t2000/sdk";
|
|
548
|
+
var LOCKFILE = resolve2(homedir3(), ".t2000", ".pin-lock");
|
|
549
|
+
var MAX_ATTEMPTS = 5;
|
|
550
|
+
var LOCKOUT_MS = 5 * 60 * 1e3;
|
|
551
|
+
async function getLockState() {
|
|
552
|
+
try {
|
|
553
|
+
const data = JSON.parse(await readFile2(LOCKFILE, "utf-8"));
|
|
554
|
+
return data;
|
|
555
|
+
} catch {
|
|
556
|
+
return { attempts: 0, lockedUntil: 0 };
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
async function setLockState(state) {
|
|
560
|
+
await mkdir2(resolve2(homedir3(), ".t2000"), { recursive: true });
|
|
561
|
+
await writeFile2(LOCKFILE, JSON.stringify(state), { mode: 384 });
|
|
562
|
+
}
|
|
563
|
+
function registerExport(program2) {
|
|
564
|
+
program2.command("export").description("Export private key (raw Ed25519 hex)").option("--key <path>", "Key file path").option("--yes", "Skip confirmation").action(async (opts) => {
|
|
565
|
+
try {
|
|
566
|
+
const lock = await getLockState();
|
|
567
|
+
if (lock.lockedUntil > Date.now()) {
|
|
568
|
+
const remainSec = Math.ceil((lock.lockedUntil - Date.now()) / 1e3);
|
|
569
|
+
printError(`Too many failed PIN attempts. Try again in ${remainSec}s.`);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (!opts.yes && !isJsonMode()) {
|
|
573
|
+
const proceed = await askConfirm(
|
|
574
|
+
"WARNING: This will display your raw private key. Anyone with this key controls your wallet. Continue?"
|
|
575
|
+
);
|
|
576
|
+
if (!proceed) return;
|
|
577
|
+
}
|
|
578
|
+
const pin = await resolvePin();
|
|
579
|
+
let agent;
|
|
580
|
+
try {
|
|
581
|
+
agent = await T20007.create({ pin, keyPath: opts.key });
|
|
582
|
+
} catch (error) {
|
|
583
|
+
const msg = error instanceof Error ? error.message : "";
|
|
584
|
+
if (msg.includes("Invalid PIN")) {
|
|
585
|
+
const newAttempts = lock.attempts + 1;
|
|
586
|
+
if (newAttempts >= MAX_ATTEMPTS) {
|
|
587
|
+
await setLockState({ attempts: newAttempts, lockedUntil: Date.now() + LOCKOUT_MS });
|
|
588
|
+
printError(`Invalid PIN. Account locked for 5 minutes (${newAttempts} failed attempts).`);
|
|
589
|
+
} else {
|
|
590
|
+
await setLockState({ attempts: newAttempts, lockedUntil: 0 });
|
|
591
|
+
printError(`Invalid PIN. ${MAX_ATTEMPTS - newAttempts} attempts remaining.`);
|
|
592
|
+
}
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
throw error;
|
|
596
|
+
}
|
|
597
|
+
await setLockState({ attempts: 0, lockedUntil: 0 });
|
|
598
|
+
const hex = agent.exportKey();
|
|
599
|
+
if (isJsonMode()) {
|
|
600
|
+
printJson({ privateKey: hex, format: "ed25519_hex" });
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
printBlank();
|
|
604
|
+
printSuccess("Private key (Ed25519, hex):");
|
|
605
|
+
console.log(` ${hex}`);
|
|
606
|
+
printBlank();
|
|
607
|
+
printInfo("Not a BIP39 mnemonic. Store securely and never share.");
|
|
608
|
+
printBlank();
|
|
609
|
+
} catch (error) {
|
|
610
|
+
handleError(error);
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// src/commands/importKey.ts
|
|
616
|
+
import { keypairFromPrivateKey, saveKey } from "@t2000/sdk";
|
|
617
|
+
import { password as password2 } from "@inquirer/prompts";
|
|
618
|
+
function registerImport(program2) {
|
|
619
|
+
program2.command("import").description("Import a wallet from private key").option("--key <path>", "Key file path").action(async (opts) => {
|
|
620
|
+
try {
|
|
621
|
+
let privateKey;
|
|
622
|
+
if (process.env.T2000_PRIVATE_KEY) {
|
|
623
|
+
privateKey = process.env.T2000_PRIVATE_KEY;
|
|
624
|
+
} else {
|
|
625
|
+
privateKey = await password2({ message: "Enter private key (hex):" });
|
|
626
|
+
}
|
|
627
|
+
if (!privateKey) throw new Error("Private key is required");
|
|
628
|
+
const pin = await resolvePin({ confirm: true });
|
|
629
|
+
const keypair = keypairFromPrivateKey(privateKey);
|
|
630
|
+
const address = keypair.getPublicKey().toSuiAddress();
|
|
631
|
+
await saveKey(keypair, pin, opts.key);
|
|
632
|
+
if (isJsonMode()) {
|
|
633
|
+
printJson({ address, imported: true });
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
printBlank();
|
|
637
|
+
printSuccess("Wallet imported (encrypted)");
|
|
638
|
+
printKeyValue("Address", address);
|
|
639
|
+
printBlank();
|
|
640
|
+
} catch (error) {
|
|
641
|
+
handleError(error);
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// src/commands/save.ts
|
|
647
|
+
import pc4 from "picocolors";
|
|
648
|
+
import { T2000 as T20009, formatUsd as formatUsd3 } from "@t2000/sdk";
|
|
649
|
+
function registerSave(program2) {
|
|
650
|
+
const action = async (amountStr, opts) => {
|
|
651
|
+
try {
|
|
652
|
+
const amount = amountStr === "all" ? "all" : parseFloat(amountStr);
|
|
653
|
+
if (amount !== "all" && (isNaN(amount) || amount <= 0)) {
|
|
654
|
+
throw new Error('Amount must be a positive number or "all"');
|
|
655
|
+
}
|
|
656
|
+
const pin = await resolvePin();
|
|
657
|
+
const agent = await T20009.create({ pin, keyPath: opts.key });
|
|
658
|
+
let gasManagerUsdc = 0;
|
|
659
|
+
agent.on("gasAutoTopUp", (data) => {
|
|
660
|
+
gasManagerUsdc = data.usdcSpent;
|
|
661
|
+
});
|
|
662
|
+
const result = await agent.save({ amount, protocol: opts.protocol });
|
|
663
|
+
if (isJsonMode()) {
|
|
664
|
+
printJson(result);
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
printBlank();
|
|
668
|
+
if (gasManagerUsdc > 0) {
|
|
669
|
+
printSuccess(`Gas manager: ${pc4.yellow(formatUsd3(gasManagerUsdc))} USDC \u2192 SUI`);
|
|
670
|
+
}
|
|
671
|
+
const protocolName = opts.protocol ?? "best rate";
|
|
672
|
+
printSuccess(`Saved ${pc4.yellow(formatUsd3(result.amount))} USDC to ${protocolName}`);
|
|
673
|
+
if (result.fee > 0) {
|
|
674
|
+
const feeRate = (result.fee / result.amount * 100).toFixed(1);
|
|
675
|
+
printSuccess(`Protocol fee: ${pc4.dim(`${formatUsd3(result.fee)} USDC (${feeRate}%)`)}`);
|
|
676
|
+
}
|
|
677
|
+
printSuccess(`Current APY: ${pc4.green(`${result.apy.toFixed(2)}%`)}`);
|
|
678
|
+
printSuccess(`Savings balance: ${pc4.yellow(formatUsd3(result.savingsBalance))} USDC`);
|
|
679
|
+
printKeyValue("Tx", explorerUrl(result.tx));
|
|
680
|
+
printBlank();
|
|
681
|
+
} catch (error) {
|
|
682
|
+
handleError(error);
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
program2.command("save").description("Deposit USDC into savings").argument("<amount>", 'Amount to save (or "all")').option("--key <path>", "Key file path").option("--protocol <name>", "Protocol to use (e.g. navi, suilend)").action(action);
|
|
686
|
+
program2.command("supply").description("Deposit USDC into savings (alias for save)").argument("<amount>", 'Amount to save (or "all")').option("--key <path>", "Key file path").option("--protocol <name>", "Protocol to use (e.g. navi, suilend)").action(action);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// src/commands/withdraw.ts
|
|
690
|
+
import { T2000 as T200010, formatUsd as formatUsd4 } from "@t2000/sdk";
|
|
691
|
+
function registerWithdraw(program2) {
|
|
692
|
+
program2.command("withdraw").description("Withdraw USDC from savings").argument("<amount>", 'Amount to withdraw (or "all")').option("--key <path>", "Key file path").option("--protocol <name>", "Protocol to use (e.g. navi, suilend)").action(async (amountStr, opts) => {
|
|
693
|
+
try {
|
|
694
|
+
const amount = amountStr === "all" ? "all" : parseFloat(amountStr);
|
|
695
|
+
if (amount !== "all" && (isNaN(amount) || amount <= 0)) {
|
|
696
|
+
throw new Error('Amount must be a positive number or "all"');
|
|
697
|
+
}
|
|
698
|
+
const pin = await resolvePin();
|
|
699
|
+
const agent = await T200010.create({ pin, keyPath: opts.key });
|
|
700
|
+
const result = await agent.withdraw({ amount, protocol: opts.protocol });
|
|
701
|
+
if (isJsonMode()) {
|
|
702
|
+
printJson(result);
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
printBlank();
|
|
706
|
+
printSuccess(`Withdrew ${formatUsd4(result.amount)} USDC`);
|
|
707
|
+
printKeyValue("Tx", explorerUrl(result.tx));
|
|
708
|
+
printBlank();
|
|
709
|
+
} catch (error) {
|
|
710
|
+
handleError(error);
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// src/commands/borrow.ts
|
|
716
|
+
import { T2000 as T200011, formatUsd as formatUsd5 } from "@t2000/sdk";
|
|
717
|
+
function registerBorrow(program2) {
|
|
718
|
+
program2.command("borrow").description("Borrow USDC against savings collateral").argument("<amount>", "Amount to borrow").option("--key <path>", "Key file path").option("--protocol <name>", "Protocol to use (e.g. navi)").action(async (amountStr, opts) => {
|
|
719
|
+
try {
|
|
720
|
+
const amount = parseFloat(amountStr);
|
|
721
|
+
if (isNaN(amount) || amount <= 0) {
|
|
722
|
+
throw new Error("Amount must be a positive number");
|
|
723
|
+
}
|
|
724
|
+
const pin = await resolvePin();
|
|
725
|
+
const agent = await T200011.create({ pin, keyPath: opts.key });
|
|
726
|
+
const maxResult = await agent.maxBorrow();
|
|
727
|
+
if (amount > maxResult.maxAmount) {
|
|
728
|
+
printWarning(`Max safe borrow: ${formatUsd5(maxResult.maxAmount)} (HF ${maxResult.currentHF.toFixed(2)} \u2192 min 1.5)`);
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
const result = await agent.borrow({ amount, protocol: opts.protocol });
|
|
732
|
+
if (isJsonMode()) {
|
|
733
|
+
printJson(result);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
printBlank();
|
|
737
|
+
printSuccess(`Borrowed ${formatUsd5(amount)} USDC`);
|
|
738
|
+
printKeyValue("Health Factor", result.healthFactor.toFixed(2));
|
|
739
|
+
printKeyValue("Tx", explorerUrl(result.tx));
|
|
740
|
+
printBlank();
|
|
741
|
+
} catch (error) {
|
|
742
|
+
handleError(error);
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// src/commands/repay.ts
|
|
748
|
+
import { T2000 as T200012, formatUsd as formatUsd6 } from "@t2000/sdk";
|
|
749
|
+
function registerRepay(program2) {
|
|
750
|
+
program2.command("repay").description("Repay borrowed USDC").argument("<amount>", 'Amount to repay (or "all")').option("--key <path>", "Key file path").option("--protocol <name>", "Protocol to use (e.g. navi)").action(async (amountStr, opts) => {
|
|
751
|
+
try {
|
|
752
|
+
const amount = amountStr === "all" ? "all" : parseFloat(amountStr);
|
|
753
|
+
if (amount !== "all" && (isNaN(amount) || amount <= 0)) {
|
|
754
|
+
throw new Error('Amount must be a positive number or "all"');
|
|
755
|
+
}
|
|
756
|
+
const pin = await resolvePin();
|
|
757
|
+
const agent = await T200012.create({ pin, keyPath: opts.key });
|
|
758
|
+
const result = await agent.repay({ amount, protocol: opts.protocol });
|
|
759
|
+
if (isJsonMode()) {
|
|
760
|
+
printJson(result);
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
printBlank();
|
|
764
|
+
printSuccess(`Repaid ${formatUsd6(result.amount)} USDC`);
|
|
765
|
+
printKeyValue("Remaining Debt", formatUsd6(result.remainingDebt));
|
|
766
|
+
printKeyValue("Tx", explorerUrl(result.tx));
|
|
767
|
+
printBlank();
|
|
768
|
+
} catch (error) {
|
|
769
|
+
handleError(error);
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// src/commands/health.ts
|
|
775
|
+
import { T2000 as T200013, formatUsd as formatUsd7 } from "@t2000/sdk";
|
|
776
|
+
function registerHealth(program2) {
|
|
777
|
+
program2.command("health").description("Check savings health factor").option("--key <path>", "Key file path").action(async (opts) => {
|
|
778
|
+
try {
|
|
779
|
+
const pin = await resolvePin();
|
|
780
|
+
const agent = await T200013.create({ pin, keyPath: opts.key });
|
|
781
|
+
const hf = await agent.healthFactor();
|
|
782
|
+
if (isJsonMode()) {
|
|
783
|
+
printJson(hf);
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
printBlank();
|
|
787
|
+
const noActiveLoan = hf.borrowed < 0.01;
|
|
788
|
+
const hfStr = hf.healthFactor === Infinity || noActiveLoan ? "\u221E" : hf.healthFactor.toFixed(2);
|
|
789
|
+
if (hf.healthFactor >= 2) {
|
|
790
|
+
printSuccess(`Health Factor: ${hfStr} (healthy)`);
|
|
791
|
+
} else if (hf.healthFactor >= 1.5) {
|
|
792
|
+
printWarning(`Health Factor: ${hfStr} (moderate)`);
|
|
793
|
+
} else if (hf.healthFactor >= 1.2) {
|
|
794
|
+
printWarning(`Health Factor: ${hfStr} (low)`);
|
|
795
|
+
} else {
|
|
796
|
+
printError(`Health Factor: ${hfStr} (CRITICAL)`);
|
|
797
|
+
}
|
|
798
|
+
printBlank();
|
|
799
|
+
printKeyValue("Supplied", `${formatUsd7(hf.supplied)} USDC`);
|
|
800
|
+
printKeyValue("Borrowed", `${formatUsd7(hf.borrowed)} USDC`);
|
|
801
|
+
printKeyValue("Max Borrow", `${formatUsd7(hf.maxBorrow)} USDC`);
|
|
802
|
+
printBlank();
|
|
803
|
+
} catch (error) {
|
|
804
|
+
handleError(error);
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// src/commands/rates.ts
|
|
810
|
+
import pc5 from "picocolors";
|
|
811
|
+
import { T2000 as T200014, SUPPORTED_ASSETS, INVESTMENT_ASSETS, STABLE_ASSETS } from "@t2000/sdk";
|
|
812
|
+
var INVEST_ASSETS = Object.keys(INVESTMENT_ASSETS);
|
|
813
|
+
function registerRates(program2) {
|
|
814
|
+
program2.command("rates").description("Show current APY rates across protocols and stablecoins").option("--key <path>", "Key file path").action(async (opts) => {
|
|
815
|
+
try {
|
|
816
|
+
const pin = await resolvePin();
|
|
817
|
+
const agent = await T200014.create({ pin, keyPath: opts.key });
|
|
818
|
+
const allRates = await agent.allRatesAcrossAssets();
|
|
819
|
+
if (isJsonMode()) {
|
|
820
|
+
printJson(allRates);
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
printBlank();
|
|
824
|
+
if (allRates.length > 0) {
|
|
825
|
+
const best = allRates.reduce((a, b) => b.rates.saveApy > a.rates.saveApy ? b : a);
|
|
826
|
+
const bestDisplay = SUPPORTED_ASSETS[best.asset]?.displayName ?? best.asset;
|
|
827
|
+
printLine(pc5.bold(pc5.green(`Best yield: ${best.rates.saveApy.toFixed(2)}% APY`)) + pc5.dim(` (${bestDisplay} on ${best.protocol})`));
|
|
828
|
+
printBlank();
|
|
829
|
+
}
|
|
830
|
+
for (const asset of STABLE_ASSETS) {
|
|
831
|
+
const assetRates = allRates.filter((r) => r.asset === asset);
|
|
832
|
+
if (assetRates.length === 0) continue;
|
|
833
|
+
const display = SUPPORTED_ASSETS[asset]?.displayName ?? asset;
|
|
834
|
+
printLine(pc5.bold(display));
|
|
835
|
+
printDivider();
|
|
836
|
+
for (const entry of assetRates) {
|
|
837
|
+
printKeyValue(entry.protocol, `Save ${entry.rates.saveApy.toFixed(2)}% Borrow ${entry.rates.borrowApy.toFixed(2)}%`);
|
|
838
|
+
}
|
|
839
|
+
printBlank();
|
|
840
|
+
}
|
|
841
|
+
const investRates = allRates.filter((r) => INVEST_ASSETS.includes(r.asset));
|
|
842
|
+
if (investRates.length > 0) {
|
|
843
|
+
printLine(pc5.bold("Investment Assets"));
|
|
844
|
+
printDivider();
|
|
845
|
+
for (const asset of INVEST_ASSETS) {
|
|
846
|
+
const assetRates = investRates.filter((r) => r.asset === asset);
|
|
847
|
+
if (assetRates.length === 0) continue;
|
|
848
|
+
const display = SUPPORTED_ASSETS[asset]?.displayName ?? asset;
|
|
849
|
+
for (const entry of assetRates) {
|
|
850
|
+
printKeyValue(`${display} (${entry.protocol})`, `Lend ${entry.rates.saveApy.toFixed(2)}%`);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
printBlank();
|
|
854
|
+
}
|
|
855
|
+
if (allRates.length === 0) {
|
|
856
|
+
printInfo("No protocol rates available");
|
|
857
|
+
printBlank();
|
|
858
|
+
}
|
|
859
|
+
} catch (error) {
|
|
860
|
+
handleError(error);
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// src/commands/positions.ts
|
|
866
|
+
import pc6 from "picocolors";
|
|
867
|
+
import { T2000 as T200015, formatUsd as formatUsd8 } from "@t2000/sdk";
|
|
868
|
+
function registerPositions(program2) {
|
|
869
|
+
program2.command("positions").description("Show savings & borrow positions across all protocols and assets").option("--key <path>", "Key file path").action(async (opts) => {
|
|
870
|
+
try {
|
|
871
|
+
const pin = await resolvePin();
|
|
872
|
+
const agent = await T200015.create({ pin, keyPath: opts.key });
|
|
873
|
+
const result = await agent.positions();
|
|
874
|
+
if (isJsonMode()) {
|
|
875
|
+
printJson(result);
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
let hasRewards = false;
|
|
879
|
+
const rewardsByKey = /* @__PURE__ */ new Map();
|
|
880
|
+
try {
|
|
881
|
+
const pending = await agent.getPendingRewards();
|
|
882
|
+
for (const r of pending) {
|
|
883
|
+
rewardsByKey.set(`${r.protocol}:${r.asset}`, true);
|
|
884
|
+
hasRewards = true;
|
|
885
|
+
}
|
|
886
|
+
} catch {
|
|
887
|
+
}
|
|
888
|
+
printBlank();
|
|
889
|
+
if (result.positions.length === 0) {
|
|
890
|
+
printInfo("No positions. Use `t2000 save <amount>` to start earning.");
|
|
891
|
+
} else {
|
|
892
|
+
const saves = result.positions.filter((p) => p.type === "save");
|
|
893
|
+
const borrows = result.positions.filter((p) => p.type === "borrow");
|
|
894
|
+
if (saves.length > 0) {
|
|
895
|
+
printLine(pc6.bold("Savings"));
|
|
896
|
+
printDivider();
|
|
897
|
+
for (const pos of saves) {
|
|
898
|
+
const earning = rewardsByKey.has(`${pos.protocol}:${pos.asset}`) ? ` ${pc6.yellow("+rewards")}` : "";
|
|
899
|
+
printKeyValue(pos.protocol, `${formatUsd8(pos.amount)} ${pos.asset} @ ${pos.apy.toFixed(2)}% APY${earning}`);
|
|
900
|
+
}
|
|
901
|
+
const totalSaved = saves.reduce((s, p) => s + p.amount, 0);
|
|
902
|
+
if (saves.length > 1) {
|
|
903
|
+
printKeyValue("Total", formatUsd8(totalSaved));
|
|
904
|
+
}
|
|
905
|
+
if (hasRewards) {
|
|
906
|
+
printLine(` ${pc6.dim("Run claim-rewards to collect and convert to USDC")}`);
|
|
907
|
+
}
|
|
908
|
+
printBlank();
|
|
909
|
+
}
|
|
910
|
+
if (borrows.length > 0) {
|
|
911
|
+
printLine(pc6.bold("Borrows"));
|
|
912
|
+
printDivider();
|
|
913
|
+
for (const pos of borrows) {
|
|
914
|
+
printKeyValue(pos.protocol, `${formatUsd8(pos.amount)} ${pos.asset} @ ${pos.apy.toFixed(2)}% APY`);
|
|
915
|
+
}
|
|
916
|
+
const totalBorrowed = borrows.reduce((s, p) => s + p.amount, 0);
|
|
917
|
+
if (borrows.length > 1) {
|
|
918
|
+
printKeyValue("Total", formatUsd8(totalBorrowed));
|
|
919
|
+
}
|
|
920
|
+
printBlank();
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
} catch (error) {
|
|
924
|
+
handleError(error);
|
|
925
|
+
}
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// src/commands/earnings.ts
|
|
930
|
+
import pc7 from "picocolors";
|
|
931
|
+
import { T2000 as T200016, formatUsd as formatUsd9 } from "@t2000/sdk";
|
|
932
|
+
function registerEarnings(program2) {
|
|
933
|
+
program2.command("earnings").description("Show yield earned to date").option("--key <path>", "Key file path").action(async (opts) => {
|
|
934
|
+
try {
|
|
935
|
+
const pin = await resolvePin();
|
|
936
|
+
const agent = await T200016.create({ pin, keyPath: opts.key });
|
|
937
|
+
const result = await agent.earnings();
|
|
938
|
+
const pos = await agent.positions();
|
|
939
|
+
const savePositions = pos.positions.filter((p) => p.type === "save");
|
|
940
|
+
if (isJsonMode()) {
|
|
941
|
+
printJson({ ...result, positions: savePositions });
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
printBlank();
|
|
945
|
+
printKeyValue("Total Saved", formatUsd9(result.supplied));
|
|
946
|
+
if (savePositions.length > 0) {
|
|
947
|
+
for (const p of savePositions) {
|
|
948
|
+
printLine(` ${pc7.dim("\u2022")} ${formatUsd9(p.amount)} ${p.asset} on ${p.protocol} @ ${p.apy.toFixed(2)}% APY`);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
printKeyValue("Blended APY", `${result.currentApy.toFixed(2)}%`);
|
|
952
|
+
printKeyValue("Daily Yield", `~${formatUsd9(result.dailyEarning)}/day`);
|
|
953
|
+
printKeyValue("Est. Earned", `~${formatUsd9(result.totalYieldEarned)}`);
|
|
954
|
+
printBlank();
|
|
955
|
+
} catch (error) {
|
|
956
|
+
handleError(error);
|
|
957
|
+
}
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// src/commands/fundStatus.ts
|
|
962
|
+
import pc8 from "picocolors";
|
|
963
|
+
import { T2000 as T200017, formatUsd as formatUsd10 } from "@t2000/sdk";
|
|
964
|
+
function registerFundStatus(program2) {
|
|
965
|
+
program2.command("fund-status").description("Full savings summary").option("--key <path>", "Key file path").action(async (opts) => {
|
|
966
|
+
try {
|
|
967
|
+
const pin = await resolvePin();
|
|
968
|
+
const agent = await T200017.create({ pin, keyPath: opts.key });
|
|
969
|
+
const result = await agent.fundStatus();
|
|
970
|
+
const pos = await agent.positions();
|
|
971
|
+
const savePositions = pos.positions.filter((p) => p.type === "save");
|
|
972
|
+
if (isJsonMode()) {
|
|
973
|
+
printJson({ ...result, positions: savePositions });
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
printBlank();
|
|
977
|
+
if (result.supplied > 0) {
|
|
978
|
+
printSuccess("Savings: ACTIVE");
|
|
979
|
+
} else {
|
|
980
|
+
printInfo("Savings: INACTIVE \u2014 run `t2000 save <amount>` to start earning");
|
|
981
|
+
}
|
|
982
|
+
printBlank();
|
|
983
|
+
printKeyValue("Total Saved", formatUsd10(result.supplied));
|
|
984
|
+
if (savePositions.length > 0) {
|
|
985
|
+
for (const p of savePositions) {
|
|
986
|
+
printLine(` ${pc8.dim("\u2022")} ${formatUsd10(p.amount)} ${p.asset} on ${p.protocol} @ ${p.apy.toFixed(2)}% APY`);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
printKeyValue("Blended APY", `${result.apy.toFixed(2)}%`);
|
|
990
|
+
printKeyValue("Earned today", `~${formatUsd10(result.earnedToday)}`);
|
|
991
|
+
printKeyValue("Earned all time", `~${formatUsd10(result.earnedAllTime)}`);
|
|
992
|
+
printKeyValue("Monthly projected", `~${formatUsd10(result.projectedMonthly)}/month`);
|
|
993
|
+
printBlank();
|
|
994
|
+
if (result.supplied > 0) {
|
|
995
|
+
printInfo("Withdraw anytime: t2000 withdraw <amount>");
|
|
996
|
+
}
|
|
997
|
+
printBlank();
|
|
998
|
+
} catch (error) {
|
|
999
|
+
handleError(error);
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// src/commands/config.ts
|
|
1005
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "fs";
|
|
1006
|
+
import { join as join2 } from "path";
|
|
1007
|
+
import { homedir as homedir4 } from "os";
|
|
1008
|
+
import { SafeguardEnforcer as SafeguardEnforcer2 } from "@t2000/sdk";
|
|
1009
|
+
var CONFIG_DIR2 = join2(homedir4(), ".t2000");
|
|
1010
|
+
var CONFIG_PATH2 = join2(CONFIG_DIR2, "config.json");
|
|
1011
|
+
var SAFEGUARD_KEYS = /* @__PURE__ */ new Set(["locked", "maxPerTx", "maxDailySend", "dailyUsed", "dailyResetDate", "alertThreshold", "maxLeverage", "maxPositionSize"]);
|
|
1012
|
+
function getNestedValue(obj, path) {
|
|
1013
|
+
const parts = path.split(".");
|
|
1014
|
+
let current = obj;
|
|
1015
|
+
for (const part of parts) {
|
|
1016
|
+
if (current == null || typeof current !== "object") return void 0;
|
|
1017
|
+
current = current[part];
|
|
1018
|
+
}
|
|
1019
|
+
return current;
|
|
1020
|
+
}
|
|
1021
|
+
function setNestedValue(obj, path, value) {
|
|
1022
|
+
const parts = path.split(".");
|
|
1023
|
+
let current = obj;
|
|
1024
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
1025
|
+
const part = parts[i];
|
|
1026
|
+
if (!(part in current) || typeof current[part] !== "object" || current[part] === null) {
|
|
1027
|
+
current[part] = {};
|
|
1028
|
+
}
|
|
1029
|
+
current = current[part];
|
|
1030
|
+
}
|
|
1031
|
+
current[parts[parts.length - 1]] = value;
|
|
1032
|
+
}
|
|
1033
|
+
function loadConfig2() {
|
|
1034
|
+
try {
|
|
1035
|
+
return JSON.parse(readFileSync2(CONFIG_PATH2, "utf-8"));
|
|
1036
|
+
} catch {
|
|
1037
|
+
return {};
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
function saveConfig2(config) {
|
|
1041
|
+
if (!existsSync2(CONFIG_DIR2)) {
|
|
1042
|
+
mkdirSync2(CONFIG_DIR2, { recursive: true });
|
|
1043
|
+
}
|
|
1044
|
+
writeFileSync2(CONFIG_PATH2, JSON.stringify(config, null, 2) + "\n");
|
|
1045
|
+
}
|
|
1046
|
+
function formatUsd11(amount) {
|
|
1047
|
+
return `$${amount.toFixed(2)}`;
|
|
1048
|
+
}
|
|
1049
|
+
function registerConfig(program2) {
|
|
1050
|
+
const configCmd = program2.command("config").description("Show or set configuration");
|
|
1051
|
+
configCmd.command("show").description("Show safeguard settings").action(() => {
|
|
1052
|
+
try {
|
|
1053
|
+
const enforcer = new SafeguardEnforcer2(CONFIG_DIR2);
|
|
1054
|
+
enforcer.load();
|
|
1055
|
+
const config = enforcer.getConfig();
|
|
1056
|
+
if (isJsonMode()) {
|
|
1057
|
+
printJson({
|
|
1058
|
+
locked: config.locked,
|
|
1059
|
+
maxPerTx: config.maxPerTx,
|
|
1060
|
+
maxDailySend: config.maxDailySend,
|
|
1061
|
+
dailyUsed: config.dailyUsed
|
|
1062
|
+
});
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
printHeader("Agent Safeguards");
|
|
1066
|
+
printDivider();
|
|
1067
|
+
printKeyValue("Locked", config.locked ? "Yes" : "No");
|
|
1068
|
+
printKeyValue("Per-transaction", config.maxPerTx > 0 ? formatUsd11(config.maxPerTx) : "Unlimited");
|
|
1069
|
+
printKeyValue("Daily send limit", config.maxDailySend > 0 ? `${formatUsd11(config.maxDailySend)} (${formatUsd11(config.dailyUsed)} used today)` : "Unlimited");
|
|
1070
|
+
printBlank();
|
|
1071
|
+
} catch (error) {
|
|
1072
|
+
handleError(error);
|
|
1073
|
+
}
|
|
1074
|
+
});
|
|
1075
|
+
configCmd.command("get").argument("[key]", "Config key to get, supports dot notation (e.g. llm.provider)").action((key) => {
|
|
1076
|
+
try {
|
|
1077
|
+
const config = loadConfig2();
|
|
1078
|
+
if (key) {
|
|
1079
|
+
const value = key.includes(".") ? getNestedValue(config, key) : config[key];
|
|
1080
|
+
if (isJsonMode()) {
|
|
1081
|
+
printJson({ [key]: value });
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
printBlank();
|
|
1085
|
+
const display = typeof value === "object" ? JSON.stringify(value) : String(value ?? "(not set)");
|
|
1086
|
+
printKeyValue(key, display);
|
|
1087
|
+
} else {
|
|
1088
|
+
if (isJsonMode()) {
|
|
1089
|
+
printJson(config);
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
printBlank();
|
|
1093
|
+
if (Object.keys(config).length === 0) {
|
|
1094
|
+
printInfo("No configuration set.");
|
|
1095
|
+
} else {
|
|
1096
|
+
for (const [k, v] of Object.entries(config)) {
|
|
1097
|
+
const display = typeof v === "object" ? JSON.stringify(v) : String(v);
|
|
1098
|
+
printKeyValue(k, display);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
printBlank();
|
|
1103
|
+
} catch (error) {
|
|
1104
|
+
handleError(error);
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
configCmd.command("set").argument("<key>", "Config key, supports dot notation (e.g. llm.provider, channels.telegram.botToken)").argument("<value>", "Config value").action((key, value) => {
|
|
1108
|
+
try {
|
|
1109
|
+
const leafKey = key.includes(".") ? key.split(".").pop() : key;
|
|
1110
|
+
if (SAFEGUARD_KEYS.has(leafKey) && !key.includes(".")) {
|
|
1111
|
+
const enforcer = new SafeguardEnforcer2(CONFIG_DIR2);
|
|
1112
|
+
enforcer.load();
|
|
1113
|
+
let parsed2 = value;
|
|
1114
|
+
if (value === "true") parsed2 = true;
|
|
1115
|
+
else if (value === "false") parsed2 = false;
|
|
1116
|
+
else if (!isNaN(Number(value)) && value.trim() !== "") parsed2 = Number(value);
|
|
1117
|
+
enforcer.set(key, parsed2);
|
|
1118
|
+
if (isJsonMode()) {
|
|
1119
|
+
printJson({ [key]: parsed2 });
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
printBlank();
|
|
1123
|
+
printSuccess(`Set ${key} = ${String(parsed2)}`);
|
|
1124
|
+
printBlank();
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
const config = loadConfig2();
|
|
1128
|
+
let parsed = value;
|
|
1129
|
+
if (value === "true") parsed = true;
|
|
1130
|
+
else if (value === "false") parsed = false;
|
|
1131
|
+
else if (!isNaN(Number(value)) && value.trim() !== "") parsed = Number(value);
|
|
1132
|
+
if (value.startsWith("[") || value.startsWith("{")) {
|
|
1133
|
+
try {
|
|
1134
|
+
parsed = JSON.parse(value);
|
|
1135
|
+
} catch {
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
if (key.includes(".")) {
|
|
1139
|
+
setNestedValue(config, key, parsed);
|
|
1140
|
+
} else {
|
|
1141
|
+
config[key] = parsed;
|
|
1142
|
+
}
|
|
1143
|
+
saveConfig2(config);
|
|
1144
|
+
if (isJsonMode()) {
|
|
1145
|
+
printJson({ [key]: parsed });
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
printBlank();
|
|
1149
|
+
printSuccess(`Set ${key} = ${String(parsed)}`);
|
|
1150
|
+
printBlank();
|
|
1151
|
+
} catch (error) {
|
|
1152
|
+
handleError(error);
|
|
1153
|
+
}
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// src/commands/serve.ts
|
|
1158
|
+
import { serve } from "@hono/node-server";
|
|
1159
|
+
import { Hono } from "hono";
|
|
1160
|
+
import { cors } from "hono/cors";
|
|
1161
|
+
import { randomBytes } from "crypto";
|
|
1162
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync3 } from "fs";
|
|
1163
|
+
import { join as join3 } from "path";
|
|
1164
|
+
import { homedir as homedir5 } from "os";
|
|
1165
|
+
import { T2000 as T200018 } from "@t2000/sdk";
|
|
1166
|
+
import { streamSSE } from "hono/streaming";
|
|
1167
|
+
var CONFIG_DIR3 = join3(homedir5(), ".t2000");
|
|
1168
|
+
var TOKEN_PATH = join3(CONFIG_DIR3, "config.json");
|
|
1169
|
+
function generateToken() {
|
|
1170
|
+
return `t2k_${randomBytes(24).toString("hex")}`;
|
|
1171
|
+
}
|
|
1172
|
+
function saveToken(token) {
|
|
1173
|
+
if (!existsSync3(CONFIG_DIR3)) {
|
|
1174
|
+
mkdirSync3(CONFIG_DIR3, { recursive: true });
|
|
1175
|
+
}
|
|
1176
|
+
let config = {};
|
|
1177
|
+
try {
|
|
1178
|
+
config = JSON.parse(readFileSync3(TOKEN_PATH, "utf-8"));
|
|
1179
|
+
} catch {
|
|
1180
|
+
}
|
|
1181
|
+
config.authToken = token;
|
|
1182
|
+
writeFileSync3(TOKEN_PATH, JSON.stringify(config, null, 2) + "\n");
|
|
1183
|
+
}
|
|
1184
|
+
function envelope(data) {
|
|
1185
|
+
return { success: true, data, timestamp: Math.floor(Date.now() / 1e3) };
|
|
1186
|
+
}
|
|
1187
|
+
function errorResponse(code, message, data, retryable = false) {
|
|
1188
|
+
return {
|
|
1189
|
+
success: false,
|
|
1190
|
+
error: { code, message, data, retryable },
|
|
1191
|
+
timestamp: Math.floor(Date.now() / 1e3)
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
function registerServe(program2) {
|
|
1195
|
+
program2.command("serve").description("Start HTTP API server").option("--port <port>", "Port number", "3001").option("--rate-limit <rps>", "Max requests per second", "10").option("--key <path>", "Key file path").action(async (opts) => {
|
|
1196
|
+
try {
|
|
1197
|
+
const pin = await resolvePin();
|
|
1198
|
+
const agent = await T200018.create({ pin, keyPath: opts.key });
|
|
1199
|
+
const port = parseInt(opts.port, 10);
|
|
1200
|
+
const rateLimit = parseInt(opts.rateLimit, 10);
|
|
1201
|
+
const token = generateToken();
|
|
1202
|
+
saveToken(token);
|
|
1203
|
+
const app = buildApp(agent, token, rateLimit);
|
|
1204
|
+
serve({ fetch: app.fetch, port });
|
|
1205
|
+
console.log(` \u2713 API server running on http://localhost:${port}`);
|
|
1206
|
+
console.log(` \u2713 Auth token: ${token}`);
|
|
1207
|
+
console.log("");
|
|
1208
|
+
console.log(" Endpoints:");
|
|
1209
|
+
console.log(" GET /v1/balance POST /v1/send");
|
|
1210
|
+
console.log(" GET /v1/address POST /v1/save");
|
|
1211
|
+
console.log(" GET /v1/history POST /v1/withdraw");
|
|
1212
|
+
console.log(" GET /v1/earnings POST /v1/borrow");
|
|
1213
|
+
console.log(" GET /v1/rates POST /v1/repay");
|
|
1214
|
+
console.log(" GET /v1/health-factor");
|
|
1215
|
+
console.log(" GET /v1/positions");
|
|
1216
|
+
console.log(" GET /v1/events (SSE)");
|
|
1217
|
+
console.log("");
|
|
1218
|
+
} catch (error) {
|
|
1219
|
+
handleError(error);
|
|
1220
|
+
}
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
function buildApp(agent, authToken, rateLimit) {
|
|
1224
|
+
const app = new Hono();
|
|
1225
|
+
app.use("*", cors());
|
|
1226
|
+
const requestLog = [];
|
|
1227
|
+
app.use("/v1/*", async (c, next) => {
|
|
1228
|
+
const now = Date.now();
|
|
1229
|
+
const windowMs = 1e3;
|
|
1230
|
+
while (requestLog.length > 0 && requestLog[0] < now - windowMs) {
|
|
1231
|
+
requestLog.shift();
|
|
1232
|
+
}
|
|
1233
|
+
if (requestLog.length >= rateLimit) {
|
|
1234
|
+
c.status(429);
|
|
1235
|
+
c.header("Retry-After", "1");
|
|
1236
|
+
return c.json(errorResponse("RATE_LIMITED", "Too many requests", null, true));
|
|
1237
|
+
}
|
|
1238
|
+
requestLog.push(now);
|
|
1239
|
+
await next();
|
|
1240
|
+
});
|
|
1241
|
+
app.use("/v1/*", async (c, next) => {
|
|
1242
|
+
const auth = c.req.header("Authorization");
|
|
1243
|
+
if (!auth || auth !== `Bearer ${authToken}`) {
|
|
1244
|
+
c.status(401);
|
|
1245
|
+
return c.json(errorResponse("UNAUTHORIZED", "Invalid or missing Bearer token"));
|
|
1246
|
+
}
|
|
1247
|
+
await next();
|
|
1248
|
+
});
|
|
1249
|
+
app.get("/v1/address", (c) => {
|
|
1250
|
+
return c.json(envelope({ address: agent.address }));
|
|
1251
|
+
});
|
|
1252
|
+
app.get("/v1/balance", async (c) => {
|
|
1253
|
+
try {
|
|
1254
|
+
const balance = await agent.balance();
|
|
1255
|
+
return c.json(envelope(balance));
|
|
1256
|
+
} catch (err) {
|
|
1257
|
+
return c.json(errorResponse("BALANCE_ERROR", errMsg(err)), 500);
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
app.get("/v1/history", async (c) => {
|
|
1261
|
+
try {
|
|
1262
|
+
const limit = parseInt(c.req.query("limit") ?? "20", 10);
|
|
1263
|
+
const history = await agent.history({ limit });
|
|
1264
|
+
return c.json(envelope(history));
|
|
1265
|
+
} catch (err) {
|
|
1266
|
+
return c.json(errorResponse("HISTORY_ERROR", errMsg(err)), 500);
|
|
1267
|
+
}
|
|
1268
|
+
});
|
|
1269
|
+
app.get("/v1/deposit", (c) => {
|
|
1270
|
+
return c.json(envelope({
|
|
1271
|
+
address: agent.address,
|
|
1272
|
+
network: "mainnet",
|
|
1273
|
+
instructions: "Send USDC to this address on Sui mainnet."
|
|
1274
|
+
}));
|
|
1275
|
+
});
|
|
1276
|
+
app.get("/v1/earnings", async (c) => {
|
|
1277
|
+
try {
|
|
1278
|
+
const earnings = await agent.earnings();
|
|
1279
|
+
return c.json(envelope(earnings));
|
|
1280
|
+
} catch (err) {
|
|
1281
|
+
return c.json(errorResponse("EARNINGS_ERROR", errMsg(err)), 500);
|
|
1282
|
+
}
|
|
1283
|
+
});
|
|
1284
|
+
app.get("/v1/rates", async (c) => {
|
|
1285
|
+
try {
|
|
1286
|
+
const rates = await agent.rates();
|
|
1287
|
+
return c.json(envelope(rates));
|
|
1288
|
+
} catch (err) {
|
|
1289
|
+
return c.json(errorResponse("RATES_ERROR", errMsg(err)), 500);
|
|
1290
|
+
}
|
|
1291
|
+
});
|
|
1292
|
+
app.get("/v1/health-factor", async (c) => {
|
|
1293
|
+
try {
|
|
1294
|
+
const hf = await agent.healthFactor();
|
|
1295
|
+
return c.json(envelope(hf));
|
|
1296
|
+
} catch (err) {
|
|
1297
|
+
return c.json(errorResponse("HEALTH_ERROR", errMsg(err)), 500);
|
|
1298
|
+
}
|
|
1299
|
+
});
|
|
1300
|
+
app.get("/v1/max-withdraw", async (c) => {
|
|
1301
|
+
try {
|
|
1302
|
+
const result = await agent.maxWithdraw();
|
|
1303
|
+
return c.json(envelope(result));
|
|
1304
|
+
} catch (err) {
|
|
1305
|
+
return c.json(errorResponse("MAX_WITHDRAW_ERROR", errMsg(err)), 500);
|
|
1306
|
+
}
|
|
1307
|
+
});
|
|
1308
|
+
app.get("/v1/max-borrow", async (c) => {
|
|
1309
|
+
try {
|
|
1310
|
+
const result = await agent.maxBorrow();
|
|
1311
|
+
return c.json(envelope(result));
|
|
1312
|
+
} catch (err) {
|
|
1313
|
+
return c.json(errorResponse("MAX_BORROW_ERROR", errMsg(err)), 500);
|
|
1314
|
+
}
|
|
1315
|
+
});
|
|
1316
|
+
app.get("/v1/positions", async (c) => {
|
|
1317
|
+
try {
|
|
1318
|
+
const positions = await agent.positions();
|
|
1319
|
+
return c.json(envelope(positions));
|
|
1320
|
+
} catch (err) {
|
|
1321
|
+
return c.json(errorResponse("POSITIONS_ERROR", errMsg(err)), 500);
|
|
1322
|
+
}
|
|
1323
|
+
});
|
|
1324
|
+
app.post("/v1/send", async (c) => {
|
|
1325
|
+
try {
|
|
1326
|
+
const body = await c.req.json();
|
|
1327
|
+
const { to, amount, asset } = body;
|
|
1328
|
+
if (!to || !amount) {
|
|
1329
|
+
c.status(400);
|
|
1330
|
+
return c.json(errorResponse("INVALID_PARAMS", "Required: to, amount"));
|
|
1331
|
+
}
|
|
1332
|
+
const result = await agent.send({ to, amount, asset: asset ?? "USDC" });
|
|
1333
|
+
return c.json(envelope(result));
|
|
1334
|
+
} catch (err) {
|
|
1335
|
+
c.status(getStatusCode(err));
|
|
1336
|
+
return c.json(handleApiError(err));
|
|
1337
|
+
}
|
|
1338
|
+
});
|
|
1339
|
+
app.post("/v1/save", async (c) => {
|
|
1340
|
+
try {
|
|
1341
|
+
const body = await c.req.json();
|
|
1342
|
+
const { amount } = body;
|
|
1343
|
+
if (!amount) {
|
|
1344
|
+
c.status(400);
|
|
1345
|
+
return c.json(errorResponse("INVALID_PARAMS", "Required: amount"));
|
|
1346
|
+
}
|
|
1347
|
+
const result = await agent.save({ amount });
|
|
1348
|
+
return c.json(envelope(result));
|
|
1349
|
+
} catch (err) {
|
|
1350
|
+
c.status(getStatusCode(err));
|
|
1351
|
+
return c.json(handleApiError(err));
|
|
1352
|
+
}
|
|
1353
|
+
});
|
|
1354
|
+
app.post("/v1/supply", async (c) => {
|
|
1355
|
+
try {
|
|
1356
|
+
const body = await c.req.json();
|
|
1357
|
+
const { amount } = body;
|
|
1358
|
+
if (!amount) {
|
|
1359
|
+
c.status(400);
|
|
1360
|
+
return c.json(errorResponse("INVALID_PARAMS", "Required: amount"));
|
|
1361
|
+
}
|
|
1362
|
+
const result = await agent.save({ amount });
|
|
1363
|
+
return c.json(envelope(result));
|
|
1364
|
+
} catch (err) {
|
|
1365
|
+
c.status(getStatusCode(err));
|
|
1366
|
+
return c.json(handleApiError(err));
|
|
1367
|
+
}
|
|
1368
|
+
});
|
|
1369
|
+
app.post("/v1/withdraw", async (c) => {
|
|
1370
|
+
try {
|
|
1371
|
+
const body = await c.req.json();
|
|
1372
|
+
const { amount } = body;
|
|
1373
|
+
if (!amount) {
|
|
1374
|
+
c.status(400);
|
|
1375
|
+
return c.json(errorResponse("INVALID_PARAMS", "Required: amount"));
|
|
1376
|
+
}
|
|
1377
|
+
const result = await agent.withdraw({ amount });
|
|
1378
|
+
return c.json(envelope(result));
|
|
1379
|
+
} catch (err) {
|
|
1380
|
+
c.status(getStatusCode(err));
|
|
1381
|
+
return c.json(handleApiError(err));
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
app.post("/v1/borrow", async (c) => {
|
|
1385
|
+
try {
|
|
1386
|
+
const body = await c.req.json();
|
|
1387
|
+
const { amount } = body;
|
|
1388
|
+
if (!amount) {
|
|
1389
|
+
c.status(400);
|
|
1390
|
+
return c.json(errorResponse("INVALID_PARAMS", "Required: amount"));
|
|
1391
|
+
}
|
|
1392
|
+
const result = await agent.borrow({ amount });
|
|
1393
|
+
return c.json(envelope(result));
|
|
1394
|
+
} catch (err) {
|
|
1395
|
+
c.status(getStatusCode(err));
|
|
1396
|
+
return c.json(handleApiError(err));
|
|
1397
|
+
}
|
|
1398
|
+
});
|
|
1399
|
+
app.post("/v1/repay", async (c) => {
|
|
1400
|
+
try {
|
|
1401
|
+
const body = await c.req.json();
|
|
1402
|
+
const { amount } = body;
|
|
1403
|
+
if (!amount) {
|
|
1404
|
+
c.status(400);
|
|
1405
|
+
return c.json(errorResponse("INVALID_PARAMS", "Required: amount"));
|
|
1406
|
+
}
|
|
1407
|
+
const result = await agent.repay({ amount });
|
|
1408
|
+
return c.json(envelope(result));
|
|
1409
|
+
} catch (err) {
|
|
1410
|
+
c.status(getStatusCode(err));
|
|
1411
|
+
return c.json(handleApiError(err));
|
|
1412
|
+
}
|
|
1413
|
+
});
|
|
1414
|
+
app.get("/v1/events", async (c) => {
|
|
1415
|
+
const subscribeParam = c.req.query("subscribe") ?? "yield,balanceChange,healthWarning";
|
|
1416
|
+
const subscriptions = new Set(subscribeParam.split(",").map((s) => s.trim()));
|
|
1417
|
+
return streamSSE(c, async (stream) => {
|
|
1418
|
+
const handlers = [];
|
|
1419
|
+
for (const eventName of subscriptions) {
|
|
1420
|
+
const handler = (data) => {
|
|
1421
|
+
stream.writeSSE({ event: eventName, data: JSON.stringify(data) }).catch(() => {
|
|
1422
|
+
});
|
|
1423
|
+
};
|
|
1424
|
+
agent.on(eventName, handler);
|
|
1425
|
+
handlers.push({
|
|
1426
|
+
event: eventName,
|
|
1427
|
+
off: () => agent.off(eventName, handler)
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
const keepAlive = setInterval(() => {
|
|
1431
|
+
stream.writeSSE({ event: "ping", data: "{}" }).catch(() => {
|
|
1432
|
+
});
|
|
1433
|
+
}, 3e4);
|
|
1434
|
+
stream.onAbort(() => {
|
|
1435
|
+
clearInterval(keepAlive);
|
|
1436
|
+
for (const h of handlers) h.off();
|
|
1437
|
+
});
|
|
1438
|
+
await new Promise(() => {
|
|
1439
|
+
});
|
|
1440
|
+
});
|
|
1441
|
+
});
|
|
1442
|
+
return app;
|
|
1443
|
+
}
|
|
1444
|
+
function errMsg(err) {
|
|
1445
|
+
return err instanceof Error ? err.message : String(err);
|
|
1446
|
+
}
|
|
1447
|
+
function handleApiError(err) {
|
|
1448
|
+
const t2kErr = err;
|
|
1449
|
+
return errorResponse(
|
|
1450
|
+
t2kErr.code ?? "UNKNOWN",
|
|
1451
|
+
t2kErr.message ?? errMsg(err),
|
|
1452
|
+
t2kErr.data,
|
|
1453
|
+
isRetryable(t2kErr.code)
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
1456
|
+
function getStatusCode(err) {
|
|
1457
|
+
const code = err.code;
|
|
1458
|
+
if (!code) return 500;
|
|
1459
|
+
if (code === "INSUFFICIENT_BALANCE") return 400;
|
|
1460
|
+
if (code === "INVALID_ADDRESS") return 400;
|
|
1461
|
+
if (code === "WITHDRAW_WOULD_LIQUIDATE") return 400;
|
|
1462
|
+
if (code === "NO_COLLATERAL") return 400;
|
|
1463
|
+
return 500;
|
|
1464
|
+
}
|
|
1465
|
+
function isRetryable(code) {
|
|
1466
|
+
if (!code) return false;
|
|
1467
|
+
return ["RPC_ERROR", "RPC_UNREACHABLE", "SPONSOR_UNAVAILABLE", "AUTO_TOPUP_FAILED"].includes(code);
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
// src/commands/pay.ts
|
|
1471
|
+
import pc9 from "picocolors";
|
|
1472
|
+
import { T2000 as T200019 } from "@t2000/sdk";
|
|
1473
|
+
import { x402Client } from "@t2000/x402";
|
|
1474
|
+
function createX402Wallet(agent) {
|
|
1475
|
+
return {
|
|
1476
|
+
client: agent.suiClient,
|
|
1477
|
+
keypair: agent.signer,
|
|
1478
|
+
address: () => agent.address(),
|
|
1479
|
+
signAndExecute: async (tx) => {
|
|
1480
|
+
const result = await agent.suiClient.signAndExecuteTransaction({
|
|
1481
|
+
signer: agent.signer,
|
|
1482
|
+
transaction: tx
|
|
1483
|
+
});
|
|
1484
|
+
return { digest: result.digest };
|
|
1485
|
+
}
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
function registerPay(program2) {
|
|
1489
|
+
program2.command("pay <url>").description("Pay for an x402-protected API resource").option("--key <path>", "Key file path").option("--method <method>", "HTTP method (GET, POST, PUT)", "GET").option("--data <json>", "Request body for POST/PUT").option("--header <key=value>", "Additional HTTP header (repeatable)", collectHeaders, {}).option("--max-price <amount>", "Max USDC price to auto-approve", "1.00").option("--timeout <seconds>", "Request timeout in seconds", "30").option("--dry-run", "Show what would be paid without paying").action(async (url, opts) => {
|
|
1490
|
+
try {
|
|
1491
|
+
const pin = await resolvePin();
|
|
1492
|
+
const agent = await T200019.create({ pin, keyPath: opts.key });
|
|
1493
|
+
agent.enforcer.check({ operation: "pay", amount: parseFloat(opts.maxPrice) });
|
|
1494
|
+
const wallet = createX402Wallet(agent);
|
|
1495
|
+
const client = new x402Client(wallet);
|
|
1496
|
+
const startTime = Date.now();
|
|
1497
|
+
if (!isJsonMode()) {
|
|
1498
|
+
printBlank();
|
|
1499
|
+
printInfo(`\u2192 ${opts.method} ${url}`);
|
|
1500
|
+
}
|
|
1501
|
+
const response = await client.fetch(url, {
|
|
1502
|
+
method: opts.method,
|
|
1503
|
+
headers: opts.header,
|
|
1504
|
+
body: opts.data,
|
|
1505
|
+
maxPrice: parseFloat(opts.maxPrice),
|
|
1506
|
+
timeout: parseInt(opts.timeout, 10) * 1e3,
|
|
1507
|
+
dryRun: opts.dryRun,
|
|
1508
|
+
onPayment: (details) => {
|
|
1509
|
+
if (!isJsonMode()) {
|
|
1510
|
+
printInfo(`\u2190 402 Payment Required: $${details.amount} USDC (Sui)`);
|
|
1511
|
+
printSuccess(`Paid $${details.amount} USDC (tx: ${details.txHash.slice(0, 10)}...)`);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
});
|
|
1515
|
+
const elapsed = Date.now() - startTime;
|
|
1516
|
+
if (!isJsonMode()) {
|
|
1517
|
+
printInfo(`\u2190 ${response.status} ${response.statusText || "OK"} ${pc9.dim(`[${elapsed}ms]`)}`);
|
|
1518
|
+
}
|
|
1519
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
1520
|
+
const body = contentType.includes("application/json") ? await response.json() : await response.text();
|
|
1521
|
+
if (isJsonMode()) {
|
|
1522
|
+
printJson({
|
|
1523
|
+
status: response.status,
|
|
1524
|
+
url,
|
|
1525
|
+
elapsed,
|
|
1526
|
+
body
|
|
1527
|
+
});
|
|
1528
|
+
} else {
|
|
1529
|
+
printBlank();
|
|
1530
|
+
if (typeof body === "string") {
|
|
1531
|
+
console.log(body);
|
|
1532
|
+
} else {
|
|
1533
|
+
console.log(JSON.stringify(body, null, 2));
|
|
1534
|
+
}
|
|
1535
|
+
printBlank();
|
|
1536
|
+
}
|
|
1537
|
+
} catch (error) {
|
|
1538
|
+
handleError(error);
|
|
1539
|
+
}
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
function collectHeaders(value, previous) {
|
|
1543
|
+
const [key, ...rest] = value.split("=");
|
|
1544
|
+
if (key && rest.length > 0) {
|
|
1545
|
+
previous[key.trim()] = rest.join("=").trim();
|
|
1546
|
+
}
|
|
1547
|
+
return previous;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
// src/commands/lock.ts
|
|
1551
|
+
import { join as join4 } from "path";
|
|
1552
|
+
import { homedir as homedir6 } from "os";
|
|
1553
|
+
import { SafeguardEnforcer as SafeguardEnforcer3 } from "@t2000/sdk";
|
|
1554
|
+
var CONFIG_DIR4 = join4(homedir6(), ".t2000");
|
|
1555
|
+
function registerLock(program2) {
|
|
1556
|
+
program2.command("lock").description("Lock agent \u2014 freeze all operations").action(async () => {
|
|
1557
|
+
try {
|
|
1558
|
+
const enforcer = new SafeguardEnforcer3(CONFIG_DIR4);
|
|
1559
|
+
enforcer.load();
|
|
1560
|
+
enforcer.lock();
|
|
1561
|
+
await clearSession();
|
|
1562
|
+
if (isJsonMode()) {
|
|
1563
|
+
printJson({ locked: true });
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
printBlank();
|
|
1567
|
+
printSuccess("Agent locked. All operations frozen.");
|
|
1568
|
+
printInfo("Run: t2000 unlock (requires PIN)");
|
|
1569
|
+
printBlank();
|
|
1570
|
+
} catch (error) {
|
|
1571
|
+
handleError(error);
|
|
1572
|
+
}
|
|
1573
|
+
});
|
|
1574
|
+
program2.command("unlock").description("Unlock agent \u2014 resume operations").action(async () => {
|
|
1575
|
+
try {
|
|
1576
|
+
const pin = await resolvePin();
|
|
1577
|
+
if (!pin) {
|
|
1578
|
+
throw new Error("PIN required to unlock agent");
|
|
1579
|
+
}
|
|
1580
|
+
const { T2000: T200028 } = await import("@t2000/sdk");
|
|
1581
|
+
await T200028.create({ pin });
|
|
1582
|
+
const enforcer = new SafeguardEnforcer3(CONFIG_DIR4);
|
|
1583
|
+
enforcer.load();
|
|
1584
|
+
enforcer.unlock();
|
|
1585
|
+
if (isJsonMode()) {
|
|
1586
|
+
printJson({ locked: false });
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
const config = enforcer.getConfig();
|
|
1590
|
+
printBlank();
|
|
1591
|
+
printSuccess("Agent unlocked. Operations resumed.");
|
|
1592
|
+
if (config.maxPerTx > 0 || config.maxDailySend > 0) {
|
|
1593
|
+
const limits = [];
|
|
1594
|
+
if (config.maxPerTx > 0) limits.push(`maxPerTx=$${config.maxPerTx}`);
|
|
1595
|
+
if (config.maxDailySend > 0) limits.push(`maxDailySend=$${config.maxDailySend}`);
|
|
1596
|
+
printInfo(`Active safeguards: ${limits.join(", ")}`);
|
|
1597
|
+
}
|
|
1598
|
+
printBlank();
|
|
1599
|
+
} catch (error) {
|
|
1600
|
+
handleError(error);
|
|
1601
|
+
}
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
// src/commands/sentinel.ts
|
|
1606
|
+
import { T2000 as T200020, MIST_PER_SUI } from "@t2000/sdk";
|
|
1607
|
+
import pc10 from "picocolors";
|
|
1608
|
+
function formatSui(mist) {
|
|
1609
|
+
return (Number(mist) / Number(MIST_PER_SUI)).toFixed(2);
|
|
1610
|
+
}
|
|
1611
|
+
function registerSentinel(program2) {
|
|
1612
|
+
const sentinel = program2.command("sentinel").description("Interact with Sui Sentinel \u2014 attack AI agents, earn bounties");
|
|
1613
|
+
sentinel.command("list").description("List active sentinels with prize pools").action(async () => {
|
|
1614
|
+
try {
|
|
1615
|
+
const pin = await resolvePin();
|
|
1616
|
+
const agent = await T200020.create({ pin });
|
|
1617
|
+
const sentinels = await agent.sentinelList();
|
|
1618
|
+
if (isJsonMode()) {
|
|
1619
|
+
printJson(sentinels.map((s) => ({
|
|
1620
|
+
...s,
|
|
1621
|
+
attackFee: s.attackFee.toString(),
|
|
1622
|
+
prizePool: s.prizePool.toString()
|
|
1623
|
+
})));
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
if (sentinels.length === 0) {
|
|
1627
|
+
printBlank();
|
|
1628
|
+
printInfo("No active sentinels found.");
|
|
1629
|
+
printBlank();
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
printHeader("Active Sentinels");
|
|
1633
|
+
sentinels.forEach((s) => {
|
|
1634
|
+
const pool = `${formatSui(s.prizePool)} SUI`.padEnd(12);
|
|
1635
|
+
const fee = `${formatSui(s.attackFee)} SUI`.padEnd(12);
|
|
1636
|
+
printLine(` ${s.name}`);
|
|
1637
|
+
printLine(` ${pc10.dim(`Pool: ${pool}Fee: ${fee}Attacks: ${s.totalAttacks}`)}`);
|
|
1638
|
+
printLine(` ${pc10.dim(s.objectId)}`);
|
|
1639
|
+
printBlank();
|
|
1640
|
+
});
|
|
1641
|
+
printBlank();
|
|
1642
|
+
printInfo(`${sentinels.length} active sentinel${sentinels.length === 1 ? "" : "s"}`);
|
|
1643
|
+
printBlank();
|
|
1644
|
+
} catch (error) {
|
|
1645
|
+
handleError(error);
|
|
1646
|
+
}
|
|
1647
|
+
});
|
|
1648
|
+
sentinel.command("info").description("Show details for a sentinel").argument("<id>", "Sentinel object ID").action(async (id) => {
|
|
1649
|
+
try {
|
|
1650
|
+
const pin = await resolvePin();
|
|
1651
|
+
const agent = await T200020.create({ pin });
|
|
1652
|
+
const s = await agent.sentinelInfo(id);
|
|
1653
|
+
if (isJsonMode()) {
|
|
1654
|
+
printJson({
|
|
1655
|
+
...s,
|
|
1656
|
+
attackFee: s.attackFee.toString(),
|
|
1657
|
+
prizePool: s.prizePool.toString()
|
|
1658
|
+
});
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
printHeader(s.name);
|
|
1662
|
+
printKeyValue("Object ID", s.objectId);
|
|
1663
|
+
printKeyValue("Agent ID", s.id);
|
|
1664
|
+
printKeyValue("Model", s.model);
|
|
1665
|
+
printKeyValue("State", s.state);
|
|
1666
|
+
printKeyValue("Attack Fee", `${formatSui(s.attackFee)} SUI`);
|
|
1667
|
+
printKeyValue("Prize Pool", `${formatSui(s.prizePool)} SUI`);
|
|
1668
|
+
printKeyValue("Total Attacks", String(s.totalAttacks));
|
|
1669
|
+
printKeyValue("Breaches", String(s.successfulBreaches));
|
|
1670
|
+
if (s.systemPrompt) {
|
|
1671
|
+
printBlank();
|
|
1672
|
+
printKeyValue("System Prompt", "");
|
|
1673
|
+
printLine(` ${pc10.dim(s.systemPrompt.slice(0, 500))}`);
|
|
1674
|
+
}
|
|
1675
|
+
printBlank();
|
|
1676
|
+
} catch (error) {
|
|
1677
|
+
handleError(error);
|
|
1678
|
+
}
|
|
1679
|
+
});
|
|
1680
|
+
sentinel.command("attack").description("Attack a sentinel with a prompt (costs SUI)").argument("<id>", "Sentinel object ID").argument("[prompt]", "Attack prompt").option("--fee <sui>", "Override attack fee in SUI").option("--key <path>", "Key file path").action(async (id, prompt, opts) => {
|
|
1681
|
+
try {
|
|
1682
|
+
if (!prompt) {
|
|
1683
|
+
throw new Error('Prompt is required. Usage: t2000 sentinel attack <id> "your prompt"');
|
|
1684
|
+
}
|
|
1685
|
+
const pin = await resolvePin();
|
|
1686
|
+
const agent = await T200020.create({ pin, keyPath: opts.key });
|
|
1687
|
+
const feeMist = opts.fee ? BigInt(Math.round(parseFloat(opts.fee) * Number(MIST_PER_SUI))) : void 0;
|
|
1688
|
+
if (isJsonMode()) {
|
|
1689
|
+
const result2 = await agent.sentinelAttack(id, prompt, feeMist);
|
|
1690
|
+
printJson({
|
|
1691
|
+
...result2,
|
|
1692
|
+
verdict: {
|
|
1693
|
+
...result2.verdict
|
|
1694
|
+
}
|
|
1695
|
+
});
|
|
1696
|
+
return;
|
|
1697
|
+
}
|
|
1698
|
+
printBlank();
|
|
1699
|
+
printLine(` ${pc10.dim("\u23F3")} Requesting attack...`);
|
|
1700
|
+
const result = await agent.sentinelAttack(id, prompt, feeMist);
|
|
1701
|
+
printBlank();
|
|
1702
|
+
if (result.won) {
|
|
1703
|
+
printSuccess(`BREACHED! (score: ${result.verdict.score}/100)`);
|
|
1704
|
+
} else {
|
|
1705
|
+
printError(`DEFENDED (score: ${result.verdict.score}/100)`);
|
|
1706
|
+
}
|
|
1707
|
+
printBlank();
|
|
1708
|
+
printKeyValue("Agent", result.verdict.agentResponse.slice(0, 200));
|
|
1709
|
+
printKeyValue("Jury", result.verdict.juryResponse.slice(0, 200));
|
|
1710
|
+
if (result.verdict.funResponse) {
|
|
1711
|
+
printKeyValue("Fun", result.verdict.funResponse.slice(0, 200));
|
|
1712
|
+
}
|
|
1713
|
+
printBlank();
|
|
1714
|
+
printKeyValue("Fee Paid", `${result.feePaid} SUI`);
|
|
1715
|
+
printKeyValue("Request Tx", explorerUrl(result.requestTx));
|
|
1716
|
+
printKeyValue("Settle Tx", explorerUrl(result.settleTx));
|
|
1717
|
+
printBlank();
|
|
1718
|
+
} catch (error) {
|
|
1719
|
+
handleError(error);
|
|
1720
|
+
}
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
// src/commands/earn.ts
|
|
1725
|
+
import { T2000 as T200021, MIST_PER_SUI as MIST_PER_SUI2, listSentinels, formatUsd as formatUsd12 } from "@t2000/sdk";
|
|
1726
|
+
import pc11 from "picocolors";
|
|
1727
|
+
function mistToSui(mist) {
|
|
1728
|
+
return Number(mist) / Number(MIST_PER_SUI2);
|
|
1729
|
+
}
|
|
1730
|
+
function bestTarget(sentinels) {
|
|
1731
|
+
const withPool = sentinels.filter((s) => s.prizePool > 0n && s.attackFee > 0n);
|
|
1732
|
+
if (withPool.length === 0) return void 0;
|
|
1733
|
+
return withPool.sort((a, b) => {
|
|
1734
|
+
const ratioA = Number(a.prizePool) / Number(a.attackFee);
|
|
1735
|
+
const ratioB = Number(b.prizePool) / Number(b.attackFee);
|
|
1736
|
+
return ratioB - ratioA;
|
|
1737
|
+
})[0];
|
|
1738
|
+
}
|
|
1739
|
+
function registerEarn(program2) {
|
|
1740
|
+
program2.command("earn").description("Show all earning opportunities \u2014 savings yield + sentinel bounties").option("--key <path>", "Key file path").action(async (opts) => {
|
|
1741
|
+
try {
|
|
1742
|
+
const pin = await resolvePin();
|
|
1743
|
+
const agent = await T200021.create({ pin, keyPath: opts.key });
|
|
1744
|
+
const [positionsResult, portfolioResult, ratesResult, sentinels] = await Promise.allSettled([
|
|
1745
|
+
agent.positions(),
|
|
1746
|
+
agent.getPortfolio(),
|
|
1747
|
+
agent.allRates("USDC"),
|
|
1748
|
+
listSentinels()
|
|
1749
|
+
]);
|
|
1750
|
+
const posData = positionsResult.status === "fulfilled" ? positionsResult.value : null;
|
|
1751
|
+
const portfolio = portfolioResult.status === "fulfilled" ? portfolioResult.value : null;
|
|
1752
|
+
const ratesData = ratesResult.status === "fulfilled" ? ratesResult.value : null;
|
|
1753
|
+
const agents = sentinels.status === "fulfilled" ? sentinels.value : null;
|
|
1754
|
+
const savePositions = posData?.positions.filter((p) => p.type === "save") ?? [];
|
|
1755
|
+
const totalSaved = savePositions.reduce((s, p) => s + p.amount, 0);
|
|
1756
|
+
const earningInvestments = portfolio?.positions.filter((p) => p.earning && p.currentValue > 0) ?? [];
|
|
1757
|
+
const bestSaveApy = ratesData?.length ? Math.max(...ratesData.map((r) => r.rates.saveApy)) : 0;
|
|
1758
|
+
if (isJsonMode()) {
|
|
1759
|
+
const best = agents ? bestTarget(agents) : void 0;
|
|
1760
|
+
const totalPool = agents ? agents.reduce((sum, s) => sum + mistToSui(s.prizePool), 0) : 0;
|
|
1761
|
+
const cheapest = agents ? Math.min(...agents.map((s) => mistToSui(s.attackFee))) : 0;
|
|
1762
|
+
printJson({
|
|
1763
|
+
savings: savePositions.map((p) => ({
|
|
1764
|
+
protocol: p.protocol,
|
|
1765
|
+
asset: p.asset,
|
|
1766
|
+
amount: p.amount,
|
|
1767
|
+
apy: p.apy
|
|
1768
|
+
})),
|
|
1769
|
+
totalSaved,
|
|
1770
|
+
availableRates: ratesData?.map((r) => ({
|
|
1771
|
+
protocol: r.protocol,
|
|
1772
|
+
asset: "USDC",
|
|
1773
|
+
saveApy: r.rates.saveApy
|
|
1774
|
+
})) ?? [],
|
|
1775
|
+
investments: earningInvestments.map((p) => ({
|
|
1776
|
+
asset: p.asset,
|
|
1777
|
+
amount: p.totalAmount,
|
|
1778
|
+
value: p.currentValue,
|
|
1779
|
+
protocol: p.earningProtocol,
|
|
1780
|
+
apy: p.earningApy
|
|
1781
|
+
})),
|
|
1782
|
+
sentinel: agents ? {
|
|
1783
|
+
activeSentinels: agents.length,
|
|
1784
|
+
totalPrizePool: Number(totalPool.toFixed(2)),
|
|
1785
|
+
cheapestFee: Number(cheapest.toFixed(2)),
|
|
1786
|
+
bestTarget: best ? {
|
|
1787
|
+
name: best.name,
|
|
1788
|
+
objectId: best.objectId,
|
|
1789
|
+
prizePool: mistToSui(best.prizePool),
|
|
1790
|
+
attackFee: mistToSui(best.attackFee),
|
|
1791
|
+
ratio: Number((Number(best.prizePool) / Number(best.attackFee)).toFixed(1))
|
|
1792
|
+
} : null
|
|
1793
|
+
} : null
|
|
1794
|
+
});
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
printHeader("Earning Opportunities");
|
|
1798
|
+
printLine(pc11.bold("SAVINGS") + pc11.dim(" \u2014 Passive Yield"));
|
|
1799
|
+
printDivider();
|
|
1800
|
+
if (savePositions.length > 0) {
|
|
1801
|
+
for (const pos of savePositions) {
|
|
1802
|
+
const dailyYield = pos.amount * pos.apy / 100 / 365;
|
|
1803
|
+
printKeyValue(pos.protocol, `${formatUsd12(pos.amount)} ${pos.asset} @ ${pos.apy.toFixed(2)}% APY`);
|
|
1804
|
+
if (dailyYield > 1e-4) {
|
|
1805
|
+
const dailyStr = dailyYield < 0.01 ? `$${dailyYield.toFixed(4)}` : formatUsd12(dailyYield);
|
|
1806
|
+
const monthlyStr = dailyYield * 30 < 0.01 ? `$${(dailyYield * 30).toFixed(4)}` : formatUsd12(dailyYield * 30);
|
|
1807
|
+
printLine(pc11.dim(` ~${dailyStr}/day \xB7 ~${monthlyStr}/month`));
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
if (savePositions.length > 1) {
|
|
1811
|
+
printBlank();
|
|
1812
|
+
printKeyValue("Total Saved", formatUsd12(totalSaved));
|
|
1813
|
+
}
|
|
1814
|
+
} else if (ratesData && ratesData.length > 0) {
|
|
1815
|
+
const sorted = [...ratesData].sort((a, b) => b.rates.saveApy - a.rates.saveApy);
|
|
1816
|
+
for (const r of sorted) {
|
|
1817
|
+
printKeyValue(r.protocol, `USDC @ ${r.rates.saveApy.toFixed(2)}% APY`);
|
|
1818
|
+
}
|
|
1819
|
+
const example = 100;
|
|
1820
|
+
const daily = example * bestSaveApy / 100 / 365;
|
|
1821
|
+
const monthly = daily * 30;
|
|
1822
|
+
printLine(pc11.dim(` Save $${example} \u2192 ~$${daily.toFixed(2)}/day \xB7 ~$${monthly.toFixed(2)}/month`));
|
|
1823
|
+
printBlank();
|
|
1824
|
+
printInfo("No savings yet \u2014 run `t2000 save <amount>` to start");
|
|
1825
|
+
} else if (posData) {
|
|
1826
|
+
printInfo("No savings yet \u2014 run `t2000 save <amount>` to start");
|
|
1827
|
+
} else {
|
|
1828
|
+
printInfo("Savings data unavailable");
|
|
1829
|
+
}
|
|
1830
|
+
if (earningInvestments.length > 0) {
|
|
1831
|
+
printBlank();
|
|
1832
|
+
printLine(pc11.bold("INVESTMENTS") + pc11.dim(" \u2014 Earning Yield"));
|
|
1833
|
+
printDivider();
|
|
1834
|
+
let totalInvestValue = 0;
|
|
1835
|
+
for (const pos of earningInvestments) {
|
|
1836
|
+
const dailyYield = pos.currentValue * (pos.earningApy ?? 0) / 100 / 365;
|
|
1837
|
+
const apyStr = pos.earningApy ? `${pos.earningApy.toFixed(2)}%` : "\u2014";
|
|
1838
|
+
printKeyValue(
|
|
1839
|
+
`${pos.asset} via ${pos.earningProtocol ?? "unknown"}`,
|
|
1840
|
+
`${formatUsd12(pos.currentValue)} (${pos.totalAmount.toFixed(4)} ${pos.asset}) @ ${apyStr} APY`
|
|
1841
|
+
);
|
|
1842
|
+
if (dailyYield > 1e-4) {
|
|
1843
|
+
const dailyStr = dailyYield < 0.01 ? `$${dailyYield.toFixed(4)}` : formatUsd12(dailyYield);
|
|
1844
|
+
const monthlyStr = dailyYield * 30 < 0.01 ? `$${(dailyYield * 30).toFixed(4)}` : formatUsd12(dailyYield * 30);
|
|
1845
|
+
printLine(pc11.dim(` ~${dailyStr}/day \xB7 ~${monthlyStr}/month`));
|
|
1846
|
+
}
|
|
1847
|
+
totalInvestValue += pos.currentValue;
|
|
1848
|
+
}
|
|
1849
|
+
if (earningInvestments.length > 1) {
|
|
1850
|
+
printBlank();
|
|
1851
|
+
printKeyValue("Total Earning", formatUsd12(totalInvestValue));
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
printBlank();
|
|
1855
|
+
printLine(pc11.bold("SENTINEL BOUNTIES") + pc11.dim(" \u2014 Active Red Teaming"));
|
|
1856
|
+
printDivider();
|
|
1857
|
+
if (agents && agents.length > 0) {
|
|
1858
|
+
const totalPool = agents.reduce((sum, s) => sum + mistToSui(s.prizePool), 0);
|
|
1859
|
+
const cheapest = Math.min(...agents.map((s) => mistToSui(s.attackFee)));
|
|
1860
|
+
const best = bestTarget(agents);
|
|
1861
|
+
printKeyValue("Active", `${agents.length} sentinels`);
|
|
1862
|
+
printKeyValue("Prize Pools", `${totalPool.toFixed(2)} SUI available`);
|
|
1863
|
+
printKeyValue("Cheapest Fee", `${cheapest.toFixed(2)} SUI`);
|
|
1864
|
+
if (best) {
|
|
1865
|
+
const ratio = (Number(best.prizePool) / Number(best.attackFee)).toFixed(1);
|
|
1866
|
+
printKeyValue("Best Target", `${best.name} \u2014 ${mistToSui(best.prizePool).toFixed(2)} SUI pool (${ratio}x ratio)`);
|
|
1867
|
+
}
|
|
1868
|
+
} else if (agents) {
|
|
1869
|
+
printInfo("No active bounties right now");
|
|
1870
|
+
} else {
|
|
1871
|
+
printInfo("Sentinel data unavailable");
|
|
1872
|
+
}
|
|
1873
|
+
printBlank();
|
|
1874
|
+
printLine(pc11.bold("Quick Actions"));
|
|
1875
|
+
printDivider();
|
|
1876
|
+
printLine(` ${pc11.dim("t2000 save <amount> [asset]")} Save stablecoins for yield`);
|
|
1877
|
+
printLine(` ${pc11.dim("t2000 invest earn <asset>")} Earn yield on investments`);
|
|
1878
|
+
printLine(` ${pc11.dim("t2000 sentinel list")} Browse sentinel bounties`);
|
|
1879
|
+
printLine(` ${pc11.dim("t2000 sentinel attack <id>")} Attack a sentinel`);
|
|
1880
|
+
printBlank();
|
|
1881
|
+
} catch (error) {
|
|
1882
|
+
handleError(error);
|
|
1883
|
+
}
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
// src/commands/rebalance.ts
|
|
1888
|
+
import pc12 from "picocolors";
|
|
1889
|
+
import { T2000 as T200022, formatUsd as formatUsd13, SUPPORTED_ASSETS as SUPPORTED_ASSETS2 } from "@t2000/sdk";
|
|
1890
|
+
function registerRebalance(program2) {
|
|
1891
|
+
program2.command("rebalance").description("Optimize yield \u2014 move savings to the best rate across protocols and stablecoins").option("--key <path>", "Key file path").option("--dry-run", "Show what would happen without executing").option("--min-diff <pct>", "Minimum APY difference to trigger (default: 0.5)", "0.5").option("--max-break-even <days>", "Max break-even days for cross-asset moves (default: 30)", "30").action(async (opts) => {
|
|
1892
|
+
try {
|
|
1893
|
+
const pin = await resolvePin();
|
|
1894
|
+
const agent = await T200022.create({ pin, keyPath: opts.key });
|
|
1895
|
+
const minYieldDiff = parseFloat(opts.minDiff ?? "0.5");
|
|
1896
|
+
const maxBreakEven = parseInt(opts.maxBreakEven ?? "30", 10);
|
|
1897
|
+
const plan = await agent.rebalance({
|
|
1898
|
+
dryRun: true,
|
|
1899
|
+
minYieldDiff,
|
|
1900
|
+
maxBreakEven
|
|
1901
|
+
});
|
|
1902
|
+
if (isJsonMode()) {
|
|
1903
|
+
if (opts.dryRun) {
|
|
1904
|
+
printJson(plan);
|
|
1905
|
+
} else {
|
|
1906
|
+
const result2 = await agent.rebalance({ dryRun: false, minYieldDiff, maxBreakEven });
|
|
1907
|
+
printJson(result2);
|
|
1908
|
+
}
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1911
|
+
printBlank();
|
|
1912
|
+
if (plan.steps.length === 0) {
|
|
1913
|
+
const diff = plan.newApy - plan.currentApy;
|
|
1914
|
+
if (diff < minYieldDiff) {
|
|
1915
|
+
printInfo(`Already optimized \u2014 ${plan.currentApy.toFixed(2)}% APY on ${plan.fromProtocol}`);
|
|
1916
|
+
printLine(pc12.dim(` Best available: ${plan.newApy.toFixed(2)}% (${displayAsset(plan.toAsset)} on ${plan.toProtocol})`));
|
|
1917
|
+
printLine(pc12.dim(` Difference: ${diff.toFixed(2)}% (below ${minYieldDiff}% threshold)`));
|
|
1918
|
+
} else if (plan.breakEvenDays > maxBreakEven && plan.estimatedSwapCost > 0) {
|
|
1919
|
+
printInfo(`Skipped \u2014 break-even of ${plan.breakEvenDays} days exceeds ${maxBreakEven}-day limit`);
|
|
1920
|
+
printLine(pc12.dim(` ${displayAsset(plan.fromAsset)} on ${plan.fromProtocol} (${plan.currentApy.toFixed(2)}%) \u2192 ${displayAsset(plan.toAsset)} on ${plan.toProtocol} (${plan.newApy.toFixed(2)}%)`));
|
|
1921
|
+
} else {
|
|
1922
|
+
printInfo("Already at the best rate. Nothing to rebalance.");
|
|
1923
|
+
}
|
|
1924
|
+
printBlank();
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
printLine(pc12.bold("Rebalance Plan"));
|
|
1928
|
+
printDivider();
|
|
1929
|
+
printKeyValue("From", `${displayAsset(plan.fromAsset)} on ${plan.fromProtocol} (${plan.currentApy.toFixed(2)}% APY)`);
|
|
1930
|
+
printKeyValue("To", `${displayAsset(plan.toAsset)} on ${plan.toProtocol} (${plan.newApy.toFixed(2)}% APY)`);
|
|
1931
|
+
printKeyValue("Amount", formatUsd13(plan.amount));
|
|
1932
|
+
printBlank();
|
|
1933
|
+
printLine(pc12.bold("Economics"));
|
|
1934
|
+
printDivider();
|
|
1935
|
+
printKeyValue("APY Gain", `+${(plan.newApy - plan.currentApy).toFixed(2)}%`);
|
|
1936
|
+
printKeyValue("Annual Gain", `${formatUsd13(plan.annualGain)}/year`);
|
|
1937
|
+
if (plan.estimatedSwapCost > 0) {
|
|
1938
|
+
printKeyValue("Swap Cost", `~${formatUsd13(plan.estimatedSwapCost)}`);
|
|
1939
|
+
printKeyValue("Break-even", `${plan.breakEvenDays} days`);
|
|
1940
|
+
}
|
|
1941
|
+
printBlank();
|
|
1942
|
+
if (plan.steps.length > 0) {
|
|
1943
|
+
printLine(pc12.bold("Steps"));
|
|
1944
|
+
printDivider();
|
|
1945
|
+
for (let i = 0; i < plan.steps.length; i++) {
|
|
1946
|
+
const step = plan.steps[i];
|
|
1947
|
+
const num = `${i + 1}.`;
|
|
1948
|
+
if (step.action === "withdraw") {
|
|
1949
|
+
printLine(` ${num} Withdraw ${formatUsd13(step.amount)} ${displayAsset(step.fromAsset ?? "")} from ${step.protocol}`);
|
|
1950
|
+
} else if (step.action === "swap") {
|
|
1951
|
+
printLine(` ${num} Swap ${displayAsset(step.fromAsset ?? "")} \u2192 ${displayAsset(step.toAsset ?? "")} (~${formatUsd13(step.estimatedOutput ?? 0)})`);
|
|
1952
|
+
} else if (step.action === "deposit") {
|
|
1953
|
+
printLine(` ${num} Deposit ${formatUsd13(step.amount)} ${displayAsset(step.toAsset ?? "")} into ${step.protocol}`);
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
printBlank();
|
|
1957
|
+
}
|
|
1958
|
+
if (opts.dryRun) {
|
|
1959
|
+
printLine(pc12.bold(pc12.yellow("DRY RUN \u2014 Preview only, no transactions executed")));
|
|
1960
|
+
printLine(pc12.dim(" Run `t2000 rebalance` to execute."));
|
|
1961
|
+
printBlank();
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
const result = await agent.rebalance({ dryRun: false, minYieldDiff, maxBreakEven });
|
|
1965
|
+
if (result.executed) {
|
|
1966
|
+
printSuccess(`Rebalanced ${formatUsd13(result.amount)} \u2192 ${result.newApy.toFixed(2)}% APY`);
|
|
1967
|
+
for (const digest of result.txDigests) {
|
|
1968
|
+
printKeyValue("Tx", explorerUrl(digest));
|
|
1969
|
+
}
|
|
1970
|
+
printKeyValue("Gas", `${result.totalGasCost.toFixed(4)} SUI`);
|
|
1971
|
+
}
|
|
1972
|
+
printBlank();
|
|
1973
|
+
} catch (error) {
|
|
1974
|
+
handleError(error);
|
|
1975
|
+
}
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
function displayAsset(asset) {
|
|
1979
|
+
return SUPPORTED_ASSETS2[asset]?.displayName ?? asset;
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
// src/commands/exchange.ts
|
|
1983
|
+
import { T2000 as T200023, formatUsd as formatUsd14, SUPPORTED_ASSETS as SUPPORTED_ASSETS3 } from "@t2000/sdk";
|
|
1984
|
+
function registerExchange(program2) {
|
|
1985
|
+
program2.command("exchange <amount> <from> <to>").description("Exchange between tokens (e.g. USDC \u21CC SUI) via Cetus DEX").option("--key <path>", "Key file path").option("--slippage <pct>", "Max slippage percentage (default: 3)", "3").action(async (amount, from, to, opts) => {
|
|
1986
|
+
try {
|
|
1987
|
+
const pin = await resolvePin();
|
|
1988
|
+
const agent = await T200023.create({ pin, keyPath: opts.key });
|
|
1989
|
+
const fromAsset = from.toUpperCase();
|
|
1990
|
+
const toAsset = to.toUpperCase();
|
|
1991
|
+
const parsedAmount = parseFloat(amount);
|
|
1992
|
+
if (isNaN(parsedAmount) || parsedAmount <= 0) {
|
|
1993
|
+
throw new Error("Amount must be a positive number");
|
|
1994
|
+
}
|
|
1995
|
+
const result = await agent.exchange({
|
|
1996
|
+
from: fromAsset,
|
|
1997
|
+
to: toAsset,
|
|
1998
|
+
amount: parsedAmount,
|
|
1999
|
+
maxSlippage: parseFloat(opts.slippage ?? "3")
|
|
2000
|
+
});
|
|
2001
|
+
if (isJsonMode()) {
|
|
2002
|
+
printJson(result);
|
|
2003
|
+
return;
|
|
2004
|
+
}
|
|
2005
|
+
const fromDisplay = SUPPORTED_ASSETS3[fromAsset]?.displayName ?? fromAsset;
|
|
2006
|
+
const toDisplay = SUPPORTED_ASSETS3[toAsset]?.displayName ?? toAsset;
|
|
2007
|
+
const toDecimals = toAsset === "SUI" ? 4 : 2;
|
|
2008
|
+
printBlank();
|
|
2009
|
+
const fromStr = ["USDC", "USDT", "USDE"].includes(fromAsset) ? formatUsd14(parsedAmount) : parsedAmount.toFixed(4);
|
|
2010
|
+
printSuccess(`Exchanged ${fromStr} ${fromDisplay} \u2192 ${result.toAmount.toFixed(toDecimals)} ${toDisplay}`);
|
|
2011
|
+
printKeyValue("Tx", explorerUrl(result.tx));
|
|
2012
|
+
printKeyValue("Gas", `${result.gasCost.toFixed(4)} SUI (${result.gasMethod})`);
|
|
2013
|
+
printBlank();
|
|
2014
|
+
} catch (error) {
|
|
2015
|
+
handleError(error);
|
|
2016
|
+
}
|
|
2017
|
+
});
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
// src/commands/mcp.ts
|
|
2021
|
+
import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
|
|
2022
|
+
import { join as join5, dirname } from "path";
|
|
2023
|
+
import { homedir as homedir7 } from "os";
|
|
2024
|
+
import { existsSync as existsSync4 } from "fs";
|
|
2025
|
+
var MCP_CONFIG = {
|
|
2026
|
+
command: "t2000",
|
|
2027
|
+
args: ["mcp"]
|
|
2028
|
+
};
|
|
2029
|
+
function getPlatformConfigs() {
|
|
2030
|
+
const home = homedir7();
|
|
2031
|
+
return [
|
|
2032
|
+
{
|
|
2033
|
+
name: "Claude Desktop",
|
|
2034
|
+
path: join5(home, "Library", "Application Support", "Claude", "claude_desktop_config.json")
|
|
2035
|
+
},
|
|
2036
|
+
{
|
|
2037
|
+
name: "Cursor (global)",
|
|
2038
|
+
path: join5(home, ".cursor", "mcp.json")
|
|
2039
|
+
}
|
|
2040
|
+
];
|
|
2041
|
+
}
|
|
2042
|
+
async function readJsonFile(path) {
|
|
2043
|
+
try {
|
|
2044
|
+
const content = await readFile3(path, "utf-8");
|
|
2045
|
+
return JSON.parse(content);
|
|
2046
|
+
} catch {
|
|
2047
|
+
return {};
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
async function writeJsonFile(path, data) {
|
|
2051
|
+
const dir = dirname(path);
|
|
2052
|
+
if (!existsSync4(dir)) {
|
|
2053
|
+
await mkdir3(dir, { recursive: true });
|
|
2054
|
+
}
|
|
2055
|
+
await writeFile3(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
2056
|
+
}
|
|
2057
|
+
function registerMcp(program2) {
|
|
2058
|
+
const mcp = program2.command("mcp").description("MCP server for AI platforms");
|
|
2059
|
+
mcp.command("start", { isDefault: true }).description("Start MCP server (stdio transport)").option("--key <path>", "Key file path").action(async (opts) => {
|
|
2060
|
+
let mod;
|
|
2061
|
+
try {
|
|
2062
|
+
mod = await import("./dist-6ADOH23K.js");
|
|
2063
|
+
} catch {
|
|
2064
|
+
console.error(
|
|
2065
|
+
"MCP server not installed. Run:\n npm install -g @t2000/mcp"
|
|
2066
|
+
);
|
|
2067
|
+
process.exit(1);
|
|
2068
|
+
}
|
|
2069
|
+
await mod.startMcpServer({ keyPath: opts.key });
|
|
2070
|
+
});
|
|
2071
|
+
mcp.command("install").description("Auto-configure MCP in Claude Desktop and Cursor").action(async () => {
|
|
2072
|
+
try {
|
|
2073
|
+
const platforms = getPlatformConfigs();
|
|
2074
|
+
const results = [];
|
|
2075
|
+
for (const platform3 of platforms) {
|
|
2076
|
+
const config = await readJsonFile(platform3.path);
|
|
2077
|
+
if (config.mcpServers && config.mcpServers["t2000"]) {
|
|
2078
|
+
results.push({ name: platform3.name, status: "exists" });
|
|
2079
|
+
continue;
|
|
2080
|
+
}
|
|
2081
|
+
config.mcpServers = {
|
|
2082
|
+
...config.mcpServers ?? {},
|
|
2083
|
+
t2000: MCP_CONFIG
|
|
2084
|
+
};
|
|
2085
|
+
await writeJsonFile(platform3.path, config);
|
|
2086
|
+
results.push({ name: platform3.name, status: "added" });
|
|
2087
|
+
}
|
|
2088
|
+
if (isJsonMode()) {
|
|
2089
|
+
printJson({ installed: results });
|
|
2090
|
+
return;
|
|
2091
|
+
}
|
|
2092
|
+
printBlank();
|
|
2093
|
+
for (const r of results) {
|
|
2094
|
+
if (r.status === "exists") {
|
|
2095
|
+
printInfo(`${r.name} already configured`);
|
|
2096
|
+
} else {
|
|
2097
|
+
printSuccess(`${r.name} configured`);
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
printBlank();
|
|
2101
|
+
printInfo("Restart your AI platform to activate.");
|
|
2102
|
+
printInfo(`Then ask: "what's my t2000 balance?"`);
|
|
2103
|
+
printBlank();
|
|
2104
|
+
} catch (error) {
|
|
2105
|
+
handleError(error);
|
|
2106
|
+
}
|
|
2107
|
+
});
|
|
2108
|
+
mcp.command("uninstall").description("Remove t2000 MCP config from Claude Desktop and Cursor").action(async () => {
|
|
2109
|
+
try {
|
|
2110
|
+
const platforms = getPlatformConfigs();
|
|
2111
|
+
const results = [];
|
|
2112
|
+
for (const platform3 of platforms) {
|
|
2113
|
+
if (!existsSync4(platform3.path)) {
|
|
2114
|
+
results.push({ name: platform3.name, removed: false });
|
|
2115
|
+
continue;
|
|
2116
|
+
}
|
|
2117
|
+
const config = await readJsonFile(platform3.path);
|
|
2118
|
+
if (!config.mcpServers || !config.mcpServers["t2000"]) {
|
|
2119
|
+
results.push({ name: platform3.name, removed: false });
|
|
2120
|
+
continue;
|
|
2121
|
+
}
|
|
2122
|
+
delete config.mcpServers["t2000"];
|
|
2123
|
+
await writeJsonFile(platform3.path, config);
|
|
2124
|
+
results.push({ name: platform3.name, removed: true });
|
|
2125
|
+
}
|
|
2126
|
+
if (isJsonMode()) {
|
|
2127
|
+
printJson({ uninstalled: results });
|
|
2128
|
+
return;
|
|
2129
|
+
}
|
|
2130
|
+
printBlank();
|
|
2131
|
+
for (const r of results) {
|
|
2132
|
+
if (r.removed) {
|
|
2133
|
+
printSuccess(`${r.name} removed`);
|
|
2134
|
+
} else {
|
|
2135
|
+
printInfo(`${r.name} not configured (skipped)`);
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
printBlank();
|
|
2139
|
+
} catch (error) {
|
|
2140
|
+
handleError(error);
|
|
2141
|
+
}
|
|
2142
|
+
});
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
// src/commands/contacts.ts
|
|
2146
|
+
import { ContactManager, truncateAddress as truncateAddress3 } from "@t2000/sdk";
|
|
2147
|
+
function registerContacts(program2) {
|
|
2148
|
+
const contacts = program2.command("contacts").description("Manage contacts (send by name instead of address)");
|
|
2149
|
+
contacts.command("add <name> <address>").description("Add or update a contact").action((name, address) => {
|
|
2150
|
+
try {
|
|
2151
|
+
const manager = new ContactManager();
|
|
2152
|
+
const result = manager.add(name, address);
|
|
2153
|
+
if (isJsonMode()) {
|
|
2154
|
+
const contact2 = manager.get(name);
|
|
2155
|
+
printJson({ action: result.action, name: contact2.name, address: contact2.address });
|
|
2156
|
+
return;
|
|
2157
|
+
}
|
|
2158
|
+
const contact = manager.get(name);
|
|
2159
|
+
printBlank();
|
|
2160
|
+
printSuccess(`${result.action === "added" ? "Added" : "Updated"} ${contact.name} (${truncateAddress3(contact.address)})`);
|
|
2161
|
+
printBlank();
|
|
2162
|
+
} catch (error) {
|
|
2163
|
+
handleError(error);
|
|
2164
|
+
}
|
|
2165
|
+
});
|
|
2166
|
+
contacts.command("remove <name>").description("Remove a contact").action((name) => {
|
|
2167
|
+
try {
|
|
2168
|
+
const manager = new ContactManager();
|
|
2169
|
+
const removed = manager.remove(name);
|
|
2170
|
+
if (isJsonMode()) {
|
|
2171
|
+
printJson({ removed, name });
|
|
2172
|
+
return;
|
|
2173
|
+
}
|
|
2174
|
+
printBlank();
|
|
2175
|
+
if (removed) {
|
|
2176
|
+
printSuccess(`Removed ${name}`);
|
|
2177
|
+
} else {
|
|
2178
|
+
printError(`Contact "${name}" not found`);
|
|
2179
|
+
}
|
|
2180
|
+
printBlank();
|
|
2181
|
+
} catch (error) {
|
|
2182
|
+
handleError(error);
|
|
2183
|
+
}
|
|
2184
|
+
});
|
|
2185
|
+
contacts.command("list", { isDefault: true }).description("List all contacts").action(() => {
|
|
2186
|
+
try {
|
|
2187
|
+
const manager = new ContactManager();
|
|
2188
|
+
const list = manager.list();
|
|
2189
|
+
if (isJsonMode()) {
|
|
2190
|
+
printJson(list);
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
if (list.length === 0) {
|
|
2194
|
+
printBlank();
|
|
2195
|
+
printLine("No contacts yet.");
|
|
2196
|
+
printLine("Add one: t2000 contacts add Tom 0x...");
|
|
2197
|
+
printBlank();
|
|
2198
|
+
return;
|
|
2199
|
+
}
|
|
2200
|
+
printHeader("Contacts");
|
|
2201
|
+
const maxNameLen = Math.max(...list.map((c) => c.name.length));
|
|
2202
|
+
for (const contact of list) {
|
|
2203
|
+
const padded = contact.name.padEnd(maxNameLen + 4);
|
|
2204
|
+
printLine(`${padded}${truncateAddress3(contact.address)}`);
|
|
2205
|
+
}
|
|
2206
|
+
printBlank();
|
|
2207
|
+
} catch (error) {
|
|
2208
|
+
handleError(error);
|
|
2209
|
+
}
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
// src/commands/invest.ts
|
|
2214
|
+
import pc13 from "picocolors";
|
|
2215
|
+
import { T2000 as T200024, formatUsd as formatUsd15, formatAssetAmount, INVESTMENT_ASSETS as INVESTMENT_ASSETS2 } from "@t2000/sdk";
|
|
2216
|
+
function registerInvest(program2) {
|
|
2217
|
+
const investCmd = program2.command("invest").description("Buy or sell investment assets");
|
|
2218
|
+
investCmd.command("buy <amount> <asset>").description("Invest USD amount in an asset").option("--key <path>", "Key file path").option("--slippage <pct>", "Max slippage percent", "3").action(async (amount, asset, opts) => {
|
|
2219
|
+
try {
|
|
2220
|
+
const parsed = parseFloat(amount);
|
|
2221
|
+
if (isNaN(parsed) || parsed <= 0 || !isFinite(parsed)) {
|
|
2222
|
+
console.error(pc13.red(" \u2717 Amount must be greater than $0"));
|
|
2223
|
+
process.exitCode = 1;
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
const pin = await resolvePin();
|
|
2227
|
+
const agent = await T200024.create({ pin, keyPath: opts.key });
|
|
2228
|
+
const result = await agent.investBuy({
|
|
2229
|
+
asset: asset.toUpperCase(),
|
|
2230
|
+
usdAmount: parsed,
|
|
2231
|
+
maxSlippage: parseFloat(opts.slippage) / 100
|
|
2232
|
+
});
|
|
2233
|
+
if (isJsonMode()) {
|
|
2234
|
+
printJson(result);
|
|
2235
|
+
return;
|
|
2236
|
+
}
|
|
2237
|
+
printBlank();
|
|
2238
|
+
const sym = asset.toUpperCase();
|
|
2239
|
+
printSuccess(`Bought ${formatAssetAmount(result.amount, sym)} ${sym} at ${formatUsd15(result.price)}`);
|
|
2240
|
+
printKeyValue("Invested", formatUsd15(result.usdValue));
|
|
2241
|
+
printKeyValue("Portfolio", `${formatAssetAmount(result.position.totalAmount, sym)} ${sym} (avg ${formatUsd15(result.position.avgPrice)})`);
|
|
2242
|
+
printKeyValue("Tx", explorerUrl(result.tx));
|
|
2243
|
+
printBlank();
|
|
2244
|
+
} catch (error) {
|
|
2245
|
+
handleError(error);
|
|
2246
|
+
}
|
|
2247
|
+
});
|
|
2248
|
+
investCmd.command("sell <amount> <asset>").description('Sell USD amount of an asset (or "all")').option("--key <path>", "Key file path").option("--slippage <pct>", "Max slippage percent", "3").action(async (amount, asset, opts) => {
|
|
2249
|
+
try {
|
|
2250
|
+
const isAll = amount.toLowerCase() === "all";
|
|
2251
|
+
if (!isAll) {
|
|
2252
|
+
const parsed = parseFloat(amount);
|
|
2253
|
+
if (isNaN(parsed) || parsed <= 0 || !isFinite(parsed)) {
|
|
2254
|
+
console.error(pc13.red(" \u2717 Amount must be greater than $0"));
|
|
2255
|
+
process.exitCode = 1;
|
|
2256
|
+
return;
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
const pin = await resolvePin();
|
|
2260
|
+
const agent = await T200024.create({ pin, keyPath: opts.key });
|
|
2261
|
+
const usdAmount = isAll ? "all" : parseFloat(amount);
|
|
2262
|
+
const result = await agent.investSell({
|
|
2263
|
+
asset: asset.toUpperCase(),
|
|
2264
|
+
usdAmount,
|
|
2265
|
+
maxSlippage: parseFloat(opts.slippage) / 100
|
|
2266
|
+
});
|
|
2267
|
+
if (isJsonMode()) {
|
|
2268
|
+
printJson(result);
|
|
2269
|
+
return;
|
|
2270
|
+
}
|
|
2271
|
+
printBlank();
|
|
2272
|
+
const sym = asset.toUpperCase();
|
|
2273
|
+
printSuccess(`Sold ${formatAssetAmount(result.amount, sym)} ${sym} at ${formatUsd15(result.price)}`);
|
|
2274
|
+
printKeyValue("Proceeds", formatUsd15(result.usdValue));
|
|
2275
|
+
if (result.realizedPnL !== void 0) {
|
|
2276
|
+
const pnlColor = result.realizedPnL >= 0 ? pc13.green : pc13.red;
|
|
2277
|
+
const pnlSign = result.realizedPnL >= 0 ? "+" : "";
|
|
2278
|
+
printKeyValue("Realized P&L", pnlColor(`${pnlSign}${formatUsd15(result.realizedPnL)}`));
|
|
2279
|
+
}
|
|
2280
|
+
if (result.position.totalAmount > 0) {
|
|
2281
|
+
printKeyValue("Remaining", `${formatAssetAmount(result.position.totalAmount, sym)} ${sym} (avg ${formatUsd15(result.position.avgPrice)})`);
|
|
2282
|
+
}
|
|
2283
|
+
printKeyValue("Tx", explorerUrl(result.tx));
|
|
2284
|
+
printBlank();
|
|
2285
|
+
} catch (error) {
|
|
2286
|
+
handleError(error);
|
|
2287
|
+
}
|
|
2288
|
+
});
|
|
2289
|
+
investCmd.command("earn <asset>").description("Deposit invested asset into best-rate lending protocol").option("--key <path>", "Key file path").option("--protocol <name>", "Force a specific protocol (navi, suilend)").action(async (asset, opts) => {
|
|
2290
|
+
try {
|
|
2291
|
+
const pin = await resolvePin();
|
|
2292
|
+
const agent = await T200024.create({ pin, keyPath: opts.key });
|
|
2293
|
+
const result = await agent.investEarn({
|
|
2294
|
+
asset: asset.toUpperCase(),
|
|
2295
|
+
protocol: opts.protocol?.toLowerCase()
|
|
2296
|
+
});
|
|
2297
|
+
if (isJsonMode()) {
|
|
2298
|
+
printJson(result);
|
|
2299
|
+
return;
|
|
2300
|
+
}
|
|
2301
|
+
printBlank();
|
|
2302
|
+
const sym = asset.toUpperCase();
|
|
2303
|
+
if (result.amount === 0 && !result.tx) {
|
|
2304
|
+
printSuccess(`${sym} is already fully earning via ${result.protocol} (${result.apy.toFixed(1)}% APY)`);
|
|
2305
|
+
} else {
|
|
2306
|
+
printSuccess(`${sym} deposited into ${result.protocol} (${result.apy.toFixed(1)}% APY)`);
|
|
2307
|
+
printKeyValue("Amount", `${formatAssetAmount(result.amount, sym)} ${sym}`);
|
|
2308
|
+
printKeyValue("Protocol", result.protocol);
|
|
2309
|
+
printKeyValue("APY", `${result.apy.toFixed(2)}%`);
|
|
2310
|
+
printKeyValue("Tx", explorerUrl(result.tx));
|
|
2311
|
+
}
|
|
2312
|
+
printBlank();
|
|
2313
|
+
} catch (error) {
|
|
2314
|
+
handleError(error);
|
|
2315
|
+
}
|
|
2316
|
+
});
|
|
2317
|
+
investCmd.command("unearn <asset>").description("Withdraw invested asset from lending (keeps in portfolio)").option("--key <path>", "Key file path").action(async (asset, opts) => {
|
|
2318
|
+
try {
|
|
2319
|
+
const pin = await resolvePin();
|
|
2320
|
+
const agent = await T200024.create({ pin, keyPath: opts.key });
|
|
2321
|
+
const result = await agent.investUnearn({
|
|
2322
|
+
asset: asset.toUpperCase()
|
|
2323
|
+
});
|
|
2324
|
+
if (isJsonMode()) {
|
|
2325
|
+
printJson(result);
|
|
2326
|
+
return;
|
|
2327
|
+
}
|
|
2328
|
+
printBlank();
|
|
2329
|
+
const sym = asset.toUpperCase();
|
|
2330
|
+
printSuccess(`Withdrew ${formatAssetAmount(result.amount, sym)} ${sym} from ${result.protocol}`);
|
|
2331
|
+
printKeyValue("Status", `${sym} remains in investment portfolio (locked)`);
|
|
2332
|
+
printKeyValue("Tx", explorerUrl(result.tx));
|
|
2333
|
+
printBlank();
|
|
2334
|
+
} catch (error) {
|
|
2335
|
+
handleError(error);
|
|
2336
|
+
}
|
|
2337
|
+
});
|
|
2338
|
+
investCmd.command("rebalance").description("Move earning positions to better-rate protocols").option("--key <path>", "Key file path").option("--dry-run", "Preview moves without executing").option("--min-diff <pct>", "Minimum APY difference to trigger move", "0.1").action(async (opts) => {
|
|
2339
|
+
try {
|
|
2340
|
+
const pin = await resolvePin();
|
|
2341
|
+
const agent = await T200024.create({ pin, keyPath: opts.key });
|
|
2342
|
+
const result = await agent.investRebalance({
|
|
2343
|
+
dryRun: opts.dryRun,
|
|
2344
|
+
minYieldDiff: opts.minDiff ? parseFloat(opts.minDiff) : void 0
|
|
2345
|
+
});
|
|
2346
|
+
if (isJsonMode()) {
|
|
2347
|
+
printJson(result);
|
|
2348
|
+
return;
|
|
2349
|
+
}
|
|
2350
|
+
printBlank();
|
|
2351
|
+
if (result.moves.length === 0) {
|
|
2352
|
+
printInfo("All earning positions are already on the best rate");
|
|
2353
|
+
if (result.skipped.length > 0) {
|
|
2354
|
+
for (const s of result.skipped) {
|
|
2355
|
+
printLine(` ${s.asset}: ${s.apy.toFixed(2)}% on ${s.protocol} (best: ${s.bestApy.toFixed(2)}% \u2014 ${s.reason.replace(/_/g, " ")})`);
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
printBlank();
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2361
|
+
if (opts.dryRun) {
|
|
2362
|
+
printHeader("Rebalance Preview");
|
|
2363
|
+
for (const m of result.moves) {
|
|
2364
|
+
printLine(` ${m.asset}: ${m.fromProtocol} (${m.oldApy.toFixed(2)}%) \u2192 ${m.toProtocol} (${m.newApy.toFixed(2)}%)`);
|
|
2365
|
+
printLine(` Gain: +${(m.newApy - m.oldApy).toFixed(2)}% APY`);
|
|
2366
|
+
}
|
|
2367
|
+
} else {
|
|
2368
|
+
printSuccess("Rebalanced earning positions");
|
|
2369
|
+
printSeparator();
|
|
2370
|
+
for (const m of result.moves) {
|
|
2371
|
+
printLine(` ${m.asset}: ${m.fromProtocol} (${m.oldApy.toFixed(2)}%) \u2192 ${m.toProtocol} (${m.newApy.toFixed(2)}%)`);
|
|
2372
|
+
printKeyValue("Amount", `${formatAssetAmount(m.amount, m.asset)} ${m.asset}`);
|
|
2373
|
+
printKeyValue("APY gain", `+${(m.newApy - m.oldApy).toFixed(2)}%`);
|
|
2374
|
+
if (m.txDigests.length > 0) {
|
|
2375
|
+
printKeyValue("Tx", explorerUrl(m.txDigests[m.txDigests.length - 1]));
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
printSeparator();
|
|
2379
|
+
printKeyValue("Gas", `${result.totalGasCost.toFixed(6)} SUI`);
|
|
2380
|
+
}
|
|
2381
|
+
if (result.skipped.length > 0) {
|
|
2382
|
+
printBlank();
|
|
2383
|
+
for (const s of result.skipped) {
|
|
2384
|
+
printLine(` ${s.asset}: kept on ${s.protocol} (${s.reason.replace(/_/g, " ")})`);
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
printBlank();
|
|
2388
|
+
} catch (error) {
|
|
2389
|
+
handleError(error);
|
|
2390
|
+
}
|
|
2391
|
+
});
|
|
2392
|
+
const strategyCmd = investCmd.command("strategy").description("Manage investment strategies");
|
|
2393
|
+
strategyCmd.command("list").description("List available strategies").option("--key <path>", "Key file path").action(async (opts) => {
|
|
2394
|
+
try {
|
|
2395
|
+
const pin = await resolvePin();
|
|
2396
|
+
const agent = await T200024.create({ pin, keyPath: opts.key });
|
|
2397
|
+
const all = agent.strategies.getAll();
|
|
2398
|
+
if (isJsonMode()) {
|
|
2399
|
+
printJson(all);
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
printBlank();
|
|
2403
|
+
printHeader("Investment Strategies");
|
|
2404
|
+
printSeparator();
|
|
2405
|
+
for (const [key, def] of Object.entries(all)) {
|
|
2406
|
+
const allocs = Object.entries(def.allocations).map(([a, p]) => `${a} ${p}%`).join(", ");
|
|
2407
|
+
const tag = def.custom ? pc13.dim(" (custom)") : "";
|
|
2408
|
+
printKeyValue(key, `${allocs}${tag}`);
|
|
2409
|
+
printLine(` ${pc13.dim(def.description)}`);
|
|
2410
|
+
}
|
|
2411
|
+
printSeparator();
|
|
2412
|
+
const hasPositions = Object.keys(all).some((k) => agent.portfolio.hasStrategyPositions(k));
|
|
2413
|
+
if (!hasPositions) {
|
|
2414
|
+
printInfo("Buy into a strategy: t2000 invest strategy buy bluechip 100");
|
|
2415
|
+
}
|
|
2416
|
+
printBlank();
|
|
2417
|
+
} catch (error) {
|
|
2418
|
+
handleError(error);
|
|
2419
|
+
}
|
|
2420
|
+
});
|
|
2421
|
+
strategyCmd.command("buy <name> <amount>").description("Buy into a strategy").option("--key <path>", "Key file path").option("--dry-run", "Preview allocation without executing").action(async (name, amount, opts) => {
|
|
2422
|
+
try {
|
|
2423
|
+
const parsed = parseFloat(amount);
|
|
2424
|
+
if (isNaN(parsed) || parsed <= 0) {
|
|
2425
|
+
console.error(pc13.red(" \u2717 Amount must be greater than $0"));
|
|
2426
|
+
process.exitCode = 1;
|
|
2427
|
+
return;
|
|
2428
|
+
}
|
|
2429
|
+
const pin = await resolvePin();
|
|
2430
|
+
const agent = await T200024.create({ pin, keyPath: opts.key });
|
|
2431
|
+
const result = await agent.investStrategy({ strategy: name.toLowerCase(), usdAmount: parsed, dryRun: opts.dryRun });
|
|
2432
|
+
if (isJsonMode()) {
|
|
2433
|
+
printJson(result);
|
|
2434
|
+
return;
|
|
2435
|
+
}
|
|
2436
|
+
printBlank();
|
|
2437
|
+
if (opts.dryRun) {
|
|
2438
|
+
printHeader(`Strategy: ${name} \u2014 Dry Run (${formatUsd15(parsed)})`);
|
|
2439
|
+
printSeparator();
|
|
2440
|
+
for (const buy of result.buys) {
|
|
2441
|
+
printKeyValue(buy.asset, `${formatUsd15(buy.usdAmount)} \u2192 ~${formatAssetAmount(buy.amount, buy.asset)} ${buy.asset} @ ${formatUsd15(buy.price)}`);
|
|
2442
|
+
}
|
|
2443
|
+
printSeparator();
|
|
2444
|
+
printInfo("Run without --dry-run to execute");
|
|
2445
|
+
} else {
|
|
2446
|
+
const txDigests = [...new Set(result.buys.map((b) => b.tx))];
|
|
2447
|
+
const isSingleTx = txDigests.length === 1;
|
|
2448
|
+
printSuccess(`Invested ${formatUsd15(parsed)} in ${name} strategy`);
|
|
2449
|
+
printSeparator();
|
|
2450
|
+
for (const buy of result.buys) {
|
|
2451
|
+
printKeyValue(buy.asset, `${formatAssetAmount(buy.amount, buy.asset)} @ ${formatUsd15(buy.price)}`);
|
|
2452
|
+
}
|
|
2453
|
+
printSeparator();
|
|
2454
|
+
printKeyValue("Total invested", formatUsd15(result.totalInvested));
|
|
2455
|
+
if (isSingleTx) {
|
|
2456
|
+
printKeyValue("Tx", explorerUrl(txDigests[0]));
|
|
2457
|
+
} else {
|
|
2458
|
+
for (const buy of result.buys) {
|
|
2459
|
+
printLine(` ${pc13.dim(`${buy.asset}: ${explorerUrl(buy.tx)}`)}`);
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
printBlank();
|
|
2464
|
+
} catch (error) {
|
|
2465
|
+
handleError(error);
|
|
2466
|
+
}
|
|
2467
|
+
});
|
|
2468
|
+
strategyCmd.command("sell <name>").description("Sell all positions in a strategy").option("--key <path>", "Key file path").action(async (name, opts) => {
|
|
2469
|
+
try {
|
|
2470
|
+
const pin = await resolvePin();
|
|
2471
|
+
const agent = await T200024.create({ pin, keyPath: opts.key });
|
|
2472
|
+
const result = await agent.sellStrategy({ strategy: name.toLowerCase() });
|
|
2473
|
+
if (isJsonMode()) {
|
|
2474
|
+
printJson(result);
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
printBlank();
|
|
2478
|
+
printSuccess(`Sold all ${name} strategy positions`);
|
|
2479
|
+
printSeparator();
|
|
2480
|
+
for (const sell of result.sells) {
|
|
2481
|
+
const pnlColor = sell.realizedPnL >= 0 ? pc13.green : pc13.red;
|
|
2482
|
+
const pnlSign = sell.realizedPnL >= 0 ? "+" : "";
|
|
2483
|
+
printKeyValue(sell.asset, `${formatAssetAmount(sell.amount, sell.asset)} \u2192 ${formatUsd15(sell.usdValue)} ${pnlColor(`${pnlSign}${formatUsd15(sell.realizedPnL)}`)}`);
|
|
2484
|
+
}
|
|
2485
|
+
if (result.failed && result.failed.length > 0) {
|
|
2486
|
+
for (const f of result.failed) {
|
|
2487
|
+
console.error(pc13.yellow(` \u26A0 ${f.asset}: ${f.reason}`));
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
printSeparator();
|
|
2491
|
+
printKeyValue("Total proceeds", formatUsd15(result.totalProceeds));
|
|
2492
|
+
const rpnlColor = result.realizedPnL >= 0 ? pc13.green : pc13.red;
|
|
2493
|
+
const rpnlSign = result.realizedPnL >= 0 ? "+" : "";
|
|
2494
|
+
printKeyValue("Realized P&L", rpnlColor(`${rpnlSign}${formatUsd15(result.realizedPnL)}`));
|
|
2495
|
+
printBlank();
|
|
2496
|
+
} catch (error) {
|
|
2497
|
+
handleError(error);
|
|
2498
|
+
}
|
|
2499
|
+
});
|
|
2500
|
+
strategyCmd.command("status <name>").description("Show current status and weights of a strategy").option("--key <path>", "Key file path").action(async (name, opts) => {
|
|
2501
|
+
try {
|
|
2502
|
+
const pin = await resolvePin();
|
|
2503
|
+
const agent = await T200024.create({ pin, keyPath: opts.key });
|
|
2504
|
+
const status = await agent.getStrategyStatus(name.toLowerCase());
|
|
2505
|
+
if (isJsonMode()) {
|
|
2506
|
+
printJson(status);
|
|
2507
|
+
return;
|
|
2508
|
+
}
|
|
2509
|
+
printBlank();
|
|
2510
|
+
printHeader(`Strategy: ${status.definition.name}`);
|
|
2511
|
+
printSeparator();
|
|
2512
|
+
if (status.positions.length === 0) {
|
|
2513
|
+
printInfo("No positions yet. Buy in with: t2000 invest strategy buy " + name + " 100");
|
|
2514
|
+
} else {
|
|
2515
|
+
for (const pos of status.positions) {
|
|
2516
|
+
const target = status.definition.allocations[pos.asset] ?? 0;
|
|
2517
|
+
const actual = status.currentWeights[pos.asset] ?? 0;
|
|
2518
|
+
const drift = actual - target;
|
|
2519
|
+
const driftColor = Math.abs(drift) > 3 ? pc13.yellow : pc13.dim;
|
|
2520
|
+
const pnlColor = pos.unrealizedPnL >= 0 ? pc13.green : pc13.red;
|
|
2521
|
+
const pnlSign = pos.unrealizedPnL >= 0 ? "+" : "";
|
|
2522
|
+
printKeyValue(
|
|
2523
|
+
pos.asset,
|
|
2524
|
+
`${formatAssetAmount(pos.totalAmount, pos.asset)} ${formatUsd15(pos.currentValue)} ${pnlColor(`${pnlSign}${formatUsd15(pos.unrealizedPnL)}`)} ${driftColor(`${actual.toFixed(0)}% / ${target}% target`)}`
|
|
2525
|
+
);
|
|
2526
|
+
}
|
|
2527
|
+
printSeparator();
|
|
2528
|
+
printKeyValue("Total value", formatUsd15(status.totalValue));
|
|
2529
|
+
}
|
|
2530
|
+
printBlank();
|
|
2531
|
+
} catch (error) {
|
|
2532
|
+
handleError(error);
|
|
2533
|
+
}
|
|
2534
|
+
});
|
|
2535
|
+
strategyCmd.command("rebalance <name>").description("Rebalance a strategy to target weights").option("--key <path>", "Key file path").action(async (name, opts) => {
|
|
2536
|
+
try {
|
|
2537
|
+
const pin = await resolvePin();
|
|
2538
|
+
const agent = await T200024.create({ pin, keyPath: opts.key });
|
|
2539
|
+
const result = await agent.rebalanceStrategy({ strategy: name.toLowerCase() });
|
|
2540
|
+
if (isJsonMode()) {
|
|
2541
|
+
printJson(result);
|
|
2542
|
+
return;
|
|
2543
|
+
}
|
|
2544
|
+
printBlank();
|
|
2545
|
+
if (result.trades.length === 0) {
|
|
2546
|
+
printInfo(`Strategy '${name}' is already balanced (within 3% threshold)`);
|
|
2547
|
+
} else {
|
|
2548
|
+
printSuccess(`Rebalanced ${name} strategy`);
|
|
2549
|
+
printSeparator();
|
|
2550
|
+
for (const t of result.trades) {
|
|
2551
|
+
const action = t.action === "buy" ? pc13.green("BUY") : pc13.red("SELL");
|
|
2552
|
+
printKeyValue(t.asset, `${action} ${formatUsd15(t.usdAmount)} (${formatAssetAmount(t.amount, t.asset)})`);
|
|
2553
|
+
}
|
|
2554
|
+
printSeparator();
|
|
2555
|
+
printInfo("Weights: " + Object.entries(result.afterWeights).map(([a, w]) => `${a} ${w.toFixed(0)}%`).join(", "));
|
|
2556
|
+
}
|
|
2557
|
+
printBlank();
|
|
2558
|
+
} catch (error) {
|
|
2559
|
+
handleError(error);
|
|
2560
|
+
}
|
|
2561
|
+
});
|
|
2562
|
+
strategyCmd.command("create <name>").description("Create a custom strategy").requiredOption("--alloc <pairs...>", "Allocations e.g. SUI:40 BTC:20 ETH:20 GOLD:20").option("--description <desc>", "Strategy description").action(async (name, opts) => {
|
|
2563
|
+
try {
|
|
2564
|
+
const allocations = {};
|
|
2565
|
+
for (const pair of opts.alloc) {
|
|
2566
|
+
const [asset, pctStr] = pair.split(":");
|
|
2567
|
+
if (!asset || !pctStr) {
|
|
2568
|
+
console.error(pc13.red(` \u2717 Invalid allocation: '${pair}'. Use ASSET:PCT format (e.g. SUI:60)`));
|
|
2569
|
+
process.exitCode = 1;
|
|
2570
|
+
return;
|
|
2571
|
+
}
|
|
2572
|
+
allocations[asset.toUpperCase()] = parseFloat(pctStr);
|
|
2573
|
+
}
|
|
2574
|
+
const pin = await resolvePin();
|
|
2575
|
+
const agent = await T200024.create({ pin });
|
|
2576
|
+
const definition = agent.strategies.create({ name, allocations, description: opts.description });
|
|
2577
|
+
if (isJsonMode()) {
|
|
2578
|
+
printJson(definition);
|
|
2579
|
+
return;
|
|
2580
|
+
}
|
|
2581
|
+
printBlank();
|
|
2582
|
+
printSuccess(`Created strategy '${name}'`);
|
|
2583
|
+
const allocs = Object.entries(definition.allocations).map(([a, p]) => `${a} ${p}%`).join(", ");
|
|
2584
|
+
printKeyValue("Allocations", allocs);
|
|
2585
|
+
printBlank();
|
|
2586
|
+
} catch (error) {
|
|
2587
|
+
handleError(error);
|
|
2588
|
+
}
|
|
2589
|
+
});
|
|
2590
|
+
strategyCmd.command("delete <name>").description("Delete a custom strategy").option("--key <path>", "Key file path").action(async (name, opts) => {
|
|
2591
|
+
try {
|
|
2592
|
+
const pin = await resolvePin();
|
|
2593
|
+
const agent = await T200024.create({ pin, keyPath: opts.key });
|
|
2594
|
+
if (agent.portfolio.hasStrategyPositions(name.toLowerCase())) {
|
|
2595
|
+
console.error(pc13.red(` \u2717 Strategy '${name}' has open positions. Sell first: t2000 invest strategy sell ${name}`));
|
|
2596
|
+
process.exitCode = 1;
|
|
2597
|
+
return;
|
|
2598
|
+
}
|
|
2599
|
+
agent.strategies.delete(name.toLowerCase());
|
|
2600
|
+
if (isJsonMode()) {
|
|
2601
|
+
printJson({ deleted: name });
|
|
2602
|
+
return;
|
|
2603
|
+
}
|
|
2604
|
+
printBlank();
|
|
2605
|
+
printSuccess(`Deleted strategy '${name}'`);
|
|
2606
|
+
printBlank();
|
|
2607
|
+
} catch (error) {
|
|
2608
|
+
handleError(error);
|
|
2609
|
+
}
|
|
2610
|
+
});
|
|
2611
|
+
const autoCmd = investCmd.command("auto").description("Dollar-cost averaging (DCA) schedules");
|
|
2612
|
+
autoCmd.command("setup <amount> <frequency> [target]").description("Create a DCA schedule (target = strategy name or asset)").option("--key <path>", "Key file path").option("--day <num>", "Day of week (1-7) or month (1-28)").action(async (amount, frequency, target, opts) => {
|
|
2613
|
+
try {
|
|
2614
|
+
const parsed = parseFloat(amount);
|
|
2615
|
+
if (isNaN(parsed) || parsed < 1) {
|
|
2616
|
+
console.error(pc13.red(" \u2717 Amount must be at least $1"));
|
|
2617
|
+
process.exitCode = 1;
|
|
2618
|
+
return;
|
|
2619
|
+
}
|
|
2620
|
+
if (!["daily", "weekly", "monthly"].includes(frequency)) {
|
|
2621
|
+
console.error(pc13.red(" \u2717 Frequency must be daily, weekly, or monthly"));
|
|
2622
|
+
process.exitCode = 1;
|
|
2623
|
+
return;
|
|
2624
|
+
}
|
|
2625
|
+
const pin = await resolvePin();
|
|
2626
|
+
const agent = await T200024.create({ pin, keyPath: opts.key });
|
|
2627
|
+
const allStrategies = agent.strategies.getAll();
|
|
2628
|
+
const isStrategy = target ? target.toLowerCase() in allStrategies : false;
|
|
2629
|
+
const isAsset = target ? target.toUpperCase() in INVESTMENT_ASSETS2 : false;
|
|
2630
|
+
if (target && !isStrategy && !isAsset) {
|
|
2631
|
+
console.error(pc13.red(` \u2717 '${target}' is not a valid strategy or asset`));
|
|
2632
|
+
process.exitCode = 1;
|
|
2633
|
+
return;
|
|
2634
|
+
}
|
|
2635
|
+
const dayNum = opts.day ? parseInt(opts.day, 10) : void 0;
|
|
2636
|
+
const schedule = agent.setupAutoInvest({
|
|
2637
|
+
amount: parsed,
|
|
2638
|
+
frequency,
|
|
2639
|
+
strategy: isStrategy ? target.toLowerCase() : void 0,
|
|
2640
|
+
asset: isAsset ? target.toUpperCase() : void 0,
|
|
2641
|
+
dayOfWeek: frequency === "weekly" ? dayNum : void 0,
|
|
2642
|
+
dayOfMonth: frequency === "monthly" ? dayNum : void 0
|
|
2643
|
+
});
|
|
2644
|
+
if (isJsonMode()) {
|
|
2645
|
+
printJson(schedule);
|
|
2646
|
+
return;
|
|
2647
|
+
}
|
|
2648
|
+
printBlank();
|
|
2649
|
+
const targetLabel = schedule.strategy ?? schedule.asset ?? "unknown";
|
|
2650
|
+
printSuccess(`Auto-invest created: ${formatUsd15(schedule.amount)} ${schedule.frequency} \u2192 ${targetLabel}`);
|
|
2651
|
+
printKeyValue("Schedule ID", schedule.id);
|
|
2652
|
+
printKeyValue("Next run", new Date(schedule.nextRun).toLocaleDateString());
|
|
2653
|
+
printInfo("Run manually: t2000 invest auto run");
|
|
2654
|
+
printBlank();
|
|
2655
|
+
} catch (error) {
|
|
2656
|
+
handleError(error);
|
|
2657
|
+
}
|
|
2658
|
+
});
|
|
2659
|
+
autoCmd.command("status").description("Show all DCA schedules").option("--key <path>", "Key file path").action(async (opts) => {
|
|
2660
|
+
try {
|
|
2661
|
+
const pin = await resolvePin();
|
|
2662
|
+
const agent = await T200024.create({ pin, keyPath: opts.key });
|
|
2663
|
+
const status = agent.getAutoInvestStatus();
|
|
2664
|
+
if (isJsonMode()) {
|
|
2665
|
+
printJson(status);
|
|
2666
|
+
return;
|
|
2667
|
+
}
|
|
2668
|
+
printBlank();
|
|
2669
|
+
if (status.schedules.length === 0) {
|
|
2670
|
+
printInfo("No auto-invest schedules. Set one up: t2000 invest auto setup 50 weekly bluechip");
|
|
2671
|
+
printBlank();
|
|
2672
|
+
return;
|
|
2673
|
+
}
|
|
2674
|
+
printHeader("Auto-Invest Schedules");
|
|
2675
|
+
printSeparator();
|
|
2676
|
+
for (const s of status.schedules) {
|
|
2677
|
+
const target = s.strategy ?? s.asset ?? "?";
|
|
2678
|
+
const statusTag = s.enabled ? pc13.green("active") : pc13.dim("paused");
|
|
2679
|
+
printKeyValue(s.id, `${formatUsd15(s.amount)} ${s.frequency} \u2192 ${target} ${statusTag}`);
|
|
2680
|
+
printLine(` ${pc13.dim(`Next: ${new Date(s.nextRun).toLocaleDateString()} \xB7 Runs: ${s.runCount} \xB7 Total: ${formatUsd15(s.totalInvested)}`)}`);
|
|
2681
|
+
}
|
|
2682
|
+
printSeparator();
|
|
2683
|
+
if (status.pendingRuns.length > 0) {
|
|
2684
|
+
printInfo(`${status.pendingRuns.length} pending run(s). Execute: t2000 invest auto run`);
|
|
2685
|
+
} else {
|
|
2686
|
+
printInfo("All schedules up to date");
|
|
2687
|
+
}
|
|
2688
|
+
printBlank();
|
|
2689
|
+
} catch (error) {
|
|
2690
|
+
handleError(error);
|
|
2691
|
+
}
|
|
2692
|
+
});
|
|
2693
|
+
autoCmd.command("run").description("Execute pending DCA purchases").option("--key <path>", "Key file path").action(async (opts) => {
|
|
2694
|
+
try {
|
|
2695
|
+
const pin = await resolvePin();
|
|
2696
|
+
const agent = await T200024.create({ pin, keyPath: opts.key });
|
|
2697
|
+
const status = agent.getAutoInvestStatus();
|
|
2698
|
+
if (status.pendingRuns.length === 0) {
|
|
2699
|
+
if (isJsonMode()) {
|
|
2700
|
+
printJson({ executed: [], skipped: [] });
|
|
2701
|
+
return;
|
|
2702
|
+
}
|
|
2703
|
+
printBlank();
|
|
2704
|
+
printInfo("No pending auto-invest runs. All schedules are up to date.");
|
|
2705
|
+
printBlank();
|
|
2706
|
+
return;
|
|
2707
|
+
}
|
|
2708
|
+
const result = await agent.runAutoInvest();
|
|
2709
|
+
if (isJsonMode()) {
|
|
2710
|
+
printJson(result);
|
|
2711
|
+
return;
|
|
2712
|
+
}
|
|
2713
|
+
printBlank();
|
|
2714
|
+
if (result.executed.length > 0) {
|
|
2715
|
+
printSuccess(`Executed ${result.executed.length} auto-invest run(s)`);
|
|
2716
|
+
for (const exec2 of result.executed) {
|
|
2717
|
+
const target = exec2.strategy ?? exec2.asset ?? "?";
|
|
2718
|
+
printKeyValue(target, formatUsd15(exec2.amount));
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
if (result.skipped.length > 0) {
|
|
2722
|
+
for (const skip of result.skipped) {
|
|
2723
|
+
printLine(` ${pc13.yellow("\u26A0")} Skipped ${skip.scheduleId}: ${skip.reason}`);
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
printBlank();
|
|
2727
|
+
} catch (error) {
|
|
2728
|
+
handleError(error);
|
|
2729
|
+
}
|
|
2730
|
+
});
|
|
2731
|
+
autoCmd.command("stop <id>").description("Stop an auto-invest schedule").option("--key <path>", "Key file path").action(async (id, opts) => {
|
|
2732
|
+
try {
|
|
2733
|
+
const pin = await resolvePin();
|
|
2734
|
+
const agent = await T200024.create({ pin, keyPath: opts.key });
|
|
2735
|
+
agent.stopAutoInvest(id);
|
|
2736
|
+
if (isJsonMode()) {
|
|
2737
|
+
printJson({ stopped: id });
|
|
2738
|
+
return;
|
|
2739
|
+
}
|
|
2740
|
+
printBlank();
|
|
2741
|
+
printSuccess(`Stopped auto-invest schedule ${id}`);
|
|
2742
|
+
printBlank();
|
|
2743
|
+
} catch (error) {
|
|
2744
|
+
handleError(error);
|
|
2745
|
+
}
|
|
2746
|
+
});
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
// src/commands/portfolio.ts
|
|
2750
|
+
import pc14 from "picocolors";
|
|
2751
|
+
import { T2000 as T200025, formatUsd as formatUsd16, formatAssetAmount as formatAssetAmount2 } from "@t2000/sdk";
|
|
2752
|
+
function printPositionLine(pos, rewardKeys) {
|
|
2753
|
+
if (pos.currentPrice === 0 && pos.totalAmount > 0) {
|
|
2754
|
+
printKeyValue(
|
|
2755
|
+
pos.asset,
|
|
2756
|
+
`${formatAssetAmount2(pos.totalAmount, pos.asset)} Avg: ${formatUsd16(pos.avgPrice)} Now: ${pc14.yellow("unavailable")}`
|
|
2757
|
+
);
|
|
2758
|
+
} else {
|
|
2759
|
+
const pnlColor = pos.unrealizedPnL >= 0 ? pc14.green : pc14.red;
|
|
2760
|
+
const pnlSign = pos.unrealizedPnL >= 0 ? "+" : "";
|
|
2761
|
+
let yieldSuffix = "";
|
|
2762
|
+
if (pos.earning && pos.earningApy) {
|
|
2763
|
+
const hasRewards = rewardKeys?.has(`${pos.earningProtocol}:${pos.asset}`);
|
|
2764
|
+
const rewardTag = hasRewards ? ` ${pc14.yellow("+rewards")}` : "";
|
|
2765
|
+
yieldSuffix = ` ${pc14.cyan(`${pos.earningApy.toFixed(1)}% APY (${pos.earningProtocol})`)}${rewardTag}`;
|
|
2766
|
+
}
|
|
2767
|
+
printKeyValue(
|
|
2768
|
+
pos.asset,
|
|
2769
|
+
`${formatAssetAmount2(pos.totalAmount, pos.asset)} Avg: ${formatUsd16(pos.avgPrice)} Now: ${formatUsd16(pos.currentPrice)} ${pnlColor(`${pnlSign}${formatUsd16(pos.unrealizedPnL)} (${pnlSign}${pos.unrealizedPnLPct.toFixed(1)}%)`)}${yieldSuffix}`
|
|
2770
|
+
);
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
function registerPortfolio(program2) {
|
|
2774
|
+
program2.command("portfolio").description("Show investment portfolio").option("--key <path>", "Key file path").action(async (opts) => {
|
|
2775
|
+
try {
|
|
2776
|
+
const pin = await resolvePin();
|
|
2777
|
+
const agent = await T200025.create({ pin, keyPath: opts.key });
|
|
2778
|
+
const portfolio = await agent.getPortfolio();
|
|
2779
|
+
if (isJsonMode()) {
|
|
2780
|
+
printJson(portfolio);
|
|
2781
|
+
return;
|
|
2782
|
+
}
|
|
2783
|
+
const rewardKeys = /* @__PURE__ */ new Set();
|
|
2784
|
+
try {
|
|
2785
|
+
const pending = await agent.getPendingRewards();
|
|
2786
|
+
for (const r of pending) rewardKeys.add(`${r.protocol}:${r.asset}`);
|
|
2787
|
+
} catch {
|
|
2788
|
+
}
|
|
2789
|
+
printBlank();
|
|
2790
|
+
const hasDirectPositions = portfolio.positions.length > 0;
|
|
2791
|
+
const hasStrategyPositions = portfolio.strategyPositions && Object.keys(portfolio.strategyPositions).length > 0;
|
|
2792
|
+
if (!hasDirectPositions && !hasStrategyPositions) {
|
|
2793
|
+
printInfo("No investments yet. Try: t2000 invest buy 100 SUI");
|
|
2794
|
+
printBlank();
|
|
2795
|
+
return;
|
|
2796
|
+
}
|
|
2797
|
+
printHeader("Investment Portfolio");
|
|
2798
|
+
if (hasStrategyPositions) {
|
|
2799
|
+
for (const [key, positions] of Object.entries(portfolio.strategyPositions)) {
|
|
2800
|
+
let stratLabel = key;
|
|
2801
|
+
try {
|
|
2802
|
+
const def = agent.strategies.get(key);
|
|
2803
|
+
stratLabel = def.name;
|
|
2804
|
+
} catch {
|
|
2805
|
+
}
|
|
2806
|
+
printLine(` ${pc14.bold(pc14.cyan(`\u25B8 ${stratLabel}`))}`);
|
|
2807
|
+
printSeparator();
|
|
2808
|
+
for (const pos of positions) {
|
|
2809
|
+
printPositionLine(pos, rewardKeys);
|
|
2810
|
+
}
|
|
2811
|
+
const stratValue = positions.reduce((s, p) => s + p.currentValue, 0);
|
|
2812
|
+
printLine(` ${pc14.dim(`Subtotal: ${formatUsd16(stratValue)}`)}`);
|
|
2813
|
+
printBlank();
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
if (hasDirectPositions) {
|
|
2817
|
+
if (hasStrategyPositions) {
|
|
2818
|
+
printLine(` ${pc14.bold(pc14.cyan("\u25B8 Direct"))}`);
|
|
2819
|
+
}
|
|
2820
|
+
printSeparator();
|
|
2821
|
+
for (const pos of portfolio.positions) {
|
|
2822
|
+
printPositionLine(pos, rewardKeys);
|
|
2823
|
+
}
|
|
2824
|
+
if (hasStrategyPositions) {
|
|
2825
|
+
const directValue = portfolio.positions.reduce((s, p) => s + p.currentValue, 0);
|
|
2826
|
+
printLine(` ${pc14.dim(`Subtotal: ${formatUsd16(directValue)}`)}`);
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
printSeparator();
|
|
2830
|
+
const hasPriceUnavailable = portfolio.positions.some((p) => p.currentPrice === 0 && p.totalAmount > 0);
|
|
2831
|
+
if (hasPriceUnavailable) {
|
|
2832
|
+
printInfo(pc14.yellow("\u26A0 Price data unavailable for some assets. Values may be inaccurate."));
|
|
2833
|
+
}
|
|
2834
|
+
printKeyValue("Total invested", formatUsd16(portfolio.totalInvested));
|
|
2835
|
+
printKeyValue("Current value", formatUsd16(portfolio.totalValue));
|
|
2836
|
+
const upnlColor = portfolio.unrealizedPnL >= 0 ? pc14.green : pc14.red;
|
|
2837
|
+
const upnlSign = portfolio.unrealizedPnL >= 0 ? "+" : "";
|
|
2838
|
+
printKeyValue("Unrealized P&L", upnlColor(`${upnlSign}${formatUsd16(portfolio.unrealizedPnL)} (${upnlSign}${portfolio.unrealizedPnLPct.toFixed(1)}%)`));
|
|
2839
|
+
if (portfolio.realizedPnL !== 0) {
|
|
2840
|
+
const rpnlColor = portfolio.realizedPnL >= 0 ? pc14.green : pc14.red;
|
|
2841
|
+
const rpnlSign = portfolio.realizedPnL >= 0 ? "+" : "";
|
|
2842
|
+
printKeyValue("Realized P&L", rpnlColor(`${rpnlSign}${formatUsd16(portfolio.realizedPnL)}`));
|
|
2843
|
+
}
|
|
2844
|
+
printBlank();
|
|
2845
|
+
} catch (error) {
|
|
2846
|
+
handleError(error);
|
|
2847
|
+
}
|
|
2848
|
+
});
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2851
|
+
// src/commands/claimRewards.ts
|
|
2852
|
+
import pc15 from "picocolors";
|
|
2853
|
+
import { T2000 as T200026, formatUsd as formatUsd17 } from "@t2000/sdk";
|
|
2854
|
+
function registerClaimRewards(program2) {
|
|
2855
|
+
program2.command("claim-rewards").description("Claim pending protocol rewards").option("--key <path>", "Key file path").action(async (opts) => {
|
|
2856
|
+
try {
|
|
2857
|
+
const pin = await resolvePin();
|
|
2858
|
+
const agent = await T200026.create({ pin, keyPath: opts.key });
|
|
2859
|
+
const result = await agent.claimRewards();
|
|
2860
|
+
if (isJsonMode()) {
|
|
2861
|
+
printJson(result);
|
|
2862
|
+
return;
|
|
2863
|
+
}
|
|
2864
|
+
printBlank();
|
|
2865
|
+
if (result.rewards.length === 0) {
|
|
2866
|
+
printLine(` ${pc15.dim("No rewards to claim")}`);
|
|
2867
|
+
printBlank();
|
|
2868
|
+
return;
|
|
2869
|
+
}
|
|
2870
|
+
const protocols = [...new Set(result.rewards.map((r) => r.protocol))];
|
|
2871
|
+
printLine(` ${pc15.green("\u2713")} Claimed and converted rewards to USDC`);
|
|
2872
|
+
printSeparator();
|
|
2873
|
+
const received = result.usdcReceived;
|
|
2874
|
+
if (received >= 0.01) {
|
|
2875
|
+
printKeyValue("Received", `${pc15.green(formatUsd17(received))} USDC`);
|
|
2876
|
+
} else if (received > 0) {
|
|
2877
|
+
printKeyValue("Received", `${pc15.green("< $0.01")} USDC`);
|
|
2878
|
+
} else {
|
|
2879
|
+
printKeyValue("Received", `${pc15.dim("< $0.01 USDC (rewards are still accruing)")}`);
|
|
2880
|
+
}
|
|
2881
|
+
printKeyValue("Source", protocols.join(", "));
|
|
2882
|
+
if (result.tx) {
|
|
2883
|
+
printKeyValue("Tx", `https://suiscan.xyz/mainnet/tx/${result.tx}`);
|
|
2884
|
+
}
|
|
2885
|
+
printBlank();
|
|
2886
|
+
} catch (error) {
|
|
2887
|
+
handleError(error);
|
|
2888
|
+
}
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
// src/commands/gateway.ts
|
|
2893
|
+
import pc16 from "picocolors";
|
|
2894
|
+
import { T2000 as T200027 } from "@t2000/sdk";
|
|
2895
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4, unlinkSync } from "fs";
|
|
2896
|
+
import { join as join6 } from "path";
|
|
2897
|
+
import { homedir as homedir8, platform as platform2 } from "os";
|
|
2898
|
+
import { execSync } from "child_process";
|
|
2899
|
+
var PLIST_LABEL = "com.t2000.gateway";
|
|
2900
|
+
var SYSTEMD_UNIT = "t2000-gateway";
|
|
2901
|
+
function getLaunchAgentPath() {
|
|
2902
|
+
return join6(homedir8(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
|
|
2903
|
+
}
|
|
2904
|
+
function getSystemdPath() {
|
|
2905
|
+
return join6(homedir8(), ".config", "systemd", "user", `${SYSTEMD_UNIT}.service`);
|
|
2906
|
+
}
|
|
2907
|
+
function getLogDir() {
|
|
2908
|
+
return join6(homedir8(), ".t2000", "logs");
|
|
2909
|
+
}
|
|
2910
|
+
function getT2000Bin() {
|
|
2911
|
+
try {
|
|
2912
|
+
return execSync("which t2000", { encoding: "utf-8" }).trim();
|
|
2913
|
+
} catch {
|
|
2914
|
+
return "npx t2000";
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
function registerGateway(program2) {
|
|
2918
|
+
const gw = program2.command("gateway").description("Start AI financial advisor gateway");
|
|
2919
|
+
gw.command("start", { isDefault: true }).description("Start the gateway (foreground)").option("--port <port>", "WebChat port", "2000").option("--no-telegram", "Skip Telegram channel").option("--no-heartbeat", "Skip heartbeat daemon").option("--verbose", "Debug logging").option("--key <path>", "Key file path").action(async (opts) => {
|
|
2920
|
+
try {
|
|
2921
|
+
const pin = await resolvePin();
|
|
2922
|
+
const agent = await T200027.create({ pin, keyPath: opts.key });
|
|
2923
|
+
console.log("");
|
|
2924
|
+
console.log(` ${pc16.bold("t2000 gateway")}`);
|
|
2925
|
+
console.log("");
|
|
2926
|
+
const { Gateway } = await import("./dist-IHO4UPEB.js");
|
|
2927
|
+
const gateway = await Gateway.create({
|
|
2928
|
+
agent,
|
|
2929
|
+
port: parseInt(opts.port, 10),
|
|
2930
|
+
noTelegram: !opts.telegram,
|
|
2931
|
+
noHeartbeat: !opts.heartbeat,
|
|
2932
|
+
verbose: opts.verbose
|
|
2933
|
+
});
|
|
2934
|
+
const info = await gateway.start();
|
|
2935
|
+
const truncAddr = info.address.slice(0, 6) + "..." + info.address.slice(-4);
|
|
2936
|
+
printSuccess(`Agent unlocked (${truncAddr})`);
|
|
2937
|
+
printSuccess(`${info.llmProvider === "anthropic" ? "Claude" : "GPT"} connected (${info.llmModel})`);
|
|
2938
|
+
if (info.telegramConnected) printSuccess("Telegram connected");
|
|
2939
|
+
if (info.webchatUrl) printSuccess(`WebChat at ${pc16.underline(info.webchatUrl)}`);
|
|
2940
|
+
if (info.heartbeatTasks > 0) printSuccess(`Heartbeat started (${info.heartbeatTasks} tasks)`);
|
|
2941
|
+
printSuccess("Ready \u2014 talk to your agent");
|
|
2942
|
+
console.log("");
|
|
2943
|
+
await new Promise(() => {
|
|
2944
|
+
});
|
|
2945
|
+
} catch (error) {
|
|
2946
|
+
handleError(error);
|
|
2947
|
+
}
|
|
2948
|
+
});
|
|
2949
|
+
gw.command("status").description("Check if gateway is running").option("--port <port>", "WebChat port to check", "2000").action(async (opts) => {
|
|
2950
|
+
try {
|
|
2951
|
+
const port = parseInt(opts.port, 10);
|
|
2952
|
+
const response = await fetch(`http://127.0.0.1:${port}/health`);
|
|
2953
|
+
if (response.ok) {
|
|
2954
|
+
const data = await response.json();
|
|
2955
|
+
printSuccess(`Gateway is running on port ${port} (${data.status})`);
|
|
2956
|
+
}
|
|
2957
|
+
} catch {
|
|
2958
|
+
console.log(` ${pc16.yellow("\u25CB")} Gateway is not running on port ${opts.port}`);
|
|
2959
|
+
}
|
|
2960
|
+
});
|
|
2961
|
+
gw.command("install").description("Install gateway as a background daemon").option("--port <port>", "WebChat port", "2000").action(async (opts) => {
|
|
2962
|
+
try {
|
|
2963
|
+
const os = platform2();
|
|
2964
|
+
const bin = getT2000Bin();
|
|
2965
|
+
if (os === "darwin") {
|
|
2966
|
+
installLaunchd(bin, opts.port);
|
|
2967
|
+
} else if (os === "linux") {
|
|
2968
|
+
installSystemd(bin, opts.port);
|
|
2969
|
+
} else {
|
|
2970
|
+
throw new Error(`Unsupported platform: ${os}. Use macOS or Linux.`);
|
|
2971
|
+
}
|
|
2972
|
+
} catch (error) {
|
|
2973
|
+
handleError(error);
|
|
2974
|
+
}
|
|
2975
|
+
});
|
|
2976
|
+
gw.command("uninstall").description("Remove gateway daemon").action(async () => {
|
|
2977
|
+
try {
|
|
2978
|
+
const os = platform2();
|
|
2979
|
+
if (os === "darwin") {
|
|
2980
|
+
uninstallLaunchd();
|
|
2981
|
+
} else if (os === "linux") {
|
|
2982
|
+
uninstallSystemd();
|
|
2983
|
+
} else {
|
|
2984
|
+
throw new Error(`Unsupported platform: ${os}`);
|
|
2985
|
+
}
|
|
2986
|
+
} catch (error) {
|
|
2987
|
+
handleError(error);
|
|
2988
|
+
}
|
|
2989
|
+
});
|
|
2990
|
+
gw.command("logs").description("Tail gateway logs").option("-n <lines>", "Number of lines", "50").option("-f, --follow", "Follow log output").action(async (opts) => {
|
|
2991
|
+
try {
|
|
2992
|
+
const logPath = join6(getLogDir(), "gateway.log");
|
|
2993
|
+
if (!existsSync5(logPath)) {
|
|
2994
|
+
printWarning("No gateway logs found yet.");
|
|
2995
|
+
printInfo(`Log path: ${logPath}`);
|
|
2996
|
+
return;
|
|
2997
|
+
}
|
|
2998
|
+
printBlank();
|
|
2999
|
+
printInfo(`Log file: ${logPath}`);
|
|
3000
|
+
printBlank();
|
|
3001
|
+
const content = readFileSync4(logPath, "utf-8");
|
|
3002
|
+
const lines = content.trim().split("\n");
|
|
3003
|
+
const tail = lines.slice(-parseInt(opts.n, 10));
|
|
3004
|
+
for (const line of tail) {
|
|
3005
|
+
try {
|
|
3006
|
+
const entry = JSON.parse(line);
|
|
3007
|
+
const time = new Date(entry.ts).toLocaleTimeString();
|
|
3008
|
+
const levelColor = entry.level === "error" ? pc16.red : entry.level === "warn" ? pc16.yellow : pc16.dim;
|
|
3009
|
+
printLine(`${pc16.dim(time)} ${levelColor(entry.level.padEnd(5))} ${entry.msg}`);
|
|
3010
|
+
} catch {
|
|
3011
|
+
printLine(line);
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
if (opts.follow) {
|
|
3015
|
+
printBlank();
|
|
3016
|
+
printInfo("Following... (Ctrl+C to stop)");
|
|
3017
|
+
const { spawn } = await import("child_process");
|
|
3018
|
+
const child = spawn("tail", ["-f", logPath], { stdio: "inherit" });
|
|
3019
|
+
process.on("SIGINT", () => {
|
|
3020
|
+
child.kill();
|
|
3021
|
+
process.exit(0);
|
|
3022
|
+
});
|
|
3023
|
+
await new Promise(() => {
|
|
3024
|
+
});
|
|
3025
|
+
}
|
|
3026
|
+
printBlank();
|
|
3027
|
+
} catch (error) {
|
|
3028
|
+
handleError(error);
|
|
3029
|
+
}
|
|
3030
|
+
});
|
|
3031
|
+
}
|
|
3032
|
+
function installLaunchd(bin, port) {
|
|
3033
|
+
const logDir = getLogDir();
|
|
3034
|
+
const plistPath = getLaunchAgentPath();
|
|
3035
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
3036
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3037
|
+
<plist version="1.0">
|
|
3038
|
+
<dict>
|
|
3039
|
+
<key>Label</key>
|
|
3040
|
+
<string>${PLIST_LABEL}</string>
|
|
3041
|
+
<key>ProgramArguments</key>
|
|
3042
|
+
<array>
|
|
3043
|
+
<string>${bin}</string>
|
|
3044
|
+
<string>gateway</string>
|
|
3045
|
+
<string>start</string>
|
|
3046
|
+
<string>--port</string>
|
|
3047
|
+
<string>${port}</string>
|
|
3048
|
+
</array>
|
|
3049
|
+
<key>RunAtLoad</key>
|
|
3050
|
+
<true/>
|
|
3051
|
+
<key>KeepAlive</key>
|
|
3052
|
+
<true/>
|
|
3053
|
+
<key>StandardOutPath</key>
|
|
3054
|
+
<string>${logDir}/gateway-stdout.log</string>
|
|
3055
|
+
<key>StandardErrorPath</key>
|
|
3056
|
+
<string>${logDir}/gateway-stderr.log</string>
|
|
3057
|
+
<key>EnvironmentVariables</key>
|
|
3058
|
+
<dict>
|
|
3059
|
+
<key>PATH</key>
|
|
3060
|
+
<string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
|
|
3061
|
+
</dict>
|
|
3062
|
+
</dict>
|
|
3063
|
+
</plist>`;
|
|
3064
|
+
if (existsSync5(plistPath)) {
|
|
3065
|
+
try {
|
|
3066
|
+
execSync(`launchctl unload "${plistPath}" 2>/dev/null`);
|
|
3067
|
+
} catch {
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
writeFileSync4(plistPath, plist);
|
|
3071
|
+
execSync(`launchctl load "${plistPath}"`);
|
|
3072
|
+
printBlank();
|
|
3073
|
+
printSuccess("Gateway daemon installed");
|
|
3074
|
+
printSuccess(`Starts on boot \u2014 runs in background`);
|
|
3075
|
+
printLine(` ${pc16.dim("Logs:")} ${logDir}/gateway.log`);
|
|
3076
|
+
printLine(` ${pc16.dim("Stop:")} t2000 gateway uninstall`);
|
|
3077
|
+
printBlank();
|
|
3078
|
+
}
|
|
3079
|
+
function uninstallLaunchd() {
|
|
3080
|
+
const plistPath = getLaunchAgentPath();
|
|
3081
|
+
if (!existsSync5(plistPath)) {
|
|
3082
|
+
printWarning("Gateway daemon is not installed.");
|
|
3083
|
+
return;
|
|
3084
|
+
}
|
|
3085
|
+
try {
|
|
3086
|
+
execSync(`launchctl unload "${plistPath}" 2>/dev/null`);
|
|
3087
|
+
} catch {
|
|
3088
|
+
}
|
|
3089
|
+
unlinkSync(plistPath);
|
|
3090
|
+
printBlank();
|
|
3091
|
+
printSuccess("Gateway daemon removed");
|
|
3092
|
+
printBlank();
|
|
3093
|
+
}
|
|
3094
|
+
function installSystemd(bin, port) {
|
|
3095
|
+
const logDir = getLogDir();
|
|
3096
|
+
const unitPath = getSystemdPath();
|
|
3097
|
+
const unitDir = join6(homedir8(), ".config", "systemd", "user");
|
|
3098
|
+
const unit = `[Unit]
|
|
3099
|
+
Description=t2000 Gateway \u2014 AI Financial Advisor
|
|
3100
|
+
After=network-online.target
|
|
3101
|
+
Wants=network-online.target
|
|
3102
|
+
|
|
3103
|
+
[Service]
|
|
3104
|
+
Type=simple
|
|
3105
|
+
ExecStart=${bin} gateway start --port ${port}
|
|
3106
|
+
Restart=on-failure
|
|
3107
|
+
RestartSec=10
|
|
3108
|
+
StandardOutput=append:${logDir}/gateway-stdout.log
|
|
3109
|
+
StandardError=append:${logDir}/gateway-stderr.log
|
|
3110
|
+
|
|
3111
|
+
[Install]
|
|
3112
|
+
WantedBy=default.target
|
|
3113
|
+
`;
|
|
3114
|
+
if (!existsSync5(unitDir)) {
|
|
3115
|
+
execSync(`mkdir -p "${unitDir}"`);
|
|
3116
|
+
}
|
|
3117
|
+
writeFileSync4(unitPath, unit);
|
|
3118
|
+
execSync("systemctl --user daemon-reload");
|
|
3119
|
+
execSync(`systemctl --user enable ${SYSTEMD_UNIT}`);
|
|
3120
|
+
execSync(`systemctl --user start ${SYSTEMD_UNIT}`);
|
|
3121
|
+
printBlank();
|
|
3122
|
+
printSuccess("Gateway daemon installed (systemd)");
|
|
3123
|
+
printSuccess("Starts on login \u2014 runs in background");
|
|
3124
|
+
printLine(` ${pc16.dim("Logs:")} ${logDir}/gateway.log`);
|
|
3125
|
+
printLine(` ${pc16.dim("Status:")} systemctl --user status ${SYSTEMD_UNIT}`);
|
|
3126
|
+
printLine(` ${pc16.dim("Stop:")} t2000 gateway uninstall`);
|
|
3127
|
+
printBlank();
|
|
3128
|
+
}
|
|
3129
|
+
function uninstallSystemd() {
|
|
3130
|
+
const unitPath = getSystemdPath();
|
|
3131
|
+
if (!existsSync5(unitPath)) {
|
|
3132
|
+
printWarning("Gateway daemon is not installed.");
|
|
3133
|
+
return;
|
|
3134
|
+
}
|
|
3135
|
+
try {
|
|
3136
|
+
execSync(`systemctl --user stop ${SYSTEMD_UNIT} 2>/dev/null`);
|
|
3137
|
+
} catch {
|
|
3138
|
+
}
|
|
3139
|
+
try {
|
|
3140
|
+
execSync(`systemctl --user disable ${SYSTEMD_UNIT} 2>/dev/null`);
|
|
3141
|
+
} catch {
|
|
3142
|
+
}
|
|
3143
|
+
unlinkSync(unitPath);
|
|
3144
|
+
try {
|
|
3145
|
+
execSync("systemctl --user daemon-reload");
|
|
3146
|
+
} catch {
|
|
3147
|
+
}
|
|
3148
|
+
printBlank();
|
|
3149
|
+
printSuccess("Gateway daemon removed");
|
|
3150
|
+
printBlank();
|
|
3151
|
+
}
|
|
3152
|
+
|
|
3153
|
+
// src/program.ts
|
|
3154
|
+
var require2 = createRequire(import.meta.url);
|
|
3155
|
+
var { version: CLI_VERSION } = require2("../package.json");
|
|
3156
|
+
function createProgram() {
|
|
3157
|
+
const program2 = new Command();
|
|
3158
|
+
program2.name("t2000").description("Your personal AI financial advisor").version(`${CLI_VERSION} (beta)`).option("--json", "Output in JSON format").hook("preAction", (thisCommand) => {
|
|
3159
|
+
const opts = thisCommand.optsWithGlobals();
|
|
3160
|
+
if (opts.json) setJsonMode(true);
|
|
3161
|
+
});
|
|
3162
|
+
registerInit(program2);
|
|
3163
|
+
registerSend(program2);
|
|
3164
|
+
registerBalance(program2);
|
|
3165
|
+
registerAddress(program2);
|
|
3166
|
+
registerDeposit(program2);
|
|
3167
|
+
registerHistory(program2);
|
|
3168
|
+
registerExport(program2);
|
|
3169
|
+
registerImport(program2);
|
|
3170
|
+
registerSave(program2);
|
|
3171
|
+
registerWithdraw(program2);
|
|
3172
|
+
registerBorrow(program2);
|
|
3173
|
+
registerRepay(program2);
|
|
3174
|
+
registerHealth(program2);
|
|
3175
|
+
registerRates(program2);
|
|
3176
|
+
registerPositions(program2);
|
|
3177
|
+
registerEarnings(program2);
|
|
3178
|
+
registerFundStatus(program2);
|
|
3179
|
+
registerConfig(program2);
|
|
3180
|
+
registerServe(program2);
|
|
3181
|
+
registerPay(program2);
|
|
3182
|
+
registerLock(program2);
|
|
3183
|
+
registerSentinel(program2);
|
|
3184
|
+
registerEarn(program2);
|
|
3185
|
+
registerRebalance(program2);
|
|
3186
|
+
registerExchange(program2);
|
|
3187
|
+
registerMcp(program2);
|
|
3188
|
+
registerContacts(program2);
|
|
3189
|
+
registerInvest(program2);
|
|
3190
|
+
registerPortfolio(program2);
|
|
3191
|
+
registerClaimRewards(program2);
|
|
3192
|
+
registerGateway(program2);
|
|
3193
|
+
return program2;
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3196
|
+
// src/index.ts
|
|
3197
|
+
var program = createProgram();
|
|
4
3198
|
program.parse();
|
|
5
3199
|
//# sourceMappingURL=index.js.map
|