@kernel.chat/kbot 3.23.0 → 3.26.1

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.
Files changed (85) hide show
  1. package/README.md +33 -22
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +3 -1
  4. package/dist/agent.js.map +1 -1
  5. package/dist/agents/trader.d.ts +32 -0
  6. package/dist/agents/trader.d.ts.map +1 -0
  7. package/dist/agents/trader.js +190 -0
  8. package/dist/agents/trader.js.map +1 -0
  9. package/dist/cli.js +219 -15
  10. package/dist/cli.js.map +1 -1
  11. package/dist/context.d.ts +4 -1
  12. package/dist/context.d.ts.map +1 -1
  13. package/dist/context.js +28 -1
  14. package/dist/context.js.map +1 -1
  15. package/dist/doctor.d.ts.map +1 -1
  16. package/dist/doctor.js +71 -1
  17. package/dist/doctor.js.map +1 -1
  18. package/dist/inference.d.ts +9 -0
  19. package/dist/inference.d.ts.map +1 -1
  20. package/dist/inference.js +38 -5
  21. package/dist/inference.js.map +1 -1
  22. package/dist/introspection.d.ts +17 -0
  23. package/dist/introspection.d.ts.map +1 -0
  24. package/dist/introspection.js +490 -0
  25. package/dist/introspection.js.map +1 -0
  26. package/dist/learned-router.d.ts.map +1 -1
  27. package/dist/learned-router.js +3 -0
  28. package/dist/learned-router.js.map +1 -1
  29. package/dist/machine.d.ts +85 -0
  30. package/dist/machine.d.ts.map +1 -0
  31. package/dist/machine.js +538 -0
  32. package/dist/machine.js.map +1 -0
  33. package/dist/matrix.d.ts.map +1 -1
  34. package/dist/matrix.js +11 -0
  35. package/dist/matrix.js.map +1 -1
  36. package/dist/provider-fallback.d.ts +6 -0
  37. package/dist/provider-fallback.d.ts.map +1 -1
  38. package/dist/provider-fallback.js +29 -0
  39. package/dist/provider-fallback.js.map +1 -1
  40. package/dist/synthesis-engine.d.ts +175 -0
  41. package/dist/synthesis-engine.d.ts.map +1 -0
  42. package/dist/synthesis-engine.js +783 -0
  43. package/dist/synthesis-engine.js.map +1 -0
  44. package/dist/tool-pipeline.d.ts +7 -1
  45. package/dist/tool-pipeline.d.ts.map +1 -1
  46. package/dist/tool-pipeline.js +39 -1
  47. package/dist/tool-pipeline.js.map +1 -1
  48. package/dist/tools/finance.d.ts +2 -0
  49. package/dist/tools/finance.d.ts.map +1 -0
  50. package/dist/tools/finance.js +1116 -0
  51. package/dist/tools/finance.js.map +1 -0
  52. package/dist/tools/finance.test.d.ts +2 -0
  53. package/dist/tools/finance.test.d.ts.map +1 -0
  54. package/dist/tools/finance.test.js +245 -0
  55. package/dist/tools/finance.test.js.map +1 -0
  56. package/dist/tools/index.d.ts.map +1 -1
  57. package/dist/tools/index.js +5 -0
  58. package/dist/tools/index.js.map +1 -1
  59. package/dist/tools/machine-tools.d.ts +2 -0
  60. package/dist/tools/machine-tools.d.ts.map +1 -0
  61. package/dist/tools/machine-tools.js +690 -0
  62. package/dist/tools/machine-tools.js.map +1 -0
  63. package/dist/tools/sentiment.d.ts +2 -0
  64. package/dist/tools/sentiment.d.ts.map +1 -0
  65. package/dist/tools/sentiment.js +513 -0
  66. package/dist/tools/sentiment.js.map +1 -0
  67. package/dist/tools/stocks.d.ts +2 -0
  68. package/dist/tools/stocks.d.ts.map +1 -0
  69. package/dist/tools/stocks.js +345 -0
  70. package/dist/tools/stocks.js.map +1 -0
  71. package/dist/tools/stocks.test.d.ts +2 -0
  72. package/dist/tools/stocks.test.d.ts.map +1 -0
  73. package/dist/tools/stocks.test.js +82 -0
  74. package/dist/tools/stocks.test.js.map +1 -0
  75. package/dist/tools/wallet.d.ts +2 -0
  76. package/dist/tools/wallet.d.ts.map +1 -0
  77. package/dist/tools/wallet.js +698 -0
  78. package/dist/tools/wallet.js.map +1 -0
  79. package/dist/tools/wallet.test.d.ts +2 -0
  80. package/dist/tools/wallet.test.d.ts.map +1 -0
  81. package/dist/tools/wallet.test.js +205 -0
  82. package/dist/tools/wallet.test.js.map +1 -0
  83. package/dist/ui.js +1 -1
  84. package/dist/ui.js.map +1 -1
  85. package/package.json +94 -42
@@ -0,0 +1,698 @@
1
+ // kbot Wallet & Swap Tools — Solana wallet management + Jupiter DEX execution
2
+ // Private keys encrypted at rest (AES-256-CBC, same scheme as API keys).
3
+ // All real transactions require explicit user confirmation.
4
+ // Read-only operations (balance, token list) never need confirmation.
5
+ import { registerTool } from './index.js';
6
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { homedir } from 'node:os';
9
+ import { createCipheriv, createDecipheriv, randomBytes, createHash } from 'node:crypto';
10
+ // ── Constants ──
11
+ const KBOT_DIR = join(homedir(), '.kbot');
12
+ const WALLET_PATH = join(KBOT_DIR, 'wallet.json');
13
+ const SOL_RPC = 'https://api.mainnet-beta.solana.com';
14
+ const JUPITER_QUOTE = 'https://quote-api.jup.ag/v6/quote';
15
+ const JUPITER_SWAP = 'https://quote-api.jup.ag/v6/swap';
16
+ const JUPITER_PRICE = 'https://api.jup.ag/price/v2';
17
+ const JUPITER_TOKENS = 'https://token.jup.ag/strict';
18
+ const SOL_MINT = 'So11111111111111111111111111111111111111112';
19
+ const USDC_MINT = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
20
+ const LAMPORTS_PER_SOL = 1_000_000_000;
21
+ // ── Encryption (matches auth.ts pattern) ──
22
+ function deriveKey() {
23
+ const machineId = `${homedir()}:${process.env.USER || 'kbot'}:${process.arch}:wallet`;
24
+ return createHash('sha256').update(machineId).digest();
25
+ }
26
+ function encrypt(plaintext) {
27
+ const key = deriveKey();
28
+ const iv = randomBytes(16);
29
+ const cipher = createCipheriv('aes-256-cbc', key, iv);
30
+ let encrypted = cipher.update(plaintext, 'utf-8', 'base64');
31
+ encrypted += cipher.final('base64');
32
+ return `enc:${iv.toString('base64')}:${encrypted}`;
33
+ }
34
+ function decrypt(encrypted) {
35
+ if (!encrypted.startsWith('enc:'))
36
+ return encrypted;
37
+ const parts = encrypted.split(':');
38
+ if (parts.length !== 3)
39
+ return encrypted;
40
+ const key = deriveKey();
41
+ const iv = Buffer.from(parts[1], 'base64');
42
+ const decipher = createDecipheriv('aes-256-cbc', key, iv);
43
+ let decrypted = decipher.update(parts[2], 'base64', 'utf-8');
44
+ decrypted += decipher.final('utf-8');
45
+ return decrypted;
46
+ }
47
+ function loadStore() {
48
+ if (!existsSync(WALLET_PATH))
49
+ return { active: '', wallets: [] };
50
+ try {
51
+ const raw = JSON.parse(readFileSync(WALLET_PATH, 'utf-8'));
52
+ // Migration: old single-wallet format → multi-wallet
53
+ if (raw.publicKey && !raw.wallets) {
54
+ return { active: raw.label || 'default', wallets: [raw] };
55
+ }
56
+ return raw;
57
+ }
58
+ catch {
59
+ return { active: '', wallets: [] };
60
+ }
61
+ }
62
+ function saveStore(store) {
63
+ if (!existsSync(KBOT_DIR))
64
+ mkdirSync(KBOT_DIR, { recursive: true });
65
+ writeFileSync(WALLET_PATH, JSON.stringify(store, null, 2));
66
+ chmodSync(WALLET_PATH, 0o600);
67
+ }
68
+ function loadWallet() {
69
+ const store = loadStore();
70
+ if (!store.wallets.length)
71
+ return null;
72
+ return store.wallets.find(w => w.label === store.active) || store.wallets[0];
73
+ }
74
+ function saveWallet(w) {
75
+ const store = loadStore();
76
+ const idx = store.wallets.findIndex(x => x.label === w.label);
77
+ if (idx >= 0)
78
+ store.wallets[idx] = w;
79
+ else
80
+ store.wallets.push(w);
81
+ if (!store.active)
82
+ store.active = w.label;
83
+ saveStore(store);
84
+ }
85
+ // ── Solana Helpers ──
86
+ async function rpcCall(method, params) {
87
+ const res = await fetch(SOL_RPC, {
88
+ method: 'POST',
89
+ headers: { 'Content-Type': 'application/json' },
90
+ body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }),
91
+ signal: AbortSignal.timeout(15_000),
92
+ });
93
+ const data = await res.json();
94
+ if (data.error)
95
+ throw new Error(`RPC error: ${data.error.message}`);
96
+ return data.result;
97
+ }
98
+ async function getSolBalance(pubkey) {
99
+ const result = await rpcCall('getBalance', [pubkey]);
100
+ return (result?.value ?? 0) / LAMPORTS_PER_SOL;
101
+ }
102
+ async function getTokenAccounts(pubkey) {
103
+ const result = await rpcCall('getTokenAccountsByOwner', [
104
+ pubkey,
105
+ { programId: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA' },
106
+ { encoding: 'jsonParsed' },
107
+ ]);
108
+ return (result?.value ?? []).map((a) => ({
109
+ mint: a.account.data.parsed.info.mint,
110
+ amount: Number(a.account.data.parsed.info.tokenAmount.uiAmount),
111
+ decimals: a.account.data.parsed.info.tokenAmount.decimals,
112
+ })).filter((t) => t.amount > 0);
113
+ }
114
+ function fmt(n, d = 2) {
115
+ return n.toLocaleString('en-US', { minimumFractionDigits: d, maximumFractionDigits: d });
116
+ }
117
+ // ── Token Resolution ──
118
+ const COMMON_TOKENS = {
119
+ sol: SOL_MINT,
120
+ usdc: USDC_MINT,
121
+ usdt: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB',
122
+ bonk: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263',
123
+ jup: 'JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN',
124
+ ray: '4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R',
125
+ wif: 'EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm',
126
+ jto: 'jtojtomepa8beP8AuQc6eXt5FriJwfFMwQx2v2f9mCL',
127
+ pyth: 'HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3',
128
+ render: 'rndrizKT3MK1iimdxRdWabcF7Zg7AR5T4nud4EkHBof',
129
+ wen: 'WENWENvqqNya429ubCdR81ZmD69brwQaaBYY6p3LCpk',
130
+ w: '85VBFQZC9TZkfaptBWjvUw7YbZjy52A6mjtPGjstQAmQ',
131
+ };
132
+ async function resolveTokenMint(symbol) {
133
+ const lower = symbol.toLowerCase();
134
+ if (COMMON_TOKENS[lower]) {
135
+ return { mint: COMMON_TOKENS[lower], symbol: lower.toUpperCase(), name: lower };
136
+ }
137
+ // Search Jupiter token list
138
+ try {
139
+ const tokens = await fetch(JUPITER_TOKENS, { signal: AbortSignal.timeout(8_000) }).then(r => r.json());
140
+ const match = tokens.find((t) => t.symbol?.toLowerCase() === lower || t.name?.toLowerCase() === lower);
141
+ if (match)
142
+ return { mint: match.address, symbol: match.symbol, name: match.name };
143
+ }
144
+ catch { /* fallback */ }
145
+ // If it looks like a mint address, use it directly
146
+ if (symbol.length >= 32 && symbol.length <= 44) {
147
+ return { mint: symbol, symbol: 'UNKNOWN', name: symbol.slice(0, 8) + '...' };
148
+ }
149
+ return null;
150
+ }
151
+ // ── Register Tools ──
152
+ export function registerWalletTools() {
153
+ // ─── Multi-Wallet Management ───
154
+ registerTool({
155
+ name: 'wallet_list',
156
+ description: 'List all kbot wallets. Shows which wallet is active, labels, addresses, and balances.',
157
+ parameters: {},
158
+ tier: 'free',
159
+ timeout: 20_000,
160
+ async execute() {
161
+ const store = loadStore();
162
+ if (!store.wallets.length)
163
+ return 'No wallets. Use `wallet_setup create` to get started.';
164
+ const lines = ['## kbot Wallets', ''];
165
+ for (const w of store.wallets) {
166
+ const isActive = w.label === store.active ? ' **(active)**' : '';
167
+ let balance = '?';
168
+ try {
169
+ const sol = await getSolBalance(w.publicKey);
170
+ balance = `${fmt(sol, 4)} SOL`;
171
+ }
172
+ catch { /* skip */ }
173
+ lines.push(`### ${w.label}${isActive}`);
174
+ lines.push(`- Address: \`${w.publicKey.slice(0, 6)}...${w.publicKey.slice(-4)}\``);
175
+ lines.push(`- Balance: ${balance}`);
176
+ lines.push(`- Max Tx: ${w.maxTxSol} SOL`);
177
+ lines.push(`- Created: ${w.createdAt.split('T')[0]}`);
178
+ lines.push('');
179
+ }
180
+ return lines.join('\n');
181
+ },
182
+ });
183
+ registerTool({
184
+ name: 'wallet_switch',
185
+ description: 'Switch the active wallet by label. All subsequent operations will use this wallet.',
186
+ parameters: {
187
+ label: { type: 'string', description: 'Label of the wallet to activate', required: true },
188
+ },
189
+ tier: 'free',
190
+ async execute(args) {
191
+ const label = String(args.label);
192
+ const store = loadStore();
193
+ const wallet = store.wallets.find(w => w.label === label);
194
+ if (!wallet)
195
+ return `No wallet with label "${label}". Available: ${store.wallets.map(w => w.label).join(', ')}`;
196
+ store.active = label;
197
+ saveStore(store);
198
+ return `Active wallet switched to **${label}** (\`${wallet.publicKey.slice(0, 6)}...${wallet.publicKey.slice(-4)}\`).`;
199
+ },
200
+ });
201
+ registerTool({
202
+ name: 'wallet_send',
203
+ description: 'Send SOL from your active wallet to another address. Requires confirmation and respects max transaction limit.',
204
+ parameters: {
205
+ to: { type: 'string', description: 'Recipient Solana address', required: true },
206
+ amount: { type: 'number', description: 'Amount of SOL to send', required: true },
207
+ confirmed: { type: 'string', description: 'Must be "yes" to execute. Safety gate.' },
208
+ },
209
+ tier: 'free',
210
+ timeout: 30_000,
211
+ async execute(args) {
212
+ const wallet = loadWallet();
213
+ if (!wallet)
214
+ return '**BLOCKED**: No wallet configured.';
215
+ if (String(args.confirmed).toLowerCase() !== 'yes') {
216
+ return `**SAFETY GATE**: This will send ${args.amount} SOL to ${String(args.to).slice(0, 8)}... — pass \`confirmed: "yes"\` to execute.`;
217
+ }
218
+ const amount = Number(args.amount);
219
+ if (!amount || amount <= 0)
220
+ return 'Amount must be positive.';
221
+ if (amount > wallet.maxTxSol) {
222
+ return `**RISK LIMIT**: ${amount} SOL exceeds your max transaction limit of ${wallet.maxTxSol} SOL.`;
223
+ }
224
+ const balance = await getSolBalance(wallet.publicKey);
225
+ if (amount > balance - 0.005)
226
+ return `Insufficient balance. Have ${fmt(balance, 4)} SOL (need ${fmt(amount + 0.005, 4)} with fee).`;
227
+ // Build transfer instruction via Solana RPC
228
+ const lamports = Math.round(amount * LAMPORTS_PER_SOL);
229
+ const { blockhash } = await rpcCall('getLatestBlockhash', [{ commitment: 'finalized' }]);
230
+ // For a simple SOL transfer, we need to construct a proper transaction
231
+ // This is complex without @solana/web3.js, so we note the limitation
232
+ return [
233
+ '## SOL Transfer Prepared',
234
+ '',
235
+ `**From**: \`${wallet.publicKey.slice(0, 6)}...${wallet.publicKey.slice(-4)}\``,
236
+ `**To**: \`${String(args.to).slice(0, 6)}...${String(args.to).slice(-4)}\``,
237
+ `**Amount**: ${fmt(amount, 4)} SOL (${lamports.toLocaleString()} lamports)`,
238
+ `**Blockhash**: ${blockhash}`,
239
+ '',
240
+ '*Note: Raw SOL transfers require @solana/web3.js for transaction serialization. Use `swap_execute` for token swaps via Jupiter, which handles serialization server-side.*',
241
+ ].join('\n');
242
+ },
243
+ });
244
+ // ─── Wallet Setup ───
245
+ registerTool({
246
+ name: 'wallet_setup',
247
+ description: 'Create or import a Solana wallet for kbot. Private key is encrypted at rest (AES-256-CBC). Use "create" for a new wallet or "import" with an existing private key.',
248
+ parameters: {
249
+ action: { type: 'string', description: '"create" for new wallet, "import" to import existing, "info" to show current wallet, "remove" to delete', required: true },
250
+ privateKey: { type: 'string', description: 'Base58 private key (only for import action). NEVER log or display this.' },
251
+ label: { type: 'string', description: 'Friendly label for this wallet (e.g. "trading", "defi")' },
252
+ maxTxSol: { type: 'number', description: 'Max transaction size in SOL (default: 1.0 — safety limit)', default: 1.0 },
253
+ },
254
+ tier: 'free',
255
+ async execute(args) {
256
+ const action = String(args.action).toLowerCase();
257
+ if (action === 'info') {
258
+ const w = loadWallet();
259
+ if (!w)
260
+ return 'No wallet configured. Use `wallet_setup create` or `wallet_setup import`.';
261
+ const sol = await getSolBalance(w.publicKey).catch(() => 0);
262
+ return [
263
+ '## kbot Wallet',
264
+ `**Label**: ${w.label}`,
265
+ `**Address**: \`${w.publicKey}\``,
266
+ `**Balance**: ${fmt(sol, 4)} SOL`,
267
+ `**Max Tx**: ${w.maxTxSol} SOL`,
268
+ `**Confirm All**: ${w.confirmAll ? 'Yes' : 'No'}`,
269
+ `**Created**: ${w.createdAt.split('T')[0]}`,
270
+ '',
271
+ `*Private key encrypted at rest (AES-256-CBC)*`,
272
+ ].join('\n');
273
+ }
274
+ if (action === 'remove') {
275
+ const w = loadWallet();
276
+ if (!w)
277
+ return 'No wallet to remove.';
278
+ const { unlinkSync } = await import('node:fs');
279
+ unlinkSync(WALLET_PATH);
280
+ return `Wallet "${w.label}" removed. Private key deleted from disk.`;
281
+ }
282
+ if (action === 'create') {
283
+ // Generate a new Ed25519 keypair using Node crypto
284
+ const { generateKeyPairSync } = await import('node:crypto');
285
+ const { publicKey, privateKey } = generateKeyPairSync('ed25519');
286
+ // Export raw bytes
287
+ const pubRaw = publicKey.export({ type: 'spki', format: 'der' });
288
+ const privRaw = privateKey.export({ type: 'pkcs8', format: 'der' });
289
+ // Ed25519 SPKI: last 32 bytes are the public key
290
+ const pubBytes = pubRaw.subarray(pubRaw.length - 32);
291
+ // Ed25519 PKCS8: bytes 16-48 are the private key seed
292
+ const privBytes = privRaw.subarray(16, 48);
293
+ // Base58 encode
294
+ const bs58Encode = (buf) => {
295
+ const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
296
+ let num = BigInt('0x' + buf.toString('hex'));
297
+ let str = '';
298
+ while (num > 0n) {
299
+ str = ALPHABET[Number(num % 58n)] + str;
300
+ num /= 58n;
301
+ }
302
+ for (const b of buf) {
303
+ if (b === 0)
304
+ str = '1' + str;
305
+ else
306
+ break;
307
+ }
308
+ return str;
309
+ };
310
+ // Solana keypair is 64 bytes: [32 seed bytes][32 public key bytes]
311
+ const fullKeypair = Buffer.concat([privBytes, pubBytes]);
312
+ const pubkey = bs58Encode(pubBytes);
313
+ const privkey = bs58Encode(fullKeypair);
314
+ const wallet = {
315
+ publicKey: pubkey,
316
+ encryptedPrivateKey: encrypt(privkey),
317
+ label: String(args.label || 'default'),
318
+ createdAt: new Date().toISOString(),
319
+ maxTxSol: Number(args.maxTxSol) || 1.0,
320
+ confirmAll: true,
321
+ };
322
+ saveWallet(wallet);
323
+ return [
324
+ '## New Solana Wallet Created',
325
+ '',
326
+ `**Address**: \`${pubkey}\``,
327
+ `**Label**: ${wallet.label}`,
328
+ `**Max Tx**: ${wallet.maxTxSol} SOL`,
329
+ '',
330
+ '**IMPORTANT**: Fund this wallet by sending SOL to the address above.',
331
+ 'Private key is encrypted and stored in `~/.kbot/wallet.json` (chmod 600).',
332
+ 'Back up your private key separately — if you lose `~/.kbot/wallet.json`, funds are unrecoverable.',
333
+ ].join('\n');
334
+ }
335
+ if (action === 'import') {
336
+ const pk = String(args.privateKey || '');
337
+ if (!pk || pk.length < 32)
338
+ return 'Error: provide a valid base58 private key for import.';
339
+ // Decode base58 to get public key (last 32 bytes of 64-byte keypair)
340
+ const bs58Decode = (str) => {
341
+ const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
342
+ let num = 0n;
343
+ for (const c of str) {
344
+ num = num * 58n + BigInt(ALPHABET.indexOf(c));
345
+ }
346
+ const hex = num.toString(16).padStart(128, '0');
347
+ return Buffer.from(hex, 'hex');
348
+ };
349
+ const decoded = bs58Decode(pk);
350
+ const pubBytes = decoded.subarray(32, 64);
351
+ const bs58Encode = (buf) => {
352
+ const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
353
+ let n = BigInt('0x' + buf.toString('hex'));
354
+ let s = '';
355
+ while (n > 0n) {
356
+ s = ALPHABET[Number(n % 58n)] + s;
357
+ n /= 58n;
358
+ }
359
+ for (const b of buf) {
360
+ if (b === 0)
361
+ s = '1' + s;
362
+ else
363
+ break;
364
+ }
365
+ return s;
366
+ };
367
+ const pubkey = bs58Encode(pubBytes);
368
+ const wallet = {
369
+ publicKey: pubkey,
370
+ encryptedPrivateKey: encrypt(pk),
371
+ label: String(args.label || 'imported'),
372
+ createdAt: new Date().toISOString(),
373
+ maxTxSol: Number(args.maxTxSol) || 1.0,
374
+ confirmAll: true,
375
+ };
376
+ saveWallet(wallet);
377
+ return [
378
+ '## Wallet Imported',
379
+ `**Address**: \`${pubkey}\``,
380
+ `**Label**: ${wallet.label}`,
381
+ `**Max Tx**: ${wallet.maxTxSol} SOL`,
382
+ '',
383
+ 'Private key encrypted and stored. Original input NOT saved in plaintext anywhere.',
384
+ ].join('\n');
385
+ }
386
+ return 'Unknown action. Use: create, import, info, or remove.';
387
+ },
388
+ });
389
+ // ─── Token Balances ───
390
+ registerTool({
391
+ name: 'wallet_tokens',
392
+ description: 'Show all token balances in your kbot Solana wallet — SOL + SPL tokens with USD values.',
393
+ parameters: {},
394
+ tier: 'free',
395
+ timeout: 20_000,
396
+ async execute() {
397
+ const w = loadWallet();
398
+ if (!w)
399
+ return 'No wallet configured. Use `wallet_setup create` first.';
400
+ const [sol, tokens] = await Promise.all([
401
+ getSolBalance(w.publicKey),
402
+ getTokenAccounts(w.publicKey),
403
+ ]);
404
+ // Get prices for all tokens + SOL
405
+ const mints = [SOL_MINT, ...tokens.map(t => t.mint)];
406
+ let prices = {};
407
+ try {
408
+ const ids = mints.join(',');
409
+ const priceData = await fetch(`${JUPITER_PRICE}?ids=${ids}`, {
410
+ signal: AbortSignal.timeout(10_000),
411
+ }).then(r => r.json());
412
+ for (const [mint, info] of Object.entries(priceData.data || {})) {
413
+ prices[mint] = Number(info.price) || 0;
414
+ }
415
+ }
416
+ catch { /* prices will be 0 */ }
417
+ const solPrice = prices[SOL_MINT] || 0;
418
+ const solValue = sol * solPrice;
419
+ const lines = [
420
+ `## Wallet: ${w.label}`,
421
+ `**Address**: \`${w.publicKey.slice(0, 6)}...${w.publicKey.slice(-4)}\``,
422
+ '',
423
+ '| Token | Balance | USD Value |',
424
+ '|-------|---------|-----------|',
425
+ `| SOL | ${fmt(sol, 4)} | $${fmt(solValue)} |`,
426
+ ];
427
+ let totalValue = solValue;
428
+ for (const t of tokens) {
429
+ const price = prices[t.mint] || 0;
430
+ const value = t.amount * price;
431
+ totalValue += value;
432
+ const symbol = t.mint.slice(0, 6) + '...';
433
+ lines.push(`| ${symbol} | ${fmt(t.amount, 4)} | $${fmt(value)} |`);
434
+ }
435
+ lines.push('', `**Total Portfolio Value**: $${fmt(totalValue)}`);
436
+ return lines.join('\n');
437
+ },
438
+ });
439
+ // ─── Jupiter Swap Quote ───
440
+ registerTool({
441
+ name: 'swap_quote',
442
+ description: 'Get a swap quote from Jupiter DEX (Solana). Shows price, route, slippage, and fees BEFORE executing. No wallet needed — read-only.',
443
+ parameters: {
444
+ from: { type: 'string', description: 'Token to sell (e.g. "SOL", "USDC", or mint address)', required: true },
445
+ to: { type: 'string', description: 'Token to buy (e.g. "SOL", "USDC", "BONK", or mint address)', required: true },
446
+ amount: { type: 'number', description: 'Amount of "from" token to swap', required: true },
447
+ slippage: { type: 'number', description: 'Max slippage in basis points (default: 50 = 0.5%)', default: 50 },
448
+ },
449
+ tier: 'free',
450
+ timeout: 15_000,
451
+ async execute(args) {
452
+ const fromToken = await resolveTokenMint(String(args.from));
453
+ const toToken = await resolveTokenMint(String(args.to));
454
+ if (!fromToken)
455
+ return `Could not resolve token "${args.from}". Try the full mint address.`;
456
+ if (!toToken)
457
+ return `Could not resolve token "${args.to}". Try the full mint address.`;
458
+ const amount = Number(args.amount);
459
+ if (!amount || amount <= 0)
460
+ return 'Amount must be positive.';
461
+ // Determine decimals for input token
462
+ const fromDecimals = fromToken.mint === SOL_MINT ? 9
463
+ : fromToken.mint === USDC_MINT ? 6
464
+ : 6; // default assumption
465
+ const rawAmount = Math.round(amount * (10 ** fromDecimals));
466
+ const slippage = Number(args.slippage) || 50;
467
+ const url = `${JUPITER_QUOTE}?inputMint=${fromToken.mint}&outputMint=${toToken.mint}&amount=${rawAmount}&slippageBps=${slippage}`;
468
+ const res = await fetch(url, { signal: AbortSignal.timeout(10_000) });
469
+ if (!res.ok)
470
+ return `Jupiter API error: ${res.status} ${res.statusText}`;
471
+ const quote = await res.json();
472
+ if (!quote.outAmount)
473
+ return 'No route found for this swap. Try a different pair or smaller amount.';
474
+ const toDecimals = toToken.mint === SOL_MINT ? 9
475
+ : toToken.mint === USDC_MINT ? 6
476
+ : 6;
477
+ const outAmount = Number(quote.outAmount) / (10 ** toDecimals);
478
+ const priceImpact = Number(quote.priceImpactPct || 0);
479
+ const routePlan = quote.routePlan?.map((r) => `${r.swapInfo?.label || 'Unknown'}`).join(' → ') || 'Direct';
480
+ return [
481
+ '## Swap Quote (Jupiter)',
482
+ '',
483
+ `**Sell**: ${fmt(amount, 4)} ${fromToken.symbol}`,
484
+ `**Buy**: ${fmt(outAmount, 6)} ${toToken.symbol}`,
485
+ `**Rate**: 1 ${fromToken.symbol} = ${fmt(outAmount / amount, 6)} ${toToken.symbol}`,
486
+ `**Price Impact**: ${fmt(priceImpact * 100, 3)}%`,
487
+ `**Slippage Tolerance**: ${slippage / 100}%`,
488
+ `**Route**: ${routePlan}`,
489
+ '',
490
+ `*Quote valid for ~30 seconds. Use \`swap_execute\` to execute.*`,
491
+ ].join('\n');
492
+ },
493
+ });
494
+ // ─── Execute Swap ───
495
+ registerTool({
496
+ name: 'swap_execute',
497
+ description: 'Execute a token swap on Jupiter DEX using your kbot Solana wallet. Gets a fresh quote and signs the transaction. REQUIRES wallet setup and user confirmation. Enforces max transaction limit.',
498
+ parameters: {
499
+ from: { type: 'string', description: 'Token to sell (e.g. "SOL", "USDC")', required: true },
500
+ to: { type: 'string', description: 'Token to buy (e.g. "SOL", "BONK")', required: true },
501
+ amount: { type: 'number', description: 'Amount of "from" token to swap', required: true },
502
+ slippage: { type: 'number', description: 'Max slippage in basis points (default: 50 = 0.5%)', default: 50 },
503
+ confirmed: { type: 'string', description: 'Must be "yes" to execute. Safety gate — always show the quote first.' },
504
+ },
505
+ tier: 'free',
506
+ timeout: 60_000,
507
+ async execute(args) {
508
+ const wallet = loadWallet();
509
+ if (!wallet)
510
+ return '**BLOCKED**: No wallet configured. Run `wallet_setup create` or `wallet_setup import` first.';
511
+ if (String(args.confirmed).toLowerCase() !== 'yes') {
512
+ return '**SAFETY GATE**: You must pass `confirmed: "yes"` to execute a real swap. Get a quote with `swap_quote` first, then confirm.';
513
+ }
514
+ const fromToken = await resolveTokenMint(String(args.from));
515
+ const toToken = await resolveTokenMint(String(args.to));
516
+ if (!fromToken)
517
+ return `Could not resolve token "${args.from}".`;
518
+ if (!toToken)
519
+ return `Could not resolve token "${args.to}".`;
520
+ const amount = Number(args.amount);
521
+ if (!amount || amount <= 0)
522
+ return 'Amount must be positive.';
523
+ // Enforce max transaction limit
524
+ if (fromToken.mint === SOL_MINT && amount > wallet.maxTxSol) {
525
+ return `**RISK LIMIT**: ${amount} SOL exceeds your max transaction limit of ${wallet.maxTxSol} SOL. Update with \`wallet_setup\` to increase.`;
526
+ }
527
+ // For non-SOL tokens, check SOL equivalent value
528
+ if (fromToken.mint !== SOL_MINT) {
529
+ try {
530
+ const priceData = await fetch(`${JUPITER_PRICE}?ids=${fromToken.mint},${SOL_MINT}`, {
531
+ signal: AbortSignal.timeout(5_000),
532
+ }).then(r => r.json());
533
+ const tokenPrice = Number(priceData.data?.[fromToken.mint]?.price) || 0;
534
+ const solPrice = Number(priceData.data?.[SOL_MINT]?.price) || 1;
535
+ const solEquivalent = (amount * tokenPrice) / solPrice;
536
+ if (solEquivalent > wallet.maxTxSol) {
537
+ return `**RISK LIMIT**: ~${fmt(solEquivalent, 2)} SOL equivalent exceeds your max of ${wallet.maxTxSol} SOL.`;
538
+ }
539
+ }
540
+ catch { /* proceed — can't verify, but other guards exist */ }
541
+ }
542
+ // Get fresh quote
543
+ const fromDecimals = fromToken.mint === SOL_MINT ? 9 : fromToken.mint === USDC_MINT ? 6 : 6;
544
+ const rawAmount = Math.round(amount * (10 ** fromDecimals));
545
+ const slippage = Number(args.slippage) || 50;
546
+ const quoteUrl = `${JUPITER_QUOTE}?inputMint=${fromToken.mint}&outputMint=${toToken.mint}&amount=${rawAmount}&slippageBps=${slippage}`;
547
+ const quoteRes = await fetch(quoteUrl, { signal: AbortSignal.timeout(10_000) });
548
+ if (!quoteRes.ok)
549
+ return `Jupiter quote failed: ${quoteRes.status}`;
550
+ const quote = await quoteRes.json();
551
+ if (!quote.outAmount)
552
+ return 'No route found.';
553
+ // Request swap transaction from Jupiter
554
+ const swapRes = await fetch(JUPITER_SWAP, {
555
+ method: 'POST',
556
+ headers: { 'Content-Type': 'application/json' },
557
+ body: JSON.stringify({
558
+ quoteResponse: quote,
559
+ userPublicKey: wallet.publicKey,
560
+ wrapAndUnwrapSol: true,
561
+ dynamicComputeUnitLimit: true,
562
+ prioritizationFeeLamports: 'auto',
563
+ }),
564
+ signal: AbortSignal.timeout(15_000),
565
+ });
566
+ if (!swapRes.ok) {
567
+ const err = await swapRes.text().catch(() => 'unknown');
568
+ return `Jupiter swap request failed: ${swapRes.status} — ${err}`;
569
+ }
570
+ const swapData = await swapRes.json();
571
+ const swapTransaction = swapData.swapTransaction;
572
+ if (!swapTransaction)
573
+ return 'Jupiter did not return a transaction. Try again.';
574
+ // Decrypt private key and sign
575
+ const privKeyB58 = decrypt(wallet.encryptedPrivateKey);
576
+ // Decode base58 private key
577
+ const bs58Decode = (str) => {
578
+ const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
579
+ let num = 0n;
580
+ for (const c of str) {
581
+ num = num * 58n + BigInt(ALPHABET.indexOf(c));
582
+ }
583
+ const hex = num.toString(16).padStart(128, '0');
584
+ return new Uint8Array(Buffer.from(hex, 'hex'));
585
+ };
586
+ const keypairBytes = bs58Decode(privKeyB58);
587
+ const secretKey = keypairBytes.slice(0, 64); // full 64-byte Solana secret key
588
+ // Decode the versioned transaction
589
+ const txBuf = Buffer.from(swapTransaction, 'base64');
590
+ // Sign using Ed25519
591
+ const { sign } = await import('node:crypto');
592
+ const edPrivKey = await import('node:crypto').then(c => c.createPrivateKey({
593
+ key: Buffer.concat([
594
+ // PKCS8 header for Ed25519
595
+ Buffer.from('302e020100300506032b657004220420', 'hex'),
596
+ Buffer.from(secretKey.slice(0, 32)), // 32-byte seed
597
+ ]),
598
+ format: 'der',
599
+ type: 'pkcs8',
600
+ }));
601
+ const signature = sign(null, txBuf.subarray(txBuf[0] === 0x80 ? 1 : 0), edPrivKey);
602
+ // Inject signature into transaction
603
+ // For versioned transactions, signature goes at offset after the compact array length
604
+ const signedTx = Buffer.concat([
605
+ txBuf.subarray(0, 1), // version/num signatures prefix
606
+ signature,
607
+ txBuf.subarray(1 + 64), // rest of tx after the placeholder signature
608
+ ]);
609
+ // Submit to Solana
610
+ const sendResult = await rpcCall('sendTransaction', [
611
+ signedTx.toString('base64'),
612
+ { encoding: 'base64', skipPreflight: false, preflightCommitment: 'confirmed' },
613
+ ]);
614
+ const toDecimals = toToken.mint === SOL_MINT ? 9 : toToken.mint === USDC_MINT ? 6 : 6;
615
+ const outAmount = Number(quote.outAmount) / (10 ** toDecimals);
616
+ return [
617
+ '## Swap Executed',
618
+ '',
619
+ `**Sold**: ${fmt(amount, 4)} ${fromToken.symbol}`,
620
+ `**Bought**: ~${fmt(outAmount, 6)} ${toToken.symbol}`,
621
+ `**Signature**: \`${sendResult}\``,
622
+ `**Explorer**: https://solscan.io/tx/${sendResult}`,
623
+ '',
624
+ '*Transaction submitted. Check explorer for confirmation status.*',
625
+ ].join('\n');
626
+ },
627
+ });
628
+ // ─── Token Search ───
629
+ registerTool({
630
+ name: 'token_search',
631
+ description: 'Search for Solana tokens by name or symbol. Returns mint address, price, and liquidity. Useful before swapping.',
632
+ parameters: {
633
+ query: { type: 'string', description: 'Token name or symbol to search for', required: true },
634
+ },
635
+ tier: 'free',
636
+ timeout: 10_000,
637
+ async execute(args) {
638
+ const query = String(args.query).toLowerCase();
639
+ const tokens = await fetch(JUPITER_TOKENS, {
640
+ signal: AbortSignal.timeout(8_000),
641
+ }).then(r => r.json());
642
+ const matches = tokens
643
+ .filter((t) => t.symbol?.toLowerCase().includes(query) ||
644
+ t.name?.toLowerCase().includes(query))
645
+ .slice(0, 10);
646
+ if (!matches.length)
647
+ return `No tokens found for "${query}".`;
648
+ const lines = [
649
+ `## Token Search: "${args.query}"`,
650
+ '',
651
+ '| Symbol | Name | Mint |',
652
+ '|--------|------|------|',
653
+ ];
654
+ for (const t of matches) {
655
+ lines.push(`| ${t.symbol} | ${t.name} | \`${t.address.slice(0, 8)}...${t.address.slice(-4)}\` |`);
656
+ }
657
+ lines.push('', '*Use the symbol or mint address with `swap_quote` or `swap_execute`.*');
658
+ return lines.join('\n');
659
+ },
660
+ });
661
+ // ─── Transaction History ───
662
+ registerTool({
663
+ name: 'wallet_history',
664
+ description: 'Show recent transaction history for your kbot Solana wallet.',
665
+ parameters: {
666
+ limit: { type: 'number', description: 'Number of transactions (default: 10, max: 20)', default: 10 },
667
+ },
668
+ tier: 'free',
669
+ timeout: 15_000,
670
+ async execute(args) {
671
+ const w = loadWallet();
672
+ if (!w)
673
+ return 'No wallet configured.';
674
+ const limit = Math.min(Number(args.limit) || 10, 20);
675
+ const result = await rpcCall('getSignaturesForAddress', [
676
+ w.publicKey,
677
+ { limit },
678
+ ]);
679
+ if (!result?.length)
680
+ return 'No transactions found.';
681
+ const lines = [
682
+ `## Recent Transactions`,
683
+ `**Wallet**: \`${w.publicKey.slice(0, 6)}...${w.publicKey.slice(-4)}\``,
684
+ '',
685
+ '| Time | Signature | Status |',
686
+ '|------|-----------|--------|',
687
+ ];
688
+ for (const tx of result) {
689
+ const time = tx.blockTime ? new Date(tx.blockTime * 1000).toISOString().replace('T', ' ').slice(0, 16) : '?';
690
+ const sig = `${tx.signature.slice(0, 8)}...${tx.signature.slice(-4)}`;
691
+ const status = tx.err ? 'Failed' : 'Success';
692
+ lines.push(`| ${time} | [\`${sig}\`](https://solscan.io/tx/${tx.signature}) | ${status} |`);
693
+ }
694
+ return lines.join('\n');
695
+ },
696
+ });
697
+ }
698
+ //# sourceMappingURL=wallet.js.map